Quellcode durchsuchen

test(订单实时): 补 OrderEventRelay 事务边界的可执行覆盖 + 订正假前提

复核 Important #3:Task 14 Step 5 的「信号不早于提交」实证用的是收银台腿,而那条腿在
controller 里裸 orderMapper.insert(LeshouyingCashierController:183-184,无事务)。
2026-10-05 实测:全仓 25 个 publisher.changed 发布点里只有
HarOrderService.syncFromUpstream(@Transactional:499、发布:550)在事务里 ⇒
AFTER_COMMIT 延迟分支此前只有注解反射断言,从未被执行过。

- 新增 OrderEventRelayTransactionBoundaryTest:真容器 + 真多播器 +
  TransactionalEventListenerFactory,三条断言 —— 事务内推迟到
  afterCompletion(STATUS_COMMITTED)、无事务立即投递、回滚不投递。
  首跑 2/3 红暴露了量具缺陷(裸 AnnotationConfigApplicationContext 只带
  DefaultEventListenerFactory,注解会退化成普通监听器),补上工厂后 3/3 绿。
  变异验证:phase→BEFORE_COMMIT / fallbackExecution=false / 换成 @EventListener
  各自只打红对应的一条(t14-fix3-m1..m3),还原后复绿。
- PaymentApplicationContextTest 补一条容器级断言:真上下文确实装配了事务型适配器
  (此前全靠推断;实测确有 —— TransactionAutoConfiguration 链生效)。
- OrderEventRelay javadoc 订正假陈述「开放 API 建单那条腿有事务」(行数 30→30 不变,
  class 行表不动;git diff 仅 3 行注释)。
- task-14-brief.md:77 的同一假前提同步订正(工作区台账)。

全量 1082/1082(修复前 1078;+3 新用例 +1 容器断言)。
alphaH vor 1 Woche
Ursprung
Commit
d12c33fdae

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

@@ -10,9 +10,9 @@ import org.springframework.transaction.event.TransactionalEventListener;
  * 事件 → 广播。两件要紧事:
  *
  * <ol>
- *   <li>{@code AFTER_COMMIT}:开放 API 建单那条腿**有事务**,在事务里广播会让页面收到信号
- *       去重拉、却看不到尚未提交的新行 —— 比不推更糟。</li>
- *   <li>{@code fallbackExecution = true}:乐收赢的写库腿**刻意不加事务**(单行条件更新),
+ *   <li>{@code AFTER_COMMIT}:真事务里发布必须等提交,否则页面收到信号重拉却看不到未提交的新行;
+ *       2026-10-05 实测:25 个发布点里只有 HarOrderService.syncFromUpstream(:499 事务、:550 发布)。</li>
+ *   <li>{@code fallbackExecution = true}:其余 24 条腿**刻意不加事务**(单行条件更新),
  *       没有事务时照常立即投递;不写它那些通道一帧都发不出去,而且是静默的。</li>
  * </ol>
  */

+ 34 - 0
java/src/test/java/com/payment/platform/PaymentApplicationContextTest.java

@@ -6,13 +6,18 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.boot.test.context.TestConfiguration;
 import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationListener;
 import org.springframework.context.annotation.Bean;
+import org.springframework.context.support.AbstractApplicationContext;
 import org.springframework.scheduling.TaskScheduler;
 import org.springframework.scheduling.Trigger;
+import org.springframework.transaction.event.TransactionalApplicationListenerMethodAdapter;
+import org.springframework.transaction.event.TransactionalEventListenerFactory;
 
 import java.time.Clock;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.Collection;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.atomic.AtomicInteger;
 
@@ -169,4 +174,33 @@ class PaymentApplicationContextTest {
                 "调度类没有被装配 —— 装配检查漏了它们");
         assertNotNull(context.getBean(LsyReconcileScheduler.class));
     }
+
+    /**
+     * 订单实时推送的 {@code @TransactionalEventListener} 在**真实上下文**里必须被装配成
+     * 事务型适配器,而不是普通监听器。
+     *
+     * <p>【为什么这条必须落在真上下文里】裸 {@code AnnotationConfigApplicationContext} 只带
+     * {@code DefaultEventListenerFactory},同一个注解会被装配成普通监听器、事务相位被整个忽略
+     * (2026-10-05 实测:{@code OrderEventRelayTransactionBoundaryTest} 首跑 2/3 红,
+     * 事件在「事务中」被立即投递)。真容器里另一支工厂由 Spring Boot 的
+     * {@code TransactionAutoConfiguration → ProxyTransactionManagementConfiguration} 提供 ——
+     * 那条自动装配链是否在这台应用上生效,是**运行时事实**,只能在这里量。
+     *
+     * <p>全局只有一个 {@code @TransactionalEventListener}({@code OrderEventRelay.onOrderChanged},
+     * 2026-10-05 grep 全 main 量得),所以「存在事务型适配器」即「那一个被装配对了」。
+     */
+    @Test
+    void orderEventRelayIsWiredAsATransactionalListener() {
+        assertTrue(context.getBeanNamesForType(TransactionalEventListenerFactory.class).length > 0,
+                "上下文里没有 TransactionalEventListenerFactory —— @TransactionalEventListener 会退化成普通监听器");
+
+        Collection<ApplicationListener<?>> listeners =
+                ((AbstractApplicationContext) context).getApplicationListeners();
+        boolean wired = listeners.stream()
+                .anyMatch(l -> l instanceof TransactionalApplicationListenerMethodAdapter);
+        assertTrue(wired,
+                "监听器列表里没有事务型适配器 —— OrderEventRelay 的 AFTER_COMMIT 语义在真实上下文里失效,"
+                        + "事务里发布的帧会提前发出去。实际监听器类型="
+                        + listeners.stream().map(l -> l.getClass().getSimpleName()).distinct().sorted().toList());
+    }
 }

+ 139 - 0
java/src/test/java/com/payment/platform/module/payment/stream/OrderEventRelayTransactionBoundaryTest.java

@@ -0,0 +1,139 @@
+package com.payment.platform.module.payment.stream;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.transaction.event.TransactionalEventListenerFactory;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * {@link OrderEventRelay} 的三条事务边界,在**真 Spring 容器 + 真事件多播器**里执行验证。
+ *
+ * <p>【为什么补这一条】2026-10-05 复核实测:本仓 25 个 {@code publisher.changed(...)} 发布点里,
+ * **只有 1 个**在 @Transactional 方法体内 ——
+ * {@code HarOrderService.syncFromUpstream}(@Transactional 在 :499,发布在 :550,是 HAR 主动
+ * 查上游的主路径,不是兜底)。其余 24 条(含开放 API 与收银台那两条建单腿:controller 里
+ * 裸 {@code orderMapper.insert} + 紧跟着发布,两处都**没有**事务)走的是
+ * {@code fallbackExecution} 立即投递那条路。
+ *
+ * <p>而 Task 14 Step 5 的端到端实证用的是**收银台**那条腿 ⇒ 它验的是自动提交腿的
+ * 「信号不早于行的可见性」,{@code AFTER_COMMIT} 的**延迟投递分支一次都没被执行过**
+ * (此前只有 {@link OrderEventRelayTest#theAfterCommitSemanticsArePinnedInCode} 的注解反射断言)。
+ * 本类把那个分支变成可执行断言。
+ *
+ * <p>【量具边界,别当它验过的】本类手工驱动 Spring 的事务同步协议
+ * ({@code initSynchronization + setActualTransactionActive + afterCompletion(STATUS_COMMITTED)}),
+ * 走的是与真事务管理器**同一套**回调(6.1.6 字节码实测:门槛是
+ * {@code isSynchronizationActive() && isActualTransactionActive()},提交信号走
+ * {@code afterCompletion(STATUS_COMMITTED)},{@code PlatformSynchronization} **不覆写**
+ * {@code afterCommit()})。它**不**覆盖:真 {@code DataSourceTransactionManager} 驱动回调
+ * 的那半边(Spring 框架自身的职责),以及真事务业务腿(HAR 主动查询回写)的端到端。
+ * 后者仍是缺口,已记在台账 Final 裁定里。
+ */
+class OrderEventRelayTransactionBoundaryTest {
+
+    /** 只记录不广播。与 {@link 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 AnnotationConfigApplicationContext context;
+
+    /**
+     * 真容器:@TransactionalEventListener 的适配器由容器的 EventListenerMethodProcessor 生成。
+     *
+     * <p>★ 必须显式注册 {@link TransactionalEventListenerFactory}:裸 {@code AnnotationConfigApplicationContext}
+     * 默认只带 {@code DefaultEventListenerFactory}(真容器里另一支是 Spring Boot 的
+     * {@code TransactionAutoConfiguration → ProxyTransactionManagementConfiguration} 注册的)。
+     * 少了它,同一个注解会被装配成**普通** ApplicationListener —— 三条断言全部量到
+     * 「立即投递」,而这不是被测代码的行为,是量具的(2026-10-05 首跑实测:2/3 红,
+     * 失败信息 expected: &lt;[]&gt; but was: &lt;[lsy_order#7]&gt;)。
+     */
+    private AnnotationConfigApplicationContext startContext() {
+        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
+        ctx.registerBean("transactionalEventListenerFactory", TransactionalEventListenerFactory.class);
+        ctx.registerBean("orderEventHub", OrderEventHub.class, () -> hub);
+        ctx.registerBean("orderEventRelay", OrderEventRelay.class, () -> new OrderEventRelay(hub));
+        ctx.refresh();
+        context = ctx;
+        return ctx;
+    }
+
+    @AfterEach
+    void tearDown() {
+        if (context != null) {
+            context.close(); // @PreDestroy → hub.shutdown()
+        }
+        TransactionSynchronizationManager.setActualTransactionActive(false);
+    }
+
+    @Test
+    void insideTransaction_deliversOnlyAfterCommit() {
+        AnnotationConfigApplicationContext ctx = startContext();
+
+        TransactionSynchronizationManager.initSynchronization();
+        TransactionSynchronizationManager.setActualTransactionActive(true);
+        try {
+            ctx.publishEvent(new OrderChangedEvent("lsy_order", 7L));
+
+            assertEquals(List.of(), hub.submitted,
+                    "事务提交前一帧都不许投递:信号先到、库还没提交 ⇒ 页面重拉看不到新行,比不推更糟");
+
+            // 事务管理器提交后回调的是 afterCompletion(STATUS_COMMITTED),不是 afterCommit()
+            for (TransactionSynchronization sync : TransactionSynchronizationManager.getSynchronizations()) {
+                sync.afterCompletion(TransactionSynchronization.STATUS_COMMITTED);
+            }
+        } finally {
+            TransactionSynchronizationManager.clearSynchronization();
+            TransactionSynchronizationManager.setActualTransactionActive(false);
+        }
+
+        assertEquals(List.of("lsy_order#7"), hub.submitted,
+                "提交后必须投递(上面那条负断言的正对照:延迟不是丢失)");
+    }
+
+    @Test
+    void outsideTransaction_deliversImmediately() {
+        AnnotationConfigApplicationContext ctx = startContext();
+        assertFalse(TransactionSynchronizationManager.isSynchronizationActive(),
+                "本用例的前提:当前线程没有活动事务");
+
+        ctx.publishEvent(new OrderChangedEvent("lsy_refund", 9L));
+
+        assertEquals(List.of("lsy_refund#9"), hub.submitted,
+                "无事务腿(25 个发布点里的其余 24 个)必须立即投递 ——"
+                        + " fallbackExecution=false 时这些通道静默地一帧都发不出去");
+    }
+
+    @Test
+    void rollback_deliversNothing() {
+        AnnotationConfigApplicationContext ctx = startContext();
+
+        TransactionSynchronizationManager.initSynchronization();
+        TransactionSynchronizationManager.setActualTransactionActive(true);
+        try {
+            ctx.publishEvent(new OrderChangedEvent("har_order", 3L));
+            for (TransactionSynchronization sync : TransactionSynchronizationManager.getSynchronizations()) {
+                sync.afterCompletion(TransactionSynchronization.STATUS_ROLLED_BACK);
+            }
+        } finally {
+            TransactionSynchronizationManager.clearSynchronization();
+            TransactionSynchronizationManager.setActualTransactionActive(false);
+        }
+
+        assertEquals(List.of(), hub.submitted,
+                "回滚 = 那一行不在库里,投递信号只会让页面白拉一次");
+    }
+}