All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m40s
- 归档 OpenSpec 变更到 archive 目录 - 创建 2 个新的主规范文件:commission-trigger 和 order-commission-snapshot - 实现订单佣金快照字段和支付自动触发 - 确保事务一致性,所有佣金操作在同一事务内完成 - 提取成本价计算为公共工具函数
498 lines
16 KiB
Go
498 lines
16 KiB
Go
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"
|
||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||
"go.uber.org/zap"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type Service struct {
|
||
db *gorm.DB
|
||
commissionRecordStore *postgres.CommissionRecordStore
|
||
shopStore *postgres.ShopStore
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore
|
||
shopSeriesOneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore
|
||
iotCardStore *postgres.IotCardStore
|
||
deviceStore *postgres.DeviceStore
|
||
walletStore *postgres.WalletStore
|
||
walletTransactionStore *postgres.WalletTransactionStore
|
||
orderStore *postgres.OrderStore
|
||
orderItemStore *postgres.OrderItemStore
|
||
packageStore *postgres.PackageStore
|
||
commissionStatsService *commission_stats.Service
|
||
logger *zap.Logger
|
||
}
|
||
|
||
func New(
|
||
db *gorm.DB,
|
||
commissionRecordStore *postgres.CommissionRecordStore,
|
||
shopStore *postgres.ShopStore,
|
||
shopSeriesAllocationStore *postgres.ShopSeriesAllocationStore,
|
||
shopSeriesOneTimeCommissionTierStore *postgres.ShopSeriesOneTimeCommissionTierStore,
|
||
iotCardStore *postgres.IotCardStore,
|
||
deviceStore *postgres.DeviceStore,
|
||
walletStore *postgres.WalletStore,
|
||
walletTransactionStore *postgres.WalletTransactionStore,
|
||
orderStore *postgres.OrderStore,
|
||
orderItemStore *postgres.OrderItemStore,
|
||
packageStore *postgres.PackageStore,
|
||
commissionStatsService *commission_stats.Service,
|
||
logger *zap.Logger,
|
||
) *Service {
|
||
return &Service{
|
||
db: db,
|
||
commissionRecordStore: commissionRecordStore,
|
||
shopStore: shopStore,
|
||
shopSeriesAllocationStore: shopSeriesAllocationStore,
|
||
shopSeriesOneTimeCommissionTierStore: shopSeriesOneTimeCommissionTierStore,
|
||
iotCardStore: iotCardStore,
|
||
deviceStore: deviceStore,
|
||
walletStore: walletStore,
|
||
walletTransactionStore: walletTransactionStore,
|
||
orderStore: orderStore,
|
||
orderItemStore: orderItemStore,
|
||
packageStore: packageStore,
|
||
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.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,
|
||
})
|
||
}
|
||
|
||
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.shopSeriesAllocationStore.GetByShopAndSeries(ctx, currentShop.ID, *order.SeriesID)
|
||
if err != nil {
|
||
s.logger.Warn("上级店铺未分配该系列,跳过", zap.Uint("shop_id", currentShop.ID), zap.Uint("series_id", *order.SeriesID))
|
||
break
|
||
}
|
||
|
||
myCostPrice := s.calculateCostPrice(allocation, order.TotalAmount)
|
||
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) calculateCostPrice(allocation *model.ShopSeriesAllocation, orderAmount int64) int64 {
|
||
return utils.CalculateCostPrice(allocation, orderAmount)
|
||
}
|
||
|
||
func (s *Service) triggerOneTimeCommissionForCardInTx(ctx context.Context, tx *gorm.DB, order *model.Order, cardID uint) error {
|
||
card, err := s.iotCardStore.GetByID(ctx, cardID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "获取卡信息失败")
|
||
}
|
||
|
||
if card.FirstCommissionPaid {
|
||
return nil
|
||
}
|
||
|
||
if card.SeriesAllocationID == nil {
|
||
return nil
|
||
}
|
||
|
||
allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, *card.SeriesAllocationID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "获取系列分配失败")
|
||
}
|
||
|
||
if !allocation.EnableOneTimeCommission {
|
||
return nil
|
||
}
|
||
|
||
var rechargeAmount int64
|
||
switch allocation.OneTimeCommissionTrigger {
|
||
case model.OneTimeCommissionTriggerSingleRecharge:
|
||
rechargeAmount = order.TotalAmount
|
||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||
rechargeAmount = card.AccumulatedRecharge + order.TotalAmount
|
||
default:
|
||
return nil
|
||
}
|
||
|
||
if rechargeAmount < allocation.OneTimeCommissionThreshold {
|
||
return nil
|
||
}
|
||
|
||
commissionAmount, err := s.calculateOneTimeCommission(ctx, allocation, order.TotalAmount)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "计算一次性佣金失败")
|
||
}
|
||
|
||
if commissionAmount <= 0 {
|
||
return nil
|
||
}
|
||
|
||
if card.ShopID == nil {
|
||
return errors.New(errors.CodeInvalidParam, "卡未归属任何店铺,无法发放佣金")
|
||
}
|
||
|
||
record := &model.CommissionRecord{
|
||
BaseModel: model.BaseModel{
|
||
Creator: order.Creator,
|
||
Updater: order.Updater,
|
||
},
|
||
ShopID: *card.ShopID,
|
||
OrderID: order.ID,
|
||
IotCardID: &cardID,
|
||
CommissionSource: model.CommissionSourceOneTime,
|
||
Amount: commissionAmount,
|
||
Status: model.CommissionStatusReleased,
|
||
Remark: "一次性佣金",
|
||
}
|
||
|
||
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, "一次性佣金入账失败")
|
||
}
|
||
|
||
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, "更新卡状态失败")
|
||
}
|
||
|
||
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 {
|
||
device, err := s.deviceStore.GetByID(ctx, deviceID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "获取设备信息失败")
|
||
}
|
||
|
||
if device.FirstCommissionPaid {
|
||
return nil
|
||
}
|
||
|
||
if device.SeriesAllocationID == nil {
|
||
return nil
|
||
}
|
||
|
||
allocation, err := s.shopSeriesAllocationStore.GetByID(ctx, *device.SeriesAllocationID)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "获取系列分配失败")
|
||
}
|
||
|
||
if !allocation.EnableOneTimeCommission {
|
||
return nil
|
||
}
|
||
|
||
var rechargeAmount int64
|
||
switch allocation.OneTimeCommissionTrigger {
|
||
case model.OneTimeCommissionTriggerSingleRecharge:
|
||
rechargeAmount = order.TotalAmount
|
||
case model.OneTimeCommissionTriggerAccumulatedRecharge:
|
||
rechargeAmount = device.AccumulatedRecharge + order.TotalAmount
|
||
default:
|
||
return nil
|
||
}
|
||
|
||
if rechargeAmount < allocation.OneTimeCommissionThreshold {
|
||
return nil
|
||
}
|
||
|
||
commissionAmount, err := s.calculateOneTimeCommission(ctx, allocation, order.TotalAmount)
|
||
if err != nil {
|
||
return errors.Wrap(errors.CodeInternalError, err, "计算一次性佣金失败")
|
||
}
|
||
|
||
if commissionAmount <= 0 {
|
||
return nil
|
||
}
|
||
|
||
if device.ShopID == nil {
|
||
return errors.New(errors.CodeInvalidParam, "设备未归属任何店铺,无法发放佣金")
|
||
}
|
||
|
||
record := &model.CommissionRecord{
|
||
BaseModel: model.BaseModel{
|
||
Creator: order.Creator,
|
||
Updater: order.Updater,
|
||
},
|
||
ShopID: *device.ShopID,
|
||
OrderID: order.ID,
|
||
DeviceID: &deviceID,
|
||
CommissionSource: model.CommissionSourceOneTime,
|
||
Amount: commissionAmount,
|
||
Status: model.CommissionStatusReleased,
|
||
Remark: "一次性佣金(设备)",
|
||
}
|
||
|
||
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, "一次性佣金入账失败")
|
||
}
|
||
|
||
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, "更新设备状态失败")
|
||
}
|
||
|
||
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) calculateOneTimeCommission(ctx context.Context, allocation *model.ShopSeriesAllocation, orderAmount int64) (int64, error) {
|
||
switch allocation.OneTimeCommissionType {
|
||
case model.OneTimeCommissionTypeFixed:
|
||
return s.calculateFixedCommission(allocation.OneTimeCommissionMode, allocation.OneTimeCommissionValue, orderAmount), nil
|
||
case model.OneTimeCommissionTypeTiered:
|
||
return s.calculateTieredCommission(ctx, allocation.ID, orderAmount)
|
||
}
|
||
return 0, nil
|
||
}
|
||
|
||
func (s *Service) calculateFixedCommission(mode string, value int64, orderAmount int64) int64 {
|
||
if mode == model.CommissionModeFixed {
|
||
return value
|
||
} else if mode == model.CommissionModePercent {
|
||
return orderAmount * value / 1000
|
||
}
|
||
return 0
|
||
}
|
||
|
||
func (s *Service) calculateTieredCommission(ctx context.Context, allocationID uint, orderAmount int64) (int64, error) {
|
||
tiers, err := s.shopSeriesOneTimeCommissionTierStore.ListByAllocationID(ctx, allocationID)
|
||
if err != nil {
|
||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "获取梯度配置失败")
|
||
}
|
||
|
||
if len(tiers) == 0 {
|
||
return 0, nil
|
||
}
|
||
|
||
stats, err := s.commissionStatsService.GetCurrentStats(ctx, allocationID, "all_time")
|
||
if err != nil {
|
||
s.logger.Error("获取销售业绩统计失败", zap.Uint("allocation_id", allocationID), zap.Error(err))
|
||
return 0, nil
|
||
}
|
||
|
||
if stats == nil {
|
||
return 0, nil
|
||
}
|
||
|
||
var matchedTier *model.ShopSeriesOneTimeCommissionTier
|
||
for _, tier := range tiers {
|
||
var salesValue int64
|
||
if tier.TierType == model.TierTypeSalesCount {
|
||
salesValue = stats.TotalSalesCount
|
||
} else if tier.TierType == model.TierTypeSalesAmount {
|
||
salesValue = stats.TotalSalesAmount
|
||
} else {
|
||
continue
|
||
}
|
||
|
||
if salesValue >= tier.ThresholdValue {
|
||
if matchedTier == nil || tier.ThresholdValue > matchedTier.ThresholdValue {
|
||
matchedTier = tier
|
||
}
|
||
}
|
||
}
|
||
|
||
if matchedTier == nil {
|
||
return 0, nil
|
||
}
|
||
|
||
if matchedTier.CommissionMode == model.CommissionModeFixed {
|
||
return matchedTier.CommissionValue, nil
|
||
} else if matchedTier.CommissionMode == model.CommissionModePercent {
|
||
return orderAmount * matchedTier.CommissionValue / 1000, nil
|
||
}
|
||
|
||
return 0, nil
|
||
}
|
||
|
||
func (s *Service) creditCommissionInTx(ctx context.Context, tx *gorm.DB, record *model.CommissionRecord) error {
|
||
var wallet model.Wallet
|
||
if err := tx.Where("resource_type = ? AND resource_id = ? AND wallet_type = ?", "shop", record.ShopID, "commission").First(&wallet).Error; err != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, err, "获取店铺钱包失败")
|
||
}
|
||
|
||
balanceBefore := wallet.Balance
|
||
result := tx.Model(&model.Wallet{}).
|
||
Where("id = ? AND version = ?", wallet.ID, wallet.Version).
|
||
Updates(map[string]any{
|
||
"balance": gorm.Expr("balance + ?", record.Amount),
|
||
"version": gorm.Expr("version + 1"),
|
||
})
|
||
if result.Error != nil {
|
||
return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新钱包余额失败")
|
||
}
|
||
if result.RowsAffected == 0 {
|
||
return errors.New(errors.CodeInternalError, "钱包版本冲突,请重试")
|
||
}
|
||
|
||
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.WalletTransaction{
|
||
WalletID: wallet.ID,
|
||
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,
|
||
}
|
||
if err := tx.Create(transaction).Error; 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
|
||
}
|