Bläddra i källkod

fix: 批量付款付款方身份采用企业identity优先回退enterprise_id - 遵循AlipayTransferService惯例

用户指出参与方身份不应硬编码企业ID,应使用企业入驻身份identity(配套identityType)。
新增 requireEnterprise/payerIdentity/payerIdentityType,authorizeApply/authorizeRebind/
batchCreate 三处统一解析:identity非空用之,为空回退enterprise_id。
alphaH 5 dagar sedan
förälder
incheckning
2bc50e3c68

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

@@ -50,6 +50,9 @@ import java.time.OffsetDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
 import java.time.format.DateTimeParseException;
+import com.payment.platform.module.payment.enterprise.entity.EnterpriseEntity;
+import com.payment.platform.module.payment.enterprise.mapper.EnterpriseMapper;
+
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
@@ -82,17 +85,19 @@ public class AlipayBatchPayService {
     private final BatchAuthorizeMapper batchAuthorizeMapper;
     private final BatchOrderMapper batchOrderMapper;
     private final BatchDetailMapper batchDetailMapper;
+    private final EnterpriseMapper enterpriseMapper;
 
     /**
      * alipay.fund.authorize.uni.apply — 生成制单授权短链接(PC 渠道)
      * <p>
-     * Ruling 19/21: 付款方即企业自己的支付宝 UID(pay_enterprise.enterprise_id),
-     * 不接收客户端传入的 participant_id(防篡改指向他人账号),恒以 enterprise_id 为付款方。
+     * Ruling 19/22: 付款方即企业自己,不接收客户端传入的 participant_id(防篡改指向他人账号)。
+     * 付款方身份遵循系统惯例(同 AlipayTransferService.createOnboard): 企业入驻身份
+     * pay_enterprise.identity 优先(配套 identityType),为空回退 enterprise_id。
      */
     @Transactional
     public Map<String, String> authorizeApply(String enterpriseId) {
-        requireEnterpriseId(enterpriseId);
-        return doAuthorizeApply(enterpriseId, enterpriseId);
+        EnterpriseEntity ent = requireEnterprise(enterpriseId);
+        return doAuthorizeApply(enterpriseId, payerIdentity(ent), payerIdentityType(ent));
     }
 
     /**
@@ -102,7 +107,7 @@ public class AlipayBatchPayService {
      */
     @Transactional
     public Map<String, String> authorizeRebind(String enterpriseId) {
-        requireEnterpriseId(enterpriseId);
+        EnterpriseEntity ent = requireEnterprise(enterpriseId);
         BatchAuthorizeEntity existing = batchAuthorizeMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchAuthorizeEntity>()
                         .eq(BatchAuthorizeEntity::getEnterpriseId, enterpriseId)
@@ -117,11 +122,11 @@ public class AlipayBatchPayService {
             log.info("制单授权重新生成,作废旧申请: old_out_biz_no={}, enterprise_id={}",
                     existing.getOutBizNo(), enterpriseId);
         }
-        return doAuthorizeApply(enterpriseId, enterpriseId);
+        return doAuthorizeApply(enterpriseId, payerIdentity(ent), payerIdentityType(ent));
     }
 
     /** 授权申请公共逻辑(三态预检 + 调支付宝 + 落库),apply 与 rebind 共用 */
-    private Map<String, String> doAuthorizeApply(String enterpriseId, String participantId) {
+    private Map<String, String> doAuthorizeApply(String enterpriseId, String participantId, String participantIdType) {
         if (participantId == null || participantId.isBlank())
             throw new BusinessException(400, "付款方支付宝账号不能为空");
         // 重复新增防护三态(设计文档 2.4/7):
@@ -154,7 +159,7 @@ public class AlipayBatchPayService {
             model.setChannel("pc");
             AuthParticipantInfo principal = new AuthParticipantInfo();
             principal.setParticipantId(participantId);
-            principal.setParticipantIdType("ALIPAY_USER_ID");
+            principal.setParticipantIdType(participantIdType);
             model.setPrincipalInfo(principal);
 
             AlipayFundAuthorizeUniApplyRequest request = new AlipayFundAuthorizeUniApplyRequest();
@@ -298,11 +303,12 @@ public class AlipayBatchPayService {
             model.setOrderTitle(dto.getOrderTitle());
             if (dto.getTimeExpire() != null) model.setTimeExpire(dto.getTimeExpire());
             if (dto.getRemark() != null) model.setRemark(dto.getRemark());
-            // 付款方 + 制单授权协议(Ruling 19/21: 付款方即企业自己的 UID,不接受客户端传入,恒用 enterprise_id)
-            String payerUid = dto.getEnterpriseId();
+            // 付款方 + 制单授权协议(Ruling 19/22: 不接受客户端指定,付款方身份遵循系统惯例 identity 优先回退 enterprise_id)
+            EnterpriseEntity ent = requireEnterprise(dto.getEnterpriseId());
+            String payerUid = payerIdentity(ent);
             Participant payer = new Participant();
             payer.setIdentity(payerUid);
-            payer.setIdentityType("ALIPAY_USER_ID");
+            payer.setIdentityType(payerIdentityType(ent));
             if (dto.getAgreementNo() != null && !dto.getAgreementNo().isBlank()) {
                 Map<String, String> ext = new LinkedHashMap<>();
                 ext.put("agreement_no", dto.getAgreementNo());
@@ -567,6 +573,23 @@ public class AlipayBatchPayService {
         }
     }
 
+    /** 企业校验 + 身份解析: 付款方/参与方支付宝身份 = 企业入驻身份 identity(配套 identityType),为空回退 enterprise_id(同 AlipayTransferService.createOnboard 惯例) */
+    private EnterpriseEntity requireEnterprise(String enterpriseId) {
+        requireEnterpriseId(enterpriseId);
+        EnterpriseEntity ent = enterpriseMapper.selectByEnterpriseIdIgnoreTenant(enterpriseId);
+        if (ent == null)
+            throw new BusinessException(400, "企业不存在");
+        return ent;
+    }
+
+    private static String payerIdentity(EnterpriseEntity ent) {
+        return ent.getIdentity() != null ? ent.getIdentity() : ent.getEnterpriseId();
+    }
+
+    private static String payerIdentityType(EnterpriseEntity ent) {
+        return ent.getIdentityType() != null ? ent.getIdentityType() : "ALIPAY_USER_ID";
+    }
+
     /** 租户隔离: 企业 ID 是业务必需参数,为空直接拒绝(防御 Controller body 路径的零校验) */
     private static void requireEnterpriseId(String enterpriseId) {
         if (enterpriseId == null || enterpriseId.isBlank())

+ 47 - 1
java/src/test/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayServiceTest.java

@@ -32,6 +32,8 @@ import com.payment.platform.module.payment.batch.entity.BatchOrderEntity;
 import com.payment.platform.module.payment.batch.mapper.BatchAuthorizeMapper;
 import com.payment.platform.module.payment.batch.mapper.BatchDetailMapper;
 import com.payment.platform.module.payment.batch.mapper.BatchOrderMapper;
+import com.payment.platform.module.payment.enterprise.entity.EnterpriseEntity;
+import com.payment.platform.module.payment.enterprise.mapper.EnterpriseMapper;
 import jakarta.validation.ConstraintViolation;
 import jakarta.validation.Validation;
 import jakarta.validation.Validator;
@@ -62,13 +64,19 @@ class AlipayBatchPayServiceTest {
     @Mock private BatchAuthorizeMapper batchAuthorizeMapper;
     @Mock private BatchOrderMapper batchOrderMapper;
     @Mock private BatchDetailMapper batchDetailMapper;
+    @Mock private EnterpriseMapper enterpriseMapper;
     private AlipayBatchPayService service;
 
     @BeforeEach
     void setUp() {
-        service = new AlipayBatchPayService(alipayClientFactory, batchAuthorizeMapper, batchOrderMapper, batchDetailMapper);
+        service = new AlipayBatchPayService(alipayClientFactory, batchAuthorizeMapper, batchOrderMapper,
+                batchDetailMapper, enterpriseMapper);
         // lenient: 重复授权预检测试用例在到达 getClient 前即抛异常,该 stub 不会被使用
         lenient().when(alipayClientFactory.getClient("E100", "BATCH_PAY")).thenReturn(alipayClient);
+        // 付款方身份解析: E100 企业无 identity → 回退 enterprise_id(createOnboard 惯例);identity 有值用例单独 stub
+        EnterpriseEntity ent = new EnterpriseEntity();
+        ent.setEnterpriseId("E100");
+        lenient().when(enterpriseMapper.selectByEnterpriseIdIgnoreTenant("E100")).thenReturn(ent);
         // 初始化 MyBatis-Plus lambda 元数据缓存,使 LambdaQueryWrapper.getSqlSegment() 可在无 Spring 上下文的单测中工作
         MybatisConfiguration configuration = new MybatisConfiguration();
         TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), BatchOrderEntity.class);
@@ -751,6 +759,44 @@ class AlipayBatchPayServiceTest {
         assertEquals("E100", ent.getValue().getParticipantId());
     }
 
+    @Test
+    void authorizeApply_usesEnterpriseIdentityWhenPresent() throws AlipayApiException {
+        // Ruling 23: 企业入驻身份 identity 优先(配套 identityType),与 AlipayTransferService.createOnboard 惯例一致
+        EnterpriseEntity ent = new EnterpriseEntity();
+        ent.setEnterpriseId("E100");
+        ent.setIdentity("2088IDENTITY");
+        ent.setIdentityType("ALIPAY_OPEN_ID");
+        when(enterpriseMapper.selectByEnterpriseIdIgnoreTenant("E100")).thenReturn(ent);
+
+        AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
+        resp.setAuthorizeLink("https://ur.alipay.com/abc");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
+
+        Map<String, String> result = service.authorizeApply("E100");
+
+        assertEquals("AUTHING", result.get("status"));
+        ArgumentCaptor<AlipayFundAuthorizeUniApplyRequest> cap = ArgumentCaptor.forClass(AlipayFundAuthorizeUniApplyRequest.class);
+        verify(alipayClient).certificateExecute(cap.capture());
+        AlipayFundAuthorizeUniApplyModel m = (AlipayFundAuthorizeUniApplyModel) cap.getValue().getBizModel();
+        assertEquals("2088IDENTITY", m.getPrincipalInfo().getParticipantId());
+        assertEquals("ALIPAY_OPEN_ID", m.getPrincipalInfo().getParticipantIdType());
+        ArgumentCaptor<BatchAuthorizeEntity> entCap = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
+        verify(batchAuthorizeMapper).insert(entCap.capture());
+        assertEquals("2088IDENTITY", entCap.getValue().getParticipantId());
+    }
+
+    @Test
+    void authorizeApply_enterpriseNotFound_throws() throws AlipayApiException {
+        // requireEnterprise 防御: 企业不存在直接拒绝,不发起授权
+        when(enterpriseMapper.selectByEnterpriseIdIgnoreTenant("E100")).thenReturn(null);
+
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.authorizeApply("E100"));
+
+        assertEquals(400, ex.getCode());
+        assertTrue(ex.getMessage().contains("企业不存在"), ex.getMessage());
+        verify(alipayClient, never()).certificateExecute(any());
+    }
+
     @Test
     void authorizeApply_nullEnterpriseId_throws() {
         // requireEnterpriseId 防御: 企业都不存在时直接拒绝而非默认空串申请