主要功能: - 实现完整的 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>
14 KiB
14 KiB
Quick Start: RBAC 表结构与 GORM 数据权限过滤
Feature: 004-rbac-data-permission Date: 2025-11-18 Estimated Time: 2-3 小时(阅读 + 环境准备 + 运行示例)
概述
本快速指南帮助你在 30 分钟内理解 RBAC 权限系统和数据权限过滤机制,并在 2 小时内完成环境准备和运行第一个示例。
核心功能:
- RBAC 权限系统:账号、角色、权限的多对多关联
- 数据权限过滤:基于 owner_id + shop_id 的自动数据隔离
- 递归查询:使用 PostgreSQL WITH RECURSIVE 查询用户的所有下级
- 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 关联标签(
foreignKey、hasMany、belongsTo等) - ✅ 通过 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)
}
// 递归查询用户 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]
}
运行示例:
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)
}
// 模拟当前用户为 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)
}
}
运行示例:
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 是行业标准,内置盐值,抗暴力破解。
下一步
- 阅读详细设计: 查看 data-model.md 了解完整的数据库设计
- 查看 API 文档: 查看 contracts/ 目录的 OpenAPI 规范
- 阅读实现任务: 查看 tasks.md 了解完整的实现任务清单
- 开始实现: 按照 Phase 1 → Phase 2 → ... 的顺序完成任务
联系和反馈
如果遇到问题或有建议,请:
- 检查 research.md 中的技术决策
- 查看 spec.md 中的功能需求
- 提交 GitHub Issue 或联系团队
祝你开发顺利! 🚀