add:添加示教接口

This commit is contained in:
2025-07-04 18:06:42 +08:00
parent 414d6c14fc
commit 950e404e8b
26 changed files with 1564 additions and 0 deletions

View File

@@ -0,0 +1,34 @@
package org.nl.apt15e;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@EnableAsync
@EnableScheduling
@SpringBootApplication
@EnableTransactionManagement
@RestController
@MapperScan("org.nl.apt15e.**.config")
public class Apt15EApplication {
public static void main(String[] args) {
SpringApplication.run(Apt15EApplication.class, args);
}
/**
* 访问首页提示
*
* @return /
*/
@GetMapping("/")
public String index() {
return "Backend service started successfully";
}
}

View File

@@ -0,0 +1,26 @@
package org.nl.apt15e.apt.dao;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* @author dsh
* 2025/7/2
*/
@Data
public class VehicleException implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 错误信息
*/
private List<String> exception;
/**
* 错误码
*/
private List<Long> exceptionCodes;
}

View File

@@ -0,0 +1,65 @@
package org.nl.apt15e.apt.dao;
import lombok.Data;
import java.io.Serializable;
/**
* @author dsh
* 2025/7/2
*/
@Data
public class VehicleInfo implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 车辆 ID
*/
private String id;
/**
* 车辆名称
*/
private String name;
/**
* 车辆 IP
*/
private String ip;
/**
* 车辆所在地图 ID
*/
private Long mapId;
/**
* 车辆所在地图 名称
*/
private String mapName;
/**
* 车辆状态 ID
*/
private Long stateId;
/**
* 车辆状态名称
*/
private String state;
/**
* 区域 ID
*/
private Long areaId;
/**
* 电量
*/
private Long batteryPower;
/**
* 异常信息
*/
private VehicleException exceptionInfo;
}

View File

@@ -0,0 +1,30 @@
package org.nl.apt15e.apt.rest;
import lombok.extern.slf4j.Slf4j;
import org.nl.apt15e.apt.dao.VehicleInfo;
import org.nl.apt15e.apt.service.VehicleInfoService;
import org.nl.apt15e.common.logging.annotation.Log;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @author dsh
* 2025/7/3
*/
@Slf4j
@RestController
@RequestMapping("/vehicle")
public class VehicleInfoController {
@Resource
private VehicleInfoService vehicleInfoService;
@GetMapping("/getVehicleInfo")
// @Log("获取车辆信息")
public VehicleInfo getVehicleInfo() {
return vehicleInfoService.getVehicleInfo();
}
}

View File

@@ -0,0 +1,13 @@
package org.nl.apt15e.apt.service;
import org.nl.apt15e.apt.dao.VehicleInfo;
/**
* @author dsh
* 2025/7/2
*/
public interface VehicleInfoService {
VehicleInfo getVehicleInfo();
}

View File

@@ -0,0 +1,98 @@
package org.nl.apt15e.apt.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.nl.apt15e.apt.dao.VehicleException;
import org.nl.apt15e.apt.dao.VehicleInfo;
import org.nl.apt15e.apt.service.VehicleInfoService;
import org.nl.apt15e.apt.websocket.WebSocketVehicleServer;
import org.nl.apt15e.util.HTTPUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author dsh
* 2025/7/2
*/
@Slf4j
@Service
public class VehicleInfoServiceImpl implements VehicleInfoService {
@Resource
private TaskScheduler scheduler;
public static VehicleInfo vehicleInfo = new VehicleInfo();
@Override
public VehicleInfo getVehicleInfo() {
return vehicleInfo;
}
@Async("asynchronousTasks")
public void queryVehicleInfo() {
HttpResponse response = null;
try {
response = HTTPUtil.get("http://192.168.100.82:8081","/amr/onlineAmr",null);
// 检查响应状态码
if (response!=null && response.isOk()) {
// 获取响应体内容
String body = response.body();
JSONArray jsonArray = JSONObject.parseObject(body).getJSONArray("data");
if (jsonArray.isEmpty()) {
log.info("车辆数据为空");
}
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject data = jsonArray.getJSONObject(i);
vehicleInfo = JSONObject.toJavaObject(data, VehicleInfo.class);
//电量
vehicleInfo.setBatteryPower(data.getLong("batteryPercentile"));
//上报的异常信息
VehicleException vehicleException = JSONObject.toJavaObject(data.getJSONObject("amrException"), VehicleException.class);
vehicleInfo.setExceptionInfo(vehicleException);
System.out.println("Response Body: " + data);
System.out.println("vehicleInfo: " + vehicleInfo);
}
} else {
log.info("查询调度车辆信息失败:{}",response);
}
} catch (Exception e) {
log.info("访问调度报错{}", e.getMessage());
}
if (response != null) {
response.close();
}
}
@Async("asynchronousTasks")
public void sendVehicleInfo() {
CopyOnWriteArraySet<WebSocketVehicleServer> webSocketSet =
WebSocketVehicleServer.getWebSocketSet();
if (webSocketSet.size() > 0) {
webSocketSet.forEach(c -> {
Map<String, Object> vehicleInfoMap = new HashMap<>();
vehicleInfoMap.put("data", vehicleInfo);
c.sendDataToClient(vehicleInfoMap);
});
}
}
@PostConstruct
public void init() {
scheduler.scheduleAtFixedRate(this::queryVehicleInfo,4000);
scheduler.scheduleAtFixedRate(this::sendVehicleInfo, 4000);
}
}

View File

@@ -0,0 +1,50 @@
package org.nl.apt15e.apt.station.dao;
import lombok.Data;
import java.io.Serializable;
/**
* @author dsh
* 2025/7/4
*/
@Data
public class Station implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 站点标识
*/
private String station_id;
/**
* 站点编码 对应地图上站点
*/
private String station_code;
/**
* 站点别名
*/
private String station_name;
/**
* 动作类型
*/
private String action_type;
/**
* x坐标
*/
private Double x;
/**
* y坐标
*/
private Double y;
/**
* 角度
*/
private Double angle;
}

View File

@@ -0,0 +1,77 @@
package org.nl.apt15e.apt.teaching.rest;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.annotations.Param;
import org.nl.apt15e.apt.teaching.service.TeachingService;
import org.nl.apt15e.common.logging.annotation.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
/**
* @author dsh
* 2025/7/3
*/
@Slf4j
@RestController
@RequestMapping("/teaching")
public class TeachingController {
@Resource
private TeachingService teachingService;
@PostMapping("/startMapping")
// @Log("开始建图")
private ResponseEntity<Object> startMapping(@RequestParam("mapName") String mapName) {
return new ResponseEntity<>(teachingService.startMapping(mapName), HttpStatus.OK);
}
@PostMapping("/stopMapping")
private ResponseEntity<Object> stopMapping() {
return new ResponseEntity<>(teachingService.stopMapping(), HttpStatus.OK);
}
@PostMapping("/setStation")
private ResponseEntity<Object> setStation(@RequestParam("stationName") String stationName) {
return new ResponseEntity<>(teachingService.setStation(stationName), HttpStatus.OK);
}
@PostMapping("/deployRunMap")
private ResponseEntity<Object> deployRunMap(@RequestParam("mapName") String mapName) {
return new ResponseEntity<>(teachingService.deployRunMap(mapName), HttpStatus.OK);
}
@PostMapping("/changeCurrentRunMap")
private ResponseEntity<Object> changeCurrentRunMap(@RequestParam("mapName") String mapName) {
return new ResponseEntity<>(teachingService.changeCurrentRunMap(mapName), HttpStatus.OK);
}
@PostMapping("/getLocalMaps")
private ResponseEntity<Object> getLocalMaps() {
return new ResponseEntity<>(teachingService.getLocalMaps(), HttpStatus.OK);
}
@PostMapping("/getRunMapZip")
private ResponseEntity<Object> getRunMapZip(@RequestParam("mapName") String mapName) {
return new ResponseEntity<>(teachingService.getRunMapZip(mapName), HttpStatus.OK);
}
@PostMapping("/synchronizeMap")
private ResponseEntity<Object> synchronizeMap(@RequestParam("mapName") String mapName) {
return new ResponseEntity<>(teachingService.synchronizeMap(mapName), HttpStatus.OK);
}
@PostMapping("/restart")
private ResponseEntity<Object> restart() {
return new ResponseEntity<>(teachingService.restart(), HttpStatus.OK);
}
@PostMapping("/relocate")
private ResponseEntity<Object> relocate(@RequestParam("x") Double x, @RequestParam("y") Double y, @RequestParam("angle") Double angle) {
return new ResponseEntity<>(teachingService.relocate(x, y, angle), HttpStatus.OK);
}
}

View File

@@ -0,0 +1,81 @@
package org.nl.apt15e.apt.teaching.service;
import java.io.File;
import java.util.Map;
/**
* @author dsh
* 2025/7/3
*/
public interface TeachingService {
/**
* 开始建图
*/
Map<String, Object> startMapping(String mapName);
/**
* 切手动
*/
Map<String, Object> startManual();
/**
* 切自动
*/
Map<String, Object> stopManual();
/**
* 结束建图
*/
Map<String, Object> stopMapping();
/**
* 建图过程中设置站点
*/
Map<String, Object> setStation(String stationName);
/**
* 获取后台地图列表
*/
Map<String, Object> getLocalMaps();
/**
* 部署地图
* @param mapName
* @return
*/
Map<String, Object> deployRunMap(String mapName);
/**
* 应用地图
* @param mapName
* @return
*/
Map<String, Object> changeCurrentRunMap(String mapName);
/**
* 获取主机地图包
* @param mapName
* @return
*/
File getRunMapZip(String mapName);
/**
* 同步地图到调度
*/
Map<String, Object> synchronizeMap(String mapName);
/**
* 重定位
* @param x
* @param y
* @param angle
* @return
*/
Map<String, Object> relocate(Double x,Double y,Double angle);
/**
* 重启车辆后台程序
*/
Map<String, Object> restart();
}

View File

@@ -0,0 +1,350 @@
package org.nl.apt15e.apt.teaching.service.impl;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.nl.apt15e.apt.dao.VehicleException;
import org.nl.apt15e.apt.dao.VehicleInfo;
import org.nl.apt15e.apt.teaching.service.TeachingService;
import org.nl.apt15e.common.BadRequestException;
import org.nl.apt15e.util.HTTPUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* @author dsh
* 2025/7/3
*/
@Slf4j
@Service
public class TeachingServiceImpl implements TeachingService {
@Override
public Map<String, Object> startMapping(String mapName) {
if ("".equals(mapName)){
throw new BadRequestException("mapName is empty");
}
JSONObject params = new JSONObject();
params.put("name", mapName);
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/startMapping", params);
} catch (Exception e) {
log.info("访问车体开始建图接口报错:{}",e.getMessage());
throw new BadRequestException("开始建图失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("开始建图:{}",body);
if ("200".equals(body.getString("code"))){
body =(JSONObject) this.startManual();
}
return body;
}
log.info("开始建图失败");
throw new BadRequestException("开始建图失败");
}
@Override
public Map<String, Object> startManual() {
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/startManual", new JSONObject());
} catch (Exception e) {
log.info("访问车体切手动接口报错:{}",e.getMessage());
throw new BadRequestException("切手动失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("切手动:{}",body);
return body;
}
log.info("切手动失败");
throw new BadRequestException("切手动失败");
}
@Override
public Map<String, Object> stopManual() {
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/stopManual", new JSONObject());
} catch (Exception e) {
log.info("访问车体切自动接口报错:{}",e.getMessage());
throw new BadRequestException("切自动失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("切自动:{}",body);
return body;
}
log.info("切自动失败");
throw new BadRequestException("切自动失败");
}
@Override
public Map<String, Object> stopMapping() {
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/stopMapping", new JSONObject());
} catch (Exception e) {
log.info("访问车体结束建图接口报错:{}",e.getMessage());
throw new BadRequestException("结束建图失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("结束建图:{}",body);
if ("200".equals(body.getString("code"))){
body =(JSONObject) this.stopManual();
}
return body;
}
log.info("结束建图失败");
throw new BadRequestException("结束建图失败");
}
@Override
public Map<String, Object> setStation(String stationName) {
if ("".equals(stationName)){
throw new BadRequestException("spotCode is empty");
}
JSONObject params = new JSONObject();
params.put("spotCode", stationName);
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/setStates", params);
} catch (Exception e) {
log.info("访问车体设置站点接口报错:{}",e.getMessage());
throw new BadRequestException("设置站点失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("设置站点:{}",body);
return body;
}
log.info("设置站点失败");
throw new BadRequestException("设置站点失败");
}
@Override
public Map<String, Object> getLocalMaps() {
HttpResponse response = null;
try {
response = HTTPUtil.get("http://192.168.100.82:9998","/tool/editor/getLocalMaps", new JSONObject());
} catch (Exception e) {
log.info("访问车体地图列表接口报错:{}",e.getMessage());
throw new BadRequestException("获取地图列表失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("获取地图列表:{}",body);
return body;
}
log.info("获取地图列表失败");
throw new BadRequestException("获取地图列表失败");
}
@Override
public Map<String, Object> deployRunMap(String mapName) {
if ("".equals(mapName)){
throw new BadRequestException("mapName is empty");
}
JSONObject params = new JSONObject();
params.put("id", mapName);
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/deployRunMap", params);
} catch (Exception e) {
log.info("访问车体部署地图接口报错:{}",e.getMessage());
throw new BadRequestException("部署地图失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("部署地图:{}",body);
return body;
}
log.info("部署地图失败");
throw new BadRequestException("部署地图失败");
}
@Override
public Map<String, Object> changeCurrentRunMap(String mapName) {
if ("".equals(mapName)){
throw new BadRequestException("mapName is empty");
}
JSONObject params = new JSONObject();
params.put("name", mapName);
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/changeCurrentRunMap", params);
} catch (Exception e) {
log.info("访问车体应用地图接口报错:{}",e.getMessage());
throw new BadRequestException("应用地图失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("应用地图:{}",body);
return body;
}
log.info("应用地图失败");
throw new BadRequestException("应用地图失败");
}
@Override
public File getRunMapZip(String mapName) {
if ("".equals(mapName)){
throw new BadRequestException("mapName is empty");
}
JSONObject params = new JSONObject();
params.put("name", mapName);
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/getRunMapZip", params);
} catch (Exception e) {
log.info("访问车体地图包接口报错:{}",e.getMessage());
throw new BadRequestException("获取地图包失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
// JSONObject body = JSON.parseObject(response.body());
// 3. 将响应体写入临时ZIP文件
byte[] zipBytes = response.bodyBytes();
File fileName = new File(mapName);
File tempZipFile = FileUtil.writeBytes(zipBytes, FileUtil.createTempFile(fileName));
// 4. 解压ZIP文件
// File unzipDir = ZipUtil.unzip(tempZipFile, Charset.defaultCharset());
// 5. 处理解压后的文件
// File[] files = unzipDir.listFiles();
// if (files != null) {
// for (File file : files) {
// System.out.println("文件名: " + file.getName());
// System.out.println("文件内容: ");
// System.out.println(FileUtil.readUtf8String(file)); // 读取文本内容
// // 如果是二进制文件使用FileUtil.readBytes(file)
// }
// }
// 6. 清理临时文件(可选)
// FileUtil.del(tempZipFile);
// FileUtil.del(unzipDir);
log.info("获取地图包");
return tempZipFile;
}
log.info("获取地图包失败");
throw new BadRequestException("获取地图包失败");
}
@Override
public Map<String, Object> synchronizeMap(String mapName) {
File zipFile = this.getRunMapZip(mapName);
HttpResponse response = null;
try {
response = HttpRequest.post("http://192.168.100.82:8081/map/uploadFile")
.setConnectionTimeout(3000)
.setReadTimeout(3000)
.header("token", "admin123")
.header("name", "lx-script")
.header("Content-Type", "multipart/form-data;charset=UTF-8")
.form("areaId",1)
.form("areaName","")
.form("data",zipFile)
.execute();
} catch (Exception e) {
log.info("同步地图到调度接口报错:{}",e.getMessage());
throw new BadRequestException("同步地图失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("同步地图:{}",body);
return body;
}
log.info("同步地图失败");
throw new BadRequestException("同步地图失败");
}
@Override
public Map<String, Object> relocate(Double x, Double y, Double angle) {
if (ObjectUtil.isEmpty(x) || ObjectUtil.isEmpty(y) || ObjectUtil.isEmpty(angle)){
throw new BadRequestException("params is empty");
}
JSONObject params = new JSONObject();
params.put("x", x);
params.put("y", y);
params.put("angle", angle / 180.0 * Math.PI);
params.put("noisyX", 0.5);
params.put("noisyY", 0.5);
params.put("noisyAngle", Math.PI);
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/relocate", params);
} catch (Exception e) {
log.info("访问车体重定位接口报错:{}",e.getMessage());
throw new BadRequestException("重定位失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("重定位:{}",body);
return body;
}
log.info("重定位失败");
throw new BadRequestException("重定位失败");
}
@Override
public Map<String, Object> restart() {
HttpResponse response = null;
try {
response = HTTPUtil.post("http://192.168.100.82:9998","/tool/rob/restart", new JSONObject());
} catch (Exception e) {
log.info("访问车体程序重启接口报错:{}",e.getMessage());
throw new BadRequestException("车体程序重启失败");
}
// 检查响应状态码
if (response.isOk()) {
// 获取响应体内容
JSONObject body = JSON.parseObject(response.body());
log.info("车体程序重启:{}",body);
return body;
}
log.info("车体程序重启失败");
throw new BadRequestException("车体程序重启失败");
}
}

View File

@@ -0,0 +1,126 @@
package org.nl.apt15e.apt.websocket;
import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* @author dsh
* 2025/7/3
*/
@Slf4j
@ServerEndpoint("/webSocket/VehicleInfo/{sid}")
@Component
public class WebSocketVehicleServer {
/**
* concurrent包的线程安全Set用来存放每个客户端对应的MyWebSocket对象。
*/
private static CopyOnWriteArraySet<WebSocketVehicleServer> webSocketSet = new CopyOnWriteArraySet<>();
/**
* 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。
*/
private static int onlineCount = 0;
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
/**
* 接收userId
*/
private String sid = "";
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
this.session = session;
//如果存在就先删除一个,防止重复推送消息
webSocketSet.removeIf(webSocket -> webSocket.sid.equals(sid));
webSocketSet.add(this);
//在线数加1
addOnlineCount();
log.info("VehicleWS:sid{}连接成功,当前在线人数为{}", sid, getOnlineCount());
this.sid = sid;
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this);
//在线数减1
subOnlineCount();
log.info("VehicleWS:sid{}关闭连接!当前在线人数为{}", sid, getOnlineCount());
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
//System.out.println(webSocketSet.size() + "_接收到消息_" + session.getId());
}
@OnError
public void onError(Session session, Throwable error) {
//log.error("发生错误");
webSocketSet.remove(session);
error.printStackTrace();
}
public Session getSession() {
return session;
}
// 发送消息,在定时任务中会调用此方法
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);
}
public void sendDataToClient(Map<String, Object> data) {
try {
if (this.session != null&& MapUtil.isNotEmpty(data)) {
this.session.getBasicRemote().sendText(JSON.toJSONString(data));
}
} catch (IOException e) {
log.error("发送消息给客户端失败", e);
}
}
public static synchronized int getOnlineCount() {
return onlineCount;
}
public static CopyOnWriteArraySet<WebSocketVehicleServer> getWebSocketSet() {
return webSocketSet;
}
public void setSession(Session session) {
this.session = session;
}
public static synchronized void addOnlineCount() {
WebSocketVehicleServer.onlineCount++;
}
public static synchronized void subOnlineCount() {
WebSocketVehicleServer.onlineCount--;
}
}

View File

@@ -0,0 +1,25 @@
package org.nl.apt15e.common;
import lombok.Getter;
import org.springframework.http.HttpStatus;
import static org.springframework.http.HttpStatus.BAD_REQUEST;
/**
* @author dsh
* 2025/7/3
*/
@Getter
public class BadRequestException extends RuntimeException{
private Integer status = BAD_REQUEST.value();
public BadRequestException(String msg){
super(msg);
}
public BadRequestException(HttpStatus status, String msg){
super(msg);
this.status = status.value();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.nl.apt15e.common.exception;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
@Data
class ApiError {
private Integer code = 400;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime Date;
private String message;
private ApiError() {
Date = LocalDateTime.now();
}
public static ApiError error(String message){
ApiError apiError = new ApiError();
apiError.setMessage(message);
return apiError;
}
public static ApiError error(Integer status, String message){
ApiError apiError = new ApiError();
apiError.setCode(status);
apiError.setMessage(message);
return apiError;
}
}

View File

@@ -0,0 +1,78 @@
package org.nl.apt15e.common.exception;
import lombok.extern.slf4j.Slf4j;
import org.nl.apt15e.common.BadRequestException;
import org.nl.apt15e.util.ThrowableUtil;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Objects;
/**
* @author liejiu
*/
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理所有不可知的异常
*/
@ExceptionHandler(Throwable.class)
public ResponseEntity<ApiError> handleException(Throwable e){
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
return buildResponseEntity(ApiError.error(e.getMessage()));
}
/**
* token 无效的异常拦截
* @param e
* @return
*/
// @ExceptionHandler(value = NotLoginException.class)
// public ResponseEntity<ApiError> notLoginException(Exception e) {
//// log.error(ThrowableUtil.getStackTrace(e));
// return buildResponseEntity(ApiError.error(401,"token 失效"));
// }
/**
* 处理自定义异常
*/
@ExceptionHandler(value = BadRequestException.class)
public ResponseEntity<ApiError> badRequestException(BadRequestException e) {
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
log.info(e.getMessage());
return buildResponseEntity(ApiError.error(e.getStatus(),e.getMessage()));
}
/**
* 处理所有接口数据验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiError> handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
// 打印堆栈信息
log.error(ThrowableUtil.getStackTrace(e));
String[] str = Objects.requireNonNull(e.getBindingResult().getAllErrors().get(0).getCodes())[1].split("\\.");
String message = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();
String msg = "不能为空";
if(msg.equals(message)){
message = str[1] + ":" + message;
}
return buildResponseEntity(ApiError.error(message));
}
/**
* 统一返回
*/
private ResponseEntity<ApiError> buildResponseEntity(ApiError apiError) {
return new ResponseEntity<>(apiError, HttpStatus.valueOf(apiError.getCode()));
}
}

View File

@@ -0,0 +1,17 @@
package org.nl.apt15e.common.logging.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author dsh
* 2025/7/3
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
String value() default "";
}

View File

@@ -0,0 +1,66 @@
package org.nl.apt15e.common.logging.aspect;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author dsh
* 2025/7/3
*/
@Slf4j
@Aspect
@Component
public class LogAspect {
/**
* 配置切入点
*/
@Pointcut("@annotation(org.nl.apt15e.common.logging.annotation.Log)")
public void logPointCut(){}
/**
* 环绕通知
* @param joinPoint
* @return
* @throws Throwable
*/
@Around("logPointCut()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// MethodSignature signature = (MethodSignature) joinPoint.getSignature();
// Method method = signature.getMethod();
// String className = joinPoint.getTarget().getClass().getName();
// 方法路径
// String methodName = joinPoint.getTarget().getClass().getName() + "." + signature.getName() + "()";
// String params = JSONObject.toJSONString(joinPoint.getArgs());
Object result;
// log.info("【日志注解】开始执行 -- {}:{} {}", className, "111", params);
result = joinPoint.proceed();
// log.info("返回参数:{}" ,JSONObject.toJSONString(result));
return result;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2019-2020 Zheng Jie
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.nl.apt15e.config.thread;
import lombok.extern.slf4j.Slf4j;
import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 异步任务线程池装配类
*
* @author https://juejin.im/entry/5abb8f6951882555677e9da2
* @date 2019年10月31日15:06:18
*/
@Slf4j
@Configuration
@EnableAsync
public class AsyncTaskExecutePool implements AsyncConfigurer{
@Override
public Executor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
//核心线程池大小
executor.setCorePoolSize(20);
//最大线程数
executor.setMaxPoolSize(50);
//队列容量
executor.setQueueCapacity(50);
//活跃时间
executor.setKeepAliveSeconds(60);
//线程名字前缀
executor.setThreadNamePrefix("Async-");
// setRejectedExecutionHandler当pool已经达到max size的时候如何处理新任务
// CallerRunsPolicy不在新线程中执行任务而是由调用者所在的线程来执行
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
/**
* 线程池配置
*/
@Bean(name = "asynchronousTasks")
public ThreadPoolTaskExecutor threadPoolTaskExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
// 核心线程池大小
threadPoolTaskExecutor.setCorePoolSize(20);
// 最大线程数
threadPoolTaskExecutor.setMaxPoolSize(50);
// 队列容量
threadPoolTaskExecutor.setQueueCapacity(50);
// 活跃时间
threadPoolTaskExecutor.setKeepAliveSeconds(60);
// 主线程等待子线程执行时间
threadPoolTaskExecutor.setAwaitTerminationSeconds(50);
// threadPoolTaskExecutor.setAwaitTerminationSeconds(30);
// 线程名字前缀
threadPoolTaskExecutor.setThreadNamePrefix("apt-thread-");
// RejectedExecutionHandler:当pool已经达到max-size的时候如何处理新任务
// CallerRunsPolicy:不在新线程中执行任务,而是由调用者所在的线程来执行
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
// 初始化
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}

View File

@@ -0,0 +1,19 @@
package org.nl.apt15e.config.thread;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* @author dsh
* 2025/7/3
*/
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}

View File

@@ -0,0 +1,40 @@
package org.nl.apt15e.util;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
/**
* @author dsh
* 2025/7/2
*/
@Slf4j
public class HTTPUtil {
public static HttpResponse post(String url, String method, JSONObject params)throws Exception{
String sendUrl = url + method;
return HttpRequest.post(sendUrl)
.setConnectionTimeout(3000)
.setReadTimeout(3000)
.header("token", "admin123")
.header("name", "lx-script")
.header("Content-Type", "application/json")
.body(String.valueOf(params))
.execute();
}
public static HttpResponse get(String url, String method, JSONObject params)throws Exception{
String sendUrl = url + method;
return HttpRequest.get(sendUrl)
.setConnectionTimeout(3000)
.setReadTimeout(3000)
.header("token", "admin123")
.header("name", "lx-script")
.header("Content-Type", "application/json")
.form(params)
.execute();
}
}

View File

@@ -0,0 +1,22 @@
package org.nl.apt15e.util;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* @author liejiu
*/
public class ThrowableUtil {
/**
* 获取堆栈信息
*/
public static String getStackTrace(Throwable throwable){
StringWriter sw = new StringWriter();
try (PrintWriter pw = new PrintWriter(sw)) {
throwable.printStackTrace(pw);
return sw.toString();
}
}
}

View File

@@ -0,0 +1,8 @@
_ _ ___________ _ _____ _ ___________ _____
| \ | | _ | ___ \ | | ___| | | ___| ___|_ _|
| \| | | | | |_/ / | | |__ | | | |__ | |_ | |
| . ` | | | | ___ \ | | __|| | | __|| _| | |
| |\ \ \_/ / |_/ / |____| |___| |____| |___| | | |
\_| \_/\___/\____/\_____/\____/\_____/\____/\_| \_/
:: Spring Boot :: (v2.6.13.RELEASE)

View File

@@ -0,0 +1,52 @@
server:
# 端口
port: 8081
spring:
datasource:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/apt_data?serverTimezone=GMT%2B8&characterEncoding=utf-8&userSSL=false
username: root
password: 123456
# 初始连接数
initial-size: 5
# 最小连接数
min-idle: 15
# 最大连接数
max-active: 30
# 超时时间(以秒数为单位)
remove-abandoned-timeout: 180
# 获取连接超时时间
max-wait: 3000
# 连接有效性检测时间
time-between-eviction-runs-millis: 60000
# 连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
# 连接在池中最大生存的时间
max-evictable-idle-time-millis: 900000
# 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除
test-while-idle: true
# 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个
test-on-borrow: true
# 是否在归还到池中前进行检验
test-on-return: false
# 检测连接是否有效
validation-query: select 1
# 配置监控统计
webStatFilter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
filter:
stat:
enabled: true
# 记录慢SQL
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true

View File

@@ -0,0 +1,52 @@
server:
# 端口
port: 8081
spring:
datasource:
druid:
db-type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/apt_data?serverTimezone=GMT%2B8&characterEncoding=utf-8&userSSL=false
username: root
password: 123456
# 初始连接数
initial-size: 5
# 最小连接数
min-idle: 15
# 最大连接数
max-active: 30
# 超时时间(以秒数为单位)
remove-abandoned-timeout: 180
# 获取连接超时时间
max-wait: 3000
# 连接有效性检测时间
time-between-eviction-runs-millis: 60000
# 连接在池中最小生存的时间
min-evictable-idle-time-millis: 300000
# 连接在池中最大生存的时间
max-evictable-idle-time-millis: 900000
# 指明连接是否被空闲连接回收器(如果有)进行检验.如果检测失败,则连接将被从池中去除
test-while-idle: true
# 指明是否在从池中取出连接前进行检验,如果检验失败, 则从池中去除连接并尝试取出另一个
test-on-borrow: true
# 是否在归还到池中前进行检验
test-on-return: false
# 检测连接是否有效
validation-query: select 1
# 配置监控统计
webStatFilter:
enabled: true
stat-view-servlet:
enabled: true
url-pattern: /druid/*
reset-enable: false
filter:
stat:
enabled: true
# 记录慢SQL
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: true
wall:
config:
multi-statement-allow: true

View File

@@ -0,0 +1,18 @@
spring:
profiles:
active: dev
jackson:
time-zone: GMT+8
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:/mapper/**/*.xml
global-config:
banner: false
logging:
file:
path: logs
config: classpath:logback-spring.xml

View File

@@ -0,0 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- 级别[ALL< Trace < DEBUG < INFO < WARN < ERROR < FATAL < OFF] -->
<!-- monitorInterval(单位s)指定log4j自动重新配置的监测间隔时间-->
<configuration scan="true" scanPeriod="30 seconds" debug="false">
<property name="log.charset" value="utf-8"/>
<property name="log.pattern" value="%black(nlAdmin-) %red(%d{yyyy-MM-dd HH:mm:ss.SSS}) %green([%thread]) %highlight(%-5level) %boldMagenta(%logger{36} - %method) - %white(%msg%n)"/>
<SpringProperty scope="context" name="logPath" source="logging.file.path" defaultValue="logs"/>
<property name="LOG_HOME" value="${logPath}"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>INFO</level>
</filter>
<encoder>
<pattern>${log.pattern}</pattern>
<charset>${log.charset}</charset>
</encoder>
</appender>
<!-- 系统日志输出 appender class 中的log.pattern 表示日志滚动输出 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志首次输出的文件地址 -->
<!-- <file>${LOG_HOME}/info.log</file>-->
<!-- 滚动输出策略:基于时间创建日志文件 ,这样第二天输出的日志,就会按照 fileNamePattern 新建日志文件 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 日志文件名格式 -->
<fileNamePattern>${LOG_HOME}/root/info.%d{yyyy-MM-dd}.log</fileNamePattern>
<!-- 日志最大的历史 60天 -->
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<!-- 日志内容输出格式设置为定义好的 log.pattern-->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>${log.charset}</charset>
</encoder>
<!-- 日志内容输出过滤器 -->
<!-- <filter class="ch.qos.logback.classic.filter.LevelFilter">-->
<!-- &lt;!&ndash; 过滤的级别 &ndash;&gt;-->
<!-- <level>INFO</level>-->
<!-- &lt;!&ndash; 匹配时的操作:接收(记录) &ndash;&gt;-->
<!-- <onMatch>ACCEPT</onMatch>-->
<!-- &lt;!&ndash; 不匹配时的操作:拒绝(不记录) &ndash;&gt;-->
<!-- <onMismatch>DENY</onMismatch>-->
<!-- </filter>-->
</appender>
<!-- <appender name="async" class="ch.qos.logback.classic.AsyncAppender">-->
<!-- &lt;!&ndash; 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 &ndash;&gt;-->
<!-- <discardingThreshold>0</discardingThreshold>-->
<!-- <queueSize>256</queueSize>-->
<!-- <appender-ref ref="console"/>-->
<!-- </appender>-->
<!-- <appender name="async_file_info" class="ch.qos.logback.classic.AsyncAppender">-->
<!-- <discardingThreshold>0</discardingThreshold>-->
<!-- <queueSize>256</queueSize>-->
<!-- <appender-ref ref="file_info"/>-->
<!-- </appender>-->
<!--系统操作日志 root 根路径的日志级别 info -->
<root level="info">
<!-- 将定义好的几个日志输出 追加到 root 上 -->
<!-- <appender-ref ref="console"/>-->
<appender-ref ref="console" />
<!-- <appender-ref ref="async_file_info" />-->
</root>
</configuration>

View File

@@ -0,0 +1,13 @@
package org.nl.apt15e;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class Apt15EApplicationTests {
@Test
void contextLoads() {
}
}