Files
Inbox/Go项目实战/用户模块/03_entity 代码.md
2025-12-11 07:24:36 +08:00

206 lines
8.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 的原则。