mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
Merge pull request #6550 from weiness/fix/alipay-payment-reconciliation
修复支付宝回调延迟或丢失,已支付订单可能长时间停留在待支付状态的问题
This commit is contained in:
@@ -98,12 +98,12 @@ func (s *PaymentOrderExpiryService) runOnce() {
|
||||
defer release()
|
||||
|
||||
reconcileCtx, cancel := context.WithTimeout(context.Background(), expiryCheckTimeout)
|
||||
recovered, err := s.paymentSvc.ReconcilePendingWxpayOrders(reconcileCtx)
|
||||
recovered, err := s.paymentSvc.ReconcilePendingPaymentOrders(reconcileCtx)
|
||||
cancel()
|
||||
if err != nil {
|
||||
slog.Warn("[PaymentOrderExpiry] failed to reconcile pending wxpay orders", "error", err)
|
||||
slog.Warn("[PaymentOrderExpiry] failed to reconcile pending payment orders", "error", err)
|
||||
} else if recovered > 0 {
|
||||
slog.Info("[PaymentOrderExpiry] reconciled paid wxpay orders", "count", recovered)
|
||||
slog.Info("[PaymentOrderExpiry] reconciled paid orders", "count", recovered)
|
||||
}
|
||||
|
||||
expireCtx, cancel := context.WithTimeout(context.Background(), expiryCheckTimeout)
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
checkPaidResultAlreadyPaid = "already_paid"
|
||||
checkPaidResultCancelled = "cancelled"
|
||||
|
||||
pendingWxpayReconcileLimit = 20
|
||||
pendingPaymentReconcileLimit = 20
|
||||
)
|
||||
|
||||
type checkPaidOptions struct {
|
||||
@@ -303,9 +303,9 @@ func (s *PaymentService) VerifyOrderByOutTradeNo(ctx context.Context, outTradeNo
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// ReconcilePendingWxpayOrders actively checks recent pending WeChat orders so
|
||||
// missed provider notifications do not wait until order expiry to fulfill.
|
||||
func (s *PaymentService) ReconcilePendingWxpayOrders(ctx context.Context) (int, error) {
|
||||
// ReconcilePendingPaymentOrders actively checks recent pending Alipay and WeChat
|
||||
// orders so missed provider notifications do not wait until order expiry to fulfill.
|
||||
func (s *PaymentService) ReconcilePendingPaymentOrders(ctx context.Context) (int, error) {
|
||||
now := time.Now()
|
||||
orders, err := s.entClient.PaymentOrder.Query().
|
||||
Where(
|
||||
@@ -316,13 +316,17 @@ func (s *PaymentService) ReconcilePendingWxpayOrders(ctx context.Context) (int,
|
||||
paymentorder.PaymentTypeHasPrefix(payment.TypeWxpay+"_"),
|
||||
paymentorder.ProviderKeyEQ(payment.TypeWxpay),
|
||||
paymentorder.ProviderKeyHasPrefix(payment.TypeWxpay+"_"),
|
||||
paymentorder.PaymentTypeEQ(payment.TypeAlipay),
|
||||
paymentorder.PaymentTypeHasPrefix(payment.TypeAlipay+"_"),
|
||||
paymentorder.ProviderKeyEQ(payment.TypeAlipay),
|
||||
paymentorder.ProviderKeyHasPrefix(payment.TypeAlipay+"_"),
|
||||
),
|
||||
).
|
||||
Order(dbent.Asc(paymentorder.FieldCreatedAt)).
|
||||
Limit(pendingWxpayReconcileLimit).
|
||||
Limit(pendingPaymentReconcileLimit).
|
||||
All(ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("query pending wxpay orders: %w", err)
|
||||
return 0, fmt.Errorf("query pending payment orders: %w", err)
|
||||
}
|
||||
|
||||
recovered := 0
|
||||
|
||||
@@ -567,7 +567,7 @@ func TestCancelOrderStillClosesUnpaidUpstreamOrder(t *testing.T) {
|
||||
require.Equal(t, OrderStatusCancelled, reloaded.Status)
|
||||
}
|
||||
|
||||
func TestReconcilePendingWxpayOrdersBackfillsPaidOrder(t *testing.T) {
|
||||
func TestReconcilePendingPaymentOrdersBackfillsPaidOrder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := newPaymentOrderLifecycleTestClient(t)
|
||||
|
||||
@@ -655,7 +655,7 @@ func TestReconcilePendingWxpayOrdersBackfillsPaidOrder(t *testing.T) {
|
||||
providersLoaded: true,
|
||||
}
|
||||
|
||||
recovered, err := svc.ReconcilePendingWxpayOrders(ctx)
|
||||
recovered, err := svc.ReconcilePendingPaymentOrders(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, 1, recovered)
|
||||
require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
|
||||
@@ -669,6 +669,59 @@ func TestReconcilePendingWxpayOrdersBackfillsPaidOrder(t *testing.T) {
|
||||
require.Len(t, redeemRepo.useCalls, 1)
|
||||
}
|
||||
|
||||
func TestReconcilePendingPaymentOrdersQueriesAlipayOrder(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := newPaymentOrderLifecycleTestClient(t)
|
||||
|
||||
user, err := client.User.Create().
|
||||
SetEmail("alipay-reconcile@example.com").
|
||||
SetPasswordHash("hash").
|
||||
SetUsername("alipay-reconcile-user").
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
order, err := client.PaymentOrder.Create().
|
||||
SetUserID(user.ID).
|
||||
SetUserEmail(user.Email).
|
||||
SetUserName(user.Username).
|
||||
SetAmount(50).
|
||||
SetPayAmount(50).
|
||||
SetFeeRate(0).
|
||||
SetRechargeCode("ALIPAY-RECONCILE").
|
||||
SetOutTradeNo("sub2_alipay_reconcile").
|
||||
SetPaymentType(payment.TypeAlipay).
|
||||
SetPaymentTradeNo("").
|
||||
SetOrderType(payment.OrderTypeBalance).
|
||||
SetStatus(OrderStatusPending).
|
||||
SetExpiresAt(time.Now().Add(time.Hour)).
|
||||
SetClientIP("127.0.0.1").
|
||||
SetSrcHost("api.example.com").
|
||||
Save(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
registry := payment.NewRegistry()
|
||||
provider := &paymentOrderLifecycleQueryProvider{
|
||||
key: payment.TypeAlipay,
|
||||
resp: &payment.QueryOrderResponse{
|
||||
TradeNo: order.OutTradeNo,
|
||||
Status: payment.ProviderStatusPending,
|
||||
},
|
||||
}
|
||||
registry.Register(provider)
|
||||
|
||||
svc := &PaymentService{
|
||||
entClient: client,
|
||||
registry: registry,
|
||||
providersLoaded: true,
|
||||
}
|
||||
|
||||
recovered, err := svc.ReconcilePendingPaymentOrders(ctx)
|
||||
require.NoError(t, err)
|
||||
require.Zero(t, recovered)
|
||||
require.Equal(t, 1, provider.queryCalls)
|
||||
require.Equal(t, order.OutTradeNo, provider.lastQueryTradeNo)
|
||||
}
|
||||
|
||||
func TestVerifyOrderByOutTradeNoUsesOutTradeNoWhenPaymentTradeNoAlreadyExistsForAlipay(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
client := newPaymentOrderLifecycleTestClient(t)
|
||||
|
||||
@@ -393,7 +393,7 @@ function saveQRCode() {
|
||||
}
|
||||
|
||||
async function tryRecoverPendingOrder(order: PaymentOrder): Promise<PaymentOrder> {
|
||||
if (!isWxpay.value && !isMobileAlipayDeepLink.value) return order
|
||||
if (!isWxpay.value && !isAlipay.value) return order
|
||||
const outTradeNo = String(order.out_trade_no || '').trim()
|
||||
if (!outTradeNo) return order
|
||||
const normalizedStatus = String(order.status || '').trim().toUpperCase()
|
||||
|
||||
@@ -226,6 +226,34 @@ describe('PaymentStatusPanel', () => {
|
||||
if (originalHidden) Object.defineProperty(document, 'hidden', originalHidden)
|
||||
})
|
||||
|
||||
it('actively verifies a pending desktop Alipay order', async () => {
|
||||
pollOrderStatus.mockResolvedValue(orderFactory('PENDING'))
|
||||
verifyOrder.mockResolvedValue({ data: orderFactory('COMPLETED') })
|
||||
|
||||
const wrapper = mount(PaymentStatusPanel, {
|
||||
props: {
|
||||
orderId: 42,
|
||||
amount: 88,
|
||||
payAmount: 88,
|
||||
qrCode: 'https://qr.alipay.com/desktop-order-42',
|
||||
expiresAt: '2099-01-01T12:30:00Z',
|
||||
paymentType: 'alipay',
|
||||
orderType: 'balance',
|
||||
outTradeNo: 'sub2_20260420abcd1234',
|
||||
},
|
||||
global: { stubs: { Icon: true } },
|
||||
})
|
||||
|
||||
await flushPromises()
|
||||
await vi.advanceTimersByTimeAsync(3000)
|
||||
await flushPromises()
|
||||
|
||||
expect(verifyOrder).toHaveBeenCalledWith('sub2_20260420abcd1234')
|
||||
expect(wrapper.emitted('success')).toHaveLength(1)
|
||||
|
||||
wrapper.unmount()
|
||||
})
|
||||
|
||||
it('keeps the QR fallback hidden until the Alipay app launch times out', async () => {
|
||||
const originalLocation = window.location
|
||||
const originalHidden = Object.getOwnPropertyDescriptor(document, 'hidden')
|
||||
|
||||
Reference in New Issue
Block a user