// MembershipRepo 成员关系仓储(t_membership,人 ↔ 机构/校区 + 角色,多租户核心,ADR-007)。 // // 平台级访问(同包注释):登录时取"这个用户有哪些机构/校区/角色"是全局查询,不做 scoping。 // 多租户防线在后续业务域 Repository 才启动。 // // 实体跨域引用说明:t_membership 的 GORM 实体定义在 **org 域 model 包**(internal/org/model.Membership, // 与 Institution/Campus/Role 同域)——登录域只"读"它取 role/作用域,不持有实体所有权。 // 本文件故用别名 orgmodel 显式标注这一跨域引用,与本包内 identity/model(User/Identity)区分。 // // 登录两步协议的支撑: // - 第一步 POST /auth/login 验账密后,Service 调 ListByCanonicalUserID 取该用户全部 membership, // 返回给前端展示"机构/校区选择页"; // - 第二步 POST /auth/issue 选定 (institutionId, campusId) 后,Service 调 // GetByUserInstitutionCampus 取对应 membership 的 role,签发 token。 package repository import ( "context" "gorm.io/gorm" orgmodel "yuansiwei/unified-backend/internal/org/model" ) // MembershipRepo 成员关系仓储。 type MembershipRepo struct { db *gorm.DB } // NewMembershipRepo 构造成员关系仓储。 // // 入参:db *gorm.DB:数据库句柄(必填)。 // 出参:*MembershipRepo:就绪仓储。 func NewMembershipRepo(db *gorm.DB) *MembershipRepo { return &MembershipRepo{db: db} } // ListByCanonicalUserID 取某用户全部成员关系(登录第一步返回机构/校区选择列表用)。 // // 入参: // - ctx context.Context:请求上下文。 // - uid string:canonical_user_id(ULID)。 // // 出参: // - []orgmodel.Membership:该用户全部 membership(含 institution_id/campus_id/role/status; // 空切片表示该用户尚无任何机构归属——登录成功后无机构可选,前端提示"请联系管理员分配机构")。 // 注意:返回非 nil 切片(空时为 []orgmodel.Membership{}),JSON 序列化为 [] 而非 null。 // - error:errors.ErrDBError(DB 异常);用户无 membership 不算错误(返回空切片,不 ErrNotFound)。 // // 实现说明:Find 而非 First(一条用户可挂多机构/多角色,多对多);按 created_at 升序 // (登录页展示按最早加入排前,用户习惯先选常用机构)。 func (r *MembershipRepo) ListByCanonicalUserID(ctx context.Context, uid string) ([]orgmodel.Membership, error) { var list []orgmodel.Membership if err := r.db.WithContext(ctx). Where("canonical_user_id = ?", uid). Order("created_at ASC"). Find(&list).Error; err != nil { return nil, mapGormErr(err) } if list == nil { list = []orgmodel.Membership{} // 非 nil:JSON 序列化 [] 而非 null } return list, nil } // GetByUserInstitutionCampus 按 (canonical_user_id, institution_id, campus_id) 取唯一 membership // (登录第二步 /auth/issue 选定作用域后,取该条的 role 签发 token)。 // // 入参: // - ctx context.Context:请求上下文。 // - uid string:canonical_user_id(ULID)。 // - institutionID int64:机构 ID(scope 根;0 表示平台级超管,无机构过滤)。 // - campusID int64:校区 ID(0 表示机构级/不限校区,如超管视角)。 // // 出参: // - *orgmodel.Membership:对应 membership(含 Role,供签发 token 的 claims.Roles)。 // - error: // - errors.ErrNotFound:该用户在此 (机构, 校区) 下无 membership(越权/数据不一致,登录第二步应报 2003 业务规则冲突); // - errors.ErrDBError:DB 异常。 // // 作用域语义(与 org 域 model 注释对齐): // - institution_id=0:平台级(超管跨机构);campus_id=0:机构级/不限校区。 // - UNIQUE(canonical_user_id, campus_id, role) 保证同一 (用户, 校区, 角色) 唯一; // 但同一 (用户, 机构, 校区) 可有多个 role(如某人既是教务又是财务),此时取**最新一条** // (created_at 最大)——多角色场景下登录默认进"最近使用的角色"。 func (r *MembershipRepo) GetByUserInstitutionCampus(ctx context.Context, uid string, institutionID, campusID int64) (*orgmodel.Membership, error) { var m orgmodel.Membership q := r.db.WithContext(ctx). Where("canonical_user_id = ?", uid). Where("institution_id = ?", institutionID). Where("campus_id = ?", campusID) // 同一 (用户, 机构, 校区) 可能多角色,取 created_at 最新的一条(最近使用的角色) if err := q.Order("created_at DESC").First(&m).Error; err != nil { return nil, mapGormErr(err) } return &m, nil }