package jnpf.audit.sdk.aspect;
import com.alibaba.fastjson.JSON;
import jnpf.audit.diff.AuditFieldDiff;
import jnpf.audit.AuditOperationIds;
import jnpf.audit.model.AuditEventDTO;
import jnpf.audit.sdk.AuditClient;
import jnpf.audit.sdk.AuditEventPublisher;
import jnpf.audit.sdk.AuditRequestCorrelationResolver;
import jnpf.audit.sdk.AuditTxHolder;
import jnpf.audit.sdk.annotation.AuditLog;
import jnpf.util.JsonUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.ApplicationContext;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.annotation.Order;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import java.io.Serializable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* 层 2 切面。
*
*
@Order(50) 是硬约束:必须小于 lims 旧 {@code BizLogAspect} 的 {@code @Order(100)},
* 使本切面包在旧切面**外层**——只有这样,本切面在 proceed() 之前写入 MDC 的 operationId,
* 内层旧切面才读得到并写进 extra_json,A/B 才能按 operation_id 精确 join。顺序写反 = 配对全空。
*
*
operationId 一律经 {@link AuditTxHolder#beginOperation} 取,**切面不得自行生成**(单源契约)。
*/
@Aspect
@Order(50)
public class AuditLogAspect {
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(AuditLogAspect.class);
private final AuditClient auditClient;
private final AuditEventPublisher publisher;
private final ApplicationContext applicationContext;
private final Map, Object> mapperCache = new ConcurrentHashMap<>();
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer nameDiscoverer = new DefaultParameterNameDiscoverer();
public AuditLogAspect(AuditClient auditClient, AuditEventPublisher publisher,
ApplicationContext applicationContext) {
this.auditClient = auditClient;
this.publisher = publisher;
this.applicationContext = applicationContext;
}
@Pointcut("@annotation(jnpf.audit.sdk.annotation.AuditLog)")
public void auditLogPointcut() {}
@Around("auditLogPointcut()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod();
AuditLog ann = AnnotationUtils.findAnnotation(method, AuditLog.class);
if (ann == null) {
return pjp.proceed();
}
// 进入即确保 operationId 存在并写入 MDC(必须在 proceed 之前——内层旧切面要读)
String correlatedOperationId = AuditOperationIds.correlation(
AuditRequestCorrelationResolver.currentBizSign());
AuditTxHolder.OperationScope scope = AuditTxHolder.beginOperation(publisher, correlatedOperationId);
try {
DiffCapture diffCapture = captureBefore(ann, method, pjp.getArgs());
Object result;
try {
result = pjp.proceed();
} catch (Throwable biz) {
// 失败留痕:只在显式声明 failureAction 时记录(默认留空 = M2 原行为不变)。
// 三条硬约束:① 记录失败绝不能改变业务异常的传播——原样 rethrow,不包装不吞;
// ② 审计自身出错也不能影响业务,故内层再套一层 catch;③ 业务异常先于审计发生,
// 事务很可能已标记回滚,所以失败事件走的仍是 holder 缓冲那条路(与成功路径一致),
// 由 SDK 决定最终投递方式,切面不自作主张直发。
if (!ann.failureAction().isEmpty()) {
try {
record(ann, method, pjp.getArgs(), null, scope.operationId(), biz, diffCapture);
} catch (Throwable t) {
log.error("[AuditLog] 记录失败事件时自身出错: method={}, failureAction={}",
method.getName(), ann.failureAction(), t);
}
}
throw biz;
}
// recordSuccess=false = 成功事实由别处记(当前只有电子签名,见注解 javadoc)。
// 只关成功侧这一扇门:上面失败侧的 failureAction 分支完全不受影响。
if (ann.recordSuccess()) {
try {
record(ann, method, pjp.getArgs(), result, scope.operationId(), null, diffCapture);
} catch (Throwable t) {
// 审计写入不得影响业务返回(与旧 BizLogAspect 同款护栏)
log.error("[AuditLog] 记录审计失败: method={}, action={}", method.getName(), ann.action(), t);
}
}
return result;
} finally {
scope.close();
}
}
/**
* @param failure 非 null 表示这是一条**失败事件**:动作码取 {@code failureAction}、
* 异常 message 落 {@code reason},且 {@code #result} 在 SpEL 里恒为 null
*/
private void record(AuditLog ann, Method method, Object[] args, Object result,
String operationId, Throwable failure, DiffCapture diffCapture) {
boolean failed = failure != null;
EvaluationContext ctx = buildContext(method, args, result);
String bizCode = evalString(ann.bizCodeExpr(), ctx, method, "bizCodeExpr");
if (bizCode == null || bizCode.isEmpty()) {
if (ann.bizCodeRequired()) {
log.warn("[AuditLog] {} 的 bizCodeExpr={} 求值为空,跳过", method.getName(), ann.bizCodeExpr());
return;
}
// bizCodeRequired=false:留空照记(审计红线是"这次操作发生过"不能缺,见注解 javadoc)
bizCode = null;
}
String actionCode = failed ? ann.failureAction() : ann.action();
String actionLabel = failed
? (ann.failureLabel().isEmpty() ? actionCode : ann.failureLabel())
: (ann.label().isEmpty() ? actionCode : ann.label());
AuditEventDTO dto = AuditEventDTO.builder()
.operationId(operationId)
.eventType(ann.eventType())
.actionCode(actionCode)
.actionLabel(actionLabel)
.bizType(ann.bizType().isEmpty() ? null : ann.bizType())
.bizCode(bizCode)
.targetTable(ann.targetTable().isEmpty() ? null : ann.targetTable())
.targetId(resolveTargetId(ann, ctx, method, diffCapture))
.sourceLayer(2)
.build();
// reason 列 = 操作者陈述的业务原因(如签名 note),成功失败都填、语义不随成败变
dto.setReason(evalString(ann.reasonExpr(), ctx, method, "reasonExpr"));
if (!failed && ann.diff()) {
populateDiff(dto, ann, method, ctx, diffCapture);
}
String requestTitle = AuditRequestTitleResolver.resolveCurrentRequest();
dto.setRecordTitle(requestTitle == null ? dto.getTargetId() : requestTitle);
Map extra = new LinkedHashMap<>();
if (!ann.extraExpr().isEmpty()) {
// 失败路径上 extraExpr 若引用 #result 会求值失败——只丢 extra 不丢整条事件
try {
Object extraObj = parser.parseExpression(ann.extraExpr()).getValue(ctx);
if (extraObj instanceof Map) {
// 拷进可变 map:SpEL 字面量 map 的可变性没有契约保证,而下面要 put failureReason
((Map, ?>) extraObj).forEach((k, v) -> extra.put(String.valueOf(k), v));
}
} catch (RuntimeException e) {
log.warn("[AuditLog] {} 的 extraExpr={} 求值失败(failed={}),本条事件不带 extra: {}",
method.getName(), ann.extraExpr(), failed, e.getMessage());
}
}
if (failed) {
// 失败的**技术**原因归 extra,不占 reason 列(见 @AuditLog#reasonExpr 的语义约定)
extra.put("failureReason", failure.getMessage());
extra.put("failureType", failure.getClass().getSimpleName());
}
if (diffCapture != null && diffCapture.unavailable != null) {
extra.put("diffUnavailable", diffCapture.unavailable);
}
if (diffCapture != null && !diffCapture.unavailableTargetIds.isEmpty()) {
extra.put("diffUnavailableTargetIds", new ArrayList<>(diffCapture.unavailableTargetIds));
}
if (!extra.isEmpty()) {
dto.setExtra(JsonUtil.getObjectToString(extra)); // String 存 JSON → 落 text 列(L043)
}
auditClient.record(dto); // 内部经 collector 填齐八必填 + tenant 回落,再交 holder 缓冲
}
private DiffCapture captureBefore(AuditLog ann, Method method, Object[] args) {
if (!ann.diff()) {
return null;
}
DiffCapture capture = new DiffCapture();
if (ann.entityClass() == Void.class
|| (ann.targetIdExpr().isEmpty() && ann.targetIdsExpr().isEmpty())) {
capture.unavailable = "diff=true requires entityClass and targetIdExpr/targetIdsExpr";
return capture;
}
try {
if (!ann.targetIdsExpr().isEmpty()) {
capture.entityIds = evalSerializableIds(
ann.targetIdsExpr(), buildContext(method, args, null));
for (Serializable entityId : capture.entityIds) {
try {
capture.batchBefore.put(entityId, selectAsMap(ann.entityClass(), entityId));
} catch (Throwable t) {
capture.unavailableTargetIds.add(String.valueOf(entityId));
log.warn("[AuditLog] {} 查询批量修改前数据失败: targetId={}, err={}",
method.getName(), entityId, t.getMessage());
}
}
return capture;
}
capture.entityId = evalSerializable(ann.targetIdExpr(), buildContext(method, args, null));
if (capture.entityId != null) {
capture.before = selectAsMap(ann.entityClass(), capture.entityId);
}
} catch (Throwable t) {
capture.unavailable = t.getMessage();
log.warn("[AuditLog] {} 查询修改前数据失败: {}", method.getName(), t.getMessage());
}
return capture;
}
private void populateDiff(AuditEventDTO dto, AuditLog ann, Method method,
EvaluationContext ctx, DiffCapture capture) {
if (capture == null) {
return;
}
if (capture.unavailable != null) {
return;
}
try {
if (!capture.entityIds.isEmpty()) {
populateBatchDiff(dto, ann, method, capture);
return;
}
Serializable entityId = capture.entityId;
if (entityId == null) {
entityId = evalSerializable(ann.targetIdExpr(), ctx);
}
Map after = entityId == null ? null : selectAsMap(ann.entityClass(), entityId);
dto.setFieldDiffs(AuditFieldDiff.diff(capture.before, after, Collections.emptySet()));
dto.setDataSnapshot(AuditFieldDiff.snapshot(
"DELETE".equalsIgnoreCase(ann.action()) ? capture.before : after,
Collections.emptySet()));
} catch (Throwable t) {
capture.unavailable = t.getMessage();
log.warn("[AuditLog] {} 查询修改后数据或生成 diff 失败: {}", method.getName(), t.getMessage());
}
}
private void populateBatchDiff(AuditEventDTO dto, AuditLog ann, Method method,
DiffCapture capture) {
Map> afterById = new LinkedHashMap<>();
for (Serializable entityId : capture.entityIds) {
if (capture.unavailableTargetIds.contains(String.valueOf(entityId))) {
continue;
}
try {
afterById.put(entityId, selectAsMap(ann.entityClass(), entityId));
} catch (Throwable t) {
capture.unavailableTargetIds.add(String.valueOf(entityId));
log.warn("[AuditLog] {} 查询批量修改后数据失败: targetId={}, err={}",
method.getName(), entityId, t.getMessage());
}
}
boolean delete = "DELETE".equalsIgnoreCase(ann.action());
dto.setFieldDiffs(batchFieldDiffs(
ann.targetTable(), capture.entityIds, capture.batchBefore, afterById));
dto.setDataSnapshot(batchSnapshot(
capture.entityIds, delete ? capture.batchBefore : afterById));
}
static String batchFieldDiffs(String targetTable,
List entityIds,
Map> beforeById,
Map> afterById) {
List