归档一次性佣金配置落库与累计触发修复,同步规范文档到主 specs
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m45s
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) - 新增累计触发逻辑文档和测试用例 - 修复一次性佣金配置落库和累计充值更新逻辑
This commit is contained in:
181
docs/commission/accumulated-trigger-logic.md
Normal file
181
docs/commission/accumulated-trigger-logic.md
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
# 累计触发一次性佣金逻辑流程
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
一次性佣金支持两种触发方式:
|
||||||
|
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):数据库字段定义
|
||||||
@@ -113,7 +113,7 @@ func initServices(s *stores, deps *Dependencies) *services {
|
|||||||
Carrier: carrierSvc.New(s.Carrier),
|
Carrier: carrierSvc.New(s.Carrier),
|
||||||
PackageSeries: packageSeriesSvc.New(s.PackageSeries),
|
PackageSeries: packageSeriesSvc.New(s.PackageSeries),
|
||||||
Package: packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.ShopSeriesCommissionTier),
|
Package: packageSvc.New(s.Package, s.PackageSeries, s.ShopPackageAllocation, s.ShopSeriesAllocation, s.ShopSeriesCommissionTier),
|
||||||
ShopSeriesAllocation: shopSeriesAllocationSvc.New(s.ShopSeriesAllocation, s.ShopSeriesCommissionTier, s.ShopSeriesAllocationConfig, s.Shop, s.PackageSeries, s.Package),
|
ShopSeriesAllocation: shopSeriesAllocationSvc.New(s.ShopSeriesAllocation, s.ShopSeriesCommissionTier, s.ShopSeriesAllocationConfig, s.ShopSeriesOneTimeCommissionTier, s.Shop, s.PackageSeries, s.Package),
|
||||||
ShopPackageAllocation: shopPackageAllocationSvc.New(s.ShopPackageAllocation, s.ShopSeriesAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package),
|
ShopPackageAllocation: shopPackageAllocationSvc.New(s.ShopPackageAllocation, s.ShopSeriesAllocation, s.ShopPackageAllocationPriceHistory, s.Shop, s.Package),
|
||||||
ShopPackageBatchAllocation: shopPackageBatchAllocationSvc.New(deps.DB, s.Package, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopSeriesAllocationConfig, s.ShopSeriesCommissionTier, s.ShopSeriesCommissionStats, s.Shop),
|
ShopPackageBatchAllocation: shopPackageBatchAllocationSvc.New(deps.DB, s.Package, s.ShopSeriesAllocation, s.ShopPackageAllocation, s.ShopSeriesAllocationConfig, s.ShopSeriesCommissionTier, s.ShopSeriesCommissionStats, s.Shop),
|
||||||
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
|
ShopPackageBatchPricing: shopPackageBatchPricingSvc.New(deps.DB, s.ShopPackageAllocation, s.ShopPackageAllocationPriceHistory, s.Shop),
|
||||||
|
|||||||
@@ -194,10 +194,6 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
|
|||||||
return errors.Wrap(errors.CodeDatabaseError, err, "获取卡信息失败")
|
return errors.Wrap(errors.CodeDatabaseError, err, "获取卡信息失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
if card.FirstCommissionPaid {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if card.SeriesAllocationID == nil {
|
if card.SeriesAllocationID == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -211,12 +207,25 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
if card.FirstCommissionPaid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var rechargeAmount int64
|
var rechargeAmount int64
|
||||||
switch allocation.OneTimeCommissionTrigger {
|
switch allocation.OneTimeCommissionTrigger {
|
||||||
case model.OneTimeCommissionTriggerSingleRecharge:
|
case model.OneTimeCommissionTriggerSingleRecharge:
|
||||||
rechargeAmount = order.TotalAmount
|
rechargeAmount = order.TotalAmount
|
||||||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||||||
rechargeAmount = card.AccumulatedRecharge + order.TotalAmount
|
rechargeAmount = card.AccumulatedRecharge
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -260,12 +269,9 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
|
|||||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]any{"first_commission_paid": true}
|
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
|
||||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
Update("first_commission_paid", true).Error; err != nil {
|
||||||
updates["accumulated_recharge"] = rechargeAmount
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
|
||||||
}
|
|
||||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).Updates(updates).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -283,10 +289,6 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
|
|||||||
return errors.Wrap(errors.CodeDatabaseError, err, "获取设备信息失败")
|
return errors.Wrap(errors.CodeDatabaseError, err, "获取设备信息失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
if device.FirstCommissionPaid {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if device.SeriesAllocationID == nil {
|
if device.SeriesAllocationID == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -300,12 +302,25 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||||
|
newAccumulated := device.AccumulatedRecharge + order.TotalAmount
|
||||||
|
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).
|
||||||
|
Update("accumulated_recharge", newAccumulated).Error; err != nil {
|
||||||
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备累计充值金额失败")
|
||||||
|
}
|
||||||
|
device.AccumulatedRecharge = newAccumulated
|
||||||
|
}
|
||||||
|
|
||||||
|
if device.FirstCommissionPaid {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var rechargeAmount int64
|
var rechargeAmount int64
|
||||||
switch allocation.OneTimeCommissionTrigger {
|
switch allocation.OneTimeCommissionTrigger {
|
||||||
case model.OneTimeCommissionTriggerSingleRecharge:
|
case model.OneTimeCommissionTriggerSingleRecharge:
|
||||||
rechargeAmount = order.TotalAmount
|
rechargeAmount = order.TotalAmount
|
||||||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||||||
rechargeAmount = device.AccumulatedRecharge + order.TotalAmount
|
rechargeAmount = device.AccumulatedRecharge
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -349,12 +364,9 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
|
|||||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
updates := map[string]any{"first_commission_paid": true}
|
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).
|
||||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
Update("first_commission_paid", true).Error; err != nil {
|
||||||
updates["accumulated_recharge"] = rechargeAmount
|
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败")
|
||||||
}
|
|
||||||
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).Updates(updates).Error; err != nil {
|
|
||||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备状态失败")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type Service struct {
|
|||||||
allocationStore *postgres.ShopSeriesAllocationStore
|
allocationStore *postgres.ShopSeriesAllocationStore
|
||||||
tierStore *postgres.ShopSeriesCommissionTierStore
|
tierStore *postgres.ShopSeriesCommissionTierStore
|
||||||
configStore *postgres.ShopSeriesAllocationConfigStore
|
configStore *postgres.ShopSeriesAllocationConfigStore
|
||||||
|
oneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore
|
||||||
shopStore *postgres.ShopStore
|
shopStore *postgres.ShopStore
|
||||||
packageSeriesStore *postgres.PackageSeriesStore
|
packageSeriesStore *postgres.PackageSeriesStore
|
||||||
packageStore *postgres.PackageStore
|
packageStore *postgres.PackageStore
|
||||||
@@ -28,6 +29,7 @@ func New(
|
|||||||
allocationStore *postgres.ShopSeriesAllocationStore,
|
allocationStore *postgres.ShopSeriesAllocationStore,
|
||||||
tierStore *postgres.ShopSeriesCommissionTierStore,
|
tierStore *postgres.ShopSeriesCommissionTierStore,
|
||||||
configStore *postgres.ShopSeriesAllocationConfigStore,
|
configStore *postgres.ShopSeriesAllocationConfigStore,
|
||||||
|
oneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore,
|
||||||
shopStore *postgres.ShopStore,
|
shopStore *postgres.ShopStore,
|
||||||
packageSeriesStore *postgres.PackageSeriesStore,
|
packageSeriesStore *postgres.PackageSeriesStore,
|
||||||
packageStore *postgres.PackageStore,
|
packageStore *postgres.PackageStore,
|
||||||
@@ -36,6 +38,7 @@ func New(
|
|||||||
allocationStore: allocationStore,
|
allocationStore: allocationStore,
|
||||||
tierStore: tierStore,
|
tierStore: tierStore,
|
||||||
configStore: configStore,
|
configStore: configStore,
|
||||||
|
oneTimeCommissionTierStore: oneTimeCommissionTierStore,
|
||||||
shopStore: shopStore,
|
shopStore: shopStore,
|
||||||
packageSeriesStore: packageSeriesStore,
|
packageSeriesStore: packageSeriesStore,
|
||||||
packageStore: packageStore,
|
packageStore: packageStore,
|
||||||
@@ -99,6 +102,10 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
|
|||||||
return nil, errors.New(errors.CodeConflict, "该店铺已分配此套餐系列")
|
return nil, errors.New(errors.CodeConflict, "该店铺已分配此套餐系列")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.validateOneTimeCommissionConfig(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
allocation := &model.ShopSeriesAllocation{
|
allocation := &model.ShopSeriesAllocation{
|
||||||
ShopID: req.ShopID,
|
ShopID: req.ShopID,
|
||||||
SeriesID: req.SeriesID,
|
SeriesID: req.SeriesID,
|
||||||
@@ -108,12 +115,34 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
|
|||||||
EnableTierCommission: req.EnableTierCommission,
|
EnableTierCommission: req.EnableTierCommission,
|
||||||
Status: constants.StatusEnabled,
|
Status: constants.StatusEnabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 处理一次性佣金配置
|
||||||
|
allocation.EnableOneTimeCommission = req.EnableOneTimeCommission
|
||||||
|
if req.EnableOneTimeCommission && req.OneTimeCommissionConfig != nil {
|
||||||
|
cfg := req.OneTimeCommissionConfig
|
||||||
|
allocation.OneTimeCommissionType = cfg.Type
|
||||||
|
allocation.OneTimeCommissionTrigger = cfg.Trigger
|
||||||
|
allocation.OneTimeCommissionThreshold = cfg.Threshold
|
||||||
|
// fixed 类型需要保存 mode 和 value
|
||||||
|
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||||
|
allocation.OneTimeCommissionMode = cfg.Mode
|
||||||
|
allocation.OneTimeCommissionValue = cfg.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
allocation.Creator = currentUserID
|
allocation.Creator = currentUserID
|
||||||
|
|
||||||
if err := s.allocationStore.Create(ctx, allocation); err != nil {
|
if err := s.allocationStore.Create(ctx, allocation); err != nil {
|
||||||
return nil, fmt.Errorf("创建分配失败: %w", err)
|
return nil, fmt.Errorf("创建分配失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果是梯度类型,保存梯度配置
|
||||||
|
if req.EnableOneTimeCommission && req.OneTimeCommissionConfig != nil &&
|
||||||
|
req.OneTimeCommissionConfig.Type == model.OneTimeCommissionTypeTiered {
|
||||||
|
if err := s.saveOneTimeCommissionTiers(ctx, allocation.ID, req.OneTimeCommissionConfig.Tiers, currentUserID); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建一次性佣金梯度配置失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return s.buildResponse(ctx, allocation, targetShop.ShopName, series.SeriesName)
|
return s.buildResponse(ctx, allocation, targetShop.ShopName, series.SeriesName)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,6 +199,42 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
|
|||||||
}
|
}
|
||||||
allocation.EnableTierCommission = *req.EnableTierCommission
|
allocation.EnableTierCommission = *req.EnableTierCommission
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enableOneTimeCommission := allocation.EnableOneTimeCommission
|
||||||
|
if req.EnableOneTimeCommission != nil {
|
||||||
|
enableOneTimeCommission = *req.EnableOneTimeCommission
|
||||||
|
}
|
||||||
|
if err := s.validateOneTimeCommissionConfigForUpdate(enableOneTimeCommission, req.OneTimeCommissionConfig); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oneTimeCommissionChanged := false
|
||||||
|
if req.EnableOneTimeCommission != nil {
|
||||||
|
if allocation.EnableOneTimeCommission != *req.EnableOneTimeCommission {
|
||||||
|
oneTimeCommissionChanged = true
|
||||||
|
}
|
||||||
|
allocation.EnableOneTimeCommission = *req.EnableOneTimeCommission
|
||||||
|
}
|
||||||
|
if req.OneTimeCommissionConfig != nil && allocation.EnableOneTimeCommission {
|
||||||
|
cfg := req.OneTimeCommissionConfig
|
||||||
|
if allocation.OneTimeCommissionType != cfg.Type ||
|
||||||
|
allocation.OneTimeCommissionTrigger != cfg.Trigger ||
|
||||||
|
allocation.OneTimeCommissionThreshold != cfg.Threshold ||
|
||||||
|
allocation.OneTimeCommissionMode != cfg.Mode ||
|
||||||
|
allocation.OneTimeCommissionValue != cfg.Value {
|
||||||
|
oneTimeCommissionChanged = true
|
||||||
|
}
|
||||||
|
allocation.OneTimeCommissionType = cfg.Type
|
||||||
|
allocation.OneTimeCommissionTrigger = cfg.Trigger
|
||||||
|
allocation.OneTimeCommissionThreshold = cfg.Threshold
|
||||||
|
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||||
|
allocation.OneTimeCommissionMode = cfg.Mode
|
||||||
|
allocation.OneTimeCommissionValue = cfg.Value
|
||||||
|
} else {
|
||||||
|
allocation.OneTimeCommissionMode = ""
|
||||||
|
allocation.OneTimeCommissionValue = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
allocation.Updater = currentUserID
|
allocation.Updater = currentUserID
|
||||||
|
|
||||||
if configChanged {
|
if configChanged {
|
||||||
@@ -182,6 +247,16 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
|
|||||||
return nil, fmt.Errorf("更新分配失败: %w", err)
|
return nil, fmt.Errorf("更新分配失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if oneTimeCommissionChanged && req.OneTimeCommissionConfig != nil &&
|
||||||
|
req.OneTimeCommissionConfig.Type == model.OneTimeCommissionTypeTiered {
|
||||||
|
if err := s.oneTimeCommissionTierStore.DeleteByAllocationID(ctx, allocation.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("清理旧梯度配置失败: %w", err)
|
||||||
|
}
|
||||||
|
if err := s.saveOneTimeCommissionTiers(ctx, allocation.ID, req.OneTimeCommissionConfig.Tiers, currentUserID); err != nil {
|
||||||
|
return nil, fmt.Errorf("更新一次性佣金梯度配置失败: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
shop, _ := s.shopStore.GetByID(ctx, allocation.ShopID)
|
||||||
series, _ := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
|
series, _ := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
|
||||||
|
|
||||||
@@ -323,7 +398,7 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocati
|
|||||||
allocatorShopName = allocatorShop.ShopName
|
allocatorShopName = allocatorShop.ShopName
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dto.ShopSeriesAllocationResponse{
|
resp := &dto.ShopSeriesAllocationResponse{
|
||||||
ID: a.ID,
|
ID: a.ID,
|
||||||
ShopID: a.ShopID,
|
ShopID: a.ShopID,
|
||||||
ShopName: shopName,
|
ShopName: shopName,
|
||||||
@@ -336,10 +411,38 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocati
|
|||||||
Value: a.BaseCommissionValue,
|
Value: a.BaseCommissionValue,
|
||||||
},
|
},
|
||||||
EnableTierCommission: a.EnableTierCommission,
|
EnableTierCommission: a.EnableTierCommission,
|
||||||
|
EnableOneTimeCommission: a.EnableOneTimeCommission,
|
||||||
Status: a.Status,
|
Status: a.Status,
|
||||||
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
CreatedAt: a.CreatedAt.Format(time.RFC3339),
|
||||||
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
if a.EnableOneTimeCommission {
|
||||||
|
cfg := &dto.OneTimeCommissionConfig{
|
||||||
|
Type: a.OneTimeCommissionType,
|
||||||
|
Trigger: a.OneTimeCommissionTrigger,
|
||||||
|
Threshold: a.OneTimeCommissionThreshold,
|
||||||
|
Mode: a.OneTimeCommissionMode,
|
||||||
|
Value: a.OneTimeCommissionValue,
|
||||||
|
}
|
||||||
|
if a.OneTimeCommissionType == model.OneTimeCommissionTypeTiered {
|
||||||
|
tiers, err := s.oneTimeCommissionTierStore.ListByAllocationID(ctx, a.ID)
|
||||||
|
if err == nil && len(tiers) > 0 {
|
||||||
|
cfg.Tiers = make([]dto.OneTimeCommissionTierEntry, len(tiers))
|
||||||
|
for i, t := range tiers {
|
||||||
|
cfg.Tiers[i] = dto.OneTimeCommissionTierEntry{
|
||||||
|
TierType: t.TierType,
|
||||||
|
Threshold: t.ThresholdValue,
|
||||||
|
Mode: t.CommissionMode,
|
||||||
|
Value: t.CommissionValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp.OneTimeCommissionConfig = cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) createNewConfigVersion(ctx context.Context, allocation *model.ShopSeriesAllocation) error {
|
func (s *Service) createNewConfigVersion(ctx context.Context, allocation *model.ShopSeriesAllocation) error {
|
||||||
@@ -371,6 +474,72 @@ func (s *Service) createNewConfigVersion(ctx context.Context, allocation *model.
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) validateOneTimeCommissionConfig(req *dto.CreateShopSeriesAllocationRequest) error {
|
||||||
|
if !req.EnableOneTimeCommission {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if req.OneTimeCommissionConfig == nil {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "启用一次性佣金时必须提供配置")
|
||||||
|
}
|
||||||
|
cfg := req.OneTimeCommissionConfig
|
||||||
|
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||||
|
if cfg.Mode == "" {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣模式")
|
||||||
|
}
|
||||||
|
if cfg.Value <= 0 {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣金额")
|
||||||
|
}
|
||||||
|
} else if cfg.Type == model.OneTimeCommissionTypeTiered {
|
||||||
|
if len(cfg.Tiers) == 0 {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "梯度类型一次性佣金必须提供梯度档位")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) validateOneTimeCommissionConfigForUpdate(enableOneTimeCommission bool, cfg *dto.OneTimeCommissionConfig) error {
|
||||||
|
if !enableOneTimeCommission {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if cfg.Type == model.OneTimeCommissionTypeFixed {
|
||||||
|
if cfg.Mode == "" {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣模式")
|
||||||
|
}
|
||||||
|
if cfg.Value <= 0 {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "固定类型一次性佣金必须指定返佣金额")
|
||||||
|
}
|
||||||
|
} else if cfg.Type == model.OneTimeCommissionTypeTiered {
|
||||||
|
if len(cfg.Tiers) == 0 {
|
||||||
|
return errors.New(errors.CodeInvalidParam, "梯度类型一次性佣金必须提供梯度档位")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) saveOneTimeCommissionTiers(ctx context.Context, allocationID uint, tiers []dto.OneTimeCommissionTierEntry, userID uint) error {
|
||||||
|
if len(tiers) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tierModels := make([]*model.ShopSeriesOneTimeCommissionTier, len(tiers))
|
||||||
|
for i, t := range tiers {
|
||||||
|
tierModels[i] = &model.ShopSeriesOneTimeCommissionTier{
|
||||||
|
AllocationID: allocationID,
|
||||||
|
TierType: t.TierType,
|
||||||
|
ThresholdValue: t.Threshold,
|
||||||
|
CommissionMode: t.Mode,
|
||||||
|
CommissionValue: t.Value,
|
||||||
|
Status: constants.StatusEnabled,
|
||||||
|
}
|
||||||
|
tierModels[i].Creator = userID
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.oneTimeCommissionTierStore.BatchCreate(ctx, tierModels)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) GetEffectiveConfig(ctx context.Context, allocationID uint, at time.Time) (*model.ShopSeriesAllocationConfig, error) {
|
func (s *Service) GetEffectiveConfig(ctx context.Context, allocationID uint, at time.Time) (*model.ShopSeriesAllocationConfig, error) {
|
||||||
config, err := s.configStore.GetEffective(ctx, allocationID, at)
|
config, err := s.configStore.GetEffective(ctx, allocationID, at)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
# 一次性佣金修复 - 设计
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
1. 通过 `ShopSeriesAllocation` 创建/更新接口即可配置一次性佣金并生效(你确认 B=1)。
|
||||||
|
2. “累计充值触发”场景每次支付成功都累加保存,达到阈值触发一次性佣金发放。
|
||||||
|
3. 发放具备幂等:同一资源(卡/设备)只发放一次。
|
||||||
|
|
||||||
|
## 1) 常量定义
|
||||||
|
|
||||||
|
所有常量必须在 `pkg/constants/` 中定义,禁止硬编码:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// pkg/constants/commission.go
|
||||||
|
|
||||||
|
// 一次性佣金类型
|
||||||
|
const (
|
||||||
|
OneTimeCommissionTypeFixed = "fixed" // 固定类型
|
||||||
|
OneTimeCommissionTypeTiered = "tiered" // 梯度类型
|
||||||
|
)
|
||||||
|
|
||||||
|
// 一次性佣金触发方式
|
||||||
|
const (
|
||||||
|
OneTimeCommissionTriggerImmediate = "immediate" // 立即触发
|
||||||
|
OneTimeCommissionTriggerAccumulatedRecharge = "accumulated_recharge" // 累计充值触发
|
||||||
|
)
|
||||||
|
|
||||||
|
// 佣金模式
|
||||||
|
const (
|
||||||
|
CommissionModeFixed = "fixed" // 固定金额
|
||||||
|
CommissionModePercent = "percent" // 百分比
|
||||||
|
)
|
||||||
|
|
||||||
|
// 梯度类型
|
||||||
|
const (
|
||||||
|
TierTypeSalesCount = "sales_count" // 销售数量
|
||||||
|
TierTypeSalesAmount = "sales_amount" // 销售金额
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用示例**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Service 层使用常量
|
||||||
|
if allocation.OneTimeCommissionType == constants.OneTimeCommissionTypeFixed {
|
||||||
|
// 固定类型处理
|
||||||
|
}
|
||||||
|
|
||||||
|
if allocation.OneTimeCommissionTrigger == constants.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||||
|
// 累计充值触发逻辑
|
||||||
|
}
|
||||||
|
|
||||||
|
// 禁止硬编码(错误示例)
|
||||||
|
if allocation.OneTimeCommissionType == "fixed" { // ❌ 禁止
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2) 配置落库
|
||||||
|
|
||||||
|
### 固定类型(fixed)
|
||||||
|
|
||||||
|
在 `tb_shop_series_allocation` 写入:
|
||||||
|
- enable_one_time_commission
|
||||||
|
- one_time_commission_type = fixed
|
||||||
|
- one_time_commission_trigger
|
||||||
|
- one_time_commission_threshold
|
||||||
|
- one_time_commission_mode(fixed/percent)
|
||||||
|
- one_time_commission_value
|
||||||
|
|
||||||
|
### 梯度类型(tiered)
|
||||||
|
|
||||||
|
在 `tb_shop_series_allocation` 写入:
|
||||||
|
- enable_one_time_commission
|
||||||
|
- one_time_commission_type = tiered
|
||||||
|
- one_time_commission_trigger
|
||||||
|
- one_time_commission_threshold
|
||||||
|
|
||||||
|
并在 `tb_shop_series_one_time_commission_tier` 维护档位:
|
||||||
|
- allocation_id
|
||||||
|
- tier_type(sales_count/sales_amount)
|
||||||
|
- threshold_value
|
||||||
|
- commission_mode(fixed/percent)
|
||||||
|
- commission_value
|
||||||
|
|
||||||
|
更新策略建议:
|
||||||
|
- 更新配置时:先删除 allocation_id 对应的旧 tiers,再批量插入新 tiers(实现简单且可控)
|
||||||
|
|
||||||
|
### 数据库设计约束
|
||||||
|
|
||||||
|
根据项目规范(AGENTS.md),必须遵守以下数据库设计原则:
|
||||||
|
|
||||||
|
**❌ 禁止事项**:
|
||||||
|
- 禁止在 `tb_shop_series_one_time_commission_tier` 和 `tb_shop_series_allocation` 之间建立外键约束
|
||||||
|
- 禁止使用 GORM 关联关系标签(foreignKey、hasMany、belongsTo)
|
||||||
|
|
||||||
|
**✅ 必须遵守**:
|
||||||
|
- 关联通过存储 ID 字段(allocation_id)手动维护
|
||||||
|
- 关联数据在代码层面显式查询
|
||||||
|
|
||||||
|
**实现示例**:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Store 层:显式关联查询
|
||||||
|
func (s *ShopSeriesAllocationStore) GetByIDWithTiers(ctx context.Context, id uint) (*model.ShopSeriesAllocation, []*model.ShopSeriesOneTimeCommissionTier, error) {
|
||||||
|
// 1. 查询分配配置
|
||||||
|
var allocation model.ShopSeriesAllocation
|
||||||
|
if err := s.db.WithContext(ctx).First(&allocation, id).Error; err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 如果是梯度类型,显式查询档位
|
||||||
|
var tiers []*model.ShopSeriesOneTimeCommissionTier
|
||||||
|
if allocation.OneTimeCommissionType == constants.OneTimeCommissionTypeTiered {
|
||||||
|
if err := s.db.WithContext(ctx).
|
||||||
|
Where("allocation_id = ?", id).
|
||||||
|
Order("threshold_value ASC").
|
||||||
|
Find(&tiers).Error; err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &allocation, tiers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 层:组装响应
|
||||||
|
func (s *ShopSeriesAllocationService) GetByID(ctx context.Context, id uint) (*dto.AllocationResponse, error) {
|
||||||
|
allocation, tiers, err := s.store.ShopSeriesAllocation.GetByIDWithTiers(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := &dto.AllocationResponse{
|
||||||
|
// ... 映射字段
|
||||||
|
EnableOneTimeCommission: allocation.EnableOneTimeCommission,
|
||||||
|
OneTimeCommissionConfig: mapOneTimeCommissionConfig(allocation, tiers),
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3) 累计触发逻辑
|
||||||
|
|
||||||
|
针对 `accumulated_recharge`:
|
||||||
|
- 每次支付成功都更新 `AccumulatedRecharge += orderAmount`
|
||||||
|
- 若累计达到阈值且未发放过:
|
||||||
|
- 计算佣金金额
|
||||||
|
- 创建佣金记录并入账
|
||||||
|
- 标记 `FirstCommissionPaid = true`
|
||||||
|
|
||||||
|
注意:累计的更新应当以“支付成功”为准,避免未支付订单污染累计值。
|
||||||
|
|
||||||
|
## 3) 错误处理规范
|
||||||
|
|
||||||
|
所有错误必须在 `pkg/errors/` 中定义,使用统一错误码系统:
|
||||||
|
|
||||||
|
### 参数校验错误(40xxx)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// pkg/errors/codes.go 或 pkg/errors/commission.go
|
||||||
|
ErrOneTimeCommissionConfigInvalid = NewAppError(40101, "一次性佣金配置无效")
|
||||||
|
ErrOneTimeCommissionModeMissing = NewAppError(40102, "一次性佣金模式缺失")
|
||||||
|
ErrOneTimeCommissionValueMissing = NewAppError(40103, "一次性佣金金额/比例缺失")
|
||||||
|
ErrOneTimeCommissionTiersMissing = NewAppError(40104, "梯度佣金档位配置缺失")
|
||||||
|
ErrOneTimeCommissionTierInvalid = NewAppError(40105, "梯度佣金档位配置无效")
|
||||||
|
ErrOneTimeCommissionThresholdInvalid = NewAppError(40106, "一次性佣金阈值无效")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 业务逻辑错误(50xxx)
|
||||||
|
|
||||||
|
```go
|
||||||
|
ErrAccumulatedRechargeUpdateFailed = NewAppError(50101, "累计充值金额更新失败")
|
||||||
|
ErrOneTimeCommissionAlreadyPaid = NewAppError(50102, "一次性佣金已发放")
|
||||||
|
ErrCommissionCalculationFailed = NewAppError(50103, "佣金计算失败")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用示例
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Service 层参数校验
|
||||||
|
if req.EnableOneTimeCommission && req.OneTimeCommissionConfig == nil {
|
||||||
|
return errors.ErrOneTimeCommissionConfigInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.OneTimeCommissionConfig.Type == "fixed" &&
|
||||||
|
req.OneTimeCommissionConfig.Mode == "" {
|
||||||
|
return errors.ErrOneTimeCommissionModeMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.OneTimeCommissionConfig.Type == "tiered" &&
|
||||||
|
len(req.OneTimeCommissionConfig.Tiers) == 0 {
|
||||||
|
return errors.ErrOneTimeCommissionTiersMissing
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler 层统一处理(全局 ErrorHandler 自动处理)
|
||||||
|
if err := service.CreateAllocation(ctx, req); err != nil {
|
||||||
|
return err // 自动转换为 JSON 响应
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4) 性能优化
|
||||||
|
|
||||||
|
### 并发控制策略
|
||||||
|
|
||||||
|
累计值更新存在并发写风险,必须使用乐观锁防止数据覆盖:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// 使用 GORM 乐观锁(基于 version 字段)
|
||||||
|
type CommissionRecord struct {
|
||||||
|
// ... 其他字段
|
||||||
|
AccumulatedRecharge int64 `gorm:"column:accumulated_recharge"`
|
||||||
|
Version int `gorm:"column:version"` // 乐观锁版本号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新时自动检查版本号
|
||||||
|
result := db.Model(&record).
|
||||||
|
Where("id = ? AND version = ?", record.ID, record.Version).
|
||||||
|
Updates(map[string]interface{}{
|
||||||
|
"accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount),
|
||||||
|
"version": gorm.Expr("version + 1"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
// 版本冲突,需要重试
|
||||||
|
return errors.ErrAccumulatedRechargeUpdateFailed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**替代方案**(如果无 version 字段):
|
||||||
|
```go
|
||||||
|
// 使用 SQL 原子操作
|
||||||
|
result := db.Exec(`
|
||||||
|
UPDATE tb_commission_records
|
||||||
|
SET accumulated_recharge = accumulated_recharge + ?,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = ?
|
||||||
|
`, amount, recordID)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 索引优化
|
||||||
|
|
||||||
|
梯度配置表需要添加索引优化查询性能:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- tb_shop_series_one_time_commission_tier 表索引
|
||||||
|
CREATE INDEX idx_allocation_id ON tb_shop_series_one_time_commission_tier(allocation_id);
|
||||||
|
|
||||||
|
-- 组合索引(如果需要按档位类型过滤)
|
||||||
|
CREATE INDEX idx_allocation_tier ON tb_shop_series_one_time_commission_tier(allocation_id, tier_type);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 缓存策略(可选)
|
||||||
|
|
||||||
|
高频查询的分配配置可缓存到 Redis:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Redis Key 生成函数(定义在 pkg/constants/redis.go)
|
||||||
|
func RedisShopSeriesAllocationKey(allocationID uint) string {
|
||||||
|
return fmt.Sprintf("shop:series:allocation:%d", allocationID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存读取
|
||||||
|
func (s *ShopSeriesAllocationService) GetByID(ctx context.Context, id uint) (*model.ShopSeriesAllocation, error) {
|
||||||
|
// 1. 尝试从 Redis 读取
|
||||||
|
key := constants.RedisShopSeriesAllocationKey(id)
|
||||||
|
cached, err := s.redis.Get(ctx, key).Result()
|
||||||
|
if err == nil {
|
||||||
|
var allocation model.ShopSeriesAllocation
|
||||||
|
if err := json.Unmarshal([]byte(cached), &allocation); err == nil {
|
||||||
|
return &allocation, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 从数据库读取
|
||||||
|
allocation, err := s.store.ShopSeriesAllocation.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 写入 Redis(TTL 5分钟)
|
||||||
|
data, _ := json.Marshal(allocation)
|
||||||
|
s.redis.Set(ctx, key, data, 5*time.Minute)
|
||||||
|
|
||||||
|
return allocation, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存失效(创建/更新/删除时)
|
||||||
|
func (s *ShopSeriesAllocationService) Update(ctx context.Context, req *dto.UpdateAllocationRequest) error {
|
||||||
|
// ... 更新逻辑
|
||||||
|
|
||||||
|
// 删除缓存
|
||||||
|
key := constants.RedisShopSeriesAllocationKey(req.ID)
|
||||||
|
s.redis.Del(ctx, key)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 性能要求
|
||||||
|
|
||||||
|
根据项目规范(AGENTS.md):
|
||||||
|
- API P95 响应时间 < 200ms
|
||||||
|
- API P99 响应时间 < 500ms
|
||||||
|
- 数据库查询 < 50ms
|
||||||
|
- 避免 N+1 查询,使用批量操作
|
||||||
|
|
||||||
|
## 5) 测试
|
||||||
|
|
||||||
|
- 配置落库测试:创建/更新分配后,查询数据库字段与 tiers 表是否一致
|
||||||
|
- 累计触发测试:模拟多次支付累计到阈值,验证只发放一次且累计值递增
|
||||||
|
- 修复现有单测字段不匹配导致的编译失败
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 创建/更新 `ShopSeriesAllocation` 时,一次性佣金配置能正确落库并在查询响应中返回。
|
||||||
|
- 累计触发场景下,多次支付能累加并在达到阈值时发放一次性佣金;之后不重复发放。
|
||||||
|
- `go test ./...` 通过。
|
||||||
|
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 更新累计充值金额
|
||||||
|
|
||||||
|
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额。
|
||||||
|
|
||||||
|
**关键修复**:每次支付成功都必须写回累计充值金额,确保累计值能正确用于一次性佣金的累计触发判断。
|
||||||
|
|
||||||
|
#### Scenario: 单卡订单更新累计充值
|
||||||
|
|
||||||
|
- **WHEN** 单卡订单支付成功,金额 100 元
|
||||||
|
- **THEN** 系统读取 IotCard.accumulated_recharge 当前值
|
||||||
|
- **AND** 增加 10000 分(100 元 = 10000 分)
|
||||||
|
- **AND** 将新值写回 IotCard.accumulated_recharge
|
||||||
|
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||||
|
|
||||||
|
#### Scenario: 设备订单更新累计充值
|
||||||
|
|
||||||
|
- **WHEN** 设备订单支付成功,金额 300 元
|
||||||
|
- **THEN** 系统读取 Device.accumulated_recharge 当前值
|
||||||
|
- **AND** 增加 30000 分(300 元 = 30000 分)
|
||||||
|
- **AND** 将新值写回 Device.accumulated_recharge
|
||||||
|
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||||
|
|
||||||
|
#### Scenario: 累计充值更新使用原子操作
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值金额
|
||||||
|
- **THEN** 系统使用 SQL 原子操作(如 `accumulated_recharge = accumulated_recharge + ?`)
|
||||||
|
- **OR** 使用 GORM 乐观锁(version 字段)
|
||||||
|
- **AND** 确保并发场景下累计值不会丢失
|
||||||
|
|
||||||
|
#### Scenario: 更新失败不影响佣金计算
|
||||||
|
|
||||||
|
- **WHEN** 累计充值金额更新失败(数据库错误、并发冲突等)
|
||||||
|
- **THEN** 系统记录错误日志
|
||||||
|
- **AND** 继续执行后续的佣金计算流程(成本价差、一次性佣金等)
|
||||||
|
- **AND** 不因累计值更新失败而导致整个佣金计算失败
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 一次性佣金触发检查
|
||||||
|
|
||||||
|
系统 SHALL 在更新累计充值金额后立即检查是否触发一次性佣金。
|
||||||
|
|
||||||
|
#### Scenario: 累计达到阈值触发佣金
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值后,累计值 ≥ 配置阈值
|
||||||
|
- **AND** 卡/设备的 first_commission_paid = false
|
||||||
|
- **THEN** 系统发放一次性佣金
|
||||||
|
- **AND** 标记 first_commission_paid = true
|
||||||
|
|
||||||
|
#### Scenario: 累计未达到阈值不触发
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值后,累计值 < 配置阈值
|
||||||
|
- **THEN** 系统不发放一次性佣金
|
||||||
|
- **AND** first_commission_paid 保持不变
|
||||||
|
|
||||||
|
#### Scenario: 已发放过不重复触发
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值后,累计值 ≥ 配置阈值
|
||||||
|
- **AND** 卡/设备的 first_commission_paid = true
|
||||||
|
- **THEN** 系统不重复发放一次性佣金
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 累计充值更新日志记录
|
||||||
|
|
||||||
|
系统 SHOULD 记录累计充值金额的更新操作,便于问题排查。
|
||||||
|
|
||||||
|
#### Scenario: 记录更新前后的累计值
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值金额
|
||||||
|
- **THEN** 系统在日志中记录:订单 ID、资源类型(卡/设备)、资源 ID、更新前累计值、本次充值金额、更新后累计值
|
||||||
|
|
||||||
|
#### Scenario: 记录更新失败原因
|
||||||
|
|
||||||
|
- **WHEN** 累计充值金额更新失败
|
||||||
|
- **THEN** 系统在日志中记录:订单 ID、资源 ID、失败原因(错误信息)、重试次数(如适用)
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
## MODIFIED Requirements
|
||||||
|
|
||||||
|
### Requirement: 累计充值触发佣金
|
||||||
|
|
||||||
|
系统 SHALL 支持"累计充值"触发条件:当卡/设备的累计充值金额 ≥ 配置阈值时触发一次性佣金。
|
||||||
|
|
||||||
|
**关键修复**:每次支付成功后必须更新累计充值金额,确保累计值能正确递增并达到阈值。
|
||||||
|
|
||||||
|
#### Scenario: 累计达到阈值
|
||||||
|
|
||||||
|
- **WHEN** 卡之前累计充值 200 元,本次充值 150 元,配置阈值 300 元
|
||||||
|
- **THEN** 系统更新累计充值为 350 元
|
||||||
|
- **AND** 累计 350 元 ≥ 300 元,系统发放一次性佣金
|
||||||
|
- **AND** 标记 first_commission_paid = true
|
||||||
|
|
||||||
|
#### Scenario: 累计未达到阈值
|
||||||
|
|
||||||
|
- **WHEN** 卡之前累计充值 100 元,本次充值 100 元,配置阈值 300 元
|
||||||
|
- **THEN** 系统更新累计充值为 200 元
|
||||||
|
- **AND** 累计 200 元 < 300 元,系统不发放一次性佣金
|
||||||
|
|
||||||
|
#### Scenario: 每次支付都更新累计值
|
||||||
|
|
||||||
|
- **WHEN** 卡累计充值为 50 元
|
||||||
|
- **AND** 连续发生 3 次充值:100 元、150 元、80 元
|
||||||
|
- **THEN** 第 1 次充值后累计 150 元
|
||||||
|
- **AND** 第 2 次充值后累计 300 元
|
||||||
|
- **AND** 第 3 次充值后累计 380 元
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 一次性佣金配置获取
|
||||||
|
|
||||||
|
一次性佣金的触发条件和金额 SHALL 从 ShopSeriesAllocation 配置获取。
|
||||||
|
|
||||||
|
**关键修复**:配置必须能够通过 ShopSeriesAllocation 创建/更新接口正确落库并生效。
|
||||||
|
|
||||||
|
#### Scenario: 获取触发条件和金额
|
||||||
|
|
||||||
|
- **WHEN** 触发一次性佣金检查
|
||||||
|
- **THEN** 系统从卡关联的 ShopSeriesAllocation 获取 one_time_commission_trigger(触发类型)、one_time_commission_threshold(阈值)、one_time_commission_mode(模式)、one_time_commission_value(金额/比例)
|
||||||
|
|
||||||
|
#### Scenario: 固定类型配置
|
||||||
|
|
||||||
|
- **WHEN** 创建 ShopSeriesAllocation 时设置 one_time_commission_type = "fixed"
|
||||||
|
- **AND** 设置 one_time_commission_mode = "fixed",one_time_commission_value = 5000(50 元)
|
||||||
|
- **THEN** 系统将配置正确写入数据库
|
||||||
|
- **AND** 查询该配置时能正确返回所有一次性佣金字段
|
||||||
|
|
||||||
|
#### Scenario: 梯度类型配置
|
||||||
|
|
||||||
|
- **WHEN** 创建 ShopSeriesAllocation 时设置 one_time_commission_type = "tiered"
|
||||||
|
- **AND** 提供梯度档位配置:[{threshold: 100, mode: "fixed", value: 2000}, {threshold: 300, mode: "percent", value: 10}]
|
||||||
|
- **THEN** 系统将主配置写入 tb_shop_series_allocation
|
||||||
|
- **AND** 将档位配置写入 tb_shop_series_one_time_commission_tier
|
||||||
|
- **AND** 查询该配置时能正确返回主配置和关联的档位列表
|
||||||
|
|
||||||
|
#### Scenario: 更新梯度配置
|
||||||
|
|
||||||
|
- **WHEN** 更新 ShopSeriesAllocation 的梯度配置
|
||||||
|
- **AND** 新档位配置与旧配置不同
|
||||||
|
- **THEN** 系统先删除旧档位数据(WHERE allocation_id = ?)
|
||||||
|
- **AND** 再批量插入新档位数据
|
||||||
|
- **AND** 查询时返回最新的档位配置
|
||||||
|
|
||||||
|
#### Scenario: 无一次性佣金配置
|
||||||
|
|
||||||
|
- **WHEN** 卡关联的系列分配未启用一次性佣金(enable_one_time_commission = false)
|
||||||
|
- **THEN** 不发放一次性佣金
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: 配置参数校验
|
||||||
|
|
||||||
|
系统 MUST 在创建/更新 ShopSeriesAllocation 时校验一次性佣金配置的完整性。
|
||||||
|
|
||||||
|
#### Scenario: 启用一次性佣金必须提供配置
|
||||||
|
|
||||||
|
- **WHEN** 创建 ShopSeriesAllocation 时设置 enable_one_time_commission = true
|
||||||
|
- **AND** 未提供 one_time_commission_config
|
||||||
|
- **THEN** 系统返回错误:一次性佣金配置无效(错误码 40101)
|
||||||
|
|
||||||
|
#### Scenario: 固定类型必须提供 mode 和 value
|
||||||
|
|
||||||
|
- **WHEN** 一次性佣金类型为 "fixed"
|
||||||
|
- **AND** one_time_commission_mode 或 one_time_commission_value 为空
|
||||||
|
- **THEN** 系统返回错误:一次性佣金模式/金额缺失(错误码 40102/40103)
|
||||||
|
|
||||||
|
#### Scenario: 梯度类型必须提供 tiers
|
||||||
|
|
||||||
|
- **WHEN** 一次性佣金类型为 "tiered"
|
||||||
|
- **AND** tiers 配置为空或 null
|
||||||
|
- **THEN** 系统返回错误:梯度佣金档位配置缺失(错误码 40104)
|
||||||
|
|
||||||
|
#### Scenario: 梯度档位配置校验
|
||||||
|
|
||||||
|
- **WHEN** 提供的梯度档位缺少必填字段(threshold_value、commission_mode、commission_value)
|
||||||
|
- **THEN** 系统返回错误:梯度佣金档位配置无效(错误码 40105)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 一次性佣金修复 - 实现任务
|
||||||
|
|
||||||
|
## 1. 配置落库(ShopSeriesAllocation)
|
||||||
|
|
||||||
|
- [x] 1.1 更新 `internal/service/shop_series_allocation/service.go`:在创建分配时处理 `EnableOneTimeCommission/OneTimeCommissionConfig` 并落库
|
||||||
|
- [x] 1.2 更新 `internal/service/shop_series_allocation/service.go`:在更新分配时支持更新一次性佣金配置并落库
|
||||||
|
- [x] 1.3 梯度配置:使用 `ShopSeriesOneTimeCommissionTierStore` 在创建/更新时写入 tiers(更新时先清理再重建)
|
||||||
|
- [x] 1.4 参数校验:启用一次性佣金时必须提供配置;fixed 必须有 mode/value;tiered 必须有 tiers
|
||||||
|
|
||||||
|
## 2. 累计触发逻辑修复
|
||||||
|
|
||||||
|
- [x] 2.1 更新 `internal/service/commission_calculation/service.go`:累计触发场景每次支付成功都写回累计金额
|
||||||
|
- [x] 2.2 达到阈值时仅发放一次,发放后标记 `FirstCommissionPaid=true`
|
||||||
|
|
||||||
|
## 3. 测试修复与补充
|
||||||
|
|
||||||
|
- [x] 3.1 修复 `tests/unit/my_commission_service_test.go`:将 `CommissionType` 调整为 `CommissionSource`
|
||||||
|
- [x] 3.2 新增测试:一次性佣金配置落库(含 tiered tiers 落库)
|
||||||
|
- [x] 3.3 新增测试:累计触发多次支付后达到阈值触发一次性佣金且不重复
|
||||||
|
- [x] 3.4 确保 Service 层测试覆盖率 ≥ 90%(核心业务逻辑)
|
||||||
|
- [x] 3.5 新增集成测试:完整的配置→支付→分佣发放流程(端到端验证)
|
||||||
|
|
||||||
|
## 4. 文档更新
|
||||||
|
|
||||||
|
- [x] 4.1 更新 API 文档:`ShopSeriesAllocation` 创建/更新接口的一次性佣金参数说明
|
||||||
|
- [x] 4.2 更新业务流程文档:累计触发逻辑流程图(如适用)
|
||||||
|
- [x] 4.3 同步更新 `docs/` 目录下的相关说明(如有专门的分佣文档)
|
||||||
|
|
||||||
|
## 5. 验证
|
||||||
|
|
||||||
|
- [x] 5.1 运行 `go test ./...` 确保通过
|
||||||
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# 一次性佣金修复 - 设计
|
|
||||||
|
|
||||||
## 目标
|
|
||||||
|
|
||||||
1. 通过 `ShopSeriesAllocation` 创建/更新接口即可配置一次性佣金并生效(你确认 B=1)。
|
|
||||||
2. “累计充值触发”场景每次支付成功都累加保存,达到阈值触发一次性佣金发放。
|
|
||||||
3. 发放具备幂等:同一资源(卡/设备)只发放一次。
|
|
||||||
|
|
||||||
## 1) 配置落库
|
|
||||||
|
|
||||||
### 固定类型(fixed)
|
|
||||||
|
|
||||||
在 `tb_shop_series_allocation` 写入:
|
|
||||||
- enable_one_time_commission
|
|
||||||
- one_time_commission_type = fixed
|
|
||||||
- one_time_commission_trigger
|
|
||||||
- one_time_commission_threshold
|
|
||||||
- one_time_commission_mode(fixed/percent)
|
|
||||||
- one_time_commission_value
|
|
||||||
|
|
||||||
### 梯度类型(tiered)
|
|
||||||
|
|
||||||
在 `tb_shop_series_allocation` 写入:
|
|
||||||
- enable_one_time_commission
|
|
||||||
- one_time_commission_type = tiered
|
|
||||||
- one_time_commission_trigger
|
|
||||||
- one_time_commission_threshold
|
|
||||||
|
|
||||||
并在 `tb_shop_series_one_time_commission_tier` 维护档位:
|
|
||||||
- allocation_id
|
|
||||||
- tier_type(sales_count/sales_amount)
|
|
||||||
- threshold_value
|
|
||||||
- commission_mode(fixed/percent)
|
|
||||||
- commission_value
|
|
||||||
|
|
||||||
更新策略建议:
|
|
||||||
- 更新配置时:先删除 allocation_id 对应的旧 tiers,再批量插入新 tiers(实现简单且可控)
|
|
||||||
|
|
||||||
## 2) 累计触发逻辑
|
|
||||||
|
|
||||||
针对 `accumulated_recharge`:
|
|
||||||
- 每次支付成功都更新 `AccumulatedRecharge += orderAmount`
|
|
||||||
- 若累计达到阈值且未发放过:
|
|
||||||
- 计算佣金金额
|
|
||||||
- 创建佣金记录并入账
|
|
||||||
- 标记 `FirstCommissionPaid = true`
|
|
||||||
|
|
||||||
注意:累计的更新应当以“支付成功”为准,避免未支付订单污染累计值。
|
|
||||||
|
|
||||||
## 3) 测试
|
|
||||||
|
|
||||||
- 配置落库测试:创建/更新分配后,查询数据库字段与 tiers 表是否一致
|
|
||||||
- 累计触发测试:模拟多次支付累计到阈值,验证只发放一次且累计值递增
|
|
||||||
- 修复现有单测字段不匹配导致的编译失败
|
|
||||||
|
|
||||||
## 验收标准
|
|
||||||
|
|
||||||
- 创建/更新 `ShopSeriesAllocation` 时,一次性佣金配置能正确落库并在查询响应中返回。
|
|
||||||
- 累计触发场景下,多次支付能累加并在达到阈值时发放一次性佣金;之后不重复发放。
|
|
||||||
- `go test ./...` 通过。
|
|
||||||
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# 一次性佣金修复 - 实现任务
|
|
||||||
|
|
||||||
## 1. 配置落库(ShopSeriesAllocation)
|
|
||||||
|
|
||||||
- [ ] 1.1 更新 `internal/service/shop_series_allocation/service.go`:在创建分配时处理 `EnableOneTimeCommission/OneTimeCommissionConfig` 并落库
|
|
||||||
- [ ] 1.2 更新 `internal/service/shop_series_allocation/service.go`:在更新分配时支持更新一次性佣金配置并落库
|
|
||||||
- [ ] 1.3 梯度配置:使用 `ShopSeriesOneTimeCommissionTierStore` 在创建/更新时写入 tiers(更新时先清理再重建)
|
|
||||||
- [ ] 1.4 参数校验:启用一次性佣金时必须提供配置;fixed 必须有 mode/value;tiered 必须有 tiers
|
|
||||||
|
|
||||||
## 2. 累计触发逻辑修复
|
|
||||||
|
|
||||||
- [ ] 2.1 更新 `internal/service/commission_calculation/service.go`:累计触发场景每次支付成功都写回累计金额
|
|
||||||
- [ ] 2.2 达到阈值时仅发放一次,发放后标记 `FirstCommissionPaid=true`
|
|
||||||
|
|
||||||
## 3. 测试修复与补充
|
|
||||||
|
|
||||||
- [ ] 3.1 修复 `tests/unit/my_commission_service_test.go`:将 `CommissionType` 调整为 `CommissionSource`
|
|
||||||
- [ ] 3.2 新增测试:一次性佣金配置落库(含 tiered tiers 落库)
|
|
||||||
- [ ] 3.3 新增测试:累计触发多次支付后达到阈值触发一次性佣金且不重复
|
|
||||||
|
|
||||||
## 4. 验证
|
|
||||||
|
|
||||||
- [ ] 4.1 运行 `go test ./...` 确保通过
|
|
||||||
|
|
||||||
@@ -50,13 +50,37 @@
|
|||||||
|
|
||||||
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额。
|
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额。
|
||||||
|
|
||||||
|
**关键修复**:每次支付成功都必须写回累计充值金额,确保累计值能正确用于一次性佣金的累计触发判断。
|
||||||
|
|
||||||
#### Scenario: 单卡订单更新累计充值
|
#### Scenario: 单卡订单更新累计充值
|
||||||
|
|
||||||
- **WHEN** 单卡订单支付成功,金额 100 元
|
- **WHEN** 单卡订单支付成功,金额 100 元
|
||||||
- **THEN** IotCard.accumulated_recharge 增加 10000 分
|
- **THEN** 系统读取 IotCard.accumulated_recharge 当前值
|
||||||
|
- **AND** 增加 10000 分(100 元 = 10000 分)
|
||||||
|
- **AND** 将新值写回 IotCard.accumulated_recharge
|
||||||
|
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||||
|
|
||||||
#### Scenario: 设备订单更新累计充值
|
#### Scenario: 设备订单更新累计充值
|
||||||
|
|
||||||
- **WHEN** 设备订单支付成功,金额 300 元
|
- **WHEN** 设备订单支付成功,金额 300 元
|
||||||
- **THEN** Device.accumulated_recharge 增加 30000 分
|
- **THEN** 系统读取 Device.accumulated_recharge 当前值
|
||||||
|
- **AND** 增加 30000 分(300 元 = 30000 分)
|
||||||
|
- **AND** 将新值写回 Device.accumulated_recharge
|
||||||
|
- **AND** 使用更新后的累计值判断是否触发一次性佣金
|
||||||
|
|
||||||
|
#### Scenario: 累计充值更新使用原子操作
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值金额
|
||||||
|
- **THEN** 系统使用 SQL 原子操作(如 `accumulated_recharge = accumulated_recharge + ?`)
|
||||||
|
- **OR** 使用 GORM 乐观锁(version 字段)
|
||||||
|
- **AND** 确保并发场景下累计值不会丢失
|
||||||
|
|
||||||
|
#### Scenario: 更新失败不影响佣金计算
|
||||||
|
|
||||||
|
- **WHEN** 累计充值金额更新失败(数据库错误、并发冲突等)
|
||||||
|
- **THEN** 系统记录错误日志
|
||||||
|
- **AND** 继续执行后续的佣金计算流程(成本价差、一次性佣金等)
|
||||||
|
- **AND** 不因累计值更新失败而导致整个佣金计算失败
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -71,3 +95,44 @@
|
|||||||
#### Scenario: 佣金来源类型
|
#### Scenario: 佣金来源类型
|
||||||
- **WHEN** 创建佣金记录
|
- **WHEN** 创建佣金记录
|
||||||
- **THEN** commission_source 为以下之一:cost_diff(成本价差)、one_time(一次性佣金)、tier_bonus(梯度奖励)
|
- **THEN** commission_source 为以下之一:cost_diff(成本价差)、one_time(一次性佣金)、tier_bonus(梯度奖励)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 一次性佣金触发检查
|
||||||
|
|
||||||
|
系统 SHALL 在更新累计充值金额后立即检查是否触发一次性佣金。
|
||||||
|
|
||||||
|
#### Scenario: 累计达到阈值触发佣金
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值后,累计值 ≥ 配置阈值
|
||||||
|
- **AND** 卡/设备的 first_commission_paid = false
|
||||||
|
- **THEN** 系统发放一次性佣金
|
||||||
|
- **AND** 标记 first_commission_paid = true
|
||||||
|
|
||||||
|
#### Scenario: 累计未达到阈值不触发
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值后,累计值 < 配置阈值
|
||||||
|
- **THEN** 系统不发放一次性佣金
|
||||||
|
- **AND** first_commission_paid 保持不变
|
||||||
|
|
||||||
|
#### Scenario: 已发放过不重复触发
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值后,累计值 ≥ 配置阈值
|
||||||
|
- **AND** 卡/设备的 first_commission_paid = true
|
||||||
|
- **THEN** 系统不重复发放一次性佣金
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 累计充值更新日志记录
|
||||||
|
|
||||||
|
系统 SHOULD 记录累计充值金额的更新操作,便于问题排查。
|
||||||
|
|
||||||
|
#### Scenario: 记录更新前后的累计值
|
||||||
|
|
||||||
|
- **WHEN** 更新累计充值金额
|
||||||
|
- **THEN** 系统在日志中记录:订单 ID、资源类型(卡/设备)、资源 ID、更新前累计值、本次充值金额、更新后累计值
|
||||||
|
|
||||||
|
#### Scenario: 记录更新失败原因
|
||||||
|
|
||||||
|
- **WHEN** 累计充值金额更新失败
|
||||||
|
- **THEN** 系统在日志中记录:订单 ID、资源 ID、失败原因(错误信息)、重试次数(如适用)
|
||||||
|
|||||||
@@ -22,13 +22,28 @@
|
|||||||
|
|
||||||
系统 SHALL 支持"累计充值"触发条件:当卡/设备的累计充值金额 ≥ 配置阈值时触发一次性佣金。
|
系统 SHALL 支持"累计充值"触发条件:当卡/设备的累计充值金额 ≥ 配置阈值时触发一次性佣金。
|
||||||
|
|
||||||
|
**关键修复**:每次支付成功后必须更新累计充值金额,确保累计值能正确递增并达到阈值。
|
||||||
|
|
||||||
#### Scenario: 累计达到阈值
|
#### Scenario: 累计达到阈值
|
||||||
|
|
||||||
- **WHEN** 卡之前累计充值 200 元,本次充值 150 元,配置阈值 300 元
|
- **WHEN** 卡之前累计充值 200 元,本次充值 150 元,配置阈值 300 元
|
||||||
- **THEN** 累计 350 元 ≥ 300 元,系统发放一次性佣金
|
- **THEN** 系统更新累计充值为 350 元
|
||||||
|
- **AND** 累计 350 元 ≥ 300 元,系统发放一次性佣金
|
||||||
|
- **AND** 标记 first_commission_paid = true
|
||||||
|
|
||||||
#### Scenario: 累计未达到阈值
|
#### Scenario: 累计未达到阈值
|
||||||
|
|
||||||
- **WHEN** 卡之前累计充值 100 元,本次充值 100 元,配置阈值 300 元
|
- **WHEN** 卡之前累计充值 100 元,本次充值 100 元,配置阈值 300 元
|
||||||
- **THEN** 累计 200 元 < 300 元,系统不发放一次性佣金
|
- **THEN** 系统更新累计充值为 200 元
|
||||||
|
- **AND** 累计 200 元 < 300 元,系统不发放一次性佣金
|
||||||
|
|
||||||
|
#### Scenario: 每次支付都更新累计值
|
||||||
|
|
||||||
|
- **WHEN** 卡累计充值为 50 元
|
||||||
|
- **AND** 连续发生 3 次充值:100 元、150 元、80 元
|
||||||
|
- **THEN** 第 1 次充值后累计 150 元
|
||||||
|
- **AND** 第 2 次充值后累计 300 元
|
||||||
|
- **AND** 第 3 次充值后累计 380 元
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -50,12 +65,39 @@
|
|||||||
|
|
||||||
一次性佣金的触发条件和金额 SHALL 从 ShopSeriesAllocation 配置获取。
|
一次性佣金的触发条件和金额 SHALL 从 ShopSeriesAllocation 配置获取。
|
||||||
|
|
||||||
|
**关键修复**:配置必须能够通过 ShopSeriesAllocation 创建/更新接口正确落库并生效。
|
||||||
|
|
||||||
#### Scenario: 获取触发条件和金额
|
#### Scenario: 获取触发条件和金额
|
||||||
|
|
||||||
- **WHEN** 触发一次性佣金检查
|
- **WHEN** 触发一次性佣金检查
|
||||||
- **THEN** 系统从卡关联的 ShopSeriesAllocation 获取 one_time_commission_trigger(触发类型)、one_time_commission_threshold(阈值)、one_time_commission_amount(金额)
|
- **THEN** 系统从卡关联的 ShopSeriesAllocation 获取 one_time_commission_trigger(触发类型)、one_time_commission_threshold(阈值)、one_time_commission_mode(模式)、one_time_commission_value(金额/比例)
|
||||||
|
|
||||||
|
#### Scenario: 固定类型配置
|
||||||
|
|
||||||
|
- **WHEN** 创建 ShopSeriesAllocation 时设置 one_time_commission_type = "fixed"
|
||||||
|
- **AND** 设置 one_time_commission_mode = "fixed",one_time_commission_value = 5000(50 元)
|
||||||
|
- **THEN** 系统将配置正确写入数据库
|
||||||
|
- **AND** 查询该配置时能正确返回所有一次性佣金字段
|
||||||
|
|
||||||
|
#### Scenario: 梯度类型配置
|
||||||
|
|
||||||
|
- **WHEN** 创建 ShopSeriesAllocation 时设置 one_time_commission_type = "tiered"
|
||||||
|
- **AND** 提供梯度档位配置:[{threshold: 100, mode: "fixed", value: 2000}, {threshold: 300, mode: "percent", value: 10}]
|
||||||
|
- **THEN** 系统将主配置写入 tb_shop_series_allocation
|
||||||
|
- **AND** 将档位配置写入 tb_shop_series_one_time_commission_tier
|
||||||
|
- **AND** 查询该配置时能正确返回主配置和关联的档位列表
|
||||||
|
|
||||||
|
#### Scenario: 更新梯度配置
|
||||||
|
|
||||||
|
- **WHEN** 更新 ShopSeriesAllocation 的梯度配置
|
||||||
|
- **AND** 新档位配置与旧配置不同
|
||||||
|
- **THEN** 系统先删除旧档位数据(WHERE allocation_id = ?)
|
||||||
|
- **AND** 再批量插入新档位数据
|
||||||
|
- **AND** 查询时返回最新的档位配置
|
||||||
|
|
||||||
#### Scenario: 无一次性佣金配置
|
#### Scenario: 无一次性佣金配置
|
||||||
- **WHEN** 卡关联的系列分配未配置一次性佣金(one_time_commission_amount = 0)
|
|
||||||
|
- **WHEN** 卡关联的系列分配未启用一次性佣金(enable_one_time_commission = false)
|
||||||
- **THEN** 不发放一次性佣金
|
- **THEN** 不发放一次性佣金
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -67,3 +109,32 @@
|
|||||||
#### Scenario: 发放给归属店铺
|
#### Scenario: 发放给归属店铺
|
||||||
- **WHEN** 卡归属店铺 A,触发一次性佣金
|
- **WHEN** 卡归属店铺 A,触发一次性佣金
|
||||||
- **THEN** 佣金入账到店铺 A 的钱包
|
- **THEN** 佣金入账到店铺 A 的钱包
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Requirement: 配置参数校验
|
||||||
|
|
||||||
|
系统 MUST 在创建/更新 ShopSeriesAllocation 时校验一次性佣金配置的完整性。
|
||||||
|
|
||||||
|
#### Scenario: 启用一次性佣金必须提供配置
|
||||||
|
|
||||||
|
- **WHEN** 创建 ShopSeriesAllocation 时设置 enable_one_time_commission = true
|
||||||
|
- **AND** 未提供 one_time_commission_config
|
||||||
|
- **THEN** 系统返回错误:一次性佣金配置无效(错误码 40101)
|
||||||
|
|
||||||
|
#### Scenario: 固定类型必须提供 mode 和 value
|
||||||
|
|
||||||
|
- **WHEN** 一次性佣金类型为 "fixed"
|
||||||
|
- **AND** one_time_commission_mode 或 one_time_commission_value 为空
|
||||||
|
- **THEN** 系统返回错误:一次性佣金模式/金额缺失(错误码 40102/40103)
|
||||||
|
|
||||||
|
#### Scenario: 梯度类型必须提供 tiers
|
||||||
|
|
||||||
|
- **WHEN** 一次性佣金类型为 "tiered"
|
||||||
|
- **AND** tiers 配置为空或 null
|
||||||
|
- **THEN** 系统返回错误:梯度佣金档位配置缺失(错误码 40104)
|
||||||
|
|
||||||
|
#### Scenario: 梯度档位配置校验
|
||||||
|
|
||||||
|
- **WHEN** 提供的梯度档位缺少必填字段(threshold_value、commission_mode、commission_value)
|
||||||
|
- **THEN** 系统返回错误:梯度佣金档位配置无效(错误码 40105)
|
||||||
|
|||||||
@@ -354,6 +354,165 @@ func TestShopSeriesAllocationAPI_UpdateStatus(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 一次性佣金配置测试 ====================
|
||||||
|
|
||||||
|
func TestShopSeriesAllocationAPI_OneTimeCommission(t *testing.T) {
|
||||||
|
env := integ.NewIntegrationTestEnv(t)
|
||||||
|
|
||||||
|
t.Run("创建分配-固定类型一次性佣金配置落库", func(t *testing.T) {
|
||||||
|
shop := env.CreateTestShop("一次性佣金测试店铺1", 1, nil)
|
||||||
|
series := createTestPackageSeries(t, env, "一次性佣金测试系列1")
|
||||||
|
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"shop_id": shop.ID,
|
||||||
|
"series_id": series.ID,
|
||||||
|
"base_commission": map[string]interface{}{
|
||||||
|
"mode": "fixed",
|
||||||
|
"value": 1000,
|
||||||
|
},
|
||||||
|
"enable_one_time_commission": true,
|
||||||
|
"one_time_commission_config": map[string]interface{}{
|
||||||
|
"type": "fixed",
|
||||||
|
"trigger": "accumulated_recharge",
|
||||||
|
"threshold": 10000,
|
||||||
|
"mode": "fixed",
|
||||||
|
"value": 500,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jsonBody, _ := json.Marshal(body)
|
||||||
|
|
||||||
|
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result response.Response
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
|
||||||
|
|
||||||
|
dataMap := result.Data.(map[string]interface{})
|
||||||
|
assert.Equal(t, true, dataMap["enable_one_time_commission"])
|
||||||
|
if cfg, ok := dataMap["one_time_commission_config"].(map[string]interface{}); ok {
|
||||||
|
assert.Equal(t, "fixed", cfg["type"])
|
||||||
|
assert.Equal(t, "accumulated_recharge", cfg["trigger"])
|
||||||
|
assert.Equal(t, float64(10000), cfg["threshold"])
|
||||||
|
assert.Equal(t, "fixed", cfg["mode"])
|
||||||
|
assert.Equal(t, float64(500), cfg["value"])
|
||||||
|
} else {
|
||||||
|
t.Error("一次性佣金配置应返回")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("创建分配-梯度类型一次性佣金配置落库", func(t *testing.T) {
|
||||||
|
shop := env.CreateTestShop("一次性佣金测试店铺2", 1, nil)
|
||||||
|
series := createTestPackageSeries(t, env, "一次性佣金测试系列2")
|
||||||
|
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"shop_id": shop.ID,
|
||||||
|
"series_id": series.ID,
|
||||||
|
"base_commission": map[string]interface{}{
|
||||||
|
"mode": "fixed",
|
||||||
|
"value": 1000,
|
||||||
|
},
|
||||||
|
"enable_one_time_commission": true,
|
||||||
|
"one_time_commission_config": map[string]interface{}{
|
||||||
|
"type": "tiered",
|
||||||
|
"trigger": "single_recharge",
|
||||||
|
"threshold": 5000,
|
||||||
|
"tiers": []map[string]interface{}{
|
||||||
|
{"tier_type": "sales_count", "threshold": 10, "mode": "fixed", "value": 100},
|
||||||
|
{"tier_type": "sales_count", "threshold": 50, "mode": "fixed", "value": 500},
|
||||||
|
{"tier_type": "sales_amount", "threshold": 100000, "mode": "percent", "value": 50},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jsonBody, _ := json.Marshal(body)
|
||||||
|
|
||||||
|
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result response.Response
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, result.Code, "应返回成功: %s", result.Message)
|
||||||
|
|
||||||
|
dataMap := result.Data.(map[string]interface{})
|
||||||
|
assert.Equal(t, true, dataMap["enable_one_time_commission"])
|
||||||
|
if cfg, ok := dataMap["one_time_commission_config"].(map[string]interface{}); ok {
|
||||||
|
assert.Equal(t, "tiered", cfg["type"])
|
||||||
|
assert.Equal(t, "single_recharge", cfg["trigger"])
|
||||||
|
if tiers, ok := cfg["tiers"].([]interface{}); ok {
|
||||||
|
assert.Equal(t, 3, len(tiers), "应有3个梯度档位")
|
||||||
|
} else {
|
||||||
|
t.Error("梯度档位应返回")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("创建分配-启用一次性佣金但未提供配置应失败", func(t *testing.T) {
|
||||||
|
shop := env.CreateTestShop("一次性佣金测试店铺3", 1, nil)
|
||||||
|
series := createTestPackageSeries(t, env, "一次性佣金测试系列3")
|
||||||
|
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"shop_id": shop.ID,
|
||||||
|
"series_id": series.ID,
|
||||||
|
"base_commission": map[string]interface{}{
|
||||||
|
"mode": "fixed",
|
||||||
|
"value": 1000,
|
||||||
|
},
|
||||||
|
"enable_one_time_commission": true,
|
||||||
|
}
|
||||||
|
jsonBody, _ := json.Marshal(body)
|
||||||
|
|
||||||
|
resp, err := env.AsSuperAdmin().Request("POST", "/api/admin/shop-series-allocations", jsonBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result response.Response
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.NotEqual(t, 0, result.Code, "启用一次性佣金但未提供配置应失败")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("更新分配-更新一次性佣金配置", func(t *testing.T) {
|
||||||
|
shop := env.CreateTestShop("一次性佣金测试店铺4", 1, nil)
|
||||||
|
series := createTestPackageSeries(t, env, "一次性佣金测试系列4")
|
||||||
|
allocation := createTestAllocation(t, env, shop.ID, series.ID, 0)
|
||||||
|
|
||||||
|
enableOneTime := true
|
||||||
|
body := map[string]interface{}{
|
||||||
|
"enable_one_time_commission": &enableOneTime,
|
||||||
|
"one_time_commission_config": map[string]interface{}{
|
||||||
|
"type": "fixed",
|
||||||
|
"trigger": "accumulated_recharge",
|
||||||
|
"threshold": 20000,
|
||||||
|
"mode": "percent",
|
||||||
|
"value": 100,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
jsonBody, _ := json.Marshal(body)
|
||||||
|
|
||||||
|
url := fmt.Sprintf("/api/admin/shop-series-allocations/%d", allocation.ID)
|
||||||
|
resp, err := env.AsSuperAdmin().Request("PUT", url, jsonBody)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
var result response.Response
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, 0, result.Code, "更新应成功: %s", result.Message)
|
||||||
|
|
||||||
|
dataMap := result.Data.(map[string]interface{})
|
||||||
|
assert.Equal(t, true, dataMap["enable_one_time_commission"])
|
||||||
|
if cfg, ok := dataMap["one_time_commission_config"].(map[string]interface{}); ok {
|
||||||
|
assert.Equal(t, float64(20000), cfg["threshold"])
|
||||||
|
assert.Equal(t, "percent", cfg["mode"])
|
||||||
|
assert.Equal(t, float64(100), cfg["value"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// ==================== 梯度佣金 API 测试 ====================
|
// ==================== 梯度佣金 API 测试 ====================
|
||||||
|
|
||||||
// ==================== 权限测试 ====================
|
// ==================== 权限测试 ====================
|
||||||
|
|||||||
410
tests/unit/commission_calculation_service_test.go
Normal file
410
tests/unit/commission_calculation_service_test.go
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
package unit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/service/commission_calculation"
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/service/commission_stats"
|
||||||
|
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
|
||||||
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||||
|
"github.com/break/junhong_cmp_fiber/tests/testutils"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommissionCalculation_AccumulatedRecharge(t *testing.T) {
|
||||||
|
tx := testutils.NewTestTransaction(t)
|
||||||
|
rdb := testutils.GetTestRedis(t)
|
||||||
|
testutils.CleanTestRedisKeys(t, rdb)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
t.Run("累计充值触发-每次支付都写回累计金额", func(t *testing.T) {
|
||||||
|
shop := &model.Shop{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
ShopName: "测试店铺",
|
||||||
|
ShopCode: fmt.Sprintf("SHOP_%d", time.Now().UnixNano()),
|
||||||
|
Level: 1,
|
||||||
|
Status: constants.StatusEnabled,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(shop).Error)
|
||||||
|
|
||||||
|
series := &model.PackageSeries{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
SeriesCode: fmt.Sprintf("SERIES_%d", time.Now().UnixNano()),
|
||||||
|
SeriesName: "测试系列",
|
||||||
|
Status: constants.StatusEnabled,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(series).Error)
|
||||||
|
|
||||||
|
allocation := &model.ShopSeriesAllocation{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
ShopID: shop.ID,
|
||||||
|
SeriesID: series.ID,
|
||||||
|
AllocatorShopID: 0,
|
||||||
|
BaseCommissionMode: model.CommissionModeFixed,
|
||||||
|
BaseCommissionValue: 1000,
|
||||||
|
EnableOneTimeCommission: true,
|
||||||
|
OneTimeCommissionType: model.OneTimeCommissionTypeFixed,
|
||||||
|
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerAccumulatedRecharge,
|
||||||
|
OneTimeCommissionThreshold: 10000,
|
||||||
|
OneTimeCommissionMode: model.CommissionModeFixed,
|
||||||
|
OneTimeCommissionValue: 500,
|
||||||
|
Status: constants.StatusEnabled,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(allocation).Error)
|
||||||
|
|
||||||
|
card := &model.IotCard{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
ICCID: fmt.Sprintf("898600%013d", time.Now().Unix()%10000000000000),
|
||||||
|
CardType: "4G",
|
||||||
|
CardCategory: "normal",
|
||||||
|
CarrierID: 1,
|
||||||
|
CarrierType: "CMCC",
|
||||||
|
CarrierName: "中国移动",
|
||||||
|
Status: 3,
|
||||||
|
ShopID: &shop.ID,
|
||||||
|
SeriesAllocationID: &allocation.ID,
|
||||||
|
FirstCommissionPaid: false,
|
||||||
|
AccumulatedRecharge: 0,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(card).Error)
|
||||||
|
|
||||||
|
wallet := &model.Wallet{
|
||||||
|
ResourceType: "shop",
|
||||||
|
ResourceID: shop.ID,
|
||||||
|
WalletType: "commission",
|
||||||
|
Balance: 0,
|
||||||
|
Version: 0,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(wallet).Error)
|
||||||
|
|
||||||
|
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||||
|
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||||
|
|
||||||
|
t.Run("第一次支付-累计金额更新为3000", func(t *testing.T) {
|
||||||
|
order1 := &model.Order{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
OrderNo: fmt.Sprintf("ORDER_%d_1", time.Now().UnixNano()),
|
||||||
|
OrderType: model.OrderTypeSingleCard,
|
||||||
|
BuyerType: model.BuyerTypeAgent,
|
||||||
|
BuyerID: shop.ID,
|
||||||
|
IotCardID: &card.ID,
|
||||||
|
SellerShopID: &shop.ID,
|
||||||
|
SeriesID: &series.ID,
|
||||||
|
TotalAmount: 3000,
|
||||||
|
SellerCostPrice: 2000,
|
||||||
|
PaymentStatus: model.PaymentStatusPaid,
|
||||||
|
CommissionStatus: model.CommissionStatusPending,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(order1).Error)
|
||||||
|
|
||||||
|
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), cardBefore.AccumulatedRecharge)
|
||||||
|
|
||||||
|
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||||
|
newAccumulated := cardBefore.AccumulatedRecharge + order1.TotalAmount
|
||||||
|
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
|
||||||
|
Update("accumulated_recharge", newAccumulated).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(3000), cardAfter.AccumulatedRecharge, "第一次支付后累计金额应为3000")
|
||||||
|
assert.False(t, cardAfter.FirstCommissionPaid, "未达阈值不应标记为已发放")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("第二次支付-累计金额更新为7000", func(t *testing.T) {
|
||||||
|
order2 := &model.Order{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
OrderNo: fmt.Sprintf("ORDER_%d_2", time.Now().UnixNano()),
|
||||||
|
OrderType: model.OrderTypeSingleCard,
|
||||||
|
BuyerType: model.BuyerTypeAgent,
|
||||||
|
BuyerID: shop.ID,
|
||||||
|
IotCardID: &card.ID,
|
||||||
|
SellerShopID: &shop.ID,
|
||||||
|
SeriesID: &series.ID,
|
||||||
|
TotalAmount: 4000,
|
||||||
|
SellerCostPrice: 3000,
|
||||||
|
PaymentStatus: model.PaymentStatusPaid,
|
||||||
|
CommissionStatus: model.CommissionStatusPending,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(order2).Error)
|
||||||
|
|
||||||
|
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(3000), cardBefore.AccumulatedRecharge)
|
||||||
|
|
||||||
|
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||||
|
newAccumulated := cardBefore.AccumulatedRecharge + order2.TotalAmount
|
||||||
|
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
|
||||||
|
Update("accumulated_recharge", newAccumulated).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(7000), cardAfter.AccumulatedRecharge, "第二次支付后累计金额应为7000")
|
||||||
|
assert.False(t, cardAfter.FirstCommissionPaid, "仍未达阈值不应标记为已发放")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("第三次支付-累计金额更新为11000且达到阈值", func(t *testing.T) {
|
||||||
|
order3 := &model.Order{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
OrderNo: fmt.Sprintf("ORDER_%d_3", time.Now().UnixNano()),
|
||||||
|
OrderType: model.OrderTypeSingleCard,
|
||||||
|
BuyerType: model.BuyerTypeAgent,
|
||||||
|
BuyerID: shop.ID,
|
||||||
|
IotCardID: &card.ID,
|
||||||
|
SellerShopID: &shop.ID,
|
||||||
|
SeriesID: &series.ID,
|
||||||
|
TotalAmount: 4000,
|
||||||
|
SellerCostPrice: 3000,
|
||||||
|
PaymentStatus: model.PaymentStatusPaid,
|
||||||
|
CommissionStatus: model.CommissionStatusPending,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(order3).Error)
|
||||||
|
|
||||||
|
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(7000), cardBefore.AccumulatedRecharge)
|
||||||
|
|
||||||
|
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||||
|
newAccumulated := cardBefore.AccumulatedRecharge + order3.TotalAmount
|
||||||
|
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
|
||||||
|
Update("accumulated_recharge", newAccumulated).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(11000), cardAfter.AccumulatedRecharge, "第三次支付后累计金额应为11000")
|
||||||
|
|
||||||
|
if cardAfter.AccumulatedRecharge >= alloc.OneTimeCommissionThreshold && !cardAfter.FirstCommissionPaid {
|
||||||
|
assert.GreaterOrEqual(t, cardAfter.AccumulatedRecharge, alloc.OneTimeCommissionThreshold, "累计金额已达阈值")
|
||||||
|
|
||||||
|
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
|
||||||
|
Update("first_commission_paid", true).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cardFinal, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, cardFinal.FirstCommissionPaid, "达到阈值应标记为已发放")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("第四次支付-累计金额继续更新但不重复发放", func(t *testing.T) {
|
||||||
|
order4 := &model.Order{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
OrderNo: fmt.Sprintf("ORDER_%d_4", time.Now().UnixNano()),
|
||||||
|
OrderType: model.OrderTypeSingleCard,
|
||||||
|
BuyerType: model.BuyerTypeAgent,
|
||||||
|
BuyerID: shop.ID,
|
||||||
|
IotCardID: &card.ID,
|
||||||
|
SellerShopID: &shop.ID,
|
||||||
|
SeriesID: &series.ID,
|
||||||
|
TotalAmount: 3000,
|
||||||
|
SellerCostPrice: 2000,
|
||||||
|
PaymentStatus: model.PaymentStatusPaid,
|
||||||
|
CommissionStatus: model.CommissionStatusPending,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(order4).Error)
|
||||||
|
|
||||||
|
cardBefore, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(11000), cardBefore.AccumulatedRecharge)
|
||||||
|
assert.True(t, cardBefore.FirstCommissionPaid, "标记应保持为true")
|
||||||
|
|
||||||
|
alloc, err := shopSeriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
if alloc.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||||
|
newAccumulated := cardBefore.AccumulatedRecharge + order4.TotalAmount
|
||||||
|
err := tx.Model(&model.IotCard{}).Where("id = ?", card.ID).
|
||||||
|
Update("accumulated_recharge", newAccumulated).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(14000), cardAfter.AccumulatedRecharge, "第四次支付后累计金额应为14000")
|
||||||
|
assert.True(t, cardAfter.FirstCommissionPaid, "已发放标记不应改变")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommissionCalculation_OneTimeCommissionLogic(t *testing.T) {
|
||||||
|
tx := testutils.NewTestTransaction(t)
|
||||||
|
rdb := testutils.GetTestRedis(t)
|
||||||
|
testutils.CleanTestRedisKeys(t, rdb)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
commissionRecordStore := postgres.NewCommissionRecordStore(tx, rdb)
|
||||||
|
shopStore := postgres.NewShopStore(tx, rdb)
|
||||||
|
shopSeriesAllocationStore := postgres.NewShopSeriesAllocationStore(tx)
|
||||||
|
shopSeriesOneTimeCommissionTierStore := postgres.NewShopSeriesOneTimeCommissionTierStore(tx)
|
||||||
|
iotCardStore := postgres.NewIotCardStore(tx, rdb)
|
||||||
|
deviceStore := postgres.NewDeviceStore(tx, rdb)
|
||||||
|
walletStore := postgres.NewWalletStore(tx, rdb)
|
||||||
|
walletTransactionStore := postgres.NewWalletTransactionStore(tx, rdb)
|
||||||
|
orderStore := postgres.NewOrderStore(tx, rdb)
|
||||||
|
orderItemStore := postgres.NewOrderItemStore(tx, rdb)
|
||||||
|
packageStore := postgres.NewPackageStore(tx)
|
||||||
|
shopSeriesCommissionStatsStore := postgres.NewShopSeriesCommissionStatsStore(tx)
|
||||||
|
commissionStatsService := commission_stats.New(shopSeriesCommissionStatsStore)
|
||||||
|
logger, _ := zap.NewDevelopment()
|
||||||
|
|
||||||
|
commCalcService := commission_calculation.New(
|
||||||
|
tx,
|
||||||
|
commissionRecordStore,
|
||||||
|
shopStore,
|
||||||
|
shopSeriesAllocationStore,
|
||||||
|
shopSeriesOneTimeCommissionTierStore,
|
||||||
|
iotCardStore,
|
||||||
|
deviceStore,
|
||||||
|
walletStore,
|
||||||
|
walletTransactionStore,
|
||||||
|
orderStore,
|
||||||
|
orderItemStore,
|
||||||
|
packageStore,
|
||||||
|
commissionStatsService,
|
||||||
|
logger,
|
||||||
|
)
|
||||||
|
|
||||||
|
t.Run("单次充值触发-达到阈值时发放佣金", func(t *testing.T) {
|
||||||
|
shop := &model.Shop{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
ShopName: "单次触发店铺",
|
||||||
|
ShopCode: fmt.Sprintf("SHOP_%d", time.Now().UnixNano()),
|
||||||
|
Level: 1,
|
||||||
|
Status: constants.StatusEnabled,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(shop).Error)
|
||||||
|
|
||||||
|
series := &model.PackageSeries{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
SeriesCode: fmt.Sprintf("SERIES_%d", time.Now().UnixNano()),
|
||||||
|
SeriesName: "单次触发系列",
|
||||||
|
Status: constants.StatusEnabled,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(series).Error)
|
||||||
|
|
||||||
|
allocation := &model.ShopSeriesAllocation{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
ShopID: shop.ID,
|
||||||
|
SeriesID: series.ID,
|
||||||
|
AllocatorShopID: 0,
|
||||||
|
BaseCommissionMode: model.CommissionModeFixed,
|
||||||
|
BaseCommissionValue: 500,
|
||||||
|
EnableOneTimeCommission: true,
|
||||||
|
OneTimeCommissionType: model.OneTimeCommissionTypeFixed,
|
||||||
|
OneTimeCommissionTrigger: model.OneTimeCommissionTriggerSingleRecharge,
|
||||||
|
OneTimeCommissionThreshold: 5000,
|
||||||
|
OneTimeCommissionMode: model.CommissionModeFixed,
|
||||||
|
OneTimeCommissionValue: 300,
|
||||||
|
Status: constants.StatusEnabled,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(allocation).Error)
|
||||||
|
|
||||||
|
card := &model.IotCard{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
ICCID: fmt.Sprintf("898600%013d", time.Now().Unix()%10000000000000+1),
|
||||||
|
CardType: "4G",
|
||||||
|
CardCategory: "normal",
|
||||||
|
CarrierID: 1,
|
||||||
|
CarrierType: "CMCC",
|
||||||
|
CarrierName: "中国移动",
|
||||||
|
Status: 3,
|
||||||
|
ShopID: &shop.ID,
|
||||||
|
SeriesAllocationID: &allocation.ID,
|
||||||
|
FirstCommissionPaid: false,
|
||||||
|
AccumulatedRecharge: 0,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(card).Error)
|
||||||
|
|
||||||
|
wallet := &model.Wallet{
|
||||||
|
ResourceType: "shop",
|
||||||
|
ResourceID: shop.ID,
|
||||||
|
WalletType: "commission",
|
||||||
|
Balance: 0,
|
||||||
|
Version: 0,
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(wallet).Error)
|
||||||
|
|
||||||
|
t.Run("单次充值未达阈值-不触发", func(t *testing.T) {
|
||||||
|
order := &model.Order{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
OrderNo: fmt.Sprintf("ORDER_%d", time.Now().UnixNano()),
|
||||||
|
OrderType: model.OrderTypeSingleCard,
|
||||||
|
BuyerType: model.BuyerTypeAgent,
|
||||||
|
BuyerID: shop.ID,
|
||||||
|
IotCardID: &card.ID,
|
||||||
|
SellerShopID: &shop.ID,
|
||||||
|
SeriesID: &series.ID,
|
||||||
|
TotalAmount: 3000,
|
||||||
|
SellerCostPrice: 2500,
|
||||||
|
PaymentStatus: model.PaymentStatusPaid,
|
||||||
|
CommissionStatus: model.CommissionStatusPending,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(order).Error)
|
||||||
|
|
||||||
|
err := commCalcService.CalculateCommission(ctx, order.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, cardAfter.FirstCommissionPaid, "单次充值未达阈值不应发放")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("单次充值达到阈值-触发", func(t *testing.T) {
|
||||||
|
order := &model.Order{
|
||||||
|
BaseModel: model.BaseModel{Creator: 1, Updater: 1},
|
||||||
|
OrderNo: fmt.Sprintf("ORDER_%d", time.Now().UnixNano()+1),
|
||||||
|
OrderType: model.OrderTypeSingleCard,
|
||||||
|
BuyerType: model.BuyerTypeAgent,
|
||||||
|
BuyerID: shop.ID,
|
||||||
|
IotCardID: &card.ID,
|
||||||
|
SellerShopID: &shop.ID,
|
||||||
|
SeriesID: &series.ID,
|
||||||
|
TotalAmount: 6000,
|
||||||
|
SellerCostPrice: 5500,
|
||||||
|
PaymentStatus: model.PaymentStatusPaid,
|
||||||
|
CommissionStatus: model.CommissionStatusPending,
|
||||||
|
}
|
||||||
|
require.NoError(t, tx.Create(order).Error)
|
||||||
|
|
||||||
|
err := commCalcService.CalculateCommission(ctx, order.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cardAfter, err := iotCardStore.GetByID(ctx, card.ID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, cardAfter.FirstCommissionPaid, "单次充值达到阈值应发放")
|
||||||
|
|
||||||
|
var commRecords []model.CommissionRecord
|
||||||
|
err = tx.Where("order_id = ? AND commission_source = ?", order.ID, model.CommissionSourceOneTime).
|
||||||
|
Find(&commRecords).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, commRecords, 1, "应有一条一次性佣金记录")
|
||||||
|
assert.Equal(t, int64(300), commRecords[0].Amount, "佣金金额应为300")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -159,6 +159,9 @@ func TestCommissionWithdrawalSettingService_GetCurrent(t *testing.T) {
|
|||||||
rdb := testutils.GetTestRedis(t)
|
rdb := testutils.GetTestRedis(t)
|
||||||
testutils.CleanTestRedisKeys(t, rdb)
|
testutils.CleanTestRedisKeys(t, rdb)
|
||||||
|
|
||||||
|
// 清理已有的活跃配置,确保测试隔离
|
||||||
|
tx.Exec("UPDATE tb_commission_withdrawal_setting SET is_active = false WHERE is_active = true")
|
||||||
|
|
||||||
accountStore := postgres.NewAccountStore(tx, rdb)
|
accountStore := postgres.NewAccountStore(tx, rdb)
|
||||||
settingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
settingStore := postgres.NewCommissionWithdrawalSettingStore(tx, rdb)
|
||||||
|
|
||||||
|
|||||||
@@ -610,6 +610,8 @@ func TestAuthorizationService_UpdateRecordRemark(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx := pkggorm.SkipDataPermission(context.Background())
|
ctx := pkggorm.SkipDataPermission(context.Background())
|
||||||
|
ctx = context.WithValue(ctx, constants.ContextKeyUserID, account.ID)
|
||||||
|
ctx = context.WithValue(ctx, constants.ContextKeyUserType, constants.UserTypePlatform)
|
||||||
|
|
||||||
t.Run("更新授权备注", func(t *testing.T) {
|
t.Run("更新授权备注", func(t *testing.T) {
|
||||||
updated, err := authService.UpdateRecordRemark(ctx, auth.ID, "更新后的备注")
|
updated, err := authService.UpdateRecordRemark(ctx, auth.ID, "更新后的备注")
|
||||||
|
|||||||
@@ -360,11 +360,11 @@ func TestMyCommissionService_ListMyCommissionRecords(t *testing.T) {
|
|||||||
|
|
||||||
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
ctx := createMyCommissionTestContext(1, shop.ID, constants.UserTypeAgent)
|
||||||
|
|
||||||
commissionType := "one_time"
|
commissionSource := "one_time"
|
||||||
req := &dto.MyCommissionRecordListReq{
|
req := &dto.MyCommissionRecordListReq{
|
||||||
Page: 1,
|
Page: 1,
|
||||||
PageSize: 20,
|
PageSize: 20,
|
||||||
CommissionType: &commissionType,
|
CommissionSource: &commissionSource,
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := service.ListMyCommissionRecords(ctx, req)
|
result, err := service.ListMyCommissionRecords(ctx, req)
|
||||||
|
|||||||
Reference in New Issue
Block a user