feat: 初始化 rkutils 项目

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-03 10:13:10 +08:00
co-authored by Claude Opus 4.7
parent 932f0f1ea9
commit 8178ea3dff
11 changed files with 661 additions and 0 deletions
+29
View File
@@ -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
+99
View File
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.noear</groupId>
<artifactId>solon-parent</artifactId>
<version>3.9.0</version>
<relativePath />
</parent>
<groupId>com.renkang</groupId>
<artifactId>rkutils</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<description>rkutils project for Solon</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-lib</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-scheduling-simple</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-logging-logback-jakarta</artifactId>
</dependency>
<!-- Source: https://mvnrepository.com/artifact/com.alibaba.fastjson2/fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.60</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-net-httputils</artifactId>
</dependency>
<dependency>
<groupId>org.noear</groupId>
<artifactId>solon-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.noear</groupId>
<artifactId>solon-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>tencent</id>
<url>https://mirrors.cloud.tencent.com/nexus/repository/maven-public/</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
</project>
@@ -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();
}
}
@@ -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;
}
@@ -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<ServiceItem> services;
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class ServiceItem {
private String name;
private int insCount;
}
}
@@ -0,0 +1,5 @@
package com.renkang.rkutils.service;
public interface LogFileService {
public void downloadLogFile();
}
@@ -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<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 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<String> 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();
}
}
@@ -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();
}
}
@@ -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;
}
}
+27
View File
@@ -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);
}
}
+35
View File
@@ -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