diff --git a/.gitignore b/.gitignore
index ab1a582..613dc6c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -68,3 +68,5 @@ out/
# 临时环境变量文件
.env
*.sqlite
+AGENTS.md
+.vscode/launch.json
diff --git a/pom.xml b/pom.xml
index 6704e0d..c279739 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,6 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
xin.ctkqiang
exploitdb
1.0-SNAPSHOT
@@ -42,6 +41,14 @@
xin.ctkqiang.Main
+
+ org.openjfx
+ javafx-maven-plugin
+ 0.0.8
+
+ xin.ctkqiang.gui.App
+
+
@@ -102,6 +109,21 @@
1.10.0
+
+
+ org.openjfx
+ javafx-controls
+ 11.0.2
+
+
+ org.openjfx
+ javafx-fxml
+ 11.0.2
+
+
+ org.slf4j
+ slf4j-simple
+ 1.7.36
+
-
\ No newline at end of file
diff --git a/src/main/java/xin/ctkqiang/config/Configuration.java b/src/main/java/xin/ctkqiang/config/Configuration.java
index 2c08799..9bb5363 100644
--- a/src/main/java/xin/ctkqiang/config/Configuration.java
+++ b/src/main/java/xin/ctkqiang/config/Configuration.java
@@ -6,15 +6,17 @@ public class Configuration {
private static String DatabaseMode = Database.MYSQL.getValue();
public static final String DB_NAME = "ExploitDB";
- public static final String DB_URL = "jdbc:mysql://localhost:3306/" + DB_NAME
+
+ // MySQL 配置(可修改)
+ private static String DB_URL = "jdbc:mysql://localhost:3306/" + DB_NAME
+ "?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false";
+ private static String DB_USER = "root";
+ private static String DB_PASSWORD = "";
+
+ // SQLite 配置(可修改)
+ private static String DB_SQLITE_URL = "jdbc:sqlite:ling.sqlite";
- public static final String DB_SQLITE_URL = "jdbc:sqlite:ling.sqlite";
-
- public static final String DB_USER = "root";
- public static final String DB_PASSWORD = "";
public static final String DB_DRIVER = "com.mysql.cj.jdbc.Driver";
-
public static final boolean DEBUG = false;
public static String getDatabaseMode() {
@@ -25,4 +27,38 @@ public static void setDatabaseMode(Database databaseMode) {
DatabaseMode = databaseMode.getValue();
System.out.println(String.format("🎀 你当前选择的数据库模式是:「%s」~酱酱 ♪(๑˃ᴗ˂)ﻭ \n", getDatabaseMode()));
}
+
+ // Getters
+ public static String getDbUrl() {
+ return DB_URL;
+ }
+
+ public static String getDbUser() {
+ return DB_USER;
+ }
+
+ public static String getDbPassword() {
+ return DB_PASSWORD;
+ }
+
+ public static String getDbSqliteUrl() {
+ return DB_SQLITE_URL;
+ }
+
+ // Setters
+ public static void setDbUrl(String url) {
+ DB_URL = url;
+ }
+
+ public static void setDbUser(String user) {
+ DB_USER = user;
+ }
+
+ public static void setDbPassword(String password) {
+ DB_PASSWORD = password;
+ }
+
+ public static void setDbSqliteUrl(String url) {
+ DB_SQLITE_URL = url;
+ }
}
diff --git a/src/main/java/xin/ctkqiang/controller/DatabaseController.java b/src/main/java/xin/ctkqiang/controller/DatabaseController.java
index 7bcd0bb..40c1356 100644
--- a/src/main/java/xin/ctkqiang/controller/DatabaseController.java
+++ b/src/main/java/xin/ctkqiang/controller/DatabaseController.java
@@ -6,6 +6,7 @@
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
+import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
@@ -35,21 +36,9 @@ public class DatabaseController {
/** 数据库名称 */
protected final static String DB_NAME = Configuration.DB_NAME;
- /** MYSQL 数据库连接URL */
- protected final static String DB_URL = Configuration.DB_URL;
-
- /** 数据库用户名 */
- protected final static String DB_USER = Configuration.DB_USER;
-
- /** 数据库密码 */
- protected final static String DB_PASSWORD = Configuration.DB_PASSWORD;
-
/** 数据库驱动类名 */
protected final static String DB_DRIVER = Configuration.DB_DRIVER;
- /** SQLITE 数据库连接URL */
- protected final static String DB_SQLITE_URL = Configuration.DB_SQLITE_URL;
-
/**
* 静态初始化块
* 在类加载时尝试加载数据库驱动
@@ -68,7 +57,7 @@ public class DatabaseController {
* @return 数据库连接对象
* @throws SQLException 如果连接数据库时发生错误
*/
- protected Connection getConnection() throws SQLException {
+ public Connection getConnection() throws SQLException {
String mode = Configuration.getDatabaseMode();
String upperMode = mode.toUpperCase();
System.out.println("💡 切换数据库模式 -> " + upperMode);
@@ -82,21 +71,21 @@ protected Connection getConnection() throws SQLException {
System.err.println("❌ 哎呀呀!SQLite JDBC 驱动不见惹~人家找不到驱动怎么贴贴数据库嘛喵呜呜。゚(゚´ω`゚)゚。");
throw new SQLException("找不到 SQLite 驱动喵~要不要检查一下依赖有没有加对呀?");
}
- return DriverManager.getConnection(DatabaseController.DB_SQLITE_URL);
+ return DriverManager.getConnection(Configuration.getDbSqliteUrl());
case "MYSQL":
default:
try {
- Class.forName(DatabaseController.DB_DRIVER);
+ Class.forName(DB_DRIVER);
System.out.println("📦 MySQL 驱动加载完成!我打扮好了,要去连接数据库小哥哥啦~(๑•̀ㅂ•́)و✧");
} catch (ClassNotFoundException e) {
System.err.println("❌ 呜呜呜 MySQL JDBC 驱动不见了~是不是打包的时候忘记带灵儿一起走啦 >///< ");
throw new SQLException("MySQL 驱动加载失败了喵~快检查一下 `.jar` 有没有遗漏吧!");
}
return DriverManager.getConnection(
- DatabaseController.DB_URL,
- DatabaseController.DB_USER,
- DatabaseController.DB_PASSWORD);
+ Configuration.getDbUrl(),
+ Configuration.getDbUser(),
+ Configuration.getDbPassword());
}
}
@@ -165,7 +154,7 @@ public void CreateTableIfNotExists() {
Class.forName("org.sqlite.JDBC");
// 构建连接(数据库文件叫 ling.sqlite)
- try (Connection conn = DriverManager.getConnection(DatabaseController.DB_SQLITE_URL);
+ try (Connection conn = DriverManager.getConnection(Configuration.getDbSqliteUrl());
Statement stmt = conn.createStatement()) {
stmt.execute(query);
System.out.println("🎀 SQLite 模式下,表结构初始化好啦!已经准备好可爱的记录了喵~");
@@ -175,13 +164,13 @@ public void CreateTableIfNotExists() {
case MYSQL:
default:
// 确保数据库驱动已加载
- Class.forName(DatabaseController.DB_DRIVER);
+ Class.forName(DB_DRIVER);
// 创建数据库连接并执行建表语句
try (Connection conn = DriverManager.getConnection(
- DatabaseController.DB_URL,
- DatabaseController.DB_USER,
- DatabaseController.DB_PASSWORD);
+ Configuration.getDbUrl(),
+ Configuration.getDbUser(),
+ Configuration.getDbPassword());
Statement stmt = conn.createStatement()) {
stmt.execute(query);
System.out.println("💾 MySQL 模式下,数据表【内容信息】初始化成功~♡");
@@ -273,7 +262,7 @@ public List GetAllExploits() {
Class.forName(DB_DRIVER);
// 创建数据库连接并执行查询
- try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
+ try (Connection conn = DriverManager.getConnection(Configuration.getDbUrl(), Configuration.getDbUser(), Configuration.getDbPassword());
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(query)) {
@@ -434,7 +423,7 @@ public int ExportToSQL(List exploits) {
* @return 完整的文件路径
*/
private String FilePath(int size, String extension) {
- DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH:mm:ss");
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss");
LocalDateTime now = LocalDateTime.now();
return "output/Exploits_" + size + "_" + formatter.format(now) + extension;
@@ -480,6 +469,41 @@ public int ExportToCSV(List exploits) {
return 0;
}
+ /**
+ * 数据库分页查询
+ *
+ * @param offset 起始位置
+ * @param limit 查询数量
+ * @return
+ */
+ public List GetExploitsByPage(int offset, int limit) {
+ String query = "SELECT * FROM records ORDER BY created_at DESC LIMIT ? OFFSET ?";
+ List exploits = new ArrayList<>();
+
+ try (Connection conn = getConnection();
+ PreparedStatement stmt = conn.prepareStatement(query)) {
+ stmt.setInt(1, limit);
+ stmt.setInt(2, offset);
+
+ try (ResultSet rs = stmt.executeQuery()) {
+ while (rs.next()) {
+ Exploit e = new Exploit();
+ e.setId(rs.getString("id"));
+ e.setDescription(rs.getString("description"));
+ e.setDate(rs.getString("date"));
+ e.setAuthor(rs.getString("author"));
+ e.setType(rs.getString("type"));
+ e.setPlatform(rs.getString("platform"));
+ e.setCve(rs.getString("cve"));
+ exploits.add(e);
+ }
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return exploits;
+ }
+
/**
* 转义SQL字符串中的特殊字符
* 主要处理单引号,防止SQL注入
diff --git a/src/main/java/xin/ctkqiang/controller/ExploitDbController.java b/src/main/java/xin/ctkqiang/controller/ExploitDbController.java
index 68b3601..319d98f 100644
--- a/src/main/java/xin/ctkqiang/controller/ExploitDbController.java
+++ b/src/main/java/xin/ctkqiang/controller/ExploitDbController.java
@@ -13,6 +13,7 @@
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
+
import org.apache.commons.text.StringEscapeUtils;
/**
@@ -39,6 +40,10 @@ public class ExploitDbController extends DatabaseController {
private static final List BLOCK_KEYWORDS = Arrays.asList(
"firewall", "access denied", "blocked", "forbidden", "connection refused");
+ public interface CrawlCallback {
+ void onPageLoaded(List pageData, int currentCount, int totalCount, int PageNumber);
+ }
+
/**
* 获取数据库中所有的漏洞信息
*
@@ -85,6 +90,18 @@ private int create(String id, String description, String date, String author, St
* @param extension 导出文件的扩展名(支持:csv, json, sql)
*/
public void Crawl(int pageSize, boolean isExport, String extension) {
+ Crawl(pageSize, isExport, extension, null);
+ }
+
+ /**
+ * 爬取Exploit-DB网站的漏洞信息
+ *
+ * @param pageSize 每页获取的记录数量
+ * @param isExport 是否导出数据
+ * @param extension 导出文件的扩展名(支持:csv, json, sql)
+ * @param callback 爬取完成后的回调函数
+ */
+ public void Crawl(int pageSize, boolean isExport, String extension, CrawlCallback callback) {
// 获取数据总条数
int total = 0;
try {
@@ -107,7 +124,8 @@ public void Crawl(int pageSize, boolean isExport, String extension) {
}
// 构建API请求URL,包含分页参数
int want = Math.min(pageSize, total);
- // String url = String.format(ExploitDbController.URL + "search?draw=1&start=%d&length=%d", start, pageSize);
+ // String url = String.format(ExploitDbController.URL +
+ // "search?draw=1&start=%d&length=%d", start, pageSize);
// 初始化状态码和结果列表
int Status = 0;
@@ -136,7 +154,7 @@ public void Crawl(int pageSize, boolean isExport, String extension) {
// 调试模式下打印响应预览
if (debug) {
- System.out.println("响应内容预览:" + Response.body().substring(0, 500));
+ System.out.println("响应内容预览:" + Response.body().substring(0, 500));
}
// 解析JSON响应
@@ -153,7 +171,6 @@ public void Crawl(int pageSize, boolean isExport, String extension) {
System.out.println("共找到 " + Data.size() + " 条记录。");
System.out.println("正在写入数据库...");
-
// 调试模式下打印完整数据
if (debug) {
System.out.println(Data);
@@ -225,13 +242,23 @@ public void Crawl(int pageSize, boolean isExport, String extension) {
}
}
- // 添加到结果列表
- exploits.add(exploit);
+ // 添加到结果列表
+ exploits.add(exploit);
+ }
+ int previousCount = 0;
+
+ if (callback != null) {
+ List currentPageData = new ArrayList<>(
+ exploits.subList(previousCount, exploits.size()));
+ callback.onPageLoaded(currentPageData, exploits.size(), want, pageSize);
+ previousCount = exploits.size(); // 更新计数
+ }
+
+ if (exploits.size() >= want)
+ break;
+ Thread.sleep(1000);
}
- if (exploits.size() >= want) break;
- Thread.sleep(1000);
- }
- } catch (IOException | InterruptedException e) {
+ } catch (IOException | InterruptedException e) {
String msg = e.getMessage();
if (msg != null && BLOCK_KEYWORDS.stream().anyMatch(k -> msg.toLowerCase().contains(k))) {
@@ -410,4 +437,16 @@ private void ExportStatus(int status, String ext) {
System.out.println(msg);
}
+ // public ObservableList loadPage(int offset, int pageSize) {
+ // String sql = "SELECT * FROM exploits LIMIT ? OFFSET ?";
+ // ObservableList list = FXCollections.observableArrayList();
+ // try (
+ // Connection conn = super.getConnection();
+ // PreparedStatement ps = conn.prepareStatement(sql)) {
+ // ps.setInt(1, pageSize);
+ // } catch (SQLException e) {
+
+ // }
+ // }
+
}
diff --git a/src/main/java/xin/ctkqiang/dto/Exploit.java b/src/main/java/xin/ctkqiang/dto/Exploit.java
index d6754a3..2173b5f 100644
--- a/src/main/java/xin/ctkqiang/dto/Exploit.java
+++ b/src/main/java/xin/ctkqiang/dto/Exploit.java
@@ -1,68 +1,71 @@
package xin.ctkqiang.dto;
+import javafx.beans.property.SimpleStringProperty;
+import javafx.beans.property.StringProperty;
+
public class Exploit {
- private String id;
- private String description;
- private String date;
- private String author;
- private String type;
- private String platform;
- private String cve;
+ private StringProperty id = new SimpleStringProperty("");
+ private StringProperty description = new SimpleStringProperty("");
+ private StringProperty date = new SimpleStringProperty("");
+ private StringProperty author = new SimpleStringProperty("");
+ private StringProperty type = new SimpleStringProperty("");
+ private StringProperty platform = new SimpleStringProperty("");
+ private StringProperty cve = new SimpleStringProperty("");
public String getId() {
- return id;
+ return id.get();
}
public void setId(String id) {
- this.id = id;
+ this.id.set(id);
}
public String getDescription() {
- return description;
+ return description.get();
}
public void setDescription(String description) {
- this.description = description;
+ this.description.set(description);
}
public String getDate() {
- return date;
+ return date.get();
}
public void setDate(String date) {
- this.date = date;
+ this.date.set(date);
}
public String getAuthor() {
- return author;
+ return author.get();
}
public void setAuthor(String author) {
- this.author = author;
+ this.author.set(author);
}
public String getType() {
- return type;
+ return type.get();
}
public void setType(String type) {
- this.type = type;
+ this.type.set(type);
}
public String getPlatform() {
- return platform;
+ return platform.get();
}
public void setPlatform(String platform) {
- this.platform = platform;
+ this.platform.set(platform);
}
public String getCve() {
- return cve == null ? "N/A" : cve;
+ return cve.get() == null ? "N/A" : cve.get();
}
public void setCve(String cve) {
- this.cve = cve;
+ this.cve.set(cve);
}
}
diff --git a/src/main/java/xin/ctkqiang/gui/AboutDialog.java b/src/main/java/xin/ctkqiang/gui/AboutDialog.java
new file mode 100644
index 0000000..f4fd0cf
--- /dev/null
+++ b/src/main/java/xin/ctkqiang/gui/AboutDialog.java
@@ -0,0 +1,54 @@
+package xin.ctkqiang.gui;
+
+import javafx.animation.PauseTransition;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Label;
+import javafx.scene.image.Image;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+import javafx.util.Duration;
+
+public class AboutDialog {
+ public static void show() {
+ Stage win = new Stage();
+ win.initModality(Modality.APPLICATION_MODAL);
+ win.setTitle("关于");
+ win.setResizable(false);
+
+ // 加载图标
+ try {
+ win.getIcons().add(new Image(AboutDialog.class.getResourceAsStream("/icon.png")));
+ } catch (Exception e) {
+ System.err.println("图标加载失败: " + e.getMessage());
+ }
+
+ Label title = new Label("ExploitDB GUI v1.0");
+ title.setStyle("-fx-font-size: 20px; -fx-font-weight: bold;");
+
+ Label info = new Label(
+ "作者:钟智强\n" +
+ "邮箱:ctkqiang@dingtalk.com\n" +
+ "GitHub:https://github.com/ctkqiang"
+ );
+ info.setStyle("-fx-font-size: 14px; -fx-line-spacing: 6px");
+ info.setAlignment(Pos.CENTER);
+
+ VBox root = new VBox(15, title, info);
+ root.setPadding(new Insets(30));
+ root.setAlignment(Pos.CENTER);
+
+ win.setScene(new Scene(root, 400, 200));
+
+ // 2秒后自动关闭
+ PauseTransition delay = new PauseTransition(Duration.seconds(2));
+ delay.setOnFinished(e -> {
+ win.close();
+ });
+ delay.play();
+
+ win.showAndWait();
+ }
+}
diff --git a/src/main/java/xin/ctkqiang/gui/App.java b/src/main/java/xin/ctkqiang/gui/App.java
new file mode 100644
index 0000000..2d516a4
--- /dev/null
+++ b/src/main/java/xin/ctkqiang/gui/App.java
@@ -0,0 +1,70 @@
+package xin.ctkqiang.gui;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.stage.Stage;
+
+/**
+ * GUI 应用程序入口
+ */
+public class App extends Application {
+
+ @Override
+ public void init() throws Exception {
+ super.init();
+ // 设置全局异常处理器
+ setupGlobalExceptionHandler();
+ }
+
+ @Override
+ public void start(Stage primaryStage) {
+ try {
+ AboutDialog.show();
+ new MainWindow().start(primaryStage);
+ } catch (Exception e) {
+ // 启动过程中的异常
+ ExceptionHandler.showError("程序启动失败", e);
+ Platform.exit();
+ }
+ }
+
+ @Override
+ public void stop() throws Exception {
+ super.stop();
+ // 程序退出前的清理
+ System.out.println("程序正在退出...");
+ }
+
+ /**
+ * 设置全局异常处理器
+ * 捕获所有未处理的异常并显示对话框
+ */
+ private void setupGlobalExceptionHandler() {
+ // 处理 JavaFX 应用线程中的未捕获异常
+ Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
+ System.err.println("未捕获的异常在线程: " + thread.getName());
+ throwable.printStackTrace();
+
+ // 在 FX 线程显示错误对话框
+ if (Platform.isFxApplicationThread()) {
+ ExceptionHandler.showError(throwable);
+ } else {
+ Platform.runLater(() -> {
+ String title = "程序异常";
+ String message = throwable.getMessage();
+ if (message == null) {
+ message = "发生未处理的错误: " + throwable.getClass().getSimpleName();
+ }
+ ExceptionDialog.showException(title, message, throwable);
+ });
+ }
+ });
+
+ // 处理 FX 工具包中的异常
+ System.setProperty("javafx.embed.singleThread", "true");
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+}
diff --git a/src/main/java/xin/ctkqiang/gui/ExceptionDialog.java b/src/main/java/xin/ctkqiang/gui/ExceptionDialog.java
new file mode 100644
index 0000000..ffb04ce
--- /dev/null
+++ b/src/main/java/xin/ctkqiang/gui/ExceptionDialog.java
@@ -0,0 +1,190 @@
+package xin.ctkqiang.gui;
+
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Accordion;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Alert.AlertType;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.control.TextArea;
+import javafx.scene.control.TitledPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.stage.Modality;
+import javafx.stage.Stage;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.sql.SQLException;
+
+/**
+ * 异常对话框
+ * 用于显示程序运行过程中的错误信息
+ */
+public class ExceptionDialog {
+
+ /**
+ * 显示简单错误提示(只显示消息)
+ * @param title 标题
+ * @param message 错误消息
+ */
+ public static void showError(String title, String message) {
+ Alert alert = new Alert(AlertType.ERROR);
+ alert.setTitle("错误");
+ alert.setHeaderText(title);
+ alert.setContentText(message);
+ alert.showAndWait();
+ }
+
+ /**
+ * 显示警告提示
+ * @param title 标题
+ * @param message 警告消息
+ */
+ public static void showWarning(String title, String message) {
+ Alert alert = new Alert(AlertType.WARNING);
+ alert.setTitle("警告");
+ alert.setHeaderText(title);
+ alert.setContentText(message);
+ alert.showAndWait();
+ }
+
+ /**
+ * 显示信息提示
+ * @param title 标题
+ * @param message 信息消息
+ */
+ public static void showInfo(String title, String message) {
+ Alert alert = new Alert(AlertType.INFORMATION);
+ alert.setTitle("提示");
+ alert.setHeaderText(title);
+ alert.setContentText(message);
+ alert.showAndWait();
+ }
+
+ /**
+ * 显示详细异常对话框(带堆栈跟踪)
+ * @param title 标题
+ * @param message 简要说明
+ * @param throwable 异常对象
+ */
+ public static void showException(String title, String message, Throwable throwable) {
+ Stage dialog = new Stage();
+ dialog.initModality(Modality.APPLICATION_MODAL);
+ dialog.setTitle("程序异常");
+ dialog.setResizable(true);
+ dialog.setMinWidth(500);
+ dialog.setMinHeight(400);
+
+ // 图标
+ try {
+ dialog.getIcons().add(new javafx.scene.image.Image(
+ ExceptionDialog.class.getResourceAsStream("/icon.png")));
+ } catch (Exception ignored) {}
+
+ // 标题和图标
+ Label iconLabel = new Label("❌");
+ iconLabel.setStyle("-fx-font-size: 48px;");
+
+ Label titleLabel = new Label(title);
+ titleLabel.setStyle("-fx-font-size: 18px; -fx-font-weight: bold; -fx-text-fill: #d9534f;");
+
+ Label messageLabel = new Label(message != null ? message : throwable.getMessage());
+ messageLabel.setWrapText(true);
+ messageLabel.setStyle("-fx-font-size: 12px;");
+
+ // 异常类型和建议
+ VBox infoBox = new VBox(5);
+ infoBox.setAlignment(Pos.CENTER);
+ infoBox.getChildren().addAll(titleLabel, messageLabel);
+
+ // 获取异常类型和建议
+ String suggestion = getSuggestion(throwable);
+ if (suggestion != null) {
+ Label suggestLabel = new Label(suggestion);
+ suggestLabel.setWrapText(true);
+ suggestLabel.setStyle("-fx-font-size: 12px; -fx-text-fill: #5bc0de;");
+ infoBox.getChildren().add(suggestLabel);
+ }
+
+ // 详细堆栈(可折叠)
+ StringWriter sw = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(sw));
+ String stackTrace = sw.toString();
+
+ TextArea stackArea = new TextArea(stackTrace);
+ stackArea.setEditable(false);
+ stackArea.setPrefHeight(200);
+ stackArea.setStyle("-fx-font-family: monospace; -fx-font-size: 11px;");
+
+ TitledPane stackPane = new TitledPane("查看详细错误信息(点击展开)", stackArea);
+ stackPane.setExpanded(false);
+
+ Accordion accordion = new Accordion(stackPane);
+
+ // 按钮
+ Button copyBtn = new Button("📋 复制错误信息");
+ copyBtn.setOnAction(e -> {
+ javafx.scene.input.Clipboard clipboard = javafx.scene.input.Clipboard.getSystemClipboard();
+ javafx.scene.input.ClipboardContent content = new javafx.scene.input.ClipboardContent();
+ content.putString(stackTrace);
+ clipboard.setContent(content);
+ copyBtn.setText("✅ 已复制");
+ });
+
+ Button closeBtn = new Button("关闭");
+ closeBtn.setStyle("-fx-background-color: #d9534f; -fx-text-fill: white;");
+ closeBtn.setPrefWidth(80);
+ closeBtn.setOnAction(e -> dialog.close());
+
+ HBox buttonBox = new HBox(15, copyBtn, closeBtn);
+ buttonBox.setAlignment(Pos.CENTER);
+ buttonBox.setPadding(new Insets(10, 0, 0, 0));
+
+ // 组装
+ VBox root = new VBox(15);
+ root.setPadding(new Insets(20));
+ root.setAlignment(Pos.TOP_CENTER);
+ root.getChildren().addAll(iconLabel, infoBox, accordion, buttonBox);
+
+ Scene scene = new Scene(root, 550, 350);
+ dialog.setScene(scene);
+ dialog.showAndWait();
+ }
+
+ /**
+ * 根据异常类型获取建议
+ */
+ private static String getSuggestion(Throwable e) {
+ if (e instanceof SQLException) {
+ String msg = e.getMessage().toLowerCase();
+ if (msg.contains("communications link failure") || msg.contains("connection refused")) {
+ return "💡 建议:请检查 MySQL 服务是否已启动,或切换到 SQLite 模式";
+ } else if (msg.contains("access denied")) {
+ return "💡 建议:请检查数据库用户名和密码是否正确";
+ } else if (msg.contains("unknown database")) {
+ return "💡 建议:请先在 MySQL 中创建数据库 'ExploitDB'";
+ } else if (msg.contains("driver")) {
+ return "💡 建议:数据库驱动未找到,请检查项目依赖配置";
+ }
+ return "💡 建议:请检查数据库配置(系统设置 → 数据库配置)";
+ } else if (e instanceof java.net.ConnectException || e.getMessage().contains("Network is unreachable")) {
+ return "💡 建议:请检查网络连接,或稍后再试";
+ } else if (e instanceof java.io.FileNotFoundException) {
+ return "💡 建议:文件不存在或路径错误,请检查文件路径";
+ } else if (e instanceof java.io.IOException) {
+ return "💡 建议:文件操作失败,请检查磁盘空间和写入权限";
+ } else if (e instanceof java.lang.OutOfMemoryError) {
+ return "💡 建议:内存不足,请尝试减少爬取数量或增加 JVM 内存";
+ } else if (e instanceof InterruptedException) {
+ return "💡 建议:操作被中断";
+ } else if (e instanceof NumberFormatException) {
+ return "💡 建议:输入格式错误,请输入有效的数字";
+ } else if (e instanceof IllegalArgumentException) {
+ return "💡 建议:参数错误,请检查输入值";
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/xin/ctkqiang/gui/ExceptionHandler.java b/src/main/java/xin/ctkqiang/gui/ExceptionHandler.java
new file mode 100644
index 0000000..77731e6
--- /dev/null
+++ b/src/main/java/xin/ctkqiang/gui/ExceptionHandler.java
@@ -0,0 +1,139 @@
+package xin.ctkqiang.gui;
+
+import javafx.application.Platform;
+
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+
+/**
+ * 异常处理器工具类
+ * 提供便捷的异常处理包装方法
+ */
+public class ExceptionHandler {
+
+ /**
+ * 包装 Runnable,在 FX 线程中捕获并显示异常
+ * @param action 要执行的操作
+ * @return 包装后的 Runnable
+ */
+ public static Runnable wrap(Runnable action) {
+ return () -> {
+ try {
+ action.run();
+ } catch (Exception e) {
+ showError(e);
+ }
+ };
+ }
+
+ /**
+ * 包装 Runnable,在 FX 线程中捕获并显示异常,带错误提示
+ * @param action 要执行的操作
+ * @param errorTitle 错误标题
+ * @return 包装后的 Runnable
+ */
+ public static Runnable wrap(Runnable action, String errorTitle) {
+ return () -> {
+ try {
+ action.run();
+ } catch (Exception e) {
+ showError(errorTitle, e);
+ }
+ };
+ }
+
+ /**
+ * 在后台线程执行操作,异常在 FX 线程显示
+ * @param action 要执行的操作
+ * @param onSuccess 成功回调(在 FX 线程)
+ * @param errorTitle 错误标题
+ */
+ public static void runAsync(Callable action, Consumer onSuccess, String errorTitle) {
+ new Thread(() -> {
+ try {
+ T result = action.call();
+ if (onSuccess != null) {
+ Platform.runLater(() -> onSuccess.accept(result));
+ }
+ } catch (Exception e) {
+ Platform.runLater(() -> showError(errorTitle, e));
+ }
+ }).start();
+ }
+
+ /**
+ * 在后台线程执行操作,异常在 FX 线程显示(无返回值)
+ * @param action 要执行的操作
+ * @param onSuccess 成功回调(在 FX 线程)
+ * @param errorTitle 错误标题
+ */
+ public static void runAsync(Runnable action, Runnable onSuccess, String errorTitle) {
+ new Thread(() -> {
+ try {
+ action.run();
+ if (onSuccess != null) {
+ Platform.runLater(onSuccess);
+ }
+ } catch (Exception e) {
+ Platform.runLater(() -> showError(errorTitle, e));
+ }
+ }).start();
+ }
+
+ /**
+ * 在 FX 线程显示异常对话框
+ * @param e 异常对象
+ */
+ public static void showError(Throwable e) {
+ Platform.runLater(() -> {
+ String title = "操作失败";
+ String message = e.getMessage();
+ if (message == null || message.isEmpty()) {
+ message = "发生未知错误";
+ }
+ ExceptionDialog.showException(title, message, e);
+ });
+ }
+
+ /**
+ * 在 FX 线程显示异常对话框
+ * @param title 错误标题
+ * @param e 异常对象
+ */
+ public static void showError(String title, Throwable e) {
+ Platform.runLater(() -> {
+ String message = e.getMessage();
+ if (message == null || message.isEmpty()) {
+ message = "发生未知错误";
+ }
+ ExceptionDialog.showException(title, message, e);
+ });
+ }
+
+ /**
+ * 显示简单错误信息
+ * @param title 标题
+ * @param message 消息
+ */
+ public static void showError(String title, String message) {
+ Platform.runLater(() -> ExceptionDialog.showError(title, message));
+ }
+
+ /**
+ * 显示警告信息
+ * @param title 标题
+ * @param message 消息
+ */
+ public static void showWarning(String title, String message) {
+ Platform.runLater(() -> ExceptionDialog.showWarning(title, message));
+ }
+
+ /**
+ * 显示提示信息
+ * @param title 标题
+ * @param message 消息
+ */
+ public static void showInfo(String title, String message) {
+ Platform.runLater(() -> ExceptionDialog.showInfo(title, message));
+ }
+}
diff --git a/src/main/java/xin/ctkqiang/gui/MainWindow.java b/src/main/java/xin/ctkqiang/gui/MainWindow.java
new file mode 100644
index 0000000..ca2c8df
--- /dev/null
+++ b/src/main/java/xin/ctkqiang/gui/MainWindow.java
@@ -0,0 +1,805 @@
+package xin.ctkqiang.gui;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javafx.application.Application;
+import javafx.application.Platform;
+import javafx.collections.FXCollections;
+import javafx.collections.ObservableList;
+import javafx.collections.transformation.FilteredList;
+import javafx.concurrent.Task;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.ComboBox;
+import javafx.scene.control.Label;
+import javafx.scene.control.PasswordField;
+import javafx.scene.control.ProgressBar;
+import javafx.scene.control.Spinner;
+import javafx.scene.control.TableColumn;
+import javafx.scene.control.TableView;
+import javafx.scene.control.TextField;
+import javafx.scene.control.TextFormatter;
+import javafx.scene.control.cell.PropertyValueFactory;
+import javafx.scene.image.Image;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.Priority;
+import javafx.scene.layout.Region;
+import javafx.scene.layout.StackPane;
+import javafx.scene.layout.VBox;
+import javafx.stage.Stage;
+import xin.ctkqiang.config.Configuration;
+import xin.ctkqiang.controller.DatabaseController;
+import xin.ctkqiang.controller.ExploitDbController;
+import xin.ctkqiang.dto.Database;
+import xin.ctkqiang.dto.Exploit;
+
+public class MainWindow extends Application {
+ private StackPane crawlerPage;
+ private StackPane dataPage;
+ private StackPane settingsPage;
+
+ private Button crawlerBtn;
+ private Button dataBtn;
+ private Button settingsBtn;
+
+ @Override
+ public void start(Stage primaryStage) {
+ BorderPane root = new BorderPane();
+ // MenuBar manuBar = new MenuBar();
+ // // 文件菜单
+ // Menu fileMenu = new Menu("文件");
+ // MenuItem exportItem = new MenuItem("导出csv");
+ // MenuItem exitItem = new MenuItem("退出");
+ // fileMenu.getItems().addAll(exportItem, exitItem);
+
+ // Menu helpMenu = new Menu("帮助");
+ // MenuItem aboutItem = new MenuItem("关于");
+ // aboutItem.setOnAction(e -> AboutDialog.show());
+ // helpMenu.getItems().add(aboutItem);
+
+ // manuBar.getMenus().addAll(fileMenu, helpMenu);
+ // root.setTop(manuBar);
+
+ VBox sideBar = new VBox(10);
+ sideBar.setPadding(new Insets(15));
+ sideBar.setStyle("-fx-background-color: #f0f0f0;");
+ sideBar.setPrefWidth(150);
+
+ crawlerBtn = createSidebarButton("爬虫控制");
+ dataBtn = createSidebarButton("数据浏览");
+ settingsBtn = createSidebarButton("系统设置");
+
+ crawlerBtn.setOnAction(e -> showPage(crawlerPage, crawlerBtn));
+ dataBtn.setOnAction(e -> showPage(dataPage, dataBtn));
+ settingsBtn.setOnAction(e -> showPage(settingsPage, settingsBtn));
+
+ sideBar.getChildren().addAll(crawlerBtn, dataBtn, settingsBtn);
+ root.setLeft(sideBar);
+
+ StackPane contentArea = new StackPane();
+ contentArea.setPadding(new Insets(20));
+
+ crawlerPage = createCrawlerPage();
+ dataPage = createDataPage();
+ settingsPage = createSettingsPage();
+
+ crawlerPage.setVisible(true);
+ dataPage.setVisible(false);
+ settingsPage.setVisible(false);
+
+ contentArea.getChildren().addAll(crawlerPage, dataPage, settingsPage);
+ root.setCenter(contentArea);
+
+ HBox statusBar = new HBox(20);
+ statusBar.setPadding(new Insets(10));
+ statusBar.setStyle("-fx-background-color: #e0e0e0;");
+ statusBar.setAlignment(Pos.CENTER_LEFT);
+ root.setBottom(statusBar);
+
+ Scene scene = new Scene(root, 1280, 800);
+ primaryStage.setTitle("ExploitDB 搜索工具");
+ try {
+ primaryStage.getIcons().add(
+ new Image(getClass().getResourceAsStream("/icon.png")));
+ } catch (Exception e) {
+ System.err.println("⚠ 图标加载失败: " + e.getMessage());
+ }
+ primaryStage.setScene(scene);
+ primaryStage.setMinWidth(1100);
+ primaryStage.setMinHeight(750);
+ primaryStage.show();
+
+ setActiveButton(crawlerBtn);
+ }
+
+ private Button createSidebarButton(String text) {
+ Button btn = new Button(text);
+ btn.setPrefWidth(120);
+ btn.setPrefHeight(40);
+ btn.setStyle(
+ "-fx-font-size: 14px;" +
+ "-fx-background-color: transparent;" +
+ "-fx-cursor: hand;");
+ return btn;
+ }
+
+ private void showPage(StackPane pageToShow, Button clickedBtn) {
+ crawlerPage.setVisible(false);
+ dataPage.setVisible(false);
+ settingsPage.setVisible(false);
+
+ pageToShow.setVisible(true);
+ setActiveButton(clickedBtn);
+ }
+
+ private void setActiveButton(Button activeBtn) {
+ String normalStyle = "-fx-font-size: 14px;" +
+ "-fx-background-color: transparent;" +
+ "-fx-cursor: hand;";
+
+ crawlerBtn.setStyle(normalStyle);
+ dataBtn.setStyle(normalStyle);
+ settingsBtn.setStyle(normalStyle);
+
+ String activeStyle = "-fx-font-size: 14px;" +
+ "-fx-background-color: #007acc;" +
+ "-fx-text-fill: white;" +
+ "-fx-cursor: hand;";
+ activeBtn.setStyle(activeStyle);
+ }
+
+ private StackPane createDataPage() {
+ BorderPane dataLayout = new BorderPane();
+ dataLayout.setPadding(new Insets(10));
+
+ HBox searchBar = new HBox(10);
+ searchBar.setPadding(new Insets(0, 0, 10, 0));
+ searchBar.setAlignment(Pos.CENTER_LEFT);
+
+ TextField searchField = new TextField();
+ searchField.setPromptText("输入关键词搜索...");
+ searchField.setPrefWidth(180);
+ searchField.setMinWidth(120);
+
+ ComboBox authorFilter = new ComboBox<>();
+ authorFilter.setPromptText("作者筛选");
+ authorFilter.setPrefWidth(130);
+ authorFilter.setMinWidth(100);
+
+ ComboBox platformFilter = new ComboBox<>();
+ platformFilter.setPromptText("平台筛选");
+ platformFilter.setPrefWidth(110);
+ platformFilter.setMinWidth(90);
+
+ Button searchBtn = new Button("🔍 搜索");
+ searchBtn.setStyle("-fx-background-color: #007acc; -fx-text-fill: white;");
+ searchBtn.setMinWidth(70);
+
+ Button resetBtn = new Button("♻ 重置");
+ resetBtn.setMinWidth(60);
+
+ Button refreshBtn = new Button("🔄 刷新");
+ refreshBtn.setMinWidth(60);
+
+ ComboBox loadModeBox = new ComboBox<>();
+ loadModeBox.getItems().addAll("加载最新1000条", "加载全部");
+ loadModeBox.setValue("加载最新1000条");
+ loadModeBox.setPrefWidth(180);
+ loadModeBox.setMinWidth(150);
+
+ Label searchLabel = new Label("搜索:");
+ searchLabel.setMinWidth(40);
+ Label loadModeLabel = new Label("加载模式:");
+ loadModeLabel.setMinWidth(60);
+
+ searchBar.getChildren().addAll(searchLabel, searchField, authorFilter, platformFilter, searchBtn,
+ resetBtn, refreshBtn, loadModeLabel, loadModeBox);
+
+ dataLayout.setTop(searchBar);
+
+ TableView tableView = new TableView<>();
+ tableView.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
+
+ TableColumn idCol = new TableColumn<>("ID");
+ idCol.setCellValueFactory(new PropertyValueFactory<>("id"));
+ idCol.setPrefWidth(80);
+ idCol.setMinWidth(60);
+
+ TableColumn descCol = new TableColumn<>("描述");
+ descCol.setCellValueFactory(new PropertyValueFactory<>("description"));
+ descCol.setPrefWidth(500);
+ descCol.setMinWidth(300);
+
+ TableColumn dateCol = new TableColumn<>("日期");
+ dateCol.setCellValueFactory(new PropertyValueFactory<>("date"));
+ dateCol.setPrefWidth(120);
+ dateCol.setMinWidth(100);
+
+ TableColumn authorCol = new TableColumn<>("作者");
+ authorCol.setCellValueFactory(new PropertyValueFactory<>("author"));
+ authorCol.setPrefWidth(150);
+ authorCol.setMinWidth(100);
+
+ TableColumn typeCol = new TableColumn<>("类型");
+ typeCol.setCellValueFactory(new PropertyValueFactory<>("type"));
+ typeCol.setPrefWidth(100);
+ typeCol.setMinWidth(80);
+
+ TableColumn platformCol = new TableColumn<>("平台");
+ platformCol.setCellValueFactory(new PropertyValueFactory<>("platform"));
+ platformCol.setPrefWidth(120);
+ platformCol.setMinWidth(80);
+
+ TableColumn cveCol = new TableColumn<>("CVE");
+ cveCol.setCellValueFactory(new PropertyValueFactory<>("cve"));
+ cveCol.setPrefWidth(150);
+ cveCol.setMinWidth(120);
+
+ tableView.getColumns().addAll(idCol, descCol, dateCol, typeCol, platformCol, cveCol);
+
+ ObservableList data = FXCollections.observableArrayList();
+ FilteredList filteredData = new FilteredList<>(data, p -> true);
+ tableView.setItems(filteredData);
+
+ Label placeHolder = new Label("暂无数据,请先爬取或点击刷新");
+ placeHolder.setStyle("-fx-font-size: 14px; -fx-text-fill: gray");
+ tableView.setPlaceholder(placeHolder);
+
+ dataLayout.setCenter(tableView);
+
+ HBox actionBar = new HBox(10);
+ actionBar.setPadding(new Insets(10, 0, 0, 0));
+ actionBar.setAlignment(Pos.CENTER_RIGHT);
+
+ Label countLabel = new Label("共 " + data.size() + " 条记录");
+
+ Label loadStatusLabel = new Label("就绪");
+ ProgressBar loadProgress = new ProgressBar();
+ loadProgress.setVisible(false);
+ Region spacer = new Region();
+ HBox.setHgrow(spacer, Priority.ALWAYS);
+
+ Runnable loadData = () -> {
+ loadStatusLabel.textProperty().unbind();
+ data.clear();
+ loadProgress.setVisible(true);
+ loadProgress.setProgress(-1);
+ loadStatusLabel.setText("");
+
+ Task> loadTask = new Task<>() {
+ @Override
+ protected List call() throws Exception {
+ DatabaseController dbController = new DatabaseController();
+ boolean loadAll = loadModeBox.getValue().equals("加载全部");
+
+ if (loadAll) {
+ updateMessage("正在加载全部数据...");
+ return dbController.GetAllExploits();
+ } else {
+ updateMessage("正在加载最新1000条");
+ return dbController.GetExploitsByPage(0, 1000);
+ }
+ }
+
+ @Override
+ protected void succeeded() {
+ super.succeeded();
+ List result = getValue();
+ data.addAll(result);
+
+ Set authors = result.stream().map(Exploit::getAuthor).filter(s -> s != null && !s.isEmpty())
+ .collect(Collectors.toSet());
+ authorFilter.getItems().setAll("全部");
+ authorFilter.getItems().addAll(authors);
+
+ Set platforms = result.stream().map(Exploit::getPlatform)
+ .filter(s -> s != null && !s.isEmpty())
+ .collect(Collectors.toSet());
+ platformFilter.getItems().setAll("全部");
+ platformFilter.getItems().addAll(platforms);
+
+ countLabel.setText(String.format("共 %d 条记录%s",
+ result.size(),
+ result.size() >= 1000 && !"加载全部".equals(loadModeBox.getValue()) ? "(仅显示最新1000条)" : ""));
+ loadStatusLabel.textProperty().unbind();
+ loadStatusLabel.setText("✅ 加载完成");
+ loadProgress.setVisible(false);
+ }
+
+ @Override
+ protected void failed() {
+ super.failed();
+ loadStatusLabel.textProperty().unbind();
+ Throwable error = getException();
+ loadStatusLabel.setText("加载失败: " + error.getMessage());
+ loadProgress.setVisible(false);
+ // 显示异常对话框
+ ExceptionHandler.showError("数据加载失败", error);
+ }
+ };
+ loadStatusLabel.textProperty().bind(loadTask.messageProperty());
+ new Thread(loadTask).start();
+ };
+
+ Runnable doSearch = () -> {
+ String keyword = searchField.getText().toLowerCase().trim();
+ String author = authorFilter.getValue();
+ String platform = platformFilter.getValue();
+
+ filteredData.setPredicate(exploit -> {
+ boolean matchesKeyword = keyword.isEmpty() || exploit.getDescription().toLowerCase().contains(keyword) || exploit.getId().contains(keyword) || exploit.getCve().toLowerCase().contains(keyword);
+
+ boolean matchesAuthor = "全部".equals(author) || author == null || author.equals(exploit.getAuthor());
+
+ boolean matchesPlatform = "全部".equals(platform) || platform == null || platform.equals(exploit.getPlatform());
+
+ return matchesKeyword && matchesAuthor && matchesPlatform;
+ });
+
+ countLabel.setText(String.format("显示 %d / %d 条记录", filteredData.size(), data.size()));
+ };
+
+ searchBtn.setOnAction(e -> doSearch.run());
+ resetBtn.setOnAction(e -> {
+ searchField.clear();
+ authorFilter.setValue("全部");
+ platformFilter.setValue("全部");
+ filteredData.setPredicate(p -> true);
+ countLabel.setText(String.format("共 %d 条记录", data.size()));
+ });
+ refreshBtn.setOnAction(e -> loadData.run());
+
+ Button exportCsvButton = new Button("📄 导出 CSV");
+ exportCsvButton.setOnAction(e -> {
+ try {
+ DatabaseController controller = new DatabaseController();
+ int status = controller.ExportToCSV(new ArrayList<>(filteredData));
+ loadStatusLabel.textProperty().unbind();
+ if (status > 0) {
+ loadStatusLabel.setText("✅ 导出成功");
+ } else {
+ loadStatusLabel.setText("❌ 导出失败");
+ ExceptionHandler.showError("导出失败", "无法导出 CSV 文件,请检查输出目录权限");
+ }
+ } catch (Exception ex) {
+ ExceptionHandler.showError("导出 CSV 失败", ex);
+ }
+ });
+ Button exportJsonButton = new Button("📄 导出 JSON");
+ exportJsonButton.setOnAction(e -> {
+ try {
+ DatabaseController controller = new DatabaseController();
+ int status = controller.ExportToJSON(new ArrayList<>(filteredData));
+ if (status > 0) {
+ loadStatusLabel.setText("✅ 导出成功");
+ } else {
+ loadStatusLabel.setText("❌ 导出失败");
+ ExceptionHandler.showError("导出失败", "无法导出 JSON 文件,请检查输出目录权限");
+ }
+ } catch (Exception ex) {
+ ExceptionHandler.showError("导出 JSON 失败", ex);
+ }
+ });
+
+ Button exportYAMLButton = new Button("📄 导出 YAML");
+ exportYAMLButton.setOnAction(e -> {
+ try {
+ DatabaseController controller = new DatabaseController();
+ int status = controller.ExportToYAML(new ArrayList<>(filteredData));
+ if (status > 0) {
+ loadStatusLabel.setText("✅ 导出成功");
+ } else {
+ loadStatusLabel.setText("❌ 导出失败");
+ ExceptionHandler.showError("导出失败", "无法导出 YAML 文件,请检查输出目录权限");
+ }
+ } catch (Exception ex) {
+ ExceptionHandler.showError("导出 YAML 失败", ex);
+ }
+ });
+
+ loadModeBox.setOnAction(e -> loadData.run());
+ loadData.run();
+ actionBar.getChildren().addAll(countLabel, exportCsvButton, exportJsonButton, exportYAMLButton, loadStatusLabel, loadProgress);
+ dataLayout.setBottom(actionBar);
+
+ StackPane page = new StackPane(dataLayout);
+ return page;
+ }
+
+ private StackPane createSettingsPage() {
+ VBox settingsLayout = new VBox(20);
+ settingsLayout.setPadding(new Insets(30));
+ settingsLayout.setAlignment(Pos.TOP_CENTER);
+
+ // 标题
+ Label title = new Label("系统设置");
+ title.setStyle("-fx-font-size: 24px; -fx-font-weight: bold;");
+
+ // 当前配置状态
+ String currentDb = Configuration.getDatabaseMode();
+ Label currentConfigLabel = new Label("当前使用: " + ("MySQL".equalsIgnoreCase(currentDb) ? "MySQL" : "SQLite"));
+ currentConfigLabel.setStyle("-fx-font-size: 14px; -fx-text-fill: #666;");
+
+ // 数据库设置区域
+ Label dbTitle = new Label("数据库配置");
+ dbTitle.setStyle("-fx-font-size: 18px; -fx-font-weight: bold;");
+
+ // 数据库模式选择
+ HBox modeBox = new HBox(15);
+ modeBox.setAlignment(Pos.CENTER_LEFT);
+ Label modeLabel = new Label("数据库模式:");
+ ComboBox modeCombo = new ComboBox<>();
+ modeCombo.getItems().addAll("MySQL", "SQLite");
+ modeCombo.setValue(Configuration.getDatabaseMode());
+ modeCombo.setPrefWidth(150);
+ modeBox.getChildren().addAll(modeLabel, modeCombo);
+
+ // MySQL 配置区域
+ VBox mysqlBox = new VBox(10);
+ mysqlBox.setPadding(new Insets(10));
+ mysqlBox.setStyle("-fx-border-color: #ddd; -fx-border-radius: 5px;");
+
+ Label mysqlTitle = new Label("MySQL 连接设置");
+ mysqlTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;");
+
+ // URL
+ HBox urlBox = new HBox(10);
+ urlBox.setAlignment(Pos.CENTER_LEFT);
+ Label urlLabel = new Label("连接URL:");
+ TextField urlField = new TextField(Configuration.getDbUrl());
+ urlField.setPrefWidth(400);
+ urlBox.getChildren().addAll(urlLabel, urlField);
+
+ // 用户名
+ HBox userBox = new HBox(10);
+ userBox.setAlignment(Pos.CENTER_LEFT);
+ Label userLabel = new Label("用户名: ");
+ TextField userField = new TextField(Configuration.getDbUser());
+ userField.setPrefWidth(150);
+ userBox.getChildren().addAll(userLabel, userField);
+
+ // 密码
+ HBox pwdBox = new HBox(10);
+ pwdBox.setAlignment(Pos.CENTER_LEFT);
+ Label pwdLabel = new Label("密码: ");
+ PasswordField pwdField = new PasswordField();
+ pwdField.setText(Configuration.getDbPassword());
+ pwdField.setPrefWidth(150);
+ pwdBox.getChildren().addAll(pwdLabel, pwdField);
+
+ mysqlBox.getChildren().addAll(mysqlTitle, urlBox, userBox, pwdBox);
+
+ // SQLite 配置区域
+ VBox sqliteBox = new VBox(10);
+ sqliteBox.setPadding(new Insets(10));
+ sqliteBox.setStyle("-fx-border-color: #ddd; -fx-border-radius: 5px;");
+
+ Label sqliteTitle = new Label("SQLite 设置");
+ sqliteTitle.setStyle("-fx-font-size: 14px; -fx-font-weight: bold;");
+
+ HBox sqliteUrlBox = new HBox(10);
+ sqliteUrlBox.setAlignment(Pos.CENTER_LEFT);
+ Label sqliteUrlLabel = new Label("数据库文件:");
+ TextField sqliteUrlField = new TextField(Configuration.getDbSqliteUrl());
+ sqliteUrlField.setPrefWidth(300);
+ sqliteUrlBox.getChildren().addAll(sqliteUrlLabel, sqliteUrlField);
+
+ sqliteBox.getChildren().addAll(sqliteTitle, sqliteUrlBox);
+
+ // 根据当前模式显示/隐藏配置区域
+ String currentMode = Configuration.getDatabaseMode();
+ mysqlBox.setVisible("MySQL".equalsIgnoreCase(currentMode));
+ mysqlBox.setManaged("MySQL".equalsIgnoreCase(currentMode));
+ sqliteBox.setVisible("SQLite".equalsIgnoreCase(currentMode));
+ sqliteBox.setManaged("SQLite".equalsIgnoreCase(currentMode));
+
+ // 模式切换事件
+ modeCombo.setOnAction(e -> {
+ String selected = modeCombo.getValue();
+ boolean isMySQL = "MySQL".equals(selected);
+ mysqlBox.setVisible(isMySQL);
+ mysqlBox.setManaged(isMySQL);
+ sqliteBox.setVisible(!isMySQL);
+ sqliteBox.setManaged(!isMySQL);
+ });
+
+ // 按钮区域
+ HBox buttonBox = new HBox(15);
+ buttonBox.setAlignment(Pos.CENTER);
+ buttonBox.setPadding(new Insets(20, 0, 0, 0));
+
+ Button testBtn = new Button("🧪 测试连接");
+ testBtn.setStyle("-fx-background-color: #5bc0de; -fx-text-fill: white; -fx-font-size: 12px;");
+ testBtn.setPrefWidth(120);
+ testBtn.setMinWidth(100);
+
+ Button saveBtn = new Button("💾 保存设置");
+ saveBtn.setStyle("-fx-background-color: #5cb85c; -fx-text-fill: white; -fx-font-size: 12px;");
+ saveBtn.setPrefWidth(120);
+ saveBtn.setMinWidth(100);
+
+ Button resetBtn = new Button("🔄 重置默认");
+ resetBtn.setStyle("-fx-font-size: 12px;");
+ resetBtn.setPrefWidth(120);
+ resetBtn.setMinWidth(100);
+
+ buttonBox.getChildren().addAll(testBtn, saveBtn, resetBtn);
+
+ // 状态标签
+ Label statusLabel = new Label("");
+ statusLabel.setStyle("-fx-font-size: 12px;");
+
+ // 按钮事件
+ testBtn.setOnAction(e -> {
+ statusLabel.setText("正在测试连接...");
+ statusLabel.setStyle("-fx-text-fill: gray;");
+
+ // 在后台线程测试连接,避免卡死UI
+ new Thread(() -> {
+ try {
+ // 临时应用当前输入框的设置进行测试
+ String selectedMode = modeCombo.getValue();
+ Configuration.setDatabaseMode(Database.fromValue(selectedMode));
+
+ if ("MySQL".equals(selectedMode)) {
+ Configuration.setDbUrl(urlField.getText());
+ Configuration.setDbUser(userField.getText());
+ Configuration.setDbPassword(pwdField.getText());
+ } else {
+ Configuration.setDbSqliteUrl(sqliteUrlField.getText());
+ }
+
+ DatabaseController controller = new DatabaseController();
+ controller.getConnection().close();
+
+ Platform.runLater(() -> {
+ statusLabel.setText("✅ 连接成功!");
+ statusLabel.setStyle("-fx-text-fill: green;");
+ });
+ } catch (Exception ex) {
+ Platform.runLater(() -> {
+ String msg = ex.getMessage();
+ if (msg == null || msg.isEmpty()) {
+ msg = "无法连接到数据库,请检查配置";
+ }
+ statusLabel.setText("❌ 连接失败: " + msg);
+ statusLabel.setStyle("-fx-text-fill: red;");
+ });
+ }
+ }).start();
+ });
+
+ saveBtn.setOnAction(e -> {
+ // 保存所有设置
+ String selectedMode = modeCombo.getValue();
+ Configuration.setDatabaseMode(Database.fromValue(selectedMode));
+ Configuration.setDbUrl(urlField.getText());
+ Configuration.setDbUser(userField.getText());
+ Configuration.setDbPassword(pwdField.getText());
+ Configuration.setDbSqliteUrl(sqliteUrlField.getText());
+
+ // 更新当前配置显示
+ currentConfigLabel.setText("当前使用: " + ("MySQL".equalsIgnoreCase(selectedMode) ? "🐬 MySQL" : "📦 SQLite"));
+
+ statusLabel.setText("✅ 设置已保存");
+ statusLabel.setStyle("-fx-text-fill: green;");
+ });
+
+ resetBtn.setOnAction(e -> {
+ // 重置为默认值
+ modeCombo.setValue("MySQL");
+ urlField.setText("jdbc:mysql://localhost:3306/ExploitDB?serverTimezone=UTC&allowPublicKeyRetrieval=true&useSSL=false");
+ userField.setText("root");
+ pwdField.clear();
+ sqliteUrlField.setText("jdbc:sqlite:ling.sqlite");
+
+ mysqlBox.setVisible(true);
+ mysqlBox.setManaged(true);
+ sqliteBox.setVisible(false);
+ sqliteBox.setManaged(false);
+
+ statusLabel.setText("已重置为默认值,点击保存生效");
+ statusLabel.setStyle("-fx-text-fill: gray;");
+ });
+
+ // 组装布局
+ settingsLayout.getChildren().addAll(
+ title,
+ currentConfigLabel,
+ new Label(""), // 间距
+ dbTitle,
+ modeBox,
+ mysqlBox,
+ sqliteBox,
+ buttonBox,
+ statusLabel
+ );
+
+ return new StackPane(settingsLayout);
+ }
+
+ private StackPane createCrawlerPage() {
+ VBox crawlerLayout = new VBox(15);
+ crawlerLayout.setPadding(new Insets(20));
+ crawlerLayout.setAlignment(Pos.TOP_CENTER);
+
+ Label title = new Label("🕷 Exploit-DB 搜索工具");
+ title.setStyle("-fx-font-size: 24px; -fx-font-weight: bold;");
+
+ HBox settingsBox = new HBox(10);
+ settingsBox.setAlignment(Pos.CENTER);
+
+ Label pageLabel = new Label("爬取条数:");
+ Spinner itemSpinner = new Spinner<>(1, 80000, 50);
+ itemSpinner.setEditable(true);
+ itemSpinner.setPrefWidth(100);
+
+ TextField editor = itemSpinner.getEditor();
+ editor.setTextFormatter(new TextFormatter<>(change -> {
+ String newText = change.getControlNewText();
+ if (newText.matches("\\d*")) { // 只允许数字
+ return change;
+ }
+ return null; // 拒绝这次修改
+ }));
+
+ editor.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
+ if (!isFocused) {
+ try {
+ int val = Integer.parseInt(editor.getText());
+ itemSpinner.getValueFactory().setValue(val);
+ } catch (NumberFormatException e) {
+ editor.setText(itemSpinner.getValue().toString());
+ }
+ }
+ });
+
+ settingsBox.getChildren().addAll(pageLabel, itemSpinner);
+
+ VBox progressBox = new VBox(10);
+ progressBox.setAlignment(Pos.CENTER);
+ progressBox.setPadding(new Insets(20, 0, 20, 0));
+
+ ProgressBar progressBar = new ProgressBar(0);
+ progressBar.setPrefWidth(400);
+ progressBar.setVisible(false);
+
+ Label statusLabel = new Label("就绪 - 等待开始");
+ statusLabel.setStyle("-fx-font-size: 14px;");
+
+ statusLabel.setText("");
+ statusLabel.setStyle("-fx-font-size: 12px; -fx-text-fill: gray;");
+
+ progressBox.getChildren().addAll(progressBar, statusLabel);
+
+ TableView previewTable = new TableView<>();
+ previewTable.setPrefHeight(300);
+
+ TableColumn idCol = new TableColumn<>("编号");
+ idCol.setCellValueFactory(new PropertyValueFactory<>("id"));
+ idCol.setPrefWidth(60);
+
+ TableColumn descCol = new TableColumn<>("描述");
+ descCol.setCellValueFactory(new PropertyValueFactory<>("description"));
+ descCol.setPrefWidth(300);
+
+ TableColumn dateCol = new TableColumn<>("日期");
+ dateCol.setCellValueFactory(new PropertyValueFactory<>("date"));
+ dateCol.setPrefWidth(100);
+
+ TableColumn authorCol = new TableColumn<>("作者");
+ authorCol.setCellValueFactory(new PropertyValueFactory<>("author"));
+ authorCol.setPrefWidth(100);
+
+ previewTable.getColumns().addAll(idCol, descCol, dateCol, authorCol);
+
+ ObservableList previewData = FXCollections.observableArrayList();
+ previewTable.setItems(previewData);
+
+ HBox buttonBox = new HBox(15);
+ buttonBox.setAlignment(Pos.CENTER);
+
+ Button startBtn = new Button("▶ 开始爬取");
+ startBtn.setStyle("-fx-font-size: 14px; -fx-background-color: #28a745; -fx-text-fill: white;");
+ startBtn.setPrefWidth(120);
+
+ Button stopBtn = new Button("■ 停止爬取");
+ stopBtn.setStyle("-fx-font-size: 14px; -fx-background-color: #dc3545; -fx-text-fill: white;");
+ stopBtn.setPrefWidth(120);
+ stopBtn.setDisable(true);
+
+ buttonBox.getChildren().addAll(startBtn, stopBtn);
+
+ crawlerLayout.getChildren().addAll(title, settingsBox, progressBox, new Label("实时预览: "), previewTable,
+ buttonBox);
+
+ final boolean[] isRunning = { false };
+
+ startBtn.setOnAction(e -> {
+ int pages = itemSpinner.getValue();
+
+ previewData.clear();
+ progressBar.setVisible(true);
+ progressBar.setProgress(0);
+ statusLabel.setText("正在连接 Exploit-DB...");
+ startBtn.setDisable(true);
+ stopBtn.setDisable(false);
+ isRunning[0] = true;
+
+ Task crawlTask = new Task<>() {
+ @Override
+ protected Void call() throws Exception {
+ ExploitDbController controller = new ExploitDbController();
+
+ controller.Crawl(pages, false, "csv", (pageData, currentCount, totalCount, pageNumber) -> {
+ Platform.runLater(() -> {
+ double progress = (double) currentCount / totalCount;
+ progressBar.setProgress(progress);
+
+ statusLabel.setText(String.format("正在爬取第 %d 页... (已获取 %d / %d 条记录)", pageNumber,
+ currentCount, totalCount));
+
+ statusLabel.setText(String.format("当前批次:%d 条新数据 | 总计%d 条", pageData.size(), currentCount));
+
+ previewData.addAll(pageData);
+ if (previewData.size() > 100) {
+ previewData.remove(0, previewData.size() - 100);
+ }
+
+ previewTable.scrollTo(previewData.size() - 1);
+ });
+ });
+
+ return null;
+ }
+
+ @Override
+ protected void succeeded() {
+ super.succeeded();
+ Platform.runLater(() -> {
+ progressBar.setProgress(1.0);
+ statusLabel.setText("✅ 爬取完成!共获取 " + previewData.size() + " 条数据");
+ startBtn.setDisable(false);
+ stopBtn.setDisable(true);
+ isRunning[0] = false;
+ });
+ }
+
+ @Override
+ protected void failed() {
+ Throwable error = getException();
+ Platform.runLater(() -> {
+ statusLabel.setText("❌ 爬取失败: " + error.getMessage());
+ startBtn.setDisable(false);
+ stopBtn.setDisable(true);
+ isRunning[0] = false;
+ // 显示异常对话框
+ ExceptionHandler.showError("爬取数据失败", error);
+ });
+ }
+ };
+
+ new Thread(crawlTask).start();
+ });
+ // TODO: 完全停止需要更复杂的实现(中断线程)
+ stopBtn.setOnAction(e -> {
+ isRunning[0] = false;
+ statusLabel.setText("已停止(当前批次完成后)");
+ startBtn.setDisable(false);
+ stopBtn.setDisable(true);
+ });
+
+ StackPane page = new StackPane(crawlerLayout);
+ return page;
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+}
diff --git a/src/main/resources/icon.png b/src/main/resources/icon.png
new file mode 100644
index 0000000..1a4f59b
Binary files /dev/null and b/src/main/resources/icon.png differ