刘光辉
昨天 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
package jnpf.audit.sdk.annotation;
 
import jnpf.audit.AuditConsts;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
/**
 * 层 2 审计注解:给非 CRUD 动作或需要精确动作名的场景一行声明语义。
 *
 * <p>与 lims 旧 {@code @BizLog} 的关系(M2 影子期):**并存不互斥**,同方法双标注、各写各表,
 * 靠 operationId 精确配对做 A/B;M3 达标后才删旧的。
 *
 * <p>已知限制沿袭:同类自调用绕开代理时注解不生效(与 @BizLog 同)。
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuditLog {
 
    /** 事件类型,受控枚举(DATA_CHANGE / BIZ_ACTION / E_SIGNATURE),默认业务动作 */
    String eventType() default AuditConsts.TYPE_BIZ_ACTION;
 
    /** 动作码:基础三动作 CREATE/UPDATE/DELETE 或业务自定义大写蛇形 */
    String action();
 
    /**
     * 方法抛异常时用这个动作码记一条**失败事件**;留空(默认)= 抛异常时不记录。
     *
     * <p>默认留空是为了**向后兼容**:M2 已有的层 2 埋点行为一字不变(只在成功后记录)。
     * 想要"失败也留痕"的场景(电子签名、权限校验、密码修改等)显式声明即可,
     * 例如 {@code action="SIGN", failureAction="SIGN_FAIL"}。
     *
     * <p>为什么拆成两个动作码而不是共用一个加标记:`audit_events` 上有
     * {@code idx_audit_events_action_code} 索引,按动作码筛是最快路径;而"签名失败尝试"
     * 在合规上是独立关注对象(连续失败可能是有人在试密码)。共用一个码就只能去 extra
     * 的 text 列里捞。
     *
     * <p>⚠️ 失败事件求值时 {@code #result} 恒为 null(方法没有返回值)。凡引用了
     * {@code #result} 的 SpEL 在失败路径上取不到值——切面对**每个字段单独兜底**
     * (求值异常只丢该字段并 WARN,不会丢掉整条事件),但写表达式时优先用安全导航
     * {@code #result?.id},让意图显式。
     */
    String failureAction() default "";
 
    /** 失败事件的人读动作名,留空则回落 {@link #failureAction()} */
    String failureLabel() default "";
 
    /**
     * 成功时是否记录事件。默认 true(M2 既有埋点行为一字不变)。
     *
     * <p>声明 {@code false} 的唯一正当场景是:<b>成功事实要由别处更完整地记录</b>。
     * 当前只有电子签名——签名成功当场落库会产生「签名有记录、业务失败没记录」的孤儿行,
     * 药厂合规上不接受(签名 + 业务是一个整体)。成功侧改由层 0 在<b>业务保存成功后</b>
     * 据表单里的签名字段补写,失败侧({@code SIGN_FAIL})仍在此记录——
     * 失败的签名尝试是独立的安全关注对象(连续失败可能是有人在试密码),
     * 它不隶属于任何一次成功业务,没有"跟着业务一起不记"的道理。
     *
     * <p>⚠️ 声明 false 前必须先确认成功事实<b>确实</b>有另一条链路在记。
     * 否则这就是在静默地删掉一个埋点。
     */
    boolean recordSuccess() default true;
 
    /**
     * {@link #bizCodeExpr()} 求值为空时是否**跳过**记录。默认 true(保持 M2 原行为)。
     *
     * <p>声明 {@code false} 表示"没有业务单号也照记"——审计红线是**这次操作发生过不能缺**,
     * bizCode 只是便于检索。层 0 早就是这个取舍(见 {@code AuditVisualLogListener} 类注释:
     * 旧实现取不到单号就整条丢弃,新实现留空照记)。
     *
     * <p>典型场景:**电子签名**。签名往往先于业务对象存在(先签名拿 biz_sign、再创建请验单),
     * 此时 dataId 天然为空。留着默认 true 会让签名事件被静默跳过、只在日志里留一行 WARN。
     */
    boolean bizCodeRequired() default true;
 
    /** 人读动作名,如"请验单 撤回" */
    String label() default "";
 
    /** 业务对象类型(对应 audit_events.biz_type) */
    String bizType() default "";
 
    /** 业务单号 SpEL,如 {@code "#qingyandan.jianyanLiushuihao"};求值为空则跳过记录并告警 */
    String bizCodeExpr();
 
    /** 附加元数据 SpEL,求值结果须为 Map;写进 audit_events.extra */
    String extraExpr() default "";
 
    /**
     * 操作原因 SpEL,写进 {@code audit_events.reason} 独立列。典型是用户在界面填的
     * "说明 / 原因"(如签名的 note、退回的原因)。成功与失败**都求值**(它取自入参,失败时同样在)。
     *
     * <p>🔴 **本列的语义固定为"操作者陈述的业务原因",不随成败切换含义**。失败的技术原因
     * (异常 message)由切面写进 {@code extra.failureReason},不占用本列——否则查询侧
     * 拿到一个 reason 值时无法判断它是"用户填的"还是"系统报的",这一列就没法解释了。
     */
    String reasonExpr() default "";
 
    /** 目标表名(对应 audit_events.target_table) */
    String targetTable() default "";
 
    /** 目标行主键 SpEL(对应 audit_events.target_id) */
    String targetIdExpr() default "";
 
    /**
     * 批量目标主键 SpEL,结果可为 Collection、Iterable、数组或逗号分隔字符串。
     *
     * <p>配置后,切面对每个主键分别查询修改前后数据,并在每个 field_diffs 项中写入
     * targetTable/targetId。事件顶层 target_id 仍由 targetIdExpr 决定,通常取第一条。
     */
    String targetIdsExpr() default "";
 
    /** 是否做字段级 diff;true 时 entityClass + targetIdExpr/targetIdsExpr 至少配置一项 */
    boolean diff() default false;
 
    /** diff=true 时用于反射定位 Mapper 的实体类 */
    Class<?> entityClass() default Void.class;
 
}