脚本修改
Some checks failed
类变更检测 / class-change-check (push) Failing after 1s

This commit is contained in:
2026-06-08 13:08:34 +08:00
parent 9e1d66c81f
commit 2f8798c38c
39 changed files with 3577 additions and 21 deletions

View File

@@ -0,0 +1,342 @@
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;
/**
* 将 ClassChangeReport 渲染为企业微信 Markdown 并发送(或仅日志输出)。
* <p>
* 使用 webhook {@code markdown}v1引用块 + 换行排版,三色 fontinfo/comment/warning
* v1 不支持无序列表,各项以 {@code >标签:值} 分行展示。
*/
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;
}
/** 企微关闭时打印 Markdown 到控制台 */
public void logAll(List<ClassChangeReport> reports, String modifier, String modifyTime) {
if (reports == null || reports.isEmpty()) {
System.out.println("无类变更,无日志输出");
return;
}
System.out.println("企业微信通知已关闭wecom.enabled=false以下结果仅输出到日志");
for (int i = 0; i < reports.size(); i++) {
ClassChangeReport report = reports.get(i);
System.out.println("========== 类变更 [" + (i + 1) + "/" + reports.size()
+ "]: " + report.getClassName() + " ==========");
System.out.println(buildMarkdown(report, modifier, modifyTime));
System.out.println("========== 结束 ==========");
}
System.out.println("" + reports.size() + " 条类变更结果(未发送到企业微信)");
}
/** 组装完整 Markdown 正文(引用块 + 换行,每项独立一行) */
public String buildMarkdown(ClassChangeReport report, String modifier, String modifyTime) {
StringBuilder sb = new StringBuilder();
sb.append("# 【类变更通知】").append("\n\n");
appendHeader(sb, report, modifier, modifyTime);
sb.append("\n## 【对象变更细节】").append("\n\n");
appendChangeDetails(sb, report);
sb.append("\n## 【影响范围】").append("\n\n");
appendImpactSections(sb, report);
return truncate(sb.toString());
}
/** 头部元信息,每项一行引用 */
private void appendHeader(StringBuilder sb, ClassChangeReport report,
String modifier, String modifyTime) {
sb.append(quoteKv("变更对象", colorInfo(safe(report.getClassName()))
+ "" + report.getClassType().getLabel() + "")).append("\n");
sb.append(quoteKv("修改人", colorComment(modifier))).append("\n");
sb.append(quoteKv("时间", colorComment(modifyTime))).append("\n");
sb.append(quoteKv("路径", colorComment(report.getSourceFile()))).append("\n");
}
/** 渲染删除 / 重命名 / 字段变更 */
private void appendChangeDetails(StringBuilder sb, ClassChangeReport report) {
if (report.isDeleted()) {
sb.append(quoteLine(colorWarning("[已删除]") + " "
+ colorComment("该类文件已被移除"))).append("\n");
return;
}
if (report.isRenamed()) {
sb.append(quoteLine(colorWarning("[类名变更]") + " "
+ colorComment(safe(report.getOldClassName())) + ""
+ colorInfo(safe(report.getClassName())))).append("\n");
}
if (report.isRenameOnly()) {
sb.append(quoteLine(colorComment("字段无变化"))).append("\n");
return;
}
if (!report.getFieldChanges().isEmpty()) {
sb.append(quoteLine(colorComment(""
+ report.getFieldChanges().size() + " 项字段变更"))).append("\n\n");
for (int i = 0; i < report.getFieldChanges().size(); i++) {
if (i > 0) {
sb.append("\n");
}
sb.append(formatFieldChange(report.getFieldChanges().get(i)));
}
sb.append("\n");
}
}
/** 按类类型选择影响段落 */
private void appendImpactSections(StringBuilder sb, ClassChangeReport report) {
appendImpactByType(sb, report);
}
/** Dto/Vo/Entity/Model 各展示不同的 request/response/转换段落 */
private void appendImpactByType(StringBuilder sb, ClassChangeReport report) {
switch (report.getClassType()) {
case DTO:
appendSectionIfNeeded(sb, report, true, false, true);
break;
case VO:
appendSectionIfNeeded(sb, report, false, true, true);
break;
case ENTITY:
case MODEL:
appendSectionIfNeeded(sb, report, false, false, true);
break;
default:
appendSectionIfNeeded(sb, report, true, true, true);
}
}
/** 按需追加 request / response / 类转换三个小节 */
private void appendSectionIfNeeded(StringBuilder sb, ClassChangeReport report,
boolean showRequest, boolean showResponse, boolean showConversion) {
if (showRequest) {
sb.append("### 影响 request 接口").append("\n");
appendEndpointList(sb, report.getInputImpactEndpoints());
sb.append("\n");
}
if (showResponse) {
sb.append("### 影响 response 接口").append("\n");
appendEndpointList(sb, report.getFrontendImpactEndpoints());
sb.append("\n");
}
if (showConversion) {
sb.append("### 类转换影响").append("\n");
appendConversionList(sb, report);
}
}
/** 渲染关联 Entity每项一行 */
private void appendConversionList(StringBuilder sb, ClassChangeReport report) {
if (!report.isConversionCheckEnabled()) {
sb.append(quoteLine(colorComment("未开启检测"))).append("\n");
return;
}
if (report.getConversionEntities().isEmpty()) {
sb.append(quoteLine(colorComment(""))).append("\n");
return;
}
for (String entity : report.getConversionEntities()) {
sb.append(quoteKv("Entity", colorInfo(safe(entity)))).append("\n");
}
}
/** 渲染接口,每项一行 */
private void appendEndpointList(StringBuilder sb, List<ApiEndpoint> endpoints) {
if (endpoints == null || endpoints.isEmpty()) {
sb.append(quoteLine(colorComment(""))).append("\n");
return;
}
for (ApiEndpoint endpoint : endpoints) {
sb.append(formatEndpointLine(endpoint)).append("\n");
}
}
/** 接口行:> POST `/path` */
private String formatEndpointLine(ApiEndpoint endpoint) {
String line = endpoint.displayLine();
int space = line.indexOf(' ');
if (space > 0) {
String method = line.substring(0, space).trim();
String path = line.substring(space).trim();
return quoteLine(colorInfo(method) + " " + inlineCode(path));
}
return quoteLine(inlineCode(safe(line)));
}
/**
* 单条字段变更:引用块多行,字段间空行分隔。
* 避免 font 内嵌 bold。
*/
private String formatFieldChange(FieldChange change) {
String fieldName = inlineCode(safe(change.getFieldName()));
String desc = change.getDescription() == null ? "" : change.getDescription();
String descPart = desc.isBlank()
? colorComment("(无说明)")
: colorComment(desc);
switch (change.getKind()) {
case ADDED:
return quoteLine(tagAdded() + " " + fieldName) + "\n"
+ quoteKv("说明", descPart);
case REMOVED:
return quoteLine(tagRemoved() + " " + fieldName) + "\n"
+ quoteKv("说明", descPart);
case MODIFIED:
default:
StringBuilder block = new StringBuilder();
block.append(quoteLine(tagModified() + " " + fieldName)).append("\n");
block.append(quoteKv("说明", descPart));
String detail = change.getDetail();
if (detail != null && !detail.isBlank()) {
block.append("\n").append(quoteKv("类型", formatTypeChange(detail)));
}
return block.toString();
}
}
/** 类型变化:旧 warning → 新 info */
private String formatTypeChange(String detail) {
int arrow = detail.indexOf("");
if (arrow < 0) {
return colorWarning(safe(detail));
}
String oldType = detail.substring(0, arrow).trim();
String newType = detail.substring(arrow + 3).trim();
return colorWarning(safe(oldType)) + "" + colorInfo(safe(newType));
}
private String tagAdded() {
return colorInfo("[新增]");
}
private String tagRemoved() {
return colorWarning("[删除]");
}
private String tagModified() {
return colorWarning("[修改]");
}
/** 引用行:{@code >标签:值} */
private String quoteKv(String key, String value) {
return "> " + key + ":" + value;
}
/** 纯引用行 */
private String quoteLine(String content) {
return "> " + content;
}
/** 行内代码 */
private String inlineCode(String text) {
return "`" + text.replace("`", "'") + "`";
}
private String colorInfo(String text) {
return "<font color=\"info\">" + text + "</font>";
}
private String colorComment(String text) {
return "<font color=\"comment\">" + safe(text) + "</font>";
}
private String colorWarning(String text) {
return "<font color=\"warning\">" + text + "</font>";
}
/** 转义 HTML 特殊字符,避免破坏 font 标签 */
private String safe(String text) {
if (text == null) {
return "";
}
return text.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;");
}
/** POST 企微 Webhookmarkdown v1 */
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;
}
}
/** 超长消息截断(企微上限 4096 字节 UTF-8 */
private String truncate(String text) {
if (text.length() <= MAX_LENGTH) {
return text;
}
return text.substring(0, MAX_LENGTH) + "\n\n... 消息过长,已截断";
}
/** JSON 字符串转义 */
private String jsonEscape(String text) {
String escaped = text
.replace("\\", "\\\\")
.replace("\"", "\\\"")
.replace("\n", "\\n")
.replace("\r", "");
return "\"" + escaped + "\"";
}
}