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> combined = new ArrayList<>(); for (Serializable entityId : entityIds) { if (!beforeById.containsKey(entityId) || !afterById.containsKey(entityId)) { continue; } String json = AuditFieldDiff.diff( beforeById.get(entityId), afterById.get(entityId), Collections.emptySet()); List items = JSON.parseArray(json, Map.class); for (Map item : items) { Map scoped = new LinkedHashMap<>(); scoped.put("targetTable", targetTable); scoped.put("targetId", String.valueOf(entityId)); scoped.putAll(item); combined.add(scoped); } } return JSON.toJSONString(combined); } static String batchSnapshot(List entityIds, Map> rowsById) { List> snapshots = new ArrayList<>(); for (Serializable entityId : entityIds) { if (!rowsById.containsKey(entityId)) { continue; } Map snapshot = new LinkedHashMap<>(); snapshot.put("targetId", String.valueOf(entityId)); String data = AuditFieldDiff.snapshot(rowsById.get(entityId), Collections.emptySet()); snapshot.put("data", data == null ? null : JSON.parseObject(data, Map.class)); snapshots.add(snapshot); } return JSON.toJSONString(snapshots); } private Serializable evalSerializable(String expr, EvaluationContext ctx) { Object value = parser.parseExpression(expr).getValue(ctx); return value instanceof Serializable ? (Serializable) value : null; } private List evalSerializableIds(String expr, EvaluationContext ctx) { return normalizeSerializableIds(parser.parseExpression(expr).getValue(ctx)); } static List normalizeSerializableIds(Object value) { LinkedHashSet ids = new LinkedHashSet<>(); if (value == null) { return new ArrayList<>(); } if (value instanceof Collection) { for (Object item : (Collection) value) { addSerializableId(ids, item); } } else if (value instanceof Iterable) { for (Object item : (Iterable) value) { addSerializableId(ids, item); } } else if (value.getClass().isArray()) { for (int i = 0; i < Array.getLength(value); i++) { addSerializableId(ids, Array.get(value, i)); } } else if (value instanceof CharSequence && String.valueOf(value).contains(",")) { for (String item : String.valueOf(value).split(",")) { addSerializableId(ids, item); } } else { addSerializableId(ids, value); } return new ArrayList<>(ids); } private static void addSerializableId(LinkedHashSet ids, Object value) { if (!(value instanceof Serializable)) { return; } if (value instanceof CharSequence) { String text = String.valueOf(value).trim(); if (!text.isEmpty()) { ids.add(text); } return; } ids.add((Serializable) value); } private Map selectAsMap(Class entityClass, Serializable id) throws Exception { Object mapper = findMapperFor(entityClass); Method selectById = null; for (Method candidate : mapper.getClass().getMethods()) { if ("selectById".equals(candidate.getName()) && candidate.getParameterTypes().length == 1) { selectById = candidate; break; } } if (selectById == null) { throw new IllegalStateException("Mapper does not expose selectById: " + mapper.getClass().getName()); } Object entity = selectById.invoke(mapper, id); return entity == null ? null : entityToColumnMap(entity); } private Object findMapperFor(Class entityClass) { Object cached = mapperCache.get(entityClass); if (cached != null) { return cached; } String simpleName = entityClass.getSimpleName(); if (!simpleName.endsWith("Entity")) { throw new IllegalArgumentException("entityClass must end with Entity: " + simpleName); } String mapperName = Character.toLowerCase(simpleName.charAt(0)) + simpleName.substring(1, simpleName.length() - "Entity".length()) + "Mapper"; Object mapper = applicationContext.getBean(mapperName); mapperCache.put(entityClass, mapper); return mapper; } /** 将实体属性按 MyBatis-Plus 注解转换为真实数据库列名,供查询侧匹配列备注。 */ private static Map entityToColumnMap(Object entity) throws IllegalAccessException { Map result = new LinkedHashMap<>(); for (Class type = entity.getClass(); type != null && type != Object.class; type = type.getSuperclass()) { for (Field field : type.getDeclaredFields()) { if (java.lang.reflect.Modifier.isStatic(field.getModifiers())) { continue; } String column = mappedColumn(field); if (column == null) { continue; } field.setAccessible(true); result.put(column, field.get(entity)); } } return result; } private static String mappedColumn(Field field) { for (Annotation annotation : field.getAnnotations()) { String annotationName = annotation.annotationType().getName(); if (!"com.baomidou.mybatisplus.annotation.TableField".equals(annotationName) && !"com.baomidou.mybatisplus.annotation.TableId".equals(annotationName)) { continue; } try { if ("com.baomidou.mybatisplus.annotation.TableField".equals(annotationName)) { Object exists = annotation.annotationType().getMethod("exist").invoke(annotation); if (Boolean.FALSE.equals(exists)) { return null; } } Object value = annotation.annotationType().getMethod("value").invoke(annotation); if (value != null && !String.valueOf(value).trim().isEmpty()) { return String.valueOf(value).trim(); } } catch (ReflectiveOperationException ignored) { // 注解版本差异时回落到属性名转换,不能让审计影响业务。 } } return camelToSnake(field.getName()); } private static String camelToSnake(String value) { StringBuilder out = new StringBuilder(value.length() + 8); for (int i = 0; i < value.length(); i++) { char ch = value.charAt(i); if (Character.isUpperCase(ch)) { out.append('_').append(Character.toLowerCase(ch)); } else { out.append(ch); } } return out.toString(); } private static final class DiffCapture { private Serializable entityId; private Map before; private List entityIds = Collections.emptyList(); private final Map> batchBefore = new LinkedHashMap<>(); private final LinkedHashSet unavailableTargetIds = new LinkedHashSet<>(); private String unavailable; } private EvaluationContext buildContext(Method method, Object[] args, Object result) { MethodBasedEvaluationContext ctx = new MethodBasedEvaluationContext(new Object(), method, args, nameDiscoverer); ctx.setVariable("result", result); return ctx; } /** * SpEL 求值 + **逐字段兜底**:求值抛异常只丢这个字段并 WARN,不让整条事件消失。 * *

为什么必须兜底:失败事件的上下文天然不完整({@code #result} 为 null), * 而 {@code #result.id} 这种非安全导航在 null 上取属性会抛 {@code SpelEvaluationException}。 * 若不兜底,一个写得不够谨慎的表达式就会让**失败事件整条消失**——那正是最需要留痕的时刻。 */ private String evalString(String expr, EvaluationContext ctx, Method method, String exprName) { if (expr == null || expr.isEmpty()) return null; try { Object v = parser.parseExpression(expr).getValue(ctx); return v == null ? null : v.toString(); } catch (RuntimeException e) { log.warn("[AuditLog] {} 的 {}={} 求值失败,该字段留空: {}", method.getName(), exprName, expr, e.getMessage()); return null; } } private String resolveTargetId(AuditLog ann, EvaluationContext ctx, Method method, DiffCapture capture) { String targetId = evalString(ann.targetIdExpr(), ctx, method, "targetIdExpr"); if (targetId != null || capture == null || capture.entityIds.isEmpty()) { return targetId; } return String.valueOf(capture.entityIds.get(0)); } }