feat(shop-role): 实现店铺角色继承功能和权限检查优化
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m39s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m39s
- 新增店铺角色管理 API 和数据模型 - 实现角色继承和权限检查逻辑 - 添加流程测试框架和集成测试 - 更新权限服务和账号管理逻辑 - 添加数据库迁移脚本 - 归档 OpenSpec 变更文档 Ultraworked with Sisyphus
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
# permission-check Specification
|
||||
|
||||
## Purpose
|
||||
TBD - created by archiving change implement-permission-check. Update Purpose after archive.
|
||||
|
||||
提供完整的权限检查能力,支持基于角色的权限验证和店铺级角色继承机制,实现细粒度的访问控制。
|
||||
## Requirements
|
||||
### Requirement: 权限检查核心服务
|
||||
|
||||
@@ -92,53 +93,117 @@ CheckPermission(ctx context.Context, userID uint, permCode string, platform stri
|
||||
|
||||
### Requirement: 权限查询链式执行
|
||||
|
||||
权限检查 SHALL 按照以下顺序执行查询:
|
||||
权限检查 SHALL 按照以下顺序执行查询(增加店铺角色继承逻辑):
|
||||
|
||||
1. 检查用户类型(超级管理员跳过)
|
||||
2. 查询用户的角色 ID 列表
|
||||
2. **查询用户的角色 ID 列表(增加店铺角色继承)**:
|
||||
- 优先查询账号级角色(`tb_account_role`)
|
||||
- 如果账号级角色为空 **且用户是代理账号(UserType=3)且有 shop_id**:
|
||||
- 查询店铺级角色(`tb_shop_role`)
|
||||
- 返回店铺级角色作为继承角色
|
||||
3. 查询角色的权限 ID 列表(去重)
|
||||
4. 查询权限详情列表
|
||||
5. 遍历匹配 `permCode` 和 `platform`
|
||||
|
||||
#### Scenario: 正常查询流程
|
||||
**角色解析函数签名**:
|
||||
```go
|
||||
GetRoleIDsForAccount(ctx context.Context, accountID uint) ([]uint, error)
|
||||
```
|
||||
|
||||
#### Scenario: 正常查询流程(现有行为保持不变)
|
||||
|
||||
- **WHEN** 调用 `CheckPermission` 检查普通用户权限
|
||||
- **THEN** 按顺序执行以下查询:
|
||||
1. `AccountRoleStore.GetRoleIDsByAccountID(ctx, userID)` 获取角色 ID 列表
|
||||
1. 调用 `AccountService.GetRoleIDsForAccount(ctx, userID)` 获取角色 ID 列表(含继承逻辑)
|
||||
2. `RolePermissionStore.GetPermIDsByRoleIDs(ctx, roleIDs)` 获取权限 ID 列表
|
||||
3. `PermissionStore.GetByIDs(ctx, permIDs)` 获取权限详情
|
||||
- **AND** 遍历权限列表进行匹配
|
||||
- **AND** 找到匹配权限后立即返回 `true`(短路优化)
|
||||
|
||||
#### Scenario: 空结果短路
|
||||
#### Scenario: 代理账号继承店铺角色
|
||||
|
||||
- **WHEN** 任意查询步骤返回空列表(如用户无角色)
|
||||
- **WHEN** 调用 `CheckPermission` 检查代理账号(UserType=3)权限
|
||||
- **AND** 该账号未分配账号级角色(`tb_account_role` 中无记录)
|
||||
- **AND** 该账号的 `shop_id` 不为 NULL
|
||||
- **AND** 该店铺已分配店铺级角色(`tb_shop_role` 中有记录)
|
||||
- **THEN** `GetRoleIDsForAccount` 返回店铺级角色 ID 列表
|
||||
- **AND** 后续权限检查使用店铺级角色的权限
|
||||
|
||||
#### Scenario: 代理账号有自己角色时不继承
|
||||
|
||||
- **WHEN** 调用 `CheckPermission` 检查代理账号权限
|
||||
- **AND** 该账号已分配账号级角色(`tb_account_role` 中有记录)
|
||||
- **THEN** `GetRoleIDsForAccount` 返回账号级角色 ID 列表
|
||||
- **AND** 不查询店铺级角色(优先级:账号 > 店铺)
|
||||
- **AND** 后续权限检查使用账号级角色的权限
|
||||
|
||||
#### Scenario: 代理账号无角色也无店铺角色
|
||||
|
||||
- **WHEN** 调用 `CheckPermission` 检查代理账号权限
|
||||
- **AND** 该账号未分配账号级角色
|
||||
- **AND** 该账号的店铺未分配店铺级角色(`tb_shop_role` 中无记录)
|
||||
- **THEN** `GetRoleIDsForAccount` 返回空数组
|
||||
- **AND** 后续权限检查返回 `false`(无权限)
|
||||
|
||||
#### Scenario: 非代理账号不继承店铺角色
|
||||
|
||||
- **WHEN** 调用 `CheckPermission` 检查平台用户(UserType=2)权限
|
||||
- **AND** 该账号未分配账号级角色
|
||||
- **THEN** `GetRoleIDsForAccount` 返回空数组
|
||||
- **AND** 不查询店铺级角色(仅代理账号支持继承)
|
||||
|
||||
#### Scenario: 空结果短路(现有行为保持不变)
|
||||
|
||||
- **WHEN** `GetRoleIDsForAccount` 返回空列表(账号无角色且店铺无角色)
|
||||
- **THEN** 立即返回 `(false, nil)`
|
||||
- **AND** 不执行后续查询
|
||||
- **AND** 不执行后续查询(角色权限查询、权限详情查询)
|
||||
|
||||
### Requirement: Service 依赖注入
|
||||
|
||||
Permission Service SHALL 在初始化时注入所需的 Store 依赖。
|
||||
Permission Service SHALL 在初始化时注入所需的 Store 和 Service 依赖。
|
||||
|
||||
**依赖**:
|
||||
- `PermissionStore` - 查询权限详情
|
||||
- `AccountRoleStore` - 查询用户角色关联
|
||||
- `AccountRoleStore` - 查询用户角色关联(保留向后兼容)
|
||||
- `RolePermissionStore` - 查询角色权限关联
|
||||
- `AccountService` - 角色解析服务(含店铺角色继承逻辑)
|
||||
- `RedisClient` - 权限缓存
|
||||
|
||||
**修改的依赖**:
|
||||
```go
|
||||
type Service struct {
|
||||
permissionStore *postgres.PermissionStore
|
||||
accountRoleStore *postgres.AccountRoleStore // 保留但不直接使用
|
||||
rolePermStore *postgres.RolePermissionStore
|
||||
accountService *account.Service // 新增:用于角色解析
|
||||
redisClient *redis.Client
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: Service 初始化
|
||||
|
||||
- **WHEN** 创建 Permission Service 实例
|
||||
- **THEN** 构造函数接收以下参数:
|
||||
- `permissionStore *postgres.PermissionStore`
|
||||
- `accountRoleStore *postgres.AccountRoleStore`
|
||||
- `accountRoleStore *postgres.AccountRoleStore`(保留向后兼容)
|
||||
- `rolePermStore *postgres.RolePermissionStore`
|
||||
- `accountService *account.Service`(新增)
|
||||
- `redisClient *redis.Client`
|
||||
- **AND** 存储在结构体字段中供 `CheckPermission` 使用
|
||||
|
||||
#### Scenario: CheckPermission 使用新的角色解析
|
||||
|
||||
- **WHEN** `CheckPermission` 需要查询用户角色时
|
||||
- **THEN** 调用 `s.accountService.GetRoleIDsForAccount(ctx, userID)`
|
||||
- **AND** 不再直接调用 `s.accountRoleStore.GetRoleIDsByAccountID()`
|
||||
- **AND** 获得的角色 ID 列表可能是账号级角色或店铺级角色
|
||||
|
||||
#### Scenario: Bootstrap 集成
|
||||
|
||||
- **WHEN** 在 `internal/bootstrap/services.go` 初始化 Permission Service
|
||||
- **THEN** 传入所有必需的 Store 依赖
|
||||
- **THEN** 传入所有必需的 Store 和 Service 依赖
|
||||
- **AND** Store 依赖已在 `initStores()` 中初始化
|
||||
- **AND** Account Service 已在 Permission Service 之前初始化
|
||||
|
||||
### Requirement: 错误处理和日志
|
||||
|
||||
@@ -163,3 +228,120 @@ Permission Service SHALL 在初始化时注入所需的 Store 依赖。
|
||||
- 检查结果
|
||||
- **AND** 用于安全审计和问题排查
|
||||
|
||||
### Requirement: 角色解析服务
|
||||
|
||||
系统 SHALL 提供 `GetRoleIDsForAccount` 方法,统一处理账号角色查询和店铺角色继承逻辑。
|
||||
|
||||
**实现位置**: `internal/service/account/role_resolver.go`
|
||||
|
||||
**方法签名**:
|
||||
```go
|
||||
func (s *Service) GetRoleIDsForAccount(ctx context.Context, accountID uint) ([]uint, error)
|
||||
```
|
||||
|
||||
**返回值**:
|
||||
- `[]uint`: 角色 ID 列表(可能是账号级角色或店铺级角色)
|
||||
- `error`: 查询失败时的错误信息
|
||||
|
||||
#### Scenario: 角色解析 - 超级管理员
|
||||
|
||||
- **WHEN** 调用 `GetRoleIDsForAccount` 查询超级管理员(UserType=1)的角色
|
||||
- **THEN** 返回空数组 `[]uint{}`(超级管理员无角色,跳过权限检查)
|
||||
- **AND** 不执行任何数据库查询
|
||||
|
||||
#### Scenario: 角色解析 - 平台用户
|
||||
|
||||
- **WHEN** 调用 `GetRoleIDsForAccount` 查询平台用户(UserType=2)的角色
|
||||
- **THEN** 查询 `tb_account_role` 表获取账号级角色
|
||||
- **AND** 返回账号级角色 ID 列表
|
||||
- **AND** 不查询店铺级角色(平台用户无 shop_id)
|
||||
|
||||
#### Scenario: 角色解析 - 代理账号有账号级角色
|
||||
|
||||
- **WHEN** 调用 `GetRoleIDsForAccount` 查询代理账号(UserType=3)的角色
|
||||
- **AND** 该账号已分配账号级角色
|
||||
- **THEN** 查询 `tb_account_role` 表获取账号级角色
|
||||
- **AND** 返回账号级角色 ID 列表
|
||||
- **AND** 不查询店铺级角色(账号角色优先)
|
||||
|
||||
#### Scenario: 角色解析 - 代理账号继承店铺角色
|
||||
|
||||
- **WHEN** 调用 `GetRoleIDsForAccount` 查询代理账号的角色
|
||||
- **AND** 该账号未分配账号级角色(`tb_account_role` 查询结果为空)
|
||||
- **AND** 该账号的 `shop_id` 不为 NULL
|
||||
- **THEN** 查询 `tb_shop_role` 表获取店铺级角色
|
||||
- **AND** 返回店铺级角色 ID 列表(继承)
|
||||
|
||||
#### Scenario: 角色解析 - 企业账号
|
||||
|
||||
- **WHEN** 调用 `GetRoleIDsForAccount` 查询企业账号(UserType=4)的角色
|
||||
- **THEN** 查询 `tb_account_role` 表获取账号级角色
|
||||
- **AND** 返回账号级角色 ID 列表
|
||||
- **AND** 不查询店铺级角色(企业账号无继承机制)
|
||||
|
||||
#### Scenario: 角色解析 - 数据库查询失败
|
||||
|
||||
- **WHEN** 调用 `GetRoleIDsForAccount` 过程中数据库查询失败
|
||||
- **THEN** 返回错误 `errors.Wrap(errors.CodeInternalError, err, "查询角色失败")`
|
||||
- **AND** 不返回部分结果
|
||||
|
||||
### Requirement: 缓存机制兼容
|
||||
|
||||
权限缓存机制 SHALL 与店铺角色继承逻辑兼容,确保角色变更后缓存及时失效。
|
||||
|
||||
**缓存键**: `user:permissions:{user_id}`
|
||||
|
||||
**缓存内容**: 用户的所有权限列表(不区分账号级角色还是店铺级角色)
|
||||
|
||||
**缓存时效**: 30 分钟
|
||||
|
||||
#### Scenario: 缓存命中时使用缓存
|
||||
|
||||
- **WHEN** 调用 `CheckPermission` 检查用户权限
|
||||
- **AND** Redis 中存在缓存键 `user:permissions:{user_id}`
|
||||
- **THEN** 直接从缓存读取权限列表
|
||||
- **AND** 不调用 `GetRoleIDsForAccount`(避免查询)
|
||||
- **AND** 使用缓存的权限进行匹配
|
||||
|
||||
#### Scenario: 缓存未命中时重建缓存
|
||||
|
||||
- **WHEN** 调用 `CheckPermission` 检查用户权限
|
||||
- **AND** Redis 中不存在缓存键
|
||||
- **THEN** 调用 `GetRoleIDsForAccount` 查询角色(含继承逻辑)
|
||||
- **AND** 查询角色的所有权限
|
||||
- **AND** 将权限列表写入 Redis,TTL 30 分钟
|
||||
|
||||
#### Scenario: 店铺角色变更时清理缓存
|
||||
|
||||
- **WHEN** 店铺角色变更(分配/删除)
|
||||
- **THEN** 查询该店铺下所有账号 ID 列表
|
||||
- **AND** 遍历删除每个账号的权限缓存键 `user:permissions:{account_id}`
|
||||
- **AND** 下次权限检查时,自动重建缓存(使用新的角色解析逻辑)
|
||||
|
||||
#### Scenario: 账号角色变更时清理缓存(现有行为)
|
||||
|
||||
- **WHEN** 账号级角色变更(分配/删除)
|
||||
- **THEN** 删除该账号的权限缓存键 `user:permissions:{account_id}`
|
||||
- **AND** 下次权限检查时,重建缓存
|
||||
|
||||
### Requirement: 性能要求
|
||||
|
||||
角色继承逻辑 SHALL 满足以下性能要求:
|
||||
|
||||
- 角色解析查询时间 < 10ms(含店铺角色查询)
|
||||
- 权限检查总时间 < 50ms(含角色解析、权限查询、匹配)
|
||||
- 缓存命中时权限检查时间 < 1ms
|
||||
|
||||
#### Scenario: 角色解析性能
|
||||
|
||||
- **WHEN** 调用 `GetRoleIDsForAccount` 查询代理账号角色
|
||||
- **AND** 账号无账号级角色,需查询店铺级角色
|
||||
- **THEN** 总查询时间(账号角色查询 + 店铺角色查询)< 10ms
|
||||
- **AND** 使用索引 `idx_shop_role_shop_id` 优化查询
|
||||
|
||||
#### Scenario: 缓存命中性能
|
||||
|
||||
- **WHEN** 调用 `CheckPermission` 且缓存命中
|
||||
- **THEN** 总处理时间 < 1ms
|
||||
- **AND** 不执行任何数据库查询
|
||||
|
||||
|
||||
323
openspec/specs/shop-role-management/spec.md
Normal file
323
openspec/specs/shop-role-management/spec.md
Normal file
@@ -0,0 +1,323 @@
|
||||
# shop-role-management Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
提供店铺级角色管理能力,允许平台为代理店铺设置默认角色,该店铺下所有账号自动继承,简化 MVP 阶段的批量角色分配操作。
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: 分配店铺角色
|
||||
|
||||
系统 SHALL 提供接口允许平台用户或店铺管理员为店铺分配角色。
|
||||
|
||||
**接口**: `POST /api/admin/shops/:shop_id/roles`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"role_ids": [5] // 角色 ID 列表,传空数组表示清空所有角色
|
||||
}
|
||||
```
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": [
|
||||
{
|
||||
"id": 1,
|
||||
"shop_id": 10,
|
||||
"role_id": 5,
|
||||
"status": 1,
|
||||
"created_at": "2026-02-02T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"timestamp": "2026-02-02T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: 成功分配单个角色
|
||||
|
||||
- **WHEN** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": [5]}`
|
||||
- **AND** 角色 ID 5 存在且为客户角色(RoleType=2)
|
||||
- **AND** 店铺 ID 10 存在
|
||||
- **THEN** 系统创建店铺-角色关联记录
|
||||
- **AND** 返回 HTTP 200 和关联记录
|
||||
- **AND** 清理该店铺下所有账号的权限缓存
|
||||
|
||||
#### Scenario: 清空店铺所有角色
|
||||
|
||||
- **WHEN** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": []}`
|
||||
- **THEN** 系统删除该店铺的所有角色关联
|
||||
- **AND** 返回 HTTP 200 和空数组
|
||||
- **AND** 清理该店铺下所有账号的权限缓存
|
||||
|
||||
#### Scenario: 替换现有角色
|
||||
|
||||
- **WHEN** 店铺已分配角色 ID 5
|
||||
- **AND** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": [7]}`
|
||||
- **THEN** 系统删除原有角色 ID 5 的关联
|
||||
- **AND** 创建新的角色 ID 7 的关联
|
||||
- **AND** 返回 HTTP 200 和新关联记录
|
||||
|
||||
#### Scenario: 角色类型校验失败
|
||||
|
||||
- **WHEN** 平台用户调用 `POST /api/admin/shops/10/roles` 请求体为 `{"role_ids": [3]}`
|
||||
- **AND** 角色 ID 3 是平台角色(RoleType=1)
|
||||
- **THEN** 返回 HTTP 400 错误码 `errors.CodeInvalidParam`
|
||||
- **AND** 错误消息为"店铺只能分配客户角色"
|
||||
- **AND** 不创建任何关联记录
|
||||
|
||||
#### Scenario: 店铺不存在
|
||||
|
||||
- **WHEN** 平台用户调用 `POST /api/admin/shops/999/roles`
|
||||
- **AND** 店铺 ID 999 不存在
|
||||
- **THEN** 返回 HTTP 404 错误码 `errors.CodeNotFound`
|
||||
- **AND** 错误消息为"店铺不存在"
|
||||
|
||||
#### Scenario: 权限不足
|
||||
|
||||
- **WHEN** 代理用户调用 `POST /api/admin/shops/20/roles`
|
||||
- **AND** 店铺 ID 20 不在该代理的管理范围内(不是自己店铺或下级店铺)
|
||||
- **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden`
|
||||
- **AND** 错误消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
### Requirement: 查询店铺角色
|
||||
|
||||
系统 SHALL 提供接口查询店铺已分配的角色列表。
|
||||
|
||||
**接口**: `GET /api/admin/shops/:shop_id/roles`
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"shop_id": 10,
|
||||
"roles": [
|
||||
{
|
||||
"shop_id": 10,
|
||||
"role_id": 5,
|
||||
"role_name": "代理店长",
|
||||
"role_desc": "代理店铺管理员",
|
||||
"status": 1
|
||||
}
|
||||
]
|
||||
},
|
||||
"timestamp": "2026-02-02T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: 查询已分配角色
|
||||
|
||||
- **WHEN** 平台用户调用 `GET /api/admin/shops/10/roles`
|
||||
- **AND** 店铺 ID 10 已分配角色 ID 5
|
||||
- **THEN** 返回 HTTP 200 和角色详情列表
|
||||
- **AND** 包含角色名称、描述等信息
|
||||
|
||||
#### Scenario: 查询未分配角色的店铺
|
||||
|
||||
- **WHEN** 平台用户调用 `GET /api/admin/shops/10/roles`
|
||||
- **AND** 店铺 ID 10 未分配任何角色
|
||||
- **THEN** 返回 HTTP 200
|
||||
- **AND** `roles` 字段为空数组
|
||||
|
||||
#### Scenario: 店铺不存在
|
||||
|
||||
- **WHEN** 平台用户调用 `GET /api/admin/shops/999/roles`
|
||||
- **AND** 店铺 ID 999 不存在
|
||||
- **THEN** 返回 HTTP 404 错误码 `errors.CodeNotFound`
|
||||
- **AND** 错误消息为"店铺不存在"
|
||||
|
||||
#### Scenario: 权限不足
|
||||
|
||||
- **WHEN** 代理用户调用 `GET /api/admin/shops/20/roles`
|
||||
- **AND** 店铺 ID 20 不在该代理的管理范围内
|
||||
- **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden`
|
||||
- **AND** 错误消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
### Requirement: 删除店铺角色
|
||||
|
||||
系统 SHALL 提供接口删除店铺的特定角色关联。
|
||||
|
||||
**接口**: `DELETE /api/admin/shops/:shop_id/roles/:role_id`
|
||||
|
||||
**响应体**:
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": null,
|
||||
"timestamp": "2026-02-02T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: 成功删除角色
|
||||
|
||||
- **WHEN** 平台用户调用 `DELETE /api/admin/shops/10/roles/5`
|
||||
- **AND** 店铺 ID 10 存在
|
||||
- **AND** 店铺已分配角色 ID 5
|
||||
- **THEN** 系统删除该关联记录
|
||||
- **AND** 返回 HTTP 200
|
||||
- **AND** 清理该店铺下所有账号的权限缓存
|
||||
|
||||
#### Scenario: 删除不存在的角色关联
|
||||
|
||||
- **WHEN** 平台用户调用 `DELETE /api/admin/shops/10/roles/5`
|
||||
- **AND** 店铺 ID 10 未分配角色 ID 5
|
||||
- **THEN** 返回 HTTP 200(幂等操作)
|
||||
- **AND** 不执行任何数据库操作
|
||||
|
||||
#### Scenario: 店铺不存在
|
||||
|
||||
- **WHEN** 平台用户调用 `DELETE /api/admin/shops/999/roles/5`
|
||||
- **AND** 店铺 ID 999 不存在
|
||||
- **THEN** 返回 HTTP 404 错误码 `errors.CodeNotFound`
|
||||
- **AND** 错误消息为"店铺不存在"
|
||||
|
||||
#### Scenario: 权限不足
|
||||
|
||||
- **WHEN** 代理用户调用 `DELETE /api/admin/shops/20/roles/5`
|
||||
- **AND** 店铺 ID 20 不在该代理的管理范围内
|
||||
- **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden`
|
||||
- **AND** 错误消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
### Requirement: 数据库表结构
|
||||
|
||||
系统 SHALL 创建 `tb_shop_role` 表存储店铺-角色关联关系。
|
||||
|
||||
**表结构**:
|
||||
```sql
|
||||
CREATE TABLE tb_shop_role (
|
||||
id SERIAL PRIMARY KEY,
|
||||
shop_id INT NOT NULL,
|
||||
role_id INT NOT NULL,
|
||||
status INT NOT NULL DEFAULT 1, -- 0=禁用 1=启用
|
||||
creator INT NOT NULL,
|
||||
updater INT NOT NULL,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
deleted_at TIMESTAMP,
|
||||
UNIQUE (shop_id, role_id) WHERE deleted_at IS NULL
|
||||
);
|
||||
```
|
||||
|
||||
**索引**:
|
||||
- `idx_shop_role_shop_id` - 查询店铺角色(高频)
|
||||
- `idx_shop_role_role_id` - 查询角色被哪些店铺使用(低频)
|
||||
- `idx_shop_role_deleted_at` - 软删除过滤
|
||||
|
||||
#### Scenario: 唯一性约束
|
||||
|
||||
- **WHEN** 尝试为同一店铺分配同一角色两次
|
||||
- **THEN** 数据库返回唯一性约束冲突错误
|
||||
- **AND** 系统捕获错误并返回友好错误消息
|
||||
|
||||
#### Scenario: 软删除机制
|
||||
|
||||
- **WHEN** 删除店铺角色关联
|
||||
- **THEN** 系统设置 `deleted_at` 字段为当前时间
|
||||
- **AND** 后续查询自动过滤 `deleted_at IS NOT NULL` 的记录
|
||||
|
||||
### Requirement: 缓存失效策略
|
||||
|
||||
系统 SHALL 在店铺角色变更时清理相关账号的权限缓存。
|
||||
|
||||
#### Scenario: 分配角色时清理缓存
|
||||
|
||||
- **WHEN** 为店铺 ID 10 分配角色
|
||||
- **THEN** 系统查询该店铺下所有账号 ID 列表
|
||||
- **AND** 遍历删除每个账号的权限缓存键 `user:permissions:{account_id}`
|
||||
- **AND** 下次权限检查时,账号会重新查询并继承新角色
|
||||
|
||||
#### Scenario: 删除角色时清理缓存
|
||||
|
||||
- **WHEN** 删除店铺 ID 10 的角色关联
|
||||
- **THEN** 系统查询该店铺下所有账号 ID 列表
|
||||
- **AND** 遍历删除每个账号的权限缓存键
|
||||
- **AND** 下次权限检查时,账号将无角色(如果无账号级角色)
|
||||
|
||||
#### Scenario: 账号有自己角色时不受影响
|
||||
|
||||
- **WHEN** 店铺角色变更
|
||||
- **AND** 某账号有自己的账号级角色
|
||||
- **THEN** 该账号的权限缓存被清理
|
||||
- **AND** 下次权限检查时,继续使用账号级角色(不继承店铺角色)
|
||||
|
||||
### Requirement: 权限控制
|
||||
|
||||
店铺角色管理接口 SHALL 实施权限控制,只有有权限的用户才能操作。
|
||||
|
||||
**权限规则**:
|
||||
- 超级管理员(UserType=1):可操作所有店铺
|
||||
- 平台用户(UserType=2):可操作所有店铺
|
||||
- 代理用户(UserType=3):只能操作自己店铺及下级店铺
|
||||
- 企业用户(UserType=4):无权限操作店铺角色
|
||||
|
||||
#### Scenario: 超级管理员操作任意店铺
|
||||
|
||||
- **WHEN** 超级管理员调用店铺角色管理接口
|
||||
- **THEN** 跳过权限检查
|
||||
- **AND** 允许操作任意店铺
|
||||
|
||||
#### Scenario: 平台用户操作任意店铺
|
||||
|
||||
- **WHEN** 平台用户调用店铺角色管理接口
|
||||
- **THEN** 允许操作任意店铺
|
||||
|
||||
#### Scenario: 代理用户操作下级店铺
|
||||
|
||||
- **WHEN** 代理用户(shop_id=10)调用店铺角色管理接口
|
||||
- **AND** 目标店铺 ID 15 是店铺 10 的下级店铺
|
||||
- **THEN** 调用 `middleware.CanManageShop(ctx, 15, shopStore)`
|
||||
- **AND** 返回 nil(有权限)
|
||||
- **AND** 允许操作
|
||||
|
||||
#### Scenario: 代理用户操作无关店铺
|
||||
|
||||
- **WHEN** 代理用户(shop_id=10)调用店铺角色管理接口
|
||||
- **AND** 目标店铺 ID 20 不是店铺 10 的下级店铺
|
||||
- **THEN** 调用 `middleware.CanManageShop(ctx, 20, shopStore)`
|
||||
- **AND** 返回 error(无权限)
|
||||
- **AND** 拒绝操作
|
||||
|
||||
#### Scenario: 企业用户尝试操作店铺角色
|
||||
|
||||
- **WHEN** 企业用户调用店铺角色管理接口
|
||||
- **THEN** 返回 HTTP 403 错误码 `errors.CodeForbidden`
|
||||
- **AND** 错误消息为"无权限操作该资源或资源不存在"
|
||||
|
||||
### Requirement: 业务规则校验
|
||||
|
||||
店铺角色分配 SHALL 执行业务规则校验,确保数据一致性。
|
||||
|
||||
#### Scenario: 角色存在性校验
|
||||
|
||||
- **WHEN** 分配店铺角色时指定角色 ID 列表
|
||||
- **THEN** 系统查询所有角色是否存在
|
||||
- **AND** 如果部分角色不存在,返回错误"部分角色不存在"
|
||||
- **AND** 不创建任何关联记录(原子操作)
|
||||
|
||||
#### Scenario: 角色状态校验
|
||||
|
||||
- **WHEN** 分配店铺角色时指定角色 ID
|
||||
- **AND** 该角色的 `status` 字段为 0(禁用)
|
||||
- **THEN** 返回错误"角色已禁用"
|
||||
- **AND** 不创建关联记录
|
||||
|
||||
#### Scenario: 角色类型校验
|
||||
|
||||
- **WHEN** 分配店铺角色时指定角色 ID
|
||||
- **AND** 该角色的 `role_type` 字段为 1(平台角色)
|
||||
- **THEN** 返回错误"店铺只能分配客户角色"
|
||||
- **AND** 不创建关联记录
|
||||
|
||||
#### Scenario: 店铺存在性校验
|
||||
|
||||
- **WHEN** 分配店铺角色时指定店铺 ID
|
||||
- **AND** 该店铺不存在或已软删除
|
||||
- **THEN** 返回错误"店铺不存在"
|
||||
- **AND** 不执行任何操作
|
||||
|
||||
Reference in New Issue
Block a user