|
@@ -185,7 +185,8 @@ public class UnifiedOrderService {
|
|
|
* 仍为非终态且尚未排期的订单,按当前 {@code queryCount} 兜底安排下一次查询。
|
|
* 仍为非终态且尚未排期的订单,按当前 {@code queryCount} 兜底安排下一次查询。
|
|
|
*
|
|
*
|
|
|
* <p>不增加 {@code queryCount}:创建响应本身已经由 {@link #recordQueryAttempt} 计过一次,
|
|
* <p>不增加 {@code queryCount}:创建响应本身已经由 {@link #recordQueryAttempt} 计过一次,
|
|
|
- * 兜底只负责写 {@code next_query_time}。
|
|
|
|
|
|
|
+ * 兜底只负责写 {@code next_query_time};排期时间与其它两处共用
|
|
|
|
|
+ * {@link #nextQueryTimeWithinWindow}(含 5s 首档与 30 分钟边界)。
|
|
|
*/
|
|
*/
|
|
|
private void ensurePollScheduled(UnifiedOrderEntity order) {
|
|
private void ensurePollScheduled(UnifiedOrderEntity order) {
|
|
|
if (order == null
|
|
if (order == null
|
|
@@ -194,8 +195,8 @@ public class UnifiedOrderService {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
int queryCount = order.getQueryCount() == null ? 0 : order.getQueryCount();
|
|
int queryCount = order.getQueryCount() == null ? 0 : order.getQueryCount();
|
|
|
- scheduleNextQuery(order, OffsetDateTime.now()
|
|
|
|
|
- .plusSeconds(UnifiedPollPolicy.nextDelaySeconds(queryCount)));
|
|
|
|
|
|
|
+ scheduleNextQuery(order,
|
|
|
|
|
+ nextQueryTimeWithinWindow(order, OffsetDateTime.now(), queryCount));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private static void validateCreate(UnifiedOrderCreateCommand cmd, UnifiedSource source) {
|
|
private static void validateCreate(UnifiedOrderCreateCommand cmd, UnifiedSource source) {
|
|
@@ -342,7 +343,7 @@ public class UnifiedOrderService {
|
|
|
OffsetDateTime now = OffsetDateTime.now();
|
|
OffsetDateTime now = OffsetDateTime.now();
|
|
|
int queryCount = order.getQueryCount() == null ? 0 : order.getQueryCount();
|
|
int queryCount = order.getQueryCount() == null ? 0 : order.getQueryCount();
|
|
|
OffsetDateTime nextQueryTime = UnifiedOrderStatus.PENDING.contains(nextState)
|
|
OffsetDateTime nextQueryTime = UnifiedOrderStatus.PENDING.contains(nextState)
|
|
|
- ? now.plusSeconds(UnifiedPollPolicy.nextDelaySeconds(queryCount))
|
|
|
|
|
|
|
+ ? nextQueryTimeWithinWindow(order, now, queryCount)
|
|
|
: null;
|
|
: null;
|
|
|
OffsetDateTime payTime = UnifiedOrderStatus.SUCCESS.equals(nextState)
|
|
OffsetDateTime payTime = UnifiedOrderStatus.SUCCESS.equals(nextState)
|
|
|
? UnifiedResponseParser.toShanghai(
|
|
? UnifiedResponseParser.toShanghai(
|
|
@@ -632,30 +633,52 @@ public class UnifiedOrderService {
|
|
|
// ────────────────────────────── 主动轮询与手动同步 ──────────────────────────────
|
|
// ────────────────────────────── 主动轮询与手动同步 ──────────────────────────────
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * 向上游查单并收敛本地状态(10 秒轮询调度器与后台「同步上游」共用)。
|
|
|
|
|
|
|
+ * 自动轮询入口({@code UnifiedOrderPollScheduler} 消费)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>先判停止条件:已满 {@link UnifiedPollPolicy#MAX_QUERY_COUNT} 次或创建超过
|
|
|
|
|
+ * {@link UnifiedPollPolicy#MAX_AGE_MINUTES} 分钟 → 不打上游,直接把 {@code next_query_time} 置空。
|
|
|
|
|
+ * 手动入口见 {@link #syncManually(UnifiedOrderEntity)}。
|
|
|
|
|
+ */
|
|
|
|
|
+ public void syncFromUpstream(UnifiedOrderEntity order) {
|
|
|
|
|
+ sync(order, false);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 手动同步入口(后台「同步上游」,Task 16 控制器必须调用本方法)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>与自动轮询的区别只有一处:<b>入口不判停止条件</b> —— spec §7 承诺「轮询已停止」的
|
|
|
|
|
+ * 订单仍可手动同步一次。查单、计数、金额核对、条件 UPDATE、事件/回调全部相同。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>停止态(超 30 分钟/满 60 次)下查完保持 {@code next_query_time = null},
|
|
|
|
|
+ * 不重开自动轮询;页面仍显示「轮询已停止」。
|
|
|
|
|
+ */
|
|
|
|
|
+ public void syncManually(UnifiedOrderEntity order) {
|
|
|
|
|
+ sync(order, true);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 查单并收敛状态的共用实现。
|
|
|
*
|
|
*
|
|
|
* <p>五个刻意保持的顺序/口径(spec §7.2):
|
|
* <p>五个刻意保持的顺序/口径(spec §7.2):
|
|
|
*
|
|
*
|
|
|
* <ol>
|
|
* <ol>
|
|
|
- * <li><b>先判停止条件</b>:已满 {@link UnifiedPollPolicy#MAX_QUERY_COUNT} 次或创建
|
|
|
|
|
- * 超过 {@link UnifiedPollPolicy#MAX_AGE_MINUTES} 分钟 → 不打上游,直接把
|
|
|
|
|
- * {@code next_query_time} 置空。</li>
|
|
|
|
|
|
|
+ * <li><b>自动入口先判停止条件</b>(手动入口跳过):到上限不打上游,直接清空排期。</li>
|
|
|
* <li><b>配置按 {@code merchant_id} 取、不看启停</b>:{@link UnifiedMerchantService#requireById}
|
|
* <li><b>配置按 {@code merchant_id} 取、不看启停</b>:{@link UnifiedMerchantService#requireById}
|
|
|
* 不校验 {@code status} —— 停用商户的历史单还要继续收敛。</li>
|
|
* 不校验 {@code status} —— 停用商户的历史单还要继续收敛。</li>
|
|
|
* <li><b>{@code recordQueryAttempt} 在解析状态之前</b>:上游回了响应就消耗一次查询,
|
|
* <li><b>{@code recordQueryAttempt} 在解析状态之前</b>:上游回了响应就消耗一次查询,
|
|
|
* 未知状态/金额不符同样计数,否则这类单会无限轮询。</li>
|
|
* 未知状态/金额不符同样计数,否则这类单会无限轮询。</li>
|
|
|
* <li><b>金额核对只在 2/5</b>(与通知腿同一规则):不符只记错误码、不推进状态。</li>
|
|
* <li><b>金额核对只在 2/5</b>(与通知腿同一规则):不符只记错误码、不推进状态。</li>
|
|
|
* <li><b>状态真的迁移时 {@link #applyOrderState} 自己写 {@code next_query_time},
|
|
* <li><b>状态真的迁移时 {@link #applyOrderState} 自己写 {@code next_query_time},
|
|
|
- * 这里不得覆盖</b>;没有迁移才按当前 {@code queryCount} 退避续排。</li>
|
|
|
|
|
|
|
+ * 这里不得覆盖</b>;没有迁移才按退避续排。停止态查完一律保持 null。</li>
|
|
|
* </ol>
|
|
* </ol>
|
|
|
*
|
|
*
|
|
|
* <p>上游异常直接抛出:调度器逐单兜住,手动同步由控制器转成失败应答。
|
|
* <p>上游异常直接抛出:调度器逐单兜住,手动同步由控制器转成失败应答。
|
|
|
*/
|
|
*/
|
|
|
- public void syncFromUpstream(UnifiedOrderEntity order) {
|
|
|
|
|
|
|
+ private void sync(UnifiedOrderEntity order, boolean manual) {
|
|
|
if (order == null || order.getOrderNo() == null) {
|
|
if (order == null || order.getOrderNo() == null) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- if (shouldStopPolling(order)) {
|
|
|
|
|
|
|
+ if (!manual && shouldStopPolling(order)) {
|
|
|
scheduleNextQuery(order, null);
|
|
scheduleNextQuery(order, null);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -676,7 +699,7 @@ public class UnifiedOrderService {
|
|
|
markAmountMismatch(order, upstreamAmountDetail(order, data));
|
|
markAmountMismatch(order, upstreamAmountDetail(order, data));
|
|
|
} else if (applyOrderState(order, state, UnifiedUpstreamSnapshot.of(data), null, null)) {
|
|
} else if (applyOrderState(order, state, UnifiedUpstreamSnapshot.of(data), null, null)) {
|
|
|
// applyOrderState 已按新状态写好 next_query_time(非终态 = 退避,终态 = null)。
|
|
// applyOrderState 已按新状态写好 next_query_time(非终态 = 退避,终态 = null)。
|
|
|
- // 唯一例外:最后一次查询恰好把订单推进到非终态 —— 策略已判定停止,必须置空。
|
|
|
|
|
|
|
+ // 唯一例外:本次查询已到停止条件(如第 60 次恰好推进到非终态)——必须置空。
|
|
|
if (UnifiedOrderStatus.PENDING.contains(order.getTradeStatus())
|
|
if (UnifiedOrderStatus.PENDING.contains(order.getTradeStatus())
|
|
|
&& shouldStopPolling(order)) {
|
|
&& shouldStopPolling(order)) {
|
|
|
scheduleNextQuery(order, null);
|
|
scheduleNextQuery(order, null);
|
|
@@ -691,8 +714,8 @@ public class UnifiedOrderService {
|
|
|
}
|
|
}
|
|
|
if (UnifiedOrderStatus.PENDING.contains(order.getTradeStatus())) {
|
|
if (UnifiedOrderStatus.PENDING.contains(order.getTradeStatus())) {
|
|
|
int queryCount = order.getQueryCount() == null ? 0 : order.getQueryCount();
|
|
int queryCount = order.getQueryCount() == null ? 0 : order.getQueryCount();
|
|
|
- scheduleNextQuery(order, OffsetDateTime.now()
|
|
|
|
|
- .plusSeconds(UnifiedPollPolicy.nextDelaySeconds(queryCount)));
|
|
|
|
|
|
|
+ scheduleNextQuery(order,
|
|
|
|
|
+ nextQueryTimeWithinWindow(order, OffsetDateTime.now(), queryCount));
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -709,6 +732,30 @@ public class UnifiedOrderService {
|
|
|
return UnifiedPollPolicy.shouldStop(queryCount, createdAt, OffsetDateTime.now());
|
|
return UnifiedPollPolicy.shouldStop(queryCount, createdAt, OffsetDateTime.now());
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 下一次自动查询时间(spec §7.2 的退避序列 + 30 分钟停止边界)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>退避索引取 {@code queryCount - 1}:三个调用点都在 {@code recordQueryAttempt}
|
|
|
|
|
+ * 之后,计数已含本次查询;spec 的 5s 首档对应「第 1 次查询之后」。
|
|
|
|
|
+ * 负索引由 {@link UnifiedPollPolicy#nextDelaySeconds(int)} 自行夹取。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p><b>边界</b>:扫描条件只取 {@code created_time > now-30min},因此排到
|
|
|
|
|
+ * {@code createdAt + 30min} 或更晚的时间永远不会被消费。必须在写入时就置 null,
|
|
|
|
|
+ * 否则该行的 {@code next_query_time} 会永远停在过去时刻,页面无法显示「轮询已停止」。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>{@code createdTime} 缺失(数据损坏/单测构造)时不做边界判断:无法得知窗口起点。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static OffsetDateTime nextQueryTimeWithinWindow(UnifiedOrderEntity order,
|
|
|
|
|
+ OffsetDateTime now, int queryCount) {
|
|
|
|
|
+ OffsetDateTime next = now.plusSeconds(UnifiedPollPolicy.nextDelaySeconds(queryCount - 1));
|
|
|
|
|
+ OffsetDateTime createdAt = order.getCreatedTime();
|
|
|
|
|
+ if (createdAt != null
|
|
|
|
|
+ && !next.isBefore(createdAt.plusMinutes(UnifiedPollPolicy.MAX_AGE_MINUTES))) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return next;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 轮询扫描(Task 11 调度器消费):非终态、到期、未超 30 分钟、未满 60 次,
|
|
* 轮询扫描(Task 11 调度器消费):非终态、到期、未超 30 分钟、未满 60 次,
|
|
|
* 按到期时间升序,最多 {@code limit} 条。
|
|
* 按到期时间升序,最多 {@code limit} 条。
|