ソースを参照

feat(store): 新增操作日志功能及相关工具类

- 添加操作日志注解 ChangeRecordLog,支持记录操作模块、类型及描述
- 实现内容生成工具 ContentGeneratorUtil,自动生成操作描述
- 开发数据比较工具 DataCompareUtil,用于对比修改前后数据差异
- 创建操作日志实体类 OperationLog 并配置 MyBatis 映射
- 提供操作日志控制器基础框架,支持分页查询与条件过滤
jyc 6 日 前
コミット
cd6ace95c9

+ 118 - 0
alien-entity/src/main/java/shop/alien/entity/store/OperationLog.java

@@ -0,0 +1,118 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 操作日志实体类
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+@Data
+@TableName("operation_log")
+@ApiModel(value = "OperationLog对象", description = "操作日志")
+public class OperationLog {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "账号ID")
+    @TableField("account_id")
+    private String accountId;
+
+    @ApiModelProperty(value = "账号")
+    @TableField("account")
+    private String account;
+
+    @ApiModelProperty(value = "姓名")
+    @TableField("name")
+    private String name;
+
+    @ApiModelProperty(value = "职务")
+    @TableField("position")
+    private String position;
+
+    @ApiModelProperty(value = "部门")
+    @TableField("department")
+    private String department;
+
+    @ApiModelProperty(value = "操作时间")
+    @TableField("operation_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date operationTime;
+
+    @ApiModelProperty(value = "操作模块")
+    @TableField("operation_module")
+    private String operationModule;
+
+    @ApiModelProperty(value = "操作类型(导入、删除、新增、修改等)")
+    @TableField("operation_type")
+    private String operationType;
+
+    @ApiModelProperty(value = "操作内容")
+    @TableField("operation_content")
+    private String operationContent;
+
+    @ApiModelProperty(value = "用户类型(平台、用户、商家、律师)")
+    @TableField("user_type")
+    private String userType;
+
+    @ApiModelProperty(value = "修改前的数据(JSON格式)")
+    @TableField("before_data")
+    private String beforeData;
+
+    @ApiModelProperty(value = "修改后的数据(JSON格式)")
+    @TableField("after_data")
+    private String afterData;
+
+    @ApiModelProperty(value = "请求方法")
+    @TableField("method")
+    private String method;
+
+    @ApiModelProperty(value = "请求路径")
+    @TableField("request_path")
+    private String requestPath;
+
+    @ApiModelProperty(value = "请求参数")
+    @TableField("request_params")
+    private String requestParams;
+
+    @ApiModelProperty(value = "IP地址")
+    @TableField("ip_address")
+    private String ipAddress;
+
+    @ApiModelProperty(value = "文件地址")
+    @TableField("file_url")
+    private String fileUrl;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField(value = "created_user_id", fill = FieldFill.INSERT)
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "更新人ID")
+    @TableField(value = "updated_user_id", fill = FieldFill.INSERT_UPDATE)
+    private Integer updatedUserId;
+
+    @ApiModelProperty(value = "删除标识(0:未删除,1:已删除)")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+}
+

+ 16 - 0
alien-entity/src/main/java/shop/alien/mapper/OperationLogMapper.java

@@ -0,0 +1,16 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.OperationLog;
+
+/**
+ * 操作日志 Mapper接口
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+@Mapper
+public interface OperationLogMapper extends BaseMapper<OperationLog> {
+}
+

+ 48 - 0
alien-store/src/main/java/shop/alien/store/annotation/ChangeRecordLog.java

@@ -0,0 +1,48 @@
+package shop.alien.store.annotation;
+
+import shop.alien.store.enums.OperationType;
+
+import java.lang.annotation.*;
+
+/**
+ * 操作日志注解
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface ChangeRecordLog  {
+
+    /**
+     * 操作模块
+     */
+    String module() default "";
+
+    /**
+     * 操作类型
+     */
+    OperationType type() default OperationType.OTHER;
+
+    /**
+     * 操作描述(支持SpEL表达式,如:"导入了excel文件,文件名#{fileName},共#{count}条数据")
+     */
+    String content() default "";
+
+    /**
+     * 是否记录修改前的数据(自動從方法參數中提取ID並查詢原始數據)
+     */
+    boolean recordBefore() default false;
+
+    /**
+     * ID字段名稱(默認為"id",如果參數對象中的ID字段名不同,可指定)
+     */
+    String idField() default "id";
+
+    /**
+     * 是否记录修改后的数据
+     */
+    boolean recordAfter() default true;
+}
+

+ 901 - 0
alien-store/src/main/java/shop/alien/store/aspect/OperationLogAspect.java

@@ -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;
+        }
+    }
+}
+

+ 83 - 0
alien-store/src/main/java/shop/alien/store/controller/OperationLogController.java

@@ -0,0 +1,83 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiParam;
+import lombok.RequiredArgsConstructor;
+import org.springframework.format.annotation.DateTimeFormat;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.OperationLog;
+import shop.alien.mapper.OperationLogMapper;
+import shop.alien.store.annotation.ChangeRecordLog;
+import shop.alien.store.enums.OperationType;
+
+import java.util.Date;
+
+/**
+ * 操作日志Controller
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+@Api(tags = {"操作日志管理"})
+@RestController
+@RequestMapping("/operation/log")
+@RequiredArgsConstructor
+public class OperationLogController {
+
+    private final OperationLogMapper operationLogMapper;
+
+    @ApiOperation("查询操作日志列表")
+    @GetMapping("/list")
+    @ChangeRecordLog(module = "操作日志管理", type = OperationType.QUERY, content = "查询了操作日志列表")
+    public R<IPage<OperationLog>> list(
+            @ApiParam("页码") @RequestParam(defaultValue = "1") Integer pageNum,
+            @ApiParam("每页数量") @RequestParam(defaultValue = "10") Integer pageSize,
+            @ApiParam("姓名") @RequestParam(required = false) String name,
+            @ApiParam("开始时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
+            @ApiParam("结束时间") @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime,
+            @ApiParam("用户类型") @RequestParam(required = false) String userType) {
+        
+        Page<OperationLog> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<OperationLog> wrapper = new LambdaQueryWrapper<>();
+        
+        wrapper.like(name != null, OperationLog::getName, name)
+               .ge(startTime != null, OperationLog::getOperationTime, startTime)
+               .le(endTime != null, OperationLog::getOperationTime, endTime)
+               .eq(userType != null, OperationLog::getUserType, userType)
+               .orderByDesc(OperationLog::getOperationTime);
+        
+        IPage<OperationLog> result = operationLogMapper.selectPage(page, wrapper);
+        return R.data(result);
+    }
+
+    @ApiOperation("根据ID查询操作日志详情")
+    @GetMapping("/{id}")
+    @ChangeRecordLog(module = "操作日志管理", type = OperationType.QUERY, content = "查询了操作日志详情,id=#{#id}")
+    public R<OperationLog> getById(@ApiParam("日志ID") @PathVariable Long id) {
+        OperationLog operationLog = operationLogMapper.selectById(id);
+        return R.data(operationLog);
+    }
+
+    @ApiOperation("直接插入操作日志")
+    @PostMapping("/insert")
+    public R insertOperationLog(@RequestBody OperationLog operationLog) {
+        try {
+            // 如果操作时间为空,设置为当前时间
+            if (operationLog.getOperationTime() == null) {
+                operationLog.setOperationTime(new Date());
+            }
+            
+            // 直接保存到数据库
+            operationLogMapper.insert(operationLog);
+            return R.data("操作日志保存成功");
+        } catch (Exception e) {
+            return R.fail("操作日志保存失败:" + e.getMessage());
+        }
+    }
+}
+

+ 55 - 0
alien-store/src/main/java/shop/alien/store/enums/OperationType.java

@@ -0,0 +1,55 @@
+package shop.alien.store.enums;
+
+/**
+ * 操作类型枚举
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+public enum OperationType {
+    /**
+     * 新增
+     */
+    ADD("新增"),
+    
+    /**
+     * 删除
+     */
+    DELETE("删除"),
+    
+    /**
+     * 修改
+     */
+    UPDATE("修改"),
+    
+    /**
+     * 导入
+     */
+    IMPORT("导入"),
+    
+    /**
+     * 导出
+     */
+    EXPORT("导出"),
+    
+    /**
+     * 查询
+     */
+    QUERY("查询"),
+    
+    /**
+     * 其他
+     */
+    OTHER("其他");
+
+    private final String description;
+
+    OperationType(String description) {
+        this.description = description;
+    }
+
+    public String getDescription() {
+        return description;
+    }
+}
+

+ 20 - 0
alien-store/src/main/java/shop/alien/store/service/OperationLogService.java

@@ -0,0 +1,20 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.OperationLog;
+
+/**
+ * 操作日志服务接口
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+public interface OperationLogService {
+
+    /**
+     * 保存操作日志
+     *
+     * @param operationLog 操作日志对象
+     */
+    void saveOperationLog(OperationLog operationLog);
+}
+

+ 54 - 0
alien-store/src/main/java/shop/alien/store/service/impl/OperationLogServiceImpl.java

@@ -0,0 +1,54 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.OperationLog;
+import shop.alien.mapper.OperationLogMapper;
+import shop.alien.store.service.OperationLogService;
+
+/**
+ * 操作日志服务实现类
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class OperationLogServiceImpl extends ServiceImpl<OperationLogMapper, OperationLog> implements OperationLogService {
+
+    @Override
+    public void saveOperationLog(OperationLog operationLog) {
+        try {
+            // 確保操作時間不為空
+            if (operationLog.getOperationTime() == null) {
+                operationLog.setOperationTime(new java.util.Date());
+            }
+            
+            // 確保操作模組不為空
+            if (operationLog.getOperationModule() == null || operationLog.getOperationModule().isEmpty()) {
+                operationLog.setOperationModule("未知模組");
+            }
+            
+            // 確保操作類型不為空
+            if (operationLog.getOperationType() == null || operationLog.getOperationType().isEmpty()) {
+                operationLog.setOperationType("其他");
+            }
+            
+            this.save(operationLog);
+            log.info("操作日志保存成功: 模組={}, 類型={}, 內容={}", 
+                    operationLog.getOperationModule(), 
+                    operationLog.getOperationType(), 
+                    operationLog.getOperationContent());
+        } catch (Exception e) {
+            log.error("保存操作日志失败: 模組={}, 類型={}, 內容={}", 
+                    operationLog.getOperationModule(), 
+                    operationLog.getOperationType(), 
+                    operationLog.getOperationContent(), e);
+            // 这里可以选择是否抛出异常,为了不影响主业务流程,这里只记录日志
+        }
+    }
+}
+

+ 476 - 0
alien-store/src/main/java/shop/alien/store/util/ContentGeneratorUtil.java

@@ -0,0 +1,476 @@
+package shop.alien.store.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Field;
+import java.time.LocalDate;
+import java.util.*;
+
+/**
+ * 操作內容自動生成工具類
+ * 用於自動反射對象字段並生成操作描述
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+@Slf4j
+public class ContentGeneratorUtil {
+
+    /**
+     * 默認排除的字段(這些字段不會出現在操作描述中)
+     */
+    private static final Set<String> DEFAULT_EXCLUDE_FIELDS = new HashSet<>(Arrays.asList(
+            "id", "deleteFlag", "delFlag", "createdTime", "updatedTime",
+            "createdUserId", "updatedUserId", "serialVersionUID", "class"
+    ));
+
+    /**
+     * 字段名到中文名稱的映射(作為備用,如果沒有@ApiModelProperty注解時使用)
+     */
+    private static final Map<String, String> FIELD_NAME_MAPPING = new HashMap<String, String>() {{
+        // 節假日相關
+        put("festivalName", "節日名稱");
+        put("particularYear", "年份");
+        put("festivalDate", "日期");
+        put("startTime", "開始時間");
+        put("endTime", "結束時間");
+        put("openFlag", "啟用狀態");
+        
+        // 通用字段
+        put("name", "名稱");
+        put("title", "標題");
+        put("status", "狀態");
+        put("type", "類型");
+        put("description", "描述");
+        put("remark", "備註");
+        put("phone", "電話");
+        put("address", "地址");
+        put("email", "郵箱");
+        put("price", "價格");
+        put("amount", "金額");
+        put("count", "數量");
+    }};
+    
+    /**
+     * 從實體類的@ApiModelProperty注解中獲取字段的中文名稱
+     */
+    private static String getFieldChineseName(Class<?> entityClass, String fieldName) {
+        if (fieldName == null || entityClass == null) {
+            return fieldName;
+        }
+        
+        // 直接從注解讀取
+        return getFieldChineseNameFromAnnotation(entityClass, fieldName);
+    }
+    
+    /**
+     * 從實體類的@ApiModelProperty注解中獲取字段的中文名稱
+     */
+    private static String getFieldChineseNameFromAnnotation(Class<?> clazz, String fieldName) {
+        if (fieldName == null || clazz == null) {
+            return fieldName;
+        }
+        
+        try {
+            // 遍歷類及其父類的所有字段
+            Class<?> currentClass = clazz;
+            while (currentClass != null && !currentClass.equals(Object.class)) {
+                try {
+                    Field field = currentClass.getDeclaredField(fieldName);
+                    // 嘗試從@ApiModelProperty注解中獲取中文名稱
+                    ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
+                    if (apiModelProperty != null) {
+                        String value = apiModelProperty.value();
+                        if (StringUtils.isNotBlank(value)) {
+                            return value;
+                        }
+                    }
+                    // 如果找到字段但沒有注解,跳出循環
+                    break;
+                } catch (NoSuchFieldException e) {
+                    // 字段不在當前類中,繼續查找父類
+                }
+                
+                // 繼續處理父類
+                currentClass = currentClass.getSuperclass();
+            }
+        } catch (Exception e) {
+            log.debug("從注解讀取字段中文名稱失敗,字段: {}, 類: {}", fieldName, clazz.getName(), e);
+        }
+        
+        // 如果沒有找到注解,返回字段名本身
+        return fieldName;
+    }
+
+    /**
+     * 自動生成操作內容
+     * 根據操作類型和對象字段自動生成描述
+     *
+     * @param operationType 操作類型(新增、修改、刪除等)
+     * @param obj           操作對象
+     * @param idField       標識字段名(如 "id")
+     * @param nameField     名稱字段名(如 "name" 或 "festivalName")
+     * @return 生成的操作內容
+     */
+    public static String generateContent(String operationType, Object obj, String idField, String nameField) {
+        if (obj == null) {
+            return operationType + "操作";
+        }
+
+        try {
+            // 獲取對象的字段和值
+            Map<String, Object> fieldValues = extractFieldValues(obj);
+            
+            // 獲取標識和名稱
+            String idValue = getFieldValueAsString(fieldValues, idField);
+            String nameValue = getFieldValueAsString(fieldValues, nameField);
+
+            // 根據操作類型生成不同的描述
+            StringBuilder content = new StringBuilder();
+            
+            // 獲取字段的中文名稱(從實體類的@ApiModelProperty注解中讀取)
+            String idFieldChinese = getFieldChineseNameFromAnnotation(obj.getClass(), idField);
+            String nameFieldChinese = getFieldChineseNameFromAnnotation(obj.getClass(), nameField);
+            
+            if ("新增".equals(operationType)) {
+                content.append("新增了");
+                if (StringUtils.isNotBlank(nameValue)) {
+                    content.append(nameValue);
+                } else if (StringUtils.isNotBlank(idValue)) {
+                    content.append(idFieldChinese).append("為").append(idValue).append("的記錄");
+                } else {
+                    content.append("記錄");
+                }
+                
+                // 添加主要字段信息(傳入對象以便讀取注解)
+                appendMainFields(content, fieldValues, idField, nameField, obj);
+                
+            } else if ("修改".equals(operationType) || "更新".equals(operationType)) {
+                content.append("修改了");
+                if (StringUtils.isNotBlank(nameValue)) {
+                    content.append(nameValue);
+                } else if (StringUtils.isNotBlank(idValue)) {
+                    content.append(idFieldChinese).append("為").append(idValue).append("的記錄");
+                } else {
+                    content.append("記錄");
+                }
+                
+                // 添加修改的字段信息(傳入對象以便讀取注解)
+                appendMainFields(content, fieldValues, idField, nameField, obj);
+                
+            } else if ("刪除".equals(operationType)) {
+                content.append("刪除了");
+                if (StringUtils.isNotBlank(nameValue)) {
+                    content.append(nameValue);
+                } else if (StringUtils.isNotBlank(idValue)) {
+                    content.append(idFieldChinese).append("為").append(idValue).append("的記錄");
+                } else {
+                    content.append("記錄");
+                }
+                
+            } else {
+                content.append(operationType);
+                if (StringUtils.isNotBlank(nameValue)) {
+                    content.append("了").append(nameValue);
+                } else if (StringUtils.isNotBlank(idValue)) {
+                    content.append("了").append(idFieldChinese).append("為").append(idValue).append("的記錄");
+                }
+            }
+
+            return content.toString();
+            
+        } catch (Exception e) {
+            log.warn("自動生成操作內容失敗", e);
+            return operationType + "操作";
+        }
+    }
+
+    /**
+     * 提取對象的字段和值
+     */
+    private static Map<String, Object> extractFieldValues(Object obj) {
+        Map<String, Object> fieldValues = new LinkedHashMap<>();
+        
+        try {
+            // 先嘗試轉換為 JSON 對象(更簡單的方式)
+            JSONObject jsonObj = JSONObject.parseObject(JSON.toJSONString(obj));
+            if (jsonObj != null) {
+                for (String key : jsonObj.keySet()) {
+                    if (!DEFAULT_EXCLUDE_FIELDS.contains(key)) {
+                        Object value = jsonObj.get(key);
+                        if (value != null && !isDefaultValue(value)) {
+                            fieldValues.put(key, value);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug("使用JSON方式提取字段失敗,嘗試反射方式", e);
+            // 如果JSON方式失敗,使用反射
+            extractFieldValuesByReflection(obj, fieldValues);
+        }
+        
+        return fieldValues;
+    }
+
+    /**
+     * 使用反射提取字段值
+     */
+    private static void extractFieldValuesByReflection(Object obj, Map<String, Object> fieldValues) {
+        Class<?> clazz = obj.getClass();
+        
+        // 跳過 JDK 內部類(如 String、Integer 等),避免模塊系統限制
+        if (clazz.getName().startsWith("java.") || clazz.getName().startsWith("javax.")) {
+            log.debug("跳過 JDK 內部類: {}", clazz.getName());
+            return;
+        }
+        
+        Field[] fields = clazz.getDeclaredFields();
+        
+        for (Field field : fields) {
+            String fieldName = field.getName();
+            if (DEFAULT_EXCLUDE_FIELDS.contains(fieldName)) {
+                continue;
+            }
+            
+            try {
+                // 設置字段可訪問(Java 8 兼容)
+                field.setAccessible(true);
+                
+                Object value = field.get(obj);
+                if (value != null && !isDefaultValue(value)) {
+                    fieldValues.put(fieldName, value);
+                }
+            } catch (IllegalAccessException | SecurityException e) {
+                log.debug("無法訪問字段: {}", fieldName, e);
+            } catch (Exception e) {
+                log.debug("提取字段值失敗: {}", fieldName, e);
+            }
+        }
+        
+        // 遞歸處理父類字段
+        Class<?> superClass = clazz.getSuperclass();
+        if (superClass != null && !superClass.equals(Object.class) && 
+            !superClass.getName().startsWith("java.")) {
+            try {
+                extractFieldValuesByReflectionFromClass(obj, superClass, fieldValues);
+            } catch (Exception e) {
+                log.debug("提取父類字段失敗", e);
+            }
+        }
+    }
+    
+    /**
+     * 從指定類中提取字段值
+     */
+    private static void extractFieldValuesByReflectionFromClass(Object obj, Class<?> clazz, Map<String, Object> fieldValues) {
+        Field[] fields = clazz.getDeclaredFields();
+        
+        for (Field field : fields) {
+            String fieldName = field.getName();
+            if (DEFAULT_EXCLUDE_FIELDS.contains(fieldName) || fieldValues.containsKey(fieldName)) {
+                continue;
+            }
+            
+            try {
+                // 設置字段可訪問(Java 8 兼容)
+                field.setAccessible(true);
+                
+                Object value = field.get(obj);
+                if (value != null && !isDefaultValue(value)) {
+                    fieldValues.put(fieldName, value);
+                }
+            } catch (IllegalAccessException | SecurityException e) {
+                log.debug("無法訪問字段: {}", fieldName, e);
+            } catch (Exception e) {
+                log.debug("提取字段值失敗: {}", fieldName, e);
+            }
+        }
+    }
+
+    /**
+     * 判斷是否為默認值(空值、0、false等)
+     */
+    private static boolean isDefaultValue(Object value) {
+        if (value == null) {
+            return true;
+        }
+        if (value instanceof String && StringUtils.isBlank((String) value)) {
+            return true;
+        }
+        if (value instanceof Number) {
+            Number num = (Number) value;
+            if (num instanceof Integer && num.intValue() == 0) {
+                return true;
+            }
+            if (num instanceof Long && num.longValue() == 0L) {
+                return true;
+            }
+            if (num instanceof Double && num.doubleValue() == 0.0) {
+                return true;
+            }
+        }
+        if (value instanceof Boolean && !((Boolean) value)) {
+            return false; // false 值也保留
+        }
+        return false;
+    }
+
+    /**
+     * 獲取字段值作為字符串
+     */
+    private static String getFieldValueAsString(Map<String, Object> fieldValues, String fieldName) {
+        if (StringUtils.isBlank(fieldName) || fieldValues == null) {
+            return null;
+        }
+        
+        Object value = fieldValues.get(fieldName);
+        if (value == null) {
+            return null;
+        }
+        
+        return formatValue(value);
+    }
+
+    /**
+     * 格式化值
+     */
+    private static String formatValue(Object value) {
+        if (value == null) {
+            return null;
+        }
+        
+        if (value instanceof Date) {
+            return new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) value);
+        }
+        if (value instanceof LocalDate) {
+            return value.toString();
+        }
+        if (value instanceof Boolean) {
+            return (Boolean) value ? "是" : "否";
+        }
+        
+        String str = value.toString();
+        // 如果字符串過長,截取
+        if (str.length() > 50) {
+            return str.substring(0, 50) + "...";
+        }
+        
+        return str;
+    }
+
+    /**
+     * 追加主要字段信息到內容中
+     */
+    private static void appendMainFields(StringBuilder content, Map<String, Object> fieldValues, 
+                                        String idField, String nameField, Object obj) {
+        List<String> fieldDescriptions = new ArrayList<>();
+        
+        // 最多顯示5個主要字段
+        int count = 0;
+        for (Map.Entry<String, Object> entry : fieldValues.entrySet()) {
+            if (count >= 5) {
+                break;
+            }
+            
+            String fieldName = entry.getKey();
+            // 跳過id和name字段(已經在開頭顯示了)
+            if (fieldName.equals(idField) || fieldName.equals(nameField)) {
+                continue;
+            }
+            
+            Object value = entry.getValue();
+            // 從實體類的@ApiModelProperty注解中讀取字段中文名稱
+            String fieldLabel = getFieldLabel(obj, fieldName);
+            String valueStr = formatValue(value);
+            
+            if (StringUtils.isNotBlank(valueStr)) {
+                fieldDescriptions.add(fieldLabel + ":" + valueStr);
+                count++;
+            }
+        }
+        
+        if (!fieldDescriptions.isEmpty()) {
+            content.append(",").append(String.join(",", fieldDescriptions));
+        }
+    }
+
+    /**
+     * 獲取字段的中文標籤(從@ApiModelProperty注解中讀取)
+     */
+    private static String getFieldLabel(Object obj, String fieldName) {
+        if (obj == null || fieldName == null) {
+            return fieldName;
+        }
+        
+        // 優先從實體類的@ApiModelProperty注解中讀取
+        String chineseName = getFieldChineseNameFromAnnotation(obj.getClass(), fieldName);
+        if (!chineseName.equals(fieldName)) {
+            return chineseName;
+        }
+        
+        // 如果沒有注解,從舊的映射中查找(兼容性)
+        String label = FIELD_NAME_MAPPING.get(fieldName);
+        if (StringUtils.isNotBlank(label)) {
+            return label;
+        }
+        
+        // 如果都沒有,返回字段名本身
+        return fieldName;
+    }
+    
+    /**
+     * 獲取字段的中文標籤(兼容舊方法)
+     */
+    private static String getFieldLabel(String fieldName) {
+        // 先從映射中查找
+        String label = FIELD_NAME_MAPPING.get(fieldName);
+        if (StringUtils.isNotBlank(label)) {
+            return label;
+        }
+        return fieldName;
+    }
+
+    /**
+     * 根據對象自動推斷id和name字段
+     */
+    public static String[] inferIdAndNameFields(Object obj) {
+        if (obj == null) {
+            return new String[]{"id", "name"};
+        }
+        
+        Map<String, Object> fieldValues = extractFieldValues(obj);
+        
+        // 嘗試找到id字段
+        String idField = "id";
+        if (!fieldValues.containsKey("id")) {
+            // 嘗試其他可能的id字段
+            for (String field : Arrays.asList("id", "Id", "ID", "primaryKey")) {
+                if (fieldValues.containsKey(field)) {
+                    idField = field;
+                    break;
+                }
+            }
+        }
+        
+        // 嘗試找到name字段
+        String nameField = "name";
+        if (!fieldValues.containsKey("name")) {
+            // 嘗試其他可能的name字段
+            for (String field : Arrays.asList("name", "Name", "title", "Title", 
+                    "festivalName", "storeName", "userName", "realName")) {
+                if (fieldValues.containsKey(field)) {
+                    nameField = field;
+                    break;
+                }
+            }
+        }
+        
+        return new String[]{idField, nameField};
+    }
+}
+

+ 542 - 0
alien-store/src/main/java/shop/alien/store/util/DataCompareUtil.java

@@ -0,0 +1,542 @@
+package shop.alien.store.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+import java.lang.reflect.Field;
+import java.util.*;
+
+/**
+ * 字段名到中文的映射(從實體類的@ApiModelProperty注解中讀取)
+ */
+@Slf4j
+class FieldNameChineseMap {
+    // 緩存:類名 -> (字段名 -> 中文名稱)
+    private static final Map<String, Map<String, String>> CLASS_FIELD_MAP_CACHE = new HashMap<>();
+    
+    /**
+     * 從實體類的@ApiModelProperty注解中獲取字段的中文名稱
+     */
+    static String getChineseName(Class<?> entityClass, String fieldName) {
+        if (fieldName == null || entityClass == null) {
+            return fieldName;
+        }
+        
+        // 從緩存中獲取
+        String className = entityClass.getName();
+        Map<String, String> fieldMap = CLASS_FIELD_MAP_CACHE.get(className);
+        if (fieldMap != null && fieldMap.containsKey(fieldName)) {
+            return fieldMap.get(fieldName);
+        }
+        
+        // 如果緩存中沒有,則通過反射讀取
+        if (fieldMap == null) {
+            fieldMap = buildFieldNameMap(entityClass);
+            CLASS_FIELD_MAP_CACHE.put(className, fieldMap);
+        }
+        
+        return fieldMap.getOrDefault(fieldName, fieldName);
+    }
+    
+    /**
+     * 從實體類的@ApiModelProperty注解中獲取字段的中文名稱(通過對象實例)
+     */
+    static String getChineseName(Object obj, String fieldName) {
+        if (obj == null || fieldName == null) {
+            return fieldName;
+        }
+        return getChineseName(obj.getClass(), fieldName);
+    }
+    
+    /**
+     * 構建字段名到中文名稱的映射
+     */
+    private static Map<String, String> buildFieldNameMap(Class<?> clazz) {
+        Map<String, String> fieldMap = new HashMap<>();
+        
+        try {
+            // 遍歷類及其父類的所有字段
+            Class<?> currentClass = clazz;
+            while (currentClass != null && !currentClass.equals(Object.class)) {
+                Field[] fields = currentClass.getDeclaredFields();
+                for (Field field : fields) {
+                    String fieldName = field.getName();
+                    
+                    // 如果已經有映射,跳過(子類字段優先)
+                    if (fieldMap.containsKey(fieldName)) {
+                        continue;
+                    }
+                    
+                    // 嘗試從@ApiModelProperty注解中獲取中文名稱
+                    ApiModelProperty apiModelProperty = field.getAnnotation(ApiModelProperty.class);
+                    if (apiModelProperty != null) {
+                        String value = apiModelProperty.value();
+                        if (StringUtils.isNotBlank(value)) {
+                            fieldMap.put(fieldName, value);
+                            continue;
+                        }
+                    }
+                    
+                    // 如果沒有注解或注解值為空,使用字段名
+                    fieldMap.put(fieldName, fieldName);
+                }
+                
+                // 繼續處理父類
+                currentClass = currentClass.getSuperclass();
+            }
+        } catch (Exception e) {
+            log.warn("構建字段名映射失敗,類: {}", clazz.getName(), e);
+        }
+        
+        return fieldMap;
+    }
+    
+    /**
+     * 獲取字段的中文名稱(兼容舊方法,使用默認映射)
+     */
+    static String getChineseName(String fieldName) {
+        if (fieldName == null) {
+            return fieldName;
+        }
+        // 如果沒有實體類信息,返回字段名本身
+        return fieldName;
+    }
+    
+    /**
+     * 批量獲取字段的中文名稱映射
+     */
+    static Map<String, String> getChineseNameMap(Class<?> entityClass, Set<String> fieldNames) {
+        Map<String, String> map = new HashMap<>();
+        if (fieldNames != null && entityClass != null) {
+            for (String fieldName : fieldNames) {
+                map.put(fieldName, getChineseName(entityClass, fieldName));
+            }
+        }
+        return map;
+    }
+}
+
+/**
+ * 數據比較工具類
+ * 用於比較修改前後的數據,生成變更描述
+ *
+ * @author ssk
+ * @since 2025-12-09
+ */
+@Slf4j
+public class DataCompareUtil {
+
+    /**
+     * 比較兩個對象的差異,生成變更描述
+     *
+     * @param beforeObj 修改前的對象
+     * @param afterObj  修改後的對象
+     * @param fieldNameMap 字段名稱映射(可選,用於將字段名轉換為中文描述)
+     * @return 變更描述列表
+     */
+    public static List<String> compareObjects(Object beforeObj, Object afterObj, Map<String, String> fieldNameMap) {
+        List<String> changes = new ArrayList<>();
+        
+        if (beforeObj == null && afterObj == null) {
+            return changes;
+        }
+        
+        if (beforeObj == null) {
+            changes.add("新增數據");
+            return changes;
+        }
+        
+        if (afterObj == null) {
+            changes.add("刪除數據");
+            return changes;
+        }
+
+        try {
+            // 檢查對象是否為字符串(可能是SpEL表達式解析失敗)
+            if (beforeObj instanceof String || afterObj instanceof String) {
+                log.warn("比較對象中包含字符串類型,跳過比較。beforeObj類型: {}, afterObj類型: {}", 
+                        beforeObj != null ? beforeObj.getClass().getSimpleName() : "null",
+                        afterObj != null ? afterObj.getClass().getSimpleName() : "null");
+                changes.add("數據變更(對象類型不正確)");
+                return changes;
+            }
+            
+            // 將對象轉換為JSON對象以便比較(避免反射問題)
+            String beforeJsonStr = JSON.toJSONString(beforeObj);
+            String afterJsonStr = JSON.toJSONString(afterObj);
+            
+            log.info("修改前的JSON數據: {}", beforeJsonStr.length() > 500 ? beforeJsonStr.substring(0, 500) + "..." : beforeJsonStr);
+            log.info("修改後的JSON數據: {}", afterJsonStr.length() > 500 ? afterJsonStr.substring(0, 500) + "..." : afterJsonStr);
+            
+            JSONObject beforeJson = JSONObject.parseObject(beforeJsonStr);
+            JSONObject afterJson = JSONObject.parseObject(afterJsonStr);
+
+            if (beforeJson == null || afterJson == null) {
+                log.warn("JSON解析失敗,beforeJson: {}, afterJson: {}", beforeJson != null, afterJson != null);
+                changes.add("數據變更(JSON解析失敗)");
+                return changes;
+            }
+            
+            log.info("修改前的JSON字段: {}", beforeJson.keySet());
+            log.info("修改後的JSON字段: {}", afterJson.keySet());
+
+            // 獲取所有字段
+            Set<String> allFields = new HashSet<>();
+            allFields.addAll(beforeJson.keySet());
+            allFields.addAll(afterJson.keySet());
+            
+            // 如果沒有提供字段映射,則從實體類的@ApiModelProperty注解中讀取
+            if (fieldNameMap == null) {
+                Class<?> entityClass = beforeObj != null ? beforeObj.getClass() : 
+                                      (afterObj != null ? afterObj.getClass() : null);
+                if (entityClass != null) {
+                    // 跳過JDK內部類
+                    if (!entityClass.getName().startsWith("java.") && !entityClass.getName().startsWith("javax.")) {
+                        fieldNameMap = FieldNameChineseMap.getChineseNameMap(entityClass, allFields);
+                        log.debug("從實體類 {} 的@ApiModelProperty注解中讀取字段映射,共 {} 個字段", 
+                                entityClass.getSimpleName(), fieldNameMap.size());
+                    }
+                }
+            }
+
+            // 過濾不需要比較的字段
+            Set<String> excludeFields = new HashSet<>(Arrays.asList(
+                    "createdTime", "updatedTime", "createdUserId", "updatedUserId",
+                    "deleteFlag", "delFlag", "version", "serialVersionUID", "class"
+            ));
+
+            // 比較每個字段
+            log.info("開始比較字段,總共 {} 個字段,排除 {} 個字段", allFields.size(), excludeFields.size());
+            for (String field : allFields) {
+                if (excludeFields.contains(field)) {
+                    log.debug("跳過排除字段: {}", field);
+                    continue;
+                }
+
+                Object beforeValue = beforeJson.get(field);
+                Object afterValue = afterJson.get(field);
+
+                // 輸出每個字段的比較信息
+                log.info("比較字段: {} - 修改前: [{}], 修改後: [{}]", field, beforeValue, afterValue);
+
+                // 比較值是否發生變化(使用深度比較)
+                boolean isEqual = deepEquals(beforeValue, afterValue);
+                if (!isEqual) {
+                    String fieldName = getFieldLabel(field, fieldNameMap);
+                    String changeDesc = formatChange(fieldName, beforeValue, afterValue);
+                    if (StringUtils.isNotBlank(changeDesc)) {
+                        changes.add(changeDesc);
+                        log.info("✓ 發現字段變更: {} - 修改前: [{}], 修改後: [{}]", fieldName, beforeValue, afterValue);
+                    } else {
+                        log.warn("字段變更但描述為空: {} - 修改前: {}, 修改後: {}", fieldName, beforeValue, afterValue);
+                    }
+                } else {
+                    log.debug("字段 {} 未變更: [{}]", field, beforeValue);
+                }
+            }
+            
+            // 如果沒有變化,添加提示並記錄詳細信息
+            if (changes.isEmpty()) {
+                log.error("========== 未發現任何字段變更 ==========");
+                log.error("比較的字段數量: {}, 排除的字段: {}", allFields.size(), excludeFields.size());
+                log.error("所有字段列表: {}", allFields);
+                log.error("排除的字段列表: {}", excludeFields);
+                log.error("修改前的完整數據: {}", beforeJsonStr);
+                log.error("修改後的完整數據: {}", afterJsonStr);
+                
+                // 強制比較幾個關鍵字段
+                String[] keyFields = {"festivalName", "festival_name", "particularYear", "particular_year", 
+                                     "festivalDate", "festival_date", "name", "title"};
+                for (String keyField : keyFields) {
+                    if (beforeJson.containsKey(keyField) || afterJson.containsKey(keyField)) {
+                        Object beforeVal = beforeJson.get(keyField);
+                        Object afterVal = afterJson.get(keyField);
+                        boolean isEqual = deepEquals(beforeVal, afterVal);
+                        log.error("關鍵字段 {} 比較: 修改前=[{}], 修改後=[{}], 是否相等={}", 
+                                keyField, beforeVal, afterVal, isEqual);
+                        if (!isEqual) {
+                            String changeDesc = formatChange(keyField, beforeVal, afterVal);
+                            if (StringUtils.isNotBlank(changeDesc)) {
+                                changes.add(changeDesc);
+                                log.error("強制發現字段變更: {}", changeDesc);
+                            }
+                        }
+                    }
+                }
+                
+                if (changes.isEmpty()) {
+                    changes.add("無字段變更");
+                }
+            } else {
+                log.info("發現 {} 個字段變更", changes.size());
+            }
+            
+        } catch (Exception e) {
+            log.error("比較數據對象失敗", e);
+            changes.add("數據變更(詳細比較失敗: " + e.getMessage() + ")");
+        }
+
+        return changes;
+    }
+    
+    /**
+     * 深度比較兩個值是否相等
+     */
+    private static boolean deepEquals(Object obj1, Object obj2) {
+        if (obj1 == obj2) {
+            return true;
+        }
+        if (obj1 == null || obj2 == null) {
+            log.debug("深度比較:一個值為null,obj1={}, obj2={}", obj1, obj2);
+            return false;
+        }
+        
+        // 如果是字符串,去除首尾空格後比較
+        if (obj1 instanceof String && obj2 instanceof String) {
+            String str1 = ((String) obj1).trim();
+            String str2 = ((String) obj2).trim();
+            boolean equals = str1.equals(str2);
+            if (!equals) {
+                log.debug("字符串比較不相等: '{}' vs '{}'", str1, str2);
+            }
+            return equals;
+        }
+        
+        // 如果是數字類型,轉換為字符串比較(避免類型不同但值相同的情況)
+        if (obj1 instanceof Number && obj2 instanceof Number) {
+            boolean equals = Objects.equals(obj1.toString(), obj2.toString());
+            if (!equals) {
+                log.debug("數字比較不相等: {} vs {}", obj1, obj2);
+            }
+            return equals;
+        }
+        
+        // 其他類型直接比較
+        boolean equals = Objects.equals(obj1, obj2);
+        if (!equals) {
+            log.debug("對象比較不相等: {} ({}) vs {} ({})", obj1, obj1.getClass().getSimpleName(), obj2, obj2.getClass().getSimpleName());
+        }
+        return equals;
+    }
+    
+    /**
+     * 獲取字段標籤(優先使用映射,否則使用字段名)
+     */
+    private static String getFieldLabel(String fieldName, Map<String, String> fieldNameMap) {
+        // 優先使用傳入的映射
+        if (fieldNameMap != null && fieldNameMap.containsKey(fieldName)) {
+            String label = fieldNameMap.get(fieldName);
+            // 如果映射中的值不是字段名本身,說明有中文名稱,直接返回
+            if (!label.equals(fieldName) && StringUtils.isNotBlank(label)) {
+                return label;
+            }
+        }
+        // 如果沒有映射或映射值為空,使用字段名
+        return fieldName;
+    }
+
+    /**
+     * 格式化變更描述
+     */
+    private static String formatChange(String fieldName, Object beforeValue, Object afterValue) {
+        if (beforeValue == null && afterValue == null) {
+            return null;
+        }
+
+        if (beforeValue == null) {
+            return String.format("%s: 新增為 %s", fieldName, formatValue(afterValue));
+        }
+
+        if (afterValue == null) {
+            return String.format("%s: 從 %s 變更為空", fieldName, formatValue(beforeValue));
+        }
+
+        return String.format("%s: 从 %s 修改成了 %s", fieldName, formatValue(beforeValue), formatValue(afterValue));
+    }
+
+    /**
+     * 格式化值(處理長字符串和特殊類型)
+     */
+    private static String formatValue(Object value) {
+        if (value == null) {
+            return "空";
+        }
+
+        String strValue = value.toString();
+        
+        // 如果是字符串且過長,截取前50個字符
+        if (strValue.length() > 50) {
+            return strValue.substring(0, 50) + "...";
+        }
+
+        return strValue;
+    }
+
+    /**
+     * 比較兩個對象並生成簡要描述
+     *
+     * @param beforeObj 修改前的對象
+     * @param afterObj  修改後的對象
+     * @param idField   標識字段(如id)
+     * @param nameField 名稱字段(如name、title等)
+     * @return 變更描述
+     */
+    public static String generateChangeDescription(Object beforeObj, Object afterObj, String idField, String nameField) {
+        try {
+            String idStr = getFieldValue(beforeObj != null ? beforeObj : afterObj, idField);
+            String nameStr = getFieldValue(beforeObj != null ? beforeObj : afterObj, nameField);
+            
+            // 獲取字段的中文名稱
+            String idFieldChinese = FieldNameChineseMap.getChineseName(idField);
+            String nameFieldChinese = FieldNameChineseMap.getChineseName(nameField);
+
+            if (beforeObj == null) {
+                return String.format("新增了%s為%s,%s為%s的記錄", 
+                        idFieldChinese, StringUtils.isNotBlank(idStr) ? idStr : "未知",
+                        nameFieldChinese, StringUtils.isNotBlank(nameStr) ? nameStr : "未知");
+            }
+
+            if (afterObj == null) {
+                return String.format("刪除了%s為%s,%s為%s的記錄", 
+                        idFieldChinese, StringUtils.isNotBlank(idStr) ? idStr : "未知",
+                        nameFieldChinese, StringUtils.isNotBlank(nameStr) ? nameStr : "未知");
+            }
+
+            // 比較對象差異(會自動從@ApiModelProperty注解中讀取中文名稱)
+            List<String> changes = compareObjects(beforeObj, afterObj, null);
+            
+            // 構建描述
+            StringBuilder desc = new StringBuilder();
+            if (StringUtils.isNotBlank(nameStr)) {
+                desc.append("修改了").append(nameStr);
+            } else if (StringUtils.isNotBlank(idStr)) {
+                desc.append("修改了").append(idFieldChinese).append("為").append(idStr).append("的記錄");
+            } else {
+                desc.append("修改了記錄");
+            }
+            
+            // 添加具體變更字段
+            if (changes != null && !changes.isEmpty() && !changes.contains("無字段變更")) {
+                desc.append(",變更內容:").append(String.join("; ", changes));
+            }
+            
+            return desc.toString();
+        } catch (Exception e) {
+            log.error("生成變更描述失敗", e);
+            return "數據發生變更";
+        }
+    }
+
+    /**
+     * 獲取對象的字段值(通過反射)
+     */
+    private static String getFieldValue(Object obj, String fieldName) {
+        if (obj == null || StringUtils.isBlank(fieldName)) {
+            return "";
+        }
+
+        try {
+            // 先嘗試通過JSON獲取
+            JSONObject jsonObj = JSONObject.parseObject(JSON.toJSONString(obj));
+            Object value = jsonObj.get(fieldName);
+            return value != null ? value.toString() : "";
+
+            // 也可以使用反射方式
+            // Class<?> clazz = obj.getClass();
+            // Field field = clazz.getDeclaredField(fieldName);
+            // field.setAccessible(true);
+            // Object value = field.get(obj);
+            // return value != null ? value.toString() : "";
+        } catch (Exception e) {
+            log.debug("獲取字段值失敗: {}", fieldName, e);
+            return "";
+        }
+    }
+
+    /**
+     * 比較兩個列表的差異(用於批量操作)
+     *
+     * @param beforeList 修改前的列表
+     * @param afterList  修改後的列表
+     * @param idField    標識字段
+     * @return 變更描述
+     */
+    public static String compareLists(List<?> beforeList, List<?> afterList, String idField) {
+        if (beforeList == null) {
+            beforeList = new ArrayList<>();
+        }
+        if (afterList == null) {
+            afterList = new ArrayList<>();
+        }
+
+        List<String> changes = new ArrayList<>();
+
+        // 查找新增的項
+        Set<String> beforeIds = extractIds(beforeList, idField);
+        Set<String> afterIds = extractIds(afterList, idField);
+
+        Set<String> addedIds = new HashSet<>(afterIds);
+        addedIds.removeAll(beforeIds);
+        if (!addedIds.isEmpty()) {
+            changes.add(String.format("新增了%d條記錄(id: %s)", addedIds.size(), String.join(", ", addedIds)));
+        }
+
+        // 查找刪除的項
+        Set<String> deletedIds = new HashSet<>(beforeIds);
+        deletedIds.removeAll(afterIds);
+        if (!deletedIds.isEmpty()) {
+            changes.add(String.format("刪除了%d條記錄(id: %s)", deletedIds.size(), String.join(", ", deletedIds)));
+        }
+
+        // 查找修改的項
+        Set<String> commonIds = new HashSet<>(beforeIds);
+        commonIds.retainAll(afterIds);
+        int modifiedCount = 0;
+        for (String id : commonIds) {
+            Object beforeObj = findById(beforeList, id, idField);
+            Object afterObj = findById(afterList, id, idField);
+            if (beforeObj != null && afterObj != null) {
+                List<String> fieldChanges = compareObjects(beforeObj, afterObj, null);
+                if (!fieldChanges.isEmpty()) {
+                    modifiedCount++;
+                }
+            }
+        }
+        if (modifiedCount > 0) {
+            changes.add(String.format("修改了%d條記錄", modifiedCount));
+        }
+
+        return String.join("; ", changes);
+    }
+
+    /**
+     * 從列表中提取ID
+     */
+    private static Set<String> extractIds(List<?> list, String idField) {
+        Set<String> ids = new HashSet<>();
+        for (Object obj : list) {
+            String id = getFieldValue(obj, idField);
+            if (StringUtils.isNotBlank(id)) {
+                ids.add(id);
+            }
+        }
+        return ids;
+    }
+
+    /**
+     * 根據ID查找對象
+     */
+    private static Object findById(List<?> list, String id, String idField) {
+        for (Object obj : list) {
+            String objId = getFieldValue(obj, idField);
+            if (id.equals(objId)) {
+                return obj;
+            }
+        }
+        return null;
+    }
+}
+