Ver Fonte

fix: 协议号自动带出不接受客户端指定 + 报备信息引导文字

协议号=支付宝制单授权协议号(agreement_no),授权完成后由支付宝生成,
企业无需手填。前端移除输入框,后端 BatchCreateDTO 移除 agreementNo 字段,
batchCreate 制单预检改 selectOne 取最新 AUTHED/NORMAL 授权并自动带出协议号
(ext_info.agreement_no + 落库)。报备信息区加类型下拉与填写引导示例。
alphaH há 5 dias atrás
pai
commit
1c1b4dbf0d

+ 1 - 2
frontend/src/api/module_payment/batch.ts

@@ -62,11 +62,10 @@ export interface BatchDetailItem {
   error_msg?: string;
 }
 
-/** 创建批次请求体(BatchCreateDTO,snake_case;付款方恒为企业自身 UID,后端以 enterprise_id 为准不接受指定) */
+/** 创建批次请求体(BatchCreateDTO,snake_case;付款方恒为企业自身 UID、协议号自动带出,均不接受客户端指定) */
 export interface BatchCreateParams {
   enterprise_id?: string;
   order_title: string;
-  agreement_no?: string;
   transfer_scene_name?: string;
   transfer_scene_report_infos?: Array<{ info_type: string; info_content: string }>;
   time_expire?: string;

+ 24 - 12
frontend/src/views/module_payment/account/components/BatchPayCreate.vue

@@ -13,13 +13,6 @@
           style="max-width: 420px"
         />
       </el-form-item>
-      <el-form-item label="协议号">
-        <el-input
-          v-model="form.agreement_no"
-          placeholder="选填,指定制单授权协议号"
-          style="max-width: 420px"
-        />
-      </el-form-item>
       <el-form-item label="转账场景" prop="transfer_scene_name">
         <el-select
           v-model="form.transfer_scene_name"
@@ -36,8 +29,21 @@
           :key="idx"
           style="display: flex; gap: 8px; margin-bottom: 8px"
         >
-          <el-input v-model="r.info_type" placeholder="报备信息类型" style="width: 200px" />
-          <el-input v-model="r.info_content" placeholder="报备信息内容" style="width: 320px" />
+          <el-select
+            v-model="r.info_type"
+            placeholder="报备信息类型"
+            style="width: 220px"
+            filterable
+            allow-create
+          >
+            <el-option
+              v-for="o in REPORT_INFO_TYPE_OPTIONS"
+              :key="o.value"
+              :label="o.label"
+              :value="o.value"
+            />
+          </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>
         </div>
         <el-button
@@ -47,7 +53,10 @@
         >
           添加报备信息
         </el-button>
-        <div class="form-item-tip">2026 年新接入商户必填,按支付宝要求填写</div>
+        <div class="form-item-tip">
+          2026 年新接入商户必填。报备信息类型选「转账场景报备」,内容填写本次转账场景的说明,
+          如:8 月家政服务人员佣金报酬发放,涉及 126 人,共 5.2 万元
+        </div>
       </el-form-item>
       <el-form-item label="超时时间">
         <el-date-picker
@@ -195,7 +204,6 @@ const createdResult = ref<{
 
 const form = reactive({
   order_title: "",
-  agreement_no: "",
   transfer_scene_name: "",
   time_expire: "",
   remark: "",
@@ -225,6 +233,11 @@ 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: "转账场景报备" },
+];
+
 const IDENTITY_TYPE_CODES = IDENTITY_TYPE_OPTIONS.map((o) => o.value);
 
 interface DetailRow {
@@ -432,7 +445,6 @@ async function handleSubmit() {
     const res = await BatchPayAPI.batchCreate({
       enterprise_id: enterpriseId.value,
       order_title: form.order_title,
-      agreement_no: form.agreement_no || undefined,
       transfer_scene_name: form.transfer_scene_name || undefined,
       transfer_scene_report_infos: sceneReportInfos.length ? sceneReportInfos : undefined,
       time_expire: form.time_expire || undefined,

+ 0 - 3
java/src/main/java/com/payment/platform/module/payment/batch/dto/BatchCreateDTO.java

@@ -27,9 +27,6 @@ public class BatchCreateDTO {
     @Schema(description = "批次标题(展示在付款方账单)")
     private String orderTitle;
 
-    @Schema(description = "制单授权协议号(payer_info.ext_info 传此值则校验指定协议;不传校验任意协议)")
-    private String agreementNo;
-
     @NotBlank(message = "转账场景不能为空(26年新接入商户必传)")
     @Schema(description = "转账场景(26年新接入商户必传): 现金营销/企业退款/佣金报酬/二手回收/业务结算/公益补助/行政补贴和退款/保险理赔")
     private String transferSceneName;

+ 11 - 7
java/src/main/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayService.java

@@ -307,21 +307,25 @@ public class AlipayBatchPayService {
             // 付款方 + 制单授权协议(Ruling 19/22: 不接受客户端指定,付款方身份遵循系统惯例 identity 优先回退 enterprise_id)
             EnterpriseEntity ent = requireEnterprise(dto.getEnterpriseId());
             String payerUid = payerIdentity(ent);
-            // 未完成授权不允许制单: 该付款方无生效授权记录(AUTHED/NORMAL)→ 本地预检拦截;
+            // 未完成授权不允许制单: 取该付款方最新生效授权(AUTHED/NORMAL)→ 无则本地预检拦截;
             // 支付宝侧 AUTH_INFO_NOT_EXISTS 兜底保留(防本地与支付宝状态不一致的竞态)
-            Long authedCount = batchAuthorizeMapper.selectCount(
+            BatchAuthorizeEntity authed = batchAuthorizeMapper.selectOne(
                     new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchAuthorizeEntity>()
                             .eq(BatchAuthorizeEntity::getEnterpriseId, dto.getEnterpriseId())
                             .eq(BatchAuthorizeEntity::getParticipantId, payerUid)
-                            .in(BatchAuthorizeEntity::getStatus, "AUTHED", "NORMAL"));
-            if (authedCount == null || authedCount == 0)
+                            .in(BatchAuthorizeEntity::getStatus, "AUTHED", "NORMAL")
+                            .orderByDesc(BatchAuthorizeEntity::getId)
+                            .last("LIMIT 1"));
+            if (authed == null)
                 throw new BusinessException(400, "该企业尚未完成制单授权,请先在「制单授权」中生成授权链接并完成授权");
+            // 协议号自动带出: 取该笔生效授权的 agreement_no(支付宝生成,不接受客户端指定)
+            String agreementNo = authed.getAgreementNo();
             Participant payer = new Participant();
             payer.setIdentity(payerUid);
             payer.setIdentityType(payerIdentityType(ent));
-            if (dto.getAgreementNo() != null && !dto.getAgreementNo().isBlank()) {
+            if (agreementNo != null && !agreementNo.isBlank()) {
                 Map<String, String> ext = new LinkedHashMap<>();
-                ext.put("agreement_no", dto.getAgreementNo());
+                ext.put("agreement_no", agreementNo);
                 try { payer.setExtInfo(oMapper.writeValueAsString(ext)); }
                 catch (Exception e) { throw new BusinessException(400, "序列化 ext_info 失败"); }
             }
@@ -367,7 +371,7 @@ public class AlipayBatchPayService {
             order.setOrderTitle(dto.getOrderTitle());
             order.setStatus("INIT");
             order.setPayerUid(payerUid);
-            order.setAgreementNo(dto.getAgreementNo());
+            order.setAgreementNo(agreementNo);
             order.setTransferSceneName(dto.getTransferSceneName());
             if (dto.getTransferSceneReportInfos() != null)
                 try { order.setTransferSceneReportInfos(oMapper.writeValueAsString(dto.getTransferSceneReportInfos())); }

+ 27 - 4
java/src/test/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayServiceTest.java

@@ -77,8 +77,6 @@ class AlipayBatchPayServiceTest {
         EnterpriseEntity ent = new EnterpriseEntity();
         ent.setEnterpriseId("E100");
         lenient().when(enterpriseMapper.selectByEnterpriseIdIgnoreTenant("E100")).thenReturn(ent);
-        // 反馈轮 5: 制单预检 — 默认该付款方已有 AUTHED 授权记录(无授权用例单独 stub 0)
-        lenient().when(batchAuthorizeMapper.selectCount(any())).thenReturn(1L);
         // 初始化 MyBatis-Plus lambda 元数据缓存,使 LambdaQueryWrapper.getSqlSegment() 可在无 Spring 上下文的单测中工作
         MybatisConfiguration configuration = new MybatisConfiguration();
         TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), BatchOrderEntity.class);
@@ -183,12 +181,16 @@ class AlipayBatchPayServiceTest {
         resp.setBatchTransId("BT1");
         resp.setStatus("INIT");
         when(alipayClient.certificateExecute(any(AlipayFundBatchCreateRequest.class))).thenReturn(resp);
+        // 制单预检: 已有 AUTHED 授权(协议号由系统自动带出,不接受客户端指定)
+        BatchAuthorizeEntity authed = new BatchAuthorizeEntity();
+        authed.setStatus("AUTHED");
+        authed.setAgreementNo("AGMT001");
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(authed);
 
         BatchCreateDTO dto = new BatchCreateDTO();
         dto.setEnterpriseId("E100");
         dto.setOutBatchNo("B1");
         dto.setOrderTitle("202608报销");
-        dto.setAgreementNo("AGMT001");
         dto.setTransferSceneName("佣金报酬");
         dto.setTransferSceneReportInfos(List.of(Map.of("info_type", "佣金报酬说明", "info_content", "8月家政服务报酬")));
         BatchCreateDTO.BatchDetailDTO detail = new BatchCreateDTO.BatchDetailDTO();
@@ -265,6 +267,10 @@ class AlipayBatchPayServiceTest {
         existing.setBatchTransId("BT1");
         existing.setStatus("INIT");
         when(batchOrderMapper.selectOne(any())).thenReturn(existing);
+        BatchAuthorizeEntity authed = new BatchAuthorizeEntity();
+        authed.setStatus("AUTHED");
+        authed.setAgreementNo("AGMT001");
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(authed);
 
         BatchCreateDTO dto = new BatchCreateDTO();
         dto.setEnterpriseId("E100");
@@ -413,6 +419,11 @@ class AlipayBatchPayServiceTest {
         resp.setSubCode("AUTH_INFO_NOT_EXISTS");
         resp.setMsg("Business Failed");
         when(alipayClient.certificateExecute(any(AlipayFundBatchCreateRequest.class))).thenReturn(resp);
+        // 本地预检通过(有 AUTHED 记录)后仍触发支付宝侧 AUTH_INFO_NOT_EXISTS 兜底路径
+        BatchAuthorizeEntity authed = new BatchAuthorizeEntity();
+        authed.setStatus("AUTHED");
+        authed.setAgreementNo("AGMT001");
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(authed);
 
         BatchCreateDTO dto = new BatchCreateDTO();
         dto.setEnterpriseId("E100");
@@ -895,6 +906,10 @@ class AlipayBatchPayServiceTest {
         AlipayFundBatchCreateResponse resp = new AlipayFundBatchCreateResponse();
         resp.setOutBatchNo("B1");
         when(alipayClient.certificateExecute(any(AlipayFundBatchCreateRequest.class))).thenReturn(resp);
+        BatchAuthorizeEntity authed = new BatchAuthorizeEntity();
+        authed.setStatus("AUTHED");
+        authed.setAgreementNo("AGMT001");
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(authed);
 
         BatchCreateDTO dto = new BatchCreateDTO();
         dto.setEnterpriseId("E100");
@@ -924,7 +939,7 @@ class AlipayBatchPayServiceTest {
     @Test
     void batchCreate_notAuthorized_throws() throws AlipayApiException {
         // 反馈轮 5: 未完成授权不允许制单 — 该付款方无 AUTHED 授权记录 → 本地预检拦截,不调支付宝
-        when(batchAuthorizeMapper.selectCount(any())).thenReturn(0L);
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(null);
 
         BatchCreateDTO dto = new BatchCreateDTO();
         dto.setEnterpriseId("E100");
@@ -953,6 +968,10 @@ class AlipayBatchPayServiceTest {
         AlipayFundBatchCreateResponse resp = new AlipayFundBatchCreateResponse();
         resp.setOutBatchNo("B1");
         when(alipayClient.certificateExecute(any(AlipayFundBatchCreateRequest.class))).thenReturn(resp);
+        BatchAuthorizeEntity authed = new BatchAuthorizeEntity();
+        authed.setStatus("NORMAL");
+        authed.setAgreementNo("AGMT001");
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(authed);
 
         BatchCreateDTO dto = new BatchCreateDTO();
         dto.setEnterpriseId("E100");
@@ -983,6 +1002,10 @@ class AlipayBatchPayServiceTest {
         AlipayFundBatchCreateResponse resp = new AlipayFundBatchCreateResponse();
         resp.setOutBatchNo("B1");
         when(alipayClient.certificateExecute(any(AlipayFundBatchCreateRequest.class))).thenReturn(resp);
+        BatchAuthorizeEntity authed = new BatchAuthorizeEntity();
+        authed.setStatus("AUTHED");
+        authed.setAgreementNo("AGMT001");
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(authed);
 
         BatchCreateDTO dto = new BatchCreateDTO();
         dto.setEnterpriseId("E100");