feat: 归档佣金计算触发和快照变更,同步规范文档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m40s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m40s
- 归档 OpenSpec 变更到 archive 目录 - 创建 2 个新的主规范文件:commission-trigger 和 order-commission-snapshot - 实现订单佣金快照字段和支付自动触发 - 确保事务一致性,所有佣金操作在同一事务内完成 - 提取成本价计算为公共工具函数
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
@@ -82,26 +83,25 @@ func (s *Service) CalculateCommission(ctx context.Context, orderID uint) error {
|
||||
}
|
||||
|
||||
for _, record := range costDiffRecords {
|
||||
if err := s.commissionRecordStore.Create(ctx, record); err != nil {
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建成本价差佣金记录失败")
|
||||
}
|
||||
if err := s.CreditCommission(ctx, record); err != nil {
|
||||
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.TriggerOneTimeCommissionForCard(ctx, order, *order.IotCardID); err != 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.TriggerOneTimeCommissionForDevice(ctx, order, *order.DeviceID); err != nil {
|
||||
if err := s.triggerOneTimeCommissionForDeviceInTx(ctx, tx, order, *order.DeviceID); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "触发设备一次性佣金失败")
|
||||
}
|
||||
}
|
||||
|
||||
order.CommissionStatus = model.CommissionStatusCalculated
|
||||
if err := s.orderStore.Update(ctx, order); err != nil {
|
||||
if err := tx.Model(&model.Order{}).Where("id = ?", orderID).Update("commission_status", model.CommissionStatusCalculated).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "更新订单佣金状态失败")
|
||||
}
|
||||
|
||||
@@ -116,6 +116,11 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
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, "获取销售店铺失败")
|
||||
@@ -180,16 +185,10 @@ func (s *Service) CalculateCostDiffCommission(ctx context.Context, order *model.
|
||||
}
|
||||
|
||||
func (s *Service) calculateCostPrice(allocation *model.ShopSeriesAllocation, orderAmount int64) int64 {
|
||||
if allocation.BaseCommissionMode == model.CommissionModeFixed {
|
||||
return orderAmount - allocation.BaseCommissionValue
|
||||
} else if allocation.BaseCommissionMode == model.CommissionModePercent {
|
||||
commission := orderAmount * allocation.BaseCommissionValue / 1000
|
||||
return orderAmount - commission
|
||||
}
|
||||
return orderAmount
|
||||
return utils.CalculateCostPrice(allocation, orderAmount)
|
||||
}
|
||||
|
||||
func (s *Service) TriggerOneTimeCommissionForCard(ctx context.Context, order *model.Order, cardID uint) error {
|
||||
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, "获取卡信息失败")
|
||||
@@ -253,26 +252,32 @@ func (s *Service) TriggerOneTimeCommissionForCard(ctx context.Context, order *mo
|
||||
Remark: "一次性佣金",
|
||||
}
|
||||
|
||||
if err := s.commissionRecordStore.Create(ctx, record); err != nil {
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
|
||||
}
|
||||
|
||||
if err := s.CreditCommission(ctx, record); err != nil {
|
||||
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||
}
|
||||
|
||||
card.FirstCommissionPaid = true
|
||||
updates := map[string]any{"first_commission_paid": true}
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||
card.AccumulatedRecharge = rechargeAmount
|
||||
updates["accumulated_recharge"] = rechargeAmount
|
||||
}
|
||||
if err := s.iotCardStore.Update(ctx, card); err != nil {
|
||||
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) TriggerOneTimeCommissionForDevice(ctx context.Context, order *model.Order, deviceID uint) error {
|
||||
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, "获取设备信息失败")
|
||||
@@ -336,25 +341,31 @@ func (s *Service) TriggerOneTimeCommissionForDevice(ctx context.Context, order *
|
||||
Remark: "一次性佣金(设备)",
|
||||
}
|
||||
|
||||
if err := s.commissionRecordStore.Create(ctx, record); err != nil {
|
||||
if err := tx.Create(record).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建一次性佣金记录失败")
|
||||
}
|
||||
|
||||
if err := s.CreditCommission(ctx, record); err != nil {
|
||||
if err := s.creditCommissionInTx(ctx, tx, record); err != nil {
|
||||
return errors.Wrap(errors.CodeInternalError, err, "一次性佣金入账失败")
|
||||
}
|
||||
|
||||
device.FirstCommissionPaid = true
|
||||
updates := map[string]any{"first_commission_paid": true}
|
||||
if allocation.OneTimeCommissionTrigger == model.OneTimeCommissionTriggerAccumulatedRecharge {
|
||||
device.AccumulatedRecharge = rechargeAmount
|
||||
updates["accumulated_recharge"] = rechargeAmount
|
||||
}
|
||||
if err := s.deviceStore.Update(ctx, device); err != nil {
|
||||
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:
|
||||
@@ -425,22 +436,32 @@ func (s *Service) calculateTieredCommission(ctx context.Context, allocationID ui
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) CreditCommission(ctx context.Context, record *model.CommissionRecord) error {
|
||||
wallet, err := s.walletStore.GetByResourceTypeAndID(ctx, "shop", record.ShopID, "commission")
|
||||
if err != 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, "获取店铺钱包失败")
|
||||
}
|
||||
|
||||
wallet.Balance += record.Amount
|
||||
if err := s.db.WithContext(ctx).Model(wallet).Update("balance", wallet.Balance).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()
|
||||
record.BalanceAfter = wallet.Balance
|
||||
record.Status = model.CommissionStatusReleased
|
||||
record.ReleasedAt = &now
|
||||
if err := s.db.WithContext(ctx).Model(record).Updates(record).Error; err != nil {
|
||||
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, "更新佣金记录失败")
|
||||
}
|
||||
|
||||
@@ -450,21 +471,27 @@ func (s *Service) CreditCommission(ctx context.Context, record *model.Commission
|
||||
UserID: record.Creator,
|
||||
TransactionType: "commission",
|
||||
Amount: record.Amount,
|
||||
BalanceBefore: wallet.Balance - record.Amount,
|
||||
BalanceAfter: wallet.Balance,
|
||||
BalanceBefore: balanceBefore,
|
||||
BalanceAfter: balanceBefore + record.Amount,
|
||||
Status: 1,
|
||||
ReferenceType: stringPtr("commission"),
|
||||
ReferenceID: &record.ID,
|
||||
Remark: &remark,
|
||||
Creator: record.Creator,
|
||||
}
|
||||
if err := s.walletTransactionStore.Create(ctx, transaction); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -12,6 +12,10 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/middleware"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/utils"
|
||||
"github.com/bytedance/sonic"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -22,6 +26,8 @@ type Service struct {
|
||||
walletStore *postgres.WalletStore
|
||||
purchaseValidationService *purchase_validation.Service
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore
|
||||
queueClient *queue.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -31,6 +37,8 @@ func New(
|
||||
walletStore *postgres.WalletStore,
|
||||
purchaseValidationService *purchase_validation.Service,
|
||||
allocationConfigStore *postgres.ShopSeriesAllocationConfigStore,
|
||||
queueClient *queue.Client,
|
||||
logger *zap.Logger,
|
||||
) *Service {
|
||||
return &Service{
|
||||
db: db,
|
||||
@@ -39,6 +47,8 @@ func New(
|
||||
walletStore: walletStore,
|
||||
purchaseValidationService: purchaseValidationService,
|
||||
allocationConfigStore: allocationConfigStore,
|
||||
queueClient: queueClient,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +77,16 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
configVersion := s.snapshotCommissionConfig(ctx, validationResult.Allocation.ID)
|
||||
|
||||
var seriesID *uint
|
||||
var sellerShopID *uint
|
||||
var sellerCostPrice int64
|
||||
|
||||
if validationResult.Allocation != nil {
|
||||
seriesID = &validationResult.Allocation.SeriesID
|
||||
sellerShopID = &validationResult.Allocation.ShopID
|
||||
sellerCostPrice = utils.CalculateCostPrice(validationResult.Allocation, validationResult.TotalPrice)
|
||||
}
|
||||
|
||||
order := &model.Order{
|
||||
BaseModel: model.BaseModel{
|
||||
Creator: userID,
|
||||
@@ -82,6 +102,9 @@ func (s *Service) Create(ctx context.Context, req *dto.CreateOrderRequest, buyer
|
||||
PaymentStatus: model.PaymentStatusPending,
|
||||
CommissionStatus: model.CommissionStatusPending,
|
||||
CommissionConfigVersion: configVersion,
|
||||
SeriesID: seriesID,
|
||||
SellerShopID: sellerShopID,
|
||||
SellerCostPrice: sellerCostPrice,
|
||||
}
|
||||
|
||||
var items []*model.OrderItem
|
||||
@@ -264,7 +287,7 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.Wallet{}).
|
||||
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
@@ -288,6 +311,13 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
||||
|
||||
return s.activatePackage(ctx, tx, order)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.enqueueCommissionCalculation(ctx, orderID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, paymentMethod string) error {
|
||||
@@ -308,7 +338,7 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Model(&model.Order{}).Where("id = ?", order.ID).Updates(map[string]any{
|
||||
"payment_status": model.PaymentStatusPaid,
|
||||
"payment_method": paymentMethod,
|
||||
@@ -319,6 +349,13 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
|
||||
|
||||
return s.activatePackage(ctx, tx, order)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.enqueueCommissionCalculation(ctx, order.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) activatePackage(ctx context.Context, tx *gorm.DB, order *model.Order) error {
|
||||
@@ -373,6 +410,37 @@ func (s *Service) snapshotCommissionConfig(ctx context.Context, allocationID uin
|
||||
return config.Version
|
||||
}
|
||||
|
||||
func (s *Service) enqueueCommissionCalculation(ctx context.Context, orderID uint) {
|
||||
if s.queueClient == nil {
|
||||
s.logger.Warn("队列客户端未初始化,跳过佣金计算任务入队", zap.Uint("order_id", orderID))
|
||||
return
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"order_id": orderID,
|
||||
}
|
||||
|
||||
payloadBytes, err := sonic.Marshal(payload)
|
||||
if err != nil {
|
||||
s.logger.Error("佣金计算任务载荷序列化失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.queueClient.EnqueueTask(ctx, constants.TaskTypeCommission, payloadBytes); err != nil {
|
||||
s.logger.Error("佣金计算任务入队失败",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.Error(err),
|
||||
zap.String("task_type", constants.TaskTypeCommission))
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Info("佣金计算任务已入队",
|
||||
zap.Uint("order_id", orderID),
|
||||
zap.String("task_type", constants.TaskTypeCommission))
|
||||
}
|
||||
|
||||
func (s *Service) buildOrderResponse(order *model.Order, items []*model.OrderItem) *dto.OrderResponse {
|
||||
var itemResponses []*dto.OrderItemResponse
|
||||
for _, item := range items {
|
||||
|
||||
@@ -124,7 +124,7 @@ func setupOrderTestEnv(t *testing.T) *testEnv {
|
||||
require.NoError(t, tx.Create(wallet).Error)
|
||||
|
||||
purchaseValidationSvc := purchase_validation.New(tx, iotCardStore, deviceStore, packageStore, seriesAllocationStore)
|
||||
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil)
|
||||
orderSvc := New(tx, orderStore, orderItemStore, walletStore, purchaseValidationSvc, nil, nil, nil)
|
||||
|
||||
userCtx := middleware.SetUserContext(ctx, &middleware.UserContextInfo{
|
||||
UserID: 1,
|
||||
|
||||
Reference in New Issue
Block a user