diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDownstreamPushLogMapper.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDownstreamPushLogMapper.java index a8b56081..a4a9c528 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDownstreamPushLogMapper.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelDownstreamPushLogMapper.java @@ -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 { + /** + * 仅取本页去重酒店 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 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> 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 diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamPushLogService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamPushLogService.java index 7898a9ac..4bc812ce 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamPushLogService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelDownstreamPushLogService.java @@ -31,15 +31,13 @@ public interface SmHotelDownstreamPushLogService extends IService exportGapsBySyncLogId(Long syncLogId); - /** 某次全量推中失败酒店数(按 hotel_id 去重,success=0) */ + /** 某次全量推中失败酒店数(按 hotel_id 去重,success=0);仅兜底,优先用同步日志 failCount */ long countFailedHotelsBySyncLogId(Long syncLogId); - /** - * 分页查某次全量推失败酒店(按 hotel_id 去重)。 - * @return 每行含 hotelId / extHotelId / errorMsg - */ - Page> pageFailedHotelsBySyncLogId(Page page, Long syncLogId); + /** 分页取失败酒店 ID(DISTINCT + LIMIT,不做全量 GROUP BY) */ + List pageDistinctFailedHotelIds(Long syncLogId, long offset, long limit); /** 某次全量推全部失败酒店 ID(供只重试失败) */ List listFailedHotelIdsBySyncLogId(Long syncLogId); + } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java index 81baa1be..2ee12831 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/SmHotelOrderService.java @@ -134,7 +134,8 @@ public interface SmHotelOrderService { R bookUpstreamForExisting(Long orderId); /** - * 下游销售前二次校验(查本地酒店/产品/上架/日历库存/可选验价)。 + * 下游销售前二次校验:本地酒店/产品/上架/日历库存/价差; + * 非本地(如飞猪/商游)会同步调上游 checkAvailability,预检阶段即拦不可订。 * 入参建议:extHotelId、extProductId、checkIn、checkOut、roomCnt、price(可选)。 */ R> checkBookableForDownstream(Map body); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java index d2c9b4bb..7be819d5 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelDownstreamPushLogServiceImpl.java @@ -340,23 +340,12 @@ public class SmHotelDownstreamPushLogServiceImpl } @Override - public Page> pageFailedHotelsBySyncLogId(Page page, Long syncLogId) { - Page> out = new Page<>(page.getCurrent(), page.getSize()); - if (syncLogId == null) { - out.setTotal(0); - out.setRecords(List.of()); - return out; + public List 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> rows = baseMapper.pageFailedHotelsBySyncLogId(syncLogId, offset, page.getSize()); - out.setRecords(rows == null ? List.of() : rows); - return out; + List ids = baseMapper.pageDistinctFailedHotelIds(syncLogId, Math.max(0L, offset), limit); + return ids == null ? List.of() : ids; } @Override 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 8bbafdc8..be49abff 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 @@ -1104,17 +1104,69 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 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 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 createByAdmin(Map body) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java index 305deeb5..caa44593 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelSyncServiceImpl.java @@ -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> raw = downstreamPushLogService.pageFailedHotelsBySyncLogId( - new Page<>(current, size), id); - out.setTotal(raw.getTotal()); - List> maps = raw.getRecords(); - List ids = new ArrayList<>(); - if (CollUtil.isNotEmpty(maps)) { - for (Map 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 pageHotelIds = downstreamPushLogService.pageDistinctFailedHotelIds(id, offset, size); + if (CollUtil.isNotEmpty(pageHotelIds) || totalHint > 0) { + if (totalHint < 0) { + totalHint = downstreamPushLogService.countFailedHotelsBySyncLogId(id); } - Map hotelMap = loadHotelMapByIds(ids); - List rows = new ArrayList<>(); - if (CollUtil.isNotEmpty(maps)) { - for (Map 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 hotelMap = loadHotelMapByIds(pageHotelIds); + Map reasonMap = loadLatestFailReasonByHotelIds(id, pageHotelIds); + List 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 loadLatestFailReasonByHotelIds(Long syncLogId, List hotelIds) { + Map map = new HashMap<>(); + if (syncLogId == null || CollUtil.isEmpty(hotelIds)) { + return map; + } + List rows = downstreamPushLogService.list(Wrappers + .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;