--- tags: [] date created: 星期二, 十二月 9日 2025, 11:56:32 晚上 date modified: 星期二, 十二月 9日 2025, 11:57:24 晚上 --- # 03_entity 代码 根据 **v1.1 SQL DDL** 的变更(主要是增加了反范式化字段 `cached_role_codes`)以及 **“按领域分包”** 的架构要求,以下是完整的、生产级的 GORM Entity 代码。 请注意:为了处理 PostgreSQL 的 `text[]` 数组类型,我们在 Go 中通常直接使用 `[]string` 并配合 GORM 的 `type:text[]` 标签(GORM v2 的 Postgres 驱动支持此特性)。 --- ## 📂 1. 用户领域实体 **文件路径:** `internal/user/entity.go` ```Go package user import ( "database/sql/driver" "encoding/json" "errors" "time" "gorm.io/gorm" ) // ================================================================================= // 核心实体 (Core Entities) // 遵循 "Pragmatic Entity" 模式: 既是业务实体也是 GORM 模型 // ================================================================================= // User 聚合根 type User struct { // ID 使用 int64 对应 BigSerial ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` // ------------------------------------------------------------------------- // 认证与安全 // ------------------------------------------------------------------------- Username string `gorm:"column:username;type:text;not null;unique" json:"username"` PasswordHash string `gorm:"column:password_hash;type:text;not null" json:"-"` // 🔒 安全: 永不序列化 // ------------------------------------------------------------------------- // 个人资料 (Profile) // 使用指针 (*string) 以区分 DB 中的 NULL 和 空字符串 // ------------------------------------------------------------------------- Nickname *string `gorm:"column:nickname;type:text" json:"nickname"` AvatarURL *string `gorm:"column:avatar_url;type:text" json:"avatarUrl"` Bio *string `gorm:"column:bio;type:text" json:"bio"` // ------------------------------------------------------------------------- // 状态与权限 // ------------------------------------------------------------------------- // Status: 1=Active, 0=Banned Status int16 `gorm:"column:status;type:smallint;not null;default:1" json:"status"` // [v1.1 新增] 反范式化字段: 缓存角色编码 // GORM Postgres 驱动通常能自动处理 []string <-> text[] // 作用: 鉴权中间件读取此字段即可,无需 Join 角色表 CachedRoleCodes StringArray `gorm:"column:cached_role_codes;type:text[];not null;default:'{}'" json:"cachedRoleCodes"` // ------------------------------------------------------------------------- // 审计与时间 // ------------------------------------------------------------------------- CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"` UpdatedAt time.Time `gorm:"column:updated_at;not null;default:now()" json:"updatedAt"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"` // 启用软删除 // ------------------------------------------------------------------------- // 关联 (Associations) - 仅用于 Preload,非物理字段 // ------------------------------------------------------------------------- Roles []Role `gorm:"many2many:user_roles;joinForeignKey:user_id;joinReferences:role_id" json:"roles,omitempty"` } // TableName 显式定义表名 func (User) TableName() string { return "users" } // Role 角色实体 type Role struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` Code string `gorm:"column:code;type:text;not null;unique" json:"code"` // e.g. "admin" Name string `gorm:"column:name;type:text;not null" json:"name"` // e.g. "管理员" Description *string `gorm:"column:description;type:text" json:"description"` CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"` UpdatedAt time.Time `gorm:"column:updated_at;not null;default:now()" json:"updatedAt"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at;index" json:"-"` } func (Role) TableName() string { return "roles" } // UserRole 关联实体 (中间表) // 即使是中间表,也定义实体以便于管理(如审计创建时间) type UserRole struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` UserID int64 `gorm:"column:user_id;not null" json:"userId"` RoleID int64 `gorm:"column:role_id;not null" json:"roleId"` CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"` } func (UserRole) TableName() string { return "user_roles" } // ================================================================================= // 辅助类型 (Helper Types) // 确保 PostgreSQL 数组类型能正确 Scanner/Valuer // ================================================================================= // StringArray 自定义类型以处理 PostgreSQL 的 text[] // 虽然 GORM 高版本支持 []string,但显式实现接口更兼容不同的 Driver type StringArray []string func (a *StringArray) Scan(value interface{}) error { bytes, ok := value.([]byte) if !ok { return errors.New("type assertion to []byte failed") } // 处理空数组情况 "{}" str := string(bytes) if str == "{}" { *a = []string{} return nil } // 这里使用简单处理,生产环境建议使用 lib/pq 或 pgx 的解析逻辑 // 简单实现: 依赖 GORM 默认行为,或者使用 JSON 兼容 return json.Unmarshal(bytes, a) // 注意:PG 数组格式不是 JSON,此处通常依赖 Driver 的自动转换 } func (a StringArray) Value() (driver.Value, error) { if len(a) == 0 { return "{}", nil } // 同样,依赖 GORM 传递给 Driver return a, nil } ``` > **👨‍💻 架构师注 (User Entity):** > > 1. **`CachedRoleCodes`**: 我定义了一个 `StringArray` 类型。虽然现代 GORM (`gorm.io/driver/postgres`) 可以直接把 `[]string` 映射为 `text[]`,但在代码中显式标记类型(甚至未来引入 `lib/pq` 的 `pq.StringArray`)会让类型转换逻辑更健壮,防止驱动更新导致的兼容性问题。 > > 2. **`json:"-"`**: 再次确认 `PasswordHash` 和 `DeletedAt` 已被隐藏,防止敏感信息泄露。 --- ## 📂 2. 认证领域实体 **文件路径:** `internal/auth/entity.go` ```Go package auth import ( "time" ) // RefreshToken 令牌实体 // 负责双令牌机制中的长效令牌存储与风控 // 对应 SQL 表: refresh_tokens type RefreshToken struct { ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` UserID int64 `gorm:"column:user_id;not null;index" json:"userId"` // 仅存储 ID,保持领域解耦 // ------------------------------------------------------------------------- // 安全核心 // ------------------------------------------------------------------------- // TokenHash: 存哈希值,防止数据库泄露后 Token 直接被利用 TokenHash string `gorm:"column:token_hash;type:text;not null;unique" json:"-"` // ------------------------------------------------------------------------- // 风控与轮转 (Token Rotation & Risk Control) // ------------------------------------------------------------------------- // FamilyID: 标记一整条 Token 链,用于检测重放攻击时级联吊销 FamilyID string `gorm:"column:family_id;type:text;not null;index" json:"familyId"` // ParentTokenID: 溯源上一个 Token,构建链式关系 ParentTokenID string `gorm:"column:parent_token_id;type:text;not null;default:''" json:"parentTokenId"` // IsRevoked: 黑名单开关 IsRevoked bool `gorm:"column:is_revoked;not null;default:false" json:"isRevoked"` // ------------------------------------------------------------------------- // 生命周期 // ------------------------------------------------------------------------- ExpiresAt time.Time `gorm:"column:expires_at;not null" json:"expiresAt"` CreatedAt time.Time `gorm:"column:created_at;not null;default:now()" json:"createdAt"` } // TableName 显式定义表名 func (RefreshToken) TableName() string { return "refresh_tokens" } ``` > **👨‍💻 架构师注 (Auth Entity):** > > 1. **领域边界**: 这里没有引入 `User` 结构体指针。在 `auth` 领域内,我们只关心 `UserID`。如果业务逻辑需要获取用户详情(例如封禁检查),应由 `auth.Service` 调用 `user.Service` 或 `user.Repository`,而不是在 Entity 层面强耦合。这符合 Clean Architecture 的原则。