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

13 KiB
Raw Blame History

permission-check Specification

Purpose

提供完整的权限检查能力,支持基于角色的权限验证和店铺级角色继承机制,实现细粒度的访问控制。

Requirements

Requirement: 权限检查核心服务

Permission Service SHALL 提供 CheckPermission 方法,用于检查用户是否拥有指定权限。

签名:

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 忽略 permCodeplatform 参数

Scenario: 有权限的普通用户

  • WHEN 调用 CheckPermission 检查普通用户权限
  • AND 用户通过角色关联拥有该权限
  • AND 权限的 permCode 匹配
  • AND 权限的 platformall 或匹配请求的 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 请求的 platformweb
  • THEN 权限匹配成功

Scenario: 精确平台匹配

  • WHEN 权限的 platform 字段为 web
  • AND 请求的 platformweb
  • THEN 权限匹配成功

Scenario: 平台不匹配

  • WHEN 权限的 platform 字段为 h5
  • AND 请求的 platformweb
  • THEN 权限不匹配
  • AND 继续检查用户的其他权限

Requirement: 权限查询链式执行

权限检查 SHALL 按照以下顺序执行查询(增加店铺角色继承逻辑):

  1. 检查用户类型(超级管理员跳过)
  2. 查询用户的角色 ID 列表(增加店铺角色继承)
    • 优先查询账号级角色(tb_account_role
    • 如果账号级角色为空 且用户是代理账号UserType=3且有 shop_id
      • 查询店铺级角色(tb_shop_role
      • 返回店铺级角色作为继承角色
  3. 查询角色的权限 ID 列表(去重)
  4. 查询权限详情列表
  5. 遍历匹配 permCodeplatform

角色解析函数签名:

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 - 权限缓存

修改的依赖:

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 集成

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