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 quyangJilu(", "limsHuanjingJianceService.quyangJilu", "HUANJING_QUYANG"); checkHuanjingFailureBoundary(huanjingController, "public R 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 一字未动。 * 请验单提交已 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 之后只做原子认领、不再写 SIGN_USED。 * *

取代原来的 {@code checkConsumption("请验单提交", ...)} 调用。除语义外还修掉了那边的 * 范围缺陷:{@code source.indexOf("recordSignUsage", operation)} 是从某位置往后 * 在整个文件里找,命中的其实是私有方法体内那行 {@code limsSignService.recordSignUsage(...)}, * 与 submit 端点没有任何绑定关系——把 submit 里的调用整行删掉,那条断言照样绿。 * 本方法把搜索范围夹在 submit 端点方法体内,改坏 submit 才会红。 * *

守的是认领不是事件:认领是「签名单次消费」的防重放护栏(ed4d908), * 删掉等于同一个签名可以反复使用。D8 停的只是事件。 * *

⚠️ 取样({@code QUYANG_FETCH})/ 接样({@code JIEYANG_RECEIVE})语义未变, * 仍走 {@link #checkConsumption} 且一字未动。它们沿用的整文件 indexOf 有同样的范围缺陷, * 但本期不在范围内,已记入 Task 8 报告待处理——不在这里顺手改,是为了让 * 「取样 / 接样没被改到」在 diff 上一眼可见。 */ private static void checkQingyandanSubmitClaim(String source) { String block = methodBody(source, "public R 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 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() { } }