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

8.4 KiB
Raw Permalink Blame History

tags, date created, date modified
tags date created date modified
星期二, 十二月 9日 2025, 11:56:32 晚上 星期二, 十二月 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

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/pqpq.StringArray)会让类型转换逻辑更健壮,防止驱动更新导致的兼容性问题。

  2. json:"-": 再次确认 PasswordHashDeletedAt 已被隐藏,防止敏感信息泄露。


📂 2. 认证领域实体

文件路径: internal/auth/entity.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.Serviceuser.Repository,而不是在 Entity 层面强耦合。这符合 Clean Architecture 的原则。