Files
huang 1b9080e3ab 实现角色权限体系重构
本次提交完成了角色权限体系的重构,主要包括:

1. 数据库迁移
   - 添加 tb_permission.platform 字段(all/web/h5)
   - 更新 tb_role.role_type 注释(1=平台角色,2=客户角色)

2. GORM 模型更新
   - Permission 模型添加 Platform 字段
   - Role 模型更新 RoleType 注释

3. 常量定义
   - 新增角色类型常量(RoleTypePlatform, RoleTypeCustomer)
   - 新增权限端口常量(PlatformAll, PlatformWeb, PlatformH5)
   - 添加角色类型与用户类型匹配规则函数

4. Store 层实现
   - Permission Store 支持按 platform 过滤
   - Account Role Store 添加 CountByAccountID 方法

5. Service 层实现
   - 角色分配支持类型匹配校验
   - 角色分配支持数量限制(超级管理员0个,平台用户无限制,代理/企业1个)
   - Permission Service 支持 platform 过滤

6. 权限校验中间件
   - 实现 RequirePermission、RequireAnyPermission、RequireAllPermissions
   - 支持 platform 字段过滤
   - 支持跳过超级管理员检查

7. 测试用例
   - 角色类型匹配规则单元测试
   - 角色分配数量限制单元测试
   - 权限 platform 过滤单元测试
   - 权限校验中间件集成测试(占位)

8. 代码清理
   - 删除过时的 subordinate 测试文件
   - 移除 Account.ParentID 相关引用
   - 更新 DTO 验证规则

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-10 09:51:52 +08:00

248 lines
8.0 KiB
Markdown
Raw Permalink 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.
# Design: 角色权限体系架构设计
## Context
### 背景
根据用户需求,系统有两类角色:
1. **平台角色**: 用于区分平台用户的不同职责(运营、客服、管理员等)
2. **客户角色**: 用于决定代理/企业客户的能力边界(可以做什么操作)
同时,权限需要按端口区分:
- Web 后台:运营/代理可登录
- H5/小程序(企业/代理):企业/代理可登录
- H5/小程序(个人):个人客户可登录
### 约束条件
- 平台用户可以分配多个角色
- 代理/企业账号只能分配一种角色
- 个人客户没有角色
- 某些接口会被复用(前端根据权限控制显示)
- 权限既要控制接口访问,又要告诉前端展示哪些菜单/按钮
## Goals / Non-Goals
### Goals
1. 重新定义角色类型,区分平台角色和客户角色
2. 为权限添加端口属性,支持按端口过滤
3. 实现账号-角色分配的数量限制
4. 为前端提供权限列表用于菜单/按钮控制
### Non-Goals
1. 本提案不实现具体的权限校验中间件(已在 auth spec 中定义)
2. 本提案不创建初始角色和权限数据(由业务初始化脚本处理)
3. 本提案不处理个人客户的登录认证
## Decisions
### Decision 1: 角色类型重定义
**决策**: 将 role_type 重新定义为:
- `1` = 平台角色(适用于平台用户)
- `2` = 客户角色(适用于代理/企业账号)
**理由**:
- 原设计的"超级/代理/企业"角色类型与用户类型耦合过紧
- 新设计区分"角色的适用范围"而非"角色的所有者类型"
- 客户角色可以同时适用于代理和企业,便于权限复用
**变更**:
- 原 role_type = 1超级→ 废弃(超级管理员不需要角色)
- 原 role_type = 2代理→ role_type = 2客户角色
- 原 role_type = 3企业→ 合并到 role_type = 2客户角色
- 新增 role_type = 1平台角色
### Decision 2: 权限端口字段
**决策**: 在 Permission 表添加 `platform` 字段,类型为 varchar(20),默认值 'all'。
```go
Platform string `gorm:"type:varchar(20);default:'all'"` // all-全部 web-Web后台 h5-H5端
```
**理由**:
- 折中方案:不强制隔离,但提供灵活性
- 前端可以根据 platform 过滤菜单
- 后端校验时可以根据请求来源和权限的 platform 进行验证
**使用场景**:
- `all`: 通用权限,如"查看订单"、"创建客户"
- `web`: 仅 Web 后台使用,如"导出报表"、"批量操作"
- `h5`: 仅 H5 使用,如"扫码登录"、"微信支付"
### Decision 3: 账号-角色数量限制
**决策**: 在 Service 层实现角色数量限制,而非数据库约束。
**实现逻辑**:
```go
func (s *AccountRoleService) AssignRole(accountID, roleID uint) error {
// 1. 查询账号信息
account := s.accountStore.GetByID(accountID)
// 2. 根据用户类型判断限制
switch account.UserType {
case constants.UserTypeSuperAdmin:
return errors.New("超级管理员不需要分配角色")
case constants.UserTypePlatform:
// 平台用户可分配多个角色,无限制
case constants.UserTypeAgent, constants.UserTypeEnterprise:
// 代理/企业只能分配一个角色,先检查是否已有角色
existingRoles := s.accountRoleStore.GetByAccountID(accountID)
if len(existingRoles) > 0 {
return errors.New("该账号类型只能分配一个角色")
}
}
// 3. 检查角色类型是否匹配用户类型
role := s.roleStore.GetByID(roleID)
if !s.isRoleTypeMatchUserType(role.RoleType, account.UserType) {
return errors.New("角色类型与账号类型不匹配")
}
// 4. 创建关联
return s.accountRoleStore.Create(accountID, roleID)
}
```
**理由**:
- 业务规则在 Service 层实现,便于修改和扩展
- 数据库层面不加限制,保持灵活性
- 错误信息更友好,便于前端展示
### Decision 4: 角色类型与用户类型匹配规则
**决策**: 定义角色类型与用户类型的匹配关系。
| 用户类型 | 可分配的角色类型 |
|---------|----------------|
| 超级管理员 (1) | 无 |
| 平台用户 (2) | 平台角色 (1) |
| 代理账号 (3) | 客户角色 (2) |
| 企业账号 (4) | 客户角色 (2) |
**理由**:
- 平台用户只能分配平台角色
- 代理和企业可以共享客户角色(如"基础查看"、"高级操作"等)
- 便于权限管理和角色复用
### Decision 5: 权限校验流程
**决策**: 权限校验分两步:
1. **接口权限**: 中间件根据请求路径匹配权限编码,检查用户是否拥有该权限
2. **端口权限**: 中间件根据请求来源Web/H5和权限的 platform 字段进行二次校验
**流程**:
```
请求 → 认证中间件 → 权限中间件
1. 解析请求路径,匹配权限编码
2. 查询用户的所有权限
3. 检查权限是否匹配
4. 检查权限的 platform 是否与请求来源匹配
通过 / 拒绝
```
## Data Models
### Role角色- 修改
```go
type Role struct {
gorm.Model
BaseModel `gorm:"embedded"`
RoleName string `gorm:"not null;size:50"` // 角色名称
RoleDesc string `gorm:"size:255"` // 角色描述
RoleType int `gorm:"not null;index"` // 角色类型 1=平台角色 2=客户角色
Status int `gorm:"not null;default:1"` // 状态 0=禁用 1=启用
}
```
### Permission权限- 修改
```go
type Permission struct {
gorm.Model
BaseModel `gorm:"embedded"`
PermName string `gorm:"not null;size:50"` // 权限名称
PermCode string `gorm:"uniqueIndex;size:100"` // 权限编码
PermType int `gorm:"not null;index"` // 权限类型 1=菜单 2=按钮
Platform string `gorm:"type:varchar(20);default:'all'"` // 适用端口 all=全部 web=Web后台 h5=H5端
URL string `gorm:"size:255"` // URL路径可选
ParentID *uint `gorm:"index"` // 上级权限ID
Sort int `gorm:"not null;default:0"` // 排序
Status int `gorm:"not null;default:1"` // 状态 0=禁用 1=启用
}
```
## API Design
### 获取当前用户权限列表
```
GET /api/v1/account/permissions
Query: platform=web|h5 (可选,过滤端口)
Response:
{
"code": 0,
"message": "success",
"data": {
"permissions": [
{
"perm_code": "order:view",
"perm_name": "查看订单",
"perm_type": 1,
"platform": "all"
}
],
"menus": [
{
"id": 1,
"name": "订单管理",
"url": "/orders",
"children": [...]
}
]
}
}
```
## Risks / Trade-offs
### Risk 1: 角色类型变更影响现有数据
- **风险**: role_type 的含义变更可能影响现有角色数据
- **缓解**: 当前系统无实际数据,可以直接重新定义
### Risk 2: 权限端口字段的维护成本
- **风险**: 新增权限时需要考虑端口属性,增加维护成本
- **缓解**: 默认值为 'all',只有特殊权限才需要设置
### Risk 3: 角色数量限制的绕过
- **风险**: 直接操作数据库可能绕过 Service 层的数量限制
- **缓解**: 所有操作通过 API 进行,数据库直接操作需审批
## Migration Plan
1. 修改 `tb_role` 表:更新 role_type 的注释说明
2. 修改 `tb_permission` 表:添加 `platform` 字段,默认值 'all'
3. 更新 GORM 模型定义
4. 添加常量定义(角色类型、权限端口)
5. 实现 Service 层的角色分配逻辑
6. 更新权限校验中间件
## Open Questions
1. ~~是否需要为不同端口创建独立的权限树?~~ - 不需要,使用 platform 字段过滤即可
2. ~~客户角色是否需要进一步细分(代理专用/企业专用)?~~ - 暂不需要,共用客户角色