response.go 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153
  1. // Package response 统一响应契约(HTTP 恒 200 + body code 信封)。
  2. //
  3. // 本包是"分包铁律第 6 条"的实现:所有接口(含业务错误)HTTP 状态码恒为 200,
  4. // 真实业务状态在 body 的 R.Code 中(成功 200,失败为分段错误码,见包 errors)。
  5. // 该契约源自教务 Go 重构在产验证 + 教学 api-conventions 双侧确证,
  6. // 设计决策见 ../../../../unified/01-architecture/backend-technical-architecture.md §3.1。
  7. //
  8. // 典型使用(Handler 层):
  9. //
  10. // func GetUser(c *fiber.Ctx) error {
  11. // var req GetUserReq
  12. // if !infra.Validate(c, &req) { // 校验失败:包内已回写 body(code=2001) 并返回 false
  13. // return nil
  14. // }
  15. // user, err := svc.Get(ctx, req.ID)
  16. // if err != nil {
  17. // return response.Fail(c, err) // 非 *errors.AppError 会被包成 5001 系统繁忙
  18. // }
  19. // return response.OK(c, user)
  20. // }
  21. package response
  22. import (
  23. "yuansiwei/unified-backend/internal/infra/errors"
  24. "github.com/gofiber/fiber/v2"
  25. )
  26. // R 统一响应信封(所有接口的出参顶层结构)。
  27. //
  28. // 前端约定:一律读 body 而非 HTTP 状态码——
  29. // 成功判断 res.success === true(兼容旧前端 res.success 写法);
  30. // 分页列表读顶层 res.total(统一放顶层,解决旧系统 Success/SuccessPage 两处 total 位置不一致的坑);
  31. // 失败时 res.code 为分段错误码(1xxx/2xxx/3xxx/4xxx/5xxx),res.message 为可直接展示给用户的文案。
  32. //
  33. // 典型 JSON 形态:
  34. //
  35. // // 成功(带分页)
  36. // {"code":200,"message":"ok","data":[...],"total":42,"success":true}
  37. // // 业务失败(注意:HTTP 状态码仍是 200)
  38. // {"code":3001,"message":"记录不存在","success":false}
  39. type R struct {
  40. // Code 业务状态码:200 表示成功;失败为分段错误码(1xxx 认证 / 2xxx 参数 / 3xxx 数据 / 4xxx 依赖 / 5xxx 系统)。
  41. Code int `json:"code"`
  42. // Message 面向用户的错误文案。脱敏铁律:不得包含 SQL 片段、堆栈、密码哈希等敏感信息。
  43. Message string `json:"message"`
  44. // Data 业务数据体(成功时填充;失败时省略)。
  45. Data interface{} `json:"data,omitempty"`
  46. // Total 分页总数,仅在列表分页响应中填充(omitempty 省略非分页场景)。
  47. Total int64 `json:"total,omitempty"`
  48. // Success 成功标志,兼容旧前端 res.success 判断,与 Code 冗余但保留。
  49. Success bool `json:"success"`
  50. }
  51. // OK 写出成功响应(HTTP 200,body code=200,Data 填充业务数据)。
  52. //
  53. // 入参:
  54. // - c *fiber.Ctx:Fiber 请求上下文(提供 c.JSON 写出能力;调用前无需手动设置状态码,本函数内部恒置 200)。
  55. // - data interface{}:业务数据体;允许 nil(无数据体的成功响应,如"操作成功"类接口)。
  56. //
  57. // 出参:
  58. // - error:仅当 JSON 序列化/写出失败时返回(如客户端已断开);业务失败请用 Fail,不要用本函数表达。
  59. //
  60. // 样例:
  61. //
  62. // return response.OK(c, fiber.Map{"user": user, "roles": roles})
  63. func OK(c *fiber.Ctx, data interface{}) error {
  64. return c.Status(fiber.StatusOK).JSON(R{Code: 200, Message: "ok", Data: data, Success: true})
  65. }
  66. // Page 写出成功分页响应(HTTP 200,body code=200,Data 为列表、Total 为总数)。
  67. //
  68. // 入参:
  69. // - c *fiber.Ctx:Fiber 请求上下文。
  70. // - list interface{}:当前页数据切片(通常 []T);空列表请传非 nil 的 `[]T{}`,避免 JSON 输出 null。
  71. // - total int64:符合筛选条件的总记录数(用于前端分页器;查询惯例见架构规格 §3.1"先 pluck 分页 id → 批量取详情"防 N+1)。
  72. //
  73. // 出参:
  74. // - error:同 OK,仅写出层失败时返回。
  75. //
  76. // 样例:
  77. //
  78. // page, size := parsePage(c) // page 默认 1、pageSize 默认 10
  79. // ids, total, err := repo.PagedIDs(ctx, filter, page, size)
  80. // users, err := repo.BatchGet(ctx, ids) // 一次批量取,防 N+1
  81. // return response.Page(c, users, total)
  82. func Page(c *fiber.Ctx, list interface{}, total int64) error {
  83. return c.Status(fiber.StatusOK).JSON(R{Code: 200, Message: "ok", Data: list, Total: total, Success: true})
  84. }
  85. // Fail 写出业务失败响应(HTTP 仍为 200,body code 为业务错误码,Data 省略)。
  86. //
  87. // 入参:
  88. // - c *fiber.Ctx:Fiber 请求上下文。
  89. // - err error:业务错误。约定:
  90. // - *errors.AppError:按其 Code/Msg 原样写出(可预期业务错误,Service 层应返回此类 sentinel);
  91. // - 其它 error:降级为 errors.ErrInternal(code=5001 系统繁忙),原始错误仅用于服务端日志,不进入响应体。
  92. //
  93. // 出参:
  94. // - error:仅写出层失败时返回。
  95. //
  96. // 异常场景:
  97. // - err 为 nil:按内部错误 5001 处理(调用方逻辑错误,应在单测中暴露);
  98. // - AppError.Msg 为空:兜底为"系统繁忙",防止把空文案透传给前端。
  99. //
  100. // 样例:
  101. //
  102. // user, err := svc.Get(ctx, id)
  103. // if err != nil {
  104. // return response.Fail(c, err) // ErrNotFound → {"code":3001,"message":"记录不存在"}
  105. // }
  106. func Fail(c *fiber.Ctx, err error) error {
  107. e, ok := err.(*errors.AppError)
  108. if !ok {
  109. e = errors.ErrInternal // 非业务错误(含 err==nil)一律降级 5001,细节只进日志
  110. }
  111. msg := e.Msg
  112. if msg == "" {
  113. msg = "系统繁忙"
  114. }
  115. return c.Status(fiber.StatusOK).JSON(R{Code: e.Code, Message: msg, Success: false})
  116. }
  117. // ParsePage 解析分页参数(page/pageSize,带默认值与上限钳制)。
  118. //
  119. // 入参:
  120. // - c *fiber.Ctx:Fiber 请求上下文,从 query 中读取 page 与 pageSize。
  121. //
  122. // 出参:
  123. // - page int:页码,从 1 开始;query 缺失或非法(<1)时返回默认值 1。
  124. // - pageSize int:每页条数;query 缺失或非法(<1)时返回默认值 10,超过 200 时钳制为 200(防大查询打满数据库)。
  125. //
  126. // 异常场景:
  127. // - query 值非数字:c.QueryInt 返回 0,本函数按默认值处理(不报错,前端传错参数不产生 5xx)。
  128. //
  129. // 样例:
  130. //
  131. // page, size := response.ParsePage(c)
  132. // // page=0 非法 → 1;pageSize=500 → 200
  133. func ParsePage(c *fiber.Ctx) (page, pageSize int) {
  134. page = c.QueryInt("page", 1)
  135. if page < 1 {
  136. page = 1
  137. }
  138. pageSize = c.QueryInt("pageSize", 10)
  139. if pageSize < 1 {
  140. pageSize = 10
  141. }
  142. if pageSize > 200 {
  143. pageSize = 200
  144. }
  145. return page, pageSize
  146. }