Przeglądaj źródła

feat: 批量付款付款方UID自动带出 - 授权/创建批次默认企业自身UID,新增authorizeRebind重新生成授权链接

- authorizeApply: participantId 为空默认 enterprise_id(企业自身支付宝 UID,DB 实证 pay_enterprise.enterprise_id == account_id)
- 新增 POST /authorize/rebind: 作废最新 AUTHING 申请(置 UNBIND)换新 out_biz_no 重新申请;AUTHED 终态拒绝;无记录等价直接申请
- batchCreate: payer_uid 为空默认 enterprise_id
- 测试 +7(48 用例全绿):blankParticipantId 兜底 / rebind 三态 / payer_uid 兜底
alphaH 5 dni temu
rodzic
commit
194ebfb142

+ 6 - 0
java/src/main/java/com/payment/platform/module/payment/batch/controller/BatchPayController.java

@@ -29,6 +29,12 @@ public class BatchPayController {
                 (String) b.get("enterprise_id"), (String) b.get("participant_id")));
     }
 
+    @PreAuthorize("@perm.hasAny('module_payment:account:authorize')")
+    @PostMapping("/authorize/rebind")
+    public Result<Map<String, String>> authorizeRebind(@RequestBody Map<String, Object> b) {
+        return Result.ok(batchPayService.authorizeRebind((String) b.get("enterprise_id")));
+    }
+
     @PreAuthorize("@perm.hasAny('module_payment:account:authorize')")
     @GetMapping("/authorize/query")
     public Result<Map<String, String>> queryAuthorize(

+ 39 - 3
java/src/main/java/com/payment/platform/module/payment/batch/service/AlipayBatchPayService.java

@@ -86,6 +86,40 @@ public class AlipayBatchPayService {
     /** alipay.fund.authorize.uni.apply — 生成制单授权短链接(PC 渠道) */
     @Transactional
     public Map<String, String> authorizeApply(String enterpriseId, String participantId) {
+        requireEnterpriseId(enterpriseId);
+        // Ruling 19/21: 付款方即企业自己的支付宝 UID(pay_enterprise.enterprise_id),
+        // participant_id 为空时默认企业 ID(前端已只读带出,此处防绕过直调)
+        String payerUid = (participantId == null || participantId.isBlank()) ? enterpriseId : participantId;
+        return doAuthorizeApply(enterpriseId, payerUid);
+    }
+
+    /**
+     * 制单授权重新生成: 作废最新一条非终态授权申请(AUTHING → UNBIND,旧链接失效),
+     * 换新 out_biz_no 重新申请。AUTHED 为终态(永久生效授权)拒绝重新生成。
+     * 无既有记录时等价于直接申请。
+     */
+    @Transactional
+    public Map<String, String> authorizeRebind(String enterpriseId) {
+        requireEnterpriseId(enterpriseId);
+        BatchAuthorizeEntity existing = batchAuthorizeMapper.selectOne(
+                new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchAuthorizeEntity>()
+                        .eq(BatchAuthorizeEntity::getEnterpriseId, enterpriseId)
+                        .ne(BatchAuthorizeEntity::getStatus, "UNBIND")
+                        .orderByDesc(BatchAuthorizeEntity::getId)
+                        .last("LIMIT 1"));
+        if (existing != null) {
+            if ("AUTHED".equals(existing.getStatus()))
+                throw new BusinessException(400, "该企业已存在生效授权,无需重新生成");
+            existing.setStatus("UNBIND");
+            batchAuthorizeMapper.updateById(existing);
+            log.info("制单授权重新生成,作废旧申请: old_out_biz_no={}, enterprise_id={}",
+                    existing.getOutBizNo(), enterpriseId);
+        }
+        return doAuthorizeApply(enterpriseId, enterpriseId);
+    }
+
+    /** 授权申请公共逻辑(三态预检 + 调支付宝 + 落库),apply 与 rebind 共用 */
+    private Map<String, String> doAuthorizeApply(String enterpriseId, String participantId) {
         if (participantId == null || participantId.isBlank())
             throw new BusinessException(400, "付款方支付宝账号不能为空");
         // 重复新增防护三态(设计文档 2.4/7):
@@ -262,9 +296,11 @@ 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.getPayerUid() == null || dto.getPayerUid().isBlank())
+                    ? dto.getEnterpriseId() : dto.getPayerUid();
             Participant payer = new Participant();
-            payer.setIdentity(dto.getPayerUid());
+            payer.setIdentity(payerUid);
             payer.setIdentityType("ALIPAY_USER_ID");
             if (dto.getAgreementNo() != null && !dto.getAgreementNo().isBlank()) {
                 Map<String, String> ext = new LinkedHashMap<>();
@@ -313,7 +349,7 @@ public class AlipayBatchPayService {
             order.setTotalCount(dto.getDetails().size());
             order.setOrderTitle(dto.getOrderTitle());
             order.setStatus("INIT");
-            order.setPayerUid(dto.getPayerUid());
+            order.setPayerUid(payerUid);
             order.setAgreementNo(dto.getAgreementNo());
             order.setTransferSceneName(dto.getTransferSceneName());
             if (dto.getTransferSceneReportInfos() != null)

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

@@ -737,6 +737,134 @@ class AlipayBatchPayServiceTest {
         verify(batchAuthorizeMapper).insert(any());
     }
 
+    // ==================== 用户反馈轮 2: 付款方 UID 自动带出 + 授权可重新生成 ====================
+
+    @Test
+    void authorizeApply_blankParticipantId_defaultsToEnterpriseId() throws AlipayApiException {
+        // Ruling 19/21: 付款方即企业自己,participant_id 为空时默认企业 enterprise_id(防绕过前端直调)
+        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("E100", m.getPrincipalInfo().getParticipantId());
+        ArgumentCaptor<BatchAuthorizeEntity> ent = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
+        verify(batchAuthorizeMapper).insert(ent.capture());
+        assertEquals("E100", ent.getValue().getParticipantId());
+    }
+
+    @Test
+    void authorizeApply_nullEnterpriseId_throws() {
+        // requireEnterpriseId 防御: 企业都不存在时直接拒绝而非默认空串申请
+        assertThrows(BusinessException.class, () -> service.authorizeApply(null, null));
+    }
+
+    @Test
+    void authorizeRebind_noExisting_appliesFresh() throws AlipayApiException {
+        // 无既有授权记录 → 直接申请(付款方 = 企业自己)
+        AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
+        resp.setAuthorizeLink("https://ur.alipay.com/abc");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
+
+        Map<String, String> result = service.authorizeRebind("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("E100", m.getPrincipalInfo().getParticipantId());
+        verify(batchAuthorizeMapper, never()).updateById(any());
+    }
+
+    @Test
+    void authorizeRebind_authingExisting_unbindsAndReapplies() throws AlipayApiException {
+        // 最新 AUTHING 记录 → 置 UNBIND(旧链接作废)→ 换新 out_biz_no 重新申请
+        BatchAuthorizeEntity existing = new BatchAuthorizeEntity();
+        existing.setId(1L);
+        existing.setEnterpriseId("E100");
+        existing.setParticipantId("E100");
+        existing.setOutBizNo("OLD1");
+        existing.setStatus("AUTHING");
+        // 序列 stub: rebind 预检命中旧记录 → 作废后 doAuthorizeApply 的二次预检(ne UNBIND)应返回 null
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing, null);
+
+        AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
+        resp.setAuthorizeLink("https://ur.alipay.com/abc");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
+
+        Map<String, String> result = service.authorizeRebind("E100");
+
+        assertEquals("AUTHING", result.get("status"));
+        verify(batchAuthorizeMapper).updateById(argThat(e -> "UNBIND".equals(e.getStatus()) && "OLD1".equals(e.getOutBizNo())));
+        ArgumentCaptor<BatchAuthorizeEntity> ent = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
+        verify(batchAuthorizeMapper).insert(ent.capture());
+        assertFalse(ent.getValue().getOutBizNo().equals("OLD1"), "应换新 out_biz_no 重新申请");
+        assertEquals("E100", ent.getValue().getParticipantId());
+    }
+
+    @Test
+    void authorizeRebind_authedExisting_throws() throws AlipayApiException {
+        // AUTHED 是终态(永久生效授权),重新生成无意义 → 拒绝
+        BatchAuthorizeEntity existing = new BatchAuthorizeEntity();
+        existing.setId(1L);
+        existing.setEnterpriseId("E100");
+        existing.setParticipantId("E100");
+        existing.setOutBizNo("OLD1");
+        existing.setStatus("AUTHED");
+        when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
+
+        BusinessException ex = assertThrows(BusinessException.class, () -> service.authorizeRebind("E100"));
+
+        assertEquals(400, ex.getCode());
+        assertTrue(ex.getMessage().contains("生效授权"), ex.getMessage());
+        verify(alipayClient, never()).certificateExecute(any());
+        verify(batchAuthorizeMapper, never()).updateById(any());
+        verify(batchAuthorizeMapper, never()).insert(any());
+    }
+
+    @Test
+    void authorizeRebind_blankEnterpriseId_throws() {
+        assertThrows(BusinessException.class, () -> service.authorizeRebind(""));
+    }
+
+    @Test
+    void batchCreate_blankPayerUid_defaultsToEnterpriseId() throws AlipayApiException {
+        // Ruling 21: payer_uid 为空时默认企业 enterprise_id(付款方即企业自己)
+        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");
+        dto.setPayerUid(" ");
+        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("E100", m.getPayerInfo().getIdentity());
+        // DB 回写 payer_uid 同样默认企业 ID
+        ArgumentCaptor<BatchOrderEntity> order = ArgumentCaptor.forClass(BatchOrderEntity.class);
+        verify(batchOrderMapper).insert(order.capture());
+        assertEquals("E100", order.getValue().getPayerUid());
+    }
+
     // ==================== M3: batchClose INIT 守卫 ====================
 
     @Test