Files
junhong_cmp_fiber/openspec/specs/permission-check/spec.md
huang 5a90caa619
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m39s
feat(shop-role): 实现店铺角色继承功能和权限检查优化
- 新增店铺角色管理 API 和数据模型
- 实现角色继承和权限检查逻辑
- 添加流程测试框架和集成测试
- 更新权限服务和账号管理逻辑
- 添加数据库迁移脚本
- 归档 OpenSpec 变更文档

Ultraworked with Sisyphus
2026-02-03 10:06:13 +08:00

348 lines
13 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.
# permission-check Specification
## Purpose
提供完整的权限检查能力,支持基于角色的权限验证和店铺级角色继承机制,实现细粒度的访问控制。
## Requirements
### Requirement: 权限检查核心服务
Permission Service SHALL 提供 `CheckPermission` 方法,用于检查用户是否拥有指定权限。
**签名**:
```go
CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error)
```
**参数**:
- `ctx`: 上下文(可选包含用户类型信息)
- `userID`: 用户 ID
- `permCode`: 权限编码(格式:`module:action`,如 `user:create`
- `platform`: 端口类型(`all`/`web`/`h5`
**返回值**:
- `bool`: 是否拥有权限true = 有权限false = 无权限)
- `error`: 错误信息(查询失败时)
#### Scenario: 超级管理员权限检查
- **WHEN** 调用 `CheckPermission` 检查超级管理员user_type = 1的权限
- **THEN** 直接返回 `(true, nil)`
- **AND** 不执行任何数据库查询
- **AND** 忽略 `permCode``platform` 参数
#### Scenario: 有权限的普通用户
- **WHEN** 调用 `CheckPermission` 检查普通用户权限
- **AND** 用户通过角色关联拥有该权限
- **AND** 权限的 `permCode` 匹配
- **AND** 权限的 `platform``all` 或匹配请求的 `platform`
- **THEN** 返回 `(true, nil)`
#### Scenario: 无权限的普通用户
- **WHEN** 调用 `CheckPermission` 检查普通用户权限
- **AND** 用户的所有角色都不包含该权限
- **THEN** 返回 `(false, nil)`
#### Scenario: 用户无角色
- **WHEN** 调用 `CheckPermission` 检查用户权限
- **AND** 用户未分配任何角色
- **THEN** 返回 `(false, nil)`
#### Scenario: 角色无权限
- **WHEN** 调用 `CheckPermission` 检查用户权限
- **AND** 用户已分配角色
- **AND** 所有角色都未分配任何权限
- **THEN** 返回 `(false, nil)`
#### Scenario: 数据库查询失败
- **WHEN** 调用 `CheckPermission` 过程中数据库查询失败
- **THEN** 返回 `(false, error)`
- **AND** error 包含详细的失败原因
### Requirement: Platform 参数匹配
权限检查 SHALL 支持 `platform` 参数过滤,实现端口隔离。
**匹配规则**:
- 权限的 `platform` 字段为 `all` → 任意 `platform` 参数都匹配
- 权限的 `platform` 字段与请求的 `platform` 相同 → 匹配
- 其他情况 → 不匹配
#### Scenario: 全平台权限匹配
- **WHEN** 权限的 `platform` 字段为 `all`
- **AND** 请求的 `platform``web`
- **THEN** 权限匹配成功
#### Scenario: 精确平台匹配
- **WHEN** 权限的 `platform` 字段为 `web`
- **AND** 请求的 `platform``web`
- **THEN** 权限匹配成功
#### Scenario: 平台不匹配
- **WHEN** 权限的 `platform` 字段为 `h5`
- **AND** 请求的 `platform``web`
- **THEN** 权限不匹配
- **AND** 继续检查用户的其他权限
### Requirement: 权限查询链式执行
权限检查 SHALL 按照以下顺序执行查询(增加店铺角色继承逻辑):
1. 检查用户类型(超级管理员跳过)
2. **查询用户的角色 ID 列表(增加店铺角色继承)**
- 优先查询账号级角色(`tb_account_role`
- 如果账号级角色为空 **且用户是代理账号UserType=3且有 shop_id**
- 查询店铺级角色(`tb_shop_role`
- 返回店铺级角色作为继承角色
3. 查询角色的权限 ID 列表(去重)
4. 查询权限详情列表
5. 遍历匹配 `permCode``platform`
**角色解析函数签名**:
```go
GetRoleIDsForAccount(ctx context.Context, accountID uint) ([]uint, error)
```
#### Scenario: 正常查询流程(现有行为保持不变)
- **WHEN** 调用 `CheckPermission` 检查普通用户权限
- **THEN** 按顺序执行以下查询:
1. 调用 `AccountService.GetRoleIDsForAccount(ctx, userID)` 获取角色 ID 列表(含继承逻辑)
2. `RolePermissionStore.GetPermIDsByRoleIDs(ctx, roleIDs)` 获取权限 ID 列表
3. `PermissionStore.GetByIDs(ctx, permIDs)` 获取权限详情
- **AND** 遍历权限列表进行匹配
- **AND** 找到匹配权限后立即返回 `true`(短路优化)
#### Scenario: 代理账号继承店铺角色
- **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** 不执行后续查询(角色权限查询、权限详情查询)
### Requirement: Service 依赖注入
Permission Service SHALL 在初始化时注入所需的 Store 和 Service 依赖。
**依赖**:
- `PermissionStore` - 查询权限详情
- `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`(保留向后兼容)
- `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 和 Service 依赖
- **AND** Store 依赖已在 `initStores()` 中初始化
- **AND** Account Service 已在 Permission Service 之前初始化
### Requirement: 错误处理和日志
权限检查 SHALL 提供详细的错误处理和日志记录。
#### Scenario: 数据库查询错误日志
- **WHEN** 数据库查询失败(如角色查询失败)
- **THEN** 记录错误日志,包含:
- 用户 ID
- 失败的查询类型(角色/权限)
- 错误详情
- **AND** 返回包装后的错误(使用 `fmt.Errorf`
#### Scenario: 权限检查成功日志(可选)
- **WHEN** 权限检查成功
- **THEN** 可选记录 debug 级别日志:
- 用户 ID
- 权限编码
- 平台类型
- 检查结果
- **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** 不执行任何数据库查询