diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamPushKind.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamPushKind.java index 3c03f41d..f80760c0 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamPushKind.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/constant/SmHotelDownstreamPushKind.java @@ -3,16 +3,24 @@ package com.bsd.pjcloud.supermarket.constant; /** * 下游资源推送分类(环启等):可售推 / 关房推 / 纯跳过。 * 见 docs/xiayou/环启下游对接总览.md §5.1 + *

+ * 圣诺特例:有 eid 后禁止因无产品/无日历 SKIP,应走 CLOSE(pushHotelClose)清对方旧价。 */ public final class SmHotelDownstreamPushKind { /** 上架且有产品且窗口内有日历 → 正常推本地价态 */ public static final String SELL = "SELL"; - /** 仍有产品,但已下架或窗口内无日历 → 推窗口内 dailyStatus=0,清对方旧价 */ + /** + * 清对方旧价:下架、窗口无日历;圣诺在无产品时也走关房(对方可能仍有旧 plan)。 + * 环启等仍通常要求本地有产品才能组关房报文。 + */ public static final String CLOSE = "CLOSE"; - /** 本地已无产品 → 无法组关房报文,只记跳过 */ + /** + * 纯跳过:酒店不存在;环启等无产品无法组关房;圣诺无 eid。 + * 圣诺有 eid 时不应因无产品进入本态。 + */ public static final String SKIP = "SKIP"; private SmHotelDownstreamPushKind() { 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 078e3771..8e49918b 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 @@ -93,6 +93,12 @@ public class SmHotelShangyouChannelConfigDTO { private Integer detailPullIntervalMs; + /** 全量详情酒店并发;建议 4~8 */ + private Integer detailPullConcurrency; + + /** 全量详情房价天数;≤0 用 priceDays;建议 15~30 */ + private Integer detailPullPriceDays; + private Integer detailPullMaxHotels; private Integer diffScanBatchSize; diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelSyncLogEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelSyncLogEntity.java index 11768532..4980707c 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelSyncLogEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelSyncLogEntity.java @@ -27,6 +27,10 @@ public class SmHotelSyncLogEntity extends Model { @Schema(description = "MANUAL / XXL / CRON") private String triggerType; + /** 上游渠道 sheet_name,或下游全量推时的 partner_code;本地滚加为 local */ + @Schema(description = "渠道/数据来源") + private String dataSource; + @Schema(description = "RUNNING / SUCCESS / FAILED") private String status; 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 04f6d4ea..ceefd11d 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 @@ -177,8 +177,20 @@ public class ShangyouHotelProperties { /** 文档:全量房价两次间隔最少小时数 */ private int fullPriceSyncMinIntervalHours = 6; - /** 全量拉详情:每家酒店间隔毫秒(防打爆上游;落库已批量,默认 50) */ - private int detailPullIntervalMs = 50; + /** 全量拉详情:每家酒店间隔毫秒(防打爆上游;并行时主要靠 price-api-qps,默认 0) */ + private int detailPullIntervalMs = 0; + + /** + * 全量拉详情并发度(酒店级并行)。对方房价约 20 QPS,建议 4~8;1=串行旧行为。 + * 多实例时请降低,避免合计超限。 + */ + private int detailPullConcurrency = 6; + + /** + * 全量拉详情房价天数;≤0 则用 {@link #priceDays}。 + * 圣诺只推约 15 天时可配 15~30,比默认 90 天少 2/3 次 getRoomPrice。 + */ + private int detailPullPriceDays = 30; /** 全量拉详情单次最多处理家数;0=不限制(慎用,酒店多时耗时长) */ private int detailPullMaxHotels = 0; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java index 82752fba..1b4e65dd 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelController.java @@ -53,7 +53,7 @@ public class SmHotelController { .eq(StrUtil.isNotBlank(query.getSheetName()), SmHotelEntity::getSheetName, query.getSheetName()) // 归属部门:渠道映射按部门推送时,便于按部门筛酒店核查 .eq(query.getDeptId() != null, SmHotelEntity::getDeptId, query.getDeptId()) - .orderByDesc(SmHotelEntity::getCreateTime) + // 默认按主键倒序:雪花 ID 近似创建时间,可走索引;避免无索引时 create_time 全表 filesort .orderByDesc(SmHotelEntity::getId); // 外部酒店ID/eid:推圣诺 getInfo 用 sHotelId,空时回落 hotelCode if (StrUtil.isNotBlank(query.getSHotelId())) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java index f624faf9..0b54c537 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java @@ -149,10 +149,16 @@ public class SmHotelMidController { @GetMapping("/mid/pushScope/preview") @PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')") public R> previewPushScope(@RequestParam("partnerCode") String partnerCode) { - List ids = channelDeptService.listHotelIdsForDownstreamPartner(partnerCode); - Map data = new LinkedHashMap<>(4); + List raw = channelDeptService.listHotelIdsForDownstreamPartner(partnerCode); + int rawCount = raw == null ? 0 : raw.size(); + // 与全量推同一套过滤:圣诺未填 eid 的不计入可推数量 + List ids = syncService.filterDownstreamPushHotelIds(partnerCode, raw); + int pushCount = ids == null ? 0 : ids.size(); + Map data = new LinkedHashMap<>(6); data.put("partnerCode", partnerCode); - data.put("hotelCount", ids == null ? 0 : ids.size()); + data.put("hotelCount", pushCount); + data.put("rawHotelCount", rawCount); + data.put("excludedNoEid", Math.max(0, rawCount - pushCount)); return R.ok(data); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java index c94e74fe..47e7603d 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelMidService.java @@ -16,9 +16,14 @@ import java.util.Map; */ public interface SmHotelMidService { - /** 从上游拉取房型/产品并按加价生成日历,落本地库 */ + /** 从上游拉取房型/产品并按加价生成日历,落本地库(房价天数用配置 priceDays) */ R> pullFromUpstream(Long hotelId); + /** + * 同上;priceDaysOverride>0 时覆盖本次房价天数(全量详情可缩窗加速)。 + */ + R> pullFromUpstream(Long hotelId, Integer priceDaysOverride); + /** * 仅调用 getHotelComplexList 落房型(不拉房价),用于单独验证综合信息接口。 */ @@ -43,6 +48,7 @@ public interface SmHotelMidService { /** * 将本地酒店+房型+产品+日历推送给下游销售方。 * 内部分类:可售推 / 关房推 / 纯跳过(见 SmHotelDownstreamPushKind)。 + * 圣诺有 eid 时:无产品/无日历走关房,不因「本地空」跳过(防对方残留旧开房)。 * @param partnerCode 指定下游伙伴编码;必填,仅推该伙伴(且须在范围映射内) */ R> pushToDownstream(Long hotelId, String partnerCode); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java index 56ba5f9c..fa4c3de3 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java @@ -50,6 +50,11 @@ public interface SmHotelSyncService { */ R startDownstreamScopePushAsync(String partnerCode, String triggerType); + /** + * 下游全量推前过滤酒店 ID(圣诺:去掉未填 sHotelId/eid 的;其它协议原样返回)。 + */ + java.util.List filterDownstreamPushHotelIds(String partnerCode, java.util.List hotelIds); + /** * 根据某次同步日志的 failDetail,只重试失败酒店(支持 DETAIL_PULL / PRICE_ROLL)。 */ 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 4473d5a1..dffcc85a 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 @@ -509,6 +509,12 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl= 0) { target.setDetailPullIntervalMs(dto.getDetailPullIntervalMs()); } + if (dto.getDetailPullConcurrency() != null && dto.getDetailPullConcurrency() > 0) { + target.setDetailPullConcurrency(dto.getDetailPullConcurrency()); + } + if (dto.getDetailPullPriceDays() != null) { + target.setDetailPullPriceDays(dto.getDetailPullPriceDays()); + } if (dto.getDetailPullMaxHotels() != null && dto.getDetailPullMaxHotels() >= 0) { target.setDetailPullMaxHotels(dto.getDetailPullMaxHotels()); } 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 8d00ce30..b83eddcc 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 @@ -7,6 +7,7 @@ import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.data.tenant.TenantBroker; import com.bsd.pjcloud.common.data.tenant.TenantContextHolder; import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties; +import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol; import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelEntity; import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper; @@ -205,6 +206,12 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu scopePartners); return; } + // 圣诺变更自动推:未填 eid 直接不进推送,不调对方接口、不记无意义流水 + partners = filterOutShengnuoWithoutEid(hotel, partners); + if (CollUtil.isEmpty(partners)) { + log.info("|{}|过滤后无伙伴可推(圣诺缺 eid 等)|hotelId={}|", TAG, hotelId); + return; + } for (String partner : partners) { long t0 = System.currentTimeMillis(); try { @@ -282,6 +289,33 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu return partners.stream().filter(this::isPartnerAutoPushEnabled).distinct().toList(); } + /** 圣诺未填 sHotelId/eid:变更自动推也不进队 */ + private List filterOutShengnuoWithoutEid(SmHotelEntity hotel, List partners) { + if (CollUtil.isEmpty(partners)) { + return List.of(); + } + boolean hasEid = hotel != null && StrUtil.isNotBlank(StrUtil.trim(hotel.getSHotelId())); + if (hasEid) { + return partners; + } + List kept = partners.stream().filter(p -> !isShengnuoPartner(p)).toList(); + if (kept.size() < partners.size()) { + log.info("|{}|酒店未填外部酒店ID,跳过圣诺自动推|hotelId={}|hotelCode={}|", TAG, + hotel == null ? null : hotel.getId(), hotel == null ? null : hotel.getHotelCode()); + } + return kept; + } + + private boolean isShengnuoPartner(String partnerCode) { + try { + HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(partnerCode); + return props != null && SmHotelDownstreamProtocol.isShengnuo(props.getProtocol()); + } + catch (Exception e) { + return false; + } + } + private boolean isPartnerAutoPushEnabled(String partnerCode) { try { HotelDownstreamProperties props = channelConfigService.resolveDownstreamRuntime(partnerCode); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java index 08af89af..0769d4a5 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java @@ -26,15 +26,24 @@ public class SmHotelDownstreamPushLogServiceImpl @Override public Page pageLogs(Page page, SmHotelDownstreamPushLogEntity query) { + // 列表不查 request_body/response_body(mediumtext):全量推后行数暴涨,带大字段会极慢;详情再查全文 + String partner = query == null ? null : query.getPartnerCode(); + Long syncLogId = query == null ? null : query.getSyncLogId(); + Long hotelId = query == null ? null : query.getHotelId(); + String extHotelId = query == null ? null : query.getExtHotelId(); + String action = query == null ? null : query.getAction(); + String success = query == null ? null : query.getSuccess(); return baseMapper.selectPage(page, Wrappers.lambdaQuery() - .eq(StrUtil.isNotBlank(query.getPartnerCode()), SmHotelDownstreamPushLogEntity::getPartnerCode, - query.getPartnerCode()) - .eq(query.getSyncLogId() != null, SmHotelDownstreamPushLogEntity::getSyncLogId, query.getSyncLogId()) - .eq(query.getHotelId() != null, SmHotelDownstreamPushLogEntity::getHotelId, query.getHotelId()) - .eq(StrUtil.isNotBlank(query.getExtHotelId()), SmHotelDownstreamPushLogEntity::getExtHotelId, - query.getExtHotelId()) - .eq(StrUtil.isNotBlank(query.getAction()), SmHotelDownstreamPushLogEntity::getAction, query.getAction()) - .eq(StrUtil.isNotBlank(query.getSuccess()), SmHotelDownstreamPushLogEntity::getSuccess, query.getSuccess()) + .select(SmHotelDownstreamPushLogEntity.class, info -> { + String col = info.getColumn(); + return !"request_body".equalsIgnoreCase(col) && !"response_body".equalsIgnoreCase(col); + }) + .eq(StrUtil.isNotBlank(partner), SmHotelDownstreamPushLogEntity::getPartnerCode, partner) + .eq(syncLogId != null, SmHotelDownstreamPushLogEntity::getSyncLogId, syncLogId) + .eq(hotelId != null, SmHotelDownstreamPushLogEntity::getHotelId, hotelId) + .eq(StrUtil.isNotBlank(extHotelId), SmHotelDownstreamPushLogEntity::getExtHotelId, extHotelId) + .eq(StrUtil.isNotBlank(action), SmHotelDownstreamPushLogEntity::getAction, action) + .eq(StrUtil.isNotBlank(success), SmHotelDownstreamPushLogEntity::getSuccess, success) .orderByDesc(SmHotelDownstreamPushLogEntity::getCreateTime)); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java index 46523a04..45a8f5a0 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java @@ -71,7 +71,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { /** 本地日历单次批量最长天数(含首尾) */ private static final int LOCAL_DAILY_BATCH_MAX_DAYS = 366; - /** 圣诺 pushHotelPrice 单次最多 15 天 */ + /** 圣诺 pushHotelPrice 单次最多 15 天(有几天推几天,不必凑满) */ private static final int SHENGNUO_PRICE_CHUNK_DAYS = 15; /** @@ -156,6 +156,12 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { @Override @Transactional(rollbackFor = Exception.class) public R> pullFromUpstream(Long hotelId) { + return pullFromUpstream(hotelId, null); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public R> pullFromUpstream(Long hotelId, Integer priceDaysOverride) { SmHotelEntity hotel = requireChannelWritableHotel(hotelId, SmHotelDataSource.SHANGYOU); String hotelCode = hotel.getHotelCode(); List upstreamTips = new ArrayList<>(); @@ -174,8 +180,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { log.warn("|酒店拉取上游|综合信息无数据|hotelCode={}|code={}|message={}|", hotelCode, complexResp.getString("code"), complexMsg); } - else { - log.info("|酒店拉取上游|综合信息|hotelCode={}|roomTypeSize={}|hotelName={}|", hotelCode, + else if (log.isDebugEnabled()) { + log.debug("|酒店拉取上游|综合信息|hotelCode={}|roomTypeSize={}|hotelName={}|", hotelCode, complex.getJSONArray("hotelRoomTypeList") == null ? 0 : complex.getJSONArray("hotelRoomTypeList").size(), complex.getString("hotelName")); @@ -185,10 +191,13 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { hotelMapper.updateById(hotel); int roomCount = upsertRoomsFromComplex(hotel, complex); - // 2) 房价房量:按 30 天切片拉取,合并产品与日历 - int priceDays = Math.max(1, Math.min(90, - upstreamProperties.getPriceDays() > 0 ? upstreamProperties.getPriceDays() - : downstreamProperties.getDailyDays())); + // 2) 房价房量:按 30 天切片拉取,合并产品与日历(全量详情可缩窗减少切片次数) + int configuredDays = upstreamProperties.getPriceDays() > 0 ? upstreamProperties.getPriceDays() + : downstreamProperties.getDailyDays(); + if (priceDaysOverride != null && priceDaysOverride > 0) { + configuredDays = priceDaysOverride; + } + int priceDays = Math.max(1, Math.min(90, configuredDays)); LocalDate start = LocalDate.now(); LocalDate endExclusive = start.plusDays(priceDays); @@ -210,8 +219,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { JSONObject priceContent = firstContentObject(priceResp.get("content")); int rateSize = priceContent.getJSONArray("roomRateList") == null ? 0 : priceContent.getJSONArray("roomRateList").size(); - log.info("|酒店拉取上游|房价切片|hotelCode={}|chunk={}|{}~{}|rateSize={}|message={}|", hotelCode, priceChunk, - cursor, chunkEnd, rateSize, priceMsg); + if (log.isDebugEnabled()) { + log.debug("|酒店拉取上游|房价切片|hotelCode={}|chunk={}|{}~{}|rateSize={}|message={}|", hotelCode, + priceChunk, cursor, chunkEnd, rateSize, priceMsg); + } if (rateSize == 0 && StrUtil.isNotBlank(priceMsg) && priceChunk == 1) { upstreamTips.add("房价房量: " + priceMsg); } @@ -451,6 +462,19 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { Map one; if (SmHotelDownstreamPushKind.isClose(kind)) { one = closeHotelDailyToPartner(hotel, partner, null); + // 圣诺 getInfo 确认对方无店时关房会跳过,按 skip 统计,不算失败 + if (Boolean.TRUE.equals(one.get("skipped"))) { + one.put("partnerCode", partner); + one.put("pushKind", SmHotelDownstreamPushKind.SKIP); + Map result = new HashMap<>(8); + result.put("pushKind", SmHotelDownstreamPushKind.SKIP); + result.put("skipped", true); + result.put("skipReason", one.get("skipReason")); + result.put("partners", List.of(partner)); + result.put("partnerResults", List.of(one)); + result.put("partnerErrors", Collections.emptyList()); + return R.ok(result); + } one.put("pushKind", SmHotelDownstreamPushKind.CLOSE); one.put("closed", true); } @@ -551,16 +575,28 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { /** * 可售推 / 关房推 / 纯跳过(总览 §5.1)。 - * SKIP:无酒店或无产品;CLOSE:有产品但下架或窗口无日历;SELL:上架+有产品+窗口有日历。 + *

*/ private String resolveDownstreamPushKind(SmHotelEntity hotel, String partnerCode) { if (hotel == null) { return SmHotelDownstreamPushKind.SKIP; } + boolean shengnuo = isShengnuoPartner(partnerCode); + // 圣诺无 eid:对方无法认店,可售/关房均跳过(入口也会预过滤) + if (shengnuo && StrUtil.isBlank(StrUtil.trim(hotel.getSHotelId()))) { + return SmHotelDownstreamPushKind.SKIP; + } Long hotelId = hotel.getId(); Long productCount = productMapper.selectCount(Wrappers.lambdaQuery() .eq(SmHotelProductEntity::getHotelId, hotelId)); - if (productCount == null || productCount <= 0) { + boolean hasProduct = productCount != null && productCount > 0; + // 环启等:无产品组不出关房区间,只能跳过;圣诺有 eid 则继续走关房 + if (!hasProduct && !shengnuo) { return SmHotelDownstreamPushKind.SKIP; } boolean offline = "OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus()); @@ -570,6 +606,13 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { .ge(SmHotelDailyEntity::getStayDate, window[0]) .le(SmHotelDailyEntity::getStayDate, window[1])); boolean hasDaily = dailyCount != null && dailyCount > 0; + // 圣诺:无产品 / 无窗内日历 / 下架 → 关房清对方旧价;有货才可售推 + if (shengnuo) { + if (!hasProduct || offline || !hasDaily) { + return SmHotelDownstreamPushKind.CLOSE; + } + return SmHotelDownstreamPushKind.SELL; + } if (offline || !hasDaily) { return SmHotelDownstreamPushKind.CLOSE; } @@ -580,6 +623,9 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { if (hotel == null) { return "酒店不存在,跳过推送"; } + if (isShengnuoPartner(partnerCode) && StrUtil.isBlank(StrUtil.trim(hotel.getSHotelId()))) { + return "未填外部酒店ID(sHotelId/eid),跳过圣诺推送(禁止用酒店编码冒充)"; + } Long productCount = productMapper.selectCount(Wrappers.lambdaQuery() .eq(SmHotelProductEntity::getHotelId, hotel.getId())); if (productCount == null || productCount <= 0) { @@ -588,6 +634,11 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { return "跳过推送"; } + private boolean isShengnuoPartner(String partnerCode) { + HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(partnerCode); + return ds != null && SmHotelDownstreamProtocol.isShengnuo(ds.getProtocol()); + } + /** * 关房推:环启推 dailyStatus=0;慧订推空价态窗口(对方打满房)。 * @param onlyProducts 非空时只关这些产品(环启);慧订忽略,整窗清空 @@ -626,17 +677,13 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { /** * 圣诺可售推:getInfo(eid) → pushHotelPlan → pushHotelPrice(15 天切片)。 - * eid 优先 sHotelId(艺龙同程 id),否则 hotelCode。 + * eid 仅用 sHotelId(艺龙同程 id),未填则上层应已 SKIP,禁止用 hotelCode 冒充。 */ private Map pushHotelToShengnuo(SmHotelEntity hotel, String partnerCode) { Long hotelId = hotel.getId(); - String sHotelId = StrUtil.trim(hotel.getSHotelId()); - String eid = StrUtil.blankToDefault(sHotelId, StrUtil.trim(hotel.getHotelCode())); + String eid = StrUtil.trim(hotel.getSHotelId()); if (StrUtil.isBlank(eid)) { - throw new IllegalArgumentException("圣诺推送缺少 eid:请在酒店资料填写「外部酒店ID(sHotelId)=艺龙同程id」 hotelId=" + hotelId); - } - if (StrUtil.isBlank(sHotelId)) { - log.warn("|下游推送[圣诺]|未填 sHotelId,回落 hotelCode 作 eid|hotelId={}|eid={}|", hotelId, eid); + throw new IllegalArgumentException("圣诺推送缺少外部酒店ID(sHotelId/eid),已禁止用酒店编码冒充 hotelId=" + hotelId); } downstreamClient.assertPartnerReady(partnerCode); HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(partnerCode); @@ -646,9 +693,8 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { infoResp = shengnuoAdapter.getInfo(partnerCode, ds, eid); } catch (Exception ex) { - throw new IllegalArgumentException("圣诺 getInfo 失败(eid=" + eid - + (StrUtil.isBlank(sHotelId) ? ",当前用的是 hotelCode,请改填外部酒店ID/艺龙同程id" : ",请核对圣诺侧是否已建档") - + "):" + ex.getMessage(), ex); + throw new IllegalArgumentException("圣诺 getInfo 失败(eid=" + eid + ",请核对外部酒店ID/圣诺侧是否已建档):" + + ex.getMessage(), ex); } cn.hutool.json.JSONObject infoData = infoResp == null ? null : infoResp.getJSONObject("data"); if (infoData == null) { @@ -813,22 +859,66 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { summary); } - /** 圣诺关房:pushHotelClose(整店) */ + /** + * 圣诺关房:先 getInfo(eid) 确认对方有店,再 pushHotelClose。 + * 对方无档/getInfo 失败 → 跳过关房,避免无意义「更新失败」。 + */ private Map closeHotelDailyToShengnuo(SmHotelEntity hotel, String partnerCode) { downstreamClient.assertPartnerReady(partnerCode); HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(partnerCode); - long nsHotelId = resolveShengnuoHotelId(partnerCode, ds, hotel); + String eid = StrUtil.trim(hotel.getSHotelId()); + if (StrUtil.isBlank(eid)) { + String reason = "未填外部酒店ID(sHotelId/eid),跳过圣诺关房"; + recordDownstreamPushSkip(partnerCode, hotel, reason); + Map skip = new HashMap<>(6); + skip.put("protocol", SmHotelDownstreamProtocol.SHENGNUO); + skip.put("skipped", true); + skip.put("skipReason", reason); + skip.put("dailyCloseCount", 0); + return skip; + } + long nsHotelId; + try { + cn.hutool.json.JSONObject infoResp = shengnuoAdapter.getInfo(partnerCode, ds, eid); + cn.hutool.json.JSONObject infoData = infoResp == null ? null : infoResp.getJSONObject("data"); + nsHotelId = infoData == null ? 0L : infoData.getLong("id", 0L); + } + catch (Exception ex) { + String reason = "圣诺侧无此酒店或 getInfo 失败,跳过关房(eid=" + eid + "):" + ex.getMessage(); + recordDownstreamPushSkip(partnerCode, hotel, reason); + log.info("|下游关房推送[圣诺]|跳过|hotelId={}|partner={}|eid={}|err={}|", hotel.getId(), partnerCode, eid, + ex.getMessage()); + Map skip = new HashMap<>(6); + skip.put("protocol", SmHotelDownstreamProtocol.SHENGNUO); + skip.put("skipped", true); + skip.put("skipReason", reason); + skip.put("dailyCloseCount", 0); + return skip; + } + if (nsHotelId <= 0) { + String reason = "圣诺 getInfo 未返回酒店 id,跳过关房 eid=" + eid; + recordDownstreamPushSkip(partnerCode, hotel, reason); + Map skip = new HashMap<>(6); + skip.put("protocol", SmHotelDownstreamProtocol.SHENGNUO); + skip.put("skipped", true); + skip.put("skipReason", reason); + skip.put("dailyCloseCount", 0); + return skip; + } Map body = new LinkedHashMap<>(shengnuoAdapter.signedBase(ds)); body.put("id", nsHotelId); shengnuoAdapter.pushHotelClose(partnerCode, ds, hotel.getId(), String.valueOf(nsHotelId), body); - log.info("|下游关房推送[圣诺]|hotelId={}|partner={}|nsHotelId={}|", hotel.getId(), partnerCode, nsHotelId); + log.info("|下游关房推送[圣诺]|hotelId={}|partner={}|nsHotelId={}|eid={}|", hotel.getId(), partnerCode, nsHotelId, + eid); Map result = new HashMap<>(6); result.put("protocol", SmHotelDownstreamProtocol.SHENGNUO); result.put("nsHotelId", nsHotelId); + result.put("eid", eid); result.put("dailyCloseCount", 1); return result; } + /** 可售推等仍可用缓存 nsHotelId;关房已改为强制 getInfo 验活 */ private long resolveShengnuoHotelId(String partnerCode, HotelDownstreamProperties ds, SmHotelEntity hotel) { cn.hutool.json.JSONObject cached = shengnuoIdMappingCache.getByLocal(partnerCode, hotel.getId()); if (cached != null) { @@ -837,9 +927,9 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { return id; } } - String eid = StrUtil.blankToDefault(StrUtil.trim(hotel.getSHotelId()), StrUtil.trim(hotel.getHotelCode())); + String eid = StrUtil.trim(hotel.getSHotelId()); if (StrUtil.isBlank(eid)) { - throw new IllegalArgumentException("圣诺关房缺少 eid/映射 hotelId=" + hotel.getId()); + throw new IllegalArgumentException("圣诺缺少外部酒店ID(sHotelId)且无本地映射 hotelId=" + hotel.getId()); } cn.hutool.json.JSONObject infoResp = shengnuoAdapter.getInfo(partnerCode, ds, eid); cn.hutool.json.JSONObject infoData = infoResp == null ? null : infoResp.getJSONObject("data"); @@ -915,6 +1005,10 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { return first == null ? 0L : first.getLong("plan_id", 0L); } + /** + * 圣诺价态按最多 15 天切片推送:只推本地有日历且有有效价格的天,不凑满、不用 cost=1 占位。 + * 窗内整天无货时由上层走 pushHotelClose,不在此用关房占位日硬铺。 + */ private int pushShengnuoPriceChunks(String partnerCode, HotelDownstreamProperties ds, Long hotelId, long nsHotelId, List products, Map roomTypeToNs, Map productToPlan, List dailies, LocalDate from, LocalDate to) { @@ -955,31 +1049,30 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { Map dayMap = dailyByProduct.getOrDefault(e.getKey(), Map.of()); for (LocalDate d = cursor; !d.isAfter(chunkEnd); d = d.plusDays(1)) { SmHotelDailyEntity row = dayMap.get(d); + if (row == null) { + continue; + } + BigDecimal price = row.getSellPrice() != null ? row.getSellPrice() : row.getCostPrice(); + if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) { + // 无有效价不推,不用 1 元占位 + continue; + } + // 整数价:四舍五入后仍可能为 0(如 0.4),圣诺要求 cost≥1 + int costInt = Math.max(1, price.setScale(0, java.math.RoundingMode.HALF_UP).intValue()); + // remainStock 是 exist=false 计算字段,库查询不会带;可售一律用 stock-sold(与环启/慧订一致) + int amount = huanqiRemainStock(row); + int statusOpen = row.getDailyStatus() == null ? 1 : row.getDailyStatus(); + int open = statusOpen == 1 && amount > 0 ? 1 : 0; Map pc = new LinkedHashMap<>(); pc.put("date", d.toString()); - if (row == null) { - // 圣诺拒 cost=0(价格格式错误);无日历也按关房占位价 1 - pc.put("cost", 1); - pc.put("amount", 0); - pc.put("status", 0); - } - else { - BigDecimal price = row.getSellPrice() != null ? row.getSellPrice() : row.getCostPrice(); - if (price == null || price.compareTo(BigDecimal.ZERO) <= 0) { - price = BigDecimal.ONE; - } - // 整数价:四舍五入后仍可能为 0(如 0.4),圣诺要求 cost≥1 - int costInt = Math.max(1, price.setScale(0, java.math.RoundingMode.HALF_UP).intValue()); - // remainStock 是 exist=false 计算字段,库查询不会带;可售一律用 stock-sold(与环启/慧订一致) - int amount = huanqiRemainStock(row); - int statusOpen = row.getDailyStatus() == null ? 1 : row.getDailyStatus(); - int open = statusOpen == 1 && amount > 0 ? 1 : 0; - pc.put("cost", costInt); - pc.put("amount", open == 1 ? Math.max(1, amount) : amount); - pc.put("status", open); - } + pc.put("cost", costInt); + pc.put("amount", open == 1 ? Math.max(1, amount) : amount); + pc.put("status", open); priceCode.add(pc); } + if (priceCode.isEmpty()) { + continue; + } Map info = new LinkedHashMap<>(); info.put("roomId", nsRoomId); info.put("planId", e.getValue()); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java index b508d354..9820f6b1 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelServiceImpl.java @@ -72,7 +72,16 @@ public class SmHotelServiceImpl extends ServiceImpl pageScope(Page page, LambdaQueryWrapper wrapper) { - // 不再聚合房型/产品/日历数与日历起价:全量详情后 daily 过大,列表会卡死 + // 不再聚合房型/产品/日历数;列表只查展示列,丢掉 intro/轮播等大字段减轻 IO + wrapper.select(SmHotelEntity::getId, SmHotelEntity::getTenantId, SmHotelEntity::getDeptId, + SmHotelEntity::getSourceId, SmHotelEntity::getGroupCode, SmHotelEntity::getHotelCode, + SmHotelEntity::getSGroupId, SmHotelEntity::getSHotelId, SmHotelEntity::getHotelName, + SmHotelEntity::getSheetName, SmHotelEntity::getUpstreamStatus, + SmHotelEntity::getLocalAutoRoll, SmHotelEntity::getLocalRollHorizonDays, + SmHotelEntity::getDistrict, SmHotelEntity::getDistrictAdcode, SmHotelEntity::getBizArea, + SmHotelEntity::getStarLevel, SmHotelEntity::getScore, SmHotelEntity::getAddress, + SmHotelEntity::getPhone, SmHotelEntity::getCreateTime, SmHotelEntity::getUpdateTime, + SmHotelEntity::getDelFlag); return baseMapper.selectPageByScope(page, wrapper, DataScope.of()); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java index 2cb81e4c..547c60a9 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java @@ -368,7 +368,8 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ if (h == null || h.getId() == null) { continue; } - String eid = StrUtil.blankToDefault(StrUtil.trim(h.getSHotelId()), StrUtil.trim(h.getHotelCode())); + // 仅用真实外部酒店ID 调 getInfo,禁止 hotelCode 冒充 eid + String eid = StrUtil.trim(h.getSHotelId()); if (StrUtil.isBlank(eid)) { continue; } 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 21437685..7e59cda4 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 @@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.supermarket.config.HotelDownstreamProperties; import com.bsd.pjcloud.supermarket.config.ShangyouHotelProperties; import com.bsd.pjcloud.supermarket.constant.SmHotelDataSource; +import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol; import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO; import com.bsd.pjcloud.supermarket.entity.SmHotelEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelSyncLogEntity; @@ -36,9 +37,12 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; +import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; /** * 酒店列表日更 + Redis SDIFF + 滚价;手动/定时均写同步日志并支持进度查询。 @@ -132,6 +136,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { redisTemplate.opsForValue().set(ShangyouHotelApi.REDIS_IMPORT_DATA_SOURCE, source, 2, TimeUnit.DAYS); SmHotelSyncLogEntity logEntity = newLog(TYPE_LIST, triggerType); + logEntity.setDataSource(source); logEntity.setStage("QUEUED"); logEntity.setStageMsg("已提交,数据来源=" + source + ",等待执行…"); logEntity.setProgressPct(1); @@ -189,6 +194,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { return R.failed("已有本地滚加任务进行中(若长时间卡住请在同步日志点「终止」)"); } SmHotelSyncLogEntity logEntity = newLog(TYPE_LOCAL_ROLL, triggerType); + logEntity.setDataSource(SmHotelDataSource.LOCAL); logEntity.setStage("QUEUED"); logEntity.setStageMsg("已提交本地滚加任务…"); logEntity.setProgressPct(1); @@ -225,6 +231,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { int maxHotels = limit != null && limit > 0 ? limit : props().getDetailPullMaxHotels(); SmHotelSyncLogEntity logEntity = newLog(TYPE_DETAIL, triggerType); + logEntity.setDataSource(channel); logEntity.setStage("QUEUED"); logEntity.setStageMsg((maxHotels > 0 ? "已提交,最多处理 " + maxHotels + " 家" : "已提交,将拉取") + "「" + channel + "」启用酒店详情,归属部门=" + deptId + "…"); @@ -265,11 +272,27 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { if (CollUtil.isEmpty(hotelIds)) { return R.failed("下游「" + partner + "」授权范围内没有上架酒店(请检查渠道部门映射与酒店上架状态)"); } + int beforeFilter = hotelIds.size(); + hotelIds = filterShengnuoHotelsRequireEid(partner, hotelIds); + int droppedNoEid = beforeFilter - hotelIds.size(); + if (CollUtil.isEmpty(hotelIds)) { + return R.failed(droppedNoEid > 0 + ? "下游「" + partner + "」范围内上架酒店均未填外部酒店ID(sHotelId/eid),已全部过滤,无需推送" + : "下游「" + partner + "」授权范围内没有可推送酒店"); + } SmHotelSyncLogEntity logEntity = newLog(TYPE_DOWNSTREAM_PUSH, triggerType); + logEntity.setDataSource(partner); logEntity.setStage("QUEUED"); - logEntity.setStageMsg("已提交:向下游「" + partner + "」推送 " + hotelIds.size() + " 家酒店(仅本地数据)…"); + String msg = "已提交:向下游「" + partner + "」推送 " + hotelIds.size() + " 家酒店(仅本地数据)"; + if (droppedNoEid > 0) { + msg += ",已排除无 eid " + droppedNoEid + " 家"; + } + logEntity.setStageMsg(msg + "…"); logEntity.setProgressPct(1); logEntity.setLocalCount((long) hotelIds.size()); + if (droppedNoEid > 0) { + logEntity.setSkipCount(droppedNoEid); + } syncLogMapper.insert(logEntity); Long tenantId = TenantContextHolder.getTenantId(); @@ -313,6 +336,7 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { } SmHotelSyncLogEntity logEntity = newLog(syncType, "RETRY"); + logEntity.setDataSource(src.getDataSource()); logEntity.setStage("QUEUED"); logEntity.setStageMsg("重试失败酒店:来源日志 " + sourceLogId + ",共 " + codes.size() + " 家" + (TYPE_ROLL.equals(syncType) ? "(滚价日=" + resolveRollDay(src) + ")" : "")); @@ -389,9 +413,13 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { @Override public Page pageSyncLogs(Page page, SmHotelSyncLogEntity query) { + String syncType = query == null ? null : query.getSyncType(); + String status = query == null ? null : query.getStatus(); + String dataSource = query == null ? null : SmHotelDataSource.normalize(query.getDataSource()).toLowerCase(); return syncLogMapper.selectPage(page, Wrappers.lambdaQuery() - .eq(StrUtil.isNotBlank(query.getSyncType()), SmHotelSyncLogEntity::getSyncType, query.getSyncType()) - .eq(StrUtil.isNotBlank(query.getStatus()), SmHotelSyncLogEntity::getStatus, query.getStatus()) + .eq(StrUtil.isNotBlank(syncType), SmHotelSyncLogEntity::getSyncType, syncType) + .eq(StrUtil.isNotBlank(status), SmHotelSyncLogEntity::getStatus, status) + .eq(StrUtil.isNotBlank(dataSource), SmHotelSyncLogEntity::getDataSource, dataSource) .orderByDesc(SmHotelSyncLogEntity::getCreateTime)); } @@ -735,7 +763,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { try { R> r = midService.pushToDownstream(hotelId, partnerCode); if (r != null && r.getCode() == 0) { - // 下架/无产品/无日历等预检跳过:算 skip,不算成功(对方也卖不了) + // 预检跳过(圣诺无 eid;其它下游无产品等):算 skip。 + // 圣诺有 eid 后无产品/无日历走关房,计成功,不再跳过。 if (isDownstreamPushSkipped(r.getData())) { skip++; } @@ -770,7 +799,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { done.setStatus(STATUS_SUCCESS); done.setStage("DONE"); done.setStageMsg("下游「" + partnerCode + "」全量推送完成:成功 " + ok + " / 失败 " + fail + " / 跳过 " + skip - + "(成功含可售推与关房推;跳过=无产品等);明细见下游推送日志(同步日志ID=" + logId + ")"); + + "(成功含可售推与关房推;圣诺跳过仅无 eid,无产品/无日历会关房;其它下游跳过含无产品等);" + + "明细见下游推送日志(同步日志ID=" + logId + ")"); done.setProgressPct(100); done.setSuccessCount(ok); done.setFailCount(fail); @@ -793,6 +823,48 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { } } + @Override + public List filterDownstreamPushHotelIds(String partnerCode, List hotelIds) { + return filterShengnuoHotelsRequireEid(partnerCode, hotelIds); + } + + /** + * 圣诺推送前过滤:未填外部酒店ID(sHotelId/eid) 的酒店不进入推送队列,避免无意义调对方 getInfo。 + * 非圣诺协议原样返回。 + */ + private List filterShengnuoHotelsRequireEid(String partnerCode, List hotelIds) { + if (CollUtil.isEmpty(hotelIds)) { + return hotelIds == null ? List.of() : hotelIds; + } + HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(partnerCode); + if (ds == null || !SmHotelDownstreamProtocol.isShengnuo(ds.getProtocol())) { + return hotelIds; + } + List rows = hotelMapper.selectList(Wrappers.lambdaQuery() + .select(SmHotelEntity::getId, SmHotelEntity::getSHotelId) + .in(SmHotelEntity::getId, hotelIds)); + Set withEid = new java.util.HashSet<>(); + if (CollUtil.isNotEmpty(rows)) { + for (SmHotelEntity h : rows) { + if (h != null && h.getId() != null && StrUtil.isNotBlank(StrUtil.trim(h.getSHotelId()))) { + withEid.add(h.getId()); + } + } + } + List kept = new ArrayList<>(withEid.size()); + for (Long id : hotelIds) { + if (id != null && withEid.contains(id)) { + kept.add(id); + } + } + int dropped = hotelIds.size() - kept.size(); + if (dropped > 0) { + log.info("|下游全量推送|圣诺推送前过滤无eid|partner={}|before={}|after={}|dropped={}|", partnerCode, + hotelIds.size(), kept.size(), dropped); + } + return kept; + } + /** pushToDownstream 预检跳过时 data.skipped=true */ private static boolean isDownstreamPushSkipped(Object data) { if (!(data instanceof Map map)) { @@ -812,12 +884,13 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { } /** - * 对商游来源启用酒店逐家 pullFromUpstream(综合信息 + 未来 price-days 房价)。 + * 全量详情:酒店级并行 + 可缩房价窗,尽量吃满上游 price-api-qps。 * 本地及其它渠道酒店一律不进此任务。 */ private void runDetailPull(Long logId, int maxHotels, String channel, Long deptId) { long t0 = System.currentTimeMillis(); String ch = SmHotelDataSource.normalize(channel).toLowerCase(); + ExecutorService pullPool = null; try { // 将该渠道酒店统一归属到维护的部门(便于支付商户/数据权限) if (deptId != null && StrUtil.isNotBlank(ch)) { @@ -836,18 +909,38 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { long target = maxHotels > 0 ? Math.min(totalAll, maxHotels) : totalAll; int batchSize = Math.max(20, props().getPriceRollBatchSize()); int intervalMs = Math.max(0, props().getDetailPullIntervalMs()); + int concurrency = Math.max(1, props().getDetailPullConcurrency()); + Integer priceDaysOverride = props().getDetailPullPriceDays() > 0 ? props().getDetailPullPriceDays() + : null; + int effectivePriceDays = priceDaysOverride != null ? priceDaysOverride : props().getPriceDays(); long lastId = 0L; - int ok = 0; - int fail = 0; - int skip = 0; - int processed = 0; + AtomicInteger ok = new AtomicInteger(); + AtomicInteger fail = new AtomicInteger(); + AtomicInteger skip = new AtomicInteger(); + AtomicInteger processed = new AtomicInteger(); List failLines = new ArrayList<>(); + final Object failLock = new Object(); - progress(logId, 5, "PULL", "开始拉取详情:目标约 " + target + " 家「" + ch + "」酒店(price-days=" - + props().getPriceDays() + ")…"); + Long tenantId = TenantContextHolder.getTenantId(); + if (tenantId == null) { + tenantId = props().getCallbackTenantId() == null ? 1L : props().getCallbackTenantId(); + } + final Long tid = tenantId; + + pullPool = Executors.newFixedThreadPool(concurrency, r -> { + Thread t = new Thread(r, "sm-hotel-detail-pull"); + t.setDaemon(true); + return t; + }); + + progress(logId, 5, "PULL", "开始拉取详情:目标约 " + target + " 家「" + ch + "」并发=" + concurrency + + " price-days=" + effectivePriceDays + "…"); - outer: while (true) { + if (isAborted(logId)) { + failLog(logId, "任务已终止", System.currentTimeMillis() - t0); + return; + } List rows = hotelMapper.selectList(Wrappers.lambdaQuery() .select(SmHotelEntity::getId, SmHotelEntity::getHotelCode, SmHotelEntity::getHotelName) @@ -861,45 +954,91 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { if (CollUtil.isEmpty(rows)) { break; } + List> tasks = new ArrayList<>(rows.size()); + boolean hitCap = false; for (SmHotelEntity h : rows) { lastId = h.getId(); - if (maxHotels > 0 && processed >= maxHotels) { - break outer; + if (maxHotels > 0 && processed.get() >= maxHotels) { + hitCap = true; + break; } - processed++; if (StrUtil.isBlank(h.getHotelCode())) { - skip++; + skip.incrementAndGet(); + processed.incrementAndGet(); continue; } - try { - R> r = midService.pullFromUpstream(h.getId()); - if (r != null && r.getCode() == 0) { - ok++; - } - else { - fail++; - appendFail(failLines, h, r == null ? "返回为空" : StrUtil.blankToDefault(r.getMsg(), "业务失败")); + // 入队前占名额,避免并行超 maxHotels + if (maxHotels > 0) { + int slot = processed.incrementAndGet(); + if (slot > maxHotels) { + processed.decrementAndGet(); + hitCap = true; + break; } } - catch (Exception e) { - fail++; - appendFail(failLines, h, e.getMessage()); - log.warn("|全量详情|单店失败|hotelId={}|code={}|{}", h.getId(), h.getHotelCode(), e.getMessage()); - } - int pct = target <= 0 ? 50 : Math.min(95, 5 + (int) (processed * 90L / target)); - progress(logId, pct, "PULL", "拉取中 " + processed + "/" + target + " 成功 " + ok + " 失败 " + fail - + " 当前 " + StrUtil.blankToDefault(h.getHotelName(), h.getHotelCode())); - if (intervalMs > 0) { + tasks.add(() -> { + TenantBroker.runAs(tid, id -> { + try { + R> r = midService.pullFromUpstream(h.getId(), + priceDaysOverride); + if (r != null && r.getCode() == 0) { + ok.incrementAndGet(); + } + else { + fail.incrementAndGet(); + synchronized (failLock) { + appendFail(failLines, h, r == null ? "返回为空" + : StrUtil.blankToDefault(r.getMsg(), "业务失败")); + } + } + } + catch (Exception e) { + fail.incrementAndGet(); + synchronized (failLock) { + appendFail(failLines, h, e.getMessage()); + } + log.warn("|全量详情|单店失败|hotelId={}|code={}|{}", h.getId(), h.getHotelCode(), + e.getMessage()); + } + finally { + if (maxHotels <= 0) { + processed.incrementAndGet(); + } + int doneCnt = processed.get(); + if (intervalMs > 0) { + try { + Thread.sleep(intervalMs); + } + catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + } + } + if (doneCnt % 20 == 0 || doneCnt >= target) { + int pct = target <= 0 ? 50 + : Math.min(95, 5 + (int) (doneCnt * 90L / target)); + progress(logId, pct, "PULL", + "拉取中 " + doneCnt + "/" + target + " 成功 " + ok.get() + + " 失败 " + fail.get() + " 并发=" + concurrency); + } + } + }); + return null; + }); + } + if (CollUtil.isNotEmpty(tasks)) { + List> futures = pullPool.invokeAll(tasks); + for (Future f : futures) { try { - Thread.sleep(intervalMs); + f.get(); } - catch (InterruptedException ie) { - Thread.currentThread().interrupt(); - failLog(logId, "任务被中断", System.currentTimeMillis() - t0); - return; + catch (Exception e) { + log.warn("|全量详情|批次任务异常|{}", e.getMessage()); } } } + if (hitCap || (maxHotels > 0 && processed.get() >= maxHotels)) { + break; + } if (rows.size() < batchSize) { break; } @@ -911,23 +1050,38 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { } done.setStatus(STATUS_SUCCESS); done.setStage("DONE"); - done.setStageMsg("全量详情拉取完成"); + done.setStageMsg("全量详情拉取完成(并发=" + concurrency + ",priceDays=" + effectivePriceDays + ")"); done.setProgressPct(100); - done.setSuccessCount(ok); - done.setFailCount(fail); - done.setSkipCount(skip); + done.setSuccessCount(ok.get()); + done.setFailCount(fail.get()); + done.setSkipCount(skip.get()); done.setLocalCount(totalAll); - applyFailDetail(done, failLines, fail); + applyFailDetail(done, failLines, fail.get()); done.setCostMs(System.currentTimeMillis() - t0); done.setFinishTime(LocalDateTime.now()); done.setUpdateTime(LocalDateTime.now()); syncLogMapper.updateById(done); - log.info("|全量详情|完成|logId={}|ok={}|fail={}|skip={}|costMs={}|", logId, ok, fail, skip, done.getCostMs()); + log.info("|全量详情|完成|logId={}|ok={}|fail={}|skip={}|concurrency={}|priceDays={}|costMs={}|", logId, + ok.get(), fail.get(), skip.get(), concurrency, effectivePriceDays, done.getCostMs()); } catch (Throwable e) { log.error("|全量详情|失败|logId={}|", logId, e); failLog(logId, e.getMessage(), System.currentTimeMillis() - t0); } + finally { + if (pullPool != null) { + pullPool.shutdown(); + try { + if (!pullPool.awaitTermination(30, TimeUnit.SECONDS)) { + pullPool.shutdownNow(); + } + } + catch (InterruptedException ie) { + pullPool.shutdownNow(); + Thread.currentThread().interrupt(); + } + } + } } /** @@ -983,7 +1137,9 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService { r = midService.pullPriceRange(h.getId(), day, end); } else { - r = midService.pullFromUpstream(h.getId()); + Integer priceDaysOverride = props().getDetailPullPriceDays() > 0 + ? props().getDetailPullPriceDays() : null; + r = midService.pullFromUpstream(h.getId(), priceDaysOverride); } if (r != null && r.getCode() == 0) { ok++; 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 92d4c1ed..c9fa26cf 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/resources/application.yml +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/resources/application.yml @@ -99,8 +99,12 @@ spring: # # 全量房价房量:每天最多次数 / 最小间隔小时(「拉取全部启用详情」受此限制) # full-price-sync-max-per-day: 3 # full-price-sync-min-interval-hours: 6 -# # 全量拉详情:每家间隔毫秒(落库已批量,默认 0;上游压力大时可调大) +# # 全量拉详情:每家间隔毫秒(并行时主要靠 price-api-qps,默认 0) # detail-pull-interval-ms: 0 +# # 全量拉详情酒店并发(吃满上游约 20 QPS;建议 4~8;多实例请调低) +# detail-pull-concurrency: 6 +# # 全量拉详情房价天数(≤0 用 price-days;圣诺约 15 天可配 15~30,比 90 天少 2/3 次房价请求) +# detail-pull-price-days: 30 # # 全量拉详情单次最多家数;0=不限制(酒店很多时建议先设 100 试跑) # detail-pull-max-hotels: 0 # # 差集扫描 / 重建本地 code SET 批大小