فهرست منبع

fix(wechat-pay): 按JSAPI官方文档规范修复支付流程

- � 支付后查询订单确认:wx.chooseWXPay回调不直接展示成功,改为调query API确认trade_state=SUCCESS
- � fail回调也加查询确认(fail不一定代表支付失败)
- � 防重debounce:handlePay入口2秒防抖
- � time_expire:JSAPI和NATIVE下单都设置2小时过期时间
MyFramework User 3 ماه پیش
والد
کامیت
3fe838ec41
2فایلهای تغییر یافته به همراه77 افزوده شده و 24 حذف شده
  1. 67 24
      my-uniapp-vue3/src/pages/payment-confirm/index.vue
  2. 10 0
      server/src/modules/payment/payment.service.ts

+ 67 - 24
my-uniapp-vue3/src/pages/payment-confirm/index.vue

@@ -136,6 +136,7 @@ const inWechat = ref(false);
 const openid = ref('');
 const wxSdkReady = ref(false);
 const paying = ref(false);
+const lastPayClick = ref(0); // 防重:记录上次点击时间
 let checkTimer: ReturnType<typeof setInterval> | null = null;
 
 // ==================== 微信 JS-SDK 初始化 ====================
@@ -335,6 +336,19 @@ async function createOrder() {
   }
 }
 
+// 查询微信订单状态(用于支付后确认)
+async function checkWechatOrderStatus(orderNo: string): Promise<string | null> {
+  try {
+    const result = await get<any>(`/payment/wechat/query/${orderNo}`);
+    const state = result?.data?.trade_state || result?.trade_state || null;
+    console.log('[WeChat Pay] 订单查询结果:', orderNo, state);
+    return state;
+  } catch (e) {
+    console.error('[WeChat Pay] 订单查询失败:', e);
+    return null;
+  }
+}
+
 // ==================== 微信 JSAPI 支付(公众号内) ====================
 async function handleWechatJsapiPay() {
   // #ifdef H5
@@ -387,39 +401,60 @@ async function handleWechatJsapiPay() {
       package: result.payParams.package,
       signType: result.payParams.signType,
       paySign: result.payParams.paySign,
-      success: () => {
-        console.log('[WeChat Pay] 支付成功');
+      success: async () => {
+        console.log('[WeChat Pay] wx.chooseWXPay 返回成功,查询订单确认...');
+        // 官方要求:必须调用查询订单API确认最终状态,避免依赖回调直接展示
+        const orderStatus = await checkWechatOrderStatus(result.orderNo);
         paying.value = false;
-        uni.showModal({
-          title: '支付成功',
-          content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
-          showCancel: false,
-          success: () => {
-            uni.switchTab({ url: '/pages/mine/index' });
-          }
-        });
+
+        if (orderStatus === 'SUCCESS') {
+          uni.showModal({
+            title: '支付成功',
+            content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
+            showCancel: false,
+            success: () => {
+              uni.switchTab({ url: '/pages/mine/index' });
+            }
+          });
+        } else {
+          uni.showToast({ title: '订单确认中,请稍后查看', icon: 'none' });
+        }
       },
       cancel: () => {
         console.log('[WeChat Pay] 用户取消支付');
         paying.value = false;
         uni.showToast({ title: '已取消支付', icon: 'none' });
       },
-      fail: (err: any) => {
-        console.error('[WeChat Pay] 支付失败:', err);
+      fail: async (err: any) => {
+        console.error('[WeChat Pay] wx.chooseWXPay fail:', err);
+        // fail 可能并不是真正的失败(用户取消也可能进这里),查询订单确认
+        const orderStatus = await checkWechatOrderStatus(result.orderNo);
         paying.value = false;
-        // 可能是签名错误等,引导用户使用扫码支付
-        uni.showModal({
-          title: '支付失败',
-          content: '微信支付调起失败,是否使用扫码支付?',
-          confirmText: '扫码支付',
-          cancelText: '取消',
-          success: (res) => {
-            if (res.confirm && result.orderNo) {
-              inWechat.value = false;
-              handlePay();
+
+        if (orderStatus === 'SUCCESS') {
+          uni.showModal({
+            title: '支付成功',
+            content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
+            showCancel: false,
+            success: () => {
+              uni.switchTab({ url: '/pages/mine/index' });
             }
-          }
-        });
+          });
+        } else {
+          // 确认支付失败,引导使用扫码支付
+          uni.showModal({
+            title: '支付失败',
+            content: '微信支付调起失败,是否使用扫码支付?',
+            confirmText: '扫码支付',
+            cancelText: '取消',
+            success: (res) => {
+              if (res.confirm && result.orderNo) {
+                inWechat.value = false;
+                handlePay();
+              }
+            }
+          });
+        }
       }
     });
   } catch (error: any) {
@@ -439,6 +474,14 @@ async function handleWechatJsapiPay() {
 }
 
 async function handlePay() {
+  // 防重处理:2秒内禁止重复点击(官方要求按钮防抖)
+  const now = Date.now();
+  if (now - lastPayClick.value < 2000) {
+    console.log('[Payment] 防重:忽略重复点击');
+    return;
+  }
+  lastPayClick.value = now;
+
   if (!planId.value && productType.value !== 'token-pack') {
     uni.showToast({ title: '参数错误', icon: 'none' });
     return;

+ 10 - 0
server/src/modules/payment/payment.service.ts

@@ -342,10 +342,15 @@ async function generateWechatPayment(
   try {
     console.log('[WeChatPay] 开始创建 NATIVE 支付订单:', { orderNo, amount, subject });
 
+    // 设置支付截止时间(2小时)
+    const timeExpire = new Date(Date.now() + 2 * 60 * 60 * 1000);
+    const timeExpireStr = timeExpire.toISOString().replace(/\.\d{3}Z$/, '+08:00');
+
     // 使用 NATIVE 支付(扫码支付)
     const result: any = await wechat.transactions_native({
       description: subject,
       out_trade_no: orderNo,
+      time_expire: timeExpireStr,
       amount: {
         total: Math.round(amount * 100), // 转换为分
         currency: 'CNY',
@@ -948,6 +953,10 @@ export async function generateWechatJsapiPayment(
   try {
     console.log('[WeChat JSAPI] 开始创建 JSAPI 支付订单:', { orderNo, amount, subject, openid });
 
+    // 设置支付截止时间(2小时后,微信默认7天但这里显式设置)
+    const timeExpire = new Date(Date.now() + 2 * 60 * 60 * 1000);
+    const timeExpireStr = timeExpire.toISOString().replace(/\.\d{3}Z$/, '+08:00');
+
     // SDK 的 transactions_jsapi 会自动:
     // 1. 添加 appid 和 mchid
     // 2. 调用 /v3/pay/transactions/jsapi
@@ -956,6 +965,7 @@ export async function generateWechatJsapiPayment(
     const result: any = await wechat.transactions_jsapi({
       description: subject,
       out_trade_no: orderNo,
+      time_expire: timeExpireStr,
       amount: {
         total: Math.round(amount * 100), // 转换为分
         currency: 'CNY',