All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m39s
- 新增店铺角色管理 API 和数据模型 - 实现角色继承和权限检查逻辑 - 添加流程测试框架和集成测试 - 更新权限服务和账号管理逻辑 - 添加数据库迁移脚本 - 归档 OpenSpec 变更文档 Ultraworked with Sisyphus
13 KiB
13 KiB
permission-check Specification
Purpose
提供完整的权限检查能力,支持基于角色的权限验证和店铺级角色继承机制,实现细粒度的访问控制。
Requirements
Requirement: 权限检查核心服务
Permission Service SHALL 提供 CheckPermission 方法,用于检查用户是否拥有指定权限。
签名:
CheckPermission(ctx context.Context, userID uint, permCode string, platform string) (bool, error)
参数:
ctx: 上下文(可选包含用户类型信息)userID: 用户 IDpermCode: 权限编码(格式: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 按照以下顺序执行查询(增加店铺角色继承逻辑):
- 检查用户类型(超级管理员跳过)
- 查询用户的角色 ID 列表(增加店铺角色继承):
- 优先查询账号级角色(
tb_account_role) - 如果账号级角色为空 且用户是代理账号(UserType=3)且有 shop_id:
- 查询店铺级角色(
tb_shop_role) - 返回店铺级角色作为继承角色
- 查询店铺级角色(
- 优先查询账号级角色(
- 查询角色的权限 ID 列表(去重)
- 查询权限详情列表
- 遍历匹配
permCode和platform
角色解析函数签名:
GetRoleIDsForAccount(ctx context.Context, accountID uint) ([]uint, error)
Scenario: 正常查询流程(现有行为保持不变)
- WHEN 调用
CheckPermission检查普通用户权限 - THEN 按顺序执行以下查询:
- 调用
AccountService.GetRoleIDsForAccount(ctx, userID)获取角色 ID 列表(含继承逻辑) RolePermissionStore.GetPermIDsByRoleIDs(ctx, roleIDs)获取权限 ID 列表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- 权限缓存
修改的依赖:
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.PermissionStoreaccountRoleStore *postgres.AccountRoleStore(保留向后兼容)rolePermStore *postgres.RolePermissionStoreaccountService *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
方法签名:
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 不执行任何数据库查询