| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122 |
- // Package repository 身份域仓储(登录/用户/成员关系 数据访问,对齐 model 包 T2 实体)。
- //
- // 平台级访问(关键设计决策):
- // - 身份域的数据(t_user/t_identity/t_membership)是**平台级全局数据**,不绑多租户作用域——
- // 登录时"这个手机号是谁"是跨机构的全局查询,不能先有作用域再查(作用域本身由登录结果产生)。
- // 故本域 Repository **不继承 scope.BaseRepo**、不做 scoping 强制过滤,直接用裸 *gorm.DB。
- // - 多租户防线由**消费方**负责:登录成功后 scope 注入(token claims 带 institution_id/campus_id),
- // 后续业务域(academic/teaching)的 Repository 才继承 BaseRepo 做 scoping。
- // - 这是"平台级元数据"的标准处置(对齐 P0-11 收敛规则 7:平台级表不进 scoping)。
- //
- // 错误转换纪律(架构规格 §3.2 三层错误):
- // - Repository 把 gorm.ErrRecordNotFound 转成 apperrors.ErrNotFound(不向上泄漏 ORM 原错);
- // gorm 其它错误(连接/SQL)转成 apperrors.ErrDBError;Service 层只认 sentinel,Handler 映射响应码。
- //
- // 命名说明:本包同时需要标准库 errors(errors.Is 解包判断)与 internal 业务错误包
- // (其包名亦 errors),故 internal 错误包一律用别名 apperrors,标准库用 stderrors。
- //
- // 依赖方向:repository → model(实体)+ apperrors + gorm;不依赖 service/handler(纯数据访问)。
- package repository
- import (
- "context"
- stderrors "errors"
- "fmt"
- "gorm.io/gorm"
- apperrors "yuansiwei/unified-backend/internal/infra/errors"
- "yuansiwei/unified-backend/internal/identity/model"
- )
- // mapGormErr 把 gorm 层错误转成业务 sentinel(Repository 统一出口,禁止泄漏 ORM 原错)。
- //
- // 入参:err *gorm.DB 的 Error 字段(或任意 gorm 调用返回的 error)。
- // 出参:*apperrors.AppError:
- // - gorm.ErrRecordNotFound → apperrors.ErrNotFound(3001 记录不存在);
- // - 其它(连接/SQL/事务)→ apperrors.ErrDBError(5002 数据库异常)。
- //
- // 说明:保留原 err 作 Cause(WithCause),便于日志排障(Cause 不进响应体)。
- // 入参为 nil 时返回 nil(调用方传 nil 视为"无错误",防御性处理)。
- func mapGormErr(err error) *apperrors.AppError {
- switch {
- case err == nil:
- return nil
- case stderrors.Is(err, gorm.ErrRecordNotFound):
- return apperrors.WithCause(apperrors.ErrNotFound, err)
- default:
- return apperrors.WithCause(apperrors.ErrDBError, err)
- }
- }
- // UserRepo 用户主表仓储(t_user,平台级全局唯一,跨机构共享)。
- type UserRepo struct {
- db *gorm.DB
- }
- // NewUserRepo 构造用户仓储。
- //
- // 入参:db *gorm.DB:数据库句柄(infra/db.NewDB 建立的连接池,必填)。
- // 出参:*UserRepo:就绪仓储。
- func NewUserRepo(db *gorm.DB) *UserRepo {
- return &UserRepo{db: db}
- }
- // GetByCanonicalUserID 按 canonical_user_id(ULID)取单个用户。
- //
- // 入参:
- // - ctx context.Context:请求上下文(透传 GORM 取消/超时)。
- // - uid string:canonical_user_id(26 字符 ULID;调用方应已校验合法性,此处不重复校验)。
- //
- // 出参:
- // - *model.User:用户实体(含密码三列——仅登录链路在内存中消费,绝不出 JSON/日志)。
- // - error:
- // - apperrors.ErrNotFound:该 ULID 无用户(登录时"账号不存在"统一文案的来源之一,防枚举);
- // - apperrors.ErrDBError:DB 异常。
- func (r *UserRepo) GetByCanonicalUserID(ctx context.Context, uid string) (*model.User, error) {
- var u model.User
- if err := r.db.WithContext(ctx).
- Where("canonical_user_id = ?", uid).
- First(&u).Error; err != nil {
- return nil, mapGormErr(err)
- }
- return &u, nil
- }
- // UpdatePasswordLazyUpgrade 惰性升级写回(ADR-016 后半段):argon2 重哈希 + hash_version='argon2' + salt 置 NULL。
- //
- // 入参:
- // - ctx context.Context:请求上下文。
- // - uid string:目标用户 canonical_user_id。
- // - newHash string:credential.HashArgon2 产出的 PHC 编码串(salt 已内嵌,故独立 salt 列置 NULL)。
- //
- // 出参:
- // - error:
- // - apperrors.ErrNotFound:用户不存在(调用方 bug——升级必在"已登录该用户"之后,理论上不该发生,单测覆盖);
- // - apperrors.ErrDBError:写失败。
- //
- // 安全纪律:
- // - 升级是"验旧成功"之后的写回,必须与"撤销该用户 token"成对执行(Service 层保证顺序);
- // - salt 置 NULL(非空串):argon2 的盐内嵌 PHC 串,独立列保留空串会让 credential.Check 误判"有盐"。
- //
- // 实现说明:用 Table + Updates(map[string]any) 而非 Model(struct)——map 形式允许显式写 NULL
- // (password_salt=nil),struct 形式 GORM 默认跳过零值字段(NULL 写不进去,会留下旧 salt 脏值)。
- // updated_at 显式 gorm.Expr:GORM 的 Updates 不触发 DB 的 ON UPDATE CURRENT_TIMESTAMP,须手动刷。
- func (r *UserRepo) UpdatePasswordLazyUpgrade(ctx context.Context, uid, newHash string) error {
- res := r.db.WithContext(ctx).
- Table("t_user").
- Where("canonical_user_id = ?", uid).
- Updates(map[string]any{
- "password_hash": newHash,
- "password_salt": nil, // 显式 NULL(argon2 盐内嵌 PHC 串)
- "hash_version": model.HashVersionArgon2,
- "updated_at": gorm.Expr("CURRENT_TIMESTAMP(3)"),
- })
- if res.Error != nil {
- return mapGormErr(res.Error)
- }
- if res.RowsAffected == 0 {
- return apperrors.WithCause(apperrors.ErrNotFound, fmt.Errorf("repository: 惰性升级未命中用户 %s", uid))
- }
- return nil
- }
|