From 34981c30a78e8bbd7791131059a9210f9928b62c Mon Sep 17 00:00:00 2001
From: 刘光辉 <347230014@qq.com>
Date: 星期四, 17 九月 2026 09:24:09 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/master' into master
---
jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/aspect/AuditLogAspect.java | 502 +++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 502 insertions(+), 0 deletions(-)
diff --git a/jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/aspect/AuditLogAspect.java b/jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/aspect/AuditLogAspect.java
new file mode 100644
index 0000000..816669e
--- /dev/null
+++ b/jnpf-biz-common/jnpf-audit-sdk/src/main/java/jnpf/audit/sdk/aspect/AuditLogAspect.java
@@ -0,0 +1,502 @@
+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 鍒囬潰銆�
+ *
+ * <p><b>@Order(50) 鏄‖绾︽潫</b>锛氬繀椤诲皬浜� lims 鏃� {@code BizLogAspect} 鐨� {@code @Order(100)}锛�
+ * 浣挎湰鍒囬潰鍖呭湪鏃у垏闈�**澶栧眰**鈥斺�斿彧鏈夎繖鏍凤紝鏈垏闈㈠湪 proceed() 涔嬪墠鍐欏叆 MDC 鐨� operationId锛�
+ * 鍐呭眰鏃у垏闈㈡墠璇诲緱鍒板苟鍐欒繘 extra_json锛孉/B 鎵嶈兘鎸� operation_id 绮剧‘ join銆傞『搴忓啓鍙� = 閰嶅鍏ㄧ┖銆�
+ *
+ * <p>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<Class<?>, 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) {
+ // 澶辫触鐣欑棔锛氬彧鍦ㄦ樉寮忓0鏄� failureAction 鏃惰褰曪紙榛樿鐣欑┖ = M2 鍘熻涓轰笉鍙橈級銆�
+ // 涓夋潯纭害鏉燂細鈶� 璁板綍澶辫触缁濅笉鑳芥敼鍙樹笟鍔″紓甯哥殑浼犳挱鈥斺�斿師鏍� rethrow锛屼笉鍖呰涓嶅悶锛�
+ // 鈶� 瀹¤鑷韩鍑洪敊涔熶笉鑳藉奖鍝嶄笟鍔★紝鏁呭唴灞傚啀濂椾竴灞� catch锛涒憿 涓氬姟寮傚父鍏堜簬瀹¤鍙戠敓锛�
+ // 浜嬪姟寰堝彲鑳藉凡鏍囪鍥炴粴锛屾墍浠ュけ璐ヤ簨浠惰蛋鐨勪粛鏄� holder 缂撳啿閭f潯璺紙涓庢垚鍔熻矾寰勪竴鑷达級锛�
+ // 鐢� 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<String, Object> extra = new LinkedHashMap<>();
+ if (!ann.extraExpr().isEmpty()) {
+ // 澶辫触璺緞涓� extraExpr 鑻ュ紩鐢� #result 浼氭眰鍊煎け璐モ�斺�斿彧涓� extra 涓嶄涪鏁存潯浜嬩欢
+ try {
+ Object extraObj = parser.parseExpression(ann.extraExpr()).getValue(ctx);
+ if (extraObj instanceof Map) {
+ // 鎷疯繘鍙彉 map锛歋pEL 瀛楅潰閲� 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<String, Object> 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<Serializable, Map<String, Object>> 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<Serializable> entityIds,
+ Map<Serializable, Map<String, Object>> beforeById,
+ Map<Serializable, Map<String, Object>> afterById) {
+ List<Map<String, Object>> 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<Map> items = JSON.parseArray(json, Map.class);
+ for (Map item : items) {
+ Map<String, Object> 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<Serializable> entityIds,
+ Map<Serializable, Map<String, Object>> rowsById) {
+ List<Map<String, Object>> snapshots = new ArrayList<>();
+ for (Serializable entityId : entityIds) {
+ if (!rowsById.containsKey(entityId)) {
+ continue;
+ }
+ Map<String, Object> 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<Serializable> evalSerializableIds(String expr, EvaluationContext ctx) {
+ return normalizeSerializableIds(parser.parseExpression(expr).getValue(ctx));
+ }
+
+ static List<Serializable> normalizeSerializableIds(Object value) {
+ LinkedHashSet<Serializable> 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<Serializable> 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<String, Object> 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<String, Object> entityToColumnMap(Object entity) throws IllegalAccessException {
+ Map<String, Object> 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<String, Object> before;
+ private List<Serializable> entityIds = Collections.emptyList();
+ private final Map<Serializable, Map<String, Object>> batchBefore = new LinkedHashMap<>();
+ private final LinkedHashSet<String> 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锛屼笉璁╂暣鏉′簨浠舵秷澶便��
+ *
+ * <p>涓轰粈涔堝繀椤诲厹搴曪細澶辫触浜嬩欢鐨勪笂涓嬫枃澶╃劧涓嶅畬鏁达紙{@code #result} 涓� null锛夛紝
+ * 鑰� {@code #result.id} 杩欑闈炲畨鍏ㄥ鑸湪 null 涓婂彇灞炴�т細鎶� {@code SpelEvaluationException}銆�
+ * 鑻ヤ笉鍏滃簳锛屼竴涓啓寰椾笉澶熻皑鎱庣殑琛ㄨ揪寮忓氨浼氳**澶辫触浜嬩欢鏁存潯娑堝け**鈥斺�旈偅姝f槸鏈�闇�瑕佺暀鐥曠殑鏃跺埢銆�
+ */
+ 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));
+ }
+}
--
Gitblit v1.8.0