feat(shop-role): 实现店铺角色继承功能和权限检查优化
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m39s

- 新增店铺角色管理 API 和数据模型
- 实现角色继承和权限检查逻辑
- 添加流程测试框架和集成测试
- 更新权限服务和账号管理逻辑
- 添加数据库迁移脚本
- 归档 OpenSpec 变更文档

Ultraworked with Sisyphus
This commit is contained in:
2026-02-03 10:06:13 +08:00
parent bc7e5d6f6d
commit 5a90caa619
61 changed files with 21284 additions and 131 deletions

View File

@@ -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** 将权限列表写入 RedisTTL 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** 不执行任何数据库查询