// Package handler 提供 HTTP 入口,只做参数解析 / 序列化 / 错误码映射, //不写业务逻辑。所有业务计算都走 service 层。 package handler import ( "errors" "net/http" "strconv" "github.com/gin-gonic/gin" "github.com/kennethcheng/fuellog-api/pkg/apperror" ) // writeError 把任意 error 渲染成统一 JSON 响应 + 对应 HTTP 状态码。 // 已知 *apperror.Error 用其 HTTPStatus,其它归 500 + INTERNAL。 func writeError(c *gin.Context, err error) { if appErr, ok := apperror.As(err); ok { c.JSON(appErr.HTTPStatus(), gin.H{ "code": appErr.Code, "message": appErr.Message, }) return } // gin-validator 抛的 binding 错误 if isValidationErr(err) { c.JSON(http.StatusBadRequest, gin.H{ "code": apperror.CodeInvalidArg, "message": err.Error(), }) return } c.JSON(http.StatusInternalServerError, gin.H{ "code": apperror.CodeInternal, "message": "服务器开了小差,请稍后再试", }) } func isValidationErr(err error) bool { if err == nil { return false } // 走错误信息启发式判断;真正严谨的做法是导入 validator/types // 引入额外依赖不划算,Phase 2 内部接口稳定后切换 msg := err.Error() return containsAny(msg, []string{"validation", "Field validation", "required", "invalid"}) } func containsAny(s string, subs []string) bool { for _, sub := range subs { if len(sub) <= len(s) { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return true } } } } return false } // parseUint64 解析路径里的 uint64 id。失败返 INVALID_ARGUMENT。 func parseUint64(c *gin.Context, key string) (uint64, error) { raw := c.Param(key) v, err := strconv.ParseUint(raw, 10, 64) if err != nil || v == 0 { return 0, apperror.New(apperror.CodeInvalidArg, "无效的 id: "+raw) } return v, nil } // bindJSON 把请求体解析到 dst;validator 失败自动转为 INVALID_ARGUMENT。 func bindJSON(c *gin.Context, dst any) error { if err := c.ShouldBindJSON(dst); err != nil { return apperror.Wrap(apperror.CodeInvalidArg, "请求体不合法: "+err.Error(), err) } return nil } // sendJSON 统一回写 200。 func sendJSON(c *gin.Context, data any) { c.JSON(http.StatusOK, data) } // errBind 内部错误快速生成函数(减少模板代码)。 func errBind(err error) error { if err == nil { return nil } return apperror.Wrap(apperror.CodeInvalidArg, err.Error(), errors.Unwrap(err)) }