增加了过滤非注册车辆的功能

This commit is contained in:
Tian jianyong 2025-08-01 14:58:38 +08:00
parent 264e8c470c
commit f8d21c1761
13 changed files with 766 additions and 121 deletions

View File

@ -0,0 +1,142 @@
# 机场车辆位置过滤功能设计文档
## 概述
本设计文档描述了如何在现有的机场车辆位置数据处理流程中添加过滤功能,通过系统配置控制是否转发未注册车辆的位置信息。
## 架构
### 整体架构
```
机场API -> DataCollectorService -> 车辆过滤器 -> 缓存/前端转发
sys_vehicle_info表查询
系统配置(ISysConfigService)
```
### 核心组件
1. **VehicleLocationFilter**: 新增的车辆位置过滤器
2. **DataCollectorService**: 修改现有的数据采集服务,集成过滤逻辑
3. **ISysConfigService**: 利用现有的系统配置服务
4. **ISysVehicleInfoService**: 利用现有的车辆信息服务
## 组件和接口
### 1. VehicleLocationFilter (新增)
```java
@Component
public class VehicleLocationFilter {
private static final String CONFIG_KEY = "unmanaged.vehicle.filter.enabled";
@Autowired
private ISysConfigService configService;
@Autowired
private ISysVehicleInfoService vehicleInfoService;
/**
* 过滤机场车辆位置数据
* @param vehicles 原始车辆列表
* @return 过滤后的车辆列表
*/
public List<AirportVehicle> filterVehicles(List<AirportVehicle> vehicles);
/**
* 检查过滤功能是否启用
* @return true表示启用过滤
*/
private boolean isFilterEnabled();
/**
* 批量检查车辆是否在数据库中存在
* @param plateNumbers 车牌号列表
* @return 存在的车牌号集合
*/
private Set<String> getExistingVehiclePlates(List<String> plateNumbers);
}
```
### 2. DataCollectorService (修改)
在现有的 `collectVehicleData()` 方法中集成过滤逻辑:
```java
@Scheduled(fixedRateString = "${data.collector.interval}")
@Async
public void collectVehicleData() {
// ... 现有代码 ...
List<AirportVehicle> vehicles = dataCollectorDao.collectVehicleData(airportVehicleEndpoint, airportBaseUrl);
// 新增:应用过滤器
List<AirportVehicle> filteredVehicles = vehicleLocationFilter.filterVehicles(vehicles);
// 继续处理过滤后的车辆数据
for (AirportVehicle vehicle : filteredVehicles) {
// ... 现有处理逻辑 ...
}
}
```
### 3. 系统配置
`sys_config` 表中添加配置项:
- config_key: `unmanaged.vehicle.filter.enabled`
- config_value: `false` (默认值,不过滤)
- config_name: `未管理车辆位置过滤开关`
- config_type: `Y` (系统内置)
## 数据模型
### 配置项数据结构
```sql
INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark)
VALUES ('未管理车辆位置过滤开关', 'unmanaged.vehicle.filter.enabled', 'false', 'Y', '控制是否过滤未在sys_vehicle_info表中注册的车辆位置信息');
```
### 车辆信息查询
利用现有的 `sys_vehicle_info` 表结构,通过 `license_plate` 字段进行匹配。
## 错误处理
### 异常处理策略
1. **配置读取失败**: 默认不过滤,记录警告日志
2. **数据库查询失败**: 默认不过滤,记录错误日志
3. **车辆数据异常**: 跳过异常数据,继续处理其他车辆
### 日志记录
- DEBUG级别: 记录过滤统计信息
- WARN级别: 记录配置读取失败
- ERROR级别: 记录数据库查询异常
## 测试策略
### 单元测试
1. **VehicleLocationFilter测试**
- 测试过滤功能启用/禁用
- 测试车辆存在性检查
- 测试异常处理
2. **DataCollectorService集成测试**
- 测试过滤器集成
- 测试数据流完整性
### 集成测试
1. 测试配置修改后的实时生效
2. 测试大量车辆数据的过滤性能
3. 测试数据库连接异常时的降级处理
## 性能考虑
### 优化策略
1. **批量查询**: 一次查询所有车牌号避免N+1问题
2. **配置缓存**: 利用现有的系统配置缓存机制
3. **快速失败**: 配置禁用时直接跳过所有过滤逻辑
### 性能指标
- 单批次车辆过滤处理时间 < 50ms (100辆车以内)
- 数据库查询次数: 每批次最多1次
- 内存占用: 忽略不计 (仅临时存储车牌号列表)

View File

@ -0,0 +1,35 @@
# 机场车辆位置过滤功能需求文档
## 介绍
为机场车辆位置 API 添加一个配置开关控制是否将不在数据库车辆信息表sys_vehicle_info中的车辆位置信息转发给前端。
## 需求
### 需求 1
**用户故事:** 作为系统管理员,我希望能够配置是否过滤未注册车辆的位置信息,以便控制前端显示的车辆数据量。
#### 验收标准
1. WHEN 系统配置中存在"unmanaged.vehicle.filter.enabled"配置项 THEN 系统 SHALL 根据该配置决定是否过滤车辆
2. WHEN 配置为"true" THEN 系统 SHALL 只转发在 sys_vehicle_info 表中存在的车辆位置信息
3. WHEN 配置为"false" THEN 系统 SHALL 转发所有接收到的机场车辆位置信息
### 需求 2
**用户故事:** 作为系统用户,我希望过滤功能不影响系统的实时性能。
#### 验收标准
1. WHEN 过滤功能启用 THEN 系统 SHALL 使用车牌号批量查询车辆信息以提高效率
2. WHEN 过滤功能禁用 THEN 系统 SHALL 跳过过滤逻辑,直接处理数据
### 需求 3
**用户故事:** 作为系统管理员,我希望能够通过现有的系统配置 API 查询和修改过滤配置。
#### 验收标准
1. WHEN 调用系统配置查询 API THEN 系统 SHALL 返回"unmanaged.vehicle.filter.enabled"配置项的值
2. WHEN 调用系统配置修改 API THEN 系统 SHALL 允许修改该配置项的值

View File

@ -0,0 +1,24 @@
# 实现计划
- [x] 1. 创建车辆位置过滤器组件
- 创建 VehicleLocationFilter 类,实现车辆过滤逻辑
- 添加配置读取和车辆存在性检查方法
- 实现批量车牌号查询优化
- _需求: 1.1, 1.2, 2.1_
- [x] 2. 修改数据采集服务集成过滤器
- 在 DataCollectorService 中注入 VehicleLocationFilter
- 修改 collectVehicleData 方法,在处理前应用过滤器
- 添加过滤统计日志记录
- _需求: 1.2, 1.3, 2.2_
- [x] 3. 添加系统配置项
- 在数据库中插入未管理车辆位置过滤开关配置项
- 验证配置项可以通过现有的系统配置 API 查询和修改
- _需求: 3.1, 3.2_
- [x] 4. 编写单元测试
- 为 VehicleLocationFilter 编写单元测试
- 测试过滤功能启用/禁用场景
- 测试异常处理和降级逻辑
- _需求: 1.1, 1.2, 1.3_

View File

@ -32,6 +32,7 @@ INSERT INTO public.sys_config VALUES (2, '用户管理-账号初始密码', 'sys
INSERT INTO public.sys_config VALUES (3, '主框架页-侧边栏主题', 'sys.index.sideTheme', 'theme-dark', 'Y', 'admin', '2021-05-26 18:56:31', 'admin', NULL, '深色主题theme-dark浅色主题theme-light');
INSERT INTO public.sys_config VALUES (4, '账号自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'admin', '2025-06-30 11:46:01.666022', 'admin', NULL, '是否开启验证码功能true开启false关闭');
INSERT INTO public.sys_config VALUES (5, '账号自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'admin', '2025-06-30 11:46:01.666242', 'admin', NULL, '是否开启注册用户功能true开启false关闭');
INSERT INTO public.sys_config VALUES (6, '未管理车辆位置过滤开关', 'unmanaged.vehicle.filter.enabled', 'false', 'Y', 'admin', '2025-08-01 12:30:00', 'admin', NULL, '控制是否过滤未在sys_vehicle_info表中注册的车辆位置信息。true=启用过滤false=禁用过滤(默认)');
ALTER TABLE public.sys_config ENABLE TRIGGER ALL;

View File

@ -0,0 +1,129 @@
package com.qaup.collision.datacollector.filter;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.qaup.collision.common.model.AirportVehicle;
import com.qaup.system.service.ISysConfigService;
import com.qaup.system.service.ISysVehicleInfoService;
/**
* 车辆位置过滤器
* 根据系统配置决定是否过滤未在数据库中注册的车辆位置信息
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-31
*/
@Component
public class VehicleLocationFilter {
private static final Logger log = LoggerFactory.getLogger(VehicleLocationFilter.class);
private static final String CONFIG_KEY = "unmanaged.vehicle.filter.enabled";
@Autowired
private ISysConfigService configService;
@Autowired
private ISysVehicleInfoService vehicleInfoService;
/**
* 过滤机场车辆位置数据
* @param vehicles 原始车辆列表
* @return 过滤后的车辆列表
*/
public List<AirportVehicle> filterVehicles(List<AirportVehicle> vehicles) {
if (CollectionUtils.isEmpty(vehicles)) {
return vehicles;
}
// 检查过滤功能是否启用
if (!isFilterEnabled()) {
log.debug("未管理车辆位置过滤功能已禁用,跳过过滤逻辑,直接返回所有车辆数据");
return vehicles;
}
try {
// 提取所有车牌号
List<String> plateNumbers = vehicles.stream()
.map(AirportVehicle::getPlateNumber)
.filter(StringUtils::hasText)
.distinct()
.collect(Collectors.toList());
if (plateNumbers.isEmpty()) {
log.warn("所有车辆都没有车牌号信息,无法进行过滤");
return Collections.emptyList();
}
// 批量查询存在的车牌号
Set<String> existingPlates = getExistingVehiclePlates(plateNumbers);
// 过滤车辆
List<AirportVehicle> filteredVehicles = vehicles.stream()
.filter(vehicle -> {
String plateNumber = vehicle.getPlateNumber();
if (!StringUtils.hasText(plateNumber)) {
return false; // 没有车牌号的车辆被过滤掉
}
return existingPlates.contains(plateNumber);
})
.collect(Collectors.toList());
// 记录过滤统计信息
int originalCount = vehicles.size();
int filteredCount = filteredVehicles.size();
int removedCount = originalCount - filteredCount;
log.debug("车辆位置过滤完成 - 原始数量: {}, 过滤后数量: {}, 被过滤数量: {}",
originalCount, filteredCount, removedCount);
return filteredVehicles;
} catch (Exception e) {
log.error("车辆位置过滤过程中发生异常,返回原始数据", e);
return vehicles; // 异常时返回原始数据确保系统正常运行
}
}
/**
* 检查过滤功能是否启用
* @return true表示启用过滤
*/
private boolean isFilterEnabled() {
try {
String configValue = configService.selectConfigByKey(CONFIG_KEY);
return "true".equalsIgnoreCase(configValue);
} catch (Exception e) {
log.warn("读取未管理车辆位置过滤配置失败,默认不过滤: {}", e.getMessage());
return false; // 配置读取失败时默认不过滤
}
}
/**
* 批量检查车辆是否在数据库中存在
* @param plateNumbers 车牌号列表
* @return 存在的车牌号集合
*/
private Set<String> getExistingVehiclePlates(List<String> plateNumbers) {
try {
// 使用批量查询方法提高查询效率
List<String> existingPlates = vehicleInfoService.selectExistingLicensePlates(plateNumbers);
return existingPlates.stream().collect(Collectors.toSet());
} catch (Exception e) {
log.error("批量查询车辆信息失败,抛出异常以触发上层降级处理", e);
throw new RuntimeException("车辆信息查询失败", e); // 抛出异常让上层catch处理
}
}
}

View File

@ -10,6 +10,7 @@ import com.qaup.collision.common.service.VehicleLocationService;
import com.qaup.collision.datacollector.dao.DataCollectorDao;
import com.qaup.collision.datacollector.dto.AircraftRouteDTO;
import com.qaup.collision.datacollector.dto.AircraftStatusDTO;
import com.qaup.collision.datacollector.filter.VehicleLocationFilter;
import com.qaup.collision.websocket.event.AircraftRouteUpdateEvent;
import jakarta.annotation.PostConstruct;
@ -79,6 +80,9 @@ public class DataCollectorService {
@Autowired
private RoutePersistenceService routePersistenceService; // 注入路由持久化服务
@Autowired
private VehicleLocationFilter vehicleLocationFilter; // 注入车辆位置过滤器
private final GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 4326); // SRID 4326 for WGS84
@ -463,13 +467,26 @@ public class DataCollectorService {
return;
}
log.info("采集到 {} 条机场车辆数据,用于实时处理", vehicles.size());
int originalCount = vehicles.size();
log.info("采集到 {} 条机场车辆数据,开始过滤处理", originalCount);
// List<MovingObject> activeMovingObjects = new ArrayList<>(); // 移除此行
// 应用车辆位置过滤器
List<AirportVehicle> filteredVehicles = vehicleLocationFilter.filterVehicles(vehicles);
int filteredCount = filteredVehicles.size();
if (filteredCount != originalCount) {
log.info("车辆过滤完成 - 原始数量: {}, 过滤后数量: {}, 被过滤数量: {}",
originalCount, filteredCount, originalCount - filteredCount);
}
if (filteredVehicles.isEmpty()) {
log.debug("过滤后没有车辆数据需要处理");
return;
}
// 机场车辆数据仅用于实时处理不存储到数据库
// 数据处理完成后发布WebSocket事件进行实时推送
for (AirportVehicle vehicle : vehicles) {
for (AirportVehicle vehicle : filteredVehicles) {
try {
if (vehicle.getCurrentPosition() == null) {
log.warn("机场车辆 {} 位置信息缺失,跳过处理。", vehicle.getObjectId());
@ -523,7 +540,7 @@ public class DataCollectorService {
// }
// }
log.info("机场车辆数据处理和事件发布完成,处理数量: {}", vehicles.size());
log.info("机场车辆数据处理和事件发布完成,处理数量: {}", filteredVehicles.size());
} catch (Exception e) {
log.error("采集机场车辆数据异常", e);

View File

@ -0,0 +1,233 @@
package com.qaup.collision.datacollector.filter;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.qaup.collision.common.model.AirportVehicle;
import com.qaup.system.service.ISysConfigService;
import com.qaup.system.service.ISysVehicleInfoService;
/**
* VehicleLocationFilter 单元测试
*
* @author AI Assistant
* @version 1.0
* @since 2025-01-31
*/
@ExtendWith(MockitoExtension.class)
class VehicleLocationFilterTest {
@Mock
private ISysConfigService configService;
@Mock
private ISysVehicleInfoService vehicleInfoService;
@InjectMocks
private VehicleLocationFilter vehicleLocationFilter;
private List<AirportVehicle> testVehicles;
@BeforeEach
void setUp() {
// 准备测试数据
AirportVehicle vehicle1 = AirportVehicle.builder()
.objectId("京A12345")
.objectName("京A12345")
.build();
AirportVehicle vehicle2 = AirportVehicle.builder()
.objectId("京B67890")
.objectName("京B67890")
.build();
AirportVehicle vehicle3 = AirportVehicle.builder()
.objectId("京C11111")
.objectName("京C11111")
.build();
// 没有车牌号的车辆
AirportVehicle vehicleNoPlate = AirportVehicle.builder()
.objectId("")
.objectName("")
.build();
testVehicles = Arrays.asList(vehicle1, vehicle2, vehicle3, vehicleNoPlate);
}
@Test
void testFilterVehicles_FilterDisabled_ShouldReturnAllVehicles() {
// 配置过滤功能禁用
when(configService.selectConfigByKey("unmanaged.vehicle.filter.enabled"))
.thenReturn("false");
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(testVehicles);
// 验证结果
assertEquals(4, result.size());
assertEquals(testVehicles, result);
// 验证没有调用车辆信息查询
verify(vehicleInfoService, never()).selectExistingLicensePlates(any());
}
@Test
void testFilterVehicles_FilterEnabled_ShouldFilterUnregisteredVehicles() {
// 配置过滤功能启用
when(configService.selectConfigByKey("unmanaged.vehicle.filter.enabled"))
.thenReturn("true");
// 模拟只有京A12345和京C11111在数据库中存在
when(vehicleInfoService.selectExistingLicensePlates(anyList()))
.thenReturn(Arrays.asList("京A12345", "京C11111"));
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(testVehicles);
// 验证结果应该只返回2辆注册的车辆
assertEquals(2, result.size());
assertTrue(result.stream().anyMatch(v -> "京A12345".equals(v.getPlateNumber())));
assertTrue(result.stream().anyMatch(v -> "京C11111".equals(v.getPlateNumber())));
assertFalse(result.stream().anyMatch(v -> "京B67890".equals(v.getPlateNumber())));
// 验证调用了车辆信息查询
verify(vehicleInfoService).selectExistingLicensePlates(Arrays.asList("京A12345", "京B67890", "京C11111"));
}
@Test
void testFilterVehicles_FilterEnabled_NoRegisteredVehicles_ShouldReturnEmpty() {
// 配置过滤功能启用
when(configService.selectConfigByKey("unmanaged.vehicle.filter.enabled"))
.thenReturn("true");
// 模拟没有车辆在数据库中存在
when(vehicleInfoService.selectExistingLicensePlates(anyList()))
.thenReturn(Collections.emptyList());
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(testVehicles);
// 验证结果应该返回空列表
assertEquals(0, result.size());
}
@Test
void testFilterVehicles_EmptyInput_ShouldReturnEmpty() {
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(Collections.emptyList());
// 验证结果
assertEquals(0, result.size());
// 验证没有调用任何服务
verify(configService, never()).selectConfigByKey(any());
verify(vehicleInfoService, never()).selectExistingLicensePlates(any());
}
@Test
void testFilterVehicles_NullInput_ShouldReturnNull() {
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(null);
// 验证结果
assertNull(result);
// 验证没有调用任何服务
verify(configService, never()).selectConfigByKey(any());
verify(vehicleInfoService, never()).selectExistingLicensePlates(any());
}
@Test
void testFilterVehicles_ConfigServiceException_ShouldReturnAllVehicles() {
// 配置服务抛出异常
when(configService.selectConfigByKey("unmanaged.vehicle.filter.enabled"))
.thenThrow(new RuntimeException("配置服务异常"));
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(testVehicles);
// 验证结果异常时应该返回所有车辆降级处理
assertEquals(4, result.size());
assertEquals(testVehicles, result);
// 验证没有调用车辆信息查询
verify(vehicleInfoService, never()).selectExistingLicensePlates(any());
}
@Test
void testFilterVehicles_VehicleInfoServiceException_ShouldReturnAllVehicles() {
// 配置过滤功能启用
when(configService.selectConfigByKey("unmanaged.vehicle.filter.enabled"))
.thenReturn("true");
// 车辆信息服务抛出异常
when(vehicleInfoService.selectExistingLicensePlates(anyList()))
.thenThrow(new RuntimeException("数据库查询异常"));
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(testVehicles);
// 验证结果异常时应该返回所有车辆降级处理
assertEquals(4, result.size());
assertEquals(testVehicles, result);
}
@Test
void testFilterVehicles_AllVehiclesWithoutPlateNumber_ShouldReturnEmpty() {
// 准备没有车牌号的车辆数据
AirportVehicle vehicleNoPlate1 = AirportVehicle.builder()
.objectId("")
.objectName("")
.build();
AirportVehicle vehicleNoPlate2 = AirportVehicle.builder()
.objectId(null)
.objectName(null)
.build();
List<AirportVehicle> vehiclesWithoutPlates = Arrays.asList(vehicleNoPlate1, vehicleNoPlate2);
// 配置过滤功能启用
when(configService.selectConfigByKey("unmanaged.vehicle.filter.enabled"))
.thenReturn("true");
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(vehiclesWithoutPlates);
// 验证结果没有车牌号的车辆应该被过滤掉
assertEquals(0, result.size());
// 验证没有调用车辆信息查询因为没有有效的车牌号
verify(vehicleInfoService, never()).selectExistingLicensePlates(any());
}
@Test
void testFilterVehicles_ConfigValueCaseInsensitive() {
// 测试配置值大小写不敏感
when(configService.selectConfigByKey("unmanaged.vehicle.filter.enabled"))
.thenReturn("TRUE"); // 大写
when(vehicleInfoService.selectExistingLicensePlates(anyList()))
.thenReturn(Arrays.asList("京A12345"));
// 执行过滤
List<AirportVehicle> result = vehicleLocationFilter.filterVehicles(testVehicles);
// 验证结果应该启用过滤
assertEquals(1, result.size());
assertEquals("京A12345", result.get(0).getPlateNumber());
}
}

View File

@ -58,4 +58,12 @@ public interface SysVehicleInfoMapper
* @return 结果
*/
public int deleteSysVehicleInfoByVehicleIds(Long[] vehicleIds);
/**
* 批量查询车牌号是否存在
*
* @param plateNumbers 车牌号列表
* @return 存在的车牌号列表
*/
public List<String> selectExistingLicensePlates(List<String> plateNumbers);
}

View File

@ -58,4 +58,12 @@ public interface ISysVehicleInfoService
* @return 结果
*/
public int deleteSysVehicleInfoByVehicleId(Long vehicleId);
/**
* 批量查询车牌号是否存在
*
* @param plateNumbers 车牌号列表
* @return 存在的车牌号列表
*/
public List<String> selectExistingLicensePlates(List<String> plateNumbers);
}

View File

@ -1,5 +1,6 @@
package com.qaup.system.service.impl;
import java.util.Collections;
import java.util.List;
import com.qaup.common.utils.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
@ -93,4 +94,19 @@ public class SysVehicleInfoServiceImpl implements ISysVehicleInfoService
{
return sysVehicleInfoMapper.deleteSysVehicleInfoByVehicleId(vehicleId);
}
/**
* 批量查询车牌号是否存在
*
* @param plateNumbers 车牌号列表
* @return 存在的车牌号列表
*/
@Override
public List<String> selectExistingLicensePlates(List<String> plateNumbers)
{
if (plateNumbers == null || plateNumbers.isEmpty()) {
return Collections.emptyList();
}
return sysVehicleInfoMapper.selectExistingLicensePlates(plateNumbers);
}
}

View File

@ -31,8 +31,7 @@ import com.qaup.system.service.ISysVehicleLocationService;
* @date 2025-01-16
*/
@Service
public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
{
public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService {
@Autowired
private SysVehicleLocationMapper sysVehicleLocationMapper;
@ -43,8 +42,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* @return 车辆运动信息
*/
@Override
public List<SysVehicleLocation> selectSysVehicleLocationList(SysVehicleLocation sysVehicleLocation)
{
public List<SysVehicleLocation> selectSysVehicleLocationList(SysVehicleLocation sysVehicleLocation) {
return sysVehicleLocationMapper.selectSysVehicleLocationList(sysVehicleLocation);
}
@ -55,8 +53,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* @return 车辆运动信息列表
*/
@Override
public List<SysVehicleLocation> selectSysVehicleLocationByVehicleId(Long vehicleId)
{
public List<SysVehicleLocation> selectSysVehicleLocationByVehicleId(Long vehicleId) {
return sysVehicleLocationMapper.selectSysVehicleLocationByVehicleId(vehicleId);
}
@ -67,8 +64,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* @return 车辆运动信息列表
*/
@Override
public List<SysVehicleLocation> selectSysVehicleLocationByLicensePlate(String licensePlate)
{
public List<SysVehicleLocation> selectSysVehicleLocationByLicensePlate(String licensePlate) {
return sysVehicleLocationMapper.selectSysVehicleLocationByLicensePlate(licensePlate);
}
@ -79,8 +75,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* @return 车辆运动信息
*/
@Override
public SysVehicleLocation selectLatestSysVehicleLocationByVehicleId(Long vehicleId)
{
public SysVehicleLocation selectLatestSysVehicleLocationByVehicleId(Long vehicleId) {
return sysVehicleLocationMapper.selectLatestSysVehicleLocationByVehicleId(vehicleId);
}
@ -91,8 +86,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* @return 车辆运动信息
*/
@Override
public SysVehicleLocation selectLatestSysVehicleLocationByLicensePlate(String licensePlate)
{
public SysVehicleLocation selectLatestSysVehicleLocationByLicensePlate(String licensePlate) {
return sysVehicleLocationMapper.selectLatestSysVehicleLocationByLicensePlate(licensePlate);
}
@ -101,7 +95,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
*/
@Override
public VehicleTrajectoryVO getVehicleTrajectory(Long vehicleId, Date startTime, Date endTime,
Boolean simplified, Integer maxPoints) {
Boolean simplified, Integer maxPoints) {
// 参数校验
if (vehicleId == null || startTime == null || endTime == null) {
throw new IllegalArgumentException("车辆ID、开始时间和结束时间不能为空");
@ -113,7 +107,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
// 查询轨迹数据
List<SysVehicleLocation> locations = sysVehicleLocationMapper
.selectVehicleTrajectory(vehicleId, startTime, endTime, simplified, maxPoints);
.selectVehicleTrajectory(vehicleId, startTime, endTime, simplified, maxPoints);
if (CollectionUtils.isEmpty(locations)) {
return createEmptyTrajectory(vehicleId, startTime, endTime);
@ -130,18 +124,18 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
// 构建返回结果
return VehicleTrajectoryVO.builder()
.vehicleId(vehicleId)
.licensePlate(firstLocation.getLicensePlate())
.vehicleType(firstLocation.getVehicleType())
.brand(firstLocation.getBrand())
.owningUnit(firstLocation.getOwningUnit())
.startTime(startTime)
.endTime(endTime)
.statistics(statistics)
.points(points)
.simplified(simplified)
.dataQuality(calculateOverallDataQuality(locations))
.build();
.vehicleId(vehicleId)
.licensePlate(firstLocation.getLicensePlate())
.vehicleType(firstLocation.getVehicleType())
.brand(firstLocation.getBrand())
.owningUnit(firstLocation.getOwningUnit())
.startTime(startTime)
.endTime(endTime)
.statistics(statistics)
.points(points)
.simplified(simplified)
.dataQuality(calculateOverallDataQuality(locations))
.build();
}
/**
@ -151,7 +145,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
public List<VehicleTrajectoryVO> getBatchVehicleTrajectory(TrajectoryQueryRequest request) {
// 参数校验
if (request == null || CollectionUtils.isEmpty(request.getVehicleIds())
|| request.getStartTime() == null || request.getEndTime() == null) {
|| request.getStartTime() == null || request.getEndTime() == null) {
throw new IllegalArgumentException("请求参数不能为空");
}
@ -159,12 +153,12 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
// 批量查询轨迹数据
List<SysVehicleLocation> allLocations = sysVehicleLocationMapper
.selectBatchVehicleTrajectory(request.getVehicleIds(), request.getStartTime(),
request.getEndTime(), request.getSimplified(), request.getMaxPoints());
.selectBatchVehicleTrajectory(request.getVehicleIds(), request.getStartTime(),
request.getEndTime(), request.getSimplified(), request.getMaxPoints());
// 按车辆ID分组
Map<Long, List<SysVehicleLocation>> locationsByVehicle = allLocations.stream()
.collect(Collectors.groupingBy(SysVehicleLocation::getVehicleId));
.collect(Collectors.groupingBy(SysVehicleLocation::getVehicleId));
// 为每个车辆构建轨迹数据
for (Long vehicleId : request.getVehicleIds()) {
@ -176,7 +170,8 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
}
VehicleTrajectoryVO trajectory = buildTrajectoryFromLocations(vehicleId, vehicleLocations,
request.getStartTime(), request.getEndTime(), request.getSimplified(), request.getIncludeStatistics());
request.getStartTime(), request.getEndTime(), request.getSimplified(),
request.getIncludeStatistics());
result.add(trajectory);
}
@ -188,7 +183,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
*/
@Override
public List<VehicleLocationPlaybackVO> getTrajectoryPlaybackData(Long vehicleId, Date startTime,
Date endTime, Integer intervalSeconds) {
Date endTime, Integer intervalSeconds) {
// 参数校验
if (vehicleId == null || startTime == null || endTime == null || intervalSeconds == null) {
throw new IllegalArgumentException("参数不能为空");
@ -200,7 +195,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
// 查询回放数据
List<SysVehicleLocation> locations = sysVehicleLocationMapper
.selectTrajectoryPlaybackData(vehicleId, startTime, endTime, intervalSeconds);
.selectTrajectoryPlaybackData(vehicleId, startTime, endTime, intervalSeconds);
if (CollectionUtils.isEmpty(locations)) {
return new ArrayList<>();
@ -215,7 +210,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
*/
@Override
public VehicleTrajectoryVO getVehicleTrajectoryInArea(Long vehicleId, Date startTime, Date endTime,
String areaWkt) {
String areaWkt) {
// 参数校验
if (vehicleId == null || startTime == null || endTime == null || !StringUtils.hasText(areaWkt)) {
throw new IllegalArgumentException("参数不能为空");
@ -223,7 +218,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
// 查询区域内轨迹数据
List<SysVehicleLocation> locations = sysVehicleLocationMapper
.selectVehicleTrajectoryInArea(vehicleId, startTime, endTime, areaWkt);
.selectVehicleTrajectoryInArea(vehicleId, startTime, endTime, areaWkt);
if (CollectionUtils.isEmpty(locations)) {
return createEmptyTrajectory(vehicleId, startTime, endTime);
@ -245,7 +240,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
// 查询统计信息
TrajectoryStatistics statistics = sysVehicleLocationMapper
.selectTrajectoryStatistics(vehicleId, startTime, endTime);
.selectTrajectoryStatistics(vehicleId, startTime, endTime);
if (statistics == null) {
return createEmptyStatistics(startTime, endTime);
@ -266,13 +261,13 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
*/
private VehicleTrajectoryVO createEmptyTrajectory(Long vehicleId, Date startTime, Date endTime) {
return VehicleTrajectoryVO.builder()
.vehicleId(vehicleId)
.startTime(startTime)
.endTime(endTime)
.statistics(createEmptyStatistics(startTime, endTime))
.points(new ArrayList<>())
.simplified(false)
.build();
.vehicleId(vehicleId)
.startTime(startTime)
.endTime(endTime)
.statistics(createEmptyStatistics(startTime, endTime))
.points(new ArrayList<>())
.simplified(false)
.build();
}
/**
@ -280,21 +275,21 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
*/
private TrajectoryStatistics createEmptyStatistics(Date startTime, Date endTime) {
return TrajectoryStatistics.builder()
.pointCount(0)
.firstPointTime(startTime)
.lastPointTime(endTime)
.averageSpeed(BigDecimal.ZERO)
.maxSpeed(BigDecimal.ZERO)
.minSpeed(BigDecimal.ZERO)
.durationSeconds(0L)
.durationFormatted("0分钟")
.totalDistance(BigDecimal.ZERO)
.stopCount(0)
.totalStopDuration(0L)
.maxAltitude(BigDecimal.ZERO)
.minAltitude(BigDecimal.ZERO)
.averageAltitude(BigDecimal.ZERO)
.build();
.pointCount(0)
.firstPointTime(startTime)
.lastPointTime(endTime)
.averageSpeed(BigDecimal.ZERO)
.maxSpeed(BigDecimal.ZERO)
.minSpeed(BigDecimal.ZERO)
.durationSeconds(0L)
.durationFormatted("0分钟")
.totalDistance(BigDecimal.ZERO)
.stopCount(0)
.totalStopDuration(0L)
.maxAltitude(BigDecimal.ZERO)
.minAltitude(BigDecimal.ZERO)
.averageAltitude(BigDecimal.ZERO)
.build();
}
/**
@ -310,15 +305,15 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
for (SysVehicleLocation location : locations) {
TrajectoryPoint.TrajectoryPointBuilder pointBuilder = TrajectoryPoint.builder()
.longitude(location.getLongitude())
.latitude(location.getLatitude())
.altitude(location.getAltitude())
.speed(location.getSpeed())
.heading(location.getHeading())
.timestamp(location.getTimestamp())
.dataQuality(location.getDataQuality())
.location(location.getLocation())
.isStopPoint(isStopPoint(location));
.longitude(location.getLongitude())
.latitude(location.getLatitude())
.altitude(location.getAltitude())
.speed(location.getSpeed())
.heading(location.getHeading())
.timestamp(location.getTimestamp())
.dataQuality(location.getDataQuality())
.location(location.getLocation())
.isStopPoint(isStopPoint(location));
// 计算与前一点的距离和时间间隔
if (previousLocation != null) {
@ -337,7 +332,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* 转换为回放数据
*/
private List<VehicleLocationPlaybackVO> convertToPlaybackData(List<SysVehicleLocation> locations,
Date startTime, Date endTime) {
Date startTime, Date endTime) {
if (CollectionUtils.isEmpty(locations)) {
return new ArrayList<>();
}
@ -360,35 +355,36 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
// 计算进度百分比
long currentTime = (location.getTimestamp().getTime() - startTime.getTime()) / 1000;
BigDecimal progressPercentage = totalDuration > 0 ?
BigDecimal.valueOf(currentTime * 100.0 / totalDuration).setScale(2, RoundingMode.HALF_UP) :
BigDecimal.ZERO;
BigDecimal progressPercentage = totalDuration > 0
? BigDecimal.valueOf(currentTime * 100.0 / totalDuration).setScale(2, RoundingMode.HALF_UP)
: BigDecimal.ZERO;
// 计算与上一点的时间间隔
Long intervalMs = previousLocation != null ?
location.getTimestamp().getTime() - previousLocation.getTimestamp().getTime() : 0L;
Long intervalMs = previousLocation != null
? location.getTimestamp().getTime() - previousLocation.getTimestamp().getTime()
: 0L;
VehicleLocationPlaybackVO playbackVO = VehicleLocationPlaybackVO.builder()
.vehicleId(location.getVehicleId())
.licensePlate(location.getLicensePlate())
.vehicleType(location.getVehicleType())
.sequenceNumber(sequenceNumber.getAndIncrement())
.longitude(location.getLongitude())
.latitude(location.getLatitude())
.altitude(location.getAltitude())
.speed(location.getSpeed())
.heading(location.getHeading())
.timestamp(location.getTimestamp())
.dataQuality(location.getDataQuality())
.intervalMs(intervalMs)
.cumulativeDistance(cumulativeDistance)
.cumulativeTime(cumulativeTime)
.progressPercentage(progressPercentage)
.isKeyFrame(isKeyFrame(location, previousLocation))
.keyFrameType(getKeyFrameType(location, previousLocation, sequenceNumber.get() == 2,
sequenceNumber.get() == locations.size() + 1))
.suggestedDelay(calculateSuggestedDelay(intervalMs))
.build();
.vehicleId(location.getVehicleId())
.licensePlate(location.getLicensePlate())
.vehicleType(location.getVehicleType())
.sequenceNumber(sequenceNumber.getAndIncrement())
.longitude(location.getLongitude())
.latitude(location.getLatitude())
.altitude(location.getAltitude())
.speed(location.getSpeed())
.heading(location.getHeading())
.timestamp(location.getTimestamp())
.dataQuality(location.getDataQuality())
.intervalMs(intervalMs)
.cumulativeDistance(cumulativeDistance)
.cumulativeTime(cumulativeTime)
.progressPercentage(progressPercentage)
.isKeyFrame(isKeyFrame(location, previousLocation))
.keyFrameType(getKeyFrameType(location, previousLocation, sequenceNumber.get() == 2,
sequenceNumber.get() == locations.size() + 1))
.suggestedDelay(calculateSuggestedDelay(intervalMs))
.build();
playbackData.add(playbackVO);
previousLocation = location;
@ -401,8 +397,8 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* 从位置数据构建轨迹对象
*/
private VehicleTrajectoryVO buildTrajectoryFromLocations(Long vehicleId, List<SysVehicleLocation> locations,
Date startTime, Date endTime, Boolean simplified,
Boolean includeStatistics) {
Date startTime, Date endTime, Boolean simplified,
Boolean includeStatistics) {
if (CollectionUtils.isEmpty(locations)) {
return createEmptyTrajectory(vehicleId, startTime, endTime);
}
@ -416,18 +412,18 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
}
return VehicleTrajectoryVO.builder()
.vehicleId(vehicleId)
.licensePlate(firstLocation.getLicensePlate())
.vehicleType(firstLocation.getVehicleType())
.brand(firstLocation.getBrand())
.owningUnit(firstLocation.getOwningUnit())
.startTime(startTime)
.endTime(endTime)
.statistics(statistics)
.points(points)
.simplified(simplified)
.dataQuality(calculateOverallDataQuality(locations))
.build();
.vehicleId(vehicleId)
.licensePlate(firstLocation.getLicensePlate())
.vehicleType(firstLocation.getVehicleType())
.brand(firstLocation.getBrand())
.owningUnit(firstLocation.getOwningUnit())
.startTime(startTime)
.endTime(endTime)
.statistics(statistics)
.points(points)
.simplified(simplified)
.dataQuality(calculateOverallDataQuality(locations))
.build();
}
/**
@ -435,17 +431,16 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
*/
private BigDecimal calculateDistance(SysVehicleLocation from, SysVehicleLocation to) {
if (from == null || to == null ||
from.getLongitude() == null || from.getLatitude() == null ||
to.getLongitude() == null || to.getLatitude() == null) {
from.getLongitude() == null || from.getLatitude() == null ||
to.getLongitude() == null || to.getLatitude() == null) {
return BigDecimal.ZERO;
}
return GeoUtils.calculateDistanceAsBigDecimal(
from.getLatitude().doubleValue(),
from.getLongitude().doubleValue(),
to.getLatitude().doubleValue(),
to.getLongitude().doubleValue()
);
from.getLatitude().doubleValue(),
from.getLongitude().doubleValue(),
to.getLatitude().doubleValue(),
to.getLongitude().doubleValue());
}
/**
@ -502,7 +497,7 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
* 获取关键帧类型
*/
private String getKeyFrameType(SysVehicleLocation current, SysVehicleLocation previous,
boolean isFirst, boolean isLast) {
boolean isFirst, boolean isLast) {
if (isFirst) {
return "START";
}
@ -545,8 +540,8 @@ public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
}
Map<String, Long> qualityCount = locations.stream()
.filter(loc -> StringUtils.hasText(loc.getDataQuality()))
.collect(Collectors.groupingBy(SysVehicleLocation::getDataQuality, Collectors.counting()));
.filter(loc -> StringUtils.hasText(loc.getDataQuality()))
.collect(Collectors.groupingBy(SysVehicleLocation::getDataQuality, Collectors.counting()));
if (qualityCount.isEmpty()) {
return "UNKNOWN";

View File

@ -133,4 +133,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{vehicleId}
</foreach>
</delete>
<select id="selectExistingLicensePlates" parameterType="java.util.List" resultType="String">
select distinct license_plate
from sys_vehicle_info
where license_plate in
<foreach item="plateNumber" collection="list" open="(" separator="," close=")">
#{plateNumber}
</foreach>
</select>
</mapper>

View File

@ -0,0 +1,28 @@
-- 添加未管理车辆位置过滤开关配置项
-- 用于控制是否过滤未在sys_vehicle_info表中注册的车辆位置信息
-- 检查配置项是否已存在,如果不存在则插入
INSERT INTO sys_config (config_name, config_key, config_value, config_type, remark, create_by, create_time)
SELECT
'未管理车辆位置过滤开关',
'unmanaged.vehicle.filter.enabled',
'true',
'Y',
'控制是否过滤未在sys_vehicle_info表中注册的车辆位置信息。true=启用过滤默认false=禁用过滤',
'admin',
NOW()
WHERE NOT EXISTS (
SELECT 1 FROM sys_config WHERE config_key = 'unmanaged.vehicle.filter.enabled'
);
-- 验证插入结果
SELECT
config_id,
config_name,
config_key,
config_value,
config_type,
remark,
create_time
FROM sys_config
WHERE config_key = 'unmanaged.vehicle.filter.enabled';