package jnpf.bizcommon.audit.service.title; import jnpf.bizcommon.audit.entity.AuditEventEntity; import jnpf.audit.diff.AuditFieldDiff; import jnpf.bizcommon.audit.entity.model.AuditFieldDiffViewVO; import jnpf.bizcommon.audit.service.AuditFieldDiffViewService; import jnpf.bizcommon.audit.service.value.AuditFieldValueContext; import jnpf.bizcommon.audit.service.value.AuditFieldValueResolver; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; /** * 标题渲染「主表 + 全部子表行一次解析」的探针(A3,2026-08-03 Codex 终审 Low:查询放大)。 * *

源码断言只守得住形状(有没有 renderLines、build 调了几次),守不住**对位**—— * 合批之后每行的展示值是按位置从同一批解析结果里切回去的,切错一格就会把 A 行的值显示到 * B 行的标题上,而 {@code record_title} 是 append-only 存十年的明文列。所以这些必须跑起来验: *

    *
  1. 整份标题(主 + N 个子行)**只解析一次**,不是 1 + N 次;
  2. *
  3. 每个子表行的标题各自对应自己那一行的值(各行刻意取不同值,错位立刻现形);
  4. *
  5. 中间某行整行为空时该行不产出标题,且**不把后面几行的对位带偏—— * 这是位置分发最容易错的地方(空行贡献 0 个解析项,切片边界必须跟着走)。
  6. *
*/ public final class AuditRecordTitleBatchProbe { private static int passed; private static int failed; public static void main(String[] args) throws Exception { CountingResolver counter = new CountingResolver(); AuditFieldDiffViewService viewService = new AuditFieldDiffViewService( Collections.singletonList(counter), "Asia/Shanghai"); AuditRecordTitleBuilder builder = new AuditRecordTitleBuilder(); Field field = AuditRecordTitleBuilder.class.getDeclaredField("fieldDiffViewService"); field.setAccessible(true); field.set(builder, viewService); AuditTitleSpec spec = new AuditTitleSpec( Arrays.asList("bian_hao", "beizhu"), Collections.singletonList(new AuditTitleSpec.SubTableSpec( "tableField101", "lims_sub", Arrays.asList("xiang_mu", "jie_guo")))); // 子表第 2 行(下标 1)刻意整行为空:它贡献 0 个解析项,后面两行的切片起点必须跟着挪。 List> subRows = new ArrayList>(); subRows.add(row("xiang_mu", "项目A", "jie_guo", "结果A")); subRows.add(row("xiang_mu", "", "jie_guo", "")); subRows.add(row("xiang_mu", "项目C", "jie_guo", "结果C")); subRows.add(row("xiang_mu", "项目D", "jie_guo", "结果D")); Map main = row("bian_hao", "BH-001", "beizhu", "备注X"); main.put("tablefield101", subRows); AuditRecordTitle title = builder.build(spec, main, context(), labels(), Collections.emptySet()); check("整份标题只解析一次(不是 1 + N 次)", counter.calls == 1, "calls=" + counter.calls); check("主标题按主表行渲染", "BH-001/备注X".equals(title.mainTitle()), title.mainTitle()); check("整行为空的子表行不产出标题", title.subTitles().size() == 3, title.subTitles().toString()); // 对位断言:空行之后的两行必须还是自己的值。切片边界没跟着空行走的话, // 这里会读到「项目D/结果D」顶到第 2 位(或越界丢行)。 check("空行之后的子表行不错位", Arrays.asList("项目A/结果A", "项目C/结果C", "项目D/结果D").equals(title.subTitles()), title.subTitles().toString()); check("子表名如实带出", "lims_sub".equals(title.subTable()), title.subTable()); check("子表数据可得时不标 unavailable", !title.subDataUnavailable(), String.valueOf(title.subDataUnavailable())); // 解析器看到的是**合并后的一整批**:主表 2 项 + 子表 3 行 × 2 项 = 8 项。 // 这条守的是「真的合批了」而不是「调了一次但只喂了主表」。 check("一次解析拿到全部 8 个字段项", counter.lastSize == 8, "size=" + counter.lastSize); // 排除列在合批路径上照样生效(签名列绝不能进标题——append-only 存十年) AuditRecordTitle excluded = builder.build(spec, main, context(), labels(), Collections.singleton("beizhu")); check("excludedFields 在合批路径上仍然生效", "BH-001".equals(excluded.mainTitle()), excluded.mainTitle()); Map frontendDisplays = new LinkedHashMap(); frontendDisplays.put("bian_hao", "前端显示编号"); frontendDisplays.put("beizhu", "前端显示备注"); frontendDisplays.put("tampered", "伪造字段"); AuditRecordTitle frontendTitle = builder.build(spec, main, context(), labels(), Collections.singleton("beizhu"), frontendDisplays); check("前端显示值覆盖服务端解析结果且保持配置顺序", "前端显示编号".equals(frontendTitle.mainTitle()), frontendTitle.mainTitle()); check("前端显示值不能绕过排除字段", !frontendTitle.mainTitle().contains("前端显示备注"), frontendTitle.mainTitle()); check("未配置的前端字段不能进入标题", !frontendTitle.mainTitle().contains("伪造字段"), frontendTitle.mainTitle()); // 子表键缺席 = 判不了,如实标 unavailable(合批重构不该把这个语义弄丢) Map noSub = row("bian_hao", "BH-002", "beizhu", "备注Y"); AuditRecordTitle missing = builder.build(spec, noSub, context(), labels(), Collections.emptySet()); check("子表数据缺席仍标 unavailable", missing.subDataUnavailable(), String.valueOf(missing.subDataUnavailable())); check("子表缺席时主标题照出", "BH-002/备注Y".equals(missing.mainTitle()), missing.mainTitle()); Map idRow = new LinkedHashMap(); idRow.put("F_ID", "ROW-001"); check("未启用标题字段时优先使用记录 f_id", "ROW-001".equals(builder.resolveMainTitle(AuditTitleSpec.EMPTY, AuditRecordTitle.EMPTY, idRow, "EVENT-001")), idRow.toString()); check("创建数据未回填 f_id 时使用事件 dataId", "EVENT-002".equals(builder.resolveMainTitle(AuditTitleSpec.EMPTY, AuditRecordTitle.EMPTY, Collections.emptyMap(), "EVENT-002")), ""); check("已启用字段但值为空时不伪装成 f_id", builder.resolveMainTitle(spec, AuditRecordTitle.EMPTY, idRow, "EVENT-003") == null, ""); System.out.printf("[AuditRecordTitleBatchProbe] %d 通过 / %d 失败%n", passed, failed); if (failed > 0) { System.exit(1); } } private static AuditEventEntity context() { AuditEventEntity event = new AuditEventEntity(); event.setTargetTable("lims_ceshi_huichuan"); event.setTenantId("0"); event.setActionCode("UPDATE"); return event; } private static Map labels() { return Collections.emptyMap(); } private static Map row(String k1, Object v1, String k2, Object v2) { Map row = new LinkedHashMap(); row.put(k1, v1); row.put(k2, v2); return row; } /** 只数调用次数与批量大小,不改任何展示值(改了就分不清是解析器还是分发出的错)。 */ private static final class CountingResolver implements AuditFieldValueResolver { private int calls; private int lastSize; @Override public boolean supports(String targetTable) { return true; } @Override public void resolve(AuditFieldValueContext context, List fields) { calls++; lastSize = fields == null ? -1 : fields.size(); } } private static void check(String name, boolean ok, String actual) { if (ok) { passed++; System.out.println(" ✅ " + name); } else { failed++; System.out.println(" ❌ " + name + " —— 实际:" + actual); } } }