diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java index 6dfa8df31..18d4d96a5 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelDownstreamChannelConfigDTO.java @@ -95,9 +95,15 @@ public class SmHotelDownstreamChannelConfigDTO { @Schema(description = "推送流水保留天数,默认 15;≤0 不清理") private Integer pushLogRetainDays; - @Schema(description = "是否参与资源日更定时推送;false=仅手动/变更推,默认 false") + @Schema(description = "是否参与资源定时推送;false=仅手动/变更推,默认 false") private Boolean resourcePushJoinSchedule; + @Schema(description = "资源定时推:每天最多启动次数,默认 1") + private Integer resourcePushTimesPerDay; + + @Schema(description = "资源定时推:两次最小间隔小时,默认 12;≤0 不校验间隔") + private Integer resourcePushMinIntervalHours; + @Schema(description = "是否参与近窗详情定时;false=仅手动近窗刷新,默认 false") private Boolean nearDetailPullJoinSchedule; diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelShangyouChannelConfigDTO.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelShangyouChannelConfigDTO.java index 6295fa663..b2319bd40 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelShangyouChannelConfigDTO.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/dto/SmHotelShangyouChannelConfigDTO.java @@ -82,11 +82,32 @@ public class SmHotelShangyouChannelConfigDTO { private String detailPullDailyCron; + /** 全量详情前是否先确保今日列表同步成功;默认 true */ + private Boolean detailPullEnsureHotelListFirst; + + /** + * 全量+范围共用:每天最多几次(上游文档配额)。 + * 与 fullPriceSyncMaxPerDay 同义;YAML 写 detail-pull-times-per-day 即可。 + */ + private Integer detailPullTimesPerDay; + + /** + * 全量+范围共用:两次最小间隔小时。 + * 与 fullPriceSyncMinIntervalHours 同义;YAML 写 detail-pull-min-interval-hours 即可。 + */ + private Integer detailPullMinIntervalHours; + /** 渠道范围合集全量详情定时(各下游范围并集,跳过本地) */ private Boolean scopeDetailPullDailyEnabled; private String scopeDetailPullDailyCron; + /** 范围合集定时:每天最多启动次数(另受上游共享配额) */ + private Integer scopeDetailPullTimesPerDay; + + /** 范围合集定时:两次启动最小间隔小时 */ + private Integer scopeDetailPullMinIntervalHours; + /** 近窗详情调度总开关(扫描 join 伙伴;不占全量配额) */ private Boolean nearDetailPullEnabled; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java index c8b8a317e..2b168e675 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/HotelDownstreamProperties.java @@ -156,15 +156,29 @@ public class HotelDownstreamProperties { private int resourceAutoPushDebounceSeconds = 90; /** - * 是否参与资源日更定时推送(授权范围全量:可售/关房/跳过)。 - * false(默认):不进日更;确认范围与限流后再在伙伴 YAML 开 true。 + * 是否参与资源定时推送(授权范围全量:可售/关房/跳过)。 + * false(默认):不进定时推;确认范围与限流后再在伙伴 YAML 开 true。 + * 推荐与近窗拆开:近窗/范围只负责拉上游,本开关负责推下游。 */ private boolean resourcePushJoinSchedule = false; + /** + * 本伙伴资源定时推:每天最多启动次数。例:2=一天推两趟。 + * 仅 join-schedule=true 时生效;≤0 按 1。 + */ + private int resourcePushTimesPerDay = 1; + + /** + * 本伙伴资源定时推:两次启动最小间隔小时数。例:12=至少隔 12 小时。 + * ≤0 表示不校验间隔(仍受 times-per-day 限制)。 + */ + private int resourcePushMinIntervalHours = 12; + /** * 是否参与「近窗详情」定时刷新(只刷本伙伴授权范围内、未来 N 天房价;不占全量配额)。 * false(默认):不进近窗定时,可 PC 手动「近窗刷新」; * true:全局近窗时钟到点后,按 {@link #nearDetailPullIntervalHours} 判断是否该刷这一家。 + * 推荐:只拉上游;推送用 resource-push-join-schedule / resource-auto-push。 */ private boolean nearDetailPullJoinSchedule = false; @@ -181,16 +195,19 @@ public class HotelDownstreamProperties { /** * 近窗详情是否边拉边推:单店拉上游成功后立刻推本伙伴(拉一家推一家)。 - * 仅对「单伙伴近窗」任务生效;开放应用(拉模式)请保持 false。 + * 上万店时很慢(并发强制 1);推荐 false,改为「近窗只拉 + 资源定时推」。 * false(默认):只刷本地。 */ private boolean nearDetailPullThenPush = false; - /** 进程级:是否跑资源日更时钟;改 cron 需重启 */ + /** 进程级:是否跑资源推送扫描时钟;改 cron 需重启 */ private boolean resourcePushScheduleEnabled = true; - /** 资源日更 cron(默认每天 02:30) */ - private String resourcePushCron = "0 30 2 * * ?"; + /** + * 资源推送扫描时钟 cron(默认每小时 :30 扫一轮 join 伙伴;是否真推看各家 times/interval)。 + * 正式环境优先改 Quartz sys_job,本地 Spring 兜底才读本项。 + */ + private String resourcePushCron = "0 30 * * * ?"; /** * 下游全量推酒店并发。1=串行。 diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java index 078faf4af..af35ee677 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/config/ShangyouHotelProperties.java @@ -172,28 +172,48 @@ public class ShangyouHotelProperties { private boolean localDailyRollEnabled = true; /** - * 上游渠道「全量详情」定时开关(拉该渠道全部启用酒店;受 full-price-sync 次数/间隔限制)。 + * 上游渠道「全量详情」定时开关(拉该渠道全部启用酒店)。 + * 真正到点是否开跑还受:先列表成功、每日次数、最小间隔、上游 full-price 配额。 * 默认关闭,按需在渠道配置打开。 */ private boolean detailPullDailyEnabled = false; /** - * 全量详情 Cron(仅 spring-schedule-enabled=true 时生效)。 - * 默认每天 04:00:0 0 4 * * ?(注意与上游 6 小时间隔、每日次数上限) + * 全量详情调度时钟 Cron(仅本地 Spring 兜底;正式走 Quartz 同名任务)。 + * 默认每小时扫一次:到点只判断「该不该跑」,不是「一到就必跑满配额」。 + * Quartz 侧默认同样按小时扫,次数/间隔见 full-price-sync-* / detail-pull-times-*。 */ - private String detailPullDailyCron = "0 0 4 * * ?"; + private String detailPullDailyCron = "0 0 * * * ?"; /** - * 「渠道范围合集」全量详情定时开关:只拉全部下游范围映射并集中的上游酒店(跳过本地)。 + * 全量详情定时:是否要求「今日已有成功的酒店列表同步」才开跑。 + * true(默认):没有则先启动列表同步,本轮详情跳过,下一时钟再试。 + */ + private boolean detailPullEnsureHotelListFirst = true; + + /** + * 「渠道范围合集」详情定时开关:只拉全部下游范围映射并集中的上游酒店(跳过本地)。 + * 与全量详情分开开关;建议生产开这个、全量详情保持关(范围更小、更贴下游)。 * 默认关闭。 */ private boolean scopeDetailPullDailyEnabled = false; /** - * 渠道范围合集详情 Cron(仅 spring-schedule-enabled=true 时生效)。 - * 默认每天 05:00:0 0 5 * * ? + * 范围合集详情调度时钟 Cron(仅本地 Spring 兜底;正式走 Quartz)。 + * 默认每小时 :20 扫一次;真正间隔/次数见 scope-detail-pull-times-per-day 等。 */ - private String scopeDetailPullDailyCron = "0 0 5 * * ?"; + private String scopeDetailPullDailyCron = "0 20 * * * ?"; + + /** + * 范围合集定时:每天最多启动次数(另受下方上游 full-price 共享配额)。 + * 例:2=一天最多开两趟范围详情。 + */ + private int scopeDetailPullTimesPerDay = 1; + + /** + * 范围合集定时:两次启动最小间隔小时数。例:6=至少隔 6 小时。 + */ + private int scopeDetailPullMinIntervalHours = 6; /** * 近窗详情定时总开关:到点后扫描「near-detail-pull-join-schedule=true」的下游伙伴, @@ -215,10 +235,16 @@ public class ShangyouHotelProperties { */ private String nearDetailPullCron = "0 */15 * * * ?"; - /** 文档:全量房价房量每天最多次数 */ + /** + * 上游文档:全量房价房量每天最多次数(全量详情 + 范围合集详情共用;近窗不占)。 + * YAML 也可写 detail-pull-times-per-day(同义覆盖本字段)。 + */ private int fullPriceSyncMaxPerDay = 3; - /** 文档:全量房价两次间隔最少小时数 */ + /** + * 上游文档:全量房价两次间隔最少小时数(全量 + 范围合集共用;近窗不占)。 + * YAML 也可写 detail-pull-min-interval-hours(同义覆盖本字段)。 + */ private int fullPriceSyncMinIntervalHours = 6; /** 全量拉详情:每家酒店间隔毫秒(防打爆上游;并行时主要靠 price-api-qps,默认 0) */ diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java index a1ed94972..9d12e3555 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/integration/shangyou/ShangyouHotelApi.java @@ -65,12 +65,18 @@ public final class ShangyouHotelApi { /** 列表同步/差集导入所选数据来源(字典值) */ public static final String REDIS_IMPORT_DATA_SOURCE = "supermarket:shangyou:hotel:import:dataSource"; - /** 今日全量房价同步次数 */ + /** 今日全量房价同步次数(全量详情+范围合集共用上游配额) */ public static final String REDIS_FULL_PRICE_SYNC_COUNT = "supermarket:shangyou:hotel:fullprice:count"; /** 上次全量房价同步时间 epochMs */ public static final String REDIS_FULL_PRICE_SYNC_LAST = "supermarket:shangyou:hotel:fullprice:last"; + /** 范围合集定时:今日已启动次数(业务节奏,另受 fullprice 配额) */ + public static final String REDIS_SCOPE_DETAIL_SCHED_COUNT = "supermarket:shangyou:hotel:scope-detail:sched:count"; + + /** 范围合集定时:上次启动 epochMs */ + public static final String REDIS_SCOPE_DETAIL_SCHED_LAST = "supermarket:shangyou:hotel:scope-detail:sched:last"; + /** 房价接口 QPS 秒窗计数前缀,完整 key = 前缀 + ":" + epochSecond */ public static final String REDIS_PRICE_QPS_PREFIX = "supermarket:shangyou:hotel:price:qps"; } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java index e7e759d58..2dea75bf7 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelChannelConfigServiceImpl.java @@ -394,6 +394,12 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl 0) { + target.setResourcePushTimesPerDay(dto.getResourcePushTimesPerDay()); + } + if (dto.getResourcePushMinIntervalHours() != null) { + target.setResourcePushMinIntervalHours(dto.getResourcePushMinIntervalHours()); + } if (dto.getNearDetailPullJoinSchedule() != null) { target.setNearDetailPullJoinSchedule(dto.getNearDetailPullJoinSchedule()); } @@ -514,12 +520,27 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl 0) { + target.setFullPriceSyncMaxPerDay(dto.getDetailPullTimesPerDay()); + } + if (dto.getDetailPullMinIntervalHours() != null && dto.getDetailPullMinIntervalHours() >= 0) { + target.setFullPriceSyncMinIntervalHours(dto.getDetailPullMinIntervalHours()); + } if (dto.getScopeDetailPullDailyEnabled() != null) { target.setScopeDetailPullDailyEnabled(dto.getScopeDetailPullDailyEnabled()); } if (StrUtil.isNotBlank(dto.getScopeDetailPullDailyCron())) { target.setScopeDetailPullDailyCron(dto.getScopeDetailPullDailyCron().trim()); } + if (dto.getScopeDetailPullTimesPerDay() != null && dto.getScopeDetailPullTimesPerDay() > 0) { + target.setScopeDetailPullTimesPerDay(dto.getScopeDetailPullTimesPerDay()); + } + if (dto.getScopeDetailPullMinIntervalHours() != null && dto.getScopeDetailPullMinIntervalHours() >= 0) { + target.setScopeDetailPullMinIntervalHours(dto.getScopeDetailPullMinIntervalHours()); + } if (dto.getNearDetailPullEnabled() != null) { target.setNearDetailPullEnabled(dto.getNearDetailPullEnabled()); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java index f548a66ab..56b05b429 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java @@ -25,6 +25,7 @@ import com.bsd.pjcloud.supermarket.service.SmHotelSyncService; import jakarta.annotation.PreDestroy; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Lazy; +import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -47,6 +48,12 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu private static final String TAG = "下游自动推送"; + /** 资源定时推:各伙伴上次启动 epochMs */ + private static final String RESOURCE_PUSH_LAST_KEY = "sm:hotel:resource-push:last:"; + + /** 资源定时推:各伙伴今日启动次数 */ + private static final String RESOURCE_PUSH_COUNT_KEY = "sm:hotel:resource-push:count:"; + private final HotelDownstreamProperties yamlDefaults; private final SmHotelChannelConfigService channelConfigService; @@ -63,10 +70,13 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu private final SmHotelDownstreamPushLogService downstreamPushLogService; + private final StringRedisTemplate redisTemplate; + public SmHotelDownstreamAutoPushServiceImpl(HotelDownstreamProperties yamlDefaults, SmHotelChannelConfigService channelConfigService, SmHotelChannelDeptService channelDeptService, SmHotelMapper hotelMapper, SmHotelDailyMapper dailyMapper, @Lazy SmHotelMidService midService, - @Lazy SmHotelSyncService syncService, SmHotelDownstreamPushLogService downstreamPushLogService) { + @Lazy SmHotelSyncService syncService, SmHotelDownstreamPushLogService downstreamPushLogService, + StringRedisTemplate redisTemplate) { this.yamlDefaults = yamlDefaults; this.channelConfigService = channelConfigService; this.channelDeptService = channelDeptService; @@ -75,6 +85,7 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu this.midService = midService; this.syncService = syncService; this.downstreamPushLogService = downstreamPushLogService; + this.redisTemplate = redisTemplate; } private final ConcurrentHashMap> pendingByHotel = new ConcurrentHashMap<>(); @@ -272,12 +283,20 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu } for (String partner : partners) { try { + String dueErr = checkResourcePushScheduleDue(partner); + if (dueErr != null) { + log.debug("|{}|定时推跳过|partner={}|reason={}|", TAG, partner, dueErr); + continue; + } var started = syncService.startDownstreamScopePushAsync(partner, "SCHEDULE"); - log.info("|{}|日更已启动|partner={}|msg={}|", TAG, partner, + if (started != null && started.getCode() == 0) { + markResourcePushScheduleStarted(partner); + } + log.info("|{}|定时推已启动|partner={}|msg={}|", TAG, partner, started == null ? null : started.getMsg()); } catch (Exception e) { - log.warn("|{}|日更启动失败|partner={}|err={}|", TAG, partner, e.getMessage()); + log.warn("|{}|定时推启动失败|partner={}|err={}|", TAG, partner, e.getMessage()); } } }); @@ -296,6 +315,53 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu + "大批量补数可在该伙伴 YAML 临时设 resource-auto-push-enabled: false"; } + /** 伙伴资源定时推:次数 + 间隔未到则返回原因。 */ + private String checkResourcePushScheduleDue(String partner) { + HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(partner); + int times = ds == null ? 1 : Math.max(1, ds.getResourcePushTimesPerDay()); + int minHours = ds == null ? 12 : ds.getResourcePushMinIntervalHours(); + String last = redisTemplate.opsForValue().get(RESOURCE_PUSH_LAST_KEY + partner); + if (StrUtil.isNotBlank(last) && minHours > 0) { + try { + long lastMs = Long.parseLong(last.trim()); + long gap = System.currentTimeMillis() - lastMs; + if (gap < minHours * 3600_000L) { + long leftMin = (minHours * 3600_000L - gap) / 60_000L + 1; + return "not-due:距上次不足 " + minHours + "h,约再等 " + leftMin + " 分钟"; + } + } + catch (NumberFormatException ignored) { + // ignore + } + } + String countStr = redisTemplate.opsForValue().get(RESOURCE_PUSH_COUNT_KEY + partner); + int count = 0; + if (StrUtil.isNotBlank(countStr)) { + try { + count = Integer.parseInt(countStr.trim()); + } + catch (NumberFormatException ignored) { + count = 0; + } + } + if (count >= times) { + return "times-reached:今日已达 " + times + " 次"; + } + return null; + } + + private void markResourcePushScheduleStarted(String partner) { + if (StrUtil.isBlank(partner)) { + return; + } + redisTemplate.opsForValue().set(RESOURCE_PUSH_LAST_KEY + partner, + String.valueOf(System.currentTimeMillis()), 2, TimeUnit.DAYS); + Long c = redisTemplate.opsForValue().increment(RESOURCE_PUSH_COUNT_KEY + partner); + if (c != null && c == 1L) { + redisTemplate.expire(RESOURCE_PUSH_COUNT_KEY + partner, 1, TimeUnit.DAYS); + } + } + private void scheduleDebounced(Long hotelId) { Long tenantId = TenantContextHolder.getTenantId(); if (tenantId == null) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java index 3c808d09f..9f1031681 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java @@ -2920,6 +2920,68 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { done.setStageMsg(StrUtil.maxLength("完成,成功见 successCount,失败明细见 failDetail", 500)); } + private boolean hasRunningListSync() { + Long running = syncLogMapper.selectCount(Wrappers.lambdaQuery() + .eq(SmHotelSyncLogEntity::getSyncType, TYPE_LIST) + .eq(SmHotelSyncLogEntity::getStatus, STATUS_RUNNING)); + return running != null && running > 0; + } + + /** 今日是否已有成功的酒店列表同步(全量详情的前置条件)。 */ + private boolean hasSuccessfulListSyncToday() { + LocalDateTime start = LocalDate.now().atStartOfDay(); + Long ok = syncLogMapper.selectCount(Wrappers.lambdaQuery() + .eq(SmHotelSyncLogEntity::getSyncType, TYPE_LIST) + .eq(SmHotelSyncLogEntity::getStatus, STATUS_SUCCESS) + .ge(SmHotelSyncLogEntity::getCreateTime, start)); + return ok != null && ok > 0; + } + + /** + * 范围合集业务节奏:每天次数 + 最小间隔(与上游 full-price 配额分开记;真正开跑仍走 checkFullPriceSyncQuota)。 + */ + private String checkScopeDetailScheduleDue() { + int maxPerDay = Math.max(1, props().getScopeDetailPullTimesPerDay()); + int minHours = Math.max(0, props().getScopeDetailPullMinIntervalHours()); + String last = redisTemplate.opsForValue().get(ShangyouHotelApi.REDIS_SCOPE_DETAIL_SCHED_LAST); + if (StrUtil.isNotBlank(last) && minHours > 0) { + try { + long lastMs = Long.parseLong(last.trim()); + long gap = System.currentTimeMillis() - lastMs; + if (gap < minHours * 3600_000L) { + long leftMin = (minHours * 3600_000L - gap) / 60_000L + 1; + return "scope-detail-not-due:距上次不足 " + minHours + "h,约再等 " + leftMin + " 分钟"; + } + } + catch (NumberFormatException ignored) { + // ignore + } + } + String countStr = redisTemplate.opsForValue().get(ShangyouHotelApi.REDIS_SCOPE_DETAIL_SCHED_COUNT); + int count = 0; + if (StrUtil.isNotBlank(countStr)) { + try { + count = Integer.parseInt(countStr.trim()); + } + catch (NumberFormatException ignored) { + count = 0; + } + } + if (count >= maxPerDay) { + return "scope-detail-times-reached:今日已达 " + maxPerDay + " 次"; + } + return null; + } + + private void markScopeDetailScheduleStarted() { + redisTemplate.opsForValue().set(ShangyouHotelApi.REDIS_SCOPE_DETAIL_SCHED_LAST, + String.valueOf(System.currentTimeMillis()), 2, TimeUnit.DAYS); + Long c = redisTemplate.opsForValue().increment(ShangyouHotelApi.REDIS_SCOPE_DETAIL_SCHED_COUNT); + if (c != null && c == 1L) { + redisTemplate.expire(ShangyouHotelApi.REDIS_SCOPE_DETAIL_SCHED_COUNT, 1, TimeUnit.DAYS); + } + } + /** 文档限制:全量房价每天次数 + 最小间隔 */ private String checkFullPriceSyncQuota() { int maxPerDay = Math.max(1, props().getFullPriceSyncMaxPerDay()); @@ -3314,6 +3376,29 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { return Map.of("skipped", true, "reason", "detail-pull-daily-enabled=false"); } String ds = StrUtil.blankToDefault(props().getDefaultImportDataSource(), SmHotelDataSource.SHANGYOU); + // 全量详情前提:先有今日成功的酒店列表;否则本轮先拉列表,详情等下一时钟 + if (props().isDetailPullEnsureHotelListFirst()) { + if (hasRunningListSync()) { + return Map.of("skipped", true, "reason", "hotel-list-busy"); + } + if (!hasSuccessfulListSyncToday()) { + if (!props().isHotelListDailySyncEnabled()) { + return Map.of("skipped", true, "reason", + "ensure-hotel-list-first 但 hotel-list-daily-sync-enabled=false,且今日无成功列表"); + } + R listR = startListSyncAsync(props().isAutoImportNewHotels(), ds, + StrUtil.blankToDefault(triggerType, "XXL")); + Map m = new LinkedHashMap<>(8); + m.put("skipped", true); + m.put("reason", "ensure-hotel-list-first"); + m.put("listStarted", listR != null && listR.getCode() == 0); + m.put("listMsg", listR == null ? "null" : listR.getMsg()); + if (listR != null && listR.getCode() == 0 && listR.getData() != null) { + m.put("listLogId", listR.getData().getId()); + } + return m; + } + } R r = startDetailPullAsync(null, StrUtil.blankToDefault(triggerType, "XXL"), ds); if (r.getCode() != 0) { return Map.of("ok", false, "msg", r.getMsg()); @@ -3334,10 +3419,15 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { if (!props().isScopeDetailPullDailyEnabled()) { return Map.of("skipped", true, "reason", "scope-detail-pull-daily-enabled=false"); } + String dueErr = checkScopeDetailScheduleDue(); + if (dueErr != null) { + return Map.of("skipped", true, "reason", dueErr); + } R r = startScopeUnionDetailPullAsync(StrUtil.blankToDefault(triggerType, "XXL")); if (r.getCode() != 0) { return Map.of("ok", false, "msg", r.getMsg()); } + markScopeDetailScheduleStarted(); return Map.of("ok", true, "async", true, "logId", r.getData().getId()); }); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/resources/application.yml b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/resources/application.yml index 57235ebc3..8dad33174 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/resources/application.yml +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/resources/application.yml @@ -167,7 +167,7 @@ spring: # resource-auto-push-enabled: true # 本地/回调变更后防抖自动推 # resource-auto-push-debounce-seconds: 90 # resource-push-schedule-enabled: true # 日更总开关 -# resource-push-cron: 0 30 2 * * ? # 日更时钟;伙伴 YAML 另开 resource-push-join-schedule +# resource-push-cron: 0 30 * * * ? # 资源推扫描时钟(每小时);伙伴 join + times/interval # # --- 推送流水(伙伴 YAML 可覆盖 mode/截断/保留天)--- # push-log-mode: fail_only # fail_only|sample|all # push-log-sample-success-max: 20