Explorar o código

Merge branch 'sit' into release_buried-point_ph

lutong hai 3 meses
pai
achega
4bafb129c1
Modificáronse 44 ficheiros con 4280 adicións e 596 borrados
  1. 9 0
      alien-entity/src/main/java/shop/alien/entity/store/CommonComment.java
  2. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/CommonRating.java
  3. 14 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreOperationalStatisticsHistory.java
  4. 8 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreStaffReview.java
  5. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationRequirementDto.java
  6. 4 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CommonRatingVo.java
  7. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeBlacklistVo.java
  8. 33 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreBusinessStatusVo.java
  9. 2 2
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreCommentAppealVo.java
  10. 32 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreOperationalStatisticsComparisonVo.java
  11. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreOperationalStatisticsVo.java
  12. 33 11
      alien-entity/src/main/java/shop/alien/mapper/CommonCommentMapper.java
  13. 10 0
      alien-entity/src/main/java/shop/alien/mapper/CommonRatingMapper.java
  14. 23 16
      alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealMapper.java
  15. 33 1
      alien-store/src/main/java/shop/alien/store/config/AsyncConfig.java
  16. 1 1
      alien-store/src/main/java/shop/alien/store/controller/CommonCommentController.java
  17. 23 14
      alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java
  18. 12 2
      alien-store/src/main/java/shop/alien/store/controller/LifeBlacklistController.java
  19. 176 2
      alien-store/src/main/java/shop/alien/store/controller/StoreFileUploadController.java
  20. 10 0
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  21. 113 3
      alien-store/src/main/java/shop/alien/store/controller/StoreOperationalStatisticsController.java
  22. 2 0
      alien-store/src/main/java/shop/alien/store/service/CommonCommentService.java
  23. 4 6
      alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java
  24. 9 1
      alien-store/src/main/java/shop/alien/store/service/LifeBlacklistService.java
  25. 4 5
      alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java
  26. 9 0
      alien-store/src/main/java/shop/alien/store/service/StoreInfoService.java
  27. 32 0
      alien-store/src/main/java/shop/alien/store/service/StoreOperationalStatisticsService.java
  28. 93 11
      alien-store/src/main/java/shop/alien/store/service/impl/CommonCommentServiceImpl.java
  29. 202 115
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  30. 12 12
      alien-store/src/main/java/shop/alien/store/service/impl/LifeBlacklistServiceImpl.java
  31. 2 0
      alien-store/src/main/java/shop/alien/store/service/impl/LifeMessageServiceImpl.java
  32. 24 8
      alien-store/src/main/java/shop/alien/store/service/impl/PerformanceListServiceImpl.java
  33. 135 7
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealServiceImpl.java
  34. 391 298
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  35. 938 60
      alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalStatisticsServiceImpl.java
  36. 33 3
      alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java
  37. 8 2
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffCommentServiceImpl.java
  38. 73 16
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffReviewServiceImpl.java
  39. 7 0
      alien-store/src/main/java/shop/alien/store/util/CommonConstant.java
  40. 33 0
      alien-store/src/main/java/shop/alien/store/util/FileUploadUtil.java
  41. 242 0
      alien-store/src/main/java/shop/alien/store/util/ImageToPdfUploadUtil.java
  42. 733 0
      alien-store/src/main/java/shop/alien/store/util/StatisticsComparisonImageUtil.java
  43. 63 0
      alien-util/src/main/java/shop/alien/util/ali/AliOSSUtil.java
  44. 682 0
      alien-util/src/main/java/shop/alien/util/pdf/ImageToPdfUtil.java

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

@@ -80,6 +80,11 @@ public class CommonComment implements Serializable {
     @TableField("reply_count")
     private Integer replyCount;
 
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+
     @ApiModelProperty(value = "创建时间")
     @TableField(value = "created_time", fill = FieldFill.INSERT)
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@@ -93,5 +98,9 @@ public class CommonComment implements Serializable {
     @ApiModelProperty(value = "扩展字段:如评论楼层")
     @TableField(value = "ext_info", typeHandler = com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler.class)
     private String extInfo;
+
+    @ApiModelProperty(value = "审核失败原因")
+    @TableField(value = "audit_reason")
+    private String auditReason;
 }
 

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/CommonRating.java

@@ -106,5 +106,9 @@ public class CommonRating implements Serializable {
     @ApiModelProperty(value = "修改人ID")
     @TableField("updated_user_id")
     private Integer updatedUserId;
+
+    @ApiModelProperty(value = "审核失败原因")
+    @TableField(value = "audit_reason")
+    private String auditReason;
 }
 

+ 14 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreOperationalStatisticsHistory.java

@@ -39,6 +39,16 @@ public class StoreOperationalStatisticsHistory {
     @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
     private Date endTime;
 
+    @ApiModelProperty(value = "上期开始时间")
+    @TableField("previous_start_time")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date previousStartTime;
+
+    @ApiModelProperty(value = "上期结束时间")
+    @TableField("previous_end_time")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date previousEndTime;
+
     @ApiModelProperty(value = "统计数据(JSON格式,包含所有统计字段)")
     @TableField("statistics_data")
     private String statisticsData;
@@ -48,6 +58,10 @@ public class StoreOperationalStatisticsHistory {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date queryTime;
 
+    @ApiModelProperty(value = "PDF文件URL(前端生成的PDF文件地址)")
+    @TableField("pdf_url")
+    private String pdfUrl;
+
     @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
     @TableField("delete_flag")
     @TableLogic

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

@@ -96,5 +96,13 @@ public class StoreStaffReview {
     @ApiModelProperty(value = "申诉id (如果该评价被申诉 申诉id会有值)")
     @TableField("appeal_id")
     private String appealId;
+
+    @ApiModelProperty(value = "审核状态:0-待审核 1-通过 2-驳回")
+    @TableField("audit_status")
+    private Integer auditStatus;
+
+    @ApiModelProperty(value = "审核失败原因")
+    @TableField(value = "audit_reason")
+    private String auditReason;
 }
 

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationRequirementDto.java

@@ -104,6 +104,9 @@ public class StoreRenovationRequirementDto {
     @ApiModelProperty(value = "商铺简介")
     private String storeBlurb;
 
+    @ApiModelProperty(value = "商铺头像")
+    private String storeAvatar;
+
     @ApiModelProperty(value = "是否已与发布商铺发生沟通(true:已沟通, false:未沟通)")
     private Boolean hasCommunicated;
 }

+ 4 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonRatingVo.java

@@ -38,4 +38,8 @@ public class CommonRatingVo extends CommonRating {
     private Long commentCount;
     @ApiModelProperty(name = "isFollow", value = "是否关注")
     private Integer isFollow;
+    @ApiModelProperty(name = "appealStatus", value = "申诉状态:null-未申诉, 0-待处理, 1-已驳回, 2-已同意, 3-处理中")
+    private Integer appealStatus;
+    @ApiModelProperty(name = "appealFlag", value = "是否已申诉:0-未申诉, 1-已申诉")
+    private Integer appealFlag;
 }

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

@@ -7,6 +7,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import shop.alien.entity.store.LifeBlacklist;
+import shop.alien.entity.store.StoreInfo;
 
 /**
  * @Author: fcw
@@ -29,4 +30,6 @@ public class LifeBlacklistVo extends LifeBlacklist {
     @ApiModelProperty(value = "用户头像")
     private String phoneId;
 
+    @ApiModelProperty(value = "店铺信息")
+    private StoreInfo storeInfo;
 }

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreBusinessStatusVo.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.store.StoreBusinessInfo;
+
+import java.util.List;
+
+/**
+ * 店铺营业状态VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "StoreBusinessStatusVo对象", description = "店铺营业状态")
+public class StoreBusinessStatusVo {
+
+    @ApiModelProperty(value = "是否营业中(0否1是)")
+    private Integer yyFlag;
+
+    @ApiModelProperty(value = "当前营业时间信息")
+    private StoreBusinessInfo storeBusinessInfo;
+
+    @ApiModelProperty(value = "所有营业时间信息列表(已排序)")
+    private List<StoreBusinessInfo> storeBusinessInfos;
+
+    @ApiModelProperty(value = "店铺营业状态(0:正常营业, 其他:非正常营业)")
+    private Integer businessStatus;
+}

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

@@ -35,10 +35,10 @@ public class StoreCommentAppealVo extends StoreCommentAppeal {
     private String commentContent;
 
     @ApiModelProperty(value = "顾客评论星级")
-    private Integer commentStar;
+    private Double commentStar;
 
     @ApiModelProperty(value = "评分")
-    private String score;
+    private Double score;
 
     @ApiModelProperty(value = "其他评分")
     private String otherScore;

+ 32 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreOperationalStatisticsComparisonVo.java

@@ -6,6 +6,7 @@ import lombok.Data;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
+import java.util.List;
 
 /**
  * 商家经营统计数据对比VO(包含当期、上期和变化率)
@@ -32,6 +33,9 @@ public class StoreOperationalStatisticsComparisonVo implements Serializable {
     @ApiModelProperty("上期结束时间")
     private String previousEndTime;
 
+    @ApiModelProperty("历史记录ID(用于后续更新PDF URL)")
+    private Integer historyId;
+
     /**
      * 流量数据对比
      */
@@ -63,6 +67,12 @@ public class StoreOperationalStatisticsComparisonVo implements Serializable {
     private ServiceQualityDataComparison serviceQualityData;
 
     /**
+     * 价目表排名数据对比
+     */
+    @ApiModelProperty("价目表排名数据对比")
+    private List<PriceListRankingComparison> priceListRanking;
+
+    /**
      * 基础对比数据
      */
     @Data
@@ -265,4 +275,26 @@ public class StoreOperationalStatisticsComparisonVo implements Serializable {
         @ApiModelProperty("差评申诉成功占比对比")
         private BaseComparisonData negativeReviewAppealsSuccessRatio;
     }
+
+    /**
+     * 价目表排名数据对比
+     */
+    @Data
+    @ApiModel("价目表排名数据对比")
+    public static class PriceListRankingComparison implements Serializable {
+        @ApiModelProperty("价目表ID")
+        private Integer priceId;
+
+        @ApiModelProperty("价目表名称")
+        private String priceListItemName;
+
+        @ApiModelProperty("浏览量对比")
+        private BaseComparisonData pageViews;
+
+        @ApiModelProperty("访客数对比")
+        private BaseComparisonData visitors;
+
+        @ApiModelProperty("分享数对比")
+        private BaseComparisonData shares;
+    }
 }

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

@@ -254,6 +254,9 @@ public class StoreOperationalStatisticsVo implements Serializable {
         @ApiModelProperty("排名")
         private Integer rank;
 
+        @ApiModelProperty("价目表ID")
+        private Integer priceId;
+
         @ApiModelProperty("价目表名称")
         private String priceListItemName;
 

+ 33 - 11
alien-entity/src/main/java/shop/alien/mapper/CommonCommentMapper.java

@@ -7,6 +7,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 import shop.alien.entity.store.CommonComment;
 import shop.alien.entity.store.vo.CommonCommentVo;
 
@@ -22,17 +23,26 @@ import java.util.List;
 public interface CommonCommentMapper extends BaseMapper<CommonComment> {
 
 
-    @Select("select *,if(comment_type = 1,lu.user_image,su.head_img) headImg,\n" +
-            "if(comment_type = 1,lu.user_name ,su.nick_name ) headName," +
-            "if(llr.dianzan_id is null,'0','1') isLike\n" +
-            "from common_comment cc\n" +
-            "left join life_user lu on cc.user_id = lu.id and lu.delete_flag = 0\n" +
-            "left join store_user su on cc.merchant_id = su.store_id and su.delete_flag = 0\n" +
-            "left join life_like_record llr on\n" +
-            "llr.huifu_id = cc.id\n" +
-            "and llr.`type` = #{type}\n" +
-            "and llr.dianzan_id = #{dianzanId}\n" +
-            "and llr.delete_flag = 0\n" +
+    /**
+     * 查询评论列表(含用户/商户信息)
+     * 
+     * 关联说明:
+     * - 用户评论(comment_type=1):关联 life_user 表获取用户头像和昵称
+     * - 商户评论(comment_type=2):关联 store_user 表获取商户头像和昵称
+     *   (通过子查询取每个店铺的主账号记录,即 id 最小的记录)
+     */
+    @Select("SELECT cc.*, " +
+            "IF(cc.comment_type = 1, lu.user_image, su.head_img) AS headImg, " +
+            "IF(cc.comment_type = 1, lu.user_name, su.nick_name) AS headName, " +
+            "IF(llr.dianzan_id IS NULL, '0', '1') AS isLike " +
+            "FROM common_comment cc " +
+            "LEFT JOIN life_user lu ON cc.user_id = lu.id AND lu.delete_flag = 0 " +
+            "LEFT JOIN store_user su ON cc.merchant_id = su.store_id AND su.delete_flag = 0 " +
+            "AND su.id = (SELECT MIN(su2.id) FROM store_user su2 WHERE su2.store_id = cc.merchant_id AND su2.delete_flag = 0) " +
+            "LEFT JOIN life_like_record llr ON llr.huifu_id = cc.id " +
+            "AND llr.`type` = #{type} " +
+            "AND llr.dianzan_id = #{dianzanId} " +
+            "AND llr.delete_flag = 0 " +
             "${ew.customSqlSegment}")
     List<CommonCommentVo> selectALlComment(@Param("page") Page<CommonCommentVo> page,
                                            @Param(Constants.WRAPPER)QueryWrapper<CommonCommentVo> wrapper,
@@ -43,7 +53,19 @@ public interface CommonCommentMapper extends BaseMapper<CommonComment> {
             "from common_comment cc\n" +
             "where \n" +
             "cc.source_type = #{businessType}\n" +
+            "and cc.delete_flag = 0\n" +
             "group by source_id")
     List<CommonCommentVo> getCommentCount(Integer type);
+
+    /**
+     * 批量逻辑删除评论(使用原生 SQL 绕过 @TableLogic 注解限制)
+     *
+     * @param sourceType 来源类型
+     * @param sourceId   来源ID(评价ID)
+     * @return 影响行数
+     */
+    @Update("UPDATE common_comment SET delete_flag = 1, updated_time = NOW() " +
+            "WHERE source_type = #{sourceType} AND source_id = #{sourceId} AND delete_flag = 0")
+    int logicDeleteBySourceId(@Param("sourceType") Integer sourceType, @Param("sourceId") Integer sourceId);
 }
 

+ 10 - 0
alien-entity/src/main/java/shop/alien/mapper/CommonRatingMapper.java

@@ -6,6 +6,7 @@ import com.baomidou.mybatisplus.core.toolkit.Constants;
 import org.apache.ibatis.annotations.Mapper;
 import org.apache.ibatis.annotations.Param;
 import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
 import shop.alien.entity.store.CommonRating;
 import shop.alien.entity.store.vo.StoreInfoScoreVo;
 
@@ -57,5 +58,14 @@ public interface CommonRatingMapper extends BaseMapper<CommonRating> {
             "    common_rating\n" +
             "   ${ew.customSqlSegment}\n")
     Map<String, Object> getRatingCount(@Param(Constants.WRAPPER) QueryWrapper<CommonRating> wrapper);
+
+    /**
+     * 逻辑删除评价(使用原生 SQL 绕过 @TableLogic 注解限制)
+     *
+     * @param id 评价ID
+     * @return 影响行数
+     */
+    @Update("UPDATE common_rating SET delete_flag = 1, updated_time = NOW() WHERE id = #{id}")
+    int logicDeleteById(@Param("id") Integer id);
 }
 

+ 23 - 16
alien-entity/src/main/java/shop/alien/mapper/StoreCommentAppealMapper.java

@@ -23,15 +23,17 @@ public interface StoreCommentAppealMapper extends BaseMapper<StoreCommentAppeal>
 
     /**
      * 申诉历史-分页查询
+     * 注意:comment_id 关联的是 common_rating.id(评价ID),不是 store_comment.id
+     * 注意:common_rating 表不加 delete_flag = 0 条件,以便申诉通过后仍能显示评价快照数据
      *
      * @param page         分页参数
      * @param queryWrapper 查询条件
      * @return IPage<StoreCommentAppealVo>
      */
-    @Select("select a.*, b.dict_detail appeal_status_str, c.comment_content, c.comment_star, c.score score, c.other_score,c.is_anonymous,c.img_id commentImgId, d.user_name, d.user_image, e.store_name, f.phone store_phone, f.name store_contact, e.store_type " +
+    @Select("select a.*, b.dict_detail appeal_status_str, c.content comment_content, c.score comment_star, c.score score, c.other_score, c.is_anonymous, c.image_urls commentImgId, d.user_name, d.user_image, e.store_name, f.phone store_phone, f.name store_contact, e.store_type " +
             "from store_comment_appeal a " +
             "left join store_dictionary b on a.appeal_status = b.dict_id and b.type_name = 'appealStatus' and b.delete_flag = 0 " +
-            "left join store_comment c on a.comment_id = c.id " +
+            "left join common_rating c on a.comment_id = c.id " +
             "left join life_user d on c.user_id = d.id and d.delete_flag = 0 " +
             "left join store_info e on a.store_id = e.id and e.delete_flag = 0 " +
             "Left join store_user f on e.id = f.store_id and f.delete_flag = 0 ${ew.customSqlSegment}")
@@ -39,43 +41,48 @@ public interface StoreCommentAppealMapper extends BaseMapper<StoreCommentAppeal>
 
     /**
      * 申诉历史-全部查询
+     * 注意:comment_id 关联的是 common_rating.id(评价ID),不是 store_comment.id
+     * 注意:common_rating 表不加 delete_flag = 0 条件,以便申诉通过后仍能显示评价快照数据
      *
      * @param queryWrapper 查询条件
-     * @return IPage<StoreCommentAppealVo>
+     * @return List<StoreCommentAppealVo>
      */
-    @Select("select a.*, b.dict_detail appeal_status_str, c.comment_content, c.comment_star, d.user_name, d.user_image, e.store_name, f.phone store_phone, f.name store_contact, e.store_type from store_comment_appeal a " +
-            "left join store_dictionary b on a.appeal_status = b.dict_id and b.type_name = 'appealStatus' " +
-            "left join store_comment c on a.comment_id = c.id " +
-            "left join life_user d on c.user_id = d.id  and d.delete_flag = 0 " +
-            "left join store_info e on a.store_id = e.id " +
+    @Select("select a.*, b.dict_detail appeal_status_str, c.content comment_content, c.score comment_star, d.user_name, d.user_image, e.store_name, f.phone store_phone, f.name store_contact, e.store_type from store_comment_appeal a " +
+            "left join store_dictionary b on a.appeal_status = b.dict_id and b.type_name = 'appealStatus' and b.delete_flag = 0 " +
+            "left join common_rating c on a.comment_id = c.id " +
+            "left join life_user d on c.user_id = d.id and d.delete_flag = 0 " +
+            "left join store_info e on a.store_id = e.id and e.delete_flag = 0 " +
             "Left join store_user f on e.id = f.store_id and f.delete_flag = 0 ${ew.customSqlSegment}")
     List<StoreCommentAppealVo> getStoreCommentAppealPage(@Param(Constants.WRAPPER) QueryWrapper<StoreCommentAppealVo> queryWrapper);
 
     /**
      * 申诉详情
+     * 注意:comment_id 关联的是 common_rating.id(评价ID),不是 store_comment.id
+     * 注意:common_rating 表不加 delete_flag = 0 条件,以便申诉通过后仍能显示评价快照数据
      *
      * @param queryWrapper 查询条件
      * @return StoreCommentAppealVo
      */
-    @Select("select a.*, b.dict_detail appeal_status_str, c.comment_content, c.score, c.other_score, a.created_time comment_time, c.comment_star,c.is_anonymous,c.img_id commentImgId, d.user_name, e.phone store_phone, d.user_name userName, d.user_image userImage " +
+    @Select("select a.*, b.dict_detail appeal_status_str, c.content comment_content, c.score, c.other_score, a.created_time comment_time, c.score comment_star, c.is_anonymous, c.image_urls commentImgId, d.user_name, e.phone store_phone, d.user_name userName, d.user_image userImage " +
             "from store_comment_appeal a " +
-            "left join store_dictionary b on a.appeal_status = b.dict_id and b.type_name = 'appealStatus' " +
-            "left join store_comment c on a.comment_id = c.id  " +
+            "left join store_dictionary b on a.appeal_status = b.dict_id and b.type_name = 'appealStatus' and b.delete_flag = 0 " +
+            "left join common_rating c on a.comment_id = c.id " +
             "left join life_user d on c.user_id = d.id and d.delete_flag = 0 " +
-            "left join store_user e on a.store_id = e.store_id and e.delete_flag = 0  ${ew.customSqlSegment}")
+            "left join store_user e on a.store_id = e.store_id and e.delete_flag = 0 ${ew.customSqlSegment}")
     StoreCommentAppealVo getCommentDetail(@Param(Constants.WRAPPER) QueryWrapper<StoreCommentAppealVo> queryWrapper);
 
 
     /**
-     * 申诉列表
+     * 申诉列表(平台端使用)
+     * 注意:comment_id 关联的是 common_rating.id(评价ID),不是 store_comment.id
+     * 注意:common_rating 表不加 delete_flag = 0 条件,以便申诉通过后仍能显示评价快照数据
      *
      * @return List<Map < String, Object>>
      */
-    @Select("SELECT sca.id, sca.appeal_reason, sca.appeal_status, sc.comment_content, si.img_url, sci.user_img_url, sc.id comment_id, sc.business_id " +
+    @Select("SELECT sca.id, sca.appeal_reason, sca.appeal_status, cr.content comment_content, si.img_url, cr.image_urls user_img_url, cr.id comment_id, cr.business_id " +
             "FROM store_comment_appeal sca " +
-            "LEFT JOIN store_comment sc ON sca.comment_id = sc.id " +
+            "LEFT JOIN common_rating cr ON sca.comment_id = cr.id " +
             "LEFT JOIN store_img si ON sca.img_id = si.id " +
-            "LEFT JOIN (SELECT sc.id id, si.img_url user_img_url FROM store_comment sc LEFT JOIN store_img si ON sc.img_id = si.id AND sc.delete_flag = 0) sci ON sci.id = sca.comment_id " +
             "WHERE sca.appeal_status = 0 and record_id is null AND sca.delete_flag = 0")
     List<Map<String, Object>> getAppealList();
 

+ 33 - 1
alien-store/src/main/java/shop/alien/store/config/AsyncConfig.java

@@ -5,7 +5,7 @@ import org.springframework.context.annotation.Configuration;
 import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
 
-import java.util.concurrent.Executor;
+import java.util.concurrent.*;
 
 /**
  * 异步任务配置类
@@ -44,5 +44,37 @@ public class AsyncConfig {
         executor.initialize();
         return executor;
     }
+
+    @Bean(name = "commonVideoTaskExecutor")
+    public ExecutorService initExecutor() {
+        // 核心参数根据业务调整,IO密集型任务线程数可设为CPU核心数*2~4
+        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
+        int maxPoolSize = Runtime.getRuntime().availableProcessors() * 4;
+        ExecutorService  videoAuditExecutor = new ThreadPoolExecutor(
+                corePoolSize,
+                maxPoolSize,
+                60L,
+                TimeUnit.SECONDS,
+                new LinkedBlockingQueue<>(500), // 任务队列,避免无界队列溢出
+                new ThreadFactory() {
+                    private int count = 0;
+
+                    @Override
+                    public Thread newThread(Runnable r) {
+                        Thread t = new Thread(r);
+                        t.setName("video-audit-" + count++); // 自定义线程名,便于排查
+                        return t;
+                    }
+                },
+                // 任务拒绝策略:记录日志+调用者线程兜底(避免任务丢失)
+                new ThreadPoolExecutor.CallerRunsPolicy() {
+                    @Override
+                    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
+                        super.rejectedExecution(r, e);
+                    }
+                }
+        );
+        return videoAuditExecutor;
+    }
 }
 

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

@@ -89,7 +89,7 @@ public class CommonCommentController {
     @ApiOperation(value = "删除评论", notes = "0:成功, 1:失败")
     @GetMapping("/deleteComment")
     public R deleteComment(@RequestParam Long commentId) {
-        boolean b = commonCommentService.removeById(commentId);
+        boolean b = commonCommentService.deleteComment(commentId);
         if (b) {
             return R.success("删除评论成功");
         }

+ 23 - 14
alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java

@@ -68,19 +68,6 @@ public class CommonRatingController {
         return R.data(commonRatingService.getRatingCount(businessId, businessType));
     }
 
-    @ApiOperation("获取回复率和评价比例(商户端)")
-    @ApiImplicitParams({
-            @ApiImplicitParam(name = "businessId", value = "业务ID(店铺ID)", dataType = "Integer", paramType = "query", required = true),
-            @ApiImplicitParam(name = "businessType", value = "业务类型:1-店铺评价", dataType = "Integer", paramType = "query", required = true)
-    })
-    @GetMapping("/getRatingPercent")
-    public R<shop.alien.entity.store.vo.RatingPercentVo> getRatingPercent(
-            @RequestParam Integer businessId,
-            @RequestParam Integer businessType) {
-        log.info("CommonRatingController.getRatingPercent?businessId={}&businessType={}", businessId, businessType);
-        return R.data(commonRatingService.getRatingPercent(businessId, businessType));
-    }
-
 //
 //    @ApiOperation("根据ID获取评价详情")
 //    @ApiImplicitParam(name = "id", value = "评价ID", dataType = "Long", paramType = "path", required = true)
@@ -124,15 +111,37 @@ public class CommonRatingController {
         }
     }
 
-    /**删除评价
+    /**
+     * 删除评价(并重新统计店铺评分)
+     * 
      * @param ratingId 评价id
      * @return 0:成功, 1:失败
      */
     @ApiOperation(value = "删除评价", notes = "0:成功, 1:失败")
     @GetMapping("/deleteRating")
     public R deleteRating(@RequestParam Long ratingId) {
+        log.info("删除评价,ratingId={}", ratingId);
+        
+        // 先获取评价信息(用于后续更新评分)
+        CommonRating rating = commonRatingService.getById(ratingId);
+        if (rating == null) {
+            return R.fail("评价不存在");
+        }
+        
+        // 删除评价
         boolean b = commonRatingService.removeById(ratingId);
+        
         if (b) {
+            // 删除成功后,重新统计店铺评分(仅店铺评价类型)
+            if (rating.getBusinessType() != null && rating.getBusinessType() == 1) {
+                try {
+                    commonRatingService.updateStoreScoreAfterDelete(rating.getBusinessId());
+                    log.info("更新店铺评分成功,businessId={}", rating.getBusinessId());
+                } catch (Exception e) {
+                    log.error("更新店铺评分失败,businessId={},error={}", rating.getBusinessId(), e.getMessage());
+                    // 评分更新失败不影响删除结果
+                }
+            }
             return R.success("删除评价成功");
         }
         return R.fail("删除评价失败");

+ 12 - 2
alien-store/src/main/java/shop/alien/store/controller/LifeBlacklistController.java

@@ -57,11 +57,21 @@ public class LifeBlacklistController {
         }
     }
 
+    /**
+     * 根据用户ID查询拉黑列表
+     *
+     * @param userType 用户类型
+     * @param userId   用户ID
+     * @param type     查找类型 1-被拉黑商户列表 2-拉黑用户列表
+     * @return 拉黑列表
+     */
     @ApiOperation("拉黑列表")
     @ApiOperationSupport(order = 2)
     @GetMapping("/blackListByUserId")
-    public R<List<LifeBlacklistVo>> blackListByUserId(@RequestParam(value = "userType") String userType, @RequestParam(value = "userId") String userId) {
-        return R.data(lifeBlacklistService.blackListByUserId(userType, userId));
+    public R<List<LifeBlacklistVo>> blackListByUserId(@RequestParam(value = "userType") String userType,
+                                                      @RequestParam(value = "userId") String userId,
+                                                      @RequestParam(value = "type", defaultValue = "1") Integer type) {
+        return R.data(lifeBlacklistService.blackListByUserId(userType, userId, type));
     }
 
     @ApiOperation("是否为已拉黑")

+ 176 - 2
alien-store/src/main/java/shop/alien/store/controller/StoreFileUploadController.java

@@ -1,17 +1,22 @@
 package shop.alien.store.controller;
 
 import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiImplicitParam;
+import io.swagger.annotations.ApiImplicitParams;
 import io.swagger.annotations.ApiOperation;
 import io.swagger.annotations.ApiOperationSupport;
 import io.swagger.annotations.ApiSort;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.util.StringUtils;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.multipart.MultipartFile;
 import org.springframework.web.multipart.MultipartRequest;
 import shop.alien.entity.result.R;
 import shop.alien.store.util.FileUploadUtil;
+import shop.alien.store.util.ImageToPdfUploadUtil;
 
+import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -31,8 +36,10 @@ import java.util.List;
 public class StoreFileUploadController {
 
     private final FileUploadUtil fileUpload;
+    
+    private final ImageToPdfUploadUtil imageToPdfUploadUtil;
 
-    @ApiOperation("单个文件上传(图片或视频,返回路径)")
+    @ApiOperation("单个文件上传(图片、视频或PDF,返回路径)")
     @ApiOperationSupport(order = 1)
     @PostMapping("/upload")
     public R<String> upload(@RequestParam("file") MultipartFile file) {
@@ -40,7 +47,7 @@ public class StoreFileUploadController {
         return R.data(fileUpload.uploadOneFile(file));
     }
 
-    @ApiOperation("多个文件上传(图片或视频,视频会截取第一秒图片,返回路径)")
+    @ApiOperation("多个文件上传(图片、视频或PDF,视频会截取第一秒图片,返回路径)")
     @ApiOperationSupport(order = 2)
     @PostMapping("/uploadMore")
     public R<List<String>> uploadMore(MultipartRequest multipartRequest) {
@@ -63,4 +70,171 @@ public class StoreFileUploadController {
         return R.data(fileUpload.uploadApp(file));
     }
 
+    @ApiOperation("上传PDF文件(返回路径)")
+    @ApiOperationSupport(order = 5)
+    @PostMapping("/uploadPdf")
+    public R<String> uploadPdf(@RequestParam("file") MultipartFile file) {
+        log.info("StoreFileUploadController.uploadPdf fileName:{}", file.getOriginalFilename());
+        try {
+            String filePath = fileUpload.uploadPdf(file);
+            if (filePath == null) {
+                return R.fail("上传失败,文件不是PDF格式");
+            }
+            return R.data(filePath);
+        } catch (Exception e) {
+            log.error("上传PDF文件失败: {}", e.getMessage(), e);
+            return R.fail("上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("图片转PDF并上传(单张图片,返回PDF的线上URL)")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "file",
+                    value = "图片文件(支持jpg、jpeg、png、bmp、webp、gif格式)",
+                    dataType = "file",
+                    paramType = "form",
+                    required = true
+            )
+    })
+    @PostMapping("/imageToPdf")
+    public R<String> imageToPdf(@RequestParam("file") MultipartFile file) {
+        log.info("StoreFileUploadController.imageToPdf fileName:{}", file.getOriginalFilename());
+        try {
+            // 参数验证
+            if (file == null || file.isEmpty()) {
+                return R.fail("请选择要转换的图片文件");
+            }
+            
+            String pdfUrl = imageToPdfUploadUtil.imageToPdfAndUpload(file);
+            if (pdfUrl == null) {
+                return R.fail("图片转PDF并上传失败,请检查图片格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("图片转PDF并上传(多张图片合并为一个PDF,返回PDF的线上URL)")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "files",
+                    value = "图片文件列表(支持jpg、jpeg、png、bmp、webp、gif格式,可上传多张)",
+                    dataType = "file",
+                    paramType = "form",
+                    required = true,
+                    allowMultiple = true
+            )
+    })
+    @PostMapping("/imagesToPdf")
+    public R<String> imagesToPdf(MultipartRequest multipartRequest) {
+        log.info("StoreFileUploadController.imagesToPdf");
+        try {
+            if (multipartRequest == null) {
+                return R.fail("请求参数不能为空");
+            }
+            
+            List<MultipartFile> imageFiles = new ArrayList<>();
+            for (String key : multipartRequest.getFileMap().keySet()) {
+                MultipartFile file = multipartRequest.getFile(key);
+                if (file != null && !file.isEmpty()) {
+                    imageFiles.add(file);
+                }
+            }
+            
+            if (imageFiles.isEmpty()) {
+                return R.fail("请至少上传一张图片");
+            }
+            
+            log.info("StoreFileUploadController.imagesToPdf 收到{}张图片", imageFiles.size());
+            
+            String pdfUrl = imageToPdfUploadUtil.imagesToPdfAndUpload(imageFiles);
+            if (pdfUrl == null) {
+                return R.fail("图片转PDF并上传失败,请检查图片格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("图片转PDF并上传(A4页面大小,单张图片,返回PDF的线上URL)")
+    @ApiOperationSupport(order = 8)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "file",
+                    value = "图片文件(支持jpg、jpeg、png、bmp、webp、gif格式,图片会自动缩放适应A4页面)",
+                    dataType = "file",
+                    paramType = "form",
+                    required = true
+            )
+    })
+    @PostMapping("/imageToPdfA4")
+    public R<String> imageToPdfA4(@RequestParam("file") MultipartFile file) {
+        log.info("StoreFileUploadController.imageToPdfA4 fileName:{}", file.getOriginalFilename());
+        try {
+            // 参数验证
+            if (file == null || file.isEmpty()) {
+                return R.fail("请选择要转换的图片文件");
+            }
+            
+            String pdfUrl = imageToPdfUploadUtil.imageToPdfA4AndUpload(file);
+            if (pdfUrl == null) {
+                return R.fail("图片转PDF并上传失败,请检查图片格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("Base64图片转PDF并上传(返回PDF的线上URL)")
+    @ApiOperationSupport(order = 9)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "base64Image",
+                    value = "Base64编码的图片字符串(支持带或不带data:image前缀,如:data:image/png;base64,iVBORw0KGgo...)",
+                    dataType = "String",
+                    paramType = "form",
+                    required = true
+            )
+    })
+    @PostMapping("/base64ToPdf")
+    public R<String> base64ToPdf(@RequestParam("base64Image") String base64Image) {
+        log.info("StoreFileUploadController.base64ToPdf 收到Base64图片");
+        try {
+            // 参数验证
+            if (!StringUtils.hasText(base64Image)) {
+                return R.fail("Base64图片字符串不能为空");
+            }
+
+            String pdfUrl = imageToPdfUploadUtil.base64ToPdfAndUpload(base64Image);
+            if (pdfUrl == null) {
+                return R.fail("Base64图片转PDF并上传失败,请检查Base64格式是否正确");
+            }
+            return R.data(pdfUrl);
+        } catch (IllegalArgumentException e) {
+            log.warn("Base64图片转PDF参数错误: {}", e.getMessage());
+            return R.fail("参数错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("Base64图片转PDF并上传失败: {}", e.getMessage(), e);
+            return R.fail("Base64图片转PDF并上传失败: " + e.getMessage());
+        }
+    }
+
 }

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

@@ -1041,6 +1041,16 @@ public class StoreInfoController {
         return R.data(storeDetail);
     }
 
+    @ApiOperation(value = "获取店铺营业状态", notes = "判断店铺当前是否在营业时间内,返回营业状态和营业时间信息")
+    @ApiOperationSupport(order = 17)
+    @GetMapping("/getStoreBusinessStatus")
+    @ResponseBody
+    public R<StoreBusinessStatusVo> getStoreBusinessStatus(@RequestParam("id") String id) {
+        log.info("StoreInfoController.getStoreBusinessStatus?id={}", id);
+        StoreBusinessStatusVo businessStatus = storeInfoService.getStoreBusinessStatus(id);
+        return R.data(businessStatus);
+    }
+
     @ApiOperation(value = "查询当前门店的资质信息", notes = "查询营业执照和其他资质证明图片")
     @ApiOperationSupport(order = 18)
     @GetMapping("/getStoreQualificationInfo")

+ 113 - 3
alien-store/src/main/java/shop/alien/store/controller/StoreOperationalStatisticsController.java

@@ -30,7 +30,7 @@ public class StoreOperationalStatisticsController {
 
     private final StoreOperationalStatisticsService storeOperationalStatisticsService;
 
-    @ApiOperation("获取商家经营统计数据")
+    @ApiOperation("获取商家经营统计数据(符合埋点统计数据JSON格式)")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({
             @ApiImplicitParam(
@@ -61,8 +61,14 @@ public class StoreOperationalStatisticsController {
             @RequestParam("startTime") String startTime,
             @RequestParam("endTime") String endTime) {
         log.info("StoreOperationalStatisticsController.getStatistics - storeId={}, startTime={}, endTime={}", storeId, startTime, endTime);
-        StoreOperationalStatisticsVo statistics = storeOperationalStatisticsService.getStatistics(storeId, startTime, endTime);
-        return R.data(statistics);
+        try {
+            StoreOperationalStatisticsVo statistics = storeOperationalStatisticsService.getStatisticsInTrackFormat(storeId, startTime, endTime);
+            return R.data(statistics);
+        } catch (Exception e) {
+            log.error("获取商家经营统计数据失败 - storeId={}, startTime={}, endTime={}, error={}", 
+                    storeId, startTime, endTime, e.getMessage(), e);
+            return R.fail("获取统计数据失败: " + e.getMessage());
+        }
     }
 
     @ApiOperation("获取商家经营统计数据对比")
@@ -222,4 +228,108 @@ public class StoreOperationalStatisticsController {
             return R.fail("批量删除失败: " + e.getMessage());
         }
     }
+
+    @ApiOperation("更新历史统计记录的PDF URL")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "historyId",
+                    value = "历史记录ID",
+                    dataType = "Integer",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "pdfUrl",
+                    value = "PDF文件URL(前端生成的PDF文件地址)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            )
+    })
+    @PutMapping("/history/updatePdfUrl")
+    public R<String> updateHistoryPdfUrl(
+            @RequestParam("historyId") Integer historyId,
+            @RequestParam("pdfUrl") String pdfUrl) {
+        log.info("StoreOperationalStatisticsController.updateHistoryPdfUrl - historyId={}, pdfUrl={}", historyId, pdfUrl);
+        try {
+            if (historyId == null || historyId <= 0) {
+                return R.fail("历史记录ID不能为空且必须大于0");
+            }
+            
+            if (pdfUrl == null || pdfUrl.trim().isEmpty()) {
+                return R.fail("PDF URL不能为空");
+            }
+            
+            boolean result = storeOperationalStatisticsService.updateHistoryPdfUrl(historyId, pdfUrl);
+            if (result) {
+                return R.success("更新PDF URL成功");
+            } else {
+                return R.fail("更新PDF URL失败,历史记录不存在或已被删除");
+            }
+        } catch (Exception e) {
+            log.error("更新历史统计记录PDF URL失败 - historyId={}, pdfUrl={}, error={}", historyId, pdfUrl, e.getMessage(), e);
+            return R.fail("更新PDF URL失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("对比数据后绘制图片并生成PDF报告上传到OSS")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(
+                    name = "storeId",
+                    value = "店铺ID",
+                    dataType = "Integer",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "currentStartTime",
+                    value = "当期开始时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "currentEndTime",
+                    value = "当期结束时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "previousStartTime",
+                    value = "上期开始时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            ),
+            @ApiImplicitParam(
+                    name = "previousEndTime",
+                    value = "上期结束时间(格式:yyyy-MM-dd)",
+                    dataType = "String",
+                    paramType = "query",
+                    required = true
+            )
+    })
+    @GetMapping("/generateStatisticsComparisonPdf")
+    public R<String> generateStatisticsComparisonPdf(
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam("currentStartTime") String currentStartTime,
+            @RequestParam("currentEndTime") String currentEndTime,
+            @RequestParam("previousStartTime") String previousStartTime,
+            @RequestParam("previousEndTime") String previousEndTime) {
+        log.info("StoreOperationalStatisticsController.generateStatisticsComparisonPdf - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}",
+                storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+        try {
+            String pdfUrl = storeOperationalStatisticsService.generateStatisticsComparisonPdf(
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+            return R.data(pdfUrl);
+        } catch (Exception e) {
+            log.error("生成统计数据对比PDF报告失败 - storeId={}, error={}", storeId, e.getMessage(), e);
+            return R.fail("生成PDF报告失败: " + e.getMessage());
+        }
+    }
+
+
 }

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

@@ -49,5 +49,7 @@ public interface CommonCommentService extends IService<CommonComment> {
     void getAllChildComment(Long userId, CommonRatingVo commonRatingVo, List<CommonCommentVo> commonComments, String likeType);
 
     Map<String,Object> getCommitCount(Integer sourceId, Integer sourceType, String userId, String userType);
+
+    boolean deleteComment(Long commentId);
 }
 

+ 4 - 6
alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java

@@ -55,13 +55,11 @@ public interface CommonRatingService extends IService<CommonRating> {
     Object getRatingDetail(Integer ratingId, Long userId);
 
     /**
-     * 获取回复率和评价比例
-     *
-     * @param businessId   业务ID(店铺ID)
-     * @param businessType 业务类型:1-店铺评价
-     * @return 回复率和评价比例信息
+     * 删除评价后更新店铺评分
+     * 
+     * @param businessId 店铺ID
      */
-    shop.alien.entity.store.vo.RatingPercentVo getRatingPercent(Integer businessId, Integer businessType);
+    void updateStoreScoreAfterDelete(Integer businessId);
 
 
   /*  /**

+ 9 - 1
alien-store/src/main/java/shop/alien/store/service/LifeBlacklistService.java

@@ -19,7 +19,15 @@ public interface LifeBlacklistService extends IService<LifeBlacklist> {
 
     int blackList(LifeBlacklist lifeBlacklist);
 
-    List<LifeBlacklistVo> blackListByUserId(String userType, String userId);
+    /**
+     * 根据用户ID查询拉黑列表
+     *
+     * @param userType 用户类型
+     * @param userId   用户ID
+     * @param type     查找类型
+     * @return 拉黑列表
+     */
+    List<LifeBlacklistVo> blackListByUserId(String userType, String userId, Integer type);
 
     Boolean isBlackListUser(LifeBlacklist lifeBlacklist);
 

+ 4 - 5
alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java

@@ -227,7 +227,7 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
             if (collect.isEmpty()) {
                 vo.setCommentCount(0);
             } else {
-                Integer count = collect.size();
+                Integer count = 0;
                 for (CommonCommentVo commentVo : collect) {
                     count += commentVo.getCommentCount();
                 }
@@ -570,18 +570,15 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
 
         // TODO  bugfix: 过滤掉我拉黑的和拉黑我的。
         //是否关注用户
-        String targetPhone;
         LifeUser myLifeUser = new LifeUser();
         StoreUser myStoreUser = new StoreUser();
 
         if ("user".equals(phoneId.split("_")[0])) {
             String myselfUserPhone = phoneId.split("_")[1];
             myLifeUser = lifeUserService.getUserByPhone(myselfUserPhone);
-            targetPhone = myLifeUser.getUserPhone();
         } else {
             String myselfStorePhone = phoneId.split("_")[1];
             myStoreUser = storeUserService.getUserByPhone(myselfStorePhone);
-            targetPhone = myStoreUser.getPhone();
         }
 
         Integer myType ;
@@ -620,7 +617,9 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
                 if (lifeUserDynamicsVo.getType().equals("2")){
                     String phoneIdNew = lifeUserDynamicsVo.getPhoneId().substring(6);
                      StoreInfo storeInfo=storeInfoMapper.getStoreNameByPhone(phoneIdNew);
-                    lifeUserDynamicsVo.setStoreName(storeInfo.getStoreName());
+                     if(storeInfo != null){
+                         lifeUserDynamicsVo.setStoreName(storeInfo.getStoreName());
+                     }
                 } else if (lifeUserDynamicsVo.getType().equals("1")) {
                     String phoneIdNew = lifeUserDynamicsVo.getPhoneId().substring(5);
                     //根据手机号查询用户表

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

@@ -440,6 +440,15 @@ public interface StoreInfoService extends IService<StoreInfo> {
     StoreInfoVo getClientStoreDetail(String storeId, String userId, String jingdu, String weidu);
 
     /**
+     * 获取店铺营业状态
+     * 判断店铺当前是否在营业时间内,返回营业状态和营业时间信息
+     *
+     * @param storeId 店铺ID
+     * @return StoreBusinessStatusVo 营业状态信息
+     */
+    StoreBusinessStatusVo getStoreBusinessStatus(String storeId);
+
+    /**
      * 获取休闲娱乐分类数据(主分类和子分类)
      * 返回层级结构的分类数据,包含主分类和对应的子分类
      *

+ 32 - 0
alien-store/src/main/java/shop/alien/store/service/StoreOperationalStatisticsService.java

@@ -61,4 +61,36 @@ public interface StoreOperationalStatisticsService {
      * @return 是否成功
      */
     boolean batchDeleteHistory(List<Integer> ids);
+
+    /**
+     * 获取商家经营统计数据(符合埋点统计数据JSON格式)
+     *
+     * @param storeId   店铺ID
+     * @param startTime 开始时间(格式:yyyy-MM-dd)
+     * @param endTime   结束时间(格式:yyyy-MM-dd)
+     * @return 经营统计数据
+     */
+    StoreOperationalStatisticsVo getStatisticsInTrackFormat(Integer storeId, String startTime, String endTime);
+
+    /**
+     * 更新历史统计记录的PDF URL
+     *
+     * @param historyId 历史记录ID
+     * @param pdfUrl    PDF文件URL
+     * @return 是否成功
+     */
+    boolean updateHistoryPdfUrl(Integer historyId, String pdfUrl);
+
+    /**
+     * 生成统计数据对比PDF报告并上传到OSS
+     *
+     * @param storeId           店铺ID
+     * @param currentStartTime  当期开始时间(格式:yyyy-MM-dd)
+     * @param currentEndTime    当期结束时间(格式:yyyy-MM-dd)
+     * @param previousStartTime 上期开始时间(格式:yyyy-MM-dd)
+     * @param previousEndTime   上期结束时间(格式:yyyy-MM-dd)
+     * @return PDF的OSS URL
+     */
+    String generateStatisticsComparisonPdf(Integer storeId, String currentStartTime, String currentEndTime,
+                                           String previousStartTime, String previousEndTime);
 }

+ 93 - 11
alien-store/src/main/java/shop/alien/store/service/impl/CommonCommentServiceImpl.java

@@ -2,6 +2,7 @@ package shop.alien.store.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -16,8 +17,8 @@ import shop.alien.entity.store.vo.CommonRatingVo;
 import shop.alien.mapper.*;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.util.CommonConstant;
+import shop.alien.store.util.ai.AiContentModerationUtil;
 import shop.alien.util.common.constant.CommentSourceTypeEnum;
-import shop.alien.util.common.safe.TextModerationResultVO;
 import shop.alien.util.common.safe.TextModerationUtil;
 
 import java.util.*;
@@ -45,7 +46,9 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
     private final StoreUserMapper storeUserMapper;
     private final LifeUserViolationMapper lifeUserViolationMapper;
     private final LifeUserMapper lifeUserMapper;
-
+    @Autowired
+    private LifeFansMapper lifeFansMapper;
+    private final AiContentModerationUtil aiContentModerationUtil;
 
     /**
      * 新增评论
@@ -61,13 +64,14 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
             return 4; // 字数超限
         }
 
-        // 文本内容审核
-        TextModerationResultVO textCheckResult = null;
+        // 文本内容审核 + 视频审核(评论没有)
         try {
-            textCheckResult = textModerationUtil.invokeFunction(commonComment.getContent(), CommonRatingServiceImpl.SERVICES_LIST);
-            if ("high".equals(textCheckResult.getRiskLevel())) {
+            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(commonComment.getContent(), null);
+            if (!auditResult.isPassed()) {
                 return 2; // 文本内容异常(包含敏感词)
             }
+            // 2.审核通过,设置评价状态为已审核
+            commonComment.setAuditStatus(1);
             return this.save(commonComment) ? 0 : 1;
         } catch (Exception e) {
             log.error("新增评论失败", e);
@@ -102,7 +106,8 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
             commentWrapper.eq("cc.source_type", CommentSourceTypeEnum.DYNAMIC_COMMENT.getType());
         }
         commentWrapper.eq("cc.source_id", sourceId)
-                .eq("cc.parent_id", 0);
+                .eq("cc.parent_id", 0)
+                .eq("cc.delete_flag", CommonConstant.DELETE_FLAG_UNDELETE);
         Page<CommonCommentVo> page = null;
         if( null != pageNum && null != pageSize){
             page = new Page<>(pageNum, pageSize);
@@ -151,6 +156,7 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
         QueryWrapper<CommonComment> queryWrapper = new QueryWrapper<>();
         queryWrapper.eq(null != sourceId, "source_id", sourceId);
         queryWrapper.eq(null != sourceType, "source_type", sourceType);
+        queryWrapper.eq("delete_flag", CommonConstant.DELETE_FLAG_UNDELETE);
         //通过当前登录人id和类型 查询举报业务id 不包含已举报的
         List<LifeUserViolation> lifeUserViolations = new ArrayList<>();
         if(sourceType == CommentSourceTypeEnum.DYNAMIC_COMMENT.getType() ){
@@ -189,9 +195,73 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
         return map;
     }
 
+    @Override
+    public boolean deleteComment(Long commentId) {
+        if (commentId == null) {
+            return false;
+        }
+
+        try {
+            // 首先获取要删除的评论
+            CommonComment comment = this.getById(commentId);
+            if (comment == null) {
+                return false;
+            }
+            if(comment.getParentId() == 0){
+                // 获取所有子评论(包括子评论的子评论)
+                List<CommonCommentVo> allChildComments = getChildCommentsRecursively(commentId, null, null);
+
+                // 准备要删除的评论ID列表,包含父评论和所有子评论
+                List<Long> commentIdsToDelete = new ArrayList<>();
+                commentIdsToDelete.add(commentId); // 添加父评论ID
+
+                // 收集所有子评论的ID
+                for (CommonCommentVo child : allChildComments) {
+                    commentIdsToDelete.add(child.getId());
+                }
+
+                // 批量更新所有相关评论的删除标志
+                if (!commentIdsToDelete.isEmpty()) {
+                    UpdateWrapper<CommonComment> wrapper = new UpdateWrapper<>();
+                    wrapper.in("id", commentIdsToDelete)
+                            .set("delete_flag", CommonConstant.DELETE_FLAG_DELETED); // 直接设置数据库字段
+
+                    boolean result = this.update(wrapper);
+
+                    if (result) {
+                        log.info("成功删除评论及其子评论,评论ID列表:{}", commentIdsToDelete);
+                        return true;
+                    } else {
+                        log.error("删除评论失败,评论ID列表:{}", commentIdsToDelete);
+                        return false;
+                    }
+                }
+            }
+
+            // 只有父评论,没有子评论的情况
+            UpdateWrapper<CommonComment> wrapper = new UpdateWrapper<>();
+            wrapper.eq("id", commentId)
+                   .set("delete_flag", CommonConstant.DELETE_FLAG_DELETED);
+
+            boolean result = this.update(wrapper);
+            if (result) {
+                log.info("成功删除评论,评论ID:{}", commentId);
+                return true;
+            } else {
+                log.error("删除评论失败,评论ID:{}", commentId);
+                return false;
+            }
+
+        } catch (Exception e) {
+            log.error("删除评论时发生异常,评论ID:{}", commentId, e);
+            return false;
+        }
+    }
+
+
     private void getOtherDataWithSourceType(Integer sourceId, Integer sourceType, String userId, Map<String, Object> map) {
         if ( sourceType == CommentSourceTypeEnum.DYNAMIC_COMMENT.getType() ) {
-            // 计算分析数和喜欢数
+            // 1.计算分析数和喜欢数
             LifeUserDynamics lifeUserDynamics = lifeUserDynamicsMapper.selectOne(new QueryWrapper<LifeUserDynamics>().eq("id", sourceId));
             if(null != lifeUserDynamics){
                 map.put("likeCount",lifeUserDynamics.getDianzanCount());
@@ -200,13 +270,14 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
                 map.put("likeCount",0);
                 map.put("transferCount",0);
             }
-            // 查询商家头像 TODO -> 动态发布的时候id不应该用Store_phone的格式,来不及重构动态的位置,后续等有缘人吧
+            // 2.查询商家头像 TODO -> 动态发布的时候id不应该用Store_phone的格式,来不及重构动态的位置,后续等有缘人吧
             StoreUser storeUser = storeUserMapper.selectOne(new QueryWrapper<StoreUser>().eq("phone", lifeUserDynamics.getPhoneId().split("_")[1]));
             map.put("userImage",storeUser.getHeadImg()!= null?storeUser.getHeadImg():"");
-            // 查询当前用户是否喜欢
+            LifeUser lifeUser = lifeUserMapper.selectById(userId);
+            // 3.查询当前用户是否喜欢
             LifeLikeRecord lifeLikeRecord = lifeLikeRecordMapper.selectOne(new QueryWrapper<LifeLikeRecord>()
                     .eq("type", CommonConstant.LIKE_TYPE_DYNAMICS)
-                    .eq("dianzan_id", userId)
+                    .eq("dianzan_id", "user_".concat(lifeUser.getUserPhone()))
                     .eq("huifu_id", sourceId)
                     .eq("delete_flag", 0));
             if(null != lifeLikeRecord){
@@ -214,6 +285,17 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
             } else {
                 map.put("isLike",0);
             }
+            // 4.查询当前登录人是否关注了动态发布者
+
+            LifeFans lifeFans = lifeFansMapper.selectOne(new QueryWrapper<LifeFans>().eq("followed_id", lifeUserDynamics.getPhoneId())
+                    .eq("fans_id", "user_".concat(lifeUser.getUserPhone()))
+                    .eq("fans_type", 1)
+                    .last("limit 1"));
+            if(null != lifeFans){
+                map.put("isFans",1);
+            } else {
+                map.put("isFans",0);
+            }
         }
     }
 

+ 202 - 115
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -15,32 +15,34 @@ import lombok.extern.slf4j.Slf4j;
 import org.jetbrains.annotations.NotNull;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.CommonCommentVo;
 import shop.alien.entity.store.vo.CommonRatingVo;
-import shop.alien.entity.store.vo.RatingPercentVo;
 import shop.alien.entity.store.vo.StoreInfoScoreVo;
 import shop.alien.entity.store.vo.WebSocketVo;
 import shop.alien.mapper.*;
-import shop.alien.entity.store.TagsSynonym;
 import shop.alien.store.config.WebSocketProcess;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.util.CommonConstant;
+import shop.alien.store.util.ai.AiContentModerationUtil;
+import shop.alien.store.util.ai.AiVideoModerationUtil;
+import shop.alien.util.common.DateUtils;
 import shop.alien.util.common.constant.CommentSourceTypeEnum;
 import shop.alien.util.common.constant.RatingBusinessTypeEnum;
-import shop.alien.util.common.safe.TextModerationResultVO;
 import shop.alien.util.common.safe.TextModerationUtil;
 import shop.alien.util.common.safe.TextReviewServiceEnum;
-import shop.alien.util.common.DateUtils;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.text.SimpleDateFormat;
 import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
 import java.util.concurrent.atomic.AtomicReference;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -72,8 +74,11 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
     private final LifeFansMapper lifeFansMapper;
     private final TagsSynonymMapper tagsSynonymMapper;
     private final CommonCommentService commonCommentService;
-
-
+    private final StoreCommentAppealMapper storeCommentAppealMapper;
+    private final AiContentModerationUtil aiContentModerationUtil;
+    @Qualifier("commonVideoTaskExecutor")
+    private final ExecutorService commonVideoTaskExecutor;
+    private final AiVideoModerationUtil aiVideoModerationUtil;
 
     public static final List<String> SERVICES_LIST = ImmutableList.of(
             TextReviewServiceEnum.COMMENT_DETECTION_PRO.getService(),
@@ -84,12 +89,8 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
 
     @Override
     public Integer saveCommonRating(CommonRating commonRating) {
-        // 1. 文本审核
+        // 1. 文本审核 + 视频审核(评价有图片和视频)
         try {
-            TextModerationResultVO textCheckResult = textModerationUtil.invokeFunction(commonRating.getContent(), SERVICES_LIST);
-            if ("high".equals(textCheckResult.getRiskLevel())) {
-                return 2;
-            }
             // 手动存评分1,2,3
             if (StringUtils.isNotEmpty(commonRating.getOtherScore())) {
                 JSONObject parse = JSONObject.parse(commonRating.getOtherScore());
@@ -98,8 +99,62 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 commonRating.setScoreThree(parse.getDouble("scoreThree"));
             }
             int i = this.save(commonRating) ? 0 : 1;
-            // 对不同的businessType进行不同的处理,
-            doBusinessWithType(commonRating);
+            // 一次遍历完成分类,避免多次流式处理
+            Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(Arrays.asList(commonRating.getImageUrls().split(",")));
+            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(commonRating.getContent(), urlCategoryMap.get("image"));
+            if (!auditResult.isPassed()) {
+                // 审核不通过
+                CommonRating rating = this.getById(commonRating.getId());
+                rating.setAuditStatus(2);
+                rating.setAuditReason(auditResult.getFailureReason());
+                this.saveOrUpdate(rating);
+            } else{
+                CompletableFuture.runAsync(() -> {
+                    AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
+                    try {
+                        // 调用审核接口,增加超时控制(避免接口挂死)
+                        videoAuditResult = CompletableFuture.supplyAsync(
+                                () -> aiVideoModerationUtil.auditVideos(urlCategoryMap.get("video")),
+                                commonVideoTaskExecutor
+                        ).get();
+
+                        // 审核不通过则更新状态和原因
+                        if (Objects.nonNull(videoAuditResult) && !videoAuditResult.isPassed()) {
+                            // 重新查询最新的rating,避免并发覆盖
+                            CommonRating rating = this.getById(commonRating.getId());
+                            if (Objects.isNull(rating)) {
+                                log.error("视频审核后更新失败,rating,ID:{}", rating.getId());
+                                return;
+                            }
+                            rating.setAuditStatus(2);
+                            // 失败原因
+                            rating.setAuditReason(videoAuditResult.getFailureReason());
+                            this.saveOrUpdate(rating);
+                            log.info("视频审核不通过,已更新状态,requirementID:{},原因:{}",
+                                    rating.getId(), videoAuditResult.getFailureReason());
+                        } else if (Objects.nonNull(videoAuditResult) && videoAuditResult.isPassed()) {
+                            // 审核通过也更新状态(可选,根据业务需求)
+                            CommonRating rating = this.getById(commonRating.getId());
+                            if (Objects.nonNull(rating)) {
+                                rating.setAuditStatus(1);
+//                                latestRequirement.setAuditReason(null);
+                                this.saveOrUpdate(rating);
+                                // 对不同的businessType进行不同的处理,
+                                doBusinessWithType(commonRating);
+                                log.info("视频审核通过,已更新状态,requirementID:{}", rating.getId());
+                            }
+                        }
+                    } catch (Exception e) {
+                        log.error("视频审核接口调用异常,commonRating:{}", commonRating.getId(), e);
+                        CommonRating rating = this.getById(commonRating.getId());
+                        if (Objects.nonNull(rating)) {
+                            rating.setAuditStatus(2);
+                            rating.setAuditReason("视频审核接口调用异常:" + e.getMessage()); // 实际需捕获具体异常信息
+                            this.saveOrUpdate(rating);
+                        }
+                    }
+                }, commonVideoTaskExecutor);
+            }
             return i;
         } catch (Exception e) {
             log.error("CommonRatingService.saveCommonRating ERROR Msg={}", e.getMessage());
@@ -346,6 +401,30 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 ratingCount.put("img", new ArrayList<>());
             }
 
+            // 4. 计算回复率(已回复的评价数 / 总评价数)
+            LambdaQueryWrapper<CommonComment> repliedWrapper = new LambdaQueryWrapper<>();
+            repliedWrapper.eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())
+                         .in(CommonComment::getSourceId, collect)
+                         .eq(CommonComment::getCommentType, 2)  // 商户评论
+                         .eq(CommonComment::getParentId, 0)     // 根评论(直接回复评价)
+                         .eq(CommonComment::getDeleteFlag, 0);  // 未删除
+
+            // 获取已回复的评价ID列表(去重)
+            Set<Long> repliedRatingIds = commonCommentMapper.selectList(repliedWrapper).stream()
+                    .map(CommonComment::getSourceId)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toSet());
+
+            int repliedCount = repliedRatingIds.size();
+            ratingCount.put("repliedCount", repliedCount);
+
+            // 计算回复率(保留2位小数)
+            Double replyRate = 0.0;
+            int ratingTotalCount = collect.size();
+            if (ratingTotalCount > 0) {
+                replyRate = Math.round((repliedCount * 100.0 / ratingTotalCount) * 100.0) / 100.0;
+            }
+            ratingCount.put("replyRate", replyRate);
 
         }
         return ratingCount;
@@ -549,7 +628,44 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 }
             }
 
-            // 4. 组装评价列表数据
+            // 4. 查询申诉信息(关联 store_comment_appeal 表)
+            Map<Long, StoreCommentAppeal> appealMap = new HashMap<>();
+            if (!ratingIdSet.isEmpty()) {
+                // 将 Long 类型的评价ID转换为 Integer 类型(因为 comment_id 是 Integer)
+                List<Integer> ratingIdList = ratingIdSet.stream()
+                        .map(Long::intValue)
+                        .collect(Collectors.toList());
+                
+                // 查询申诉记录(comment_id 关联 common_rating.id)
+                LambdaQueryWrapper<StoreCommentAppeal> appealWrapper = new LambdaQueryWrapper<>();
+                appealWrapper.in(StoreCommentAppeal::getCommentId, ratingIdList)
+                            .eq(StoreCommentAppeal::getDeleteFlag, 0);
+                List<StoreCommentAppeal> appeals = storeCommentAppealMapper.selectList(appealWrapper);
+                
+                // 构建评价ID -> 申诉记录的映射(一个评价可能有多条申诉,取最新的一条)
+                if (CollectionUtils.isNotEmpty(appeals)) {
+                    // 按评价ID分组,每组只取最新的一条申诉记录
+                    Map<Integer, StoreCommentAppeal> tempAppealMap = new HashMap<>();
+                    for (StoreCommentAppeal appeal : appeals) {
+                        Integer commentId = appeal.getCommentId();
+                        if (commentId != null) {
+                            // 如果该评价还没有申诉记录,或者当前申诉记录更新,则更新
+                            if (!tempAppealMap.containsKey(commentId) || 
+                                (appeal.getUpdatedTime() != null && 
+                                 tempAppealMap.get(commentId).getUpdatedTime() != null &&
+                                 appeal.getUpdatedTime().after(tempAppealMap.get(commentId).getUpdatedTime()))) {
+                                tempAppealMap.put(commentId, appeal);
+                            }
+                        }
+                    }
+                    // 转换为 Long 类型的 key
+                    for (Map.Entry<Integer, StoreCommentAppeal> entry : tempAppealMap.entrySet()) {
+                        appealMap.put(entry.getKey().longValue(), entry.getValue());
+                    }
+                }
+            }
+
+            // 5. 组装评价列表数据
             for (CommonRating record : page1.getRecords()) {
                 CommonRatingVo commonRatingVo = new CommonRatingVo();
                 BeanUtil.copyProperties(record, commonRatingVo);
@@ -580,6 +696,18 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                     commonRatingVo.setChildCommonComments(new ArrayList<>());
                 }
 
+                // 设置申诉状态和申诉标识
+                StoreCommentAppeal appeal = appealMap.get(record.getId());
+                if (appeal != null) {
+                    // 已申诉,设置申诉状态
+                    commonRatingVo.setAppealStatus(appeal.getAppealStatus());
+                    commonRatingVo.setAppealFlag(1);
+                } else {
+                    // 未申诉
+                    commonRatingVo.setAppealStatus(null);
+                    commonRatingVo.setAppealFlag(0);
+                }
+
                 resultList.add(commonRatingVo);
             }
 
@@ -590,6 +718,66 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         IPage<CommonRatingVo> emptyResult = new Page<>(page1.getCurrent(), page1.getSize(), 0);
         return R.data(emptyResult);
     }
+    /**
+     * 删除评价后更新店铺评分
+     * 
+     * @param businessId 店铺ID
+     */
+    @Override
+    public void updateStoreScoreAfterDelete(Integer businessId) {
+        if (businessId == null) {
+            return;
+        }
+        
+        try {
+            // 查询该店铺的所有有效评价
+            LambdaQueryWrapper<CommonRating> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(CommonRating::getBusinessType, 1)
+                   .eq(CommonRating::getBusinessId, businessId)
+                   .eq(CommonRating::getDeleteFlag, 0)
+                   .eq(CommonRating::getIsShow, 1)
+                   .eq(CommonRating::getAuditStatus, 1);
+            List<CommonRating> ratings = commonRatingMapper.selectList(wrapper);
+            
+            int total = ratings.size();
+            if (total == 0) {
+                // 没有评价,设置默认评分为0
+                StoreInfo storeInfo = new StoreInfo();
+                storeInfo.setId(businessId);
+                storeInfo.setScoreAvg(0.0);
+                storeInfo.setScoreOne(0.0);
+                storeInfo.setScoreTwo(0.0);
+                storeInfo.setScoreThree(0.0);
+                storeInfoMapper.updateById(storeInfo);
+                log.info("店铺无有效评价,重置评分为0,businessId={}", businessId);
+                return;
+            }
+            
+            // 计算平均评分
+            double scoreSum = ratings.stream().mapToDouble(r -> r.getScore() != null ? r.getScore() : 0.0).sum();
+            double scoreOneSum = ratings.stream().mapToDouble(r -> r.getScoreOne() != null ? r.getScoreOne() : 0.0).sum();
+            double scoreTwoSum = ratings.stream().mapToDouble(r -> r.getScoreTwo() != null ? r.getScoreTwo() : 0.0).sum();
+            double scoreThreeSum = ratings.stream().mapToDouble(r -> r.getScoreThree() != null ? r.getScoreThree() : 0.0).sum();
+            
+            double scoreAvg = Math.round((scoreSum / total) * 100.0) / 100.0;
+            double scoreOne = Math.round((scoreOneSum / total) * 100.0) / 100.0;
+            double scoreTwo = Math.round((scoreTwoSum / total) * 100.0) / 100.0;
+            double scoreThree = Math.round((scoreThreeSum / total) * 100.0) / 100.0;
+            
+            StoreInfo storeInfo = new StoreInfo();
+            storeInfo.setId(businessId);
+            storeInfo.setScoreAvg(scoreAvg);
+            storeInfo.setScoreOne(scoreOne);
+            storeInfo.setScoreTwo(scoreTwo);
+            storeInfo.setScoreThree(scoreThree);
+            storeInfoMapper.updateById(storeInfo);
+            
+            log.info("更新店铺评分成功,businessId={},评价数={},scoreAvg={}", businessId, total, scoreAvg);
+        } catch (Exception e) {
+            log.error("更新店铺评分失败,businessId={},error={}", businessId, e.getMessage());
+        }
+    }
+
 /*
     @Override
     public Double getAverageScore(Integer businessType, Long businessId) {
@@ -645,106 +833,5 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
 
         return 0;
     }
-
-    /**
-     * 获取回复率和评价比例
-     *
-     * @param businessId   业务ID(店铺ID)
-     * @param businessType 业务类型:1-店铺评价
-     * @return 回复率和评价比例信息
-     */
-    @Override
-    public RatingPercentVo getRatingPercent(Integer businessId, Integer businessType) {
-        log.info("CommonRatingServiceImpl.getRatingPercent?businessId={}&businessType={}", businessId, businessType);
-
-        RatingPercentVo vo = new RatingPercentVo();
-
-        // 1. 查询全部评价记录(仅展示的、审核通过的)
-        LambdaQueryWrapper<CommonRating> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(CommonRating::getBusinessId, businessId);
-        wrapper.eq(CommonRating::getBusinessType, businessType);
-        wrapper.eq(CommonRating::getIsShow, 1);
-        wrapper.eq(CommonRating::getAuditStatus, 1);  // 仅统计审核通过的
-        List<CommonRating> commonRatings = commonRatingMapper.selectList(wrapper);
-
-        // 如果为空,返回默认值
-        if (CollectionUtils.isEmpty(commonRatings)) {
-            vo.setTotalRatingCount(0);
-            vo.setGoodCount(0);
-            vo.setMidCount(0);
-            vo.setBadCount(0);
-            vo.setGoodPercent(0.0);
-            vo.setMidPercent(0.0);
-            vo.setBadPercent(0.0);
-            vo.setRepliedCount(0);
-            vo.setReplyRate(0.0);
-            return vo;
-        }
-
-        List<Long> ratingIdList = commonRatings.stream()
-                .map(CommonRating::getId)
-                .collect(Collectors.toList());
-
-        // 2. 获取评价统计信息(好评、中评、差评数量)
-        Map<String, Object> ratingCount = commonRatingMapper.getRatingCount(
-                new QueryWrapper<CommonRating>().in("id", ratingIdList));
-
-        // 3. 计算好评、中评、差评数量和占比
-        int goodCount = getIntValue(ratingCount.get("goodCount"));
-        int midCount = getIntValue(ratingCount.get("midCount"));
-        int badCount = getIntValue(ratingCount.get("badCount"));
-        int totalCount = goodCount + midCount + badCount;
-
-        vo.setTotalRatingCount(totalCount);
-        vo.setGoodCount(goodCount);
-        vo.setMidCount(midCount);
-        vo.setBadCount(badCount);
-
-        // 计算占比(保留2位小数)
-        if (totalCount > 0) {
-            Double goodPercent = Math.round((goodCount * 100.0 / totalCount) * 100.0) / 100.0;
-            Double midPercent = Math.round((midCount * 100.0 / totalCount) * 100.0) / 100.0;
-            Double badPercent = Math.round((badCount * 100.0 / totalCount) * 100.0) / 100.0;
-
-            vo.setGoodPercent(goodPercent);
-            vo.setMidPercent(midPercent);
-            vo.setBadPercent(badPercent);
-        } else {
-            vo.setGoodPercent(0.0);
-            vo.setMidPercent(0.0);
-            vo.setBadPercent(0.0);
-        }
-
-        // 4. 计算回复率
-        // 查询已回复的评价数(存在商户评论 comment_type=2, parent_id=0)
-        LambdaQueryWrapper<CommonComment> repliedWrapper = new LambdaQueryWrapper<>();
-        repliedWrapper.eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())
-                     .in(CommonComment::getSourceId, ratingIdList)
-                     .eq(CommonComment::getCommentType, 2)  // 商户评论
-                     .eq(CommonComment::getParentId, 0)     // 根评论(直接回复评价)
-                     .eq(CommonComment::getIsShow, 1)
-                     .eq(CommonComment::getAuditStatus, 1);
-
-        // 获取已回复的评价ID列表(去重)
-        Set<Long> repliedRatingIds = commonCommentMapper.selectList(repliedWrapper).stream()
-                .map(CommonComment::getSourceId)
-                .filter(Objects::nonNull)
-                .collect(Collectors.toSet());
-
-        int repliedCount = repliedRatingIds.size();
-        vo.setRepliedCount(repliedCount);
-
-        // 计算回复率(保留2位小数)
-        Double replyRate = 0.0;
-        if (totalCount > 0) {
-            replyRate = Math.round((repliedCount * 100.0 / totalCount) * 100.0) / 100.0;
-        }
-        vo.setReplyRate(replyRate);
-
-        log.info("CommonRatingServiceImpl.getRatingPercent result: totalCount={}, goodCount={}, midCount={}, badCount={}, repliedCount={}, replyRate={}%",
-                totalCount, goodCount, midCount, badCount, repliedCount, replyRate);
-
-        return vo;
-    }
 }
 

+ 12 - 12
alien-store/src/main/java/shop/alien/store/service/impl/LifeBlacklistServiceImpl.java

@@ -7,15 +7,9 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
-import shop.alien.entity.store.LifeBlacklist;
-import shop.alien.entity.store.LifeUser;
-import shop.alien.entity.store.StoreImg;
-import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.LifeBlacklistVo;
-import shop.alien.mapper.LifeBlacklistMapper;
-import shop.alien.mapper.LifeUserMapper;
-import shop.alien.mapper.StoreImgMapper;
-import shop.alien.mapper.StoreUserMapper;
+import shop.alien.mapper.*;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.LifeBlacklistService;
 import shop.alien.util.common.JwtUtil;
@@ -44,6 +38,7 @@ public class LifeBlacklistServiceImpl extends ServiceImpl<LifeBlacklistMapper, L
     private final LifeUserMapper lifeUserMapper;
     private final StoreImgMapper storeImgMapper;
     private final BaseRedisService baseRedisService;
+    private final StoreInfoMapper storeInfoMapper;
 
     @Override
     public int blackList(LifeBlacklist lifeBlacklist) {
@@ -88,11 +83,16 @@ public class LifeBlacklistServiceImpl extends ServiceImpl<LifeBlacklistMapper, L
     }
 
     @Override
-    public List<LifeBlacklistVo> blackListByUserId(String userType, String userId) {
+    public List<LifeBlacklistVo> blackListByUserId(String userType, String userId, Integer type) {
         List<LifeBlacklistVo> lifeBlacklistVos = new ArrayList<>();
         LambdaUpdateWrapper<LifeBlacklist> wrapper = new LambdaUpdateWrapper<>();
         wrapper.eq(LifeBlacklist::getBlockerId, userId);
         wrapper.eq(LifeBlacklist::getBlockerType, userType);
+        if (1 == type) {
+            wrapper.eq(LifeBlacklist::getBlockedType, "1");
+        } else if (2 == type) {
+            wrapper.eq(LifeBlacklist::getBlockedType, "2");
+        }
         List<LifeBlacklist> lifeBlacklists = lifeBlacklistMapper.selectList(wrapper);
 
         for (LifeBlacklist lifeBlacklist : lifeBlacklists) {
@@ -105,14 +105,14 @@ public class LifeBlacklistServiceImpl extends ServiceImpl<LifeBlacklistMapper, L
             if (lifeBlacklist.getBlockedId() != null && lifeBlacklist.getBlockedType() != null) {
                 if ("1".equals(lifeBlacklist.getBlockedType())) {
                     StoreUser storeUser = storeUserMapper.selectById(lifeBlacklist.getBlockedId());
+                    StoreInfo storeInfo = storeInfoMapper.selectById(storeUser.getStoreId());
+                    lifeBlacklist.setStoreInfo(storeInfo);
                     StoreImg storeImg = null;
                     if (storeUser != null) {
-                        storeImg = storeImgMapper.selectOne(new LambdaUpdateWrapper<StoreImg>().eq(StoreImg::getStoreId, storeUser.getStoreId()).eq(StoreImg::getImgType, 10));
-                    }
-                    if (storeUser != null) {
                         lifeBlacklist.setName(storeUser.getName());
                         lifeBlacklist.setPhoneId(storeUser.getPhone());
                         lifeBlacklist.setUserImage(storeUser.getHeadImg());
+                        storeImg = storeImgMapper.selectOne(new LambdaUpdateWrapper<StoreImg>().eq(StoreImg::getStoreId, storeUser.getStoreId()).eq(StoreImg::getImgType, 10));
                         if (storeImg != null) {
                             lifeBlacklist.setUserImage(storeImg.getImgUrl());
                         }

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

@@ -219,6 +219,8 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
             Integer status = order.getOrderStatus();
             enabled = !Objects.equals(status, LawyerStatusEnum.COMPLETE.getStatus())
                     && !Objects.equals(status, LawyerStatusEnum.CANCEL.getStatus())
+                    && !Objects.equals(status, LawyerStatusEnum.WAIT_PAY.getStatus())
+                    && !Objects.equals(status, LawyerStatusEnum.WAIT_ACCEPT.getStatus())
                     && !Objects.equals(status, LawyerStatusEnum.REFUNDED.getStatus());
         }
         String chatStr = "1";

+ 24 - 8
alien-store/src/main/java/shop/alien/store/service/impl/PerformanceListServiceImpl.java

@@ -469,12 +469,13 @@ public class PerformanceListServiceImpl implements PerformanceListService {
         String[] weekDays = performanceWeek.split(",");
         List<String> weekDayList = new ArrayList<>();
 
+        String[] weeklyLabels = {"周一", "周二", "周三", "周四", "周五", "周六", "周日"};
         for (String weekDay : weekDays) {
             if (StringUtils.isNotEmpty(weekDay.trim())) {
                 try {
                     int dayIndex = Integer.parseInt(weekDay.trim());
-                    if (dayIndex >= 0 && dayIndex < WEEK_DAYS.length) {
-                        weekDayList.add(WEEK_DAYS[dayIndex]);
+                    if (dayIndex >= 0 && dayIndex < weeklyLabels.length) {
+                        weekDayList.add(weeklyLabels[dayIndex]);
                     }
                 } catch (NumberFormatException e) {
                     log.warn("解析星期几失败,无效的值:{}", weekDay);
@@ -486,12 +487,26 @@ public class PerformanceListServiceImpl implements PerformanceListService {
             return "";
         }
 
-        String weekDayStr = String.join("、", weekDayList);
+        String weekDayStr = buildWeeklyDayText(weekDayList);
         String timeRange = buildLocalTimeRange(startTime, endTime);
 
         return "每周" + weekDayStr + timeRange;
     }
 
+    private String buildWeeklyDayText(List<String> weekDayList) {
+        if (weekDayList == null || weekDayList.isEmpty()) {
+            return "";
+        }
+        List<String> normalized = new ArrayList<>();
+        for (String day : weekDayList) {
+            if (day != null && day.startsWith("周")) {
+                day = day.substring(1);
+            }
+            normalized.add(day);
+        }
+        return String.join("、", normalized);
+    }
+
     /**
      * 构建时间范围(使用Date)
      *
@@ -829,7 +844,7 @@ public class PerformanceListServiceImpl implements PerformanceListService {
             vo.setPerformanceTime(buildPerformanceTime(performance));
 
             // 构建演出风格
-            vo.setPerformanceStyle(buildPerformanceStyle(performance.getPerformanceStyle()));
+            vo.setPerformanceStyle(performance.getPerformanceStyle());
 
             // 查询演出嘉宾列表
             List<PerformanceGuestVo> guestList = queryGuestList(performance.getStaffConfigIds());
@@ -878,10 +893,9 @@ public class PerformanceListServiceImpl implements PerformanceListService {
             String scheduleInfo = buildScheduleInfo(performance);
             if (StringUtils.isNotEmpty(scheduleInfo)) {
                 if (timeStr.length() > 0) {
-                    timeStr.append(scheduleInfo);
-                } else {
-                    timeStr.append(scheduleInfo);
+                    timeStr.append(" ");
                 }
+                timeStr.append(scheduleInfo);
             }
 
             return timeStr.toString();
@@ -976,7 +990,9 @@ public class PerformanceListServiceImpl implements PerformanceListService {
 
             // 转换为VO列表
             for (StoreStaffConfig staff : staffList) {
-                if (staff == null || !CommonConstant.DELETE_FLAG_UNDELETE.equals(staff.getDeleteFlag())) {
+                if (staff == null || !CommonConstant.DELETE_FLAG_UNDELETE.equals(staff.getDeleteFlag())
+                        ||!"1".equals(staff.getStatus()) ||!CommonConstant.ONLINE_STATUS.equals(staff.getOnlineStatus())
+                        ) {
                     continue;
                 }
 

+ 135 - 7
alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentAppealServiceImpl.java

@@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.google.common.collect.Lists;
 import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpHeaders;
@@ -34,6 +35,7 @@ import shop.alien.util.common.netease.TextCheckUtil;
 import shop.alien.util.common.safe.TextModerationResultVO;
 import shop.alien.util.common.safe.TextModerationUtil;
 import shop.alien.util.common.safe.TextReviewServiceEnum;
+import shop.alien.util.common.constant.CommentSourceTypeEnum;
 
 import java.net.URLEncoder;
 import java.text.SimpleDateFormat;
@@ -46,6 +48,7 @@ import java.util.stream.Collectors;
  * @author ssk
  * @since 2025-01-02
  */
+@Slf4j
 @Service
 @Transactional
 @RequiredArgsConstructor
@@ -76,6 +79,12 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
 
     private final WebSocketProcess webSocketProcess;
 
+    private final CommonRatingMapper commonRatingMapper;
+
+    private final CommonCommentMapper commonCommentMapper;
+
+    private final LifeLikeRecordMapper lifeLikeRecordMapper;
+
     /**
      * 懒得查, 留着导出Excel
      */
@@ -345,13 +354,23 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
         }
         commentDetail.setImgList(imgList);
 
+        // 处理评价图片(common_rating.image_urls 已经是URL字符串,不是ID)
         if (StringUtils.isNotEmpty(commentDetail.getCommentImgId())) {
             String[] split2 = commentDetail.getCommentImgId().split(",");
             List<String> imgList2 = new ArrayList<>();
             for (String s : split2) {
-                StoreImg storeImg = storeImgMapper.selectById(s);
-                if (null != storeImg) {
-                    imgList2.add(storeImg.getImgUrl());
+                String trimmedUrl = s.trim();
+                if (StringUtils.isNotEmpty(trimmedUrl)) {
+                    // 如果已经是URL格式(包含http://或https://),直接使用
+                    if (trimmedUrl.startsWith("http://") || trimmedUrl.startsWith("https://")) {
+                        imgList2.add(trimmedUrl);
+                    } else {
+                        // 否则尝试作为ID查询(兼容旧数据)
+                        StoreImg storeImg = storeImgMapper.selectById(trimmedUrl);
+                        if (null != storeImg) {
+                            imgList2.add(storeImg.getImgUrl());
+                        }
+                    }
                 }
             }
             commentDetail.setCommentImgList(imgList2);
@@ -453,11 +472,66 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
                 break;
             case 2:
                 storeCommentAppeal.setFinalResult("已同意");
-                //删除原评论
+                // 审核通过后删除评价(common_rating)和评论(common_comment)
                 StoreCommentAppeal byId = this.getById(id);
-                storeCommentMapper.deleteById(byId.getCommentId());
-                //删除原评论下的评论
-                storeCommentMapper.update(null, new LambdaUpdateWrapper<StoreComment>().eq(StoreComment::getReplyId, byId.getCommentId()).set(StoreComment::getDeleteFlag, 1));
+                Integer ratingId = byId.getCommentId(); // commentId 实际存储的是评价ID(common_rating.id)
+                
+                if (ratingId != null) {
+                    // 1. 删除评价(逻辑删除 common_rating 表)
+                    // 注意:因为 deleteFlag 字段有 @TableLogic 注解,必须使用原生 SQL 绕过限制
+                    CommonRating rating = commonRatingMapper.selectById(ratingId);
+                    if (rating != null) {
+                        // 使用原生 SQL 方法直接更新 delete_flag
+                        int rows = commonRatingMapper.logicDeleteById(ratingId);
+                        log.info("删除评价结果,ratingId={},影响行数={}", ratingId, rows);
+                    }
+                    
+                    // 2. 查询该评价下的所有评论ID(用于后续删除点赞记录)
+                    LambdaQueryWrapper<CommonComment> commentQueryWrapper = new LambdaQueryWrapper<>();
+                    commentQueryWrapper.eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())
+                                      .eq(CommonComment::getSourceId, ratingId)
+                                      .eq(CommonComment::getDeleteFlag, 0);
+                    List<CommonComment> comments = commonCommentMapper.selectList(commentQueryWrapper);
+                    List<Long> commentIds = comments.stream().map(CommonComment::getId).collect(Collectors.toList());
+                    
+                    // 3. 删除该评价下的所有评论(使用原生 SQL 绕过 @TableLogic 限制)
+                    int commentRows = commonCommentMapper.logicDeleteBySourceId(
+                            CommentSourceTypeEnum.STORE_COMMENT.getType(), ratingId);
+                    log.info("删除评价下的评论结果,ratingId={},影响行数={}", ratingId, commentRows);
+                    
+                    // 4. 删除评价的点赞记录(type=7 表示评价点赞)
+                    LambdaUpdateWrapper<LifeLikeRecord> ratingLikeWrapper = new LambdaUpdateWrapper<>();
+                    ratingLikeWrapper.eq(LifeLikeRecord::getType, "7")
+                                    .eq(LifeLikeRecord::getHuifuId, String.valueOf(ratingId))
+                                    .eq(LifeLikeRecord::getDeleteFlag, 0);
+                    ratingLikeWrapper.set(LifeLikeRecord::getDeleteFlag, 1);
+                    ratingLikeWrapper.set(LifeLikeRecord::getUpdatedTime, new Date());
+                    lifeLikeRecordMapper.update(null, ratingLikeWrapper);
+                    log.info("删除评价的点赞记录,ratingId={}", ratingId);
+                    
+                    // 5. 删除评论的点赞记录(type=1 表示评论点赞)
+                    if (!commentIds.isEmpty()) {
+                        for (Long commentId : commentIds) {
+                            LambdaUpdateWrapper<LifeLikeRecord> commentLikeWrapper = new LambdaUpdateWrapper<>();
+                            commentLikeWrapper.eq(LifeLikeRecord::getType, "1")
+                                             .eq(LifeLikeRecord::getHuifuId, String.valueOf(commentId))
+                                             .eq(LifeLikeRecord::getDeleteFlag, 0);
+                            commentLikeWrapper.set(LifeLikeRecord::getDeleteFlag, 1);
+                            commentLikeWrapper.set(LifeLikeRecord::getUpdatedTime, new Date());
+                            lifeLikeRecordMapper.update(null, commentLikeWrapper);
+                        }
+                        log.info("删除评论的点赞记录,评论数={}", commentIds.size());
+                    }
+                    
+                    // 6. 重新统计店铺评分(删除评价后需要更新门店评分)
+                    if (rating != null && rating.getBusinessType() == 1) {
+                        updateStoreScore(rating.getBusinessId());
+                    }
+                }
+                
+                // 7. 不删除申诉记录,保留以便申诉历史能显示已通过的记录
+                // 申诉记录保留,只更新状态为"已同意",评价快照数据仍可在申诉历史中查看
+                log.info("申诉审核通过,保留申诉记录,申诉ID={},评价ID={}", id, ratingId);
                 break;
         }
         //商家申诉
@@ -541,4 +615,58 @@ public class StoreCommentAppealServiceImpl extends ServiceImpl<StoreCommentAppea
         }
     }
 
+    /**
+     * 更新店铺评分(删除评价后重新计算门店评分)
+     *
+     * @param businessId 店铺ID
+     */
+    private void updateStoreScore(Integer businessId) {
+        try {
+            // 查询该店铺的所有有效评价
+            LambdaQueryWrapper<CommonRating> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(CommonRating::getBusinessType, 1)
+                   .eq(CommonRating::getBusinessId, businessId)
+                   .eq(CommonRating::getDeleteFlag, 0)
+                   .eq(CommonRating::getIsShow, 1)
+                   .eq(CommonRating::getAuditStatus, 1);
+            List<CommonRating> ratings = commonRatingMapper.selectList(wrapper);
+            
+            int total = ratings.size();
+            if (total == 0) {
+                // 没有评价,设置默认评分为0
+                StoreInfo storeInfo = new StoreInfo();
+                storeInfo.setId(businessId);
+                storeInfo.setScoreAvg(0.0);
+                storeInfo.setScoreOne(0.0);
+                storeInfo.setScoreTwo(0.0);
+                storeInfo.setScoreThree(0.0);
+                storeInfoMapper.updateById(storeInfo);
+                log.info("店铺无有效评价,重置评分为0,businessId={}", businessId);
+                return;
+            }
+            
+            // 计算平均评分
+            double scoreSum = ratings.stream().mapToDouble(r -> r.getScore() != null ? r.getScore() : 0.0).sum();
+            double scoreOneSum = ratings.stream().mapToDouble(r -> r.getScoreOne() != null ? r.getScoreOne() : 0.0).sum();
+            double scoreTwoSum = ratings.stream().mapToDouble(r -> r.getScoreTwo() != null ? r.getScoreTwo() : 0.0).sum();
+            double scoreThreeSum = ratings.stream().mapToDouble(r -> r.getScoreThree() != null ? r.getScoreThree() : 0.0).sum();
+            
+            double scoreAvg = Math.round((scoreSum / total) * 100.0) / 100.0;
+            double scoreOne = Math.round((scoreOneSum / total) * 100.0) / 100.0;
+            double scoreTwo = Math.round((scoreTwoSum / total) * 100.0) / 100.0;
+            double scoreThree = Math.round((scoreThreeSum / total) * 100.0) / 100.0;
+            
+            StoreInfo storeInfo = new StoreInfo();
+            storeInfo.setId(businessId);
+            storeInfo.setScoreAvg(scoreAvg);
+            storeInfo.setScoreOne(scoreOne);
+            storeInfo.setScoreTwo(scoreTwo);
+            storeInfo.setScoreThree(scoreThree);
+            storeInfoMapper.updateById(storeInfo);
+            
+            log.info("更新店铺评分成功,businessId={},评价数={},scoreAvg={}", businessId, total, scoreAvg);
+        } catch (Exception e) {
+            log.error("更新店铺评分失败,businessId={},error={}", businessId, e.getMessage());
+        }
+    }
 }

+ 391 - 298
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -5835,304 +5835,8 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
             result.setStoreBusinessInfos(storeBusinessInfos);
         }
         if(storeInfo.getBusinessStatus() == 0){
-            // 优先判断当前时间是否是特殊日期
-            LocalDate currentDate = LocalDate.now();
-            LocalTime currentTime = LocalTime.now();
-
-            log.info("开始判断营业状态 - 当前日期: {}, 当前时间: {}, 店铺ID: {}", currentDate, currentTime, storeId);
-
-            // 标记当前是否是特殊营业时间
-            boolean isCurrentSpecialBusinessTime = false;
-
-            // 查找特殊营业时间(businessType == 2)
-            StoreBusinessInfo specialBusinessInfo = storeBusinessInfos.stream()
-                    .filter(item -> item.getBusinessType() != null && item.getBusinessType() == 2)
-                    .filter(item -> {
-                        // 判断当前日期是否匹配特殊日期
-                        if (StringUtils.isEmpty(item.getBusinessDate())) {
-                            return false;
-                        }
-                        try {
-                            String businessDate = item.getBusinessDate().trim();
-
-                            // 移除开头的数字和空格(如 "2 元旦、春节、儿童节" -> "元旦、春节、儿童节")
-                            String holidayNames = businessDate.replaceAll("^\\d+\\s*", "").trim();
-
-                            // 判断是否包含节假日名称(支持所有特殊日期:元旦、春节、情人节、元宵节、清明节、劳动节、儿童节、端午节、七夕、中秋节、国庆节、冬至、平安夜、圣诞节)
-                            boolean isHolidayName = holidayNames.contains("元旦") || holidayNames.contains("春节") ||
-                                holidayNames.contains("情人节") || holidayNames.contains("元宵") ||
-                                holidayNames.contains("七夕") || holidayNames.contains("冬至") ||
-                                holidayNames.contains("平安夜") || holidayNames.contains("圣诞") ||
-                                holidayNames.contains("清明") || holidayNames.contains("劳动") ||
-                                holidayNames.contains("端午") || holidayNames.contains("中秋") ||
-                                holidayNames.contains("国庆") || holidayNames.contains("儿童");
-
-                            if (isHolidayName) {
-
-                                List<LocalDate[]> holidayRanges = getHolidayDateRanges(currentDate.getYear(), holidayNames);
-                                for (LocalDate[] range : holidayRanges) {
-                                    if (range != null && range.length == 2) {
-                                        LocalDate startDate = range[0];
-                                        LocalDate endDate = range[1];
-                                        if (!currentDate.isBefore(startDate) && !currentDate.isAfter(endDate)) {
-                                            log.info("特殊日期判断(节假日) - 节假日: {}, 日期范围: {}至{}, 当前日期: {}, 是否在范围内: true",
-                                                    holidayNames, startDate, endDate, currentDate);
-                                            return true;
-                                        }
-                                    }
-                                }
-                                log.info("特殊日期判断(节假日) - 节假日: {}, 当前日期: {}, 是否在范围内: false",
-                                        holidayNames, currentDate);
-                                return false;
-                            }
-
-                            // 如果不是节假日名称,尝试解析日期格式(兼容旧格式)
-                            String[] dateFormats = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd"};
-
-                            // 先检查是否包含"至"(日期范围)
-                            if (businessDate.contains("至")) {
-                                String[] dateRange = businessDate.split("至");
-                                if (dateRange.length >= 2) {
-                                    try {
-                                        LocalDate startDate = null;
-                                        LocalDate endDate = null;
-                                        for (String format : dateFormats) {
-                                            try {
-                                                DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
-                                                startDate = LocalDate.parse(dateRange[0].trim(), formatter);
-                                                endDate = LocalDate.parse(dateRange[1].trim(), formatter);
-                                                break;
-                                            } catch (Exception ignored) {
-                                            }
-                                        }
-                                        if (startDate != null && endDate != null) {
-                                            boolean inRange = !currentDate.isBefore(startDate) && !currentDate.isAfter(endDate);
-                                            log.info("特殊日期范围判断 - 日期范围: {}至{}, 当前日期: {}, 是否在范围内: {}",
-                                                    startDate, endDate, currentDate, inRange);
-                                            return inRange;
-                                        }
-                                    } catch (Exception e) {
-                                        log.warn("解析特殊日期范围失败: {}", businessDate, e);
-                                    }
-                                }
-                            } else {
-                                // 尝试解析单个日期格式
-                                for (String format : dateFormats) {
-                                    try {
-                                        DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
-                                        LocalDate specialDate = LocalDate.parse(businessDate, formatter);
-                                        boolean matches = specialDate.equals(currentDate);
-                                        log.info("特殊日期判断 - 特殊日期: {}, 当前日期: {}, 是否匹配: {}",
-                                                specialDate, currentDate, matches);
-                                        if (matches) {
-                                            return true;
-                                        }
-                                    } catch (Exception ignored) {
-                                        // 继续尝试下一个格式
-                                    }
-                                }
-                            }
-                        } catch (Exception e) {
-                            log.warn("解析特殊日期失败: {}", item.getBusinessDate(), e);
-                        }
-                        return false;
-                    })
-                    .findFirst()
-                    .orElse(null);
-
-            // 如果找到匹配的特殊日期,判断是否在营业时间内
-            if (specialBusinessInfo != null) {
-                log.info("找到匹配的特殊日期营业时间 - 开始时间: {}, 结束时间: {}",
-                        specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime());
-                if (StringUtils.isNotEmpty(specialBusinessInfo.getStartTime())
-                        && StringUtils.isNotEmpty(specialBusinessInfo.getEndTime())) {
-                    try {
-                        String[] startArr = specialBusinessInfo.getStartTime().split(":");
-                        String[] endArr = specialBusinessInfo.getEndTime().split(":");
-                        if (startArr.length >= 2 && endArr.length >= 2) {
-                            LocalTime start = LocalTime.of(
-                                    Integer.parseInt(startArr[0]),
-                                    Integer.parseInt(startArr[1])
-                            );
-                            LocalTime end = LocalTime.of(
-                                    Integer.parseInt(endArr[0]),
-                                    Integer.parseInt(endArr[1])
-                            );
-
-                            boolean isInBusiness;
-                            // 判断是否是全天营业(00:00到00:00)
-                            LocalTime midnight = LocalTime.of(0, 0);
-                            if (start.equals(midnight) && end.equals(midnight)) {
-                                // 全天营业
-                                isInBusiness = true;
-                                log.info("特殊日期营业时间判断 - 全天营业(00:00-00:00),当前时间: {},是否在营业时间内: true", currentTime);
-                            } else {
-                                // 处理跨天营业
-                                if (start.isBefore(end) || start.equals(end)) {
-                                    // 同一天营业:包含边界值
-                                    isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
-                                            && (currentTime.isBefore(end) || currentTime.equals(end));
-                                } else {
-                                    // 跨天营业:包含边界值
-                                    isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
-                                            || (currentTime.isBefore(end) || currentTime.equals(end));
-                                }
-                                log.info("特殊日期营业时间判断 - 开始时间: {}, 结束时间: {}, 当前时间: {}, 是否在营业时间内: {}",
-                                        start, end, currentTime, isInBusiness);
-                            }
-
-                            result.setYyFlag(isInBusiness ? 1 : 0);
-                            isCurrentSpecialBusinessTime = isInBusiness;
-                        } else {
-                            log.warn("特殊日期营业时间格式错误 - 开始时间: {}, 结束时间: {}",
-                                    specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime());
-                            result.setYyFlag(0);
-                            isCurrentSpecialBusinessTime = false;
-                        }
-                    } catch (NumberFormatException e) {
-                        log.warn("解析特殊日期营业时间失败 - 开始时间: {}, 结束时间: {}",
-                                specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime(), e);
-                        result.setYyFlag(0);
-                        isCurrentSpecialBusinessTime = false;
-                    }
-                } else {
-                    log.warn("特殊日期营业时间为空 - 开始时间: {}, 结束时间: {}",
-                            specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime());
-                    result.setYyFlag(0);
-                    isCurrentSpecialBusinessTime = false;
-                }
-            } else {
-                // 如果不是特殊日期,判断正常营业时间(businessType == 1)
-                StoreBusinessInfo normalBusinessInfo = storeBusinessInfos.stream()
-                        .filter(item -> item.getBusinessType() != null && item.getBusinessType() == 1)
-                        .findFirst()
-                        .orElse(null);
-
-                if (normalBusinessInfo != null) {
-                    log.info("判断正常营业时间 - 营业日期: {}, 开始时间: {}, 结束时间: {}",
-                            normalBusinessInfo.getBusinessDate(),
-                            normalBusinessInfo.getStartTime(),
-                            normalBusinessInfo.getEndTime());
-                    // 获取当前星期并转换为业务格式
-//                    Calendar calendar = Calendar.getInstance();
-//                    int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); // 1=周日,2=周一...7=周六
-//                    String[] days = {"7", "1", "2", "3", "4", "5", "6"};
-//                    String day = days[dayOfWeek - 1];
-
-                    Calendar calendar = Calendar.getInstance();
-                    int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); // 1=周日,2=周一...7=周六
-                    // 映射数组:索引对应dayOfWeek-1,值为中文星期
-                    String[] weekDaysCN = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
-                    String day = weekDaysCN[dayOfWeek - 1];
-
-                    log.info("当前星期: {}, 转换后: {}", dayOfWeek, day);
-
-                    // 判断当前日期是否在营业日期范围内
-                    if (StringUtils.isNotEmpty(normalBusinessInfo.getBusinessDate())
-                            && normalBusinessInfo.getBusinessDate().contains(day)) {
-                        log.info("当前星期在营业日期范围内");
-                        if (StringUtils.isNotEmpty(normalBusinessInfo.getStartTime())
-                                && StringUtils.isNotEmpty(normalBusinessInfo.getEndTime())) {
-                            try {
-                                String[] startArr = normalBusinessInfo.getStartTime().split(":");
-                                String[] endArr = normalBusinessInfo.getEndTime().split(":");
-                                if (startArr.length >= 2 && endArr.length >= 2) {
-                                    LocalTime start = LocalTime.of(
-                                            Integer.parseInt(startArr[0]),
-                                            Integer.parseInt(startArr[1])
-                                    );
-                                    LocalTime end = LocalTime.of(
-                                            Integer.parseInt(endArr[0]),
-                                            Integer.parseInt(endArr[1])
-                                    );
-
-                                    boolean isInBusiness;
-                                    // 判断是否是全天营业(00:00到00:00)
-                                    LocalTime midnight = LocalTime.of(0, 0);
-                                    if (start.equals(midnight) && end.equals(midnight)) {
-                                        // 全天营业
-                                        isInBusiness = true;
-                                        log.info("正常营业时间判断 - 全天营业(00:00-00:00),当前时间: {},是否在营业时间内: true", currentTime);
-                                    } else {
-                                        // 处理跨天营业
-                                        if (start.isBefore(end) || start.equals(end)) {
-                                            // 同一天营业:包含边界值
-                                            isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
-                                                    && (currentTime.isBefore(end) || currentTime.equals(end));
-                                        } else {
-                                            // 跨天营业:包含边界值
-                                            isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
-                                                    || (currentTime.isBefore(end) || currentTime.equals(end));
-                                        }
-                                        log.info("正常营业时间判断 - 开始时间: {}, 结束时间: {}, 当前时间: {}, 是否在营业时间内: {}",
-                                                start, end, currentTime, isInBusiness);
-                                    }
-
-                                    result.setYyFlag(isInBusiness ? 1 : 0);
-                                } else {
-                                    log.warn("正常营业时间格式错误 - 开始时间: {}, 结束时间: {}",
-                                            normalBusinessInfo.getStartTime(), normalBusinessInfo.getEndTime());
-                                    result.setYyFlag(0);
-                                }
-                            } catch (NumberFormatException e) {
-                                log.warn("解析正常营业时间失败 - 开始时间: {}, 结束时间: {}",
-                                        normalBusinessInfo.getStartTime(), normalBusinessInfo.getEndTime(), e);
-                                result.setYyFlag(0);
-                            }
-                        } else {
-                            log.warn("正常营业时间为空 - 开始时间: {}, 结束时间: {}",
-                                    normalBusinessInfo.getStartTime(), normalBusinessInfo.getEndTime());
-                            result.setYyFlag(0);
-                        }
-                    } else {
-                        log.info("当前星期不在营业日期范围内 - 营业日期: {}, 当前星期: {}",
-                                normalBusinessInfo.getBusinessDate(), day);
-                        result.setYyFlag(0);
-                    }
-                } else {
-                    log.warn("没有找到正常营业时间配置");
-                    result.setYyFlag(0);
-                }
-            }
-            log.info("最终营业状态 - yyFlag: {}", result.getYyFlag());
-
-            // 根据当前是否是特殊营业时间,对storeBusinessInfos进行排序
-            if (ObjectUtils.isNotEmpty(storeBusinessInfos)) {
-                final boolean isSpecial = isCurrentSpecialBusinessTime;
-                List<StoreBusinessInfo> sortedBusinessInfos = storeBusinessInfos.stream()
-                        .sorted((a, b) -> {
-                            Integer typeA = a.getBusinessType() != null ? a.getBusinessType() : 0;
-                            Integer typeB = b.getBusinessType() != null ? b.getBusinessType() : 0;
-
-                            // 如果当前是特殊营业时间,businessType == 2 排在前面
-                            if (isSpecial) {
-                                if (typeA == 2 && typeB != 2) {
-                                    return -1; // a排在前面
-                                } else if (typeA != 2 && typeB == 2) {
-                                    return 1; // b排在前面
-                                }
-                            } else {
-                                // 如果当前不是特殊营业时间,businessType == 1 排在前面
-                                if (typeA == 1 && typeB != 1) {
-                                    return -1; // a排在前面
-                                } else if (typeA != 1 && typeB == 1) {
-                                    return 1; // b排在前面
-                                }
-                            }
-                            // 相同类型,保持原有顺序
-                            return 0;
-                        })
-                        .collect(Collectors.toList());
-
-                // 重新设置排序后的列表
-                result.setStoreBusinessInfos(sortedBusinessInfos);
-                if (!sortedBusinessInfos.isEmpty()) {
-                    result.setStoreBusinessInfo(sortedBusinessInfos.get(0));
-                }
-
-                log.info("营业时间列表排序完成 - 当前是特殊营业时间: {}, 排序后第一个businessType: {}",
-                        isSpecial, sortedBusinessInfos.isEmpty() ? "无" : sortedBusinessInfos.get(0).getBusinessType());
-            }
+            // 调用提取的营业时间判断方法
+            calculateBusinessStatus(storeId, storeBusinessInfos, result);
         } else {
             log.info("店铺营业状态不为0,不判断营业时间 - businessStatus: {}", storeInfo.getBusinessStatus());
         }
@@ -6177,6 +5881,395 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     }
 
     @Override
+    public StoreBusinessStatusVo getStoreBusinessStatus(String storeId) {
+        log.info("StoreInfoServiceImpl.getStoreBusinessStatus?storeId={}", storeId);
+        StoreBusinessStatusVo result = new StoreBusinessStatusVo();
+        
+        StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            log.warn("店铺不存在,storeId={}", storeId);
+            return result;
+        }
+        
+        result.setBusinessStatus(storeInfo.getBusinessStatus());
+        
+        // 查询营业时间
+        List<StoreBusinessInfo> storeBusinessInfos = storeBusinessInfoMapper.selectList(
+                new LambdaQueryWrapper<StoreBusinessInfo>()
+                        .eq(StoreBusinessInfo::getStoreId, storeId)
+                        .eq(StoreBusinessInfo::getDeleteFlag, 0)
+        );
+        
+        // 回显所有营业时间信息(特殊营业时间和正常营业时间)
+        if (ObjectUtils.isNotEmpty(storeBusinessInfos)) {
+            result.setStoreBusinessInfo(storeBusinessInfos.get(0));
+            result.setStoreBusinessInfos(storeBusinessInfos);
+        }
+        
+        // 判断营业状态
+        if (storeInfo.getBusinessStatus() == 0) {
+            // 调用提取的营业时间判断方法
+            calculateBusinessStatus(storeId, storeBusinessInfos, result);
+        } else {
+            log.info("店铺营业状态不为0,不判断营业时间 - businessStatus: {}", storeInfo.getBusinessStatus());
+            result.setYyFlag(0);
+        }
+        
+        return result;
+    }
+
+    /**
+     * 计算店铺营业状态
+     * 判断当前时间是否在营业时间内,并设置营业状态和排序后的营业时间列表
+     *
+     * @param storeId 店铺ID
+     * @param storeBusinessInfos 营业时间列表
+     * @param result 结果对象(StoreInfoVo或StoreBusinessStatusVo)
+     */
+    private void calculateBusinessStatus(String storeId, List<StoreBusinessInfo> storeBusinessInfos, Object result) {
+        // 优先判断当前时间是否是特殊日期
+        LocalDate currentDate = LocalDate.now();
+        LocalTime currentTime = LocalTime.now();
+
+        log.info("开始判断营业状态 - 当前日期: {}, 当前时间: {}, 店铺ID: {}", currentDate, currentTime, storeId);
+
+        // 标记当前是否是特殊营业时间
+        boolean isCurrentSpecialBusinessTime = false;
+
+        // 查找特殊营业时间(businessType == 2)
+        StoreBusinessInfo specialBusinessInfo = storeBusinessInfos.stream()
+                .filter(item -> item.getBusinessType() != null && item.getBusinessType() == 2)
+                .filter(item -> {
+                    // 判断当前日期是否匹配特殊日期
+                    if (StringUtils.isEmpty(item.getBusinessDate())) {
+                        return false;
+                    }
+                    try {
+                        String businessDate = item.getBusinessDate().trim();
+
+                        // 移除开头的数字和空格(如 "2 元旦、春节、儿童节" -> "元旦、春节、儿童节")
+                        String holidayNames = businessDate.replaceAll("^\\d+\\s*", "").trim();
+
+                        // 判断是否包含节假日名称(支持所有特殊日期:元旦、春节、情人节、元宵节、清明节、劳动节、儿童节、端午节、七夕、中秋节、国庆节、冬至、平安夜、圣诞节)
+                        boolean isHolidayName = holidayNames.contains("元旦") || holidayNames.contains("春节") ||
+                            holidayNames.contains("情人节") || holidayNames.contains("元宵") ||
+                            holidayNames.contains("七夕") || holidayNames.contains("冬至") ||
+                            holidayNames.contains("平安夜") || holidayNames.contains("圣诞") ||
+                            holidayNames.contains("清明") || holidayNames.contains("劳动") ||
+                            holidayNames.contains("端午") || holidayNames.contains("中秋") ||
+                            holidayNames.contains("国庆") || holidayNames.contains("儿童");
+
+                        if (isHolidayName) {
+
+                            List<LocalDate[]> holidayRanges = getHolidayDateRanges(currentDate.getYear(), holidayNames);
+                            for (LocalDate[] range : holidayRanges) {
+                                if (range != null && range.length == 2) {
+                                    LocalDate startDate = range[0];
+                                    LocalDate endDate = range[1];
+                                    if (!currentDate.isBefore(startDate) && !currentDate.isAfter(endDate)) {
+                                        log.info("特殊日期判断(节假日) - 节假日: {}, 日期范围: {}至{}, 当前日期: {}, 是否在范围内: true",
+                                                holidayNames, startDate, endDate, currentDate);
+                                        return true;
+                                    }
+                                }
+                            }
+                            log.info("特殊日期判断(节假日) - 节假日: {}, 当前日期: {}, 是否在范围内: false",
+                                    holidayNames, currentDate);
+                            return false;
+                        }
+
+                        // 如果不是节假日名称,尝试解析日期格式(兼容旧格式)
+                        String[] dateFormats = {"yyyy-MM-dd", "yyyy/MM/dd", "yyyyMMdd"};
+
+                        // 先检查是否包含"至"(日期范围)
+                        if (businessDate.contains("至")) {
+                            String[] dateRange = businessDate.split("至");
+                            if (dateRange.length >= 2) {
+                                try {
+                                    LocalDate startDate = null;
+                                    LocalDate endDate = null;
+                                    for (String format : dateFormats) {
+                                        try {
+                                            DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
+                                            startDate = LocalDate.parse(dateRange[0].trim(), formatter);
+                                            endDate = LocalDate.parse(dateRange[1].trim(), formatter);
+                                            break;
+                                        } catch (Exception ignored) {
+                                        }
+                                    }
+                                    if (startDate != null && endDate != null) {
+                                        boolean inRange = !currentDate.isBefore(startDate) && !currentDate.isAfter(endDate);
+                                        log.info("特殊日期范围判断 - 日期范围: {}至{}, 当前日期: {}, 是否在范围内: {}",
+                                                startDate, endDate, currentDate, inRange);
+                                        return inRange;
+                                    }
+                                } catch (Exception e) {
+                                    log.warn("解析特殊日期范围失败: {}", businessDate, e);
+                                }
+                            }
+                        } else {
+                            // 尝试解析单个日期格式
+                            for (String format : dateFormats) {
+                                try {
+                                    DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format);
+                                    LocalDate specialDate = LocalDate.parse(businessDate, formatter);
+                                    boolean matches = specialDate.equals(currentDate);
+                                    log.info("特殊日期判断 - 特殊日期: {}, 当前日期: {}, 是否匹配: {}",
+                                            specialDate, currentDate, matches);
+                                    if (matches) {
+                                        return true;
+                                    }
+                                } catch (Exception ignored) {
+                                    // 继续尝试下一个格式
+                                }
+                            }
+                        }
+                    } catch (Exception e) {
+                        log.warn("解析特殊日期失败: {}", item.getBusinessDate(), e);
+                    }
+                    return false;
+                })
+                .findFirst()
+                .orElse(null);
+
+        // 如果找到匹配的特殊日期,判断是否在营业时间内
+        if (specialBusinessInfo != null) {
+            log.info("找到匹配的特殊日期营业时间 - 开始时间: {}, 结束时间: {}",
+                    specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime());
+            if (StringUtils.isNotEmpty(specialBusinessInfo.getStartTime())
+                    && StringUtils.isNotEmpty(specialBusinessInfo.getEndTime())) {
+                try {
+                    String[] startArr = specialBusinessInfo.getStartTime().split(":");
+                    String[] endArr = specialBusinessInfo.getEndTime().split(":");
+                    if (startArr.length >= 2 && endArr.length >= 2) {
+                        LocalTime start = LocalTime.of(
+                                Integer.parseInt(startArr[0]),
+                                Integer.parseInt(startArr[1])
+                        );
+                        LocalTime end = LocalTime.of(
+                                Integer.parseInt(endArr[0]),
+                                Integer.parseInt(endArr[1])
+                        );
+
+                        boolean isInBusiness;
+                        // 判断是否是全天营业(00:00到00:00)
+                        LocalTime midnight = LocalTime.of(0, 0);
+                        if (start.equals(midnight) && end.equals(midnight)) {
+                            // 全天营业
+                            isInBusiness = true;
+                            log.info("特殊日期营业时间判断 - 全天营业(00:00-00:00),当前时间: {},是否在营业时间内: true", currentTime);
+                        } else {
+                            // 处理跨天营业
+                            if (start.isBefore(end) || start.equals(end)) {
+                                // 同一天营业:包含边界值
+                                isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
+                                        && (currentTime.isBefore(end) || currentTime.equals(end));
+                            } else {
+                                // 跨天营业:包含边界值
+                                isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
+                                        || (currentTime.isBefore(end) || currentTime.equals(end));
+                            }
+                            log.info("特殊日期营业时间判断 - 开始时间: {}, 结束时间: {}, 当前时间: {}, 是否在营业时间内: {}",
+                                    start, end, currentTime, isInBusiness);
+                        }
+
+                        setYyFlag(result, isInBusiness ? 1 : 0);
+                        isCurrentSpecialBusinessTime = isInBusiness;
+                    } else {
+                        log.warn("特殊日期营业时间格式错误 - 开始时间: {}, 结束时间: {}",
+                                specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime());
+                        setYyFlag(result, 0);
+                        isCurrentSpecialBusinessTime = false;
+                    }
+                } catch (NumberFormatException e) {
+                    log.warn("解析特殊日期营业时间失败 - 开始时间: {}, 结束时间: {}",
+                            specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime(), e);
+                    setYyFlag(result, 0);
+                    isCurrentSpecialBusinessTime = false;
+                }
+            } else {
+                log.warn("特殊日期营业时间为空 - 开始时间: {}, 结束时间: {}",
+                        specialBusinessInfo.getStartTime(), specialBusinessInfo.getEndTime());
+                setYyFlag(result, 0);
+                isCurrentSpecialBusinessTime = false;
+            }
+        } else {
+            // 如果不是特殊日期,判断正常营业时间(businessType == 1)
+            StoreBusinessInfo normalBusinessInfo = storeBusinessInfos.stream()
+                    .filter(item -> item.getBusinessType() != null && item.getBusinessType() == 1)
+                    .findFirst()
+                    .orElse(null);
+
+            if (normalBusinessInfo != null) {
+                log.info("判断正常营业时间 - 营业日期: {}, 开始时间: {}, 结束时间: {}",
+                        normalBusinessInfo.getBusinessDate(),
+                        normalBusinessInfo.getStartTime(),
+                        normalBusinessInfo.getEndTime());
+                // 获取当前星期并转换为业务格式
+                Calendar calendar = Calendar.getInstance();
+                int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); // 1=周日,2=周一...7=周六
+                // 映射数组:索引对应dayOfWeek-1,值为中文星期
+                String[] weekDaysCN = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
+                String day = weekDaysCN[dayOfWeek - 1];
+
+                log.info("当前星期: {}, 转换后: {}", dayOfWeek, day);
+
+                // 判断当前日期是否在营业日期范围内
+                if (StringUtils.isNotEmpty(normalBusinessInfo.getBusinessDate())
+                        && normalBusinessInfo.getBusinessDate().contains(day)) {
+                    log.info("当前星期在营业日期范围内");
+                    if (StringUtils.isNotEmpty(normalBusinessInfo.getStartTime())
+                            && StringUtils.isNotEmpty(normalBusinessInfo.getEndTime())) {
+                        try {
+                            String[] startArr = normalBusinessInfo.getStartTime().split(":");
+                            String[] endArr = normalBusinessInfo.getEndTime().split(":");
+                            if (startArr.length >= 2 && endArr.length >= 2) {
+                                LocalTime start = LocalTime.of(
+                                        Integer.parseInt(startArr[0]),
+                                        Integer.parseInt(startArr[1])
+                                );
+                                LocalTime end = LocalTime.of(
+                                        Integer.parseInt(endArr[0]),
+                                        Integer.parseInt(endArr[1])
+                                );
+
+                                boolean isInBusiness;
+                                // 判断是否是全天营业(00:00到00:00)
+                                LocalTime midnight = LocalTime.of(0, 0);
+                                if (start.equals(midnight) && end.equals(midnight)) {
+                                    // 全天营业
+                                    isInBusiness = true;
+                                    log.info("正常营业时间判断 - 全天营业(00:00-00:00),当前时间: {},是否在营业时间内: true", currentTime);
+                                } else {
+                                    // 处理跨天营业
+                                    if (start.isBefore(end) || start.equals(end)) {
+                                        // 同一天营业:包含边界值
+                                        isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
+                                                && (currentTime.isBefore(end) || currentTime.equals(end));
+                                    } else {
+                                        // 跨天营业:包含边界值
+                                        isInBusiness = (currentTime.isAfter(start) || currentTime.equals(start))
+                                                || (currentTime.isBefore(end) || currentTime.equals(end));
+                                    }
+                                    log.info("正常营业时间判断 - 开始时间: {}, 结束时间: {}, 当前时间: {}, 是否在营业时间内: {}",
+                                            start, end, currentTime, isInBusiness);
+                                }
+
+                                setYyFlag(result, isInBusiness ? 1 : 0);
+                            } else {
+                                log.warn("正常营业时间格式错误 - 开始时间: {}, 结束时间: {}",
+                                        normalBusinessInfo.getStartTime(), normalBusinessInfo.getEndTime());
+                                setYyFlag(result, 0);
+                            }
+                        } catch (NumberFormatException e) {
+                            log.warn("解析正常营业时间失败 - 开始时间: {}, 结束时间: {}",
+                                    normalBusinessInfo.getStartTime(), normalBusinessInfo.getEndTime(), e);
+                            setYyFlag(result, 0);
+                        }
+                    } else {
+                        log.warn("正常营业时间为空 - 开始时间: {}, 结束时间: {}",
+                                normalBusinessInfo.getStartTime(), normalBusinessInfo.getEndTime());
+                        setYyFlag(result, 0);
+                    }
+                } else {
+                    log.info("当前星期不在营业日期范围内 - 营业日期: {}, 当前星期: {}",
+                            normalBusinessInfo.getBusinessDate(), day);
+                    setYyFlag(result, 0);
+                }
+            } else {
+                log.warn("没有找到正常营业时间配置");
+                setYyFlag(result, 0);
+            }
+        }
+        
+        Integer yyFlag = getYyFlag(result);
+        log.info("最终营业状态 - yyFlag: {}", yyFlag);
+
+        // 根据当前是否是特殊营业时间,对storeBusinessInfos进行排序
+        if (ObjectUtils.isNotEmpty(storeBusinessInfos)) {
+            final boolean isSpecial = isCurrentSpecialBusinessTime;
+            List<StoreBusinessInfo> sortedBusinessInfos = storeBusinessInfos.stream()
+                    .sorted((a, b) -> {
+                        Integer typeA = a.getBusinessType() != null ? a.getBusinessType() : 0;
+                        Integer typeB = b.getBusinessType() != null ? b.getBusinessType() : 0;
+
+                        // 如果当前是特殊营业时间,businessType == 2 排在前面
+                        if (isSpecial) {
+                            if (typeA == 2 && typeB != 2) {
+                                return -1; // a排在前面
+                            } else if (typeA != 2 && typeB == 2) {
+                                return 1; // b排在前面
+                            }
+                        } else {
+                            // 如果当前不是特殊营业时间,businessType == 1 排在前面
+                            if (typeA == 1 && typeB != 1) {
+                                return -1; // a排在前面
+                            } else if (typeA != 1 && typeB == 1) {
+                                return 1; // b排在前面
+                            }
+                        }
+                        // 相同类型,保持原有顺序
+                        return 0;
+                    })
+                    .collect(Collectors.toList());
+
+            // 重新设置排序后的列表
+            setStoreBusinessInfos(result, sortedBusinessInfos);
+            if (!sortedBusinessInfos.isEmpty()) {
+                setStoreBusinessInfo(result, sortedBusinessInfos.get(0));
+            }
+
+            log.info("营业时间列表排序完成 - 当前是特殊营业时间: {}, 排序后第一个businessType: {}",
+                    isSpecial, sortedBusinessInfos.isEmpty() ? "无" : sortedBusinessInfos.get(0).getBusinessType());
+        }
+    }
+
+    /**
+     * 设置yyFlag(支持StoreInfoVo和StoreBusinessStatusVo)
+     */
+    private void setYyFlag(Object result, Integer yyFlag) {
+        if (result instanceof StoreInfoVo) {
+            ((StoreInfoVo) result).setYyFlag(yyFlag);
+        } else if (result instanceof StoreBusinessStatusVo) {
+            ((StoreBusinessStatusVo) result).setYyFlag(yyFlag);
+        }
+    }
+
+    /**
+     * 获取yyFlag(支持StoreInfoVo和StoreBusinessStatusVo)
+     */
+    private Integer getYyFlag(Object result) {
+        if (result instanceof StoreInfoVo) {
+            return ((StoreInfoVo) result).getYyFlag();
+        } else if (result instanceof StoreBusinessStatusVo) {
+            return ((StoreBusinessStatusVo) result).getYyFlag();
+        }
+        return 0;
+    }
+
+    /**
+     * 设置storeBusinessInfo(支持StoreInfoVo和StoreBusinessStatusVo)
+     */
+    private void setStoreBusinessInfo(Object result, StoreBusinessInfo storeBusinessInfo) {
+        if (result instanceof StoreInfoVo) {
+            ((StoreInfoVo) result).setStoreBusinessInfo(storeBusinessInfo);
+        } else if (result instanceof StoreBusinessStatusVo) {
+            ((StoreBusinessStatusVo) result).setStoreBusinessInfo(storeBusinessInfo);
+        }
+    }
+
+    /**
+     * 设置storeBusinessInfos(支持StoreInfoVo和StoreBusinessStatusVo)
+     */
+    private void setStoreBusinessInfos(Object result, List<StoreBusinessInfo> storeBusinessInfos) {
+        if (result instanceof StoreInfoVo) {
+            ((StoreInfoVo) result).setStoreBusinessInfos(storeBusinessInfos);
+        } else if (result instanceof StoreBusinessStatusVo) {
+            ((StoreBusinessStatusVo) result).setStoreBusinessInfos(storeBusinessInfos);
+        }
+    }
+
+    @Override
     public List<LifeCouponVo> getStoreCouponList(String storeId) {
         // 获取店铺代金券列表
         LambdaUpdateWrapper<LifeCoupon> quanWrapper = new LambdaUpdateWrapper<>();

+ 938 - 60
alien-store/src/main/java/shop/alien/store/service/impl/StoreOperationalStatisticsServiceImpl.java

@@ -11,6 +11,10 @@ import shop.alien.entity.store.vo.StoreOperationalStatisticsComparisonVo;
 import shop.alien.entity.store.vo.StoreOperationalStatisticsVo;
 import shop.alien.mapper.*;
 import shop.alien.store.service.StoreOperationalStatisticsService;
+import shop.alien.store.util.StatisticsComparisonImageUtil;
+import shop.alien.util.ali.AliOSSUtil;
+import shop.alien.util.common.RandomCreateUtil;
+import shop.alien.util.pdf.ImageToPdfUtil;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
@@ -48,55 +52,117 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private final StorePriceMapper storePriceMapper;
     private final StoreUserMapper storeUserMapper;
     private final StoreOperationalStatisticsHistoryMapper statisticsHistoryMapper;
+    private final StoreTrackStatisticsMapper storeTrackStatisticsMapper;
+    private final AliOSSUtil aliOSSUtil;
 
     private static final String DATE_FORMAT = "yyyy-MM-dd";
+    private static final String STAT_TYPE_DAILY = "DAILY";
 
     @Override
     public StoreOperationalStatisticsVo getStatistics(Integer storeId, String startTime, String endTime) {
         log.info("StoreOperationalStatisticsServiceImpl.getStatistics - storeId={}, startTime={}, endTime={}", storeId, startTime, endTime);
+        return calculateStatistics(storeId, startTime, endTime);
+    }
 
-        StoreOperationalStatisticsVo statistics = new StoreOperationalStatisticsVo();
-
+    /**
+     * 计算统计数据(从store_track_statistics表查询并累加)
+     */
+    private StoreOperationalStatisticsVo calculateStatistics(Integer storeId, String startTime, String endTime) {
         try {
             // 解析时间范围
             SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
             Date startDate = sdf.parse(startTime);
             Date endDate = sdf.parse(endTime);
-            // 设置为当天的结束时间
-            Calendar endCal = Calendar.getInstance();
-            endCal.setTime(endDate);
-            endCal.set(Calendar.HOUR_OF_DAY, 23);
-            endCal.set(Calendar.MINUTE, 59);
-            endCal.set(Calendar.SECOND, 59);
-            endDate = endCal.getTime();
-
-            // 1. 流量数据统计
-            statistics.setTrafficData(getTrafficData(storeId, startDate, endDate));
-
-            // 2. 互动数据统计
-            statistics.setInteractionData(getInteractionData(storeId, startDate, endDate));
-
-            // 3. 优惠券数据统计
-            statistics.setCouponData(getCouponData(storeId, startDate, endDate));
-
-            // 4. 代金券数据统计(与优惠券类似,根据实际业务区分)
-            statistics.setVoucherData(getVoucherData(storeId, startDate, endDate));
-
-            // 5. 服务质量数据统计
-            statistics.setServiceQualityData(getServiceQualityData(storeId, startDate, endDate));
-
-            // 6. 价目表排名统计
-            statistics.setPriceListRanking(getPriceListRanking(storeId, startDate, endDate));
-
-            // 保存统计数据到历史表
-            saveStatisticsHistory(storeId, startTime, endTime, statistics);
-
+            
+            // 查询指定时间范围内的日统计数据
+            LambdaQueryWrapper<StoreTrackStatistics> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreTrackStatistics::getStoreId, storeId)
+                    .eq(StoreTrackStatistics::getStatType, STAT_TYPE_DAILY)
+                    .ge(StoreTrackStatistics::getStatDate, startDate)
+                    .le(StoreTrackStatistics::getStatDate, endDate);
+            
+            List<StoreTrackStatistics> statisticsList = storeTrackStatisticsMapper.selectList(wrapper);
+            
+            if (statisticsList == null || statisticsList.isEmpty()) {
+                log.warn("未查询到统计数据 - storeId={}, startTime={}, endTime={}", storeId, startTime, endTime);
+                return new StoreOperationalStatisticsVo();
+            }
+            
+            // 累加所有统计数据
+            return aggregateStatistics(statisticsList);
+            
         } catch (ParseException e) {
-            log.error("StoreOperationalStatisticsServiceImpl.getStatistics - 时间解析错误", e);
+            log.error("StoreOperationalStatisticsServiceImpl.calculateStatistics - 时间解析错误", e);
             throw new RuntimeException("时间格式错误: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("计算统计数据失败 - storeId={}, startTime={}, endTime={}, error={}", 
+                    storeId, startTime, endTime, e.getMessage(), e);
+            throw new RuntimeException("计算统计数据失败: " + e.getMessage());
         }
-
-        return statistics;
+    }
+    
+    /**
+     * 累加统计数据
+     */
+    private StoreOperationalStatisticsVo aggregateStatistics(List<StoreTrackStatistics> statisticsList) {
+        StoreOperationalStatisticsVo result = new StoreOperationalStatisticsVo();
+        
+        // 初始化累加器
+        Map<String, Long> trafficAccumulator = new HashMap<>();
+        Map<String, Long> interactionAccumulator = new HashMap<>();
+        Map<String, Object> couponAccumulator = new HashMap<>();
+        Map<String, Object> voucherAccumulator = new HashMap<>();
+        Map<String, Object> serviceAccumulator = new HashMap<>();
+        Map<Integer, Map<String, Long>> priceRankingAccumulator = new HashMap<>();
+        
+        // 用于计算平均值的计数器
+        int trafficCount = 0;
+        int serviceCount = 0;
+        
+        // 遍历所有统计数据并累加
+        for (StoreTrackStatistics stat : statisticsList) {
+            // 累加流量数据
+            if (stat.getTrafficData() != null && !stat.getTrafficData().isEmpty()) {
+                aggregateTrafficData(stat.getTrafficData(), trafficAccumulator);
+                trafficCount++;
+            }
+            
+            // 累加互动数据
+            if (stat.getInteractionData() != null && !stat.getInteractionData().isEmpty()) {
+                aggregateInteractionData(stat.getInteractionData(), interactionAccumulator);
+            }
+            
+            // 累加优惠券数据
+            if (stat.getCouponData() != null && !stat.getCouponData().isEmpty()) {
+                aggregateCouponData(stat.getCouponData(), couponAccumulator);
+            }
+            
+            // 累加代金券数据
+            if (stat.getVoucherData() != null && !stat.getVoucherData().isEmpty()) {
+                aggregateVoucherData(stat.getVoucherData(), voucherAccumulator);
+            }
+            
+            // 累加服务质量数据
+            if (stat.getServiceData() != null && !stat.getServiceData().isEmpty()) {
+                aggregateServiceData(stat.getServiceData(), serviceAccumulator);
+                serviceCount++;
+            }
+            
+            // 累加价目表排名数据
+            if (stat.getPriceRankingData() != null && !stat.getPriceRankingData().isEmpty()) {
+                aggregatePriceRankingData(stat.getPriceRankingData(), priceRankingAccumulator);
+            }
+        }
+        
+        // 转换为VO对象
+        result.setTrafficData(convertToTrafficDataVo(trafficAccumulator, trafficCount));
+        result.setInteractionData(convertToInteractionDataVo(interactionAccumulator));
+        result.setCouponData(convertToCouponDataVo(couponAccumulator));
+        result.setVoucherData(convertToVoucherDataVo(voucherAccumulator));
+        result.setServiceQualityData(convertToServiceQualityDataVo(serviceAccumulator, serviceCount));
+        result.setPriceListRanking(convertToPriceListRankingVo(priceRankingAccumulator));
+        
+        return result;
     }
 
     @Override
@@ -111,9 +177,9 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         comparison.setPreviousStartTime(previousStartTime);
         comparison.setPreviousEndTime(previousEndTime);
 
-        // 获取当期和上期的统计数据(getStatistics方法内部已经保存了历史记录)
-        StoreOperationalStatisticsVo currentStatistics = getStatistics(storeId, currentStartTime, currentEndTime);
-        StoreOperationalStatisticsVo previousStatistics = getStatistics(storeId, previousStartTime, previousEndTime);
+        // 获取当期和上期的统计数据
+        StoreOperationalStatisticsVo currentStatistics = calculateStatistics(storeId, currentStartTime, currentEndTime);
+        StoreOperationalStatisticsVo previousStatistics = calculateStatistics(storeId, previousStartTime, previousEndTime);
 
         // 构建对比数据
         comparison.setTrafficData(buildTrafficDataComparison(currentStatistics.getTrafficData(), previousStatistics.getTrafficData()));
@@ -121,36 +187,149 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         comparison.setCouponData(buildCouponDataComparison(currentStatistics.getCouponData(), previousStatistics.getCouponData()));
         comparison.setVoucherData(buildVoucherDataComparison(currentStatistics.getVoucherData(), previousStatistics.getVoucherData()));
         comparison.setServiceQualityData(buildServiceQualityDataComparison(currentStatistics.getServiceQualityData(), previousStatistics.getServiceQualityData()));
+        comparison.setPriceListRanking(buildPriceListRankingComparison(currentStatistics.getPriceListRanking(), previousStatistics.getPriceListRanking()));
+
+        // 不再在此处保存历史记录,历史记录只在 generateStatisticsComparisonPdf 接口中保存
 
         return comparison;
     }
 
+
     /**
-     * 保存统计数据到历史表
+     * 将StoreOperationalStatisticsVo转换为符合埋点统计数据JSON格式
      */
-    private void saveStatisticsHistory(Integer storeId, String startTime, String endTime, StoreOperationalStatisticsVo statistics) {
-        try {
-            SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
-            Date startDate = sdf.parse(startTime);
-            Date endDate = sdf.parse(endTime);
-
-            StoreOperationalStatisticsHistory history = new StoreOperationalStatisticsHistory();
-            history.setStoreId(storeId);
-            history.setStartTime(startDate);
-            history.setEndTime(endDate);
-            history.setQueryTime(new Date());
-            
-            // 将统计数据序列化为JSON字符串
-            String statisticsJson = JSON.toJSONString(statistics);
-            history.setStatisticsData(statisticsJson);
-
-            statisticsHistoryMapper.insert(history);
-            log.info("保存统计数据到历史表成功 - storeId={}, startTime={}, endTime={}", storeId, startTime, endTime);
-        } catch (Exception e) {
-            log.error("保存统计数据到历史表失败 - storeId={}, startTime={}, endTime={}, error={}", 
-                    storeId, startTime, endTime, e.getMessage(), e);
-            // 保存失败不影响主流程,只记录日志
+    private Map<String, Object> convertToTrackStatisticsFormat(StoreOperationalStatisticsVo statistics) {
+        Map<String, Object> result = new HashMap<>();
+        
+        // 1. 转换流量数据
+        if (statistics.getTrafficData() != null) {
+            StoreOperationalStatisticsVo.TrafficData traffic = statistics.getTrafficData();
+            Map<String, Object> trafficData = new HashMap<>();
+            trafficData.put("searchCount", traffic.getStoreSearchVolume() != null ? traffic.getStoreSearchVolume() : 0L);
+            trafficData.put("viewCount", traffic.getPageViews() != null ? traffic.getPageViews() : 0L);
+            trafficData.put("visitorCount", traffic.getVisitors() != null ? traffic.getVisitors() : 0L);
+            trafficData.put("newVisitorCount", traffic.getNewVisitors() != null ? traffic.getNewVisitors() : 0L);
+            // 访问时长从秒转换为毫秒
+            trafficData.put("totalDuration", traffic.getVisitDuration() != null ? traffic.getVisitDuration() * 1000 : 0L);
+            trafficData.put("avgDuration", traffic.getAvgVisitDuration() != null ? traffic.getAvgVisitDuration() * 1000 : 0L);
+            result.put("trafficData", trafficData);
+        }
+        
+        // 2. 转换互动数据
+        if (statistics.getInteractionData() != null) {
+            StoreOperationalStatisticsVo.InteractionData interaction = statistics.getInteractionData();
+            Map<String, Object> interactionData = new HashMap<>();
+            interactionData.put("collectCount", interaction.getStoreCollectionCount() != null ? interaction.getStoreCollectionCount() : 0L);
+            interactionData.put("shareCount", interaction.getStoreShareCount() != null ? interaction.getStoreShareCount() : 0L);
+            interactionData.put("checkinCount", interaction.getStoreCheckInCount() != null ? interaction.getStoreCheckInCount() : 0L);
+            interactionData.put("consultCount", interaction.getConsultMerchantCount() != null ? interaction.getConsultMerchantCount() : 0L);
+            interactionData.put("friendCount", interaction.getFriendsCount() != null ? interaction.getFriendsCount() : 0L);
+            interactionData.put("followCount", interaction.getFollowCount() != null ? interaction.getFollowCount() : 0L);
+            interactionData.put("fansCount", interaction.getFansCount() != null ? interaction.getFansCount() : 0L);
+            interactionData.put("postCount", interaction.getPostsPublishedCount() != null ? interaction.getPostsPublishedCount() : 0L);
+            interactionData.put("postLikeCount", interaction.getPostLikesCount() != null ? interaction.getPostLikesCount() : 0L);
+            interactionData.put("postCommentCount", interaction.getPostCommentsCount() != null ? interaction.getPostCommentsCount() : 0L);
+            interactionData.put("postRepostCount", interaction.getPostSharesCount() != null ? interaction.getPostSharesCount() : 0L);
+            interactionData.put("reportCount", interaction.getReportedCount() != null ? interaction.getReportedCount() : 0L);
+            interactionData.put("blockCount", interaction.getBlockedCount() != null ? interaction.getBlockedCount() : 0L);
+            result.put("interactionData", interactionData);
+        }
+        
+        // 3. 转换优惠券数据
+        if (statistics.getCouponData() != null) {
+            StoreOperationalStatisticsVo.CouponData coupon = statistics.getCouponData();
+            Map<String, Object> couponData = new HashMap<>();
+            couponData.put("giveToFriendCount", coupon.getGiftToFriendsCount() != null ? coupon.getGiftToFriendsCount() : 0L);
+            couponData.put("giveToFriendAmount", coupon.getGiftToFriendsAmount() != null ? coupon.getGiftToFriendsAmount() : BigDecimal.ZERO);
+            couponData.put("giveToFriendUseCount", coupon.getGiftToFriendsUsedCount() != null ? coupon.getGiftToFriendsUsedCount() : 0L);
+            couponData.put("giveToFriendUseAmount", coupon.getGiftToFriendsUsedAmount() != null ? coupon.getGiftToFriendsUsedAmount() : BigDecimal.ZERO);
+            // 百分比转换:BigDecimal -> Double
+            if (coupon.getGiftToFriendsUsedAmountRatio() != null) {
+                couponData.put("giveToFriendUseAmountPercent", coupon.getGiftToFriendsUsedAmountRatio().doubleValue());
+            } else {
+                couponData.put("giveToFriendUseAmountPercent", 0.0);
+            }
+            couponData.put("friendGiveCount", coupon.getFriendsGiftCount() != null ? coupon.getFriendsGiftCount() : 0L);
+            couponData.put("friendGiveAmount", coupon.getFriendsGiftAmount() != null ? coupon.getFriendsGiftAmount() : BigDecimal.ZERO);
+            couponData.put("friendGiveUseCount", coupon.getFriendsGiftUsedCount() != null ? coupon.getFriendsGiftUsedCount() : 0L);
+            couponData.put("friendGiveUseAmount", coupon.getFriendsGiftUsedAmount() != null ? coupon.getFriendsGiftUsedAmount() : BigDecimal.ZERO);
+            if (coupon.getFriendsGiftUsedAmountRatio() != null) {
+                couponData.put("friendGiveUseAmountPercent", coupon.getFriendsGiftUsedAmountRatio().doubleValue());
+            } else {
+                couponData.put("friendGiveUseAmountPercent", 0.0);
+            }
+            result.put("couponData", couponData);
+        }
+        
+        // 4. 转换代金券数据
+        if (statistics.getVoucherData() != null) {
+            StoreOperationalStatisticsVo.VoucherData voucher = statistics.getVoucherData();
+            Map<String, Object> voucherData = new HashMap<>();
+            voucherData.put("giveToFriendCount", voucher.getGiftToFriendsCount() != null ? voucher.getGiftToFriendsCount() : 0L);
+            voucherData.put("giveToFriendAmount", voucher.getGiftToFriendsAmount() != null ? voucher.getGiftToFriendsAmount() : BigDecimal.ZERO);
+            voucherData.put("giveToFriendUseCount", voucher.getGiftToFriendsUsedCount() != null ? voucher.getGiftToFriendsUsedCount() : 0L);
+            voucherData.put("giveToFriendUseAmount", voucher.getGiftToFriendsUsedAmount() != null ? voucher.getGiftToFriendsUsedAmount() : BigDecimal.ZERO);
+            if (voucher.getGiftToFriendsUsedAmountRatio() != null) {
+                voucherData.put("giveToFriendUseAmountPercent", voucher.getGiftToFriendsUsedAmountRatio().doubleValue());
+            } else {
+                voucherData.put("giveToFriendUseAmountPercent", 0.0);
+            }
+            voucherData.put("friendGiveCount", voucher.getFriendsGiftCount() != null ? voucher.getFriendsGiftCount() : 0L);
+            voucherData.put("friendGiveAmount", voucher.getFriendsGiftAmount() != null ? voucher.getFriendsGiftAmount() : BigDecimal.ZERO);
+            voucherData.put("friendGiveUseCount", voucher.getFriendsGiftUsedCount() != null ? voucher.getFriendsGiftUsedCount() : 0L);
+            voucherData.put("friendGiveUseAmount", voucher.getFriendsGiftUsedAmount() != null ? voucher.getFriendsGiftUsedAmount() : BigDecimal.ZERO);
+            if (voucher.getFriendsGiftUsedAmountRatio() != null) {
+                voucherData.put("friendGiveUseAmountPercent", voucher.getFriendsGiftUsedAmountRatio().doubleValue());
+            } else {
+                voucherData.put("friendGiveUseAmountPercent", 0.0);
+            }
+            result.put("voucherData", voucherData);
+        }
+        
+        // 5. 转换服务质量数据
+        if (statistics.getServiceQualityData() != null) {
+            StoreOperationalStatisticsVo.ServiceQualityData service = statistics.getServiceQualityData();
+            Map<String, Object> serviceData = new HashMap<>();
+            serviceData.put("storeScore", service.getStoreRating() != null ? service.getStoreRating().doubleValue() : 0.0);
+            serviceData.put("tasteScore", service.getTasteRating() != null ? service.getTasteRating().doubleValue() : 0.0);
+            serviceData.put("environmentScore", service.getEnvironmentRating() != null ? service.getEnvironmentRating().doubleValue() : 0.0);
+            serviceData.put("serviceScore", service.getServiceRating() != null ? service.getServiceRating().doubleValue() : 0.0);
+            serviceData.put("ratingCount", service.getTotalReviews() != null ? service.getTotalReviews() : 0L);
+            serviceData.put("goodRatingCount", service.getPositiveReviews() != null ? service.getPositiveReviews() : 0L);
+            serviceData.put("midRatingCount", service.getNeutralReviews() != null ? service.getNeutralReviews() : 0L);
+            serviceData.put("badRatingCount", service.getNegativeReviews() != null ? service.getNegativeReviews() : 0L);
+            if (service.getNegativeReviewRatio() != null) {
+                serviceData.put("badRatingPercent", service.getNegativeReviewRatio().doubleValue());
+            } else {
+                serviceData.put("badRatingPercent", 0.0);
+            }
+            serviceData.put("appealCount", service.getNegativeReviewAppealsCount() != null ? service.getNegativeReviewAppealsCount() : 0L);
+            serviceData.put("appealSuccessCount", service.getNegativeReviewAppealsSuccessCount() != null ? service.getNegativeReviewAppealsSuccessCount() : 0L);
+            if (service.getNegativeReviewAppealsSuccessRatio() != null) {
+                serviceData.put("appealSuccessPercent", service.getNegativeReviewAppealsSuccessRatio().doubleValue());
+            } else {
+                serviceData.put("appealSuccessPercent", 0.0);
+            }
+            result.put("serviceData", serviceData);
+        }
+        
+        // 6. 转换价目表排名数据
+        if (statistics.getPriceListRanking() != null && !statistics.getPriceListRanking().isEmpty()) {
+            List<Map<String, Object>> priceRankingData = new ArrayList<>();
+            for (StoreOperationalStatisticsVo.PriceListRanking ranking : statistics.getPriceListRanking()) {
+                Map<String, Object> priceItem = new HashMap<>();
+                priceItem.put("priceId", ranking.getPriceId() != null ? ranking.getPriceId() : 0);
+                priceItem.put("viewCount", ranking.getPageViews() != null ? ranking.getPageViews().intValue() : 0);
+                priceItem.put("visitorCount", ranking.getVisitors() != null ? ranking.getVisitors().intValue() : 0);
+                priceItem.put("shareCount", ranking.getShares() != null ? ranking.getShares().intValue() : 0);
+                priceRankingData.add(priceItem);
+            }
+            result.put("priceRankingData", priceRankingData);
+        } else {
+            result.put("priceRankingData", new ArrayList<>());
         }
+        
+        return result;
     }
 
     /**
@@ -439,6 +618,7 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
         for (StorePrice price : prices) {
             StoreOperationalStatisticsVo.PriceListRanking ranking = new StoreOperationalStatisticsVo.PriceListRanking();
             ranking.setRank(rank++);
+            ranking.setPriceId(price.getId());
             ranking.setPriceListItemName(price.getName());
             // TODO: 统计该价目表的浏览量、访客数、分享数
             ranking.setPageViews(0L);
@@ -456,6 +636,13 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private StoreOperationalStatisticsComparisonVo.TrafficDataComparison buildTrafficDataComparison(
             StoreOperationalStatisticsVo.TrafficData current, StoreOperationalStatisticsVo.TrafficData previous) {
         StoreOperationalStatisticsComparisonVo.TrafficDataComparison comparison = new StoreOperationalStatisticsComparisonVo.TrafficDataComparison();
+        // 如果 previous 为 null,创建空对象避免 NullPointerException
+        if (previous == null) {
+            previous = new StoreOperationalStatisticsVo.TrafficData();
+        }
+        if (current == null) {
+            current = new StoreOperationalStatisticsVo.TrafficData();
+        }
         comparison.setStoreSearchVolume(buildComparisonData(current.getStoreSearchVolume(), previous.getStoreSearchVolume()));
         comparison.setPageViews(buildComparisonData(current.getPageViews(), previous.getPageViews()));
         comparison.setVisitors(buildComparisonData(current.getVisitors(), previous.getVisitors()));
@@ -471,6 +658,13 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private StoreOperationalStatisticsComparisonVo.InteractionDataComparison buildInteractionDataComparison(
             StoreOperationalStatisticsVo.InteractionData current, StoreOperationalStatisticsVo.InteractionData previous) {
         StoreOperationalStatisticsComparisonVo.InteractionDataComparison comparison = new StoreOperationalStatisticsComparisonVo.InteractionDataComparison();
+        // 如果 previous 为 null,创建空对象避免 NullPointerException
+        if (previous == null) {
+            previous = new StoreOperationalStatisticsVo.InteractionData();
+        }
+        if (current == null) {
+            current = new StoreOperationalStatisticsVo.InteractionData();
+        }
         comparison.setStoreCollectionCount(buildComparisonData(current.getStoreCollectionCount(), previous.getStoreCollectionCount()));
         comparison.setStoreShareCount(buildComparisonData(current.getStoreShareCount(), previous.getStoreShareCount()));
         comparison.setStoreCheckInCount(buildComparisonData(current.getStoreCheckInCount(), previous.getStoreCheckInCount()));
@@ -493,6 +687,13 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private StoreOperationalStatisticsComparisonVo.CouponDataComparison buildCouponDataComparison(
             StoreOperationalStatisticsVo.CouponData current, StoreOperationalStatisticsVo.CouponData previous) {
         StoreOperationalStatisticsComparisonVo.CouponDataComparison comparison = new StoreOperationalStatisticsComparisonVo.CouponDataComparison();
+        // 如果 previous 为 null,创建空对象避免 NullPointerException
+        if (previous == null) {
+            previous = new StoreOperationalStatisticsVo.CouponData();
+        }
+        if (current == null) {
+            current = new StoreOperationalStatisticsVo.CouponData();
+        }
         comparison.setGiftToFriendsCount(buildComparisonData(current.getGiftToFriendsCount(), previous.getGiftToFriendsCount()));
         comparison.setGiftToFriendsAmount(buildComparisonData(current.getGiftToFriendsAmount(), previous.getGiftToFriendsAmount()));
         comparison.setGiftToFriendsUsedCount(buildComparisonData(current.getGiftToFriendsUsedCount(), previous.getGiftToFriendsUsedCount()));
@@ -512,6 +713,13 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private StoreOperationalStatisticsComparisonVo.VoucherDataComparison buildVoucherDataComparison(
             StoreOperationalStatisticsVo.VoucherData current, StoreOperationalStatisticsVo.VoucherData previous) {
         StoreOperationalStatisticsComparisonVo.VoucherDataComparison comparison = new StoreOperationalStatisticsComparisonVo.VoucherDataComparison();
+        // 如果 previous 为 null,创建空对象避免 NullPointerException
+        if (previous == null) {
+            previous = new StoreOperationalStatisticsVo.VoucherData();
+        }
+        if (current == null) {
+            current = new StoreOperationalStatisticsVo.VoucherData();
+        }
         comparison.setGiftToFriendsCount(buildComparisonData(current.getGiftToFriendsCount(), previous.getGiftToFriendsCount()));
         comparison.setGiftToFriendsAmount(buildComparisonData(current.getGiftToFriendsAmount(), previous.getGiftToFriendsAmount()));
         comparison.setGiftToFriendsUsedCount(buildComparisonData(current.getGiftToFriendsUsedCount(), previous.getGiftToFriendsUsedCount()));
@@ -531,6 +739,13 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     private StoreOperationalStatisticsComparisonVo.ServiceQualityDataComparison buildServiceQualityDataComparison(
             StoreOperationalStatisticsVo.ServiceQualityData current, StoreOperationalStatisticsVo.ServiceQualityData previous) {
         StoreOperationalStatisticsComparisonVo.ServiceQualityDataComparison comparison = new StoreOperationalStatisticsComparisonVo.ServiceQualityDataComparison();
+        // 如果 previous 为 null,创建空对象避免 NullPointerException
+        if (previous == null) {
+            previous = new StoreOperationalStatisticsVo.ServiceQualityData();
+        }
+        if (current == null) {
+            current = new StoreOperationalStatisticsVo.ServiceQualityData();
+        }
         comparison.setStoreRating(buildComparisonData(current.getStoreRating(), previous.getStoreRating()));
         comparison.setTasteRating(buildComparisonData(current.getTasteRating(), previous.getTasteRating()));
         comparison.setEnvironmentRating(buildComparisonData(current.getEnvironmentRating(), previous.getEnvironmentRating()));
@@ -547,6 +762,63 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     }
 
     /**
+     * 构建价目表排名数据对比
+     */
+    private List<StoreOperationalStatisticsComparisonVo.PriceListRankingComparison> buildPriceListRankingComparison(
+            List<StoreOperationalStatisticsVo.PriceListRanking> current, List<StoreOperationalStatisticsVo.PriceListRanking> previous) {
+        List<StoreOperationalStatisticsComparisonVo.PriceListRankingComparison> result = new ArrayList<>();
+        
+        // 如果当期数据为空,返回空列表
+        if (current == null || current.isEmpty()) {
+            return result;
+        }
+        
+        // 如果上期数据为空,创建空列表
+        if (previous == null) {
+            previous = new ArrayList<>();
+        }
+        
+        // 创建上期数据的Map,以priceId为key,方便查找
+        Map<Integer, StoreOperationalStatisticsVo.PriceListRanking> previousMap = new HashMap<>();
+        for (StoreOperationalStatisticsVo.PriceListRanking prev : previous) {
+            if (prev.getPriceId() != null) {
+                previousMap.put(prev.getPriceId(), prev);
+            }
+        }
+        
+        // 遍历当期数据,构建对比
+        for (StoreOperationalStatisticsVo.PriceListRanking curr : current) {
+            StoreOperationalStatisticsComparisonVo.PriceListRankingComparison comparison = 
+                new StoreOperationalStatisticsComparisonVo.PriceListRankingComparison();
+            
+            comparison.setPriceId(curr.getPriceId());
+            comparison.setPriceListItemName(curr.getPriceListItemName());
+            
+            // 获取上期对应的价目表数据
+            StoreOperationalStatisticsVo.PriceListRanking prev = previousMap.get(curr.getPriceId());
+            
+            // 构建浏览量对比
+            Long currentPageViews = curr.getPageViews() != null ? curr.getPageViews() : 0L;
+            Long previousPageViews = (prev != null && prev.getPageViews() != null) ? prev.getPageViews() : 0L;
+            comparison.setPageViews(buildComparisonData(currentPageViews, previousPageViews));
+            
+            // 构建访客数对比
+            Long currentVisitors = curr.getVisitors() != null ? curr.getVisitors() : 0L;
+            Long previousVisitors = (prev != null && prev.getVisitors() != null) ? prev.getVisitors() : 0L;
+            comparison.setVisitors(buildComparisonData(currentVisitors, previousVisitors));
+            
+            // 构建分享数对比
+            Long currentShares = curr.getShares() != null ? curr.getShares() : 0L;
+            Long previousShares = (prev != null && prev.getShares() != null) ? prev.getShares() : 0L;
+            comparison.setShares(buildComparisonData(currentShares, previousShares));
+            
+            result.add(comparison);
+        }
+        
+        return result;
+    }
+
+    /**
      * 构建对比数据
      */
     private StoreOperationalStatisticsComparisonVo.BaseComparisonData buildComparisonData(Object current, Object previous) {
@@ -607,6 +879,69 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
     }
 
     @Override
+    public StoreOperationalStatisticsVo getStatisticsInTrackFormat(Integer storeId, String startTime, String endTime) {
+        log.info("StoreOperationalStatisticsServiceImpl.getStatisticsInTrackFormat - storeId={}, startTime={}, endTime={}", 
+                storeId, startTime, endTime);
+        
+        // 计算统计数据
+        StoreOperationalStatisticsVo statistics = calculateStatistics(storeId, startTime, endTime);
+        
+        // 不再保存统计数据到历史表,只有对比接口才会保存历史数据
+        
+        return statistics;
+    }
+    
+    /**
+     * 保存对比统计数据到历史表
+     * @param storeId 店铺ID
+     * @param currentStartTime 当期开始时间
+     * @param currentEndTime 当期结束时间
+     * @param previousStartTime 上期开始时间
+     * @param previousEndTime 上期结束时间
+     * @param comparison 对比数据
+     * @param pdfUrl PDF文件URL(可选)
+     * @return 历史记录ID,保存失败返回null
+     */
+    private Integer saveStatisticsHistory(Integer storeId, String currentStartTime, String currentEndTime,
+                                          String previousStartTime, String previousEndTime,
+                                          StoreOperationalStatisticsComparisonVo comparison, String pdfUrl) {
+        try {
+            SimpleDateFormat sdf = new SimpleDateFormat(DATE_FORMAT);
+            Date startDate = sdf.parse(currentStartTime);
+            Date endDate = sdf.parse(currentEndTime);
+            Date previousStartDate = sdf.parse(previousStartTime);
+            Date previousEndDate = sdf.parse(previousEndTime);
+
+            StoreOperationalStatisticsHistory history = new StoreOperationalStatisticsHistory();
+            history.setStoreId(storeId);
+            history.setStartTime(startDate);
+            history.setEndTime(endDate);
+            history.setPreviousStartTime(previousStartDate);
+            history.setPreviousEndTime(previousEndDate);
+            history.setQueryTime(new Date());
+            
+            // 将对比统计数据序列化为JSON字符串(包含当期、上期和变化率等完整对比数据)
+            String statisticsJson = JSON.toJSONString(comparison);
+            history.setStatisticsData(statisticsJson);
+            
+            // 如果提供了PDF URL,则保存
+            if (pdfUrl != null && !pdfUrl.trim().isEmpty()) {
+                history.setPdfUrl(pdfUrl);
+            }
+
+            statisticsHistoryMapper.insert(history);
+            log.info("保存对比统计数据到历史表成功 - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}, historyId={}", 
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime, history.getId());
+            return history.getId();
+        } catch (Exception e) {
+            log.error("保存对比统计数据到历史表失败 - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}, error={}", 
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime, e.getMessage(), e);
+            // 保存失败不影响主流程,只记录日志
+            return null;
+        }
+    }
+
+    @Override
     public List<StoreOperationalStatisticsHistory> getHistoryList(Integer storeId) {
         log.info("StoreOperationalStatisticsServiceImpl.getHistoryList - storeId={}", storeId);
         
@@ -660,4 +995,547 @@ public class StoreOperationalStatisticsServiceImpl implements StoreOperationalSt
             return false;
         }
     }
+
+    @Override
+    public boolean updateHistoryPdfUrl(Integer historyId, String pdfUrl) {
+        log.info("StoreOperationalStatisticsServiceImpl.updateHistoryPdfUrl - historyId={}, pdfUrl={}", historyId, pdfUrl);
+        
+        if (historyId == null || historyId <= 0) {
+            log.warn("更新历史统计记录PDF URL失败,历史记录ID无效 - historyId={}", historyId);
+            return false;
+        }
+        
+        if (pdfUrl == null || pdfUrl.trim().isEmpty()) {
+            log.warn("更新历史统计记录PDF URL失败,PDF URL为空 - historyId={}", historyId);
+            return false;
+        }
+        
+        try {
+            // 查询历史记录是否存在且未删除
+            StoreOperationalStatisticsHistory history = statisticsHistoryMapper.selectOne(
+                    new LambdaQueryWrapper<StoreOperationalStatisticsHistory>()
+                            .eq(StoreOperationalStatisticsHistory::getId, historyId)
+                            .eq(StoreOperationalStatisticsHistory::getDeleteFlag, 0));
+            
+            if (history == null) {
+                log.warn("更新历史统计记录PDF URL失败,历史记录不存在或已删除 - historyId={}", historyId);
+                return false;
+            }
+            
+            // 更新PDF URL
+            LambdaUpdateWrapper<StoreOperationalStatisticsHistory> wrapper = new LambdaUpdateWrapper<>();
+            wrapper.eq(StoreOperationalStatisticsHistory::getId, historyId)
+                   .eq(StoreOperationalStatisticsHistory::getDeleteFlag, 0)
+                   .set(StoreOperationalStatisticsHistory::getPdfUrl, pdfUrl.trim());
+            
+            int result = statisticsHistoryMapper.update(null, wrapper);
+            if (result > 0) {
+                log.info("更新历史统计记录PDF URL成功 - historyId={}, pdfUrl={}", historyId, pdfUrl);
+                return true;
+            } else {
+                log.warn("更新历史统计记录PDF URL失败,未更新任何记录 - historyId={}", historyId);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("更新历史统计记录PDF URL失败 - historyId={}, pdfUrl={}, error={}", historyId, pdfUrl, e.getMessage(), e);
+            return false;
+        }
+    }
+    
+    // ==================== 累加和转换方法 ====================
+    
+    /**
+     * 累加流量数据
+     */
+    @SuppressWarnings("unchecked")
+    private void aggregateTrafficData(String trafficDataJson, Map<String, Long> accumulator) {
+        try {
+            Map<String, Object> data = (Map<String, Object>) JSON.parseObject(trafficDataJson, Map.class);
+            if (data == null) return;
+            
+            // 累加各个字段
+            accumulator.put("searchCount", accumulator.getOrDefault("searchCount", 0L) + 
+                    getLongValue(data, "searchCount"));
+            accumulator.put("viewCount", accumulator.getOrDefault("viewCount", 0L) + 
+                    getLongValue(data, "viewCount"));
+            accumulator.put("visitorCount", accumulator.getOrDefault("visitorCount", 0L) + 
+                    getLongValue(data, "visitorCount"));
+            accumulator.put("newVisitorCount", accumulator.getOrDefault("newVisitorCount", 0L) + 
+                    getLongValue(data, "newVisitorCount"));
+            accumulator.put("totalDuration", accumulator.getOrDefault("totalDuration", 0L) + 
+                    getLongValue(data, "totalDuration"));
+            accumulator.put("avgDuration", accumulator.getOrDefault("avgDuration", 0L) + 
+                    getLongValue(data, "avgDuration"));
+        } catch (Exception e) {
+            log.warn("解析流量数据失败: {}", trafficDataJson, e);
+        }
+    }
+    
+    /**
+     * 累加互动数据
+     */
+    @SuppressWarnings("unchecked")
+    private void aggregateInteractionData(String interactionDataJson, Map<String, Long> accumulator) {
+        try {
+            Map<String, Object> data = (Map<String, Object>) JSON.parseObject(interactionDataJson, Map.class);
+            if (data == null) return;
+            
+            accumulator.put("collectCount", accumulator.getOrDefault("collectCount", 0L) + 
+                    getLongValue(data, "collectCount"));
+            accumulator.put("shareCount", accumulator.getOrDefault("shareCount", 0L) + 
+                    getLongValue(data, "shareCount"));
+            accumulator.put("checkinCount", accumulator.getOrDefault("checkinCount", 0L) + 
+                    getLongValue(data, "checkinCount"));
+            accumulator.put("consultCount", accumulator.getOrDefault("consultCount", 0L) + 
+                    getLongValue(data, "consultCount"));
+            accumulator.put("friendCount", accumulator.getOrDefault("friendCount", 0L) + 
+                    getLongValue(data, "friendCount"));
+            accumulator.put("followCount", accumulator.getOrDefault("followCount", 0L) + 
+                    getLongValue(data, "followCount"));
+            accumulator.put("fansCount", accumulator.getOrDefault("fansCount", 0L) + 
+                    getLongValue(data, "fansCount"));
+            accumulator.put("postCount", accumulator.getOrDefault("postCount", 0L) + 
+                    getLongValue(data, "postCount"));
+            accumulator.put("postLikeCount", accumulator.getOrDefault("postLikeCount", 0L) + 
+                    getLongValue(data, "postLikeCount"));
+            accumulator.put("postCommentCount", accumulator.getOrDefault("postCommentCount", 0L) + 
+                    getLongValue(data, "postCommentCount"));
+            accumulator.put("postRepostCount", accumulator.getOrDefault("postRepostCount", 0L) + 
+                    getLongValue(data, "postRepostCount"));
+            accumulator.put("reportCount", accumulator.getOrDefault("reportCount", 0L) + 
+                    getLongValue(data, "reportCount"));
+            accumulator.put("blockCount", accumulator.getOrDefault("blockCount", 0L) + 
+                    getLongValue(data, "blockCount"));
+        } catch (Exception e) {
+            log.warn("解析互动数据失败: {}", interactionDataJson, e);
+        }
+    }
+    
+    /**
+     * 累加优惠券数据
+     */
+    @SuppressWarnings("unchecked")
+    private void aggregateCouponData(String couponDataJson, Map<String, Object> accumulator) {
+        try {
+            Map<String, Object> data = (Map<String, Object>) JSON.parseObject(couponDataJson, Map.class);
+            if (data == null) return;
+            
+            // 累加Long类型字段
+            accumulator.put("giveToFriendCount", getLongValue(accumulator, "giveToFriendCount") + 
+                    getLongValue(data, "giveToFriendCount"));
+            accumulator.put("giveToFriendUseCount", getLongValue(accumulator, "giveToFriendUseCount") + 
+                    getLongValue(data, "giveToFriendUseCount"));
+            accumulator.put("friendGiveCount", getLongValue(accumulator, "friendGiveCount") + 
+                    getLongValue(data, "friendGiveCount"));
+            accumulator.put("friendGiveUseCount", getLongValue(accumulator, "friendGiveUseCount") + 
+                    getLongValue(data, "friendGiveUseCount"));
+            
+            // 累加BigDecimal类型字段
+            accumulator.put("giveToFriendAmount", getBigDecimalValue(accumulator, "giveToFriendAmount")
+                    .add(getBigDecimalValue(data, "giveToFriendAmount")));
+            accumulator.put("giveToFriendUseAmount", getBigDecimalValue(accumulator, "giveToFriendUseAmount")
+                    .add(getBigDecimalValue(data, "giveToFriendUseAmount")));
+            accumulator.put("friendGiveAmount", getBigDecimalValue(accumulator, "friendGiveAmount")
+                    .add(getBigDecimalValue(data, "friendGiveAmount")));
+            accumulator.put("friendGiveUseAmount", getBigDecimalValue(accumulator, "friendGiveUseAmount")
+                    .add(getBigDecimalValue(data, "friendGiveUseAmount")));
+        } catch (Exception e) {
+            log.warn("解析优惠券数据失败: {}", couponDataJson, e);
+        }
+    }
+    
+    /**
+     * 累加代金券数据
+     */
+    @SuppressWarnings("unchecked")
+    private void aggregateVoucherData(String voucherDataJson, Map<String, Object> accumulator) {
+        try {
+            Map<String, Object> data = (Map<String, Object>) JSON.parseObject(voucherDataJson, Map.class);
+            if (data == null) return;
+            
+            // 累加Long类型字段
+            accumulator.put("giveToFriendCount", getLongValue(accumulator, "giveToFriendCount") + 
+                    getLongValue(data, "giveToFriendCount"));
+            accumulator.put("giveToFriendUseCount", getLongValue(accumulator, "giveToFriendUseCount") + 
+                    getLongValue(data, "giveToFriendUseCount"));
+            accumulator.put("friendGiveCount", getLongValue(accumulator, "friendGiveCount") + 
+                    getLongValue(data, "friendGiveCount"));
+            accumulator.put("friendGiveUseCount", getLongValue(accumulator, "friendGiveUseCount") + 
+                    getLongValue(data, "friendGiveUseCount"));
+            
+            // 累加BigDecimal类型字段
+            accumulator.put("giveToFriendAmount", getBigDecimalValue(accumulator, "giveToFriendAmount")
+                    .add(getBigDecimalValue(data, "giveToFriendAmount")));
+            accumulator.put("giveToFriendUseAmount", getBigDecimalValue(accumulator, "giveToFriendUseAmount")
+                    .add(getBigDecimalValue(data, "giveToFriendUseAmount")));
+            accumulator.put("friendGiveAmount", getBigDecimalValue(accumulator, "friendGiveAmount")
+                    .add(getBigDecimalValue(data, "friendGiveAmount")));
+            accumulator.put("friendGiveUseAmount", getBigDecimalValue(accumulator, "friendGiveUseAmount")
+                    .add(getBigDecimalValue(data, "friendGiveUseAmount")));
+        } catch (Exception e) {
+            log.warn("解析代金券数据失败: {}", voucherDataJson, e);
+        }
+    }
+    
+    /**
+     * 累加服务质量数据
+     */
+    @SuppressWarnings("unchecked")
+    private void aggregateServiceData(String serviceDataJson, Map<String, Object> accumulator) {
+        try {
+            Map<String, Object> data = (Map<String, Object>) JSON.parseObject(serviceDataJson, Map.class);
+            if (data == null) return;
+            
+            // 累加评分(需要计算平均值)
+            accumulator.put("storeScoreSum", getBigDecimalValue(accumulator, "storeScoreSum")
+                    .add(getBigDecimalValue(data, "storeScore")));
+            accumulator.put("tasteScoreSum", getBigDecimalValue(accumulator, "tasteScoreSum")
+                    .add(getBigDecimalValue(data, "tasteScore")));
+            accumulator.put("environmentScoreSum", getBigDecimalValue(accumulator, "environmentScoreSum")
+                    .add(getBigDecimalValue(data, "environmentScore")));
+            accumulator.put("serviceScoreSum", getBigDecimalValue(accumulator, "serviceScoreSum")
+                    .add(getBigDecimalValue(data, "serviceScore")));
+            
+            // 累加Long类型字段
+            accumulator.put("ratingCount", getLongValue(accumulator, "ratingCount") + 
+                    getLongValue(data, "ratingCount"));
+            accumulator.put("goodRatingCount", getLongValue(accumulator, "goodRatingCount") + 
+                    getLongValue(data, "goodRatingCount"));
+            accumulator.put("midRatingCount", getLongValue(accumulator, "midRatingCount") + 
+                    getLongValue(data, "midRatingCount"));
+            accumulator.put("badRatingCount", getLongValue(accumulator, "badRatingCount") + 
+                    getLongValue(data, "badRatingCount"));
+            accumulator.put("appealCount", getLongValue(accumulator, "appealCount") + 
+                    getLongValue(data, "appealCount"));
+            accumulator.put("appealSuccessCount", getLongValue(accumulator, "appealSuccessCount") + 
+                    getLongValue(data, "appealSuccessCount"));
+        } catch (Exception e) {
+            log.warn("解析服务质量数据失败: {}", serviceDataJson, e);
+        }
+    }
+    
+    /**
+     * 累加价目表排名数据
+     */
+    @SuppressWarnings("unchecked")
+    private void aggregatePriceRankingData(String priceRankingDataJson, Map<Integer, Map<String, Long>> accumulator) {
+        try {
+            List<Map<String, Object>> dataList = (List<Map<String, Object>>) (List<?>) JSON.parseArray(priceRankingDataJson);
+            if (dataList == null) return;
+            
+            for (Map<String, Object> item : dataList) {
+                Integer priceId = getIntegerValue(item, "priceId");
+                if (priceId == null) continue;
+                
+                Map<String, Long> priceData = accumulator.computeIfAbsent(priceId, k -> new HashMap<>());
+                priceData.put("viewCount", priceData.getOrDefault("viewCount", 0L) + 
+                        getLongValue(item, "viewCount"));
+                priceData.put("visitorCount", priceData.getOrDefault("visitorCount", 0L) + 
+                        getLongValue(item, "visitorCount"));
+                priceData.put("shareCount", priceData.getOrDefault("shareCount", 0L) + 
+                        getLongValue(item, "shareCount"));
+            }
+        } catch (Exception e) {
+            log.warn("解析价目表排名数据失败: {}", priceRankingDataJson, e);
+        }
+    }
+    
+    /**
+     * 转换为流量数据VO
+     */
+    private StoreOperationalStatisticsVo.TrafficData convertToTrafficDataVo(Map<String, Long> accumulator, int count) {
+        StoreOperationalStatisticsVo.TrafficData vo = new StoreOperationalStatisticsVo.TrafficData();
+        vo.setStoreSearchVolume(accumulator.getOrDefault("searchCount", 0L));
+        vo.setPageViews(accumulator.getOrDefault("viewCount", 0L));
+        vo.setVisitors(accumulator.getOrDefault("visitorCount", 0L));
+        vo.setNewVisitors(accumulator.getOrDefault("newVisitorCount", 0L));
+        
+        // 访问时长转换为秒(原数据是毫秒)
+        Long totalDuration = accumulator.getOrDefault("totalDuration", 0L);
+        vo.setVisitDuration(totalDuration / 1000);
+        
+        // 平均访问时长:如果有多个统计记录,需要重新计算平均值
+        Long avgDuration = accumulator.getOrDefault("avgDuration", 0L);
+        if (count > 0 && avgDuration > 0) {
+            vo.setAvgVisitDuration(avgDuration / 1000); // 转换为秒
+        } else {
+            vo.setAvgVisitDuration(0L);
+        }
+        
+        return vo;
+    }
+    
+    /**
+     * 转换为互动数据VO
+     */
+    private StoreOperationalStatisticsVo.InteractionData convertToInteractionDataVo(Map<String, Long> accumulator) {
+        StoreOperationalStatisticsVo.InteractionData vo = new StoreOperationalStatisticsVo.InteractionData();
+        vo.setStoreCollectionCount(accumulator.getOrDefault("collectCount", 0L));
+        vo.setStoreShareCount(accumulator.getOrDefault("shareCount", 0L));
+        vo.setStoreCheckInCount(accumulator.getOrDefault("checkinCount", 0L));
+        vo.setConsultMerchantCount(accumulator.getOrDefault("consultCount", 0L));
+        vo.setFriendsCount(accumulator.getOrDefault("friendCount", 0L));
+        vo.setFollowCount(accumulator.getOrDefault("followCount", 0L));
+        vo.setFansCount(accumulator.getOrDefault("fansCount", 0L));
+        vo.setPostsPublishedCount(accumulator.getOrDefault("postCount", 0L));
+        vo.setPostLikesCount(accumulator.getOrDefault("postLikeCount", 0L));
+        vo.setPostCommentsCount(accumulator.getOrDefault("postCommentCount", 0L));
+        vo.setPostSharesCount(accumulator.getOrDefault("postRepostCount", 0L));
+        vo.setReportedCount(accumulator.getOrDefault("reportCount", 0L));
+        vo.setBlockedCount(accumulator.getOrDefault("blockCount", 0L));
+        return vo;
+    }
+    
+    /**
+     * 转换为优惠券数据VO
+     */
+    private StoreOperationalStatisticsVo.CouponData convertToCouponDataVo(Map<String, Object> accumulator) {
+        StoreOperationalStatisticsVo.CouponData vo = new StoreOperationalStatisticsVo.CouponData();
+        vo.setGiftToFriendsCount(getLongValue(accumulator, "giveToFriendCount"));
+        vo.setGiftToFriendsAmount(getBigDecimalValue(accumulator, "giveToFriendAmount"));
+        vo.setGiftToFriendsUsedCount(getLongValue(accumulator, "giveToFriendUseCount"));
+        vo.setGiftToFriendsUsedAmount(getBigDecimalValue(accumulator, "giveToFriendUseAmount"));
+        
+        // 计算使用金额占比
+        BigDecimal giftAmount = getBigDecimalValue(accumulator, "giveToFriendAmount");
+        BigDecimal giftUsedAmount = getBigDecimalValue(accumulator, "giveToFriendUseAmount");
+        if (giftAmount.compareTo(BigDecimal.ZERO) > 0) {
+            vo.setGiftToFriendsUsedAmountRatio(giftUsedAmount.divide(giftAmount, 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP));
+        } else {
+            vo.setGiftToFriendsUsedAmountRatio(BigDecimal.ZERO);
+        }
+        
+        vo.setFriendsGiftCount(getLongValue(accumulator, "friendGiveCount"));
+        vo.setFriendsGiftAmount(getBigDecimalValue(accumulator, "friendGiveAmount"));
+        vo.setFriendsGiftUsedCount(getLongValue(accumulator, "friendGiveUseCount"));
+        vo.setFriendsGiftUsedAmount(getBigDecimalValue(accumulator, "friendGiveUseAmount"));
+        
+        // 计算好友赠送使用金额占比
+        BigDecimal friendAmount = getBigDecimalValue(accumulator, "friendGiveAmount");
+        BigDecimal friendUsedAmount = getBigDecimalValue(accumulator, "friendGiveUseAmount");
+        if (friendAmount.compareTo(BigDecimal.ZERO) > 0) {
+            vo.setFriendsGiftUsedAmountRatio(friendUsedAmount.divide(friendAmount, 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP));
+        } else {
+            vo.setFriendsGiftUsedAmountRatio(BigDecimal.ZERO);
+        }
+        
+        return vo;
+    }
+    
+    /**
+     * 转换为代金券数据VO
+     */
+    private StoreOperationalStatisticsVo.VoucherData convertToVoucherDataVo(Map<String, Object> accumulator) {
+        StoreOperationalStatisticsVo.VoucherData vo = new StoreOperationalStatisticsVo.VoucherData();
+        vo.setGiftToFriendsCount(getLongValue(accumulator, "giveToFriendCount"));
+        vo.setGiftToFriendsAmount(getBigDecimalValue(accumulator, "giveToFriendAmount"));
+        vo.setGiftToFriendsUsedCount(getLongValue(accumulator, "giveToFriendUseCount"));
+        vo.setGiftToFriendsUsedAmount(getBigDecimalValue(accumulator, "giveToFriendUseAmount"));
+        
+        // 计算使用金额占比
+        BigDecimal giftAmount = getBigDecimalValue(accumulator, "giveToFriendAmount");
+        BigDecimal giftUsedAmount = getBigDecimalValue(accumulator, "giveToFriendUseAmount");
+        if (giftAmount.compareTo(BigDecimal.ZERO) > 0) {
+            vo.setGiftToFriendsUsedAmountRatio(giftUsedAmount.divide(giftAmount, 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP));
+        } else {
+            vo.setGiftToFriendsUsedAmountRatio(BigDecimal.ZERO);
+        }
+        
+        vo.setFriendsGiftCount(getLongValue(accumulator, "friendGiveCount"));
+        vo.setFriendsGiftAmount(getBigDecimalValue(accumulator, "friendGiveAmount"));
+        vo.setFriendsGiftUsedCount(getLongValue(accumulator, "friendGiveUseCount"));
+        vo.setFriendsGiftUsedAmount(getBigDecimalValue(accumulator, "friendGiveUseAmount"));
+        
+        // 计算好友赠送使用金额占比
+        BigDecimal friendAmount = getBigDecimalValue(accumulator, "friendGiveAmount");
+        BigDecimal friendUsedAmount = getBigDecimalValue(accumulator, "friendGiveUseAmount");
+        if (friendAmount.compareTo(BigDecimal.ZERO) > 0) {
+            vo.setFriendsGiftUsedAmountRatio(friendUsedAmount.divide(friendAmount, 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP));
+        } else {
+            vo.setFriendsGiftUsedAmountRatio(BigDecimal.ZERO);
+        }
+        
+        return vo;
+    }
+    
+    /**
+     * 转换为服务质量数据VO
+     */
+    private StoreOperationalStatisticsVo.ServiceQualityData convertToServiceQualityDataVo(Map<String, Object> accumulator, int count) {
+        StoreOperationalStatisticsVo.ServiceQualityData vo = new StoreOperationalStatisticsVo.ServiceQualityData();
+        
+        // 计算平均评分
+        if (count > 0) {
+            BigDecimal storeScoreSum = getBigDecimalValue(accumulator, "storeScoreSum");
+            BigDecimal tasteScoreSum = getBigDecimalValue(accumulator, "tasteScoreSum");
+            BigDecimal environmentScoreSum = getBigDecimalValue(accumulator, "environmentScoreSum");
+            BigDecimal serviceScoreSum = getBigDecimalValue(accumulator, "serviceScoreSum");
+            
+            vo.setStoreRating(storeScoreSum.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP));
+            vo.setTasteRating(tasteScoreSum.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP));
+            vo.setEnvironmentRating(environmentScoreSum.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP));
+            vo.setServiceRating(serviceScoreSum.divide(BigDecimal.valueOf(count), 2, RoundingMode.HALF_UP));
+        } else {
+            vo.setStoreRating(BigDecimal.ZERO);
+            vo.setTasteRating(BigDecimal.ZERO);
+            vo.setEnvironmentRating(BigDecimal.ZERO);
+            vo.setServiceRating(BigDecimal.ZERO);
+        }
+        
+        vo.setTotalReviews(getLongValue(accumulator, "ratingCount"));
+        vo.setPositiveReviews(getLongValue(accumulator, "goodRatingCount"));
+        vo.setNeutralReviews(getLongValue(accumulator, "midRatingCount"));
+        vo.setNegativeReviews(getLongValue(accumulator, "badRatingCount"));
+        
+        // 计算差评占比
+        Long ratingCount = getLongValue(accumulator, "ratingCount");
+        Long badRatingCount = getLongValue(accumulator, "badRatingCount");
+        if (ratingCount > 0) {
+            vo.setNegativeReviewRatio(BigDecimal.valueOf(badRatingCount)
+                    .divide(BigDecimal.valueOf(ratingCount), 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP));
+        } else {
+            vo.setNegativeReviewRatio(BigDecimal.ZERO);
+        }
+        
+        vo.setNegativeReviewAppealsCount(getLongValue(accumulator, "appealCount"));
+        vo.setNegativeReviewAppealsSuccessCount(getLongValue(accumulator, "appealSuccessCount"));
+        
+        // 计算差评申诉成功占比
+        Long appealCount = getLongValue(accumulator, "appealCount");
+        Long appealSuccessCount = getLongValue(accumulator, "appealSuccessCount");
+        if (appealCount > 0) {
+            vo.setNegativeReviewAppealsSuccessRatio(BigDecimal.valueOf(appealSuccessCount)
+                    .divide(BigDecimal.valueOf(appealCount), 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100)).setScale(2, RoundingMode.HALF_UP));
+        } else {
+            vo.setNegativeReviewAppealsSuccessRatio(BigDecimal.ZERO);
+        }
+        
+        return vo;
+    }
+    
+    /**
+     * 转换为价目表排名数据VO
+     */
+    private List<StoreOperationalStatisticsVo.PriceListRanking> convertToPriceListRankingVo(Map<Integer, Map<String, Long>> accumulator) {
+        List<StoreOperationalStatisticsVo.PriceListRanking> result = new ArrayList<>();
+        
+        // 转换为列表并按浏览量降序排序
+        for (Map.Entry<Integer, Map<String, Long>> entry : accumulator.entrySet()) {
+            StoreOperationalStatisticsVo.PriceListRanking ranking = new StoreOperationalStatisticsVo.PriceListRanking();
+            ranking.setPriceId(entry.getKey());
+            Map<String, Long> data = entry.getValue();
+            ranking.setPageViews(data.getOrDefault("viewCount", 0L));
+            ranking.setVisitors(data.getOrDefault("visitorCount", 0L));
+            ranking.setShares(data.getOrDefault("shareCount", 0L));
+            
+            // 获取价目表名称(如果需要)
+            // ranking.setPriceListItemName(...);
+            
+            result.add(ranking);
+        }
+        
+        // 按浏览量降序排序
+        result.sort((a, b) -> Long.compare(b.getPageViews(), a.getPageViews()));
+        
+        // 设置排名
+        for (int i = 0; i < result.size(); i++) {
+            result.get(i).setRank(i + 1);
+        }
+        
+        return result;
+    }
+    
+    // ==================== 工具方法 ====================
+    
+    /**
+     * 从Map中获取Long值
+     */
+    private Long getLongValue(Map<String, Object> map, String key) {
+        Object value = map.get(key);
+        if (value == null) return 0L;
+        if (value instanceof Long) return (Long) value;
+        if (value instanceof Number) return ((Number) value).longValue();
+        try {
+            return Long.parseLong(value.toString());
+        } catch (Exception e) {
+            return 0L;
+        }
+    }
+    
+    /**
+     * 从Map中获取Integer值
+     */
+    private Integer getIntegerValue(Map<String, Object> map, String key) {
+        Object value = map.get(key);
+        if (value == null) return null;
+        if (value instanceof Integer) return (Integer) value;
+        if (value instanceof Number) return ((Number) value).intValue();
+        try {
+            return Integer.parseInt(value.toString());
+        } catch (Exception e) {
+            return null;
+        }
+    }
+    
+    /**
+     * 从Map中获取BigDecimal值
+     */
+    private BigDecimal getBigDecimalValue(Map<String, Object> map, String key) {
+        Object value = map.get(key);
+        if (value == null) return BigDecimal.ZERO;
+        if (value instanceof BigDecimal) return (BigDecimal) value;
+        if (value instanceof Number) return BigDecimal.valueOf(((Number) value).doubleValue());
+        try {
+            return new BigDecimal(value.toString());
+        } catch (Exception e) {
+            return BigDecimal.ZERO;
+        }
+    }
+
+    @Override
+    public String generateStatisticsComparisonPdf(Integer storeId, String currentStartTime, String currentEndTime,
+                                                   String previousStartTime, String previousEndTime) {
+        log.info("StoreOperationalStatisticsServiceImpl.generateStatisticsComparisonPdf - storeId={}, currentStartTime={}, currentEndTime={}, previousStartTime={}, previousEndTime={}",
+                storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+        
+        try {
+            // 1. 获取统计数据对比
+            StoreOperationalStatisticsComparisonVo comparison = getStatisticsComparison(
+                    storeId, currentStartTime, currentEndTime, previousStartTime, previousEndTime);
+            
+            // 2. 生成图片
+            byte[] imageBytes = StatisticsComparisonImageUtil.generateImage(comparison);
+            
+            // 3. 将图片转换为PDF
+            java.io.ByteArrayInputStream imageInputStream = new java.io.ByteArrayInputStream(imageBytes);
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfBytes(imageInputStream);
+            
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                throw new RuntimeException("图片转PDF失败");
+            }
+            
+            // 4. 上传PDF到OSS
+            String ossFilePath = "statistics/comparison_" + storeId + "_" + RandomCreateUtil.getRandomNum(8) + ".pdf";
+            java.io.ByteArrayInputStream pdfInputStream = new java.io.ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+            
+            log.info("统计数据对比图片转PDF并上传成功 - storeId={}, pdfUrl={}", storeId, pdfUrl);
+            
+            // 5. 保存对比数据到历史表(包含所有入参和PDF URL)
+            saveStatisticsHistory(storeId, currentStartTime, currentEndTime, 
+                    previousStartTime, previousEndTime, comparison, pdfUrl);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("生成统计数据对比图片并转PDF上传失败 - storeId={}, error={}", storeId, e.getMessage(), e);
+            throw new RuntimeException("生成图片并转PDF上传失败: " + e.getMessage(), e);
+        }
+    }
+
 }

+ 33 - 3
alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java

@@ -285,7 +285,7 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
      * @param attachmentUrls 原始URL列表
      * @return 分类后的Map
      */
-    private static Map<String, List<String>> classifyUrls(List<String> attachmentUrls) {
+    public static Map<String, List<String>> classifyUrls(List<String> attachmentUrls) {
         // 初始化分类容器
         List<String> imageUrls = new ArrayList<>();
         List<String> videoUrls = new ArrayList<>();
@@ -325,7 +325,7 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
     /**
      * 构建分类结果Map
      */
-    private static Map<String, List<String>> buildResultMap(List<String> imageUrls, List<String> videoUrls, List<String> otherUrls) {
+    public static Map<String, List<String>> buildResultMap(List<String> imageUrls, List<String> videoUrls, List<String> otherUrls) {
         Map<String, List<String>> resultMap = new HashMap<>(3);
         resultMap.put("image", Collections.unmodifiableList(imageUrls)); // 返回不可修改列表,避免外部篡改
         resultMap.put("video", Collections.unmodifiableList(videoUrls));
@@ -419,6 +419,26 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
             dto.setAttachmentUrls(new ArrayList<>());
         }
 
+        // 填充商铺信息和商户头像
+        if (requirement.getStoreId() != null) {
+            // 查询商铺信息
+            StoreInfo storeInfo = storeInfoMapper.selectById(requirement.getStoreId());
+            if (storeInfo != null) {
+                dto.setStoreName(storeInfo.getStoreName());
+                dto.setStoreTel(storeInfo.getStoreTel());
+                dto.setStoreAddress(storeInfo.getStoreAddress());
+                dto.setStoreBlurb(storeInfo.getStoreBlurb());
+            }
+
+            // 查询商户头像(从store_user表获取head_img)
+            StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>()
+                    .eq(StoreUser::getStoreId, requirement.getStoreId())
+                    .eq(StoreUser::getDeleteFlag, 0));
+            if (storeUser != null && StringUtils.hasText(storeUser.getHeadImg())) {
+                dto.setStoreAvatar(storeUser.getHeadImg());
+            }
+        }
+
         return dto;
     }
 
@@ -503,7 +523,16 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
                 Map<Integer, StoreInfo> storeInfoMap = storeInfoList.stream()
                         .collect(Collectors.toMap(StoreInfo::getId, storeInfo -> storeInfo, (v1, v2) -> v1));
 
-                // 填充商铺信息到DTO
+                // 批量查询商户头像(store_user.head_img)
+                LambdaQueryWrapper<StoreUser> userWrapper = new LambdaQueryWrapper<StoreUser>()
+                        .in(StoreUser::getStoreId, storeIds)
+                        .eq(StoreUser::getDeleteFlag, 0);
+                List<StoreUser> storeUserList = storeUserMapper.selectList(userWrapper);
+                Map<Integer, String> storeAvatarMap = storeUserList.stream()
+                        .filter(u -> u.getStoreId() != null && u.getHeadImg() != null)
+                        .collect(Collectors.toMap(StoreUser::getStoreId, StoreUser::getHeadImg, (v1, v2) -> v1));
+
+                // 填充商铺信息(含头像)到DTO
                 dtoPage.getRecords().forEach(dto -> {
                     StoreInfo storeInfo = storeInfoMap.get(dto.getStoreId());
                     if (storeInfo != null) {
@@ -512,6 +541,7 @@ public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreReno
                         dto.setStoreAddress(storeInfo.getStoreAddress());
                         dto.setStoreBlurb(storeInfo.getStoreBlurb());
                     }
+                    dto.setStoreAvatar(storeAvatarMap.get(dto.getStoreId()));
                 });
             }
 

+ 8 - 2
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffCommentServiceImpl.java

@@ -20,6 +20,7 @@ import shop.alien.mapper.LifeLikeRecordMapper;
 import shop.alien.mapper.StoreStaffCommentMapper;
 import shop.alien.store.service.StoreStaffCommentService;
 import shop.alien.store.service.StoreStaffReviewService;
+import shop.alien.store.util.ai.AiContentModerationUtil;
 
 import java.util.Date;
 import java.util.List;
@@ -43,13 +44,15 @@ public class StoreStaffCommentServiceImpl extends ServiceImpl<StoreStaffCommentM
     private final StoreStaffCommentMapper storeStaffCommentMapper;
     private final StoreStaffReviewService storeStaffReviewService;
     private final LifeLikeRecordMapper lifeLikeRecordMapper;
+    private final AiContentModerationUtil aiContentModerationUtil;
 
     public StoreStaffCommentServiceImpl(StoreStaffCommentMapper storeStaffCommentMapper,
                                         @Lazy StoreStaffReviewService storeStaffReviewService,
-                                        LifeLikeRecordMapper lifeLikeRecordMapper) {
+                                        LifeLikeRecordMapper lifeLikeRecordMapper, AiContentModerationUtil aiContentModerationUtil) {
         this.storeStaffCommentMapper = storeStaffCommentMapper;
         this.storeStaffReviewService = storeStaffReviewService;
         this.lifeLikeRecordMapper = lifeLikeRecordMapper;
+        this.aiContentModerationUtil = aiContentModerationUtil;
     }
 
     @Override
@@ -70,7 +73,10 @@ public class StoreStaffCommentServiceImpl extends ServiceImpl<StoreStaffCommentM
 
         // 设置评论属性
         setCommentDefaults(comment, review);
-
+        AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(comment.getCommentContent(), null);
+        if (!auditResult.isPassed()) {
+            return R.fail(auditResult.getFailureReason());
+        }
         boolean success = this.save(comment);
         if (success) {
             // 更新评价的评论数

+ 73 - 16
alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffReviewServiceImpl.java

@@ -9,33 +9,28 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import shop.alien.entity.result.R;
-import shop.alien.entity.store.LifeLikeRecord;
-import shop.alien.entity.store.StoreStaffComment;
-import shop.alien.entity.store.StoreStaffReview;
+import shop.alien.entity.store.*;
 import shop.alien.entity.store.dto.StoreStaffReviewDto;
-import shop.alien.entity.store.vo.StoreStaffCommentVo;
-import shop.alien.entity.store.vo.StoreStaffReviewDetailVo;
-import shop.alien.entity.store.vo.StoreStaffReviewListWithStaffVo;
-import shop.alien.entity.store.vo.StoreStaffReviewStatisticsVo;
-import shop.alien.entity.store.vo.StoreStaffReviewVo;
-import shop.alien.entity.store.StoreStaffConfig;
+import shop.alien.entity.store.vo.*;
 import shop.alien.mapper.LifeLikeRecordMapper;
 import shop.alien.mapper.StoreStaffCommentMapper;
 import shop.alien.mapper.StoreStaffConfigMapper;
 import shop.alien.mapper.StoreStaffReviewMapper;
 import shop.alien.store.service.StoreStaffCommentService;
 import shop.alien.store.service.StoreStaffReviewService;
+import shop.alien.store.util.ai.AiContentModerationUtil;
+import shop.alien.store.util.ai.AiVideoModerationUtil;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-
-import java.util.ArrayList;
-import java.util.Date;
-import java.util.List;
+import java.util.*;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
 
 /**
  * 员工评价 服务实现类
@@ -56,6 +51,10 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
     private final StoreStaffCommentMapper storeStaffCommentMapper;
     private final LifeLikeRecordMapper lifeLikeRecordMapper;
     private final StoreStaffConfigMapper storeStaffConfigMapper;
+    private final AiContentModerationUtil aiContentModerationUtil;
+    @Qualifier("commonVideoTaskExecutor")
+    private final ExecutorService commonVideoTaskExecutor;
+    private final AiVideoModerationUtil aiVideoModerationUtil;
 
     @Override
     public R<StoreStaffReview> createReview(StoreStaffReviewDto reviewDto) {
@@ -70,11 +69,69 @@ public class StoreStaffReviewServiceImpl extends ServiceImpl<StoreStaffReviewMap
         // 创建评价
         StoreStaffReview review = buildReviewFromDto(reviewDto);
         boolean success = this.save(review);
-        
+        // AI审核
+        // 一次遍历完成分类,避免多次流式处理
+        Map<String, List<String>> urlCategoryMap = StoreRenovationRequirementServiceImpl.classifyUrls(reviewDto.getReviewImages());
+        AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(reviewDto.getReviewContent(), urlCategoryMap.get("image"));
+        if (!auditResult.isPassed()) {
+            // 审核不通过
+            StoreStaffReview staffReview = this.getById(review.getId());
+            staffReview.setAuditStatus(2);
+            staffReview.setAuditReason(auditResult.getFailureReason());
+            this.saveOrUpdate(staffReview);
+        } else{
+            CompletableFuture.runAsync(() -> {
+                AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
+                try {
+                    // 调用审核接口,增加超时控制(避免接口挂死)
+                    videoAuditResult = CompletableFuture.supplyAsync(
+                            () -> aiVideoModerationUtil.auditVideos(urlCategoryMap.get("video")),
+                            commonVideoTaskExecutor
+                    ).get();
+
+                    // 审核不通过则更新状态和原因
+                    if (Objects.nonNull(videoAuditResult) && !videoAuditResult.isPassed()) {
+                        // 重新查询最新的rating,避免并发覆盖
+                        StoreStaffReview staffReview  = this.getById(review.getId());
+                        if (Objects.isNull(review)) {
+                            log.error("视频审核后更新失败,rating,ID:{}", review.getId());
+                            return;
+                        }
+                        staffReview.setAuditStatus(2);
+                        // 失败原因
+                        staffReview.setAuditReason(videoAuditResult.getFailureReason());
+                        this.saveOrUpdate(staffReview);
+                        log.info("视频审核不通过,已更新状态,reviewID:{},原因:{}",
+                                staffReview.getId(), videoAuditResult.getFailureReason());
+                    } else if (Objects.nonNull(videoAuditResult) && videoAuditResult.isPassed()) {
+                        // 审核通过也更新状态(可选,根据业务需求)
+                        StoreStaffReview staffReview = this.getById(review.getId());
+                        if (Objects.nonNull(staffReview)) {
+                            staffReview.setAuditStatus(1);
+                        // latestRequirement.setAuditReason(null);
+                            this.saveOrUpdate(staffReview);
+                            // 更新员工评分
+                            updateStaffServiceScore(review.getStaffUserId());
+                            log.info("视频审核通过,已更新状态,reviewID:{}", staffReview.getId());
+                        }
+                    }
+                } catch (Exception e) {
+                    log.error("视频审核接口调用异常,reviewID:{}", review.getId(), e);
+                    StoreStaffReview staffReview = this.getById(review.getId());
+                    if (Objects.nonNull(staffReview)) {
+                        staffReview.setAuditStatus(2);
+                        staffReview.setAuditReason("视频审核接口调用异常:" + e.getMessage()); // 实际需捕获具体异常信息
+                        this.saveOrUpdate(staffReview);
+                    }
+                }
+            }, commonVideoTaskExecutor);
+        }
+
+
+
+
         if (success) {
             log.info("创建评价成功, 评价ID={}", review.getId());
-            // 更新员工评分
-            updateStaffServiceScore(review.getStaffUserId());
             return R.data(review, "提交成功");
         } else {
             log.error("创建评价失败");

+ 7 - 0
alien-store/src/main/java/shop/alien/store/util/CommonConstant.java

@@ -112,4 +112,11 @@ public class CommonConstant {
     public static final String RATING_LIKE = "11";
     public static final String COMMENT_LIKE = "12";
     public static final String DYNAMIC_LIKE = "13";
+
+
+    /**
+     * 上线状态,0:上线,2:下线
+     */
+    public static final Integer ONLINE_STATUS = 0;
+    public static final Integer OFFLINE_STATUS = 1;
 }

+ 33 - 0
alien-store/src/main/java/shop/alien/store/util/FileUploadUtil.java

@@ -45,6 +45,7 @@ public class FileUploadUtil {
     List<String> privacyFileType = Arrays.asList("htm","html");
     List<String> ohterFileType = Arrays.asList("xls","xlsx");
     List<String> appFileType = Arrays.asList("apk", "ipk", "wgt");
+    List<String> pdfFileType = Arrays.asList("pdf");
 
     /**
      * 上传文件
@@ -60,6 +61,8 @@ public class FileUploadUtil {
                 prefix = "image/";
             } else if (videoFileType.contains(fileNameAndType.get("type").toLowerCase())) {
                 prefix = "video/";
+            } else if (pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
+                prefix = "pdf/";
             }
             // 去除文件名中的逗号,避免URL拼接时被错误分割
             String fileName = fileNameAndType.get("name").replaceAll(",", "");
@@ -92,6 +95,29 @@ public class FileUploadUtil {
     }
 
     /**
+     * 上传PDF文件
+     *
+     * @param multipartFile 文件名
+     * @return 文件路径
+     */
+    public String uploadPdf(MultipartFile multipartFile) {
+        try {
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(multipartFile);
+            if (!pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
+                log.error("FileUpload.uploadPdf ERROR 该文件不是PDF格式文件");
+                return null;
+            }
+            String prefix = "pdf/";
+            // 去除文件名中的逗号,避免URL拼接时被错误分割
+            String fileName = fileNameAndType.get("name").replaceAll(",", "");
+            return aliOSSUtil.uploadFile(multipartFile, prefix + fileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type"));
+        } catch (Exception e) {
+            log.error("FileUpload.uploadPdf ERROR Msg={}", e.getMessage());
+            return null;
+        }
+    }
+
+    /**
      * 上传多个文件
      *
      * @param multipartRequest 多文件
@@ -155,6 +181,13 @@ public class FileUploadUtil {
                     // 去除文件名中的逗号,避免URL拼接时被错误分割
                     String privacyFileName = fileNameAndType.get("name").replaceAll(",", "");
                     filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + privacyFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
+                } else if (pdfFileType.contains(fileNameAndType.get("type").toLowerCase())) {
+                    uploadDir += "/pdf";
+                    prefix = "pdf/";
+                    log.info("FileUpload.uploadMoreFile 获取到PDF文件准备复制 {} {} {}", uploadDir, prefix, multipartFile.getOriginalFilename());
+                    // 去除文件名中的逗号,避免URL拼接时被错误分割
+                    String pdfFileName = fileNameAndType.get("name").replaceAll(",", "");
+                    filePathList.add(aliOSSUtil.uploadFile(multipartFile, prefix + pdfFileName + RandomCreateUtil.getRandomNum(6) + "." + fileNameAndType.get("type")));
                 } else if (ohterFileType.contains(fileNameAndType.get("type").toLowerCase())) {
                     uploadDir += "/other/";
                     prefix = "other/";

+ 242 - 0
alien-store/src/main/java/shop/alien/store/util/ImageToPdfUploadUtil.java

@@ -0,0 +1,242 @@
+package shop.alien.store.util;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+import shop.alien.util.ali.AliOSSUtil;
+import shop.alien.util.common.RandomCreateUtil;
+import shop.alien.util.file.FileUtil;
+import shop.alien.util.pdf.ImageToPdfUtil;
+
+import org.springframework.util.StringUtils;
+
+import java.io.ByteArrayInputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 图片转PDF并上传工具类
+ * 接收前端上传的图片,转换为PDF后上传到OSS,返回PDF的线上URL
+ *
+ * @author system
+ * @since 2026-01-05
+ */
+@Slf4j
+@Component
+@RefreshScope
+@RequiredArgsConstructor
+public class ImageToPdfUploadUtil {
+
+    private final AliOSSUtil aliOSSUtil;
+
+    /**
+     * 支持的图片格式
+     */
+    private static final List<String> SUPPORTED_IMAGE_TYPES = Arrays.asList("jpg", "jpeg", "png", "bmp", "webp", "gif");
+
+    /**
+     * 将单张图片转换为PDF并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param imageFile 图片文件
+     * @return PDF的线上URL,失败返回null
+     */
+    public String imageToPdfAndUpload(MultipartFile imageFile) {
+        if (imageFile == null || imageFile.isEmpty()) {
+            log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: 图片文件为空");
+            return null;
+        }
+
+        try {
+            // 验证文件类型
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(imageFile);
+            String fileType = fileNameAndType.get("type").toLowerCase();
+            if (!SUPPORTED_IMAGE_TYPES.contains(fileType)) {
+                log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: 不支持的图片格式: {}", fileType);
+                return null;
+            }
+
+            // 直接从输入流读取图片并转换为PDF字节数组(内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfBytes(imageFile.getInputStream());
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String fileName = fileNameAndType.get("name").replaceAll(",", "");
+            String ossFilePath = "pdf/" + fileName + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.imageToPdfAndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 将多张图片合并为一个PDF并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param imageFiles 图片文件列表
+     * @return PDF的线上URL,失败返回null
+     */
+    public String imagesToPdfAndUpload(List<MultipartFile> imageFiles) {
+        if (imageFiles == null || imageFiles.isEmpty()) {
+            log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: 图片文件列表为空");
+            return null;
+        }
+
+        try {
+            // 收集所有有效的图片输入流
+            List<java.io.InputStream> imageStreams = new ArrayList<>();
+            for (MultipartFile imageFile : imageFiles) {
+                if (imageFile == null || imageFile.isEmpty()) {
+                    continue;
+                }
+
+                // 验证文件类型
+                Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(imageFile);
+                String fileType = fileNameAndType.get("type").toLowerCase();
+                if (!SUPPORTED_IMAGE_TYPES.contains(fileType)) {
+                    log.warn("ImageToPdfUploadUtil.imagesToPdfAndUpload WARN: 跳过不支持的图片格式: {}", fileType);
+                    continue;
+                }
+
+                // 直接使用输入流(内存方式)
+                imageStreams.add(imageFile.getInputStream());
+            }
+
+            if (imageStreams.isEmpty()) {
+                log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: 没有有效的图片文件");
+                return null;
+            }
+
+            // 将多张图片合并为一个PDF字节数组(内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imagesToPdfBytes(imageStreams);
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String ossFilePath = "pdf/merged_" + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.imagesToPdfAndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 将单张图片转换为PDF(A4页面)并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param imageFile 图片文件
+     * @return PDF的线上URL,失败返回null
+     */
+    public String imageToPdfA4AndUpload(MultipartFile imageFile) {
+        if (imageFile == null || imageFile.isEmpty()) {
+            log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: 图片文件为空");
+            return null;
+        }
+
+        try {
+            // 验证文件类型
+            Map<String, String> fileNameAndType = FileUtil.getFileNameAndType(imageFile);
+            String fileType = fileNameAndType.get("type").toLowerCase();
+            if (!SUPPORTED_IMAGE_TYPES.contains(fileType)) {
+                log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: 不支持的图片格式: {}", fileType);
+                return null;
+            }
+
+            // 直接从输入流读取图片并转换为PDF字节数组(A4页面,内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfA4Bytes(imageFile.getInputStream());
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String fileName = fileNameAndType.get("name").replaceAll(",", "");
+            String ossFilePath = "pdf/" + fileName + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.imageToPdfA4AndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 将Base64编码的图片转换为PDF并上传到OSS(内存方式,不保存临时文件)
+     *
+     * @param base64Image Base64编码的图片字符串(支持带或不带data:image前缀)
+     * @return PDF的线上URL,失败返回null
+     */
+    public String base64ToPdfAndUpload(String base64Image) {
+        if (!StringUtils.hasText(base64Image)) {
+            log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: Base64图片字符串为空");
+            return null;
+        }
+
+        try {
+            // 处理Base64字符串:移除可能的前缀(如 data:image/png;base64,)
+            String base64Data = base64Image.trim();
+            if (base64Data.contains(",")) {
+                base64Data = base64Data.substring(base64Data.indexOf(",") + 1);
+            }
+
+            // 解码Base64为字节数组
+            byte[] imageBytes;
+            try {
+                imageBytes = Base64.getDecoder().decode(base64Data);
+            } catch (IllegalArgumentException e) {
+                log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: Base64解码失败: {}", e.getMessage());
+                return null;
+            }
+
+            if (imageBytes == null || imageBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: Base64解码后图片数据为空");
+                return null;
+            }
+
+            // 将字节数组转换为输入流
+            ByteArrayInputStream imageInputStream = new ByteArrayInputStream(imageBytes);
+
+            // 转换为PDF字节数组(内存方式)
+            byte[] pdfBytes = ImageToPdfUtil.imageToPdfBytes(imageInputStream);
+            if (pdfBytes == null || pdfBytes.length == 0) {
+                log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: 图片转PDF失败");
+                return null;
+            }
+
+            // 生成OSS文件路径
+            String ossFilePath = "pdf/base64_" + RandomCreateUtil.getRandomNum(6) + ".pdf";
+
+            // 将PDF字节数组转换为输入流并上传到OSS
+            ByteArrayInputStream pdfInputStream = new ByteArrayInputStream(pdfBytes);
+            String pdfUrl = aliOSSUtil.uploadFile(pdfInputStream, ossFilePath);
+
+            return pdfUrl;
+        } catch (Exception e) {
+            log.error("ImageToPdfUploadUtil.base64ToPdfAndUpload ERROR: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+}

+ 733 - 0
alien-store/src/main/java/shop/alien/store/util/StatisticsComparisonImageUtil.java

@@ -0,0 +1,733 @@
+package shop.alien.store.util;
+
+import lombok.extern.slf4j.Slf4j;
+import shop.alien.entity.store.vo.StoreOperationalStatisticsComparisonVo;
+
+import javax.imageio.ImageIO;
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.ByteArrayOutputStream;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 经营统计数据对比图片生成工具类
+ * 将 StoreOperationalStatisticsComparisonVo 数据转换为图片
+ *
+ * @author system
+ * @since 2026-01-05
+ */
+@Slf4j
+public class StatisticsComparisonImageUtil {
+
+    // 颜色定义
+    private static final Color BACKGROUND_COLOR = Color.WHITE;
+    private static final Color TEXT_COLOR = new Color(51, 51, 51); // #333333
+    private static final Color HEADER_BG_COLOR = new Color(245, 245, 245); // #F5F5F5
+    private static final Color POSITIVE_COLOR = new Color(76, 175, 80); // 绿色 #4CAF50
+    private static final Color NEGATIVE_COLOR = new Color(244, 67, 54); // 红色 #F44336
+    private static final Color SECTION_TITLE_COLOR = new Color(33, 33, 33); // #212121
+    private static final Color BORDER_COLOR = new Color(224, 224, 224); // #E0E0E0
+
+    // 字体定义
+    private static final String FONT_NAME = "Microsoft YaHei"; // 微软雅黑,如果系统没有则使用默认字体
+    private static final int TITLE_FONT_SIZE = 24;
+    private static final int SECTION_TITLE_FONT_SIZE = 18;
+    private static final int DATA_FONT_SIZE = 14;
+    private static final int LABEL_FONT_SIZE = 12;
+
+    // 尺寸定义
+    private static final int IMAGE_WIDTH = 800;
+    private static final int PADDING = 20;
+    private static final int SECTION_SPACING = 30;
+    private static final int ROW_HEIGHT = 35;
+    private static final int HEADER_HEIGHT = 50;
+
+    private static final DecimalFormat DECIMAL_FORMAT = new DecimalFormat("#,##0.00");
+    private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("#,##0.00%");
+
+    /**
+     * 将统计数据对比转换为图片字节数组
+     *
+     * @param comparison 统计数据对比对象
+     * @return 图片字节数组
+     */
+    public static byte[] generateImage(StoreOperationalStatisticsComparisonVo comparison) {
+        if (comparison == null) {
+            throw new IllegalArgumentException("统计数据对比对象不能为空");
+        }
+
+        try {
+            // 计算图片高度
+            int totalHeight = calculateImageHeight(comparison);
+            
+            // 创建图片
+            BufferedImage image = new BufferedImage(IMAGE_WIDTH, totalHeight, BufferedImage.TYPE_INT_RGB);
+            Graphics2D g2d = image.createGraphics();
+            
+            // 设置抗锯齿
+            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
+            g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+            
+            // 填充背景
+            g2d.setColor(BACKGROUND_COLOR);
+            g2d.fillRect(0, 0, IMAGE_WIDTH, totalHeight);
+            
+            int currentY = PADDING;
+            
+            // 绘制标题
+            currentY = drawTitle(g2d, currentY, comparison);
+            
+            // 绘制日期范围
+            currentY = drawDateRange(g2d, currentY, comparison);
+            
+            currentY += SECTION_SPACING;
+            
+            // 绘制流量数据
+            if (comparison.getTrafficData() != null) {
+                currentY = drawSection(g2d, currentY, "流量数据", 
+                    buildTrafficDataRows(comparison.getTrafficData()));
+            }
+            
+            // 绘制互动数据
+            if (comparison.getInteractionData() != null) {
+                currentY = drawSection(g2d, currentY, "互动数据", 
+                    buildInteractionDataRows(comparison.getInteractionData()));
+            }
+            
+            // 绘制优惠券数据
+            if (comparison.getCouponData() != null) {
+                currentY = drawSection(g2d, currentY, "优惠券", 
+                    buildCouponDataRows(comparison.getCouponData()));
+            }
+            
+            // 绘制代金券数据
+            if (comparison.getVoucherData() != null) {
+                currentY = drawSection(g2d, currentY, "代金券", 
+                    buildVoucherDataRows(comparison.getVoucherData()));
+            }
+            
+            // 绘制服务质量数据
+            if (comparison.getServiceQualityData() != null) {
+                currentY = drawSection(g2d, currentY, "服务质量", 
+                    buildServiceQualityDataRows(comparison.getServiceQualityData()));
+            }
+            
+            // 绘制价目表排名数据
+            if (comparison.getPriceListRanking() != null && !comparison.getPriceListRanking().isEmpty()) {
+                currentY = drawPriceListRankingSection(g2d, currentY, "价目表排名", 
+                    comparison.getPriceListRanking());
+            }
+            
+            g2d.dispose();
+            
+            // 转换为字节数组
+            ByteArrayOutputStream baos = new ByteArrayOutputStream();
+            ImageIO.write(image, "PNG", baos);
+            return baos.toByteArray();
+            
+        } catch (Exception e) {
+            log.error("生成统计数据对比图片失败", e);
+            throw new RuntimeException("生成图片失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 计算图片高度
+     */
+    private static int calculateImageHeight(StoreOperationalStatisticsComparisonVo comparison) {
+        int height = PADDING * 2;
+        height += HEADER_HEIGHT; // 标题
+        height += 40; // 日期范围
+        height += SECTION_SPACING;
+        
+        int rowCount = 0;
+        if (comparison.getTrafficData() != null) {
+            rowCount += 6; // 流量数据行数(6个字段)
+        }
+        if (comparison.getInteractionData() != null) {
+            rowCount += 13; // 互动数据行数(13个字段)
+        }
+        if (comparison.getCouponData() != null) {
+            rowCount += 10; // 优惠券数据行数(10个字段)
+        }
+        if (comparison.getVoucherData() != null) {
+            rowCount += 10; // 代金券数据行数(10个字段)
+        }
+        if (comparison.getServiceQualityData() != null) {
+            rowCount += 12; // 服务质量数据行数(12个字段)
+        }
+        if (comparison.getPriceListRanking() != null && !comparison.getPriceListRanking().isEmpty()) {
+            // 价目表数据:每个价目表3个指标(浏览量、访客数、分享数)
+            rowCount += comparison.getPriceListRanking().size() * 3;
+        }
+        
+        height += rowCount * ROW_HEIGHT;
+        height += (rowCount / 6 + 4) * SECTION_SPACING; // 区块间距
+        
+        return height;
+    }
+
+    /**
+     * 绘制标题
+     */
+    private static int drawTitle(Graphics2D g2d, int y, StoreOperationalStatisticsComparisonVo comparison) {
+        Font titleFont = new Font(FONT_NAME, Font.BOLD, TITLE_FONT_SIZE);
+        g2d.setFont(titleFont);
+        g2d.setColor(SECTION_TITLE_COLOR);
+        
+        String title = "经营数据";
+        FontMetrics fm = g2d.getFontMetrics();
+        int titleWidth = fm.stringWidth(title);
+        int titleX = (IMAGE_WIDTH - titleWidth) / 2;
+        
+        g2d.drawString(title, titleX, y + TITLE_FONT_SIZE);
+        return y + HEADER_HEIGHT;
+    }
+
+    /**
+     * 绘制日期范围
+     */
+    private static int drawDateRange(Graphics2D g2d, int y, StoreOperationalStatisticsComparisonVo comparison) {
+        Font dateFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
+        g2d.setFont(dateFont);
+        g2d.setColor(TEXT_COLOR);
+        
+        String currentDate = formatDate(comparison.getCurrentStartTime()) + "-" + formatDate(comparison.getCurrentEndTime());
+        String previousDate = formatDate(comparison.getPreviousStartTime()) + "-" + formatDate(comparison.getPreviousEndTime());
+        
+        int dateX = PADDING;
+        g2d.drawString(currentDate, dateX, y);
+        g2d.drawString(previousDate, dateX, y + 20);
+        
+        return y + 40;
+    }
+
+    /**
+     * 格式化日期
+     */
+    private static String formatDate(String date) {
+        if (date == null || date.length() < 10) {
+            return date;
+        }
+        // 将 yyyy-MM-dd 转换为 yyyy/MM/dd
+        return date.replace("-", "/");
+    }
+
+    /**
+     * 绘制数据区块
+     */
+    private static int drawSection(Graphics2D g2d, int y, String sectionTitle, List<DataRow> rows) {
+        if (rows == null || rows.isEmpty()) {
+            return y;
+        }
+        
+        // 绘制区块标题
+        Font sectionFont = new Font(FONT_NAME, Font.BOLD, SECTION_TITLE_FONT_SIZE);
+        g2d.setFont(sectionFont);
+        g2d.setColor(SECTION_TITLE_COLOR);
+        g2d.drawString(sectionTitle, PADDING, y);
+        y += 30;
+        
+        // 绘制表头
+        y = drawTableHeader(g2d, y);
+        
+        // 绘制数据行
+        Font dataFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
+        Font labelFont = new Font(FONT_NAME, Font.PLAIN, LABEL_FONT_SIZE);
+        
+        for (DataRow row : rows) {
+            y = drawDataRow(g2d, y, row, dataFont, labelFont);
+        }
+        
+        return y + SECTION_SPACING;
+    }
+
+    /**
+     * 绘制表头
+     */
+    private static int drawTableHeader(Graphics2D g2d, int y) {
+        // 绘制表头背景
+        g2d.setColor(HEADER_BG_COLOR);
+        g2d.fillRect(PADDING, y, IMAGE_WIDTH - PADDING * 2, ROW_HEIGHT);
+        
+        // 绘制表头文字
+        Font headerFont = new Font(FONT_NAME, Font.BOLD, LABEL_FONT_SIZE);
+        g2d.setFont(headerFont);
+        g2d.setColor(TEXT_COLOR);
+        
+        int col1X = PADDING + 10;
+        int col2X = IMAGE_WIDTH / 2 - 80;
+        int col3X = IMAGE_WIDTH / 2 + 20;
+        int col4X = IMAGE_WIDTH - PADDING - 100;
+        
+        g2d.drawString("指标", col1X, y + 20);
+        g2d.drawString("当期", col2X, y + 20);
+        g2d.drawString("上期", col3X, y + 20);
+        g2d.drawString("变化率", col4X, y + 20);
+        
+        // 绘制边框
+        g2d.setColor(BORDER_COLOR);
+        g2d.drawRect(PADDING, y, IMAGE_WIDTH - PADDING * 2, ROW_HEIGHT);
+        
+        return y + ROW_HEIGHT;
+    }
+
+    /**
+     * 绘制数据行
+     */
+    private static int drawDataRow(Graphics2D g2d, int y, DataRow row, Font dataFont, Font labelFont) {
+        // 绘制行背景(交替颜色)
+        if ((y / ROW_HEIGHT) % 2 == 0) {
+            g2d.setColor(new Color(250, 250, 250));
+            g2d.fillRect(PADDING, y, IMAGE_WIDTH - PADDING * 2, ROW_HEIGHT);
+        }
+        
+        // 绘制指标名称
+        g2d.setFont(labelFont);
+        g2d.setColor(TEXT_COLOR);
+        g2d.drawString(row.getLabel(), PADDING + 10, y + 22);
+        
+        // 绘制当期值
+        g2d.setFont(dataFont);
+        String currentValue = formatValue(row.getCurrent());
+        g2d.drawString(currentValue, IMAGE_WIDTH / 2 - 80, y + 22);
+        
+        // 绘制上期值
+        String previousValue = formatValue(row.getPrevious());
+        g2d.drawString(previousValue, IMAGE_WIDTH / 2 + 20, y + 22);
+        
+        // 绘制变化率(带颜色)
+        String changeRate = formatChangeRate(row.getChangeRate());
+        Color changeColor = row.getChangeRate() != null && row.getChangeRate().compareTo(BigDecimal.ZERO) >= 0 
+            ? POSITIVE_COLOR : NEGATIVE_COLOR;
+        g2d.setColor(changeColor);
+        g2d.drawString(changeRate, IMAGE_WIDTH - PADDING - 100, y + 22);
+        
+        // 绘制边框
+        g2d.setColor(BORDER_COLOR);
+        g2d.drawLine(PADDING, y + ROW_HEIGHT, IMAGE_WIDTH - PADDING, y + ROW_HEIGHT);
+        
+        return y + ROW_HEIGHT;
+    }
+
+    /**
+     * 格式化数值
+     */
+    private static String formatValue(Object value) {
+        if (value == null) {
+            return "0";
+        }
+        if (value instanceof BigDecimal) {
+            BigDecimal bd = (BigDecimal) value;
+            if (bd.scale() > 0) {
+                return DECIMAL_FORMAT.format(bd);
+            } else {
+                return String.valueOf(bd.longValue());
+            }
+        }
+        if (value instanceof Number) {
+            return DECIMAL_FORMAT.format(((Number) value).doubleValue());
+        }
+        return String.valueOf(value);
+    }
+
+    /**
+     * 格式化变化率
+     */
+    private static String formatChangeRate(BigDecimal changeRate) {
+        if (changeRate == null) {
+            return "0.00%";
+        }
+        String sign = changeRate.compareTo(BigDecimal.ZERO) >= 0 ? "+" : "";
+        return sign + PERCENT_FORMAT.format(changeRate.divide(new BigDecimal(100), 4, BigDecimal.ROUND_HALF_UP));
+    }
+
+    /**
+     * 构建流量数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildTrafficDataRows(StoreOperationalStatisticsComparisonVo.TrafficDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 店铺搜索量对比
+        if (data.getStoreSearchVolume() != null) {
+            rows.add(new DataRow("店铺搜索量对比", data.getStoreSearchVolume().getCurrent(),
+                data.getStoreSearchVolume().getPrevious(), data.getStoreSearchVolume().getChangeRate()));
+        }
+        // 2. 浏览量对比
+        if (data.getPageViews() != null) {
+            rows.add(new DataRow("浏览量对比", data.getPageViews().getCurrent(),
+                data.getPageViews().getPrevious(), data.getPageViews().getChangeRate()));
+        }
+        // 3. 访客数对比
+        if (data.getVisitors() != null) {
+            rows.add(new DataRow("访客数对比", data.getVisitors().getCurrent(),
+                data.getVisitors().getPrevious(), data.getVisitors().getChangeRate()));
+        }
+        // 4. 新增访客数对比
+        if (data.getNewVisitors() != null) {
+            rows.add(new DataRow("新增访客数对比", data.getNewVisitors().getCurrent(),
+                data.getNewVisitors().getPrevious(), data.getNewVisitors().getChangeRate()));
+        }
+        // 5. 访问时长对比
+        if (data.getVisitDuration() != null) {
+            rows.add(new DataRow("访问时长对比", data.getVisitDuration().getCurrent(),
+                data.getVisitDuration().getPrevious(), data.getVisitDuration().getChangeRate()));
+        }
+        // 6. 平均访问时长对比
+        if (data.getAvgVisitDuration() != null) {
+            rows.add(new DataRow("平均访问时长对比", data.getAvgVisitDuration().getCurrent(),
+                data.getAvgVisitDuration().getPrevious(), data.getAvgVisitDuration().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建互动数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildInteractionDataRows(StoreOperationalStatisticsComparisonVo.InteractionDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 店铺收藏次数对比
+        if (data.getStoreCollectionCount() != null) {
+            rows.add(new DataRow("店铺收藏次数对比", data.getStoreCollectionCount().getCurrent(),
+                data.getStoreCollectionCount().getPrevious(), data.getStoreCollectionCount().getChangeRate()));
+        }
+        // 2. 店铺分享次数对比
+        if (data.getStoreShareCount() != null) {
+            rows.add(new DataRow("店铺分享次数对比", data.getStoreShareCount().getCurrent(),
+                data.getStoreShareCount().getPrevious(), data.getStoreShareCount().getChangeRate()));
+        }
+        // 3. 店铺打卡次数对比
+        if (data.getStoreCheckInCount() != null) {
+            rows.add(new DataRow("店铺打卡次数对比", data.getStoreCheckInCount().getCurrent(),
+                data.getStoreCheckInCount().getPrevious(), data.getStoreCheckInCount().getChangeRate()));
+        }
+        // 4. 咨询商家次数对比
+        if (data.getConsultMerchantCount() != null) {
+            rows.add(new DataRow("咨询商家次数对比", data.getConsultMerchantCount().getCurrent(),
+                data.getConsultMerchantCount().getPrevious(), data.getConsultMerchantCount().getChangeRate()));
+        }
+        // 5. 好友数量对比
+        if (data.getFriendsCount() != null) {
+            rows.add(new DataRow("好友数量对比", data.getFriendsCount().getCurrent(),
+                data.getFriendsCount().getPrevious(), data.getFriendsCount().getChangeRate()));
+        }
+        // 6. 关注数量对比
+        if (data.getFollowCount() != null) {
+            rows.add(new DataRow("关注数量对比", data.getFollowCount().getCurrent(),
+                data.getFollowCount().getPrevious(), data.getFollowCount().getChangeRate()));
+        }
+        // 7. 粉丝数量对比
+        if (data.getFansCount() != null) {
+            rows.add(new DataRow("粉丝数量对比", data.getFansCount().getCurrent(),
+                data.getFansCount().getPrevious(), data.getFansCount().getChangeRate()));
+        }
+        // 8. 发布动态数量对比
+        if (data.getPostsPublishedCount() != null) {
+            rows.add(new DataRow("发布动态数量对比", data.getPostsPublishedCount().getCurrent(),
+                data.getPostsPublishedCount().getPrevious(), data.getPostsPublishedCount().getChangeRate()));
+        }
+        // 9. 动态点赞数量对比
+        if (data.getPostLikesCount() != null) {
+            rows.add(new DataRow("动态点赞数量对比", data.getPostLikesCount().getCurrent(),
+                data.getPostLikesCount().getPrevious(), data.getPostLikesCount().getChangeRate()));
+        }
+        // 10. 动态评论数量对比
+        if (data.getPostCommentsCount() != null) {
+            rows.add(new DataRow("动态评论数量对比", data.getPostCommentsCount().getCurrent(),
+                data.getPostCommentsCount().getPrevious(), data.getPostCommentsCount().getChangeRate()));
+        }
+        // 11. 动态转发数量对比
+        if (data.getPostSharesCount() != null) {
+            rows.add(new DataRow("动态转发数量对比", data.getPostSharesCount().getCurrent(),
+                data.getPostSharesCount().getPrevious(), data.getPostSharesCount().getChangeRate()));
+        }
+        // 12. 被举报次数对比
+        if (data.getReportedCount() != null) {
+            rows.add(new DataRow("被举报次数对比", data.getReportedCount().getCurrent(),
+                data.getReportedCount().getPrevious(), data.getReportedCount().getChangeRate()));
+        }
+        // 13. 被拉黑次数对比
+        if (data.getBlockedCount() != null) {
+            rows.add(new DataRow("被拉黑次数对比", data.getBlockedCount().getCurrent(),
+                data.getBlockedCount().getPrevious(), data.getBlockedCount().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建优惠券数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildCouponDataRows(StoreOperationalStatisticsComparisonVo.CouponDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 赠送好友数量对比
+        if (data.getGiftToFriendsCount() != null) {
+            rows.add(new DataRow("赠送好友数量对比", data.getGiftToFriendsCount().getCurrent(), 
+                data.getGiftToFriendsCount().getPrevious(), data.getGiftToFriendsCount().getChangeRate()));
+        }
+        // 2. 赠送好友金额合计对比
+        if (data.getGiftToFriendsAmount() != null) {
+            rows.add(new DataRow("赠送好友金额合计对比", data.getGiftToFriendsAmount().getCurrent(), 
+                data.getGiftToFriendsAmount().getPrevious(), data.getGiftToFriendsAmount().getChangeRate()));
+        }
+        // 3. 赠送好友使用数量对比
+        if (data.getGiftToFriendsUsedCount() != null) {
+            rows.add(new DataRow("赠送好友使用数量对比", data.getGiftToFriendsUsedCount().getCurrent(), 
+                data.getGiftToFriendsUsedCount().getPrevious(), data.getGiftToFriendsUsedCount().getChangeRate()));
+        }
+        // 4. 赠送好友使用金额合计对比
+        if (data.getGiftToFriendsUsedAmount() != null) {
+            rows.add(new DataRow("赠送好友使用金额合计对比", data.getGiftToFriendsUsedAmount().getCurrent(), 
+                data.getGiftToFriendsUsedAmount().getPrevious(), data.getGiftToFriendsUsedAmount().getChangeRate()));
+        }
+        // 5. 赠送好友使用金额占比对比
+        if (data.getGiftToFriendsUsedAmountRatio() != null) {
+            rows.add(new DataRow("赠送好友使用金额占比对比", data.getGiftToFriendsUsedAmountRatio().getCurrent(), 
+                data.getGiftToFriendsUsedAmountRatio().getPrevious(), data.getGiftToFriendsUsedAmountRatio().getChangeRate()));
+        }
+        // 6. 好友赠送数量对比
+        if (data.getFriendsGiftCount() != null) {
+            rows.add(new DataRow("好友赠送数量对比", data.getFriendsGiftCount().getCurrent(), 
+                data.getFriendsGiftCount().getPrevious(), data.getFriendsGiftCount().getChangeRate()));
+        }
+        // 7. 好友赠送金额合计对比
+        if (data.getFriendsGiftAmount() != null) {
+            rows.add(new DataRow("好友赠送金额合计对比", data.getFriendsGiftAmount().getCurrent(), 
+                data.getFriendsGiftAmount().getPrevious(), data.getFriendsGiftAmount().getChangeRate()));
+        }
+        // 8. 好友赠送使用数量对比
+        if (data.getFriendsGiftUsedCount() != null) {
+            rows.add(new DataRow("好友赠送使用数量对比", data.getFriendsGiftUsedCount().getCurrent(), 
+                data.getFriendsGiftUsedCount().getPrevious(), data.getFriendsGiftUsedCount().getChangeRate()));
+        }
+        // 9. 好友赠送使用金额合计对比
+        if (data.getFriendsGiftUsedAmount() != null) {
+            rows.add(new DataRow("好友赠送使用金额合计对比", data.getFriendsGiftUsedAmount().getCurrent(), 
+                data.getFriendsGiftUsedAmount().getPrevious(), data.getFriendsGiftUsedAmount().getChangeRate()));
+        }
+        // 10. 好友赠送使用金额占比对比
+        if (data.getFriendsGiftUsedAmountRatio() != null) {
+            rows.add(new DataRow("好友赠送使用金额占比对比", data.getFriendsGiftUsedAmountRatio().getCurrent(), 
+                data.getFriendsGiftUsedAmountRatio().getPrevious(), data.getFriendsGiftUsedAmountRatio().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建代金券数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildVoucherDataRows(StoreOperationalStatisticsComparisonVo.VoucherDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 赠送好友数量对比
+        if (data.getGiftToFriendsCount() != null) {
+            rows.add(new DataRow("赠送好友数量对比", data.getGiftToFriendsCount().getCurrent(), 
+                data.getGiftToFriendsCount().getPrevious(), data.getGiftToFriendsCount().getChangeRate()));
+        }
+        // 2. 赠送好友金额合计对比
+        if (data.getGiftToFriendsAmount() != null) {
+            rows.add(new DataRow("赠送好友金额合计对比", data.getGiftToFriendsAmount().getCurrent(), 
+                data.getGiftToFriendsAmount().getPrevious(), data.getGiftToFriendsAmount().getChangeRate()));
+        }
+        // 3. 赠送好友使用数量对比
+        if (data.getGiftToFriendsUsedCount() != null) {
+            rows.add(new DataRow("赠送好友使用数量对比", data.getGiftToFriendsUsedCount().getCurrent(), 
+                data.getGiftToFriendsUsedCount().getPrevious(), data.getGiftToFriendsUsedCount().getChangeRate()));
+        }
+        // 4. 赠送好友使用金额合计对比
+        if (data.getGiftToFriendsUsedAmount() != null) {
+            rows.add(new DataRow("赠送好友使用金额合计对比", data.getGiftToFriendsUsedAmount().getCurrent(), 
+                data.getGiftToFriendsUsedAmount().getPrevious(), data.getGiftToFriendsUsedAmount().getChangeRate()));
+        }
+        // 5. 赠送好友使用金额占比对比
+        if (data.getGiftToFriendsUsedAmountRatio() != null) {
+            rows.add(new DataRow("赠送好友使用金额占比对比", data.getGiftToFriendsUsedAmountRatio().getCurrent(), 
+                data.getGiftToFriendsUsedAmountRatio().getPrevious(), data.getGiftToFriendsUsedAmountRatio().getChangeRate()));
+        }
+        // 6. 好友赠送数量对比
+        if (data.getFriendsGiftCount() != null) {
+            rows.add(new DataRow("好友赠送数量对比", data.getFriendsGiftCount().getCurrent(), 
+                data.getFriendsGiftCount().getPrevious(), data.getFriendsGiftCount().getChangeRate()));
+        }
+        // 7. 好友赠送金额合计对比
+        if (data.getFriendsGiftAmount() != null) {
+            rows.add(new DataRow("好友赠送金额合计对比", data.getFriendsGiftAmount().getCurrent(), 
+                data.getFriendsGiftAmount().getPrevious(), data.getFriendsGiftAmount().getChangeRate()));
+        }
+        // 8. 好友赠送使用数量对比
+        if (data.getFriendsGiftUsedCount() != null) {
+            rows.add(new DataRow("好友赠送使用数量对比", data.getFriendsGiftUsedCount().getCurrent(), 
+                data.getFriendsGiftUsedCount().getPrevious(), data.getFriendsGiftUsedCount().getChangeRate()));
+        }
+        // 9. 好友赠送使用金额合计对比
+        if (data.getFriendsGiftUsedAmount() != null) {
+            rows.add(new DataRow("好友赠送使用金额合计对比", data.getFriendsGiftUsedAmount().getCurrent(), 
+                data.getFriendsGiftUsedAmount().getPrevious(), data.getFriendsGiftUsedAmount().getChangeRate()));
+        }
+        // 10. 好友赠送使用金额占比对比
+        if (data.getFriendsGiftUsedAmountRatio() != null) {
+            rows.add(new DataRow("好友赠送使用金额占比对比", data.getFriendsGiftUsedAmountRatio().getCurrent(), 
+                data.getFriendsGiftUsedAmountRatio().getPrevious(), data.getFriendsGiftUsedAmountRatio().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 构建服务质量数据行(按照实体类字段顺序)
+     */
+    private static List<DataRow> buildServiceQualityDataRows(StoreOperationalStatisticsComparisonVo.ServiceQualityDataComparison data) {
+        List<DataRow> rows = new ArrayList<>();
+        // 1. 店铺评分对比
+        if (data.getStoreRating() != null) {
+            rows.add(new DataRow("店铺评分对比", data.getStoreRating().getCurrent(), 
+                data.getStoreRating().getPrevious(), data.getStoreRating().getChangeRate()));
+        }
+        // 2. 口味评分对比
+        if (data.getTasteRating() != null) {
+            rows.add(new DataRow("口味评分对比", data.getTasteRating().getCurrent(), 
+                data.getTasteRating().getPrevious(), data.getTasteRating().getChangeRate()));
+        }
+        // 3. 环境评分对比
+        if (data.getEnvironmentRating() != null) {
+            rows.add(new DataRow("环境评分对比", data.getEnvironmentRating().getCurrent(), 
+                data.getEnvironmentRating().getPrevious(), data.getEnvironmentRating().getChangeRate()));
+        }
+        // 4. 服务评分对比
+        if (data.getServiceRating() != null) {
+            rows.add(new DataRow("服务评分对比", data.getServiceRating().getCurrent(), 
+                data.getServiceRating().getPrevious(), data.getServiceRating().getChangeRate()));
+        }
+        // 5. 评价数量对比
+        if (data.getTotalReviews() != null) {
+            rows.add(new DataRow("评价数量对比", data.getTotalReviews().getCurrent(), 
+                data.getTotalReviews().getPrevious(), data.getTotalReviews().getChangeRate()));
+        }
+        // 6. 好评数量对比
+        if (data.getPositiveReviews() != null) {
+            rows.add(new DataRow("好评数量对比", data.getPositiveReviews().getCurrent(), 
+                data.getPositiveReviews().getPrevious(), data.getPositiveReviews().getChangeRate()));
+        }
+        // 7. 中评数量对比
+        if (data.getNeutralReviews() != null) {
+            rows.add(new DataRow("中评数量对比", data.getNeutralReviews().getCurrent(), 
+                data.getNeutralReviews().getPrevious(), data.getNeutralReviews().getChangeRate()));
+        }
+        // 8. 差评数量对比
+        if (data.getNegativeReviews() != null) {
+            rows.add(new DataRow("差评数量对比", data.getNegativeReviews().getCurrent(), 
+                data.getNegativeReviews().getPrevious(), data.getNegativeReviews().getChangeRate()));
+        }
+        // 9. 差评占比对比
+        if (data.getNegativeReviewRatio() != null) {
+            rows.add(new DataRow("差评占比对比", data.getNegativeReviewRatio().getCurrent(), 
+                data.getNegativeReviewRatio().getPrevious(), data.getNegativeReviewRatio().getChangeRate()));
+        }
+        // 10. 差评申诉次数对比
+        if (data.getNegativeReviewAppealsCount() != null) {
+            rows.add(new DataRow("差评申诉次数对比", data.getNegativeReviewAppealsCount().getCurrent(), 
+                data.getNegativeReviewAppealsCount().getPrevious(), data.getNegativeReviewAppealsCount().getChangeRate()));
+        }
+        // 11. 差评申诉成功次数对比
+        if (data.getNegativeReviewAppealsSuccessCount() != null) {
+            rows.add(new DataRow("差评申诉成功次数对比", data.getNegativeReviewAppealsSuccessCount().getCurrent(), 
+                data.getNegativeReviewAppealsSuccessCount().getPrevious(), data.getNegativeReviewAppealsSuccessCount().getChangeRate()));
+        }
+        // 12. 差评申诉成功占比对比
+        if (data.getNegativeReviewAppealsSuccessRatio() != null) {
+            rows.add(new DataRow("差评申诉成功占比对比", data.getNegativeReviewAppealsSuccessRatio().getCurrent(), 
+                data.getNegativeReviewAppealsSuccessRatio().getPrevious(), data.getNegativeReviewAppealsSuccessRatio().getChangeRate()));
+        }
+        return rows;
+    }
+
+    /**
+     * 绘制价目表排名数据区块(特殊格式,每个价目表显示3个指标)
+     */
+    private static int drawPriceListRankingSection(Graphics2D g2d, int y, String sectionTitle, 
+                                                   List<StoreOperationalStatisticsComparisonVo.PriceListRankingComparison> rankings) {
+        if (rankings == null || rankings.isEmpty()) {
+            return y;
+        }
+        
+        // 绘制区块标题
+        Font sectionFont = new Font(FONT_NAME, Font.BOLD, SECTION_TITLE_FONT_SIZE);
+        g2d.setFont(sectionFont);
+        g2d.setColor(SECTION_TITLE_COLOR);
+        g2d.drawString(sectionTitle, PADDING, y);
+        y += 30;
+        
+        Font dataFont = new Font(FONT_NAME, Font.PLAIN, DATA_FONT_SIZE);
+        Font labelFont = new Font(FONT_NAME, Font.PLAIN, LABEL_FONT_SIZE);
+        
+        // 遍历每个价目表
+        for (StoreOperationalStatisticsComparisonVo.PriceListRankingComparison ranking : rankings) {
+            // 绘制价目表名称(作为子标题)
+            g2d.setFont(new Font(FONT_NAME, Font.BOLD, LABEL_FONT_SIZE));
+            g2d.setColor(new Color(66, 66, 66));
+            String priceListName = ranking.getPriceListItemName() != null ? ranking.getPriceListItemName() : 
+                ("价目表ID: " + ranking.getPriceId());
+            g2d.drawString(priceListName, PADDING + 20, y);
+            y += 25;
+            
+            // 绘制表头
+            y = drawTableHeader(g2d, y);
+            
+            // 绘制该价目表的3个指标
+            List<DataRow> rows = new ArrayList<>();
+            if (ranking.getPageViews() != null) {
+                rows.add(new DataRow("浏览量", ranking.getPageViews().getCurrent(), 
+                    ranking.getPageViews().getPrevious(), ranking.getPageViews().getChangeRate()));
+            }
+            if (ranking.getVisitors() != null) {
+                rows.add(new DataRow("访客", ranking.getVisitors().getCurrent(), 
+                    ranking.getVisitors().getPrevious(), ranking.getVisitors().getChangeRate()));
+            }
+            if (ranking.getShares() != null) {
+                rows.add(new DataRow("分享数", ranking.getShares().getCurrent(), 
+                    ranking.getShares().getPrevious(), ranking.getShares().getChangeRate()));
+            }
+            
+            // 绘制数据行
+            for (DataRow row : rows) {
+                y = drawDataRow(g2d, y, row, dataFont, labelFont);
+            }
+            
+            y += SECTION_SPACING / 2; // 价目表之间的间距
+        }
+        
+        return y + SECTION_SPACING;
+    }
+
+    /**
+     * 数据行内部类
+     */
+    private static class DataRow {
+        private String label;
+        private Object current;
+        private Object previous;
+        private BigDecimal changeRate;
+
+        public DataRow(String label, Object current, Object previous, BigDecimal changeRate) {
+            this.label = label;
+            this.current = current;
+            this.previous = previous;
+            this.changeRate = changeRate;
+        }
+
+        public String getLabel() {
+            return label;
+        }
+
+        public Object getCurrent() {
+            return current;
+        }
+
+        public Object getPrevious() {
+            return previous;
+        }
+
+        public BigDecimal getChangeRate() {
+            return changeRate;
+        }
+    }
+}

+ 63 - 0
alien-util/src/main/java/shop/alien/util/ali/AliOSSUtil.java

@@ -15,6 +15,7 @@ import org.springframework.web.multipart.MultipartFile;
 import shop.alien.util.file.FileUtil;
 
 import java.io.File;
+import java.io.InputStream;
 
 /**
  * 阿里云oss工具类
@@ -136,4 +137,66 @@ public class AliOSSUtil {
         }
     }
 
+    /**
+     * oss上传文件(从输入流)
+     *
+     * @param inputStream 输入流
+     * @param ossFilePath oss中文件全路径(image/xxx.jpg)
+     * @return filePath
+     */
+    public String uploadFile(InputStream inputStream, String ossFilePath) {
+        // 验证参数
+        if (inputStream == null) {
+            log.error("AliOSSUtil.uploadFile ERROR: inputStream is null");
+            return null;
+        }
+        
+        if (StringUtils.isEmpty(ossFilePath)) {
+            log.error("AliOSSUtil.uploadFile ERROR: ossFilePath is empty");
+            return null;
+        }
+        
+        // 验证ossFilePath格式 - 不能以/开头
+        if (ossFilePath.startsWith("/")) {
+            log.error("AliOSSUtil.uploadFile ERROR: ossFilePath cannot start with '/': {}", ossFilePath);
+            return null;
+        }
+        
+        if (ossFilePath.contains("..") || ossFilePath.contains("\\")) {
+            log.error("AliOSSUtil.uploadFile ERROR: invalid ossFilePath format: {}", ossFilePath);
+            return null;
+        }
+
+        // 创建OSSClient实例
+        ClientBuilderConfiguration clientBuilderConfiguration = new ClientBuilderConfiguration();
+        clientBuilderConfiguration.setSignatureVersion(SignVersion.V4);
+        OSS ossClient = new OSSClientBuilder().build(endPoint, accessKeyId, accessKeySecret);
+        try {
+            // 创建PutObjectRequest对象(使用输入流)
+            PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, ossFilePath, inputStream);
+            // 上传文件
+            PutObjectResult result = ossClient.putObject(putObjectRequest);
+            String eTag = result.getETag();
+            if (StringUtils.isNotEmpty(eTag)) {
+                return "https://" + bucketName + "." + endPoint + "/" + ossFilePath;
+            }
+            return null;
+        } catch (Exception e) {
+            log.error("AliOSSUtil.uploadFile ERROR: {}", e.getMessage(), e);
+            return null;
+        } finally {
+            // 关闭输入流
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (Exception e) {
+                    log.warn("AliOSSUtil.uploadFile WARN: 关闭输入流失败: {}", e.getMessage());
+                }
+            }
+            if (ossClient != null) {
+                ossClient.shutdown();
+            }
+        }
+    }
+
 }

+ 682 - 0
alien-util/src/main/java/shop/alien/util/pdf/ImageToPdfUtil.java

@@ -0,0 +1,682 @@
+package shop.alien.util.pdf;
+
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDPage;
+import org.apache.pdfbox.pdmodel.PDPageContentStream;
+import org.apache.pdfbox.pdmodel.common.PDRectangle;
+import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
+
+import javax.imageio.ImageIO;
+import java.awt.image.BufferedImage;
+import java.io.*;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 图片转PDF工具类
+ * 支持单张图片转PDF、多张图片合并为一个PDF
+ * 支持从本地文件、URL、字节数组转换
+ *
+ * @author system
+ * @since 2026-01-05
+ */
+public class ImageToPdfUtil {
+
+    /**
+     * 将单张图片文件转换为PDF
+     *
+     * @param imagePath 图片文件路径
+     * @param pdfPath   PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdf(String imagePath, String pdfPath) {
+        return imageToPdf(new File(imagePath), new File(pdfPath));
+    }
+
+    /**
+     * 将单张图片文件转换为PDF
+     *
+     * @param imageFile 图片文件
+     * @param pdfFile   PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdf(File imageFile, File pdfFile) {
+        if (imageFile == null || !imageFile.exists()) {
+            throw new IllegalArgumentException("图片文件不存在: " + (imageFile != null ? imageFile.getPath() : "null"));
+        }
+
+        try (InputStream imageStream = new FileInputStream(imageFile)) {
+            return imageToPdf(imageStream, pdfFile);
+        } catch (IOException e) {
+            throw new RuntimeException("读取图片文件失败: " + imageFile.getPath(), e);
+        }
+    }
+
+    /**
+     * 将图片输入流转换为PDF
+     *
+     * @param imageStream 图片输入流
+     * @param pdfFile     PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdf(InputStream imageStream, File pdfFile) {
+        if (imageStream == null) {
+            throw new IllegalArgumentException("图片输入流不能为空");
+        }
+        if (pdfFile == null) {
+            throw new IllegalArgumentException("PDF文件不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            BufferedImage bufferedImage = ImageIO.read(imageStream);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建PDF页面,使用图片的尺寸
+            PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+            document.addPage(page);
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将图片URL转换为PDF
+     *
+     * @param imageUrl 图片URL
+     * @param pdfPath  PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlToPdf(String imageUrl, String pdfPath) {
+        return imageUrlToPdf(imageUrl, new File(pdfPath));
+    }
+
+    /**
+     * 将图片URL转换为PDF
+     *
+     * @param imageUrl 图片URL
+     * @param pdfFile  PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlToPdf(String imageUrl, File pdfFile) {
+        if (imageUrl == null || imageUrl.trim().isEmpty()) {
+            throw new IllegalArgumentException("图片URL不能为空");
+        }
+
+        HttpURLConnection connection = null;
+        try {
+            URL url = new URL(imageUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setConnectTimeout(10000); // 10秒连接超时
+            connection.setReadTimeout(30000);    // 30秒读取超时
+            connection.setRequestMethod("GET");
+            connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+            InputStream imageStream = connection.getInputStream();
+            try {
+                return imageToPdf(imageStream, pdfFile);
+            } finally {
+                if (imageStream != null) {
+                    imageStream.close();
+                }
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("从URL下载图片失败: " + imageUrl, e);
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 将图片字节数组转换为PDF
+     *
+     * @param imageBytes 图片字节数组
+     * @param pdfPath    PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageBytesToPdf(byte[] imageBytes, String pdfPath) {
+        return imageBytesToPdf(imageBytes, new File(pdfPath));
+    }
+
+    /**
+     * 将图片字节数组转换为PDF
+     *
+     * @param imageBytes 图片字节数组
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageBytesToPdf(byte[] imageBytes, File pdfFile) {
+        if (imageBytes == null || imageBytes.length == 0) {
+            throw new IllegalArgumentException("图片字节数组不能为空");
+        }
+
+        ByteArrayInputStream imageStream = new ByteArrayInputStream(imageBytes);
+        try {
+            return imageToPdf(imageStream, pdfFile);
+        } finally {
+            try {
+                imageStream.close();
+            } catch (IOException e) {
+                // ByteArrayInputStream的close()实际上不会抛出异常,但为了编译通过需要捕获
+            }
+        }
+    }
+
+    /**
+     * 将多张图片合并为一个PDF
+     *
+     * @param imagePaths 图片文件路径列表
+     * @param pdfPath    PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdf(List<String> imagePaths, String pdfPath) {
+        return imagesToPdfFromPaths(imagePaths, new File(pdfPath));
+    }
+
+    /**
+     * 将多张图片合并为一个PDF(从文件路径)
+     *
+     * @param imagePaths 图片文件路径列表
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdfFromPaths(List<String> imagePaths, File pdfFile) {
+        if (imagePaths == null || imagePaths.isEmpty()) {
+            throw new IllegalArgumentException("图片路径列表不能为空");
+        }
+
+        List<File> imageFiles = new ArrayList<>();
+        for (String imagePath : imagePaths) {
+            imageFiles.add(new File(imagePath));
+        }
+        return imagesToPdfFromFiles(imageFiles, pdfFile);
+    }
+
+    /**
+     * 将多张图片合并为一个PDF(从文件对象)
+     *
+     * @param imageFiles 图片文件列表
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdfFromFiles(List<File> imageFiles, File pdfFile) {
+        if (imageFiles == null || imageFiles.isEmpty()) {
+            throw new IllegalArgumentException("图片文件列表不能为空");
+        }
+        if (pdfFile == null) {
+            throw new IllegalArgumentException("PDF文件不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            for (File imageFile : imageFiles) {
+                if (imageFile == null || !imageFile.exists()) {
+                    continue; // 跳过不存在的文件
+                }
+
+                try (FileInputStream imageStream = new FileInputStream(imageFile)) {
+                    BufferedImage bufferedImage = ImageIO.read(imageStream);
+                    if (bufferedImage == null) {
+                        continue; // 跳过无法读取的图片
+                    }
+
+                    // 创建PDF页面,使用图片的尺寸
+                    PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+                    document.addPage(page);
+
+                    // 将图片添加到PDF页面
+                    PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                            document, imageToByteArray(bufferedImage), "image");
+
+                    try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                        contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片失败: " + imageFile.getPath() + ", 错误: " + e.getMessage());
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将多张图片URL合并为一个PDF
+     *
+     * @param imageUrls 图片URL列表
+     * @param pdfPath   PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlsToPdf(List<String> imageUrls, String pdfPath) {
+        return imageUrlsToPdf(imageUrls, new File(pdfPath));
+    }
+
+    /**
+     * 将多张图片URL合并为一个PDF
+     *
+     * @param imageUrls 图片URL列表
+     * @param pdfFile   PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageUrlsToPdf(List<String> imageUrls, File pdfFile) {
+        if (imageUrls == null || imageUrls.isEmpty()) {
+            throw new IllegalArgumentException("图片URL列表不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            for (String imageUrl : imageUrls) {
+                if (imageUrl == null || imageUrl.trim().isEmpty()) {
+                    continue; // 跳过空的URL
+                }
+
+                try {
+                    URL url = new URL(imageUrl);
+                    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+                    connection.setConnectTimeout(10000);
+                    connection.setReadTimeout(30000);
+                    connection.setRequestMethod("GET");
+                    connection.setRequestProperty("User-Agent", "Mozilla/5.0");
+
+                    InputStream imageStream = connection.getInputStream();
+                    try {
+                        BufferedImage bufferedImage = ImageIO.read(imageStream);
+                        if (bufferedImage == null) {
+                            continue; // 跳过无法读取的图片
+                        }
+
+                        // 创建PDF页面,使用图片的尺寸
+                        PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+                        document.addPage(page);
+
+                        // 将图片添加到PDF页面
+                        PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                                document, imageToByteArray(bufferedImage), "image");
+
+                        try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                            contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+                        }
+                    } finally {
+                        if (imageStream != null) {
+                            imageStream.close();
+                        }
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片URL失败: " + imageUrl + ", 错误: " + e.getMessage());
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片URL转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将图片转换为字节数组
+     *
+     * @param bufferedImage 图片对象
+     * @return 图片字节数组
+     */
+    private static byte[] imageToByteArray(BufferedImage bufferedImage) throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        String format = "png"; // 默认使用PNG格式
+        ImageIO.write(bufferedImage, format, baos);
+        return baos.toByteArray();
+    }
+
+    /**
+     * 将图片输入流转换为PDF字节数组(内存方式,不保存文件)
+     *
+     * @param imageStream 图片输入流
+     * @return PDF字节数组
+     */
+    public static byte[] imageToPdfBytes(InputStream imageStream) {
+        if (imageStream == null) {
+            throw new IllegalArgumentException("图片输入流不能为空");
+        }
+
+        try (PDDocument document = new PDDocument();
+             ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
+            BufferedImage bufferedImage = ImageIO.read(imageStream);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建PDF页面,使用图片的尺寸
+            PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+            document.addPage(page);
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+            }
+
+            // 将PDF保存到字节数组
+            document.save(pdfOutputStream);
+            return pdfOutputStream.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将图片输入流转换为PDF字节数组(A4页面大小,内存方式)
+     *
+     * @param imageStream 图片输入流
+     * @return PDF字节数组
+     */
+    public static byte[] imageToPdfA4Bytes(InputStream imageStream) {
+        if (imageStream == null) {
+            throw new IllegalArgumentException("图片输入流不能为空");
+        }
+
+        try (PDDocument document = new PDDocument();
+             ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
+            BufferedImage bufferedImage = ImageIO.read(imageStream);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建A4页面
+            PDPage page = new PDPage(PDRectangle.A4);
+            document.addPage(page);
+
+            // 计算图片在A4页面上的尺寸(保持宽高比)
+            float pageWidth = PDRectangle.A4.getWidth();
+            float pageHeight = PDRectangle.A4.getHeight();
+            float imageWidth = bufferedImage.getWidth();
+            float imageHeight = bufferedImage.getHeight();
+
+            // 计算缩放比例,使图片适应A4页面
+            float scaleX = pageWidth / imageWidth;
+            float scaleY = pageHeight / imageHeight;
+            float scale = Math.min(scaleX, scaleY);
+
+            float scaledWidth = imageWidth * scale;
+            float scaledHeight = imageHeight * scale;
+
+            // 居中显示
+            float x = (pageWidth - scaledWidth) / 2;
+            float y = (pageHeight - scaledHeight) / 2;
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, x, y, scaledWidth, scaledHeight);
+            }
+
+            // 将PDF保存到字节数组
+            document.save(pdfOutputStream);
+            return pdfOutputStream.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将单张图片转换为PDF(A4页面大小,图片自适应)
+     *
+     * @param imagePath 图片文件路径
+     * @param pdfPath   PDF文件保存路径
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdfA4(String imagePath, String pdfPath) {
+        return imageToPdfA4(new File(imagePath), new File(pdfPath));
+    }
+
+    /**
+     * 将单张图片转换为PDF(A4页面大小,图片自适应)
+     *
+     * @param imageFile 图片文件
+     * @param pdfFile   PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imageToPdfA4(File imageFile, File pdfFile) {
+        if (imageFile == null || !imageFile.exists()) {
+            throw new IllegalArgumentException("图片文件不存在: " + (imageFile != null ? imageFile.getPath() : "null"));
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            BufferedImage bufferedImage = ImageIO.read(imageFile);
+            if (bufferedImage == null) {
+                throw new IllegalArgumentException("无法读取图片,可能不是有效的图片格式");
+            }
+
+            // 创建A4页面
+            PDPage page = new PDPage(PDRectangle.A4);
+            document.addPage(page);
+
+            // 计算图片在A4页面上的尺寸(保持宽高比)
+            float pageWidth = PDRectangle.A4.getWidth();
+            float pageHeight = PDRectangle.A4.getHeight();
+            float imageWidth = bufferedImage.getWidth();
+            float imageHeight = bufferedImage.getHeight();
+
+            // 计算缩放比例,使图片适应A4页面
+            float scaleX = pageWidth / imageWidth;
+            float scaleY = pageHeight / imageHeight;
+            float scale = Math.min(scaleX, scaleY);
+
+            float scaledWidth = imageWidth * scale;
+            float scaledHeight = imageHeight * scale;
+
+            // 居中显示
+            float x = (pageWidth - scaledWidth) / 2;
+            float y = (pageHeight - scaledHeight) / 2;
+
+            // 将图片添加到PDF页面
+            PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                    document, imageToByteArray(bufferedImage), "image");
+
+            try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                contentStream.drawImage(pdImage, x, y, scaledWidth, scaledHeight);
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将多张图片合并为一个PDF(A4页面大小,每张图片一页,图片自适应)
+     *
+     * @param imageFiles 图片文件列表
+     * @param pdfFile    PDF文件
+     * @return 是否转换成功
+     */
+    public static boolean imagesToPdfA4(List<File> imageFiles, File pdfFile) {
+        if (imageFiles == null || imageFiles.isEmpty()) {
+            throw new IllegalArgumentException("图片文件列表不能为空");
+        }
+        if (pdfFile == null) {
+            throw new IllegalArgumentException("PDF文件不能为空");
+        }
+
+        // 确保PDF文件目录存在
+        File parentDir = pdfFile.getParentFile();
+        if (parentDir != null && !parentDir.exists()) {
+            parentDir.mkdirs();
+        }
+
+        try (PDDocument document = new PDDocument()) {
+            float pageWidth = PDRectangle.A4.getWidth();
+            float pageHeight = PDRectangle.A4.getHeight();
+
+            for (File imageFile : imageFiles) {
+                if (imageFile == null || !imageFile.exists()) {
+                    continue; // 跳过不存在的文件
+                }
+
+                try (FileInputStream imageStream = new FileInputStream(imageFile)) {
+                    BufferedImage bufferedImage = ImageIO.read(imageStream);
+                    if (bufferedImage == null) {
+                        continue; // 跳过无法读取的图片
+                    }
+
+                    // 创建A4页面
+                    PDPage page = new PDPage(PDRectangle.A4);
+                    document.addPage(page);
+
+                    // 计算图片在A4页面上的尺寸(保持宽高比)
+                    float imageWidth = bufferedImage.getWidth();
+                    float imageHeight = bufferedImage.getHeight();
+
+                    // 计算缩放比例,使图片适应A4页面
+                    float scaleX = pageWidth / imageWidth;
+                    float scaleY = pageHeight / imageHeight;
+                    float scale = Math.min(scaleX, scaleY);
+
+                    float scaledWidth = imageWidth * scale;
+                    float scaledHeight = imageHeight * scale;
+
+                    // 居中显示
+                    float x = (pageWidth - scaledWidth) / 2;
+                    float y = (pageHeight - scaledHeight) / 2;
+
+                    // 将图片添加到PDF页面
+                    PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                            document, imageToByteArray(bufferedImage), "image");
+
+                    try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                        contentStream.drawImage(pdImage, x, y, scaledWidth, scaledHeight);
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片失败: " + imageFile.getPath() + ", 错误: " + e.getMessage());
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            document.save(pdfFile);
+            return true;
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片转PDF失败", e);
+        }
+    }
+
+    /**
+     * 将多张图片输入流合并为一个PDF字节数组(内存方式,不保存文件)
+     *
+     * @param imageStreams 图片输入流列表
+     * @return PDF字节数组
+     */
+    public static byte[] imagesToPdfBytes(List<InputStream> imageStreams) {
+        if (imageStreams == null || imageStreams.isEmpty()) {
+            throw new IllegalArgumentException("图片输入流列表不能为空");
+        }
+
+        try (PDDocument document = new PDDocument();
+             ByteArrayOutputStream pdfOutputStream = new ByteArrayOutputStream()) {
+            
+            for (InputStream imageStream : imageStreams) {
+                if (imageStream == null) {
+                    continue; // 跳过空的输入流
+                }
+
+                try {
+                    BufferedImage bufferedImage = ImageIO.read(imageStream);
+                    if (bufferedImage == null) {
+                        continue; // 跳过无法读取的图片
+                    }
+
+                    // 创建PDF页面,使用图片的尺寸
+                    PDPage page = new PDPage(new PDRectangle(bufferedImage.getWidth(), bufferedImage.getHeight()));
+                    document.addPage(page);
+
+                    // 将图片添加到PDF页面
+                    PDImageXObject pdImage = PDImageXObject.createFromByteArray(
+                            document, imageToByteArray(bufferedImage), "image");
+
+                    try (PDPageContentStream contentStream = new PDPageContentStream(document, page)) {
+                        contentStream.drawImage(pdImage, 0, 0, bufferedImage.getWidth(), bufferedImage.getHeight());
+                    }
+                } catch (IOException e) {
+                    // 记录错误但继续处理其他图片
+                    System.err.println("处理图片输入流失败: " + e.getMessage());
+                } finally {
+                    // 关闭输入流
+                    if (imageStream != null) {
+                        try {
+                            imageStream.close();
+                        } catch (IOException e) {
+                            // 忽略关闭异常
+                        }
+                    }
+                }
+            }
+
+            if (document.getNumberOfPages() == 0) {
+                throw new RuntimeException("没有成功添加任何图片到PDF");
+            }
+
+            // 将PDF保存到字节数组
+            document.save(pdfOutputStream);
+            return pdfOutputStream.toByteArray();
+        } catch (IOException e) {
+            throw new RuntimeException("多张图片转PDF失败", e);
+        }
+    }
+}