Bladeren bron

fix(统一支付前端): 收银台二维码不拦非 http,404 停轮询

alphaH 1 week geleden
bovenliggende
commit
9011cc9657
1 gewijzigde bestanden met toevoegingen van 39 en 12 verwijderingen
  1. 39 12
      frontend/src/views/module_payment/unified/cashier/index.vue

+ 39 - 12
frontend/src/views/module_payment/unified/cashier/index.vue

@@ -131,8 +131,9 @@
       </el-descriptions>
 
       <!--
-        主扫:pay_data_type 判等忽略大小写(codeUrl / payUrl / payurl / codeImgUrl);
-        任何 URL 进二维码或 <img> 之前都先过 isSafeHttpUrl —— pay_data 是上游原样返回的数据。
+        主扫:pay_data_type 判等忽略大小写(codeUrl / payUrl / payurl / codeImgUrl)。
+        二维码内容是上游给的任意字符串(如 weixin://wxpay/bizpayurl?…),不是导航目标,
+        原样交给 QRCode;只有进 <img src> 的 codeImgUrl 才过 isSafeHttpUrl。
       -->
       <div v-if="isScan" class="pay-area">
         <template v-if="qrDataUrl">
@@ -175,7 +176,8 @@
       </div>
 
       <div class="poll-line">
-        <template v-if="pollTimedOut">已停止自动查询,请到订单页手动同步</template>
+        <span v-if="pollGone" class="poll-gone">订单不存在或已失效</span>
+        <template v-else-if="pollTimedOut">已停止自动查询,请到订单页手动同步</template>
         <template v-else-if="isPending">正在等待支付结果,每 3 秒自动查询…</template>
       </div>
 
@@ -326,6 +328,8 @@ const qrDataUrl = ref("");
 const h5Link = ref("");
 const h5LinkLoading = ref(false);
 const pollTimedOut = ref(false);
+/** R75:快照接口判定订单不存在(body code=404 / HTTP 404)—— 停止态,不再接表 */
+const pollGone = ref(false);
 
 let pollTimer: number | undefined;
 /** 本单开始轮询的时刻:5 分钟窗口从建单成功那一秒算,不因失活/重连重置 */
@@ -387,10 +391,15 @@ const statusTagType = computed<TagType>(() => {
   return (st && ORDER_STATUS_TAG[st]) || "info";
 });
 
-/** R72:未终态且该方式能被公开 H5 页受理时,才提供「复制 H5 支付链接」 */
+/** R72:未终态且该方式能被公开 H5 页受理时,才提供「复制 H5 支付链接」;已失效的单不给入口 */
 const canGenerateH5Link = computed(() => {
   const o = order.value;
-  return !!o && PENDING_STATUS.has(o.trade_status) && H5_LINK_WAY_CODES.has(o.way_code);
+  return (
+    !!o &&
+    !pollGone.value &&
+    PENDING_STATUS.has(o.trade_status) &&
+    H5_LINK_WAY_CODES.has(o.way_code)
+  );
 });
 
 const safeH5Link = computed(() => isSafeHttpUrl(h5Link.value));
@@ -448,6 +457,7 @@ function applyOrder(vo: UnifiedOrderVO) {
   h5Link.value = "";
   h5LinkLoading.value = false;
   pollTimedOut.value = false;
+  pollGone.value = false;
   pollStartedAt = Date.now();
   if (shouldPollUnified(vo.trade_status, 0)) {
     startPolling();
@@ -573,8 +583,22 @@ async function poll() {
         pollTimedOut.value = true;
       }
     }
-  } catch {
-    // R70:静默轮询失败保留上次已知状态,不弹 toast。下一个 3 秒继续问。
+  } catch (err) {
+    // R70:静默轮询失败保留上次已知状态,不弹 toast。
+    // R75:订单不存在不是抖动 —— 后端以 HTTP 200 + body code=404 应答(GlobalExceptionHandler
+    // 不设状态码),再问也不会有结果。停表并把结果区切到「订单不存在或已失效」。
+    const e = err as {
+      data?: { code?: number | string };
+      response?: { status?: number };
+    };
+    if (String(e?.data?.code) === "404" || e?.response?.status === 404) {
+      stopPolling();
+      if (order.value?.order_no === orderNo) {
+        pollGone.value = true;
+      }
+      return;
+    }
+    // 其他失败:下一个 3 秒继续问。
   } finally {
     pollInFlight = false;
   }
@@ -621,6 +645,7 @@ function reset() {
   h5Link.value = "";
   h5LinkLoading.value = false;
   pollTimedOut.value = false;
+  pollGone.value = false;
   pollStartedAt = 0;
   pendingOutTradeNo = "";
   inputHint.value = "";
@@ -654,10 +679,8 @@ watch(
       return;
     }
     if (kind === "codeurl" || kind === "payurl") {
-      if (!isSafeHttpUrl(value)) {
-        console.warn("不安全的支付二维码内容,已按文本展示");
-        return;
-      }
+      // R74:二维码内容不是 URL(WX_NATIVE 返回 weixin://wxpay/bizpayurl?…),
+      // 不能按 http(s) 白名单拦;它只会被编码进 QR,不会被浏览器导航。
       try {
         const url = await QRCode.toDataURL(value, { width: 260, margin: 2 });
         if (seq === qrSeq) {
@@ -699,7 +722,7 @@ onDeactivated(stopPolling);
 // 回来时若这一单还在等,把表接上(不重置 5 分钟窗口:它是从建单开始算的墙钟)
 onActivated(() => {
   const o = order.value;
-  if (!o) {
+  if (!o || pollGone.value) {
     return;
   }
   const elapsed = Date.now() - pollStartedAt;
@@ -820,6 +843,10 @@ onActivated(() => {
   text-align: center;
 }
 
+.poll-gone {
+  color: var(--el-color-danger);
+}
+
 .h5-link {
   padding-top: 12px;
   margin-top: 16px;