|
|
@@ -0,0 +1,901 @@
|
|
|
+package shop.alien.store.aspect;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+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.core.DefaultParameterNameDiscoverer;
|
|
|
+import org.springframework.core.annotation.Order;
|
|
|
+import org.springframework.expression.EvaluationContext;
|
|
|
+import org.springframework.expression.Expression;
|
|
|
+import org.springframework.expression.common.TemplateParserContext;
|
|
|
+import org.springframework.expression.spel.standard.SpelExpressionParser;
|
|
|
+import org.springframework.beans.BeansException;
|
|
|
+import org.springframework.context.ApplicationContext;
|
|
|
+import org.springframework.context.ApplicationContextAware;
|
|
|
+import org.springframework.context.expression.BeanFactoryResolver;
|
|
|
+import org.springframework.expression.spel.support.StandardEvaluationContext;
|
|
|
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.web.context.request.RequestContextHolder;
|
|
|
+import org.springframework.web.context.request.ServletRequestAttributes;
|
|
|
+import shop.alien.entity.store.OperationLog;
|
|
|
+import shop.alien.store.service.OperationLogService;
|
|
|
+import shop.alien.store.util.ContentGeneratorUtil;
|
|
|
+import shop.alien.store.util.DataCompareUtil;
|
|
|
+import shop.alien.util.common.JwtUtil;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import java.io.Serializable;
|
|
|
+import java.lang.reflect.Field;
|
|
|
+import java.lang.reflect.Method;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 操作日志切面
|
|
|
+ *
|
|
|
+ * @author ssk
|
|
|
+ * @since 2025-12-09
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Aspect
|
|
|
+@Component
|
|
|
+@Order(1)
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class OperationLogAspect implements ApplicationContextAware {
|
|
|
+
|
|
|
+ private ApplicationContext applicationContext;
|
|
|
+
|
|
|
+ private final OperationLogService operationLogService;
|
|
|
+
|
|
|
+ private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
|
|
|
+ private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
|
|
|
+ // 用於解析模板表達式(如 "新增了節假日,節日名稱:#{#holiday.festivalName}")
|
|
|
+ private final TemplateParserContext templateParserContext = new TemplateParserContext("#{", "}");
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
|
|
+ this.applicationContext = applicationContext;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 定义切点:所有标注了@OperationLog注解的方法
|
|
|
+ */
|
|
|
+ @Pointcut("@annotation(shop.alien.store.annotation.ChangeRecordLog)")
|
|
|
+ public void operationLogPointcut() {
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 环绕通知:记录操作日志
|
|
|
+ */
|
|
|
+ @Around("operationLogPointcut()")
|
|
|
+ public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
|
|
+ log.info("========== AOP 切面被觸發 ==========");
|
|
|
+ log.info("目標類: {}", joinPoint.getTarget().getClass().getName());
|
|
|
+ log.info("方法: {}", joinPoint.getSignature().getName());
|
|
|
+
|
|
|
+ // 在方法执行前获取注解信息,用于查询修改前的数据
|
|
|
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
|
+ Method method = signature.getMethod();
|
|
|
+ shop.alien.store.annotation.ChangeRecordLog annotation = method.getAnnotation(shop.alien.store.annotation.ChangeRecordLog.class);
|
|
|
+
|
|
|
+ // 在方法执行前查询修改前的数据(对于UPDATE操作,必须在方法执行前查询)
|
|
|
+ Object beforeDataObj = null;
|
|
|
+ if (annotation != null && annotation.recordBefore() && annotation.type().name().equals("UPDATE")) {
|
|
|
+ try {
|
|
|
+ log.info("========== 方法執行前:開始查詢修改前的數據 ==========");
|
|
|
+ Object[] args = joinPoint.getArgs();
|
|
|
+ String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
|
|
|
+
|
|
|
+ Object paramObj = null;
|
|
|
+ Object idValue = null;
|
|
|
+
|
|
|
+ // 查找包含ID的參數對象
|
|
|
+ if (args != null && parameterNames != null) {
|
|
|
+ for (int i = 0; i < args.length; i++) {
|
|
|
+ Object arg = args[i];
|
|
|
+ if (arg != null && !isPrimitiveOrWrapper(arg.getClass()) && !isString(arg.getClass())) {
|
|
|
+ // 嘗試從對象中提取ID
|
|
|
+ idValue = extractIdFromObject(arg, annotation.idField());
|
|
|
+ if (idValue != null) {
|
|
|
+ paramObj = arg;
|
|
|
+ log.info("從參數中提取到ID: {}, 對象類型: {}", idValue, arg.getClass().getSimpleName());
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果找到了ID,通過Mapper查詢原始數據(方法執行前)
|
|
|
+ if (idValue != null && paramObj != null) {
|
|
|
+ beforeDataObj = queryBeforeDataById(paramObj.getClass(), idValue);
|
|
|
+ if (beforeDataObj != null) {
|
|
|
+ log.info("✓ 成功查詢到修改前的數據,類型: {}", beforeDataObj.getClass().getSimpleName());
|
|
|
+ log.info("修改前的數據JSON: {}", JSON.toJSONString(beforeDataObj));
|
|
|
+ } else {
|
|
|
+ log.warn("查詢修改前的數據返回null,ID: {}", idValue);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.warn("無法從參數中提取ID,跳過查詢修改前的數據");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("方法執行前查詢修改前的數據失敗", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ long startTime = System.currentTimeMillis();
|
|
|
+ Object result = null;
|
|
|
+ Exception exception = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 执行目标方法
|
|
|
+ result = joinPoint.proceed();
|
|
|
+ log.info("目標方法執行成功,返回值: {}", result != null ? result.getClass().getSimpleName() : "null");
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ exception = e;
|
|
|
+ log.error("目標方法執行失敗", e);
|
|
|
+ throw e;
|
|
|
+ } finally {
|
|
|
+ try {
|
|
|
+ // 记录操作日志(传入方法执行前查询的修改前数据)
|
|
|
+ log.info("開始記錄操作日誌...");
|
|
|
+ recordOperationLog(joinPoint, result, exception, startTime, beforeDataObj);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("记录操作日志失败", e);
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 记录操作日志
|
|
|
+ */
|
|
|
+ private void recordOperationLog(ProceedingJoinPoint joinPoint, Object result, Exception exception, long startTime, Object preQueryBeforeData) {
|
|
|
+ try {
|
|
|
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
|
+ Method method = signature.getMethod();
|
|
|
+ shop.alien.store.annotation.ChangeRecordLog operationLogAnnotation = method.getAnnotation(shop.alien.store.annotation.ChangeRecordLog.class);
|
|
|
+
|
|
|
+ if (operationLogAnnotation == null) {
|
|
|
+ log.warn("方法 {} 上未找到 @ChangeRecordLog 註解", method.getName());
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("開始記錄操作日誌: 方法={}, 模組={}", method.getName(), operationLogAnnotation.module());
|
|
|
+
|
|
|
+ // 获取方法参数
|
|
|
+ Object[] args = joinPoint.getArgs();
|
|
|
+ String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
|
|
|
+
|
|
|
+ // 创建SpEL上下文
|
|
|
+ EvaluationContext context = createEvaluationContext(method, args, parameterNames, result);
|
|
|
+
|
|
|
+ // 构建操作日志对象
|
|
|
+ OperationLog operationLog = new OperationLog();
|
|
|
+
|
|
|
+ // 设置用户信息
|
|
|
+ setUserInfo(operationLog);
|
|
|
+
|
|
|
+ // 设置操作模块
|
|
|
+ operationLog.setOperationModule(parseSpEL(operationLogAnnotation.module(), context, String.class));
|
|
|
+
|
|
|
+ // 设置操作类型
|
|
|
+ operationLog.setOperationType(operationLogAnnotation.type().getDescription());
|
|
|
+
|
|
|
+ // 设置操作内容(支持SpEL表达式,但優先使用自動生成)
|
|
|
+ String content = null;
|
|
|
+
|
|
|
+ // 如果content不為空,嘗試解析SpEL表達式
|
|
|
+ if (StringUtils.isNotBlank(operationLogAnnotation.content())) {
|
|
|
+ content = parseSpEL(operationLogAnnotation.content(), context, String.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果content為空或為空字符串,嘗試自動生成
|
|
|
+ if (StringUtils.isBlank(content)) {
|
|
|
+ content = autoGenerateContent(operationLogAnnotation, joinPoint, args, result);
|
|
|
+ }
|
|
|
+
|
|
|
+ operationLog.setOperationContent(content);
|
|
|
+
|
|
|
+ // 设置请求信息
|
|
|
+ setRequestInfo(operationLog, joinPoint);
|
|
|
+
|
|
|
+ // 设置操作时间
|
|
|
+ operationLog.setOperationTime(new Date());
|
|
|
+
|
|
|
+ // 记录修改前后的数据(传入方法执行前查询的修改前数据)
|
|
|
+ if (operationLogAnnotation.recordBefore() || operationLogAnnotation.recordAfter()) {
|
|
|
+ recordDataChange(operationLog, joinPoint, operationLogAnnotation, context, result, preQueryBeforeData);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存操作日志
|
|
|
+ operationLogService.saveOperationLog(operationLog);
|
|
|
+ log.info("操作日誌記錄完成: 模組={}, 類型={}, 內容={}",
|
|
|
+ operationLog.getOperationModule(),
|
|
|
+ operationLog.getOperationType(),
|
|
|
+ operationLog.getOperationContent());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("記錄操作日誌時發生異常", e);
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建SpEL表达式上下文
|
|
|
+ */
|
|
|
+ private EvaluationContext createEvaluationContext(Method method, Object[] args, String[] parameterNames, Object result) {
|
|
|
+ StandardEvaluationContext context = new StandardEvaluationContext();
|
|
|
+
|
|
|
+ // 添加方法参数
|
|
|
+ if (parameterNames != null && args != null) {
|
|
|
+ for (int i = 0; i < parameterNames.length; i++) {
|
|
|
+ context.setVariable(parameterNames[i], args[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加返回值
|
|
|
+ if (result != null) {
|
|
|
+ context.setVariable("result", result);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 添加方法信息
|
|
|
+ context.setVariable("method", method);
|
|
|
+
|
|
|
+ // 添加 Spring Bean 解析器,支持在 SpEL 中訪問 Spring Bean
|
|
|
+ // 例如:@holidayService.getHolidayById(#id) 或 @holidayServiceImpl.getHolidayById(#id)
|
|
|
+ if (applicationContext != null) {
|
|
|
+ try {
|
|
|
+ context.setBeanResolver(new BeanFactoryResolver(applicationContext));
|
|
|
+ log.debug("BeanResolver設置成功");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("設置BeanResolver失敗", e);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.warn("ApplicationContext為null,無法設置BeanResolver");
|
|
|
+ }
|
|
|
+
|
|
|
+ return context;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析SpEL表达式
|
|
|
+ * 支持兩種模式:
|
|
|
+ * 1. 純字符串:直接返回
|
|
|
+ * 2. 包含SpEL表達式的模板字符串(如 "新增了節假日,節日名稱:#{#holiday.festivalName}"):解析表達式並替換
|
|
|
+ * 3. 純SpEL表達式(如 "#{#holiday.festivalName}"):解析並返回結果
|
|
|
+ */
|
|
|
+ private <T> T parseSpEL(String expression, EvaluationContext context, Class<T> clazz) {
|
|
|
+ if (expression == null || expression.trim().isEmpty()) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果字符串不包含SpEL表達式語法(#{}),直接返回字符串
|
|
|
+ if (!expression.contains("#{") && !expression.contains("${")) {
|
|
|
+ if (clazz == String.class) {
|
|
|
+ return clazz.cast(expression);
|
|
|
+ }
|
|
|
+ // 如果不是String類型,嘗試轉換
|
|
|
+ try {
|
|
|
+ if (clazz.isInstance(expression)) {
|
|
|
+ return clazz.cast(expression);
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("類型轉換失敗: {}", expression, e);
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 如果字符串包含模板表達式(如 "新增了節假日,節日名稱:#{#holiday.festivalName}")
|
|
|
+ // 使用 TemplateParserContext 來解析模板
|
|
|
+ if (expression.contains("#{") && clazz == String.class) {
|
|
|
+ Expression exp = spelExpressionParser.parseExpression(expression, templateParserContext);
|
|
|
+ Object value = exp.getValue(context, String.class);
|
|
|
+ if (value != null) {
|
|
|
+ return clazz.cast(value);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是純SpEL表達式(如 "#{#holiday.festivalName}" 或 "@holidayService.getHolidayById(#id)"),直接解析
|
|
|
+ Expression exp = spelExpressionParser.parseExpression(expression);
|
|
|
+ Object value = exp.getValue(context);
|
|
|
+ if (value == null) {
|
|
|
+ log.debug("SpEL表達式解析結果為null: {}", expression);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 檢查解析結果是否為字符串且等於原始表達式(表示解析失敗)
|
|
|
+ if (value instanceof String) {
|
|
|
+ String valueStr = (String) value;
|
|
|
+ // 如果返回的字符串等於原始表達式,或者包含 @ 或 # 符號,可能是解析失敗
|
|
|
+ if (valueStr.equals(expression) ||
|
|
|
+ (expression.startsWith("@") && valueStr.startsWith("@")) ||
|
|
|
+ (expression.startsWith("#") && valueStr.startsWith("#"))) {
|
|
|
+ log.error("SpEL表達式解析失敗,返回了原始表達式字符串。表達式: {}, 返回值: {}", expression, valueStr);
|
|
|
+ // 對於非String類型,返回null而不是原始字符串,避免後續處理錯誤
|
|
|
+ if (clazz != String.class) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (clazz.isInstance(value)) {
|
|
|
+ return clazz.cast(value);
|
|
|
+ }
|
|
|
+ return (T) value;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("解析SpEL表达式失败: {}, 錯誤: {}", expression, e.getMessage(), e);
|
|
|
+ // 如果解析失敗,且是String類型,返回原字符串
|
|
|
+ // 對於Object類型,返回null而不是原始字符串,避免後續JSON解析錯誤
|
|
|
+ if (clazz == String.class) {
|
|
|
+ return clazz.cast(expression);
|
|
|
+ }
|
|
|
+ // 對於Object.class,返回null
|
|
|
+ if (clazz == Object.class) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置用户信息
|
|
|
+ */
|
|
|
+ private void setUserInfo(OperationLog operationLog) {
|
|
|
+ try {
|
|
|
+ JSONObject userInfo = JwtUtil.getCurrentUserInfo();
|
|
|
+ if (userInfo != null) {
|
|
|
+ operationLog.setAccountId(userInfo.getString("userId"));
|
|
|
+ operationLog.setAccount(userInfo.getString("phone") != null ? userInfo.getString("phone") : userInfo.getString("account"));
|
|
|
+ operationLog.setName(userInfo.getString("userName") != null ? userInfo.getString("userName") : userInfo.getString("name"));
|
|
|
+ operationLog.setPosition(userInfo.getString("position"));
|
|
|
+ operationLog.setDepartment(userInfo.getString("department"));
|
|
|
+
|
|
|
+ // 设置用户类型
|
|
|
+ String userType = userInfo.getString("userType");
|
|
|
+ if ("platform".equals(userType) || "platform".equals(userInfo.getString("role"))) {
|
|
|
+ operationLog.setUserType("平台");
|
|
|
+ } else if ("store".equals(userType) || "merchant".equals(userType)) {
|
|
|
+ operationLog.setUserType("商家");
|
|
|
+ } else if ("lawyer".equals(userType)) {
|
|
|
+ operationLog.setUserType("律师");
|
|
|
+ } else {
|
|
|
+ operationLog.setUserType("用户");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取用户信息失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置请求信息
|
|
|
+ */
|
|
|
+ private void setRequestInfo(OperationLog operationLog, ProceedingJoinPoint joinPoint) {
|
|
|
+ try {
|
|
|
+ ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
|
|
+ if (attributes != null) {
|
|
|
+ HttpServletRequest request = attributes.getRequest();
|
|
|
+ operationLog.setMethod(request.getMethod());
|
|
|
+ operationLog.setRequestPath(request.getRequestURI());
|
|
|
+ operationLog.setIpAddress(getIpAddress(request));
|
|
|
+
|
|
|
+ // 获取请求参数
|
|
|
+ Map<String, Object> params = new HashMap<>();
|
|
|
+ if (request.getParameterMap() != null && !request.getParameterMap().isEmpty()) {
|
|
|
+ request.getParameterMap().forEach((key, values) -> {
|
|
|
+ if (values != null && values.length > 0) {
|
|
|
+ params.put(key, values.length == 1 ? values[0] : values);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ operationLog.setRequestParams(JSON.toJSONString(params));
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置方法信息
|
|
|
+ MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
|
|
+ operationLog.setMethod(signature.getDeclaringTypeName() + "." + signature.getMethod().getName());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取请求信息失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 记录数据变更(修改前后的数据)
|
|
|
+ */
|
|
|
+ private void recordDataChange(OperationLog operationLog, ProceedingJoinPoint joinPoint,
|
|
|
+ shop.alien.store.annotation.ChangeRecordLog annotation,
|
|
|
+ EvaluationContext context, Object result, Object preQueryBeforeData) {
|
|
|
+ try {
|
|
|
+ // 使用在方法执行前查询的修改前数据(如果存在)
|
|
|
+ Object beforeDataObj = preQueryBeforeData;
|
|
|
+ Object afterDataObj = null; // 修改後的數據對象
|
|
|
+
|
|
|
+ // 對於UPDATE操作,先從方法參數中獲取修改後的數據
|
|
|
+ if (annotation.type().name().equals("UPDATE")) {
|
|
|
+ Object[] args = joinPoint.getArgs();
|
|
|
+ if (args != null) {
|
|
|
+ for (Object arg : args) {
|
|
|
+ if (arg != null && !isPrimitiveOrWrapper(arg.getClass()) && !isString(arg.getClass())) {
|
|
|
+ // 找到第一個非基本類型的參數對象(通常是實體對象)
|
|
|
+ afterDataObj = arg;
|
|
|
+ log.info("從方法參數中獲取修改後的數據,類型: {}", afterDataObj.getClass().getSimpleName());
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果方法执行前没有查询到修改前的数据,则在这里查询(对于非UPDATE操作或其他情况)
|
|
|
+ if (annotation.recordBefore() && beforeDataObj == null) {
|
|
|
+ try {
|
|
|
+ log.info("========== 方法執行後:開始查詢修改前的數據 ==========");
|
|
|
+ // 從方法參數中提取ID和對象
|
|
|
+ Object[] args = joinPoint.getArgs();
|
|
|
+ String[] parameterNames = parameterNameDiscoverer.getParameterNames(
|
|
|
+ ((MethodSignature) joinPoint.getSignature()).getMethod());
|
|
|
+
|
|
|
+ Object paramObj = null;
|
|
|
+ Object idValue = null;
|
|
|
+
|
|
|
+ // 查找包含ID的參數對象
|
|
|
+ if (args != null && parameterNames != null) {
|
|
|
+ for (int i = 0; i < args.length; i++) {
|
|
|
+ Object arg = args[i];
|
|
|
+ if (arg != null && !isPrimitiveOrWrapper(arg.getClass()) && !isString(arg.getClass())) {
|
|
|
+ // 嘗試從對象中提取ID
|
|
|
+ idValue = extractIdFromObject(arg, annotation.idField());
|
|
|
+ if (idValue != null) {
|
|
|
+ paramObj = arg;
|
|
|
+ log.info("從參數中提取到ID: {}, 對象類型: {}", idValue, arg.getClass().getSimpleName());
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ } else if (arg != null && (arg instanceof Integer || arg instanceof Long || arg instanceof String)) {
|
|
|
+ // 如果參數本身就是ID
|
|
|
+ idValue = arg;
|
|
|
+ log.info("參數本身就是ID: {}", idValue);
|
|
|
+ // 需要找到對應的實體類,這裡先嘗試從其他參數中找
|
|
|
+ for (int j = 0; j < args.length; j++) {
|
|
|
+ if (j != i && args[j] != null &&
|
|
|
+ !isPrimitiveOrWrapper(args[j].getClass()) &&
|
|
|
+ !isString(args[j].getClass())) {
|
|
|
+ paramObj = args[j];
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果找到了ID,通過Mapper查詢原始數據
|
|
|
+ if (idValue != null && paramObj != null) {
|
|
|
+ beforeDataObj = queryBeforeDataById(paramObj.getClass(), idValue);
|
|
|
+ } else if (idValue != null) {
|
|
|
+ // 只有ID,沒有對象,嘗試通過ID類型推斷實體類
|
|
|
+ log.warn("只有ID沒有對象,無法自動查詢原始數據。ID: {}", idValue);
|
|
|
+ } else {
|
|
|
+ log.warn("無法從參數中提取ID,跳過查詢修改前的數據");
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查詢修改前的數據失敗", e);
|
|
|
+ // 記錄錯誤但不影響主流程
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 记录修改前的数据
|
|
|
+ if (beforeDataObj != null) {
|
|
|
+ try {
|
|
|
+ String beforeData = JSON.toJSONString(beforeDataObj);
|
|
|
+ operationLog.setBeforeData(beforeData);
|
|
|
+ log.info("========== 成功獲取修改前的數據 ==========");
|
|
|
+ log.info("修改前的數據類型: {}", beforeDataObj.getClass().getName());
|
|
|
+ log.info("修改前的完整JSON: {}", beforeData);
|
|
|
+ log.info("數據長度: {}", beforeData.length());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("序列化修改前的數據失敗", e);
|
|
|
+ operationLog.setBeforeData("序列化失敗: " + e.getMessage());
|
|
|
+ }
|
|
|
+ } else if (annotation.recordBefore()) {
|
|
|
+ log.error("========== 修改前的數據為null ==========");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 记录修改后的数据
|
|
|
+ if (annotation.recordAfter()) {
|
|
|
+ // 優先使用從參數中獲取的修改後數據(對於UPDATE操作)
|
|
|
+ Object actualResult = afterDataObj;
|
|
|
+
|
|
|
+ // 如果參數中沒有,則從返回值中提取
|
|
|
+ if (actualResult == null && result != null) {
|
|
|
+ log.info("========== 從返回值提取修改後的數據 ==========");
|
|
|
+ log.info("result類型: {}", result.getClass().getName());
|
|
|
+ log.info("result的JSON: {}", JSON.toJSONString(result));
|
|
|
+
|
|
|
+ actualResult = extractDataFromResult(result);
|
|
|
+
|
|
|
+ log.info("提取後的actualResult類型: {}", actualResult != null ? actualResult.getClass().getName() : "null");
|
|
|
+ log.info("提取後的actualResult的JSON: {}", actualResult != null ? JSON.toJSONString(actualResult) : "null");
|
|
|
+ }
|
|
|
+
|
|
|
+ if (actualResult != null) {
|
|
|
+ String afterData = JSON.toJSONString(actualResult);
|
|
|
+ operationLog.setAfterData(afterData);
|
|
|
+ log.info("記錄修改後的數據,類型: {}, 數據長度: {}",
|
|
|
+ actualResult.getClass().getSimpleName(),
|
|
|
+ afterData.length());
|
|
|
+ } else {
|
|
|
+ log.warn("無法獲取修改後的數據");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果同時有修改前的數據,則生成詳細的變更描述
|
|
|
+ if (beforeDataObj != null && annotation.recordBefore() && actualResult != null) {
|
|
|
+ try {
|
|
|
+ // 使用修改後的數據對象
|
|
|
+ Object afterObj = actualResult;
|
|
|
+
|
|
|
+ if (afterObj == null) {
|
|
|
+ log.warn("修改後的對象為null,無法進行比較");
|
|
|
+ } else {
|
|
|
+ log.info("開始生成詳細變更描述,修改前對象類型: {}, 修改後對象類型: {}",
|
|
|
+ beforeDataObj.getClass().getSimpleName(),
|
|
|
+ afterObj.getClass().getSimpleName());
|
|
|
+
|
|
|
+ // 輸出實際的JSON數據以便調試
|
|
|
+ String beforeJson = JSON.toJSONString(beforeDataObj);
|
|
|
+ String afterJson = JSON.toJSONString(afterObj);
|
|
|
+ log.info("========== 數據比較開始 ==========");
|
|
|
+ log.info("修改前的完整JSON: {}", beforeJson);
|
|
|
+ log.info("修改後的完整JSON: {}", afterJson);
|
|
|
+ log.info("修改前的對象類型: {}", beforeDataObj.getClass().getName());
|
|
|
+ log.info("修改後的對象類型: {}", afterObj.getClass().getName());
|
|
|
+
|
|
|
+ // 自動推斷id和name字段
|
|
|
+ String[] idAndName = ContentGeneratorUtil.inferIdAndNameFields(beforeDataObj);
|
|
|
+ String idField = idAndName[0];
|
|
|
+ String nameField = idAndName[1];
|
|
|
+
|
|
|
+ log.info("推斷的字段: idField={}, nameField={}", idField, nameField);
|
|
|
+
|
|
|
+ // 生成詳細的變更描述
|
|
|
+ String changeDesc = DataCompareUtil.generateChangeDescription(beforeDataObj, afterObj, idField, nameField);
|
|
|
+ if (changeDesc != null && !changeDesc.isEmpty()) {
|
|
|
+ log.info("生成的變更描述: {}", changeDesc);
|
|
|
+ // 如果操作內容為空或使用默認值,則使用變更描述
|
|
|
+ String currentContent = operationLog.getOperationContent();
|
|
|
+ if (currentContent == null || currentContent.isEmpty() ||
|
|
|
+ (annotation.content() != null && !annotation.content().isEmpty() && currentContent.equals(annotation.content()))) {
|
|
|
+ operationLog.setOperationContent(changeDesc);
|
|
|
+ } else {
|
|
|
+ // 否則追加變更描述
|
|
|
+ operationLog.setOperationContent(currentContent + " - " + changeDesc);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ log.warn("變更描述為空,可能沒有字段變更或比較失敗");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("生成變更描述失敗", e);
|
|
|
+ e.printStackTrace();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("记录数据变更失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 自動生成操作內容
|
|
|
+ * 當content為空時,根據操作類型和對象字段自動生成
|
|
|
+ */
|
|
|
+ private String autoGenerateContent(shop.alien.store.annotation.ChangeRecordLog annotation,
|
|
|
+ ProceedingJoinPoint joinPoint, Object[] args, Object result) {
|
|
|
+ try {
|
|
|
+ String operationType = annotation.type().getDescription();
|
|
|
+
|
|
|
+ // 嘗試從方法參數或返回值中獲取對象
|
|
|
+ Object targetObj = null;
|
|
|
+
|
|
|
+ // 優先使用返回值(對於新增、修改操作)
|
|
|
+ if (result != null && ("新增".equals(operationType) || "修改".equals(operationType) || "更新".equals(operationType))) {
|
|
|
+ // 如果返回值是 R 包裝類,嘗試獲取 data
|
|
|
+ if (result.getClass().getName().contains("R")) {
|
|
|
+ try {
|
|
|
+ java.lang.reflect.Method getDataMethod = result.getClass().getMethod("getData");
|
|
|
+ Object data = getDataMethod.invoke(result);
|
|
|
+ if (data != null) {
|
|
|
+ targetObj = data;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 如果獲取失敗,使用原返回值
|
|
|
+ targetObj = result;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ targetObj = result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ // 如果沒有返回值,嘗試從參數中獲取第一個對象參數
|
|
|
+ else if (args != null && args.length > 0) {
|
|
|
+ for (Object arg : args) {
|
|
|
+ if (arg != null && !isPrimitiveOrWrapper(arg.getClass()) && !isString(arg.getClass())) {
|
|
|
+ targetObj = arg;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (targetObj != null) {
|
|
|
+ // 自動推斷id和name字段
|
|
|
+ String[] idAndName = ContentGeneratorUtil.inferIdAndNameFields(targetObj);
|
|
|
+ String idField = idAndName[0];
|
|
|
+ String nameField = idAndName[1];
|
|
|
+
|
|
|
+ // 生成內容
|
|
|
+ return ContentGeneratorUtil.generateContent(operationType, targetObj, idField, nameField);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果無法獲取對象,返回默認內容
|
|
|
+ return operationType + "操作";
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("自動生成操作內容失敗", e);
|
|
|
+ return annotation.type().getDescription() + "操作";
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判斷是否為基本類型或包裝類型
|
|
|
+ */
|
|
|
+ private boolean isPrimitiveOrWrapper(Class<?> clazz) {
|
|
|
+ return clazz.isPrimitive() ||
|
|
|
+ clazz == Boolean.class ||
|
|
|
+ clazz == Byte.class ||
|
|
|
+ clazz == Character.class ||
|
|
|
+ clazz == Short.class ||
|
|
|
+ clazz == Integer.class ||
|
|
|
+ clazz == Long.class ||
|
|
|
+ clazz == Float.class ||
|
|
|
+ clazz == Double.class;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判斷是否為字符串類型
|
|
|
+ */
|
|
|
+ private boolean isString(Class<?> clazz) {
|
|
|
+ return clazz == String.class;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取客户端IP地址
|
|
|
+ */
|
|
|
+ private String getIpAddress(HttpServletRequest request) {
|
|
|
+ String ip = request.getHeader("X-Forwarded-For");
|
|
|
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
|
|
+ ip = request.getHeader("Proxy-Client-IP");
|
|
|
+ }
|
|
|
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
|
|
+ ip = request.getHeader("WL-Proxy-Client-IP");
|
|
|
+ }
|
|
|
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
|
|
+ ip = request.getHeader("HTTP_CLIENT_IP");
|
|
|
+ }
|
|
|
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
|
|
+ ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
|
|
+ }
|
|
|
+ if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
|
|
+ ip = request.getRemoteAddr();
|
|
|
+ }
|
|
|
+ return ip;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 從對象中提取ID值
|
|
|
+ */
|
|
|
+ private Object extractIdFromObject(Object obj, String idFieldName) {
|
|
|
+ if (obj == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 先嘗試通過JSON方式獲取
|
|
|
+ JSONObject jsonObj = JSONObject.parseObject(JSON.toJSONString(obj));
|
|
|
+ Object id = jsonObj.get(idFieldName);
|
|
|
+ if (id != null) {
|
|
|
+ return id;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果JSON方式失敗,嘗試反射
|
|
|
+ Class<?> clazz = obj.getClass();
|
|
|
+ Field field = clazz.getDeclaredField(idFieldName);
|
|
|
+ field.setAccessible(true);
|
|
|
+ return field.get(obj);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("提取ID失敗,字段名: {}, 對象類型: {}", idFieldName, obj.getClass().getSimpleName(), e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 通過ID查詢修改前的數據
|
|
|
+ */
|
|
|
+ private Object queryBeforeDataById(Class<?> entityClass, Object id) {
|
|
|
+ if (id == null || entityClass == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 根據實體類名推斷Mapper名稱
|
|
|
+ // 例如:EssentialHolidayComparison -> EssentialHolidayComparisonMapper
|
|
|
+ String entityName = entityClass.getSimpleName();
|
|
|
+ String mapperBeanName = entityName.substring(0, 1).toLowerCase() + entityName.substring(1) + "Mapper";
|
|
|
+
|
|
|
+ // 嘗試從Spring容器中獲取Mapper
|
|
|
+ if (applicationContext != null) {
|
|
|
+ try {
|
|
|
+ // 先嘗試標準命名
|
|
|
+ BaseMapper<?> mapper = (BaseMapper<?>) applicationContext.getBean(mapperBeanName);
|
|
|
+ if (mapper != null) {
|
|
|
+ // selectById 需要 Serializable 類型
|
|
|
+ Serializable serializableId = convertToSerializable(id);
|
|
|
+ if (serializableId != null) {
|
|
|
+ Object result = mapper.selectById(serializableId);
|
|
|
+ log.info("通過Mapper查詢成功,Mapper: {}, ID: {}", mapperBeanName, id);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("無法獲取Mapper: {}", mapperBeanName, e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 嘗試查找所有BaseMapper類型的Bean
|
|
|
+ try {
|
|
|
+ String[] beanNames = applicationContext.getBeanNamesForType(BaseMapper.class);
|
|
|
+ for (String beanName : beanNames) {
|
|
|
+ try {
|
|
|
+ BaseMapper<?> mapper = applicationContext.getBean(beanName, BaseMapper.class);
|
|
|
+ // 檢查Mapper的泛型類型是否匹配
|
|
|
+ java.lang.reflect.Type[] types = mapper.getClass().getGenericInterfaces();
|
|
|
+ for (java.lang.reflect.Type type : types) {
|
|
|
+ if (type instanceof java.lang.reflect.ParameterizedType) {
|
|
|
+ java.lang.reflect.ParameterizedType pt = (java.lang.reflect.ParameterizedType) type;
|
|
|
+ if (pt.getRawType().equals(BaseMapper.class)) {
|
|
|
+ java.lang.reflect.Type[] actualTypes = pt.getActualTypeArguments();
|
|
|
+ if (actualTypes.length > 0 && actualTypes[0] instanceof Class) {
|
|
|
+ Class<?> mapperEntityClass = (Class<?>) actualTypes[0];
|
|
|
+ if (mapperEntityClass.equals(entityClass)) {
|
|
|
+ // selectById 需要 Serializable 類型
|
|
|
+ Serializable serializableId = convertToSerializable(id);
|
|
|
+ if (serializableId != null) {
|
|
|
+ Object result = mapper.selectById(serializableId);
|
|
|
+ log.info("通過Mapper查詢成功,Mapper: {}, ID: {}", beanName, id);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 繼續查找下一個
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("查找Mapper失敗", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.warn("無法找到對應的Mapper,實體類: {}, ID: {}", entityClass.getSimpleName(), id);
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("查詢修改前的數據失敗,實體類: {}, ID: {}", entityClass.getSimpleName(), id, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 從結果對象中提取實際數據(處理R包裝類)
|
|
|
+ */
|
|
|
+ private Object extractDataFromResult(Object result) {
|
|
|
+ if (result == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ String resultClassName = result.getClass().getName();
|
|
|
+ log.debug("提取數據,result類型: {}", resultClassName);
|
|
|
+
|
|
|
+ // 如果不是R包裝類,直接返回
|
|
|
+ if (!resultClassName.contains("R")) {
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 嘗試多種方法獲取data
|
|
|
+ try {
|
|
|
+ // 方法1: 嘗試 getData() 方法
|
|
|
+ try {
|
|
|
+ java.lang.reflect.Method getDataMethod = result.getClass().getMethod("getData");
|
|
|
+ Object data = getDataMethod.invoke(result);
|
|
|
+ if (data != null) {
|
|
|
+ log.info("通過getData()方法從R對象中提取data成功,類型: {}", data.getClass().getSimpleName());
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("getData()方法不存在", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方法2: 嘗試 data() 方法
|
|
|
+ try {
|
|
|
+ java.lang.reflect.Method dataMethod = result.getClass().getMethod("data");
|
|
|
+ Object data = dataMethod.invoke(result);
|
|
|
+ if (data != null) {
|
|
|
+ log.info("通過data()方法從R對象中提取data成功,類型: {}", data.getClass().getSimpleName());
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("data()方法不存在", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 方法3: 嘗試通過字段獲取
|
|
|
+ try {
|
|
|
+ java.lang.reflect.Field dataField = result.getClass().getDeclaredField("data");
|
|
|
+ dataField.setAccessible(true);
|
|
|
+ Object data = dataField.get(result);
|
|
|
+ if (data != null) {
|
|
|
+ log.info("通過字段從R對象中提取data成功,類型: {}", data.getClass().getSimpleName());
|
|
|
+ return data;
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.debug("無法通過字段獲取data", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.warn("無法從R對象中提取data,返回原對象。R對象: {}", JSON.toJSONString(result));
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("提取R對象中的data失敗", e);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 將對象轉換為 Serializable 類型
|
|
|
+ */
|
|
|
+ private Serializable convertToSerializable(Object id) {
|
|
|
+ if (id == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果已經是 Serializable 類型,直接返回
|
|
|
+ if (id instanceof Serializable) {
|
|
|
+ return (Serializable) id;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 處理常見的 ID 類型
|
|
|
+ if (id instanceof Integer) {
|
|
|
+ return (Integer) id;
|
|
|
+ } else if (id instanceof Long) {
|
|
|
+ return (Long) id;
|
|
|
+ } else if (id instanceof String) {
|
|
|
+ return (String) id;
|
|
|
+ } else if (id instanceof Number) {
|
|
|
+ // 其他數字類型轉換為 Long
|
|
|
+ return ((Number) id).longValue();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 嘗試轉換為字符串
|
|
|
+ try {
|
|
|
+ return id.toString();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("無法將ID轉換為Serializable: {}", id, e);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|