feat: V1.1 - 写入点展示修复
This commit is contained in:
@@ -17,7 +17,8 @@ import java.util.Set;
|
||||
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
||||
* <ul>
|
||||
* <li>企微:按 key 展示位置/类型/序列化骨架变更;类型变更另附摘要行,不分 P0/P1/P2</li>
|
||||
* <li>未解析 key 展示源码表达式 + 灰色「key 无法解析」提示</li>
|
||||
* <li>新增/删除写入点:专用文案 + 骨架整段染色(绿/橙)</li>
|
||||
* <li>未解析 key 展示截断后的源码表达式 + 灰色 mapping 提示</li>
|
||||
* <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li>
|
||||
* <li>CI:先打字段明细,再完整输出企微 Markdown(拆分后的每条)</li>
|
||||
* </ul>
|
||||
@@ -26,6 +27,8 @@ public class ReportBuilder {
|
||||
|
||||
/** 企微 markdown content 字节上限(UTF-8)。 */
|
||||
public static final int WECOM_MARKDOWN_MAX_BYTES = 4096;
|
||||
/** 未解析 destination 表达式展示最大字符数(超出截断)。 */
|
||||
static final int UNRESOLVED_KEY_DISPLAY_MAX = 80;
|
||||
|
||||
private final String titlePrefix;
|
||||
|
||||
@@ -130,33 +133,66 @@ public class ReportBuilder {
|
||||
sb.append(" > **通道**: ").append(kc.channelDisplay()).append('\n');
|
||||
}
|
||||
appendMetaLines(sb, kc.getWriteLocation(), kc.getValueType());
|
||||
|
||||
String oldJson = nvl(kc.getOldSkeletonJson());
|
||||
String newJson = nvl(kc.getNewSkeletonJson());
|
||||
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
|
||||
Set<String> wrappersRemoved = SkeletonAnnotator.pathsForWrapperRemoved(kc.getFieldDetails());
|
||||
Set<String> newGreen = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails());
|
||||
Set<String> wrappersAdded = SkeletonAnnotator.pathsForWrapperAdded(kc.getFieldDetails());
|
||||
Set<String> typeGray = SkeletonAnnotator.pathsForTypeChanged(kc.getFieldDetails());
|
||||
String oldRendered = oldJson.isEmpty()
|
||||
? "" : "“" + SkeletonAnnotator.annotateOldForWecom(
|
||||
oldJson, oldHighlight, typeGray, wrappersRemoved) + "”";
|
||||
String newRendered = newJson.isEmpty()
|
||||
? "" : "“" + SkeletonAnnotator.annotateNewForWecom(
|
||||
newJson, newGreen, typeGray, wrappersAdded) + "”";
|
||||
if (oldJson.isEmpty() && !newJson.isEmpty()) {
|
||||
sb.append(" > **value 新增为:** ").append(newRendered).append('\n');
|
||||
} else if (!oldJson.isEmpty() && newJson.isEmpty()) {
|
||||
sb.append(" > **value 原结构:** ").append(oldRendered)
|
||||
.append(kc.isMq() ? "(已删除投递)\n" : "(已删除写入)\n");
|
||||
boolean addedOnly = isOnlyChangeType(kc.getFieldDetails(), ChangeType.WRITE_POINT_ADDED);
|
||||
boolean removedOnly = isOnlyChangeType(kc.getFieldDetails(), ChangeType.WRITE_POINT_REMOVED);
|
||||
|
||||
if (oldJson.isEmpty() && !newJson.isEmpty() && addedOnly) {
|
||||
sb.append(" > **变更类型**: 新增写入点\n");
|
||||
String label = kc.isMq() ? "新增投递,消息体结构为" : "首次写入该 Key,value 结构为";
|
||||
sb.append(" > **").append(label).append(":** “")
|
||||
.append(SkeletonAnnotator.wrapEntire(newJson, SkeletonAnnotator.COLOR_ADD))
|
||||
.append("”\n");
|
||||
} else if (!oldJson.isEmpty() && newJson.isEmpty() && removedOnly) {
|
||||
sb.append(" > **变更类型**: 删除写入点\n");
|
||||
String label = kc.isMq() ? "原消息体结构" : "原 value 结构";
|
||||
String suffix = kc.isMq() ? "(投递点已删除)" : "(写入点已删除)";
|
||||
sb.append(" > **").append(label).append(":** “")
|
||||
.append(SkeletonAnnotator.wrapEntire(oldJson, SkeletonAnnotator.COLOR_REMOVE))
|
||||
.append("”").append(suffix).append('\n');
|
||||
} else {
|
||||
sb.append(" > **value值由:** ").append(oldRendered).append('\n');
|
||||
sb.append(" > **变更为:** ").append(newRendered).append('\n');
|
||||
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
|
||||
Set<String> wrappersRemoved = SkeletonAnnotator.pathsForWrapperRemoved(kc.getFieldDetails());
|
||||
Set<String> newGreen = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails());
|
||||
Set<String> wrappersAdded = SkeletonAnnotator.pathsForWrapperAdded(kc.getFieldDetails());
|
||||
Set<String> typeGray = SkeletonAnnotator.pathsForTypeChanged(kc.getFieldDetails());
|
||||
String oldRendered = oldJson.isEmpty()
|
||||
? "" : "“" + SkeletonAnnotator.annotateOldForWecom(
|
||||
oldJson, oldHighlight, typeGray, wrappersRemoved) + "”";
|
||||
String newRendered = newJson.isEmpty()
|
||||
? "" : "“" + SkeletonAnnotator.annotateNewForWecom(
|
||||
newJson, newGreen, typeGray, wrappersAdded) + "”";
|
||||
if (oldJson.isEmpty() && !newJson.isEmpty()) {
|
||||
// 兜底:有新骨架但明细不是纯 WRITE_POINT_ADDED
|
||||
sb.append(" > **首次写入,value 结构为:** ").append(newRendered).append('\n');
|
||||
} else if (!oldJson.isEmpty() && newJson.isEmpty()) {
|
||||
sb.append(" > **原 value 结构:** ").append(oldRendered)
|
||||
.append(kc.isMq() ? "(投递点已删除)\n" : "(写入点已删除)\n");
|
||||
} else {
|
||||
sb.append(" > **value值由:** ").append(oldRendered).append('\n');
|
||||
sb.append(" > **变更为:** ").append(newRendered).append('\n');
|
||||
}
|
||||
appendTypeChangeSummary(sb, kc.getFieldDetails());
|
||||
}
|
||||
appendTypeChangeSummary(sb, kc.getFieldDetails());
|
||||
sb.append('\n');
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/** 明细非空且全部为同一变更类型(用于整段新增/删除展示)。 */
|
||||
private static boolean isOnlyChangeType(List<SchemaChange> details, ChangeType expected) {
|
||||
if (details == null || details.isEmpty() || expected == null) {
|
||||
return false;
|
||||
}
|
||||
for (SchemaChange c : details) {
|
||||
if (c == null || c.getChangeType() != expected) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型变更单独一行摘要,避免仅靠骨架颜色/占位难以识别。
|
||||
* 例:{@code > **类型变更**: amount <font color="warning">BigDecimal → Integer</font>}
|
||||
@@ -230,7 +266,7 @@ public class ReportBuilder {
|
||||
String label = mq ? "Topic -->" : "Key -->";
|
||||
String keyText = kc.isKeyUnresolved() && kc.getKeyExpression() != null
|
||||
&& !kc.getKeyExpression().trim().isEmpty()
|
||||
? kc.getKeyExpression().trim()
|
||||
? truncateDisplay(kc.getKeyExpression().trim())
|
||||
: nvl(kc.displayKey());
|
||||
if (keyText.isEmpty()) {
|
||||
keyText = "unknown-key";
|
||||
@@ -238,8 +274,8 @@ public class ReportBuilder {
|
||||
sb.append("- ").append(label).append(" `").append(escapeWeComCode(keyText)).append('`');
|
||||
if (kc.isKeyUnresolved()) {
|
||||
sb.append(mq
|
||||
? " <font color=\"comment\">(destination 未解析)</font>"
|
||||
: " <font color=\"comment\">(key 无法解析)</font>");
|
||||
? " <font color=\"comment\">(destination 未解析,建议 manual_mappings 补充)</font>"
|
||||
: " <font color=\"comment\">(key 无法解析,建议 manual_mappings 补充)</font>");
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
@@ -250,18 +286,25 @@ public class ReportBuilder {
|
||||
private void appendKeyLine(StringBuilder sb, String displayKey, String keyExpression,
|
||||
boolean unresolved) {
|
||||
String keyText = unresolved && keyExpression != null && !keyExpression.trim().isEmpty()
|
||||
? keyExpression.trim()
|
||||
? truncateDisplay(keyExpression.trim())
|
||||
: nvl(displayKey);
|
||||
if (keyText.isEmpty()) {
|
||||
keyText = "unknown-key";
|
||||
}
|
||||
sb.append("- Key --> `").append(escapeWeComCode(keyText)).append('`');
|
||||
if (unresolved) {
|
||||
sb.append(" <font color=\"comment\">(key 无法解析)</font>");
|
||||
sb.append(" <font color=\"comment\">(key 无法解析,建议 manual_mappings 补充)</font>");
|
||||
}
|
||||
sb.append('\n');
|
||||
}
|
||||
|
||||
static String truncateDisplay(String text) {
|
||||
if (text == null || text.length() <= UNRESOLVED_KEY_DISPLAY_MAX) {
|
||||
return text == null ? "" : text;
|
||||
}
|
||||
return text.substring(0, UNRESOLVED_KEY_DISPLAY_MAX - 3) + "...";
|
||||
}
|
||||
|
||||
/**
|
||||
* 企微 markdown 代码片段内的转义:{@code *} 会触发斜体(即使包在反引号里)。
|
||||
*/
|
||||
@@ -361,7 +404,8 @@ public class ReportBuilder {
|
||||
}
|
||||
String[] knownPrefixes = {
|
||||
"删除字段 ", "新增字段 ", "字段路径迁移 ", "新增包装层 ", "删除包装层 ",
|
||||
"新增缓存写入点,", "删除缓存写入点,原 value 类型: "
|
||||
"新增缓存写入点,", "删除缓存写入点,原 value 类型: ",
|
||||
"新增 MQ 投递点,", "删除 MQ 投递点,原 value 类型: "
|
||||
};
|
||||
for (String prefix : knownPrefixes) {
|
||||
if (trimmed.startsWith(prefix)) {
|
||||
|
||||
@@ -176,6 +176,17 @@ final class SkeletonAnnotator {
|
||||
return annotate(json, highlightPaths, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* 整段骨架染色(新增/删除写入点场景):整 JSON 包一层 font。
|
||||
*/
|
||||
static String wrapEntire(String json, String color) {
|
||||
if (json == null || json.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
String c = color == null || color.isEmpty() ? COLOR_ADD : color;
|
||||
return "<font color=\"" + c + "\">" + json + FONT_CLOSE;
|
||||
}
|
||||
|
||||
/** @deprecated 使用 {@link #annotateForWecom},勿再整段包代码块 */
|
||||
static String annotateAsCodeBlock(String json, Set<String> highlightPaths) {
|
||||
return annotateForWecom(json, highlightPaths);
|
||||
|
||||
@@ -362,12 +362,127 @@ class ReportBuilderTest {
|
||||
report.getKeyChanges().add(key);
|
||||
|
||||
String md = new ReportBuilder("[序列化结构变更]").toMarkdown(report);
|
||||
assertTrue(md.contains("- Key --> `req.getKey()` <font color=\"comment\">(key 无法解析)</font>"));
|
||||
assertTrue(md.contains("- Key --> `req.getKey()` <font color=\"comment\">(key 无法解析,建议 manual_mappings 补充)</font>"));
|
||||
assertTrue(md.contains("> **位置**: ClockInXxxService#export:128"));
|
||||
assertTrue(md.contains("> **类型**: List<ClockInExportVo>"));
|
||||
assertFalse(md.contains("`unknown-key`"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void writePointAddedMqShowsFriendlyCopyAndWholeSkeletonGreen() {
|
||||
String skeleton = "{\"objectId\":\"\",\"content\":\"\",\"type\":0,\"pushStatus\":0,"
|
||||
+ "\"tenantId\":\"\",\"deleteMark\":0,\"executeTime\":0}";
|
||||
CheckReport report = baseReport();
|
||||
SchemaChange added = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
|
||||
added.setMessage("新增 MQ 投递点,value 类型: DutyImNotice");
|
||||
added.setKeyPattern("duty_im_notice_topic");
|
||||
report.getChanges().add(added);
|
||||
|
||||
KeyStructureChange key = new KeyStructureChange();
|
||||
key.setKeyPattern("duty_im_notice_topic");
|
||||
key.setChannel("ROCKETMQ");
|
||||
key.setWriteLocation("DutyDelayQueueImNoticeService#addMq:43");
|
||||
key.setValueType("DutyImNotice");
|
||||
key.setOldSkeletonJson("");
|
||||
key.setNewSkeletonJson(skeleton);
|
||||
key.getFieldDetails().add(added);
|
||||
report.getKeyChanges().add(key);
|
||||
|
||||
String md = new ReportBuilder("【序列化结构变更】").toMarkdown(report);
|
||||
assertTrue(md.contains("- Topic --> `duty_im_notice_topic`"), md);
|
||||
assertTrue(md.contains("> **通道**: RocketMQ"), md);
|
||||
assertTrue(md.contains("> **变更类型**: 新增写入点"), md);
|
||||
assertTrue(md.contains("> **新增投递,消息体结构为:**"), md);
|
||||
assertTrue(md.contains("<font color=\"info\">" + skeleton + "</font>"), md);
|
||||
assertFalse(md.contains("value 新增为"), md);
|
||||
|
||||
String console = new ReportBuilder("【序列化结构变更】").toConsole(report);
|
||||
assertTrue(console.contains("**新增 MQ 投递点**: value 类型: DutyImNotice"), console);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writePointAddedRedisShowsFriendlyCopyAndWholeSkeletonGreen() {
|
||||
String skeleton = "{\"taskId\":\"\",\"status\":\"\"}";
|
||||
CheckReport report = baseReport();
|
||||
SchemaChange added = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
|
||||
added.setMessage("新增缓存写入点,value 类型: MigrationCurrentVo");
|
||||
|
||||
KeyStructureChange key = new KeyStructureChange();
|
||||
key.setKeyPattern("saas:period-config:migration:current");
|
||||
key.setWriteLocation("SaasPeriodConfigMigrationRedisSupport#putCurrent:41");
|
||||
key.setValueType("MigrationCurrentVo");
|
||||
key.setOldSkeletonJson("");
|
||||
key.setNewSkeletonJson(skeleton);
|
||||
key.getFieldDetails().add(added);
|
||||
report.getKeyChanges().add(key);
|
||||
|
||||
String md = new ReportBuilder("[序列化结构变更]").toMarkdown(report);
|
||||
assertTrue(md.contains("> **变更类型**: 新增写入点"), md);
|
||||
assertTrue(md.contains("> **首次写入该 Key,value 结构为:**"), md);
|
||||
assertTrue(md.contains("<font color=\"info\">" + skeleton + "</font>"), md);
|
||||
assertFalse(md.contains("value 新增为"), md);
|
||||
}
|
||||
|
||||
@Test
|
||||
void writePointRemovedHighlightsWholeOldSkeletonOrange() {
|
||||
String skeleton = "{\"objectId\":\"\",\"content\":\"\"}";
|
||||
CheckReport report = baseReport();
|
||||
SchemaChange removed = new SchemaChange(ChangeType.WRITE_POINT_REMOVED);
|
||||
removed.setMessage("删除 MQ 投递点,原 value 类型: DutyImNotice");
|
||||
|
||||
KeyStructureChange key = new KeyStructureChange();
|
||||
key.setKeyPattern("duty_im_notice_topic");
|
||||
key.setChannel("ROCKETMQ");
|
||||
key.setWriteLocation("DutyDelayQueueImNoticeService#addMq:43");
|
||||
key.setValueType("DutyImNotice");
|
||||
key.setOldSkeletonJson(skeleton);
|
||||
key.setNewSkeletonJson("");
|
||||
key.getFieldDetails().add(removed);
|
||||
report.getKeyChanges().add(key);
|
||||
|
||||
String md = new ReportBuilder("[序列化结构变更]").toMarkdown(report);
|
||||
assertTrue(md.contains("> **变更类型**: 删除写入点"), md);
|
||||
assertTrue(md.contains("> **原消息体结构:**"), md);
|
||||
assertTrue(md.contains("<font color=\"warning\">" + skeleton + "</font>"), md);
|
||||
assertTrue(md.contains("(投递点已删除)"), md);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fieldLevelChangeStillUsesFragmentHighlightNotWholeWrap() {
|
||||
CheckReport report = baseReport();
|
||||
report.getKeyChanges().add(simpleKey("k1", "{\"a\":\"\"}", "{\"a\":\"\",\"b\":\"\"}", "b"));
|
||||
String md = new ReportBuilder("[序列化结构变更]").toMarkdown(report);
|
||||
assertTrue(md.contains("> **value值由:**"), md);
|
||||
assertTrue(md.contains("> **变更为:**"), md);
|
||||
assertTrue(md.contains("<font color=\"info\">\"b\":\"\"</font>"), md);
|
||||
assertFalse(md.contains("<font color=\"info\">{\"a\":\"\",\"b\":\"\"}</font>"), md);
|
||||
assertFalse(md.contains("变更类型"), md);
|
||||
}
|
||||
|
||||
@Test
|
||||
void unresolvedLongExpressionTruncated() {
|
||||
StringBuilder expr = new StringBuilder("prefix.");
|
||||
while (expr.length() < 100) {
|
||||
expr.append("getVeryLongMethodName()");
|
||||
}
|
||||
CheckReport report = baseReport();
|
||||
KeyStructureChange key = new KeyStructureChange();
|
||||
key.setKeyPattern("unknown-key");
|
||||
key.setKeyExpression(expr.toString());
|
||||
key.setKeyUnresolved(true);
|
||||
key.setOldSkeletonJson("{}");
|
||||
key.setNewSkeletonJson("{\"a\":\"\"}");
|
||||
SchemaChange added = new SchemaChange(ChangeType.FIELD_ADDED);
|
||||
added.setFieldPath("a");
|
||||
key.getFieldDetails().add(added);
|
||||
report.getKeyChanges().add(key);
|
||||
|
||||
String md = new ReportBuilder("[序列化结构变更]").toMarkdown(report);
|
||||
assertTrue(md.contains("...`"), "应截断并以 ... 结尾,实际:\n" + md);
|
||||
assertTrue(md.contains("建议 manual_mappings 补充"), md);
|
||||
assertFalse(md.contains(expr.toString()), "完整超长表达式不应出现");
|
||||
}
|
||||
|
||||
@Test
|
||||
void pathAwareNestedHighlight() {
|
||||
String json = "{\"vo\":{\"dbName\":\"\",\"id\":\"\"},\"expiresAtMs\":0}";
|
||||
|
||||
Reference in New Issue
Block a user