收口审计治理与套餐任务进展

Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展
Confidence: medium
Scope-risk: broad
Directive: 后续修改需保持审计事件与业务事务边界一致
Tested: git diff --cached --check
Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
This commit is contained in:
2026-08-05 14:30:54 +08:00
parent b3499adfca
commit 5e552d99bc
178 changed files with 16797 additions and 5674 deletions

View File

@@ -13,6 +13,8 @@ import (
cardObservationApp "github.com/break/junhong_cmp_fiber/internal/application/cardobservation"
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/infrastructure/audit"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/integrationlog"
"github.com/break/junhong_cmp_fiber/internal/model"
"github.com/break/junhong_cmp_fiber/internal/model/dto"
packagepkg "github.com/break/junhong_cmp_fiber/internal/service/package"
@@ -20,6 +22,7 @@ import (
"github.com/break/junhong_cmp_fiber/internal/service/purchase_validation"
"github.com/break/junhong_cmp_fiber/internal/store"
"github.com/break/junhong_cmp_fiber/internal/store/postgres"
"github.com/break/junhong_cmp_fiber/pkg/auditcontext"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
"github.com/break/junhong_cmp_fiber/pkg/fuiou"
@@ -68,6 +71,8 @@ type Service struct {
agentWalletDebit *walletapp.DebitService
agentWalletReservation *walletapp.ReservationService
observationSeriesEvents cardObservationApp.SeriesEventWriter
auditWriter *audit.Writer
paymentIntegration *integrationlog.Repository
}
// SetObservationSeriesEventWriter 注入购包成功观测序列 Outbox Writer。
@@ -145,7 +150,7 @@ func (s *Service) SetResumeCallback(callback packagepkg.ResumeCallback) {
// CreateAdminOrder 后台订单创建(仅支持 wallet/offline立即扣款或激活
// 与 CreateH5Order 的核心区别后台订单不创建待支付状态wallet 立即扣款offline 立即激活
// POST /api/admin/orders
func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrderRequest, buyerType string, buyerID uint) (resp *dto.OrderResponse, err error) {
resolvedCard, resolvedDevice, resolveErr := s.resolveAssetByIdentifier(ctx, req.Identifier)
if resolveErr != nil {
return nil, resolveErr
@@ -164,9 +169,16 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
deviceID = &resolvedDevice.ID
resourceShopID = resolvedDevice.ShopID
}
auditOrder := &model.Order{
OrderNo: "create:" + orderType + ":" + req.Identifier, OrderType: orderType,
BuyerType: buyerType, BuyerID: buyerID, IotCardID: iotCardID, DeviceID: deviceID,
AssetIdentifier: req.Identifier, PaymentMethod: req.PaymentMethod,
}
defer func() {
s.recordOrderFailure(ctx, constants.AuditActionOrderCreated, "创建订单失败", auditOrder, err)
}()
var validationResult *purchase_validation.PurchaseValidationResult
var err error
operatorUserType := middleware.GetUserTypeFromContext(ctx)
validationCtx := ctx
if req.PaymentMethod == model.PaymentMethodOffline && (operatorUserType == constants.UserTypeSuperAdmin || operatorUserType == constants.UserTypePlatform) {
@@ -477,6 +489,7 @@ func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrde
PurchaseRole: purchaseRole,
PaymentConfigID: paymentConfigID,
}
auditOrder = order
// 线下支付订单写入支付凭证 file_key 列表
if req.PaymentMethod == model.PaymentMethodOffline {
@@ -549,9 +562,16 @@ func rewriteAdminAgentPackageOffShelfError(err error, resourceShopID *uint, shou
// CreateH5Order H5 端订单创建(支持 wallet/wechat/alipay支持待支付状态
// 保留原 Create() 方法的完整逻辑H5 端行为不变
// POST /api/h5/orders
func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (resp *dto.OrderResponse, err error) {
var validationResult *purchase_validation.PurchaseValidationResult
var err error
auditOrder := &model.Order{
OrderNo: "create:" + req.OrderType + ":" + strconv.FormatUint(uint64(buyerID), 10),
OrderType: req.OrderType, BuyerType: buyerType, BuyerID: buyerID,
IotCardID: req.IotCardID, DeviceID: req.DeviceID, PaymentMethod: req.PaymentMethod,
}
defer func() {
s.recordOrderFailure(ctx, constants.AuditActionOrderCreated, "创建订单失败", auditOrder, err)
}()
if req.OrderType == model.OrderTypeSingleCard {
if req.IotCardID == nil {
@@ -776,6 +796,7 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
ExpiresAt: expiresAt,
PaymentConfigID: h5PaymentConfigID,
}
auditOrder = order
items := s.buildOrderItems(userID, validationResult.Packages, itemUnitPriceMap, itemCostPriceMap)
@@ -816,7 +837,7 @@ func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest
// 待支付订单设置过期时间,超过 30 分钟未支付则自动取消
expireTime := now.Add(constants.OrderExpireTimeout)
order.ExpiresAt = &expireTime
if err := s.orderStore.Create(ctx, order, items); err != nil {
if err := s.CreatePendingOrder(ctx, order, items); err != nil {
return nil, err
}
s.markOrderCreated(ctx, idempotencyKey, order.ID)
@@ -1134,6 +1155,7 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
}
actualAmount := *order.ActualPaidAmount
var existingOrderID uint
walletDebitAttempted := false
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
matchedOrderID, err := lockAndFindRecentWalletOrder(ctx, tx, order.IdempotencyKey)
@@ -1159,6 +1181,7 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
if order.PurchaseRole == model.PurchaseRolePurchaseForSubordinate {
relatedShopID = &buyerShopID
}
walletDebitAttempted = true
if err := s.debitAgentMainWalletInTx(ctx, tx, order, operatorShopID, actualAmount, relatedShopID); err != nil {
return err
}
@@ -1168,11 +1191,17 @@ func (s *Service) createOrderWithWalletPayment(ctx context.Context, order *model
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
if err := s.appendOrderAudit(ctx, tx, constants.AuditActionOrderCreated, "创建并使用钱包支付订单", order, nil, orderStateData(order)); err != nil {
return err
}
return nil
})
if err != nil {
if walletDebitAttempted {
s.recordAgentWalletOrderFailure(ctx, constants.AuditActionAgentWalletOrderDebited, "代理主钱包订单扣款失败", order, operatorShopID, err)
}
return 0, err
}
if existingOrderID > 0 {
@@ -1200,7 +1229,29 @@ func (s *Service) createOrderWithActivation(ctx context.Context, order *model.Or
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
return s.activatePackage(ctx, tx, order)
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
return s.appendOrderAudit(ctx, tx, constants.AuditActionOrderCreated, "创建并完成订单", order, nil, orderStateData(order))
})
}
// CreatePendingOrder 在同一事务中创建待支付订单、冻结个人资产钱包、创建明细和审计事件。
func (s *Service) CreatePendingOrder(ctx context.Context, order *model.Order, items []*model.OrderItem) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := s.freezeAssetWalletForOrder(ctx, tx, order); err != nil {
return err
}
if err := tx.Create(order).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单失败")
}
for _, item := range items {
item.OrderID = order.ID
if err := tx.Create(item).Error; err != nil {
return errors.Wrap(errors.CodeDatabaseError, err, "创建订单明细失败")
}
}
return s.appendOrderAudit(ctx, tx, constants.AuditActionOrderCreated, "创建待支付订单", order, nil, orderStateData(order))
})
}
@@ -1394,14 +1445,19 @@ func (s *Service) Cancel(ctx context.Context, id uint, buyerType string, buyerID
}
if order.BuyerType != buyerType || order.BuyerID != buyerID {
return errors.New(errors.CodeForbidden, "无权操作此订单")
err = errors.New(errors.CodeForbidden, "无权操作此订单")
s.recordOrderFailure(ctx, constants.AuditActionOrderCancelled, "取消订单被拒绝", order, err)
return err
}
if order.PaymentStatus != model.PaymentStatusPending {
return errors.New(errors.CodeInvalidStatus, "只能取消待支付的订单")
err = errors.New(errors.CodeInvalidStatus, "只能取消待支付的订单")
s.recordOrderFailure(ctx, constants.AuditActionOrderCancelled, "取消订单被拒绝", order, err)
return err
}
return s.cancelOrder(ctx, order)
_, err = s.cancelOrder(ctx, order, constants.AuditActionOrderCancelled)
return err
}
// CancelExpiredOrders 批量取消已超时的待支付订单
@@ -1420,7 +1476,8 @@ func (s *Service) CancelExpiredOrders(ctx context.Context) (int, error) {
cancelledCount := 0
for _, order := range orders {
if err := s.cancelOrder(ctx, order); err != nil {
orderCtx := auditcontext.With(ctx, auditcontext.Context{CorrelationID: order.OrderNo})
if _, err := s.cancelOrder(orderCtx, order, constants.AuditActionOrderExpiredClosed); err != nil {
s.logger.Error("自动取消超时订单失败",
zap.Uint("order_id", order.ID),
zap.String("order_no", order.OrderNo),
@@ -1443,8 +1500,10 @@ func (s *Service) CancelExpiredOrders(ctx context.Context) (int, error) {
// cancelOrder 内部取消订单逻辑(共用于手动取消和自动超时取消)
// 在事务中执行:更新订单状态为已取消、清除过期时间、解冻钱包余额(如有)
func (s *Service) cancelOrder(ctx context.Context, order *model.Order) error {
return s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
func (s *Service) cancelOrder(ctx context.Context, order *model.Order, actionCode string) (bool, error) {
changed := false
walletReleaseAttempted := false
err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 使用条件更新确保幂等性:只有待支付的订单才能取消
result := tx.Model(&model.Order{}).
Where("id = ? AND payment_status = ?", order.ID, model.PaymentStatusPending).
@@ -1459,11 +1518,11 @@ func (s *Service) cancelOrder(ctx context.Context, order *model.Order) error {
// 订单已被处理(幂等),直接返回
return nil
}
changed = true
// 检查是否需要解冻钱包余额(混合支付场景)
// 当前系统中钱包支付订单是立即支付的,不会进入待支付状态
// 此处为预留逻辑,支持未来混合支付场景的钱包解冻
// 待支付钱包订单取消时释放该订单创建时记录的资金预占。
if order.PaymentMethod == model.PaymentMethodWallet {
walletReleaseAttempted = order.BuyerType == model.BuyerTypeAgent
if err := s.unfreezeWalletForCancel(ctx, tx, order); err != nil {
s.logger.Error("取消订单时解冻钱包失败",
zap.Uint("order_id", order.ID),
@@ -1473,8 +1532,22 @@ func (s *Service) cancelOrder(ctx context.Context, order *model.Order) error {
}
}
return nil
after := *order
after.PaymentStatus = model.PaymentStatusCancelled
after.ExpiresAt = nil
summary := "取消待支付订单"
if actionCode == constants.AuditActionOrderExpiredClosed {
summary = "关闭过期待支付订单"
}
return s.appendOrderAudit(ctx, tx, actionCode, summary, &after, orderStateData(order), orderStateData(&after))
})
if err != nil {
s.recordOrderFailure(ctx, actionCode, "订单关闭失败", order, err)
if walletReleaseAttempted {
s.recordAgentWalletOrderFailure(ctx, constants.AuditActionAgentWalletOrderReleased, "释放代理主钱包订单预占失败", order, 0, err)
}
}
return changed, err
}
// unfreezeWalletForCancel 取消订单时解冻钱包余额。
@@ -1494,39 +1567,83 @@ func (s *Service) unfreezeWalletForCancel(ctx context.Context, tx *gorm.DB, orde
RequestID: requestID, CorrelationID: order.OrderNo, Remark: "取消订单释放预占资金",
})
} else if order.BuyerType == model.BuyerTypePersonal {
// 个人客户钱包(卡/设备钱包)
var resourceType string
var resourceID uint
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
resourceType = "iot_card"
resourceID = *order.IotCardID
} else if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
resourceType = "device"
resourceID = *order.DeviceID
} else {
return errors.New(errors.CodeInternalError, "无法确定钱包归属")
}
wallet, err := s.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, resourceID)
if err != nil {
return errors.Wrap(errors.CodeWalletNotFound, err, "查询资产钱包失败")
}
// 资产钱包解冻:直接减少冻结余额
result := tx.Model(&model.AssetWallet{}).
Where("id = ? AND frozen_balance >= ?", wallet.ID, order.TotalAmount).
Updates(map[string]any{
"frozen_balance": gorm.Expr("frozen_balance - ?", order.TotalAmount),
})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "冻结余额不足,无法解冻")
}
return nil
return s.releaseAssetWalletReservation(ctx, tx, order)
}
return nil
}
// freezeAssetWalletForOrder 为个人待支付钱包订单冻结精确金额,并把钱包与金额写入订单快照。
func (s *Service) freezeAssetWalletForOrder(ctx context.Context, tx *gorm.DB, order *model.Order) error {
if order == nil || order.BuyerType != model.BuyerTypePersonal || order.PaymentMethod != model.PaymentMethodWallet || order.TotalAmount == 0 {
return nil
}
if order.TotalAmount < 0 {
return errors.New(errors.CodeInvalidParam, "订单金额无效")
}
resourceType, resourceID, err := resolveOrderAssetWalletReference(order)
if err != nil {
return err
}
var wallet model.AssetWallet
if err := tx.WithContext(ctx).Where("resource_type = ? AND resource_id = ?", resourceType, resourceID).First(&wallet).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return errors.New(errors.CodeWalletNotFound, "资产钱包不存在")
}
return errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
}
result := tx.WithContext(ctx).Model(&model.AssetWallet{}).
Where("id = ? AND balance - frozen_balance >= ? AND version = ?", wallet.ID, order.TotalAmount, wallet.Version).
Updates(map[string]any{
"frozen_balance": gorm.Expr("frozen_balance + ?", order.TotalAmount),
"version": gorm.Expr("version + 1"),
"updated_at": time.Now(),
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "冻结资产钱包余额失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeInsufficientBalance, "钱包可用余额不足或并发冲突")
}
order.AssetWalletReservationWalletID = &wallet.ID
order.AssetWalletReservedAmount = order.TotalAmount
return nil
}
// releaseAssetWalletReservation 释放订单快照指向的个人资产钱包预占。
// 历史订单没有预占快照,取消时只更新订单状态,避免误释放其他订单的冻结资金。
func (s *Service) releaseAssetWalletReservation(ctx context.Context, tx *gorm.DB, order *model.Order) error {
if order.AssetWalletReservationWalletID == nil && order.AssetWalletReservedAmount == 0 {
return nil
}
if order.AssetWalletReservationWalletID == nil || order.AssetWalletReservedAmount <= 0 {
return errors.New(errors.CodeInternalError, "资产钱包预占快照不完整")
}
result := tx.WithContext(ctx).Model(&model.AssetWallet{}).
Where("id = ? AND frozen_balance >= ?", *order.AssetWalletReservationWalletID, order.AssetWalletReservedAmount).
Updates(map[string]any{
"frozen_balance": gorm.Expr("frozen_balance - ?", order.AssetWalletReservedAmount),
"version": gorm.Expr("version + 1"),
"updated_at": time.Now(),
})
if result.Error != nil {
return errors.Wrap(errors.CodeDatabaseError, result.Error, "释放资产钱包预占失败")
}
if result.RowsAffected == 0 {
return errors.New(errors.CodeConflict, "资产钱包预占状态不一致")
}
return nil
}
func resolveOrderAssetWalletReference(order *model.Order) (string, uint, error) {
if order.OrderType == model.OrderTypeSingleCard && order.IotCardID != nil {
return constants.AssetWalletResourceTypeIotCard, *order.IotCardID, nil
}
if order.OrderType == model.OrderTypeDevice && order.DeviceID != nil {
return constants.AssetWalletResourceTypeDevice, *order.DeviceID, nil
}
return "", 0, errors.New(errors.CodeInternalError, "无法确定资产钱包归属")
}
func (s *Service) createWalletPaymentRecord(tx *gorm.DB, order *model.Order, paymentMethod string, amount int64) error {
paidAt := order.PaidAt
if paidAt == nil {
@@ -1548,7 +1665,7 @@ func (s *Service) createWalletPaymentRecord(tx *gorm.DB, order *model.Order, pay
return nil
}
func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) error {
func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string, buyerID uint) (err error) {
order, err := s.orderStore.GetByID(ctx, orderID)
if err != nil {
if err == gorm.ErrRecordNotFound {
@@ -1556,6 +1673,13 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
}
return err
}
agentWalletDebitAttempted := false
defer func() {
s.recordOrderFailure(ctx, constants.AuditActionOrderWalletPaid, "钱包支付订单失败", order, err)
if agentWalletDebitAttempted {
s.recordAgentWalletOrderFailure(ctx, constants.AuditActionAgentWalletOrderDebited, "代理主钱包订单扣款失败", order, buyerID, err)
}
}()
if order.BuyerType != buyerType || order.BuyerID != buyerID {
return errors.New(errors.CodeForbidden, "无权操作此订单")
@@ -1636,6 +1760,7 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
order.PaidAt = &now
shouldEnqueueCommission = true
agentWalletDebitAttempted = true
if err := s.debitAgentMainWalletInTx(ctx, tx, order, resourceID, order.TotalAmount, nil); err != nil {
return err
}
@@ -1644,22 +1769,18 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
return err
}
return s.activatePackage(ctx, tx, order)
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
after := *order
after.PaymentStatus = model.PaymentStatusPaid
after.PaymentMethod = model.PaymentMethodWallet
after.PaidAt = &now
after.ExpiresAt = nil
return s.appendOrderAudit(ctx, tx, constants.AuditActionOrderWalletPaid, "使用代理钱包支付订单", &after, orderStateData(order), orderStateData(&after))
})
} else {
// 资产钱包系统iot_card 或 device
wallet, err := s.assetWalletStore.GetByResourceTypeAndID(ctx, resourceType, 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).
@@ -1695,22 +1816,12 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
actualPaidAmountSnapshot := actualPaidAmount
order.ActualPaidAmount = &actualPaidAmountSnapshot
// 扣款前记录余额快照,用于写入流水
wallet, err := s.deductAssetWalletForOrder(ctx, tx, order, resourceType, resourceID)
if err != nil {
return err
}
balanceBefore := wallet.Balance
walletResult := tx.Model(&model.AssetWallet{}).
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, "余额不足或并发冲突")
}
// 扣款成功后补写扣款流水,填补流水表中扣款记录缺失的问题
deductTx := &model.AssetWalletTransaction{
AssetWalletID: wallet.ID,
@@ -1736,7 +1847,15 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
return err
}
return s.activatePackage(ctx, tx, order)
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
after := *order
after.PaymentStatus = model.PaymentStatusPaid
after.PaymentMethod = model.PaymentMethodWallet
after.PaidAt = &now
after.ExpiresAt = nil
return s.appendOrderAudit(ctx, tx, constants.AuditActionOrderWalletPaid, "使用资产钱包支付订单", &after, orderStateData(order), orderStateData(&after))
})
}
@@ -1750,6 +1869,47 @@ func (s *Service) WalletPay(ctx context.Context, orderID uint, buyerType string,
return nil
}
// deductAssetWalletForOrder 完成个人钱包订单扣款;新订单同时核销冻结额,历史订单只使用可用余额。
func (s *Service) deductAssetWalletForOrder(ctx context.Context, tx *gorm.DB, order *model.Order, resourceType string, resourceID uint) (*model.AssetWallet, error) {
var wallet model.AssetWallet
query := tx.WithContext(ctx)
if order.AssetWalletReservationWalletID != nil || order.AssetWalletReservedAmount != 0 {
if order.AssetWalletReservationWalletID == nil || order.AssetWalletReservedAmount != order.TotalAmount || order.AssetWalletReservedAmount <= 0 {
return nil, errors.New(errors.CodeConflict, "资产钱包预占快照与订单金额不一致")
}
query = query.Where("id = ? AND resource_type = ? AND resource_id = ?", *order.AssetWalletReservationWalletID, resourceType, resourceID)
} else {
query = query.Where("resource_type = ? AND resource_id = ?", resourceType, resourceID)
}
if err := query.First(&wallet).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, errors.New(errors.CodeWalletNotFound, "资产钱包不存在")
}
return nil, errors.Wrap(errors.CodeDatabaseError, err, "查询资产钱包失败")
}
updates := map[string]any{
"balance": gorm.Expr("balance - ?", order.TotalAmount),
"version": gorm.Expr("version + 1"),
"updated_at": time.Now(),
}
updateQuery := tx.WithContext(ctx).Model(&model.AssetWallet{}).Where("id = ? AND version = ?", wallet.ID, wallet.Version)
if order.AssetWalletReservedAmount > 0 {
updateQuery = updateQuery.Where("balance >= ? AND frozen_balance >= ?", order.TotalAmount, order.AssetWalletReservedAmount)
updates["frozen_balance"] = gorm.Expr("frozen_balance - ?", order.AssetWalletReservedAmount)
} else {
updateQuery = updateQuery.Where("balance - frozen_balance >= ?", order.TotalAmount)
}
result := updateQuery.Updates(updates)
if result.Error != nil {
return nil, errors.Wrap(errors.CodeDatabaseError, result.Error, "扣减资产钱包余额失败")
}
if result.RowsAffected == 0 {
return nil, errors.New(errors.CodeInsufficientBalance, "钱包可用余额不足或并发冲突")
}
return &wallet, nil
}
func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, paymentMethod string, actualPaidAmount int64) error {
order, err := s.orderStore.GetByOrderNo(ctx, orderNo)
if err != nil {
@@ -1760,6 +1920,7 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
}
now := time.Now()
beforeOrder := *order
shouldResumeAfterPayment := false
shouldEnqueueCommission := false
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -1800,7 +1961,15 @@ func (s *Service) HandlePaymentCallback(ctx context.Context, orderNo string, pay
shouldResumeAfterPayment = true
shouldEnqueueCommission = true
return s.activatePackage(ctx, tx, order)
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
after := *order
after.PaymentStatus = model.PaymentStatusPaid
after.PaymentMethod = paymentMethod
after.PaidAt = &now
after.ExpiresAt = nil
return s.appendOrderAudit(ctx, tx, constants.AuditActionOrderOnlinePaid, "第三方支付确认订单已支付", &after, orderStateData(&beforeOrder), orderStateData(&after))
})
if err != nil {
@@ -1838,6 +2007,8 @@ func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo str
}
now := time.Now()
beforePayment := *payment
beforeOrder := *order
shouldResumeAfterPayment := false
shouldEnqueueCommission := false
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
@@ -1900,7 +2071,23 @@ func (s *Service) HandlePaymentRecordCallback(ctx context.Context, paymentNo str
shouldResumeAfterPayment = true
shouldEnqueueCommission = true
return s.activatePackage(ctx, tx, order)
if err := s.activatePackage(ctx, tx, order); err != nil {
return err
}
afterPayment := beforePayment
afterPayment.Status = model.PaymentRecordStatusPaid
afterPayment.PaidAt = &now
if thirdPartyTradeNo != "" {
afterPayment.ThirdPartyTradeNo = thirdPartyTradeNo
}
afterOrder := beforeOrder
afterOrder.PaymentStatus = model.PaymentStatusPaid
afterOrder.PaymentMethod = paymentMethod
afterOrder.PaidAt = &now
afterOrder.ExpiresAt = nil
afterOrder.ActualPaidAmount = &actualPaidAmountSnapshot
return s.appendPaymentConfirmedAudit(ctx, tx, &afterPayment, &afterOrder,
paymentStateData(&beforePayment), paymentStateData(&afterPayment), orderStateData(&beforeOrder), orderStateData(&afterOrder))
})
if err != nil {
@@ -2765,8 +2952,15 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
description = items[0].PackageName
}
attempt, startedAt, err := s.startOrderPaymentAttempt(ctx, order, constants.IntegrationProviderWechatPay, "order_jsapi")
if err != nil {
return nil, err
}
result, err := paymentSvc.CreateJSAPIOrder(ctx, order.OrderNo, description, openID, int(order.TotalAmount))
if err != nil {
if completeErr := s.completeOrderPaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "微信支付预下单结果未知"); completeErr != nil {
return nil, completeErr
}
s.logger.Error("创建 JSAPI 支付失败",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
@@ -2774,6 +2968,9 @@ func (s *Service) WechatPayJSAPI(ctx context.Context, orderID uint, openID strin
)
return nil, err
}
if err := s.completeOrderPaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultSuccess, "SUCCESS", ""); err != nil {
return nil, err
}
s.logger.Info("创建 JSAPI 支付成功",
zap.Uint("order_id", orderID),
@@ -2831,8 +3028,15 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
H5Type: sceneInfo.H5Info.Type,
}
attempt, startedAt, err := s.startOrderPaymentAttempt(ctx, order, constants.IntegrationProviderWechatPay, "order_h5")
if err != nil {
return nil, err
}
result, err := paymentSvc.CreateH5Order(ctx, order.OrderNo, description, int(order.TotalAmount), h5SceneInfo)
if err != nil {
if completeErr := s.completeOrderPaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "微信支付预下单结果未知"); completeErr != nil {
return nil, completeErr
}
s.logger.Error("创建 H5 支付失败",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
@@ -2840,6 +3044,9 @@ func (s *Service) WechatPayH5(ctx context.Context, orderID uint, sceneInfo *dto.
)
return nil, err
}
if err := s.completeOrderPaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultSuccess, "SUCCESS", ""); err != nil {
return nil, err
}
s.logger.Info("创建 H5 支付成功",
zap.Uint("order_id", orderID),
@@ -3075,6 +3282,10 @@ func (s *Service) fuiouPreCreate(
termIP = *ip
}
attempt, startedAt, err := s.startOrderPaymentAttempt(ctx, order, constants.IntegrationProviderFuiou, "order_"+strings.ToLower(tradeType))
if err != nil {
return nil, err
}
resp, err := client.WxPreCreate(
order.OrderNo,
strconv.FormatInt(order.TotalAmount, 10),
@@ -3085,6 +3296,9 @@ func (s *Service) fuiouPreCreate(
openID,
)
if err != nil {
if completeErr := s.completeOrderPaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultUnknown, "request_unknown", "富友支付预下单结果未知"); completeErr != nil {
return nil, completeErr
}
s.logger.Error("富友预下单失败",
zap.Uint("order_id", orderID),
zap.String("order_no", order.OrderNo),
@@ -3093,6 +3307,9 @@ func (s *Service) fuiouPreCreate(
)
return nil, errors.Wrap(errors.CodeFuiouPayFailed, err, "富友预下单失败")
}
if err := s.completeOrderPaymentAttempt(ctx, attempt, startedAt, constants.IntegrationResultSuccess, "000000", ""); err != nil {
return nil, err
}
s.logger.Info("富友预下单成功",
zap.Uint("order_id", orderID),