主要功能: - 实现完整的 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>
8.1 KiB
8.1 KiB
架构说明: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 工作原理
- 从 Context 提取用户 ID、用户类型、店铺 ID
- 检查是否为 root 用户(跳过过滤)
- 递归查询当前用户的所有下级 ID(含自己)
- 应用 WHERE 条件:
owner_id IN (...) AND shop_id = ?
递归查询优化
使用 PostgreSQL 的 WITH RECURSIVE 进行递归查询:
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 上下文传递
上下文键
const (
UserIDKey = "user_id"
UserTypeKey = "user_type"
ShopIDKey = "shop_id"
)
辅助函数
SetUserContext(ctx, userID, userType, shopID)- 设置用户上下文GetUserIDFromContext(ctx)- 获取用户 IDGetShopIDFromContext(ctx)- 获取店铺 IDIsRootUser(ctx)- 检查是否为 root 用户
路由模块化
目录结构
internal/routes/
├── routes.go # 主入口,Services 容器
├── account.go # 账号路由
├── role.go # 角色路由
├── permission.go # 权限路由
├── health.go # 健康检查路由
└── task.go # 任务路由
Services 容器
type Services struct {
AccountHandler *handler.AccountHandler
RoleHandler *handler.RoleHandler
PermissionHandler *handler.PermissionHandler
}
主函数重构
编排模式
main 函数仅做编排,不包含具体实现:
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
优化措施
- Redis 缓存:缓存递归查询结果,避免重复数据库查询
- 索引优化:关键字段都建立索引(
parent_id、shop_id、owner_id) - 批量操作:账号-角色、角色-权限支持批量创建
- 连接池:数据库和 Redis 配置合理的连接池
安全考量
数据隔离
- 店铺隔离:通过
shop_id实现多租户数据隔离 - 层级权限:用户只能访问自己及下级创建的数据
- root 用户:跳过数据权限过滤,可访问所有数据
密码安全
- 密码字段使用
json:"-"标签,不返回给客户端 - 密码使用 bcrypt 加密存储
错误处理
- 查询下级 ID 失败时,降级为只返回自己的数据
- 所有错误通过统一错误处理机制返回
扩展指南
添加新业务表的数据权限
- 在业务表中添加
owner_id和shop_id字段 - 在 Store 方法中应用
DataPermissionScope - 确保创建记录时设置正确的
owner_id和shop_id
示例:
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 实体
- 创建 Model 和 DTO
- 创建 Store(CRUD 方法)
- 创建 Service(业务逻辑)
- 创建 Handler(HTTP 接口)
- 添加路由文件
- 更新 Services 容器
测试策略
单元测试
- 递归查询测试
- 缓存读写测试
- 数据权限 Scope 测试
- 软删除测试
集成测试
- 数据库迁移测试
- 层级数据权限过滤测试
- 跨店铺数据隔离测试
- API 端点测试
技术决策记录
为什么不使用外键?
- 灵活性:业务逻辑完全在代码中控制
- 性能:无外键约束检查开销
- 分布式友好:便于未来拆分微服务
为什么使用 WITH RECURSIVE?
- 原生支持:PostgreSQL 内置支持
- 性能优异:单次查询获取所有下级
- 深度无限:支持任意层级的递归
为什么缓存过期时间是 30 分钟?
- 平衡性:在实时性和性能之间取得平衡
- 业务特点:账号层级变化不频繁
- 可配置:可根据业务需求调整