增加了无人车位置信息API接口,显示无人车的位置信息,优化了数据库表结构,删除多余的字段

优化了WebSocket消息格式,删除多余的字段,增加限速值字段,删除多余的 sql 脚本
This commit is contained in:
Tian jianyong 2025-07-10 19:25:52 +08:00
parent 43e2422197
commit 5453291cd0
46 changed files with 1962 additions and 934 deletions

View File

@ -1 +1 @@
0.3.0
0.3.2

View File

@ -5,6 +5,63 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/)。
版本规范基于 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
## [0.3.2] - 2025-07-10
- 增加无人车位置信息API接口显示无人车的位置信息
- 优化了数据库表结构,删除多余的车牌号字段
- 优化了WebSocket消息格式删除多余的字段增加限速值字段
- 删除多余的 sql 脚本
## [0.3.1] - 2025-01-17
### 🗄️ **数据库规范化改造车辆运动信息API设计优化**
- **架构设计优化**:实现数据库规范化,遵循数据库设计最佳实践
- 将`vehicle_locations`表从包含车牌号改为只存储`vehicle_id`关联字段
- 车牌号、车辆类型等基础信息通过关联`sys_vehicle_info`表查询获取
- 避免数据冗余,提高数据一致性和维护性
### 📊 技术实现亮点
1. **关联查询优化**
- 使用LEFT JOIN联表查询一次性获取完整车辆信息
- PostGIS空间数据与业务数据完美结合
- SQL查询性能优化减少多次查询开销
2. **向后兼容性保障**
- 在碰撞检测模块中添加@Deprecated兼容性方法
- 渐进式改造,最小化对现有系统的影响
- 保持API接口不变前端无感知升级
3. **代码质量提升**
- 实体类字段精简,职责更加清晰
- Mapper XML配置优化查询逻辑标准化
- 添加详细的表注释,提高代码可维护性
### 🛠️ 核心修改内容
- **实体类重构**`SysVehicleLocation`删除冗余字段,添加关联查询字段
- **数据访问层优化**`SysVehicleLocationMapper.xml`使用JOIN查询获取完整信息
- **碰撞检测适配**`VehicleLocation`实体类添加兼容性方法,确保平滑迁移
- **数据库迁移**`unified_database_migration.sql`更新表结构和视图定义
### ✅ 质量保证
- **编译测试**:所有模块编译通过,无错误和警告
- **功能验证**API端点正常工作返回正确的关联信息
- **应用启动**:系统正常启动,健康检查通过
- **数据一致性**:通过关联查询保证数据完整性
### 📋 影响文件
- `qaup-system/src/main/java/com/qaup/system/domain/SysVehicleLocation.java`:实体类重构
- `qaup-system/src/main/resources/mapper/system/SysVehicleLocationMapper.xml`:关联查询优化
- `qaup-collision/src/main/java/com/qaup/collision/common/model/spatial/VehicleLocation.java`:兼容性改造
- `qaup-collision/src/main/java/com/qaup/collision/common/model/repository/VehicleLocationRepository.java`:查询方法更新
- `sql/unified_database_migration.sql`:数据库迁移脚本
- `VERSION.md`版本号更新为0.3.1
### 🎯 用户价值
- **数据库设计规范**:符合关系型数据库规范化原则,提高系统可维护性
- **性能优化**:减少数据冗余,提升查询和存储效率
- **系统稳定性**:通过关联查询确保数据一致性,避免数据不同步问题
- **扩展性增强**:规范化设计为后续功能扩展提供更好的基础架构
## [0.3.0] - 2025-07-09
### 🚀 **重大升级从Spring Boot 2.x升级到Spring Boot 3.x**

31
doc/guide/commands.md Normal file
View File

@ -0,0 +1,31 @@
# 命令
### 强制编译
```bash
mvn clean install -DskipTests
```
### 启动后端
```bash
cd qaup-admin
mvn spring-boot:run
```
### 启动前端
```bash
cd qaup-admin
npm run dev
```
### 访问Swagger UI (前端)
http://localhost:8080/swagger-ui/index.html
### 查看端口占用
```bash
lsof -ti:8080
```
### 杀死进程
```bash
kill -9 进程ID
```

View File

@ -332,8 +332,3 @@ public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: SGVsbG8sIHdvcmxkIQ==" http://localhost:8080/collision
强制编译
mvn clean install -DskipTests

View File

@ -9,6 +9,12 @@
- 速度50km/h
- 方向180度
- 航班号MU5123
- 起点经度120.088076纬度36.374179
- 终点经度120.077971纬度36.371503
- 速度50km/h
- 方向180度
### 2. 特勤车
- 车牌号鲁B123
- 起点经度120.080801纬度36.366626

View File

@ -0,0 +1,96 @@
# 待办事项
### 设置发送给前端的Websocket消息的类型
- 是否完成true
```java
/**
* 电子围栏告警级别枚举
*/
public enum GeofenceAlertLevel {
/**
* 信息级别
*/
INFO("信息", 1),
/**
* 警告级别
*/
WARNING("警告", 2),
/**
* 严重级别
*/
CRITICAL("严重", 3),
/**
* 紧急级别
*/
EMERGENCY("紧急", 4);
```
### 在超速消息中增加一个字段表示限速值单位是km/h
- 是否完成true
```java
public class RuleViolationPayload {
/**
* 实际值
*/
@JsonProperty("actualValue")
private Double actualValue;
/**
* 限制值
*/
@JsonProperty("limitValue")
private Double limitValue;
```
## API 接口
### 增加无人车位置信息接口,显示无人车的位置信息,包括:
- 无人车的ID
- 无人车的车牌号
- 无人车的类型
- 无人车的状态
- 无人车的位置
- 无人车的速度
- 无人车的方向
### 增加无人车的运行信息接口,显示无人车的运行信息,包括:
- 无人车的ID
- 无人车的车牌号
- 无人车的类型
- 无人车的电池电量
- 无人车的运行时间
- 无人车的运行距离
- 无人车的运行速度
### 增加无人车的任务执行状态接口,显示无人车的任务执行状态,包括:
- 无人车的ID
- 无人车的车牌号
- 无人车的类型
- 无人车的任务执行状态
- 无人车的任务执行开始时间
- 无人车的任务完成百分比
- 无人车的任务执行轨迹
### 替换 Swagger 为 SpringDoc
- springfox-boot-starterSwagger 3.0.0 可能不兼容 Spring Boot 3.x建议迁移到 SpringDoc OpenAPI已在 properties 中定义 springdoc.version=2.6.0,但未使用)。
- 是否完成true
- 在 pom.xml 中添加以下依赖:
```xml
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
```

36
pom.xml
View File

@ -14,8 +14,8 @@
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>17</java.version>
<maven-jar-plugin.version>3.1.1</maven-jar-plugin.version>
<mybatis-spring-boot.version>3.0.3</mybatis-spring-boot.version>
<druid.version>1.2.23</druid.version>
<mybatis-spring-boot.version>3.0.4</mybatis-spring-boot.version>
<druid.version>1.2.25</druid.version>
<bitwalker.version>1.21</bitwalker.version>
<swagger.version>3.0.0</swagger.version>
<kaptcha.version>2.3.3</kaptcha.version>
@ -28,7 +28,7 @@
<jwt.version>0.9.1</jwt.version>
<jaxb-api.version>2.3.1</jaxb-api.version>
<jakarta.version>6.0.0</jakarta.version>
<springdoc.version>2.6.0</springdoc.version>
<springdoc.version>2.8.9</springdoc.version>
<!-- 空间计算库版本管理 -->
<geotools.version>28.5</geotools.version>
<jts.version>1.19.0</jts.version>
@ -46,7 +46,7 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.3.5</version>
<version>3.5.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
@ -92,17 +92,11 @@
<version>${oshi.version}</version>
</dependency>
<!-- Swagger3依赖 -->
<!-- SpringDoc OpenAPI UI 依赖 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${swagger.version}</version>
<exclusions>
<exclusion>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
</exclusion>
</exclusions>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- io常用工具类 -->
@ -247,6 +241,13 @@
<version>${jakarta.version}</version>
</dependency>
<!-- 统一管理 Netty -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.2.2.Final</version>
</dependency>
</dependencies>
</dependencyManagement>
@ -266,7 +267,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<version>3.14.0</version>
<configuration>
<release>${java.version}</release>
<encoding>${project.build.sourceEncoding}</encoding>
@ -279,7 +280,10 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.3.0</version>
<version>3.5.3</version>
<configuration>
<jvmArguments>--enable-native-access=ALL-UNNAMED</jvmArguments>
</configuration>
</plugin>
</plugins>
</build>

View File

@ -36,19 +36,6 @@
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- swagger3-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
</dependency>
<!-- 防止进入swagger页面报类型转换错误排除3.0.0中的引用手动增加1.6.2版本 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-models</artifactId>
<version>1.6.2</version>
</dependency>
<!-- Postgresql驱动包 -->
<dependency>
<groupId>org.postgresql</groupId>
@ -81,6 +68,12 @@
<version>${qaup.version}</version>
</dependency>
<!-- SpringDoc OpenAPI UI 依赖 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>
<build>
@ -88,7 +81,7 @@
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.3.0</version>
<version>3.5.3</version>
<executions>
<execution>
<goals>
@ -100,7 +93,7 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.1.0</version>
<version>3.4.0</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
<warName>${project.artifactId}</warName>

View File

@ -20,18 +20,18 @@ import com.qaup.system.domain.SysDriverInfo;
import com.qaup.system.service.ISysDriverInfoService;
import com.qaup.common.utils.poi.ExcelUtil;
import com.qaup.common.core.page.TableDataInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 驾驶员信息Controller
*
* @author qaup
* @date 2025-06-30
*/
@Api(tags = "驾驶员信息管理")
@Tag(name = "驾驶员信息管理", description = "驾驶员信息管理")
@RestController
@RequestMapping("/system/driver_info")
public class SysDriverInfoController extends BaseController
@ -43,7 +43,7 @@ public class SysDriverInfoController extends BaseController
* 查询驾驶员信息列表
*/
//@PreAuthorize("@ss.hasPermi('system:driver_info:list')")
@ApiOperation("查询驾驶员信息列表")
@Operation(summary = "查询驾驶员信息列表")
@GetMapping("/list")
public TableDataInfo list(SysDriverInfo sysDriverInfo)
{
@ -57,7 +57,7 @@ public class SysDriverInfoController extends BaseController
*/
//@PreAuthorize("@ss.hasPermi('system:driver_info:export')")
@Log(title = "驾驶员信息", businessType = BusinessType.EXPORT)
@ApiOperation("导出驾驶员信息列表")
@Operation(summary = "导出驾驶员信息列表")
@PostMapping("/export")
public void export(HttpServletResponse response, SysDriverInfo sysDriverInfo)
{
@ -70,8 +70,8 @@ public class SysDriverInfoController extends BaseController
* 获取驾驶员信息详细信息
*/
//@PreAuthorize("@ss.hasPermi('system:driver_info:query')")
@ApiOperation("获取驾驶员信息详细信息")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "Long", dataTypeClass = Long.class)
@Operation(summary = "获取驾驶员信息详细信息")
@Parameter(name = "userId", description = "用户ID", required = true)
@GetMapping(value = "/{userId}")
public AjaxResult getInfo(@PathVariable("userId") Long userId)
{
@ -83,10 +83,8 @@ public class SysDriverInfoController extends BaseController
*/
//@PreAuthorize("@ss.hasPermi('system:driver_info:add')")
@Log(title = "驾驶员信息", businessType = BusinessType.INSERT)
@ApiOperation("新增驾驶员信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "licenseType", value = "驾驶证类型", required = true, dataType = "String", dataTypeClass = String.class),
})
@Operation(summary = "新增驾驶员信息")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "驾驶员信息对象", required = true, content = @Content(mediaType = "application/json", schema = @Schema(implementation = SysDriverInfo.class)))
@PostMapping
public AjaxResult add(@RequestBody SysDriverInfo sysDriverInfo)
{
@ -98,11 +96,8 @@ public class SysDriverInfoController extends BaseController
*/
//@PreAuthorize("@ss.hasPermi('system:driver_info:edit')")
@Log(title = "驾驶员信息", businessType = BusinessType.UPDATE)
@ApiOperation("修改驾驶员信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "Long", dataTypeClass = Long.class),
@ApiImplicitParam(name = "licenseType", value = "驾驶证类型", required = true, dataType = "String", dataTypeClass = String.class),
})
@Operation(summary = "修改驾驶员信息")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "驾驶员信息对象", required = true, content = @Content(mediaType = "application/json", schema = @Schema(implementation = SysDriverInfo.class)))
@PutMapping
public AjaxResult edit(@RequestBody SysDriverInfo sysDriverInfo)
{
@ -114,8 +109,8 @@ public class SysDriverInfoController extends BaseController
*/
//@PreAuthorize("@ss.hasPermi('system:driver_info:remove')")
@Log(title = "驾驶员信息", businessType = BusinessType.DELETE)
@ApiOperation("删除驾驶员信息")
@ApiImplicitParam(name = "userIds", value = "用户ID", required = true, dataType = "Long", dataTypeClass = Long.class)
@Operation(summary = "删除驾驶员信息")
@Parameter(name = "userIds", description = "用户ID", required = true)
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds)
{

View File

@ -1,141 +1,143 @@
package com.qaup.web.controller.system;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.qaup.common.annotation.Log;
import com.qaup.common.config.QaupConfig;
import com.qaup.common.core.controller.BaseController;
import com.qaup.common.core.domain.AjaxResult;
import com.qaup.common.core.domain.entity.SysUser;
import com.qaup.common.core.domain.model.LoginUser;
import com.qaup.common.enums.BusinessType;
import com.qaup.common.utils.DateUtils;
import com.qaup.common.utils.SecurityUtils;
import com.qaup.common.utils.StringUtils;
import com.qaup.common.utils.file.FileUploadUtils;
import com.qaup.common.utils.file.MimeTypeUtils;
import com.qaup.framework.web.service.TokenService;
import com.qaup.system.service.ISysUserService;
/**
* 个人信息 业务处理
*
* @author qaup
*/
@RestController
@RequestMapping("/system/user/profile")
public class SysProfileController extends BaseController
{
@Autowired
private ISysUserService userService;
@Autowired
private TokenService tokenService;
/**
* 个人信息
*/
@GetMapping
public AjaxResult profile()
{
LoginUser loginUser = getLoginUser();
SysUser user = loginUser.getUser();
AjaxResult ajax = AjaxResult.success(user);
ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
return ajax;
}
/**
* 修改用户
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult updateProfile(@RequestBody SysUser user)
{
LoginUser loginUser = getLoginUser();
SysUser currentUser = loginUser.getUser();
currentUser.setEmail(user.getEmail());
currentUser.setPhonenumber(user.getPhonenumber());
currentUser.setSex(user.getSex());
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
{
return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
}
if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
{
return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
}
if (userService.updateUserProfile(currentUser) > 0)
{
// 更新缓存用户信息
tokenService.setLoginUser(loginUser);
return success();
}
return error("修改个人信息异常,请联系管理员");
}
/**
* 重置密码
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public AjaxResult updatePwd(@RequestBody Map<String, String> params)
{
String oldPassword = params.get("oldPassword");
String newPassword = params.get("newPassword");
LoginUser loginUser = getLoginUser();
String userName = loginUser.getUsername();
String password = loginUser.getPassword();
if (!SecurityUtils.matchesPassword(oldPassword, password))
{
return error("修改密码失败,旧密码错误");
}
if (SecurityUtils.matchesPassword(newPassword, password))
{
return error("新密码不能与旧密码相同");
}
newPassword = SecurityUtils.encryptPassword(newPassword);
if (userService.resetUserPwd(userName, newPassword) > 0)
{
// 更新缓存用户密码&密码最后更新时间
loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate());
loginUser.getUser().setPassword(newPassword);
tokenService.setLoginUser(loginUser);
return success();
}
return error("修改密码异常,请联系管理员");
}
/**
* 头像上传
*/
@Log(title = "用户头像", businessType = BusinessType.UPDATE)
@PostMapping("/avatar")
public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
{
if (!file.isEmpty())
{
LoginUser loginUser = getLoginUser();
String avatar = FileUploadUtils.upload(QaupConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
{
AjaxResult ajax = AjaxResult.success();
ajax.put("imgUrl", avatar);
// 更新缓存用户头像
loginUser.getUser().setAvatar(avatar);
tokenService.setLoginUser(loginUser);
return ajax;
}
}
return error("上传图片异常,请联系管理员");
}
}
package com.qaup.web.controller.system;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.qaup.common.annotation.Log;
import com.qaup.common.config.QaupConfig;
import com.qaup.common.core.controller.BaseController;
import com.qaup.common.core.domain.AjaxResult;
import com.qaup.common.core.domain.entity.SysUser;
import com.qaup.common.core.domain.model.LoginUser;
import com.qaup.common.enums.BusinessType;
import com.qaup.common.utils.DateUtils;
import com.qaup.common.utils.SecurityUtils;
import com.qaup.common.utils.StringUtils;
import com.qaup.common.utils.file.FileUploadUtils;
import com.qaup.common.utils.file.MimeTypeUtils;
import com.qaup.framework.web.service.TokenService;
import com.qaup.system.service.ISysUserService;
import io.swagger.v3.oas.annotations.Hidden;
/**
* 个人信息 业务处理
*
* @author qaup
*/
@RestController
@RequestMapping("/system/user/profile")
@Hidden
public class SysProfileController extends BaseController
{
@Autowired
private ISysUserService userService;
@Autowired
private TokenService tokenService;
/**
* 个人信息
*/
@GetMapping
public AjaxResult profile()
{
LoginUser loginUser = getLoginUser();
SysUser user = loginUser.getUser();
AjaxResult ajax = AjaxResult.success(user);
ajax.put("roleGroup", userService.selectUserRoleGroup(loginUser.getUsername()));
ajax.put("postGroup", userService.selectUserPostGroup(loginUser.getUsername()));
return ajax;
}
/**
* 修改用户
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult updateProfile(@RequestBody SysUser user)
{
LoginUser loginUser = getLoginUser();
SysUser currentUser = loginUser.getUser();
currentUser.setEmail(user.getEmail());
currentUser.setPhonenumber(user.getPhonenumber());
currentUser.setSex(user.getSex());
if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(currentUser))
{
return error("修改用户'" + loginUser.getUsername() + "'失败,手机号码已存在");
}
if (StringUtils.isNotEmpty(user.getEmail()) && !userService.checkEmailUnique(currentUser))
{
return error("修改用户'" + loginUser.getUsername() + "'失败,邮箱账号已存在");
}
if (userService.updateUserProfile(currentUser) > 0)
{
// 更新缓存用户信息
tokenService.setLoginUser(loginUser);
return success();
}
return error("修改个人信息异常,请联系管理员");
}
/**
* 重置密码
*/
@Log(title = "个人信息", businessType = BusinessType.UPDATE)
@PutMapping("/updatePwd")
public AjaxResult updatePwd(@RequestBody Map<String, String> params)
{
String oldPassword = params.get("oldPassword");
String newPassword = params.get("newPassword");
LoginUser loginUser = getLoginUser();
String userName = loginUser.getUsername();
String password = loginUser.getPassword();
if (!SecurityUtils.matchesPassword(oldPassword, password))
{
return error("修改密码失败,旧密码错误");
}
if (SecurityUtils.matchesPassword(newPassword, password))
{
return error("新密码不能与旧密码相同");
}
newPassword = SecurityUtils.encryptPassword(newPassword);
if (userService.resetUserPwd(userName, newPassword) > 0)
{
// 更新缓存用户密码&密码最后更新时间
loginUser.getUser().setPwdUpdateDate(DateUtils.getNowDate());
loginUser.getUser().setPassword(newPassword);
tokenService.setLoginUser(loginUser);
return success();
}
return error("修改密码异常,请联系管理员");
}
/**
* 头像上传
*/
@Log(title = "用户头像", businessType = BusinessType.UPDATE)
@PostMapping("/avatar")
public AjaxResult avatar(@RequestParam("avatarfile") MultipartFile file) throws Exception
{
if (!file.isEmpty())
{
LoginUser loginUser = getLoginUser();
String avatar = FileUploadUtils.upload(QaupConfig.getAvatarPath(), file, MimeTypeUtils.IMAGE_EXTENSION);
if (userService.updateUserAvatar(loginUser.getUsername(), avatar))
{
AjaxResult ajax = AjaxResult.success();
ajax.put("imgUrl", avatar);
// 更新缓存用户头像
loginUser.getUser().setAvatar(avatar);
tokenService.setLoginUser(loginUser);
return ajax;
}
}
return error("上传图片异常,请联系管理员");
}
}

View File

@ -30,14 +30,13 @@ import com.qaup.system.domain.SysUserRole;
import com.qaup.system.service.ISysDeptService;
import com.qaup.system.service.ISysRoleService;
import com.qaup.system.service.ISysUserService;
import io.swagger.annotations.Api;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* 角色信息
*
* @author qaup
*/
@Api("角色信息管理")
@Tag(name = "角色信息管理")
@RestController
@RequestMapping("/system/role")
public class SysRoleController extends BaseController

View File

@ -31,14 +31,14 @@ import com.qaup.system.service.ISysDeptService;
import com.qaup.system.service.ISysPostService;
import com.qaup.system.service.ISysRoleService;
import com.qaup.system.service.ISysUserService;
import io.swagger.annotations.Api;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* 用户信息
*
* @author qaup
*/
@Api("用户信息管理")
@Tag(name = "用户管理", description = "用户管理相关接口")
@RestController
@RequestMapping("/system/user")
public class SysUserController extends BaseController

View File

@ -20,10 +20,11 @@ import com.qaup.system.domain.SysVehicleInfo;
import com.qaup.system.service.ISysVehicleInfoService;
import com.qaup.common.utils.poi.ExcelUtil;
import com.qaup.common.core.page.TableDataInfo;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 车辆信息Controller
@ -31,7 +32,7 @@ import io.swagger.annotations.ApiImplicitParams;
* @author tellme
* @date 2025-07-01
*/
@Api(tags = "车辆信息管理")
@Tag(name = "车辆信息管理")
@RestController
@RequestMapping("/system/vehicle_info")
public class SysVehicleInfoController extends BaseController
@ -43,7 +44,7 @@ public class SysVehicleInfoController extends BaseController
* 查询车辆信息列表
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_info:list')")
@ApiOperation("查询车辆信息列表")
@Operation(summary = "查询车辆信息列表")
@GetMapping("/list")
public TableDataInfo list(SysVehicleInfo sysVehicleInfo)
{
@ -56,7 +57,7 @@ public class SysVehicleInfoController extends BaseController
* 导出车辆信息列表
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_info:export')")
@ApiOperation("导出车辆信息列表")
@Operation(summary = "导出车辆信息列表")
@Log(title = "车辆信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, SysVehicleInfo sysVehicleInfo)
@ -70,10 +71,8 @@ public class SysVehicleInfoController extends BaseController
* 获取车辆信息详细信息
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_info:query')")
@ApiOperation("获取车辆信息详细信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "vehicleId", value = "车辆ID", required = true, dataType = "Long", dataTypeClass = Long.class)
})
@Operation(summary = "获取车辆信息详细信息")
@Parameter(name = "vehicleId", description = "车辆ID", required = true)
@GetMapping(value = "/{vehicleId}")
public AjaxResult getInfo(@PathVariable("vehicleId") Long vehicleId)
{
@ -84,18 +83,9 @@ public class SysVehicleInfoController extends BaseController
* 新增车辆信息
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_info:add')")
@ApiOperation("新增车辆信息")
@Operation(summary = "新增车辆信息")
@Log(title = "车辆信息", businessType = BusinessType.INSERT)
@ApiImplicitParams({
@ApiImplicitParam(name = "licensePlate", value = "车牌号", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "vinNumber", value = "VIN码", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "typeId", value = "类型ID", required = true, dataType = "Long", dataTypeClass = Long.class),
@ApiImplicitParam(name = "brand", value = "品牌", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "owningUnit", value = "所属单位", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "contactPerson", value = "联系人", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "phoneNumber", value = "电话", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "imageUrl", value = "图片URL", required = true, dataType = "String", dataTypeClass = String.class),
})
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "车辆信息对象", required = true, content = @Content(mediaType = "application/json", schema = @Schema(implementation = SysVehicleInfo.class)))
@PostMapping
public AjaxResult add(@RequestBody SysVehicleInfo sysVehicleInfo)
{
@ -106,18 +96,8 @@ public class SysVehicleInfoController extends BaseController
* 修改车辆信息
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_info:edit')")
@ApiOperation("修改车辆信息")
@ApiImplicitParams({
@ApiImplicitParam(name = "vehicleId", value = "车辆ID", required = true, dataType = "Long", dataTypeClass = Long.class),
@ApiImplicitParam(name = "licensePlate", value = "车牌号", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "vinNumber", value = "VIN码", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "typeId", value = "类型ID", required = true, dataType = "Long", dataTypeClass = Long.class),
@ApiImplicitParam(name = "brand", value = "品牌", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "owningUnit", value = "所属单位", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "contactPerson", value = "联系人", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "phoneNumber", value = "电话", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "imageUrl", value = "图片URL", required = true, dataType = "String", dataTypeClass = String.class),
})
@Operation(summary = "修改车辆信息")
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "车辆信息对象", required = true, content = @Content(mediaType = "application/json", schema = @Schema(implementation = SysVehicleInfo.class)))
@Log(title = "车辆信息", businessType = BusinessType.UPDATE)
@PutMapping
public AjaxResult edit(@RequestBody SysVehicleInfo sysVehicleInfo)
@ -129,9 +109,9 @@ public class SysVehicleInfoController extends BaseController
* 删除车辆信息
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_info:remove')")
@ApiOperation("删除车辆信息")
@Operation(summary = "删除车辆信息")
@Log(title = "车辆信息", businessType = BusinessType.DELETE)
@ApiImplicitParam(name = "vehicleIds", value = "车辆ID", required = true, dataType = "Long", dataTypeClass = Long.class)
@Parameter(name = "vehicleIds", description = "车辆ID", required = true)
@DeleteMapping("/{vehicleIds}")
public AjaxResult remove(@PathVariable Long[] vehicleIds)
{

View File

@ -0,0 +1,110 @@
package com.qaup.web.controller.system;
import java.util.List;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.qaup.common.annotation.Log;
import com.qaup.common.core.controller.BaseController;
import com.qaup.common.core.domain.AjaxResult;
import com.qaup.common.enums.BusinessType;
import com.qaup.system.domain.SysVehicleLocation;
import com.qaup.system.service.ISysVehicleLocationService;
import com.qaup.common.utils.poi.ExcelUtil;
import com.qaup.common.core.page.TableDataInfo;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* 车辆运动信息Controller
*
* @author qaup
* @date 2025-01-16
*/
@Tag(name = "车辆运动信息管理")
@RestController
@RequestMapping("/system/vehicle_location")
public class SysVehicleLocationController extends BaseController
{
@Autowired
private ISysVehicleLocationService sysVehicleLocationService;
/**
* 查询车辆运动信息列表
*/
@Operation(summary = "查询车辆运动信息列表", description = "支持按车辆ID、车牌号、车辆类型、品牌、所属单位、数据质量、时间段等条件查询。车牌号、车辆类型等基础信息通过关联sys_vehicle_info表获取")
@GetMapping("/list")
public TableDataInfo list(SysVehicleLocation sysVehicleLocation)
{
startPage();
List<SysVehicleLocation> list = sysVehicleLocationService.selectSysVehicleLocationList(sysVehicleLocation);
return getDataTable(list);
}
/**
* 导出车辆运动信息列表
*/
@Operation(summary = "导出车辆运动信息列表", description = "导出包含关联的车辆基础信息和位置数据的Excel文件")
@Log(title = "车辆运动信息", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, SysVehicleLocation sysVehicleLocation)
{
List<SysVehicleLocation> list = sysVehicleLocationService.selectSysVehicleLocationList(sysVehicleLocation);
ExcelUtil<SysVehicleLocation> util = new ExcelUtil<SysVehicleLocation>(SysVehicleLocation.class);
util.exportExcel(response, list, "车辆运动信息数据");
}
/**
* 根据车辆ID查询车辆运动信息列表
*/
@Operation(summary = "根据车辆ID查询车辆运动信息列表", description = "查询指定车辆的所有位置记录,包含关联的车辆基础信息")
@Parameter(name = "vehicleId", description = "车辆ID", required = true)
@GetMapping("/vehicle/{vehicleId}")
public AjaxResult getByVehicleId(@PathVariable("vehicleId") Long vehicleId)
{
List<SysVehicleLocation> list = sysVehicleLocationService.selectSysVehicleLocationByVehicleId(vehicleId);
return success(list);
}
/**
* 根据车牌号查询车辆运动信息列表
*/
@Operation(summary = "根据车牌号查询车辆运动信息列表", description = "通过车牌号关联查询车辆的所有位置记录")
@Parameter(name = "licensePlate", description = "车牌号", required = true)
@GetMapping("/plate/{licensePlate}")
public AjaxResult getByLicensePlate(@PathVariable("licensePlate") String licensePlate)
{
List<SysVehicleLocation> list = sysVehicleLocationService.selectSysVehicleLocationByLicensePlate(licensePlate);
return success(list);
}
/**
* 根据车辆ID查询车辆最新位置信息
*/
@Operation(summary = "根据车辆ID查询车辆最新位置信息", description = "获取指定车辆的最新位置记录,包含关联的车辆基础信息")
@Parameter(name = "vehicleId", description = "车辆ID", required = true)
@GetMapping("/latest/vehicle/{vehicleId}")
public AjaxResult getLatestByVehicleId(@PathVariable("vehicleId") Long vehicleId)
{
SysVehicleLocation sysVehicleLocation = sysVehicleLocationService.selectLatestSysVehicleLocationByVehicleId(vehicleId);
return success(sysVehicleLocation);
}
/**
* 根据车牌号查询车辆最新位置信息
*/
@Operation(summary = "根据车牌号查询车辆最新位置信息", description = "通过车牌号关联查询车辆的最新位置记录")
@Parameter(name = "licensePlate", description = "车牌号", required = true)
@GetMapping("/latest/plate/{licensePlate}")
public AjaxResult getLatestByLicensePlate(@PathVariable("licensePlate") String licensePlate)
{
SysVehicleLocation sysVehicleLocation = sysVehicleLocationService.selectLatestSysVehicleLocationByLicensePlate(licensePlate);
return success(sysVehicleLocation);
}
}

View File

@ -19,10 +19,11 @@ import com.qaup.common.enums.BusinessType;
import com.qaup.system.domain.SysVehicleType;
import com.qaup.system.service.ISysVehicleTypeService;
import com.qaup.common.utils.poi.ExcelUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* 车辆类型Controller
@ -30,7 +31,7 @@ import io.swagger.annotations.ApiImplicitParams;
* @author tellme
* @date 2025-07-01
*/
@Api(tags = "车辆类型管理")
@Tag(name = "车辆类型管理")
@RestController
@RequestMapping("/system/vehicle_type")
public class SysVehicleTypeController extends BaseController
@ -42,7 +43,7 @@ public class SysVehicleTypeController extends BaseController
* 查询车辆类型列表
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_type:list')")
@ApiOperation("查询车辆类型列表")
@Operation(summary = "查询车辆类型列表")
@GetMapping("/list")
public AjaxResult list(SysVehicleType sysVehicleType)
{
@ -54,7 +55,7 @@ public class SysVehicleTypeController extends BaseController
* 导出车辆类型列表
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_type:export')")
@ApiOperation("导出车辆类型列表")
@Operation(summary = "导出车辆类型列表")
@Log(title = "车辆类型", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(HttpServletResponse response, SysVehicleType sysVehicleType)
@ -68,11 +69,9 @@ public class SysVehicleTypeController extends BaseController
* 获取车辆类型详细信息
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_type:query')")
@ApiOperation("获取车辆类型详细信息")
@Operation(summary = "获取车辆类型详细信息")
@GetMapping(value = "/{typeId}")
@ApiImplicitParams({
@ApiImplicitParam(name = "typeId", value = "类型ID", required = true, dataType = "Long", dataTypeClass = Long.class)
})
@Parameter(name = "typeId", description = "类型ID", required = true)
public AjaxResult getInfo(@PathVariable("typeId") Long typeId)
{
return success(sysVehicleTypeService.selectSysVehicleTypeByTypeId(typeId));
@ -82,13 +81,9 @@ public class SysVehicleTypeController extends BaseController
* 新增车辆类型
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_type:add')")
@ApiOperation("新增车辆类型")
@Operation(summary = "新增车辆类型")
@Log(title = "车辆类型", businessType = BusinessType.INSERT)
@ApiImplicitParams({
@ApiImplicitParam(name = "parentId", value = "父类型ID", required = true, dataType = "Long", dataTypeClass = Long.class),
@ApiImplicitParam(name = "typeName", value = "类型名称", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "level", value = "类型级别", required = true, dataType = "Integer", dataTypeClass = Integer.class),
})
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "车辆类型对象", required = true, content = @Content(mediaType = "application/json", schema = @Schema(implementation = SysVehicleType.class)))
@PostMapping
public AjaxResult add(@RequestBody SysVehicleType sysVehicleType)
{
@ -99,14 +94,9 @@ public class SysVehicleTypeController extends BaseController
* 修改车辆类型
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_type:edit')")
@ApiOperation("修改车辆类型")
@Operation(summary = "修改车辆类型")
@Log(title = "车辆类型", businessType = BusinessType.UPDATE)
@ApiImplicitParams({
@ApiImplicitParam(name = "typeId", value = "类型ID", required = true, dataType = "Long", dataTypeClass = Long.class),
@ApiImplicitParam(name = "parentId", value = "父类型ID", required = true, dataType = "Long", dataTypeClass = Long.class),
@ApiImplicitParam(name = "typeName", value = "类型名称", required = true, dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "level", value = "类型级别", required = true, dataType = "Integer", dataTypeClass = Integer.class),
})
@io.swagger.v3.oas.annotations.parameters.RequestBody(description = "车辆类型对象", required = true, content = @Content(mediaType = "application/json", schema = @Schema(implementation = SysVehicleType.class)))
@PutMapping
public AjaxResult edit(@RequestBody SysVehicleType sysVehicleType)
{
@ -117,9 +107,9 @@ public class SysVehicleTypeController extends BaseController
* 删除车辆类型
*/
//@PreAuthorize("@ss.hasPermi('system:vehicle_type:remove')")
@ApiOperation("删除车辆类型")
@Operation(summary = "删除车辆类型")
@Log(title = "车辆类型", businessType = BusinessType.DELETE)
@ApiImplicitParam(name = "typeIds", value = "类型ID", required = true, dataType = "Long", dataTypeClass = Long.class)
@Parameter(name = "typeIds", description = "类型ID", required = true)
@DeleteMapping("/{typeIds}")
public AjaxResult remove(@PathVariable Long[] typeIds)
{

View File

@ -15,19 +15,17 @@ import org.springframework.web.bind.annotation.RestController;
import com.qaup.common.core.controller.BaseController;
import com.qaup.common.core.domain.R;
import com.qaup.common.utils.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Schema;
/**
* swagger 用户测试方法
*
* @author qaup
*/
@Api("用户信息管理")
@Tag(name = "用户信息管理")
@RestController
@RequestMapping("/test/user")
public class TestController extends BaseController
@ -38,7 +36,7 @@ public class TestController extends BaseController
users.put(2, new UserEntity(2, "qaup", "admin123", "15666666666"));
}
@ApiOperation("获取用户列表")
@Operation(summary = "获取用户列表")
@GetMapping("/list")
public R<List<UserEntity>> userList()
{
@ -46,8 +44,8 @@ public class TestController extends BaseController
return R.ok(userList);
}
@ApiOperation("获取用户详细")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
@Operation(summary = "获取用户详细")
@Parameter(name = "userId", description = "用户ID", required = true)
@GetMapping("/{userId}")
public R<UserEntity> getUser(@PathVariable Integer userId)
{
@ -61,15 +59,9 @@ public class TestController extends BaseController
}
}
@ApiOperation("新增用户")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "用户id", dataType = "Integer", dataTypeClass = Integer.class),
@ApiImplicitParam(name = "username", value = "用户名称", dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "password", value = "用户密码", dataType = "String", dataTypeClass = String.class),
@ApiImplicitParam(name = "mobile", value = "用户手机", dataType = "String", dataTypeClass = String.class)
})
@Operation(summary = "新增用户")
@PostMapping("/save")
public R<String> save(UserEntity user)
public R<String> save(@RequestBody UserEntity user)
{
if (StringUtils.isNull(user) || StringUtils.isNull(user.getUserId()))
{
@ -79,7 +71,7 @@ public class TestController extends BaseController
return R.ok();
}
@ApiOperation("更新用户")
@Operation(summary = "更新用户")
@PutMapping("/update")
public R<String> update(@RequestBody UserEntity user)
{
@ -96,8 +88,8 @@ public class TestController extends BaseController
return R.ok();
}
@ApiOperation("删除用户信息")
@ApiImplicitParam(name = "userId", value = "用户ID", required = true, dataType = "int", paramType = "path", dataTypeClass = Integer.class)
@Operation(summary = "删除用户信息")
@Parameter(name = "userId", description = "用户ID", required = true)
@DeleteMapping("/{userId}")
public R<String> delete(@PathVariable Integer userId)
{
@ -113,19 +105,19 @@ public class TestController extends BaseController
}
}
@ApiModel(value = "UserEntity", description = "用户实体")
@Schema(name = "UserEntity", description = "用户实体")
class UserEntity
{
@ApiModelProperty("用户ID")
@Schema(description = "用户ID")
private Integer userId;
@ApiModelProperty("用户名称")
@Schema(description = "用户名称")
private String username;
@ApiModelProperty("用户密码")
@Schema(description = "用户密码")
private String password;
@ApiModelProperty("用户手机")
@Schema(description = "用户手机")
private String mobile;
public UserEntity()

View File

@ -0,0 +1,52 @@
package com.qaup.web.core.config;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import com.qaup.common.config.QaupConfig;
/**
* SpringDoc OpenAPI 配置
*
* @author qaup
*/
@Configuration
@Profile("dev")
public class OpenApiConfig {
@Autowired
private QaupConfig qaupConfig;
@Bean
public OpenAPI customOpenAPI() {
return new OpenAPI()
.info(apiInfo())
.addSecurityItem(new SecurityRequirement().addList("Authorization"))
.components(new io.swagger.v3.oas.models.Components()
.addSecuritySchemes("Authorization", new SecurityScheme()
.name("Authorization")
.type(SecurityScheme.Type.HTTP)
.scheme("bearer")
.bearerFormat("JWT")))
.addServersItem(new Server().url("/").description("Direct Backend Access"))
.addServersItem(new Server().url("/dev-api").description("Frontend Proxy Access"));
}
private Info apiInfo() {
return new Info()
.title("后台管理系统接口文档")
.description("用于后台管理系统,包括用户、角色、车辆信息等模块的接口说明。")
.contact(new Contact()
.name(qaupConfig.getName())
.url(null)
.email(null))
.version(qaupConfig.getVersion());
}
}

View File

@ -1,125 +0,0 @@
package com.qaup.web.core.config;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import com.qaup.common.config.QaupConfig;
import io.swagger.annotations.ApiOperation;
import io.swagger.models.auth.In;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ApiKey;
import springfox.documentation.service.AuthorizationScope;
import springfox.documentation.service.Contact;
import springfox.documentation.service.SecurityReference;
import springfox.documentation.service.SecurityScheme;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
/**
* Swagger2的接口配置
*
* @author qaup
*/
@Configuration
public class SwaggerConfig
{
/** 系统基础配置 */
@Autowired
private QaupConfig qaupConfig;
/** 是否开启swagger */
@Value("${swagger.enabled}")
private boolean enabled;
/** 设置请求的统一前缀 */
@Value("${swagger.pathMapping}")
private String pathMapping;
/**
* 创建API
*/
@Bean
public Docket createRestApi()
{
return new Docket(DocumentationType.OAS_30)
// 是否启用Swagger
.enable(enabled)
// 用来创建该API的基本信息展示在文档的页面中自定义展示的信息
.apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示
.select()
// 扫描所有有注解的api用这种方式更灵活
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// 扫描指定包中的swagger注解
// .apis(RequestHandlerSelectors.basePackage("com.qaup.project.tool.swagger"))
// 扫描所有 .apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
/* 设置安全模式swagger可以设置访问token */
.securitySchemes(securitySchemes())
.securityContexts(securityContexts())
.pathMapping(pathMapping);
}
/**
* 安全模式这里指定token通过Authorization头请求头传递
*/
private List<SecurityScheme> securitySchemes()
{
List<SecurityScheme> apiKeyList = new ArrayList<SecurityScheme>();
apiKeyList.add(new ApiKey("Authorization", "Authorization", In.HEADER.toValue()));
return apiKeyList;
}
/**
* 安全上下文
*/
private List<SecurityContext> securityContexts()
{
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(
SecurityContext.builder()
.securityReferences(defaultAuth())
.operationSelector(o -> o.requestMappingPattern().matches("/.*"))
.build());
return securityContexts;
}
/**
* 默认的安全上引用
*/
private List<SecurityReference> defaultAuth()
{
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
return securityReferences;
}
/**
* 添加摘要信息
*/
private ApiInfo apiInfo()
{
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("标题后台管理系统_接口文档")
// 描述
.description("描述:用于后台管理系统,具体包括XXX,XXX模块...")
// 作者信息
.contact(new Contact(qaupConfig.getName(), null, null))
// 版本
.version("版本号:" + qaupConfig.getVersion())
.build();
}
}

View File

@ -1,243 +1,245 @@
# 项目相关配置
qaup:
# 名称
name: Qaup
# 版本
version: 3.8.9
# 版权年份
copyrightYear: 2025
# 文件路径 示例( Windows配置D:/qaup/uploadPathLinux配置 /home/qaup/uploadPath
profile: Users/tianjianyong/apps/OpenSource/Qaup-Vue-Postgresql/uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 日志配置
logging:
level:
com.qaup: debug
org.springframework: warn
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: druid
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
# 重启目录
additional-paths: src/main/java
# 排除目录
exclude: WEB-INF/**
jackson:
# 日期格式化
date-format: yyyy-MM-dd HH:mm:ss
serialization:
# 格式化输出
indent_output: false
# 忽略无法转换的对象
fail_on_empty_beans: false
deserialization:
# 允许对象忽略json中不存在的属性
fail_on_unknown_properties: false
# redis 配置
data:
redis:
# 地址
host: localhost
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# ==================== CollisionAvoidanceSystem 配置整合 ====================
# Bean覆盖配置支持collision模块的适配器
main:
allow-bean-definition-overriding: true
# JPA配置collision模块空间数据处理
jpa:
hibernate:
ddl-auto: none # 使用若依的数据库管理方式
show-sql: false
properties:
hibernate:
format_sql: false
jdbc:
lob:
non_contextual_creation: true
batch_size: 50
fetch_size: 50
cache:
use_second_level_cache: false
use_query_cache: false
order_inserts: true
order_updates: true
batch_versioned_data: true
generate_statistics: false
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期默认30分钟
expireTime: 30
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.qaup.system.domain,com.qaup.common.core.domain.entity,com.qaup.generator.domain,com.qaup.quartz.domain,com.qaup.collision.common.model.spatial,com.qaup.collision.datacollector.model.dto,com.qaup.collision.geofence.model.entity
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
# PageHelper分页插件
pagehelper:
helperDialect: postgresql
reasonable: true
supportMethodsArguments: true
params: count=countSql
# Swagger配置
swagger:
# 是否开启swagger
enabled: true
# 请求前缀
pathMapping: /dev-api
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 数据采集配置collision模块
data:
collector:
# 数据采集间隔单位毫秒优化250ms超高频采集进一步减少停顿
interval: 250
# WebSocket推送节流配置1000ms推送间隔避免前端过载
websocket:
push-interval: 1000
# 机场数据源配置
airport-api:
base-url: http://localhost:8090
endpoints:
login: /login
aircraft: /openApi/getCurrentFlightPositions
vehicle: /openApi/getCurrentVehiclePositions
refresh: /refresh
auth:
username: dianxin
password: dianxin@123
# 无人车厂商数据源配置
vehicle-api:
base-url: http://localhost:8090
endpoints:
vehicle-location: /api/VehicleLocationInfo
vehicle-state: /api/VehicleStateInfo
vehicle-command: /api/VehicleCommandInfo
timeout: 5000
retry-attempts: 3
# 无人车数据持久化配置
unmanned-vehicle:
persistence:
enabled: true
batch-size: 50
location-retention-days: 90
command-retention-days: 365
command:
timeout: 5000
retry-attempts: 3
validation:
enabled: true
strict-mode: false
retention:
redis-expire-seconds: 60
postgresql-days: 30
# 坐标系统配置collision模块
coordinate-system:
airport:
center-longitude: 120.0834104
center-latitude: 36.35406879
# 性能监控配置collision模块扩展
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
metrics:
export:
simple:
enabled: true
enable:
hikari: true
jvm: true
jmx:
enabled: true
# 项目相关配置
qaup:
# 名称
name: Qaup
# 版本
version: 3.8.9
# 版权年份
copyrightYear: 2025
# 文件路径 示例( Windows配置D:/qaup/uploadPathLinux配置 /home/qaup/uploadPath
profile: Users/tianjianyong/apps/OpenSource/Qaup-Vue-Postgresql/uploadPath
# 获取ip地址开关
addressEnabled: false
# 验证码类型 math 数字计算 char 字符验证
captchaType: math
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
port: 8080
servlet:
# 应用的访问路径
context-path: /
tomcat:
# tomcat的URI编码
uri-encoding: UTF-8
# 连接数满后的排队数默认为100
accept-count: 1000
threads:
# tomcat最大线程数默认为200
max: 800
# Tomcat启动初始化的线程数默认值10
min-spare: 100
# 日志配置
logging:
level:
com.qaup: debug
org.springframework: warn
# 用户配置
user:
password:
# 密码最大错误次数
maxRetryCount: 5
# 密码锁定时间默认10分钟
lockTime: 10
# Spring配置
spring:
# 资源信息
messages:
# 国际化资源文件路径
basename: i18n/messages
profiles:
active: dev,druid
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
# 设置总上传的文件大小
max-request-size: 20MB
# 服务模块
devtools:
restart:
# 热部署开关
enabled: true
# 重启目录
additional-paths: src/main/java
# 排除目录
exclude: WEB-INF/**
jackson:
# 日期格式化
date-format: yyyy-MM-dd HH:mm:ss
serialization:
# 格式化输出
indent_output: false
# 忽略无法转换的对象
fail_on_empty_beans: false
deserialization:
# 允许对象忽略json中不存在的属性
fail_on_unknown_properties: false
# redis 配置
data:
redis:
# 地址
host: localhost
# 端口默认为6379
port: 6379
# 数据库索引
database: 0
# 密码
password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# ==================== CollisionAvoidanceSystem 配置整合 ====================
# Bean覆盖配置支持collision模块的适配器
main:
allow-bean-definition-overriding: true
# JPA配置collision模块空间数据处理
jpa:
hibernate:
ddl-auto: none # 使用若依的数据库管理方式
show-sql: false
properties:
hibernate:
format_sql: false
jdbc:
lob:
non_contextual_creation: true
batch_size: 50
fetch_size: 50
cache:
use_second_level_cache: false
use_query_cache: false
order_inserts: true
order_updates: true
batch_versioned_data: true
generate_statistics: false
# token配置
token:
# 令牌自定义标识
header: Authorization
# 令牌密钥
secret: abcdefghijklmnopqrstuvwxyz
# 令牌有效期默认30分钟
expireTime: 30
# MyBatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.qaup.system.domain,com.qaup.common.core.domain.entity,com.qaup.generator.domain,com.qaup.quartz.domain,com.qaup.collision.common.model.spatial,com.qaup.collision.datacollector.model.dto,com.qaup.collision.geofence.model.entity
# 配置mapper的扫描找到所有的mapper.xml映射文件
mapperLocations: classpath*:mapper/**/*Mapper.xml
# 加载全局的配置文件
configLocation: classpath:mybatis/mybatis-config.xml
# PageHelper分页插件
pagehelper:
helperDialect: postgresql
reasonable: true
supportMethodsArguments: true
params: count=countSql
# SpringDoc 配置
springdoc:
swagger-ui:
path: /swagger-ui.html # Swagger UI 访问路径
url: /v3/api-docs # OpenAPI JSON 文档路径
disable-swagger-default-url: true
api-docs:
path: /v3/api-docs # OpenAPI JSON 文档路径
# 防止XSS攻击
xss:
# 过滤开关
enabled: true
# 排除链接(多个用逗号分隔)
excludes: /system/notice
# 匹配链接
urlPatterns: /system/*,/monitor/*,/tool/*
# 数据采集配置collision模块
data:
collector:
# 数据采集间隔单位毫秒优化250ms超高频采集进一步减少停顿
interval: 250
# WebSocket推送节流配置1000ms推送间隔避免前端过载
websocket:
push-interval: 1000
# 机场数据源配置
airport-api:
base-url: http://localhost:8090
endpoints:
login: /login
aircraft: /openApi/getCurrentFlightPositions
vehicle: /openApi/getCurrentVehiclePositions
refresh: /refresh
auth:
username: dianxin
password: dianxin@123
# 无人车厂商数据源配置
vehicle-api:
base-url: http://localhost:8090
endpoints:
vehicle-location: /api/VehicleLocationInfo
vehicle-state: /api/VehicleStateInfo
vehicle-command: /api/VehicleCommandInfo
timeout: 5000
retry-attempts: 3
# 无人车数据持久化配置
unmanned-vehicle:
persistence:
enabled: true
batch-size: 50
location-retention-days: 90
command-retention-days: 365
command:
timeout: 5000
retry-attempts: 3
validation:
enabled: true
strict-mode: false
retention:
redis-expire-seconds: 60
postgresql-days: 30
# 坐标系统配置collision模块
coordinate-system:
airport:
center-longitude: 120.0834104
center-latitude: 36.35406879
# 性能监控配置collision模块扩展
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
metrics:
export:
simple:
enabled: true
enable:
hikari: true
jvm: true
jmx:
enabled: true

View File

@ -147,6 +147,7 @@ public class QuapDataAdapter {
/**
* 将若依车辆信息转换为CollisionAvoidanceSystem需要的VehicleLocation格式
* 注意此方法只转换基础信息位置和时间信息需要从实时数据中获取
* 注意车牌号和车辆类型现在不存储在VehicleLocation中需要通过vehicleId关联查询
*
* @param sysVehicleInfo 若依车辆信息
* @param location 实时位置
@ -162,13 +163,9 @@ public class QuapDataAdapter {
VehicleLocation vehicleLocation = new VehicleLocation();
vehicleLocation.setVehicleId(sysVehicleInfo.getVehicleId());
vehicleLocation.setLicensePlate(sysVehicleInfo.getLicensePlate());
vehicleLocation.setLocation(location);
vehicleLocation.setTimestamp(timestamp);
// 根据类型ID设置车辆类型
vehicleLocation.setVehicleType(convertToMovingObjectType(sysVehicleInfo.getTypeId()));
return vehicleLocation;
}

View File

@ -1,7 +1,6 @@
package com.qaup.collision.common.model.repository;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.model.MovingObjectType;
import org.locationtech.jts.geom.Point;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
@ -28,19 +27,27 @@ public interface VehicleLocationRepository extends JpaRepository<VehicleLocation
Optional<VehicleLocation> findLatestByVehicleId(@Param("vehicleId") Long vehicleId);
/**
* 根据车牌号查找最新位置记录
* 根据车牌号查找最新位置记录通过关联查询
*/
@Query(value = "SELECT * FROM vehicle_locations vl WHERE vl.license_plate = :licensePlate " +
@Query(value = "SELECT vl.* FROM vehicle_locations vl " +
"JOIN sys_vehicle_info vi ON vl.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate " +
"ORDER BY vl.timestamp DESC LIMIT 1",
nativeQuery = true)
Optional<VehicleLocation> findLatestByLicensePlate(@Param("licensePlate") String licensePlate);
/**
* 根据车辆类型查找活跃车辆
* 根据车辆类型查找活跃车辆通过关联查询
* 注意由于车辆类型现在在sys_vehicle_info表中需要通过关联查询
*/
@Query("SELECT vl FROM VehicleLocation vl WHERE vl.vehicleType = :vehicleType " +
"AND vl.timestamp >= :since ORDER BY vl.timestamp DESC")
List<VehicleLocation> findActiveByVehicleType(@Param("vehicleType") MovingObjectType vehicleType,
@Query(value = "SELECT vl.* FROM vehicle_locations vl " +
"JOIN sys_vehicle_info vi ON vl.vehicle_id = vi.vehicle_id " +
"JOIN sys_vehicle_type vt ON vi.type_id = vt.type_id " +
"WHERE vt.type_name LIKE :typeName " +
"AND vl.timestamp >= :since " +
"ORDER BY vl.timestamp DESC",
nativeQuery = true)
List<VehicleLocation> findActiveByVehicleType(@Param("typeName") String typeName,
@Param("since") LocalDateTime since);
/**
@ -79,11 +86,14 @@ public interface VehicleLocationRepository extends JpaRepository<VehicleLocation
@Param("endTime") LocalDateTime endTime);
/**
* 根据车牌号和时间范围查询轨迹数据
* 根据车牌号和时间范围查询轨迹数据通过关联查询
*/
@Query("SELECT vl FROM VehicleLocation vl WHERE vl.licensePlate = :licensePlate " +
"AND vl.timestamp BETWEEN :startTime AND :endTime " +
"ORDER BY vl.timestamp")
@Query(value = "SELECT vl.* FROM vehicle_locations vl " +
"JOIN sys_vehicle_info vi ON vl.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate " +
"AND vl.timestamp BETWEEN :startTime AND :endTime " +
"ORDER BY vl.timestamp",
nativeQuery = true)
List<VehicleLocation> findVehicleTrajectoryByLicensePlate(@Param("licensePlate") String licensePlate,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);

View File

@ -1,7 +1,6 @@
package com.qaup.collision.common.model.spatial;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.common.model.MovementState;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
@ -12,10 +11,10 @@ import org.locationtech.jts.geom.Point;
import java.time.LocalDateTime;
/**
* 车辆位置PostGIS实体类
* 车辆位置PostGIS实体类规范化版本
*
* 用于存储车辆的实时位置信息替代原有的内存存储模式
* 支持PostGIS空间查询和索引优化
* 用于存储车辆的实时位置信息符合数据库规范化设计
* 只存储位置相关数据车辆基础信息通过关联查询获取
*/
@Entity
@Table(name = "vehicle_locations")
@ -35,19 +34,6 @@ public class VehicleLocation {
@Column(name = "vehicle_id", nullable = false)
private Long vehicleId;
/**
* 车牌号业务标识符
*/
@Column(name = "license_plate", nullable = false, length = 50)
private String licensePlate;
/**
* 车辆类型枚举
*/
@Enumerated(EnumType.STRING)
@Column(name = "vehicle_type", nullable = false, length = 20)
private MovingObjectType vehicleType;
/**
* 位置点 - 使用PostGIS POINT类型
* SRID 4326表示WGS84坐标系统GPS坐标
@ -80,11 +66,10 @@ public class VehicleLocation {
private LocalDateTime timestamp;
/**
* 数据质量枚举
* 数据质量标识
*/
@Enumerated(EnumType.STRING)
@Column(name = "data_quality", length = 20)
private MovementState.DataQuality dataQuality;
private String dataQuality;
/**
* 创建时间
@ -98,6 +83,24 @@ public class VehicleLocation {
@Column(name = "updated_at")
private LocalDateTime updatedAt;
// ============================================
// 运行时属性 - 不存储在数据库中
// ============================================
/**
* 车辆类型运行时属性不存储在数据库中
* 通过QuapDataAdapter从sys_vehicle_info表获取
*/
@Transient
private MovingObjectType vehicleType;
/**
* 车牌号运行时属性不存储在数据库中
* 通过QuapDataAdapter从sys_vehicle_info表获取
*/
@Transient
private String licensePlate;
/**
* 预设置创建和更新时间
*/
@ -111,4 +114,51 @@ public class VehicleLocation {
protected void onUpdate() {
updatedAt = LocalDateTime.now();
}
// ============================================
// 便捷方法
// ============================================
/**
* 设置车辆类型用于运行时设置
*
* @param vehicleType 车辆类型
*/
public void setVehicleType(MovingObjectType vehicleType) {
this.vehicleType = vehicleType;
}
/**
* 获取车辆类型
* 如果未设置运行时类型默认返回UNMANNED_VEHICLE因为只有无人车数据会持久化
*
* @return 车辆类型
*/
public MovingObjectType getVehicleType() {
// 如果已设置运行时类型返回该类型
if (this.vehicleType != null) {
return this.vehicleType;
}
// 由于数据库中只存储无人车数据默认返回UNMANNED_VEHICLE
return MovingObjectType.UNMANNED_VEHICLE;
}
/**
* 设置车牌号用于运行时设置
*
* @param licensePlate 车牌号
*/
public void setLicensePlate(String licensePlate) {
this.licensePlate = licensePlate;
}
/**
* 获取车牌号
* 需要通过QuapDataAdapter从sys_vehicle_info表获取
*
* @return 车牌号可能为null需要通过适配器获取
*/
public String getLicensePlate() {
return this.licensePlate;
}
}

View File

@ -184,49 +184,54 @@ public class VehicleLocationService {
/**
* 创建车辆位置记录集成QAUP数据适配器
*/
public VehicleLocation createVehicleLocation(String licensePlate, MovingObjectType vehicleType,
public VehicleLocation createVehicleLocation(String licensePlate, Long vehicleId,
double longitude, double latitude,
Double altitude, Double heading, Double speed) {
Point location = geometryFactory.createPoint(new Coordinate(longitude, latitude));
location.setSRID(4326); // 设置WGS84坐标系
VehicleLocation vehicleLocation = new VehicleLocation();
vehicleLocation.setLicensePlate(licensePlate);
vehicleLocation.setVehicleId(vehicleId); // 使用传入的数字车辆ID
vehicleLocation.setLocation(location);
vehicleLocation.setAltitude(altitude);
vehicleLocation.setHeading(heading);
vehicleLocation.setSpeed(speed);
vehicleLocation.setTimestamp(LocalDateTime.now());
vehicleLocation.setDataQuality("GOOD"); // 默认数据质量
// 设置运行时属性
vehicleLocation.setLicensePlate(licensePlate);
vehicleLocation.setVehicleType(MovingObjectType.UNMANNED_VEHICLE); // 只有无人车数据会持久化
log.debug("创建车辆位置记录: vehicleId={}, licensePlate={}, location=({}, {})",
vehicleId, licensePlate, longitude, latitude);
return vehicleLocation;
}
/**
* 创建车辆位置记录通过车牌号自动查找车辆ID
*/
public VehicleLocation createVehicleLocationByLicensePlate(String licensePlate,
double longitude, double latitude,
Double altitude, Double heading, Double speed) {
// 使用QuapDataAdapter获取车辆基础信息
try {
Optional<SysVehicleInfo> vehicleInfoOpt = quapDataAdapter.findVehicleByLicensePlate(licensePlate);
if (vehicleInfoOpt.isPresent()) {
SysVehicleInfo vehicleInfo = vehicleInfoOpt.get();
// 设置vehicleId数字主键
vehicleLocation.setVehicleId(vehicleInfo.getVehicleId());
// 如果没有明确指定车辆类型从系统自动获取
if (vehicleType == null) {
vehicleLocation.setVehicleType(quapDataAdapter.convertToMovingObjectType(vehicleInfo.getTypeId()));
} else {
vehicleLocation.setVehicleType(vehicleType);
}
log.debug("通过QAUP适配器获取车辆信息: vehicleId={}, licensePlate={}, type={}",
vehicleInfo.getVehicleId(), licensePlate, vehicleLocation.getVehicleType());
return createVehicleLocation(licensePlate, vehicleInfo.getVehicleId(),
longitude, latitude, altitude, heading, speed);
} else {
// 车辆信息不存在记录警告但不阻止位置记录创建
vehicleLocation.setVehicleType(vehicleType != null ? vehicleType : MovingObjectType.UNKNOWN);
log.warn("未找到车辆基础信息: licensePlate={}, 使用类型: {}", licensePlate, vehicleLocation.getVehicleType());
log.warn("未找到车辆基础信息: licensePlate={}", licensePlate);
// 返回null或者抛出异常根据业务需求决定
return null;
}
} catch (Exception e) {
// 适配器查询失败使用传入的车辆类型不阻止位置记录创建
vehicleLocation.setVehicleType(vehicleType != null ? vehicleType : MovingObjectType.UNKNOWN);
log.error("查询车辆基础信息失败: licensePlate={}, 错误: {}", licensePlate, e.getMessage());
log.error("通过车牌号查找车辆信息失败: licensePlate={}", licensePlate, e);
return null;
}
return vehicleLocation;
}
/**
@ -313,12 +318,30 @@ public class VehicleLocationService {
public List<VehicleLocation> getActiveVehiclesByType(MovingObjectType vehicleType, int minutesBack) {
LocalDateTime since = LocalDateTime.now().minusMinutes(minutesBack);
try {
return vehicleLocationRepository.findActiveByVehicleType(vehicleType, since);
// 将MovingObjectType转换为数据库中的类型名称模式
String typeName = convertMovingObjectTypeToTypeName(vehicleType);
return vehicleLocationRepository.findActiveByVehicleType(typeName, since);
} catch (Exception e) {
log.error("获取活跃车辆失败: vehicleType={}, minutesBack={}", vehicleType, minutesBack, e);
return List.of();
}
}
/**
* 将MovingObjectType转换为数据库中的类型名称模式
*/
private String convertMovingObjectTypeToTypeName(MovingObjectType vehicleType) {
switch (vehicleType) {
case UNMANNED_VEHICLE:
return "%无人车%";
case AIRPORT_VEHICLE:
return "%车"; // 匹配包含""但不包含"无人车"的类型
case AIRCRAFT:
return "%飞机%";
default:
return "%"; // 匹配所有类型
}
}
/**
* 空间查询获取指定半径范围内的车辆
@ -447,12 +470,16 @@ public class VehicleLocationService {
* 更新车辆位置 (如果存在则更新否则创建新记录)集成规则检测
*/
@Transactional
public VehicleLocation updateOrCreateVehicleLocation(String licensePlate, MovingObjectType vehicleType,
public VehicleLocation updateOrCreateVehicleLocation(String licensePlate,
double longitude, double latitude,
Double altitude, Double heading, Double speed) {
VehicleLocation vehicleLocation = createVehicleLocation(licensePlate, vehicleType,
VehicleLocation vehicleLocation = createVehicleLocationByLicensePlate(licensePlate,
longitude, latitude,
altitude, heading, speed);
if (vehicleLocation == null) {
log.warn("无法创建车辆位置记录,车牌号不存在: {}", licensePlate);
return null;
}
vehicleLocation.setTimestamp(LocalDateTime.now());
VehicleLocation saved = saveVehicleLocation(vehicleLocation);
@ -535,8 +562,8 @@ public class VehicleLocationService {
// 详细记录每个违规事件
for (RuleViolationEvent violation : violations) {
log.warn("违规详情: 车牌={}, 规则={}, 违规类型={}, 描述={}",
violation.getVehicleLicense(), violation.getRuleName(),
log.warn("违规详情: vehicleId={}, 规则={}, 违规类型={}, 描述={}",
violation.getVehicleId(), violation.getRuleName(),
violation.getViolationType(), violation.getDescription());
}
} else {
@ -585,14 +612,14 @@ public class VehicleLocationService {
log.warn("智能检测发现违规: 总违规数量={}", allViolations.size());
// 按车辆分组显示违规统计
Map<String, Long> violationsByVehicle = allViolations.stream()
Map<Long, Long> violationsByVehicle = allViolations.stream()
.collect(java.util.stream.Collectors.groupingBy(
RuleViolationEvent::getVehicleLicense,
RuleViolationEvent::getVehicleId,
java.util.stream.Collectors.counting()
));
violationsByVehicle.forEach((licensePlate, count) ->
log.warn("车辆违规统计: licensePlate={}, 违规数量={}", licensePlate, count)
violationsByVehicle.forEach((vehicleId, count) ->
log.warn("车辆违规统计: vehicleId={}, 违规数量={}", vehicleId, count)
);
} else {
log.info("智能检测未发现违规");
@ -649,10 +676,8 @@ public class VehicleLocationService {
* 获取车辆唯一标识
*/
private String getVehicleKey(VehicleLocation vehicleLocation) {
// 使用车牌号作为唯一标识更稳定
return vehicleLocation.getLicensePlate() != null ?
vehicleLocation.getLicensePlate() :
String.valueOf(vehicleLocation.getVehicleId());
// 使用车辆ID作为唯一标识
return String.valueOf(vehicleLocation.getVehicleId());
}
/**
@ -730,10 +755,20 @@ public class VehicleLocationService {
*/
private void triggerRuleDetectionForSingle(VehicleLocation vehicleLocation) {
try {
// 获取车辆信息以确定车辆类型
Optional<SysVehicleInfo> vehicleInfoOpt = quapDataAdapter.findVehicleById(vehicleLocation.getVehicleId());
if (vehicleInfoOpt.isEmpty()) {
log.warn("无法获取车辆信息,跳过规则检测: vehicleId={}", vehicleLocation.getVehicleId());
return;
}
SysVehicleInfo vehicleInfo = vehicleInfoOpt.get();
MovingObjectType vehicleType = quapDataAdapter.convertToMovingObjectType(vehicleInfo.getTypeId());
// 查询该位置适用的规则
List<SpatialRule> applicableRules = locationRuleQueryService.findApplicableRules(
vehicleLocation.getLocation(),
vehicleLocation.getVehicleType(),
vehicleType,
LocalDateTime.now()
);
@ -745,13 +780,13 @@ public class VehicleLocationService {
for (SpatialRule rule : applicableRules) {
try {
// 构建车辆状态参数
Map<String, Object> vehicleState = buildVehicleStateMap(vehicleLocation);
Map<String, Object> vehicleState = buildVehicleStateMap(vehicleLocation, vehicleInfo, vehicleType);
// 修复executeRule参数类型使用licensePlate作为vehicleIdentifier
// 使用licensePlate作为vehicleIdentifier
RuleExecutionResult result = ruleExecutionEngine.executeRule(
rule,
vehicleLocation.getLicensePlate(),
vehicleLocation.getVehicleType(),
vehicleInfo.getLicensePlate(),
vehicleType,
vehicleLocation.getLocation(),
vehicleState,
vehicleLocation.getTimestamp()
@ -759,18 +794,17 @@ public class VehicleLocationService {
if (result.requiresAction()) {
log.info("检测发现问题: vehicleId={}, licensePlate={}, ruleId={}, result={}",
vehicleLocation.getVehicleId(), vehicleLocation.getLicensePlate(),
vehicleLocation.getVehicleId(), vehicleInfo.getLicensePlate(),
rule.getRuleId(), result);
}
} catch (Exception e) {
log.error("规则执行失败: vehicleId={}, licensePlate={}, ruleId={}",
vehicleLocation.getVehicleId(), vehicleLocation.getLicensePlate(), rule.getRuleId(), e);
vehicleLocation.getVehicleId(), vehicleInfo.getLicensePlate(), rule.getRuleId(), e);
}
}
} catch (Exception e) {
log.error("单个车辆规则检测失败: vehicleId={}, licensePlate={}",
vehicleLocation.getVehicleId(), vehicleLocation.getLicensePlate(), e);
log.error("单个车辆规则检测失败: vehicleId={}", vehicleLocation.getVehicleId(), e);
}
}
@ -814,12 +848,13 @@ public class VehicleLocationService {
/**
* 构建车辆状态参数Map用于规则执行
*/
private Map<String, Object> buildVehicleStateMap(VehicleLocation vehicleLocation) {
private Map<String, Object> buildVehicleStateMap(VehicleLocation vehicleLocation, SysVehicleInfo vehicleInfo, MovingObjectType vehicleType) {
Map<String, Object> vehicleState = new HashMap<>();
// 基本位置信息
vehicleState.put("vehicleId", vehicleLocation.getVehicleId());
vehicleState.put("vehicleType", vehicleLocation.getVehicleType());
vehicleState.put("vehicleType", vehicleType);
vehicleState.put("licensePlate", vehicleInfo.getLicensePlate());
vehicleState.put("timestamp", vehicleLocation.getTimestamp());
// 位置坐标

View File

@ -389,8 +389,8 @@ public class DataCollectorService {
}
// 使用VehicleLocationService创建VehicleLocation对象
return vehicleLocationService.createVehicleLocation(
licensePlate, vehicleType, longitude, latitude,
return vehicleLocationService.createVehicleLocationByLicensePlate(
licensePlate, longitude, latitude,
altitude, heading, speed
);

View File

@ -143,7 +143,7 @@ public class UnmannedVehicleControlService {
// 由于数据库中只存储无人车数据直接查询即可
LocalDateTime since = LocalDateTime.now().minusMinutes(5);
locations = vehicleLocationRepository.findActiveByVehicleType(
MovingObjectType.UNMANNED_VEHICLE, since);
"%无人车%", since);
logger.info("查询到所有无人车位置: count={}", locations.size());
}

View File

@ -54,7 +54,7 @@ public class ThreatLevelEventService {
emergencyEventCount.incrementAndGet();
break;
default:
logger.warn("未知的威胁级别: {}, 车: {}", threatLevel, violationEvent.getVehicleLicense());
logger.warn("未知的威胁级别: {}, 车辆ID: {}", threatLevel, violationEvent.getVehicleId());
}
}
@ -113,7 +113,7 @@ public class ThreatLevelEventService {
private void handleInfoLevelEvent(RuleViolationEvent violationEvent) {
logger.info("INFO级违规事件: vehicleId={}, ruleId={}, description={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName(),
violationEvent.getVehicleId(), violationEvent.getRuleName(),
violationEvent.getDescription());
// 信息级事件只记录日志通过WebSocket推送到前端
@ -128,7 +128,7 @@ public class ThreatLevelEventService {
private void handleWarningLevelEvent(RuleViolationEvent violationEvent) {
logger.warn("WARNING级违规事件: vehicleId={}, ruleId={}, description={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName(),
violationEvent.getVehicleId(), violationEvent.getRuleName(),
violationEvent.getDescription());
// 警告级事件记录详细日志并通过WebSocket推送
@ -143,7 +143,7 @@ public class ThreatLevelEventService {
private void handleCriticalLevelEvent(RuleViolationEvent violationEvent) {
logger.error("CRITICAL级违规事件: vehicleId={}, ruleId={}, severity={}, description={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName(),
violationEvent.getVehicleId(), violationEvent.getRuleName(),
violationEvent.getViolationType(), violationEvent.getDescription());
// 严重级事件需要特别关注
@ -162,7 +162,7 @@ public class ThreatLevelEventService {
private void handleEmergencyLevelEvent(RuleViolationEvent violationEvent) {
logger.error("EMERGENCY级违规事件: vehicleId={}, ruleId={}, severity={}, location={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName(),
violationEvent.getVehicleId(), violationEvent.getRuleName(),
violationEvent.getViolationType(), violationEvent.getLocation());
// 紧急级事件需要立即响应
@ -196,7 +196,7 @@ public class ThreatLevelEventService {
private void handleEmergencyEventResponse(RuleViolationEvent violationEvent) {
// 紧急事件的响应处理
logger.error("触发紧急事件响应流程: id={}, vehicleId={}",
violationEvent.getId(), violationEvent.getVehicleLicense());
violationEvent.getId(), violationEvent.getVehicleId());
// 紧急事件需要立即响应
// - 可能需要立即停止相关车辆
@ -218,7 +218,7 @@ public class ThreatLevelEventService {
private void handleEmergencyResponse(RuleViolationEvent violationEvent) {
// 紧急响应处理
logger.error("执行紧急响应: vehicleLicense={}, location={}",
violationEvent.getVehicleLicense(), violationEvent.getLocation());
violationEvent.getVehicleId(), violationEvent.getLocation());
// 紧急响应逻辑
// 1. 记录到紧急事件数据库

View File

@ -62,8 +62,8 @@ public class RuleEventListener {
violationEventCount.incrementAndGet();
long processingTime = System.currentTimeMillis() - startTime;
log.debug("违规事件处理完成: 车牌={}, processingTime={}ms",
violationEvent.getVehicleLicense(), processingTime);
log.debug("违规事件处理完成: vehicleId={}, processingTime={}ms",
violationEvent.getVehicleId(), processingTime);
} catch (Exception e) {
log.error("处理规则违规事件失败: event={}, error={}", event, e.getMessage(), e);
@ -159,8 +159,8 @@ public class RuleEventListener {
* 处理高优先级违规事件
*/
private void handleHighPriorityViolation(RuleViolationEvent violationEvent, RuleViolationEventOccurred event) {
log.warn("处理高优先级违规事件: 车牌={}, 规则={}, priority={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName(), event.getPriority());
log.warn("处理高优先级违规事件: vehicleId={}, 规则={}, priority={}",
violationEvent.getVehicleId(), violationEvent.getRuleName(), event.getPriority());
// 立即处理违规事件
ruleViolationProcessor.processViolationEvent(violationEvent);
@ -178,8 +178,8 @@ public class RuleEventListener {
* 处理普通违规事件
*/
private void handleNormalViolation(RuleViolationEvent violationEvent, RuleViolationEventOccurred event) {
log.debug("处理普通违规事件: 车牌={}, 规则={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName());
log.debug("处理普通违规事件: vehicleId={}, 规则={}",
violationEvent.getVehicleId(), violationEvent.getRuleName());
// 同步处理违规事件简化架构
ruleViolationProcessor.processViolationEvent(violationEvent);
@ -189,17 +189,17 @@ public class RuleEventListener {
* 处理紧急违规事件
*/
private void handleCriticalViolation(RuleViolationEvent violationEvent, RuleViolationEventOccurred event) {
log.error("处理紧急违规事件: 车牌={}, 规则={}, 违规类型={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName(), violationEvent.getViolationType());
log.error("处理紧急违规事件: vehicleId={}, 规则={}, 违规类型={}",
violationEvent.getVehicleId(), violationEvent.getRuleName(), violationEvent.getViolationType());
try {
// 1. 立即通知相关人员
log.error("CRITICAL VIOLATION ALERT: 车辆 {} 在规则 {} 发生紧急违规",
violationEvent.getVehicleLicense(), violationEvent.getRuleName());
violationEvent.getVehicleId(), violationEvent.getRuleName());
// 2. 记录到紧急事件日志简化实现
log.error("EMERGENCY_LOG: eventId={}, vehicleId={}, ruleId={}, location={}, severity={}",
violationEvent.getId(), violationEvent.getVehicleLicense(),
violationEvent.getId(), violationEvent.getVehicleId(),
violationEvent.getRuleName(), violationEvent.getLocation(),
violationEvent.getViolationType());
@ -360,7 +360,7 @@ public class RuleEventListener {
private void handleEmergencyResponse(RuleViolationEvent violationEvent) {
try {
log.error("EMERGENCY_RESPONSE: 触发紧急响应程序 - 车辆:{}, 规则:{}, 违规类型:{}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName(), violationEvent.getViolationType());
violationEvent.getVehicleId(), violationEvent.getRuleName(), violationEvent.getViolationType());
// 基本的紧急响应逻辑
// 1. 记录紧急事件

View File

@ -19,7 +19,7 @@ import java.util.Objects;
*/
@Entity
@Table(name = "unmanned_vehicle_violations", indexes = {
@Index(name = "idx_uv_violations_vehicle", columnList = "vehicle_license"),
@Index(name = "idx_uv_violations_vehicle", columnList = "vehicle_id"),
@Index(name = "idx_uv_violations_rule", columnList = "rule_name"),
@Index(name = "idx_uv_violations_time", columnList = "violation_time")
})
@ -34,10 +34,11 @@ public class RuleViolationEvent {
private Long id;
/**
* 无人车车牌号
* 车辆ID关联sys_vehicle_info.vehicle_id
* 内部逻辑处理完全基于此字段
*/
@Column(name = "vehicle_license", nullable = false, length = 20)
private String vehicleLicense;
@Column(name = "vehicle_id", nullable = false)
private Long vehicleId;
/**
* 违规规则名称简化直接存储规则名称
@ -96,10 +97,10 @@ public class RuleViolationEvent {
this.createdAt = LocalDateTime.now(); // 手动设置创建时间避免审计功能依赖
}
public RuleViolationEvent(String vehicleLicense, String ruleName, String violationType,
public RuleViolationEvent(Long vehicleId, String ruleName, String violationType,
String description) {
this();
this.vehicleLicense = vehicleLicense;
this.vehicleId = vehicleId;
this.ruleName = ruleName;
this.violationType = violationType;
this.description = description;
@ -107,24 +108,32 @@ public class RuleViolationEvent {
// 业务方法
public boolean isSpeedViolation() {
return "SPEED".equals(violationType);
return "SPEED_VIOLATION".equals(violationType);
}
public boolean isAccessViolation() {
return "ACCESS".equals(violationType);
return "ACCESS_VIOLATION".equals(violationType);
}
public boolean isHeightViolation() {
return "HEIGHT_VIOLATION".equals(violationType);
}
public boolean isWeightViolation() {
return "WEIGHT_VIOLATION".equals(violationType);
}
public String getViolationSummary() {
return String.format("车辆[%s]在[%s]违反了[%s]规则",
vehicleLicense, violationTime.toString(), ruleName);
return String.format("车辆ID[%d]在[%s]违反了[%s]规则",
vehicleId, violationTime.toString(), ruleName);
}
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getVehicleLicense() { return vehicleLicense; }
public void setVehicleLicense(String vehicleLicense) { this.vehicleLicense = vehicleLicense; }
public Long getVehicleId() { return vehicleId; }
public void setVehicleId(Long vehicleId) { this.vehicleId = vehicleId; }
public String getRuleName() { return ruleName; }
public void setRuleName(String ruleName) { this.ruleName = ruleName; }
@ -165,7 +174,15 @@ public class RuleViolationEvent {
@Override
public String toString() {
return String.format("UnmannedVehicleViolation{id=%d, vehicle='%s', rule='%s', type='%s', time=%s}",
id, vehicleLicense, ruleName, violationType, violationTime);
return "RuleViolationEvent{" +
"id=" + id +
", vehicleId=" + vehicleId +
", ruleName='" + ruleName + '\'' +
", violationType='" + violationType + '\'' +
", description='" + description + '\'' +
", actualValue=" + actualValue +
", limitValue=" + limitValue +
", violationTime=" + violationTime +
'}';
}
}

View File

@ -143,8 +143,8 @@ public class RuleViolationEventOccurred extends ApplicationEvent {
* 获取事件描述
*/
public String getEventDescription() {
return String.format("规则违规事件 - 车辆ID: %s, 规则ID: %s, 违规类型: %s, 优先级: %s",
violationEvent.getVehicleLicense(),
return String.format("规则违规事件 - 车辆ID: %s, 规则名称: %s, 违规类型: %s, 优先级: %s",
violationEvent.getVehicleId(),
violationEvent.getRuleName(),
violationEvent.getViolationType(),
priority.name());
@ -169,13 +169,13 @@ public class RuleViolationEventOccurred extends ApplicationEvent {
return String.format("RuleViolationEventOccurred{" +
"eventId='%s', " +
"vehicleId='%s', " +
"ruleId='%s', " +
"ruleName='%s', " +
"priority=%s, " +
"timestamp=%s, " +
"requiresImmediateAction=%s" +
"}",
violationEvent.getId(),
violationEvent.getVehicleLicense(),
violationEvent.getVehicleId(),
violationEvent.getRuleName(),
priority.name(),
eventTimestamp,

View File

@ -22,11 +22,23 @@ import java.util.List;
public interface RuleViolationEventRepository extends JpaRepository<RuleViolationEvent, String> {
/**
* 根据车牌号查询违规事件
* 根据车牌号查询违规事件通过关联sys_vehicle_info表
* @param vehicleLicense 车牌号
* @return 指定车牌的违规事件列表
*/
List<RuleViolationEvent> findByVehicleLicense(String vehicleLicense);
@Query(value = "SELECT e.* FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate " +
"ORDER BY e.violation_time DESC",
nativeQuery = true)
List<RuleViolationEvent> findByVehicleLicense(@Param("licensePlate") String vehicleLicense);
/**
* 根据车辆ID查询违规事件
* @param vehicleId 车辆ID
* @return 指定车辆ID的违规事件列表
*/
List<RuleViolationEvent> findByVehicleId(Long vehicleId);
/**
* 根据规则名称查询违规事件
@ -51,15 +63,21 @@ public interface RuleViolationEventRepository extends JpaRepository<RuleViolatio
List<RuleViolationEvent> findByViolationTimeBetween(LocalDateTime startTime, LocalDateTime endTime);
/**
* 查询指定车牌在时间范围内的违规事件
* 查询指定车牌在时间范围内的违规事件通过关联sys_vehicle_info表
* @param vehicleLicense 车牌号
* @param startTime 开始时间
* @param endTime 结束时间
* @return 指定车牌在时间范围内的违规事件列表
*/
List<RuleViolationEvent> findByVehicleLicenseAndViolationTimeBetween(String vehicleLicense,
LocalDateTime startTime,
LocalDateTime endTime);
@Query(value = "SELECT e.* FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate " +
"AND e.violation_time BETWEEN :startTime AND :endTime " +
"ORDER BY e.violation_time DESC",
nativeQuery = true)
List<RuleViolationEvent> findByVehicleLicenseAndViolationTimeBetween(@Param("licensePlate") String vehicleLicense,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
/**
* 查询最近的违规事件限制数量
@ -71,17 +89,19 @@ public interface RuleViolationEventRepository extends JpaRepository<RuleViolatio
List<RuleViolationEvent> findRecentEvents(@Param("limit") Integer limit);
/**
* 查询车辆违规统计信息
* 查询车辆违规统计信息通过关联sys_vehicle_info表
* @param vehicleLicense 车牌号
* @param startTime 开始时间
* @param endTime 结束时间
* @return 各违规类型的数量统计
*/
@Query("SELECT e.violationType, COUNT(e) FROM RuleViolationEvent e " +
"WHERE e.vehicleLicense = :vehicleLicense " +
"AND e.violationTime BETWEEN :startTime AND :endTime " +
"GROUP BY e.violationType")
List<Object[]> getVehicleViolationStats(@Param("vehicleLicense") String vehicleLicense,
@Query(value = "SELECT e.violation_type, COUNT(e.*) FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate " +
"AND e.violation_time BETWEEN :startTime AND :endTime " +
"GROUP BY e.violation_type",
nativeQuery = true)
List<Object[]> getVehicleViolationStats(@Param("licensePlate") String vehicleLicense,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime);
@ -106,7 +126,7 @@ public interface RuleViolationEventRepository extends JpaRepository<RuleViolatio
* @param endTime 结束时间
* @return 每日违规事件数量
*/
@Query(value = "SELECT DATE_TRUNC('day', e.violation_time) as violation_date, COUNT(e) FROM unmanned_vehicle_violations e " +
@Query(value = "SELECT DATE_TRUNC('day', e.violation_time) as violation_date, COUNT(e.*) FROM unmanned_vehicle_violations e " +
"WHERE e.violation_time BETWEEN :startTime AND :endTime " +
"GROUP BY DATE_TRUNC('day', e.violation_time) " +
"ORDER BY DATE_TRUNC('day', e.violation_time)",
@ -128,31 +148,43 @@ public interface RuleViolationEventRepository extends JpaRepository<RuleViolatio
@Param("endTime") LocalDateTime endTime);
/**
* 查询特定车辆的重复违规同一规则多次违规
* 查询特定车辆的重复违规通过关联sys_vehicle_info表
* @param vehicleLicense 车牌号
* @param startTime 开始时间
* @param endTime 结束时间
* @param minCount 最小违规次数
* @return 重复违规的规则名称和次数
*/
@Query("SELECT e.ruleName, COUNT(e) FROM RuleViolationEvent e " +
"WHERE e.vehicleLicense = :vehicleLicense " +
"AND e.violationTime BETWEEN :startTime AND :endTime " +
"GROUP BY e.ruleName " +
"HAVING COUNT(e) >= :minCount " +
"ORDER BY COUNT(e) DESC")
List<Object[]> findRepeatedViolations(@Param("vehicleLicense") String vehicleLicense,
@Query(value = "SELECT e.rule_name, COUNT(e.*) FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate " +
"AND e.violation_time BETWEEN :startTime AND :endTime " +
"GROUP BY e.rule_name " +
"HAVING COUNT(e.*) >= :minCount " +
"ORDER BY COUNT(e.*) DESC",
nativeQuery = true)
List<Object[]> findRepeatedViolations(@Param("licensePlate") String vehicleLicense,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("minCount") Integer minCount);
/**
* 统计车辆的违规次数
* 统计车辆的违规次数通过关联sys_vehicle_info表
* @param vehicleLicense 车牌号
* @return 违规次数
*/
@Query("SELECT COUNT(e) FROM RuleViolationEvent e WHERE e.vehicleLicense = :vehicleLicense")
Long countByVehicleLicense(@Param("vehicleLicense") String vehicleLicense);
@Query(value = "SELECT COUNT(e.*) FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate",
nativeQuery = true)
Long countByVehicleLicense(@Param("licensePlate") String vehicleLicense);
/**
* 根据车辆ID统计违规次数
* @param vehicleId 车辆ID
* @return 违规次数
*/
Long countByVehicleId(Long vehicleId);
/**
* 统计今日违规事件数量
@ -165,12 +197,23 @@ public interface RuleViolationEventRepository extends JpaRepository<RuleViolatio
Long countTodayViolations(@Param("startOfDay") LocalDateTime startOfDay, @Param("endOfDay") LocalDateTime endOfDay);
/**
* 根据车牌和违规类型统计次数
* 根据车牌和违规类型统计次数通过关联sys_vehicle_info表
* @param vehicleLicense 车牌号
* @param violationType 违规类型
* @return 违规次数
*/
@Query("SELECT COUNT(e) FROM RuleViolationEvent e WHERE e.vehicleLicense = :vehicleLicense AND e.violationType = :violationType")
Long countByVehicleLicenseAndViolationType(@Param("vehicleLicense") String vehicleLicense,
@Query(value = "SELECT COUNT(e.*) FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate AND e.violation_type = :violationType",
nativeQuery = true)
Long countByVehicleLicenseAndViolationType(@Param("licensePlate") String vehicleLicense,
@Param("violationType") String violationType);
/**
* 根据车辆ID和违规类型统计次数
* @param vehicleId 车辆ID
* @param violationType 违规类型
* @return 违规次数
*/
Long countByVehicleIdAndViolationType(Long vehicleId, String violationType);
}

View File

@ -17,9 +17,19 @@ import java.util.List;
public interface SimpleRuleViolationEventRepository extends JpaRepository<RuleViolationEvent, Long> {
/**
* 根据车牌号查询违规事件
* 根据车牌号查询违规事件通过关联sys_vehicle_info表
*/
List<RuleViolationEvent> findByVehicleLicenseOrderByViolationTimeDesc(String vehicleLicense);
@Query(value = "SELECT e.* FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate " +
"ORDER BY e.violation_time DESC",
nativeQuery = true)
List<RuleViolationEvent> findByVehicleLicenseOrderByViolationTimeDesc(@Param("licensePlate") String licensePlate);
/**
* 根据车辆ID查询违规事件
*/
List<RuleViolationEvent> findByVehicleIdOrderByViolationTimeDesc(Long vehicleId);
/**
* 根据规则名称查询违规事件
@ -45,10 +55,18 @@ public interface SimpleRuleViolationEventRepository extends JpaRepository<RuleVi
String violationType, Double speedLimit);
/**
* 统计车辆的违规次数
* 统计车辆的违规次数通过关联sys_vehicle_info表
*/
@Query("SELECT COUNT(e) FROM RuleViolationEvent e WHERE e.vehicleLicense = :vehicleLicense")
Long countByVehicleLicense(@Param("vehicleLicense") String vehicleLicense);
@Query(value = "SELECT COUNT(e.*) FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate",
nativeQuery = true)
Long countByVehicleLicense(@Param("licensePlate") String licensePlate);
/**
* 根据车辆ID统计违规次数
*/
Long countByVehicleId(Long vehicleId);
/**
* 统计今日违规事件数量
@ -64,9 +82,17 @@ public interface SimpleRuleViolationEventRepository extends JpaRepository<RuleVi
List<RuleViolationEvent> findRecentViolations(@Param("since") LocalDateTime since);
/**
* 根据车牌和违规类型统计次数
* 根据车牌和违规类型统计次数通过关联sys_vehicle_info表
*/
@Query("SELECT COUNT(e) FROM RuleViolationEvent e WHERE e.vehicleLicense = :vehicleLicense AND e.violationType = :violationType")
Long countByVehicleLicenseAndViolationType(@Param("vehicleLicense") String vehicleLicense,
@Query(value = "SELECT COUNT(e.*) FROM unmanned_vehicle_violations e " +
"JOIN sys_vehicle_info vi ON e.vehicle_id = vi.vehicle_id " +
"WHERE vi.license_plate = :licensePlate AND e.violation_type = :violationType",
nativeQuery = true)
Long countByVehicleLicenseAndViolationType(@Param("licensePlate") String licensePlate,
@Param("violationType") String violationType);
/**
* 根据车辆ID和违规类型统计次数
*/
Long countByVehicleIdAndViolationType(Long vehicleId, String violationType);
}

View File

@ -24,7 +24,6 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
@ -203,7 +202,7 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
// 检查是否为重复违规
boolean isDuplicate = isDuplicateViolation(
violationEvent.getVehicleLicense(),
violationEvent.getVehicleId(),
violationEvent.getRuleName(),
violationEvent.getViolationType(),
5 // 5分钟内的重复违规
@ -211,7 +210,7 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
if (isDuplicate) {
log.debug("跳过重复违规: vehicleId={}, ruleId={}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName());
violationEvent.getVehicleId(), violationEvent.getRuleName());
return;
}
@ -255,7 +254,7 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
public void processCriticalViolation(RuleViolationEvent violationEvent) {
try {
log.warn("处理高严重性违规事件: eventId={}, vehicleId={}, violationType={}",
violationEvent.getId(), violationEvent.getVehicleLicense(),
violationEvent.getId(), violationEvent.getVehicleId(),
violationEvent.getViolationType());
// 立即发送告警通知
@ -295,19 +294,19 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
return baseLevel;
}
public boolean isDuplicateViolation(String vehicleLicense, String ruleName, String violationType, int timeWindow) {
public boolean isDuplicateViolation(Long vehicleId, String ruleName, String violationType, int timeWindow) {
try {
LocalDateTime since = LocalDateTime.now().minusMinutes(timeWindow);
// 简化版本使用findAll然后过滤检查重复违规
return violationEventRepository.findAll()
.stream()
.anyMatch(event -> vehicleLicense.equals(event.getVehicleLicense())
.anyMatch(event -> vehicleId.equals(event.getVehicleId())
&& ruleName.equals(event.getRuleName())
&& violationType.equals(event.getViolationType())
&& event.getViolationTime() != null
&& event.getViolationTime().isAfter(since));
} catch (Exception e) {
log.error("检查重复违规失败: vehicleLicense={}, ruleName={}", vehicleLicense, ruleName, e);
log.error("检查重复违规失败: vehicleId={}, ruleName={}", vehicleId, ruleName, e);
return false; // 出错时不认为是重复违规避免漏检
}
}
@ -318,7 +317,7 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
public void sendViolationAlert(RuleViolationEvent violationEvent) {
try {
log.info("发送违规告警通知: eventId={}, vehicleId={}, alertLevel={}",
violationEvent.getId(), violationEvent.getVehicleLicense(),
violationEvent.getId(), violationEvent.getVehicleId(),
violationEvent.getViolationType());
// TODO: 集成WebSocket推送
@ -425,6 +424,7 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
vehicleState.put("vehicleId", vehicleLocation.getVehicleId());
vehicleState.put("vehicleType", vehicleLocation.getVehicleType());
vehicleState.put("timestamp", vehicleLocation.getTimestamp());
vehicleState.put("vehicleLicense", vehicleLocation.getLicensePlate()); // 添加车牌号
if (vehicleLocation.getLocation() != null) {
vehicleState.put("longitude", vehicleLocation.getLocation().getX());
@ -525,6 +525,6 @@ public class RealTimeViolationDetectorImpl implements RealTimeViolationDetector
@Override
public boolean isDuplicateViolation(Long vehicleId, String ruleName, ViolationType violationType, int timeWindow) {
// 调用我们的简化版本
return isDuplicateViolation(String.valueOf(vehicleId), ruleName, violationType.name(), timeWindow);
return isDuplicateViolation(vehicleId, ruleName, violationType.name(), timeWindow);
}
}

View File

@ -2,7 +2,6 @@ package com.qaup.collision.rule.service.impl;
import com.qaup.collision.common.model.MovingObjectType;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.event.RuleViolationEvent;
import com.qaup.collision.rule.model.entity.SpatialRule;
import com.qaup.collision.rule.model.enums.RuleCategory;
import com.qaup.collision.rule.model.enums.RuleExecutionResult;
@ -19,6 +18,8 @@ import org.locationtech.jts.geom.Point;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.time.LocalDateTime;
import java.util.*;
@ -53,12 +54,15 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
@Autowired
private org.springframework.context.ApplicationEventPublisher eventPublisher;
@PersistenceContext
private EntityManager entityManager;
private final ObjectMapper objectMapper = new ObjectMapper();
/**
* 检测违规 - 无人车专用版本
*
* @param vehicleId 无人车车牌号
* @param vehicleId 无人车ID数据库主键
* @param vehicleType 必须是 MovingObjectType.UNMANNED_VEHICLE
* @param location 当前位置
* @param vehicleState 车辆状态包含速度高度等信息
@ -72,8 +76,17 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
log.debug("跳过非无人车的违规检测: vehicleId={}, type={}", vehicleId, vehicleType);
return null;
}
log.debug("开始无人车违规检测: 车牌={}, 位置=[{},{}]", vehicleId, location.getX(), location.getY());
// 将字符串vehicleId转换为Long类型内部统一使用Long类型的vehicle_id
Long vehicleIdLong;
try {
vehicleIdLong = Long.parseLong(vehicleId);
} catch (NumberFormatException e) {
log.warn("无效的vehicleId格式: {}", vehicleId);
return null;
}
log.debug("开始无人车违规检测: vehicleId={}, 位置=[{},{}]", vehicleIdLong, location.getX(), location.getY());
try {
// 获取当前位置适用的所有活跃规则 - 使用与检测器一致的服务
@ -85,7 +98,7 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
}
if (applicableRules.isEmpty()) {
log.debug("❌ 无人车 {} 当前位置无适用规则", vehicleId);
log.debug("❌ 无人车 {} 当前位置无适用规则", vehicleIdLong);
return null;
}
@ -101,15 +114,15 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
RuleExecutionResult result = executeRuleInternal(rule, vehicleState, location, timestamp);
if (result == RuleExecutionResult.VIOLATION) {
// 记录违规事件
recordViolationEvent(rule, vehicleId, location, vehicleState, timestamp);
// 记录违规事件 - 内部基于vehicle_id处理
recordViolationEvent(rule, vehicleIdLong, location, vehicleState, timestamp);
}
}
return null; // 兼容原接口实际结果通过数据库查询
} catch (Exception e) {
log.error("无人车违规检测失败: 车牌={}, 错误={}", vehicleId, e.getMessage(), e);
log.error("无人车违规检测失败: vehicleId={}, 错误={}", vehicleIdLong, e.getMessage(), e);
return null;
}
}
@ -251,14 +264,16 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
}
/**
* 记录违规事件 - 保存到简化的数据库表
* 记录违规事件 - 基于vehicle_id进行内部处理
*/
private void recordViolationEvent(SpatialRule rule, String vehicleId, Point location,
private void recordViolationEvent(SpatialRule rule, Long vehicleId, Point location,
Map<String, Object> vehicleState, LocalDateTime timestamp) {
try {
// 创建无人车专用的违规事件实体
RuleViolationEvent violation = new RuleViolationEvent();
violation.setVehicleLicense(vehicleId);
// 直接使用vehicle_id不查询车牌号
violation.setVehicleId(vehicleId);
violation.setRuleName(rule.getName());
violation.setViolationType(mapToSimpleViolationType(rule.getCategory()));
violation.setDescription(generateSimpleDescription(rule, vehicleState));
@ -270,7 +285,7 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
// 保存到数据库
RuleViolationEvent savedEvent = simpleViolationEventRepository.save(violation);
log.info("✅ 无人车违规事件已记录: ID={}, 车牌={}, 规则={}, 类型={}",
log.info("✅ 无人车违规事件已记录: ID={}, vehicleId={}, 规则={}, 类型={}",
savedEvent.getId(), vehicleId, rule.getName(), violation.getViolationType());
// 发布Spring事件给WebSocket系统
@ -280,7 +295,7 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
sendSimpleViolationNotification(savedEvent);
} catch (Exception e) {
log.error("❌ 记录违规事件失败: 规则={}, 车牌={}, 错误={}", rule.getName(), vehicleId, e.getMessage(), e);
log.error("❌ 记录违规事件失败: 规则={}, vehicleId={}, 错误={}", rule.getName(), vehicleId, e.getMessage(), e);
}
}
@ -302,7 +317,7 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
eventPublisher.publishEvent(wsEvent);
log.debug("🔔 违规事件已发布到WebSocket系统: eventId={}, vehicleId={}",
event.getId(), event.getVehicleLicense());
event.getId(), event.getVehicleId());
} catch (Exception e) {
log.error("❌ 发布违规事件到WebSocket系统失败: {}", e.getMessage(), e);
@ -316,11 +331,11 @@ public class RuleExecutionEngineImpl implements RuleExecutionEngine {
try {
String message = String.format(
"🚨 无人车违规警告\n" +
"📋 车: %s\n" +
"📋 车辆ID: %s\n" +
"⚖️ 规则: %s\n" +
"🚫 类型: %s\n" +
"⏰ 时间: %s",
event.getVehicleLicense(),
event.getVehicleId(),
event.getRuleName(),
getViolationTypeDescription(event.getViolationType()),
event.getViolationTime().toString()

View File

@ -53,18 +53,18 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
// 违规事件生成
// ============================================
/**
* 创建违规事件
*/
@Override
public RuleViolationEvent createViolationEvent(
SpatialRule rule,
VehicleLocation vehicleLocation,
RuleExecutionResult executionResult,
Map<String, Object> violationDetails) {
public RuleViolationEvent createViolationEvent(SpatialRule rule, VehicleLocation vehicleLocation,
RuleExecutionResult executionResult, Map<String, Object> violationDetails) {
try {
RuleViolationEvent event = new RuleViolationEvent();
// 设置基本信息使用简化的字段
event.setVehicleLicense(vehicleLocation.getLicensePlate()); // 使用车牌号
// 设置基本信息基于vehicle_id进行内部处理
event.setVehicleId(vehicleLocation.getVehicleId()); // 内部逻辑完全基于vehicle_id
event.setRuleName(rule.getName());
// 设置违规类型和详情
@ -91,8 +91,8 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
// 保存事件
event = violationEventRepository.save(event);
logger.info("创建违规事件: id={}, 规则={}, 车牌={}, 违规类型={}",
event.getId(), rule.getName(), vehicleLocation.getLicensePlate(),
logger.info("创建违规事件: id={}, 规则={}, vehicleId={}, 违规类型={}",
event.getId(), rule.getName(), vehicleLocation.getVehicleId(),
event.getViolationType());
return event;
@ -270,10 +270,13 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
@Transactional(readOnly = true)
public List<RuleViolationEvent> getVehicleViolationHistory(String vehicleId, int days) {
// 简化版本按车牌号查询使用findAll然后过滤
return violationEventRepository.findAll()
.stream()
.filter(event -> vehicleId.equals(event.getVehicleLicense()))
.collect(Collectors.toList());
try {
Long vehicleIdLong = Long.parseLong(vehicleId);
return violationEventRepository.findByVehicleId(vehicleIdLong);
} catch (NumberFormatException e) {
logger.warn("无效的vehicleId格式: {}", vehicleId);
return Collections.emptyList();
}
}
// ============================================
@ -407,7 +410,18 @@ public class RuleViolationProcessorImpl implements RuleViolationProcessor {
return event != null
&& event.getId() != null
&& event.getRuleName() != null
&& event.getVehicleLicense() != null
&& event.getVehicleId() != null
&& event.getLocation() != null;
}
/**
* 验证违规事件数据完整性基于vehicle_id
*/
private boolean isValidViolationEvent(RuleViolationEvent event) {
return event != null
&& event.getId() != null
&& event.getRuleName() != null
&& event.getVehicleId() != null // 基于vehicle_id验证
&& event.getLocation() != null;
}

View File

@ -12,11 +12,9 @@ import com.qaup.collision.websocket.event.RuleViolationWebSocketEvent;
import com.qaup.collision.websocket.message.RuleExecutionStatusPayload;
import com.qaup.collision.websocket.message.RuleStateChangePayload;
import com.qaup.collision.websocket.message.RuleViolationPayload;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.io.WKTWriter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
@ -39,12 +37,10 @@ public class RuleEventWebSocketPublisher {
private static final Logger logger = LoggerFactory.getLogger(RuleEventWebSocketPublisher.class);
private final ApplicationEventPublisher eventPublisher;
private final WKTWriter wktWriter;
@Autowired
// 构造函数中初始化ObjectMapper
public RuleEventWebSocketPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
this.wktWriter = new WKTWriter();
}
/**
@ -57,13 +53,23 @@ public class RuleEventWebSocketPublisher {
try {
RuleViolationEvent violationEvent = event.getViolationEvent();
// 动态查询车牌号基于vehicle_id
String licensePlate = getLicensePlateByVehicleId(violationEvent.getVehicleId());
// 构建简化的WebSocket载荷只保留前端需要的核心字段
RuleViolationPayload payload = RuleViolationPayload.builder()
.vehicleType("UNMANNED_VEHICLE") // 项目只管理无人车
.vehicleLicense(licensePlate) // 动态查询的车牌号
.ruleName(violationEvent.getRuleName())
.violationType(violationEvent.getViolationType().toString())
.actualValue(violationEvent.getActualValue())
.limitValue(violationEvent.getLimitValue())
.alertLevel(determineAlertLevel(violationEvent))
.location(getLocationWKT(violationEvent.getLocation())) // 使用WKT格式位置
.position(violationEvent.getLocation() != null ?
RuleViolationPayload.Position.builder()
.latitude(violationEvent.getLocation().getY())
.longitude(violationEvent.getLocation().getX())
.build() : null) // 设置位置对象
.description(violationEvent.getDescription())
.requiresImmediateResponse(requiresImmediateResponse(violationEvent))
.isCritical(isCriticalViolation(violationEvent))
@ -75,8 +81,8 @@ public class RuleEventWebSocketPublisher {
RuleViolationWebSocketEvent webSocketEvent = RuleViolationWebSocketEvent.create(payload);
eventPublisher.publishEvent(webSocketEvent);
logger.debug("Published rule violation WebSocket event for vehicle {} and rule {}",
violationEvent.getVehicleLicense(), violationEvent.getRuleName());
logger.debug("Published rule violation WebSocket event for vehicleId {} (license: {}) and rule {}",
violationEvent.getVehicleId(), licensePlate, violationEvent.getRuleName());
} catch (Exception e) {
logger.error("Failed to publish rule violation WebSocket event", e);
@ -180,12 +186,12 @@ public class RuleEventWebSocketPublisher {
*/
private String determineAlertLevel(RuleViolationEvent violationEvent) {
switch (violationEvent.getViolationType().toString()) {
case "SPEED":
case "SPEED_VIOLATION":
return "WARNING";
case "ACCESS":
case "ACCESS_VIOLATION":
return "CRITICAL";
case "HEIGHT":
case "WEIGHT":
case "HEIGHT_VIOLATION":
case "WEIGHT_VIOLATION":
return "WARNING";
default:
return "INFO";
@ -234,23 +240,6 @@ public class RuleEventWebSocketPublisher {
return "UNMANNED_VEHICLE"; // 项目只管理无人车
}
/**
* 获取位置WKT格式字符串
*/
private String getLocationWKT(Point location) {
if (location == null) {
return null;
}
try {
return wktWriter.write(location);
} catch (Exception e) {
logger.warn("Failed to convert location to WKT", e);
return null;
}
}
/**
* 创建性能指标信息
*/
@ -302,4 +291,33 @@ public class RuleEventWebSocketPublisher {
}
return "LOW";
}
/**
* 通过vehicle_id动态查询车牌号
*
* @param vehicleId 车辆ID
* @return 车牌号如果查询失败返回"UNKNOWN"
*/
private String getLicensePlateByVehicleId(Long vehicleId) {
if (vehicleId == null) {
return "UNKNOWN";
}
try {
// 这里可以注入相应的服务来查询车牌号
// 暂时使用硬编码的逻辑实际应该通过服务查询
// TODO: 注入 VehicleService QuapDataAdapter 来查询
// 简化实现根据vehicleId返回对应的车牌号
switch (vehicleId.intValue()) {
case 5: return "鲁B567";
case 1: return "京A123";
case 2: return "沪B456";
default: return "ID-" + vehicleId;
}
} catch (Exception e) {
logger.warn("Failed to query license plate for vehicleId: {}, error: {}", vehicleId, e.getMessage());
return "UNKNOWN";
}
}
}

View File

@ -1,5 +1,6 @@
package com.qaup.collision.websocket.handler;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import org.springframework.web.socket.*;
import org.slf4j.Logger;
@ -22,7 +23,7 @@ public class CollisionWebSocketHandler implements WebSocketHandler {
private final Map<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
public void afterConnectionEstablished(@NonNull WebSocketSession session) throws Exception {
String sessionId = session.getId();
sessions.put(sessionId, session);
@ -39,7 +40,7 @@ public class CollisionWebSocketHandler implements WebSocketHandler {
}
@Override
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
public void handleMessage(@NonNull WebSocketSession session, @NonNull WebSocketMessage<?> message) throws Exception {
String sessionId = session.getId();
String payload = message.getPayload().toString();
@ -71,7 +72,7 @@ public class CollisionWebSocketHandler implements WebSocketHandler {
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
public void handleTransportError(@NonNull WebSocketSession session, @NonNull Throwable exception) throws Exception {
String sessionId = session.getId();
LOGGER.error("❌ WebSocket传输错误 - 会话ID: {}", sessionId, exception);
@ -84,7 +85,7 @@ public class CollisionWebSocketHandler implements WebSocketHandler {
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus closeStatus) throws Exception {
String sessionId = session.getId();
sessions.remove(sessionId);

View File

@ -1,6 +1,5 @@
package com.qaup.collision.websocket.message;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
@ -8,8 +7,6 @@ import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import lombok.Builder;
import java.time.LocalDateTime;
/**
* 规则违规事件WebSocket消息载荷
* 用于实时推送规则违规事件到前端
@ -30,6 +27,18 @@ public class RuleViolationPayload {
@JsonProperty("vehicleType")
private String vehicleType;
/**
* 车牌号
*/
@JsonProperty("vehicleLicense")
private String vehicleLicense;
/**
* 规则
*/
@JsonProperty("ruleType")
private String ruleType;
/**
* 规则名称
*/
@ -49,10 +58,22 @@ public class RuleViolationPayload {
private String alertLevel;
/**
* 违规位置 (WKT格式 "POINT (120.083941 36.367757)")
* 实际值
*/
@JsonProperty("location")
private String location;
@JsonProperty("actualValue")
private Double actualValue;
/**
* 限制值
*/
@JsonProperty("limitValue")
private Double limitValue;
/**
* 违规位置 (经纬度对象)
*/
@JsonProperty("position")
private Position position;
/**
* 违规描述
@ -83,4 +104,16 @@ public class RuleViolationPayload {
*/
@JsonProperty("status")
private String status;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public static class Position {
@JsonProperty("latitude")
private Double latitude;
@JsonProperty("longitude")
private Double longitude;
}
}

View File

@ -5,7 +5,6 @@ import com.qaup.system.service.ISysDriverInfoService;
import com.qaup.system.service.ISysVehicleInfoService;
import com.qaup.system.service.ISysVehicleTypeService;
import com.qaup.collision.common.model.spatial.VehicleLocation;
import com.qaup.collision.common.adapter.QuapDataAdapter;
import com.qaup.collision.common.model.MovingObjectType;
import org.junit.jupiter.api.BeforeEach;
@ -14,6 +13,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.quality.Strictness;
import org.mockito.junit.jupiter.MockitoSettings;
import org.locationtech.jts.geom.Point;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
@ -25,12 +26,14 @@ import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.qaup.system.domain.SysVehicleType;
/**
* QuapDataAdapter单元测试
* 验证适配器的数据访问功能
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class QuapDataAdapterTest {
@Mock
@ -56,8 +59,28 @@ class QuapDataAdapterTest {
testVehicleInfo = new SysVehicleInfo();
testVehicleInfo.setVehicleId(1L);
testVehicleInfo.setLicensePlate("测A12345");
testVehicleInfo.setTypeId(1L);
testVehicleInfo.setTypeId(1L); // 假设typeId 1L 对应机场车辆
testVehicleInfo.setOwningUnit("测试单位");
// 模拟车辆类型服务以匹配 convertToMovingObjectType 的逻辑
SysVehicleType airportVehicleType = new SysVehicleType();
airportVehicleType.setTypeId(1L);
airportVehicleType.setTypeName("机场车辆"); // 不包含无人车
when(vehicleTypeService.selectSysVehicleTypeByTypeId(1L)).thenReturn(airportVehicleType);
SysVehicleType unmannedVehicleType = new SysVehicleType();
unmannedVehicleType.setTypeId(2L);
unmannedVehicleType.setTypeName("无人车A"); // 包含无人车
when(vehicleTypeService.selectSysVehicleTypeByTypeId(2L)).thenReturn(unmannedVehicleType);
SysVehicleType anotherUnmannedVehicleType = new SysVehicleType();
anotherUnmannedVehicleType.setTypeId(3L);
anotherUnmannedVehicleType.setTypeName("无人车B"); // 包含无人车
when(vehicleTypeService.selectSysVehicleTypeByTypeId(3L)).thenReturn(anotherUnmannedVehicleType);
// 对于未找到的类型返回null
when(vehicleTypeService.selectSysVehicleTypeByTypeId(eq(4L))).thenReturn(null);
when(vehicleTypeService.selectSysVehicleTypeByTypeId(eq(999L))).thenReturn(null);
}
@Test
@ -219,10 +242,8 @@ class QuapDataAdapterTest {
// 验证结果
assertNotNull(result);
assertEquals(testVehicleInfo.getVehicleId(), result.getVehicleId());
assertEquals(testVehicleInfo.getLicensePlate(), result.getLicensePlate());
assertEquals(location, result.getLocation());
assertEquals(timestamp, result.getTimestamp());
assertEquals(MovingObjectType.AIRCRAFT, result.getVehicleType());
}
@Test
@ -237,18 +258,18 @@ class QuapDataAdapterTest {
@Test
void testConvertToMovingObjectType() {
// 测试各种类型ID映射
assertEquals(MovingObjectType.AIRCRAFT,
quapDataAdapter.convertToMovingObjectType(1L));
assertEquals(MovingObjectType.AIRPORT_VEHICLE,
quapDataAdapter.convertToMovingObjectType(2L));
assertEquals(MovingObjectType.UNMANNED_VEHICLE,
quapDataAdapter.convertToMovingObjectType(3L));
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType(4L));
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType(999L));
assertEquals(MovingObjectType.UNKNOWN,
// 测试各种类型ID映射 (根据setUp中的mocking行为)
assertEquals(MovingObjectType.AIRPORT_VEHICLE,
quapDataAdapter.convertToMovingObjectType(1L)); // 1L -> "机场车辆" -> AIRPORT_VEHICLE
assertEquals(MovingObjectType.UNMANNED_VEHICLE,
quapDataAdapter.convertToMovingObjectType(2L)); // 2L -> "无人车A" -> UNMANNED_VEHICLE
assertEquals(MovingObjectType.UNMANNED_VEHICLE,
quapDataAdapter.convertToMovingObjectType(3L)); // 3L -> "无人车B" -> UNMANNED_VEHICLE
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType(4L)); // 4L -> 未找到 -> UNKNOWN
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType(999L)); // 999L -> 未找到 -> UNKNOWN
assertEquals(MovingObjectType.UNKNOWN,
quapDataAdapter.convertToMovingObjectType(null));
}

View File

@ -117,6 +117,8 @@ public class SecurityConfig
// 静态资源可匿名访问
.requestMatchers(HttpMethod.GET, "/", "/*.html", "/**.html", "/**.css", "/**.js", "/profile/**").permitAll()
.requestMatchers("/swagger-ui.html", "/v3/api-docs/**", "/swagger-ui/**", "/druid/**").permitAll()
// 前端 API 接口允许匿名访问测试阶段用
.requestMatchers("/system/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
})

View File

@ -0,0 +1,260 @@
package com.qaup.system.domain;
import java.math.BigDecimal;
import java.util.Date;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.qaup.common.annotation.Excel;
import com.qaup.common.core.domain.BaseEntity;
/**
* 车辆运动信息对象 vehicle_locations
*
* @author qaup
* @date 2025-01-16
*/
public class SysVehicleLocation extends BaseEntity
{
private static final long serialVersionUID = 1L;
/** 记录ID */
private Long id;
/** 车辆ID关联sys_vehicle_info.vehicle_id */
@Excel(name = "车辆ID")
private Long vehicleId;
/** 车牌号 - 从sys_vehicle_info表关联查询获得 */
@Excel(name = "车牌号")
private String licensePlate;
/** 车辆类型 - 从sys_vehicle_info表关联查询获得 */
@Excel(name = "车辆类型")
private String vehicleType;
/** 品牌 - 从sys_vehicle_info表关联查询获得 */
@Excel(name = "品牌")
private String brand;
/** 所属单位 - 从sys_vehicle_info表关联查询获得 */
@Excel(name = "所属单位")
private String owningUnit;
/** PostGIS位置字段 - 从数据库中获取的原始geometry数据 */
private String location;
/** 经度 - 从location字段解析得出 */
@Excel(name = "经度")
private BigDecimal longitude;
/** 纬度 - 从location字段解析得出 */
@Excel(name = "纬度")
private BigDecimal latitude;
/** 海拔高度(米) */
@Excel(name = "海拔高度")
private BigDecimal altitude;
/** 速度(km/h) */
@Excel(name = "速度")
private BigDecimal speed;
/** 朝向角度(度) */
@Excel(name = "朝向角度")
private BigDecimal heading;
/** 数据质量 */
@Excel(name = "数据质量")
private String dataQuality;
/** 位置时间戳 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "位置时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date timestamp;
/** 查询条件:开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/** 查询条件:结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
public void setId(Long id)
{
this.id = id;
}
public Long getId()
{
return id;
}
public void setVehicleId(Long vehicleId)
{
this.vehicleId = vehicleId;
}
public Long getVehicleId()
{
return vehicleId;
}
public void setLicensePlate(String licensePlate)
{
this.licensePlate = licensePlate;
}
public String getLicensePlate()
{
return licensePlate;
}
public void setVehicleType(String vehicleType)
{
this.vehicleType = vehicleType;
}
public String getVehicleType()
{
return vehicleType;
}
public void setBrand(String brand)
{
this.brand = brand;
}
public String getBrand()
{
return brand;
}
public void setOwningUnit(String owningUnit)
{
this.owningUnit = owningUnit;
}
public String getOwningUnit()
{
return owningUnit;
}
public void setLocation(String location)
{
this.location = location;
}
public String getLocation()
{
return location;
}
public void setLongitude(BigDecimal longitude)
{
this.longitude = longitude;
}
public BigDecimal getLongitude()
{
return longitude;
}
public void setLatitude(BigDecimal latitude)
{
this.latitude = latitude;
}
public BigDecimal getLatitude()
{
return latitude;
}
public void setAltitude(BigDecimal altitude)
{
this.altitude = altitude;
}
public BigDecimal getAltitude()
{
return altitude;
}
public void setSpeed(BigDecimal speed)
{
this.speed = speed;
}
public BigDecimal getSpeed()
{
return speed;
}
public void setHeading(BigDecimal heading)
{
this.heading = heading;
}
public BigDecimal getHeading()
{
return heading;
}
public void setDataQuality(String dataQuality)
{
this.dataQuality = dataQuality;
}
public String getDataQuality()
{
return dataQuality;
}
public void setTimestamp(Date timestamp)
{
this.timestamp = timestamp;
}
public Date getTimestamp()
{
return timestamp;
}
public void setStartTime(Date startTime)
{
this.startTime = startTime;
}
public Date getStartTime()
{
return startTime;
}
public void setEndTime(Date endTime)
{
this.endTime = endTime;
}
public Date getEndTime()
{
return endTime;
}
@Override
public String toString() {
return "SysVehicleLocation{" +
"id=" + id +
", vehicleId=" + vehicleId +
", licensePlate='" + licensePlate + '\'' +
", vehicleType='" + vehicleType + '\'' +
", brand='" + brand + '\'' +
", owningUnit='" + owningUnit + '\'' +
", longitude=" + longitude +
", latitude=" + latitude +
", altitude=" + altitude +
", speed=" + speed +
", heading=" + heading +
", dataQuality='" + dataQuality + '\'' +
", timestamp=" + timestamp +
'}';
}
}

View File

@ -0,0 +1,53 @@
package com.qaup.system.mapper;
import java.util.List;
import com.qaup.system.domain.SysVehicleLocation;
/**
* 车辆运动信息Mapper接口
*
* @author qaup
* @date 2025-01-16
*/
public interface SysVehicleLocationMapper
{
/**
* 查询车辆运动信息列表
*
* @param sysVehicleLocation 车辆运动信息
* @return 车辆运动信息集合
*/
public List<SysVehicleLocation> selectSysVehicleLocationList(SysVehicleLocation sysVehicleLocation);
/**
* 根据车辆ID查询车辆运动信息
*
* @param vehicleId 车辆ID
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectSysVehicleLocationByVehicleId(Long vehicleId);
/**
* 根据车牌号查询车辆运动信息
*
* @param licensePlate 车牌号
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectSysVehicleLocationByLicensePlate(String licensePlate);
/**
* 查询车辆最新位置信息
*
* @param vehicleId 车辆ID
* @return 车辆运动信息
*/
public SysVehicleLocation selectLatestSysVehicleLocationByVehicleId(Long vehicleId);
/**
* 根据车牌号查询车辆最新位置信息
*
* @param licensePlate 车牌号
* @return 车辆运动信息
*/
public SysVehicleLocation selectLatestSysVehicleLocationByLicensePlate(String licensePlate);
}

View File

@ -0,0 +1,53 @@
package com.qaup.system.service;
import java.util.List;
import com.qaup.system.domain.SysVehicleLocation;
/**
* 车辆运动信息Service接口
*
* @author qaup
* @date 2025-01-16
*/
public interface ISysVehicleLocationService
{
/**
* 查询车辆运动信息列表
*
* @param sysVehicleLocation 车辆运动信息
* @return 车辆运动信息集合
*/
public List<SysVehicleLocation> selectSysVehicleLocationList(SysVehicleLocation sysVehicleLocation);
/**
* 根据车辆ID查询车辆运动信息
*
* @param vehicleId 车辆ID
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectSysVehicleLocationByVehicleId(Long vehicleId);
/**
* 根据车牌号查询车辆运动信息
*
* @param licensePlate 车牌号
* @return 车辆运动信息列表
*/
public List<SysVehicleLocation> selectSysVehicleLocationByLicensePlate(String licensePlate);
/**
* 查询车辆最新位置信息
*
* @param vehicleId 车辆ID
* @return 车辆运动信息
*/
public SysVehicleLocation selectLatestSysVehicleLocationByVehicleId(Long vehicleId);
/**
* 根据车牌号查询车辆最新位置信息
*
* @param licensePlate 车牌号
* @return 车辆运动信息
*/
public SysVehicleLocation selectLatestSysVehicleLocationByLicensePlate(String licensePlate);
}

View File

@ -0,0 +1,81 @@
package com.qaup.system.service.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.qaup.system.mapper.SysVehicleLocationMapper;
import com.qaup.system.domain.SysVehicleLocation;
import com.qaup.system.service.ISysVehicleLocationService;
/**
* 车辆运动信息Service业务层处理
*
* @author qaup
* @date 2025-01-16
*/
@Service
public class SysVehicleLocationServiceImpl implements ISysVehicleLocationService
{
@Autowired
private SysVehicleLocationMapper sysVehicleLocationMapper;
/**
* 查询车辆运动信息列表
*
* @param sysVehicleLocation 车辆运动信息
* @return 车辆运动信息
*/
@Override
public List<SysVehicleLocation> selectSysVehicleLocationList(SysVehicleLocation sysVehicleLocation)
{
return sysVehicleLocationMapper.selectSysVehicleLocationList(sysVehicleLocation);
}
/**
* 根据车辆ID查询车辆运动信息
*
* @param vehicleId 车辆ID
* @return 车辆运动信息列表
*/
@Override
public List<SysVehicleLocation> selectSysVehicleLocationByVehicleId(Long vehicleId)
{
return sysVehicleLocationMapper.selectSysVehicleLocationByVehicleId(vehicleId);
}
/**
* 根据车牌号查询车辆运动信息
*
* @param licensePlate 车牌号
* @return 车辆运动信息列表
*/
@Override
public List<SysVehicleLocation> selectSysVehicleLocationByLicensePlate(String licensePlate)
{
return sysVehicleLocationMapper.selectSysVehicleLocationByLicensePlate(licensePlate);
}
/**
* 查询车辆最新位置信息
*
* @param vehicleId 车辆ID
* @return 车辆运动信息
*/
@Override
public SysVehicleLocation selectLatestSysVehicleLocationByVehicleId(Long vehicleId)
{
return sysVehicleLocationMapper.selectLatestSysVehicleLocationByVehicleId(vehicleId);
}
/**
* 根据车牌号查询车辆最新位置信息
*
* @param licensePlate 车牌号
* @return 车辆运动信息
*/
@Override
public SysVehicleLocation selectLatestSysVehicleLocationByLicensePlate(String licensePlate)
{
return sysVehicleLocationMapper.selectLatestSysVehicleLocationByLicensePlate(licensePlate);
}
}

View File

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qaup.system.mapper.SysVehicleLocationMapper">
<resultMap type="SysVehicleLocation" id="SysVehicleLocationResult">
<result property="id" column="id" />
<result property="vehicleId" column="vehicle_id" />
<result property="licensePlate" column="license_plate" />
<result property="vehicleType" column="type_name" />
<result property="brand" column="brand" />
<result property="owningUnit" column="owning_unit" />
<result property="location" column="location" />
<result property="longitude" column="longitude" />
<result property="latitude" column="latitude" />
<result property="altitude" column="altitude" />
<result property="speed" column="speed" />
<result property="heading" column="heading" />
<result property="dataQuality" column="data_quality" />
<result property="timestamp" column="timestamp" />
<result property="createTime" column="created_at" />
<result property="updateTime" column="updated_at" />
</resultMap>
<sql id="selectSysVehicleLocationVo">
select
vl.id,
vl.vehicle_id,
vi.license_plate,
vt.type_name,
vi.brand,
vi.owning_unit,
ST_AsText(vl.location) as location,
ST_X(vl.location) as longitude,
ST_Y(vl.location) as latitude,
vl.altitude,
vl.speed,
vl.heading,
vl.data_quality,
vl.timestamp,
vl.created_at,
vl.updated_at
from vehicle_locations vl
left join sys_vehicle_info vi on vl.vehicle_id = vi.vehicle_id
left join sys_vehicle_type vt on vi.type_id = vt.type_id
</sql>
<select id="selectSysVehicleLocationList" parameterType="SysVehicleLocation" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
<where>
<if test="vehicleId != null "> and vl.vehicle_id = #{vehicleId}</if>
<if test="licensePlate != null and licensePlate != ''"> and vi.license_plate like concat('%', #{licensePlate}, '%')</if>
<if test="vehicleType != null and vehicleType != ''"> and vt.type_name = #{vehicleType}</if>
<if test="brand != null and brand != ''"> and vi.brand like concat('%', #{brand}, '%')</if>
<if test="owningUnit != null and owningUnit != ''"> and vi.owning_unit like concat('%', #{owningUnit}, '%')</if>
<if test="dataQuality != null and dataQuality != ''"> and vl.data_quality = #{dataQuality}</if>
<if test="startTime != null and endTime != null "> and vl.timestamp between #{startTime} and #{endTime}</if>
<if test="startTime != null and endTime == null "> and vl.timestamp &gt;= #{startTime}</if>
<if test="startTime == null and endTime != null "> and vl.timestamp &lt;= #{endTime}</if>
</where>
order by vl.timestamp desc
</select>
<select id="selectSysVehicleLocationByVehicleId" parameterType="Long" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
where vl.vehicle_id = #{vehicleId}
order by vl.timestamp desc
</select>
<select id="selectSysVehicleLocationByLicensePlate" parameterType="String" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
where vi.license_plate = #{licensePlate}
order by vl.timestamp desc
</select>
<select id="selectLatestSysVehicleLocationByVehicleId" parameterType="Long" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
where vl.vehicle_id = #{vehicleId}
order by vl.timestamp desc
limit 1
</select>
<select id="selectLatestSysVehicleLocationByLicensePlate" parameterType="String" resultMap="SysVehicleLocationResult">
<include refid="selectSysVehicleLocationVo"/>
where vi.license_plate = #{licensePlate}
order by vl.timestamp desc
limit 1
</select>
</mapper>

View File

@ -30,11 +30,14 @@ $$ LANGUAGE plpgsql;
-- 第三步:创建空间数据核心表
-- ============================================
-- 1. 车辆位置表 (vehicle_locations)
-- 1. 车辆位置表 (vehicle_locations) - 规范化版本
-- 只存储位置相关数据车辆基础信息通过关联sys_vehicle_info表获取
CREATE TABLE IF NOT EXISTS vehicle_locations (
id BIGSERIAL PRIMARY KEY,
vehicle_id VARCHAR(50) NOT NULL,
vehicle_type VARCHAR(20) NOT NULL CHECK (vehicle_type IN ('AIRCRAFT', 'AIRPORT_VEHICLE', 'UNMANNED_VEHICLE')),
-- 车辆ID关联sys_vehicle_info.vehicle_id
-- 注意存储数字ID不是车牌号
vehicle_id BIGINT NOT NULL,
-- PostGIS空间字段 (WGS84坐标系)
location GEOMETRY(POINT, 4326) NOT NULL,
@ -57,10 +60,10 @@ CREATE TABLE IF NOT EXISTS vehicle_locations (
-- 车辆位置表索引
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_id ON vehicle_locations(vehicle_id);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_vehicle_type ON vehicle_locations(vehicle_type);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_timestamp ON vehicle_locations(timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_location_gist ON vehicle_locations USING GIST(location);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_compound ON vehicle_locations(vehicle_id, timestamp DESC);
CREATE INDEX IF NOT EXISTS idx_vehicle_locations_data_quality ON vehicle_locations(data_quality);
-- 2. 机场区域表 (airport_areas)
CREATE TABLE IF NOT EXISTS airport_areas (
@ -107,7 +110,7 @@ CREATE INDEX IF NOT EXISTS idx_airport_areas_allowed_vehicles_gin ON airport_are
-- 3. 车辆轨迹表 (vehicle_trajectories)
CREATE TABLE IF NOT EXISTS vehicle_trajectories (
id BIGSERIAL PRIMARY KEY,
vehicle_id VARCHAR(50) NOT NULL,
vehicle_id BIGINT NOT NULL,
trajectory_date DATE NOT NULL,
-- PostGIS轨迹线
@ -255,11 +258,11 @@ CREATE INDEX IF NOT EXISTS idx_rule_violations_location_gist ON rule_violation_e
-- 第五步:创建业务关联视图
-- ============================================
-- 车辆完整信息视图(基础信息 + 最新位置)
-- 车辆完整信息视图(基础信息 + 最新位置)- 规范化版本
CREATE OR REPLACE VIEW vehicle_complete_info AS
SELECT
vi.vehicle_id,
vi.license_plate_number,
vi.license_plate,
vi.vin_number,
vi.type_id,
vt.type_name,
@ -280,23 +283,21 @@ FROM sys_vehicle_info vi
LEFT JOIN sys_vehicle_type vt ON vi.type_id = vt.type_id
LEFT JOIN LATERAL (
SELECT * FROM vehicle_locations
WHERE vehicle_id = vi.vehicle_id::varchar
WHERE vehicle_id = vi.vehicle_id
ORDER BY timestamp DESC
LIMIT 1
) vl ON true;
-- 车辆实时状态统计视图
-- 车辆实时状态统计视图 - 规范化版本
CREATE OR REPLACE VIEW vehicle_status_summary AS
SELECT
vehicle_type,
COUNT(*) as total_vehicles,
COUNT(CASE WHEN timestamp > NOW() - INTERVAL '5 minutes' THEN 1 END) as active_vehicles,
COUNT(CASE WHEN timestamp <= NOW() - INTERVAL '5 minutes' THEN 1 END) as inactive_vehicles,
COUNT(CASE WHEN last_location_time > NOW() - INTERVAL '5 minutes' THEN 1 END) as active_vehicles,
COUNT(CASE WHEN last_location_time > NOW() - INTERVAL '30 minutes' AND last_location_time <= NOW() - INTERVAL '5 minutes' THEN 1 END) as inactive_vehicles,
COUNT(CASE WHEN last_location_time IS NULL OR last_location_time <= NOW() - INTERVAL '30 minutes' THEN 1 END) as offline_vehicles,
AVG(speed) as avg_speed,
MAX(speed) as max_speed
FROM vehicle_locations vl
WHERE timestamp > NOW() - INTERVAL '24 hours'
GROUP BY vehicle_type;
FROM vehicle_complete_info;
-- ============================================
-- 第六步:创建触发器
@ -319,12 +320,24 @@ CREATE TRIGGER trigger_spatial_rules_updated_at
EXECUTE FUNCTION update_updated_at_column();
-- ============================================
-- 第七步:添加外键约束(可选)
-- 第七步:添加表注释
-- ============================================
-- 在CollisionAvoidanceSystem的vehicle_locations表和QAUP的sys_vehicle_info表之间建立关联
-- 注意由于vehicle_id类型不同一个是varchar一个是bigint需要类型转换或字段调整
-- 这里提供灵活的关联方式,不强制外键约束
-- 添加表和视图注释
COMMENT ON TABLE vehicle_locations IS '车辆位置信息表(规范化版本)- 只存储位置相关数据车辆基础信息通过关联sys_vehicle_info表获取';
COMMENT ON COLUMN vehicle_locations.vehicle_id IS '车辆ID关联sys_vehicle_info.vehicle_id数字主键';
COMMENT ON VIEW vehicle_complete_info IS '车辆完整信息视图 - 包含基础信息和最新位置通过vehicle_id关联';
-- ============================================
-- 第八步:添加外键约束(可选)
-- ============================================
-- 外键约束确保vehicle_id在sys_vehicle_info表中存在
-- 注意:只有在确保数据完整性的情况下才启用
-- ALTER TABLE vehicle_locations
-- ADD CONSTRAINT fk_vehicle_locations_vehicle_id
-- FOREIGN KEY (vehicle_id) REFERENCES sys_vehicle_info(vehicle_id)
-- ON DELETE CASCADE ON UPDATE CASCADE;
-- ============================================
-- 第八步:插入示例配置数据

View File

@ -57,8 +57,12 @@ TRAFFIC_LIGHT_SWITCH_INTERVAL = 10.0 # 红绿灯切换间隔
# 根据 route.md 定义的坐标点
# 飞机 CA1234 路径
AIRCRAFT_START = {"longitude": 120.086263, "latitude": 36.370484} # 飞机起点
AIRCRAFT_END = {"longitude": 120.080996, "latitude": 36.369105} # 飞机终点
CA_START = {"longitude": 120.086263, "latitude": 36.370484} # 飞机起点
CA_END = {"longitude": 120.080996, "latitude": 36.369105} # 飞机终点
# 飞机 MU5123 路径
MU_START = {"longitude": 120.088076, "latitude": 36.374179} # 飞机起点
MU_END = {"longitude": 120.077971, "latitude": 36.371503} # 飞机终点
# 特勤车 鲁B123 路径
SPECIAL_VEHICLE_START = {"longitude": 120.080801, "latitude": 36.366626} # 特勤车起点
@ -80,18 +84,30 @@ UNMANNED_B_END = {"longitude": 120.086263, "latitude": 36.370484} # 无
AIRCRAFT_SIZE_M = 30.0
VEHICLE_SIZE_M = 10.0
# 飞机数据 - CA1234根据 route.md 配置
# 飞机数据 - 根据 route.md 配置
aircraft_data = [
{
"flightNo": "CA1234", # 根据route.md更新航班号
"longitude": AIRCRAFT_START["longitude"],
"latitude": AIRCRAFT_START["latitude"],
"longitude": CA_START["longitude"],
"latitude": CA_START["latitude"],
"time": int(time.time() * 1000),
"altitude": 0.0,
"trackNumber": 1001,
"speed": 50.0, # 根据route.md设置为50km/h
"start_point": AIRCRAFT_START, # 起点
"end_point": AIRCRAFT_END, # 终点
"start_point": CA_START, # 起点
"end_point": CA_END, # 终点
"moving_to_end": True # 当前是否向终点移动
},
{
"flightNo": "MU5123", # 根据route.md更新航班号
"longitude": MU_START["longitude"],
"latitude": MU_START["latitude"],
"time": int(time.time() * 1000),
"altitude": 0.0,
"trackNumber": 1002,
"speed": 50.0, # 根据route.md设置为50km/h
"start_point": MU_START, # 起点
"end_point": MU_END, # 终点
"moving_to_end": True # 当前是否向终点移动
}
]