package com.chenhai.chenhaiai.service; import com.chenhai.chenhaiai.entity.git.*; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import jakarta.annotation.PostConstruct; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Service; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.time.DayOfWeek; import java.time.Duration; import java.time.LocalDateTime; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; @Slf4j @Service public class GiteaAnalysisService { @Value("${gitea.url:http://192.168.1.224:3000}") private String giteaBaseUrl; @Value("${gitea.token:a9f1c8d3d6fefd73956604f496457faaa3672f89}") private String accessToken; @Value("${gitea.analysis.max-active-repos:100}") private int maxActiveRepos; @Autowired private ThreadPoolTaskExecutor giteaTaskExecutor; @Autowired private TaskExecutor taskExecutor; private HttpClient httpClient; private ObjectMapper objectMapper; @PostConstruct public void init() { log.info("初始化Gitea分析服务..."); httpClient = HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(10)) .build(); objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); log.info("Gitea分析服务初始化完成"); } /** * 异步执行Git分析,直接返回结构化数据 * @param since 开始时间 (ISO格式: 2025-12-01T00:00:00+08:00) * @param until 结束时间 (ISO格式: 2025-12-31T23:59:59+08:00) * @return 结构化分析数据 */ @Async("giteaTaskExecutor") public CompletableFuture analyzeGitDataAsync(String since, String until) { String taskId = UUID.randomUUID().toString().substring(0, 8); long startTime = System.currentTimeMillis(); log.info("开始Git分析任务[{}]: {} 至 {}", taskId, since, until); return CompletableFuture.supplyAsync(() -> { try { GitAnalysisData analysisData = performCompleteAnalysis(since, until, taskId); long cost = System.currentTimeMillis() - startTime; log.info("Git分析任务[{}]完成,耗时: {}ms", taskId, cost); return analysisData; } catch (Exception e) { log.error("Git分析任务[{}]失败: {}", taskId, e.getMessage(), e); throw new RuntimeException("Git分析失败: " + e.getMessage(), e); } }, giteaTaskExecutor); } /** * 执行完整分析,返回结构化数据 */ private GitAnalysisData performCompleteAnalysis(String since, String until, String taskId) throws Exception { long startTime = System.currentTimeMillis(); // 1. 获取所有仓库 log.debug("任务[{}] 获取仓库列表...", taskId); List allRepos = getAllUserRepositories(); int totalRepos = allRepos.size(); if (totalRepos == 0) { // 返回空的基础数据 return buildEmptyGitAnalysisData("无仓库数据"); } log.info("任务[{}] 发现仓库: {} 个", taskId, totalRepos); // 2. 解析时间范围 ZonedDateTime sinceTime = ZonedDateTime.parse(since); ZonedDateTime untilTime = ZonedDateTime.parse(until); // 3. 快速筛选活跃仓库 log.debug("任务[{}] 快速筛选活跃仓库...", taskId); List activeRepos = findActiveRepositories(allRepos, sinceTime, untilTime, taskId) .get(20, TimeUnit.SECONDS); int activeRepoCount = activeRepos.size(); log.info("任务[{}] 活跃仓库: {} 个", taskId, activeRepoCount); if (activeRepoCount == 0) { // 返回简单结果(无活跃仓库) return buildSimpleGitAnalysisData(since, until, totalRepos, 0, 0, startTime); } // 限制活跃仓库数量 if (activeRepoCount > maxActiveRepos) { log.warn("任务[{}] 活跃仓库过多({}个),采样分析前{}个", taskId, activeRepoCount, maxActiveRepos); activeRepos = activeRepos.subList(0, maxActiveRepos); activeRepoCount = maxActiveRepos; } // 4. 详细分析活跃仓库 log.debug("任务[{}] 详细分析活跃仓库...", taskId); DetailedAnalysisResult detailResult = analyzeActiveRepositories(activeRepos, sinceTime, untilTime, taskId) .get(20, TimeUnit.SECONDS); // 5. 构建结构化数据 long analysisTime = System.currentTimeMillis() - startTime; return buildGitAnalysisData(since, until, totalRepos, activeRepoCount, detailResult, analysisTime); } /** * 快速筛选活跃仓库 */ private CompletableFuture> findActiveRepositories(List allRepos, ZonedDateTime since, ZonedDateTime until, String taskId) { return CompletableFuture.supplyAsync(() -> { List activeRepos = Collections.synchronizedList(new ArrayList<>()); List> futures = new ArrayList<>(); AtomicInteger checked = new AtomicInteger(0); final int totalRepos = allRepos.size(); for (GiteaRepository repo : allRepos) { final GiteaRepository currentRepo = repo; CompletableFuture future = CompletableFuture.supplyAsync(() -> { try { return hasCommitsInRange(currentRepo.getFullPath(), since, until); } catch (Exception e) { log.debug("任务[{}] 仓库 {} 快速检查失败: {}", taskId, currentRepo.getFullPath(), e.getMessage()); return false; } }, taskExecutor); future.thenAccept(hasCommits -> { if (hasCommits) { activeRepos.add(currentRepo); } int done = checked.incrementAndGet(); if (done % 20 == 0 || done == totalRepos) { log.debug("任务[{}] 快速检查进度: {}/{} | 活跃: {}", taskId, done, totalRepos, activeRepos.size()); } }); futures.add(future); } // 等待所有检查完成 CompletableFuture allChecks = CompletableFuture.allOf( futures.toArray(new CompletableFuture[0])); try { allChecks.get(10, TimeUnit.SECONDS); } catch (Exception e) { log.warn("任务[{}] 部分仓库快速检查未完成: {}", taskId, e.getMessage()); } return activeRepos; }, taskExecutor); } /** * 详细分析活跃仓库 */ private CompletableFuture analyzeActiveRepositories(List activeRepos, ZonedDateTime sinceTime, ZonedDateTime untilTime, String taskId) { return CompletableFuture.supplyAsync(() -> { Map devDataMap = new ConcurrentHashMap<>(); Map repoDataMap = new ConcurrentHashMap<>(); Map dayStats = new ConcurrentHashMap<>(); Map hourStats = new ConcurrentHashMap<>(); Map fileTypeStats = new ConcurrentHashMap<>(); AtomicInteger totalCommits = new AtomicInteger(0); AtomicInteger processed = new AtomicInteger(0); List> futures = activeRepos.stream() .map(repo -> CompletableFuture.runAsync(() -> { try { analyzeSingleRepository(repo, sinceTime, untilTime, devDataMap, repoDataMap, dayStats, hourStats, fileTypeStats, totalCommits); } catch (Exception e) { log.debug("任务[{}] 仓库 {} 详细分析失败: {}", taskId, repo.getFullPath(), e.getMessage()); } finally { int done = processed.incrementAndGet(); if (done % 10 == 0 || done == activeRepos.size()) { log.debug("任务[{}] 详细分析进度: {}/{} | 提交: {}", taskId, done, activeRepos.size(), totalCommits.get()); } } }, taskExecutor)) .collect(Collectors.toList()); // 等待所有分析完成 CompletableFuture allFutures = CompletableFuture.allOf( futures.toArray(new CompletableFuture[0])); try { allFutures.get(15, TimeUnit.SECONDS); } catch (Exception e) { log.warn("任务[{}] 部分仓库详细分析未完成: {}", taskId, e.getMessage()); } return new DetailedAnalysisResult(devDataMap, repoDataMap, dayStats, hourStats, fileTypeStats, totalCommits.get()); }, taskExecutor); } /** * 分析单个仓库详情 */ private void analyzeSingleRepository(GiteaRepository repo, ZonedDateTime sinceTime, ZonedDateTime untilTime, Map devDataMap, Map repoDataMap, Map dayStats, Map hourStats, Map fileTypeStats, AtomicInteger totalCommits) throws Exception { if (Thread.currentThread().isInterrupted()) { log.debug("任务被中断,跳过仓库分析: {}", repo.getFullPath()); return; } String repoFullName = repo.getFullPath(); List commits = getCommitsInRange(repoFullName, sinceTime, untilTime); if (!commits.isEmpty()) { RepoData repoData = new RepoData(); repoData.repoName = repoFullName; repoData.displayName = repo.getRepoName(); for (GiteaCommit commit : commits) { if (Thread.currentThread().isInterrupted()) { log.debug("处理提交时被中断"); return; } totalCommits.incrementAndGet(); String author = getAuthorName(commit); // 更新开发者数据 DeveloperData devData = devDataMap.computeIfAbsent(author, k -> new DeveloperData(author)); devData.commitCount++; devData.repos.add(repoFullName); // 更新仓库数据 repoData.commitCount++; repoData.developers.add(author); // 时间统计 ZonedDateTime commitTime = commit.getCommitTime(); if (commitTime != null) { DayOfWeek day = commitTime.getDayOfWeek(); int hour = commitTime.getHour(); dayStats.merge(day, 1, Integer::sum); hourStats.merge(hour, 1, Integer::sum); } // 文件类型统计 if (commit.getFiles() != null) { for (GiteaCommit.ChangedFile file : commit.getFiles()) { String fileType = file.getFileType(); fileTypeStats.merge(fileType, 1, Integer::sum); } } } repoDataMap.put(repoFullName, repoData); } } // ==================== Gitea API调用方法 ==================== private boolean hasCommitsInRange(String repoFullName, ZonedDateTime since, ZonedDateTime until) throws Exception { // 直接调用 getCommitsInRange 检查是否有提交 List commits = getCommitsInRange(repoFullName, since, until); return !commits.isEmpty(); } private List getCommitsInRange(String repoFullName, ZonedDateTime since, ZonedDateTime until) throws Exception { // 先获取最近3个月的提交(减少数据量) LocalDateTime threeMonthsAgo = LocalDateTime.now().minusMonths(3); ZonedDateTime recentSince = threeMonthsAgo.atZone(since.getZone()); String sinceStr = recentSince.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); String untilStr = until.format(DateTimeFormatter.ISO_OFFSET_DATE_TIME); String baseUrl = String.format("%s/api/v1/repos/%s/commits?since=%s&until=%s", giteaBaseUrl, repoFullName, sinceStr, untilStr); List recentCommits = fetchWithPagination(baseUrl, GiteaCommit.class, 8); // 在代码层面再次过滤到精确时间范围 List filteredCommits = new ArrayList<>(); for (GiteaCommit commit : recentCommits) { ZonedDateTime commitTime = commit.getCommitTime(); if (commitTime != null && !commitTime.isBefore(since) && !commitTime.isAfter(until)) { filteredCommits.add(commit); } } return filteredCommits; } private List getAllUserRepositories() throws Exception { String baseUrl = giteaBaseUrl + "/api/v1/user/repos?limit=50"; return fetchWithPagination(baseUrl, GiteaRepository.class, 10); } private List fetchWithPagination(String baseUrl, Class clazz, int timeoutSeconds) throws Exception { List results = new ArrayList<>(); int page = 1; int maxPages = 10; while (page <= maxPages) { if (Thread.currentThread().isInterrupted()) { log.debug("分页获取被中断"); break; } String pageUrl = baseUrl + (baseUrl.contains("?") ? "&" : "?") + "page=" + page; HttpRequest request = HttpRequest.newBuilder() .uri(java.net.URI.create(pageUrl)) .header("Authorization", "token " + accessToken) .timeout(Duration.ofSeconds(timeoutSeconds)) .GET() .build(); HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); if (response.statusCode() != 200) { log.warn("API请求失败: {} - {}", response.statusCode(), response.body()); break; } List pageResults = objectMapper.readValue( response.body(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz)); if (pageResults.isEmpty()) { break; } results.addAll(pageResults); if (pageResults.size() < 50) { break; } page++; } return results; } private String getAuthorName(GiteaCommit commit) { if (commit.getCommit() != null && commit.getCommit().getAuthor() != null && commit.getCommit().getAuthor().getName() != null) { return commit.getCommit().getAuthor().getName(); } return "未知作者"; } // ==================== GitAnalysisData构建方法 ==================== /** * 构建完整的GitAnalysisData */ private GitAnalysisData buildGitAnalysisData(String since, String until, int totalRepos, int activeRepos, DetailedAnalysisResult detailResult, long analysisTime) { GitAnalysisData data = new GitAnalysisData(); // 基础信息 data.setBasicInfo(new BasicInfo( since + " 至 " + until, totalRepos, activeRepos, detailResult.getDevDataMap().size(), detailResult.getTotalCommits(), analysisTime, "快速筛选 + 详细分析" )); // 开发者排行榜 if (!detailResult.getDevDataMap().isEmpty()) { List devList = new ArrayList<>(detailResult.getDevDataMap().values()); devList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); List developerRanks = new ArrayList<>(); int rank = 1; for (DeveloperData dev : devList) { if (rank > 10) break; developerRanks.add(new DeveloperRank(rank++, dev.name, dev.commitCount, dev.repos.size())); } data.setDeveloperRanks(developerRanks); } // 仓库排行榜 if (!detailResult.getRepoDataMap().isEmpty()) { List repoList = new ArrayList<>(detailResult.getRepoDataMap().values()); repoList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount)); List repoRanks = new ArrayList<>(); int rank = 1; for (RepoData repo : repoList) { if (rank > 10) break; repoRanks.add(new RepoRank(rank++, repo.repoName, repo.displayName, repo.commitCount, repo.developers.size())); } data.setRepoRanks(repoRanks); } // 时间分布(按星期) if (!detailResult.getDayStats().isEmpty()) { String[] dayNames = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"}; DayOfWeek[] days = DayOfWeek.values(); List dayStatsList = new ArrayList<>(); for (int i = 0; i < 7; i++) { int count = detailResult.getDayStats().getOrDefault(days[i], 0); dayStatsList.add(new DayStats(dayNames[i], count)); } data.setDayStats(dayStatsList); } // 文件类型统计 if (!detailResult.getFileTypeStats().isEmpty()) { List> fileList = new ArrayList<>(detailResult.getFileTypeStats().entrySet()); fileList.sort((a, b) -> Integer.compare(b.getValue(), a.getValue())); List fileTypeStatsList = new ArrayList<>(); for (Map.Entry entry : fileList) { fileTypeStatsList.add(new FileTypeStats(entry.getKey(), entry.getValue())); } data.setFileTypeStats(fileTypeStatsList); } // 生成时间 data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); return data; } /** * 构建空的GitAnalysisData(无仓库时) */ private GitAnalysisData buildEmptyGitAnalysisData(String message) { GitAnalysisData data = new GitAnalysisData(); data.setBasicInfo(new BasicInfo( "无时间范围", 0, 0, 0, 0, 0, "无仓库数据" )); data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); return data; } /** * 构建简单的GitAnalysisData(无活跃仓库时) */ private GitAnalysisData buildSimpleGitAnalysisData(String since, String until, int totalRepos, int activeRepos, int totalDevs, long startTime) { long analysisTime = System.currentTimeMillis() - startTime; GitAnalysisData data = new GitAnalysisData(); data.setBasicInfo(new BasicInfo( since + " 至 " + until, totalRepos, activeRepos, totalDevs, 0, analysisTime, "快速筛选" )); data.setGeneratedTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); return data; } // ==================== 内部数据类 ==================== private static class DeveloperData { String name; int commitCount = 0; Set repos = new HashSet<>(); DeveloperData(String name) { this.name = name; } } private static class RepoData { String repoName; String displayName; int commitCount = 0; Set developers = new HashSet<>(); } // ==================== 详细分析结果类 ==================== private static class DetailedAnalysisResult { private final Map devDataMap; private final Map repoDataMap; private final Map dayStats; private final Map hourStats; private final Map fileTypeStats; private final int totalCommits; public DetailedAnalysisResult(Map devDataMap, Map repoDataMap, Map dayStats, Map hourStats, Map fileTypeStats, int totalCommits) { this.devDataMap = devDataMap; this.repoDataMap = repoDataMap; this.dayStats = dayStats; this.hourStats = hourStats; this.fileTypeStats = fileTypeStats; this.totalCommits = totalCommits; } public Map getDevDataMap() { return devDataMap; } public Map getRepoDataMap() { return repoDataMap; } public Map getDayStats() { return dayStats; } public Map getHourStats() { return hourStats; } public Map getFileTypeStats() { return fileTypeStats; } public int getTotalCommits() { return totalCommits; } } }