You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

570 lines
23 KiB

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<GitAnalysisData> 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<GiteaRepository> 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<GiteaRepository> 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<List<GiteaRepository>> findActiveRepositories(List<GiteaRepository> allRepos,
ZonedDateTime since,
ZonedDateTime until,
String taskId) {
return CompletableFuture.supplyAsync(() -> {
List<GiteaRepository> activeRepos = Collections.synchronizedList(new ArrayList<>());
List<CompletableFuture<Boolean>> futures = new ArrayList<>();
AtomicInteger checked = new AtomicInteger(0);
final int totalRepos = allRepos.size();
for (GiteaRepository repo : allRepos) {
final GiteaRepository currentRepo = repo;
CompletableFuture<Boolean> 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<Void> 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<DetailedAnalysisResult> analyzeActiveRepositories(List<GiteaRepository> activeRepos,
ZonedDateTime sinceTime,
ZonedDateTime untilTime,
String taskId) {
return CompletableFuture.supplyAsync(() -> {
Map<String, DeveloperData> devDataMap = new ConcurrentHashMap<>();
Map<String, RepoData> repoDataMap = new ConcurrentHashMap<>();
Map<DayOfWeek, Integer> dayStats = new ConcurrentHashMap<>();
Map<Integer, Integer> hourStats = new ConcurrentHashMap<>();
Map<String, Integer> fileTypeStats = new ConcurrentHashMap<>();
AtomicInteger totalCommits = new AtomicInteger(0);
AtomicInteger processed = new AtomicInteger(0);
List<CompletableFuture<Void>> 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<Void> 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<String, DeveloperData> devDataMap,
Map<String, RepoData> repoDataMap,
Map<DayOfWeek, Integer> dayStats,
Map<Integer, Integer> hourStats,
Map<String, Integer> fileTypeStats,
AtomicInteger totalCommits) throws Exception {
if (Thread.currentThread().isInterrupted()) {
log.debug("任务被中断,跳过仓库分析: {}", repo.getFullPath());
return;
}
String repoFullName = repo.getFullPath();
List<GiteaCommit> 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<GiteaCommit> commits = getCommitsInRange(repoFullName, since, until);
return !commits.isEmpty();
}
private List<GiteaCommit> 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<GiteaCommit> recentCommits = fetchWithPagination(baseUrl, GiteaCommit.class, 8);
// 在代码层面再次过滤到精确时间范围
List<GiteaCommit> 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<GiteaRepository> getAllUserRepositories() throws Exception {
String baseUrl = giteaBaseUrl + "/api/v1/user/repos?limit=50";
return fetchWithPagination(baseUrl, GiteaRepository.class, 10);
}
private <T> List<T> fetchWithPagination(String baseUrl, Class<T> clazz, int timeoutSeconds) throws Exception {
List<T> 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<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() != 200) {
log.warn("API请求失败: {} - {}", response.statusCode(), response.body());
break;
}
List<T> 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<DeveloperData> devList = new ArrayList<>(detailResult.getDevDataMap().values());
devList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount));
List<DeveloperRank> 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<RepoData> repoList = new ArrayList<>(detailResult.getRepoDataMap().values());
repoList.sort((a, b) -> Integer.compare(b.commitCount, a.commitCount));
List<RepoRank> 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<DayStats> 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<Map.Entry<String, Integer>> fileList = new ArrayList<>(detailResult.getFileTypeStats().entrySet());
fileList.sort((a, b) -> Integer.compare(b.getValue(), a.getValue()));
List<FileTypeStats> fileTypeStatsList = new ArrayList<>();
for (Map.Entry<String, Integer> 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<String> repos = new HashSet<>();
DeveloperData(String name) {
this.name = name;
}
}
private static class RepoData {
String repoName;
String displayName;
int commitCount = 0;
Set<String> developers = new HashSet<>();
}
// ==================== 详细分析结果类 ====================
private static class DetailedAnalysisResult {
private final Map<String, DeveloperData> devDataMap;
private final Map<String, RepoData> repoDataMap;
private final Map<DayOfWeek, Integer> dayStats;
private final Map<Integer, Integer> hourStats;
private final Map<String, Integer> fileTypeStats;
private final int totalCommits;
public DetailedAnalysisResult(Map<String, DeveloperData> devDataMap, Map<String, RepoData> repoDataMap,
Map<DayOfWeek, Integer> dayStats, Map<Integer, Integer> hourStats,
Map<String, Integer> fileTypeStats, int totalCommits) {
this.devDataMap = devDataMap;
this.repoDataMap = repoDataMap;
this.dayStats = dayStats;
this.hourStats = hourStats;
this.fileTypeStats = fileTypeStats;
this.totalCommits = totalCommits;
}
public Map<String, DeveloperData> getDevDataMap() { return devDataMap; }
public Map<String, RepoData> getRepoDataMap() { return repoDataMap; }
public Map<DayOfWeek, Integer> getDayStats() { return dayStats; }
public Map<Integer, Integer> getHourStats() { return hourStats; }
public Map<String, Integer> getFileTypeStats() { return fileTypeStats; }
public int getTotalCommits() { return totalCommits; }
}
}