Files
junhong_cmp_fiber/docs/004-rbac-data-permission/架构说明.md
huang eaa70ac255 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>
2025-11-18 16:44:06 +08:00

265 lines
8.1 KiB
Markdown
Raw 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.
# 架构说明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. **可配置**:可根据业务需求调整