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 动作或需要精确动作名的场景一行声明语义。 * *

与 lims 旧 {@code @BizLog} 的关系(M2 影子期):**并存不互斥**,同方法双标注、各写各表, * 靠 operationId 精确配对做 A/B;M3 达标后才删旧的。 * *

已知限制沿袭:同类自调用绕开代理时注解不生效(与 @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(); /** * 方法抛异常时用这个动作码记一条**失败事件**;留空(默认)= 抛异常时不记录。 * *

默认留空是为了**向后兼容**:M2 已有的层 2 埋点行为一字不变(只在成功后记录)。 * 想要"失败也留痕"的场景(电子签名、权限校验、密码修改等)显式声明即可, * 例如 {@code action="SIGN", failureAction="SIGN_FAIL"}。 * *

为什么拆成两个动作码而不是共用一个加标记:`audit_events` 上有 * {@code idx_audit_events_action_code} 索引,按动作码筛是最快路径;而"签名失败尝试" * 在合规上是独立关注对象(连续失败可能是有人在试密码)。共用一个码就只能去 extra * 的 text 列里捞。 * *

⚠️ 失败事件求值时 {@code #result} 恒为 null(方法没有返回值)。凡引用了 * {@code #result} 的 SpEL 在失败路径上取不到值——切面对**每个字段单独兜底** * (求值异常只丢该字段并 WARN,不会丢掉整条事件),但写表达式时优先用安全导航 * {@code #result?.id},让意图显式。 */ String failureAction() default ""; /** 失败事件的人读动作名,留空则回落 {@link #failureAction()} */ String failureLabel() default ""; /** * 成功时是否记录事件。默认 true(M2 既有埋点行为一字不变)。 * *

声明 {@code false} 的唯一正当场景是:成功事实要由别处更完整地记录。 * 当前只有电子签名——签名成功当场落库会产生「签名有记录、业务失败没记录」的孤儿行, * 药厂合规上不接受(签名 + 业务是一个整体)。成功侧改由层 0 在业务保存成功后 * 据表单里的签名字段补写,失败侧({@code SIGN_FAIL})仍在此记录—— * 失败的签名尝试是独立的安全关注对象(连续失败可能是有人在试密码), * 它不隶属于任何一次成功业务,没有"跟着业务一起不记"的道理。 * *

⚠️ 声明 false 前必须先确认成功事实确实有另一条链路在记。 * 否则这就是在静默地删掉一个埋点。 */ boolean recordSuccess() default true; /** * {@link #bizCodeExpr()} 求值为空时是否**跳过**记录。默认 true(保持 M2 原行为)。 * *

声明 {@code false} 表示"没有业务单号也照记"——审计红线是**这次操作发生过不能缺**, * bizCode 只是便于检索。层 0 早就是这个取舍(见 {@code AuditVisualLogListener} 类注释: * 旧实现取不到单号就整条丢弃,新实现留空照记)。 * *

典型场景:**电子签名**。签名往往先于业务对象存在(先签名拿 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、退回的原因)。成功与失败**都求值**(它取自入参,失败时同样在)。 * *

🔴 **本列的语义固定为"操作者陈述的业务原因",不随成败切换含义**。失败的技术原因 * (异常 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、数组或逗号分隔字符串。 * *

配置后,切面对每个主键分别查询修改前后数据,并在每个 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; }