# 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(数据库迁移工具) ### 环境检查 ```bash # 检查 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 关联标签(`foreignKey`、`hasMany`、`belongsTo` 等) - ✅ 通过 ID 字段手动维护关联 - ✅ 所有表支持软删除(`deleted_at` 字段) ### 2. 数据权限过滤机制 **过滤条件**: ```sql 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 的数据) **实现方式**: ```go // GORM Scopes 自动应用过滤 query := db.WithContext(ctx).Scopes(DataPermissionScope(accountStore)) ``` ### 3. 递归查询下级 ID 使用 **PostgreSQL WITH RECURSIVE** 查询所有下级(包含软删除账号): ```sql 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. 创建数据库 ```bash # 连接到 PostgreSQL psql -U postgres # 创建数据库 CREATE DATABASE junhong_cmp_fiber; # 退出 \q ``` ### 2. 运行数据库迁移 ```bash # 进入项目目录 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. 初始化测试数据 ```sql -- 连接到数据库 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 ```bash # 启动 Redis 服务 redis-server # 或使用 Homebrew 启动(macOS) brew services start redis # 验证连接 redis-cli ping # 应该返回 PONG ``` ### 配置 Redis 连接 确保 `config/config.yaml` 中配置正确: ```yaml redis: addr: localhost:6379 password: "" db: 0 pool_size: 10 min_idle_conns: 5 ``` --- ## 第四步:运行示例(30 分钟) ### 1. 递归查询下级 ID 示例 创建测试文件 `examples/recursive_query.go`: ```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) } // 递归查询用户 B(ID=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] } ``` 运行示例: ```bash go run examples/recursive_query.go ``` ### 2. 数据权限过滤示例 创建测试文件 `examples/data_filter.go`: ```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) } // 模拟当前用户为 B(ID=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) } } ``` 运行示例: ```bash go run examples/data_filter.go ``` ### 3. Redis 缓存示例 创建测试文件 `examples/redis_cache.go`: ```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] } ``` 运行示例: ```bash go run examples/redis_cache.go ``` --- ## 第五步:API 测试(30 分钟) ### 1. 启动 API 服务 ```bash # 确保数据库和 Redis 已启动 # 启动 API 服务 go run cmd/api/main.go ``` **预期输出**: ``` 2025-11-18T10:00:00.000Z INFO 服务启动 {"addr": "localhost:8080"} ``` ### 2. 测试账号创建 ```bash # 使用 curl 创建账号(需要先登录获取 token) curl -X POST http://localhost:8080/api/v1/accounts \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "username": "test_user", "phone": "13900000001", "password": "Password123", "user_type": 3, "shop_id": 10, "parent_id": 2 }' ``` **预期响应**: ```json { "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. 测试数据权限过滤 ```bash # 使用用户 B 的 token 查询账号列表 curl -X GET "http://localhost:8080/api/v1/accounts?page=1&page_size=20" \ -H "Authorization: Bearer " ``` **预期响应**(只返回 B 和 C 的账号): ```json { "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. 测试角色分配 ```bash # 为账号分配角色 curl -X POST http://localhost:8080/api/v1/accounts/3/roles \ -H "Content-Type: application/json" \ -H "Authorization: Bearer " \ -d '{ "role_ids": [1, 2] }' ``` **预期响应**: ```json { "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` 选项: ```go users, err := store.List(ctx, &store.QueryOptions{ WithoutDataFilter: true, }) ``` ### Q4: 如何清除 Redis 缓存? **A**: 账号创建/删除时自动清除,也可以手动清除: ```bash redis-cli DEL account:subordinates:2 ``` ### Q5: 密码应该使用 MD5 还是 bcrypt? **A**: **强烈建议使用 bcrypt**。MD5 已被废弃,易受彩虹表攻击。bcrypt 是行业标准,内置盐值,抗暴力破解。 --- ## 下一步 1. **阅读详细设计**: 查看 [data-model.md](./data-model.md) 了解完整的数据库设计 2. **查看 API 文档**: 查看 [contracts/](./contracts/) 目录的 OpenAPI 规范 3. **阅读实现任务**: 查看 [tasks.md](./tasks.md) 了解完整的实现任务清单 4. **开始实现**: 按照 Phase 1 → Phase 2 → ... 的顺序完成任务 --- ## 联系和反馈 如果遇到问题或有建议,请: 1. 检查 [research.md](./research.md) 中的技术决策 2. 查看 [spec.md](./spec.md) 中的功能需求 3. 提交 GitHub Issue 或联系团队 **祝你开发顺利!** 🚀