// Package resp 统一 API 响应助手(渐进式契约统一): // - 成功:直接返回业务数据;分页统一 {items, total, page, page_size} 信封 // - 失败:HTTP 状态码语义保持不变,错误体 {detail, code?}(兼容既有前端与测试) // - 内部错误:服务端记录完整 error,客户端只收通用文案,杜绝 DB 错误原文泄漏 // // 新代码禁止直接 c.JSON 错误响应;存量调用点在迭代中逐步迁移到本包。 package resp import ( "errors" "log" "net/http" "github.com/gin-gonic/gin" ) // OK 200:直接返回业务数据 func OK(c *gin.Context, data any) { c.JSON(http.StatusOK, data) } // Created 201:资源创建成功 func Created(c *gin.Context, data any) { c.JSON(http.StatusCreated, data) } // Page 200:统一分页信封 {items, total, page, page_size} func Page(c *gin.Context, items any, total int64, page, pageSize int) { c.JSON(http.StatusOK, gin.H{ "items": items, "total": total, "page": page, "page_size": pageSize, }) } // Message 200:仅含 {message} 的轻量确认(删除/测试类端点) func Message(c *gin.Context, msg string) { c.JSON(http.StatusOK, gin.H{"message": msg}) } // Fail 通用失败:{detail} func Fail(c *gin.Context, status int, detail string) { c.JSON(status, gin.H{"detail": detail}) } // FailCode 失败并携带机器可读错误码:{detail, code} func FailCode(c *gin.Context, status int, code, detail string) { c.JSON(status, gin.H{"detail": detail, "code": code}) } func BadRequest(c *gin.Context, detail string) { Fail(c, http.StatusBadRequest, detail) } func Unauthorized(c *gin.Context, detail string) { Fail(c, http.StatusUnauthorized, detail) } func Forbidden(c *gin.Context, detail string) { Fail(c, http.StatusForbidden, detail) } func NotFound(c *gin.Context, detail string) { Fail(c, http.StatusNotFound, detail) } // InternalErr 500:服务端 log 完整错误,客户端只收通用文案(防 DB/内部错误原文泄漏) // UserFacingError 标记「可以展示给用户」的错误。 // // 典型场景:调用方用错了接口/参数不满足前置条件(例如拿 MySQL 实例去调 // /api/v1/redis/:id/*),这既不是服务端故障也不该泄漏内部细节, // 用 400 + 明确原因比 500 + "服务器内部错误" 更有助于定位。 type UserFacingError struct{ Msg string } func (e *UserFacingError) Error() string { return e.Msg } // UserFacing 构造一个可展示给用户的错误 func UserFacing(msg string) error { return &UserFacingError{Msg: msg} } func InternalErr(c *gin.Context, err error) { var uf *UserFacingError if errors.As(err, &uf) { log.Printf("[resp] bad request on %s %s: %v", c.Request.Method, c.Request.URL.Path, err) c.JSON(http.StatusBadRequest, gin.H{"detail": uf.Msg, "code": "bad_request"}) return } log.Printf("[resp] internal error on %s %s: %v", c.Request.Method, c.Request.URL.Path, err) c.JSON(http.StatusInternalServerError, gin.H{"detail": "服务器内部错误,请稍后重试", "code": "internal"}) } // InternalMsg 500:无底层 error、需要给用户可读文案时使用 func InternalMsg(c *gin.Context, userMsg string) { c.JSON(http.StatusInternalServerError, gin.H{"detail": userMsg}) } // TooManyRequests 429:携带 retry_after_minutes func TooManyRequests(c *gin.Context, detail string, retryAfterMinutes int) { c.JSON(http.StatusTooManyRequests, gin.H{"detail": detail, "retry_after_minutes": retryAfterMinutes}) }