From a6817fd9bfbb0ca48cbd4354a693db34d089c5b3 Mon Sep 17 00:00:00 2001 From: liumangmang Date: Thu, 5 Feb 2026 09:11:17 +0800 Subject: [PATCH] =?UTF-8?q?feat(core):=20=E6=B7=BB=E5=8A=A0SVN=E6=97=A5?= =?UTF-8?q?=E5=BF=97=E6=9F=A5=E8=AF=A2=E5=B7=A5=E5=85=B7=E5=92=8CDeepSeek?= =?UTF-8?q?=20AI=E5=A4=84=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 实现SVN日志查询工具,支持版本范围和用户过滤 - 添加DeepSeek API集成,用于AI分析日志内容 - 创建Excel生成器,输出工作量统计报表 - 添加日志实体类和项目配置管理功能 - 集成POI库支持Excel文件操作 - 实现Markdown格式日志导出功能 --- .idea/misc.xml | 2 +- docs/README_DeepSeek.md | 121 ++++ docs/example_log.md | 164 +++++ pom.xml | 109 ++++ scripts/test_multifile_processor.sh | 106 ++++ .../java/com/svnlog/DeepSeekLogProcessor.java | 587 ++++++++++++++++++ src/main/java/com/svnlog/ExcelAnalyzer.java | 84 +++ src/main/java/com/svnlog/LogEntry.java | 71 +++ src/main/java/com/svnlog/Main.java | 215 +++++++ src/main/java/com/svnlog/SVNLogFetcher.java | 98 +++ 10 files changed, 1556 insertions(+), 1 deletion(-) create mode 100644 docs/README_DeepSeek.md create mode 100644 docs/example_log.md create mode 100644 pom.xml create mode 100755 scripts/test_multifile_processor.sh create mode 100644 src/main/java/com/svnlog/DeepSeekLogProcessor.java create mode 100644 src/main/java/com/svnlog/ExcelAnalyzer.java create mode 100644 src/main/java/com/svnlog/LogEntry.java create mode 100644 src/main/java/com/svnlog/Main.java create mode 100644 src/main/java/com/svnlog/SVNLogFetcher.java diff --git a/.idea/misc.xml b/.idea/misc.xml index 3e8d08a..17e4782 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -21,7 +21,7 @@ - + \ No newline at end of file diff --git a/docs/README_DeepSeek.md b/docs/README_DeepSeek.md new file mode 100644 index 0000000..e62b135 --- /dev/null +++ b/docs/README_DeepSeek.md @@ -0,0 +1,121 @@ +# SVN日志工作量统计工具(DeepSeek版) + +## 功能说明 + +这个工具可以根据SVN日志的markdown文件,调用DeepSeek API分析日志内容,并生成符合格式要求的工作量统计Excel文件。 + +## 使用步骤 + +### 1. 准备SVN日志markdown文件 + +使用原有的SVN日志工具生成markdown文件: +```bash +java -jar svn-log-tool-1.0.0-jar-with-dependencies.jar +``` + +按照提示输入SVN仓库地址、账号、密码等信息,生成markdown格式的日志文件。 + +### 2. 运行DeepSeek日志处理工具 + +```bash +java -cp target/svn-log-tool-1.0.0-jar-with-dependencies.jar com.svnlog.DeepSeekLogProcessor +``` + +或者使用Maven运行: +```bash +mvn exec:java -Dexec.mainClass="com.svnlog.DeepSeekLogProcessor" +``` + +### 3. 按照提示输入信息 + +程序会依次提示输入: +- **markdown日志文件路径**:可以直接回车使用当前目录下最新的`svn_log_*.md`文件 +- **DeepSeek API Key**:请提供有效的DeepSeek API Key(也可以直接在代码中修改`API_KEY`常量) +- **输出Excel文件名**:可以直接回车使用默认文件名(格式:`YYYYMM工作量统计.xlsx`) + +### 4. 等待处理完成 + +程序会自动: +1. 读取markdown日志文件 +2. 调用DeepSeek API分析日志内容 +3. 根据分析结果生成Excel文件 + +生成的Excel文件格式与`202512工作量统计_刘靖.xlsx`保持一致。 + +## Excel文件格式说明 + +生成的Excel文件包含以下列: +- 序号 +- 所属班组 +- 技术对接 +- 开发人员 +- 工作周期 +- 开发项目名称 +- 具体工作内容 +- 空列(4个) + +## DeepSeek API配置 + +在`DeepSeekLogProcessor.java`中,可以修改以下配置: + +```java +private static final String DEEPSEEK_API_URL = "https://api.deepseek.com/chat/completions"; +private static final String API_KEY = "YOUR_DEEPSEEK_API_KEY"; // 请替换为实际的API Key +``` + +## 提示词说明 + +工具会向DeepSeek发送以下提示词,要求AI以JSON格式返回工作量统计: + +```json +{ + "team": "所属班组", + "contact": "技术对接人", + "developer": "开发人员", + "period": "工作周期 (例如: 2025年12月)", + "records": [ + { + "sequence": 1, + "project": "项目名称", + "content": "具体工作内容" + } + ] +} +``` + +## 注意事项 + +1. **API Key安全**:请妥善保管您的DeepSeek API Key,不要将其提交到代码仓库中 +2. **网络连接**:需要能够访问DeepSeek API服务器 +3. **日志格式**:markdown文件需要由SVN日志工具生成,包含完整的日志信息 +4. **成本控制**:DeepSeek API可能产生费用,请注意控制使用频率 + +## 故障排查 + +### 编译错误 +```bash +mvn clean package -DskipTests +``` + +### 运行时找不到主类 +确保使用正确的jar文件: +```bash +java -cp target/svn-log-tool-1.0.0-jar-with-dependencies.jar com.svnlog.DeepSeekLogProcessor +``` + +### API调用失败 +- 检查API Key是否正确 +- 检查网络连接是否正常 +- 检查DeepSeek API服务是否可用 + +## 依赖说明 + +项目使用以下主要依赖: +- SVNKit 1.10.11:SVN操作 +- Apache POI 5.2.5:Excel文件读写 +- OkHttp 4.12.0:HTTP客户端 +- Gson 2.10.1:JSON处理 + +## 许可证 + +本工具仅供内部使用。 \ No newline at end of file diff --git a/docs/example_log.md b/docs/example_log.md new file mode 100644 index 0000000..5ec127e --- /dev/null +++ b/docs/example_log.md @@ -0,0 +1,164 @@ +# SVN 日志报告 + +## 查询条件 + +- **SVN地址**: `https://svn.example.com/project` +- **账号**: `testuser` +- **版本范围**: r1000 - r1050 +- **生成时间**: 2025-01-30 10:00:00 + +## 统计信息 + +- **总记录数**: 5 条 + +### 按作者统计 + +| 作者 | 提交次数 | +|------|----------| +| `zhangsan` | 3 | +| `lisi` | 2 | + +## 日志详情 + +### r1050 + +**作者**: `zhangsan` +**时间**: 2025-01-30 09:30:00 +**版本**: r1050 + +**变更文件**: + +``` +/src/main/java/com/example/Service.java +/src/test/java/com/example/ServiceTest.java +``` + +**提交信息**: + +``` +修复用户登录时的空指针异常问题 + +1. 修复用户服务中的空指针检查 +2. 添加单元测试验证修复 +3. 更新相关文档 +``` + +--- + +### r1049 + +**作者**: `zhangsan` +**时间**: 2025-01-29 16:45:00 +**版本**: r1049 + +**变更文件**: + +``` +/src/main/java/com/example/Controller.java +``` + +**提交信息**: + +``` +优化API接口响应速度 + +# 性能优化 +1. 添加数据库查询缓存 +2. 优化SQL查询语句 +3. 减少不必要的对象创建 + +# 测试验证 +- 响应时间从500ms降低到200ms +- 通过所有单元测试 +``` + +--- + +### r1048 + +**作者**: `lisi` +**时间**: 2025-01-29 14:20:00 +**版本**: r1048 + +**变更文件**: + +``` +/src/main/java/com/example/Dao.java +/src/main/resources/mapper/UserMapper.xml +``` + +**提交信息**: + +``` +实现用户数据批量导入功能 + +# 核心功能 +1. 支持Excel文件上传 +2. 数据验证和错误处理 +3. 批量插入数据库 + +# 配置变更 +- 添加文件上传大小限制 +- 配置批量插入批次大小 +``` + +--- + +### r1047 + +**作者**: `zhangsan` +**时间**: 2025-01-28 11:00:00 +**版本**: r1047 + +**变更文件**: + +``` +/src/main/java/com/example/Util.java +/src/main/resources/application.yml +``` + +**提交信息**: + +``` +添加日志记录功能 + +# 新增功能 +1. 集成Log4j2日志框架 +2. 配置日志输出格式 +3. 添加关键操作日志记录 + +# 配置更新 +- 设置日志级别为INFO +- 配置日志文件滚动策略 +``` + +--- + +### r1046 + +**作者**: `lisi` +**时间**: 2025-01-27 15:30:00 +**版本**: r1046 + +**变更文件**: + +``` +/src/main/java/com/example/Model.java +``` + +**提交信息**: + +``` +重构数据模型类 + +# 重构内容 +1. 优化字段命名规范 +2. 添加数据验证注解 +3. 实现序列化接口 + +# 兼容性 +- 保持向后兼容 +- 更新相关测试用例 +``` + +--- \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..863f19f --- /dev/null +++ b/pom.xml @@ -0,0 +1,109 @@ + + + 4.0.0 + + com.svnlog + svn-log-tool + 1.0.0 + jar + + SVN Log Tool + SVN日志查询工具,支持版本范围过滤和用户名过滤,可导出Markdown格式 + + + UTF-8 + 1.8 + 1.8 + + + + + + org.tmatesoft.svnkit + svnkit + 1.10.11 + + + + + org.apache.poi + poi + 5.2.5 + + + org.apache.poi + poi-ooxml + 5.2.5 + + + + + com.squareup.okhttp3 + okhttp + 4.12.0 + + + + + com.google.code.gson + gson + 2.10.1 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 1.8 + 1.8 + UTF-8 + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + com.svnlog.Main + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.3.0 + + + jar-with-dependencies + + + + com.svnlog.Main + + + + + + make-assembly + package + + single + + + + + + + \ No newline at end of file diff --git a/scripts/test_multifile_processor.sh b/scripts/test_multifile_processor.sh new file mode 100755 index 0000000..af76d06 --- /dev/null +++ b/scripts/test_multifile_processor.sh @@ -0,0 +1,106 @@ +#!/bin/bash + +# 测试多文件处理功能 +# 注意:此脚本需要有效的 DeepSeek API Key 才能完成测试 + +cd /home/liumangmang/opencode/日志 + +echo "===========================================" +echo " 测试 DeepSeek 日志分析工具" +echo "===========================================" +echo "" + +# 检查是否有日志文件 +md_files=$(find . -maxdepth 1 -name "svn_log_*.md" | wc -l) + +if [ "$md_files" -eq 0 ]; then + echo "警告: 当前目录没有找到 svn_log_*.md 文件" + echo "请先使用 Main.java 生成日志文件" + echo "" + echo "或者手动创建测试文件..." + # 创建测试文件 + cat > test_project1.md << 'EOF' +# SVN 日志报告 + +## 查询条件 +- **SVN地址**: `https://test.svn.com/project1` +- **账号**: `testuser` +- **版本范围**: r1 - r10 +- **生成时间**: 2026-01-30 + +## 统计信息 +- **总记录数**: 2 条 + +## 日志详情 + +### r10 +**作者**: `liujing@SZNARI` +**时间**: 2026-01-27 10:00:00 +**版本**: r10 + +**提交信息**: +feat: 添加用户登录功能 + +### r9 +**作者**: `liujing@SZNARI` +**时间**: 2026-01-26 15:00:00 +**版本**: r9 + +**提交信息**: +fix: 修复登录页面样式问题 +EOF + + cat > test_project2.md << 'EOF' +# SVN 日志报告 + +## 查询条件 +- **SVN地址**: `https://test.svn.com/project2` +- **账号**: `testuser` +- **版本范围**: r1 - r10 +- **生成时间**: 2026-01-30 + +## 统计信息 +- **总记录数**: 1 条 + +## 日志详情 + +### r8 +**作者**: `liujing@SZNARI` +**时间**: 2026-01-25 14:00:00 +**版本**: r8 + +**提交信息**: +refactor: 优化数据库查询性能 +EOF + + echo "已创建测试文件: test_project1.md, test_project2.md" +fi + +echo "" +echo "当前目录下的日志文件:" +ls -lh svn_log_*.md test_*.md 2>/dev/null || echo " (无文件)" +echo "" + +echo "===========================================" +echo " 程序使用说明" +echo "===========================================" +echo "" +echo "要运行 DeepSeek 日志分析工具,请执行:" +echo "" +echo " cd /home/liumangmang/opencode/日志" +echo " java -jar svn-log-tool/target/svn-log-tool-1.0.0-jar-with-dependencies.jar" +echo "" +echo "然后按提示输入:" +echo " 1. 日志文件所在目录路径(回车使用当前目录)" +echo " 2. 工作周期(例如:2025年12月)" +echo " 3. DeepSeek API Key" +echo " 4. 输出 Excel 文件名(回车使用默认)" +echo "" +echo "程序将自动读取目录中的所有 .md 文件,合并后发送给 DeepSeek API 分析," +echo "并生成包含多项目工作内容的 Excel 文件。" +echo "" +echo "Excel 输出格式(与参考文件一致):" +echo " - 7列:序号、所属班组、技术对接、开发人员、工作周期、开发项目名称、具体工作内容" +echo " - 项目名称用 / 分隔(如:PRS7050/PRS7950)" +echo " - 工作内容用 # 标识不同项目" +echo "" \ No newline at end of file diff --git a/src/main/java/com/svnlog/DeepSeekLogProcessor.java b/src/main/java/com/svnlog/DeepSeekLogProcessor.java new file mode 100644 index 0000000..0046676 --- /dev/null +++ b/src/main/java/com/svnlog/DeepSeekLogProcessor.java @@ -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 java.util.List toList() { + java.util.List list = new ArrayList<>(); + for (int i = 0; i < jsonArray.size(); i++) { + list.add(get(i)); + } + return list; + } + } +} diff --git a/src/main/java/com/svnlog/ExcelAnalyzer.java b/src/main/java/com/svnlog/ExcelAnalyzer.java new file mode 100644 index 0000000..12b39eb --- /dev/null +++ b/src/main/java/com/svnlog/ExcelAnalyzer.java @@ -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 ""; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/svnlog/LogEntry.java b/src/main/java/com/svnlog/LogEntry.java new file mode 100644 index 0000000..58054ad --- /dev/null +++ b/src/main/java/com/svnlog/LogEntry.java @@ -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 + '\'' + + '}'; + } +} diff --git a/src/main/java/com/svnlog/Main.java b/src/main/java/com/svnlog/Main.java new file mode 100644 index 0000000..70a8552 --- /dev/null +++ b/src/main/java/com/svnlog/Main.java @@ -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 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 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; + } + } +} \ No newline at end of file diff --git a/src/main/java/com/svnlog/SVNLogFetcher.java b/src/main/java/com/svnlog/SVNLogFetcher.java new file mode 100644 index 0000000..5e0875a --- /dev/null +++ b/src/main/java/com/svnlog/SVNLogFetcher.java @@ -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 fetchLogs(long startRevision, long endRevision) throws SVNException { + return fetchLogs(startRevision, endRevision, null); + } + + public List fetchLogs(long startRevision, long endRevision, String filterUser) throws SVNException { + List 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 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 paths = new ArrayList<>(); + for (Map.Entry 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(); + } +} \ No newline at end of file