|
@@ -244,6 +244,14 @@
|
|
|
</el-tag>
|
|
</el-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 上游应答原话(后端存在退款行上)。2026-10-06 起必须有这一列:
|
|
|
|
|
+ 状态停在「退款中」而上游其实早已答了「余额不足」时,原因只在这里看得到。
|
|
|
|
|
+ 没有应答的行(如预插入还没拿到同步应答)显式渲染成 -,不留空白格。
|
|
|
|
|
+ -->
|
|
|
|
|
+ <el-table-column label="上游应答" min-width="180" show-overflow-tooltip>
|
|
|
|
|
+ <template #default="{ row }">{{ row.upstream_result_msg || "-" }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
<!--
|
|
<!--
|
|
|
上游退款单号可能是空的:它只在**同步应答**里才学到(预插入的那一行还没拿到)。
|
|
上游退款单号可能是空的:它只在**同步应答**里才学到(预插入的那一行还没拿到)。
|
|
|
空串在这里会看起来像「退款单号丢了」,所以显式渲染成 -。
|
|
空串在这里会看起来像「退款单号丢了」,所以显式渲染成 -。
|
|
@@ -307,13 +315,22 @@
|
|
|
而这里每笔退的是**该单的可退余额**,且金额**不由前端计算后发出** ——
|
|
而这里每笔退的是**该单的可退余额**,且金额**不由前端计算后发出** ——
|
|
|
请求体里不带 refund_fee,剩余由服务端算(`LsyRefundService` 对台账与快照取 max,
|
|
请求体里不带 refund_fee,剩余由服务端算(`LsyRefundService` 对台账与快照取 max,
|
|
|
比页面上的算式更严)。页面上那一列只是**预计**,写明「以服务端为准」。
|
|
比页面上的算式更严)。页面上那一列只是**预计**,写明「以服务端为准」。
|
|
|
|
|
+
|
|
|
|
|
+ ★ 宽度 860→1040 是给「说明」列腾地方的:那张表 6 列 min-width 合计 700,
|
|
|
|
|
+ 加说明列(200)到 900,超过 860 的内容区就会横向滚动 ——
|
|
|
|
|
+ 而说明是**唯一**讲原因的一列,恰好会被挤出可视区(用户 2026-10-06 报障时
|
|
|
|
|
+ 遮罩里转着圈、界面上一个字的原因都没有)。
|
|
|
|
|
+ ★ `:show-close` 跑批时关掉:X 关的是对话框,不是轮询 ——
|
|
|
|
|
+ 轮询会继续在后台跑、两分钟后把「批量退款结果」框弹出来,操作员会以为见鬼了。
|
|
|
|
|
+ 要中断就 F5(页面级动作,符合直觉)。
|
|
|
-->
|
|
-->
|
|
|
<el-dialog
|
|
<el-dialog
|
|
|
v-model="batchRefundVisible"
|
|
v-model="batchRefundVisible"
|
|
|
title="批量退款"
|
|
title="批量退款"
|
|
|
- width="860px"
|
|
|
|
|
|
|
+ width="1040px"
|
|
|
:close-on-click-modal="false"
|
|
:close-on-click-modal="false"
|
|
|
:close-on-press-escape="false"
|
|
:close-on-press-escape="false"
|
|
|
|
|
+ :show-close="!batchRefundRunning"
|
|
|
>
|
|
>
|
|
|
<div class="batch-refund-container" :class="{ 'has-overlay': batchRefundRunning }">
|
|
<div class="batch-refund-container" :class="{ 'has-overlay': batchRefundRunning }">
|
|
|
<el-alert type="warning" :closable="false" show-icon>
|
|
<el-alert type="warning" :closable="false" show-icon>
|
|
@@ -344,6 +361,14 @@
|
|
|
</el-tag>
|
|
</el-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
|
|
+ <!--
|
|
|
|
|
+ 「说明」列在**轮询期间**就说话:此前这列只存在于结果框,而遮罩转圈的
|
|
|
|
|
+ 那两分钟里操作员盯着的正是这张表(2026-10-06 报障:上游已答「余额不足」,
|
|
|
|
|
+ 界面上一个字都没有)。文案由 submitMessageOf / 轮询那一侧写入。
|
|
|
|
|
+ -->
|
|
|
|
|
+ <el-table-column label="说明" min-width="200" show-overflow-tooltip>
|
|
|
|
|
+ <template #default="{ row }">{{ batchResultOf(row.out_trade_no)?.message || "-" }}</template>
|
|
|
|
|
+ </el-table-column>
|
|
|
</el-table>
|
|
</el-table>
|
|
|
|
|
|
|
|
<!-- 遮罩:形态照批量转账(pointer-events 关在容器上,防止处理中改表单) -->
|
|
<!-- 遮罩:形态照批量转账(pointer-events 关在容器上,防止处理中改表单) -->
|
|
@@ -393,9 +418,19 @@
|
|
|
★ 超时那条腿也必须说话:轮询到上限仍有未到终态的笔数时,这里给的是
|
|
★ 超时那条腿也必须说话:轮询到上限仍有未到终态的笔数时,这里给的是
|
|
|
「还在退款中,稍后刷新查看」——**不**谎报成功,也不谎报失败。
|
|
「还在退款中,稍后刷新查看」——**不**谎报成功,也不谎报失败。
|
|
|
(谎报的代价:操作员按「失败」再退一次。)
|
|
(谎报的代价:操作员按「失败」再退一次。)
|
|
|
|
|
+ ★ 其中「上游已明确应答失败」的笔数要单独再说一句:对它们,
|
|
|
|
|
+ 「仍在处理中」只是半个真相 —— 上游的原话已经摆在说明列里了。
|
|
|
-->
|
|
-->
|
|
|
<el-alert v-if="batchRefundTimedOut" type="warning" :closable="false" show-icon
|
|
<el-alert v-if="batchRefundTimedOut" type="warning" :closable="false" show-icon
|
|
|
title="仍有退款未到终态,已停止等待。它们仍在处理中,请稍后刷新页面查看,不要重复发起。" />
|
|
title="仍有退款未到终态,已停止等待。它们仍在处理中,请稍后刷新页面查看,不要重复发起。" />
|
|
|
|
|
+ <el-alert
|
|
|
|
|
+ v-if="batchRefundTimedOut && batchRefundUpstreamRejectedCount > 0"
|
|
|
|
|
+ type="error"
|
|
|
|
|
+ :closable="false"
|
|
|
|
|
+ show-icon
|
|
|
|
|
+ style="margin-top: 8px"
|
|
|
|
|
+ :title="`其中 ${batchRefundUpstreamRejectedCount} 笔上游已明确应答失败(原因见下表说明列),需人工核对后再处理。`"
|
|
|
|
|
+ />
|
|
|
<el-table :data="batchRefundResults" border stripe>
|
|
<el-table :data="batchRefundResults" border stripe>
|
|
|
<el-table-column prop="out_trade_no" label="商户订单号" min-width="190" show-overflow-tooltip />
|
|
<el-table-column prop="out_trade_no" label="商户订单号" min-width="190" show-overflow-tooltip />
|
|
|
<el-table-column label="退款金额(元)" width="120" align="right">
|
|
<el-table-column label="退款金额(元)" width="120" align="right">
|
|
@@ -612,6 +647,17 @@ const batchRefundFailCount = computed(
|
|
|
const batchRefundPendingCount = computed(
|
|
const batchRefundPendingCount = computed(
|
|
|
() => batchRefundResults.value.filter((r) => r.status === "processing").length
|
|
() => batchRefundResults.value.filter((r) => r.status === "processing").length
|
|
|
);
|
|
);
|
|
|
|
|
+/**
|
|
|
|
|
+ * 停在 processing、但上游同步应答已经明说失败(PAY_FAIL)的笔数。
|
|
|
|
|
+ * **只喂结果框里那句补充说明**,不上色、不进失败计数 —— 按 R11(资金方向)
|
|
|
|
|
+ * 同步 PAY_FAIL 不落终态:上游可能误报,落 FAIL 会放开在途闸门给二次退款开门,
|
|
|
|
|
+ * 要等对账查询给权威答案。所以它们**照旧算「退款中」**,这里只是提醒人工核对。
|
|
|
|
|
+ */
|
|
|
|
|
+const batchRefundUpstreamRejectedCount = computed(
|
|
|
|
|
+ () =>
|
|
|
|
|
+ batchRefundResults.value.filter((r) => r.status === "processing" && r.upstream_rejected)
|
|
|
|
|
+ .length
|
|
|
|
|
+);
|
|
|
/**
|
|
/**
|
|
|
* 进度条 = **已到终态的笔数 / 总数**。
|
|
* 进度条 = **已到终态的笔数 / 总数**。
|
|
|
* 提交阶段它停在 0% 是**如实**的:钱一笔都还没退回顾客,只是请求发出去了。
|
|
* 提交阶段它停在 0% 是**如实**的:钱一笔都还没退回顾客,只是请求发出去了。
|
|
@@ -648,6 +694,33 @@ function batchRowTagType(outTradeNo: string) {
|
|
|
return "warning";
|
|
return "warning";
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * 上游同步应答里「已明确失败」的判据 —— 只认 PAY_FAIL 这一个值,
|
|
|
|
|
+ * 与后端 `LeshouyingResponseParser.isPayFail` 同一口径(「只有显式 PAY_FAIL 才算失败」)。
|
|
|
|
|
+ */
|
|
|
|
|
+function isUpstreamRejected(created: any) {
|
|
|
|
|
+ return created?.upstream_result_code === "PAY_FAIL";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 提交那一刻上游同步应答的**如实**转述。
|
|
|
|
|
+ *
|
|
|
|
|
+ * ★ 退款接口的响应体里就带着上游的原始回答(后端把 `upstream_result_code/
|
|
|
|
|
+ * upstream_result_msg` 存在退款行上)。2026-10-06 线上实测:余额不足时
|
|
|
|
|
+ * result_code=PAY_FAIL、msg=「业务执行失败[6666000245376951_F53933370可用余额不足]」——
|
|
|
|
|
+ * 上游已经把原因说尽了,而旧文案对每一笔都只说「已受理,等待上游结果」,
|
|
|
|
|
+ * 界面上只剩一只转圈的对话框(用户当天报障的原样)。
|
|
|
|
|
+ * ★ 为什么**不直接标成「失败」**:这不是最终裁决。按 R11,同步 PAY_FAIL 不落终态、
|
|
|
|
|
+ * 也不放闸(怕上游误报时被重复退款),要等查询/回调给权威答案;
|
|
|
|
|
+ * 所以状态照旧算「退款中」、轮询照走到上限,这里只把**上游原话**摆出来。
|
|
|
|
|
+ */
|
|
|
|
|
+function submitMessageOf(created: any) {
|
|
|
|
|
+ if (isUpstreamRejected(created)) {
|
|
|
|
|
+ return `上游应答失败:${created.upstream_result_msg || "未给出原因"}(等待对账确认,勿重复发起)`;
|
|
|
|
|
+ }
|
|
|
|
|
+ return "已受理,等待上游结果";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function openBatchRefund() {
|
|
function openBatchRefund() {
|
|
|
// 勾选里可能夹着**开单之后状态变了**的行(页面是跨页保留勾选的,那一页可能已经刷新过)。
|
|
// 勾选里可能夹着**开单之后状态变了**的行(页面是跨页保留勾选的,那一页可能已经刷新过)。
|
|
|
// 这里再滤一遍 —— `:selectable` 只管「点的时候」,管不到「点完之后数据变了」。
|
|
// 这里再滤一遍 —— `:selectable` 只管「点的时候」,管不到「点完之后数据变了」。
|
|
@@ -716,7 +789,8 @@ async function handleBatchRefund() {
|
|
|
refund_fee: created?.refund_fee ?? remainingFen(row),
|
|
refund_fee: created?.refund_fee ?? remainingFen(row),
|
|
|
out_refund_no: created?.out_refund_no || "",
|
|
out_refund_no: created?.out_refund_no || "",
|
|
|
status: "processing",
|
|
status: "processing",
|
|
|
- message: "已受理,等待上游结果",
|
|
|
|
|
|
|
+ message: submitMessageOf(created),
|
|
|
|
|
+ upstream_rejected: isUpstreamRejected(created),
|
|
|
});
|
|
});
|
|
|
} catch (e: any) {
|
|
} catch (e: any) {
|
|
|
// 业务前置校验不通过(状态不可退 / 有退款在途 / 超余额)在这里是 HTTP 200 + code 400,
|
|
// 业务前置校验不通过(状态不可退 / 有退款在途 / 超余额)在这里是 HTTP 200 + code 400,
|
|
@@ -789,13 +863,20 @@ async function pollBatchRefundOnce() {
|
|
|
}
|
|
}
|
|
|
try {
|
|
try {
|
|
|
const res: any = await getLsyRefundDetail(r.out_refund_no);
|
|
const res: any = await getLsyRefundDetail(r.out_refund_no);
|
|
|
- const st = res.data.data?.status;
|
|
|
|
|
|
|
+ const d = res.data.data;
|
|
|
|
|
+ const st = d?.status;
|
|
|
if (st === "SUCCESS") {
|
|
if (st === "SUCCESS") {
|
|
|
r.status = "success";
|
|
r.status = "success";
|
|
|
r.message = "退款成功,已到账";
|
|
r.message = "退款成功,已到账";
|
|
|
} else if (st === "FAIL") {
|
|
} else if (st === "FAIL") {
|
|
|
r.status = "fail";
|
|
r.status = "fail";
|
|
|
- r.message = "退款失败,可稍后重试";
|
|
|
|
|
|
|
+ // 终态 FAIL 是**权威答案**(只有对账查询能写它,见 LsyRefundService 的 R-1 注释),
|
|
|
|
|
+ // 上游原话比「可稍后重试」更有用。
|
|
|
|
|
+ r.message = d?.upstream_result_msg ? `退款失败:${d.upstream_result_msg}` : "退款失败,可稍后重试";
|
|
|
|
|
+ } else if (isUpstreamRejected(d)) {
|
|
|
|
|
+ // 还没到终态,但上游同步应答已明说失败 —— 如实转述(为什么不算 fail:见 submitMessageOf)。
|
|
|
|
|
+ r.upstream_rejected = true;
|
|
|
|
|
+ r.message = `上游应答失败:${d?.upstream_result_msg || "未给出原因"}(等待对账确认,勿重复发起)`;
|
|
|
} else {
|
|
} else {
|
|
|
r.message = `上游处理中(${refundStatusLabel(st)})`;
|
|
r.message = `上游处理中(${refundStatusLabel(st)})`;
|
|
|
}
|
|
}
|
|
@@ -1053,10 +1134,19 @@ async function handleRefund(row: any) {
|
|
|
try {
|
|
try {
|
|
|
const res: any = await refundLsyOrder(row.out_trade_no, fen);
|
|
const res: any = await refundLsyOrder(row.out_trade_no, fen);
|
|
|
const created = res.data.data;
|
|
const created = res.data.data;
|
|
|
- // 照实说「已受理」:这一步之后退款单停在「退款中」,钱还没回到顾客账上。
|
|
|
|
|
- ElMessage.success(
|
|
|
|
|
- `退款已受理(${refundStatusLabel(created?.status)}),到账以状态变为「退款成功」为准`
|
|
|
|
|
- );
|
|
|
|
|
|
|
+ // 照实说:同步应答里若上游已明说失败(PAY_FAIL),就不能报「已受理」——
|
|
|
|
|
+ // 用户 2026-10-06 报的就是这类退款单在「退款中」卡住时界面一声不吭。
|
|
|
|
|
+ // 但也**不能**报「失败」:按 R11,PAY_FAIL 不落终态、要等对账查询定案(见 submitMessageOf)。
|
|
|
|
|
+ if (isUpstreamRejected(created)) {
|
|
|
|
|
+ ElMessage.warning(
|
|
|
|
|
+ `上游应答失败:${created.upstream_result_msg || "未给出原因"}(退款单仍在「退款中」,等待对账确认,勿重复发起)`
|
|
|
|
|
+ );
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // 已受理:这一步之后退款单停在「退款中」,钱还没回到顾客账上。
|
|
|
|
|
+ ElMessage.success(
|
|
|
|
|
+ `退款已受理(${refundStatusLabel(created?.status)}),到账以状态变为「退款成功」为准`
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
if (detailVisible.value && detail.value?.out_trade_no === row.out_trade_no) {
|
|
if (detailVisible.value && detail.value?.out_trade_no === row.out_trade_no) {
|
|
|
// 抽屉正开着同一单:三块数据一起重拉,别让界面停在退款前的快照上
|
|
// 抽屉正开着同一单:三块数据一起重拉,别让界面停在退款前的快照上
|
|
|
await openDetail({ out_trade_no: row.out_trade_no });
|
|
await openDetail({ out_trade_no: row.out_trade_no });
|