feat: 任务模板支持预设分类与任务级拦截/允许规则
- db: task_templates 加 category_id(FK task_categories, ON DELETE SET NULL) 与 intercept_rules JSONB(规则快照);带幂等 ALTER 补旧库。 - db/task_templates.go: 用 JSONB 存取规则;PATCH 用 Set 标志区分改/清。 - server: 模板请求体加 category_id + intercept_rules,复用 buildTaskInterceptRules 校验;PATCH 靠原始 key presence 判断是否更新(可清分类为 null)。 - web: 抽共享组件 asset-intercept-rules-editor(NativeSelect 版,避免 Sheet 内 下拉 portal 误关抽屉),创建表单与模板管理器共用;模板管理器新增分类下拉与 规则编辑区,另存为模板带当前分类/规则;应用模板回填分类与规则。
This commit is contained in:
1 parent
d5359e1f04
commit
777730e255
8 files changed
+342
-143
No files matched your search
+61
-17
@@ -1,8 +1,10 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
@@ -14,24 +16,44 @@ const maxTaskTemplateRequestBytes = 512 << 10
|
||||
|
||||
// taskTemplateRequest uses pointers so PATCH can distinguish omitted fields from
|
||||
// explicit empty values. Empty values are still rejected by the DB validator.
|
||||
// category_id / intercept_rules presence is detected via the raw key set (see
|
||||
// decodeTaskTemplateRequest) so a PATCH can clear category to null.
|
||||
type taskTemplateRequest struct {
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Goal *string `json:"goal"`
|
||||
Name *string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
Goal *string `json:"goal"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
InterceptRules []taskInterceptRuleReq `json:"intercept_rules"`
|
||||
}
|
||||
|
||||
func decodeTaskTemplateRequest(w http.ResponseWriter, r *http.Request, req *taskTemplateRequest) bool {
|
||||
// decodeTaskTemplateRequest decodes the body into req and returns the set of
|
||||
// top-level keys present in the JSON (for PATCH presence detection).
|
||||
func decodeTaskTemplateRequest(w http.ResponseWriter, r *http.Request, req *taskTemplateRequest) (map[string]struct{}, bool) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxTaskTemplateRequestBytes)
|
||||
if err := decode(r, req); err != nil {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
var tooLarge *http.MaxBytesError
|
||||
if errors.As(err, &tooLarge) {
|
||||
writeErr(w, http.StatusRequestEntityTooLarge, "请求正文过大")
|
||||
} else {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
}
|
||||
return false
|
||||
return nil, false
|
||||
}
|
||||
return true
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(body, &raw); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return nil, false
|
||||
}
|
||||
if err := json.Unmarshal(body, req); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return nil, false
|
||||
}
|
||||
present := make(map[string]struct{}, len(raw))
|
||||
for k := range raw {
|
||||
present[k] = struct{}{}
|
||||
}
|
||||
return present, true
|
||||
}
|
||||
|
||||
func validateTaskTemplateRequest(req taskTemplateRequest) error {
|
||||
@@ -91,17 +113,24 @@ func (s *Server) pgCreateTaskTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var req taskTemplateRequest
|
||||
if !decodeTaskTemplateRequest(w, r, &req) {
|
||||
if _, ok := decodeTaskTemplateRequest(w, r, &req); !ok {
|
||||
return
|
||||
}
|
||||
if err := validateTaskTemplateRequest(req); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
rules, err := buildTaskInterceptRules(req.InterceptRules)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "拦截/允许规则无效:"+err.Error())
|
||||
return
|
||||
}
|
||||
template, err := pg.CreateTaskTemplate(db.TaskTemplateInput{
|
||||
Name: stringValue(req.Name),
|
||||
Description: stringValue(req.Description),
|
||||
Goal: stringValue(req.Goal),
|
||||
Name: stringValue(req.Name),
|
||||
Description: stringValue(req.Description),
|
||||
Goal: stringValue(req.Goal),
|
||||
CategoryID: req.CategoryID,
|
||||
InterceptRules: rules,
|
||||
})
|
||||
if err != nil {
|
||||
writeTaskTemplateErr(w, err)
|
||||
@@ -121,20 +150,35 @@ func (s *Server) pgUpdateTaskTemplate(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
var req taskTemplateRequest
|
||||
if !decodeTaskTemplateRequest(w, r, &req) {
|
||||
present, ok := decodeTaskTemplateRequest(w, r, &req)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := validateTaskTemplateRequest(req); err != nil {
|
||||
writeErr(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
if req.Name == nil && req.Description == nil && req.Goal == nil {
|
||||
writeErr(w, http.StatusBadRequest, "至少需要提供 name、description 或 goal")
|
||||
_, catPresent := present["category_id"]
|
||||
_, rulesPresent := present["intercept_rules"]
|
||||
if req.Name == nil && req.Description == nil && req.Goal == nil && !catPresent && !rulesPresent {
|
||||
writeErr(w, http.StatusBadRequest, "至少需要提供 name、description、goal、category_id 或 intercept_rules")
|
||||
return
|
||||
}
|
||||
template, err := pg.PatchTaskTemplate(id, db.TaskTemplatePatch{
|
||||
Name: req.Name, Description: req.Description, Goal: req.Goal,
|
||||
})
|
||||
patch := db.TaskTemplatePatch{Name: req.Name, Description: req.Description, Goal: req.Goal}
|
||||
if catPresent {
|
||||
patch.SetCategoryID = true
|
||||
patch.CategoryID = req.CategoryID
|
||||
}
|
||||
if rulesPresent {
|
||||
rules, err := buildTaskInterceptRules(req.InterceptRules)
|
||||
if err != nil {
|
||||
writeErr(w, http.StatusBadRequest, "拦截/允许规则无效:"+err.Error())
|
||||
return
|
||||
}
|
||||
patch.SetInterceptRules = true
|
||||
patch.InterceptRules = rules
|
||||
}
|
||||
template, err := pg.PatchTaskTemplate(id, patch)
|
||||
if err != nil {
|
||||
writeTaskTemplateErr(w, err)
|
||||
return
|
||||
|
||||
Reference in new issue
Block a user