fix: 刷新微信支付到账泥点
This commit is contained in:
@@ -145,6 +145,7 @@
|
|||||||
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品。
|
- 小程序 web-view 内的 H5 只负责加载微信 JS-SDK 并通过 `wx.miniProgram.navigateTo` 跳转到 `/pages/wechat-pay/index`;实际支付必须在小程序 native 页调用 `wx.requestPayment`,不要切换为 H5 支付产品。
|
||||||
- native 支付页通过 `wx_pay_result=<requestId>:success|cancel|fail` 回填 web-view;H5 在 `hashchange`、`focus`、`pageshow` 和 `visibilitychange` 中都会尝试消费该结果,避免小程序返回 web-view 时没有触发单一事件导致状态不刷新。
|
- native 支付页通过 `wx_pay_result=<requestId>:success|cancel|fail` 回填 web-view;H5 在 `hashchange`、`focus`、`pageshow` 和 `visibilitychange` 中都会尝试消费该结果,避免小程序返回 web-view 时没有触发单一事件导致状态不刷新。
|
||||||
- `success` 只表示微信客户端支付流程返回成功,前端随后调用 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 由服务端查单确认;只有通知或服务端查单确认为 `SUCCESS` 才入账。
|
- `success` 只表示微信客户端支付流程返回成功,前端随后调用 `POST /api/profile/recharge/orders/{order_id}/wechat/confirm` 由服务端查单确认;只有通知或服务端查单确认为 `SUCCESS` 才入账。
|
||||||
|
- 小程序返回后,前端会对确认接口做短轮询,覆盖微信通知/查单结果与 web-view 恢复之间的秒级时间差;只有确认响应里的订单状态变成 `paid` 后,才触发父级 `profileDashboard` 刷新,确保“我的”页泥点卡片读取到最新余额。
|
||||||
- `cancel` 和 `fail` 只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。
|
- `cancel` 和 `fail` 只复位按钮、刷新账户中心并通过全局支付结果模态展示,不调用入账逻辑。
|
||||||
5. 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。
|
5. 支付结果使用页面级全局模态展示,不写回商品卡片或账户充值弹窗内部;充值弹窗只负责套餐选择、加载失败和下单失败。
|
||||||
6. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。
|
6. 弹窗内不写大段说明文案,只保留必要金额、泥点、会员权益和操作状态。
|
||||||
|
|||||||
@@ -1040,6 +1040,7 @@ test('profile recharge modal buys points through mock channel outside mini progr
|
|||||||
|
|
||||||
test('profile recharge modal posts requestPayment params in mini program web-view', async () => {
|
test('profile recharge modal posts requestPayment params in mini program web-view', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
const onRechargeSuccess = vi.fn();
|
||||||
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
|
window.history.replaceState(null, '', '/?clientRuntime=wechat_mini_program');
|
||||||
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
|
const navigateTo = vi.fn((options: { url: string; success?: () => void }) => {
|
||||||
options.success?.();
|
options.success?.();
|
||||||
@@ -1088,7 +1089,7 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
renderProfileView();
|
renderProfileView(onRechargeSuccess);
|
||||||
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||||
await user.click(
|
await user.click(
|
||||||
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||||
@@ -1124,6 +1125,151 @@ test('profile recharge modal posts requestPayment params in mini program web-vie
|
|||||||
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledWith(
|
||||||
'order-wechat-1',
|
'order-wechat-1',
|
||||||
);
|
);
|
||||||
|
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('profile recharge modal waits for paid confirmation before refreshing dashboard', 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-pending-then-paid',
|
||||||
|
productId: 'points_60',
|
||||||
|
productTitle: '60泥点',
|
||||||
|
kind: 'points',
|
||||||
|
amountCents: 600,
|
||||||
|
status: 'pending' as const,
|
||||||
|
paymentChannel: 'wechat_mp',
|
||||||
|
paidAt: null as string | null,
|
||||||
|
providerTransactionId: null,
|
||||||
|
createdAt: '2026-04-25T10:00:00Z',
|
||||||
|
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: {
|
||||||
|
timeStamp: '1777110165',
|
||||||
|
nonceStr: 'nonce',
|
||||||
|
package: 'prepay_id=wx-prepay',
|
||||||
|
signType: 'RSA',
|
||||||
|
paySign: 'signature',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
mockConfirmWechatRpgProfileRechargeOrder
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
order: {
|
||||||
|
orderId: 'order-wechat-pending-then-paid',
|
||||||
|
productId: 'points_60',
|
||||||
|
productTitle: '60泥点',
|
||||||
|
kind: 'points',
|
||||||
|
amountCents: 600,
|
||||||
|
status: 'pending' as const,
|
||||||
|
paymentChannel: 'wechat_mp',
|
||||||
|
paidAt: null,
|
||||||
|
providerTransactionId: null,
|
||||||
|
createdAt: '2026-04-25T10:00:00Z',
|
||||||
|
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-pending-then-paid',
|
||||||
|
productId: 'points_60',
|
||||||
|
productTitle: '60泥点',
|
||||||
|
kind: 'points',
|
||||||
|
amountCents: 600,
|
||||||
|
status: 'paid' as const,
|
||||||
|
paymentChannel: 'wechat_mp',
|
||||||
|
paidAt: '2026-04-25T10:01:00Z',
|
||||||
|
providerTransactionId: 'wx-transaction-2',
|
||||||
|
createdAt: '2026-04-25T10:00:00Z',
|
||||||
|
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);
|
||||||
|
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
|
||||||
|
await user.click(
|
||||||
|
within(shortcutRegion).getByRole('button', { name: /充值/u }),
|
||||||
|
);
|
||||||
|
await user.click(await screen.findByRole('button', { name: /60泥点/u }));
|
||||||
|
|
||||||
|
const navigateUrl = navigateTo.mock.calls[0]?.[0].url ?? '';
|
||||||
|
const requestId = new URL(`https://mini.test${navigateUrl}`).searchParams.get(
|
||||||
|
'requestId',
|
||||||
|
);
|
||||||
|
expect(requestId).toBeTruthy();
|
||||||
|
await act(async () => {
|
||||||
|
window.location.hash = `wx_pay_result=${requestId}:success`;
|
||||||
|
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onRechargeSuccess).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockConfirmWechatRpgProfileRechargeOrder).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await screen.findByRole('dialog', { name: '支付成功' }),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('profile recharge modal loads wechat js sdk before mini program payment bridge', async () => {
|
test('profile recharge modal loads wechat js sdk before mini program payment bridge', async () => {
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import type { PublicUserSummary } from '../../../packages/shared/src/contracts/a
|
|||||||
import type {
|
import type {
|
||||||
CustomWorldLibraryEntry,
|
CustomWorldLibraryEntry,
|
||||||
PlatformBrowseHistoryEntry,
|
PlatformBrowseHistoryEntry,
|
||||||
|
ConfirmWechatProfileRechargeOrderResponse,
|
||||||
ProfileDashboardCardKey,
|
ProfileDashboardCardKey,
|
||||||
ProfileDashboardSummary,
|
ProfileDashboardSummary,
|
||||||
ProfilePlayedWorkSummary,
|
ProfilePlayedWorkSummary,
|
||||||
@@ -216,6 +217,7 @@ const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
|
|||||||
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
|
||||||
const WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL = 'wechat_mp';
|
const WECHAT_MINI_PROGRAM_PAYMENT_CHANNEL = 'wechat_mp';
|
||||||
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
|
||||||
|
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
|
||||||
|
|
||||||
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
||||||
type RechargeTab = 'points' | 'membership';
|
type RechargeTab = 'points' | 'membership';
|
||||||
@@ -2461,6 +2463,32 @@ async function requestWechatMiniProgramPayment(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function waitWechatPayConfirmDelay(delayMs: number) {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
window.setTimeout(resolve, delayMs);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmWechatRechargeOrderUntilSettled(
|
||||||
|
orderId: string,
|
||||||
|
): Promise<ConfirmWechatProfileRechargeOrderResponse> {
|
||||||
|
let latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
|
||||||
|
if (latestResponse.order.status === 'paid') {
|
||||||
|
return latestResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const delayMs of WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS) {
|
||||||
|
await waitWechatPayConfirmDelay(delayMs);
|
||||||
|
|
||||||
|
latestResponse = await confirmWechatRpgProfileRechargeOrder(orderId);
|
||||||
|
if (latestResponse.order.status === 'paid') {
|
||||||
|
return latestResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return latestResponse;
|
||||||
|
}
|
||||||
|
|
||||||
function RechargeProductCard({
|
function RechargeProductCard({
|
||||||
product,
|
product,
|
||||||
submittingProductId,
|
submittingProductId,
|
||||||
@@ -3934,11 +3962,12 @@ export function RpgEntryHomeView({
|
|||||||
message: '正在确认到账状态,请稍后查看余额或会员状态。',
|
message: '正在确认到账状态,请稍后查看余额或会员状态。',
|
||||||
});
|
});
|
||||||
if (payResult.orderId) {
|
if (payResult.orderId) {
|
||||||
void confirmWechatRpgProfileRechargeOrder(payResult.orderId)
|
void confirmWechatRechargeOrderUntilSettled(payResult.orderId)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
const isPaid = response.order.status === 'paid';
|
||||||
setRechargeCenter(response.center);
|
setRechargeCenter(response.center);
|
||||||
setRechargePaymentResult(
|
setRechargePaymentResult(
|
||||||
response.order.status === 'paid'
|
isPaid
|
||||||
? {
|
? {
|
||||||
kind: 'success',
|
kind: 'success',
|
||||||
title: '支付成功',
|
title: '支付成功',
|
||||||
@@ -3950,6 +3979,9 @@ export function RpgEntryHomeView({
|
|||||||
message: '正在等待微信支付确认,请稍后查看账户状态。',
|
message: '正在等待微信支付确认,请稍后查看账户状态。',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
if (isPaid) {
|
||||||
|
void onRechargeSuccess?.();
|
||||||
|
}
|
||||||
setSubmittingRechargeProductId(null);
|
setSubmittingRechargeProductId(null);
|
||||||
pendingWechatRechargeOrderIdRef.current = null;
|
pendingWechatRechargeOrderIdRef.current = null;
|
||||||
})
|
})
|
||||||
@@ -3964,7 +3996,6 @@ export function RpgEntryHomeView({
|
|||||||
} else {
|
} else {
|
||||||
refreshRechargeState();
|
refreshRechargeState();
|
||||||
}
|
}
|
||||||
void onRechargeSuccess?.();
|
|
||||||
} else if (payResult.status === 'cancel') {
|
} else if (payResult.status === 'cancel') {
|
||||||
setRechargePaymentResult({
|
setRechargePaymentResult({
|
||||||
kind: 'cancel',
|
kind: 'cancel',
|
||||||
|
|||||||
Reference in New Issue
Block a user