Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Failing after 3m55s
新增收款商户、商户池轮询、微信授权配置独立管理;三类新支付 (C端套餐购买、C端资产钱包充值、代理在线预存款充值)无条件 经商户池选择并冻结路由,无旧综合配置回退。merchant_id 为空 历史支付继续按 payment_config_id 双读。凭证版本化加载与 ID+版本缓存保证轮换一致性。删除商户池新支付创建开关及全部 引用。
218 lines
10 KiB
Go
218 lines
10 KiB
Go
package agentrecharge
|
|
|
|
import (
|
|
"context"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
|
|
merchantpayment "github.com/break/junhong_cmp_fiber/internal/application/merchantpayment"
|
|
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.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
|
|
}
|
|
if payment.MerchantID == nil && command.ConfigID == 0 {
|
|
return errors.New(errors.CodeInvalidParam, "代理充值支付确认参数不完整")
|
|
}
|
|
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, "代理充值支付单状态已变化")
|
|
}
|
|
|
|
if err := merchantpayment.RecordFirstSuccess(ctx, tx, payment, paidAt); err != nil {
|
|
return err
|
|
}
|
|
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: payment.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,
|
|
FrozenMerchant: payment.MerchantID != nil, FrozenMerchantPaymentMethod: payment.MerchantPaymentMethodSnapshot,
|
|
FrozenMerchantProviderType: payment.MerchantProviderTypeSnapshot,
|
|
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
|
|
}
|