Quellcode durchsuchen

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

lyx vor 1 Woche
Ursprung
Commit
7c0eefdad1
47 geänderte Dateien mit 4277 neuen und 30 gelöschten Zeilen
  1. 74 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeFeedback.java
  2. 66 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeImg.java
  3. 19 4
      alien-entity/src/main/java/shop/alien/entity/store/LifeLog.java
  4. 118 0
      alien-entity/src/main/java/shop/alien/entity/store/OperationLog.java
  5. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/FeedbackReplyDto.java
  6. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackAssignDto.java
  7. 39 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackDto.java
  8. 35 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackQueryDto.java
  9. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackReplyWebDto.java
  10. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackStatusDto.java
  11. 10 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeUserViolationDto.java
  12. 29 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackAttachmentVo.java
  13. 41 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackLogVo.java
  14. 75 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackDetailVo.java
  15. 62 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackListVo.java
  16. 60 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackVo.java
  17. 26 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeStaffListVo.java
  18. 9 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserViolationVo.java
  19. 92 0
      alien-entity/src/main/java/shop/alien/mapper/LifeFeedbackMapper.java
  20. 44 0
      alien-entity/src/main/java/shop/alien/mapper/LifeImgMapper.java
  21. 24 0
      alien-entity/src/main/java/shop/alien/mapper/LifeLogMapper.java
  22. 39 0
      alien-entity/src/main/java/shop/alien/mapper/LifeUserViolationMapper.java
  23. 16 0
      alien-entity/src/main/java/shop/alien/mapper/OperationLogMapper.java
  24. 207 0
      alien-entity/src/main/resources/mapper/LifeFeedbackMapper.xml
  25. 70 0
      alien-entity/src/main/resources/mapper/LifeImgMapper.xml
  26. 2 2
      alien-entity/src/main/resources/mapper/LifeUserDynamicsMapper.xml
  27. 48 0
      alien-store/src/main/java/shop/alien/store/annotation/ChangeRecordLog.java
  28. 901 0
      alien-store/src/main/java/shop/alien/store/aspect/OperationLogAspect.java
  29. 131 0
      alien-store/src/main/java/shop/alien/store/controller/LifeFeedbackController.java
  30. 83 0
      alien-store/src/main/java/shop/alien/store/controller/OperationLogController.java
  31. 12 6
      alien-store/src/main/java/shop/alien/store/controller/ProtocolManagementController.java
  32. 1 0
      alien-store/src/main/java/shop/alien/store/controller/TemplateDownloadController.java
  33. 25 0
      alien-store/src/main/java/shop/alien/store/controller/UserViolationController.java
  34. 55 0
      alien-store/src/main/java/shop/alien/store/enums/OperationType.java
  35. 87 0
      alien-store/src/main/java/shop/alien/store/service/LifeFeedbackService.java
  36. 41 0
      alien-store/src/main/java/shop/alien/store/service/LifeImgService.java
  37. 2 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserViolationService.java
  38. 20 0
      alien-store/src/main/java/shop/alien/store/service/OperationLogService.java
  39. 2 2
      alien-store/src/main/java/shop/alien/store/service/ProtocolManagementService.java
  40. 374 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeFeedbackServiceImpl.java
  41. 46 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeImgServiceImpl.java
  42. 34 8
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserViolationServiceImpl.java
  43. 54 0
      alien-store/src/main/java/shop/alien/store/service/impl/OperationLogServiceImpl.java
  44. 79 8
      alien-store/src/main/java/shop/alien/store/service/impl/ProtocolManagementServiceImpl.java
  45. 476 0
      alien-store/src/main/java/shop/alien/store/util/ContentGeneratorUtil.java
  46. 542 0
      alien-store/src/main/java/shop/alien/store/util/DataCompareUtil.java
  47. BIN
      alien-store/src/main/resources/templates/holiday.xlsx

+ 74 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeFeedback.java

@@ -0,0 +1,74 @@
+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;
+
+/**
+ * 意见反馈表
+ */
+@Data
+@JsonInclude
+@TableName("life_feedback")
+@ApiModel(value = "LifeFeedback对象", description = "意见反馈")
+public class LifeFeedback implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "用户ID")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "反馈来源:0-用户端,1-商家端")
+    @TableField("feedback_source")
+    private Integer feedbackSource;
+
+    @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
+    @TableField("feedback_way")
+    private Integer feedbackWay;
+
+    @ApiModelProperty(value = "反馈类型:0-优化建议,1-问题")
+    @TableField("feedback_type")
+    private Integer feedbackType;
+
+    @ApiModelProperty(value = "反馈内容")
+    @TableField("content")
+    private String content;
+
+    @ApiModelProperty(value = "联系方式(手机号或邮箱)")
+    @TableField("contact_way")
+    private String contactWay;
+
+    @ApiModelProperty(value = "反馈时间")
+    @TableField("feedback_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date feedbackTime;
+
+    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决")
+    @TableField("handle_status")
+    private Integer handleStatus;
+
+    @ApiModelProperty(value = "跟进人员ID(关联life_sys表的id)")
+    @TableField("staff_id")
+    private Integer staffId;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updateTime;
+}
+

+ 66 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeImg.java

@@ -0,0 +1,66 @@
+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;
+
+/**
+ * 反馈图片表
+ */
+@Data
+@JsonInclude
+@TableName("life_img")
+@ApiModel(value = "LifeImg对象", description = "反馈图片")
+public class LifeImg implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "反馈ID(关联life_feedback)")
+    @TableField("feedback_id")
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "文件类型:1-图片,2-视频")
+    @TableField("file_type")
+    private Integer fileType;
+
+    @ApiModelProperty(value = "图片/视频URL")
+    @TableField("img_url")
+    private String imgUrl;
+
+    @ApiModelProperty(value = "缩略图URL(视频的封面图)")
+    @TableField("thumbnail_url")
+    private String thumbnailUrl;
+
+    @ApiModelProperty(value = "上传时间")
+    @TableField("upload_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date uploadTime;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "create_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createTime;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "update_time", fill = FieldFill.UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updateTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 19 - 4
alien-entity/src/main/java/shop/alien/entity/store/LifeLog.java

@@ -3,24 +3,39 @@ 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;
 
 /**
- * 日志
+ * 意见反馈日志
  */
 @Data
 @JsonInclude
 @TableName("life_log")
-public class LifeLog {
+@ApiModel(value = "LifeLog对象", description = "意见反馈日志")
+public class LifeLog implements Serializable {
+    private static final long serialVersionUID = 1L;
 
+    @ApiModelProperty(value = "主键ID")
     @TableId(value = "id", type = IdType.AUTO)
-    private String id;
+    private Integer id;
 
+    @ApiModelProperty(value = "意见反馈主表ID")
+    @TableField("feedback_id")
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "日志内容")
+    @TableField("context")
     private String context;
 
+    @ApiModelProperty(value = "操作类型:0-问题解决状态,1-分配跟踪人员,3-回复用户")
+    @TableField("type")
+    private String type;
+
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @TableField("delete_flag")
     @TableLogic
@@ -36,7 +51,7 @@ public class LifeLog {
     private Integer createdUserId;
 
     @ApiModelProperty(value = "修改时间")
-    @TableField(value = "updated_time", fill = FieldFill.UPDATE)
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date updatedTime;
 

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

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/FeedbackReplyDto.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 平台回复反馈DTO
+ */
+@Data
+@ApiModel(value = "FeedbackReplyDto对象", description = "平台回复反馈DTO")
+public class FeedbackReplyDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "原始反馈ID")
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "平台工作人员ID")
+    private Integer staffId;
+
+    @ApiModelProperty(value = "回复内容")
+    private String content;
+}
+

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackAssignDto.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 中台分配跟踪人员DTO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackAssignDto对象", description = "中台分配跟踪人员DTO")
+public class LifeFeedbackAssignDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈ID", required = true)
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "跟踪人员ID(关联life_sys表的id)", required = true)
+    private Integer staffId;
+
+    @ApiModelProperty(value = "操作人员ID")
+    private Integer operatorId;
+}
+

+ 39 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackDto.java

@@ -0,0 +1,39 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 意见反馈提交DTO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackDto对象", description = "意见反馈提交DTO")
+public class LifeFeedbackDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "用户ID")
+    private Integer userId;
+
+    @ApiModelProperty(value = "反馈来源:1-用户端,2-商家端")
+    private Integer feedbackSource;
+
+    @ApiModelProperty(value = "反馈方式:1-主动反馈,2-平台回复")
+    private Integer feedbackWay;
+
+    @ApiModelProperty(value = "反馈类型:1-优化建议,2-问题")
+    private Integer feedbackType;
+
+    @ApiModelProperty(value = "反馈内容")
+    private String content;
+
+    @ApiModelProperty(value = "联系方式(手机号或邮箱)")
+    private String contactWay;
+
+    @ApiModelProperty(value = "图片URL列表")
+    private List<String> imgUrlList;
+}
+

+ 35 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackQueryDto.java

@@ -0,0 +1,35 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 中台意见反馈查询DTO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackQueryDto对象", description = "中台意见反馈查询DTO")
+public class LifeFeedbackQueryDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
+    private Integer feedbackType;
+
+    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决,2-未分配,3-无需解决")
+    private Integer handleStatus;
+
+    @ApiModelProperty(value = "反馈来源:0-用户端,1-商家端")
+    private Integer feedbackSource;
+
+    @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
+    private Integer feedbackWay;
+
+    @ApiModelProperty(value = "页码", required = true)
+    private Integer page = 1;
+
+    @ApiModelProperty(value = "每页数量", required = true)
+    private Integer size = 10;
+}
+

+ 29 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackReplyWebDto.java

@@ -0,0 +1,29 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 中台回复用户DTO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackReplyWebDto对象", description = "中台回复用户DTO")
+public class LifeFeedbackReplyWebDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈ID", required = true)
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "回复内容", required = true)
+    private String content;
+
+    @ApiModelProperty(value = "操作人员ID")
+    private Integer operatorId;
+
+    @ApiModelProperty(value = "用户端回复内容")
+    private String userReply;
+}
+

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackStatusDto.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 中台更新反馈处理状态DTO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackStatusDto对象", description = "中台更新反馈处理状态DTO")
+public class LifeFeedbackStatusDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈ID", required = true)
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决", required = true)
+    private Integer handleStatus;
+
+    // @ApiModelProperty(value = "操作人员ID")
+    // private Integer operatorId;
+}
+

+ 10 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/LifeUserViolationDto.java

@@ -105,6 +105,16 @@ public class LifeUserViolationDto {
     @ApiModelProperty(value = "举报内容")
     private String LifeNotice;
 
+    //  举报类型状态名称
+    @ApiModelProperty(value = "举报类型状态名称")
+    private String violationTypeName;
 
+    // 举报类型名称
+    @ApiModelProperty(value = "举报类型名称")
+    private String reportContextName;
+
+    // 处理状态名称
+    @ApiModelProperty(value = "处理状态名称")
+    private String processingName;
 
 }

+ 29 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackAttachmentVo.java

@@ -0,0 +1,29 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 反馈附件VO
+ */
+@Data
+@ApiModel(value = "FeedbackAttachmentVo对象", description = "反馈附件VO")
+public class FeedbackAttachmentVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "附件ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "文件类型:1-图片,2-视频")
+    private Integer fileType;
+
+    @ApiModelProperty(value = "文件URL")
+    private String fileUrl;
+
+    @ApiModelProperty(value = "缩略图URL(视频的封面图)")
+    private String thumbnailUrl;
+}
+

+ 41 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackLogVo.java

@@ -0,0 +1,41 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 反馈操作日志VO
+ */
+@Data
+@ApiModel(value = "FeedbackLogVo对象", description = "反馈操作日志VO")
+public class FeedbackLogVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "日志ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "操作类型:0-问题解决状态,1-分配跟踪人员,3-回复用户")
+    private Integer type;
+
+    @ApiModelProperty(value = "操作类型名称")
+    private String typeName;
+
+    @ApiModelProperty(value = "日志内容")
+    private String context;
+
+    @ApiModelProperty(value = "子内容(用于回复用户时显示用户回复)")
+    private String subContext;
+
+    @ApiModelProperty(value = "操作时间")
+    @JsonFormat(pattern = "yyyy/MM/dd HH:mm", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "操作人姓名")
+    private String operatorName;
+}
+

+ 75 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackDetailVo.java

@@ -0,0 +1,75 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 中台意见反馈详情VO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackDetailVo对象", description = "中台意见反馈详情VO")
+public class LifeFeedbackDetailVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "用户昵称")
+    private String nickName;
+
+    @ApiModelProperty(value = "账号(手机号)")
+    private String phone;
+
+    @ApiModelProperty(value = "跟踪人员姓名+联系方式")
+    private String staffInfo;
+
+    @ApiModelProperty(value = "跟踪人员ID")
+    private Integer staffId;
+
+    @ApiModelProperty(value = "反馈来源:0-用户端,1-商家端")
+    private Integer feedbackSource;
+
+    @ApiModelProperty(value = "反馈来源名称")
+    private String feedbackSourceName;
+
+    @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
+    private Integer feedbackWay;
+
+    @ApiModelProperty(value = "反馈方式名称")
+    private String feedbackWayName;
+
+    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
+    private Integer feedbackType;
+
+    @ApiModelProperty(value = "反馈类型名称")
+    private String feedbackTypeName;
+
+    @ApiModelProperty(value = "反馈时间")
+    @JsonFormat(pattern = "yyyy/MM/dd HH:mm", timezone = "GMT+8")
+    private Date feedbackTime;
+
+    @ApiModelProperty(value = "问题描述")
+    private String content;
+
+    @ApiModelProperty(value = "联系方式")
+    private String contactWay;
+
+    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决,2-未分配,3-无需解决")
+    private Integer handleStatus;
+
+    @ApiModelProperty(value = "处理状态名称")
+    private String handleStatusName;
+
+    @ApiModelProperty(value = "附件列表")
+    private List<FeedbackAttachmentVo> attachments;
+
+    @ApiModelProperty(value = "操作日志列表")
+    private List<FeedbackLogVo> logs;
+}
+

+ 62 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackListVo.java

@@ -0,0 +1,62 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 中台意见反馈列表VO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackListVo对象", description = "中台意见反馈列表VO")
+public class LifeFeedbackListVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "用户昵称")
+    private String nickName;
+
+    @ApiModelProperty(value = "账号(手机号)")
+    private String phone;
+
+    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
+    private Integer feedbackType;
+
+    @ApiModelProperty(value = "反馈类型名称")
+    private String feedbackTypeName;
+
+    @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
+    private Integer feedbackWay;
+
+    @ApiModelProperty(value = "反馈方式名称")
+    private String feedbackWayName;
+
+    @ApiModelProperty(value = "反馈来源:0-用户端,1-商家端")
+    private Integer feedbackSource;
+
+    @ApiModelProperty(value = "反馈来源名称")
+    private String feedbackSourceName;
+
+    @ApiModelProperty(value = "反馈时间")
+    @JsonFormat(pattern = "yyyy/MM/dd HH:mm", timezone = "GMT+8")
+    private Date feedbackTime;
+
+    @ApiModelProperty(value = "跟踪人员姓名+部门")
+    private String staffInfo;
+
+    @ApiModelProperty(value = "跟踪人员ID")
+    private Integer staffId;
+
+    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决,2-未分配,3-无需解决")
+    private Integer handleStatus;
+
+    @ApiModelProperty(value = "处理状态名称")
+    private String handleStatusName;
+}
+

+ 60 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackVo.java

@@ -0,0 +1,60 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 意见反馈展示VO
+ */
+@Data
+@ApiModel(value = "LifeFeedbackVo对象", description = "意见反馈展示VO")
+public class LifeFeedbackVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "用户ID")
+    private Integer userId;
+
+    @ApiModelProperty(value = "反馈来源:1-用户端,2-商家端")
+    private Integer feedbackSource;
+
+    @ApiModelProperty(value = "反馈方式:1-主动反馈,2-平台回复")
+    private Integer feedbackWay;
+
+    @ApiModelProperty(value = "反馈类型:1-优化建议,2-问题")
+    private Integer feedbackType;
+
+    @ApiModelProperty(value = "反馈内容")
+    private String content;
+
+    @ApiModelProperty(value = "联系方式")
+    private String contactWay;
+
+    @ApiModelProperty(value = "反馈时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date feedbackTime;
+
+    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决,2-未分配,3-无需解决")
+    private Integer handleStatus;
+
+    @ApiModelProperty(value = "跟进工作人员ID")
+    private Integer staffId;
+
+    @ApiModelProperty(value = "跟进工作人员姓名")
+    private String staffName;
+
+    @ApiModelProperty(value = "附件图片列表")
+    private List<String> imgUrlList;
+
+    @ApiModelProperty(value = "平台反馈建议列表")
+    private List<LifeFeedbackVo> platformReplies;
+}
+

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeStaffListVo.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 中台跟踪人员列表VO
+ */
+@Data
+@ApiModel(value = "LifeStaffListVo对象", description = "中台跟踪人员列表VO")
+public class LifeStaffListVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "人员ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "人员信息(姓名)")
+    private String staffInfo;
+
+    @ApiModelProperty(value = "姓名")
+    private String realName;
+}
+

+ 9 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserViolationVo.java

@@ -29,4 +29,13 @@ public class LifeUserViolationVo extends LifeUserViolation {
     private String nickName;
 
     private String image;
+
+    //  举报类型状态名称
+    private String violationTypeName;
+
+    // 举报类型名称
+    private String reportContextName;
+
+    // 处理状态名称
+    private String processingName;
 }

+ 92 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeFeedbackMapper.java

@@ -0,0 +1,92 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.LifeFeedback;
+import shop.alien.entity.store.vo.LifeFeedbackDetailVo;
+import shop.alien.entity.store.vo.LifeFeedbackListVo;
+import shop.alien.entity.store.vo.LifeFeedbackVo;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 意见反馈 Mapper 接口
+ */
+@Mapper
+public interface LifeFeedbackMapper extends BaseMapper<LifeFeedback> {
+
+    /**
+     * 查询用户反馈列表(带工作人员名称)
+     * @param page 分页对象
+     * @param userId 用户ID
+     * @param feedbackSource 反馈来源
+     * @param feedbackWay 反馈方式
+     * @param handleStatus 处理状态
+     * @return 反馈列表
+     */
+    IPage<LifeFeedbackVo> selectFeedbackListWithStaff(
+            Page<LifeFeedbackVo> page,
+            @Param("userId") Integer userId,
+            @Param("feedbackSource") Integer feedbackSource,
+            @Param("feedbackWay") Integer feedbackWay,
+            @Param("handleStatus") Integer handleStatus
+    );
+
+    /**
+     * 查询反馈详情(带工作人员名称)
+     * @param feedbackId 反馈ID
+     * @return 反馈详情
+     */
+    LifeFeedbackVo selectFeedbackDetail(@Param("feedbackId") Integer feedbackId);
+
+    /**
+     * 统计待处理反馈数量
+     * @param feedbackSource 反馈来源
+     * @return 待处理数量
+     */
+    Integer countPendingFeedback(@Param("feedbackSource") Integer feedbackSource);
+
+    /**
+     * 查询平台回复列表
+     * @param userId 用户ID
+     * @param feedbackSource 反馈来源
+     * @param startTime 开始时间
+     * @return 平台回复列表
+     */
+    List<LifeFeedbackVo> selectPlatformReplies(
+            @Param("userId") Integer userId,
+            @Param("feedbackSource") Integer feedbackSource,
+            @Param("startTime") Date startTime
+    );
+
+    // ==================== Web中台接口 ====================
+
+    /**
+     * 中台-查询意见反馈列表
+     * @param page 分页对象
+     * @param feedbackType 反馈类型
+     * @param handleStatus 处理状态
+     * @param feedbackSource 反馈来源
+     * @param feedbackWay 反馈方式
+     * @return 反馈列表
+     */
+    IPage<LifeFeedbackListVo> selectWebFeedbackList(
+            Page<LifeFeedbackListVo> page,
+            @Param("feedbackType") Integer feedbackType,
+            @Param("handleStatus") Integer handleStatus,
+            @Param("feedbackSource") Integer feedbackSource,
+            @Param("feedbackWay") Integer feedbackWay
+    );
+
+    /**
+     * 中台-查询反馈详情
+     * @param feedbackId 反馈ID
+     * @return 反馈详情
+     */
+    LifeFeedbackDetailVo selectWebFeedbackDetail(@Param("feedbackId") Integer feedbackId);
+}
+

+ 44 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeImgMapper.java

@@ -0,0 +1,44 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.LifeImg;
+
+import java.util.List;
+
+/**
+ * 反馈图片 Mapper 接口
+ */
+@Mapper
+public interface LifeImgMapper extends BaseMapper<LifeImg> {
+
+    /**
+     * 根据反馈ID查询图片列表
+     * @param feedbackId 反馈ID
+     * @return 图片列表
+     */
+    List<LifeImg> selectByFeedbackId(@Param("feedbackId") Integer feedbackId);
+
+    /**
+     * 批量插入图片
+     * @param list 图片列表
+     * @return 插入数量
+     */
+    Integer batchInsert(@Param("list") List<LifeImg> list);
+
+    /**
+     * 根据反馈ID删除图片(逻辑删除)
+     * @param feedbackId 反馈ID
+     * @return 删除数量
+     */
+    Integer deleteByFeedbackId(@Param("feedbackId") Integer feedbackId);
+
+    /**
+     * 查询反馈的图片URL列表
+     * @param feedbackId 反馈ID
+     * @return 图片URL列表
+     */
+    List<String> selectImgUrlsByFeedbackId(@Param("feedbackId") Integer feedbackId);
+}
+

+ 24 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeLogMapper.java

@@ -2,11 +2,35 @@ package shop.alien.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 import shop.alien.entity.store.LifeLog;
+import shop.alien.entity.store.vo.FeedbackLogVo;
+
+import java.util.List;
 
 /**
  * 日志
  */
 @Mapper
 public interface LifeLogMapper extends BaseMapper<LifeLog> {
+
+    /**
+     * 根据反馈ID查询操作日志列表
+     * @param feedbackId 反馈ID
+     * @return 日志列表
+     */
+    @Select("SELECT l.id, l.type, l.context, l.created_time AS createdTime, " +
+            "CASE l.type " +
+            "   WHEN 0 THEN '问题解决状态' " +
+            "   WHEN 1 THEN '分配跟踪人员' " +
+            "   WHEN 2 THEN '创建反馈工单' " +
+            "   WHEN 3 THEN '回复用户' " +
+            "END AS typeName, " +
+            "s.user_name AS operatorName " +
+            "FROM life_log l " +
+            "LEFT JOIN life_sys s ON l.created_user_id = s.id " +
+            "WHERE l.feedback_id = #{feedbackId} AND l.delete_flag = 0 " +
+            "ORDER BY l.created_time DESC")
+    List<FeedbackLogVo> selectLogsByFeedbackId(@Param("feedbackId") Integer feedbackId);
 }

+ 39 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeUserViolationMapper.java

@@ -113,4 +113,43 @@ public interface LifeUserViolationMapper extends BaseMapper<LifeUserViolation> {
     List<LifeUserViolationVo> getViolationList(
             @Param(Constants.WRAPPER) QueryWrapper<LifeUserViolationVo> queryWrapper
     );
+
+
+    @Select("<script>" +
+            " WITH userInfo AS ( " +
+            " SELECT su.phone, su.id, CASE su.delete_flag WHEN 1 THEN CONCAT(su.nick_name, '(账号已注销)') ELSE su.nick_name END AS nick_name, '1' AS type FROM store_user su " +
+            " UNION ALL " +
+            " SELECT lu.user_phone AS phone, lu.id, CASE lu.delete_flag WHEN 1 THEN CONCAT(lu.user_name, '(账号已注销)') ELSE lu.user_name END AS nick_name, '2' AS type FROM life_user lu " +
+            " ) " +
+            " , violationInfo as ( " +
+            " SELECT " +
+            " luv.id, ui.nick_name, ui.phone, luv.report_context_type,  " +
+            " case when luv.violation_type = 1 then '用户违规' when luv.violation_type = 2 then '色情低俗' when luv.violation_type = 3 then '违法违规' when luv.violation_type = 4 then '谩骂嘲讽、煽动对立' when luv.violation_type = 5 then '涉嫌诈骗' " +
+            " when luv.violation_type = 6 then '人身攻击' when luv.violation_type = 7 then '种族歧视' when luv.violation_type = 8 then '政治敏感' when luv.violation_type = 9 then '虚假、不实内容' " +
+            " when luv.violation_type = 10 then '违反公德秩序' when luv.violation_type = 11 then '危害人身安全' when luv.violation_type = 12 then '网络暴力' else '其他原因' end violation_type_name,  " +
+            " case when luv.report_context_type = 0 then '商户' when luv.report_context_type = 1 then '用户' when luv.report_context_type = 2 then '动态' else '评论' end report_context_name, " +
+            " luv.processing_status, case when luv.processing_status = 0 then '待处理' when luv.processing_status = 1 then '已通过' else '已驳回' end processing_name,  " +
+            " luv.processing_time, luv.created_time, img.img_url image, luv.updated_time " +
+            " FROM life_user_violation luv " +
+            " LEFT JOIN userInfo ui ON ui.type = luv.reporting_user_type AND ui.id = luv.reporting_user_id " +
+            " left join store_img img on luv.id = img.store_id and img.delete_flag = 0 " +
+            " where luv.delete_flag = 0 and luv.report_context_type in ('1', '2', '3') " +
+            " union all " +
+            " select " +
+            " luv.id, lu.user_name nick_name, lu.user_phone phone, luv.report_context_type, sd.dict_detail,  " +
+            " case when luv.report_context_type = '4' then '商品' else '用户' end report_context_name,  " +
+            " luv.processing_status, case when luv.processing_status = 0 then '待处理' when luv.processing_status = 1 then '已通过' else '已驳回' end processing_name, " +
+            " luv.processing_time, luv.created_time, '' image, luv.updated_time " +
+            " from life_user_violation luv " +
+            " left join life_user lu on luv.reporting_user_id = lu.id and lu.delete_flag = 0 " +
+            " left join store_dictionary sd on luv.dict_type = sd.type_name and luv.dict_id = sd.dict_id and sd.delete_flag = 0 " +
+            " where luv.report_context_type in ('4', '5') and sd.delete_flag = 0 " +
+            " ) " +
+            " select * from violationInfo " +
+            " ${ew.customSqlSegment}" +
+            "</script>")
+    IPage<LifeUserViolationVo> getAllViolationPage(
+            IPage<LifeUserViolationVo> page,
+            @Param(Constants.WRAPPER) QueryWrapper<LifeUserViolationVo> queryWrapper
+    );
 }

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

+ 207 - 0
alien-entity/src/main/resources/mapper/LifeFeedbackMapper.xml

@@ -0,0 +1,207 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.LifeFeedbackMapper">
+
+    <!-- 通用结果映射 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.LifeFeedback">
+        <id column="id" property="id" />
+        <result column="user_id" property="userId" />
+        <result column="feedback_source" property="feedbackSource" />
+        <result column="feedback_way" property="feedbackWay" />
+        <result column="feedback_type" property="feedbackType" />
+        <result column="content" property="content" />
+        <result column="contact_way" property="contactWay" />
+        <result column="feedback_time" property="feedbackTime" />
+        <result column="staff_id" property="staffId" />
+        <result column="handle_status" property="handleStatus" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
+    </resultMap>
+
+    <!-- 基础字段 -->
+    <sql id="Base_Column_List">
+        id, user_id, feedback_source, feedback_way, feedback_type, 
+        content, contact_way, feedback_time, staff_id, handle_status,
+        create_time, update_time
+    </sql>
+
+    <!-- 查询用户反馈列表(带工作人员名称) -->
+    <select id="selectFeedbackListWithStaff" resultType="shop.alien.entity.store.vo.LifeFeedbackVo">
+        SELECT 
+            f.id,
+            f.user_id AS userId,
+            f.feedback_source AS feedbackSource,
+            f.feedback_way AS feedbackWay,
+            f.feedback_type AS feedbackType,
+            f.content,
+            f.contact_way AS contactWay,
+            f.feedback_time AS feedbackTime,
+            f.staff_id AS staffId,
+            f.handle_status AS handleStatus,
+            s.user_name AS staffName
+        FROM life_feedback f
+        LEFT JOIN life_sys s ON f.staff_id = s.id
+        WHERE 1=1
+        <if test="userId != null">
+            AND f.user_id = #{userId}
+        </if>
+        <if test="feedbackSource != null">
+            AND f.feedback_source = #{feedbackSource}
+        </if>
+        <if test="feedbackWay != null">
+            AND f.feedback_way = #{feedbackWay}
+        </if>
+        <if test="handleStatus != null">
+            AND f.handle_status = #{handleStatus}
+        </if>
+        ORDER BY f.feedback_time DESC
+    </select>
+
+    <!-- 查询反馈详情(带工作人员名称和图片) -->
+    <select id="selectFeedbackDetail" resultType="shop.alien.entity.store.vo.LifeFeedbackVo">
+        SELECT 
+            f.id,
+            f.user_id AS userId,
+            f.feedback_source AS feedbackSource,
+            f.feedback_way AS feedbackWay,
+            f.feedback_type AS feedbackType,
+            f.content,
+            f.contact_way AS contactWay,
+            f.feedback_time AS feedbackTime,
+            f.staff_id AS staffId,
+            f.handle_status AS handleStatus,
+            s.user_name AS staffName
+        FROM life_feedback f
+        LEFT JOIN life_sys s ON f.staff_id = s.id
+        WHERE f.id = #{feedbackId}
+    </select>
+
+    <!-- 统计待处理反馈数量 -->
+    <select id="countPendingFeedback" resultType="java.lang.Integer">
+        SELECT COUNT(1)
+        FROM life_feedback
+        WHERE handle_status = 0
+        <if test="feedbackSource != null">
+            AND feedback_source = #{feedbackSource}
+        </if>
+    </select>
+
+    <!-- 查询平台回复列表 -->
+    <select id="selectPlatformReplies" resultType="shop.alien.entity.store.vo.LifeFeedbackVo">
+        SELECT 
+            f.id,
+            f.user_id AS userId,
+            f.feedback_source AS feedbackSource,
+            f.feedback_way AS feedbackWay,
+            f.feedback_type AS feedbackType,
+            f.content,
+            f.feedback_time AS feedbackTime,
+            f.staff_id AS staffId,
+            f.handle_status AS handleStatus,
+            s.user_name AS staffName
+        FROM life_feedback f
+        LEFT JOIN life_sys s ON f.staff_id = s.id
+        WHERE f.user_id = #{userId}
+        AND f.feedback_source = #{feedbackSource}
+        AND f.feedback_way = 1
+        AND f.feedback_time >= #{startTime}
+        ORDER BY f.feedback_time ASC
+    </select>
+
+    <!-- ==================== Web中台接口 ==================== -->
+
+    <!-- 中台-查询意见反馈列表 -->
+    <select id="selectWebFeedbackList" resultType="shop.alien.entity.store.vo.LifeFeedbackListVo">
+        SELECT 
+            f.id,
+            u.user_name AS nickName,
+            u.user_phone AS phone,
+            f.feedback_type AS feedbackType,
+            CASE f.feedback_type 
+                WHEN 0 THEN 'bug反馈' 
+                WHEN 1 THEN '优化反馈' 
+                WHEN 2 THEN '新增功能反馈' 
+            END AS feedbackTypeName,
+            f.feedback_way AS feedbackWay,
+            CASE f.feedback_way 
+                WHEN 0 THEN '用户反馈' 
+                WHEN 1 THEN 'AI识别' 
+            END AS feedbackWayName,
+            f.feedback_source AS feedbackSource,
+            CASE f.feedback_source 
+                WHEN 0 THEN '用户端' 
+                WHEN 1 THEN '商家端' 
+            END AS feedbackSourceName,
+            f.feedback_time AS feedbackTime,
+            f.staff_id AS staffId,
+            CONCAT(IFNULL(s.user_name, '')) AS staffInfo,
+            f.handle_status AS handleStatus,
+            CASE f.handle_status 
+                WHEN 0 THEN '处理中' 
+                WHEN 1 THEN '已解决' 
+                WHEN 2 THEN '未分配' 
+                WHEN 3 THEN '无需解决' 
+            END AS handleStatusName
+        FROM life_feedback f
+        LEFT JOIN life_user u ON f.user_id = u.id
+        LEFT JOIN life_sys s ON f.staff_id = s.id
+        WHERE 1=1
+        <if test="feedbackType != null">
+            AND f.feedback_type = #{feedbackType}
+        </if>
+        <if test="handleStatus != null">
+            AND f.handle_status = #{handleStatus}
+        </if>
+        <if test="feedbackSource != null">
+            AND f.feedback_source = #{feedbackSource}
+        </if>
+        <if test="feedbackWay != null">
+            AND f.feedback_way = #{feedbackWay}
+        </if>
+        ORDER BY f.feedback_time DESC
+    </select>
+
+    <!-- 中台-查询反馈详情 -->
+    <select id="selectWebFeedbackDetail" resultType="shop.alien.entity.store.vo.LifeFeedbackDetailVo">
+        SELECT 
+            f.id,
+            u.user_name AS nickName,
+            u.user_phone AS phone,
+            f.staff_id AS staffId,
+            CONCAT(IFNULL(s.user_name, '')) AS staffInfo,
+            f.feedback_source AS feedbackSource,
+            CASE f.feedback_source 
+                WHEN 0 THEN '用户端' 
+                WHEN 1 THEN '商家端' 
+            END AS feedbackSourceName,
+            f.feedback_way AS feedbackWay,
+            CASE f.feedback_way 
+                WHEN 0 THEN '用户反馈' 
+                WHEN 1 THEN 'AI识别' 
+            END AS feedbackWayName,
+            f.feedback_type AS feedbackType,
+            CASE f.feedback_type 
+                WHEN 0 THEN 'bug反馈' 
+                WHEN 1 THEN '优化反馈' 
+                WHEN 2 THEN '新增功能反馈' 
+            END AS feedbackTypeName,
+            f.feedback_time AS feedbackTime,
+            f.content,
+            f.contact_way AS contactWay,
+            f.handle_status AS handleStatus,
+            CASE f.handle_status 
+                WHEN 0 THEN '处理中' 
+                WHEN 1 THEN '已解决' 
+                WHEN 2 THEN '未分配' 
+                WHEN 3 THEN '无需解决' 
+            END AS handleStatusName
+        FROM life_feedback f
+        LEFT JOIN life_user u ON f.user_id = u.id
+        LEFT JOIN life_sys s ON f.staff_id = s.id
+        WHERE f.id = #{feedbackId}
+    </select>
+
+</mapper>
+

+ 70 - 0
alien-entity/src/main/resources/mapper/LifeImgMapper.xml

@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.LifeImgMapper">
+
+    <!-- 通用结果映射 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.LifeImg">
+        <id column="id" property="id" />
+        <result column="feedback_id" property="feedbackId" />
+        <result column="file_type" property="fileType" />
+        <result column="img_url" property="imgUrl" />
+        <result column="thumbnail_url" property="thumbnailUrl" />
+        <result column="upload_time" property="uploadTime" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
+        <result column="created_user_id" property="createdUserId" />
+        <result column="updated_user_id" property="updatedUserId" />
+    </resultMap>
+
+    <!-- 基础字段 -->
+    <sql id="Base_Column_List">
+        id, feedback_id, file_type, img_url, thumbnail_url,
+        upload_time, create_time, update_time, created_user_id, updated_user_id
+    </sql>
+
+    <!-- 根据反馈ID查询图片列表 -->
+    <select id="selectByFeedbackId" resultMap="BaseResultMap">
+        SELECT 
+            <include refid="Base_Column_List" />
+        FROM life_img
+        WHERE feedback_id = #{feedbackId}
+        ORDER BY id ASC
+    </select>
+
+    <!-- 批量插入图片 -->
+    <insert id="batchInsert" parameterType="java.util.List">
+        INSERT INTO life_img (
+            feedback_id, file_type, img_url, thumbnail_url,
+            upload_time, create_time, created_user_id
+        ) VALUES
+        <foreach collection="list" item="item" separator=",">
+            (
+                #{item.feedbackId},
+                #{item.fileType},
+                #{item.imgUrl},
+                #{item.thumbnailUrl},
+                NOW(),
+                NOW(),
+                #{item.createdUserId}
+            )
+        </foreach>
+    </insert>
+
+    <!-- 根据反馈ID删除图片 -->
+    <delete id="deleteByFeedbackId">
+        DELETE FROM life_img 
+        WHERE feedback_id = #{feedbackId}
+    </delete>
+
+    <!-- 查询反馈的图片URL列表 -->
+    <select id="selectImgUrlsByFeedbackId" resultType="java.lang.String">
+        SELECT img_url
+        FROM life_img
+        WHERE feedback_id = #{feedbackId}
+        ORDER BY id ASC
+    </select>
+
+</mapper>
+

+ 2 - 2
alien-entity/src/main/resources/mapper/LifeUserDynamicsMapper.xml

@@ -15,14 +15,14 @@
         from life_user_dynamics
         where delete_flag = 0 and draft = 0 order by created_time desc
         )
-        select dynamice.*, user.nick_name userName, user.head_img userImage, info.id storeUserId, user.id storeOrUserId, 0 isExpert
+        select dynamice.*, user.nick_name userName, user.head_img userImage, info.id storeUserId, user.id storeOrUserId, 0 isExpert, info.store_name
         from dynamice
         left join store_user user on dynamice.phone = user.phone and user.delete_flag = 0
         left join store_info info on info.id = user.store_id and info.delete_flag = 0
         left join store_img img on img.store_id = user.store_id and img.img_type = '10' and img.delete_flag = 0
         where dynamice.userType = 'store'
         union
-        select dynamice.*, user.user_name userName, user.user_image userImage, user.id storeUserId, user.id storeOrUserId, IF(lue.expert_code IS NOT NULL , 1, 0) AS isExpert
+        select dynamice.*, user.user_name userName, user.user_image userImage, user.id storeUserId, user.id storeOrUserId, IF(lue.expert_code IS NOT NULL , 1, 0) AS isExpert, '' store_name
         from dynamice
         join life_user user on dynamice.phone = user.user_phone and user.delete_flag = 0
         left join life_user_expert  lue on lue.user_id = user.id and lue.delete_flag = 0

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

+ 131 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeFeedbackController.java

@@ -0,0 +1,131 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
+import io.swagger.annotations.ApiOperation;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.*;
+import shop.alien.entity.store.vo.LifeFeedbackDetailVo;
+import shop.alien.entity.store.vo.LifeFeedbackListVo;
+import shop.alien.entity.store.vo.LifeFeedbackVo;
+import shop.alien.store.service.LifeFeedbackService;
+
+/**
+ * 意见反馈 Controller
+ */
+@Api(tags = {"意见反馈模块"})
+@Slf4j
+@CrossOrigin
+@RestController
+@RequestMapping("/feedback")
+@RequiredArgsConstructor
+public class LifeFeedbackController {
+
+    private final LifeFeedbackService lifeFeedbackService;
+
+    @ApiOperation(value = "提交反馈", httpMethod = "POST")
+    @PostMapping("/submit")
+    public R<String> submitFeedback(@RequestBody LifeFeedbackDto dto) {
+        log.info("LifeFeedbackController.submitFeedback, dto={}", dto);
+        return lifeFeedbackService.submitFeedback(dto);
+    }
+
+    @ApiOperation(value = "平台回复反馈", httpMethod = "POST")
+    @PostMapping("/reply")
+    public R<String> replyFeedback(@RequestBody FeedbackReplyDto dto) {
+        log.info("LifeFeedbackController.replyFeedback, dto={}", dto);
+        return lifeFeedbackService.replyFeedback(dto);
+    }
+
+    @ApiOperation(value = "查询用户历史反馈列表", httpMethod = "GET")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "feedbackSource", value = "反馈来源:1-用户端,2-商家端", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "page", value = "页码", dataType = "int", paramType = "query", required = true),
+            @ApiImplicitParam(name = "size", value = "每页数量", dataType = "int", paramType = "query", required = true)
+    })
+    @GetMapping("/list")
+    public R<IPage<LifeFeedbackVo>> getFeedbackList(
+            @RequestParam("userId") Integer userId,
+            @RequestParam("feedbackSource") Integer feedbackSource,
+            @RequestParam(value = "page", defaultValue = "1") int page,
+            @RequestParam(value = "size", defaultValue = "10") int size) {
+        log.info("LifeFeedbackController.getFeedbackList, userId={}, feedbackSource={}, page={}, size={}", 
+                userId, feedbackSource, page, size);
+        IPage<LifeFeedbackVo> result = lifeFeedbackService.getFeedbackList(userId, feedbackSource, page, size);
+        return R.data(result);
+    }
+
+    @ApiOperation(value = "查询反馈详情", httpMethod = "GET")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "feedbackId", value = "反馈ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/detail")
+    public R<LifeFeedbackVo> getFeedbackDetail(@RequestParam("feedbackId") Integer feedbackId) {
+        log.info("LifeFeedbackController.getFeedbackDetail, feedbackId={}", feedbackId);
+        return lifeFeedbackService.getFeedbackDetail(feedbackId);
+    }
+
+    @ApiOperation(value = "更新反馈处理状态", httpMethod = "POST")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "feedbackId", value = "反馈ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "handleStatus", value = "处理状态:0-待处理,1-处理中,2-已完成", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "staffId", value = "跟进工作人员ID", dataType = "Integer", paramType = "query", required = false)
+    })
+    @PostMapping("/updateStatus")
+    public R<String> updateHandleStatus(
+            @RequestParam("feedbackId") Integer feedbackId,
+            @RequestParam("handleStatus") Integer handleStatus,
+            @RequestParam(value = "staffId", required = false) Integer staffId) {
+        log.info("LifeFeedbackController.updateHandleStatus, feedbackId={}, handleStatus={}, staffId={}", 
+                feedbackId, handleStatus, staffId);
+        return lifeFeedbackService.updateHandleStatus(feedbackId, handleStatus, staffId);
+    }
+
+    // ==================== 中台接口 ====================
+
+    @ApiOperation(value = "中台-查询意见反馈列表", httpMethod = "GET")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "feedbackType", value = "反馈类型:0-优化建议,1-问题", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "handleStatus", value = "处理状态:0-处理中,1-已解决,2-未分配,3-无需解决", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "feedbackSource", value = "反馈来源:0-用户端,1-商家端", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "feedbackWay", value = "反馈方式:0-用户反馈,1-AI识别", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "page", value = "页码", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "size", value = "每页数量", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/platform/list")
+    public R<IPage<LifeFeedbackListVo>> getWebFeedbackList(LifeFeedbackQueryDto queryDto) {
+        log.info("LifeFeedbackController.getWebFeedbackList, queryDto={}", queryDto);
+        return lifeFeedbackService.getWebFeedbackList(queryDto);
+    }
+
+    @ApiOperation(value = "中台-查询反馈详情", httpMethod = "GET")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "feedbackId", value = "反馈ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/platform/detail")
+    public R<LifeFeedbackDetailVo> getWebFeedbackDetail(@RequestParam("feedbackId") Integer feedbackId) {
+        log.info("LifeFeedbackController.getWebFeedbackDetail, feedbackId={}", feedbackId);
+        return lifeFeedbackService.getWebFeedbackDetail(feedbackId);
+    }
+
+    @ApiOperation(value = "中台-回复用户", httpMethod = "POST")
+    @PostMapping("/platform/reply")
+    public R<String> webReplyUser(@RequestBody LifeFeedbackReplyWebDto replyDto) {
+        log.info("LifeFeedbackController.webReplyUser, replyDto={}", replyDto);
+        return lifeFeedbackService.webReplyUser(replyDto);
+    }
+
+    @ApiOperation(value = "中台-更新处理状态", httpMethod = "POST")
+    @PostMapping("/platform/updateStatus")
+    public R<String> updateWebFeedbackStatus(@RequestBody LifeFeedbackStatusDto statusDto) {
+        log.info("LifeFeedbackController.updateWebFeedbackStatus, statusDto={}", statusDto);
+        return lifeFeedbackService.updateWebFeedbackStatus(statusDto);
+    }
+}
+

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

+ 12 - 6
alien-store/src/main/java/shop/alien/store/controller/ProtocolManagementController.java

@@ -11,6 +11,9 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.ProtocolManagement;
 import shop.alien.store.service.ProtocolManagementService;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
 /**
  * 协议管理表 前端控制器
  *
@@ -58,18 +61,21 @@ public class ProtocolManagementController {
             @ApiImplicitParam(name = "id", value = "协议ID", dataType = "Integer", paramType = "query", required = true)
     })
     @GetMapping("/getById")
-    public R<ProtocolManagement> getById(@RequestParam("id") Integer id) {
+    public void getById(@RequestParam("id") Integer id, HttpServletResponse response) throws IOException {
         log.info("ProtocolManagementController.getById?id={}", id);
-        
+
         try {
-            ProtocolManagement protocol = protocolManagementService.getProtocolById(id);
-            return R.data(protocol);
+            protocolManagementService.getProtocolById(id, response);
         } catch (IllegalArgumentException e) {
             log.error("查询协议详情失败", e);
-            return R.fail(e.getMessage());
+            if (!response.isCommitted()) {
+                response.sendError(HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
+            }
         } catch (Exception e) {
             log.error("查询协议详情失败", e);
-            return R.fail("查询失败: " + e.getMessage());
+            if (!response.isCommitted()) {
+                response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "查询失败: " + 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");

+ 25 - 0
alien-store/src/main/java/shop/alien/store/controller/UserViolationController.java

@@ -81,6 +81,31 @@ public class UserViolationController {
         return R.data(lifeUserViolationService.getViolationPage(pageNum, pageSize, nickname, phone, processingStatus));
     }
 
+    @ApiOperation("举报分页(带时间筛选)")
+    @ApiOperationSupport(order = 9)
+    @GetMapping("/allPage")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "int", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "pageSize", value = "每页条数", dataType = "int", paramType = "query", defaultValue = "10"),
+            @ApiImplicitParam(name = "nickname", value = "昵称", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "phone", value = "手机号", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "processingStatus", value = "处理状态", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "startTime", value = "开始时间", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "endTime", value = "结束时间", dataType = "String", paramType = "query")
+    })
+    public R<IPage<LifeUserViolationDto>> getAllViolationPage(
+            @RequestParam(defaultValue = "1") int pageNum,
+            @RequestParam(defaultValue = "10") int pageSize,
+            @RequestParam(required = false) String nickname,
+            @RequestParam(required = false) String phone,
+            @RequestParam(required = false) String processingStatus,
+            @RequestParam(required = false) String startTime,
+            @RequestParam(required = false) String endTime) {
+        log.info("UserViolationController.getAllViolationPage?pageNum={},pageSize={},nickName={},phone={},processingStatus={},startTime={},endTime={}", 
+                pageNum, pageSize, nickname, phone, processingStatus, startTime, endTime);
+        return R.data(lifeUserViolationService.getAllViolationPage(pageNum, pageSize, nickname, phone, processingStatus, startTime, endTime));
+    }
+
     @ApiOperation(value = "举报审核")
     @ApiOperationSupport(order = 5)
     @GetMapping("/approve")

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

+ 87 - 0
alien-store/src/main/java/shop/alien/store/service/LifeFeedbackService.java

@@ -0,0 +1,87 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeFeedback;
+import shop.alien.entity.store.dto.*;
+import shop.alien.entity.store.vo.LifeFeedbackDetailVo;
+import shop.alien.entity.store.vo.LifeFeedbackListVo;
+import shop.alien.entity.store.vo.LifeFeedbackVo;
+
+/**
+ * 意见反馈 Service
+ */
+public interface LifeFeedbackService extends IService<LifeFeedback> {
+
+    /**
+     * 提交反馈
+     * @param dto 反馈信息
+     * @return 反馈结果
+     */
+    R<String> submitFeedback(LifeFeedbackDto dto);
+
+    /**
+     * 平台回复反馈
+     * @param dto 回复信息
+     * @return 回复结果
+     */
+    R<String> replyFeedback(FeedbackReplyDto dto);
+
+    /**
+     * 查询用户历史反馈列表
+     * @param userId 用户ID
+     * @param feedbackSource 反馈来源
+     * @param page 页码
+     * @param size 每页数量
+     * @return 反馈列表
+     */
+    IPage<LifeFeedbackVo> getFeedbackList(Integer userId, Integer feedbackSource, int page, int size);
+
+    /**
+     * 查询反馈详情
+     * @param feedbackId 反馈ID
+     * @return 反馈详情
+     */
+    R<LifeFeedbackVo> getFeedbackDetail(Integer feedbackId);
+
+    /**
+     * 更新反馈处理状态
+     * @param feedbackId 反馈ID
+     * @param handleStatus 处理状态
+     * @param staffId 跟进人员ID
+     * @return 更新结果
+     */
+    R<String> updateHandleStatus(Integer feedbackId, Integer handleStatus, Integer staffId);
+
+    // ==================== 中台接口 ====================
+
+    /**
+     * 中台-查询意见反馈列表
+     * @param queryDto 查询条件
+     * @return 反馈列表
+     */
+    R<IPage<LifeFeedbackListVo>> getWebFeedbackList(LifeFeedbackQueryDto queryDto);
+
+    /**
+     * 中台-查询反馈详情
+     * @param feedbackId 反馈ID
+     * @return 反馈详情
+     */
+    R<LifeFeedbackDetailVo> getWebFeedbackDetail(Integer feedbackId);
+
+    /**
+     * 中台-回复用户
+     * @param replyDto 回复信息
+     * @return 回复结果
+     */
+    R<String> webReplyUser(LifeFeedbackReplyWebDto replyDto);
+
+    /**
+     * 中台-更新反馈处理状态
+     * @param statusDto 状态信息
+     * @return 更新结果
+     */
+    R<String> updateWebFeedbackStatus(LifeFeedbackStatusDto statusDto);
+}
+

+ 41 - 0
alien-store/src/main/java/shop/alien/store/service/LifeImgService.java

@@ -0,0 +1,41 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.LifeImg;
+
+import java.util.List;
+
+/**
+ * 反馈图片 Service
+ */
+public interface LifeImgService extends IService<LifeImg> {
+
+    /**
+     * 根据反馈ID查询图片列表
+     * @param feedbackId 反馈ID
+     * @return 图片列表
+     */
+    List<LifeImg> getByFeedbackId(Integer feedbackId);
+
+    /**
+     * 批量保存图片
+     * @param imgList 图片列表
+     * @return 是否成功
+     */
+    boolean batchSave(List<LifeImg> imgList);
+
+    /**
+     * 根据反馈ID删除图片
+     * @param feedbackId 反馈ID
+     * @return 是否成功
+     */
+    boolean removeByFeedbackId(Integer feedbackId);
+
+    /**
+     * 查询反馈的图片URL列表
+     * @param feedbackId 反馈ID
+     * @return 图片URL列表
+     */
+    List<String> getImgUrlsByFeedbackId(Integer feedbackId);
+}
+

+ 2 - 0
alien-store/src/main/java/shop/alien/store/service/LifeUserViolationService.java

@@ -27,6 +27,8 @@ public interface LifeUserViolationService extends IService<LifeUserViolation> {
 
     IPage<LifeUserViolationDto> getViolationPage(int page, int size, String nickName, String phone, String processingStatus);
 
+    IPage<LifeUserViolationDto> getAllViolationPage(int page, int size, String nickName, String phone, String processingStatus, String startTime, String endTime);
+
     void approve(int id, String processingStatus, String reportResult);
 
     LifeUserViolationDto byId(Integer id);

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

+ 2 - 2
alien-store/src/main/java/shop/alien/store/service/ProtocolManagementService.java

@@ -29,9 +29,9 @@ public interface ProtocolManagementService extends IService<ProtocolManagement>
      * 根据ID查询协议详情
      *
      * @param id 协议ID
-     * @return ProtocolManagement
+     * @param response 响应流
      */
-    ProtocolManagement getProtocolById(Integer id);
+    void getProtocolById(Integer id, javax.servlet.http.HttpServletResponse response);
 
     /**
      * 新增协议

+ 374 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeFeedbackServiceImpl.java

@@ -0,0 +1,374 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+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.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.LifeFeedback;
+import shop.alien.entity.store.LifeImg;
+import shop.alien.entity.store.LifeLog;
+import shop.alien.entity.store.dto.*;
+import shop.alien.entity.store.vo.*;
+import shop.alien.mapper.LifeFeedbackMapper;
+import shop.alien.mapper.LifeLogMapper;
+import shop.alien.store.service.LifeFeedbackService;
+import shop.alien.store.service.LifeImgService;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 意见反馈 Service实现类
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, LifeFeedback> implements LifeFeedbackService {
+
+    private final LifeFeedbackMapper lifeFeedbackMapper;
+    private final LifeImgService lifeImgService;
+    private final LifeLogMapper lifeLogMapper;
+
+    @Override
+    public R<String> submitFeedback(LifeFeedbackDto dto) {
+        try {
+            // 1. 参数校验
+            if (dto.getUserId() == null) {
+                return R.fail("用户ID不能为空");
+            }
+            if (dto.getFeedbackSource() == null) {
+                return R.fail("反馈来源不能为空");
+            }
+            if (dto.getFeedbackType() == null) {
+                return R.fail("反馈类型不能为空");
+            }
+            if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
+                return R.fail("反馈内容不能为空");
+            }
+
+            // 2. 创建反馈记录(使用MyBatis Plus的save方法)
+            LifeFeedback feedback = new LifeFeedback();
+            BeanUtils.copyProperties(dto, feedback);
+            feedback.setFeedbackTime(new Date());
+            feedback.setHandleStatus(0); // 处理中
+            feedback.setCreateTime(new Date());
+
+            boolean saveResult = this.save(feedback);
+            if (!saveResult) {
+                return R.fail("提交反馈失败");
+            }
+
+            // 3. 保存附件图片(使用批量插入)
+            if (!CollectionUtils.isEmpty(dto.getImgUrlList())) {
+                List<LifeImg> imgList = new java.util.ArrayList<>();
+                for (String imgUrl : dto.getImgUrlList()) {
+                    LifeImg img = new LifeImg();
+                    img.setFeedbackId(feedback.getId());
+                    img.setImgUrl(imgUrl);
+                    img.setFileType(1); // 默认图片
+                    img.setUploadTime(new Date());
+                    img.setCreateTime(new Date());
+                    img.setCreatedUserId(dto.getUserId());
+                    imgList.add(img);
+                }
+                lifeImgService.batchSave(imgList);
+            }
+
+            // 4. 记录日志 - 创建反馈工单
+            saveFeedbackLog(feedback.getId(), 2, "商家主动反馈/AI识别");
+
+            return R.success("提交成功");
+        } catch (Exception e) {
+            log.error("提交反馈失败", e);
+            return R.fail("提交反馈失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> replyFeedback(FeedbackReplyDto dto) {
+        try {
+            // 1. 参数校验
+            if (dto.getFeedbackId() == null) {
+                return R.fail("反馈ID不能为空");
+            }
+            if (dto.getStaffId() == null) {
+                return R.fail("工作人员ID不能为空");
+            }
+            if (dto.getContent() == null || dto.getContent().trim().isEmpty()) {
+                return R.fail("回复内容不能为空");
+            }
+
+            // 2. 查询原始反馈
+            LifeFeedback originalFeedback = lifeFeedbackMapper.selectById(dto.getFeedbackId());
+            if (originalFeedback == null) {
+                return R.fail("反馈记录不存在");
+            }
+
+            // 3. 创建平台回复记录(使用MyBatis Plus的save方法)
+            LifeFeedback reply = new LifeFeedback();
+            reply.setUserId(originalFeedback.getUserId());
+            reply.setFeedbackSource(originalFeedback.getFeedbackSource());
+            reply.setFeedbackWay(1); // 平台回复(AI识别方式)
+            reply.setFeedbackType(originalFeedback.getFeedbackType());
+            reply.setContent(dto.getContent());
+            reply.setFeedbackTime(new Date());
+            reply.setStaffId(dto.getStaffId());
+            reply.setHandleStatus(1); // 已解决
+            reply.setCreateTime(new Date());
+
+            boolean saveResult = this.save(reply);
+            if (!saveResult) {
+                return R.fail("回复失败");
+            }
+
+            // 4. 更新原始反馈的处理状态和跟进人员(使用MyBatis Plus的updateById方法)
+            LifeFeedback updateFeedback = new LifeFeedback();
+            updateFeedback.setId(dto.getFeedbackId());
+            updateFeedback.setHandleStatus(0); // 处理中
+            updateFeedback.setStaffId(dto.getStaffId());
+            updateFeedback.setUpdateTime(new Date());
+            this.updateById(updateFeedback);
+
+            // 5. 记录日志
+            saveLog("平台回复反馈,原始反馈ID:" + dto.getFeedbackId() + ",回复ID:" + reply.getId());
+
+            return R.success("回复成功");
+        } catch (Exception e) {
+            log.error("回复反馈失败", e);
+            return R.fail("回复反馈失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public IPage<LifeFeedbackVo> getFeedbackList(Integer userId, Integer feedbackSource, int page, int size) {
+        try {
+            // 使用自定义SQL查询(已包含工作人员名称)
+            Page<LifeFeedbackVo> pageParam = new Page<>(page, size);
+            IPage<LifeFeedbackVo> voPage = lifeFeedbackMapper.selectFeedbackListWithStaff(
+                    pageParam, userId, feedbackSource, 1, null
+            );
+
+            // 为每条记录查询附件图片
+            voPage.getRecords().forEach(vo -> {
+                List<String> imgUrls = lifeImgService.getImgUrlsByFeedbackId(vo.getId());
+                vo.setImgUrlList(imgUrls);
+            });
+
+            return voPage;
+        } catch (Exception e) {
+            log.error("查询反馈列表失败", e);
+            return new Page<>(page, size);
+        }
+    }
+
+    @Override
+    public R<LifeFeedbackVo> getFeedbackDetail(Integer feedbackId) {
+        try {
+            // 1. 使用自定义SQL查询反馈详情(已包含工作人员名称)
+            LifeFeedbackVo vo = lifeFeedbackMapper.selectFeedbackDetail(feedbackId);
+            if (vo == null) {
+                return R.fail("反馈记录不存在");
+            }
+
+            // 2. 查询附件图片
+            List<String> imgUrls = lifeImgService.getImgUrlsByFeedbackId(feedbackId);
+            vo.setImgUrlList(imgUrls);
+
+            // 3. 查询平台回复列表(如果是主动反馈)
+            if (vo.getFeedbackWay() == 1) {
+                List<LifeFeedbackVo> replyList = lifeFeedbackMapper.selectPlatformReplies(
+                        vo.getUserId(), vo.getFeedbackSource(), vo.getFeedbackTime()
+                );
+                vo.setPlatformReplies(replyList);
+            }
+
+            return R.data(vo);
+        } catch (Exception e) {
+            log.error("查询反馈详情失败", e);
+            return R.fail("查询反馈详情失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<String> updateHandleStatus(Integer feedbackId, Integer handleStatus, Integer staffId) {
+        try {
+            // 使用MyBatis Plus的updateById方法
+            LifeFeedback feedback = new LifeFeedback();
+            feedback.setId(feedbackId);
+            feedback.setHandleStatus(handleStatus);
+            feedback.setStaffId(staffId);
+            feedback.setUpdateTime(new Date());
+
+            boolean result = this.updateById(feedback);
+            if (result) {
+                saveLog("更新反馈处理状态,反馈ID:" + feedbackId + ",状态:" + handleStatus);
+                return R.success("更新成功");
+            }
+            return R.fail("更新失败");
+        } catch (Exception e) {
+            log.error("更新反馈处理状态失败", e);
+            return R.fail("更新失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 保存操作日志
+     */
+    private void saveLog(String context) {
+        try {
+            LifeLog log = new LifeLog();
+            log.setContext(context);
+            log.setCreatedTime(new Date());
+            lifeLogMapper.insert(log);
+        } catch (Exception e) {
+            log.error("保存日志失败", e);
+        }
+    }
+
+    // ==================== 中台接口实现 ====================
+
+    @Override
+    public R<IPage<LifeFeedbackListVo>> getWebFeedbackList(LifeFeedbackQueryDto queryDto) {
+        try {
+            Page<LifeFeedbackListVo> pageParam = new Page<>(queryDto.getPage(), queryDto.getSize());
+            IPage<LifeFeedbackListVo> result = lifeFeedbackMapper.selectWebFeedbackList(
+                    pageParam,
+                    queryDto.getFeedbackType(),
+                    queryDto.getHandleStatus(),
+                    queryDto.getFeedbackSource(),
+                    queryDto.getFeedbackWay()
+            );
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("中台-查询意见反馈列表失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    public R<LifeFeedbackDetailVo> getWebFeedbackDetail(Integer feedbackId) {
+        try {
+            if (feedbackId == null) {
+                return R.fail("反馈ID不能为空");
+            }
+
+            // 1. 查询反馈详情
+            LifeFeedbackDetailVo detail = lifeFeedbackMapper.selectWebFeedbackDetail(feedbackId);
+            if (detail == null) {
+                return R.fail("反馈记录不存在");
+            }
+
+            // 2. 查询附件列表(图片/视频)
+            List<FeedbackAttachmentVo> attachments = new ArrayList<>();
+            List<LifeImg> imgList = lifeImgService.getByFeedbackId(feedbackId);
+            if (!CollectionUtils.isEmpty(imgList)) {
+                for (LifeImg img : imgList) {
+                    FeedbackAttachmentVo attachment = new FeedbackAttachmentVo();
+                    attachment.setId(img.getId());
+                    attachment.setFileType(img.getFileType() != null ? img.getFileType() : 1);
+                    attachment.setFileUrl(img.getImgUrl());
+                    attachment.setThumbnailUrl(img.getThumbnailUrl());
+                    attachments.add(attachment);
+                }
+            }
+            detail.setAttachments(attachments);
+
+            return R.data(detail);
+        } catch (Exception e) {
+            log.error("中台-查询反馈详情失败", e);
+            return R.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> webReplyUser(LifeFeedbackReplyWebDto replyDto) {
+        try {
+            // 1. 参数校验
+            if (replyDto.getFeedbackId() == null) {
+                return R.fail("反馈ID不能为空");
+            }
+            if (replyDto.getContent() == null || replyDto.getContent().trim().isEmpty()) {
+                return R.fail("回复内容不能为空");
+            }
+
+            // 2. 查询原始反馈
+            LifeFeedback feedback = lifeFeedbackMapper.selectById(replyDto.getFeedbackId());
+            if (feedback == null) {
+                return R.fail("反馈记录不存在");
+            }
+
+            // 3. 记录回复日志(类型3-回复用户)
+            String logContent = replyDto.getContent();
+            if (replyDto.getUserReply() != null && !replyDto.getUserReply().trim().isEmpty()) {
+                logContent = replyDto.getContent() + "||用户回复:" + replyDto.getUserReply();
+            }
+            saveFeedbackLog(replyDto.getFeedbackId(), 3, logContent);
+
+            return R.success("回复成功");
+        } catch (Exception e) {
+            log.error("中台-回复用户失败", e);
+            return R.fail("回复失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> updateWebFeedbackStatus(LifeFeedbackStatusDto statusDto) {
+        try {
+            // 1. 参数校验
+            if (statusDto.getFeedbackId() == null) {
+                return R.fail("反馈ID不能为空");
+            }
+
+            // 2. 更新状态为已解决
+            LifeFeedback updateFeedback = new LifeFeedback();
+            updateFeedback.setId(statusDto.getFeedbackId());
+            updateFeedback.setHandleStatus(1); // 已解决
+            updateFeedback.setUpdateTime(new Date());
+
+            boolean result = this.updateById(updateFeedback);
+            if (!result) {
+                return R.fail("更新失败");
+            }
+
+            // 3. 记录日志(类型0-问题解决状态)
+            String logContent = "问题已解决";
+            saveFeedbackLog(statusDto.getFeedbackId(), 0, logContent);
+
+            return R.success("更新成功");
+        } catch (Exception e) {
+            log.error("中台-更新反馈状态失败", e);
+            return R.fail("更新失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 保存反馈操作日志
+     * @param feedbackId 反馈ID
+     * @param type 操作类型:0-问题解决状态,1-分配跟踪人员,2-创建反馈工单,3-回复用户
+     * @param context 日志内容
+     */
+    private void saveFeedbackLog(Integer feedbackId, Integer type, String context) {
+        try {
+            LifeLog lifeLog = new LifeLog();
+            lifeLog.setFeedbackId(feedbackId);
+            lifeLog.setType(String.valueOf(type));
+            lifeLog.setContext(context);
+            lifeLog.setCreatedTime(new Date());
+            lifeLog.setDeleteFlag(0);
+            lifeLogMapper.insert(lifeLog);
+        } catch (Exception e) {
+            log.error("保存反馈日志失败", e);
+        }
+    }
+}
+

+ 46 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeImgServiceImpl.java

@@ -0,0 +1,46 @@
+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.LifeImg;
+import shop.alien.mapper.LifeImgMapper;
+import shop.alien.store.service.LifeImgService;
+
+import java.util.List;
+
+/**
+ * 反馈图片 Service实现类
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LifeImgServiceImpl extends ServiceImpl<LifeImgMapper, LifeImg> implements LifeImgService {
+
+    private final LifeImgMapper lifeImgMapper;
+
+    @Override
+    public List<LifeImg> getByFeedbackId(Integer feedbackId) {
+        return lifeImgMapper.selectByFeedbackId(feedbackId);
+    }
+
+    @Override
+    public boolean batchSave(List<LifeImg> imgList) {
+        if (imgList == null || imgList.isEmpty()) {
+            return false;
+        }
+        return lifeImgMapper.batchInsert(imgList) > 0;
+    }
+
+    @Override
+    public boolean removeByFeedbackId(Integer feedbackId) {
+        return lifeImgMapper.deleteByFeedbackId(feedbackId) > 0;
+    }
+
+    @Override
+    public List<String> getImgUrlsByFeedbackId(Integer feedbackId) {
+        return lifeImgMapper.selectImgUrlsByFeedbackId(feedbackId);
+    }
+}
+

+ 34 - 8
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserViolationServiceImpl.java

@@ -331,19 +331,15 @@ public class LifeUserViolationServiceImpl extends ServiceImpl<LifeUserViolationM
         IPage<LifeUserViolationVo> pageRequest = new Page<>(page, size);
         QueryWrapper<LifeUserViolationVo> queryWrapper = new QueryWrapper<>();
 
-        // 基础查询条件
-        queryWrapper.eq("luv.delete_flag", 0)
-                .in("luv.report_context_type", Arrays.asList("1", "2", "3"));
-
         // 动态查询条件
-        queryWrapper.like(StringUtils.isNotEmpty(nickName), "ui.nick_name", nickName)
-                .like(StringUtils.isNotEmpty(phone), "ui.phone", phone);
+        queryWrapper.like(StringUtils.isNotEmpty(nickName), "nick_name", nickName)
+                .like(StringUtils.isNotEmpty(phone), "phone", phone);
 
         if (StringUtils.isNotEmpty(processingStatus)) {
-            queryWrapper.eq("luv.processing_status", processingStatus);
+            queryWrapper.eq("processing_status", processingStatus);
         }
 
-        queryWrapper.orderByDesc("luv.updated_time");
+        queryWrapper.orderByDesc("updated_time");
 
         IPage<LifeUserViolationVo> resultPage = lifeUserViolationMapper.getViolationPage(pageRequest, queryWrapper);
 
@@ -726,4 +722,34 @@ public class LifeUserViolationServiceImpl extends ServiceImpl<LifeUserViolationM
         if (count >= 3) return "B";
         return "A";
     }
+
+    @Override
+    public IPage<LifeUserViolationDto> getAllViolationPage(int page, int size, String nickName, String phone, String processingStatus, String startTime, String endTime) {
+        IPage<LifeUserViolationVo> pageRequest = new Page<>(page, size);
+        QueryWrapper<LifeUserViolationVo> queryWrapper = new QueryWrapper<>();
+
+        // 动态查询条件
+        queryWrapper.like(StringUtils.isNotEmpty(nickName), "nick_name", nickName)
+                .like(StringUtils.isNotEmpty(phone), "phone", phone);
+
+        if (StringUtils.isNotEmpty(processingStatus)) {
+            queryWrapper.eq("processing_status", processingStatus);
+        }
+        // 根据开始时间和结束时间过滤
+        if (StringUtils.isNotEmpty(startTime)) {
+            queryWrapper.ge("created_time", startTime);
+        }
+        if (StringUtils.isNotEmpty(endTime)) {
+            queryWrapper.le("created_time", endTime);
+        }
+        queryWrapper.orderByDesc("updated_time");
+
+        IPage<LifeUserViolationVo> resultPage = lifeUserViolationMapper.getViolationPage(pageRequest, queryWrapper);
+
+        return resultPage.convert(e -> {
+            LifeUserViolationDto dto = new LifeUserViolationDto();
+            BeanUtils.copyProperties(e, dto);
+            return dto;
+        });
+    }
 }

+ 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);
+            // 这里可以选择是否抛出异常,为了不影响主业务流程,这里只记录日志
+        }
+    }
+}
+

+ 79 - 8
alien-store/src/main/java/shop/alien/store/service/impl/ProtocolManagementServiceImpl.java

@@ -19,6 +19,14 @@ import shop.alien.util.common.RandomCreateUtil;
 import shop.alien.util.ali.AliOSSUtil;
 import shop.alien.util.file.FileUtil;
 
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.util.Arrays;
 import java.util.List;
 import java.util.Map;
@@ -67,20 +75,84 @@ public class ProtocolManagementServiceImpl extends ServiceImpl<ProtocolManagemen
     }
 
     @Override
-    public ProtocolManagement getProtocolById(Integer id) {
+    public void getProtocolById(Integer id, HttpServletResponse response) {
         log.info("协议管理-查询详情, id: {}", id);
-        
+
         if (id == null) {
             throw new IllegalArgumentException("协议ID不能为空");
         }
-        
+
         ProtocolManagement protocol = protocolManagementMapper.selectById(id);
-        
+
         if (protocol == null) {
             throw new IllegalArgumentException("协议不存在");
         }
-        
-        return protocol;
+
+        String protocolUrl = protocol.getProtocolFilePath();
+        if (StringUtils.isBlank(protocolUrl)) {
+            throw new IllegalArgumentException("协议文件路径不存在");
+        }
+
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+        try {
+            URL url = new URL(protocolUrl);
+            URLConnection connection = url.openConnection();
+            connection.setConnectTimeout(5000);
+            connection.setReadTimeout(10000);
+
+            // 推断文件名与Content-Type
+            String path = url.getPath();
+            int lastSlash = path.lastIndexOf('/');
+            String fileName = lastSlash >= 0 ? path.substring(lastSlash + 1) : "protocol.html";
+            if (fileName.contains("?")) {
+                fileName = fileName.substring(0, fileName.indexOf('?'));
+            }
+            if (StringUtils.isBlank(fileName)) {
+                fileName = "protocol.html";
+            }
+            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.name()).replaceAll("\\+", "%20");
+
+            String contentType = connection.getContentType();
+            if (StringUtils.isBlank(contentType)) {
+                // 默认按照html返回,兜底八进制流
+                contentType = protocolUrl.toLowerCase().endsWith(".html")
+                        ? "text/html;charset=utf-8"
+                        : "application/octet-stream";
+            }
+
+            response.reset();
+            response.setCharacterEncoding("utf-8");
+            response.setContentType(contentType);
+            response.setHeader("Content-Disposition", "attachment;filename*=utf-8''" + encodedFileName);
+
+            inputStream = connection.getInputStream();
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[8192];
+            int len;
+            while ((len = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, len);
+            }
+            outputStream.flush();
+        } catch (IOException e) {
+            log.error("协议管理-协议文件流输出失败, id: {}, url: {}", id, protocolUrl, e);
+            throw new RuntimeException("协议文件下载失败");
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭协议文件输入流失败", e);
+                }
+            }
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    log.error("关闭协议文件输出流失败", e);
+                }
+            }
+        }
     }
 
     @Override
@@ -175,10 +247,9 @@ public class ProtocolManagementServiceImpl extends ServiceImpl<ProtocolManagemen
         if (protocol == null) {
             throw new IllegalArgumentException("协议不存在");
         }
-        protocol.setDeleteFlag(1);
         
         // 逻辑删除
-        int result = protocolManagementMapper.updateById(protocol);
+        int result = protocolManagementMapper.deleteById(protocol);
         log.info("协议管理-删除, 结果: {}", result > 0 ? "成功" : "失败");
         
         return result > 0;

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

BIN
alien-store/src/main/resources/templates/holiday.xlsx