# 店铺级角色继承功能设计

## Context

### 当前系统状态

**RBAC 架构**：
- 系统使用标准 RBAC（Role-Based Access Control）模型
- 角色分配粒度：账号级别（`tb_account_role` 表维护账号-角色关联）
- 权限检查流程：`Account → AccountRole → Role → RolePermission → Permission`
- 权限缓存：Redis 缓存用户权限 30 分钟

**用户类型**：
1. 超级管理员（UserType=1）：跳过权限检查
2. 平台用户（UserType=2）：账号级角色，可分配多个平台角色
3. 代理账号（UserType=3）：账号级角色，可分配 1 个客户角色，归属于店铺（shop_id）
4. 企业账号（UserType=4）：账号级角色，可分配 1 个客户角色，归属于企业（enterprise_id）
5. 个人客户（UserType=5）：无角色系统

**店铺层级结构**：
- 支持最多 7 级代理层级（通过 `tb_shop.parent_id` 维护）
- 一个店铺可有多个代理账号（员工）
- 店铺层级用于数据权限过滤（GORM Callback 自动注入 `WHERE shop_id IN (...)` 条件）

### 问题

在 MVP 阶段，一个店铺内的所有账号权限通常一致（如 10 个员工都是"代理店长"角色）。平台需要为每个账号逐一分配角色，操作繁琐且容易出错。

### 约束

- 必须保持向后兼容，不能破坏现有账号级角色功能
- 仅适用于代理账号（UserType=3），其他用户类型保持现状
- 必须遵循项目架构分层：Handler → Service → Store → Model
- 禁止使用外键约束和 GORM 关联关系

## Goals / Non-Goals

### Goals

1. **简化 MVP 阶段操作**：平台可在店铺层面设置默认角色，店铺内所有账号自动继承
2. **支持未来扩展**：保留账号级角色覆盖能力，特殊账号可单独设置角色
3. **完全向后兼容**：现有账号级角色功能不受影响，不设置店铺角色的店铺行为保持一致
4. **清晰的继承规则**：账号级角色优先，无则继承店铺级角色

### Non-Goals

1. **不支持企业级角色继承**：企业账号保持账号级角色（一企业一账号，暂无批量需求）
2. **不支持平台级角色继承**：平台账号数量少且权限差异大，不适合继承
3. **不支持多角色继承**：店铺只能设置单个角色（代理账号最大角色数为 1）
4. **不支持角色叠加**：账号角色和店铺角色二选一，不取并集

## Decisions

### 决策 1：角色继承规则（默认继承 + 账号级覆盖）

**选择**：账号级角色优先，无则继承店铺级角色

**替代方案考虑**：
| 方案 | 优点 | 缺点 | 决策 |
|------|------|------|------|
| A. 强制继承 | 最简单，MVP 体验最好 | 未来扩展需要数据迁移 | ❌ 不选 |
| B. 默认继承 + 覆盖 | 简单且灵活，无缝升级 | 逻辑稍复杂（需判断优先级） | ✅ 选择 |
| C. 角色叠加（并集） | 最灵活 | 难理解，容易造成权限混乱 | ❌ 不选 |

**理由**：
- MVP 阶段：不设置账号角色，自动继承店铺 → 达到简化目标
- 未来扩展：特殊账号（如财务）可单独设置角色 → 覆盖店铺默认
- 优先级明确：账号角色 > 店铺角色，易于理解和调试

**实现逻辑**：
```go
func GetRoleIDsForAccount(accountID) []uint {
    // 1. 查询账号级角色
    accountRoles := GetRoleIDsByAccountID(accountID)
    if len(accountRoles) > 0 {
        return accountRoles  // 有账号角色，不继承
    }
    
    // 2. 查询账号所属店铺
    account := GetAccountByID(accountID)
    if account.UserType != UserTypeAgent || account.ShopID == nil {
        return []  // 非代理账号或无店铺，无继承
    }
    
    // 3. 查询店铺级角色（继承）
    shopRoles := GetRoleIDsByShopID(account.ShopID)
    return shopRoles
}
```

### 决策 2：用户类型范围（仅代理账号）

**选择**：仅对代理账号（UserType=3）启用店铺级角色继承

**理由**：
- **代理账号**：有批量需求（一个店铺多个员工）
- **平台账号**：数量少（<10 个），权限差异大，不适合继承
- **企业账号**：一企业一账号，暂无批量需求
- **超级管理员**：跳过权限检查，无角色
- **个人客户**：无角色系统

**影响**：角色解析逻辑中需要判断 `UserType == 3` 才执行店铺角色查询。

### 决策 3：数据库设计（新增 tb_shop_role 表）

**选择**：新增 `tb_shop_role` 表，保留 `tb_account_role` 表

**表结构**：
```sql
CREATE TABLE tb_shop_role (
    id SERIAL PRIMARY KEY,
    shop_id INT NOT NULL,
    role_id INT NOT NULL,
    status INT NOT NULL DEFAULT 1,  -- 0=禁用 1=启用
    creator INT NOT NULL,
    updater INT NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP,
    UNIQUE (shop_id, role_id) WHERE deleted_at IS NULL
);
```

**索引设计**：
- `idx_shop_role_shop_id`：查询店铺角色（高频）
- `idx_shop_role_role_id`：查询角色被哪些店铺使用（低频）
- `idx_shop_role_deleted_at`：软删除过滤

**理由**：
- 保留 `tb_account_role`：向后兼容，不影响现有功能
- 新增 `tb_shop_role`：明确语义，避免表字段冗余
- 禁止外键约束：遵循项目规范，关联在代码层维护

### 决策 4：角色类型校验（只能分配客户角色）

**选择**：店铺只能分配客户角色（RoleType=2）

**校验逻辑**：
```go
for _, role := range roles {
    if role.RoleType != constants.RoleTypeCustomer {
        return errors.New("店铺只能分配客户角色")
    }
}
```

**理由**：
- 代理账号只能分配客户角色（RoleType=2）
- 平台角色（RoleType=1）只能分配给平台用户
- 防止配置错误导致权限混乱

### 决策 5：缓存失效策略（清理店铺下所有账号缓存）

**选择**：店铺角色修改时，清理该店铺下所有账号的权限缓存

**实现**：
```go
func (s *ShopRoleStore) clearShopRoleCache(ctx context.Context, shopID uint) {
    // 查询该店铺下所有账号
    var accountIDs []uint
    s.db.Model(&Account{}).Where("shop_id = ?", shopID).Pluck("id", &accountIDs)
    
    // 逐个清理权限缓存
    for _, accountID := range accountIDs {
        cacheKey := constants.RedisUserPermissionsKey(accountID)
        s.redisClient.Del(ctx, cacheKey)
    }
}
```

**理由**：
- 店铺角色修改后，继承该角色的账号权限立即生效
- 有账号级角色的账号不受影响（因为优先级更高）
- 下次权限检查时，自动使用新的继承逻辑重建缓存

### 决策 6：API 设计（RESTful 风格）

**新增接口**：
- `POST /api/admin/shops/:shop_id/roles` - 分配店铺角色
- `GET /api/admin/shops/:shop_id/roles` - 查询店铺角色
- `DELETE /api/admin/shops/:shop_id/roles/:role_id` - 删除店铺角色

**请求体设计**：
```json
// POST /api/admin/shops/:shop_id/roles
{
  "role_ids": [5]  // 传空数组 = 清空所有角色
}
```

**响应体设计**：
```json
// GET /api/admin/shops/:shop_id/roles
{
  "code": 0,
  "msg": "success",
  "data": {
    "shop_id": 10,
    "roles": [
      {
        "shop_id": 10,
        "role_id": 5,
        "role_name": "代理店长",
        "role_desc": "代理店铺管理员",
        "status": 1
      }
    ]
  }
}
```

**权限检查**：
- 使用现有的 `middleware.CanManageShop()` 验证权限
- 平台用户和管理该店铺的代理才能操作

### 决策 7：依赖注入（通过结构体字段）

**Service 层依赖**：
```go
// internal/service/shop/service.go
type Service struct {
    shopStore     *postgres.ShopStore
    shopRoleStore *postgres.ShopRoleStore  // 新增
    roleStore     *postgres.RoleStore
    accountStore  *postgres.AccountStore
}

// internal/service/permission/service.go
type Service struct {
    permissionStore  *postgres.PermissionStore
    accountRoleStore *postgres.AccountRoleStore
    rolePermStore    *postgres.RolePermissionStore
    accountService   *account.Service  // 新增：用于调用角色解析
    redisClient      *redis.Client
}
```

**Bootstrap 注册**：
```go
// internal/bootstrap/stores.go
stores := &Stores{
    // ... 现有 stores
    ShopRole: postgres.NewShopRoleStore(deps.DB, deps.Redis),  // 新增
}

// internal/bootstrap/handlers.go
handlers := &Handlers{
    // ... 现有 handlers
    ShopRole: admin.NewShopRoleHandler(services.Shop),  // 新增
}
```

## Risks / Trade-offs

### 风险 1：角色解析增加数据库查询

**风险**：每次权限检查需要额外查询店铺角色（如果账号无角色）

**缓解措施**：
- 权限结果在 Redis 缓存 30 分钟，大部分请求不会触发查询
- 店铺角色修改频率极低，缓存命中率高
- 查询有索引支持（`idx_shop_role_shop_id`），查询速度快（< 5ms）

**性能影响评估**：
- 最坏情况：首次权限检查增加 1 次查询（~ 5ms）
- 后续请求：从缓存读取（~ 0.1ms）
- 预计对 API P95 响应时间影响 < 5ms，满足性能要求

### 风险 2：缓存失效可能影响多个账号

**风险**：店铺角色修改时，需清理该店铺下所有账号缓存，可能导致短暂性能下降

**缓解措施**：
- 缓存清理是异步操作，不阻塞主流程
- 店铺角色修改是低频操作（平均每天 < 10 次）
- 缓存重建是懒加载（下次请求时才重建），不会集中请求数据库

**最坏情况**：店铺有 100 个账号，角色修改后，下次 100 个账号同时请求 → 产生 100 次查询。但这种情况极少，且 PostgreSQL 可承受（连接池默认 100）。

### 风险 3：继承规则理解偏差

**风险**：用户可能不理解"账号角色优先"规则，误以为店铺角色修改会影响所有账号

**缓解措施**：
- UI 层明确标识继承状态（"继承自店铺" vs "账号单独设置"）
- 店铺角色设置页面显示"影响范围：10 个账号，1 个有单独设置不受影响"
- API 文档和操作指南明确说明继承规则

### Trade-off 1：灵活性 vs 复杂度

**Trade-off**：选择"默认继承 + 覆盖"模式增加了逻辑复杂度

**权衡**：
- 增加的复杂度：角色解析逻辑从 1 次查询变为最多 2 次查询（先查账号，再查店铺）
- 获得的灵活性：MVP 简化 + 未来无缝扩展，无需数据迁移
- 结论：复杂度增加有限（~20 行代码），灵活性收益显著，权衡合理

### Trade-off 2：用户类型限制 vs 通用性

**Trade-off**：仅支持代理账号，不支持企业和平台账号

**权衡**：
- 限制原因：企业账号暂无批量需求（一企业一账号），平台账号不适合继承
- 未来扩展：如果企业需要多账号，可复制同样逻辑创建 `tb_enterprise_role` 表
- 结论：优先解决当前痛点（代理店铺批量分配），避免过度设计

## Migration Plan

### 部署步骤

1. **数据库迁移**（无需停机）：
   ```bash
   # 执行迁移
   migrate -path migrations -database "postgres://..." up
   ```
   - 创建 `tb_shop_role` 表
   - 不影响现有数据和功能

2. **代码部署**（滚动更新）：
   - 部署新版本 API 服务
   - 新增接口向后兼容，不影响现有功能

3. **验证**：
   - 调用 `POST /api/admin/shops/:id/roles` 设置店铺角色
   - 验证该店铺下账号权限生效
   - 验证有账号角色的账号不受影响

### 回滚策略

**如需回滚**：
1. 回滚代码到旧版本
2. 保留 `tb_shop_role` 表（不删除，避免数据丢失）
3. 清理所有权限缓存：`redis-cli KEYS "user:permissions:*" | xargs redis-cli DEL`
4. 旧版本代码忽略 `tb_shop_role` 表，继续使用 `tb_account_role`

**数据一致性**：
- 回滚不影响 `tb_account_role` 数据
- `tb_shop_role` 数据保留，重新部署新版本后继续生效

## Open Questions

### Q1: 是否需要支持多角色继承？

**当前设计**：店铺只能设置单个角色（代理账号最大角色数为 1）

**未来考虑**：如果代理账号需要支持多角色（如"代理店长" + "销售专员"），需要：
- 修改 `constants.GetMaxRolesForUserType()` 返回值
- 修改 `AssignRolesToShop()` 支持多角色分配
- 修改角色解析逻辑支持多角色继承

**决策时机**：需求明确后再调整（暂不实现）

### Q2: 是否需要记录角色继承历史？

**当前设计**：不记录继承历史，只记录当前状态

**未来考虑**：如果需要审计"某账号在某时间段继承了哪个店铺角色"，需要：
- 修改操作审计日志，记录角色继承关系变更
- 修改权限检查日志，记录使用的是账号角色还是店铺角色

**决策时机**：审计需求明确后再实现（暂不实现）

### Q3: 账号转移店铺后，角色如何处理？

**当前设计**：账号转移店铺后，自动继承新店铺角色（如果无账号级角色）

**替代方案**：账号转移店铺时，是否应该清除账号级角色？

**建议**：保持现状（账号角色不跟随店铺），理由：
- 账号角色是独立设置的，转移店铺不应影响
- 如果需要重新继承新店铺角色，可手动删除账号角色

**决策时机**：观察实际使用情况后决定（暂不修改）
