Ver Fonte

fix: 未完成授权不允许制单 - batchCreate前置校验AUTHED授权记录

本地预检该付款方是否存在 AUTHED 制单授权记录,无则 400 拦截,
不再依赖支付宝侧 AUTH_INFO_NOT_EXISTS 兜底(保留以防竞态)。
alphaH há 5 dias atrás
pai
commit
4a405cf150

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayService.java

@@ -306,6 +306,15 @@ public class AlipayBatchPayService {
             // 付款方 + 制单授权协议(Ruling 19/22: 不接受客户端指定,付款方身份遵循系统惯例 identity 优先回退 enterprise_id)
             EnterpriseEntity ent = requireEnterprise(dto.getEnterpriseId());
             String payerUid = payerIdentity(ent);
+            // 未完成授权不允许制单: 该付款方无 AUTHED 授权记录 → 本地预检拦截;
+            // 支付宝侧 AUTH_INFO_NOT_EXISTS 兜底保留(防本地与支付宝状态不一致的竞态)
+            Long authedCount = batchAuthorizeMapper.selectCount(
+                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchAuthorizeEntity>()
+                            .eq(BatchAuthorizeEntity::getEnterpriseId, dto.getEnterpriseId())
+                            .eq(BatchAuthorizeEntity::getParticipantId, payerUid)
+                            .eq(BatchAuthorizeEntity::getStatus, "AUTHED"));
+            if (authedCount == null || authedCount == 0)
+                throw new BusinessException(400, "该企业尚未完成制单授权,请先在「制单授权」中生成授权链接并完成授权");
             Participant payer = new Participant();
             payer.setIdentity(payerUid);
             payer.setIdentityType(payerIdentityType(ent));

+ 62 - 0
java/src/test/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayServiceTest.java

@@ -77,6 +77,8 @@ 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);
@@ -903,6 +905,66 @@ class AlipayBatchPayServiceTest {
         assertEquals("E100", order.getValue().getPayerUid());
     }
 
+    @Test
+    void batchCreate_notAuthorized_throws() throws AlipayApiException {
+        // 反馈轮 5: 未完成授权不允许制单 — 该付款方无 AUTHED 授权记录 → 本地预检拦截,不调支付宝
+        when(batchAuthorizeMapper.selectCount(any())).thenReturn(0L);
+
+        BatchCreateDTO dto = new BatchCreateDTO();
+        dto.setEnterpriseId("E100");
+        dto.setOutBatchNo("B1");
+        dto.setOrderTitle("t");
+        BatchCreateDTO.BatchDetailDTO detail = new BatchCreateDTO.BatchDetailDTO();
+        detail.setOutBizNo("D1");
+        detail.setAmount(new BigDecimal("10"));
+        detail.setPayeeIdentity("a@b.com");
+        detail.setPayeeIdentityType("ALIPAY_LOGON_ID");
+        detail.setPayeeName("张三");
+        dto.setDetails(List.of(detail));
+
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.batchCreate(dto));
+
+        assertEquals(400, ex.getCode());
+        assertTrue(ex.getMessage().contains("尚未完成制单授权"), ex.getMessage());
+        verify(alipayClient, never()).certificateExecute(any());
+        verify(batchOrderMapper, never()).insert(any());
+        verify(batchDetailMapper, never()).insert(any());
+    }
+
+    @Test
+    void batchCreate_authorizedIdentity_proceedsWithIdentityAsPayer() throws AlipayApiException {
+        // 反馈轮 4/5 协同: identity 有值时预检与付款方均用 identity(而非 enterprise_id)
+        EnterpriseEntity ent = new EnterpriseEntity();
+        ent.setEnterpriseId("E100");
+        ent.setIdentity("2088IDENTITY");
+        ent.setIdentityType("ALIPAY_OPEN_ID");
+        when(enterpriseMapper.selectByEnterpriseIdIgnoreTenant("E100")).thenReturn(ent);
+        AlipayFundBatchCreateResponse resp = new AlipayFundBatchCreateResponse();
+        resp.setOutBatchNo("B1");
+        when(alipayClient.certificateExecute(any(AlipayFundBatchCreateRequest.class))).thenReturn(resp);
+
+        BatchCreateDTO dto = new BatchCreateDTO();
+        dto.setEnterpriseId("E100");
+        dto.setOutBatchNo("B1");
+        dto.setOrderTitle("t");
+        BatchCreateDTO.BatchDetailDTO detail = new BatchCreateDTO.BatchDetailDTO();
+        detail.setOutBizNo("D1");
+        detail.setAmount(new BigDecimal("10"));
+        detail.setPayeeIdentity("a@b.com");
+        detail.setPayeeIdentityType("ALIPAY_LOGON_ID");
+        detail.setPayeeName("张三");
+        dto.setDetails(List.of(detail));
+
+        Map<String, Object> result = service.batchCreate(dto);
+
+        assertEquals("B1", result.get("out_batch_no"));
+        ArgumentCaptor<AlipayFundBatchCreateRequest> cap = ArgumentCaptor.forClass(AlipayFundBatchCreateRequest.class);
+        verify(alipayClient).certificateExecute(cap.capture());
+        AlipayFundBatchCreateModel m = (AlipayFundBatchCreateModel) cap.getValue().getBizModel();
+        assertEquals("2088IDENTITY", m.getPayerInfo().getIdentity());
+        assertEquals("ALIPAY_OPEN_ID", m.getPayerInfo().getIdentityType());
+    }
+
     // ==================== M3: batchClose INIT 守卫 ====================
 
     @Test