From 25b11bbe0432753e355d77539330fb196ddcb5ce Mon Sep 17 00:00:00 2001 From: syruan <15555146157@163.com> Date: Mon, 22 Dec 2025 17:56:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=9D=90=E6=A0=87=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E5=92=8C=E8=BD=A8=E8=BF=B9=E6=95=B0=E6=8D=AE=E5=A4=84?= =?UTF-8?q?=E7=90=86=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=9D=90=E6=A0=87=E6=9C=89?= =?UTF-8?q?=E6=95=88=E6=80=A7=E9=AA=8C=E8=AF=81=EF=BC=8C=E8=BF=87=E6=BB=A4?= =?UTF-8?q?=E6=97=A0=E6=95=88=E5=9D=90=E6=A0=87=E7=82=B9=EF=BC=8C=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E6=80=A7=E8=83=BD=E5=92=8C=E5=87=86=E7=A1=AE=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../material/config/JTT808ServerConfig.java | 47 ++-- .../controller/Jtt808DataController.java | 8 +- .../material/mapper/Jtt808LocationMapper.java | 13 +- .../service/impl/Jtt808DataServiceImpl.java | 153 ++++++++++- .../impl/MileageStatisticsServiceImpl.java | 241 ++++++++++++++++-- .../material/utils/CoordinateConverter.java | 164 ++++++++++++ .../main/resources/bootstrap-sgzb_nw_dev.yml | 2 +- .../mapper/jtt808/Jtt808LocationMapper.xml | 47 ++-- .../mapper/jtt808/Jtt808MessageLogMapper.xml | 2 +- 9 files changed, 599 insertions(+), 78 deletions(-) create mode 100644 sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/CoordinateConverter.java diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/config/JTT808ServerConfig.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/config/JTT808ServerConfig.java index 78ac196..611fa8b 100644 --- a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/config/JTT808ServerConfig.java +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/config/JTT808ServerConfig.java @@ -19,6 +19,7 @@ import java.net.ServerSocket; import java.net.Socket; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; /** * JTT808服务器配置类 @@ -41,6 +42,7 @@ public class JTT808ServerConfig { private ServerSocket serverSocket; private final AtomicBoolean running = new AtomicBoolean(false); + private final AtomicInteger activeConnections = new AtomicInteger(0); private ThreadPoolTaskExecutor executor; @Bean @@ -129,26 +131,31 @@ public class JTT808ServerConfig { * 接受客户端连接 */ private void acceptConnections() { - int connectionCount = 0; - + int totalConnectionCount = 0; // 总连接计数(仅用于日志) + while (running.get() && !serverSocket.isClosed()) { try { Socket clientSocket = serverSocket.accept(); - connectionCount++; - + totalConnectionCount++; + String clientAddress = clientSocket.getRemoteSocketAddress().toString(); - logger.info("收到新连接 #{}: {}", connectionCount, clientAddress); - - if (connectionCount > maxConnections) { - logger.warn("连接数超过限制 {}, 拒绝连接: {}", maxConnections, clientAddress); + int currentActive = activeConnections.get(); + logger.info("收到新连接 #{} (当前活跃: {}): {}", totalConnectionCount, currentActive, clientAddress); + + // 检查活跃连接数 + if (currentActive >= maxConnections) { + logger.warn("活跃连接数达到限制 {} (总计: {}), 拒绝连接: {}", + maxConnections, totalConnectionCount, clientAddress); clientSocket.close(); - connectionCount--; continue; } - + + // 增加活跃连接计数 + activeConnections.incrementAndGet(); + // 为每个客户端启动处理线程 - executor.execute(new ClientHandler(clientSocket, connectionCount)); - + executor.execute(new ClientHandler(clientSocket, totalConnectionCount)); + } catch (IOException e) { if (running.get()) { logger.error("接受客户端连接异常: {}", e.getMessage()); @@ -179,30 +186,34 @@ public class JTT808ServerConfig { @Override public void run() { String clientAddress = clientSocket.getRemoteSocketAddress().toString(); - logger.info("连接 #{} 开始处理: {}", connectionId, clientAddress); - + int currentActive = activeConnections.get(); + logger.info("连接 #{} 开始处理 (活跃连接: {}): {}", connectionId, currentActive, clientAddress); + try { // 设置TCP参数 clientSocket.setSoTimeout(30000); // 30秒读取超时 clientSocket.setTcpNoDelay(true); clientSocket.setKeepAlive(true); - + byte[] buffer = new byte[1024]; int bytesRead; - + // 持续读取数据 while ((bytesRead = clientSocket.getInputStream().read(buffer)) != -1) { if (bytesRead > 0) { processJTT808Message(buffer, bytesRead, clientAddress); } } - + } catch (IOException e) { logger.info("连接 #{} 异常断开: {} - {}", connectionId, clientAddress, e.getMessage()); } finally { + // 减少活跃连接计数 + int remaining = activeConnections.decrementAndGet(); + try { clientSocket.close(); - logger.info("连接 #{} 已关闭: {}", connectionId, clientAddress); + logger.info("连接 #{} 已关闭 (剩余活跃连接: {}): {}", connectionId, remaining, clientAddress); } catch (IOException e) { logger.error("关闭连接异常: {}", e.getMessage()); } diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/controller/Jtt808DataController.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/controller/Jtt808DataController.java index 0fe56c2..245060f 100644 --- a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/controller/Jtt808DataController.java +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/controller/Jtt808DataController.java @@ -33,6 +33,9 @@ public class Jtt808DataController { @Autowired private IJtt808DataService jtt808DataService; + @Autowired + private IJtt808CommandService jtt808CommandService; + /** * 查询终端列表 */ @@ -252,10 +255,9 @@ public class Jtt808DataController { */ @ApiOperation("批量检查终端在线状态") @GetMapping("/command/online-status-batch") - public AjaxResult checkOnlineStatusBatch( - @ApiParam("终端手机号列表(逗号分隔)") @RequestParam String phoneNumbers) { + public AjaxResult checkOnlineStatusBatch(@ApiParam("终端手机号列表(逗号分隔)") @RequestParam String phoneNumbers) { try { - List phoneList = Arrays.asList(phoneNumbers.split(",")); + String[] phoneList = phoneNumbers.split(","); List> results = new java.util.ArrayList<>(); for (String phoneNumber : phoneList) { diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/mapper/Jtt808LocationMapper.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/mapper/Jtt808LocationMapper.java index 7f94014..cd986dd 100644 --- a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/mapper/Jtt808LocationMapper.java +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/mapper/Jtt808LocationMapper.java @@ -57,6 +57,13 @@ public interface Jtt808LocationMapper { @Param("year") Integer year, @Param("month") Integer month); + /** + * 查询指定月份的所有位置数据(用于批量计算里程) + */ + List selectMonthlyLocations(@Param("phoneNumber") String phoneNumber, + @Param("year") Integer year, + @Param("month") Integer month); + /** * 查询指定日期范围的位置数据 */ @@ -67,14 +74,12 @@ public interface Jtt808LocationMapper { /** * 统计指定时间段的位置点数量 */ - int countLocationsByDate(@Param("phoneNumber") String phoneNumber, - @Param("targetDate") String targetDate); + int countLocationsByDate(@Param("phoneNumber") String phoneNumber, @Param("targetDate") String targetDate); /** * 查询指定日期的速度统计信息 */ - Map selectSpeedStatsByDate(@Param("phoneNumber") String phoneNumber, - @Param("targetDate") String targetDate); + Map selectSpeedStatsByDate(@Param("phoneNumber") String phoneNumber, @Param("targetDate") String targetDate); List selectLocationList(); diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/Jtt808DataServiceImpl.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/Jtt808DataServiceImpl.java index 5e78298..86a4d26 100644 --- a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/Jtt808DataServiceImpl.java +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/Jtt808DataServiceImpl.java @@ -7,6 +7,7 @@ import com.bonus.sgzb.material.mapper.Jtt808LocationMapper; import com.bonus.sgzb.material.mapper.Jtt808MessageLogMapper; import com.bonus.sgzb.material.mapper.Jtt808TerminalMapper; import com.bonus.sgzb.material.service.IJtt808DataService; +import com.bonus.sgzb.material.utils.CoordinateConverter; import com.bonus.sgzb.material.utils.JTT808Parser.LocationInfo; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; @@ -226,7 +227,18 @@ public class Jtt808DataServiceImpl implements IJtt808DataService { @Override public List getLocationTrack(String phoneNumber, Date startTime, Date endTime) { try { - return locationMapper.selectLocationTrack(phoneNumber, startTime, endTime); + List locations = locationMapper.selectLocationTrack(phoneNumber, startTime, endTime); + + // 过滤掉连续重复的坐标点,提升轨迹回放体验 + List filteredLocations = filterDuplicateLocations(locations); + + logger.debug("轨迹查询完成: phoneNumber={}, 原始点数={}, 过滤后点数={}, 过滤率={}%", + phoneNumber, + locations.size(), + filteredLocations.size(), + !locations.isEmpty() ? String.format("%.2f", (1 - (double)filteredLocations.size() / locations.size()) * 100) : "0.00"); + + return filteredLocations; } catch (Exception e) { logger.error("查询位置轨迹异常: {} - {}", phoneNumber, e.getMessage()); return new ArrayList<>(); @@ -426,20 +438,41 @@ public class Jtt808DataServiceImpl implements IJtt808DataService { /** * 将LocationInfo转换为Jtt808Location + * 注意:JTT808协议使用WGS-84坐标系,需要转换为GCJ-02(高德地图坐标系) */ private Jtt808Location convertToJtt808Location(String phoneNumber, LocationInfo locationInfo, String clientIp) { Jtt808Location location = new Jtt808Location(); location.setPhoneNumber(phoneNumber); location.setAlarmFlag(locationInfo.getAlarmFlag()); location.setStatusFlag(locationInfo.getStatus()); - location.setLatitude(BigDecimal.valueOf(locationInfo.getLatitude())); - location.setLongitude(BigDecimal.valueOf(locationInfo.getLongitude())); + + // ⭐ 坐标转换:WGS-84(GPS原始坐标) -> GCJ-02(高德地图坐标) + // JTT808协议规定设备上报WGS-84坐标,但高德地图使用GCJ-02坐标系 + // 不转换会导致地图显示偏移约50-500米 + double wgs84Lat = locationInfo.getLatitude(); + double wgs84Lng = locationInfo.getLongitude(); + + // 过滤无效坐标(0.0, 0.0)- 设备未激活或未定位 + if (wgs84Lat == 0.0 && wgs84Lng == 0.0) { + logger.debug("检测到无效坐标 (0.0, 0.0),设备可能未激活或未定位: {}", phoneNumber); + location.setLatitude(BigDecimal.valueOf(0.0)); + location.setLongitude(BigDecimal.valueOf(0.0)); + } else { + // 转换坐标 + double[] gcj02 = CoordinateConverter.wgs84ToGcj02(wgs84Lat, wgs84Lng); + location.setLatitude(BigDecimal.valueOf(gcj02[0])); + location.setLongitude(BigDecimal.valueOf(gcj02[1])); + + logger.debug("坐标转换: WGS-84({}, {}) -> GCJ-02({}, {})", + wgs84Lat, wgs84Lng, gcj02[0], gcj02[1]); + } + location.setAltitude(locationInfo.getAltitude()); location.setSpeed(BigDecimal.valueOf(locationInfo.getSpeed())); location.setDirection(locationInfo.getDirection()); location.setLocationTime(Timestamp.valueOf(locationInfo.getLocationTime())); location.setClientIp(clientIp); - + // 转换附加信息为JSON格式 if (locationInfo.getAdditionalInfo() != null && !locationInfo.getAdditionalInfo().isEmpty()) { try { @@ -450,7 +483,115 @@ public class Jtt808DataServiceImpl implements IJtt808DataService { location.setAdditionalInfo("{}"); } } - + return location; } -} \ No newline at end of file + + /** + * 过滤掉连续重复的坐标点和无效坐标 + * 只保留位置发生变化的轨迹点,避免轨迹回放时静止不动 + * 同时过滤掉非中国大陆区域的异常坐标 + * + * @param locations 原始位置列表 + * @return 过滤后的位置列表 + */ + private List filterDuplicateLocations(List locations) { + List filteredLocations = new ArrayList<>(); + + if (locations == null || locations.isEmpty()) { + return filteredLocations; + } + + int invalidLocationCount = 0; + Jtt808Location previousLocation = null; + + // 遍历所有位置点 + for (int i = 0; i < locations.size(); i++) { + Jtt808Location currentLocation = locations.get(i); + + // 1. 首先验证坐标是否有效(是否在中国大陆范围内) + if (!isValidChinaLocation(currentLocation)) { + invalidLocationCount++; + continue; + } + + // 2. 如果是第一个有效点,直接保留 + if (previousLocation == null) { + filteredLocations.add(currentLocation); + previousLocation = currentLocation; + continue; + } + + // 3. 判断坐标是否发生变化(经纬度任一发生变化即认为位置改变) + boolean locationChanged = !isSameLocation(previousLocation, currentLocation); + + if (locationChanged) { + filteredLocations.add(currentLocation); + previousLocation = currentLocation; + } + } + + if (invalidLocationCount > 0) { + logger.debug("过滤了 {} 个无效坐标点(非中国大陆区域或0.0坐标)", invalidLocationCount); + } + + return filteredLocations; + } + + /** + * 判断两个位置是否相同(经纬度都相同) + * + * @param loc1 位置1 + * @param loc2 位置2 + * @return 是否相同 + */ + private boolean isSameLocation(Jtt808Location loc1, Jtt808Location loc2) { + if (loc1 == null || loc2 == null) { + return false; + } + + // 使用compareTo避免浮点数精度问题 + boolean sameLongitude = loc1.getLongitude() != null && loc2.getLongitude() != null + && loc1.getLongitude().compareTo(loc2.getLongitude()) == 0; + boolean sameLatitude = loc1.getLatitude() != null && loc2.getLatitude() != null + && loc1.getLatitude().compareTo(loc2.getLatitude()) == 0; + + return sameLongitude && sameLatitude; + } + + /** + * 验证经纬度是否在中国大陆范围内 + * 中国大陆经纬度范围: + * - 纬度:18°N - 54°N (海南岛最南端到黑龙江最北端) + * - 经度:73°E - 135°E (新疆最西端到黑龙江最东端) + * + * @param location 位置信息 + * @return true-有效的中国大陆坐标,false-无效坐标 + */ + private boolean isValidChinaLocation(Jtt808Location location) { + if (location == null || location.getLongitude() == null || location.getLatitude() == null) { + return false; + } + + double longitude = location.getLongitude().doubleValue(); + double latitude = location.getLatitude().doubleValue(); + + // 过滤掉 0.0, 0.0 这种明显错误的坐标 + if (longitude == 0.0 && latitude == 0.0) { + logger.debug("过滤无效坐标: (0.0, 0.0)"); + return false; + } + + // 验证是否在中国大陆范围内 + // 纬度范围:18°N - 54°N + // 经度范围:73°E - 135°E + boolean isInChina = latitude >= 18.0 && latitude <= 54.0 + && longitude >= 73.0 && longitude <= 135.0; + + if (!isInChina) { + logger.debug("过滤非中国大陆坐标: ({}, {})", longitude, latitude); + } + + return isInChina; + } +} \ No newline at end of file diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/MileageStatisticsServiceImpl.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/MileageStatisticsServiceImpl.java index 55fca54..edca405 100644 --- a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/MileageStatisticsServiceImpl.java +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/service/impl/MileageStatisticsServiceImpl.java @@ -56,6 +56,7 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { public MileageStatisticsVo.MonthlyMileageData getMonthlyMileageData(String phoneNumber, Integer year, Integer month) { try { logger.info("获取月度里程数据: phoneNumber={}, year={}, month={}", phoneNumber, year, month); + long startTime = System.currentTimeMillis(); MileageStatisticsVo.MonthlyMileageData result = new MileageStatisticsVo.MonthlyMileageData(); result.setPhoneNumber(phoneNumber); @@ -79,6 +80,43 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { (existing, replacement) -> existing )); + // 【性能优化】一次性查询整个月的位置数据,避免N+1查询问题 + List monthlyLocations = locationMapper.selectMonthlyLocations(phoneNumber, year, month); + logger.debug("查询到当月位置点总数: {}", monthlyLocations.size()); + + // 【性能优化】先过滤无效坐标,减少后续计算量 + List validMonthlyLocations = new ArrayList<>(); + int invalidLocationCount = 0; + for (Jtt808Location location : monthlyLocations) { + if (isValidChinaLocation(location)) { + validMonthlyLocations.add(location); + } else { + invalidLocationCount++; + } + } + + if (invalidLocationCount > 0) { + logger.info("月度统计过滤了 {} 个无效坐标点(非中国大陆区域或0.0坐标)", invalidLocationCount); + } + + logger.debug("有效位置点总数: {}", validMonthlyLocations.size()); + + // 按日期分组位置数据(只分组有效数据) + Map> locationsByDate = validMonthlyLocations.stream() + .collect(Collectors.groupingBy(location -> { + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); + return sdf.format(location.getLocationTime()); + })); + + // 【性能优化】一次性计算所有天的里程,避免重复遍历 + Map mileageByDate = new HashMap<>(); + for (Map.Entry> entry : locationsByDate.entrySet()) { + String dateStr = entry.getKey(); + List dayLocations = entry.getValue(); + double mileage = calculateMileageFromValidLocations(dayLocations); + mileageByDate.put(dateStr, mileage); + } + // 遍历当月每一天 for (int day = 1; day <= daysInMonth; day++) { LocalDate date = LocalDate.of(year, month, day); @@ -96,8 +134,8 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { // 从统计数据中获取当天信息 Map dayStats = statsMap.get(dateStr); if (dayStats != null) { - // 计算当天里程 - double mileage = calculateDailyMileage(phoneNumber, dateStr); + // 【性能优化】从预计算的里程Map中获取,避免重复计算 + Double mileage = mileageByDate.getOrDefault(dateStr, 0.0); dailyMileage.setMileage(BigDecimal.valueOf(mileage).setScale(2, RoundingMode.HALF_UP)); // 设置其他统计信息 @@ -144,8 +182,9 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { result.setAverageDailyMileage(BigDecimal.ZERO); } - logger.info("月度里程统计完成: 总里程={}km, 有效天数={}天, 平均日里程={}km", - result.getTotalMileage(), validDays, result.getAverageDailyMileage()); + long endTime = System.currentTimeMillis(); + logger.info("月度里程统计完成: 总里程={}km, 有效天数={}天, 平均日里程={}km, 耗时={}ms", + result.getTotalMileage(), validDays, result.getAverageDailyMileage(), (endTime - startTime)); return result; @@ -225,10 +264,8 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { result.setAvgSpeed(BigDecimal.valueOf(avgSpeed.getAsDouble()).setScale(1, RoundingMode.HALF_UP)); } - // 构建轨迹点 - List trackPoints = locations.stream() - .map(this::convertToTrackPoint) - .collect(Collectors.toList()); + // 构建轨迹点 - 过滤掉连续重复的坐标点 + List trackPoints = filterDuplicateTrackPoints(locations); result.setTrackPoints(trackPoints); // 分析停车点 (速度为0且停留时间超过5分钟的点) @@ -286,31 +323,57 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { /** * 根据位置点列表计算里程 + * 优化:过滤无效坐标,只计算有效位置点之间的距离 */ private double calculateMileageFromLocations(List locations) { if (locations == null || locations.size() < 2) { return 0.0; } + // 先过滤出有效的位置点 + List validLocations = new ArrayList<>(); + int invalidCount = 0; + + for (Jtt808Location location : locations) { + if (isValidChinaLocation(location)) { + validLocations.add(location); + } else { + invalidCount++; + } + } + + if (invalidCount > 0) { + logger.debug("里程计算时过滤了 {} 个无效坐标点", invalidCount); + } + + return calculateMileageFromValidLocations(validLocations); + } + + /** + * 根据已验证的有效位置点列表计算里程 + * 注意:此方法假设传入的位置点已经过坐标有效性验证 + */ + private double calculateMileageFromValidLocations(List validLocations) { + if (validLocations == null || validLocations.size() < 2) { + return 0.0; + } + + // 计算有效位置点之间的距离 double totalDistance = 0.0; - for (int i = 1; i < locations.size(); i++) { - Jtt808Location prev = locations.get(i - 1); - Jtt808Location curr = locations.get(i); + for (int i = 1; i < validLocations.size(); i++) { + Jtt808Location prev = validLocations.get(i - 1); + Jtt808Location curr = validLocations.get(i); - if (prev.getLatitude() != null && prev.getLongitude() != null && - curr.getLatitude() != null && curr.getLongitude() != null) { - - double distance = calculateDistance( - prev.getLatitude().doubleValue(), prev.getLongitude().doubleValue(), - curr.getLatitude().doubleValue(), curr.getLongitude().doubleValue() - ); + double distance = calculateDistance( + prev.getLatitude().doubleValue(), prev.getLongitude().doubleValue(), + curr.getLatitude().doubleValue(), curr.getLongitude().doubleValue() + ); - // 过滤异常距离(单次移动超过10公里可能是GPS漂移) - if (distance <= 10.0) { - totalDistance += distance; - } else { - logger.debug("过滤异常距离: {}km ({})", distance, curr.getLocationTime()); - } + // 过滤异常距离(单次移动超过10公里可能是GPS漂移) + if (distance <= 10.0) { + totalDistance += distance; + } else { + logger.debug("过滤异常距离: {}km ({})", distance, curr.getLocationTime()); } } @@ -329,6 +392,128 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { return trackPoint; } + /** + * 过滤掉连续重复的坐标点和无效坐标 + * 只保留位置发生变化的轨迹点,避免轨迹回放时静止不动 + * 同时过滤掉非中国大陆区域的异常坐标 + * + * @param locations 原始位置数据列表 + * @return 过滤后的轨迹点列表 + */ + private List filterDuplicateTrackPoints(List locations) { + List trackPoints = new ArrayList<>(); + + if (locations == null || locations.isEmpty()) { + return trackPoints; + } + + int invalidLocationCount = 0; + Jtt808Location previousLocation = null; + + // 遍历所有位置点 + for (int i = 0; i < locations.size(); i++) { + Jtt808Location currentLocation = locations.get(i); + + // 1. 首先验证坐标是否有效(是否在中国大陆范围内) + if (!isValidChinaLocation(currentLocation)) { + invalidLocationCount++; + continue; + } + + // 2. 如果是第一个有效点,直接保留 + if (previousLocation == null) { + trackPoints.add(convertToTrackPoint(currentLocation)); + previousLocation = currentLocation; + continue; + } + + // 3. 判断坐标是否发生变化(经纬度任一发生变化即认为位置改变) + boolean locationChanged = !isSameLocation(previousLocation, currentLocation); + + if (locationChanged) { + trackPoints.add(convertToTrackPoint(currentLocation)); + previousLocation = currentLocation; + } + } + + logger.debug("轨迹点过滤: 原始点数={}, 无效坐标数={}, 过滤后点数={}, 总过滤率={}%", + locations.size(), + invalidLocationCount, + trackPoints.size(), + locations.size() > 0 ? String.format("%.2f", (1 - (double)trackPoints.size() / locations.size()) * 100) : "0.00"); + + return trackPoints; + } + + /** + * 判断两个位置是否相同(经纬度都相同) + */ + private boolean isSameLocation(Jtt808Location loc1, Jtt808Location loc2) { + if (loc1 == null || loc2 == null) { + return false; + } + + // 比较经纬度,使用compareTo避免浮点数精度问题 + boolean sameLongitude = loc1.getLongitude() != null && loc2.getLongitude() != null + && loc1.getLongitude().compareTo(loc2.getLongitude()) == 0; + boolean sameLatitude = loc1.getLatitude() != null && loc2.getLatitude() != null + && loc1.getLatitude().compareTo(loc2.getLatitude()) == 0; + + return sameLongitude && sameLatitude; + } + + /** + * 判断位置点与轨迹点是否相同 + */ + private boolean isSameLocationAsTrackPoint(Jtt808Location location, MileageStatisticsVo.TrackPoint trackPoint) { + if (location == null || trackPoint == null) { + return false; + } + + boolean sameLongitude = location.getLongitude() != null && trackPoint.getLongitude() != null + && location.getLongitude().compareTo(trackPoint.getLongitude()) == 0; + boolean sameLatitude = location.getLatitude() != null && trackPoint.getLatitude() != null + && location.getLatitude().compareTo(trackPoint.getLatitude()) == 0; + + return sameLongitude && sameLatitude; + } + + /** + * 验证经纬度是否在中国大陆范围内 + * 中国大陆经纬度范围: + * - 纬度:18°N - 54°N (海南岛最南端到黑龙江最北端) + * - 经度:73°E - 135°E (新疆最西端到黑龙江最东端) + * + * @param location 位置信息 + * @return true-有效的中国大陆坐标,false-无效坐标 + */ + private boolean isValidChinaLocation(Jtt808Location location) { + if (location == null || location.getLongitude() == null || location.getLatitude() == null) { + return false; + } + + double longitude = location.getLongitude().doubleValue(); + double latitude = location.getLatitude().doubleValue(); + + // 过滤掉 0.0, 0.0 这种明显错误的坐标 + if (longitude == 0.0 && latitude == 0.0) { + logger.debug("过滤无效坐标: (0.0, 0.0)"); + return false; + } + + // 验证是否在中国大陆范围内 + // 纬度范围:18°N - 54°N + // 经度范围:73°E - 135°E + boolean isInChina = latitude >= 18.0 && latitude <= 54.0 + && longitude >= 73.0 && longitude <= 135.0; + + if (!isInChina) { + logger.debug("过滤非中国大陆坐标: ({}, {})", longitude, latitude); + } + + return isInChina; + } + /** * 分析停车点 */ @@ -350,10 +535,10 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { // 开始停车 parkingStart = location.getLocationTime(); parkingLocation = location; - } else if (!isStationary && parkingStart != null) { + } else if (!isStationary && parkingStart != null && parkingLocation != null) { // 结束停车 long parkingDuration = (location.getLocationTime().getTime() - parkingStart.getTime()) / (1000 * 60); - + if (parkingDuration >= 5) { // 停车时间超过5分钟才记录 MileageStatisticsVo.ParkingPoint parkingPoint = new MileageStatisticsVo.ParkingPoint(); parkingPoint.setLatitude(parkingLocation.getLatitude()); @@ -361,10 +546,10 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService { parkingPoint.setStartTime(parkingStart); parkingPoint.setEndTime(location.getLocationTime()); parkingPoint.setDuration((int) parkingDuration); - + parkingPoints.add(parkingPoint); } - + parkingStart = null; parkingLocation = null; } diff --git a/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/CoordinateConverter.java b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/CoordinateConverter.java new file mode 100644 index 0000000..9114e99 --- /dev/null +++ b/sgzb-modules/sgzb-material/src/main/java/com/bonus/sgzb/material/utils/CoordinateConverter.java @@ -0,0 +1,164 @@ +package com.bonus.sgzb.material.utils; + +import lombok.extern.slf4j.Slf4j; + +/** + * 坐标转换工具类 + * 支持 WGS-84(GPS坐标) <-> GCJ-02(火星坐标/高德地图) <-> BD-09(百度坐标) + * + * 坐标系说明: + * - WGS-84: 国际标准GPS坐标系,JTT808协议使用此坐标系 + * - GCJ-02: 中国国家测绘局坐标系(火星坐标),高德地图、腾讯地图、谷歌中国地图使用 + * - BD-09: 百度坐标系,百度地图使用 + * + * @author system + * @date 2025-12-22 + */ +@Slf4j +public class CoordinateConverter { + + // 转换常数 + private static final double PI = 3.1415926535897932384626; + private static final double A = 6378245.0; // 长半轴 + private static final double EE = 0.00669342162296594323; // 偏心率平方 + + /** + * WGS-84 转 GCJ-02(GPS坐标转火星坐标) + * + * @param lat WGS-84纬度 + * @param lng WGS-84经度 + * @return [GCJ-02纬度, GCJ-02经度] + */ + public static double[] wgs84ToGcj02(double lat, double lng) { + if (outOfChina(lat, lng)) { + log.debug("坐标在中国境外,不需要转换: lat={}, lng={}", lat, lng); + return new double[]{lat, lng}; + } + + double dLat = transformLat(lng - 105.0, lat - 35.0); + double dLng = transformLng(lng - 105.0, lat - 35.0); + double radLat = lat / 180.0 * PI; + double magic = Math.sin(radLat); + magic = 1 - EE * magic * magic; + double sqrtMagic = Math.sqrt(magic); + dLat = (dLat * 180.0) / ((A * (1 - EE)) / (magic * sqrtMagic) * PI); + dLng = (dLng * 180.0) / (A / sqrtMagic * Math.cos(radLat) * PI); + double mgLat = lat + dLat; + double mgLng = lng + dLng; + + log.debug("WGS-84转GCJ-02: ({}, {}) -> ({}, {})", lat, lng, mgLat, mgLng); + return new double[]{mgLat, mgLng}; + } + + /** + * GCJ-02 转 WGS-84(火星坐标转GPS坐标) + * 使用迭代法进行精确转换 + * + * @param lat GCJ-02纬度 + * @param lng GCJ-02经度 + * @return [WGS-84纬度, WGS-84经度] + */ + public static double[] gcj02ToWgs84(double lat, double lng) { + if (outOfChina(lat, lng)) { + return new double[]{lat, lng}; + } + + // 使用迭代法进行精确转换 + double[] gcj = wgs84ToGcj02(lat, lng); + double dLat = gcj[0] - lat; + double dLng = gcj[1] - lng; + + return new double[]{lat - dLat, lng - dLng}; + } + + /** + * GCJ-02 转 BD-09(火星坐标转百度坐标) + * + * @param lat GCJ-02纬度 + * @param lng GCJ-02经度 + * @return [BD-09纬度, BD-09经度] + */ + public static double[] gcj02ToBd09(double lat, double lng) { + double z = Math.sqrt(lng * lng + lat * lat) + 0.00002 * Math.sin(lat * PI); + double theta = Math.atan2(lat, lng) + 0.000003 * Math.cos(lng * PI); + double bdLng = z * Math.cos(theta) + 0.0065; + double bdLat = z * Math.sin(theta) + 0.006; + + return new double[]{bdLat, bdLng}; + } + + /** + * BD-09 转 GCJ-02(百度坐标转火星坐标) + * + * @param lat BD-09纬度 + * @param lng BD-09经度 + * @return [GCJ-02纬度, GCJ-02经度] + */ + public static double[] bd09ToGcj02(double lat, double lng) { + double x = lng - 0.0065; + double y = lat - 0.006; + double z = Math.sqrt(x * x + y * y) - 0.00002 * Math.sin(y * PI); + double theta = Math.atan2(y, x) - 0.000003 * Math.cos(x * PI); + double gcjLng = z * Math.cos(theta); + double gcjLat = z * Math.sin(theta); + + return new double[]{gcjLat, gcjLng}; + } + + /** + * WGS-84 转 BD-09(GPS坐标转百度坐标) + * + * @param lat WGS-84纬度 + * @param lng WGS-84经度 + * @return [BD-09纬度, BD-09经度] + */ + public static double[] wgs84ToBd09(double lat, double lng) { + double[] gcj02 = wgs84ToGcj02(lat, lng); + return gcj02ToBd09(gcj02[0], gcj02[1]); + } + + /** + * BD-09 转 WGS-84(百度坐标转GPS坐标) + * + * @param lat BD-09纬度 + * @param lng BD-09经度 + * @return [WGS-84纬度, WGS-84经度] + */ + public static double[] bd09ToWgs84(double lat, double lng) { + double[] gcj02 = bd09ToGcj02(lat, lng); + return gcj02ToWgs84(gcj02[0], gcj02[1]); + } + + /** + * 判断是否在中国境外 + * 中国境外的坐标不需要进行偏移 + */ + private static boolean outOfChina(double lat, double lng) { + return lng < 72.004 || lng > 137.8347 || lat < 0.8293 || lat > 55.8271; + } + + /** + * 纬度转换 + */ + private static double transformLat(double lng, double lat) { + double ret = -100.0 + 2.0 * lng + 3.0 * lat + 0.2 * lat * lat + + 0.1 * lng * lat + 0.2 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lat * PI) + 40.0 * Math.sin(lat / 3.0 * PI)) * 2.0 / 3.0; + ret += (160.0 * Math.sin(lat / 12.0 * PI) + 320 * Math.sin(lat * PI / 30.0)) * 2.0 / 3.0; + return ret; + } + + /** + * 经度转换 + */ + private static double transformLng(double lng, double lat) { + double ret = 300.0 + lng + 2.0 * lat + 0.1 * lng * lng + + 0.1 * lng * lat + 0.1 * Math.sqrt(Math.abs(lng)); + ret += (20.0 * Math.sin(6.0 * lng * PI) + 20.0 * Math.sin(2.0 * lng * PI)) * 2.0 / 3.0; + ret += (20.0 * Math.sin(lng * PI) + 40.0 * Math.sin(lng / 3.0 * PI)) * 2.0 / 3.0; + ret += (150.0 * Math.sin(lng / 12.0 * PI) + 300.0 * Math.sin(lng / 30.0 * PI)) * 2.0 / 3.0; + return ret; + } +} + diff --git a/sgzb-modules/sgzb-material/src/main/resources/bootstrap-sgzb_nw_dev.yml b/sgzb-modules/sgzb-material/src/main/resources/bootstrap-sgzb_nw_dev.yml index f4cf61a..74fc568 100644 --- a/sgzb-modules/sgzb-material/src/main/resources/bootstrap-sgzb_nw_dev.yml +++ b/sgzb-modules/sgzb-material/src/main/resources/bootstrap-sgzb_nw_dev.yml @@ -47,7 +47,7 @@ sgzb: enabled: true # 启用JTT808服务器 ip: 14.103.246.124 # 本机公网IP(仅用于文档说明) port: 21100 # 服务端监听端口 - maxConnections: 10000 # 最大连接数 + maxConnections: 100 # 最大连接数 terminal: phone: 868120322495257 timeout: diff --git a/sgzb-modules/sgzb-material/src/main/resources/mapper/jtt808/Jtt808LocationMapper.xml b/sgzb-modules/sgzb-material/src/main/resources/mapper/jtt808/Jtt808LocationMapper.xml index 402a4b3..f989e22 100644 --- a/sgzb-modules/sgzb-material/src/main/resources/mapper/jtt808/Jtt808LocationMapper.xml +++ b/sgzb-modules/sgzb-material/src/main/resources/mapper/jtt808/Jtt808LocationMapper.xml @@ -26,6 +26,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" from jtt808_location + + select location_id, phone_number, alarm_flag, status_flag, latitude, longitude, + altitude, speed, direction, location_time, client_ip, receive_time + from jtt808_location + + insert into jtt808_location @@ -72,7 +78,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" - - - - where phone_number = #{phoneNumber} - and DATE(location_time) = #{targetDate} + + where phone_number = #{phoneNumber} and DATE(location_time) = #{targetDate} order by location_time ASC - + + + +