酒店优化
This commit is contained in:
1 parent
ce0e9e3868
commit
a74dec5e7a
4 files changed
+251
-136
No files matched your search
+150
-128
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.supermarket.controller;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
@@ -16,16 +17,20 @@ 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.SmHotelProductMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelRoomMapper;
|
||||
import com.bsd.pjcloud.supermarket.mapper.SmHotelUserSearchPrefMapper;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelMidService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelOrderService;
|
||||
import com.bsd.pjcloud.supermarket.service.SmHotelService;
|
||||
import com.bsd.pjcloud.supermarket.util.HotelAreaAdcodeUtil;
|
||||
import com.bsd.pjcloud.supermarket.vo.SmHotelDetailVO;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.annotations.ParameterObject;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
@@ -42,11 +47,13 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 移动端 TOC 酒店预订(列表/详情/下单/我的订单)。
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/app/hotel")
|
||||
@@ -62,9 +69,23 @@ public class SmHotelAppController {
|
||||
|
||||
private static final DateTimeFormatter DAY = DateTimeFormatter.ISO_LOCAL_DATE;
|
||||
|
||||
/** 候选酒店 id 用 IN 时的上限;过大则回退 EXISTS,避免超长 SQL */
|
||||
/** 候选酒店 id 用 IN 时的上限;过大则回退 IN(子查询),避免超长 SQL / 相关 EXISTS */
|
||||
private static final int MAX_IN_HOTEL_IDS = 8000;
|
||||
|
||||
/** 筛选项缓存(租户前缀由 Redis 基础设施自动处理) */
|
||||
private static final String FILTER_META_CACHE_KEY = "sm:hotel:app:filter-meta";
|
||||
|
||||
private static final long FILTER_META_CACHE_MINUTES = 10L;
|
||||
|
||||
/** 可售候选酒店 id 短缓存 */
|
||||
private static final String CANDIDATE_CACHE_PREFIX = "sm:hotel:app:cand:";
|
||||
|
||||
private static final long CANDIDATE_CACHE_MINUTES = 3L;
|
||||
|
||||
private static final String CANDIDATE_CACHE_EMPTY = "EMPTY";
|
||||
|
||||
private static final String CANDIDATE_CACHE_OVERFLOW = "OVERFLOW";
|
||||
|
||||
private final SmHotelService hotelService;
|
||||
|
||||
private final SmHotelMidService midService;
|
||||
@@ -77,34 +98,82 @@ public class SmHotelAppController {
|
||||
|
||||
private final SmHotelRoomMapper roomMapper;
|
||||
|
||||
private final SmHotelProductMapper productMapper;
|
||||
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
@Operation(summary = "列表筛选项(区域/星级/设施)")
|
||||
@GetMapping("/filter-meta")
|
||||
public R<Map<String, Object>> filterMeta() {
|
||||
var online = Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
try {
|
||||
String cached = stringRedisTemplate.opsForValue().get(FILTER_META_CACHE_KEY);
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> hit = JSONUtil.toBean(cached, Map.class);
|
||||
if (hit != null && !hit.isEmpty()) {
|
||||
return R.ok(hit);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|filter-meta缓存读取失败|{}", e.getMessage());
|
||||
}
|
||||
Map<String, Object> out = buildFilterMeta();
|
||||
try {
|
||||
stringRedisTemplate.opsForValue()
|
||||
.set(FILTER_META_CACHE_KEY, JSONUtil.toJsonStr(out), FILTER_META_CACHE_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|filter-meta缓存写入失败|{}", e.getMessage());
|
||||
}
|
||||
return R.ok(out);
|
||||
}
|
||||
|
||||
/** 上架酒店公共条件(未下架) */
|
||||
private static LambdaQueryWrapper<SmHotelEntity> onlineHotelWrapper() {
|
||||
return Wrappers.<SmHotelEntity>lambdaQuery()
|
||||
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
|
||||
.or()
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
|
||||
.select(SmHotelEntity::getId, SmHotelEntity::getDistrict, SmHotelEntity::getBizArea,
|
||||
SmHotelEntity::getFacilityTags);
|
||||
List<SmHotelEntity> rows = hotelService.list(online);
|
||||
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选项:按列 GROUP BY,避免全表加载酒店实体;districts 仅兜底(App 优先走行政区划接口)。
|
||||
*/
|
||||
private Map<String, Object> buildFilterMeta() {
|
||||
Set<String> districts = new LinkedHashSet<>();
|
||||
Set<String> bizAreas = new LinkedHashSet<>();
|
||||
Set<String> facilities = new LinkedHashSet<>(DEFAULT_FACILITY_OPTIONS);
|
||||
for (SmHotelEntity h : rows) {
|
||||
if (h == null) {
|
||||
continue;
|
||||
}
|
||||
if (StrUtil.isNotBlank(h.getDistrict())) {
|
||||
for (SmHotelEntity h : hotelService.list(onlineHotelWrapper()
|
||||
.select(SmHotelEntity::getDistrict)
|
||||
.isNotNull(SmHotelEntity::getDistrict)
|
||||
.ne(SmHotelEntity::getDistrict, "")
|
||||
.groupBy(SmHotelEntity::getDistrict))) {
|
||||
if (h != null && StrUtil.isNotBlank(h.getDistrict())) {
|
||||
districts.add(h.getDistrict().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(h.getBizArea())) {
|
||||
}
|
||||
Set<String> bizAreas = new LinkedHashSet<>();
|
||||
for (SmHotelEntity h : hotelService.list(onlineHotelWrapper()
|
||||
.select(SmHotelEntity::getBizArea)
|
||||
.isNotNull(SmHotelEntity::getBizArea)
|
||||
.ne(SmHotelEntity::getBizArea, "")
|
||||
.groupBy(SmHotelEntity::getBizArea))) {
|
||||
if (h != null && StrUtil.isNotBlank(h.getBizArea())) {
|
||||
bizAreas.add(h.getBizArea().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(h.getFacilityTags())) {
|
||||
for (String t : h.getFacilityTags().split("[,,]")) {
|
||||
if (StrUtil.isNotBlank(t)) {
|
||||
facilities.add(t.trim());
|
||||
}
|
||||
}
|
||||
Set<String> facilities = new LinkedHashSet<>(DEFAULT_FACILITY_OPTIONS);
|
||||
for (SmHotelEntity h : hotelService.list(onlineHotelWrapper()
|
||||
.select(SmHotelEntity::getFacilityTags)
|
||||
.isNotNull(SmHotelEntity::getFacilityTags)
|
||||
.ne(SmHotelEntity::getFacilityTags, "")
|
||||
.groupBy(SmHotelEntity::getFacilityTags)
|
||||
.last("LIMIT 2000"))) {
|
||||
if (h == null || StrUtil.isBlank(h.getFacilityTags())) {
|
||||
continue;
|
||||
}
|
||||
for (String t : h.getFacilityTags().split("[,,]")) {
|
||||
if (StrUtil.isNotBlank(t)) {
|
||||
facilities.add(t.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -117,7 +186,6 @@ public class SmHotelAppController {
|
||||
});
|
||||
starLevels.sort((a, b) -> Integer.compare((Integer) b.get("value"), (Integer) a.get("value")));
|
||||
Map<String, Object> out = new LinkedHashMap<>();
|
||||
// districts 仅作兜底;移动端优先走 /admin/sysArea/app/children(行政区划)
|
||||
out.put("districts", new ArrayList<>(districts));
|
||||
out.put("bizAreas", new ArrayList<>(bizAreas));
|
||||
out.put("starLevels", starLevels);
|
||||
@@ -129,7 +197,7 @@ public class SmHotelAppController {
|
||||
Map.of("label", "300-500", "min", 300, "max", 500),
|
||||
Map.of("label", "500以上", "min", 500, "max", 999999)));
|
||||
out.put("areaSource", "sysArea");
|
||||
return R.ok(out);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Operation(summary = "上架酒店分页(关键字/区域/星级/评分/价格/设施/人数/入住区间)")
|
||||
@@ -170,10 +238,14 @@ public class SmHotelAppController {
|
||||
.or()
|
||||
.like(SmHotelEntity::getDistrict, city.trim())
|
||||
.or()
|
||||
.like(SmHotelEntity::getHotelName, city.trim()))
|
||||
.eq(districtAdcode == null && StrUtil.isNotBlank(district), SmHotelEntity::getDistrict, StrUtil.trim(district));
|
||||
// district 兼作区域名称(选省/市未选下级时传「浙江省」等),与 adcode 前缀一起匹配
|
||||
applyAreaAdcode(wrapper, districtAdcode, district);
|
||||
.like(SmHotelEntity::getHotelName, city.trim()));
|
||||
// 有 adcode:数值区间走索引 + 名称兜底;仅名称:保持精确匹配(与历史 App 行为一致)
|
||||
if (districtAdcode != null) {
|
||||
HotelAreaAdcodeUtil.applyHotelListFilter(wrapper, districtAdcode, district);
|
||||
}
|
||||
else if (StrUtil.isNotBlank(district)) {
|
||||
wrapper.eq(SmHotelEntity::getDistrict, StrUtil.trim(district));
|
||||
}
|
||||
wrapper.like(StrUtil.isNotBlank(bizArea), SmHotelEntity::getBizArea, StrUtil.trim(bizArea))
|
||||
.in(!starList.isEmpty(), SmHotelEntity::getStarLevel, starList)
|
||||
.ge(minScore != null, SmHotelEntity::getScore, minScore)
|
||||
@@ -199,7 +271,7 @@ public class SmHotelAppController {
|
||||
wrapper.in(SmHotelEntity::getId, candidateIds);
|
||||
}
|
||||
else {
|
||||
// 候选过多:回退轻量 EXISTS(不再 join product)
|
||||
// 候选过多:用 IN(子查询) 替代相关 EXISTS,语义不变、更易走 stay_date 索引
|
||||
applyBookableHotelFilter(wrapper, stayFrom, stayToExclusive);
|
||||
applyGuestCapacityFilter(wrapper, needCap);
|
||||
}
|
||||
@@ -515,61 +587,6 @@ public class SmHotelAppController {
|
||||
return orderService.cancelByAdmin(id, reason);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按行政区划过滤:
|
||||
* - 省/市 adcode 用前缀覆盖全部下级(选省不选市 = 该省全部)
|
||||
* - 同时兼容仅填了区域名称、未落 district_adcode 的酒店(按名称模糊)
|
||||
*/
|
||||
private void applyAreaAdcode(LambdaQueryWrapper<SmHotelEntity> wrapper, Long districtAdcode, String areaName) {
|
||||
if (districtAdcode == null) {
|
||||
return;
|
||||
}
|
||||
String prefix = areaAdcodePrefix(districtAdcode);
|
||||
String name = StrUtil.trim(areaName);
|
||||
String shortName = shortenAreaName(name);
|
||||
wrapper.and(w -> {
|
||||
w.apply("district_adcode is not null and CAST(district_adcode AS CHAR) LIKE {0}", prefix + "%");
|
||||
if (StrUtil.isNotBlank(name)) {
|
||||
w.or().like(SmHotelEntity::getDistrict, name)
|
||||
.or()
|
||||
.like(SmHotelEntity::getAddress, name);
|
||||
}
|
||||
if (StrUtil.isNotBlank(shortName) && !shortName.equals(name)) {
|
||||
w.or().like(SmHotelEntity::getDistrict, shortName)
|
||||
.or()
|
||||
.like(SmHotelEntity::getAddress, shortName);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 330000→33,330100→3301,330102→330102;保证选省能命中区县码 */
|
||||
static String areaAdcodePrefix(long adcode) {
|
||||
String s = String.valueOf(adcode);
|
||||
// 街道等长码:用所属区县 6 位前缀
|
||||
if (s.length() > 6) {
|
||||
return s.substring(0, 6);
|
||||
}
|
||||
if (s.length() == 6 && s.endsWith("0000")) {
|
||||
return s.substring(0, 2);
|
||||
}
|
||||
if (s.length() == 6 && s.endsWith("00")) {
|
||||
return s.substring(0, 4);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 「浙江省」→「浙江」,便于名称兜底 */
|
||||
static String shortenAreaName(String name) {
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return "";
|
||||
}
|
||||
String n = name.trim();
|
||||
n = n.replaceAll("(壮族|回族|维吾尔|藏族|彝族|朝鲜族)?自治区$", "");
|
||||
n = n.replaceAll("特别行政区$", "");
|
||||
n = n.replaceAll("(省|市|区|县|旗|盟|州)$", "");
|
||||
return n.trim();
|
||||
}
|
||||
|
||||
private Map<String, Object> toListRow(SmHotelEntity h, BigDecimal minSellPrice) {
|
||||
Map<String, Object> row = new LinkedHashMap<>();
|
||||
if (h == null) {
|
||||
@@ -598,18 +615,15 @@ public class SmHotelAppController {
|
||||
if (hotelIds == null || hotelIds.isEmpty()) {
|
||||
return map;
|
||||
}
|
||||
List<SmHotelProductEntity> products = Db.lambdaQuery(SmHotelProductEntity.class)
|
||||
.in(SmHotelProductEntity::getHotelId, hotelIds)
|
||||
.select(SmHotelProductEntity::getHotelId, SmHotelProductEntity::getSellPrice)
|
||||
.list();
|
||||
for (SmHotelProductEntity p : products) {
|
||||
if (p.getHotelId() == null || p.getSellPrice() == null) {
|
||||
List<Map<String, Object>> rows = productMapper.selectMinSellPriceByHotelIds(hotelIds);
|
||||
if (rows == null) {
|
||||
return map;
|
||||
}
|
||||
for (Map<String, Object> row : rows) {
|
||||
if (row == null || row.get("hotelId") == null || row.get("minSell") == null) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal old = map.get(p.getHotelId());
|
||||
if (old == null || p.getSellPrice().compareTo(old) < 0) {
|
||||
map.put(p.getHotelId(), p.getSellPrice());
|
||||
}
|
||||
map.put(Long.valueOf(String.valueOf(row.get("hotelId"))), new BigDecimal(String.valueOf(row.get("minSell"))));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -618,7 +632,7 @@ public class SmHotelAppController {
|
||||
* 解析列表候选酒店:
|
||||
* 1) 入住区间内有日历;
|
||||
* 2) 可选:房型可住人数达标。
|
||||
* 返回 null 表示候选过多,调用方回退 EXISTS。
|
||||
* 返回 null 表示候选过多,调用方回退 IN(子查询)。
|
||||
*/
|
||||
private List<Long> resolveAppListCandidateHotelIds(LocalDate stayFrom, LocalDate stayToExclusive, int needCapPerRoom) {
|
||||
LocalDate from = stayFrom != null ? stayFrom : LocalDate.now();
|
||||
@@ -627,29 +641,63 @@ public class SmHotelAppController {
|
||||
// 未传离店:只扫未来 30 天有日历的酒店,避免无上界扫全表
|
||||
toEx = from.plusDays(30);
|
||||
}
|
||||
String cacheKey = CANDIDATE_CACHE_PREFIX + from + ":" + toEx + ":" + needCapPerRoom;
|
||||
try {
|
||||
String cached = stringRedisTemplate.opsForValue().get(cacheKey);
|
||||
if (CANDIDATE_CACHE_EMPTY.equals(cached)) {
|
||||
return List.of();
|
||||
}
|
||||
if (CANDIDATE_CACHE_OVERFLOW.equals(cached)) {
|
||||
return null;
|
||||
}
|
||||
if (StrUtil.isNotBlank(cached)) {
|
||||
return Arrays.stream(cached.split(","))
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.map(Long::valueOf)
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|候选缓存读取失败|{}", e.getMessage());
|
||||
}
|
||||
List<Long> bookable = dailyMapper.selectDistinctHotelIdsByStayRange(from, toEx);
|
||||
if (bookable == null || bookable.isEmpty()) {
|
||||
cacheCandidateIds(cacheKey, CANDIDATE_CACHE_EMPTY);
|
||||
return List.of();
|
||||
}
|
||||
List<Long> ids = bookable.stream().filter(Objects::nonNull).distinct().toList();
|
||||
if (needCapPerRoom > 1) {
|
||||
List<Long> capHotels = roomMapper.selectDistinctHotelIdsByMinCapacity(needCapPerRoom);
|
||||
if (capHotels == null || capHotels.isEmpty()) {
|
||||
cacheCandidateIds(cacheKey, CANDIDATE_CACHE_EMPTY);
|
||||
return List.of();
|
||||
}
|
||||
Set<Long> capSet = new java.util.HashSet<>(capHotels);
|
||||
ids = ids.stream().filter(capSet::contains).toList();
|
||||
if (ids.isEmpty()) {
|
||||
cacheCandidateIds(cacheKey, CANDIDATE_CACHE_EMPTY);
|
||||
return List.of();
|
||||
}
|
||||
}
|
||||
if (ids.size() > MAX_IN_HOTEL_IDS) {
|
||||
cacheCandidateIds(cacheKey, CANDIDATE_CACHE_OVERFLOW);
|
||||
return null;
|
||||
}
|
||||
cacheCandidateIds(cacheKey, ids.stream().map(String::valueOf).collect(Collectors.joining(",")));
|
||||
return ids;
|
||||
}
|
||||
|
||||
/** 轻量 EXISTS:仅日历表,不再 join product */
|
||||
private void cacheCandidateIds(String cacheKey, String value) {
|
||||
try {
|
||||
stringRedisTemplate.opsForValue().set(cacheKey, value, CANDIDATE_CACHE_MINUTES, TimeUnit.MINUTES);
|
||||
}
|
||||
catch (Exception e) {
|
||||
log.warn("|酒店App|候选缓存写入失败|{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/** 可售:IN(日历 DISTINCT 子查询),替代相关 EXISTS */
|
||||
private void applyBookableHotelFilter(LambdaQueryWrapper<SmHotelEntity> wrapper, LocalDate stayFrom,
|
||||
LocalDate stayToExclusive) {
|
||||
LocalDate from = stayFrom != null ? stayFrom : LocalDate.now();
|
||||
@@ -658,10 +706,9 @@ public class SmHotelAppController {
|
||||
toEx = from.plusDays(30);
|
||||
}
|
||||
wrapper.apply("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM canteen_hotel_daily d
|
||||
WHERE d.hotel_id = canteen_hotel.id
|
||||
AND d.del_flag = '0'
|
||||
id IN (
|
||||
SELECT DISTINCT d.hotel_id FROM canteen_hotel_daily d
|
||||
WHERE d.del_flag = '0'
|
||||
AND d.stay_date >= {0}
|
||||
AND d.stay_date < {1}
|
||||
)
|
||||
@@ -677,10 +724,9 @@ public class SmHotelAppController {
|
||||
return;
|
||||
}
|
||||
wrapper.apply("""
|
||||
EXISTS (
|
||||
SELECT 1 FROM canteen_hotel_room r
|
||||
WHERE r.hotel_id = canteen_hotel.id
|
||||
AND r.del_flag = '0'
|
||||
id IN (
|
||||
SELECT DISTINCT r.hotel_id FROM canteen_hotel_room r
|
||||
WHERE r.del_flag = '0'
|
||||
AND (r.capacity IS NULL OR r.capacity >= {0})
|
||||
)
|
||||
""", needCapPerRoom);
|
||||
@@ -754,34 +800,10 @@ public class SmHotelAppController {
|
||||
return m;
|
||||
}
|
||||
|
||||
/** 按产品最低卖价落在区间内的酒店 id(起价筛选) */
|
||||
/** 按产品最低卖价落在区间内的酒店 id(起价筛选,SQL 聚合) */
|
||||
private List<Long> loadHotelIdsByMinPriceRange(BigDecimal minPrice, BigDecimal maxPrice) {
|
||||
List<SmHotelProductEntity> products = Db.lambdaQuery(SmHotelProductEntity.class)
|
||||
.isNotNull(SmHotelProductEntity::getSellPrice)
|
||||
.select(SmHotelProductEntity::getHotelId, SmHotelProductEntity::getSellPrice)
|
||||
.list();
|
||||
Map<Long, BigDecimal> minMap = new HashMap<>();
|
||||
for (SmHotelProductEntity p : products) {
|
||||
if (p.getHotelId() == null || p.getSellPrice() == null) {
|
||||
continue;
|
||||
}
|
||||
BigDecimal old = minMap.get(p.getHotelId());
|
||||
if (old == null || p.getSellPrice().compareTo(old) < 0) {
|
||||
minMap.put(p.getHotelId(), p.getSellPrice());
|
||||
}
|
||||
}
|
||||
List<Long> ids = new ArrayList<>();
|
||||
for (Map.Entry<Long, BigDecimal> e : minMap.entrySet()) {
|
||||
BigDecimal v = e.getValue();
|
||||
if (minPrice != null && v.compareTo(minPrice) < 0) {
|
||||
continue;
|
||||
}
|
||||
if (maxPrice != null && v.compareTo(maxPrice) > 0) {
|
||||
continue;
|
||||
}
|
||||
ids.add(e.getKey());
|
||||
}
|
||||
return ids;
|
||||
List<Long> ids = productMapper.selectHotelIdsByMinSellPriceRange(minPrice, maxPrice);
|
||||
return ids == null ? List.of() : ids.stream().filter(Objects::nonNull).distinct().toList();
|
||||
}
|
||||
|
||||
private static List<Integer> parseIntCsv(String csv) {
|
||||
|
||||
+32
@@ -40,4 +40,36 @@ public interface SmHotelProductMapper extends PjcloudBaseMapper<SmHotelProductEn
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> countGroupByHotelIds(@Param("ids") Collection<Long> ids);
|
||||
|
||||
/**
|
||||
* 移动端列表起价筛选:按酒店最低卖价落在区间内。
|
||||
*/
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT hotel_id FROM canteen_hotel_product
|
||||
WHERE del_flag = '0' AND sell_price IS NOT NULL
|
||||
GROUP BY hotel_id
|
||||
HAVING 1 = 1
|
||||
<if test="minPrice != null">
|
||||
AND MIN(sell_price) >= #{minPrice}
|
||||
</if>
|
||||
<if test="maxPrice != null">
|
||||
AND MIN(sell_price) <= #{maxPrice}
|
||||
</if>
|
||||
</script>
|
||||
""")
|
||||
List<Long> selectHotelIdsByMinSellPriceRange(@Param("minPrice") BigDecimal minPrice,
|
||||
@Param("maxPrice") BigDecimal maxPrice);
|
||||
|
||||
/** 本页酒店批量取最低卖价 */
|
||||
@Select("""
|
||||
<script>
|
||||
SELECT hotel_id AS hotelId, MIN(sell_price) AS minSell
|
||||
FROM canteen_hotel_product
|
||||
WHERE del_flag = '0' AND sell_price IS NOT NULL AND hotel_id IN
|
||||
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||
GROUP BY hotel_id
|
||||
</script>
|
||||
""")
|
||||
List<Map<String, Object>> selectMinSellPriceByHotelIds(@Param("ids") Collection<Long> ids);
|
||||
}
|
||||
+43
-8
@@ -5,8 +5,8 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
|
||||
|
||||
/**
|
||||
* 行政区划 adcode 前缀匹配(与 App 列表筛选一致):
|
||||
* 选省/市可覆盖全部下级区县。
|
||||
* 行政区划 adcode 匹配(App / PC 列表一致):
|
||||
* 选省/市覆盖全部下级区县;优先数值区间走索引,名称仅作无码数据兜底。
|
||||
*/
|
||||
public final class HotelAreaAdcodeUtil {
|
||||
|
||||
@@ -15,7 +15,7 @@ public final class HotelAreaAdcodeUtil {
|
||||
|
||||
/**
|
||||
* 酒店列表按位置区域过滤:
|
||||
* - 省/市 adcode 用前缀覆盖全部下级;
|
||||
* - 省/市 adcode 用数值区间覆盖全部下级(走 district_adcode 索引);
|
||||
* - 同时兼容仅填了区域名称、未落 district_adcode 的酒店(按名称模糊)。
|
||||
*/
|
||||
public static void applyHotelListFilter(LambdaQueryWrapper<SmHotelEntity> wrapper, Long districtAdcode,
|
||||
@@ -34,11 +34,13 @@ public final class HotelAreaAdcodeUtil {
|
||||
});
|
||||
return;
|
||||
}
|
||||
String prefix = areaAdcodePrefix(districtAdcode);
|
||||
long[] range = areaAdcodeRange(districtAdcode);
|
||||
String name = StrUtil.trim(areaName);
|
||||
String shortName = shortenAreaName(name);
|
||||
wrapper.and(w -> {
|
||||
w.apply("district_adcode is not null and CAST(district_adcode AS CHAR) LIKE {0}", prefix + "%");
|
||||
// 数值半开区间,等价于原 CAST LIKE 前缀,可走 idx_hotel_district_adcode
|
||||
w.apply("district_adcode is not null and district_adcode >= {0} and district_adcode < {1}", range[0],
|
||||
range[1]);
|
||||
if (StrUtil.isNotBlank(name)) {
|
||||
w.or().like(SmHotelEntity::getDistrict, name).or().like(SmHotelEntity::getAddress, name);
|
||||
}
|
||||
@@ -48,7 +50,40 @@ public final class HotelAreaAdcodeUtil {
|
||||
});
|
||||
}
|
||||
|
||||
/** 330000→33,330100→3301,330102→330102 */
|
||||
/**
|
||||
* adcode → 半开区间 [min, maxExclusive)。
|
||||
* <ul>
|
||||
* <li>省 330000 → [330000, 340000)</li>
|
||||
* <li>市 330100 → [330100, 330200)</li>
|
||||
* <li>区 330102 / 街道长码 → [330102, 330103)</li>
|
||||
* </ul>
|
||||
*/
|
||||
public static long[] areaAdcodeRange(long adcode) {
|
||||
String s = String.valueOf(adcode);
|
||||
if (s.length() > 6) {
|
||||
long district = Long.parseLong(s.substring(0, 6));
|
||||
return new long[] { district, district + 1 };
|
||||
}
|
||||
if (s.length() < 6) {
|
||||
StringBuilder sb = new StringBuilder(s);
|
||||
while (sb.length() < 6) {
|
||||
sb.append('0');
|
||||
}
|
||||
s = sb.toString();
|
||||
}
|
||||
if (s.endsWith("0000")) {
|
||||
long base = Long.parseLong(s.substring(0, 2)) * 10000L;
|
||||
return new long[] { base, base + 10000L };
|
||||
}
|
||||
if (s.endsWith("00")) {
|
||||
long base = Long.parseLong(s.substring(0, 4)) * 100L;
|
||||
return new long[] { base, base + 100L };
|
||||
}
|
||||
long district = Long.parseLong(s);
|
||||
return new long[] { district, district + 1 };
|
||||
}
|
||||
|
||||
/** 330000→33,330100→3301,330102→330102(兼容旧调用) */
|
||||
public static String areaAdcodePrefix(long adcode) {
|
||||
String s = String.valueOf(adcode);
|
||||
if (s.length() > 6) {
|
||||
@@ -76,8 +111,8 @@ public final class HotelAreaAdcodeUtil {
|
||||
return true;
|
||||
}
|
||||
if (hotelAdcode != null) {
|
||||
String prefix = areaAdcodePrefix(scopeAdcode);
|
||||
return String.valueOf(hotelAdcode).startsWith(prefix);
|
||||
long[] range = areaAdcodeRange(scopeAdcode);
|
||||
return hotelAdcode >= range[0] && hotelAdcode < range[1];
|
||||
}
|
||||
// 酒店未落 adcode:按区域名兜底
|
||||
String name = StrUtil.trim(scopeName);
|
||||
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
package com.bsd.pjcloud.supermarket.util;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class HotelAreaAdcodeUtilTest {
|
||||
|
||||
@Test
|
||||
void areaAdcodeRange_provinceCityDistrict() {
|
||||
assertArrayEquals(new long[] { 330000L, 340000L }, HotelAreaAdcodeUtil.areaAdcodeRange(330000L));
|
||||
assertArrayEquals(new long[] { 330100L, 330200L }, HotelAreaAdcodeUtil.areaAdcodeRange(330100L));
|
||||
assertArrayEquals(new long[] { 330102L, 330103L }, HotelAreaAdcodeUtil.areaAdcodeRange(330102L));
|
||||
assertArrayEquals(new long[] { 330102L, 330103L }, HotelAreaAdcodeUtil.areaAdcodeRange(330102001L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void matchRegion_numericEquivalentToPrefix() {
|
||||
assertTrue(HotelAreaAdcodeUtil.matchRegion(330000L, "浙江", 330102L, "南湖区"));
|
||||
assertTrue(HotelAreaAdcodeUtil.matchRegion(330100L, "嘉兴", 330102L, "南湖区"));
|
||||
assertTrue(HotelAreaAdcodeUtil.matchRegion(330102L, "南湖区", 330102L, "南湖区"));
|
||||
assertFalse(HotelAreaAdcodeUtil.matchRegion(330100L, "嘉兴", 330200L, "宁波"));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user