This commit is contained in:
2025-12-08 14:50:27 +08:00
parent 74bee3e232
commit 6168f4c57d
25 changed files with 492 additions and 194 deletions

View File

@ -77,7 +77,7 @@ public class AuthGenerator {
public static void main(String[] args) {
try {
// 生成加密的授权字符串
String authContent = generateAuth("标准版", 1000, 365, "8B1FB12E9F8E80109724989E0B25773B");
String authContent = generateAuth("标准版", 1000, 1, "25F429FDA965007B72BB7A6B2C03535A");
// 定义授权文件路径(当前目录下的 yjearth.YJ
Path licPath = Paths.get("yjearth.YJ");
@ -92,3 +92,4 @@ public class AuthGenerator {
}
}
}

View File

@ -10,18 +10,25 @@ import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Tag(name = "系统设置管理")
@RestController
@RequestMapping("/auth")
public class AuthController {
// 授权文件存储路径、项目根目录下的 license 目录
private static final String AUTH_FILE_PATH = "license/yjearth.YJ";
// 授权文件存储目录(项目根目录下的 license 目录
private static final String LICENSE_DIR = "license";
// 授权文件后缀(固定为.YJ
private static final String AUTH_FILE_SUFFIX = ".YJ";
@GetMapping("/info")
@Operation(summary = "获取系统授权码")
@ -37,28 +44,34 @@ public class AuthController {
return ApiResponse.failure("请选择授权文件");
}
// 验证文件名是否为 yjearth.YJ
String fileName = file.getOriginalFilename();
if (fileName == null || !fileName.equals("yjearth.YJ")) {
return ApiResponse.failure("请上传 yjearth.YJ");
// 验证文件名不为空且后缀为.YJ
if (fileName == null || !fileName.endsWith(AUTH_FILE_SUFFIX)) {
return ApiResponse.failure("授权码错误、请联系管理员提供正确的授权文件");
}
try {
// 读取文件内容
// 读取文件内容并验证有效性
String authContent = new String(file.getBytes(), StandardCharsets.UTF_8).trim();
// 验证授权内容有效性
String serverHardwareMd5 = ServerUniqueIdUtil.getServerUniqueId();
boolean isValid = AuthValidator.validateAuth(authContent, serverHardwareMd5);
if (!isValid) {
return ApiResponse.failure("授权文件无效或已过期");
return ApiResponse.failure("授权码错误、请联系管理员提供正确的授权文件");
}
// 创建目录(如果不存在)
Path path = Paths.get(AUTH_FILE_PATH);
if (!Files.exists(path.getParent())) {
Files.createDirectories(path.getParent());
// 处理license目录不存在则创建
Path licenseDirPath = Paths.get(LICENSE_DIR);
if (!Files.exists(licenseDirPath)) {
Files.createDirectories(licenseDirPath);
}
// 保存授权文件
Files.write(path, authContent.getBytes(StandardCharsets.UTF_8));
// 删除目录下已存在的所有.YJ文件确保仅保留一个
deleteAllYjFilesInLicenseDir();
// 保存新的授权文件(使用原文件名)
Path newAuthFilePath = licenseDirPath.resolve(fileName);
Files.write(newAuthFilePath, authContent.getBytes(StandardCharsets.UTF_8));
return ApiResponse.success(null);
} catch (Exception e) {
return ApiResponse.failure("导入授权文件失败: " + e.getMessage());
@ -69,18 +82,60 @@ public class AuthController {
@Operation(summary = "查看授权信息")
public ApiResponse showAuth() {
try {
// 检查授权文件是否存在
Path path = Paths.get(AUTH_FILE_PATH);
if (!Files.exists(path)) {
return ApiResponse.failure("请先导入授权");
// 查找license目录下的所有.YJ文件
List<Path> yjFileList = findAllYjFilesInLicenseDir();
// 处理文件数量异常
if (yjFileList.isEmpty()) {
return ApiResponse.successWithMessage("未找到授权文件");
}
// 读取授权文件内容
String authContent = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
// 获取授权详情
if (yjFileList.size() > 1) {
return ApiResponse.successWithMessage("授权目录下存在多个" + AUTH_FILE_SUFFIX + "文件,请删除多余文件后重试");
}
// 读取唯一的.YJ文件内容
Path authFilePath = yjFileList.get(0);
String authContent = new String(Files.readAllBytes(authFilePath), StandardCharsets.UTF_8);
// 解析并返回授权信息
AuthInfo authInfo = AuthValidator.getAuthInfo(authContent);
return ApiResponse.success(authInfo);
} catch (Exception e) {
return ApiResponse.failure("获取授权信息失败: " + e.getMessage());
return ApiResponse.successWithMessage("授权码错误、请联系管理员提供正确的授权文件");
}
}
/**
* 查找 license 目录下所有后缀为 .YJ 的文件
*/
private List<Path> findAllYjFilesInLicenseDir() throws IOException {
Path licenseDirPath = Paths.get(LICENSE_DIR);
if (!Files.exists(licenseDirPath)) {
return Collections.emptyList();
}
if (!Files.isDirectory(licenseDirPath)) {
throw new IOException("license路径不是合法目录");
}
// 遍历目录、筛选后缀为 .YJ的文件不区分大小写
try (var directoryStream = Files.list(licenseDirPath)) {
return directoryStream
.filter(Files::isRegularFile)
.filter(path -> {
String fileName = path.getFileName().toString().toLowerCase();
return fileName.endsWith(AUTH_FILE_SUFFIX.toLowerCase());
})
.collect(Collectors.toList());
}
}
/**
* 删除 license 目录下所有后缀为 .YJ 的文件
*/
private void deleteAllYjFilesInLicenseDir() throws IOException {
List<Path> yjFileList = findAllYjFilesInLicenseDir();
for (Path yjFile : yjFileList) {
Files.delete(yjFile);
}
}
}

View File

@ -193,7 +193,7 @@ public class FileInfoController {
// 标准化路径
targetFilePath = Paths.get(fileAbsolutePath).toRealPath();
// 校验文件合法性: 是否存在、是否为普通文件
// 是否存在、是否为普通文件
BasicFileAttributes fileAttr = Files.readAttributes(targetFilePath, BasicFileAttributes.class);
if (!fileAttr.isRegularFile()) {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
@ -204,21 +204,27 @@ public class FileInfoController {
// 设置预览响应头
String fileName = targetFilePath.getFileName().toString();
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name());
// 手动映射常见图片格式的 MIME 类型
String contentType = Files.probeContentType(targetFilePath);
String fileExtension = getFileExtension(fileName).toLowerCase();
// 覆盖默认探测结果
contentType = mapImageContentType(fileExtension, contentType);
// 对于文本类型文件、指定字符编码
if (contentType != null && contentType.startsWith("text/")) {
response.setContentType(contentType + "; charset=UTF-8");
} else {
response.setContentType(contentType != null ? contentType : "application/octet-stream");
// 确保 Content-Type 不为空
response.setContentType(contentType != null ? contentType : "image/png");
}
response.setContentLengthLong(fileAttr.size());
// 关键修改: 将attachment改为inline实现预览
// inline 已正确设置、无需修改
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
"inline; filename=\"" + encodedFileName + "\"; filename*=UTF-8''" + encodedFileName);
// 写入文件流
// 写入文件流(无需修改)
try (InputStream inputStream = Files.newInputStream(targetFilePath);
OutputStream outputStream = response.getOutputStream()) {
byte[] buffer = new byte[1024 * 8];
@ -240,4 +246,32 @@ public class FileInfoController {
fileInfoService.writeResponseMessage(response, "预览失败: " + e.getMessage());
}
}
/**
* 获取文件后缀
*/
private String getFileExtension(String fileName) {
int lastDotIndex = fileName.lastIndexOf('.');
return lastDotIndex == -1 ? "" : fileName.substring(lastDotIndex + 1);
}
/**
* 映射图片格式到标准 MIME 类型
*/
private String mapImageContentType(String fileExtension, String probeContentType) {
Map<String, String> imageMimeMap = new HashMap<>();
imageMimeMap.put("webp", "image/webp");
imageMimeMap.put("png", "image/png");
imageMimeMap.put("jpg", "image/jpeg");
imageMimeMap.put("jpeg", "image/jpeg");
imageMimeMap.put("gif", "image/gif");
imageMimeMap.put("bmp", "image/bmp");
imageMimeMap.put("svg", "image/svg+xml");
// 如果探针结果已存在且是图片类型
if (probeContentType != null && probeContentType.startsWith("image/")) {
return probeContentType;
}
return imageMimeMap.getOrDefault(fileExtension, probeContentType);
}
}

View File

@ -29,16 +29,18 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
@Data
@Slf4j
@Tag(name = "路径规划管理")
@RestController
@RequestMapping("/graphhopper")
public class GraphHopperController {
@ -56,20 +58,44 @@ public class GraphHopperController {
private final AtomicBoolean isLoading = new AtomicBoolean(false);
private final AtomicBoolean isLoaded = new AtomicBoolean(false);
@Operation(summary = "加载地图数据")
@Operation(summary = "加载地图数据(启用路网文件)")
@PostMapping("/loadMap")
public ApiResponse loadMap(@Parameter(description = "文件路径") @RequestParam String path) {
File osmFile = new File(path);
if (!osmFile.exists()) {
return ApiResponse.failure("地图文件不存在: " + path);
}
if (!osmFile.isFile() || !osmFile.getName().endsWith(".pbf")) {
return ApiResponse.failure("仅支持有效的.pbf格式OSM文件");
if (!osmFile.isFile()) {
return ApiResponse.failure("请提供有效的文件路径");
}
String fileName = osmFile.getName();
File tempPbfFile = null;
// 判断文件后缀
if (fileName.endsWith(".pbf")) {
// 直接使用原文件
} else if (fileName.endsWith(".pbfl")) {
try {
// 创建临时文件,后缀改为 .pbf
String tempFileName = UUID.randomUUID().toString() + ".pbf";
tempPbfFile = new File(osmFile.getParent(), tempFileName);
// 复制文件内容
Files.copy(osmFile.toPath(), tempPbfFile.toPath());
// 将路径指向临时文件
path = tempPbfFile.getAbsolutePath();
} catch (IOException e) {
return ApiResponse.failure("创建临时文件失败: " + e.getMessage());
}
} else {
return ApiResponse.failure("仅支持有效的.pbf或.pbfl格式OSM文件");
}
// 防止并发加载
if (isLoading.get()) {
return ApiResponse.failure("地图正在加载中、请稍后查询状态");
return ApiResponse.failure("对应区域的路网文件正在启用中、请稍后查询状态");
}
// 标记加载状态
@ -77,19 +103,21 @@ public class GraphHopperController {
isLoaded.set(false);
// 异步执行: 删除旧数据 → 创建新实例 → 加载新地图
String finalPath = path;
File finalTempPbfFile = tempPbfFile;
new Thread(() -> {
GraphHopper newHopper = null;
try {
// 关键步骤1: 彻底删除旧地图数据目录
deleteOldGraphDir();
// 关键步骤2: 创建全新的GraphHopper实例
newHopper = createNewGraphHopperInstance(path);
newHopper = createNewGraphHopperInstance(finalPath);
// 关键步骤3: 加载新地图
newHopper.importOrLoad();
// 关键步骤4: 加载成功 → 替换当前实例 + 更新状态
currentHopper = newHopper;
isLoaded.set(true);
log.info("地图加载成功");
log.info("对应区域的路网文件启用成功");
} catch (Exception e) {
// 加载失败 → 清理新实例资源
if (newHopper != null) {
@ -97,22 +125,31 @@ public class GraphHopperController {
}
isLoaded.set(false);
e.printStackTrace();
log.error("地图加载失败: " + e.getMessage());
log.error("对应区域的路网文件启用失败: " + e.getMessage());
} finally {
// 无论成功/失败、释放加载锁
isLoading.set(false);
// 删除临时文件
if (finalTempPbfFile != null && finalTempPbfFile.exists()) {
if (finalTempPbfFile.delete()) {
log.info("临时文件已删除: " + finalTempPbfFile.getAbsolutePath());
} else {
log.warn("临时文件删除失败: " + finalTempPbfFile.getAbsolutePath());
}
}
}
}).start();
return ApiResponse.success(null);
}
@Operation(summary = "清除地图服务")
@Operation(summary = "清除地图服务(停用路网文件)")
@PostMapping("/clearMap")
public ApiResponse clearMap() {
// 防止并发操作(与加载操作互斥)
if (isLoading.get()) {
return ApiResponse.failure("地图正在加载中、无法清除、请稍后再试");
return ApiResponse.failure("对应区域的路网文件正在启用中、无法清除、请稍后再试");
}
// 标记正在处理清除操作
@ -120,24 +157,24 @@ public class GraphHopperController {
try {
// 1. 关闭当前GraphHopper实例
if (currentHopper != null) {
log.info("开始关闭当前地图服务实例");
log.info("开始关闭当前路网服务实例");
currentHopper.close();
currentHopper = null;
log.info("地图服务实例已关闭");
log.info("路网服务实例已关闭");
}
// 2. 删除地图数据目录
log.info("开始删除地图数据目录");
log.info("开始删除路网数据目录");
deleteOldGraphDir();
log.info("地图数据目录已删除");
log.info("路网数据目录已删除");
// 3. 重置状态变量
isLoaded.set(false);
return ApiResponse.success("地图服务已成功清除");
return ApiResponse.success("对应区域的路网文件已成功停用并清除");
} catch (Exception e) {
log.error("清除地图服务失败", e);
return ApiResponse.failure("清除地图服务失败: " + e.getMessage());
log.error("清除路网服务失败", e);
return ApiResponse.failure("清除路网服务失败: " + e.getMessage());
} finally {
// 释放操作锁
isLoading.set(false);
@ -146,15 +183,17 @@ public class GraphHopperController {
@Operation(summary = "路径规划")
@PostMapping("/route")
public ApiResponse calculateRoute(@RequestBody RouteRequest request) {
// 区分未加载地图和加载中两种状态
// 区分三种核心状态提示
if (isLoading.get()) {
return ApiResponse.failure("地图正在加载中、请稍后再试");
// 场景1: 导入了路网文件但未启用(加载中)
return ApiResponse.failure("请启用对应区域的路网文件");
}
if (!isLoaded.get() || currentHopper == null) {
return ApiResponse.failure("地图未加载、请先加载地图");
// 场景2: 未导入路网文件
return ApiResponse.failure("请导入并启用对应区域的路网文件");
}
try {
// 构建路径点列表
List<GHPoint> ghPoints = new ArrayList<>();
@ -182,9 +221,10 @@ public class GraphHopperController {
boolean hasPointNotFoundError = response.getErrors().stream()
.anyMatch(e -> e instanceof com.graphhopper.util.exceptions.PointNotFoundException);
if (hasOutOfBoundsError) {
return ApiResponse.failure("路径超出地图范围");
// 场景3: 已启用但路径超出路网范围
return ApiResponse.failure("绘制路径超出有效路网范围");
} else if (hasPointNotFoundError) {
return ApiResponse.failure("未超地图范围但找不到路径点");
return ApiResponse.failure("未超路网范围但找不到有效路径点");
} else {
return ApiResponse.failure("路径计算异常: " + response.getErrors().get(0).getMessage());
}
@ -199,27 +239,26 @@ public class GraphHopperController {
RouteResponse routeResponse = new RouteResponse(bestPath.getDistance() / 1000, (double) (bestPath.getTime() / 60000), pathPoints);
return ApiResponse.success(routeResponse);
} catch (com.graphhopper.util.exceptions.PointOutOfBoundsException e) {
// 捕获单点超出范围的异常
return ApiResponse.failure("路径超出地图范围");
// 捕获单点超出范围的异常场景3
return ApiResponse.failure("绘制路径超出有效路网范围");
} catch (Exception e) {
return ApiResponse.failure("路径计算异常: " + e.getMessage());
}
}
@Operation(summary = "获取地图加载状态")
@Operation(summary = "获取路网文件加载状态")
@GetMapping("/status")
public ApiResponse getMapStatus() {
if (isLoading.get()) {
return ApiResponse.success("地图正在加载");
return ApiResponse.success("对应区域的路网文件正在启用");
} else if (isLoaded.get() && currentHopper != null) {
return ApiResponse.success("地图已加载完成");
return ApiResponse.success("对应区域的路网文件已启用");
} else {
return ApiResponse.success("地图未加载");
return ApiResponse.success("未导入对应区域的路网文件");
}
}
@Operation(summary = "获取交通方式")
@PostMapping("/profiles")
public ApiResponse profiles() {
return ApiResponse.success(graphHopperProperties.getProfiles());
@ -252,7 +291,7 @@ public class GraphHopperController {
private void deleteOldGraphDir() {
File graphDir = new File(graphHopperProperties.getGraphLocation());
if (!graphDir.exists()) {
log.info("地图目录不存在、无需删除: " + graphHopperProperties.getGraphLocation());
log.info("路网目录不存在、无需删除: " + graphHopperProperties.getGraphLocation());
return;
}
@ -264,14 +303,14 @@ public class GraphHopperController {
deleteOldGraphDir(file);
} else {
boolean deleted = file.delete();
log.info("删除旧地图文件: " + file.getAbsolutePath() + "" + (deleted ? "成功" : "失败"));
log.info("删除旧路网文件: " + file.getAbsolutePath() + "" + (deleted ? "成功" : "失败"));
}
}
}
// 删除空目录
boolean dirDeleted = graphDir.delete();
System.out.println("删除旧地图目录: " + graphDir.getAbsolutePath() + "" + (dirDeleted ? "成功" : "失败"));
log.info("删除旧路网目录: " + graphDir.getAbsolutePath() + "" + (dirDeleted ? "成功" : "失败"));
}
// 重载:递归删除子目录

View File

@ -193,6 +193,9 @@ public class IconLibraryController {
@GetMapping("/iconTypeTree")
public ApiResponse iconTypeTree(@Parameter(description = "图标名称") @RequestParam(value = "iconName", required = false) String iconName) throws SQLException, IllegalAccessException, InstantiationException {
List<IconTypeVo> treeList = iconTypeList(iconName);
if(treeList == null){
return ApiResponse.successWithMessage("请先创建或导入图标库");
}
return ApiResponse.success(treeList);
}
@ -392,7 +395,7 @@ public class IconLibraryController {
queryWrapper.eq(IconLibrary::getIsEnable, 1);
IconLibrary library = iconLibraryService.getOne(queryWrapper);
if(library == null){
throw new RuntimeException("请先创建或导入图标库");
return null;
}
return library.getPath();
}

View File

@ -206,6 +206,9 @@ public class MilitaryLibraryController {
@GetMapping("/militaryTypeTree")
public ApiResponse militaryTypeTree(@Parameter(description = "军标名称") @RequestParam(value = "militaryName", required = false) String militaryName) throws SQLException, IllegalAccessException, InstantiationException {
List<MilitaryTypeVo> treeList = militaryTypeList(militaryName);
if (treeList == null) {
return ApiResponse.successWithMessage("请先创建或导入军标库");
}
return ApiResponse.success(treeList);
}
@ -430,7 +433,7 @@ public class MilitaryLibraryController {
queryWrapper.eq(MilitaryLibrary::getIsEnable, 1); // 1=启用、0=未启用
MilitaryLibrary library = militaryLibraryService.getOne(queryWrapper);
if (library == null) {
throw new RuntimeException("请先创建或导入军标库");
return null;
}
return library.getPath();
}

View File

@ -175,7 +175,11 @@ public class ModelLibraryController {
@Operation(summary = "模型类型列表")
@GetMapping("/modelTypeList")
public ApiResponse modelTypeTree(@Parameter(description = "模型名称") @RequestParam(value = "modelName", required = false) String modelName) throws SQLException, IllegalAccessException, InstantiationException {
return ApiResponse.success(modelTypeList(modelName));
List<ModelTypeVo> modelTypeVos = modelTypeList(modelName);
if (modelTypeVos == null) {
return ApiResponse.successWithMessage("请先创建或导入模型库");
}
return ApiResponse.success(modelTypeVos);
}
@Operation(summary = "添加模型文件")
@ -547,7 +551,7 @@ public class ModelLibraryController {
queryWrapper.eq(ModelLibrary::getIsEnable, 1);
ModelLibrary modelLibrary = modelLibraryService.getOne(queryWrapper);
if (modelLibrary == null) {
throw new RuntimeException("请先创建或导入模型库");
return null;
}
return modelLibrary.getPath();
}

View File

@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
@Tag(name = "地图文件管理")
@RestController
@ -27,12 +28,22 @@ public class PbfInfoController {
@Operation(summary = "添加地图文件")
@PostMapping("/add")
public ApiResponse add(@Parameter(description = "地图文件路径") @RequestParam(required = true) String path) {
PbfInfo pbfInfo = new PbfInfo();
pbfInfo.setPath(path);
pbfInfo.setName(FileUtil.mainName(path));
pbfInfo.setIsEnable(0);
pbfInfoService.save(pbfInfo);
public ApiResponse add(@Parameter(description = "地图文件路径列表") @RequestParam(required = true) List<String> paths) {
// 批量转换为PbfInfo对象
List<PbfInfo> pbfInfoList = paths.stream()
.map(path -> {
PbfInfo pbfInfo = new PbfInfo();
pbfInfo.setPath(path);
pbfInfo.setName(FileUtil.mainName(path));
pbfInfo.setIsEnable(0);
return pbfInfo;
})
.collect(Collectors.toList());
// 批量保存
pbfInfoService.saveBatch(pbfInfoList);
// 返回成功结果
return ApiResponse.success(null);
}
@ -83,6 +94,4 @@ public class PbfInfoController {
graphHopperController.clearMap();
return ApiResponse.success(null);
}
}

View File

@ -19,6 +19,7 @@ import java.io.File;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@Tag(name = "POI文件管理")
@ -27,14 +28,22 @@ public class PoiInfoController {
@Resource
private PoiInfoService poiInfoService;
@Operation(summary = "添加POI文件")
@Operation(summary = "批量添加POI文件")
@PostMapping("/add")
public ApiResponse add(@Parameter(description = "POI文件路径") @RequestParam(required = true) String path) {
PoiInfo poiInfo = new PoiInfo();
poiInfo.setPath(path);
poiInfo.setName(FileUtil.mainName(path));
poiInfo.setIsEnable(0);
poiInfoService.save(poiInfo);
public ApiResponse add(@Parameter(description = "POI文件路径列表") @RequestParam(required = true) List<String> paths) {
// 批量转换为PoiInfo对象
List<PoiInfo> poiInfoList = paths.stream()
.map(path -> {
PoiInfo poiInfo = new PoiInfo();
poiInfo.setPath(path);
poiInfo.setName(FileUtil.mainName(path));
poiInfo.setIsEnable(0);
return poiInfo;
})
.collect(Collectors.toList());
// 批量保存
poiInfoService.saveBatch(poiInfoList);
return ApiResponse.success(null);
}
@ -57,7 +66,7 @@ public class PoiInfoController {
@Operation(summary = "启用POI文件")
@PostMapping("/enable")
public ApiResponse enable(@Parameter(description = "地图文件ID") @RequestParam(required = true) String id) {
public ApiResponse enable(@Parameter(description = "POI文件ID") @RequestParam(required = true) String id) {
PoiInfo poiInfo = poiInfoService.getById(id);
poiInfo.setIsEnable(1);
poiInfoService.updateById(poiInfo);
@ -74,7 +83,7 @@ public class PoiInfoController {
@Operation(summary = "禁用POI文件")
@PostMapping("/disable")
public ApiResponse disable(@Parameter(description = "地图文件ID") @RequestParam(required = true) String id) {
public ApiResponse disable(@Parameter(description = "POI文件ID") @RequestParam(required = true) String id) {
PoiInfo poiInfo = new PoiInfo();
poiInfo.setId(id);
poiInfo.setIsEnable(0);
@ -84,10 +93,7 @@ public class PoiInfoController {
@Operation(summary = "POI搜索")
@GetMapping("/search")
public ApiResponse search(
@Parameter(description = "分页页码") Integer pageNum,
@Parameter(description = "分页大小") Integer pageSize,
@Parameter(description = "名称搜索") String name) {
public ApiResponse search(@Parameter(description = "分页页码") Integer pageNum, @Parameter(description = "分页大小") Integer pageSize, @Parameter(description = "名称搜索") String name) {
int offset = (pageNum - 1) * pageSize;
// 查询启用的POI文件
PoiInfo poiInfo = poiInfoService.getOne(new QueryWrapper<PoiInfo>().lambda().eq(PoiInfo::getIsEnable, 1));

View File

@ -26,6 +26,8 @@ import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.File;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
@ -186,10 +188,20 @@ public class SourceController {
queryWrapper.eq(Source::getId, updateLevelDto.getId());
Source source = sourceService.getOne(queryWrapper);
String params = source.getParams();
// 修改这个 JSON 的值
params = JsonUtil.modifyJsonValue(params, "layerIndex", updateLevelDto.getLayerIndex());
source.setParams(params);
sourceService.updateById(source);
if (params != null) {
// 修改这个 JSON 的值
params = JsonUtil.modifyJsonValue(params, "layerIndex", updateLevelDto.getLayerIndex());
source.setParams(params);
sourceService.updateById(source);
}
// 否则是新增
if (params == null) {
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put("layerIndex", updateLevelDto.getLayerIndex());
paramsMap.put("id", updateLevelDto.getId());
source.setParams(MapUtil.mapToString(paramsMap));
sourceService.updateById(source);
}
}
return ApiResponse.success(null);
}
@ -250,76 +262,92 @@ public class SourceController {
@PostMapping("/uploadLocationImage")
@Operation(summary = "新增定位图片")
public ApiResponse uploadLocationImage(@RequestParam("ids") @Parameter(description = "上传定位图片ID列表") List<String> ids,
@RequestParam(value = "parentId", required = false) @Parameter(description = "父节点ID") String parentId,
@RequestParam(value = "treeIndex", required = false) @Parameter(description = "树状索引") Integer treeIndex,
@RequestParam(value = "params", required = false) @Parameter(description = "参数") String params,
@RequestParam(value = "sourceType", required = false) @Parameter(description = "资源类型") String sourceType,
@RequestParam("files") @Parameter(description = "带有定位的图片文件", required = true) MultipartFile[] files) {
// 验证并转换参数
public ApiResponse uploadLocationImage(
@RequestParam("ids") @Parameter(description = "上传定位图片ID列表") List<String> ids,
@RequestParam(value = "parentId", required = false) @Parameter(description = "父节点ID") String parentId,
@RequestParam(value = "treeIndex", required = false) @Parameter(description = "树状索引") Integer treeIndex,
@RequestParam(value = "params", required = false) @Parameter(description = "参数") String params,
@RequestParam(value = "sourceType", required = false) @Parameter(description = "资源类型") String sourceType,
@RequestParam("filePaths") @Parameter(description = "图片文件绝对路径列表", required = true) List<String> filePaths) {
sourceParamsValidator.validateAndConvert(
sourceType,
JsonUtil.jsonToMap(params)
);
// 检查是否有重复的filePath
for (String filePath : filePaths) {
// 检查数据库中是否已存在该文件路径
boolean exists = sourceService.lambdaQuery()
.eq(Source::getSourcePath, filePath)
.eq(Source::getIsShow, SHOW)
.exists();
if (exists) {
return ApiResponse.failure("数据库存在参数中部分文件路径、不允许重复上传:" + filePath);
}
}
List<Source> sources = new ArrayList<>();
for (int i = 0; i < files.length; i++) {
Map<String, Object> dataMap = fileInfoService.handleLocationImageUpload(files[i]);
// 构建并保存资源对象
Source source = new Source();
source.setId(ids.get(i));
source.setSourceName(files[i].getOriginalFilename());
source.setParentId(parentId);
source.setSourceType(sourceType);
source.setTreeIndex(treeIndex);
for (int i = 0; i < filePaths.size(); i++) {
String filePath = filePaths.get(i);
File file = new File(filePath);
// 转换为对象
Point point = JsonUtil.mapToObject(JsonUtil.jsonToMap(params), Point.class);
point.setId(ids.get(i));
Point.Position position = new Point.Position();
point.setName(files[i].getOriginalFilename());
point.getLabel().setText(files[i].getOriginalFilename());
Object lonObj = dataMap.get("lon");
if (lonObj != null && lonObj instanceof Double) {
position.setLng((Double) lonObj);
try {
Map<String, Object> dataMap = fileInfoService.handleLocationImageUpload(file);
Source source = new Source();
source.setSourcePath(filePath);
source.setId(ids.get(i));
source.setSourceName(Paths.get(filePath).getFileName().toString());
source.setParentId(parentId);
source.setSourceType(sourceType);
source.setTreeIndex(treeIndex);
Point point = JsonUtil.mapToObject(JsonUtil.jsonToMap(params), Point.class);
point.setId(ids.get(i));
Point.Position position = new Point.Position();
point.setName(Paths.get(filePath).getFileName().toString());
point.getLabel().setText(Paths.get(filePath).getFileName().toString());
// 解析经纬度、海拔
Object lonObj = dataMap.get("lon");
if (lonObj != null && lonObj instanceof Double) {
position.setLng((Double) lonObj);
}
Object latObj = dataMap.get("lat");
if (latObj != null && latObj instanceof Double) {
position.setLat((Double) latObj);
}
Object altObj = dataMap.get("alt");
if (altObj != null && altObj instanceof Double) {
position.setAlt((Double) altObj);
}
point.setPosition(position);
// 根据资源类型设置链接/VR内容
if ("linkImage".equals(sourceType)) {
List<Point.Attribute.Link.LinkContent> list = new ArrayList<>();
Point.Attribute.Link.LinkContent linkContent = new Point.Attribute.Link.LinkContent();
linkContent.setName("带定位照片");
linkContent.setUrl(dataMap.get("url").toString());
list.add(linkContent);
point.getAttribute().getLink().setContent(list);
} else {
List<Point.Attribute.Vr.VrContent> list = new ArrayList<>();
Point.Attribute.Vr.VrContent vrContent = new Point.Attribute.Vr.VrContent();
vrContent.setName("带全景照片");
vrContent.setUrl(dataMap.get("url").toString());
list.add(vrContent);
point.getAttribute().getVr().setContent(list);
}
// 保存资源参数
source.setParams(JsonUtil.toJson(point));
source.setIsShow(SHOW);
sourceService.save(source);
// 添加角色关联
roleSourceService.addRoleSource(userService.getById(StpUtil.getLoginIdAsString()).getRoleId(), source.getId());
sources.add(source);
} catch (Exception e) {
return ApiResponse.failure("处理文件失败:" + filePath + ",原因:" + e.getMessage());
}
Object latObj = dataMap.get("lat");
if (latObj != null && latObj instanceof Double) {
position.setLat((Double) latObj);
}
Object altObj = dataMap.get("alt");
if (altObj != null && altObj instanceof Double) {
position.setAlt((Double) altObj);
}
point.setPosition(position);
if ("linkImage".equals(sourceType)) {
// 设置地址
List<Point.Attribute.Link.LinkContent> list = new ArrayList<>();
Point.Attribute.Link.LinkContent linkContent = new Point.Attribute.Link.LinkContent();
linkContent.setName("带定位照片");
linkContent.setUrl(dataMap.get("url").toString());
list.add(linkContent);
point.getAttribute().getLink().setContent(list);
} else {
List<Point.Attribute.Vr.VrContent> list = new ArrayList<>();
Point.Attribute.Vr.VrContent vrContent = new Point.Attribute.Vr.VrContent();
vrContent.setName("带全景照片");
vrContent.setUrl(dataMap.get("url").toString());
list.add(vrContent);
point.getAttribute().getVr().setContent(list);
}
// 将 vrImage 转化为 JSON
source.setParams(JsonUtil.toJson(point));
source.setIsShow(SHOW);
sourceService.save(source);
// 添加资源到该用户的角色下
roleSourceService.addRoleSource(userService.getById(StpUtil.getLoginIdAsString()).getRoleId(), source.getId());
sources.add(source);
}
return ApiResponse.success(sources);
}

View File

@ -71,10 +71,11 @@ public class TsPlanController {
}
private TsSource createSourceIfNotExists(String sourceName, String sourceType, String parentId, int treeIndex, int isShow, String tsPlanId) {
// 检查资源是否已存在通过名称和父ID组合判断唯一性
// 检查资源是否已存在通过名称和父ID和计划ID组合判断唯一性)
TsSource existingSource = tsSourceService.getOne(new LambdaQueryWrapper<TsSource>()
.eq(TsSource::getSourceName, sourceName)
.eq(parentId != null, TsSource::getParentId, parentId)
.eq(TsSource::getPlanId, tsPlanId)
.isNull(parentId == null, TsSource::getParentId));
if (existingSource != null) {
return existingSource;
@ -128,21 +129,24 @@ public class TsPlanController {
@Parameter(description = "开始时间") @RequestParam(required = false) String startTime,
@Parameter(description = "结束时间") @RequestParam(required = false) String endTime) {
LambdaQueryWrapper<TsPlan> queryWrapper = new LambdaQueryWrapper<>();
// 创建人昵称模糊查询
if (StringUtils.isNotBlank(nickname)) {
LambdaQueryWrapper<User> userQueryWrapper = new LambdaQueryWrapper<User>()
.select(User::getId)
.like(User::getNickname, nickname);
List<String> userIds = userService.list(userQueryWrapper).stream()
.map(User::getId)
.collect(Collectors.toList());
if (userIds.isEmpty()) {
// 返回空数据
return ApiResponse.success(new Page<TsPlan>());
}
}
// 方案名称模糊查询
if (StringUtils.isNotBlank(name)) {
queryWrapper.like(TsPlan::getName, name);
}
// 创建人昵称模糊查询
if (StringUtils.isNotBlank(nickname)) {
queryWrapper.in(TsPlan::getCreatedBy,
new LambdaQueryWrapper<User>()
.select(User::getId)
.like(User::getNickname, nickname)
);
}
// 时间范围查询
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
try {
@ -154,36 +158,17 @@ public class TsPlanController {
LocalDateTime end = LocalDateTime.parse(endTime, formatter);
queryWrapper.le(TsPlan::getCreatedAt, end);
}
} catch (DateTimeParseException e) {
} catch (
DateTimeParseException e) {
return ApiResponse.failure("时间格式错误、需为 yyyy-MM-dd HH:mm:ss");
}
// 执行分页查询
Page<TsPlan> page = tsPlanService.page(new Page<>(pageNum, pageSize), queryWrapper);
// 关联查询用户信息并封装结果
if (!page.getRecords().isEmpty()) {
List<String> creatorIds = page.getRecords().stream()
.map(TsPlan::getCreatedBy)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (!creatorIds.isEmpty()) {
List<User> creators = userService.listByIds(creatorIds);
Map<String, String> creatorNicknameMap = creators.stream()
.collect(Collectors.toMap(
User::getId,
user -> user.getNickname() != null ? user.getNickname() : "未知昵称",
(k1, k2) -> k1
));
page.getRecords().forEach(plan -> {
plan.setCreatedBy(creatorNicknameMap.getOrDefault(plan.getCreatedBy(), "未知用户"));
});
}
List<TsPlan> records = page.getRecords();
for (TsPlan record : records) {
record.setCreatedBy(userService.getById(record.getCreatedBy()).getNickname());
}
return ApiResponse.success(page);
}

View File

@ -2,14 +2,17 @@ package com.yj.earth.business.controller;
import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.yj.earth.business.domain.Source;
import com.yj.earth.business.domain.TsSource;
import com.yj.earth.business.service.TsSourceService;
import com.yj.earth.common.util.ApiResponse;
import com.yj.earth.common.util.JsonUtil;
import com.yj.earth.common.util.MapUtil;
import com.yj.earth.dto.source.DragSourceDto;
import com.yj.earth.dto.tsPlan.BatchUpdateShowStatusDto;
import com.yj.earth.dto.tsSource.AddTsModelSourceDto;
import com.yj.earth.dto.tsSource.AddTsSourceDto;
import com.yj.earth.dto.tsSource.DragTsSourceDto;
import com.yj.earth.dto.tsSource.UpdateTsSourceDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -68,6 +71,20 @@ public class TsSourceController {
return ApiResponse.success(null);
}
@Operation(summary = "拖动资源")
@PostMapping("/dragSource")
public ApiResponse drag(@RequestBody List<DragTsSourceDto> dragTsSourceDtoList) {
for (DragTsSourceDto dragTsSourceDto : dragTsSourceDtoList) {
LambdaQueryWrapper<TsSource> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TsSource::getId, dragTsSourceDto.getId());
TsSource tsSource = tsSourceService.getOne(queryWrapper);
tsSource.setTreeIndex(dragTsSourceDto.getTreeIndex());
tsSource.setParentId(dragTsSourceDto.getParentId());
tsSourceService.updateById(tsSource);
}
return ApiResponse.success(null);
}
@Operation(summary = "查询某个方案下的态势资源")
@PostMapping("/query")
public ApiResponse query(@Parameter(description = "态势方案ID") @RequestParam(required = true) String id) {

View File

@ -32,6 +32,9 @@ public class TsPlan implements Serializable {
@Schema(description = "创建人")
private String createdBy;
@Schema(description = "滑轮")
private Integer wheel;
@Schema(description = "仿真开始时间")
private LocalDateTime simulationStartTime;

View File

@ -5,12 +5,14 @@ import com.baomidou.mybatisplus.extension.service.IService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.Map;
public interface FileInfoService extends IService<FileInfo> {
// 根据文件ID获取文件绝对路径
String getFileAbsolutePath(String id);
Map<String, Object> handleLocationImageUpload(MultipartFile file);
Map<String, Object> handleLocationImageUpload(File file);
String uploadWithPreview(MultipartFile file);
void writeResponseMessage(HttpServletResponse response, String message);
String getFullUploadPath();

View File

@ -104,6 +104,62 @@ public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> i
return result;
}
public Map<String, Object> handleLocationImageUpload(File file) {
// 构建并返回结果
Map<String, Object> result = new HashMap<>();
try {
if (file == null || !file.exists()) {
throw new IllegalArgumentException("文件不存在");
}
if (file.length() == 0) {
throw new IllegalArgumentException("上传文件不能为空(空文件)");
}
// 原始文件名
String originalFilename = file.getName();
// 文件后缀
String fileSuffix = FileUtil.extName(originalFilename);
// 文件MIME类型
String contentType = Files.probeContentType(file.toPath());
if (contentType == null || !contentType.startsWith("image/")) {
throw new IllegalArgumentException("请上传图片文件,当前文件类型:" + contentType);
}
// 获取完整的上传目录路径
Path fullUploadPath = Paths.get(getFullUploadPath());
// 生成唯一文件名
String uniqueFileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileSuffix;
// 创建文件存储目录(不存在则创建)
Files.createDirectories(fullUploadPath);
// 构建目标文件路径
Path destFilePath = fullUploadPath.resolve(uniqueFileName);
// 覆盖已存在的文件
Files.copy(file.toPath(), destFilePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
String fileMd5 = calculateFileMd5(destFilePath.toFile());
Map<String, Double> metadata;
try (InputStream is = Files.newInputStream(destFilePath)) {
metadata = extractImageMetadata(is);
}
FileInfo fileInfo = new FileInfo();
fileInfo.setFileName(originalFilename);
fileInfo.setFileSuffix(fileSuffix);
fileInfo.setContentType(contentType);
fileInfo.setFileSize(file.length());
fileInfo.setFilePath(uniqueFileName);
fileInfo.setFileMd5(fileMd5);
this.save(fileInfo);
result.put("url", "/fileInfo/preview/" + fileInfo.getId());
result.put("lon", metadata.get("lon"));
result.put("lat", metadata.get("lat"));
result.put("alt", metadata.get("alt"));
} catch (IOException e) {
throw new RuntimeException("文件处理失败: " + e.getMessage(), e);
}
return result;
}
public String uploadWithPreview(MultipartFile file) {
FileInfo fileInfo = new FileInfo();
try {

View File

@ -95,12 +95,19 @@ public class SourceServiceImpl extends ServiceImpl<SourceMapper, Source> impleme
public Source addModelSource(AddModelSourceDto addModelSourceDto) {
// 获取资源绝对路径
String sourcePath = addModelSourceDto.getSourcePath();
// 判断数据是否在数据库存在
LambdaQueryWrapper<Source> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Source::getSourcePath, sourcePath);
if (sourceService.count(queryWrapper) > 0) {
throw new RuntimeException("资源已存在");
}
// 获取资源名称
String sourceName = FileUtil.mainName(sourcePath);
// 校验是否通过
String message = sourceService.checkIsPass(addModelSourceDto.getParentId(), sourceName);
if (message != null) {
return null;
throw new RuntimeException("父节点不存在");
}
// 调用SDK加载资源
String sourceId = sourceService.addAndGetSourceId(sourcePath);

View File

@ -31,7 +31,9 @@ public class SaTokenConfig implements WebMvcConfigurer {
excludePathPatterns.add("/iconLibrary/data/icon/**");
excludePathPatterns.add("/militaryLibrary/data/military/**");
excludePathPatterns.add("/modelLibrary/data/**");
excludePathPatterns.add("/**");
excludePathPatterns.add("/auth/show");
excludePathPatterns.add("/auth/info");
excludePathPatterns.add("/auth/import");
// 注册 Sa-Token 拦截器
registry.addInterceptor(new SaInterceptor(handle -> {

View File

@ -39,7 +39,7 @@ public class ServerInitService {
for (Source source : list) {
// 同步资源
sourceService.getDetail(source.getSourcePath(), sourceService.addAndGetSourceId(source.getSourcePath()));
log.info("初始化资源<--{}-->完成", source.getSourceName());
log.info("初始化资源 <--{}--> 完成", source.getSourceName());
}
}

View File

@ -13,6 +13,8 @@ public class TsPlan {
private String name;
@Schema(description = "方案描述")
private String desc;
@Schema(description = "滑轮")
private Integer wheel;
@Schema(description = "创建人")
private String createdBy;
@Schema(description = "仿真开始时间")

View File

@ -11,6 +11,8 @@ public class AddTsPlanDto {
private String name;
@Schema(description = "方案描述")
private String desc;
@Schema(description = "滑轮")
private Integer wheel;
@Schema(description = "仿真开始时间")
private LocalDateTime simulationStartTime;
}

View File

@ -13,6 +13,8 @@ public class UpdateTsPlanDto {
private String name;
@Schema(description = "方案描述")
private String desc;
@Schema(description = "滑轮")
private Integer wheel;
@Schema(description = "仿真开始时间")
private LocalDateTime simulationStartTime;
}

View File

@ -0,0 +1,16 @@
package com.yj.earth.dto.tsSource;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
public class DragTsSourceDto {
@Schema(description = "主键")
private String id;
@Schema(description = "父级ID")
private String parentId;
@Schema(description = "树形索引")
private Integer treeIndex;
}

View File

@ -2,6 +2,7 @@ package com.yj.earth.params;
import com.yj.earth.annotation.SourceType;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@ -22,6 +23,7 @@ public class MilitaryParam {
private Attribute attribute;
private String attributeType;
private Text text;
private CustomView customView;
@Data
public static class Position {
@ -63,4 +65,26 @@ public class MilitaryParam {
private int far;
private Position position;
}
@Data
@NoArgsConstructor
public static class CustomView {
private LinkImage.CustomView.Orientation orientation;
private LinkImage.CustomView.RelativePosition relativePosition;
@Data
@NoArgsConstructor
public static class Orientation {
private double heading;
private double pitch;
private double roll;
}
@Data
@NoArgsConstructor
public static class RelativePosition {
private double lng;
private double lat;
private double alt;
}
}
}

View File

@ -17,6 +17,6 @@ public class Vector {
private List<Object> headTables;
private boolean show;
private String color;
private double opacity;
private Double opacity;
private Object customView;
}

View File

@ -1,7 +1,5 @@
package com.yj.earth.vo;
import com.yj.earth.business.domain.Icon;
import com.yj.earth.business.domain.Military;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;