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