|
|
@@ -2,6 +2,7 @@ package com.payment.platform.module.payment.batch.service;
|
|
|
|
|
|
import com.alipay.api.AlipayClient;
|
|
|
import com.alipay.api.AlipayApiException;
|
|
|
+import com.alipay.api.domain.AccDetailModel;
|
|
|
import com.alipay.api.domain.AlipayFundAuthorizeUniApplyModel;
|
|
|
import com.alipay.api.domain.AlipayFundAuthorizeUniQueryModel;
|
|
|
import com.alipay.api.domain.AlipayFundBatchCreateModel;
|
|
|
@@ -17,7 +18,12 @@ import com.alipay.api.response.AlipayFundBatchCloseResponse;
|
|
|
import com.alipay.api.response.AlipayFundBatchCreateResponse;
|
|
|
import com.alipay.api.response.AlipayFundBatchDetailQueryResponse;
|
|
|
import com.alipay.api.response.AlipayFundTransRenderPayResponse;
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.payment.platform.common.exception.BusinessException;
|
|
|
+import com.payment.platform.common.response.PageResult;
|
|
|
import com.payment.platform.core.alipay.AlipayClientFactory;
|
|
|
import com.payment.platform.module.payment.batch.dto.BatchCreateDTO;
|
|
|
import com.payment.platform.module.payment.batch.entity.BatchAuthorizeEntity;
|
|
|
@@ -26,16 +32,23 @@ 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 jakarta.validation.ConstraintViolation;
|
|
|
+import jakarta.validation.Validation;
|
|
|
+import jakarta.validation.Validator;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
import org.mockito.ArgumentCaptor;
|
|
|
import org.mockito.Mock;
|
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+import org.springframework.dao.DuplicateKeyException;
|
|
|
|
|
|
import java.math.BigDecimal;
|
|
|
+import java.time.OffsetDateTime;
|
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
|
@@ -56,6 +69,11 @@ class AlipayBatchPayServiceTest {
|
|
|
service = new AlipayBatchPayService(alipayClientFactory, batchAuthorizeMapper, batchOrderMapper, batchDetailMapper);
|
|
|
// lenient: 重复授权预检测试用例在到达 getClient 前即抛异常,该 stub 不会被使用
|
|
|
lenient().when(alipayClientFactory.getClient("E100", "BATCH_PAY")).thenReturn(alipayClient);
|
|
|
+ // 初始化 MyBatis-Plus lambda 元数据缓存,使 LambdaQueryWrapper.getSqlSegment() 可在无 Spring 上下文的单测中工作
|
|
|
+ MybatisConfiguration configuration = new MybatisConfiguration();
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), BatchOrderEntity.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), BatchDetailEntity.class);
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), BatchAuthorizeEntity.class);
|
|
|
}
|
|
|
|
|
|
@Test
|
|
|
@@ -407,4 +425,332 @@ class AlipayBatchPayServiceTest {
|
|
|
assertEquals(400, ex.getCode());
|
|
|
assertTrue(ex.getMessage().contains("制单授权"), "应提示先完成制单授权: " + ex.getMessage());
|
|
|
}
|
|
|
+
|
|
|
+ // ==================== C2: 租户隔离(renderPay/batchQuery/batchClose 缺 enterpriseId 条件) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void renderPay_otherEnterpriseBatch_notFound() {
|
|
|
+ // 他人批次: selectOne 带 enterpriseId 条件查不到 → 404
|
|
|
+ when(batchOrderMapper.selectOne(any())).thenReturn(null);
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class, () -> service.renderPay("E999", "B1"));
|
|
|
+
|
|
|
+ assertEquals(404, ex.getCode());
|
|
|
+ @SuppressWarnings({ "unchecked", "rawtypes" })
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<BatchOrderEntity>> cap = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(batchOrderMapper).selectOne(cap.capture());
|
|
|
+ assertTrue(cap.getValue().getSqlSegment().contains("enterprise_id"),
|
|
|
+ "renderPay 查询必须带 enterpriseId 条件: " + cap.getValue().getSqlSegment());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchQuery_otherEnterpriseBatch_notFound() {
|
|
|
+ when(batchOrderMapper.selectOne(any())).thenReturn(null);
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class, () -> service.batchQuery("E999", "B1"));
|
|
|
+
|
|
|
+ assertEquals(404, ex.getCode());
|
|
|
+ @SuppressWarnings({ "unchecked", "rawtypes" })
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<BatchOrderEntity>> cap = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(batchOrderMapper).selectOne(cap.capture());
|
|
|
+ assertTrue(cap.getValue().getSqlSegment().contains("enterprise_id"),
|
|
|
+ "batchQuery 查询必须带 enterpriseId 条件: " + cap.getValue().getSqlSegment());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchClose_otherEnterpriseBatch_notFound() throws AlipayApiException {
|
|
|
+ when(batchOrderMapper.selectOne(any())).thenReturn(null);
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class, () -> service.batchClose("E999", "B1"));
|
|
|
+
|
|
|
+ assertEquals(404, ex.getCode());
|
|
|
+ verify(alipayClient, never()).certificateExecute(any());
|
|
|
+ @SuppressWarnings({ "unchecked", "rawtypes" })
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<BatchOrderEntity>> cap = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(batchOrderMapper).selectOne(cap.capture());
|
|
|
+ assertTrue(cap.getValue().getSqlSegment().contains("enterprise_id"),
|
|
|
+ "batchClose 查询必须带 enterpriseId 条件: " + cap.getValue().getSqlSegment());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== I1: 定时同步兜底(getPendingBatches) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void getPendingBatches_returnsOnlyNonTerminal() {
|
|
|
+ BatchOrderEntity b1 = new BatchOrderEntity();
|
|
|
+ b1.setId(1L);
|
|
|
+ b1.setOutBatchNo("B1");
|
|
|
+ b1.setStatus("INIT");
|
|
|
+ b1.setBatchTransId("BT1");
|
|
|
+ when(batchOrderMapper.selectList(any())).thenReturn(List.of(b1));
|
|
|
+
|
|
|
+ List<BatchOrderEntity> result = service.getPendingBatches();
|
|
|
+
|
|
|
+ assertEquals(1, result.size());
|
|
|
+ assertEquals("B1", result.get(0).getOutBatchNo());
|
|
|
+ @SuppressWarnings({ "unchecked", "rawtypes" })
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<BatchOrderEntity>> cap = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(batchOrderMapper).selectList(cap.capture());
|
|
|
+ String sql = cap.getValue().getSqlSegment();
|
|
|
+ assertTrue(sql.contains("NOT IN"), "应排除终态: " + sql);
|
|
|
+ assertTrue(sql.contains("batch_trans_id"), "应限定 batch_trans_id 非空: " + sql);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== I2: batchQuery 明细回写(batchId 条件 + 批量查询) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchQuery_crossBatchSameOutBizNo_updatesOnlyOwnBatch() throws AlipayApiException {
|
|
|
+ // 两个批次含相同 out_biz_no(out_biz_no 仅批内唯一)→ 不得抛 TooManyResultsException,且只回写本批次明细
|
|
|
+ BatchOrderEntity order = new BatchOrderEntity();
|
|
|
+ order.setId(1L);
|
|
|
+ order.setEnterpriseId("E100");
|
|
|
+ order.setOutBatchNo("B1");
|
|
|
+ order.setBatchTransId("BT1");
|
|
|
+ order.setStatus("INIT");
|
|
|
+ when(batchOrderMapper.selectOne(any())).thenReturn(order);
|
|
|
+
|
|
|
+ AccDetailModel m = new AccDetailModel();
|
|
|
+ m.setOutBizNo("D1");
|
|
|
+ m.setStatus("SUCCESS");
|
|
|
+ m.setErrorCode("ACCOUNT_BALANCE_NOT_ENOUGH");
|
|
|
+ m.setErrorMsg("余额不足");
|
|
|
+ AlipayFundBatchDetailQueryResponse resp = new AlipayFundBatchDetailQueryResponse();
|
|
|
+ resp.setBatchStatus("SUCCESS");
|
|
|
+ resp.setAccDetailList(List.of(m));
|
|
|
+ when(alipayClient.certificateExecute(any(AlipayFundBatchDetailQueryRequest.class))).thenReturn(resp);
|
|
|
+
|
|
|
+ // 本批次明细(mapper 按 batchId + out_biz_no 过滤后返回一条;另一批次同号明细不在结果中)
|
|
|
+ BatchDetailEntity own = new BatchDetailEntity();
|
|
|
+ own.setId(10L);
|
|
|
+ own.setBatchId(1L);
|
|
|
+ own.setOutBizNo("D1");
|
|
|
+ own.setStatus("INIT");
|
|
|
+ when(batchDetailMapper.selectList(any())).thenReturn(List.of(own));
|
|
|
+
|
|
|
+ Map<String, Object> result = service.batchQuery("E100", "B1");
|
|
|
+
|
|
|
+ assertEquals("SUCCESS", result.get("status"));
|
|
|
+ assertEquals("SUCCESS", own.getStatus());
|
|
|
+ assertEquals("余额不足", own.getErrorMsg());
|
|
|
+ verify(batchDetailMapper).updateById(own);
|
|
|
+ // N+1 修复: 批量 selectList 而非循环 selectOne
|
|
|
+ verify(batchDetailMapper, never()).selectOne(any());
|
|
|
+ // 查询条件必须带 batchId(跨批次同号明细不串批)
|
|
|
+ @SuppressWarnings({ "unchecked", "rawtypes" })
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<BatchDetailEntity>> cap = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(batchDetailMapper).selectList(cap.capture());
|
|
|
+ assertTrue(cap.getValue().getSqlSegment().contains("batch_id"),
|
|
|
+ "明细回写查询必须带 batchId: " + cap.getValue().getSqlSegment());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== I3: 场景报备必填校验 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchCreateDTO_transferSceneNameBlank_failsValidation() {
|
|
|
+ Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
|
|
+ BatchCreateDTO dto = new BatchCreateDTO();
|
|
|
+ dto.setOrderTitle("t");
|
|
|
+ dto.setPayerUid("2088PAYER");
|
|
|
+ dto.setTransferSceneName("");
|
|
|
+ dto.setTransferSceneReportInfos(List.of(Map.of("info_type", "佣金报酬说明", "info_content", "8月报酬")));
|
|
|
+ dto.setDetails(List.of(detail("D1", "10")));
|
|
|
+
|
|
|
+ Set<ConstraintViolation<BatchCreateDTO>> violations = validator.validate(dto);
|
|
|
+
|
|
|
+ assertTrue(violations.stream().anyMatch(v -> "transferSceneName".equals(v.getPropertyPath().toString())),
|
|
|
+ "transferSceneName 为空应触发 @NotBlank: " + violations);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchCreateDTO_transferSceneReportInfosEmpty_failsValidation() {
|
|
|
+ Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
|
|
+ BatchCreateDTO dto = new BatchCreateDTO();
|
|
|
+ dto.setOrderTitle("t");
|
|
|
+ dto.setPayerUid("2088PAYER");
|
|
|
+ dto.setTransferSceneName("佣金报酬");
|
|
|
+ dto.setTransferSceneReportInfos(List.of());
|
|
|
+ dto.setDetails(List.of(detail("D1", "10")));
|
|
|
+
|
|
|
+ Set<ConstraintViolation<BatchCreateDTO>> violations = validator.validate(dto);
|
|
|
+
|
|
|
+ assertTrue(violations.stream().anyMatch(v -> "transferSceneReportInfos".equals(v.getPropertyPath().toString())),
|
|
|
+ "场景报备为空应触发 @NotEmpty: " + violations);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static BatchCreateDTO.BatchDetailDTO detail(String outBizNo, String amount) {
|
|
|
+ BatchCreateDTO.BatchDetailDTO d = new BatchCreateDTO.BatchDetailDTO();
|
|
|
+ d.setOutBizNo(outBizNo);
|
|
|
+ d.setAmount(new BigDecimal(amount));
|
|
|
+ d.setPayeeIdentity("a@b.com");
|
|
|
+ d.setPayeeIdentityType("ALIPAY_LOGON_ID");
|
|
|
+ d.setPayeeName("张三");
|
|
|
+ return d;
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== I4: 10 位纯日期时间筛选 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchList_pureDateRange_parsesToDayBoundaries() {
|
|
|
+ when(batchOrderMapper.selectPage(any(), any())).thenReturn(new Page<>());
|
|
|
+
|
|
|
+ PageResult<BatchOrderEntity> result = service.batchList("E100", null, "2026-08-01", "2026-08-02", 1, 20);
|
|
|
+
|
|
|
+ assertNotNull(result);
|
|
|
+ verify(batchOrderMapper).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchExport_pureDateRange_parses() {
|
|
|
+ when(batchOrderMapper.selectList(any())).thenReturn(List.of());
|
|
|
+
|
|
|
+ byte[] bytes = service.batchExport("E100", null, "2026-08-01", "2026-08-02");
|
|
|
+
|
|
|
+ assertNotNull(bytes);
|
|
|
+ assertTrue(bytes.length > 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== I5: pay_url 长 HTML body(>1024) ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void renderPay_longHtmlBody_persistsWithoutError() throws AlipayApiException {
|
|
|
+ BatchOrderEntity order = new BatchOrderEntity();
|
|
|
+ order.setEnterpriseId("E100");
|
|
|
+ order.setOutBatchNo("B1");
|
|
|
+ order.setBatchTransId("BT1");
|
|
|
+ order.setStatus("INIT");
|
|
|
+ when(batchOrderMapper.selectOne(any())).thenReturn(order);
|
|
|
+
|
|
|
+ // 真实 pageExecute 返回自动提交 HTML 表单(1.5-4KB),远超原 varchar(1024)
|
|
|
+ String html = "<html><body><form action=\"https://render.alipay.com/paypage\">" + "x".repeat(2000) + "</form></body></html>";
|
|
|
+ AlipayFundTransRenderPayResponse resp = new AlipayFundTransRenderPayResponse();
|
|
|
+ resp.setBody(html);
|
|
|
+ when(alipayClient.pageExecute(any(AlipayFundTransRenderPayRequest.class))).thenReturn(resp);
|
|
|
+
|
|
|
+ Map<String, String> result = service.renderPay("E100", "B1");
|
|
|
+
|
|
|
+ assertEquals(html, result.get("pay_url"));
|
|
|
+ verify(batchOrderMapper).updateById(order);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== M1: 授权并发 DB 兜底 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void authorizeApply_insertDuplicateKey_throwsFriendlyMessage() throws AlipayApiException {
|
|
|
+ AlipayFundAuthorizeUniApplyResponse resp = new AlipayFundAuthorizeUniApplyResponse();
|
|
|
+ resp.setAuthorizeLink("https://ur.alipay.com/abc");
|
|
|
+ when(alipayClient.certificateExecute(any(AlipayFundAuthorizeUniApplyRequest.class))).thenReturn(resp);
|
|
|
+ // 并发双击: 预检通过后 insert 命中 uk_batch_authorize_active 唯一索引
|
|
|
+ doThrow(new DuplicateKeyException("duplicate key")).when(batchAuthorizeMapper).insert(any());
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class,
|
|
|
+ () -> service.authorizeApply("E100", "2088123412341234"));
|
|
|
+
|
|
|
+ assertEquals(400, ex.getCode());
|
|
|
+ assertTrue(ex.getMessage().contains("请勿重复操作"), ex.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== M2: AUTHING 过期重申请 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void authorizeApply_authingNotExpired_throws() {
|
|
|
+ BatchAuthorizeEntity existing = new BatchAuthorizeEntity();
|
|
|
+ existing.setEnterpriseId("E100");
|
|
|
+ existing.setParticipantId("2088123412341234");
|
|
|
+ existing.setStatus("AUTHING");
|
|
|
+ existing.setAuthorizeExpireTime(OffsetDateTime.now().plusHours(1));
|
|
|
+ when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class,
|
|
|
+ () -> service.authorizeApply("E100", "2088123412341234"));
|
|
|
+
|
|
|
+ assertEquals(400, ex.getCode());
|
|
|
+ assertTrue(ex.getMessage().contains("未完成"), ex.getMessage());
|
|
|
+ verify(batchAuthorizeMapper, never()).updateById(any());
|
|
|
+ verify(batchAuthorizeMapper, never()).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void authorizeApply_authingFreshCreatedTime_throws() {
|
|
|
+ // authorize_expire_time 为空时按 created_time + 24h 判定: 刚创建的 AUTHING 未过期 → 拒绝
|
|
|
+ BatchAuthorizeEntity existing = new BatchAuthorizeEntity();
|
|
|
+ existing.setEnterpriseId("E100");
|
|
|
+ existing.setParticipantId("2088123412341234");
|
|
|
+ existing.setStatus("AUTHING");
|
|
|
+ existing.setCreatedTime(OffsetDateTime.now());
|
|
|
+ when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
|
|
|
+
|
|
|
+ assertThrows(BusinessException.class, () -> service.authorizeApply("E100", "2088123412341234"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void authorizeApply_authingExpired_rebindWithNewOutBizNo() throws AlipayApiException {
|
|
|
+ BatchAuthorizeEntity existing = new BatchAuthorizeEntity();
|
|
|
+ existing.setId(1L);
|
|
|
+ existing.setEnterpriseId("E100");
|
|
|
+ existing.setParticipantId("2088123412341234");
|
|
|
+ existing.setOutBizNo("OLD1");
|
|
|
+ existing.setStatus("AUTHING");
|
|
|
+ existing.setAuthorizeExpireTime(OffsetDateTime.now().minusHours(1));
|
|
|
+ when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
|
|
|
+
|
|
|
+ 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", "2088123412341234");
|
|
|
+
|
|
|
+ assertEquals("AUTHING", result.get("status"));
|
|
|
+ // 旧记录作废置 UNBIND(与 uk_batch_authorize_active 的 WHERE status <> 'UNBIND' 协同)
|
|
|
+ verify(batchAuthorizeMapper).updateById(argThat(e -> "UNBIND".equals(e.getStatus())));
|
|
|
+ // 新记录插入 + 新 out_biz_no
|
|
|
+ ArgumentCaptor<BatchAuthorizeEntity> ent = ArgumentCaptor.forClass(BatchAuthorizeEntity.class);
|
|
|
+ verify(batchAuthorizeMapper).insert(ent.capture());
|
|
|
+ assertEquals("AUTHING", ent.getValue().getStatus());
|
|
|
+ assertFalse(ent.getValue().getOutBizNo().equals("OLD1"), "应换新 out_biz_no 重新申请");
|
|
|
+ assertEquals("E100", ent.getValue().getEnterpriseId());
|
|
|
+ // 支付宝侧使用新 out_biz_no
|
|
|
+ ArgumentCaptor<AlipayFundAuthorizeUniApplyRequest> cap = ArgumentCaptor.forClass(AlipayFundAuthorizeUniApplyRequest.class);
|
|
|
+ verify(alipayClient).certificateExecute(cap.capture());
|
|
|
+ AlipayFundAuthorizeUniApplyModel m = (AlipayFundAuthorizeUniApplyModel) cap.getValue().getBizModel();
|
|
|
+ assertEquals(ent.getValue().getOutBizNo(), m.getOutBizNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void authorizeApply_authingExpiredByCreatedTimeFallback_allowsReapply() throws AlipayApiException {
|
|
|
+ // expire 为空 + created_time 超过 24h → 视为过期,允许重新申请
|
|
|
+ BatchAuthorizeEntity existing = new BatchAuthorizeEntity();
|
|
|
+ existing.setId(1L);
|
|
|
+ existing.setEnterpriseId("E100");
|
|
|
+ existing.setParticipantId("2088123412341234");
|
|
|
+ existing.setOutBizNo("OLD1");
|
|
|
+ existing.setStatus("AUTHING");
|
|
|
+ existing.setCreatedTime(OffsetDateTime.now().minusDays(2));
|
|
|
+ when(batchAuthorizeMapper.selectOne(any())).thenReturn(existing);
|
|
|
+
|
|
|
+ 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", "2088123412341234");
|
|
|
+
|
|
|
+ assertNotNull(result.get("authorize_link"));
|
|
|
+ verify(batchAuthorizeMapper).updateById(argThat(e -> "UNBIND".equals(e.getStatus())));
|
|
|
+ verify(batchAuthorizeMapper).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== M3: batchClose INIT 守卫 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void batchClose_nonInitStatus_throwsBusinessException() throws AlipayApiException {
|
|
|
+ BatchOrderEntity order = new BatchOrderEntity();
|
|
|
+ order.setEnterpriseId("E100");
|
|
|
+ order.setOutBatchNo("B1");
|
|
|
+ order.setStatus("SUCCESS");
|
|
|
+ when(batchOrderMapper.selectOne(any())).thenReturn(order);
|
|
|
+
|
|
|
+ BusinessException ex = assertThrows(BusinessException.class, () -> service.batchClose("E100", "B1"));
|
|
|
+
|
|
|
+ assertEquals(400, ex.getCode());
|
|
|
+ assertTrue(ex.getMessage().contains("INIT"), ex.getMessage());
|
|
|
+ verify(alipayClient, never()).certificateExecute(any());
|
|
|
+ }
|
|
|
}
|