归档一次性佣金配置落库与累计触发修复,同步规范文档到主 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

@@ -113,7 +113,7 @@ func initServices(s *stores, deps *Dependencies) *services {
Carrier: carrierSvc.New(s.Carrier),
PackageSeries: packageSeriesSvc.New(s.PackageSeries),
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),
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),

View File

@@ -194,10 +194,6 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
return errors.Wrap(errors.CodeDatabaseError, err, "获取卡信息失败")
}
if card.FirstCommissionPaid {
return nil
}
if card.SeriesAllocationID == nil {
return nil
}
@@ -211,12 +207,25 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
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
switch allocation.OneTimeCommissionTrigger {
case model.OneTimeCommissionTriggerSingleRecharge:
rechargeAmount = order.TotalAmount
case model.OneTimeCommissionTriggerAccumulatedRecharge:
rechargeAmount = card.AccumulatedRecharge + order.TotalAmount
rechargeAmount = card.AccumulatedRecharge
default:
return nil
}
@@ -260,12 +269,9 @@ func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *g
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
}
updates := map[string]any{"first_commission_paid": true}
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
updates["accumulated_recharge"] = rechargeAmount
}
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).Updates(updates).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡状态失败")
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
Update("first_commission_paid", true).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
}
return nil
@@ -283,10 +289,6 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
return errors.Wrap(errors.CodeDatabaseError, err, "获取设备信息失败")
}
if device.FirstCommissionPaid {
return nil
}
if device.SeriesAllocationID == nil {
return nil
}
@@ -300,12 +302,25 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
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
switch allocation.OneTimeCommissionTrigger {
case model.OneTimeCommissionTriggerSingleRecharge:
rechargeAmount = order.TotalAmount
case model.OneTimeCommissionTriggerAccumulatedRecharge:
rechargeAmount = device.AccumulatedRecharge + order.TotalAmount
rechargeAmount = device.AccumulatedRecharge
default:
return nil
}
@@ -349,12 +364,9 @@ func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
}
updates := map[string]any{"first_commission_paid": true}
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
updates["accumulated_recharge"] = rechargeAmount
}
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).Updates(updates).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备状态失败")
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).
Update("first_commission_paid", true).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败")
}
return nil

View File

@@ -16,29 +16,32 @@ import (
)
type Service struct {
allocationStore *postgres.ShopSeriesAllocationStore
tierStore *postgres.ShopSeriesCommissionTierStore
configStore *postgres.ShopSeriesAllocationConfigStore
shopStore *postgres.ShopStore
packageSeriesStore *postgres.PackageSeriesStore
packageStore *postgres.PackageStore
allocationStore *postgres.ShopSeriesAllocationStore
tierStore *postgres.ShopSeriesCommissionTierStore
configStore *postgres.ShopSeriesAllocationConfigStore
oneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore
shopStore *postgres.ShopStore
packageSeriesStore *postgres.PackageSeriesStore
packageStore *postgres.PackageStore
}
func New(
allocationStore *postgres.ShopSeriesAllocationStore,
tierStore *postgres.ShopSeriesCommissionTierStore,
configStore *postgres.ShopSeriesAllocationConfigStore,
oneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore,
shopStore *postgres.ShopStore,
packageSeriesStore *postgres.PackageSeriesStore,
packageStore *postgres.PackageStore,
) *Service {
return &Service{
allocationStore: allocationStore,
tierStore: tierStore,
configStore: configStore,
shopStore: shopStore,
packageSeriesStore: packageSeriesStore,
packageStore: packageStore,
allocationStore: allocationStore,
tierStore: tierStore,
configStore: configStore,
oneTimeCommissionTierStore: oneTimeCommissionTierStore,
shopStore: shopStore,
packageSeriesStore: packageSeriesStore,
packageStore: packageStore,
}
}
@@ -99,6 +102,10 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
return nil, errors.New(errors.CodeConflict, "该店铺已分配此套餐系列")
}
if err := s.validateOneTimeCommissionConfig(req); err != nil {
return nil, err
}
allocation := &model.ShopSeriesAllocation{
ShopID: req.ShopID,
SeriesID: req.SeriesID,
@@ -108,12 +115,34 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateShopSeriesAllocatio
EnableTierCommission: req.EnableTierCommission,
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
if err := s.allocationStore.Create(ctx, allocation); err != nil {
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)
}
@@ -170,6 +199,42 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
}
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
if configChanged {
@@ -182,6 +247,16 @@ func (s *Service) Update(ctx context.Context, id uint, req *dto.UpdateShopSeries
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)
series, _ := s.packageSeriesStore.GetByID(ctx, allocation.SeriesID)
@@ -323,7 +398,7 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocati
allocatorShopName = allocatorShop.ShopName
}
return &dto.ShopSeriesAllocationResponse{
resp := &dto.ShopSeriesAllocationResponse{
ID: a.ID,
ShopID: a.ShopID,
ShopName: shopName,
@@ -335,11 +410,39 @@ func (s *Service) buildResponse(ctx context.Context, a *model.ShopSeriesAllocati
Mode: a.BaseCommissionMode,
Value: a.BaseCommissionValue,
},
EnableTierCommission: a.EnableTierCommission,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
}, nil
EnableTierCommission: a.EnableTierCommission,
EnableOneTimeCommission: a.EnableOneTimeCommission,
Status: a.Status,
CreatedAt: a.CreatedAt.Format(time.RFC3339),
UpdatedAt: a.UpdatedAt.Format(time.RFC3339),
}
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 {
@@ -371,6 +474,72 @@ func (s *Service) createNewConfigVersion(ctx context.Context, allocation *model.
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) {
config, err := s.configStore.GetEffective(ctx, allocationID, at)
if err != nil {