| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193 |
- // Package model 身份域 GORM 领域模型(与 unified/02-data-model/core-model-ddl.sql 逐列对齐)。
- //
- // 设计决策(T1 D1/D2/D3,2026-11-06 用户拍板):
- // - D1 用户类主键 = canonical_user_id string(ULID 26 位,VARCHAR(26),ADR-013)——
- // t_user 主键;t_identity/t_membership/t_guardian_link 中"引用人"的外键列一律 string。
- // - D2 机构/校区主键 = int64 自增(org/model,非 ULID,对齐 scope.Scope 的 int64 作用域字段)。
- // - D3 关系/日志表(t_identity/t_legacy_mapping/t_audit_log)主键 = int64 自增,
- // 持有 canonical_user_id string 外键列。
- //
- // GORM 类型映射纪律(架构规格 §3.6,T2 验收"DDL 逐表与 GORM Model 对齐"的硬要求):
- // - **string 必须带 size:N**:GORM MySQL 驱动对未指定 size 的 string 字段默认建 **longtext**,
- // 与 DDL 的 varchar(N) 严重漂移(T2 对账测试实证:漏 size 导致 26 列全变 longtext)。
- // 每个 string/*string 字段的 size 严格等于 DDL 的 varchar(N) 的 N。
- // - **tinyint(1) 用 bool**:GORM bool → tinyint(1);int64 会建 bigint(与 DDL 不符)。
- // - **int(11) 用 int32**:GORM int32 → int;int64 会建 bigint(legacy_*_user_id 列对齐 DDL int)。
- // - **tinyint(非 1)用 int8**:GORM int8 → tinyint(match_confidence 置信度列)。
- // - **JSON 列用 string 直存 → longtext**:对齐 task.Payload 纪律,避免 GORM 自定义类型序列化坑
- // (DDL t_audit_log.detail 已同步为 longtext)。
- // - 可空列(IS_NULLABLE=YES)一律指针化(*string/*int64/*time.Time);写边界 nil 归一由 Service 负责,
- // model 只如实映射列,不在 model 里做业务归一(防"converting NULL" 500 + 防字段语义漂移)。
- // - 列名显式:所有字段都写 `gorm:"column:xxx"`,不依赖 GORM 默认命名。
- // - 时间列 CreatedAt/UpdatedAt:GORM 按字段名自动管理(GORM v1.31 MySQL 默认 datetime(3),
- // 与 DDL 的 DEFAULT/ON UPDATE CURRENT_TIMESTAMP(3) 语义一致)。
- //
- // 依赖方向铁律:本包属 identity 域 model,只被 identity 域 service/repo 依赖;
- // 不依赖 handler/service,不碰 DB 句柄(纯实体 + 列映射)。
- package model
- import "time"
- // PersonType User 的身份类型(t_user.person_type,规划 §5.1:User 级区分"人"的业务身份)。
- // 与 Membership.Role(运营角色 7 类)是**两个维度**:person_type 是"这什么人",role 是"在哪个机构/校区担什么职"。
- const (
- PersonTypeStudent = "student" // 学生
- PersonTypeTeacher = "teacher" // 老师
- PersonTypeParent = "parent" // 家长
- PersonTypeStaff = "staff" // 员工(教务/财务/管理等运营角色,默认值)
- )
- // UserStatus 用户全局状态(t_user.status,平台级,跨机构)。
- const (
- UserStatusActive = "active" // 正常
- UserStatusDisabled = "disabled" // 禁用(账号封禁,全机构不可登录)
- UserStatusMerged = "merged" // 已合并(归一 pipeline 软合并,不物理删,数据保留供审计)
- )
- // HashVersion 密码哈希版本(t_user.hash_version,ADR-016 惰性升级路由依据)。
- // 登录时先按版本路由比对,避免双算法误判;命中旧版本即升级 argon2。
- const (
- HashVersionMd5Salt = "md5_salt" // 旧教务:md5(password + salt)
- HashVersionShaSalt = "sha_salt" // 旧教学:sha1(password + salt)
- HashVersionArgon2 = "argon2" // 目标态:argon2id(惰性升级终态)
- )
- // User 用户主表 t_user(人,平台级全局唯一,跨机构共享)。
- //
- // D1:主键 CanonicalUserID 为 string(ULID),size:26 对齐 varchar(26);
- // GORM string 主键用 primaryKey tag 标注(非自增)。
- type User struct {
- CanonicalUserID string `gorm:"column:canonical_user_id;primaryKey;size:26" json:"canonicalUserId"`
- Username *string `gorm:"column:username;size:64" json:"username,omitempty"`
- DisplayName *string `gorm:"column:display_name;size:64" json:"displayName,omitempty"`
- AvatarURL *string `gorm:"column:avatar_url;size:512" json:"avatarUrl,omitempty"`
- PersonType string `gorm:"column:person_type;size:16;not null" json:"personType"`
- PasswordHash *string `gorm:"column:password_hash;size:255" json:"-"` // 密码哈希永不出 JSON(敏感,脱敏纪律 §3.3)
- PasswordSalt *string `gorm:"column:password_salt;size:128" json:"-"` // 盐永不出 JSON
- HashVersion *string `gorm:"column:hash_version;size:16" json:"-"` // 版本永不出 JSON
- Status string `gorm:"column:status;size:16;not null" json:"status"`
- CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
- UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
- }
- // TableName 指定表名(GORM 默认会把 User → users 复数,与 DDL 单数 t_user 不符,必须显式)。
- func (User) TableName() string { return "t_user" }
- // IdentityType 登录身份类型(t_identity.type,身份归一后的"登录方式"枚举)。
- const (
- IdentityTypePhone = "phone" // 手机号(11 位归一)
- IdentityTypeEmail = "email" // 邮箱(小写归一)
- IdentityTypeWechat = "wechat" // 微信 open_id(per-appid 天然唯一)
- IdentityTypeUsername = "username" // 用户名(账密登录账号)
- IdentityTypeLegacyAcademic = "legacy_academic" // 教务旧 user 记录(迁移种子)
- IdentityTypeLegacyTeaching = "legacy_teaching" // 教学旧 user 记录(迁移种子)
- )
- // Identity 登录身份表 t_identity(多对一挂 User,一人多种登录方式)。
- //
- // D3:主键 IdentityID int64 自增;CanonicalUserID string(ULID,size:26)。
- // v1.2:(Type, Identifier, AppID) 联合唯一(uk_type_identifier_appid)——
- // phone/email/username 类型 AppID 恒 "",wechat 类型 AppID 必填(per-appid 唯一)。
- // AppID 从 *string 改 string(NOT NULL DEFAULT ''),对齐 DDL v1.2 拍板"不用可空列"。
- type Identity struct {
- IdentityID int64 `gorm:"column:identity_id;primaryKey" json:"identityId"`
- CanonicalUserID string `gorm:"column:canonical_user_id;size:26;not null" json:"canonicalUserId"`
- Type string `gorm:"column:type;size:32;not null;uniqueIndex:uk_type_identifier_appid,priority:1" json:"type"`
- Identifier string `gorm:"column:identifier;size:128;not null;uniqueIndex:uk_type_identifier_appid,priority:2" json:"identifier"`
- AppID string `gorm:"column:appid;size:64;not null;uniqueIndex:uk_type_identifier_appid,priority:3" json:"appId"`
- Verified bool `gorm:"column:verified;not null" json:"verified"` // tinyint(1) → bool
- CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
- }
- // TableName 指定表名(GORM 默认 Identity → identities,与 DDL 单数 t_identity 不符)。
- func (Identity) TableName() string { return "t_identity" }
- // WechatUnion 微信 unionid 锚点表 t_wechat_union(跨 appid 自然人归一锚点,v1.2 新增)。
- //
- // D3:主键 ID int64 自增;CanonicalUserID string(ULID,size:26,UNIQUE——一人一 unionid)。
- // UnionID string(size:100,UNIQUE——一个 unionid 只能锚定一个 canonical 用户)。
- // unionid 不是登录标识(不落 t_identity),是归一 pipeline / 登录绑定时把多 appid 的
- // t_identity(wechat) 收敛到同一 canonical_user_id 的锚点。
- type WechatUnion struct {
- ID int64 `gorm:"column:id;primaryKey" json:"id"`
- CanonicalUserID string `gorm:"column:canonical_user_id;size:26;not null;uniqueIndex:uk_canonical" json:"canonicalUserId"`
- UnionID string `gorm:"column:unionid;size:100;not null;uniqueIndex:uk_unionid" json:"unionId"`
- CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
- UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
- }
- // TableName 指定表名(GORM 默认 WechatUnion → wechat_unions,与 DDL 单数 t_wechat_union 不符)。
- func (WechatUnion) TableName() string { return "t_wechat_union" }
- // LegacyMapping 遗留映射表 t_legacy_mapping(三源旧 int user_id → ULID,永久保留,审计/回滚底线)。
- //
- // D3:主键 ID int64 自增;CanonicalUserID string(ULID,size:26,UNIQUE)。
- // 旧 int 只存这里(ADR-013);legacy_*_user_id 用 *int32 对齐 DDL 的 int(非 bigint)。
- type LegacyMapping struct {
- ID int64 `gorm:"column:id;primaryKey" json:"id"`
- CanonicalUserID string `gorm:"column:canonical_user_id;size:26;not null" json:"canonicalUserId"`
- LegacyAcademicUserID *int32 `gorm:"column:legacy_academic_user_id" json:"legacyAcademicUserId,omitempty"` // int → int32
- LegacyTeachingUserID *int32 `gorm:"column:legacy_teaching_user_id" json:"legacyTeachingUserId,omitempty"` // int → int32
- LegacyIUserID *int32 `gorm:"column:legacy_i_user_id" json:"legacyIUserId,omitempty"` // int → int32
- InstitutionID int64 `gorm:"column:institution_id;not null" json:"institutionId"`
- CampusID int64 `gorm:"column:campus_id;not null" json:"campusId"`
- Role *string `gorm:"column:role;size:32" json:"role,omitempty"`
- MatchSource *string `gorm:"column:match_source;size:32" json:"matchSource,omitempty"`
- MatchConfidence int8 `gorm:"column:match_confidence;not null" json:"matchConfidence"` // tinyint → int8
- AuditRef *string `gorm:"column:audit_ref;size:64" json:"auditRef,omitempty"`
- MergedAt *time.Time `gorm:"column:merged_at" json:"mergedAt,omitempty"`
- CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
- UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
- }
- // TableName 指定表名(GORM 默认 LegacyMapping → legacy_mappings,与 DDL 单数 t_legacy_mapping 不符)。
- func (LegacyMapping) TableName() string { return "t_legacy_mapping" }
- // AuditAction 审计动作枚举(t_audit_log.action,登录/合并/权限变更全量,ADR-016 §6.4 安全要点)。
- const (
- AuditActionLogin = "login" // 登录成功
- AuditActionLogout = "logout" // 登出
- AuditActionRefresh = "refresh" // refresh 轮换
- AuditActionMerge = "merge" // 用户归一合并
- AuditActionRoleGrant = "role_grant" // 授权
- AuditActionRoleRevoke = "role_revoke" // 撤权
- AuditActionScopeChange = "scope_change" // 作用域变更(换机构/校区)
- AuditActionPasswordChange = "password_change" // 改密(含惰性升级)
- )
- // AuditLog 审计日志表 t_audit_log(登录/合并/权限变更全量留痕)。
- //
- // D3:主键 ID int64 自增;ActorUserID string(ULID,size:26,可空——系统操作 NULL)。
- // Detail 用 *string 直存 JSON(GORM → longtext;DDL detail 列已同步为 longtext,对齐 task.Payload 纪律)。
- // 审计表只增不改(DDL 无 updated_at 列,故 model 不设 UpdatedAt,GORM 不为无字段建列)。
- type AuditLog struct {
- ID int64 `gorm:"column:id;primaryKey" json:"id"`
- ActorUserID *string `gorm:"column:actor_user_id;size:26" json:"actorUserId,omitempty"`
- Action string `gorm:"column:action;size:64;not null" json:"action"`
- EntityType *string `gorm:"column:entity_type;size:32" json:"entityType,omitempty"`
- EntityID *string `gorm:"column:entity_id;size:64" json:"entityId,omitempty"`
- InstitutionID *int64 `gorm:"column:institution_id" json:"institutionId,omitempty"`
- CampusID *int64 `gorm:"column:campus_id" json:"campusId,omitempty"`
- Detail *string `gorm:"column:detail" json:"detail,omitempty"` // JSON → longtext(string 直存)
- TraceID *string `gorm:"column:trace_id;size:64" json:"traceId,omitempty"`
- IP *string `gorm:"column:ip;size:64" json:"ip,omitempty"`
- CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
- }
- // TableName 指定表名(GORM 默认 AuditLog → audit_logs,与 DDL 单数 t_audit_log 不符)。
- func (AuditLog) TableName() string { return "t_audit_log" }
- // AllTables 返回本域全部 model 指针切片(供装配层/集成测试合并后传入 infra/db.Migrate)。
- //
- // 入参:无。
- // 出参:[]any——identity 域 5 表(User/Identity/WechatUnion/LegacyMapping/AuditLog;v1.2 +WechatUnion)。
- // 说明:返回新切片(非共享底层数组),调用方可安全 append。
- func AllTables() []any {
- return []any{
- &User{},
- &Identity{},
- &WechatUnion{},
- &LegacyMapping{},
- &AuditLog{},
- }
- }
|