package agentrecharge import ( "context" "strconv" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" domain "github.com/break/junhong_cmp_fiber/internal/domain/agentrecharge" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // ConfirmOnlinePaymentCommand 描述验签或主动查单后得到的第三方收款事实。 type ConfirmOnlinePaymentCommand struct { PaymentNo string PaymentMethod string ConfigID uint MerchantIdentity string ThirdPartyTradeNo string Amount int64 PaidAt time.Time RequestID string CorrelationID string ParentEventID string } // PaymentConfirmedEvent 是第三方收款事实提交后的代理充值入账事件。 type PaymentConfirmedEvent struct { EventID string `json:"event_id"` RechargeID uint `json:"recharge_id"` RechargeNo string `json:"recharge_no"` PaymentID uint `json:"payment_id"` PaymentNo string `json:"payment_no"` ShopID uint `json:"shop_id"` WalletID uint `json:"wallet_id"` UserID uint `json:"user_id"` Amount int64 `json:"amount"` PaymentMethod string `json:"payment_method"` ThirdPartyTradeNo string `json:"third_party_trade_no"` PaidAt time.Time `json:"paid_at"` RequestID string `json:"request_id,omitempty"` CorrelationID string `json:"correlation_id,omitempty"` ParentEventID string `json:"parent_event_id,omitempty"` } // PaymentConfirmedEventWriter 在支付确认事务内追加可靠入账事件。 type PaymentConfirmedEventWriter interface { Append(ctx context.Context, tx *gorm.DB, event PaymentConfirmedEvent) error } // ConfirmOnlinePaymentResult 返回支付确认是否属于幂等重放。 type ConfirmOnlinePaymentResult struct { RechargeID uint PaymentID uint AlreadyConfirmed bool } // ConfirmOnlinePaymentService 统一处理回调和主动查单得到的代理充值支付事实。 type ConfirmOnlinePaymentService struct { db *gorm.DB eventWriter PaymentConfirmedEventWriter auditWriter PaymentAuditWriter } // NewConfirmOnlinePaymentService 创建代理充值支付确认用例。 func NewConfirmOnlinePaymentService(db *gorm.DB, eventWriter PaymentConfirmedEventWriter, auditWriter PaymentAuditWriter) *ConfirmOnlinePaymentService { return &ConfirmOnlinePaymentService{db: db, eventWriter: eventWriter, auditWriter: auditWriter} } // Execute 在一个短事务中校验并固化支付事实和可靠入账事件。 func (s *ConfirmOnlinePaymentService) Execute(ctx context.Context, command ConfirmOnlinePaymentCommand) (*ConfirmOnlinePaymentResult, error) { if s == nil || s.db == nil || s.eventWriter == nil || s.auditWriter == nil { return nil, errors.New(errors.CodeServiceUnavailable, "代理充值支付确认能力未配置") } command.PaymentNo = strings.TrimSpace(command.PaymentNo) command.PaymentMethod = strings.TrimSpace(command.PaymentMethod) command.MerchantIdentity = strings.TrimSpace(command.MerchantIdentity) command.ThirdPartyTradeNo = strings.TrimSpace(command.ThirdPartyTradeNo) if command.PaymentNo == "" || command.ConfigID == 0 || command.PaidAt.IsZero() { return nil, errors.New(errors.CodeInvalidParam, "代理充值支付确认参数不完整") } result := &ConfirmOnlinePaymentResult{} err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { payment, recharge, err := lockPaymentConfirmationFacts(ctx, tx, command.PaymentNo) if err != nil { return err } alreadyConfirmed, err := domain.ValidatePaymentConfirmation(toDomainConfirmationFacts(payment, recharge, command)) if err != nil { return err } result.RechargeID = recharge.ID result.PaymentID = payment.ID if alreadyConfirmed { result.AlreadyConfirmed = true return nil } if err := ensureTradeNoAvailable(ctx, tx, payment, command); err != nil { return err } paidAt := command.PaidAt.UTC() paymentUpdate := tx.WithContext(ctx).Model(&model.Payment{}). Where("id = ? AND status IN ?", payment.ID, []int{model.PaymentRecordStatusPending, model.PaymentRecordStatusFailed}). Updates(map[string]any{"status": model.PaymentRecordStatusPaid, "third_party_trade_no": command.ThirdPartyTradeNo, "paid_at": paidAt}) if paymentUpdate.Error != nil { return errors.Wrap(errors.CodeDatabaseError, paymentUpdate.Error, "更新代理充值支付单失败") } if paymentUpdate.RowsAffected != 1 { return errors.New(errors.CodeConflict, "代理充值支付单状态已变化") } rechargeUpdate := tx.WithContext(ctx).Model(&model.AgentRechargeRecord{}). Where("id = ? AND status IN ?", recharge.ID, []int{constants.RechargeStatusPending, constants.RechargeStatusClosed}). Updates(map[string]any{"status": constants.RechargeStatusPaid, "payment_transaction_id": command.ThirdPartyTradeNo, "paid_at": paidAt}) if rechargeUpdate.Error != nil { return errors.Wrap(errors.CodeDatabaseError, rechargeUpdate.Error, "更新代理充值单支付状态失败") } if rechargeUpdate.RowsAffected != 1 { return errors.New(errors.CodeConflict, "代理充值单状态已变化") } event := PaymentConfirmedEvent{ EventID: "agent-recharge:" + strconv.FormatUint(uint64(recharge.ID), 10) + ":payment-confirmed", RechargeID: recharge.ID, RechargeNo: recharge.RechargeNo, PaymentID: payment.ID, PaymentNo: payment.PaymentNo, ShopID: recharge.ShopID, WalletID: recharge.AgentWalletID, UserID: recharge.UserID, Amount: recharge.Amount, PaymentMethod: command.PaymentMethod, ThirdPartyTradeNo: command.ThirdPartyTradeNo, PaidAt: paidAt, RequestID: command.RequestID, CorrelationID: command.CorrelationID, ParentEventID: command.ParentEventID, } if err := s.eventWriter.Append(ctx, tx, event); err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "写入代理充值支付确认事件失败") } afterPayment := *payment afterPayment.Status = model.PaymentRecordStatusPaid afterPayment.ThirdPartyTradeNo = command.ThirdPartyTradeNo afterPayment.PaidAt = &paidAt return s.auditWriter.WriteAgentRechargePayment(ctx, tx, PaymentAudit{ ActionCode: constants.AuditActionPaymentConfirmed, Summary: "确认代理充值支付成功", Payment: &afterPayment, Recharge: recharge, BeforeData: map[string]any{"status": payment.Status, "third_party_trade_no": payment.ThirdPartyTradeNo, "paid_at": payment.PaidAt}, AfterData: map[string]any{"status": afterPayment.Status, "third_party_trade_no": afterPayment.ThirdPartyTradeNo, "paid_at": afterPayment.PaidAt}, RechargeBeforeData: map[string]any{"status": recharge.Status, "payment_transaction_id": recharge.PaymentTransactionID, "paid_at": recharge.PaidAt}, RechargeAfterData: map[string]any{"status": constants.RechargeStatusPaid, "payment_transaction_id": command.ThirdPartyTradeNo, "paid_at": paidAt}, }) }) if err != nil { return nil, err } return result, nil } func lockPaymentConfirmationFacts(ctx context.Context, tx *gorm.DB, paymentNo string) (*model.Payment, *model.AgentRechargeRecord, error) { var payment model.Payment if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("payment_no = ?", paymentNo).First(&payment).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, nil, errors.New(errors.CodeNotFound, "代理充值支付单不存在") } return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理充值支付单失败") } var recharge model.AgentRechargeRecord if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", payment.OrderID).First(&recharge).Error; err != nil { if err == gorm.ErrRecordNotFound { return nil, nil, errors.New(errors.CodeConflict, "支付单关联的代理充值单不存在") } return nil, nil, errors.Wrap(errors.CodeDatabaseError, err, "锁定代理充值单失败") } return &payment, &recharge, nil } func toDomainConfirmationFacts(payment *model.Payment, recharge *model.AgentRechargeRecord, command ConfirmOnlinePaymentCommand) domain.PaymentConfirmationFacts { paymentConfigID, rechargeConfigID, rechargeChannel := uint(0), uint(0), "" if payment.PaymentConfigID != nil { paymentConfigID = *payment.PaymentConfigID } if recharge.PaymentConfigID != nil { rechargeConfigID = *recharge.PaymentConfigID } if recharge.PaymentChannel != nil { rechargeChannel = *recharge.PaymentChannel } return domain.PaymentConfirmationFacts{ OrderType: payment.OrderType, ExpectedOrderType: model.PaymentOrderTypeAgentRecharge, PaymentMethod: payment.PaymentMethod, RechargePaymentMethod: recharge.PaymentMethod, RechargePaymentChannel: rechargeChannel, PaymentConfigID: paymentConfigID, RechargePaymentConfigID: rechargeConfigID, ConfirmedConfigID: command.ConfigID, MerchantIdentity: payment.MerchantIdentity, ConfirmedMerchantIdentity: command.MerchantIdentity, PaymentAmount: payment.Amount, RechargeAmount: recharge.Amount, ConfirmedAmount: command.Amount, PaymentOrderID: payment.OrderID, RechargeID: recharge.ID, PaymentState: domain.PaymentState(payment.Status), RechargeStatus: recharge.Status, StoredTradeNo: payment.ThirdPartyTradeNo, ConfirmedTradeNo: command.ThirdPartyTradeNo, } } func ensureTradeNoAvailable(ctx context.Context, tx *gorm.DB, payment *model.Payment, command ConfirmOnlinePaymentCommand) error { var count int64 if err := tx.WithContext(ctx).Unscoped().Model(&model.Payment{}). Where("id <> ? AND payment_method = ? AND third_party_trade_no = ?", payment.ID, command.PaymentMethod, command.ThirdPartyTradeNo). Count(&count).Error; err != nil { return errors.Wrap(errors.CodeDatabaseError, err, "校验第三方交易号唯一性失败") } if count > 0 { return errors.New(errors.CodeConflict, "第三方交易号已被其他支付单使用") } return nil }