feat(core): 添加SVN日志查询工具和DeepSeek AI处理功能
- 实现SVN日志查询工具,支持版本范围和用户过滤 - 添加DeepSeek API集成,用于AI分析日志内容 - 创建Excel生成器,输出工作量统计报表 - 添加日志实体类和项目配置管理功能 - 集成POI库支持Excel文件操作 - 实现Markdown格式日志导出功能
This commit is contained in:
587
src/main/java/com/svnlog/DeepSeekLogProcessor.java
Normal file
587
src/main/java/com/svnlog/DeepSeekLogProcessor.java
Normal file
@@ -0,0 +1,587 @@
|
||||
package com.svnlog;
|
||||
|
||||
import okhttp3.*;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
* 使用DeepSeek API处理SVN日志并生成工作量统计Excel
|
||||
*/
|
||||
public class DeepSeekLogProcessor {
|
||||
private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions";
|
||||
private static final String API_KEY = "sk-48c59012c93b43a08fecbaf3e74799e7"; // 用户需要替换为实际的API Key
|
||||
private static final OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.readTimeout(300, java.util.concurrent.TimeUnit.SECONDS) // 5分钟读取超时
|
||||
.writeTimeout(60, java.util.concurrent.TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
Scanner scanner = new Scanner(System.in);
|
||||
System.out.println("===========================================");
|
||||
System.out.println(" SVN日志工作量统计工具(DeepSeek版)");
|
||||
System.out.println(" 支持多项目汇总分析");
|
||||
System.out.println("===========================================");
|
||||
System.out.println();
|
||||
|
||||
// 读取markdown日志文件目录
|
||||
System.out.print("请输入markdown日志文件所在目录路径 (回车使用当前目录): ");
|
||||
String dirPath = scanner.nextLine().trim();
|
||||
|
||||
File dir;
|
||||
if (dirPath.isEmpty()) {
|
||||
dir = new File(".");
|
||||
} else {
|
||||
dir = new File(dirPath);
|
||||
}
|
||||
|
||||
if (!dir.exists() || !dir.isDirectory()) {
|
||||
System.err.println("错误: 目录不存在或不是有效目录!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 扫描目录中的所有 .md 文件
|
||||
File[] mdFiles = dir.listFiles((d, name) -> name.endsWith(".md"));
|
||||
if (mdFiles == null || mdFiles.length == 0) {
|
||||
System.err.println("错误: 目录中未找到任何 .md 文件!");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("找到 " + mdFiles.length + " 个日志文件:");
|
||||
for (File file : mdFiles) {
|
||||
System.out.println(" - " + file.getName());
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
// 输入工作周期
|
||||
SimpleDateFormat periodSdf = new SimpleDateFormat("yyyy年MM月");
|
||||
String defaultPeriod = periodSdf.format(new Date());
|
||||
System.out.print("请输入工作周期 (例如: 2025年12月,回车使用默认: " + defaultPeriod + "): ");
|
||||
String period = scanner.nextLine().trim();
|
||||
|
||||
if (period.isEmpty()) {
|
||||
period = defaultPeriod;
|
||||
System.out.println("使用默认工作周期: " + period);
|
||||
}
|
||||
|
||||
// 读取并合并所有markdown文件
|
||||
String combinedContent = readAndCombineMarkdownFiles(mdFiles);
|
||||
System.out.println("成功读取并合并 " + mdFiles.length + " 个日志文件,总长度: " + combinedContent.length() + " 字符");
|
||||
|
||||
// 提示API Key
|
||||
System.out.print("请输入DeepSeek API Key (留空使用代码中预设的): ");
|
||||
String inputApiKey = scanner.nextLine().trim();
|
||||
String apiKey = inputApiKey.isEmpty() ? API_KEY : inputApiKey;
|
||||
|
||||
if (apiKey.equals("YOUR_DEEPSEEK_API_KEY")) {
|
||||
System.err.println("错误: 请提供有效的DeepSeek API Key!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 询问输出文件名
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMM");
|
||||
String defaultOutput = sdf.format(new Date()) + "工作量统计.xlsx";
|
||||
System.out.print("请输入输出Excel文件名 (回车使用默认: " + defaultOutput + "): ");
|
||||
String outputPath = scanner.nextLine().trim();
|
||||
if (outputPath.isEmpty()) {
|
||||
outputPath = defaultOutput;
|
||||
}
|
||||
|
||||
System.out.println();
|
||||
System.out.println("正在调用DeepSeek API分析日志...");
|
||||
|
||||
// 调用DeepSeek API处理日志
|
||||
String prompt = buildPrompt(combinedContent, period);
|
||||
String aiResponse = callDeepSeekAPI(apiKey, prompt);
|
||||
|
||||
if (aiResponse == null) {
|
||||
System.err.println("DeepSeek API调用失败!请检查网络连接和API Key。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (aiResponse.isEmpty()) {
|
||||
System.err.println("DeepSeek API返回空响应!请重试或联系技术支持。");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("DeepSeek分析完成,正在生成Excel...");
|
||||
|
||||
// 生成Excel
|
||||
generateExcel(outputPath, aiResponse);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("Excel文件生成成功: " + outputPath);
|
||||
System.out.println();
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("发生错误: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取文件内容
|
||||
*/
|
||||
private static String readFile(String path) throws IOException {
|
||||
return new String(Files.readAllBytes(new File(path).toPath()), "UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取并合并多个markdown文件的内容
|
||||
*/
|
||||
private static String readAndCombineMarkdownFiles(File[] mdFiles) throws IOException {
|
||||
StringBuilder combinedContent = new StringBuilder();
|
||||
|
||||
for (File file : mdFiles) {
|
||||
String projectName = extractProjectName(file.getName());
|
||||
String content = readFile(file.getAbsolutePath());
|
||||
|
||||
combinedContent.append("\n\n");
|
||||
combinedContent.append("=== 项目: ").append(projectName).append(" ===\n");
|
||||
combinedContent.append(content);
|
||||
}
|
||||
|
||||
return combinedContent.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从文件名中提取项目名称
|
||||
* 例如: svn_log_PRS-7050场站智慧管控_20260130_093348.md -> PRS-7050场站智慧管控
|
||||
*/
|
||||
private static String extractProjectName(String fileName) {
|
||||
// 去掉 svn_log_ 前缀
|
||||
if (fileName.startsWith("svn_log_")) {
|
||||
fileName = fileName.substring(8);
|
||||
}
|
||||
|
||||
// 去掉 .md 后缀
|
||||
if (fileName.endsWith(".md")) {
|
||||
fileName = fileName.substring(0, fileName.length() - 3);
|
||||
}
|
||||
|
||||
// 去掉时间戳部分 (格式: _YYYYMMDD_HHMMSS)
|
||||
int lastUnderscore = fileName.lastIndexOf('_');
|
||||
if (lastUnderscore > 0) {
|
||||
// 检查是否是时间戳格式
|
||||
String timestampPart = fileName.substring(lastUnderscore + 1);
|
||||
if (timestampPart.matches("\\d{8}_\\d{6}")) {
|
||||
fileName = fileName.substring(0, lastUnderscore);
|
||||
}
|
||||
}
|
||||
|
||||
return fileName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建发送给DeepSeek的提示词
|
||||
*/
|
||||
private static String buildPrompt(String markdownContent, String period) {
|
||||
return "你是一个专业的项目管理助手。请分析以下多个项目的SVN日志,并生成工作量统计数据。\n\n" +
|
||||
"日志内容包含多个项目,每个项目之间用 === 项目: xxx === 标识。\n" +
|
||||
"工作周期: " + period + "\n\n" +
|
||||
"SVN日志内容:\n" + markdownContent + "\n\n" +
|
||||
"请按照以下JSON格式返回工作量统计数据:\n" +
|
||||
"{\n" +
|
||||
" \"team\": \"所属班组\",\n" +
|
||||
" \"contact\": \"技术对接人\",\n" +
|
||||
" \"developer\": \"开发人员\",\n" +
|
||||
" \"period\": \"" + period + "\",\n" +
|
||||
" \"records\": [\n" +
|
||||
" {\n" +
|
||||
" \"sequence\": 1,\n" +
|
||||
" \"project\": \"项目1/项目2/项目3\",\n" +
|
||||
" \"content\": \"# 项目1\\n1.工作内容1\\n2.工作内容2\\n\\n# 项目2\\n1.工作内容1\\n2.工作内容2\\n\\n# 项目3\\n1.工作内容1\\n2.工作内容2\"\n" +
|
||||
" }\n" +
|
||||
" ]\n" +
|
||||
"}\n\n" +
|
||||
"重要要求:\n" +
|
||||
"1. 根据日志作者确定开发人员\n" +
|
||||
"2. 将所有项目的工作内容合并到一条记录中\n" +
|
||||
"3. 项目名称字段(project):使用 / 分隔多个项目,例如:\"PRS7050场站系统/PRS7950智能巡视现场问题/PRS7950电科院测试\"\n" +
|
||||
"4. 具体工作内容字段(content):使用 # 作为项目分类标识,格式为:\"# 项目名称\\n1.工作内容\\n2.工作内容\\n\\n# 下一个项目\\n1.工作内容\"\n" +
|
||||
"5. 不同项目之间用空行分隔\n" +
|
||||
"6. 只返回JSON,不要有其他文字\n" +
|
||||
"7. 提取具体工作内容,要详细和有条理\n" +
|
||||
"8. 项目名称要简洁明确,去掉多余的前缀和后缀";
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用DeepSeek API(流式输出)
|
||||
*/
|
||||
private static String callDeepSeekAPI(String apiKey, String prompt) throws IOException {
|
||||
JSONObject requestBody = new JSONObject();
|
||||
requestBody.put("model", "deepseek-chat");
|
||||
|
||||
// 创建消息对象,包含 role 和 content 字段
|
||||
JSONObject messageObj = new JSONObject();
|
||||
messageObj.put("role", "user");
|
||||
messageObj.put("content", prompt);
|
||||
|
||||
// 创建消息数组
|
||||
com.google.gson.JsonArray messagesArray = new com.google.gson.JsonArray();
|
||||
messagesArray.add(messageObj.jsonObject);
|
||||
requestBody.put("messages", messagesArray);
|
||||
|
||||
requestBody.put("temperature", 0.7);
|
||||
requestBody.put("max_tokens", 4000);
|
||||
requestBody.put("stream", Optional.of(true)); // 启用流式输出
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(DEEPSEEK_API_URL)
|
||||
.addHeader("Authorization", "Bearer " + apiKey)
|
||||
.addHeader("Content-Type", "application/json")
|
||||
.post(RequestBody.create(requestBody.toString(), MediaType.parse("application/json")))
|
||||
.build();
|
||||
|
||||
StringBuilder fullResponse = new StringBuilder();
|
||||
int chunkCount = 0;
|
||||
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (!response.isSuccessful()) {
|
||||
System.err.println("API调用失败: " + response.code() + " " + response.message());
|
||||
String errorResponse = response.body().string();
|
||||
System.err.println("响应: " + errorResponse);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 读取流式响应
|
||||
try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.body().byteStream()))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.startsWith("data: ")) {
|
||||
String data = line.substring(6);
|
||||
if (data.equals("[DONE]")) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
JSONObject chunk = new JSONObject(data);
|
||||
if (chunk.has("choices") && chunk.getJSONArray("choices").size() > 0) {
|
||||
JSONObject choice = chunk.getJSONArray("choices").get(0);
|
||||
if (choice.has("delta")) {
|
||||
JSONObject delta = choice.getJSONObject("delta");
|
||||
if (delta.has("content")) {
|
||||
String content = delta.optString("content", "");
|
||||
fullResponse.append(content);
|
||||
chunkCount++;
|
||||
// 实时打印处理进度
|
||||
System.out.print(content);
|
||||
System.out.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// 忽略解析错误,继续处理下一行
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
System.err.println("API调用过程中发生异常: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
|
||||
System.out.println(); // 换行
|
||||
System.out.println("收到 " + chunkCount + " 个数据块");
|
||||
|
||||
if (fullResponse.length() == 0) {
|
||||
System.err.println("警告: 未收到任何响应内容");
|
||||
}
|
||||
|
||||
return fullResponse.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从响应中提取纯 JSON 内容
|
||||
*/
|
||||
private static String extractJson(String response) {
|
||||
String trimmed = response.trim();
|
||||
|
||||
// 去除 ```json 标记
|
||||
if (trimmed.startsWith("```json")) {
|
||||
trimmed = trimmed.substring(7);
|
||||
} else if (trimmed.startsWith("```")) {
|
||||
trimmed = trimmed.substring(3);
|
||||
}
|
||||
|
||||
// 去除 ``` 结束标记
|
||||
if (trimmed.endsWith("```")) {
|
||||
trimmed = trimmed.substring(0, trimmed.length() - 3);
|
||||
}
|
||||
|
||||
return trimmed.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成Excel文件
|
||||
*/
|
||||
private static void generateExcel(String outputPath, String jsonResponse) throws IOException {
|
||||
// 提取纯 JSON 内容(去除 ```json 和 ``` 标记)
|
||||
String cleanJson = extractJson(jsonResponse);
|
||||
|
||||
// 解析JSON响应
|
||||
JSONObject data = new JSONObject(cleanJson);
|
||||
|
||||
// 创建工作簿
|
||||
Workbook workbook = new XSSFWorkbook();
|
||||
Sheet sheet = workbook.createSheet("工作表1");
|
||||
|
||||
// 创建样式
|
||||
CellStyle headerStyle = createHeaderStyle(workbook);
|
||||
CellStyle contentStyle = createContentStyle(workbook);
|
||||
CellStyle workContentStyle = createWorkContentStyle(workbook);
|
||||
|
||||
// 创建表头(7列,与参考文件一致)
|
||||
Row headerRow = sheet.createRow(0);
|
||||
headerRow.setHeightInPoints(14.25f); // 表头行高
|
||||
String[] headers = {"序号", "所属班组", "技术对接", "开发人员", "工作周期", "开发项目名称", "具体工作内容"};
|
||||
for (int i = 0; i < headers.length; i++) {
|
||||
Cell cell = headerRow.createCell(i);
|
||||
cell.setCellValue(headers[i]);
|
||||
cell.setCellStyle(headerStyle);
|
||||
}
|
||||
|
||||
// 设置固定列宽(与参考文件一致)
|
||||
sheet.setColumnWidth(0, 2048); // 序号:8.00字符
|
||||
sheet.setColumnWidth(1, 3328); // 所属班组:13.00字符
|
||||
sheet.setColumnWidth(2, 4608); // 技术对接:18.00字符
|
||||
sheet.setColumnWidth(3, 3840); // 开发人员:15.00字符
|
||||
sheet.setColumnWidth(4, 5888); // 工作周期:23.00字符
|
||||
sheet.setColumnWidth(5, 14080); // 开发项目名称:55.00字符
|
||||
sheet.setColumnWidth(6, 43991); // 具体工作内容:171.84字符
|
||||
|
||||
// 获取记录
|
||||
String team = data.optString("team", "");
|
||||
String contact = data.optString("contact", "");
|
||||
String developer = data.optString("developer", "");
|
||||
String period = data.optString("period", "");
|
||||
|
||||
if (data.has("records")) {
|
||||
JSONArray recordsArray = data.getJSONArray("records");
|
||||
int rowNum = 1;
|
||||
|
||||
for (int i = 0; i < recordsArray.size(); i++) {
|
||||
JSONObject record = recordsArray.get(i);
|
||||
Row row = sheet.createRow(rowNum++);
|
||||
row.setHeightInPoints(16.50f); // 内容行高
|
||||
|
||||
// 序号
|
||||
Cell cell0 = row.createCell(0);
|
||||
cell0.setCellValue(record.optDouble("sequence", i + 1));
|
||||
cell0.setCellStyle(contentStyle);
|
||||
|
||||
// 所属班组
|
||||
Cell cell1 = row.createCell(1);
|
||||
cell1.setCellValue(team);
|
||||
cell1.setCellStyle(contentStyle);
|
||||
|
||||
// 技术对接
|
||||
Cell cell2 = row.createCell(2);
|
||||
cell2.setCellValue(contact);
|
||||
cell2.setCellStyle(contentStyle);
|
||||
|
||||
// 开发人员
|
||||
Cell cell3 = row.createCell(3);
|
||||
cell3.setCellValue(developer);
|
||||
cell3.setCellStyle(contentStyle);
|
||||
|
||||
// 工作周期
|
||||
Cell cell4 = row.createCell(4);
|
||||
cell4.setCellValue(period);
|
||||
cell4.setCellStyle(contentStyle);
|
||||
|
||||
// 项目名称(多个项目用 / 分隔)
|
||||
Cell cell5 = row.createCell(5);
|
||||
cell5.setCellValue(record.optString("project", ""));
|
||||
cell5.setCellStyle(contentStyle);
|
||||
|
||||
// 工作内容(支持换行,用 # 标识不同项目)
|
||||
Cell cell6 = row.createCell(6);
|
||||
cell6.setCellValue(record.optString("content", ""));
|
||||
cell6.setCellStyle(workContentStyle); // 使用工作内容样式
|
||||
}
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
try (FileOutputStream fos = new FileOutputStream(outputPath)) {
|
||||
workbook.write(fos);
|
||||
}
|
||||
workbook.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建表头样式
|
||||
*/
|
||||
private static CellStyle createHeaderStyle(Workbook workbook) {
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
Font font = workbook.createFont();
|
||||
font.setFontName("SimSun"); // 字体名称:SimSun
|
||||
font.setFontHeightInPoints((short) 11); // 字体大小:11磅
|
||||
font.setBold(false); // 不粗体
|
||||
font.setColor(IndexedColors.BLACK.getIndex()); // 黑色
|
||||
style.setFont(font);
|
||||
style.setAlignment(HorizontalAlignment.GENERAL); // 水平对齐:常规
|
||||
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直对齐:居中
|
||||
style.setFillPattern(FillPatternType.NO_FILL); // 无填充
|
||||
style.setBorderTop(BorderStyle.THIN);
|
||||
style.setBorderBottom(BorderStyle.THIN);
|
||||
style.setBorderLeft(BorderStyle.THIN);
|
||||
style.setBorderRight(BorderStyle.THIN);
|
||||
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
|
||||
style.setBottomBorderColor(IndexedColors.BLACK.getIndex());
|
||||
style.setLeftBorderColor(IndexedColors.BLACK.getIndex());
|
||||
style.setRightBorderColor(IndexedColors.BLACK.getIndex());
|
||||
style.setWrapText(false); // 不换行
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建普通内容样式(列A-F)
|
||||
*/
|
||||
private static CellStyle createContentStyle(Workbook workbook) {
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
Font font = workbook.createFont();
|
||||
font.setFontName("宋体"); // 字体名称:宋体
|
||||
font.setFontHeightInPoints((short) 11); // 字体大小:11磅
|
||||
font.setBold(false); // 不粗体
|
||||
style.setFont(font);
|
||||
style.setAlignment(HorizontalAlignment.GENERAL); // 水平对齐:常规
|
||||
style.setVerticalAlignment(VerticalAlignment.CENTER); // 垂直对齐:居中
|
||||
style.setFillPattern(FillPatternType.NO_FILL); // 无填充
|
||||
style.setBorderTop(BorderStyle.THIN);
|
||||
style.setBorderBottom(BorderStyle.NONE);
|
||||
style.setBorderLeft(BorderStyle.NONE);
|
||||
style.setBorderRight(BorderStyle.NONE);
|
||||
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
|
||||
style.setWrapText(false); // 不换行
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作内容样式(列G)
|
||||
*/
|
||||
private static CellStyle createWorkContentStyle(Workbook workbook) {
|
||||
CellStyle style = workbook.createCellStyle();
|
||||
Font font = workbook.createFont();
|
||||
font.setFontName("NSimSun"); // 字体名称:新宋体
|
||||
font.setFontHeightInPoints((short) 14); // 字体大小:14磅
|
||||
font.setBold(true); // 粗体
|
||||
font.setColor(IndexedColors.BLACK.getIndex()); // 黑色
|
||||
style.setFont(font);
|
||||
style.setAlignment(HorizontalAlignment.LEFT); // 水平对齐:左对齐
|
||||
style.setVerticalAlignment(VerticalAlignment.TOP); // 垂直对齐:顶部
|
||||
style.setFillForegroundColor(IndexedColors.YELLOW.getIndex()); // 黄色背景
|
||||
style.setFillPattern(FillPatternType.SOLID_FOREGROUND); // 实心填充
|
||||
style.setBorderTop(BorderStyle.THIN);
|
||||
style.setBorderBottom(BorderStyle.NONE);
|
||||
style.setBorderLeft(BorderStyle.NONE);
|
||||
style.setBorderRight(BorderStyle.NONE);
|
||||
style.setTopBorderColor(IndexedColors.BLACK.getIndex());
|
||||
style.setWrapText(true); // 自动换行
|
||||
return style;
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的JSON工具类
|
||||
*/
|
||||
static class JSONObject {
|
||||
private final com.google.gson.JsonObject jsonObject;
|
||||
|
||||
public JSONObject() {
|
||||
this.jsonObject = new com.google.gson.JsonObject();
|
||||
}
|
||||
|
||||
public JSONObject(String jsonString) {
|
||||
com.google.gson.Gson gson = new com.google.gson.Gson();
|
||||
this.jsonObject = gson.fromJson(jsonString, com.google.gson.JsonObject.class);
|
||||
}
|
||||
|
||||
public JSONObject(String key, String value) {
|
||||
this();
|
||||
put(key, value);
|
||||
}
|
||||
|
||||
public void put(String key, String value) {
|
||||
jsonObject.addProperty(key, value);
|
||||
}
|
||||
|
||||
public void put(String key, int value) {
|
||||
jsonObject.addProperty(key, String.valueOf(value));
|
||||
}
|
||||
|
||||
public void put(String key, double value) {
|
||||
jsonObject.addProperty(key, String.valueOf(value));
|
||||
}
|
||||
|
||||
public void put(String key, Object value) {
|
||||
com.google.gson.Gson gson = new com.google.gson.Gson();
|
||||
jsonObject.add(key, gson.toJsonTree(value));
|
||||
}
|
||||
|
||||
public String optString(String key, String defaultValue) {
|
||||
if (jsonObject.has(key) && !jsonObject.get(key).isJsonNull()) {
|
||||
return jsonObject.get(key).getAsString();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public double optDouble(String key, double defaultValue) {
|
||||
if (jsonObject.has(key) && !jsonObject.get(key).isJsonNull()) {
|
||||
return jsonObject.get(key).getAsDouble();
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
public boolean has(String key) {
|
||||
return jsonObject.has(key);
|
||||
}
|
||||
|
||||
public JSONArray getJSONArray(String key) {
|
||||
return new JSONArray(jsonObject.get(key).getAsJsonArray());
|
||||
}
|
||||
|
||||
public JSONObject getJSONObject(String key) {
|
||||
return new JSONObject(jsonObject.get(key).getAsJsonObject().toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return jsonObject.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 简单的JSONArray工具类
|
||||
*/
|
||||
static class JSONArray {
|
||||
private final com.google.gson.JsonArray jsonArray;
|
||||
|
||||
public JSONArray(com.google.gson.JsonArray jsonArray) {
|
||||
this.jsonArray = jsonArray;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return jsonArray.size();
|
||||
}
|
||||
|
||||
public JSONObject get(int index) {
|
||||
return new JSONObject(jsonArray.get(index).getAsJsonObject().toString());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <T> java.util.List<JSONObject> toList() {
|
||||
java.util.List<JSONObject> list = new ArrayList<>();
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
list.add(get(i));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/main/java/com/svnlog/ExcelAnalyzer.java
Normal file
84
src/main/java/com/svnlog/ExcelAnalyzer.java
Normal file
@@ -0,0 +1,84 @@
|
||||
package com.svnlog;
|
||||
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 临时工具类,用于分析现有Excel文件格式
|
||||
*/
|
||||
public class ExcelAnalyzer {
|
||||
public static void main(String[] args) {
|
||||
String excelPath = "/home/liumangmang/opencode/日志/202512工作量统计_刘靖.xlsx";
|
||||
|
||||
try (FileInputStream fis = new FileInputStream(excelPath);
|
||||
Workbook workbook = new XSSFWorkbook(fis)) {
|
||||
|
||||
System.out.println("工作表数量: " + workbook.getNumberOfSheets());
|
||||
for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
|
||||
System.out.println("工作表 " + i + ": " + workbook.getSheetName(i));
|
||||
}
|
||||
|
||||
Sheet sheet = workbook.getSheetAt(0);
|
||||
System.out.println("\n工作表名称: " + sheet.getSheetName());
|
||||
System.out.println("总行数: " + sheet.getPhysicalNumberOfRows());
|
||||
System.out.println("最后一行索引: " + sheet.getLastRowNum());
|
||||
|
||||
// 读取前20行数据
|
||||
System.out.println("\n前20行数据:");
|
||||
for (int i = 0; i <= Math.min(19, sheet.getLastRowNum()); i++) {
|
||||
Row row = sheet.getRow(i);
|
||||
if (row != null) {
|
||||
System.out.print("第" + (i + 1) + "行: ");
|
||||
for (Cell cell : row) {
|
||||
String value = getCellValueAsString(cell);
|
||||
System.out.print("[" + value + "] ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
}
|
||||
|
||||
// 读取表头
|
||||
Row headerRow = sheet.getRow(0);
|
||||
if (headerRow != null) {
|
||||
System.out.println("\n表头列数: " + headerRow.getLastCellNum());
|
||||
System.out.print("表头: ");
|
||||
for (Cell cell : headerRow) {
|
||||
System.out.print("[" + getCellValueAsString(cell) + "] ");
|
||||
}
|
||||
System.out.println();
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
System.err.println("读取Excel文件出错: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getCellValueAsString(Cell cell) {
|
||||
if (cell == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (cell.getCellType()) {
|
||||
case STRING:
|
||||
return cell.getStringCellValue().trim();
|
||||
case NUMERIC:
|
||||
if (DateUtil.isCellDateFormatted(cell)) {
|
||||
return cell.getDateCellValue().toString();
|
||||
} else {
|
||||
return String.valueOf(cell.getNumericCellValue());
|
||||
}
|
||||
case BOOLEAN:
|
||||
return String.valueOf(cell.getBooleanCellValue());
|
||||
case FORMULA:
|
||||
return cell.getCellFormula();
|
||||
case BLANK:
|
||||
return "";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
71
src/main/java/com/svnlog/LogEntry.java
Normal file
71
src/main/java/com/svnlog/LogEntry.java
Normal file
@@ -0,0 +1,71 @@
|
||||
package com.svnlog;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class LogEntry {
|
||||
private long revision;
|
||||
private String author;
|
||||
private Date date;
|
||||
private String message;
|
||||
private String[] changedPaths;
|
||||
|
||||
public LogEntry() {
|
||||
}
|
||||
|
||||
public LogEntry(long revision, String author, Date date, String message) {
|
||||
this.revision = revision;
|
||||
this.author = author;
|
||||
this.date = date;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public long getRevision() {
|
||||
return revision;
|
||||
}
|
||||
|
||||
public void setRevision(long revision) {
|
||||
this.revision = revision;
|
||||
}
|
||||
|
||||
public String getAuthor() {
|
||||
return author;
|
||||
}
|
||||
|
||||
public void setAuthor(String author) {
|
||||
this.author = author;
|
||||
}
|
||||
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(Date date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String[] getChangedPaths() {
|
||||
return changedPaths;
|
||||
}
|
||||
|
||||
public void setChangedPaths(String[] changedPaths) {
|
||||
this.changedPaths = changedPaths;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "LogEntry{" +
|
||||
"revision=" + revision +
|
||||
", author='" + author + '\'' +
|
||||
", date=" + date +
|
||||
", message='" + message + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
215
src/main/java/com/svnlog/Main.java
Normal file
215
src/main/java/com/svnlog/Main.java
Normal file
@@ -0,0 +1,215 @@
|
||||
package com.svnlog;
|
||||
|
||||
import org.tmatesoft.svn.core.SVNException;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class Main {
|
||||
private static final Scanner scanner = new Scanner(System.in);
|
||||
private static final SimpleDateFormat fileNameDateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
|
||||
|
||||
// 预设项目列表
|
||||
private static final Project[] PRESET_PROJECTS = {
|
||||
new Project("PRS-7050场站智慧管控", "https://10.6.220.216:48080/svn/houtai/001_后台软件/PRS-7050场站智慧管控/01_开发库/V1.00/src_java"),
|
||||
new Project("PRS-7950在线巡视", "https://10.6.220.216:48080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V2.00/src_java"),
|
||||
new Project("PRS-7950在线巡视电科院测试版", "https://10.6.220.216:48080/svn/houtai/001_后台软件/PRS-7950在线巡视/01_开发库/V1.00_2024/src_java")
|
||||
};
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println("===========================================");
|
||||
System.out.println(" SVN 日志查询工具 v1.0");
|
||||
System.out.println("===========================================");
|
||||
System.out.println();
|
||||
|
||||
try {
|
||||
// 创建 md 目录
|
||||
File mdDir = new File("md");
|
||||
if (!mdDir.exists()) {
|
||||
boolean created = mdDir.mkdir();
|
||||
if (created) {
|
||||
System.out.println("已创建 md 目录用于存放日志文件");
|
||||
}
|
||||
}
|
||||
System.out.println();
|
||||
|
||||
// 选择项目
|
||||
Project selectedProject = selectProject();
|
||||
String url = selectedProject.getUrl();
|
||||
System.out.println("已选择项目: " + selectedProject.getName());
|
||||
System.out.println("SVN地址: " + url);
|
||||
System.out.println();
|
||||
|
||||
String username = readInput("请输入SVN账号: ");
|
||||
String password = readPassword("请输入SVN密码: ");
|
||||
|
||||
System.out.println("正在连接SVN仓库...");
|
||||
SVNLogFetcher fetcher = new SVNLogFetcher(url, username, password);
|
||||
fetcher.testConnection();
|
||||
System.out.println("连接成功!");
|
||||
System.out.println();
|
||||
|
||||
long latestRevision = fetcher.getLatestRevision();
|
||||
System.out.println("最新版本号: " + latestRevision);
|
||||
System.out.println();
|
||||
|
||||
long startRevision = readLongInput("请输入开始版本号 (回车使用最新版本): ", latestRevision);
|
||||
long endRevision = readLongInput("请输入结束版本号 (回车使用最新版本): ", latestRevision);
|
||||
String filterUser = readInput("请输入过滤用户名 (包含匹配,回车跳过过滤): ");
|
||||
|
||||
System.out.println();
|
||||
System.out.println("正在获取日志...");
|
||||
List<LogEntry> logs = fetcher.fetchLogs(startRevision, endRevision, filterUser);
|
||||
|
||||
if (logs.isEmpty()) {
|
||||
System.out.println("没有找到符合条件的日志记录。");
|
||||
return;
|
||||
}
|
||||
|
||||
System.out.println("获取到 " + logs.size() + " 条日志记录。");
|
||||
System.out.println();
|
||||
|
||||
// 生成Markdown文件(保存到 md 目录)
|
||||
String fileName = "md/svn_log_" + selectedProject.getName() + "_" + fileNameDateFormat.format(new Date()) + ".md";
|
||||
generateMarkdown(fileName, url, username, startRevision, endRevision, filterUser, logs, fetcher);
|
||||
|
||||
System.out.println();
|
||||
System.out.println("日志已成功导出到: " + fileName);
|
||||
System.out.println();
|
||||
|
||||
} catch (SVNException e) {
|
||||
System.err.println("SVN错误: " + e.getMessage());
|
||||
} catch (Exception e) {
|
||||
System.err.println("发生错误: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 让用户选择项目
|
||||
*/
|
||||
private static Project selectProject() {
|
||||
System.out.println("请选择SVN项目:");
|
||||
for (int i = 0; i < PRESET_PROJECTS.length; i++) {
|
||||
System.out.println(" " + (i + 1) + ". " + PRESET_PROJECTS[i].getName());
|
||||
}
|
||||
System.out.println(" 0. 自定义SVN地址");
|
||||
System.out.println();
|
||||
|
||||
while (true) {
|
||||
System.out.print("请输入项目编号 (1-" + PRESET_PROJECTS.length + ", 0为自定义): ");
|
||||
String input = scanner.nextLine().trim();
|
||||
|
||||
try {
|
||||
int choice = Integer.parseInt(input);
|
||||
|
||||
if (choice == 0) {
|
||||
String customUrl = readInput("请输入SVN仓库地址: ");
|
||||
return new Project("自定义项目", customUrl);
|
||||
} else if (choice >= 1 && choice <= PRESET_PROJECTS.length) {
|
||||
return PRESET_PROJECTS[choice - 1];
|
||||
} else {
|
||||
System.out.println("输入无效,请重新选择!");
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
System.out.println("输入无效,请输入数字!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String readInput(String prompt) {
|
||||
System.out.print(prompt);
|
||||
return scanner.nextLine().trim();
|
||||
}
|
||||
|
||||
private static String readPassword(String prompt) {
|
||||
if (System.console() != null) {
|
||||
char[] password = System.console().readPassword("%s", prompt);
|
||||
return new String(password);
|
||||
} else {
|
||||
System.out.print(prompt);
|
||||
return scanner.nextLine();
|
||||
}
|
||||
}
|
||||
|
||||
private static long readLongInput(String prompt, long defaultValue) {
|
||||
System.out.print(prompt);
|
||||
String input = scanner.nextLine().trim();
|
||||
|
||||
if (input.isEmpty()) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
try {
|
||||
return Long.parseLong(input);
|
||||
} catch (NumberFormatException e) {
|
||||
System.out.println("输入无效,使用默认值: " + defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateMarkdown(String fileName, String url, String username,
|
||||
long startRevision, long endRevision, String filterUser,
|
||||
List<LogEntry> logs, SVNLogFetcher fetcher) throws IOException {
|
||||
StringBuilder markdown = new StringBuilder();
|
||||
|
||||
// 标题
|
||||
markdown.append("# SVN 日志报告\n\n");
|
||||
|
||||
// 查询条件(简化版)
|
||||
markdown.append("## 查询条件\n\n");
|
||||
markdown.append("- **SVN地址**: `").append(url).append("`\n");
|
||||
markdown.append("- **版本范围**: r").append(startRevision).append(" - r").append(endRevision).append("\n");
|
||||
if (filterUser != null && !filterUser.isEmpty()) {
|
||||
markdown.append("- **过滤用户**: `").append(filterUser).append("`\n");
|
||||
}
|
||||
markdown.append("\n");
|
||||
|
||||
// 日志详情(简化版,只包含作者、时间、版本、提交信息)
|
||||
markdown.append("## 日志详情\n\n");
|
||||
|
||||
for (LogEntry entry : logs) {
|
||||
markdown.append("### r").append(entry.getRevision()).append("\n\n");
|
||||
markdown.append("**作者**: `").append(entry.getAuthor()).append("` \n");
|
||||
markdown.append("**时间**: ").append(fetcher.formatDate(entry.getDate())).append(" \n");
|
||||
markdown.append("**版本**: r").append(entry.getRevision()).append("\n\n");
|
||||
|
||||
String message = entry.getMessage();
|
||||
if (message != null && !message.isEmpty()) {
|
||||
markdown.append("**提交信息**:\n\n");
|
||||
markdown.append("```\n").append(message).append("\n```\n\n");
|
||||
} else {
|
||||
markdown.append("**提交信息**: (无)\n\n");
|
||||
}
|
||||
|
||||
markdown.append("---\n\n");
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
try (BufferedWriter writer = new BufferedWriter(new FileWriter(fileName))) {
|
||||
writer.write(markdown.toString());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 项目信息类
|
||||
*/
|
||||
private static class Project {
|
||||
private String name;
|
||||
private String url;
|
||||
|
||||
public Project(String name, String url) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
}
|
||||
98
src/main/java/com/svnlog/SVNLogFetcher.java
Normal file
98
src/main/java/com/svnlog/SVNLogFetcher.java
Normal file
@@ -0,0 +1,98 @@
|
||||
package com.svnlog;
|
||||
|
||||
import org.tmatesoft.svn.core.*;
|
||||
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
|
||||
import org.tmatesoft.svn.core.internal.io.svn.SVNRepositoryFactoryImpl;
|
||||
import org.tmatesoft.svn.core.io.SVNRepository;
|
||||
import org.tmatesoft.svn.core.io.SVNRepositoryFactory;
|
||||
import org.tmatesoft.svn.core.wc.SVNWCUtil;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
|
||||
public class SVNLogFetcher {
|
||||
private String url;
|
||||
private String username;
|
||||
private String password;
|
||||
private SVNRepository repository;
|
||||
private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
public SVNLogFetcher(String url, String username, String password) throws SVNException {
|
||||
this.url = url;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
|
||||
SVNRepositoryFactoryImpl.setup();
|
||||
this.repository = SVNRepositoryFactory.create(SVNURL.parseURIEncoded(url));
|
||||
|
||||
ISVNAuthenticationManager authManager = SVNWCUtil.createDefaultAuthenticationManager(username, password.toCharArray());
|
||||
repository.setAuthenticationManager(authManager);
|
||||
}
|
||||
|
||||
public List<LogEntry> fetchLogs(long startRevision, long endRevision) throws SVNException {
|
||||
return fetchLogs(startRevision, endRevision, null);
|
||||
}
|
||||
|
||||
public List<LogEntry> fetchLogs(long startRevision, long endRevision, String filterUser) throws SVNException {
|
||||
List<LogEntry> entries = new ArrayList<>();
|
||||
|
||||
if (startRevision < 0) {
|
||||
startRevision = repository.getLatestRevision();
|
||||
}
|
||||
|
||||
if (endRevision < 0) {
|
||||
endRevision = repository.getLatestRevision();
|
||||
}
|
||||
|
||||
if (startRevision > endRevision) {
|
||||
long temp = startRevision;
|
||||
startRevision = endRevision;
|
||||
endRevision = temp;
|
||||
}
|
||||
|
||||
Collection<SVNLogEntry> logEntries = repository.log(new String[]{""}, null, startRevision, endRevision, true, true);
|
||||
|
||||
for (SVNLogEntry logEntry : logEntries) {
|
||||
String author = logEntry.getAuthor();
|
||||
|
||||
// 如果设置了用户名过滤器,则跳过不匹配的记录(包含匹配,不区分大小写)
|
||||
if (filterUser != null && !filterUser.isEmpty() && (author == null || !author.toLowerCase().contains(filterUser.toLowerCase()))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
LogEntry entry = new LogEntry();
|
||||
entry.setRevision(logEntry.getRevision());
|
||||
entry.setAuthor(author != null ? author : "(无作者)");
|
||||
entry.setDate(logEntry.getDate());
|
||||
entry.setMessage(logEntry.getMessage() != null ? logEntry.getMessage().trim() : "");
|
||||
|
||||
// 获取变更的文件路径
|
||||
if (logEntry.getChangedPaths() != null) {
|
||||
List<String> paths = new ArrayList<>();
|
||||
for (Map.Entry<String, SVNLogEntryPath> pathEntry : logEntry.getChangedPaths().entrySet()) {
|
||||
paths.add(pathEntry.getKey());
|
||||
}
|
||||
entry.setChangedPaths(paths.toArray(new String[0]));
|
||||
}
|
||||
|
||||
entries.add(entry);
|
||||
}
|
||||
|
||||
// 按版本号降序排序
|
||||
entries.sort((e1, e2) -> Long.compare(e2.getRevision(), e1.getRevision()));
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
public long getLatestRevision() throws SVNException {
|
||||
return repository.getLatestRevision();
|
||||
}
|
||||
|
||||
public String formatDate(Date date) {
|
||||
return dateFormat.format(date);
|
||||
}
|
||||
|
||||
public void testConnection() throws SVNException {
|
||||
repository.testConnection();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user