刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
package jnpf.limsService.support;
 
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
 
/** SIGN_USED 业务入口、模块语义和折叠接线的源码级回归探针。 */
public final class LimsSignUsageWiringProbe {
 
    private static int passed;
    private static int failed;
 
    public static void main(String[] args) throws Exception {
        String qingyandanController = read("jnpf-lims/jnpf-lims-controller/src/main/java/"
                + "jnpf/limsController/LimsQingyandanController.java");
        String quyangController = read("jnpf-lims/jnpf-lims-controller/src/main/java/"
                + "jnpf/limsController/LimsQuyangController.java");
        String jieyangController = read("jnpf-lims/jnpf-lims-controller/src/main/java/"
                + "jnpf/limsController/LimsJieyangController.java");
        String huanjingController = read("jnpf-lims/jnpf-lims-controller/src/main/java/"
                + "jnpf/limsController/LimsHuanjingJianceController.java");
        String qingyandanService = read("jnpf-lims/jnpf-lims-biz/src/main/java/"
                + "jnpf/limsService/impl/LimsQingyandanServiceImpl.java");
        String signService = read("jnpf-lims/jnpf-lims-biz/src/main/java/"
                + "jnpf/limsService/impl/LimsSignServiceImpl.java");
        String signOpType = read("jnpf-lims/jnpf-lims-entity/src/main/java/"
                + "jnpf/limsEntity/SignOpType.java");
        String qingyandanEntity = read("jnpf-lims/jnpf-lims-entity/src/main/java/"
                + "jnpf/limsEntity/LimsQingyandanEntity.java");
 
        // 2026-07-31 retarget(原为 checkConsumption("请验单提交", qingyandanController,
        //   "QINGYANDAN_SUBMIT")):旧断言查「submit 之后调用了 recordSignUsage」,
        //   而 D8 明确表单路径**停写事件、保留原子认领**。语义变了,判据跟着变;
        //   认领这件事**必须继续有人守**(防重放护栏,删了同一签名可反复使用),故是 retarget 不是删除。
        checkQingyandanSubmitClaim(qingyandanController);
        checkQingyandanFreshSign(qingyandanController);
        checkCancelConsumption(qingyandanController);
        checkConsumption("取样", quyangController, "QUYANG_FETCH");
        checkConsumption("接样", jieyangController, "JIEYANG_RECEIVE");
        checkHuanjingFailureBoundary(huanjingController, "public R<String> quyangJilu(",
                "limsHuanjingJianceService.quyangJilu", "HUANJING_QUYANG");
        checkHuanjingFailureBoundary(huanjingController, "public R<String> terminateQuyangJilu(",
                "limsHuanjingJianceService.terminateQuyangJilu", "HUANJING_QUYANG_TERMINATE");
 
        // ┌─ 2026-07-31 删除,留痕如下(不是有人删断言换绿)────────────────────────────┐
        // │ 曾经断言:check("请验单提交 Layer 2 与消费事件共事务",                        │
        // │             annotatedBusinessAction(qingyandanService,                      │
        // │                     "action = \"SUBMIT\"", "public void submit("),          │
        // │             "submit 缺少 @AuditLog");                                       │
        // │   即:LimsQingyandanServiceImpl.submit() 必须带 @AuditLog(action="SUBMIT")。│
        // │                                                                            │
        // │ 为什么被取代:spec D7 已把两处 SUBMIT 埋点整块删除——客户关注的是他自己触发   │
        // │   的动作(表单的新增/修改),那已由层 0 记录;submit() 是表单保存之后的后端   │
        // │   内部编排,再记一条「请验单 提交」只会让列表出现用户没做过的动作行。         │
        // │   所以这条断言的**前提**(submit 有层 2 事件)不复存在,不是覆盖面丢失。      │
        // │                                                                            │
        // │ 谁接替:tasks/lims-sign-usage-checks.sh G 组「SUBMIT 埋点已删(恰好 0 处)」,│
        // │   同一个位置从「必须在」倒转成「必须不在」,且做过双向变异验证(加回来也红)。 │
        // └────────────────────────────────────────────────────────────────────────────┘
        check("取样 Layer 2 与消费事件共事务",
                annotatedBusinessAction(qingyandanService, "action = \"FETCH\"", "public void fetch("),
                "fetch 缺少 @AuditLog");
        check("接样 Layer 2 与消费事件共事务",
                annotatedBusinessAction(qingyandanService, "action = \"RECEIVE\"", "public void jieyangReceive("),
                "jieyangReceive 缺少 @AuditLog");
        check("请验单撤回使用稳定签名操作码",
                signOpType.contains("QINGYANDAN_CANCEL(\"qingyandan_cancel\", \"请验单撤回\")"),
                "缺少 QINGYANDAN_CANCEL 稳定 code");
        checkCancelAffectedGuard(qingyandanService);
        checkQingyandanEntityContract(qingyandanEntity);
        check("SIGN_USED 写入 bizModule",
                signService.contains(".bizModule(group.getBizModule())"),
                "AuditEventDTO builder 未写 bizModule");
        String publishSignUsed = methodBody(signService, "private void publishSignUsedEvents(");
        check("SIGN_USED 将签名 note 写入 reason",
                publishSignUsed.contains(".reason(sign.getNote())"),
                "SIGN_USED AuditEventDTO 未把 lims_sign.note 映射到 reason");
        checkSignClaimGuard(signService);
        check("全部签名使用目标显式声明 bizModule", allTargetsDeclareBizModule(),
                "存在未声明 bizModule 的 LimsSignUsageTarget.builder() 块");
 
        System.out.println("=== SIGN_USED 接线断言:" + passed + " 通过 / " + failed + " 失败 ===");
        System.exit(failed == 0 ? 0 : 1);
    }
 
    /**
     * 取样 / 接样仍走本方法,语义未变(成功后仍应写 SIGN_USED),2026-07-31 <b>一字未动</b>。
     * 请验单提交已 retarget 到 {@link #checkQingyandanSubmitClaim}。
     */
    private static void checkConsumption(String label, String source, String opType) {
        check(label + "校验签名", source.contains("SignOpType." + opType), opType + " 未接入");
        int operation = source.indexOf("SignOpType." + opType);
        int record = source.indexOf("recordSignUsage", operation);
        check(label + "成功后记录 SIGN_USED", operation >= 0 && record > operation,
                opType + " 后未调用 recordSignUsage");
    }
 
    /**
     * 请验单提交:D8 之后<b>只做原子认领、不再写 SIGN_USED</b>。
     *
     * <p>取代原来的 {@code checkConsumption("请验单提交", ...)} 调用。除语义外还修掉了那边的
     * <b>范围缺陷</b>:{@code source.indexOf("recordSignUsage", operation)} 是从某位置往后
     * <b>在整个文件里</b>找,命中的其实是私有方法体内那行 {@code limsSignService.recordSignUsage(...)},
     * 与 submit 端点没有任何绑定关系——把 submit 里的调用整行删掉,那条断言照样绿。
     * 本方法把搜索范围<b>夹在 submit 端点方法体内</b>,改坏 submit 才会红。
     *
     * <p>守的是<b>认领</b>不是事件:认领是「签名单次消费」的防重放护栏(ed4d908),
     * 删掉等于同一个签名可以反复使用。D8 停的只是事件。
     *
     * <p>⚠️ 取样({@code QUYANG_FETCH})/ 接样({@code JIEYANG_RECEIVE})语义未变,
     * 仍走 {@link #checkConsumption} 且一字未动。它们沿用的整文件 indexOf 有同样的范围缺陷,
     * 但本期不在范围内,已记入 Task 8 报告待处理——不在这里顺手改,是为了让
     * 「取样 / 接样没被改到」在 diff 上一眼可见。
     */
    private static void checkQingyandanSubmitClaim(String source) {
        String block = methodBody(source, "public R<String> qingyandanSubmit(");
        // 判据写成**完整调用**而不是光一个 SignOpType.QINGYANDAN_SUBMIT:
        // 认领那行也带着同一个枚举名,只查枚举名的话,把校验改成传 null 照样绿(变异 N3 实测)。
        int verify = block.indexOf(
                "requireQingyandanSign(qingyandan, formData, SignOpType.QINGYANDAN_SUBMIT)");
        int business = block.indexOf("limsQingyandanService.submit(", verify);
        int claim = block.indexOf("claimQingyandanSignUsageOnly(", business);
        check("请验单提交按 QINGYANDAN_SUBMIT 校验签名", verify >= 0,
                "submit 端点未按 QINGYANDAN_SUBMIT 校验签名");
        // 顺序也钉死:认领必须在业务写库**之后**。提前认领 = 业务失败时签名已被消费,
        // 正是 ed4d908 修掉的那个 bug。
        check("请验单提交校验签名后执行、业务成功后才原子认领(不再写 SIGN_USED)",
                verify >= 0 && business > verify && claim > business,
                "submit 端点体内未在业务成功后调用 claimQingyandanSignUsageOnly"
                        + " —— 防重放护栏丢失或顺序颠倒(ed4d908 回归)");
        check("请验单提交不再走发事件路径",
                !block.isEmpty()
                        && !block.contains("recordQingyandanSignUsage(")
                        && !block.contains("limsSignService.recordSignUsage("),
                "submit 端点仍写 SIGN_USED,与层 0 据签名字段补写的签名事件重复(D8)");
    }
 
    /** 按方法签名夹出方法体(到同缩进的收尾大括号);取不到时返回空串,让断言按红处理。 */
    private static String methodBody(String source, String signature) {
        int start = source.indexOf(signature);
        if (start < 0) {
            return "";
        }
        int end = source.indexOf("\n    }", start);
        return end > start ? source.substring(start, end) : "";
    }
 
    private static void checkCancelConsumption(String source) {
        int cancelCase = source.indexOf("case \"cancel\":");
        int operation = source.indexOf("SignOpType.QINGYANDAN_CANCEL", cancelCase);
        int cancel = source.indexOf("limsQingyandanService.cancel", operation);
        int record = source.indexOf("recordQingyandanSignUsage", cancel);
        check("请验单撤回从请求读取签名",
                source.contains("params.get(\"biz_sign\")") && source.contains("params.get(\"id\")"),
                "撤回未读取 biz_sign 或兼容 id");
        check("请验单撤回校验签名后执行且成功后记录 SIGN_USED",
                cancelCase >= 0 && operation > cancelCase && cancel > operation && record > cancel,
                "撤回的签名校验、业务更新、SIGN_USED 顺序错误");
    }
 
    private static void checkQingyandanFreshSign(String source) {
        int start = source.indexOf("private LimsSignEntity requireQingyandanSign(");
        int end = source.indexOf("private LimsSignEntity requireQingyandanActionSign(", start);
        String block = start >= 0 && end > start ? source.substring(start, end) : "";
        check("请验单提交和编辑必须携带本次签名",
                block.contains("request.getBizSign()") && !block.contains("persisted.getBizSign()"),
                "请求缺签名时仍会复用业务表中的历史 biz_sign");
    }
 
    private static void checkHuanjingFailureBoundary(String source,
                                                      String method,
                                                      String businessCall,
                                                      String opType) {
        int methodAt = source.indexOf(method);
        int nextMethod = source.indexOf("\n    public ", methodAt + method.length());
        String block = methodAt >= 0
                ? source.substring(methodAt, nextMethod > methodAt ? nextMethod : source.length())
                : "";
        int validation = block.indexOf("limsSignService.requireValidSign(");
        int catchAt = block.indexOf("catch (DataException e)", validation);
        int catchClose = block.indexOf("\n        }", catchAt);
        int business = block.indexOf(businessCall, catchClose);
        int record = block.indexOf("recordHuanjingSignUsage", business);
        check("环境监测 " + opType + " 只捕获签名校验异常",
                validation >= 0 && catchAt > validation && catchClose > catchAt
                        && business > catchClose && record > business,
                "业务更新或 SIGN_USED 仍在 DataException 捕获范围内");
    }
 
    private static void checkCancelAffectedGuard(String source) {
        int start = source.indexOf("public boolean cancel(");
        int end = source.indexOf("\n    @Override", start);
        String block = start >= 0 && end > start ? source.substring(start, end) : "";
        int conditionalUpdate = block.indexOf("update.eq(\"biz_status\", BizStatus.CONFIRMED.getCode())");
        int zeroReturn = block.indexOf("return false;");
        int softDelete = block.indexOf("softDeleteJianyanXiangRecords");
        check("请验单撤回按 confirmed 状态原子更新",
                block.contains("activeRowUpdate(qingyandan.getId())") && conditionalUpdate >= 0,
                "cancel 缺少状态或未删除条件");
        check("affected=0 不执行撤回副作用",
                zeroReturn > conditionalUpdate && softDelete > zeroReturn,
                "cancel 在确认更新命中前执行了软删除");
        check("affected=0 不生成业务事件",
                source.substring(Math.max(0, source.lastIndexOf("@AuditLog(", start)), start)
                        .contains("#result ? #qingyandan.jianyanLiushuihao : null"),
                "cancel 的 @AuditLog 未按返回值跳过 0 行事件");
    }
 
    private static void checkQingyandanEntityContract(String entity) {
        String[] fields = {"qingyan_fenlei", "zhouqi", "zhouqi_danwei"};
        for (String field : fields) {
            check("实体包含请验字段 " + field,
                    entity.contains("@TableField(\"" + field + "\")"),
                    field + " 的实体契约不完整");
        }
    }
 
    private static void checkSignClaimGuard(String source) {
        int requireStart = source.indexOf("public LimsSignEntity requireValidSign(");
        int recordStart = source.indexOf("public void recordSignUsage(", requireStart);
        String requireBlock = requireStart >= 0 && recordStart > requireStart
                ? source.substring(requireStart, recordStart) : "";
        int claim = source.indexOf("claimSignUsage(sign, opType, usageJson)", recordStart);
        int event = source.indexOf("auditClientProvider.ifAvailable", recordStart);
        check("签名校验不提前写库认领",
                !requireBlock.contains("updateById(") && !requireBlock.contains("this.update("),
                "requireValidSign 仍会在业务成功前写签名");
        check("已消费签名不可跨业务周期复用",
                requireBlock.contains("sign.getExtraJson()")
                        && requireBlock.contains("电子签名已被使用,请重新签名")
                        && source.contains("w.isNull(\"extra_json\").or().eq(\"extra_json\", \"\")")
                        && !source.contains(".or().eq(\"extra_json\", usageJson)"),
                "extra_json 已有值时仍可能再次消费同一签名");
        check("签名成功后先原子认领再记录事件",
                claim > recordStart && event > claim
                        && source.contains("w.isNull(\"op_type\")")
                        && source.contains("w.isNull(\"data_id\")")
                        && source.contains("w.isNull(\"extra_json\")"),
                "SIGN_USED 缺少认领冲突条件或事件顺序错误");
        check("签名认领失败标记事务回滚",
                source.contains("currentTransactionStatus().setRollbackOnly()")
                        && source.contains("throw signUsageFailure("),
                "controller 捕获 DataException 时事务可能继续提交");
    }
 
    private static boolean annotatedBusinessAction(String source, String action, String method) {
        int methodAt = source.indexOf(method);
        int actionAt = source.lastIndexOf(action, methodAt);
        int annotationAt = source.lastIndexOf("@AuditLog(", methodAt);
        return methodAt >= 0 && annotationAt >= 0 && actionAt > annotationAt
                && methodAt - annotationAt < 600;
    }
 
    private static boolean allTargetsDeclareBizModule() throws IOException {
        Path root = Paths.get("jnpf-lims");
        try (Stream<Path> files = Files.walk(root)) {
            return files.filter(path -> path.toString().contains("/src/main/java/")
                            && path.toString().endsWith(".java"))
                    .allMatch(LimsSignUsageWiringProbe::targetBlocksDeclareBizModule);
        }
    }
 
    private static boolean targetBlocksDeclareBizModule(Path path) {
        try {
            String source = new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
            int from = 0;
            while (true) {
                int builder = source.indexOf("LimsSignUsageTarget.builder()", from);
                if (builder < 0) {
                    return true;
                }
                int build = source.indexOf(".build()", builder);
                if (build < 0 || !source.substring(builder, build).contains(".bizModule(")) {
                    return false;
                }
                from = build + 8;
            }
        } catch (IOException e) {
            throw new IllegalStateException("读取源码失败: " + path, e);
        }
    }
 
    private static String read(String path) throws IOException {
        return new String(Files.readAllBytes(Paths.get(path)), StandardCharsets.UTF_8);
    }
 
    private static void check(String name, boolean ok, String detail) {
        if (ok) {
            passed++;
            System.out.println("  [PASS] " + name);
        } else {
            failed++;
            System.out.println("  [FAIL] " + name + " -- " + detail);
        }
    }
 
    private LimsSignUsageWiringProbe() {
    }
}