Files
junhong_cmp_fiber/specs/004-rbac-data-permission/quickstart.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

14 KiB
Raw Blame History

Quick Start: RBAC 表结构与 GORM 数据权限过滤

Feature: 004-rbac-data-permission Date: 2025-11-18 Estimated Time: 2-3 小时(阅读 + 环境准备 + 运行示例)

概述

本快速指南帮助你在 30 分钟内理解 RBAC 权限系统和数据权限过滤机制,并在 2 小时内完成环境准备和运行第一个示例。

核心功能:

  1. RBAC 权限系统:账号、角色、权限的多对多关联
  2. 数据权限过滤:基于 owner_id + shop_id 的自动数据隔离
  3. 递归查询:使用 PostgreSQL WITH RECURSIVE 查询用户的所有下级
  4. Redis 缓存:缓存下级 ID 列表,提升性能

前置条件

必需环境

  • Go: 1.25.4+
  • PostgreSQL: 14+
  • Redis: 6.0+
  • golang-migrate: v4.x数据库迁移工具

环境检查

# 检查 Go 版本
go version  # 应该显示 go1.25.4 或更高

# 检查 PostgreSQL
psql --version  # 应该显示 14.x 或更高

# 检查 Redis
redis-cli --version  # 应该显示 6.x 或更高

# 安装 golang-migrate如果未安装
brew install golang-migrate  # macOS
# 或
go install -tags 'postgres' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

第一步理解核心概念10 分钟)

1. RBAC 数据模型

tb_account (账号表)
    ├── parent_id → tb_account.id (自关联,层级关系)
    ├── tb_account_role.account_id (多对多关联)
    │   └── tb_account_role.role_id → tb_role.id
    │       └── tb_role_permission.role_id → tb_role.id
    │           └── tb_role_permission.perm_id → tb_permission.id
    │
    └── owner_id (业务表数据归属)
        ├── tb_user.owner_id
        ├── tb_order.owner_id
        └── tb_data_transfer_log.old_owner_id / new_owner_id

关键原则:

  • 禁止外键约束Foreign Key Constraints
  • 禁止 GORM 关联标签(foreignKeyhasManybelongsTo 等)
  • 通过 ID 字段手动维护关联
  • 所有表支持软删除(deleted_at 字段)

2. 数据权限过滤机制

过滤条件:

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

示例场景:

假设用户层级关系为A(root) → B(平台) → C(代理)

  • 用户 A 查询返回所有数据root 用户跳过过滤)
  • 用户 B 查询:返回 owner_id IN (2, 3) AND shop_id = 10 的数据B 和 C 的数据)
  • 用户 C 查询:返回 owner_id = 3 AND shop_id = 10 的数据(只有 C 的数据)

实现方式:

// GORM Scopes 自动应用过滤
query := db.WithContext(ctx).Scopes(DataPermissionScope(accountStore))

3. 递归查询下级 ID

使用 PostgreSQL WITH RECURSIVE 查询所有下级(包含软删除账号):

WITH RECURSIVE subordinates AS (
    -- 基础查询:选择当前账号
    SELECT id FROM tb_account WHERE id = ? AND deleted_at IS NULL

    UNION ALL

    -- 递归查询:选择所有下级(包括软删除的账号)
    SELECT a.id
    FROM tb_account a
    INNER JOIN subordinates s ON a.parent_id = s.id
)
SELECT id FROM subordinates WHERE id != ?

缓存优化:

  • Redis Key: account:subordinates:{账号ID}
  • 过期时间: 30 分钟
  • 清除时机: 账号创建/删除时主动清除

第二步数据库准备20 分钟)

1. 创建数据库

# 连接到 PostgreSQL
psql -U postgres

# 创建数据库
CREATE DATABASE junhong_cmp_fiber;

# 退出
\q

2. 运行数据库迁移

# 进入项目目录
cd /Users/break/csxjProject/junhong_cmp_fiber

# 运行迁移(创建 5 个 RBAC 表)
migrate -path migrations -database "postgresql://postgres:password@localhost:5432/junhong_cmp_fiber?sslmode=disable" up

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

预期输出:

                 List of relations
 Schema |         Name          | Type  |  Owner
--------+-----------------------+-------+----------
 public | tb_account            | table | postgres
 public | tb_account_role       | table | postgres
 public | tb_data_transfer_log  | table | postgres
 public | tb_permission         | table | postgres
 public | tb_role               | table | postgres
 public | tb_role_permission    | table | postgres
 public | tb_user               | table | postgres
 public | tb_order              | table | postgres

3. 初始化测试数据

-- 连接到数据库
psql -U postgres -d junhong_cmp_fiber

-- 创建 root 账号
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$...', 1, NULL, NULL, 1, 1, 1, NOW(), NOW());

-- 创建平台账号 B上级为 root
INSERT INTO tb_account (username, phone, password, user_type, shop_id, parent_id, status, creator, updater, created_at, updated_at)
VALUES ('platform_user', '13800000001', '$2a$10$...', 2, 10, 1, 1, 1, 1, NOW(), NOW());

-- 创建代理账号 C上级为 B
INSERT INTO tb_account (username, phone, password, user_type, shop_id, parent_id, status, creator, updater, created_at, updated_at)
VALUES ('agent_user', '13800000002', '$2a$10$...', 3, 10, 2, 1, 2, 2, NOW(), NOW());

-- 创建超级角色
INSERT INTO tb_role (role_name, role_desc, role_type, status, creator, updater, created_at, updated_at)
VALUES ('超级管理员', '系统超级管理员', 1, 1, 1, 1, NOW(), NOW());

-- 创建权限
INSERT INTO tb_permission (perm_name, perm_code, perm_type, url, parent_id, sort, status, creator, updater, created_at, updated_at)
VALUES ('用户管理', 'user:manage', 1, '/admin/users', NULL, 1, 1, 1, 1, NOW(), NOW());

-- 为账号分配角色
INSERT INTO tb_account_role (account_id, role_id, status, creator, updater, created_at, updated_at)
VALUES (1, 1, 1, 1, 1, NOW(), NOW());

-- 为角色分配权限
INSERT INTO tb_role_permission (role_id, perm_id, status, creator, updater, created_at, updated_at)
VALUES (1, 1, 1, 1, 1, NOW(), NOW());

第三步Redis 准备5 分钟)

启动 Redis

# 启动 Redis 服务
redis-server

# 或使用 Homebrew 启动macOS
brew services start redis

# 验证连接
redis-cli ping  # 应该返回 PONG

配置 Redis 连接

确保 config/config.yaml 中配置正确:

redis:
  addr: localhost:6379
  password: ""
  db: 0
  pool_size: 10
  min_idle_conns: 5

第四步运行示例30 分钟)

1. 递归查询下级 ID 示例

创建测试文件 examples/recursive_query.go

package main

import (
    "context"
    "fmt"
    "log"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

func main() {
    // 连接数据库
    dsn := "host=localhost user=postgres password=password dbname=junhong_cmp_fiber port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    // 递归查询用户 BID=2的所有下级
    ctx := context.Background()
    accountID := uint(2)

    query := `
        WITH RECURSIVE subordinates AS (
            SELECT id FROM tb_account WHERE id = ? AND deleted_at IS NULL
            UNION ALL
            SELECT a.id FROM tb_account a
            INNER JOIN subordinates s ON a.parent_id = s.id
        )
        SELECT id FROM subordinates WHERE id != ?
    `

    var subordinateIDs []uint
    if err := db.WithContext(ctx).Raw(query, accountID, accountID).Scan(&subordinateIDs).Error; err != nil {
        log.Fatal(err)
    }

    // 包含当前用户自己的 ID
    allIDs := append([]uint{accountID}, subordinateIDs...)

    fmt.Printf("用户 %d 的所有下级 ID包含自己: %v\n", accountID, allIDs)
    // 预期输出:用户 2 的所有下级 ID包含自己: [2 3]
}

运行示例:

go run examples/recursive_query.go

2. 数据权限过滤示例

创建测试文件 examples/data_filter.go

package main

import (
    "context"
    "fmt"
    "log"

    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type User struct {
    ID      uint   `gorm:"primarykey"`
    Name    string
    OwnerID *uint `gorm:"index"`
    ShopID  *uint `gorm:"index"`
}

func main() {
    // 连接数据库
    dsn := "host=localhost user=postgres password=password dbname=junhong_cmp_fiber port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatal(err)
    }

    // 模拟当前用户为 BID=2下级为 [2, 3]shop_id=10
    ctx := context.Background()
    subordinateIDs := []uint{2, 3}
    shopID := uint(10)

    // 应用数据权限过滤
    var users []User
    query := db.WithContext(ctx).
        Where("owner_id IN ? AND shop_id = ?", subordinateIDs, shopID).
        Find(&users)

    if query.Error != nil {
        log.Fatal(query.Error)
    }

    fmt.Printf("用户 B 可访问的数据(%d 条):\n", len(users))
    for _, user := range users {
        fmt.Printf("  - ID: %d, Name: %s, OwnerID: %d, ShopID: %d\n",
            user.ID, user.Name, *user.OwnerID, *user.ShopID)
    }
}

运行示例:

go run examples/data_filter.go

3. Redis 缓存示例

创建测试文件 examples/redis_cache.go

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/bytedance/sonic"
    "github.com/redis/go-redis/v9"
)

func main() {
    // 连接 Redis
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "",
        DB:       0,
    })

    ctx := context.Background()

    // 缓存下级 ID 列表
    accountID := uint(2)
    subordinateIDs := []uint{2, 3}

    cacheKey := fmt.Sprintf("account:subordinates:%d", accountID)
    data, _ := sonic.Marshal(subordinateIDs)

    // 写入缓存30 分钟过期)
    if err := rdb.Set(ctx, cacheKey, data, 30*time.Minute).Err(); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("已缓存下级 ID 列表到 Redis: %s\n", cacheKey)

    // 从缓存读取
    cached, err := rdb.Get(ctx, cacheKey).Result()
    if err != nil {
        log.Fatal(err)
    }

    var cachedIDs []uint
    if err := sonic.Unmarshal([]byte(cached), &cachedIDs); err != nil {
        log.Fatal(err)
    }

    fmt.Printf("从缓存读取的下级 ID: %v\n", cachedIDs)
    // 预期输出:从缓存读取的下级 ID: [2 3]
}

运行示例:

go run examples/redis_cache.go

第五步API 测试30 分钟)

1. 启动 API 服务

# 确保数据库和 Redis 已启动

# 启动 API 服务
go run cmd/api/main.go

预期输出:

2025-11-18T10:00:00.000Z  INFO  服务启动  {"addr": "localhost:8080"}

2. 测试账号创建

# 使用 curl 创建账号(需要先登录获取 token
curl -X POST http://localhost:8080/api/v1/accounts \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "username": "test_user",
    "phone": "13900000001",
    "password": "Password123",
    "user_type": 3,
    "shop_id": 10,
    "parent_id": 2
  }'

预期响应:

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

3. 测试数据权限过滤

# 使用用户 B 的 token 查询账号列表
curl -X GET "http://localhost:8080/api/v1/accounts?page=1&page_size=20" \
  -H "Authorization: Bearer <user_b_token>"

预期响应(只返回 B 和 C 的账号):

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

4. 测试角色分配

# 为账号分配角色
curl -X POST http://localhost:8080/api/v1/accounts/3/roles \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <token>" \
  -d '{
    "role_ids": [1, 2]
  }'

预期响应:

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

常见问题FAQ

Q1: 递归查询性能问题?

A: 使用 Redis 缓存优化,缓存命中率应 > 90%。如果层级深度超过 10 层建议使用闭包表Closure Table替代。

Q2: 软删除账号的数据如何处理?

A: 软删除账号后,该账号的数据对上级仍然可见(递归查询下级 ID 包含已删除账号)。

Q3: 如何跳过数据权限过滤?

A: 在 Store 方法调用时传入 WithoutDataFilter 选项:

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

Q4: 如何清除 Redis 缓存?

A: 账号创建/删除时自动清除,也可以手动清除:

redis-cli DEL account:subordinates:2

Q5: 密码应该使用 MD5 还是 bcrypt

A: 强烈建议使用 bcrypt。MD5 已被废弃易受彩虹表攻击。bcrypt 是行业标准,内置盐值,抗暴力破解。


下一步

  1. 阅读详细设计: 查看 data-model.md 了解完整的数据库设计
  2. 查看 API 文档: 查看 contracts/ 目录的 OpenAPI 规范
  3. 阅读实现任务: 查看 tasks.md 了解完整的实现任务清单
  4. 开始实现: 按照 Phase 1 → Phase 2 → ... 的顺序完成任务

联系和反馈

如果遇到问题或有建议,请:

  1. 检查 research.md 中的技术决策
  2. 查看 spec.md 中的功能需求
  3. 提交 GitHub Issue 或联系团队

祝你开发顺利! 🚀