小如酒店

This commit is contained in:
zjf committed 2026-11-02 19:45:14 +08:00
1 parent 5b88ce844c
commit eb242ef185
10 files changed
+334 -26

No files matched your search

@@ -82,5 +82,8 @@ public interface ErrorCodeConstants {
ErrorCode HOTEL_ZAX_DAILY_NOT_EXISTS = new ErrorCode(1_008_020_003, "本地酒店日历不存在");
ErrorCode HOTEL_ZAX_ORDER_NOT_EXISTS = new ErrorCode(1_008_020_004, "本地酒店订单不存在");
ErrorCode HOTEL_ZAX_ORDER_CANCEL_FAIL = new ErrorCode(1_008_020_005, "酒店订单取消失败:{}");
/** 供应方业务拒绝(可订校验/下单等),msg 透传给 C 端 */
ErrorCode HOTEL_ZAX_SUPPLIER_BIZ_FAIL = new ErrorCode(1_008_020_006, "{}");
ErrorCode HOTEL_ZAX_OFFLINE = new ErrorCode(1_008_020_007, "酒店已下架");
}
@@ -4,6 +4,7 @@ import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.product.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenClient;
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxLocalService;
import cn.iocoder.yudao.module.product.service.hotel.HotelZaxSyncService;
@@ -73,6 +74,11 @@ public class AppHotelController {
String hotelCode = body == null ? null : str(body.get("hotelCode"));
Map<String, Object> local = hotelZaxLocalService.detail(hotelCode);
if (local != null) {
String st = str(local.get("status"));
if (Boolean.TRUE.equals(local.get("offline"))
|| (StrUtil.isNotBlank(st) && !"ACTIVE".equalsIgnoreCase(st))) {
return CommonResult.error(ErrorCodeConstants.HOTEL_ZAX_OFFLINE);
}
return success(local);
}
return success(dataOf(zaxHotelOpenClient.post("hotel/detail", body)));
@@ -80,13 +86,24 @@ public class AppHotelController {
@PermitAll
@PostMapping("/daily")
@Operation(summary = "价态日历(有本地日历用本地,否则供应方)")
@Operation(summary = "价态日历(供应方与本地合并;同产品同日期以供应方为准)")
public CommonResult<Object> daily(@RequestBody Map<String, Object> body) {
Map<String, Object> local = hotelZaxLocalService.daily(body);
if (local != null) {
return success(local);
Object remote = null;
try {
remote = dataOf(zaxHotelOpenClient.post("hotel/daily", body));
}
return success(dataOf(zaxHotelOpenClient.post("hotel/daily", body)));
catch (Exception ignored) {
// 供应方超时/鉴权失败时仍可用本地镜像
}
Map<String, Object> local = null;
try {
local = hotelZaxLocalService.daily(body);
}
catch (Exception ignored) {
// ignore
}
Object merged = mergeDaily(body == null ? null : str(body.get("hotelCode")), remote, local);
return success(merged);
}
@PostMapping("/order/check")
@@ -186,6 +203,87 @@ public class AppHotelController {
return r.get("data");
}
/**
* 合并供应方/本地日历。
* <p>
* 小如后台维护的本地日历优先:此前「供应方覆盖本地」会把本地已开房有库存冲成无库存,详情页全部售罄。
* 供应方只补本地没有的产品/日期。
*/
@SuppressWarnings("unchecked")
private static Map<String, Object> mergeDaily(String hotelCode, Object remote, Map<String, Object> local) {
java.util.LinkedHashMap<String, Map<String, Object>> byKey = new java.util.LinkedHashMap<>();
// 先供应方垫底,再本地覆盖(小如库存页维护为准)
appendDailyRows(byKey, remote, false);
appendDailyRows(byKey, local, true);
Map<String, Object> data = new java.util.LinkedHashMap<>(4);
if (StrUtil.isNotBlank(hotelCode)) {
data.put("hotelCode", hotelCode);
}
else if (local != null && local.get("hotelCode") != null) {
data.put("hotelCode", local.get("hotelCode"));
}
else if (remote instanceof Map && ((Map<?, ?>) remote).get("hotelCode") != null) {
data.put("hotelCode", ((Map<?, ?>) remote).get("hotelCode"));
}
data.put("list", new java.util.ArrayList<>(byKey.values()));
return data;
}
@SuppressWarnings("unchecked")
private static void appendDailyRows(java.util.LinkedHashMap<String, Map<String, Object>> byKey, Object src,
boolean overwrite) {
if (!(src instanceof Map)) {
return;
}
Object listObj = ((Map<?, ?>) src).get("list");
if (!(listObj instanceof java.util.Collection)) {
return;
}
for (Object item : (java.util.Collection<?>) listObj) {
if (!(item instanceof Map)) {
continue;
}
Map<String, Object> row = (Map<String, Object>) item;
String productCode = str(row.get("productCode"));
String stayDate = normalizeStayDate(row.get("stayDate"));
if (StrUtil.hasBlank(productCode, stayDate)) {
continue;
}
String key = productCode.trim() + "|" + stayDate;
if (overwrite || !byKey.containsKey(key)) {
Map<String, Object> copy = new java.util.LinkedHashMap<>(row);
copy.put("productCode", productCode.trim());
copy.put("stayDate", stayDate);
byKey.put(key, copy);
}
}
}
private static String normalizeStayDate(Object raw) {
if (raw == null) {
return null;
}
if (raw instanceof java.util.List) {
java.util.List<?> arr = (java.util.List<?>) raw;
if (arr.size() >= 3) {
try {
int y = Integer.parseInt(String.valueOf(arr.get(0)));
int m = Integer.parseInt(String.valueOf(arr.get(1)));
int d = Integer.parseInt(String.valueOf(arr.get(2)));
return String.format("%04d-%02d-%02d", y, m, d);
}
catch (Exception ignored) {
return null;
}
}
}
String s = String.valueOf(raw).trim().replace('/', '-');
if (s.length() >= 10) {
s = s.substring(0, 10);
}
return s;
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
@@ -62,7 +62,13 @@ public class HotelZaxDO extends BaseDO {
private BigDecimal minSellPrice;
/** ACTIVE / OFFLINE */
/**
* 上架状态:
* ACTIVE=上架;
* OFFLINE=供应方下架;
* LOCAL_OFFLINE=小如后台手动下架(同步不覆盖);
* OUT_OF_SCOPE=授权范围外(重新授权后可恢复上架)
*/
private String status;
private String intro;
@@ -7,6 +7,7 @@ import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
@@ -15,6 +16,9 @@ import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.product.enums.ErrorCodeConstants.HOTEL_ZAX_SUPPLIER_BIZ_FAIL;
/**
* 调用资安心酒店下游开放 API(服务端代签)。
*/
@@ -27,10 +31,10 @@ public class ZaxHotelOpenClient {
public JSONObject post(String apiPath, Map<String, Object> body) {
if (!properties.isEnabled()) {
throw new IllegalStateException("酒店中转未启用,请配置 xrhome.hotel.zax.enabled=true");
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店预订服务暂未开启");
}
if (StrUtil.hasBlank(properties.getBaseUrl(), properties.getAppKey(), properties.getAppSecret())) {
throw new IllegalStateException("酒店中转缺少 baseUrl/appKey/appSecret 配置");
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店预订服务配置不完整");
}
String bodyJson = body == null || body.isEmpty() ? "{}" : JSONUtil.toJsonStr(body);
String timestamp = String.valueOf(System.currentTimeMillis());
@@ -51,7 +55,8 @@ public class ZaxHotelOpenClient {
if (!resp.isOk()) {
log.warn("|zax-hotel|HTTP失败|url={}|status={}|body={}|", url, resp.getStatus(),
StrUtil.maxLength(raw, 300));
throw new IllegalStateException("酒店供应方接口异常 HTTP " + resp.getStatus());
// 业务可读异常,避免被 GlobalExceptionHandler 吞成「系统异常」
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, "酒店供应方接口异常,请稍后重试");
}
JSONObject json = JSONUtil.parseObj(raw);
// 资安心 R:code=0 成功
@@ -59,12 +64,21 @@ public class ZaxHotelOpenClient {
if (code != null && code != 0) {
String msg = StrUtil.blankToDefault(json.getStr("msg"), "酒店供应方业务失败");
log.warn("|zax-hotel|业务失败|url={}|code={}|msg={}|", url, code, StrUtil.maxLength(msg, 200));
throw new IllegalStateException("供应方下单失败:" + msg);
// 透传供应方文案(库存不足/不可订等),供小程序直接弹出
throw exception(HOTEL_ZAX_SUPPLIER_BIZ_FAIL, msg);
}
return json;
}
}
/** 是否供应方业务拒绝(可订校验失败等),便于调用方决定是否刷新本地日历 */
public static boolean isSupplierBizFail(Throwable e) {
if (e instanceof ServiceException) {
return HOTEL_ZAX_SUPPLIER_BIZ_FAIL.getCode().equals(((ServiceException) e).getCode());
}
return false;
}
private static String sign(String appKey, String appSecret, String timestamp, String nonce, String bodyJson) {
Map<String, String> params = new TreeMap<>();
params.put("appKey", StrUtil.nullToEmpty(appKey));
@@ -597,10 +597,15 @@ public class HotelZaxAdminServiceImpl implements HotelZaxAdminService {
return hotel;
}
/**
* 后台上下架:下架写成 LOCAL_OFFLINE,避免与供应方 OFFLINE、授权外 OUT_OF_SCOPE 混淆;
* 同步不会把 LOCAL_OFFLINE 冲回 ACTIVE。
*/
private static String normalizeStatus(String status) {
String s = StrUtil.trim(status);
if ("OFFLINE".equalsIgnoreCase(s)) {
return "OFFLINE";
if ("OFFLINE".equalsIgnoreCase(s) || "LOCAL_OFFLINE".equalsIgnoreCase(s)
|| "OUT_OF_SCOPE".equalsIgnoreCase(s)) {
return "LOCAL_OFFLINE";
}
return "ACTIVE";
}
@@ -33,4 +33,10 @@ public interface HotelZaxLocalService {
/** 用已查到的日历行报价,规则同 quoteStay。 */
BigDecimal quoteFromDailyRows(List<Map<String, Object>> rows, String productCode, LocalDate checkIn,
LocalDate checkOut, int rooms);
/**
* 供应方判定不可订后,把本地对应夜次标成不可售,避免详情页仍显示可订。
* 仅改已有日历行;独立于外层事务由调用方用独立事务或 REQUIRES_NEW 包一层更稳妥。
*/
int markStayUnbookable(String hotelCode, String productCode, LocalDate checkIn, LocalDate checkOut);
}
@@ -14,6 +14,8 @@ import cn.iocoder.yudao.module.product.dal.mysql.hotel.HotelZaxRoomMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
@@ -89,6 +91,14 @@ public class HotelZaxLocalServiceImpl implements HotelZaxLocalService {
if (h == null) {
return null;
}
// 非上架:返回标记,避免 App 再回源供应方把下架/未授权酒店透出
if (!isAppOnlineStatus(h.getStatus())) {
Map<String, Object> offline = new LinkedHashMap<>(4);
offline.put("hotelCode", h.getHotelCode());
offline.put("status", StrUtil.blankToDefault(StrUtil.trim(h.getStatus()), "OFFLINE"));
offline.put("offline", Boolean.TRUE);
return offline;
}
Map<String, Object> data = toBrief(h);
data.put("carouselImages", h.getCarouselImages());
data.put("intro", h.getIntro());
@@ -148,10 +158,12 @@ public class HotelZaxLocalServiceImpl implements HotelZaxLocalService {
row.put("roomTypeCode", d.getRoomTypeCode());
row.put("stayDate", d.getStayDate() == null ? null : d.getStayDate().toString());
row.put("sellPrice", d.getSellPrice());
row.put("costPrice", d.getCostPrice());
row.put("stock", stock);
row.put("sold", sold);
row.put("remain", Math.max(0, stock - sold));
row.put("dailyStatus", d.getDailyStatus());
// 空状态按开房,避免前端 Number(null)===0 误判售罄
row.put("dailyStatus", d.getDailyStatus() == null ? 1 : d.getDailyStatus());
list.add(row);
}
Map<String, Object> data = new LinkedHashMap<>(4);
@@ -225,6 +237,50 @@ public class HotelZaxLocalServiceImpl implements HotelZaxLocalService {
return nightSum.multiply(new BigDecimal(need)).setScale(2, java.math.RoundingMode.HALF_UP);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public int markStayUnbookable(String hotelCode, String productCode, LocalDate checkIn, LocalDate checkOut) {
if (StrUtil.hasBlank(hotelCode, productCode) || checkIn == null || checkOut == null
|| !checkOut.isAfter(checkIn)) {
return 0;
}
HotelZaxDO hotel = hotelZaxMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDO>()
.eq(HotelZaxDO::getHotelCode, hotelCode.trim())
.last("limit 1"));
if (hotel == null) {
return 0;
}
int updated = 0;
for (LocalDate day = checkIn; day.isBefore(checkOut); day = day.plusDays(1)) {
HotelZaxDailyDO row = dailyMapper.selectOne(new LambdaQueryWrapperX<HotelZaxDailyDO>()
.eq(HotelZaxDailyDO::getHotelId, hotel.getId())
.eq(HotelZaxDailyDO::getProductCode, productCode.trim())
.eq(HotelZaxDailyDO::getStayDate, day)
.last("limit 1"));
if (row == null) {
continue;
}
int stock = row.getStock() == null ? 0 : row.getStock();
int sold = row.getSold() == null ? 0 : row.getSold();
boolean changed = false;
// 无剩余可售:把 sold 顶到 stock,页面 remain=0
if (stock - sold > 0) {
row.setSold(stock);
changed = true;
}
if (row.getDailyStatus() == null || row.getDailyStatus() != 0) {
// 供应方已拒订,本地先关房,避免仍显示可订
row.setDailyStatus(0);
changed = true;
}
if (changed) {
dailyMapper.updateById(row);
updated++;
}
}
return updated;
}
private static String stayYmd(Object raw) {
if (raw == null) {
return null;
@@ -326,6 +382,7 @@ public class HotelZaxLocalServiceImpl implements HotelZaxLocalService {
row.put("mealType", p.getMealType());
row.put("mealCount", p.getMealCount());
row.put("sellPrice", p.getSellPrice());
row.put("stock", p.getStock() == null ? 0 : p.getStock());
out.add(row);
}
return out;
@@ -414,6 +471,11 @@ public class HotelZaxLocalServiceImpl implements HotelZaxLocalService {
}
}
/** App 可见:仅 ACTIVE */
private static boolean isAppOnlineStatus(String status) {
return "ACTIVE".equalsIgnoreCase(StrUtil.trim(status));
}
private static String trim(Object o) {
return o == null ? null : StrUtil.trim(String.valueOf(o));
}
@@ -23,6 +23,13 @@ public interface HotelZaxSyncService {
*/
Map<String, Object> syncHotelByCode(String hotelCode);
/**
* 可订失败后强制拉详情+日历。
* <p>
* 使用独立事务:避免外层「下单」事务因校验失败回滚,把刚写入的镜像一并冲掉。
*/
Map<String, Object> syncHotelByCodeIsolated(String hotelCode);
/**
* 标记下架/删除(资源回调 HOTEL_OFFLINE / HOTEL_DELETE)。
*/
@@ -20,6 +20,7 @@ import cn.iocoder.yudao.module.product.framework.hotel.ZaxHotelOpenProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@@ -114,6 +115,10 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
syncedCodes.add(code);
upsertHotelBrief(row);
upsert++;
// 供应方已下架:只落状态,不再拉详情/价态
if (isOfflineStatus(row.getStr("status"))) {
continue;
}
try {
DetailPullResult dr = pullDetailResult(code);
if (dr.ok) {
@@ -167,7 +172,10 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
return out;
}
/** 全量同步后,不在本次授权结果集中的本地酒店下架 */
/**
* 全量同步后:不在本次授权结果集中的本地酒店标为 OUT_OF_SCOPE(非 LOCAL_OFFLINE)。
* 重新授权后再同步可恢复 ACTIVE,避免「先撤权再授权」一直下架。
*/
private int markOutOfScopeOffline(Set<String> syncedCodes) {
List<HotelZaxDO> locals = hotelZaxMapper.selectList(new LambdaQueryWrapperX<>());
int n = 0;
@@ -178,10 +186,12 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
if (syncedCodes.contains(h.getHotelCode().trim())) {
continue;
}
if ("OFFLINE".equalsIgnoreCase(h.getStatus())) {
String st = StrUtil.trim(h.getStatus());
// 后台手动下架、供应方下架、已是范围外:不再改
if (isLocalOffline(st) || isOfflineStatus(st) || isOutOfScope(st)) {
continue;
}
h.setStatus("OFFLINE");
h.setStatus("OUT_OF_SCOPE");
h.setSyncedAt(LocalDateTime.now());
hotelZaxMapper.updateById(h);
n++;
@@ -192,17 +202,29 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, Object> syncHotelByCode(String hotelCode) {
return doSyncHotelByCode(hotelCode);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public Map<String, Object> syncHotelByCodeIsolated(String hotelCode) {
return doSyncHotelByCode(hotelCode);
}
private Map<String, Object> doSyncHotelByCode(String hotelCode) {
if (StrUtil.isBlank(hotelCode)) {
throw new IllegalArgumentException("hotelCode 不能为空");
}
String code = hotelCode.trim();
boolean ok = pullDetailResult(code).ok;
if (!ok) {
DetailPullResult pull = pullDetailResult(code);
if (!pull.ok) {
throw new IllegalStateException("供应方无此酒店详情: " + code);
}
Map<String, Object> out = new LinkedHashMap<>(4);
out.put("hotelCode", code);
out.put("ok", true);
out.put("dailyRows", pull.dailyRows);
out.put("dailyFail", pull.dailyFail);
return out;
}
@@ -249,7 +271,7 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
if (minSell != null) {
entity.setMinSellPrice(minSell);
}
entity.setStatus(StrUtil.blankToDefault(row.getStr("status"), "ACTIVE"));
entity.setStatus(resolveSyncStatus(exist == null ? null : exist.getStatus(), row.getStr("status")));
entity.setSupplierUpdateTime(parseDt(row.getStr("updateTime")));
entity.setSyncedAt(LocalDateTime.now());
if (exist == null) {
@@ -260,6 +282,44 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
}
}
/**
* 同步状态合并:
* - 供应方 OFFLINE → OFFLINE
* - 本地 LOCAL_OFFLINE(后台手动下架)不被 ACTIVE 冲掉
* - OUT_OF_SCOPE / 历史误标的 OFFLINE:供应方 ACTIVE 时可恢复上架(重新授权场景)
*/
private static String resolveSyncStatus(String localStatus, String incomingRaw) {
String local = StrUtil.trim(localStatus);
String incoming = StrUtil.trim(incomingRaw);
if (isOfflineStatus(incoming)) {
return "OFFLINE";
}
if (isLocalOffline(local)) {
return "LOCAL_OFFLINE";
}
boolean incomingActive = StrUtil.isBlank(incoming) || "ACTIVE".equalsIgnoreCase(incoming);
if (incomingActive) {
// 重新进入授权范围:OUT_OF_SCOPE 恢复;历史撤权曾写成 OFFLINE 的也可恢复
if (isOutOfScope(local) || isOfflineStatus(local)) {
return "ACTIVE";
}
return StrUtil.blankToDefault(local, "ACTIVE");
}
return StrUtil.blankToDefault(local, "ACTIVE");
}
private static boolean isOfflineStatus(String status) {
return "OFFLINE".equalsIgnoreCase(StrUtil.trim(status));
}
private static boolean isLocalOffline(String status) {
return "LOCAL_OFFLINE".equalsIgnoreCase(StrUtil.trim(status));
}
private static boolean isOutOfScope(String status) {
return "OUT_OF_SCOPE".equalsIgnoreCase(StrUtil.trim(status));
}
private DetailPullResult pullDetailResult(String hotelCode) {
DetailPullResult result = new DetailPullResult();
Map<String, Object> req = new HashMap<>();
@@ -299,9 +359,8 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
if (StrUtil.isNotBlank(data.getStr("hotelName"))) {
exist.setHotelName(data.getStr("hotelName"));
}
if (StrUtil.isNotBlank(data.getStr("status"))) {
exist.setStatus(data.getStr("status"));
}
// 详情同步同样遵守:仅供应方 OFFLINE 可改状态,不把本地下架冲成上架
exist.setStatus(resolveSyncStatus(exist.getStatus(), data.getStr("status")));
exist.setSyncedAt(LocalDateTime.now());
hotelZaxMapper.updateById(exist);
@@ -433,9 +492,18 @@ public class HotelZaxSyncServiceImpl implements HotelZaxSyncService {
entity.setStock(row.getInt("stock"));
Integer status = row.getInt("dailyStatus");
entity.setDailyStatus(status == null ? Integer.valueOf(1) : status);
// 可售以供应方为准:更新时也要同步 sold,否则本地仍显示有房
Integer sold = row.getInt("sold");
if (sold != null) {
entity.setSold(sold);
}
else if (exist == null) {
entity.setSold(0);
}
else if (entity.getSold() == null) {
entity.setSold(0);
}
if (exist == null) {
Integer sold = row.getInt("sold");
entity.setSold(sold == null ? Integer.valueOf(0) : sold);
dailyMapper.insert(entity);
}
else {
@@ -68,6 +68,8 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
private final HotelZaxLocalService hotelZaxLocalService;
private final HotelZaxSyncService hotelZaxSyncService;
private final PayOrderApi payOrderApi;
private final PayRefundApi payRefundApi;
@@ -139,8 +141,14 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
openBody.put("priceCode", trim(req.get("priceCode")));
}
// 1) 可订校验(不落供应方订单)
openClient.post("order/check", openBody);
// 1) 可订校验(不落供应方订单);失败说明本地镜像落后,独立事务拉齐并关不可售夜次
try {
openClient.post("order/check", openBody);
}
catch (ServiceException e) {
refreshHotelMirrorAfterCheckFail(hotelCode, productCode, checkIn, checkOut, e.getMessage());
throw e;
}
// 2) 本地待支付单;供应方预定推迟到微信支付成功
HotelZaxOrderDO local = new HotelZaxOrderDO();
@@ -583,6 +591,37 @@ public class HotelZaxTradeServiceImpl implements HotelZaxTradeService {
return openBody;
}
/**
* 供应方拒订 = 本地酒店信息已落后。
* 用独立事务拉详情+日历;再把本次入住夜次本地标成不可售,避免「页面可订却一直订不了」。
*/
private void refreshHotelMirrorAfterCheckFail(String hotelCode, String productCode, LocalDate checkIn,
LocalDate checkOut, String failMsg) {
if (StrUtil.isBlank(hotelCode)) {
return;
}
// failMsg 仅用于日志;供应方拒订一律拉齐,避免镜像漂移导致「页面可订却订不了」
log.info("|hotel-order|可订失败准备刷新镜像|hotelCode={}|product={}|msg={}|", hotelCode, productCode,
StrUtil.maxLength(StrUtil.nullToEmpty(failMsg), 120));
try {
Map<String, Object> sync = hotelZaxSyncService.syncHotelByCodeIsolated(hotelCode);
log.info("|hotel-order|可订失败已独立事务刷新酒店镜像|hotelCode={}|dailyRows={}|dailyFail={}|",
hotelCode, sync == null ? null : sync.get("dailyRows"),
sync == null ? null : sync.get("dailyFail"));
}
catch (Exception ex) {
log.warn("|hotel-order|可订失败后刷新本地镜像失败|hotelCode={}|{}", hotelCode, ex.getMessage());
}
try {
int n = hotelZaxLocalService.markStayUnbookable(hotelCode, productCode, checkIn, checkOut);
log.info("|hotel-order|可订失败已标记本地不可售夜次|hotelCode={}|product={}|nights={}|", hotelCode,
productCode, n);
}
catch (Exception ex) {
log.warn("|hotel-order|可订失败后标记本地不可售失败|hotelCode={}|{}", hotelCode, ex.getMessage());
}
}
private BigDecimal quoteStayAmount(String hotelCode, String productCode, LocalDate checkIn, LocalDate checkOut,
int rooms) {
BigDecimal local = hotelZaxLocalService.quoteStay(hotelCode, productCode, checkIn, checkOut, rooms);