feat: md样式优化
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped

This commit is contained in:
2026-07-14 16:22:05 +08:00
parent cab3fea666
commit 6e06cbf5b5
3 changed files with 62 additions and 30 deletions

View File

@@ -130,9 +130,9 @@ public class ReportBuilder {
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails()); Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
Set<String> newHighlight = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails()); Set<String> newHighlight = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails());
String oldRendered = oldJson.isEmpty() String oldRendered = oldJson.isEmpty()
? "" : "" + SkeletonAnnotator.annotateForWecom(oldJson, oldHighlight) + ""; ? "" : "" + SkeletonAnnotator.annotateOldForWecom(oldJson, oldHighlight) + "";
String newRendered = newJson.isEmpty() String newRendered = newJson.isEmpty()
? "" : "" + SkeletonAnnotator.annotateForWecom(newJson, newHighlight) + ""; ? "" : "" + SkeletonAnnotator.annotateNewForWecom(newJson, newHighlight) + "";
if (oldJson.isEmpty() && !newJson.isEmpty()) { if (oldJson.isEmpty() && !newJson.isEmpty()) {
sb.append(" > **value 新增为:** ").append(newRendered).append("\n\n"); sb.append(" > **value 新增为:** ").append(newRendered).append("\n\n");
} else if (!oldJson.isEmpty() && newJson.isEmpty()) { } else if (!oldJson.isEmpty() && newJson.isEmpty()) {

View File

@@ -10,16 +10,20 @@ import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* 在骨架 JSON 中为改动字段加企微 warning 标注(仅改动片段染色,其余明文)。 * 在骨架 JSON 中为改动字段加企微颜色标注(仅改动片段染色,其余明文)。
* <ul> * <ul>
* <li>删除字段 → 标在旧骨架</li> * <li>删除字段 → 旧骨架,橙色 {@code warning}</li>
* <li>新增字段 / 新增包装层 → 标在新骨架</li> * <li>新增字段 / 新增包装层 → 新骨架,绿色 {@code info}</li>
* <li>路径迁移 → 旧路径标旧骨架、新路径标新骨架</li> * <li>路径迁移 → 旧路径橙、新路径绿</li>
* </ul> * </ul>
*/ */
final class SkeletonAnnotator { final class SkeletonAnnotator {
private static final String FONT_OPEN = "<font color=\"warning\">"; /** 企微橙:删除 / 旧侧变更 */
static final String COLOR_REMOVE = "warning";
/** 企微绿:新增 / 新侧变更 */
static final String COLOR_ADD = "info";
private static final String FONT_CLOSE = "</font>"; private static final String FONT_CLOSE = "</font>";
private SkeletonAnnotator() { private SkeletonAnnotator() {
@@ -66,14 +70,32 @@ final class SkeletonAnnotator {
} }
/** /**
* 标注改动字段:其余正文保持普通文本(避免整段包反引号导致企微整段变红)。 * 标注旧骨架改动字段(删除 → 橙色)。
* 改动片段格式:{@code <font color="warning">"field":value</font>} */
static String annotateOldForWecom(String json, Set<String> highlightPaths) {
return annotateForWecom(json, highlightPaths, COLOR_REMOVE);
}
/**
* 标注新骨架改动字段(新增 → 绿色)。
*/
static String annotateNewForWecom(String json, Set<String> highlightPaths) {
return annotateForWecom(json, highlightPaths, COLOR_ADD);
}
/**
* 仅标注改动字段:其余正文保持普通文本。
* 改动片段格式:{@code <font color="warning|info">"field":value</font>}
*/ */
static String annotateForWecom(String json, Set<String> highlightPaths) { static String annotateForWecom(String json, Set<String> highlightPaths) {
return annotateForWecom(json, highlightPaths, COLOR_REMOVE);
}
static String annotateForWecom(String json, Set<String> highlightPaths, String color) {
if (json == null || json.isEmpty()) { if (json == null || json.isEmpty()) {
return ""; return "";
} }
return annotate(json, highlightPaths); return annotate(json, highlightPaths, color);
} }
/** @deprecated 使用 {@link #annotateForWecom},勿再整段包代码块 */ /** @deprecated 使用 {@link #annotateForWecom},勿再整段包代码块 */
@@ -82,18 +104,27 @@ final class SkeletonAnnotator {
} }
static String annotate(String json, Set<String> highlightPaths) { static String annotate(String json, Set<String> highlightPaths) {
return annotate(json, highlightPaths, COLOR_REMOVE);
}
static String annotate(String json, Set<String> highlightPaths, String color) {
if (json == null || json.isEmpty() || highlightPaths == null || highlightPaths.isEmpty()) { if (json == null || json.isEmpty() || highlightPaths == null || highlightPaths.isEmpty()) {
return json == null ? "" : json; return json == null ? "" : json;
} }
String fontOpen = fontOpen(color);
String result = json; String result = json;
List<String> sorted = new ArrayList<>(highlightPaths); List<String> sorted = new ArrayList<>(highlightPaths);
sorted.sort(Comparator.comparingInt(String::length).reversed()); sorted.sort(Comparator.comparingInt(String::length).reversed());
for (String path : sorted) { for (String path : sorted) {
result = wrapOnePath(result, path); result = wrapOnePath(result, path, fontOpen);
} }
return result; return result;
} }
private static String fontOpen(String color) {
String c = color == null || color.isEmpty() ? COLOR_REMOVE : color;
return "<font color=\"" + c + "\">";
}
private static void addIfPresent(Set<String> paths, String path) { private static void addIfPresent(Set<String> paths, String path) {
if (path != null && !path.trim().isEmpty()) { if (path != null && !path.trim().isEmpty()) {
@@ -101,11 +132,10 @@ final class SkeletonAnnotator {
} }
} }
private static String wrapOnePath(String json, String path) { private static String wrapOnePath(String json, String path, String fontOpen) {
if (path == null || path.isEmpty()) { if (path == null || path.isEmpty()) {
return json; return json;
} }
// 已标注过则跳过(避免重复)
String key = lastSegment(path); String key = lastSegment(path);
if (key.isEmpty()) { if (key.isEmpty()) {
return json; return json;
@@ -114,8 +144,7 @@ final class SkeletonAnnotator {
if (propStart < 0) { if (propStart < 0) {
return json; return json;
} }
// 已在 font 内则跳过 int fontBefore = json.lastIndexOf("<font color=\"", propStart);
int fontBefore = json.lastIndexOf(FONT_OPEN, propStart);
int fontCloseBefore = json.lastIndexOf(FONT_CLOSE, propStart); int fontCloseBefore = json.lastIndexOf(FONT_CLOSE, propStart);
if (fontBefore > fontCloseBefore) { if (fontBefore > fontCloseBefore) {
return json; return json;
@@ -127,11 +156,11 @@ final class SkeletonAnnotator {
return json; return json;
} }
String frag = json.substring(propStart, valueEnd); String frag = json.substring(propStart, valueEnd);
if (frag.contains(FONT_OPEN)) { if (frag.contains("<font color=\"")) {
return json; return json;
} }
return json.substring(0, propStart) return json.substring(0, propStart)
+ FONT_OPEN + frag + FONT_CLOSE + fontOpen + frag + FONT_CLOSE
+ json.substring(valueEnd); + json.substring(valueEnd);
} }
@@ -169,7 +198,6 @@ final class SkeletonAnnotator {
return -1; return -1;
} }
} else if (c == '[') { } else if (c == '[') {
// 数组:进入第一个元素对象
int arrEnd = findMatchingBracket(json, valueStart); int arrEnd = findMatchingBracket(json, valueStart);
int elemObj = json.indexOf('{', valueStart); int elemObj = json.indexOf('{', valueStart);
if (elemObj < 0 || elemObj >= arrEnd) { if (elemObj < 0 || elemObj >= arrEnd) {
@@ -193,7 +221,6 @@ final class SkeletonAnnotator {
if (idx < 0 || idx >= to) { if (idx < 0 || idx >= to) {
return -1; return -1;
} }
// 粗略校验:前面应是 { 或 ,
int p = idx - 1; int p = idx - 1;
while (p >= from && Character.isWhitespace(json.charAt(p))) { while (p >= from && Character.isWhitespace(json.charAt(p))) {
p--; p--;

View File

@@ -71,23 +71,25 @@ class ReportBuilderTest {
String oldPart = md.substring(oldSection, newSection); String oldPart = md.substring(oldSection, newSection);
String newPart = md.substring(newSection); String newPart = md.substring(newSection);
// 旧骨架:无 warning,且不能整段被反引号包裹 // 旧骨架:无染色,且不能整段被反引号包裹
assertFalse(oldPart.contains("<font color=\"warning\">")); assertFalse(oldPart.contains("<font color=\"warning\">"));
assertFalse(oldPart.contains("<font color=\"info\">"));
assertFalse(oldPart.contains("`" + oldJson + "`")); assertFalse(oldPart.contains("`" + oldJson + "`"));
assertTrue(oldPart.contains(oldJson)); assertTrue(oldPart.contains(oldJson));
// 新骨架:新增字段带 warning,其余明文 // 新骨架:新增字段绿色 info,其余明文
assertTrue(newPart.contains("<font color=\"warning\">\"message\":\"\"</font>")); assertTrue(newPart.contains("<font color=\"info\">\"message\":\"\"</font>"));
assertTrue(newPart.contains("<font color=\"warning\">\"lastError\":\"\"</font>")); assertTrue(newPart.contains("<font color=\"info\">\"lastError\":\"\"</font>"));
assertTrue(newPart.contains("\"taskId\":\"\"")); assertTrue(newPart.contains("\"taskId\":\"\""));
assertFalse(newPart.contains("<font color=\"warning\">\"taskId\":\"\"</font>")); assertFalse(newPart.contains("<font color=\"info\">\"taskId\":\"\"</font>"));
assertFalse(newPart.contains("<font color=\"warning\">"));
// 禁止整段代码块 // 禁止整段代码块
assertFalse(newPart.contains("`" + newJson)); assertFalse(newPart.contains("`" + newJson));
assertFalse(md.contains("### P0")); assertFalse(md.contains("### P0"));
} }
@Test @Test
void removedFieldsHighlightedOnlyInOldSkeleton() { void removedFieldsHighlightedOrangeInOldSkeleton() {
SchemaChange removed = new SchemaChange(ChangeType.FIELD_REMOVED); SchemaChange removed = new SchemaChange(ChangeType.FIELD_REMOVED);
removed.setFieldPath("lastError"); removed.setFieldPath("lastError");
removed.setMessage("删除字段 lastError"); removed.setMessage("删除字段 lastError");
@@ -100,10 +102,11 @@ class ReportBuilderTest {
assertEquals(Collections.singleton("lastError"), oldPaths); assertEquals(Collections.singleton("lastError"), oldPaths);
assertTrue(newPaths.isEmpty()); assertTrue(newPaths.isEmpty());
String oldMd = SkeletonAnnotator.annotateForWecom(oldJson, oldPaths); String oldMd = SkeletonAnnotator.annotateOldForWecom(oldJson, oldPaths);
String newMd = SkeletonAnnotator.annotateForWecom(newJson, newPaths); String newMd = SkeletonAnnotator.annotateNewForWecom(newJson, newPaths);
assertTrue(oldMd.contains("<font color=\"warning\">\"lastError\":\"\"</font>")); assertTrue(oldMd.contains("<font color=\"warning\">\"lastError\":\"\"</font>"));
assertFalse(oldMd.contains("<font color=\"warning\">\"taskId\":\"\"</font>")); assertFalse(oldMd.contains("<font color=\"warning\">\"taskId\":\"\"</font>"));
assertFalse(oldMd.contains("<font color=\"info\">"));
assertEquals(newJson, newMd); assertEquals(newJson, newMd);
assertFalse(newMd.startsWith("`")); assertFalse(newMd.startsWith("`"));
} }
@@ -134,6 +137,7 @@ class ReportBuilderTest {
assertTrue(console.contains("======== 字段明细 ========")); assertTrue(console.contains("======== 字段明细 ========"));
assertTrue(console.contains("**删除字段**: x")); assertTrue(console.contains("**删除字段**: x"));
assertTrue(console.contains("<font color=\"warning\">\"x\":\"\"</font>")); assertTrue(console.contains("<font color=\"warning\">\"x\":\"\"</font>"));
assertFalse(console.contains("<font color=\"info\">\"x\":\"\"</font>"));
assertTrue(console.contains("> **位置**: `")); assertTrue(console.contains("> **位置**: `"));
assertTrue(console.contains("> **类型**: `")); assertTrue(console.contains("> **类型**: `"));
assertTrue(console.contains("> **value值由**")); assertTrue(console.contains("> **value值由**"));
@@ -169,9 +173,10 @@ class ReportBuilderTest {
String json = "{\"vo\":{\"dbName\":\"\",\"id\":\"\"},\"expiresAtMs\":0}"; String json = "{\"vo\":{\"dbName\":\"\",\"id\":\"\"},\"expiresAtMs\":0}";
Set<String> paths = new LinkedHashSet<>(); Set<String> paths = new LinkedHashSet<>();
paths.add("vo.dbName"); paths.add("vo.dbName");
String out = SkeletonAnnotator.annotate(json, paths); String out = SkeletonAnnotator.annotate(json, paths, SkeletonAnnotator.COLOR_ADD);
assertTrue(out.contains("<font color=\"warning\">\"dbName\":\"\"</font>")); assertTrue(out.contains("<font color=\"info\">\"dbName\":\"\"</font>"));
assertFalse(out.contains("<font color=\"warning\">\"id\":\"\"</font>")); assertFalse(out.contains("<font color=\"info\">\"id\":\"\"</font>"));
assertFalse(out.contains("<font color=\"warning\">\"dbName\":\"\"</font>"));
} }
@Test @Test