酒店优化

This commit is contained in:
zjf committed 2026-10-31 15:34:26 +08:00
1 parent b9fd7f1294
commit dfae8205a8
6 files changed
+132 -66

No files matched your search

@@ -7,28 +7,28 @@ import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface SmHotelDownstreamPushLogMapper extends PjcloudBaseMapper<SmHotelDownstreamPushLogEntity> {
/**
* 仅取本页去重酒店 ID(走 sync_log_id+success+hotel_id 索引),避免整表 GROUP BY MAX。
*/
@Select("""
SELECT DISTINCT hotel_id FROM canteen_hotel_downstream_push_log
WHERE del_flag = '0' AND sync_log_id = #{syncLogId} AND success = '0' AND hotel_id IS NOT NULL
ORDER BY hotel_id
LIMIT #{offset}, #{limit}
""")
List<Long> pageDistinctFailedHotelIds(@Param("syncLogId") Long syncLogId, @Param("offset") long offset,
@Param("limit") long limit);
@Select("""
SELECT COUNT(DISTINCT hotel_id) FROM canteen_hotel_downstream_push_log
WHERE del_flag = '0' AND sync_log_id = #{syncLogId} AND success = '0' AND hotel_id IS NOT NULL
""")
Long countFailedHotelsBySyncLogId(@Param("syncLogId") Long syncLogId);
@Select("""
SELECT hotel_id AS hotelId, MAX(ext_hotel_id) AS extHotelId, MAX(error_msg) AS errorMsg
FROM canteen_hotel_downstream_push_log
WHERE del_flag = '0' AND sync_log_id = #{syncLogId} AND success = '0' AND hotel_id IS NOT NULL
GROUP BY hotel_id
ORDER BY hotel_id
LIMIT #{offset}, #{limit}
""")
List<Map<String, Object>> pageFailedHotelsBySyncLogId(@Param("syncLogId") Long syncLogId,
@Param("offset") long offset, @Param("limit") long limit);
@Select("""
SELECT DISTINCT hotel_id FROM canteen_hotel_downstream_push_log
WHERE del_flag = '0' AND sync_log_id = #{syncLogId} AND success = '0' AND hotel_id IS NOT NULL
@@ -31,15 +31,13 @@ public interface SmHotelDownstreamPushLogService extends IService<SmHotelDownstr
*/
List<SmHotelDownstreamGapExcel> exportGapsBySyncLogId(Long syncLogId);
/** 某次全量推中失败酒店数(按 hotel_id 去重,success=0) */
/** 某次全量推中失败酒店数(按 hotel_id 去重,success=0);仅兜底,优先用同步日志 failCount */
long countFailedHotelsBySyncLogId(Long syncLogId);
/**
* 分页查某次全量推失败酒店(按 hotel_id 去重)。
* @return 每行含 hotelId / extHotelId / errorMsg
*/
Page<java.util.Map<String, Object>> pageFailedHotelsBySyncLogId(Page<?> page, Long syncLogId);
/** 分页取失败酒店 ID(DISTINCT + LIMIT,不做全量 GROUP BY) */
List<Long> pageDistinctFailedHotelIds(Long syncLogId, long offset, long limit);
/** 某次全量推全部失败酒店 ID(供只重试失败) */
List<Long> listFailedHotelIdsBySyncLogId(Long syncLogId);
}
@@ -134,7 +134,8 @@ public interface SmHotelOrderService {
R<Boolean> bookUpstreamForExisting(Long orderId);
/**
* 下游销售前二次校验(查本地酒店/产品/上架/日历库存/可选验价)。
* 下游销售前二次校验:本地酒店/产品/上架/日历库存/价差;
* 非本地(如飞猪/商游)会同步调上游 checkAvailability,预检阶段即拦不可订。
* 入参建议:extHotelId、extProductId、checkIn、checkOut、roomCnt、price(可选)。
*/
R<Map<String, Object>> checkBookableForDownstream(Map<String, Object> body);
@@ -340,23 +340,12 @@ public class SmHotelDownstreamPushLogServiceImpl
}
@Override
public Page<Map<String, Object>> pageFailedHotelsBySyncLogId(Page<?> page, Long syncLogId) {
Page<Map<String, Object>> out = new Page<>(page.getCurrent(), page.getSize());
if (syncLogId == null) {
out.setTotal(0);
out.setRecords(List.of());
return out;
public List<Long> pageDistinctFailedHotelIds(Long syncLogId, long offset, long limit) {
if (syncLogId == null || limit <= 0) {
return List.of();
}
long total = countFailedHotelsBySyncLogId(syncLogId);
out.setTotal(total);
if (total <= 0) {
out.setRecords(List.of());
return out;
}
long offset = Math.max(0L, (page.getCurrent() - 1) * page.getSize());
List<Map<String, Object>> rows = baseMapper.pageFailedHotelsBySyncLogId(syncLogId, offset, page.getSize());
out.setRecords(rows == null ? List.of() : rows);
return out;
List<Long> ids = baseMapper.pageDistinctFailedHotelIds(syncLogId, Math.max(0L, offset), limit);
return ids == null ? List.of() : ids;
}
@Override
@@ -1104,17 +1104,69 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
}
}
// 第三方来源:额外提示上游可定需在真正下单时再 DoubleCheck
data.put("needUpstreamDoubleCheck", !localDs);
// 非本地(飞猪/商游等):预检阶段就必须打上游可订,避免校验过了下单才发现满房
boolean upstreamChecked = false;
if (!localDs) {
String upstreamMsg = checkUpstreamAvailabilityForDownstream(hotel, product, checkIn, checkOut, roomCnt);
if (upstreamMsg != null) {
reasons.add(upstreamMsg);
}
else {
upstreamChecked = true;
}
}
data.put("upstreamAvailChecked", upstreamChecked);
data.put("needUpstreamDoubleCheck", !localDs && !upstreamChecked);
data.put("bookable", reasons.isEmpty());
data.put("reasons", reasons);
// 仅价差:圣诺等可落单待人工确认,其它硬伤仍拒单
// 仅价差:圣诺等可落单待人工确认,其它硬伤仍拒单(上游不可订不算价差)
boolean priceMismatchOnly = !reasons.isEmpty() && reasons.stream()
.allMatch(r -> r != null && String.valueOf(r).contains("结算价不一致"));
data.put("priceMismatchOnly", priceMismatchOnly);
return R.ok(data);
}
/**
* 下游预检专用:调上游 checkAvailability。
* @return null=可订;非空=不可订/异常原因(写入 reasons)
*/
private String checkUpstreamAvailabilityForDownstream(SmHotelEntity hotel, SmHotelProductEntity product,
LocalDate checkIn, LocalDate checkOut, int roomCnt) {
if (hotel == null || product == null || checkIn == null || checkOut == null) {
return "上游可订校验参数不完整";
}
if (upstreamGatewayRegistry.isUnsupportedChannel(hotel.getSheetName())) {
return "未接入的上游渠道,无法校验可订:" + upstreamGatewayRegistry.resolveChannelKey(hotel.getSheetName());
}
Optional<HotelUpstreamOrderGateway> gatewayOpt = upstreamGatewayRegistry.find(hotel.getSheetName());
if (gatewayOpt.isEmpty()) {
return "未找到上游订单网关,无法校验可订";
}
HotelUpstreamOrderGateway gateway = gatewayOpt.get();
com.alibaba.fastjson.JSONObject upstreamBody = new com.alibaba.fastjson.JSONObject();
upstreamBody.put("hotelId", hotel.getHotelCode());
upstreamBody.put("roomTypeId", StrUtil.blankToDefault(product.getRoomTypeCode(), ""));
upstreamBody.put("rateCode", StrUtil.blankToDefault(product.getPriceCode(), ""));
upstreamBody.put("checkInDate", checkIn.toString());
upstreamBody.put("checkOutDate", checkOut.toString());
upstreamBody.put("guaranteeMode",
StrUtil.blankToDefault(channelConfigService.resolveShangyouRuntime().getOrderGuaranteeMode(), "NONE"));
upstreamBody.put("isGroup", false);
if (StrUtil.isBlank(upstreamBody.getString("hotelId")) || StrUtil.isBlank(upstreamBody.getString("rateCode"))) {
return "上游可订校验缺少 hotelId/rateCode(请检查本地酒店编码与产品价格码)";
}
SmHotelOrderEntity probe = new SmHotelOrderEntity();
probe.setId(null);
try {
return doubleCheckAvailability(gateway, probe, upstreamBody, Math.max(1, roomCnt));
}
catch (Exception e) {
log.warn("|下游预检|上游可订异常|hotel={}|product={}|{}~{}|err={}|", hotel.getHotelCode(),
product.getProductCode(), checkIn, checkOut, e.getMessage());
return "上游可订校验失败:" + StrUtil.blankToDefault(e.getMessage(), e.getClass().getSimpleName());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public R<JSONObject> createByAdmin(Map<String, Object> body) {
@@ -11,6 +11,7 @@ import com.bsd.pjcloud.supermarket.constant.SmHotelDownstreamProtocol;
import com.bsd.pjcloud.supermarket.dto.SmHotelLocalDailyBatchDTO;
import com.bsd.pjcloud.supermarket.dto.SmHotelSyncFailExcel;
import com.bsd.pjcloud.supermarket.dto.SmHotelSyncFailLineVO;
import com.bsd.pjcloud.supermarket.entity.SmHotelDownstreamPushLogEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelSyncLogEntity;
import com.bsd.pjcloud.supermarket.integration.downstream.DownstreamPushLogContext;
@@ -641,36 +642,35 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
long size = out.getSize() <= 0 ? 20 : out.getSize();
if (TYPE_DOWNSTREAM_PUSH.equals(e.getSyncType())) {
long pushFailHotels = downstreamPushLogService.countFailedHotelsBySyncLogId(id);
if (pushFailHotels > 0) {
Page<Map<String, Object>> raw = downstreamPushLogService.pageFailedHotelsBySyncLogId(
new Page<>(current, size), id);
out.setTotal(raw.getTotal());
List<Map<String, Object>> maps = raw.getRecords();
List<Long> ids = new ArrayList<>();
if (CollUtil.isNotEmpty(maps)) {
for (Map<String, Object> m : maps) {
Long hid = toLong(m.get("hotelId"));
if (hid != null) {
ids.add(hid);
}
}
// 总数优先用同步日志 failCount,避免每次 COUNT(DISTINCT) 扫几十万流水
long totalHint = e.getFailCount() == null ? -1L : e.getFailCount().longValue();
long offset = Math.max(0L, (current - 1) * size);
List<Long> pageHotelIds = downstreamPushLogService.pageDistinctFailedHotelIds(id, offset, size);
if (CollUtil.isNotEmpty(pageHotelIds) || totalHint > 0) {
if (totalHint < 0) {
totalHint = downstreamPushLogService.countFailedHotelsBySyncLogId(id);
}
Map<Long, SmHotelEntity> hotelMap = loadHotelMapByIds(ids);
List<SmHotelSyncFailLineVO> rows = new ArrayList<>();
if (CollUtil.isNotEmpty(maps)) {
for (Map<String, Object> m : maps) {
Long hid = toLong(m.get("hotelId"));
SmHotelEntity h = hid == null ? null : hotelMap.get(hid);
SmHotelSyncFailLineVO vo = new SmHotelSyncFailLineVO();
vo.setHotelId(hid);
vo.setHotelCode(h != null ? StrUtil.blankToDefault(h.getHotelCode(), String.valueOf(hid))
: (hid == null ? "" : String.valueOf(hid)));
vo.setHotelName(h != null ? StrUtil.blankToDefault(h.getHotelName(), "-") : "-");
vo.setReason(StrUtil.blankToDefault(str(m.get("errorMsg")), "推送失败"));
vo.setRawLine(vo.getHotelCode() + "|" + vo.getHotelName() + "|" + vo.getReason());
rows.add(vo);
out.setTotal(Math.max(totalHint, 0L));
if (CollUtil.isEmpty(pageHotelIds)) {
out.setRecords(List.of());
return out;
}
Map<Long, SmHotelEntity> hotelMap = loadHotelMapByIds(pageHotelIds);
Map<Long, String> reasonMap = loadLatestFailReasonByHotelIds(id, pageHotelIds);
List<SmHotelSyncFailLineVO> rows = new ArrayList<>(pageHotelIds.size());
for (Long hid : pageHotelIds) {
if (hid == null) {
continue;
}
SmHotelEntity h = hotelMap.get(hid);
SmHotelSyncFailLineVO vo = new SmHotelSyncFailLineVO();
vo.setHotelId(hid);
vo.setHotelCode(h != null ? StrUtil.blankToDefault(h.getHotelCode(), String.valueOf(hid))
: String.valueOf(hid));
vo.setHotelName(h != null ? StrUtil.blankToDefault(h.getHotelName(), "-") : "-");
vo.setReason(StrUtil.blankToDefault(reasonMap.get(hid), "推送失败"));
vo.setRawLine(vo.getHotelCode() + "|" + vo.getHotelName() + "|" + vo.getReason());
rows.add(vo);
}
out.setRecords(rows);
return out;
@@ -733,6 +733,32 @@ public class SmHotelSyncServiceImpl implements SmHotelSyncService {
return map;
}
/** 本页失败酒店的最新失败原因(只查这批 hotelId,不扫全量) */
private Map<Long, String> loadLatestFailReasonByHotelIds(Long syncLogId, List<Long> hotelIds) {
Map<Long, String> map = new HashMap<>();
if (syncLogId == null || CollUtil.isEmpty(hotelIds)) {
return map;
}
List<SmHotelDownstreamPushLogEntity> rows = downstreamPushLogService.list(Wrappers
.<SmHotelDownstreamPushLogEntity>lambdaQuery()
.select(SmHotelDownstreamPushLogEntity::getId, SmHotelDownstreamPushLogEntity::getHotelId,
SmHotelDownstreamPushLogEntity::getErrorMsg)
.eq(SmHotelDownstreamPushLogEntity::getSyncLogId, syncLogId)
.eq(SmHotelDownstreamPushLogEntity::getSuccess, "0")
.in(SmHotelDownstreamPushLogEntity::getHotelId, hotelIds)
.orderByDesc(SmHotelDownstreamPushLogEntity::getId));
if (CollUtil.isEmpty(rows)) {
return map;
}
for (SmHotelDownstreamPushLogEntity row : rows) {
if (row == null || row.getHotelId() == null || map.containsKey(row.getHotelId())) {
continue;
}
map.put(row.getHotelId(), row.getErrorMsg());
}
return map;
}
private static Long toLong(Object v) {
if (v == null) {
return null;