This commit is contained in:
dzc committed 2026-10-30 13:41:37 +08:00
commit ae5af61d6e
16 files changed
+472 -113

No files matched your search

@@ -3,16 +3,24 @@ package com.bsd.pjcloud.supermarket.constant;
/**
* 下游资源推送分类(环启等):可售推 / 关房推 / 纯跳过。
* 见 docs/xiayou/环启下游对接总览.md §5.1
* <p>
* 圣诺特例:有 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() {
@@ -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;
@@ -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;
@@ -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;
@@ -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())) {
@@ -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);
}
@@ -16,9 +16,14 @@ import java.util.Map;
*/
public interface SmHotelMidService {
/** 从上游拉取房型/产品并按加价生成日历,落本地库 */
/** 从上游拉取房型/产品并按加价生成日历,落本地库(房价天数用配置 priceDays) */
R<Map<String, Object>> pullFromUpstream(Long hotelId);
/**
* 同上;priceDaysOverride&gt;0 时覆盖本次房价天数(全量详情可缩窗加速)。
*/
R<Map<String, Object>> pullFromUpstream(Long hotelId, Integer priceDaysOverride);
/**
* 仅调用 getHotelComplexList 落房型(不拉房价),用于单独验证综合信息接口。
*/
@@ -43,6 +48,7 @@ public interface SmHotelMidService {
/**
* 将本地酒店+房型+产品+日历推送给下游销售方。
* 内部分类:可售推 / 关房推 / 纯跳过(见 SmHotelDownstreamPushKind)。
* 圣诺有 eid 时:无产品/无日历走关房,不因「本地空」跳过(防对方残留旧开房)。
* @param partnerCode 指定下游伙伴编码;必填,仅推该伙伴(且须在范围映射内)
*/
R<Map<String, Object>> pushToDownstream(Long hotelId, String partnerCode);
@@ -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)。
*/
@@ -509,6 +509,12 @@ public class SmHotelChannelConfigServiceImpl extends ServiceImpl<SmHotelChannelC
if (dto.getDetailPullIntervalMs() != null && dto.getDetailPullIntervalMs() >= 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());
}
@@ -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<String> filterOutShengnuoWithoutEid(SmHotelEntity hotel, List<String> partners) {
if (CollUtil.isEmpty(partners)) {
return List.of();
}
boolean hasEid = hotel != null && StrUtil.isNotBlank(StrUtil.trim(hotel.getSHotelId()));
if (hasEid) {
return partners;
}
List<String> 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);
@@ -26,15 +26,24 @@ public class SmHotelDownstreamPushLogServiceImpl
@Override
public Page<SmHotelDownstreamPushLogEntity> pageLogs(Page<SmHotelDownstreamPushLogEntity> 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.<SmHotelDownstreamPushLogEntity>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));
}
@@ -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<Map<String, Object>> pullFromUpstream(Long hotelId) {
return pullFromUpstream(hotelId, null);
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<Map<String, Object>> pullFromUpstream(Long hotelId, Integer priceDaysOverride) {
SmHotelEntity hotel = requireChannelWritableHotel(hotelId, SmHotelDataSource.SHANGYOU);
String hotelCode = hotel.getHotelCode();
List<String> 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<String, Object> 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<String, Object> 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:上架+有产品+窗口有日历。
* <ul>
* <li>圣诺:仅无 eid 可 SKIP;有 eid 后无产品/无日历/下架一律 CLOSE(pushHotelClose),
* 避免对方残留旧开房被下单。</li>
* <li>其它下游:无产品仍 SKIP(关房报文依赖本地产品);下架或窗口无日历 → CLOSE;
* 上架+有产品+有日历 → SELL。</li>
* </ul>
*/
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.<SmHotelProductEntity>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.<SmHotelProductEntity>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<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 +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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> skip = new HashMap<>(6);
skip.put("protocol", SmHotelDownstreamProtocol.SHENGNUO);
skip.put("skipped", true);
skip.put("skipReason", reason);
skip.put("dailyCloseCount", 0);
return skip;
}
Map<String, Object> 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<String, Object> 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<SmHotelProductEntity> products, Map<String, Long> roomTypeToNs, Map<String, Long> productToPlan,
List<SmHotelDailyEntity> dailies, LocalDate from, LocalDate to) {
@@ -955,31 +1049,30 @@ public class SmHotelMidServiceImpl implements SmHotelMidService {
Map<LocalDate, SmHotelDailyEntity> 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<String, Object> 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<String, Object> info = new LinkedHashMap<>();
info.put("roomId", nsRoomId);
info.put("planId", e.getValue());
@@ -72,7 +72,16 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
@Override
public Page<SmHotelEntity> pageScope(Page<SmHotelEntity> page,
LambdaQueryWrapper<SmHotelEntity> 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());
}
@@ -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;
@@ -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<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));
}
@@ -735,7 +763,8 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
try {
R<Map<String, Object>> 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<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)) {
@@ -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<String> 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<SmHotelEntity> rows = hotelMapper.selectList(Wrappers.<SmHotelEntity>lambdaQuery()
.select(SmHotelEntity::getId, SmHotelEntity::getHotelCode,
SmHotelEntity::getHotelName)
@@ -861,45 +954,91 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
if (CollUtil.isEmpty(rows)) {
break;
}
List<Callable<Void>> 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<Map<String, Object>> 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<Map<String, Object>> 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<Future<Void>> futures = pullPool.invokeAll(tasks);
for (Future<Void> 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++;
@@ -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 批大小