小如酒店
This commit is contained in:
1 parent
5b88ce844c
commit
eb242ef185
10 files changed
+334
-26
No files matched your search
+3
@@ -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, "酒店已下架");
|
||||
|
||||
}
|
||||
+103
-5
@@ -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);
|
||||
}
|
||||
|
||||
+7
-1
@@ -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;
|
||||
|
||||
+18
-4
@@ -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));
|
||||
|
||||
+7
-2
@@ -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";
|
||||
}
|
||||
|
||||
+6
@@ -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);
|
||||
}
|
||||
+63
-1
@@ -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));
|
||||
}
|
||||
|
||||
+7
@@ -23,6 +23,13 @@ public interface HotelZaxSyncService {
|
||||
*/
|
||||
Map<String, Object> syncHotelByCode(String hotelCode);
|
||||
|
||||
/**
|
||||
* 可订失败后强制拉详情+日历。
|
||||
* <p>
|
||||
* 使用独立事务:避免外层「下单」事务因校验失败回滚,把刚写入的镜像一并冲掉。
|
||||
*/
|
||||
Map<String, Object> syncHotelByCodeIsolated(String hotelCode);
|
||||
|
||||
/**
|
||||
* 标记下架/删除(资源回调 HOTEL_OFFLINE / HOTEL_DELETE)。
|
||||
*/
|
||||
|
||||
+79
-11
@@ -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 {
|
||||
|
||||
+41
-2
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user