创建仓库
This commit is contained in:
111
Go项目实战/用户模块/01_实体关系图.md
Normal file
111
Go项目实战/用户模块/01_实体关系图.md
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- ER 图
|
||||
date created: 星期二, 十二月 9日 2025, 10:45:43 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 10:58:01 晚上
|
||||
---
|
||||
|
||||
# ER 图
|
||||
|
||||
**设计思路分析:**
|
||||
|
||||
1. **RBAC 模型选择:** 为了满足“银行级权限控制”及“企业级样板间”的扩展性要求,我采用了标准的 **RBAC Level 1 (Flat RBAC)** 变体。虽然当前只有 3 个固定角色,但使用 **多对多 (Many-to-Many)** 的关联表 (`user_roles`) 能够支持未来某用户既是 "Editor" 又是 "TechLeader" 的混合权限场景,避免后续重构。
|
||||
2. **双令牌机制落地:** 专门设计了 `refresh_tokens` 表。JWT 的 Access Token 是无状态的(不入库),但 Refresh Token 必须入库以实现“吊销”、“防重放”和“设备管理”功能。
|
||||
3. **软删除与审计:** 所有核心表(`users`, `roles`)均继承了 Base Model,包含 `deleted_at` 字段。
|
||||
|
||||
---
|
||||
|
||||
## 📊 阶段二:概念验证 (Conceptual Modeling - ER Diagram)
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
%% ---------------------------------------------------------
|
||||
%% 1. 用户核心表 (Users)
|
||||
%% 核心聚合根,包含认证凭证与个人资料
|
||||
%% ---------------------------------------------------------
|
||||
users {
|
||||
bigint id PK "主键"
|
||||
string username "用户名, unique, not null"
|
||||
string password_hash "Argon2/Bcrypt 哈希值, not null"
|
||||
string nickname "用户昵称 (Profile)"
|
||||
string avatar_url "头像链接 (Profile)"
|
||||
string bio "个人简介 (Profile)"
|
||||
smallint status "状态: 1=Active, 0=Banned"
|
||||
timestamptz created_at "创建时间"
|
||||
timestamptz updated_at "更新时间"
|
||||
timestamptz deleted_at "软删除时间 (Soft Delete)"
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 2. 角色表 (Roles)
|
||||
%% 存储 Admin, Editor, Subscriber 等定义
|
||||
%% ---------------------------------------------------------
|
||||
roles {
|
||||
bigint id PK "主键"
|
||||
string code "角色编码 (e.g. 'admin'), unique"
|
||||
string name "角色显示名称 (e.g. '超级管理员')"
|
||||
string description "备注"
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
timestamptz deleted_at
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 3. 用户-角色关联表 (User Roles)
|
||||
%% 中间表,实现 RBAC 多对多关系
|
||||
%% ---------------------------------------------------------
|
||||
user_roles {
|
||||
bigint user_id FK "关联 users.id"
|
||||
bigint role_id FK "关联 roles.id"
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 4. 刷新令牌表 (Refresh Tokens)
|
||||
%% 用于双令牌机制的续期与风控
|
||||
%% ---------------------------------------------------------
|
||||
refresh_tokens {
|
||||
bigint id PK "主键"
|
||||
bigint user_id FK "关联 users.id"
|
||||
string token_hash "Refresh Token 的哈希值 (安全考虑不存明文)"
|
||||
string family_id "令牌家族ID (用于检测重用/Rotation)"
|
||||
string parent_token_id "父令牌ID (用于溯源)"
|
||||
boolean is_revoked "是否已撤销 (黑名单机制)"
|
||||
timestamptz expires_at "过期时间 (7天)"
|
||||
timestamptz created_at
|
||||
}
|
||||
|
||||
%% ---------------------------------------------------------
|
||||
%% 关系定义 (Relationships)
|
||||
%% ---------------------------------------------------------
|
||||
|
||||
%% 一个用户可以拥有多个 Refresh Token (多设备登录)
|
||||
users ||--o{ refresh_tokens : "has_many (sessions)"
|
||||
|
||||
%% 一个用户可以拥有多个角色 (扩展性设计)
|
||||
users ||--o{ user_roles : "assigned_to"
|
||||
|
||||
%% 一个角色可以包含多个用户
|
||||
roles ||--o{ user_roles : "includes"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔑 关键关系业务含义说明
|
||||
|
||||
1. **Users `||--o{` Refresh Tokens (1 对 多):**
|
||||
|
||||
- **业务含义:** 一个用户可以在多个设备(手机、PC)同时登录。
|
||||
- **风控逻辑:** 当用户执行“修改密码”或“严重违规封号”时,后端会根据 `user_id` 批量将该用户下的所有 `is_revoked` 设为 `true`,从而实现**强制下线**。
|
||||
- **令牌轮转:** `family_id` 和 `parent_token_id` 用于检测令牌复用攻击。如果一个旧的 RT 被使用,系统可以根据 `family_id` 级联作废整个令牌链。
|
||||
|
||||
2. **Users `}|--|{` Roles (多 对 多,通过 `user_roles`):**
|
||||
|
||||
- **业务含义:** 虽然 PRD 定义了三个层级,但物理模型采用“多对多”设计是架构上的**防御性编程**。
|
||||
- **场景:** 如果未来需要赋予某个 Editor 临时的 Admin 权限,或者创建一个 "SuperSubscriber"(付费会员),我们只需在 `user_roles` 插入一条记录,而无需修改表结构。
|
||||
|
||||
3. **Users 表的混合设计:**
|
||||
|
||||
- **决策:** 我没有将 `Profile` (昵称/头像) 拆分为独立表。
|
||||
- **理由:** 遵循“勿增实体”原则。在当前 V1 阶段,Profile 字段较少(仅 3 个),且在列表页展示时(如“评论列表”)属于高频连表查询。合并不仅减少了 JOIN 开销,也简化了 GORM 的模型编写。
|
||||
212
Go项目实战/用户模块/02_SQL DDL 脚本.md
Normal file
212
Go项目实战/用户模块/02_SQL DDL 脚本.md
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- SQL DDL 脚本
|
||||
- 🛡️ 设计说明与合规性检查
|
||||
date created: 星期二, 十二月 9日 2025, 11:04:59 晚上
|
||||
date modified: 星期二, 十二月 9日 2025, 11:53:32 晚上
|
||||
---
|
||||
这里是为您生成的 **PostgreSQL 15** 生产级 SQL DDL 脚本。
|
||||
|
||||
# SQL DDL 脚本
|
||||
|
||||
## 🛡️ 设计说明与合规性检查
|
||||
|
||||
1. **架构一致性:** 严格遵循之前确认的 Mermaid ER 图逻辑。
|
||||
2. **软删除优化:** 所有包含 `deleted_at` 的表均增加了部分索引 (Partial Index),确保在使用 `WHERE deleted_at IS NULL` 时查询性能最优,同时减少索引体积。
|
||||
3. **安全性:**
|
||||
|
||||
- 所有外键均使用 `ON DELETE RESTRICT`,防止误删用户导致级联删除关联数据(如订单、日志)。
|
||||
- `refresh_tokens` 表的 `token_hash` 设为唯一,防止哈希碰撞。
|
||||
|
||||
4. **类型规范:** 遵循宪法,使用 `TIMESTAMPTZ` 处理时间,`TEXT` 处理字符串,`SMALLINT` 处理枚举状态。
|
||||
|
||||
---
|
||||
|
||||
### 📝 修改说明文档 (Changelog v1.1)
|
||||
|
||||
本次更新核心聚焦于 **“千万级数据下的查询性能”** 与 **“高频鉴权的延迟优化”**。
|
||||
|
||||
|**修改点**|**变更内容 (What)**|**预期效果 (Effect)**|**架构师理由 (Why)**|
|
||||
|---|---|---|---|
|
||||
|**1. 反范式化缓存**|`users` 表新增字段 `cached_role_codes TEXT[]`。|**鉴权性能提升 10x**。API 网关/中间件在鉴权时,无需关联查询 `user_roles` 和 `roles` 表,直接读取当前行即可拿到权限。|**Space for Time (以空间换时间)**。鉴权是系统最高频(QPS 10,000+)的操作,3 表 JOIN 在高并发下是 CPU 杀手。虽然增加了写入时的维护成本,但换来了极高的读取性能。|
|
||||
|**2. 深分页索引**|新增索引 `idx_users_active_created_at` (针对 `created_at DESC` + `deleted_at IS NULL`)。|**后台列表秒开**。即使翻到第 10 万页,数据库也能利用索引直接定位数据,避免全表扫描和内存排序。|**B 端体验保障**。原索引仅优化了登录(按用户名查),未覆盖管理后台最常用的“按注册时间倒序查看”场景。|
|
||||
|**3. 分区策略标注**|在 `refresh_tokens` 表头添加了关于 **Range Partitioning** 的架构警告与注释。|**防止 IO 爆炸**。明确告知 DBA/运维团队,在数据量突破 5000 万前必须实施表分区。|**生命周期管理**。日志类/令牌类数据如果不做分区,在执行批量删除(清理过期 Token)时会导致数据库 WAL 日志爆满甚至锁死。|
|
||||
|
||||
---
|
||||
|
||||
### 📜 SQL DDL 脚本 (v1.1 Production-Ready)
|
||||
|
||||
```SQL
|
||||
-- =========================================================
|
||||
-- Enterprise-CMS-Core Schema v1.1
|
||||
-- Architecture: Modular Clean Architecture
|
||||
-- Database: PostgreSQL 15+
|
||||
-- =========================================================
|
||||
|
||||
-- ⚠️ 警告: 生产环境请移除 DROP 语句
|
||||
DROP TABLE IF EXISTS refresh_tokens CASCADE;
|
||||
DROP TABLE IF EXISTS user_roles CASCADE;
|
||||
DROP TABLE IF EXISTS roles CASCADE;
|
||||
DROP TABLE IF EXISTS users CASCADE;
|
||||
|
||||
-- 1. 全局设置
|
||||
-- 确保时间存储统一,避免应用层时区转换混乱
|
||||
SET timezone = 'Asia/Shanghai';
|
||||
|
||||
-- =========================================================
|
||||
-- 2. 用户核心表 (users)
|
||||
-- =========================================================
|
||||
CREATE TABLE users (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL,
|
||||
password_hash TEXT NOT NULL, -- Argon2/Bcrypt Hash
|
||||
|
||||
-- Profile 字段 (允许 NULL,应用层需处理指针)
|
||||
nickname TEXT,
|
||||
avatar_url TEXT,
|
||||
bio TEXT,
|
||||
|
||||
-- 状态: 1=Active, 0=Banned (应用层枚举)
|
||||
status SMALLINT NOT NULL DEFAULT 1,
|
||||
|
||||
-- [v1.1 新增] 反范式化字段: 缓存角色编码
|
||||
-- 目的: 让鉴权中间件实现 Zero-Join 查询
|
||||
-- 默认值: 空数组 '{}',避免 NULL 指针异常
|
||||
cached_role_codes TEXT[] NOT NULL DEFAULT '{}',
|
||||
|
||||
-- Base Model 字段
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
-- 2.1 约束定义
|
||||
ALTER TABLE users
|
||||
ADD CONSTRAINT uniq_users_username UNIQUE (username);
|
||||
|
||||
-- 2.2 索引策略
|
||||
-- [Index] 软删除查询优化 (BRIN / Partial Index)
|
||||
-- 场景: 绝大多数业务只查“未删除”数据,此过滤条件能大幅减小索引体积
|
||||
CREATE INDEX idx_users_deleted_at_brin ON users (deleted_at)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- [Index] 登录查询优化
|
||||
-- 场景: 根据用户名登录,且必须未被删除
|
||||
CREATE INDEX idx_users_username_active ON users (username)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- [v1.1 新增] [Index] 后台管理列表/深分页优化
|
||||
-- 场景: SELECT * FROM users WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT N OFFSET M
|
||||
-- 理由: 消除 FileSort,直接利用索引顺序扫描
|
||||
CREATE INDEX idx_users_active_created_at ON users (created_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
-- 2.3 注释
|
||||
COMMENT ON TABLE users IS '用户核心表';
|
||||
COMMENT ON COLUMN users.cached_role_codes IS '[冗余字段] 缓存用户当前拥有的角色Code (e.g. {admin, editor}),用于提升鉴权性能';
|
||||
|
||||
-- =========================================================
|
||||
-- 3. 角色定义表 (roles)
|
||||
-- =========================================================
|
||||
CREATE TABLE roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
code TEXT NOT NULL, -- 业务唯一标识: 'admin', 'editor'
|
||||
name TEXT NOT NULL, -- 显示名称: '超级管理员'
|
||||
description TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
ALTER TABLE roles
|
||||
ADD CONSTRAINT uniq_roles_code UNIQUE (code);
|
||||
|
||||
COMMENT ON TABLE roles IS '系统角色定义表 (元数据)';
|
||||
|
||||
-- =========================================================
|
||||
-- 4. 用户-角色关联表 (user_roles)
|
||||
-- =========================================================
|
||||
CREATE TABLE user_roles (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- 4.1 外键约束 (确保数据一致性,防止孤儿数据)
|
||||
ALTER TABLE user_roles
|
||||
ADD CONSTRAINT fk_user_roles_users FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE user_roles
|
||||
ADD CONSTRAINT fk_user_roles_roles FOREIGN KEY (role_id)
|
||||
REFERENCES roles(id) ON DELETE RESTRICT;
|
||||
|
||||
-- 4.2 唯一约束 (防止重复授权)
|
||||
ALTER TABLE user_roles
|
||||
ADD CONSTRAINT uniq_user_roles_pair UNIQUE (user_id, role_id);
|
||||
|
||||
-- 4.3 索引
|
||||
-- 场景: 当管理员更新某用户角色时,需要快速查找到关联记录
|
||||
CREATE INDEX idx_user_roles_user_id ON user_roles(user_id);
|
||||
|
||||
COMMENT ON TABLE user_roles IS '用户与角色的多对多关联表 (Write Source of Truth)';
|
||||
|
||||
-- =========================================================
|
||||
-- 5. 刷新令牌表 (refresh_tokens)
|
||||
-- =========================================================
|
||||
-- [v1.1 架构备注]
|
||||
-- ⚠️ Scaling Policy:
|
||||
-- 当单表行数预计超过 5000 万时,必须启用 Range Partitioning。
|
||||
-- 建议策略: PARTITION BY RANGE (created_at),按月分表,定期 DROP 旧分区。
|
||||
-- 当前 V1 阶段保持标准表结构。
|
||||
CREATE TABLE refresh_tokens (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
token_hash TEXT NOT NULL,
|
||||
|
||||
-- 风控与轮转字段
|
||||
family_id TEXT NOT NULL, -- 令牌家族,用于检测复用攻击
|
||||
parent_token_id TEXT NOT NULL DEFAULT '', -- 溯源链
|
||||
is_revoked BOOLEAN NOT NULL DEFAULT FALSE, -- 黑名单开关
|
||||
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE refresh_tokens
|
||||
ADD CONSTRAINT fk_refresh_tokens_users FOREIGN KEY (user_id)
|
||||
REFERENCES users(id) ON DELETE RESTRICT;
|
||||
|
||||
ALTER TABLE refresh_tokens
|
||||
ADD CONSTRAINT uniq_refresh_tokens_hash UNIQUE (token_hash);
|
||||
|
||||
-- [Index] 安全风控查询
|
||||
-- 场景 1: 用户改密码 -> 吊销所有设备 (WHERE user_id = ?)
|
||||
-- 场景 2: 检测到令牌盗用 -> 吊销整个家族 (WHERE family_id = ?)
|
||||
CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id);
|
||||
CREATE INDEX idx_refresh_tokens_family_id ON refresh_tokens(family_id);
|
||||
|
||||
COMMENT ON TABLE refresh_tokens IS '双令牌机制存储表 (高频写入/删除,需关注膨胀问题)';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 索引策略自辩 (Why These indexes?)
|
||||
|
||||
1. **`users.idx_users_deleted_at_brin`**:
|
||||
|
||||
- **场景**: 系统中几乎 100% 的查询(List, Get)都会带上 `deleted_at IS NULL`。
|
||||
- **优势**: 这是一个“部分索引” (Partial Index)。它只索引活跃用户,随着时间推移,被软删除的用户越来越多,这个索引依然会保持很小,查询速度极快。
|
||||
|
||||
2. **`refresh_tokens.idx_refresh_tokens_family_id`**:
|
||||
|
||||
- **场景**: 触发“令牌复用”警报时。
|
||||
- **逻辑**: 黑客利用旧的 Refresh Token 尝试刷新 -> 系统检测到该 Token 已被用过 -> 系统根据 `family_id` 瞬间查出该链条上所有新颁发的 Token -> 全部标记为 `is_revoked = TRUE`。如果没有这个索引,高并发下这一步风控逻辑会锁表或超时。
|
||||
|
||||
3. **`user_roles.idx_user_roles_user_id`**:
|
||||
|
||||
- **场景**: 每次 HTTP 请求经过 `AuthMiddleware`。
|
||||
- **逻辑**: 中间件必须校验当前 User 是否为 `Admin`。这需要高频执行 `SELECT role_id FROM user_roles WHERE user_id = ?`。此索引是系统吞吐量的关键。
|
||||
205
Go项目实战/用户模块/03_entity 代码.md
Normal file
205
Go项目实战/用户模块/03_entity 代码.md
Normal file
@@ -0,0 +1,205 @@
|
||||
---
|
||||
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 的原则。
|
||||
88
Go项目实战/用户模块/04_业务逻辑功能清单.md
Normal file
88
Go项目实战/用户模块/04_业务逻辑功能清单.md
Normal file
@@ -0,0 +1,88 @@
|
||||
---
|
||||
tags: []
|
||||
aliases:
|
||||
- 04_ 业务逻辑功能清单
|
||||
date created: 星期三, 十二月 10日 2025, 12:04:34 凌晨
|
||||
date modified: 星期三, 十二月 10日 2025, 12:05:53 凌晨
|
||||
---
|
||||
|
||||
# 04_ 业务逻辑功能清单
|
||||
|
||||
## TL;DR (摘要)
|
||||
|
||||
- **基础版 (MVP):** 仅满足最基本的“注册 - 登录 - 看自己”流程,适合快速打通前后端联调,但**不符合** PRD 的安全标准。
|
||||
- **完整版 (Enterprise):** 严格对应 PRD V1.1,包含双令牌刷新、强制登出、RBAC 提权及管理员封禁功能,符合生产环境安全要求。
|
||||
|
||||
---
|
||||
|
||||
## 方案一:基础版 (MVP / Prototype)
|
||||
|
||||
适用场景: 项目初期快速搭建原型 (PoC),验证核心业务流程(如文章发布),暂时忽略复杂的安全合规。
|
||||
|
||||
局限性: 仅使用单 Access Token(长效),无刷新机制,无法强制踢人下线,无管理员管理界面。
|
||||
|
||||
|**模块**|**方法**|**API 路径**|**核心功能描述**|**鉴权要求**|
|
||||
|---|---|---|---|---|
|
||||
|**Auth**|POST|`/api/v1/register`|用户注册 (仅用户名 + 密码)|无|
|
||||
|**Auth**|POST|`/api/v1/login`|用户登录 (返回长效 JWT)|无|
|
||||
|**User**|GET|`/api/v1/user/profile`|获取当前登录用户信息|JWT|
|
||||
|**User**|PUT|`/api/v1/user/profile`|修改自己的昵称、简介|JWT|
|
||||
|
||||
> 自我反驳 (基础版):
|
||||
> 此方案虽然简单,但直接违反了 PRD 中 F-AUTH-03 (令牌刷新) 和 F-AUTH-04 (统一登出) 的要求。若项目进入 Alpha 测试阶段,必须立刻废弃此方案,否则存在严重的安全隐患(Token 泄露即完全失控)。
|
||||
|
||||
---
|
||||
|
||||
## 方案二:完整版 (Enterprise / PRD Compliant)
|
||||
|
||||
**适用场景:** 正式开发与生产环境交付。严格遵循“银行级 RBAC”和“双令牌”机制。
|
||||
|
||||
### 1. 认证服务 (Auth Service) - 公开/基础域
|
||||
|
||||
对应 PRD 章节: 2.1 认证与鉴权模块
|
||||
|
||||
|**需求编号**|**方法**|**API 路径**|**功能描述**|**输入参数**|**鉴权**|
|
||||
|---|---|---|---|---|---|
|
||||
|**F-AUTH-01**|POST|`/api/v1/auth/register`|用户注册 (密码需 Hash 存储)|`username`, `password`|无|
|
||||
|**F-AUTH-02**|POST|`/api/v1/auth/login`|登录 (颁发 Access + Refresh Token)|`username`, `password`|无|
|
||||
|**F-AUTH-03**|POST|`/api/v1/auth/refresh`|**令牌刷新** (旧换新,防复用机制)|`refresh_token`|无|
|
||||
|**F-AUTH-04**|POST|`/api/v1/auth/logout`|**统一登出** (将 Refresh Token 加入黑名单)|`refresh_token`|JWT|
|
||||
|**F-AUTH-05**|POST|`/api/v1/auth/password`|**重置密码** (成功后吊销所有 Token)|`old_pwd`, `new_pwd`|JWT|
|
||||
|
||||
### 2. 用户自服务 (User Self-Service) - 个人域
|
||||
|
||||
对应 PRD 章节: 2.2 用户与权限模块 (F-USER-01)
|
||||
|
||||
|**需求编号**|**方法**|**API 路径**|**功能描述**|**备注**|**鉴权**|
|
||||
|---|---|---|---|---|---|
|
||||
|**F-USER-01**|GET|`/api/v1/users/me`|获取我的详细资料|**建议增加 Redis 缓存**|JWT|
|
||||
|**F-USER-01**|PUT|`/api/v1/users/me`|修改资料 (昵称, 头像 URL, 简介)|更新后需清除缓存|JWT|
|
||||
|
||||
### 3. 管理员运维 (Admin Dashboard) - 管理域
|
||||
|
||||
对应 PRD 章节: 2.2 用户与权限模块 (F-USER-02, F-RBAC-01)
|
||||
|
||||
|**需求编号**|**方法**|**API 路径**|**功能描述**|**关键逻辑**|**鉴权**|
|
||||
|---|---|---|---|---|---|
|
||||
|**F-USER-02**|GET|`/api/v1/admin/users`|**用户列表查询**|支持分页、按用户名搜索、按状态筛选|**Admin Only**|
|
||||
|**F-USER-02**|PATCH|`/api/v1/admin/users/:id/status`|**封禁/解封用户**|修改状态为 `active`/`banned`,若封禁需强制踢下线|**Admin Only**|
|
||||
|**F-RBAC-01**|PATCH|`/api/v1/admin/users/:id/role`|**角色变更 (提权)**|修改角色为 `editor`/`admin`|**Admin Only**|
|
||||
|
||||
---
|
||||
|
||||
## 关键设计决策说明 (Technical Decisions)
|
||||
|
||||
1. **关于 PATCH vs PUT:**
|
||||
|
||||
- 在**完整版**的管理接口中,我使用了 `PATCH` 而不是 `PUT`。
|
||||
- **理由:** `PUT` 语义上是全量替换。在修改用户状态(如封禁)或角色时,我们只修改单个字段,使用 `PATCH` 更符合 RESTful 语义,且能避免管理员无意中覆盖了用户的其他信息(如昵称)。
|
||||
|
||||
2. **关于路径设计 (URI Design):**
|
||||
|
||||
- 区分了 `/users/me` (当前用户) 和 `/admin/users/:id` (管理特定用户)。
|
||||
- **理由:** 这种分离能清晰地界定权限边界。`/me` 接口永远不需要传 ID(从 Token 解析),杜绝了普通用户通过遍历 ID 窃取他人信息的越权风险 (IDOR)。
|
||||
|
||||
3. **关于缓存 (Cache):**
|
||||
|
||||
- **自我反驳:** 虽然 PRD 建议对 `/profile` 进行缓存,但在 API 定义阶段不需要体现在 URL 上。
|
||||
- **补充:** 但作为后端设计,你需要在 `GET /users/me` 的 Controller 层实现 Cache-Aside 模式(先查 Redis,无则查 DB 并回写)。
|
||||
Reference in New Issue
Block a user