From a74dec5e7abd684f16f0ade5ad504a5bf1fe5212 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Tue, 20 Oct 2026 12:47:16 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=92=E5=BA=97=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/SmHotelAppController.java | 278 ++++++++++-------- .../mapper/SmHotelProductMapper.java | 32 ++ .../supermarket/util/HotelAreaAdcodeUtil.java | 51 +++- .../util/HotelAreaAdcodeUtilTest.java | 26 ++ 4 files changed, 251 insertions(+), 136 deletions(-) create mode 100644 pjcloud-supermarket/pjcloud-supermarket-biz/src/test/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtilTest.java 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 9774ec8d8..63b5127a5 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 @@ -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> filterMeta() { - var online = Wrappers.lambdaQuery() + try { + String cached = stringRedisTemplate.opsForValue().get(FILTER_META_CACHE_KEY); + if (StrUtil.isNotBlank(cached)) { + @SuppressWarnings("unchecked") + Map 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 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 onlineHotelWrapper() { + return Wrappers.lambdaQuery() .and(w -> w.isNull(SmHotelEntity::getUpstreamStatus) .or() - .ne(SmHotelEntity::getUpstreamStatus, "OFFLINE")) - .select(SmHotelEntity::getId, SmHotelEntity::getDistrict, SmHotelEntity::getBizArea, - SmHotelEntity::getFacilityTags); - List rows = hotelService.list(online); + .ne(SmHotelEntity::getUpstreamStatus, "OFFLINE")); + } + + /** + * 筛选项:按列 GROUP BY,避免全表加载酒店实体;districts 仅兜底(App 优先走行政区划接口)。 + */ + private Map buildFilterMeta() { Set districts = new LinkedHashSet<>(); - Set bizAreas = new LinkedHashSet<>(); - Set 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 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 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 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 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 toListRow(SmHotelEntity h, BigDecimal minSellPrice) { Map row = new LinkedHashMap<>(); if (h == null) { @@ -598,18 +615,15 @@ public class SmHotelAppController { if (hotelIds == null || hotelIds.isEmpty()) { return map; } - List 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> rows = productMapper.selectMinSellPriceByHotelIds(hotelIds); + if (rows == null) { + return map; + } + for (Map 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 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 bookable = dailyMapper.selectDistinctHotelIdsByStayRange(from, toEx); if (bookable == null || bookable.isEmpty()) { + cacheCandidateIds(cacheKey, CANDIDATE_CACHE_EMPTY); return List.of(); } List ids = bookable.stream().filter(Objects::nonNull).distinct().toList(); if (needCapPerRoom > 1) { List capHotels = roomMapper.selectDistinctHotelIdsByMinCapacity(needCapPerRoom); if (capHotels == null || capHotels.isEmpty()) { + cacheCandidateIds(cacheKey, CANDIDATE_CACHE_EMPTY); return List.of(); } Set 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 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 loadHotelIdsByMinPriceRange(BigDecimal minPrice, BigDecimal maxPrice) { - List products = Db.lambdaQuery(SmHotelProductEntity.class) - .isNotNull(SmHotelProductEntity::getSellPrice) - .select(SmHotelProductEntity::getHotelId, SmHotelProductEntity::getSellPrice) - .list(); - Map 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 ids = new ArrayList<>(); - for (Map.Entry 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 ids = productMapper.selectHotelIdsByMinSellPriceRange(minPrice, maxPrice); + return ids == null ? List.of() : ids.stream().filter(Objects::nonNull).distinct().toList(); } private static List parseIntCsv(String csv) { diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelProductMapper.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelProductMapper.java index f9301f5c8..eb2fb1252 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelProductMapper.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/mapper/SmHotelProductMapper.java @@ -40,4 +40,36 @@ public interface SmHotelProductMapper extends PjcloudBaseMapper """) List> countGroupByHotelIds(@Param("ids") Collection ids); + + /** + * 移动端列表起价筛选:按酒店最低卖价落在区间内。 + */ + @Select(""" + + """) + List selectHotelIdsByMinSellPriceRange(@Param("minPrice") BigDecimal minPrice, + @Param("maxPrice") BigDecimal maxPrice); + + /** 本页酒店批量取最低卖价 */ + @Select(""" + + """) + List> selectMinSellPriceByHotelIds(@Param("ids") Collection ids); } diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtil.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtil.java index 11aa44e14..38fcdb421 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtil.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtil.java @@ -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 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)。 + *
    + *
  • 省 330000 → [330000, 340000)
  • + *
  • 市 330100 → [330100, 330200)
  • + *
  • 区 330102 / 街道长码 → [330102, 330103)
  • + *
+ */ + 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); diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/test/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtilTest.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/test/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtilTest.java new file mode 100644 index 000000000..b59e6b47f --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/test/java/com/bsd/pjcloud/supermarket/util/HotelAreaAdcodeUtilTest.java @@ -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, "宁波")); + } +}