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 11768532c..4980707ce 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/controller/SmHotelMidController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelMidController.java index f624faf9b..0b54c537f 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/SmHotelSyncService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelSyncService.java index 56ba5f9cc..fa4c3de3c 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/SmHotelMidServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelMidServiceImpl.java index 46523a04e..1cdfd4c64 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 @@ -552,6 +552,7 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { /** * 可售推 / 关房推 / 纯跳过(总览 §5.1)。 * SKIP:无酒店或无产品;CLOSE:有产品但下架或窗口无日历;SELL:上架+有产品+窗口有日历。 + * 圣诺:未填外部酒店ID(sHotelId/eid) 不调对方接口;关房仅在已有 ns 映射时可继续。 */ private String resolveDownstreamPushKind(SmHotelEntity hotel, String partnerCode) { if (hotel == null) { @@ -570,7 +571,15 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { .ge(SmHotelDailyEntity::getStayDate, window[0]) .le(SmHotelDailyEntity::getStayDate, window[1])); boolean hasDaily = dailyCount != null && dailyCount > 0; - if (offline || !hasDaily) { + boolean needClose = offline || !hasDaily; + if (isShengnuoPartner(partnerCode) && StrUtil.isBlank(StrUtil.trim(hotel.getSHotelId()))) { + // 可售推必须 getInfo(eid),无 eid 直接跳过;关房仅 Redis 已有 nsHotelId 时可不调 getInfo + if (needClose && hasShengnuoNsMapping(partnerCode, hotelId)) { + return SmHotelDownstreamPushKind.CLOSE; + } + return SmHotelDownstreamPushKind.SKIP; + } + if (needClose) { return SmHotelDownstreamPushKind.CLOSE; } return SmHotelDownstreamPushKind.SELL; @@ -585,9 +594,26 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { if (productCount == null || productCount <= 0) { return "无产品,无法关房也无法售卖,跳过推送(摘牌前请先关房)"; } + if (isShengnuoPartner(partnerCode) && StrUtil.isBlank(StrUtil.trim(hotel.getSHotelId()))) { + return "未填外部酒店ID(sHotelId/eid),跳过圣诺推送(禁止用酒店编码冒充)"; + } return "跳过推送"; } + private boolean isShengnuoPartner(String partnerCode) { + HotelDownstreamProperties ds = channelConfigService.resolveDownstreamRuntime(partnerCode); + return ds != null && SmHotelDownstreamProtocol.isShengnuo(ds.getProtocol()); + } + + /** 圣诺本地↔nsHotelId 缓存是否已存在(关房可不依赖 eid) */ + private boolean hasShengnuoNsMapping(String partnerCode, Long hotelId) { + if (StrUtil.isBlank(partnerCode) || hotelId == null) { + return false; + } + cn.hutool.json.JSONObject cached = shengnuoIdMappingCache.getByLocal(partnerCode, hotelId); + return cached != null && cached.getLong("nsHotelId", 0L) > 0; + } + /** * 关房推:环启推 dailyStatus=0;慧订推空价态窗口(对方打满房)。 * @param onlyProducts 非空时只关这些产品(环启);慧订忽略,整窗清空 @@ -626,17 +652,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 +668,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) { @@ -837,9 +858,11 @@ public class SmHotelMidServiceImpl implements SmHotelMidService { return id; } } - String eid = StrUtil.blankToDefault(StrUtil.trim(hotel.getSHotelId()), StrUtil.trim(hotel.getHotelCode())); + // 无缓存时只能靠真实 eid 调 getInfo;禁止 hotelCode 回落 + 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"); 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 2cb81e4cd..547c60a94 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 214376858..cf7bc5c21 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; @@ -132,6 +133,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 +191,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 +228,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 +269,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 +333,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 +410,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)); } @@ -793,6 +818,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)) {