Переглянути джерело

feat(订单实时): 领域事件三件套 —— 提交后转发(无事务兜底)

OrderChangedEvent:通道 + 租户,不带数据(页面拿信号重拉自己的列表接口)。
OrderEventPublisher:写库方唯一入口;三个通道名是前后端线上契约。
OrderEventRelay:@TransactionalEventListener(AFTER_COMMIT, fallbackExecution=true)——
有事务的腿等提交后投递(信号早于提交 = 页面看不到新行),刻意无事务的腿照常立即投递。

测试:OrderEventPublisherTest 3 例 + OrderEventRelayTest 2 例(AFTER_COMMIT 语义由注解反射钉住,
无容器;真事务行为留给 Task 14 的 E2E)。
alphaH 1 тиждень тому
батько
коміт
763e11bd24

+ 13 - 0
java/src/main/java/com/payment/platform/module/payment/stream/OrderChangedEvent.java

@@ -0,0 +1,13 @@
+package com.payment.platform.module.payment.stream;
+
+/**
+ * 订单数据变了(任何写入)。
+ *
+ * <p>只带通道与租户、**不带数据** —— 页面拿信号去重拉它自己的列表接口,
+ * 租户隔离/分页/筛选/权限全部免费复用(见 spec「投递语义」)。
+ *
+ * @param channel  见 {@link OrderEventPublisher} 的三个通道常量
+ * @param tenantId 归属租户;可空
+ */
+public record OrderChangedEvent(String channel, Long tenantId) {
+}

+ 30 - 0
java/src/main/java/com/payment/platform/module/payment/stream/OrderEventPublisher.java

@@ -0,0 +1,30 @@
+package com.payment.platform.module.payment.stream;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+/**
+ * 写库方的唯一入口:**行真的改了之后**调 {@link #changed}(守卫没命中的分支不许发,
+ * 那种分支什么都没变,发了就是让页面白拉一次)。
+ *
+ * <p>通道名是与前端的线上契约(前端按通道名决定重拉哪个列表)——改名 = 断链。
+ * 后端 {@code OrderEventPublisherTest} 与前端 {@code scripts/verify-order-stream.mjs} 各钉了一遍。
+ */
+@Component
+@RequiredArgsConstructor
+public class OrderEventPublisher {
+
+    /** 乐收赢支付订单表 {@code lsy_order}。 */
+    public static final String CHANNEL_LSY_ORDER = "lsy_order";
+    /** 乐收赢退款表 {@code lsy_refund}。 */
+    public static final String CHANNEL_LSY_REFUND = "lsy_refund";
+    /** 汇安融订单表 {@code har_order}。 */
+    public static final String CHANNEL_HAR_ORDER = "har_order";
+
+    private final ApplicationEventPublisher events;
+
+    public void changed(String channel, Long tenantId) {
+        events.publishEvent(new OrderChangedEvent(channel, tenantId));
+    }
+}

+ 30 - 0
java/src/main/java/com/payment/platform/module/payment/stream/OrderEventRelay.java

@@ -0,0 +1,30 @@
+package com.payment.platform.module.payment.stream;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+/**
+ * 事件 → 广播。两件要紧事:
+ *
+ * <ol>
+ *   <li>{@code AFTER_COMMIT}:开放 API 建单那条腿**有事务**,在事务里广播会让页面收到信号
+ *       去重拉、却看不到尚未提交的新行 —— 比不推更糟。</li>
+ *   <li>{@code fallbackExecution = true}:乐收赢的写库腿**刻意不加事务**(单行条件更新),
+ *       没有事务时照常立即投递;不写它那些通道一帧都发不出去,而且是静默的。</li>
+ * </ol>
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class OrderEventRelay {
+
+    private final OrderEventHub hub;
+
+    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true)
+    public void onOrderChanged(OrderChangedEvent event) {
+        hub.submitBroadcast(event.channel(), event.tenantId());
+    }
+}

+ 42 - 0
java/src/test/java/com/payment/platform/module/payment/stream/OrderEventPublisherTest.java

@@ -0,0 +1,42 @@
+package com.payment.platform.module.payment.stream;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class OrderEventPublisherTest {
+
+    private final List<Object> published = new CopyOnWriteArrayList<>();
+    // ApplicationEventPublisher 是 @FunctionalInterface(唯一抽象方法 publishEvent(Object)),
+    // 这里直接拿 List::add 当实现 —— 不需要 Spring 容器
+    private final OrderEventPublisher publisher = new OrderEventPublisher(published::add);
+
+    @Test
+    void changed_publishesOneEventWithChannelAndTenant() {
+        publisher.changed(OrderEventPublisher.CHANNEL_LSY_ORDER, 7L);
+
+        assertEquals(1, published.size());
+        OrderChangedEvent event = (OrderChangedEvent) published.get(0);
+        assertEquals("lsy_order", event.channel());
+        assertEquals(7L, event.tenantId());
+    }
+
+    @Test
+    void channelNames_areTheWireContractWithFrontend() {
+        // 与前端 scripts/verify-order-stream.mjs 里钉的三个串必须逐字一致:改名 = 断链
+        assertEquals("lsy_order", OrderEventPublisher.CHANNEL_LSY_ORDER);
+        assertEquals("lsy_refund", OrderEventPublisher.CHANNEL_LSY_REFUND);
+        assertEquals("har_order", OrderEventPublisher.CHANNEL_HAR_ORDER);
+    }
+
+    @Test
+    void changed_allowsNullTenant() {
+        publisher.changed(OrderEventPublisher.CHANNEL_HAR_ORDER, null);
+
+        assertEquals(1, published.size());
+        assertNull(((OrderChangedEvent) published.get(0)).tenantId());
+    }
+}

+ 53 - 0
java/src/test/java/com/payment/platform/module/payment/stream/OrderEventRelayTest.java

@@ -0,0 +1,53 @@
+package com.payment.platform.module.payment.stream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.transaction.event.TransactionPhase;
+import org.springframework.transaction.event.TransactionalEventListener;
+
+import java.lang.reflect.Method;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class OrderEventRelayTest {
+
+    static class CapturingHub extends OrderEventHub {
+        final List<String> submitted = new CopyOnWriteArrayList<>();
+
+        @Override
+        public void submitBroadcast(String channel, Long tenantId) {
+            submitted.add(channel + "#" + tenantId);
+        }
+    }
+
+    private final CapturingHub hub = new CapturingHub();
+    private final OrderEventRelay relay = new OrderEventRelay(hub);
+
+    @AfterEach
+    void tearDown() {
+        hub.shutdown();
+    }
+
+    @Test
+    void onOrderChanged_delegatesToHub() {
+        relay.onOrderChanged(new OrderChangedEvent("lsy_order", 5L));
+
+        assertEquals(List.of("lsy_order#5"), hub.submitted);
+    }
+
+    @Test
+    void theAfterCommitSemanticsArePinnedInCode() throws Exception {
+        // 容器级测试要拉 Spring 上下文(本仓 81 个测试类一个都没拉过,会撞 leshouying.enabled
+        // 启动闸与库隧道依赖),所以「提交后才投递 + 无事务也投递」这条语义在这里用注解断言钉住;
+        // 真事务行为由 Task 14 的开放 API 建单 E2E 端到端验(信号早于提交 ⇒ 重拉看不到新行 ⇒ 红)。
+        Method m = OrderEventRelay.class.getMethod("onOrderChanged", OrderChangedEvent.class);
+        TransactionalEventListener ann = m.getAnnotation(TransactionalEventListener.class);
+        assertNotNull(ann, "没有这个注解 = 事件永远不被投递");
+        assertEquals(TransactionPhase.AFTER_COMMIT, ann.phase(),
+                "改回 BEFORE_COMMIT 会造出「信号先到、库还没提交」的竞态");
+        assertTrue(ann.fallbackExecution(),
+                "false = 乐收赢那些刻意无事务的写库腿一帧都发不出去(静默失效)");
+    }
+}