handler.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. // Package handler 组织域 HTTP 接入层(/org/* 端点,架构规格 §3.2 三层之"Handler",T6)。
  2. //
  3. // 分层职责:
  4. // - 本包(Handler):从 query 绑定 ListQuery → 经 scope.FromFiber 取中间件注入的作用域 →
  5. // 调 org/service.OrgService(纯业务 + 越权判定)→ 把 *AppError 映射为统一响应信封。
  6. // **不写业务逻辑、不直接碰 DB**;作用域不进 query 兜底(query 显式 id 仅作"越权判定"输入,
  7. // 真正的数据过滤由 Repository Scoping 完成——query 缺 id 也安全)。
  8. // - Service(org/service):机构/校区只读编排 + assertInScope 越权判定(code 1004)。
  9. // - Repository(org/repository):t_institution / t_campus 作用域数据访问(强制 Scoping)。
  10. //
  11. // 端点清单(路由挂载在 T6 main 完成,经 Auth 验签中间件前置——见 cmd/api/main.go):
  12. // - GET /org/institutions 机构分页列表(keyword/status/page/pageSize 可空;institutionId/campusId 为越权判定输入)
  13. // - GET /org/institutions/:institutionId 单个机构
  14. // - GET /org/campuses 校区分页列表(同上)
  15. // - GET /org/campuses/:campusId 单个校区
  16. //
  17. // 契约要点:
  18. // - HTTP 恒 200,真实状态在 body code(铁律 6;见 infra/response)。
  19. // - 作用域越界(query 显式 id 与 token 作用域冲突)→ code 1004(前端据此校正 scope)。
  20. // - 空列表:data.list 恒为非 nil 的 [](response.Page 要求)。
  21. package handler
  22. import (
  23. "strconv"
  24. "github.com/gofiber/fiber/v2"
  25. apperrors "yuansiwei/unified-backend/internal/infra/errors"
  26. "yuansiwei/unified-backend/internal/infra/response"
  27. "yuansiwei/unified-backend/internal/infra/scope"
  28. "yuansiwei/unified-backend/internal/org/service"
  29. )
  30. // Handler 组织域 HTTP 处理器(启动装配一次,全进程共享)。
  31. type Handler struct {
  32. svc *service.OrgService
  33. }
  34. // Config Handler 装配参数(T6 main 按此构造)。
  35. type Config struct {
  36. // Svc 组织域业务服务(必填)。
  37. Svc *service.OrgService
  38. }
  39. // New 构造组织域 HTTP 处理器。
  40. //
  41. // 入参:cfg Config。
  42. // 出参:
  43. // - *Handler:就绪处理器。
  44. // - error:必填依赖缺失(Svc 为 nil,fail-fast——无业务服务不允许挂载端点)。
  45. func New(cfg Config) (*Handler, error) {
  46. if cfg.Svc == nil {
  47. return nil, apperrors.New(apperrors.CodeInternal, "org handler: OrgService 未注入")
  48. }
  49. return &Handler{svc: cfg.Svc}, nil
  50. }
  51. // scopeFromCtx 取中间件注入的多租户作用域;nil 时按未授权拒绝(受保护路由中间件必注入)。
  52. func scopeFromCtx(c *fiber.Ctx) (*scope.Scope, error) {
  53. sc := scope.FromFiber(c)
  54. if sc == nil {
  55. return nil, apperrors.ErrUnauth
  56. }
  57. return sc, nil
  58. }
  59. // bindListQuery 从 query 绑定机构/校区列表查询条件。
  60. //
  61. // 入参:c *fiber.Ctx。
  62. // 出参:service.ListQuery:
  63. // - institutionId/campusId:query 显式携带(int64,缺失/非法为 0——0 表示未显式指定,交由 Scoping 兜底);
  64. // - keyword/status:可空字符串;
  65. // - page/pageSize:经 response.ParsePage 解析(默认 1/10,上限 200)。
  66. func bindListQuery(c *fiber.Ctx) service.ListQuery {
  67. page, pageSize := response.ParsePage(c)
  68. return service.ListQuery{
  69. InstitutionID: parseID64(c.Query("institutionId")),
  70. CampusID: parseID64(c.Query("campusId")),
  71. Keyword: c.Query("keyword"),
  72. Status: c.Query("status"),
  73. Page: page,
  74. PageSize: pageSize,
  75. }
  76. }
  77. // parseID64 把 query 的机构/校区 id 解析为 int64(缺失/非法 → 0,表示"未显式指定",
  78. // 交由 Repository Scoping 兜底——0 值本身不触发越权判定,也不构成裸查)。
  79. func parseID64(s string) int64 {
  80. if s == "" {
  81. return 0
  82. }
  83. v, err := strconv.ParseInt(s, 10, 64)
  84. if err != nil {
  85. return 0 // 非法 id 不报错(作用域判定对 0 放行,数据过滤由 Scoping 保证安全)
  86. }
  87. return v
  88. }
  89. // Institutions 机构分页列表(GET /org/institutions,受保护)。
  90. //
  91. // 出参(成功):code 200,data 为 PageData(list 为 []model.Institution)。
  92. // 出参(失败):code 1004(作用域越界)/ 1001(未登录)/ 5002(DB)。
  93. func (h *Handler) Institutions(c *fiber.Ctx) error {
  94. sc, err := scopeFromCtx(c)
  95. if err != nil {
  96. return response.Fail(c, err)
  97. }
  98. q := bindListQuery(c)
  99. page, pageSize := q.Page, q.PageSize
  100. list, total, err := h.svc.ListInstitutions(c.Context(), sc, q)
  101. if err != nil {
  102. return response.Fail(c, err)
  103. }
  104. return response.Page(c, list, page, pageSize, total)
  105. }
  106. // Institution 单个机构(GET /org/institutions/:institutionId,受保护)。
  107. //
  108. // 出参(成功):code 200,data 为 model.Institution。
  109. // 出参(失败):code 1004(越界)/ 3001(不存在或作用域外)/ 1001 / 5002。
  110. func (h *Handler) Institution(c *fiber.Ctx) error {
  111. sc, err := scopeFromCtx(c)
  112. if err != nil {
  113. return response.Fail(c, err)
  114. }
  115. id, perr := strconv.ParseInt(c.Params("institutionId"), 10, 64)
  116. if perr != nil {
  117. return response.Fail(c, apperrors.New(apperrors.CodeBadFormat, "institutionId 格式错误"))
  118. }
  119. it, err := h.svc.GetInstitution(c.Context(), sc, id)
  120. if err != nil {
  121. return response.Fail(c, err)
  122. }
  123. return response.OK(c, it)
  124. }
  125. // Campuses 校区分页列表(GET /org/campuses,受保护)。
  126. //
  127. // 出参(成功):code 200,data 为 PageData(list 为 []model.Campus)。
  128. // 出参(失败):code 1004 / 1001 / 5002。
  129. func (h *Handler) Campuses(c *fiber.Ctx) error {
  130. sc, err := scopeFromCtx(c)
  131. if err != nil {
  132. return response.Fail(c, err)
  133. }
  134. q := bindListQuery(c)
  135. page, pageSize := q.Page, q.PageSize
  136. list, total, err := h.svc.ListCampuses(c.Context(), sc, q)
  137. if err != nil {
  138. return response.Fail(c, err)
  139. }
  140. return response.Page(c, list, page, pageSize, total)
  141. }
  142. // Campus 单个校区(GET /org/campuses/:campusId,受保护)。
  143. //
  144. // 出参(成功):code 200,data 为 model.Campus。
  145. // 出参(失败):code 1004 / 3001 / 1001 / 5002。
  146. func (h *Handler) Campus(c *fiber.Ctx) error {
  147. sc, err := scopeFromCtx(c)
  148. if err != nil {
  149. return response.Fail(c, err)
  150. }
  151. id, perr := strconv.ParseInt(c.Params("campusId"), 10, 64)
  152. if perr != nil {
  153. return response.Fail(c, apperrors.New(apperrors.CodeBadFormat, "campusId 格式错误"))
  154. }
  155. cp, err := h.svc.GetCampus(c.Context(), sc, id)
  156. if err != nil {
  157. return response.Fail(c, err)
  158. }
  159. return response.OK(c, cp)
  160. }