Explorar el Código

feat: 批量付款 - 制单授权申请与查询

alphaH hace 6 días
padre
commit
9e8eded96e

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

@@ -0,0 +1,130 @@
+package com.payment.platform.module.payment.batch.service;
+
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.domain.AlipayFundAuthorizeUniApplyModel;
+import com.alipay.api.domain.AlipayFundAuthorizeUniQueryModel;
+import com.alipay.api.domain.AuthParticipantInfo;
+import com.alipay.api.request.AlipayFundAuthorizeUniApplyRequest;
+import com.alipay.api.request.AlipayFundAuthorizeUniQueryRequest;
+import com.alipay.api.response.AlipayFundAuthorizeUniApplyResponse;
+import com.alipay.api.response.AlipayFundAuthorizeUniQueryResponse;
+import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.common.response.PageResult;
+import com.payment.platform.common.utils.ExcelUtil;
+import com.payment.platform.common.utils.SnowflakeIdGenerator;
+import com.payment.platform.core.alipay.AlipayClientFactory;
+import com.payment.platform.module.payment.batch.entity.BatchAuthorizeEntity;
+import com.payment.platform.module.payment.batch.entity.BatchDetailEntity;
+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.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 批量付款到户有密 — 制单授权
+ * <p>
+ * 授权链路: authorizeApply 生成短链(PC) → 付款方端内授权(永久生效) →
+ * fund.authorize.status.notify 异步通知回写 agreement_no。
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AlipayBatchPayService {
+
+    private static final String AUTHORIZE_PRODUCT_CODE = "TRANSFER_API_STANDARD_AUTHORIZATION";
+    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";
+
+    /** 静态 ObjectMapper(同 NotificationService 第 55 行模式),用于 ext_info / 报备 JSON 序列化 */
+    private static final ObjectMapper oMapper = new ObjectMapper();
+
+    private final AlipayClientFactory alipayClientFactory;
+    private final BatchAuthorizeMapper batchAuthorizeMapper;
+
+    /** alipay.fund.authorize.uni.apply — 生成制单授权短链接(PC 渠道) */
+    @Transactional
+    public Map<String, String> authorizeApply(String enterpriseId, String participantId) {
+        if (participantId == null || participantId.isBlank())
+            throw new BusinessException(400, "付款方支付宝账号不能为空");
+        String outBizNo = SnowflakeIdGenerator.nextIdStr();
+        try {
+            AlipayFundAuthorizeUniApplyModel model = new AlipayFundAuthorizeUniApplyModel();
+            model.setProductCode(AUTHORIZE_PRODUCT_CODE);
+            model.setBizScene(AUTHORIZE_BIZ_SCENE);
+            model.setOutBizNo(outBizNo);
+            model.setAuthorizeLinkType(AUTHORIZE_LINK_TYPE);
+            model.setChannel("pc");
+            AuthParticipantInfo principal = new AuthParticipantInfo();
+            principal.setParticipantId(participantId);
+            principal.setParticipantIdType("ALIPAY_USER_ID");
+            model.setPrincipalInfo(principal);
+
+            AlipayFundAuthorizeUniApplyRequest request = new AlipayFundAuthorizeUniApplyRequest();
+            request.setBizModel(model);
+            AlipayClient client = alipayClientFactory.getClient(enterpriseId, BIZ_TYPE);
+            AlipayFundAuthorizeUniApplyResponse response = client.certificateExecute(request);
+            if (!response.isSuccess())
+                throw new BusinessException(400, "生成授权链接失败: " + response.getMsg());
+
+            BatchAuthorizeEntity entity = new BatchAuthorizeEntity();
+            entity.setEnterpriseId(enterpriseId);
+            entity.setOutBizNo(outBizNo);
+            entity.setParticipantId(participantId);
+            entity.setParticipantIdType("ALIPAY_USER_ID");
+            entity.setStatus("AUTHING");
+            entity.setAuthorizeLink(response.getAuthorizeLink());
+            batchAuthorizeMapper.insert(entity);
+
+            return Map.of("authorize_link", response.getAuthorizeLink(),
+                    "out_biz_no", outBizNo, "status", "AUTHING");
+        } catch (AlipayApiException e) {
+            throw new BusinessException(400, "生成授权链接失败: " + e.getMessage());
+        }
+    }
+
+    /** alipay.fund.authorize.uni.query — 查询制单授权状态(单协议) */
+    public Map<String, String> queryAuthorize(String enterpriseId, String outBizNo) {
+        try {
+            AlipayFundAuthorizeUniQueryModel model = new AlipayFundAuthorizeUniQueryModel();
+            model.setProductCode(AUTHORIZE_PRODUCT_CODE);
+            model.setBizScene(AUTHORIZE_BIZ_SCENE);
+            model.setOutBizNo(outBizNo);
+
+            AlipayFundAuthorizeUniQueryRequest request = new AlipayFundAuthorizeUniQueryRequest();
+            request.setBizModel(model);
+            AlipayFundAuthorizeUniQueryResponse response =
+                    alipayClientFactory.getClient(enterpriseId, BIZ_TYPE).certificateExecute(request);
+            if (!response.isSuccess())
+                throw new BusinessException(400, "查询授权状态失败: " + response.getMsg());
+
+            if (response.getAgreementNo() != null && !response.getAgreementNo().isBlank()) {
+                BatchAuthorizeEntity entity = batchAuthorizeMapper.selectOne(
+                        new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<BatchAuthorizeEntity>()
+                                .eq(BatchAuthorizeEntity::getOutBizNo, outBizNo));
+                if (entity != null) {
+                    entity.setAgreementNo(response.getAgreementNo());
+                    entity.setStatus("AUTHED");
+                    batchAuthorizeMapper.updateById(entity);
+                }
+            }
+            return Map.of("agreement_no", response.getAgreementNo() != null ? response.getAgreementNo() : "",
+                    "status", response.getStatus() != null ? response.getStatus() : "AUTHING");
+        } catch (AlipayApiException e) {
+            throw new BusinessException(400, "查询授权状态失败: " + e.getMessage());
+        }
+    }
+}

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

@@ -0,0 +1,89 @@
+package com.payment.platform.module.payment.batch.service;
+
+import com.alipay.api.AlipayClient;
+import com.alipay.api.AlipayApiException;
+import com.alipay.api.domain.AlipayFundAuthorizeUniApplyModel;
+import com.alipay.api.domain.AlipayFundAuthorizeUniQueryModel;
+import com.alipay.api.request.AlipayFundAuthorizeUniApplyRequest;
+import com.alipay.api.request.AlipayFundAuthorizeUniQueryRequest;
+import com.alipay.api.response.AlipayFundAuthorizeUniApplyResponse;
+import com.alipay.api.response.AlipayFundAuthorizeUniQueryResponse;
+import com.payment.platform.common.exception.BusinessException;
+import com.payment.platform.core.alipay.AlipayClientFactory;
+import com.payment.platform.module.payment.batch.entity.BatchAuthorizeEntity;
+import com.payment.platform.module.payment.batch.mapper.BatchAuthorizeMapper;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+@ExtendWith(MockitoExtension.class)
+class AlipayBatchPayServiceTest {
+
+    @Mock private AlipayClientFactory alipayClientFactory;
+    @Mock private AlipayClient alipayClient;
+    @Mock private BatchAuthorizeMapper batchAuthorizeMapper;
+    private AlipayBatchPayService service;
+
+    @BeforeEach
+    void setUp() {
+        service = new AlipayBatchPayService(alipayClientFactory, batchAuthorizeMapper);
+        when(alipayClientFactory.getClient("E100", "BATCH_PAY")).thenReturn(alipayClient);
+    }
+
+    @Test
+    void authorizeApply_returnsShortLinkAndPersists() throws AlipayApiException {
+        AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
+        resp.setAuthorizeLink("https://ur.alipay.com/abc");
+        resp.setOutBizNo("A1");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
+
+        Map<String, String> result = service.authorizeApply("E100", "2088123412341234");
+
+        assertEquals("https://ur.alipay.com/abc", result.get("authorize_link"));
+        assertEquals("AUTHING", result.get("status"));
+
+        ArgumentCaptor<AlipayFundAuthorizeUniApplyRequest> cap = ArgumentCaptor.forClass(AlipayFundAuthorizeUniApplyRequest.class);
+        verify(alipayClient).certificateExecute(cap.capture());
+        AlipayFundAuthorizeUniApplyModel m = (AlipayFundAuthorizeUniApplyModel) cap.getValue().getBizModel();
+        assertEquals("TRANSFER_API_STANDARD_AUTHORIZATION", m.getProductCode());
+        assertEquals("STANDARD_CREATE_FUND_ORDER", m.getBizScene());
+        assertEquals("SHORT_URL", m.getAuthorizeLinkType());
+        assertEquals("pc", m.getChannel());
+        assertEquals("2088123412341234", m.getPrincipalInfo().getParticipantId());
+
+        ArgumentCaptor<BatchAuthorizeEntity> ent = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
+        verify(batchAuthorizeMapper).insert(ent.capture());
+        assertEquals("E100", ent.getValue().getEnterpriseId());
+        assertEquals("AUTHING", ent.getValue().getStatus());
+    }
+
+    @Test
+    void authorizeApply_failure_throwsBusinessException() throws AlipayApiException {
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class)))
+                .thenThrow(new AlipayApiException("network error"));
+
+        assertThrows(BusinessException.class, () -> service.authorizeApply("E100", "2088123412341234"));
+    }
+
+    @Test
+    void queryAuthorize_returnsAgreementNo() throws AlipayApiException {
+        AlipayFundAuthorizeUniQueryResponse resp = new AlipayFundAuthorizeUniQueryResponse();
+        resp.setAgreementNo("AGMT001");
+        resp.setStatus("AUTHED");
+        when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniQueryRequest.class))).thenReturn(resp);
+
+        Map<String, String> result = service.queryAuthorize("E100", "A1");
+
+        assertEquals("AGMT001", result.get("agreement_no"));
+        assertEquals("AUTHED", result.get("status"));
+    }
+}