|
@@ -193,15 +193,15 @@
|
|
|
:loading="h5LinkLoading"
|
|
:loading="h5LinkLoading"
|
|
|
@click="generateH5Link"
|
|
@click="generateH5Link"
|
|
|
>
|
|
>
|
|
|
- 复制 H5 支付链接
|
|
|
|
|
|
|
+ 生成 H5 支付链接
|
|
|
</el-button>
|
|
</el-button>
|
|
|
- <template v-if="h5Link">
|
|
|
|
|
|
|
+ <template v-if="safeH5Link">
|
|
|
<div class="link-line">
|
|
<div class="link-line">
|
|
|
<span class="link-text">{{ h5Link }}</span>
|
|
<span class="link-text">{{ h5Link }}</span>
|
|
|
- <CopyButton v-if="safeH5Link" :text="h5Link" />
|
|
|
|
|
|
|
+ <CopyButton :text="h5Link" />
|
|
|
</div>
|
|
</div>
|
|
|
- <div v-if="!safeH5Link" class="warn">该链接不是 http/https 地址,只展示不复制</div>
|
|
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+ <div v-else-if="h5Link" class="warn">该链接不是 http/https 地址,已隐藏</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="footer">
|
|
<div class="footer">
|
|
@@ -237,6 +237,8 @@ import UnifiedAPI, {
|
|
|
type UnifiedOrderCreateParams,
|
|
type UnifiedOrderCreateParams,
|
|
|
type UnifiedOrderVO,
|
|
type UnifiedOrderVO,
|
|
|
} from "@/api/module_payment/unified";
|
|
} from "@/api/module_payment/unified";
|
|
|
|
|
+import { useUserStore } from "@/store";
|
|
|
|
|
+import { ROLE_ROOT } from "@/constants";
|
|
|
import {
|
|
import {
|
|
|
UNIFIED_ORDER_STATUS_TEXT,
|
|
UNIFIED_ORDER_STATUS_TEXT,
|
|
|
UNIFIED_WAY_CODE_OPTIONS,
|
|
UNIFIED_WAY_CODE_OPTIONS,
|
|
@@ -328,7 +330,7 @@ const qrDataUrl = ref("");
|
|
|
const h5Link = ref("");
|
|
const h5Link = ref("");
|
|
|
const h5LinkLoading = ref(false);
|
|
const h5LinkLoading = ref(false);
|
|
|
const pollTimedOut = ref(false);
|
|
const pollTimedOut = ref(false);
|
|
|
-/** R75:快照接口判定订单不存在(body code=404 / HTTP 404)—— 停止态,不再接表 */
|
|
|
|
|
|
|
+/** R75:快照接口判定订单不存在(body code=404)—— 停止态,不再接表 */
|
|
|
const pollGone = ref(false);
|
|
const pollGone = ref(false);
|
|
|
|
|
|
|
|
let pollTimer: number | undefined;
|
|
let pollTimer: number | undefined;
|
|
@@ -340,6 +342,8 @@ let pendingOutTradeNo = "";
|
|
|
/** 二维码异步生成的代次:迟到的 A 单结果不能渲染到 B 单上 */
|
|
/** 二维码异步生成的代次:迟到的 A 单结果不能渲染到 B 单上 */
|
|
|
let qrSeq = 0;
|
|
let qrSeq = 0;
|
|
|
|
|
|
|
|
|
|
+const userStore = useUserStore();
|
|
|
|
|
+
|
|
|
const wayCodeOptions = computed(() =>
|
|
const wayCodeOptions = computed(() =>
|
|
|
UNIFIED_WAY_CODE_OPTIONS.filter((o) => WAY_CODES_BY_MODE[mode.value].includes(o.value))
|
|
UNIFIED_WAY_CODE_OPTIONS.filter((o) => WAY_CODES_BY_MODE[mode.value].includes(o.value))
|
|
|
);
|
|
);
|
|
@@ -355,6 +359,19 @@ const submitDisabled = computed(
|
|
|
(merchants.value.length > 1 && !form.mch_no)
|
|
(merchants.value.length > 1 && !form.mch_no)
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 与 `v-hasPerm` 同口径(`src/directives/permission/index.ts:20-40`):超管角色直接放行,
|
|
|
|
|
+ * 其余看权限串。按钮会被指令摘掉,但输入框里按回车仍会调用 `submit()` ——
|
|
|
|
|
+ * 这里必须再拦一次,否则无权限的人按回车只会收到一条 403 toast。
|
|
|
|
|
+ */
|
|
|
|
|
+const canCreate = computed(() => {
|
|
|
|
|
+ const roles = userStore.basicInfo.roles;
|
|
|
|
|
+ if (roles && roles.map((r) => r.code).includes(ROLE_ROOT)) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ return userStore.prems.includes("module_payment:unified:cashier:create");
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
const payKind = computed(() => (order.value?.pay_data_type || "").toLowerCase());
|
|
const payKind = computed(() => (order.value?.pay_data_type || "").toLowerCase());
|
|
|
const payValue = computed(() => order.value?.pay_data || "");
|
|
const payValue = computed(() => order.value?.pay_data || "");
|
|
|
|
|
|
|
@@ -391,7 +408,7 @@ const statusTagType = computed<TagType>(() => {
|
|
|
return (st && ORDER_STATUS_TAG[st]) || "info";
|
|
return (st && ORDER_STATUS_TAG[st]) || "info";
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
-/** R72:未终态且该方式能被公开 H5 页受理时,才提供「复制 H5 支付链接」;已失效的单不给入口 */
|
|
|
|
|
|
|
+/** R72:未终态且该方式能被公开 H5 页受理时,才提供「生成 H5 支付链接」;已失效的单不给入口 */
|
|
|
const canGenerateH5Link = computed(() => {
|
|
const canGenerateH5Link = computed(() => {
|
|
|
const o = order.value;
|
|
const o = order.value;
|
|
|
return (
|
|
return (
|
|
@@ -465,6 +482,9 @@ function applyOrder(vo: UnifiedOrderVO) {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function submit() {
|
|
async function submit() {
|
|
|
|
|
+ if (!canCreate.value) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
if (creating.value) {
|
|
if (creating.value) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -540,8 +560,15 @@ async function submit() {
|
|
|
}
|
|
}
|
|
|
pendingOutTradeNo = "";
|
|
pendingOutTradeNo = "";
|
|
|
applyOrder(vo);
|
|
applyOrder(vo);
|
|
|
- } catch {
|
|
|
|
|
|
|
+ } catch (err) {
|
|
|
// 拦截器已用后端原话弹过 toast(例如「商户已停用」)。输入保留,直接重试。
|
|
// 拦截器已用后端原话弹过 toast(例如「商户已停用」)。输入保留,直接重试。
|
|
|
|
|
+ // R77:409 = 这个 out_trade_no 已存在且本次参数不一致(后端 UnifiedOrderService 判定),
|
|
|
|
|
+ // 继续复用旧号永远建不出来 —— 丢掉幂等键,下一次点击换新号。
|
|
|
|
|
+ const e = err as { data?: { code?: number | string } };
|
|
|
|
|
+ if (String(e?.data?.code) === "409") {
|
|
|
|
|
+ pendingOutTradeNo = "";
|
|
|
|
|
+ inputHint.value = "订单参数已变化,已更换新的单号,请再试一次";
|
|
|
|
|
+ }
|
|
|
} finally {
|
|
} finally {
|
|
|
creating.value = false;
|
|
creating.value = false;
|
|
|
}
|
|
}
|
|
@@ -587,11 +614,10 @@ async function poll() {
|
|
|
// R70:静默轮询失败保留上次已知状态,不弹 toast。
|
|
// R70:静默轮询失败保留上次已知状态,不弹 toast。
|
|
|
// R75:订单不存在不是抖动 —— 后端以 HTTP 200 + body code=404 应答(GlobalExceptionHandler
|
|
// 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) {
|
|
|
|
|
|
|
+ // R76:HTTP 层的 404 到不了这里 —— 拦截器把它压成裸 `new Error("请求处理失败")`
|
|
|
|
|
+ // (`utils/request.ts:169-173`),页面只能看到 body code,故只判它。
|
|
|
|
|
+ const e = err as { data?: { code?: number | string } };
|
|
|
|
|
+ if (String(e?.data?.code) === "404") {
|
|
|
// 迟到的 404 不能停掉新单的表:先确认屏幕上还是同一单,再停表/置失效。
|
|
// 迟到的 404 不能停掉新单的表:先确认屏幕上还是同一单,再停表/置失效。
|
|
|
if (order.value?.order_no === orderNo) {
|
|
if (order.value?.order_no === orderNo) {
|
|
|
stopPolling();
|
|
stopPolling();
|
|
@@ -665,8 +691,10 @@ watch(mode, (m) => {
|
|
|
|
|
|
|
|
// 主扫:codeUrl / payUrl 生成二维码,codeImgUrl 交给 <img>(computed)。
|
|
// 主扫:codeUrl / payUrl 生成二维码,codeImgUrl 交给 <img>(computed)。
|
|
|
// seq 用来丢弃迟到的异步结果 —— A 单的码不能渲染到 B 单上。
|
|
// seq 用来丢弃迟到的异步结果 —— A 单的码不能渲染到 B 单上。
|
|
|
|
|
+// 多个标量源(而不是一个返回新数组的 getter):轮询每次替换 order 对象时,
|
|
|
|
|
+// 只要这三个值没变就不重复编码/重复 warn。
|
|
|
watch(
|
|
watch(
|
|
|
- () => [order.value?.order_no, order.value?.pay_data, order.value?.pay_data_type] as const,
|
|
|
|
|
|
|
+ [() => order.value?.order_no, () => order.value?.pay_data, () => order.value?.pay_data_type],
|
|
|
async () => {
|
|
async () => {
|
|
|
const seq = ++qrSeq;
|
|
const seq = ++qrSeq;
|
|
|
qrDataUrl.value = "";
|
|
qrDataUrl.value = "";
|
|
@@ -698,8 +726,9 @@ watch(
|
|
|
);
|
|
);
|
|
|
|
|
|
|
|
// H5:非 http/https 的 pay_data 只按文本展示,并在控制台留一条 WARN
|
|
// H5:非 http/https 的 pay_data 只按文本展示,并在控制台留一条 WARN
|
|
|
|
|
+// 同样用多个标量源,避免每 3 秒轮询都重打一次 warn。
|
|
|
watch(
|
|
watch(
|
|
|
- () => [order.value?.order_no, order.value?.way_code, order.value?.pay_data] as const,
|
|
|
|
|
|
|
+ [() => order.value?.order_no, () => order.value?.way_code, () => order.value?.pay_data],
|
|
|
() => {
|
|
() => {
|
|
|
const o = order.value;
|
|
const o = order.value;
|
|
|
if (!o || o.way_code !== "WX_H5") {
|
|
if (!o || o.way_code !== "WX_H5") {
|