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-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/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/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/SmHotelMidServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java index 55a6f5d1..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); } @@ -564,21 +575,28 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { /** * 可售推 / 关房推 / 纯跳过(总览 §5.1)。 - * SKIP:无酒店或无产品;CLOSE:有产品但下架或窗口无日历;SELL:上架+有产品+窗口有日历。 - * 圣诺:未填外部酒店ID(sHotelId/eid) 不调对方接口(可售/关房均跳过)。 + *

*/ 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) { - return SmHotelDownstreamPushKind.SKIP; - } - // 圣诺无 eid:对方无法认店,关房也会「更新失败」,直接跳过 - if (isShengnuoPartner(partnerCode) && StrUtil.isBlank(StrUtil.trim(hotel.getSHotelId()))) { + boolean hasProduct = productCount != null && productCount > 0; + // 环启等:无产品组不出关房区间,只能跳过;圣诺有 eid 则继续走关房 + if (!hasProduct && !shengnuo) { return SmHotelDownstreamPushKind.SKIP; } boolean offline = "OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus()); @@ -588,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; } @@ -598,14 +623,14 @@ 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) { return "无产品,无法关房也无法售卖,跳过推送(摘牌前请先关房)"; } - if (isShengnuoPartner(partnerCode) && StrUtil.isBlank(StrUtil.trim(hotel.getSHotelId()))) { - return "未填外部酒店ID(sHotelId/eid),跳过圣诺推送(禁止用酒店编码冒充)"; - } return "跳过推送"; } @@ -980,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) { @@ -1020,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/SmHotelSyncServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java index cf7bc5c2..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 @@ -37,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 + 滚价;手动/定时均写同步日志并支持进度查询。 @@ -760,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++; } @@ -795,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); @@ -879,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)) { @@ -903,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) @@ -928,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; } @@ -978,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(); + } + } + } } /** @@ -1050,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 批大小