酒店
This commit is contained in:
1 parent
d88f29f842
commit
aa766987f9
2 files changed
+149
-207
No files matched your search
+149
-198
@@ -89,24 +89,12 @@ public class SmHotelAppController {
|
||||
private static final String CANDIDATE_CACHE_OVERFLOW = "OVERFLOW";
|
||||
|
||||
/** 列表页结果短缓存(同条件重复进入/下拉) */
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v8:";
|
||||
private static final String PAGE_CACHE_PREFIX = "sm:hotel:app:page:v9:";
|
||||
|
||||
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;
|
||||
/** 城市/关键字等缩小后,在该规模内内存排序(超出则 SQL LIMIT,禁止全表 list) */
|
||||
private static final int MAX_SCOPED_HOTEL_IDS = 8000;
|
||||
|
||||
private final SmHotelService hotelService;
|
||||
|
||||
@@ -260,61 +248,80 @@ public class SmHotelAppController {
|
||||
return R.ok(cached);
|
||||
}
|
||||
|
||||
var idWrapper = baseOnlineListWrapper(keyword, city, district, districtAdcode, bizArea, starList, minScore,
|
||||
facilityList);
|
||||
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()) {
|
||||
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()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
}
|
||||
List<Long> matchedIds = matched.stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList();
|
||||
Map<Long, BigDecimal> productMinMap = loadProductMinPrices(matchedIds);
|
||||
if (minPrice != null || maxPrice != null) {
|
||||
matched = matched.stream().filter(h -> {
|
||||
BigDecimal p = h.getId() == null ? null : productMinMap.get(h.getId());
|
||||
if (p == null) {
|
||||
return false;
|
||||
boolean smallScope = scopedIds != null && !scopedIds.isEmpty() && scopedIds.size() <= MAX_SCOPED_HOTEL_IDS;
|
||||
Page<SmHotelEntity> raw;
|
||||
int candSize = -1;
|
||||
if (smallScope) {
|
||||
List<Long> candidateIds = scopedIds;
|
||||
if (needCap > 1) {
|
||||
List<Long> capHotels = roomMapper.selectDistinctHotelIdsByMinCapacityInHotels(needCap, candidateIds);
|
||||
if (capHotels == null || capHotels.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
if (minPrice != null && p.compareTo(minPrice) < 0) {
|
||||
return false;
|
||||
Set<Long> capSet = new HashSet<>(capHotels);
|
||||
candidateIds = candidateIds.stream().filter(capSet::contains).toList();
|
||||
if (candidateIds.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
return maxPrice == null || p.compareTo(maxPrice) <= 0;
|
||||
}).toList();
|
||||
if (matched.isEmpty()) {
|
||||
}
|
||||
Map<Long, BigDecimal> productMinMap = queryProductMinPricesByIds(candidateIds);
|
||||
if (minPrice != null || maxPrice != null) {
|
||||
candidateIds = candidateIds.stream().filter(id -> inProductMinRange(productMinMap.get(id), minPrice, maxPrice))
|
||||
.toList();
|
||||
if (candidateIds.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
}
|
||||
candSize = candidateIds.size();
|
||||
List<SmHotelEntity> matched = hotelService.list(baseOnlineListWrapper(keyword, city, district,
|
||||
districtAdcode, bizArea, starList, minScore, facilityList)
|
||||
.select(SmHotelEntity::getId, SmHotelEntity::getStarLevel)
|
||||
.in(SmHotelEntity::getId, candidateIds));
|
||||
if (matched == null || matched.isEmpty()) {
|
||||
Page<Map<String, Object>> empty = emptyPage(page);
|
||||
writePageCache(pageCacheKey, empty);
|
||||
return R.ok(empty);
|
||||
}
|
||||
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()));
|
||||
raw = sliceWithoutCount(page, matched);
|
||||
List<Long> pageIds = raw.getRecords().stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList();
|
||||
raw.setRecords(loadListRowsInOrder(pageIds));
|
||||
}
|
||||
else {
|
||||
// 全国或匹配过多:只取一页,禁止 list 全表。排序用星级+id,避免产品相关子查询把请求拖死。
|
||||
var wrapper = applyListColumns(baseOnlineListWrapper(keyword, city, district, districtAdcode, bizArea,
|
||||
starList, minScore, facilityList));
|
||||
applyGuestCapacityFilter(wrapper, needCap);
|
||||
applyMinProductPriceFilter(wrapper, minPrice, maxPrice);
|
||||
wrapper.last("ORDER BY IFNULL(star_level, 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)) {
|
||||
@@ -322,10 +329,14 @@ 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(pageHotels.stream().map(h -> toListRow(h, minPriceMap.get(h.getId()))).toList());
|
||||
out.setRecords(raw.getRecords().stream()
|
||||
.filter(Objects::nonNull)
|
||||
.map(h -> toListRow(h, minPriceMap.get(h.getId())))
|
||||
.toList());
|
||||
writePageCache(pageCacheKey, out);
|
||||
log.info("|酒店App|page|current={}|size={}|total={}|matched={}|costMs={}|", page.getCurrent(), page.getSize(),
|
||||
raw.getTotal(), matchedIds.size(), System.currentTimeMillis() - t0);
|
||||
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);
|
||||
return R.ok(out);
|
||||
}
|
||||
|
||||
@@ -349,162 +360,58 @@ public class SmHotelAppController {
|
||||
return out;
|
||||
}
|
||||
|
||||
private Map<Long, SmHotelEntity> loadListColumnsByIds(List<Long> ids) {
|
||||
Map<Long, SmHotelEntity> map = new LinkedHashMap<>();
|
||||
private static LambdaQueryWrapper<SmHotelEntity> applyListColumns(LambdaQueryWrapper<SmHotelEntity> wrapper) {
|
||||
return 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);
|
||||
}
|
||||
|
||||
private List<SmHotelEntity> loadListRowsInOrder(List<Long> ids) {
|
||||
if (ids == null || ids.isEmpty()) {
|
||||
return map;
|
||||
return List.of();
|
||||
}
|
||||
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;
|
||||
List<SmHotelEntity> rows = hotelService.list(applyListColumns(Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.in(SmHotelEntity::getId, ids)));
|
||||
if (rows == null || rows.isEmpty()) {
|
||||
return List.of();
|
||||
}
|
||||
Map<Long, SmHotelEntity> byId = new HashMap<>();
|
||||
for (SmHotelEntity h : rows) {
|
||||
if (h != null && h.getId() != null) {
|
||||
map.put(h.getId(), h);
|
||||
byId.put(h.getId(), h);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
List<SmHotelEntity> ordered = new ArrayList<>(ids.size());
|
||||
for (Long id : ids) {
|
||||
SmHotelEntity h = byId.get(id);
|
||||
if (h != null) {
|
||||
ordered.add(h);
|
||||
}
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
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();
|
||||
private static boolean inProductMinRange(BigDecimal minSell, BigDecimal minPrice, BigDecimal maxPrice) {
|
||||
if (minSell == null) {
|
||||
return false;
|
||||
}
|
||||
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);
|
||||
if (minPrice != null && minSell.compareTo(minPrice) < 0) {
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
return maxPrice == null || minSell.compareTo(maxPrice) <= 0;
|
||||
}
|
||||
|
||||
private Map<Long, BigDecimal> queryProductMinPricesByIds(List<Long> hotelIds) {
|
||||
Map<Long, BigDecimal> out = new HashMap<>();
|
||||
if (hotelIds == null || hotelIds.isEmpty()) {
|
||||
return out;
|
||||
}
|
||||
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());
|
||||
}
|
||||
putHotelMinPrices(out, productMapper.selectMinSellPriceByHotelIds(part));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
@@ -521,6 +428,50 @@ public class SmHotelAppController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 起价筛选等价于 MIN(sell_price) 落在区间:有产品、没有更低越界价、且存在不超过上限的价。
|
||||
* 只挂在 LIMIT 分页 SQL 上,禁止全表 GROUP BY。
|
||||
*/
|
||||
private void applyMinProductPriceFilter(LambdaQueryWrapper<SmHotelEntity> wrapper, BigDecimal minPrice,
|
||||
BigDecimal maxPrice) {
|
||||
if (minPrice == null && maxPrice == null) {
|
||||
return;
|
||||
}
|
||||
if (minPrice != null) {
|
||||
wrapper.apply("""
|
||||
NOT EXISTS (
|
||||
SELECT 1 FROM canteen_hotel_product p
|
||||
WHERE p.hotel_id = canteen_hotel.id
|
||||
AND p.del_flag = '0'
|
||||
AND p.sell_price IS NOT NULL
|
||||
AND p.sell_price < {0}
|
||||
LIMIT 1
|
||||
)
|
||||
""", minPrice);
|
||||
wrapper.apply("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM canteen_hotel_product p
|
||||
WHERE p.hotel_id = canteen_hotel.id
|
||||
AND p.del_flag = '0'
|
||||
AND p.sell_price IS NOT NULL
|
||||
LIMIT 1
|
||||
)
|
||||
""");
|
||||
}
|
||||
if (maxPrice != null) {
|
||||
wrapper.apply("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM canteen_hotel_product p
|
||||
WHERE p.hotel_id = canteen_hotel.id
|
||||
AND p.del_flag = '0'
|
||||
AND p.sell_price IS NOT NULL
|
||||
AND p.sell_price <= {0}
|
||||
LIMIT 1
|
||||
)
|
||||
""", maxPrice);
|
||||
}
|
||||
}
|
||||
|
||||
/** 列表只要一页,不做 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,15 +61,6 @@ 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