|
|
@@ -0,0 +1,572 @@
|
|
|
+package com.payment.platform.module.payment.har.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.payment.platform.common.exception.BusinessException;
|
|
|
+import com.payment.platform.module.payment.har.client.HarApiException;
|
|
|
+import com.payment.platform.module.payment.har.client.HarClient;
|
|
|
+import com.payment.platform.module.payment.har.client.HarResponse;
|
|
|
+import com.payment.platform.module.payment.har.client.dto.HarPayRequest;
|
|
|
+import com.payment.platform.module.payment.har.client.dto.HarPrecreateRequest;
|
|
|
+import com.payment.platform.module.payment.har.client.dto.HarQueryRequest;
|
|
|
+import com.payment.platform.module.payment.har.dto.HarOpenOrderQueryDTO;
|
|
|
+import com.payment.platform.module.payment.har.dto.HarOrderCreateDTO;
|
|
|
+import com.payment.platform.module.payment.har.dto.HarOrderVO;
|
|
|
+import com.payment.platform.module.payment.har.entity.HarMerchantEntity;
|
|
|
+import com.payment.platform.module.payment.har.entity.HarOrderEntity;
|
|
|
+import com.payment.platform.module.payment.har.enums.HarOrderStatus;
|
|
|
+import com.payment.platform.module.payment.har.mapper.HarOrderMapper;
|
|
|
+import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
+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.InOrder;
|
|
|
+import org.mockito.Mock;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
+
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+class HarOrderServiceTest {
|
|
|
+
|
|
|
+ @Mock private HarOrderMapper orderMapper;
|
|
|
+ @Mock private HarMerchantService merchantService;
|
|
|
+ @Mock private HarClient harClient;
|
|
|
+
|
|
|
+ private HarOrderService service;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ service = new HarOrderService(orderMapper, merchantService, harClient);
|
|
|
+ // 初始化 MyBatis-Plus lambda 元数据缓存,使 LambdaQueryWrapper.getSqlSegment() 可在无 Spring 上下文的单测中工作
|
|
|
+ // (否则 getSqlSegment() 抛 MybatisPlusException: can not find lambda cache for this entity。
|
|
|
+ // 同款引导见 LsyCallbackServiceTest / AlipayBatchPayServiceTest。)
|
|
|
+ MybatisConfiguration configuration = new MybatisConfiguration();
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), HarOrderEntity.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ private HarOrderCreateDTO dto() {
|
|
|
+ HarOrderCreateDTO dto = new HarOrderCreateDTO();
|
|
|
+ dto.setEnterpriseId("E1");
|
|
|
+ dto.setOutBizNo("BIZ-0001");
|
|
|
+ dto.setPrice(new BigDecimal("12.34"));
|
|
|
+ dto.setTitle("测试商品");
|
|
|
+ dto.setTradeType("alipay_payment");
|
|
|
+ return dto;
|
|
|
+ }
|
|
|
+
|
|
|
+ private HarMerchantEntity merchant() {
|
|
|
+ HarMerchantEntity m = new HarMerchantEntity();
|
|
|
+ m.setEnterpriseId("E1");
|
|
|
+ m.setMerchantCode("M001");
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ private HarResponse response(String code, String detail) {
|
|
|
+ return new HarResponse(code, "msg", "sign", detail, null);
|
|
|
+ }
|
|
|
+
|
|
|
+ private HarOrderEntity order(String orderNo, String tradeStatus) {
|
|
|
+ HarOrderEntity o = new HarOrderEntity();
|
|
|
+ o.setOrderNo(orderNo);
|
|
|
+ o.setEnterpriseId("E1");
|
|
|
+ o.setTradeStatus(tradeStatus);
|
|
|
+ o.setQueryCount(0);
|
|
|
+ return o;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String precreateDetail() {
|
|
|
+ return "{\"pay_url\":\"https://qr.example/pay\",\"order_no\":\"UP-1\"}";
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 下单 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 本地记录必须先于上游调用落库。
|
|
|
+ *
|
|
|
+ * 顺序反过来的话,上游超时抛异常时本地什么都没有 —— 而那笔单在上游
|
|
|
+ * 可能已经建好了,通知到达时找不到订单只能丢弃,就成了扣了钱没记录的账。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void create_persistsLocalRecordBeforeCallingUpstream() {
|
|
|
+ when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+
|
|
|
+ service.create(dto(), "E1", "KEY1");
|
|
|
+
|
|
|
+ InOrder inOrder = inOrder(orderMapper, harClient);
|
|
|
+ inOrder.verify(orderMapper).insert(any());
|
|
|
+ inOrder.verify(harClient).precreate(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void create_upstreamTimeout_stillPersistedLocalRecord() {
|
|
|
+ when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
+ when(harClient.precreate(any())).thenThrow(new HarApiException("read timeout"));
|
|
|
+
|
|
|
+ assertThrows(HarApiException.class, () -> service.create(dto(), "E1", "KEY1"));
|
|
|
+
|
|
|
+ // 记录已落库才有机会被轮询收敛
|
|
|
+ verify(orderMapper).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * third_order_no 必须是「我方生成的订单号」而不是企业自己的 out_biz_no:
|
|
|
+ * out_biz_no 由企业提供,不同企业的单号可能重复,而上游要求全局唯一。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void create_sendsGeneratedOrderNoAsThirdOrderNo_notOutBizNo() {
|
|
|
+ when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+
|
|
|
+ HarOrderVO vo = service.create(dto(), "E1", "KEY1");
|
|
|
+
|
|
|
+ ArgumentCaptor<HarPrecreateRequest> captor =
|
|
|
+ ArgumentCaptor.forClass(HarPrecreateRequest.class);
|
|
|
+ verify(harClient).precreate(captor.capture());
|
|
|
+ assertEquals(vo.getOrderNo(), captor.getValue().getThirdOrderNo());
|
|
|
+ assertNotEquals("BIZ-0001", captor.getValue().getThirdOrderNo());
|
|
|
+ assertEquals("BIZ-0001", vo.getOutBizNo());
|
|
|
+ assertTrue(vo.getOrderNo().startsWith("HAR"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 下单时排入轮询:上游通知窗口只有约 25 秒,超时后只能靠主动查询收敛 */
|
|
|
+ @Test
|
|
|
+ void create_schedulesFirstPoll() {
|
|
|
+ when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+
|
|
|
+ service.create(dto(), "E1", "KEY1");
|
|
|
+
|
|
|
+ ArgumentCaptor<HarOrderEntity> captor = ArgumentCaptor.forClass(HarOrderEntity.class);
|
|
|
+ verify(orderMapper).insert(captor.capture());
|
|
|
+ assertNotNull(captor.getValue().getNextQueryTime());
|
|
|
+ assertEquals(HarOrderStatus.WAIT_PAY, captor.getValue().getTradeStatus());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void create_storesApiKeyForCallbackRouting() {
|
|
|
+ when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+
|
|
|
+ service.create(dto(), "E1", "KEY1");
|
|
|
+
|
|
|
+ ArgumentCaptor<HarOrderEntity> captor = ArgumentCaptor.forClass(HarOrderEntity.class);
|
|
|
+ verify(orderMapper).insert(captor.capture());
|
|
|
+ assertEquals("KEY1", captor.getValue().getApiKey());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 有付款码即被扫,走 pay 而非 precreate */
|
|
|
+ @Test
|
|
|
+ void create_withAuthCode_usesPayInsteadOfPrecreate() {
|
|
|
+ HarOrderCreateDTO dto = dto();
|
|
|
+ dto.setAuthCode("280812345678901234");
|
|
|
+ when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
+ when(harClient.pay(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+
|
|
|
+ service.create(dto, "E1", "KEY1");
|
|
|
+
|
|
|
+ verify(harClient).pay(any(HarPayRequest.class));
|
|
|
+ verify(harClient, never()).precreate(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void create_upstreamNonSuccessCode_throws() {
|
|
|
+ when(merchantService.requireByEnterpriseId("E1")).thenReturn(merchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("40004", null));
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> service.create(dto(), "E1", "KEY1"));
|
|
|
+
|
|
|
+ assertTrue(e.getMessage().contains("下单失败"));
|
|
|
+ verify(orderMapper).insert(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void create_noMerchantMapping_throwsBeforeTouchingUpstream() {
|
|
|
+ when(merchantService.requireByEnterpriseId("E1"))
|
|
|
+ .thenThrow(new BusinessException("企业尚未配置汇安融商户编号"));
|
|
|
+
|
|
|
+ assertThrows(BusinessException.class, () -> service.create(dto(), "E1", "KEY1"));
|
|
|
+
|
|
|
+ verify(orderMapper, never()).insert(any());
|
|
|
+ verify(harClient, never()).precreate(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 主动查询 ====================
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 优先用上游单号查。
|
|
|
+ *
|
|
|
+ * 曾经写成「传我方 order_no 当 order_no」—— 下单超时的订单本地没有上游单号,
|
|
|
+ * 那样每次查询都是空值,永远查不出结果。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void sync_prefersUpstreamOrderNo() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+ o.setUpstreamOrderNo("UP-1");
|
|
|
+ when(harClient.query(any())).thenReturn(response("10000", "{\"status\":0}"));
|
|
|
+
|
|
|
+ service.syncFromUpstream(o);
|
|
|
+
|
|
|
+ ArgumentCaptor<HarQueryRequest> captor = ArgumentCaptor.forClass(HarQueryRequest.class);
|
|
|
+ verify(harClient).query(captor.capture());
|
|
|
+ assertEquals("UP-1", captor.getValue().getOrderNo());
|
|
|
+ assertNull(captor.getValue().getThirdOrderNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void sync_fallsBackToThirdOrderNo_whenUpstreamOrderNoUnknown() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+ when(harClient.query(any())).thenReturn(response("10000", "{\"status\":0}"));
|
|
|
+
|
|
|
+ service.syncFromUpstream(o);
|
|
|
+
|
|
|
+ ArgumentCaptor<HarQueryRequest> captor = ArgumentCaptor.forClass(HarQueryRequest.class);
|
|
|
+ verify(harClient).query(captor.capture());
|
|
|
+ assertNull(captor.getValue().getOrderNo());
|
|
|
+ assertEquals("HAR1", captor.getValue().getThirdOrderNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void sync_paidStatus_mapsAllFieldsAndStopsPolling() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+ when(harClient.query(any())).thenReturn(response("10000", """
|
|
|
+ {"order_no":"UP-1","platform_trade_no":"PT-1","status":1,"price":"12.34",
|
|
|
+ "rcv_amount":"12.20","fee_amount":"0.14","channel_amount":"12.34",
|
|
|
+ "trade_time":"2026-09-16 10:30:00","create_time":"2026-09-16 10:29:00"}
|
|
|
+ """));
|
|
|
+
|
|
|
+ HarOrderVO vo = service.syncFromUpstream(o);
|
|
|
+
|
|
|
+ assertEquals(HarOrderStatus.PAID, vo.getTradeStatus());
|
|
|
+ assertEquals("UP-1", vo.getUpstreamOrderNo());
|
|
|
+ assertEquals("PT-1", vo.getPlatformTradeNo());
|
|
|
+ assertEquals(new BigDecimal("12.34"), vo.getPrice());
|
|
|
+ assertEquals(new BigDecimal("12.20"), vo.getRcvAmount());
|
|
|
+ assertEquals(new BigDecimal("0.14"), vo.getFeeAmount());
|
|
|
+ assertNotNull(vo.getTradeTime());
|
|
|
+ assertNotNull(vo.getUpstreamCreateTime());
|
|
|
+ // 终态必须清空,否则轮询任务永远捞起已结束的订单
|
|
|
+ assertNull(o.getNextQueryTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void sync_waitingStatus_schedulesNextPoll() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+ when(harClient.query(any())).thenReturn(response("10000", "{\"status\":0}"));
|
|
|
+
|
|
|
+ HarOrderVO vo = service.syncFromUpstream(o);
|
|
|
+
|
|
|
+ assertEquals(HarOrderStatus.WAIT_PAY, vo.getTradeStatus());
|
|
|
+ assertNotNull(o.getNextQueryTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 轮询排期 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void reschedulePoll_finalStatus_clearsNextQueryTime() {
|
|
|
+ for (String status : new String[]{
|
|
|
+ HarOrderStatus.PAID, HarOrderStatus.REFUNDED, HarOrderStatus.CLOSED}) {
|
|
|
+ HarOrderEntity o = order("HAR1", status);
|
|
|
+ o.setNextQueryTime(java.time.OffsetDateTime.now().plusMinutes(5));
|
|
|
+
|
|
|
+ service.reschedulePoll(o);
|
|
|
+
|
|
|
+ assertNull(o.getNextQueryTime(), "终态应停止轮询: " + status);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 退款把订单从终态(1 已支付)拉回非终态(4 退款中)。
|
|
|
+ * 若只做「终态清空」而不给非终态重新排期,next_query_time 会停在
|
|
|
+ * 之前清空的 null 上,轮询按 IS NOT NULL 捞单就再也捞不到它。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void reschedulePoll_refundingAfterPaid_reschedulesNextQueryTime() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.REFUNDING);
|
|
|
+ o.setNextQueryTime(null); // 支付成功时已被清空
|
|
|
+
|
|
|
+ service.reschedulePoll(o);
|
|
|
+
|
|
|
+ assertNotNull(o.getNextQueryTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void reschedulePoll_waiting_schedulesNextQueryTime() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+
|
|
|
+ service.reschedulePoll(o);
|
|
|
+
|
|
|
+ assertNotNull(o.getNextQueryTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 上游将来加了新状态,继续查比静默停掉安全 */
|
|
|
+ @Test
|
|
|
+ void reschedulePoll_unknownStatus_keepsPolling() {
|
|
|
+ HarOrderEntity o = order("HAR1", "99");
|
|
|
+
|
|
|
+ service.reschedulePoll(o);
|
|
|
+
|
|
|
+ assertNotNull(o.getNextQueryTime());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 查询失败也要推进下次查询时间,否则这条记录每轮都被捞起,打爆上游 */
|
|
|
+ @Test
|
|
|
+ void sync_queryFailure_stillAdvancesNextQueryTime() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+ when(harClient.query(any())).thenReturn(response("40004", null));
|
|
|
+
|
|
|
+ service.syncFromUpstream(o);
|
|
|
+
|
|
|
+ assertNotNull(o.getNextQueryTime());
|
|
|
+ assertEquals(1, o.getQueryCount());
|
|
|
+ verify(orderMapper).updateById(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void sync_queryFailure_doesNotThrow() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+ when(harClient.query(any())).thenReturn(response("40004", null));
|
|
|
+
|
|
|
+ assertDoesNotThrow(() -> service.syncFromUpstream(o));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 空串不能被当成 0,否则对账口径失真 */
|
|
|
+ @Test
|
|
|
+ void sync_blankAmounts_stayNull() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.WAIT_PAY);
|
|
|
+ when(harClient.query(any())).thenReturn(response("10000", """
|
|
|
+ {"status":0,"price":"","rcv_amount":"","fee_amount":"","channel_amount":""}
|
|
|
+ """));
|
|
|
+
|
|
|
+ HarOrderVO vo = service.syncFromUpstream(o);
|
|
|
+
|
|
|
+ assertNull(vo.getRcvAmount());
|
|
|
+ assertNull(vo.getFeeAmount());
|
|
|
+ assertNull(vo.getChannelAmount());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 开放 API 查单 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void openQuery_byOrderNo_returnsOrder() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.PAID);
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setEnterpriseId("E1");
|
|
|
+ q.setOrderNo("HAR1");
|
|
|
+
|
|
|
+ HarOrderVO vo = service.openQuery(q);
|
|
|
+
|
|
|
+ assertEquals("HAR1", vo.getOrderNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 同一租户下可能有多个企业,A 企业不能查到 B 企业的订单 */
|
|
|
+ @Test
|
|
|
+ void openQuery_otherEnterpriseOrder_throws() {
|
|
|
+ HarOrderEntity other = order("HAR1", HarOrderStatus.PAID);
|
|
|
+ other.setEnterpriseId("E2");
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(other);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setEnterpriseId("E1");
|
|
|
+ q.setOrderNo("HAR1");
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> service.openQuery(q));
|
|
|
+ // 越权与不存在返回同一句话,不泄露他人订单是否存在
|
|
|
+ assertEquals("订单不存在", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void openQuery_orderNotFound_throws() {
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(null);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setEnterpriseId("E1");
|
|
|
+ q.setOutBizNo("BIZ-1");
|
|
|
+
|
|
|
+ assertThrows(BusinessException.class, () -> service.openQuery(q));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void openQuery_neitherKeyProvided_throws() {
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setEnterpriseId("E1");
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> service.openQuery(q));
|
|
|
+ assertTrue(e.getMessage().contains("至少填一个"));
|
|
|
+ verify(orderMapper, never()).selectOne(any());
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== 租户级下单/查单(去企业化,2026-09-24) ====================
|
|
|
+
|
|
|
+ private HarMerchantEntity tenantMerchant() {
|
|
|
+ HarMerchantEntity m = new HarMerchantEntity();
|
|
|
+ m.setTenantId(7L);
|
|
|
+ m.setEnterpriseId(null);
|
|
|
+ m.setMerchantCode("M700");
|
|
|
+ return m;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 企业两处都没有 ⇒ 按**租户**寻址。分流写反的话会拿 null 去查企业映射,
|
|
|
+ * 直接抛「企业尚未配置」—— 而调用方明明是一个配好了租户级映射的租户。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void create_noEnterpriseAnywhere_usesTenantMapping() {
|
|
|
+ when(merchantService.requireByTenant(7L)).thenReturn(tenantMerchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+ HarOrderCreateDTO dto = dto();
|
|
|
+ dto.setEnterpriseId(null);
|
|
|
+
|
|
|
+ HarOrderVO vo = HarTestAuth.asTenant(7L, () -> service.create(dto, null, "KEY1"));
|
|
|
+
|
|
|
+ verify(merchantService).requireByTenant(7L);
|
|
|
+ verify(merchantService, never()).requireByEnterpriseId(any());
|
|
|
+ assertEquals("M700", vo.getMerchantCode());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 租户级订单的 enterprise_id 必须落 NULL:openQuery 里那条「不传企业即按租户」正是靠它 */
|
|
|
+ @Test
|
|
|
+ void create_tenantLevel_storesNullEnterpriseId() {
|
|
|
+ when(merchantService.requireByTenant(7L)).thenReturn(tenantMerchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+ HarOrderCreateDTO dto = dto();
|
|
|
+ dto.setEnterpriseId(""); // 前端清空下拉框给的是空串
|
|
|
+
|
|
|
+ HarTestAuth.asTenant(7L, () -> service.create(dto, null, "KEY1"));
|
|
|
+
|
|
|
+ ArgumentCaptor<HarOrderEntity> captor = ArgumentCaptor.forClass(HarOrderEntity.class);
|
|
|
+ verify(orderMapper).insert(captor.capture());
|
|
|
+ assertNull(captor.getValue().getEnterpriseId(), "空串必须落 NULL");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 显式 merchant_code **优先于** enterprise_id(2026-09-24 一租户多商户)。
|
|
|
+ *
|
|
|
+ * dto 同时带着 E1 与商户号,两条分支都能走通 —— 只有这样才验得出优先级。
|
|
|
+ * 若两个 if 调换,企业级那条先命中,本断言即红:那时「钱进哪个商户账号」
|
|
|
+ * 由企业号决定,调用方指定的那张码形同虚设。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void create_withExplicitMerchantCode_takesPriorityOverEnterprise() {
|
|
|
+ when(merchantService.requireByTenantAndCode(7L, "M700")).thenReturn(tenantMerchant());
|
|
|
+ when(harClient.precreate(any())).thenReturn(response("10000", precreateDetail()));
|
|
|
+ HarOrderCreateDTO dto = dto(); // 带 enterpriseId = "E1"
|
|
|
+ dto.setMerchantCode("M700");
|
|
|
+
|
|
|
+ HarOrderVO vo = HarTestAuth.asTenant(7L, () -> service.create(dto, "E1", "KEY1"));
|
|
|
+
|
|
|
+ verify(merchantService).requireByTenantAndCode(7L, "M700");
|
|
|
+ verify(merchantService, never()).requireByEnterpriseId(any());
|
|
|
+ verify(merchantService, never()).requireByTenant(any());
|
|
|
+ assertEquals("M700", vo.getMerchantCode());
|
|
|
+ ArgumentCaptor<HarPrecreateRequest> req =
|
|
|
+ ArgumentCaptor.forClass(HarPrecreateRequest.class);
|
|
|
+ verify(harClient).precreate(req.capture());
|
|
|
+ assertEquals("M700", req.getValue().getMerchantCode(), "发给上游的必须是调用方指定的那个");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 缺企业号的查单必须**不加**企业谓词,而不是加一句恒假的 {@code = NULL}。
|
|
|
+ *
|
|
|
+ * 只能在 SQL 结构上验:mock 的 mapper 不执行 wrapper,两种写法返回同一个值。
|
|
|
+ * 而现实中 {@code = NULL} 恒为 UNKNOWN,租户级订单**永远查不到**。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void getByOutBizNo_blankEnterprise_hasNoEnterprisePredicate() {
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(null);
|
|
|
+
|
|
|
+ service.getByOutBizNo(null, "BIZ-0001");
|
|
|
+
|
|
|
+ @SuppressWarnings({ "unchecked", "rawtypes" })
|
|
|
+ ArgumentCaptor<LambdaQueryWrapper<HarOrderEntity>> cap =
|
|
|
+ ArgumentCaptor.forClass(LambdaQueryWrapper.class);
|
|
|
+ verify(orderMapper).selectOne(cap.capture());
|
|
|
+ String where = cap.getValue().getSqlSegment();
|
|
|
+ assertTrue(where.contains("out_biz_no"), "实测 WHERE: " + where);
|
|
|
+ assertFalse(where.contains("enterprise_id"), "不传企业就不该有企业谓词,实测 WHERE: " + where);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 不传 enterprise_id ⇒ 按租户范围查单,同一租户下任何一个企业的订单都算命中。
|
|
|
+ *
|
|
|
+ * 这条是行为级的:比对那一步若写成无条件执行,租户级调用方拿着 null
|
|
|
+ * 去比 enterpriseId,任何订单都会变成「订单不存在」。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void openQuery_blankEnterprise_acceptsAnyOrderOfTheTenant() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.PAID); // 属于企业 E1
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setOutBizNo("BIZ-0001"); // 不传企业
|
|
|
+
|
|
|
+ HarOrderVO vo = service.openQuery(q);
|
|
|
+
|
|
|
+ assertEquals("HAR1", vo.getOrderNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 传了企业就仍要比对 —— 去企业化不能把同租户内的企业隔离一起取消掉 */
|
|
|
+ @Test
|
|
|
+ void openQuery_withEnterprise_stillRejectsOtherEnterprisesOrder() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.PAID);
|
|
|
+ o.setEnterpriseId("E2");
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setEnterpriseId("E1");
|
|
|
+ q.setOutBizNo("BIZ-0001");
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> service.openQuery(q));
|
|
|
+
|
|
|
+ assertEquals("订单不存在", e.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 指定了商户就只认这个商户的订单。
|
|
|
+ *
|
|
|
+ * 少了这一步,多商户的租户按 order_no 查到的可能是**另一个商户**的单,
|
|
|
+ * 而返回体和请求对不上时调用方无从察觉(两个商户都在他名下,谁看都「正常」)。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void openQuery_withMerchantCode_rejectsOrderOfAnotherMerchant() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.PAID);
|
|
|
+ o.setMerchantCode("M701");
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setMerchantCode("M700");
|
|
|
+ q.setOutBizNo("BIZ-0001");
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> service.openQuery(q));
|
|
|
+
|
|
|
+ assertEquals("订单不存在", e.getMessage(), "与越权同一个说法,不区分");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void openQuery_withMerchantCode_acceptsMatchingOrder() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.PAID);
|
|
|
+ o.setMerchantCode("M700");
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setMerchantCode("M700");
|
|
|
+ q.setOutBizNo("BIZ-0001");
|
|
|
+
|
|
|
+ assertEquals("HAR1", service.openQuery(q).getOrderNo());
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 不传商户就不比对 —— 否则单商户租户(绝大多数)会一笔都查不到 */
|
|
|
+ @Test
|
|
|
+ void openQuery_blankMerchantCode_acceptsAnyMerchantOfTheTenant() {
|
|
|
+ HarOrderEntity o = order("HAR1", HarOrderStatus.PAID);
|
|
|
+ o.setMerchantCode("M701");
|
|
|
+ when(orderMapper.selectOne(any())).thenReturn(o);
|
|
|
+ HarOpenOrderQueryDTO q = new HarOpenOrderQueryDTO();
|
|
|
+ q.setOutBizNo("BIZ-0001");
|
|
|
+
|
|
|
+ assertEquals("HAR1", service.openQuery(q).getOrderNo());
|
|
|
+ }
|
|
|
+}
|