代码提交
This commit is contained in:
@@ -20,14 +20,10 @@ import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.CancellationException;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
@@ -558,11 +554,18 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
|
||||
|
||||
// ==================== Git 命令执行 ====================
|
||||
|
||||
/**
|
||||
* 执行一条 Git 命令,总超时 CMD_TIMEOUT_SECONDS + 10 秒。
|
||||
* <p>
|
||||
* 关键:进程输出在独立线程中读取,主线程通过 process.waitFor(timeout) 等待。
|
||||
* 超时后强制杀进程 + 中断读线程,杜绝僵尸进程 / 永久阻塞。
|
||||
*/
|
||||
private CmdResult runGit(String workingDir, String command) {
|
||||
// 用 Future 包一层,防止 readLine() 因进程卡死而永久阻塞
|
||||
ExecutorService singleExecutor = Executors.newSingleThreadExecutor();
|
||||
Future<CmdResult> future = singleExecutor.submit(() -> {
|
||||
CmdResult result = new CmdResult();
|
||||
Process process = null;
|
||||
Thread readerThread = null;
|
||||
StringBuilder output = new StringBuilder();
|
||||
|
||||
try {
|
||||
ProcessBuilder pb = isWindows()
|
||||
? new ProcessBuilder("cmd.exe", "/c", command)
|
||||
@@ -572,25 +575,36 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
|
||||
pb.directory(new File(workingDir));
|
||||
}
|
||||
pb.redirectErrorStream(true);
|
||||
process = pb.start();
|
||||
final Process proc = process; // effectively final 副本供 lambda 引用
|
||||
|
||||
Process process = pb.start();
|
||||
StringBuilder output = new StringBuilder();
|
||||
// 独立线程读取进程输出,避免 readLine() 阻塞主逻辑
|
||||
readerThread = new Thread(() -> {
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) {
|
||||
new InputStreamReader(proc.getInputStream(), java.nio.charset.StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.append(line).append("\n");
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
// 进程被杀时流关闭会抛异常,正常情况
|
||||
}
|
||||
}, "git-reader");
|
||||
readerThread.setDaemon(true);
|
||||
readerThread.start();
|
||||
|
||||
// 等待进程结束(带超时)
|
||||
boolean finished = process.waitFor(CMD_TIMEOUT_SECONDS, TimeUnit.SECONDS);
|
||||
if (!finished) {
|
||||
process.destroyForcibly();
|
||||
log.warn("Git 命令超时({}s): {} -> {}", CMD_TIMEOUT_SECONDS, workingDir, command);
|
||||
result.ok = false;
|
||||
result.error = "命令超时(" + CMD_TIMEOUT_SECONDS + "s)";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 进程已结束,等待读线程收尾(最多10秒)
|
||||
readerThread.join(10_000);
|
||||
|
||||
int exitCode = process.exitValue();
|
||||
result.ok = (exitCode == 0);
|
||||
result.output = output.toString().trim();
|
||||
@@ -600,31 +614,28 @@ public class GitSyncPlugin implements Plugin, InitializingBean {
|
||||
? "exit code " + exitCode
|
||||
: "exit code " + exitCode + ": " + detail;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
result.ok = false;
|
||||
result.error = "命令被中断";
|
||||
} catch (Exception e) {
|
||||
result.ok = false;
|
||||
result.error = e.getMessage();
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
try {
|
||||
// 整体超时比内部 waitFor 多给 10 秒缓冲
|
||||
return future.get(CMD_TIMEOUT_SECONDS + 10, TimeUnit.SECONDS);
|
||||
} catch (TimeoutException e) {
|
||||
future.cancel(true);
|
||||
CmdResult result = new CmdResult();
|
||||
result.ok = false;
|
||||
result.error = "命令超时(" + (CMD_TIMEOUT_SECONDS + 10) + "s,进程被强制终止)";
|
||||
return result;
|
||||
} catch (InterruptedException | ExecutionException | CancellationException e) {
|
||||
CmdResult result = new CmdResult();
|
||||
result.ok = false;
|
||||
result.error = "执行异常: " + e.getMessage();
|
||||
return result;
|
||||
} finally {
|
||||
singleExecutor.shutdownNow();
|
||||
// 确保进程和读线程都被清理
|
||||
if (process != null) {
|
||||
if (process.isAlive()) {
|
||||
process.destroyForcibly();
|
||||
try { process.waitFor(5, TimeUnit.SECONDS); } catch (InterruptedException ignored) { }
|
||||
}
|
||||
}
|
||||
if (readerThread != null && readerThread.isAlive()) {
|
||||
readerThread.interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private String getRemoteUrl(String repoPath, Map<String, String> baseUrlMap) {
|
||||
// 1. 尝试 git remote get-url
|
||||
|
||||
@@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
@@ -15,6 +16,8 @@ import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -22,6 +25,9 @@ import java.util.regex.Pattern;
|
||||
@Slf4j
|
||||
public class ScannerService {
|
||||
|
||||
/** 单个项目扫描超时时间(分钟) */
|
||||
private static final long SCAN_TIMEOUT_MINUTES = 5;
|
||||
|
||||
private final ExecutorService executorService = Executors.newFixedThreadPool(10);
|
||||
@Autowired
|
||||
private FileOperationHelper fileOperationHelper;
|
||||
@@ -30,11 +36,25 @@ public class ScannerService {
|
||||
@Autowired
|
||||
private WorkUtilsProperties properties;
|
||||
|
||||
@PreDestroy
|
||||
public void shutdown() {
|
||||
log.info("正在关闭扫描线程池...");
|
||||
executorService.shutdown();
|
||||
try {
|
||||
if (!executorService.awaitTermination(10, TimeUnit.SECONDS)) {
|
||||
executorService.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
executorService.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
log.info("扫描线程池已关闭");
|
||||
}
|
||||
|
||||
public ScanResult scanAllProjects(ScanType scanType, String pattern) throws Exception {
|
||||
List<String> projectPaths = fileOperationHelper.getProjectPaths();
|
||||
if (projectPaths.isEmpty()) {
|
||||
log.warn("No project paths found");
|
||||
return new ScanResult();
|
||||
throw new IllegalStateException("未配置项目路径,请先在「系统设置」中添加要扫描的项目路径");
|
||||
}
|
||||
|
||||
ScanResult result = new ScanResult();
|
||||
@@ -53,9 +73,25 @@ public class ScannerService {
|
||||
futures.add(future);
|
||||
}
|
||||
|
||||
int total = futures.size();
|
||||
int idx = 0;
|
||||
for (CompletableFuture<ProjectScanResult> future : futures) {
|
||||
ProjectScanResult projectResult = future.get();
|
||||
idx++;
|
||||
String pathHint = (idx <= projectPaths.size())
|
||||
? " (" + fileOperationHelper.extractProjectName(projectPaths.get(idx - 1)) + ")"
|
||||
: "";
|
||||
try {
|
||||
ProjectScanResult projectResult = future.get(SCAN_TIMEOUT_MINUTES, TimeUnit.MINUTES);
|
||||
result.addProjectResult(projectResult);
|
||||
} catch (TimeoutException e) {
|
||||
log.error("项目扫描超时({}分钟)第{}/{}个项目{},已跳过",
|
||||
SCAN_TIMEOUT_MINUTES, idx, total, pathHint);
|
||||
// 超时后仍尝试取消该任务
|
||||
future.cancel(true);
|
||||
} catch (Exception e) {
|
||||
log.error("获取扫描结果异常 第{}/{}个项目{}: {}",
|
||||
idx, total, pathHint, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
@@ -25,8 +25,10 @@ public class ConfigInitializer implements ApplicationListener<ApplicationReadyEv
|
||||
log.info("发现外部配置文件: {}", configManager.getConfigFile().toAbsolutePath());
|
||||
properties.mergeExternal(configManager.load());
|
||||
} else {
|
||||
log.info("未找到外部配置文件 workutils.config,使用 application.yml 默认值");
|
||||
log.info("可通过「系统设置(S)」创建并编辑 workutils.config");
|
||||
log.info("未找到外部配置文件 workutils.config,正在使用 application.yml 默认值生成...");
|
||||
configManager.saveSectioned(configManager.generateDefaults(properties));
|
||||
properties.mergeExternal(configManager.load());
|
||||
log.info("已生成默认外部配置文件: {}", configManager.getConfigFile().toAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,14 @@ public class ExternalConfigManager {
|
||||
for (String k : mybatisKeys) {
|
||||
props.setProperty(k, "");
|
||||
}
|
||||
|
||||
// 字体大小默认值
|
||||
props.setProperty(KEY_FONT_SECTION_TITLE, String.valueOf(appProps.getFontSectionTitle()));
|
||||
props.setProperty(KEY_FONT_FIELD_LABEL, String.valueOf(appProps.getFontFieldLabel()));
|
||||
props.setProperty(KEY_FONT_BUTTON, String.valueOf(appProps.getFontButton()));
|
||||
props.setProperty(KEY_FONT_BODY, String.valueOf(appProps.getFontBody()));
|
||||
props.setProperty(KEY_FONT_MONO, String.valueOf(appProps.getFontMono()));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,9 @@ public class SettingsWindow {
|
||||
private TextArea taDamagedCache;
|
||||
private Label lblProjectPathsFile, lblDamagedCacheFile;
|
||||
|
||||
// --- 项目路径替换 ---
|
||||
private TextField tfReplaceParentDir, tfReplaceAccount;
|
||||
|
||||
// --- 字体大小 ---
|
||||
private int fontSectionTitle;
|
||||
private int fontFieldLabel;
|
||||
@@ -270,7 +273,7 @@ public class SettingsWindow {
|
||||
btnOpenPP.setOnAction(e -> openInNotepad(properties.getProjectPathsFilePath()));
|
||||
|
||||
HBox ppBtns = new HBox(10, btnLoadPP, btnSavePP, btnOpenPP);
|
||||
ppBox.getChildren().addAll(lblProjectPathsFile, taProjectPaths, ppBtns);
|
||||
ppBox.getChildren().addAll(lblProjectPathsFile, taProjectPaths, ppBtns, createReplaceTool());
|
||||
|
||||
Tab ppTab = new Tab("项目路径");
|
||||
ppTab.setContent(ppBox);
|
||||
@@ -724,6 +727,237 @@ public class SettingsWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 项目路径替换工具 ====================
|
||||
|
||||
private VBox createReplaceTool() {
|
||||
VBox box = new VBox(8);
|
||||
box.setPadding(new Insets(8, 0, 0, 0));
|
||||
box.setStyle("-fx-border-color: #e0e0e0; -fx-border-width: 1 0 0 0; -fx-padding: 10 0 0 0;");
|
||||
|
||||
Label title = new Label("批量字符替换");
|
||||
title.setStyle("-fx-font-weight: bold; -fx-font-size: " + fontSectionTitle + "px;");
|
||||
titleLabels.add(title);
|
||||
box.getChildren().add(title);
|
||||
|
||||
// 父目录替换行
|
||||
HBox parentRow = new HBox(8);
|
||||
parentRow.setAlignment(Pos.CENTER_LEFT);
|
||||
Label lblParent = new Label("父目录: ");
|
||||
lblParent.setFont(font(fontFieldLabel));
|
||||
fieldLabels.add(lblParent);
|
||||
tfReplaceParentDir = new TextField();
|
||||
tfReplaceParentDir.setPromptText("自动检测当前父目录,在此输入新父目录");
|
||||
tfReplaceParentDir.setPrefWidth(260);
|
||||
textInputs.add(tfReplaceParentDir);
|
||||
Button btnBrowseParent = addSmallButton("浏览...");
|
||||
btnBrowseParent.setOnAction(e -> browseForParentDir());
|
||||
Button btnDetectParent = addSmallButton("检测");
|
||||
btnDetectParent.setOnAction(e -> detectParentDir());
|
||||
Button btnApplyParent = addSmallButton("替换");
|
||||
btnApplyParent.setOnAction(e -> applyReplaceParentDir());
|
||||
parentRow.getChildren().addAll(lblParent, tfReplaceParentDir, btnBrowseParent, btnDetectParent, btnApplyParent);
|
||||
|
||||
// 账号替换行
|
||||
HBox accountRow = new HBox(8);
|
||||
accountRow.setAlignment(Pos.CENTER_LEFT);
|
||||
Label lblAccount = new Label("账 号: ");
|
||||
lblAccount.setFont(font(fontFieldLabel));
|
||||
fieldLabels.add(lblAccount);
|
||||
tfReplaceAccount = new TextField();
|
||||
tfReplaceAccount.setPromptText("自动检测当前账号,在此输入新账号");
|
||||
tfReplaceAccount.setPrefWidth(300);
|
||||
textInputs.add(tfReplaceAccount);
|
||||
Button btnDetectAccount = addSmallButton("检测");
|
||||
btnDetectAccount.setOnAction(e -> detectAccount());
|
||||
Button btnApplyAccount = addSmallButton("替换");
|
||||
btnApplyAccount.setOnAction(e -> applyReplaceAccount());
|
||||
accountRow.getChildren().addAll(lblAccount, tfReplaceAccount, btnDetectAccount, btnApplyAccount);
|
||||
|
||||
box.getChildren().addAll(parentRow, accountRow);
|
||||
return box;
|
||||
}
|
||||
|
||||
/** 从第一条有效项目路径中检测父目录 */
|
||||
private void detectParentDir() {
|
||||
String firstPath = findFirstProjectPath();
|
||||
if (firstPath == null) {
|
||||
showAlert(AlertType.WARNING, "项目路径为空,无法检测父目录。");
|
||||
return;
|
||||
}
|
||||
// 取 / 分割后的第一个非空部分作为父目录,如 D:/bestpay-srdcloud/ft-xxx 中的 D:/bestpay-srdcloud
|
||||
int slashIdx = firstPath.indexOf('/');
|
||||
if (slashIdx < 0) {
|
||||
tfReplaceParentDir.setText(firstPath);
|
||||
return;
|
||||
}
|
||||
// 找到第一个 / 后,继续找 /ft-xxx 的 /,即项目目录前一个 /
|
||||
int nextSlash = firstPath.indexOf('/', slashIdx + 1);
|
||||
if (nextSlash > 0) {
|
||||
tfReplaceParentDir.setText(firstPath.substring(0, nextSlash));
|
||||
} else {
|
||||
tfReplaceParentDir.setText(firstPath);
|
||||
}
|
||||
}
|
||||
|
||||
/** 浏览选择父目录 */
|
||||
private void browseForParentDir() {
|
||||
DirectoryChooser chooser = new DirectoryChooser();
|
||||
chooser.setTitle("选择父目录");
|
||||
String current = tfReplaceParentDir.getText().trim();
|
||||
if (!current.isEmpty()) {
|
||||
File f = new File(current);
|
||||
if (f.exists()) {
|
||||
chooser.setInitialDirectory(f);
|
||||
} else {
|
||||
// 尝试取已存在的上级目录
|
||||
File parent = f.getParentFile();
|
||||
while (parent != null && !parent.exists()) {
|
||||
parent = parent.getParentFile();
|
||||
}
|
||||
if (parent != null) {
|
||||
chooser.setInitialDirectory(parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
File selected = chooser.showDialog(stage);
|
||||
if (selected != null) {
|
||||
tfReplaceParentDir.setText(selected.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
|
||||
/** 从第一条带 URL 的项目路径中检测账号 */
|
||||
private void detectAccount() {
|
||||
String firstLine = findFirstLineWithUrl();
|
||||
if (firstLine == null) {
|
||||
showAlert(AlertType.WARNING, "未找到带远程 URL 的项目路径,请确保路径格式为 path=URL。");
|
||||
return;
|
||||
}
|
||||
// 格式: path=https://account@code.srdcloud.cn/...
|
||||
int eqIdx = firstLine.indexOf('=');
|
||||
if (eqIdx < 0) return;
|
||||
String url = firstLine.substring(eqIdx + 1).trim();
|
||||
// 提取 @ 前面的账号:https://xxx → 取 // 和 @ 之间的部分
|
||||
int atIdx = url.indexOf('@');
|
||||
int protoIdx = url.indexOf("://");
|
||||
if (atIdx > 0 && protoIdx > 0) {
|
||||
String account = url.substring(protoIdx + 3, atIdx);
|
||||
tfReplaceAccount.setText(account);
|
||||
}
|
||||
}
|
||||
|
||||
/** 批量替换父目录 */
|
||||
private void applyReplaceParentDir() {
|
||||
String newParent = tfReplaceParentDir.getText().trim();
|
||||
if (newParent.isEmpty()) {
|
||||
showAlert(AlertType.WARNING, "请输入新的父目录。");
|
||||
return;
|
||||
}
|
||||
// 检测当前父目录
|
||||
String firstPath = findFirstProjectPath();
|
||||
if (firstPath == null) {
|
||||
showAlert(AlertType.WARNING, "项目路径为空。");
|
||||
return;
|
||||
}
|
||||
int slashIdx = firstPath.indexOf('/');
|
||||
if (slashIdx < 0) {
|
||||
showAlert(AlertType.WARNING, "无法解析父目录。");
|
||||
return;
|
||||
}
|
||||
int nextSlash = firstPath.indexOf('/', slashIdx + 1);
|
||||
String oldParent = (nextSlash > 0) ? firstPath.substring(0, nextSlash) : firstPath;
|
||||
|
||||
// 规范化:去掉末尾 /
|
||||
if (newParent.endsWith("/") || newParent.endsWith("\\")) {
|
||||
newParent = newParent.substring(0, newParent.length() - 1);
|
||||
}
|
||||
if (oldParent.endsWith("/") || oldParent.endsWith("\\")) {
|
||||
oldParent = oldParent.substring(0, oldParent.length() - 1);
|
||||
}
|
||||
|
||||
int count = replaceInTextArea(oldParent, newParent);
|
||||
showAlert(AlertType.INFORMATION,
|
||||
"已将 " + count + " 条路径中的父目录从\n" + oldParent + "\n替换为\n" + newParent);
|
||||
}
|
||||
|
||||
/** 批量替换账号 */
|
||||
private void applyReplaceAccount() {
|
||||
String newAccount = tfReplaceAccount.getText().trim();
|
||||
if (newAccount.isEmpty()) {
|
||||
showAlert(AlertType.WARNING, "请输入新的账号。");
|
||||
return;
|
||||
}
|
||||
// 检测当前账号
|
||||
String firstLine = findFirstLineWithUrl();
|
||||
if (firstLine == null) {
|
||||
showAlert(AlertType.WARNING, "未找到带远程 URL 的项目路径。");
|
||||
return;
|
||||
}
|
||||
int eqIdx = firstLine.indexOf('=');
|
||||
if (eqIdx < 0) return;
|
||||
String url = firstLine.substring(eqIdx + 1).trim();
|
||||
int atIdx = url.indexOf('@');
|
||||
int protoIdx = url.indexOf("://");
|
||||
if (atIdx <= 0 || protoIdx <= 0) {
|
||||
showAlert(AlertType.WARNING, "无法解析当前账号。");
|
||||
return;
|
||||
}
|
||||
String oldAccount = url.substring(protoIdx + 3, atIdx);
|
||||
|
||||
int count = replaceInTextArea(oldAccount, newAccount);
|
||||
showAlert(AlertType.INFORMATION,
|
||||
"已将 " + count + " 条URL中的账号从\n" + oldAccount + "\n替换为\n" + newAccount);
|
||||
}
|
||||
|
||||
/** 在 TextArea 中替换文本,返回替换行数 */
|
||||
private int replaceInTextArea(String oldStr, String newStr) {
|
||||
String text = taProjectPaths.getText();
|
||||
int count = 0;
|
||||
String[] lines = text.split("\n");
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (String line : lines) {
|
||||
if (!line.trim().isEmpty() && !line.trim().startsWith("#") && line.contains(oldStr)) {
|
||||
sb.append(line.replace(oldStr, newStr)).append("\n");
|
||||
count++;
|
||||
} else {
|
||||
sb.append(line).append("\n");
|
||||
}
|
||||
}
|
||||
taProjectPaths.setText(sb.toString());
|
||||
return count;
|
||||
}
|
||||
|
||||
/** 找到第一条有效项目路径(只取路径部分,不含 URL) */
|
||||
private String findFirstProjectPath() {
|
||||
String text = taProjectPaths.getText();
|
||||
if (text == null || text.trim().isEmpty()) return null;
|
||||
for (String line : text.split("\n")) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
int eqIdx = line.indexOf('=');
|
||||
if (eqIdx >= 0) {
|
||||
return line.substring(0, eqIdx).trim();
|
||||
}
|
||||
return line;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 找到第一条同时包含路径和 URL 的行 */
|
||||
private String findFirstLineWithUrl() {
|
||||
String text = taProjectPaths.getText();
|
||||
if (text == null || text.trim().isEmpty()) return null;
|
||||
for (String line : text.split("\n")) {
|
||||
line = line.trim();
|
||||
if (line.isEmpty() || line.startsWith("#")) continue;
|
||||
int eqIdx = line.indexOf('=');
|
||||
if (eqIdx >= 0) {
|
||||
String url = line.substring(eqIdx + 1).trim();
|
||||
if (!url.isEmpty()) return line;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void openInNotepad(Path file) {
|
||||
try {
|
||||
if (!Files.exists(file)) {
|
||||
|
||||
@@ -7,6 +7,7 @@ import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -69,6 +70,24 @@ public class WorkUtilsProperties {
|
||||
|
||||
@PostConstruct
|
||||
void snapshotDefaults() {
|
||||
// 基础目录:未配置则默认取运行目录(user.dir)
|
||||
if (baseDirectory == null || baseDirectory.trim().isEmpty()) {
|
||||
baseDirectory = System.getProperty("user.dir");
|
||||
log.info("基础目录未配置,默认取运行目录: {}", baseDirectory);
|
||||
}
|
||||
// 导出目录:未配置则默认 {user.dir}/export
|
||||
if (exportDirectory == null || exportDirectory.trim().isEmpty()) {
|
||||
exportDirectory = System.getProperty("user.dir") + "/export";
|
||||
log.info("导出目录未配置,默认: {}", exportDirectory);
|
||||
ensureExportDirExists();
|
||||
} else if (!Paths.get(exportDirectory).isAbsolute()) {
|
||||
// 相对路径 → 解析到 user.dir 下
|
||||
exportDirectory = Paths.get(System.getProperty("user.dir"), exportDirectory).toString();
|
||||
ensureExportDirExists();
|
||||
} else {
|
||||
ensureExportDirExists();
|
||||
}
|
||||
|
||||
defaults.put(ExternalConfigManager.KEY_BASE_DIR, baseDirectory);
|
||||
defaults.put(ExternalConfigManager.KEY_EXPORT_DIR, exportDirectory);
|
||||
defaults.put(ExternalConfigManager.KEY_PROJECT_PATHS, projectPathsFile);
|
||||
@@ -77,6 +96,19 @@ public class WorkUtilsProperties {
|
||||
ensureDataFilesExist();
|
||||
}
|
||||
|
||||
/** 确保导出目录存在 */
|
||||
private void ensureExportDirExists() {
|
||||
try {
|
||||
Path dir = Paths.get(exportDirectory);
|
||||
if (!Files.exists(dir)) {
|
||||
Files.createDirectories(dir);
|
||||
log.info("已创建导出目录: {}", dir.toAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.warn("创建导出目录失败: {} ({})", exportDirectory, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并外部配置 —— 外部值覆盖 application.yml 值
|
||||
*/
|
||||
@@ -155,7 +187,7 @@ public class WorkUtilsProperties {
|
||||
* 文件不存在时自动创建空文件。
|
||||
*/
|
||||
public Path getProjectPathsFilePath() {
|
||||
Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "代码仓库项目路径.txt");
|
||||
Path p = Paths.get(projectPathsFile != null ? projectPathsFile : "project_paths.txt");
|
||||
if (p.isAbsolute()) {
|
||||
return p;
|
||||
}
|
||||
@@ -183,7 +215,7 @@ public class WorkUtilsProperties {
|
||||
* 无法读取模板时则创建空文件。
|
||||
*/
|
||||
private void ensureDataFilesExist() {
|
||||
ensureDataFile(getProjectPathsFilePath(), "config/代码仓库项目路径.txt", "项目路径文件");
|
||||
ensureDataFile(getProjectPathsFilePath(), "config/project_paths.txt", "项目路径文件");
|
||||
ensureDataFile(getDamagedReposFilePath(), "config/damaged_repos.cache", "损坏缓存文件");
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,12 @@ spring:
|
||||
name: workutils
|
||||
|
||||
workutils:
|
||||
base-directory: "D:/workspace/workutils"
|
||||
export-directory: "D:/workspace/workutils/export"
|
||||
# 数据文件路径(相对路径 → user.dir 同级目录,绝对路径 → 直接使用)
|
||||
project-paths-file: "代码仓库项目路径.txt"
|
||||
# base dir, empty = defaults to user.dir
|
||||
base-directory: ""
|
||||
# export dir, empty = defaults to {user.dir}/export (auto-created on startup)
|
||||
export-directory: ""
|
||||
# data file paths (relative -> under user.dir, absolute -> as-is)
|
||||
project-paths-file: "project_paths.txt"
|
||||
damaged-repos-file: "damaged_repos.cache"
|
||||
|
||||
logging:
|
||||
|
||||
Reference in New Issue
Block a user