This commit is contained in:
dzc committed 2026-10-12 13:44:26 +08:00
commit e407fe3da6
8 files changed
+658 -10

No files matched your search

@@ -71,6 +71,24 @@ public class SmHotelEntity extends Model<SmHotelEntity> {
@Schema(description = "详细地址")
private String address;
@Schema(description = "位置区域名称(如南湖区,对应 sys_area.name)")
private String district;
@Schema(description = "位置区域编码(sys_area.adcode,区县)")
private Long districtAdcode;
@Schema(description = "商圈/地标(如嘉兴南站附近)")
private String bizArea;
@Schema(description = "星级档次:1民宿 2经济 3舒适 4高档 5五星")
private Integer starLevel;
@Schema(description = "评分(0.0~5.0)")
private BigDecimal score;
@Schema(description = "设施标签,逗号分隔")
private String facilityTags;
@Schema(description = "经度")
private BigDecimal longitude;
@@ -0,0 +1,482 @@
package com.bsd.pjcloud.supermarket.controller;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.toolkit.Db;
import com.bsd.pjcloud.common.api.encrypt.annotation.crypto.ApiCryptoAes;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.supermarket.entity.SmHotelDailyEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelOrderEntity;
import com.bsd.pjcloud.supermarket.entity.SmHotelProductEntity;
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.vo.SmHotelDetailVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 移动端 TOC 酒店预订(列表/详情/下单/我的订单)。
*/
@RestController
@RequiredArgsConstructor
@RequestMapping("/app/hotel")
@Tag(name = "移动端酒店预订")
@ApiCryptoAes
public class SmHotelAppController {
/** 星级档次文案:1民宿 2经济 3舒适 4高档 5五星 */
private static final Map<Integer, String> STAR_LABEL = Map.of(1, "民宿", 2, "经济", 3, "舒适", 4, "高档", 5, "五星");
private static final List<String> DEFAULT_FACILITY_OPTIONS = List.of("含早餐", "免费取消", "免费停车", "立即确认",
"限时优惠", "接站服务");
private final SmHotelService hotelService;
private final SmHotelMidService midService;
private final SmHotelOrderService orderService;
@Operation(summary = "列表筛选项(区域/星级/设施)")
@GetMapping("/filter-meta")
public R<Map<String, Object>> filterMeta() {
var online = 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);
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())) {
districts.add(h.getDistrict().trim());
}
if (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());
}
}
}
}
List<Map<String, Object>> starLevels = new ArrayList<>();
STAR_LABEL.forEach((k, v) -> {
Map<String, Object> item = new LinkedHashMap<>();
item.put("value", k);
item.put("label", v);
starLevels.add(item);
});
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);
out.put("facilityTags", new ArrayList<>(facilities));
out.put("scoreOptions", List.of(4.5, 4.0, 3.5, 3.0));
out.put("priceOptions", List.of(
Map.of("label", "150以下", "min", 0, "max", 150),
Map.of("label", "150-300", "min", 150, "max", 300),
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);
}
@Operation(summary = "上架酒店分页(关键字/区域/星级/评分/价格/设施)")
@GetMapping("/page")
public R<Page<Map<String, Object>>> page(@ParameterObject Page<SmHotelEntity> page,
@RequestParam(required = false) String keyword,
@RequestParam(required = false) String city,
@RequestParam(required = false) String district,
@RequestParam(required = false) Long districtAdcode,
@RequestParam(required = false) String bizArea,
@RequestParam(required = false) String starLevels,
@RequestParam(required = false) BigDecimal minScore,
@RequestParam(required = false) BigDecimal minPrice,
@RequestParam(required = false) BigDecimal maxPrice,
@RequestParam(required = false) String facilityTags) {
List<Integer> starList = parseIntCsv(starLevels);
List<String> facilityList = parseStrCsv(facilityTags);
var wrapper = Wrappers.<SmHotelEntity>lambdaQuery()
.and(w -> w.isNull(SmHotelEntity::getUpstreamStatus)
.or()
.ne(SmHotelEntity::getUpstreamStatus, "OFFLINE"))
.and(StrUtil.isNotBlank(keyword), w -> w.like(SmHotelEntity::getHotelName, keyword.trim())
.or()
.like(SmHotelEntity::getAddress, keyword.trim())
.or()
.like(SmHotelEntity::getDistrict, keyword.trim())
.or()
.like(SmHotelEntity::getBizArea, keyword.trim())
.or()
.like(SmHotelEntity::getHotelCode, keyword.trim()))
.and(districtAdcode == null && StrUtil.isNotBlank(city), w -> w.like(SmHotelEntity::getAddress, city.trim())
.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);
wrapper.like(StrUtil.isNotBlank(bizArea), SmHotelEntity::getBizArea, StrUtil.trim(bizArea))
.in(!starList.isEmpty(), SmHotelEntity::getStarLevel, starList)
.ge(minScore != null, SmHotelEntity::getScore, minScore)
.orderByDesc(SmHotelEntity::getScore)
.orderByDesc(SmHotelEntity::getUpdateTime)
.orderByDesc(SmHotelEntity::getId);
for (String tag : facilityList) {
wrapper.like(SmHotelEntity::getFacilityTags, tag);
}
// 起价区间:按产品最低卖价过滤(分页前约束)
if (minPrice != null || maxPrice != null) {
List<Long> priceHitIds = loadHotelIdsByMinPriceRange(minPrice, maxPrice);
if (priceHitIds.isEmpty()) {
Page<Map<String, Object>> empty = new Page<>(page.getCurrent(), page.getSize(), 0);
empty.setRecords(List.of());
return R.ok(empty);
}
wrapper.in(SmHotelEntity::getId, priceHitIds);
}
// 移动端 TOC 不走管理端数据权限,按租户查已上架酒店
Page<SmHotelEntity> raw = hotelService.page(page, wrapper);
List<Long> ids = raw.getRecords().stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList();
Map<Long, BigDecimal> minPriceMap = loadMinSellPrice(ids);
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());
return R.ok(out);
}
@Operation(summary = "酒店详情(含房型/产品)")
@GetMapping("/detail/{id}")
public R<SmHotelDetailVO> detail(@PathVariable Long id) {
R<SmHotelDetailVO> r = midService.getHotelDetail(id);
if (r.getCode() != 0 || r.getData() == null || r.getData().getHotel() == null) {
return r;
}
if ("OFFLINE".equalsIgnoreCase(r.getData().getHotel().getUpstreamStatus())) {
return R.failed("酒店已下架");
}
return r;
}
@Operation(summary = "酒店日历分页")
@GetMapping("/detail/{id}/daily")
public R<Page<SmHotelDailyEntity>> daily(@PathVariable Long id,
@ParameterObject Page<SmHotelDailyEntity> page,
@RequestParam(required = false) String productCode) {
SmHotelEntity hotel = hotelService.getById(id);
if (hotel == null) {
return R.failed("酒店不存在");
}
if ("OFFLINE".equalsIgnoreCase(hotel.getUpstreamStatus())) {
return R.failed("酒店已下架");
}
return midService.pageDaily(id, page, productCode);
}
@Operation(summary = "提交预订")
@PostMapping("/order/create")
public R<JSONObject> createOrder(@RequestBody Map<String, Object> body) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null) {
return R.failed("请先登录");
}
Map<String, Object> req = body == null ? new HashMap<>() : new HashMap<>(body);
if (StrUtil.isBlank(str(req.get("channelNo")))) {
req.put("channelNo", "APP" + user.getId() + System.currentTimeMillis());
}
if (StrUtil.isBlank(str(req.get("contactName")))) {
req.put("contactName", StrUtil.blankToDefault(user.getName(), user.getUsername()));
}
if (StrUtil.isBlank(str(req.get("contactPhone")))) {
req.put("contactPhone", user.getPhone());
}
return orderService.createByAdmin(req);
}
@Operation(summary = "我的酒店订单分页")
@GetMapping("/order/page")
public R<Page<Map<String, Object>>> myOrders(@ParameterObject Page<SmHotelOrderEntity> page,
@RequestParam(required = false) String orderStatus) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null) {
return R.failed("请先登录");
}
SmHotelOrderEntity query = new SmHotelOrderEntity();
query.setOrderStatus(StrUtil.trimToNull(orderStatus));
query.setCreateBy(user.getUsername());
Page<SmHotelOrderEntity> raw = orderService.pageOrders(page, query);
List<Long> hotelIds = raw.getRecords().stream()
.map(SmHotelOrderEntity::getHotelId)
.filter(Objects::nonNull)
.distinct()
.toList();
Map<Long, String> hotelNameMap = new HashMap<>();
if (!hotelIds.isEmpty()) {
List<SmHotelEntity> hotels = hotelService.listByIds(hotelIds);
for (SmHotelEntity h : hotels) {
hotelNameMap.put(h.getId(), h.getHotelName());
}
}
Page<Map<String, Object>> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal());
out.setRecords(raw.getRecords().stream().map(o -> {
Map<String, Object> row = new LinkedHashMap<>();
row.put("id", o.getId());
row.put("channelNo", o.getChannelNo());
row.put("platformNo", o.getPlatformNo());
row.put("hotelId", o.getHotelId());
row.put("hotelCode", o.getHotelCode());
row.put("hotelName", hotelNameMap.get(o.getHotelId()));
row.put("productCode", o.getProductCode());
row.put("roomTypeCode", o.getRoomTypeCode());
row.put("checkInDate", o.getCheckInDate());
row.put("checkOutDate", o.getCheckOutDate());
row.put("count", o.getCount());
row.put("sellAmount", o.getSellAmount());
row.put("orderStatus", o.getOrderStatus());
row.put("bookStatus", o.getBookStatus());
row.put("contactName", o.getContactName());
row.put("contactPhone", o.getContactPhone());
row.put("createTime", o.getCreateTime());
return row;
}).toList());
return R.ok(out);
}
@Operation(summary = "我的订单详情")
@GetMapping("/order/{id}")
public R<SmHotelOrderEntity> orderDetail(@PathVariable Long id) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null) {
return R.failed("请先登录");
}
SmHotelOrderEntity order = orderService.getDetail(id);
if (order == null) {
return R.failed("订单不存在");
}
if (!user.getUsername().equals(order.getCreateBy())) {
return R.failed("无权查看该订单");
}
return R.ok(order);
}
@Operation(summary = "取消我的订单")
@PostMapping("/order/cancel/{id}")
public R<Boolean> cancelOrder(@PathVariable Long id, @RequestBody(required = false) Map<String, Object> body) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null) {
return R.failed("请先登录");
}
SmHotelOrderEntity order = orderService.getDetail(id);
if (order == null) {
return R.failed("订单不存在");
}
if (!user.getUsername().equals(order.getCreateBy())) {
return R.failed("无权取消该订单");
}
String reason = body == null ? "用户取消" : String.valueOf(body.getOrDefault("reason", "用户取消"));
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) {
return row;
}
row.put("id", h.getId());
row.put("hotelName", h.getHotelName());
row.put("hotelCode", h.getHotelCode());
row.put("address", h.getAddress());
row.put("district", h.getDistrict());
row.put("districtAdcode", h.getDistrictAdcode());
row.put("bizArea", h.getBizArea());
row.put("starLevel", h.getStarLevel());
row.put("starLabel", starLabel(h.getStarLevel()));
row.put("score", h.getScore());
row.put("facilityTags", h.getFacilityTags());
row.put("coverImage", h.getCoverImage());
row.put("phone", h.getPhone());
row.put("sheetName", h.getSheetName());
row.put("minSellPrice", minSellPrice);
return row;
}
private Map<Long, BigDecimal> loadMinSellPrice(List<Long> hotelIds) {
Map<Long, BigDecimal> map = new HashMap<>();
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) {
continue;
}
BigDecimal old = map.get(p.getHotelId());
if (old == null || p.getSellPrice().compareTo(old) < 0) {
map.put(p.getHotelId(), p.getSellPrice());
}
}
return map;
}
/** 按产品最低卖价落在区间内的酒店 id(起价筛选) */
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;
}
private static List<Integer> parseIntCsv(String csv) {
if (StrUtil.isBlank(csv)) {
return List.of();
}
return Arrays.stream(csv.split("[,,]"))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.map(s -> {
try {
return Integer.parseInt(s);
}
catch (Exception e) {
return null;
}
})
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
}
private static List<String> parseStrCsv(String csv) {
if (StrUtil.isBlank(csv)) {
return List.of();
}
return Arrays.stream(csv.split("[,,]"))
.map(String::trim)
.filter(StrUtil::isNotBlank)
.distinct()
.collect(Collectors.toList());
}
private static String starLabel(Integer starLevel) {
return starLevel == null ? null : STAR_LABEL.get(starLevel);
}
private static String str(Object o) {
return o == null ? null : String.valueOf(o);
}
}
@@ -77,6 +77,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
.eq(StrUtil.isNotBlank(query.getCompensateStatus()), SmHotelOrderEntity::getCompensateStatus,
query.getCompensateStatus())
.eq(StrUtil.isNotBlank(query.getHotelCode()), SmHotelOrderEntity::getHotelCode, query.getHotelCode())
.eq(StrUtil.isNotBlank(query.getCreateBy()), SmHotelOrderEntity::getCreateBy, query.getCreateBy())
.orderByDesc(SmHotelOrderEntity::getCreateTime));
}
@@ -241,8 +242,13 @@ public class SmHotelOrderServiceImpl extends ServiceImpl<SmHotelOrderMapper, SmH
if (product == null) {
return R.failed("产品不存在,请先拉取酒店详情");
}
if (StrUtil.isBlank(product.getRoomTypeCode()) || StrUtil.isBlank(product.getPriceCode())) {
return R.failed("产品缺少房型码或房价码,请重新拉取上游详情");
boolean localHotelPreview = SmHotelDataSource.isLocal(hotel.getSheetName());
if (StrUtil.isBlank(product.getRoomTypeCode())) {
return R.failed("产品缺少房型码");
}
// 第三方必须有价格码;本地可手工维护,价格码可空
if (!localHotelPreview && StrUtil.isBlank(product.getPriceCode())) {
return R.failed("产品缺少房价码,请重新拉取上游详情");
}
LocalDate checkIn = parseDate(body.get("checkInDate"));
LocalDate checkOut = parseDate(body.get("checkOutDate"));
@@ -425,8 +425,23 @@ public class SmHotelServiceImpl extends ServiceImpl<SmHotelMapper, SmHotelEntity
entity.setCoverImage(trimToNull(entity.getCoverImage()));
entity.setCarouselImages(trimToNull(entity.getCarouselImages()));
entity.setAddress(trimToNull(entity.getAddress()));
entity.setDistrict(trimToNull(entity.getDistrict()));
entity.setBizArea(trimToNull(entity.getBizArea()));
entity.setFacilityTags(trimToNull(entity.getFacilityTags()));
if (entity.getDistrictAdcode() != null && entity.getDistrictAdcode() <= 0) {
entity.setDistrictAdcode(null);
}
entity.setPhone(trimToNull(entity.getPhone()));
entity.setIntro(trimToNull(entity.getIntro()));
if (entity.getStarLevel() != null && (entity.getStarLevel() < 1 || entity.getStarLevel() > 5)) {
entity.setStarLevel(null);
}
if (entity.getScore() != null) {
if (entity.getScore().compareTo(java.math.BigDecimal.ZERO) < 0
|| entity.getScore().compareTo(new java.math.BigDecimal("5.0")) > 0) {
entity.setScore(null);
}
}
}
/** 本地滚加仅数据来源=local 有效;天数钳制 1~366 */
@@ -125,4 +125,14 @@ public class SysAreaEntity extends Model<SysAreaEntity> {
@TableField(fill = FieldFill.INSERT)
@Schema(description = "删除标记")
private String delFlag;
/** 列表展示:父级地区名称(非表字段) */
@TableField(exist = false)
@Schema(description = "父级地区名称")
private String parentName;
/** 列表展示:是否有下级(非表字段) */
@TableField(exist = false)
@Schema(description = "是否有下级")
private Boolean hasChildren;
}
@@ -62,6 +62,37 @@ public class SysAreaController {
return R.ok(sysAreaService.selectTree(sysArea));
}
/**
* 移动端/TOC:下级区域列表(登录即可,无需行政区划管理权限)
*/
@Operation(summary = "App下级区域列表")
@GetMapping("/app/children")
public R listAppChildren(@RequestParam(required = false) Long pid,
@RequestParam(required = false) String areaType,
@RequestParam(required = false) String keyword) {
return R.ok(sysAreaService.listChildren(pid, areaType, keyword));
}
/**
* 移动端/TOC:按名称查区域(城市定位等)
*/
@Operation(summary = "App按名称查区域")
@GetMapping("/app/search")
public R searchApp(@RequestParam String name,
@RequestParam(required = false) String areaType,
@RequestParam(required = false) Integer limit) {
return R.ok(sysAreaService.listByName(name, areaType, limit));
}
/**
* 移动端/TOC:懒加载树(同管理端 tree,登录即可)
*/
@Operation(summary = "App区域树")
@GetMapping("/app/tree")
public R getAppTree(@ParameterObject SysAreaEntity sysArea) {
return R.ok(sysAreaService.selectTree(sysArea));
}
/**
* 获取详细信息
*
@@ -18,11 +18,24 @@ public interface SysAreaService extends IService<SysAreaEntity> {
List<Tree<Long>> selectTree(SysAreaEntity sysArea);
/**
* 分页查询
* 分页查询(附带父级名称)
*
* @param page 分页对象
* @param sysArea 行政区划
* @return Page
*/
Page selectPage(Page page, SysAreaEntity sysArea);
/**
* App/联动:按父编码查询启用中的下级(可按类型过滤)。
* @param pid 父级 adcode,空则默认 100000(中国)
* @param areaType 可选:0国家 1省 2市 3区县 4街道
* @param keyword 名称模糊
*/
List<SysAreaEntity> listChildren(Long pid, String areaType, String keyword);
/**
* 按名称精确/模糊查找区域(如城市名「嘉兴市」)
*/
List<SysAreaEntity> listByName(String name, String areaType, Integer limit);
}
@@ -18,8 +18,10 @@ import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* 行政区划
@@ -29,6 +31,9 @@ import java.util.Optional;
*/
@Service
public class SysAreaServiceImpl extends ServiceImpl<SysAreaMapper, SysAreaEntity> implements SysAreaService {
private static final Long ROOT_ADCODE = 100000L;
/**
* 查询行政区划树
*
@@ -48,27 +53,95 @@ public class SysAreaServiceImpl extends ServiceImpl<SysAreaMapper, SysAreaEntity
HashMap<String, Object> extraMap = MapUtil.of(SysAreaEntity.Fields.adcode, sysAreaEntity.getAdcode());
extraMap.put(SysAreaEntity.Fields.hot, sysAreaEntity.getHot());
extraMap.put(SysAreaEntity.Fields.areaType, sysAreaEntity.getAreaType());
extraMap.put(SysAreaEntity.Fields.name, sysAreaEntity.getName());
extraMap.put("id", sysAreaEntity.getAdcode());
treeNode.setExtra(extraMap);
nodeList.add(treeNode);
}
return TreeUtil.build(nodeList, Optional.ofNullable(sysArea.getPid()).orElse(100000L));
return TreeUtil.build(nodeList, Optional.ofNullable(sysArea.getPid()).orElse(ROOT_ADCODE));
}
/**
* 分页查询
*
* @param page 分页对象
* @param sysArea 行政区划
* @return Page
* 分页查询(附带父级名称,便于管理端看清上下级)
*/
@Override
public Page selectPage(Page page, SysAreaEntity sysArea) {
LambdaQueryWrapper<SysAreaEntity> wrapper = Wrappers.lambdaQuery();
wrapper.like(StrUtil.isNotBlank(sysArea.getName()), SysAreaEntity::getName, sysArea.getName());
// 约定:入参 adcode 表示「查该节点下的直接子级」
wrapper.eq(Objects.nonNull(sysArea.getAdcode()), SysAreaEntity::getPid, sysArea.getAdcode());
wrapper.eq(StrUtil.isNotBlank(sysArea.getAreaType()), SysAreaEntity::getAreaType, sysArea.getAreaType());
wrapper.orderByAsc(SysAreaEntity::getAreaType);
wrapper.orderByDesc(SysAreaEntity::getAreaSort);
return baseMapper.selectPage(page, wrapper);
Page<SysAreaEntity> result = baseMapper.selectPage(page, wrapper);
fillParentName(result.getRecords());
return result;
}
@Override
public List<SysAreaEntity> listChildren(Long pid, String areaType, String keyword) {
Long parent = pid == null ? ROOT_ADCODE : pid;
// App/TOC 与后台列表对齐:不按 area_status 过滤,避免后台能见、移动端空白
LambdaQueryWrapper<SysAreaEntity> wrapper = Wrappers.<SysAreaEntity>lambdaQuery()
.eq(SysAreaEntity::getPid, parent)
.eq(StrUtil.isNotBlank(areaType), SysAreaEntity::getAreaType, areaType)
.like(StrUtil.isNotBlank(keyword), SysAreaEntity::getName, StrUtil.trim(keyword))
.orderByDesc(SysAreaEntity::getHot)
.orderByDesc(SysAreaEntity::getAreaSort)
.orderByAsc(SysAreaEntity::getAdcode);
List<SysAreaEntity> list = baseMapper.selectList(wrapper);
fillParentName(list);
return list;
}
@Override
public List<SysAreaEntity> listByName(String name, String areaType, Integer limit) {
if (StrUtil.isBlank(name)) {
return List.of();
}
int size = limit == null || limit < 1 ? 20 : Math.min(limit, 100);
return baseMapper.selectList(Wrappers.<SysAreaEntity>lambdaQuery()
.eq(StrUtil.isNotBlank(areaType), SysAreaEntity::getAreaType, areaType)
.and(w -> w.eq(SysAreaEntity::getName, name.trim())
.or()
.like(SysAreaEntity::getName, name.trim()))
.orderByDesc(SysAreaEntity::getHot)
.orderByAsc(SysAreaEntity::getAreaType)
.last("limit " + size));
}
private void fillParentName(List<SysAreaEntity> records) {
if (CollUtil.isEmpty(records)) {
return;
}
List<Long> pids = records.stream().map(SysAreaEntity::getPid).filter(Objects::nonNull).distinct().toList();
Map<Long, String> nameMap = new HashMap<>();
if (!pids.isEmpty()) {
List<SysAreaEntity> parents = baseMapper.selectList(Wrappers.<SysAreaEntity>lambdaQuery()
.in(SysAreaEntity::getAdcode, pids)
.select(SysAreaEntity::getAdcode, SysAreaEntity::getName));
nameMap = parents.stream()
.collect(Collectors.toMap(SysAreaEntity::getAdcode, SysAreaEntity::getName, (a, b) -> a));
}
List<Long> adcodes = records.stream().map(SysAreaEntity::getAdcode).filter(Objects::nonNull).toList();
Map<Long, Long> childCount = new HashMap<>();
if (!adcodes.isEmpty()) {
List<SysAreaEntity> children = baseMapper.selectList(Wrappers.<SysAreaEntity>lambdaQuery()
.in(SysAreaEntity::getPid, adcodes)
.select(SysAreaEntity::getPid));
for (SysAreaEntity c : children) {
if (c.getPid() != null) {
childCount.merge(c.getPid(), 1L, Long::sum);
}
}
}
for (SysAreaEntity row : records) {
if (row.getPid() != null) {
row.setParentName(nameMap.get(row.getPid()));
}
row.setHasChildren(childCount.getOrDefault(row.getAdcode(), 0L) > 0);
}
}
}