# 累计触发一次性佣金逻辑流程

## 概述

一次性佣金支持两种触发方式：
1. **单次充值触发**：单笔订单金额达到阈值即发放
2. **累计充值触发**：累计多次充值金额达到阈值后发放（本文档重点）

## 累计触发流程

```
┌─────────────────────────────────────────────────────────────┐
│                    用户购买套餐                              │
└────────────────────┬────────────────────────────────────────┘
                     │
        ┌────────────▼──────────────┐
        │  支付成功触发佣金计算      │
        └────────────┬──────────────┘
                     │
        ┌────────────▼──────────────┐
        │  读取一次性佣金配置        │
        │  - EnableOneTimeCommission │
        │  - Trigger = accumulated   │
        │  - Threshold 阈值          │
        └────────────┬──────────────┘
                     │
        ┌────────────▼──────────────┐
        │  累计金额 += 本次订单金额   │
        │  写回 AccumulatedRecharge  │
        └────────────┬──────────────┘
                     │
        ┌────────────▼──────────────┐
        │  累计金额 >= 阈值？         │
        └─────┬───────────┬──────────┘
              │ 否        │ 是
              │           │
              │      ┌────▼───────────────┐
              │      │ FirstCommissionPaid│
              │      │ 已标记？            │
              │      └────┬───────┬───────┘
              │           │ 是    │ 否
              │           │       │
              │      ┌────▼────┐  │
              │      │ 跳过    │  │
              │      └─────────┘  │
              │                   │
              │              ┌────▼────────┐
              │              │ 计算佣金金额 │
              │              └────┬────────┘
              │                   │
              │              ┌────▼────────┐
              │              │ 创建佣金记录 │
              │              │ 佣金入账     │
              │              └────┬────────┘
              │                   │
              │              ┌────▼────────┐
              │              │ 标记         │
              │              │FirstCommission│
              │              │Paid = true   │
              │              └────┬────────┘
              │                   │
              └───────────────────▼
                     │
        ┌────────────▼──────────────┐
        │  完成                       │
        └───────────────────────────┘
```

## 关键字段

### IotCard / Device 模型

| 字段 | 类型 | 说明 |
|------|------|------|
| `accumulated_recharge` | int64 | 累计充值金额（分），每次支付成功都会累加 |
| `first_commission_paid` | bool | 一次性佣金是否已发放，防止重复发放 |
| `series_allocation_id` | uint | 关联的系列分配ID，用于获取配置 |

### ShopSeriesAllocation 模型

| 字段 | 类型 | 说明 |
|------|------|------|
| `enable_one_time_commission` | bool | 是否启用一次性佣金 |
| `one_time_commission_trigger` | string | 触发方式：`single_recharge` 或 `accumulated_recharge` |
| `one_time_commission_threshold` | int64 | 最低阈值（分） |
| `one_time_commission_type` | string | 佣金类型：`fixed` 或 `tiered` |
| `one_time_commission_mode` | string | 返佣模式：`fixed` 或 `percent` |
| `one_time_commission_value` | int64 | 佣金值（分或千分比） |

## 核心逻辑实现

### 1. 累计金额更新

**位置**：`internal/service/commission_calculation/service.go`

```go
// 累计充值触发场景：每次支付都写回累计金额
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
    newAccumulated := card.AccumulatedRecharge + order.TotalAmount
    if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
        Update("accumulated_recharge", newAccumulated).Error; err != nil {
        return errors.Wrap(errors.CodeDatabaseError, err, "更新卡累计充值金额失败")
    }
    card.AccumulatedRecharge = newAccumulated
}
```

### 2. 阈值判断与发放

```go
// 判断是否已发放过
if card.FirstCommissionPaid {
    return nil  // 已发放，跳过
}

// 根据触发方式选择充值金额
var rechargeAmount int64
switch allocation.OneTimeCommissionTrigger {
case model.OneTimeCommissionTriggerSingleRecharge:
    rechargeAmount = order.TotalAmount  // 单次金额
case model.OneTimeCommissionTriggerAccumulatedRecharge:
    rechargeAmount = card.AccumulatedRecharge  // 累计金额
default:
    return nil
}

// 判断是否达到阈值
if rechargeAmount < allocation.OneTimeCommissionThreshold {
    return nil  // 未达阈值，不发放
}

// 计算佣金、创建记录、入账
commissionAmount, err := s.calculateOneTimeCommission(ctx, allocation, order.TotalAmount)
// ...

// 标记已发放
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
    Update("first_commission_paid", true).Error; err != nil {
    return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
}
```

## 典型场景示例

### 场景：阈值 10000 分（100元），佣金 500 分（5元）

| 支付次数 | 订单金额 | 累计金额 | 是否发放佣金 | 说明 |
|---------|---------|---------|-------------|------|
| 第1次 | 3000 | 3000 | ❌ | 未达阈值 |
| 第2次 | 4000 | 7000 | ❌ | 未达阈值 |
| 第3次 | 4000 | 11000 | ✅ 发放500 | 达到阈值，首次发放 |
| 第4次 | 3000 | 14000 | ❌ | 已发放过，不重复 |
| 第5次 | 5000 | 19000 | ❌ | 已发放过，不重复 |

## 注意事项

1. **每次支付都写回累计金额**：即使未达阈值，也要更新 `accumulated_recharge`
2. **仅发放一次**：通过 `first_commission_paid` 标记防止重复发放
3. **事务保证一致性**：累计金额更新、佣金记录创建、标记更新都在同一事务中
4. **适用于单卡和设备**：`IotCard` 和 `Device` 模型都有相同字段，逻辑完全一致

## 测试覆盖

单元测试：`tests/unit/commission_calculation_service_test.go`

- ✅ 第一次支付：累计金额更新为 3000，未发放
- ✅ 第二次支付：累计金额更新为 7000，未发放
- ✅ 第三次支付：累计金额更新为 11000，发放佣金 500
- ✅ 第四次支付：累计金额更新为 14000，不重复发放

集成测试：`tests/integration/shop_series_allocation_test.go`

- ✅ 一次性佣金配置落库（固定类型）
- ✅ 一次性佣金配置落库（梯度类型）
- ✅ 启用一次性佣金但未提供配置应失败

## 相关文档

- [API 文档](../api-documentation-guide.md)：自动生成的 OpenAPI 文档包含所有参数说明
- [DTO 规范](../dto-standards.md)：一次性佣金配置的请求/响应结构
- [Model 规范](../model-standards.md)：数据库字段定义
