From 875c0f3ccbba7a7c5bbec089378a0579128e9c98 Mon Sep 17 00:00:00 2001 From: dongzi Date: Mon, 3 Aug 2026 13:40:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20V1.1=20-=20=E5=86=99=E5=85=A5=E7=82=B9?= =?UTF-8?q?=E5=B1=95=E7=A4=BA=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/V1.1-新增写入点展示整改方案.md | 268 ++++++++++++++++++ docs/配置说明.md | 31 +- .../cache/report/ReportBuilder.java | 96 +++++-- .../cache/report/SkeletonAnnotator.java | 11 + .../cache/report/ReportBuilderTest.java | 117 +++++++- 5 files changed, 490 insertions(+), 33 deletions(-) create mode 100644 docs/V1.1-新增写入点展示整改方案.md diff --git a/docs/V1.1-新增写入点展示整改方案.md b/docs/V1.1-新增写入点展示整改方案.md new file mode 100644 index 0000000..c9d9710 --- /dev/null +++ b/docs/V1.1-新增写入点展示整改方案.md @@ -0,0 +1,268 @@ +# V1.1 新增写入点展示 — 整改方案 + +> 版本:v1.1 +> 日期:2026-08-03 +> 状态:**已落地** +> 关联:`docs/配置说明.md` §5、`docs/V1.1-非结构写入收敛整改方案.md`(建点收敛已落地,本方案只改**展示层**) +> 约束:**不新增配置项**;文案与染色为工具默认行为 + +--- + +## 1. 背景与实跑样本 + +### 1.1 企微原文(问题形态) + +```text +- Topic --> duty_im_notice_topic +通道: RocketMQ +位置: DutyDelayQueueImNoticeService#addMq:43 +类型: DutyImNotice +value 新增为: “{"objectId":"","content":"","type":0,"pushStatus":0,"tenantId":"","deleteMark":0,"executeTime":0}” +``` + +含义:对比区间内**首次出现**该 Topic 的投递点(`WRITE_POINT_ADDED`),消息体类型与骨架已正确解析,但展示层体验差。 + +### 1.2 问题拆解 + +| # | 现象 | 影响 | +|---|------|------| +| P1 | 文案「value 新增为」语义含糊 | 看不出是「新增投递 / 首次写入」,与字段级「变更为」易混淆 | +| P2 | 新骨架整段无绿色 | 字段级变更有 ``,新增整 Key/Topic 时全是明文,**结构颜色未渲染** | +| P3 | 缺少「变更类型」明示 | 读者需自行推断这是新增写入点还是结构变更 | +| P4 | (次要)删除写入点文案不对称 | 「value 原结构 + 已删除投递/写入」可读,但可与新增侧统一语气 | + +Redis 侧新增 Key 走同一 `ReportBuilder#renderKeyBlock` 分支(`oldJson` 空、`newJson` 非空),**同病**,须一并改。 + +### 1.3 非目标 + +- 不改检测 / Schema 提取 / Diff 逻辑(建点收敛见另一文档) +- 不新增 YAML 开关 +- 不改字段级变更的片段染色规则(`FIELD_ADDED` 等仍按路径染色) +- 不改企微 4096 拆条策略 + +--- + +## 2. 根因分析 + +### 2.1 调用链 + +```text +SchemaCheckAnalyzer + → WRITE_POINT_ADDED + newSkeletonJson(完整骨架) + → KeyStructureChange.fieldDetails = [WRITE_POINT_ADDED] // 无 fieldPath +ReportBuilder.renderKeyBlock + → pathsForNewSkeleton(details) // 只认 FIELD_ADDED / FIELD_PATH_MOVED + → newGreen = ∅ + → annotateNewForWecom(json, ∅) → 原样明文 + → 文案固定:「value 新增为:** +``` + +### 2.2 根因表 + +| 编号 | 根因 | 证据 | +|------|------|------| +| R1 | 新增写入只有 `ChangeType.WRITE_POINT_ADDED`,**无字段路径** | `SchemaCheckAnalyzer` 建点逻辑 | +| R2 | `SkeletonAnnotator.pathsForNewSkeleton` **不处理** `WRITE_POINT_ADDED` | 仅 `FIELD_ADDED` / `FIELD_PATH_MOVED` | +| R3 | `ReportBuilder` 对「旧空新有」写死 `value 新增为`,且 **不区分 Redis / MQ** | `renderKeyBlock` 约 146~147 行 | +| R4 | 无「变更类型」行;控制台 `formatDetailMessage` 对 MQ 新增前缀也不完整 | `ReportBuilder` | + +结论:**数据已够用(有完整 newJson + 通道 + 类型),缺口全在报告渲染。** + +--- + +## 3. 整改目标 + +1. 一眼可读:这是 **新增投递 / 首次写入该 Key**,不是字段 diff。 +2. 新骨架在企微中有 **绿色** 渲染(整段 `info`)。 +3. Redis / MQ 文案区分清楚,与字段级「value值由 / 变更为」不打架。 +4. 删除写入点展示与新增侧语气对称;控制台明细前缀对齐。 +5. 既有字段级染色回归不变。 + +--- + +## 4. 方案设计 + +### 4.1 判定:何时走「整段新增/删除」展示 + +在 `renderKeyBlock` 中: + +```text +仅当 fieldDetails 全部为 WRITE_POINT_ADDED(或全部为 WRITE_POINT_REMOVED), +且不混有 FIELD_* / WRAPPER_* / TYPE_CHANGED 时 + → 走「整段染色 + 专用文案」 +否则 + → 保持现有片段染色逻辑 +``` + +实跑新增 Topic/Key 场景几乎总是「仅一条 WRITE_POINT_ADDED」,命中上述分支。 + +### 4.2 文案(定稿) + +| 场景 | 通道 | 现行 | 整改后 | +|------|------|------|--------| +| 旧空、新有 | Redis | `value 新增为:` | `首次写入该 Key,value 结构为:` | +| 旧空、新有 | MQ | 同上 | `新增投递,消息体结构为:` | +| 旧有、新空 | Redis | `value 原结构:…(已删除写入)` | `原 value 结构:…(写入点已删除)` | +| 旧有、新空 | MQ | `…(已删除投递)` | `原消息体结构:…(投递点已删除)` | +| 旧有、新有 | 共用 | `value值由` / `变更为` | **保持不变** | + +建议在 meta(位置/类型)之后、骨架之前增加一行: + +```markdown + > **变更类型**: 新增写入点 +``` + +删除侧: + +```markdown + > **变更类型**: 删除写入点 +``` + +字段级变更块不加此行(避免噪音);仅整段新增/删除分支输出。 + +### 4.3 颜色渲染(定稿) + +| 场景 | 染色 | +|------|------| +| 整段新增(仅 WRITE_POINT_ADDED) | 新骨架整段包裹 ``,再外包中文引号 `“…”` | +| 整段删除(仅 WRITE_POINT_REMOVED) | 旧骨架整段包裹 `` | +| 字段级变更 | **不改**:仍按路径片段染色 | + +实现建议(择一,推荐 A): + +**A. ReportBuilder 内直接整段 wrap(简单)** + +```text +if (isWritePointAddedOnly(details) && !newJson.isEmpty()) { + newRendered = "“" + fontInfo(newJson) + "”"; +} +``` + +**B. SkeletonAnnotator 增加 `wrapAll(json, color)`** +供 ReportBuilder 调用,便于单测与复用。 + +本方案采用 **A + 可选抽出 wrap 小方法到 SkeletonAnnotator**,避免改路径匹配算法。 + +企微颜色约定与现网一致: + +| 语义 | color | +|------|-------| +| 新增 / 新结构 | `info`(绿) | +| 删除 / 旧结构 | `warning`(橙) | + +### 4.4 目标企微形态(对照样本) + +整改后期望接近: + +```markdown +- Topic --> `duty_im_notice_topic` + > **通道**: RocketMQ + > **位置**: DutyDelayQueueImNoticeService#addMq:43 + > **类型**: DutyImNotice + > **变更类型**: 新增写入点 + > **新增投递,消息体结构为:** “{"objectId":"","content":"","type":0,"pushStatus":0,"tenantId":"","deleteMark":0,"executeTime":0}” +``` + +Redis 新增 Key 对称示例: + +```markdown +- Key --> `saas:period-config:migration:current` + > **位置**: … + > **类型**: MigrationCurrentVo + > **变更类型**: 新增写入点 + > **首次写入该 Key,value 结构为:** “{…}” +``` + +### 4.5 未解析 Key / Topic(顺带小优化,可选同批) + +若 `keyUnresolved`: + +- 提示由 `(key 无法解析)` / `(destination 未解析)` + 调整为:`(key 无法解析,建议 manual_mappings 补充)`(MQ 用 destination 措辞) +- 表达式长度 > 80 时截断为前 77 字符 + `...`(常量即可,**不进配置**) + +本项为体验增强,可与主改动同 PR;若排期紧可二期。 + +### 4.6 控制台明细 + +`formatDetailMessage` 的 `knownPrefixes` 补齐: + +```text +"新增 MQ 投递点," +"删除 MQ 投递点,原 value 类型: " +``` + +与 analyzer 现有 `setMessage` 对齐,避免 CI 明细不加粗。 + +--- + +## 5. 涉及文件 + +| 文件 | 变更 | +|------|------| +| `report/ReportBuilder.java` | 文案分支、变更类型行、整段染色、可选未解析截断、prefixes | +| `report/SkeletonAnnotator.java` | 可选:`wrapEntire(json, color)` | +| `report/ReportBuilderTest.java` | 新增 WRITE_POINT_ADDED(MQ/Redis)展示断言;字段级染色回归 | +| `docs/配置说明.md` §5 | 同步新增/删除写入点文案与整段绿色约定 | + +**不改**:`SchemaCheckAnalyzer` 建点逻辑、`SkeletonJsonRenderer`、检测器。 + +--- + +## 6. 测试计划 + +| 用例 | 输入要点 | 期望 | +|------|----------|------| +| T1 | 仅 `WRITE_POINT_ADDED` + MQ + 非空 newJson(对齐 duty_im 样本) | 含「新增投递,消息体结构为」;含「变更类型: 新增写入点」;newJson 外包 `` | +| T2 | 仅 `WRITE_POINT_ADDED` + Redis | 含「首次写入该 Key,value 结构为」;整段绿 | +| T3 | 仅 `WRITE_POINT_REMOVED` + 非空 oldJson | 「原…结构」+ 整段 `` | +| T4 | `FIELD_ADDED` 字段级变更(既有) | 仍为片段绿,**不**整段包 info;文案仍为 value值由/变更为 | +| T5 | `toConsole` 含「新增 MQ 投递点」 | 明细前缀加粗正确 | +| T6 | (若做)超长未解析 key 表达式 | 截断 + mapping 提示 | + +可用最小化 `CheckReport` / `KeyStructureChange` 构造,不必起 git;骨架 JSON 可用样本中的 DutyImNotice 字段串。 + +--- + +## 7. 风险与回滚 + +| 风险 | 缓解 | +|------|------| +| 整段绿色在超长 JSON 下刺眼 | 骨架已有 maxLen;新增点通常可接受 | +| 企微对超长 font 标签不友好 | 保持 4096 拆条;单块过长仍按 key 拆 | +| 文案变更影响阅读习惯 | 发布说明给对照表(§4.2) | +| 误把字段级变更整段染色 | 严格 `isWritePointAddedOnly` 判定 | + +回滚:还原 `ReportBuilder` / `SkeletonAnnotator` 展示逻辑即可,与检测无关。 + +--- + +## 8. 与「非结构收敛」的边界 + +| | 非结构写入收敛(已落地) | 本方案(展示) | +|--|-------------------------|----------------| +| 解决什么 | 不该建的点(String/`{}`)不再告警 | **该告的点**怎么读得懂、看得见绿 | +| 样本 | RecordingTodo 原文缓存 | DutyImNotice 新增投递(类型/骨架正确) | +| 落点 | Detector + Analyzer | ReportBuilder(+ Annotator) | + +两者互补:收敛后留下的新增写入点,更需要本方案的文案与整段染色。 + +--- + +## 9. 实施检查表 + +- [x] 认可 §4.2 文案与 §4.3 整段染色(不新增配置) +- [x] 同批做未解析 Key 截断(§4.5) +- [x] 代码落地 + T1~T6 +- [x] 同步 `docs/配置说明.md` §5 +- [x] 全量 `mvn test` + +--- + +## 10. 文案对照速查 + +| 场景 | V1.0 | V1.1(本方案) | +|------|------|----------------| +| MQ 新增投递 | `value 新增为:“{…}”`(无色) | `变更类型: 新增写入点` + `新增投递,消息体结构为:“{…}”` | +| Redis 新增写入 | 同上 | `首次写入该 Key,value 结构为:“{…}”` | +| 字段级变更 | `value值由` / `变更为` + 片段染色 | 保持 | diff --git a/docs/配置说明.md b/docs/配置说明.md index 9594ebf..68b697a 100644 --- a/docs/配置说明.md +++ b/docs/配置说明.md @@ -332,9 +332,11 @@ suppressions: | 字段删除(标在旧骨架) | 橙色 | `` | | 字段新增 / 包装层(标在新骨架) | 绿色 | `` | | 路径迁移 | 旧橙 / 新绿 | 同上 | -| key 未解析提示 | 灰色 | `(key 未解析)` | +| **新增写入点**(整段新骨架) | 绿色 | 整段 `` | +| **删除写入点**(整段旧骨架) | 橙色 | 整段 `` | +| key 未解析提示 | 灰色 | `(key 无法解析,建议 manual_mappings 补充)` | -### 5.3 示例(已解析 key) +### 5.3 示例(字段级变更) ```markdown ## [序列化结构变更] jnpf-java-cloud @@ -351,19 +353,36 @@ suppressions: > **变更为:** “…(仅新增/迁移字段片段带 绿色)…” ``` -实际发送时仅对改动属性片段染色:新增 → `info`(绿),删除 → `warning`(橙)。 +字段级变更仍对改动属性片段染色:新增 → `info`(绿),删除 → `warning`(橙)。 -### 5.4 示例(key 未解析) +### 5.4 示例(新增写入点 / 新增投递) ```markdown -- Key --> `req.getKey()` (key 未解析) +- Topic --> `duty_im_notice_topic` + > **通道**: RocketMQ + > **位置**: DutyDelayQueueImNoticeService#addMq:43 + > **类型**: DutyImNotice + > **变更类型**: 新增写入点 + > **新增投递,消息体结构为:** “{"objectId":"","content":"",…}” + +- Key --> `saas:period-config:migration:current` + > **位置**: SaasPeriodConfigMigrationRedisSupport#putCurrent:41 + > **类型**: MigrationCurrentVo + > **变更类型**: 新增写入点 + > **首次写入该 Key,value 结构为:** “{…}” +``` + +### 5.5 示例(key 未解析) + +```markdown +- Key --> `req.getKey()` (key 无法解析,建议 manual_mappings 补充) > **位置**: `ClockInXxxService#export:128` > **类型**: `List` > **value值由:** “{"a":""}” > **变更为:** “…新增字段带绿色高亮…” ``` -未解析时按「写入位置 + key 表达式」拆分聚合,避免多个未知 key 串在一起。 +未解析时按「写入位置 + key 表达式」拆分聚合;表达式过长(>80)会截断。 --- diff --git a/src/main/java/com/codechecker/cache/report/ReportBuilder.java b/src/main/java/com/codechecker/cache/report/ReportBuilder.java index ce67492..603aa4f 100644 --- a/src/main/java/com/codechecker/cache/report/ReportBuilder.java +++ b/src/main/java/com/codechecker/cache/report/ReportBuilder.java @@ -17,7 +17,8 @@ import java.util.Set; * 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。 *
    *
  • 企微:按 key 展示位置/类型/序列化骨架变更;类型变更另附摘要行,不分 P0/P1/P2
  • - *
  • 未解析 key 展示源码表达式 + 灰色「key 无法解析」提示
  • + *
  • 新增/删除写入点:专用文案 + 骨架整段染色(绿/橙)
  • + *
  • 未解析 key 展示截断后的源码表达式 + 灰色 mapping 提示
  • *
  • 多 key 优先拼成一条;超过企微上限则按 key 拆成多条
  • *
  • CI:先打字段明细,再完整输出企微 Markdown(拆分后的每条)
  • *
@@ -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 oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails()); - Set wrappersRemoved = SkeletonAnnotator.pathsForWrapperRemoved(kc.getFieldDetails()); - Set newGreen = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails()); - Set wrappersAdded = SkeletonAnnotator.pathsForWrapperAdded(kc.getFieldDetails()); - Set 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 oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails()); + Set wrappersRemoved = SkeletonAnnotator.pathsForWrapperRemoved(kc.getFieldDetails()); + Set newGreen = SkeletonAnnotator.pathsForNewSkeleton(kc.getFieldDetails()); + Set wrappersAdded = SkeletonAnnotator.pathsForWrapperAdded(kc.getFieldDetails()); + Set 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 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 BigDecimal → Integer} @@ -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 - ? " (destination 未解析)" - : " (key 无法解析)"); + ? " (destination 未解析,建议 manual_mappings 补充)" + : " (key 无法解析,建议 manual_mappings 补充)"); } 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(" (key 无法解析)"); + sb.append(" (key 无法解析,建议 manual_mappings 补充)"); } 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)) { diff --git a/src/main/java/com/codechecker/cache/report/SkeletonAnnotator.java b/src/main/java/com/codechecker/cache/report/SkeletonAnnotator.java index bae1ba0..d26e9a3 100644 --- a/src/main/java/com/codechecker/cache/report/SkeletonAnnotator.java +++ b/src/main/java/com/codechecker/cache/report/SkeletonAnnotator.java @@ -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 "" + json + FONT_CLOSE; + } + /** @deprecated 使用 {@link #annotateForWecom},勿再整段包代码块 */ static String annotateAsCodeBlock(String json, Set highlightPaths) { return annotateForWecom(json, highlightPaths); diff --git a/src/test/java/com/codechecker/cache/report/ReportBuilderTest.java b/src/test/java/com/codechecker/cache/report/ReportBuilderTest.java index cbd504d..0a0476c 100644 --- a/src/test/java/com/codechecker/cache/report/ReportBuilderTest.java +++ b/src/test/java/com/codechecker/cache/report/ReportBuilderTest.java @@ -362,12 +362,127 @@ class ReportBuilderTest { report.getKeyChanges().add(key); String md = new ReportBuilder("[序列化结构变更]").toMarkdown(report); - assertTrue(md.contains("- Key --> `req.getKey()` (key 无法解析)")); + assertTrue(md.contains("- Key --> `req.getKey()` (key 无法解析,建议 manual_mappings 补充)")); assertTrue(md.contains("> **位置**: ClockInXxxService#export:128")); assertTrue(md.contains("> **类型**: List")); 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("" + skeleton + ""), 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("" + skeleton + ""), 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("" + skeleton + ""), 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("\"b\":\"\""), md); + assertFalse(md.contains("{\"a\":\"\",\"b\":\"\"}"), 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}";