This commit is contained in:
zjf committed 2026-11-06 12:24:28 +08:00
1 parent e5156e29e8
commit 708352cfcb
9 files changed
+164 -1

No files matched your search

@@ -57,6 +57,9 @@ public class SmHotelHuanqiOrderController {
header(request, "sign"), rawBody);
partner = huanqiOrderService.resolvePartnerByAppKey(appKey);
JSONObject payload = JSONUtil.parseObj(rawBody);
if (StrUtil.isNotBlank(partner)) {
payload.set("partnerCode", partner);
}
R<Map<String, Object>> r = hotelOrderService.checkBookableForDownstream(payload);
if (r != null && r.getCode() == 0) {
resp.put("code", 200);
@@ -1,5 +1,7 @@
package com.bsd.pjcloud.supermarket.service;
import java.time.LocalDate;
/**
* 下游资源自动推送:本地/回调变更后防抖推;日更按授权范围扫。
*/
@@ -10,6 +12,13 @@ public interface SmHotelDownstreamAutoPushService {
*/
void scheduleAfterHotelChanged(Long hotelId);
/**
* 下游可订校验失败后异步补偿:上游酒店先按入住区间重拉价态,再向该伙伴推送最新资源;
* 本地酒店跳过上游拉取,直接向该伙伴推送。partnerCode 为空时仅重拉(上游)并走通用自动推。
*/
void scheduleRefreshAndPushAfterCheckFail(Long hotelId, String partnerCode, LocalDate checkIn,
LocalDate checkOut);
/**
* 删产品前:对指定产品先向下游关房(避免删光后无法组关房报文)。
*/
@@ -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.SmHotelDataSource;
import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
@@ -24,6 +25,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -70,6 +72,9 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu
private final ConcurrentHashMap<Long, ScheduledFuture<?>> pendingByHotel = new ConcurrentHashMap<>();
/** 验价失败补偿:hotelId|partner 防抖,避免下游短时间连验刷爆上游/推送 */
private final ConcurrentHashMap<String, ScheduledFuture<?>> pendingCheckFail = new ConcurrentHashMap<>();
private final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor(r -> {
Thread t = new Thread(r, "sm-hotel-downstream-auto-push");
t.setDaemon(true);
@@ -98,6 +103,93 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu
}
}
@Override
public void scheduleRefreshAndPushAfterCheckFail(Long hotelId, String partnerCode, LocalDate checkIn,
LocalDate checkOut) {
if (hotelId == null) {
return;
}
String partner = StrUtil.trim(partnerCode);
String key = hotelId + "|" + StrUtil.blankToDefault(partner, "*");
Long tenantId = TenantContextHolder.getTenantId();
if (tenantId == null) {
tenantId = 1L;
}
Long tid = tenantId;
LocalDate in = checkIn == null ? LocalDate.now() : checkIn;
LocalDate out = checkOut == null ? in.plusDays(1) : checkOut;
pendingCheckFail.compute(key, (k, old) -> {
if (old != null) {
old.cancel(false);
}
// 短防抖:先回验价响应,再异步拉/推,合并下游连验
return scheduler.schedule(() -> {
pendingCheckFail.remove(k);
try {
TenantBroker.runAs(tid, t -> doRefreshAndPushAfterCheckFail(hotelId, partner, in, out));
}
catch (Exception e) {
log.warn("|{}|验价失败补偿异常|hotelId={}|partner={}|err={}|", TAG, hotelId, partner,
e.getMessage());
}
}, 2, TimeUnit.SECONDS);
});
log.info("|{}|已安排验价失败补偿|hotelId={}|partner={}|{}~{}|", TAG, hotelId, partner, in, out);
}
/**
* 上游:按入住区间重拉价态(至少覆盖入住日,不足 7 天补到 7 天);本地:跳过拉取。
* 随后向指定伙伴直推;无伙伴则走通用自动推。
*/
private void doRefreshAndPushAfterCheckFail(Long hotelId, String partner, LocalDate checkIn, LocalDate checkOut) {
SmHotelEntity hotel = hotelMapper.selectById(hotelId);
if (hotel == null) {
return;
}
boolean local = SmHotelDataSource.isLocal(hotel.getSheetName());
if (!local) {
LocalDate start = checkIn;
LocalDate endExclusive = checkOut != null && checkOut.isAfter(checkIn) ? checkOut : checkIn.plusDays(1);
// 验价失败常因价态过期:至少拉 7 天,最多 30 天,避免一次过重
if (endExclusive.isBefore(start.plusDays(7))) {
endExclusive = start.plusDays(7);
}
if (endExclusive.isAfter(start.plusDays(30))) {
endExclusive = start.plusDays(30);
}
try {
if (SmHotelDataSource.isWritableByChannel(hotel.getSheetName(), SmHotelDataSource.SHANGYOU)
|| StrUtil.isBlank(SmHotelDataSource.normalize(hotel.getSheetName()))) {
R<Map<String, Object>> pulled = midService.pullPriceRange(hotelId, start, endExclusive);
log.info("|{}|验价失败已重拉上游|hotelId={}|partner={}|{}~{}|ok={}|msg={}|", TAG, hotelId, partner,
start, endExclusive, pulled != null && pulled.isOk(),
pulled == null ? null : pulled.getMsg());
}
else {
log.info("|{}|验价失败跳过重拉(非商游可写来源)|hotelId={}|ds={}|", TAG, hotelId,
hotel.getSheetName());
}
}
catch (Exception e) {
log.warn("|{}|验价失败重拉上游失败,仍尝试推本地快照|hotelId={}|err={}|", TAG, hotelId, e.getMessage());
}
}
if (StrUtil.isNotBlank(partner)) {
long t0 = System.currentTimeMillis();
try {
R<Map<String, Object>> r = midService.pushToDownstream(hotelId, partner);
log.info("|{}|验价失败已直推伙伴|hotelId={}|partner={}|ok={}|costMs={}|msg={}|", TAG, hotelId, partner,
r != null && r.isOk(), System.currentTimeMillis() - t0, r == null ? null : r.getMsg());
}
catch (Exception e) {
log.warn("|{}|验价失败直推伙伴失败|hotelId={}|partner={}|err={}|", TAG, hotelId, partner, e.getMessage());
}
return;
}
// 无指定伙伴:走原有命中范围自动推
scheduleAfterHotelChanged(hotelId);
}
@Override
public void closeProductsOnMatchedPartners(Long hotelId, String productCode, String roomTypeCode) {
if (hotelId == null || StrUtil.isBlank(productCode)) {
@@ -347,6 +439,18 @@ public class SmHotelDownstreamAutoPushServiceImpl implements SmHotelDownstreamAu
@PreDestroy
public void shutdown() {
pendingCheckFail.values().forEach(f -> {
if (f != null) {
f.cancel(false);
}
});
pendingCheckFail.clear();
pendingByHotel.values().forEach(f -> {
if (f != null) {
f.cancel(false);
}
});
pendingByHotel.clear();
scheduler.shutdownNow();
}
}
@@ -136,6 +136,9 @@ public class SmHotelDownstreamInboundRetryServiceImpl implements SmHotelDownstre
JSONObject payload = JSONUtil.parseObj(rawBody);
Map<String, Object> resp = new LinkedHashMap<>(4);
if ("inbound_check".equals(action)) {
if (StrUtil.isNotBlank(partner)) {
payload.set("partnerCode", partner);
}
R<Map<String, Object>> r = hotelOrderService.checkBookableForDownstream(payload);
if (r != null && r.getCode() == 0) {
resp.put("code", 200);
@@ -184,6 +184,9 @@ public class SmHotelHuanqiOrderServiceImpl implements SmHotelHuanqiOrderService
checkBody.put("checkIn", order.getCheckInDate() == null ? null : order.getCheckInDate().toString());
checkBody.put("checkOut", order.getCheckOutDate() == null ? null : order.getCheckOutDate().toString());
checkBody.put("roomCnt", order.getCount() == null ? 1 : order.getCount());
if (StrUtil.isNotBlank(partnerCode)) {
checkBody.put("partnerCode", partnerCode);
}
// 推单履约预检不带 price:环启 price 是结算价,与本地日历卖价口径不同,避免误拦
R<Map<String, Object>> check = hotelOrderService.checkBookableForDownstream(checkBody);
boolean bookable = check != null && check.getCode() == 0 && check.getData() != null
@@ -83,6 +83,9 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
public Map<String, Object> check(String partnerCode, JSONObject data) {
Map<String, Object> outer = ok200();
Map<String, Object> checkBody = toCheckBody(data);
if (StrUtil.isNotBlank(partnerCode)) {
checkBody.put("partnerCode", partnerCode);
}
R<Map<String, Object>> r = hotelOrderService.checkBookableForDownstream(checkBody);
Map<String, Object> inner = new LinkedHashMap<>(8);
List<Map<String, Object>> priceList = new ArrayList<>();
@@ -175,6 +178,9 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic
checkBody.put("checkOut", mapped.checkOut.toString());
checkBody.put("roomCnt", mapped.quantity);
checkBody.put("price", mapped.orderAmount);
if (StrUtil.isNotBlank(partnerCode)) {
checkBody.put("partnerCode", partnerCode);
}
R<Map<String, Object>> check = hotelOrderService.checkBookableForDownstream(checkBody);
boolean bookable = check != null && check.getCode() == 0 && check.getData() != null
&& Boolean.TRUE.equals(check.getData().get("bookable"));
@@ -359,6 +359,10 @@ public class SmHotelOpenApiServiceImpl implements SmHotelOpenApiService {
checkBody.put("checkOut", body.get("checkOutDate"));
checkBody.put("roomCnt", body.get("count") != null ? body.get("count") : body.get("quantity"));
checkBody.put("price", body.get("totalAmount") != null ? body.get("totalAmount") : body.get("totalPrice"));
String partner = resolvePartnerCode(app);
if (StrUtil.isNotBlank(partner)) {
checkBody.put("partnerCode", partner);
}
R<Map<String, Object>> r = hotelOrderService.checkBookableForDownstream(checkBody);
if (r == null || !r.isOk()) {
return r == null ? R.failed("可订校验失败") : r;
@@ -1075,6 +1075,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
}
}
BigDecimal expectPrice = toDecimal(body.get("price") != null ? body.get("price") : body.get("totalAmount"));
String partnerCode = StrUtil.trim(str(body.get("partnerCode")));
data.put("extHotelId", extHotelId);
data.put("extProductId", extProductId);
@@ -1082,6 +1083,9 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
data.put("checkIn", checkIn == null ? null : checkIn.toString());
data.put("checkOut", checkOut == null ? null : checkOut.toString());
data.put("roomCnt", roomCnt);
if (StrUtil.isNotBlank(partnerCode)) {
data.put("partnerCode", partnerCode);
}
if (StrUtil.isBlank(extHotelId)) {
reasons.add("extHotelId 不能为空");
@@ -1126,6 +1130,8 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
data.put("productExists", false);
data.put("bookable", false);
data.put("reasons", reasons);
// 产品缺失也触发补偿:上游酒店重拉后推下游,便于补齐价态/产品
scheduleRefreshAfterDownstreamCheckFail(hotel.getId(), partnerCode, checkIn, checkOut);
return R.ok(data);
}
data.put("productExists", true);
@@ -1194,15 +1200,37 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
}
data.put("upstreamAvailChecked", upstreamChecked);
data.put("needUpstreamDoubleCheck", !localDs && !upstreamChecked);
data.put("bookable", reasons.isEmpty());
boolean bookable = reasons.isEmpty();
data.put("bookable", bookable);
data.put("reasons", reasons);
// 仅价差:圣诺等可落单待人工确认,其它硬伤仍拒单(上游不可订不算价差)
boolean priceMismatchOnly = !reasons.isEmpty() && reasons.stream()
.allMatch(r -> r != null && String.valueOf(r).contains("结算价不一致"));
data.put("priceMismatchOnly", priceMismatchOnly);
if (!bookable) {
// 验价失败异步补偿:上游先重拉价态,再向该伙伴推最新资源,避免下游一直用旧快照
scheduleRefreshAfterDownstreamCheckFail(hotel.getId(), partnerCode, checkIn, checkOut);
}
return R.ok(data);
}
/**
* 下游可订校验失败后异步补偿(不阻塞验价响应)。
*/
private void scheduleRefreshAfterDownstreamCheckFail(Long hotelId, String partnerCode, LocalDate checkIn,
LocalDate checkOut) {
if (hotelId == null) {
return;
}
try {
downstreamAutoPushService.scheduleRefreshAndPushAfterCheckFail(hotelId, partnerCode, checkIn, checkOut);
}
catch (Exception e) {
log.warn("|下游预检|安排验价失败补偿失败|hotelId={}|partner={}|err={}|", hotelId, partnerCode,
e.getMessage());
}
}
/**
* 下游预检专用:调上游 checkAvailability。
* @return null=可订;非空=不可订/异常原因(写入 reasons)
@@ -127,6 +127,9 @@ public class SmHotelShengnuoOrderServiceImpl implements SmHotelShengnuoOrderServ
checkBody.put("checkOut", m.checkOut.toString());
checkBody.put("roomCnt", m.quantity);
checkBody.put("price", m.orderAmount);
if (StrUtil.isNotBlank(partnerCode)) {
checkBody.put("partnerCode", partnerCode);
}
R<Map<String, Object>> check = hotelOrderService.checkBookableForDownstream(checkBody);
Map<String, Object> checkData = check != null && check.getCode() == 0 ? check.getData() : null;
boolean bookable = checkData != null && Boolean.TRUE.equals(checkData.get("bookable"));