Files
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

603 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 或联系团队
**祝你开发顺利!** 🚀