添加车辆实时轨迹接口

This commit is contained in:
2025-11-17 18:45:06 +08:00
parent 0c3b14e010
commit 73988b8828
13 changed files with 374 additions and 4 deletions

View File

@ -137,6 +137,7 @@ security:
- /facility/matrix/**
- /hat/device/data
- /websocket/ue
- /websocket/vehicle
# 多租户配置
tenant:

View File

@ -92,6 +92,11 @@ public class GpsEquipmentController extends BaseController {
return gpsEquipmentService.queryPageList(bo, pageQuery);
}
/**
* 查询GPS设备详细列表给车辆
* @param bo
* @return
*/
@GetMapping("/getListToVehicle")
public R<List<GpsEquipmentVo>> getListToVehicle(GpsEquipmentBo bo) {
return R.ok(gpsEquipmentService.getListToVehicle(bo));

View File

@ -87,4 +87,5 @@ public interface IGpsEquipmentSonService extends IService<GpsEquipmentSon>{
List<GpsEquipmentSonVo> getUeUserListByProjectId(LocalDateTime startOfDay, LocalDateTime now);
List<GpsEquipmentSonVo> getVehicleList(GpsEquipmentSonBo bo);
List<GpsEquipmentSonVo> getNewVehicleList(GpsEquipmentSonBo bo);
}

View File

@ -35,6 +35,7 @@ import org.dromara.project.domain.vo.project.BusProjectVo;
import org.dromara.project.service.IBusProjectService;
import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.service.ISysUserService;
import org.dromara.websocket.websocket.service.VehicleWebSocketServer;
import org.redisson.api.DeletedObjectListener;
import org.redisson.api.ExpiredObjectListener;
import org.redisson.api.listener.SetObjectListener;
@ -263,7 +264,6 @@ public class GpsEquipmentServiceImpl extends ServiceImpl<GpsEquipmentMapper, Gps
gpsEquipmentSonService.insertByBo(gpsEquipmentSon);
//TODO 后续创建websocket长连接发送信息
//保存到redis如果存在则更新存活时间
@ -272,14 +272,26 @@ public class GpsEquipmentServiceImpl extends ServiceImpl<GpsEquipmentMapper, Gps
// --------------------------
// 1. 构造需要推送的消息内容String类型
//判断是否有连接
if (equipment != null && StringUtils.isNotEmpty(equipment.getModelId())) {
int onlineCount = InitOnStartWebSocketServer.getOnlineCount();
if (onlineCount > 0){
if (equipment != null && StringUtils.isNotEmpty(equipment.getModelId())) {
String ued = ueStructureJsonMessage(gpsEquipmentSon, equipment.getModelId());
InitOnStartWebSocketServer.sendToAll(ued);
}
}
//判断车辆轨迹是否有连接
if (equipment != null && equipment.getClientType() == 1 && equipment.getUserId() != null) {
int onlineCount1 = VehicleWebSocketServer.getOnlineCount();
if (onlineCount1 > 0) {
String vehicled = vehicleStructureJsonMessage(gpsEquipmentSon);
VehicleWebSocketServer.sendToSubscription(gpsEquipmentSon.getUserId()+"-"+gpsEquipmentSon.getTripId(), vehicled);
}
}
Set<Long> sessionsAll = WebSocketSessionHolder.getSessionsAll();
if (!sessionsAll.isEmpty()) {
String pushContent = buildPushMessage(gpsEquipmentSon);
@ -348,6 +360,19 @@ public class GpsEquipmentServiceImpl extends ServiceImpl<GpsEquipmentMapper, Gps
}
/**
* 构建推送消息内容String类型
*/
private String vehicleStructureJsonMessage(GpsEquipmentSonBo sonBo) {
// 构造消息对象(包含关键信息)
JSONObject messageObj = new JSONObject();
messageObj.put("locLatitude", sonBo.getLocLatitude().toString()); // 消息类型
messageObj.put("locLongitude", sonBo.getLocLongitude().toString()); // 消息类型
// 转换为String类型返回
return messageObj.toString();
}
private static final int DEVICE_ALIVE_TIMEOUT = 120; // 5分钟
/**
* 更新设备存活状态到Redis并添加过期监听

View File

@ -239,4 +239,12 @@ public class GpsEquipmentSonServiceImpl extends ServiceImpl<GpsEquipmentSonMappe
.eq(bo.getTripId() != null,GpsEquipmentSon::getTripId,bo.getTripId())
.between(GpsEquipmentSon::getCreateTime,bo.getStartTime(),bo.getEndTime()));
}
@Override
public List<GpsEquipmentSonVo> getNewVehicleList(GpsEquipmentSonBo bo) {
return baseMapper.selectVoList(new LambdaQueryWrapper<GpsEquipmentSon>()
.eq(GpsEquipmentSon::getUserId,bo.getUserId())
.eq(GpsEquipmentSon::getTripId,bo.getTripId())
.orderByDesc(GpsEquipmentSon::getCreateTime));
}
}

View File

@ -17,6 +17,7 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.vehicle.domain.bo.VehVehicleInfoBo;
import org.dromara.vehicle.domain.vo.VehVehicleInfoVo;
import org.dromara.vehicle.domain.vo.VehVehicleTripVo;
import org.dromara.vehicle.service.IVehVehicleInfoService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -45,6 +46,17 @@ public class VehVehicleInfoController extends BaseController {
public TableDataInfo<VehVehicleInfoVo> list(VehVehicleInfoBo bo, PageQuery pageQuery) {
return vehVehicleInfoService.queryPageList(bo, pageQuery);
}
/**
* 获取车辆行程信息
*
* @param id 主键
*/
// @SaCheckPermission("vehicle:vehicleInfo:query")
@GetMapping("/getTrip/{id}")
public R<VehVehicleTripVo> getTripInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(vehVehicleInfoService.getTripInfo(id));
}
/**
* 查询车辆信息列表

View File

@ -1,5 +1,6 @@
package org.dromara.vehicle.service;
import jakarta.validation.constraints.NotNull;
import org.dromara.vehicle.domain.vo.VehVehicleInfoVo;
import org.dromara.vehicle.domain.bo.VehVehicleInfoBo;
import org.dromara.vehicle.domain.VehVehicleInfo;
@ -7,6 +8,8 @@ import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.mybatis.core.page.PageQuery;
import com.baomidou.mybatisplus.extension.service.IService;
import org.dromara.vehicle.domain.vo.VehVehicleTripVo;
import java.util.Collection;
import java.util.List;
@ -81,4 +84,6 @@ public interface IVehVehicleInfoService extends IService<VehVehicleInfo>{
* @return
*/
int unBindClient(VehVehicleInfoBo bo);
VehVehicleTripVo getTripInfo(Long id);
}

View File

@ -107,4 +107,11 @@ public interface IVehVehicleTripService extends IService<VehVehicleTrip> {
* @return
*/
Long getTripId(Long id);
/**
* 根据车辆id获取最新行程信息
* @param id
* @return
*/
VehVehicleTripVo getTripInfoByVehicleId(Long id);
}

View File

@ -18,9 +18,12 @@ import org.dromara.gps.service.IGpsEquipmentService;
import org.dromara.gps.service.IGpsManmachineService;
import org.dromara.vehicle.domain.VehVehicleInfo;
import org.dromara.vehicle.domain.bo.VehVehicleInfoBo;
import org.dromara.vehicle.domain.enums.VehVehicleInfoStatusEnum;
import org.dromara.vehicle.domain.vo.VehVehicleInfoVo;
import org.dromara.vehicle.domain.vo.VehVehicleTripVo;
import org.dromara.vehicle.mapper.VehVehicleInfoMapper;
import org.dromara.vehicle.service.IVehVehicleInfoService;
import org.dromara.vehicle.service.IVehVehicleTripService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@ -50,6 +53,10 @@ public class VehVehicleInfoServiceImpl extends ServiceImpl<VehVehicleInfoMapper,
@Lazy
private IGpsManmachineService gpsManmachineService;
@Autowired
@Lazy
private IVehVehicleTripService vehVehicleTripService;
/**
* 查询车辆信息
*
@ -209,4 +216,22 @@ public class VehVehicleInfoServiceImpl extends ServiceImpl<VehVehicleInfoMapper,
.eq(GpsManmachine::getUserId, bo.getId()));
return baseMapper.update(new LambdaUpdateWrapper<VehVehicleInfo>().set(VehVehicleInfo::getClientId,null).eq(VehVehicleInfo::getId,bo.getId()));
}
/**
* 通过车辆id获取车辆的当前行程信息
* @param id
* @return
*/
@Override
public VehVehicleTripVo getTripInfo(Long id) {
VehVehicleInfoVo vehVehicleInfoVo = baseMapper.selectVoById(id);
if (vehVehicleInfoVo == null) {
throw new ServiceException("车辆信息为空");
}
if (!VehVehicleInfoStatusEnum.IN_USE.getValue().equals(vehVehicleInfoVo.getVehicleStatus())){
return null;
}
return vehVehicleTripService.getTripInfoByVehicleId(id);
}
}

View File

@ -476,6 +476,14 @@ public class VehVehicleTripServiceImpl extends ServiceImpl<VehVehicleTripMapper,
return baseMapper.getTripId(id);
}
@Override
public VehVehicleTripVo getTripInfoByVehicleId(Long id) {
return baseMapper.selectVoOne(new LambdaQueryWrapper<VehVehicleTrip>()
.eq(VehVehicleTrip::getVehicleId, id)
.orderByDesc(VehVehicleTrip::getCreateTime)
.last("limit 1"));
}
/**
* 构建用户行程及其对应申请的展示列表
*

View File

@ -0,0 +1,27 @@
package org.dromara.websocket.websocket.domain.vo;
import com.alibaba.excel.annotation.ExcelProperty;
import lombok.Data;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import java.math.BigDecimal;
@Data
public class VehicleVo {
/**
* 纬度精确到6位小数
*/
@ExcelProperty(value = "纬度", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "精=确到6位小数")
private BigDecimal locLatitude;
/**
* 经度精确到6位小数
*/
@ExcelProperty(value = "经度", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "精=确到6位小数")
private BigDecimal locLongitude;
}

View File

@ -0,0 +1,227 @@
package org.dromara.websocket.websocket.service;
import cn.hutool.json.JSONUtil;
import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.dromara.bigscreen.service.ProjectBigScreenService;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.gps.domain.bo.GpsEquipmentSonBo;
import org.dromara.gps.domain.vo.GpsEquipmentSonVo;
import org.dromara.gps.service.IGpsEquipmentSonService;
import org.dromara.websocket.websocket.domain.vo.VehicleVo;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
* 车辆轨迹 WebSocket 服务端(支持订阅消息)
* 端点路径:/websocket/vehicle
*/
@Slf4j
@ServerEndpoint("/websocket/vehicle") // 客户端连接时需携带订阅ID参数ws://xxx/websocket/vehicle?subscriptionId=xxx
@Component
public class VehicleWebSocketServer {
// 1. 存储所有在线会话sessionId -> Session
private static final Map<String, Session> ONLINE_SESSIONS = new ConcurrentHashMap<>();
// 2. 核心订阅ID与会话的映射subscriptionId -> Session
private static final Map<String, Session> SUBSCRIPTION_SESSIONS = new ConcurrentHashMap<>();
// 3. 反向映射会话ID与订阅ID的映射用于断开连接时清理订阅关系
private static final Map<String, String> SESSION_TO_SUBSCRIPTION = new ConcurrentHashMap<>();
// 当前会话对应的订阅ID每个连接实例的专属变量
private String currentSubscriptionId;
static {
log.info("✅ 车辆轨迹 WebSocket 服务端已随项目启动初始化!端点路径:/websocket/vehicle");
}
/**
* 客户端连接时触发解析订阅ID并建立映射关系
*/
@OnOpen
public void onOpen(Session session) {
// 从连接URL的查询参数中获取订阅ID客户端连接格式ws://xxx/websocket/vehicle?subscriptionId=123
Map<String, List<String>> params = session.getRequestParameterMap();
List<String> subscriptionIds = params.get("subscriptionId");
if (subscriptionIds != null && !subscriptionIds.isEmpty()) {
this.currentSubscriptionId = subscriptionIds.get(0); // 取第一个订阅ID
// 建立映射关系
SUBSCRIPTION_SESSIONS.put(currentSubscriptionId, session);
SESSION_TO_SUBSCRIPTION.put(session.getId(), currentSubscriptionId);
log.info("📌 客户端订阅成功订阅ID{}会话ID{},当前订阅数:{}",
currentSubscriptionId, session.getId(), SUBSCRIPTION_SESSIONS.size());
} else {
log.warn("📌 客户端连接未携带订阅ID会话ID{}", session.getId());
}
// 存储会话到在线列表
ONLINE_SESSIONS.put(session.getId(), session);
log.info("📌 客户端连接成功会话ID{},当前在线数:{}", session.getId(), ONLINE_SESSIONS.size());
// 异步推送初始化数据(原有逻辑保留)
CompletableFuture.runAsync(() -> {
try {
String[] split = currentSubscriptionId.split("-");
IGpsEquipmentSonService service = SpringUtils.getBean(IGpsEquipmentSonService.class);
GpsEquipmentSonBo bo = new GpsEquipmentSonBo();
bo.setUserId(Long.parseLong(split[0]));
bo.setTripId(Long.parseLong(split[1]));
List<GpsEquipmentSonVo> list = service.getNewVehicleList(bo);
if (list == null || list.isEmpty()) {
session.getBasicRemote().sendText("初始化数据为空");
log.warn("会话[{}]未获取到初始化数据", session.getId());
return;
}
List<VehicleVo> vehicleVos = new ArrayList<>();
for (GpsEquipmentSonVo ueClient : list) {
VehicleVo vo = new VehicleVo();
vo.setLocLatitude(ueClient.getLocLatitude());
vo.setLocLongitude(ueClient.getLocLongitude());
vehicleVos.add(vo);
}
session.getBasicRemote().sendText(JSONUtil.toJsonStr(vehicleVos));
log.info("📤 已向会话[{}]推送初始化数据,长度:{}字节", session.getId(), vehicleVos.size());
} catch (Exception e) {
log.error("会话[{}]初始化数据处理失败", session.getId(), e);
try {
if (session.isOpen()) {
session.getBasicRemote().sendText("初始化失败:" + e.getMessage());
}
} catch (IOException ex) {
log.error("会话[{}]推送错误信息失败", session.getId(), ex);
}
}
});
}
/**
* 接收客户端消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("📥 收到会话[{}]订阅ID{})消息:{}", session.getId(), currentSubscriptionId, message);
// 可选:回复客户端
try {
session.getBasicRemote().sendText("服务端已收到消息:" + message);
} catch (IOException e) {
log.error("📤 回复会话[{}]失败:{}", session.getId(), e.getMessage());
}
}
/**
* 客户端断开连接(清理订阅关系)
*/
@OnClose
public void onClose(Session session, CloseReason reason) {
// 1. 移除在线会话
ONLINE_SESSIONS.remove(session.getId());
// 2. 清理订阅关系
String subscriptionId = SESSION_TO_SUBSCRIPTION.get(session.getId());
if (subscriptionId != null) {
SUBSCRIPTION_SESSIONS.remove(subscriptionId);
SESSION_TO_SUBSCRIPTION.remove(session.getId());
log.info("🔌 客户端订阅关系已清除订阅ID{}会话ID{}", subscriptionId, session.getId());
}
log.info("🔌 客户端断开连接会话ID{},原因:{},当前在线数:{},当前订阅数:{}",
session.getId(), reason.getReasonPhrase(),
ONLINE_SESSIONS.size(), SUBSCRIPTION_SESSIONS.size());
}
/**
* 连接异常
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("⚠️ 会话[{}]订阅ID{})异常:{}", session.getId(), currentSubscriptionId, error.getMessage(), error);
}
// ------------------------------ 订阅消息发送工具方法(供外部调用) ------------------------------
/**
* 向指定订阅ID的客户端发送消息
* @param subscriptionId 订阅ID
* @param message 消息内容
* @return 是否发送成功
*/
public static boolean sendToSubscription(String subscriptionId, String message) {
if (subscriptionId == null || message == null) {
log.warn("⚠️ 订阅ID或消息为空发送失败");
return false;
}
// 从订阅映射中获取目标会话
Session session = SUBSCRIPTION_SESSIONS.get(subscriptionId);
if (session == null || !session.isOpen()) {
log.warn("⚠️ 订阅ID[{}]对应的客户端未连接或已断开", subscriptionId);
return false;
}
// 发送消息
try {
session.getBasicRemote().sendText(message);
log.info("📤 已向订阅ID[{}]发送消息:{}", subscriptionId, message);
return true;
} catch (IOException e) {
log.error("📤 向订阅ID[{}]发送消息失败", subscriptionId, e);
return false;
}
}
/**
* 向所有订阅客户端广播消息
* @param message 消息内容
*/
public static void broadcastToAllSubscriptions(String message) {
if (SUBSCRIPTION_SESSIONS.isEmpty()) {
log.warn("⚠️ 无订阅客户端,无需广播消息");
return;
}
SUBSCRIPTION_SESSIONS.forEach((subscriptionId, session) -> {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
log.info("📤 已向订阅ID[{}]广播消息", subscriptionId);
} catch (IOException e) {
log.error("📤 向订阅ID[{}]广播消息失败", subscriptionId, e);
}
}
});
}
/**
* 获取当前订阅数
*/
public static int getSubscriptionCount() {
return SUBSCRIPTION_SESSIONS.size();
}
// 原有工具方法保留
public static void sendToAll(String message) {
if (ONLINE_SESSIONS.isEmpty()) {
log.warn("⚠️ 无在线客户端,无需发送消息");
return;
}
ONLINE_SESSIONS.values().forEach(session -> {
if (session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
log.error("📤 向会话[{}]发送消息失败:{}", session.getId(), e.getMessage());
}
}
});
}
public static int getOnlineCount() {
return ONLINE_SESSIONS.size();
}
}

View File

@ -15,7 +15,9 @@ import org.dromara.xzd.comprehensive.service.IXzdCsContractChangeService;
import org.dromara.xzd.comprehensive.service.IXzdCsContractSuspendService;
import org.dromara.xzd.domain.bo.XzdBusinessSealBo;
import org.dromara.xzd.domain.vo.XzdBusinessSealVo;
import org.dromara.xzd.domain.vo.XzdProjectVo;
import org.dromara.xzd.service.IXzdBusinessSealService;
import org.dromara.xzd.service.IXzdProjectService;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.*;
import org.springframework.validation.annotation.Validated;
@ -55,6 +57,23 @@ public class XzdCsContractInformationController extends BaseController {
@Lazy
private final IXzdCsContractChangeService xzdCsContractChangeService;
@Lazy
private final IXzdProjectService xzdProjectService;
/**
* 获取项目信息详细信息
*
* @param id 主键
*/
@SaCheckPermission(value = {"comprehensive:csContractInformation:add","comprehensive:csContractInformation:edit","comprehensive:csContractInformation:list"},mode = SaMode.OR)
@GetMapping("/getProject/{id}")
public R<XzdProjectVo> getProjectInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(xzdProjectService.queryById(id));
}
/**
* 查询综合服务合同变更列表