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 e6d2d5589..c89194389 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 @@ -98,6 +98,15 @@ public class SmHotelDownstreamChannelConfigDTO { @Schema(description = "是否参与资源日更定时推送;false=仅手动/变更推,默认 false") private Boolean resourcePushJoinSchedule; + @Schema(description = "是否参与近窗详情定时;false=仅手动近窗刷新,默认 false") + private Boolean nearDetailPullJoinSchedule; + + @Schema(description = "本伙伴近窗刷新间隔小时;例 2 / 24 / 1;≤0 按 2") + private Integer nearDetailPullIntervalHours; + + @Schema(description = "本伙伴近窗天数;≤0 用上游 near-detail-pull-days") + private Integer nearDetailPullDays; + @Schema(description = "本地改价/库存/房态后是否自动推本伙伴;大批量补数可临时 false,默认 true(未配则跟进程级)") private Boolean resourceAutoPushEnabled; } 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 9d94c2509..dd88b3136 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 @@ -87,12 +87,13 @@ public class SmHotelShangyouChannelConfigDTO { private String scopeDetailPullDailyCron; - /** 近窗详情定时(范围合集未来 N 天;不占全量配额) */ + /** 近窗详情调度总开关(扫描 join 伙伴;不占全量配额) */ private Boolean nearDetailPullEnabled; - /** 近窗天数,建议 7 或 15 */ + /** 近窗天数默认值(伙伴未配时用),建议 7 或 15 */ private Integer nearDetailPullDays; + /** 近窗调度时钟;真正间隔看各下游 near-detail-pull-interval-hours */ private String nearDetailPullCron; private Boolean autoImportNewHotels; 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 917cc7a89..f4fe5f9eb 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 @@ -146,6 +146,24 @@ public class HotelDownstreamProperties { */ private boolean resourcePushJoinSchedule = false; + /** + * 是否参与「近窗详情」定时刷新(只刷本伙伴授权范围内、未来 N 天房价;不占全量配额)。 + * false(默认):不进近窗定时,可 PC 手动「近窗刷新」; + * true:全局近窗时钟到点后,按 {@link #nearDetailPullIntervalHours} 判断是否该刷这一家。 + */ + private boolean nearDetailPullJoinSchedule = false; + + /** + * 本伙伴近窗刷新最小间隔(小时)。例:2=约两小时一次;24=一天一次;1=一小时一次。 + * 仅当 {@link #nearDetailPullJoinSchedule}=true 时生效;≤0 时按 2。 + */ + private int nearDetailPullIntervalHours = 2; + + /** + * 本伙伴近窗天数;≤0 则用上游商游配置 near-detail-pull-days(默认 15)。 + */ + private int nearDetailPullDays = 0; + /** 进程级:是否跑资源日更时钟;改 cron 需重启 */ private boolean resourcePushScheduleEnabled = true; 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 2a2c4bc35..27c459a4f 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 @@ -196,22 +196,24 @@ public class ShangyouHotelProperties { private String scopeDetailPullDailyCron = "0 0 5 * * ?"; /** - * 近窗详情定时:只刷渠道范围合集「未来 N 天」房价(保持可售近窗新鲜;不占全量 6h/每日次数配额)。 - * 默认开启。 + * 近窗详情定时总开关:到点后扫描「near-detail-pull-join-schedule=true」的下游伙伴, + * 按各家 intervalHours 决定是否刷其授权范围近窗(不占全量 6h/每日次数配额)。 + * 默认开启;无伙伴 join 时任务空跑。 */ private boolean nearDetailPullEnabled = true; /** - * 近窗天数:建议 7 或 15(≤30 通常只需 1 次 getRoomPrice/店)。 + * 近窗天数默认值:伙伴 YAML 未配 near-detail-pull-days 时使用。建议 7 或 15。 * 默认 15。 */ private int nearDetailPullDays = 15; /** - * 近窗详情 Cron(仅 spring-schedule-enabled=true 时生效)。 - * 默认每 2 小时: + * 近窗调度时钟 Cron(仅 spring-schedule-enabled=true 时生效)。 + * 到点只「扫一轮」到期伙伴,真正间隔由各下游 near-detail-pull-interval-hours 控制。 + * 默认每 15 分钟扫一次: */ - private String nearDetailPullCron = "0 0 */2 * * ?"; + private String nearDetailPullCron = "0 */15 * * * ?"; /** 文档:全量房价房量每天最多次数 */ private int fullPriceSyncMaxPerDay = 3; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java index 473c4e8e2..9b35cbddb 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/job/SmHotelShangyouSpringSchedule.java @@ -124,7 +124,7 @@ public class SmHotelShangyouSpringSchedule { } } - @Scheduled(cron = "${shangyou-hotel.near-detail-pull-cron:0 0 */2 * * ?}") + @Scheduled(cron = "${shangyou-hotel.near-detail-pull-cron:0 */15 * * * ?}") public void scheduledNearDetailPull() { ShangyouHotelProperties properties = props(); if (!properties.isSpringScheduleEnabled()) { @@ -134,8 +134,8 @@ public class SmHotelShangyouSpringSchedule { return; } try { - log.info("|酒店日更|Spring Cron 触发近窗详情|days={}|cron={}|", properties.getNearDetailPullDays(), - properties.getNearDetailPullCron()); + log.info("|酒店日更|Spring Cron 扫近窗详情|defaultDays={}|cron={}|", + properties.getNearDetailPullDays(), properties.getNearDetailPullCron()); syncService.jobNearDetailPull("CRON"); } catch (Exception e) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelChannelConfigService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelChannelConfigService.java index 1bf1f5b3b..18b793cf4 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelChannelConfigService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelChannelConfigService.java @@ -41,6 +41,9 @@ public interface SmHotelChannelConfigService extends IService listResourcePushSchedulePartners(); + /** 参与近窗详情定时的启用伙伴(near-detail-pull-join-schedule=true) */ + List listNearDetailPullSchedulePartners(); + /** 渠道是否在库表中启用(无行时回落 YAML enabled;默认按上游) */ boolean isChannelEnabled(String channel); 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 dcfe477f4..97f7bb01b 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 @@ -197,6 +197,14 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl listNearDetailPullSchedulePartners() { + return listEnabledDownstreamPartners().stream().filter(code -> { + HotelDownstreamProperties props = resolveDownstreamRuntime(code); + return props != null && props.isEnabled() && props.isNearDetailPullJoinSchedule(); + }).toList(); + } + @Override public boolean isChannelEnabled(String channel) { return isChannelEnabled(channel, SmHotelChannelDirection.UPSTREAM); @@ -386,6 +394,15 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl 0) { + return Math.min(90, daysOverride); + } + if (StrUtil.isNotBlank(partnerCode) && !SCOPE_UNION_DS.equals(partnerCode)) { + HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(partnerCode); + if (ds != null && ds.getNearDetailPullDays() > 0) { + return Math.min(90, ds.getNearDetailPullDays()); + } + } + return resolveNearDetailDays(null); + } + + private int resolveNearIntervalHours(HotelDownstreamProperties ds) { + if (ds == null || ds.getNearDetailPullIntervalHours() <= 0) { + return 2; + } + return Math.min(24 * 7, ds.getNearDetailPullIntervalHours()); + } + + private boolean isNearPartnerDue(String partnerCode, int intervalHours) { + String raw = redisTemplate.opsForValue().get(NEAR_LAST_KEY + partnerCode); + if (StrUtil.isBlank(raw)) { + return true; + } + try { + long lastMs = Long.parseLong(raw.trim()); + long gapMs = Math.max(1, intervalHours) * 3600_000L; + return System.currentTimeMillis() - lastMs >= gapMs; + } + catch (Exception e) { + return true; + } + } + + private void markNearPartnerStarted(String partnerCode) { + if (StrUtil.isBlank(partnerCode)) { + return; + } + redisTemplate.opsForValue().set(NEAR_LAST_KEY + partnerCode, + String.valueOf(System.currentTimeMillis()), 14, TimeUnit.DAYS); + } + /** 解析范围详情任务对应的上游可拉酒店 ID(续跑用)。 */ private List resolveScopeDetailHotelIds(String dataSourceLabel) { String ds = SmHotelDataSource.normalize(dataSourceLabel).toLowerCase(); @@ -2844,12 +2898,17 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { m.put("nearDetailPullEnabled", props().isNearDetailPullEnabled()); m.put("nearDetailPullDays", props().getNearDetailPullDays()); m.put("nearDetailPullCron", props().getNearDetailPullCron()); + List nearPartners = channelConfigService.listNearDetailPullSchedulePartners(); + m.put("nearDetailPullJoinPartners", nearPartners); + m.put("nearDetailPullJoinPartnerCount", nearPartners.size()); m.put("fullPriceSyncMaxPerDay", props().getFullPriceSyncMaxPerDay()); m.put("fullPriceSyncMinIntervalHours", props().getFullPriceSyncMinIntervalHours()); m.put("autoImportNewHotels", props().isAutoImportNewHotels()); m.put("autoMarkOfflineHotels", props().isAutoMarkOfflineHotels()); - m.put("hint", "近窗详情 near-detail-pull-* 刷渠道范围未来 " - + props().getNearDetailPullDays() + " 天(不占全量配额);" + m.put("hint", "近窗详情:时钟 " + + props().getNearDetailPullCron() + " 扫描 join 伙伴(当前 " + nearPartners.size() + + " 家),各家 intervalHours 独立;默认天数 " + + props().getNearDetailPullDays() + "(不占全量配额);" + "全量/范围详情共用上游 " + props().getFullPriceSyncMinIntervalHours() + "h 与每日" + props().getFullPriceSyncMaxPerDay() + " 次;" + "XXL:smHotelNearDetailPullJobHandler"); @@ -2970,13 +3029,59 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { if (!props().isNearDetailPullEnabled()) { return Map.of("skipped", true, "reason", "near-detail-pull-enabled=false"); } - R r = startNearScopeDetailPullAsync(null, null, + List joinPartners = channelConfigService.listNearDetailPullSchedulePartners(); + if (CollUtil.isEmpty(joinPartners)) { + return Map.of("skipped", true, "reason", + "无伙伴开启 near-detail-pull-join-schedule(请在下游渠道 YAML 按家配置)"); + } + if (hasRunningNearDetailPull()) { + return Map.of("skipped", true, "reason", "near-detail-pull-busy"); + } + if (hasRunningHeavyDetailPull()) { + return Map.of("skipped", true, "reason", "heavy-detail-pull-busy"); + } + List due = new ArrayList<>(); + Map intervals = new LinkedHashMap<>(); + for (String code : joinPartners) { + HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(code); + int hours = resolveNearIntervalHours(ds); + intervals.put(code, hours); + if (isNearPartnerDue(code, hours)) { + due.add(code); + } + } + if (due.isEmpty()) { + Map m = new LinkedHashMap<>(6); + m.put("skipped", true); + m.put("reason", "no-partner-due"); + m.put("joinPartners", joinPartners); + m.put("intervals", intervals); + return m; + } + // 近窗任务互斥:每轮只提交一家到期伙伴,其余等下一拍时钟 + String partner = due.get(0); + int nearDays = resolveNearDetailDaysForPartner(partner, null); + R r = startNearScopeDetailPullAsync(partner, nearDays, StrUtil.blankToDefault(triggerType, "XXL")); if (r.getCode() != 0) { - return Map.of("ok", false, "msg", r.getMsg()); + Map fail = new LinkedHashMap<>(6); + fail.put("ok", false); + fail.put("msg", r.getMsg()); + fail.put("partner", partner); + fail.put("duePartners", due); + return fail; } - return Map.of("ok", true, "async", true, "logId", r.getData().getId(), "nearDays", - resolveNearDetailDays(null)); + markNearPartnerStarted(partner); + Map ok = new LinkedHashMap<>(10); + ok.put("ok", true); + ok.put("async", true); + ok.put("partner", partner); + ok.put("logId", r.getData().getId()); + ok.put("nearDays", nearDays); + ok.put("intervalHours", intervals.get(partner)); + ok.put("duePartners", due); + ok.put("remainDue", due.size() - 1); + return ok; }); } }