Sfoglia il codice sorgente

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

lyx 1 giorno fa
parent
commit
c59522a8c3
55 ha cambiato i file con 6056 aggiunte e 282 eliminazioni
  1. 2 2
      alien-entity/src/main/java/shop/alien/entity/second/vo/SecondUserViolationDetailVo.java
  2. 7 7
      alien-entity/src/main/java/shop/alien/entity/store/LifeFeedback.java
  3. 49 0
      alien-entity/src/main/java/shop/alien/entity/store/LifeFeedbackReply.java
  4. 8 8
      alien-entity/src/main/java/shop/alien/entity/store/LifeImg.java
  5. 2 2
      alien-entity/src/main/java/shop/alien/entity/store/LifeLog.java
  6. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/StoreImg.java
  7. 48 4
      alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java
  8. 5 5
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackDto.java
  9. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/dto/LifeFeedbackReplyWebDto.java
  10. 17 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreInfoDto.java
  11. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/UserReplyDto.java
  12. 43 0
      alien-entity/src/main/java/shop/alien/entity/store/excelVo/DictionaryLibraryExcelVo.java
  13. 45 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackReplyVo.java
  14. 23 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackTypeVo.java
  15. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackDetailVo.java
  16. 11 5
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackVo.java
  17. 12 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreInfoVo.java
  18. 81 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreLicenseInfoVo.java
  19. 23 0
      alien-entity/src/main/java/shop/alien/mapper/LifeFeedbackReplyMapper.java
  20. 7 0
      alien-entity/src/main/java/shop/alien/mapper/LifeImgMapper.java
  21. 1 1
      alien-entity/src/main/java/shop/alien/mapper/LifeUserViolationMapper.java
  22. 21 0
      alien-entity/src/main/java/shop/alien/mapper/StoreInfoMapper.java
  23. 39 62
      alien-entity/src/main/resources/mapper/LifeFeedbackMapper.xml
  24. 32 0
      alien-entity/src/main/resources/mapper/LifeFeedbackReplyMapper.xml
  25. 32 12
      alien-entity/src/main/resources/mapper/LifeImgMapper.xml
  26. 104 0
      alien-entity/src/main/resources/mapper/StoreInfoMapper.xml
  27. 3 1
      alien-entity/src/main/resources/mapper/second/SecondUserViolationMapper.xml
  28. 167 0
      alien-store/src/main/java/shop/alien/store/controller/DictOpinionFeedbackController.java
  29. 167 0
      alien-store/src/main/java/shop/alien/store/controller/DictStoreTagController.java
  30. 157 0
      alien-store/src/main/java/shop/alien/store/controller/DictionaryLibraryController.java
  31. 158 0
      alien-store/src/main/java/shop/alien/store/controller/FilterConditionController.java
  32. 9 26
      alien-store/src/main/java/shop/alien/store/controller/LifeFeedbackController.java
  33. 29 0
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  34. 55 0
      alien-store/src/main/java/shop/alien/store/service/DictOpinionFeedbackService.java
  35. 55 0
      alien-store/src/main/java/shop/alien/store/service/DictStoreTagService.java
  36. 57 0
      alien-store/src/main/java/shop/alien/store/service/DictionaryLibraryService.java
  37. 58 0
      alien-store/src/main/java/shop/alien/store/service/FilterConditionService.java
  38. 20 0
      alien-store/src/main/java/shop/alien/store/service/LifeFeedbackReplyService.java
  39. 6 13
      alien-store/src/main/java/shop/alien/store/service/LifeFeedbackService.java
  40. 7 1
      alien-store/src/main/java/shop/alien/store/service/LifeImgService.java
  41. 22 0
      alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java
  42. 826 0
      alien-store/src/main/java/shop/alien/store/service/impl/DictOpinionFeedbackServiceImpl.java
  43. 833 0
      alien-store/src/main/java/shop/alien/store/service/impl/DictStoreTagServiceImpl.java
  44. 955 0
      alien-store/src/main/java/shop/alien/store/service/impl/DictionaryLibraryServiceImpl.java
  45. 980 0
      alien-store/src/main/java/shop/alien/store/service/impl/FilterConditionServiceImpl.java
  46. 28 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeFeedbackReplyServiceImpl.java
  47. 432 101
      alien-store/src/main/java/shop/alien/store/service/impl/LifeFeedbackServiceImpl.java
  48. 11 1
      alien-store/src/main/java/shop/alien/store/service/impl/LifeImgServiceImpl.java
  49. 9 5
      alien-store/src/main/java/shop/alien/store/service/impl/LifeUserViolationServiceImpl.java
  50. 28 1
      alien-store/src/main/java/shop/alien/store/service/impl/ProtocolManagementServiceImpl.java
  51. 334 23
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  52. BIN
      alien-store/src/main/resources/templates/举报导入模版.xlsx
  53. BIN
      alien-store/src/main/resources/templates/意见反馈导入模版.xlsx
  54. BIN
      alien-store/src/main/resources/templates/筛选条件导入模版.xlsx
  55. BIN
      alien-store/src/main/resources/templates/门店标签导入模版.xlsx

+ 2 - 2
alien-entity/src/main/java/shop/alien/entity/second/vo/SecondUserViolationDetailVo.java

@@ -5,8 +5,8 @@ import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
-import shop.alien.entity.store.LifeComment;
 import shop.alien.entity.store.LifeUserDynamics;
+import shop.alien.entity.store.StoreComment;
 
 import java.util.List;
 import java.util.Map;
@@ -40,6 +40,6 @@ public class SecondUserViolationDetailVo extends SecondUserViolationVo{
 
     // 评论信息
     @ApiModelProperty(value = "评论信息")
-    LifeComment commentInfo;
+    StoreComment commentInfo;
 
 }

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

@@ -36,7 +36,7 @@ public class LifeFeedback implements Serializable {
     @TableField("feedback_way")
     private Integer feedbackWay;
 
-    @ApiModelProperty(value = "反馈类型:0-优化建议,1-问题")
+    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
     @TableField("feedback_type")
     private Integer feedbackType;
 
@@ -53,21 +53,21 @@ public class LifeFeedback implements Serializable {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date feedbackTime;
 
+    @ApiModelProperty(value = "工作人员ID(关联life_sys)")
+    @TableField("staff_id")
+    private Integer staffId;
+
     @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)
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "update_time", fill = FieldFill.UPDATE)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date updateTime;
 }

+ 49 - 0
alien-entity/src/main/java/shop/alien/entity/store/LifeFeedbackReply.java

@@ -0,0 +1,49 @@
+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_reply")
+@ApiModel(value = "LifeFeedbackReply对象", description = "反馈回复")
+public class LifeFeedbackReply implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "回复ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "关联的反馈ID")
+    @TableField("feedback_id")
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "回复类型:0-平台回复,1-用户回复")
+    @TableField("reply_type")
+    private Integer replyType;
+
+    @ApiModelProperty(value = "回复内容")
+    @TableField("reply_content")
+    private String replyContent;
+
+    @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;
+}
+

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

@@ -11,12 +11,12 @@ import java.io.Serializable;
 import java.util.Date;
 
 /**
- * 反馈图片表
+ * 反馈附件表(图片和视频)
  */
 @Data
 @JsonInclude
 @TableName("life_img")
-@ApiModel(value = "LifeImg对象", description = "反馈图片")
+@ApiModel(value = "LifeImg对象", description = "反馈附件(图片和视频)")
 public class LifeImg implements Serializable {
     private static final long serialVersionUID = 1L;
 
@@ -28,18 +28,18 @@ public class LifeImg implements Serializable {
     @TableField("feedback_id")
     private Integer feedbackId;
 
-    @ApiModelProperty(value = "文件类型:1-图片,2-视频")
-    @TableField("file_type")
-    private Integer fileType;
-
-    @ApiModelProperty(value = "图片/视频URL")
+    @ApiModelProperty(value = "文件URL(图片或视频)")
     @TableField("img_url")
     private String imgUrl;
 
-    @ApiModelProperty(value = "缩略图URL(视频的封面图)")
+    @ApiModelProperty(value = "缩略图URL(视频的封面图)")
     @TableField("thumbnail_url")
     private String thumbnailUrl;
 
+    @ApiModelProperty(value = "文件类型:1-图片,2-视频")
+    @TableField("file_type")
+    private Integer fileType;
+
     @ApiModelProperty(value = "上传时间")
     @TableField("upload_time")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

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

@@ -32,7 +32,7 @@ public class LifeLog implements Serializable {
     @TableField("context")
     private String context;
 
-    @ApiModelProperty(value = "操作类型:0-问题解决状态,1-分配跟踪人员,3-回复用户")
+    @ApiModelProperty(value = "操作类型:0-创建反馈工单,1-分配跟踪人员,2-回复用户,3-问题解决状态")
     @TableField("type")
     private String type;
 
@@ -51,7 +51,7 @@ public class LifeLog implements Serializable {
     private Integer createdUserId;
 
     @ApiModelProperty(value = "修改时间")
-    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @TableField(value = "updated_time", fill = FieldFill.UPDATE)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date updatedTime;
 

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/store/StoreImg.java

@@ -30,7 +30,7 @@ public class StoreImg extends Model<StoreImg> {
     @TableField("store_id")
     private Integer storeId;
 
-    @ApiModelProperty(value = "图片类型, 0:其他, 1:入口图, 2:相册, 3:菜品, 4:环境, 5:价目表, 6:推荐菜, 7:菜单, 8:用户评论, 9:商家申诉,10:商家头像,11:店铺轮播图,12:联名卡图片,13:动态折扣, 14:营业执照,15:合同照片,17:打卡广场小人图片 18: 二手商品发布图片 19:二手商品与用户举报图片,20头图单图模式,21头图多图模式 , 22续签合同, 23 二手商品记录图片类型, 24 食品经营许可证审核前类型 25.食品经营许可证审核后类型 26.运营活动活动标题图 27.运营活动活动详情图 28.运动设施 29.洗浴设施及服务 30.酒水 31.娱乐经营许可证 32.娱乐经营许可证审核前")
+    @ApiModelProperty(value = "图片类型, 0:其他, 1:入口图, 2:相册, 3:菜品, 4:环境, 5:价目表, 6:推荐菜, 7:菜单, 8:用户评论, 9:商家申诉,10:商家头像,11:店铺轮播图,12:联名卡图片,13:动态折扣, 14:营业执照,15:合同照片,17:打卡广场小人图片 18: 二手商品发布图片 19:二手商品与用户举报图片,20头图单图模式,21头图多图模式 , 22续签合同, 23 二手商品记录图片类型, 24 食品经营许可证审核前类型 25.食品经营许可证审核后类型 26.运营活动活动标题图 27.运营活动活动详情图 28.运动设施 29.洗浴设施及服务 30.酒水 31.娱乐经营许可证 32.娱乐经营许可证审核前, 33:身份证正面, 34:身份证反面")
     @TableField("img_type")
     private Integer imgType;
 

+ 48 - 4
alien-entity/src/main/java/shop/alien/entity/store/StoreInfo.java

@@ -221,12 +221,12 @@ public class StoreInfo {
     @TableField("food_licence_reason")
     private String foodLicenceReason;
 
-    @ApiModelProperty(value = "经营许可证到期时间")
+    @ApiModelProperty(value = "食品经营许可证到期时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField("food_licence_expiration_time")
     private Date  foodLicenceExpirationTime;
 
-    @ApiModelProperty(value = "变更经营许可证提交时间")
+    @ApiModelProperty(value = "变更食品经营许可证提交时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField("update_food_licence_time")
     private Date  updateFoodLicenceTime;
@@ -244,8 +244,6 @@ public class StoreInfo {
     @TableField("business_classify_name")
     private String businessClassifyName;
 
-
-
     @ApiModelProperty(value = "是否提供餐食")
     @TableField("meals_flag")
     private Integer  mealsFlag;
@@ -267,4 +265,50 @@ public class StoreInfo {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     @TableField("update_entertainment_licence_time")
     private Date updateEntertainmentLicenceTime;
+
+    @ApiModelProperty(value = "审核时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("review_date")
+    private Date reviewDate;
+
+    @ApiModelProperty(value = "续签合同到期时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("renew_contract_expiration_time")
+    private Date renewContractExpirationTime;
+
+    @ApiModelProperty(value = "营业执照状态 字典 foodLicenceStatus")
+    @TableField("business_license_status")
+    private Integer businessLicenseStatus;
+
+    @ApiModelProperty(value = "营业执照失败原因")
+    @TableField("business_license_reason")
+    private String businessLicenseReason;
+
+    @ApiModelProperty(value = "营业执照到期时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("business_license_expiration_time")
+    private Date businessLicenseExpirationTime;
+
+    @ApiModelProperty(value = "变更营业执照提交时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("update_business_license_time")
+    private Date updateBusinessLicenseTime;
+
+    @ApiModelProperty(value = "身份证状态 字典 foodLicenceStatus")
+    @TableField("id_card_status")
+    private Integer idCardStatus;
+
+    @ApiModelProperty(value = "身份证审核失败原因")
+    @TableField("id_card_reason")
+    private String idCardReason;
+
+    @ApiModelProperty(value = "身份证到期时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("id_card_expiration_time")
+    private Date idCardExpirationTime;
+
+    @ApiModelProperty(value = "变更身份证提交时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @TableField("update_id_card_time")
+    private Date updateIdCardTime;
 }

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

@@ -18,13 +18,13 @@ public class LifeFeedbackDto implements Serializable {
     @ApiModelProperty(value = "用户ID")
     private Integer userId;
 
-    @ApiModelProperty(value = "反馈来源:1-用户端,2-商家端")
+    @ApiModelProperty(value = "反馈来源:0-用户端,1-商家端")
     private Integer feedbackSource;
 
-    @ApiModelProperty(value = "反馈方式:1-主动反馈,2-平台回复")
+    @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
     private Integer feedbackWay;
 
-    @ApiModelProperty(value = "反馈类型:1-优化建议,2-问题")
+    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
     private Integer feedbackType;
 
     @ApiModelProperty(value = "反馈内容")
@@ -33,7 +33,7 @@ public class LifeFeedbackDto implements Serializable {
     @ApiModelProperty(value = "联系方式(手机号或邮箱)")
     private String contactWay;
 
-    @ApiModelProperty(value = "图片URL列表")
-    private List<String> imgUrlList;
+    @ApiModelProperty(value = "文件URL列表(图片和视频,系统会自动识别类型。视频会自动匹配封面图)")
+    private List<String> fileUrlList;
 }
 

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

@@ -23,7 +23,7 @@ public class LifeFeedbackReplyWebDto implements Serializable {
     @ApiModelProperty(value = "操作人员ID")
     private Integer operatorId;
 
-    @ApiModelProperty(value = "用户回复内容")
+    @ApiModelProperty(value = "用户回复内容")
     private String userReply;
 }
 

+ 17 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreInfoDto.java

@@ -213,8 +213,25 @@ public class StoreInfoDto {
     @DateTimeFormat(pattern = "yyyy-MM-dd")
     private Date entertainmentLicenceExpirationTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    @ApiModelProperty(value = "审核时间")
+    private Date reviewDate;
+
 //    @ApiModelProperty(value = "分类id(词典表 键为 business_classify)(多个ID用逗号拼接)")
 //    @JsonDeserialize(using = StringToListDeserializer.class)
 //    private List<String> businessClassify;
+    @ApiModelProperty(value = "营业执照图片URL")
+    private String businessLicenseUrl;
+
+    @ApiModelProperty(value = "营业执照状态 字典 foodLicenceStatus")
+    private Integer businessLicenseStatus;
 
+    @ApiModelProperty(value = "营业执照失败原因")
+    private String businessLicenseReason;
+
+    @ApiModelProperty(value = "营业执照到期时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private Date businessLicenseExpirationTime;
 }

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/UserReplyDto.java

@@ -0,0 +1,33 @@
+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 = "UserReplyDto对象", description = "用户回复DTO")
+public class UserReplyDto implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "用户ID", required = true)
+    private Integer userId;
+
+    @ApiModelProperty(value = "反馈来源:0-用户端,1-商家端", required = true)
+    private Integer feedbackSource;
+
+    @ApiModelProperty(value = "原始反馈ID(反馈详情页的反馈ID)", required = true)
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "回复内容", required = true)
+    private String content;
+
+    @ApiModelProperty(value = "文件URL列表(图片和视频,系统会自动识别类型。视频会自动匹配封面图)")
+    private List<String> fileUrlList;
+}
+

+ 43 - 0
alien-entity/src/main/java/shop/alien/entity/store/excelVo/DictionaryLibraryExcelVo.java

@@ -0,0 +1,43 @@
+package shop.alien.entity.store.excelVo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.excelVo.util.ExcelHeader;
+
+/**
+ * 经营版块Excel导入导出对象
+ *
+ * @author ssk
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "DictionaryLibraryExcelVo对象", description = "举报Excel导入导出对象")
+public class DictionaryLibraryExcelVo {
+
+    @ExcelHeader("序号")
+    @ApiModelProperty(value = "序号")
+    private String sort;
+
+    @ExcelHeader("一级分类名称")
+    @ApiModelProperty(value = "一级分类名称(必填,如果二级和三级为空,则创建一级分类)")
+    private String firstLevelName;
+
+    @ExcelHeader("二级分类名称")
+    @ApiModelProperty(value = "二级分类名称(可选,如果三级为空,则创建二级分类)")
+    private String secondLevelName;
+
+    @ExcelHeader("三级分类名称")
+    @ApiModelProperty(value = "三级分类名称(可选,如果填写则创建三级分类)")
+    private String thirdLevelName;
+
+    @ExcelHeader("四级分类名称")
+    @ApiModelProperty(value = "四级分类名称(可选,如果填写则创建四级分类)")
+    private String fourLevelName;
+
+    @ExcelHeader("状态")
+    @ApiModelProperty(value = "状态为显示隐藏,如不填默认显示")
+    private String hidden;
+}

+ 45 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackReplyVo.java

@@ -0,0 +1,45 @@
+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 = "FeedbackReplyVo对象", description = "反馈回复VO")
+public class FeedbackReplyVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "回复ID")
+    private Integer id;
+
+    @ApiModelProperty(value = "反馈ID")
+    private Integer feedbackId;
+
+    @ApiModelProperty(value = "回复类型:0-平台回复,1-用户回复")
+    private Integer replyType;
+
+    @ApiModelProperty(value = "回复类型名称")
+    private String replyTypeName;
+
+    @ApiModelProperty(value = "回复内容")
+    private String replyContent;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy/MM/dd HH:mm", timezone = "GMT+8")
+    private Date createTime;
+
+    @ApiModelProperty(value = "图片列表")
+    private List<String> imgUrlList;
+
+    @ApiModelProperty(value = "视频列表")
+    private List<String> videoUrlList;
+}
+

+ 23 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/FeedbackTypeVo.java

@@ -0,0 +1,23 @@
+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 = "FeedbackTypeVo对象", description = "反馈类型VO")
+public class FeedbackTypeVo implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "反馈类型值:0-bug反馈,1-优化反馈,2-新增功能反馈")
+    private Integer value;
+
+    @ApiModelProperty(value = "反馈类型名称")
+    private String label;
+}
+

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

@@ -71,5 +71,8 @@ public class LifeFeedbackDetailVo implements Serializable {
 
     @ApiModelProperty(value = "操作日志列表")
     private List<FeedbackLogVo> logs;
+
+    @ApiModelProperty(value = "回复列表(平台回复和用户回复)")
+    private List<FeedbackReplyVo> replies;
 }
 

+ 11 - 5
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeFeedbackVo.java

@@ -23,15 +23,18 @@ public class LifeFeedbackVo implements Serializable {
     @ApiModelProperty(value = "用户ID")
     private Integer userId;
 
-    @ApiModelProperty(value = "反馈来源:1-用户端,2-商家端")
+    @ApiModelProperty(value = "反馈来源:0-用户端,1-商家端")
     private Integer feedbackSource;
 
-    @ApiModelProperty(value = "反馈方式:1-主动反馈,2-平台回复")
+    @ApiModelProperty(value = "反馈方式:0-用户反馈,1-AI识别")
     private Integer feedbackWay;
 
-    @ApiModelProperty(value = "反馈类型:1-优化建议,2-问题")
+    @ApiModelProperty(value = "反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈")
     private Integer feedbackType;
 
+    @ApiModelProperty(value = "反馈类型名称")
+    private String feedbackTypeName;
+
     @ApiModelProperty(value = "反馈内容")
     private String content;
 
@@ -42,10 +45,10 @@ public class LifeFeedbackVo implements Serializable {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date feedbackTime;
 
-    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决,2-未分配,3-无需解决")
+    @ApiModelProperty(value = "处理状态:0-处理中,1-已解决")
     private Integer handleStatus;
 
-    @ApiModelProperty(value = "跟进工作人员ID")
+    @ApiModelProperty(value = "工作人员ID")
     private Integer staffId;
 
     @ApiModelProperty(value = "跟进工作人员姓名")
@@ -54,6 +57,9 @@ public class LifeFeedbackVo implements Serializable {
     @ApiModelProperty(value = "附件图片列表")
     private List<String> imgUrlList;
 
+    @ApiModelProperty(value = "附件视频列表")
+    private List<String> videoUrlList;
+
     @ApiModelProperty(value = "平台反馈建议列表")
     private List<LifeFeedbackVo> platformReplies;
 }

+ 12 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreInfoVo.java

@@ -1,6 +1,7 @@
 package shop.alien.entity.store.vo;
 
 import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
@@ -247,4 +248,15 @@ public class StoreInfoVo extends StoreInfo {
     private JSONObject foodLicence;
     @ApiModelProperty(value = "娱乐经营许可证")
     private JSONObject entertainmentLicence;
+
+    @ApiModelProperty(value = "审核时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date reviewDate;
+
+    @ApiModelProperty(value = "续签合同状态")
+    private Integer renewContractStatus;
+
+    @ApiModelProperty(value = "续签合同拒绝原因")
+    private String contractReason;
+
 }

+ 81 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreLicenseInfoVo.java

@@ -0,0 +1,81 @@
+package shop.alien.entity.store.vo;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+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.util.Date;
+
+/**
+ * 门店证照查询结果 VO
+ *
+ * 对应证照类型(营业执照 / 娱乐经营许可证 / 食品经营许可证)
+ * 及其当前状态、提交时间、到期时间等信息。
+ */
+@Data
+@ApiModel(value = "StoreLicenseInfoVo对象", description = "门店证照信息")
+@JsonInclude(JsonInclude.Include.ALWAYS)
+public class StoreLicenseInfoVo {
+
+    @ApiModelProperty("证照类型描述:营业执照 / 娱乐经营许可证 / 食品经营许可证")
+    private String imgDescription;
+
+    @ApiModelProperty("证照图片类型(14:营业执照;24/25:食品经营许可证;31/32:娱乐经营许可证)")
+    private Integer imgType;
+
+    @ApiModelProperty("门店ID")
+    private Integer id;
+
+    @ApiModelProperty("门店名称")
+    private String storeName;
+
+    @ApiModelProperty("门店联系电话")
+    private String storeTel;
+
+    @ApiModelProperty("当前证照状态")
+    private String states;
+
+    @ApiModelProperty("当前证照状态名字")
+    private String statesName;
+
+    @ApiModelProperty("证照图片地址")
+    private String imgUrl;
+
+    @ApiModelProperty("证照最近一次提交时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date submitDate;
+
+    @ApiModelProperty("证照到期时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date expirationTime;
+
+    @ApiModelProperty("用户")
+    private String name;
+
+    @ApiModelProperty("审核失败原因")
+    private String expirationReason;
+
+    @ApiModelProperty(value = "经营板块id(词典表 键为 business_section)")
+    private Integer businessSection;
+
+    @ApiModelProperty(value = "经营板块名称")
+    private String businessSectionName;
+
+    @ApiModelProperty(value = "经营种类ids")
+    private String businessTypes;
+
+    @ApiModelProperty(value = "经营种类名称s")
+    private String businessTypesName;
+
+    @ApiModelProperty(value = "分类ids")
+    private String businessClassify;
+
+    @ApiModelProperty(value = "分类名称s")
+    private String businessClassifyName;
+}
+
+
+

+ 23 - 0
alien-entity/src/main/java/shop/alien/mapper/LifeFeedbackReplyMapper.java

@@ -0,0 +1,23 @@
+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.LifeFeedbackReply;
+
+import java.util.List;
+
+/**
+ * 反馈回复 Mapper 接口
+ */
+@Mapper
+public interface LifeFeedbackReplyMapper extends BaseMapper<LifeFeedbackReply> {
+
+    /**
+     * 根据反馈ID查询回复列表
+     * @param feedbackId 反馈ID
+     * @return 回复列表
+     */
+    List<LifeFeedbackReply> selectByFeedbackId(@Param("feedbackId") Integer feedbackId);
+}
+

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

@@ -40,5 +40,12 @@ public interface LifeImgMapper extends BaseMapper<LifeImg> {
      * @return 图片URL列表
      */
     List<String> selectImgUrlsByFeedbackId(@Param("feedbackId") Integer feedbackId);
+
+    /**
+     * 查询反馈的视频URL列表
+     * @param feedbackId 反馈ID
+     * @return 视频URL列表
+     */
+    List<String> selectVideoUrlsByFeedbackId(@Param("feedbackId") Integer feedbackId);
 }
 

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

@@ -136,7 +136,7 @@ public interface LifeUserViolationMapper extends BaseMapper<LifeUserViolation> {
             " 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,  " +
+            " luv.id, lu.user_name nick_name, lu.user_phone phone, luv.report_context_type, sd.dict_detail violation_type_name,  " +
             " 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 " +

+ 21 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreInfoMapper.java

@@ -10,6 +10,7 @@ import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.vo.StoreInfoVo;
+import shop.alien.entity.store.vo.StoreLicenseInfoVo;
 import shop.alien.entity.store.vo.StoreMainInfoVo;
 
 import java.util.List;
@@ -184,4 +185,24 @@ public interface StoreInfoMapper extends BaseMapper<StoreInfo> {
             "and ROUND(ST_Distance_Sphere(ST_GeomFromText(CONCAT('POINT(', REPLACE(#{position}, ',', ' '), ')' )), ST_GeomFromText(CONCAT('POINT(', REPLACE(a.store_position, ',', ' '), ')' ))) / 1000, 2) <= 1 " +
             "order by distance3 asc limit 20")
     List<StoreInfoVo> getMoreRecommendedStores(@Param(Constants.WRAPPER) QueryWrapper<StoreInfoVo> queryWrapper, @Param("position") String position);
+
+    /**
+     * 门店证照查询(分页)
+     *
+     * @param page            分页对象
+     * @param storeName       门店名称(模糊)
+     * @param storeTel        门店电话(模糊)
+     * @param imgType         证照图片类型(14:营业执照;24/25:食品经营许可证;31/32:娱乐经营许可证)
+     * @param states          证照状态
+     * @param startSubmitDate 提交开始时间(yyyy-MM-dd HH:mm:ss)
+     * @param endSubmitDate   提交结束时间(yyyy-MM-dd HH:mm:ss)
+     * @return 证照分页结果
+     */
+    IPage<StoreLicenseInfoVo> getStoreLicensePage(IPage<StoreLicenseInfoVo> page,
+                                                  @Param("storeName") String storeName,
+                                                  @Param("storeTel") String storeTel,
+                                                  @Param("imgType") Integer imgType,
+                                                  @Param("states") String states,
+                                                  @Param("startSubmitDate") String startSubmitDate,
+                                                  @Param("endSubmitDate") String endSubmitDate);
 }

+ 39 - 62
alien-entity/src/main/resources/mapper/LifeFeedbackMapper.xml

@@ -56,10 +56,13 @@
         <if test="handleStatus != null">
             AND f.handle_status = #{handleStatus}
         </if>
+        <!-- 只查询原始反馈:feedback_way=0(用户反馈)或feedback_way=1(AI识别) -->
+        <!-- 回复记录已存储在life_feedback_reply表中,life_feedback表中只包含原始反馈 -->
+        AND (f.feedback_way = 0 OR f.feedback_way = 1)
         ORDER BY f.feedback_time DESC
     </select>
 
-    <!-- 查询反馈详情(带工作人员名称和图片) -->
+    <!-- 查询反馈详情(带工作人员名称) -->
     <select id="selectFeedbackDetail" resultType="shop.alien.entity.store.vo.LifeFeedbackVo">
         SELECT 
             f.id,
@@ -78,7 +81,7 @@
         WHERE f.id = #{feedbackId}
     </select>
 
-    <!-- 统计处理反馈数量 -->
+    <!-- 统计处理反馈数量 -->
     <select id="countPendingFeedback" resultType="java.lang.Integer">
         SELECT COUNT(1)
         FROM life_feedback
@@ -88,64 +91,40 @@
         </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 
+        SELECT
             f.id,
-            u.user_name AS nickName,
-            u.user_phone AS phone,
+            u.nick_name AS nickName,
+            u.phone AS phone,
             f.feedback_type AS feedbackType,
-            CASE f.feedback_type 
-                WHEN 0 THEN 'bug反馈' 
-                WHEN 1 THEN '优化反馈' 
-                WHEN 2 THEN '新增功能反馈' 
+            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识别' 
+            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 '商家端' 
+            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 '无需解决' 
+            CASE f.handle_status
+                WHEN 0 THEN '处理中'
+                WHEN 1 THEN '已解决'
             END AS handleStatusName
         FROM life_feedback f
-        LEFT JOIN life_user u ON f.user_id = u.id
+        LEFT JOIN store_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">
@@ -165,40 +144,38 @@
 
     <!-- 中台-查询反馈详情 -->
     <select id="selectWebFeedbackDetail" resultType="shop.alien.entity.store.vo.LifeFeedbackDetailVo">
-        SELECT 
+        SELECT
             f.id,
-            u.user_name AS nickName,
-            u.user_phone AS phone,
+            u.nick_name AS nickName,
+            u.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 '商家端' 
+            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识别' 
+            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 '新增功能反馈' 
+            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 '无需解决' 
+            CASE f.handle_status
+                WHEN 0 THEN '处理中'
+                WHEN 1 THEN '已解决'
             END AS handleStatusName
         FROM life_feedback f
-        LEFT JOIN life_user u ON f.user_id = u.id
+        LEFT JOIN store_user u ON f.user_id = u.id
         LEFT JOIN life_sys s ON f.staff_id = s.id
         WHERE f.id = #{feedbackId}
     </select>

+ 32 - 0
alien-entity/src/main/resources/mapper/LifeFeedbackReplyMapper.xml

@@ -0,0 +1,32 @@
+<?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.LifeFeedbackReplyMapper">
+
+    <!-- 通用结果映射 -->
+    <resultMap id="BaseResultMap" type="shop.alien.entity.store.LifeFeedbackReply">
+        <id column="id" property="id" />
+        <result column="feedback_id" property="feedbackId" />
+        <result column="reply_type" property="replyType" />
+        <result column="reply_content" property="replyContent" />
+        <result column="create_time" property="createTime" />
+        <result column="update_time" property="updateTime" />
+    </resultMap>
+
+    <!-- 基础字段 -->
+    <sql id="Base_Column_List">
+        id, feedback_id, reply_type, reply_content, create_time, update_time
+    </sql>
+
+    <!-- 根据反馈ID查询回复列表 -->
+    <select id="selectByFeedbackId" resultMap="BaseResultMap">
+        SELECT 
+            <include refid="Base_Column_List" />
+        FROM life_feedback_reply
+        WHERE feedback_id = #{feedbackId}
+        ORDER BY create_time ASC
+    </select>
+
+</mapper>
+

+ 32 - 12
alien-entity/src/main/resources/mapper/LifeImgMapper.xml

@@ -8,9 +8,9 @@
     <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="file_type" property="fileType" />
         <result column="upload_time" property="uploadTime" />
         <result column="create_time" property="createTime" />
         <result column="update_time" property="updateTime" />
@@ -20,8 +20,8 @@
 
     <!-- 基础字段 -->
     <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
+        id, feedback_id, img_url, thumbnail_url, file_type, upload_time,
+        create_time, update_time, created_user_id, updated_user_id
     </sql>
 
     <!-- 根据反馈ID查询图片列表 -->
@@ -30,31 +30,32 @@
             <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
+            feedback_id, img_url, thumbnail_url, file_type, upload_time,
+            create_time, update_time, created_user_id, updated_user_id
         ) VALUES
         <foreach collection="list" item="item" separator=",">
             (
                 #{item.feedbackId},
-                #{item.fileType},
                 #{item.imgUrl},
                 #{item.thumbnailUrl},
+                #{item.fileType},
+                #{item.uploadTime},
                 NOW(),
                 NOW(),
-                #{item.createdUserId}
+                #{item.createdUserId},
+                #{item.updatedUserId}
             )
         </foreach>
     </insert>
 
     <!-- 根据反馈ID删除图片 -->
     <delete id="deleteByFeedbackId">
-        DELETE FROM life_img 
+        DELETE FROM life_img
         WHERE feedback_id = #{feedbackId}
     </delete>
 
@@ -63,8 +64,27 @@
         SELECT img_url
         FROM life_img
         WHERE feedback_id = #{feedbackId}
-        ORDER BY id ASC
+        AND file_type = 1
+        ORDER BY upload_time ASC
     </select>
 
-</mapper>
+    <!-- 查询反馈的视频URL列表 -->
+    <select id="selectVideoUrlsByFeedbackId" resultType="java.lang.String">
+        SELECT img_url
+        FROM life_img
+        WHERE feedback_id = #{feedbackId}
+        AND file_type = 2
+        ORDER BY upload_time ASC
+    </select>
 
+    <!-- 查询反馈的视频信息(包含视频URL和缩略图URL) -->
+    <select id="selectVideoInfoByFeedbackId" resultMap="BaseResultMap">
+        SELECT
+            <include refid="Base_Column_List" />
+        FROM life_img
+        WHERE feedback_id = #{feedbackId}
+        AND file_type = 2
+        ORDER BY upload_time ASC
+    </select>
+
+</mapper>

+ 104 - 0
alien-entity/src/main/resources/mapper/StoreInfoMapper.xml

@@ -0,0 +1,104 @@
+<?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.StoreInfoMapper">
+
+    <!--
+        门店证照查询
+        证照类型与状态、提交时间、到期时间映射关系:
+          - img_type = 14: 营业执照
+          - img_type IN (31, 32): 娱乐经营许可证
+          - img_type IN (24, 25): 食品经营许可证
+    -->
+    <select id="getStoreLicensePage"
+            resultType="shop.alien.entity.store.vo.StoreLicenseInfoVo">
+        SELECT
+            CASE
+                WHEN si.img_type = 14 THEN '营业执照'
+                WHEN si.img_type IN (31, 32) THEN '娱乐经营许可证'
+                WHEN si.img_type IN (24, 25) THEN '食品经营许可证'
+                WHEN si.img_type IN (33) THEN '身份证正面'
+                WHEN si.img_type IN (34) THEN '身份证反面'
+                ELSE ''
+            END AS img_description,
+            si.img_type AS img_type,
+            s.id AS id,
+            s.store_name AS store_name,
+            s.store_tel AS store_tel,
+            su.name AS name,
+            si.img_url,
+            s.business_section_name,
+            s.business_classify_name,
+            s.business_types_name,
+            CASE
+                WHEN si.img_type = 14 THEN s.business_license_status
+                WHEN si.img_type IN (31, 32) THEN s.entertainment_licence_status
+                WHEN si.img_type IN (24, 25) THEN s.food_licence_status
+                WHEN si.img_type IN (33, 34) THEN s.id_card_status
+                ELSE ''
+            END AS states,
+            CASE
+                WHEN si.img_type = 14 THEN s.update_business_license_time
+                WHEN si.img_type IN (31, 32) THEN s.update_entertainment_licence_time
+                WHEN si.img_type IN (24, 25) THEN s.update_food_licence_time
+                WHEN si.img_type IN (33, 34) THEN s.update_id_card_time
+                ELSE NULL
+            END AS submit_date,
+            CASE
+                WHEN si.img_type = 14 THEN s.business_license_expiration_time
+                WHEN si.img_type IN (31, 32) THEN s.entertainment_licence_expiration_time
+                WHEN si.img_type IN (24, 25) THEN s.food_licence_expiration_time
+                WHEN si.img_type IN (33, 34) THEN s.id_card_expiration_time
+                ELSE NULL
+            END AS expiration_time,
+            CASE
+                WHEN si.img_type = 14 THEN s.business_license_reason
+                WHEN si.img_type IN (31, 32) THEN s.entertainment_licence_reason
+                WHEN si.img_type IN (24, 25) THEN s.food_licence_reason
+                WHEN si.img_type IN (33, 34) THEN s.id_card_reason
+                ELSE NULL
+            END AS expiration_reason
+        FROM store_info s
+                 LEFT JOIN store_img si
+                           ON s.id = si.store_id
+                               AND si.img_type IN (14, 24, 25, 31, 32, 33, 34)
+                               AND si.delete_flag = 0
+                 left join store_user su on s.id = su.store_id
+        WHERE s.delete_flag = 0
+        <if test="storeName != null and storeName != ''">
+            AND s.store_name LIKE CONCAT('%', #{storeName}, '%')
+        </if>
+        <if test="storeTel != null and storeTel != ''">
+            AND s.store_tel LIKE CONCAT('%', #{storeTel}, '%')
+        </if>
+        <if test="imgType != null">
+            AND si.img_type = #{imgType}
+        </if>
+        <if test="states != null and states != ''">
+            AND (
+                (si.img_type = 14 AND s.business_license_status = #{states})
+                OR (si.img_type IN (31, 32) AND s.entertainment_licence_status = #{states})
+                OR (si.img_type IN (24, 25) AND s.food_licence_status = #{states})
+            )
+        </if>
+        <if test="startSubmitDate != null and startSubmitDate != ''">
+            AND (
+                (si.img_type = 14 AND s.update_business_license_time &gt;= #{startSubmitDate})
+                OR (si.img_type IN (31, 32) AND s.update_entertainment_licence_time &gt;= #{startSubmitDate})
+                OR (si.img_type IN (24, 25) AND s.update_food_licence_time &gt;= #{startSubmitDate})
+            )
+        </if>
+        <if test="endSubmitDate != null and endSubmitDate != ''">
+            AND (
+                (si.img_type = 14 AND s.update_business_license_time &lt;= #{endSubmitDate})
+                OR (si.img_type IN (31, 32) AND s.update_entertainment_licence_time &lt;= #{endSubmitDate})
+                OR (si.img_type IN (24, 25) AND s.update_food_licence_time &lt;= #{endSubmitDate})
+            )
+        </if>
+        ORDER BY s.store_name ASC
+    </select>
+
+</mapper>
+
+

+ 3 - 1
alien-entity/src/main/resources/mapper/second/SecondUserViolationMapper.xml

@@ -113,7 +113,9 @@
             v.report_evidence_img,
             v.processing_time,
             v.report_result,
-            v.video_first_frame
+            v.video_first_frame,
+            v.comment_id,
+            v.dynamics_id
         from
             life_user_violation v
             left join life_user u on v.reported_user_id = u.id

+ 167 - 0
alien-store/src/main/java/shop/alien/store/controller/DictOpinionFeedbackController.java

@@ -0,0 +1,167 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.store.service.DictOpinionFeedbackService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * 意见反馈字典控制器
+ */
+@Api(tags = {"平台-意见反馈字典管理"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/dictOpinionFeedback")
+@RequiredArgsConstructor
+public class DictOpinionFeedbackController {
+
+    private final DictOpinionFeedbackService dictOpinionFeedbackService;
+
+    @ApiOperation("查询意见反馈字典(三级树形结构)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/queryDictOpinionFeedbackTree")
+    public R<List<StoreDictionary>> queryDictOpinionFeedbackTree() {
+        log.info("dictOpinionFeedback.queryDictOpinionFeedbackTree");
+        List<StoreDictionary> result = dictOpinionFeedbackService.queryDictOpinionFeedbackTree();
+        return R.data(result);
+    }
+
+    @ApiOperation("新增意见反馈字典(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/addDictOpinionFeedback")
+    public R<Boolean> addDictOpinionFeedback(@RequestBody StoreDictionary storeDictionary) {
+        log.info("dictOpinionFeedback.addDictOpinionFeedback:{}", storeDictionary);
+        try {
+            boolean result = dictOpinionFeedbackService.addDictOpinionFeedback(storeDictionary);
+            if (result) {
+                return R.success("新增成功");
+            } else {
+                return R.fail("新增失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("dictOpinionFeedback.addDictOpinionFeedback error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("dictOpinionFeedback.addDictOpinionFeedback error", e);
+            return R.fail("新增失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("修改意见反馈字典(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 3)
+    @PutMapping("/updateDictOpinionFeedback")
+    public R<Boolean> updateDictOpinionFeedback(@RequestBody StoreDictionary storeDictionary) {
+        log.info("dictOpinionFeedback.updateDictOpinionFeedback:{}", storeDictionary);
+        try {
+            boolean result = dictOpinionFeedbackService.updateDictOpinionFeedback(storeDictionary);
+            if (result) {
+                return R.success("修改成功");
+            } else {
+                return R.fail("修改失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("dictOpinionFeedback.updateDictOpinionFeedback error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("dictOpinionFeedback.updateDictOpinionFeedback error", e);
+            return R.fail("修改失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("批量导入意见反馈字典")
+    @ApiOperationSupport(order = 4)
+    @PostMapping("/import")
+    public R<String> importDictOpinionFeedback(@RequestParam("file") MultipartFile file) {
+        log.info("dictOpinionFeedback.importDictOpinionFeedback fileName={}",
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            return dictOpinionFeedbackService.importDictOpinionFeedback(file);
+        } catch (Exception e) {
+            log.error("dictOpinionFeedback.importDictOpinionFeedback error", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+//    @ApiOperation("下载意见反馈字典导入模板")
+//    @ApiOperationSupport(order = 5)
+//    @GetMapping("/downloadTemplate")
+//    public void downloadTemplate(HttpServletResponse response) throws IOException {
+//        log.info("dictOpinionFeedback.downloadTemplate");
+//        dictOpinionFeedbackService.downloadTemplate(response);
+//    }
+
+    @ApiOperation("下载意见反馈导入模板")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/downloadTemplate")
+    public void downloadTemplate(HttpServletResponse response) {
+        log.info("StoreMenuPlatformController.downloadTemplate");
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // 从resources/templates目录读取模板文件
+            Resource resource = new ClassPathResource("templates/意见反馈导入模版.xlsx");
+            inputStream = resource.getInputStream();
+
+            // 设置响应头
+            response.reset();
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+
+            String fileName = "意见反馈导入模板.xlsx";
+            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
+            response.setHeader("Content-Disposition", "attachment;filename=\"" + encodedFileName + "\";filename*=utf-8''" + encodedFileName);
+
+            // 输出文件流
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+            }
+            outputStream.flush();
+
+            log.info("意见反馈导入模板下载成功");
+        } catch (Exception e) {
+            log.error("下载意见反馈导入模板失败", e);
+            throw new RuntimeException("下载模板失败:" + e.getMessage());
+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (Exception e) {
+                log.error("关闭流失败", e);
+            }
+        }
+    }
+}
+
+

+ 167 - 0
alien-store/src/main/java/shop/alien/store/controller/DictStoreTagController.java

@@ -0,0 +1,167 @@
+package shop.alien.store.controller;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.web.bind.annotation.CrossOrigin;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.store.service.DictStoreTagService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * 店铺字典标签控制器
+ */
+@Api(tags = {"2.7平台-店铺字典标签管理"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/dictStoreTag")
+@RequiredArgsConstructor
+public class DictStoreTagController {
+
+    private final DictStoreTagService dictStoreTagService;
+
+    @ApiOperation("查询店铺标签字典(三级树形结构)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/queryDictStoreTagTree")
+    public R<List<StoreDictionary>> queryDictStoreTagTree() {
+        log.info("dictStoreTag.queryDictStoreTagTree");
+        List<StoreDictionary> result = dictStoreTagService.queryDictStoreTagTree();
+        return R.data(result);
+    }
+
+    @ApiOperation("新增店铺标签字典(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/addDictStoreTag")
+    public R<Boolean> addDictStoreTag(@RequestBody StoreDictionary storeDictionary) {
+        log.info("dictStoreTag.addDictStoreTag:{}", storeDictionary);
+        try {
+            boolean result = dictStoreTagService.addDictStoreTag(storeDictionary);
+            if (result) {
+                return R.success("新增成功");
+            } else {
+                return R.fail("新增失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("dictStoreTag.addDictStoreTag error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("dictStoreTag.addDictStoreTag error", e);
+            return R.fail("新增失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("修改店铺标签字典(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 3)
+    @PutMapping("/updateDictStoreTag")
+    public R<Boolean> updateDictStoreTag(@RequestBody StoreDictionary storeDictionary) {
+        log.info("dictStoreTag.updateDictStoreTag:{}", storeDictionary);
+        try {
+            boolean result = dictStoreTagService.updateDictStoreTag(storeDictionary);
+            if (result) {
+                return R.success("修改成功");
+            } else {
+                return R.fail("修改失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("dictStoreTag.updateDictStoreTag error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("dictStoreTag.updateDictStoreTag error", e);
+            return R.fail("修改失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("批量导入店铺标签字典")
+    @ApiOperationSupport(order = 4)
+    @PostMapping("/import")
+    public R<String> importDictStoreTag(@RequestParam("file") MultipartFile file) {
+        log.info("dictStoreTag.importDictStoreTag fileName={}",
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            return dictStoreTagService.importDictStoreTag(file);
+        } catch (Exception e) {
+            log.error("dictStoreTag.importDictStoreTag error", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+//    @ApiOperation("下载店铺标签字典导入模板")
+//    @ApiOperationSupport(order = 5)
+//    @GetMapping("/downloadTemplate")
+//    public void downloadTemplate(HttpServletResponse response) throws IOException {
+//        log.info("dictStoreTag.downloadTemplate");
+//        dictStoreTagService.downloadTemplate(response);
+//    }
+
+    @ApiOperation("下载门店标签导入模板")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/downloadTemplate")
+    public void downloadTemplate(HttpServletResponse response) {
+        log.info("StoreMenuPlatformController.downloadTemplate");
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // 从resources/templates目录读取模板文件
+            Resource resource = new ClassPathResource("templates/门店标签导入模版.xlsx");
+            inputStream = resource.getInputStream();
+
+            // 设置响应头
+            response.reset();
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+
+            String fileName = "门店标签导入模板.xlsx";
+            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
+            response.setHeader("Content-Disposition", "attachment;filename=\"" + encodedFileName + "\";filename*=utf-8''" + encodedFileName);
+
+            // 输出文件流
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+            }
+            outputStream.flush();
+
+            log.info("门店标签导入模板下载成功");
+        } catch (Exception e) {
+            log.error("下载门店标签导入模板失败", e);
+            throw new RuntimeException("下载模板失败:" + e.getMessage());
+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (Exception e) {
+                log.error("关闭流失败", e);
+            }
+        }
+    }
+}
+
+

+ 157 - 0
alien-store/src/main/java/shop/alien/store/controller/DictionaryLibraryController.java

@@ -0,0 +1,157 @@
+package shop.alien.store.controller;
+
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.store.service.DictionaryLibraryService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+@Api(tags = {"2.6平台-字典库管理"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/dictionaryLibrary")
+@RequiredArgsConstructor
+public class DictionaryLibraryController {
+
+    private final DictionaryLibraryService dictionaryLibraryService;
+
+    @ApiOperation("查询字典库(三级树形结构)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/queryDictionaryLibraryTree")
+    public R<List<StoreDictionary>> queryDictionaryLibraryTree() {
+        log.info("dictionaryLibrary.queryDictionaryLibraryTree");
+        List<StoreDictionary> result = dictionaryLibraryService.queryDictionaryLibraryTree();
+        return R.data(result);
+    }
+
+    @ApiOperation("新增字典库(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/addDictionaryLibrary")
+    public R<Boolean> addDictionaryLibrary(@RequestBody StoreDictionary storeDictionary) {
+        log.info("dictionaryLibrary.addDictionaryLibrary:{}", storeDictionary);
+        try {
+            boolean result = dictionaryLibraryService.addDictionaryLibrary(storeDictionary);
+            if (result) {
+                return R.success("新增成功");
+            } else {
+                return R.fail("新增失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("dictionaryLibrary.addDictionaryLibrary error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("dictionaryLibrary.addDictionaryLibrary error", e);
+            return R.fail("新增失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("修改字典库(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 3)
+    @PutMapping("/updateDictionaryLibrary")
+    public R<Boolean> updateDictionaryLibrary(@RequestBody StoreDictionary storeDictionary) {
+        log.info("dictionaryLibrary.updateDictionaryLibrary:{}", storeDictionary);
+        try {
+            boolean result = dictionaryLibraryService.updateDictionaryLibrary(storeDictionary);
+            if (result) {
+                return R.success("修改成功");
+            } else {
+                return R.fail("修改失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("dictionaryLibrary.updateDictionaryLibrary error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("dictionaryLibrary.updateDictionaryLibrary error", e);
+            return R.fail("修改失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("批量导入字典库")
+    @ApiOperationSupport(order = 4)
+    @PostMapping("/import")
+    public R<String> importDictionaryLibrary(@RequestParam("file") MultipartFile file) {
+        log.info("dictionaryLibrary.importDictionaryLibrary fileName={}", 
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            return dictionaryLibraryService.importDictionaryLibrary(file);
+        } catch (Exception e) {
+            log.error("dictionaryLibrary.importDictionaryLibrary error", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+//    @ApiOperation("下载字典库导入模板")
+//    @ApiOperationSupport(order = 5)
+//    @GetMapping("/downloadTemplate")
+//    public void downloadTemplate(HttpServletResponse response) throws IOException {
+//        log.info("dictionaryLibrary.downloadTemplate");
+//        dictionaryLibraryService.downloadTemplate(response);
+//    }
+
+    @ApiOperation("下载举报导入模板")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/downloadTemplate")
+    public void downloadTemplate(HttpServletResponse response) {
+        log.info("StoreMenuPlatformController.downloadTemplate");
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // 从resources/templates目录读取模板文件
+            Resource resource = new ClassPathResource("templates/举报导入模版.xlsx");
+            inputStream = resource.getInputStream();
+
+            // 设置响应头
+            response.reset();
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+
+            String fileName = "举报导入模板.xlsx";
+            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
+            response.setHeader("Content-Disposition", "attachment;filename=\"" + encodedFileName + "\";filename*=utf-8''" + encodedFileName);
+
+            // 输出文件流
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+            }
+            outputStream.flush();
+
+            log.info("举报导入模板下载成功");
+        } catch (Exception e) {
+            log.error("下载举报导入模板失败", e);
+            throw new RuntimeException("下载模板失败:" + e.getMessage());
+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (Exception e) {
+                log.error("关闭流失败", e);
+            }
+        }
+    }
+
+}

+ 158 - 0
alien-store/src/main/java/shop/alien/store/controller/FilterConditionController.java

@@ -0,0 +1,158 @@
+package shop.alien.store.controller;
+
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.store.service.FilterConditionService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+@Api(tags = {"平台-筛选条件管理"})
+@Slf4j
+@RestController
+@CrossOrigin
+@RequestMapping("/filterCondition")
+@RequiredArgsConstructor
+public class FilterConditionController {
+
+    private final FilterConditionService filterConditionService;
+
+    @ApiOperation("查询筛选条件(三级树形结构)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/queryFilterConditionTree")
+    public R<List<StoreDictionary>> queryFilterConditionTree() {
+        log.info("filterCondition.queryFilterConditionTree");
+        List<StoreDictionary> result = filterConditionService.queryFilterConditionTree();
+        return R.data(result);
+    }
+
+    @ApiOperation("新增筛选条件(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/addFilterCondition")
+    public R<Boolean> addFilterCondition(@RequestBody StoreDictionary storeDictionary) {
+        log.info("storeDictionary.addFilterCondition:{}", storeDictionary);
+        try {
+            boolean result = filterConditionService.addFilterCondition(storeDictionary);
+            if (result) {
+                return R.success("新增成功");
+            } else {
+                return R.fail("新增失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("filterCondition.addFilterCondition error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("filterCondition.addFilterCondition error", e);
+            return R.fail("新增失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("修改筛选条件(支持一级、二级、三级)")
+    @ApiOperationSupport(order = 3)
+    @PutMapping("/updateFilterCondition")
+    public R<Boolean> updateFilterCondition(@RequestBody StoreDictionary storeDictionary) {
+        log.info("storeDictionary.updateFilterCondition:{}", storeDictionary);
+        try {
+            boolean result = filterConditionService.updateFilterCondition(storeDictionary);
+            if (result) {
+                return R.success("修改成功");
+            } else {
+                return R.fail("修改失败");
+            }
+        } catch (IllegalArgumentException e) {
+            log.error("storeDictionary.updateFilterCondition error: {}", e.getMessage());
+            return R.fail(e.getMessage());
+        } catch (Exception e) {
+            log.error("storeDictionary.updateFilterCondition error", e);
+            return R.fail("修改失败:" + e.getMessage());
+        }
+    }
+
+    @ApiOperation("批量导入筛选条件")
+    @ApiOperationSupport(order = 4)
+    @PostMapping("/import")
+    public R<String> importFilterCondition(@RequestParam("file") MultipartFile file) {
+        log.info("filterCondition.importFilterCondition fileName={}", 
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            return filterConditionService.importFilterCondition(file);
+        } catch (Exception e) {
+            log.error("filterCondition.importFilterCondition error", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+//    @ApiOperation("下载筛选条件导入模板")
+//    @ApiOperationSupport(order = 5)
+//    @GetMapping("/downloadTemplate")
+//    public void downloadTemplate(HttpServletResponse response) throws IOException {
+//        log.info("filterCondition.downloadTemplate");
+//        filterConditionService.downloadTemplate(response);
+//    }
+
+    @ApiOperation("下载筛选条件导入模板")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/downloadTemplate")
+    public void downloadTemplate(HttpServletResponse response) {
+        log.info("StoreMenuPlatformController.downloadTemplate");
+        InputStream inputStream = null;
+        OutputStream outputStream = null;
+
+        try {
+            // 从resources/templates目录读取模板文件
+            Resource resource = new ClassPathResource("templates/筛选条件导入模版.xlsx");
+            inputStream = resource.getInputStream();
+
+            // 设置响应头
+            response.reset();
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+
+            String fileName = "筛选条件导入模板.xlsx";
+            String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8.toString()).replaceAll("\\+", "%20");
+            response.setHeader("Content-Disposition", "attachment;filename=\"" + encodedFileName + "\";filename*=utf-8''" + encodedFileName);
+
+            // 输出文件流
+            outputStream = response.getOutputStream();
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) > 0) {
+                outputStream.write(buffer, 0, length);
+            }
+            outputStream.flush();
+
+            log.info("筛选条件导入模板下载成功");
+        } catch (Exception e) {
+            log.error("下载筛选条件导入模板失败", e);
+            throw new RuntimeException("下载模板失败:" + e.getMessage());
+        } finally {
+            try {
+                if (inputStream != null) {
+                    inputStream.close();
+                }
+                if (outputStream != null) {
+                    outputStream.close();
+                }
+            } catch (Exception e) {
+                log.error("关闭流失败", e);
+            }
+        }
+    }
+
+}
+

+ 9 - 26
alien-store/src/main/java/shop/alien/store/controller/LifeFeedbackController.java

@@ -1,10 +1,7 @@
 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 io.swagger.annotations.*;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
@@ -12,6 +9,8 @@ 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.dto.LifeFeedbackDto;
+import shop.alien.entity.store.dto.UserReplyDto;
 import shop.alien.entity.store.vo.LifeFeedbackVo;
 import shop.alien.store.service.LifeFeedbackService;
 
@@ -35,17 +34,10 @@ public class LifeFeedbackController {
         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 = "feedbackSource", value = "反馈来源:0-用户端,1-商家端", 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)
     })
@@ -71,20 +63,11 @@ public class LifeFeedbackController {
         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 = "POST")
+    @PostMapping("/userReply")
+    public R<String> userReply(@RequestBody UserReplyDto dto) {
+        log.info("LifeFeedbackController.userReply, dto={}", dto);
+        return lifeFeedbackService.userReply(dto);
     }
 
     // ==================== 中台接口 ====================

+ 29 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java

@@ -1103,6 +1103,35 @@ public class StoreInfoController {
     }
 
 
+    @ApiOperation(value = "证照查询")
+    @ApiOperationSupport(order = 23)
+    @GetMapping("/getStoreLicenseList")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "int", paramType = "query", required = true),
+            @ApiImplicitParam(name = "pageSize", value = "页容", dataType = "int", paramType = "query", required = true),
+            @ApiImplicitParam(name = "storeName", value = "门店名称", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "storeTel", value = "门店电话", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "imgType", value = "证照图片类型(14:营业执照;24/25:食品经营许可证;31/32:娱乐经营许可证)", dataType = "int", paramType = "query"),
+            @ApiImplicitParam(name = "states", value = "证照状态", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "startSubmitDate", value = "提交开始时间(yyyy-MM-dd HH:mm:ss)", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "endSubmitDate", value = "提交结束时间(yyyy-MM-dd HH:mm:ss)", dataType = "String", paramType = "query")
+    })
+    public R<IPage<StoreLicenseInfoVo>> getStoreLicenseList(
+            @RequestParam(value = "pageNum", defaultValue = "1") int pageNum,
+            @RequestParam(value = "pageSize", defaultValue = "10") int pageSize,
+            @RequestParam(value = "storeName", required = false) String storeName,
+            @RequestParam(value = "storeTel", required = false) String storeTel,
+            @RequestParam(value = "imgType", required = false) Integer imgType,
+            @RequestParam(value = "states", required = false) String states,
+            @RequestParam(value = "startSubmitDate", required = false) String startSubmitDate,
+            @RequestParam(value = "endSubmitDate", required = false) String endSubmitDate) {
+        log.info("StoreInfoController.getStoreLicenseList?pageNum={},pageSize={},storeName={},storeTel={},imgType={},states={},startSubmitDate={},endSubmitDate={}",
+                pageNum, pageSize, storeName, storeTel, imgType, states, startSubmitDate, endSubmitDate);
+        IPage<StoreLicenseInfoVo> page = storeInfoService.getStoreLicenseList(pageNum, pageSize, storeName, storeTel, imgType, states, startSubmitDate, endSubmitDate);
+        return R.data(page);
+    }
+
+
     /**
      * 查询两种类型店铺(丽人美发、运动健身)并按距离筛选
      *

+ 55 - 0
alien-store/src/main/java/shop/alien/store/service/DictOpinionFeedbackService.java

@@ -0,0 +1,55 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * 意见反馈字典管理Service
+ */
+public interface DictOpinionFeedbackService {
+
+    /**
+     * 查询意见反馈字典(三级树形结构)
+     *
+     * @return 字典树形结构列表
+     */
+    List<StoreDictionary> queryDictOpinionFeedbackTree();
+
+    /**
+     * 新增意见反馈字典(支持一级、二级、三级)
+     *
+     * @param storeDictionary 字典信息
+     * @return 新增结果
+     */
+    boolean addDictOpinionFeedback(StoreDictionary storeDictionary);
+
+    /**
+     * 修改意见反馈字典(支持一级、二级、三级)
+     *
+     * @param storeDictionary 字典信息
+     * @return 修改结果
+     */
+    boolean updateDictOpinionFeedback(StoreDictionary storeDictionary);
+
+    /**
+     * 批量导入意见反馈字典
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     */
+    R<String> importDictOpinionFeedback(org.springframework.web.multipart.MultipartFile file);
+
+    /**
+     * 下载意见反馈字典导入模板
+     *
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void downloadTemplate(HttpServletResponse response) throws IOException;
+}
+
+

+ 55 - 0
alien-store/src/main/java/shop/alien/store/service/DictStoreTagService.java

@@ -0,0 +1,55 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * 店铺字典标签管理Service
+ */
+public interface DictStoreTagService {
+
+    /**
+     * 查询店铺字典标签(三级树形结构)
+     *
+     * @return 标签树形结构列表
+     */
+    List<StoreDictionary> queryDictStoreTagTree();
+
+    /**
+     * 新增店铺字典标签(支持一级、二级、三级)
+     *
+     * @param storeDictionary 标签信息
+     * @return 新增结果
+     */
+    boolean addDictStoreTag(StoreDictionary storeDictionary);
+
+    /**
+     * 修改店铺字典标签(支持一级、二级、三级)
+     *
+     * @param storeDictionary 标签信息
+     * @return 修改结果
+     */
+    boolean updateDictStoreTag(StoreDictionary storeDictionary);
+
+    /**
+     * 批量导入店铺字典标签
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     */
+    R<String> importDictStoreTag(org.springframework.web.multipart.MultipartFile file);
+
+    /**
+     * 下载店铺字典标签导入模板
+     *
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void downloadTemplate(HttpServletResponse response) throws IOException;
+}
+
+

+ 57 - 0
alien-store/src/main/java/shop/alien/store/service/DictionaryLibraryService.java

@@ -0,0 +1,57 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.entity.result.R;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * 平台字典库管理Service
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/01
+ */
+public interface DictionaryLibraryService {
+
+    /**
+     * 查询字典库(三级树形结构)
+     *
+     * @return 字典库树形结构列表
+     */
+    List<StoreDictionary> queryDictionaryLibraryTree();
+
+    /**
+     * 新增字典库(支持一级、二级、三级)
+     *
+     * @param storeDictionary 字典库信息
+     * @return 新增结果
+     */
+    boolean addDictionaryLibrary(StoreDictionary storeDictionary);
+
+    /**
+     * 修改字典库(支持一级、二级、三级)
+     *
+     * @param storeDictionary 字典库信息
+     * @return 修改结果
+     */
+    boolean updateDictionaryLibrary(StoreDictionary storeDictionary);
+
+    /**
+     * 批量导入字典库
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     */
+    R<String> importDictionaryLibrary(org.springframework.web.multipart.MultipartFile file);
+
+    /**
+     * 下载字典库导入模板
+     *
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void downloadTemplate(HttpServletResponse response) throws IOException;
+}

+ 58 - 0
alien-store/src/main/java/shop/alien/store/service/FilterConditionService.java

@@ -0,0 +1,58 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * 平台筛选条件管理Service
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/01
+ */
+public interface FilterConditionService {
+
+    /**
+     * 查询筛选条件(三级树形结构)
+     *
+     * @return 筛选条件树形结构列表
+     */
+    List<StoreDictionary> queryFilterConditionTree();
+
+    /**
+     * 新增筛选条件(支持一级、二级、三级)
+     *
+     * @param storeDictionary 筛选条件信息
+     * @return 新增结果
+     */
+    boolean addFilterCondition(StoreDictionary storeDictionary);
+
+    /**
+     * 修改筛选条件(支持一级、二级、三级)
+     *
+     * @param storeDictionary 筛选条件信息
+     * @return 修改结果
+     */
+    boolean updateFilterCondition(StoreDictionary storeDictionary);
+
+    /**
+     * 批量导入筛选条件
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     */
+    R<String> importFilterCondition(org.springframework.web.multipart.MultipartFile file);
+
+    /**
+     * 下载筛选条件导入模板
+     *
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    void downloadTemplate(HttpServletResponse response) throws IOException;
+}
+

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

@@ -0,0 +1,20 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.LifeFeedbackReply;
+
+import java.util.List;
+
+/**
+ * 反馈回复 Service
+ */
+public interface LifeFeedbackReplyService extends IService<LifeFeedbackReply> {
+
+    /**
+     * 根据反馈ID查询回复列表
+     * @param feedbackId 反馈ID
+     * @return 回复列表
+     */
+    List<LifeFeedbackReply> getByFeedbackId(Integer feedbackId);
+}
+

+ 6 - 13
alien-store/src/main/java/shop/alien/store/service/LifeFeedbackService.java

@@ -7,6 +7,8 @@ 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.dto.LifeFeedbackDto;
+import shop.alien.entity.store.dto.UserReplyDto;
 import shop.alien.entity.store.vo.LifeFeedbackVo;
 
 /**
@@ -22,13 +24,6 @@ public interface LifeFeedbackService extends IService<LifeFeedback> {
     R<String> submitFeedback(LifeFeedbackDto dto);
 
     /**
-     * 平台回复反馈
-     * @param dto 回复信息
-     * @return 回复结果
-     */
-    R<String> replyFeedback(FeedbackReplyDto dto);
-
-    /**
      * 查询用户历史反馈列表
      * @param userId 用户ID
      * @param feedbackSource 反馈来源
@@ -46,13 +41,11 @@ public interface LifeFeedbackService extends IService<LifeFeedback> {
     R<LifeFeedbackVo> getFeedbackDetail(Integer feedbackId);
 
     /**
-     * 更新反馈处理状态
-     * @param feedbackId 反馈ID
-     * @param handleStatus 处理状态
-     * @param staffId 跟进人员ID
-     * @return 更新结果
+     * 用户回复
+     * @param dto 用户回复信息
+     * @return 回复结果
      */
-    R<String> updateHandleStatus(Integer feedbackId, Integer handleStatus, Integer staffId);
+    R<String> userReply(UserReplyDto dto);
 
     // ==================== 中台接口 ====================
 

+ 7 - 1
alien-store/src/main/java/shop/alien/store/service/LifeImgService.java

@@ -37,5 +37,11 @@ public interface LifeImgService extends IService<LifeImg> {
      * @return 图片URL列表
      */
     List<String> getImgUrlsByFeedbackId(Integer feedbackId);
-}
 
+    /**
+     * 查询反馈的视频URL列表
+     * @param feedbackId 反馈ID
+     * @return 视频URL列表
+     */
+    List<String> getVideoUrlsByFeedbackId(Integer feedbackId);
+}

+ 22 - 0
alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java

@@ -446,4 +446,26 @@ public interface StoreInfoService extends IService<StoreInfo> {
      * @return R<IPage<StoreInfoVo>> 分页的门店信息列表
      */
     IPage<StoreInfoVo> getLifeServicesByDistance(Double lon, Double lat, Double distance, Integer sortType, Integer businessType, Integer categoryId, String storeName, int pageNum, int pageSize);
+
+    /**
+     * 门店证照查询(分页)
+     *
+     * @param pageNum         页码
+     * @param pageSize        页容
+     * @param storeName       门店名称(模糊)
+     * @param storeTel        门店电话(模糊)
+     * @param imgType         证照图片类型(14:营业执照;24/25:食品经营许可证;31/32:娱乐经营许可证)
+     * @param states          证照状态
+     * @param startSubmitDate 提交开始时间(yyyy-MM-dd HH:mm:ss)
+     * @param endSubmitDate   提交结束时间(yyyy-MM-dd HH:mm:ss)
+     * @return 证照分页结果
+     */
+    IPage<StoreLicenseInfoVo> getStoreLicenseList(int pageNum,
+                                                  int pageSize,
+                                                  String storeName,
+                                                  String storeTel,
+                                                  Integer imgType,
+                                                  String states,
+                                                  String startSubmitDate,
+                                                  String endSubmitDate);
 }

+ 826 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DictOpinionFeedbackServiceImpl.java

@@ -0,0 +1,826 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.entity.store.excelVo.DictionaryLibraryExcelVo;
+import shop.alien.entity.store.excelVo.util.ExcelHeader;
+import shop.alien.mapper.StoreDictionaryMapper;
+import shop.alien.store.service.DictOpinionFeedbackService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 意见反馈字典管理ServiceImpl
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DictOpinionFeedbackServiceImpl extends ServiceImpl<StoreDictionaryMapper, StoreDictionary> implements DictOpinionFeedbackService {
+
+    private static final String DEFAULT_TYPE_NAME = "dict_opinion_feedback";
+
+    private final StoreDictionaryMapper storeDictionaryMapper;
+
+    /**
+     * 查询意见反馈字典(三级树形结构)
+     */
+    @Override
+    public List<StoreDictionary> queryDictOpinionFeedbackTree() {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.orderByAsc(StoreDictionary::getSortId);
+        List<StoreDictionary> dictList = storeDictionaryMapper.selectList(queryWrapper);
+
+        return buildTreeOptimized(dictList);
+    }
+
+    /**
+     * 构建树形结构(优化版)
+     */
+    private List<StoreDictionary> buildTreeOptimized(List<StoreDictionary> flatList) {
+        if (flatList == null || flatList.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        Map<Integer, StoreDictionary> nodeMap = new HashMap<>();
+        Map<Integer, List<StoreDictionary>> parentChildMap = new HashMap<>();
+        List<StoreDictionary> result = new ArrayList<>();
+
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            Integer parentId = entity.getParentId();
+
+            nodeMap.put(id, entity);
+            entity.setStoreDictionaryList(new ArrayList<>());
+
+            if (parentId == null || parentId == 0) {
+                result.add(entity);
+            } else {
+                parentChildMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(entity);
+            }
+        }
+
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            if (parentChildMap.containsKey(id)) {
+                entity.getStoreDictionaryList().addAll(parentChildMap.get(id));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 新增意见反馈字典(支持一级、二级、三级)
+     */
+    @Override
+    public boolean addDictOpinionFeedback(StoreDictionary storeDictionary) {
+        if (storeDictionary == null || StringUtils.isBlank(storeDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("字典描述不能为空");
+        }
+
+        storeDictionary.setDeleteFlag(0);
+        storeDictionary.setCreatedTime(new Date());
+
+        Integer parentId = storeDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        if (parentId != 0) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+            if (parent == null || parent.getDeleteFlag() == 1) {
+                throw new IllegalArgumentException("父节点不存在或已删除");
+            }
+        }
+
+        String typeName = storeDictionary.getTypeName();
+        if (StringUtils.isBlank(typeName)) {
+            throw new IllegalArgumentException("typeName不能为空");
+        }
+
+        String maxDictId = getMaxDictIdByTypeName(typeName);
+        int nextDictId = (maxDictId == null ? 1 : Integer.parseInt(maxDictId)) + 1;
+        storeDictionary.setDictId(String.valueOf(nextDictId));
+
+        Integer maxSortId = getMaxSortIdByParentId(parentId);
+        storeDictionary.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        return this.save(storeDictionary);
+    }
+
+    /**
+     * 修改意见反馈字典(支持一级、二级、三级)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateDictOpinionFeedback(StoreDictionary storeDictionary) {
+        if (storeDictionary == null || storeDictionary.getId() == null) {
+            throw new IllegalArgumentException("ID不能为空");
+        }
+        if (StringUtils.isBlank(storeDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("字典描述不能为空");
+        }
+
+        StoreDictionary existing = storeDictionaryMapper.selectById(storeDictionary.getId());
+        if (existing == null) {
+            throw new IllegalArgumentException("记录不存在");
+        }
+        if (existing.getDeleteFlag() == 1) {
+            throw new IllegalArgumentException("该记录已删除");
+        }
+
+        Integer existingParentId = existing.getParentId();
+        boolean isFirstLevel = (existingParentId == null || existingParentId == 0);
+        boolean isSecondLevel = false;
+
+        if (!isFirstLevel) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(existingParentId);
+            if (parent != null) {
+                Integer grandParentId = parent.getParentId();
+                isSecondLevel = (grandParentId == null || grandParentId == 0);
+            }
+        }
+
+        Integer parentId = storeDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        Integer normalizedParentId = parentId == null ? 0 : parentId;
+        Integer normalizedExistingParentId = existingParentId == null ? 0 : existingParentId;
+        Integer oldSortId = existing.getSortId();
+        Integer newSortId = storeDictionary.getSortId();
+        boolean parentIdChanged = !normalizedParentId.equals(normalizedExistingParentId);
+
+        // 判断是否修改了关键字段(parent_id)
+        boolean keyFieldChanged = parentIdChanged;
+
+        if ((isFirstLevel || isSecondLevel) && keyFieldChanged) {
+            boolean hasChildren = hasUndeletedChildren(existing.getId());
+            if (hasChildren) {
+                throw new IllegalArgumentException("该节点存在未删除的子节点,不允许修改关键字段(父节点)");
+            }
+        }
+
+        if (parentIdChanged) {
+            if (parentId != 0) {
+                StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+                if (parent == null || parent.getDeleteFlag() == 1) {
+                    throw new IllegalArgumentException("父节点不存在或已删除");
+                }
+            }
+
+            if (newSortId == null) {
+                Integer maxSortId = getMaxSortIdByParentId(parentId);
+                int nextSortId = (maxSortId == null ? 1 : maxSortId) + 1;
+                newSortId = nextSortId;
+                storeDictionary.setSortId(newSortId);
+            }
+
+            adjustSortOrderForMoveOut(existing.getId(), normalizedExistingParentId, oldSortId);
+
+            if (newSortId != null) {
+                try {
+                    int newOrder = newSortId;
+                    Integer maxSortId = getMaxSortIdByParentId(parentId);
+                    int maxOrder = (maxSortId == null ? 0 : maxSortId);
+                    if (newOrder <= maxOrder) {
+                        adjustSortOrderForMoveIn(existing.getId(), normalizedParentId, newSortId);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析新sortId: {}", newSortId, e);
+                }
+            }
+        }
+
+        Integer oldHidden = existing.getHidden();
+        Integer newHidden = storeDictionary.getHidden() == null ? oldHidden : storeDictionary.getHidden();
+        storeDictionary.setHidden(newHidden);
+        storeDictionary.setDeleteFlag(existing.getDeleteFlag());
+        storeDictionary.setCreatedTime(existing.getCreatedTime());
+        storeDictionary.setCreatedUserId(existing.getCreatedUserId());
+        storeDictionary.setUpdatedTime(new Date());
+
+        if (!parentIdChanged) {
+            if (newSortId == null) {
+                storeDictionary.setSortId(oldSortId);
+            } else if (!newSortId.equals(oldSortId)) {
+                adjustSortOrder(existing.getId(), normalizedParentId, oldSortId, newSortId);
+            }
+        }
+
+        boolean updateResult = this.updateById(storeDictionary);
+
+        boolean hiddenChanged = (oldHidden == null && newHidden != null) ||
+                (oldHidden != null && !oldHidden.equals(newHidden));
+        if (updateResult && (isFirstLevel || isSecondLevel) && hiddenChanged && newHidden != null) {
+            updateChildrenHidden(existing.getId(), newHidden);
+        }
+
+        return updateResult;
+    }
+
+    /**
+     * 判断节点是否有未删除的子节点
+     */
+    private boolean hasUndeletedChildren(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.last("LIMIT 1");
+
+        StoreDictionary child = storeDictionaryMapper.selectOne(queryWrapper);
+        return child != null;
+    }
+
+    /**
+     * 递归更新子节点的显示/隐藏状态
+     */
+    private void updateChildrenHidden(Integer parentId, Integer hidden) {
+        if (parentId == null || hidden == null) {
+            return;
+        }
+
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+
+        List<StoreDictionary> children = storeDictionaryMapper.selectList(queryWrapper);
+
+        if (children == null || children.isEmpty()) {
+            return;
+        }
+
+        Date updateTime = new Date();
+        for (StoreDictionary child : children) {
+            child.setHidden(hidden);
+            child.setUpdatedTime(updateTime);
+            storeDictionaryMapper.updateById(child);
+            updateChildrenHidden(child.getId(), hidden);
+        }
+    }
+
+    /**
+     * 根据 typeName 获取最大 dict_id
+     */
+    private String getMaxDictIdByTypeName(String typeName) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreDictionary::getTypeName, typeName);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+
+        queryWrapper.orderByDesc(StoreDictionary::getDictId);
+        queryWrapper.last("LIMIT 1");
+
+        StoreDictionary maxDict = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxDict != null ? maxDict.getDictId() : null;
+    }
+
+    /**
+     * 根据parentId获取最大的sortId
+     */
+    private Integer getMaxSortIdByParentId(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+
+        queryWrapper.orderByDesc(StoreDictionary::getSortId);
+        queryWrapper.last("LIMIT 1");
+
+        StoreDictionary maxDict = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxDict != null && maxDict.getSortId() != null ? maxDict.getSortId() : null;
+    }
+
+    /**
+     * 调整排序:当记录的sortId改变时,调整同级其他记录的排序
+     */
+    private void adjustSortOrder(Integer currentId, Integer parentId, Integer oldSortId, Integer newSortId) {
+        try {
+            int oldOrder = oldSortId;
+            int newOrder = newSortId;
+
+            if (oldOrder == newOrder) {
+                return;
+            }
+
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+
+            if (parentId == null || parentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, parentId);
+            }
+
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+
+            if (newOrder < oldOrder) {
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder >= newOrder && siblingOrder < oldOrder) {
+                            sibling.setSortId(siblingOrder + 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            } else if (newOrder > oldOrder) {
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder > oldOrder && siblingOrder <= newOrder) {
+                            sibling.setSortId(siblingOrder - 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("调整排序失败,sortId格式错误: oldSortId={}, newSortId={}", oldSortId, newSortId, e);
+        } catch (Exception e) {
+            log.error("调整排序失败", e);
+        }
+    }
+
+    /**
+     * 处理从原节点移出时的排序调整
+     */
+    private void adjustSortOrderForMoveOut(Integer currentId, Integer oldParentId, Integer oldSortId) {
+        try {
+            int oldOrder = oldSortId;
+
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+
+            if (oldParentId == null || oldParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, oldParentId);
+            }
+
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder > oldOrder) {
+                        sibling.setSortId(siblingOrder - 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移出节点排序调整失败,sortId格式错误: oldSortId={}", oldSortId, e);
+        } catch (Exception e) {
+            log.error("移出节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 处理移入新节点时的排序调整
+     */
+    private void adjustSortOrderForMoveIn(Integer currentId, Integer newParentId, Integer newSortId) {
+        try {
+            int newOrder = newSortId;
+
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+
+            if (newParentId == null || newParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, newParentId);
+            }
+
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder >= newOrder) {
+                        sibling.setSortId(siblingOrder + 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析sortId: {}", sibling.getDictId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移入节点排序调整失败,sortId格式错误: newSortId={}", newSortId, e);
+        } catch (Exception e) {
+            log.error("移入节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 批量导入意见反馈字典
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> importDictOpinionFeedback(MultipartFile file) {
+        log.info("DictOpinionFeedbackServiceImpl.importDictOpinionFeedback fileName={}",
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            if (file == null || file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+
+            String fileName = file.getOriginalFilename();
+            if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
+                return R.fail("文件格式不正确,请上传Excel文件");
+            }
+
+            List<String> errorMessages = new ArrayList<>();
+            int successCount = 0;
+            int totalCount = 0;
+
+            try (InputStream inputStream = file.getInputStream();
+                 Workbook workbook = new XSSFWorkbook(inputStream)) {
+                Sheet sheet = workbook.getSheetAt(0);
+
+                Row headerRow = sheet.getRow(5);
+                if (headerRow == null) {
+                    return R.fail("Excel文件格式不正确,缺少表头");
+                }
+
+                Map<String, Integer> headerMap = new HashMap<>();
+                Field[] fields = DictionaryLibraryExcelVo.class.getDeclaredFields();
+                for (int i = 0; i < headerRow.getLastCellNum(); i++) {
+                    Cell cell = headerRow.getCell(i);
+                    if (cell != null) {
+                        String headerName = getCellValueAsString(cell);
+                        if (StringUtils.isNotBlank(headerName)) {
+                            headerMap.put(headerName.trim(), i);
+                        }
+                    }
+                }
+
+                for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
+                    Row row = sheet.getRow(rowIndex);
+                    if (row == null) {
+                        continue;
+                    }
+
+                    boolean isEmptyRow = true;
+                    for (int i = 0; i < row.getLastCellNum(); i++) {
+                        Cell cell = row.getCell(i);
+                        if (cell != null && cell.getCellType() != CellType.BLANK) {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                isEmptyRow = false;
+                                break;
+                            }
+                        }
+                    }
+                    if (isEmptyRow) {
+                        continue;
+                    }
+
+                    totalCount++;
+                    DictionaryLibraryExcelVo excelVo = new DictionaryLibraryExcelVo();
+
+                    for (Field field : fields) {
+                        if (!field.isAnnotationPresent(ExcelHeader.class)) {
+                            continue;
+                        }
+                        ExcelHeader excelHeader = field.getAnnotation(ExcelHeader.class);
+                        String headerName = excelHeader.value();
+                        Integer colIndex = headerMap.get(headerName);
+                        if (colIndex == null) {
+                            continue;
+                        }
+
+                        Cell cell = row.getCell(colIndex);
+                        if (cell == null) {
+                            continue;
+                        }
+
+                        field.setAccessible(true);
+                        try {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                field.set(excelVo, cellValue.trim());
+                            }
+                        } catch (Exception e) {
+                            log.warn("读取字段{}失败:{}", headerName, e.getMessage());
+                        }
+                    }
+
+                    try {
+                        processImportData(excelVo, rowIndex + 1, errorMessages);
+                        successCount++;
+                    } catch (Exception e) {
+                        errorMessages.add(String.format("第%d行:%s", rowIndex + 1, e.getMessage()));
+                        log.error("导入第{}行数据失败", rowIndex + 1, e);
+                    }
+                }
+            }
+
+            StringBuilder message = new StringBuilder();
+            message.append(String.format("导入完成:成功%d条,失败%d条", successCount, totalCount - successCount));
+            if (!errorMessages.isEmpty()) {
+                message.append("\n失败详情:\n");
+                int maxErrors = Math.min(errorMessages.size(), 10);
+                for (int i = 0; i < maxErrors; i++) {
+                    message.append(errorMessages.get(i)).append("\n");
+                }
+                if (errorMessages.size() > 10) {
+                    message.append(String.format("...还有%d条错误未显示", errorMessages.size() - 10));
+                }
+            }
+
+            if (successCount == 0) {
+                return R.fail(message.toString());
+            } else {
+                return R.success(message.toString());
+            }
+        } catch (Exception e) {
+            log.error("导入意见反馈字典失败", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理导入数据
+     */
+    private void processImportData(DictionaryLibraryExcelVo excelVo, int rowIndex, List<String> errorMessages) {
+        if (StringUtils.isBlank(excelVo.getFirstLevelName())) {
+            throw new IllegalArgumentException("一级分类名称不能为空");
+        }
+
+        String firstLevelName = excelVo.getFirstLevelName().trim();
+        String secondLevelName = StringUtils.isNotBlank(excelVo.getSecondLevelName())
+                ? excelVo.getSecondLevelName().trim() : null;
+        String thirdLevelName = StringUtils.isNotBlank(excelVo.getThirdLevelName())
+                ? excelVo.getThirdLevelName().trim() : null;
+
+        if (StringUtils.isNotBlank(thirdLevelName)) {
+            createOrUpdateLevel(firstLevelName, secondLevelName, thirdLevelName, 3, excelVo.getHidden(), errorMessages, rowIndex);
+        } else if (StringUtils.isNotBlank(secondLevelName)) {
+            createOrUpdateLevel(firstLevelName, secondLevelName, null, 2, excelVo.getHidden(), errorMessages, rowIndex);
+        } else {
+            createOrUpdateLevel(firstLevelName, null, null, 1, excelVo.getHidden(), errorMessages, rowIndex);
+        }
+    }
+
+    /**
+     * 创建或更新分类级别
+     */
+    private void createOrUpdateLevel(String firstLevelName, String secondLevelName, String thirdLevelName,
+                                     int level, String hidden, List<String> errorMessages, int rowIndex) {
+        try {
+            StoreDictionary firstLevel = findOrCreateLevel(firstLevelName, null, 0, hidden);
+            if (firstLevel == null) {
+                throw new IllegalArgumentException("创建一级分类失败");
+            }
+            if (level >= 2 && StringUtils.isNotBlank(secondLevelName)) {
+                StoreDictionary secondLevel = findOrCreateLevel(secondLevelName, firstLevel.getId(), 1, hidden);
+                if (secondLevel == null) {
+                    throw new IllegalArgumentException("创建二级分类失败");
+                }
+
+                if (level >= 3 && StringUtils.isNotBlank(thirdLevelName)) {
+                    StoreDictionary thirdLevel = findOrCreateLevel(thirdLevelName, secondLevel.getId(), 2, hidden);
+                    if (thirdLevel == null) {
+                        throw new IllegalArgumentException("创建三级分类失败");
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+
+    /**
+     * 查找或创建分类
+     */
+    private StoreDictionary findOrCreateLevel(String dictDetail, Integer parentId, int expectedLevel, String hidden) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "opinion_feedback", "opinion_feedback_type", "opinion_feedback_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.eq(StoreDictionary::getDictDetail, dictDetail);
+
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+
+        StoreDictionary existing = storeDictionaryMapper.selectOne(queryWrapper);
+
+        if (existing != null) {
+            return existing;
+        }
+
+        StoreDictionary newDict = new StoreDictionary();
+        newDict.setDictDetail(dictDetail);
+        // 一级为筛选条件,二级为筛选条件种类,三级为分类,四级为时间范围
+        if (expectedLevel == 0) {
+            newDict.setTypeDetail("意见反馈");
+            newDict.setTypeName("opinion_feedback");
+        } else if (expectedLevel == 1) {
+            newDict.setTypeDetail("意见反馈种类");
+            newDict.setTypeName("opinion_feedback_type");
+        } else if (expectedLevel == 2) {
+            newDict.setTypeDetail("意见反馈分类");
+            newDict.setTypeName("opinion_feedback_classify");
+        }
+        newDict.setParentId(parentId == null ? null : parentId);
+        newDict.setHidden(StringUtils.isNotBlank(hidden) && hidden.equals("隐藏") ? 1 : 0);
+        newDict.setDeleteFlag(0);
+        newDict.setCreatedTime(new Date());
+
+        String maxDictId = getMaxDictIdByTypeName(newDict.getTypeName());
+        newDict.setDictId(String.valueOf(maxDictId == null ? 1 : Integer.parseInt(maxDictId) + 1));
+
+        Integer maxSortId = getMaxSortIdByParentId(newDict.getParentId());
+        newDict.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        boolean saved = this.save(newDict);
+        return saved ? newDict : null;
+    }
+
+    private String buildTypeDetail(int expectedLevel) {
+        if (expectedLevel == 0) {
+            return "意见反馈一级";
+        } else if (expectedLevel == 1) {
+            return "意见反馈二级";
+        } else if (expectedLevel == 2) {
+            return "意见反馈三级";
+        }
+        return "意见反馈";
+    }
+
+    /**
+     * 获取单元格值(字符串格式)
+     */
+    private String getCellValueAsString(Cell cell) {
+        if (cell == null) {
+            return null;
+        }
+
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    return cell.getDateCellValue().toString();
+                } else {
+                    double numericValue = cell.getNumericCellValue();
+                    if (numericValue == (long) numericValue) {
+                        return String.valueOf((long) numericValue);
+                    } else {
+                        return String.valueOf(numericValue);
+                    }
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                return cell.getCellFormula();
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * 下载意见反馈字典导入模板
+     */
+    @Override
+    public void downloadTemplate(HttpServletResponse response) throws IOException {
+        log.info("DictOpinionFeedbackServiceImpl.downloadTemplate");
+
+        XSSFWorkbook workbook = new XSSFWorkbook();
+        try {
+            Sheet sheet = workbook.createSheet("意见反馈字典导入模板");
+
+            Field[] fields = DictionaryLibraryExcelVo.class.getDeclaredFields();
+            List<Field> excelFields = new ArrayList<>();
+            for (Field field : fields) {
+                if (field.isAnnotationPresent(ExcelHeader.class)) {
+                    excelFields.add(field);
+                }
+            }
+
+            Font headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerFont.setFontHeightInPoints((short) 12);
+            CellStyle headerCellStyle = workbook.createCellStyle();
+            headerCellStyle.setFont(headerFont);
+            headerCellStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+            headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerCellStyle.setBorderBottom(BorderStyle.THIN);
+            headerCellStyle.setBorderTop(BorderStyle.THIN);
+            headerCellStyle.setBorderLeft(BorderStyle.THIN);
+            headerCellStyle.setBorderRight(BorderStyle.THIN);
+
+            Row headerRow = sheet.createRow(0);
+            for (int i = 0; i < excelFields.size(); i++) {
+                Field field = excelFields.get(i);
+                ExcelHeader excelHeader = field.getAnnotation(ExcelHeader.class);
+                Cell cell = headerRow.createCell(i);
+                cell.setCellValue(excelHeader.value());
+                cell.setCellStyle(headerCellStyle);
+            }
+
+            sheet.setColumnWidth(0, 25 * 256);
+            sheet.setColumnWidth(1, 25 * 256);
+            sheet.setColumnWidth(2, 25 * 256);
+            sheet.setColumnWidth(3, 15 * 256);
+
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+            String fileName = URLEncoder.encode("意见反馈字典导入模板", "UTF-8").replaceAll("\\+", "%20");
+            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+
+            OutputStream outputStream = response.getOutputStream();
+            workbook.write(outputStream);
+            outputStream.flush();
+        } finally {
+            if (workbook != null) {
+                try {
+                    workbook.close();
+                } catch (IOException e) {
+                    log.error("关闭workbook失败", e);
+                }
+            }
+        }
+    }
+}
+
+

+ 833 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DictStoreTagServiceImpl.java

@@ -0,0 +1,833 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.CellType;
+import org.apache.poi.ss.usermodel.DateUtil;
+import org.apache.poi.ss.usermodel.FillPatternType;
+import org.apache.poi.ss.usermodel.Font;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.IndexedColors;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.VerticalAlignment;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.entity.store.excelVo.DictionaryLibraryExcelVo;
+import shop.alien.entity.store.excelVo.util.ExcelHeader;
+import shop.alien.mapper.StoreDictionaryMapper;
+import shop.alien.store.service.DictStoreTagService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 店铺字典标签管理ServiceImpl
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DictStoreTagServiceImpl extends ServiceImpl<StoreDictionaryMapper, StoreDictionary> implements DictStoreTagService {
+
+    private static final String DEFAULT_TYPE_NAME = "dict_store_tag";
+
+    private final StoreDictionaryMapper storeDictionaryMapper;
+
+    /**
+     * 查询店铺字典标签(三级树形结构)
+     *
+     * @return 标签树形结构列表
+     */
+    @Override
+    public List<StoreDictionary> queryDictStoreTagTree() {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.orderByAsc(StoreDictionary::getSortId);
+        List<StoreDictionary> dictStoreTags = storeDictionaryMapper.selectList(queryWrapper);
+
+        return buildTreeOptimized(dictStoreTags);
+    }
+
+    /**
+     * 构建树形结构
+     */
+    private List<StoreDictionary> buildTreeOptimized(List<StoreDictionary> flatList) {
+        if (flatList == null || flatList.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        Map<Integer, StoreDictionary> nodeMap = new HashMap<>();
+        Map<Integer, List<StoreDictionary>> parentChildMap = new HashMap<>();  // 父ID到子节点列表的映射
+        List<StoreDictionary> result = new ArrayList<>();  // 结果列表
+
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            Integer parentId = entity.getParentId();
+
+            // 存入节点映射
+            nodeMap.put(id, entity);
+
+            // 初始化子节点列表
+            entity.setStoreDictionaryList(new ArrayList<>());
+
+            // 如果是根节点(parentId为null或0),直接添加到结果
+            if (parentId == null || parentId == 0) {
+                result.add(entity);
+            } else {
+                // 否则,记录父子关系
+                parentChildMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(entity);
+            }
+        }
+
+        // 建立父子关系
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            if (parentChildMap.containsKey(id)) {
+                entity.getStoreDictionaryList().addAll(parentChildMap.get(id));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 新增店铺字典标签(支持一级、二级、三级)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean addDictStoreTag(StoreDictionary storeDictionary) {
+        if (storeDictionary == null || StringUtils.isBlank(storeDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("字典描述不能为空");
+        }
+
+        storeDictionary.setDeleteFlag(0);
+        storeDictionary.setCreatedTime(new Date());
+
+        Integer parentId = storeDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        if (parentId != 0) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+            if (parent == null || parent.getDeleteFlag() == 1) {
+                throw new IllegalArgumentException("父节点不存在或已删除");
+            }
+        }
+
+        String typeName = storeDictionary.getTypeName();
+        if (StringUtils.isBlank(typeName)) {
+            throw new IllegalArgumentException("typeName不能为空");
+        }
+        String maxDictId = getMaxDictIdByTypeName(typeName);
+        int nextDictId = (maxDictId == null ? 1 : Integer.parseInt(maxDictId)) + 1;
+        storeDictionary.setDictId(String.valueOf(nextDictId));
+
+        Integer maxSortId = getMaxSortIdByParentId(parentId);
+        storeDictionary.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        return this.save(storeDictionary);
+    }
+
+    /**
+     * 修改店铺字典标签(支持一级、二级、三级)
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateDictStoreTag(StoreDictionary storeDictionary) {
+        // 参数校验
+        if (storeDictionary == null || storeDictionary.getId() == null) {
+            throw new IllegalArgumentException("ID不能为空");
+        }
+        if (StringUtils.isBlank(storeDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("字典库描述不能为空");
+        }
+
+        StoreDictionary existing = storeDictionaryMapper.selectById(storeDictionary.getId());
+        if (existing == null) {
+            throw new IllegalArgumentException("记录不存在");
+        }
+        if (existing.getDeleteFlag() == 1) {
+            throw new IllegalArgumentException("该记录已删除");
+        }
+
+        Integer existingParentId = existing.getParentId();
+        boolean isFirstLevel = (existingParentId == null || existingParentId == 0);
+        boolean isSecondLevel = false;
+
+        if (!isFirstLevel) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(existingParentId);
+            if (parent != null) {
+                Integer grandParentId = parent.getParentId();
+                isSecondLevel = (grandParentId == null || grandParentId == 0);
+            }
+        }
+
+        Integer parentId = storeDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        Integer normalizedParentId = parentId == null ? 0 : parentId;
+        Integer normalizedExistingParentId = existingParentId == null ? 0 : existingParentId;
+        Integer oldSortId = existing.getSortId();
+        Integer newSortId = storeDictionary.getSortId();
+        boolean parentIdChanged = !normalizedParentId.equals(normalizedExistingParentId);
+        boolean keyFieldChanged = parentIdChanged;
+
+        if ((isFirstLevel || isSecondLevel) && keyFieldChanged) {
+            boolean hasChildren = hasUndeletedChildren(existing.getId());
+            if (hasChildren) {
+                throw new IllegalArgumentException("该节点存在未删除的子节点,不允许修改关键字段(父节点)");
+            }
+        }
+
+        if (parentIdChanged) {
+            if (parentId != 0) {
+                StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+                if (parent == null || parent.getDeleteFlag() == 1) {
+                    throw new IllegalArgumentException("父节点不存在或已删除");
+                }
+            }
+
+            if (newSortId == null) {
+                Integer maxSortId = getMaxSortIdByParentId(parentId);
+                int nextSortId = (maxSortId == null ? 1 : maxSortId) + 1;
+                newSortId = nextSortId;
+                storeDictionary.setSortId(newSortId);
+            }
+
+            adjustSortOrderForMoveOut(existing.getId(), normalizedExistingParentId, oldSortId);
+
+            if (newSortId != null) {
+                try {
+                    int newOrder = newSortId;
+                    Integer maxSortId = getMaxSortIdByParentId(parentId);
+                    int maxOrder = (maxSortId == null ? 0 : maxSortId);
+                    if (newOrder <= maxOrder) {
+                        adjustSortOrderForMoveIn(existing.getId(), normalizedParentId, newSortId);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析新sortId: {}", newSortId, e);
+                }
+            }
+        }
+
+        Integer oldHidden = existing.getHidden();
+        Integer newHidden = storeDictionary.getHidden() == null ? oldHidden : storeDictionary.getHidden();
+        storeDictionary.setHidden(newHidden);
+        storeDictionary.setDeleteFlag(existing.getDeleteFlag());
+        storeDictionary.setCreatedTime(existing.getCreatedTime());
+        storeDictionary.setCreatedUserId(existing.getCreatedUserId());
+        storeDictionary.setUpdatedTime(new Date());
+
+        if (!parentIdChanged) {
+            if (newSortId == null) {
+                storeDictionary.setSortId(oldSortId);
+            } else if (!newSortId.equals(oldSortId)) {
+                adjustSortOrder(existing.getId(), normalizedParentId, oldSortId, newSortId);
+            }
+        }
+
+        boolean updateResult = this.updateById(storeDictionary);
+
+        boolean hiddenChanged = (oldHidden == null && newHidden != null) ||
+                (oldHidden != null && !oldHidden.equals(newHidden));
+        if (updateResult && (isFirstLevel || isSecondLevel) && hiddenChanged && newHidden != null) {
+            updateChildrenHidden(existing.getId(), newHidden);
+        }
+
+        return updateResult;
+    }
+
+    /**
+     * 判断节点是否有未删除的子节点
+     */
+    private boolean hasUndeletedChildren(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.last("LIMIT 1");
+
+        StoreDictionary child = storeDictionaryMapper.selectOne(queryWrapper);
+        return child != null;
+    }
+
+    /**
+     * 递归更新子节点的显示/隐藏状态
+     */
+    private void updateChildrenHidden(Integer parentId, Integer hidden) {
+        if (parentId == null || hidden == null) {
+            return;
+        }
+
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+
+        List<StoreDictionary> children = storeDictionaryMapper.selectList(queryWrapper);
+
+        if (children == null || children.isEmpty()) {
+            return;
+        }
+
+        Date updateTime = new Date();
+        for (StoreDictionary child : children) {
+            child.setHidden(hidden);
+            child.setUpdatedTime(updateTime);
+            storeDictionaryMapper.updateById(child);
+            updateChildrenHidden(child.getId(), hidden);
+        }
+    }
+
+    /**
+     * 根据 typeName 获取最大 dict_id
+     */
+    private String getMaxDictIdByTypeName(String typeName) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreDictionary::getTypeName, typeName);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+
+        queryWrapper.orderByDesc(StoreDictionary::getDictId);
+        queryWrapper.last("LIMIT 1");
+
+        StoreDictionary maxDict = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxDict != null ? maxDict.getDictId() : null;
+    }
+
+    /**
+     * 根据parentId获取最大的sortId
+     */
+    private Integer getMaxSortIdByParentId(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+
+        queryWrapper.orderByDesc(StoreDictionary::getSortId);
+        queryWrapper.last("LIMIT 1");
+
+        StoreDictionary maxDict = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxDict != null && maxDict.getSortId() != null ? maxDict.getSortId() : null;
+    }
+
+    /**
+     * 调整排序:当记录的sortId改变时,调整同级其他记录的排序
+     */
+    private void adjustSortOrder(Integer currentId, Integer parentId, Integer oldSortId, Integer newSortId) {
+        try {
+            int oldOrder = oldSortId;
+            int newOrder = newSortId;
+
+            if (oldOrder == newOrder) {
+                return;
+            }
+
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+
+            if (parentId == null || parentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, parentId);
+            }
+
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+
+            if (newOrder < oldOrder) {
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder >= newOrder && siblingOrder < oldOrder) {
+                            sibling.setSortId(siblingOrder + 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            } else if (newOrder > oldOrder) {
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder > oldOrder && siblingOrder <= newOrder) {
+                            sibling.setSortId(siblingOrder - 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("调整排序失败,sortId格式错误: oldSortId={}, newSortId={}", oldSortId, newSortId, e);
+        } catch (Exception e) {
+            log.error("调整排序失败", e);
+        }
+    }
+
+    /**
+     * 处理从原节点移出时的排序调整
+     */
+    private void adjustSortOrderForMoveOut(Integer currentId, Integer oldParentId, Integer oldSortId) {
+        try {
+            int oldOrder = oldSortId;
+
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+
+            if (oldParentId == null || oldParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, oldParentId);
+            }
+
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder > oldOrder) {
+                        sibling.setSortId(siblingOrder - 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移出节点排序调整失败,sortId格式错误: oldSortId={}", oldSortId, e);
+        } catch (Exception e) {
+            log.error("移出节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 处理移入新节点时的排序调整
+     */
+    private void adjustSortOrderForMoveIn(Integer currentId, Integer newParentId, Integer newSortId) {
+        try {
+            int newOrder = newSortId;
+
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+
+            if (newParentId == null || newParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, newParentId);
+            }
+
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder >= newOrder) {
+                        sibling.setSortId(siblingOrder + 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析sortId: {}", sibling.getDictId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移入节点排序调整失败,sortId格式错误: newSortId={}", newSortId, e);
+        } catch (Exception e) {
+            log.error("移入节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 批量导入店铺字典标签
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> importDictStoreTag(MultipartFile file) {
+        log.info("DictStoreTagServiceImpl.importDictStoreTag fileName={}",
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            if (file == null || file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+
+            String fileName = file.getOriginalFilename();
+            if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
+                return R.fail("文件格式不正确,请上传Excel文件");
+            }
+
+            List<String> errorMessages = new ArrayList<>();
+            int successCount = 0;
+            int totalCount = 0;
+
+            try (InputStream inputStream = file.getInputStream();
+                 Workbook workbook = new XSSFWorkbook(inputStream)) {
+                Sheet sheet = workbook.getSheetAt(0);
+
+                Row headerRow = sheet.getRow(5);
+                if (headerRow == null) {
+                    return R.fail("Excel文件格式不正确,缺少表头");
+                }
+
+                Map<String, Integer> headerMap = new HashMap<>();
+                Field[] fields = DictionaryLibraryExcelVo.class.getDeclaredFields();
+                for (int i = 0; i < headerRow.getLastCellNum(); i++) {
+                    Cell cell = headerRow.getCell(i);
+                    if (cell != null) {
+                        String headerName = getCellValueAsString(cell);
+                        if (StringUtils.isNotBlank(headerName)) {
+                            headerMap.put(headerName.trim(), i);
+                        }
+                    }
+                }
+
+                for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
+                    Row row = sheet.getRow(rowIndex);
+                    if (row == null) {
+                        continue;
+                    }
+
+                    boolean isEmptyRow = true;
+                    for (int i = 0; i < row.getLastCellNum(); i++) {
+                        Cell cell = row.getCell(i);
+                        if (cell != null && cell.getCellType() != CellType.BLANK) {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                isEmptyRow = false;
+                                break;
+                            }
+                        }
+                    }
+                    if (isEmptyRow) {
+                        continue;
+                    }
+
+                    totalCount++;
+                    DictionaryLibraryExcelVo excelVo = new DictionaryLibraryExcelVo();
+
+                    for (Field field : fields) {
+                        if (!field.isAnnotationPresent(ExcelHeader.class)) {
+                            continue;
+                        }
+                        ExcelHeader excelHeader = field.getAnnotation(ExcelHeader.class);
+                        String headerName = excelHeader.value();
+                        Integer colIndex = headerMap.get(headerName);
+                        if (colIndex == null) {
+                            continue;
+                        }
+
+                        Cell cell = row.getCell(colIndex);
+                        if (cell == null) {
+                            continue;
+                        }
+
+                        field.setAccessible(true);
+                        try {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                field.set(excelVo, cellValue.trim());
+                            }
+                        } catch (Exception e) {
+                            log.warn("读取字段{}失败:{}", headerName, e.getMessage());
+                        }
+                    }
+
+                    try {
+                        processImportData(excelVo, rowIndex + 1, errorMessages);
+                        successCount++;
+                    } catch (Exception e) {
+                        errorMessages.add(String.format("第%d行:%s", rowIndex + 1, e.getMessage()));
+                        log.error("导入第{}行数据失败", rowIndex + 1, e);
+                    }
+                }
+            }
+
+            StringBuilder message = new StringBuilder();
+            message.append(String.format("导入完成:成功%d条,失败%d条", successCount, totalCount - successCount));
+            if (!errorMessages.isEmpty()) {
+                message.append("\n失败详情:\n");
+                int maxErrors = Math.min(errorMessages.size(), 10);
+                for (int i = 0; i < maxErrors; i++) {
+                    message.append(errorMessages.get(i)).append("\n");
+                }
+                if (errorMessages.size() > 10) {
+                    message.append(String.format("...还有%d条错误未显示", errorMessages.size() - 10));
+                }
+            }
+
+            if (successCount == 0) {
+                return R.fail(message.toString());
+            } else {
+                return R.success(message.toString());
+            }
+        } catch (Exception e) {
+            log.error("导入店铺字典标签失败", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理导入数据
+     */
+    private void processImportData(DictionaryLibraryExcelVo excelVo, int rowIndex, List<String> errorMessages) {
+        if (StringUtils.isBlank(excelVo.getFirstLevelName())) {
+            throw new IllegalArgumentException("一级分类名称不能为空");
+        }
+
+        String firstLevelName = excelVo.getFirstLevelName().trim();
+        String secondLevelName = StringUtils.isNotBlank(excelVo.getSecondLevelName())
+                ? excelVo.getSecondLevelName().trim() : null;
+        String thirdLevelName = StringUtils.isNotBlank(excelVo.getThirdLevelName())
+                ? excelVo.getThirdLevelName().trim() : null;
+
+        if (StringUtils.isNotBlank(thirdLevelName)) {
+            createOrUpdateLevel(firstLevelName, secondLevelName, thirdLevelName, 3, excelVo.getHidden(), errorMessages, rowIndex);
+        } else if (StringUtils.isNotBlank(secondLevelName)) {
+            createOrUpdateLevel(firstLevelName, secondLevelName, null, 2, excelVo.getHidden(), errorMessages, rowIndex);
+        } else {
+            createOrUpdateLevel(firstLevelName, null, null, 1, excelVo.getHidden(), errorMessages, rowIndex);
+        }
+    }
+
+    /**
+     * 创建或更新分类级别
+     */
+    private void createOrUpdateLevel(String firstLevelName, String secondLevelName, String thirdLevelName,
+                                     int level, String hidden, List<String> errorMessages, int rowIndex) {
+        try {
+            StoreDictionary firstLevel = findOrCreateLevel(firstLevelName, null, 0, hidden);
+            if (firstLevel == null) {
+                throw new IllegalArgumentException("创建一级分类失败");
+            }
+            if (level >= 2 && StringUtils.isNotBlank(secondLevelName)) {
+                StoreDictionary secondLevel = findOrCreateLevel(secondLevelName, firstLevel.getId(), 1, hidden);
+                if (secondLevel == null) {
+                    throw new IllegalArgumentException("创建二级分类失败");
+                }
+
+                if (level >= 3 && StringUtils.isNotBlank(thirdLevelName)) {
+                    StoreDictionary thirdLevel = findOrCreateLevel(thirdLevelName, secondLevel.getId(), 2, hidden);
+                    if (thirdLevel == null) {
+                        throw new IllegalArgumentException("创建三级分类失败");
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+
+    /**
+     * 查找或创建分类
+     */
+    private StoreDictionary findOrCreateLevel(String dictDetail, Integer parentId, int expectedLevel, String hidden) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "store_tag", "store_tag_type", "store_tag_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.eq(StoreDictionary::getDictDetail, dictDetail);
+
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+
+        StoreDictionary existing = storeDictionaryMapper.selectOne(queryWrapper);
+
+        if (existing != null) {
+            return existing;
+        }
+
+        StoreDictionary newDict = new StoreDictionary();
+        newDict.setDictDetail(dictDetail);
+        // 一级为筛选条件,二级为筛选条件种类,三级为分类,四级为时间范围
+        if (expectedLevel == 0) {
+            newDict.setTypeDetail("门店标签");
+            newDict.setTypeName("store_tag");
+        } else if (expectedLevel == 1) {
+            newDict.setTypeDetail("门店标签种类");
+            newDict.setTypeName("store_tag_type");
+        } else if (expectedLevel == 2) {
+            newDict.setTypeDetail("门店标签分类");
+            newDict.setTypeName("store_tag_classify");
+        }
+        newDict.setParentId(parentId == null ? null : parentId);
+        newDict.setHidden(StringUtils.isNotBlank(hidden) && hidden.equals("隐藏") ? 1 : 0);
+        newDict.setDeleteFlag(0);
+        newDict.setCreatedTime(new Date());
+
+        String maxDictId = getMaxDictIdByTypeName(newDict.getTypeName());
+        newDict.setDictId(String.valueOf(maxDictId == null ? 1 : Integer.parseInt(maxDictId) + 1));
+
+        Integer maxSortId = getMaxSortIdByParentId(newDict.getParentId());
+        newDict.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        boolean saved = this.save(newDict);
+        return saved ? newDict : null;
+    }
+
+    private String buildTypeDetail(int expectedLevel) {
+        if (expectedLevel == 0) {
+            return "店铺标签一级";
+        } else if (expectedLevel == 1) {
+            return "店铺标签二级";
+        } else if (expectedLevel == 2) {
+            return "店铺标签三级";
+        }
+        return "店铺标签";
+    }
+
+    /**
+     * 获取单元格值(字符串格式)
+     */
+    private String getCellValueAsString(Cell cell) {
+        if (cell == null) {
+            return null;
+        }
+
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    return cell.getDateCellValue().toString();
+                } else {
+                    double numericValue = cell.getNumericCellValue();
+                    if (numericValue == (long) numericValue) {
+                        return String.valueOf((long) numericValue);
+                    } else {
+                        return String.valueOf(numericValue);
+                    }
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                return cell.getCellFormula();
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * 下载店铺字典标签导入模板
+     */
+    @Override
+    public void downloadTemplate(HttpServletResponse response) throws IOException {
+        log.info("DictStoreTagServiceImpl.downloadTemplate");
+
+        XSSFWorkbook workbook = new XSSFWorkbook();
+        try {
+            Sheet sheet = workbook.createSheet("店铺字典标签导入模板");
+
+            Field[] fields = DictionaryLibraryExcelVo.class.getDeclaredFields();
+            List<Field> excelFields = new ArrayList<>();
+            for (Field field : fields) {
+                if (field.isAnnotationPresent(ExcelHeader.class)) {
+                    excelFields.add(field);
+                }
+            }
+
+            Font headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerFont.setFontHeightInPoints((short) 12);
+            CellStyle headerCellStyle = workbook.createCellStyle();
+            headerCellStyle.setFont(headerFont);
+            headerCellStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+            headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerCellStyle.setBorderBottom(BorderStyle.THIN);
+            headerCellStyle.setBorderTop(BorderStyle.THIN);
+            headerCellStyle.setBorderLeft(BorderStyle.THIN);
+            headerCellStyle.setBorderRight(BorderStyle.THIN);
+
+            Row headerRow = sheet.createRow(0);
+            for (int i = 0; i < excelFields.size(); i++) {
+                Field field = excelFields.get(i);
+                ExcelHeader excelHeader = field.getAnnotation(ExcelHeader.class);
+                Cell cell = headerRow.createCell(i);
+                cell.setCellValue(excelHeader.value());
+                cell.setCellStyle(headerCellStyle);
+            }
+
+            sheet.setColumnWidth(0, 25 * 256);
+            sheet.setColumnWidth(1, 25 * 256);
+            sheet.setColumnWidth(2, 25 * 256);
+            sheet.setColumnWidth(3, 15 * 256);
+
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+            String fileName = URLEncoder.encode("店铺字典标签导入模板", "UTF-8").replaceAll("\\+", "%20");
+            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+
+            OutputStream outputStream = response.getOutputStream();
+            workbook.write(outputStream);
+            outputStream.flush();
+        } finally {
+            if (workbook != null) {
+                try {
+                    workbook.close();
+                } catch (IOException e) {
+                    log.error("关闭workbook失败", e);
+                }
+            }
+        }
+    }
+}
+
+

+ 955 - 0
alien-store/src/main/java/shop/alien/store/service/impl/DictionaryLibraryServiceImpl.java

@@ -0,0 +1,955 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.entity.store.excelVo.DictionaryLibraryExcelVo;
+import shop.alien.entity.store.excelVo.util.ExcelHeader;
+import shop.alien.mapper.StoreDictionaryMapper;
+import shop.alien.store.service.DictionaryLibraryService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 平台字典库管理ServiceImpl
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/01
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DictionaryLibraryServiceImpl extends ServiceImpl<StoreDictionaryMapper, StoreDictionary> implements DictionaryLibraryService {
+
+    private final StoreDictionaryMapper storeDictionaryMapper;
+
+    /**
+     * 查询字典库(三级树形结构)
+     *
+     * @return 字典库树形结构列表
+     */
+    @Override
+    public List<StoreDictionary> queryDictionaryLibraryTree() {
+        // 查询所有字典库数据
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.orderByAsc(StoreDictionary::getSortId);
+        List<StoreDictionary> storeDictionaryList = storeDictionaryMapper.selectList(queryWrapper);
+
+        // 构建三级树形结构
+        return buildTreeOptimized(storeDictionaryList);
+    }
+
+    /**
+     * 构建树形结构(优化版)
+     *
+     * @param flatList 扁平列表
+     * @return 树形结构列表
+     */
+    private List<StoreDictionary> buildTreeOptimized(List<StoreDictionary> flatList) {
+        if (flatList == null || flatList.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 创建三个存储结构
+        Map<Integer, StoreDictionary> nodeMap = new HashMap<>();  // ID到节点的映射
+        Map<Integer, List<StoreDictionary>> parentChildMap = new HashMap<>();  // 父ID到子节点列表的映射
+        List<StoreDictionary> result = new ArrayList<>();  // 结果列表
+
+        // 填充nodeMap和parentChildMap
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            Integer parentId = entity.getParentId();
+
+            // 存入节点映射
+            nodeMap.put(id, entity);
+
+            // 初始化子节点列表
+            entity.setStoreDictionaryList(new ArrayList<>());
+
+            // 如果是根节点(parentId为null或0),直接添加到结果
+            if (parentId == null || parentId == 0) {
+                result.add(entity);
+            } else {
+                // 否则,记录父子关系
+                parentChildMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(entity);
+            }
+        }
+
+        // 建立父子关系
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            if (parentChildMap.containsKey(id)) {
+                entity.getStoreDictionaryList().addAll(parentChildMap.get(id));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 新增字典库(支持一级、二级、三级)
+     *
+     * @param storeDictionary 字典库信息
+     * @return 新增结果
+     */
+    @Override
+    public boolean addDictionaryLibrary(StoreDictionary storeDictionary) {
+        // 参数校验
+        if (storeDictionary == null || StringUtils.isBlank(storeDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("字典库描述不能为空");
+        }
+
+        // 设置固定字段
+//        storeDictionary.setTypeName("dictionary_library");
+//        if (StringUtils.isBlank(storeDictionary.getTypeDetail())) {
+//            storeDictionary.setTypeDetail("字典库");
+//        }
+        storeDictionary.setDeleteFlag(0);
+        storeDictionary.setCreatedTime(new Date());
+
+        // 处理 parent_id:如果为 null,设置为 0(一级)
+        Integer parentId = storeDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        // 如果是二级或三级,验证父节点是否存在
+        if (parentId != 0) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+            if (parent == null || parent.getDeleteFlag() == 1) {
+                throw new IllegalArgumentException("父节点不存在或已删除");
+            }
+        }
+
+        // 生成 dict_id:根据 typeName 查询最大 dict_id,然后 +1
+        String typeName = storeDictionary.getTypeName();
+        if (StringUtils.isBlank(typeName)) {
+            throw new IllegalArgumentException("typeName不能为空");
+        }
+        String maxDictId = getMaxDictIdByTypeName(typeName);
+        int nextDictId = (maxDictId == null ? 1 : Integer.parseInt(maxDictId)) + 1;
+        storeDictionary.setDictId(String.valueOf(nextDictId));
+
+        // 生成 sort_id
+        Integer maxSortId = getMaxSortIdByParentId(parentId);
+        storeDictionary.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        // 保存
+        return this.save(storeDictionary);
+    }
+
+    /**
+     * 修改字典库(支持一级、二级、三级)
+     *
+     * @param storeDictionary 字典库信息
+     * @return 修改结果
+     */
+    @Override
+    public boolean updateDictionaryLibrary(StoreDictionary storeDictionary) {
+        // 参数校验
+        if (storeDictionary == null || storeDictionary.getId() == null) {
+            throw new IllegalArgumentException("ID不能为空");
+        }
+        if (StringUtils.isBlank(storeDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("字典库描述不能为空");
+        }
+
+        // 查询原记录
+        StoreDictionary existing = storeDictionaryMapper.selectById(storeDictionary.getId());
+        if (existing == null) {
+            throw new IllegalArgumentException("记录不存在");
+        }
+        if (existing.getDeleteFlag() == 1) {
+            throw new IllegalArgumentException("该记录已删除");
+        }
+
+        // 判断是否是一级或二级节点
+        Integer existingParentId = existing.getParentId();
+        boolean isFirstLevel = (existingParentId == null || existingParentId == 0);
+        boolean isSecondLevel = false;
+        
+        // 如果不是一级节点,判断是否是二级节点(父节点是一级节点)
+        if (!isFirstLevel) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(existingParentId);
+            if (parent != null) {
+                Integer grandParentId = parent.getParentId();
+                isSecondLevel = (grandParentId == null || grandParentId == 0);
+            }
+        }
+
+        // 处理 parent_id:如果为 null,设置为 0(一级)
+        Integer parentId = storeDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        // 标准化parentId用于比较(null转为0)
+        Integer normalizedParentId = (parentId == null) ? 0 : parentId;
+        Integer normalizedExistingParentId = (existingParentId == null) ? 0 : existingParentId;
+        Integer oldSortId = existing.getSortId();
+        Integer newSortId = storeDictionary.getSortId();
+        boolean parentIdChanged = !normalizedParentId.equals(normalizedExistingParentId);
+        
+        // 判断是否修改了关键字段(parent_id)
+        boolean keyFieldChanged = parentIdChanged;
+        
+        // 如果是一级或二级节点,且修改了关键字段(parent_id),检查是否有未删除的子节点
+        if ((isFirstLevel || isSecondLevel) && keyFieldChanged) {
+            boolean hasChildren = hasUndeletedChildren(existing.getId());
+            if (hasChildren) {
+                throw new IllegalArgumentException("该节点存在未删除的子节点,不允许修改关键字段(父节点)");
+            }
+        }
+        
+        // 如果修改了 parentId,需要验证新的父节点是否存在
+        if (parentIdChanged) {
+            if (parentId != 0) {
+                StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+                if (parent == null || parent.getDeleteFlag() == 1) {
+                    throw new IllegalArgumentException("父节点不存在或已删除");
+                }
+            }
+            
+            // 如果用户指定了新位置(newSortId不为空),使用用户指定的位置
+            // 否则,使用新节点下的最大+1
+            if (newSortId == null) {
+                Integer maxSortId = getMaxSortIdByParentId(parentId);
+                int nextSortId = (maxSortId == null ? 1 : maxSortId) + 1;
+                newSortId = nextSortId;
+                storeDictionary.setSortId(newSortId);
+            }
+            
+            // 原节点下,原位置之后的记录需要前移(dictId - 1)
+            adjustSortOrderForMoveOut(existing.getId(), normalizedExistingParentId, oldSortId);
+            
+            // 新节点下,如果指定了新位置,需要调整新节点下的排序
+            if (newSortId != null) {
+                try {
+                    int newOrder = newSortId;
+                    Integer maxSortId = getMaxSortIdByParentId(parentId);
+                    int maxOrder = (maxSortId == null ? 0 : maxSortId);
+                    // 如果新位置不是最大+1,需要调整新节点下的排序
+                    if (newOrder <= maxOrder) {
+                        adjustSortOrderForMoveIn(existing.getId(), normalizedParentId, newSortId);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析新sortId: {}", newSortId, e);
+                }
+            }
+        }
+
+        // 设置固定字段
+//        storeDictionary.setTypeName("dictionary_library");
+        // 如果没有指定删除标记,保持原有值;如果指定了,使用新值
+        Integer oldHidden = existing.getHidden();
+        Integer newHidden = storeDictionary.getHidden() == null ? oldHidden : storeDictionary.getHidden();
+        storeDictionary.setHidden(newHidden);
+        storeDictionary.setDeleteFlag(existing.getDeleteFlag()); // 保持原有删除标记
+        storeDictionary.setCreatedTime(existing.getCreatedTime()); // 保持原有创建时间
+        storeDictionary.setCreatedUserId(existing.getCreatedUserId()); // 保持原有创建人
+        storeDictionary.setUpdatedTime(new Date()); // 更新修改时间
+
+        // 处理排序逻辑:如果 parentId 没有改变,但 dictId 改变了,需要调整同级其他记录的排序
+        if (!parentIdChanged) {
+            // 如果 dictId 为空,保持原有值
+            if (newSortId == null) {
+                storeDictionary.setSortId(oldSortId);
+            } else if (!newSortId.equals(oldSortId)) {
+                // 调整同级其他记录的排序
+                adjustSortOrder(existing.getId(), normalizedParentId, oldSortId, newSortId);
+            }
+        }
+
+        // 更新当前节点
+        boolean updateResult = this.updateById(storeDictionary);
+        
+        // 如果是一级或二级节点,且hidden值发生了变化,同步更新所有子节点的hidden值
+        boolean hiddenChanged = (oldHidden == null && newHidden != null) || 
+                                (oldHidden != null && !oldHidden.equals(newHidden));
+        if (updateResult && (isFirstLevel || isSecondLevel) && hiddenChanged && newHidden != null) {
+            updateChildrenHidden(existing.getId(), newHidden);
+        }
+        
+        return updateResult;
+    }
+
+    /**
+     * 判断节点是否有未删除的子节点
+     *
+     * @param parentId 父节点ID
+     * @return 如果有未删除的子节点返回 true,否则返回 false
+     */
+    private boolean hasUndeletedChildren(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.last("LIMIT 1");
+        
+        StoreDictionary child = storeDictionaryMapper.selectOne(queryWrapper);
+        return child != null;
+    }
+
+    /**
+     * 递归更新子节点的显示/隐藏状态
+     * 当一级或二级节点的hidden值改变时,同步更新其所有子节点的hidden值
+     *
+     * @param parentId 父节点ID
+     * @param hidden 新的hidden值(0:不隐藏, 1:隐藏)
+     */
+    private void updateChildrenHidden(Integer parentId, Integer hidden) {
+        if (parentId == null || hidden == null) {
+            return;
+        }
+        
+        // 查询所有未删除的子节点
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        
+        List<StoreDictionary> children = storeDictionaryMapper.selectList(queryWrapper);
+        
+        if (children == null || children.isEmpty()) {
+            return;
+        }
+        
+        // 批量更新子节点的hidden值
+        Date updateTime = new Date();
+        for (StoreDictionary child : children) {
+            child.setHidden(hidden);
+            child.setUpdatedTime(updateTime);
+            storeDictionaryMapper.updateById(child);
+            
+            // 递归更新子节点的子节点(二级节点的子节点是三级节点)
+            updateChildrenHidden(child.getId(), hidden);
+        }
+    }
+
+    /**
+     * 根据 typeName 获取最大 dict_id
+     *
+     * @param typeName 类型名称
+     * @return 最大 dict_id,如果不存在则返回 null
+     */
+    private String getMaxDictIdByTypeName(String typeName) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreDictionary::getTypeName, typeName);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        
+        queryWrapper.orderByDesc(StoreDictionary::getDictId);
+        queryWrapper.last("LIMIT 1");
+        
+        StoreDictionary maxDict = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxDict != null ? maxDict.getDictId() : null;
+    }
+
+    /**
+     * 根据parentId获取最大的sortId
+     *
+     * @param parentId 父节点ID
+     * @return 最大的sortId,如果不存在则返回null
+     */
+    private Integer getMaxSortIdByParentId(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+        
+        queryWrapper.orderByDesc(StoreDictionary::getSortId);
+        queryWrapper.last("LIMIT 1");
+        
+        StoreDictionary maxDict = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxDict != null && maxDict.getSortId() != null ? maxDict.getSortId() : null;
+    }
+
+    /**
+     * 调整排序:当记录的dictId改变时,调整同级其他记录的排序
+     * 上升则顺推:如果新dictId < 原dictId,将原dictId到新dictId之间的记录的dictId都+1
+     * 下降则顺升:如果新dictId > 原dictId,将原dictId到新dictId之间的记录的dictId都-1
+     *
+     * @param currentId 当前记录ID
+     * @param parentId 父节点ID
+     * @param oldSortId 原sortId
+     * @param newSortId 新sortId
+     */
+    private void adjustSortOrder(Integer currentId, Integer parentId, Integer oldSortId, Integer newSortId) {
+        try {
+            int oldOrder = oldSortId;
+            int newOrder = newSortId;
+            
+            // 如果排序没有改变,直接返回
+            if (oldOrder == newOrder) {
+                return;
+            }
+            
+            // 查询同级所有记录(排除当前记录)
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+            
+            if (parentId == null || parentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, parentId);
+            }
+            
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+            
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+            
+            // 上升则顺推:新dictId < 原dictId
+            if (newOrder < oldOrder) {
+                // 将新dictId到原dictId之间的记录的dictId都+1
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder >= newOrder && siblingOrder < oldOrder) {
+                            sibling.setSortId(siblingOrder + 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            } 
+            // 下降则顺升:新dictId > 原dictId
+            else if (newOrder > oldOrder) {
+                // 将原dictId到新dictId之间的记录的dictId都-1
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder > oldOrder && siblingOrder <= newOrder) {
+                            sibling.setSortId(siblingOrder - 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("调整排序失败,sortId格式错误: oldSortId={}, newSortId={}", oldSortId, newSortId, e);
+        } catch (Exception e) {
+            log.error("调整排序失败", e);
+        }
+    }
+
+    /**
+     * 处理从原节点移出时的排序调整:原位置之后的记录需要前移(dictId - 1)
+     *
+     * @param currentId 当前记录ID
+     * @param oldParentId 原父节点ID
+     * @param oldSortId 原sortId
+     */
+    private void adjustSortOrderForMoveOut(Integer currentId, Integer oldParentId, Integer oldSortId) {
+        try {
+            int oldOrder = oldSortId;
+            
+            // 查询原节点同级所有记录(排除当前记录)
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+            
+            if (oldParentId == null || oldParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, oldParentId);
+            }
+            
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+            
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+            
+            // 原位置之后的记录需要前移(dictId - 1)
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder > oldOrder) {
+                        sibling.setSortId(siblingOrder - 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移出节点排序调整失败,sortId格式错误: oldSortId={}", oldSortId, e);
+        } catch (Exception e) {
+            log.error("移出节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 处理移入新节点时的排序调整:新位置之后的记录需要后移(dictId + 1)
+     *
+     * @param currentId 当前记录ID
+     * @param newParentId 新父节点ID
+     * @param newSortId 新sortId
+     */
+    private void adjustSortOrderForMoveIn(Integer currentId, Integer newParentId, Integer newSortId) {
+        try {
+            int newOrder = newSortId;
+            
+            // 查询新节点同级所有记录(排除当前记录)
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+            
+            if (newParentId == null || newParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, newParentId);
+            }
+            
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+            
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+            
+            // 新位置之后的记录需要后移(dictId + 1)
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder >= newOrder) {
+                        sibling.setSortId(siblingOrder + 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析dictId: {}", sibling.getDictId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移入节点排序调整失败,sortId格式错误: newSortId={}", newSortId, e);
+        } catch (Exception e) {
+            log.error("移入节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 批量导入字典库
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> importDictionaryLibrary(MultipartFile file) {
+        log.info("DictionaryLibraryServiceImpl.importDictionaryLibrary fileName={}", 
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            if (file == null || file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+
+            String fileName = file.getOriginalFilename();
+            if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
+                return R.fail("文件格式不正确,请上传Excel文件");
+            }
+
+            List<String> errorMessages = new ArrayList<>();
+            int successCount = 0;
+            int totalCount = 0;
+
+            // 使用POI读取Excel
+            try (InputStream inputStream = file.getInputStream();
+                 Workbook workbook = new XSSFWorkbook(inputStream)) {
+                Sheet sheet = workbook.getSheetAt(0);
+
+                // 获取表头
+                Row headerRow = sheet.getRow(5);
+                if (headerRow == null) {
+                    return R.fail("Excel文件格式不正确,缺少表头");
+                }
+
+                // 构建字段映射(表头名称 -> 列索引)
+                Map<String, Integer> headerMap = new HashMap<>();
+                Field[] fields = DictionaryLibraryExcelVo.class.getDeclaredFields();
+                for (int i = 0; i < headerRow.getLastCellNum(); i++) {
+                    Cell cell = headerRow.getCell(i);
+                    if (cell != null) {
+                        String headerName = getCellValueAsString(cell);
+                        if (StringUtils.isNotBlank(headerName)) {
+                            headerMap.put(headerName.trim(), i);
+                        }
+                    }
+                }
+
+                // 读取数据行
+                for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
+                    Row row = sheet.getRow(rowIndex);
+                    if (row == null) {
+                        continue;
+                    }
+
+                    // 检查是否为空行
+                    boolean isEmptyRow = true;
+                    for (int i = 0; i < row.getLastCellNum(); i++) {
+                        Cell cell = row.getCell(i);
+                        if (cell != null && cell.getCellType() != CellType.BLANK) {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                isEmptyRow = false;
+                                break;
+                            }
+                        }
+                    }
+                    if (isEmptyRow) {
+                        continue;
+                    }
+
+                    totalCount++;
+                    DictionaryLibraryExcelVo excelVo = new DictionaryLibraryExcelVo();
+
+                    // 读取每个字段
+                    for (Field field : fields) {
+                        if (!field.isAnnotationPresent(ExcelHeader.class)) {
+                            continue;
+                        }
+                        ExcelHeader excelHeader = field.getAnnotation(ExcelHeader.class);
+                        String headerName = excelHeader.value();
+                        Integer colIndex = headerMap.get(headerName);
+                        if (colIndex == null) {
+                            continue;
+                        }
+
+                        Cell cell = row.getCell(colIndex);
+                        if (cell == null) {
+                            continue;
+                        }
+
+                        field.setAccessible(true);
+                        try {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                field.set(excelVo, cellValue.trim());
+                            }
+                        } catch (Exception e) {
+                            log.warn("读取字段{}失败:{}", headerName, e.getMessage());
+                        }
+                    }
+
+                    // 处理导入数据
+                    try {
+                        processImportData(excelVo, rowIndex + 1, errorMessages);
+                        successCount++;
+                    } catch (Exception e) {
+                        errorMessages.add(String.format("第%d行:%s", rowIndex + 1, e.getMessage()));
+                        log.error("导入第{}行数据失败", rowIndex + 1, e);
+                    }
+                }
+            }
+
+            // 构建返回消息
+            StringBuilder message = new StringBuilder();
+            message.append(String.format("导入完成:成功%d条,失败%d条", successCount, totalCount - successCount));
+            if (!errorMessages.isEmpty()) {
+                message.append("\n失败详情:\n");
+                int maxErrors = Math.min(errorMessages.size(), 10); // 最多显示10条错误
+                for (int i = 0; i < maxErrors; i++) {
+                    message.append(errorMessages.get(i)).append("\n");
+                }
+                if (errorMessages.size() > 10) {
+                    message.append(String.format("...还有%d条错误未显示", errorMessages.size() - 10));
+                }
+            }
+
+            if (successCount == 0) {
+                return R.fail(message.toString());
+            } else if (totalCount - successCount > 0) {
+                return R.success(message.toString());
+            } else {
+                return R.success(message.toString());
+            }
+        } catch (Exception e) {
+            log.error("导入字典库失败", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 处理导入数据
+     *
+     * @param excelVo Excel数据对象
+     * @param rowIndex 行号
+     * @param errorMessages 错误消息列表
+     */
+    private void processImportData(DictionaryLibraryExcelVo excelVo, int rowIndex, List<String> errorMessages) {
+        // 校验必填字段
+        if (StringUtils.isBlank(excelVo.getFirstLevelName())) {
+            throw new IllegalArgumentException("一级分类名称不能为空");
+        }
+
+        String firstLevelName = excelVo.getFirstLevelName().trim();
+        String secondLevelName = StringUtils.isNotBlank(excelVo.getSecondLevelName()) 
+                ? excelVo.getSecondLevelName().trim() : null;
+        String thirdLevelName = StringUtils.isNotBlank(excelVo.getThirdLevelName()) 
+                ? excelVo.getThirdLevelName().trim() : null;
+
+        // 确定要创建的级别
+        if (StringUtils.isNotBlank(thirdLevelName)) {
+            // 创建三级分类
+            createOrUpdateLevel(firstLevelName, secondLevelName, thirdLevelName, 3, excelVo.getHidden(), errorMessages, rowIndex);
+        } else if (StringUtils.isNotBlank(secondLevelName)) {
+            // 创建二级分类
+            createOrUpdateLevel(firstLevelName, secondLevelName, null, 2, excelVo.getHidden(), errorMessages, rowIndex);
+        } else {
+            // 创建一级分类
+            createOrUpdateLevel(firstLevelName, null, null, 1, excelVo.getHidden(), errorMessages, rowIndex);
+        }
+    }
+
+    /**
+     * 创建或更新分类级别
+     *
+     * @param firstLevelName 一级分类名称
+     * @param secondLevelName 二级分类名称
+     * @param thirdLevelName 三级分类名称
+     * @param level 级别(1、2、3)
+     * @param hidden 状态
+     * @param errorMessages 错误消息列表
+     * @param rowIndex 行号
+     */
+    private void createOrUpdateLevel(String firstLevelName, String secondLevelName, String thirdLevelName,
+                                     int level, String hidden, List<String> errorMessages, int rowIndex) {
+        try {
+            // 查找或创建一级分类
+            StoreDictionary firstLevel = findOrCreateLevel(firstLevelName, null, 0, hidden);
+            if (firstLevel == null) {
+                throw new IllegalArgumentException("创建一级分类失败");
+            }
+            if (level >= 2 && StringUtils.isNotBlank(secondLevelName)) {
+                // 查找或创建二级分类
+                StoreDictionary secondLevel = findOrCreateLevel(secondLevelName, firstLevel.getId(), 1, hidden);
+                if (secondLevel == null) {
+                    throw new IllegalArgumentException("创建二级分类失败");
+                }
+
+                if (level >= 3 && StringUtils.isNotBlank(thirdLevelName)) {
+                    // 查找或创建三级分类
+                    StoreDictionary thirdLevel = findOrCreateLevel(thirdLevelName, secondLevel.getId(), 2, hidden);
+                    if (thirdLevel == null) {
+                        throw new IllegalArgumentException("创建三级分类失败");
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+
+    /**
+     * 查找或创建分类
+     *
+     * @param dictDetail 分类名称
+     * @param parentId 父节点ID
+     * @param expectedLevel 期望的级别(用于校验)
+     * @param hidden 状态
+     * @return 分类对象
+     */
+    private StoreDictionary findOrCreateLevel(String dictDetail, Integer parentId, int expectedLevel, String hidden) {
+        // 查找是否存在
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "report", "report_type", "report_classify");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.eq(StoreDictionary::getDictDetail, dictDetail);
+
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+        
+        StoreDictionary existing = storeDictionaryMapper.selectOne(queryWrapper);
+        
+        if (existing != null) {
+            return existing;
+        }
+
+        // 创建新分类
+        StoreDictionary newDict = new StoreDictionary();
+        newDict.setDictDetail(dictDetail);
+        // 一级为经营版块 ,二级为经营种类,三级为分类
+        if (expectedLevel == 0) {
+            newDict.setTypeDetail("举报");
+            newDict.setTypeName("report");
+        } else if (expectedLevel == 1) {
+            newDict.setTypeDetail("举报种类");
+            newDict.setTypeName("report_type");
+        } else if (expectedLevel == 2) {
+            newDict.setTypeDetail("分类");
+            newDict.setTypeName("report_classify");
+        }
+        newDict.setParentId(parentId == null ? null : parentId);
+        newDict.setHidden(StringUtils.isNotBlank(hidden) && hidden.equals("隐藏") ? 1 : 0);
+        newDict.setDeleteFlag(0);
+        newDict.setCreatedTime(new Date());
+
+        // 生成 dict_id
+        String maxDictId = getMaxDictIdByTypeName("dictionary_library");
+        newDict.setDictId(String.valueOf(maxDictId == null ? 1 : Integer.parseInt(maxDictId) + 1));
+        
+        // 生成 sort_id
+        Integer maxSortId = getMaxSortIdByParentId(newDict.getParentId());
+        newDict.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        boolean saved = this.save(newDict);
+        return saved ? newDict : null;
+    }
+
+    /**
+     * 获取单元格值(字符串格式)
+     *
+     * @param cell 单元格
+     * @return 字符串值
+     */
+    private String getCellValueAsString(Cell cell) {
+        if (cell == null) {
+            return null;
+        }
+        
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    return cell.getDateCellValue().toString();
+                } else {
+                    // 处理数字,避免科学计数法
+                    double numericValue = cell.getNumericCellValue();
+                    if (numericValue == (long) numericValue) {
+                        return String.valueOf((long) numericValue);
+                    } else {
+                        return String.valueOf(numericValue);
+                    }
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                return cell.getCellFormula();
+            default:
+                return null;
+        }
+    }
+
+    /**
+     * 下载字典库导入模板
+     *
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    @Override
+    public void downloadTemplate(HttpServletResponse response) throws IOException {
+        log.info("DictionaryLibraryServiceImpl.downloadTemplate");
+        
+        XSSFWorkbook workbook = new XSSFWorkbook();
+        try {
+            Sheet sheet = workbook.createSheet("字典库导入模板");
+
+            // 获取带有 @ExcelHeader 注解的字段
+            Field[] fields = DictionaryLibraryExcelVo.class.getDeclaredFields();
+            List<Field> excelFields = new ArrayList<>();
+            for (Field field : fields) {
+                if (field.isAnnotationPresent(ExcelHeader.class)) {
+                    excelFields.add(field);
+                }
+            }
+
+            // 创建表头样式
+            Font headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerFont.setFontHeightInPoints((short) 12);
+            CellStyle headerCellStyle = workbook.createCellStyle();
+            headerCellStyle.setFont(headerFont);
+            headerCellStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+            headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerCellStyle.setBorderBottom(BorderStyle.THIN);
+            headerCellStyle.setBorderTop(BorderStyle.THIN);
+            headerCellStyle.setBorderLeft(BorderStyle.THIN);
+            headerCellStyle.setBorderRight(BorderStyle.THIN);
+
+            // 创建表头
+            Row headerRow = sheet.createRow(0);
+            for (int i = 0; i < excelFields.size(); i++) {
+                Field field = excelFields.get(i);
+                ExcelHeader excelHeader = field.getAnnotation(ExcelHeader.class);
+                Cell cell = headerRow.createCell(i);
+                cell.setCellValue(excelHeader.value());
+                cell.setCellStyle(headerCellStyle);
+            }
+
+            // 设置列宽
+            sheet.setColumnWidth(0, 25 * 256); // 一级分类名称
+            sheet.setColumnWidth(1, 25 * 256); // 二级分类名称
+            sheet.setColumnWidth(2, 25 * 256); // 三级分类名称
+            sheet.setColumnWidth(3, 15 * 256); // 状态(隐藏/显示)
+
+            // 设置响应头(必须在获取输出流之前设置)
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+            String fileName = URLEncoder.encode("字典库导入模板", "UTF-8").replaceAll("\\+", "%20");
+            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+
+            // 输出到响应流
+            OutputStream outputStream = response.getOutputStream();
+            workbook.write(outputStream);
+            outputStream.flush();
+        } finally {
+            // 确保 workbook 被正确关闭
+            if (workbook != null) {
+                try {
+                    workbook.close();
+                } catch (IOException e) {
+                    log.error("关闭workbook失败", e);
+                }
+            }
+        }
+    }
+}
+

+ 980 - 0
alien-store/src/main/java/shop/alien/store/service/impl/FilterConditionServiceImpl.java

@@ -0,0 +1,980 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.poi.ss.usermodel.*;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.entity.store.StoreDictionary;
+import shop.alien.entity.store.excelVo.DictionaryLibraryExcelVo;
+import shop.alien.entity.store.excelVo.util.ExcelHeader;
+import shop.alien.mapper.StoreDictionaryMapper;
+import shop.alien.store.service.FilterConditionService;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Field;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 平台筛选条件管理ServiceImpl
+ *
+ * @author ssk
+ * @version 1.0
+ * @date 2025/01/01
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class FilterConditionServiceImpl extends ServiceImpl<StoreDictionaryMapper, StoreDictionary> implements FilterConditionService {
+
+    private final StoreDictionaryMapper storeDictionaryMapper;
+
+    /**
+     * 查询筛选条件(四级树形结构)
+     *
+     * @return 筛选条件树形结构列表
+     */
+    @Override
+    public List<StoreDictionary> queryFilterConditionTree() {
+        // 查询所有筛选条件数据
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.orderByAsc(StoreDictionary::getSortId);
+        List<StoreDictionary> filterConditionList = storeDictionaryMapper.selectList(queryWrapper);
+
+        // 构建四级树形结构
+        return buildTreeOptimized(filterConditionList);
+    }
+
+    /**
+     * 构建树形结构(优化版)
+     *
+     * @param flatList 扁平列表
+     * @return 树形结构列表
+     */
+    private List<StoreDictionary> buildTreeOptimized(List<StoreDictionary> flatList) {
+        if (flatList == null || flatList.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 创建三个存储结构
+        Map<Integer, StoreDictionary> nodeMap = new HashMap<>();  // ID到节点的映射
+        Map<Integer, List<StoreDictionary>> parentChildMap = new HashMap<>();  // 父ID到子节点列表的映射
+        List<StoreDictionary> result = new ArrayList<>();  // 结果列表
+
+        // 填充nodeMap和parentChildMap
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            Integer parentId = entity.getParentId();
+
+            // 存入节点映射
+            nodeMap.put(id, entity);
+
+            // 初始化子节点列表
+            entity.setStoreDictionaryList(new ArrayList<>());
+
+            // 如果是根节点(parentId为null或0),直接添加到结果
+            if (parentId == null || parentId == 0) {
+                result.add(entity);
+            } else {
+                // 否则,记录父子关系
+                parentChildMap.computeIfAbsent(parentId, k -> new ArrayList<>()).add(entity);
+            }
+        }
+
+        // 建立父子关系
+        for (StoreDictionary entity : flatList) {
+            Integer id = entity.getId();
+            if (parentChildMap.containsKey(id)) {
+                entity.getStoreDictionaryList().addAll(parentChildMap.get(id));
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * 新增筛选条件(支持一级、二级、三级、四级)
+     *
+     * @param StoreDictionary 筛选条件信息
+     * @return 新增结果
+     */
+    @Override
+    public boolean addFilterCondition(StoreDictionary StoreDictionary) {
+        // 参数校验
+        if (StoreDictionary == null || StringUtils.isBlank(StoreDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("筛选条件描述不能为空");
+        }
+
+        // 设置固定字段
+        StoreDictionary.setDeleteFlag(0);
+        StoreDictionary.setCreatedTime(new Date());
+
+        // 处理 parent_id:如果为 null,设置为 0(一级)
+        Integer parentId = StoreDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        // 如果是二级或三级,验证父节点是否存在
+        if (parentId != 0) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+            if (parent == null || parent.getDeleteFlag() == 1) {
+                throw new IllegalArgumentException("父节点不存在或已删除");
+            }
+        }
+
+        // 生成 condition_id:根据 typeName 查询最大 condition_id,然后 +1
+        String typeName = StoreDictionary.getTypeName();
+        if (StringUtils.isBlank(typeName)) {
+            throw new IllegalArgumentException("typeName不能为空");
+        }
+        String maxConditionId = getMaxConditionIdByTypeName(typeName);
+        int nextConditionId = (maxConditionId == null ? 1 : Integer.parseInt(maxConditionId)) + 1;
+        StoreDictionary.setDictId(String.valueOf(nextConditionId));
+
+        // 生成 sort_id
+        Integer maxSortId = getMaxSortIdByParentId(parentId);
+        StoreDictionary.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        // 保存
+        return this.save(StoreDictionary);
+    }
+
+    /**
+     * 修改筛选条件(支持一级、二级、三级、四级)
+     *
+     * @param StoreDictionary 筛选条件信息
+     * @return 修改结果
+     */
+    @Override
+    public boolean updateFilterCondition(StoreDictionary StoreDictionary) {
+        // 参数校验
+        if (StoreDictionary == null || StoreDictionary.getId() == null) {
+            throw new IllegalArgumentException("ID不能为空");
+        }
+        if (StringUtils.isBlank(StoreDictionary.getDictDetail())) {
+            throw new IllegalArgumentException("筛选条件描述不能为空");
+        }
+
+        // 查询原记录
+        StoreDictionary existing = storeDictionaryMapper.selectById(StoreDictionary.getId());
+        if (existing == null) {
+            throw new IllegalArgumentException("记录不存在");
+        }
+        if (existing.getDeleteFlag() == 1) {
+            throw new IllegalArgumentException("该记录已删除");
+        }
+
+        // 判断是否是一级或二级节点
+        Integer existingParentId = existing.getParentId();
+        boolean isFirstLevel = (existingParentId == null || existingParentId == 0);
+        boolean isSecondLevel = false;
+        
+        // 如果不是一级节点,判断是否是二级节点(父节点是一级节点)
+        if (!isFirstLevel) {
+            StoreDictionary parent = storeDictionaryMapper.selectById(existingParentId);
+            if (parent != null) {
+                Integer grandParentId = parent.getParentId();
+                isSecondLevel = (grandParentId == null || grandParentId == 0);
+            }
+        }
+
+        // 处理 parent_id:如果为 null,设置为 0(一级)
+        Integer parentId = StoreDictionary.getParentId();
+        if (parentId == null) {
+            parentId = 0;
+        }
+
+        // 标准化parentId用于比较(null转为0)
+        Integer normalizedParentId = (parentId == null) ? 0 : parentId;
+        Integer normalizedExistingParentId = (existingParentId == null) ? 0 : existingParentId;
+        Integer oldSortId = existing.getSortId();
+        Integer newSortId = StoreDictionary.getSortId();
+        boolean parentIdChanged = !normalizedParentId.equals(normalizedExistingParentId);
+        
+        // 判断是否修改了关键字段(parent_id)
+        boolean keyFieldChanged = parentIdChanged;
+        
+        // 如果是一级或二级节点,且修改了关键字段(parent_id),检查是否有未删除的子节点
+        if ((isFirstLevel || isSecondLevel) && keyFieldChanged) {
+            boolean hasChildren = hasUndeletedChildren(existing.getId());
+            if (hasChildren) {
+                throw new IllegalArgumentException("该节点存在未删除的子节点,不允许修改关键字段(父节点)");
+            }
+        }
+        
+        // 如果修改了 parentId,需要验证新的父节点是否存在
+        if (parentIdChanged) {
+            if (parentId != 0) {
+                StoreDictionary parent = storeDictionaryMapper.selectById(parentId);
+                if (parent == null || parent.getDeleteFlag() == 1) {
+                    throw new IllegalArgumentException("父节点不存在或已删除");
+                }
+            }
+            
+            // 如果用户指定了新位置(newSortId不为空),使用用户指定的位置
+            // 否则,使用新节点下的最大+1
+            if (newSortId == null) {
+                Integer maxSortId = getMaxSortIdByParentId(parentId);
+                int nextSortId = (maxSortId == null ? 1 : maxSortId) + 1;
+                newSortId = nextSortId;
+                StoreDictionary.setSortId(newSortId);
+            }
+            
+            // 原节点下,原位置之后的记录需要前移(sortId - 1)
+            adjustSortOrderForMoveOut(existing.getId(), normalizedExistingParentId, oldSortId);
+            
+            // 新节点下,如果指定了新位置,需要调整新节点下的排序
+            if (newSortId != null) {
+                try {
+                    int newOrder = newSortId;
+                    Integer maxSortId = getMaxSortIdByParentId(parentId);
+                    int maxOrder = (maxSortId == null ? 0 : maxSortId);
+                    // 如果新位置不是最大+1,需要调整新节点下的排序
+                    if (newOrder <= maxOrder) {
+                        adjustSortOrderForMoveIn(existing.getId(), normalizedParentId, newSortId);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析新sortId: {}", newSortId, e);
+                }
+            }
+        }
+
+        // 设置固定字段
+        // 如果没有指定删除标记,保持原有值;如果指定了,使用新值
+        Integer oldHidden = existing.getHidden();
+        Integer newHidden = StoreDictionary.getHidden() == null ? oldHidden : StoreDictionary.getHidden();
+        StoreDictionary.setHidden(newHidden);
+        StoreDictionary.setDeleteFlag(existing.getDeleteFlag()); // 保持原有删除标记
+        StoreDictionary.setCreatedTime(existing.getCreatedTime()); // 保持原有创建时间
+        StoreDictionary.setCreatedUserId(existing.getCreatedUserId()); // 保持原有创建人
+        StoreDictionary.setUpdatedTime(new Date()); // 更新修改时间
+
+        // 处理排序逻辑:如果 parentId 没有改变,但 sortId 改变了,需要调整同级其他记录的排序
+        if (!parentIdChanged) {
+            // 如果 sortId 为空,保持原有值
+            if (newSortId == null) {
+                StoreDictionary.setSortId(oldSortId);
+            } else if (!newSortId.equals(oldSortId)) {
+                // 调整同级其他记录的排序
+                adjustSortOrder(existing.getId(), normalizedParentId, oldSortId, newSortId);
+            }
+        }
+
+        // 更新当前节点
+        boolean updateResult = this.updateById(StoreDictionary);
+        
+        // 如果是一级或二级节点,且hidden值发生了变化,同步更新所有子节点的hidden值
+        boolean hiddenChanged = (oldHidden == null && newHidden != null) || 
+                                (oldHidden != null && !oldHidden.equals(newHidden));
+        if (updateResult && (isFirstLevel || isSecondLevel) && hiddenChanged && newHidden != null) {
+            updateChildrenHidden(existing.getId(), newHidden);
+        }
+        
+        return updateResult;
+    }
+
+    /**
+     * 判断节点是否有未删除的子节点
+     *
+     * @param parentId 父节点ID
+     * @return 如果有未删除的子节点返回 true,否则返回 false
+     */
+    private boolean hasUndeletedChildren(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.last("LIMIT 1");
+        
+        StoreDictionary child = storeDictionaryMapper.selectOne(queryWrapper);
+        return child != null;
+    }
+
+    /**
+     * 递归更新子节点的显示/隐藏状态
+     * 当一级或二级节点的hidden值改变时,同步更新其所有子节点的hidden值
+     *
+     * @param parentId 父节点ID
+     * @param hidden 新的hidden值(0:不隐藏, 1:隐藏)
+     */
+    private void updateChildrenHidden(Integer parentId, Integer hidden) {
+        if (parentId == null || hidden == null) {
+            return;
+        }
+        
+        // 查询所有未删除的子节点
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+        queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        
+        List<StoreDictionary> children = storeDictionaryMapper.selectList(queryWrapper);
+        
+        if (children == null || children.isEmpty()) {
+            return;
+        }
+        
+        // 批量更新子节点的hidden值
+        Date updateTime = new Date();
+        for (StoreDictionary child : children) {
+            child.setHidden(hidden);
+            child.setUpdatedTime(updateTime);
+            storeDictionaryMapper.updateById(child);
+            
+            // 递归更新子节点的子节点(二级节点的子节点是三级节点)
+            updateChildrenHidden(child.getId(), hidden);
+        }
+    }
+
+    /**
+     * 根据 typeName 获取最大 condition_id
+     *
+     * @param typeName 类型名称
+     * @return 最大 condition_id,如果不存在则返回 null
+     */
+    private String getMaxConditionIdByTypeName(String typeName) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreDictionary::getTypeName, typeName);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        
+        queryWrapper.orderByDesc(StoreDictionary::getDictId);
+        queryWrapper.last("LIMIT 1");
+        
+        StoreDictionary maxCondition = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxCondition != null ? maxCondition.getDictId() : null;
+    }
+
+    /**
+     * 根据parentId获取最大的sortId
+     *
+     * @param parentId 父节点ID
+     * @return 最大的sortId,如果不存在则返回null
+     */
+    private Integer getMaxSortIdByParentId(Integer parentId) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+        
+        queryWrapper.orderByDesc(StoreDictionary::getSortId);
+        queryWrapper.last("LIMIT 1");
+        
+        StoreDictionary maxCondition = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxCondition != null && maxCondition.getSortId() != null ? maxCondition.getSortId() : null;
+    }
+
+    /**
+     * 调整排序:当记录的sortId改变时,调整同级其他记录的排序
+     * 上升则顺推:如果新sortId < 原sortId,将原sortId到新sortId之间的记录的sortId都+1
+     * 下降则顺升:如果新sortId > 原sortId,将原sortId到新sortId之间的记录的sortId都-1
+     *
+     * @param currentId 当前记录ID
+     * @param parentId 父节点ID
+     * @param oldSortId 原sortId
+     * @param newSortId 新sortId
+     */
+    private void adjustSortOrder(Integer currentId, Integer parentId, Integer oldSortId, Integer newSortId) {
+        try {
+            int oldOrder = oldSortId;
+            int newOrder = newSortId;
+            
+            // 如果排序没有改变,直接返回
+            if (oldOrder == newOrder) {
+                return;
+            }
+            
+            // 查询同级所有记录(排除当前记录)
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+            
+            if (parentId == null || parentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, parentId);
+            }
+            
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+            
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+            
+            // 上升则顺推:新sortId < 原sortId
+            if (newOrder < oldOrder) {
+                // 将新sortId到原sortId之间的记录的sortId都+1
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder >= newOrder && siblingOrder < oldOrder) {
+                            sibling.setSortId(siblingOrder + 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            } 
+            // 下降则顺升:新sortId > 原sortId
+            else if (newOrder > oldOrder) {
+                // 将原sortId到新sortId之间的记录的sortId都-1
+                for (StoreDictionary sibling : siblings) {
+                    try {
+                        int siblingOrder = sibling.getSortId();
+                        if (siblingOrder > oldOrder && siblingOrder <= newOrder) {
+                            sibling.setSortId(siblingOrder - 1);
+                            sibling.setUpdatedTime(new Date());
+                            storeDictionaryMapper.updateById(sibling);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                    }
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("调整排序失败,sortId格式错误: oldSortId={}, newSortId={}", oldSortId, newSortId, e);
+        } catch (Exception e) {
+            log.error("调整排序失败", e);
+        }
+    }
+
+    /**
+     * 处理从原节点移出时的排序调整:原位置之后的记录需要前移(sortId - 1)
+     *
+     * @param currentId 当前记录ID
+     * @param oldParentId 原父节点ID
+     * @param oldSortId 原sortId
+     */
+    private void adjustSortOrderForMoveOut(Integer currentId, Integer oldParentId, Integer oldSortId) {
+        try {
+            int oldOrder = oldSortId;
+            
+            // 查询原节点同级所有记录(排除当前记录)
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+            
+            if (oldParentId == null || oldParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, oldParentId);
+            }
+            
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+            
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+            
+            // 原位置之后的记录需要前移(sortId - 1)
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder > oldOrder) {
+                        sibling.setSortId(siblingOrder - 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移出节点排序调整失败,sortId格式错误: oldSortId={}", oldSortId, e);
+        } catch (Exception e) {
+            log.error("移出节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 处理移入新节点时的排序调整:新位置之后的记录需要后移(sortId + 1)
+     *
+     * @param currentId 当前记录ID
+     * @param newParentId 新父节点ID
+     * @param newSortId 新sortId
+     */
+    private void adjustSortOrderForMoveIn(Integer currentId, Integer newParentId, Integer newSortId) {
+        try {
+            int newOrder = newSortId;
+            
+            // 查询新节点同级所有记录(排除当前记录)
+            LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+            queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+            queryWrapper.ne(StoreDictionary::getId, currentId);
+            
+            if (newParentId == null || newParentId == 0) {
+                queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                        .or().eq(StoreDictionary::getParentId, 0));
+            } else {
+                queryWrapper.eq(StoreDictionary::getParentId, newParentId);
+            }
+            
+            List<StoreDictionary> siblings = storeDictionaryMapper.selectList(queryWrapper);
+            
+            if (siblings == null || siblings.isEmpty()) {
+                return;
+            }
+            
+            // 新位置之后的记录需要后移(sortId + 1)
+            for (StoreDictionary sibling : siblings) {
+                try {
+                    int siblingOrder = sibling.getSortId();
+                    if (siblingOrder >= newOrder) {
+                        sibling.setSortId(siblingOrder + 1);
+                        sibling.setUpdatedTime(new Date());
+                        storeDictionaryMapper.updateById(sibling);
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("无法解析sortId: {}", sibling.getSortId(), e);
+                }
+            }
+        } catch (NumberFormatException e) {
+            log.error("移入节点排序调整失败,sortId格式错误: newSortId={}", newSortId, e);
+        } catch (Exception e) {
+            log.error("移入节点排序调整失败", e);
+        }
+    }
+
+    /**
+     * 批量导入筛选条件
+     *
+     * @param file Excel文件
+     * @return 导入结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public R<String> importFilterCondition(MultipartFile file)  {
+        log.info("PlatformBusinessSectionServiceImpl.importBusinessSection fileName={}",
+                file != null ? file.getOriginalFilename() : "null");
+        try {
+            if (file == null || file.isEmpty()) {
+                return R.fail("文件不能为空");
+            }
+
+            String fileName = file.getOriginalFilename();
+            if (fileName == null || (!fileName.endsWith(".xlsx") && !fileName.endsWith(".xls"))) {
+                return R.fail("文件格式不正确,请上传Excel文件");
+            }
+
+            List<String> errorMessages = new ArrayList<>();
+            int successCount = 0;
+            int totalCount = 0;
+
+            // 使用POI读取Excel
+            try (InputStream inputStream = file.getInputStream();
+                 Workbook workbook = new XSSFWorkbook(inputStream)) {
+                Sheet sheet = workbook.getSheetAt(0);
+
+                // 获取表头
+                Row headerRow = sheet.getRow(5);
+                if (headerRow == null) {
+                    return R.fail("Excel文件格式不正确,缺少表头");
+                }
+
+                // 构建字段映射(表头名称 -> 列索引)
+                Map<String, Integer> headerMap = new HashMap<>();
+                Field[] fields = DictionaryLibraryExcelVo.class.getDeclaredFields();
+                for (int i = 0; i < headerRow.getLastCellNum(); i++) {
+                    Cell cell = headerRow.getCell(i);
+                    if (cell != null) {
+                        String headerName = getCellValueAsString(cell);
+                        if (StringUtils.isNotBlank(headerName)) {
+                            headerMap.put(headerName.trim(), i);
+                        }
+                    }
+                }
+
+                // 读取数据行
+                for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++) {
+                    Row row = sheet.getRow(rowIndex);
+                    if (row == null) {
+                        continue;
+                    }
+
+                    // 检查是否为空行
+                    boolean isEmptyRow = true;
+                    for (int i = 0; i < row.getLastCellNum(); i++) {
+                        Cell cell = row.getCell(i);
+                        if (cell != null && cell.getCellType() != CellType.BLANK) {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                isEmptyRow = false;
+                                break;
+                            }
+                        }
+                    }
+                    if (isEmptyRow) {
+                        continue;
+                    }
+
+                    totalCount++;
+                    DictionaryLibraryExcelVo excelVo = new DictionaryLibraryExcelVo();
+
+                    // 读取每个字段
+                    for (Field field : fields) {
+                        if (!field.isAnnotationPresent(ExcelHeader.class)) {
+                            continue;
+                        }
+                        ExcelHeader excelHeader = field.getAnnotation(ExcelHeader.class);
+                        String headerName = excelHeader.value();
+                        Integer colIndex = headerMap.get(headerName);
+                        if (colIndex == null) {
+                            continue;
+                        }
+
+                        Cell cell = row.getCell(colIndex);
+                        if (cell == null) {
+                            continue;
+                        }
+
+                        field.setAccessible(true);
+                        try {
+                            String cellValue = getCellValueAsString(cell);
+                            if (StringUtils.isNotBlank(cellValue)) {
+                                field.set(excelVo, cellValue.trim());
+                            }
+                        } catch (Exception e) {
+                            log.warn("读取字段{}失败:{}", headerName, e.getMessage());
+                        }
+                    }
+
+                    // 处理导入数据
+                    try {
+                        processImportData(excelVo, rowIndex + 1, errorMessages);
+                        successCount++;
+                    } catch (Exception e) {
+                        errorMessages.add(String.format("第%d行:%s", rowIndex + 1, e.getMessage()));
+                        log.error("导入第{}行数据失败", rowIndex + 1, e);
+                    }
+                }
+            }
+
+            // 构建返回消息
+            StringBuilder message = new StringBuilder();
+            message.append(String.format("导入完成:成功%d条,失败%d条", successCount, totalCount - successCount));
+            if (!errorMessages.isEmpty()) {
+                message.append("\n失败详情:\n");
+                int maxErrors = Math.min(errorMessages.size(), 10); // 最多显示10条错误
+                for (int i = 0; i < maxErrors; i++) {
+                    message.append(errorMessages.get(i)).append("\n");
+                }
+                if (errorMessages.size() > 10) {
+                    message.append(String.format("...还有%d条错误未显示", errorMessages.size() - 10));
+                }
+            }
+
+            if (successCount == 0) {
+                return R.fail(message.toString());
+            } else if (totalCount - successCount > 0) {
+                return R.success(message.toString());
+            } else {
+                return R.success(message.toString());
+            }
+        } catch (Exception e) {
+            log.error("导入经营版块失败", e);
+            return R.fail("导入失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取单元格值(字符串格式)
+     *
+     * @param cell 单元格
+     * @return 字符串值
+     */
+    private String getCellValueAsString(Cell cell) {
+        if (cell == null) {
+            return null;
+        }
+
+        switch (cell.getCellType()) {
+            case STRING:
+                return cell.getStringCellValue();
+            case NUMERIC:
+                if (DateUtil.isCellDateFormatted(cell)) {
+                    return cell.getDateCellValue().toString();
+                } else {
+                    // 处理数字,避免科学计数法
+                    double numericValue = cell.getNumericCellValue();
+                    if (numericValue == (long) numericValue) {
+                        return String.valueOf((long) numericValue);
+                    } else {
+                        return String.valueOf(numericValue);
+                    }
+                }
+            case BOOLEAN:
+                return String.valueOf(cell.getBooleanCellValue());
+            case FORMULA:
+                return cell.getCellFormula();
+            default:
+                return null;
+        }
+    }
+
+
+    /**
+     * 处理导入数据
+     *
+     * @param excelVo Excel数据对象
+     * @param rowIndex 行号
+     * @param errorMessages 错误消息列表
+     */
+    private void processImportData(DictionaryLibraryExcelVo excelVo, int rowIndex, List<String> errorMessages) {
+        // 校验必填字段
+        if (StringUtils.isBlank(excelVo.getFirstLevelName())) {
+            throw new IllegalArgumentException("一级分类名称不能为空");
+        }
+
+        String firstLevelName = excelVo.getFirstLevelName().trim();
+        String secondLevelName = StringUtils.isNotBlank(excelVo.getSecondLevelName())
+                ? excelVo.getSecondLevelName().trim() : null;
+        String thirdLevelName = StringUtils.isNotBlank(excelVo.getThirdLevelName())
+                ? excelVo.getThirdLevelName().trim() : null;
+        String fourLevelName = StringUtils.isNotBlank(excelVo.getFourLevelName())
+                ? excelVo.getFourLevelName().trim() : null;
+
+        // 确定要创建的级别
+        if (StringUtils.isNotBlank(fourLevelName)) {
+            // 创建四级分类
+            createOrUpdateLevel(firstLevelName, secondLevelName, thirdLevelName, fourLevelName, 4, excelVo.getHidden(), errorMessages, rowIndex);
+        } else if (StringUtils.isNotBlank(thirdLevelName)) {
+            // 创建三级分类
+            createOrUpdateLevel(firstLevelName, secondLevelName, thirdLevelName, null, 3, excelVo.getHidden(), errorMessages, rowIndex);
+        } else if (StringUtils.isNotBlank(secondLevelName)) {
+            // 创建二级分类
+            createOrUpdateLevel(firstLevelName, secondLevelName, null, null, 2, excelVo.getHidden(), errorMessages, rowIndex);
+        } else {
+            // 创建一级分类
+            createOrUpdateLevel(firstLevelName, null, null, null, 1, excelVo.getHidden(), errorMessages, rowIndex);
+        }
+    }
+
+
+    /**
+     * 下载筛选条件导入模板
+     *
+     * @param response HTTP响应
+     * @throws IOException IO异常
+     */
+    @Override
+    public void downloadTemplate(HttpServletResponse response) throws IOException {
+        log.info("FilterConditionServiceImpl.downloadTemplate");
+        
+        XSSFWorkbook workbook = new XSSFWorkbook();
+        try {
+            Sheet sheet = workbook.createSheet("筛选条件导入模板");
+
+            // 创建表头样式
+            Font headerFont = workbook.createFont();
+            headerFont.setBold(true);
+            headerFont.setFontHeightInPoints((short) 12);
+            CellStyle headerCellStyle = workbook.createCellStyle();
+            headerCellStyle.setFont(headerFont);
+            headerCellStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerCellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+            headerCellStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
+            headerCellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
+            headerCellStyle.setBorderBottom(BorderStyle.THIN);
+            headerCellStyle.setBorderTop(BorderStyle.THIN);
+            headerCellStyle.setBorderLeft(BorderStyle.THIN);
+            headerCellStyle.setBorderRight(BorderStyle.THIN);
+
+            // 创建表头
+            Row headerRow = sheet.createRow(0);
+            String[] headers = {"一级分类名称", "二级分类名称", "三级分类名称", "四级分类名称", "状态(隐藏/显示)"};
+            for (int i = 0; i < headers.length; i++) {
+                Cell cell = headerRow.createCell(i);
+                cell.setCellValue(headers[i]);
+                cell.setCellStyle(headerCellStyle);
+            }
+
+            // 设置列宽
+            sheet.setColumnWidth(0, 25 * 256); // 一级分类名称
+            sheet.setColumnWidth(1, 25 * 256); // 二级分类名称
+            sheet.setColumnWidth(2, 25 * 256); // 三级分类名称
+            sheet.setColumnWidth(3, 25 * 256); // 四级分类名称
+            sheet.setColumnWidth(4, 15 * 256); // 状态(隐藏/显示)
+
+            // 设置响应头(必须在获取输出流之前设置)
+            response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+            response.setCharacterEncoding("utf-8");
+            String fileName = URLEncoder.encode("筛选条件导入模板", "UTF-8").replaceAll("\\+", "%20");
+            response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName + ".xlsx");
+
+            // 输出到响应流
+            OutputStream outputStream = response.getOutputStream();
+            workbook.write(outputStream);
+            outputStream.flush();
+        } finally {
+            // 确保 workbook 被正确关闭
+            if (workbook != null) {
+                try {
+                    workbook.close();
+                } catch (IOException e) {
+                    log.error("关闭workbook失败", e);
+                }
+            }
+        }
+    }
+
+
+    /**
+     * 创建或更新分类级别
+     *
+     * @param firstLevelName 一级分类名称
+     * @param secondLevelName 二级分类名称
+     * @param thirdLevelName 三级分类名称
+     * @param fourLevelName 四级分类名称
+     * @param level 级别(1、2、3、4)
+     * @param hidden 状态
+     * @param errorMessages 错误消息列表
+     * @param rowIndex 行号
+     */
+    private void createOrUpdateLevel(String firstLevelName, String secondLevelName, String thirdLevelName,
+                                     String fourLevelName, int level, String hidden, List<String> errorMessages, int rowIndex) {
+        try {
+            // 查找或创建一级分类
+            StoreDictionary firstLevel = findOrCreateLevel(firstLevelName, null, 0, hidden);
+            if (firstLevel == null) {
+                throw new IllegalArgumentException("创建一级分类失败");
+            }
+            if (level >= 2 && StringUtils.isNotBlank(secondLevelName)) {
+                // 查找或创建二级分类
+                StoreDictionary secondLevel = findOrCreateLevel(secondLevelName, firstLevel.getId(), 1, hidden);
+                if (secondLevel == null) {
+                    throw new IllegalArgumentException("创建二级分类失败");
+                }
+
+                if (level >= 3 && StringUtils.isNotBlank(thirdLevelName)) {
+                    // 查找或创建三级分类
+                    StoreDictionary thirdLevel = findOrCreateLevel(thirdLevelName, secondLevel.getId(), 2, hidden);
+                    if (thirdLevel == null) {
+                        throw new IllegalArgumentException("创建三级分类失败");
+                    }
+
+                    if (level >= 4 && StringUtils.isNotBlank(fourLevelName)) {
+                        // 查找或创建四级分类
+                        StoreDictionary fourLevel = findOrCreateLevel(fourLevelName, thirdLevel.getId(), 3, hidden);
+                        if (fourLevel == null) {
+                            throw new IllegalArgumentException("创建四级分类失败");
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new IllegalArgumentException(e.getMessage());
+        }
+    }
+
+    /**
+     * 查找或创建分类
+     *
+     * @param dictDetail 分类名称
+     * @param parentId 父节点ID
+     * @param expectedLevel 期望的级别(用于校验)
+     * @param hidden 状态
+     * @return 分类对象
+     */
+    private StoreDictionary findOrCreateLevel(String dictDetail, Integer parentId, int expectedLevel, String hidden) {
+        // 查找是否存在
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.in(StoreDictionary::getTypeName, "filter_condition","filter_condition_type","filter_condition_classify", "filter_condition_timeRange");
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+        queryWrapper.eq(StoreDictionary::getDictDetail, dictDetail);
+
+        if (parentId == null || parentId == 0) {
+            queryWrapper.and(wrapper -> wrapper.isNull(StoreDictionary::getParentId)
+                    .or().eq(StoreDictionary::getParentId, 0));
+        } else {
+            queryWrapper.eq(StoreDictionary::getParentId, parentId);
+        }
+
+        StoreDictionary existing = storeDictionaryMapper.selectOne(queryWrapper);
+
+        if (existing != null) {
+            return existing;
+        }
+
+        // 创建新分类
+        StoreDictionary newDict = new StoreDictionary();
+        newDict.setDictDetail(dictDetail);
+        // 一级为筛选条件,二级为筛选条件种类,三级为分类,四级为时间范围
+        if (expectedLevel == 0) {
+            newDict.setTypeDetail("筛选条件");
+            newDict.setTypeName("filter_condition");
+        } else if (expectedLevel == 1) {
+            newDict.setTypeDetail("筛选条件种类");
+            newDict.setTypeName("filter_condition_type");
+        } else if (expectedLevel == 2) {
+            newDict.setTypeDetail("分类");
+            newDict.setTypeName("filter_condition_classify");
+        } else if (expectedLevel == 3) {
+            newDict.setTypeDetail("时间范围");
+            newDict.setTypeName("filter_condition_timeRange");
+        }
+        newDict.setParentId(parentId == null ? null : parentId);
+        newDict.setHidden(StringUtils.isNotBlank(hidden) && hidden.equals("隐藏") ? 1 : 0);
+        newDict.setDeleteFlag(0);
+        newDict.setCreatedTime(new Date());
+
+        // 生成 dict_id
+        String maxDictId = getMaxDictIdByTypeName(newDict.getTypeName());
+        newDict.setDictId(String.valueOf(maxDictId == null ? 1 : Integer.parseInt(maxDictId) + 1));
+
+        // 生成 sort_id
+        Integer maxSortId = getMaxSortIdByParentId(newDict.getParentId());
+        newDict.setSortId(maxSortId == null ? 1 : maxSortId + 1);
+
+        boolean saved = this.save(newDict);
+        return saved ? newDict : null;
+    }
+
+    /**
+     * 根据 typeName 获取最大 dict_id
+     *
+     * @param typeName 类型名称
+     * @return 最大 dict_id,如果不存在则返回 null
+     */
+    private String getMaxDictIdByTypeName(String typeName) {
+        LambdaQueryWrapper<StoreDictionary> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreDictionary::getTypeName, typeName);
+        queryWrapper.eq(StoreDictionary::getDeleteFlag, 0);
+
+        queryWrapper.orderByDesc(StoreDictionary::getDictId);
+        queryWrapper.last("LIMIT 1");
+
+        StoreDictionary maxDict = storeDictionaryMapper.selectOne(queryWrapper);
+        return maxDict != null ? maxDict.getDictId() : null;
+    }
+}
+

+ 28 - 0
alien-store/src/main/java/shop/alien/store/service/impl/LifeFeedbackReplyServiceImpl.java

@@ -0,0 +1,28 @@
+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.LifeFeedbackReply;
+import shop.alien.mapper.LifeFeedbackReplyMapper;
+import shop.alien.store.service.LifeFeedbackReplyService;
+
+import java.util.List;
+
+/**
+ * 反馈回复 Service实现类
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LifeFeedbackReplyServiceImpl extends ServiceImpl<LifeFeedbackReplyMapper, LifeFeedbackReply> implements LifeFeedbackReplyService {
+
+    private final LifeFeedbackReplyMapper lifeFeedbackReplyMapper;
+
+    @Override
+    public List<LifeFeedbackReply> getByFeedbackId(Integer feedbackId) {
+        return lifeFeedbackReplyMapper.selectByFeedbackId(feedbackId);
+    }
+}
+

+ 432 - 101
alien-store/src/main/java/shop/alien/store/service/impl/LifeFeedbackServiceImpl.java

@@ -11,18 +11,28 @@ 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.LifeFeedbackReply;
 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.entity.store.vo.FeedbackReplyVo;
 import shop.alien.mapper.LifeFeedbackMapper;
 import shop.alien.mapper.LifeLogMapper;
+import shop.alien.mapper.LifeNoticeMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.entity.store.LifeNotice;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.vo.WebSocketVo;
+import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.LifeFeedbackService;
+import shop.alien.store.service.LifeFeedbackReplyService;
 import shop.alien.store.service.LifeImgService;
+import com.alibaba.fastjson2.JSONObject;
 
-import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.ArrayList;
 
 /**
  * 意见反馈 Service实现类
@@ -36,6 +46,10 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
     private final LifeFeedbackMapper lifeFeedbackMapper;
     private final LifeImgService lifeImgService;
     private final LifeLogMapper lifeLogMapper;
+    private final LifeFeedbackReplyService lifeFeedbackReplyService;
+    private final LifeNoticeMapper lifeNoticeMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final WebSocketProcess webSocketProcess;
 
     @Override
     public R<String> submitFeedback(LifeFeedbackDto dto) {
@@ -57,6 +71,10 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
             // 2. 创建反馈记录(使用MyBatis Plus的save方法)
             LifeFeedback feedback = new LifeFeedback();
             BeanUtils.copyProperties(dto, feedback);
+            // 如果feedbackWay为空,默认为用户主动反馈(0)
+            if (feedback.getFeedbackWay() == null) {
+                feedback.setFeedbackWay(0);
+            }
             feedback.setFeedbackTime(new Date());
             feedback.setHandleStatus(0); // 处理中
             feedback.setCreateTime(new Date());
@@ -66,84 +84,81 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
                 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);
+            // 3. 保存附件(图片和视频)
+            List<LifeImg> fileList = new ArrayList<>();
+            // 收集所有视频的截图URL,避免重复保存为普通图片
+            List<String> videoThumbnailUrls = new ArrayList<>();
+
+            if (!CollectionUtils.isEmpty(dto.getFileUrlList())) {
+                // 先处理视频,找到所有视频及其封面图
+                List<String> videoUrls = new ArrayList<>();
+                List<String> imageUrls = new ArrayList<>();
+
+                // 分类:区分视频和图片
+                for (String fileUrl : dto.getFileUrlList()) {
+                    if (isVideoUrl(fileUrl)) {
+                        videoUrls.add(fileUrl);
+                    } else if (isImageUrl(fileUrl)) {
+                        imageUrls.add(fileUrl);
+                    }
                 }
-                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("回复内容不能为空");
-            }
+                // 处理视频:自动匹配封面图
+                for (String videoUrl : videoUrls) {
+                    LifeImg video = new LifeImg();
+                    video.setFeedbackId(feedback.getId());
+                    video.setImgUrl(videoUrl);
+                    video.setFileType(2); // 2-视频
+                    video.setUploadTime(new Date());
+
+                    // 从fileUrlList中查找对应的封面图URL(通过文件名匹配)
+                    // 视频URL格式: .../video/xxx123456.mp4
+                    // 封面图URL格式: .../video/xxx123456.jpg 或 .../image/xxx123456.jpg
+                    String videoFileName = videoUrl.substring(videoUrl.lastIndexOf('/') + 1);
+                    String videoNameWithoutExt = videoFileName.substring(0, videoFileName.lastIndexOf('.'));
+
+                    // 在图片列表中查找匹配的封面图
+                    for (String imgUrl : imageUrls) {
+                        String imgFileName = imgUrl.substring(imgUrl.lastIndexOf('/') + 1);
+                        if (imgFileName.contains(".")) {
+                            String imgNameWithoutExt = imgFileName.substring(0, imgFileName.lastIndexOf('.'));
+                            // 如果文件名(不含扩展名)相同,且是图片格式,则认为是该视频的封面图
+                            if (videoNameWithoutExt.equals(imgNameWithoutExt) && isImageUrl(imgUrl)) {
+                                video.setThumbnailUrl(imgUrl);
+                                videoThumbnailUrls.add(imgUrl); // 记录已使用的封面图URL
+                                break;
+                            }
+                        }
+                    }
+
+                    fileList.add(video);
+                }
 
-            // 2. 查询原始反馈
-            LifeFeedback originalFeedback = lifeFeedbackMapper.selectById(dto.getFeedbackId());
-            if (originalFeedback == null) {
-                return R.fail("反馈记录不存在");
+                // 处理图片(排除已作为视频封面的URL)
+                for (String imgUrl : imageUrls) {
+                    // 如果该URL已被用作视频封面,则跳过,不重复保存
+                    if (!videoThumbnailUrls.contains(imgUrl)) {
+                        LifeImg img = new LifeImg();
+                        img.setFeedbackId(feedback.getId());
+                        img.setImgUrl(imgUrl);
+                        img.setFileType(1); // 1-图片
+                        img.setUploadTime(new Date());
+                        fileList.add(img);
+                    }
+                }
             }
 
-            // 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("回复失败");
+            if (!fileList.isEmpty()) {
+                lifeImgService.batchSave(fileList);
             }
 
-            // 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());
+            // 4. 记录日志(只记录详细内容)
+            saveLog(feedback.getId(), feedback.getContent(), "0");
 
-            return R.success("回复成功");
+            return R.success("提交成功");
         } catch (Exception e) {
-            log.error("回复反馈失败", e);
-            return R.fail("回复反馈失败:" + e.getMessage());
+            log.error("提交反馈失败", e);
+            return R.fail("提交反馈失败:" + e.getMessage());
         }
     }
 
@@ -151,15 +166,20 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
     public IPage<LifeFeedbackVo> getFeedbackList(Integer userId, Integer feedbackSource, int page, int size) {
         try {
             // 使用自定义SQL查询(已包含工作人员名称)
+            // 查询用户反馈(feedbackWay=0)和AI识别(feedbackWay=1)的记录
             Page<LifeFeedbackVo> pageParam = new Page<>(page, size);
             IPage<LifeFeedbackVo> voPage = lifeFeedbackMapper.selectFeedbackListWithStaff(
-                    pageParam, userId, feedbackSource, 1, null
+                    pageParam, userId, feedbackSource, null, null
             );
 
-            // 为每条记录查询附件图片
+            // 为每条记录查询附件图片和视频)并设置反馈类型名称
             voPage.getRecords().forEach(vo -> {
                 List<String> imgUrls = lifeImgService.getImgUrlsByFeedbackId(vo.getId());
                 vo.setImgUrlList(imgUrls);
+                List<String> videoUrls = lifeImgService.getVideoUrlsByFeedbackId(vo.getId());
+                vo.setVideoUrlList(videoUrls);
+                // 设置反馈类型名称
+                vo.setFeedbackTypeName(getFeedbackTypeName(vo.getFeedbackType()));
             });
 
             return voPage;
@@ -178,18 +198,102 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
                 return R.fail("反馈记录不存在");
             }
 
-            // 2. 查询附件图片
-            List<String> imgUrls = lifeImgService.getImgUrlsByFeedbackId(feedbackId);
+            // 2. 查询附件(图片和视频)
+            // 查询所有附件,然后过滤出原始反馈的附件(排除回复附件)
+            List<LifeImg> allImgs = lifeImgService.getByFeedbackId(feedbackId);
+            List<String> imgUrls = new ArrayList<>();
+            List<String> videoUrls = new ArrayList<>();
+            Date feedbackTime = vo.getFeedbackTime();
+            if (feedbackTime != null) {
+                long feedbackTimeMs = feedbackTime.getTime();
+                for (LifeImg img : allImgs) {
+                    if (img.getUploadTime() != null) {
+                        long imgTimeMs = img.getUploadTime().getTime();
+                        // 判断附件是否属于原始反馈(时间差在5分钟内,且早于最早的回复)
+                        // 简化处理:如果是反馈后5分钟内的附件,认为是原始反馈的附件
+                        List<LifeFeedbackReply> replyList = lifeFeedbackReplyService.getByFeedbackId(feedbackId);
+                        boolean isOriginalFeedback = true;
+                        if (!replyList.isEmpty()) {
+                            Date firstReplyTime = replyList.get(0).getCreateTime();
+                            // 如果附件时间在最早回复时间之后,则不属于原始反馈
+                            if (img.getUploadTime().after(firstReplyTime)) {
+                                isOriginalFeedback = false;
+                            } else {
+                                long timeDiff = Math.abs(imgTimeMs - feedbackTimeMs);
+                                if (timeDiff > 5 * 60 * 1000) { // 超过5分钟
+                                    isOriginalFeedback = false;
+                                }
+                            }
+                        } else {
+                            long timeDiff = Math.abs(imgTimeMs - feedbackTimeMs);
+                            if (timeDiff > 5 * 60 * 1000) { // 超过5分钟
+                                isOriginalFeedback = false;
+                            }
+                        }
+
+                        if (isOriginalFeedback) {
+                            if (img.getFileType() == 1) {
+                                imgUrls.add(img.getImgUrl());
+                            } else if (img.getFileType() == 2) {
+                                videoUrls.add(img.getImgUrl());
+                            }
+                        }
+                    }
+                }
+            } else {
+                // 如果没有反馈时间,使用简单方式:只查询图片和视频
+                for (LifeImg img : allImgs) {
+                    if (img.getFileType() == 1) {
+                        imgUrls.add(img.getImgUrl());
+                    } else if (img.getFileType() == 2) {
+                        videoUrls.add(img.getImgUrl());
+                    }
+                }
+            }
             vo.setImgUrlList(imgUrls);
-
-            // 3. 查询平台回复列表(如果是主动反馈)
-            if (vo.getFeedbackWay() == 1) {
-                List<LifeFeedbackVo> replyList = lifeFeedbackMapper.selectPlatformReplies(
-                        vo.getUserId(), vo.getFeedbackSource(), vo.getFeedbackTime()
-                );
-                vo.setPlatformReplies(replyList);
+            vo.setVideoUrlList(videoUrls);
+            // 设置反馈类型名称
+            vo.setFeedbackTypeName(getFeedbackTypeName(vo.getFeedbackType()));
+
+            // 3. 查询回复列表(从life_feedback_reply表)
+            List<LifeFeedbackReply> replyList = lifeFeedbackReplyService.getByFeedbackId(feedbackId);
+            // 转换为VO格式(使用之前查询的allImgs)
+            List<LifeFeedbackVo> replyVoList = new ArrayList<>();
+            for (LifeFeedbackReply reply : replyList) {
+                LifeFeedbackVo replyVo = new LifeFeedbackVo();
+                replyVo.setId(reply.getId());
+                replyVo.setContent(reply.getReplyContent());
+                replyVo.setFeedbackTime(reply.getCreateTime());
+                // reply_type: 0-平台回复, 1-我的回复
+                replyVo.setStaffId(reply.getReplyType() == 0 ? 1 : null); // 平台回复有staffId,用户回复为null
+                // 查询回复的附件(通过时间判断:上传时间在回复创建时间前后5分钟内)
+                List<String> replyImgUrls = new ArrayList<>();
+                List<String> replyVideoUrls = new ArrayList<>();
+                Date replyTime = reply.getCreateTime();
+                long replyTimeMs = replyTime.getTime();
+                for (LifeImg img : allImgs) {
+                    if (img.getUploadTime() != null) {
+                        long imgTimeMs = img.getUploadTime().getTime();
+                        // 判断附件是否属于该回复(时间差在5分钟内)
+                        long timeDiff = Math.abs(imgTimeMs - replyTimeMs);
+                        if (timeDiff <= 5 * 60 * 1000) { // 5分钟
+                            if (img.getFileType() == 1) {
+                                replyImgUrls.add(img.getImgUrl());
+                            } else if (img.getFileType() == 2) {
+                                replyVideoUrls.add(img.getImgUrl());
+                            }
+                        }
+                    }
+                }
+                replyVo.setImgUrlList(replyImgUrls);
+                replyVo.setVideoUrlList(replyVideoUrls);
+                replyVoList.add(replyVo);
             }
 
+            // 4. 按时间升序排序回复
+            replyVoList.sort((a, b) -> a.getFeedbackTime().compareTo(b.getFeedbackTime()));
+            vo.setPlatformReplies(replyVoList);
+
             return R.data(vo);
         } catch (Exception e) {
             log.error("查询反馈详情失败", e);
@@ -198,41 +302,131 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
     }
 
     @Override
-    public R<String> updateHandleStatus(Integer feedbackId, Integer handleStatus, Integer staffId) {
+    public R<String> userReply(UserReplyDto dto) {
         try {
-            // 使用MyBatis Plus的updateById方法
-            LifeFeedback feedback = new LifeFeedback();
-            feedback.setId(feedbackId);
-            feedback.setHandleStatus(handleStatus);
-            feedback.setStaffId(staffId);
-            feedback.setUpdateTime(new Date());
+            // 1. 参数校验
+            if (dto.getUserId() == null) {
+                return R.fail("用户ID不能为空");
+            }
+            if (dto.getFeedbackSource() == null) {
+                return R.fail("反馈来源不能为空");
+            }
+            if (dto.getFeedbackId() == 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. 创建用户回复记录(保存到life_feedback_reply表)
+            LifeFeedbackReply userReply = new LifeFeedbackReply();
+            userReply.setFeedbackId(dto.getFeedbackId());
+            userReply.setReplyType(1); // 1-我的回复(用户回复)
+            userReply.setReplyContent(dto.getContent());
+            userReply.setCreateTime(new Date());
+            userReply.setUpdateTime(new Date());
 
-            boolean result = this.updateById(feedback);
-            if (result) {
-                saveLog("更新反馈处理状态,反馈ID:" + feedbackId + ",状态:" + handleStatus);
-                return R.success("更新成功");
+            boolean saveResult = lifeFeedbackReplyService.save(userReply);
+            if (!saveResult) {
+                return R.fail("回复失败");
             }
-            return R.fail("更新失败");
+
+            // 4. 记录日志(只记录内容)
+            saveLog(dto.getFeedbackId(), dto.getContent(), "2");
+
+            return R.success("回复成功");
         } catch (Exception e) {
-            log.error("更新反馈处理状态失败", e);
-            return R.fail("更新失败:" + e.getMessage());
+            log.error("用户回复失败", e);
+            return R.fail("用户回复失败:" + e.getMessage());
         }
     }
 
     /**
      * 保存操作日志
+     * @param feedbackId 反馈ID
+     * @param context 日志内容
+     * @param type 操作类型:0-创建反馈工单,1-分配跟踪人员,2-回复用户,3-问题解决状态
      */
-    private void saveLog(String context) {
+    private void saveLog(Integer feedbackId, String context, String type) {
         try {
-            LifeLog log = new LifeLog();
-            log.setContext(context);
-            log.setCreatedTime(new Date());
-            lifeLogMapper.insert(log);
+            LifeLog lifeLog = new LifeLog();
+            lifeLog.setFeedbackId(feedbackId);
+            lifeLog.setContext(context);
+            lifeLog.setType(type);
+            lifeLog.setCreatedTime(new Date());
+            lifeLogMapper.insert(lifeLog);
         } catch (Exception e) {
             log.error("保存日志失败", e);
         }
     }
 
+    /**
+     * 获取反馈类型名称
+     * @param feedbackType 反馈类型:0-bug反馈,1-优化反馈,2-新增功能反馈
+     * @return 反馈类型名称
+     */
+    private String getFeedbackTypeName(Integer feedbackType) {
+        if (feedbackType == null) {
+            return "";
+        }
+        switch (feedbackType) {
+            case 0:
+                return "bug反馈";
+            case 1:
+                return "优化反馈";
+            case 2:
+                return "新增功能反馈";
+            default:
+                return "";
+        }
+    }
+
+    /**
+     * 判断URL是否为视频
+     * @param url 文件URL
+     * @return true-视频,false-非视频
+     */
+    private boolean isVideoUrl(String url) {
+        if (url == null || url.isEmpty()) {
+            return false;
+        }
+        String lowerUrl = url.toLowerCase();
+        return lowerUrl.endsWith(".mp4") ||
+               lowerUrl.endsWith(".avi") ||
+               lowerUrl.endsWith(".flv") ||
+               lowerUrl.endsWith(".mkv") ||
+               lowerUrl.endsWith(".rmvb") ||
+               lowerUrl.endsWith(".wmv") ||
+               lowerUrl.endsWith(".3gp") ||
+               lowerUrl.endsWith(".mov");
+    }
+
+    /**
+     * 判断URL是否为图片
+     * @param url 文件URL
+     * @return true-图片,false-非图片
+     */
+    private boolean isImageUrl(String url) {
+        if (url == null || url.isEmpty()) {
+            return false;
+        }
+        String lowerUrl = url.toLowerCase();
+        return lowerUrl.endsWith(".jpg") ||
+               lowerUrl.endsWith(".jpeg") ||
+               lowerUrl.endsWith(".png") ||
+               lowerUrl.endsWith(".bmp") ||
+               lowerUrl.endsWith(".webp") ||
+               lowerUrl.endsWith(".gif") ||
+               lowerUrl.endsWith(".svg");
+    }
+
+
     // ==================== 中台接口实现 ====================
 
     @Override
@@ -281,6 +475,48 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
             }
             detail.setAttachments(attachments);
 
+            // 3. 查询回复列表(平台回复和用户回复)
+            List<LifeFeedbackReply> replyList = lifeFeedbackReplyService.getByFeedbackId(feedbackId);
+            List<FeedbackReplyVo> replyVoList = new ArrayList<>();
+            for (LifeFeedbackReply reply : replyList) {
+                FeedbackReplyVo replyVo = new FeedbackReplyVo();
+                replyVo.setId(reply.getId());
+                replyVo.setFeedbackId(reply.getFeedbackId());
+                replyVo.setReplyType(reply.getReplyType());
+                replyVo.setReplyTypeName(reply.getReplyType() == 0 ? "平台回复" : "用户回复");
+                replyVo.setReplyContent(reply.getReplyContent());
+                replyVo.setCreateTime(reply.getCreateTime());
+
+                // 查询回复的附件(通过时间判断:上传时间在回复创建时间前后5分钟内)
+                List<String> replyImgUrls = new ArrayList<>();
+                List<String> replyVideoUrls = new ArrayList<>();
+                Date replyTime = reply.getCreateTime();
+                if (replyTime != null && !CollectionUtils.isEmpty(imgList)) {
+                    long replyTimeMs = replyTime.getTime();
+                    for (LifeImg img : imgList) {
+                        if (img.getUploadTime() != null) {
+                            long imgTimeMs = img.getUploadTime().getTime();
+                            // 判断附件是否属于该回复(时间差在5分钟内)
+                            long timeDiff = Math.abs(imgTimeMs - replyTimeMs);
+                            if (timeDiff <= 5 * 60 * 1000) { // 5分钟
+                                if (img.getFileType() == 1) {
+                                    replyImgUrls.add(img.getImgUrl());
+                                } else if (img.getFileType() == 2) {
+                                    replyVideoUrls.add(img.getImgUrl());
+                                }
+                            }
+                        }
+                    }
+                }
+                replyVo.setImgUrlList(replyImgUrls);
+                replyVo.setVideoUrlList(replyVideoUrls);
+                replyVoList.add(replyVo);
+            }
+
+            // 按时间升序排序回复
+            replyVoList.sort((a, b) -> a.getCreateTime().compareTo(b.getCreateTime()));
+            detail.setReplies(replyVoList);
+
             return R.data(detail);
         } catch (Exception e) {
             log.error("中台-查询反馈详情失败", e);
@@ -306,13 +542,29 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
                 return R.fail("反馈记录不存在");
             }
 
-            // 3. 记录回复日志(类型3-回复用户)
+            // 3. 保存平台回复到life_feedback_reply表
+            LifeFeedbackReply platformReply = new LifeFeedbackReply();
+            platformReply.setFeedbackId(replyDto.getFeedbackId());
+            platformReply.setReplyType(0); // 0-平台回复
+            platformReply.setReplyContent(replyDto.getContent());
+            platformReply.setCreateTime(new Date());
+            platformReply.setUpdateTime(new Date());
+
+            boolean saveResult = lifeFeedbackReplyService.save(platformReply);
+            if (!saveResult) {
+                return R.fail("保存回复失败");
+            }
+
+            // 4. 记录回复日志(类型3-回复用户)
             String logContent = replyDto.getContent();
             if (replyDto.getUserReply() != null && !replyDto.getUserReply().trim().isEmpty()) {
                 logContent = replyDto.getContent() + "||用户回复:" + replyDto.getUserReply();
             }
             saveFeedbackLog(replyDto.getFeedbackId(), 3, logContent);
 
+            // 5. 发送通知给用户
+            sendFeedbackReplyNotice(feedback, replyDto.getContent());
+
             return R.success("回复成功");
         } catch (Exception e) {
             log.error("中台-回复用户失败", e);
@@ -370,5 +622,84 @@ public class LifeFeedbackServiceImpl extends ServiceImpl<LifeFeedbackMapper, Lif
             log.error("保存反馈日志失败", e);
         }
     }
+
+    /**
+     * 发送平台回复通知给用户
+     * @param feedback 反馈记录
+     * @param replyContent 回复内容
+     */
+    private void sendFeedbackReplyNotice(LifeFeedback feedback, String replyContent) {
+        try {
+            String receiverId = null;
+
+            // 根据反馈来源判断是用户端还是商家端
+            if (feedback.getFeedbackSource() == null) {
+                log.warn("反馈来源为空,无法发送通知,feedbackId={}", feedback.getId());
+                return;
+            }
+            
+            // userId对应store_user表的id,统一从store_user表查询
+            if (feedback.getUserId() == null) {
+                log.warn("用户ID为空,无法发送通知,feedbackId={}", feedback.getId());
+                return;
+            }
+
+            StoreUser storeUser = storeUserMapper.selectById(feedback.getUserId());
+            if (storeUser == null || storeUser.getPhone() == null || storeUser.getPhone().trim().isEmpty()) {
+                log.warn("未找到商户用户信息或手机号为空,无法发送通知,userId={}", feedback.getUserId());
+                return;
+            }
+
+            // 根据feedbackSource设置不同的接收者ID格式
+            if (feedback.getFeedbackSource() == 0) {
+                // 用户端 - 使用user_手机号格式
+                receiverId = "user_" + storeUser.getPhone();
+            } else if (feedback.getFeedbackSource() == 1) {
+                // 商家端 - 使用store_手机号格式
+                receiverId = "store_" + storeUser.getPhone();
+            } else {
+                log.warn("未知的反馈来源,feedbackSource={}, feedbackId={}", feedback.getFeedbackSource(), feedback.getId());
+                return;
+            }
+
+            // 构建通知消息
+            JSONObject messageJson = new JSONObject();
+            messageJson.put("feedbackId", feedback.getId()); // 添加反馈ID用于区分
+            messageJson.put("message", "平台已回复您的意见反馈:" + replyContent);
+
+            // 创建通知记录
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setReceiverId(receiverId);
+            lifeNotice.setContext(messageJson.toJSONString());
+            lifeNotice.setTitle("意见反馈回复通知");
+            lifeNotice.setSenderId("system");
+            lifeNotice.setIsRead(0);
+            lifeNotice.setNoticeType(1); // 1-系统通知
+            lifeNotice.setBusinessId(feedback.getId());
+
+            // 保存通知
+            lifeNoticeMapper.insert(lifeNotice);
+
+            // 通过WebSocket发送实时通知
+            WebSocketVo webSocketVo = new WebSocketVo();
+            webSocketVo.setSenderId("system");
+            webSocketVo.setReceiverId(receiverId);
+            webSocketVo.setCategory("notice");
+            webSocketVo.setNoticeType("1");
+            webSocketVo.setIsRead(0);
+            webSocketVo.setText(JSONObject.toJSONString(lifeNotice));
+
+            try {
+                webSocketProcess.sendMessage(receiverId, JSONObject.toJSONString(webSocketVo));
+                log.info("平台回复通知发送成功,feedbackId={}, receiverId={}", feedback.getId(), receiverId);
+            } catch (Exception e) {
+                log.error("发送WebSocket通知失败,feedbackId={}, receiverId={}, error={}",
+                        feedback.getId(), receiverId, e.getMessage());
+            }
+
+        } catch (Exception e) {
+            log.error("发送平台回复通知异常,feedbackId={}, error={}", feedback.getId(), e.getMessage(), e);
+        }
+    }
 }
 

+ 11 - 1
alien-store/src/main/java/shop/alien/store/service/impl/LifeImgServiceImpl.java

@@ -30,6 +30,12 @@ public class LifeImgServiceImpl extends ServiceImpl<LifeImgMapper, LifeImg> impl
         if (imgList == null || imgList.isEmpty()) {
             return false;
         }
+        // 确保每个附件都有上传时间
+        imgList.forEach(item -> {
+            if (item.getUploadTime() == null) {
+                item.setUploadTime(new java.util.Date());
+            }
+        });
         return lifeImgMapper.batchInsert(imgList) > 0;
     }
 
@@ -42,5 +48,9 @@ public class LifeImgServiceImpl extends ServiceImpl<LifeImgMapper, LifeImg> impl
     public List<String> getImgUrlsByFeedbackId(Integer feedbackId) {
         return lifeImgMapper.selectImgUrlsByFeedbackId(feedbackId);
     }
-}
 
+    @Override
+    public List<String> getVideoUrlsByFeedbackId(Integer feedbackId) {
+        return lifeImgMapper.selectVideoUrlsByFeedbackId(feedbackId);
+    }
+}

+ 9 - 5
alien-store/src/main/java/shop/alien/store/service/impl/LifeUserViolationServiceImpl.java

@@ -755,6 +755,7 @@ public class LifeUserViolationServiceImpl extends ServiceImpl<LifeUserViolationM
         return resultPage.convert(e -> {
             LifeUserViolationDto dto = new LifeUserViolationDto();
             BeanUtils.copyProperties(e, dto);
+            dto.setNickname(e.getNickName());
             return dto;
         });
     }
@@ -769,7 +770,7 @@ public class LifeUserViolationServiceImpl extends ServiceImpl<LifeUserViolationM
         List<String> videoFileType = Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv");
 
         if ("2".equals(item.getReportContextType())) {
-            LifeUserDynamics dynamicsInfo = lifeUserDynamicsMapper.selectById(id);
+            LifeUserDynamics dynamicsInfo = lifeUserDynamicsMapper.selectById(item.getDynamicsId());
             item.setDynamicsInfo(dynamicsInfo);
             // 将逗号分隔的图片路径拆分成 List<String>
             if (dynamicsInfo != null && StringUtils.isNotEmpty(dynamicsInfo.getImagePath())) {
@@ -783,15 +784,18 @@ public class LifeUserViolationServiceImpl extends ServiceImpl<LifeUserViolationM
 
         if ("3".equals(item.getReportContextType())) {
             // TODO: 处理评论类型
-            LifeComment commentInfo = lifeCommentMapper.selectById(id);
+            StoreComment commentInfo = storeCommentMapper.selectById(item.getBusinessId());
             item.setCommentInfo(commentInfo);
+
+            LifeUserDynamics dynamicsInfo = lifeUserDynamicsMapper.selectById(commentInfo.getBusinessId());
+            item.setDynamicsInfo(dynamicsInfo);
             // 将逗号分隔的图片路径拆分成 List<String>
-            if (commentInfo != null && StringUtils.isNotEmpty(commentInfo.getImagePath())) {
-                List<String> imagePathList = Arrays.stream(commentInfo.getImagePath().split(","))
+            if (dynamicsInfo != null && StringUtils.isNotEmpty(dynamicsInfo.getImagePath())) {
+                List<String> imagePathList = Arrays.stream(dynamicsInfo.getImagePath().split(","))
                         .map(String::trim)
                         .filter(StringUtils::isNotEmpty)
                         .collect(Collectors.toList());
-                item.setCommentImg(imagePathList);
+                item.setDynamicsImg(imagePathList);
             }
         }
 

+ 28 - 1
alien-store/src/main/java/shop/alien/store/service/impl/ProtocolManagementServiceImpl.java

@@ -93,10 +93,29 @@ public class ProtocolManagementServiceImpl extends ServiceImpl<ProtocolManagemen
             throw new IllegalArgumentException("协议文件路径不存在");
         }
 
+        // 添加域名判断和转换逻辑
+        String newDomain = "https://ossfile.ailien.shop/";
+        String oldDomain = "https://alien-volume.oss-cn-beijing.aliyuncs.com/";
+
+        if (protocolUrl.startsWith(oldDomain) && !protocolUrl.contains(newDomain)) {
+            protocolUrl = newDomain + protocolUrl.substring(oldDomain.length());
+        }
+
         InputStream inputStream = null;
         OutputStream outputStream = null;
         try {
             URL url = new URL(protocolUrl);
+
+            // 绕过SSL证书验证
+            if (protocolUrl.startsWith("https://ossfile.ailien.shop/")) {
+                // 对于自定义域名,暂时回退到原始OSS域名
+                String fallbackUrl = protocolUrl.replace(
+                        "https://ossfile.ailien.shop/",
+                        "https://alien-volume.oss-cn-beijing.aliyuncs.com/"
+                );
+                url = new URL(fallbackUrl);
+            }
+
             URLConnection connection = url.openConnection();
             connection.setConnectTimeout(5000);
             connection.setReadTimeout(10000);
@@ -168,7 +187,15 @@ public class ProtocolManagementServiceImpl extends ServiceImpl<ProtocolManagemen
         Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
 
         String s = aliOSSUtil.uploadFile(multipartFile, prefix + fileNameAndType.get("name") + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type"));
-        // 上传文件
+        // 修改oss路径为域名路径
+        String newDomain = "https://ossfile.ailien.shop/";
+        String oldDomain = "https://alien-volume.oss-cn-beijing.aliyuncs.com/";
+
+        if (s.startsWith(oldDomain)) {
+            s = newDomain + s.substring(oldDomain.length());
+        }
+
+
         protocolManagement.setProtocolFilePath(s);
 
         // 根据 type(displayPosition) 和名称(protocolFileName) 查询是否存在

+ 334 - 23
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -13,7 +13,6 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.geo.Point;
 import org.springframework.http.*;
@@ -37,7 +36,6 @@ import shop.alien.entity.storePlatform.StoreOperationalActivity;
 import shop.alien.mapper.*;
 import shop.alien.mapper.storePlantform.StoreLicenseHistoryMapper;
 import shop.alien.mapper.storePlantform.StoreOperationalActivityMapper;
-import shop.alien.mapper.storePlantform.StoreOperationalActivityMapper;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.config.GaoDeMapUtil;
 import shop.alien.store.config.WebSocketProcess;
@@ -46,7 +44,6 @@ import shop.alien.store.util.CommonConstant;
 import shop.alien.store.util.FileUploadUtil;
 import shop.alien.store.util.GroupConstant;
 import shop.alien.store.util.ai.AiAuthTokenUtil;
-import shop.alien.store.util.ali.AliApi;
 import shop.alien.util.ali.AliOSSUtil;
 import shop.alien.util.common.DistanceUtil;
 import shop.alien.util.common.constant.CouponStatusEnum;
@@ -914,10 +911,13 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                 storeInfo.setFoodLicenceStatus(2);
                 storeInfo.setUpdateFoodLicenceTime(new Date());
             }
-        } else if (storeInfoDto.getFoodLicenceExpirationTime() != null) {
-            // 没有食品经营许可证URL,但有传入到期时间时直接使用
-            storeInfo.setFoodLicenceExpirationTime(storeInfoDto.getFoodLicenceExpirationTime());
-            log.info("无食品经营许可证URL,使用DTO中的到期时间:{}", storeInfoDto.getFoodLicenceExpirationTime());
+        } else {
+            // 没有食品经营许可证URL,初始化状态为"未提交"(字典值0)
+            storeInfo.setFoodLicenceStatus(0);
+            if (storeInfoDto.getFoodLicenceExpirationTime() != null) {
+                storeInfo.setFoodLicenceExpirationTime(storeInfoDto.getFoodLicenceExpirationTime());
+                log.info("无食品经营许可证URL,使用DTO中的到期时间:{}", storeInfoDto.getFoodLicenceExpirationTime());
+            }
         }
 
         // 处理娱乐经营许可证OCR数据(复用营业执照OCR类型,数据库中ocr_type存的是BUSINESS_LICENSE)
@@ -965,13 +965,78 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                 storeInfo.setEntertainmentLicenceStatus(2);
                 storeInfo.setUpdateEntertainmentLicenceTime(new Date());
             }
-        } else if (storeInfoDto.getEntertainmentLicenceExpirationTime() != null) {
-            // 没有娱乐经营许可证URL,但有传入到期时间时直接使用
-            storeInfo.setEntertainmentLicenceExpirationTime(storeInfoDto.getEntertainmentLicenceExpirationTime());
-            log.info("无娱乐经营许可证URL,使用DTO中的到期时间:{}", storeInfoDto.getEntertainmentLicenceExpirationTime());
+        } else {
+            // 没有娱乐经营许可证URL,初始化状态为"未提交"(字典值0)
+            storeInfo.setEntertainmentLicenceStatus(0);
+            if (storeInfoDto.getEntertainmentLicenceExpirationTime() != null) {
+                storeInfo.setEntertainmentLicenceExpirationTime(storeInfoDto.getEntertainmentLicenceExpirationTime());
+                log.info("无娱乐经营许可证URL,使用DTO中的到期时间:{}", storeInfoDto.getEntertainmentLicenceExpirationTime());
+            }
         }
 
-        // 计算并设置到期时间为三个过期时间的最小值
+        // 处理营业执照OCR数据(复用营业执照OCR类型,数据库中ocr_type存的是BUSINESS_LICENSE)
+        // 营业执照使用 businessLicenseAddress 列表中的第一个URL查询OCR
+        String businessLicenseOcrUrl = null;
+        if (!CollectionUtils.isEmpty(storeInfoDto.getBusinessLicenseAddress())) {
+            businessLicenseOcrUrl = storeInfoDto.getBusinessLicenseAddress().get(0);
+        } else if (StringUtils.isNotEmpty(storeInfoDto.getBusinessLicenseUrl())) {
+            businessLicenseOcrUrl = storeInfoDto.getBusinessLicenseUrl();
+        }
+
+        if (StringUtils.isNotEmpty(businessLicenseOcrUrl)) {
+            // 查询营业执照OCR识别记录
+            OcrImageUpload businessLicenseOcr = ocrImageUploadMapper.selectOne(
+                    new LambdaQueryWrapper<OcrImageUpload>()
+                            .eq(OcrImageUpload::getImageUrl, businessLicenseOcrUrl)
+                            .eq(OcrImageUpload::getOcrType, OcrTypeEnum.BUSINESS_LICENSE.getCode())
+                            .orderByDesc(OcrImageUpload::getCreateTime)
+                            .last("limit 1")
+            );
+            if (businessLicenseOcr != null && StringUtils.isNotEmpty(businessLicenseOcr.getOcrResult())) {
+                try {
+                    com.alibaba.fastjson2.JSONObject ocrResult = com.alibaba.fastjson2.JSONObject.parseObject(businessLicenseOcr.getOcrResult());
+                    // 营业执照OCR字段:validToDate(优先,格式"20241217")、validPeriod(格式"2020年09月04日至2022年09月03日")
+                    Date expirationTime = parseBusinessLicenseExpirationDate(ocrResult);
+                    if (expirationTime != null) {
+                        storeInfo.setBusinessLicenseExpirationTime(expirationTime);
+                    } else if (storeInfoDto.getBusinessLicenseExpirationTime() != null) {
+                        // OCR解析结果为空时,使用DTO中传入的值
+                        storeInfo.setBusinessLicenseExpirationTime(storeInfoDto.getBusinessLicenseExpirationTime());
+                        log.info("使用DTO中的营业执照到期时间:{}", storeInfoDto.getBusinessLicenseExpirationTime());
+                    }
+                    // 设置营业执照状态为"待审核"(字典值2)
+                    storeInfo.setBusinessLicenseStatus(2);
+                    storeInfo.setUpdateBusinessLicenseTime(new Date());
+                    log.info("营业执照OCR数据解析成功,到期时间:{}", storeInfo.getBusinessLicenseExpirationTime());
+                } catch (Exception e) {
+                    log.error("解析营业执照OCR数据失败", e);
+                    // 解析失败时使用DTO中传入的值
+                    if (storeInfoDto.getBusinessLicenseExpirationTime() != null) {
+                        storeInfo.setBusinessLicenseExpirationTime(storeInfoDto.getBusinessLicenseExpirationTime());
+                        log.info("OCR解析失败,使用DTO中的营业执照到期时间:{}", storeInfoDto.getBusinessLicenseExpirationTime());
+                    }
+                    storeInfo.setBusinessLicenseStatus(2);
+                    storeInfo.setUpdateBusinessLicenseTime(new Date());
+                }
+            } else {
+                // 没有OCR记录时,使用DTO中传入的值
+                if (storeInfoDto.getBusinessLicenseExpirationTime() != null) {
+                    storeInfo.setBusinessLicenseExpirationTime(storeInfoDto.getBusinessLicenseExpirationTime());
+                    log.info("无OCR记录,使用DTO中的营业执照到期时间:{}", storeInfoDto.getBusinessLicenseExpirationTime());
+                }
+                storeInfo.setBusinessLicenseStatus(2);
+                storeInfo.setUpdateBusinessLicenseTime(new Date());
+            }
+        } else {
+            // 没有营业执照URL,初始化状态为"未提交"(字典值0)
+            storeInfo.setBusinessLicenseStatus(0);
+            if (storeInfoDto.getBusinessLicenseExpirationTime() != null) {
+                storeInfo.setBusinessLicenseExpirationTime(storeInfoDto.getBusinessLicenseExpirationTime());
+                log.info("无营业执照URL,使用DTO中的到期时间:{}", storeInfoDto.getBusinessLicenseExpirationTime());
+            }
+        }
+
+        // 计算并设置到期时间为五个过期时间的最小值(包含身份证过期时间)
         List<Date> expirationTimeList = new ArrayList<>();
         // 收集所有非空的过期时间
         if (storeInfoDto.getExpirationTime() != null) {
@@ -983,6 +1048,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         if (storeInfo.getEntertainmentLicenceExpirationTime() != null) {
             expirationTimeList.add(storeInfo.getEntertainmentLicenceExpirationTime());
         }
+        if (storeInfo.getBusinessLicenseExpirationTime() != null) {
+            expirationTimeList.add(storeInfo.getBusinessLicenseExpirationTime());
+        }
         // 取最小值设置为门店到期时间
         if (!expirationTimeList.isEmpty()) {
             Date minExpirationTime = Collections.min(expirationTimeList);
@@ -1054,6 +1122,23 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             storeImgMapper.insert(storeImg);
         }
 
+        //存入身份证正反面图片
+        saveIdCardImages(storeInfo.getId(), storeUser.getId().toString(), storeInfo);
+
+        // 更新门店到期时间,包含身份证过期时间
+        if (storeInfo.getIdCardExpirationTime() != null) {
+            List<Date> allExpirationTimeList = new ArrayList<>();
+            if (storeInfo.getExpirationTime() != null) {
+                allExpirationTimeList.add(storeInfo.getExpirationTime());
+            }
+            allExpirationTimeList.add(storeInfo.getIdCardExpirationTime());
+            Date minExpirationTime = Collections.min(allExpirationTimeList);
+            storeInfo.setExpirationTime(minExpirationTime);
+            // 更新数据库中的过期时间
+            storeInfoMapper.updateById(storeInfo);
+            log.info("更新门店到期时间,包含身份证过期时间,最小值:{}", minExpirationTime);
+        }
+
         //初始化标签数据
         LambdaQueryWrapper<TagStoreRelation> tagStoreRelationLambdaQueryWrapper = new LambdaQueryWrapper<>();
         tagStoreRelationLambdaQueryWrapper.eq(TagStoreRelation::getStoreId, storeInfo.getId());
@@ -1336,8 +1421,12 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
     @Override
     public List<StoreDictionaryVo> getBusinessSectionTypes(String parentId) {
-        StoreDictionary businessSection = storeDictionaryMapper.selectOne(new LambdaQueryWrapper<StoreDictionary>().eq(StoreDictionary::getTypeName, "business_section").eq(StoreDictionary::getDictId, parentId));
-        List<StoreDictionary> storeDictionaries = storeDictionaryMapper.selectList(new LambdaQueryWrapper<StoreDictionary>().eq(StoreDictionary::getParentId, businessSection.getId()));
+        StoreDictionary businessSection = storeDictionaryMapper.selectOne(new LambdaQueryWrapper<StoreDictionary>()
+                .eq(StoreDictionary::getTypeName, "business_section")
+                .eq(StoreDictionary::getDictId, parentId)
+                .isNull(StoreDictionary::getParentId));
+        List<StoreDictionary> storeDictionaries = storeDictionaryMapper.selectList(new LambdaQueryWrapper<StoreDictionary>()
+                .eq(StoreDictionary::getParentId, businessSection.getId()));
         List<StoreDictionaryVo> voList = new ArrayList<>();
         for (StoreDictionary storeDictionary : storeDictionaries) {
             StoreDictionaryVo vo = new StoreDictionaryVo();
@@ -1731,6 +1820,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
         storeInfo.setStoreApplicationStatus(approvalStatus);
         storeInfo.setReason(reason);
+        storeInfo.setReviewDate(new Date());
         StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>().eq(StoreUser::getStoreId, storeInfo.getId()).eq(StoreUser::getDeleteFlag, 0));
         storeInfoMapper.updateById(storeInfo);
 
@@ -1950,7 +2040,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             List<String> storeIds = Arrays.asList(storeId.split(","));
             if (StringUtils.isNotEmpty(startTime) && StringUtils.isNotEmpty(endTime)) {
                 List<StoreBusinessInfo> storeBusinessInfos = storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().in(StoreBusinessInfo::getStoreId, storeIds));
-                DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 1, 2, java.time.format.SignStyle.NOT_NEGATIVE).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, 2).toFormatter();
+                DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, 2).toFormatter();
                 List<StoreBusinessInfo> list = storeBusinessInfos.stream().filter(item -> {
                     // 商家开门时间
                     LocalTime timeStart = LocalTime.parse(item.getEndTime(), formatter);
@@ -2030,7 +2120,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             List<String> storeIds = Arrays.asList(storeId.split(","));
             if (StringUtils.isNotEmpty(startTime) && StringUtils.isNotEmpty(endTime)) {
                 List<StoreBusinessInfo> storeBusinessInfos = storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().in(StoreBusinessInfo::getStoreId, storeIds));
-                DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 1, 2, java.time.format.SignStyle.NOT_NEGATIVE).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, 2).toFormatter();
+                DateTimeFormatter formatter = new DateTimeFormatterBuilder().appendValue(ChronoField.HOUR_OF_DAY, 1, 2, SignStyle.NOT_NEGATIVE).appendLiteral(':').appendValue(ChronoField.MINUTE_OF_HOUR, 2).toFormatter();
                 List<StoreBusinessInfo> list = storeBusinessInfos.stream().filter(item -> {
                     // 商家开门时间
                     LocalTime timeStart = LocalTime.parse(item.getEndTime(), formatter);
@@ -2973,17 +3063,17 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                 .eq(StoreOperationalActivity::getDeleteFlag, 0)
                 .eq(StoreOperationalActivity::getStatus, 5);
         List<StoreOperationalActivity> activities = storeOperationalActivityMapper.selectList(activityWrapper);
-        
+
         // 如果没有活动,返回空列表
         if (CollectionUtils.isEmpty(activities)) {
             return new ArrayList<>();
         }
-        
+
         // 获取活动ID列表
         List<Integer> activityIds = activities.stream()
                 .map(StoreOperationalActivity::getId)
                 .collect(Collectors.toList());
-        
+
         // 查询与活动关联的图片
         LambdaQueryWrapper<StoreImg> queryWrapper = new LambdaQueryWrapper<StoreImg>()
                 .eq(StoreImg::getStoreId, Integer.parseInt(storeId))
@@ -3110,9 +3200,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             // KTV=3、洗浴汗蒸=4、按摩足浴=5,酒吧的数值未知(由调用方传入)
             queryWrapper.eq("a.business_section", businessType);
         } else {
-            // 如果没有指定businessType,则查询所有种类型的店铺
+            // 如果没有指定businessType,则查询所有种类型的店铺
             // 需要查询字典表获取所有四种类型的dictId
-            List<String> storeTypeNames = Arrays.asList("丽人美发", "运动健身");
+            List<String> storeTypeNames = Arrays.asList("酒吧", "KTV", "洗浴汗蒸", "按摩足疗");
             List<StoreDictionary> storeDictionaries = storeDictionaryMapper.selectList(
                     new LambdaQueryWrapper<StoreDictionary>()
                             .eq(StoreDictionary::getTypeName, "business_section")
@@ -3218,7 +3308,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             LambdaQueryWrapper<LifeUserOrder> orderWrapper = new LambdaQueryWrapper<>();
             orderWrapper.in(LifeUserOrder::getStoreId, storeIds)
                     .and(w -> w.and(w1 -> w1.eq(LifeUserOrder::getStatus, 7)
-                                    .ge(LifeUserOrder::getFinishTime, sevenDaysAgoStr))
+                            .ge(LifeUserOrder::getFinishTime, sevenDaysAgoStr))
                             .or(w2 -> w2.eq(LifeUserOrder::getStatus, 1)
                                     .ge(LifeUserOrder::getPayTime, sevenDaysAgoStr)))
                     .eq(LifeUserOrder::getDeleteFlag, 0);
@@ -3349,7 +3439,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                     store.setDistance(storeDistance);
                     // 使用反射或扩展字段存储finalScore,这里我们使用一个临时字段
                     // 由于StoreInfoVo没有finalScore字段,我们使用distance字段临时存储,排序后再恢复
-                    return new Object[]{store, finalScore};
+                    return new Object[] { store, finalScore };
                 })
                 .filter(item -> {
                     // 距离优先模式:过滤掉超出范围的
@@ -3682,6 +3772,40 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         return storeInfoVoList;
     }
 
+    /**
+     * 门店证照查询(分页)
+     *
+     * @param pageNum         页码
+     * @param pageSize        页容
+     * @param storeName       门店名称(模糊)
+     * @param storeTel        门店电话(模糊)
+     * @param imgType         证照图片类型(14:营业执照;24/25:食品经营许可证;31/32:娱乐经营许可证)
+     * @param states          证照状态
+     * @param startSubmitDate 提交开始时间(yyyy-MM-dd HH:mm:ss)
+     * @param endSubmitDate   提交结束时间(yyyy-MM-dd HH:mm:ss)
+     * @return 证照分页结果
+     */
+    @Override
+    public IPage<StoreLicenseInfoVo> getStoreLicenseList(int pageNum,
+                                                         int pageSize,
+                                                         String storeName,
+                                                         String storeTel,
+                                                         Integer imgType,
+                                                         String states,
+                                                         String startSubmitDate,
+                                                         String endSubmitDate) {
+        IPage<StoreLicenseInfoVo> page = new Page<>(pageNum, pageSize);
+        IPage<StoreLicenseInfoVo> storeLicensePage = storeInfoMapper.getStoreLicensePage(page, storeName, storeTel, imgType, states, startSubmitDate, endSubmitDate);
+        for (StoreLicenseInfoVo record : storeLicensePage.getRecords()) {
+            if(record.getStates() != null){
+                StoreDictionary storeDictionary = storeDictionaryMapper.selectOne(new LambdaQueryWrapper<StoreDictionary>().eq(StoreDictionary::getDictId, record.getStates())
+                        .eq(StoreDictionary::getTypeName, "foodLicenceStatus"));
+                record.setStatesName(storeDictionary.getDictDetail());
+            }
+        }
+        return storeLicensePage;
+    }
+
 
     /**
      * 构建树形结构(优化版)
@@ -4649,6 +4773,42 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         return null;
     }
 
+
+    /**
+     * 解析营业执照OCR结果中的到期时间(复用营业执照OCR类型)
+     * 营业执照OCR返回字段:validToDate(格式"20241217"或空)、validPeriod(可能为空)
+     * 注意:营业执照的validToDate可能为空,表示长期有效
+     *
+     * @param ocrResult OCR识别结果JSON对象
+     * @return 到期日期,如果解析失败或为长期有效则返回null
+     */
+    private Date parseBusinessLicenseExpirationDate(com.alibaba.fastjson2.JSONObject ocrResult) {
+        if (ocrResult == null) {
+            return null;
+        }
+
+        // 优先使用validToDate字段(格式为yyyyMMdd,如"20241217",可能为空表示长期有效)
+        String validToDate = ocrResult.getString("validToDate");
+        if (StringUtils.isNotEmpty(validToDate)) {
+            Date date = parseDateString(validToDate);
+            if (date != null) {
+                return date;
+            }
+        }
+
+        // 其次使用validPeriod字段(格式为"2020年09月04日至2022年09月03日",可能为空)
+        String validPeriod = ocrResult.getString("validPeriod");
+        if (StringUtils.isNotEmpty(validPeriod)) {
+            Date date = parseValidPeriodEndDate(validPeriod);
+            if (date != null) {
+                return date;
+            }
+        }
+
+        log.info("营业执照OCR结果中未找到有效的到期时间,validToDate={},validPeriod={},可能为长期有效", validToDate, validPeriod);
+        return null;
+    }
+
     /**
      * 解析日期字符串
      * 支持的格式:yyyyMMdd、yyyy-MM-dd、yyyy/MM/dd、yyyy年MM月dd日
@@ -4722,6 +4882,157 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         return parseDateString(endDateStr);
     }
 
+    /**
+     * 保存身份证正反面图片到store_img表,并解析OCR结果设置过期时间和状态
+     *
+     * @param storeId   门店ID
+     * @param storeUserId 店铺用户ID
+     * @param storeInfo 门店信息对象(用于设置身份证状态和过期时间)
+     */
+    private void saveIdCardImages(Integer storeId, String storeUserId, StoreInfo storeInfo) {
+        try {
+            // 查询身份证OCR识别记录(根据storeUserId和ocrType=ID_CARD)
+            List<OcrImageUpload> idCardOcrList = ocrImageUploadMapper.selectList(
+                    new LambdaQueryWrapper<OcrImageUpload>()
+                            .eq(OcrImageUpload::getStoreUserId, storeUserId)
+                            .eq(OcrImageUpload::getOcrType, OcrTypeEnum.ID_CARD.getCode())
+                            .orderByDesc(OcrImageUpload::getCreateTime)
+            );
+
+            if (CollectionUtils.isEmpty(idCardOcrList)) {
+                log.info("未找到身份证OCR记录,storeUserId={}", storeUserId);
+                // 没有OCR记录时,初始化状态为"未提交"(字典值0)
+                storeInfo.setIdCardStatus(0);
+                return;
+            }
+
+            String idCardFrontUrl = null;
+            String idCardBackUrl = null;
+            Date idCardExpirationTime = null;
+
+            // 遍历OCR记录,区分正面和反面
+            for (OcrImageUpload ocrRecord : idCardOcrList) {
+                if (StringUtils.isEmpty(ocrRecord.getOcrResult())) {
+                    continue;
+                }
+
+                try {
+                    // 解析OCR结果JSON
+                    com.alibaba.fastjson2.JSONObject ocrResultJson = com.alibaba.fastjson2.JSONObject.parseObject(ocrRecord.getOcrResult());
+
+                    // 检查是正面还是反面
+                    if (ocrResultJson.containsKey("face")) {
+                        // 身份证正面
+                        idCardFrontUrl = ocrRecord.getImageUrl();
+                        log.info("找到身份证正面图片,URL={}", idCardFrontUrl);
+                    } else if (ocrResultJson.containsKey("back")) {
+                        // 身份证反面
+                        idCardBackUrl = ocrRecord.getImageUrl();
+                        log.info("找到身份证反面图片,URL={}", idCardBackUrl);
+
+                        // 从反面OCR结果中提取有效期限
+                        com.alibaba.fastjson2.JSONObject backData = ocrResultJson.getJSONObject("back");
+                        if (backData != null) {
+                            com.alibaba.fastjson2.JSONObject data = backData.getJSONObject("data");
+                            if (data != null) {
+                                String validPeriod = data.getString("validPeriod");
+                                if (StringUtils.isNotEmpty(validPeriod)) {
+                                    idCardExpirationTime = parseIdCardExpirationDate(validPeriod);
+                                    if (idCardExpirationTime != null) {
+                                        log.info("解析身份证有效期限成功,过期时间:{}", idCardExpirationTime);
+                                    }
+                                }
+                            }
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("解析身份证OCR结果失败,ocrRecordId={}", ocrRecord.getId(), e);
+                }
+            }
+
+            // 保存身份证正面图片到store_img表(img_type=33)
+            if (StringUtils.isNotEmpty(idCardFrontUrl)) {
+                StoreImg frontImg = new StoreImg();
+                frontImg.setStoreId(storeId);
+                frontImg.setImgType(33);
+                frontImg.setImgSort(1);
+                frontImg.setImgDescription("身份证正面");
+                frontImg.setImgUrl(idCardFrontUrl);
+                storeImgMapper.insert(frontImg);
+                log.info("保存身份证正面图片成功,storeId={}, imgUrl={}", storeId, idCardFrontUrl);
+            }
+
+            // 保存身份证反面图片到store_img表(img_type=34)
+            if (StringUtils.isNotEmpty(idCardBackUrl)) {
+                StoreImg backImg = new StoreImg();
+                backImg.setStoreId(storeId);
+                backImg.setImgType(34);
+                backImg.setImgSort(2);
+                backImg.setImgDescription("身份证反面");
+                backImg.setImgUrl(idCardBackUrl);
+                storeImgMapper.insert(backImg);
+                log.info("保存身份证反面图片成功,storeId={}, imgUrl={}", storeId, idCardBackUrl);
+            }
+
+            // 设置身份证状态和过期时间
+            if (StringUtils.isNotEmpty(idCardFrontUrl) || StringUtils.isNotEmpty(idCardBackUrl)) {
+                // 有身份证图片时,设置状态为"待审核"(字典值2,使用foodLicenceStatus字典)
+                storeInfo.setIdCardStatus(2);
+                storeInfo.setUpdateIdCardTime(new Date());
+                if (idCardExpirationTime != null) {
+                    storeInfo.setIdCardExpirationTime(idCardExpirationTime);
+                }
+                log.info("设置身份证状态为待审核,过期时间:{}", idCardExpirationTime);
+            } else {
+                // 没有身份证图片时,初始化状态为"未提交"(字典值0)
+                storeInfo.setIdCardStatus(0);
+            }
+
+        } catch (Exception e) {
+            log.error("保存身份证图片失败,storeId={}, storeUserId={}", storeId, storeUserId, e);
+            // 发生异常时,初始化状态为"未提交"(字典值0)
+            storeInfo.setIdCardStatus(0);
+        }
+    }
+
+    /**
+     * 解析身份证有效期限
+     * 身份证OCR返回格式:validPeriod="2023.05.29-2033.05.29"
+     *
+     * @param validPeriod 有效期限字符串,格式:"2023.05.29-2033.05.29"
+     * @return 到期日期,如果解析失败则返回null
+     */
+    private Date parseIdCardExpirationDate(String validPeriod) {
+        if (StringUtils.isEmpty(validPeriod)) {
+            return null;
+        }
+
+        // 处理"长期"或"永久"情况
+        if (validPeriod.contains("长期") || validPeriod.contains("永久")) {
+            return null;
+        }
+
+        try {
+            // 身份证格式:2023.05.29-2033.05.29,提取结束日期
+            if (validPeriod.contains("-")) {
+                String[] parts = validPeriod.split("-");
+                if (parts.length >= 2) {
+                    String endDateStr = parts[1].trim();
+                    // 将 "2023.05.29" 格式转换为 "2023-05-29"
+                    endDateStr = endDateStr.replace(".", "-");
+                    return parseDateString(endDateStr);
+                }
+            }
+
+            // 如果格式不匹配,尝试直接解析
+            String normalizedDateStr = validPeriod.replace(".", "-");
+            return parseDateString(normalizedDateStr);
+        } catch (Exception e) {
+            log.warn("解析身份证有效期限失败,validPeriod={}", validPeriod, e);
+            return null;
+        }
+    }
+
     @Override
     public StoreInfoVo getClientStoreDetail(String storeId, String userId, String jingdu, String weidu) {
         StoreInfoVo result = new StoreInfoVo();

BIN
alien-store/src/main/resources/templates/举报导入模版.xlsx


BIN
alien-store/src/main/resources/templates/意见反馈导入模版.xlsx


BIN
alien-store/src/main/resources/templates/筛选条件导入模版.xlsx


BIN
alien-store/src/main/resources/templates/门店标签导入模版.xlsx