暂存一下,防止丢失
This commit is contained in:
@@ -2,12 +2,15 @@ package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
walletapp "github.com/break/junhong_cmp_fiber/internal/application/wallet"
|
||||
packagedomain "github.com/break/junhong_cmp_fiber/internal/domain/package"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model"
|
||||
"github.com/break/junhong_cmp_fiber/internal/model/dto"
|
||||
@@ -23,6 +26,7 @@ import (
|
||||
"github.com/break/junhong_cmp_fiber/pkg/payment"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/queue"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/wechat"
|
||||
"github.com/google/uuid"
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
@@ -60,6 +64,18 @@ type Service struct {
|
||||
assetIdentifierStore *postgres.AssetIdentifierStore
|
||||
personalCustomerStore *postgres.PersonalCustomerStore
|
||||
personalCustomerPhoneStore *postgres.PersonalCustomerPhoneStore
|
||||
agentWalletDebit *walletapp.DebitService
|
||||
agentWalletReservation *walletapp.ReservationService
|
||||
}
|
||||
|
||||
// SetAgentWalletDebitService 注入代理主钱包统一扣款用例。
|
||||
func (s *Service) SetAgentWalletDebitService(service *walletapp.DebitService) {
|
||||
s.agentWalletDebit = service
|
||||
}
|
||||
|
||||
// SetAgentWalletReservationService 注入代理主钱包统一预占用例。
|
||||
func (s *Service) SetAgentWalletReservationService(service *walletapp.ReservationService) {
|
||||
s.agentWalletReservation = service
|
||||
}
|
||||
|
||||
func New(
|
||||
@@ -188,16 +204,17 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
||||
}
|
||||
|
||||
carrierType, carrierID := resolveAdminCarrierInfoFromVars(orderType, iotCardID, deviceID)
|
||||
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, orderType, carrierType, carrierID, req.PackageIDs)
|
||||
existingOrderID, lockToken, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, orderType, carrierType, carrierID, req.PackageIDs)
|
||||
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
|
||||
if lockToken != "" {
|
||||
defer s.releaseOrderCreateLock(ctx, lockKey, lockToken)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingOrderID > 0 {
|
||||
return s.Get(ctx, existingOrderID)
|
||||
}
|
||||
// 获取到分布式锁后,确保无论成功还是失败都释放
|
||||
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
// offline 订单不检查强充:平台直接操作,不涉及消费者支付门槛,不产生一次性佣金触发条件
|
||||
if req.PaymentMethod != model.PaymentMethodOffline {
|
||||
@@ -486,10 +503,16 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
|
||||
operatorShopID = *operatorID
|
||||
}
|
||||
buyerShopID := orderBuyerID
|
||||
order.IdempotencyKey = orderIdempotencyDigest(idempotencyKey)
|
||||
|
||||
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
|
||||
existingOrderID, err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingOrderID > 0 {
|
||||
s.markOrderCreated(ctx, idempotencyKey, existingOrderID)
|
||||
return s.Get(ctx, existingOrderID)
|
||||
}
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(ctx, order, items), nil
|
||||
|
||||
@@ -548,16 +571,17 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
|
||||
|
||||
// 幂等性检查:防止同一买家对同一载体短时间内重复下单
|
||||
carrierType, carrierID := resolveCarrierInfo(req)
|
||||
existingOrderID, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
|
||||
existingOrderID, lockToken, err := s.checkOrderIdempotency(ctx, buyerType, buyerID, req.OrderType, carrierType, carrierID, req.PackageIDs)
|
||||
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
|
||||
if lockToken != "" {
|
||||
defer s.releaseOrderCreateLock(ctx, lockKey, lockToken)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingOrderID > 0 {
|
||||
return s.Get(ctx, existingOrderID)
|
||||
}
|
||||
// 获取到分布式锁后,确保无论成功还是失败都释放
|
||||
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
|
||||
defer s.redis.Del(ctx, lockKey)
|
||||
|
||||
forceRechargeCheck := s.checkForceRechargeRequirement(ctx, validationResult)
|
||||
if forceRechargeCheck.NeedForceRecharge && validationResult.TotalPrice < forceRechargeCheck.ForceRechargeAmount {
|
||||
@@ -767,10 +791,16 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
|
||||
}
|
||||
operatorShopID := *operatorID
|
||||
buyerShopID := orderBuyerID
|
||||
order.IdempotencyKey = orderIdempotencyDigest(idempotencyKey)
|
||||
|
||||
if err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID); err != nil {
|
||||
existingOrderID, err := s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if existingOrderID > 0 {
|
||||
s.markOrderCreated(ctx, idempotencyKey, existingOrderID)
|
||||
return s.Get(ctx, existingOrderID)
|
||||
}
|
||||
s.markOrderCreated(ctx, idempotencyKey, order.ID)
|
||||
return s.buildOrderResponse(ctx, order, items), nil
|
||||
|
||||
@@ -989,32 +1019,23 @@ func (s *Service) getCostPrice(ctx context.Context, shopID uint, packageID uint)
|
||||
return allocation.CostPrice, nil
|
||||
}
|
||||
|
||||
// createWalletTransaction 创建钱包流水记录
|
||||
// ctx: 上下文
|
||||
// tx: 事务对象
|
||||
// wallet: 扣款前的钱包快照
|
||||
// order: 订单快照
|
||||
// amount: 扣款金额(正数)
|
||||
// purchaseRole: 订单角色
|
||||
// relatedShopID: 关联店铺ID(代购场景填充下级店铺ID)
|
||||
func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, wallet *model.AgentWallet, order *model.Order, amount int64, purchaseRole string, relatedShopID *uint) error {
|
||||
var subtype *string
|
||||
// debitAgentMainWalletInTx 通过统一 Wallet Application 接缝扣款并写入流水与 Outbox。
|
||||
func (s *Service) debitAgentMainWalletInTx(ctx context.Context, tx *gorm.DB, order *model.Order, shopID uint, amount int64, relatedShopID *uint) error {
|
||||
if s.agentWalletDebit == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理主钱包扣款能力未配置")
|
||||
}
|
||||
subtype := ""
|
||||
remark := "购买套餐"
|
||||
purchaseRole := order.PurchaseRole
|
||||
if purchaseRole == "" {
|
||||
purchaseRole = model.PurchaseRoleSelfPurchase
|
||||
}
|
||||
|
||||
// 根据订单角色确定交易子类型和备注
|
||||
switch purchaseRole {
|
||||
case model.PurchaseRoleSelfPurchase:
|
||||
subtypeVal := constants.WalletTransactionSubtypeSelfPurchase
|
||||
subtype = &subtypeVal
|
||||
|
||||
subtype = constants.WalletTransactionSubtypeSelfPurchase
|
||||
case model.PurchaseRolePurchaseForSubordinate:
|
||||
subtypeVal := constants.WalletTransactionSubtypePurchaseForSubordinate
|
||||
subtype = &subtypeVal
|
||||
|
||||
// 查询下级店铺名称,填充到备注
|
||||
subtype = constants.WalletTransactionSubtypePurchaseForSubordinate
|
||||
if relatedShopID != nil {
|
||||
var shop model.Shop
|
||||
if err := tx.Where("id = ?", *relatedShopID).First(&shop).Error; err == nil {
|
||||
@@ -1027,34 +1048,19 @@ func (s *Service) createWalletTransaction(ctx context.Context, tx *gorm.DB, wall
|
||||
|
||||
userID := middleware.GetUserIDFromContext(ctx)
|
||||
assetType, assetID, assetIdentifier := buildAgentWalletTransactionAssetSnapshot(order)
|
||||
|
||||
// 创建钱包流水记录
|
||||
transaction := &model.AgentWalletTransaction{
|
||||
AgentWalletID: wallet.ID,
|
||||
ShopID: wallet.ShopID,
|
||||
UserID: userID,
|
||||
TransactionType: constants.AgentTransactionTypeDeduct,
|
||||
TransactionSubtype: subtype,
|
||||
Amount: -amount, // 扣款为负数
|
||||
BalanceBefore: wallet.Balance,
|
||||
BalanceAfter: wallet.Balance - amount,
|
||||
Status: constants.TransactionStatusSuccess,
|
||||
ReferenceType: strPtr(constants.ReferenceTypeOrder),
|
||||
ReferenceID: &order.ID,
|
||||
RelatedShopID: relatedShopID,
|
||||
AssetType: assetType,
|
||||
AssetID: assetID,
|
||||
AssetIdentifier: assetIdentifier,
|
||||
Remark: &remark,
|
||||
Creator: userID,
|
||||
ShopIDTag: wallet.ShopIDTag,
|
||||
EnterpriseIDTag: wallet.EnterpriseIDTag,
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
|
||||
if err := tx.Create(transaction).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包流水失败")
|
||||
_, err := s.agentWalletDebit.DebitInTx(ctx, tx, walletapp.DebitCommand{
|
||||
ShopID: shopID, Amount: amount, ReferenceType: constants.ReferenceTypeOrder, ReferenceID: order.ID,
|
||||
UserID: userID, Creator: userID, TransactionSubtype: subtype, RelatedShopID: relatedShopID,
|
||||
AssetType: assetType, AssetID: assetID, AssetIdentifier: assetIdentifier, Remark: remark,
|
||||
RequestID: requestID, CorrelationID: order.OrderNo,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1108,44 +1114,33 @@ func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
// createOrderWithWalletPayment 使用钱包支付创建订单并完成支付
|
||||
// 包含余额检查、扣款、创建流水、激活套餐等操作,在事务中执行
|
||||
// createOrderWithWalletPayment 使用钱包支付创建订单并完成支付。
|
||||
// 订单、扣款流水、Payment、套餐处理与 Outbox 在同一事务提交。
|
||||
// ctx: 上下文
|
||||
// order: 订单对象
|
||||
// items: 订单明细列表
|
||||
// operatorShopID: 操作者店铺ID(扣款的店铺)
|
||||
// buyerShopID: 买家店铺ID(代购场景下级店铺ID)
|
||||
func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model.Order, items []*model.OrderItem, operatorShopID uint, buyerShopID uint) error {
|
||||
func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model.Order, items []*model.OrderItem, operatorShopID uint, buyerShopID uint) (uint, error) {
|
||||
if order.ActualPaidAmount == nil {
|
||||
return errors.New(errors.CodeInternalError, "实际支付金额不能为空")
|
||||
return 0, errors.New(errors.CodeInternalError, "实际支付金额不能为空")
|
||||
}
|
||||
actualAmount := *order.ActualPaidAmount
|
||||
var existingOrderID uint
|
||||
|
||||
// 事务内:加锁读取钱包 + 余额检查 + 创建订单 + 扣款 + 创建流水 + 激活套餐
|
||||
// 使用 FOR UPDATE 悲观锁替代乐观锁,避免并发请求因 version 过期导致误报余额不足
|
||||
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 加锁读取钱包,并发请求排队等待,不会冲突
|
||||
var wallet model.AgentWallet
|
||||
if err := tx.Set("gorm:query_option", "FOR UPDATE").
|
||||
Where("shop_id = ? AND wallet_type = ?", operatorShopID, constants.AgentWalletTypeMain).
|
||||
First(&wallet).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
|
||||
}
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询钱包失败")
|
||||
matchedOrderID, err := lockAndFindRecentWalletOrder(ctx, tx, order.IdempotencyKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 2. 余额检查(加锁后读到的是最新余额,结果准确)
|
||||
if wallet.Balance < actualAmount {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足")
|
||||
if matchedOrderID > 0 {
|
||||
existingOrderID = matchedOrderID
|
||||
return nil
|
||||
}
|
||||
|
||||
// 3. 创建订单
|
||||
if err := tx.Create(order).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败")
|
||||
}
|
||||
|
||||
// 4. 创建订单明细
|
||||
for _, item := range items {
|
||||
item.OrderID = order.ID
|
||||
}
|
||||
@@ -1153,24 +1148,16 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
|
||||
}
|
||||
|
||||
// 5. 扣减钱包余额(FOR UPDATE 已保证串行,无需 version 条件)
|
||||
if err := tx.Model(&wallet).Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance - ?", actualAmount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
}).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "扣减钱包余额失败")
|
||||
}
|
||||
|
||||
// 6. 创建钱包流水
|
||||
var relatedShopID *uint
|
||||
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
|
||||
relatedShopID = &buyerShopID
|
||||
}
|
||||
if err := s.createWalletTransaction(ctx, tx, &wallet, order, actualAmount, order.PurchaseRole, relatedShopID); err != nil {
|
||||
if err := s.debitAgentMainWalletInTx(ctx, tx, order, operatorShopID, actualAmount, relatedShopID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet, actualAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 7. 激活套餐
|
||||
if err := s.activatePackage(ctx, tx, order); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1179,7 +1166,10 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
return 0, err
|
||||
}
|
||||
if existingOrderID > 0 {
|
||||
return existingOrderID, nil
|
||||
}
|
||||
|
||||
// 3. 事务外:所有已支付且适用差价佣金的订单都进入佣金计算
|
||||
@@ -1187,7 +1177,7 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
|
||||
s.enqueueCommissionCalculation(ctx, order.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (s *Service) createOrderWithActivation(ctx context.Context, order *model.Order, items []*model.OrderItem) error {
|
||||
@@ -1480,16 +1470,22 @@ func (s *Service) cancelOrder(ctx context.Context, order *model.Order) error {
|
||||
})
|
||||
}
|
||||
|
||||
// unfreezeWalletForCancel 取消订单时解冻钱包余额
|
||||
// 根据买家类型和订单金额确定解冻金额和目标钱包
|
||||
// unfreezeWalletForCancel 取消订单时解冻钱包余额。
|
||||
// 代理主钱包以预占事实中的钱包和金额为权威,避免代购订单误用买方店铺。
|
||||
func (s *Service) unfreezeWalletForCancel(ctx context.Context, tx *gorm.DB, order *model.Order) error {
|
||||
if order.BuyerType == model.BuyerTypeAgent {
|
||||
// 代理商钱包(店铺钱包)
|
||||
wallet, err := s.agentWalletStore.GetMainWallet(ctx, order.BuyerID)
|
||||
if err != nil {
|
||||
return errors.Wrap(errors.CodeWalletNotFound, err, "查询代理钱包失败")
|
||||
if s.agentWalletReservation == nil {
|
||||
return errors.New(errors.CodeInternalError, "代理主钱包预占能力未配置")
|
||||
}
|
||||
return s.agentWalletStore.UnfreezeBalanceWithTx(ctx, tx, wallet.ID, order.TotalAmount)
|
||||
requestID := ""
|
||||
if value := middleware.GetRequestIDFromContext(ctx); value != nil {
|
||||
requestID = *value
|
||||
}
|
||||
return s.agentWalletReservation.ReleaseInTx(ctx, tx, walletapp.ReservationCommand{
|
||||
ReferenceType: constants.ReferenceTypeOrder, ReferenceID: order.ID,
|
||||
UserID: middleware.GetUserIDFromContext(ctx), Creator: middleware.GetUserIDFromContext(ctx),
|
||||
RequestID: requestID, CorrelationID: order.OrderNo, Remark: "取消订单释放预占资金",
|
||||
})
|
||||
} else if order.BuyerType == model.BuyerTypePersonal {
|
||||
// 个人客户钱包(卡/设备钱包)
|
||||
var resourceType string
|
||||
@@ -1524,16 +1520,25 @@ func (s *Service) unfreezeWalletForCancel(ctx context.Context, tx *gorm.DB, orde
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) createWalletPaymentRecord(tx *gorm.DB, order *model.Order, paymentMethod string) error {
|
||||
func (s *Service) createWalletPaymentRecord(tx *gorm.DB, order *model.Order, paymentMethod string, amount int64) error {
|
||||
paidAt := order.PaidAt
|
||||
if paidAt == nil {
|
||||
now := time.Now()
|
||||
paidAt = &now
|
||||
}
|
||||
payment := &model.Payment{
|
||||
PaymentNo: order.OrderNo,
|
||||
OrderID: order.ID,
|
||||
OrderType: model.PaymentOrderTypePackage,
|
||||
PaymentMethod: paymentMethod,
|
||||
Amount: order.TotalAmount,
|
||||
Amount: amount,
|
||||
Status: model.PaymentRecordStatusPaid,
|
||||
PaidAt: paidAt,
|
||||
}
|
||||
return tx.Create(payment).Error
|
||||
if err := tx.Create(payment).Error; err != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建钱包支付记录失败")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error {
|
||||
@@ -1553,7 +1558,10 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
||||
return errors.New(errors.CodeInvalidStatus, "代购订单无需支付")
|
||||
}
|
||||
|
||||
existingPayment, _ := s.paymentStore.GetByOrderIDAndStatus(ctx, orderID, model.PaymentRecordStatusPaid)
|
||||
existingPayment, err := s.paymentStore.GetByOrderIDAndStatus(ctx, orderID, model.PaymentRecordStatusPaid)
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "查询订单支付记录失败")
|
||||
}
|
||||
if existingPayment != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -1584,19 +1592,6 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
||||
shouldEnqueueCommission := false
|
||||
|
||||
if resourceType == "shop" {
|
||||
// 代理钱包系统(店铺)
|
||||
wallet, err := s.agentWalletStore.GetMainWallet(ctx, resourceID)
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return errors.New(errors.CodeWalletNotFound, "钱包不存在")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if wallet.Balance < order.TotalAmount {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足")
|
||||
}
|
||||
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
result := tx.Model(&model.Order{}).
|
||||
Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
|
||||
@@ -1631,26 +1626,14 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
||||
|
||||
actualPaidAmountSnapshot := actualPaidAmount
|
||||
order.ActualPaidAmount = &actualPaidAmountSnapshot
|
||||
order.PaidAt = &now
|
||||
shouldEnqueueCommission = true
|
||||
|
||||
walletResult := tx.Model(&model.AgentWallet{}).
|
||||
Where("id = ? AND balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
|
||||
Updates(map[string]any{
|
||||
"balance": gorm.Expr("balance - ?", order.TotalAmount),
|
||||
"version": gorm.Expr("version + 1"),
|
||||
})
|
||||
if walletResult.Error != nil {
|
||||
return errors.Wrap(errors.CodeDatabaseError, walletResult.Error, "扣减钱包余额失败")
|
||||
}
|
||||
if walletResult.RowsAffected == 0 {
|
||||
return errors.New(errors.CodeInsufficientBalance, "余额不足或并发冲突")
|
||||
}
|
||||
|
||||
if err := s.createWalletTransaction(ctx, tx, wallet, order, order.TotalAmount, order.PurchaseRole, nil); err != nil {
|
||||
if err := s.debitAgentMainWalletInTx(ctx, tx, order, resourceID, order.TotalAmount, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil {
|
||||
if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet, order.TotalAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1742,7 +1725,7 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
|
||||
return errors.Wrap(errors.CodeDatabaseError, err, "创建扣款流水失败")
|
||||
}
|
||||
|
||||
if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet); err != nil {
|
||||
if err := s.createWalletPaymentRecord(tx, order, model.PaymentByWallet, order.TotalAmount); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2160,14 +2143,24 @@ func buildOrderIdempotencyKey(buyerType string, buyerID uint, orderType string,
|
||||
buyerType, buyerID, orderType, carrierType, carrierID, strings.Join(idStrs, ","))
|
||||
}
|
||||
|
||||
func orderIdempotencyDigest(key string) string {
|
||||
digest := sha256.Sum256([]byte(key))
|
||||
return fmt.Sprintf("%x", digest)
|
||||
}
|
||||
|
||||
func orderIdempotencyAdvisoryKey(digest string) int64 {
|
||||
value := sha256.Sum256([]byte(digest))
|
||||
return int64(binary.BigEndian.Uint64(value[:8]))
|
||||
}
|
||||
|
||||
const (
|
||||
orderIdempotencyTTL = 3 * time.Minute
|
||||
orderCreateLockTTL = 10 * time.Second
|
||||
orderCreateLockTTL = orderIdempotencyTTL
|
||||
)
|
||||
|
||||
// checkOrderIdempotency 检查订单是否已创建(Redis SETNX + 分布式锁)
|
||||
// 返回已存在的 orderID(>0 表示重复请求),或 0 表示可以创续创建
|
||||
func (s *Service) checkOrderIdempotency(ctx context.Context, buyerType string, buyerID uint, orderType string, carrierType string, carrierID uint, packageIDs []uint) (uint, error) {
|
||||
// 返回已存在的 orderID、当前请求持有的锁令牌以及错误。
|
||||
func (s *Service) checkOrderIdempotency(ctx context.Context, buyerType string, buyerID uint, orderType string, carrierType string, carrierID uint, packageIDs []uint) (uint, string, error) {
|
||||
idempotencyKey := buildOrderIdempotencyKey(buyerType, buyerID, orderType, carrierType, carrierID, packageIDs)
|
||||
redisKey := constants.RedisOrderIdempotencyKey(idempotencyKey)
|
||||
|
||||
@@ -2179,21 +2172,22 @@ func (s *Service) checkOrderIdempotency(ctx context.Context, buyerType string, b
|
||||
s.logger.Info("订单幂等性命中,返回已有订单",
|
||||
zap.Uint("order_id", uint(orderID)),
|
||||
zap.String("idempotency_key", idempotencyKey))
|
||||
return uint(orderID), nil
|
||||
return uint(orderID), "", nil
|
||||
}
|
||||
}
|
||||
if err != nil && err != redis.Nil {
|
||||
return 0, "", errors.Wrap(errors.CodeServiceUnavailable, err, "订单幂等缓存不可用")
|
||||
}
|
||||
|
||||
// 第 2 层:分布式锁,防止并发创建
|
||||
lockKey := constants.RedisOrderCreateLockKey(carrierType, carrierID)
|
||||
locked, err := s.redis.SetNX(ctx, lockKey, time.Now().String(), orderCreateLockTTL).Result()
|
||||
lockToken := uuid.NewString()
|
||||
locked, err := s.redis.SetNX(ctx, lockKey, lockToken, orderCreateLockTTL).Result()
|
||||
if err != nil {
|
||||
s.logger.Warn("获取订单创建分布式锁失败,继续执行",
|
||||
zap.Error(err),
|
||||
zap.String("lock_key", lockKey))
|
||||
return 0, nil
|
||||
return 0, "", errors.Wrap(errors.CodeServiceUnavailable, err, "获取订单创建锁失败")
|
||||
}
|
||||
if !locked {
|
||||
return 0, errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交")
|
||||
return 0, "", errors.New(errors.CodeTooManyRequests, "订单正在创建中,请勿重复提交")
|
||||
}
|
||||
|
||||
// 第 3 层:加锁后二次检测,防止锁等待期间已被处理
|
||||
@@ -2204,14 +2198,50 @@ func (s *Service) checkOrderIdempotency(ctx context.Context, buyerType string, b
|
||||
s.logger.Info("订单幂等性二次检测命中",
|
||||
zap.Uint("order_id", uint(orderID)),
|
||||
zap.String("idempotency_key", idempotencyKey))
|
||||
return uint(orderID), nil
|
||||
return uint(orderID), lockToken, nil
|
||||
}
|
||||
}
|
||||
if err != nil && err != redis.Nil {
|
||||
return 0, lockToken, errors.Wrap(errors.CodeServiceUnavailable, err, "复核订单幂等标记失败")
|
||||
}
|
||||
|
||||
return 0, nil
|
||||
return 0, lockToken, nil
|
||||
}
|
||||
|
||||
// markOrderCreated 订单创建成功后标记 Redis 并释放分布式锁
|
||||
// releaseOrderCreateLock 仅释放当前请求持有的订单创建锁。
|
||||
func (s *Service) releaseOrderCreateLock(ctx context.Context, lockKey, lockToken string) {
|
||||
const compareAndDelete = `
|
||||
if redis.call("GET", KEYS[1]) == ARGV[1] then
|
||||
return redis.call("DEL", KEYS[1])
|
||||
end
|
||||
return 0`
|
||||
if err := s.redis.Eval(ctx, compareAndDelete, []string{lockKey}, lockToken).Err(); err != nil {
|
||||
s.logger.Warn("释放订单创建锁失败", zap.String("lock_key", lockKey), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
func lockAndFindRecentWalletOrder(ctx context.Context, tx *gorm.DB, idempotencyKey string) (uint, error) {
|
||||
if idempotencyKey == "" {
|
||||
return 0, errors.New(errors.CodeInternalError, "钱包订单缺少持久化幂等指纹")
|
||||
}
|
||||
if err := tx.WithContext(ctx).Exec("SELECT pg_advisory_xact_lock(?)", orderIdempotencyAdvisoryKey(idempotencyKey)).Error; err != nil {
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "锁定钱包订单幂等指纹失败")
|
||||
}
|
||||
var existing model.Order
|
||||
err := tx.WithContext(ctx).
|
||||
Where("idempotency_key = ? AND created_at >= CURRENT_TIMESTAMP - (? * INTERVAL '1 second')",
|
||||
idempotencyKey, int64(orderIdempotencyTTL/time.Second)).
|
||||
Order("id DESC").First(&existing).Error
|
||||
if err == nil {
|
||||
return existing.ID, nil
|
||||
}
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, errors.Wrap(errors.CodeDatabaseError, err, "查询近期重复钱包订单失败")
|
||||
}
|
||||
|
||||
// markOrderCreated 在订单事务提交后写入 Redis 快速幂等标记。
|
||||
func (s *Service) markOrderCreated(ctx context.Context, idempotencyKey string, orderID uint) {
|
||||
redisKey := constants.RedisOrderIdempotencyKey(idempotencyKey)
|
||||
if err := s.redis.Set(ctx, redisKey, strconv.FormatUint(uint64(orderID), 10), orderIdempotencyTTL).Err(); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user