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:
264
docs/004-rbac-data-permission/架构说明.md
Normal file
264
docs/004-rbac-data-permission/架构说明.md
Normal 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. 创建 Store(CRUD 方法)
|
||||
3. 创建 Service(业务逻辑)
|
||||
4. 创建 Handler(HTTP 接口)
|
||||
5. 添加路由文件
|
||||
6. 更新 Services 容器
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
|
||||
- 递归查询测试
|
||||
- 缓存读写测试
|
||||
- 数据权限 Scope 测试
|
||||
- 软删除测试
|
||||
|
||||
### 集成测试
|
||||
|
||||
- 数据库迁移测试
|
||||
- 层级数据权限过滤测试
|
||||
- 跨店铺数据隔离测试
|
||||
- API 端点测试
|
||||
|
||||
## 技术决策记录
|
||||
|
||||
### 为什么不使用外键?
|
||||
|
||||
1. **灵活性**:业务逻辑完全在代码中控制
|
||||
2. **性能**:无外键约束检查开销
|
||||
3. **分布式友好**:便于未来拆分微服务
|
||||
|
||||
### 为什么使用 WITH RECURSIVE?
|
||||
|
||||
1. **原生支持**:PostgreSQL 内置支持
|
||||
2. **性能优异**:单次查询获取所有下级
|
||||
3. **深度无限**:支持任意层级的递归
|
||||
|
||||
### 为什么缓存过期时间是 30 分钟?
|
||||
|
||||
1. **平衡性**:在实时性和性能之间取得平衡
|
||||
2. **业务特点**:账号层级变化不频繁
|
||||
3. **可配置**:可根据业务需求调整
|
||||
Reference in New Issue
Block a user