206 lines
8.4 KiB
Markdown
206 lines
8.4 KiB
Markdown
|
|
---
|
|||
|
|
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 的原则。
|