1210 lines
43 KiB
Go
1210 lines
43 KiB
Go
package payment
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
PaymentAlipay = "alipay"
|
|
PaymentAlipayV3 = "alipay-v3"
|
|
PaymentWechatV2 = "wechat-v2"
|
|
PaymentWechatV3 = "wechat-v3"
|
|
PaymentApple = "apple-iap"
|
|
PaymentChinaums = "chinaums"
|
|
PaymentSFT = "sft"
|
|
PaymentSuperPay = "supper-pay"
|
|
PaymentWechatGame = "wechat-game-pay"
|
|
PaymentDouyinGame = "douyin-game-pay"
|
|
// GoPay-backed providers. These are kept separate from the existing
|
|
// configuration-driven game/aggregator protocols above.
|
|
PaymentDouyin = "douyin"
|
|
PaymentQQ = "qq"
|
|
PaymentAllinPay = "allinpay"
|
|
PaymentLakala = "lakala"
|
|
PaymentPayPal = "paypal"
|
|
PaymentSaobei = "saobei"
|
|
PaymentInternal = "internal"
|
|
|
|
PaymentModeExternal = "external"
|
|
PaymentModeInternal = "internal"
|
|
)
|
|
|
|
var SupportedPaymentProviders = []string{
|
|
PaymentAlipay, PaymentAlipayV3, PaymentWechatV2, PaymentWechatV3, PaymentApple,
|
|
PaymentChinaums, PaymentSFT, PaymentSuperPay, PaymentWechatGame, PaymentDouyinGame,
|
|
PaymentDouyin, PaymentQQ, PaymentAllinPay, PaymentLakala, PaymentPayPal, PaymentSaobei,
|
|
}
|
|
|
|
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
|
|
Status string
|
|
TradeNo string
|
|
ProviderTradeNo string
|
|
Amount int64
|
|
PayerPaidAmount int64
|
|
CashPaidAmount int64
|
|
PointPaidAmount int64
|
|
DiscountAmount int64
|
|
ProviderDiscountAmount int64
|
|
MerchantDiscountAmount int64
|
|
SettlementAmount int64
|
|
Currency string
|
|
PayerCurrency string
|
|
AmountBreakdownKnown bool
|
|
Duplicate bool
|
|
Payload json.RawMessage
|
|
OrderStatus string
|
|
FulfillmentStatus string
|
|
RefundStatus string
|
|
EventID string
|
|
QueryID string
|
|
SuccessAck PaymentCallbackAck
|
|
FailureAck PaymentCallbackAck
|
|
}
|
|
|
|
// 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
|
|
Status string
|
|
Message string
|
|
TradeNo string
|
|
Duration int64
|
|
}
|
|
|
|
type PaymentTestResult struct {
|
|
Provider string
|
|
TradeNo string
|
|
Passed bool
|
|
FullFlow bool
|
|
Mode string
|
|
Stages []PaymentTestStage
|
|
Result *PaymentResult
|
|
}
|
|
|
|
type PaymentCallbackAck struct {
|
|
StatusCode int
|
|
ContentType string
|
|
Body []byte
|
|
}
|
|
|
|
type PaymentCallbackError struct {
|
|
Cause error
|
|
Ack PaymentCallbackAck
|
|
}
|
|
|
|
func (e *PaymentCallbackError) Error() string {
|
|
if e == nil || e.Cause == nil {
|
|
return "支付回调处理失败"
|
|
}
|
|
return e.Cause.Error()
|
|
}
|
|
|
|
func (e *PaymentCallbackError) Unwrap() error {
|
|
if e == nil {
|
|
return nil
|
|
}
|
|
return e.Cause
|
|
}
|
|
|
|
func CallbackFailure(err error, fallback PaymentCallbackAck) PaymentCallbackAck {
|
|
var callbackErr *PaymentCallbackError
|
|
if errors.As(err, &callbackErr) && callbackErr.Ack.StatusCode != 0 {
|
|
return callbackErr.Ack
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func DefaultPaymentCallbackAck(provider string, success bool) PaymentCallbackAck {
|
|
status := 200
|
|
if !success {
|
|
status = 500
|
|
}
|
|
switch provider {
|
|
case PaymentAlipay, PaymentAlipayV3:
|
|
body := "success"
|
|
if !success {
|
|
body = "failure"
|
|
}
|
|
return PaymentCallbackAck{StatusCode: status, ContentType: "text/plain; charset=utf-8", Body: []byte(body)}
|
|
case PaymentWechatV2:
|
|
code, message := "SUCCESS", "OK"
|
|
if !success {
|
|
code, message = "FAIL", "FAIL"
|
|
}
|
|
body := fmt.Sprintf("<xml><return_code><![CDATA[%s]]></return_code><return_msg><![CDATA[%s]]></return_msg></xml>", code, message)
|
|
return PaymentCallbackAck{StatusCode: status, ContentType: "application/xml; charset=utf-8", Body: []byte(body)}
|
|
case PaymentWechatV3:
|
|
code, message := "SUCCESS", "成功"
|
|
if !success {
|
|
code, message = "FAIL", "失败"
|
|
}
|
|
body, _ := json.Marshal(map[string]string{"code": code, "message": message})
|
|
return PaymentCallbackAck{StatusCode: status, ContentType: "application/json; charset=utf-8", Body: body}
|
|
case PaymentApple:
|
|
return PaymentCallbackAck{StatusCode: status}
|
|
default:
|
|
body := "success"
|
|
if !success {
|
|
body = "failure"
|
|
}
|
|
return PaymentCallbackAck{StatusCode: status, ContentType: "text/plain; charset=utf-8", Body: []byte(body)}
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
// PaymentAdapter is the integration boundary for provider protocols. SDK
|
|
// clients and wire formats stay outside the business and data packages.
|
|
type PaymentAdapter interface {
|
|
Create(context.Context, *PaymentRequest, map[string]any) (*PaymentResult, error)
|
|
Query(context.Context, string, map[string]any) (*PaymentResult, error)
|
|
Refund(context.Context, *PaymentRefundRequest, map[string]any) (*PaymentResult, error)
|
|
Callback(context.Context, *PaymentCallback, map[string]any) (*PaymentResult, error)
|
|
}
|
|
|
|
type PaymentAdapterFactory interface {
|
|
New(string) (PaymentAdapter, 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 }
|
|
|
|
func NewPaymentHooks() PaymentHooks { return NoopPaymentHooks{} }
|
|
|
|
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, hooks PaymentHooks, sources *PaymentOrderSourceRegistry, fulfillments *PaymentFulfillmentRegistry, appLogger *slog.Logger) *PaymentUsecase {
|
|
if hooks == nil {
|
|
hooks = NoopPaymentHooks{}
|
|
}
|
|
if sources == nil {
|
|
sources = NewPaymentOrderSourceRegistry()
|
|
}
|
|
if fulfillments == nil {
|
|
fulfillments = NewPaymentFulfillmentRegistry()
|
|
}
|
|
return &PaymentUsecase{
|
|
repo: repo,
|
|
orders: orders,
|
|
hooks: hooks,
|
|
sources: sources,
|
|
fulfillments: fulfillments,
|
|
logger: NewPaymentLogger(appLogger),
|
|
}
|
|
}
|
|
|
|
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("原始金额不能小于支付订单金额")
|
|
}
|
|
canonicalExtra, err := json.Marshal(req.Extra)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("支付扩展参数无效: %w", err)
|
|
}
|
|
canonicalFingerprint := paymentOrderFingerprint(req, canonicalExtra)
|
|
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 paymentOrderFingerprint(req, currentExtra) != canonicalFingerprint {
|
|
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()
|
|
}
|
|
order, err = uc.orders.CompletePaymentRefundRequest(ctx, provider, tradeNo, token, accepted, message)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !accepted {
|
|
return nil, providerErr
|
|
}
|
|
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
|
|
}
|