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 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 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 + "\""; } }