Files
junhong_cmp_fiber/internal/service/commission_calculation/service.go
huang c7b8ecfebf refactor: 佣金计算适配梯度阶梯 Operator 比较,套餐服务集成代理强充逻辑
commission_calculation: matchOneTimeCommissionTier() 接收 agentTiers 参数,根据 tier.Operator(>、>=、<、<=,默认 >=)执行对应比较逻辑,支持代理专属梯度阶梯计算。package/service: 套餐购买预检调用更新后的强充层级判断接口。

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-04 11:37:02 +08:00

653 lines
22 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package commission_calculation
import (
"context"
"fmt"
"time"
"github.com/break/junhong_cmp_fiber/internal/model"
"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/errors"
"go.uber.org/zap"
"gorm.io/gorm"
)
type Service struct {
db *gorm.DB
commissionRecordStore *postgres.CommissionRecordStore
shopStore *postgres.ShopStore
shopPackageAllocationStore *postgres.ShopPackageAllocationStore
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
packageSeriesStore *postgres.PackageSeriesStore
iotCardStore *postgres.IotCardStore
deviceStore *postgres.DeviceStore
agentWalletStore *postgres.AgentWalletStore
agentWalletTransactionStore *postgres.AgentWalletTransactionStore
orderStore *postgres.OrderStore
orderItemStore *postgres.OrderItemStore
packageStore *postgres.PackageStore
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore
commissionStatsService *commission_stats.Service
logger *zap.Logger
}
func New(
db *gorm.DB,
commissionRecordStore *postgres.CommissionRecordStore,
shopStore *postgres.ShopStore,
shopPackageAllocationStore *postgres.ShopPackageAllocationStore,
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
packageSeriesStore *postgres.PackageSeriesStore,
iotCardStore *postgres.IotCardStore,
deviceStore *postgres.DeviceStore,
agentWalletStore *postgres.AgentWalletStore,
agentWalletTransactionStore *postgres.AgentWalletTransactionStore,
orderStore *postgres.OrderStore,
orderItemStore *postgres.OrderItemStore,
packageStore *postgres.PackageStore,
commissionStatsStore *postgres.ShopSeriesCommissionStatsStore,
commissionStatsService *commission_stats.Service,
logger *zap.Logger,
) *Service {
return &Service{
db: db,
commissionRecordStore: commissionRecordStore,
shopStore: shopStore,
shopPackageAllocationStore: shopPackageAllocationStore,
shopSeriesAllocationStore: shopSeriesAllocationStore,
packageSeriesStore: packageSeriesStore,
iotCardStore: iotCardStore,
deviceStore: deviceStore,
agentWalletStore: agentWalletStore,
agentWalletTransactionStore: agentWalletTransactionStore,
orderStore: orderStore,
orderItemStore: orderItemStore,
packageStore: packageStore,
commissionStatsStore: commissionStatsStore,
commissionStatsService: commissionStatsService,
logger: logger,
}
}
func (s *Service) CalculateCommission(ctx context.Context, orderID uint) error {
return s.db.Transaction(func(tx *gorm.DB) error {
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取订单失败")
}
if order.CommissionStatus == model.CommissionStatusCalculated {
s.logger.Warn("订单佣金已计算,跳过", zap.String("order_id", fmt.Sprint(orderID)))
return nil
}
costDiffRecords, err := s.CalculateCostDiffCommission(ctx, order)
if err != nil {
return errors.Wrap(errors.CodeInternalError, err, "计算成本价差佣金失败")
}
for _, record := range costDiffRecords {
if err := tx.Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建成本价差佣金记录失败")
}
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "佣金入账失败")
}
}
// 代购订单不触发一次性佣金和累计充值更新
if !order.IsPurchaseOnBehalf {
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
if err := s.triggerOneTimeCommissionForCardInTx(ctx, tx, order, *order.IotCardID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "触发单卡一次性佣金失败")
}
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
if err := s.triggerOneTimeCommissionForDeviceInTx(ctx, tx, order, *order.DeviceID); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "触发设备一次性佣金失败")
}
}
}
if err := tx.Model(&model.Order{}).Where("id = ?", orderID).Update("commission_status", model.CommissionStatusCalculated).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新订单佣金状态失败")
}
return nil
})
}
func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.Order) ([]*model.CommissionRecord, error) {
var records []*model.CommissionRecord
if order.SellerShopID == nil {
return records, nil
}
if order.SeriesID == nil {
s.logger.Warn("订单缺少系列ID跳过成本价差佣金计算", zap.Uint("order_id", order.ID))
return records, nil
}
sellerShop, err := s.shopStore.GetByID(ctx, *order.SellerShopID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "获取销售店铺失败")
}
sellerProfit := order.TotalAmount - order.SellerCostPrice
if sellerProfit > 0 {
records = append(records, &model.CommissionRecord{
BaseModel: model.BaseModel{
Creator: order.Creator,
Updater: order.Updater,
},
ShopID: *order.SellerShopID,
OrderID: order.ID,
IotCardID: order.IotCardID,
DeviceID: order.DeviceID,
CommissionSource: model.CommissionSourceCostDiff,
Amount: sellerProfit,
Status: model.CommissionStatusReleased,
})
}
// 获取订单明细以获取套餐ID用于成本价查询
orderItems, err := s.orderItemStore.ListByOrderID(ctx, order.ID)
if err != nil || len(orderItems) == 0 {
s.logger.Warn("获取订单明细失败或订单无明细,跳过成本价差佣金计算", zap.Uint("order_id", order.ID), zap.Error(err))
return records, nil
}
packageID := orderItems[0].PackageID
childCostPrice := order.SellerCostPrice
currentShopID := sellerShop.ParentID
for currentShopID != nil && *currentShopID > 0 {
currentShop, err := s.shopStore.GetByID(ctx, *currentShopID)
if err != nil {
s.logger.Error("获取上级店铺失败", zap.Uint("shop_id", *currentShopID), zap.Error(err))
break
}
allocation, err := s.shopPackageAllocationStore.GetByShopAndPackage(ctx, currentShop.ID, packageID)
if err != nil {
s.logger.Warn("上级店铺未分配该套餐,跳过", zap.Uint("shop_id", currentShop.ID), zap.Uint("package_id", packageID))
break
}
myCostPrice := allocation.CostPrice
profit := childCostPrice - myCostPrice
if profit > 0 {
records = append(records, &model.CommissionRecord{
BaseModel: model.BaseModel{
Creator: order.Creator,
Updater: order.Updater,
},
ShopID: currentShop.ID,
OrderID: order.ID,
IotCardID: order.IotCardID,
DeviceID: order.DeviceID,
CommissionSource: model.CommissionSourceCostDiff,
Amount: profit,
Status: model.CommissionStatusReleased,
})
}
childCostPrice = myCostPrice
currentShopID = currentShop.ParentID
}
return records, nil
}
func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *gorm.DB, order *model.Order, cardID uint) error {
if order.IsPurchaseOnBehalf {
return nil
}
card, err := s.iotCardStore.GetByID(ctx, cardID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取卡信息失败")
}
if card.SeriesID == nil || card.ShopID == nil {
return nil
}
seriesID := *card.SeriesID
series, err := s.packageSeriesStore.GetByID(ctx, seriesID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取套餐系列失败")
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil || !config.Enable {
return nil
}
if s.isOneTimeCommissionExpired(config, card.ActivatedAt) {
s.logger.Info("一次性佣金规则已过期,跳过",
zap.Uint("card_id", cardID),
zap.Uint("series_id", seriesID),
zap.String("validity_type", config.ValidityType))
return nil
}
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
accumulatedBySeries := card.GetAccumulatedRechargeBySeries(seriesID)
newAccumulated := accumulatedBySeries + order.TotalAmount
card.AddAccumulatedRechargeBySeries(seriesID, order.TotalAmount)
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
Update("accumulated_recharge_by_series", card.AccumulatedRechargeBySeriesJSON).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡累计充值金额失败")
}
if card.IsFirstRechargeTriggeredBySeries(seriesID) {
return nil
}
if newAccumulated < config.Threshold {
return nil
}
}
if card.IsFirstRechargeTriggeredBySeries(seriesID) {
return nil
}
records, err := s.calculateChainOneTimeCommission(ctx, *card.ShopID, seriesID, order, &cardID, nil)
if err != nil {
return err
}
for _, record := range records {
if err := tx.Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
}
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
}
}
card.SetFirstRechargeTriggeredBySeries(seriesID, true)
if err := tx.Model(&model.IotCard{}).Where("id = ?", cardID).
Update("first_recharge_triggered_by_series", card.FirstRechargeTriggeredBySeriesJSON).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新卡佣金发放状态失败")
}
return nil
}
func (s *Service) TriggerOneTimeCommissionForCard(ctx context.Context, order *model.Order, cardID uint) error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.triggerOneTimeCommissionForCardInTx(ctx, tx, order, cardID)
})
}
func (s *Service) triggerOneTimeCommissionForDeviceInTx(ctx context.Context, tx *gorm.DB, order *model.Order, deviceID uint) error {
if order.IsPurchaseOnBehalf {
return nil
}
device, err := s.deviceStore.GetByID(ctx, deviceID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取设备信息失败")
}
if device.SeriesID == nil || device.ShopID == nil {
return nil
}
seriesID := *device.SeriesID
series, err := s.packageSeriesStore.GetByID(ctx, seriesID)
if err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取套餐系列失败")
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil || !config.Enable {
return nil
}
if s.isOneTimeCommissionExpired(config, device.ActivatedAt) {
s.logger.Info("一次性佣金规则已过期,跳过",
zap.Uint("device_id", deviceID),
zap.Uint("series_id", seriesID),
zap.String("validity_type", config.ValidityType))
return nil
}
if config.TriggerType == model.OneTimeCommissionTriggerFirstRecharge {
accumulatedBySeries := device.GetAccumulatedRechargeBySeries(seriesID)
newAccumulated := accumulatedBySeries + order.TotalAmount
device.AddAccumulatedRechargeBySeries(seriesID, order.TotalAmount)
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).
Update("accumulated_recharge_by_series", device.AccumulatedRechargeBySeriesJSON).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备累计充值金额失败")
}
if device.IsFirstRechargeTriggeredBySeries(seriesID) {
return nil
}
if newAccumulated < config.Threshold {
return nil
}
}
if device.IsFirstRechargeTriggeredBySeries(seriesID) {
return nil
}
records, err := s.calculateChainOneTimeCommission(ctx, *device.ShopID, seriesID, order, nil, &deviceID)
if err != nil {
return err
}
for _, record := range records {
if err := tx.Create(record).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
}
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
}
}
device.SetFirstRechargeTriggeredBySeries(seriesID, true)
if err := tx.Model(&model.Device{}).Where("id = ?", deviceID).
Update("first_recharge_triggered_by_series", device.FirstRechargeTriggeredBySeriesJSON).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新设备佣金发放状态失败")
}
return nil
}
func (s *Service) TriggerOneTimeCommissionForDevice(ctx context.Context, order *model.Order, deviceID uint) error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.triggerOneTimeCommissionForDeviceInTx(ctx, tx, order, deviceID)
})
}
func (s *Service) isOneTimeCommissionExpired(config *model.OneTimeCommissionConfig, activatedAt *time.Time) bool {
if config == nil {
return true
}
now := time.Now()
switch config.ValidityType {
case model.OneTimeCommissionValidityPermanent:
return false
case model.OneTimeCommissionValidityFixedDate:
if config.ValidityValue == "" {
return false
}
expiryDate, err := time.Parse("2006-01-02", config.ValidityValue)
if err != nil {
s.logger.Warn("解析一次性佣金到期日期失败",
zap.String("validity_value", config.ValidityValue),
zap.Error(err))
return false
}
expiryDate = expiryDate.Add(24*time.Hour - time.Second)
return now.After(expiryDate)
case model.OneTimeCommissionValidityRelative:
if activatedAt == nil {
return false
}
if config.ValidityValue == "" {
return false
}
months := 0
if _, err := fmt.Sscanf(config.ValidityValue, "%d", &months); err != nil || months <= 0 {
s.logger.Warn("解析一次性佣金相对时长失败",
zap.String("validity_value", config.ValidityValue),
zap.Error(err))
return false
}
expiryTime := activatedAt.AddDate(0, months, 0)
return now.After(expiryTime)
default:
return false
}
}
func (s *Service) calculateChainOneTimeCommission(ctx context.Context, bottomShopID uint, seriesID uint, order *model.Order, cardID *uint, deviceID *uint) ([]*model.CommissionRecord, error) {
var records []*model.CommissionRecord
series, err := s.packageSeriesStore.GetByID(ctx, seriesID)
if err != nil {
s.logger.Warn("获取套餐系列失败,跳过一次性佣金", zap.Uint("series_id", seriesID), zap.Error(err))
return records, nil
}
config, err := series.GetOneTimeCommissionConfig()
if err != nil || config == nil || !config.Enable {
return records, nil
}
bottomSeriesAllocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, bottomShopID, seriesID)
if err != nil {
s.logger.Warn("底层店铺未分配该系列,跳过一次性佣金", zap.Uint("shop_id", bottomShopID), zap.Uint("series_id", seriesID))
return records, nil
}
bottomShop, err := s.shopStore.GetByID(ctx, bottomShopID)
if err != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, err, "获取店铺信息失败")
}
childAmountGiven := int64(0)
currentShopID := bottomShopID
currentShop := bottomShop
currentSeriesAllocation := bottomSeriesAllocation
for {
var myAmount int64
if config.CommissionType == "tiered" && len(config.Tiers) > 0 {
// 获取该代理的专属阶梯金额列表
agentTiers, tiersErr := currentSeriesAllocation.GetCommissionTiers()
if tiersErr != nil {
s.logger.Warn("解析代理阶梯佣金失败,使用固定金额", zap.Uint("shop_id", currentShopID), zap.Error(tiersErr))
myAmount = currentSeriesAllocation.OneTimeCommissionAmount
} else if len(agentTiers) == 0 {
// commission_tiers_json 为空(历史数据),降级到 OneTimeCommissionAmount
s.logger.Warn("代理专属阶梯为空fallback 到固定金额", zap.Uint("shop_id", currentShopID))
myAmount = currentSeriesAllocation.OneTimeCommissionAmount
} else {
tieredAmount, tierErr := s.matchOneTimeCommissionTier(ctx, currentShopID, seriesID, currentSeriesAllocation.ID, config.Tiers, agentTiers)
if tierErr != nil {
s.logger.Warn("匹配梯度佣金失败,使用固定金额", zap.Uint("shop_id", currentShopID), zap.Error(tierErr))
myAmount = currentSeriesAllocation.OneTimeCommissionAmount
} else {
myAmount = tieredAmount
}
}
} else {
myAmount = currentSeriesAllocation.OneTimeCommissionAmount
}
actualProfit := myAmount - childAmountGiven
if actualProfit > 0 {
remark := "一次性佣金"
if deviceID != nil {
remark = "一次性佣金(设备)"
}
records = append(records, &model.CommissionRecord{
BaseModel: model.BaseModel{
Creator: order.Creator,
Updater: order.Updater,
},
ShopID: currentShopID,
OrderID: order.ID,
IotCardID: cardID,
DeviceID: deviceID,
CommissionSource: model.CommissionSourceOneTime,
Amount: actualProfit,
Status: model.CommissionStatusReleased,
Remark: remark,
})
}
if currentShop.ParentID == nil || *currentShop.ParentID == 0 {
break
}
parentShopID := *currentShop.ParentID
parentSeriesAllocation, err := s.shopSeriesAllocationStore.GetByShopAndSeries(ctx, parentShopID, seriesID)
if err != nil {
s.logger.Warn("上级店铺未分配该系列,停止链式计算",
zap.Uint("parent_shop_id", parentShopID),
zap.Uint("series_id", seriesID))
break
}
parentShop, err := s.shopStore.GetByID(ctx, parentShopID)
if err != nil {
s.logger.Error("获取上级店铺失败", zap.Uint("shop_id", parentShopID), zap.Error(err))
break
}
childAmountGiven = myAmount
currentShopID = parentShopID
currentShop = parentShop
currentSeriesAllocation = parentSeriesAllocation
}
return records, nil
}
func (s *Service) matchOneTimeCommissionTier(ctx context.Context, shopID uint, seriesID uint, allocationID uint, tiers []model.OneTimeCommissionTier, agentTiers []model.AllocationCommissionTier) (int64, error) {
if len(tiers) == 0 {
return 0, nil
}
now := time.Now()
var matchedAmount int64 = 0
for _, tier := range tiers {
var salesCount, salesAmount int64
var err error
if tier.StatScope == model.OneTimeCommissionStatScopeSelfAndSub {
subordinateIDs, subErr := s.shopStore.GetSubordinateShopIDs(ctx, shopID)
if subErr != nil {
s.logger.Warn("获取下级店铺失败", zap.Uint("shop_id", shopID), zap.Error(subErr))
subordinateIDs = []uint{shopID}
}
allocationIDs, allocErr := s.shopSeriesAllocationStore.GetIDsByShopIDsAndSeries(ctx, subordinateIDs, seriesID)
if allocErr != nil {
return 0, errors.Wrap(errors.CodeDatabaseError, allocErr, "获取下级分配ID失败")
}
salesCount, salesAmount, err = s.commissionStatsStore.GetAggregatedStats(ctx, allocationIDs, "monthly", now)
} else {
salesCount, salesAmount, err = s.commissionStatsStore.GetAggregatedStats(ctx, []uint{allocationID}, "monthly", now)
}
if err != nil {
s.logger.Warn("获取销售统计失败", zap.Uint("allocation_id", allocationID), zap.Error(err))
continue
}
var currentValue int64
if tier.Dimension == model.TierTypeSalesCount {
currentValue = salesCount
} else {
currentValue = salesAmount
}
// 根据 tier.Operator 判断是否命中阈值Operator 为空时默认 >=
var hit bool
switch tier.Operator {
case model.TierOperatorGT:
hit = currentValue > tier.Threshold
case model.TierOperatorLT:
hit = currentValue < tier.Threshold
case model.TierOperatorLTE:
hit = currentValue <= tier.Threshold
default: // >= 或空字符串
hit = currentValue >= tier.Threshold
}
if !hit {
continue
}
// 从代理专属阶梯列表中按 threshold 查找对应金额
for _, agentTier := range agentTiers {
if agentTier.Threshold == tier.Threshold && agentTier.Amount > matchedAmount {
matchedAmount = agentTier.Amount
break
}
}
}
return matchedAmount, nil
}
func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record *model.CommissionRecord) error {
// 获取店铺的分佣钱包
var wallet model.AgentWallet
if err := tx.Where("shop_id = ? AND wallet_type = ?", record.ShopID, "commission").First(&wallet).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "获取店铺分佣钱包失败")
}
balanceBefore := wallet.Balance
// 使用 AgentWalletStore 的方法增加余额(带事务)
if err := s.agentWalletStore.AddBalanceWithTx(ctx, tx, wallet.ID, record.Amount); err != nil {
return errors.Wrap(errors.CodeInternalError, err, "更新钱包余额失败")
}
// 更新佣金记录状态
now := time.Now()
if err := tx.Model(record).Updates(map[string]any{
"balance_after": balanceBefore + record.Amount,
"status": model.CommissionStatusReleased,
"released_at": now,
}).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "更新佣金记录失败")
}
// 创建代理钱包交易记录
remark := "佣金入账"
transaction := &model.AgentWalletTransaction{
AgentWalletID: wallet.ID,
ShopID: record.ShopID,
UserID: record.Creator,
TransactionType: "commission",
Amount: record.Amount,
BalanceBefore: balanceBefore,
BalanceAfter: balanceBefore + record.Amount,
Status: 1,
ReferenceType: stringPtr("commission"),
ReferenceID: &record.ID,
Remark: &remark,
Creator: record.Creator,
ShopIDTag: record.ShopID,
}
if err := s.agentWalletTransactionStore.CreateWithTx(ctx, tx, transaction); err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包交易记录失败")
}
return nil
}
func (s *Service) CreditCommission(ctx context.Context, record *model.CommissionRecord) error {
return s.db.Transaction(func(tx *gorm.DB) error {
return s.creditCommissionInTx(ctx, tx, record)
})
}
func stringPtr(s string) *string {
return &s
}