From dbc990271629dffb77f457962f9caa5de52cc99d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E5=AE=B6=E8=AE=AD?= Date: Fri, 19 Jun 2026 18:26:52 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=B7=B2=E7=A8=B3=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- scripts/build_release.bat | 10 ++ .../workutils/biz/git/GitSyncPlugin.java | 148 ++++++++++++------ .../workutils/biz/git/GitSyncWindow.java | 9 +- .../mybatisexport/MyBatisExportWindow.java | 35 ++++- .../service/MyBatisExportService.java | 52 +++++- .../system/config/WorkUtilsProperties.java | 2 +- .../workutils/system/ui/MainWindow.java | 1 - 8 files changed, 195 insertions(+), 64 deletions(-) diff --git a/pom.xml b/pom.xml index 86ec9ef..cd3dd61 100644 --- a/pom.xml +++ b/pom.xml @@ -24,7 +24,7 @@ -Dfile.encoding=UTF-8 - 17.0.14 + 17.0.15 2.0.1 diff --git a/scripts/build_release.bat b/scripts/build_release.bat index 77a9cca..eadcf91 100644 --- a/scripts/build_release.bat +++ b/scripts/build_release.bat @@ -110,6 +110,16 @@ if %errorlevel% neq 0 ( pause exit /b 1 ) + +::: 4.5 补充 JavaFX 原生 DLL(jlink 不会自动从 SDK bin/ 复制) +echo Copy JavaFX native DLLs... +if exist "%JFX_SDK_HOME%\bin\glass.dll" ( + xcopy /y /q "%JFX_SDK_HOME%\bin\*.dll" "%TARGET%\jre\bin\" >nul + echo [OK] JavaFX DLLs copied +) else ( + echo [WARN] No JavaFX DLLs found in %JFX_SDK_HOME%\bin +) + echo [OK] JRE generated : %TARGET%\jre ::: ============================================================ diff --git a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java index 2b204e6..a64bbab 100644 --- a/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java +++ b/src/main/java/com/example/workutils/biz/git/GitSyncPlugin.java @@ -70,6 +70,8 @@ public class GitSyncPlugin implements Plugin, InitializingBean { private List lastDamagedRepos; /** 缓存上一次使用的目标分支 */ private String lastBranch; + /** 同步取消标记(供 GUI 终止按钮使用) */ + private volatile boolean syncCancelled = false; // ==================== 启动时从日志恢复缓存 ==================== @@ -257,11 +259,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { Map baseUrlMap = new LinkedHashMap<>(); for (FileOperationHelper.ProjectPathInfo proj : projects) { if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) { - String url = proj.getRemoteBaseUrl(); - if (!url.endsWith("/")) { - url = url + "/"; - } - baseUrlMap.put(normalizePath(proj.getPath()), url); + baseUrlMap.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl()); } } log.info("从项目路径文件加载了 {} 条远程URL映射", baseUrlMap.size()); @@ -290,7 +288,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { int done = completed.incrementAndGet(); String tag; switch (result) { - case OK: tag = "成功"; break; + case OK: tag = result.cloned ? "拉取" : "成功"; break; case DAMAGED: tag = "损坏"; break; default: tag = "失败"; break; } @@ -485,11 +483,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { List projects = fileOperationHelper.getProjectPathsWithStatus(); for (FileOperationHelper.ProjectPathInfo proj : projects) { if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) { - String url = proj.getRemoteBaseUrl(); - if (!url.endsWith("/")) { - url = url + "/"; - } - map.put(normalizePath(proj.getPath()), url); + map.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl()); } } } catch (IOException e) { @@ -498,15 +492,14 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return map; } - /** 解析 clone URL:优先从项目路径文件的 BASE_URL 拼接,其次用缓存的 remoteUrl */ + /** 解析 clone URL:BASE_URL 本身就是完整的 clone 地址,直接返回 */ private String resolveCloneUrl(DamagedRepo dr, Map baseUrlMap) { - // 1. 从项目路径文件的 BASE_URL 拼接(项目路径文件为准) + // 1. 从项目路径文件的 BASE_URL(已是完整 clone URL,如 https://user@host/path/repo.git) if (baseUrlMap != null) { String normalizedPath = normalizePath(dr.path); String baseUrl = baseUrlMap.get(normalizedPath); if (baseUrl != null) { - String repoName = extractName(dr.path); - return baseUrl + repoName; + return baseUrl; } } // 2. 兜底:缓存的 remoteUrl @@ -520,9 +513,31 @@ public class GitSyncPlugin implements Plugin, InitializingBean { private SyncResult syncOneRepo(String repoPath, String branch, Map baseUrlMap) { File repoDir = new File(repoPath); - if (!repoDir.exists() || !new File(repoDir, ".git").exists()) { + + // 目录不存在 或 空目录 → 自动 clone + if (!repoDir.exists() || (repoDir.isDirectory() && isEmptyDir(repoDir))) { + String reason = !repoDir.exists() ? "目录不存在" : "目录为空"; + log.info("{},自动 clone: {}", reason, repoPath); + String cloneUrl = resolveSimpleCloneUrl(repoPath, baseUrlMap); + if (cloneUrl != null) { + if (doClone(repoPath, branch, cloneUrl)) { + SyncResult r = SyncResult.OK; + r.output = "auto-cloned"; + r.cloned = true; + return r; + } + log.warn("自动 clone 失败: {} -> {}", repoPath, "exit code != 0"); + } SyncResult r = SyncResult.DAMAGED; - r.error = "目录不存在或无 .git"; + r.error = reason + ",自动 clone 失败(远程URL: " + (cloneUrl != null ? cloneUrl : "未知") + ")"; + r.remoteUrl = cloneUrl; + return r; + } + + // 目录存在但无 .git → 仓库损坏 + if (!new File(repoDir, ".git").exists()) { + SyncResult r = SyncResult.DAMAGED; + r.error = "目录存在但无 .git(仓库损坏)"; r.remoteUrl = getRemoteUrl(repoPath, baseUrlMap); return r; } @@ -583,40 +598,49 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return result; } - // ==================== 重拉(clone)逻辑 ==================== + // ==================== clone 逻辑 ==================== - private boolean doReclone(DamagedRepo dr, String cloneUrl) { - String repoPath = dr.path; - String branch = dr.branch; - - // 删除旧目录 + /** 执行 git clone(删除旧目录后克隆),供 auto-clone 和手动重拉共用 */ + private boolean doClone(String repoPath, String branch, String cloneUrl) { File dir = new File(repoPath); + // 确保父目录存在 + if (dir.getParentFile() != null && !dir.getParentFile().exists()) { + dir.getParentFile().mkdirs(); + } + // 删除旧目录(如果存在) if (dir.exists()) { try { deleteRecursive(dir.toPath()); log.info("已删除仓库: {}", repoPath); - System.out.println(" 已删除旧目录"); } catch (IOException e) { - System.err.println(" 删除失败: " + e.getMessage()); + log.error("删除失败: {}", repoPath, e); return false; } } - // clone - String parentDir = dir.getParent(); - System.out.println(" clone URL: " + cloneUrl); + String parentDir = dir.getParent() != null ? dir.getParent() : "."; CmdResult cr = runGit(parentDir, "git clone -b " + branch + " " + cloneUrl + " \"" + repoPath + "\""); - if (cr.ok) { - System.out.println(" clone 成功"); return true; } else { - System.err.println(" clone 失败: " + cr.error); + log.error("clone 失败: {} -> {}", cloneUrl, cr.error); return false; } } + /** 手动重拉(供 DamagedRepo 场景使用) */ + private boolean doReclone(DamagedRepo dr, String cloneUrl) { + System.out.println(" clone URL: " + cloneUrl); + boolean ok = doClone(dr.path, dr.branch, cloneUrl); + if (ok) { + System.out.println(" clone 成功"); + } else { + System.err.println(" clone 失败"); + } + return ok; + } + // ==================== Git 命令执行 ==================== /** @@ -721,16 +745,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } catch (IOException ignored) { } } - // 3. 兜底: 从项目路径文件中的 BASE_URL 拼接完整 clone URL - // 格式: 项目路径文件 key=仓库路径, value=BASE_URL; clone URL = BASE_URL + cf/ + 仓库名 + // 3. 兜底: 项目路径文件中的 BASE_URL 即完整的 clone URL if (baseUrlMap != null) { String normalizedPath = normalizePath(repoPath); String baseUrl = baseUrlMap.get(normalizedPath); if (baseUrl != null) { - String repoName = extractName(repoPath); - String fullUrl = baseUrl + repoName; - log.info("从项目路径文件拼接远程URL: {} -> {}", repoPath, fullUrl); - return fullUrl; + log.info("从项目路径文件获取远程URL: {} -> {}", repoPath, baseUrl); + return baseUrl; } } return null; @@ -766,6 +787,19 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return idx >= 0 ? path.substring(idx + 1) : path; } + /** 判断目录是否为空 */ + private boolean isEmptyDir(File dir) { + if (!dir.isDirectory()) return false; + String[] files = dir.list(); + return files != null && files.length == 0; + } + + /** 从 baseUrlMap 获取 clone URL(不拼仓库名,BASE_URL 即完整 clone URL) */ + private String resolveSimpleCloneUrl(String repoPath, Map baseUrlMap) { + if (baseUrlMap == null) return null; + return baseUrlMap.get(normalizePath(repoPath)); + } + /** 路径规范化:统一用 / 分隔、小写,用于映射表匹配 */ private String normalizePath(String path) { if (path == null || path.isEmpty()) return ""; @@ -801,6 +835,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { * 以回调方式同步所有项目(供可视窗口调用) */ public void syncWithCallback(String branch, SyncCallback cb) { + syncCancelled = false; List projects; try { projects = fileOperationHelper.getProjectPathsWithStatus(); @@ -814,12 +849,12 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return; } + if (syncCancelled) { cb.onLog("[INFO] 同步已取消"); return; } + Map baseUrlMap = new LinkedHashMap<>(); for (FileOperationHelper.ProjectPathInfo proj : projects) { if (proj.getRemoteBaseUrl() != null && !proj.getRemoteBaseUrl().isEmpty()) { - String url = proj.getRemoteBaseUrl(); - if (!url.endsWith("/")) url = url + "/"; - baseUrlMap.put(normalizePath(proj.getPath()), url); + baseUrlMap.put(normalizePath(proj.getPath()), proj.getRemoteBaseUrl()); } } @@ -835,7 +870,13 @@ public class GitSyncPlugin implements Plugin, InitializingBean { cb.onLog("并行线程: " + PARALLEL); cb.onLog("开始同步...\n"); + if (syncCancelled) { cb.onLog("[INFO] 同步已取消"); pool.shutdownNow(); return; } + for (int i = 0; i < total; i++) { + if (syncCancelled) { + // 不再提交新任务,但已提交的会在 latch 中计完 + break; + } final FileOperationHelper.ProjectPathInfo proj = projects.get(i); final String repoPath = proj.getPath(); final String repoName = extractName(repoPath); @@ -850,7 +891,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { int done = completed.incrementAndGet(); String tag; switch (result) { - case OK: tag = "成功"; break; + case OK: tag = result.cloned ? "拉取" : "成功"; break; case DAMAGED: tag = "损坏"; break; default: tag = "失败"; break; } @@ -869,10 +910,20 @@ public class GitSyncPlugin implements Plugin, InitializingBean { pool.shutdown(); try { - boolean allDone = latch.await(SYNC_TOTAL_TIMEOUT_MINUTES, TimeUnit.MINUTES); - if (!allDone) { - pool.shutdownNow(); - cb.onLog("\n[ERROR] 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止"); + // 缩短每次 await 时间,以便及时响应取消 + long checkIntervalMs = 500; + long deadline = System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(SYNC_TOTAL_TIMEOUT_MINUTES); + while (!latch.await(checkIntervalMs, TimeUnit.MILLISECONDS)) { + if (syncCancelled) { + cb.onLog("\n[WARN] 用户取消了同步,正在终止..."); + pool.shutdownNow(); + break; + } + if (System.currentTimeMillis() > deadline) { + pool.shutdownNow(); + cb.onLog("\n[ERROR] 同步超时(超过 " + SYNC_TOTAL_TIMEOUT_MINUTES + " 分钟),强制终止"); + break; + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -881,6 +932,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { return; } + // 汇总结果 int success = 0, failed = 0; List damagedRepos = new ArrayList<>(); for (FileOperationHelper.ProjectPathInfo proj : projects) { @@ -914,6 +966,11 @@ public class GitSyncPlugin implements Plugin, InitializingBean { } } + /** 取消当前正在执行的同步(供 GUI 终止按钮调用) */ + public void cancelSync() { + this.syncCancelled = true; + } + /** * 获取损坏仓库的 clone URL(供窗口使用) */ @@ -969,6 +1026,7 @@ public class GitSyncPlugin implements Plugin, InitializingBean { String output; String error; String remoteUrl; + boolean cloned; // 标记是否是通过 auto-clone 完成的,区别于正常 fetch+checkout+pull } /** 损坏仓库信息 */ diff --git a/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java b/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java index 1a615ff..c1bfc56 100644 --- a/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java +++ b/src/main/java/com/example/workutils/biz/git/GitSyncWindow.java @@ -20,7 +20,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; -import java.util.concurrent.atomic.AtomicBoolean; /** * Git 仓库同步可视化窗口 @@ -46,7 +45,6 @@ public class GitSyncWindow { private ProgressBar progressBar; private Label progressLabel; private TextArea logArea; - private final AtomicBoolean stopRequested = new AtomicBoolean(false); // Tab 3: 损坏仓库处理 private TableView damagedTable; @@ -204,7 +202,11 @@ public class GitSyncWindow { stopBtn = StyledButton.danger("终止", Theme.base(14)); stopBtn.setDisable(true); - stopBtn.setOnAction(e -> stopRequested.set(true)); + stopBtn.setOnAction(e -> { + stopBtn.setDisable(true); + progressLabel.setText("正在终止..."); + plugin.cancelSync(); + }); branchRow.getChildren().addAll(branchLabel, branchField, syncBtn, stopBtn); @@ -240,7 +242,6 @@ public class GitSyncWindow { syncBtn.setDisable(true); stopBtn.setDisable(false); - stopRequested.set(false); logArea.clear(); progressBar.setProgress(0); progressLabel.setText("正在同步..."); diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java index d15effb..d88e23c 100644 --- a/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java +++ b/src/main/java/com/example/workutils/biz/mybatisexport/MyBatisExportWindow.java @@ -44,9 +44,16 @@ public class MyBatisExportWindow { // 操作 private Button exportBtn; + private Button stopBtn; private ProgressBar progressBar; private Label progressLabel; + // 日志 + private TextArea logArea; + + // 取消标记 + private volatile boolean exportCancelled = false; + // 历史记录 private TableView historyTable; private final ObservableList historyRows = FXCollections.observableArrayList(); @@ -131,20 +138,40 @@ public class MyBatisExportWindow { exportBtn = StyledButton.primary("导出", Theme.bold(fsBtn())); exportBtn.setOnAction(e -> startExport()); - filterRow.getChildren().addAll(modeLabel, filterModeCmb, filterValueField, filterHint, exportBtn); + stopBtn = StyledButton.danger("终止", Theme.base(fsBtn())); + stopBtn.setDisable(true); + stopBtn.setOnAction(e -> { + stopBtn.setDisable(true); + progressLabel.setText("正在终止..."); + exportCancelled = true; + }); + + filterRow.getChildren().addAll(modeLabel, filterModeCmb, filterValueField, filterHint, exportBtn, stopBtn); // === 进度 === HBox progressRow = new HBox(Theme.S2); progressRow.setAlignment(Pos.CENTER_LEFT); progressBar = new ProgressBar(0); - progressBar.setPrefWidth(300); - progressBar.setPrefHeight(16); + progressBar.setPrefWidth(400); + progressBar.setPrefHeight(20); progressBar.setVisible(false); progressLabel = new Label(""); progressLabel.setFont(Theme.base(fsBody() - 2)); progressLabel.setTextFill(Theme.TEXT_SECONDARY); progressRow.getChildren().addAll(progressBar, progressLabel); + // === 日志区域 === + Label logTitle = new Label("执行日志"); + logTitle.setFont(Theme.heading(fsBody())); + logTitle.setTextFill(Theme.TEXT_PRIMARY); + logArea = new TextArea(); + logArea.setEditable(false); + logArea.setFont(Theme.mono(12)); + logArea.getStyleClass().add(Theme.CSS_LOG_AREA); + logArea.setPrefHeight(150); + logArea.setStyle("-fx-background-color: " + Theme.toWeb(Theme.BG_BODY) + ";"); + VBox.setVgrow(logArea, Priority.NEVER); + // === 标题 === Label tableTitle = new Label("导出历史记录"); tableTitle.setFont(Theme.heading(fsSection())); @@ -184,7 +211,7 @@ public class MyBatisExportWindow { historyTable.getColumns().addAll(colAction, colIndex, colQuery, colFile, colCount, colTime); VBox.setVgrow(historyTable, Priority.ALWAYS); - root.getChildren().addAll(filterRow, progressRow, tableTitle, historyTable); + root.getChildren().addAll(filterRow, progressRow, logTitle, logArea, tableTitle, historyTable); stage.setScene(new Scene(root)); } diff --git a/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java b/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java index 7bbf943..48cb5eb 100644 --- a/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java +++ b/src/main/java/com/example/workutils/biz/mybatisexport/service/MyBatisExportService.java @@ -106,18 +106,28 @@ public class MyBatisExportService { } } + /** 导出进度回调 */ + public interface ExportCallback { + void onProjectStart(int current, int total, String projectName); + void onProjectDone(int current, int total, String projectName, int recordCount); + /** 是否已请求取消 */ + boolean isCancelled(); + } + /** * 按过滤条件导出并返回结果(含文件路径,供窗口展示 + 保存 CSV) */ public ExportResult exportWithResult(String fileNamePrefix, String filterMode, String filterValue) throws Exception { - SqlFilter filter = null; - if ("table".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) { - filter = new TableFilter(filterValue); - } else if ("keyword".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) { - filter = new KeywordFilter(filterValue); - } else if ("tagid".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) { - filter = new TagIdFilter(filterValue); - } + return exportWithCallback(fileNamePrefix, filterMode, filterValue, null); + } + + /** + * 带进度回调的导出(供可视化窗口使用) + * @param cb 进度回调,传 null 则降级为无回调模式 + */ + public ExportResult exportWithCallback(String fileNamePrefix, String filterMode, String filterValue, + ExportCallback cb) throws Exception { + SqlFilter filter = buildFilter(filterMode, filterValue); List projectPaths = fileOperationHelper.getProjectPaths(); if (projectPaths.isEmpty()) { @@ -125,16 +135,30 @@ public class MyBatisExportService { return new ExportResult(new ArrayList<>(), ""); } + int total = projectPaths.size(); List resultList = new ArrayList<>(); + int current = 0; for (String projectPath : projectPaths) { + current++; + String projectName = fileOperationHelper.extractProjectName(projectPath); + if (cb != null) { + if (cb.isCancelled()) { break; } + cb.onProjectStart(current, total, projectName); + } try { List projectRecords = processProject(projectPath, filter, null); for (String[] r : projectRecords) { resultList.add(new SqlExportRow(r[0], r[1], r[2], r[3], r[4])); } + if (cb != null) { + cb.onProjectDone(current, total, projectName, projectRecords.size()); + } } catch (Exception e) { log.error("处理项目时出错: " + projectPath, e); + if (cb != null) { + cb.onProjectDone(current, total, projectName + " (出错)", 0); + } } } @@ -150,6 +174,18 @@ public class MyBatisExportService { return new ExportResult(resultList, fileName); } + /** 根据过滤模式和值构建过滤器 */ + private SqlFilter buildFilter(String filterMode, String filterValue) { + if ("table".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) { + return new TableFilter(filterValue); + } else if ("keyword".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) { + return new KeywordFilter(filterValue); + } else if ("tagid".equals(filterMode) && filterValue != null && !filterValue.isEmpty()) { + return new TagIdFilter(filterValue); + } + return null; + } + private void exportWithFilter(String fileNamePrefix, SqlFilter filter, ProcessStrategy processStrategy) throws Exception { List projectPaths = fileOperationHelper.getProjectPaths(); diff --git a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java index 7641f11..7180850 100644 --- a/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java +++ b/src/main/java/com/example/workutils/system/config/WorkUtilsProperties.java @@ -231,7 +231,7 @@ public class WorkUtilsProperties { * 文件不存在时自动创建空文件。 */ public Path getProjectPathsFilePath() { - Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "project_paths.txt"); + Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "data/project_paths.txt"); if (p.isAbsolute()) { return p; } diff --git a/src/main/java/com/example/workutils/system/ui/MainWindow.java b/src/main/java/com/example/workutils/system/ui/MainWindow.java index 301e617..d1e26ae 100644 --- a/src/main/java/com/example/workutils/system/ui/MainWindow.java +++ b/src/main/java/com/example/workutils/system/ui/MainWindow.java @@ -171,7 +171,6 @@ public class MainWindow { stage.setScene(scene); showWelcomePage(); - if (!navItems.isEmpty()) selectNavItem(navItems.get(0)); } // ==================== 统一顶部栏 ====================