优化结构

This commit is contained in:
Yvan
2026-08-21 08:54:29 +08:00
parent c968f9f24e
commit 77e192b6c0
167 changed files with 3657 additions and 1528 deletions
+44 -15
View File
@@ -493,6 +493,30 @@ func (uc *PaymentUsecase) Refund(ctx context.Context, provider, tradeNo string,
return uc.refundWithOrder(ctx, provider, tradeNo, amount)
}
// 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.
@@ -671,10 +695,21 @@ func (uc *PaymentUsecase) reconcilePaymentResult(ctx context.Context, result, ca
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 fail(err)
return nil, err
}
if duplicate {
result.Duplicate = true
@@ -685,29 +720,23 @@ func (uc *PaymentUsecase) reconcilePaymentResult(ctx context.Context, result, ca
if handler == nil {
err = fmt.Errorf("支付发货 handler 未注册: %s", order.BusinessType)
_, _ = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, false, err.Error())
uc.logger.Error(ctx, "payment.order.fulfillment_handler_missing", "provider", result.Provider, "trade_no", result.TradeNo, "business_type", order.BusinessType, "error", err)
return fail(err)
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,
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(),
}
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())
uc.logger.Error(ctx, "payment.order.fulfillment_failed", "provider", result.Provider, "trade_no", result.TradeNo, "confirmation_id", confirmation.ID, "error", err)
return fail(err)
return nil, err
}
order, err = uc.orders.CompletePaymentFulfillment(ctx, result.Provider, result.TradeNo, token, true, "")
if err != nil {
uc.logger.Error(ctx, "payment.order.fulfillment_record_failed", "provider", result.Provider, "trade_no", result.TradeNo, "confirmation_id", confirmation.ID, "error", err)
return fail(err)
return nil, err
}
result.Status = "fulfilled"
return attachOrderResult(result, order), nil
+18
View File
@@ -330,6 +330,24 @@ func TestPersistentPaymentCallbackFulfillsAndPersistsDuplicate(t *testing.T) {
}
}
func TestPaymentFulfillRetriesFailedOrder(t *testing.T) {
orders := &paymentOrderRepoStub{order: &PaymentOrder{Provider: PaymentAlipay, TradeNo: "order-1", BusinessType: "game_item", BusinessID: "item-1", Subject: "item", Amount: 100, Currency: "CNY", PaymentStatus: PaymentStatusPaid, FulfillmentStatus: FulfillmentStatusFailed, RefundStatus: RefundStatusNone, ConfirmationID: "11111111-1111-1111-1111-111111111111"}}
uc := NewPaymentUsecase(&paymentRepoStub{}, orders, slog.New(slog.NewTextHandler(io.Discard, nil)))
handler := &paymentFulfillmentStub{}
registry := NewPaymentFulfillmentRegistry()
if err := registry.Register(handler); err != nil {
t.Fatal(err)
}
uc.SetFulfillmentRegistry(registry)
result, err := uc.Fulfill(context.Background(), PaymentAlipay, "order-1")
if err != nil {
t.Fatal(err)
}
if result.Status != "fulfilled" || handler.calls != 1 || orders.order.FulfillmentStatus != FulfillmentStatusSucceeded {
t.Fatalf("fulfillment retry failed: result=%+v calls=%d status=%s", result, handler.calls, orders.order.FulfillmentStatus)
}
}
func TestPaymentConfirmationAcceptsProviderDiscount(t *testing.T) {
intent := &PaymentIntent{Provider: PaymentAlipay, TradeNo: "T1", Amount: 1000, Currency: "CNY", BusinessType: "item", BusinessID: "I1"}
result := &PaymentResult{Provider: PaymentAlipay, TradeNo: "T1", ProviderTradeNo: "P1", Status: "success", Amount: 1000, PayerPaidAmount: 800, CashPaidAmount: 800, DiscountAmount: 200, ProviderDiscountAmount: 150, MerchantDiscountAmount: 50, SettlementAmount: 950, Currency: "CNY", AmountBreakdownKnown: true}