// 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) }