membership_repo.go 4.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
  1. // MembershipRepo 成员关系仓储(t_membership,人 ↔ 机构/校区 + 角色,多租户核心,ADR-007)。
  2. //
  3. // 平台级访问(同包注释):登录时取"这个用户有哪些机构/校区/角色"是全局查询,不做 scoping。
  4. // 多租户防线在后续业务域 Repository 才启动。
  5. //
  6. // 实体跨域引用说明:t_membership 的 GORM 实体定义在 **org 域 model 包**(internal/org/model.Membership,
  7. // 与 Institution/Campus/Role 同域)——登录域只"读"它取 role/作用域,不持有实体所有权。
  8. // 本文件故用别名 orgmodel 显式标注这一跨域引用,与本包内 identity/model(User/Identity)区分。
  9. //
  10. // 登录两步协议的支撑:
  11. // - 第一步 POST /auth/login 验账密后,Service 调 ListByCanonicalUserID 取该用户全部 membership,
  12. // 返回给前端展示"机构/校区选择页";
  13. // - 第二步 POST /auth/issue 选定 (institutionId, campusId) 后,Service 调
  14. // GetByUserInstitutionCampus 取对应 membership 的 role,签发 token。
  15. package repository
  16. import (
  17. "context"
  18. "gorm.io/gorm"
  19. orgmodel "yuansiwei/unified-backend/internal/org/model"
  20. )
  21. // MembershipRepo 成员关系仓储。
  22. type MembershipRepo struct {
  23. db *gorm.DB
  24. }
  25. // NewMembershipRepo 构造成员关系仓储。
  26. //
  27. // 入参:db *gorm.DB:数据库句柄(必填)。
  28. // 出参:*MembershipRepo:就绪仓储。
  29. func NewMembershipRepo(db *gorm.DB) *MembershipRepo {
  30. return &MembershipRepo{db: db}
  31. }
  32. // ListByCanonicalUserID 取某用户全部成员关系(登录第一步返回机构/校区选择列表用)。
  33. //
  34. // 入参:
  35. // - ctx context.Context:请求上下文。
  36. // - uid string:canonical_user_id(ULID)。
  37. //
  38. // 出参:
  39. // - []orgmodel.Membership:该用户全部 membership(含 institution_id/campus_id/role/status;
  40. // 空切片表示该用户尚无任何机构归属——登录成功后无机构可选,前端提示"请联系管理员分配机构")。
  41. // 注意:返回非 nil 切片(空时为 []orgmodel.Membership{}),JSON 序列化为 [] 而非 null。
  42. // - error:errors.ErrDBError(DB 异常);用户无 membership 不算错误(返回空切片,不 ErrNotFound)。
  43. //
  44. // 实现说明:Find 而非 First(一条用户可挂多机构/多角色,多对多);按 created_at 升序
  45. // (登录页展示按最早加入排前,用户习惯先选常用机构)。
  46. func (r *MembershipRepo) ListByCanonicalUserID(ctx context.Context, uid string) ([]orgmodel.Membership, error) {
  47. var list []orgmodel.Membership
  48. if err := r.db.WithContext(ctx).
  49. Where("canonical_user_id = ?", uid).
  50. Order("created_at ASC").
  51. Find(&list).Error; err != nil {
  52. return nil, mapGormErr(err)
  53. }
  54. if list == nil {
  55. list = []orgmodel.Membership{} // 非 nil:JSON 序列化 [] 而非 null
  56. }
  57. return list, nil
  58. }
  59. // GetByUserInstitutionCampus 按 (canonical_user_id, institution_id, campus_id) 取唯一 membership
  60. // (登录第二步 /auth/issue 选定作用域后,取该条的 role 签发 token)。
  61. //
  62. // 入参:
  63. // - ctx context.Context:请求上下文。
  64. // - uid string:canonical_user_id(ULID)。
  65. // - institutionID int64:机构 ID(scope 根;0 表示平台级超管,无机构过滤)。
  66. // - campusID int64:校区 ID(0 表示机构级/不限校区,如超管视角)。
  67. //
  68. // 出参:
  69. // - *orgmodel.Membership:对应 membership(含 Role,供签发 token 的 claims.Roles)。
  70. // - error:
  71. // - errors.ErrNotFound:该用户在此 (机构, 校区) 下无 membership(越权/数据不一致,登录第二步应报 2003 业务规则冲突);
  72. // - errors.ErrDBError:DB 异常。
  73. //
  74. // 作用域语义(与 org 域 model 注释对齐):
  75. // - institution_id=0:平台级(超管跨机构);campus_id=0:机构级/不限校区。
  76. // - UNIQUE(canonical_user_id, campus_id, role) 保证同一 (用户, 校区, 角色) 唯一;
  77. // 但同一 (用户, 机构, 校区) 可有多个 role(如某人既是教务又是财务),此时取**最新一条**
  78. // (created_at 最大)——多角色场景下登录默认进"最近使用的角色"。
  79. func (r *MembershipRepo) GetByUserInstitutionCampus(ctx context.Context, uid string, institutionID, campusID int64) (*orgmodel.Membership, error) {
  80. var m orgmodel.Membership
  81. q := r.db.WithContext(ctx).
  82. Where("canonical_user_id = ?", uid).
  83. Where("institution_id = ?", institutionID).
  84. Where("campus_id = ?", campusID)
  85. // 同一 (用户, 机构, 校区) 可能多角色,取 created_at 最新的一条(最近使用的角色)
  86. if err := q.Order("created_at DESC").First(&m).Error; err != nil {
  87. return nil, mapGormErr(err)
  88. }
  89. return &m, nil
  90. }