刘光辉
昨天 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
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="jnpf.bizcommon.audit.mapper.AuditEventMapper">
 
    <!-- 自定义 SQL 不读取 MyBatis-Plus @TableField,必须显式映射下划线列名。 -->
    <resultMap id="AuditEventResultMap" type="jnpf.bizcommon.audit.entity.AuditEventEntity">
        <id column="id" property="id"/>
        <result column="client_event_id" property="clientEventId"/>
        <result column="operation_id" property="operationId"/>
        <result column="event_time" property="eventTime"/>
        <result column="tenant_id" property="tenantId"/>
        <result column="operator_id" property="operatorId"/>
        <result column="operator_name" property="operatorName"/>
        <result column="operator_org_id" property="operatorOrgId"/>
        <result column="app_name" property="appName"/>
        <result column="biz_module" property="bizModule"/>
        <result column="ip" property="ip"/>
        <result column="ip_region" property="ipRegion"/>
        <result column="browser" property="browser"/>
        <result column="os" property="os"/>
        <result column="request_uri" property="requestUri"/>
        <result column="event_type" property="eventType"/>
        <result column="event_category" property="eventCategory"/>
        <result column="action_code" property="actionCode"/>
        <result column="action_label" property="actionLabel"/>
        <result column="source_layer" property="sourceLayer"/>
        <result column="target_table" property="targetTable"/>
        <result column="target_id" property="targetId"/>
        <result column="biz_type" property="bizType"/>
        <result column="biz_code" property="bizCode"/>
        <result column="field_diffs" property="fieldDiffs"/>
        <result column="extra" property="extra"/>
        <result column="data_snapshot" property="dataSnapshot"/>
        <result column="reason" property="reason"/>
        <result column="record_title" property="recordTitle"/>
        <result column="entry_type" property="entryType"/>
        <result column="entry_id" property="entryId"/>
        <result column="entry_name" property="entryName"/>
        <result column="prev_hash" property="prevHash"/>
        <result column="created_at" property="createdAt"/>
    </resultMap>
 
    <insert id="insertIgnore" parameterType="jnpf.bizcommon.audit.entity.AuditEventEntity">
        INSERT INTO audit_events (client_event_id, operation_id, event_time, tenant_id,
            operator_id, operator_name, operator_org_id, app_name, biz_module,
            ip, ip_region, browser, os, request_uri,
            event_type, event_category, action_code, action_label, source_layer,
            target_table, target_id, biz_type, biz_code,
            field_diffs, extra, data_snapshot, reason,
            record_title, entry_type, entry_id, entry_name, prev_hash)
        VALUES (#{clientEventId}, #{operationId}, #{eventTime}, #{tenantId},
            #{operatorId}, #{operatorName}, #{operatorOrgId}, #{appName}, #{bizModule},
            #{ip}, #{ipRegion}, #{browser}, #{os}, #{requestUri},
            #{eventType}, #{eventCategory}, #{actionCode}, #{actionLabel}, #{sourceLayer},
            #{targetTable}, #{targetId}, #{bizType}, #{bizCode},
            #{fieldDiffs}, #{extra}, #{dataSnapshot}, #{reason},
            #{recordTitle}, #{entryType}, #{entryId}, #{entryName}, #{prevHash})
        ON CONFLICT DO NOTHING
    </insert>
 
    <select id="selectByOperationCategoryForUpdate" resultMap="AuditEventResultMap">
        SELECT id, client_event_id, operation_id, event_time, tenant_id,
               operator_id, operator_name, operator_org_id, app_name, biz_module,
               ip, ip_region, browser, os, request_uri,
               event_type, event_category, action_code, action_label, source_layer,
               target_table, target_id, biz_type, biz_code,
               field_diffs, extra, data_snapshot, reason,
               record_title, entry_type, entry_id, entry_name, prev_hash, created_at
          FROM audit_events
         WHERE operation_id = #{operationId}
           AND event_category = #{eventCategory}
         FOR UPDATE
    </select>
 
    <update id="updateMerged" parameterType="jnpf.bizcommon.audit.entity.AuditEventEntity">
        UPDATE audit_events
           SET event_time = #{eventTime}, tenant_id = #{tenantId},
               operator_id = #{operatorId}, operator_name = #{operatorName},
               operator_org_id = #{operatorOrgId}, app_name = #{appName},
               biz_module = #{bizModule}, ip = #{ip}, ip_region = #{ipRegion},
               browser = #{browser}, os = #{os}, request_uri = #{requestUri},
               event_type = #{eventType}, action_code = #{actionCode},
               action_label = #{actionLabel}, source_layer = #{sourceLayer},
               target_table = #{targetTable}, target_id = #{targetId},
               biz_type = #{bizType}, biz_code = #{bizCode},
               field_diffs = #{fieldDiffs}, extra = #{extra},
               data_snapshot = #{dataSnapshot}, reason = #{reason},
               record_title = #{recordTitle}, entry_type = #{entryType},
               entry_id = #{entryId}, entry_name = #{entryName}, prev_hash = #{prevHash}
         WHERE id = #{id}
    </update>
 
    <!-- 已记过签名事件的 signId(A1-b,2026-08-03,Codex 终审 High③)。
         走 idx_audit_events_target (target_table, target_id)——target_table 是前导列,
         而签名事件的 target_table 恒为签名凭证表、target_id 恒为 signId,故 IN 直接打在索引上。
 
         **排除本次 operation_id**:MQ 重投递会让同一条事件被消费两次,批量场景下 N 条事件
         还共享同一个 batchOpId。不排除的话,同一次操作自己刚写下的签名事件会把自己判成历史,
         判定就不再对「同一次操作」保持稳定。operation_id 可为 NULL(旧数据),显式放行。
 
         **不按 tenant_id 收窄**:signId 是雪花 id、全局唯一,多一个条件只会在租户列漂移时
         把「记过」误判成「没记过」,而那个方向的误判会重发一条历史签名事件——正是要防的。 -->
    <!-- 🔴 判据必须同时钉死 action_code **与 source_layer**(2026-08-03 Codex 终审缺陷 2)。
         `event_type='E_SIGNATURE' + target_table='lims_sign'` 这个组合下实测有三个层、四种动作:
             source_layer=0 action_code=SIGN       83 条  ← 只有这一类是本判定的对象
             source_layer=2 action_code=SIGN        8 条
             source_layer=2 action_code=SIGN_FAIL   2 条
             source_layer=3 action_code=SIGN_USED   8 条
         只筛 event_type 会把后 18 条也算成「层 0 记过了」,于是真实的复核签名被判成历史、
         本次 SIGN 事件被抑制。**光加 action_code='SIGN' 还不够**——层 2 也写这个动作码,
         所以 source_layer 一并钉住。
         租户收窄(缺陷 5):lims_sign 主键是 (f_id, f_tenant_id),f_id 单列唯一**不是**
         数据库不变量,跨租户撞车时会把别人租户记过的签名算成本租户的历史。
         **恒收窄,没有「缺席时放行」的逃逸**(2026-08-05 Codex 复审 Medium③):调用方的
         AuditCtx.tenantId 初值就是 AuditApiConsts.DEFAULT_TENANT_ID、applyTenant 只在拿到
         非空白值时才覆盖,**永不为 null**——原先那个 `#{tenantId} IS NULL OR` 前半永远不成立,
         是从未生效过的死代码,连带那段「缺席时不收窄」的注释也在描述一个不存在的行为。
         而写入侧的签名事件同样落 ctx.tenantId(见 listener 的 .tenantId(ctx.tenantId)),
         两侧同源,恒收窄不会漏查自己写下的那些行。 -->
    <select id="selectRecordedSignIds" resultType="java.lang.String">
        SELECT DISTINCT target_id FROM audit_events
         WHERE target_table = #{signTable}
           AND event_type = #{eventType}
           AND action_code = #{actionCode}
           AND source_layer = #{sourceLayer}
           AND target_id IN
        <foreach collection="signIds" item="signId" open="(" separator="," close=")">
            #{signId}
        </foreach>
           AND (operation_id IS NULL OR operation_id &lt;&gt; #{operationId})
           AND tenant_id = #{tenantId}
    </select>
 
</mapper>