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