refactor: 一次性佣金配置从套餐级别提升到系列级别
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m29s
主要变更: - 新增 tb_shop_series_allocation 表,存储系列级别的一次性佣金配置 - ShopPackageAllocation 移除 one_time_commission_amount 字段 - PackageSeries 新增 enable_one_time_commission 字段控制是否启用一次性佣金 - 新增 /api/admin/shop-series-allocations CRUD 接口 - 佣金计算逻辑改为从 ShopSeriesAllocation 获取一次性佣金金额 - 删除废弃的 ShopSeriesOneTimeCommissionTier 模型 - OpenAPI Tag '系列分配' 和 '单套餐分配' 合并为 '套餐分配' 迁移脚本: - 000042: 重构佣金套餐模型 - 000043: 简化佣金分配 - 000044: 一次性佣金分配重构 - 000045: PackageSeries 添加 enable_one_time_commission 字段 测试: - 新增验收测试 (shop_series_allocation, commission_calculation) - 新增流程测试 (one_time_commission_chain) - 删除过时的单元测试(已被验收测试覆盖)
This commit is contained in:
@@ -38,6 +38,7 @@ type Service struct {
|
||||
iotCardStore *postgres.IotCardStore
|
||||
deviceStore *postgres.DeviceStore
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||||
packageSeriesStore *postgres.PackageSeriesStore
|
||||
commissionRecordStore *postgres.CommissionRecordStore
|
||||
logger *zap.Logger
|
||||
}
|
||||
@@ -51,6 +52,7 @@ func New(
|
||||
iotCardStore *postgres.IotCardStore,
|
||||
deviceStore *postgres.DeviceStore,
|
||||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||||
packageSeriesStore *postgres.PackageSeriesStore,
|
||||
commissionRecordStore *postgres.CommissionRecordStore,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
@@ -62,6 +64,7 @@ func New(
|
||||
iotCardStore: iotCardStore,
|
||||
deviceStore: deviceStore,
|
||||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||||
packageSeriesStore: packageSeriesStore,
|
||||
commissionRecordStore: commissionRecordStore,
|
||||
logger: logger,
|
||||
}
|
||||
@@ -379,7 +382,6 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
|
||||
var accumulatedRecharge int64
|
||||
var firstCommissionPaid bool
|
||||
|
||||
// 1. 查询资源信息
|
||||
if resourceType == "iot_card" {
|
||||
card, err := s.iotCardStore.GetByID(ctx, resourceID)
|
||||
if err != nil {
|
||||
@@ -390,8 +392,10 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
|
||||
}
|
||||
seriesID = card.SeriesID
|
||||
shopID = card.ShopID
|
||||
accumulatedRecharge = card.AccumulatedRecharge
|
||||
firstCommissionPaid = card.FirstCommissionPaid
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
} else if resourceType == "device" {
|
||||
device, err := s.deviceStore.GetByID(ctx, resourceID)
|
||||
if err != nil {
|
||||
@@ -402,80 +406,101 @@ func (s *Service) checkForceRechargeRequirement(ctx context.Context, resourceTyp
|
||||
}
|
||||
seriesID = device.SeriesID
|
||||
shopID = device.ShopID
|
||||
accumulatedRecharge = device.AccumulatedRecharge
|
||||
firstCommissionPaid = device.FirstCommissionPaid
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
}
|
||||
|
||||
result.CurrentAccumulated = accumulatedRecharge
|
||||
result.FirstCommissionPaid = firstCommissionPaid
|
||||
|
||||
// 2. 如果没有系列ID或店铺ID,无强充要求
|
||||
if seriesID == nil || shopID == nil {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 3. 查询系列分配配置
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID)
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return result, nil
|
||||
}
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败")
|
||||
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
|
||||
}
|
||||
|
||||
// 4. 如果未启用一次性佣金,无强充要求
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
config, err := series.GetOneTimeCommissionConfig()
|
||||
if err != nil || config == nil || !config.Enable {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.Threshold = allocation.OneTimeCommissionThreshold
|
||||
result.TriggerType = allocation.OneTimeCommissionTrigger
|
||||
result.Threshold = config.Threshold
|
||||
result.TriggerType = config.TriggerType
|
||||
|
||||
// 5. 如果一次性佣金已发放,无强充要求
|
||||
if firstCommissionPaid {
|
||||
result.Message = "一次性佣金已发放,无强充要求"
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// 6. 根据触发类型判断强充要求
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerSingleRecharge {
|
||||
// 首次充值触发:必须充值阈值金额
|
||||
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
|
||||
result.NeedForceRecharge = true
|
||||
result.ForceRechargeAmount = allocation.OneTimeCommissionThreshold
|
||||
result.Message = fmt.Sprintf("首次充值必须充值%d分", allocation.OneTimeCommissionThreshold)
|
||||
} else if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||
// 累计充值触发:检查是否启用强充
|
||||
if allocation.EnableForceRecharge {
|
||||
result.NeedForceRecharge = true
|
||||
// 强充金额优先使用配置值,否则使用阈值
|
||||
if allocation.ForceRechargeAmount > 0 {
|
||||
result.ForceRechargeAmount = allocation.ForceRechargeAmount
|
||||
} else {
|
||||
result.ForceRechargeAmount = allocation.OneTimeCommissionThreshold
|
||||
}
|
||||
result.Message = fmt.Sprintf("每次充值必须充值%d分", result.ForceRechargeAmount)
|
||||
result.ForceRechargeAmount = config.Threshold
|
||||
result.Message = fmt.Sprintf("首次充值必须充值%d分", config.Threshold)
|
||||
} else if config.EnableForceRecharge {
|
||||
result.NeedForceRecharge = true
|
||||
if config.ForceAmount > 0 {
|
||||
result.ForceRechargeAmount = config.ForceAmount
|
||||
} else {
|
||||
result.Message = "累计充值模式,可自由充值"
|
||||
result.ForceRechargeAmount = config.Threshold
|
||||
}
|
||||
result.Message = fmt.Sprintf("每次充值必须充值%d分", result.ForceRechargeAmount)
|
||||
} else {
|
||||
result.Message = "累计充值模式,可自由充值"
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// updateAccumulatedRechargeInTx 更新累计充值(事务内使用)
|
||||
// 原子操作更新卡或设备的累计充值金额
|
||||
// 同时更新旧的 accumulated_recharge 字段和新的 accumulated_recharge_by_series JSON 字段
|
||||
func (s *Service) updateAccumulatedRechargeInTx(ctx context.Context, tx *gorm.DB, resourceType string, resourceID uint, amount int64) error {
|
||||
if resourceType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
|
||||
if card.SeriesID != nil {
|
||||
if err := card.AddAccumulatedRechargeBySeries(*card.SeriesID, amount); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新卡按系列累计充值失败")
|
||||
}
|
||||
}
|
||||
|
||||
result := tx.Model(&model.IotCard{}).
|
||||
Where("id = ?", resourceID).
|
||||
Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", amount))
|
||||
Updates(map[string]any{
|
||||
"accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount),
|
||||
"accumulated_recharge_by_series": card.AccumulatedRechargeBySeriesJSON,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新卡累计充值失败")
|
||||
}
|
||||
} else if resourceType == "device" {
|
||||
var device model.Device
|
||||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
|
||||
if device.SeriesID != nil {
|
||||
if err := device.AddAccumulatedRechargeBySeries(*device.SeriesID, amount); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "更新设备按系列累计充值失败")
|
||||
}
|
||||
}
|
||||
|
||||
result := tx.Model(&model.Device{}).
|
||||
Where("id = ?", resourceID).
|
||||
Update("accumulated_recharge", gorm.Expr("accumulated_recharge + ?", amount))
|
||||
Updates(map[string]any{
|
||||
"accumulated_recharge": gorm.Expr("accumulated_recharge + ?", amount),
|
||||
"accumulated_recharge_by_series": device.AccumulatedRechargeBySeriesJSON,
|
||||
})
|
||||
if result.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新设备累计充值失败")
|
||||
}
|
||||
@@ -491,33 +516,34 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
var firstCommissionPaid bool
|
||||
var shopID *uint
|
||||
|
||||
// 1. 查询资源当前状态(需要从数据库重新查询以获取更新后的累计充值)
|
||||
if resourceType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
seriesID = card.SeriesID
|
||||
accumulatedRecharge = card.AccumulatedRecharge
|
||||
firstCommissionPaid = card.FirstCommissionPaid
|
||||
shopID = card.ShopID
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = card.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = card.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
} else if resourceType == "device" {
|
||||
var device model.Device
|
||||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
seriesID = device.SeriesID
|
||||
accumulatedRecharge = device.AccumulatedRecharge
|
||||
firstCommissionPaid = device.FirstCommissionPaid
|
||||
shopID = device.ShopID
|
||||
if seriesID != nil {
|
||||
accumulatedRecharge = device.GetAccumulatedRechargeBySeries(*seriesID)
|
||||
firstCommissionPaid = device.IsFirstRechargeTriggeredBySeries(*seriesID)
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果没有系列ID或已发放佣金,跳过
|
||||
if seriesID == nil || firstCommissionPaid {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. 如果没有归属店铺,无法发放佣金
|
||||
if shopID == nil {
|
||||
s.logger.Warn("资源未归属店铺,无法发放一次性佣金",
|
||||
zap.String("resource_type", resourceType),
|
||||
@@ -526,7 +552,19 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return nil
|
||||
}
|
||||
|
||||
// 4. 查询系列分配配置
|
||||
series, err := s.packageSeriesStore.GetByID(ctx, *seriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询套餐系列失败")
|
||||
}
|
||||
|
||||
config, cfgErr := series.GetOneTimeCommissionConfig()
|
||||
if cfgErr != nil || config == nil || !config.Enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
allocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, *shopID, *seriesID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
@@ -535,34 +573,23 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询系列分配失败")
|
||||
}
|
||||
|
||||
// 5. 如果未启用一次性佣金,跳过
|
||||
if !allocation.EnableOneTimeCommission {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 6. 根据触发类型判断是否满足条件
|
||||
var rechargeAmountToCheck int64
|
||||
switch allocation.OneTimeCommissionTrigger {
|
||||
case model.OneTimeCommissionTriggerSingleRecharge:
|
||||
switch config.TriggerType {
|
||||
case model.OneTimeCommissionTriggerFirstRecharge:
|
||||
rechargeAmountToCheck = rechargeAmount
|
||||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||||
rechargeAmountToCheck = accumulatedRecharge
|
||||
default:
|
||||
rechargeAmountToCheck = accumulatedRecharge
|
||||
}
|
||||
|
||||
if rechargeAmountToCheck < config.Threshold {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 7. 检查是否达到阈值
|
||||
if rechargeAmountToCheck < allocation.OneTimeCommissionThreshold {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 8. 计算佣金金额
|
||||
commissionAmount := s.calculateOneTimeCommission(allocation, rechargeAmount)
|
||||
commissionAmount := allocation.OneTimeCommissionAmount
|
||||
if commissionAmount <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 9. 查询店铺的佣金钱包
|
||||
var commissionWallet model.Wallet
|
||||
if err := tx.Where("resource_type = ? AND resource_id = ? AND wallet_type = ?", "shop", *shopID, "commission").
|
||||
First(&commissionWallet).Error; err != nil {
|
||||
@@ -575,7 +602,6 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询店铺佣金钱包失败")
|
||||
}
|
||||
|
||||
// 10. 创建佣金记录
|
||||
var iotCardID, deviceID *uint
|
||||
if resourceType == "iot_card" {
|
||||
iotCardID = &resourceID
|
||||
@@ -647,13 +673,33 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
|
||||
// 14. 标记一次性佣金已发放
|
||||
if resourceType == "iot_card" {
|
||||
var card model.IotCard
|
||||
if err := tx.First(&card, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询IoT卡失败")
|
||||
}
|
||||
if err := card.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "设置卡佣金发放状态失败")
|
||||
}
|
||||
if err := tx.Model(&model.IotCard{}).Where("id = ?", resourceID).
|
||||
Update("first_commission_paid", true).Error; err != nil {
|
||||
Updates(map[string]any{
|
||||
"first_commission_paid": true,
|
||||
"first_recharge_triggered_by_series": card.FirstRechargeTriggeredBySeriesJSON,
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
|
||||
}
|
||||
} else {
|
||||
var device model.Device
|
||||
if err := tx.First(&device, resourceID).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询设备失败")
|
||||
}
|
||||
if err := device.SetFirstRechargeTriggeredBySeries(*seriesID, true); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "设置设备佣金发放状态失败")
|
||||
}
|
||||
if err := tx.Model(&model.Device{}).Where("id = ?", resourceID).
|
||||
Update("first_commission_paid", true).Error; err != nil {
|
||||
Updates(map[string]any{
|
||||
"first_commission_paid": true,
|
||||
"first_recharge_triggered_by_series": device.FirstRechargeTriggeredBySeriesJSON,
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败")
|
||||
}
|
||||
}
|
||||
@@ -668,21 +714,6 @@ func (s *Service) triggerOneTimeCommissionIfNeededInTx(ctx context.Context, tx *
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateOneTimeCommission 计算一次性佣金金额
|
||||
func (s *Service) calculateOneTimeCommission(allocation *model.ShopSeriesAllocation, orderAmount int64) int64 {
|
||||
if allocation.OneTimeCommissionType == model.OneTimeCommissionTypeFixed {
|
||||
// 固定佣金
|
||||
if allocation.OneTimeCommissionMode == model.CommissionModeFixed {
|
||||
return allocation.OneTimeCommissionValue
|
||||
} else if allocation.OneTimeCommissionMode == model.CommissionModePercent {
|
||||
// 百分比佣金(千分比)
|
||||
return orderAmount * allocation.OneTimeCommissionValue / 1000
|
||||
}
|
||||
}
|
||||
// 梯度佣金在此不处理,由 commission_calculation 服务处理
|
||||
return 0
|
||||
}
|
||||
|
||||
// generateRechargeNo 生成充值订单号
|
||||
// 格式: RCH + 14位时间戳 + 6位随机数
|
||||
func (s *Service) generateRechargeNo() string {
|
||||
|
||||
Reference in New Issue
Block a user