All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
- 归档 fix-one-time-commission-config-and-accumulation 到 archive/2026-01-29-* - 同步 delta specs 到主规范(one-time-commission-trigger、commission-calculation) - 新增累计触发逻辑文档和测试用例 - 修复一次性佣金配置落库和累计充值更新逻辑
182 lines
8.1 KiB
Markdown
182 lines
8.1 KiB
Markdown
# 累计触发一次性佣金逻辑流程
|
||
|
||
## 概述
|
||
|
||
一次性佣金支持两种触发方式:
|
||
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):数据库字段定义
|