From efe4ddf97bbfb1ea4591c80cd8ff3a688261489a Mon Sep 17 00:00:00 2001 From: ZZX9599 <536509593@qq.com> Date: Mon, 29 Sep 2025 17:34:21 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=BA=93=E3=80=81=E7=9F=A2?= =?UTF-8?q?=E9=87=8F=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 7 + .../controller/BusinessConfigController.java | 1 + .../controller/MilitaryLibraryController.java | 620 ++++++++++------ .../controller/ModelLibraryController.java | 17 +- .../yj/earth/business/domain/Military.java | 4 +- .../yj/earth/common/config/ServerConfig.java | 1 + .../yj/earth/common/util/FileCommonUtil.java | 16 +- .../yj/earth/common/util/SQLiteConverter.java | 495 ++++++------- .../com/yj/earth/common/util/SQLiteUtil.java | 685 ++++++++++++------ .../java/com/yj/earth/vo/MilitaryDataVo.java | 12 + src/main/java/com/yj/earth/vo/MilitaryVo.java | 20 + 11 files changed, 1159 insertions(+), 719 deletions(-) create mode 100644 src/main/java/com/yj/earth/vo/MilitaryDataVo.java diff --git a/pom.xml b/pom.xml index ccf03c3..37dd43c 100644 --- a/pom.xml +++ b/pom.xml @@ -175,6 +175,13 @@ + + + org.apache.commons + commons-dbcp2 + 2.10.0 + + org.yaml diff --git a/src/main/java/com/yj/earth/business/controller/BusinessConfigController.java b/src/main/java/com/yj/earth/business/controller/BusinessConfigController.java index e17b6d4..a840f56 100644 --- a/src/main/java/com/yj/earth/business/controller/BusinessConfigController.java +++ b/src/main/java/com/yj/earth/business/controller/BusinessConfigController.java @@ -12,6 +12,7 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; + @Tag(name = "业务配置管理") @CheckAuth @RestController diff --git a/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java b/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java index e260390..6909a67 100644 --- a/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java +++ b/src/main/java/com/yj/earth/business/controller/MilitaryLibraryController.java @@ -4,21 +4,27 @@ 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.CheckAuth; -import com.yj.earth.business.domain.MilitaryLibrary; -import com.yj.earth.business.domain.MilitaryType; +import com.yj.earth.business.domain.MilitaryLibrary; // 需自定义:对应军标库配置表(同ModelLibrary结构) +import com.yj.earth.business.domain.MilitaryType; // 需自定义:对应military_type表实体 import com.yj.earth.business.service.FileInfoService; -import com.yj.earth.business.service.MilitaryLibraryService; +import com.yj.earth.business.service.MilitaryLibraryService; // 需自定义:军标库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.AddMilitaryTypeDto; -import com.yj.earth.dto.militaryLibrary.CreateMilitaryLibraryDto; -import com.yj.earth.dto.militaryLibrary.DragMilitaryTypeDto; -import com.yj.earth.dto.militaryLibrary.UpdateMilitaryTypeNameDto; -import com.yj.earth.vo.MilitaryTypeVo; -import com.yj.earth.vo.MilitaryVo; +import com.yj.earth.dto.militaryLibrary.AddMilitaryTypeDto; // 需自定义:添加军标类型DTO +import com.yj.earth.dto.militaryLibrary.CreateMilitaryLibraryDto; // 需自定义:创建军标库DTO +import com.yj.earth.dto.militaryLibrary.DragMilitaryTypeDto; // 需自定义:拖动军标类型DTO +import com.yj.earth.dto.militaryLibrary.UpdateMilitaryTypeNameDto; // 需自定义:修改军标类型名称DTO +import com.yj.earth.vo.MilitaryDataVo; +import com.yj.earth.vo.MilitaryTypeVo; // 需自定义:军标类型树形VO(同ModelTypeVo结构) +import com.yj.earth.vo.MilitaryVo; // 需自定义:军标列表VO(剔除海报相关字段) 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.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; @@ -26,9 +32,12 @@ 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.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -38,44 +47,52 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/militaryLibrary") public class MilitaryLibraryController { + @Resource private MilitaryLibraryService militaryLibraryService; - @Resource - private FileInfoService fileInfoService; @Operation(summary = "创建军标库") @PostMapping("/createMilitaryLibrary") - public ApiResponse createMilitaryLibrary(@RequestBody CreateMilitaryLibraryDto createMilitaryLibraryDto) { + public ApiResponse createMilitaryLibrary(@RequestBody CreateMilitaryLibraryDto createDto) { try { - // 参数校验 - String folderPath = createMilitaryLibraryDto.getPath(); - String militaryName = createMilitaryLibraryDto.getName(); - // 处理路径、组合为完整军标库文件路径 + // 参数校验与路径处理 + String folderPath = createDto.getPath(); + String libraryName = createDto.getName(); + if (!StringUtils.hasText(folderPath) || !StringUtils.hasText(libraryName)) { + return ApiResponse.failure("路径和名称不能为空"); + } + + // 构建完整军标库文件路径(SQLite文件) File parentDir = new File(folderPath); - File militaryFile = new File(parentDir, militaryName); + File militaryFile = new File(parentDir, libraryName); String militaryPath = militaryFile.getAbsolutePath().replace("\\", "/"); - // 检查父目录是否存在、不存在则创建 + + // 父目录不存在则创建 if (!parentDir.exists()) { boolean mkdirsSuccess = parentDir.mkdirs(); if (!mkdirsSuccess) { return ApiResponse.failure("创建父目录失败:" + folderPath); } } - // 检查军标库文件是否已存在 + + // 校验军标库文件是否已存在 if (militaryFile.exists()) { if (militaryFile.isDirectory()) { - return ApiResponse.failure("同名目录已存在、无法创建文件:" + militaryPath); + return ApiResponse.failure("同名目录已存在,无法创建文件:" + militaryPath); } return ApiResponse.failure("军标库文件已存在:" + militaryPath); } - // 创建军标库文件 - boolean createSuccess = militaryFile.createNewFile(); - if (!createSuccess) { + + // 创建SQLite文件 + boolean createFileSuccess = militaryFile.createNewFile(); + if (!createFileSuccess) { return ApiResponse.failure("创建军标库文件失败:" + militaryPath); } - // 添加军标库信息 - addMilitaryLibrary(militaryPath); + + // 初始化军标库表结构 SQLiteUtil.initializationMilitary(militaryPath); + // 添加军标库配置到数据库 + addMilitaryLibrary(militaryPath); return ApiResponse.success(null); } catch (Exception e) { return ApiResponse.failure("创建军标库失败:" + e.getMessage()); @@ -84,304 +101,441 @@ public class MilitaryLibraryController { @Operation(summary = "导入军标库") @PostMapping("/importMilitaryLibrary") - public ApiResponse importMilitaryLibrary(@RequestParam("militaryPath") @Parameter(description = "军标库路径") String militaryPath) { - addMilitaryLibrary(militaryPath); - return ApiResponse.success(null); + public ApiResponse importMilitaryLibrary( + @RequestParam("militaryPath") @Parameter(description = "军标库SQLite文件路径") String militaryPath) { + try { + // 校验路径是否存在 + File militaryFile = new File(militaryPath); + if (!militaryFile.exists() || !militaryFile.isFile()) { + return ApiResponse.failure("军标库文件不存在:" + militaryPath); + } + // 添加到配置表并启用 + addMilitaryLibrary(militaryPath); + return ApiResponse.success(null); + } catch (Exception e) { + return ApiResponse.failure("导入军标库失败:" + e.getMessage()); + } } @Operation(summary = "添加军标类型") @PostMapping("/addMilitaryType") - public ApiResponse addMilitaryType(@RequestBody AddMilitaryTypeDto addMilitaryTypeDto) throws SQLException, IllegalAccessException, InstantiationException { + public ApiResponse addMilitaryType(@RequestBody AddMilitaryTypeDto addDto) throws SQLException, IllegalAccessException, InstantiationException { + // 获取当前启用的军标库路径 String militaryPath = getMilitaryLibrary(); if (militaryPath == null) { return ApiResponse.failure("请先创建或导入军标库"); } - // 检查父级是否存在 - String parentId = addMilitaryTypeDto.getParentId(); - if (parentId != null) { - String sql = "SELECT * FROM military_type WHERE id = ?"; - List params = new ArrayList<>(); - params.add(parentId); - MilitaryType militaryType = SQLiteUtil.queryForObject(militaryPath, sql, params, MilitaryType.class); - if (militaryType == null) { - return ApiResponse.failure("父级军标类型不存在"); + + // 校验父级类型(若有) + String parentId = addDto.getParentId(); + if (StringUtils.hasText(parentId)) { + String checkParentSql = "SELECT id FROM military_type WHERE id = ?"; + List parentParams = new ArrayList<>(); + parentParams.add(parentId); + MilitaryType parentType = SQLiteUtil.queryForObject( + militaryPath, checkParentSql, parentParams, MilitaryType.class + ); + if (parentType == null) { + return ApiResponse.failure("父级军标类型不存在:" + parentId); } } - String sql = "INSERT INTO military_type " + + + // 插入军标类型 + String insertSql = "INSERT INTO military_type " + "(id, name, parent_id, tree_index, created_at) " + "VALUES (?, ?, ?, ?, ?)"; List params = new ArrayList<>(); params.add(UUID.fastUUID().toString(true)); - params.add(addMilitaryTypeDto.getName()); - params.add(addMilitaryTypeDto.getParentId()); + params.add(addDto.getName()); + params.add(parentId); params.add(0); - params.add(LocalDateTime.now()); - SQLiteUtil.executeUpdate(militaryPath, sql, params); - return ApiResponse.success(null); - } - - @Operation(summary = "删除军标类型") - @PostMapping("/deleteMilitaryType") - public ApiResponse deleteMilitaryType(@Parameter(description = "军标类型ID") @RequestParam("militaryTypeId") String militaryTypeId) throws SQLException { - String militaryPath = getMilitaryLibrary(); - if (militaryPath == null) { - return ApiResponse.failure("请先创建或导入军标库"); - } - String sql = "DELETE FROM military_type WHERE id = ?"; - List params = new ArrayList<>(); - params.add(militaryTypeId); - SQLiteUtil.executeUpdate(militaryPath, sql, params); + params.add(LocalDateTime.now().toString()); + SQLiteUtil.executeUpdate(militaryPath, insertSql, params); return ApiResponse.success(null); } @Operation(summary = "修改军标类型名称") @PostMapping("/updateMilitaryTypeName") - public ApiResponse updateMilitaryTypeName(@RequestBody UpdateMilitaryTypeNameDto updateMilitaryTypeNameDto) throws SQLException { + public ApiResponse updateMilitaryTypeName(@RequestBody UpdateMilitaryTypeNameDto updateDto) + throws SQLException { String militaryPath = getMilitaryLibrary(); if (militaryPath == null) { return ApiResponse.failure("请先创建或导入军标库"); } - String sql = "UPDATE military_type SET name = ? WHERE id = ?"; + + // 执行更新 + String updateSql = "UPDATE military_type SET name = ?, updated_at = ? WHERE id = ?"; List params = new ArrayList<>(); - params.add(updateMilitaryTypeNameDto.getName()); - params.add(updateMilitaryTypeNameDto.getId()); - SQLiteUtil.executeUpdate(militaryPath, sql, params); + params.add(updateDto.getName()); + params.add(LocalDateTime.now().toString()); + params.add(updateDto.getId()); + SQLiteUtil.executeUpdate(militaryPath, updateSql, params); return ApiResponse.success(null); } - @Operation(summary = "军标类型列表") - @GetMapping("/militaryTypeList") + @Operation(summary = "删除军标类型") + @PostMapping("/deleteMilitaryType") + public ApiResponse deleteMilitaryType( + @RequestParam("militaryTypeId") @Parameter(description = "军标类型ID") String typeId) + throws SQLException { + String militaryPath = getMilitaryLibrary(); + if (militaryPath == null) { + return ApiResponse.failure("请先创建或导入军标库"); + } + + // 执行删除 + String deleteSql = "DELETE FROM military_type WHERE id = ?"; + List params = new ArrayList<>(); + params.add(typeId); + + SQLiteUtil.executeUpdate(militaryPath, deleteSql, params); + return ApiResponse.success(null); + } + + @Operation(summary = "军标类型树形列表") + @GetMapping("/militaryTypeTree") public ApiResponse militaryTypeTree() throws SQLException, IllegalAccessException, InstantiationException { - String militaryPath = getMilitaryLibrary(); - if (militaryPath == null) { - return ApiResponse.failure("请先创建或导入军标库"); - } - // 查询所有军标类型 - String sql = """ - SELECT id, name, parent_id as parentId, - tree_index as treeIndex, created_at as createdAt, - updated_at as updatedAt FROM military_type ORDER BY tree_index ASC - """; - List militaryTypes = SQLiteUtil.queryForList(militaryPath, sql, null, MilitaryType.class); - // 转换为树形结构 - List treeList = buildMilitaryTypeTree(militaryTypes); + List treeList = militaryTypeList(); return ApiResponse.success(treeList); } - @Operation(summary = "拖动军标类型树") - @PostMapping("/dragMilitaryType") - public ApiResponse dragMilitaryType(@RequestBody DragMilitaryTypeDto dragMilitaryTypeDto) throws SQLException { - String militaryPath = getMilitaryLibrary(); - if (militaryPath == null) { - return ApiResponse.failure("请先创建或导入军标库"); - } - - // 动态构建SQL和参数 - List updateFields = new ArrayList<>(); - List params = new ArrayList<>(); - - // 判断 parentId 是否存在 - if (dragMilitaryTypeDto.getParentId() != null) { - updateFields.add("parent_id = ?"); - params.add(dragMilitaryTypeDto.getParentId()); - } - - // 判断 treeIndex 是否存在 - if (dragMilitaryTypeDto.getTreeIndex() != null) { - updateFields.add("tree_index = ?"); - params.add(dragMilitaryTypeDto.getTreeIndex()); - } - - // 构建完整 SQL - String sql = "UPDATE military_type SET " + String.join(", ", updateFields) + " WHERE id = ?"; - params.add(dragMilitaryTypeDto.getId()); - SQLiteUtil.executeUpdate(militaryPath, sql, params); - return ApiResponse.success(null); - } - @Operation(summary = "添加军标文件") @PostMapping("/addMilitaryFile") - public ApiResponse addMilitaryFile(@RequestParam("files") MultipartFile[] files, @Parameter(description = "军标类型ID") @RequestParam("militaryTypeId") String militaryTypeId) throws IOException, SQLException { - // 获取最新的军标库路径 + public ApiResponse addMilitaryFile(@RequestParam("files") MultipartFile[] files, @RequestParam("militaryTypeId") @Parameter(description = "军标类型ID") String typeId) throws IOException, SQLException, IllegalAccessException, InstantiationException { + // 获取当前启用的军标库 String militaryPath = getMilitaryLibrary(); if (militaryPath == null) { return ApiResponse.failure("请先创建或导入军标库"); } - // 循环处理每个上传的文件 + + // 校验类型是否存在 + if (!isMilitaryTypeExist(militaryPath, typeId)) { + return ApiResponse.failure("军标类型不存在:" + typeId); + } + + // 循环处理每个文件 for (MultipartFile file : files) { - // 跳过空文件 if (file.isEmpty()) { - continue; + continue; // 跳过空文件 } - // 获取文件信息 - String fileName = file.getOriginalFilename(); - if (fileName == null) { - continue; - } - String fileSuffix = FileUtil.extName(fileName); - String fileNameWithoutSuffix = FileUtil.mainName(fileName); - // 构建插入SQL - String sql = "INSERT INTO military " + - "(id, military_type_id, military_name, military_type, data, created_at) " + + // 解析文件名与后缀 + String originalFileName = file.getOriginalFilename(); + if (originalFileName == null) { + continue; + } + String fileSuffix = FileUtil.extName(originalFileName); + String fileNameWithoutSuffix = FileUtil.mainName(originalFileName); + + // 插入军标数据 + String insertSql = "INSERT INTO military " + + "(id, military_type_id, military_name, military_type, military_data, created_at) " + "VALUES (?, ?, ?, ?, ?, ?)"; - - String url = fileInfoService.uploadWithPreview(file); List params = new ArrayList<>(); params.add(UUID.fastUUID().toString(true)); - params.add(militaryTypeId); + params.add(typeId); params.add(fileNameWithoutSuffix); params.add(fileSuffix); - params.add(url); - params.add(LocalDateTime.now()); + params.add(file.getBytes()); + params.add(LocalDateTime.now().toString()); - // 执行插入操作 - SQLiteUtil.executeUpdate(militaryPath, sql, params); + SQLiteUtil.executeUpdate(militaryPath, insertSql, params); } return ApiResponse.success(null); } - @Operation(summary = "根据军标类型查看军标列表") + @Operation(summary = "获取军标文件数据") + @GetMapping("/data/military/{militaryId}/{fileSuffix}") + public ResponseEntity getMilitaryData(@PathVariable("militaryId") @Parameter(description = "军标ID") String militaryId, @PathVariable("fileSuffix") @Parameter(description = "军标文件后缀") String fileSuffix) { + try { + String militaryPath = getMilitaryLibrary(); + if (militaryPath == null) { + return ResponseEntity.notFound().build(); + } + // 查询军标二进制数据 + String querySql = "SELECT military_name, military_data FROM military WHERE id = ?"; + List params = new ArrayList<>(); + params.add(militaryId); + MilitaryDataVo dataVo = SQLiteUtil.queryForObject( + militaryPath, querySql, params, MilitaryDataVo.class + ); + if (dataVo == null || dataVo.getMilitaryData() == null) { + return ResponseEntity.notFound().build(); + } + // 构建响应头【支持中文文件名+预览】 + String originalFileName = dataVo.getMilitaryName() + "." + fileSuffix; + String encodedFileName = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name()); + MediaType contentType = FileCommonUtil.getImageMediaType(fileSuffix); + + return ResponseEntity.ok() + .contentType(contentType) + .header(HttpHeaders.CONTENT_DISPOSITION, + "inline; filename=\"" + encodedFileName + "\"") + .body(dataVo.getMilitaryData()); + } catch (Exception e) { + e.printStackTrace(); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + @Operation(summary = "根据类型查询军标列表") @PostMapping("/militaryList") - public ApiResponse militaryList(@Parameter(description = "军标类型ID") @RequestParam("militaryTypeId") String militaryTypeId) throws SQLException, IllegalAccessException, InstantiationException { - // 获取最新的军标库 + public ApiResponse getMilitaryList(@RequestParam("militaryTypeId") @Parameter(description = "军标类型ID") String typeId) throws SQLException, IllegalAccessException, InstantiationException { String militaryPath = getMilitaryLibrary(); if (militaryPath == null) { return ApiResponse.failure("请先创建或导入军标库"); } - // 多表联查、查询所有的军标 - String sql = """ - SELECT - military.id, - military.military_type_id as militaryTypeId, - military.military_name as militaryName, - military.military_type as militaryType, - military.data, - military.view, - military.created_at as createdAt, - military.updated_at as updatedAt, - military_type.name as militaryTypeName from - military JOIN military_type ON military.military_type_id = military_type.id - WHERE military.military_type_id = ? - """; - List params = new ArrayList<>(); - params.add(militaryTypeId); - List militaryVos = SQLiteUtil.queryForList(militaryPath, sql, params, MilitaryVo.class); - return ApiResponse.success(militaryVos); + + // 获取当前类型及所有子类型ID(递归) + List typeIdList = getMilitaryTypeIdsWithChildren(typeId); + if (typeIdList == null || typeIdList.isEmpty()) { + return ApiResponse.success(new ArrayList<>()); + } + + // 构建IN条件(处理SQL注入风险) + String idsWithQuotes = typeIdList.stream() + .map(id -> "'" + id + "'") + .collect(Collectors.joining(",")); + + // 多表联查 + String querySql = """ + SELECT + m.id, + m.military_type_id as militaryTypeId, + m.military_name as militaryName, + m.military_type as militaryType, + m.created_at as createdAt, + m.updated_at as updatedAt, + t.name as militaryTypeName + FROM military m + JOIN military_type t ON m.military_type_id = t.id + WHERE m.military_type_id IN (%s) + ORDER BY m.created_at DESC + """.replace("%s", idsWithQuotes); + + // 查询并转换为VO + List militaryVoList = SQLiteUtil.queryForList( + militaryPath, querySql, null, MilitaryVo.class + ); + for (MilitaryVo vo : militaryVoList) { + vo.setMilitaryDataUrl("/militaryLibrary/data/military/" + vo.getId() + "/" + vo.getMilitaryType()); + } + return ApiResponse.success(militaryVoList); } - @Operation(summary = "更新军标信息") - @PostMapping("/uploadMilitaryInfo") - public ApiResponse uploadMilitaryInfo( - @Parameter(description = "军标ID") @RequestParam("militaryId") String militaryId, - @Parameter(description = "军标名称") @RequestParam(value = "militaryName", required = false) String militaryName) + @Operation(summary = "更新军标名称") + @PostMapping("/updateMilitaryInfo") + public ApiResponse updateMilitaryInfo( + @RequestParam("militaryId") @Parameter(description = "军标ID") String militaryId, + @RequestParam("militaryName") @Parameter(description = "新军标名称") String militaryName) throws SQLException { - - // 获取最新的军标库路径 String militaryPath = getMilitaryLibrary(); if (militaryPath == null) { return ApiResponse.failure("请先创建或导入军标库"); } - - // 如果没有需要更新的字段、直接返回成功 - if (!StringUtils.hasText(militaryName)) { - return ApiResponse.success(null); - } - - // 构建更新SQL和参数 - String sql = "UPDATE military SET updated_at = ?, military_name = ? WHERE id = ?"; + String updateSql = "UPDATE military SET military_name = ?, updated_at = ? WHERE id = ?"; List params = new ArrayList<>(); - params.add(LocalDateTime.now()); params.add(militaryName); + params.add(LocalDateTime.now().toString()); params.add(militaryId); - - // 执行更新 - SQLiteUtil.executeUpdate(militaryPath, sql, params); + SQLiteUtil.executeUpdate(militaryPath, updateSql, params); return ApiResponse.success(null); } - @Operation(summary = "删除军标") @PostMapping("/deleteMilitary") - public ApiResponse deleteMilitary(@Parameter(description = "军标ID") @RequestParam("militaryId") String militaryId) throws SQLException { + public ApiResponse deleteMilitary(@RequestParam("militaryId") @Parameter(description = "军标ID") String militaryId) throws SQLException { String militaryPath = getMilitaryLibrary(); if (militaryPath == null) { return ApiResponse.failure("请先创建或导入军标库"); } - String sql = "DELETE FROM military WHERE id = ?"; + + // 执行删除 + String deleteSql = "DELETE FROM military WHERE id = ?"; List params = new ArrayList<>(); params.add(militaryId); - SQLiteUtil.executeUpdate(militaryPath, sql, params); - return ApiResponse.success(null); + + SQLiteUtil.executeUpdate(militaryPath, deleteSql, params); + return ApiResponse.success("删除军标成功"); } - private List buildMilitaryTypeTree(List militaryTypes) { - List treeNodes = militaryTypes.stream() - .map(militaryType -> new MilitaryTypeVo(militaryType)) - .collect(Collectors.toList()); - // 构建节点ID到节点的映射 - Map nodeMap = treeNodes.stream() - .collect(Collectors.toMap(MilitaryTypeVo::getId, node -> node)); - // 根节点列表 - List rootNodes = new ArrayList<>(); - // 为每个节点添加子节点 - for (MilitaryTypeVo node : treeNodes) { - String parentId = node.getParentId(); + + @Operation(summary = "拖动调整军标类型层级") + @PostMapping("/dragMilitaryType") + public ApiResponse dragMilitaryType(@RequestBody List dragDtoList) + throws SQLException, IllegalAccessException, InstantiationException { + String militaryPath = getMilitaryLibrary(); + if (militaryPath == null) { + return ApiResponse.failure("请先创建或导入军标库"); + } + + // 批量更新层级和排序 + for (DragMilitaryTypeDto dto : dragDtoList) { + String updateSql = "UPDATE military_type " + + "SET parent_id = ?, tree_index = ?, updated_at = ? " + + "WHERE id = ?"; + List params = new ArrayList<>(); + params.add(dto.getParentId()); + params.add(dto.getTreeIndex()); + params.add(LocalDateTime.now().toString()); + params.add(dto.getId()); + + SQLiteUtil.executeUpdate(militaryPath, updateSql, params); + } + + // 返回更新后的树形列表 + List treeList = militaryTypeList(); + return ApiResponse.success(treeList); + } + + private String getMilitaryLibrary() { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + queryWrapper.eq(MilitaryLibrary::getIsEnable, 1); // 1=启用,0=未启用 + MilitaryLibrary library = militaryLibraryService.getOne(queryWrapper); + return library == null ? null : library.getPath(); + } + + private void addMilitaryLibrary(String militaryPath) { + // 所有已存在的军标库设置为「未启用」 + List existLibraries = militaryLibraryService.list(); + for (MilitaryLibrary library : existLibraries) { + library.setIsEnable(0); + militaryLibraryService.updateById(library); + } + + // 检查路径是否已存在(存在则启用,不存在则新增) + LambdaQueryWrapper pathWrapper = new LambdaQueryWrapper<>(); + pathWrapper.eq(MilitaryLibrary::getPath, militaryPath); + MilitaryLibrary existLibrary = militaryLibraryService.getOne(pathWrapper); + + if (existLibrary != null) { + existLibrary.setIsEnable(1); + militaryLibraryService.updateById(existLibrary); + } else { + MilitaryLibrary newLibrary = new MilitaryLibrary(); + newLibrary.setId(UUID.fastUUID().toString(true)); + newLibrary.setPath(militaryPath); + newLibrary.setName(FileUtil.mainName(militaryPath)); + newLibrary.setIsEnable(1); + newLibrary.setCreatedAt(LocalDateTime.now()); + militaryLibraryService.save(newLibrary); + } + } + + private List militaryTypeList() throws SQLException, IllegalAccessException, InstantiationException { + String militaryPath = getMilitaryLibrary(); + if (militaryPath == null) { + return new ArrayList<>(); + } + + String querySql = """ + SELECT + id, name, parent_id as parentId, + tree_index as treeIndex, created_at as createdAt, + updated_at as updatedAt + FROM military_type + ORDER BY tree_index ASC + """; + List typeList = SQLiteUtil.queryForList(militaryPath, querySql, null, MilitaryType.class); + + // 构建树形结构 + return buildMilitaryTypeTree(typeList); + } + + private List buildMilitaryTypeTree(List typeList) { + // 转换为VO + List voList = typeList.stream() + .map(militaryType -> new MilitaryTypeVo(militaryType)).collect(Collectors.toList()); + + // 构建ID→VO的映射(方便快速查找父级) + Map voMap = voList.stream() + .collect(Collectors.toMap(MilitaryTypeVo::getId, vo -> vo)); + + // 组装树形结构 + List rootList = new ArrayList<>(); + for (MilitaryTypeVo vo : voList) { + String parentId = vo.getParentId(); if (parentId == null || parentId.isEmpty()) { - // 没有父节点的是根节点 - rootNodes.add(node); + rootList.add(vo); } else { - // 找到父节点并添加为子节点 - MilitaryTypeVo parentNode = nodeMap.get(parentId); - if (parentNode != null) { - parentNode.getChildren().add(node); + MilitaryTypeVo parentVo = voMap.get(parentId); + if (parentVo != null) { + parentVo.getChildren().add(vo); } } } - return rootNodes; + // 排序 + rootList.sort(Comparator.comparingInt(MilitaryTypeVo::getTreeIndex)); + sortMilitaryTypeChildren(rootList); + + return rootList; } - private String getMilitaryLibrary() { - // 获取启用的军标库 - LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); - queryWrapper.eq(MilitaryLibrary::getIsEnable, 1); - MilitaryLibrary militaryLibrary = militaryLibraryService.getOne(queryWrapper); - if (militaryLibrary == null) { - return null; - } - return militaryLibrary.getPath(); - } - - private void addMilitaryLibrary(String militaryPath) { - // 查询系统所有的军标库 - List militaryLibraries = militaryLibraryService.list(); - // 遍历并更新状态 - for (MilitaryLibrary militaryLibrary : militaryLibraries) { - // 设置启用状态为0 - militaryLibrary.setIsEnable(0); - militaryLibraryService.updateById(militaryLibrary); - } - - // 检查相同路径的军标库是否已存在 - LambdaQueryWrapper pathWrapper = new LambdaQueryWrapper<>(); - pathWrapper.eq(MilitaryLibrary::getPath, militaryPath); - MilitaryLibrary existingMilitary = militaryLibraryService.getOne(pathWrapper); - // 若存在相同路径的军标库、不做处理、仅仅更新状态为显示 - if (existingMilitary != null) { - existingMilitary.setIsEnable(1); - militaryLibraryService.updateById(existingMilitary); + private void sortMilitaryTypeChildren(List voList) { + if (voList == null || voList.isEmpty()) { return; - } else { - // 新增军标库 - MilitaryLibrary newMilitary = new MilitaryLibrary(); - File file = FileUtil.file(militaryPath); - newMilitary.setId(UUID.fastUUID().toString(true)); - newMilitary.setPath(militaryPath); - newMilitary.setName(FileUtil.extName(file)); - newMilitary.setIsEnable(1); - militaryLibraryService.save(newMilitary); } + // 排序当前层级 + voList.sort(Comparator.comparingInt(MilitaryTypeVo::getTreeIndex)); + // 递归排序子层级 + for (MilitaryTypeVo vo : voList) { + sortMilitaryTypeChildren(vo.getChildren()); + } + } + + private List getMilitaryTypeIdsWithChildren(String typeId) + throws SQLException, IllegalAccessException, InstantiationException { + List idList = new ArrayList<>(); + if (!StringUtils.hasText(typeId)) { + return idList; + } + + String militaryPath = getMilitaryLibrary(); + if (militaryPath == null) { + return idList; + } + + // 校验类型是否存在 + if (!isMilitaryTypeExist(militaryPath, typeId)) { + return idList; + } + + // 递归收集ID(包含自身) + recursiveGetMilitaryTypeChildren(militaryPath, typeId, idList); + return idList; + } + + private void recursiveGetMilitaryTypeChildren(String militaryPath, String currentId, List idList) + throws SQLException, IllegalAccessException, InstantiationException { + // 先添加当前ID + idList.add(currentId); + + // 查询直接子类型 + String querySql = "SELECT id FROM military_type WHERE parent_id = ? ORDER BY tree_index ASC"; + List params = new ArrayList<>(); + params.add(currentId); + List childList = SQLiteUtil.queryForList( + militaryPath, querySql, params, MilitaryType.class + ); + + // 递归查询子类型的子类型 + if (childList != null && !childList.isEmpty()) { + for (MilitaryType child : childList) { + recursiveGetMilitaryTypeChildren(militaryPath, child.getId(), idList); + } + } + } + + private boolean isMilitaryTypeExist(String militaryPath, String typeId) + throws SQLException, IllegalAccessException, InstantiationException { + String checkSql = "SELECT id FROM military_type WHERE id = ?"; + List params = new ArrayList<>(); + params.add(typeId); + List existList = SQLiteUtil.queryForList( + militaryPath, checkSql, params, MilitaryType.class + ); + return existList != null && !existList.isEmpty(); } } 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 3e88afa..ef8d8c1 100644 --- a/src/main/java/com/yj/earth/business/controller/ModelLibraryController.java +++ b/src/main/java/com/yj/earth/business/controller/ModelLibraryController.java @@ -313,12 +313,17 @@ public class ModelLibraryController { // 循环遍历数据 for (ModelVo modelVo : modelVos) { if (modelVo.getModelType() != null) { - modelVo.setModelDataUrl("/modelLibrary/data/model/" + modelVo.getId() + "/" + modelVo.getModelType()); + String processedModelType = trimDot(modelVo.getModelType()); + modelVo.setModelDataUrl("/modelLibrary/data/model/" + modelVo.getId() + "/" + processedModelType); + modelVo.setModelType(processedModelType); } if (modelVo.getPosterType() != null) { - modelVo.setPosterDataUrl("/modelLibrary/data/poster/" + modelVo.getId() + "/" + modelVo.getPosterType()); + String processedPosterType = trimDot(modelVo.getPosterType()); + modelVo.setPosterDataUrl("/modelLibrary/data/poster/" + modelVo.getId() + "/" + processedPosterType); + modelVo.setPosterType(processedPosterType); } } + return ApiResponse.success(modelVos); } @@ -566,4 +571,12 @@ public class ModelLibraryController { // 若查询结果非空、说明分类存在 return existTypes != null && !existTypes.isEmpty(); } + + private String trimDot(String str) { + if (str == null) { + return null; + } + return str.startsWith(".") ? str.substring(1) : str; + } + } diff --git a/src/main/java/com/yj/earth/business/domain/Military.java b/src/main/java/com/yj/earth/business/domain/Military.java index b48a43b..5858dab 100644 --- a/src/main/java/com/yj/earth/business/domain/Military.java +++ b/src/main/java/com/yj/earth/business/domain/Military.java @@ -20,9 +20,7 @@ public class Military { @Schema(description = "军标类型") private String militaryType; @Schema(description = "军标数据") - private String data; - @Schema(description = "军标视图") - private String view; + private byte[] militaryData; @Schema(description = "创建时间") private LocalDateTime createdAt; @Schema(description = "更新时间") diff --git a/src/main/java/com/yj/earth/common/config/ServerConfig.java b/src/main/java/com/yj/earth/common/config/ServerConfig.java index 91eca24..34a2826 100644 --- a/src/main/java/com/yj/earth/common/config/ServerConfig.java +++ b/src/main/java/com/yj/earth/common/config/ServerConfig.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; + @Data @Component public class ServerConfig { diff --git a/src/main/java/com/yj/earth/common/util/FileCommonUtil.java b/src/main/java/com/yj/earth/common/util/FileCommonUtil.java index f787f88..0c17337 100644 --- a/src/main/java/com/yj/earth/common/util/FileCommonUtil.java +++ b/src/main/java/com/yj/earth/common/util/FileCommonUtil.java @@ -29,20 +29,20 @@ public class FileCommonUtil { public static MediaType getImageMediaType(String suffix) { String lowerSuffix = suffix.toLowerCase(); switch (lowerSuffix) { - case ".jpg": - case ".jpeg": + case "jpg": + case "jpeg": return MediaType.IMAGE_JPEG; - case ".glb": + case "glb": return MediaType.valueOf("model/gltf-binary"); - case ".png": + case "png": return MediaType.IMAGE_PNG; - case ".gif": + case "gif": return MediaType.IMAGE_GIF; - case ".bmp": + case "bmp": return MediaType.valueOf("image/bmp"); - case ".webp": + case "webp": return MediaType.valueOf("image/webp"); - case ".svg": + 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/SQLiteConverter.java b/src/main/java/com/yj/earth/common/util/SQLiteConverter.java index 4c02c0b..0bb6a35 100644 --- a/src/main/java/com/yj/earth/common/util/SQLiteConverter.java +++ b/src/main/java/com/yj/earth/common/util/SQLiteConverter.java @@ -1,274 +1,249 @@ +package com.yj.earth.common.util; + +import cn.hutool.core.lang.UUID; + import java.sql.*; -import java.util.UUID; +import java.time.LocalDateTime; public class SQLiteConverter { - // SQLite JDBC驱动 private static final String JDBC_DRIVER = "org.sqlite.JDBC"; + // 源数据库和目标数据库路径 + private String sourceDbPath; + private String targetDbPath; + // 批处理大小、可根据内存情况调整 + private static final int BATCH_SIZE = 100; - // 原始数据库路径 - private static final String ORIGINAL_DB_PATH = "jdbc:sqlite:D:\\YJEarth.model"; + public SQLiteConverter(String sourceDbPath, String targetDbPath) { + this.sourceDbPath = sourceDbPath; + this.targetDbPath = targetDbPath; + } - // 新数据库路径 - private static final String NEW_DB_PATH = "jdbc:sqlite:E:\\通用模型库.model"; + public void convert() { + Connection sourceConn = null; + Connection targetConn = null; + try { + // 加载驱动 + Class.forName(JDBC_DRIVER); + // 连接源数据库和目标数据库 + sourceConn = DriverManager.getConnection("jdbc:sqlite:" + sourceDbPath); + targetConn = DriverManager.getConnection("jdbc:sqlite:" + targetDbPath); + // 禁用自动提交、以便在出现错误时可以回滚 + targetConn.setAutoCommit(false); + // 创建目标表结构 + createTargetTables(targetConn); + // 复制并转换数据 + copyModeTypesData(sourceConn, targetConn); + copyModelsData(sourceConn, targetConn); + // 为model表添加索引 + createModelTableIndexes(targetConn); + // 提交事务 + targetConn.commit(); + System.out.println("数据库转换成功!"); + } catch (Exception e) { + e.printStackTrace(); + try { + if (targetConn != null) { + targetConn.rollback(); + System.out.println("转换失败、已回滚操作!"); + } + } catch (SQLException ex) { + ex.printStackTrace(); + } + } finally { + // 关闭连接 + try { + if (sourceConn != null) sourceConn.close(); + if (targetConn != null) targetConn.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + private void createTargetTables(Connection conn) throws SQLException { + System.out.println("开始创建目标表结构..."); + Statement stmt = conn.createStatement(); + String sql = """ + CREATE TABLE "model_type" ( + "id" TEXT, + "name" TEXT, + "parent_id" TEXT, + "tree_index" INTEGER, + "created_at" TEXT, + "updated_at" TEXT, + PRIMARY KEY ("id") + ); + """; + stmt.execute(sql); + sql = """ + CREATE TABLE "model" ( + "id" TEXT, + "model_type_id" TEXT, + "model_name" TEXT, + "model_type" TEXT, + "model_data" BLOB, + "poster_type" TEXT, + "poster_data" BLOB, + "created_at" TEXT, + "updated_at" TEXT, + PRIMARY KEY ("id") + ); + """; + stmt.execute(sql); + stmt.close(); + System.out.println("目标表结构创建完成"); + } + + /** + * 为 model 表的每个字段创建索引 + */ + private void createModelTableIndexes(Connection conn) throws SQLException { + System.out.println("开始为创建索引..."); + Statement stmt = conn.createStatement(); + + String sql = """ + CREATE INDEX idx_model_covering ON model( + model_type_id, + id, + model_name, + model_type, + poster_type, + created_at, + updated_at + ); + """; + stmt.execute(sql); + + stmt.close(); + System.out.println("model表索引创建完成"); + } + + private int getTotalRecords(Connection conn, String tableName) throws SQLException { + PreparedStatement stmt = conn.prepareStatement("SELECT COUNT(*) AS total FROM " + tableName); + ResultSet rs = stmt.executeQuery(); + int total = rs.next() ? rs.getInt("total") : 0; + rs.close(); + stmt.close(); + return total; + } + + private void copyModeTypesData(Connection sourceConn, Connection targetConn) throws SQLException { + int totalRecords = getTotalRecords(sourceConn, "mode_types"); + System.out.println("开始转换 mode_types 表数据、共" + totalRecords + "条记录"); + PreparedStatement sourceStmt = sourceConn.prepareStatement("SELECT * FROM mode_types"); + ResultSet rs = sourceStmt.executeQuery(); + PreparedStatement targetStmt = targetConn.prepareStatement( + "INSERT INTO model_type (id, name, parent_id, tree_index, created_at, updated_at) " + + "VALUES (?, ?, ?, ?, ?, ?)" + ); + int count = 0; + + while (rs.next()) { + targetStmt.setString(1, rs.getString("type_id")); + targetStmt.setString(2, rs.getString("type_name")); + targetStmt.setString(3, rs.getString("p_id")); + targetStmt.setInt(4, 0); + targetStmt.setObject(5, LocalDateTime.now()); + targetStmt.setObject(6, LocalDateTime.now()); + + // 添加到批处理 + targetStmt.addBatch(); + count++; + + // 每达到批处理大小或最后一条记录时执行批处理 + if (count % BATCH_SIZE == 0 || count == totalRecords) { + targetStmt.executeBatch(); + displayProgress(count, totalRecords, "mode_types 表"); + } + } + + System.out.println("\n成功转换 mode_types 表数据:" + count + "条记录"); + rs.close(); + sourceStmt.close(); + targetStmt.close(); + } + + private void copyModelsData(Connection sourceConn, Connection targetConn) throws SQLException { + int totalRecords = getTotalRecords(sourceConn, "models"); + System.out.println("开始转换 models 表数据、共" + totalRecords + "条记录"); + + // 对于大字段、使用向前滚动的结果集、避免缓存所有数据 + PreparedStatement sourceStmt = sourceConn.prepareStatement( + "SELECT * FROM models", + ResultSet.TYPE_FORWARD_ONLY, + ResultSet.CONCUR_READ_ONLY + ); + sourceStmt.setFetchSize(100); + ResultSet rs = sourceStmt.executeQuery(); + PreparedStatement targetStmt = targetConn.prepareStatement( + "INSERT INTO model (id, model_type_id, model_name, model_type, model_data, " + + "poster_type, poster_data, created_at, updated_at) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" + ); + + int count = 0; + while (rs.next()) { + targetStmt.setString(1, rs.getString("id")); + targetStmt.setString(2, rs.getString("p_id")); + targetStmt.setString(3, rs.getString("model_name")); + targetStmt.setString(4, rs.getString("model_type")); + byte[] dataBytes = rs.getBytes("data"); + if (dataBytes != null) { + targetStmt.setBytes(5, dataBytes); + } else { + targetStmt.setNull(5, Types.BLOB); + } + targetStmt.setString(6, rs.getString("poster_type")); + byte[] posterBytes = rs.getBytes("poster"); + if (posterBytes != null) { + targetStmt.setBytes(7, posterBytes); + } + targetStmt.setObject(8, LocalDateTime.now()); + targetStmt.setObject(9, LocalDateTime.now()); + + // 添加到批处理 + targetStmt.addBatch(); + count++; + // 执行批处理 + if (count % BATCH_SIZE == 0 || count == totalRecords) { + targetStmt.executeBatch(); + displayProgress(count, totalRecords, "models 表"); + } + } + + System.out.println("\n成功转换 models 表数据:" + count + "条记录"); + rs.close(); + sourceStmt.close(); + targetStmt.close(); + } + + /** + * 显示进度信息 + */ + private void displayProgress(int current, int total, String tableName) { + double percentage = (double) current / total * 100; + // 清除当前行并显示进度 + System.out.printf("\r%s: 已完成 %.1f%% (%d/%d条)", tableName, percentage, current, total); + } public static void main(String[] args) { - System.out.println("===== 开始数据库转换程序 ====="); - System.out.println("原始数据库: " + ORIGINAL_DB_PATH); - System.out.println("目标数据库: " + NEW_DB_PATH + "\n"); + // 源数据库路径 + String sourcePath = "F:\\公司通用模型库.model"; + // 目标数据库路径 + String targetPath = "F:\\通用模型库.model"; - // 使用try-with-resources自动管理连接资源 - try (Connection originalConn = DriverManager.getConnection(ORIGINAL_DB_PATH); - Connection newConn = DriverManager.getConnection(NEW_DB_PATH)) { + System.out.println("开始数据库转换..."); + System.out.println("源数据库: " + sourcePath); + System.out.println("目标数据库: " + targetPath); - System.out.println("✅ 成功连接到原始数据库"); - System.out.println("✅ 成功创建并连接到新数据库\n"); + long startTime = System.currentTimeMillis(); - // 在新数据库中创建表结构 - System.out.println("===== 开始创建新表结构 ====="); - createNewTables(newConn); - System.out.println("===== 新表结构创建完成 ====="); + // 创建转换器并执行转换 + SQLiteConverter converter = new SQLiteConverter(sourcePath, targetPath); + converter.convert(); - // 迁移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(); + long endTime = System.currentTimeMillis(); + double elapsedTime = (endTime - startTime) / 1000.0; + System.out.printf("转换完成、耗时: %.2f秒%n", elapsedTime); } } 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 93d2354..525dfdd 100644 --- a/src/main/java/com/yj/earth/common/util/SQLiteUtil.java +++ b/src/main/java/com/yj/earth/common/util/SQLiteUtil.java @@ -1,16 +1,21 @@ package com.yj.earth.common.util; +import org.apache.commons.dbcp2.BasicDataSource; + import java.sql.*; import java.util.*; import java.lang.reflect.*; -import java.util.Date; -import java.time.LocalDateTime; // 新增导入 -import java.time.format.DateTimeFormatter; // 新增导入 -import java.time.format.DateTimeParseException; // 新增导入 +import java.util.concurrent.ConcurrentHashMap; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +/** + * 优化版SQLite工具类 + */ public class SQLiteUtil { - // 加载 SQLite JDBC 驱动 + // 加载SQLite JDBC驱动(静态初始化) static { try { Class.forName("org.sqlite.JDBC"); @@ -19,140 +24,508 @@ public class SQLiteUtil { } } - // 统一日期格式(匹配数据库TEXT字段存储的格式:yyyy-MM-dd'T'HH:mm:ss.SSS) + // 统一日期格式(LocalDateTime) private static final DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME; + // 连接池缓存:key=数据库文件路径,value=DBCP2数据源(支持多连接复用) + private static final Map DATA_SOURCE_POOL = new ConcurrentHashMap<>(); + + // 字段缓存:缓存类的字段映射(避免反射重复开销) + private static final Map, Map> FIELD_CACHE = new ConcurrentHashMap<>(); + + + // ========================== 连接池核心方法 ========================== + /** - * 根据数据库文件绝对路径获取连接 + * 从DBCP2连接池获取连接(自动复用/创建连接) + * + * @param dbFilePath 数据库文件绝对路径 + * @return 线程安全的Connection(使用后需通过try-with-resources自动归还) */ public static Connection getConnection(String dbFilePath) throws SQLException { - String url = "jdbc:sqlite:" + dbFilePath; - return DriverManager.getConnection(url); + if (dbFilePath == null || dbFilePath.trim().isEmpty()) { + throw new IllegalArgumentException("数据库文件路径不能为空"); + } + // 不存在则创建数据源,存在则直接从池获取连接 + BasicDataSource dataSource = DATA_SOURCE_POOL.computeIfAbsent(dbFilePath, SQLiteUtil::createDataSource); + return dataSource.getConnection(); } /** - * 执行查询并返回单个对象 + * 创建DBCP2数据源(配置SQLite性能参数) + * + * @param dbFilePath 数据库文件路径 + * @return 配置优化后的BasicDataSource */ - public static T queryForObject(String dbFilePath, String sql, List params, Class clazz) throws SQLException, IllegalAccessException, InstantiationException { + private static BasicDataSource createDataSource(String dbFilePath) { + BasicDataSource dataSource = new BasicDataSource(); + // 1. 基础JDBC配置 + dataSource.setDriverClassName("org.sqlite.JDBC"); + dataSource.setUrl("jdbc:sqlite:" + dbFilePath); + + // 2. 连接池核心参数(根据并发量调整,SQLite不建议过多连接) + dataSource.setMaxTotal(30); // 最大连接数:20-50(根据服务器CPU/内存调整) + dataSource.setMaxIdle(15); // 最大空闲连接:保留部分连接避免频繁创建 + dataSource.setMinIdle(5); // 最小空闲连接:保证基础并发响应速度 + dataSource.setTimeBetweenEvictionRunsMillis(60000); // 连接检测间隔:1分钟 + dataSource.setMinEvictableIdleTimeMillis(300000); // 连接空闲超时:5分钟(清理长期空闲连接) + + // 3. 连接有效性验证(避免使用失效连接) + dataSource.setTestOnBorrow(true); // 借连接时验证 + dataSource.setTestOnReturn(false); // 还连接时不验证(减少开销) + dataSource.setValidationQuery("SELECT 1"); // 轻量验证SQL(SQLite支持) + dataSource.setValidationQueryTimeout(2); // 验证超时:2秒 + + // 4. PreparedStatement缓存(减少SQL解析开销) + dataSource.setPoolPreparedStatements(true); + dataSource.setMaxOpenPreparedStatements(100); // 最大缓存100个PreparedStatement + + // 5. SQLite底层性能优化(关键!提升并发能力) + try (Connection conn = dataSource.getConnection(); + Statement stmt = conn.createStatement()) { + stmt.execute("PRAGMA journal_mode=WAL;"); // 启用WAL模式:支持多读者+单写者(核心优化) + stmt.execute("PRAGMA cache_size=-20000;"); // 页面缓存20MB(负号表示KB单位,内存足可调大) + stmt.execute("PRAGMA synchronous=NORMAL;"); // 同步级别:平衡性能与安全(崩溃最多丢WAL日志) + stmt.execute("PRAGMA temp_store=MEMORY;"); // 临时表/索引存内存(减少磁盘IO) + stmt.execute("PRAGMA busy_timeout=2000;"); // 忙等待超时:2秒(避免瞬时并发锁等待) + } catch (SQLException e) { + throw new RuntimeException("初始化SQLite数据源失败(路径:" + dbFilePath + ")", e); + } + + return dataSource; + } + + /** + * 关闭指定数据库的数据源(释放所有连接) + * + * @param dbFilePath 数据库文件路径 + */ + public static void closeDataSource(String dbFilePath) { + if (dbFilePath == null) return; + BasicDataSource dataSource = DATA_SOURCE_POOL.remove(dbFilePath); + if (dataSource != null) { + try { + dataSource.close(); // DBCP2会自动关闭所有活跃/空闲连接 + } catch (SQLException e) { + System.err.println("关闭SQLite数据源失败(路径:" + dbFilePath + "):" + e.getMessage()); + } + } + } + + /** + * 关闭所有数据源(JVM退出时自动调用) + */ + public static void closeAllDataSources() { + for (BasicDataSource dataSource : DATA_SOURCE_POOL.values()) { + try { + dataSource.close(); + } catch (SQLException e) { + System.err.println("关闭SQLite数据源失败:" + e.getMessage()); + } + } + // 清理缓存(避免内存泄漏) + DATA_SOURCE_POOL.clear(); + FIELD_CACHE.clear(); + } + + // JVM关闭钩子:确保程序退出时释放所有数据源资源 + static { + Runtime.getRuntime().addShutdownHook(new Thread(SQLiteUtil::closeAllDataSources)); + } + + + // ========================== 数据查询核心方法 ========================== + + /** + * 执行查询并返回单个对象(优化版:连接池+字段缓存) + * + * @param dbFilePath 数据库路径 + * @param sql 查询SQL + * @param params SQL参数 + * @param clazz 返回对象类型 + * @return 单个对象(无结果返回null) + */ + public static T queryForObject(String dbFilePath, String sql, List params, Class clazz) throws SQLException { List results = queryForList(dbFilePath, sql, params, clazz); return results.isEmpty() ? null : results.get(0); } /** - * 执行查询并返回对象列表 + * 执行查询并返回对象列表(优化版:预构建字段映射+资源自动回收) + * + * @param dbFilePath 数据库路径 + * @param sql 查询SQL + * @param params SQL参数 + * @param clazz 返回对象类型 + * @return 结果列表(无结果返回空列表) */ - 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 { List resultList = new ArrayList<>(); + if (sql == null || sql.trim().isEmpty()) { + throw new IllegalArgumentException("查询SQL不能为空"); + } - // 使用try-with-resources确保资源自动关闭 + // 预加载字段映射(缓存生效,避免重复反射) + Map fieldMap = getFieldMap(clazz); + + // try-with-resources:自动关闭Connection、PreparedStatement、ResultSet try (Connection conn = getConnection(dbFilePath); - PreparedStatement pstmt = createPreparedStatement(conn, sql, params)) { + PreparedStatement pstmt = createPreparedStatement(conn, sql, params); + ResultSet rs = pstmt.executeQuery()) { - // 执行查询 - try (ResultSet rs = pstmt.executeQuery()) { - ResultSetMetaData metaData = rs.getMetaData(); - int columnCount = metaData.getColumnCount(); + ResultSetMetaData metaData = rs.getMetaData(); + int columnCount = metaData.getColumnCount(); + // 预构建「列名-字段」映射(一次构建,循环复用) + ColumnFieldMapping[] mappings = buildColumnFieldMappings(metaData, columnCount, fieldMap); - // 处理结果集 - while (rs.next()) { - T obj = clazz.newInstance(); - for (int i = 1; i <= columnCount; i++) { - String columnName = metaData.getColumnName(i); - Object value = rs.getObject(i); - - // 设置对象属性(自动处理类型转换) - setFieldValue(obj, columnName, value); + // 处理结果集(反射赋值) + while (rs.next()) { + try { + T obj = clazz.getDeclaredConstructor().newInstance(); // 无参构造器(需确保类有) + for (int i = 0; i < columnCount; i++) { + ColumnFieldMapping mapping = mappings[i]; + if (mapping.field != null) { + Object value = rs.getObject(i + 1); // ResultSet列索引从1开始 + setFieldValueOptimized(obj, mapping.field, value); + } } resultList.add(obj); + } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | + InvocationTargetException e) { + throw new SQLException("创建对象实例失败(类型:" + clazz.getName() + ")", e); } } + } catch (SQLException e) { + // 异常时关闭当前数据源(避免后续请求使用异常连接) + closeDataSource(dbFilePath); + throw new SQLException("执行查询失败(SQL:" + sql + ")", e); } return resultList; } /** - * 执行增删改SQL语句 + * 执行增删改SQL(优化版:连接池+自动提交) + * + * @param dbFilePath 数据库路径 + * @param sql 增删改SQL + * @param params SQL参数 + * @return 影响行数 */ public static int executeUpdate(String dbFilePath, String sql, List params) throws SQLException { + if (sql == null || sql.trim().isEmpty()) { + throw new IllegalArgumentException("执行SQL不能为空"); + } + try (Connection conn = getConnection(dbFilePath); PreparedStatement pstmt = createPreparedStatement(conn, sql, params)) { return pstmt.executeUpdate(); + } catch (SQLException e) { + closeDataSource(dbFilePath); + throw new SQLException("执行增删改失败(SQL:" + sql + ")", e); } } /** - * 创建并设置参数化的PreparedStatement(关键:处理LocalDateTime转String) + * 执行计数查询(优化版:轻量结果处理) + * + * @param dbFilePath 数据库路径 + * @param sql 计数SQL(如SELECT COUNT(*) ...) + * @param params SQL参数 + * @return 计数结果(无结果返回0) */ - private static PreparedStatement createPreparedStatement(Connection conn, String sql, List params) - throws SQLException { - PreparedStatement pstmt = conn.prepareStatement(sql); - - if (params != null && !params.isEmpty()) { - int index = 1; - for (Object value : params) { - // 新增:LocalDateTime类型转为String、适配SQLite的TEXT字段 - if (value instanceof LocalDateTime) { - String dateStr = ((LocalDateTime) value).format(LOCAL_DATE_TIME_FORMATTER); - pstmt.setObject(index++, dateStr); - } - // 新增:byte[]类型显式指定为BLOB(避免SQLite自动转换异常) - else if (value instanceof byte[]) { - pstmt.setBytes(index++, (byte[]) value); - } else { - pstmt.setObject(index++, value); - } - } + public static int queryForCount(String dbFilePath, String sql, List params) throws SQLException { + if (sql == null || sql.trim().isEmpty()) { + throw new IllegalArgumentException("计数SQL不能为空"); } + try (Connection conn = getConnection(dbFilePath); + PreparedStatement pstmt = createPreparedStatement(conn, sql, params); + ResultSet rs = pstmt.executeQuery()) { + return rs.next() ? rs.getInt(1) : 0; + } catch (SQLException e) { + closeDataSource(dbFilePath); + throw new SQLException("执行计数查询失败(SQL:" + sql + ")", e); + } + } + + /** + * 执行DDL语句(创建表/索引等) + * + * @param dbFilePath 数据库路径 + * @param sql DDL语句 + * @param params SQL参数(可选,如动态表名) + */ + public static void executeDDL(String dbFilePath, String sql, List params) throws SQLException { + if (dbFilePath == null || dbFilePath.trim().isEmpty()) { + throw new IllegalArgumentException("数据库文件路径不能为空"); + } + if (sql == null || sql.trim().isEmpty()) { + throw new IllegalArgumentException("DDL语句不能为空"); + } + + try (Connection conn = getConnection(dbFilePath); + PreparedStatement pstmt = createPreparedStatement(conn, sql, params)) { + pstmt.execute(); + } catch (SQLException e) { + closeDataSource(dbFilePath); + throw new SQLException("执行DDL失败(SQL:" + sql + ",路径:" + dbFilePath + ")", e); + } + } + + /** + * 重载:无参数的DDL执行 + */ + public static void executeDDL(String dbFilePath, String sql) throws SQLException { + executeDDL(dbFilePath, sql, null); + } + + + // ========================== 工具辅助方法 ========================== + + /** + * 创建PreparedStatement(复用Connection,避免重复获取) + * + * @param conn 已获取的Connection(从连接池来) + * @param sql SQL语句 + * @param params 参数列表 + * @return 配置好的PreparedStatement + */ + private static PreparedStatement createPreparedStatement(Connection conn, String sql, List params) throws SQLException { + PreparedStatement pstmt = conn.prepareStatement(sql); + // 设置SQL参数(处理特殊类型如LocalDateTime、byte[]) + if (params != null && !params.isEmpty()) { + for (int i = 0; i < params.size(); i++) { + setPreparedStatementValue(pstmt, i + 1, params.get(i)); + } + } return pstmt; } /** - * 通过反射设置对象的字段值 + * 设置PreparedStatement参数(处理SQLite特殊类型映射) + * + * @param pstmt PreparedStatement对象 + * @param index 参数索引(从1开始) + * @param value 参数值 */ - private static void setFieldValue(Object obj, String columnName, Object value) { + private static void setPreparedStatementValue(PreparedStatement pstmt, int index, Object value) throws SQLException { + if (value == null) { + pstmt.setNull(index, Types.NULL); + return; + } + + // 特殊类型处理 + if (value instanceof LocalDateTime) { + // LocalDateTime转字符串(SQLite无原生DateTime类型) + String dateStr = ((LocalDateTime) value).format(LOCAL_DATE_TIME_FORMATTER); + pstmt.setString(index, dateStr); + } else if (value instanceof byte[]) { + // 二进制数据(如BLOB) + pstmt.setBytes(index, (byte[]) value); + } else if (value instanceof java.util.Date) { + // Date转Timestamp(兼容SQLite时间处理) + pstmt.setTimestamp(index, new Timestamp(((java.util.Date) value).getTime())); + } else { + // 通用类型(依赖JDBC自动映射) + pstmt.setObject(index, value); + } + } + + /** + * 预构建「列名-字段」映射(支持下划线转驼峰) + * + * @param metaData 结果集元数据 + * @param columnCount 列数 + * @param fieldMap 类字段缓存 + * @return 映射数组(与列顺序对应) + */ + private static ColumnFieldMapping[] buildColumnFieldMappings(ResultSetMetaData metaData, int columnCount, Map fieldMap) throws SQLException { + ColumnFieldMapping[] mappings = new ColumnFieldMapping[columnCount]; + for (int i = 0; i < columnCount; i++) { + String columnName = metaData.getColumnName(i + 1); // 列名(如military_name) + ColumnFieldMapping mapping = new ColumnFieldMapping(); + mapping.columnName = columnName; + + // 1. 直接匹配字段名(如列名=字段名) + Field field = fieldMap.get(columnName); + // 2. 下划线转驼峰匹配(如military_name → militaryName) + if (field == null) { + String camelCaseName = underscoreToCamelCase(columnName); + field = fieldMap.get(camelCaseName); + } + + mapping.field = field; + mappings[i] = mapping; + } + return mappings; + } + + /** + * 获取类的字段缓存(含父类字段,一次反射永久缓存) + * + * @param clazz 目标类 + * @return 字段名→Field的映射(不可修改) + */ + private static Map getFieldMap(Class clazz) { + return FIELD_CACHE.computeIfAbsent(clazz, key -> { + Map fieldMap = new HashMap<>(); + // 遍历当前类及所有父类(直到Object) + Class currentClass = clazz; + while (currentClass != null && currentClass != Object.class) { + Field[] fields = currentClass.getDeclaredFields(); + for (Field field : fields) { + field.setAccessible(true); // 突破访问权限(private字段可赋值) + fieldMap.put(field.getName(), field); + } + currentClass = currentClass.getSuperclass(); + } + return Collections.unmodifiableMap(fieldMap); // 避免外部修改缓存 + }); + } + + /** + * 优化的字段赋值(处理类型转换,避免重复异常捕获) + * + * @param obj 目标对象 + * @param field 字段 + * @param value 待赋值的值 + */ + private static void setFieldValueOptimized(Object obj, Field field, Object value) { + if (value == null) return; + try { - Field field = findField(obj.getClass(), columnName); - if (field != null) { - field.setAccessible(true); - Object convertedValue = convertValue(field.getType(), value); + Class fieldType = field.getType(); + // 类型匹配直接赋值(无转换开销) + if (fieldType.isInstance(value)) { + field.set(obj, value); + return; + } + // 类型不匹配时转换(支持基础类型、时间、二进制等) + Object convertedValue = convertValueOptimized(fieldType, value); + if (convertedValue != null) { field.set(obj, convertedValue); } } catch (IllegalAccessException e) { - System.err.println("警告: 无法设置字段 " + columnName + " 的值 - " + e.getMessage()); + System.err.println("警告:字段赋值失败(字段:" + field.getName() + ",值类型:" + value.getClass().getName() + "):" + e.getMessage()); } } /** - * 查找字段、支持下划线命名转驼峰命名(如created_at → createdAt) + * 优化的类型转换(支持常见SQLite类型→Java类型) + * + * @param targetType 目标类型(字段类型) + * @param value 原始值(ResultSet获取的值) + * @return 转换后的值(无法转换返回null) */ - private static Field findField(Class clazz, String columnName) { - // 1. 直接匹配字段名(如数据库列名与字段名一致) + private static Object convertValueOptimized(Class targetType, Object value) { + // 基础数值类型转换 + if (targetType == int.class || targetType == Integer.class) { + if (value instanceof Number) return ((Number) value).intValue(); + if (value instanceof String) return parseInteger((String) value); + } else if (targetType == long.class || targetType == Long.class) { + if (value instanceof Number) return ((Number) value).longValue(); + if (value instanceof String) return parseLong((String) value); + } else if (targetType == double.class || targetType == Double.class) { + if (value instanceof Number) return ((Number) value).doubleValue(); + if (value instanceof String) return parseDouble((String) value); + } else if (targetType == boolean.class || targetType == Boolean.class) { + if (value instanceof Number) return ((Number) value).intValue() != 0; + if (value instanceof String) return "true".equalsIgnoreCase((String) value) || "1".equals(value); + } + // 字符串类型 + else if (targetType == String.class) { + return value.toString(); + } + // 时间类型(LocalDateTime) + else if (targetType == LocalDateTime.class) { + return convertToLocalDateTime(value); + } + // 时间类型(Date) + else if (targetType == java.util.Date.class) { + return convertToDate(value); + } + // 二进制类型(byte[]) + else if (targetType == byte[].class) { + return convertToByteArray(value); + } + + // 不支持的类型转换(打印警告) + System.err.println("警告:不支持的类型转换(目标类型:" + targetType.getName() + ",原始值类型:" + value.getClass().getName() + ")"); + return null; + } + + // ========================== 类型转换辅助方法 ========================== + private static Integer parseInteger(String value) { try { - return clazz.getDeclaredField(columnName); - } catch (NoSuchFieldException e) { - // 2. 下划线转驼峰后匹配(如created_at → createdAt) - String camelCaseName = underscoreToCamelCase(columnName); + return Integer.parseInt(value.trim()); + } catch (NumberFormatException e) { + System.err.println("警告:字符串转Integer失败(值:" + value + ")"); + return null; + } + } + + private static Long parseLong(String value) { + try { + return Long.parseLong(value.trim()); + } catch (NumberFormatException e) { + System.err.println("警告:字符串转Long失败(值:" + value + ")"); + return null; + } + } + + private static Double parseDouble(String value) { + try { + return Double.parseDouble(value.trim()); + } catch (NumberFormatException e) { + System.err.println("警告:字符串转Double失败(值:" + value + ")"); + return null; + } + } + + private static LocalDateTime convertToLocalDateTime(Object value) { + if (value instanceof String) { try { - return clazz.getDeclaredField(camelCaseName); - } catch (NoSuchFieldException e1) { - // 3. 递归查找父类(支持继承场景) - if (clazz.getSuperclass() != null) { - return findField(clazz.getSuperclass(), columnName); - } + return LocalDateTime.parse((String) value, LOCAL_DATE_TIME_FORMATTER); + } catch (DateTimeParseException e) { + System.err.println("警告:字符串转LocalDateTime失败(值:" + value + ")"); return null; } + } else if (value instanceof Timestamp) { + return ((Timestamp) value).toLocalDateTime(); } + return null; + } + + private static java.util.Date convertToDate(Object value) { + if (value instanceof Timestamp) { + return new java.util.Date(((Timestamp) value).getTime()); + } else if (value instanceof LocalDateTime) { + return java.util.Date.from(((LocalDateTime) value).atZone(java.time.ZoneId.systemDefault()).toInstant()); + } + return null; + } + + private static byte[] convertToByteArray(Object value) { + if (value instanceof Blob) { + Blob blob = (Blob) value; + try { + return blob.getBytes(1, (int) blob.length()); + } catch (SQLException e) { + System.err.println("警告:Blob转byte[]失败:" + e.getMessage()); + } + } + return null; } /** - * 下划线命名转驼峰命名(工具方法) + * 下划线命名转驼峰命名(如military_type → militaryType) + * + * @param str 下划线字符串 + * @return 驼峰字符串 */ private static String underscoreToCamelCase(String str) { - if (str == null || str.isEmpty()) { - return str; - } + if (str == null || str.isEmpty()) return str; StringBuilder sb = new StringBuilder(); boolean nextUpperCase = false; for (char c : str.toCharArray()) { @@ -163,7 +536,6 @@ public class SQLiteUtil { sb.append(Character.toUpperCase(c)); nextUpperCase = false; } else { - // 修正:首字母小写(如CREATED_AT → createdAt、而非CreatedAt) sb.append(Character.toLowerCase(c)); } } @@ -171,136 +543,26 @@ public class SQLiteUtil { return sb.toString(); } + + // ========================== 内部辅助类 ========================== + /** - * 核心:类型转换(新增LocalDateTime解析、优化BLOB适配) + * 列-字段映射模型(一次性构建,减少循环内计算) */ - private static Object convertValue(Class targetType, Object value) { - if (value == null) { - return null; - } - - // 类型已匹配、直接返回 - if (targetType.isInstance(value)) { - return value; - } - - // 1. 数字类型转换(int/long/double等) - if (targetType == Integer.class || targetType == int.class) { - return ((Number) value).intValue(); - } else if (targetType == Long.class || targetType == long.class) { - return ((Number) value).longValue(); - } else if (targetType == Double.class || targetType == double.class) { - return ((Number) value).doubleValue(); - } else if (targetType == Float.class || targetType == float.class) { - return ((Number) value).floatValue(); - } - - // 2. 布尔类型转换(支持数字/字符串转布尔) - else if (targetType == Boolean.class || targetType == boolean.class) { - if (value instanceof Number) { - return ((Number) value).intValue() != 0; - } else if (value instanceof String) { - return "true".equalsIgnoreCase((String) value); - } - } - - // 3. 字符串类型转换(所有类型转String) - else if (targetType == String.class) { - return value.toString(); - } - - // 4. 日期类型转换(java.util.Date) - else if (targetType == Date.class) { - if (value instanceof Timestamp) { - return new Date(((Timestamp) value).getTime()); - } else if (value instanceof LocalDateTime) { - // 支持LocalDateTime转Date(如需) - return Date.from(((LocalDateTime) value).atZone(java.time.ZoneId.systemDefault()).toInstant()); - } - } - - // 5. 新增:LocalDateTime类型转换(SQLite TEXT → Java LocalDateTime) - else if (targetType == LocalDateTime.class) { - if (value instanceof String) { - try { - // 解析数据库存储的ISO格式字符串(如"2025-09-18T17:30:27.143") - return LocalDateTime.parse((String) value, LOCAL_DATE_TIME_FORMATTER); - } catch (DateTimeParseException e) { - System.err.println("警告: 日期解析失败、字符串=" + value + "、格式应为yyyy-MM-dd'T'HH:mm:ss.SSS - " + e.getMessage()); - return null; - } - } else if (value instanceof Timestamp) { - // 兼容Timestamp类型(如其他数据库迁移场景) - return ((Timestamp) value).toLocalDateTime(); - } - } - - // 6. 新增:byte[]类型转换(SQLite BLOB → Java byte[]) - else if (targetType == byte[].class && value instanceof Blob) { - Blob blob = (Blob) value; - try { - return blob.getBytes(1, (int) blob.length()); - } catch (SQLException e) { - System.err.println("警告: BLOB转byte[]失败 - " + e.getMessage()); - return null; - } - } - - // 无法转换时返回原始值(避免崩溃、打印警告) - System.err.println("警告: 不支持的类型转换、目标类型=" + targetType.getName() + "、原始值类型=" + value.getClass().getName()); - return value; + private static class ColumnFieldMapping { + String columnName; // 数据库列名 + Field field; // 对应的Java字段 } - /** - * 执行DDL语句(CREATE, ALTER, DROP等) - */ - private static void executeDDL(String dbFilePath, String sql, List params) throws SQLException { - try (Connection conn = getConnection(dbFilePath); - PreparedStatement pstmt = createPreparedStatement(conn, sql, params)) { - pstmt.execute(); - } - } + + // ========================== 初始化表结构方法(保留原逻辑) ========================== /** - * 执行无参数的DDL语句 + * 初始化模型相关表(model_type + model) */ - public static void executeDDL(String dbFilePath, String sql) { - try { - executeDDL(dbFilePath, sql, null); - } catch (SQLException e) { - throw new RuntimeException("执行DDL语句失败、SQL=" + sql, e); - } - } - - /** - * 执行查询并返回count结果 - */ - public static int queryForCount(String dbFilePath, String sql, List params) throws SQLException { - try (Connection conn = getConnection(dbFilePath); - PreparedStatement pstmt = createPreparedStatement(conn, sql, params); - ResultSet rs = pstmt.executeQuery()) { - - if (rs.next()) { - return rs.getInt(1); - } - return 0; - } - } - - /** - * 执行无参数的查询并返回count结果 - */ - public static int queryForCount(String dbFilePath, String sql) throws SQLException { - return queryForCount(dbFilePath, sql, null); - } - - /** - * 初始化数据库表 - */ - public static void initializationModel(String modelPath) { - // 创建模型类型表 + public static void initializationModel(String modelPath) throws SQLException { String sql = """ - CREATE TABLE "model_type" ( + CREATE TABLE IF NOT EXISTS "model_type" ( "id" TEXT, "name" TEXT, "parent_id" TEXT, @@ -312,9 +574,8 @@ public class SQLiteUtil { """; executeDDL(modelPath, sql); - // 创建模型表 sql = """ - CREATE TABLE "model" ( + CREATE TABLE IF NOT EXISTS "model" ( "id" TEXT, "model_type_id" TEXT, "model_name" TEXT, @@ -331,12 +592,11 @@ public class SQLiteUtil { } /** - * 初始化数据库表 + * 初始化军标相关表(military_type + military) */ - public static void initializationMilitary(String modelPath) { - // 创建军标类型表 + public static void initializationMilitary(String militaryPath) throws SQLException { String sql = """ - CREATE TABLE "military_type" ( + CREATE TABLE IF NOT EXISTS "military_type" ( "id" TEXT, "name" TEXT, "parent_id" TEXT, @@ -346,29 +606,29 @@ public class SQLiteUtil { PRIMARY KEY ("id") ); """; - executeDDL(modelPath, sql); + executeDDL(militaryPath, sql); - // 创建军标表 sql = """ - CREATE TABLE "military" ( + CREATE TABLE IF NOT EXISTS "military" ( "id" TEXT, "military_type_id" TEXT, "military_name" TEXT, "military_type" TEXT, - "data" TEXT, - "view" TEXT, + "military_data" BLOB, "created_at" TEXT, "updated_at" TEXT, PRIMARY KEY ("id") ); """; - executeDDL(modelPath, sql); + executeDDL(militaryPath, sql); } - public static void initializationIcon(String iconPath) { - // 创建图标类型表 + /** + * 初始化图标相关表(icon_type + icon) + */ + public static void initializationIcon(String iconPath) throws SQLException { String sql = """ - CREATE TABLE "icon_type" ( + CREATE TABLE IF NOT EXISTS "icon_type" ( "id" TEXT, "name" TEXT, "parent_id" TEXT, @@ -380,9 +640,8 @@ public class SQLiteUtil { """; executeDDL(iconPath, sql); - // 创建图标表 sql = """ - CREATE TABLE "icon" ( + CREATE TABLE IF NOT EXISTS "icon" ( "id" TEXT, "icon_type_id" TEXT, "icon_name" TEXT, diff --git a/src/main/java/com/yj/earth/vo/MilitaryDataVo.java b/src/main/java/com/yj/earth/vo/MilitaryDataVo.java new file mode 100644 index 0000000..762e767 --- /dev/null +++ b/src/main/java/com/yj/earth/vo/MilitaryDataVo.java @@ -0,0 +1,12 @@ +package com.yj.earth.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +@Data +public class MilitaryDataVo { + @Schema(description = "军标名称") + private String militaryName; + @Schema(description = "军标数据") + private byte[] militaryData; +} diff --git a/src/main/java/com/yj/earth/vo/MilitaryVo.java b/src/main/java/com/yj/earth/vo/MilitaryVo.java index a39740e..ebd5695 100644 --- a/src/main/java/com/yj/earth/vo/MilitaryVo.java +++ b/src/main/java/com/yj/earth/vo/MilitaryVo.java @@ -2,9 +2,29 @@ package com.yj.earth.vo; import com.yj.earth.business.domain.Military; 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 MilitaryVo extends Military { + @Schema(description = "军标类型名称") private String militaryTypeName; + @Schema(description = "军标数据URL") + private String militaryDataUrl; + @Schema(description = "主键") + private String id; + @Schema(description = "军标类型ID") + private String militaryTypeId; + @Schema(description = "军标名称") + private String militaryName; + @Schema(description = "军标类型") + private String militaryType; + @Schema(description = "军标数据") + private byte[] militaryData; + @Schema(description = "创建时间") + private LocalDateTime createdAt; + @Schema(description = "更新时间") + private LocalDateTime updatedAt; }