From 3b92659e7ac75602babeaad56a55972dc7d68bc9 Mon Sep 17 00:00:00 2001 From: zjf <1799650030@qq.com> Date: Sat, 10 Oct 2026 18:02:02 +0800 Subject: [PATCH] =?UTF-8?q?=E9=85=92=E5=BA=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../supermarket/entity/SmHotelEntity.java | 18 + .../controller/SmHotelAppController.java | 482 ++++++++++++++++++ .../service/impl/SmHotelOrderServiceImpl.java | 10 +- .../service/impl/SmHotelServiceImpl.java | 15 + .../admin/api/entity/SysAreaEntity.java | 10 + .../admin/controller/SysAreaController.java | 31 ++ .../pjcloud/admin/service/SysAreaService.java | 15 +- .../service/impl/SysAreaServiceImpl.java | 87 +++- 8 files changed, 658 insertions(+), 10 deletions(-) create mode 100644 pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java diff --git a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java index b710cbf60..dac0ee283 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java +++ b/pjcloud-supermarket/pjcloud-supermarket-api/src/main/java/com/bsd/pjcloud/supermarket/entity/SmHotelEntity.java @@ -71,6 +71,24 @@ public class SmHotelEntity extends Model { @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; 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 new file mode 100644 index 000000000..d10165df4 --- /dev/null +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/controller/SmHotelAppController.java @@ -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 STAR_LABEL = Map.of(1, "民宿", 2, "经济", 3, "舒适", 4, "高档", 5, "五星"); + + private static final List DEFAULT_FACILITY_OPTIONS = List.of("含早餐", "免费取消", "免费停车", "立即确认", + "限时优惠", "接站服务"); + + private final SmHotelService hotelService; + + private final SmHotelMidService midService; + + private final SmHotelOrderService orderService; + + @Operation(summary = "列表筛选项(区域/星级/设施)") + @GetMapping("/filter-meta") + public R> filterMeta() { + var online = 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); + 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())) { + 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> starLevels = new ArrayList<>(); + STAR_LABEL.forEach((k, v) -> { + Map 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 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(@ParameterObject Page 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 starList = parseIntCsv(starLevels); + List facilityList = parseStrCsv(facilityTags); + var wrapper = Wrappers.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 priceHitIds = loadHotelIdsByMinPriceRange(minPrice, maxPrice); + if (priceHitIds.isEmpty()) { + Page> empty = new Page<>(page.getCurrent(), page.getSize(), 0); + empty.setRecords(List.of()); + return R.ok(empty); + } + wrapper.in(SmHotelEntity::getId, priceHitIds); + } + // 移动端 TOC 不走管理端数据权限,按租户查已上架酒店 + Page raw = hotelService.page(page, wrapper); + List ids = raw.getRecords().stream().map(SmHotelEntity::getId).filter(Objects::nonNull).toList(); + Map minPriceMap = loadMinSellPrice(ids); + Page> 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 detail(@PathVariable Long id) { + R 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> daily(@PathVariable Long id, + @ParameterObject Page 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 createOrder(@RequestBody Map body) { + PjcloudUser user = SecurityUtils.getUser(); + if (user == null) { + return R.failed("请先登录"); + } + Map 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>> myOrders(@ParameterObject Page 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 raw = orderService.pageOrders(page, query); + List hotelIds = raw.getRecords().stream() + .map(SmHotelOrderEntity::getHotelId) + .filter(Objects::nonNull) + .distinct() + .toList(); + Map hotelNameMap = new HashMap<>(); + if (!hotelIds.isEmpty()) { + List hotels = hotelService.listByIds(hotelIds); + for (SmHotelEntity h : hotels) { + hotelNameMap.put(h.getId(), h.getHotelName()); + } + } + Page> out = new Page<>(raw.getCurrent(), raw.getSize(), raw.getTotal()); + out.setRecords(raw.getRecords().stream().map(o -> { + Map 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 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 cancelOrder(@PathVariable Long id, @RequestBody(required = false) Map 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 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) { + 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 loadMinSellPrice(List hotelIds) { + Map map = new HashMap<>(); + 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) { + 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 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; + } + + private static List 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 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); + } +} diff --git a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java index 14246a68d..5db538670 100644 --- a/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java +++ b/pjcloud-supermarket/pjcloud-supermarket-biz/src/main/java/com/bsd/pjcloud/supermarket/service/impl/SmHotelOrderServiceImpl.java @@ -77,6 +77,7 @@ public class SmHotelOrderServiceImpl extends ServiceImpl 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 */ diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysAreaEntity.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysAreaEntity.java index ff2885096..cf207bd0d 100644 --- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysAreaEntity.java +++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/entity/SysAreaEntity.java @@ -125,4 +125,14 @@ public class SysAreaEntity extends Model { @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; } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysAreaController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysAreaController.java index 37093b384..e62b7e17c 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysAreaController.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysAreaController.java @@ -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)); + } + /** * 获取详细信息 * diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysAreaService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysAreaService.java index 0968ae24d..61d7fbe30 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysAreaService.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysAreaService.java @@ -18,11 +18,24 @@ public interface SysAreaService extends IService { List> 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 listChildren(Long pid, String areaType, String keyword); + + /** + * 按名称精确/模糊查找区域(如城市名「嘉兴市」) + */ + List listByName(String name, String areaType, Integer limit); } diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysAreaServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysAreaServiceImpl.java index 9b4d33a4b..e0ebbae2a 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysAreaServiceImpl.java +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysAreaServiceImpl.java @@ -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 implements SysAreaService { + + private static final Long ROOT_ADCODE = 100000L; + /** * 查询行政区划树 * @@ -48,27 +53,95 @@ public class SysAreaServiceImpl extends ServiceImpl 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 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 result = baseMapper.selectPage(page, wrapper); + fillParentName(result.getRecords()); + return result; } + + @Override + public List listChildren(Long pid, String areaType, String keyword) { + Long parent = pid == null ? ROOT_ADCODE : pid; + // App/TOC 与后台列表对齐:不按 area_status 过滤,避免后台能见、移动端空白 + LambdaQueryWrapper wrapper = Wrappers.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 list = baseMapper.selectList(wrapper); + fillParentName(list); + return list; + } + + @Override + public List 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.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 records) { + if (CollUtil.isEmpty(records)) { + return; + } + List pids = records.stream().map(SysAreaEntity::getPid).filter(Objects::nonNull).distinct().toList(); + Map nameMap = new HashMap<>(); + if (!pids.isEmpty()) { + List parents = baseMapper.selectList(Wrappers.lambdaQuery() + .in(SysAreaEntity::getAdcode, pids) + .select(SysAreaEntity::getAdcode, SysAreaEntity::getName)); + nameMap = parents.stream() + .collect(Collectors.toMap(SysAreaEntity::getAdcode, SysAreaEntity::getName, (a, b) -> a)); + } + List adcodes = records.stream().map(SysAreaEntity::getAdcode).filter(Objects::nonNull).toList(); + Map childCount = new HashMap<>(); + if (!adcodes.isEmpty()) { + List children = baseMapper.selectList(Wrappers.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); + } + } }