package server import ( "errors" "fmt" "net/http" "strings" "unicode/utf8" "github.com/Autumn-27/artex/db" ) 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. type taskTemplateRequest struct { Name *string `json:"name"` Description *string `json:"description"` Goal *string `json:"goal"` } func decodeTaskTemplateRequest(w http.ResponseWriter, r *http.Request, req *taskTemplateRequest) bool { r.Body = http.MaxBytesReader(w, r.Body, maxTaskTemplateRequestBytes) if err := decode(r, req); 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 true } func validateTaskTemplateRequest(req taskTemplateRequest) error { checks := []struct { name string value *string limit int }{ {name: "name", value: req.Name, limit: db.MaxTaskTemplateNameRunes}, {name: "description", value: req.Description, limit: db.MaxTaskTemplateTextRunes}, {name: "goal", value: req.Goal, limit: db.MaxTaskTemplateTextRunes}, } for _, check := range checks { if check.value == nil { continue } value := strings.TrimSpace(*check.value) if check.name == "name" { value = strings.Join(strings.Fields(value), " ") } if utf8.RuneCountInString(value) > check.limit { return fmt.Errorf("%s 最多 %d 个字符", check.name, check.limit) } } return nil } func writeTaskTemplateErr(w http.ResponseWriter, err error) { switch { case errors.Is(err, db.ErrTaskTemplateInvalid): writeErr(w, http.StatusBadRequest, err.Error()) case errors.Is(err, db.ErrTaskTemplateNameConflict): writeErr(w, http.StatusConflict, "模板名称已存在") case errors.Is(err, db.ErrTaskTemplateNotFound): writeErr(w, http.StatusNotFound, "task template not found") default: writeErr(w, http.StatusInternalServerError, err.Error()) } } func (s *Server) pgListTaskTemplates(w http.ResponseWriter, _ *http.Request) { pg := s.pg(w) if pg == nil { return } templates, err := pg.ListTaskTemplates() if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } writeJSON(w, http.StatusOK, map[string]any{"templates": templates}) } func (s *Server) pgCreateTaskTemplate(w http.ResponseWriter, r *http.Request) { pg := s.pg(w) if pg == nil { return } var req taskTemplateRequest if !decodeTaskTemplateRequest(w, r, &req) { return } if err := validateTaskTemplateRequest(req); 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), }) if err != nil { writeTaskTemplateErr(w, err) return } writeJSON(w, http.StatusCreated, template) } func (s *Server) pgUpdateTaskTemplate(w http.ResponseWriter, r *http.Request) { pg := s.pg(w) if pg == nil { return } id, ok := pathInt(r, "id") if !ok { writeErr(w, http.StatusBadRequest, "bad task template id") return } var req taskTemplateRequest if !decodeTaskTemplateRequest(w, r, &req) { 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") return } template, err := pg.PatchTaskTemplate(id, db.TaskTemplatePatch{ Name: req.Name, Description: req.Description, Goal: req.Goal, }) if err != nil { writeTaskTemplateErr(w, err) return } writeJSON(w, http.StatusOK, template) } func (s *Server) pgDeleteTaskTemplate(w http.ResponseWriter, r *http.Request) { pg := s.pg(w) if pg == nil { return } id, ok := pathInt(r, "id") if !ok { writeErr(w, http.StatusBadRequest, "bad task template id") return } deleted, err := pg.DeleteTaskTemplate(id) if err != nil { writeErr(w, http.StatusInternalServerError, err.Error()) return } if !deleted { writeErr(w, http.StatusNotFound, "task template not found") return } writeJSON(w, http.StatusOK, map[string]any{"deleted": id}) } func stringValue(value *string) string { if value == nil { return "" } return *value }