From 8f40542ab0a48d3a70a036f7bd118f777ad5b934 Mon Sep 17 00:00:00 2001 From: tzdwindows 7 <3076584115@qq.com> Date: Tue, 7 Oct 2025 12:38:53 +0800 Subject: [PATCH] =?UTF-8?q?feat(browser):=20=E6=B7=BB=E5=8A=A0=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=BA=93=E7=AE=A1=E7=90=86=E5=B7=A5=E5=85=B7=E5=92=8C?= =?UTF-8?q?JS=E5=AF=B9=E8=AF=9D=E6=A1=86=E5=A4=84=E7=90=86-=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E4=BA=86=E6=B5=8F=E8=A7=88=E5=99=A8=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E4=B8=AD=E7=9A=84JavaScript=20alert=E5=BC=B9=E7=AA=97=E6=8B=A6?= =?UTF-8?q?=E6=88=AA=E4=B8=8E=E5=A4=84=E7=90=86=20-=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E6=95=B0=E6=8D=AE=E5=BA=93=E8=BF=9E=E6=8E=A5=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=99=A8=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A=E7=A7=8D?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=BA=93=E7=B1=BB=E5=9E=8B=EF=BC=88MySQL?= =?UTF-8?q?=E3=80=81PostgreSQL=E3=80=81SQLite=E3=80=81Oracle=E3=80=81H2?= =?UTF-8?q?=EF=BC=89=20-=20=E5=BC=80=E5=8F=91=E4=BA=86=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E7=90=86=E5=B7=A5=E5=85=B7=E7=9A=84=E5=89=8D?= =?UTF-8?q?=E7=AB=AF=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=8C=85=E5=90=AB=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E9=85=8D=E7=BD=AE=E3=80=81=E6=9F=A5=E8=AF=A2=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E5=99=A8=E5=92=8C=E7=BB=93=E6=9E=9C=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=20-=20=E6=94=AF=E6=8C=81=E6=9C=AC=E5=9C=B0=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E5=BA=93=E5=88=9B=E5=BB=BA=E4=B8=8E=E7=A4=BA=E4=BE=8B=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=88=9D=E5=A7=8B=E5=8C=96=20-=20=E6=8F=90=E4=BE=9B?= =?UTF-8?q?=E4=BA=86=E6=95=B0=E6=8D=AE=E5=BA=93=E8=A1=A8=E7=BB=93=E6=9E=84?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E5=92=8C=E5=9F=BA=E7=A1=80SQL=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=8A=9F=E8=83=BD-=20=E5=A2=9E=E5=8A=A0=E4=BA=86?= =?UTF-8?q?=E6=9A=97=E8=89=B2=E4=B8=BB=E9=A2=98=E5=88=87=E6=8D=A2=E5=92=8C?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E5=BC=8F=E5=B8=83=E5=B1=80=E8=AE=BE=E8=AE=A1?= =?UTF-8?q?=20-=20=E9=9B=86=E6=88=90=E4=BA=86=E4=BA=8B=E4=BB=B6=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E9=9D=A2=E6=9D=BF=E7=94=A8=E4=BA=8E=E8=B0=83=E8=AF=95?= =?UTF-8?q?=E5=92=8C=E7=8A=B6=E6=80=81=E8=B7=9F=E8=B8=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 + javascript/DatabaseTool.html | 793 +++++++++++++ .../innovators/box/Log4j2OutputStream.java | 106 +- .../innovators/box/browser/BrowserWindow.java | 20 + .../box/browser/BrowserWindowJDialog.java | 22 + .../box/browser/MainApplication.java | 1022 +++++++++++++++++ .../util/DatabaseConnectionManager.java | 397 +++++++ .../register/RegistrationSettingsItem.java | 57 +- .../box/register/RegistrationTool.java | 10 + .../innovators/box/window/MainWindow.java | 10 +- .../JarApiViewer/database_dark.png | Bin 0 -> 16127 bytes .../resources/icons/programming/database.png | Bin 0 -> 15205 bytes .../icons/programming/database_dark.png | Bin 0 -> 16127 bytes 13 files changed, 2404 insertions(+), 38 deletions(-) create mode 100644 javascript/DatabaseTool.html create mode 100644 src/main/java/com/axis/innovators/box/browser/util/DatabaseConnectionManager.java create mode 100644 src/main/resources/icons/programming/JarApiViewer/database_dark.png create mode 100644 src/main/resources/icons/programming/database.png create mode 100644 src/main/resources/icons/programming/database_dark.png diff --git a/build.gradle b/build.gradle index 97e7f44..b22e124 100644 --- a/build.gradle +++ b/build.gradle @@ -103,6 +103,11 @@ dependencies { implementation 'me.friwi:jcefmaven:122.1.10' implementation 'com.alphacephei:vosk:0.3.45' implementation 'net.java.dev.jna:jna:5.13.0' + implementation 'com.h2database:h2:2.2.220' + implementation 'org.xerial:sqlite-jdbc:3.41.2.1' + implementation 'mysql:mysql-connector-java:8.0.33' + implementation 'org.postgresql:postgresql:42.6.0' + implementation 'net.java.dev.jna:jna-platform:5.13.0' implementation 'org.apache.commons:commons-math3:3.6.1' implementation 'com.google.guava:guava:31.1-jre' diff --git a/javascript/DatabaseTool.html b/javascript/DatabaseTool.html new file mode 100644 index 0000000..8720d00 --- /dev/null +++ b/javascript/DatabaseTool.html @@ -0,0 +1,793 @@ + + + + + + 数据库管理工具 - Axis Innovators Box + + + +
+ + +
+ + +
+
+ +
+ + + + +
+
+ + + + +
+
状态: 就绪
+
+ +
+
+
+
查询编辑器
+
+ + +
+
+ +
+ +
+
+
查询结果
+
+
+
+
+

请连接数据库并执行查询以查看结果

+
+
+
+
+ +
+
就绪
+
行: 0, 列: 0
+
+
+
+ + + + + +
+ + + + + + + + + + + + + diff --git a/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java b/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java index 6f95af6..17476a4 100644 --- a/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java +++ b/src/main/java/com/axis/innovators/box/Log4j2OutputStream.java @@ -6,33 +6,121 @@ import org.apache.logging.log4j.Logger; import java.io.ByteArrayOutputStream; import java.io.OutputStream; import java.io.PrintStream; +import java.nio.charset.StandardCharsets; /** - * 将输出传递给 Log4j2 的日志记录器 + * 将输出传递给 Log4j2 的日志记录器,同时保持控制台输出 + * 修复问题:控制台输出被Log4j2覆盖 * @author tzdwindows 7 */ public class Log4j2OutputStream extends OutputStream { private static final Logger logger = LogManager.getLogger(); + + // 恢复静态变量 public static final ByteArrayOutputStream systemOutContent = new ByteArrayOutputStream(); public static final ByteArrayOutputStream systemErrContent = new ByteArrayOutputStream(); + + private final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + private final boolean isErrorStream; + private final PrintStream originalStream; // 保存原始的控制台流 + + public Log4j2OutputStream(boolean isErrorStream, PrintStream originalStream) { + this.isErrorStream = isErrorStream; + this.originalStream = originalStream; + } + @Override public void write(int b) { - systemOutContent.write(b); - logger.info(String.valueOf((char) b)); + // 写入原始控制台 + originalStream.write(b); + + buffer.write(b); + // 将内容同时写入对应的静态变量 + if (isErrorStream) { + systemErrContent.write(b); + } else { + systemOutContent.write(b); + } + + // 遇到换行符时刷新缓冲区到日志 + if (b == '\n') { + flush(); + } } @Override public void write(byte[] b, int off, int len) { - systemOutContent.write(b, off, len); - String message = new String(b, off, len).trim(); - logger.info(message); + // 写入原始控制台 + originalStream.write(b, off, len); + + buffer.write(b, off, len); + // 将内容同时写入对应的静态变量 + if (isErrorStream) { + systemErrContent.write(b, off, len); + } else { + systemOutContent.write(b, off, len); + } + + // 检查是否包含换行符 + for (int i = off; i < off + len; i++) { + if (b[i] == '\n') { + flush(); + break; + } + } + } + + @Override + public void flush() { + originalStream.flush(); + + String message = buffer.toString(StandardCharsets.UTF_8).trim(); + if (!message.isEmpty()) { + if (isErrorStream) { + logger.error(message); + } else { + logger.info(message); + } + } + buffer.reset(); // 清空缓冲区 + } + + @Override + public void close() { + flush(); + originalStream.close(); } /** - * 重定向 System.out 和 System.err 到 Log4j2 + * 重定向 System.out 和 System.err 到 Log4j2,同时保持控制台输出 */ public static void redirectSystemStreams() { - System.setOut(new PrintStream(new Log4j2OutputStream(), true)); - System.setErr(new PrintStream(new Log4j2OutputStream(), true)); + // 保存原始流 + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + + // System.out 使用 INFO 级别,同时输出到原始控制台 + System.setOut(new PrintStream(new Log4j2OutputStream(false, originalOut), true, StandardCharsets.UTF_8)); + // System.err 使用 ERROR 级别,同时输出到原始控制台 + System.setErr(new PrintStream(new Log4j2OutputStream(true, originalErr), true, StandardCharsets.UTF_8)); + } + + /** + * 清空静态缓冲区内容 + */ + public static void clearBuffers() { + systemOutContent.reset(); + systemErrContent.reset(); + } + + /** + * 获取输出内容 + */ + public static String getSystemOutContent() { + return systemOutContent.toString(StandardCharsets.UTF_8); + } + + public static String getSystemErrContent() { + return systemErrContent.toString(StandardCharsets.UTF_8); } } \ No newline at end of file diff --git a/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java index 6ecd79e..5272059 100644 --- a/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java +++ b/src/main/java/com/axis/innovators/box/browser/BrowserWindow.java @@ -11,9 +11,11 @@ import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; import org.cef.browser.CefMessageRouter; import org.cef.callback.CefContextMenuParams; +import org.cef.callback.CefJSDialogCallback; import org.cef.callback.CefMenuModel; import org.cef.callback.CefQueryCallback; import org.cef.handler.*; +import org.cef.misc.BoolRef; import org.cef.network.CefRequest; import javax.swing.*; @@ -425,6 +427,24 @@ public class BrowserWindow extends JFrame { } }); + client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { + @Override + public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { + if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog( + BrowserWindow.this, + message_text, + "警告", + JOptionPane.INFORMATION_MESSAGE + ); + }); + callback.Continue(true, ""); + return true; + } + return false; + } + }); // 3. 拦截所有新窗口(关键修复点!) diff --git a/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java b/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java index b68c87c..ee132d8 100644 --- a/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java +++ b/src/main/java/com/axis/innovators/box/browser/BrowserWindowJDialog.java @@ -11,9 +11,11 @@ import org.cef.browser.CefBrowser; import org.cef.browser.CefFrame; import org.cef.browser.CefMessageRouter; import org.cef.callback.CefContextMenuParams; +import org.cef.callback.CefJSDialogCallback; import org.cef.callback.CefMenuModel; import org.cef.callback.CefQueryCallback; import org.cef.handler.*; +import org.cef.misc.BoolRef; import org.cef.network.CefRequest; import org.json.JSONObject; @@ -432,6 +434,26 @@ public class BrowserWindowJDialog extends JDialog { } }); + // 添加 alert 弹窗监控处理 + client.addJSDialogHandler(new CefJSDialogHandlerAdapter() { + @Override + public boolean onJSDialog(CefBrowser browser, String origin_url, CefJSDialogHandler.JSDialogType dialog_type, String message_text, String default_prompt_text, CefJSDialogCallback callback, BoolRef suppress_message) { + if (dialog_type == CefJSDialogHandler.JSDialogType.JSDIALOGTYPE_ALERT) { + SwingUtilities.invokeLater(() -> { + JOptionPane.showMessageDialog( + BrowserWindowJDialog.this, + message_text, + "警告", + JOptionPane.INFORMATION_MESSAGE + ); + }); + callback.Continue(true, ""); + return true; + } + return false; + } + }); + // 3. 拦截所有新窗口(关键修复点!) diff --git a/src/main/java/com/axis/innovators/box/browser/MainApplication.java b/src/main/java/com/axis/innovators/box/browser/MainApplication.java index 95ec6aa..3198055 100644 --- a/src/main/java/com/axis/innovators/box/browser/MainApplication.java +++ b/src/main/java/com/axis/innovators/box/browser/MainApplication.java @@ -1,6 +1,7 @@ package com.axis.innovators.box.browser; import com.axis.innovators.box.browser.util.CodeExecutor; +import com.axis.innovators.box.browser.util.DatabaseConnectionManager; import com.axis.innovators.box.tools.FolderCreator; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -11,6 +12,7 @@ import org.cef.callback.CefQueryCallback; import org.cef.handler.CefMessageRouterHandlerAdapter; import org.graalvm.polyglot.Context; import org.graalvm.polyglot.Value; +import org.json.JSONArray; import org.json.JSONObject; import org.tzd.lm.LM; @@ -20,6 +22,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.sql.*; import java.util.List; import java.util.Objects; import java.util.concurrent.ExecutorService; @@ -351,4 +354,1023 @@ public class MainApplication { .withDefaultOperations() .build(); } + + public static void popupDataBaseWindow() { + // 预加载常用 JDBC 驱动(警告级别,不阻塞 UI) + try { + try { Class.forName("org.h2.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.h2.Driver 未找到"); } + try { Class.forName("org.sqlite.JDBC"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.sqlite.JDBC 未找到"); } + try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: org.postgresql.Driver 未找到"); } + try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: com.mysql.cj.jdbc.Driver 未找到"); } + try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException ignored) { System.err.println("WARN: oracle.jdbc.OracleDriver 未找到"); } + } catch (Throwable t) { + System.err.println("预加载 JDBC 驱动时发生异常: " + t.getMessage()); + } + + AtomicReference window = new AtomicReference<>(); + SwingUtilities.invokeLater(() -> { + WindowRegistry.getInstance().createNewWindow("main", builder -> + window.set(builder.title("Axis Innovators Box 数据库管理工具") + .icon(new ImageIcon(Objects.requireNonNull(MainApplication.class.getClassLoader().getResource("icons/logo.png"))).getImage()) + .size(1487, 836) + .htmlPath(FolderCreator.getJavaScriptFolder() + "\\" + "DatabaseTool.html") + .operationHandler(createOperationHandler()) + .build()) + ); + + if (window.get() == null) { + System.err.println("popupDataBaseWindow: window 创建失败,window.get() == null"); + return; + } + + CefMessageRouter msgRouter = window.get().getMsgRouter(); + if (msgRouter != null) { + msgRouter.addHandler(new CefMessageRouterHandlerAdapter() { + @Override + public boolean onQuery(CefBrowser browser, CefFrame frame, long queryId, + String request, boolean persistent, CefQueryCallback callback) { + try { + JSONObject requestJson = new JSONObject(request); + String type = requestJson.optString("type", ""); + + // 补默认端口与标准化 driver 名称 + String drv = requestJson.optString("driver", "").toLowerCase(); + if (!drv.isEmpty()) { + String port = requestJson.optString("port", "").trim(); + if (port.isEmpty()) { + switch (drv) { + case "mysql": + requestJson.put("port", "3306"); + break; + case "postgresql": + case "postgres": + requestJson.put("port", "5432"); + break; + case "oracle": + requestJson.put("port", "1521"); + break; + } + } + if ("postgres".equals(drv)) requestJson.put("driver", "postgresql"); + } + + // 安全标识符校验 pattern(表名/列名等仅允许常见字符) + final java.util.regex.Pattern SAFE_IDENT = java.util.regex.Pattern.compile("^[A-Za-z0-9_\\.\\$]+$"); + + switch (type) { + // 已有功能:继续使用现有处理函数(假定这些方法在类中定义) + case "connectDatabase": + handleDatabaseConnect(requestJson, callback); + break; + case "createLocalDatabase": + handleCreateLocalDatabase(requestJson, callback); + break; + case "disconnectDatabase": + handleDisconnectDatabase(requestJson, callback); + break; + case "executeQuery": + handleExecuteQuery(requestJson, callback); + break; + case "getTables": + handleGetTables(requestJson, callback); + break; + case "getTableData": + handleGetTableData(requestJson, callback); + break; + case "getTableStructure": + handleGetTableStructure(requestJson, callback); + break; + case "updateTheme": + handleUpdateTheme(requestJson, callback); + break; + case "getFonts": + handleGetFonts(requestJson, callback); + break; + + // 新增后端支持:analyzeQuery(EXPLAIN / EXPLAIN ANALYZE) + case "analyzeQuery": { + String connectionId = requestJson.optString("connectionId", ""); + String query = requestJson.optString("query", "").trim(); + if (connectionId.isEmpty() || query.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 query 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + // 依据驱动选择 EXPLAIN 语法 + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + String explainSql = "EXPLAIN " + query; + if ("postgresql".equals(drvName)) { + explainSql = "EXPLAIN ANALYZE " + query; + } + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery(explainSql)) { + org.json.JSONArray out = new org.json.JSONArray(); + while (rs.next()) { + // EXPLAIN 输出常为单列文本 + out.put(rs.getString(1)); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("explain", out); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","分析失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // exportData -> 导出为 CSV 或 JSON,写入用户目录下 .axis_innovators_box/exports/ + case "exportData": { + String connectionId = requestJson.optString("connectionId", ""); + String table = requestJson.optString("table", ""); + String format = requestJson.optString("format", "csv").toLowerCase(); + if (connectionId.isEmpty() || table.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 或 table 为空").toString()); + break; + } + if (!SAFE_IDENT.matcher(table).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + java.nio.file.Path exportDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "exports"); + try { java.nio.file.Files.createDirectories(exportDir); } catch (Exception e) { /* ignore */ } + + String filenameBase = table + "_" + System.currentTimeMillis(); + java.nio.file.Path outPath = exportDir.resolve(filenameBase + (format.equals("json") ? ".json" : ".csv")); + + String query = "SELECT * FROM " + table; + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery(query)) { + + ResultSetMetaData md = rs.getMetaData(); + int cols = md.getColumnCount(); + + if ("json".equals(format)) { + org.json.JSONArray arr = new org.json.JSONArray(); + while (rs.next()) { + org.json.JSONObject obj = new org.json.JSONObject(); + for (int i = 1; i <= cols; i++) { + Object val = rs.getObject(i); + obj.put(md.getColumnLabel(i), val == null ? org.json.JSONObject.NULL : val); + } + arr.put(obj); + } + java.nio.file.Files.write(outPath, arr.toString(2).getBytes(java.nio.charset.StandardCharsets.UTF_8)); + } else { + // CSV + try (java.io.BufferedWriter writer = java.nio.file.Files.newBufferedWriter(outPath, java.nio.charset.StandardCharsets.UTF_8)) { + // header + for (int i = 1; i <= cols; i++) { + if (i > 1) writer.write(","); + writer.write("\"" + md.getColumnLabel(i).replace("\"","\"\"") + "\""); + } + writer.write("\n"); + while (rs.next()) { + for (int i = 1; i <= cols; i++) { + if (i > 1) writer.write(","); + Object val = rs.getObject(i); + String cell = val == null ? "" : String.valueOf(val); + writer.write("\"" + cell.replace("\"","\"\"") + "\""); + } + writer.write("\n"); + } + } + } + + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("path", outPath.toAbsolutePath().toString()); + resp.put("message","导出成功"); + callback.success(resp.toString()); + } catch (SQLException | java.io.IOException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","导出失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // importCsv -> 从给定 path 导入 CSV,要求首行为列名且列名匹配表字段 + case "importCsv": { + String connectionId = requestJson.optString("connectionId", ""); + String table = requestJson.optString("table", ""); + String path = requestJson.optString("path", ""); + if (connectionId.isEmpty() || table.isEmpty() || path.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","参数不完整").toString()); + break; + } + if (!SAFE_IDENT.matcher(table).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法表名").toString()); + break; + } + java.nio.file.Path csvPath = java.nio.file.Paths.get(path); + if (!java.nio.file.Files.exists(csvPath)) { + callback.failure(400, new JSONObject().put("status","error").put("message","CSV 文件不存在").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + + // 读取首行作为列头 + try (java.io.BufferedReader br = java.nio.file.Files.newBufferedReader(csvPath, java.nio.charset.StandardCharsets.UTF_8)) { + String headerLine = br.readLine(); + if (headerLine == null) { + callback.failure(400, new JSONObject().put("status","error").put("message","CSV 为空").toString()); + break; + } + // 简单 CSV 解析(支持双引号),但要求列名没有逗号内部双引号结构复杂情形 + String[] columns = headerLine.split(","); + for (int i = 0; i < columns.length; i++) { + columns[i] = columns[i].trim().replaceAll("^\"|\"$", ""); // 去掉可能的两端引号 + if (!SAFE_IDENT.matcher(columns[i]).matches()) { + callback.failure(400, new JSONObject().put("status","error").put("message","非法列名: " + columns[i]).toString()); + return true; + } + } + // 构建 INSERT SQL + StringBuilder placeholders = new StringBuilder(); + for (int i = 0; i < columns.length; i++) { + if (i > 0) placeholders.append(","); + placeholders.append("?"); + } + String insertSql = "INSERT INTO " + table + " (" + String.join(",", columns) + ") VALUES (" + placeholders.toString() + ")"; + conn.setAutoCommit(false); + int imported = 0; + try (java.sql.PreparedStatement pstmt = conn.prepareStatement(insertSql)) { + String line; + while ((line = br.readLine()) != null) { + // 简单分割(不处理复杂引号内部逗号) + String[] parts = line.split(",", -1); + for (int i = 0; i < columns.length; i++) { + String cell = i < parts.length ? parts[i].trim().replaceAll("^\"|\"$", "") : ""; + pstmt.setString(i + 1, cell.isEmpty() ? null : cell); + } + pstmt.addBatch(); + if (++imported % 500 == 0) pstmt.executeBatch(); + } + pstmt.executeBatch(); + conn.commit(); + } catch (SQLException ex) { + conn.rollback(); + throw ex; + } finally { + conn.setAutoCommit(true); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("imported", imported); + resp.put("message", "导入完成"); + callback.success(resp.toString()); + } catch (Exception ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","导入失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // generateEr -> 收集表、列信息并返回 JSON(前端可据此生成 ER 图) + case "generateEr": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + try { + DatabaseMetaData meta = conn.getMetaData(); + org.json.JSONArray tablesArr = new org.json.JSONArray(); + + try (ResultSet rsTables = meta.getTables(conn.getCatalog(), null, null, new String[]{"TABLE"})) { + while (rsTables.next()) { + String tbl = rsTables.getString("TABLE_NAME"); + org.json.JSONObject tObj = new org.json.JSONObject(); + tObj.put("name", tbl); + org.json.JSONArray cols = new org.json.JSONArray(); + try (ResultSet rsCols = meta.getColumns(conn.getCatalog(), null, tbl, null)) { + while (rsCols.next()) { + org.json.JSONObject c = new org.json.JSONObject(); + c.put("name", rsCols.getString("COLUMN_NAME")); + c.put("type", rsCols.getString("TYPE_NAME")); + c.put("size", rsCols.getInt("COLUMN_SIZE")); + c.put("nullable", rsCols.getInt("NULLABLE") == DatabaseMetaData.columnNullable); + cols.put(c); + } + } + tObj.put("columns", cols); + tablesArr.put(tObj); + } + } + + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("er", new JSONObject().put("tables", new JSONArray(tablesArr.toString()))); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","生成 ER 失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // analyzePerformance -> 尝试返回当前数据库会话/进程信息(简单实现,依据数据库类型) + case "analyzePerformance": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + try { + org.json.JSONArray out = new org.json.JSONArray(); + if ("postgresql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT pid, usename, state, query FROM pg_stat_activity LIMIT 50")) { + while (rs.next()) { + org.json.JSONObject r = new org.json.JSONObject(); + r.put("pid", rs.getObject("pid")); + r.put("user", rs.getString("usename")); + r.put("state", rs.getString("state")); + r.put("query", rs.getString("query")); + out.put(r); + } + } + } else if ("mysql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SHOW PROCESSLIST")) { + while (rs.next()) { + org.json.JSONObject r = new org.json.JSONObject(); + r.put("Id", rs.getObject("Id")); + r.put("User", rs.getString("User")); + r.put("Host", rs.getString("Host")); + r.put("db", rs.getString("db")); + r.put("Command", rs.getString("Command")); + r.put("Time", rs.getString("Time")); + r.put("State", rs.getString("State")); + r.put("Info", rs.getString("Info")); + out.put(r); + } + } + } else { + // 通用替代:返回当前时间与简单连接信息 + org.json.JSONObject r = new org.json.JSONObject(); + r.put("now", java.time.Instant.now().toString()); + r.put("message","未实现针对该数据库的详细性能查询,返回通用信息"); + out.put(r); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("data", new JSONArray(out.toString())); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","性能分析失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + // listUsers -> 列出数据库用户(尝试常用查询) + case "listUsers": { + String connectionId = requestJson.optString("connectionId", ""); + if (connectionId.isEmpty()) { + callback.failure(400, new JSONObject().put("status","error").put("message","connectionId 为空").toString()); + break; + } + Connection conn = DatabaseConnectionManager.getConnection(connectionId); + if (conn == null || conn.isClosed()) { + callback.failure(500, new JSONObject().put("status","error").put("message","连接不存在或已关闭").toString()); + break; + } + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String drvName = info == null ? "" : (info.driver == null ? "" : info.driver.toLowerCase()); + try { + org.json.JSONArray out = new org.json.JSONArray(); + if ("postgresql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT usename FROM pg_user")) { + while (rs.next()) { + out.put(rs.getString(1)); + } + } + } else if ("mysql".equals(drvName)) { + try (Statement st = conn.createStatement(); + ResultSet rs = st.executeQuery("SELECT User, Host FROM mysql.user")) { + while (rs.next()) { + org.json.JSONObject u = new org.json.JSONObject(); + u.put("user", rs.getString("User")); + u.put("host", rs.getString("Host")); + out.put(u); + } + } + } else { + // H2 / SQLite: 列出连接用户或简单返回空 + out.put("not_supported_for_db"); + } + JSONObject resp = new JSONObject(); + resp.put("status","success"); + resp.put("users", new JSONArray(out.toString())); + callback.success(resp.toString()); + } catch (SQLException ex) { + JSONObject err = new JSONObject(); + err.put("status","error"); + err.put("message","列出用户失败: " + ex.getMessage()); + callback.failure(500, err.toString()); + } + break; + } + + default: { + JSONObject err = new JSONObject(); + err.put("status", "error"); + err.put("message", "未知的操作类型: " + type); + callback.failure(400, err.toString()); + } + } + return true; + } catch (org.json.JSONException je) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "请求解析失败: " + je.getMessage()); + callback.failure(400, error.toString()); + return true; + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", e.getMessage() == null ? e.toString() : e.getMessage()); + callback.failure(500, error.toString()); + return true; + } + } + + @Override + public void onQueryCanceled(CefBrowser browser, CefFrame frame, long queryId) { + // 查询取消,可选地记录日志 + } + }, true); + } else { + System.err.println("popupDataBaseWindow: msgRouter 为 null,消息路由无法注册"); + } + }); + } + + // 处理数据库连接 + private static void handleDatabaseConnect(JSONObject request, CefQueryCallback callback) { + try { + String driver = request.optString("driver", "mysql"); + String host = request.optString("host", "localhost"); + String port = request.optString("port", "3306"); + String database = request.optString("database", ""); + String username = request.optString("username", ""); + String password = request.optString("password", ""); + + // 验证必要参数 + if (database.isEmpty()) { + throw new IllegalArgumentException("数据库名不能为空"); + } + + // 建立真实数据库连接 + String connectionId = DatabaseConnectionManager.connect(driver, host, port, database, username, password); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "数据库连接成功"); + response.put("connectionId", connectionId); + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + response.put("database", info.database); + response.put("driver", info.driver); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "连接失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理创建本地数据库 + private static void handleCreateLocalDatabase(JSONObject request, CefQueryCallback callback) { + try { + String driver = request.optString("driver", "sqlite"); + String dbName = request.optString("dbName", "my_database"); + + if (dbName.isEmpty()) { + throw new IllegalArgumentException("数据库名称不能为空"); + } + + // 创建本地数据库 + String connectionId = DatabaseConnectionManager.createLocalDatabase(driver, dbName); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "本地数据库创建成功"); + response.put("connectionId", connectionId); + response.put("database", dbName); + response.put("driver", driver); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "创建数据库失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理断开数据库连接 + private static void handleDisconnectDatabase(JSONObject request, CefQueryCallback callback) { + try { + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + + DatabaseConnectionManager.disconnect(connectionId); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("message", "数据库连接已断开"); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "断开连接失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理SQL查询执行 + private static void handleExecuteQuery(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + Statement statement = null; + ResultSet resultSet = null; + + try { + String query = request.optString("query", "").trim(); + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (query.isEmpty()) { + throw new IllegalArgumentException("SQL查询不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + long startTime = System.currentTimeMillis(); + + JSONObject response = new JSONObject(); + + // 判断查询类型 + boolean isSelect = query.toLowerCase().startsWith("select"); + boolean isUpdate = query.toLowerCase().startsWith("update") || + query.toLowerCase().startsWith("insert") || + query.toLowerCase().startsWith("delete"); + + if (isSelect) { + statement = connection.createStatement(); + resultSet = statement.executeQuery(query); + + // 获取元数据 + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // 构建列信息 + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + // 构建数据 + JSONArray data = new JSONArray(); + int rowCount = 0; + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + rowCount++; + } + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("rowCount", rowCount); + response.put("columns", columns); + response.put("data", data); + + } else if (isUpdate) { + statement = connection.createStatement(); + int affectedRows = statement.executeUpdate(query); + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("affectedRows", affectedRows); + response.put("message", "操作成功,影响 " + affectedRows + " 行"); + + } else { + // 其他类型的查询(CREATE, DROP, ALTER等) + statement = connection.createStatement(); + boolean hasResults = statement.execute(query); + + long endTime = System.currentTimeMillis(); + double executionTime = (endTime - startTime) / 1000.0; + + response.put("status", "success"); + response.put("executionTime", String.format("%.3fs", executionTime)); + response.put("message", "查询执行成功"); + + if (hasResults) { + resultSet = statement.getResultSet(); + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + JSONArray data = new JSONArray(); + int rowCount = 0; + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + rowCount++; + } + + response.put("rowCount", rowCount); + response.put("columns", columns); + response.put("data", data); + } + } + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "查询执行失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + // 关闭资源 + try { + if (resultSet != null) resultSet.close(); + if (statement != null) statement.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表列表 + private static void handleGetTables(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + ResultSet resultSet = null; + + try { + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + JSONArray tables = new JSONArray(); + + // 根据数据库类型获取表信息 + String catalog = null; + String schema = null; + + switch (info.driver.toLowerCase()) { + case "mysql": + catalog = info.database; + break; + case "postgresql": + schema = "public"; + break; + case "sqlite": + case "h2": + // SQLite和H2不需要特定的catalog或schema + break; + } + + DatabaseMetaData metaData = connection.getMetaData(); + resultSet = metaData.getTables(catalog, schema, null, new String[]{"TABLE"}); + + while (resultSet.next()) { + String tableName = resultSet.getString("TABLE_NAME"); + String tableType = resultSet.getString("TABLE_TYPE"); + + // 获取表的行数 + int rowCount = 0; + try (Statement countStmt = connection.createStatement(); + ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + if (countRs.next()) { + rowCount = countRs.getInt(1); + } + } catch (SQLException e) { + // 如果无法获取行数,忽略错误 + } + + JSONObject table = new JSONObject(); + table.put("name", tableName); + table.put("type", tableType); + table.put("rows", rowCount); + tables.put(table); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tables", tables); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表列表失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表数据 + private static void handleGetTableData(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + Statement statement = null; + ResultSet resultSet = null; + + try { + String tableName = request.optString("tableName", ""); + String connectionId = request.optString("connectionId", ""); + int limit = request.optInt("limit", 50); + int offset = request.optInt("offset", 0); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (tableName.isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + // 构建分页查询 + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + String query; + + switch (info.driver.toLowerCase()) { + case "mysql": + query = String.format("SELECT * FROM `%s` LIMIT %d OFFSET %d", tableName, limit, offset); + break; + case "postgresql": + query = String.format("SELECT * FROM \"%s\" LIMIT %d OFFSET %d", tableName, limit, offset); + break; + default: + query = String.format("SELECT * FROM %s LIMIT %d OFFSET %d", tableName, limit, offset); + } + + statement = connection.createStatement(); + resultSet = statement.executeQuery(query); + + // 获取元数据 + ResultSetMetaData metaData = resultSet.getMetaData(); + int columnCount = metaData.getColumnCount(); + + // 构建列信息 + JSONArray columns = new JSONArray(); + for (int i = 1; i <= columnCount; i++) { + columns.put(metaData.getColumnName(i)); + } + + // 构建数据 + JSONArray data = new JSONArray(); + while (resultSet.next()) { + JSONObject row = new JSONObject(); + for (int i = 1; i <= columnCount; i++) { + String columnName = metaData.getColumnName(i); + Object value = resultSet.getObject(i); + row.put(columnName, value != null ? value.toString() : null); + } + data.put(row); + } + + // 获取总行数 + int total = 0; + try (Statement countStmt = connection.createStatement(); + ResultSet countRs = countStmt.executeQuery("SELECT COUNT(*) FROM " + tableName)) { + if (countRs.next()) { + total = countRs.getInt(1); + } + } catch (SQLException e) { + // 如果无法获取总行数,使用当前数据行数 + total = data.length(); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tableName", tableName); + response.put("columns", columns); + response.put("data", data); + response.put("total", total); + response.put("offset", offset); + response.put("limit", limit); + + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表数据失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + if (statement != null) statement.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理获取表结构 + private static void handleGetTableStructure(JSONObject request, CefQueryCallback callback) { + Connection connection = null; + ResultSet resultSet = null; + + try { + String tableName = request.optString("tableName", ""); + String connectionId = request.optString("connectionId", ""); + + if (connectionId.isEmpty()) { + throw new IllegalArgumentException("连接ID不能为空"); + } + if (tableName.isEmpty()) { + throw new IllegalArgumentException("表名不能为空"); + } + + connection = DatabaseConnectionManager.getConnection(connectionId); + if (connection == null || connection.isClosed()) { + throw new SQLException("数据库连接已断开或不存在"); + } + + DatabaseConnectionManager.DatabaseInfo info = DatabaseConnectionManager.getConnectionInfo(connectionId); + DatabaseMetaData metaData = connection.getMetaData(); + + String catalog = null; + String schema = null; + + switch (info.driver.toLowerCase()) { + case "mysql": + catalog = info.database; + break; + case "postgresql": + schema = "public"; + break; + } + + resultSet = metaData.getColumns(catalog, schema, tableName, null); + + JSONArray columns = new JSONArray(); + while (resultSet.next()) { + JSONObject column = new JSONObject(); + column.put("name", resultSet.getString("COLUMN_NAME")); + column.put("type", resultSet.getString("TYPE_NAME")); + column.put("size", resultSet.getInt("COLUMN_SIZE")); + column.put("nullable", resultSet.getInt("NULLABLE") == DatabaseMetaData.columnNullable); + column.put("defaultValue", resultSet.getString("COLUMN_DEF")); + columns.put(column); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("tableName", tableName); + response.put("columns", columns); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取表结构失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } finally { + try { + if (resultSet != null) resultSet.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } + } + + // 处理主题更新(保持不变) + private static void handleUpdateTheme(JSONObject request, CefQueryCallback callback) { + try { + String theme = request.optString("theme", "light"); + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("theme", theme); + response.put("message", "主题已更新为: " + theme); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "主题更新失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } + + // 处理字体获取(保持不变) + private static void handleGetFonts(JSONObject request, CefQueryCallback callback) { + try { + JSONArray fonts = new JSONArray(); + String[] fontList = { + "Segoe UI", "Microsoft YaHei", "SimSun", "Arial", + "Helvetica", "Times New Roman", "Courier New", "Verdana" + }; + + for (String font : fontList) { + fonts.put(font); + } + + JSONObject response = new JSONObject(); + response.put("status", "success"); + response.put("fonts", fonts); + callback.success(response.toString()); + + } catch (Exception e) { + JSONObject error = new JSONObject(); + error.put("status", "error"); + error.put("message", "获取字体失败: " + e.getMessage()); + callback.failure(500, error.toString()); + } + } } diff --git a/src/main/java/com/axis/innovators/box/browser/util/DatabaseConnectionManager.java b/src/main/java/com/axis/innovators/box/browser/util/DatabaseConnectionManager.java new file mode 100644 index 0000000..9d87dd0 --- /dev/null +++ b/src/main/java/com/axis/innovators/box/browser/util/DatabaseConnectionManager.java @@ -0,0 +1,397 @@ +package com.axis.innovators.box.browser.util; + +import java.sql.*; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +/** + * 数据库连接管理器 + * @author tzdwindows 7 + */ +public class DatabaseConnectionManager { + private static final java.util.Map connections = new java.util.concurrent.ConcurrentHashMap<>(); + private static final java.util.Map connectionInfo = new java.util.concurrent.ConcurrentHashMap<>(); + + public static class DatabaseInfo { + public String driver; + public String url; + public String host; + public String port; + public String database; + public String username; + + public DatabaseInfo(String driver, String url, String host, String port, String database, String username) { + this.driver = driver; + this.url = url; + this.host = host; + this.port = port; + this.database = database; + this.username = username; + } + } + + public static String connect(String driver, String host, String port, + String database, String username, String password) throws SQLException { + String connectionId = "conn_" + System.currentTimeMillis(); + + String drv = driver == null ? "" : driver.toLowerCase(); + + // 规范化 database 路径(特别是 Windows 反斜杠问题) + if (database != null) { + database = database.replace("\\", "/"); + } else { + database = ""; + } + + // 先显式加载驱动,避免因为 classloader 问题找不到驱动 + try { + switch (drv) { + case "mysql": + Class.forName("com.mysql.cj.jdbc.Driver"); + break; + case "postgresql": + Class.forName("org.postgresql.Driver"); + break; + case "sqlite": + Class.forName("org.sqlite.JDBC"); + break; + case "oracle": + Class.forName("oracle.jdbc.OracleDriver"); + break; + case "h2": + Class.forName("org.h2.Driver"); + break; + default: + // 不抛出,使后续 URL 构造仍可检查类型 + } + } catch (ClassNotFoundException e) { + throw new SQLException("JDBC 驱动未找到,请确认对应驱动已加入 classpath: " + e.getMessage(), e); + } + + String url = buildConnectionUrl(driver, host, port, database); + + Connection connection; + Properties props = new Properties(); + if (username != null && !username.isEmpty()) props.setProperty("user", username); + if (password != null && !password.isEmpty()) props.setProperty("password", password); + + switch (drv) { + case "mysql": + props.setProperty("useSSL", "false"); + props.setProperty("serverTimezone", "UTC"); + connection = DriverManager.getConnection(url, props); + break; + case "postgresql": + connection = DriverManager.getConnection(url, props); + break; + case "sqlite": + // sqlite 不需要 props,URL 已经是文件路径(已做过替换) + connection = DriverManager.getConnection(url); + break; + case "oracle": + connection = DriverManager.getConnection(url, props); + break; + case "h2": + // H2 使用默认用户 sa / 空密码(如果需要可调整) + connection = DriverManager.getConnection(url, "sa", ""); + break; + default: + throw new SQLException("不支持的数据库类型: " + driver); + } + + connections.put(connectionId, connection); + connectionInfo.put(connectionId, new DatabaseInfo(driver, url, host, port, database, username)); + return connectionId; + } + + public static void disconnect(String connectionId) throws SQLException { + Connection connection = connections.get(connectionId); + if (connection != null && !connection.isClosed()) { + connection.close(); + } + connections.remove(connectionId); + connectionInfo.remove(connectionId); + } + + public static Connection getConnection(String connectionId) { + return connections.get(connectionId); + } + + public static DatabaseInfo getConnectionInfo(String connectionId) { + return connectionInfo.get(connectionId); + } + + private static String buildConnectionUrl(String driver, String host, String port, String database) { + String drv = driver == null ? "" : driver.toLowerCase(); + switch (drv) { + case "mysql": + return "jdbc:mysql://" + host + ":" + port + "/" + database; + case "postgresql": + return "jdbc:postgresql://" + host + ":" + port + "/" + database; + case "sqlite": + // 对于 SQLite,database 可能是绝对路径或相对文件名,先把反斜杠替成正斜杠 + if (database == null || database.isEmpty()) { + return "jdbc:sqlite::memory:"; + } + String normalized = database.replace("\\", "/"); + // 如果看起来像相对文件名(不含冒号也不以 / 开头),则当作相对于用户目录的路径 + if (!normalized.contains(":") && !normalized.startsWith("/")) { + String userHome = System.getProperty("user.home").replace("\\", "/"); + normalized = userHome + "/" + normalized; + } + return "jdbc:sqlite:" + normalized; + case "oracle": + return "jdbc:oracle:thin:@" + host + ":" + port + ":" + database; + case "h2": + // H2 文件路径同样做反斜杠处理 + if (database == null || database.isEmpty()) { + String userHome = System.getProperty("user.home").replace("\\", "/"); + return "jdbc:h2:file:" + userHome + "/.axis_innovators_box/databases/h2db"; + } else { + String norm = database.replace("\\", "/"); + // 如果传入仅是名字(无斜杠或冒号),则存到用户目录下 + if (!norm.contains("/") && !norm.contains(":")) { + String userHome = System.getProperty("user.home").replace("\\", "/"); + norm = userHome + "/.axis_innovators_box/databases/" + norm; + } + return "jdbc:h2:file:" + norm; + } + default: + throw new IllegalArgumentException("不支持的数据库类型: " + driver); + } + } + + + /** + * 在服务器上创建数据库(MySQL / PostgreSQL / Oracle(示例)) + * @param driver mysql | postgresql | oracle + * @param host 数据库主机 + * @param port 端口 + * @param dbName 要创建的数据库名(或 schema 名) + * @param adminUser 管理员用户名(用于创建数据库) + * @param adminPassword 管理员密码 + * @return 如果创建成功返回一个简短消息,否则抛出 SQLException + * @throws SQLException + */ + public static String createDatabaseOnServer(String driver, String host, String port, + String dbName, String adminUser, String adminPassword) throws SQLException { + if (driver == null) throw new SQLException("driver 不能为空"); + String drv = driver.toLowerCase().trim(); + + // 简单校验 dbName(避免注入)——只允许字母数字下划线 + if (dbName == null || !dbName.matches("[A-Za-z0-9_]+")) { + throw new SQLException("不合法的数据库名: " + dbName); + } + + try { + switch (drv) { + case "mysql": + // 加载驱动(如果尚未加载) + try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { + throw new SQLException("MySQL 驱动未找到,请加入 mysql-connector-java 到 classpath", e); + } + // 连接到服务器的默认库(不指定数据库)以执行 CREATE DATABASE + String mysqlUrl = "jdbc:mysql://" + host + ":" + port + "/?useSSL=false&serverTimezone=UTC"; + try (Connection conn = DriverManager.getConnection(mysqlUrl, adminUser, adminPassword); + Statement st = conn.createStatement()) { + String sql = "CREATE DATABASE `" + dbName + "` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci"; + st.executeUpdate(sql); + } + return "MySQL 数据库创建成功: " + dbName; + + case "postgresql": + case "postgres": + try { Class.forName("org.postgresql.Driver"); } catch (ClassNotFoundException e) { + throw new SQLException("Postgres 驱动未找到,请加入 postgresql 到 classpath", e); + } + // 连接到默认 postgres 数据库以创建新数据库 + String pgUrl = "jdbc:postgresql://" + host + ":" + port + "/postgres"; + try (Connection conn = DriverManager.getConnection(pgUrl, adminUser, adminPassword); + Statement st = conn.createStatement()) { + String sql = "CREATE DATABASE " + dbName + " WITH ENCODING 'UTF8'"; + st.executeUpdate(sql); + } + return "PostgreSQL 数据库创建成功: " + dbName; + + case "oracle": + // Oracle 数据库“创建数据库”通常由 DBA 完成(复杂),这里示例创建用户/模式(更常见) + try { Class.forName("oracle.jdbc.OracleDriver"); } catch (ClassNotFoundException e) { + throw new SQLException("Oracle 驱动未找到,请把 ojdbc.jar 加入 classpath", e); + } + // 需使用具有足够权限的账户(通常为 sys or system),并且 URL 需要正确(SID / ServiceName) + // 下面示例假设通过 SID 连接: jdbc:oracle:thin:@host:port:SID + String oracleUrl = "jdbc:oracle:thin:@" + host + ":" + port + ":" + "ORCL"; // 把 ORCL 换成实际 SID + try (Connection conn = DriverManager.getConnection(oracleUrl, adminUser, adminPassword); + Statement st = conn.createStatement()) { + // 创建 user(schema)示例 + String pwd = adminPassword; // 实际应使用独立密码,不推荐用 adminPassword + String createUser = "CREATE USER " + dbName + " IDENTIFIED BY \"" + pwd + "\""; + String grant = "GRANT CONNECT, RESOURCE TO " + dbName; + st.executeUpdate(createUser); + st.executeUpdate(grant); + } catch (SQLException ex) { + // Oracle 操作更容易失败,给出提示 + throw new SQLException("Oracle: 无法创建用户/模式,请检查权限和 URL(通常需由 DBA 操作): " + ex.getMessage(), ex); + } + return "Oracle 用户/模式创建成功(注意:真正的 DB 实例通常由 DBA 管理): " + dbName; + + default: + throw new SQLException("不支持的数据库类型: " + driver); + } + } catch (SQLException se) { + // 透传 SQLException,调用方会拿到 message 并反馈给前端 + throw se; + } catch (Exception e) { + throw new SQLException("创建数据库时发生异常: " + e.getMessage(), e); + } + } + public static String createLocalDatabase(String driver, String dbName) throws SQLException { + switch (driver.toLowerCase()) { + case "sqlite": + // 创建目录并构造规范化路径(确保路径使用正斜杠) + String dbFileName = dbName.endsWith(".db") ? dbName : (dbName + ".db"); + java.nio.file.Path dbDir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases"); + try { + java.nio.file.Files.createDirectories(dbDir); + } catch (Exception e) { + throw new SQLException("无法创建数据库目录: " + e.getMessage(), e); + } + String dbPath = dbDir.resolve(dbFileName).toAbsolutePath().toString().replace("\\", "/"); + + // 显式加载 sqlite 驱动(避免 No suitable driver) + try { + Class.forName("org.sqlite.JDBC"); + } catch (ClassNotFoundException e) { + throw new SQLException("未找到 sqlite 驱动,请确认 sqlite-jdbc 已加入 classpath", e); + } + + // 直接使用 connect 构建连接(connect 中会通过 buildConnectionUrl 处理 path) + String connectionId = connect("sqlite", "", "", dbPath, "", ""); + + // 创建示例表 + createSampleTables(connectionId); + + return connectionId; + + case "h2": + java.nio.file.Path h2Dir = java.nio.file.Paths.get(System.getProperty("user.home"), ".axis_innovators_box", "databases"); + try { + java.nio.file.Files.createDirectories(h2Dir); + } catch (Exception e) { + throw new SQLException("无法创建数据库目录: " + e.getMessage(), e); + } + String h2Path = h2Dir.resolve(dbName).toAbsolutePath().toString().replace("\\", "/"); + String h2Url = "jdbc:h2:file:" + h2Path; + + try { + Class.forName("org.h2.Driver"); + } catch (ClassNotFoundException e) { + throw new SQLException("未找到 H2 驱动,请确认 h2.jar 已加入 classpath", e); + } + + Connection h2Conn = DriverManager.getConnection(h2Url, "sa", ""); + String h2ConnectionId = "conn_" + System.currentTimeMillis(); + connections.put(h2ConnectionId, h2Conn); + connectionInfo.put(h2ConnectionId, new DatabaseInfo("h2", h2Url, "localhost", "", dbName, "sa")); + + createSampleTables(h2ConnectionId); + + return h2ConnectionId; + + default: + throw new SQLException("不支持创建本地数据库类型: " + driver); + } + } + + private static void createSampleTables(String connectionId) throws SQLException { + Connection conn = getConnection(connectionId); + DatabaseInfo info = getConnectionInfo(connectionId); + + if ("sqlite".equals(info.driver) || "h2".equals(info.driver)) { + try (Statement stmt = conn.createStatement()) { + // 创建用户表 + stmt.execute("CREATE TABLE IF NOT EXISTS users (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "username VARCHAR(50) NOT NULL UNIQUE, " + + "email VARCHAR(100) NOT NULL, " + + "password VARCHAR(100) NOT NULL, " + + "status VARCHAR(20) DEFAULT 'active', " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + + ")"); + + // 创建产品表 + stmt.execute("CREATE TABLE IF NOT EXISTS products (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "name VARCHAR(100) NOT NULL, " + + "description TEXT, " + + "price DECIMAL(10,2) NOT NULL, " + + "stock INTEGER DEFAULT 0, " + + "category VARCHAR(50), " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" + + ")"); + + // 创建订单表 + stmt.execute("CREATE TABLE IF NOT EXISTS orders (" + + "id INTEGER PRIMARY KEY AUTOINCREMENT, " + + "user_id INTEGER, " + + "product_id INTEGER, " + + "quantity INTEGER NOT NULL, " + + "total_price DECIMAL(10,2) NOT NULL, " + + "status VARCHAR(20) DEFAULT 'pending', " + + "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " + + "FOREIGN KEY (user_id) REFERENCES users(id), " + + "FOREIGN KEY (product_id) REFERENCES products(id)" + + ")"); + + // 插入示例数据 + insertSampleData(conn); + } + } + } + + private static void insertSampleData(Connection conn) throws SQLException { + // 检查是否已有数据 + try (Statement checkStmt = conn.createStatement(); + ResultSet rs = checkStmt.executeQuery("SELECT COUNT(*) FROM users")) { + if (rs.next() && rs.getInt(1) == 0) { + // 插入用户数据 + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO users (username, email, password) VALUES (?, ?, ?)")) { + String[][] users = { + {"admin", "admin@example.com", "password123"}, + {"user1", "user1@example.com", "password123"}, + {"user2", "user2@example.com", "password123"} + }; + + for (String[] user : users) { + pstmt.setString(1, user[0]); + pstmt.setString(2, user[1]); + pstmt.setString(3, user[2]); + pstmt.executeUpdate(); + } + } + + // 插入产品数据 + try (PreparedStatement pstmt = conn.prepareStatement( + "INSERT INTO products (name, description, price, stock, category) VALUES (?, ?, ?, ?, ?)")) { + Object[][] products = { + {"笔记本电脑", "高性能笔记本电脑", 5999.99, 50, "电子"}, + {"智能手机", "最新款智能手机", 3999.99, 100, "电子"}, + {"办公椅", "舒适办公椅", 299.99, 30, "家居"}, + {"咖啡机", "全自动咖啡机", 899.99, 20, "家电"} + }; + + for (Object[] product : products) { + pstmt.setString(1, (String) product[0]); + pstmt.setString(2, (String) product[1]); + pstmt.setDouble(3, (Double) product[2]); + pstmt.setInt(4, (Integer) product[3]); + pstmt.setString(5, (String) product[4]); + pstmt.executeUpdate(); + } + } + } + } + } +} diff --git a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java index 2f60d8d..831fc27 100644 --- a/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java +++ b/src/main/java/com/axis/innovators/box/register/RegistrationSettingsItem.java @@ -43,32 +43,33 @@ public class RegistrationSettingsItem extends WindowsJDialog { private final AxisInnovatorsBox mainWindow; static { - RegistrationSettingsItem registrationSettingsItem = new RegistrationSettingsItem(); - JPanel pluginPanel = createPluginSettingsPanel(); - JPanel generalPanel = createGeneralSettingsPanel(); - JPanel aboutPanel = createAboutPanel(); - JPanel themePanel = createThemePanel(); - - registrationSettingsItem.addSettings( - generalPanel, language.getText("settings.2.title"), - null, language.getText("settings.2.tip"), "system:settings_appearance_item" - ); - registrationSettingsItem.addSettings( - pluginPanel, language.getText("settings.1.title"), - null, language.getText("settings.1.tip"), "system:settings_plugins_item" - ); - registrationSettingsItem.addSettings( - themePanel, language.getText("settings.4.title"), - null, language.getText("settings.4.tip"), "system:settings_theme_item" - ); - registrationSettingsItem.addSettings( - aboutPanel, language.getText("settings.3.title"), - null, language.getText("settings.3.tip"), "system:settings_information_item" - ); - - registrationSettingsItemList.add( - registrationSettingsItem - ); + //RegistrationSettingsItem registrationSettingsItem = new RegistrationSettingsItem(); + //JPanel pluginPanel = createPluginSettingsPanel(); + //JPanel generalPanel = createGeneralSettingsPanel(); + //JPanel aboutPanel = createAboutPanel(); + //JPanel themePanel = createThemePanel(); +// + //registrationSettingsItem.addSettings( + // generalPanel, language.getText("settings.2.title"), + // null, language.getText("settings.2.tip"), "system:settings_appearance_item" + //); + //registrationSettingsItem.addSettings( + // pluginPanel, language.getText("settings.1.title"), + // null, language.getText("settings.1.tip"), "system:settings_plugins_item" + //); + //registrationSettingsItem.addSettings( + // themePanel, language.getText("settings.4.title"), + // null, language.getText("settings.4.tip"), "system:settings_theme_item" + //); + //registrationSettingsItem.addSettings( + // aboutPanel, language.getText("settings.3.title"), + // null, language.getText("settings.3.tip"), "system:settings_information_item" + //); +// + //registrationSettingsItemList.add( + // registrationSettingsItem + //); + overloading(); } public static void overloading() { @@ -624,7 +625,7 @@ public class RegistrationSettingsItem extends WindowsJDialog { MainWindow mainWindow = getMainWindow(); if (mainWindow != null) { mainWindow.setBackgroundWithGlassEffect(bgImage, blurAmount,1.0f); - logger.info("图片背景已应用,模糊度: " + blurAmount); + logger.info("图片背景已应用,模糊度: {}", blurAmount); // 保存设置到 StateManager String backgroundPath = (String) backgroundPreview.getClientProperty("backgroundPath"); @@ -957,7 +958,7 @@ public class RegistrationSettingsItem extends WindowsJDialog { settingsManager.saveState("background.path", backgroundPath); settingsManager.saveState("background.blur", blurAmount); settingsManager.saveState("background.enabled", true); - logger.info("背景设置已保存: " + backgroundPath + ", 模糊度: " + blurAmount); + logger.info("背景设置已保存: {}, 模糊度: {}", backgroundPath, blurAmount); } } diff --git a/src/main/java/com/axis/innovators/box/register/RegistrationTool.java b/src/main/java/com/axis/innovators/box/register/RegistrationTool.java index 4ad99a4..e910bbe 100644 --- a/src/main/java/com/axis/innovators/box/register/RegistrationTool.java +++ b/src/main/java/com/axis/innovators/box/register/RegistrationTool.java @@ -82,6 +82,16 @@ public class RegistrationTool { } })); + programmingToolsCategory.addTool(new MainWindow.ToolItem("数据库管理工具", "programming/programming_dark.png", + "用于管理数据库" + + "\n作者:tzdwindows 7", ++id, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + // Window owner = SwingUtilities.windowForComponent((Component) e.getSource()); + MainApplication.popupDataBaseWindow(); + } + })); + MainWindow.ToolCategory aICategory = new MainWindow.ToolCategory("AI工具", "ai/ai.png", "人工智能/大语言模型"); diff --git a/src/main/java/com/axis/innovators/box/window/MainWindow.java b/src/main/java/com/axis/innovators/box/window/MainWindow.java index 05dd1c6..be796e6 100644 --- a/src/main/java/com/axis/innovators/box/window/MainWindow.java +++ b/src/main/java/com/axis/innovators/box/window/MainWindow.java @@ -938,7 +938,15 @@ public class MainWindow extends JFrame { // ---------- 工具卡/面板 ---------- private JPanel createToolsPanel(ToolCategory category) { - JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16)); + JPanel panel = new JPanel(new WrapLayout(FlowLayout.LEFT, 16, 16)) { + @Override + public Dimension getPreferredSize() { + // 计算容器宽度以恰好容纳3个卡片和间隙 + int cardWidth = 240; // 卡片宽度 + int gap = 16; + return new Dimension(cardWidth * 3 + gap * 2, super.getPreferredSize().height); + } + }; panel.setOpaque(false); panel.setBorder(null); diff --git a/src/main/resources/icons/programming/JarApiViewer/database_dark.png b/src/main/resources/icons/programming/JarApiViewer/database_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..434579c7d9ca5d569bb70255b600ee5a302da637 GIT binary patch literal 16127 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/icons/programming/database_dark.png b/src/main/resources/icons/programming/database_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..434579c7d9ca5d569bb70255b600ee5a302da637 GIT binary patch literal 16127 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