## Context

当前系统中，"给代理分配套餐"需要两步独立操作：先调用 `POST /shop-series-allocations` 创建系列分配，再多次调用 `POST /shop-package-allocations` 逐一分配套餐。

`ShopSeriesAllocation` 有 6 个字段存在问题：
- 前 3 个死字段（`enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold`）从未被计算引擎读取，与 PackageSeries 配置完全重复
- 强充 3 个字段（`enable_force_recharge`、`force_recharge_amount`、`force_recharge_trigger_type`）语义正确，但 `checkForceRechargeRequirement` 只读 PackageSeries，代理配置完全无效

梯度模式下，`calculateChainOneTimeCommission` 直接把 PackageSeries 全局 tiers 传给 `matchOneTimeCommissionTier`，即所有代理用同一套阶梯金额。但业务模型要求每个代理有自己的专属阶梯金额（上级可以把金额压低给下级，但阈值不变），数据库中完全没有存储这个信息的字段。

## Goals / Non-Goals

**Goals:**
- 新增统一的"系列授权" API，一次请求原子性完成系列分配 + 套餐列表分配
- 修复强充层级：平台未设强充时，销售代理自设的强充配置真正生效
- 修复梯度模式数据模型：新增 `commission_tiers_json` 字段存储每代理专属阶梯金额
- 修复梯度模式计算引擎：读取各代理自己的阶梯金额，而非全局 tiers
- 新增金额上限校验：固定模式单值天花板；梯度模式每档位天花板
- 清理 3 个语义重复的死字段（数据库迁移删除）
- 删除旧的 `/shop-series-allocations` 和 `/shop-package-allocations` 接口，干净重构

**Non-Goals:**
- 不重写现有的佣金计算链式逻辑（数学结构正确，只改数据读取来源）
- 不修改 ShopPackageBatchAllocation、ShopPackageBatchPricing 这两个 Service（批量操作不在本次范围）
- 不修改差价佣金（`CalculateCostDiffCommission`）逻辑
- 不改变数据存储底层结构（仍是两张表）

## Decisions

### 决策 1：新接口策略——外观层

**选择**：新增 `ShopSeriesGrantService`，内部事务性地调用已有 `ShopSeriesAllocationStore` 和 `ShopPackageAllocationStore`。

**理由**：底层两张表的数据结构不变，对外以"授权"概念聚合呈现。

### 决策 2：梯度模式数据模型——JSONB 字段

**选择**：在 `ShopSeriesAllocation` 新增 `commission_tiers_json JSONB` 字段，存储该代理的专属阶梯金额列表。

数据格式：
```json
[
  {"threshold": 100, "amount": 80},
  {"threshold": 150, "amount": 120}
]
```

- `threshold` 必须与 PackageSeries 全局 tiers 的阈值一一对应（不允许创建不存在的阈值）
- `amount` 由上级在授权时设定，不得超过上级同阈值的 amount
- 固定模式下此字段为空数组 `[]`

**理由**：阶梯金额是每代理独立的配置，与分配记录 1:1 关联，JSONB 存储简洁无需额外联表；阈值条件（dimension、stat_scope、threshold 数值）全局一致，无需在分配层重复存储。

**备选方案**：新建 `tb_shop_series_allocation_tier` 子表 → 被否决，阈值不可修改且数量固定（跟随系列配置），子表增加了查询复杂度。

### 决策 3：梯度模式计算引擎改写

**原逻辑**（错误）：
```
matchOneTimeCommissionTier(ctx, shopID, seriesID, allocationID, config.Tiers)
                                                                ↑ PackageSeries 全局阶梯金额
```

**新逻辑**：
```
1. 从 PackageSeries 取 tiers（dimension、stat_scope、threshold 列表）
2. 查询当前代理的 ShopSeriesAllocation.commission_tiers_json（专属金额）
3. 根据该代理的销售统计匹配命中的 threshold
4. 从步骤 2 的专属金额列表中取出该 threshold 对应的 amount → 即 myAmount
```

阶梯金额的来源由全局改为各代理自己的记录，dimension/stat_scope/threshold 仍从全局读取。

### 决策 4：统一授权响应格式

`GET /shop-series-grants/:id` 返回聚合视图：

```
ShopSeriesGrantResponse {
  id                           // ShopSeriesAllocation.ID
  shop_id / shop_name
  series_id / series_name / series_code
  commission_type              // "fixed" 或 "tiered"，从 PackageSeries 读取
  one_time_commission_amount   // 固定模式：该代理的天花板；梯度模式：返回 0
  commission_tiers             // 梯度模式：专属阶梯列表；固定模式：空数组
  force_recharge_locked        // true=平台已设强充，前端只读
  force_recharge_enabled
  force_recharge_amount
  allocator_shop_id / allocator_shop_name
  status
  packages: [{
    package_id, package_name, package_code, cost_price, shelf_status, status
  }]
  created_at / updated_at
}
```

### 决策 5：金额上限校验规则

**固定模式**：
```
allocator_shop_id == 0（平台）→ one_time_commission_amount ≤ PackageSeries.commission_amount
allocator_shop_id > 0（代理）→ one_time_commission_amount ≤ 分配者自身的 one_time_commission_amount
```

**梯度模式**：
```
对请求中每一个 {threshold, amount}：
  allocator_shop_id == 0（平台）→ amount ≤ PackageSeries 同 threshold 的 amount
  allocator_shop_id > 0（代理）→ amount ≤ 分配者的 commission_tiers_json 中同 threshold 的 amount
```

### 决策 6：强充层级的实现位置

在 `order/service.go` 的 `checkForceRechargeRequirement` 中增加：

```
1. 从 PackageSeries 获取配置，config.Enable == false → 无强充

2. firstCommissionPaid == true → 无强充

3. config.TriggerType == "first_recharge"：
   → 直接返回需要强充（amount = config.Threshold）
   → 首次充值本身即为强充机制，代理无法修改此行为
   → 创建授权时忽略代理传入的 enable_force_recharge，响应 force_recharge_locked=true

4. config.TriggerType == "accumulated_recharge"：
   a. config.EnableForceRecharge == true
      → 使用平台强充（amount = config.ForceAmount），force_recharge_locked=true
   b. config.EnableForceRecharge == false
      → 查询 result.Card.ShopID（或 result.Device.ShopID）的 ShopSeriesAllocation
      → 若 allocation.EnableForceRecharge == true → 使用代理强充配置
      → 否则 → 无强充
```

**Grant API 强充字段的可写条件**：
- `TriggerType == first_recharge`：`enable_force_recharge` 字段无效，创建/更新时忽略，响应 `force_recharge_locked=true`
- `TriggerType == accumulated_recharge` + 平台已设：同上，`force_recharge_locked=true`
- `TriggerType == accumulated_recharge` + 平台未设：`enable_force_recharge` 和 `force_recharge_amount` 生效，`force_recharge_locked=false`

### 决策 7：删除旧接口范围

删除以下内容（开发阶段干净重构）：
- `internal/handler/admin/shop_series_allocation.go`
- `internal/handler/admin/shop_package_allocation.go`
- `internal/routes/shop_series_allocation.go`
- `internal/routes/shop_package_allocation.go`
- `internal/model/dto/shop_series_allocation.go`
- `internal/model/dto/shop_package_allocation.go`
- `internal/service/shop_series_allocation/`
- `internal/service/shop_package_allocation/`
- 从 `bootstrap/types.go`、`bootstrap/handlers.go`、`bootstrap/services.go`、`pkg/openapi/handlers.go`、`routes/admin.go` 中移除相关引用

保留（不删除）：
- 两个 Store（`ShopSeriesAllocationStore`、`ShopPackageAllocationStore`）——被佣金计算、订单服务、新 Grant Service 使用
- `ShopPackageBatchAllocation`、`ShopPackageBatchPricing` Service + Handler + routes（批量操作不在本次范围）

### 决策 8：数据库迁移策略

单次迁移文件完成：
- 删除 3 列：`enable_one_time_commission`、`one_time_commission_trigger`、`one_time_commission_threshold`
- 新增 1 列：`commission_tiers_json JSONB NOT NULL DEFAULT '[]'`

迁移顺序：先部署代码（新代码不再读写 3 个死字段，新字段有默认值），再执行迁移，规避滚动部署期间的字段缺失问题。DOWN 脚本不可逆（不恢复数据）。

### 决策 9：梯度阶梯运算符支持

**背景**：当前 `matchOneTimeCommissionTier` 对所有阶梯固定使用 `>=` 做阈值比较，无法支持不同运算符（如"销量 < 100 给 X 元"）。

**选择**：在 `OneTimeCommissionTier` 结构体新增 `Operator string` 字段，支持 `>`、`>=`、`<`、`<=`，默认值 `>=`（向前兼容）。

**存储位置**：
- `Operator` 仅存于 `PackageSeries.config.Tiers`（全局，由套餐系列配置决定）
- `ShopSeriesAllocation.commission_tiers_json` 不存 `Operator`（代理不能修改条件，只能修改金额）
- Grant 响应的 `commission_tiers` 展示时，将 `Operator` 从 PackageSeries 全局 tiers 按 threshold 合并进来

**计算引擎**：`matchOneTimeCommissionTier` 根据 `Operator` 选择对应比较函数；`Operator` 为空时 fallback 到 `>=`。

**不修改**：PackageSeries 的创建/编辑 API 不在本次范围（通过直接写 JSONB 设置 operator）。

## Risks / Trade-offs

| 风险 | 缓解措施 |
|------|----------|
| 旧接口删除后前端需同步更新 | 开发阶段删除无历史数据顾虑，与前端对齐后统一上线 |
| 现有梯度模式历史分配记录的 commission_tiers_json 为空 | 新字段 DEFAULT '[]'，计算引擎遇到空数组时 fallback 到 one_time_commission_amount（存 0）→ 不发佣金，需在上线前批量补填历史数据或确认历史记录均为固定模式 |
| checkForceRechargeRequirement 新增 DB 查询 | 函数已有 2 次查询，增加 1 次；量级可接受，后续可加缓存 |
| 梯度模式计算引擎改写影响正在运行的佣金计算 | 新逻辑仅在 commission_tiers_json 有值时生效；历史空记录走 fallback，不影响已发放佣金 |
