资源相关
This commit is contained in:
		| @ -2,6 +2,17 @@ package com.yj.earth.business.controller; | ||||
|  | ||||
| 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.metadata.Directory; | ||||
| import com.drew.metadata.Metadata; | ||||
|  | ||||
| import java.io.*; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.Path; | ||||
| import java.nio.file.Paths; | ||||
| import java.util.*; | ||||
|  | ||||
| import cn.hutool.crypto.digest.DigestUtil; | ||||
| import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | ||||
| import com.baomidou.mybatisplus.core.metadata.IPage; | ||||
| @ -9,6 +20,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page; | ||||
| import com.yj.earth.business.domain.FileInfo; | ||||
| import com.yj.earth.business.service.FileInfoService; | ||||
| import com.yj.earth.common.util.ApiResponse; | ||||
| import com.yj.earth.common.util.JsonMapConverter; | ||||
| import com.yj.earth.vo.FileInfoVo; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||
| @ -17,17 +29,16 @@ import jakarta.servlet.http.HttpServletResponse; | ||||
| import org.springframework.beans.BeanUtils; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| import org.springframework.util.DigestUtils; | ||||
| 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.OutputStream; | ||||
| import java.io.InputStream; | ||||
| import java.net.URLEncoder; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| @Tag(name = "文件数据管理") | ||||
| @ -168,11 +179,7 @@ public class FileInfoController { | ||||
|  | ||||
|     @Operation(summary = "文件预览") | ||||
|     @GetMapping("/preview/{id}") | ||||
|     public void previewFile( | ||||
|             @Parameter(description = "文件ID", required = true) | ||||
|             @PathVariable String id, | ||||
|             HttpServletResponse response) throws IOException { | ||||
|  | ||||
|     public void previewFile(@Parameter(description = "文件ID", required = true) @PathVariable String id, HttpServletResponse response) throws IOException { | ||||
|         // 根据ID查询文件信息 | ||||
|         FileInfo fileInfo = fileInfoService.getById(id); | ||||
|         if (fileInfo == null) { | ||||
| @ -200,42 +207,100 @@ public class FileInfoController { | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "文件列表") | ||||
|     @GetMapping("/list") | ||||
|     public ApiResponse getFileList( | ||||
|             @Parameter(description = "页码", required = true) Integer pageNum, | ||||
|             @Parameter(description = "每页条数", required = true) Integer pageSize, | ||||
|             @Parameter(description = "文件名称") String fileName) { | ||||
|  | ||||
|         // 创建分页对象 | ||||
|         Page<FileInfo> page = new Page<>(pageNum, pageSize); | ||||
|         // 构建查询条件 | ||||
|         LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>(); | ||||
|         if (fileName != null && !fileName.isEmpty()) { | ||||
|             queryWrapper.like(FileInfo::getFileName, fileName); | ||||
|     public String handleLocationImageUpload(MultipartFile file) { | ||||
|         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 JsonMapConverter.mapToJson(result); | ||||
|         } catch (IOException e) { | ||||
|             throw new RuntimeException("文件上传失败: " + e.getMessage(), e); | ||||
|         } | ||||
|         // 按创建时间倒序排列、最新上传的文件在前 | ||||
|         queryWrapper.orderByDesc(FileInfo::getCreatedAt); | ||||
|     } | ||||
|  | ||||
|         // 执行分页查询 | ||||
|         IPage<FileInfo> fileInfoPage = fileInfoService.page(page, queryWrapper); | ||||
|     /** | ||||
|      * 计算文件的 MD5 | ||||
|      */ | ||||
|     private String calculateFileMd5(File file) throws IOException { | ||||
|         try (InputStream is = new FileInputStream(file)) { | ||||
|             return DigestUtils.md5DigestAsHex(is); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|         // 转换为VO对象并设置URL | ||||
|         List<FileInfoVo> records = fileInfoPage.getRecords().stream().map(fileInfo -> { | ||||
|             FileInfoVo vo = new FileInfoVo(); | ||||
|             BeanUtils.copyProperties(fileInfo, vo); | ||||
|             vo.setPreviewUrl("/fileInfo/preview/" + fileInfo.getId()); | ||||
|             vo.setDownloadUrl("/fileInfo/download/" + fileInfo.getId()); | ||||
|             return vo; | ||||
|         }).collect(Collectors.toList()); | ||||
|     /** | ||||
|      * 提取图片的EXIF元数据(包括定位信息) | ||||
|      */ | ||||
|     private Map<String, Object> extractImageMetadata(InputStream inputStream) { | ||||
|         try { | ||||
|             Map<String, Object> result = new HashMap<>(); | ||||
|             Metadata metadata = ImageMetadataReader.readMetadata(inputStream); | ||||
|  | ||||
|         // 构建分页结果 | ||||
|         Page<FileInfoVo> resultPage = new Page<>(); | ||||
|         resultPage.setRecords(records); | ||||
|         resultPage.setTotal(fileInfoPage.getTotal()); | ||||
|         resultPage.setSize(fileInfoPage.getSize()); | ||||
|         resultPage.setCurrent(fileInfoPage.getCurrent()); | ||||
|         resultPage.setPages(fileInfoPage.getPages()); | ||||
|         return ApiResponse.success(resultPage); | ||||
|             // 遍历所有元数据目录 | ||||
|             for (Directory directory : metadata.getDirectories()) { | ||||
|                 String directoryName = directory.getName(); | ||||
|                 Map<String, String> directoryTags = new HashMap<>(); | ||||
|  | ||||
|                 // 提取当前目录下的所有标签 | ||||
|                 for (com.drew.metadata.Tag tag : directory.getTags()) { | ||||
|                     directoryTags.put(tag.getTagName(), tag.getDescription()); | ||||
|                 } | ||||
|  | ||||
|                 // 存储当前目录的所有标签 | ||||
|                 if (!directoryTags.isEmpty()) { | ||||
|                     result.put(directoryName, directoryTags); | ||||
|                 } | ||||
|             } | ||||
|  | ||||
|             return result; | ||||
|         } catch (ImageProcessingException | IOException e) { | ||||
|             return Collections.emptyMap(); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -8,6 +8,7 @@ import com.graphhopper.GraphHopper; | ||||
| import com.graphhopper.ResponsePath; | ||||
| import com.graphhopper.config.Profile; | ||||
| import com.graphhopper.util.shapes.GHPoint; | ||||
| import com.yj.earth.annotation.CheckAuth; | ||||
| import com.yj.earth.business.domain.FileInfo; | ||||
| import com.yj.earth.business.service.FileInfoService; | ||||
| import com.yj.earth.common.config.GraphHopperProperties; | ||||
| @ -51,6 +52,7 @@ public class GraphHopperController { | ||||
|     private final AtomicBoolean isLoaded = new AtomicBoolean(false); | ||||
|  | ||||
|     @Operation(summary = "获取地图列表") | ||||
|     @CheckAuth | ||||
|     @GetMapping("/list") | ||||
|     public ApiResponse list() { | ||||
|         LambdaQueryWrapper<FileInfo> queryWrapper = new LambdaQueryWrapper<>(); | ||||
| @ -60,6 +62,7 @@ public class GraphHopperController { | ||||
|  | ||||
|     @Operation(summary = "加载地图数据") | ||||
|     @PostMapping("/loadMap") | ||||
|     @CheckAuth | ||||
|     public ApiResponse loadMap(@Parameter(description = "文件ID") @RequestParam String fileId) { | ||||
|         // 参数校验 | ||||
|         if (fileId == null) { | ||||
| @ -118,6 +121,7 @@ public class GraphHopperController { | ||||
|  | ||||
|     @Operation(summary = "路径规划") | ||||
|     @PostMapping("/route") | ||||
|     @CheckAuth | ||||
|     public ApiResponse calculateRoute(@RequestBody RouteRequest request) { | ||||
|         // 校验地图是否加载完成 + 实例是否可用 | ||||
|         if (!isLoaded.get() || currentHopper == null) { | ||||
| @ -145,26 +149,36 @@ public class GraphHopperController { | ||||
|  | ||||
|             // 处理错误 | ||||
|             if (response.hasErrors()) { | ||||
|                 return ApiResponse.failure("路径计算失败: " + response.getErrors().toString()); | ||||
|             } | ||||
|                 // 检查是否有超出范围的错误 | ||||
|                 boolean hasOutOfBoundsError = response.getErrors().stream() | ||||
|                         .anyMatch(e -> e instanceof com.graphhopper.util.exceptions.PointOutOfBoundsException); | ||||
|  | ||||
|                 if (hasOutOfBoundsError) { | ||||
|                     // 返回超出范围的特定格式响应 | ||||
|                     return ApiResponse.failure("路径超出地图范围"); | ||||
|                 } else { | ||||
|                     return ApiResponse.failure("路径计算失败: " + response.getErrors().toString()); | ||||
|                 } | ||||
|             } | ||||
|             // 解析结果 | ||||
|             ResponsePath bestPath = response.getBest(); | ||||
|             List<Point> pathPoints = new ArrayList<>(); | ||||
|             bestPath.getPoints().forEach(ghPoint -> | ||||
|                     pathPoints.add(new Point(ghPoint.getLat(), ghPoint.getLon())) | ||||
|             ); | ||||
|  | ||||
|             // 封装返回 | ||||
|             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("路径超出地图范围"); | ||||
|         } catch (Exception e) { | ||||
|             return ApiResponse.failure("路径计算异常: " + e.getMessage()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "获取交通方式") | ||||
|     @CheckAuth | ||||
|     @PostMapping("/profiles") | ||||
|     public ApiResponse profiles() { | ||||
|         return ApiResponse.success(graphHopperProperties.getProfiles()); | ||||
|  | ||||
| @ -2,26 +2,40 @@ 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.RoleSourceService; | ||||
| import com.yj.earth.business.service.SourceService; | ||||
| import com.yj.earth.business.service.UserService; | ||||
| import com.yj.earth.business.service.*; | ||||
| import com.yj.earth.common.service.SourceDataGenerator; | ||||
| import com.yj.earth.common.service.SourceParamsValidator; | ||||
| import com.yj.earth.common.util.ApiResponse; | ||||
| import com.yj.earth.common.util.MapUtil; | ||||
| import com.yj.earth.dto.source.*; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.Parameter; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import lombok.extern.slf4j.Slf4j; | ||||
| import org.springframework.beans.BeanUtils; | ||||
| import org.springframework.beans.factory.annotation.Value; | ||||
| 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; | ||||
| @ -39,10 +53,19 @@ public class SourceController { | ||||
|     @Resource | ||||
|     private RoleSourceService roleSourceService; | ||||
|     @Resource | ||||
|     private RoleService roleService; | ||||
|     @Resource | ||||
|     private SourceParamsValidator sourceParamsValidator; | ||||
|  | ||||
|     @Resource | ||||
|     private SourceDataGenerator sourceDataGenerator; | ||||
|     @Resource | ||||
|     private FileInfoService fileInfoService; | ||||
|  | ||||
|     @Resource | ||||
|     private FileInfoController fileInfoControllerl; | ||||
|  | ||||
|     @Value("${file.upload.path}") | ||||
|     private String uploadPath; | ||||
|  | ||||
|     @PostMapping("/addDirectory") | ||||
|     @Operation(summary = "新增目录资源") | ||||
| @ -86,10 +109,10 @@ public class SourceController { | ||||
|         source.setSourceName(sourceName); | ||||
|         source.setParentId(addModelSourceDto.getParentId()); | ||||
|         source.setTreeIndex(addModelSourceDto.getTreeIndex()); | ||||
|         source.setParams(addModelSourceDto.getParams()); | ||||
|         source.setDetail(detail); | ||||
|         source.setSourceType(MapUtil.getString(MapUtil.jsonToMap(detail), "fileType")); | ||||
|         source.setIsShow(SHOW); | ||||
|         source.setParams(addModelSourceDto.getParams()); | ||||
|         sourceService.save(source); | ||||
|         // 添加资源到该用户的角色下 | ||||
|         roleSourceService.addRoleSource(userService.getById(StpUtil.getLoginIdAsString()).getRoleId(), source.getId()); | ||||
| @ -196,6 +219,30 @@ public class SourceController { | ||||
|         return ApiResponse.success(null); | ||||
|     } | ||||
|  | ||||
|     @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("files") @Parameter(description = "带有定位的图片文件", required = true) MultipartFile[] files) { | ||||
|         for (int i = 0; i < files.length; i++) { | ||||
|             String detail = fileInfoControllerl.handleLocationImageUpload(files[i]); | ||||
|             // 构建并保存资源对象 | ||||
|             Source source = new Source(); | ||||
|             source.setId(ids.get(i)); | ||||
|             source.setSourceName(files[i].getOriginalFilename()); | ||||
|             source.setParentId(parentId); | ||||
|             source.setSourceType("locationImage"); | ||||
|             source.setTreeIndex(treeIndex); | ||||
|             source.setDetail(detail); | ||||
|             source.setIsShow(SHOW); | ||||
|             sourceService.save(source); | ||||
|             // 添加资源到该用户的角色下 | ||||
|             roleSourceService.addRoleSource(userService.getById(StpUtil.getLoginIdAsString()).getRoleId(), source.getId()); | ||||
|         } | ||||
|         return ApiResponse.success(null); | ||||
|     } | ||||
|  | ||||
|     @GetMapping("/type") | ||||
|     @Operation(summary = "获取已有类型") | ||||
|     public ApiResponse getSupportedSourceTypes() { | ||||
| @ -208,4 +255,61 @@ public class SourceController { | ||||
|     public String getExampleData(String sourceType) throws JsonProcessingException { | ||||
|         return sourceDataGenerator.generateDefaultJson(sourceType); | ||||
|     } | ||||
|  | ||||
|     @Operation(summary = "设置默认数据") | ||||
|     @GetMapping("/default") | ||||
|     public ApiResponse getDefaultData() { | ||||
|         log.info("开始初始化默认资源数据"); | ||||
|         String userId = StpUtil.getLoginIdAsString(); | ||||
|         String roleId = userService.getById(userId).getRoleId(); | ||||
|  | ||||
|         // 创建一级目录 | ||||
|         createSourceIfNotExists("倾斜模型", "directory", null, 0, 1, roleId); | ||||
|         createSourceIfNotExists("人工模型", "directory", null, 0, 1, roleId); | ||||
|         createSourceIfNotExists("卫星底图", "directory", null, 0, 1, roleId); | ||||
|         createSourceIfNotExists("地形", "directory", null, 0, 1, roleId); | ||||
|  | ||||
|         // 创建"在线图源"目录及其子资源 | ||||
|         Source onlineSource = createSourceIfNotExists("在线图源", "directory", null, 0, 1, roleId); | ||||
|         if (onlineSource != null) { | ||||
|             String onlineSourceId = onlineSource.getId(); | ||||
|             createSourceIfNotExists("卫星图", "arcgisWximagery", onlineSourceId, 0, 1, roleId); | ||||
|             createSourceIfNotExists("暗黑地图", "arcgisBlueImagery", onlineSourceId, 0, 1, roleId); | ||||
|             createSourceIfNotExists("路网图", "gdlwImagery", onlineSourceId, 0, 1, roleId); | ||||
|             createSourceIfNotExists("矢量图", "gdslImagery", onlineSourceId, 0, 1, roleId); | ||||
|         } | ||||
|  | ||||
|         log.info("默认资源数据初始化完成"); | ||||
|         return ApiResponse.success(null); | ||||
|     } | ||||
|  | ||||
|     /** | ||||
|      * 检查资源是否存在、不存在则创建并关联角色资源 | ||||
|      */ | ||||
|     private Source createSourceIfNotExists(String sourceName, String sourceType, String parentId, int treeIndex, int isShow, String roleId) { | ||||
|         // 检查资源是否已存在(通过名称和父ID组合判断唯一性) | ||||
|         Source existingSource = sourceService.getOne(new LambdaQueryWrapper<Source>() | ||||
|                 .eq(Source::getSourceName, sourceName) | ||||
|                 .eq(parentId != null, Source::getParentId, parentId) | ||||
|                 .isNull(parentId == null, Source::getParentId)); | ||||
|         if (existingSource != null) { | ||||
|             return existingSource; | ||||
|         } | ||||
|         // 不存在则创建新资源 | ||||
|         try { | ||||
|             Source newSource = new Source(); | ||||
|             newSource.setId(cn.hutool.core.lang.UUID.fastUUID().toString(true)); | ||||
|             newSource.setSourceName(sourceName); | ||||
|             newSource.setSourceType(sourceType); | ||||
|             newSource.setParentId(parentId); | ||||
|             newSource.setTreeIndex(treeIndex); | ||||
|             newSource.setIsShow(isShow); | ||||
|             sourceService.save(newSource); | ||||
|             // 关联角色资源 | ||||
|             roleSourceService.addRoleSource(roleId, newSource.getId()); | ||||
|             return newSource; | ||||
|         } catch (Exception e) { | ||||
|             return null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -0,0 +1,436 @@ | ||||
| package com.yj.earth.business.controller; | ||||
|  | ||||
| import com.yj.earth.common.util.ApiResponse; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import jakarta.validation.Valid; | ||||
| import lombok.Data; | ||||
| import org.springframework.format.annotation.DateTimeFormat; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| 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 java.time.Duration; | ||||
| import java.time.LocalDateTime; | ||||
| import java.time.format.DateTimeFormatter; | ||||
| import java.util.regex.Matcher; | ||||
| import java.util.regex.Pattern; | ||||
|  | ||||
| @Tag(name = "战斗计算相关") | ||||
| @RestController | ||||
| @RequestMapping("/api/tactical") | ||||
| public class TacticalCalculationController { | ||||
|  | ||||
|     // 日期时间格式化器 | ||||
|     private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); | ||||
|  | ||||
|     @PostMapping("/meet") | ||||
|     @Operation(summary = "计算相遇结果") | ||||
|     public ApiResponse calculateMeet(@Valid @RequestBody MeetInputDTO input) { | ||||
|         MeetResultDTO result = new MeetResultDTO(); | ||||
|  | ||||
|         // 计算相遇时间(小时) | ||||
|         double meetTimeHours; | ||||
|         if (input.isSameDirection()) { | ||||
|             // 同向而行:时间 = 距离 / (速度差) | ||||
|             double speedDiff = Math.abs(input.getSpeed1() - input.getSpeed2()); | ||||
|             if (speedDiff <= 0) { | ||||
|                 throw new IllegalArgumentException("同向而行时,速度不能相等或后方速度小于前方"); | ||||
|             } | ||||
|             meetTimeHours = input.getInitialDistance() / speedDiff; | ||||
|         } else { | ||||
|             // 相向而行:时间 = 距离 / (速度和) | ||||
|             meetTimeHours = input.getInitialDistance() / (input.getSpeed1() + input.getSpeed2()); | ||||
|         } | ||||
|  | ||||
|         // 格式化时间结果 | ||||
|         result.setMeetTime(formatDuration(Duration.ofHours((long) meetTimeHours) | ||||
|                 .plusMinutes((long) ((meetTimeHours % 1) * 60)))); | ||||
|  | ||||
|         // 计算相遇点距离 | ||||
|         result.setMeetDistanceFrom1(input.getSpeed1() * meetTimeHours); | ||||
|         result.setMeetDistanceFrom2(input.getSpeed2() * meetTimeHours); | ||||
|  | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/pursuit") | ||||
|     @Operation(summary = "计算追击结果") | ||||
|     public ApiResponse calculatePursuit(@Valid @RequestBody PursuitInputDTO input) { | ||||
|         PursuitResultDTO result = new PursuitResultDTO(); | ||||
|  | ||||
|         // 检查速度合理性 | ||||
|         if (input.getPursuerSpeed() <= input.getTargetSpeed()) { | ||||
|             throw new IllegalArgumentException("追击速度必须大于目标速度"); | ||||
|         } | ||||
|  | ||||
|         // 计算追击时间(小时) | ||||
|         double pursuitTimeHours = input.getDistance() / (input.getPursuerSpeed() - input.getTargetSpeed()); | ||||
|         result.setPursuitTime(formatDuration(Duration.ofHours((long) pursuitTimeHours) | ||||
|                 .plusMinutes((long) ((pursuitTimeHours % 1) * 60)))); | ||||
|  | ||||
|         // 计算追击距离 | ||||
|         result.setPursuitDistance(input.getPursuerSpeed() * pursuitTimeHours); | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/formation/person") | ||||
|     @Operation(summary = "计算人员队列长度") | ||||
|     public ApiResponse calculatePersonFormation(@Valid @RequestBody PersonFormationInputDTO input) { | ||||
|         FormationLengthResultDTO result = new FormationLengthResultDTO(); | ||||
|  | ||||
|         // 人员队列长度 = (人数 - 1) * 间距 | ||||
|         if (input.getPersonCount() <= 1) { | ||||
|             result.setTotalLength(0.0); | ||||
|         } else { | ||||
|             result.setTotalLength((input.getPersonCount() - 1) * input.getDistanceBetween()); | ||||
|         } | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/formation/vehicle") | ||||
|     @Operation(summary = "计算车辆队列长度") | ||||
|     public ApiResponse calculateVehicleFormation(@Valid @RequestBody VehicleFormationInputDTO input) { | ||||
|         FormationLengthResultDTO result = new FormationLengthResultDTO(); | ||||
|  | ||||
|         // 车辆队列长度 = (单辆车长 * 数量) + (间距 * (数量 - 1)) | ||||
|         if (input.getVehicleCount() <= 0) { | ||||
|             result.setTotalLength(0.0); | ||||
|         } else if (input.getVehicleCount() == 1) { | ||||
|             result.setTotalLength(input.getVehicleLength()); | ||||
|         } else { | ||||
|             result.setTotalLength((input.getVehicleLength() * input.getVehicleCount()) + | ||||
|                     (input.getDistanceBetween() * (input.getVehicleCount() - 1))); | ||||
|         } | ||||
|  | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/formation/gun") | ||||
|     @Operation(summary = "计算火炮队列长度") | ||||
|     public ApiResponse calculateGunFormation(@Valid @RequestBody GunFormationInputDTO input) { | ||||
|         FormationLengthResultDTO result = new FormationLengthResultDTO(); | ||||
|  | ||||
|         // 火炮本身长度 | ||||
|         double gunTotalLength = input.getGunLength() * input.getGunCount(); | ||||
|         // 牵引车辆长度 | ||||
|         double vehicleTotalLength = input.getVehicleLength() * input.getVehicleCount(); | ||||
|         // 车辆间距 | ||||
|         double vehicleDistance = input.getVehicleDistance() * (input.getVehicleCount() - 1); | ||||
|  | ||||
|         // 总长度 | ||||
|         result.setTotalLength(gunTotalLength + vehicleTotalLength + vehicleDistance); | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/march/time") | ||||
|     @Operation(summary = "计算行军时间") | ||||
|     public ApiResponse calculateMarchTime(@Valid @RequestBody MarchTimeInputDTO input) { | ||||
|         MarchTimeResultDTO result = new MarchTimeResultDTO(); | ||||
|  | ||||
|         // 总行军距离 = 行军距离 + 部队长度(需要完全通过) | ||||
|         double totalDistance = input.getMarchDistance() + input.getFormationLength(); | ||||
|  | ||||
|         // 行军时间(小时)= 总距离 / 速度 + 等待时间 | ||||
|         double marchTimeHours = (totalDistance / input.getSpeed()) + input.getWaitHours(); | ||||
|  | ||||
|         // 格式化行军时间 | ||||
|         result.setTotalMarchTime(formatDuration(Duration.ofHours((long) marchTimeHours) | ||||
|                 .plusMinutes((long) ((marchTimeHours % 1) * 60)))); | ||||
|  | ||||
|         // 计算出发时间(如果提供了要求抵达时间) | ||||
|         if (input.getRequiredArriveTime() != null) { | ||||
|             long hours = (long) marchTimeHours; | ||||
|             long minutes = (long) ((marchTimeHours - hours) * 60); | ||||
|             LocalDateTime departTime = input.getRequiredArriveTime() | ||||
|                     .minusHours(hours) | ||||
|                     .minusMinutes(minutes); | ||||
|             result.setDepartureTime(departTime.format(DATE_TIME_FORMATTER)); | ||||
|         } | ||||
|  | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/material/consumption") | ||||
|     @Operation(summary = "计算物资消耗") | ||||
|     public ApiResponse calculateMaterialConsumption(@Valid @RequestBody MaterialConsumptionInputDTO input) { | ||||
|         MaterialConsumptionResultDTO result = new MaterialConsumptionResultDTO(); | ||||
|  | ||||
|         switch (input.getMaterialType()) { | ||||
|             case AMMUNITION: | ||||
|                 // 弹药消耗 = 单位基数 * 装备数量 * 时间 | ||||
|                 result.setTotalConsumption(input.getBaseConsumption() * input.getEquipmentCount() * input.getHours()); | ||||
|                 break; | ||||
|             case OIL: | ||||
|                 // 油料消耗 = 单位时间耗油量 * 装备数量 * 时间 | ||||
|                 result.setTotalConsumption(input.getBaseConsumption() * input.getEquipmentCount() * input.getHours()); | ||||
|                 break; | ||||
|             case WEAPON: | ||||
|                 // 武器需求 = 总需求 / (单装备配备数) | ||||
|                 result.setTotalConsumption(Math.ceil(input.getTotalRequirement() / input.getBaseConsumption())); | ||||
|                 break; | ||||
|         } | ||||
|  | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/unit/formation") | ||||
|     @Operation(summary = "计算部队编成") | ||||
|     public ApiResponse calculateUnitFormation(@Valid @RequestBody UnitFormationInputDTO input) { | ||||
|         UnitFormationResultDTO result = new UnitFormationResultDTO(); | ||||
|  | ||||
|         // 计算编成数量 | ||||
|         result.setFormationCount(Math.ceil(input.getTotalStrength() / input.getUnitStrength())); | ||||
|  | ||||
|         // 计算最后一个编成的兵力 | ||||
|         double lastFormationStrength = input.getTotalStrength() % input.getUnitStrength(); | ||||
|         if (lastFormationStrength <= 0) { | ||||
|             lastFormationStrength = input.getUnitStrength(); | ||||
|         } | ||||
|         result.setLastFormationStrength(lastFormationStrength); | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/special/crossRiver") | ||||
|     @Operation(summary = "计算渡河时间") | ||||
|     public ApiResponse calculateCrossRiverTime(@Valid @RequestBody CrossRiverInputDTO input) { | ||||
|         SpecialTimeResultDTO result = new SpecialTimeResultDTO(); | ||||
|  | ||||
|         // 计算批次数量 | ||||
|         double batchCount = Math.ceil(input.getPersonCount() / input.getBatchCapacity()); | ||||
|  | ||||
|         // 总时间 = 批次 * 单批次时间 + 准备时间 | ||||
|         double totalHours = (batchCount * input.getBatchHours()) + input.getPrepareHours(); | ||||
|  | ||||
|         // 格式化结果 | ||||
|         result.setTotalTime(formatDuration(Duration.ofHours((long) totalHours) | ||||
|                 .plusMinutes((long) ((totalHours % 1) * 60)))); | ||||
|  | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     @PostMapping("/loss/rate") | ||||
|     @Operation(summary = "计算损失率") | ||||
|     public ApiResponse calculateLossRate(@Valid @RequestBody LossRateInputDTO input) { | ||||
|         LossRateResultDTO result = new LossRateResultDTO(); | ||||
|  | ||||
|         if (input.getTotalStrength() <= 0) { | ||||
|             throw new IllegalArgumentException("总兵力必须大于0"); | ||||
|         } | ||||
|  | ||||
|         // 计算损失率 | ||||
|         result.setLossRate((input.getLostStrength() / input.getTotalStrength()) * 100); | ||||
|  | ||||
|         // 计算剩余兵力 | ||||
|         result.setRemainingStrength(input.getTotalStrength() - input.getLostStrength()); | ||||
|         return ApiResponse.success(result); | ||||
|     } | ||||
|  | ||||
|     // 格式化Duration为"X天X时X分X秒" | ||||
|     private String formatDuration(Duration duration) { | ||||
|         long days = duration.toDays(); | ||||
|         long hours = duration.toHours() % 24; | ||||
|         long minutes = duration.toMinutes() % 60; | ||||
|         long seconds = duration.getSeconds() % 60; | ||||
|  | ||||
|         return String.format("%d天%d时%d分%d秒", days, hours, minutes, seconds); | ||||
|     } | ||||
|  | ||||
|     // 解析时间字符串为Duration | ||||
|     private Duration parseTimeString(String timeStr) { | ||||
|         Pattern pattern = Pattern.compile("(?<d>\\d+)天(?<h>\\d+)时(?<m>\\d+)分(?<s>\\d+)秒"); | ||||
|         Matcher matcher = pattern.matcher(timeStr); | ||||
|  | ||||
|         if (matcher.matches()) { | ||||
|             int days = Integer.parseInt(matcher.group("d")); | ||||
|             int hours = Integer.parseInt(matcher.group("h")); | ||||
|             int minutes = Integer.parseInt(matcher.group("m")); | ||||
|             int seconds = Integer.parseInt(matcher.group("s")); | ||||
|  | ||||
|             return Duration.ofDays(days) | ||||
|                     .plusHours(hours) | ||||
|                     .plusMinutes(minutes) | ||||
|                     .plusSeconds(seconds); | ||||
|         } | ||||
|         return Duration.ZERO; | ||||
|     } | ||||
|  | ||||
|  | ||||
|     @Data | ||||
|     public static class MeetInputDTO { | ||||
|         @Schema(description = "初始距离") | ||||
|         private double initialDistance; | ||||
|         @Schema(description = "速度1") | ||||
|         private double speed1; | ||||
|         @Schema(description = "速度2") | ||||
|         private double speed2; | ||||
|         @Schema(description = "是否同向") | ||||
|         private boolean isSameDirection; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class MeetResultDTO { | ||||
|         @Schema(description = "相遇时间") | ||||
|         private String meetTime; | ||||
|         @Schema(description = "距离1的距离") | ||||
|         private double meetDistanceFrom1; | ||||
|         @Schema(description = "距离2的距离") | ||||
|         private double meetDistanceFrom2; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class PursuitInputDTO { | ||||
|         @Schema(description = "初始距离") | ||||
|         private double distance; | ||||
|         @Schema(description = "追击者速度") | ||||
|         private double pursuerSpeed; | ||||
|         @Schema(description = "目标速度") | ||||
|         private double targetSpeed; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class PursuitResultDTO { | ||||
|         @Schema(description = "追击时间") | ||||
|         private String pursuitTime; | ||||
|         @Schema(description = "追击距离") | ||||
|         private double pursuitDistance; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class PersonFormationInputDTO { | ||||
|         @Schema(description = "人数") | ||||
|         private int personCount; | ||||
|         @Schema(description = "人间距") | ||||
|         private double distanceBetween; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class VehicleFormationInputDTO { | ||||
|         @Schema(description = "车辆数量") | ||||
|         private int vehicleCount; | ||||
|         @Schema(description = "车辆长") | ||||
|         private double vehicleLength; | ||||
|         @Schema(description = "车辆距") | ||||
|         private double distanceBetween; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class GunFormationInputDTO { | ||||
|         @Schema(description = "火炮数量") | ||||
|         private int gunCount; | ||||
|         @Schema(description = "单门炮长") | ||||
|         private double gunLength; | ||||
|         @Schema(description = "牵引车辆数量") | ||||
|         private int vehicleCount; | ||||
|         @Schema(description = "单辆车长") | ||||
|         private double vehicleLength; | ||||
|         @Schema(description = "车距") | ||||
|         private double vehicleDistance; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class FormationLengthResultDTO { | ||||
|         @Schema(description = "总长度") | ||||
|         private double totalLength; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class MarchTimeInputDTO { | ||||
|         @Schema(description = "行军距离") | ||||
|         private double marchDistance; | ||||
|         @Schema(description = "部队长度") | ||||
|         private double formationLength; | ||||
|         @Schema(description = "速度") | ||||
|         private double speed; | ||||
|         @Schema(description = "等待时间(小时)") | ||||
|         private double waitHours; | ||||
|         @Schema(description = "要求抵达时间") | ||||
|         @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") | ||||
|         private LocalDateTime requiredArriveTime; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class MarchTimeResultDTO { | ||||
|         @Schema(description = "总行军时间") | ||||
|         private String totalMarchTime; | ||||
|         @Schema(description = "出发时间") | ||||
|         private String departureTime; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class MaterialConsumptionInputDTO { | ||||
|         @Schema(description = "物资类型") | ||||
|         private MaterialType materialType; | ||||
|         @Schema(description = "单位消耗/配备数") | ||||
|         private double baseConsumption; | ||||
|         @Schema(description = "装备数量") | ||||
|         private int equipmentCount; | ||||
|         @Schema(description = "时间(小时)") | ||||
|         private double hours; | ||||
|         @Schema(description = "总需求") | ||||
|         private double totalRequirement; | ||||
|  | ||||
|         public enum MaterialType { | ||||
|             AMMUNITION, OIL, WEAPON | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class MaterialConsumptionResultDTO { | ||||
|         @Schema(description = "总消耗量/需求量") | ||||
|         private double totalConsumption; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class UnitFormationInputDTO { | ||||
|         @Schema(description = "总兵力") | ||||
|         private double totalStrength; | ||||
|         @Schema(description = "单位兵力") | ||||
|         private double unitStrength; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class UnitFormationResultDTO { | ||||
|         @Schema(description = "编成数量") | ||||
|         private double formationCount; | ||||
|         @Schema(description = "最后一个编成的兵力") | ||||
|         private double lastFormationStrength; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class CrossRiverInputDTO { | ||||
|         @Schema(description = "总人数") | ||||
|         private int personCount; | ||||
|         @Schema(description = "单批次容量") | ||||
|         private int batchCapacity; | ||||
|         @Schema(description = "单批次时间(小时)") | ||||
|         private double batchHours; | ||||
|         @Schema(description = "准备时间(小时)") | ||||
|         private double prepareHours; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class SpecialTimeResultDTO { | ||||
|         @Schema(description = "总时间") | ||||
|         private String totalTime; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class LossRateInputDTO { | ||||
|         @Schema(description = "总兵力") | ||||
|         private double totalStrength; | ||||
|         @Schema(description = "损失兵力") | ||||
|         private double lostStrength; | ||||
|     } | ||||
|  | ||||
|     @Data | ||||
|     public static class LossRateResultDTO { | ||||
|         @Schema(description = "损失率(百分比)") | ||||
|         private double lossRate; | ||||
|         @Schema(description = "剩余兵力") | ||||
|         private double remainingStrength; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user
	 ZZX9599
					ZZX9599