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:查询放大)。
|
*
|
* <p>源码断言只守得住形状(有没有 renderLines、build 调了几次),守不住**对位**——
|
* 合批之后每行的展示值是按位置从同一批解析结果里切回去的,切错一格就会把 A 行的值显示到
|
* B 行的标题上,而 {@code record_title} 是 append-only 存十年的明文列。所以这些必须跑起来验:
|
* <ol>
|
* <li>整份标题(主 + N 个子行)**只解析一次**,不是 1 + N 次;</li>
|
* <li>每个子表行的标题各自对应自己那一行的值(各行刻意取不同值,错位立刻现形);</li>
|
* <li><b>中间某行整行为空</b>时该行不产出标题,且**不把后面几行的对位带偏</b>——
|
* 这是位置分发最容易错的地方(空行贡献 0 个解析项,切片边界必须跟着走)。</li>
|
* </ol>
|
*/
|
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.<AuditFieldValueResolver>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<Map<String, Object>> subRows = new ArrayList<Map<String, Object>>();
|
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<String, Object> main = row("bian_hao", "BH-001", "beizhu", "备注X");
|
main.put("tablefield101", subRows);
|
|
AuditRecordTitle title = builder.build(spec, main, context(), labels(),
|
Collections.<String>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<String, String> frontendDisplays = new LinkedHashMap<String, String>();
|
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<String, Object> noSub = row("bian_hao", "BH-002", "beizhu", "备注Y");
|
AuditRecordTitle missing = builder.build(spec, noSub, context(), labels(),
|
Collections.<String>emptySet());
|
check("子表数据缺席仍标 unavailable", missing.subDataUnavailable(),
|
String.valueOf(missing.subDataUnavailable()));
|
check("子表缺席时主标题照出", "BH-002/备注Y".equals(missing.mainTitle()), missing.mainTitle());
|
|
Map<String, Object> idRow = new LinkedHashMap<String, Object>();
|
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.<String, Object>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<String, AuditFieldDiff.FieldMeta> labels() {
|
return Collections.emptyMap();
|
}
|
|
private static Map<String, Object> row(String k1, Object v1, String k2, Object v2) {
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
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<AuditFieldDiffViewVO> 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);
|
}
|
}
|
}
|