feat: 新增对 未知key 的处理逻辑
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped
All checks were successful
缓存序列化结构检查 / cache-schema-check (push) Has been skipped
This commit is contained in:
@@ -135,19 +135,18 @@ public class SchemaCheckAnalyzer {
|
|||||||
oldSchema.getConfidence(), newSchema.getConfidence());
|
oldSchema.getConfidence(), newSchema.getConfidence());
|
||||||
enrich(changes, nw, confidence);
|
enrich(changes, nw, confidence);
|
||||||
allChanges.addAll(changes);
|
allChanges.addAll(changes);
|
||||||
mergeKeyChange(keyChanges, nw.getResolvedKeyPattern(), changes,
|
mergeKeyChange(keyChanges, nw, changes,
|
||||||
skeletonRenderer.render(oldSchema, protectedPaths(changes),
|
skeletonRenderer.render(oldSchema, protectedPaths(changes),
|
||||||
SkeletonJsonRenderer.DEFAULT_MAX_LEN),
|
SkeletonJsonRenderer.DEFAULT_MAX_LEN),
|
||||||
skeletonRenderer.render(newSchema, protectedPaths(changes),
|
skeletonRenderer.render(newSchema, protectedPaths(changes),
|
||||||
SkeletonJsonRenderer.DEFAULT_MAX_LEN));
|
SkeletonJsonRenderer.DEFAULT_MAX_LEN));
|
||||||
} else if (fileChanged) {
|
} else if (fileChanged) {
|
||||||
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
|
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_ADDED);
|
||||||
c.setKeyPattern(nw.getResolvedKeyPattern());
|
fillFromWritePoint(c, nw);
|
||||||
c.setWriteLocation(nw.location());
|
c.setMessage("新增缓存写入点,value 类型: " + displayType(nw));
|
||||||
c.setMessage("新增缓存写入点,value 类型: " + shortType(nw.getResolvedValueType()));
|
|
||||||
allChanges.add(c);
|
allChanges.add(c);
|
||||||
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
|
TypeSchema newSchema = extractorNew.extract(nw.getResolvedValueType(), nw.isRootArray());
|
||||||
mergeKeyChange(keyChanges, nw.getResolvedKeyPattern(),
|
mergeKeyChange(keyChanges, nw,
|
||||||
Collections.singletonList(c),
|
Collections.singletonList(c),
|
||||||
"",
|
"",
|
||||||
skeletonRenderer.render(newSchema, protectedPaths(
|
skeletonRenderer.render(newSchema, protectedPaths(
|
||||||
@@ -161,12 +160,11 @@ public class SchemaCheckAnalyzer {
|
|||||||
if (!newSigs.contains(ow.signature())
|
if (!newSigs.contains(ow.signature())
|
||||||
&& !isKeyIgnored(ow.getResolvedKeyPattern()) && !isWriterIgnored(ow)) {
|
&& !isKeyIgnored(ow.getResolvedKeyPattern()) && !isWriterIgnored(ow)) {
|
||||||
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_REMOVED);
|
SchemaChange c = new SchemaChange(ChangeType.WRITE_POINT_REMOVED);
|
||||||
c.setKeyPattern(ow.getResolvedKeyPattern());
|
fillFromWritePoint(c, ow);
|
||||||
c.setWriteLocation(ow.location());
|
c.setMessage("删除缓存写入点,原 value 类型: " + displayType(ow));
|
||||||
c.setMessage("删除缓存写入点,原 value 类型: " + shortType(ow.getResolvedValueType()));
|
|
||||||
allChanges.add(c);
|
allChanges.add(c);
|
||||||
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
|
TypeSchema oldSchema = extractorOld.extract(ow.getResolvedValueType(), ow.isRootArray());
|
||||||
mergeKeyChange(keyChanges, ow.getResolvedKeyPattern(),
|
mergeKeyChange(keyChanges, ow,
|
||||||
Collections.singletonList(c),
|
Collections.singletonList(c),
|
||||||
skeletonRenderer.render(oldSchema, protectedPaths(
|
skeletonRenderer.render(oldSchema, protectedPaths(
|
||||||
Collections.singletonList(c)),
|
Collections.singletonList(c)),
|
||||||
@@ -178,19 +176,31 @@ public class SchemaCheckAnalyzer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<SchemaChange> finalChanges = postProcess(allChanges);
|
List<SchemaChange> finalChanges = postProcess(allChanges);
|
||||||
// 按最终字段明细重新对齐 key 摘要(过滤被 suppress/dedup 掉的)
|
|
||||||
Map<String, KeyStructureChange> finalKeys = filterKeyChanges(keyChanges, finalChanges);
|
Map<String, KeyStructureChange> finalKeys = filterKeyChanges(keyChanges, finalChanges);
|
||||||
return buildReport(oldSha, newSha, finalChanges, finalKeys);
|
return buildReport(oldSha, newSha, finalChanges, finalKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void mergeKeyChange(Map<String, KeyStructureChange> keyChanges, String keyPattern,
|
private void mergeKeyChange(Map<String, KeyStructureChange> keyChanges, WritePoint wp,
|
||||||
List<SchemaChange> changes, String oldSkeleton, String newSkeleton) {
|
List<SchemaChange> changes, String oldSkeleton, String newSkeleton) {
|
||||||
String key = keyPattern == null ? "<unknown>" : keyPattern;
|
String aggKey = aggregationKey(wp);
|
||||||
KeyStructureChange kc = keyChanges.computeIfAbsent(key, k -> {
|
KeyStructureChange kc = keyChanges.computeIfAbsent(aggKey, k -> {
|
||||||
KeyStructureChange n = new KeyStructureChange();
|
KeyStructureChange n = new KeyStructureChange();
|
||||||
n.setKeyPattern(k);
|
n.setKeyPattern(wp.getResolvedKeyPattern());
|
||||||
|
n.setKeyExpression(wp.getKeyExpression());
|
||||||
|
n.setWriteLocation(wp.location());
|
||||||
|
n.setValueType(displayType(wp));
|
||||||
|
n.setKeyUnresolved(isUnresolvedKey(wp.getResolvedKeyPattern()));
|
||||||
return n;
|
return n;
|
||||||
});
|
});
|
||||||
|
if (kc.getWriteLocation() == null || kc.getWriteLocation().isEmpty()) {
|
||||||
|
kc.setWriteLocation(wp.location());
|
||||||
|
}
|
||||||
|
if (kc.getValueType() == null || kc.getValueType().isEmpty()) {
|
||||||
|
kc.setValueType(displayType(wp));
|
||||||
|
}
|
||||||
|
if (kc.getKeyExpression() == null || kc.getKeyExpression().isEmpty()) {
|
||||||
|
kc.setKeyExpression(wp.getKeyExpression());
|
||||||
|
}
|
||||||
kc.getFieldDetails().addAll(changes);
|
kc.getFieldDetails().addAll(changes);
|
||||||
for (SchemaChange c : changes) {
|
for (SchemaChange c : changes) {
|
||||||
kc.raiseSeverity(c.getSeverity());
|
kc.raiseSeverity(c.getSeverity());
|
||||||
@@ -209,6 +219,41 @@ public class SchemaCheckAnalyzer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 已解析 key 按模式聚合;未解析按「位置+表达式」拆分,避免串单。 */
|
||||||
|
private String aggregationKey(WritePoint wp) {
|
||||||
|
String pattern = wp.getResolvedKeyPattern();
|
||||||
|
if (!isUnresolvedKey(pattern)) {
|
||||||
|
return pattern == null ? "<unknown>" : pattern;
|
||||||
|
}
|
||||||
|
return "unknown|" + nvl(wp.location()) + "|" + nvl(wp.getKeyExpression());
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUnresolvedKey(String keyPattern) {
|
||||||
|
return keyPattern == null
|
||||||
|
|| keyPattern.isEmpty()
|
||||||
|
|| "unknown-key".equals(keyPattern)
|
||||||
|
|| "<unknown>".equals(keyPattern);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String displayType(WritePoint wp) {
|
||||||
|
String simple = shortType(wp.getResolvedValueType());
|
||||||
|
if (wp.isRootArray()) {
|
||||||
|
return "List<" + simple + ">";
|
||||||
|
}
|
||||||
|
return simple;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fillFromWritePoint(SchemaChange c, WritePoint wp) {
|
||||||
|
c.setKeyPattern(wp.getResolvedKeyPattern());
|
||||||
|
c.setKeyExpression(wp.getKeyExpression());
|
||||||
|
c.setWriteLocation(wp.location());
|
||||||
|
c.setValueType(displayType(wp));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String nvl(String s) {
|
||||||
|
return s == null ? "" : s;
|
||||||
|
}
|
||||||
|
|
||||||
private Set<String> protectedPaths(List<SchemaChange> changes) {
|
private Set<String> protectedPaths(List<SchemaChange> changes) {
|
||||||
Set<String> paths = new LinkedHashSet<>();
|
Set<String> paths = new LinkedHashSet<>();
|
||||||
for (SchemaChange c : changes) {
|
for (SchemaChange c : changes) {
|
||||||
@@ -230,30 +275,41 @@ public class SchemaCheckAnalyzer {
|
|||||||
|
|
||||||
private Map<String, KeyStructureChange> filterKeyChanges(
|
private Map<String, KeyStructureChange> filterKeyChanges(
|
||||||
Map<String, KeyStructureChange> keyChanges, List<SchemaChange> finalChanges) {
|
Map<String, KeyStructureChange> keyChanges, List<SchemaChange> finalChanges) {
|
||||||
Set<String> liveKeys = new HashSet<>();
|
Set<String> liveDedup = new HashSet<>();
|
||||||
for (SchemaChange c : finalChanges) {
|
for (SchemaChange c : finalChanges) {
|
||||||
liveKeys.add(c.getKeyPattern() == null ? "<unknown>" : c.getKeyPattern());
|
liveDedup.add(changeDedupKey(c));
|
||||||
}
|
}
|
||||||
Map<String, KeyStructureChange> result = new LinkedHashMap<>();
|
Map<String, KeyStructureChange> result = new LinkedHashMap<>();
|
||||||
for (Map.Entry<String, KeyStructureChange> e : keyChanges.entrySet()) {
|
for (Map.Entry<String, KeyStructureChange> e : keyChanges.entrySet()) {
|
||||||
if (liveKeys.contains(e.getKey())) {
|
|
||||||
KeyStructureChange kc = e.getValue();
|
KeyStructureChange kc = e.getValue();
|
||||||
// 仅保留最终明细中仍存在的字段变更
|
|
||||||
List<SchemaChange> retained = new ArrayList<>();
|
List<SchemaChange> retained = new ArrayList<>();
|
||||||
Set<String> finalDedup = new HashSet<>();
|
Set<String> seen = new HashSet<>();
|
||||||
for (SchemaChange c : finalChanges) {
|
for (SchemaChange c : kc.getFieldDetails()) {
|
||||||
String k = c.getKeyPattern() == null ? "<unknown>" : c.getKeyPattern();
|
String dk = changeDedupKey(c);
|
||||||
if (!k.equals(e.getKey())) {
|
if (liveDedup.contains(dk) && seen.add(dk)) {
|
||||||
continue;
|
|
||||||
}
|
|
||||||
String dedupKey = c.getChangeType() + "|" + c.getKeyPattern() + "|"
|
|
||||||
+ c.getWriteLocation() + "|" + c.getFieldPath();
|
|
||||||
if (finalDedup.add(dedupKey)) {
|
|
||||||
retained.add(c);
|
retained.add(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (retained.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
kc.getFieldDetails().clear();
|
kc.getFieldDetails().clear();
|
||||||
kc.getFieldDetails().addAll(retained);
|
kc.getFieldDetails().addAll(retained);
|
||||||
|
// 从明细回填通用展示字段(若聚合时未带上)
|
||||||
|
for (SchemaChange c : retained) {
|
||||||
|
if ((kc.getWriteLocation() == null || kc.getWriteLocation().isEmpty())
|
||||||
|
&& c.getWriteLocation() != null) {
|
||||||
|
kc.setWriteLocation(c.getWriteLocation());
|
||||||
|
}
|
||||||
|
if ((kc.getValueType() == null || kc.getValueType().isEmpty())
|
||||||
|
&& c.getValueType() != null) {
|
||||||
|
kc.setValueType(c.getValueType());
|
||||||
|
}
|
||||||
|
if ((kc.getKeyExpression() == null || kc.getKeyExpression().isEmpty())
|
||||||
|
&& c.getKeyExpression() != null) {
|
||||||
|
kc.setKeyExpression(c.getKeyExpression());
|
||||||
|
}
|
||||||
|
}
|
||||||
Severity max = Severity.P2;
|
Severity max = Severity.P2;
|
||||||
for (SchemaChange c : retained) {
|
for (SchemaChange c : retained) {
|
||||||
if (c.getSeverity() != null && c.getSeverity().ordinal() < max.ordinal()) {
|
if (c.getSeverity() != null && c.getSeverity().ordinal() < max.ordinal()) {
|
||||||
@@ -263,15 +319,20 @@ public class SchemaCheckAnalyzer {
|
|||||||
kc.setSeverity(max);
|
kc.setSeverity(max);
|
||||||
result.put(e.getKey(), kc);
|
result.put(e.getKey(), kc);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String changeDedupKey(SchemaChange c) {
|
||||||
|
return c.getChangeType() + "|" + c.getKeyPattern() + "|"
|
||||||
|
+ c.getWriteLocation() + "|" + c.getFieldPath();
|
||||||
|
}
|
||||||
|
|
||||||
private void enrich(List<SchemaChange> changes, WritePoint wp, double confidence) {
|
private void enrich(List<SchemaChange> changes, WritePoint wp, double confidence) {
|
||||||
boolean lowConfidence = confidence < config.getDetection().getMinConfidence();
|
boolean lowConfidence = confidence < config.getDetection().getMinConfidence();
|
||||||
|
String type = displayType(wp);
|
||||||
for (SchemaChange c : changes) {
|
for (SchemaChange c : changes) {
|
||||||
c.setKeyPattern(wp.getResolvedKeyPattern());
|
fillFromWritePoint(c, wp);
|
||||||
c.setWriteLocation(wp.location());
|
c.setValueType(type);
|
||||||
if (lowConfidence) {
|
if (lowConfidence) {
|
||||||
c.setSeverity(Severity.P2);
|
c.setSeverity(Severity.P2);
|
||||||
c.setMessage(c.getMessage() + "(低置信度,建议人工确认)");
|
c.setMessage(c.getMessage() + "(低置信度,建议人工确认)");
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ public class SchemaChange {
|
|||||||
private Severity severity;
|
private Severity severity;
|
||||||
private ChangeType changeType;
|
private ChangeType changeType;
|
||||||
private String keyPattern;
|
private String keyPattern;
|
||||||
|
private String keyExpression;
|
||||||
private String writeLocation;
|
private String writeLocation;
|
||||||
|
private String valueType;
|
||||||
private String fieldPath;
|
private String fieldPath;
|
||||||
private String oldValue;
|
private String oldValue;
|
||||||
private String newValue;
|
private String newValue;
|
||||||
@@ -43,6 +45,14 @@ public class SchemaChange {
|
|||||||
this.keyPattern = keyPattern;
|
this.keyPattern = keyPattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getKeyExpression() {
|
||||||
|
return keyExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyExpression(String keyExpression) {
|
||||||
|
this.keyExpression = keyExpression;
|
||||||
|
}
|
||||||
|
|
||||||
public String getWriteLocation() {
|
public String getWriteLocation() {
|
||||||
return writeLocation;
|
return writeLocation;
|
||||||
}
|
}
|
||||||
@@ -51,6 +61,14 @@ public class SchemaChange {
|
|||||||
this.writeLocation = writeLocation;
|
this.writeLocation = writeLocation;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getValueType() {
|
||||||
|
return valueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValueType(String valueType) {
|
||||||
|
this.valueType = valueType;
|
||||||
|
}
|
||||||
|
|
||||||
public String getFieldPath() {
|
public String getFieldPath() {
|
||||||
return fieldPath;
|
return fieldPath;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,11 +7,19 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按 Redis key 聚合后的结构变更摘要(用于企微骨架展示)。
|
* 按 Redis key(或未知 key 时的写入点)聚合后的结构变更摘要。
|
||||||
*/
|
*/
|
||||||
public class KeyStructureChange {
|
public class KeyStructureChange {
|
||||||
|
|
||||||
|
/** 解析到的 key 模式;未知时为 unknown-key */
|
||||||
private String keyPattern;
|
private String keyPattern;
|
||||||
|
/** 源码中的 key 表达式,如 req.getKey() */
|
||||||
|
private String keyExpression;
|
||||||
|
/** 写入位置 Class#method:line */
|
||||||
|
private String writeLocation;
|
||||||
|
/** 展示用 value 类型,如 List<ClockInExportVo> */
|
||||||
|
private String valueType;
|
||||||
|
private boolean keyUnresolved;
|
||||||
private String oldSkeletonJson;
|
private String oldSkeletonJson;
|
||||||
private String newSkeletonJson;
|
private String newSkeletonJson;
|
||||||
private Severity severity = Severity.P2;
|
private Severity severity = Severity.P2;
|
||||||
@@ -25,6 +33,38 @@ public class KeyStructureChange {
|
|||||||
this.keyPattern = keyPattern;
|
this.keyPattern = keyPattern;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getKeyExpression() {
|
||||||
|
return keyExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyExpression(String keyExpression) {
|
||||||
|
this.keyExpression = keyExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getWriteLocation() {
|
||||||
|
return writeLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setWriteLocation(String writeLocation) {
|
||||||
|
this.writeLocation = writeLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getValueType() {
|
||||||
|
return valueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValueType(String valueType) {
|
||||||
|
this.valueType = valueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isKeyUnresolved() {
|
||||||
|
return keyUnresolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setKeyUnresolved(boolean keyUnresolved) {
|
||||||
|
this.keyUnresolved = keyUnresolved;
|
||||||
|
}
|
||||||
|
|
||||||
public String getOldSkeletonJson() {
|
public String getOldSkeletonJson() {
|
||||||
return oldSkeletonJson;
|
return oldSkeletonJson;
|
||||||
}
|
}
|
||||||
@@ -58,8 +98,18 @@ public class KeyStructureChange {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (severity == null || candidate.ordinal() < severity.ordinal()) {
|
if (severity == null || candidate.ordinal() < severity.ordinal()) {
|
||||||
// P0=0, P1=1, P2=2 → ordinal 更小更严重
|
|
||||||
severity = candidate;
|
severity = candidate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 通知里 Key 行展示文本:未解析优先用表达式。 */
|
||||||
|
public String displayKey() {
|
||||||
|
if (keyUnresolved && keyExpression != null && !keyExpression.trim().isEmpty()) {
|
||||||
|
return keyExpression.trim();
|
||||||
|
}
|
||||||
|
if (keyPattern != null && !keyPattern.trim().isEmpty()) {
|
||||||
|
return keyPattern.trim();
|
||||||
|
}
|
||||||
|
return keyExpression != null ? keyExpression : "unknown-key";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ import java.util.Set;
|
|||||||
/**
|
/**
|
||||||
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
* 将 {@link CheckReport} 渲染为企微 Markdown / 控制台文本。
|
||||||
* <ul>
|
* <ul>
|
||||||
* <li>企微:按 key 展示序列化骨架变更,不含位置/字段明细,不分 P0/P1/P2</li>
|
* <li>企微:按 key 展示位置/类型/序列化骨架变更,不含字段明细,不分 P0/P1/P2</li>
|
||||||
|
* <li>未解析 key 展示源码表达式 + 灰色「key 未解析」提示</li>
|
||||||
* <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li>
|
* <li>多 key 优先拼成一条;超过企微上限则按 key 拆成多条</li>
|
||||||
* <li>CI:先打字段明细,再完整输出企微 Markdown(拆分后的每条)</li>
|
* <li>CI:先打字段明细,再完整输出企微 Markdown(拆分后的每条)</li>
|
||||||
* </ul>
|
* </ul>
|
||||||
@@ -111,7 +112,8 @@ public class ReportBuilder {
|
|||||||
// 无 key 摘要时:整段字段明细作为一块(仍可超长再整体发,不按字段拆)
|
// 无 key 摘要时:整段字段明细作为一块(仍可超长再整体发,不按字段拆)
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
for (SchemaChange c : report.getChanges()) {
|
for (SchemaChange c : report.getChanges()) {
|
||||||
appendKeyLine(sb, c.getKeyPattern());
|
appendKeyLine(sb, c.getKeyPattern(), c.getKeyExpression(), isUnresolvedKey(c.getKeyPattern()));
|
||||||
|
appendMetaLines(sb, c.getWriteLocation(), c.getValueType());
|
||||||
if (c.getMessage() != null) {
|
if (c.getMessage() != null) {
|
||||||
sb.append(" ").append(c.getMessage()).append('\n');
|
sb.append(" ").append(c.getMessage()).append('\n');
|
||||||
}
|
}
|
||||||
@@ -121,7 +123,8 @@ public class ReportBuilder {
|
|||||||
|
|
||||||
private String renderKeyBlock(KeyStructureChange kc) {
|
private String renderKeyBlock(KeyStructureChange kc) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
appendKeyLine(sb, kc.getKeyPattern());
|
appendKeyLine(sb, kc.displayKey(), kc.getKeyExpression(), kc.isKeyUnresolved());
|
||||||
|
appendMetaLines(sb, kc.getWriteLocation(), kc.getValueType());
|
||||||
String oldJson = nvl(kc.getOldSkeletonJson());
|
String oldJson = nvl(kc.getOldSkeletonJson());
|
||||||
String newJson = nvl(kc.getNewSkeletonJson());
|
String newJson = nvl(kc.getNewSkeletonJson());
|
||||||
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
|
Set<String> oldHighlight = SkeletonAnnotator.pathsForOldSkeleton(kc.getFieldDetails());
|
||||||
@@ -131,9 +134,9 @@ public class ReportBuilder {
|
|||||||
String newRendered = newJson.isEmpty()
|
String newRendered = newJson.isEmpty()
|
||||||
? "" : "“" + SkeletonAnnotator.annotateForWecom(newJson, newHighlight) + "”";
|
? "" : "“" + SkeletonAnnotator.annotateForWecom(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()) {
|
||||||
sb.append(" value 原结构 :").append(oldRendered).append(" 已删除写入\n\n");
|
sb.append(" > **value 原结构:** ").append(oldRendered).append("(已删除写入)\n\n");
|
||||||
} else {
|
} else {
|
||||||
sb.append(" > **value值由:** ").append(oldRendered).append('\n');
|
sb.append(" > **value值由:** ").append(oldRendered).append('\n');
|
||||||
sb.append(" > **变更为:** ").append(newRendered).append("\n\n");
|
sb.append(" > **变更为:** ").append(newRendered).append("\n\n");
|
||||||
@@ -142,13 +145,35 @@ public class ReportBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Key 行:企微 warning 颜色 + 行内代码。
|
* Key 行:展示解析后的模式或未解析时的源码表达式;未解析时追加灰色提示。
|
||||||
* 不加粗:企微无法稳定嵌套 {@code **} 与反引号,会露出字面量 {@code *}/{@code **}。
|
* 反引号仅包裹 key 文本,避免与加粗/颜色嵌套冲突。
|
||||||
*/
|
*/
|
||||||
private void appendKeyLine(StringBuilder sb, String keyPattern) {
|
private void appendKeyLine(StringBuilder sb, String displayKey, String keyExpression,
|
||||||
sb.append("- <font color=\"warning\">`Key --> ")
|
boolean unresolved) {
|
||||||
.append(nvl(keyPattern))
|
String keyText = unresolved && keyExpression != null && !keyExpression.trim().isEmpty()
|
||||||
.append("`</font>\n");
|
? keyExpression.trim()
|
||||||
|
: nvl(displayKey);
|
||||||
|
if (keyText.isEmpty()) {
|
||||||
|
keyText = "unknown-key";
|
||||||
|
}
|
||||||
|
sb.append("- Key --> `").append(keyText).append('`');
|
||||||
|
if (unresolved) {
|
||||||
|
sb.append(" <font color=\"comment\">(key 未解析)</font>");
|
||||||
|
}
|
||||||
|
sb.append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 位置、类型作为每个 Key 块的通用项。 */
|
||||||
|
private void appendMetaLines(StringBuilder sb, String writeLocation, String valueType) {
|
||||||
|
sb.append(" > **位置**: `").append(nvl(writeLocation)).append("`\n");
|
||||||
|
sb.append(" > **类型**: `").append(nvl(valueType)).append("`\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isUnresolvedKey(String keyPattern) {
|
||||||
|
return keyPattern == null
|
||||||
|
|| keyPattern.isEmpty()
|
||||||
|
|| "unknown-key".equals(keyPattern)
|
||||||
|
|| "<unknown>".equals(keyPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class ReportBuilderTest {
|
|||||||
|
|
||||||
KeyStructureChange key = new KeyStructureChange();
|
KeyStructureChange key = new KeyStructureChange();
|
||||||
key.setKeyPattern("saas:period-config:migration:current");
|
key.setKeyPattern("saas:period-config:migration:current");
|
||||||
|
key.setWriteLocation("SaasPeriodConfigMigrationRedisSupport#putCurrent:41");
|
||||||
|
key.setValueType("MigrationCurrentVo");
|
||||||
key.setOldSkeletonJson(oldJson);
|
key.setOldSkeletonJson(oldJson);
|
||||||
key.setNewSkeletonJson(newJson);
|
key.setNewSkeletonJson(newJson);
|
||||||
key.setSeverity(Severity.P1);
|
key.setSeverity(Severity.P1);
|
||||||
@@ -56,9 +58,10 @@ class ReportBuilderTest {
|
|||||||
|
|
||||||
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
|
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
|
||||||
|
|
||||||
// Key:颜色 + 行内代码(与 ReportBuilder.appendKeyLine 一致)
|
assertTrue(md.contains("- Key --> `saas:period-config:migration:current`"));
|
||||||
assertTrue(md.contains(
|
assertFalse(md.contains("(key 未解析)"));
|
||||||
"- <font color=\"warning\">`Key --> saas:period-config:migration:current`</font>"));
|
assertTrue(md.contains("> **位置**: `SaasPeriodConfigMigrationRedisSupport#putCurrent:41`"));
|
||||||
|
assertTrue(md.contains("> **类型**: `MigrationCurrentVo`"));
|
||||||
|
|
||||||
int oldSection = md.indexOf("> **value值由:**");
|
int oldSection = md.indexOf("> **value值由:**");
|
||||||
int newSection = md.indexOf("> **变更为:**");
|
int newSection = md.indexOf("> **变更为:**");
|
||||||
@@ -131,11 +134,36 @@ 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>"));
|
||||||
|
assertTrue(console.contains("> **位置**: `"));
|
||||||
|
assertTrue(console.contains("> **类型**: `"));
|
||||||
assertTrue(console.contains("> **value值由:**"));
|
assertTrue(console.contains("> **value值由:**"));
|
||||||
assertTrue(console.contains("> **变更为:**"));
|
assertTrue(console.contains("> **变更为:**"));
|
||||||
assertFalse(console.contains("`{\"x\":\"\"}`"));
|
assertFalse(console.contains("`{\"x\":\"\"}`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void unresolvedKeyShowsExpressionAndCommentHint() {
|
||||||
|
CheckReport report = baseReport();
|
||||||
|
KeyStructureChange key = new KeyStructureChange();
|
||||||
|
key.setKeyPattern("unknown-key");
|
||||||
|
key.setKeyExpression("req.getKey()");
|
||||||
|
key.setKeyUnresolved(true);
|
||||||
|
key.setWriteLocation("ClockInXxxService#export:128");
|
||||||
|
key.setValueType("List<ClockInExportVo>");
|
||||||
|
key.setOldSkeletonJson("{\"a\":\"\"}");
|
||||||
|
key.setNewSkeletonJson("{\"a\":\"\",\"b\":\"\"}");
|
||||||
|
SchemaChange added = new SchemaChange(ChangeType.FIELD_ADDED);
|
||||||
|
added.setFieldPath("b");
|
||||||
|
key.getFieldDetails().add(added);
|
||||||
|
report.getKeyChanges().add(key);
|
||||||
|
|
||||||
|
String md = new ReportBuilder("[缓存结构变更]").toMarkdown(report);
|
||||||
|
assertTrue(md.contains("- Key --> `req.getKey()` <font color=\"comment\">(key 未解析)</font>"));
|
||||||
|
assertTrue(md.contains("> **位置**: `ClockInXxxService#export:128`"));
|
||||||
|
assertTrue(md.contains("> **类型**: `List<ClockInExportVo>`"));
|
||||||
|
assertFalse(md.contains("unknown-key"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void pathAwareNestedHighlight() {
|
void pathAwareNestedHighlight() {
|
||||||
String json = "{\"vo\":{\"dbName\":\"\",\"id\":\"\"},\"expiresAtMs\":0}";
|
String json = "{\"vo\":{\"dbName\":\"\",\"id\":\"\"},\"expiresAtMs\":0}";
|
||||||
@@ -198,6 +226,8 @@ class ReportBuilderTest {
|
|||||||
String addedField) {
|
String addedField) {
|
||||||
KeyStructureChange key = new KeyStructureChange();
|
KeyStructureChange key = new KeyStructureChange();
|
||||||
key.setKeyPattern(pattern);
|
key.setKeyPattern(pattern);
|
||||||
|
key.setWriteLocation("DemoService#write:1");
|
||||||
|
key.setValueType("DemoVo");
|
||||||
key.setOldSkeletonJson(oldJson);
|
key.setOldSkeletonJson(oldJson);
|
||||||
key.setNewSkeletonJson(newJson);
|
key.setNewSkeletonJson(newJson);
|
||||||
if (addedField != null) {
|
if (addedField != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user