This commit is contained in:
zjf committed 2026-11-09 18:21:37 +08:00
1 parent 739c34ca28
commit fdd33d3e0a
8 files changed
+174 -19

No files matched your search

@@ -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;
}
@@ -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;
@@ -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;
@@ -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;
@@ -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) {
@@ -41,6 +41,9 @@ public interface SmHotelChannelConfigService extends IService<SmHotelChannelConf
/** 参与资源日更定时推送的启用伙伴(resource-push-join-schedule=true) */
List<String> listResourcePushSchedulePartners();
/** 参与近窗详情定时的启用伙伴(near-detail-pull-join-schedule=true) */
List<String> listNearDetailPullSchedulePartners();
/** 渠道是否在库表中启用(无行时回落 YAML enabled;默认按上游) */
boolean isChannelEnabled(String channel);
@@ -197,6 +197,14 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl<SmHotelChannelC
}).toList();
}
@Override
public List<String> 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<SmHotelChannelC
if (dto.getResourcePushJoinSchedule() != null) {
target.setResourcePushJoinSchedule(dto.getResourcePushJoinSchedule());
}
if (dto.getNearDetailPullJoinSchedule() != null) {
target.setNearDetailPullJoinSchedule(dto.getNearDetailPullJoinSchedule());
}
if (dto.getNearDetailPullIntervalHours() != null) {
target.setNearDetailPullIntervalHours(dto.getNearDetailPullIntervalHours());
}
if (dto.getNearDetailPullDays() != null) {
target.setNearDetailPullDays(dto.getNearDetailPullDays());
}
if (dto.getResourceAutoPushEnabled() != null) {
target.setResourceAutoPushEnabled(dto.getResourceAutoPushEnabled());
}
@@ -76,6 +76,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
/** 范围合集详情任务在同步日志 dataSource 上的标记 */
private static final String SCOPE_UNION_DS = "scope_union";
/** 近窗定时:各下游上次提交时间 Redis Key 前缀 */
private static final String NEAR_LAST_KEY = "sm:hotel:near:last:";
/** 按下游范围映射全量推送 */
private static final String TYPE_DOWNSTREAM_PUSH = "DOWNSTREAM_PUSH";
@@ -324,7 +327,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
dsLabel = partner;
scopeLabel = "渠道「" + partner + "」范围";
}
int nearDays = resolveNearDetailDays(days);
int nearDays = resolveNearDetailDaysForPartner(dsLabel, days);
return startNearScopeDetailPullInternal(raw, dsLabel, scopeLabel, nearDays, triggerType);
}
@@ -349,7 +352,14 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
data.put("rawHotelCount", rawCount);
data.put("hotelCount", pullCount);
data.put("localSkipped", Math.max(0, rawCount - pullCount));
data.put("nearDetailPullDays", resolveNearDetailDays(null));
data.put("nearDetailPullDays", resolveNearDetailDaysForPartner(label, null));
if (StrUtil.isNotBlank(label) && !SCOPE_UNION_DS.equals(label)) {
HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(label);
if (ds != null) {
data.put("nearDetailPullJoinSchedule", ds.isNearDetailPullJoinSchedule());
data.put("nearDetailPullIntervalHours", resolveNearIntervalHours(ds));
}
}
return data;
}
@@ -2248,6 +2258,50 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
return Math.min(90, configured);
}
/** 伙伴 YAML 近窗天数优先,否则用上游默认。 */
private int resolveNearDetailDaysForPartner(String partnerCode, Integer daysOverride) {
if (daysOverride != null && daysOverride > 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<Long> 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<String> 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<SmHotelSyncLogEntity> r = startNearScopeDetailPullAsync(null, null,
List<String> 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<String> due = new ArrayList<>();
Map<String, Integer> 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<String, Object> 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<SmHotelSyncLogEntity> r = startNearScopeDetailPullAsync(partner, nearDays,
StrUtil.blankToDefault(triggerType, "XXL"));
if (r.getCode() != 0) {
return Map.of("ok", false, "msg", r.getMsg());
Map<String, Object> 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<String, Object> 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;
});
}
}