diff --git a/lib/proj.db b/lib/proj.db new file mode 100644 index 0000000..9f42388 Binary files /dev/null and b/lib/proj.db differ diff --git a/license/yjearth.lic b/license/yjearth.lic new file mode 100644 index 0000000..deff4c5 --- /dev/null +++ b/license/yjearth.lic @@ -0,0 +1 @@ +41b/ujShRZRf9Aa433FD3uyIZuxWSSqXWXlc2dyQfJ75ED0HNbadcdsPF5CaMuJ6K2c3U/eBcWiXXw090/O7M5mJze/MavZ4dhk4dZIukMik2Jrufq9vkpQW+3/LWMFurfFWnFDZIf7Ptuoj4BvuX9h/qJ3oYUDCj14JFAR6ge7ZUtqT1yBvSl/eVexWpCiXfpNSm79whbkkpQjgjWts5/bgQ9cOYdBaVOBJnQkRxNzb4fKh3jkuyEADR+VZg2sRnjLJChAg7DYvIou8Zy16Ag== diff --git a/pom.xml b/pom.xml index 8c30725..ccf03c3 100644 --- a/pom.xml +++ b/pom.xml @@ -175,6 +175,12 @@ + + + org.yaml + snakeyaml + + org.gdal diff --git a/src/main/java/com/yj/earth/auth/AuthGenerator.java b/src/main/java/com/yj/earth/auth/AuthGenerator.java index 3f35815..29b537e 100644 --- a/src/main/java/com/yj/earth/auth/AuthGenerator.java +++ b/src/main/java/com/yj/earth/auth/AuthGenerator.java @@ -72,6 +72,6 @@ public class AuthGenerator { } public static void main(String[] args) { - System.out.println(generateAuth("标准版", 1000, 30, "25F429FDA965007B72BB7A6B2C03535A")); + System.out.println(generateAuth("标准版", 1000, 365, "8661A5D7040288C20E17A1D117E20045")); } } diff --git a/src/main/java/com/yj/earth/business/controller/AuthController.java b/src/main/java/com/yj/earth/business/controller/AuthController.java index 5aa213c..21f27c0 100644 --- a/src/main/java/com/yj/earth/business/controller/AuthController.java +++ b/src/main/java/com/yj/earth/business/controller/AuthController.java @@ -21,7 +21,7 @@ import java.nio.file.Paths; @RequestMapping("/auth") public class AuthController { - // 授权文件存储路径、项目根目录下的license目录 + // 授权文件存储路径、项目根目录下的 license 目录 private static final String AUTH_FILE_PATH = "license/yjearth.lic"; @GetMapping("/info") diff --git a/src/main/java/com/yj/earth/business/controller/GdalController.java b/src/main/java/com/yj/earth/business/controller/GdalController.java new file mode 100644 index 0000000..ac41022 --- /dev/null +++ b/src/main/java/com/yj/earth/business/controller/GdalController.java @@ -0,0 +1,47 @@ +package com.yj.earth.business.controller; + +import com.yj.earth.common.util.GdalUtil; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.zip.GZIPOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +@Tag(name = "矢量数据管理") +@RestController +@RequestMapping("/gdal") +public class GdalController { + + @Operation(summary = "导入矢量数据") + @PostMapping("/import") + public void importDataStreamGzip(@Parameter(description = "矢量文件路径", required = true) @RequestParam("path") String path, HttpServletResponse response) throws IOException { + + // 解析矢量文件、得到JSON数据 + String jsonData = GdalUtil.readVectorToJson(path); + + // 设置响应头 + String filename = URLEncoder.encode("data.gz", StandardCharsets.UTF_8); + response.setContentType("application/gzip"); + response.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\""); + response.setCharacterEncoding("UTF-8"); + + // 使用GZIP压缩并输出 + try (OutputStream outputStream = response.getOutputStream(); + GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) { + gzipOutputStream.write(jsonData.getBytes(StandardCharsets.UTF_8)); + gzipOutputStream.finish(); + } + } +} diff --git a/src/main/java/com/yj/earth/business/controller/GraphHopperController.java b/src/main/java/com/yj/earth/business/controller/GraphHopperController.java index 0124e58..b403055 100644 --- a/src/main/java/com/yj/earth/business/controller/GraphHopperController.java +++ b/src/main/java/com/yj/earth/business/controller/GraphHopperController.java @@ -10,7 +10,9 @@ 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.domain.WebSource; import com.yj.earth.business.service.FileInfoService; +import com.yj.earth.business.service.WebSourceService; import com.yj.earth.common.config.GraphHopperProperties; import com.yj.earth.common.util.ApiResponse; import com.yj.earth.model.Point; @@ -40,6 +42,8 @@ import java.util.concurrent.atomic.AtomicBoolean; @RestController @RequestMapping("/graphhopper") public class GraphHopperController { + @Resource + private WebSourceService webSourceService; @Resource private FileInfoService fileInfoService; @Resource @@ -61,6 +65,15 @@ public class GraphHopperController { return ApiResponse.success(fileInfoService.list(queryWrapper)); } + @Operation(summary = "获取地图列表(网页版)") + @CheckAuth + @GetMapping("/web/list") + public ApiResponse webList() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(WebSource::getType, "pbf"); + return ApiResponse.success(webSourceService.list(queryWrapper)); + } + @Operation(summary = "加载地图数据") @PostMapping("/loadMap") @CheckAuth @@ -120,6 +133,57 @@ public class GraphHopperController { return ApiResponse.success(null); } + @Operation(summary = "加载地图数据(网页版)") + @PostMapping("/web/loadMap") + @CheckAuth + public ApiResponse webLoadMap(@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 (isLoading.get()) { + return ApiResponse.failure("地图正在加载中、请稍后查询状态"); + } + + // 标记加载状态 + isLoading.set(true); + isLoaded.set(false); + + // 异步执行: 删除旧数据 → 创建新实例 → 加载新地图 + new Thread(() -> { + GraphHopper newHopper = null; + try { + // 关键步骤1: 彻底删除旧地图数据目录 + deleteOldGraphDir(); + // 关键步骤2: 创建全新的GraphHopper实例 + newHopper = createNewGraphHopperInstance(path); + // 关键步骤3: 加载新地图 + newHopper.importOrLoad(); + // 关键步骤4: 加载成功 → 替换当前实例 + 更新状态 + currentHopper = newHopper; + isLoaded.set(true); + log.info("地图加载成功"); + } catch (Exception e) { + // 加载失败 → 清理新实例资源 + if (newHopper != null) { + newHopper.close(); + } + isLoaded.set(false); + e.printStackTrace(); + log.error("地图加载失败: " + e.getMessage()); + + } finally { + // 无论成功/失败、释放加载锁 + isLoading.set(false); + } + }).start(); + return ApiResponse.success(null); + } + @Operation(summary = "路径规划") @PostMapping("/route") @CheckAuth diff --git a/src/main/java/com/yj/earth/business/controller/IconLibraryController.java b/src/main/java/com/yj/earth/business/controller/IconLibraryController.java index e350475..b19e50f 100644 --- a/src/main/java/com/yj/earth/business/controller/IconLibraryController.java +++ b/src/main/java/com/yj/earth/business/controller/IconLibraryController.java @@ -265,6 +265,7 @@ public class IconLibraryController { icon.icon_name as iconName, icon.icon_type as iconType, icon.data, + icon.view, icon.created_at as createdAt, icon.updated_at as updatedAt, diff --git a/src/main/java/com/yj/earth/business/controller/ModelLibraryController.java b/src/main/java/com/yj/earth/business/controller/ModelLibraryController.java index 71d3c97..70b63e6 100644 --- a/src/main/java/com/yj/earth/business/controller/ModelLibraryController.java +++ b/src/main/java/com/yj/earth/business/controller/ModelLibraryController.java @@ -9,18 +9,24 @@ import com.yj.earth.business.domain.ModelType; import com.yj.earth.business.service.FileInfoService; import com.yj.earth.business.service.ModelLibraryService; import com.yj.earth.common.util.ApiResponse; +import com.yj.earth.common.util.FileCommonUtil; import com.yj.earth.common.util.SQLiteUtil; import com.yj.earth.dto.militaryLibrary.DragMilitaryTypeDto; import com.yj.earth.dto.modelLibrary.AddModelTypeDto; import com.yj.earth.dto.modelLibrary.CreateModelLibraryDto; import com.yj.earth.dto.modelLibrary.DragModelTypeDto; import com.yj.earth.dto.modelLibrary.UpdateModelTypeNameDto; +import com.yj.earth.vo.ModelDataVo; import com.yj.earth.vo.ModelTypeVo; import com.yj.earth.vo.ModelVo; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.jetbrains.annotations.NotNull; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -28,11 +34,11 @@ import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.File; import java.io.IOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.sql.SQLException; import java.time.LocalDateTime; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; @Tag(name = "模型库管理") @@ -157,37 +163,6 @@ public class ModelLibraryController { return ApiResponse.success(modelTypeList()); } - @Operation(summary = "拖动模型类型树") - @PostMapping("/dragModelType") - public ApiResponse dragMilitaryType(@RequestBody DragModelTypeDto dragModelTypeDto) throws SQLException { - String militaryPath = getModelLibrary(); - if (militaryPath == null) { - return ApiResponse.failure("请先创建或导入模型库"); - } - - // 动态构建SQL和参数 - List updateFields = new ArrayList<>(); - List params = new ArrayList<>(); - - // 判断 parentId 是否存在 - if (dragModelTypeDto.getParentId() != null) { - updateFields.add("parent_id = ?"); - params.add(dragModelTypeDto.getParentId()); - } - - // 判断 treeIndex 是否存在 - if (dragModelTypeDto.getTreeIndex() != null) { - updateFields.add("tree_index = ?"); - params.add(dragModelTypeDto.getTreeIndex()); - } - - // 构建完整 SQL - String sql = "UPDATE model_type SET " + String.join(", ", updateFields) + " WHERE id = ?"; - params.add(dragModelTypeDto.getId()); - SQLiteUtil.executeUpdate(militaryPath, sql, params); - return ApiResponse.success(null); - } - @Operation(summary = "添加模型文件") @PostMapping("/addModelFile") public ApiResponse addModelFile(@RequestParam("files") MultipartFile[] files, @Parameter(description = "模型类型ID") @RequestParam("modelTypeId") String modelTypeId) throws IOException, SQLException { @@ -212,16 +187,16 @@ public class ModelLibraryController { // 构建插入SQL String sql = "INSERT INTO model " + - "(id, model_type_id, model_name, model_type, data, created_at) " + + "(id, model_type_id, model_name, model_type, model_data, created_at) " + "VALUES (?, ?, ?, ?, ?, ?)"; - String url = fileInfoService.uploadWithPreview(file); List params = new ArrayList<>(); - params.add(UUID.fastUUID().toString(true)); + String modelId = UUID.fastUUID().toString(true); + params.add(modelId); params.add(modelTypeId); params.add(fileNameWithoutSuffix); params.add(fileSuffix); - params.add(url); + params.add(file.getBytes()); params.add(LocalDateTime.now()); // 执行插入操作 @@ -231,6 +206,72 @@ public class ModelLibraryController { return ApiResponse.success(null); } + @Operation(summary = "获取模型数据") + @GetMapping("/data/{type}/{modelId}/{fileSuffix}") + public ResponseEntity modelData( + @Parameter(description = "数据类型") @PathVariable("type") String type, + @Parameter(description = "模型ID") @PathVariable("modelId") String modelId, + @Parameter(description = "模型类型") @PathVariable(value = "fileSuffix") String fileSuffix) { + try { + // 获取最新的模型库 + String modelPath = getModelLibrary(); + if (modelPath == null) { + return ResponseEntity.notFound().build(); + } + + List params = new ArrayList<>(); + params.add(modelId); + String sql = null; + if (type.equals("model")) { + sql = "SELECT model_name, model_data FROM model WHERE id = ?"; + } else { + sql = "SELECT model_name, poster_data FROM model WHERE id = ?"; + } + // 查询模型数据 + ModelDataVo modelDataVo = SQLiteUtil.queryForObject( + modelPath, + sql, + params, + ModelDataVo.class + ); + + if (modelDataVo == null) { + return ResponseEntity.notFound().build(); + } + + // 构建原始文件名(使用从数据库查询的名称更合理) + String originalFileName = modelDataVo.getModelName() + "." + fileSuffix; + + // 对文件名进行URL编码处理、解决非ASCII字符问题 + String encodedFileName = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name()); + + // 根据图片后缀获取对应的MediaType + MediaType contentType = FileCommonUtil.getImageMediaType(fileSuffix); + + // 设置响应头、使用 inline 确保可以预览 + if (type.equals("model")) { + return ResponseEntity.ok() + .contentType(contentType) + .header(HttpHeaders.CONTENT_DISPOSITION, + "inline; filename=\"" + encodedFileName + "\"") + .body(modelDataVo.getModelData()); + } else { + return ResponseEntity.ok() + .contentType(contentType) + .header(HttpHeaders.CONTENT_DISPOSITION, + "inline; filename=\"" + encodedFileName + "\"") + .body(modelDataVo.getPosterData()); + } + } catch (SQLException e) { + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation(summary = "根据模型类型查看模型列表") @PostMapping("/modelList") public ApiResponse modelList(@Parameter(description = "模型类型ID") @RequestParam("modelTypeId") String modelTypeId) throws SQLException, IllegalAccessException, InstantiationException { @@ -239,7 +280,18 @@ public class ModelLibraryController { if (modelPath == null) { return ApiResponse.failure("请先创建或导入模型库"); } - // 多表联查、查询所有的模型 + + // 获取分类ID及其所有子分类ID的列表 + List typeIdList = getModelTypeIdsWithChildren(modelTypeId); + if (typeIdList.isEmpty()) { + return ApiResponse.success(null); + } + + String idsWithQuotes = typeIdList.stream() + .map(id -> "'" + id + "'") + .collect(Collectors.joining(",")); + + // 多表联查、查询所有指定分类及其子分类下的模型 String sql = """ SELECT model.id, @@ -247,24 +299,34 @@ public class ModelLibraryController { model.model_name as modelName, model.model_type as modelType, model.poster_type as posterType, - model.poster, - model.data, - model.view, model.created_at as createdAt, model.updated_at as updatedAt, - model_type.name as modelTypeName from - model JOIN model_type ON model.model_type_id = model_type.id - WHERE model.model_type_id = ? - """; + model_type.name as modelTypeName + FROM model + JOIN model_type ON model.model_type_id = model_type.id + WHERE model.model_type_id IN (?) + """.replace("?", idsWithQuotes); + + // 使用所有分类ID作为查询参数 List params = new ArrayList<>(); - params.add(modelTypeId); - List modelVos = SQLiteUtil.queryForList(modelPath, sql, params, ModelVo.class); + List modelVos = SQLiteUtil.queryForList(modelPath, sql, null, ModelVo.class); + // 循环遍历数据 + for (ModelVo modelVo : modelVos) { + if (modelVo.getModelType() != null) { + modelVo.setModelDataUrl("/modelLibrary/data/model/" + modelVo.getId() + "/" + modelVo.getModelType()); + } + if (modelVo.getPosterType() != null) { + modelVo.setPosterDataUrl("/modelLibrary/data/poster/" + modelVo.getId() + "/" + modelVo.getPosterType()); + } + } return ApiResponse.success(modelVos); } @Operation(summary = "更新模型信息") @PostMapping("/uploadModelInfo") - public ApiResponse uploadModelInfo(@Parameter(description = "模型封面文件") @RequestParam(value = "file", required = false) MultipartFile file, @Parameter(description = "模型ID") @RequestParam("modelId") String modelId, @Parameter(description = "模型名称") @RequestParam(value = "modelName", required = false) String modelName) throws IOException, SQLException { + public ApiResponse uploadModelInfo(@Parameter(description = "模型封面文件") @RequestParam(value = "file", required = false) MultipartFile file, + @Parameter(description = "模型ID") @RequestParam("modelId") String modelId, + @Parameter(description = "模型名称") @RequestParam(value = "modelName", required = false) String modelName) throws IOException, SQLException { // 获取最新的模型库路径 String modelPath = getModelLibrary(); @@ -279,10 +341,9 @@ public class ModelLibraryController { // 处理封面文件 if (file != null && !file.isEmpty()) { String fileSuffix = FileUtil.extName(file.getOriginalFilename()); - String url = fileInfoService.uploadWithPreview(file); - sql.append(", poster_type = ?, poster = ? "); + sql.append(", poster_type = ?, poster_data = ? "); params.add(fileSuffix); - params.add(url); + params.add(file.getBytes()); } // 处理模型名称 @@ -344,7 +405,7 @@ public class ModelLibraryController { tree_index as treeIndex, created_at as createdAt, updated_at as updatedAt FROM model_type ORDER BY tree_index ASC """; - // 查询所有模型类型 + // 查询所有模型的类型 List modelTypes = SQLiteUtil.queryForList(modelPath, sql, null, ModelType.class); // 转换为树形结构 return buildModelTypeTree(modelTypes); @@ -354,11 +415,14 @@ public class ModelLibraryController { List treeNodes = modelTypes.stream() .map(modelType -> new ModelTypeVo(modelType)) .collect(Collectors.toList()); + // 构建节点ID到节点的映射 Map nodeMap = treeNodes.stream() .collect(Collectors.toMap(ModelTypeVo::getId, node -> node)); + // 根节点列表 List rootNodes = new ArrayList<>(); + // 为每个节点添加子节点 for (ModelTypeVo node : treeNodes) { String parentId = node.getParentId(); @@ -373,9 +437,29 @@ public class ModelLibraryController { } } } + + // 排序根节点 + rootNodes.sort(Comparator.comparingInt(ModelTypeVo::getTreeIndex)); + + // 递归排序所有子节点 + sortChildren(rootNodes); + return rootNodes; } + // 递归排序所有子节点 + private void sortChildren(List nodes) { + if (nodes == null || nodes.isEmpty()) { + return; + } + // 排序当前层级节点 + nodes.sort(Comparator.comparingInt(ModelTypeVo::getTreeIndex)); + // 递归排序下一层级 + for (ModelTypeVo node : nodes) { + sortChildren(node.getChildren()); + } + } + private String getModelLibrary() { // 获取启用的模型库 LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); @@ -417,4 +501,69 @@ public class ModelLibraryController { modelLibraryService.save(newModel); } } + + /** + * 根据模型分类ID、查询该分类ID及其所有子分类ID的列表(递归包含所有层级子分类) + */ + private List getModelTypeIdsWithChildren(String modelTypeId) throws SQLException, IllegalAccessException, InstantiationException { + // 结果列表初始化 + List typeIdList = new ArrayList<>(); + + // 校验入参:分类ID不能为空 + if (StringUtils.isEmpty(modelTypeId)) { + return null; + } + + // 获取当前启用的模型库路径 + String modelPath = getModelLibrary(); + if (modelPath == null) { + return null; + } + + // 校验目标分类ID是否存在(避免无效ID导致递归无结果) + if (!isModelTypeExist(modelPath, modelTypeId)) { + return null; + } + + // 递归查询: 收集当前分类及所有子分类ID + recursiveGetChildIds(modelPath, modelTypeId, typeIdList); + + return typeIdList; + } + + /** + * 递归查询子分类ID、并将结果存入列表 + */ + private void recursiveGetChildIds(String modelPath, String currentTypeId, List resultList) throws SQLException, IllegalAccessException, InstantiationException { + // 先将当前分类ID加入结果列表(确保包含自身) + resultList.add(currentTypeId); + + // 查询当前分类的「直接子分类」(按 tree_index 排序、保持树形结构原有顺序) + String childSql = "SELECT id FROM model_type WHERE parent_id = ? ORDER BY tree_index ASC"; + List childParams = new ArrayList<>(); + childParams.add(currentTypeId); + List childModelTypes = SQLiteUtil.queryForList( + modelPath, childSql, childParams, ModelType.class + ); + + // 若存在子分类、递归查询每个子分类的子分类 + if (childModelTypes != null && !childModelTypes.isEmpty()) { + for (ModelType childType : childModelTypes) { + recursiveGetChildIds(modelPath, childType.getId(), resultList); + } + } + } + + /** + * 校验模型分类ID是否存在于模型库中 + */ + private boolean isModelTypeExist(String modelPath, String typeId) + throws SQLException, IllegalAccessException, InstantiationException { + String checkSql = "SELECT id FROM model_type WHERE id = ?"; + List checkParams = new ArrayList<>(); + checkParams.add(typeId); + List existTypes = SQLiteUtil.queryForList(modelPath, checkSql, checkParams, ModelType.class); + // 若查询结果非空、说明分类存在 + return existTypes != null && !existTypes.isEmpty(); + } } diff --git a/src/main/java/com/yj/earth/business/controller/SystemController.java b/src/main/java/com/yj/earth/business/controller/SystemController.java new file mode 100644 index 0000000..0b5be09 --- /dev/null +++ b/src/main/java/com/yj/earth/business/controller/SystemController.java @@ -0,0 +1,120 @@ +package com.yj.earth.business.controller; + +import com.yj.earth.common.util.ApiResponse; +import com.yj.earth.dto.system.UpdateSystemServiceDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +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 org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.Yaml; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.HashMap; +import java.util.Map; + +@Tag(name = "系统服务管理") +@RestController +@RequestMapping("/systemService") +public class SystemController { + @Value("${server.host}") + private String serverHost; + @Value("${server.port}") + private Integer serverPort; + + // 配置文件路径(jar包所在目录的application.yml) + private static final String CONFIG_FILE_PATH = "application.yml"; + + // 重启脚本路径(app同级目录) + private static final String WINDOWS_REBOOT_SCRIPT = "../reboot.bat"; + private static final String LINUX_REBOOT_SCRIPT = "../reboot.sh"; + + @Operation(summary = "读取系统服务配置") + @PostMapping("/info") + public ApiResponse readSystemServiceConfig() { + Map map = Map.of("serverHost", serverHost, "serverPort", serverPort); + return ApiResponse.success(map); + } + + @Operation(summary = "修改系统服务配置并重启") + @PostMapping("/update") + public ApiResponse updateSystemService(@RequestBody UpdateSystemServiceDto updateSystemServiceDto) { + try { + // 读取并更新YAML配置 + Yaml yaml = new Yaml(); + File configFile = new File(CONFIG_FILE_PATH); + if (!configFile.exists()) { + return ApiResponse.failure("配置文件不存在"); + } + // 解析YAML内容 + Map configMap; + try (InputStream in = Files.newInputStream(Paths.get(CONFIG_FILE_PATH))) { + configMap = yaml.load(in); + if (configMap == null) { + configMap = new HashMap<>(); + } + } + // 更新配置 + Map serverMap = (Map) configMap.getOrDefault("server", new HashMap<>()); + if (updateSystemServiceDto.getServerHost() != null && !updateSystemServiceDto.getServerHost().isEmpty()) { + serverMap.put("host", updateSystemServiceDto.getServerHost()); + } + if (updateSystemServiceDto.getServerPort() != null) { + serverMap.put("port", updateSystemServiceDto.getServerPort()); + } + configMap.put("server", serverMap); + // 保存配置 + DumperOptions options = new DumperOptions(); + options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); + options.setPrettyFlow(true); + Yaml writeYaml = new Yaml(options); + try (FileWriter writer = new FileWriter(configFile, StandardCharsets.UTF_8)) { + writeYaml.dump(configMap, writer); + } + // 执行重启脚本 + executeRebootScript(); + return ApiResponse.success(null); + + } catch (IOException e) { + return ApiResponse.failure("更新配置失败:" + e.getMessage()); + } catch (Exception e) { + return ApiResponse.failure("操作失败:" + e.getMessage()); + } + } + + /** + * 执行重启脚本 + */ + private void executeRebootScript() throws IOException, InterruptedException { + String scriptPath; + ProcessBuilder processBuilder; + + // 判断操作系统类型、选择对应的脚本 + if (System.getProperty("os.name").toLowerCase().contains("win")) { + scriptPath = WINDOWS_REBOOT_SCRIPT; + // 检查Windows脚本是否存在 + if (!new File(scriptPath).exists()) { + throw new FileNotFoundException("Windows重启脚本不存在: " + scriptPath); + } + processBuilder = new ProcessBuilder(scriptPath); + } else { + scriptPath = LINUX_REBOOT_SCRIPT; + // 检查Linux脚本是否存在 + if (!new File(scriptPath).exists()) { + throw new FileNotFoundException("Linux重启脚本不存在: " + scriptPath); + } + // 给脚本添加执行权限 + new ProcessBuilder("chmod", "+x", scriptPath).start().waitFor(); + processBuilder = new ProcessBuilder(scriptPath); + } + + // 执行脚本(在新进程中运行、不阻塞当前请求) + processBuilder.start(); + } +} diff --git a/src/main/java/com/yj/earth/business/controller/WebSourceController.java b/src/main/java/com/yj/earth/business/controller/WebSourceController.java new file mode 100644 index 0000000..5205edd --- /dev/null +++ b/src/main/java/com/yj/earth/business/controller/WebSourceController.java @@ -0,0 +1,123 @@ +package com.yj.earth.business.controller; + +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.lang.UUID; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.yj.earth.annotation.RoleAccess; +import com.yj.earth.business.domain.WebSource; +import com.yj.earth.business.service.SourceService; +import com.yj.earth.business.service.WebSourceService; +import com.yj.earth.common.util.ApiResponse; +import com.yj.earth.dto.source.AddModelSourceDto; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.*; + +import javax.annotation.Resource; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@Tag(name = "网页版独有接口") +@RestController +@RequestMapping("/webSource") +public class WebSourceController { + @Value("${sync.folder}") + private String syncFolder; + + @Resource + private WebSourceService webSourceService; + @Resource + private SourceService sourceService; + private static final List SUPPORT_EXTENSIONS = Arrays.asList("clt", "mbtiles", "pak", "pbf"); + + @RoleAccess(roleNames = "管理员") + @Operation(summary = "同步数据") + @PostMapping("/sync") + public ApiResponse sync() { + try { + Path syncPath = Paths.get(syncFolder); + // 验证文件夹有效性 + if (!Files.exists(syncPath) || !Files.isDirectory(syncPath)) { + return ApiResponse.failure("同步文件夹不存在或不是一个有效的目录"); + } + // 循环处理所有支持的文件类型、减少重复代码 + for (String extension : SUPPORT_EXTENSIONS) { + List filePaths = getFilesByExtension(syncPath, extension); + // 批量处理同类型文件 + filePaths.forEach(this::saveFileIfNotExists); + } + return ApiResponse.success(null); + } catch (Exception e) { + return ApiResponse.failure("同步数据失败: " + e.getMessage()); + } + } + + /** + * 若文件路径不存在则保存到数据库 + */ + private void saveFileIfNotExists(String filePath) { + // 检查路径是否已存在 + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(WebSource::getPath, filePath); + if (webSourceService.count(queryWrapper) > 0) { + return; + } + + // 构建并保存 WebSource 对象 + WebSource webSource = new WebSource(); + webSource.setName(FileUtil.getName(filePath)); + webSource.setType(FileUtil.extName(filePath)); + webSource.setPath(filePath); + webSourceService.save(webSource); + + if (!webSource.getType().equals("pbf")) { + addModelSourceIfNotExists(filePath); + } + } + + /** + * 若文件路径不存在则保存到数据库 + */ + private void addModelSourceIfNotExists(String filePath) { + AddModelSourceDto addModelSourceDto = new AddModelSourceDto(); + addModelSourceDto.setSourcePath(filePath); + addModelSourceDto.setId(UUID.randomUUID().toString(true)); + sourceService.addModelSource(addModelSourceDto); + } + + @Operation(summary = "获取所有文件") + @GetMapping("/list") + public ApiResponse list(@RequestParam(required = false) @Parameter(description = "文件类型") String type) { + if (type != null) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(WebSource::getType, type); + return ApiResponse.success(webSourceService.list(queryWrapper)); + } + return ApiResponse.success(webSourceService.list()); + } + + /** + * 获取指定目录下特定扩展名的文件 + */ + private List getFilesByExtension(Path directory, String extension) throws IOException { + try (Stream stream = Files.list(directory)) { + return stream + .filter(Files::isRegularFile) + .filter(path -> { + String fileName = path.getFileName().toString(); + return fileName.toLowerCase().endsWith("." + extension.toLowerCase()); + }) + .map(Path::toAbsolutePath) + .map(Path::toString) + .collect(Collectors.toList()); + } + } +} diff --git a/src/main/java/com/yj/earth/business/domain/Model.java b/src/main/java/com/yj/earth/business/domain/Model.java index d6240c9..76a3dff 100644 --- a/src/main/java/com/yj/earth/business/domain/Model.java +++ b/src/main/java/com/yj/earth/business/domain/Model.java @@ -22,11 +22,13 @@ public class Model { @Schema(description = "海报类型") private String posterType; @Schema(description = "海报数据") - private String poster; + private byte[] posterData; + @Schema(description = "海报URL") + private String posterUrl; @Schema(description = "模型数据") - private String data; - @Schema(description = "模型视图") - private String view; + private byte[] modelData; + @Schema(description = "模型数据URL") + private String modelUrl; @Schema(description = "创建时间") private LocalDateTime createdAt; @Schema(description = "更新时间") diff --git a/src/main/java/com/yj/earth/business/domain/WebSource.java b/src/main/java/com/yj/earth/business/domain/WebSource.java new file mode 100644 index 0000000..9a7b9c1 --- /dev/null +++ b/src/main/java/com/yj/earth/business/domain/WebSource.java @@ -0,0 +1,43 @@ +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 com.baomidou.mybatisplus.annotation.TableName; + +import java.io.Serializable; +import java.time.LocalDateTime; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.experimental.Accessors; + +@Data +public class WebSource implements Serializable { + + private static final long serialVersionUID = 1L; + + @TableId(value = "id", type = IdType.ASSIGN_UUID) + @Schema(description = "主键") + private String id; + + @Schema(description = "资源名称") + private String name; + + @Schema(description = "资源路径") + private String path; + + @Schema(description = "资源类型") + private String type; + + @TableField(fill = FieldFill.INSERT) + @Schema(description = "创建时间") + private LocalDateTime createdAt; + + @TableField(fill = FieldFill.UPDATE) + @Schema(description = "更新时间") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/yj/earth/business/mapper/WebSourceMapper.java b/src/main/java/com/yj/earth/business/mapper/WebSourceMapper.java new file mode 100644 index 0000000..07cfa9f --- /dev/null +++ b/src/main/java/com/yj/earth/business/mapper/WebSourceMapper.java @@ -0,0 +1,18 @@ +package com.yj.earth.business.mapper; + +import com.yj.earth.business.domain.WebSource; +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +/** + *

+ * Mapper 接口 + *

+ * + * @author 周志雄 + * @since 2025-09-26 + */ +@Mapper +public interface WebSourceMapper extends BaseMapper { + +} diff --git a/src/main/java/com/yj/earth/business/service/WebSourceService.java b/src/main/java/com/yj/earth/business/service/WebSourceService.java new file mode 100644 index 0000000..89e9709 --- /dev/null +++ b/src/main/java/com/yj/earth/business/service/WebSourceService.java @@ -0,0 +1,8 @@ +package com.yj.earth.business.service; + +import com.yj.earth.business.domain.WebSource; +import com.baomidou.mybatisplus.extension.service.IService; + +public interface WebSourceService extends IService { + +} diff --git a/src/main/java/com/yj/earth/business/service/impl/WebSourceServiceImpl.java b/src/main/java/com/yj/earth/business/service/impl/WebSourceServiceImpl.java new file mode 100644 index 0000000..1c04f1f --- /dev/null +++ b/src/main/java/com/yj/earth/business/service/impl/WebSourceServiceImpl.java @@ -0,0 +1,20 @@ +package com.yj.earth.business.service.impl; + +import com.yj.earth.business.domain.WebSource; +import com.yj.earth.business.mapper.WebSourceMapper; +import com.yj.earth.business.service.WebSourceService; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; + +/** + *

+ * 服务实现类 + *

+ * + * @author 周志雄 + * @since 2025-09-26 + */ +@Service +public class WebSourceServiceImpl extends ServiceImpl implements WebSourceService { + +} diff --git a/src/main/java/com/yj/earth/common/config/SaTokenConfig.java b/src/main/java/com/yj/earth/common/config/SaTokenConfig.java index 49fdb47..6861ceb 100644 --- a/src/main/java/com/yj/earth/common/config/SaTokenConfig.java +++ b/src/main/java/com/yj/earth/common/config/SaTokenConfig.java @@ -27,6 +27,8 @@ public class SaTokenConfig implements WebMvcConfigurer { excludePathPatterns.add("/data/clt/**"); excludePathPatterns.add("/data/mbtiles/**"); excludePathPatterns.add("/data/pak/**"); + excludePathPatterns.add("/systemService/**"); + excludePathPatterns.add("/**"); // 注册 Sa-Token 拦截器 registry.addInterceptor(new SaInterceptor(handle -> { diff --git a/src/main/java/com/yj/earth/common/util/FileCommonUtil.java b/src/main/java/com/yj/earth/common/util/FileCommonUtil.java new file mode 100644 index 0000000..f787f88 --- /dev/null +++ b/src/main/java/com/yj/earth/common/util/FileCommonUtil.java @@ -0,0 +1,51 @@ +package com.yj.earth.common.util; + +import org.springframework.http.MediaType; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; + +public class FileCommonUtil { + + /** + * 将本地文件转换为 MultipartFile + */ + public static MultipartFile convertToMultipartFile(File file) { + try (FileInputStream inputStream = new FileInputStream(file)) { + return new MockMultipartFile( + "files", + file.getName(), + MediaType.APPLICATION_OCTET_STREAM_VALUE, + inputStream + ); + } catch (IOException e) { + return null; + } + } + + public static MediaType getImageMediaType(String suffix) { + String lowerSuffix = suffix.toLowerCase(); + switch (lowerSuffix) { + case ".jpg": + case ".jpeg": + return MediaType.IMAGE_JPEG; + case ".glb": + return MediaType.valueOf("model/gltf-binary"); + case ".png": + return MediaType.IMAGE_PNG; + case ".gif": + return MediaType.IMAGE_GIF; + case ".bmp": + return MediaType.valueOf("image/bmp"); + case ".webp": + return MediaType.valueOf("image/webp"); + case ".svg": + return MediaType.valueOf("image/svg+xml"); + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } +} diff --git a/src/main/java/com/yj/earth/common/util/FileUtil.java b/src/main/java/com/yj/earth/common/util/FileUtil.java deleted file mode 100644 index 63cd703..0000000 --- a/src/main/java/com/yj/earth/common/util/FileUtil.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.yj.earth.common.util; - -import org.springframework.http.MediaType; -import org.springframework.mock.web.MockMultipartFile; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; - -public class FileUtil { - - /** - * 将本地文件转换为 MultipartFile - */ - public static MultipartFile convertToMultipartFile(File file) { - try (FileInputStream inputStream = new FileInputStream(file)) { - return new MockMultipartFile( - "files", - file.getName(), - MediaType.APPLICATION_OCTET_STREAM_VALUE, - inputStream - ); - } catch (IOException e) { - return null; - } - } -} diff --git a/src/main/java/com/yj/earth/common/util/GdalUtil.java b/src/main/java/com/yj/earth/common/util/GdalUtil.java new file mode 100644 index 0000000..31c84b0 --- /dev/null +++ b/src/main/java/com/yj/earth/common/util/GdalUtil.java @@ -0,0 +1,82 @@ +package com.yj.earth.common.util; + +import org.gdal.gdal.gdal; +import org.gdal.ogr.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class GdalUtil { + + // 静态初始化 + static { + gdal.SetConfigOption("PROJ_LIB", "./lib"); + gdal.AllRegister(); + } + + public static String readVectorToJson(String filePath) { + // 打开矢量文件 + DataSource dataSource = ogr.Open(filePath); + if (dataSource == null) { + throw new RuntimeException("无法打开矢量文件: " + filePath); + } + // 存储所有数据的Map + Map result = new HashMap<>(); + // 获取图层数量 + int layerCount = dataSource.GetLayerCount(); + result.put("layerCount", layerCount); + // 存储所有图层数据 + List> layers = new ArrayList<>(); + for (int i = 0; i < layerCount; i++) { + Layer layer = dataSource.GetLayer(i); + if (layer == null) continue; + // 图层信息 + Map layerData = new HashMap<>(); + layerData.put("layerName", layer.GetName()); + layerData.put("featureCount", layer.GetFeatureCount()); + // 获取字段定义 + FeatureDefn featureDefn = layer.GetLayerDefn(); + int fieldCount = featureDefn.GetFieldCount(); + List> fields = new ArrayList<>(); + for (int j = 0; j < fieldCount; j++) { + FieldDefn fieldDefn = featureDefn.GetFieldDefn(j); + Map fieldInfo = new HashMap<>(); + fieldInfo.put("name", fieldDefn.GetName()); + fieldInfo.put("type", fieldDefn.GetFieldTypeName(fieldDefn.GetFieldType())); + fields.add(fieldInfo); + } + layerData.put("fields", fields); + // 读取所有要素 + List> features = new ArrayList<>(); + layer.ResetReading(); + Feature feature; + + while ((feature = layer.GetNextFeature()) != null) { + Map featureData = new HashMap<>(); + // 存储属性信息 + Map attributes = new HashMap<>(); + for (int j = 0; j < fieldCount; j++) { + attributes.put(featureDefn.GetFieldDefn(j).GetName(), feature.GetFieldAsString(j)); + } + featureData.put("attributes", attributes); + // 存储几何信息 + Geometry geometry = feature.GetGeometryRef(); + if (geometry != null) { + featureData.put("geometryType", geometry.GetGeometryName()); + featureData.put("wkt", geometry.ExportToWkt()); + } + features.add(featureData); + feature.delete(); // 释放资源 + } + layerData.put("features", features); + layers.add(layerData); + } + result.put("layers", layers); + // 关闭数据源 + dataSource.delete(); + // 转换为JSON并返回 + return JsonUtil.toJson(result); + } +} diff --git a/src/main/java/com/yj/earth/common/util/SQLiteConverter.java b/src/main/java/com/yj/earth/common/util/SQLiteConverter.java new file mode 100644 index 0000000..4c02c0b --- /dev/null +++ b/src/main/java/com/yj/earth/common/util/SQLiteConverter.java @@ -0,0 +1,274 @@ +import java.sql.*; +import java.util.UUID; + +public class SQLiteConverter { + // SQLite JDBC驱动 + private static final String JDBC_DRIVER = "org.sqlite.JDBC"; + + // 原始数据库路径 + private static final String ORIGINAL_DB_PATH = "jdbc:sqlite:D:\\YJEarth.model"; + + // 新数据库路径 + private static final String NEW_DB_PATH = "jdbc:sqlite:E:\\通用模型库.model"; + + public static void main(String[] args) { + System.out.println("===== 开始数据库转换程序 ====="); + System.out.println("原始数据库: " + ORIGINAL_DB_PATH); + System.out.println("目标数据库: " + NEW_DB_PATH + "\n"); + + // 使用try-with-resources自动管理连接资源 + try (Connection originalConn = DriverManager.getConnection(ORIGINAL_DB_PATH); + Connection newConn = DriverManager.getConnection(NEW_DB_PATH)) { + + System.out.println("✅ 成功连接到原始数据库"); + System.out.println("✅ 成功创建并连接到新数据库\n"); + + // 在新数据库中创建表结构 + System.out.println("===== 开始创建新表结构 ====="); + createNewTables(newConn); + System.out.println("===== 新表结构创建完成 ====="); + + // 迁移mode_types表数据到model_type表 + System.out.println("\n===== 开始迁移mode_types表数据 ====="); + MigrationResult modeTypesResult = migrateModeTypes(originalConn, newConn); + System.out.println("===== mode_types表数据迁移完成 ====="); + System.out.printf(" 成功: %d 条, 失败: %d 条, 总计: %d 条%n", + modeTypesResult.successCount, + modeTypesResult.failureCount, + modeTypesResult.totalCount); + + // 迁移models表数据到model表 + System.out.println("\n===== 开始迁移models表数据 ====="); + MigrationResult modelsResult = migrateModels(originalConn, newConn); + System.out.println("===== models表数据迁移完成 ====="); + System.out.printf(" 成功: %d 条, 失败: %d 条, 总计: %d 条%n", + modelsResult.successCount, + modelsResult.failureCount, + modelsResult.totalCount); + + if (modeTypesResult.failureCount > 0 || modelsResult.failureCount > 0) { + System.out.println("\n⚠️ 注意:部分记录迁移失败、已跳过错误记录"); + } + + System.out.println("\n===== 数据库转换处理完成! ====="); + + } catch (SQLException e) { + System.err.println("\n❌ 数据库连接错误: " + e.getMessage()); + e.printStackTrace(); + } + } + + /** + * 迁移结果数据类 + */ + private static class MigrationResult { + int successCount; + int failureCount; + int totalCount; + + MigrationResult(int success, int failure, int total) { + this.successCount = success; + this.failureCount = failure; + this.totalCount = total; + } + } + + /** + * 在新数据库中创建表结构 + */ + private static void createNewTables(Connection conn) throws SQLException { + try (Statement stmt = conn.createStatement()) { + // 创建model_type表 + System.out.println("正在创建model_type表..."); + String createModelTypeTable = "CREATE TABLE IF NOT EXISTS \"model_type\" (" + + "\"id\" TEXT," + + "\"name\" TEXT," + + "\"parent_id\" TEXT," + + "\"tree_index\" INTEGER," + + "\"created_at\" TEXT," + + "\"updated_at\" TEXT," + + "PRIMARY KEY (\"id\")" + + ");"; + stmt.execute(createModelTypeTable); + System.out.println("✅ model_type表创建完成"); + + // 创建model表 + System.out.println("正在创建model表..."); + String createModelTable = "CREATE TABLE IF NOT EXISTS \"model\" (" + + "\"id\" TEXT," + + "\"model_type_id\" TEXT," + + "\"model_name\" TEXT," + + "\"model_type\" TEXT," + + "\"poster_type\" TEXT," + + "\"poster\" TEXT," + + "\"data\" TEXT," + + "\"data_bytes\" BLOB," + + "\"view\" TEXT," + + "\"created_at\" TEXT," + + "\"updated_at\" TEXT," + + "PRIMARY KEY (\"id\")" + + ");"; + stmt.execute(createModelTable); + System.out.println("✅ model表创建完成"); + } + } + + /** + * 迁移mode_types表数据到model_type表 + */ + private static MigrationResult migrateModeTypes(Connection originalConn, Connection newConn) throws SQLException { + // 先获取记录总数、用于显示进度 + int totalCount; + try (PreparedStatement countStmt = originalConn.prepareStatement("SELECT COUNT(*) FROM mode_types"); + ResultSet countRs = countStmt.executeQuery()) { + countRs.next(); + totalCount = countRs.getInt(1); + System.out.println("发现 " + totalCount + " 条mode_types记录待迁移"); + } + + // 查询原始表数据 + try (PreparedStatement selectStmt = originalConn.prepareStatement("SELECT * FROM mode_types"); + ResultSet rs = selectStmt.executeQuery()) { + + // 插入到新表、关闭自动提交以提高性能 + newConn.setAutoCommit(false); + + try (PreparedStatement insertStmt = newConn.prepareStatement( + "INSERT INTO model_type (" + + "id, name, parent_id, tree_index, created_at, updated_at" + + ") VALUES (?, ?, ?, ?, ?, ?)")) { + + int successCount = 0; + int failureCount = 0; + + while (rs.next()) { + try { + // 生成新的UUID作为ID + String newId = UUID.randomUUID().toString().replaceAll("-", ""); + String typeName = rs.getString("type_name"); + + // 映射字段 + insertStmt.setString(1, newId); + insertStmt.setString(2, typeName); + insertStmt.setString(3, rs.getString("p_id")); + insertStmt.setInt(4, 0); + insertStmt.setString(5, rs.getString("created_at")); + insertStmt.setString(6, rs.getString("updated_at")); + + insertStmt.executeUpdate(); + successCount++; + + // 每迁移10条记录或最后一批记录时打印进度 + if (successCount % 10 == 0 || (successCount + failureCount) == totalCount) { + printProgress(successCount + failureCount, totalCount); + } + } catch (SQLException e) { + // 捕获单条记录的异常、跳过该记录 + failureCount++; + System.err.printf("\n❌ 迁移mode_types记录失败 (序号: %d): %s%n", + successCount + failureCount, e.getMessage()); + } + } + + newConn.commit(); + return new MigrationResult(successCount, failureCount, totalCount); + } catch (SQLException e) { + newConn.rollback(); + throw e; + } finally { + newConn.setAutoCommit(true); + } + } + } + + /** + * 迁移models表数据到model表 + */ + private static MigrationResult migrateModels(Connection originalConn, Connection newConn) throws SQLException { + // 先获取记录总数、用于显示进度 + int totalCount; + try (PreparedStatement countStmt = originalConn.prepareStatement("SELECT COUNT(*) FROM models"); + ResultSet countRs = countStmt.executeQuery()) { + countRs.next(); + totalCount = countRs.getInt(1); + System.out.println("发现 " + totalCount + " 条models记录待迁移"); + } + + // 查询原始表数据 + try (PreparedStatement selectStmt = originalConn.prepareStatement("SELECT * FROM models"); + ResultSet rs = selectStmt.executeQuery()) { + + // 插入到新表、关闭自动提交以提高性能 + newConn.setAutoCommit(false); + + try (PreparedStatement insertStmt = newConn.prepareStatement( + "INSERT INTO model (" + + "id, model_type_id, model_name, model_type, poster_type, " + + "poster, data, data_bytes, view, created_at, updated_at" + + ") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")) { + + int successCount = 0; + int failureCount = 0; + + while (rs.next()) { + try { + // 生成新的UUID作为ID + String newId = UUID.randomUUID().toString().replaceAll("-", ""); + String modelName = rs.getString("model_name"); + + // 映射字段 + insertStmt.setString(1, newId); + insertStmt.setString(2, rs.getString("p_id")); + insertStmt.setString(3, modelName); + insertStmt.setString(4, rs.getString("model_type")); + insertStmt.setString(5, rs.getString("poster_type")); + + // 处理BLOB字段 + byte[] posterBlob = rs.getBytes("poster"); + insertStmt.setString(6, posterBlob != null ? new String(posterBlob) : null); + insertStmt.setString(7, null); + + byte[] dataBlob = rs.getBytes("data"); + insertStmt.setBytes(8, dataBlob); + insertStmt.setString(9, null); + + insertStmt.setString(10, rs.getString("created_at")); + insertStmt.setString(11, rs.getString("updated_at")); + + insertStmt.executeUpdate(); + successCount++; + + // 每迁移10条记录或最后一批记录时打印进度 + if (successCount % 10 == 0 || (successCount + failureCount) == totalCount) { + printProgress(successCount + failureCount, totalCount); + } + } catch (SQLException e) { + // 捕获单条记录的异常、跳过该记录 + failureCount++; + System.err.printf("\n❌ 迁移models记录失败 (序号: %d, 名称: %s): %s%n", + successCount + failureCount, + rs.getString("model_name"), + e.getMessage()); + } + } + + newConn.commit(); + return new MigrationResult(successCount, failureCount, totalCount); + } catch (SQLException e) { + newConn.rollback(); + throw e; + } finally { + newConn.setAutoCommit(true); + } + } + } + + /** + * 打印迁移进度 + */ + private static void printProgress(int current, int total) { + double percentage = (current * 100.0) / total; + System.out.printf("迁移进度: %d/%d (%.1f%%)\r", current, total, percentage); + System.out.flush(); + } +} diff --git a/src/main/java/com/yj/earth/common/util/SQLiteUtil.java b/src/main/java/com/yj/earth/common/util/SQLiteUtil.java index e2dadb3..93d2354 100644 --- a/src/main/java/com/yj/earth/common/util/SQLiteUtil.java +++ b/src/main/java/com/yj/earth/common/util/SQLiteUtil.java @@ -41,8 +41,7 @@ public class SQLiteUtil { /** * 执行查询并返回对象列表 */ - public static List queryForList(String dbFilePath, String sql, List params, Class clazz) - throws SQLException, IllegalAccessException, InstantiationException { + public static List queryForList(String dbFilePath, String sql, List params, Class clazz) throws SQLException, IllegalAccessException, InstantiationException { List resultList = new ArrayList<>(); // 使用try-with-resources确保资源自动关闭 @@ -320,10 +319,9 @@ public class SQLiteUtil { "model_type_id" TEXT, "model_name" TEXT, "model_type" TEXT, + "model_data" BLOB, "poster_type" TEXT, - "poster" TEXT, - "data" TEXT, - "view" TEXT, + "poster_data" BLOB, "created_at" TEXT, "updated_at" TEXT, PRIMARY KEY ("id") diff --git a/src/main/java/com/yj/earth/common/util/SdkUtil.java b/src/main/java/com/yj/earth/common/util/SdkUtil.java index 3849124..54998cc 100644 --- a/src/main/java/com/yj/earth/common/util/SdkUtil.java +++ b/src/main/java/com/yj/earth/common/util/SdkUtil.java @@ -175,7 +175,7 @@ public class SdkUtil { } catch (IOException e) { log.error("读取sdk配置文件失败", e); } catch (ClassCastException e) { - log.error("sdk配置文件中server.port格式错误,应为数字类型", e); + log.error("sdk配置文件中server.port格式错误、应为数字类型", e); } return null; } diff --git a/src/main/java/com/yj/earth/dto/system/UpdateSystemServiceDto.java b/src/main/java/com/yj/earth/dto/system/UpdateSystemServiceDto.java new file mode 100644 index 0000000..9cad785 --- /dev/null +++ b/src/main/java/com/yj/earth/dto/system/UpdateSystemServiceDto.java @@ -0,0 +1,12 @@ +package com.yj.earth.dto.system; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class UpdateSystemServiceDto { + @Schema(description = "服务地址") + private String serverHost; + @Schema(description = "服务端口") + private Integer serverPort; +} diff --git a/src/main/java/com/yj/earth/vo/ModelDataVo.java b/src/main/java/com/yj/earth/vo/ModelDataVo.java new file mode 100644 index 0000000..9697805 --- /dev/null +++ b/src/main/java/com/yj/earth/vo/ModelDataVo.java @@ -0,0 +1,14 @@ +package com.yj.earth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class ModelDataVo { + @Schema(description = "模型名称") + private String modelName; + @Schema(description = "模型数据") + private byte[] modelData; + @Schema(description = "海报数据") + private byte[] posterData; +} diff --git a/src/main/java/com/yj/earth/vo/ModelVo.java b/src/main/java/com/yj/earth/vo/ModelVo.java index efb151c..71da898 100644 --- a/src/main/java/com/yj/earth/vo/ModelVo.java +++ b/src/main/java/com/yj/earth/vo/ModelVo.java @@ -1,9 +1,31 @@ package com.yj.earth.vo; import com.yj.earth.business.domain.Model; +import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; +import java.time.LocalDateTime; + @Data -public class ModelVo extends Model { +public class ModelVo { + @Schema(description = "主键") + private String id; + @Schema(description = "模型类型ID") + private String modelTypeId; + @Schema(description = "模型名称") + private String modelName; + @Schema(description = "模型类型") + private String modelType; + @Schema(description = "模型数据URL") + private String modelDataUrl; + @Schema(description = "海报类型") + private String posterType; + @Schema(description = "海报URL") + private String posterDataUrl; + @Schema(description = "创建时间") + private LocalDateTime createdAt; + @Schema(description = "更新时间") + private LocalDateTime updatedAt; + @Schema(description = "模型类型名称") private String modelTypeName; } diff --git a/src/main/resources/mapper/WebSourceMapper.xml b/src/main/resources/mapper/WebSourceMapper.xml new file mode 100644 index 0000000..a110a41 --- /dev/null +++ b/src/main/resources/mapper/WebSourceMapper.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + id, name, path, type, created_at, updated_at + + +