# permission-check Delta Specification

## Purpose

为权限检查能力增加店铺级角色继承逻辑，支持代理账号在没有账号级角色时自动继承店铺级角色。

## MODIFIED Requirements

### 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** 不执行后续查询（角色权限查询、权限详情查询）

## ADDED Requirements

### 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: Permission Service 依赖注入升级

Permission Service SHALL 增加对 Account Service 的依赖，用于调用角色解析逻辑。

**修改的依赖**:
```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 列表可能是账号级角色或店铺级角色

### 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** 不执行任何数据库查询
