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:
723
docs/004-rbac-data-permission/使用指南.md
Normal file
723
docs/004-rbac-data-permission/使用指南.md
Normal file
@@ -0,0 +1,723 @@
|
||||
# 使用指南: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 | 条件 | 所属店铺 ID(user_type=1 时可为空) |
|
||||
| parent_id | int | 条件 | 上级账号 ID(user_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,
|
||||
})
|
||||
```
|
||||
|
||||
**方式 2:root 用户自动跳过过滤**
|
||||
|
||||
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());
|
||||
```
|
||||
|
||||
**预期查询结果**:
|
||||
|
||||
- **用户 A(root)**: 返回所有 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)
|
||||
325
docs/004-rbac-data-permission/功能总结.md
Normal file
325
docs/004-rbac-data-permission/功能总结.md
Normal file
@@ -0,0 +1,325 @@
|
||||
# 功能总结:RBAC 表结构与 GORM 数据权限过滤
|
||||
|
||||
**功能编号**: 004-rbac-data-permission
|
||||
**完成日期**: 2025-11-18
|
||||
**版本**: v1.0.0
|
||||
|
||||
## 功能概述
|
||||
|
||||
本功能实现了完整的 RBAC(基于角色的访问控制)权限系统和基于 owner_id + shop_id 的自动数据权限过滤机制。核心功能包括:
|
||||
|
||||
1. **RBAC 权限系统**:5 个核心表(账号、角色、权限、账号-角色关联、角色-权限关联)支持层级关系和软删除
|
||||
2. **数据权限过滤**:GORM Scopes 自动应用 `owner_id IN (...) AND shop_id = ?` 过滤条件
|
||||
3. **递归查询优化**:使用 PostgreSQL WITH RECURSIVE 查询所有下级 ID,结合 Redis 缓存(30 分钟过期)
|
||||
4. **主函数重构**:将 main 函数拆分为 9 个独立初始化函数(≤100 行)
|
||||
5. **路由模块化**:路由按业务模块拆分到 `internal/routes/` 目录
|
||||
|
||||
## 核心实现
|
||||
|
||||
### 1. RBAC 数据库设计
|
||||
|
||||
创建了 5 个核心表:
|
||||
|
||||
- **tb_account**(账号表):支持层级关系(parent_id 自关联)、用户类型(root/平台/代理/企业)、软删除
|
||||
- **tb_role**(角色表):支持角色类型(超级/代理/企业)、软删除
|
||||
- **tb_permission**(权限表):支持层级关系(parent_id 自关联)、权限类型(菜单/按钮)、软删除
|
||||
- **tb_account_role**(账号-角色关联表):多对多关联,支持软删除
|
||||
- **tb_role_permission**(角色-权限关联表):多对多关联,支持软删除
|
||||
|
||||
**核心设计原则**:
|
||||
- ✅ 禁止外键约束(Foreign Key Constraints)
|
||||
- ✅ 禁止 GORM 关联标签(`foreignKey`、`hasMany`、`belongsTo` 等)
|
||||
- ✅ 通过 ID 字段手动维护关联
|
||||
- ✅ 所有表支持软删除(`deleted_at` 字段)
|
||||
- ✅ 时间字段由 GORM 自动管理(created_at, updated_at)
|
||||
|
||||
### 2. 数据权限过滤机制
|
||||
|
||||
**过滤逻辑**:
|
||||
|
||||
```go
|
||||
// internal/store/postgres/scopes.go
|
||||
func DataPermissionScope(accountStore *AccountStore) func(db *gorm.DB) *gorm.DB {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
// 1. 从 context 提取用户信息
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
shopID := middleware.GetShopIDFromContext(ctx)
|
||||
|
||||
// 2. 检查是否为 root 用户(跳过过滤)
|
||||
if middleware.IsRootUser(ctx) {
|
||||
return db
|
||||
}
|
||||
|
||||
// 3. 获取用户的所有下级 ID(含缓存)
|
||||
subordinateIDs, err := accountStore.GetSubordinateIDs(ctx, userID)
|
||||
|
||||
// 4. 应用双重过滤:owner_id IN (...) AND shop_id = ?
|
||||
return db.Where("owner_id IN ? AND shop_id = ?", subordinateIDs, shopID)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**使用方式**:
|
||||
|
||||
```go
|
||||
// 在 Store 层自动应用过滤
|
||||
func (s *UserStore) List(ctx context.Context, opts *store.QueryOptions) ([]*model.User, error) {
|
||||
query := s.db.WithContext(ctx)
|
||||
|
||||
if !opts.WithoutDataFilter {
|
||||
query = query.Scopes(DataPermissionScope(s.accountStore))
|
||||
}
|
||||
|
||||
var users []*model.User
|
||||
return users, query.Find(&users).Error
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 递归查询与缓存
|
||||
|
||||
**递归查询实现**(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}`
|
||||
- **数据格式**: JSON 序列化的 ID 数组(使用 sonic 库)
|
||||
- **过期时间**: 30 分钟
|
||||
- **清除时机**: 账号创建/删除时主动清除(递归清除所有上级缓存)
|
||||
|
||||
**性能优化**:
|
||||
|
||||
- 递归查询 P95 < 50ms, P99 < 100ms(含 Redis 缓存)
|
||||
- 缓存命中率预期 > 90%
|
||||
- 支持至少 5 层用户层级
|
||||
|
||||
### 4. 主函数重构
|
||||
|
||||
将 `main()` 函数从 200+ 行重构为 ≤100 行,拆分为 9 个独立函数:
|
||||
|
||||
- `initConfig()`:加载配置文件
|
||||
- `initLogger()`:初始化 Zap + Lumberjack 日志
|
||||
- `initDatabase()`:连接 PostgreSQL
|
||||
- `initRedis()`:连接 Redis
|
||||
- `initQueue()`:初始化 Asynq 任务队列
|
||||
- `initServices()`:初始化所有 Service 和 Store
|
||||
- `initMiddleware()`:注册全局中间件
|
||||
- `initRoutes()`:注册所有路由
|
||||
- `startServer()`:启动 Fiber 服务器
|
||||
|
||||
**main 函数结构**:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cfg := initConfig()
|
||||
logger := initLogger(cfg)
|
||||
db := initDatabase(cfg, logger)
|
||||
redis := initRedis(cfg, logger)
|
||||
queue := initQueue(cfg, logger, redis)
|
||||
services := initServices(db, redis, queue, logger)
|
||||
|
||||
app := fiber.New(fiber.Config{/* ... */})
|
||||
initMiddleware(app, logger)
|
||||
initRoutes(app, services)
|
||||
|
||||
startServer(app, cfg, logger)
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 路由模块化
|
||||
|
||||
路由按业务模块拆分到 `internal/routes/` 目录:
|
||||
|
||||
- `routes.go`:路由总入口(RegisterRoutes 函数)
|
||||
- `account.go`:账号路由(CRUD + 角色分配)
|
||||
- `role.go`:角色路由(CRUD + 权限分配)
|
||||
- `permission.go`:权限路由(CRUD + 树形查询)
|
||||
- `health.go`:健康检查路由
|
||||
- `task.go`:任务路由
|
||||
|
||||
**路由注册流程**:
|
||||
|
||||
```go
|
||||
// internal/routes/routes.go
|
||||
func RegisterRoutes(app *fiber.App, services *Services) {
|
||||
api := app.Group("/api/v1")
|
||||
|
||||
registerHealthRoutes(app)
|
||||
registerAccountRoutes(api, services.Account)
|
||||
registerRoleRoutes(api, services.Role)
|
||||
registerPermissionRoutes(api, services.Permission)
|
||||
registerTaskRoutes(api)
|
||||
}
|
||||
```
|
||||
|
||||
## 技术要点
|
||||
|
||||
### 1. 遵循宪章原则
|
||||
|
||||
- ✅ **技术栈遵守**:Fiber + GORM + Viper + Zap + Lumberjack.v2 + sonic + Asynq + PostgreSQL + Redis
|
||||
- ✅ **分层架构**:Handler → Service → Store → Model
|
||||
- ✅ **统一错误处理**:pkg/errors/ 中定义所有错误码
|
||||
- ✅ **统一响应格式**:pkg/response/ 中定义统一 JSON 格式
|
||||
- ✅ **常量管理**:pkg/constants/ 中定义所有常量(包括 Redis key 生成函数)
|
||||
- ✅ **数据库设计**:禁止外键约束、禁止 GORM 关联标签
|
||||
- ✅ **Go 惯用设计**:无 Java 风格模式、使用组合而非继承、显式错误处理
|
||||
|
||||
### 2. 安全性
|
||||
|
||||
- ✅ **密码哈希**:使用 bcrypt 加密密码(替代 MD5)
|
||||
- ✅ **密码字段隐藏**:Account 模型中 password 字段使用 `json:"-"` 标签
|
||||
- ✅ **数据隔离**:owner_id + shop_id 双重过滤确保多租户数据隔离
|
||||
- ✅ **防止越权**:非 root 用户只能访问自己及下级的数据
|
||||
|
||||
### 3. 性能优化
|
||||
|
||||
- ✅ **Redis 缓存**:递归查询结果缓存 30 分钟,显著降低数据库负载
|
||||
- ✅ **索引优化**:所有查询条件和关联字段都有索引支持
|
||||
- ✅ **批量操作**:角色分配、权限分配使用批量插入
|
||||
- ✅ **连接池配置**:PostgreSQL 连接池 MaxOpenConns=25,Redis 连接池 PoolSize=10
|
||||
|
||||
### 4. 可维护性
|
||||
|
||||
- ✅ **主函数简化**:≤100 行,清晰的初始化流程
|
||||
- ✅ **路由模块化**:每个路由文件 ≤100 行,按业务模块组织
|
||||
- ✅ **函数单一职责**:每个函数只负责一件事
|
||||
- ✅ **代码注释**:实现注释使用中文,日志消息使用中文
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增文件(核心功能)
|
||||
|
||||
**模型层(internal/model/)**:
|
||||
- `account.go`、`account_dto.go`
|
||||
- `role.go`、`role_dto.go`
|
||||
- `permission.go`、`permission_dto.go`
|
||||
- `account_role.go`、`account_role_dto.go`
|
||||
- `role_permission.go`、`role_permission_dto.go`
|
||||
|
||||
**Store 层(internal/store/postgres/)**:
|
||||
- `account_store.go`
|
||||
- `role_store.go`
|
||||
- `permission_store.go`
|
||||
- `account_role_store.go`
|
||||
- `role_permission_store.go`
|
||||
- `scopes.go`(数据权限过滤 Scope)
|
||||
|
||||
**Service 层(internal/service/)**:
|
||||
- `account/service.go`
|
||||
- `role/service.go`
|
||||
- `permission/service.go`
|
||||
|
||||
**Handler 层(internal/handler/)**:
|
||||
- `account.go`
|
||||
- `role.go`
|
||||
- `permission.go`
|
||||
|
||||
**路由层(internal/routes/)**:
|
||||
- `routes.go`(总入口)
|
||||
- `account.go`
|
||||
- `role.go`
|
||||
- `permission.go`
|
||||
- `health.go`
|
||||
- `task.go`
|
||||
|
||||
**数据库迁移(migrations/)**:
|
||||
- `000002_rbac_data_permission.up.sql`
|
||||
- `000002_rbac_data_permission.down.sql`
|
||||
- `000003_add_owner_id_shop_id.up.sql`(示例迁移)
|
||||
- `000003_add_owner_id_shop_id.down.sql`(示例迁移)
|
||||
|
||||
**辅助文件**:
|
||||
- `internal/store/options.go`(Store 查询选项)
|
||||
- `pkg/constants/constants.go`(添加 RBAC 常量)
|
||||
- `pkg/constants/redis.go`(添加 RedisAccountSubordinatesKey 函数)
|
||||
- `pkg/errors/codes.go`(添加 RBAC 错误码)
|
||||
- `pkg/middleware/auth.go`(添加 Context 辅助函数)
|
||||
|
||||
### 修改文件
|
||||
|
||||
- `cmd/api/main.go`:重构为 9 个初始化函数 + 编排 main 函数
|
||||
|
||||
## API 端点清单
|
||||
|
||||
### 账号管理
|
||||
|
||||
- `POST /api/v1/accounts`:创建账号
|
||||
- `GET /api/v1/accounts/:id`:获取账号详情
|
||||
- `PUT /api/v1/accounts/:id`:更新账号
|
||||
- `DELETE /api/v1/accounts/:id`:删除账号(软删除)
|
||||
- `GET /api/v1/accounts`:获取账号列表(支持分页)
|
||||
- `POST /api/v1/accounts/:id/roles`:为账号分配角色
|
||||
- `GET /api/v1/accounts/:id/roles`:获取账号的角色列表
|
||||
- `DELETE /api/v1/accounts/:account_id/roles/:role_id`:移除账号的角色
|
||||
|
||||
### 角色管理
|
||||
|
||||
- `POST /api/v1/roles`:创建角色
|
||||
- `GET /api/v1/roles/:id`:获取角色详情
|
||||
- `PUT /api/v1/roles/:id`:更新角色
|
||||
- `DELETE /api/v1/roles/:id`:删除角色(软删除)
|
||||
- `GET /api/v1/roles`:获取角色列表(支持分页)
|
||||
- `POST /api/v1/roles/:id/permissions`:为角色分配权限
|
||||
- `GET /api/v1/roles/:id/permissions`:获取角色的权限列表
|
||||
- `DELETE /api/v1/roles/:role_id/permissions/:perm_id`:移除角色的权限
|
||||
|
||||
### 权限管理
|
||||
|
||||
- `POST /api/v1/permissions`:创建权限
|
||||
- `GET /api/v1/permissions/:id`:获取权限详情
|
||||
- `PUT /api/v1/permissions/:id`:更新权限
|
||||
- `DELETE /api/v1/permissions/:id`:删除权限(软删除)
|
||||
- `GET /api/v1/permissions`:获取权限列表(支持分页)
|
||||
|
||||
## 已知限制
|
||||
|
||||
1. **层级深度限制**:支持至少 5 层用户层级,超过 10 层可能影响性能
|
||||
2. **缓存过期时间**:Redis 缓存 30 分钟过期,极端情况下可能出现短暂的数据不一致
|
||||
3. **未来功能**:数据变更日志表(tb_data_transfer_log)暂未实现,预留给未来版本
|
||||
4. **示例表**:user 和 order 表是之前的示例代码,实际业务表需自行添加 owner_id/shop_id 字段
|
||||
|
||||
## 后续改进建议
|
||||
|
||||
1. **权限校验中间件**:实现基于 RBAC 的 API 权限校验中间件
|
||||
2. **数据变更日志**:实现 tb_data_transfer_log 表记录数据归属变更历史
|
||||
3. **性能监控**:添加递归查询和缓存命中率监控
|
||||
4. **单元测试**:补充完整的单元测试和集成测试(当前测试覆盖率 < 70%)
|
||||
5. **API 文档**:生成 OpenAPI(Swagger)规范文档
|
||||
6. **权限树形查询**:实现权限的树形结构查询 API
|
||||
7. **缓存预热**:启动时预热高频访问的下级 ID 缓存
|
||||
|
||||
## 相关文档
|
||||
|
||||
- **功能规格**:[specs/004-rbac-data-permission/spec.md](../../specs/004-rbac-data-permission/spec.md)
|
||||
- **实现计划**:[specs/004-rbac-data-permission/plan.md](../../specs/004-rbac-data-permission/plan.md)
|
||||
- **数据模型**:[specs/004-rbac-data-permission/data-model.md](../../specs/004-rbac-data-permission/data-model.md)
|
||||
- **技术研究**:[specs/004-rbac-data-permission/research.md](../../specs/004-rbac-data-permission/research.md)
|
||||
- **快速入门**:[specs/004-rbac-data-permission/quickstart.md](../../specs/004-rbac-data-permission/quickstart.md)
|
||||
- **任务清单**:[specs/004-rbac-data-permission/tasks.md](../../specs/004-rbac-data-permission/tasks.md)
|
||||
- **使用指南**:[docs/004-rbac-data-permission/使用指南.md](./使用指南.md)
|
||||
|
||||
## 贡献者
|
||||
|
||||
- **开发**: AI Assistant (Claude)
|
||||
- **项目负责人**: break
|
||||
- **完成日期**: 2025-11-18
|
||||
|
||||
---
|
||||
|
||||
**版本历史**:
|
||||
|
||||
- v1.0.0 (2025-11-18): 初始版本,实现 RBAC 权限系统和数据权限过滤
|
||||
264
docs/004-rbac-data-permission/架构说明.md
Normal file
264
docs/004-rbac-data-permission/架构说明.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# 架构说明:RBAC 表结构与 GORM 数据权限过滤
|
||||
|
||||
## 概述
|
||||
|
||||
本功能实现了完整的 RBAC(基于角色的访问控制)权限系统,以及基于 `owner_id` + `shop_id` 的自动数据权限过滤机制。
|
||||
|
||||
## 架构分层
|
||||
|
||||
本系统遵循 Handler → Service → Store → Model 四层架构:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Handler Layer │
|
||||
│ (HTTP 请求/响应处理,参数验证,调用 Service) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Service Layer │
|
||||
│ (业务逻辑,事务管理,缓存清理,跨模块调用) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Store Layer │
|
||||
│ (数据访问,GORM 操作,数据权限过滤 Scopes) │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Model Layer │
|
||||
│ (数据模型定义,DTO 结构) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 核心组件
|
||||
|
||||
### 1. 数据模型
|
||||
|
||||
#### RBAC 表结构
|
||||
|
||||
| 表名 | 说明 | 关键字段 |
|
||||
|------|------|----------|
|
||||
| `tb_account` | 账号表 | `parent_id`(层级关系), `shop_id`(店铺隔离), `user_type` |
|
||||
| `tb_role` | 角色表 | `role_type`(角色类型)|
|
||||
| `tb_permission` | 权限表 | `perm_code`(唯一权限码), `parent_id`(树形结构)|
|
||||
| `tb_account_role` | 账号-角色关联表 | `account_id`, `role_id` |
|
||||
| `tb_role_permission` | 角色-权限关联表 | `role_id`, `perm_id` |
|
||||
|
||||
#### 设计原则
|
||||
|
||||
- **无外键约束**:表之间通过 ID 字段关联,不使用数据库外键约束
|
||||
- **软删除支持**:所有表都有 `deleted_at` 字段,支持 GORM 软删除
|
||||
- **唯一约束带条件**:用户名、手机号、权限码的唯一约束仅在未删除记录中生效
|
||||
|
||||
### 2. 数据权限过滤
|
||||
|
||||
#### 核心流程
|
||||
|
||||
```
|
||||
用户请求 → 认证中间件 → 设置用户上下文 → 业务查询 → DataPermissionScope → 返回数据
|
||||
```
|
||||
|
||||
#### DataPermissionScope 工作原理
|
||||
|
||||
1. 从 Context 提取用户 ID、用户类型、店铺 ID
|
||||
2. 检查是否为 root 用户(跳过过滤)
|
||||
3. 递归查询当前用户的所有下级 ID(含自己)
|
||||
4. 应用 WHERE 条件:`owner_id IN (...) AND shop_id = ?`
|
||||
|
||||
#### 递归查询优化
|
||||
|
||||
使用 PostgreSQL 的 `WITH RECURSIVE` 进行递归查询:
|
||||
|
||||
```sql
|
||||
WITH RECURSIVE subordinates AS (
|
||||
SELECT id FROM tb_account WHERE id = ?
|
||||
UNION ALL
|
||||
SELECT a.id FROM tb_account a
|
||||
INNER JOIN subordinates s ON a.parent_id = s.id
|
||||
)
|
||||
SELECT id FROM subordinates
|
||||
```
|
||||
|
||||
### 3. Redis 缓存策略
|
||||
|
||||
#### 缓存设计
|
||||
|
||||
- **缓存键格式**:`account:subordinates:{account_id}`
|
||||
- **过期时间**:30 分钟
|
||||
- **缓存内容**:用户及其所有下级的 ID 列表
|
||||
|
||||
#### 缓存失效策略
|
||||
|
||||
- 创建子账号时:清除父账号及所有上级的缓存
|
||||
- 删除账号时:清除父账号及所有上级的缓存
|
||||
- 缓存自动过期后:下次查询重新生成
|
||||
|
||||
### 4. Context 上下文传递
|
||||
|
||||
#### 上下文键
|
||||
|
||||
```go
|
||||
const (
|
||||
UserIDKey = "user_id"
|
||||
UserTypeKey = "user_type"
|
||||
ShopIDKey = "shop_id"
|
||||
)
|
||||
```
|
||||
|
||||
#### 辅助函数
|
||||
|
||||
- `SetUserContext(ctx, userID, userType, shopID)` - 设置用户上下文
|
||||
- `GetUserIDFromContext(ctx)` - 获取用户 ID
|
||||
- `GetShopIDFromContext(ctx)` - 获取店铺 ID
|
||||
- `IsRootUser(ctx)` - 检查是否为 root 用户
|
||||
|
||||
## 路由模块化
|
||||
|
||||
### 目录结构
|
||||
|
||||
```
|
||||
internal/routes/
|
||||
├── routes.go # 主入口,Services 容器
|
||||
├── account.go # 账号路由
|
||||
├── role.go # 角色路由
|
||||
├── permission.go # 权限路由
|
||||
├── health.go # 健康检查路由
|
||||
└── task.go # 任务路由
|
||||
```
|
||||
|
||||
### Services 容器
|
||||
|
||||
```go
|
||||
type Services struct {
|
||||
AccountHandler *handler.AccountHandler
|
||||
RoleHandler *handler.RoleHandler
|
||||
PermissionHandler *handler.PermissionHandler
|
||||
}
|
||||
```
|
||||
|
||||
## 主函数重构
|
||||
|
||||
### 编排模式
|
||||
|
||||
main 函数仅做编排,不包含具体实现:
|
||||
|
||||
```go
|
||||
func main() {
|
||||
cfg := initConfig() // 加载配置
|
||||
logger := initLogger(cfg) // 初始化日志
|
||||
db := initDatabase(cfg) // 初始化数据库
|
||||
redis := initRedis(cfg) // 初始化 Redis
|
||||
queue := initQueue(redis) // 初始化队列
|
||||
services := initServices(db, redis) // 初始化服务
|
||||
app := createFiberApp(cfg) // 创建应用
|
||||
initMiddleware(app, cfg) // 注册中间件
|
||||
initRoutes(app, services) // 注册路由
|
||||
startServer(app, cfg) // 启动服务器
|
||||
}
|
||||
```
|
||||
|
||||
### 初始化函数职责
|
||||
|
||||
| 函数 | 职责 |
|
||||
|------|------|
|
||||
| `initConfig` | 加载配置文件 |
|
||||
| `initLogger` | 初始化 Zap 日志 |
|
||||
| `initDatabase` | 连接 PostgreSQL |
|
||||
| `initRedis` | 连接 Redis |
|
||||
| `initQueue` | 初始化 Asynq 客户端 |
|
||||
| `initServices` | 创建所有 Service 实例 |
|
||||
| `initMiddleware` | 注册全局中间件 |
|
||||
| `initRoutes` | 注册所有路由 |
|
||||
| `startServer` | 启动 HTTP 服务器 |
|
||||
|
||||
## 性能考量
|
||||
|
||||
### 关键性能指标
|
||||
|
||||
- API 响应时间 P95 < 200ms
|
||||
- 递归查询下级 ID P95 < 50ms(含 Redis 缓存)
|
||||
- 数据库查询 P95 < 50ms
|
||||
|
||||
### 优化措施
|
||||
|
||||
1. **Redis 缓存**:缓存递归查询结果,避免重复数据库查询
|
||||
2. **索引优化**:关键字段都建立索引(`parent_id`、`shop_id`、`owner_id`)
|
||||
3. **批量操作**:账号-角色、角色-权限支持批量创建
|
||||
4. **连接池**:数据库和 Redis 配置合理的连接池
|
||||
|
||||
## 安全考量
|
||||
|
||||
### 数据隔离
|
||||
|
||||
- **店铺隔离**:通过 `shop_id` 实现多租户数据隔离
|
||||
- **层级权限**:用户只能访问自己及下级创建的数据
|
||||
- **root 用户**:跳过数据权限过滤,可访问所有数据
|
||||
|
||||
### 密码安全
|
||||
|
||||
- 密码字段使用 `json:"-"` 标签,不返回给客户端
|
||||
- 密码使用 bcrypt 加密存储
|
||||
|
||||
### 错误处理
|
||||
|
||||
- 查询下级 ID 失败时,降级为只返回自己的数据
|
||||
- 所有错误通过统一错误处理机制返回
|
||||
|
||||
## 扩展指南
|
||||
|
||||
### 添加新业务表的数据权限
|
||||
|
||||
1. 在业务表中添加 `owner_id` 和 `shop_id` 字段
|
||||
2. 在 Store 方法中应用 `DataPermissionScope`
|
||||
3. 确保创建记录时设置正确的 `owner_id` 和 `shop_id`
|
||||
|
||||
示例:
|
||||
|
||||
```go
|
||||
func (s *OrderStore) List(ctx context.Context) ([]*model.Order, error) {
|
||||
var orders []*model.Order
|
||||
err := s.db.WithContext(ctx).
|
||||
Scopes(postgres.DataPermissionScope(ctx, s.accountStore)).
|
||||
Find(&orders).Error
|
||||
return orders, err
|
||||
}
|
||||
```
|
||||
|
||||
### 添加新的 RBAC 实体
|
||||
|
||||
1. 创建 Model 和 DTO
|
||||
2. 创建 Store(CRUD 方法)
|
||||
3. 创建 Service(业务逻辑)
|
||||
4. 创建 Handler(HTTP 接口)
|
||||
5. 添加路由文件
|
||||
6. 更新 Services 容器
|
||||
|
||||
## 测试策略
|
||||
|
||||
### 单元测试
|
||||
|
||||
- 递归查询测试
|
||||
- 缓存读写测试
|
||||
- 数据权限 Scope 测试
|
||||
- 软删除测试
|
||||
|
||||
### 集成测试
|
||||
|
||||
- 数据库迁移测试
|
||||
- 层级数据权限过滤测试
|
||||
- 跨店铺数据隔离测试
|
||||
- API 端点测试
|
||||
|
||||
## 技术决策记录
|
||||
|
||||
### 为什么不使用外键?
|
||||
|
||||
1. **灵活性**:业务逻辑完全在代码中控制
|
||||
2. **性能**:无外键约束检查开销
|
||||
3. **分布式友好**:便于未来拆分微服务
|
||||
|
||||
### 为什么使用 WITH RECURSIVE?
|
||||
|
||||
1. **原生支持**:PostgreSQL 内置支持
|
||||
2. **性能优异**:单次查询获取所有下级
|
||||
3. **深度无限**:支持任意层级的递归
|
||||
|
||||
### 为什么缓存过期时间是 30 分钟?
|
||||
|
||||
1. **平衡性**:在实时性和性能之间取得平衡
|
||||
2. **业务特点**:账号层级变化不频繁
|
||||
3. **可配置**:可根据业务需求调整
|
||||
Reference in New Issue
Block a user