This commit is contained in:
zjf committed 2026-10-29 19:20:42 +08:00
1 parent 14c5856eb1
commit 03757353de
6 files changed
+126 -20

No files matched your search

@@ -27,6 +27,10 @@ public class SmHotelSyncLogEntity extends Model<SmHotelSyncLogEntity> {
@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;
@@ -149,10 +149,16 @@ public class SmHotelMidController {
@GetMapping("/mid/pushScope/preview")
@PreAuthorize("@pms.hasPermission('supermarket_smHotel_sync')")
public R<Map<String, Object>> previewPushScope(@RequestParam("partnerCode") String partnerCode) {
List<Long> ids = channelDeptService.listHotelIdsForDownstreamPartner(partnerCode);
Map<String, Object> data = new LinkedHashMap<>(4);
List<Long> raw = channelDeptService.listHotelIdsForDownstreamPartner(partnerCode);
int rawCount = raw == null ? 0 : raw.size();
// 与全量推同一套过滤:圣诺未填 eid 的不计入可推数量
List<Long> ids = syncService.filterDownstreamPushHotelIds(partnerCode, raw);
int pushCount = ids == null ? 0 : ids.size();
Map<String, Object> 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);
}
@@ -50,6 +50,11 @@ public interface SmHotelSyncService {
*/
R<SmHotelSyncLogEntity> startDownstreamScopePushAsync(String partnerCode, String triggerType);
/**
* 下游全量推前过滤酒店 ID(圣诺:去掉未填 sHotelId/eid 的;其它协议原样返回)。
*/
java.util.List<Long> filterDownstreamPushHotelIds(String partnerCode, java.util.List<Long> hotelIds);
/**
* 根据某次同步日志的 failDetail,只重试失败酒店(支持 DETAIL_PULL / PRICE_ROLL)。
*/
@@ -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<String, Object> 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");
@@ -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;
}
@@ -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<SmHotelSyncLogEntity> pageSyncLogs(Page<SmHotelSyncLogEntity> 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.<SmHotelSyncLogEntity>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<Long> filterDownstreamPushHotelIds(String partnerCode, List<Long> hotelIds) {
return filterShengnuoHotelsRequireEid(partnerCode, hotelIds);
}
/**
* 圣诺推送前过滤:未填外部酒店ID(sHotelId/eid) 的酒店不进入推送队列,避免无意义调对方 getInfo。
* 非圣诺协议原样返回。
*/
private List<Long> filterShengnuoHotelsRequireEid(String partnerCode, List<Long> 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<SmHotelEntity> rows = hotelMapper.selectList(Wrappers.<SmHotelEntity>lambdaQuery()
.select(SmHotelEntity::getId, SmHotelEntity::getSHotelId)
.in(SmHotelEntity::getId, hotelIds));
Set<Long> 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<Long> 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)) {