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/constants" "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 || order.Source != constants.OrderSourceClient { 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 || order.Source != constants.OrderSourceClient { 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 }