Переглянути джерело

feat: 场景报备信息改级联下拉 - 严格按支付宝文档0iaxid

文档实证(opendocs.alipay.com/open/0iaxid,2026-04-14 更新):
- info_type 是中文取值而非英文枚举,之前的 TRANSFER_SCENE_REPORT 是错的
- 每个转账场景有固定信息类型集,多类型场景需完整传入(现金营销2条、保险理赔3条)

改造:
- 选转账场景后自动生成该场景全部信息类型行(类型下拉级联自场景)
- 内容下拉级联自类型,选项为文档示例值(会员红包/新会员入会红包等),
  filterable allow-create 可自定义,placeholder 显示文档描述原文
- 类型不可重复(添加按钮在类型占满时禁用),删行后引导文案提示缺失类型
- 提交兜底: 该场景全部类型必须齐全 + 每行内容完整
alphaH 4 днів тому
батько
коміт
57d6dc363d

+ 190 - 21
frontend/src/views/module_payment/account/components/BatchPayCreate.vue

@@ -23,7 +23,7 @@
           <el-option v-for="s in SCENE_OPTIONS" :key="s" :label="s" :value="s" />
         </el-select>
       </el-form-item>
-      <!-- 无 prop: 报备信息是动态行数组不入 form,prop 会触发内置「{field} is required」假校验;必填由 handleSubmit 的 hasValidReport 兜底 -->
+      <!-- 无 prop: 报备信息是动态行数组不入 form,prop 会触发内置「{field} is required」假校验;必填由 handleSubmit 兜底 -->
       <el-form-item label="场景报备信息" required>
         <div
           v-for="(r, idx) in reportInfos"
@@ -32,31 +32,46 @@
         >
           <el-select
             v-model="r.info_type"
-            placeholder="报备信息类型"
+            :placeholder="form.transfer_scene_name ? '选择信息类型' : '请先选择转账场景'"
             style="width: 220px"
+            :disabled="!form.transfer_scene_name"
+          >
+            <el-option
+              v-for="t in unusedReportTypes(form.transfer_scene_name, r.info_type)"
+              :key="t"
+              :label="t"
+              :value="t"
+            />
+          </el-select>
+          <el-select
+            v-model="r.info_content"
+            :placeholder="reportTip(r.info_type) || '请选择或输入报备内容'"
+            style="width: 320px"
             filterable
             allow-create
+            default-first-option
+            :disabled="!r.info_type"
+            :maxlength="256"
           >
             <el-option
-              v-for="o in REPORT_INFO_TYPE_OPTIONS"
-              :key="o.value"
-              :label="o.label"
-              :value="o.value"
+              v-for="c in reportContentOptions(r.info_type)"
+              :key="c"
+              :label="c"
+              :value="c"
             />
           </el-select>
-          <el-input v-model="r.info_content" placeholder="报备内容说明" style="width: 300px" />
-          <el-button type="danger" plain @click="reportInfos.splice(idx, 1)">删除</el-button>
+          <el-button type="danger" plain @click="removeReportRow(idx)">删除</el-button>
         </div>
         <el-button
           type="primary"
           plain
-          @click="reportInfos.push({ info_type: '', info_content: '' })"
+          :disabled="!form.transfer_scene_name || unusedReportTypes(form.transfer_scene_name, '').length === 0"
+          @click="addReportRow"
         >
           添加报备信息
         </el-button>
         <div class="form-item-tip">
-          2026 年新接入商户必填。报备信息类型选「转账场景报备」,内容填写本次转账场景的说明,
-          如:8 月家政服务人员佣金报酬发放,涉及 126 人,共 5.2 万元
+          {{ reportTipText }}
         </div>
       </el-form-item>
       <el-form-item label="超时时间">
@@ -183,7 +198,7 @@
 </template>
 
 <script setup lang="ts">
-import { computed, onMounted, reactive, ref } from "vue";
+import { computed, onMounted, reactive, ref, watch } from "vue";
 import BatchPayAPI from "@/api/module_payment/batch";
 import { useEnterpriseStore } from "@/store";
 import { ElMessage } from "element-plus";
@@ -234,10 +249,115 @@ const IDENTITY_TYPE_OPTIONS = [
   { value: "ALIPAY_OPEN_ID", label: "支付宝账户(OpenID)" },
 ];
 
-/** 场景报备信息类型(支付宝 transfer_scene_report_infos.info_type 枚举) */
-const REPORT_INFO_TYPE_OPTIONS = [
-  { value: "TRANSFER_SCENE_REPORT", label: "转账场景报备" },
-];
+/**
+ * 转账场景报备信息(支付宝官方文档「转账场景信息字段说明」0iaxid,2026-04-14 更新)
+ * info_type 为中文取值,按转账场景级联;info_content 自定义描述,选项为文档示例模板
+ */
+interface SceneReportType {
+  /** info_type 取值(严格按文档示例,不可重复) */
+  type: string;
+  /** 文档描述列原文,作内容下拉的引导提示 */
+  tip: string;
+  /** 文档示例值,作内容下拉的快捷选项(filterable allow-create 可自定义) */
+  contentOptions: string[];
+}
+
+const REPORT_SCENE_MAP: Record<string, SceneReportType[]> = {
+  现金营销: [
+    { type: "活动名称", tip: "请在信息内容描述收款方参与活动的名称", contentOptions: ["会员红包"] },
+    {
+      type: "奖励说明",
+      tip: "请在信息内容描述收款方因为什么奖励获取这笔资金",
+      contentOptions: ["新会员入会红包"],
+    },
+  ],
+  企业退款: [
+    {
+      type: "退款原因",
+      tip: "请在信息内容描述收款方接收当前这笔退款的原因,如商品质量问题退款",
+      contentOptions: ["商品质量问题退款"],
+    },
+  ],
+  佣金报酬: [
+    {
+      type: "佣金报酬说明",
+      tip: "请在信息内容描述收款方接收当前这笔款项的原因,如 8 月家政服务报酬",
+      contentOptions: ["8月家政服务报酬"],
+    },
+  ],
+  业务结算: [
+    {
+      type: "结算款项名称",
+      tip: "请在信息内容描述收款方接收的款项名称,如材料货款",
+      contentOptions: ["材料货款"],
+    },
+  ],
+  二手回收: [
+    {
+      type: "回收商品名称",
+      tip: "请在信息内容描述收款方回收的商品名称,如衣服",
+      contentOptions: ["衣服"],
+    },
+  ],
+  公益补助: [
+    {
+      type: "公益活动名称",
+      tip: "请在信息内容描述公益活动在民政部的备案名称",
+      contentOptions: [],
+    },
+  ],
+  行政补贴和退款: [
+    {
+      type: "补贴/退款类型",
+      tip: "请在信息内容描述补贴/退款类型,如某地人才补贴",
+      contentOptions: ["某地人才补贴"],
+    },
+  ],
+  保险理赔: [
+    {
+      type: "业务类型",
+      tip: "请在信息内容描述这笔转账的业务类型,例如理赔、退保、其他",
+      contentOptions: ["理赔", "退保", "其他"],
+    },
+    {
+      type: "保险险种",
+      tip: "请在信息内容描述保险险种及保险产品名称,例如医疗险-某百万医疗保险、意外险-某交通意外险等",
+      contentOptions: [],
+    },
+    {
+      type: "业务交易订单号",
+      tip: "请在信息内容描述这笔转账的业务内部交易订单号",
+      contentOptions: [],
+    },
+  ],
+};
+
+/** 某信息类型的文档引导文案(内容下拉 placeholder 用) */
+function reportTip(infoType: string): string {
+  for (const types of Object.values(REPORT_SCENE_MAP)) {
+    const t = types.find((x) => x.type === infoType);
+    if (t) return t.tip;
+  }
+  return "";
+}
+
+/** 某信息类型的文档示例选项(内容下拉用) */
+function reportContentOptions(infoType: string): string[] {
+  for (const types of Object.values(REPORT_SCENE_MAP)) {
+    const t = types.find((x) => x.type === infoType);
+    if (t) return t.contentOptions;
+  }
+  return [];
+}
+
+/** 该场景未被当前行占用的信息类型(文档要求类型不可重复) */
+function unusedReportTypes(scene: string, excludeType: string): string[] {
+  const all = (REPORT_SCENE_MAP[scene] ?? []).map((t) => t.type);
+  const used = reportInfos.value
+    .map((r) => r.info_type)
+    .filter((t) => t !== "" && t !== excludeType);
+  return all.filter((t) => !used.includes(t));
+}
 
 const IDENTITY_TYPE_CODES = IDENTITY_TYPE_OPTIONS.map((o) => o.value);
 
@@ -254,6 +374,45 @@ const detailRows = ref<DetailRow[]>([]);
 const reportInfos = ref<Array<{ info_type: string; info_content: string }>>([]);
 const importFileList = ref<any[]>([]);
 
+/** 选择转账场景后自动生成该场景全部信息类型行(文档:多类型场景需完整传入) */
+watch(
+  () => form.transfer_scene_name,
+  (scene) => {
+    if (scene) {
+      reportInfos.value = (REPORT_SCENE_MAP[scene] ?? []).map((t) => ({
+        info_type: t.type,
+        info_content: "",
+      }));
+    } else {
+      reportInfos.value = [];
+    }
+  }
+);
+
+/** 添加一行未占用的信息类型(文档要求类型不可重复) */
+function addReportRow() {
+  if (!form.transfer_scene_name) return;
+  const unused = unusedReportTypes(form.transfer_scene_name, "");
+  if (unused.length === 0) return;
+  reportInfos.value.push({ info_type: unused[0], info_content: "" });
+}
+
+function removeReportRow(idx: number) {
+  reportInfos.value.splice(idx, 1);
+}
+
+/** 报备区引导文案:场景级联 + 类型级联 + 缺类型提醒 */
+const reportTipText = computed(() => {
+  if (!form.transfer_scene_name) {
+    return "2026 年新接入商户必填。请先在上方选择转账场景,系统将按支付宝文档自动生成该场景需要报备的信息类型。";
+  }
+  const types = REPORT_SCENE_MAP[form.transfer_scene_name] ?? [];
+  const missing = types.filter((t) => !reportInfos.value.some((r) => r.info_type === t.type));
+  const base = `该场景需报备 ${types.length} 条信息(支付宝文档),已自动生成。`;
+  if (missing.length) return `${base} 还缺少:${missing.map((t) => t.type).join("、")}`;
+  return `${base} 请为每条信息填写报备内容说明。`;
+});
+
 let detailSeq = 0;
 function genOutBizNo() {
   detailSeq += 1;
@@ -408,12 +567,22 @@ async function handleSubmit() {
   }
   const valid = await formRef.value?.validate().catch(() => false);
   if (!valid) return;
-  // I3: 场景报备信息必填(后端 DTO 同步 @NotEmpty 校验),至少一条 info_type/info_content 非空
-  const hasValidReport = reportInfos.value.some(
-    (r) => r.info_type.trim() !== "" && r.info_content.trim() !== ""
+  // 场景报备信息必填(后端 DTO @NotEmpty 同步校验):每行完整 + 该场景全部信息类型齐全
+  const sceneTypes = REPORT_SCENE_MAP[form.transfer_scene_name] ?? [];
+  const missingTypes = sceneTypes.filter(
+    (t) => !reportInfos.value.some((r) => r.info_type === t.type)
+  );
+  if (missingTypes.length) {
+    ElMessage.warning(
+      `该转账场景需完整报备:${missingTypes.map((t) => t.type).join("、")}(支付宝文档要求,不可省略)`
+    );
+    return;
+  }
+  const incompleteRow = reportInfos.value.find(
+    (r) => !r.info_type || !r.info_content.trim()
   );
-  if (!hasValidReport) {
-    ElMessage.warning("请填写至少一条完整的场景报备信息(类型与内容均必填)");
+  if (incompleteRow) {
+    ElMessage.warning("请为每条场景报备信息填写报备内容说明");
     return;
   }
   if (detailRows.value.length === 0) {