模型库
This commit is contained in:
		| @ -4,17 +4,22 @@ import cn.hutool.core.io.FileUtil; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import com.drew.imaging.ImageMetadataReader; | ||||
| import com.drew.imaging.ImageProcessingException; | ||||
| import com.drew.lang.Rational; | ||||
| import com.drew.metadata.Directory; | ||||
| import com.drew.metadata.Metadata; | ||||
|  | ||||
| import java.io.*; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.NoSuchFileException; | ||||
| import java.nio.file.Path; | ||||
| import java.nio.file.Paths; | ||||
| import java.nio.file.attribute.BasicFileAttributes; | ||||
| import java.util.*; | ||||
|  | ||||
| import cn.hutool.crypto.digest.DigestUtil; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||
| import com.drew.metadata.MetadataException; | ||||
| import com.drew.metadata.exif.GpsDirectory; | ||||
| import com.yj.earth.business.domain.FileInfo; | ||||
| import com.yj.earth.business.service.FileInfoService; | ||||
| import com.yj.earth.common.util.ApiResponse; | ||||
| @ -46,41 +51,22 @@ public class FileInfoController { | ||||
|     @Resource | ||||
|     private FileInfoService fileInfoService; | ||||
|  | ||||
|     @Value("${file.upload.path}") | ||||
|     private String uploadPath; | ||||
|  | ||||
|     // 获取项目根目录 | ||||
|     private String getProjectRootPath() { | ||||
|         return System.getProperty("user.dir"); | ||||
|     } | ||||
|  | ||||
|     // 获取完整的上传目录路径 | ||||
|     private String getFullUploadPath() { | ||||
|         // 拼接项目根目录和配置的上传路径 | ||||
|         return getProjectRootPath() + File.separator + uploadPath; | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "文件上传") | ||||
|     @PostMapping("/upload") | ||||
|     public ApiResponse uploadFiles(@Parameter(description = "上传的文件数组", required = true) @RequestParam("files") MultipartFile[] files) throws IOException { | ||||
|  | ||||
|         // 校验文件数组是否为空 | ||||
|         if (files == null || files.length == 0) { | ||||
|             return ApiResponse.failure("上传文件不能为空"); | ||||
|         } | ||||
|  | ||||
|         // 获取完整的上传目录路径 | ||||
|         String fullUploadPath = getFullUploadPath(); | ||||
|  | ||||
|         String fullUploadPath = fileInfoService.getFullUploadPath(); | ||||
|         List<FileInfoVo> fileInfoVoList = new ArrayList<>(); | ||||
|  | ||||
|         // 遍历处理每个文件 | ||||
|         for (MultipartFile file : files) { | ||||
|             // 跳过空文件 | ||||
|             if (file.isEmpty()) { | ||||
|                 continue; | ||||
|             } | ||||
|  | ||||
|             // 获取原始文件名和后缀 | ||||
|             String originalFilename = file.getOriginalFilename(); | ||||
|             String fileSuffix = FileUtil.extName(originalFilename); | ||||
| @ -106,7 +92,7 @@ public class FileInfoController { | ||||
|             // 查询有没有文件名一样并且 MD5 也一样的数据 | ||||
|             LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>(); | ||||
|             queryWrapper.eq(FileInfo::getFileName, originalFilename).eq(FileInfo::getFileMd5, fileMd5); | ||||
|             if (fileInfoService.count(queryWrapper) > 0) {  // 修复了此处的bug、添加了queryWrapper参数 | ||||
|             if (fileInfoService.count(queryWrapper) > 0) { | ||||
|                 return ApiResponse.failure("已存在文件名相同且内容完全一致的文件"); | ||||
|             } | ||||
|  | ||||
| @ -116,7 +102,7 @@ public class FileInfoController { | ||||
|             fileInfo.setFileSuffix(fileSuffix); | ||||
|             fileInfo.setContentType(contentType); | ||||
|             fileInfo.setFileSize(file.getSize()); | ||||
|             fileInfo.setFilePath(uniqueFileName);  // 只保存相对文件名、不保存完整路径 | ||||
|             fileInfo.setFilePath(uniqueFileName); | ||||
|             fileInfo.setFileMd5(fileMd5); | ||||
|  | ||||
|             // 保存文件信息并获取ID | ||||
| @ -152,7 +138,7 @@ public class FileInfoController { | ||||
|         } | ||||
|  | ||||
|         // 构建完整文件路径 | ||||
|         String fullPath = getFullUploadPath() + File.separator + fileInfo.getFilePath(); | ||||
|         String fullPath = fileInfoService.getFullUploadPath() + File.separator + fileInfo.getFilePath(); | ||||
|         File file = new File(fullPath); | ||||
|  | ||||
|         // 校验文件是否存在 | ||||
| @ -185,7 +171,7 @@ public class FileInfoController { | ||||
|         } | ||||
|  | ||||
|         // 构建完整文件路径 | ||||
|         String fullPath = getFullUploadPath() + File.separator + fileInfo.getFilePath(); | ||||
|         String fullPath = fileInfoService.getFullUploadPath() + File.separator + fileInfo.getFilePath(); | ||||
|         File file = new File(fullPath); | ||||
|  | ||||
|         // 校验文件是否存在 | ||||
| @ -204,100 +190,59 @@ public class FileInfoController { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public String handleLocationImageUpload(MultipartFile file) { | ||||
|     @Operation(summary = "本地文件预览") | ||||
|     @GetMapping("/previewLocal") | ||||
|     public void previewLocalFile(@Parameter(description = "本地文件绝对路径") @RequestParam String fileAbsolutePath, HttpServletResponse response) { | ||||
|         Path targetFilePath = null; | ||||
|         try { | ||||
|             // 校验文件是否为空 | ||||
|             if (file.isEmpty()) { | ||||
|                 throw new IllegalArgumentException("上传文件不能为空"); | ||||
|             } | ||||
|             // 获取文件基本信息 | ||||
|             String originalFilename = file.getOriginalFilename(); | ||||
|             String fileSuffix = FileUtil.extName(originalFilename); | ||||
|             String contentType = file.getContentType(); | ||||
|             // 验证是否为图片文件 | ||||
|             if (contentType == null || !contentType.startsWith("image/")) { | ||||
|                 throw new IllegalArgumentException("请上传图片文件"); | ||||
|             } | ||||
|             // 获取完整的上传目录路径 | ||||
|             Path fullUploadPath = Paths.get(getFullUploadPath()); | ||||
|             // 生成唯一文件名 | ||||
|             String uniqueFileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileSuffix; | ||||
|             // 创建文件存储目录 | ||||
|             Files.createDirectories(fullUploadPath); | ||||
|             // 构建完整文件路径并保存文件 | ||||
|             Path destFilePath = fullUploadPath.resolve(uniqueFileName); | ||||
|             // 先将文件保存到目标位置 | ||||
|             file.transferTo(destFilePath); | ||||
|             // 计算文件MD5(使用已保存的文件) | ||||
|             String fileMd5 = calculateFileMd5(destFilePath.toFile()); | ||||
|             // 检查文件是否已存在 | ||||
|             LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>(); | ||||
|             queryWrapper.eq(FileInfo::getFileName, originalFilename) | ||||
|                     .eq(FileInfo::getFileMd5, fileMd5); | ||||
|             if (fileInfoService.count(queryWrapper) > 0) { | ||||
|                 throw new IllegalStateException("已存在相同的图片文件"); | ||||
|             } | ||||
|             // 提取图片元数据(使用已保存的文件,避免使用临时文件) | ||||
|             Map<String, Object> 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.getSize()); | ||||
|             fileInfo.setFilePath(uniqueFileName); | ||||
|             fileInfo.setFileMd5(fileMd5); | ||||
|             fileInfoService.save(fileInfo); | ||||
|             // 构建并返回结果 | ||||
|             Map<String, Object> result = new HashMap<>(); | ||||
|             result.put("previewUrl", "/fileInfo/preview/" + fileInfo.getId()); | ||||
|             result.put("downloadUrl", "/fileInfo/download/" + fileInfo.getId()); | ||||
|             result.put("metadata", metadata); | ||||
|             return JsonUtil.mapToJson(result); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException("文件上传失败: " + e.getMessage(), e); | ||||
|         } | ||||
|     } | ||||
|             // 标准化路径 | ||||
|             targetFilePath = Paths.get(fileAbsolutePath).toRealPath(); | ||||
|  | ||||
|     /** | ||||
|      * 计算文件的 MD5 | ||||
|      */ | ||||
|     private String calculateFileMd5(File file) throws IOException { | ||||
|         try (InputStream is = new FileInputStream(file)) { | ||||
|             return DigestUtils.md5DigestAsHex(is); | ||||
|         } | ||||
|     } | ||||
|             // 校验文件合法性:是否存在、是否为普通文件 | ||||
|             BasicFileAttributes fileAttr = Files.readAttributes(targetFilePath, BasicFileAttributes.class); | ||||
|             if (!fileAttr.isRegularFile()) { | ||||
|                 response.setStatus(HttpServletResponse.SC_BAD_REQUEST); | ||||
|                 fileInfoService.writeResponseMessage(response, "目标路径不是有效的文件"); | ||||
|                 return; | ||||
|             } | ||||
|  | ||||
|     /** | ||||
|      * 提取图片的EXIF元数据(包括定位信息) | ||||
|      */ | ||||
|     private Map<String, Object> extractImageMetadata(InputStream inputStream) { | ||||
|         try { | ||||
|             Map<String, Object> result = new HashMap<>(); | ||||
|             Metadata metadata = ImageMetadataReader.readMetadata(inputStream); | ||||
|             // 设置预览响应头 | ||||
|             String fileName = targetFilePath.getFileName().toString(); | ||||
|             String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()); | ||||
|             String contentType = Files.probeContentType(targetFilePath); | ||||
|  | ||||
|             // 遍历所有元数据目录 | ||||
|             for (Directory directory : metadata.getDirectories()) { | ||||
|                 String directoryName = directory.getName(); | ||||
|                 Map<String, String> directoryTags = new HashMap<>(); | ||||
|             // 对于文本类型文件、指定字符编码 | ||||
|             if (contentType != null && contentType.startsWith("text/")) { | ||||
|                 response.setContentType(contentType + "; charset=UTF-8"); | ||||
|             } else { | ||||
|                 response.setContentType(contentType != null ? contentType : "application/octet-stream"); | ||||
|             } | ||||
|  | ||||
|                 // 提取当前目录下的所有标签 | ||||
|                 for (com.drew.metadata.Tag tag : directory.getTags()) { | ||||
|                     directoryTags.put(tag.getTagName(), tag.getDescription()); | ||||
|                 } | ||||
|  | ||||
|                 // 存储当前目录的所有标签 | ||||
|                 if (!directoryTags.isEmpty()) { | ||||
|                     result.put(directoryName, directoryTags); | ||||
|             response.setContentLengthLong(fileAttr.size()); | ||||
|             // 关键修改:将attachment改为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]; | ||||
|                 int len; | ||||
|                 while ((len = inputStream.read(buffer)) != -1) { | ||||
|                     outputStream.write(buffer, 0, len); | ||||
|                 } | ||||
|                 outputStream.flush(); | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         } catch (ImageProcessingException | IOException e) { | ||||
|             return Collections.emptyMap(); | ||||
|         } catch (NoSuchFileException e) { | ||||
|             response.setStatus(HttpServletResponse.SC_NOT_FOUND); | ||||
|             fileInfoService.writeResponseMessage(response, "文件不存在:" + fileAbsolutePath); | ||||
|         } catch (SecurityException e) { | ||||
|             response.setStatus(HttpServletResponse.SC_FORBIDDEN); | ||||
|             fileInfoService.writeResponseMessage(response, "访问拒绝:无权限读取该文件"); | ||||
|         } catch (Exception e) { | ||||
|             response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); | ||||
|             fileInfoService.writeResponseMessage(response, "预览失败:" + e.getMessage()); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,65 +0,0 @@ | ||||
| package com.yj.earth.business.controller; | ||||
|  | ||||
| import com.yj.earth.business.service.ModelService; | ||||
| import com.yj.earth.business.service.ModelTypeService; | ||||
| import com.yj.earth.common.util.ApiResponse; | ||||
| import com.yj.earth.datasource.DatabaseManager; | ||||
| import com.yj.earth.design.Model; | ||||
| import com.yj.earth.design.ModelType; | ||||
| import com.yj.earth.dto.model.CreateModelFileDto; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import org.springframework.web.bind.annotation.PostMapping; | ||||
| import org.springframework.web.bind.annotation.RequestBody; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.io.File; | ||||
| import java.sql.Connection; | ||||
| import java.sql.DriverManager; | ||||
|  | ||||
| @Tag(name = "模型相关") | ||||
| @RestController | ||||
| @RequestMapping("/model") | ||||
| public class ModelController { | ||||
|     @Resource | ||||
|     private ModelService modelService; | ||||
|     @Resource | ||||
|     private ModelTypeService modelTypeService; | ||||
|  | ||||
|     @Operation(summary = "创建模型库") | ||||
|     @PostMapping("/createModelFile") | ||||
|     public ApiResponse createModelFile(@RequestBody CreateModelFileDto createModelFileDto) { | ||||
|         try { | ||||
|             // 获取参数并处理文件路径 | ||||
|             String folderPath = createModelFileDto.getFolderPath(); | ||||
|             String modelFileName = createModelFileDto.getModelFileName() + ".model"; | ||||
|             // 创建文件夹(如果不存在) | ||||
|             File folder = new File(folderPath); | ||||
|             if (!folder.exists()) { | ||||
|                 boolean folderCreated = folder.mkdirs(); | ||||
|                 if (!folderCreated) { | ||||
|                     return ApiResponse.failure("无法创建文件夹: " + folderPath); | ||||
|                 } | ||||
|             } | ||||
|             // 构建完整文件路径 | ||||
|             String filePath = folderPath + File.separator + modelFileName; | ||||
|             // 加载 SQLite 驱动并创建数据库文件 | ||||
|             Class.forName("org.sqlite.JDBC"); | ||||
|             // SQLite会自动创建不存在的数据库文件 | ||||
|             try (Connection connection = DriverManager.getConnection("jdbc:sqlite:" + filePath)) { | ||||
|                 if (connection != null) { | ||||
|                     // 初始化表结构 | ||||
|                     DatabaseManager.createTablesForEntities(DatabaseManager.DatabaseType.SQLITE, ModelType.class, connection); | ||||
|                     DatabaseManager.createTablesForEntities(DatabaseManager.DatabaseType.SQLITE, Model.class, connection); | ||||
|                     return ApiResponse.success(null); | ||||
|                 } | ||||
|             } | ||||
|         } catch (Exception e) { | ||||
|             return ApiResponse.failure("创建模型库失败: " + e.getMessage()); | ||||
|         } | ||||
|         return ApiResponse.failure("未知错误"); | ||||
|     } | ||||
| } | ||||
| @ -1,12 +0,0 @@ | ||||
| package com.yj.earth.business.controller; | ||||
|  | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import org.springframework.web.bind.annotation.RequestMapping; | ||||
| import org.springframework.web.bind.annotation.RestController; | ||||
|  | ||||
| @Tag(name = "模型类型管理") | ||||
| @RestController | ||||
| @RequestMapping("/modelType") | ||||
| public class ModelTypeController { | ||||
|  | ||||
| } | ||||
| @ -2,20 +2,8 @@ package com.yj.earth.business.controller; | ||||
|  | ||||
| import cn.dev33.satoken.stp.StpUtil; | ||||
| import cn.hutool.core.io.FileUtil; | ||||
| import cn.hutool.core.lang.UUID; | ||||
| import cn.hutool.core.util.IdUtil; | ||||
| import cn.hutool.crypto.digest.DigestUtil; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | ||||
| import com.drew.imaging.ImageMetadataReader; | ||||
| import com.drew.imaging.ImageProcessingException; | ||||
| import com.drew.metadata.Directory; | ||||
| import com.drew.metadata.Metadata; | ||||
| import com.fasterxml.jackson.core.JsonProcessingException; | ||||
| import com.fasterxml.jackson.databind.ObjectMapper; | ||||
| import com.yj.earth.annotation.CheckAuth; | ||||
| import com.yj.earth.business.domain.FileInfo; | ||||
| import com.yj.earth.business.domain.Role; | ||||
| import com.yj.earth.business.domain.Source; | ||||
| import com.yj.earth.business.service.*; | ||||
| import com.yj.earth.common.service.SourceDataGenerator; | ||||
| @ -24,6 +12,7 @@ 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.*; | ||||
| import com.yj.earth.params.Point; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| @ -34,9 +23,6 @@ import org.springframework.web.bind.annotation.*; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
|  | ||||
| import javax.annotation.Resource; | ||||
| import java.io.File; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.*; | ||||
|  | ||||
| import static com.yj.earth.common.constant.GlobalConstant.DIRECTORY; | ||||
| @ -62,12 +48,6 @@ public class SourceController { | ||||
|     @Resource | ||||
|     private FileInfoService fileInfoService; | ||||
|  | ||||
|     @Resource | ||||
|     private FileInfoController fileInfoControllerl; | ||||
|  | ||||
|     @Value("${file.upload.path}") | ||||
|     private String uploadPath; | ||||
|  | ||||
|     @PostMapping("/addDirectory") | ||||
|     @Operation(summary = "新增目录资源") | ||||
|     public ApiResponse addDirectory(@RequestBody AddDirectoryDto addDirectoryDto) { | ||||
| @ -223,23 +203,75 @@ public class SourceController { | ||||
|     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) { | ||||
|         // 验证并转换参数 | ||||
|         sourceParamsValidator.validateAndConvert( | ||||
|                 sourceType, | ||||
|                 JsonUtil.jsonToMap(params) | ||||
|         ); | ||||
|  | ||||
|         List<Source> sources = new ArrayList<>(); | ||||
|         for (int i = 0; i < files.length; i++) { | ||||
|             String detail = fileInfoControllerl.handleLocationImageUpload(files[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("locationImage"); | ||||
|             source.setSourceType(sourceType); | ||||
|             source.setTreeIndex(treeIndex); | ||||
|             source.setDetail(detail); | ||||
|  | ||||
|             // 转换为对象 | ||||
|             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); | ||||
|             } | ||||
|  | ||||
|             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(null); | ||||
|         return ApiResponse.success(sources); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("/type") | ||||
|  | ||||
| @ -38,7 +38,7 @@ public class TacticalCalculationController { | ||||
|             // 同向而行:时间 = 距离 / (速度差) | ||||
|             double speedDiff = Math.abs(input.getSpeed1() - input.getSpeed2()); | ||||
|             if (speedDiff <= 0) { | ||||
|                 throw new IllegalArgumentException("同向而行时,速度不能相等或后方速度小于前方"); | ||||
|                 throw new IllegalArgumentException("同向而行时、速度不能相等或后方速度小于前方"); | ||||
|             } | ||||
|             meetTimeHours = input.getInitialDistance() / speedDiff; | ||||
|         } else { | ||||
|  | ||||
| @ -1,49 +1,34 @@ | ||||
| package com.yj.earth.business.domain; | ||||
|  | ||||
| import com.baomidou.mybatisplus.annotation.FieldFill; | ||||
| import com.baomidou.mybatisplus.annotation.IdType; | ||||
| import com.baomidou.mybatisplus.annotation.TableField; | ||||
| import com.baomidou.mybatisplus.annotation.TableId; | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.AllArgsConstructor; | ||||
| import lombok.Data; | ||||
| import lombok.NoArgsConstructor; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
|  | ||||
| @Data | ||||
| public class Model implements Serializable { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
| @NoArgsConstructor | ||||
| @AllArgsConstructor | ||||
| public class Model { | ||||
|     @Schema(description = "主键") | ||||
|     @TableId(value = "id", type = IdType.ASSIGN_UUID) | ||||
|     private String id; | ||||
|  | ||||
|     @Schema(description = "模型类型ID") | ||||
|     private String modelTypeId; | ||||
|  | ||||
|     @Schema(description = "模型名称") | ||||
|     private String modelName; | ||||
|  | ||||
|     @Schema(description = "模型类型") | ||||
|     private String modelType; | ||||
|  | ||||
|     @Schema(description = "海报类型") | ||||
|     private String posterType; | ||||
|  | ||||
|     @Schema(description = "海报") | ||||
|     private byte[] poster; | ||||
|  | ||||
|     @Schema(description = "数据") | ||||
|     private byte[] data; | ||||
|  | ||||
|     @Schema(description = "视图") | ||||
|     @Schema(description = "海报数据") | ||||
|     private String poster; | ||||
|     @Schema(description = "模型数据") | ||||
|     private String data; | ||||
|     @Schema(description = "模型视图") | ||||
|     private String view; | ||||
|  | ||||
|     @TableField(fill = FieldFill.INSERT) | ||||
|     @Schema(description = "创建时间") | ||||
|     private LocalDateTime createdAt; | ||||
|  | ||||
|     @TableField(fill = FieldFill.UPDATE) | ||||
|     @Schema(description = "更新时间") | ||||
|     private LocalDateTime updatedAt; | ||||
| } | ||||
|  | ||||
| @ -1,35 +1,20 @@ | ||||
| package com.yj.earth.business.domain; | ||||
|  | ||||
| import com.baomidou.mybatisplus.annotation.FieldFill; | ||||
| import com.baomidou.mybatisplus.annotation.IdType; | ||||
| import com.baomidou.mybatisplus.annotation.TableField; | ||||
| import com.baomidou.mybatisplus.annotation.TableId; | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
|  | ||||
| import java.io.Serializable; | ||||
| import java.time.LocalDateTime; | ||||
|  | ||||
| @Data | ||||
| public class ModelType implements Serializable { | ||||
|  | ||||
|     private static final long serialVersionUID = 1L; | ||||
|  | ||||
| public class ModelType { | ||||
|     @Schema(description = "主键") | ||||
|     @TableId(value = "id", type = IdType.ASSIGN_UUID) | ||||
|     private String id; | ||||
|  | ||||
|     @Schema(description = "模型类型名称") | ||||
|     private String name; | ||||
|  | ||||
|     @Schema(description = "模型类型父级ID") | ||||
|     @Schema(description = "父级节点ID") | ||||
|     private String parentId; | ||||
|  | ||||
|     @Schema(description = "创建时间") | ||||
|     @TableField(fill = FieldFill.INSERT) | ||||
|     private LocalDateTime createdAt; | ||||
|  | ||||
|     @Schema(description = "更新时间") | ||||
|     @TableField(fill = FieldFill.UPDATE) | ||||
|     private LocalDateTime updatedAt; | ||||
| } | ||||
|  | ||||
| @ -1,18 +0,0 @@ | ||||
| package com.yj.earth.business.mapper; | ||||
|  | ||||
| import com.yj.earth.business.domain.Model; | ||||
| import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||||
| import org.apache.ibatis.annotations.Mapper; | ||||
|  | ||||
| /** | ||||
|  * <p> | ||||
|  *  Mapper 接口 | ||||
|  * </p> | ||||
|  * | ||||
|  * @author 周志雄 | ||||
|  * @since 2025-09-16 | ||||
|  */ | ||||
| @Mapper | ||||
| public interface ModelMapper extends BaseMapper<Model> { | ||||
|  | ||||
| } | ||||
| @ -1,18 +0,0 @@ | ||||
| package com.yj.earth.business.mapper; | ||||
|  | ||||
| import com.yj.earth.business.domain.ModelType; | ||||
| import com.baomidou.mybatisplus.core.mapper.BaseMapper; | ||||
| import org.apache.ibatis.annotations.Mapper; | ||||
|  | ||||
| /** | ||||
|  * <p> | ||||
|  *  Mapper 接口 | ||||
|  * </p> | ||||
|  * | ||||
|  * @author 周志雄 | ||||
|  * @since 2025-09-16 | ||||
|  */ | ||||
| @Mapper | ||||
| public interface ModelTypeMapper extends BaseMapper<ModelType> { | ||||
|  | ||||
| } | ||||
| @ -2,8 +2,16 @@ package com.yj.earth.business.service; | ||||
|  | ||||
| import com.yj.earth.business.domain.FileInfo; | ||||
| import com.baomidou.mybatisplus.extension.service.IService; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
|  | ||||
| import java.util.Map; | ||||
|  | ||||
| public interface FileInfoService extends IService<FileInfo> { | ||||
|     // 根据文件ID获取文件绝对路径 | ||||
|     String getFileAbsolutePath(String id); | ||||
|     Map<String, Object> handleLocationImageUpload(MultipartFile file); | ||||
|     String uploadWithPreview(MultipartFile file); | ||||
|     void writeResponseMessage(HttpServletResponse response, String message); | ||||
|     String getFullUploadPath(); | ||||
| } | ||||
|  | ||||
| @ -1,16 +0,0 @@ | ||||
| package com.yj.earth.business.service; | ||||
|  | ||||
| import com.yj.earth.business.domain.Model; | ||||
| import com.baomidou.mybatisplus.extension.service.IService; | ||||
|  | ||||
| /** | ||||
|  * <p> | ||||
|  *  服务类 | ||||
|  * </p> | ||||
|  * | ||||
|  * @author 周志雄 | ||||
|  * @since 2025-09-16 | ||||
|  */ | ||||
| public interface ModelService extends IService<Model> { | ||||
|  | ||||
| } | ||||
| @ -1,16 +0,0 @@ | ||||
| package com.yj.earth.business.service; | ||||
|  | ||||
| import com.yj.earth.business.domain.ModelType; | ||||
| import com.baomidou.mybatisplus.extension.service.IService; | ||||
|  | ||||
| /** | ||||
|  * <p> | ||||
|  *  服务类 | ||||
|  * </p> | ||||
|  * | ||||
|  * @author 周志雄 | ||||
|  * @since 2025-09-16 | ||||
|  */ | ||||
| public interface ModelTypeService extends IService<ModelType> { | ||||
|  | ||||
| } | ||||
| @ -1,21 +1,38 @@ | ||||
| package com.yj.earth.business.service.impl; | ||||
|  | ||||
| import cn.hutool.core.io.FileUtil; | ||||
| import com.drew.imaging.ImageMetadataReader; | ||||
| import com.drew.lang.Rational; | ||||
| import com.drew.metadata.Metadata; | ||||
| import com.drew.metadata.exif.GpsDirectory; | ||||
| import com.yj.earth.business.domain.FileInfo; | ||||
| import com.yj.earth.business.mapper.FileInfoMapper; | ||||
| import com.yj.earth.business.service.FileInfoService; | ||||
| import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | ||||
| import com.yj.earth.datasource.DatabaseManager; | ||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||
| import jakarta.servlet.http.HttpServletResponse; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.stereotype.Service; | ||||
| import org.springframework.util.DigestUtils; | ||||
| import org.springframework.web.bind.annotation.PathVariable; | ||||
| import org.springframework.web.multipart.MultipartFile; | ||||
|  | ||||
| import java.io.File; | ||||
| import java.io.FileInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.Path; | ||||
| import java.nio.file.Paths; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.UUID; | ||||
|  | ||||
| @Service | ||||
| public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> implements FileInfoService { | ||||
|  | ||||
|     @Value("${file.upload.path}") | ||||
|     private String uploadPath; | ||||
|     private static final String DEFAULT_UPLOAD_PATH = "upload"; | ||||
|  | ||||
|     public String getFileAbsolutePath(String id) { | ||||
|  | ||||
| @ -26,7 +43,7 @@ public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> i | ||||
|         } | ||||
|  | ||||
|         // 构建完整文件路径 | ||||
|         String fullPath = uploadPath + File.separator + fileInfo.getFilePath(); | ||||
|         String fullPath = DEFAULT_UPLOAD_PATH + File.separator + fileInfo.getFilePath(); | ||||
|         File file = new File(fullPath); | ||||
|  | ||||
|         // 校验文件是否存在 | ||||
| @ -37,4 +54,187 @@ public class FileInfoServiceImpl extends ServiceImpl<FileInfoMapper, FileInfo> i | ||||
|         // 获取并返回绝对路径 | ||||
|         return file.getAbsolutePath(); | ||||
|     } | ||||
|  | ||||
|     public Map<String, Object> handleLocationImageUpload(MultipartFile file) { | ||||
|         // 构建并返回结果 | ||||
|         Map<String, Object> result = new HashMap<>(); | ||||
|         try { | ||||
|             // 校验文件是否为空 | ||||
|             if (file.isEmpty()) { | ||||
|                 throw new IllegalArgumentException("上传文件不能为空"); | ||||
|             } | ||||
|             // 获取文件基本信息 | ||||
|             String originalFilename = file.getOriginalFilename(); | ||||
|             String fileSuffix = FileUtil.extName(originalFilename); | ||||
|             String contentType = file.getContentType(); | ||||
|             // 验证是否为图片文件 | ||||
|             if (contentType == null || !contentType.startsWith("image/")) { | ||||
|                 throw new IllegalArgumentException("请上传图片文件"); | ||||
|             } | ||||
|             // 获取完整的上传目录路径 | ||||
|             Path fullUploadPath = Paths.get(getFullUploadPath()); | ||||
|             // 生成唯一文件名 | ||||
|             String uniqueFileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileSuffix; | ||||
|             // 创建文件存储目录 | ||||
|             Files.createDirectories(fullUploadPath); | ||||
|             // 构建完整文件路径并保存文件 | ||||
|             Path destFilePath = fullUploadPath.resolve(uniqueFileName); | ||||
|             // 先将文件保存到目标位置 | ||||
|             file.transferTo(destFilePath); | ||||
|             // 计算文件MD5(使用已保存的文件) | ||||
|             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.getSize()); | ||||
|             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(); | ||||
|         // 构建并返回结果 | ||||
|         Map<String, Object> result = new HashMap<>(); | ||||
|         try { | ||||
|             // 校验文件是否为空 | ||||
|             if (file.isEmpty()) { | ||||
|                 throw new IllegalArgumentException("上传文件不能为空"); | ||||
|             } | ||||
|             // 获取文件基本信息 | ||||
|             String originalFilename = file.getOriginalFilename(); | ||||
|             String fileSuffix = FileUtil.extName(originalFilename); | ||||
|             String contentType = file.getContentType(); | ||||
|             // 获取完整的上传目录路径 | ||||
|             Path fullUploadPath = Paths.get(getFullUploadPath()); | ||||
|             // 生成唯一文件名 | ||||
|             String uniqueFileName = UUID.randomUUID().toString().replaceAll("-", "") + "." + fileSuffix; | ||||
|             // 创建文件存储目录 | ||||
|             Files.createDirectories(fullUploadPath); | ||||
|             // 构建完整文件路径并保存文件 | ||||
|             Path destFilePath = fullUploadPath.resolve(uniqueFileName); | ||||
|             // 先将文件保存到目标位置 | ||||
|             file.transferTo(destFilePath); | ||||
|             // 计算文件MD5(使用已保存的文件) | ||||
|             String fileMd5 = calculateFileMd5(destFilePath.toFile()); | ||||
|             // 保存文件信息到数据库 | ||||
|             fileInfo.setFileName(originalFilename); | ||||
|             fileInfo.setFileSuffix(fileSuffix); | ||||
|             fileInfo.setContentType(contentType); | ||||
|             fileInfo.setFileSize(file.getSize()); | ||||
|             fileInfo.setFilePath(uniqueFileName); | ||||
|             fileInfo.setFileMd5(fileMd5); | ||||
|             this.save(fileInfo); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException("文件上传失败: " + e.getMessage(), e); | ||||
|         } | ||||
|         return "/fileInfo/preview/" + fileInfo.getId(); | ||||
|     } | ||||
|  | ||||
|     public void writeResponseMessage(HttpServletResponse response, String message) { | ||||
|         try { | ||||
|             response.setContentType("text/plain; charset=UTF-8"); | ||||
|             response.getWriter().write(message); | ||||
|             response.getWriter().flush(); | ||||
|         } catch (IOException e) { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public String getFullUploadPath() { | ||||
|         // 拼接项目根目录和配置的上传路径 | ||||
|         return DatabaseManager.getRecommendedCacheDirectory() + File.separator + DEFAULT_UPLOAD_PATH; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 提取图片的EXIF元数据 | ||||
|      */ | ||||
|     public Map<String, Double> extractImageMetadata(InputStream inputStream) { | ||||
|         Map<String, Double> result = new HashMap<>(3); | ||||
|         try { | ||||
|             Metadata metadata = ImageMetadataReader.readMetadata(inputStream); | ||||
|             // 获取GPS相关元数据目录(图片的经纬度和高度通常存储在GPS目录中) | ||||
|             GpsDirectory gpsDirectory = metadata.getFirstDirectoryOfType(GpsDirectory.class); | ||||
|             if (gpsDirectory == null) { | ||||
|                 return result; | ||||
|             } | ||||
|  | ||||
|             // 解析纬度(度分秒转十进制) | ||||
|             Double latitude = parseLatitudeOrLongitude( | ||||
|                     gpsDirectory.getRationalArray(GpsDirectory.TAG_LATITUDE), | ||||
|                     gpsDirectory.getString(GpsDirectory.TAG_LATITUDE_REF) | ||||
|             ); | ||||
|  | ||||
|             // 解析经度(度分秒转十进制) | ||||
|             Double longitude = parseLatitudeOrLongitude( | ||||
|                     gpsDirectory.getRationalArray(GpsDirectory.TAG_LONGITUDE), | ||||
|                     gpsDirectory.getString(GpsDirectory.TAG_LONGITUDE_REF) | ||||
|             ); | ||||
|  | ||||
|             // 解析高度(考虑海拔参考、0表示海平面以上、1表示以下) | ||||
|             Double altitude = null; | ||||
|             if (gpsDirectory.containsTag(GpsDirectory.TAG_ALTITUDE)) { | ||||
|                 Rational altitudeRational = gpsDirectory.getRational(GpsDirectory.TAG_ALTITUDE); | ||||
|                 if (altitudeRational != null) { | ||||
|                     altitude = altitudeRational.doubleValue(); | ||||
|                     // 处理海拔参考(是否在海平面以下) | ||||
|                     if (gpsDirectory.containsTag(GpsDirectory.TAG_ALTITUDE_REF) && | ||||
|                             gpsDirectory.getInt(GpsDirectory.TAG_ALTITUDE_REF) == 1) { | ||||
|                         altitude = -altitude; | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             // 存入结果 Map | ||||
|             result.put("lat", latitude); | ||||
|             result.put("lon", longitude); | ||||
|             result.put("alt", altitude); | ||||
|         } catch (Exception e) { | ||||
|             throw new RuntimeException(e); | ||||
|         } | ||||
|         return result; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 将GPS的度分秒格式转换为十进制坐标 | ||||
|      */ | ||||
|     public Double parseLatitudeOrLongitude(Rational[] degreesMinutesSeconds, String ref) { | ||||
|         if (degreesMinutesSeconds == null || degreesMinutesSeconds.length != 3 || ref == null) { | ||||
|             return null; | ||||
|         } | ||||
|         // 度分秒转十进制:度 + 分/60 + 秒/3600 | ||||
|         double degrees = degreesMinutesSeconds[0].doubleValue(); | ||||
|         double minutes = degreesMinutesSeconds[1].doubleValue(); | ||||
|         double seconds = degreesMinutesSeconds[2].doubleValue(); | ||||
|         double value = degrees + (minutes / 60) + (seconds / 3600); | ||||
|         // 根据方向参考调整正负(南纬/西经为负) | ||||
|         if (ref.equalsIgnoreCase("S") || ref.equalsIgnoreCase("W")) { | ||||
|             value = -value; | ||||
|         } | ||||
|  | ||||
|         return value; | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 计算文件的 MD5 | ||||
|      */ | ||||
|     public String calculateFileMd5(File file) throws IOException { | ||||
|         try (InputStream is = new FileInputStream(file)) { | ||||
|             return DigestUtils.md5DigestAsHex(is); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -1,20 +0,0 @@ | ||||
| package com.yj.earth.business.service.impl; | ||||
|  | ||||
| import com.yj.earth.business.domain.Model; | ||||
| import com.yj.earth.business.mapper.ModelMapper; | ||||
| import com.yj.earth.business.service.ModelService; | ||||
| import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| /** | ||||
|  * <p> | ||||
|  *  服务实现类 | ||||
|  * </p> | ||||
|  * | ||||
|  * @author 周志雄 | ||||
|  * @since 2025-09-16 | ||||
|  */ | ||||
| @Service | ||||
| public class ModelServiceImpl extends ServiceImpl<ModelMapper, Model> implements ModelService { | ||||
|  | ||||
| } | ||||
| @ -1,20 +0,0 @@ | ||||
| package com.yj.earth.business.service.impl; | ||||
|  | ||||
| import com.yj.earth.business.domain.ModelType; | ||||
| import com.yj.earth.business.mapper.ModelTypeMapper; | ||||
| import com.yj.earth.business.service.ModelTypeService; | ||||
| import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; | ||||
| import org.springframework.stereotype.Service; | ||||
|  | ||||
| /** | ||||
|  * <p> | ||||
|  *  服务实现类 | ||||
|  * </p> | ||||
|  * | ||||
|  * @author 周志雄 | ||||
|  * @since 2025-09-16 | ||||
|  */ | ||||
| @Service | ||||
| public class ModelTypeServiceImpl extends ServiceImpl<ModelTypeMapper, ModelType> implements ModelTypeService { | ||||
|  | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 ZZX9599
					ZZX9599