diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/job/HardAggregateOrderPayJob.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/job/HardAggregateOrderPayJob.java index 3af02e996..5d601c533 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/job/HardAggregateOrderPayJob.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/job/HardAggregateOrderPayJob.java @@ -6,12 +6,14 @@ import com.bsd.pjcloud.common.data.tenant.TenantBroker; import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum; import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum; import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; +import com.bsd.pjcloud.hard.service.HardAggregatePayPollingCoordinator; import com.bsd.pjcloud.hard.service.HardOrderService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; +import java.time.LocalDateTime; import java.util.List; /** @@ -30,25 +32,99 @@ public class HardAggregateOrderPayJob { */ private static final int BATCH_SIZE = 100; + /** + * 近期订单高频补偿窗口,单位分钟;覆盖聚合码有效期及 Thirdparty 关单补偿时间。 + */ + private static final long ACTIVE_ORDER_WINDOW_MINUTES = 15L; + + /** + * 超出近期窗口但尚未达到自动取消时间的订单低频核对间隔,单位毫秒。 + */ + private static final long STALE_RECONCILIATION_INTERVAL_MILLIS = 300000L; + + /** + * 服务启动后首次核对遗留订单的等待时间,单位毫秒,避免与启动恢复扫描同时争用资源。 + */ + private static final long STALE_INITIAL_DELAY_MILLIS = 60000L; + /** * Hardware 订单服务,复用带订单锁和事务的聚合支付状态确认及结算流程。 */ private final HardOrderService hardOrderService; /** - * 每五秒补偿仍处于创建状态且已绑定聚合会话的 Hardware 订单。 + * 近期聚合订单轮询协调器,用于在空闲时退避数据库扫描并响应新订单唤醒。 + */ + private final HardAggregatePayPollingCoordinator pollingCoordinator; + + /** + * 高频补偿最近十五分钟内仍处于创建状态且已绑定聚合会话的 Hardware 订单。 * *
调度线程无租户上下文时先跨租户读取候选记录,再按记录持久化的租户 ID 恢复上下文; - * 具体结算继续由业务服务的分布式订单锁和状态条件保证幂等。
+ * 具体结算继续由业务服务的分布式订单锁和状态条件保证幂等。方法每五秒轻量触发一次,连续空结果时 + * 真正的数据库扫描按十秒、二十秒、四十秒退避到最长一分钟。 */ - @Scheduled(fixedDelay = 5000L, initialDelay = 5000L) + @Scheduled(fixedDelay = HardAggregatePayPollingCoordinator.TRIGGER_INTERVAL_MILLIS, + initialDelay = HardAggregatePayPollingCoordinator.TRIGGER_INTERVAL_MILLIS) public void settlePendingAggregateOrders() { - List一小时以内仍保留服务异常恢复能力;超过一小时后不再调用 Thirdparty 查单,而是把仍处于创建状态 + * 的 Hardware 订单原子更新为未支付取消,避免异常记录被永久轮询。第三方若在此后报告延迟支付, + * 由线下渠道核对和协商,不再自动改变 Hardware 库存及订单状态。
+ */ + @Scheduled(fixedDelay = STALE_RECONCILIATION_INTERVAL_MILLIS, initialDelay = STALE_INITIAL_DELAY_MILLIS) + public void settleStaleAggregateOrders() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime activeSince = now.minusMinutes(ACTIVE_ORDER_WINDOW_MINUTES); + LocalDateTime cancelBefore = now.minusMinutes(HardAggregatePayPollingCoordinator.MAX_AUTO_SETTLEMENT_MINUTES); + List更新同时校验支付方式、创建状态和创建时间,避免与临界时刻的终端查询或支付结算互相覆盖。 + * 此处使用 {@link HardOrderStatusEnum#UNFOLLOW} 表示未支付自动取消,不能使用代表已核销完成的 + * {@link HardOrderStatusEnum#CANCEL}。
+ * + * @param orders 本轮查询到的超时 Hardware 聚合支付订单 + * @param cancelBefore 本轮统一的一小时截止时间 + */ + private void cancelExpiredOrders(List协调器只控制当前服务实例访问数据库的频率,不保存业务订单状态。存在近期待结算订单或查询异常时 + * 保持五秒补偿;连续空扫描时指数退避,创建订单后主动唤醒,避免无订单时全天执行固定频率 SQL。
+ */ +@Component +public class HardAggregatePayPollingCoordinator { + + /** + * 近期聚合订单结算任务的轻量触发周期,单位毫秒;数据库扫描还会受空闲退避门控制。 + */ + public static final long TRIGGER_INTERVAL_MILLIS = 5000L; + + /** + * 聚合支付 Hardware 订单允许自动结算的最长时间,单位分钟;状态查询和后台任务必须共用该值。 + */ + public static final long MAX_AUTO_SETTLEMENT_MINUTES = 60L; + + /** + * 连续空扫描时允许达到的最大数据库扫描间隔,单位毫秒。 + */ + private static final long MAX_IDLE_INTERVAL_MILLIS = 60000L; + + /** + * 指数退避最大左移位数,防止连续空扫描计数和间隔计算无限增长。 + */ + private static final int MAX_BACKOFF_SHIFT = 8; + + /** + * 连续空扫描次数;发现订单或收到创建唤醒后清零。 + */ + private int consecutiveEmptyScans; + + /** + * 使用 {@link System#nanoTime()} 时间基准保存的下次允许扫描时刻。 + */ + private long nextScanAtNanos = System.nanoTime(); + + /** + * 当前是否已有线程取得扫描资格,用于防止调度线程重叠访问数据库。 + */ + private boolean scanInProgress; + + /** + * 扫描执行期间是否收到新订单唤醒,确保扫描结束时不会覆盖唤醒信号。 + */ + private boolean wakeUpRequested; + + /** + * 原子判断近期聚合订单是否到达下次数据库扫描时间,并锁定本轮扫描资格。 + * + * @return 到达扫描时间且当前没有同类扫描执行时返回 {@code true} + */ + public synchronized boolean tryStartPolling() { + long now = System.nanoTime(); + if (scanInProgress || now - nextScanAtNanos < 0) { + return false; + } + scanInProgress = true; + return true; + } + + /** + * 释放扫描资格并按查询结果安排下一次数据库扫描。 + * + * @param hasPendingOrders 本轮是否查询到近期待结算订单;查询异常时传 {@code true} 以保持短间隔重试 + */ + public synchronized void finishPolling(boolean hasPendingOrders) { + scanInProgress = false; + if (wakeUpRequested) { + wakeUpRequested = false; + consecutiveEmptyScans = 0; + nextScanAtNanos = System.nanoTime(); + return; + } + long nextIntervalMillis = hasPendingOrders ? TRIGGER_INTERVAL_MILLIS : nextIdleIntervalMillis(); + if (hasPendingOrders) { + consecutiveEmptyScans = 0; + } + nextScanAtNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(nextIntervalMillis); + } + + /** + * 聚合订单创建或复用成功后唤醒近期扫描,最迟在下一个五秒调度周期访问数据库。 + */ + public synchronized void wakePolling() { + consecutiveEmptyScans = 0; + if (scanInProgress) { + wakeUpRequested = true; + return; + } + nextScanAtNanos = System.nanoTime(); + } + + /** + * 按连续空扫描次数计算下一次等待时长,并限制在一分钟以内。 + * + * @return 下一次数据库扫描等待时长,单位毫秒 + */ + private long nextIdleIntervalMillis() { + consecutiveEmptyScans = Math.min(consecutiveEmptyScans + 1, MAX_BACKOFF_SHIFT); + long multiplier = 1L << consecutiveEmptyScans; + return Math.min(TRIGGER_INTERVAL_MILLIS * multiplier, MAX_IDLE_INTERVAL_MILLIS); + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java index b7e62933b..473680f2c 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java @@ -40,6 +40,7 @@ import com.bsd.pjcloud.thirdparty.dto.ks.KsRecognitionCallbackDto; import com.bsd.pjcloud.wallet.api.vo.MchManageVo; import jakarta.validation.Valid; +import java.time.LocalDateTime; import java.util.List; public interface HardOrderService extends IService调度线程初始没有租户上下文,单条处理前使用记录自身 tenantId 恢复上下文; - * Redis 键仍由公共基础设施自动附加租户前缀。
+ * Redis 键仍由公共基础设施自动附加租户前缀。方法每五秒轻量触发一次,但连续空结果时协调器会把 + * 真正的数据库扫描逐级退避到最长六十秒;创建超过一小时的记录不再进入高频处理。 */ - @Scheduled(fixedDelay = PENDING_POLL_INTERVAL_MILLIS, initialDelay = PENDING_POLL_INTERVAL_MILLIS) + @Scheduled(fixedDelay = ThirdPayPollingCoordinator.ACTIVE_TRIGGER_INTERVAL_MILLIS, + initialDelay = ThirdPayPollingCoordinator.ACTIVE_TRIGGER_INTERVAL_MILLIS) public void handlePendingSessions() { - List补偿窗口从会话原始失效时间开始计算,最多持续十分钟;因此客户端退出、断网或不再查询状态时, - * 服务端仍会独立完成有限次查单和关单,同时不会对一天前的未支付订单永久轮询。
+ * 服务端仍会独立完成有限次查单和关单。连续空结果时数据库扫描从三十秒逐级退避到最长五分钟, + * 超过一小时后只把会话及未支付子订单标记为关闭,不再请求支付宝或微信。 */ - @Scheduled(fixedDelay = CLOSE_COMPENSATION_INTERVAL_MILLIS, initialDelay = CLOSE_COMPENSATION_INTERVAL_MILLIS) + @Scheduled(fixedDelay = ThirdPayPollingCoordinator.COMPENSATION_TRIGGER_INTERVAL_MILLIS, + initialDelay = ThirdPayPollingCoordinator.COMPENSATION_TRIGGER_INTERVAL_MILLIS) public void handleExpiredSessionCompensation() { - LocalDateTime now = LocalDateTime.now(); - List该分支不调用任何支付平台接口,也不再根据子订单恢复会话。会话先进入关闭终态,随后仅把仍为 + * 未支付的子订单标记为关闭;已经落库的真实支付结果保留给人工对账,但不会重新激活聚合会话。
+ * + * @param sessions 超过一小时且仍处于活动或过期状态的聚合会话 + */ + private void closeHistoricalSessions(List初始查询在线程无租户上下文时跨租户读取,处理单条订单前通过 TenantBroker - * 恢复订单所属租户,确保后续读取和原子更新都受正确租户条件约束。
+ * 恢复订单所属租户,确保后续读取和原子更新都受正确租户条件约束。方法每五秒轻量触发一次, + * 但连续空结果时协调器会把真正的数据库扫描逐级退避到最长六十秒。 */ - @Scheduled(fixedDelay = ORDER_CODE_POLL_INTERVAL_MILLIS, initialDelay = ORDER_CODE_POLL_INTERVAL_MILLIS) + @Scheduled(fixedDelay = ThirdPayPollingCoordinator.ACTIVE_TRIGGER_INTERVAL_MILLIS, + initialDelay = ThirdPayPollingCoordinator.ACTIVE_TRIGGER_INTERVAL_MILLIS) public void handleOrderCodeOrders() { - LocalDateTime earliestExpireTime = LocalDateTime.now().minusMinutes(ORDER_CODE_CLOSE_COMPENSATION_MINUTES); + if (!pollingCoordinator.tryStartOrderCodePolling()) { + return; + } + boolean hasPendingOrders = true; + try { + LocalDateTime earliestExpireTime = LocalDateTime.now().minusMinutes(ORDER_CODE_CLOSE_COMPENSATION_MINUTES); + List该任务只执行带未支付状态条件的数据库更新,不再向支付宝或微信发起查单、撤销或关单请求; + * 平台若在硬截止后出现延迟支付,由人工渠道核对处理。
+ */ + @Scheduled(fixedDelay = ORDER_CODE_HISTORY_CLOSE_INTERVAL_MILLIS, + initialDelay = ORDER_CODE_HISTORY_CLOSE_INITIAL_DELAY_MILLIS) + public void closeHistoricalOrderCodeOrders() { + LocalDateTime cutoff = LocalDateTime.now() + .minusMinutes(ThirdPayPollingCoordinator.MAX_AUTO_PROCESSING_MINUTES); List支付平台回调仍是结果确认主路径;本协调器只控制数据库补偿扫描频率。有待处理记录时维持短间隔, + * 连续空扫描时指数退避,订单创建成功后由当前服务实例主动唤醒,从而避免全天执行无效 SQL。
+ */ +@Component +public class ThirdPayPollingCoordinator { + + /** + * 活动订单任务的轻量触发周期,单位毫秒;真正的数据库扫描还会受空闲退避门控制。 + */ + public static final long ACTIVE_TRIGGER_INTERVAL_MILLIS = 5000L; + + /** + * 过期补偿任务的轻量触发周期,单位毫秒;真正的数据库扫描还会受空闲退避门控制。 + */ + public static final long COMPENSATION_TRIGGER_INTERVAL_MILLIS = 30000L; + + /** + * 订单码允许服务端自动查单、关单或恢复会话状态的最长时间,单位分钟。 + * + *超过该硬截止后只允许本地标记关闭,客户端查询、定时任务和创建重试均不得再次访问支付平台。
+ */ + public static final long MAX_AUTO_PROCESSING_MINUTES = 60L; + + /** + * 原生订单码和聚合活动会话连续为空时的最大数据库扫描间隔,单位毫秒。 + */ + private static final long ACTIVE_MAX_IDLE_INTERVAL_MILLIS = 60000L; + + /** + * 聚合过期补偿连续为空时的最大数据库扫描间隔,单位毫秒。 + */ + private static final long COMPENSATION_MAX_IDLE_INTERVAL_MILLIS = 300000L; + + /** + * 指数退避最大左移位数,防止连续空扫描计数和间隔计算无限增长。 + */ + private static final int MAX_BACKOFF_SHIFT = 8; + + /** + * 旧支付宝、微信原生订单码扫描门,状态仅属于当前服务实例进程。 + */ + private final AdaptivePollingGate orderCodeGate; + + /** + * 支付宝、微信聚合活动会话扫描门,状态仅属于当前服务实例进程。 + */ + private final AdaptivePollingGate aggregatePendingGate; + + /** + * 聚合会话到期关单补偿扫描门,状态仅属于当前服务实例进程。 + */ + private final AdaptivePollingGate aggregateCompensationGate; + + /** + * 创建轮询协调器并让三个扫描门在服务启动后各执行一次恢复扫描。 + */ + public ThirdPayPollingCoordinator() { + orderCodeGate = new AdaptivePollingGate(ACTIVE_TRIGGER_INTERVAL_MILLIS, ACTIVE_MAX_IDLE_INTERVAL_MILLIS); + aggregatePendingGate = new AdaptivePollingGate(ACTIVE_TRIGGER_INTERVAL_MILLIS, ACTIVE_MAX_IDLE_INTERVAL_MILLIS); + aggregateCompensationGate = new AdaptivePollingGate( + COMPENSATION_TRIGGER_INTERVAL_MILLIS, COMPENSATION_MAX_IDLE_INTERVAL_MILLIS); + } + + /** + * 尝试取得本轮原生订单码数据库扫描资格。 + * + * @return 到达下次扫描时间且当前没有同类扫描执行时返回 {@code true} + */ + public boolean tryStartOrderCodePolling() { + return orderCodeGate.tryStart(); + } + + /** + * 完成本轮原生订单码扫描并根据是否存在待处理订单安排下次数据库扫描。 + * + * @param hasPendingOrders 本轮查询是否返回待处理订单;查询异常时应传 {@code true} 以便尽快重试 + */ + public void finishOrderCodePolling(boolean hasPendingOrders) { + orderCodeGate.finish(hasPendingOrders); + } + + /** + * 原生订单码创建或复用成功后唤醒当前实例扫描门,最迟在下一个五秒触发周期执行数据库扫描。 + */ + public void wakeOrderCodePolling() { + orderCodeGate.wakeUp(); + } + + /** + * 尝试取得本轮聚合活动会话数据库扫描资格。 + * + * @return 到达下次扫描时间且当前没有同类扫描执行时返回 {@code true} + */ + public boolean tryStartAggregatePendingPolling() { + return aggregatePendingGate.tryStart(); + } + + /** + * 完成本轮聚合活动会话扫描并根据是否存在待处理会话安排下次数据库扫描。 + * + * @param hasPendingSessions 本轮查询是否返回活动会话;查询异常时应传 {@code true} 以便尽快重试 + */ + public void finishAggregatePendingPolling(boolean hasPendingSessions) { + aggregatePendingGate.finish(hasPendingSessions); + } + + /** + * 聚合会话创建或复用成功后同时唤醒活动扫描与到期补偿扫描。 + * + *补偿扫描同步唤醒可避免它正处于五分钟空闲间隔时错过新会话的两分钟失效节点。
+ */ + public void wakeAggregatePolling() { + aggregatePendingGate.wakeUp(); + aggregateCompensationGate.wakeUp(); + } + + /** + * 尝试取得本轮聚合过期补偿数据库扫描资格。 + * + * @return 到达下次扫描时间且当前没有同类扫描执行时返回 {@code true} + */ + public boolean tryStartAggregateCompensationPolling() { + return aggregateCompensationGate.tryStart(); + } + + /** + * 完成本轮聚合过期补偿扫描并根据是否存在待补偿会话安排下次数据库扫描。 + * + * @param hasExpiredSessions 本轮查询是否返回待补偿会话;查询异常时应传 {@code true} 以便尽快重试 + */ + public void finishAggregateCompensationPolling(boolean hasExpiredSessions) { + aggregateCompensationGate.finish(hasExpiredSessions); + } + + /** + * 单类轮询任务的进程内自适应扫描门。 + * + *扫描门不保存业务订单信息,也不替代数据库恢复;服务重启后第一次触发始终允许扫描。
+ */ + private static final class AdaptivePollingGate { + + /** + * 发现待处理记录或扫描异常时使用的基础间隔,单位毫秒。 + */ + private final long activeIntervalMillis; + + /** + * 连续空扫描允许退避到的最大间隔,单位毫秒。 + */ + private final long maxIdleIntervalMillis; + + /** + * 连续空扫描次数,用于计算指数退避倍数,唤醒或发现记录后清零。 + */ + private int consecutiveEmptyScans; + + /** + * 使用 {@link System#nanoTime()} 时间基准保存的下次允许扫描时刻。 + */ + private long nextScanAtNanos; + + /** + * 当前是否已有线程取得扫描资格,防止调度重叠执行同一类数据库扫描。 + */ + private boolean scanInProgress; + + /** + * 扫描执行期间是否收到订单创建唤醒,确保完成扫描时不会覆盖这次唤醒。 + */ + private boolean wakeUpRequested; + + /** + * 创建自适应扫描门,初始下次扫描时刻设为当前时间以支持服务启动恢复。 + * + * @param activeIntervalMillis 活动或异常状态的基础扫描间隔,单位毫秒 + * @param maxIdleIntervalMillis 连续空闲状态的最大扫描间隔,单位毫秒 + */ + private AdaptivePollingGate(long activeIntervalMillis, long maxIdleIntervalMillis) { + this.activeIntervalMillis = activeIntervalMillis; + this.maxIdleIntervalMillis = maxIdleIntervalMillis; + this.nextScanAtNanos = System.nanoTime(); + } + + /** + * 原子判断当前是否到达扫描时间,并锁定本轮扫描资格。 + * + * @return 当前线程取得扫描资格时返回 {@code true} + */ + private synchronized boolean tryStart() { + long now = System.nanoTime(); + if (scanInProgress || now - nextScanAtNanos < 0) { + return false; + } + scanInProgress = true; + return true; + } + + /** + * 释放扫描资格并根据查询结果设置下次允许扫描的时间。 + * + * @param hasPendingRecords 本轮是否存在待处理记录,异常场景按 {@code true} 处理 + */ + private synchronized void finish(boolean hasPendingRecords) { + scanInProgress = false; + if (wakeUpRequested) { + wakeUpRequested = false; + consecutiveEmptyScans = 0; + nextScanAtNanos = System.nanoTime(); + return; + } + long nextIntervalMillis = hasPendingRecords ? activeIntervalMillis : nextIdleIntervalMillis(); + if (hasPendingRecords) { + consecutiveEmptyScans = 0; + } + nextScanAtNanos = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(nextIntervalMillis); + } + + /** + * 唤醒扫描门;扫描进行中时记录延迟唤醒,空闲时立即允许下一次调度触发扫描。 + */ + private synchronized void wakeUp() { + consecutiveEmptyScans = 0; + if (scanInProgress) { + wakeUpRequested = true; + return; + } + nextScanAtNanos = System.nanoTime(); + } + + /** + * 按连续空扫描次数计算下一次退避间隔,并限制在当前扫描门最大空闲间隔内。 + * + * @return 下一次数据库扫描等待时长,单位毫秒 + */ + private long nextIdleIntervalMillis() { + consecutiveEmptyScans = Math.min(consecutiveEmptyScans + 1, MAX_BACKOFF_SHIFT); + long multiplier = 1L << consecutiveEmptyScans; + return Math.min(activeIntervalMillis * multiplier, maxIdleIntervalMillis); + } + } +} diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java index 20d4328bf..bd1cacfff 100644 --- a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java +++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/main/java/com/bsd/pjcloud/thirdparty/service/impl/appplus/ThirdAggregateOrderPayServiceImpl.java @@ -27,11 +27,7 @@ import com.bsd.pjcloud.thirdparty.model.pay.ThirdAggregateAlipayOauthIdentity; import com.bsd.pjcloud.thirdparty.model.pay.ThirdAggregateCashierLaunch; import com.bsd.pjcloud.thirdparty.model.pay.ThirdAggregateCashierPageData; import com.bsd.pjcloud.thirdparty.model.pay.ThirdAggregatePayCreateContext; -import com.bsd.pjcloud.thirdparty.service.ThirdAggregateAliPayService; -import com.bsd.pjcloud.thirdparty.service.ThirdAggregateOrderPayService; -import com.bsd.pjcloud.thirdparty.service.ThirdAggregatePaySessionService; -import com.bsd.pjcloud.thirdparty.service.ThirdAggregateWechatPayService; -import com.bsd.pjcloud.thirdparty.service.ThirdPayOrderService; +import com.bsd.pjcloud.thirdparty.service.*; import com.bsd.pjcloud.thirdparty.vo.appplus.pay.ThirdAggregateOrderCodePayStatusVo; import com.bsd.pjcloud.thirdparty.vo.appplus.pay.ThirdAggregateOrderCodePayVo; import com.bsd.pjcloud.wallet.api.entity.mch.MchPaySettingEntity; @@ -218,6 +214,11 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay */ private final StringEncryptor stringEncryptor; + /** + * 服务端轮询协调器,在聚合会话创建或复用成功后唤醒当前实例的自适应扫描任务。 + */ + private final ThirdPayPollingCoordinator pollingCoordinator; + /** * 进程级强随机数生成器,用于不可预测的聚合访问令牌。 */ @@ -239,14 +240,18 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay throw new RuntimeException("聚合订单码正在处理中,请勿重复提交"); } try { - return createOrReuse(dto); + ThirdAggregateOrderCodePayVo result = createOrReuse(dto); + pollingCoordinator.wakeAggregatePolling(); + return result; } finally { releaseLock(lockKey, lockToken); } } /** - * 查询当前租户聚合会话,并先同步已到达终态的第三方子订单。 + * 查询当前租户聚合会话,并在自动处理期限内同步第三方子订单终态。 + * + *创建已满一小时的会话只执行本地关闭,不因客户端持续查询再次访问支付平台或恢复历史会话。
* * @param aggregateNo 聚合支付会话编号 * @return 最小聚合支付状态 @@ -260,7 +265,9 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay if (session == null) { throw new RuntimeException("聚合支付会话不存在"); } - if (isActive(session) && isExpired(session)) { + if (isBeyondAutoProcessingWindow(session)) { + processPendingSession(session.getId()); + } else if (isActive(session) && isExpired(session)) { processPendingSession(session.getId()); } else { syncSessionFromChildOrder(session); @@ -407,7 +414,8 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay * 处理一条待确认聚合会话或处于有限补偿窗口内的过期会话。 * *活动会话到期后即使第三方查询或关单异常,也会在本轮结束时推进为过期状态,退出五秒高频队列; - * 后续仅由服务端低频补偿任务在限定窗口内重试,不依赖客户端继续查询。
+ * 后续仅由服务端低频补偿任务在限定窗口内重试。创建已满一小时后只执行本地关闭,不再访问平台, + * 因此客户端查询和创建重试均不能形成无限历史恢复。 * * @param sessionId 聚合支付会话主键 */ @@ -419,6 +427,10 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay if (session == null || (!isActive(session) && !expiredCompensation)) { return; } + if (isBeyondAutoProcessingWindow(session)) { + closeHistoricalSessionLocally(session); + return; + } if (ObjectUtil.equals(session.getStatus(), ThirdAggregatePaySessionStatusEnum.WAIT_SCAN.getType())) { if (isExpired(session)) { aggregatePaySessionService.markTerminal(session.getId(), ThirdAggregatePaySessionStatusEnum.EXPIRED); @@ -457,7 +469,9 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay } /** - * 识别共享回调中的聚合 JSAPI 子订单并同步聚合会话,旧支付订单返回 {@code false}。 + * 识别共享回调中的聚合 JSAPI 子订单并在自动处理期限内同步聚合会话。 + * + *超过一小时到达的延迟回调仍保留子订单真实支付事实,但聚合会话只标记关闭,不恢复为成功。
* * @param payOrder 已通过平台验签和关键字段校验的最新第三方支付订单 * @return 找到 pay_no 绑定聚合会话时返回 {@code true} @@ -471,6 +485,10 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay if (session == null) { return false; } + if (isBeyondAutoProcessingWindow(session)) { + closeHistoricalSessionLocally(session); + return true; + } aggregatePaySessionService.syncFromPayOrder(payOrder); return true; } @@ -664,6 +682,10 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay if (session == null || StrUtil.isBlank(session.getPayNo())) { return; } + if (isBeyondAutoProcessingWindow(session)) { + closeHistoricalSessionLocally(session); + return; + } aggregatePaySessionService.syncFromPayOrder(thirdPayOrderService.getByPayNo(session.getPayNo())); } @@ -671,7 +693,8 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay * 新建支付订单前确认已过期会话绑定的第三方子订单不再处于可支付状态。 * *后台补偿超过十分钟后会停止,但创建新订单时仍会同步执行一次查单和关单;只有旧子订单已经成功、 - * 关闭、失败或不存在时才允许继续,避免旧单与新单同时可支付。
+ * 关闭、失败或不存在时才允许继续,避免旧单与新单同时可支付。创建已满一小时的会话只做本地关闭, + * 不再通过该入口访问平台或恢复会话状态。 * * @param session 同业务最新聚合会话,允许为空或非过期状态 */ @@ -688,7 +711,7 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay ThirdAggregatePaySessionEntity latestSession = aggregatePaySessionService.getById(session.getId()); ThirdPayOrderEntity payOrder = StrUtil.isBlank(session.getPayNo()) ? null : thirdPayOrderService.getByPayNo(session.getPayNo()); - if (payOrder != null) { + if (payOrder != null && !isBeyondAutoProcessingWindow(session)) { aggregatePaySessionService.syncFromPayOrder(payOrder); } if ((latestSession != null && ObjectUtil.equals(latestSession.getStatus(), @@ -954,6 +977,43 @@ public class ThirdAggregateOrderPayServiceImpl implements ThirdAggregateOrderPay return session == null || session.getExpireTime() == null || !session.getExpireTime().isAfter(LocalDateTime.now()); } + /** + * 判断聚合会话是否已经越过服务端自动处理硬截止。 + * + *创建时间缺失的异常记录同样按越过截止处理,避免任何入口对无法确定年龄的订单持续查单。
+ * + * @param session 聚合支付会话 + * @return 创建时间为空或创建已满一小时时返回 {@code true} + */ + private boolean isBeyondAutoProcessingWindow(ThirdAggregatePaySessionEntity session) { + LocalDateTime cutoff = LocalDateTime.now() + .minusMinutes(ThirdPayPollingCoordinator.MAX_AUTO_PROCESSING_MINUTES); + return session == null || session.getCreateTime() == null || !session.getCreateTime().isAfter(cutoff); + } + + /** + * 在自动处理硬截止后仅将聚合会话及其未支付子订单标记为关闭。 + * + *先关闭会话可阻止随后到达的子订单同步重新激活会话;子订单更新带未支付条件,不覆盖已经落库的 + * 真实成功状态。该方法不调用支付宝或微信接口。
+ * + * @param session 已越过自动处理硬截止的活动或过期会话 + */ + private void closeHistoricalSessionLocally(ThirdAggregatePaySessionEntity session) { + aggregatePaySessionService.markTerminal(session.getId(), ThirdAggregatePaySessionStatusEnum.PAY_CLOSED); + if (StrUtil.isBlank(session.getPayNo())) { + return; + } + ThirdPayOrderEntity payOrder = thirdPayOrderService.getByPayNo(session.getPayNo()); + if (payOrder == null) { + return; + } + thirdPayOrderService.update(new LambdaUpdateWrapper