|
|
@@ -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;
|