package com.codechecker.api.notify; import com.codechecker.api.model.ApiChangeKind; import com.codechecker.api.model.EndpointChangeReport; import com.codechecker.api.model.ParameterChange; import com.codechecker.common.MarkdownStyles; import com.codechecker.common.WeComMarkdownSender; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * API 变更通知(路径 / 请求方式 / 参数分类型、分条发送,与类变更通知解耦)。 */ public class ApiChangeNotifier { private final WeComMarkdownSender sender = new WeComMarkdownSender(); public int sendAll(String webhookUrl, List reports, String modifier, String modifyTime, boolean wecomEnabled) { if (reports == null || reports.isEmpty()) { System.out.println("无 API 变更,不发送通知"); return 0; } int sent = 0; for (EndpointChangeReport report : reports) { String markdown = buildMarkdown(report, modifier, modifyTime); if (wecomEnabled) { if (sender.send(webhookUrl, markdown)) { sent++; System.out.println("已发送 API 变更通知: " + report.getChangeKind() + " " + report.getHttpMethod() + " " + report.getUri()); } } else { sender.logPreview("API 变更 [" + report.getChangeKind() + "]", markdown); sent++; } } if (sent > 0) { System.out.println("总共发送 " + sent + " 条 API 变更通知"); } return sent; } public String buildMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { ApiChangeKind kind = report.getChangeKind(); if (kind == ApiChangeKind.PATH_CHANGED || kind == ApiChangeKind.NEW_ENDPOINT || kind == ApiChangeKind.REMOVED_ENDPOINT) { return buildPathMarkdown(report, modifier, modifyTime); } if (kind == ApiChangeKind.METHOD_CHANGED) { return buildMethodMarkdown(report, modifier, modifyTime); } return buildParamMarkdown(report, modifier, modifyTime); } private String buildPathMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { String changeLabel; switch (report.getChangeKind()) { case NEW_ENDPOINT: changeLabel = "新增接口"; break; case REMOVED_ENDPOINT: changeLabel = "删除接口"; break; default: changeLabel = "修改路径"; break; } StringBuilder sb = new StringBuilder(); sb.append("# 【API路径变更通知】").append("\n\n"); sb.append(MarkdownStyles.quoteKvBold("变更类型", MarkdownStyles.colorWarning(changeLabel))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("路径", MarkdownStyles.colorInfo(MarkdownStyles.safe(report.getSourceFile())))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("修改人", MarkdownStyles.colorComment(modifier))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("时间", MarkdownStyles.colorComment(modifyTime))).append("\n"); sb.append("\n## 【URI变更详情】").append("\n\n"); sb.append(MarkdownStyles.quoteKvBold("接口说明", formatEndpointDescription(report))).append("\n"); appendPathUriLines(sb, report, changeLabel); return sb.toString(); } private void appendPathUriLines(StringBuilder sb, EndpointChangeReport report, String changeLabel) { if ("新增接口".equals(changeLabel)) { sb.append(MarkdownStyles.quoteKvBold("原路径", "`-`")).append("\n"); sb.append(MarkdownStyles.quoteKvBold("新路径", formatUriWithMethod(report.getHttpMethod(), report.getUri(), true) + " " + MarkdownStyles.colorInfo("[新增]"))).append("\n"); } else if ("删除接口".equals(changeLabel)) { sb.append(MarkdownStyles.quoteKvBold("原路径", formatUriWithMethod(report.getHttpMethod(), report.getUri(), false) + " " + MarkdownStyles.colorWarning("[已删除]"))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("新路径", "`已删除`")).append("\n"); } else { sb.append(MarkdownStyles.quoteKvBold("原路径", formatUriWithMethod(report.getHttpMethod(), report.getOldUri(), false) + " " + MarkdownStyles.colorWarning("[旧路径]"))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("新路径", formatUriWithMethod(report.getHttpMethod(), report.getUri(), true) + " " + MarkdownStyles.colorInfo("[新路径]"))).append("\n"); } } private String buildMethodMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { StringBuilder sb = new StringBuilder(); sb.append("# 【API请求方式变更通知】").append("\n\n"); sb.append(MarkdownStyles.quoteKvBold("变更类型", MarkdownStyles.colorWarning("修改请求方式"))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("路径", MarkdownStyles.colorInfo(MarkdownStyles.safe(report.getSourceFile())))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("修改人", MarkdownStyles.colorComment(modifier))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("时间", MarkdownStyles.colorComment(modifyTime))).append("\n"); sb.append("\n## 【请求方式变更详情】").append("\n\n"); sb.append(MarkdownStyles.quoteKvBold("接口说明", formatEndpointDescription(report))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("URI", MarkdownStyles.colorInfo(report.getUri()))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("原请求方式", MarkdownStyles.colorWarning(report.getOldHttpMethod()))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("新请求方式", MarkdownStyles.colorInfo(report.getHttpMethod()) + " " + MarkdownStyles.colorInfo("[请求方式已变更]"))).append("\n"); return sb.toString(); } private String buildParamMarkdown(EndpointChangeReport report, String modifier, String modifyTime) { StringBuilder sb = new StringBuilder(); sb.append("# 【API参数变更通知】").append("\n\n"); sb.append(MarkdownStyles.quoteKvBold("修改人", MarkdownStyles.colorComment(modifier))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("时间", MarkdownStyles.colorComment(modifyTime))).append("\n"); //sb.append(MarkdownStyles.quoteKvBold("变更类型", MarkdownStyles.colorWarning("修改参数"))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("URI", MarkdownStyles.colorInfo(report.getHttpMethod()) + " " + MarkdownStyles.inlineCode(report.getUri()))).append("\n"); sb.append(MarkdownStyles.quoteKvBold("路径", MarkdownStyles.colorInfo(MarkdownStyles.safe(report.getSourceFile())))).append("\n"); sb.append("\n## 【接口参数变动详情】").append("\n\n"); appendParameterDetails(sb, report); return sb.toString(); } private void appendParameterDetails(StringBuilder sb, EndpointChangeReport report) { List bodyChanges = new ArrayList<>(); List regularChanges = new ArrayList<>(); for (ParameterChange change : report.getParameterChanges()) { if (change.isBodyField()) { bodyChanges.add(change); } else { regularChanges.add(change); } } if (!bodyChanges.isEmpty()) { sb.append("**类对象变更(含嵌套对象字段)**").append("\n\n"); appendBodyGroups(sb, bodyChanges); sb.append("\n"); } if (!regularChanges.isEmpty()) { sb.append("**普通参数变更**").append("\n\n"); sb.append(MarkdownStyles.quoteLine("**共 " + MarkdownStyles.colorWarning(String.valueOf(regularChanges.size())) + " 项变更**")).append("\n\n"); for (ParameterChange change : regularChanges) { sb.append(formatParameterLine(change)).append("\n\n"); } } if (bodyChanges.isEmpty() && regularChanges.isEmpty()) { sb.append(MarkdownStyles.quoteLine(MarkdownStyles.colorComment("无"))).append("\n"); } } private void appendBodyGroups(StringBuilder sb, List bodyChanges) { Map> groups = new LinkedHashMap<>(); for (ParameterChange change : bodyChanges) { String key = change.getParentDto() == null || change.getParentDto().isBlank() ? (change.getBodyParamName() == null ? "body" : change.getBodyParamName()) : change.getParentDto(); groups.computeIfAbsent(key, k -> new ArrayList<>()).add(change); } int total = bodyChanges.size(); sb.append(MarkdownStyles.quoteLine("**共 " + MarkdownStyles.colorWarning(String.valueOf(groups.size())) + " 个类对象 · " + MarkdownStyles.colorWarning(String.valueOf(total)) + " 项变更**")).append("\n\n"); for (List group : groups.values()) { ParameterChange first = group.get(0); if (first.getParentDto() != null && !first.getParentDto().isBlank()) { sb.append("**").append(MarkdownStyles.inlineCode(first.getParentDto())).append("**"); } else if (first.getBodyParamName() != null && !first.getBodyParamName().isBlank()) { sb.append("**").append(MarkdownStyles.inlineCode(first.getBodyParamName())).append("**"); } sb.append("\n\n"); for (ParameterChange change : group) { sb.append(formatParameterLine(change)).append("\n\n"); } } } private String formatParameterLine(ParameterChange change) { String tag; switch (change.getChangeType()) { case ADDED: tag = MarkdownStyles.colorInfo("[新增]"); break; case REMOVED: tag = MarkdownStyles.colorWarning("[删除]"); break; case RENAMED: tag = MarkdownStyles.colorWarning("[重命名]"); break; case MODIFIED: tag = MarkdownStyles.colorWarning("[类型变更]"); break; default: tag = MarkdownStyles.colorWarning("[修改]"); break; } String name = MarkdownStyles.inlineCode(MarkdownStyles.safe(change.displayName())); String desc = change.getDescription() == null || change.getDescription().isBlank() ? MarkdownStyles.colorComment("(无说明)") : MarkdownStyles.colorComment(change.getDescription()); StringBuilder line = new StringBuilder(); if (change.getChangeType() == ParameterChange.ChangeType.RENAMED) { line.append(tag).append(" ") .append(MarkdownStyles.colorComment(MarkdownStyles.safe(change.getOldName()))).append(" → ") .append(MarkdownStyles.colorInfo(MarkdownStyles.safe(change.getParamName()))) .append(" 说明: ").append(desc); } else { line.append(tag).append(" ").append(name).append(" 说明: ").append(desc); } appendParameterType(line, change); return MarkdownStyles.quoteLine(line.toString()); } private void appendParameterType(StringBuilder line, ParameterChange change) { String typePart = resolveTypePart(change); if (!typePart.isBlank()) { line.append(" 类型: ").append(typePart); } } private String formatUriWithMethod(String httpMethod, String uri, boolean isNew) { String path = MarkdownStyles.inlineCode(MarkdownStyles.safe(uri)); if (httpMethod == null || httpMethod.isBlank()) { return path; } String methodPart = isNew ? MarkdownStyles.colorInfo(httpMethod.toUpperCase()) : MarkdownStyles.colorWarning(httpMethod.toUpperCase()); return methodPart + " " + path; } private String formatEndpointDescription(EndpointChangeReport report) { String desc = report.getEndpointDescription(); if (desc == null || desc.isBlank()) { return MarkdownStyles.colorComment("(无说明)"); } return MarkdownStyles.colorComment(MarkdownStyles.safe(desc)); } private String resolveTypePart(ParameterChange change) { if (change.getChangeType() == ParameterChange.ChangeType.MODIFIED && change.getDetail() != null && !change.getDetail().isBlank()) { return MarkdownStyles.formatTypeChange(change.getDetail()); } if (change.getParamType() != null && !change.getParamType().isBlank()) { boolean isNew = change.getChangeType() == ParameterChange.ChangeType.ADDED || change.getChangeType() == ParameterChange.ChangeType.RENAMED; return MarkdownStyles.formatSingleType(change.getParamType(), isNew); } return ""; } }