| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173 |
- // Package handler 组织域 HTTP 接入层(/org/* 端点,架构规格 §3.2 三层之"Handler",T6)。
- //
- // 分层职责:
- // - 本包(Handler):从 query 绑定 ListQuery → 经 scope.FromFiber 取中间件注入的作用域 →
- // 调 org/service.OrgService(纯业务 + 越权判定)→ 把 *AppError 映射为统一响应信封。
- // **不写业务逻辑、不直接碰 DB**;作用域不进 query 兜底(query 显式 id 仅作"越权判定"输入,
- // 真正的数据过滤由 Repository Scoping 完成——query 缺 id 也安全)。
- // - Service(org/service):机构/校区只读编排 + assertInScope 越权判定(code 1004)。
- // - Repository(org/repository):t_institution / t_campus 作用域数据访问(强制 Scoping)。
- //
- // 端点清单(路由挂载在 T6 main 完成,经 Auth 验签中间件前置——见 cmd/api/main.go):
- // - GET /org/institutions 机构分页列表(keyword/status/page/pageSize 可空;institutionId/campusId 为越权判定输入)
- // - GET /org/institutions/:institutionId 单个机构
- // - GET /org/campuses 校区分页列表(同上)
- // - GET /org/campuses/:campusId 单个校区
- //
- // 契约要点:
- // - HTTP 恒 200,真实状态在 body code(铁律 6;见 infra/response)。
- // - 作用域越界(query 显式 id 与 token 作用域冲突)→ code 1004(前端据此校正 scope)。
- // - 空列表:data.list 恒为非 nil 的 [](response.Page 要求)。
- package handler
- import (
- "strconv"
- "github.com/gofiber/fiber/v2"
- apperrors "yuansiwei/unified-backend/internal/infra/errors"
- "yuansiwei/unified-backend/internal/infra/response"
- "yuansiwei/unified-backend/internal/infra/scope"
- "yuansiwei/unified-backend/internal/org/service"
- )
- // Handler 组织域 HTTP 处理器(启动装配一次,全进程共享)。
- type Handler struct {
- svc *service.OrgService
- }
- // Config Handler 装配参数(T6 main 按此构造)。
- type Config struct {
- // Svc 组织域业务服务(必填)。
- Svc *service.OrgService
- }
- // New 构造组织域 HTTP 处理器。
- //
- // 入参:cfg Config。
- // 出参:
- // - *Handler:就绪处理器。
- // - error:必填依赖缺失(Svc 为 nil,fail-fast——无业务服务不允许挂载端点)。
- func New(cfg Config) (*Handler, error) {
- if cfg.Svc == nil {
- return nil, apperrors.New(apperrors.CodeInternal, "org handler: OrgService 未注入")
- }
- return &Handler{svc: cfg.Svc}, nil
- }
- // scopeFromCtx 取中间件注入的多租户作用域;nil 时按未授权拒绝(受保护路由中间件必注入)。
- func scopeFromCtx(c *fiber.Ctx) (*scope.Scope, error) {
- sc := scope.FromFiber(c)
- if sc == nil {
- return nil, apperrors.ErrUnauth
- }
- return sc, nil
- }
- // bindListQuery 从 query 绑定机构/校区列表查询条件。
- //
- // 入参:c *fiber.Ctx。
- // 出参:service.ListQuery:
- // - institutionId/campusId:query 显式携带(int64,缺失/非法为 0——0 表示未显式指定,交由 Scoping 兜底);
- // - keyword/status:可空字符串;
- // - page/pageSize:经 response.ParsePage 解析(默认 1/10,上限 200)。
- func bindListQuery(c *fiber.Ctx) service.ListQuery {
- page, pageSize := response.ParsePage(c)
- return service.ListQuery{
- InstitutionID: parseID64(c.Query("institutionId")),
- CampusID: parseID64(c.Query("campusId")),
- Keyword: c.Query("keyword"),
- Status: c.Query("status"),
- Page: page,
- PageSize: pageSize,
- }
- }
- // parseID64 把 query 的机构/校区 id 解析为 int64(缺失/非法 → 0,表示"未显式指定",
- // 交由 Repository Scoping 兜底——0 值本身不触发越权判定,也不构成裸查)。
- func parseID64(s string) int64 {
- if s == "" {
- return 0
- }
- v, err := strconv.ParseInt(s, 10, 64)
- if err != nil {
- return 0 // 非法 id 不报错(作用域判定对 0 放行,数据过滤由 Scoping 保证安全)
- }
- return v
- }
- // Institutions 机构分页列表(GET /org/institutions,受保护)。
- //
- // 出参(成功):code 200,data 为 PageData(list 为 []model.Institution)。
- // 出参(失败):code 1004(作用域越界)/ 1001(未登录)/ 5002(DB)。
- func (h *Handler) Institutions(c *fiber.Ctx) error {
- sc, err := scopeFromCtx(c)
- if err != nil {
- return response.Fail(c, err)
- }
- q := bindListQuery(c)
- page, pageSize := q.Page, q.PageSize
- list, total, err := h.svc.ListInstitutions(c.Context(), sc, q)
- if err != nil {
- return response.Fail(c, err)
- }
- return response.Page(c, list, page, pageSize, total)
- }
- // Institution 单个机构(GET /org/institutions/:institutionId,受保护)。
- //
- // 出参(成功):code 200,data 为 model.Institution。
- // 出参(失败):code 1004(越界)/ 3001(不存在或作用域外)/ 1001 / 5002。
- func (h *Handler) Institution(c *fiber.Ctx) error {
- sc, err := scopeFromCtx(c)
- if err != nil {
- return response.Fail(c, err)
- }
- id, perr := strconv.ParseInt(c.Params("institutionId"), 10, 64)
- if perr != nil {
- return response.Fail(c, apperrors.New(apperrors.CodeBadFormat, "institutionId 格式错误"))
- }
- it, err := h.svc.GetInstitution(c.Context(), sc, id)
- if err != nil {
- return response.Fail(c, err)
- }
- return response.OK(c, it)
- }
- // Campuses 校区分页列表(GET /org/campuses,受保护)。
- //
- // 出参(成功):code 200,data 为 PageData(list 为 []model.Campus)。
- // 出参(失败):code 1004 / 1001 / 5002。
- func (h *Handler) Campuses(c *fiber.Ctx) error {
- sc, err := scopeFromCtx(c)
- if err != nil {
- return response.Fail(c, err)
- }
- q := bindListQuery(c)
- page, pageSize := q.Page, q.PageSize
- list, total, err := h.svc.ListCampuses(c.Context(), sc, q)
- if err != nil {
- return response.Fail(c, err)
- }
- return response.Page(c, list, page, pageSize, total)
- }
- // Campus 单个校区(GET /org/campuses/:campusId,受保护)。
- //
- // 出参(成功):code 200,data 为 model.Campus。
- // 出参(失败):code 1004 / 3001 / 1001 / 5002。
- func (h *Handler) Campus(c *fiber.Ctx) error {
- sc, err := scopeFromCtx(c)
- if err != nil {
- return response.Fail(c, err)
- }
- id, perr := strconv.ParseInt(c.Params("campusId"), 10, 64)
- if perr != nil {
- return response.Fail(c, apperrors.New(apperrors.CodeBadFormat, "campusId 格式错误"))
- }
- cp, err := h.svc.GetCampus(c.Context(), sc, id)
- if err != nil {
- return response.Fail(c, err)
- }
- return response.OK(c, cp)
- }
|