This commit is contained in:
zjf committed 2026-10-22 23:53:09 +08:00
1 parent 8dcaae6e3d
commit 730ae9b447
2 files changed
+137 -26

No files matched your search

@@ -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<SmHotelEntity> 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<SmHotelEntity> 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<Long> 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<SmHotelEntity> pageByHotelNameKeyword(Page<SmHotelEntity> page, String keyword, String city,
String district, Long districtAdcode, List<Integer> 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<Integer> stars = starList == null || starList.isEmpty() ? null : starList;
List<SmHotelEntity> 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<SmHotelEntity> 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<SmHotelEntity> 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<SmHotelEntity> out = new Page<>(current, size, total);
out.setRecords(new ArrayList<>(slice));
return out;
}
/** 上架 + 区域/关键字/星级等公共条件(不含可售日历、排序) */
private LambdaQueryWrapper<SmHotelEntity> baseOnlineListWrapper(String keyword, String city, String district,
Long districtAdcode, String bizArea, List<Integer> 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<SmHotelEntity> rows = hotelService.list(Wrappers.<SmHotelEntity>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()
@@ -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<SmHotelEntity> {
int insertBatch(@Param("list") List<SmHotelEntity> list);
/**
* App 店名前缀搜索:走 idx_hotel_name,可带区划区间;带 ORDER BY 安全(前缀命中集小)。
*/
@Select("""
<script>
SELECT id, hotel_name AS hotelName, hotel_code AS hotelCode, address, district,
district_adcode AS districtAdcode, biz_area AS bizArea, star_level AS starLevel,
score, facility_tags AS facilityTags, cover_image AS coverImage, phone,
sheet_name AS sheetName, upstream_status AS upstreamStatus, update_time AS updateTime
FROM canteen_hotel FORCE INDEX (idx_hotel_name)
WHERE del_flag = '0'
AND (upstream_status IS NULL OR upstream_status &lt;&gt; 'OFFLINE')
AND (hotel_name LIKE CONCAT(#{keyword}, '%') OR hotel_code = #{keyword})
<if test="adFrom != null">
AND district_adcode &gt;= #{adFrom} AND district_adcode &lt; #{adTo}
</if>
<if test="starLevels != null and starLevels.size() &gt; 0">
AND star_level IN
<foreach collection="starLevels" item="s" open="(" separator="," close=")">#{s}</foreach>
</if>
ORDER BY IFNULL(star_level, 0) DESC, id DESC
LIMIT #{limit}
</script>
""")
List<SmHotelEntity> selectAppByNamePrefix(@Param("keyword") String keyword, @Param("adFrom") Long adFrom,
@Param("adTo") Long adTo, @Param("starLevels") Collection<Integer> starLevels,
@Param("limit") int limit);
/**
* App 店名包含搜索:禁止 ORDER BY(含匹配 + 排序 = 全表 filesort 超时)。
* 先 LIMIT 取候选,由调用方内存排序。有 adcode 时扫描范围极小。
*/
@Select("""
<script>
SELECT id, hotel_name AS hotelName, hotel_code AS hotelCode, address, district,
district_adcode AS districtAdcode, biz_area AS bizArea, star_level AS starLevel,
score, facility_tags AS facilityTags, cover_image AS coverImage, phone,
sheet_name AS sheetName, upstream_status AS upstreamStatus, update_time AS updateTime
FROM canteen_hotel
WHERE del_flag = '0'
AND (upstream_status IS NULL OR upstream_status &lt;&gt; 'OFFLINE')
AND hotel_name LIKE CONCAT('%', #{keyword}, '%')
<if test="adFrom != null">
AND district_adcode &gt;= #{adFrom} AND district_adcode &lt; #{adTo}
</if>
<if test="starLevels != null and starLevels.size() &gt; 0">
AND star_level IN
<foreach collection="starLevels" item="s" open="(" separator="," close=")">#{s}</foreach>
</if>
LIMIT #{limit}
</script>
""")
List<SmHotelEntity> selectAppByNameContains(@Param("keyword") String keyword, @Param("adFrom") Long adFrom,
@Param("adTo") Long adTo, @Param("starLevels") Collection<Integer> starLevels,
@Param("limit") int limit);
}