Files
svn-log-tool/src/main/java/com/svnlog/DeepSeekLogProcessor.java
liumangmang a6817fd9bf feat(core): 添加SVN日志查询工具和DeepSeek AI处理功能
- 实现SVN日志查询工具,支持版本范围和用户过滤
- 添加DeepSeek API集成,用于AI分析日志内容
- 创建Excel生成器,输出工作量统计报表
- 添加日志实体类和项目配置管理功能
- 集成POI库支持Excel文件操作
- 实现Markdown格式日志导出功能
2026-02-05 09:11:17 +08:00

588 lines
23 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
}
}
}