酒店
This commit is contained in:
1 parent
617bd2ef24
commit
d88f29f842
2 files changed
+261
-65
No files matched your search
+252
-65
@@ -40,7 +40,9 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@@ -87,13 +89,25 @@ public class SmHotelAppController {
|
||||
private static final String CANDIDATE_CACHE_OVERFLOW = "OVERFLOW";
|
||||
|
||||
/** 列表页结果短缓存(同条件重复进入/下拉) */
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v7:";
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v8:";
|
||||
|
||||
private static final long PAGE_CACHE_SECONDS = 45L;
|
||||
|
||||
/** 先按区域/关键字缩小酒店集合时的上限,超出则回退 EXISTS */
|
||||
private static final int MAX_SCOPED_HOTEL_IDS = 3000;
|
||||
|
||||
/** 产品最低卖价全量短缓存(排序/起价筛选,避免相关子查询扫产品表) */
|
||||
private static final String PMIN_CACHE_KEY = "sm:hotel:app:pmin:v1";
|
||||
|
||||
private static final long PMIN_CACHE_MINUTES = 5L;
|
||||
|
||||
private static final String CAP_CACHE_PREFIX = "sm:hotel:app:cap:v1:";
|
||||
|
||||
private static final long CAP_CACHE_MINUTES = 5L;
|
||||
|
||||
/** 超过该规模不再逐酒店 IN 聚合,改走全量最低价缓存 */
|
||||
private static final int MAX_PMIN_IN_HOTELS = 10000;
|
||||
|
||||
private final SmHotelService hotelService;
|
||||
|
||||
private final SmHotelMidService midService;
|
||||
@@ -246,70 +260,61 @@ public class SmHotelAppController {
|
||||
return R.ok(cached);
|
||||
}
|
||||
|
||||
var wrapper = baseOnlineListWrapper(keyword, city, district, districtAdcode, bizArea, starList, minScore,
|
||||
var idWrapper = baseOnlineListWrapper(keyword, city, district, districtAdcode, bizArea, starList, minScore,
|
||||
facilityList);
|
||||
// 列表不查 intro/carousel 大字段,显著降低 IO 与加密体积
|
||||
wrapper.select(SmHotelEntity::getId, SmHotelEntity::getHotelName, SmHotelEntity::getHotelCode,
|
||||
SmHotelEntity::getAddress, SmHotelEntity::getDistrict, SmHotelEntity::getDistrictAdcode,
|
||||
SmHotelEntity::getBizArea, SmHotelEntity::getStarLevel, SmHotelEntity::getScore,
|
||||
SmHotelEntity::getFacilityTags, SmHotelEntity::getCoverImage, SmHotelEntity::getPhone,
|
||||
SmHotelEntity::getSheetName, SmHotelEntity::getUpstreamStatus, SmHotelEntity::getUpdateTime);
|
||||
|
||||
boolean hasScopeFilter = districtAdcode != null || StrUtil.isNotBlank(district) || StrUtil.isNotBlank(city)
|
||||
|| StrUtil.isNotBlank(keyword) || StrUtil.isNotBlank(bizArea) || !starList.isEmpty()
|
||||
|| minScore != null || !facilityList.isEmpty();
|
||||
List<Long> scopedIds = null;
|
||||
if (hasScopeFilter) {
|
||||
scopedIds = loadScopedHotelIds(keyword, city, district, districtAdcode, bizArea, starList, minScore,
|
||||
facilityList);
|
||||
if (scopedIds != null && scopedIds.isEmpty()) {
|
||||
return R.ok(emptyPage(page));
|
||||
idWrapper.select(SmHotelEntity::getId, SmHotelEntity::getStarLevel);
|
||||
List<SmHotelEntity> matched = hotelService.list(idWrapper);
|
||||
if (matched == null || matched.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
if (needCap > 1) {
|
||||
matched = filterByGuestCapacity(matched, needCap);
|
||||
if (matched.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
}
|
||||
|
||||
Page<SmHotelEntity> raw;
|
||||
int candSize = -1;
|
||||
boolean smallScope = scopedIds != null && !scopedIds.isEmpty() && scopedIds.size() <= MAX_SCOPED_HOTEL_IDS;
|
||||
if (smallScope) {
|
||||
List<Long> candidateIds = scopedIds;
|
||||
if (needCap > 1) {
|
||||
List<Long> capHotels = roomMapper.selectDistinctHotelIdsByMinCapacity(needCap);
|
||||
if (capHotels == null || capHotels.isEmpty()) {
|
||||
return R.ok(emptyPage(page));
|
||||
}
|
||||
Set<Long> capSet = new java.util.HashSet<>(capHotels);
|
||||
candidateIds = scopedIds.stream().filter(capSet::contains).toList();
|
||||
if (candidateIds.isEmpty()) {
|
||||
return R.ok(emptyPage(page));
|
||||
}
|
||||
}
|
||||
candSize = candidateIds.size();
|
||||
wrapper.in(SmHotelEntity::getId, candidateIds);
|
||||
}
|
||||
else {
|
||||
// 全国:上架酒店都出列表;所选日期无日历的在行上标已售罄,不再按可订日历过滤掉
|
||||
applyGuestCapacityFilter(wrapper, needCap);
|
||||
}
|
||||
List<Long> matchedIds = matched.stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, BigDecimal> productMinMap = loadProductMinPrices(matchedIds);
|
||||
if (minPrice != null || maxPrice != null) {
|
||||
List<Long> priceHitIds = loadHotelIdsByMinPriceRange(minPrice, maxPrice);
|
||||
if (priceHitIds.isEmpty()) {
|
||||
return R.ok(emptyPage(page));
|
||||
matched = matched.stream().filter(h -> {
|
||||
BigDecimal p = h.getId() == null ? null : productMinMap.get(h.getId());
|
||||
if (p == null) {
|
||||
return false;
|
||||
}
|
||||
if (minPrice != null && p.compareTo(minPrice) < 0) {
|
||||
return false;
|
||||
}
|
||||
return maxPrice == null || p.compareTo(maxPrice) <= 0;
|
||||
}).toList();
|
||||
if (matched.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
wrapper.in(SmHotelEntity::getId, priceHitIds);
|
||||
}
|
||||
// 五星档次优先,同档按产品标价从高到低(无星级/无价格的靠后)
|
||||
wrapper.last("""
|
||||
ORDER BY IFNULL(star_level, 0) DESC,
|
||||
IFNULL((
|
||||
SELECT MIN(p.sell_price) FROM canteen_hotel_product p
|
||||
WHERE p.del_flag = '0'
|
||||
AND p.hotel_id = canteen_hotel.id
|
||||
AND p.sell_price IS NOT NULL
|
||||
), 0) DESC,
|
||||
id DESC
|
||||
""");
|
||||
raw = pageWithoutCount(page, wrapper);
|
||||
// 五星优先,同档产品最低卖价从高到低,无星级/无价格视为 0(与原 SQL IFNULL 一致)
|
||||
matched = new ArrayList<>(matched);
|
||||
matched.sort(Comparator
|
||||
.comparing((SmHotelEntity h) -> h.getStarLevel() == null ? 0 : h.getStarLevel())
|
||||
.reversed()
|
||||
.thenComparing((SmHotelEntity h) -> productMinMap.getOrDefault(h.getId(), BigDecimal.ZERO),
|
||||
Comparator.reverseOrder())
|
||||
.thenComparing((SmHotelEntity h) -> h.getId() == null ? 0L : h.getId(), Comparator.reverseOrder()));
|
||||
Page<SmHotelEntity> raw = sliceWithoutCount(page, matched);
|
||||
List<Long> ids = raw.getRecords().stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, SmHotelEntity> fullById = loadListColumnsByIds(ids);
|
||||
List<SmHotelEntity> pageHotels = new ArrayList<>(ids.size());
|
||||
for (Long id : ids) {
|
||||
SmHotelEntity h = fullById.get(id);
|
||||
if (h != null) {
|
||||
pageHotels.add(h);
|
||||
}
|
||||
}
|
||||
raw.setRecords(pageHotels);
|
||||
LocalDate priceFrom = stayFrom != null ? stayFrom : LocalDate.now();
|
||||
LocalDate priceToEx = stayToExclusive;
|
||||
if (priceToEx == null || !priceToEx.isAfter(priceFrom)) {
|
||||
@@ -317,14 +322,10 @@ public class SmHotelAppController {
|
||||
}
|
||||
Map<Long, BigDecimal> minPriceMap = loadMinSellPrice(ids, priceFrom, priceToEx);
|
||||
Page<Map<String, Object>> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal());
|
||||
out.setRecords(raw.getRecords().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(h -> toListRow(h, minPriceMap.get(h.getId())))
|
||||
.toList());
|
||||
out.setRecords(pageHotels.stream().map(h -> toListRow(h, minPriceMap.get(h.getId()))).toList());
|
||||
writePageCache(pageCacheKey, out);
|
||||
log.info("|酒店App|page|current={}|size={}|total={}|scope={}|cand={}|costMs={}|", page.getCurrent(),
|
||||
page.getSize(), raw.getTotal(), scopedIds == null ? -1 : scopedIds.size(), candSize,
|
||||
System.currentTimeMillis() - t0);
|
||||
log.info("|酒店App|page|current={}|size={}|total={}|matched={}|costMs={}|", page.getCurrent(), page.getSize(),
|
||||
raw.getTotal(), matchedIds.size(), System.currentTimeMillis() - t0);
|
||||
return R.ok(out);
|
||||
}
|
||||
|
||||
@@ -334,6 +335,192 @@ public class SmHotelAppController {
|
||||
return empty;
|
||||
}
|
||||
|
||||
/** 内存分页:语义与 pageWithoutCount 一致,不做 COUNT(*) */
|
||||
private static Page<SmHotelEntity> sliceWithoutCount(Page<SmHotelEntity> page, List<SmHotelEntity> sorted) {
|
||||
long size = Math.max(1L, Math.min(page.getSize(), 50L));
|
||||
long current = Math.max(1L, page.getCurrent());
|
||||
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;
|
||||
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 Map<Long, SmHotelEntity> loadListColumnsByIds(List<Long> ids) {
|
||||
Map<Long, SmHotelEntity> map = new LinkedHashMap<>();
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return map;
|
||||
}
|
||||
List<SmHotelEntity> rows = hotelService.list(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.in(SmHotelEntity::getId, ids)
|
||||
.select(SmHotelEntity::getId, SmHotelEntity::getHotelName, SmHotelEntity::getHotelCode,
|
||||
SmHotelEntity::getAddress, SmHotelEntity::getDistrict, SmHotelEntity::getDistrictAdcode,
|
||||
SmHotelEntity::getBizArea, SmHotelEntity::getStarLevel, SmHotelEntity::getScore,
|
||||
SmHotelEntity::getFacilityTags, SmHotelEntity::getCoverImage, SmHotelEntity::getPhone,
|
||||
SmHotelEntity::getSheetName, SmHotelEntity::getUpstreamStatus, SmHotelEntity::getUpdateTime));
|
||||
if (rows == null) {
|
||||
return map;
|
||||
}
|
||||
for (SmHotelEntity h : rows) {
|
||||
if (h != null && h.getId() != null) {
|
||||
map.put(h.getId(), h);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private List<SmHotelEntity> filterByGuestCapacity(List<SmHotelEntity> hotels, int needCap) {
|
||||
List<Long> ids = hotels.stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList();
|
||||
if (ids.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Set<Long> capSet;
|
||||
if (ids.size() <= MAX_IN_HOTEL_IDS) {
|
||||
List<Long> capHotels = roomMapper.selectDistinctHotelIdsByMinCapacityInHotels(needCap, ids);
|
||||
capSet = capHotels == null ? Set.of() : new HashSet<>(capHotels);
|
||||
}
|
||||
else {
|
||||
capSet = loadCapacityHotelIdSet(needCap);
|
||||
}
|
||||
if (capSet.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
return hotels.stream().filter(h -> h.getId() != null && capSet.contains(h.getId())).toList();
|
||||
}
|
||||
|
||||
private Set<Long> loadCapacityHotelIdSet(int needCap) {
|
||||
String key = CAP_CACHE_PREFIX + needCap;
|
||||
try {
|
||||
String cached = stringRedisTemplate.opsForValue().get(key);
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
if (CANDIDATE_CACHE_EMPTY.equals(cached)) {
|
||||
return Set.of();
|
||||
}
|
||||
return Arrays.stream(cached.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(Long::valueOf)
|
||||
.collect(Collectors.toCollection(HashSet::new));
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|可住人数缓存读取失败|{}", e.getMessage());
|
||||
}
|
||||
List<Long> capHotels = roomMapper.selectDistinctHotelIdsByMinCapacity(needCap);
|
||||
if (capHotels == null || capHotels.isEmpty()) {
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(key, CANDIDATE_CACHE_EMPTY, CAP_CACHE_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|可住人数缓存写入失败|{}", e.getMessage());
|
||||
}
|
||||
return Set.of();
|
||||
}
|
||||
Set<Long> set = capHotels.stream().filter(Objects::nonNull).collect(Collectors.toCollection(HashSet::new));
|
||||
try {
|
||||
stringRedisTemplate.opsForValue()
|
||||
.set(key, set.stream().map(String::valueOf).collect(Collectors.joining(",")), CAP_CACHE_MINUTES,
|
||||
TimeUnit.MINUTES);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|可住人数缓存写入失败|{}", e.getMessage());
|
||||
}
|
||||
return set;
|
||||
}
|
||||
|
||||
/**
|
||||
* 产品最低卖价:小集合走 hotel_id IN(走 idx_hotel_id);大集合走全量 GROUP BY 短缓存。
|
||||
*/
|
||||
private Map<Long, BigDecimal> loadProductMinPrices(List<Long> hotelIds) {
|
||||
if (hotelIds == null || hotelIds.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
if (hotelIds.size() <= MAX_PMIN_IN_HOTELS) {
|
||||
return queryProductMinPricesByIds(hotelIds);
|
||||
}
|
||||
Map<Long, BigDecimal> all = loadAllProductMinPricesCached();
|
||||
if (all.isEmpty()) {
|
||||
return Map.of();
|
||||
}
|
||||
Map<Long, BigDecimal> out = new HashMap<>(Math.min(hotelIds.size(), all.size()));
|
||||
for (Long id : hotelIds) {
|
||||
BigDecimal p = all.get(id);
|
||||
if (p != null) {
|
||||
out.put(id, p);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<Long, BigDecimal> queryProductMinPricesByIds(List<Long> hotelIds) {
|
||||
Map<Long, BigDecimal> out = new HashMap<>();
|
||||
int batch = 800;
|
||||
for (int i = 0; i < hotelIds.size(); i += batch) {
|
||||
List<Long> part = hotelIds.subList(i, Math.min(i + batch, hotelIds.size()));
|
||||
List<Map<String, Object>> rows = productMapper.selectMinSellPriceByHotelIds(part);
|
||||
putHotelMinPrices(out, rows);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<Long, BigDecimal> loadAllProductMinPricesCached() {
|
||||
try {
|
||||
Map<Object, Object> cached = stringRedisTemplate.opsForHash().entries(PMIN_CACHE_KEY);
|
||||
if (cached != null && !cached.isEmpty()) {
|
||||
Map<Long, BigDecimal> out = new HashMap<>(cached.size());
|
||||
for (Map.Entry<Object, Object> e : cached.entrySet()) {
|
||||
if (e.getKey() == null || e.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
String v = String.valueOf(e.getValue());
|
||||
if (StrUtil.isBlank(v)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
out.put(Long.valueOf(String.valueOf(e.getKey())), new BigDecimal(v));
|
||||
}
|
||||
catch (Exception ignore) {
|
||||
// skip bad cache field
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|产品最低价缓存读取失败|{}", e.getMessage());
|
||||
}
|
||||
List<Map<String, Object>> rows = productMapper.selectAllHotelMinSellPrice();
|
||||
Map<Long, BigDecimal> out = new HashMap<>();
|
||||
putHotelMinPrices(out, rows);
|
||||
if (!out.isEmpty()) {
|
||||
try {
|
||||
Map<String, String> hash = new HashMap<>(out.size());
|
||||
out.forEach((id, p) -> hash.put(String.valueOf(id), p.toPlainString()));
|
||||
stringRedisTemplate.opsForHash().putAll(PMIN_CACHE_KEY, hash);
|
||||
stringRedisTemplate.expire(PMIN_CACHE_KEY, PMIN_CACHE_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|产品最低价缓存写入失败|{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static void putHotelMinPrices(Map<Long, BigDecimal> out, List<Map<String, Object>> rows) {
|
||||
if (rows == null) {
|
||||
return;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (row == null || row.get("hotelId") == null || row.get("minSell") == null) {
|
||||
continue;
|
||||
}
|
||||
out.put(Long.valueOf(String.valueOf(row.get("hotelId"))), new BigDecimal(String.valueOf(row.get("minSell"))));
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表只要一页,不做 COUNT(*)(全国 COUNT+EXISTS 会扫全表十几秒) */
|
||||
private Page<SmHotelEntity> pageWithoutCount(Page<SmHotelEntity> page, LambdaQueryWrapper<SmHotelEntity> wrapper) {
|
||||
long size = Math.max(1L, Math.min(page.getSize(), 50L));
|
||||
|
||||
+9
@@ -61,6 +61,15 @@ public interface SmHotelProductMapper extends PjcloudBaseMapper<SmHotelProductEn
|
||||
List<Long> selectHotelIdsByMinSellPriceRange(@Param("minPrice") BigDecimal minPrice,
|
||||
@Param("maxPrice") BigDecimal maxPrice);
|
||||
|
||||
/** 全库酒店最低卖价(短缓存热身,避免每次列表 GROUP BY 千万产品) */
|
||||
@Select("""
|
||||
SELECT hotel_id AS hotelId, MIN(sell_price) AS minSell
|
||||
FROM canteen_hotel_product
|
||||
WHERE del_flag = '0' AND sell_price IS NOT NULL
|
||||
GROUP BY hotel_id
|
||||
""")
|
||||
List<Map<String, Object>> selectAllHotelMinSellPrice();
|
||||
|
||||
/** 本页酒店批量取最低卖价 */
|
||||
@Select("""
|
||||
<script>
|
||||
|
||||
Reference in new issue
Block a user