|
|
@@ -297,9 +297,9 @@ class UnifiedOrderSyncTest {
|
|
|
|
|
|
@Test
|
|
|
void eachSyncCountsOneQuery_andBackoffFollowsPolicy() {
|
|
|
- // 服务在任何解析之前先 recordQueryAttempt:查询结束后的退避按「第 n 次查询之后」取值。
|
|
|
- // 因此初始 queryCount=0..6 对应 nextDelaySeconds(1..7),即 10/20/30/60/120/300/300。
|
|
|
- long[] expectedSeconds = {10, 20, 30, 60, 120, 300, 300};
|
|
|
+ // 服务在任何解析之前先 recordQueryAttempt:退避索引取「本次查询是第几次之前」,
|
|
|
+ // 即 nextDelaySeconds(queryCount - 1)。初始 queryCount=0..6 对应 5/10/20/30/60/120/300。
|
|
|
+ long[] expectedSeconds = {5, 10, 20, 30, 60, 120, 300};
|
|
|
for (int initial = 0; initial <= 6; initial++) {
|
|
|
when(merchantService.requireById(11L)).thenReturn(merchant());
|
|
|
when(client.queryOrder(any(), eq("UP5"))).thenReturn(json("{\"state\":0}"));
|
|
|
@@ -374,6 +374,113 @@ class UnifiedOrderSyncTest {
|
|
|
verifyNoInteractions(client, merchantService);
|
|
|
}
|
|
|
|
|
|
+ // ──────────────────────── 30 分钟排期边界(I1)与手动同步(I2) ────────────────────────
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void nearThirtyMinuteBoundary_withLongDelay_stopsPolling() {
|
|
|
+ when(merchantService.requireById(11L)).thenReturn(merchant());
|
|
|
+ when(client.queryOrder(any(), eq("UP5"))).thenReturn(json("{\"state\":0}"));
|
|
|
+ when(orderMapper.update(isNull(), any())).thenReturn(1);
|
|
|
+
|
|
|
+ UnifiedOrderEntity order = order("0");
|
|
|
+ // 29 分钟时退避已到 300s(本次是第 11 次查询):排期必越过「创建 + 30 分钟」。
|
|
|
+ order.setQueryCount(10);
|
|
|
+ order.setCreatedTime(OffsetDateTime.now().minusMinutes(UnifiedPollPolicy.MAX_AGE_MINUTES - 1));
|
|
|
+ service.syncFromUpstream(order);
|
|
|
+
|
|
|
+ assertEquals("0", order.getTradeStatus());
|
|
|
+ assertNull(order.getNextQueryTime(),
|
|
|
+ "排期越过 createdAt+30min 必须置空,页面才能显示「轮询已停止」");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void nearThirtyMinuteBoundary_withShortDelay_stillSchedules() {
|
|
|
+ when(merchantService.requireById(11L)).thenReturn(merchant());
|
|
|
+ when(client.queryOrder(any(), eq("UP5"))).thenReturn(json("{\"state\":0}"));
|
|
|
+ when(orderMapper.update(isNull(), any())).thenReturn(1);
|
|
|
+
|
|
|
+ UnifiedOrderEntity order = order("0");
|
|
|
+ order.setQueryCount(0);
|
|
|
+ order.setCreatedTime(OffsetDateTime.now().minusMinutes(UnifiedPollPolicy.MAX_AGE_MINUTES - 1));
|
|
|
+ service.syncFromUpstream(order);
|
|
|
+
|
|
|
+ assertScheduledIn(order, UnifiedPollPolicy.nextDelaySeconds(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void transitionToPayingAtTwentyNineFiftyFive_stopsPolling() {
|
|
|
+ when(merchantService.requireById(11L)).thenReturn(merchant());
|
|
|
+ when(client.queryOrder(any(), eq("UP5"))).thenReturn(json("{\"state\":1}"));
|
|
|
+ when(orderMapper.update(isNull(), any())).thenReturn(1);
|
|
|
+
|
|
|
+ UnifiedOrderEntity order = order("0");
|
|
|
+ // 29:55,首档 5s 后正好到达 createdAt+30min(含等号即越界)。
|
|
|
+ order.setCreatedTime(OffsetDateTime.now().minusSeconds(29 * 60 + 55));
|
|
|
+ service.syncFromUpstream(order);
|
|
|
+
|
|
|
+ assertEquals("1", order.getTradeStatus());
|
|
|
+ assertNull(order.getNextQueryTime(),
|
|
|
+ "0→1 恰好发生在 29:55 时,applyOrderState 排的 5s 也必须被 30 分钟边界压成 null");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void syncManually_queriesOverAgeOrderAndAppliesState() {
|
|
|
+ when(merchantService.requireById(11L)).thenReturn(merchant());
|
|
|
+ when(client.queryOrder(any(), eq("UP5"))).thenReturn(
|
|
|
+ json("{\"state\":2,\"amount\":100,\"currency\":\"cny\"}"));
|
|
|
+ when(orderMapper.update(isNull(), any())).thenReturn(1);
|
|
|
+
|
|
|
+ UnifiedOrderEntity order = order("0");
|
|
|
+ order.setCreatedTime(OffsetDateTime.now().minusMinutes(UnifiedPollPolicy.MAX_AGE_MINUTES + 1));
|
|
|
+ service.syncManually(order);
|
|
|
+
|
|
|
+ verify(client).queryOrder(any(), eq("UP5"));
|
|
|
+ assertEquals("2", order.getTradeStatus(), "超龄单手动同步仍必须能收敛状态");
|
|
|
+ assertNull(order.getNextQueryTime(), "手动同步不得重开自动轮询");
|
|
|
+ verify(publisher).changed(OrderEventPublisher.CHANNEL_UNIFIED_ORDER, TENANT);
|
|
|
+ verify(callbackService).notifyOrder(same(order), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void syncManually_atQueryLimitQueriesButDoesNotReopenPolling() {
|
|
|
+ when(merchantService.requireById(11L)).thenReturn(merchant());
|
|
|
+ when(client.queryOrder(any(), eq("UP5"))).thenReturn(json("{\"state\":0}"));
|
|
|
+ when(orderMapper.update(isNull(), any())).thenReturn(1);
|
|
|
+
|
|
|
+ UnifiedOrderEntity order = order("0");
|
|
|
+ order.setQueryCount(UnifiedPollPolicy.MAX_QUERY_COUNT);
|
|
|
+ service.syncManually(order);
|
|
|
+
|
|
|
+ verify(client).queryOrder(any(), eq("UP5"));
|
|
|
+ assertEquals(UnifiedPollPolicy.MAX_QUERY_COUNT + 1, order.getQueryCount(),
|
|
|
+ "手动同步本身也是一次查询");
|
|
|
+ assertNull(order.getNextQueryTime(), "停止态手动同步不得重开自动轮询");
|
|
|
+ verifyNoInteractions(publisher, callbackService);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void syncManually_whenNotStopped_schedulesNormally() {
|
|
|
+ when(merchantService.requireById(11L)).thenReturn(merchant());
|
|
|
+ when(client.queryOrder(any(), eq("UP5"))).thenReturn(json("{\"state\":0}"));
|
|
|
+ when(orderMapper.update(isNull(), any())).thenReturn(1);
|
|
|
+
|
|
|
+ UnifiedOrderEntity order = order("0");
|
|
|
+ service.syncManually(order);
|
|
|
+
|
|
|
+ assertScheduledIn(order, UnifiedPollPolicy.nextDelaySeconds(0));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void syncFromUpstream_atQueryLimit_skipsUpstream() {
|
|
|
+ UnifiedOrderEntity order = order("0");
|
|
|
+ order.setQueryCount(UnifiedPollPolicy.MAX_QUERY_COUNT);
|
|
|
+
|
|
|
+ service.syncFromUpstream(order);
|
|
|
+
|
|
|
+ verifyNoInteractions(client, merchantService);
|
|
|
+ assertNull(order.getNextQueryTime(), "自动入口到上限只清空排期");
|
|
|
+ }
|
|
|
+
|
|
|
// ────────────────────────────── 轮询扫描 ──────────────────────────────
|
|
|
|
|
|
@Test
|