# 使用指南：RBAC 表结构与 GORM 数据权限过滤

**功能编号**: 004-rbac-data-permission
**适用版本**: v1.0.0
**更新日期**: 2025-11-18

## 目录

1. [快速开始](#快速开始)
2. [账号管理](#账号管理)
3. [角色管理](#角色管理)
4. [权限管理](#权限管理)
5. [数据权限过滤](#数据权限过滤)
6. [业务表集成指南](#业务表集成指南)
7. [常见问题](#常见问题)
8. [最佳实践](#最佳实践)

---

## 快速开始

### 环境要求

- Go 1.25.4+
- PostgreSQL 14+
- Redis 6.0+
- golang-migrate v4.x

### 数据库初始化

```bash
# 运行数据库迁移
migrate -path migrations -database "postgresql://postgres:password@localhost:5432/junhong_cmp_fiber?sslmode=disable" up

# 验证表创建
psql -U postgres -d junhong_cmp_fiber -c "\dt"
```

### 创建 root 账号

```sql
-- 使用 bcrypt 哈希密码（Password123）
INSERT INTO tb_account (username, phone, password, user_type, shop_id, parent_id, status, creator, updater, created_at, updated_at)
VALUES ('root', '13800000000', '$2a$10$N9qo8uLOickgx2ZMRZoMye1P7Z.mKAeQ7pjSeG7gYDobOAZCnOMUa', 1, NULL, NULL, 1, 1, 1, NOW(), NOW());
```

---

## 账号管理

### 1. 创建账号

**API 端点**: `POST /api/v1/accounts`

**请求示例**:

```bash
curl -X POST http://localhost:8080/api/v1/accounts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "username": "platform_user",
    "phone": "13900000001",
    "password": "Password123",
    "user_type": 2,
    "shop_id": 10,
    "parent_id": 1
  }'
```

**参数说明**:

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| username | string | 是 | 用户名（3-20 个字符，字母/数字/下划线） |
| phone | string | 是 | 手机号（11 位中国大陆手机号） |
| password | string | 是 | 密码（最少 8 位，包含字母和数字） |
| user_type | int | 是 | 用户类型：1=root, 2=平台, 3=代理, 4=企业 |
| shop_id | int | 条件 | 所属店铺 ID（user_type=1 时可为空） |
| parent_id | int | 条件 | 上级账号 ID（user_type=1 时可为空） |

**响应示例**:

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "id": 2,
    "username": "platform_user",
    "phone": "13900000001",
    "user_type": 2,
    "shop_id": 10,
    "parent_id": 1,
    "status": 1,
    "created_at": "2025-11-18T10:00:00Z",
    "updated_at": "2025-11-18T10:00:00Z"
  },
  "timestamp": "2025-11-18T10:00:00Z"
}
```

**注意事项**:

- ✅ 密码会自动使用 bcrypt 加密存储
- ✅ username 和 phone 必须唯一（软删除后可重复使用）
- ✅ 非 root 用户必须提供 parent_id
- ✅ 创建成功后会自动清除父账号的下级 ID 缓存

### 2. 获取账号详情

**API 端点**: `GET /api/v1/accounts/:id`

**请求示例**:

```bash
curl -X GET http://localhost:8080/api/v1/accounts/2 \
  -H "Authorization: Bearer <token>"
```

**响应示例**:

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "id": 2,
    "username": "platform_user",
    "phone": "13900000001",
    "user_type": 2,
    "shop_id": 10,
    "parent_id": 1,
    "status": 1,
    "created_at": "2025-11-18T10:00:00Z",
    "updated_at": "2025-11-18T10:00:00Z"
  },
  "timestamp": "2025-11-18T10:00:00Z"
}
```

**注意**: password 字段不会返回给客户端（已通过 `json:"-"` 标签隐藏）

### 3. 更新账号

**API 端点**: `PUT /api/v1/accounts/:id`

**请求示例**:

```bash
curl -X PUT http://localhost:8080/api/v1/accounts/2 \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "username": "new_username",
    "phone": "13900000002",
    "status": 1
  }'
```

**注意事项**:

- ❌ **禁止修改**: user_type, parent_id（创建后不可更改）
- ✅ **可选修改**: username, phone, status

### 4. 删除账号（软删除）

**API 端点**: `DELETE /api/v1/accounts/:id`

**请求示例**:

```bash
curl -X DELETE http://localhost:8080/api/v1/accounts/2 \
  -H "Authorization: Bearer <token>"
```

**响应示例**:

```json
{
  "code": 0,
  "message": "success",
  "data": null,
  "timestamp": "2025-11-18T10:00:00Z"
}
```

**注意事项**:

- ✅ 软删除：只设置 `deleted_at` 字段，数据仍保留
- ✅ 软删除后，username 和 phone 可以被重新使用
- ✅ 删除成功后会递归清除所有上级账号的下级 ID 缓存
- ⚠️ 软删除账号的数据对上级仍然可见（递归查询包含已删除账号）

### 5. 获取账号列表

**API 端点**: `GET /api/v1/accounts`

**请求示例**:

```bash
curl -X GET "http://localhost:8080/api/v1/accounts?page=1&page_size=20" \
  -H "Authorization: Bearer <token>"
```

**查询参数**:

| 参数 | 类型 | 必填 | 默认值 | 说明 |
|------|------|------|--------|------|
| page | int | 否 | 1 | 页码 |
| page_size | int | 否 | 20 | 每页数量（最大 100） |

**响应示例**:

```json
{
  "code": 0,
  "message": "success",
  "data": {
    "items": [
      {
        "id": 2,
        "username": "platform_user",
        "user_type": 2,
        "shop_id": 10,
        "parent_id": 1,
        "status": 1
      }
    ],
    "total": 1,
    "page": 1,
    "page_size": 20
  },
  "timestamp": "2025-11-18T10:00:00Z"
}
```

### 6. 为账号分配角色

**API 端点**: `POST /api/v1/accounts/:id/roles`

**请求示例**:

```bash
curl -X POST http://localhost:8080/api/v1/accounts/2/roles \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "role_ids": [1, 2]
  }'
```

**响应示例**:

```json
{
  "code": 0,
  "message": "success",
  "data": [
    {
      "id": 1,
      "account_id": 2,
      "role_id": 1,
      "status": 1,
      "created_at": "2025-11-18T10:00:00Z"
    },
    {
      "id": 2,
      "account_id": 2,
      "role_id": 2,
      "status": 1,
      "created_at": "2025-11-18T10:00:00Z"
    }
  ],
  "timestamp": "2025-11-18T10:00:00Z"
}
```

### 7. 获取账号的角色列表

**API 端点**: `GET /api/v1/accounts/:id/roles`

**请求示例**:

```bash
curl -X GET http://localhost:8080/api/v1/accounts/2/roles \
  -H "Authorization: Bearer <token>"
```

### 8. 移除账号的角色

**API 端点**: `DELETE /api/v1/accounts/:account_id/roles/:role_id`

**请求示例**:

```bash
curl -X DELETE http://localhost:8080/api/v1/accounts/2/roles/1 \
  -H "Authorization: Bearer <token>"
```

---

## 角色管理

### 1. 创建角色

**API 端点**: `POST /api/v1/roles`

**请求示例**:

```bash
curl -X POST http://localhost:8080/api/v1/roles \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "role_name": "超级管理员",
    "role_desc": "系统超级管理员",
    "role_type": 1
  }'
```

**参数说明**:

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| role_name | string | 是 | 角色名称（长度 ≤50） |
| role_desc | string | 否 | 角色描述（长度 ≤255） |
| role_type | int | 是 | 角色类型：1=超级, 2=代理, 3=企业 |

### 2. 为角色分配权限

**API 端点**: `POST /api/v1/roles/:id/permissions`

**请求示例**:

```bash
curl -X POST http://localhost:8080/api/v1/roles/1/permissions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "perm_ids": [1, 2, 3]
  }'
```

### 3. 获取角色的权限列表

**API 端点**: `GET /api/v1/roles/:id/permissions`

**请求示例**:

```bash
curl -X GET http://localhost:8080/api/v1/roles/1/permissions \
  -H "Authorization: Bearer <token>"
```

---

## 权限管理

### 1. 创建权限

**API 端点**: `POST /api/v1/permissions`

**请求示例**:

```bash
curl -X POST http://localhost:8080/api/v1/permissions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "perm_name": "用户管理",
    "perm_code": "user:manage",
    "perm_type": 1,
    "url": "/admin/users",
    "parent_id": null,
    "sort": 1
  }'
```

**参数说明**:

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| perm_name | string | 是 | 权限名称（长度 ≤50） |
| perm_code | string | 是 | 权限编码（格式：`module:action`，如 `user:create`） |
| perm_type | int | 是 | 权限类型：1=菜单, 2=按钮 |
| url | string | 否 | URL 路径（长度 ≤255） |
| parent_id | int | 否 | 上级权限 ID（支持层级） |
| sort | int | 否 | 排序序号（默认 0） |

**注意事项**:

- ✅ perm_code 必须唯一（软删除后可重复使用）
- ✅ 支持层级权限（通过 parent_id 构建权限树）

---

## 数据权限过滤

### 核心概念

数据权限过滤机制确保每个用户只能访问自己及下级的数据，通过 `owner_id` 和 `shop_id` 双重过滤实现多租户数据隔离。

### 过滤规则

```sql
WHERE owner_id IN (当前用户及所有下级的ID列表) AND shop_id = 当前用户的shop_id
```

### 示例场景

假设用户层级关系为：A(root, ID=1) → B(平台, ID=2) → C(代理, ID=3)

- **用户 A 查询**：返回所有数据（root 用户跳过过滤）
- **用户 B 查询**：返回 `owner_id IN (2, 3) AND shop_id = 10` 的数据
- **用户 C 查询**：返回 `owner_id = 3 AND shop_id = 10` 的数据

### 递归查询下级 ID

系统使用 PostgreSQL WITH RECURSIVE 查询所有下级 ID，并通过 Redis 缓存优化性能：

- **缓存 Key**: `account:subordinates:{账号ID}`
- **过期时间**: 30 分钟
- **清除时机**: 账号创建/删除时主动清除

### 跳过数据权限过滤

某些特殊场景（如 C 端业务用户、系统任务）需要跳过数据权限过滤：

**方式 1：在 Store 层使用 WithoutDataFilter 选项**

```go
users, err := userStore.List(ctx, &store.QueryOptions{
    WithoutDataFilter: true,
})
```

**方式 2：root 用户自动跳过过滤**

root 用户（user_type=1）的所有查询会自动跳过数据权限过滤。

---

## 业务表集成指南

### 步骤 1：添加数据权限字段

为业务表添加 `owner_id` 和 `shop_id` 字段：

**数据库迁移**:

```sql
-- migrations/000004_add_owner_id_to_business_table.up.sql

ALTER TABLE tb_your_table ADD COLUMN owner_id INTEGER;
ALTER TABLE tb_your_table ADD COLUMN shop_id INTEGER;

CREATE INDEX idx_your_table_owner_id ON tb_your_table(owner_id);
CREATE INDEX idx_your_table_shop_id ON tb_your_table(shop_id);
```

**GORM 模型更新**:

```go
// internal/model/your_model.go

type YourModel struct {
    ID        uint           `gorm:"primarykey" json:"id"`
    // ... 其他字段 ...
    OwnerID   *uint          `gorm:"index" json:"owner_id,omitempty"` // 新增
    ShopID    *uint          `gorm:"index" json:"shop_id,omitempty"`  // 新增
    CreatedAt time.Time      `gorm:"not null" json:"created_at"`
    UpdatedAt time.Time      `gorm:"not null" json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}
```

### 步骤 2：在 Store 层应用数据权限过滤

```go
// internal/store/postgres/your_store.go

import (
    "context"
    "your-project/internal/model"
    "your-project/internal/store"
    "gorm.io/gorm"
)

type YourStore struct {
    db           *gorm.DB
    accountStore *AccountStore // 注入 AccountStore（用于递归查询）
}

func (s *YourStore) List(ctx context.Context, opts *store.QueryOptions) ([]*model.YourModel, error) {
    query := s.db.WithContext(ctx)

    // 应用数据权限过滤（如果未禁用）
    if !opts.WithoutDataFilter {
        query = query.Scopes(DataPermissionScope(s.accountStore))
    }

    var items []*model.YourModel
    if err := query.Find(&items).Error; err != nil {
        return nil, err
    }

    return items, nil
}

func (s *YourStore) GetByID(ctx context.Context, id uint, opts *store.QueryOptions) (*model.YourModel, error) {
    query := s.db.WithContext(ctx)

    // 应用数据权限过滤（如果未禁用）
    if !opts.WithoutDataFilter {
        query = query.Scopes(DataPermissionScope(s.accountStore))
    }

    var item model.YourModel
    if err := query.First(&item, id).Error; err != nil {
        return nil, err
    }

    return &item, nil
}
```

### 步骤 3：在 Service 层设置 owner_id 和 shop_id

```go
// internal/service/your_service/service.go

import (
    "context"
    "your-project/internal/model"
    "your-project/pkg/middleware"
)

func (s *Service) Create(ctx context.Context, req *CreateRequest) (*model.YourModel, error) {
    // 从 context 提取当前用户信息
    userID := middleware.GetUserIDFromContext(ctx)
    shopID := middleware.GetShopIDFromContext(ctx)

    item := &model.YourModel{
        // ... 其他字段 ...
        OwnerID: &userID, // 设置为当前用户 ID
        ShopID:  &shopID, // 设置为当前用户的 shop_id
    }

    if err := s.store.Create(ctx, item); err != nil {
        return nil, err
    }

    return item, nil
}
```

### 步骤 4：验证数据权限过滤

创建测试数据并验证不同用户的查询结果：

```sql
-- 创建测试数据
INSERT INTO tb_your_table (name, owner_id, shop_id, created_at, updated_at)
VALUES
    ('数据A - 用户B创建', 2, 10, NOW(), NOW()),
    ('数据B - 用户C创建', 3, 10, NOW(), NOW()),
    ('数据C - 其他店铺', 2, 20, NOW(), NOW());
```

**预期查询结果**:

- **用户 A（root）**: 返回所有 3 条数据
- **用户 B（平台）**: 返回 2 条数据（owner_id=2 或 3，且 shop_id=10）
- **用户 C（代理）**: 返回 1 条数据（owner_id=3，且 shop_id=10）

---

## 常见问题

### Q1: 如何验证密码？

**A**: 使用 bcrypt 验证密码：

```go
import "golang.org/x/crypto/bcrypt"

func ValidatePassword(plainPassword, hashedPassword string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
    return err == nil
}
```

### Q2: 递归查询性能问题？

**A**: 系统已通过 Redis 缓存优化，缓存命中率预期 > 90%。如果层级深度超过 10 层，建议：

- 监控递归查询耗时（P95 应 < 50ms）
- 考虑使用闭包表（Closure Table）替代递归查询

### Q3: 软删除账号的数据如何处理？

**A**: 软删除账号后：

- ✅ 该账号的数据对上级仍然可见（递归查询包含已删除账号）
- ✅ username 和 phone 可以被重新使用
- ✅ 所有上级的下级 ID 缓存会被清除

### Q4: 如何清除 Redis 缓存？

**A**: 账号创建/删除时会自动清除缓存，也可以手动清除：

```bash
# 清除指定账号的下级 ID 缓存
redis-cli DEL account:subordinates:2

# 清除所有下级 ID 缓存
redis-cli KEYS "account:subordinates:*" | xargs redis-cli DEL
```

### Q5: 如何为 C 端业务用户实现数据过滤？

**A**: C 端业务用户通常不使用 owner_id 过滤，而是基于业务字段（如 iccid/device_id）：

```go
// 使用 WithoutDataFilter 跳过 owner_id 过滤
users, err := userStore.List(ctx, &store.QueryOptions{
    WithoutDataFilter: true,
})

// 在 Service 层应用业务字段过滤
filteredUsers := filterByICCID(users, targetICCID)
```

### Q6: 如何处理跨店铺查询？

**A**: 数据权限过滤强制 `shop_id = 当前用户的shop_id`，不支持跨店铺查询。如果需要跨店铺查询：

- 方式 1：使用 root 用户（自动跳过过滤）
- 方式 2：使用 `WithoutDataFilter` 选项（需要在业务层额外校验权限）

---

## 最佳实践

### 1. 创建账号时的层级关系

✅ **推荐**：只有本级账号能创建下级账号

```
A(root) 创建 B(平台)
B(平台) 创建 C(代理)
C(代理) 创建 D(企业)
```

❌ **不推荐**：跨级创建（A 直接创建 C）

### 2. 数据归属设置

✅ **推荐**：创建数据时 owner_id 设置为当前用户 ID

```go
item.OwnerID = &userID  // 当前用户 ID
item.ShopID = &shopID   // 当前用户的 shop_id
```

❌ **不推荐**：owner_id 设置为其他用户 ID（除非是数据转移场景）

### 3. 缓存管理

✅ **推荐**：依赖自动缓存清除机制

- 账号创建时自动清除父账号缓存
- 账号删除时递归清除所有上级缓存

❌ **不推荐**：手动清除缓存（除非调试或紧急修复）

### 4. 错误处理

✅ **推荐**：使用统一错误处理机制

```go
import "your-project/pkg/errors"

if account == nil {
    return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
}
```

❌ **不推荐**：手动构造错误响应

```go
// ❌ 不推荐
return c.Status(404).JSON(fiber.Map{"error": "账号不存在"})
```

### 5. 安全性

✅ **推荐**：

- 使用 bcrypt 加密密码
- password 字段使用 `json:"-"` 隐藏
- 验证用户权限后再执行敏感操作

❌ **不推荐**：

- 使用 MD5 加密密码（已废弃）
- 返回 password 字段给客户端
- 跳过权限校验

---

## 相关文档

- **功能总结**: [功能总结.md](./功能总结.md)
- **快速入门**: [specs/004-rbac-data-permission/quickstart.md](../../specs/004-rbac-data-permission/quickstart.md)
- **数据模型**: [specs/004-rbac-data-permission/data-model.md](../../specs/004-rbac-data-permission/data-model.md)
- **API 文档**: [specs/004-rbac-data-permission/contracts/](../../specs/004-rbac-data-permission/contracts/)

---

**更新日期**: 2025-11-18
**维护者**: AI Assistant (Claude)
