<?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 <> #{operationId})
|
AND tenant_id = #{tenantId}
|
</select>
|
|
</mapper>
|