diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelHuanqiOrderController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelHuanqiOrderController.java index 5cb6fa25..6865da2d 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelHuanqiOrderController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelHuanqiOrderController.java @@ -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> r = hotelOrderService.checkBookableForDownstream(payload); if (r != null && r.getCode() == 0) { resp.put("code", 200); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamAutoPushService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamAutoPushService.java index bde7296f..13e44fca 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamAutoPushService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamAutoPushService.java @@ -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); + /** * 删产品前:对指定产品先向下游关房(避免删光后无法组关房报文)。 */ diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java index b83eddcc..b8d73c32 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamAutoPushServiceImpl.java @@ -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> pendingByHotel = new ConcurrentHashMap<>(); + /** 验价失败补偿:hotelId|partner 防抖,避免下游短时间连验刷爆上游/推送 */ + private final ConcurrentHashMap> 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> 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> 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(); } } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamInboundRetryServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamInboundRetryServiceImpl.java index b4ba5fd5..ddfc1962 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamInboundRetryServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamInboundRetryServiceImpl.java @@ -136,6 +136,9 @@ public class SmHotelDownstreamInboundRetryServiceImpl implements SmHotelDownstre JSONObject payload = JSONUtil.parseObj(rawBody); Map resp = new LinkedHashMap<>(4); if ("inbound_check".equals(action)) { + if (StrUtil.isNotBlank(partner)) { + payload.set("partnerCode", partner); + } R> r = hotelOrderService.checkBookableForDownstream(payload); if (r != null && r.getCode() == 0) { resp.put("code", 200); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuanqiOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuanqiOrderServiceImpl.java index 28e478ad..044fb3c7 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuanqiOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuanqiOrderServiceImpl.java @@ -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> check = hotelOrderService.checkBookableForDownstream(checkBody); boolean bookable = check != null && check.getCode() == 0 && check.getData() != null diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java index c2d3595f..aeb064dc 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelHuidingOrderServiceImpl.java @@ -83,6 +83,9 @@ public class SmHotelHuidingOrderServiceImpl implements SmHotelHuidingOrderServic public Map check(String partnerCode, JSONObject data) { Map outer = ok200(); Map checkBody = toCheckBody(data); + if (StrUtil.isNotBlank(partnerCode)) { + checkBody.put("partnerCode", partnerCode); + } R> r = hotelOrderService.checkBookableForDownstream(checkBody); Map inner = new LinkedHashMap<>(8); List> 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> check = hotelOrderService.checkBookableForDownstream(checkBody); boolean bookable = check != null && check.getCode() == 0 && check.getData() != null && Boolean.TRUE.equals(check.getData().get("bookable")); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOpenApiServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOpenApiServiceImpl.java index 77010d3b..bbd047a9 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOpenApiServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOpenApiServiceImpl.java @@ -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> r = hotelOrderService.checkBookableForDownstream(checkBody); if (r == null || !r.isOk()) { return r == null ? R.failed("可订校验失败") : r; diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java index 9fe2aeb4..8e9c5fde 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java @@ -1075,6 +1075,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 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) diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java index 547c60a9..80c6199b 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelShengnuoOrderServiceImpl.java @@ -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> check = hotelOrderService.checkBookableForDownstream(checkBody); Map checkData = check != null && check.getCode() == 0 ? check.getData() : null; boolean bookable = checkData != null && Boolean.TRUE.equals(checkData.get("bookable"));