Bladeren bron

feat(订单实时): lsy 退款表四处写点发布信号

alphaH 1 week geleden
bovenliggende
commit
5ff9bfbab8

+ 27 - 0
java/src/main/java/com/payment/platform/module/payment/leshouying/service/LsyRefundService.java

@@ -14,6 +14,7 @@ import com.payment.platform.module.payment.leshouying.enums.LsyRefundStatus;
 import com.payment.platform.module.payment.leshouying.mapper.LsyOrderMapper;
 import com.payment.platform.module.payment.leshouying.mapper.LsyRefundMapper;
 import com.payment.platform.module.payment.leshouying.util.LsyLogSanitizer;
+import com.payment.platform.module.payment.stream.OrderEventPublisher;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -74,6 +75,20 @@ public class LsyRefundService {
      * 两处不会各改各的。
      */
     private final LeshouyingProperties props;
+    /**
+     * 退款信号发布(订单实时推送)。
+     *
+     * <p>【发布纪律】只在**真的写了行**之后发 —— 守卫没命中的分支({@link #applyNotifyResult}
+     * 命中 0 行、{@link #persistRefundSettle} 的 {@code settle == null} 早退)一个字节都没改,
+     * 发了就是让退款页白拉一次。
+     *
+     * <p>【挂在哪几处,为什么不止一处】退款页要看的那几格分别由**不同写点**改,
+     * 所以各挂一条:{@link #createRefund} 的预插入(新行出现)与 {@link #persistUpstreamIdentity}
+     * (refund_no 等租户可见列补上)、{@link #persistRefundSettle}(实退金额补上)、
+     * {@link #applyNotifyResult}(状态流转)。只挂最后那个「唯一状态入口」的话,
+     * 前三类发生时退款页什么都不动。清册以本文件里的发布调用点为准(按方法名找,别按行号)。
+     */
+    private final OrderEventPublisher publisher;
 
     /**
      * 申请退款。支持部分退款。
@@ -233,6 +248,9 @@ public class LsyRefundService {
         entity.setNextQueryTime(OffsetDateTime.now()
                 .plusSeconds(props.getReconcileDelaySeconds()));
         refundMapper.insert(entity);
+        // ① 新行出现 = 退款页上多一条记录。这一行**已经落库**,所以必须发 ——
+        //    下面那次上游调用即使超时抛异常(本方法就此终止),页面也该立刻看得到它。
+        publisher.changed(OrderEventPublisher.CHANNEL_LSY_REFUND, entity.getTenantId());
 
         Map<String, Object> resp;
         try {
@@ -556,6 +574,9 @@ public class LsyRefundService {
         UpdateWrapper<LsyRefundEntity> w = new UpdateWrapper<>();
         w.eq("id", entity.getId());
         refundMapper.update(patch, w);
+        // ② 补上的是租户可见的三列(refund_no / qd_refund_no / refund_settle),
+        //    退款页直接展示它们 ⇒ 写成功就发。这一句不判分:条件只看 id,写不中只能是并发删行。
+        publisher.changed(OrderEventPublisher.CHANNEL_LSY_REFUND, entity.getTenantId());
     }
 
     /**
@@ -592,6 +613,9 @@ public class LsyRefundService {
         UpdateWrapper<LsyRefundEntity> w = new UpdateWrapper<>();
         w.eq("id", refund.getId());
         refundMapper.update(patch, w);
+        // ③ 实退金额(用户报障的那个「实退(元)」列)落库 ⇒ 退款页那一格要变。
+        //    ★ 发布行在**早退之后**:settle == null 时上面已经 return,那一支一个字节都没写。
+        publisher.changed(OrderEventPublisher.CHANNEL_LSY_REFUND, refund.getTenantId());
     }
 
     /**
@@ -665,6 +689,9 @@ public class LsyRefundService {
         refund.setStatus(newStatus);
         refund.setUpstreamResultCode(resultCode);
         refund.setUpstreamResultMsg(resultMsg);
+        // ④ 状态真的流转了(上面守卫命中 1 行)⇒ 退款页那一行的状态/结果列要变。
+        //    ★ 必须挂在这个 if 之后:命中 0 行是重复回调,一个字节都没改(负例见测试同名用例)。
+        publisher.changed(OrderEventPublisher.CHANNEL_LSY_REFUND, refund.getTenantId());
         pushRefundCallbackIfTerminal(refund);
         return true;
     }

+ 37 - 1
java/src/test/java/com/payment/platform/module/payment/leshouying/service/LsyRefundServiceTest.java

@@ -8,6 +8,7 @@ import com.payment.platform.module.payment.leshouying.entity.LsyOrderEntity;
 import com.payment.platform.module.payment.leshouying.entity.LsyRefundEntity;
 import com.payment.platform.module.payment.leshouying.mapper.LsyOrderMapper;
 import com.payment.platform.module.payment.leshouying.mapper.LsyRefundMapper;
+import com.payment.platform.module.payment.stream.OrderEventPublisher;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -26,11 +27,18 @@ import static org.mockito.Mockito.*;
 @ExtendWith(MockitoExtension.class)
 class LsyRefundServiceTest {
 
+    /**
+     * 夹具租户 —— 订单/退款两个夹具都挂它,好让「发布用的是实体自己的租户」这条断言
+     * 有**非 null 且唯一**的值可比:都留 null 的话,一个恒发 {@code null} 的实现也会绿。
+     */
+    private static final Long TENANT = 7L;
+
     @Mock private LeshouyingClient client;
     @Mock private LsyRefundMapper refundMapper;
     @Mock private LsyOrderMapper orderMapper;
     @Mock private LsyOrderService orderService;
     @Mock private LsyCallbackService callbackService;
+    @Mock private OrderEventPublisher publisher;
     private LsyRefundService service;
     /** 只为对账游标的初值(`reconcileDelaySeconds`)而需要;用默认值即可。 */
     private final LeshouyingProperties props = new LeshouyingProperties();
@@ -38,12 +46,13 @@ class LsyRefundServiceTest {
     @BeforeEach
     void setUp() {
         service = new LsyRefundService(client, new LeshouyingResponseParser(), refundMapper,
-                orderMapper, orderService, callbackService, props);
+                orderMapper, orderService, callbackService, props, publisher);
     }
 
     private LsyOrderEntity order() {
         LsyOrderEntity o = new LsyOrderEntity();
         o.setId(1L);
+        o.setTenantId(TENANT);
         o.setOutTradeNo("OUT001");
         o.setMchNo("Y52211090004");
         o.setPayTraceNo("PT001");
@@ -261,6 +270,12 @@ class LsyRefundServiceTest {
         org.mockito.InOrder io = inOrder(refundMapper, client);
         io.verify(refundMapper).insert(any());
         io.verify(client).post(eq("/v3/refundorder"), anyMap());
+
+        // 订单实时信号:本路径上 pay_lsy_refund **真的写了 4 次**,所以恰好 4 条信号 ——
+        //   预插入(insert) ①、补身份(persistUpstreamIdentity) ②、CREATED→REFUNDING ④、
+        //   REFUNDING→SUCCESS ④。用精确次数而不是 atLeastOnce:后者在「后面的写点会补发」
+        //   之下**恒真**,删掉任何一条发布行它都不会红(这正是本任务要钉的东西)。
+        verify(publisher, times(4)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
     }
 
     @Test
@@ -337,6 +352,10 @@ class LsyRefundServiceTest {
         // 状态推进也必须真的发生(CREATED → REFUNDING)。
         assertTrue(captor.getAllValues().stream().anyMatch(x -> "REFUNDING".equals(x.getStatus())),
                 "收到响应 = 上游已受理 ⇒ 必须离开 CREATED");
+
+        // 订单实时信号:与上一条同路(同步成功),写点同样是 4 个 ⇒ 4 条信号,
+        //   其中**补身份那一次**(persistUpstreamIdentity)是本用例自己钉的那个写点。
+        verify(publisher, times(4)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
     }
 
     @Test
@@ -779,6 +798,11 @@ class LsyRefundServiceTest {
         assertTrue(captor.getAllValues().stream()
                         .anyMatch(x -> Long.valueOf(97L).equals(x.getRefundSettle())),
                 "退款查询回传的实退金额必须落库 —— 它是 refund_settle 的唯一来源");
+
+        // 订单实时信号:本路径两个写点 —— 实退补写(persistRefundSettle) ③、状态收敛 ④
+        //   ⇒ 恰好 2 条。写成 2 而不是 1,是因为①这个数就钉住了「实退那条也发了」:
+        //   删掉 persistRefundSettle 的发布行 → 只剩 1 条 → 红。
+        verify(publisher, times(2)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
     }
 
     /**
@@ -805,6 +829,11 @@ class LsyRefundServiceTest {
         verify(refundMapper, atLeastOnce()).update(captor.capture(), any());
         assertTrue(captor.getAllValues().stream().allMatch(x -> x.getRefundSettle() == null),
                 "响应里没有这个字段时不得编一个数字 —— 只能留空等下一轮");
+
+        // 订单实时信号(负例的正控):实退缺席 ⇒ persistRefundSettle 早退、**一个字节都没写**
+        //   ⇒ 本路径只剩状态收敛那一条信号。写成 1 而不是 never():状态那一次写是真的,
+        //   它该发。若有人把发布行挪到早退分支之上(或挪到 settle==null 之前),这里会是 2 → 红。
+        verify(publisher, times(1)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
     }
 
     /**
@@ -862,6 +891,9 @@ class LsyRefundServiceTest {
 
         assertTrue(service.applyNotifyResult(refund, "TRADE_REFUND", "成功", "SUCCESS"));
         assertEquals("SUCCESS", refund.getStatus());
+        // 订单实时信号:真的流转了(受影响行数 = 1)⇒ 恰好一条。本方法只此一个写点,
+        //   所以这里的 times(1) 也就是「命中一行才发」的最小正例。
+        verify(publisher, times(1)).changed(OrderEventPublisher.CHANNEL_LSY_REFUND, TENANT);
     }
 
     @Test
@@ -908,6 +940,9 @@ class LsyRefundServiceTest {
 
         assertFalse(service.applyNotifyResult(refund, "TRADE_REFUND", "成功", "SUCCESS"),
                 "重复回调必须返回 false,否则订单侧会重复累加 refunded_amount");
+        // 订单实时信号(负例):命中 0 行 = 一个字节都没改。发了就是让退款页白拉一次,
+        //   而且是在「什么都没变」的时候 —— 信号必须挂在守卫**之后**。
+        verifyNoInteractions(publisher);
     }
 
     /**
@@ -1005,6 +1040,7 @@ class LsyRefundServiceTest {
     private LsyRefundEntity refund(String status) {
         LsyRefundEntity r = new LsyRefundEntity();
         r.setId(9L);
+        r.setTenantId(TENANT);
         r.setOutRefundNo("RF001");
         r.setRefundFee(30L);
         r.setStatus(status);