fix: lock recharge flow until virtual payment settles
This commit is contained in:
@@ -49,6 +49,7 @@ const {
|
||||
mockGetRpgProfileTasks,
|
||||
mockGetRpgProfileWalletLedger,
|
||||
mockRedeemRpgProfileReferralInviteCode,
|
||||
mockWatchWechatRpgProfileRechargeOrder,
|
||||
} = vi.hoisted(() => {
|
||||
const qrCodeToDataUrl = vi.fn(async () => 'data:image/png;base64,QR');
|
||||
const redirectToPaymentUrl = vi.fn();
|
||||
@@ -313,6 +314,7 @@ const {
|
||||
},
|
||||
],
|
||||
})),
|
||||
mockWatchWechatRpgProfileRechargeOrder: vi.fn(async () => null),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -379,6 +381,7 @@ vi.mock('../../services/rpg-entry/rpgProfileClient', () => ({
|
||||
createRpgProfileRechargeOrder: mockCreateRpgProfileRechargeOrder,
|
||||
confirmWechatRpgProfileRechargeOrder:
|
||||
mockConfirmWechatRpgProfileRechargeOrder,
|
||||
watchWechatRpgProfileRechargeOrder: mockWatchWechatRpgProfileRechargeOrder,
|
||||
}));
|
||||
|
||||
vi.mock('../ResolvedAssetImage', () => ({
|
||||
@@ -1407,7 +1410,7 @@ test('profile recharge modal posts virtual payment params in mini program web-vi
|
||||
'requestId',
|
||||
);
|
||||
expect(requestId).toBeTruthy();
|
||||
expect(screen.getByRole('dialog', { name: '正在支付' })).toBeTruthy();
|
||||
expect(screen.queryByRole('dialog', { name: '正在支付' })).toBeNull();
|
||||
act(() => {
|
||||
window.location.hash = `wx_pay_result=${requestId}:success:order-wechat-1`;
|
||||
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||
@@ -1928,6 +1931,234 @@ test('profile recharge modal waits for paid confirmation before refreshing dashb
|
||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('profile recharge modal confirms virtual payment after returning without hash result', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onRechargeSuccess = vi.fn();
|
||||
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
|
||||
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
|
||||
options.success?.();
|
||||
});
|
||||
window.wx = {
|
||||
miniProgram: {
|
||||
navigateTo,
|
||||
},
|
||||
};
|
||||
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-wechat-no-hash-paid',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null as string | null,
|
||||
providerTransactionId: null,
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
},
|
||||
wechatMiniProgramPayParams: {
|
||||
mode: 'short_series_coin',
|
||||
signData:
|
||||
'{"offerId":"offer-1","buyQuantity":60,"env":0,"currencyType":"CNY","outTradeNo":"order-wechat-no-hash-paid","attach":"mud_points_60"}',
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
},
|
||||
});
|
||||
mockConfirmWechatRpgProfileRechargeOrder
|
||||
.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-wechat-no-hash-paid',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-wechat-no-hash-paid',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'paid' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: '2026-04-25T10:01:00Z',
|
||||
providerTransactionId: 'wx-transaction-no-hash',
|
||||
pointsDelta: 120,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 120,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: true,
|
||||
},
|
||||
});
|
||||
|
||||
renderProfileView(onRechargeSuccess);
|
||||
await openRechargeModal(user);
|
||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new PageTransitionEvent('pageshow'));
|
||||
});
|
||||
expect(screen.getByRole('dialog', { name: '正在确认支付' })).toBeTruthy();
|
||||
await waitFor(() => {
|
||||
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
||||
'order-wechat-no-hash-paid',
|
||||
);
|
||||
});
|
||||
expect(await screen.findByRole('dialog', { name: '支付成功' })).toBeTruthy();
|
||||
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('profile recharge modal blocks tab navigation while virtual payment confirmation is pending', async () => {
|
||||
const user = userEvent.setup();
|
||||
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
|
||||
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
|
||||
options.success?.();
|
||||
});
|
||||
window.wx = {
|
||||
miniProgram: {
|
||||
navigateTo,
|
||||
},
|
||||
};
|
||||
mockCreateRpgProfileRechargeOrder.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-wechat-confirm-mask',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null as string | null,
|
||||
providerTransactionId: null,
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
},
|
||||
wechatMiniProgramPayParams: {
|
||||
mode: 'short_series_coin',
|
||||
signData:
|
||||
'{"offerId":"offer-1","buyQuantity":60,"env":0,"currencyType":"CNY","outTradeNo":"order-wechat-confirm-mask","attach":"mud_points_60"}',
|
||||
paySig: 'pay-sig',
|
||||
signature: 'user-sig',
|
||||
},
|
||||
});
|
||||
mockConfirmWechatRpgProfileRechargeOrder.mockResolvedValueOnce({
|
||||
order: {
|
||||
orderId: 'order-wechat-confirm-mask',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending' as const,
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
},
|
||||
center: {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
},
|
||||
});
|
||||
mockWatchWechatRpgProfileRechargeOrder.mockReturnValueOnce(new Promise(() => undefined));
|
||||
|
||||
renderProfileView();
|
||||
await openRechargeModal(user);
|
||||
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||
|
||||
act(() => {
|
||||
window.dispatchEvent(new PageTransitionEvent('pageshow'));
|
||||
});
|
||||
|
||||
expect(screen.getByRole('dialog', { name: '正在确认支付' })).toBeTruthy();
|
||||
expect(
|
||||
(screen.getByRole('button', { name: '创作' }) as HTMLButtonElement)
|
||||
.disabled,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('profile recharge modal loads wechat js sdk before mini program payment bridge', async () => {
|
||||
const user = userEvent.setup();
|
||||
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const { requestJsonMock } = vi.hoisted(() => ({
|
||||
const { fetchWithApiAuthMock, requestJsonMock } = vi.hoisted(() => ({
|
||||
fetchWithApiAuthMock: vi.fn(),
|
||||
requestJsonMock: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
submitRpgProfileFeedback,
|
||||
syncRpgProfileBrowseHistory,
|
||||
upsertRpgProfileBrowseHistory,
|
||||
watchWechatRpgProfileRechargeOrder,
|
||||
} from './rpgProfileClient';
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
@@ -21,9 +23,30 @@ vi.mock('../apiClient', () => ({
|
||||
notifyAuthStateChange: false,
|
||||
clearAuthOnUnauthorized: false,
|
||||
},
|
||||
fetchWithApiAuth: fetchWithApiAuthMock,
|
||||
requestJson: requestJsonMock,
|
||||
}));
|
||||
|
||||
function createSseResponse(bodyText: string) {
|
||||
return new Response(
|
||||
new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(bodyText));
|
||||
controller.close();
|
||||
},
|
||||
}),
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream; charset=utf-8',
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
fetchWithApiAuthMock.mockReset();
|
||||
});
|
||||
|
||||
describe('rpgProfileClient browse history routes', () => {
|
||||
beforeEach(() => {
|
||||
requestJsonMock.mockReset();
|
||||
@@ -231,3 +254,86 @@ describe('rpgProfileClient feedback routes', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('rpgProfileClient recharge order events', () => {
|
||||
beforeEach(() => {
|
||||
fetchWithApiAuthMock.mockReset();
|
||||
});
|
||||
|
||||
it('waits for a non-pending order event before completing the SSE watch', async () => {
|
||||
const pendingOrder = {
|
||||
orderId: 'order-wechat-sse-1',
|
||||
productId: 'points_60',
|
||||
productTitle: '60泥点',
|
||||
kind: 'points',
|
||||
amountCents: 600,
|
||||
status: 'pending',
|
||||
paymentChannel: 'wechat_mp_virtual',
|
||||
paidAt: null,
|
||||
providerTransactionId: null,
|
||||
createdAt: '2026-04-25T10:00:00Z',
|
||||
pointsDelta: 0,
|
||||
membershipExpiresAt: null,
|
||||
};
|
||||
const center = {
|
||||
walletBalance: 0,
|
||||
membership: {
|
||||
status: 'normal',
|
||||
tier: 'normal',
|
||||
startedAt: null,
|
||||
expiresAt: null,
|
||||
updatedAt: null,
|
||||
},
|
||||
pointProducts: [],
|
||||
membershipProducts: [],
|
||||
benefits: [],
|
||||
latestOrder: null,
|
||||
hasPointsRecharged: false,
|
||||
};
|
||||
const paidOrder = {
|
||||
...pendingOrder,
|
||||
status: 'paid',
|
||||
paidAt: '2026-04-25T10:01:00Z',
|
||||
providerTransactionId: 'wx-sse-1',
|
||||
pointsDelta: 120,
|
||||
};
|
||||
fetchWithApiAuthMock.mockResolvedValueOnce(
|
||||
createSseResponse(
|
||||
[
|
||||
'event: order',
|
||||
`data: ${JSON.stringify({ order: pendingOrder, center })}`,
|
||||
'',
|
||||
'event: order',
|
||||
`data: ${JSON.stringify({
|
||||
order: paidOrder,
|
||||
center: {
|
||||
...center,
|
||||
walletBalance: 120,
|
||||
hasPointsRecharged: true,
|
||||
},
|
||||
})}`,
|
||||
'',
|
||||
'event: done',
|
||||
'data: {"orderId":"order-wechat-sse-1","status":"paid"}',
|
||||
'',
|
||||
'',
|
||||
].join('\n'),
|
||||
),
|
||||
);
|
||||
|
||||
const result = await watchWechatRpgProfileRechargeOrder(
|
||||
'order-wechat-sse-1',
|
||||
);
|
||||
|
||||
expect(fetchWithApiAuthMock).toHaveBeenCalledWith(
|
||||
'/api/profile/recharge/orders/order-wechat-sse-1/wechat/events',
|
||||
expect.objectContaining({
|
||||
method: 'GET',
|
||||
headers: { Accept: 'text/event-stream' },
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(result.order.status).toBe('paid');
|
||||
expect(result.center.walletBalance).toBe(120);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -19,8 +19,10 @@ import type {
|
||||
SubmitProfileFeedbackRequest,
|
||||
SubmitProfileFeedbackResponse,
|
||||
} from '../../../packages/shared/src/contracts/runtime';
|
||||
import { appendApiErrorRequestId, parseApiErrorMessage } from '../../../packages/shared/src/http';
|
||||
import { rehydrateSavedSnapshot } from '../../persistence/runtimeSnapshot';
|
||||
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
|
||||
import { fetchWithApiAuth } from '../apiClient';
|
||||
import {
|
||||
RUNTIME_BACKGROUND_AUTH_OPTIONS,
|
||||
requestRpgRuntimeJson,
|
||||
@@ -116,6 +118,235 @@ export function confirmWechatRpgProfileRechargeOrder(
|
||||
);
|
||||
}
|
||||
|
||||
type RechargeOrderSseEvent =
|
||||
| {
|
||||
type: 'order';
|
||||
payload: ConfirmWechatProfileRechargeOrderResponse;
|
||||
}
|
||||
| {
|
||||
type: 'done';
|
||||
payload: { orderId: string; status: string };
|
||||
}
|
||||
| {
|
||||
type: 'error';
|
||||
payload: { message: string };
|
||||
};
|
||||
|
||||
function findSseEventBoundary(buffer: string) {
|
||||
const lfBoundary = buffer.indexOf('\n\n');
|
||||
const crlfBoundary = buffer.indexOf('\r\n\r\n');
|
||||
|
||||
if (lfBoundary === -1 && crlfBoundary === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lfBoundary === -1) {
|
||||
return {
|
||||
index: crlfBoundary,
|
||||
length: 4,
|
||||
};
|
||||
}
|
||||
|
||||
if (crlfBoundary === -1 || lfBoundary < crlfBoundary) {
|
||||
return {
|
||||
index: lfBoundary,
|
||||
length: 2,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
index: crlfBoundary,
|
||||
length: 4,
|
||||
};
|
||||
}
|
||||
|
||||
function parseSseEventBlock(eventBlock: string) {
|
||||
let eventName = 'message';
|
||||
const dataLines: string[] = [];
|
||||
|
||||
for (const rawLine of eventBlock.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
eventName = line.slice(6).trim() || 'message';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('data:')) {
|
||||
dataLines.push(line.slice(5).trim());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
eventName,
|
||||
data: dataLines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function parseJsonObject(data: string) {
|
||||
try {
|
||||
return JSON.parse(data) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeRechargeOrderSseEvent(
|
||||
eventName: string,
|
||||
parsed: Record<string, unknown>,
|
||||
): RechargeOrderSseEvent | null {
|
||||
if (eventName === 'order' && parsed.order && parsed.center) {
|
||||
return {
|
||||
type: 'order',
|
||||
payload: parsed as ConfirmWechatProfileRechargeOrderResponse,
|
||||
};
|
||||
}
|
||||
|
||||
if (eventName === 'done') {
|
||||
const orderId =
|
||||
typeof parsed.orderId === 'string' ? parsed.orderId.trim() : '';
|
||||
const status = typeof parsed.status === 'string' ? parsed.status.trim() : '';
|
||||
if (orderId && status) {
|
||||
return {
|
||||
type: 'done',
|
||||
payload: { orderId, status },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (eventName === 'error') {
|
||||
const message =
|
||||
typeof parsed.message === 'string' && parsed.message.trim()
|
||||
? parsed.message.trim()
|
||||
: '';
|
||||
return {
|
||||
type: 'error',
|
||||
payload: { message },
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function watchWechatRpgProfileRechargeOrder(
|
||||
orderId: string,
|
||||
options: RuntimeRequestOptions = {},
|
||||
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
|
||||
const response = await fetchWithApiAuth(
|
||||
`/api/profile/recharge/orders/${encodeURIComponent(orderId)}/wechat/events`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Accept: 'text/event-stream',
|
||||
},
|
||||
signal: options.signal,
|
||||
},
|
||||
{
|
||||
skipRefresh: options.skipRefresh,
|
||||
skipAuth: options.skipAuth,
|
||||
authImpact: options.authImpact,
|
||||
notifyAuthStateChange: options.notifyAuthStateChange,
|
||||
clearAuthOnUnauthorized: options.clearAuthOnUnauthorized,
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const responseText = await response.text();
|
||||
throw new Error(
|
||||
appendApiErrorRequestId(
|
||||
parseApiErrorMessage(responseText, '订阅充值订单状态失败'),
|
||||
response.headers.get('x-request-id'),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (!response.body) {
|
||||
throw new Error('streaming response body is unavailable');
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let finalResponse: ConfirmWechatProfileRechargeOrderResponse | null = null;
|
||||
let lastResponse: ConfirmWechatProfileRechargeOrderResponse | null = null;
|
||||
let streamDone = false;
|
||||
|
||||
const consumeBuffer = () => {
|
||||
for (;;) {
|
||||
const boundary = findSseEventBoundary(buffer);
|
||||
if (!boundary) {
|
||||
break;
|
||||
}
|
||||
|
||||
const eventBlock = buffer.slice(0, boundary.index);
|
||||
buffer = buffer.slice(boundary.index + boundary.length);
|
||||
const { eventName, data } = parseSseEventBlock(eventBlock);
|
||||
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = parseJsonObject(data);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
const normalized = normalizeRechargeOrderSseEvent(eventName, parsed);
|
||||
if (!normalized) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalized.type === 'order') {
|
||||
lastResponse = normalized.payload;
|
||||
if (normalized.payload.order.status !== 'pending') {
|
||||
finalResponse = normalized.payload;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (normalized.type === 'done') {
|
||||
streamDone = true;
|
||||
if (!finalResponse && lastResponse) {
|
||||
finalResponse = lastResponse;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new Error(normalized.payload.message || '订阅充值订单状态失败');
|
||||
}
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
consumeBuffer();
|
||||
if (finalResponse) {
|
||||
break;
|
||||
}
|
||||
if (streamDone) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
buffer += decoder.decode();
|
||||
consumeBuffer();
|
||||
|
||||
if (!finalResponse) {
|
||||
if (lastResponse) {
|
||||
finalResponse = lastResponse;
|
||||
}
|
||||
}
|
||||
|
||||
if (!finalResponse) {
|
||||
throw new Error('充值订单状态流返回不完整');
|
||||
}
|
||||
|
||||
return finalResponse;
|
||||
}
|
||||
|
||||
export function submitRpgProfileFeedback(
|
||||
payload: SubmitProfileFeedbackRequest,
|
||||
options: RuntimeRequestOptions = {},
|
||||
|
||||
Reference in New Issue
Block a user