刘光辉
15 小时以前 34981c30a78e8bbd7791131059a9210f9928b62c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
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);
        }
    }
}