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:
602
specs/004-rbac-data-permission/quickstart.md
Normal file
602
specs/004-rbac-data-permission/quickstart.md
Normal 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)
|
||||
}
|
||||
|
||||
// 递归查询用户 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 <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 或联系团队
|
||||
|
||||
**祝你开发顺利!** 🚀
|
||||
Reference in New Issue
Block a user