package com.codechecker; import com.codechecker.analyzer.ClassChangeAnalyzer; import com.codechecker.analyzer.DtoNestIndex; import com.codechecker.analyzer.EndpointIndexBuilder; import com.codechecker.api.analyzer.ApiChangeAnalyzer; import com.codechecker.api.analyzer.DtoImpactedApiAnalyzer; import com.codechecker.api.model.ApiChangeKind; import com.codechecker.api.model.EndpointChangeReport; import com.codechecker.api.notify.ApiChangeNotifier; import com.codechecker.api.scanner.ApiFileChangeScanner; import com.codechecker.config.AppConfig; import com.codechecker.git.GitChangeScanner; import com.codechecker.model.ApiEndpoint; import com.codechecker.model.ClassChangeReport; import com.codechecker.notify.OverlapNotificationFilter; import com.codechecker.notify.WeComNotifier; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; import java.nio.file.Path; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; /** * CLI 入口:加载配置 → 扫描 git 变更 → 分析影响 → 输出/发送企微通知。 */ @Command(name = "code-checker", mixinStandardHelpOptions = true, description = "检测类变更与 API 变更并发送企业微信通知") public class CodeCheckMain implements Callable { @Option(names = "--config", required = true, description = "配置文件路径") private Path config; @Option(names = "--repo-root", required = true, description = "仓库根目录") private Path repoRoot; @Option(names = "--old-sha", required = true, description = "旧提交 SHA") private String oldSha; @Option(names = "--new-sha", required = true, description = "新提交 SHA") private String newSha; @Option(names = "--modifier", required = true, description = "修改人") private String modifier; @Option(names = "--modify-time", required = true, description = "修改时间") private String modifyTime; /** 程序入口 */ public static void main(String[] args) { int exitCode = new CommandLine(new CodeCheckMain()).execute(args); System.exit(exitCode); } /** 主流程:类变更与 API 变更检测,支持 Dto 跟进与重叠通知策略 */ @Override public Integer call() throws Exception { AppConfig appConfig = AppConfig.load(config.toAbsolutePath()); if (!appConfig.isMasterEnabled()) { System.out.println("变更检测已全部关闭(checker.enabled=false)"); return 0; } GitChangeScanner gitScanner = new GitChangeScanner(repoRoot.toAbsolutePath()); DtoNestIndex nestIndex = DtoNestIndex.build(repoRoot.toAbsolutePath(), appConfig); List classReports = List.of(); List apiReports = List.of(); if (appConfig.isClassCheckEnabled()) { classReports = analyzeClassChanges(appConfig, gitScanner, nestIndex); } else { System.out.println("类变更检测已关闭(class_check.enabled=false)"); } if (appConfig.isApiCheckEnabled()) { apiReports = analyzeApiChanges(appConfig, gitScanner, classReports, nestIndex); } else { System.out.println("API 变更检测已关闭(api_check.enabled=false)"); } OverlapNotificationFilter.FilterResult filtered = OverlapNotificationFilter.apply( classReports, apiReports, appConfig.getDtoOverlapMode(), nestIndex); int totalSent = sendClassNotifications(appConfig, filtered.classReports()) + sendApiNotifications(appConfig, filtered.apiReports()); if (totalSent == 0 && appConfig.isOnlyOnChange()) { System.out.println("无变更,静默退出"); } return 0; } private List analyzeClassChanges(AppConfig appConfig, GitChangeScanner gitScanner, DtoNestIndex nestIndex) throws Exception { System.out.println("=== 类变更检测 ==="); EndpointIndexBuilder indexBuilder = new EndpointIndexBuilder(); Map endpointIndex = indexBuilder.buildIndex(repoRoot.toAbsolutePath(), appConfig); System.out.println("已索引接口数量: " + endpointIndex.size()); ClassChangeAnalyzer analyzer = new ClassChangeAnalyzer(gitScanner); List reports = analyzer.analyze( repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex, nestIndex); System.out.println("检测到需通知的类变更数量: " + reports.size()); return reports; } private List analyzeApiChanges(AppConfig appConfig, GitChangeScanner gitScanner, List classReports, DtoNestIndex nestIndex) throws Exception { System.out.println("=== API 变更检测 ==="); ApiFileChangeScanner fileScanner = new ApiFileChangeScanner(gitScanner); Set changedApiFiles = new LinkedHashSet<>(fileScanner.scanChangedFiles( repoRoot.toAbsolutePath(), appConfig.getAllApiScanDirs(), oldSha, newSha)); ApiChangeAnalyzer analyzer = new ApiChangeAnalyzer(gitScanner); List reports = new ArrayList<>(); if (!changedApiFiles.isEmpty()) { reports.addAll(analyzer.analyze(repoRoot.toAbsolutePath(), appConfig, oldSha, newSha)); } if (appConfig.isDtoApiFollowUpEnabled() && !classReports.isEmpty()) { DtoImpactedApiAnalyzer dtoAnalyzer = new DtoImpactedApiAnalyzer(gitScanner); List followUpReports = dtoAnalyzer.analyze( repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, classReports, changedApiFiles, nestIndex); if (!followUpReports.isEmpty()) { System.out.println("Dto 跟进检测到 API 参数变更数量: " + followUpReports.size()); reports.addAll(followUpReports); } } reports = dedupeApiReports(reports); System.out.println("检测到需通知的 API 变更数量: " + reports.size()); return reports; } private List dedupeApiReports(List reports) { Map merged = new LinkedHashMap<>(); for (EndpointChangeReport report : reports) { String key = report.getChangeKind() + "|" + report.getHttpMethod() + "|" + report.getUri(); EndpointChangeReport existing = merged.get(key); if (existing == null) { merged.put(key, report); continue; } if (report.getChangeKind() == ApiChangeKind.PARAM_CHANGED && existing.getChangeKind() == ApiChangeKind.PARAM_CHANGED) { report.getParameterChanges().forEach(existing::addParameterChange); } } return new ArrayList<>(merged.values()); } private int sendClassNotifications(AppConfig appConfig, List reports) { if (reports.isEmpty()) { return 0; } WeComNotifier notifier = new WeComNotifier(); if (appConfig.isWecomEnabled()) { return notifier.sendAll(appConfig.getWecomWebhookUrl(), reports, modifier, modifyTime); } notifier.logAll(reports, modifier, modifyTime); return reports.size(); } private int sendApiNotifications(AppConfig appConfig, List reports) { if (reports.isEmpty()) { return 0; } ApiChangeNotifier notifier = new ApiChangeNotifier(); return notifier.sendAll(appConfig.getWecomWebhookUrl(), reports, modifier, modifyTime, appConfig.isWecomEnabled()); } }