Files
Inbox/Go项目实战/用户模块/03_entity 代码.md

206 lines
8.4 KiB
Markdown
Raw Permalink Normal View History

2025-12-11 07:24:36 +08:00
---
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 的原则。