package jnpf.bizcommon.audit.service.sign;
|
|
import jnpf.bizcommon.audit.service.title.AuditTitleSpec;
|
|
import java.util.Arrays;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
|
/**
|
* 签名字段候选挑选的纯 JVM 断言。
|
*
|
* <p><b>判定规则为什么长这样</b>:{@code biz_log_enabled} 在「操作记录标题」语境里是
|
* "这个字段进标题",在「多签名」语境里是"这是签名字段"——同一个开关两种语义,
|
* 光看开关分不出来。于是判定拆成三层:
|
* <ol>
|
* <li>{@code biz_sign} 及命名规则命中 {@code isCredentialReference} 的字段:<b>恒</b>为签名字段,
|
* 不需要勾(这批字段本就已被强制脱敏);</li>
|
* <li>其余勾选字段:值拿去查签名表,<b>命中的</b>当签名字段;</li>
|
* <li>没命中的当标题字段。</li>
|
* </ol>
|
* 本探针只覆盖第 1、2 层的<b>候选挑选</b>(纯逻辑);"查表命中"那一跳需要 DB,放端到端。
|
*
|
* <p>runner:{@code tasks/audit-sign-field-checks.sh}
|
*/
|
public final class AuditSignFieldProbe {
|
|
private static int pass;
|
private static int fail;
|
|
public static void main(String[] args) {
|
credentialNamesAreAlwaysCandidates();
|
enabledFieldsAreCandidates();
|
emptyValuesAreNotCandidates();
|
unmarkedNonCredentialFieldsAreNotCandidates();
|
candidatesAreDeduplicatedAndStable();
|
configuredFieldsTakePriority();
|
System.out.println("=== 签名字段候选断言:" + pass + " 通过 / " + fail + " 失败 ===");
|
if (fail > 0) {
|
System.exit(1);
|
}
|
}
|
|
/** S1 biz_sign 恒为候选,哪怕表单一个字段都没勾。 */
|
private static void credentialNamesAreAlwaysCandidates() {
|
Map<String, Object> row = row("biz_sign", "852000000000000001");
|
List<String> c = AuditSignEvidenceReader.candidateFields(
|
row, AuditTitleSpec.EMPTY, Collections.<String>emptyList());
|
check("S1 biz_sign 恒为候选", c.contains("biz_sign"), "实际 " + c);
|
|
Map<String, Object> row2 = row("fuhe_sign", "852000000000000002");
|
List<String> c2 = AuditSignEvidenceReader.candidateFields(
|
row2, AuditTitleSpec.EMPTY, Collections.<String>emptyList());
|
check("S1 *_sign 命名规则命中", c2.contains("fuhe_sign"), "实际 " + c2);
|
}
|
|
/** S2 勾选字段进候选(是不是签名由查表定,不由本方法定)。 */
|
private static void enabledFieldsAreCandidates() {
|
Map<String, Object> row = row("pizhun_ren", "852000000000000003");
|
AuditTitleSpec spec = new AuditTitleSpec(
|
Arrays.asList("pizhun_ren"), Collections.<AuditTitleSpec.SubTableSpec>emptyList());
|
List<String> c = AuditSignEvidenceReader.candidateFields(row, spec, Collections.<String>emptyList());
|
check("S2 勾选字段进候选", c.contains("pizhun_ren"), "实际 " + c);
|
}
|
|
/** S3 值为空的不进候选——空值查签名表纯属浪费,还会把空串当 IN 参数。 */
|
private static void emptyValuesAreNotCandidates() {
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
row.put("biz_sign", "");
|
row.put("fuhe_sign", null);
|
row.put("pizhun_ren", " ");
|
AuditTitleSpec spec = new AuditTitleSpec(
|
Arrays.asList("pizhun_ren"), Collections.<AuditTitleSpec.SubTableSpec>emptyList());
|
List<String> c = AuditSignEvidenceReader.candidateFields(row, spec, Collections.<String>emptyList());
|
check("S3 空值不进候选", c.isEmpty(), "实际 " + c);
|
}
|
|
/** S4 既没勾、名字也不像签名的字段不进候选——否则每次保存都要拿一堆业务值去撞签名表。 */
|
private static void unmarkedNonCredentialFieldsAreNotCandidates() {
|
Map<String, Object> row = row("chanpin_mingcheng", "青岩丹");
|
List<String> c = AuditSignEvidenceReader.candidateFields(
|
row, AuditTitleSpec.EMPTY, Collections.<String>emptyList());
|
check("S4 未勾选的普通字段不进候选", c.isEmpty(), "实际 " + c);
|
}
|
|
/** S5 同一字段两条路径都命中时只出现一次,且顺序稳定(幂等键要靠它稳定)。 */
|
private static void candidatesAreDeduplicatedAndStable() {
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
row.put("biz_sign", "852000000000000001");
|
row.put("pizhun_ren", "852000000000000003");
|
AuditTitleSpec spec = new AuditTitleSpec(
|
Arrays.asList("biz_sign", "pizhun_ren"),
|
Collections.<AuditTitleSpec.SubTableSpec>emptyList());
|
List<String> a = AuditSignEvidenceReader.candidateFields(row, spec, Collections.<String>emptyList());
|
List<String> b = AuditSignEvidenceReader.candidateFields(row, spec, Collections.<String>emptyList());
|
check("S5 去重", a.size() == 2, "实际 " + a);
|
check("S5 顺序稳定", a.equals(b), a + " vs " + b);
|
}
|
|
/**
|
* S6(评审修复轮次 1,D4):配置来源必须排在候选集合最前面——这是本任务(D4)新加的行为,
|
* 前 5 条断言都是改动前就有的旧覆盖,全传空 {@code configuredFields},测不到这条。
|
*
|
* <p>row 里故意先放一个命名规则命中的字段({@code fuhe_sign}),若没有配置来源它本应排最前;
|
* 再放一个只靠配置来源才进候选的字段({@code pizhun_ren},不命中命名规则、未勾选)。
|
* 断言 {@code get(0)} 是配置字段,直接执行验证优先级,而不是像
|
* {@code tasks/audit-delete-coverage-checks.sh} 那样只看源码文本形状——
|
* 容器换成无序 Set 这类"文本还在、行为已变"的回归,源码级断言未必能稳定抓住,靠这条兜底。
|
*/
|
private static void configuredFieldsTakePriority() {
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
row.put("fuhe_sign", "852000000000000010");
|
row.put("pizhun_ren", "852000000000000011");
|
List<String> configured = Arrays.asList("pizhun_ren");
|
List<String> c = AuditSignEvidenceReader.candidateFields(row, AuditTitleSpec.EMPTY, configured);
|
check("S6 配置来源排在候选最前", !c.isEmpty() && "pizhun_ren".equals(c.get(0)), "实际 " + c);
|
check("S6 命名规则命中的字段仍在候选里(兜底未失效)", c.contains("fuhe_sign"), "实际 " + c);
|
}
|
|
private static Map<String, Object> row(String field, Object value) {
|
Map<String, Object> row = new LinkedHashMap<String, Object>();
|
row.put(field, value);
|
return row;
|
}
|
|
private static void check(String name, boolean ok, String detail) {
|
if (ok) {
|
pass++;
|
System.out.println(" ✅ " + name);
|
} else {
|
fail++;
|
System.out.println(" ❌ " + name + " —— " + detail);
|
}
|
}
|
}
|