This commit is contained in:
51
.gitea/class-checker/dependency-reduced-pom.xml
Normal file
51
.gitea/class-checker/dependency-reduced-pom.xml
Normal file
@@ -0,0 +1,51 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>com.aicheck</groupId>
|
||||
<artifactId>class-checker</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<build>
|
||||
<finalName>class-checker</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer>
|
||||
<mainClass>com.aicheck.ClassCheckMain</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<properties>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<javaparser.version>3.25.10</javaparser.version>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
</project>
|
||||
82
.gitea/class-checker/pom.xml
Normal file
82
.gitea/class-checker/pom.xml
Normal file
@@ -0,0 +1,82 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.aicheck</groupId>
|
||||
<artifactId>class-checker</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>11</maven.compiler.source>
|
||||
<maven.compiler.target>11</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<javaparser.version>3.25.10</javaparser.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.github.javaparser</groupId>
|
||||
<artifactId>javaparser-symbol-solver-core</artifactId>
|
||||
<version>${javaparser.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.yaml</groupId>
|
||||
<artifactId>snakeyaml</artifactId>
|
||||
<version>2.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>info.picocli</groupId>
|
||||
<artifactId>picocli</artifactId>
|
||||
<version>4.7.6</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>class-checker</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.13.0</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.6.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<transformers>
|
||||
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<mainClass>com.aicheck.ClassCheckMain</mainClass>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,74 @@
|
||||
package com.aicheck;
|
||||
|
||||
import com.aicheck.analyzer.ClassChangeAnalyzer;
|
||||
import com.aicheck.analyzer.EndpointIndexBuilder;
|
||||
import com.aicheck.config.AppConfig;
|
||||
import com.aicheck.git.GitChangeScanner;
|
||||
import com.aicheck.model.ApiEndpoint;
|
||||
import com.aicheck.model.ClassChangeReport;
|
||||
import com.aicheck.notify.WeComNotifier;
|
||||
import picocli.CommandLine;
|
||||
import picocli.CommandLine.Command;
|
||||
import picocli.CommandLine.Option;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
@Command(name = "class-checker", mixinStandardHelpOptions = true,
|
||||
description = "检测 Vo/Dto/Entity/Model 类变更并发送企业微信通知")
|
||||
public class ClassCheckMain implements Callable<Integer> {
|
||||
@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 ClassCheckMain()).execute(args);
|
||||
System.exit(exitCode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer call() throws Exception {
|
||||
AppConfig appConfig = AppConfig.load(config.toAbsolutePath());
|
||||
if (!appConfig.isEnabled()) {
|
||||
System.out.println("类变更检测已关闭(class_check.enabled=false)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
GitChangeScanner gitScanner = new GitChangeScanner(repoRoot.toAbsolutePath());
|
||||
EndpointIndexBuilder indexBuilder = new EndpointIndexBuilder();
|
||||
Map<String, ApiEndpoint> endpointIndex = indexBuilder.buildIndex(repoRoot.toAbsolutePath(), appConfig);
|
||||
System.out.println("已索引接口数量: " + endpointIndex.size());
|
||||
|
||||
ClassChangeAnalyzer analyzer = new ClassChangeAnalyzer(gitScanner);
|
||||
List<ClassChangeReport> reports = analyzer.analyze(
|
||||
repoRoot.toAbsolutePath(), appConfig, oldSha, newSha, endpointIndex);
|
||||
System.out.println("检测到需通知的类变更数量: " + reports.size());
|
||||
|
||||
if (reports.isEmpty()) {
|
||||
if (appConfig.isOnlyOnChange()) {
|
||||
System.out.println("无类变更,静默退出");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
WeComNotifier notifier = new WeComNotifier();
|
||||
notifier.sendAll(appConfig.getWecomWebhookUrl(), reports, modifier, modifyTime);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.aicheck.analyzer;
|
||||
|
||||
import com.aicheck.config.AppConfig;
|
||||
import com.aicheck.git.GitChangeScanner;
|
||||
import com.aicheck.model.ChangedClassFile;
|
||||
import com.aicheck.model.ClassChangeReport;
|
||||
import com.aicheck.model.FieldChange;
|
||||
import com.aicheck.model.FieldInfo;
|
||||
import com.aicheck.parser.ClassFieldParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ClassChangeAnalyzer {
|
||||
private final GitChangeScanner gitScanner;
|
||||
private final ClassFieldParser classFieldParser = new ClassFieldParser();
|
||||
private final FieldDiffEngine fieldDiffEngine = new FieldDiffEngine();
|
||||
private final ImpactAnalyzer impactAnalyzer = new ImpactAnalyzer();
|
||||
|
||||
public ClassChangeAnalyzer(GitChangeScanner gitScanner) {
|
||||
this.gitScanner = gitScanner;
|
||||
}
|
||||
|
||||
public List<ClassChangeReport> analyze(Path repoRoot, AppConfig config, String oldSha, String newSha,
|
||||
Map<String, com.aicheck.model.ApiEndpoint> endpointIndex) throws IOException {
|
||||
List<ChangedClassFile> changedFiles = gitScanner.scanChangedClasses(oldSha, newSha);
|
||||
List<ClassChangeReport> reports = new ArrayList<>();
|
||||
|
||||
for (ChangedClassFile changedFile : changedFiles) {
|
||||
if (changedFile.getStatus() == ChangedClassFile.ChangeStatus.DELETED) {
|
||||
ClassChangeReport report = new ClassChangeReport(
|
||||
changedFile.getClassName(),
|
||||
changedFile.getClassType(),
|
||||
changedFile.getRelativePath(),
|
||||
true,
|
||||
config.isDtoEntityConversionEnabled()
|
||||
);
|
||||
String oldSource = gitScanner.readFileAtCommit(oldSha, changedFile.getRelativePath());
|
||||
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, oldSource);
|
||||
reports.add(report);
|
||||
continue;
|
||||
}
|
||||
|
||||
String oldSource = gitScanner.readFileAtCommit(oldSha, changedFile.getRelativePath());
|
||||
String newSource = gitScanner.readFileAtCommit(newSha, changedFile.getRelativePath());
|
||||
if (newSource == null || newSource.isBlank()) {
|
||||
newSource = gitScanner.readFileAtHead(changedFile.getRelativePath());
|
||||
}
|
||||
List<FieldInfo> oldFields = classFieldParser.parseFields(oldSource, changedFile.getClassName());
|
||||
List<FieldInfo> newFields = classFieldParser.parseFields(newSource, changedFile.getClassName());
|
||||
List<FieldChange> fieldChanges = fieldDiffEngine.diff(oldFields, newFields);
|
||||
if (fieldChanges.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ClassChangeReport report = new ClassChangeReport(
|
||||
changedFile.getClassName(),
|
||||
changedFile.getClassType(),
|
||||
changedFile.getRelativePath(),
|
||||
false,
|
||||
config.isDtoEntityConversionEnabled()
|
||||
);
|
||||
fieldChanges.forEach(report::addFieldChange);
|
||||
impactAnalyzer.analyze(report, endpointIndex, config, repoRoot, newSource);
|
||||
reports.add(report);
|
||||
}
|
||||
return reports;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.aicheck.analyzer;
|
||||
|
||||
import com.aicheck.config.AppConfig;
|
||||
import com.aicheck.model.ApiEndpoint;
|
||||
import com.aicheck.parser.EndpointParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class EndpointIndexBuilder {
|
||||
private final EndpointParser endpointParser = new EndpointParser();
|
||||
|
||||
public Map<String, ApiEndpoint> buildIndex(Path repoRoot, AppConfig config) throws IOException {
|
||||
Map<String, ApiEndpoint> index = new LinkedHashMap<>();
|
||||
for (String dir : config.getControllerScanDirs()) {
|
||||
addEndpoints(index, endpointParser.scanControllerDirectory(repoRoot.resolve(dir), dir));
|
||||
}
|
||||
for (String dir : config.getFeignScanDirs()) {
|
||||
addEndpoints(index, endpointParser.scanFeignDirectory(repoRoot.resolve(dir), dir));
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
private void addEndpoints(Map<String, ApiEndpoint> index, List<ApiEndpoint> endpoints) {
|
||||
for (ApiEndpoint endpoint : endpoints) {
|
||||
index.putIfAbsent(endpoint.endpointKey(), endpoint);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.aicheck.analyzer;
|
||||
|
||||
import com.aicheck.model.FieldChange;
|
||||
import com.aicheck.model.FieldInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class FieldDiffEngine {
|
||||
public List<FieldChange> diff(List<FieldInfo> oldFields, List<FieldInfo> newFields) {
|
||||
Map<String, FieldInfo> oldMap = toMap(oldFields);
|
||||
Map<String, FieldInfo> newMap = toMap(newFields);
|
||||
List<FieldChange> changes = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, FieldInfo> entry : newMap.entrySet()) {
|
||||
FieldInfo oldField = oldMap.get(entry.getKey());
|
||||
if (oldField == null) {
|
||||
changes.add(FieldChange.added(entry.getValue()));
|
||||
} else if (!oldField.equals(entry.getValue())) {
|
||||
changes.add(FieldChange.modified(oldField, entry.getValue(), buildDetail(oldField, entry.getValue())));
|
||||
}
|
||||
}
|
||||
|
||||
for (Map.Entry<String, FieldInfo> entry : oldMap.entrySet()) {
|
||||
if (!newMap.containsKey(entry.getKey())) {
|
||||
changes.add(FieldChange.removed(entry.getValue()));
|
||||
}
|
||||
}
|
||||
return changes;
|
||||
}
|
||||
|
||||
private Map<String, FieldInfo> toMap(List<FieldInfo> fields) {
|
||||
Map<String, FieldInfo> map = new LinkedHashMap<>();
|
||||
for (FieldInfo field : fields) {
|
||||
map.put(field.getName(), field);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
private String buildDetail(FieldInfo oldField, FieldInfo newField) {
|
||||
List<String> parts = new ArrayList<>();
|
||||
if (!oldField.getType().equals(newField.getType())) {
|
||||
parts.add(oldField.getType() + " → " + newField.getType());
|
||||
}
|
||||
if (!oldField.getDescription().equals(newField.getDescription())) {
|
||||
String oldDesc = oldField.getDescription().isBlank() ? "无" : oldField.getDescription();
|
||||
String newDesc = newField.getDescription().isBlank() ? "无" : newField.getDescription();
|
||||
parts.add("说明:" + oldDesc + " → " + newDesc);
|
||||
}
|
||||
return String.join(";", parts);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.aicheck.analyzer;
|
||||
|
||||
import com.aicheck.config.AppConfig;
|
||||
import com.aicheck.model.ApiEndpoint;
|
||||
import com.aicheck.model.ClassChangeReport;
|
||||
import com.aicheck.model.ClassType;
|
||||
import com.aicheck.parser.ConversionParser;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ImpactAnalyzer {
|
||||
private final ConversionParser conversionParser = new ConversionParser();
|
||||
|
||||
public void analyze(ClassChangeReport report, Map<String, ApiEndpoint> endpointIndex,
|
||||
AppConfig config, Path repoRoot, String classSource) throws IOException {
|
||||
if (report.getClassType() == ClassType.ENTITY) {
|
||||
return;
|
||||
}
|
||||
|
||||
String className = report.getClassName();
|
||||
List<ApiEndpoint> inputImpacts = new ArrayList<>();
|
||||
List<ApiEndpoint> frontendImpacts = new ArrayList<>();
|
||||
|
||||
for (ApiEndpoint endpoint : endpointIndex.values()) {
|
||||
if (matchesType(endpoint.getParamTypes(), className)) {
|
||||
inputImpacts.add(endpoint);
|
||||
}
|
||||
if (matchesType(endpoint.getReturnTypes(), className)) {
|
||||
frontendImpacts.add(endpoint);
|
||||
}
|
||||
}
|
||||
|
||||
inputImpacts.forEach(report::addInputImpact);
|
||||
frontendImpacts.forEach(report::addFrontendImpact);
|
||||
|
||||
if (!config.isDtoEntityConversionEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (classSource != null && !classSource.isBlank()) {
|
||||
conversionParser.findConvertTargetsInClass(classSource, className)
|
||||
.forEach(report::addConversionEntity);
|
||||
}
|
||||
|
||||
for (String scanDir : config.getConversionScanDirs()) {
|
||||
conversionParser.findBeanUtilsTargets(repoRoot.resolve(scanDir), className)
|
||||
.forEach(report::addConversionEntity);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean matchesType(Collection<String> types, String className) {
|
||||
return types != null && types.contains(className);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.aicheck.config;
|
||||
|
||||
import org.yaml.snakeyaml.Yaml;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class AppConfig {
|
||||
private boolean enabled = true;
|
||||
private boolean dtoEntityConversionEnabled = true;
|
||||
private List<String> modelDirs = new ArrayList<>();
|
||||
private List<String> controllerScanDirs = new ArrayList<>();
|
||||
private List<String> feignScanDirs = new ArrayList<>();
|
||||
private List<String> conversionScanDirs = new ArrayList<>();
|
||||
private String wecomWebhookUrl = "";
|
||||
private boolean onlyOnChange = true;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public static AppConfig load(Path configPath) throws IOException {
|
||||
Yaml yaml = new Yaml();
|
||||
Map<String, Object> root;
|
||||
try (InputStream in = Files.newInputStream(configPath)) {
|
||||
root = yaml.load(in);
|
||||
}
|
||||
if (root == null) {
|
||||
root = Map.of();
|
||||
}
|
||||
|
||||
AppConfig config = new AppConfig();
|
||||
Map<String, Object> classCheck = mapOrEmpty(root.get("class_check"));
|
||||
config.enabled = boolOrDefault(classCheck.get("enabled"), true);
|
||||
|
||||
Map<String, Object> conversion = mapOrEmpty(classCheck.get("dto_entity_conversion"));
|
||||
config.dtoEntityConversionEnabled = boolOrDefault(conversion.get("enabled"), true);
|
||||
|
||||
config.modelDirs = stringList(classCheck.get("model_dirs"));
|
||||
Map<String, Object> endpointScan = mapOrEmpty(classCheck.get("endpoint_scan"));
|
||||
config.controllerScanDirs = stringList(endpointScan.get("controllers"));
|
||||
config.feignScanDirs = stringList(endpointScan.get("feign_apis"));
|
||||
config.conversionScanDirs = stringList(classCheck.get("conversion_scan"));
|
||||
|
||||
Map<String, Object> wecom = mapOrEmpty(root.get("wecom"));
|
||||
config.wecomWebhookUrl = stringOrEmpty(wecom.get("webhook_url"));
|
||||
|
||||
Map<String, Object> notify = mapOrEmpty(root.get("notify"));
|
||||
config.onlyOnChange = boolOrDefault(notify.get("only_on_change"), true);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static Map<String, Object> mapOrEmpty(Object value) {
|
||||
if (value instanceof Map) {
|
||||
return (Map<String, Object>) value;
|
||||
}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static List<String> stringList(Object value) {
|
||||
if (value instanceof List) {
|
||||
List<?> list = (List<?>) value;
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
if (item != null) {
|
||||
result.add(item.toString());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
private static boolean boolOrDefault(Object value, boolean defaultValue) {
|
||||
if (value instanceof Boolean) {
|
||||
return (Boolean) value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private static String stringOrEmpty(Object value) {
|
||||
return value == null ? "" : value.toString();
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public boolean isDtoEntityConversionEnabled() {
|
||||
return dtoEntityConversionEnabled;
|
||||
}
|
||||
|
||||
public List<String> getModelDirs() {
|
||||
return modelDirs;
|
||||
}
|
||||
|
||||
public List<String> getControllerScanDirs() {
|
||||
return controllerScanDirs;
|
||||
}
|
||||
|
||||
public List<String> getFeignScanDirs() {
|
||||
return feignScanDirs;
|
||||
}
|
||||
|
||||
public List<String> getConversionScanDirs() {
|
||||
return conversionScanDirs;
|
||||
}
|
||||
|
||||
public String getWecomWebhookUrl() {
|
||||
return wecomWebhookUrl;
|
||||
}
|
||||
|
||||
public boolean isOnlyOnChange() {
|
||||
return onlyOnChange;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package com.aicheck.git;
|
||||
|
||||
import com.aicheck.model.ChangedClassFile;
|
||||
import com.aicheck.model.ClassType;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class GitChangeScanner {
|
||||
private final Path repoRoot;
|
||||
|
||||
public GitChangeScanner(Path repoRoot) {
|
||||
this.repoRoot = repoRoot;
|
||||
}
|
||||
|
||||
public List<ChangedClassFile> scanChangedClasses(String oldSha, String newSha) throws IOException {
|
||||
List<String> lines = runGit("diff", "--name-status", oldSha, newSha);
|
||||
List<ChangedClassFile> result = new ArrayList<>();
|
||||
|
||||
for (String line : lines) {
|
||||
if (line.isBlank()) {
|
||||
continue;
|
||||
}
|
||||
String[] parts = line.split("\t");
|
||||
if (parts.length < 2) {
|
||||
continue;
|
||||
}
|
||||
String status = parts[0].trim();
|
||||
String path = normalizePath(parts[parts.length - 1]);
|
||||
if (!path.endsWith(".java")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String className = extractClassName(path);
|
||||
ClassType classType = ClassType.fromClassName(className);
|
||||
if (classType == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status.equals("A")) {
|
||||
continue;
|
||||
}
|
||||
if (status.equals("D")) {
|
||||
result.add(new ChangedClassFile(path, ChangedClassFile.ChangeStatus.DELETED, className, classType));
|
||||
} else if (status.startsWith("M") || status.startsWith("R")) {
|
||||
result.add(new ChangedClassFile(path, ChangedClassFile.ChangeStatus.MODIFIED, className, classType));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public String readFileAtCommit(String commitSha, String relativePath) throws IOException {
|
||||
List<String> lines = runGit("show", commitSha + ":" + relativePath);
|
||||
if (lines.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
if (lines.size() == 1 && lines.get(0).startsWith("fatal:")) {
|
||||
return "";
|
||||
}
|
||||
return String.join("\n", lines);
|
||||
}
|
||||
|
||||
public String readFileAtHead(String relativePath) throws IOException {
|
||||
Path file = repoRoot.resolve(relativePath);
|
||||
if (!Files.exists(file)) {
|
||||
return null;
|
||||
}
|
||||
return Files.readString(file, StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
private List<String> runGit(String... args) throws IOException {
|
||||
String[] command = new String[args.length + 3];
|
||||
command[0] = "git";
|
||||
command[1] = "-C";
|
||||
command[2] = repoRoot.toString();
|
||||
System.arraycopy(args, 0, command, 3, args.length);
|
||||
|
||||
ProcessBuilder builder = new ProcessBuilder(command);
|
||||
builder.redirectErrorStream(true);
|
||||
Process process = builder.start();
|
||||
|
||||
List<String> output = new ArrayList<>();
|
||||
try (BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
|
||||
String line;
|
||||
while ((line = reader.readLine()) != null) {
|
||||
output.add(line);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
int exitCode = process.waitFor();
|
||||
if (exitCode != 0 && !isBenignGitShowFailure(args, output)) {
|
||||
throw new IOException("git 命令失败: " + String.join(" ", command)
|
||||
+ "\n" + String.join("\n", output));
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
throw new IOException("git 命令被中断", e);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private boolean isBenignGitShowFailure(String[] args, List<String> output) {
|
||||
if (args.length > 0 && "show".equals(args[0])) {
|
||||
String joined = String.join("\n", output).toLowerCase(Locale.ROOT);
|
||||
return joined.contains("exists on disk") || joined.contains("bad object")
|
||||
|| joined.contains("path") && joined.contains("does not exist");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private String normalizePath(String path) {
|
||||
return path.replace("\\", "/");
|
||||
}
|
||||
|
||||
private String extractClassName(String path) {
|
||||
String fileName = path.substring(path.lastIndexOf('/') + 1);
|
||||
return fileName.substring(0, fileName.length() - 5);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.aicheck.model;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public class ApiEndpoint {
|
||||
private final String httpMethod;
|
||||
private final String uri;
|
||||
private final String sourceFile;
|
||||
private final Set<String> paramTypes;
|
||||
private final Set<String> returnTypes;
|
||||
|
||||
public ApiEndpoint(String httpMethod, String uri, String sourceFile,
|
||||
Set<String> paramTypes, Set<String> returnTypes) {
|
||||
this.httpMethod = httpMethod;
|
||||
this.uri = uri;
|
||||
this.sourceFile = sourceFile;
|
||||
this.paramTypes = paramTypes == null ? Set.of() : new LinkedHashSet<>(paramTypes);
|
||||
this.returnTypes = returnTypes == null ? Set.of() : new LinkedHashSet<>(returnTypes);
|
||||
}
|
||||
|
||||
public String getHttpMethod() {
|
||||
return httpMethod;
|
||||
}
|
||||
|
||||
public String getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public String getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public Set<String> getParamTypes() {
|
||||
return paramTypes;
|
||||
}
|
||||
|
||||
public Set<String> getReturnTypes() {
|
||||
return returnTypes;
|
||||
}
|
||||
|
||||
public String endpointKey() {
|
||||
return httpMethod + " " + uri;
|
||||
}
|
||||
|
||||
public String displayLine() {
|
||||
return httpMethod + " " + uri;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.aicheck.model;
|
||||
|
||||
public class ChangedClassFile {
|
||||
public enum ChangeStatus {
|
||||
MODIFIED, DELETED
|
||||
}
|
||||
|
||||
private final String relativePath;
|
||||
private final ChangeStatus status;
|
||||
private final String className;
|
||||
private final ClassType classType;
|
||||
|
||||
public ChangedClassFile(String relativePath, ChangeStatus status, String className, ClassType classType) {
|
||||
this.relativePath = relativePath;
|
||||
this.status = status;
|
||||
this.className = className;
|
||||
this.classType = classType;
|
||||
}
|
||||
|
||||
public String getRelativePath() {
|
||||
return relativePath;
|
||||
}
|
||||
|
||||
public ChangeStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return className;
|
||||
}
|
||||
|
||||
public ClassType getClassType() {
|
||||
return classType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.aicheck.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class ClassChangeReport {
|
||||
private final String className;
|
||||
private final ClassType classType;
|
||||
private final String sourceFile;
|
||||
private final boolean deleted;
|
||||
private final List<FieldChange> fieldChanges = new ArrayList<>();
|
||||
private final List<ApiEndpoint> inputImpactEndpoints = new ArrayList<>();
|
||||
private final List<String> conversionEntities = new ArrayList<>();
|
||||
private final List<ApiEndpoint> frontendImpactEndpoints = new ArrayList<>();
|
||||
private final boolean conversionCheckEnabled;
|
||||
|
||||
public ClassChangeReport(String className, ClassType classType, String sourceFile,
|
||||
boolean deleted, boolean conversionCheckEnabled) {
|
||||
this.className = className;
|
||||
this.classType = classType;
|
||||
this.sourceFile = sourceFile;
|
||||
this.deleted = deleted;
|
||||
this.conversionCheckEnabled = conversionCheckEnabled;
|
||||
}
|
||||
|
||||
public String getClassName() {
|
||||
return className;
|
||||
}
|
||||
|
||||
public ClassType getClassType() {
|
||||
return classType;
|
||||
}
|
||||
|
||||
public String getSourceFile() {
|
||||
return sourceFile;
|
||||
}
|
||||
|
||||
public boolean isDeleted() {
|
||||
return deleted;
|
||||
}
|
||||
|
||||
public List<FieldChange> getFieldChanges() {
|
||||
return fieldChanges;
|
||||
}
|
||||
|
||||
public List<ApiEndpoint> getInputImpactEndpoints() {
|
||||
return inputImpactEndpoints;
|
||||
}
|
||||
|
||||
public List<String> getConversionEntities() {
|
||||
return conversionEntities;
|
||||
}
|
||||
|
||||
public List<ApiEndpoint> getFrontendImpactEndpoints() {
|
||||
return frontendImpactEndpoints;
|
||||
}
|
||||
|
||||
public boolean isConversionCheckEnabled() {
|
||||
return conversionCheckEnabled;
|
||||
}
|
||||
|
||||
public void addFieldChange(FieldChange change) {
|
||||
fieldChanges.add(change);
|
||||
}
|
||||
|
||||
public void addInputImpact(ApiEndpoint endpoint) {
|
||||
inputImpactEndpoints.add(endpoint);
|
||||
}
|
||||
|
||||
public void addConversionEntity(String entityName) {
|
||||
if (!conversionEntities.contains(entityName)) {
|
||||
conversionEntities.add(entityName);
|
||||
}
|
||||
}
|
||||
|
||||
public void addFrontendImpact(ApiEndpoint endpoint) {
|
||||
frontendImpactEndpoints.add(endpoint);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.aicheck.model;
|
||||
|
||||
public enum ClassType {
|
||||
DTO("Dto"),
|
||||
VO("Vo"),
|
||||
ENTITY("Entity"),
|
||||
MODEL("Model");
|
||||
|
||||
private final String label;
|
||||
|
||||
ClassType(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public static ClassType fromClassName(String className) {
|
||||
if (className.endsWith("Dto")) {
|
||||
return DTO;
|
||||
}
|
||||
if (className.endsWith("VO")) {
|
||||
return VO;
|
||||
}
|
||||
if (className.endsWith("Vo")) {
|
||||
return VO;
|
||||
}
|
||||
if (className.endsWith("Entity")) {
|
||||
return ENTITY;
|
||||
}
|
||||
if (className.endsWith("Model")) {
|
||||
return MODEL;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean isTargetSuffix(String className) {
|
||||
return fromClassName(className) == this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.aicheck.model;
|
||||
|
||||
public class FieldChange {
|
||||
public enum ChangeKind {
|
||||
ADDED, REMOVED, MODIFIED
|
||||
}
|
||||
|
||||
private final ChangeKind kind;
|
||||
private final String fieldName;
|
||||
private final String description;
|
||||
private final String oldType;
|
||||
private final String newType;
|
||||
private final String oldDescription;
|
||||
private final String detail;
|
||||
|
||||
private FieldChange(ChangeKind kind, String fieldName, String description,
|
||||
String oldType, String newType, String oldDescription, String detail) {
|
||||
this.kind = kind;
|
||||
this.fieldName = fieldName;
|
||||
this.description = description;
|
||||
this.oldType = oldType;
|
||||
this.newType = newType;
|
||||
this.oldDescription = oldDescription;
|
||||
this.detail = detail;
|
||||
}
|
||||
|
||||
public static FieldChange added(FieldInfo field) {
|
||||
return new FieldChange(ChangeKind.ADDED, field.getName(), field.getDescription(),
|
||||
null, field.getType(), null, null);
|
||||
}
|
||||
|
||||
public static FieldChange removed(FieldInfo field) {
|
||||
return new FieldChange(ChangeKind.REMOVED, field.getName(), field.getDescription(),
|
||||
field.getType(), null, field.getDescription(), null);
|
||||
}
|
||||
|
||||
public static FieldChange modified(FieldInfo oldField, FieldInfo newField, String detail) {
|
||||
return new FieldChange(ChangeKind.MODIFIED, newField.getName(), newField.getDescription(),
|
||||
oldField.getType(), newField.getType(), oldField.getDescription(), detail);
|
||||
}
|
||||
|
||||
public ChangeKind getKind() {
|
||||
return kind;
|
||||
}
|
||||
|
||||
public String getFieldName() {
|
||||
return fieldName;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getOldType() {
|
||||
return oldType;
|
||||
}
|
||||
|
||||
public String getNewType() {
|
||||
return newType;
|
||||
}
|
||||
|
||||
public String getOldDescription() {
|
||||
return oldDescription;
|
||||
}
|
||||
|
||||
public String getDetail() {
|
||||
return detail;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.aicheck.model;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class FieldInfo {
|
||||
private final String name;
|
||||
private final String type;
|
||||
private final String description;
|
||||
|
||||
public FieldInfo(String name, String type, String description) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.description = description == null ? "" : description;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (!(o instanceof FieldInfo)) {
|
||||
return false;
|
||||
}
|
||||
FieldInfo other = (FieldInfo) o;
|
||||
return Objects.equals(name, other.name)
|
||||
&& Objects.equals(type, other.type)
|
||||
&& Objects.equals(description, other.description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name, type, description);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package com.aicheck.notify;
|
||||
|
||||
import com.aicheck.model.ApiEndpoint;
|
||||
import com.aicheck.model.ClassChangeReport;
|
||||
import com.aicheck.model.ClassType;
|
||||
import com.aicheck.model.FieldChange;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class WeComNotifier {
|
||||
private static final int MAX_LENGTH = 3800;
|
||||
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
|
||||
|
||||
private final OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.readTimeout(10, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
public int sendAll(String webhookUrl, List<ClassChangeReport> reports, String modifier, String modifyTime) {
|
||||
if (reports == null || reports.isEmpty()) {
|
||||
System.out.println("无类变更,不发送到企业微信");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int sent = 0;
|
||||
for (ClassChangeReport report : reports) {
|
||||
String markdown = buildMarkdown(report, modifier, modifyTime);
|
||||
if (postMarkdown(webhookUrl, markdown)) {
|
||||
sent++;
|
||||
System.out.println("已发送类变更通知: " + report.getClassName());
|
||||
}
|
||||
}
|
||||
if (sent > 0) {
|
||||
System.out.println("总共发送 " + sent + " 条类变更通知到企业微信");
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
public String buildMarkdown(ClassChangeReport report, String modifier, String modifyTime) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("# 【类变更通知】").append("\n\n");
|
||||
sb.append("■ 变更对象:**").append(report.getClassName()).append("**(")
|
||||
.append(report.getClassType().getLabel()).append(")").append("\n");
|
||||
sb.append("■ 修改人:").append(modifier).append("\n");
|
||||
sb.append("■ 修改时间:").append(modifyTime).append("\n\n");
|
||||
sb.append("────────────────────────────────").append("\n");
|
||||
sb.append("▶ 对象变更细节").append("\n");
|
||||
sb.append("────────────────────────────────").append("\n");
|
||||
|
||||
if (report.isDeleted()) {
|
||||
sb.append("[已删除] 该类文件已被移除").append("\n\n");
|
||||
} else {
|
||||
sb.append("字段变更列表:").append("\n");
|
||||
for (FieldChange change : report.getFieldChanges()) {
|
||||
sb.append(formatFieldChange(change)).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append("────────────────────────────────").append("\n");
|
||||
sb.append("▶ 影响范围").append("\n");
|
||||
sb.append("────────────────────────────────").append("\n");
|
||||
appendImpactSections(sb, report);
|
||||
return truncate(sb.toString());
|
||||
}
|
||||
|
||||
private void appendImpactSections(StringBuilder sb, ClassChangeReport report) {
|
||||
if (report.getClassType() == ClassType.ENTITY) {
|
||||
sb.append("① 入参影响(Dto变更导致接口参数变化):").append("\n");
|
||||
sb.append(" 无").append("\n\n");
|
||||
sb.append("② 类转换影响(Dto → Entity 转换,已开启检测):").append("\n");
|
||||
sb.append(" 无").append("\n\n");
|
||||
sb.append("③ 前端影响(Vo变更导致返回结构变化):").append("\n");
|
||||
sb.append(" 无").append("\n");
|
||||
return;
|
||||
}
|
||||
|
||||
sb.append("① 入参影响(Dto变更导致接口参数变化):").append("\n");
|
||||
appendEndpointList(sb, report.getInputImpactEndpoints());
|
||||
|
||||
sb.append("\n");
|
||||
sb.append("② 类转换影响(Dto → Entity 转换");
|
||||
if (report.isConversionCheckEnabled()) {
|
||||
sb.append(",已开启检测");
|
||||
}
|
||||
sb.append("):").append("\n");
|
||||
if (!report.isConversionCheckEnabled()) {
|
||||
sb.append(" 未开启检测").append("\n\n");
|
||||
} else if (report.getConversionEntities().isEmpty()) {
|
||||
sb.append(" 无").append("\n\n");
|
||||
} else {
|
||||
for (String entity : report.getConversionEntities()) {
|
||||
sb.append(" 涉及Entity类:").append(entity).append("\n");
|
||||
}
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
sb.append("③ 前端影响(Vo变更导致返回结构变化):").append("\n");
|
||||
appendEndpointList(sb, report.getFrontendImpactEndpoints());
|
||||
}
|
||||
|
||||
private void appendEndpointList(StringBuilder sb, List<ApiEndpoint> endpoints) {
|
||||
if (endpoints == null || endpoints.isEmpty()) {
|
||||
sb.append(" 无").append("\n");
|
||||
return;
|
||||
}
|
||||
sb.append(" 影响接口列表:").append("\n");
|
||||
for (ApiEndpoint endpoint : endpoints) {
|
||||
sb.append(" - ").append(endpoint.displayLine()).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
private String formatFieldChange(FieldChange change) {
|
||||
String desc = change.getDescription() == null || change.getDescription().isBlank()
|
||||
? "无" : change.getDescription();
|
||||
switch (change.getKind()) {
|
||||
case ADDED:
|
||||
return " [新增] 字段名: " + change.getFieldName() + " 说明: " + desc;
|
||||
case REMOVED:
|
||||
return " [删除] 字段名: " + change.getFieldName() + " 说明: " + desc;
|
||||
case MODIFIED:
|
||||
default:
|
||||
String detail = change.getDetail() == null || change.getDetail().isBlank()
|
||||
? desc : change.getDetail();
|
||||
return " [修改] 字段名: " + change.getFieldName() + " 说明: " + detail;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean postMarkdown(String webhookUrl, String content) {
|
||||
if (webhookUrl == null || webhookUrl.isBlank() || webhookUrl.contains("YOUR_WECOM")) {
|
||||
System.out.println("[警告] 未配置有效的企业微信 Webhook URL");
|
||||
System.out.println("--- 通知预览 ---");
|
||||
System.out.println(content.length() > 1000 ? content.substring(0, 1000) : content);
|
||||
return false;
|
||||
}
|
||||
|
||||
String payload = "{\"msgtype\":\"markdown\",\"markdown\":{\"content\":"
|
||||
+ jsonEscape(content) + "}}";
|
||||
Request request = new Request.Builder()
|
||||
.url(webhookUrl)
|
||||
.post(RequestBody.create(payload, JSON))
|
||||
.build();
|
||||
try (Response response = client.newCall(request).execute()) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
String body = response.body().string();
|
||||
return body.contains("\"errcode\":0");
|
||||
}
|
||||
System.out.println("[错误] 企微返回异常: " + response.code()
|
||||
+ (response.body() != null ? " " + response.body().string() : ""));
|
||||
return false;
|
||||
} catch (IOException e) {
|
||||
System.out.println("[错误] 发送企微消息失败: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private String truncate(String text) {
|
||||
if (text.length() <= MAX_LENGTH) {
|
||||
return text;
|
||||
}
|
||||
return text.substring(0, MAX_LENGTH) + "\n\n... 消息过长,已截断";
|
||||
}
|
||||
|
||||
private String jsonEscape(String text) {
|
||||
String escaped = text
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("\n", "\\n")
|
||||
.replace("\r", "");
|
||||
return "\"" + escaped + "\"";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.aicheck.model.FieldInfo;
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.FieldDeclaration;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
import com.github.javaparser.ast.body.VariableDeclarator;
|
||||
import com.github.javaparser.ast.comments.JavadocComment;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ClassFieldParser {
|
||||
public List<FieldInfo> parseFields(String source, String expectedClassName) {
|
||||
if (source == null || source.isBlank()) {
|
||||
return List.of();
|
||||
}
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (type instanceof ClassOrInterfaceDeclaration) {
|
||||
ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) type;
|
||||
if (classDecl.getNameAsString().equals(expectedClassName)) {
|
||||
return parseClassFields(classDecl);
|
||||
}
|
||||
}
|
||||
}
|
||||
return List.of();
|
||||
}
|
||||
|
||||
private List<FieldInfo> parseClassFields(ClassOrInterfaceDeclaration classDecl) {
|
||||
Map<String, FieldInfo> fields = new LinkedHashMap<>();
|
||||
for (FieldDeclaration fieldDecl : classDecl.getFields()) {
|
||||
if (fieldDecl.isStatic() && fieldDecl.isFinal()) {
|
||||
continue;
|
||||
}
|
||||
String type = TypeNameUtils.typeToString(fieldDecl.getElementType());
|
||||
String description = extractDescription(fieldDecl);
|
||||
for (VariableDeclarator variable : fieldDecl.getVariables()) {
|
||||
fields.put(variable.getNameAsString(), new FieldInfo(variable.getNameAsString(), type, description));
|
||||
}
|
||||
}
|
||||
return new ArrayList<>(fields.values());
|
||||
}
|
||||
|
||||
private String extractDescription(FieldDeclaration fieldDecl) {
|
||||
Optional<JavadocComment> javadoc = fieldDecl.getJavadocComment();
|
||||
if (javadoc.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String text = javadoc.get().parse().getDescription().toText();
|
||||
return text == null ? "" : text.trim().replaceAll("\\s+", " ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
import com.github.javaparser.ast.expr.MethodCallExpr;
|
||||
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class ConversionParser {
|
||||
public Set<String> findConvertTargetsInClass(String source, String className) {
|
||||
Set<String> entities = new LinkedHashSet<>();
|
||||
if (source == null || source.isBlank()) {
|
||||
return entities;
|
||||
}
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (type instanceof ClassOrInterfaceDeclaration) {
|
||||
ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) type;
|
||||
if (!classDecl.getNameAsString().equals(className)) {
|
||||
continue;
|
||||
}
|
||||
for (MethodDeclaration method : classDecl.getMethods()) {
|
||||
if (!"convert".equals(method.getNameAsString())) {
|
||||
continue;
|
||||
}
|
||||
String returnType = TypeNameUtils.simpleName(TypeNameUtils.typeToString(method.getType()));
|
||||
if (returnType.endsWith("Entity")) {
|
||||
entities.add(returnType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
public Set<String> findBeanUtilsTargets(Path rootDir, String sourceClassName) throws IOException {
|
||||
Set<String> entities = new LinkedHashSet<>();
|
||||
if (!Files.exists(rootDir)) {
|
||||
return entities;
|
||||
}
|
||||
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||
paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> {
|
||||
try {
|
||||
String source = Files.readString(path, StandardCharsets.UTF_8);
|
||||
entities.addAll(scanBeanUtilsInSource(source, sourceClassName));
|
||||
} catch (IOException ignored) {
|
||||
// 跳过
|
||||
}
|
||||
});
|
||||
}
|
||||
return entities;
|
||||
}
|
||||
|
||||
private Set<String> scanBeanUtilsInSource(String source, String sourceClassName) {
|
||||
Set<String> entities = new LinkedHashSet<>();
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
cu.accept(new VoidVisitorAdapter<Void>() {
|
||||
@Override
|
||||
public void visit(MethodCallExpr call, Void arg) {
|
||||
super.visit(call, arg);
|
||||
if (!call.getNameAsString().equals("copyProperties")) {
|
||||
return;
|
||||
}
|
||||
if (call.getScope().isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String scope = call.getScope().get().toString();
|
||||
if (!scope.endsWith("BeanUtils")) {
|
||||
return;
|
||||
}
|
||||
if (call.getArguments().size() < 2) {
|
||||
return;
|
||||
}
|
||||
String firstArg = TypeNameUtils.simpleName(call.getArguments().get(0).toString());
|
||||
String secondArg = TypeNameUtils.simpleName(call.getArguments().get(1).toString());
|
||||
if (sourceClassName.equals(firstArg) && secondArg.endsWith("Entity")) {
|
||||
entities.add(secondArg);
|
||||
}
|
||||
}
|
||||
}, null);
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.aicheck.model.ApiEndpoint;
|
||||
import com.github.javaparser.StaticJavaParser;
|
||||
import com.github.javaparser.ast.CompilationUnit;
|
||||
import com.github.javaparser.ast.NodeList;
|
||||
import com.github.javaparser.ast.expr.Expression;
|
||||
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
|
||||
import com.github.javaparser.ast.body.MethodDeclaration;
|
||||
import com.github.javaparser.ast.body.Parameter;
|
||||
import com.github.javaparser.ast.body.TypeDeclaration;
|
||||
import com.github.javaparser.ast.expr.AnnotationExpr;
|
||||
import com.github.javaparser.ast.type.Type;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class EndpointParser {
|
||||
private static final Set<String> MAPPING_ANNOTATIONS = Set.of(
|
||||
"GetMapping", "PostMapping", "PutMapping", "DeleteMapping", "PatchMapping", "RequestMapping"
|
||||
);
|
||||
private static final Map<String, String> MAPPING_DEFAULT_METHOD = Map.of(
|
||||
"GetMapping", "GET",
|
||||
"PostMapping", "POST",
|
||||
"PutMapping", "PUT",
|
||||
"DeleteMapping", "DELETE",
|
||||
"PatchMapping", "PATCH"
|
||||
);
|
||||
|
||||
public List<ApiEndpoint> scanControllerDirectory(Path rootDir, String relativePrefix) throws IOException {
|
||||
return scanDirectory(rootDir, relativePrefix, ScanMode.CONTROLLER);
|
||||
}
|
||||
|
||||
public List<ApiEndpoint> scanFeignDirectory(Path rootDir, String relativePrefix) throws IOException {
|
||||
return scanDirectory(rootDir, relativePrefix, ScanMode.FEIGN);
|
||||
}
|
||||
|
||||
private List<ApiEndpoint> scanDirectory(Path rootDir, String relativePrefix, ScanMode mode) throws IOException {
|
||||
if (!Files.exists(rootDir)) {
|
||||
return List.of();
|
||||
}
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
try (Stream<Path> paths = Files.walk(rootDir)) {
|
||||
paths.filter(path -> path.toString().endsWith(".java")).forEach(path -> {
|
||||
try {
|
||||
String source = Files.readString(path, StandardCharsets.UTF_8);
|
||||
String relativePath = toRelativePath(relativePrefix, rootDir, path);
|
||||
endpoints.addAll(parseCompilationUnit(source, relativePath, mode));
|
||||
} catch (IOException ignored) {
|
||||
// 跳过无法读取的文件
|
||||
}
|
||||
});
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private List<ApiEndpoint> parseCompilationUnit(String source, String relativePath, ScanMode mode) {
|
||||
CompilationUnit cu = StaticJavaParser.parse(source);
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
|
||||
for (TypeDeclaration<?> type : cu.getTypes()) {
|
||||
if (!(type instanceof ClassOrInterfaceDeclaration)) {
|
||||
continue;
|
||||
}
|
||||
ClassOrInterfaceDeclaration declaration = (ClassOrInterfaceDeclaration) type;
|
||||
if (mode == ScanMode.CONTROLLER && !isController(declaration)) {
|
||||
continue;
|
||||
}
|
||||
if (mode == ScanMode.FEIGN && !isFeignClient(declaration)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String basePath = mode == ScanMode.FEIGN
|
||||
? joinPaths(extractFeignBasePath(declaration), extractTypeLevelPath(declaration))
|
||||
: extractTypeLevelPath(declaration);
|
||||
for (MethodDeclaration method : declaration.getMethods()) {
|
||||
if (mode == ScanMode.FEIGN && declaration.isInterface()) {
|
||||
endpoints.addAll(parseMethod(method, basePath, relativePath));
|
||||
} else if (mode == ScanMode.CONTROLLER && !declaration.isInterface()) {
|
||||
endpoints.addAll(parseMethod(method, basePath, relativePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private List<ApiEndpoint> parseMethod(MethodDeclaration method, String basePath, String sourceFile) {
|
||||
List<ApiEndpoint> endpoints = new ArrayList<>();
|
||||
for (AnnotationExpr annotation : method.getAnnotations()) {
|
||||
String annName = annotation.getNameAsString();
|
||||
if (!MAPPING_ANNOTATIONS.contains(annName)) {
|
||||
continue;
|
||||
}
|
||||
List<String> subPaths = extractPaths(annotation);
|
||||
List<String> httpMethods = extractHttpMethods(annotation, annName);
|
||||
for (String httpMethod : httpMethods) {
|
||||
for (String subPath : subPaths) {
|
||||
String uri = joinPaths(basePath, subPath);
|
||||
Set<String> paramTypes = extractParamTypes(method);
|
||||
Set<String> returnTypes = TypeNameUtils.peelDirectTypeNames(method.getType());
|
||||
endpoints.add(new ApiEndpoint(httpMethod, uri, sourceFile, paramTypes, returnTypes));
|
||||
}
|
||||
}
|
||||
}
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private Set<String> extractParamTypes(MethodDeclaration method) {
|
||||
Set<String> paramTypes = new LinkedHashSet<>();
|
||||
for (Parameter parameter : method.getParameters()) {
|
||||
Type type = parameter.getType();
|
||||
paramTypes.add(TypeNameUtils.simpleName(TypeNameUtils.typeToString(type)));
|
||||
paramTypes.addAll(TypeNameUtils.peelDirectTypeNames(type));
|
||||
}
|
||||
return paramTypes;
|
||||
}
|
||||
|
||||
private boolean isController(ClassOrInterfaceDeclaration declaration) {
|
||||
return declaration.getAnnotations().stream()
|
||||
.anyMatch(ann -> {
|
||||
String name = ann.getNameAsString();
|
||||
return "RestController".equals(name) || "Controller".equals(name);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isFeignClient(ClassOrInterfaceDeclaration declaration) {
|
||||
return declaration.isInterface() && declaration.getAnnotations().stream()
|
||||
.anyMatch(ann -> "FeignClient".equals(ann.getNameAsString()));
|
||||
}
|
||||
|
||||
private String extractTypeLevelPath(ClassOrInterfaceDeclaration declaration) {
|
||||
for (AnnotationExpr annotation : declaration.getAnnotations()) {
|
||||
if ("RequestMapping".equals(annotation.getNameAsString())) {
|
||||
List<String> paths = extractPaths(annotation);
|
||||
if (!paths.isEmpty()) {
|
||||
return paths.get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private String extractFeignBasePath(ClassOrInterfaceDeclaration declaration) {
|
||||
for (AnnotationExpr annotation : declaration.getAnnotations()) {
|
||||
if ("FeignClient".equals(annotation.getNameAsString())) {
|
||||
List<String> paths = AnnotationValueReader.readStringArray(annotation, "path");
|
||||
if (!paths.isEmpty()) {
|
||||
return paths.get(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private List<String> extractPaths(AnnotationExpr annotation) {
|
||||
return AnnotationValueReader.readStringArray(annotation, "value", "path");
|
||||
}
|
||||
|
||||
private List<String> extractHttpMethods(AnnotationExpr annotation, String annName) {
|
||||
if (!"RequestMapping".equals(annName)) {
|
||||
return List.of(MAPPING_DEFAULT_METHOD.getOrDefault(annName, "GET"));
|
||||
}
|
||||
List<String> methods = AnnotationValueReader.readEnumArray(annotation, "method");
|
||||
if (methods.isEmpty()) {
|
||||
return List.of("GET");
|
||||
}
|
||||
return methods;
|
||||
}
|
||||
|
||||
private String joinPaths(String base, String sub) {
|
||||
String normalizedBase = normalizePath(base);
|
||||
String normalizedSub = normalizePath(sub);
|
||||
if (normalizedBase.isEmpty()) {
|
||||
return normalizedSub.isEmpty() ? "/" : normalizedSub;
|
||||
}
|
||||
if (normalizedSub.isEmpty()) {
|
||||
return normalizedBase;
|
||||
}
|
||||
String joined = normalizedBase + "/" + normalizedSub.substring(1);
|
||||
return joined.replaceAll("/+", "/");
|
||||
}
|
||||
|
||||
private String normalizePath(String path) {
|
||||
if (path == null || path.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String trimmed = path.trim();
|
||||
if (!trimmed.startsWith("/")) {
|
||||
trimmed = "/" + trimmed;
|
||||
}
|
||||
return trimmed.replaceAll("/+", "/");
|
||||
}
|
||||
|
||||
private String toRelativePath(String relativePrefix, Path rootDir, Path file) {
|
||||
String relative = rootDir.relativize(file).toString().replace("\\", "/");
|
||||
if (relativePrefix == null || relativePrefix.isBlank()) {
|
||||
return relative;
|
||||
}
|
||||
String prefix = relativePrefix.endsWith("/")
|
||||
? relativePrefix.substring(0, relativePrefix.length() - 1)
|
||||
: relativePrefix;
|
||||
return prefix + "/" + relative;
|
||||
}
|
||||
|
||||
private enum ScanMode {
|
||||
CONTROLLER, FEIGN
|
||||
}
|
||||
|
||||
static final class AnnotationValueReader {
|
||||
private AnnotationValueReader() {
|
||||
}
|
||||
|
||||
static List<String> readStringArray(AnnotationExpr annotation, String... keys) {
|
||||
NodeList<?> values = readArrayValues(annotation, keys);
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Object value : values) {
|
||||
String text = value.toString().replace("\"", "").trim();
|
||||
if (!text.isBlank()) {
|
||||
result.add(text);
|
||||
}
|
||||
}
|
||||
if (result.isEmpty()) {
|
||||
result.add("");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static List<String> readEnumArray(AnnotationExpr annotation, String key) {
|
||||
NodeList<?> values = readArrayValues(annotation, key);
|
||||
List<String> result = new ArrayList<>();
|
||||
for (Object value : values) {
|
||||
String text = value.toString().trim();
|
||||
if (text.contains(".")) {
|
||||
text = text.substring(text.lastIndexOf('.') + 1);
|
||||
}
|
||||
result.add(text.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static NodeList<?> readArrayValues(AnnotationExpr annotation, String... keys) {
|
||||
if (annotation.isSingleMemberAnnotationExpr()) {
|
||||
Expression value = annotation.asSingleMemberAnnotationExpr().getMemberValue();
|
||||
if (value.isArrayInitializerExpr()) {
|
||||
return value.asArrayInitializerExpr().getValues();
|
||||
}
|
||||
return new NodeList<>(value);
|
||||
}
|
||||
if (annotation.isNormalAnnotationExpr()) {
|
||||
var pairs = annotation.asNormalAnnotationExpr().getPairs();
|
||||
for (var pair : pairs) {
|
||||
for (String key : keys) {
|
||||
if (pair.getNameAsString().equals(key)) {
|
||||
if (pair.getValue().isArrayInitializerExpr()) {
|
||||
return pair.getValue().asArrayInitializerExpr().getValues();
|
||||
}
|
||||
return new NodeList<>(pair.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
for (var pair : pairs) {
|
||||
if ("value".equals(pair.getNameAsString())) {
|
||||
if (pair.getValue().isArrayInitializerExpr()) {
|
||||
return pair.getValue().asArrayInitializerExpr().getValues();
|
||||
}
|
||||
return new NodeList<>(pair.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
return new NodeList<>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.aicheck.parser;
|
||||
|
||||
import com.github.javaparser.ast.type.ClassOrInterfaceType;
|
||||
import com.github.javaparser.ast.type.Type;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class TypeNameUtils {
|
||||
private static final Set<String> WRAPPER_TYPES = Set.of(
|
||||
"ActionResult", "List", "PageListVO", "Set", "Collection", "Iterable", "Optional"
|
||||
);
|
||||
|
||||
private TypeNameUtils() {
|
||||
}
|
||||
|
||||
public static String typeToString(Type type) {
|
||||
if (type == null) {
|
||||
return "Object";
|
||||
}
|
||||
return type.toString().replaceAll("\\s+", "");
|
||||
}
|
||||
|
||||
public static String simpleName(String typeName) {
|
||||
if (typeName == null || typeName.isBlank()) {
|
||||
return "";
|
||||
}
|
||||
String cleaned = typeName.replaceAll("\\s+", "");
|
||||
int genericStart = cleaned.indexOf('<');
|
||||
String base = genericStart >= 0 ? cleaned.substring(0, genericStart) : cleaned;
|
||||
int dot = base.lastIndexOf('.');
|
||||
return dot >= 0 ? base.substring(dot + 1) : base;
|
||||
}
|
||||
|
||||
public static Set<String> peelDirectTypeNames(Type type) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
collectPeelTargets(type, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Set<String> peelDirectTypeNames(String typeName) {
|
||||
Set<String> result = new LinkedHashSet<>();
|
||||
collectPeelTargets(typeName, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void collectPeelTargets(Type type, Set<String> result) {
|
||||
if (type == null) {
|
||||
return;
|
||||
}
|
||||
if (type.isClassOrInterfaceType()) {
|
||||
ClassOrInterfaceType classType = type.asClassOrInterfaceType();
|
||||
String name = simpleName(classType.getNameAsString());
|
||||
if (WRAPPER_TYPES.contains(name) && classType.getTypeArguments().isPresent()) {
|
||||
for (Type arg : classType.getTypeArguments().get()) {
|
||||
collectPeelTargets(arg, result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
result.add(name);
|
||||
return;
|
||||
}
|
||||
result.add(simpleName(typeToString(type)));
|
||||
}
|
||||
|
||||
private static void collectPeelTargets(String typeName, Set<String> result) {
|
||||
String cleaned = typeName.replaceAll("\\s+", "");
|
||||
int genericStart = cleaned.indexOf('<');
|
||||
if (genericStart < 0) {
|
||||
result.add(simpleName(cleaned));
|
||||
return;
|
||||
}
|
||||
String outer = simpleName(cleaned.substring(0, genericStart));
|
||||
String inner = cleaned.substring(genericStart + 1, cleaned.lastIndexOf('>'));
|
||||
if (WRAPPER_TYPES.contains(outer)) {
|
||||
for (String part : splitGenericArgs(inner)) {
|
||||
collectPeelTargets(part, result);
|
||||
}
|
||||
return;
|
||||
}
|
||||
result.add(outer);
|
||||
}
|
||||
|
||||
private static List<String> splitGenericArgs(String inner) {
|
||||
List<String> parts = new java.util.ArrayList<>();
|
||||
int depth = 0;
|
||||
StringBuilder current = new StringBuilder();
|
||||
for (char ch : inner.toCharArray()) {
|
||||
if (ch == '<') {
|
||||
depth++;
|
||||
} else if (ch == '>') {
|
||||
depth--;
|
||||
} else if (ch == ',' && depth == 0) {
|
||||
parts.add(current.toString().trim());
|
||||
current.setLength(0);
|
||||
continue;
|
||||
}
|
||||
current.append(ch);
|
||||
}
|
||||
if (current.length() > 0) {
|
||||
parts.add(current.toString().trim());
|
||||
}
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#Generated by Maven
|
||||
#Fri Jun 05 18:18:14 GMT+08:00 2026
|
||||
groupId=com.aicheck
|
||||
artifactId=class-checker
|
||||
version=1.0.0
|
||||
@@ -0,0 +1,24 @@
|
||||
com\aicheck\model\FieldChange$ChangeKind.class
|
||||
com\aicheck\analyzer\ClassChangeAnalyzer.class
|
||||
com\aicheck\notify\WeComNotifier.class
|
||||
com\aicheck\model\ClassType.class
|
||||
com\aicheck\notify\WeComNotifier$1.class
|
||||
com\aicheck\model\ChangedClassFile.class
|
||||
com\aicheck\analyzer\FieldDiffEngine.class
|
||||
com\aicheck\parser\ConversionParser$1.class
|
||||
com\aicheck\parser\ConversionParser.class
|
||||
com\aicheck\config\AppConfig.class
|
||||
com\aicheck\parser\ClassFieldParser.class
|
||||
com\aicheck\analyzer\ImpactAnalyzer.class
|
||||
com\aicheck\ClassCheckMain.class
|
||||
com\aicheck\model\ApiEndpoint.class
|
||||
com\aicheck\model\ClassChangeReport.class
|
||||
com\aicheck\parser\EndpointParser.class
|
||||
com\aicheck\analyzer\EndpointIndexBuilder.class
|
||||
com\aicheck\parser\TypeNameUtils.class
|
||||
com\aicheck\model\ChangedClassFile$ChangeStatus.class
|
||||
com\aicheck\parser\EndpointParser$AnnotationValueReader.class
|
||||
com\aicheck\model\FieldInfo.class
|
||||
com\aicheck\git\GitChangeScanner.class
|
||||
com\aicheck\model\FieldChange.class
|
||||
com\aicheck\parser\EndpointParser$ScanMode.class
|
||||
@@ -0,0 +1,18 @@
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\analyzer\ClassChangeAnalyzer.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\analyzer\EndpointIndexBuilder.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\analyzer\FieldDiffEngine.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\analyzer\ImpactAnalyzer.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\ClassCheckMain.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\config\AppConfig.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\git\GitChangeScanner.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\model\ApiEndpoint.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\model\ChangedClassFile.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\model\ClassChangeReport.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\model\ClassType.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\model\FieldChange.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\model\FieldInfo.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\notify\WeComNotifier.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\parser\ClassFieldParser.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\parser\ConversionParser.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\parser\EndpointParser.java
|
||||
C:\Users\EDY\Desktop\Demo\AI-Check-Test\.gitea\class-checker\src\main\java\com\aicheck\parser\TypeNameUtils.java
|
||||
27
.gitea/config.yaml
Normal file
27
.gitea/config.yaml
Normal file
@@ -0,0 +1,27 @@
|
||||
# ============================================================
|
||||
# 类变更检测配置
|
||||
# ============================================================
|
||||
|
||||
class_check:
|
||||
enabled: true
|
||||
|
||||
dto_entity_conversion:
|
||||
enabled: true
|
||||
|
||||
model_dirs:
|
||||
- jnpf-ftb/jnpf-ftb-entity/src/main/java
|
||||
|
||||
endpoint_scan:
|
||||
controllers:
|
||||
- jnpf-ftb/jnpf-ftb-biz/src/main/java
|
||||
feign_apis:
|
||||
- jnpf-ftb/jnpf-ftb-api/src/main/java
|
||||
|
||||
conversion_scan:
|
||||
- jnpf-ftb/jnpf-ftb-biz/src/main/java
|
||||
|
||||
wecom:
|
||||
webhook_url: "YOUR_WECOM_WEBHOOK_URL"
|
||||
|
||||
notify:
|
||||
only_on_change: true
|
||||
50
.gitea/workflows/class-change-check.yml
Normal file
50
.gitea/workflows/class-change-check.yml
Normal file
@@ -0,0 +1,50 @@
|
||||
name: 类变更检测
|
||||
run-name: ${{ gitea.actor }}的类变更检测
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
class-change-check:
|
||||
if: ${{ gitea.ref != 'refs/heads/pre' && gitea.ref != 'refs/heads/dev' && gitea.ref != 'refs/heads/master-2.0' }}
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: 检出代码
|
||||
run: |
|
||||
git config --global http.sslVerify false
|
||||
git clone "https://${{ gitea.token }}@git.niujiekeji.com/${{ gitea.repository }}.git" .
|
||||
git checkout ${{ gitea.sha }}
|
||||
echo "当前提交: $(git rev-parse HEAD)"
|
||||
echo "上一提交: $(git rev-parse HEAD~1 2>/dev/null || echo '无')"
|
||||
|
||||
- name: 检查配置文件
|
||||
run: |
|
||||
if [ ! -f .gitea/config.yaml ]; then
|
||||
echo "错误: 缺少 .gitea/config.yaml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: 设置 JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- name: 编译检测工具
|
||||
run: mvn -q -f .gitea/class-checker/pom.xml package -DskipTests
|
||||
|
||||
- name: 执行类变更检测
|
||||
run: |
|
||||
OLD_SHA=$(git rev-parse HEAD~1 2>/dev/null || echo "")
|
||||
if [ -z "$OLD_SHA" ]; then
|
||||
echo "首次提交,跳过类变更检测"
|
||||
exit 0
|
||||
fi
|
||||
COMMIT_TIME=$(git log -1 --format=%cd --date=format:'%Y-%m-%d %H:%M:%S')
|
||||
java -jar .gitea/class-checker/target/class-checker.jar \
|
||||
--config .gitea/config.yaml \
|
||||
--repo-root . \
|
||||
--old-sha "$OLD_SHA" \
|
||||
--new-sha "$(git rev-parse HEAD)" \
|
||||
--modifier "${{ gitea.actor }}" \
|
||||
--modify-time "$COMMIT_TIME"
|
||||
Reference in New Issue
Block a user