主要功能: - 实现完整的 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>
17 KiB
使用指南:RBAC 表结构与 GORM 数据权限过滤
功能编号: 004-rbac-data-permission 适用版本: v1.0.0 更新日期: 2025-11-18
目录
快速开始
环境要求
- Go 1.25.4+
- PostgreSQL 14+
- Redis 6.0+
- golang-migrate v4.x
数据库初始化
# 运行数据库迁移
migrate -path migrations -database "postgresql://postgres:password@localhost:5432/junhong_cmp_fiber?sslmode=disable" up
# 验证表创建
psql -U postgres -d junhong_cmp_fiber -c "\dt"
创建 root 账号
-- 使用 bcrypt 哈希密码(Password123)
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$N9qo8uLOickgx2ZMRZoMye1P7Z.mKAeQ7pjSeG7gYDobOAZCnOMUa', 1, NULL, NULL, 1, 1, 1, NOW(), NOW());
账号管理
1. 创建账号
API 端点: POST /api/v1/accounts
请求示例:
curl -X POST http://localhost:8080/api/v1/accounts \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"username": "platform_user",
"phone": "13900000001",
"password": "Password123",
"user_type": 2,
"shop_id": 10,
"parent_id": 1
}'
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| username | string | 是 | 用户名(3-20 个字符,字母/数字/下划线) |
| phone | string | 是 | 手机号(11 位中国大陆手机号) |
| password | string | 是 | 密码(最少 8 位,包含字母和数字) |
| user_type | int | 是 | 用户类型:1=root, 2=平台, 3=代理, 4=企业 |
| shop_id | int | 条件 | 所属店铺 ID(user_type=1 时可为空) |
| parent_id | int | 条件 | 上级账号 ID(user_type=1 时可为空) |
响应示例:
{
"code": 0,
"message": "success",
"data": {
"id": 2,
"username": "platform_user",
"phone": "13900000001",
"user_type": 2,
"shop_id": 10,
"parent_id": 1,
"status": 1,
"created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z"
},
"timestamp": "2025-11-18T10:00:00Z"
}
注意事项:
- ✅ 密码会自动使用 bcrypt 加密存储
- ✅ username 和 phone 必须唯一(软删除后可重复使用)
- ✅ 非 root 用户必须提供 parent_id
- ✅ 创建成功后会自动清除父账号的下级 ID 缓存
2. 获取账号详情
API 端点: GET /api/v1/accounts/:id
请求示例:
curl -X GET http://localhost:8080/api/v1/accounts/2 \
-H "Authorization: Bearer <token>"
响应示例:
{
"code": 0,
"message": "success",
"data": {
"id": 2,
"username": "platform_user",
"phone": "13900000001",
"user_type": 2,
"shop_id": 10,
"parent_id": 1,
"status": 1,
"created_at": "2025-11-18T10:00:00Z",
"updated_at": "2025-11-18T10:00:00Z"
},
"timestamp": "2025-11-18T10:00:00Z"
}
注意: password 字段不会返回给客户端(已通过 json:"-" 标签隐藏)
3. 更新账号
API 端点: PUT /api/v1/accounts/:id
请求示例:
curl -X PUT http://localhost:8080/api/v1/accounts/2 \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"username": "new_username",
"phone": "13900000002",
"status": 1
}'
注意事项:
- ❌ 禁止修改: user_type, parent_id(创建后不可更改)
- ✅ 可选修改: username, phone, status
4. 删除账号(软删除)
API 端点: DELETE /api/v1/accounts/:id
请求示例:
curl -X DELETE http://localhost:8080/api/v1/accounts/2 \
-H "Authorization: Bearer <token>"
响应示例:
{
"code": 0,
"message": "success",
"data": null,
"timestamp": "2025-11-18T10:00:00Z"
}
注意事项:
- ✅ 软删除:只设置
deleted_at字段,数据仍保留 - ✅ 软删除后,username 和 phone 可以被重新使用
- ✅ 删除成功后会递归清除所有上级账号的下级 ID 缓存
- ⚠️ 软删除账号的数据对上级仍然可见(递归查询包含已删除账号)
5. 获取账号列表
API 端点: GET /api/v1/accounts
请求示例:
curl -X GET "http://localhost:8080/api/v1/accounts?page=1&page_size=20" \
-H "Authorization: Bearer <token>"
查询参数:
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|---|---|---|---|---|
| page | int | 否 | 1 | 页码 |
| page_size | int | 否 | 20 | 每页数量(最大 100) |
响应示例:
{
"code": 0,
"message": "success",
"data": {
"items": [
{
"id": 2,
"username": "platform_user",
"user_type": 2,
"shop_id": 10,
"parent_id": 1,
"status": 1
}
],
"total": 1,
"page": 1,
"page_size": 20
},
"timestamp": "2025-11-18T10:00:00Z"
}
6. 为账号分配角色
API 端点: POST /api/v1/accounts/:id/roles
请求示例:
curl -X POST http://localhost:8080/api/v1/accounts/2/roles \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"role_ids": [1, 2]
}'
响应示例:
{
"code": 0,
"message": "success",
"data": [
{
"id": 1,
"account_id": 2,
"role_id": 1,
"status": 1,
"created_at": "2025-11-18T10:00:00Z"
},
{
"id": 2,
"account_id": 2,
"role_id": 2,
"status": 1,
"created_at": "2025-11-18T10:00:00Z"
}
],
"timestamp": "2025-11-18T10:00:00Z"
}
7. 获取账号的角色列表
API 端点: GET /api/v1/accounts/:id/roles
请求示例:
curl -X GET http://localhost:8080/api/v1/accounts/2/roles \
-H "Authorization: Bearer <token>"
8. 移除账号的角色
API 端点: DELETE /api/v1/accounts/:account_id/roles/:role_id
请求示例:
curl -X DELETE http://localhost:8080/api/v1/accounts/2/roles/1 \
-H "Authorization: Bearer <token>"
角色管理
1. 创建角色
API 端点: POST /api/v1/roles
请求示例:
curl -X POST http://localhost:8080/api/v1/roles \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"role_name": "超级管理员",
"role_desc": "系统超级管理员",
"role_type": 1
}'
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| role_name | string | 是 | 角色名称(长度 ≤50) |
| role_desc | string | 否 | 角色描述(长度 ≤255) |
| role_type | int | 是 | 角色类型:1=超级, 2=代理, 3=企业 |
2. 为角色分配权限
API 端点: POST /api/v1/roles/:id/permissions
请求示例:
curl -X POST http://localhost:8080/api/v1/roles/1/permissions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"perm_ids": [1, 2, 3]
}'
3. 获取角色的权限列表
API 端点: GET /api/v1/roles/:id/permissions
请求示例:
curl -X GET http://localhost:8080/api/v1/roles/1/permissions \
-H "Authorization: Bearer <token>"
权限管理
1. 创建权限
API 端点: POST /api/v1/permissions
请求示例:
curl -X POST http://localhost:8080/api/v1/permissions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <token>" \
-d '{
"perm_name": "用户管理",
"perm_code": "user:manage",
"perm_type": 1,
"url": "/admin/users",
"parent_id": null,
"sort": 1
}'
参数说明:
| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
| perm_name | string | 是 | 权限名称(长度 ≤50) |
| perm_code | string | 是 | 权限编码(格式:module:action,如 user:create) |
| perm_type | int | 是 | 权限类型:1=菜单, 2=按钮 |
| url | string | 否 | URL 路径(长度 ≤255) |
| parent_id | int | 否 | 上级权限 ID(支持层级) |
| sort | int | 否 | 排序序号(默认 0) |
注意事项:
- ✅ perm_code 必须唯一(软删除后可重复使用)
- ✅ 支持层级权限(通过 parent_id 构建权限树)
数据权限过滤
核心概念
数据权限过滤机制确保每个用户只能访问自己及下级的数据,通过 owner_id 和 shop_id 双重过滤实现多租户数据隔离。
过滤规则
WHERE owner_id IN (当前用户及所有下级的ID列表) AND shop_id = 当前用户的shop_id
示例场景
假设用户层级关系为:A(root, ID=1) → B(平台, ID=2) → C(代理, ID=3)
- 用户 A 查询:返回所有数据(root 用户跳过过滤)
- 用户 B 查询:返回
owner_id IN (2, 3) AND shop_id = 10的数据 - 用户 C 查询:返回
owner_id = 3 AND shop_id = 10的数据
递归查询下级 ID
系统使用 PostgreSQL WITH RECURSIVE 查询所有下级 ID,并通过 Redis 缓存优化性能:
- 缓存 Key:
account:subordinates:{账号ID} - 过期时间: 30 分钟
- 清除时机: 账号创建/删除时主动清除
跳过数据权限过滤
某些特殊场景(如 C 端业务用户、系统任务)需要跳过数据权限过滤:
方式 1:在 Store 层使用 WithoutDataFilter 选项
users, err := userStore.List(ctx, &store.QueryOptions{
WithoutDataFilter: true,
})
方式 2:root 用户自动跳过过滤
root 用户(user_type=1)的所有查询会自动跳过数据权限过滤。
业务表集成指南
步骤 1:添加数据权限字段
为业务表添加 owner_id 和 shop_id 字段:
数据库迁移:
-- migrations/000004_add_owner_id_to_business_table.up.sql
ALTER TABLE tb_your_table ADD COLUMN owner_id INTEGER;
ALTER TABLE tb_your_table ADD COLUMN shop_id INTEGER;
CREATE INDEX idx_your_table_owner_id ON tb_your_table(owner_id);
CREATE INDEX idx_your_table_shop_id ON tb_your_table(shop_id);
GORM 模型更新:
// internal/model/your_model.go
type YourModel struct {
ID uint `gorm:"primarykey" json:"id"`
// ... 其他字段 ...
OwnerID *uint `gorm:"index" json:"owner_id,omitempty"` // 新增
ShopID *uint `gorm:"index" json:"shop_id,omitempty"` // 新增
CreatedAt time.Time `gorm:"not null" json:"created_at"`
UpdatedAt time.Time `gorm:"not null" json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at,omitempty"`
}
步骤 2:在 Store 层应用数据权限过滤
// internal/store/postgres/your_store.go
import (
"context"
"your-project/internal/model"
"your-project/internal/store"
"gorm.io/gorm"
)
type YourStore struct {
db *gorm.DB
accountStore *AccountStore // 注入 AccountStore(用于递归查询)
}
func (s *YourStore) List(ctx context.Context, opts *store.QueryOptions) ([]*model.YourModel, error) {
query := s.db.WithContext(ctx)
// 应用数据权限过滤(如果未禁用)
if !opts.WithoutDataFilter {
query = query.Scopes(DataPermissionScope(s.accountStore))
}
var items []*model.YourModel
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (s *YourStore) GetByID(ctx context.Context, id uint, opts *store.QueryOptions) (*model.YourModel, error) {
query := s.db.WithContext(ctx)
// 应用数据权限过滤(如果未禁用)
if !opts.WithoutDataFilter {
query = query.Scopes(DataPermissionScope(s.accountStore))
}
var item model.YourModel
if err := query.First(&item, id).Error; err != nil {
return nil, err
}
return &item, nil
}
步骤 3:在 Service 层设置 owner_id 和 shop_id
// internal/service/your_service/service.go
import (
"context"
"your-project/internal/model"
"your-project/pkg/middleware"
)
func (s *Service) Create(ctx context.Context, req *CreateRequest) (*model.YourModel, error) {
// 从 context 提取当前用户信息
userID := middleware.GetUserIDFromContext(ctx)
shopID := middleware.GetShopIDFromContext(ctx)
item := &model.YourModel{
// ... 其他字段 ...
OwnerID: &userID, // 设置为当前用户 ID
ShopID: &shopID, // 设置为当前用户的 shop_id
}
if err := s.store.Create(ctx, item); err != nil {
return nil, err
}
return item, nil
}
步骤 4:验证数据权限过滤
创建测试数据并验证不同用户的查询结果:
-- 创建测试数据
INSERT INTO tb_your_table (name, owner_id, shop_id, created_at, updated_at)
VALUES
('数据A - 用户B创建', 2, 10, NOW(), NOW()),
('数据B - 用户C创建', 3, 10, NOW(), NOW()),
('数据C - 其他店铺', 2, 20, NOW(), NOW());
预期查询结果:
- 用户 A(root): 返回所有 3 条数据
- 用户 B(平台): 返回 2 条数据(owner_id=2 或 3,且 shop_id=10)
- 用户 C(代理): 返回 1 条数据(owner_id=3,且 shop_id=10)
常见问题
Q1: 如何验证密码?
A: 使用 bcrypt 验证密码:
import "golang.org/x/crypto/bcrypt"
func ValidatePassword(plainPassword, hashedPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(plainPassword))
return err == nil
}
Q2: 递归查询性能问题?
A: 系统已通过 Redis 缓存优化,缓存命中率预期 > 90%。如果层级深度超过 10 层,建议:
- 监控递归查询耗时(P95 应 < 50ms)
- 考虑使用闭包表(Closure Table)替代递归查询
Q3: 软删除账号的数据如何处理?
A: 软删除账号后:
- ✅ 该账号的数据对上级仍然可见(递归查询包含已删除账号)
- ✅ username 和 phone 可以被重新使用
- ✅ 所有上级的下级 ID 缓存会被清除
Q4: 如何清除 Redis 缓存?
A: 账号创建/删除时会自动清除缓存,也可以手动清除:
# 清除指定账号的下级 ID 缓存
redis-cli DEL account:subordinates:2
# 清除所有下级 ID 缓存
redis-cli KEYS "account:subordinates:*" | xargs redis-cli DEL
Q5: 如何为 C 端业务用户实现数据过滤?
A: C 端业务用户通常不使用 owner_id 过滤,而是基于业务字段(如 iccid/device_id):
// 使用 WithoutDataFilter 跳过 owner_id 过滤
users, err := userStore.List(ctx, &store.QueryOptions{
WithoutDataFilter: true,
})
// 在 Service 层应用业务字段过滤
filteredUsers := filterByICCID(users, targetICCID)
Q6: 如何处理跨店铺查询?
A: 数据权限过滤强制 shop_id = 当前用户的shop_id,不支持跨店铺查询。如果需要跨店铺查询:
- 方式 1:使用 root 用户(自动跳过过滤)
- 方式 2:使用
WithoutDataFilter选项(需要在业务层额外校验权限)
最佳实践
1. 创建账号时的层级关系
✅ 推荐:只有本级账号能创建下级账号
A(root) 创建 B(平台)
B(平台) 创建 C(代理)
C(代理) 创建 D(企业)
❌ 不推荐:跨级创建(A 直接创建 C)
2. 数据归属设置
✅ 推荐:创建数据时 owner_id 设置为当前用户 ID
item.OwnerID = &userID // 当前用户 ID
item.ShopID = &shopID // 当前用户的 shop_id
❌ 不推荐:owner_id 设置为其他用户 ID(除非是数据转移场景)
3. 缓存管理
✅ 推荐:依赖自动缓存清除机制
- 账号创建时自动清除父账号缓存
- 账号删除时递归清除所有上级缓存
❌ 不推荐:手动清除缓存(除非调试或紧急修复)
4. 错误处理
✅ 推荐:使用统一错误处理机制
import "your-project/pkg/errors"
if account == nil {
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
}
❌ 不推荐:手动构造错误响应
// ❌ 不推荐
return c.Status(404).JSON(fiber.Map{"error": "账号不存在"})
5. 安全性
✅ 推荐:
- 使用 bcrypt 加密密码
- password 字段使用
json:"-"隐藏 - 验证用户权限后再执行敏感操作
❌ 不推荐:
- 使用 MD5 加密密码(已废弃)
- 返回 password 字段给客户端
- 跳过权限校验
相关文档
- 功能总结: 功能总结.md
- 快速入门: specs/004-rbac-data-permission/quickstart.md
- 数据模型: specs/004-rbac-data-permission/data-model.md
- API 文档: specs/004-rbac-data-permission/contracts/
更新日期: 2025-11-18 维护者: AI Assistant (Claude)