Kaynağa Gözat

feat: 小程序附近站点按距离过滤,超 50 公里站点不再展示

- listStation 接口新增 latitude/longitude/maxDistance 入参(0,0 视为未定位)
- 站点坐标存于 JSON 字符串无法建索引,采用全量查询后内存 Haversine 过滤排序再手动分页
- 未传经纬度走原 PageHelper 分页逻辑,旧调用方行为不变
- WashStationVo 新增 distance 字段,首页优先使用后端距离并保留本地兜底计算
skyline 3 gün önce
ebeveyn
işleme
badf210fca

+ 15 - 0
car-wash-entity/src/main/java/com/kym/entity/queryParams/StationQueryParams.java

@@ -12,4 +12,19 @@ public class StationQueryParams extends PageParams {
     private String stationName;
     private String stationType;
     private String address;
+
+    /**
+     * 用户纬度,与 longitude 同时传入时启用附近站点距离过滤(0,0 视为未定位)
+     */
+    private Double latitude;
+
+    /**
+     * 用户经度
+     */
+    private Double longitude;
+
+    /**
+     * 最大距离(公里),仅在传入经纬度时生效,不传默认 50 公里
+     */
+    private Double maxDistance;
 }

+ 5 - 0
car-wash-entity/src/main/java/com/kym/entity/vo/WashStationVo.java

@@ -66,6 +66,11 @@ public class WashStationVo extends BaseEntity {
      */
     private String location;
 
+    /**
+     * 与用户的距离(公里),仅在传入用户经纬度时返回
+     */
+    private Double distance;
+
     /**
      * 站点引导
      */

+ 9 - 2
car-wash-mp/src/pages/index/index.vue

@@ -423,10 +423,17 @@ const loadCurrentLocation = () => {
 
 const loadWashStationList = () => {
   isLoading.value = true;
-  post(`/wash-station/listStation`, {pageSize: 5}).then((res: any) => {
+  // 有定位时传经纬度,后端按距离过滤(默认50公里)并按距离升序返回
+  let params: any = {pageSize: 5};
+  if (state.location && (state.location.latitude || state.location.longitude)) {
+    params.latitude = state.location.latitude;
+    params.longitude = state.location.longitude;
+  }
+  post(`/wash-station/listStation`, params).then((res: any) => {
     let {list, total} = res;
     list.forEach((station: any) => {
-      if (station.location) {
+      // 后端未返回距离时兜底本地计算(兼容旧后端)
+      if (station.distance == null && station.location && state.location && (state.location.latitude || state.location.longitude)) {
         let stationLocation = JSON.parse(station.location);
         station.distance = calcMapDistance(state.location.latitude, state.location.longitude, stationLocation.stationLat, stationLocation.stationLng);
       }

+ 106 - 3
car-wash-service/src/main/java/com/kym/service/impl/WashStationServiceImpl.java

@@ -1,5 +1,8 @@
 package com.kym.service.impl;
 
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.github.pagehelper.Page;
 import com.github.pagehelper.PageHelper;
 import com.kym.common.utils.CommUtil;
 import com.kym.entity.common.PageBean;
@@ -18,7 +21,10 @@ import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.stream.Collectors;
 
 /**
@@ -95,8 +101,18 @@ public class WashStationServiceImpl extends MyBaseServiceImpl<WashStationMapper,
 
 
     // ====================================================== 小程序接口 Start ===========================================================
+
+    /**
+     * 默认最大距离(公里)
+     */
+    private static final double DEFAULT_MAX_DISTANCE_KM = 50d;
+
     @Override
     public PageBean<WashStationVo> listStationForApp(StationQueryParams params) {
+        // 传入有效经纬度时启用附近站点模式:内存计算距离、过滤、排序后手动分页
+        if (hasUserLocation(params)) {
+            return listStationByDistance(params);
+        }
         PageHelper.startPage(params.getPageNum(), params.getPageSize());
         var stationList = lambdaQuery()
                 .eq(CommUtil.isNotEmptyAndNull(params.getStationId()), WashStation::getStationId, params.getStationId())
@@ -105,18 +121,105 @@ public class WashStationServiceImpl extends MyBaseServiceImpl<WashStationMapper,
         if (stationList.isEmpty()) {
             return new PageBean<>(Collections.emptyList());
         }
+        var voList = buildStationVoList(stationList, Collections.emptyMap());
+        return new PageBean<>(voList);
+    }
+
+    /**
+     * 附近站点:站点表数据量小且坐标存于 JSON 字符串无法走索引,
+     * 故全量查出后在内存中按 Haversine 公式计算距离,过滤超远站点并按距离升序,再分页。
+     */
+    private PageBean<WashStationVo> listStationByDistance(StationQueryParams params) {
+        double userLat = params.getLatitude();
+        double userLng = params.getLongitude();
+        double maxDistance = params.getMaxDistance() != null && params.getMaxDistance() > 0
+                ? params.getMaxDistance() : DEFAULT_MAX_DISTANCE_KM;
+
+        // 距离模式下需要先过滤排序再分页,因此不能用 PageHelper 分页查询
+        Map<String, Double> distanceMap = new HashMap<>();
+        var matchedList = lambdaQuery()
+                .eq(CommUtil.isNotEmptyAndNull(params.getStationId()), WashStation::getStationId, params.getStationId())
+                .like(CommUtil.isNotEmptyAndNull(params.getStationName()), WashStation::getStationName, params.getStationName())
+                .list().stream()
+                .filter(station -> {
+                    Double distance = calcDistanceKm(userLat, userLng, station.getLocation());
+                    if (distance == null || distance > maxDistance) {
+                        return false;
+                    }
+                    distanceMap.put(station.getStationId(), distance);
+                    return true;
+                })
+                .sorted(Comparator.comparing(distanceMap::get))
+                .toList();
+
+        int total = matchedList.size();
+        int pageNum = Math.max(params.getPageNum(), 1);
+        int pageSize = Math.max(params.getPageSize(), 1);
+        int fromIndex = (pageNum - 1) * pageSize;
+        List<WashStation> pageList = fromIndex >= total
+                ? Collections.emptyList()
+                : matchedList.subList(fromIndex, Math.min(fromIndex + pageSize, total));
+        if (pageList.isEmpty()) {
+            Page<WashStationVo> emptyPage = new Page<>(pageNum, pageSize);
+            emptyPage.setTotal(total);
+            return new PageBean<>(emptyPage);
+        }
+        var voList = buildStationVoList(pageList, distanceMap);
+        Page<WashStationVo> page = new Page<>(pageNum, pageSize);
+        page.setTotal(total);
+        page.addAll(voList);
+        return new PageBean<>(page);
+    }
+
+    private static boolean hasUserLocation(StationQueryParams params) {
+        return params.getLatitude() != null && params.getLongitude() != null
+                && (params.getLatitude() != 0 || params.getLongitude() != 0);
+    }
+
+    /**
+     * 解析站点 location JSON 并计算与用户的球面距离(公里),坐标缺失或格式异常返回 null
+     */
+    private static Double calcDistanceKm(double userLat, double userLng, String location) {
+        if (CommUtil.isEmptyOrNull(location)) {
+            return null;
+        }
+        double stationLat;
+        double stationLng;
+        try {
+            JSONObject json = JSON.parseObject(location);
+            stationLat = json.getDoubleValue("stationLat");
+            stationLng = json.getDoubleValue("stationLng");
+        } catch (Exception e) {
+            return null;
+        }
+        if (stationLat == 0 && stationLng == 0) {
+            return null;
+        }
+        double radLat1 = Math.toRadians(userLat);
+        double radLat2 = Math.toRadians(stationLat);
+        double a = radLat1 - radLat2;
+        double b = Math.toRadians(userLng) - Math.toRadians(stationLng);
+        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a / 2), 2)
+                + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
+        s = s * 6378.137;
+        return Math.round(s * 100) / 100.0;
+    }
+
+    /**
+     * 组装站点 VO(含空闲设备数),distanceMap 非空时回填距离
+     */
+    private List<WashStationVo> buildStationVoList(List<WashStation> stationList, Map<String, Double> distanceMap) {
         // 查询站点下的所有设备,按照站点统计可用设备数量
         var res = washDeviceService.lambdaQuery().in(WashDevice::getStationId, stationList.stream().map(WashStation::getStationId).toList())
                 .eq(WashDevice::getState, WashDevice.STATE_空闲)
                 .list().stream().collect(Collectors.groupingBy(WashDevice::getStationId, Collectors.counting()));
-        var voList = stationList.stream().map(station -> {
+        return stationList.stream().map(station -> {
             var vo = new WashStationVo();
             BeanUtils.copyProperties(station, vo);
             vo.setIdleParkingNum(Math.toIntExact(res.getOrDefault(station.getStationId(), 0L)));
+            vo.setDistance(distanceMap.get(station.getStationId()));
             return vo;
         }).toList();
-        return new PageBean<>(voList);
-
     }
 
     // ====================================================== 小程序接口 End ===========================================================