模型库、矢量文件
This commit is contained in:
		| @ -7,6 +7,7 @@ import org.springframework.stereotype.Component; | ||||
|  | ||||
| import javax.annotation.PostConstruct; | ||||
|  | ||||
|  | ||||
| @Data | ||||
| @Component | ||||
| public class ServerConfig { | ||||
|  | ||||
| @ -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; | ||||
|  | ||||
| @ -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); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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<String, BasicDataSource> DATA_SOURCE_POOL = new ConcurrentHashMap<>(); | ||||
|  | ||||
|     // 字段缓存:缓存类的字段映射(避免反射重复开销) | ||||
|     private static final Map<Class<?>, Map<String, Field>> 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> T queryForObject(String dbFilePath, String sql, List<Object> params, Class<T> 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> T queryForObject(String dbFilePath, String sql, List<Object> params, Class<T> clazz) throws SQLException { | ||||
|         List<T> 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 <T> List<T> queryForList(String dbFilePath, String sql, List<Object> params, Class<T> clazz) throws SQLException, IllegalAccessException, InstantiationException { | ||||
|     public static <T> List<T> queryForList(String dbFilePath, String sql, List<Object> params, Class<T> clazz) throws SQLException { | ||||
|         List<T> resultList = new ArrayList<>(); | ||||
|         if (sql == null || sql.trim().isEmpty()) { | ||||
|             throw new IllegalArgumentException("查询SQL不能为空"); | ||||
|         } | ||||
|  | ||||
|         // 使用try-with-resources确保资源自动关闭 | ||||
|         // 预加载字段映射(缓存生效,避免重复反射) | ||||
|         Map<String, Field> 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<Object> 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<Object> 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<Object> 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<Object> 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<Object> 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<String, Field> 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<String, Field> getFieldMap(Class<?> clazz) { | ||||
|         return FIELD_CACHE.computeIfAbsent(clazz, key -> { | ||||
|             Map<String, Field> 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<Object> 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<Object> 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, | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 ZZX9599
					ZZX9599