Просмотр исходного кода

feat(汇安融): 订单页加退款按钮 —— 只在「1 支付成功」出现

列表操作列第四个按钮 + 详情弹层页脚各一个,都挂
module_payment:har:order:refund(与后端注解、与菜单 SQL 三处逐字相同)。

v-if 收在 trade_status === "1":上游只受理从已支付发起的退款,4 退款中 /
2 退款成功 / 8 交易关闭 点了必然是 400 —— 与其给一个永远失败的按钮,不如不给。
新增 HAR_ORDER_STATUS_PAID 常量而不是在三处写死 "1"。

确认框照实说三件事,少一件这个按钮就是在误导操作员:
① 全额、金额不可修改(上游退款 biz 里根本没有金额字段,本通道不存在部分退款);
② 提交成功只代表上游已受理,到账以状态变成「退款成功」为准;
③ 原因选填,留空由服务端补默认值(上游对空原因是**整笔拒**,所以不设
   inputPattern 逼人填,只在占位符里说明)。
成功提示同理,写的是「退款已受理(退款中)…到账以状态变为「退款成功」为准」,
**不写**「退款成功」—— 那一步拿到的是 4,不是 2。

单号走文本插值,不用 dangerouslyUseHTMLString。

读数:ts:check 与改前三份日志**逐字节相同**。但「输出没变」本身不证明它读了我改
的文件(289 条历史基线里本就没有这两个文件的条目)⇒ 做了**逐文件正对照**:
har.ts 注入类型错 ⇒ 报 har.ts(364,7) TS2322;index.vue 注入模板错 ⇒ 报
index.vue(445,38) TS2339(模板确实在检查范围内)。还原后 sha256 回到原值、
输出回到逐字节相同的基线。
(第一次给 index.vue 注入的是 row.refund_reason.nope —— **没报错**,因为 el-table
 的 row 是 any;那不是「模板没被检查」,是探针选错了基座,换 detail 才量到。)
alphaH 2 недель назад
Родитель
Сommit
b7208018ee

+ 25 - 0
frontend/src/api/module_payment/har.ts

@@ -180,6 +180,14 @@ export const HAR_TRADE_TYPE_OPTIONS = [
 /** 非分期的业务类型(上游 biz)。规范 260924 订单创建参数表 biz 行 只给了这一个取值。 */
 export const HAR_BIZ_UN_STAGE = "har_ali_un_stage";
 
+/**
+ * 「1 支付成功」—— 本通道**唯一**允许发起退款的起点状态。
+ *
+ * 有名字才敢在模板里比:写死 "1" 的判据散在三处时,改枚举的人只会改到一处。
+ * 上游语义:退款只能从已支付发起,4 退款中 / 2 退款成功 / 8 交易关闭 都会被拒。
+ */
+export const HAR_ORDER_STATUS_PAID = "1";
+
 /** 上游订单状态枚举,与后端 HarOrderStatus 逐字一致 */
 export const HAR_TRADE_STATUS_TEXT: Record<string, string> = {
   "0": "等待支付",
@@ -332,6 +340,23 @@ const HarAPI = {
       method: "post",
     });
   },
+
+  /**
+   * 退款(全额,调用上游)。
+   *
+   * ★ **全额**:本通道上游没有金额字段,所以没有「退多少」这个选择,按钮上不要暗示能部分退。
+   * ★ 返回的是**受理结果**(trade_status=4 退款中),**不是已退款到账** ——
+   *   到账与否由通知/轮询把状态收敛成 2 退款成功,调用方必须按返回值如实提示。
+   * ★ reason 留空由服务端补默认值:上游对空原因**整笔直接拒**,所以这里不能省成一个
+   *   「服务端有默认值」的假设 —— 默认值兜的是调用方,界面仍应引导填一句真实原因。
+   */
+  orderRefund(id: string, reason?: string) {
+    return request<ApiResponse<HarOrderVO>>({
+      url: `${API_PATH}/order/${id}/refund`,
+      method: "post",
+      data: { reason },
+    });
+  },
 };
 
 export default HarAPI;

+ 74 - 2
frontend/src/views/module_payment/har/order/index.vue

@@ -165,7 +165,7 @@
         <el-table-column prop="refund_reason" label="退款原因" min-width="160" show-overflow-tooltip>
           <template #default="{ row }">{{ row.refund_reason || "-" }}</template>
         </el-table-column>
-        <el-table-column label="操作" width="150" fixed="right">
+        <el-table-column label="操作" width="200" fixed="right">
           <template #default="{ row }">
             <el-button
               v-hasPerm="['module_payment:har:order:detail']"
@@ -183,6 +183,20 @@
             >
               同步
             </el-button>
+            <!--
+              只在「1 支付成功」出现:上游只受理从已支付发起的退款,其余状态(含 4 退款中、
+              2 退款成功)点了必然是 400 —— 与其给一个永远失败的按钮,不如不给。
+            -->
+            <el-button
+              v-if="row.trade_status === HAR_ORDER_STATUS_PAID"
+              v-hasPerm="['module_payment:har:order:refund']"
+              size="small"
+              type="danger"
+              :loading="refundingId === row.id"
+              @click="handleRefund(row)"
+            >
+              退款
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -427,6 +441,15 @@
         >
           同步上游状态
         </el-button>
+        <el-button
+          v-if="detail?.trade_status === HAR_ORDER_STATUS_PAID"
+          v-hasPerm="['module_payment:har:order:refund']"
+          type="danger"
+          :loading="refundingId === detail?.id"
+          @click="detail && handleRefund(detail)"
+        >
+          退款
+        </el-button>
         <el-button @click="detailVisible = false">关闭</el-button>
       </template>
     </el-dialog>
@@ -435,11 +458,12 @@
 
 <script setup lang="ts">
 import { onMounted, reactive, ref } from "vue";
-import { ElMessage, type FormInstance, type FormRules } from "element-plus";
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from "element-plus";
 import dayjs from "dayjs";
 import HarAPI, {
   HAR_BIZ_UN_STAGE,
   HAR_NOTIFY_STATUS_TEXT,
+  HAR_ORDER_STATUS_PAID,
   HAR_TRADE_STATUS_TAG,
   HAR_TRADE_STATUS_TEXT,
   HAR_TRADE_TYPE_OPTIONS,
@@ -463,6 +487,7 @@ const pageNo = ref(1);
 const pageSize = ref(20);
 const loading = ref(false);
 const syncingId = ref<string | undefined>();
+const refundingId = ref<string | undefined>();
 
 const createVisible = ref(false);
 const saving = ref(false);
@@ -675,6 +700,53 @@ async function handleSync(row: HarOrderVO) {
   }
 }
 
+/**
+ * 退款(**全额**,动钱)。
+ *
+ * 确认框里三件事必须写出来,少一件这个按钮就是在误导操作员:
+ * ① 金额不可选 —— 上游退款 biz 只有单号、没有金额字段,本通道不存在部分退款;
+ * ② 提交成功 ≠ 到账 —— 拿到的是「4 退款中」,到账以收敛成「2 退款成功」为准(提示语照此写);
+ * ③ 原因选填,但留空由服务端补默认值(上游对空原因是**整笔拒**,所以这里不设 inputPattern
+ *    逼人填,只在占位符里说明)。
+ *
+ * 不做 HTML 拼接:单号走文本插值,不用 dangerouslyUseHTMLString。
+ */
+async function handleRefund(row: HarOrderVO) {
+  let reason = "";
+  try {
+    const { value } = await ElMessageBox.prompt(
+      `订单 ${row.order_no} 将按 ¥${row.price} 发起全额退款,金额不可修改;` +
+        "提交成功只代表上游已受理,到账以状态变为「退款成功」为准。",
+      "确认退款",
+      {
+        confirmButtonText: "确认全额退款",
+        cancelButtonText: "取消",
+        inputPlaceholder: "退款原因(选填),留空将使用默认原因",
+      }
+    );
+    reason = value ?? "";
+  } catch {
+    return; // 取消(ElMessageBox 以 "cancel" reject),不发请求
+  }
+
+  refundingId.value = row.id;
+  try {
+    const res = await HarAPI.orderRefund(row.id as string, reason);
+    const updated = res.data.data;
+    // 照实说「已受理」:这一步之后订单停在「4 退款中」,不是「2 退款成功」
+    ElMessage.success(
+      `退款已受理(${HAR_TRADE_STATUS_TEXT[updated.trade_status] || updated.trade_status}),` +
+        "到账以状态变为「退款成功」为准"
+    );
+    if (detailVisible.value && detail.value?.id === row.id) {
+      detail.value = updated;
+    }
+    load();
+  } finally {
+    refundingId.value = undefined;
+  }
+}
+
 function openPayUrl(url?: string) {
   if (!url) {
     return;