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