diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java index ad579341..1ea3d769 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java @@ -17,6 +17,7 @@ import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity; import com.bsd.pjcloud.supermarket.entity.SmHotelUserSearchPrefEntity; import com.bsd.pjcloud.supermarket.mapper.SmHotelDailyMapper; +import com.bsd.pjcloud.supermarket.mapper.SmHotelMapper; import com.bsd.pjcloud.supermarket.mapper.SmHotelProductMapper; import com.bsd.pjcloud.supermarket.mapper.SmHotelRoomMapper; import com.bsd.pjcloud.supermarket.mapper.SmHotelUserSearchPrefMapper; @@ -89,7 +90,7 @@ public class SmHotelAppController { private static final String CANDIDATE_CACHE_OVERFLOW = "OVERFLOW"; /** 列表页结果短缓存(同条件重复进入/下拉) */ - private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v12:"; + private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v13:"; private static final long PAGE_CACHE_SECONDS = 45L; @@ -100,8 +101,13 @@ public class SmHotelAppController { /** 城市/关键字等缩小后,在该规模内内存排序(超出则 SQL LIMIT,禁止全表 list) */ private static final int MAX_SCOPED_HOTEL_IDS = 8000; + /** 店名包含搜索最多捞取条数(禁止 ORDER BY 全表 filesort) */ + private static final int KEYWORD_CONTAINS_FETCH = 80; + private final SmHotelService hotelService; + private final SmHotelMapper hotelMapper; + private final SmHotelMidService midService; private final SmHotelOrderService orderService; @@ -228,31 +234,28 @@ public class SmHotelAppController { return R.ok(cached); } - boolean hasArea = districtAdcode != null || StrUtil.isNotBlank(city) || StrUtil.isNotBlank(district); - var wrapper = applyListColumns(baseOnlineListWrapper(keyword, city, district, districtAdcode, bizArea, - starList, minScore, facilityList, true)); - if (hasArea) { - applyGuestCapacityFilter(wrapper, needCap); - applyMinProductPriceFilter(wrapper, minPrice, maxPrice); + Page raw; + if (StrUtil.isNotBlank(keyword)) { + // 店名搜索专用路径:禁止「LIKE %kw% + ORDER BY」全表 filesort(搜「小如」会超时变空) + raw = pageByHotelNameKeyword(page, keyword.trim(), city, district, districtAdcode, starList); } - wrapper.last("ORDER BY star_level DESC, id DESC"); - Page raw = pageWithoutCount(page, wrapper); - if (StrUtil.isNotBlank(keyword) && (raw.getRecords() == null || raw.getRecords().isEmpty())) { - var infix = applyListColumns(baseOnlineListWrapper(keyword, city, district, districtAdcode, bizArea, - starList, minScore, facilityList, false)); + else { + boolean hasArea = districtAdcode != null || StrUtil.isNotBlank(city) || StrUtil.isNotBlank(district); + var wrapper = applyListColumns(baseOnlineListWrapper(null, city, district, districtAdcode, bizArea, + starList, minScore, facilityList, true)); if (hasArea) { - applyGuestCapacityFilter(infix, needCap); - applyMinProductPriceFilter(infix, minPrice, maxPrice); + applyGuestCapacityFilter(wrapper, needCap); + applyMinProductPriceFilter(wrapper, minPrice, maxPrice); } - infix.last("ORDER BY star_level DESC, id DESC"); - raw = pageWithoutCount(page, infix); - } - int got = raw.getRecords() == null ? 0 : raw.getRecords().size(); - if (hasArea && got < Math.max(1L, Math.min(page.getSize(), 50L)) && page.getCurrent() <= 1) { - String uncodedName = StrUtil.firstNonBlank(city, district); - if (StrUtil.isNotBlank(uncodedName)) { - fillUncodedHotels(raw, uncodedName, keyword, bizArea, starList, minScore, facilityList, needCap, - minPrice, maxPrice); + wrapper.last("ORDER BY star_level DESC, id DESC"); + raw = pageWithoutCount(page, wrapper); + int got = raw.getRecords() == null ? 0 : raw.getRecords().size(); + if (hasArea && got < Math.max(1L, Math.min(page.getSize(), 50L)) && page.getCurrent() <= 1) { + String uncodedName = StrUtil.firstNonBlank(city, district); + if (StrUtil.isNotBlank(uncodedName)) { + fillUncodedHotels(raw, uncodedName, null, bizArea, starList, minScore, facilityList, needCap, + minPrice, maxPrice); + } } } List ids = raw.getRecords().stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList(); @@ -420,6 +423,55 @@ public class SmHotelAppController { return out; } + /** + * 按酒店名搜索: + * 1) 前缀匹配(idx_hotel_name + ORDER BY,快); + * 2) 无结果再含匹配——SQL 不做 ORDER BY,只 LIMIT,再内存排序(避免超时变空列表)。 + * 有城市时尽量落到 district_adcode 区间。 + */ + private Page pageByHotelNameKeyword(Page page, String keyword, String city, + String district, Long districtAdcode, List starList) { + long size = Math.max(1L, Math.min(page.getSize(), 50L)); + long current = Math.max(1L, page.getCurrent()); + int fetch = (int) Math.min(KEYWORD_CONTAINS_FETCH, Math.max(size + 1, size * current + 1)); + Long adFrom = null; + Long adTo = null; + Long adcode = districtAdcode; + if (adcode == null && StrUtil.isNotBlank(city)) { + adcode = resolvePlaceAdcode(city.trim(), true); + } + if (adcode == null && StrUtil.isNotBlank(district) && StrUtil.isBlank(city)) { + adcode = resolvePlaceAdcode(district.trim(), false); + } + if (adcode != null) { + long[] range = HotelAreaAdcodeUtil.areaAdcodeRange(adcode); + adFrom = range[0]; + adTo = range[1]; + } + Collection stars = starList == null || starList.isEmpty() ? null : starList; + List rows = hotelMapper.selectAppByNamePrefix(keyword, adFrom, adTo, stars, fetch); + if (rows == null || rows.isEmpty()) { + // 含匹配绝不带 ORDER BY;有城市时区间小,全国则最多捞 KEYWORD_CONTAINS_FETCH 条后内存排 + rows = hotelMapper.selectAppByNameContains(keyword, adFrom, adTo, stars, fetch); + } + if (rows == null) { + rows = List.of(); + } + List sorted = new ArrayList<>(rows); + sorted.sort(Comparator + .comparing((SmHotelEntity h) -> h.getStarLevel() == null ? 0 : h.getStarLevel()) + .reversed() + .thenComparing((SmHotelEntity h) -> h.getId() == null ? 0L : h.getId(), Comparator.reverseOrder())); + int from = (int) Math.min((current - 1) * size, sorted.size()); + int to = (int) Math.min(from + size, sorted.size()); + List slice = from >= sorted.size() ? List.of() : sorted.subList(from, to); + boolean hasMore = sorted.size() > to || sorted.size() >= fetch; + long total = (current - 1) * size + slice.size() + (hasMore ? 1 : 0); + Page out = new Page<>(current, size, total); + out.setRecords(new ArrayList<>(slice)); + return out; + } + /** 上架 + 区域/关键字/星级等公共条件(不含可售日历、排序) */ private LambdaQueryWrapper baseOnlineListWrapper(String keyword, String city, String district, Long districtAdcode, String bizArea, List starList, BigDecimal minScore, @@ -577,6 +629,7 @@ public class SmHotelAppController { catch (Exception e) { log.warn("|酒店App|地名adcode缓存读取失败|{}", e.getMessage()); } + // 只精确匹配区名,禁止 LIKE 全表扫(否则选城市本身就会卡死) String shortName = HotelAreaAdcodeUtil.shortenAreaName(name); List rows = hotelService.list(Wrappers.lambdaQuery() .select(SmHotelEntity::getDistrictAdcode, SmHotelEntity::getDistrict) @@ -589,11 +642,11 @@ public class SmHotelAppController { .eq(SmHotelEntity::getDistrict, name + "区") .or() .eq(SmHotelEntity::getDistrict, name + "县"); - if (StrUtil.isNotBlank(shortName)) { - w.or().likeRight(SmHotelEntity::getDistrict, shortName); + if (StrUtil.isNotBlank(shortName) && !shortName.equals(name)) { + w.or().eq(SmHotelEntity::getDistrict, shortName); } }) - .last("LIMIT 50")); + .last("LIMIT 20")); Long adcode = pickPlaceAdcode(rows, promoteToCity); try { stringRedisTemplate.opsForValue() diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelMapper.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelMapper.java index 337e97b3..ac7a066c 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelMapper.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelMapper.java @@ -4,11 +4,69 @@ import com.bsd.pjcloud.supermarket.entity.SmHotelEntity; import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; +import java.util.Collection; import java.util.List; @Mapper public interface SmHotelMapper extends PjcloudBaseMapper { int insertBatch(@Param("list") List list); + + /** + * App 店名前缀搜索:走 idx_hotel_name,可带区划区间;带 ORDER BY 安全(前缀命中集小)。 + */ + @Select(""" + + """) + List selectAppByNamePrefix(@Param("keyword") String keyword, @Param("adFrom") Long adFrom, + @Param("adTo") Long adTo, @Param("starLevels") Collection starLevels, + @Param("limit") int limit); + + /** + * App 店名包含搜索:禁止 ORDER BY(含匹配 + 排序 = 全表 filesort 超时)。 + * 先 LIMIT 取候选,由调用方内存排序。有 adcode 时扫描范围极小。 + */ + @Select(""" + + """) + List selectAppByNameContains(@Param("keyword") String keyword, @Param("adFrom") Long adFrom, + @Param("adTo") Long adTo, @Param("starLevels") Collection starLevels, + @Param("limit") int limit); }