institution_repo.go 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. // 机构仓储(T6):t_institution 的**作用域**数据访问。
  2. //
  3. // 与 identity 平台级仓储的关键差异(架构规格 §3.5):
  4. // - **继承 scope.BaseRepo**:t_institution 是"租户内的机构"维度资源,每次查询必须经
  5. // Scoping 强制过滤——多租户防线的唯一收口点在 BaseRepo.Scoping,本包禁止裸查
  6. // (r.DB.Where(...) 不经过 Scoping = 跨校区/跨机构越权,风险登记册项)。
  7. // - scoping 行为(由 scope 包保证):
  8. // Sc 为 nil → panic(fail-fast,正常请求链路 Auth 中间件保证作用域已注入);
  9. // SuperAdmin=true → 不加机构过滤(平台超管跨机构视角);
  10. // 普通用户 → 强制 institution_id = Sc.InstitutionID(CampusID>0 时再强制 campus_id,
  11. // 对 t_institution 无 campus_id 列,该校验在 service 作用域校验层完成)。
  12. //
  13. // 依赖方向铁律:org repository → infra/scope + org/model + infra/errors(全下层/同域),
  14. // 不依赖 handler/service。
  15. package repository
  16. import (
  17. "context"
  18. stderrors "errors"
  19. "gorm.io/gorm"
  20. "yuansiwei/unified-backend/internal/infra/errors"
  21. appscope "yuansiwei/unified-backend/internal/infra/scope"
  22. "yuansiwei/unified-backend/internal/org/model"
  23. )
  24. // InstitutionRepo 机构仓储(作用域强制过滤)。
  25. type InstitutionRepo struct {
  26. appscope.BaseRepo
  27. }
  28. // NewInstitutionRepo 构造机构仓储(Handler 在拿到 Scope 后调用;db 必填、sc 可为 nil——
  29. // nil 进入 Scoping 会 panic,属预期 fail-fast,见 scope.BaseRepo 注释)。
  30. //
  31. // 注:t_institution 无 campus_id 列,故 CampusScoped=false——Scoping 只加 institution_id
  32. // 过滤(校区级用户查机构列表时,机构按其机构归属展示,不下钻到校区维度)。
  33. func NewInstitutionRepo(db *gorm.DB, sc *appscope.Scope) *InstitutionRepo {
  34. r := &InstitutionRepo{BaseRepo: appscope.NewBaseRepo(db, sc)}
  35. r.CampusScoped = false // 机构级资源:无 campus_id 列
  36. return r
  37. }
  38. // InstitutionFilter 机构列表筛选条件(keyword 模糊匹配机构名/状态精确匹配;均可空)。
  39. type InstitutionFilter struct {
  40. Keyword string
  41. Status string
  42. }
  43. // PagedList 分页取机构列表(强制作用域过滤,防 N+1 的标准两段式由 service 决定——
  44. // 机构表数据量小(23 所业务机构),直接单查询 Count+Find)。
  45. //
  46. // 入参:
  47. // - ctx context.Context:请求上下文(GORM WithContext 传递取消/超时)。
  48. // - f InstitutionFilter:筛选条件(Keyword/Status 为空则不过滤该维度)。
  49. // - page int:页码(从 1 开始;<1 按 1 处理)。
  50. // - pageSize int:每页条数(0/负数按 10 兜底——与 response.ParsePage 同语义)。
  51. //
  52. // 出参:
  53. // - []model.Institution:当前页记录(无记录时返回非 nil 空切片,JSON 序列化为 [] 而非 null)。
  54. // - int64:符合筛选条件的总记录数(分页器"共 N 条")。
  55. // - error:*AppError——ErrDBError(5002) 数据库异常;ErrUnscoped 由 scoping panic 兜底(全局 Recover)。
  56. //
  57. // 样例:
  58. //
  59. // list, total, err := repo.PagedList(ctx, InstitutionFilter{Keyword: "实验"}, 1, 10)
  60. func (r *InstitutionRepo) PagedList(ctx context.Context, f InstitutionFilter, page, pageSize int) ([]model.Institution, int64, error) {
  61. if page < 1 {
  62. page = 1
  63. }
  64. if pageSize < 1 {
  65. pageSize = 10
  66. }
  67. q := r.Scoping(r.WithContext(ctx).Model(&model.Institution{}))
  68. if f.Keyword != "" {
  69. q = q.Where("institution_name LIKE ?", "%"+f.Keyword+"%")
  70. }
  71. if f.Status != "" {
  72. q = q.Where("status = ?", f.Status)
  73. }
  74. var total int64
  75. if err := q.Count(&total).Error; err != nil { // GORM 纪律:Count 收 *int64
  76. return nil, 0, errors.WithCause(errors.ErrDBError, err)
  77. }
  78. list := make([]model.Institution, 0, pageSize)
  79. if err := q.Order("institution_id ASC").
  80. Offset((page - 1) * pageSize).
  81. Limit(pageSize).
  82. Find(&list).Error; err != nil {
  83. return nil, 0, errors.WithCause(errors.ErrDBError, err)
  84. }
  85. return list, total, nil
  86. }
  87. // Get 按机构 ID 取单个机构(强制作用域过滤:非超管取别家机构 → 查不到 → ErrNotFound,
  88. // 由 service 层进一步判定"跨机构"语义,见 OrgService.assertInScope)。
  89. //
  90. // 入参:ctx 请求上下文;institutionID 机构 ID(>0)。
  91. // 出参:*model.Institution(命中);error——ErrNotFound(3001) 不存在或作用域外 / ErrDBError(5002)。
  92. func (r *InstitutionRepo) Get(ctx context.Context, institutionID int64) (*model.Institution, error) {
  93. var it model.Institution
  94. err := r.Scoping(r.WithContext(ctx)).
  95. Where("institution_id = ?", institutionID).
  96. First(&it).Error
  97. if stderrors.Is(err, gorm.ErrRecordNotFound) {
  98. return nil, errors.ErrNotFound
  99. }
  100. if err != nil {
  101. return nil, errors.WithCause(errors.ErrDBError, err)
  102. }
  103. return &it, nil
  104. }