feat: 支持多系统独立认证与日志下载

- 删除 BaseConfig,认证信息并入 LogFileConfig.SystemItem
- 每个系统独立配置用户名/密码/RSA公钥/网关地址/登录路径
- 支持 subPath 自定义每个系统的存储子目录
- 文件路径隔离:{path}/{subPath}/{serviceName}
- 单个系统登录/下载失败不影响其他系统

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-06-15 16:09:43 +08:00
co-authored by Claude
parent 8178ea3dff
commit 433ea5a995
5 changed files with 400 additions and 250 deletions
+77
View File
@@ -0,0 +1,77 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## 项目概述
基于 **Solon 3.9.0** 框架的日志文件定时下载工具。通过远程网关 API 认证后,拉取多个微服务实例的日志文件(`.log``.gz`)到本地磁盘,并管理本地日志文件的清理与归档。
- Java 17Maven 构建
- 启动类:`com.renkang.rkutils.App`
- 配置文件:`src/main/resources/app.yml`(外挂 `conf/app-xj.yml`
## 构建与运行
```bash
# 编译打包
mvn clean package -DskipTests
# 直接运行(Solon Maven Plugin
mvn solon:run
```
当前无测试用例。
## 架构概览
```
App.java (入口, @SolonMain + @EnableScheduling)
├── DownFileJob — 定时任务, cron: 0 30 * * * ? (每小时第30分钟)
└── LogFileService — 接口, 定义 downloadLogFile()
└── LogFileServiceImpl — 核心下载逻辑
├── BaseConfig — ${base}: username/password/publicKey/gatewayUrl
└── LogFileConfig — ${file}: path/services (name + insCount)
```
### 核心流程 (LogFileServiceImpl.downloadLogFile)
1. **登录认证**RSA 公钥加密密码 → POST `/sys/mLogin` → 获取 JWT token
2. **获取文件列表**token 鉴权 → GET `/{serviceName}/service/log/files` → JSON 中取 `result.records`
3. **本地清理**:删除今日之前的 `.log` 文件;将不属于当前实例前缀的历史文件移入 `tmp/` 目录
4. **下载文件**
- `.gz` 压缩文件(已归档)→ 下载到 `{path}/{serviceName}/archive/`,已存在则跳过
- `.log` 文件 → 下载到 `{path}/{serviceName}/`,与本地同名文件比大小:远程更大则覆盖,更小则新建序号文件(`.log.001` 类推)
### 配置结构
```yaml
solon.config.load: # 外挂配置文件
solon.logging.appender: # 控制台 + 文件日志
solon.scheduling.job: # downFileJob 定时任务配置
base: # 网关认证信息(BaseConfig
username / password / publicKey / gatewayUrl
file: # 日志下载配置(LogFileConfig
path: D:/xjlogs
services:
- name: jeecg-system
insCount: 2
```
### 关键依赖
| 依赖 | 用途 |
|------|------|
| solon-lib | Solon 核心 |
| solon-scheduling-simple | `@Scheduled` 定时任务支持 |
| solon-logging-logback-jakarta | 日志框架(Logback |
| solon-net-httputils | HTTP 客户端(`HttpUtils.http()` |
| fastjson2 | JSON 解析 |
| lombok | 注解简化(`@Data`, `@Slf4j` |
| solon-maven-plugin | Maven 打包/运行插件 |
### 注意事项
- `BaseConfig` 中的密码通过 RSA 公钥加密后传输,`RSAUtil``util` 包)只提供公钥加密
- `LogFileServiceImpl.countUniquePrefixes()` 统计文件名最后一个 `_` 前的部分作为实例前缀,用于判断是否有实例变更
- 日志文件路径使用硬编码 `/` 分隔符,在 Windows 本地运行时 Java 会正确处理
- `solon.config.load` 指向 `conf/app-xj.yml` 作为外挂配置覆盖,该文件不在仓库内
@@ -1,16 +0,0 @@
package com.renkang.rkutils.config;
import lombok.Data;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
@Configuration
@Inject("${base}")
@Data
public class BaseConfig {
private String username;
private String password;
private String gatewayUrl;
private String publicKey;
}
@@ -1,25 +1,54 @@
package com.renkang.rkutils.config;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.noear.solon.annotation.Configuration;
import org.noear.solon.annotation.Inject;
import java.util.List;
/**
* 日志文件下载配置,支持多系统独立认证与下载。
*/
@Configuration
@Inject("${file}")
@Data
public class LogFileConfig {
/** 本地日志存储根路径 */
private String path;
private List<ServiceItem> services;
/** 多系统配置列表 */
private List<SystemItem> systems;
/**
* 单个系统配置,包含认证信息、登录接口路径及该系统的微服务列表。
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ServiceItem {
public static class SystemItem {
/** 系统标识名称 */
private String name;
/** 本地存储子目录(相对于 file.path),不配置则默认使用 name */
private String subPath;
/** 网关用户名 */
private String username;
/** 网关密码(明文,传输时通过 RSA 公钥加密) */
private String password;
/** RSA 公钥(Base64),用于加密密码 */
private String publicKey;
/** 网关基础地址 */
private String gatewayUrl;
/** 登录接口路径,如 /sys/mLogin */
private String loginPath;
/** 该系统下的微服务列表 */
private List<ServiceItem> services;
}
/**
* 微服务配置项。
*/
@Data
public static class ServiceItem {
/** 微服务名称(对应网关路由) */
private String name;
/** 该微服务的实例数量 */
private int insCount;
}
}
@@ -2,7 +2,6 @@ package com.renkang.rkutils.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.renkang.rkutils.config.BaseConfig;
import com.renkang.rkutils.config.LogFileConfig;
import com.renkang.rkutils.service.LogFileService;
import com.renkang.rkutils.vo.FileInfo;
@@ -15,7 +14,6 @@ import org.noear.solon.net.http.HttpUtils;
import util.RSAUtil;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
@@ -25,227 +23,294 @@ import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* 日志文件下载服务实现,支持多系统独立认证与下载。
*/
@Component
@Slf4j
public class LogFileServiceImpl implements LogFileService {
@Inject
private LogFileConfig logFileConfig;
@Inject
private BaseConfig baseConfig;
@Override
public void downloadLogFile() {
String path = logFileConfig.getPath();
log.info("开始下载日志文件: {}", path);
log.info("开始下载日志文件,存储路径: {}", path);
String username = baseConfig.getUsername();
String password = baseConfig.getPassword();
String gatewayUrl = baseConfig.getGatewayUrl();
String publicKey = baseConfig.getPublicKey();
try {
// 使用RSA公钥加密密码
String encryptedPassword = RSAUtil.encryptByPublicKey(password, publicKey);
if (logFileConfig.getSystems() == null || logFileConfig.getSystems().isEmpty()) {
log.warn("未配置任何系统,跳过下载");
return;
}
log.info("密码已加密");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username", username);
jsonObject.put("password", encryptedPassword);
// 发送登录请求,使用加密后的密码
String response = HttpUtils.http(gatewayUrl + "/sys/mLogin")
.timeout(20)
.bodyOfJson(jsonObject.toJSONString())
.post();
JSONObject body = JSONObject.parseObject(response);
log.info("登录返回: {}", body);
boolean success = body.getBoolean("success");
if (!success) {
log.info("登录结果: {}", "失败");
return;
// 外层循环:遍历每个系统,独立登录和下载
for (LogFileConfig.SystemItem system : logFileConfig.getSystems()) {
String systemName = system.getName();
log.info("========== 开始处理系统: {} ==========", systemName);
try {
downloadSystemLogs(system, path);
} catch (Exception e) {
log.error("处理系统[{}]时发生错误,继续下一个系统", systemName, e);
// 单个系统失败不影响其他系统
}
String token = body.getJSONObject("result").getString("token");
log.info("登录成功,获取到的token为: {}", token);
}
log.info("所有系统日志下载任务完成");
}
for (LogFileConfig.ServiceItem serviceItem : logFileConfig.getServices()) {
String serviceName = serviceItem.getName();
int insCount = serviceItem.getInsCount();
log.info("开始下载服务: {}", serviceName);
String downloadUrl = gatewayUrl + "/" + serviceName + "/service/log/files?service=" + serviceName;
/**
* 下载单个系统下所有微服务的日志文件。
*/
private void downloadSystemLogs(LogFileConfig.SystemItem system, String rootPath) throws Exception {
String systemName = system.getName();
// 存储子目录:优先用配置的 subPath,未配置则回退为系统名
String subPath = system.getSubPath() != null && !system.getSubPath().isBlank()
? system.getSubPath() : systemName;
String username = system.getUsername();
String password = system.getPassword();
String gatewayUrl = system.getGatewayUrl();
String publicKey = system.getPublicKey();
String loginPath = system.getLoginPath();
try (HttpResponse resp = HttpUtils.http(downloadUrl).timeout(30).header("x-access-token", token).exec("GET")) {
int code = resp.code();
if (code != 200) {
log.info("获取[{}]日志文件列表接口错误码: {}", serviceName, code);
continue;
}
// 使用该系统自己的 RSA 公钥加密密码
String encryptedPassword = RSAUtil.encryptByPublicKey(password, publicKey);
log.info("[{}] 密码已加密", systemName);
String responseBody = resp.bodyAsString(); // 在流关闭前立即读取
log.info("返回接口数据: {}", responseBody);
JSONObject files = JSONObject.parseObject(responseBody);
// 发送登录请求,使用该系统自己的 loginPath
JSONObject loginBody = new JSONObject();
loginBody.put("username", username);
loginBody.put("password", encryptedPassword);
// 修复:使用正确的对象检查success状态
boolean serSuccess = files.getBoolean("success");
if (!serSuccess) {
log.info("获取日志文件结果: {}", "失败");
continue;
}
String loginUrl = gatewayUrl + loginPath;
log.info("[{}] 登录地址: {}", systemName, loginUrl);
String response = HttpUtils.http(loginUrl)
.timeout(20)
.bodyOfJson(loginBody.toJSONString())
.post();
JSONObject body = JSONObject.parseObject(response);
log.info("[{}] 登录返回: {}", systemName, body);
JSONArray filesList = files.getJSONObject("result").getJSONArray("records");
log.info("获取到的日志文件为: {}", filesList);
boolean success = body.getBoolean("success");
if (!success) {
log.warn("[{}] 登录失败,跳过该系统", systemName);
return;
}
// 开始下载日志文件
if (filesList != null && !filesList.isEmpty()) {
Set<String> instanceSet = new HashSet<>();
filesList.stream()
.filter(JSONObject.class::isInstance)
.map(JSONObject.class::cast)
.map(file -> file.getString("instance"))
.filter(Objects::nonNull)
.map(instance -> instance.replace(".", "_").replace(":", "_")) // 替换 . 和 : 为 _
.forEach(instanceSet::add);
for (int i = 0; i < filesList.size(); i++) {
JSONObject file = filesList.getJSONObject(i);
String fileName = file.getString("fileName");
String instance = file.getString("instance");
String lastModified = file.getString("lastModified");
String token = body.getJSONObject("result").getString("token");
log.info("[{}] 登录成功,获取到 token: {}", systemName, token);
String filePath = path + "/" + serviceName;
// 遍历该系统下的微服务列表
if (system.getServices() == null || system.getServices().isEmpty()) {
log.warn("[{}] 该系统未配置微服务,跳过", systemName);
return;
}
log.info("开始下载日志文件: {}/{}", filePath, fileName);
for (LogFileConfig.ServiceItem serviceItem : system.getServices()) {
String serviceName = serviceItem.getName();
int insCount = serviceItem.getInsCount();
// 文件存储路径:{rootPath}/{subPath}/{serviceName}
String servicePath = rootPath + "/" + subPath + "/" + serviceName;
log.info("[{}] 开始下载服务: {},存储目录: {}", systemName, serviceName, servicePath);
String downloadFileUrl = gatewayUrl + "/" + serviceName + "/service/log/file/down?filename="
+ fileName + "&instance=" + instance + "&service=" + serviceName;
String instanceStr = instance.replace(":", "_").replace(".", "_");
File downFile = new File(filePath + "/" + instanceStr + "_" + fileName);
if (fileName.endsWith(".gz")) {//如果是压缩文件,说明已经归档压缩了,不会再改变,先查询一下本地是否存在,如果存在就跳过
filePath = filePath + "/archive/";//压缩文件存放在archive目录下
downFile = new File(filePath + "/" + instanceStr + "_" + fileName);
if (downFile.exists()) {
log.info("文件已存在,跳过下载: {}", downFile.getAbsolutePath());
continue;
}
} else {
//判断一下所有log文件更新日期,将小于今天的全部删除
File[] logFiles = new File(filePath).listFiles((dir, name) ->
name.endsWith(".log") || name.matches(".*\\.log\\.\\d{3}")
);
if (logFiles != null) {
// 提前创建 /tmp/ 目录,避免重复创建
File tmpBaseDir = new File(filePath + "/tmp/");
if (!tmpBaseDir.exists()) {
tmpBaseDir.mkdirs();
}
for (File logFile : logFiles) {
try {
long lastModifiedTime = logFile.lastModified();
String lastDate = DateUtil.format(new Date(lastModifiedTime), "yyyy-MM-dd");
String haveFileName = logFile.getName();
// 判断是否需要移动文件(实例变化)
// 判断 haveFileName 是否以 instanceSet 中任意一个实例开头
boolean isMatched = instanceSet.stream().anyMatch(haveFileName::startsWith);
int prefixesCount = countUniquePrefixes(filePath);//查询文件夹下有多少个不同前缀
if (!isMatched && prefixesCount > insCount) {
File mvDir = new File(tmpBaseDir, lastDate);
if (!mvDir.exists()) {
mvDir.mkdirs();
}
Files.move(logFile.toPath(), mvDir.toPath().resolve(haveFileName), StandardCopyOption.REPLACE_EXISTING);
log.info("文件已移动至临时目录: {}", mvDir.toPath().resolve(haveFileName));
continue; // 已移动的文件无需进一步处理
}
if (lastModifiedTime < getTodayStartTime()) {
log.info("删除过期文件: {}", logFile.getAbsolutePath());
logFile.delete();
}
} catch (IOException e) {
log.error("处理文件时发生错误: {}", logFile.getAbsolutePath(), e);
}
}
}
}
// 在新的try-with-resources块中处理文件下载
try (HttpResponse downloadRes = HttpUtils.http(downloadFileUrl)
.header("x-access-token", token)
.header("Content-Type", "application/octet-stream")
.timeout(60).exec("GET")) {
int downloadCode = downloadRes.code();
if (downloadCode != 200) {
log.info("下载[{}]日志文件接口错误码: {}", serviceName, downloadCode);
continue;
}
// 立即读取字节数据,在流关闭前完成
byte[] bytes = downloadRes.bodyAsBytes();
log.info("下载文件大小: {}", bytes.length);
// 创建目录
File downFileDir = new File(filePath);
if (!downFileDir.exists()) {
downFileDir.mkdirs(); // 修复:使用mkdirs()创建多级目录
}
// 确保目录存在后再创建文件
if (!downFile.getParentFile().exists()) {
downFile.getParentFile().mkdirs();
}
//当天的文件.log结尾,如果有存在的文件,文件名加序号
if (fileName.endsWith(".log")) {
// 获取基础文件名(不包含序号部分)
String baseFileName = getBaseFileNameWithoutSequence(downFile.getName());
// 查找目标目录中所有匹配的基础文件名
File parentDir = downFile.getParentFile();
FileInfo fileInfo = this.getFileInfo(parentDir, baseFileName);
//判断文件大小,如果本地文件大于线上文件,需要下载并重命名;如果本地文件小于线上文件,直接下载替换(本地文件以最大序号为准)
long localFileSize = fileInfo.getFileSize();
if (localFileSize == bytes.length) {
log.info("文件已存在,跳过下载: {}", downFile.getAbsolutePath());
continue;
}
if (localFileSize > bytes.length) {
downFile = new File(parentDir, fileInfo.getNextFileName());
} else {
downFile = new File(parentDir, fileInfo.getCurrentMaxFileName());
}
}
// 写入文件
try (FileOutputStream fos = new FileOutputStream(downFile)) {
fos.write(bytes);
log.info("成功下载文件: {}", downFile.getAbsolutePath());
}
}
}
}
} // 外层try-with-resources结束
}
} catch (Exception e) {
log.error("下载日志文件过程中发生错误: ", e);
throw new RuntimeException("下载日志文件失败", e);
downloadServiceLogs(gatewayUrl, token, systemName, serviceName, insCount, servicePath);
}
}
/**
* 获取基础文件名(去除序列号部分)
* 下载单个微服务的日志文件。
*/
private void downloadServiceLogs(String gatewayUrl, String token, String systemName,
String serviceName, int insCount, String filePath) {
String downloadUrl = gatewayUrl + "/" + serviceName + "/service/log/files?service=" + serviceName;
log.info("[{}] 获取文件列表地址: {}", systemName, downloadUrl);
try (HttpResponse resp = HttpUtils.http(downloadUrl)
.timeout(30)
.header("x-access-token", token)
.exec("GET")) {
int code = resp.code();
if (code != 200) {
log.warn("[{}] 获取[{}]日志文件列表接口错误码: {}", systemName, serviceName, code);
return;
}
String responseBody = resp.bodyAsString();
log.info("[{}] 返回接口数据: {}", systemName, responseBody);
JSONObject files = JSONObject.parseObject(responseBody);
boolean serSuccess = files.getBoolean("success");
if (!serSuccess) {
log.warn("[{}] 获取[{}]日志文件结果失败", systemName, serviceName);
return;
}
JSONArray filesList = files.getJSONObject("result").getJSONArray("records");
log.info("[{}] 获取到的日志文件为: {}", systemName, filesList);
if (filesList == null || filesList.isEmpty()) {
log.info("[{}] [{}] 无日志文件需要下载", systemName, serviceName);
return;
}
// 收集当前实例集合(用于后续清理判断)
Set<String> instanceSet = new HashSet<>();
filesList.stream()
.filter(JSONObject.class::isInstance)
.map(JSONObject.class::cast)
.map(file -> file.getString("instance"))
.filter(Objects::nonNull)
.map(instance -> instance.replace(".", "_").replace(":", "_"))
.forEach(instanceSet::add);
// 本地清理非 .gz 文件
cleanLocalLogs(filePath, insCount, instanceSet);
// 逐文件下载
for (int i = 0; i < filesList.size(); i++) {
JSONObject file = filesList.getJSONObject(i);
String fileName = file.getString("fileName");
String instance = file.getString("instance");
log.info("[{}] 开始下载日志文件: {}/{}", systemName, filePath, fileName);
String downloadFileUrl = gatewayUrl + "/" + serviceName
+ "/service/log/file/down?filename=" + fileName
+ "&instance=" + instance + "&service=" + serviceName;
String instanceStr = instance.replace(":", "_").replace(".", "_");
File downFile;
if (fileName.endsWith(".gz")) {
// 压缩归档文件,存放在 archive 目录
String archivePath = filePath + "/archive/";
downFile = new File(archivePath + instanceStr + "_" + fileName);
if (downFile.exists()) {
log.info("[{}] 文件已存在,跳过下载: {}", systemName, downFile.getAbsolutePath());
continue;
}
} else {
downFile = new File(filePath + "/" + instanceStr + "_" + fileName);
}
// 下载文件
downloadSingleFile(downloadFileUrl, token, systemName, downFile, fileName);
}
} catch (Exception e) {
log.error("[{}] 处理服务[{}]时发生错误", systemName, serviceName, e);
}
}
/**
* 清理本地过期/无效日志文件。
*/
private void cleanLocalLogs(String filePath, int insCount, Set<String> instanceSet) {
File[] logFiles = new File(filePath).listFiles((dir, name) ->
name.endsWith(".log") || name.matches(".*\\.log\\.\\d{3}")
);
if (logFiles == null) {
return;
}
// 提前创建 tmp 目录
File tmpBaseDir = new File(filePath + "/tmp/");
if (!tmpBaseDir.exists()) {
tmpBaseDir.mkdirs();
}
for (File logFile : logFiles) {
try {
long lastModifiedTime = logFile.lastModified();
String lastDate = DateUtil.format(new Date(lastModifiedTime), "yyyy-MM-dd");
String haveFileName = logFile.getName();
// 判断是否需要将过期实例的文件移入 tmp
boolean isMatched = instanceSet.stream().anyMatch(haveFileName::startsWith);
int prefixesCount = countUniquePrefixes(filePath);
if (!isMatched && prefixesCount > insCount) {
File mvDir = new File(tmpBaseDir, lastDate);
if (!mvDir.exists()) {
mvDir.mkdirs();
}
Files.move(logFile.toPath(), mvDir.toPath().resolve(haveFileName),
StandardCopyOption.REPLACE_EXISTING);
log.info("文件已移动至临时目录: {}", mvDir.toPath().resolve(haveFileName));
continue;
}
// 删除今日之前的文件
if (lastModifiedTime < getTodayStartTime()) {
log.info("删除过期文件: {}", logFile.getAbsolutePath());
logFile.delete();
}
} catch (IOException e) {
log.error("处理文件时发生错误: {}", logFile.getAbsolutePath(), e);
}
}
}
/**
* 下载并保存单个日志文件,处理本地同名文件的序号逻辑。
*/
private void downloadSingleFile(String downloadFileUrl, String token, String systemName,
File downFile, String fileName) {
try (HttpResponse downloadRes = HttpUtils.http(downloadFileUrl)
.header("x-access-token", token)
.header("Content-Type", "application/octet-stream")
.timeout(60).exec("GET")) {
int downloadCode = downloadRes.code();
if (downloadCode != 200) {
log.warn("[{}] 下载文件接口错误码: {}", systemName, downloadCode);
return;
}
byte[] bytes = downloadRes.bodyAsBytes();
log.info("[{}] 下载文件大小: {} bytes", systemName, bytes.length);
// 确保目录存在
File downFileDir = downFile.getParentFile();
if (!downFileDir.exists()) {
downFileDir.mkdirs();
}
// .log 文件处理序号逻辑
if (fileName.endsWith(".log")) {
String baseFileName = getBaseFileNameWithoutSequence(downFile.getName());
File parentDir = downFile.getParentFile();
FileInfo fileInfo = this.getFileInfo(parentDir, baseFileName);
long localFileSize = fileInfo.getFileSize();
if (localFileSize == bytes.length) {
log.info("[{}] 文件已存在且大小相同,跳过下载: {}", systemName, downFile.getAbsolutePath());
return;
}
if (localFileSize > bytes.length) {
// 本地更大 → 新文件用下一个序号
downFile = new File(parentDir, fileInfo.getNextFileName());
} else {
// 远程更大 → 覆盖当前最大序号文件
downFile = new File(parentDir, fileInfo.getCurrentMaxFileName());
}
}
// 写入文件
try (FileOutputStream fos = new FileOutputStream(downFile)) {
fos.write(bytes);
log.info("[{}] 成功下载文件: {}", systemName, downFile.getAbsolutePath());
}
} catch (Exception e) {
log.error("[{}] 下载文件失败: {}", systemName, downloadFileUrl, e);
}
}
/**
* 获取基础文件名(去除序号部分,如 .log.001 → .log)。
*/
private String getBaseFileNameWithoutSequence(String fileName) {
// 检查是否包含序号格式(如 .log.001)
if (fileName.contains(".")) {
String[] parts = fileName.split("\\.");
if (parts.length >= 3) {
// 检查倒数第二个部分是否为数字
try {
Integer.parseInt(parts[parts.length - 1]);
// 返回去掉序号的部分
StringBuilder baseName = new StringBuilder();
for (int i = 0; i < parts.length - 1; i++) {
if (i > 0) baseName.append(".");
@@ -253,7 +318,6 @@ public class LogFileServiceImpl implements LogFileService {
}
return baseName.toString();
} catch (NumberFormatException e) {
// 不是数字,说明没有序号,返回原始名称
return fileName;
}
}
@@ -262,7 +326,7 @@ public class LogFileServiceImpl implements LogFileService {
}
/**
* 获取下一个可用序号
* 获取下一个可用序号
*/
private int getNextAvailableSequence(File directory, String baseFileName) {
File[] existingFiles = directory.listFiles((dir, name) ->
@@ -270,19 +334,16 @@ public class LogFileServiceImpl implements LogFileService {
(name.equals(baseFileName) || name.matches(baseFileName + "\\.\\d{3}")));
if (existingFiles == null) {
return 1; // 目录不存在或无法访问
return 1;
}
int maxSequence = 0;
for (File file : existingFiles) {
String fileName = file.getName();
if (fileName.equals(baseFileName)) {
// 基础文件存在,从1开始
maxSequence = Math.max(maxSequence, 1);
} else if (fileName.matches(baseFileName + "\\.\\d{3}")) {
// 提取序号
String sequencePart = fileName.substring(baseFileName.length() + 1); // +1 for the dot
String sequencePart = fileName.substring(baseFileName.length() + 1);
try {
int sequence = Integer.parseInt(sequencePart);
maxSequence = Math.max(maxSequence, sequence + 1);
@@ -291,15 +352,14 @@ public class LogFileServiceImpl implements LogFileService {
}
}
}
return maxSequence;
}
/**
* 获取今天零点的时间戳。
*/
private long getTodayStartTime() {
// 获取当前时间
long currentTime = System.currentTimeMillis();
// 计算今天0点的时间
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.setTimeInMillis(currentTime);
calendar.set(java.util.Calendar.HOUR_OF_DAY, 0);
@@ -310,43 +370,33 @@ public class LogFileServiceImpl implements LogFileService {
}
/**
* 获取当前最大序号文件名、下一个序号的文件名以及最大序号文件的大小
*
* @param directory 目标目录
* @param baseFileName 基础文件名(不含序号)
* @return FileInfo 包含当前最大序号文件名、下一个序号文件名和文件大小的信息
* 获取当前最大序号文件的信息。
*/
private FileInfo getFileInfo(File directory, String baseFileName) {
// 获取最大序号
int maxSequence = getNextAvailableSequence(directory, baseFileName) - 1;
// 构建当前最大序号文件名
String currentMaxFileName;
if (maxSequence <= 0) {
// 没有序号文件,直接使用基础文件名
currentMaxFileName = baseFileName;
} else {
// 存在序号文件,拼接序号
currentMaxFileName = baseFileName + "." + String.format("%03d", maxSequence);
}
// 构建下一个序号文件名
int nextSequence = maxSequence + 1;
String nextFileName = baseFileName + "." + String.format("%03d", nextSequence);
// 构造当前最大序号文件对象
File maxSequenceFile = new File(directory, currentMaxFileName);
// 检查文件是否存在并获取大小
long fileSize = 0;
if (maxSequenceFile.exists()) {
fileSize = maxSequenceFile.length();
}
// 返回封装好的信息
return new FileInfo(currentMaxFileName, nextFileName, fileSize);
}
/**
* 统计目录下文件名最后一个 _ 前的不同前缀数量(用于判断实例是否变更)。
*/
public static int countUniquePrefixes(String directoryPath) {
File directory = new File(directoryPath);
if (!directory.exists() || !directory.isDirectory()) {
@@ -360,7 +410,6 @@ public class LogFileServiceImpl implements LogFileService {
if (files != null) {
for (File file : files) {
String fileName = file.getName();
// 提取最后一个 _ 前面的部分作为前缀
int lastUnderscoreIndex = fileName.lastIndexOf('_');
if (lastUnderscoreIndex != -1) {
String prefix = fileName.substring(0, lastUnderscoreIndex);
@@ -368,8 +417,6 @@ public class LogFileServiceImpl implements LogFileService {
}
}
}
// log.info("唯一前缀数量: " + prefixSet.size());
return prefixSet.size();
}
}
+22 -9
View File
@@ -21,15 +21,28 @@ solon.scheduling.job:
initialDelay: 0
enable: true #用任务进行启停控制
base:
username: admin
password: "#fCjW%x2dv@a"
publicKey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmZfR/bA9X3vp86y1aEpvwzXJYKRRF1fLau2+05/ZtaITLpV8bhkmSf3neSy/Q9gAdvG75Fr73E+GWE+K5b0BpvIS1jDGo319+PpZR39SaZTKZ27XFXrosmJTZutN79t819HS1VseleunHAFgMVufE9U5jP6LGzl/wbkSy01GhzwIDAQAB"
gatewayUrl: https://api-jkglpt.iosp.ydpt.tech
# gatewayUrl: https://xj-api.mcrm.vip:8888
# 多系统日志下载配置
file:
path: D:/xjlogs
services:
- name: health-system
insCount: 2
systems:
- name: 健康管理系统
subPath: health # 自定义存储子目录(相对于 file.path),不配则默认用 name
username: admin
password: "#fCjW%x2dv@a"
publicKey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmZfR/bA9X3vp86y1aEpvwzXJYKRRF1fLau2+05/ZtaITLpV8bhkmSf3neSy/Q9gAdvG75Fr73E+GWE+K5b0BpvIS1jDGo319+PpZR39SaZTKZ27XFXrosmJTZutN79t819HS1VseleunHAFgMVufE9U5jP6LGzl/wbkSy01GhzwIDAQAB"
gatewayUrl: https://api-jkglpt.iosp.ydpt.tech
loginPath: /sys/mLogin
services:
- name: health-system
insCount: 2
- name: 健康监测系统
subPath: datacenter # 自定义存储子目录(相对于 file.path),不配则默认用 name
username: admin
password: Aa135790!123
publicKey: "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDbc+/BMkhCHZ5RDj6r4TXsFQJwP7HUd8qq+6wXo1KuvE6B1/9JmM7pNThmnkebSa+yP1ECa/H/QCSSKKHBb1JUeHKwUymvxVd78DPOanshqiAKSf+1bKx4g1OMT/5DPTn5Dg0lH5t4wvnUIpWM/7XH0Y1NWsgF5r8C90I3QcBkOwIDAQAB"
gatewayUrl: https://dcapi-jkglpt.iosp.ydpt.tech
loginPath: /sys/getToken
services:
- name: jeecg-system
insCount: 1
# - health-watch