项目结构变更
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
package com.codechecker.notify;
|
||||
|
||||
import com.codechecker.model.ApiEndpoint;
|
||||
import com.codechecker.model.ClassChangeReport;
|
||||
import com.codechecker.model.ClassType;
|
||||
import com.codechecker.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):引用块 + 换行排版,三色 font(info/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 String formatChangeTarget(ClassChangeReport report) {
|
||||
String name = colorInfo(safe(report.getClassName()));
|
||||
String description = report.getClassDescription();
|
||||
if (description == null || description.isBlank()) {
|
||||
return name;
|
||||
}
|
||||
return name + "(" + colorComment(description) + ")";
|
||||
}
|
||||
|
||||
/** 头部元信息,每项一行引用(加粗) */
|
||||
private void appendHeader(StringBuilder sb, ClassChangeReport report,
|
||||
String modifier, String modifyTime) {
|
||||
sb.append(quoteKvBold("变更对象", formatChangeTarget(report))).append("\n");
|
||||
sb.append(quoteKvBold("修改人", colorComment(modifier))).append("\n");
|
||||
sb.append(quoteKvBold("时间", colorComment(modifyTime))).append("\n");
|
||||
sb.append(quoteKvBold("路径", 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()) {
|
||||
int count = report.getFieldChanges().size();
|
||||
sb.append(quoteLine("**共 " + colorWarning(String.valueOf(count)) + " 项变更**"))
|
||||
.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)));
|
||||
}
|
||||
|
||||
/** 单条字段变更:标签、说明、类型合并为一行,字段间空行分隔 */
|
||||
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: {
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(tagAdded()).append(" ").append(fieldName)
|
||||
.append(" 说明: ").append(descPart);
|
||||
appendFieldType(line, change);
|
||||
return quoteLine(line.toString());
|
||||
}
|
||||
case REMOVED: {
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(tagRemoved()).append(" ").append(fieldName)
|
||||
.append(" 说明: ").append(descPart);
|
||||
appendFieldType(line, change);
|
||||
return quoteLine(line.toString());
|
||||
}
|
||||
case RENAMED: {
|
||||
StringBuilder renameLine = new StringBuilder();
|
||||
renameLine.append(tagRenamed()).append(" ")
|
||||
.append(colorComment(safe(change.getOldFieldName()))).append(" → ")
|
||||
.append(colorInfo(safe(change.getFieldName())))
|
||||
.append(" 说明: ").append(descPart);
|
||||
appendFieldType(renameLine, change);
|
||||
return quoteLine(renameLine.toString());
|
||||
}
|
||||
case MODIFIED:
|
||||
default: {
|
||||
StringBuilder line = new StringBuilder();
|
||||
line.append(tagModified()).append(" ").append(fieldName)
|
||||
.append(" 说明: ").append(descPart);
|
||||
appendFieldType(line, change);
|
||||
return quoteLine(line.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 追加字段类型:新增/重命名(仅改名)用 info,删除用 warning,修改/重命名(改类型)用 old → new */
|
||||
private void appendFieldType(StringBuilder line, FieldChange change) {
|
||||
if (change.getKind() == FieldChange.ChangeKind.RENAMED
|
||||
|| change.getKind() == FieldChange.ChangeKind.MODIFIED) {
|
||||
String typeDetail = change.getDetail();
|
||||
if (typeDetail != null && !typeDetail.isBlank()) {
|
||||
line.append(" 类型: ").append(formatTypeChange(typeDetail));
|
||||
return;
|
||||
}
|
||||
}
|
||||
String singleType = change.getKind() == FieldChange.ChangeKind.REMOVED
|
||||
? change.getOldType()
|
||||
: change.getNewType();
|
||||
if (singleType == null || singleType.isBlank()) {
|
||||
return;
|
||||
}
|
||||
line.append(" 类型: ");
|
||||
if (change.getKind() == FieldChange.ChangeKind.REMOVED) {
|
||||
line.append(colorWarning(singleType));
|
||||
} else {
|
||||
line.append(colorInfo(singleType));
|
||||
}
|
||||
}
|
||||
|
||||
/** 类型变化:旧 warning → 新 info;泛型尖括号原样展示,不做 HTML 转义 */
|
||||
private String formatTypeChange(String detail) {
|
||||
int arrow = detail.indexOf(" → ");
|
||||
if (arrow < 0) {
|
||||
return colorWarning(detail);
|
||||
}
|
||||
String oldType = detail.substring(0, arrow).trim();
|
||||
String newType = detail.substring(arrow + 3).trim();
|
||||
return colorWarning(oldType) + " → " + colorInfo(newType);
|
||||
}
|
||||
|
||||
private String tagAdded() {
|
||||
return colorInfo("[新增]");
|
||||
}
|
||||
|
||||
private String tagRemoved() {
|
||||
return colorWarning("[删除]");
|
||||
}
|
||||
|
||||
private String tagModified() {
|
||||
return colorWarning("[修改]");
|
||||
}
|
||||
|
||||
private String tagRenamed() {
|
||||
return colorWarning("[重命名]");
|
||||
}
|
||||
|
||||
/** 引用行:{@code >标签: 值}(冒号后两空格) */
|
||||
private String quoteKv(String key, String value) {
|
||||
return "> " + key + ": " + value;
|
||||
}
|
||||
|
||||
/** 加粗引用行:用于类变更通知头部 */
|
||||
private String quoteKvBold(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("&", "&").replace("<", "<").replace(">", ">");
|
||||
}
|
||||
|
||||
/** POST 企微 Webhook(markdown 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 + "\"";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user