归档一次性佣金配置落库与累计触发修复,同步规范文档到主 specs
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:
2026-01-29 16:00:18 +08:00
parent d977000a66
commit 2b0f79be81
19 changed files with 1654 additions and 136 deletions

View File

@@ -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_modefixed/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_typesales_count/sales_amount
- threshold_value
- commission_modefixed/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. 写入 RedisTTL 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 ./...` 通过。

View File

@@ -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、失败原因错误信息、重试次数如适用

View File

@@ -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 = 500050 元)
- **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

View File

@@ -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/valuetiered 必须有 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 ./...` 确保通过

View File

@@ -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_modefixed/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_typesales_count/sales_amount
- threshold_value
- commission_modefixed/percent
- commission_value
更新策略建议:
- 更新配置时:先删除 allocation_id 对应的旧 tiers再批量插入新 tiers实现简单且可控
## 2) 累计触发逻辑
针对 `accumulated_recharge`
- 每次支付成功都更新 `AccumulatedRecharge += orderAmount`
- 若累计达到阈值且未发放过:
- 计算佣金金额
- 创建佣金记录并入账
- 标记 `FirstCommissionPaid = true`
注意:累计的更新应当以“支付成功”为准,避免未支付订单污染累计值。
## 3) 测试
- 配置落库测试:创建/更新分配后,查询数据库字段与 tiers 表是否一致
- 累计触发测试:模拟多次支付累计到阈值,验证只发放一次且累计值递增
- 修复现有单测字段不匹配导致的编译失败
## 验收标准
- 创建/更新 `ShopSeriesAllocation` 时,一次性佣金配置能正确落库并在查询响应中返回。
- 累计触发场景下,多次支付能累加并在达到阈值时发放一次性佣金;之后不重复发放。
- `go test ./...` 通过。

View File

@@ -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/valuetiered 必须有 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 ./...` 确保通过

View File

@@ -50,13 +50,37 @@
订单支付成功后系统 SHALL 更新卡/设备的累计充值金额。
**关键修复**:每次支付成功都必须写回累计充值金额,确保累计值能正确用于一次性佣金的累计触发判断。
#### Scenario: 单卡订单更新累计充值
- **WHEN** 单卡订单支付成功,金额 100 元
- **THEN** IotCard.accumulated_recharge 增加 10000 分
- **THEN** 系统读取 IotCard.accumulated_recharge 当前值
- **AND** 增加 10000 分100 元 = 10000 分)
- **AND** 将新值写回 IotCard.accumulated_recharge
- **AND** 使用更新后的累计值判断是否触发一次性佣金
#### Scenario: 设备订单更新累计充值
- **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: 佣金来源类型
- **WHEN** 创建佣金记录
- **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、失败原因错误信息、重试次数如适用

View File

@@ -22,13 +22,28 @@
系统 SHALL 支持"累计充值"触发条件:当卡/设备的累计充值金额 ≥ 配置阈值时触发一次性佣金。
**关键修复**:每次支付成功后必须更新累计充值金额,确保累计值能正确递增并达到阈值。
#### Scenario: 累计达到阈值
- **WHEN** 卡之前累计充值 200 元,本次充值 150 元,配置阈值 300 元
- **THEN** 累计 350 元 ≥ 300 元,系统发放一次性佣金
- **THEN** 系统更新累计充值为 350 元
- **AND** 累计 350 元 ≥ 300 元,系统发放一次性佣金
- **AND** 标记 first_commission_paid = true
#### Scenario: 累计未达到阈值
- **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 配置获取。
**关键修复**:配置必须能够通过 ShopSeriesAllocation 创建/更新接口正确落库并生效。
#### Scenario: 获取触发条件和金额
- **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 = 500050 元)
- **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** 卡关联的系列分配未配置一次性佣金one_time_commission_amount = 0
- **WHEN** 卡关联的系列分配未启用一次性佣金enable_one_time_commission = false
- **THEN** 不发放一次性佣金
---
@@ -67,3 +109,32 @@
#### Scenario: 发放给归属店铺
- **WHEN** 卡归属店铺 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