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>
This commit is contained in:
2025-11-18 16:44:06 +08:00
parent e8eb5766cb
commit eaa70ac255
86 changed files with 15395 additions and 245 deletions

View File

@@ -0,0 +1,602 @@
# 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)
}
// 递归查询用户 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]
}
```
运行示例:
```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)
}
// 模拟当前用户为 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)
}
}
```
运行示例:
```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 <token>" \
-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 <user_b_token>"
```
**预期响应**(只返回 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 <token>" \
-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 或联系团队
**祝你开发顺利!** 🚀