feat: 实现 RBAC 权限系统和数据权限控制 (004-rbac-data-permission)

主要功能:
- 实现完整的 RBAC 权限系统(账号、角色、权限的多对多关联)
- 基于 owner_id + shop_id 的自动数据权限过滤
- 使用 PostgreSQL WITH RECURSIVE 查询下级账号
- Redis 缓存优化下级账号查询性能(30分钟过期)
- 支持多租户数据隔离和层级权限管理

技术实现:
- 新增 Account、Role、Permission 模型及关联关系表
- 实现 GORM Scopes 自动应用数据权限过滤
- 添加数据库迁移脚本(000002_rbac_data_permission、000003_add_owner_id_shop_id)
- 完善错误码定义(1010-1027 为 RBAC 相关错误)
- 重构 main.go 采用函数拆分提高可读性

测试覆盖:
- 添加 Account、Role、Permission 的集成测试
- 添加数据权限过滤的单元测试和集成测试
- 添加下级账号查询和缓存的单元测试
- 添加 API 回归测试确保向后兼容

文档更新:
- 更新 README.md 添加 RBAC 功能说明
- 更新 CLAUDE.md 添加技术栈和开发原则
- 添加 docs/004-rbac-data-permission/ 功能总结和使用指南

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-18 16:44:06 +08:00
parent e8eb5766cb
commit eaa70ac255
86 changed files with 15395 additions and 245 deletions

View File

@@ -0,0 +1,264 @@
# 架构说明RBAC 表结构与 GORM 数据权限过滤
## 概述
本功能实现了完整的 RBAC基于角色的访问控制权限系统以及基于 `owner_id` + `shop_id` 的自动数据权限过滤机制。
## 架构分层
本系统遵循 Handler → Service → Store → Model 四层架构:
```
┌─────────────────────────────────────────────────────────┐
│ Handler Layer │
│ (HTTP 请求/响应处理,参数验证,调用 Service) │
├─────────────────────────────────────────────────────────┤
│ Service Layer │
│ (业务逻辑,事务管理,缓存清理,跨模块调用) │
├─────────────────────────────────────────────────────────┤
│ Store Layer │
│ (数据访问GORM 操作,数据权限过滤 Scopes) │
├─────────────────────────────────────────────────────────┤
│ Model Layer │
│ (数据模型定义DTO 结构) │
└─────────────────────────────────────────────────────────┘
```
## 核心组件
### 1. 数据模型
#### RBAC 表结构
| 表名 | 说明 | 关键字段 |
|------|------|----------|
| `tb_account` | 账号表 | `parent_id`(层级关系), `shop_id`(店铺隔离), `user_type` |
| `tb_role` | 角色表 | `role_type`(角色类型)|
| `tb_permission` | 权限表 | `perm_code`(唯一权限码), `parent_id`(树形结构)|
| `tb_account_role` | 账号-角色关联表 | `account_id`, `role_id` |
| `tb_role_permission` | 角色-权限关联表 | `role_id`, `perm_id` |
#### 设计原则
- **无外键约束**:表之间通过 ID 字段关联,不使用数据库外键约束
- **软删除支持**:所有表都有 `deleted_at` 字段,支持 GORM 软删除
- **唯一约束带条件**:用户名、手机号、权限码的唯一约束仅在未删除记录中生效
### 2. 数据权限过滤
#### 核心流程
```
用户请求 → 认证中间件 → 设置用户上下文 → 业务查询 → DataPermissionScope → 返回数据
```
#### DataPermissionScope 工作原理
1. 从 Context 提取用户 ID、用户类型、店铺 ID
2. 检查是否为 root 用户(跳过过滤)
3. 递归查询当前用户的所有下级 ID含自己
4. 应用 WHERE 条件:`owner_id IN (...) AND shop_id = ?`
#### 递归查询优化
使用 PostgreSQL 的 `WITH RECURSIVE` 进行递归查询:
```sql
WITH RECURSIVE subordinates AS (
SELECT id FROM tb_account WHERE id = ?
UNION ALL
SELECT a.id FROM tb_account a
INNER JOIN subordinates s ON a.parent_id = s.id
)
SELECT id FROM subordinates
```
### 3. Redis 缓存策略
#### 缓存设计
- **缓存键格式**`account:subordinates:{account_id}`
- **过期时间**30 分钟
- **缓存内容**:用户及其所有下级的 ID 列表
#### 缓存失效策略
- 创建子账号时:清除父账号及所有上级的缓存
- 删除账号时:清除父账号及所有上级的缓存
- 缓存自动过期后:下次查询重新生成
### 4. Context 上下文传递
#### 上下文键
```go
const (
UserIDKey = "user_id"
UserTypeKey = "user_type"
ShopIDKey = "shop_id"
)
```
#### 辅助函数
- `SetUserContext(ctx, userID, userType, shopID)` - 设置用户上下文
- `GetUserIDFromContext(ctx)` - 获取用户 ID
- `GetShopIDFromContext(ctx)` - 获取店铺 ID
- `IsRootUser(ctx)` - 检查是否为 root 用户
## 路由模块化
### 目录结构
```
internal/routes/
├── routes.go # 主入口Services 容器
├── account.go # 账号路由
├── role.go # 角色路由
├── permission.go # 权限路由
├── health.go # 健康检查路由
└── task.go # 任务路由
```
### Services 容器
```go
type Services struct {
AccountHandler *handler.AccountHandler
RoleHandler *handler.RoleHandler
PermissionHandler *handler.PermissionHandler
}
```
## 主函数重构
### 编排模式
main 函数仅做编排,不包含具体实现:
```go
func main() {
cfg := initConfig() // 加载配置
logger := initLogger(cfg) // 初始化日志
db := initDatabase(cfg) // 初始化数据库
redis := initRedis(cfg) // 初始化 Redis
queue := initQueue(redis) // 初始化队列
services := initServices(db, redis) // 初始化服务
app := createFiberApp(cfg) // 创建应用
initMiddleware(app, cfg) // 注册中间件
initRoutes(app, services) // 注册路由
startServer(app, cfg) // 启动服务器
}
```
### 初始化函数职责
| 函数 | 职责 |
|------|------|
| `initConfig` | 加载配置文件 |
| `initLogger` | 初始化 Zap 日志 |
| `initDatabase` | 连接 PostgreSQL |
| `initRedis` | 连接 Redis |
| `initQueue` | 初始化 Asynq 客户端 |
| `initServices` | 创建所有 Service 实例 |
| `initMiddleware` | 注册全局中间件 |
| `initRoutes` | 注册所有路由 |
| `startServer` | 启动 HTTP 服务器 |
## 性能考量
### 关键性能指标
- API 响应时间 P95 < 200ms
- 递归查询下级 ID P95 < 50ms含 Redis 缓存)
- 数据库查询 P95 < 50ms
### 优化措施
1. **Redis 缓存**:缓存递归查询结果,避免重复数据库查询
2. **索引优化**:关键字段都建立索引(`parent_id``shop_id``owner_id`
3. **批量操作**:账号-角色、角色-权限支持批量创建
4. **连接池**:数据库和 Redis 配置合理的连接池
## 安全考量
### 数据隔离
- **店铺隔离**:通过 `shop_id` 实现多租户数据隔离
- **层级权限**:用户只能访问自己及下级创建的数据
- **root 用户**:跳过数据权限过滤,可访问所有数据
### 密码安全
- 密码字段使用 `json:"-"` 标签,不返回给客户端
- 密码使用 bcrypt 加密存储
### 错误处理
- 查询下级 ID 失败时,降级为只返回自己的数据
- 所有错误通过统一错误处理机制返回
## 扩展指南
### 添加新业务表的数据权限
1. 在业务表中添加 `owner_id``shop_id` 字段
2. 在 Store 方法中应用 `DataPermissionScope`
3. 确保创建记录时设置正确的 `owner_id``shop_id`
示例:
```go
func (s *OrderStore) List(ctx context.Context) ([]*model.Order, error) {
var orders []*model.Order
err := s.db.WithContext(ctx).
Scopes(postgres.DataPermissionScope(ctx, s.accountStore)).
Find(&orders).Error
return orders, err
}
```
### 添加新的 RBAC 实体
1. 创建 Model 和 DTO
2. 创建 StoreCRUD 方法)
3. 创建 Service业务逻辑
4. 创建 HandlerHTTP 接口)
5. 添加路由文件
6. 更新 Services 容器
## 测试策略
### 单元测试
- 递归查询测试
- 缓存读写测试
- 数据权限 Scope 测试
- 软删除测试
### 集成测试
- 数据库迁移测试
- 层级数据权限过滤测试
- 跨店铺数据隔离测试
- API 端点测试
## 技术决策记录
### 为什么不使用外键?
1. **灵活性**:业务逻辑完全在代码中控制
2. **性能**:无外键约束检查开销
3. **分布式友好**:便于未来拆分微服务
### 为什么使用 WITH RECURSIVE
1. **原生支持**PostgreSQL 内置支持
2. **性能优异**:单次查询获取所有下级
3. **深度无限**:支持任意层级的递归
### 为什么缓存过期时间是 30 分钟?
1. **平衡性**:在实时性和性能之间取得平衡
2. **业务特点**:账号层级变化不频繁
3. **可配置**:可根据业务需求调整