fix: 校验端默认延迟不超过调度上限
This commit is contained in:
@@ -917,6 +917,15 @@
|
|||||||
- 备选方案:占满哈希池;未采用,给 MQTT 登录留槽位。
|
- 备选方案:占满哈希池;未采用,给 MQTT 登录留槽位。
|
||||||
- 影响:导入吞吐上升;冲突不再表现为无行号的 500。
|
- 影响:导入吞吐上升;冲突不再表现为无行号的 500。
|
||||||
|
|
||||||
|
### 复审修复 H-05
|
||||||
|
|
||||||
|
- 日期:2026-09-30
|
||||||
|
- 原条款:PRD F11 最远可定到 365 天后(可配置);审查 #48。
|
||||||
|
- 实际做法:`validateEndpointFields` 增加 `MaxScheduleSeconds`,开通/修改/导入与端侧改默认延迟同一上限(0 ≤ delay ≤ max)。界面 `:max` 在 W-05。
|
||||||
|
- 原因:后台原先只检查 `>= 0`,可设出永远发不出消息的端,极大值还会溢出。
|
||||||
|
- 备选方案:导入超限静默改成上限;未采用,改为行错误让管理员改表。
|
||||||
|
- 影响:超上限返回 400 或 CSV 行错误。
|
||||||
|
|
||||||
## 后台网页 W
|
## 后台网页 W
|
||||||
|
|
||||||
1. **W1–W3 阶段使用内存假数据,不请求真实 `/api/admin`**
|
1. **W1–W3 阶段使用内存假数据,不请求真实 `/api/admin`**
|
||||||
|
|||||||
@@ -233,7 +233,7 @@ func (h *Handler) handleEndpointCreate(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.DefaultDelaySeconds != nil {
|
if req.DefaultDelaySeconds != nil {
|
||||||
delaySec = *req.DefaultDelaySeconds
|
delaySec = *req.DefaultDelaySeconds
|
||||||
}
|
}
|
||||||
if errMsg := validateEndpointFields(req.ID, req.Name, req.Remark, req.LoginPassword, req.TalkPassword, delaySec); errMsg != "" {
|
if errMsg := validateEndpointFields(req.ID, req.Name, req.Remark, req.LoginPassword, req.TalkPassword, delaySec, h.maxScheduleSeconds()); errMsg != "" {
|
||||||
h.audit(actorString(p), "endpoint_create", req.ID, "bad_request", ip)
|
h.audit(actorString(p), "endpoint_create", req.ID, "bad_request", ip)
|
||||||
httpx.WriteError(w, http.StatusBadRequest, "bad_request", errMsg)
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", errMsg)
|
||||||
return
|
return
|
||||||
@@ -341,11 +341,13 @@ func (h *Handler) handleEndpointPatch(w http.ResponseWriter, r *http.Request) {
|
|||||||
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "备注过长")
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "备注过长")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if req.DefaultDelaySeconds != nil && *req.DefaultDelaySeconds < 0 {
|
if req.DefaultDelaySeconds != nil {
|
||||||
|
if msg := validateDelaySeconds(*req.DefaultDelaySeconds, h.maxScheduleSeconds()); msg != "" {
|
||||||
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
|
h.audit(actorString(p), "endpoint_patch", id, "bad_request", ip)
|
||||||
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "默认延迟无效")
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", msg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
hasMeta := req.Name != nil || req.Remark != nil || req.DefaultDelaySeconds != nil
|
hasMeta := req.Name != nil || req.Remark != nil || req.DefaultDelaySeconds != nil
|
||||||
var wasEnabled bool
|
var wasEnabled bool
|
||||||
@@ -614,7 +616,7 @@ func (h *Handler) handleEndpointUnlock(w http.ResponseWriter, r *http.Request) {
|
|||||||
httpx.WriteOK(w, map[string]any{})
|
httpx.WriteOK(w, map[string]any{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateEndpointFields(id, name, remark, loginPW, talkPW string, delaySec int64) string {
|
func validateEndpointFields(id, name, remark, loginPW, talkPW string, delaySec, maxDelaySec int64) string {
|
||||||
if id != "" && !protocol.ValidEndpointID(id) {
|
if id != "" && !protocol.ValidEndpointID(id) {
|
||||||
return "编号不合法"
|
return "编号不合法"
|
||||||
}
|
}
|
||||||
@@ -633,12 +635,26 @@ func validateEndpointFields(id, name, remark, loginPW, talkPW string, delaySec i
|
|||||||
if !protocol.ValidTalkPassword(talkPW) {
|
if !protocol.ValidTalkPassword(talkPW) {
|
||||||
return "对话密码不合法"
|
return "对话密码不合法"
|
||||||
}
|
}
|
||||||
|
if msg := validateDelaySeconds(delaySec, maxDelaySec); msg != "" {
|
||||||
|
return msg
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateDelaySeconds(delaySec, maxDelaySec int64) string {
|
||||||
if delaySec < 0 {
|
if delaySec < 0 {
|
||||||
return "默认延迟无效"
|
return "默认延迟无效"
|
||||||
}
|
}
|
||||||
|
if maxDelaySec > 0 && delaySec > maxDelaySec {
|
||||||
|
return "默认延迟无效"
|
||||||
|
}
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) maxScheduleSeconds() int64 {
|
||||||
|
return int64(h.cfg.Limits.MaxScheduleSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
func writeCSVValidationError(w http.ResponseWriter, errs []csvLineError) {
|
func writeCSVValidationError(w http.ResponseWriter, errs []csvLineError) {
|
||||||
httpx.WriteJSON(w, http.StatusBadRequest, httpx.Envelope{
|
httpx.WriteJSON(w, http.StatusBadRequest, httpx.Envelope{
|
||||||
OK: false,
|
OK: false,
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ func (h *Handler) validateImportCSV(ctx context.Context, raw []byte) ([]importPr
|
|||||||
}
|
}
|
||||||
delaySec = n
|
delaySec = n
|
||||||
}
|
}
|
||||||
if msg := validateEndpointFields(id, name, remark, loginPW, talkPW, delaySec); msg != "" {
|
if msg := validateEndpointFields(id, name, remark, loginPW, talkPW, delaySec, h.maxScheduleSeconds()); msg != "" {
|
||||||
errs = append(errs, csvLineError{Line: line, Reason: msg})
|
errs = append(errs, csvLineError{Line: line, Reason: msg})
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package admin_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCreateRejectsDefaultDelayOverMax(t *testing.T) {
|
||||||
|
_, srv, client, _, _ := setupEndpoints(t)
|
||||||
|
body := `{"id":"dly-c","name":"延迟","login_password":"password1","default_delay_seconds":31536001}`
|
||||||
|
res := postJSON(t, client, srv.URL+"/api/admin/endpoints", body, csrfHeaders())
|
||||||
|
env := decodeEnv(t, res)
|
||||||
|
if res.StatusCode != http.StatusBadRequest {
|
||||||
|
t.Fatalf("want 400 got %d %+v", res.StatusCode, env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPatchRejectsDefaultDelayOverMax(t *testing.T) {
|
||||||
|
_, srv, client, _, _ := setupEndpoints(t)
|
||||||
|
res := postJSON(t, client, srv.URL+"/api/admin/endpoints",
|
||||||
|
`{"id":"dly-p","name":"延迟","login_password":"password1"}`, csrfHeaders())
|
||||||
|
env := decodeEnv(t, res)
|
||||||
|
if res.StatusCode != 200 || !env.OK {
|
||||||
|
t.Fatalf("create: %d %+v", res.StatusCode, env)
|
||||||
|
}
|
||||||
|
res = doReq(t, client, http.MethodPatch, srv.URL+"/api/admin/endpoints/dly-p",
|
||||||
|
`{"default_delay_seconds":31536001}`, csrfHeaders())
|
||||||
|
env = decodeEnv(t, res)
|
||||||
|
if res.StatusCode != http.StatusBadRequest {
|
||||||
|
t.Fatalf("want 400 got %d %+v", res.StatusCode, env)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestImportRejectsDefaultDelayOverMax(t *testing.T) {
|
||||||
|
_, srv, client, _, _ := setupEndpoints(t)
|
||||||
|
csvBody := "" +
|
||||||
|
"id,name,login_password,talk_password,default_delay_seconds,remark\n" +
|
||||||
|
"dly-i,甲,password1,,31536001,\n"
|
||||||
|
res := postImportCSV(t, client, srv.URL, csvBody)
|
||||||
|
raw, _ := io.ReadAll(res.Body)
|
||||||
|
_ = res.Body.Close()
|
||||||
|
if res.StatusCode != http.StatusBadRequest {
|
||||||
|
t.Fatalf("want 400 got %d body=%s", res.StatusCode, raw)
|
||||||
|
}
|
||||||
|
var parsed struct {
|
||||||
|
Data *struct {
|
||||||
|
Errors []struct {
|
||||||
|
Line int `json:"line"`
|
||||||
|
Reason string `json:"reason"`
|
||||||
|
} `json:"errors"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(raw, &parsed); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if parsed.Data == nil || len(parsed.Data.Errors) == 0 {
|
||||||
|
t.Fatalf("want row error, body=%s", raw)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user