package httpapi import ( "encoding/json" "errors" "log/slog" "net/http" "strconv" "strings" "git.yeij.top/AskaEth/Scoreboard/server/internal/domain" ) // apiError 是可直接映射为统一响应结构的业务错误(见 devdocs/05 第 8 节错误码)。 type apiError struct { status int code string message string detail any } func (e *apiError) Error() string { return e.code + ": " + e.message } func badRequest(message string, detail any) *apiError { return &apiError{status: http.StatusBadRequest, code: "VALIDATION_FAILED", message: message, detail: detail} } func notFound(message string) *apiError { return &apiError{status: http.StatusNotFound, code: "VALIDATION_FAILED", message: message} } func conflict(message string) *apiError { return &apiError{status: http.StatusConflict, code: "CONFLICT", message: message} } func stateInvalid(message string) *apiError { return &apiError{status: http.StatusConflict, code: "STATE_INVALID", message: message} } func internal(err error) *apiError { return &apiError{status: http.StatusInternalServerError, code: "INTERNAL", message: "服务端异常", detail: err.Error()} } func notImplemented(message string) *apiError { return &apiError{status: http.StatusNotImplemented, code: "INTERNAL", message: message} } // handler 是业务处理器:返回 data 时自动包成 {ok:true,data},返回 *apiError 时输出统一错误。 type handler func(w http.ResponseWriter, r *http.Request) (any, error) // handle 统一把业务结果/错误写成 REST 响应。 func (s *server) handle(fn handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { data, err := fn(w, r) if err != nil { if errors.Is(err, errHandled) { return } writeAPIError(w, s.logger, err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "data": data}) } } // handleVoid 用于无返回体的写操作。 func (s *server) handleVoid(fn func(w http.ResponseWriter, r *http.Request) error) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if err := fn(w, r); err != nil { writeAPIError(w, s.logger, err) return } writeJSON(w, http.StatusOK, map[string]any{"ok": true, "data": nil}) } } func writeAPIError(w http.ResponseWriter, logger *slog.Logger, err error) { var api *apiError if !errors.As(err, &api) { api = internal(err) } if api.status >= http.StatusInternalServerError { logger.Error("请求失败", "code", api.code, "err", api.message) } body := map[string]any{ "ok": false, "error": map[string]any{ "code": api.code, "message": api.message, }, } if api.detail != nil { body["error"].(map[string]any)["detail"] = api.detail } writeJSON(w, api.status, body) } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } // decodeJSON 解析请求体;空体视为错误,避免把「没传参数」当成合法写操作。 // 不启用 DisallowUnknownFields:前端会在每个写请求体里注入 requestId, // 服务端按需读取即可,不该因为多一个字段就拒绝整个请求。 func decodeJSON(r *http.Request, dst any) error { dec := json.NewDecoder(r.Body) if err := dec.Decode(dst); err != nil { return badRequest("请求体不是合法 JSON", err.Error()) } return nil } // pageQuery 解析 ?page=&size=,默认第 1 页、每页 50 条,上限 500。 func pageQuery(r *http.Request) (page, size int) { page = intQuery(r, "page", 1) size = intQuery(r, "size", 50) if page < 1 { page = 1 } switch { case size < 1: size = 50 case size > 500: size = 500 } return page, size } func intQuery(r *http.Request, key string, def int) int { raw := strings.TrimSpace(r.URL.Query().Get(key)) if raw == "" { return def } n, err := strconv.Atoi(raw) if err != nil { return def } return n } func int64Query(r *http.Request, key string) (int64, bool) { raw := strings.TrimSpace(r.URL.Query().Get(key)) if raw == "" { return 0, false } n, err := strconv.ParseInt(raw, 10, 64) if err != nil { return 0, false } return n, true } // pathID 解析路由通配符 {id}。 func pathID(r *http.Request, name string) (int64, error) { raw := r.PathValue(name) n, err := strconv.ParseInt(raw, 10, 64) if err != nil || n <= 0 { return 0, badRequest("路径参数 "+name+" 非法", raw) } return n, nil } // pageResult 是分页响应体(见 devdocs/05 第 6 节)。 type pageResult[T any] struct { Items []T `json:"items"` Total int `json:"total"` Page int `json:"page"` Size int `json:"size"` } // mapDomainError 把 domain 引擎的哨兵错误映射为 REST 错误码。 func mapDomainError(err error) error { switch { case err == nil: return nil case errors.Is(err, domain.ErrManualAdjustmentDisabled), errors.Is(err, domain.ErrAdjustmentReasonRequired), errors.Is(err, domain.ErrCompareRequired), errors.Is(err, domain.ErrUnknownBasis), errors.Is(err, domain.ErrUnknownLedgerOp): return stateInvalid(err.Error()) case errors.Is(err, domain.ErrProfileNotFound): return badRequest("计分通道引用的积分模板不存在", err.Error()) case errors.Is(err, domain.ErrLedgerAlreadyVoided): return stateInvalid("该积分流水已作废,不能重复作废") case errors.Is(err, domain.ErrLedgerNotFound): return notFound("积分流水不存在") default: return internal(err) } }