kra-new/internal/biz/payment.go

1151 lines
43 KiB
Go

package biz
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log/slog"
"strings"
"sync"
"time"
"kra/pkg/paymentkit"
"github.com/google/uuid"
)
const (
PaymentAlipay = paymentutil.ProviderAlipay
PaymentAlipayV3 = paymentutil.ProviderAlipayV3
PaymentWechatV2 = paymentutil.ProviderWechatV2
PaymentWechatV3 = paymentutil.ProviderWechatV3
PaymentApple = paymentutil.ProviderApple
PaymentChinaums = paymentutil.ProviderChinaums
PaymentSFT = paymentutil.ProviderSFT
PaymentSuperPay = paymentutil.ProviderSuperPay
PaymentWechatGame = paymentutil.ProviderWechatGame
PaymentDouyinGame = paymentutil.ProviderDouyinGame
// GoPay-backed providers. These are kept separate from the existing
// configuration-driven game/aggregator protocols above.
PaymentDouyin = paymentutil.ProviderDouyin
PaymentQQ = paymentutil.ProviderQQ
PaymentAllinPay = paymentutil.ProviderAllinPay
PaymentLakala = paymentutil.ProviderLakala
PaymentPayPal = paymentutil.ProviderPayPal
PaymentSaobei = paymentutil.ProviderSaobei
PaymentInternal = paymentutil.ProviderInternal
PaymentModeExternal = paymentutil.ModeExternal
PaymentModeInternal = paymentutil.ModeInternal
)
var SupportedPaymentProviders = paymentutil.SupportedProviders()
var (
ErrPaymentProviderNotFound = errors.New("支付渠道未配置")
ErrPaymentOperationRejected = errors.New("支付操作被平台拒绝")
)
func PaymentRejected(err error) error {
if err == nil {
return ErrPaymentOperationRejected
}
return fmt.Errorf("%w: %v", ErrPaymentOperationRejected, err)
}
type PaymentRequest struct {
Provider string `json:"provider"`
TradeNo string `json:"tradeNo"`
Subject string `json:"subject"`
Amount int64 `json:"amount"`
Currency string `json:"currency"`
NotifyURL string `json:"notifyUrl"`
ReturnURL string `json:"returnUrl"`
ClientIP string `json:"clientIp"`
Extra map[string]any `json:"extra"`
BusinessType string `json:"businessType"`
BusinessID string `json:"businessId"`
PaymentMode string `json:"paymentMode"`
OriginalAmount int64 `json:"originalAmount"`
}
// PaymentRefundRequest carries the durable provider identities needed to
// retry a refund without treating runtime configuration as per-order state.
type PaymentRefundRequest struct {
Provider string
TradeNo string
ProviderTradeNo string
QueryID string
RefundNo string
Amount int64
TotalAmount int64
Currency string
}
// PaymentIntent is the canonical business-facing payment snapshot.
type PaymentIntent struct {
Provider string
TradeNo string
BusinessType string
BusinessID string
Subject string
Amount int64
Currency string
Extra json.RawMessage
PaymentMode string
OriginalAmount int64
}
// InternalPaymentAuthorization is returned by a business module after it has
// atomically and idempotently deducted points, balance, or another platform
// owned asset. TradeNo must be the idempotency key for the deduction.
type InternalPaymentAuthorization struct {
AuthorizationID string
Amount int64
Currency string
PayerPaidAmount int64
CashPaidAmount int64
PointPaidAmount int64
DiscountAmount int64
SettlementAmount int64
PayerCurrency string
AmountBreakdownKnown bool
Payload json.RawMessage
}
type InternalPaymentRefund struct {
RefundID string
Amount int64
Currency string
Payload json.RawMessage
}
type PaymentInternalPayer interface {
PayInternal(context.Context, *PaymentIntent) (*InternalPaymentAuthorization, error)
}
type PaymentInternalRefundProcessor interface {
RefundInternal(context.Context, *PaymentOrder, int64) (*InternalPaymentRefund, error)
}
type PaymentConfirmation struct {
ID string
Provider string
TradeNo string
ProviderTradeNo string
BusinessType string
BusinessID string
Subject string
Amount int64
OriginalAmount int64
PayerPaidAmount int64
CashPaidAmount int64
PointPaidAmount int64
DiscountAmount int64
ProviderDiscountAmount int64
MerchantDiscountAmount int64
SettlementAmount int64
Currency string
PayerCurrency string
AmountBreakdownKnown bool
Payload json.RawMessage
CreatedAt time.Time
}
type PaymentResult struct {
Provider string `json:"provider"`
Status string `json:"status"`
TradeNo string `json:"tradeNo"`
ProviderTradeNo string `json:"providerTradeNo"`
Amount int64 `json:"amount"`
PayerPaidAmount int64 `json:"payerPaidAmount,omitempty"`
CashPaidAmount int64 `json:"cashPaidAmount,omitempty"`
PointPaidAmount int64 `json:"pointPaidAmount,omitempty"`
DiscountAmount int64 `json:"discountAmount,omitempty"`
ProviderDiscountAmount int64 `json:"providerDiscountAmount,omitempty"`
MerchantDiscountAmount int64 `json:"merchantDiscountAmount,omitempty"`
SettlementAmount int64 `json:"settlementAmount,omitempty"`
Currency string `json:"currency"`
PayerCurrency string `json:"payerCurrency,omitempty"`
AmountBreakdownKnown bool `json:"amountBreakdownKnown"`
Duplicate bool `json:"duplicate"`
Payload json.RawMessage `json:"payload"`
OrderStatus string `json:"orderStatus,omitempty"`
FulfillmentStatus string `json:"fulfillmentStatus,omitempty"`
RefundStatus string `json:"refundStatus,omitempty"`
EventID string `json:"-"`
QueryID string `json:"-"`
SuccessAck PaymentCallbackAck `json:"-"`
FailureAck PaymentCallbackAck `json:"-"`
}
// PaymentTestStage is one step of a provider connectivity test. A skipped
// query/refund is reported explicitly because a prepay flow cannot be marked
// paid without a sandbox payer interaction.
type PaymentTestStage struct {
Name string `json:"name"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
TradeNo string `json:"tradeNo,omitempty"`
Duration int64 `json:"durationMs,omitempty"`
}
type PaymentTestResult struct {
Provider string `json:"provider"`
TradeNo string `json:"tradeNo"`
Passed bool `json:"passed"`
FullFlow bool `json:"fullFlow"`
Mode string `json:"mode,omitempty"`
Stages []PaymentTestStage `json:"stages"`
Result *PaymentResult `json:"result,omitempty"`
}
type PaymentCallbackAck = paymentutil.CallbackAck
type PaymentCallbackError = paymentutil.CallbackError
func CallbackFailure(err error, fallback PaymentCallbackAck) PaymentCallbackAck {
return paymentutil.CallbackFailure(err, fallback)
}
func DefaultPaymentCallbackAck(provider string, success bool) PaymentCallbackAck {
return paymentutil.DefaultCallbackAck(provider, success)
}
type PaymentCallback struct {
Provider string
Headers map[string]string
Body []byte
Query map[string]string
}
type PaymentRepo interface {
Create(context.Context, *PaymentRequest) (*PaymentResult, error)
Query(context.Context, string, string) (*PaymentResult, error)
Refund(context.Context, *PaymentRefundRequest) (*PaymentResult, error)
HandleCallback(context.Context, *PaymentCallback) (*PaymentResult, error)
}
type PaymentProviderTester interface {
TestProvider(context.Context, string) (*PaymentTestResult, error)
}
type PaymentHooks interface {
BeforeCreate(context.Context, *PaymentRequest) error
}
type NoopPaymentHooks struct{}
func (NoopPaymentHooks) BeforeCreate(context.Context, *PaymentRequest) error { return nil }
type PaymentFulfillmentHandler interface {
Type() string
Fulfill(context.Context, *PaymentConfirmation) error
}
type PaymentFulfillmentRegistry struct {
mu sync.RWMutex
handlers map[string]PaymentFulfillmentHandler
}
func NewPaymentFulfillmentRegistry() *PaymentFulfillmentRegistry {
return &PaymentFulfillmentRegistry{handlers: map[string]PaymentFulfillmentHandler{}}
}
func (r *PaymentFulfillmentRegistry) Register(handler PaymentFulfillmentHandler) error {
if handler == nil || strings.TrimSpace(handler.Type()) == "" {
return errors.New("支付发货 handler 无效")
}
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.handlers[handler.Type()]; exists {
return errors.New("支付发货 handler 已注册: " + handler.Type())
}
r.handlers[handler.Type()] = handler
return nil
}
func (r *PaymentFulfillmentRegistry) Handler(kind string) PaymentFulfillmentHandler {
r.mu.RLock()
defer r.mu.RUnlock()
return r.handlers[kind]
}
type PaymentUsecase struct {
repo PaymentRepo
orders PaymentOrderRepo
hooks PaymentHooks
sources *PaymentOrderSourceRegistry
fulfillments *PaymentFulfillmentRegistry
logger PaymentLogger
}
func NewPaymentUsecase(repo PaymentRepo, orders PaymentOrderRepo, appLogger *slog.Logger) *PaymentUsecase {
return &PaymentUsecase{
repo: repo,
orders: orders,
hooks: NoopPaymentHooks{},
sources: NewPaymentOrderSourceRegistry(),
fulfillments: NewPaymentFulfillmentRegistry(),
logger: NewPaymentLogger(appLogger),
}
}
func (uc *PaymentUsecase) SetHooks(hooks PaymentHooks) {
if hooks != nil {
uc.hooks = hooks
}
}
func (uc *PaymentUsecase) SetOrderSourceRegistry(registry *PaymentOrderSourceRegistry) {
if registry != nil {
uc.sources = registry
}
}
func (uc *PaymentUsecase) SetFulfillmentRegistry(registry *PaymentFulfillmentRegistry) {
if registry != nil {
uc.fulfillments = registry
}
}
func (uc *PaymentUsecase) RegisterBusinessModule(module PaymentBusinessModule) error {
if module == nil {
return errors.New("支付业务模块为空")
}
if err := uc.sources.Register(module); err != nil {
return err
}
if err := uc.fulfillments.Register(module); err != nil {
uc.sources.Unregister(module.Type())
return err
}
return nil
}
func (uc *PaymentUsecase) Order(ctx context.Context, provider, tradeNo string) (*PaymentOrder, error) {
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
if !validPaymentText(provider, 64) || !validPaymentText(tradeNo, 128) {
return nil, errors.New("支付订单查询参数不完整")
}
return uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
}
func (uc *PaymentUsecase) Orders(ctx context.Context, page, pageSize int, filter PaymentOrderFilter) ([]*PaymentOrder, int64, error) {
if uc.orders == nil {
return nil, 0, errors.New("支付订单仓储未接入")
}
if page <= 0 {
page = 1
}
if pageSize <= 0 {
pageSize = 10
}
return uc.orders.ListPaymentOrders(ctx, page, pageSize, filter)
}
func (uc *PaymentUsecase) Create(ctx context.Context, req *PaymentRequest) (*PaymentResult, error) {
if req == nil || !validPaymentText(req.Provider, 64) || !validPaymentText(req.TradeNo, 128) || !validPaymentText(req.BusinessType, 64) || !validPaymentText(req.BusinessID, 128) {
return nil, errors.New("支付参数不完整")
}
if !supportedPaymentProvider(req.Provider) {
return nil, errors.New("不支持的支付渠道")
}
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
prepared, err := uc.preparePaymentRequest(ctx, req)
if err != nil {
return nil, err
}
req = prepared
if !validPaymentText(req.Currency, 16) || !validPaymentText(req.Subject, 256) || req.Amount <= 0 {
return nil, errors.New("支付订单金额、币种或标题无效")
}
req.PaymentMode = defaultPaymentMode(req.Provider, req.PaymentMode)
if req.PaymentMode != PaymentModeExternal && req.PaymentMode != PaymentModeInternal {
return nil, ErrPaymentOrderConflict
}
if req.Provider == PaymentInternal && req.PaymentMode != PaymentModeInternal {
return nil, ErrPaymentOrderConflict
}
if req.Provider != PaymentInternal && req.PaymentMode == PaymentModeInternal {
return nil, ErrPaymentOrderConflict
}
if req.OriginalAmount <= 0 {
req.OriginalAmount = req.Amount
}
if req.OriginalAmount < req.Amount {
return nil, errors.New("原始金额不能小于支付订单金额")
}
canonicalProvider, canonicalTradeNo := req.Provider, req.TradeNo
canonicalBusinessType, canonicalBusinessID := req.BusinessType, req.BusinessID
canonicalSubject, canonicalCurrency, canonicalAmount := req.Subject, req.Currency, req.Amount
canonicalOriginalAmount, canonicalPaymentMode := req.OriginalAmount, req.PaymentMode
canonicalExtra, err := json.Marshal(req.Extra)
if err != nil {
return nil, fmt.Errorf("支付扩展参数无效: %w", err)
}
if err := uc.hooks.BeforeCreate(ctx, req); err != nil {
return nil, err
}
currentExtra, err := json.Marshal(req.Extra)
if err != nil {
return nil, fmt.Errorf("支付扩展参数无效: %w", err)
}
if req.Provider != canonicalProvider || req.TradeNo != canonicalTradeNo || req.BusinessType != canonicalBusinessType || req.BusinessID != canonicalBusinessID || req.Subject != canonicalSubject || req.Currency != canonicalCurrency || req.Amount != canonicalAmount || req.OriginalAmount != canonicalOriginalAmount || req.PaymentMode != canonicalPaymentMode || string(currentExtra) != string(canonicalExtra) {
return nil, ErrPaymentOrderConflict
}
return uc.createWithOrder(ctx, req)
}
func (uc *PaymentUsecase) preparePaymentRequest(ctx context.Context, req *PaymentRequest) (*PaymentRequest, error) {
source := uc.sources.Source(req.BusinessType)
if source == nil {
return nil, fmt.Errorf("支付业务订单来源未注册: %s", req.BusinessType)
}
intent, err := source.PreparePayment(ctx, req.Provider, req.TradeNo, req.BusinessID)
if err != nil {
return nil, err
}
if intent == nil || intent.Provider != req.Provider || intent.TradeNo != req.TradeNo || intent.BusinessType != req.BusinessType || intent.BusinessID != req.BusinessID {
return nil, ErrPaymentOrderConflict
}
prepared := *req
prepared.Subject = intent.Subject
prepared.Amount = intent.Amount
prepared.Currency = strings.ToUpper(intent.Currency)
prepared.PaymentMode = defaultPaymentMode(intent.Provider, intent.PaymentMode)
prepared.OriginalAmount = intent.OriginalAmount
if prepared.OriginalAmount <= 0 {
prepared.OriginalAmount = prepared.Amount
}
prepared.Extra = mergeCanonicalPaymentExtra(req.Extra, intent.Extra)
return &prepared, nil
}
func mergeCanonicalPaymentExtra(client map[string]any, canonical json.RawMessage) map[string]any {
merged := make(map[string]any, len(client)+4)
for key, value := range client {
merged[key] = value
}
var values map[string]any
if json.Unmarshal(canonical, &values) == nil {
for key, value := range values {
merged[key] = value
}
}
return merged
}
func supportedPaymentProvider(provider string) bool {
if provider == PaymentInternal {
return true
}
for _, supported := range SupportedPaymentProviders {
if provider == supported {
return true
}
}
return false
}
func (uc *PaymentUsecase) Query(ctx context.Context, provider, tradeNo string) (*PaymentResult, error) {
if !validPaymentText(provider, 64) || !validPaymentText(tradeNo, 128) {
return nil, errors.New("查询支付参数不完整")
}
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
order, err := uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
if err != nil {
return nil, err
}
if provider == PaymentInternal || order.PaymentMode == PaymentModeInternal {
return paymentResultFromOrder(order), nil
}
queryID := tradeNo
if order.QueryID != "" {
queryID = order.QueryID
} else if provider == PaymentApple {
if order.ProviderTradeNo == "" {
return nil, ErrPaymentOrderState
}
queryID = order.ProviderTradeNo
}
result, err := uc.repo.Query(ctx, provider, queryID)
if err != nil {
uc.logger.Error(ctx, "payment.query.failed", "provider", provider, "trade_no", tradeNo, "query_id", queryID, "error", err)
return nil, err
}
if err = validatePaymentQueryResult(provider, queryID, result); err != nil {
uc.logger.Error(ctx, "payment.query.invalid", "provider", provider, "trade_no", tradeNo, "query_id", queryID, "error", err)
return nil, err
}
if result.TradeNo != tradeNo {
return nil, errors.New("支付平台查单商户订单号不匹配")
}
return uc.reconcilePaymentResult(ctx, result, nil)
}
func (uc *PaymentUsecase) Refund(ctx context.Context, provider, tradeNo string, amount int64) (*PaymentResult, error) {
if !validPaymentText(provider, 64) || !validPaymentText(tradeNo, 128) || amount <= 0 {
return nil, errors.New("退款参数不完整")
}
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
return uc.refundWithOrder(ctx, provider, tradeNo, amount)
}
// TestProvider exercises the configured provider without requiring a business
// module or a real customer order. Provider adapters use their configured
// sandbox/test endpoint when one is selected in integration settings.
func (uc *PaymentUsecase) TestProvider(ctx context.Context, provider string) (*PaymentTestResult, error) {
provider = strings.TrimSpace(provider)
if !supportedPaymentProvider(provider) {
return nil, errors.New("不支持的支付渠道")
}
tester, ok := uc.repo.(PaymentProviderTester)
if !ok {
return nil, errors.New("支付渠道仓储未接入")
}
return tester.TestProvider(ctx, provider)
}
// Fulfill retries delivery for an already-paid order. It is intentionally
// idempotent: the repository lease prevents concurrent attempts and a
// succeeded order is returned as a duplicate without invoking the handler.
func (uc *PaymentUsecase) Fulfill(ctx context.Context, provider, tradeNo string) (*PaymentResult, error) {
if !validPaymentText(provider, 64) || !validPaymentText(tradeNo, 128) {
return nil, errors.New("发货参数不完整")
}
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
order, err := uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
if err != nil {
return nil, err
}
result := paymentResultFromOrder(order)
if result == nil {
return nil, ErrPaymentOrderNotFound
}
if order.PaymentStatus != PaymentStatusPaid {
return nil, ErrPaymentOrderState
}
return uc.fulfillOrder(ctx, order, result)
}
// Callback verifies the notification in the adapter, then always performs a
// provider-side query before it can dispatch fulfillment. Callback payloads
// are never trusted for amount, currency, or final payment state.
func (uc *PaymentUsecase) Callback(ctx context.Context, callback *PaymentCallback) (*PaymentResult, error) {
if callback == nil || strings.TrimSpace(callback.Provider) == "" || len(callback.Body) == 0 {
err := errors.New("支付回调参数不完整")
uc.logger.Error(ctx, "payment.callback.invalid", "error", err)
return nil, err
}
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
return uc.callbackWithOrder(ctx, callback)
}
func (uc *PaymentUsecase) createWithOrder(ctx context.Context, req *PaymentRequest) (*PaymentResult, error) {
extra, err := json.Marshal(req.Extra)
if err != nil {
return nil, fmt.Errorf("支付扩展参数无效: %w", err)
}
if len(extra) == 0 || string(extra) == "null" {
extra = []byte("{}")
}
order := &PaymentOrder{
TradeNo: req.TradeNo,
Provider: req.Provider,
BusinessType: req.BusinessType,
BusinessID: req.BusinessID,
Subject: req.Subject,
PaymentMode: defaultPaymentMode(req.Provider, req.PaymentMode),
OriginalAmount: req.OriginalAmount,
Amount: req.Amount,
Currency: strings.ToUpper(req.Currency),
PaymentStatus: PaymentStatusInitialized,
FulfillmentStatus: FulfillmentStatusPending,
RefundStatus: RefundStatusNone,
ConfirmationID: paymentConfirmationID(req.Provider, req.TradeNo),
RequestFingerprint: paymentOrderFingerprint(req, extra),
Extra: extra,
}
order, created, err := uc.orders.CreatePaymentOrder(ctx, order)
if err != nil {
return nil, err
}
if order.RequestFingerprint != paymentOrderFingerprint(req, extra) {
return nil, ErrPaymentOrderConflict
}
if !created && len(order.CreatePayload) > 0 && string(order.CreatePayload) != "{}" {
return paymentResultFromOrder(order), nil
}
if req.Provider == PaymentInternal || order.PaymentMode == PaymentModeInternal {
source := uc.sources.Source(req.BusinessType)
payer, ok := source.(PaymentInternalPayer)
if !ok {
return nil, fmt.Errorf("支付业务未实现内部支付: %s", req.BusinessType)
}
authorization, err := payer.PayInternal(ctx, &PaymentIntent{
Provider: req.Provider, TradeNo: req.TradeNo, BusinessType: req.BusinessType,
BusinessID: req.BusinessID, Subject: req.Subject, Amount: req.Amount,
Currency: req.Currency, Extra: extra, PaymentMode: PaymentModeInternal,
OriginalAmount: req.OriginalAmount,
})
if err != nil {
return nil, err
}
if authorization == nil || authorization.AuthorizationID == "" || authorization.Amount != req.Amount || !strings.EqualFold(authorization.Currency, req.Currency) {
return nil, errors.New("内部支付授权结果无效")
}
result := &PaymentResult{
Provider: PaymentInternal, Status: "success", TradeNo: req.TradeNo,
ProviderTradeNo: authorization.AuthorizationID, QueryID: authorization.AuthorizationID,
Amount: req.Amount, PayerPaidAmount: authorization.PayerPaidAmount,
CashPaidAmount: authorization.CashPaidAmount, PointPaidAmount: authorization.PointPaidAmount,
DiscountAmount: authorization.DiscountAmount, SettlementAmount: authorization.SettlementAmount,
Currency: strings.ToUpper(req.Currency), PayerCurrency: strings.ToUpper(authorization.PayerCurrency),
AmountBreakdownKnown: authorization.AmountBreakdownKnown, Payload: authorization.Payload,
}
if result.PayerCurrency == "" {
result.PayerCurrency = result.Currency
}
if !authorization.AmountBreakdownKnown {
return nil, errors.New("内部支付必须返回完整金额拆分")
}
if err = validatePaymentBreakdown(result); err != nil {
return nil, fmt.Errorf("内部支付金额拆分无效: %w", err)
}
if _, err = uc.orders.RecordPaymentCreate(ctx, req.Provider, req.TradeNo, paymentProviderUpdate(result)); err != nil {
return nil, err
}
return uc.reconcilePaymentResult(ctx, result, nil)
}
uc.logger.Info(ctx, "payment.order.create", "provider", req.Provider, "trade_no", req.TradeNo, "business_type", req.BusinessType, "business_id", req.BusinessID)
result, err := uc.repo.Create(ctx, req)
if err != nil {
uc.logger.Error(ctx, "payment.order.provider_create_failed", "provider", req.Provider, "trade_no", req.TradeNo, "error", err)
return nil, err
}
if err = validatePaymentCreateResult(req, result); err != nil {
uc.logger.Error(ctx, "payment.order.provider_create_invalid", "provider", req.Provider, "trade_no", req.TradeNo, "error", err)
return nil, err
}
order, err = uc.orders.RecordPaymentCreate(ctx, req.Provider, req.TradeNo, paymentProviderUpdate(result))
if err != nil {
uc.logger.Error(ctx, "payment.order.provider_create_record_failed", "provider", req.Provider, "trade_no", req.TradeNo, "error", err)
return nil, err
}
if strings.EqualFold(result.Status, "success") {
result.Status = "pending"
}
return attachOrderResult(result, order), nil
}
func (uc *PaymentUsecase) callbackWithOrder(ctx context.Context, callback *PaymentCallback) (*PaymentResult, error) {
callbackResult, err := uc.repo.HandleCallback(ctx, callback)
if err != nil {
uc.logger.Error(ctx, "payment.callback.verify_failed", "provider", callback.Provider, "error", err)
return nil, err
}
if callbackResult == nil || callbackResult.Provider != callback.Provider || !validPaymentText(callbackResult.TradeNo, 128) || (callbackResult.QueryID != "" && !validPaymentText(callbackResult.QueryID, 128)) {
err = errors.New("支付回调渠道或商户订单号校验失败")
return nil, callbackResultError(callbackResult, err)
}
queryID := callbackResult.QueryID
if queryID == "" {
queryID = callbackResult.TradeNo
}
queryResult, err := uc.repo.Query(ctx, callbackResult.Provider, queryID)
if err != nil {
uc.logger.Error(ctx, "payment.callback.query_failed", "provider", callbackResult.Provider, "trade_no", callbackResult.TradeNo, "event_id", callbackResult.EventID, "error", err)
return nil, callbackResultError(callbackResult, err)
}
if err = validatePaymentQueryResult(callbackResult.Provider, queryID, queryResult); err != nil {
uc.logger.Error(ctx, "payment.callback.query_invalid", "provider", callbackResult.Provider, "trade_no", callbackResult.TradeNo, "event_id", callbackResult.EventID, "error", err)
return nil, callbackResultError(callbackResult, err)
}
if queryResult.TradeNo != callbackResult.TradeNo {
err = errors.New("支付回调商户订单号与主动查单结果不匹配")
return nil, callbackResultError(callbackResult, err)
}
queryResult = clonePaymentResult(queryResult)
queryResult.EventID = callbackResult.EventID
queryResult.SuccessAck = callbackResult.SuccessAck
queryResult.FailureAck = callbackResult.FailureAck
return uc.reconcilePaymentResult(ctx, queryResult, callbackResult)
}
func (uc *PaymentUsecase) reconcilePaymentResult(ctx context.Context, result, callbackSource *PaymentResult) (*PaymentResult, error) {
if result == nil {
return nil, errors.New("支付查单结果为空")
}
fail := func(err error) (*PaymentResult, error) {
if callbackSource != nil {
return nil, callbackResultError(callbackSource, err)
}
return nil, err
}
order, err := uc.orders.FindPaymentOrder(ctx, result.Provider, result.TradeNo)
if err != nil {
uc.logger.Error(ctx, "payment.order.not_found", "provider", result.Provider, "trade_no", result.TradeNo, "error", err)
return fail(err)
}
if strings.EqualFold(result.Status, "success") {
if err = validatePaymentConfirmation(order.Intent(), result.Provider, result.TradeNo, result); err != nil {
uc.logger.Error(ctx, "payment.order.validation_failed", "provider", result.Provider, "trade_no", result.TradeNo, "error", err)
return fail(err)
}
}
order, err = uc.orders.ApplyPaymentResult(ctx, result.Provider, result.TradeNo, paymentProviderUpdate(result))
if err != nil {
uc.logger.Error(ctx, "payment.order.result_record_failed", "provider", result.Provider, "trade_no", result.TradeNo, "error", err)
return fail(err)
}
result = attachOrderResult(result, order)
if !strings.EqualFold(result.Status, "success") {
return result, nil
}
fulfilled, err := uc.fulfillOrder(ctx, order, result)
if err != nil {
return fail(err)
}
return fulfilled, nil
}
func (uc *PaymentUsecase) fulfillOrder(ctx context.Context, order *PaymentOrder, result *PaymentResult) (*PaymentResult, error) {
if order == nil || result == nil {
return nil, errors.New("支付发货订单为空")
}
order, token, duplicate, err := uc.orders.BeginPaymentFulfillment(ctx, result.Provider, result.TradeNo, 10*time.Minute)
if err != nil {
uc.logger.Error(ctx, "payment.order.fulfillment_begin_failed", "provider", result.Provider, "trade_no", result.TradeNo, "error", err)
return nil, err
}
if duplicate {
result.Duplicate = true
result.Status = "fulfilled"
return attachOrderResult(result, order), nil
}
handler := uc.fulfillments.Handler(order.BusinessType)
if handler == nil {
err = fmt.Errorf("支付发货 handler 未注册: %s", order.BusinessType)
_, _ = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, false, err.Error())
return nil, err
}
confirmation := &PaymentConfirmation{ID: order.ConfirmationID, Provider: order.Provider, TradeNo: order.TradeNo,
ProviderTradeNo: result.ProviderTradeNo, BusinessType: order.BusinessType, BusinessID: order.BusinessID,
Subject: order.Subject, Amount: order.Amount, OriginalAmount: order.OriginalAmount,
PayerPaidAmount: order.PayerPaidAmount, CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount,
DiscountAmount: order.DiscountAmount, ProviderDiscountAmount: order.ProviderDiscountAmount,
MerchantDiscountAmount: order.MerchantDiscountAmount, SettlementAmount: order.SettlementAmount,
Currency: order.Currency, PayerCurrency: order.PayerCurrency, AmountBreakdownKnown: order.AmountBreakdownKnown,
Payload: result.Payload, CreatedAt: time.Now().UTC()}
if err = handler.Fulfill(ctx, confirmation); err != nil {
_, _ = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, false, err.Error())
return nil, err
}
order, err = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, true, "")
if err != nil {
return nil, err
}
result.Status = "fulfilled"
return attachOrderResult(result, order), nil
}
func (uc *PaymentUsecase) refundWithOrder(ctx context.Context, provider, tradeNo string, amount int64) (*PaymentResult, error) {
current, err := uc.orders.FindPaymentOrder(ctx, provider, tradeNo)
if err != nil {
return nil, err
}
handler := uc.fulfillments.Handler(current.BusinessType)
authorizer, ok := handler.(PaymentRefundAuthorizer)
if !ok {
return nil, fmt.Errorf("支付业务未实现退款授权: %s", current.BusinessType)
}
if err = authorizer.AuthorizeRefund(ctx, current, amount); err != nil {
return nil, err
}
order, token, err := uc.orders.BeginPaymentRefund(ctx, provider, tradeNo, amount, 10*time.Minute)
if err != nil {
return nil, err
}
if provider == PaymentInternal || current.PaymentMode == PaymentModeInternal {
processor, ok := handler.(PaymentInternalRefundProcessor)
if !ok {
_, _ = uc.orders.CompletePaymentRefundRequest(ctx, provider, tradeNo, token, false, "支付业务未实现内部退款")
return nil, fmt.Errorf("支付业务未实现内部退款: %s", current.BusinessType)
}
refund, refundErr := processor.RefundInternal(ctx, order, amount)
accepted := refundErr == nil && refund != nil && refund.Amount == amount && strings.EqualFold(refund.Currency, current.Currency)
message := ""
if refundErr != nil {
message = refundErr.Error()
} else if !accepted {
message = "内部退款结果无效"
}
if refundErr != nil && !errors.Is(refundErr, ErrPaymentOperationRejected) {
return nil, refundErr
}
if refundErr == nil && refund == nil {
return nil, errors.New("内部退款结果为空")
}
if _, err = uc.orders.CompletePaymentRefundRequest(ctx, provider, tradeNo, token, accepted, message); err != nil {
return nil, err
}
if !accepted {
return nil, errors.New(message)
}
order, err = uc.orders.ConfirmPaymentRefund(ctx, provider, tradeNo, order.RefundNo, amount, true, "")
if err != nil {
return nil, err
}
return attachOrderResult(&PaymentResult{Provider: provider, TradeNo: tradeNo, ProviderTradeNo: refund.RefundID, Amount: amount, Currency: current.Currency, Payload: refund.Payload, Status: "refunded"}, order), nil
}
result, providerErr := uc.repo.Refund(ctx, &PaymentRefundRequest{
Provider: order.Provider,
TradeNo: order.TradeNo,
ProviderTradeNo: order.ProviderTradeNo,
QueryID: order.QueryID,
RefundNo: order.RefundNo,
Amount: amount,
TotalAmount: order.Amount,
Currency: order.Currency,
})
if providerErr == nil {
providerErr = validatePaymentRefundResult(provider, tradeNo, result)
}
if providerErr != nil && !errors.Is(providerErr, ErrPaymentOperationRejected) {
// A timeout or malformed response cannot prove that the provider did not
// accept the refund. Keep the durable refund operation and its idempotency
// number; the lease expiry path will retry the same operation.
return nil, providerErr
}
accepted := providerErr == nil
message := ""
if providerErr != nil {
message = providerErr.Error()
} else if !accepted {
message = "支付平台未接受退款请求"
}
order, err = uc.orders.CompletePaymentRefundRequest(ctx, provider, tradeNo, token, accepted, message)
if err != nil {
return nil, err
}
if !accepted {
if providerErr != nil {
return nil, providerErr
}
return nil, errors.New(message)
}
result.Status = "refund_pending"
return attachOrderResult(result, order), nil
}
// ConfirmRefund is called by a provider-specific refund notification or a
// reconciliation worker after the platform reports the final refund state.
func (uc *PaymentUsecase) ConfirmRefund(ctx context.Context, provider, tradeNo, refundNo string, amount int64, success bool, message string) (*PaymentResult, error) {
if uc.orders == nil {
return nil, errors.New("支付订单仓储未接入")
}
if !validPaymentText(provider, 64) || !validPaymentText(tradeNo, 128) || !validPaymentText(refundNo, 128) || amount <= 0 {
return nil, errors.New("退款确认参数无效")
}
order, err := uc.orders.ConfirmPaymentRefund(ctx, provider, tradeNo, refundNo, amount, success, message)
if err != nil {
return nil, err
}
result := &PaymentResult{Provider: provider, TradeNo: tradeNo, Status: "refund_failed"}
if success {
result.Status = "refunded"
}
return attachOrderResult(result, order), nil
}
func paymentProviderUpdate(result *PaymentResult) *PaymentProviderUpdate {
if result == nil {
return &PaymentProviderUpdate{}
}
var payloadHash string
if len(result.Payload) > 0 {
sum := sha256.Sum256(result.Payload)
payloadHash = hex.EncodeToString(sum[:])
}
return &PaymentProviderUpdate{
Status: result.Status, ProviderStatus: result.Status, ProviderTradeNo: result.ProviderTradeNo,
QueryID: result.QueryID, Amount: result.Amount, PayerPaidAmount: result.PayerPaidAmount,
CashPaidAmount: result.CashPaidAmount, PointPaidAmount: result.PointPaidAmount, DiscountAmount: result.DiscountAmount,
ProviderDiscountAmount: result.ProviderDiscountAmount, MerchantDiscountAmount: result.MerchantDiscountAmount,
SettlementAmount: result.SettlementAmount, Currency: result.Currency, PayerCurrency: result.PayerCurrency,
AmountBreakdownKnown: result.AmountBreakdownKnown,
EventID: result.EventID, PayloadHash: payloadHash, CreatePayload: result.Payload,
}
}
func paymentOrderFingerprint(req *PaymentRequest, extra json.RawMessage) string {
value, _ := json.Marshal(map[string]any{
"provider": req.Provider, "trade_no": req.TradeNo, "business_type": req.BusinessType,
"business_id": req.BusinessID, "subject": req.Subject, "amount": req.Amount,
"original_amount": req.OriginalAmount, "payment_mode": defaultPaymentMode(req.Provider, req.PaymentMode),
"currency": strings.ToUpper(req.Currency), "extra": json.RawMessage(extra),
})
sum := sha256.Sum256(value)
return hex.EncodeToString(sum[:])
}
func paymentResultFromOrder(order *PaymentOrder) *PaymentResult {
if order == nil {
return nil
}
status := "pending"
switch order.PaymentStatus {
case PaymentStatusPaid:
status = "success"
case PaymentStatusPartiallyRefunded:
status = "partially_refunded"
case PaymentStatusRefunded:
status = "refunded"
case PaymentStatusFailed, PaymentStatusClosed:
status = "failed"
}
if order.FulfillmentStatus == FulfillmentStatusSucceeded && status == "success" {
status = "fulfilled"
}
amount := order.PaidAmount
if amount == 0 {
amount = order.Amount
}
return &PaymentResult{
Provider: order.Provider, Status: status, TradeNo: order.TradeNo,
ProviderTradeNo: order.ProviderTradeNo, Amount: amount, PayerPaidAmount: order.PayerPaidAmount,
CashPaidAmount: order.CashPaidAmount, PointPaidAmount: order.PointPaidAmount, DiscountAmount: order.DiscountAmount,
ProviderDiscountAmount: order.ProviderDiscountAmount, MerchantDiscountAmount: order.MerchantDiscountAmount,
SettlementAmount: order.SettlementAmount, Currency: order.Currency, PayerCurrency: order.PayerCurrency,
AmountBreakdownKnown: order.AmountBreakdownKnown,
Payload: append(json.RawMessage(nil), order.CreatePayload...), OrderStatus: order.PaymentStatus,
FulfillmentStatus: order.FulfillmentStatus, RefundStatus: order.RefundStatus,
}
}
func attachOrderResult(result *PaymentResult, order *PaymentOrder) *PaymentResult {
if result == nil {
result = paymentResultFromOrder(order)
return result
}
if order == nil {
return result
}
result.OrderStatus = order.PaymentStatus
result.FulfillmentStatus = order.FulfillmentStatus
result.RefundStatus = order.RefundStatus
return result
}
func validatePaymentQueryResult(provider, queryID string, result *PaymentResult) error {
if result == nil {
return errors.New("支付平台查单结果为空")
}
if result.Provider != provider || !validPaymentText(result.TradeNo, 128) {
return errors.New("支付平台查单渠道或商户订单号无效")
}
if result.QueryID != "" {
if result.QueryID != queryID {
return errors.New("支付平台查单键不匹配")
}
} else if result.TradeNo != queryID {
return errors.New("支付平台查单商户订单号不匹配")
}
switch strings.ToLower(strings.TrimSpace(result.Status)) {
case "success":
if !validPaymentText(result.ProviderTradeNo, 128) {
return errors.New("支付平台查单成功但缺少平台交易号")
}
if result.Amount <= 0 {
return errors.New("支付平台查单成功但金额无效")
}
if !validPaymentText(result.Currency, 16) {
return errors.New("支付平台查单成功但币种为空")
}
case "pending", "failed":
default:
return fmt.Errorf("支付平台查单返回未知状态: %s", result.Status)
}
return nil
}
func validatePaymentCreateResult(req *PaymentRequest, result *PaymentResult) error {
if req == nil || result == nil {
return errors.New("支付平台下单结果为空")
}
if result.Provider != req.Provider || result.TradeNo != req.TradeNo {
return errors.New("支付平台下单结果渠道或商户订单号不匹配")
}
if result.ProviderTradeNo != "" && !validPaymentText(result.ProviderTradeNo, 128) {
return errors.New("支付平台交易号无效")
}
if result.QueryID != "" && !validPaymentText(result.QueryID, 128) {
return errors.New("支付平台查单键无效")
}
switch strings.ToLower(strings.TrimSpace(result.Status)) {
case "created", "client_pending", "pending", "processing", "success":
return nil
default:
return fmt.Errorf("支付平台下单返回未知状态: %s", result.Status)
}
}
func validatePaymentRefundResult(provider, tradeNo string, result *PaymentResult) error {
if result == nil {
return errors.New("支付平台退款结果为空")
}
if result.Provider != provider || result.TradeNo != tradeNo {
return errors.New("支付平台退款结果渠道或商户订单号不匹配")
}
if result.ProviderTradeNo != "" && !validPaymentText(result.ProviderTradeNo, 128) {
return errors.New("支付平台退款交易号无效")
}
switch strings.ToLower(strings.TrimSpace(result.Status)) {
case "created", "pending", "processing", "success", "refunded":
return nil
case "failed":
return PaymentRejected(errors.New("支付平台拒绝退款请求"))
default:
return fmt.Errorf("支付平台退款返回未知状态: %s", result.Status)
}
}
func validPaymentText(value string, maxBytes int) bool {
trimmed := strings.TrimSpace(value)
if trimmed == "" || trimmed != value || len(value) > maxBytes {
return false
}
for _, r := range value {
if r < 0x20 || r == 0x7f {
return false
}
}
return true
}
func callbackResultError(result *PaymentResult, err error) error {
if err == nil {
return nil
}
if result == nil || result.FailureAck.StatusCode == 0 {
return err
}
return &PaymentCallbackError{Cause: err, Ack: result.FailureAck}
}
func validatePaymentConfirmation(intent *PaymentIntent, provider, tradeNo string, result *PaymentResult) error {
if intent == nil || result == nil {
return errors.New("支付确认数据为空")
}
if !strings.EqualFold(intent.Provider, provider) || intent.TradeNo != tradeNo || intent.TradeNo == "" {
return errors.New("支付商户订单号校验失败")
}
if strings.ToLower(result.Status) != "success" || result.Provider != provider || result.TradeNo != tradeNo {
return errors.New("支付平台订单状态校验失败")
}
if result.ProviderTradeNo == "" || result.Amount <= 0 || intent.Amount <= 0 || result.Amount != intent.Amount {
return errors.New("支付金额校验失败")
}
if result.PayerPaidAmount < 0 || result.CashPaidAmount < 0 || result.PointPaidAmount < 0 || result.DiscountAmount < 0 || result.ProviderDiscountAmount < 0 || result.MerchantDiscountAmount < 0 || result.SettlementAmount < 0 {
return errors.New("支付优惠或实付金额校验失败")
}
if result.AmountBreakdownKnown {
if err := validatePaymentBreakdown(result); err != nil {
return err
}
}
if strings.TrimSpace(intent.Currency) == "" || strings.TrimSpace(result.Currency) == "" || !strings.EqualFold(intent.Currency, result.Currency) {
return errors.New("支付币种校验失败")
}
if strings.TrimSpace(intent.BusinessType) == "" || strings.TrimSpace(intent.BusinessID) == "" {
return errors.New("支付业务标识不完整")
}
if provider == PaymentApple {
if err := validateAppleProduct(intent.Extra, result.Payload); err != nil {
return err
}
}
return nil
}
func validateAppleProduct(intentExtra, providerPayload json.RawMessage) error {
var expected struct {
ProductID string `json:"product_id"`
}
if err := json.Unmarshal(intentExtra, &expected); err != nil {
return errors.New("Apple 支付订单商品配置无效")
}
expected.ProductID = strings.TrimSpace(expected.ProductID)
if expected.ProductID == "" {
return errors.New("Apple 支付订单缺少 product_id")
}
var actual struct {
ProductID string `json:"productId"`
}
if err := json.Unmarshal(providerPayload, &actual); err != nil {
return errors.New("Apple 交易载荷无效")
}
actual.ProductID = strings.TrimSpace(actual.ProductID)
if actual.ProductID == "" || actual.ProductID != expected.ProductID {
return errors.New("Apple 交易 productId 与业务订单不匹配")
}
return nil
}
func validatePaymentBreakdown(result *PaymentResult) error {
if result == nil {
return errors.New("支付金额拆分为空")
}
payerCurrency := result.PayerCurrency
if payerCurrency == "" {
payerCurrency = result.Currency
}
if strings.EqualFold(payerCurrency, result.Currency) {
if result.PayerPaidAmount > result.Amount || result.DiscountAmount > result.Amount-result.PayerPaidAmount || result.PayerPaidAmount+result.DiscountAmount != result.Amount {
return errors.New("支付总额、实付和优惠金额不守恒")
}
if result.CashPaidAmount > result.PayerPaidAmount || result.PointPaidAmount > result.PayerPaidAmount-result.CashPaidAmount || result.CashPaidAmount+result.PointPaidAmount != result.PayerPaidAmount {
return errors.New("支付现金和积分金额不守恒")
}
}
if result.ProviderDiscountAmount > result.DiscountAmount || result.MerchantDiscountAmount > result.DiscountAmount-result.ProviderDiscountAmount {
return errors.New("支付优惠出资金额超过总优惠")
}
if result.SettlementAmount > result.Amount {
return errors.New("支付结算金额超过订单总额")
}
return nil
}
func paymentConfirmationID(provider, tradeNo string) string {
return uuid.NewSHA1(uuid.Nil, []byte(provider+"\x00"+tradeNo)).String()
}
func clonePaymentResult(result *PaymentResult) *PaymentResult {
if result == nil {
return nil
}
cloned := *result
cloned.Payload = append(json.RawMessage(nil), result.Payload...)
cloned.SuccessAck.Body = append([]byte(nil), result.SuccessAck.Body...)
cloned.FailureAck.Body = append([]byte(nil), result.FailureAck.Body...)
return &cloned
}