|
|
@@ -1,5 +1,9 @@
|
|
|
package com.payment.platform.module.payment.leshouying.service;
|
|
|
|
|
|
+import ch.qos.logback.classic.Level;
|
|
|
+import ch.qos.logback.classic.Logger;
|
|
|
+import ch.qos.logback.classic.spi.ILoggingEvent;
|
|
|
+import ch.qos.logback.core.read.ListAppender;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
|
|
import com.payment.platform.module.payment.leshouying.client.LeshouyingClient;
|
|
|
import com.payment.platform.module.payment.leshouying.client.LeshouyingResponseParser;
|
|
|
@@ -12,8 +16,11 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
import org.mockito.ArgumentCaptor;
|
|
|
import org.mockito.Mock;
|
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
|
+import java.util.List;
|
|
|
import java.util.Set;
|
|
|
+import java.util.function.Consumer;
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
@@ -75,6 +82,62 @@ class LsyOrderServiceStateTest {
|
|
|
"重复通知必须返回 false,调用方据此不重复回调下游");
|
|
|
}
|
|
|
|
|
|
+ @Test
|
|
|
+ void applyPayResult_alarmsOnlyForTheOrderMissingTradeNo() {
|
|
|
+ when(orderMapper.update(any(), any(UpdateWrapper.class))).thenReturn(1);
|
|
|
+ LsyOrderEntity withTradeNo = order("PAYING");
|
|
|
+ withTradeNo.setOutTradeNo("OT_HAS_TRADE_NO");
|
|
|
+ LsyOrderEntity withoutTradeNo = order("PAYING");
|
|
|
+ withoutTradeNo.setId(2L);
|
|
|
+ withoutTradeNo.setOutTradeNo("OT_NO_TRADE_NO");
|
|
|
+
|
|
|
+ // ★ 正负对照放在**同一次捕获**里:第二条断言要成立,第一条必须先在同一个 appender 上
|
|
|
+ // 留下记录。捕获若失效(appender 没挂上、类名写错),得到的是 0 条 → 第一条断言红,
|
|
|
+ // 而不是让「无 trade_no 不响」这个负读数空洞通过。
|
|
|
+ withLogCapture(LsyOrderService.class, events -> {
|
|
|
+ assertTrue(service.applyPayResult(withTradeNo, "SUCCESS", patch("T1")));
|
|
|
+ assertTrue(service.applyPayResult(withoutTradeNo, "SUCCESS", patch(null)));
|
|
|
+
|
|
|
+ List<String> errs = errors(events);
|
|
|
+ assertEquals(1, errs.size(),
|
|
|
+ "SUCCESS 已落库、上游却没给 trade_no = 静默丢字段,必须响**且只响这一单**;"
|
|
|
+ + "实际全部事件=" + messages(events));
|
|
|
+ assertTrue(errs.get(0).contains("OT_NO_TRADE_NO"),
|
|
|
+ "响的必须是缺 trade_no 的那一单(日志要带得走单号,运营才定位得到);"
|
|
|
+ + "实际=" + errs);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void applyPayResult_blankTradeNo_countsAsMissing() {
|
|
|
+ // 上游回 `"trade_no": ""` 时落的是**非 null 空串** —— 同样是「没给我们交易号」,
|
|
|
+ // 而它连运营的捞法(trade_no IS NULL)都捞不到。判据必须用 isBlank 而非 != null。
|
|
|
+ when(orderMapper.update(any(), any(UpdateWrapper.class))).thenReturn(1);
|
|
|
+ LsyOrderEntity o = order("PAYING");
|
|
|
+ o.setOutTradeNo("OT_BLANK_TRADE_NO");
|
|
|
+
|
|
|
+ withLogCapture(LsyOrderService.class, events -> {
|
|
|
+ assertTrue(service.applyPayResult(o, "SUCCESS", patch("")));
|
|
|
+
|
|
|
+ List<String> errs = errors(events);
|
|
|
+ assertEquals(1, errs.size(), "空串与缺字段同义,必须响;实际全部事件=" + messages(events));
|
|
|
+ assertTrue(errs.get(0).contains("OT_BLANK_TRADE_NO"), "实际=" + errs);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void applyPayResult_duplicateNotification_doesNotAlarmAgain() {
|
|
|
+ when(orderMapper.update(any(), any(UpdateWrapper.class))).thenReturn(0); // 条件更新未命中 = 重复
|
|
|
+
|
|
|
+ withLogCapture(LsyOrderService.class, events -> {
|
|
|
+ assertFalse(service.applyPayResult(order("SUCCESS"), "SUCCESS", patch(null)));
|
|
|
+
|
|
|
+ assertEquals(List.of(), errors(events),
|
|
|
+ "同一单被上游重推是常态,重复通知不得再响一次 —— 否则真告警会被淹掉;"
|
|
|
+ + "实际全部事件=" + messages(events));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
@Test
|
|
|
void applyPayResult_onlyAllowsCreatedAndPayingAsSource() {
|
|
|
when(orderMapper.update(any(), any(UpdateWrapper.class))).thenReturn(1);
|
|
|
@@ -167,4 +230,28 @@ class LsyOrderServiceStateTest {
|
|
|
assertFalse(service.isTerminal("PAYING"));
|
|
|
assertFalse(service.isTerminal("PARTIAL_REFUNDED"));
|
|
|
}
|
|
|
+
|
|
|
+ /** 与 LsyNotifyServiceTest 同款(本仓库按测试文件各自持有一份,不抽公共类)。 */
|
|
|
+ private static void withLogCapture(Class<?> type, Consumer<List<ILoggingEvent>> body) {
|
|
|
+ Logger logger = (Logger) LoggerFactory.getLogger(type);
|
|
|
+ ListAppender<ILoggingEvent> appender = new ListAppender<>();
|
|
|
+ appender.start();
|
|
|
+ logger.addAppender(appender);
|
|
|
+ try {
|
|
|
+ body.accept(appender.list);
|
|
|
+ } finally {
|
|
|
+ logger.detachAppender(appender);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 只看 ERROR —— 本类正常路径会打 INFO(如「支付回写未命中」),断言得把级别分开。 */
|
|
|
+ private static List<String> errors(List<ILoggingEvent> events) {
|
|
|
+ return events.stream().filter(e -> e.getLevel() == Level.ERROR)
|
|
|
+ .map(ILoggingEvent::getFormattedMessage).toList();
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 断言失败时把实际捕获到的日志正文全列出来 —— 否则只看到「没匹配上」,看不到实际打了什么。 */
|
|
|
+ private static List<String> messages(List<ILoggingEvent> events) {
|
|
|
+ return events.stream().map(ILoggingEvent::getFormattedMessage).toList();
|
|
|
+ }
|
|
|
}
|