Files
junhong_cmp_fiber/docs/004-rbac-data-permission/使用指南.md
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

724 lines
17 KiB
Markdown
Raw 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.
# 使用指南RBAC 表结构与 GORM 数据权限过滤
**功能编号**: 004-rbac-data-permission
**适用版本**: v1.0.0
**更新日期**: 2025-11-18
## 目录
1. [快速开始](#快速开始)
2. [账号管理](#账号管理)
3. [角色管理](#角色管理)
4. [权限管理](#权限管理)
5. [数据权限过滤](#数据权限过滤)
6. [业务表集成指南](#业务表集成指南)
7. [常见问题](#常见问题)
8. [最佳实践](#最佳实践)
---
## 快速开始
### 环境要求
- Go 1.25.4+
- PostgreSQL 14+
- Redis 6.0+
- golang-migrate v4.x
### 数据库初始化
```bash
# 运行数据库迁移
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 账号
```sql
-- 使用 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`
**请求示例**:
```bash
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 | 条件 | 所属店铺 IDuser_type=1 时可为空) |
| parent_id | int | 条件 | 上级账号 IDuser_type=1 时可为空) |
**响应示例**:
```json
{
"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`
**请求示例**:
```bash
curl -X GET http://localhost:8080/api/v1/accounts/2 \
-H "Authorization: Bearer <token>"
```
**响应示例**:
```json
{
"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`
**请求示例**:
```bash
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`
**请求示例**:
```bash
curl -X DELETE http://localhost:8080/api/v1/accounts/2 \
-H "Authorization: Bearer <token>"
```
**响应示例**:
```json
{
"code": 0,
"message": "success",
"data": null,
"timestamp": "2025-11-18T10:00:00Z"
}
```
**注意事项**:
- ✅ 软删除:只设置 `deleted_at` 字段,数据仍保留
- ✅ 软删除后username 和 phone 可以被重新使用
- ✅ 删除成功后会递归清除所有上级账号的下级 ID 缓存
- ⚠️ 软删除账号的数据对上级仍然可见(递归查询包含已删除账号)
### 5. 获取账号列表
**API 端点**: `GET /api/v1/accounts`
**请求示例**:
```bash
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 |
**响应示例**:
```json
{
"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`
**请求示例**:
```bash
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]
}'
```
**响应示例**:
```json
{
"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`
**请求示例**:
```bash
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`
**请求示例**:
```bash
curl -X DELETE http://localhost:8080/api/v1/accounts/2/roles/1 \
-H "Authorization: Bearer <token>"
```
---
## 角色管理
### 1. 创建角色
**API 端点**: `POST /api/v1/roles`
**请求示例**:
```bash
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`
**请求示例**:
```bash
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`
**请求示例**:
```bash
curl -X GET http://localhost:8080/api/v1/roles/1/permissions \
-H "Authorization: Bearer <token>"
```
---
## 权限管理
### 1. 创建权限
**API 端点**: `POST /api/v1/permissions`
**请求示例**:
```bash
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` 双重过滤实现多租户数据隔离。
### 过滤规则
```sql
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 选项**
```go
users, err := userStore.List(ctx, &store.QueryOptions{
WithoutDataFilter: true,
})
```
**方式 2root 用户自动跳过过滤**
root 用户user_type=1的所有查询会自动跳过数据权限过滤。
---
## 业务表集成指南
### 步骤 1添加数据权限字段
为业务表添加 `owner_id``shop_id` 字段:
**数据库迁移**:
```sql
-- 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 模型更新**:
```go
// 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 层应用数据权限过滤
```go
// 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
```go
// 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验证数据权限过滤
创建测试数据并验证不同用户的查询结果:
```sql
-- 创建测试数据
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());
```
**预期查询结果**:
- **用户 Aroot**: 返回所有 3 条数据
- **用户 B平台**: 返回 2 条数据owner_id=2 或 3且 shop_id=10
- **用户 C代理**: 返回 1 条数据owner_id=3且 shop_id=10
---
## 常见问题
### Q1: 如何验证密码?
**A**: 使用 bcrypt 验证密码:
```go
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**: 账号创建/删除时会自动清除缓存,也可以手动清除:
```bash
# 清除指定账号的下级 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
```go
// 使用 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
```go
item.OwnerID = &userID // 当前用户 ID
item.ShopID = &shopID // 当前用户的 shop_id
```
**不推荐**owner_id 设置为其他用户 ID除非是数据转移场景
### 3. 缓存管理
**推荐**:依赖自动缓存清除机制
- 账号创建时自动清除父账号缓存
- 账号删除时递归清除所有上级缓存
**不推荐**:手动清除缓存(除非调试或紧急修复)
### 4. 错误处理
**推荐**:使用统一错误处理机制
```go
import "your-project/pkg/errors"
if account == nil {
return nil, errors.New(errors.CodeAccountNotFound, "账号不存在")
}
```
**不推荐**:手动构造错误响应
```go
// ❌ 不推荐
return c.Status(404).JSON(fiber.Map{"error": "账号不存在"})
```
### 5. 安全性
**推荐**
- 使用 bcrypt 加密密码
- password 字段使用 `json:"-"` 隐藏
- 验证用户权限后再执行敏感操作
**不推荐**
- 使用 MD5 加密密码(已废弃)
- 返回 password 字段给客户端
- 跳过权限校验
---
## 相关文档
- **功能总结**: [功能总结.md](./功能总结.md)
- **快速入门**: [specs/004-rbac-data-permission/quickstart.md](../../specs/004-rbac-data-permission/quickstart.md)
- **数据模型**: [specs/004-rbac-data-permission/data-model.md](../../specs/004-rbac-data-permission/data-model.md)
- **API 文档**: [specs/004-rbac-data-permission/contracts/](../../specs/004-rbac-data-permission/contracts/)
---
**更新日期**: 2025-11-18
**维护者**: AI Assistant (Claude)