Merge branch 'master' of https://gitea.zianxin.com/zianxin/biz-api-cloud-zax
This commit is contained in:
commit
ae5af61d6e
16 files changed
+472
-113
No files matched your search
+10
-2
@@ -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() {
|
||||
|
||||
+6
@@ -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;
|
||||
|
||||
+4
@@ -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;
|
||||
|
||||
|
||||
+14
-2
@@ -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;
|
||||
|
||||
+1
-1
@@ -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())) {
|
||||
|
||||
+9
-3
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+7
-1
@@ -16,9 +16,14 @@ import java.util.Map;
|
||||
*/
|
||||
public interface SmHotelMidService {
|
||||
|
||||
/** 从上游拉取房型/产品并按加价生成日历,落本地库 */
|
||||
/** 从上游拉取房型/产品并按加价生成日历,落本地库(房价天数用配置 priceDays) */
|
||||
R<Map<String, Object>> pullFromUpstream(Long hotelId);
|
||||
|
||||
/**
|
||||
* 同上;priceDaysOverride>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);
|
||||
|
||||
+5
@@ -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)。
|
||||
*/
|
||||
|
||||
+6
@@ -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());
|
||||
}
|
||||
|
||||
+34
@@ -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);
|
||||
|
||||
+17
-8
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
+140
-47
@@ -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());
|
||||
|
||||
+10
-1
@@ -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());
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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;
|
||||
}
|
||||
|
||||
+202
-46
@@ -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 批大小
|
||||
|
||||
Reference in new issue
Block a user