优化坐标转换和轨迹数据处理,增加坐标有效性验证,过滤无效坐标点,提升性能和准确性

This commit is contained in:
syruan 2025-12-22 17:56:21 +08:00
parent 05341e2a77
commit 25b11bbe04
9 changed files with 599 additions and 78 deletions

View File

@ -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());
}

View File

@ -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<String> phoneList = Arrays.asList(phoneNumbers.split(","));
String[] phoneList = phoneNumbers.split(",");
List<Map<String, Object>> results = new java.util.ArrayList<>();
for (String phoneNumber : phoneList) {

View File

@ -57,6 +57,13 @@ public interface Jtt808LocationMapper {
@Param("year") Integer year,
@Param("month") Integer month);
/**
* 查询指定月份的所有位置数据用于批量计算里程
*/
List<Jtt808Location> 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<String, Object> selectSpeedStatsByDate(@Param("phoneNumber") String phoneNumber,
@Param("targetDate") String targetDate);
Map<String, Object> selectSpeedStatsByDate(@Param("phoneNumber") String phoneNumber, @Param("targetDate") String targetDate);
List<Jtt808Location> selectLocationList();

View File

@ -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<Jtt808Location> getLocationTrack(String phoneNumber, Date startTime, Date endTime) {
try {
return locationMapper.selectLocationTrack(phoneNumber, startTime, endTime);
List<Jtt808Location> locations = locationMapper.selectLocationTrack(phoneNumber, startTime, endTime);
// 过滤掉连续重复的坐标点提升轨迹回放体验
List<Jtt808Location> 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-84GPS原始坐标 -> 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;
}
}
/**
* 过滤掉连续重复的坐标点和无效坐标
* 只保留位置发生变化的轨迹点避免轨迹回放时静止不动
* 同时过滤掉非中国大陆区域的异常坐标
*
* @param locations 原始位置列表
* @return 过滤后的位置列表
*/
private List<Jtt808Location> filterDuplicateLocations(List<Jtt808Location> locations) {
List<Jtt808Location> 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;
}
}

View File

@ -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<Jtt808Location> monthlyLocations = locationMapper.selectMonthlyLocations(phoneNumber, year, month);
logger.debug("查询到当月位置点总数: {}", monthlyLocations.size());
// 性能优化先过滤无效坐标减少后续计算量
List<Jtt808Location> 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<String, List<Jtt808Location>> locationsByDate = validMonthlyLocations.stream()
.collect(Collectors.groupingBy(location -> {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
return sdf.format(location.getLocationTime());
}));
// 性能优化一次性计算所有天的里程避免重复遍历
Map<String, Double> mileageByDate = new HashMap<>();
for (Map.Entry<String, List<Jtt808Location>> entry : locationsByDate.entrySet()) {
String dateStr = entry.getKey();
List<Jtt808Location> 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<String, Object> 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<MileageStatisticsVo.TrackPoint> trackPoints = locations.stream()
.map(this::convertToTrackPoint)
.collect(Collectors.toList());
// 构建轨迹点 - 过滤掉连续重复的坐标点
List<MileageStatisticsVo.TrackPoint> trackPoints = filterDuplicateTrackPoints(locations);
result.setTrackPoints(trackPoints);
// 分析停车点 (速度为0且停留时间超过5分钟的点)
@ -286,31 +323,57 @@ public class MileageStatisticsServiceImpl implements IMileageStatisticsService {
/**
* 根据位置点列表计算里程
* 优化过滤无效坐标只计算有效位置点之间的距离
*/
private double calculateMileageFromLocations(List<Jtt808Location> locations) {
if (locations == null || locations.size() < 2) {
return 0.0;
}
// 先过滤出有效的位置点
List<Jtt808Location> 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<Jtt808Location> 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<MileageStatisticsVo.TrackPoint> filterDuplicateTrackPoints(List<Jtt808Location> locations) {
List<MileageStatisticsVo.TrackPoint> 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;
}

View File

@ -0,0 +1,164 @@
package com.bonus.sgzb.material.utils;
import lombok.extern.slf4j.Slf4j;
/**
* 坐标转换工具类
* 支持 WGS-84GPS坐标 <-> 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-02GPS坐标转火星坐标
*
* @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-09GPS坐标转百度坐标
*
* @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;
}
}

View File

@ -47,7 +47,7 @@ sgzb:
enabled: true # 启用JTT808服务器
ip: 14.103.246.124 # 本机公网IP仅用于文档说明
port: 21100 # 服务端监听端口
maxConnections: 10000 # 最大连接数
maxConnections: 100 # 最大连接数
terminal:
phone: 868120322495257
timeout:

View File

@ -26,6 +26,12 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
from jtt808_location
</sql>
<sql id="selectJtt808LocationVoSimple">
select location_id, phone_number, alarm_flag, status_flag, latitude, longitude,
altitude, speed, direction, location_time, client_ip, receive_time
from jtt808_location
</sql>
<insert id="insertLocation" parameterType="com.bonus.sgzb.material.domain.Jtt808Location" useGeneratedKeys="true" keyProperty="locationId">
insert into jtt808_location
<trim prefix="(" suffix=")" suffixOverrides=",">
@ -72,7 +78,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</insert>
<select id="selectLocationList" parameterType="com.bonus.sgzb.material.domain.Jtt808Location" resultMap="Jtt808LocationResult">
<include refid="selectJtt808LocationVo"/>
<include refid="selectJtt808LocationVoSimple"/>
<where>
<if test="phoneNumber != null and phoneNumber != ''">
and phone_number = #{phoneNumber}
@ -95,7 +101,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectLocationTrack" resultMap="Jtt808LocationResult">
<include refid="selectJtt808LocationVo"/>
<include refid="selectJtt808LocationVoSimple"/>
where phone_number = #{phoneNumber}
<if test="startTime != null">
and location_time &gt;= #{startTime}
@ -107,7 +113,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select>
<select id="selectLocationsByArea" resultMap="Jtt808LocationResult">
<include refid="selectJtt808LocationVo"/>
<include refid="selectJtt808LocationVoSimple"/>
where phone_number = #{phoneNumber}
and latitude between #{minLat} and #{maxLat}
and longitude between #{minLng} and #{maxLng}
@ -120,8 +126,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
order by location_time desc
</select>
<select id="selectTodayMileage" parameterType="String" resultType="Double">
select COALESCE(
(select CAST(JSON_UNQUOTE(JSON_EXTRACT(additional_info, '$.1')) AS DECIMAL(10,2))
@ -144,32 +148,41 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<!-- 查询指定日期的位置数据(按时间排序) -->
<select id="selectLocationsByDate" resultMap="Jtt808LocationResult">
<include refid="selectJtt808LocationVo"/>
where phone_number = #{phoneNumber}
and DATE(location_time) = #{targetDate}
<include refid="selectJtt808LocationVoSimple"/>
where phone_number = #{phoneNumber} and DATE(location_time) = #{targetDate}
order by location_time ASC
</select>
<!-- 查询指定月份的位置数据统计 -->
<!-- 查询指定月份的位置数据统计 - 优化版本,避免使用函数索引 -->
<select id="selectMonthlyLocationStats" resultType="java.util.HashMap">
SELECT
DATE(location_time) as date,
SELECT
DATE_FORMAT(location_time, '%Y-%m-%d') as date,
COUNT(*) as location_count,
MIN(location_time) as first_location_time,
MAX(location_time) as last_location_time,
MAX(speed) as max_speed,
AVG(speed) as avg_speed
FROM jtt808_location
FROM jtt808_location
WHERE phone_number = #{phoneNumber}
AND YEAR(location_time) = #{year}
AND MONTH(location_time) = #{month}
GROUP BY DATE(location_time)
ORDER BY DATE(location_time)
AND location_time &gt;= CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01 00:00:00')
AND location_time &lt; DATE_ADD(CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01'), INTERVAL 1 MONTH)
GROUP BY DATE_FORMAT(location_time, '%Y-%m-%d')
ORDER BY DATE_FORMAT(location_time, '%Y-%m-%d')
</select>
<!-- 查询指定月份的所有位置数据(批量计算里程) -->
<select id="selectMonthlyLocations" resultMap="Jtt808LocationResult">
SELECT location_id, phone_number, latitude, longitude, speed, location_time
FROM jtt808_location
WHERE phone_number = #{phoneNumber}
AND location_time &gt;= CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01 00:00:00')
AND location_time &lt; DATE_ADD(CONCAT(#{year}, '-', LPAD(#{month}, 2, '0'), '-01'), INTERVAL 1 MONTH)
ORDER BY location_time
</select>
<!-- 查询指定日期范围的位置数据 -->
<select id="selectLocationsByDateRange" resultMap="Jtt808LocationResult">
<include refid="selectJtt808LocationVo"/>
<include refid="selectJtt808LocationVoSimple"/>
where phone_number = #{phoneNumber}
and DATE(location_time) &gt;= #{startDate}
and DATE(location_time) &lt;= #{endDate}

View File

@ -19,7 +19,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</resultMap>
<sql id="selectJtt808MessageLogVo">
select log_id, phone_number, message_id, message_type, serial_number, message_direction,
select log_id, phone_number, message_id, serial_number, message_direction,
message_content, process_result, error_message, client_ip, process_time
from jtt808_message_log
</sql>