刘光辉
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
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);
        }
    }
}