diff --git a/.gitignore b/.gitignore index 9154f4c..65cfb52 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,32 @@ hs_err_pid* replay_pid* +target/ +!.mvn/wrapper/maven-wrapper.jar + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +*.log +*.flattened-pom.xml + +### NetBeans ### +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +.nb-gradle/ + +### Mac files ### +*.DS_Store diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..fe06dff --- /dev/null +++ b/pom.xml @@ -0,0 +1,99 @@ + + + 4.0.0 + + + org.noear + solon-parent + 3.9.0 + + + + com.renkang + rkutils + 1.0 + + jar + + rkutils project for Solon + + + 17 + + + + + org.noear + solon-lib + + + + org.noear + solon-scheduling-simple + + + + org.noear + solon-logging-logback-jakarta + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.60 + compile + + + org.projectlombok + lombok + provided + + + + org.noear + solon-net-httputils + + + + org.noear + solon-test + test + + + + + ${project.artifactId} + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.noear + solon-maven-plugin + + + + + + + tencent + https://mirrors.cloud.tencent.com/nexus/repository/maven-public/ + + false + + + + + \ No newline at end of file diff --git a/src/main/java/com/renkang/rkutils/App.java b/src/main/java/com/renkang/rkutils/App.java new file mode 100644 index 0000000..3c66f15 --- /dev/null +++ b/src/main/java/com/renkang/rkutils/App.java @@ -0,0 +1,16 @@ +package com.renkang.rkutils; + +import com.renkang.rkutils.service.LogFileService; +import org.noear.solon.Solon; +import org.noear.solon.annotation.SolonMain; +import org.noear.solon.scheduling.annotation.EnableScheduling; + +@EnableScheduling +@SolonMain +public class App { + public static void main(String[] args) { + Solon.start(App.class, args); + LogFileService logFileService = Solon.context().getBean(LogFileService.class); + logFileService.downloadLogFile(); + } +} \ No newline at end of file diff --git a/src/main/java/com/renkang/rkutils/config/BaseConfig.java b/src/main/java/com/renkang/rkutils/config/BaseConfig.java new file mode 100644 index 0000000..9eb2ddf --- /dev/null +++ b/src/main/java/com/renkang/rkutils/config/BaseConfig.java @@ -0,0 +1,16 @@ +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; + +} diff --git a/src/main/java/com/renkang/rkutils/config/LogFileConfig.java b/src/main/java/com/renkang/rkutils/config/LogFileConfig.java new file mode 100644 index 0000000..7207f3b --- /dev/null +++ b/src/main/java/com/renkang/rkutils/config/LogFileConfig.java @@ -0,0 +1,25 @@ +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 services; + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class ServiceItem { + private String name; + private int insCount; + } +} diff --git a/src/main/java/com/renkang/rkutils/service/LogFileService.java b/src/main/java/com/renkang/rkutils/service/LogFileService.java new file mode 100644 index 0000000..fae72a3 --- /dev/null +++ b/src/main/java/com/renkang/rkutils/service/LogFileService.java @@ -0,0 +1,5 @@ +package com.renkang.rkutils.service; + +public interface LogFileService { + public void downloadLogFile(); +} diff --git a/src/main/java/com/renkang/rkutils/service/impl/LogFileServiceImpl.java b/src/main/java/com/renkang/rkutils/service/impl/LogFileServiceImpl.java new file mode 100644 index 0000000..ab112a9 --- /dev/null +++ b/src/main/java/com/renkang/rkutils/service/impl/LogFileServiceImpl.java @@ -0,0 +1,375 @@ +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; +import lombok.extern.slf4j.Slf4j; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; +import org.noear.solon.core.util.DateUtil; +import org.noear.solon.net.http.HttpResponse; +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; +import java.nio.file.StandardCopyOption; +import java.util.Date; +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); + + String username = baseConfig.getUsername(); + String password = baseConfig.getPassword(); + String gatewayUrl = baseConfig.getGatewayUrl(); + String publicKey = baseConfig.getPublicKey(); + try { + // 使用RSA公钥加密密码 + String encryptedPassword = RSAUtil.encryptByPublicKey(password, publicKey); + + 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; + } + String token = body.getJSONObject("result").getString("token"); + log.info("登录成功,获取到的token为: {}", token); + + 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; + + 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; + } + + String responseBody = resp.bodyAsString(); // 在流关闭前立即读取 + log.info("返回接口数据: {}", responseBody); + JSONObject files = JSONObject.parseObject(responseBody); + + // 修复:使用正确的对象检查success状态 + boolean serSuccess = files.getBoolean("success"); + if (!serSuccess) { + log.info("获取日志文件结果: {}", "失败"); + continue; + } + + JSONArray filesList = files.getJSONObject("result").getJSONArray("records"); + log.info("获取到的日志文件为: {}", filesList); + + // 开始下载日志文件 + if (filesList != null && !filesList.isEmpty()) { + Set 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 filePath = path + "/" + serviceName; + + log.info("开始下载日志文件: {}/{}", filePath, fileName); + + 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); + } + } + + /** + * 获取基础文件名(去除序列号部分) + */ + 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("."); + baseName.append(parts[i]); + } + return baseName.toString(); + } catch (NumberFormatException e) { + // 不是数字,说明没有序号,返回原始名称 + return fileName; + } + } + } + return fileName; + } + + /** + * 获取下一个可用的序号 + */ + private int getNextAvailableSequence(File directory, String baseFileName) { + File[] existingFiles = directory.listFiles((dir, name) -> + name.startsWith(baseFileName) && + (name.equals(baseFileName) || name.matches(baseFileName + "\\.\\d{3}"))); + + if (existingFiles == null) { + 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 + try { + int sequence = Integer.parseInt(sequencePart); + maxSequence = Math.max(maxSequence, sequence + 1); + } catch (NumberFormatException e) { + // 序号解析失败,跳过 + } + } + } + + 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); + calendar.set(java.util.Calendar.MINUTE, 0); + calendar.set(java.util.Calendar.SECOND, 0); + calendar.set(java.util.Calendar.MILLISECOND, 0); + return calendar.getTimeInMillis(); + } + + /** + * 获取当前最大序号的文件名、下一个序号的文件名以及最大序号文件的大小 + * + * @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()) { + log.info("目录不存在或不是一个有效目录: {}", directoryPath); + return 0; + } + + Set prefixSet = new HashSet<>(); + File[] files = directory.listFiles(); + + if (files != null) { + for (File file : files) { + String fileName = file.getName(); + // 提取最后一个 _ 前面的部分作为前缀 + int lastUnderscoreIndex = fileName.lastIndexOf('_'); + if (lastUnderscoreIndex != -1) { + String prefix = fileName.substring(0, lastUnderscoreIndex); + prefixSet.add(prefix); + } + } + } + +// log.info("唯一前缀数量: " + prefixSet.size()); + return prefixSet.size(); + } +} diff --git a/src/main/java/com/renkang/rkutils/task/DownFileJob.java b/src/main/java/com/renkang/rkutils/task/DownFileJob.java new file mode 100644 index 0000000..3e8f3b4 --- /dev/null +++ b/src/main/java/com/renkang/rkutils/task/DownFileJob.java @@ -0,0 +1,19 @@ +package com.renkang.rkutils.task; + +import com.renkang.rkutils.service.LogFileService; +import org.noear.solon.annotation.Component; +import org.noear.solon.annotation.Inject; +import org.noear.solon.scheduling.annotation.Scheduled; + +import java.util.Date; + +@Component +public class DownFileJob { + + @Inject + private LogFileService logFileService; + @Scheduled(name = "downFileJob") + public void downFile(){ + logFileService.downloadLogFile(); + } +} \ No newline at end of file diff --git a/src/main/java/com/renkang/rkutils/vo/FileInfo.java b/src/main/java/com/renkang/rkutils/vo/FileInfo.java new file mode 100644 index 0000000..3d49a40 --- /dev/null +++ b/src/main/java/com/renkang/rkutils/vo/FileInfo.java @@ -0,0 +1,15 @@ +package com.renkang.rkutils.vo; + +import lombok.Data; + +@Data +public class FileInfo { + private String currentMaxFileName; // 当前最大序号的文件名 + private String nextFileName; // 下一个序号的文件名 + private long fileSize; // 最大序号文件的大小 + public FileInfo(String currentMaxFileName, String nextFileName, long fileSize) { + this.currentMaxFileName = currentMaxFileName; + this.nextFileName = nextFileName; + this.fileSize = fileSize; + } +} diff --git a/src/main/java/util/RSAUtil.java b/src/main/java/util/RSAUtil.java new file mode 100644 index 0000000..1cc7dce --- /dev/null +++ b/src/main/java/util/RSAUtil.java @@ -0,0 +1,27 @@ +package util; + +import javax.crypto.Cipher; +import java.nio.charset.StandardCharsets; +import java.security.PublicKey; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +public class RSAUtil { + /** + * 使用公钥加密 + */ + public static String encryptByPublicKey(String data, String publicKeyStr) throws Exception { + // 将公钥字符串转换为PublicKey对象 + byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr); + X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes); + java.security.KeyFactory keyFactory = java.security.KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(x509KeySpec); + + // 加密 + Cipher cipher = Cipher.getInstance("RSA"); + cipher.init(Cipher.ENCRYPT_MODE, publicKey); + byte[] encryptedData = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); + + return Base64.getEncoder().encodeToString(encryptedData); + } +} diff --git a/src/main/resources/app.yml b/src/main/resources/app.yml new file mode 100644 index 0000000..7fffe94 --- /dev/null +++ b/src/main/resources/app.yml @@ -0,0 +1,35 @@ +solon.config.load: + - "conf/app-xj.yml" + +solon.logging.appender: + console: + pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %magenta(${PID:-}) --- %-15([%15.15thread]) %-56(%cyan(%-40.40logger{39}%L)) : %msg%n" + file: + pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:-} --- %-15([%15.15thread]) %-56(%-40.40logger{39}%L) : %msg%n" + enable: true #是否启用 + extension: ".log" #v2.2.18 后支持(例:.log, .log.gz, .log.zip) + maxFileSize: "10 MB" + maxHistory: "7" #单位:天 + +solon.scheduling.job: + downFileJob: + cron: "0 30 * * * ?" # 每小时的第30分钟执行 + zone: "+08" + fixedRate: 0 + # 任务间隔时间:1小时 + fixedDelay: 0 + 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 +# - health-watch \ No newline at end of file