Ver código fonte

fix: 批量付款三个 tab 联动问题

- 主体下拉切 tab 重载: BatchPayCreate 暴露 active prop + watch,
  切到批量制单 tab 时重新请求 authorize/list, 授权完成后立即可选
- 详情刷新联动列表: BatchPayDetail 刷新/关闭后 emit refresh,
  index.vue batchListKey++ 重挂载列表
- 切 tab 关闭详情: watch activeTab 清空 detailBatchNo, 详情不再残留
- AuthorizeList 表单新增主体类型下拉(ALIPAY_USER_ID/LOGON_ID/OPEN_ID),
  列表主体账号列显示类型小字, 主体名称提示须与实名一致
alphaH 4 dias atrás
pai
commit
a98227bcca

+ 10 - 3
frontend/src/api/module_payment/batch.ts

@@ -4,7 +4,8 @@ const API_PATH = "/payment/batch";
 
 /** 制单授权记录(后端 BatchAuthorizeEntity,全局 SNAKE_CASE 序列化) */
 export interface BatchAuthorizeVO {
-  id?: number;
+  /** 雪花ID序列化为字符串(后端 @JsonSerialize(ToStringSerializer)),防止 JS 精度丢失 */
+  id?: string;
   out_biz_no: string;
   status: string;
   agreement_no?: string;
@@ -80,7 +81,13 @@ export interface BatchCreateParams {
 
 export const BatchPayAPI = {
   /** 生成制单授权链接(账号级: 主体信息来自表单,服务商下拉选择) */
-  authorizeApply(data: { participant_name: string; participant_id: string; service_provider_id: number }) {
+  authorizeApply(data: {
+    participant_name: string;
+    participant_id: string;
+    /** 主体类型: ALIPAY_USER_ID 支付宝账号ID(默认) / ALIPAY_LOGON_ID 登录号 / ALIPAY_OPEN_ID OpenID */
+    participant_id_type?: string;
+    service_provider_id: number;
+  }) {
     return request<ApiResponse<{ authorize_link: string; out_biz_no: string; status: string }>>({
       url: `${API_PATH}/authorize/apply`,
       method: "post",
@@ -89,7 +96,7 @@ export const BatchPayAPI = {
   },
 
   /** 制单授权重新生成:作废当前 AUTHING 申请(旧链接失效),换新 out_biz_no 重新申请 */
-  authorizeRebind(id: number) {
+  authorizeRebind(id: string) {
     return request<ApiResponse<{ authorize_link: string; out_biz_no: string; status: string }>>({
       url: `${API_PATH}/authorize/rebind`,
       method: "post",

+ 42 - 6
frontend/src/views/module_payment/batch/components/AuthorizeList.vue

@@ -21,7 +21,14 @@
       <el-table-column prop="participant_name" label="主体名称" min-width="140">
         <template #default="{ row }">{{ row.participant_name || "-" }}</template>
       </el-table-column>
-      <el-table-column prop="participant_id" label="支付宝UID" min-width="160" />
+      <el-table-column label="主体账号" min-width="170">
+        <template #default="{ row }">
+          <div>{{ row.participant_id || "-" }}</div>
+          <div v-if="row.participant_id_type" class="sub-text">
+            {{ participantTypeText(row.participant_id_type) }}
+          </div>
+        </template>
+      </el-table-column>
       <el-table-column label="服务商" min-width="140">
         <template #default="{ row }">
           {{ providerName(row.service_provider_id) }}
@@ -88,14 +95,25 @@
         <el-form-item label="主体名称" prop="participant_name">
           <el-input
             v-model="form.participant_name"
-            placeholder="如:张三(个人)/ 某某公司"
+            placeholder="姓名/企业名称(须与账号实名一致)"
             style="max-width: 360px"
           />
+          <div class="form-item-tip">须与账号实名一致(个人填真实姓名,企业填注册名称)</div>
+        </el-form-item>
+        <el-form-item label="主体类型" prop="participant_id_type">
+          <el-select v-model="form.participant_id_type" style="max-width: 360px">
+            <el-option
+              v-for="o in PARTICIPANT_TYPE_OPTIONS"
+              :key="o.value"
+              :label="o.label"
+              :value="o.value"
+            />
+          </el-select>
         </el-form-item>
         <el-form-item label="支付宝账号" prop="participant_id">
           <el-input
             v-model="form.participant_id"
-            placeholder="支付宝用户ID(2088开头)"
+            :placeholder="PARTICIPANT_ID_PLACEHOLDER[form.participant_id_type]"
             style="max-width: 360px"
           />
         </el-form-item>
@@ -108,9 +126,6 @@
           >
             <el-option v-for="p in providerOptions" :key="p.id" :label="p.name" :value="p.id" />
           </el-select>
-          <div class="form-item-tip">
-            无法自动获取服务商时手动选择(与新增企业页一致)
-          </div>
         </el-form-item>
       </el-form>
       <template #footer>
@@ -208,9 +223,23 @@ onMounted(async () => {
 
 const applyVisible = ref(false);
 const formRef = ref<FormInstance>();
+/** 授权主体类型(AuthParticipantInfo.participantIdType 文档枚举) */
+const PARTICIPANT_TYPE_OPTIONS = [
+  { value: "ALIPAY_USER_ID", label: "支付宝账号ID(2088开头)" },
+  { value: "ALIPAY_LOGON_ID", label: "支付宝账号(手机号或邮箱)" },
+  { value: "ALIPAY_OPEN_ID", label: "应用OpenID" },
+];
+const PARTICIPANT_ID_PLACEHOLDER: Record<string, string> = {
+  ALIPAY_USER_ID: "支付宝账号ID(2088开头)",
+  ALIPAY_LOGON_ID: "手机号或邮箱",
+  ALIPAY_OPEN_ID: "应用OpenID",
+};
+const participantTypeText = (type?: string) =>
+  PARTICIPANT_TYPE_OPTIONS.find((o) => o.value === type)?.label || type || "-";
 const form = reactive({
   participant_name: "",
   participant_id: "",
+  participant_id_type: "ALIPAY_USER_ID",
   service_provider_id: null as number | null,
 });
 const rules: FormRules = {
@@ -232,6 +261,7 @@ async function handleApply() {
     const res = await BatchPayAPI.authorizeApply({
       participant_name: form.participant_name.trim(),
       participant_id: form.participant_id.trim(),
+      participant_id_type: form.participant_id_type,
       service_provider_id: form.service_provider_id!,
     });
     applyVisible.value = false;
@@ -363,6 +393,12 @@ async function handleRefresh(row: BatchAuthorizeVO) {
   color: #909399;
 }
 
+.sub-text {
+  font-size: 12px;
+  color: #909399;
+  line-height: 1.4;
+}
+
 .qrcode-wrapper {
   display: flex;
   flex-direction: column;

+ 16 - 2
frontend/src/views/module_payment/batch/components/BatchPayCreate.vue

@@ -250,8 +250,8 @@ const rules: FormRules = {
 /** 已签约(AUTHED)主体 — 制单付款方候选 */
 const authedSubjects = ref<BatchAuthorizeVO[]>([]);
 
-onMounted(async () => {
-  addDetailRow();
+/** 重新拉取已授权主体(每次切换到本 tab 时调用,保证授权完成后立即可选) */
+async function reloadSubjects() {
   // 主体量小,page_size=100 一次拉全(后端 list 按 id 倒序返回)
   try {
     const res = await BatchPayAPI.authorizeList({ page_no: 1, page_size: 100 });
@@ -260,8 +260,22 @@ onMounted(async () => {
   } catch {
     // 主体加载失败不阻塞制单(后端制单预检仍会拦截未授权主体)
   }
+}
+
+onMounted(async () => {
+  addDetailRow();
+  reloadSubjects();
 });
 
+const props = defineProps<{ active?: boolean }>();
+// el-tabs 非 lazy: 组件随页面挂载,切 tab 不重建 → 切到本 tab 时重拉主体
+watch(
+  () => props.active,
+  (v) => {
+    if (v) reloadSubjects();
+  }
+);
+
 /** 26 年新接入商户可选转账场景(与后端 BatchCreateDTO 注释一致) */
 const SCENE_OPTIONS = [
   "现金营销",

+ 4 - 1
frontend/src/views/module_payment/batch/components/BatchPayDetail.vue

@@ -96,7 +96,7 @@ import { ElMessage, ElMessageBox } from "element-plus";
 import dayjs from "dayjs";
 
 const props = defineProps<{ outBatchNo: string }>();
-const emit = defineEmits<{ close: [] }>();
+const emit = defineEmits<{ close: []; refresh: [] }>();
 
 const order = ref<BatchOrderVO | null>(null);
 const details = ref<BatchDetailItem[]>([]);
@@ -161,6 +161,8 @@ async function refresh() {
     refreshing.value = false;
   }
   await load();
+  // 批次/明细状态可能已变化 → 通知列表页重载
+  emit("refresh");
 }
 
 async function handlePay() {
@@ -194,6 +196,7 @@ async function handleClose() {
   await BatchPayAPI.batchClose(order.value.out_batch_no);
   ElMessage.success("批次已关闭");
   load();
+  emit("refresh");
 }
 
 onMounted(load);

+ 10 - 2
frontend/src/views/module_payment/batch/index.vue

@@ -7,7 +7,10 @@
         <AuthorizeList />
       </el-tab-pane>
       <el-tab-pane label="批量制单" name="create">
-        <BatchPayCreate @created="handleBatchPayCreated" />
+        <BatchPayCreate
+          :active="activeTab === 'create'"
+          @created="handleBatchPayCreated"
+        />
       </el-tab-pane>
       <el-tab-pane label="制单历史" name="list">
         <BatchPayList :key="batchListKey" @view="handleViewBatchPay" />
@@ -18,12 +21,13 @@
       v-if="detailBatchNo"
       :out-batch-no="detailBatchNo"
       @close="detailBatchNo = ''"
+      @refresh="batchListKey++"
     />
   </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from "vue";
+import { ref, watch } from "vue";
 import AuthorizeList from "./components/AuthorizeList.vue";
 import BatchPayCreate from "./components/BatchPayCreate.vue";
 import BatchPayList from "./components/BatchPayList.vue";
@@ -39,4 +43,8 @@ function handleBatchPayCreated() {
   activeTab.value = "list";
   batchListKey.value++;
 }
+// 切换 tab 时关闭批次详情(详情挂在 tabs 外部,避免残留展示)
+watch(activeTab, () => {
+  detailBatchNo.value = "";
+});
 </script>