## Context

Phase 4 完成了订单和支付流程，现在需要实现佣金计算。当终端用户购买套餐支付成功后，系统自动计算各级代理的佣金并入账。

**佣金来源**：
1. **成本价差收入**：每笔订单必触发，售价 - 成本价 = 代理收入
2. **一次性佣金**：满足触发条件时发放一次，每张卡/设备仅发放一次
3. **周期性梯度返佣**：已在 refactor-shop-package-allocation 中实现，本期不涉及

**一次性佣金的两种类型**：
- **固定一次性佣金**：充值达标后发放固定金额或比例（如首充≥100元返20元）
- **梯度一次性佣金**：根据系列销售业绩返不同佣金（如系列销售额≥5000元时首充返15元）

**核心业务逻辑**：
- **触发条件**：基于单张卡/设备的充值情况（首充或累计充值达标）
- **返佣金额**：基于该系列分配的累计销售业绩（销量或销售额）选择梯度档位
- **发放次数**：每张卡/设备仅发放一次（通过 first_commission_paid 标记）
- **统计来源**：使用 ShopSeriesCommissionStats 查询该系列分配的销售业绩

**与重构的关系**：
- refactor-shop-package-allocation 已实现 ShopSeriesCommissionStats 统计表（按 allocation_id 统计销售业绩）
- 一次性佣金复用该统计表，通过 allocation_id 查询该系列分配的累计销量/销售额
- 需要在 ShopSeriesAllocation 表新增一次性佣金配置字段
- 需要新增 ShopSeriesOneTimeCommissionTier 表存储梯度配置

**当前 CommissionRecord 模型过于复杂**（包含冻结/解冻字段），需要简化。

## Goals / Non-Goals

**Goals:**
- 简化 CommissionRecord 模型
- 实现成本价差收入计算（每笔订单）
- 实现一次性佣金触发（充值阈值）
- 佣金直接入账到店铺钱包
- 提供佣金记录查询和统计

**Non-Goals:**
- 不实现冻结/解冻机制
- 不实现长期佣金（号卡专用）
- 不实现梯度佣金统计（本期只做配置，统计后续优化）
- 不实现佣金审批流程

## Decisions

### 1. CommissionRecord 模型简化

**决策**：删除冻结相关字段，新增来源和关联字段

```go
// 简化后的 CommissionRecord
type CommissionRecord struct {
    gorm.Model
    BaseModel
    ShopID           uint       // 店铺ID（佣金归属）
    OrderID          uint       // 关联订单ID
    IotCardID        uint       // 关联卡ID（可空）
    DeviceID         uint       // 关联设备ID（可空）
    CommissionSource string     // 佣金来源: cost_diff-成本价差 one_time-一次性佣金 tier_bonus-梯度奖励
    Amount           int64      // 佣金金额（分）
    BalanceAfter     int64      // 入账后钱包余额（分）
    Status           int        // 状态: 1-已入账 2-已失效
    ReleasedAt       *time.Time // 入账时间
    Remark           string     // 备注
}
```

**删除字段**：
- `agent_id`（改用 shop_id）
- `rule_id`（不再关联复杂规则）
- `commission_type`（改用 commission_source）
- `unfrozen_at`、冻结相关状态

### 2. 佣金计算流程

**决策**：订单支付成功后异步计算

```
订单支付成功
    ↓
发送异步任务 (Asynq)
    ↓
佣金计算任务执行：
    1. 获取订单信息
    2. 遍历代理层级（从销售店铺到顶级）
    3. 每级计算成本价差收入
    4. 检查一次性佣金触发条件
    5. 创建 CommissionRecord
    6. 更新店铺钱包余额
    7. 更新订单 commission_status
```

### 3. 成本价差收入计算

**决策**：各级代理按自己的成本价差计算

**计算规则**：
- 终端销售代理：收入 = 售价 - 自己的成本价
- 中间层级代理：收入 = 下级的成本价 - 自己的成本价

```go
func CalculateCostDiffCommission(order *Order) []CommissionRecord {
    var records []CommissionRecord
    
    // 获取销售店铺（终端销售的代理）
    sellerShop := GetShop(order.SellerShopID)
    sellerCostPrice := GetCostPrice(sellerShop.ID, order.PackageID)
    
    // 终端销售代理的收入 = 售价 - 成本价
    sellerProfit := order.TotalAmount - sellerCostPrice
    if sellerProfit > 0 {
        records = append(records, CommissionRecord{
            ShopID:           sellerShop.ID,
            OrderID:          order.ID,
            CommissionSource: "cost_diff",
            Amount:           sellerProfit,
        })
    }
    
    // 遍历上级代理链
    childCostPrice := sellerCostPrice
    currentShop := GetShop(sellerShop.ParentID)
    
    for currentShop != nil {
        // 获取当前店铺的成本价
        myCostPrice := GetCostPrice(currentShop.ID, order.PackageID)
        
        // 收入 = 下级成本价 - 自己成本价
        profit := childCostPrice - myCostPrice
        if profit > 0 {
            records = append(records, CommissionRecord{
                ShopID:           currentShop.ID,
                OrderID:          order.ID,
                CommissionSource: "cost_diff",
                Amount:           profit,
            })
        }
        
        // 移动到上级
        childCostPrice = myCostPrice
        currentShop = GetShop(currentShop.ParentID)
    }
    
    return records
}
```

### 4. 一次性佣金数据结构

**决策**：在 ShopSeriesAllocation 新增配置字段 + 新增梯度表

#### 4.1 ShopSeriesAllocation 新增字段

```go
type ShopSeriesAllocation struct {
    // ... 现有字段（base_commission, enable_tier_commission 等）
    
    // 🆕 一次性佣金配置
    EnableOneTimeCommission     bool   `gorm:"column:enable_one_time_commission;default:false;comment:是否启用一次性佣金"`
    OneTimeCommissionType       string `gorm:"column:one_time_commission_type;type:varchar(20);comment:类型:fixed-固定 tiered-梯度"`
    OneTimeCommissionTrigger    string `gorm:"column:one_time_commission_trigger;type:varchar(30);comment:触发条件:single_recharge-单次充值 accumulated_recharge-累计充值"`
    OneTimeCommissionThreshold  int64  `gorm:"column:one_time_commission_threshold;type:bigint;comment:最低阈值(分)"`
    
    // 固定一次性佣金配置（type="fixed" 时使用）
    OneTimeCommissionMode       string `gorm:"column:one_time_commission_mode;type:varchar(20);comment:模式:fixed-固定金额 percent-百分比"`
    OneTimeCommissionValue      int64  `gorm:"column:one_time_commission_value;type:bigint;comment:佣金金额(分)或比例(千分比)"`
}
```

#### 4.2 新增 ShopSeriesOneTimeCommissionTier 表

```go
// 梯度一次性佣金配置
type ShopSeriesOneTimeCommissionTier struct {
    gorm.Model
    BaseModel
    AllocationID        uint   `gorm:"column:allocation_id;not null;index;comment:系列分配ID"`
    
    // 梯度判断配置（基于系列销售业绩）
    TierType            string `gorm:"column:tier_type;type:varchar(20);not null;comment:梯度类型:sales_count-销量 sales_amount-销售额"`
    ThresholdValue      int64  `gorm:"column:threshold_value;type:bigint;not null;comment:梯度阈值(销量或销售额分)"`
    
    // 返佣配置
    CommissionMode      string `gorm:"column:commission_mode;type:varchar(20);not null;comment:返佣模式:fixed-固定金额 percent-百分比"`
    CommissionValue     int64  `gorm:"column:commission_value;type:bigint;not null;comment:返佣值(分或千分比)"`
    
    Status              int    `gorm:"column:status;type:int;default:1;comment:状态:1-启用 2-停用"`
}

// TableName 指定表名
func (ShopSeriesOneTimeCommissionTier) TableName() string {
    return "tb_shop_series_one_time_commission_tier"
}
```

**关键说明**：
- `TierType`: 梯度判断类型，与 ShopSeriesCommissionTier 的 tier_type 一致（sales_count 或 sales_amount）
- `ThresholdValue`: 系列销售业绩的阈值（如系列累计销售额≥5000元）
- 梯度判断使用 ShopSeriesCommissionStats 表中的统计数据（按 allocation_id 查询）

### 5. 一次性佣金触发逻辑

**决策**：两种触发类型 × 两种佣金类型，每张卡/设备只触发一次

#### 5.1 触发条件判断

```go
// 触发条件 A：单次充值 ≥ 阈值
func CheckSingleRecharge(order *Order, threshold int64) (bool, int64) {
    triggered := order.TotalAmount >= threshold
    return triggered, order.TotalAmount
}

// 触发条件 B：累计充值 ≥ 阈值
func CheckAccumulatedRecharge(card *IotCard, order *Order, threshold int64) (bool, int64) {
    // 先累加当前订单金额
    newAccumulated := card.AccumulatedRecharge + order.TotalAmount
    triggered := newAccumulated >= threshold
    return triggered, newAccumulated
}
```

#### 5.2 佣金金额计算

```go
// 检查并发放一次性佣金（单卡购买场景）
func TriggerOneTimeCommissionForCard(order *Order, card *IotCard) error {
    // 1. 检查是否已发放
    if card.FirstCommissionPaid {
        return nil
    }
    
    // 2. 获取配置
    allocation := GetAllocation(card.SeriesAllocationID)
    if !allocation.EnableOneTimeCommission {
        return nil
    }
    
    // 3. 检查充值触发条件
    var rechargeAmount int64
    switch allocation.OneTimeCommissionTrigger {
    case "single_recharge":
        rechargeAmount = order.TotalAmount
    case "accumulated_recharge":
        rechargeAmount = card.AccumulatedRecharge + order.TotalAmount
    }
    
    if rechargeAmount < allocation.OneTimeCommissionThreshold {
        return nil // 充值金额未达标
    }
    
    // 4. 计算佣金金额
    commissionAmount := calculateOneTimeCommission(allocation, order.TotalAmount)
    
    if commissionAmount <= 0 {
        return nil
    }
    
    // 5. 创建佣金记录
    record := &CommissionRecord{
        ShopID:           card.OwnerShopID,
        OrderID:          order.ID,
        IotCardID:        card.ID,
        CommissionSource: "one_time",
        Amount:           commissionAmount,
    }
    CreateCommissionRecord(record)
    CreditCommission(record)
    
    // 6. 标记已发放并更新累计充值
    card.FirstCommissionPaid = true
    if allocation.OneTimeCommissionTrigger == "accumulated_recharge" {
        card.AccumulatedRecharge = rechargeAmount
    }
    UpdateCard(card)
    
    return nil
}

// 检查并发放一次性佣金（设备购买场景）
func TriggerOneTimeCommissionForDevice(order *Order, device *Device) error {
    // 1. 检查是否已发放
    if device.FirstCommissionPaid {
        return nil
    }
    
    // 2. 获取配置
    allocation := GetAllocation(device.SeriesAllocationID)
    if !allocation.EnableOneTimeCommission {
        return nil
    }
    
    // 3. 检查充值触发条件
    var rechargeAmount int64
    switch allocation.OneTimeCommissionTrigger {
    case "single_recharge":
        rechargeAmount = order.TotalAmount
    case "accumulated_recharge":
        rechargeAmount = device.AccumulatedRecharge + order.TotalAmount
    }
    
    if rechargeAmount < allocation.OneTimeCommissionThreshold {
        return nil
    }
    
    // 4. 计算佣金金额
    commissionAmount := calculateOneTimeCommission(allocation, order.TotalAmount)
    
    if commissionAmount <= 0 {
        return nil
    }
    
    // 5. 创建佣金记录（注意：设备级购买只发放一次，不按卡数倍增）
    record := &CommissionRecord{
        ShopID:           device.OwnerShopID,
        OrderID:          order.ID,
        DeviceID:         device.ID,
        CommissionSource: "one_time",
        Amount:           commissionAmount,
    }
    CreateCommissionRecord(record)
    CreditCommission(record)
    
    // 6. 标记已发放
    device.FirstCommissionPaid = true
    if allocation.OneTimeCommissionTrigger == "accumulated_recharge" {
        device.AccumulatedRecharge = rechargeAmount
    }
    UpdateDevice(device)
    
    return nil
}

// 计算一次性佣金金额（固定或梯度）
func calculateOneTimeCommission(allocation *ShopSeriesAllocation, orderAmount int64) int64 {
    switch allocation.OneTimeCommissionType {
    case "fixed":
        // 固定一次性佣金
        return calculateFixedCommission(
            allocation.OneTimeCommissionMode,
            allocation.OneTimeCommissionValue,
            orderAmount,
        )
        
    case "tiered":
        // 梯度一次性佣金 - 基于系列销售业绩选择档位
        return calculateTieredCommission(allocation.ID, orderAmount)
    }
    
    return 0
}

// 计算固定佣金金额
func calculateFixedCommission(mode string, value int64, orderAmount int64) int64 {
    if mode == "fixed" {
        return value // 固定金额
    } else if mode == "percent" {
        return orderAmount * value / 1000 // 按充值金额的百分比
    }
    return 0
}

// 计算梯度佣金金额（基于系列销售业绩）
func calculateTieredCommission(allocationID uint, orderAmount int64) int64 {
    // 1. 获取梯度配置
    tiers := GetOneTimeCommissionTiers(allocationID)
    if len(tiers) == 0 {
        return 0
    }
    
    // 2. 查询该系列分配的销售业绩统计
    stats, _ := commissionStatsService.GetCurrentStats(ctx, allocationID, "all_time")
    if stats == nil {
        return 0
    }
    
    // 3. 找到最高匹配档位
    var matchedTier *ShopSeriesOneTimeCommissionTier
    for i := range tiers {
        // 获取销售业绩值
        var salesValue int64
        if tiers[i].TierType == "sales_count" {
            salesValue = stats.TotalSalesCount
        } else { // sales_amount
            salesValue = stats.TotalSalesAmount
        }
        
        // 检查是否达到梯度阈值
        if salesValue >= tiers[i].ThresholdValue {
            if matchedTier == nil || tiers[i].ThresholdValue > matchedTier.ThresholdValue {
                matchedTier = &tiers[i]
            }
        }
    }
    
    if matchedTier == nil {
        return 0
    }
    
    // 4. 计算佣金金额
    if matchedTier.CommissionMode == "fixed" {
        return matchedTier.CommissionValue
    } else if matchedTier.CommissionMode == "percent" {
        return orderAmount * matchedTier.CommissionValue / 1000
    }
    
    return 0
}
```

**关键说明**：
1. **触发条件**：基于单张卡/设备的充值金额（首充或累计充值）
2. **梯度判断**：基于该系列分配的销售业绩（从 ShopSeriesCommissionStats 查询）
3. **设备场景**：设备购买时只发放一次佣金，不按卡数倍增
4. **统计来源**：使用 `allocationID` 查询该系列分配的累计销量/销售额

#### 5.3 配置示例

**示例 1：固定一次性佣金（首充触发）**
```json
{
  "enable_one_time_commission": true,
  "one_time_commission_type": "fixed",
  "one_time_commission_trigger": "single_recharge",
  "one_time_commission_threshold": 10000,  // 首充≥100元触发
  "one_time_commission_mode": "fixed",
  "one_time_commission_value": 2000        // 返20元
}
```

**业务效果**：
- 用户首次充值≥100元时，代理获得20元一次性佣金
- 该卡/设备后续再充值不再触发

---

**示例 2：梯度一次性佣金（基于销售金额 + 累计充值触发）**
```json
{
  "enable_one_time_commission": true,
  "one_time_commission_type": "tiered",
  "one_time_commission_trigger": "accumulated_recharge",
  "one_time_commission_threshold": 10000,  // 累计充值≥100元才触发
  "tiers": [
    {
      "tier_type": "sales_amount",
      "threshold": 200000,      // 系列累计销售额≥2000元
      "mode": "fixed",
      "value": 1000             // 返10元
    },
    {
      "tier_type": "sales_amount",
      "threshold": 400000,      // 系列累计销售额≥4000元
      "mode": "fixed",
      "value": 1500             // 返15元
    },
    {
      "tier_type": "sales_amount",
      "threshold": 1000000,     // 系列累计销售额≥10000元
      "mode": "percent",
      "value": 100              // 返10%（按充值金额）
    }
  ]
}
```

**业务效果**：
- 当该系列分配的累计销售额≥4000元时
- 用户累计充值≥100元触发一次性佣金
- 代理获得15元（匹配到第二档梯度）
- 如果系列销售额后续达到10000元，新用户首充≥100元时，代理获得充值金额的10%

---

**示例 3：梯度一次性佣金（基于销售数量 + 首充触发）**
```json
{
  "enable_one_time_commission": true,
  "one_time_commission_type": "tiered",
  "one_time_commission_trigger": "single_recharge",
  "one_time_commission_threshold": 10000,  // 首充≥100元触发
  "tiers": [
    {
      "tier_type": "sales_count",
      "threshold": 20,          // 系列累计销量≥20个
      "mode": "fixed",
      "value": 1000             // 返10元
    },
    {
      "tier_type": "sales_count",
      "threshold": 40,          // 系列累计销量≥40个
      "mode": "fixed",
      "value": 1500             // 返15元
    },
    {
      "tier_type": "sales_count",
      "threshold": 120,         // 系列累计销量≥120个
      "mode": "percent",
      "value": 200              // 返20%
    }
  ]
}
```

**业务效果**：
- 当该系列分配的累计销量≥40个时
- 用户首充≥100元触发一次性佣金
- 代理获得15元（匹配到第二档梯度）

### 6. 钱包入账

**决策**：直接入账，无冻结期

```go
func CreditCommission(record *CommissionRecord) error {
    return Transaction(func(tx *gorm.DB) error {
        // 1. 获取店铺钱包
        wallet := GetWallet("shop", record.ShopID)
        
        // 2. 增加余额
        wallet.Balance += record.Amount
        UpdateWallet(wallet)
        
        // 3. 记录余额
        record.BalanceAfter = wallet.Balance
        record.Status = 1 // 已入账
        record.ReleasedAt = time.Now()
        UpdateCommissionRecord(record)
        
        // 4. 创建钱包交易记录
        CreateWalletTransaction(...)
        
        return nil
    })
}
```

### 7. API 设计

```
# 佣金记录查询
GET /api/admin/commission-records              佣金记录列表
GET /api/admin/commission-records/:id          佣金记录详情

# 佣金统计
GET /api/admin/commission-stats                佣金统计（总收入、各来源占比）
GET /api/admin/commission-stats/daily          每日佣金统计
```

## Risks / Trade-offs

### 风险 1：异步计算失败

**风险**：佣金计算任务失败导致佣金未发放

**缓解**：
- Asynq 自动重试机制
- 记录任务执行日志
- 提供手动触发补偿接口

### 风险 2：并发更新钱包余额

**风险**：多笔佣金同时入账导致余额计算错误

**缓解**：
- 使用数据库事务
- 钱包更新使用乐观锁或悲观锁

### 风险 3：代理层级变更

**风险**：订单支付后代理层级变更，佣金计算基于哪个时间点？

**缓解**：
- 佣金计算基于订单支付时的代理关系
- 订单中可记录销售店铺ID快照

## Open Questions

1. **梯度一次性佣金的档位排序规则？**
   - 当前设计：选择最高匹配档位（达标档位中阈值最高的）
   - 待确认：是否正确？是否需要支持阶梯式累加？

2. **设备级购买如何处理？**
   - 当前设计：设备购买时使用 Device.SeriesAllocationID 和 Device.FirstCommissionPaid
   - 待确认：设备下多张卡时，一次性佣金只发一次（按设备），还是按卡数倍增？

3. **累计充值的统计周期？**
   - 当前设计：永久累计（从卡开始使用至今）
   - 待确认：是否需要支持按自然年/月重置累计金额？

4. **一次性佣金是否支持多级分佣？**
   - 当前设计：只发放给卡/设备的直接归属店铺
   - 待确认：是否需要上级代理也获得一次性佣金？如何分配比例？
