Bläddra i källkod

feat: 授权申请主体类型可选 + 雪花ID序列化修复

- participant_id_type 可选: ALIPAY_USER_ID(默认)/ALIPAY_LOGON_ID/ALIPAY_OPEN_ID,
  空值回落默认, 非法值 400 拦截; rebind 复用原申请类型
- BatchAuthorizeEntity.getId 覆写 @JsonSerialize(ToStringSerializer):
  19位雪花ID超 JS 安全整数致前端精度丢失(后两位变0), rebind 回传错误 id 404
- batch-account-level.sql 补 DROP NOT NULL: 去企业化后实体无 enterprise_id,
  INSERT 不含该列, 原 NOT NULL 无默认值致授权/批次落库失败
alphaH 4 dagar sedan
förälder
incheckning
95ca793a6a

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

@@ -30,7 +30,7 @@ public class BatchPayController {
     public Result<Map<String, String>> authorizeApply(@Valid @RequestBody AuthorizeApplyDTO dto) {
         // 账号级: 主体信息来自表单(名称/uid/服务商),不再从企业解析
         return Result.ok(batchSubjectService.apply(
-                dto.getParticipantName(), dto.getParticipantId(), dto.getServiceProviderId()));
+                dto.getParticipantName(), dto.getParticipantId(), dto.getParticipantIdType(), dto.getServiceProviderId()));
     }
 
     @PreAuthorize("@perm.hasAny('module_payment:batch:authorize')")

+ 4 - 1
java/src/main/java/com/payment/platform/module/payment/batch/dto/AuthorizeApplyDTO.java

@@ -14,9 +14,12 @@ public class AuthorizeApplyDTO {
     private String participantName;
 
     @NotBlank(message = "支付宝账号不能为空")
-    @Schema(description = "授权主体支付宝 uid(个人/企业支付宝账号均可)")
+    @Schema(description = "授权主体账号(按主体类型:UID/登录号/OpenID)")
     private String participantId;
 
+    @Schema(description = "主体类型: ALIPAY_USER_ID 支付宝账号(默认) / ALIPAY_LOGON_ID 登录号 / ALIPAY_OPEN_ID OpenID")
+    private String participantIdType;
+
     @NotNull(message = "请选择服务商")
     @Schema(description = "服务商(授权申请 client 解析依据)")
     private Long serviceProviderId;

+ 9 - 0
java/src/main/java/com/payment/platform/module/payment/batch/entity/BatchAuthorizeEntity.java

@@ -2,6 +2,8 @@ package com.payment.platform.module.payment.batch.entity;
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
 import com.payment.platform.common.base.PaymentTenantBaseEntity;
 import com.payment.platform.common.handler.JsonbTypeHandler;
 import lombok.Data;
@@ -14,6 +16,13 @@ import java.time.OffsetDateTime;
 @EqualsAndHashCode(callSuper = true)
 @TableName("pay_batch_authorize")
 public class BatchAuthorizeEntity extends PaymentTenantBaseEntity {
+    /** 雪花ID(19位)超 JS 安全整数,序列化为字符串防前端精度丢失(rebind 以 id 为参数) */
+    @Override
+    @JsonSerialize(using = ToStringSerializer.class)
+    public Long getId() {
+        return super.getId();
+    }
+
     private String outBizNo;
     private String participantId;
     private String participantIdType;

+ 20 - 6
java/src/main/java/com/payment/platform/module/payment/batch/service/BatchSubjectService.java

@@ -45,6 +45,10 @@ public class BatchSubjectService {
     private static final String AUTHORIZE_BIZ_SCENE = "STANDARD_CREATE_FUND_ORDER";
     private static final String AUTHORIZE_LINK_TYPE = "SHORT_URL";
     private static final String BIZ_TYPE = "BATCH_PAY";
+    /** 授权主体类型(AuthParticipantInfo.participantIdType 文档枚举) */
+    private static final String DEFAULT_PARTICIPANT_ID_TYPE = "ALIPAY_USER_ID";
+    private static final java.util.Set<String> PARTICIPANT_ID_TYPES = java.util.Set.of(
+            "ALIPAY_LOGON_ID", "ALIPAY_OPEN_ID", "ALIPAY_USER_ID");
 
     private final AlipayClientFactory alipayClientFactory;
     private final BatchAuthorizeMapper batchAuthorizeMapper;
@@ -59,14 +63,23 @@ public class BatchSubjectService {
      * DB 兜底: uk_batch_authorize_active partial unique 索引 (tenant_id, participant_id) WHERE status <> 'UNBIND'
      */
     @Transactional
-    public Map<String, String> apply(String participantName, String participantId, Long serviceProviderId) {
+    public Map<String, String> apply(String participantName, String participantId, String participantIdType, Long serviceProviderId) {
         if (participantId == null || participantId.isBlank())
             throw new BusinessException(400, "支付宝账号不能为空");
         if (participantName == null || participantName.isBlank())
             throw new BusinessException(400, "主体名称不能为空");
         if (serviceProviderId == null)
             throw new BusinessException(400, "请选择服务商");
-        return doApply(participantName, participantId, serviceProviderId);
+        return doApply(participantName, participantId, normalizeParticipantIdType(participantIdType), serviceProviderId);
+    }
+
+    /** 主体类型归一: 空 → 默认支付宝账号(UID);非法值 → 拒绝 */
+    private String normalizeParticipantIdType(String participantIdType) {
+        if (participantIdType == null || participantIdType.isBlank())
+            return DEFAULT_PARTICIPANT_ID_TYPE;
+        if (!PARTICIPANT_ID_TYPES.contains(participantIdType))
+            throw new BusinessException(400, "主体类型非法: " + participantIdType);
+        return participantIdType;
     }
 
     /**
@@ -84,11 +97,12 @@ public class BatchSubjectService {
         batchAuthorizeMapper.updateById(existing);
         log.info("制单授权重新生成,作废旧申请: old_out_biz_no={}, participant_id={}",
                 existing.getOutBizNo(), existing.getParticipantId());
-        return doApply(existing.getParticipantName(), existing.getParticipantId(), existing.getServiceProviderId());
+        return doApply(existing.getParticipantName(), existing.getParticipantId(),
+                normalizeParticipantIdType(existing.getParticipantIdType()), existing.getServiceProviderId());
     }
 
     /** 授权申请公共逻辑(三态预检 + 调支付宝 + 落库),apply 与 rebind 共用 */
-    private Map<String, String> doApply(String participantName, String participantId, Long serviceProviderId) {
+    private Map<String, String> doApply(String participantName, String participantId, String participantIdType, Long serviceProviderId) {
         // 重复预检: 同租户同主体非 UNBIND 记录(租户隔离由拦截器自动追加)
         BatchAuthorizeEntity existing = batchAuthorizeMapper.selectOne(
                 new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchAuthorizeEntity>()
@@ -114,7 +128,7 @@ public class BatchSubjectService {
             model.setChannel("pc");
             AuthParticipantInfo principal = new AuthParticipantInfo();
             principal.setParticipantId(participantId);
-            principal.setParticipantIdType("ALIPAY_USER_ID");
+            principal.setParticipantIdType(participantIdType);
             principal.setName(participantName);
             model.setPrincipalInfo(principal);
 
@@ -128,7 +142,7 @@ public class BatchSubjectService {
             BatchAuthorizeEntity entity = new BatchAuthorizeEntity();
             entity.setOutBizNo(outBizNo);
             entity.setParticipantId(participantId);
-            entity.setParticipantIdType("ALIPAY_USER_ID");
+            entity.setParticipantIdType(participantIdType);
             entity.setParticipantName(participantName);
             entity.setServiceProviderId(serviceProviderId);
             entity.setStatus("AUTHING");

+ 5 - 0
java/src/main/resources/db/batch-account-level.sql

@@ -7,6 +7,11 @@ ALTER TABLE pay_batch_authorize
     ADD COLUMN IF NOT EXISTS participant_name varchar(128),
     ADD COLUMN IF NOT EXISTS service_provider_id bigint;
 
+-- 1b. 去企业化: 实体已无 enterprise_id 字段,INSERT 不含该列;原 NOT NULL 无默认值
+--     会导致授权申请/批次创建落库失败(null value in column "enterprise_id")→ 改为可空
+ALTER TABLE pay_batch_authorize ALTER COLUMN enterprise_id DROP NOT NULL;
+ALTER TABLE pay_batch_order     ALTER COLUMN enterprise_id DROP NOT NULL;
+
 -- 2. 唯一索引改租户级(先删旧 enterprise 索引)
 DROP INDEX IF EXISTS uk_batch_authorize_active;
 -- 联调期存量数据若存在同租户同 participant 多行,先清理再建索引:

+ 83 - 13
java/src/test/java/com/payment/platform/module/payment/batch/service/BatchSubjectServiceTest.java

@@ -71,7 +71,7 @@ class BatchSubjectServiceTest {
         resp.setAuthorizeLink("https://ur.alipay.com/abc");
         when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
 
-        Map<String, String> result = service.apply("张三公司", "2088111122223333", 1L);
+        Map<String, String> result = service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L);
 
         assertEquals("https://ur.alipay.com/abc", result.get("authorize_link"));
         assertEquals("AUTHING", result.get("status"));
@@ -97,28 +97,98 @@ class BatchSubjectServiceTest {
         verify(alipayClientFactory).getClientByProvider(1L, "BATCH_PAY");
     }
 
+    @Test
+    void apply_usesCustomParticipantIdType() throws AlipayApiException {
+        AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
+        resp.setAuthorizeLink("https://ur.alipay.com/custom");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
+
+        service.apply("张三公司", "18812345678", "ALIPAY_LOGON_ID", 1L);
+
+        ArgumentCaptor<AlipayFundAuthorizeUniApplyRequest> cap = ArgumentCaptor.forClass(AlipayFundAuthorizeUniApplyRequest.class);
+        verify(alipayClient).certificateExecute(cap.capture());
+        AlipayFundAuthorizeUniApplyModel m = (AlipayFundAuthorizeUniApplyModel) cap.getValue().getBizModel();
+        assertEquals("ALIPAY_LOGON_ID", m.getPrincipalInfo().getParticipantIdType());
+
+        ArgumentCaptor<BatchAuthorizeEntity> ent = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
+        verify(batchAuthorizeMapper).insert(ent.capture());
+        assertEquals("ALIPAY_LOGON_ID", ent.getValue().getParticipantIdType());
+    }
+
+    @Test
+    void apply_nullParticipantIdType_defaultsToUserId() throws AlipayApiException {
+        AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
+        resp.setAuthorizeLink("https://ur.alipay.com/abc");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
+
+        service.apply("张三公司", "2088111122223333", null, 1L);
+
+        ArgumentCaptor<AlipayFundAuthorizeUniApplyRequest> cap = ArgumentCaptor.forClass(AlipayFundAuthorizeUniApplyRequest.class);
+        verify(alipayClient).certificateExecute(cap.capture());
+        AlipayFundAuthorizeUniApplyModel m = (AlipayFundAuthorizeUniApplyModel) cap.getValue().getBizModel();
+        assertEquals("ALIPAY_USER_ID", m.getPrincipalInfo().getParticipantIdType());
+    }
+
+    @Test
+    void apply_invalidParticipantIdType_throws() throws AlipayApiException {
+        BusinessException ex = assertThrows(BusinessException.class,
+                () -> service.apply("张三公司", "2088111122223333", "BOGUS_TYPE", 1L));
+
+        assertEquals(400, ex.getCode());
+        assertTrue(ex.getMessage().contains("主体类型"), ex.getMessage());
+        verify(alipayClient, never()).certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class));
+        verify(batchAuthorizeMapper, never()).insert(any());
+    }
+
+    @Test
+    void rebind_reusesExistingParticipantIdType() throws AlipayApiException {
+        BatchAuthorizeEntity existing = new BatchAuthorizeEntity();
+        existing.setId(9L);
+        existing.setOutBizNo("OLD-2");
+        existing.setParticipantName("张三公司");
+        existing.setParticipantId("2088111122223333");
+        existing.setParticipantIdType("ALIPAY_OPEN_ID");
+        existing.setServiceProviderId(1L);
+        existing.setStatus("AUTHING");
+        when(batchAuthorizeMapper.selectById(9L)).thenReturn(existing);
+        AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
+        resp.setAuthorizeLink("https://ur.alipay.com/new");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
+
+        service.rebind(9L);
+
+        ArgumentCaptor<AlipayFundAuthorizeUniApplyRequest> cap = ArgumentCaptor.forClass(AlipayFundAuthorizeUniApplyRequest.class);
+        verify(alipayClient).certificateExecute(cap.capture());
+        AlipayFundAuthorizeUniApplyModel m = (AlipayFundAuthorizeUniApplyModel) cap.getValue().getBizModel();
+        assertEquals("ALIPAY_OPEN_ID", m.getPrincipalInfo().getParticipantIdType());
+        // 新申请落库沿用原主体类型
+        ArgumentCaptor<BatchAuthorizeEntity> ent = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
+        verify(batchAuthorizeMapper).insert(ent.capture());
+        assertEquals("ALIPAY_OPEN_ID", ent.getValue().getParticipantIdType());
+    }
+
     @Test
     void apply_failure_throwsBusinessException() throws AlipayApiException {
         when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class)))
                 .thenThrow(new AlipayApiException("network error"));
 
-        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", 1L));
+        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L));
     }
 
     @Test
     void apply_missingParticipantId_throwsBusinessException() {
-        assertThrows(BusinessException.class, () -> service.apply("张三公司", " ", 1L));
-        assertThrows(BusinessException.class, () -> service.apply("张三公司", null, 1L));
+        assertThrows(BusinessException.class, () -> service.apply("张三公司", " ", "ALIPAY_USER_ID", 1L));
+        assertThrows(BusinessException.class, () -> service.apply("张三公司", null, "ALIPAY_USER_ID", 1L));
     }
 
     @Test
     void apply_missingParticipantName_throwsBusinessException() {
-        assertThrows(BusinessException.class, () -> service.apply(" ", "2088111122223333", 1L));
+        assertThrows(BusinessException.class, () -> service.apply(" ", "2088111122223333", "ALIPAY_USER_ID", 1L));
     }
 
     @Test
     void apply_missingServiceProvider_throwsBusinessException() {
-        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", null));
+        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", null));
     }
 
     @Test
@@ -129,7 +199,7 @@ class BatchSubjectServiceTest {
         existing.setStatus("AUTHED");
         when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
 
-        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", 1L));
+        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L));
 
         verify(alipayClient, never()).certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class));
         verify(batchAuthorizeMapper, never()).insert(any());
@@ -149,7 +219,7 @@ class BatchSubjectServiceTest {
         resp.setAuthorizeLink("https://ur.alipay.com/new");
         when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
 
-        service.apply("张三公司", "2088111122223333", 1L);
+        service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L);
 
         verify(batchAuthorizeMapper).updateById(argThat(e -> "UNBIND".equals(e.getStatus()) && "OLD-1".equals(e.getOutBizNo())));
         ArgumentCaptor<BatchAuthorizeEntity> ent = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
@@ -162,7 +232,7 @@ class BatchSubjectServiceTest {
         AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
         when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
 
-        Map<String, String> result = service.apply("张三公司", "2088111122223333", 1L);
+        Map<String, String> result = service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L);
 
         assertEquals("", result.get("authorize_link"));
         assertEquals("AUTHING", result.get("status"));
@@ -271,7 +341,7 @@ class BatchSubjectServiceTest {
         doThrow(new org.springframework.dao.DuplicateKeyException("duplicate key")).when(batchAuthorizeMapper).insert(any());
 
         BusinessException ex = assertThrows(BusinessException.class,
-                () -> service.apply("张三公司", "2088111122223333", 1L));
+                () -> service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L));
 
         assertEquals(400, ex.getCode());
         assertTrue(ex.getMessage().contains("请勿重复操作"), ex.getMessage());
@@ -287,7 +357,7 @@ class BatchSubjectServiceTest {
         when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
 
         BusinessException ex = assertThrows(BusinessException.class,
-                () -> service.apply("张三公司", "2088111122223333", 1L));
+                () -> service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L));
 
         assertEquals(400, ex.getCode());
         assertTrue(ex.getMessage().contains("未完成"), ex.getMessage());
@@ -304,7 +374,7 @@ class BatchSubjectServiceTest {
         existing.setCreatedTime(java.time.OffsetDateTime.now());
         when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
 
-        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", 1L));
+        assertThrows(BusinessException.class, () -> service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L));
     }
 
     @Test
@@ -316,7 +386,7 @@ class BatchSubjectServiceTest {
         when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
 
         BusinessException ex = assertThrows(BusinessException.class,
-                () -> service.apply("张三公司", "2088111122223333", 1L));
+                () -> service.apply("张三公司", "2088111122223333", "ALIPAY_USER_ID", 1L));
 
         assertEquals(400, ex.getCode());
         verify(alipayClient, never()).certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class));