221 lines
6.8 KiB
Go
221 lines
6.8 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
const (
|
|
PaymentStatusInitialized = "initialized"
|
|
PaymentStatusPending = "pending"
|
|
PaymentStatusPaid = "paid"
|
|
PaymentStatusFailed = "failed"
|
|
PaymentStatusClosed = "closed"
|
|
PaymentStatusPartiallyRefunded = "partially_refunded"
|
|
PaymentStatusRefunded = "refunded"
|
|
|
|
FulfillmentStatusPending = "pending"
|
|
FulfillmentStatusProcessing = "processing"
|
|
FulfillmentStatusSucceeded = "succeeded"
|
|
FulfillmentStatusFailed = "failed"
|
|
|
|
RefundStatusNone = "none"
|
|
RefundStatusProcessing = "processing"
|
|
RefundStatusPending = "pending"
|
|
RefundStatusPartial = "partial"
|
|
RefundStatusSucceeded = "succeeded"
|
|
RefundStatusFailed = "failed"
|
|
)
|
|
|
|
var (
|
|
ErrPaymentOrderNotFound = errors.New("支付订单不存在")
|
|
ErrPaymentOrderConflict = errors.New("支付订单参数冲突")
|
|
ErrPaymentOrderBusy = errors.New("支付订单正在处理中")
|
|
ErrPaymentOrderState = errors.New("支付订单状态不允许当前操作")
|
|
ErrPaymentProviderConflict = errors.New("支付平台交易号冲突")
|
|
)
|
|
|
|
// PaymentOrder is one payment attempt. Business modules own their domain
|
|
// orders and link to this record through BusinessType and BusinessID.
|
|
type PaymentOrder struct {
|
|
ID uint64
|
|
TradeNo string
|
|
Provider string
|
|
ProviderTradeNo string
|
|
QueryID string
|
|
BusinessType string
|
|
BusinessID string
|
|
Subject string
|
|
PaymentMode string
|
|
OriginalAmount int64
|
|
Amount int64
|
|
PaidAmount int64
|
|
PayerPaidAmount int64
|
|
CashPaidAmount int64
|
|
PointPaidAmount int64
|
|
DiscountAmount int64
|
|
ProviderDiscountAmount int64
|
|
MerchantDiscountAmount int64
|
|
SettlementAmount int64
|
|
Currency string
|
|
PayerCurrency string
|
|
AmountBreakdownKnown bool
|
|
PaymentStatus string
|
|
ProviderStatus string
|
|
FulfillmentStatus string
|
|
RefundStatus string
|
|
RefundedAmount int64
|
|
RefundRequestedAmount int64
|
|
RefundNo string
|
|
ConfirmationID string
|
|
RequestFingerprint string
|
|
CreatePayload json.RawMessage
|
|
Extra json.RawMessage
|
|
LastEventID string
|
|
LastPayloadHash string
|
|
LastError string
|
|
FulfillmentToken string
|
|
RefundToken string
|
|
Version uint64
|
|
CreatedAt time.Time
|
|
UpdatedAt time.Time
|
|
PaidAt *time.Time
|
|
FulfilledAt *time.Time
|
|
RefundedAt *time.Time
|
|
FulfillmentLeaseUntil *time.Time
|
|
RefundLeaseUntil *time.Time
|
|
}
|
|
|
|
type PaymentProviderUpdate struct {
|
|
Status string
|
|
ProviderStatus string
|
|
ProviderTradeNo string
|
|
QueryID string
|
|
Amount int64
|
|
PayerPaidAmount int64
|
|
CashPaidAmount int64
|
|
PointPaidAmount int64
|
|
DiscountAmount int64
|
|
ProviderDiscountAmount int64
|
|
MerchantDiscountAmount int64
|
|
SettlementAmount int64
|
|
Currency string
|
|
PayerCurrency string
|
|
AmountBreakdownKnown bool
|
|
EventID string
|
|
PayloadHash string
|
|
CreatePayload json.RawMessage
|
|
}
|
|
|
|
type PaymentOrderFilter struct {
|
|
Provider string
|
|
TradeNo string
|
|
BusinessType string
|
|
BusinessID string
|
|
PaymentStatus string
|
|
RefundStatus string
|
|
}
|
|
|
|
type PaymentOrderRepo interface {
|
|
CreatePaymentOrder(context.Context, *PaymentOrder) (order *PaymentOrder, created bool, err error)
|
|
FindPaymentOrder(context.Context, string, string) (*PaymentOrder, error)
|
|
ListPaymentOrders(context.Context, int, int, PaymentOrderFilter) ([]*PaymentOrder, int64, error)
|
|
RecordPaymentCreate(context.Context, string, string, *PaymentProviderUpdate) (*PaymentOrder, error)
|
|
ApplyPaymentResult(context.Context, string, string, *PaymentProviderUpdate) (*PaymentOrder, error)
|
|
BeginPaymentFulfillment(context.Context, string, string, time.Duration) (order *PaymentOrder, token string, duplicate bool, err error)
|
|
CompletePaymentFulfillment(context.Context, string, string, string, bool, string) (*PaymentOrder, error)
|
|
BeginPaymentRefund(context.Context, string, string, int64, time.Duration) (order *PaymentOrder, token string, err error)
|
|
CompletePaymentRefundRequest(context.Context, string, string, string, bool, string) (*PaymentOrder, error)
|
|
ConfirmPaymentRefund(context.Context, string, string, string, int64, bool, string) (*PaymentOrder, error)
|
|
}
|
|
|
|
// PaymentOrderSource is implemented by each business module. It returns the
|
|
// canonical order facts; client-provided amount, currency and subject are not
|
|
// trusted once the persistent payment-order flow is enabled.
|
|
type PaymentOrderSource interface {
|
|
Type() string
|
|
PreparePayment(context.Context, string, string, string) (*PaymentIntent, error)
|
|
}
|
|
|
|
type PaymentBusinessModule interface {
|
|
PaymentOrderSource
|
|
PaymentFulfillmentHandler
|
|
PaymentRefundAuthorizer
|
|
}
|
|
|
|
type PaymentRefundAuthorizer interface {
|
|
AuthorizeRefund(context.Context, *PaymentOrder, int64) error
|
|
}
|
|
|
|
type PaymentOrderSourceRegistry struct {
|
|
mu sync.RWMutex
|
|
sources map[string]PaymentOrderSource
|
|
}
|
|
|
|
func NewPaymentOrderSourceRegistry() *PaymentOrderSourceRegistry {
|
|
return &PaymentOrderSourceRegistry{sources: map[string]PaymentOrderSource{}}
|
|
}
|
|
|
|
func (r *PaymentOrderSourceRegistry) Register(source PaymentOrderSource) error {
|
|
if source == nil || strings.TrimSpace(source.Type()) == "" {
|
|
return errors.New("支付业务订单来源无效")
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
if _, exists := r.sources[source.Type()]; exists {
|
|
return errors.New("支付业务订单来源已注册: " + source.Type())
|
|
}
|
|
r.sources[source.Type()] = source
|
|
return nil
|
|
}
|
|
|
|
func (r *PaymentOrderSourceRegistry) Source(kind string) PaymentOrderSource {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
r.mu.RLock()
|
|
defer r.mu.RUnlock()
|
|
return r.sources[kind]
|
|
}
|
|
|
|
func (r *PaymentOrderSourceRegistry) Unregister(kind string) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
delete(r.sources, kind)
|
|
}
|
|
|
|
func (o *PaymentOrder) Intent() *PaymentIntent {
|
|
if o == nil {
|
|
return nil
|
|
}
|
|
return &PaymentIntent{
|
|
Provider: o.Provider,
|
|
TradeNo: o.TradeNo,
|
|
BusinessType: o.BusinessType,
|
|
BusinessID: o.BusinessID,
|
|
Subject: o.Subject,
|
|
Amount: o.Amount,
|
|
Currency: o.Currency,
|
|
Extra: append(json.RawMessage(nil), o.Extra...),
|
|
PaymentMode: defaultPaymentMode(o.Provider, o.PaymentMode),
|
|
OriginalAmount: o.OriginalAmount,
|
|
}
|
|
}
|
|
|
|
func defaultPaymentMode(provider, mode string) string {
|
|
if strings.TrimSpace(mode) != "" {
|
|
return mode
|
|
}
|
|
if provider == PaymentInternal {
|
|
return PaymentModeInternal
|
|
}
|
|
return PaymentModeExternal
|
|
}
|