浏览代码

Merge remote-tracking branch 'origin/sit-plantform' into sit-plantform

penghao 6 天之前
父节点
当前提交
599b679efd
共有 19 个文件被更改,包括 2698 次插入0 次删除
  1. 44 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeSysUserRole.java
  2. 118 0
      alien-entity/src/main/java/shop/alien/entity/store/OperationLog.java
  3. 16 0
      alien-entity/src/main/java/shop/alien/mapper/LifeSysUserRoleMapper.java
  4. 16 0
      alien-entity/src/main/java/shop/alien/mapper/OperationLogMapper.java
  5. 48 0
      alien-store/src/main/java/shop/alien/store/annotation/ChangeRecordLog.java
  6. 901 0
      alien-store/src/main/java/shop/alien/store/aspect/OperationLogAspect.java
  7. 9 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCouponController.java
  8. 83 0
      alien-store/src/main/java/shop/alien/store/controller/OperationLogController.java
  9. 1 0
      alien-store/src/main/java/shop/alien/store/controller/TemplateDownloadController.java
  10. 55 0
      alien-store/src/main/java/shop/alien/store/enums/OperationType.java
  11. 8 0
      alien-store/src/main/java/shop/alien/store/service/LifeCouponService.java
  12. 15 0
      alien-store/src/main/java/shop/alien/store/service/LifeSysUserRoleService.java
  13. 20 0
      alien-store/src/main/java/shop/alien/store/service/OperationLogService.java
  14. 265 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeCouponServiceImpl.java
  15. 27 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeSysUserRoleServiceImpl.java
  16. 54 0
      alien-store/src/main/java/shop/alien/store/service/impl/OperationLogServiceImpl.java
  17. 476 0
      alien-store/src/main/java/shop/alien/store/util/ContentGeneratorUtil.java
  18. 542 0
      alien-store/src/main/java/shop/alien/store/util/DataCompareUtil.java
  19. 二进制
      alien-store/src/main/resources/templates/holiday.xlsx

+ 44 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeSysUserRole.java

@@ -0,0 +1,44 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 用户角色关联表
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@TableName("life_sys_user_role")
+@ApiModel(value = "LifeSysUserRole对象", description = "用户角色关联表")
+public class LifeSysUserRole implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "用户ID(关联life_sys.id)")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "角色ID(关联life_sys_role.role_id)")
+    @TableField("role_id")
+    private Long roleId;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+}
+

+ 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/LifeSysUserRoleMapper.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.LifeSysUserRole;
+
+/**
+ * 用户角色关联表 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Mapper
+public interface LifeSysUserRoleMapper extends BaseMapper<LifeSysUserRole> {
+}
+

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

+ 9 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeCouponController.java

@@ -6,6 +6,7 @@ import io.swagger.annotations.*;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.EssentialHolidayComparison;
 import shop.alien.entity.store.LifeCoupon;
@@ -181,4 +182,12 @@ public class LifeCouponController {
         }
         return R.fail("失败");
     }
+
+    @ApiOperation("导入假期管理")
+    //@ApiImplicitParams({@ApiImplicitParam(name = "file", value = "Excel文件", dataType = "MultipartFile", paramType = "form", required = true)})
+    @PostMapping("/importHoliday")
+    public R<String> importHoliday(MultipartFile file) {
+        log.info("LifeCouponController.importHoliday fileName={}", file.getOriginalFilename());
+        return lifeCouponService.importHolidayFromExcel(file);
+    }
 }

+ 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());
+        }
+    }
+}
+

+ 1 - 0
alien-store/src/main/java/shop/alien/store/controller/TemplateDownloadController.java

@@ -40,6 +40,7 @@ public class TemplateDownloadController {
      */
     private static final Map<String, String> TEMPLATE_MAP = new HashMap<String, String>() {{
         put("clearing_receipt", "clearing_receipt.xlsx");
+        put("holiday", "holiday.xlsx");
         // 可以根据需要添加更多模板类型映射
         // put("type2", "template2.xlsx");
         // put("type3", "template3.xls");

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

+ 8 - 0
alien-store/src/main/java/shop/alien/store/service/LifeCouponService.java

@@ -3,6 +3,7 @@ package shop.alien.store.service;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
 import org.springframework.web.bind.annotation.RequestParam;
 import shop.alien.entity.store.EssentialHolidayComparison;
@@ -63,4 +64,11 @@ public interface LifeCouponService extends IService<LifeCoupon> {
      * @return LifeCouponVo
      */
     shop.alien.entity.store.vo.LifeCouponVo getNewCouponDetail(String id);
+
+    /**
+     * 导入假期管理
+     * @param file Excel文件
+     * @return 导入结果
+     */
+    R<String> importHolidayFromExcel(MultipartFile file);
 }

+ 15 - 0
alien-store/src/main/java/shop/alien/store/service/LifeSysUserRoleService.java

@@ -0,0 +1,15 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.LifeSysUserRole;
+
+/**
+ * 用户角色关联表 服务类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+public interface LifeSysUserRoleService extends IService<LifeSysUserRole> {
+
+}
+

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

+ 265 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeCouponServiceImpl.java

@@ -8,10 +8,14 @@ import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
+import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.LifeDiscountCouponStoreFriendDto;
@@ -23,8 +27,10 @@ import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.util.common.UniqueRandomNumGenerator;
 import shop.alien.util.common.constant.OrderStatusEnum;
 
+import java.io.InputStream;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
 import java.time.*;
 import java.time.format.DateTimeFormatter;
 import java.time.format.TextStyle;
@@ -38,6 +44,7 @@ import java.util.stream.Collectors;
  * @version 1.0
  * @date 2024/12/23 15:08
  */
+@Slf4j
 @Service
 @RequiredArgsConstructor
 public class LifeCouponServiceImpl extends ServiceImpl<LifeCouponMapper, LifeCoupon> implements LifeCouponService {
@@ -694,4 +701,262 @@ public class LifeCouponServiceImpl extends ServiceImpl<LifeCouponMapper, LifeCou
         return lifeCouponMapper.getNewCouponDetail(id);
     }
 
+    @Override
+    public R<String> importHolidayFromExcel(MultipartFile file) {
+        log.info("LifeCouponServiceImpl.importHolidayFromExcel fileName={}", file.getOriginalFilename());
+
+        if (file == null || file.isEmpty()) {
+            return R.fail("上传文件为空");
+        }
+
+        String fileName = file.getOriginalFilename();
+        if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
+            return R.fail("文件格式不正确,请上传Excel文件");
+        }
+
+        List<String> errorMessages = new ArrayList<>();
+        int successCount = 0;
+        int totalCount = 0;
+
+        try (InputStream inputStream = file.getInputStream();
+             XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
+            Sheet sheet = workbook.getSheetAt(0);
+
+            // 获取表头(第6行,索引为5)
+            Row headerRow = sheet.getRow(5);
+            if (headerRow == null) {
+                return R.fail("Excel文件格式不正确,缺少表头");
+            }
+
+            // 构建字段映射(表头名称 -> 列索引)
+            Map<String, Integer> headerMap = new HashMap<>();
+            for (int i = 0; i < headerRow.getLastCellNum(); i++) {
+                Cell cell = headerRow.getCell(i);
+                if (cell != null) {
+                    String headerName = getCellValueAsString(cell);
+                    if (com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(headerName)) {
+                        headerMap.put(headerName.trim(), i);
+                    }
+                }
+            }
+
+            // 验证表头
+            if (!headerMap.containsKey("年份") || !headerMap.containsKey("节日名称") 
+                    || !headerMap.containsKey("开始时间") || !headerMap.containsKey("结束时间")) {
+                return R.fail("Excel文件格式不正确,缺少必要的表头字段(年份、节日名称、开始时间、结束时间)");
+            }
+
+            // 读取数据行(从第7行开始,索引为6)
+            for (int rowIndex = 6; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
+                Row row = sheet.getRow(rowIndex);
+                if (row == null) {
+                    continue;
+                }
+
+                // 检查是否为空行
+                boolean isEmptyRow = true;
+                for (int i = 0; i < row.getLastCellNum(); i++) {
+                    Cell cell = row.getCell(i);
+                    if (cell != null && cell.getCellType() != CellType.BLANK) {
+                        String cellValue = getCellValueAsString(cell);
+                        if (com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(cellValue)) {
+                            isEmptyRow = false;
+                            break;
+                        }
+                    }
+                }
+                if (isEmptyRow) {
+                    continue;
+                }
+
+                totalCount++;
+                EssentialHolidayComparison holiday = new EssentialHolidayComparison();
+
+                try {
+                    // 读取年份(必填)
+                    Integer yearColIndex = headerMap.get("年份");
+                    if (yearColIndex == null) {
+                        throw new RuntimeException("缺少年份字段");
+                    }
+                    Cell yearCell = row.getCell(yearColIndex);
+                    String yearValue = getCellValueAsString(yearCell);
+                    if (StringUtils.isEmpty(yearValue)) {
+                        throw new RuntimeException("年份不能为空");
+                    }
+                    // 处理年份可能是数字的情况
+                    if (yearCell != null && yearCell.getCellType() == CellType.NUMERIC) {
+                        double numericValue = yearCell.getNumericCellValue();
+                        yearValue = String.valueOf((long) numericValue);
+                    }
+                    holiday.setParticularYear(yearValue.trim());
+
+                    // 读取节日名称(必填)
+                    Integer nameColIndex = headerMap.get("节日名称");
+                    if (nameColIndex == null) {
+                        throw new RuntimeException("缺少节日名称字段");
+                    }
+                    Cell nameCell = row.getCell(nameColIndex);
+                    String nameValue = getCellValueAsString(nameCell);
+                    if (StringUtils.isEmpty(nameValue)) {
+                        throw new RuntimeException("节日名称不能为空");
+                    }
+                    holiday.setFestivalName(nameValue.trim());
+
+                    // 读取开始时间(必填,格式:2026-01-01)
+                    Integer startTimeColIndex = headerMap.get("开始时间");
+                    if (startTimeColIndex == null) {
+                        throw new RuntimeException("缺少开始时间字段");
+                    }
+                    Cell startTimeCell = row.getCell(startTimeColIndex);
+                    String startTimeValue = getCellValueAsString(startTimeCell);
+                    if (StringUtils.isEmpty(startTimeValue)) {
+                        throw new RuntimeException("开始时间不能为空");
+                    }
+                    Date startTime = parseDate(startTimeValue.trim(), rowIndex + 1);
+                    holiday.setStartTime(startTime);
+
+                    // 读取结束时间(必填,格式:2026-01-01)
+                    Integer endTimeColIndex = headerMap.get("结束时间");
+                    if (endTimeColIndex == null) {
+                        throw new RuntimeException("缺少结束时间字段");
+                    }
+                    Cell endTimeCell = row.getCell(endTimeColIndex);
+                    String endTimeValue = getCellValueAsString(endTimeCell);
+                    if (StringUtils.isEmpty(endTimeValue)) {
+                        throw new RuntimeException("结束时间不能为空");
+                    }
+                    Date endTime = parseDate(endTimeValue.trim(), rowIndex + 1);
+                    holiday.setEndTime(endTime);
+
+                    // 验证结束时间必须大于等于开始时间
+                    if (endTime.before(startTime)) {
+                        throw new RuntimeException("结束时间必须大于等于开始时间");
+                    }
+
+                    // 读取状态(可选,默认启用)
+                    Integer statusColIndex = headerMap.get("状态");
+                    int openFlag = 1; // 默认启用
+                    if (statusColIndex != null) {
+                        Cell statusCell = row.getCell(statusColIndex);
+                        String statusValue = getCellValueAsString(statusCell);
+                        if (com.baomidou.mybatisplus.core.toolkit.StringUtils.isNotEmpty(statusValue)) {
+                            String status = statusValue.trim();
+                            if ("启用".equals(status) || "1".equals(status)) {
+                                openFlag = 1;
+                            } else if ("禁用".equals(status) || "0".equals(status)) {
+                                openFlag = 0;
+                            } else {
+                                throw new RuntimeException("状态格式错误,请输入'启用'或'禁用'");
+                            }
+                        }
+                    }
+                    holiday.setOpenFlag(openFlag);
+                    holiday.setDelFlag(0);
+
+                    // 设置节日日期为开始时间
+                    LocalDate startLocalDate = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+                    holiday.setFestivalDate(startLocalDate);
+
+                    // 保存数据
+                    essentialHolidayComparisonMapper.insert(holiday);
+                    successCount++;
+                } catch (Exception e) {
+                    errorMessages.add(String.format("第%d行:%s", rowIndex + 1, e.getMessage()));
+                    log.error("导入第{}行数据失败", rowIndex + 1, e);
+                }
+            }
+        } catch (Exception e) {
+            log.error("导入Excel失败", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+
+        // 构建返回消息
+        StringBuilder message = new StringBuilder();
+        message.append(String.format("导入完成:成功%d条,失败%d条", successCount, totalCount - successCount));
+        if (!errorMessages.isEmpty()) {
+            message.append("\n失败详情:\n");
+            for (int i = 0; i < Math.min(errorMessages.size(), 10); i++) {
+                message.append(errorMessages.get(i)).append("\n");
+            }
+            if (errorMessages.size() > 10) {
+                message.append("...还有").append(errorMessages.size() - 10).append("条错误信息");
+            }
+        }
+
+        return R.success(message.toString());
+    }
+
+    /**
+     * 解析日期字符串
+     */
+    private Date parseDate(String dateStr, int rowNum) {
+        try {
+            // 尝试多种日期格式
+            SimpleDateFormat[] formats = {
+                new SimpleDateFormat("yyyy-MM-dd"),
+                new SimpleDateFormat("yyyy/MM/dd"),
+                new SimpleDateFormat("yyyy年MM月dd日")
+            };
+
+            for (SimpleDateFormat format : formats) {
+                try {
+                    return format.parse(dateStr);
+                } catch (Exception e) {
+                    // 继续尝试下一个格式
+                }
+            }
+
+            // 如果都失败,尝试解析数字日期(Excel日期格式)
+            try {
+                double numericValue = Double.parseDouble(dateStr);
+                return org.apache.poi.ss.usermodel.DateUtil.getJavaDate(numericValue);
+            } catch (Exception e) {
+                // 忽略
+            }
+
+            throw new RuntimeException("日期格式错误,请使用格式:2026-01-01");
+        } catch (Exception e) {
+            throw new RuntimeException("日期格式错误,请使用格式:2026-01-01");
+        }
+    }
+
+    /**
+     * 获取单元格值(字符串格式)
+     */
+    private String getCellValueAsString(Cell cell) {
+        if (cell == null) {
+            return null;
+        }
+
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) {
+                    // 日期格式
+                    Date date = cell.getDateCellValue();
+                    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+                    return sdf.format(date);
+                } else {
+                    // 处理数字,避免科学计数法
+                    double numericValue = cell.getNumericCellValue();
+                    if (numericValue == (long) numericValue) {
+                        return String.valueOf((long) numericValue);
+                    } else {
+                        return String.valueOf(numericValue);
+                    }
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                try {
+                    return cell.getStringCellValue();
+                } catch (Exception e) {
+                    return String.valueOf(cell.getNumericCellValue());
+                }
+            default:
+                return null;
+        }
+    }
+
 }

+ 27 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeSysUserRoleServiceImpl.java

@@ -0,0 +1,27 @@
+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 org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.LifeSysUserRole;
+import shop.alien.mapper.LifeSysUserRoleMapper;
+import shop.alien.store.service.LifeSysUserRoleService;
+
+/**
+ * 用户角色关联表 服务实现类
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class LifeSysUserRoleServiceImpl extends ServiceImpl<LifeSysUserRoleMapper, LifeSysUserRole> implements LifeSysUserRoleService {
+
+    private final LifeSysUserRoleMapper lifeSysUserRoleMapper;
+
+}
+

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

二进制
alien-store/src/main/resources/templates/holiday.xlsx