|
|
@@ -0,0 +1,180 @@
|
|
|
+package com.payment.platform.module.payment.account.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.payment.platform.core.alipay.AlipayClientFactory;
|
|
|
+import com.payment.platform.module.payment.account.entity.TransferEntity;
|
|
|
+import com.payment.platform.module.payment.account.mapper.AccountMapper;
|
|
|
+import com.payment.platform.module.payment.account.mapper.DepositMapper;
|
|
|
+import com.payment.platform.module.payment.account.mapper.TransferMapper;
|
|
|
+import com.payment.platform.module.payment.account.mapper.WithdrawMapper;
|
|
|
+import com.payment.platform.module.payment.facetoface.mapper.F2fTradeRecordMapper;
|
|
|
+import com.payment.platform.module.payment.leshouying.enums.LsyOrderStatus;
|
|
|
+import com.payment.platform.module.payment.leshouying.mapper.LsyOrderMapper;
|
|
|
+import com.payment.platform.module.payment.notification.mapper.PayBillMapper;
|
|
|
+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.Mock;
|
|
|
+import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.time.OffsetDateTime;
|
|
|
+import java.time.ZoneOffset;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.Set;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
+import static org.mockito.ArgumentMatchers.isNull;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.verifyNoInteractions;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 首页「支付宝收款」这张卡的数从哪来 —— 乐收赢聚合腿(用户 2026-10-05 裁定:不再统计当面付)。
|
|
|
+ *
|
|
|
+ * <p>钉三件事:
|
|
|
+ * <ol>
|
|
|
+ * <li><b>分转元</b>:{@code pay_lsy_order} 的金额列是分(BIGINT),而首页四块统计对外
|
|
|
+ * 都是元字符串。少这一步,卡片上的数会比真实金额大 100 倍;</li>
|
|
|
+ * <li><b>状态集合与后端同一来源</b>:{@code LsyOrderStatus.PAY_RECORDED}
|
|
|
+ * (SUCCESS / PARTIAL_REFUNDED / REFUNDED)从 Java 传进 SQL,不在注解里手抄 ——
|
|
|
+ * 本仓既有的「同一集合」裁定(退款在途标记那一条)。</li>
|
|
|
+ * <li><b>汇总统计的第三段换成乐收赢</b>,且当面付那条腿一次都不再被碰
|
|
|
+ * ({@code verifyNoInteractions}:不是「结果恰好对」,是「根本没走那条腿」)。</li>
|
|
|
+ * </ol>
|
|
|
+ *
|
|
|
+ * <p>SQL 本身的形状(净额公式逐窗口、finish_time 窗口列、占位符状态集合)由
|
|
|
+ * {@code LsyOrderMapperSqlTest} 用 MyBatis 真求值来钉 —— 这里 mapper 是 mock,
|
|
|
+ * 注解 SQL 在这个测试里**完全不可见**。
|
|
|
+ */
|
|
|
+@ExtendWith(MockitoExtension.class)
|
|
|
+class AccountServiceStatTest {
|
|
|
+
|
|
|
+ @Mock private AccountMapper accountMapper;
|
|
|
+ @Mock private TransferMapper transferMapper;
|
|
|
+ @Mock private DepositMapper depositMapper;
|
|
|
+ @Mock private WithdrawMapper withdrawMapper;
|
|
|
+ @Mock private AlipayClientFactory alipayClientFactory;
|
|
|
+ @Mock private RedisTemplate<String, Object> redisTemplate;
|
|
|
+ @Mock private PayBillMapper payBillMapper;
|
|
|
+ @Mock private F2fTradeRecordMapper f2fTradeRecordMapper;
|
|
|
+ @Mock private LsyOrderMapper lsyOrderMapper;
|
|
|
+ @Mock private AlipayTransferService alipayTransferService;
|
|
|
+ @Mock private ReceiptFileFetcher receiptFileFetcher;
|
|
|
+
|
|
|
+ private AccountService service;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() {
|
|
|
+ service = new AccountService(accountMapper, transferMapper, depositMapper, withdrawMapper,
|
|
|
+ alipayClientFactory, redisTemplate, payBillMapper, f2fTradeRecordMapper,
|
|
|
+ lsyOrderMapper, alipayTransferService, receiptFileFetcher);
|
|
|
+ // 初始化 MyBatis-Plus lambda 元数据缓存,使 LambdaQueryWrapper 可在无 Spring 上下文的单测里构造
|
|
|
+ MybatisConfiguration configuration = new MybatisConfiguration();
|
|
|
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), TransferEntity.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 库里是分,卡片上是元:12345 分 = 123.45 元;整百分位保留两位(100000 分 = 1000.00 元)。 */
|
|
|
+ @Test
|
|
|
+ void 乐收赢统计_分转元并保留两位() {
|
|
|
+ when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
|
|
|
+ .thenReturn(Map.of(
|
|
|
+ "amount_of_today", new BigDecimal("12345"),
|
|
|
+ "amount_of_7days", new BigDecimal("100000"),
|
|
|
+ "amount_of_all", new BigDecimal("0")));
|
|
|
+
|
|
|
+ Map<String, String> r = service.statLsyCollectedAmount(null, null);
|
|
|
+
|
|
|
+ assertEquals("123.45", r.get("amount_of_today"));
|
|
|
+ assertEquals("1000.00", r.get("amount_of_7days"));
|
|
|
+ assertEquals("0.00", r.get("amount_of_all"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 空表 / 键缺失时是 0.00 而不是 null 或异常 —— 首页任何一块统计都不该让整页报错。 */
|
|
|
+ @Test
|
|
|
+ void 乐收赢统计_键缺失时按零() {
|
|
|
+ when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
|
|
|
+ .thenReturn(Map.of("amount_of_all", new BigDecimal("250")));
|
|
|
+
|
|
|
+ Map<String, String> r = service.statLsyCollectedAmount(null, null);
|
|
|
+
|
|
|
+ assertEquals("0.00", r.get("amount_of_today"));
|
|
|
+ assertEquals("0.00", r.get("amount_of_7days"));
|
|
|
+ assertEquals("2.50", r.get("amount_of_all"));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 传进 SQL 的状态集合必须**就是** {@code LsyOrderStatus.PAY_RECORDED}(同一个集合对象,
|
|
|
+ * 不是等价的字面量)—— 将来枚举加/删成员,这里跟着一起动,不会两边各写一份。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void 乐收赢统计_状态集合取自PAY_RECORDED() {
|
|
|
+ when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
|
|
|
+ .thenReturn(Map.of("amount_of_today", BigDecimal.ZERO,
|
|
|
+ "amount_of_7days", BigDecimal.ZERO, "amount_of_all", BigDecimal.ZERO));
|
|
|
+
|
|
|
+ service.statLsyCollectedAmount(null, null);
|
|
|
+
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ ArgumentCaptor<Collection<String>> captor = ArgumentCaptor.forClass(Collection.class);
|
|
|
+ verify(lsyOrderMapper).statCollectedAmount(isNull(), isNull(), captor.capture(), any(), any());
|
|
|
+ assertEquals(Set.copyOf(LsyOrderStatus.PAY_RECORDED), Set.copyOf(captor.getValue()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 时间窗:北京时间当日零点起的「今日」,近 7 日 = 今日减 7 天(与另外三块统计同一口径)。 */
|
|
|
+ @Test
|
|
|
+ void 乐收赢统计_时间窗是北京时间当日零点() {
|
|
|
+ when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any()))
|
|
|
+ .thenReturn(Map.of("amount_of_today", BigDecimal.ZERO,
|
|
|
+ "amount_of_7days", BigDecimal.ZERO, "amount_of_all", BigDecimal.ZERO));
|
|
|
+
|
|
|
+ service.statLsyCollectedAmount(null, null);
|
|
|
+
|
|
|
+ ArgumentCaptor<OffsetDateTime> today = ArgumentCaptor.forClass(OffsetDateTime.class);
|
|
|
+ ArgumentCaptor<OffsetDateTime> weekAgo = ArgumentCaptor.forClass(OffsetDateTime.class);
|
|
|
+ verify(lsyOrderMapper).statCollectedAmount(isNull(), isNull(), any(),
|
|
|
+ today.capture(), weekAgo.capture());
|
|
|
+ assertEquals(ZoneOffset.ofHours(8), today.getValue().getOffset());
|
|
|
+ assertEquals(0, today.getValue().getHour());
|
|
|
+ assertEquals(0, today.getValue().getMinute());
|
|
|
+ assertEquals(0, today.getValue().getSecond());
|
|
|
+ assertEquals(today.getValue().minusDays(7), weekAgo.getValue());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 汇总 = 转账 + 消费 + 乐收赢(净额,分已转元)。
|
|
|
+ *
|
|
|
+ * <p>数字是挑过的:乐收赢那 2500 分若不转元,今日会算出 2650.00 而不是 175.00 ——
|
|
|
+ * 这条断言顺带把「汇总里漏了换算」也钉住。7 日/全部同理(3000 分 / 4000 分)。
|
|
|
+ * <p>{@code verifyNoInteractions(f2fTradeRecordMapper)}:当面付那一段是**不再走**,
|
|
|
+ * 不是「走出来的结果恰好为 0」。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void 汇总统计_当面付那段换成乐收赢且分已转元() {
|
|
|
+ when(transferMapper.statAmounts(any(), any(), any())).thenReturn(Map.of(
|
|
|
+ "amount_of_today", new BigDecimal("100"),
|
|
|
+ "amount_of_7days", new BigDecimal("200"),
|
|
|
+ "amount_of_all", new BigDecimal("300")));
|
|
|
+ when(payBillMapper.statConsumeAmount(any(), any(), any(), any(), any())).thenReturn(Map.of(
|
|
|
+ "amount_of_today", new BigDecimal("50"),
|
|
|
+ "amount_of_7days", new BigDecimal("60"),
|
|
|
+ "amount_of_all", new BigDecimal("70")));
|
|
|
+ when(lsyOrderMapper.statCollectedAmount(any(), any(), any(), any(), any())).thenReturn(Map.of(
|
|
|
+ "amount_of_today", new BigDecimal("2500"),
|
|
|
+ "amount_of_7days", new BigDecimal("3000"),
|
|
|
+ "amount_of_all", new BigDecimal("4000")));
|
|
|
+
|
|
|
+ Map<String, String> r = service.statSummaryAmount(null, null, null);
|
|
|
+
|
|
|
+ assertEquals("175.00", r.get("amount_of_today")); // 100 + 50 + 25.00
|
|
|
+ assertEquals("290.00", r.get("amount_of_7days")); // 200 + 60 + 30.00
|
|
|
+ assertEquals("410.00", r.get("amount_of_all")); // 300 + 70 + 40.00
|
|
|
+ verifyNoInteractions(f2fTradeRecordMapper);
|
|
|
+ }
|
|
|
+}
|