Forráskód Böngészése

Merge branch 'release_20260123' into sit

# Conflicts:
#	alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java
#	alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
lutong 2 hónapja
szülő
commit
de275bb8c0
86 módosított fájl, 6899 hozzáadás és 190 törlés
  1. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/CommonComment.java
  2. 81 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreRenovationBrowseRecord.java
  3. 148 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreRenovationRequirement.java
  4. 95 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreTrackEvent.java
  5. 74 0
      alien-entity/src/main/java/shop/alien/entity/store/StoreTrackStatistics.java
  6. 1 1
      alien-entity/src/main/java/shop/alien/entity/store/TagsSynonym.java
  7. 84 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationBrowseRequirementDto.java
  8. 110 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationRequirementDto.java
  9. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/dto/StoreStaffConfigListQueryDto.java
  10. 5 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/CommonCommentVo.java
  11. 5 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeMessageVo.java
  12. 3 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/LifeUserVo.java
  13. 50 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/RatingPercentVo.java
  14. 3 2
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreMainInfoVo.java
  15. 27 0
      alien-entity/src/main/java/shop/alien/entity/store/vo/StoreQualificationInfoVo.java
  16. 1 1
      alien-entity/src/main/java/shop/alien/entity/storePlatform/StoreLicenseHistory.java
  17. 10 1
      alien-entity/src/main/java/shop/alien/mapper/CommonCommentMapper.java
  18. 7 4
      alien-entity/src/main/java/shop/alien/mapper/LifeUserMapper.java
  19. 33 0
      alien-entity/src/main/java/shop/alien/mapper/StoreQualificationMapper.java
  20. 17 0
      alien-entity/src/main/java/shop/alien/mapper/StoreRenovationBrowseRecordMapper.java
  21. 17 0
      alien-entity/src/main/java/shop/alien/mapper/StoreRenovationRequirementMapper.java
  22. 21 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTagDetailMapper.java
  23. 15 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTrackEventMapper.java
  24. 15 0
      alien-entity/src/main/java/shop/alien/mapper/StoreTrackStatisticsMapper.java
  25. 171 0
      alien-entity/src/main/resources/db/migration/renovation_chat_analysis.md
  26. 38 0
      alien-entity/src/main/resources/db/migration/store_renovation_requirement.sql
  27. 67 0
      alien-entity/src/main/resources/mapper/StoreQualificationMapper.xml
  28. 50 0
      alien-entity/src/main/resources/mapper/StoreTagDetailMapper.xml
  29. 59 0
      alien-store/doc/埋点接口清单.md
  30. 302 0
      alien-store/doc/埋点统计数据JSON格式说明.md
  31. 449 0
      alien-store/doc/埋点需求完整方案.md
  32. 4 0
      alien-store/pom.xml
  33. 61 0
      alien-store/src/main/java/shop/alien/store/annotation/TrackEvent.java
  34. 557 0
      alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java
  35. 2 0
      alien-store/src/main/java/shop/alien/store/config/WebSocketProcess.java
  36. 11 3
      alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java
  37. 7 0
      alien-store/src/main/java/shop/alien/store/controller/CommentAppealController.java
  38. 66 4
      alien-store/src/main/java/shop/alien/store/controller/CommonCommentController.java
  39. 31 5
      alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java
  40. 9 0
      alien-store/src/main/java/shop/alien/store/controller/LifeBlacklistController.java
  41. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCollectController.java
  42. 9 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCommentController.java
  43. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeCouponController.java
  44. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponStoreFriendController.java
  45. 7 0
      alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java
  46. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreClockInController.java
  47. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java
  48. 20 0
      alien-store/src/main/java/shop/alien/store/controller/StoreInfoController.java
  49. 7 0
      alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java
  50. 259 0
      alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java
  51. 2 1
      alien-store/src/main/java/shop/alien/store/controller/StoreStaffConfigController.java
  52. 11 0
      alien-store/src/main/java/shop/alien/store/controller/TagCategoryController.java
  53. 141 0
      alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java
  54. 7 0
      alien-store/src/main/java/shop/alien/store/controller/UserStoreController.java
  55. 9 0
      alien-store/src/main/java/shop/alien/store/controller/UserViolationController.java
  56. 38 1
      alien-store/src/main/java/shop/alien/store/service/CommonCommentService.java
  57. 14 2
      alien-store/src/main/java/shop/alien/store/service/CommonRatingService.java
  58. 2 2
      alien-store/src/main/java/shop/alien/store/service/LifeCommentService.java
  59. 21 14
      alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java
  60. 2 0
      alien-store/src/main/java/shop/alien/store/service/LifeUserService.java
  61. 21 0
      alien-store/src/main/java/shop/alien/store/service/StoreQualificationService.java
  62. 52 0
      alien-store/src/main/java/shop/alien/store/service/StoreRenovationBrowseRecordService.java
  63. 105 0
      alien-store/src/main/java/shop/alien/store/service/StoreRenovationRequirementService.java
  64. 20 0
      alien-store/src/main/java/shop/alien/store/service/StoreTagDetailService.java
  65. 84 0
      alien-store/src/main/java/shop/alien/store/service/TrackEventConsumer.java
  66. 38 0
      alien-store/src/main/java/shop/alien/store/service/TrackEventService.java
  67. 171 0
      alien-store/src/main/java/shop/alien/store/service/TrackStatisticsScheduler.java
  68. 117 8
      alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceServiceImpl.java
  69. 10 3
      alien-store/src/main/java/shop/alien/store/service/impl/CommentAppealServiceImpl.java
  70. 208 3
      alien-store/src/main/java/shop/alien/store/service/impl/CommonCommentServiceImpl.java
  71. 336 85
      alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java
  72. 62 5
      alien-store/src/main/java/shop/alien/store/service/impl/LicenseAuditAsyncService.java
  73. 75 1
      alien-store/src/main/java/shop/alien/store/service/impl/LifeMessageServiceImpl.java
  74. 20 4
      alien-store/src/main/java/shop/alien/store/service/impl/SportsEquipmentFacilityServiceImpl.java
  75. 2 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCommentServiceImpl.java
  76. 15 1
      alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineServiceImpl.java
  77. 626 36
      alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java
  78. 51 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreQualificationServiceImpl.java
  79. 307 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationBrowseRecordServiceImpl.java
  80. 666 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java
  81. 2 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreStaffConfigServiceImpl.java
  82. 34 0
      alien-store/src/main/java/shop/alien/store/service/impl/StoreTagDetailServiceImpl.java
  83. 391 0
      alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java
  84. 1 0
      alien-store/src/main/java/shop/alien/store/util/CommonConstant.java
  85. 189 0
      alien-store/src/main/java/shop/alien/store/util/UserAgentParserUtil.java
  86. 2 2
      alien-util/src/main/java/shop/alien/util/common/constant/CommentSourceTypeEnum.java

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

@@ -28,7 +28,7 @@ public class CommonComment implements Serializable {
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
-    @ApiModelProperty(value = "评论来源类型:1-评价的评论 2-社区动态 3-活动留言")
+    @ApiModelProperty(value = "评论来源类型:1-评价的评论 2-动态评论")
     @TableField("source_type")
     private Integer sourceType;
 

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

@@ -0,0 +1,81 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+/**
+ * 装修商铺访问装修需求浏览记录表
+ * <p>
+ * 记录装修商铺(装修公司)查看/访问装修需求动态的历史记录
+ * </p>
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@TableName("store_renovation_browse_record")
+@ApiModel(value = "StoreRenovationBrowseRecord对象", description = "装修商铺访问装修需求浏览记录表")
+public class StoreRenovationBrowseRecord {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "装修商铺ID(访问者,装修公司的门店ID)")
+    @TableField("renovation_store_id")
+    private Integer renovationStoreId;
+
+    @ApiModelProperty(value = "装修需求ID(被访问的装修需求)")
+    @TableField("requirement_id")
+    private Integer requirementId;
+
+    @ApiModelProperty(value = "访问时间")
+    @TableField("browse_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date browseTime;
+
+    @ApiModelProperty(value = "是否已联系(0:未联系, 1:已联系)")
+    @TableField("is_contacted")
+    private Integer isContacted;
+
+    @ApiModelProperty(value = "联系时间(首次联系时间)")
+    @TableField("contact_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date contactTime;
+
+    @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")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 148 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreRenovationRequirement.java

@@ -0,0 +1,148 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 装修需求动态表
+ * <p>
+ * 注意:装修需求的图片和视频附件直接存储在本表的 attachment_urls 字段中
+ * 存储格式:多个URL用逗号拼接,例如:"url1,url2,url3"
+ * 最多支持9个附件(图片或视频)
+ * </p>
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@TableName("store_renovation_requirement")
+@ApiModel(value = "StoreRenovationRequirement对象", description = "装修需求动态表")
+public class StoreRenovationRequirement {
+
+    @ApiModelProperty(value = "主键")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID(发布装修需求的普通公司门店)")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "需求标题")
+    @TableField("requirement_title")
+    private String requirementTitle;
+
+    @ApiModelProperty(value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)")
+    @TableField("renovation_type")
+    private Integer renovationType;
+
+    @ApiModelProperty(value = "房屋面积(平方米)")
+    @TableField("house_area")
+    private BigDecimal houseArea;
+
+    @ApiModelProperty(value = "装修预算(万元)")
+    @TableField("renovation_budget")
+    private BigDecimal renovationBudget;
+
+    @ApiModelProperty(value = "详细需求描述")
+    @TableField("detailed_requirement")
+    private String detailedRequirement;
+
+    @ApiModelProperty(value = "房屋图纸附件(图片和视频URL,多个URL用逗号拼接,最多9个)")
+    @TableField("attachment_urls")
+    private String attachmentUrls;
+
+    @ApiModelProperty(value = "期望装修时间")
+    @TableField("expected_renovation_time")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private Date expectedRenovationTime;
+
+    @ApiModelProperty(value = "联系人姓名")
+    @TableField("contact_name")
+    private String contactName;
+
+    @ApiModelProperty(value = "联系电话")
+    @TableField("contact_phone")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "所在城市")
+    @TableField("city")
+    private String city;
+
+    @ApiModelProperty(value = "城市编码(adcode)")
+    @TableField("city_adcode")
+    private String cityAdcode;
+
+    @ApiModelProperty(value = "详细地址")
+    @TableField("detailed_address")
+    private String detailedAddress;
+
+    @ApiModelProperty(value = "服务协议确认(0:未确认, 1:已确认)")
+    @TableField("agreement_confirmed")
+    private Integer agreementConfirmed;
+
+    @ApiModelProperty(value = "动态状态(0:草稿, 1:已发布, 2:已下架)")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty(value = "浏览数")
+    @TableField("view_count")
+    private Integer viewCount;
+
+    @ApiModelProperty(value = "咨询数(装修商家查看并联系的数量)")
+    @TableField("inquiry_count")
+    private Integer inquiryCount;
+
+    @ApiModelProperty(value = "审核状态(0:待审核, 1:审核通过, 2:审核失败)")
+    @TableField("audit_status")
+    private Integer auditStatus;
+
+    @ApiModelProperty(value = "审核失败原因")
+    @TableField("audit_reason")
+    private String auditReason;
+
+    @ApiModelProperty(value = "审核时间")
+    @TableField("audit_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date auditTime;
+
+    @ApiModelProperty(value = "审核人ID")
+    @TableField("audit_user_id")
+    private Integer auditUserId;
+
+    @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")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "创建人ID")
+    @TableField("created_user_id")
+    private Integer createdUserId;
+
+    @ApiModelProperty(value = "修改时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    @ApiModelProperty(value = "修改人ID")
+    @TableField("updated_user_id")
+    private Integer updatedUserId;
+}
+

+ 95 - 0
alien-entity/src/main/java/shop/alien/entity/store/StoreTrackEvent.java

@@ -0,0 +1,95 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 埋点事件实体类
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Data
+@JsonInclude
+@TableName("store_track_event")
+@ApiModel(value = "StoreTrackEvent对象", description = "埋点事件表")
+public class StoreTrackEvent {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "事件类型")
+    @TableField("event_type")
+    private String eventType;
+
+    @ApiModelProperty(value = "事件分类")
+    @TableField("event_category")
+    private String eventCategory;
+
+    @ApiModelProperty(value = "用户ID")
+    @TableField("user_id")
+    private Integer userId;
+
+    @ApiModelProperty(value = "店铺ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "目标对象ID")
+    @TableField("target_id")
+    private Integer targetId;
+
+    @ApiModelProperty(value = "目标对象类型")
+    @TableField("target_type")
+    private String targetType;
+
+    @ApiModelProperty(value = "事件附加数据(JSON格式)")
+    @TableField("event_data")
+    private String eventData;
+
+    @ApiModelProperty(value = "金额")
+    @TableField("amount")
+    private BigDecimal amount;
+
+    @ApiModelProperty(value = "时长(毫秒)")
+    @TableField(value = "duration", insertStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.IGNORED, updateStrategy = com.baomidou.mybatisplus.annotation.FieldStrategy.IGNORED)
+    private Long duration;
+
+    @ApiModelProperty(value = "IP地址")
+    @TableField("ip_address")
+    private String ipAddress;
+
+    @ApiModelProperty(value = "用户代理")
+    @TableField("user_agent")
+    private String userAgent;
+
+    @ApiModelProperty(value = "设备类型")
+    @TableField("device_type")
+    private String deviceType;
+
+    @ApiModelProperty(value = "APP版本号")
+    @TableField("app_version")
+    private String appVersion;
+
+    @ApiModelProperty(value = "事件发生时间")
+    @TableField("event_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date eventTime;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "删除标记, 0:未删除, 1:已删除")
+    @TableField("delete_flag")
+    @TableLogic
+    private Integer deleteFlag;
+}

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

@@ -0,0 +1,74 @@
+package shop.alien.entity.store;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 埋点统计数据实体类
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Data
+@JsonInclude
+@TableName("store_track_statistics")
+@ApiModel(value = "StoreTrackStatistics对象", description = "埋点统计表")
+public class StoreTrackStatistics {
+
+    @ApiModelProperty(value = "主键ID")
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty(value = "店铺ID")
+    @TableField("store_id")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "统计日期")
+    @TableField("stat_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date statDate;
+
+    @ApiModelProperty(value = "统计类型")
+    @TableField("stat_type")
+    private String statType;
+
+    @ApiModelProperty(value = "流量数据(JSON格式)")
+    @TableField("traffic_data")
+    private String trafficData;
+
+    @ApiModelProperty(value = "互动数据(JSON格式)")
+    @TableField("interaction_data")
+    private String interactionData;
+
+    @ApiModelProperty(value = "优惠券数据(JSON格式)")
+    @TableField("coupon_data")
+    private String couponData;
+
+    @ApiModelProperty(value = "代金券数据(JSON格式)")
+    @TableField("voucher_data")
+    private String voucherData;
+
+    @ApiModelProperty(value = "服务质量数据(JSON格式)")
+    @TableField("service_data")
+    private String serviceData;
+
+    @ApiModelProperty(value = "价目表排名数据(JSON格式)")
+    @TableField("price_ranking_data")
+    private String priceRankingData;
+
+    @ApiModelProperty(value = "创建时间")
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "更新时间")
+    @TableField(value = "updated_time", fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date updatedTime;
+}

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

@@ -27,7 +27,7 @@ public class TagsSynonym {
     @TableField("main_tag_id")
     private Integer mainTagId;
 
-    @ApiModelProperty(value = "评论ID")
+    @ApiModelProperty(value = "评论ID(关联common_comment.id)")
     @TableField("comment_id")
     private Integer commentId;
 

+ 84 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/StoreRenovationBrowseRequirementDto.java

@@ -0,0 +1,84 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+import shop.alien.entity.store.dto.deserializer.StringToListDeserializer;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 装修商铺浏览过的装修需求DTO(包含浏览信息)
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "StoreRenovationBrowseRequirementDto对象", description = "装修商铺浏览过的装修需求DTO")
+public class StoreRenovationBrowseRequirementDto {
+
+    @ApiModelProperty(value = "装修需求ID")
+    private Integer requirementId;
+
+    @ApiModelProperty(value = "需求标题")
+    private String requirementTitle;
+
+    @ApiModelProperty(value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)")
+    private Integer renovationType;
+
+    @ApiModelProperty(value = "房屋面积(平方米)")
+    private BigDecimal houseArea;
+
+    @ApiModelProperty(value = "装修预算(万元)")
+    private BigDecimal renovationBudget;
+
+    @ApiModelProperty(value = "详细需求描述")
+    private String detailedRequirement;
+
+    @ApiModelProperty(value = "期望装修时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private Date expectedRenovationTime;
+
+    @ApiModelProperty(value = "联系人姓名")
+    private String contactName;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "所在城市")
+    private String city;
+
+    @ApiModelProperty(value = "详细地址")
+    private String detailedAddress;
+
+    @ApiModelProperty(value = "附件列表(图片/视频URL列表)")
+    @JsonDeserialize(using = StringToListDeserializer.class)
+    private List<String> attachmentUrls;
+
+    @ApiModelProperty(value = "浏览时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date browseTime;
+
+    @ApiModelProperty(value = "是否已联系(0:未联系, 1:已联系)")
+    private Integer isContacted;
+
+    @ApiModelProperty(value = "联系时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date contactTime;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+}
+

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

@@ -0,0 +1,110 @@
+package shop.alien.entity.store.dto;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+import shop.alien.entity.store.dto.deserializer.StringToListDeserializer;
+
+import java.math.BigDecimal;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 装修需求动态DTO
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "StoreRenovationRequirementDto对象", description = "装修需求动态DTO")
+public class StoreRenovationRequirementDto {
+
+    @ApiModelProperty(value = "主键")
+    private Integer id;
+
+    @ApiModelProperty(value = "门店ID")
+    private Integer storeId;
+
+    @ApiModelProperty(value = "需求标题")
+    private String requirementTitle;
+
+    @ApiModelProperty(value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)")
+    private Integer renovationType;
+
+    @ApiModelProperty(value = "房屋面积(平方米)")
+    private BigDecimal houseArea;
+
+    @ApiModelProperty(value = "装修预算(万元)")
+    private BigDecimal renovationBudget;
+
+    @ApiModelProperty(value = "详细需求描述")
+    private String detailedRequirement;
+
+    @ApiModelProperty(value = "期望装修时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd")
+    private Date expectedRenovationTime;
+
+    @ApiModelProperty(value = "联系人姓名")
+    private String contactName;
+
+    @ApiModelProperty(value = "联系电话")
+    private String contactPhone;
+
+    @ApiModelProperty(value = "所在城市")
+    private String city;
+
+    @ApiModelProperty(value = "城市编码(adcode)")
+    private String cityAdcode;
+
+    @ApiModelProperty(value = "详细地址")
+    private String detailedAddress;
+
+    @ApiModelProperty(value = "服务协议确认(0:未确认, 1:已确认)")
+    private Integer agreementConfirmed;
+
+    @ApiModelProperty(value = "动态状态(0:草稿, 1:已发布, 2:已下架)")
+    private Integer status;
+
+    @ApiModelProperty(value = "浏览数")
+    private Integer viewCount;
+
+    @ApiModelProperty(value = "咨询数")
+    private Integer inquiryCount;
+
+    @ApiModelProperty(value = "附件列表(图片/视频URL列表)")
+    @JsonDeserialize(using = StringToListDeserializer.class)
+    private List<String> attachmentUrls;
+
+    @ApiModelProperty(value = "创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date createdTime;
+
+    @ApiModelProperty(value = "修改时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date updatedTime;
+
+    // --- 商铺信息字段 ---
+    @ApiModelProperty(value = "商铺名称")
+    private String storeName;
+
+    @ApiModelProperty(value = "商铺电话")
+    private String storeTel;
+
+    @ApiModelProperty(value = "商铺地址")
+    private String storeAddress;
+
+    @ApiModelProperty(value = "商铺简介")
+    private String storeBlurb;
+
+    @ApiModelProperty(value = "是否已与发布商铺发生沟通(true:已沟通, false:未沟通)")
+    private Boolean hasCommunicated;
+}
+

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

@@ -30,6 +30,9 @@ public class StoreStaffConfigListQueryDto {
 
     @ApiModelProperty(value = "职位")
     private String staffPosition;
+
+    @ApiModelProperty(value = "员工名称")
+    private String staffName;
 }
 
 

+ 5 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonCommentVo.java

@@ -18,4 +18,9 @@ public class CommonCommentVo extends CommonComment {
     private String headName;
     @ApiModelProperty(value = "是否点赞")
     private String isLike;
+    @ApiModelProperty(value = "来源关联id")
+    private Long sourceId;
+    @ApiModelProperty(value = "数量")
+    private Integer commentCount;
+
 }

+ 5 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/LifeMessageVo.java

@@ -4,10 +4,12 @@ import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
+import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
 import shop.alien.entity.store.LifeMessage;
 
 @Data
+@EqualsAndHashCode(callSuper = true)
 @JsonInclude
 @NoArgsConstructor
 @ApiModel(value = "LifeMessageVo对象", description = "消息通知")
@@ -51,4 +53,7 @@ public class LifeMessageVo extends LifeMessage {
 
     @ApiModelProperty(value = "发送方图片展示")
     private String senderImg;
+
+    @ApiModelProperty(value = "是否允许继续聊天(1-允许,0-不允许)")
+    private String canChat;
 }

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

@@ -62,6 +62,9 @@ public class LifeUserVo extends LifeUser {
     @ApiModelProperty(value = "简介")
     private String blurb;
 
+    @ApiModelProperty(value = "账户名称(商户昵称或用户名称)")
+    private String accountName;
+
     @ApiModelProperty(value = "验证码")
     private String verificationCode;
 }

+ 50 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/RatingPercentVo.java

@@ -0,0 +1,50 @@
+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 java.io.Serializable;
+
+/**
+ * 评价回复率和评价比例VO
+ *
+ * @author system
+ * @since 2025-01-XX
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "RatingPercentVo对象", description = "评价回复率和评价比例")
+public class RatingPercentVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "总评价数")
+    private Integer totalRatingCount;
+
+    @ApiModelProperty(value = "好评数")
+    private Integer goodCount;
+
+    @ApiModelProperty(value = "中评数")
+    private Integer midCount;
+
+    @ApiModelProperty(value = "差评数")
+    private Integer badCount;
+
+    @ApiModelProperty(value = "好评占比(%)")
+    private Double goodPercent;
+
+    @ApiModelProperty(value = "中评占比(%)")
+    private Double midPercent;
+
+    @ApiModelProperty(value = "差评占比(%)")
+    private Double badPercent;
+
+    @ApiModelProperty(value = "已回复的评价数")
+    private Integer repliedCount;
+
+    @ApiModelProperty(value = "回复率(%)")
+    private Double replyRate;
+}
+

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

@@ -1,7 +1,5 @@
 package shop.alien.entity.store.vo;
 
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
@@ -120,4 +118,7 @@ public class StoreMainInfoVo extends StoreInfo {
     @ApiModelProperty(value = "其他资质证明图片列表")
     private List<StoreImg> otherQualificationImages;
 
+    @ApiModelProperty(value = "评价数量")
+    private Integer ratingCount;
+
 }

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/StoreQualificationInfoVo.java

@@ -0,0 +1,27 @@
+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.StoreImg;
+
+import java.util.List;
+
+/**
+ * 门店资质信息 VO
+ *
+ * @author system
+ * @since 2025-01-01
+ */
+@Data
+@ApiModel(value = "StoreQualificationInfoVo对象", description = "门店资质信息")
+@JsonInclude(JsonInclude.Include.ALWAYS)
+public class StoreQualificationInfoVo {
+
+    @ApiModelProperty(value = "营业执照列表(img_type = 14)")
+    private List<StoreImg> businessLicenseList;
+
+    @ApiModelProperty(value = "其他资质证明图片列表(img_type = 35)")
+    private List<StoreImg> otherQualificationList;
+}

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/storePlatform/StoreLicenseHistory.java

@@ -34,7 +34,7 @@ public class StoreLicenseHistory extends Model<StoreLicenseHistory> {
     @TableField("license_status")
     private Integer licenseStatus;
 
-    @ApiModelProperty(value = "审核状态: 1-审核中, 2-审核拒绝, 3-审核通过")
+    @ApiModelProperty(value = "审核状态: 1-审核通过, 2-审核中, 3-审核拒绝")
     @TableField("license_execute_status")
     private Integer licenseExecuteStatus;
 

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

@@ -3,6 +3,7 @@ package shop.alien.mapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.toolkit.Constants;
+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;
@@ -33,8 +34,16 @@ public interface CommonCommentMapper extends BaseMapper<CommonComment> {
             "and llr.dianzan_id = #{dianzanId}\n" +
             "and llr.delete_flag = 0\n" +
             "${ew.customSqlSegment}")
-    List<CommonCommentVo> selectALlComment(@Param(Constants.WRAPPER)QueryWrapper<CommonCommentVo> wrapper,
+    List<CommonCommentVo> selectALlComment(@Param("page") Page<CommonCommentVo> page,
+                                           @Param(Constants.WRAPPER)QueryWrapper<CommonCommentVo> wrapper,
                                            @Param("type") String type,
                                            @Param("dianzanId") Long dianzanId);
+
+    @Select("select cc.source_id sourceId,count(*) commentCount\n" +
+            "from common_comment cc\n" +
+            "where \n" +
+            "cc.source_type = #{businessType}\n" +
+            "group by source_id")
+    List<CommonCommentVo> getCommentCount(Integer type);
 }
 

+ 7 - 4
alien-entity/src/main/java/shop/alien/mapper/LifeUserMapper.java

@@ -35,10 +35,13 @@ public interface LifeUserMapper extends BaseMapper<LifeUser> {
     LifeFansVo getUserInfoByPhoneIdList(@Param("phoneId") String phoneId);
 
     @Select("SELECT * FROM ( " +
-            " SELECT store.id, concat( 'store_', USER.phone ) phoneId, USER.head_img imgUrl, store_blurb blurb, " +
-            " if(store.id is null,USER.name ,store.store_name) storeUserName " +
-            " FROM store_user USER LEFT JOIN store_info store ON USER.store_id = store.id  and USER.delete_flag = 0 and  store.delete_flag = 0  " +
-            " UNION ALL SELECT id, concat( 'user_', user_phone ) phoneId, user_image imgUrl, jianjie blurb, user_name NAME " +
+            " SELECT store.id, concat( 'store_', USER.phone ) phoneId, USER.head_img imgUrl, " +
+            " IF(store.id IS NULL, USER.account_blurb, store.store_blurb) blurb, " +
+            " IF(store.id IS NULL, USER.nick_name, store.store_name) storeUserName, " +
+            " USER.nick_name accountName " +
+            " FROM store_user USER LEFT JOIN store_info store ON USER.store_id = store.id " +
+            " WHERE USER.delete_flag = 0 AND (store.id IS NULL OR store.delete_flag = 0) " +
+            " UNION ALL SELECT id, concat( 'user_', user_phone ) phoneId, user_image imgUrl, jianjie blurb, user_name NAME, user_name accountName " +
             " FROM life_user WHERE delete_flag = 0 ) a " +
             " ${ew.customSqlSegment}")
     IPage<LifeUserVo> getStoreAndUserByName(IPage<LifeUserVo> iPage, @Param(Constants.WRAPPER) QueryWrapper<LifeUserVo> wrapper);

+ 33 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreQualificationMapper.java

@@ -0,0 +1,33 @@
+package shop.alien.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.StoreImg;
+
+import java.util.List;
+
+/**
+ * 门店资质信息 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-01
+ */
+public interface StoreQualificationMapper {
+    
+    /**
+     * 根据门店id和图片类型查询图片列表
+     *
+     * @param storeId 门店id
+     * @param imgType 图片类型
+     * @return 图片列表
+     */
+    List<StoreImg> selectStoreImgsByType(@Param("storeId") Integer storeId, @Param("imgType") Integer imgType);
+    
+    /**
+     * 根据门店id和多个图片类型查询图片列表
+     *
+     * @param storeId 门店id
+     * @param imgTypes 图片类型数组
+     * @return 图片列表
+     */
+    List<StoreImg> selectStoreImgsByTypes(@Param("storeId") Integer storeId, @Param("imgTypes") Integer[] imgTypes);
+}

+ 17 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreRenovationBrowseRecordMapper.java

@@ -0,0 +1,17 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreRenovationBrowseRecord;
+
+/**
+ * 装修商铺访问装修需求浏览记录表 Mapper 接口
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Mapper
+public interface StoreRenovationBrowseRecordMapper extends BaseMapper<StoreRenovationBrowseRecord> {
+
+}
+

+ 17 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreRenovationRequirementMapper.java

@@ -0,0 +1,17 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreRenovationRequirement;
+
+/**
+ * 装修需求动态表 Mapper 接口
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Mapper
+public interface StoreRenovationRequirementMapper extends BaseMapper<StoreRenovationRequirement> {
+
+}
+

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

@@ -0,0 +1,21 @@
+package shop.alien.mapper;
+
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.store.TagStoreRelation;
+
+/**
+ * 店铺标签详情 Mapper 接口
+ *
+ * @author system
+ * @since 2025-01-01
+ */
+public interface StoreTagDetailMapper {
+    
+    /**
+     * 根据门店id查询店铺标签详情
+     *
+     * @param storeId 门店id
+     * @return 店铺标签详情
+     */
+    TagStoreRelation selectStoreTagDetailByStoreId(@Param("storeId") Integer storeId);
+}

+ 15 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreTrackEventMapper.java

@@ -0,0 +1,15 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreTrackEvent;
+
+/**
+ * 埋点事件Mapper接口
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Mapper
+public interface StoreTrackEventMapper extends BaseMapper<StoreTrackEvent> {
+}

+ 15 - 0
alien-entity/src/main/java/shop/alien/mapper/StoreTrackStatisticsMapper.java

@@ -0,0 +1,15 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.store.StoreTrackStatistics;
+
+/**
+ * 埋点统计数据Mapper接口
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Mapper
+public interface StoreTrackStatisticsMapper extends BaseMapper<StoreTrackStatistics> {
+}

+ 171 - 0
alien-entity/src/main/resources/db/migration/renovation_chat_analysis.md

@@ -0,0 +1,171 @@
+# 装修需求聊天功能分析报告
+
+## 一、现有系统支持情况
+
+### ✅ 已支持的功能
+
+1. **聊天表结构(LifeMessage)**
+   - 支持 `user_` + 手机号 格式(用户端)
+   - 支持 `store_` + 手机号 格式(商户端)
+   - 有 `businessId` 字段可关联装修需求ID
+   - 支持多种消息类型(文本、图片、链接等)
+   - 支持已读/未读状态管理
+
+2. **WebSocket实时通信**
+   - 已实现 WebSocket 实时消息推送
+   - 消息保存时可通过 `businessId` 关联装修需求
+
+3. **现有消息接口**
+   - `/message/getMessageList` - 获取消息列表
+   - `/message/getMessageListByReceiverId` - 获取聊天详情
+   - `/message/getAllNoReadCount` - 获取未读消息数
+
+## 二、需要补充的功能
+
+### 1. 从装修需求发起聊天接口
+
+**需求场景:**
+- 装修商铺查看装修需求后,点击"联系商家"按钮发起聊天
+- 商户查看装修需求后,点击"联系装修商"按钮发起聊天
+
+**需要实现的接口:**
+```
+POST /renovation/startChat
+参数:
+  - requirementId: 装修需求ID
+  - currentUserId: 当前登录用户ID(商户或装修商铺)
+  
+返回:
+  - senderId: 发送方ID(格式:store_手机号 或 user_手机号)
+  - receiverId: 接收方ID(格式:store_手机号 或 user_手机号)
+  - chatInitiated: 是否首次发起聊天(用于统计咨询数)
+```
+
+**实现逻辑:**
+1. 根据 requirementId 查询装修需求信息
+2. 获取装修需求的联系信息(contactPhone)
+3. 根据 store_id 查询装修商铺的 StoreUser,获取手机号
+4. 根据 contactPhone 查询对应的用户(LifeUser 或 StoreUser)
+5. 构造发送方和接收方的ID(user_ 或 store_ + 手机号)
+6. 如果是首次发起聊天,更新 inquiry_count + 1
+7. 返回对方ID,前端可直接跳转到聊天页面
+
+### 2. ID格式转换逻辑
+
+**问题:**
+- `StoreRenovationRequirement.contactPhone` 存储的是普通手机号
+- `StoreRenovationRequirement.store_id` 关联装修商铺
+- 需要转换为聊天系统需要的格式(`user_`/`store_` + 手机号)
+
+**解决方案:**
+```java
+// 1. 获取装修商铺的ID
+StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+StoreUser storeUser = storeUserMapper.selectOne(
+    new QueryWrapper<StoreUser>().eq("store_id", storeId)
+);
+String renovationStoreId = "store_" + storeUser.getPhone();
+
+// 2. 获取发布需求的商户ID
+// 方式1:通过 contactPhone 查找
+LifeUser lifeUser = lifeUserMapper.selectOne(
+    new QueryWrapper<LifeUser>().eq("user_phone", contactPhone)
+);
+String merchantId = "user_" + contactPhone;
+
+// 方式2:如果有 created_user_id,可通过 StoreUser 查找
+StoreUser merchantStoreUser = storeUserMapper.selectById(createdUserId);
+String merchantId = "store_" + merchantStoreUser.getPhone();
+```
+
+### 3. 消息类型扩展
+
+**当前消息类型:**
+- 1-文本
+- 2-图片
+- 3-链接
+- 4-12: 二手交易相关
+- 8: 律师消息
+
+**建议新增:**
+- 13: 装修需求咨询消息(可在消息列表显示特殊标识)
+- 或直接使用 type=1(文本),通过 businessId 区分业务场景
+
+### 4. 咨询数统计
+
+**实现位置:**
+在发起聊天时(首次聊天),更新 `inquiry_count` 字段:
+```java
+// 检查是否已有聊天记录
+LambdaQueryWrapper<LifeMessage> wrapper = new LambdaQueryWrapper<>();
+wrapper.eq(LifeMessage::getBusinessId, requirementId);
+wrapper.eq(LifeMessage::getSenderId, senderId);
+wrapper.eq(LifeMessage::getReceiverId, receiverId);
+long chatCount = lifeMessageMapper.selectCount(wrapper);
+
+if (chatCount == 0) {
+    // 首次聊天,增加咨询数
+    StoreRenovationRequirement requirement = requirementMapper.selectById(requirementId);
+    requirement.setInquiryCount(requirement.getInquiryCount() + 1);
+    requirementMapper.updateById(requirement);
+}
+```
+
+### 5. 消息列表展示装修需求信息
+
+**需求:**
+在消息列表中,如果是装修需求相关的聊天,显示装修需求标题等信息
+
+**实现方式:**
+- 查询消息列表时,通过 `businessId` 关联查询装修需求信息
+- 在 LifeMessageVo 中添加装修需求相关字段(可选)
+
+## 三、建议实现的接口
+
+### 1. 发起装修需求聊天接口
+```
+POST /renovation/startChat
+```
+
+### 2. 获取装修需求聊天信息接口
+```
+GET /renovation/getChatInfo/{requirementId}
+返回:发送方ID、接收方ID、是否已发起过聊天等
+```
+
+### 3. WebSocket消息扩展
+在发送消息时,确保 `businessId` 字段设置为装修需求ID:
+```java
+lifeMessage.setBusinessId(requirementId);
+```
+
+## 四、数据库字段说明
+
+### StoreRenovationRequirement 表
+- `id`: 装修需求ID(用作 businessId)
+- `store_id`: 装修商铺ID(可通过此ID查询装修商铺的StoreUser获取手机号)
+- `contact_phone`: 发布需求的商户手机号
+- `created_user_id`: 创建用户ID(可辅助查找商户)
+
+### LifeMessage 表
+- `sender_id`: 发送方ID(user_手机号 或 store_手机号)
+- `receiver_id`: 接收方ID(user_手机号 或 store_手机号)
+- `business_id`: 业务ID(设置为装修需求ID)
+- `type`: 消息类型(建议使用 1-文本,或新增 13-装修需求咨询)
+
+## 五、总结
+
+**现状:** 基础的聊天功能已完整支持,可直接使用 LifeMessage 表进行聊天。
+
+**待补充:** 
+1. 从装修需求页面发起聊天的便捷接口
+2. ID格式转换逻辑(手机号 -> user_/store_ + 手机号)
+3. 咨询数统计功能
+4. 可选:装修需求消息类型标识
+
+**优先级:**
+- 🔴 高:发起聊天接口、ID格式转换
+- 🟡 中:咨询数统计
+- 🟢 低:消息类型扩展、消息列表展示装修需求信息
+
+

+ 38 - 0
alien-entity/src/main/resources/db/migration/store_renovation_requirement.sql

@@ -0,0 +1,38 @@
+-- 装修需求动态表
+CREATE TABLE `store_renovation_requirement` (
+    `id` INT NOT NULL AUTO_INCREMENT COMMENT '主键',
+    `store_id` INT NOT NULL COMMENT '门店ID(发布装修需求的普通公司门店)',
+    `requirement_title` VARCHAR(200) NOT NULL COMMENT '需求标题',
+    `renovation_type` TINYINT NOT NULL COMMENT '装修类型(1:新房装修, 2:旧房改造, 3:局部装修)',
+    `house_area` DECIMAL(10, 2) DEFAULT NULL COMMENT '房屋面积(平方米)',
+    `renovation_budget` DECIMAL(10, 2) DEFAULT NULL COMMENT '装修预算(万元)',
+    `detailed_requirement` TEXT COMMENT '详细需求描述',
+    `attachment_urls` VARCHAR(2000) DEFAULT NULL COMMENT '房屋图纸附件(图片和视频URL,多个URL用逗号拼接,最多9个)',
+    `expected_renovation_time` DATE DEFAULT NULL COMMENT '期望装修时间',
+    `contact_name` VARCHAR(50) NOT NULL COMMENT '联系人姓名',
+    `contact_phone` VARCHAR(20) NOT NULL COMMENT '联系电话',
+    `city` VARCHAR(50) NOT NULL COMMENT '所在城市',
+    `city_adcode` VARCHAR(20) DEFAULT NULL COMMENT '城市编码(adcode)',
+    `detailed_address` VARCHAR(200) NOT NULL COMMENT '详细地址',
+    `agreement_confirmed` TINYINT(1) DEFAULT '0' COMMENT '服务协议确认(0:未确认, 1:已确认)',
+    `status` TINYINT DEFAULT '0' COMMENT '动态状态(0:草稿, 1:已发布, 2:已下架)',
+    `view_count` INT DEFAULT '0' COMMENT '浏览数',
+    `inquiry_count` INT DEFAULT '0' COMMENT '咨询数(装修商家查看并联系的数量)',
+    `audit_status` TINYINT DEFAULT '0' COMMENT '审核状态(0:待审核, 1:审核通过, 2:审核失败)',
+    `audit_reason` VARCHAR(500) DEFAULT NULL COMMENT '审核失败原因',
+    `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间',
+    `audit_user_id` INT DEFAULT NULL COMMENT '审核人ID',
+    `delete_flag` TINYINT(1) DEFAULT '0' COMMENT '删除标记, 0:未删除, 1:已删除',
+    `created_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `created_user_id` INT DEFAULT NULL COMMENT '创建人ID',
+    `updated_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
+    `updated_user_id` INT DEFAULT NULL COMMENT '修改人ID',
+    PRIMARY KEY (`id`),
+    KEY `idx_store_id` (`store_id`),
+    KEY `idx_status` (`status`),
+    KEY `idx_audit_status` (`audit_status`),
+    KEY `idx_city` (`city`),
+    KEY `idx_renovation_type` (`renovation_type`),
+    KEY `idx_updated_time` (`updated_time`) USING BTREE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='装修需求动态表';
+

+ 67 - 0
alien-entity/src/main/resources/mapper/StoreQualificationMapper.xml

@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.StoreQualificationMapper">
+
+    <!-- 根据门店id和图片类型查询图片列表 -->
+    <select id="selectStoreImgsByType" resultType="shop.alien.entity.store.StoreImg">
+        SELECT
+            id,
+            store_id AS storeId,
+            img_type AS imgType,
+            img_description AS imgDescription,
+            img_sort AS imgSort,
+            img_url AS imgUrl,
+            is_extract AS isExtract,
+            color_code AS colorCode,
+            delete_flag AS deleteFlag,
+            created_time AS createdTime,
+            created_user_id AS createdUserId,
+            updated_time AS updatedTime,
+            updated_user_id AS updatedUserId,
+            business_id AS businessId
+        FROM
+            store_img
+        WHERE
+            store_id = #{storeId}
+            AND img_type = #{imgType}
+            AND delete_flag = 0
+        ORDER BY
+            img_sort ASC,
+            created_time DESC
+    </select>
+
+    <!-- 根据门店id和多个图片类型查询图片列表 -->
+    <select id="selectStoreImgsByTypes" resultType="shop.alien.entity.store.StoreImg">
+        SELECT
+            id,
+            store_id AS storeId,
+            img_type AS imgType,
+            img_description AS imgDescription,
+            img_sort AS imgSort,
+            img_url AS imgUrl,
+            is_extract AS isExtract,
+            color_code AS colorCode,
+            delete_flag AS deleteFlag,
+            created_time AS createdTime,
+            created_user_id AS createdUserId,
+            updated_time AS updatedTime,
+            updated_user_id AS updatedUserId,
+            business_id AS businessId
+        FROM
+            store_img
+        WHERE
+            store_id = #{storeId}
+            AND img_type IN
+            <foreach collection="imgTypes" item="imgType" open="(" separator="," close=")">
+                #{imgType}
+            </foreach>
+            AND delete_flag = 0
+        ORDER BY
+            img_type ASC,
+            img_sort ASC,
+            created_time DESC
+    </select>
+
+</mapper>

+ 50 - 0
alien-entity/src/main/resources/mapper/StoreTagDetailMapper.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="shop.alien.mapper.StoreTagDetailMapper">
+
+    <!-- 根据门店id查询店铺标签详情 -->
+    <select id="selectStoreTagDetailByStoreId" resultType="shop.alien.entity.store.TagStoreRelation">
+        SELECT
+            id,
+            store_id AS storeId,
+            tag_column_a AS tagColumn1,
+            tag_column_b AS tagColumn2,
+            tag_column_c AS tagColumn3,
+            tag_column_d AS tagColumn4,
+            tag_column_e AS tagColumn5,
+            tag_column_f AS tagColumn6,
+            tag_column_g AS tagColumn7,
+            tag_column_h AS tagColumn8,
+            tag_column_i AS tagColumn9,
+            tag_column_j AS tagColumn10,
+            tag_column_k AS tagColumn11,
+            tag_column_l AS tagColumn12,
+            tag_column_m AS tagColumn13,
+            tag_column_n AS tagColumn14,
+            tag_column_o AS tagColumn15,
+            tag_column_p AS tagColumn16,
+            tag_column_q AS tagColumn17,
+            tag_column_r AS tagColumn18,
+            tag_column_s AS tagColumn19,
+            tag_column_t AS tagColumn20,
+            tag_column_u AS tagColumn21,
+            tag_column_v AS tagColumn22,
+            tag_column_w AS tagColumn23,
+            tag_column_x AS tagColumn24,
+            tag_column_y AS tagColumn25,
+            created_time AS createdTime,
+            created_user_id AS createdUserId,
+            updated_time AS updatedTime,
+            updated_user_id AS updatedUserId,
+            delete_flag AS deleteFlag
+        FROM
+            tag_store_relation
+        WHERE
+            store_id = #{storeId}
+            AND delete_flag = 0
+        LIMIT 1
+    </select>
+
+</mapper>

+ 59 - 0
alien-store/doc/埋点接口清单.md

@@ -0,0 +1,59 @@
+# 埋点接口清单
+
+本文档列出了所有已添加埋点注解(`@TrackEvent`)的接口方法。
+
+## 一、流量数据(TRAFFIC)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 1 | `/aiSearch/search` | POST | `search` | `AiSearchController` | SEARCH |
+| 2 | `/store/info/getClientStoreDetail` | GET | `getClientStoreDetail` | `StoreInfoController` | VIEW |
+| 3 | `/userStore/getStoreDetailById` | GET | `getStoreDetailById` | `UserStoreController` | VIEW |
+
+## 二、互动数据(INTERACTION)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 4 | `/collect/addCollect` | POST | `addCollect` | `LifeCollectController` | COLLECT |
+| 5 | `/storeClockIn/addStoreClockIn` | POST | `addStoreClockIn` | `StoreClockInController` | CHECKIN |
+| 6 | `/renovation/requirement/consultRequirement` | POST | `consultRequirement` | `StoreRenovationRequirementController` | CONSULT |
+| 7 | `/userDynamics/addOrUpdate` | POST | `addOrUpdate` | `LifeUserDynamicsController` | POST_PUBLISH |
+| 8 | `/comment/like` | POST | `like` | `LifeCommentController` | POST_LIKE |
+| 9 | `/commonComment/addComment` | POST | `addComment` | `CommonCommentController` | POST_COMMENT |
+| 10 | `/user-violation/reporting` | POST | `reporting` | `UserViolationController` | REPORT |
+| 11 | `/life-blacklist/blackList` | POST | `blackList` | `LifeBlacklistController` | BLOCK |
+
+## 三、服务质量(SERVICE)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 12 | `/commonRating/addRating` | POST | `add` | `CommonRatingController` | RATING_ADD |
+| 13 | `/commentAppeal/submit` | POST | `submitAppeal` | `CommentAppealController` | APPEAL |
+
+## 四、价目表(PRICE)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 14 | `/cuisine/getPage` | GET | `getPage` | `StoreCuisineController` | PRICE_VIEW |
+| 15 | `/price/getPage` | GET | `getPage` | `StorePriceController` | PRICE_VIEW |
+
+## 五、优惠券(COUPON)
+
+| 序号 | 接口路径 | HTTP方法 | 方法名 | 所在类 | 事件类型 |
+|------|---------|---------|--------|--------|---------|
+| 16 | `/life-discount-coupon-store-friend/setFriendCoupon` | POST | `setFriendCoupon` | `LifeDiscountCouponStoreFriendController` | COUPON_GIVE |
+| 17 | `/coupon/verify` | GET | `verify` | `LifeCouponController` | COUPON_USE |
+
+## 统计汇总
+
+- **总接口数**:17个
+- **流量数据**:3个
+- **互动数据**:8个
+- **服务质量**:2个
+- **价目表**:2个
+- **优惠券**:2个
+
+---
+
+**文档版本**:v1.1  
+**最后更新**:2026-01-14

+ 302 - 0
alien-store/doc/埋点统计数据JSON格式说明.md

@@ -0,0 +1,302 @@
+# 埋点统计数据JSON格式说明
+
+## 一、概述
+
+`store_track_statistics` 表中的各个数据字段存储的是JSON格式的统计数据。本文档详细说明各个字段的JSON格式。
+
+````sql
+-- 埋点统计表
+CREATE TABLE `store_track_statistics` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `store_id` int(11) NOT NULL COMMENT '店铺ID',
+  `stat_date` date NOT NULL COMMENT '统计日期',
+  `stat_type` varchar(50) NOT NULL COMMENT '统计类型(DAILY-日统计,WEEKLY-周统计,MONTHLY-月统计)',
+  `traffic_data` text COMMENT '流量数据(JSON格式)',
+  `interaction_data` text COMMENT '互动数据(JSON格式)',
+  `coupon_data` text COMMENT '优惠券数据(JSON格式)',
+  `voucher_data` text COMMENT '代金券数据(JSON格式)',
+  `service_data` text COMMENT '服务质量数据(JSON格式)',
+  `price_ranking_data` text COMMENT '价目表排名数据(JSON格式)',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_store_date_type` (`store_id`,`stat_date`,`stat_type`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='埋点统计表';
+```
+
+## 二、各字段JSON格式
+
+### 2.1 traffic_data(流量数据)
+
+**字段说明**:存储流量相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "searchCount": 100,           // 搜索量(Long类型)
+  "viewCount": 500,             // 浏览量(Long类型)
+  "visitorCount": 300,          // 访客数(去重后的用户数,Long类型)
+  "newVisitorCount": 50,        // 新增访客数(Long类型)
+  "totalDuration": 1500000,     // 总访问时长(毫秒,Long类型)
+  "avgDuration": 5000           // 平均访问时长(毫秒,Long类型)
+}
+```
+
+**字段说明**:
+- `searchCount`: 店铺搜索次数
+- `viewCount`: 店铺浏览次数
+- `visitorCount`: 访客数(去重后的用户ID数量)
+- `newVisitorCount`: 新增访客数(在统计日期之前没有访问记录的用户)
+- `totalDuration`: 总访问时长(所有浏览事件duration字段的总和,单位:毫秒)
+- `avgDuration`: 平均访问时长(totalDuration / 有duration的浏览事件数,单位:毫秒)
+
+---
+
+### 2.2 interaction_data(互动数据)
+
+**字段说明**:存储用户互动相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "collectCount": 50,           // 收藏次数(Long类型)
+  "shareCount": 30,             // 分享次数(Long类型)
+  "checkinCount": 20,           // 打卡次数(Long类型)
+  "consultCount": 10,           // 咨询次数(Long类型)
+  "friendCount": 100,           // 好友数量(互相关注的用户数,Long类型)
+  "followCount": 200,           // 关注数量(店铺用户关注的人数,Long类型)
+  "fansCount": 150,             // 粉丝数量(关注店铺用户的人数,Long类型)
+  "postCount": 25,              // 发布动态数量(Long类型)
+  "postLikeCount": 200,         // 动态点赞数量(Long类型)
+  "postCommentCount": 80,      // 动态评论数量(Long类型)
+  "postRepostCount": 15,        // 动态转发数量(Long类型)
+  "reportCount": 5,             // 被举报次数(Long类型)
+  "blockCount": 2               // 被拉黑次数(Long类型)
+}
+```
+
+**字段说明**:
+- `collectCount`: 店铺收藏次数(从埋点事件中统计COLLECT类型)
+- `shareCount`: 店铺分享次数(从埋点事件中统计SHARE类型)
+- `checkinCount`: 店铺打卡次数(从埋点事件中统计CHECKIN类型)
+- `consultCount`: 咨询商家次数(从埋点事件中统计CONSULT类型)
+- `friendCount`: 好友数量(通过life_fans表查询互相关注的用户数)
+- `followCount`: 关注数量(店铺用户关注的人数)
+- `fansCount`: 粉丝数量(关注店铺用户的人数)
+- `postCount`: 发布动态数量(从life_user_dynamics表查询,type=2商家社区)
+- `postLikeCount`: 动态点赞数量(从埋点事件中统计POST_LIKE类型)
+- `postCommentCount`: 动态评论数量(从埋点事件中统计POST_COMMENT类型)
+- `postRepostCount`: 动态转发数量(从埋点事件中统计POST_REPOST类型)
+- `reportCount`: 被举报次数(从埋点事件中统计REPORT类型)
+- `blockCount`: 被拉黑次数(从埋点事件中统计BLOCK类型)
+
+---
+
+### 2.3 coupon_data(优惠券数据)
+
+**字段说明**:存储优惠券相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "giveToFriendCount": 50,                    // 赠送好友数量(Long类型)
+  "giveToFriendAmount": 5000.00,             // 赠送好友金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseCount": 30,                // 赠送好友使用数量(Long类型)
+  "giveToFriendUseAmount": 3000.00,          // 赠送好友使用金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseAmountPercent": 60.00,     // 赠送好友使用金额占比(Double类型,单位:%)
+  "friendGiveCount": 20,                     // 好友赠送数量(Long类型)
+  "friendGiveAmount": 2000.00,               // 好友赠送金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseCount": 15,                  // 好友赠送使用数量(Long类型)
+  "friendGiveUseAmount": 1500.00,            // 好友赠送使用金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseAmountPercent": 75.00        // 好友赠送使用金额占比(Double类型,单位:%)
+}
+```
+
+**字段说明**:
+- `giveToFriendCount`: 赠送好友数量(从life_discount_coupon_user表统计,关联店铺的优惠券)
+- `giveToFriendAmount`: 赠送好友金额合计(优惠券面值的总和)
+- `giveToFriendUseCount`: 赠送好友使用数量(status=1已使用的数量)
+- `giveToFriendUseAmount`: 赠送好友使用金额合计(已使用优惠券的面值总和)
+- `giveToFriendUseAmountPercent`: 赠送好友使用金额占比(giveToFriendUseAmount / giveToFriendAmount * 100)
+- `friendGiveCount`: 好友赠送数量(好友赠送给店铺用户的优惠券数量,从life_discount_coupon_store_friend表统计)
+- `friendGiveAmount`: 好友赠送金额合计(好友赠送的优惠券面值总和)
+- `friendGiveUseCount`: 好友赠送使用数量(已使用的数量)
+- `friendGiveUseAmount`: 好友赠送使用金额合计(已使用的优惠券面值总和)
+- `friendGiveUseAmountPercent`: 好友赠送使用金额占比(friendGiveUseAmount / friendGiveAmount * 100)
+
+---
+
+### 2.4 voucher_data(代金券数据)
+
+**字段说明**:存储代金券相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "giveToFriendCount": 30,                    // 赠送好友数量(Long类型)
+  "giveToFriendAmount": 3000.00,             // 赠送好友金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseCount": 20,               // 赠送好友使用数量(Long类型)
+  "giveToFriendUseAmount": 2000.00,         // 赠送好友使用金额合计(BigDecimal类型,单位:元)
+  "giveToFriendUseAmountPercent": 66.67,    // 赠送好友使用金额占比(Double类型,单位:%)
+  "friendGiveCount": 10,                     // 好友赠送数量(Long类型)
+  "friendGiveAmount": 1000.00,               // 好友赠送金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseCount": 8,                   // 好友赠送使用数量(Long类型)
+  "friendGiveUseAmount": 800.00,             // 好友赠送使用金额合计(BigDecimal类型,单位:元)
+  "friendGiveUseAmountPercent": 80.00        // 好友赠送使用金额占比(Double类型,单位:%)
+}
+```
+
+**字段说明**:
+- `giveToFriendCount`: 赠送好友数量(从埋点事件中统计VOUCHER_GIVE类型)
+- `giveToFriendAmount`: 赠送好友金额合计(从埋点事件的amount字段汇总)
+- `giveToFriendUseCount`: 赠送好友使用数量(从埋点事件中统计VOUCHER_USE类型)
+- `giveToFriendUseAmount`: 赠送好友使用金额合计(从埋点事件的amount字段汇总)
+- `giveToFriendUseAmountPercent`: 赠送好友使用金额占比(giveToFriendUseAmount / giveToFriendAmount * 100)
+- `friendGiveCount`: 好友赠送数量(好友赠送给店铺用户的代金券数量,从life_discount_coupon_store_friend表统计,type=1代金券)
+- `friendGiveAmount`: 好友赠送金额合计(好友赠送的代金券面值总和)
+- `friendGiveUseCount`: 好友赠送使用数量(已使用的数量)
+- `friendGiveUseAmount`: 好友赠送使用金额合计(已使用的代金券面值总和)
+- `friendGiveUseAmountPercent`: 好友赠送使用金额占比(friendGiveUseAmount / friendGiveAmount * 100)
+
+---
+
+### 2.5 service_data(服务质量数据)
+
+**字段说明**:存储服务质量相关的统计数据
+
+**JSON格式**:
+```json
+{
+  "storeScore": 4.5,              // 店铺评分(Double类型,0-5分)
+  "tasteScore": 4.3,              // 口味评分(Double类型,0-5分)
+  "environmentScore": 4.2,        // 环境评分(Double类型,0-5分)
+  "serviceScore": 4.4,            // 服务评分(Double类型,0-5分)
+  "ratingCount": 100,             // 评价数量(Long类型)
+  "goodRatingCount": 60,          // 好评数量(score >= 4.5,Long类型)
+  "midRatingCount": 30,           // 中评数量(3.0 <= score <= 4.0,Long类型)
+  "badRatingCount": 10,           // 差评数量(0.5 <= score <= 2.5,Long类型)
+  "badRatingPercent": 10.00,      // 差评占比(Double类型,单位:%)
+  "appealCount": 5,               // 差评申诉次数(Long类型)
+  "appealSuccessCount": 3,        // 差评申诉成功次数(Long类型)
+  "appealSuccessPercent": 60.00   // 差评申诉成功占比(Double类型,单位:%)
+}
+```
+
+**字段说明**:
+- `storeScore`: 店铺评分(从common_rating表统计,businessType=1,计算平均分)
+- `tasteScore`: 口味评分(从common_rating表的otherScore字段解析"口味"评分,计算平均分)
+- `environmentScore`: 环境评分(从common_rating表的otherScore字段解析"环境"评分,计算平均分)
+- `serviceScore`: 服务评分(从common_rating表的otherScore字段解析"服务"评分,计算平均分)
+- `ratingCount`: 评价数量(common_rating表中该店铺的评价总数)
+- `goodRatingCount`: 好评数量(score >= 4.5的评价数)
+- `midRatingCount`: 中评数量(3.0 <= score <= 4.0的评价数)
+- `badRatingCount`: 差评数量(0.5 <= score <= 2.5的评价数)
+- `badRatingPercent`: 差评占比(badRatingCount / ratingCount * 100)
+- `appealCount`: 差评申诉次数(从埋点事件中统计APPEAL类型)
+- `appealSuccessCount`: 差评申诉成功次数(从store_comment_appeal表统计,appealStatus=2已同意)
+- `appealSuccessPercent`: 差评申诉成功占比(appealSuccessCount / appealCount * 100)
+
+**otherScore字段格式示例**:
+```json
+{
+  "口味": 5.0,
+  "环境": 4.5,
+  "服务": 4.0
+}
+```
+
+---
+
+### 2.6 price_ranking_data(价目表排名数据)
+
+**字段说明**:存储价目表排名相关的统计数据(数组格式)
+
+**JSON格式**:
+```json
+[
+  {
+    "priceId": 1001,        // 价目表ID(Integer类型)
+    "viewCount": 500,       // 浏览量(Integer类型)
+    "visitorCount": 300,    // 访客数(Integer类型)
+    "shareCount": 50        // 分享数(Integer类型)
+  },
+  {
+    "priceId": 1002,
+    "viewCount": 400,
+    "visitorCount": 250,
+    "shareCount": 40
+  },
+  {
+    "priceId": 1003,
+    "viewCount": 300,
+    "visitorCount": 200,
+    "shareCount": 30
+  }
+]
+```
+
+**字段说明**:
+- 数组按`viewCount`(浏览量)降序排列
+- 每个元素包含一个价目表的统计数据
+- `priceId`: 价目表ID(targetId字段)
+- `viewCount`: 浏览量(PRICE_VIEW事件数量)
+- `visitorCount`: 访客数(去重后的用户ID数量)
+- `shareCount`: 分享数(PRICE_SHARE事件数量)
+
+---
+
+## 三、数据保存说明
+
+### 3.1 统计数据如何保存
+
+统计数据通过定时任务自动计算并保存:
+
+1. **日统计任务**:每天凌晨1点执行,计算前一天的统计数据(statType=DAILY)
+2. **周统计任务**:每周一凌晨2点执行,计算上一周的统计数据(statType=WEEKLY)
+3. **月统计任务**:每月1号凌晨3点执行,计算上一个月的统计数据(statType=MONTHLY)
+
+**定时任务类**:`TrackStatisticsScheduler.java`
+
+### 3.2 手动触发统计
+
+如果需要手动触发统计数据计算,可以调用:
+
+```java
+trackEventService.calculateAndSaveStatistics(storeId, statDate, statType);
+```
+
+**参数说明**:
+- `storeId`: 店铺ID
+- `statDate`: 统计日期
+- `statType`: 统计类型(DAILY/WEEKLY/MONTHLY)
+
+### 3.3 数据更新策略
+
+- 如果该店铺、该日期、该统计类型的记录已存在,则更新数据
+- 如果不存在,则创建新记录
+- 使用唯一索引 `uk_store_date_type` 保证唯一性
+
+---
+
+## 四、注意事项
+
+1. **数据类型**:
+   - Long类型字段在JSON中显示为数字(如:100)
+   - BigDecimal类型字段在JSON中显示为数字(如:5000.00)
+   - Double类型字段在JSON中显示为数字(如:4.5)
+   - 数组类型字段在JSON中显示为数组(如:[])
+
+2. **空值处理**:
+   - 如果某个分类没有数据,对应的字段可能为null或空对象
+   - 建议在查询时进行null判断
+
+3. **数据精度**:
+   - 金额字段使用BigDecimal保证精度
+   - 百分比字段保留2位小数
+   - 评分字段保留1位小数
+
+4. **性能优化**:
+   - 统计数据已预计算并存储,查询时直接读取,无需实时计算
+   - 建议定期清理历史统计数据,避免数据表过大

+ 449 - 0
alien-store/doc/埋点需求完整方案.md

@@ -0,0 +1,449 @@
+# 用户端埋点需求完整方案文档
+
+## 一、需求概述
+
+为 alien-store 模块的用户端实现完整的埋点系统,用于统计和分析用户行为数据,并通过AI提供智能推荐服务。
+
+### 1.1 埋点数据类型
+
+根据原型图,需要收集以下类型的数据:
+
+#### 1.1.1 流量数据
+- 店铺搜索量
+- 浏览量
+- 访客数
+- 新增访客数
+- 访问时长
+- 平均访问时长
+
+#### 1.1.2 互动数据
+- 店铺收藏次数
+- 店铺分享次数
+- 店铺打卡次数
+- 咨询商家次数
+- 好友数量
+- 关注数量
+- 粉丝数量
+- 发布动态数量
+- 动态点赞数量
+- 动态评论数量
+- 动态转发数量
+- 被举报次数
+- 被拉黑次数
+
+#### 1.1.3 优惠券数据
+- 赠送好友数量
+- 赠送好友金额合计
+- 赠送好友使用数量
+- 赠送好友使用金额合计
+- 赠送好友使用金额占比
+- 好友赠送数量
+- 好友赠送金额合计
+- 好友赠送使用数量
+- 好友赠送使用金额合计
+- 好友赠送使用金额占比
+
+#### 1.1.4 代金券数据
+- 赠送好友数量
+- 赠送好友金额合计
+- 赠送好友使用数量
+- 赠送好友使用金额合计
+- 赠送好友使用金额占比
+- 好友赠送数量
+- 好友赠送金额合计
+- 好友赠送使用数量
+- 好友赠送使用金额合计
+- 好友赠送使用金额占比
+
+#### 1.1.5 服务质量数据
+- 店铺评分
+- 口味评分
+- 环境评分
+- 服务评分
+- 评价数量
+- 好评数量
+- 中评数量
+- 差评数量
+- 差评占比
+- 差评申诉次数
+- 差评申诉成功次数
+- 差评申诉成功占比
+
+#### 1.1.6 价目表排名数据
+- 价目表浏览量
+- 价目表访客数
+- 价目表分享数
+
+## 二、技术架构
+
+### 2.1 架构设计
+
+```
+前端 (客户端)
+  ↓ HTTP请求
+Controller (埋点上报接口)
+  ↓ 异步写入
+Redis List (消息队列)
+  ↓ 定时任务批量消费
+数据库 (MySQL)
+  ↓ 统计分析
+统计查询接口
+  ↓ AI分析
+AI推荐服务
+```
+
+### 2.2 核心组件
+
+1. **埋点注解** (`@TrackEvent`): 标注需要埋点的方法
+2. **AOP切面** (`TrackEventAspect`): 拦截标注的方法,自动收集数据
+3. **Redis List**: 作为消息队列,异步存储埋点数据
+4. **消费服务** (`TrackEventConsumer`): 定时从Redis List批量消费数据并写入数据库
+5. **前端接口** (`TrackEventController`): 提供前端主动上报埋点的接口
+6. **统计接口** (`BusinessDataController`): 提供经营数据统计查询
+7. **AI推荐接口** (`AIRecoveryController`): 基于埋点数据提供AI推荐
+
+## 三、数据库表设计
+
+### 3.1 埋点事件主表 (store_track_event)
+
+```sql
+CREATE TABLE `store_track_event` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `event_type` varchar(50) NOT NULL COMMENT '事件类型(VIEW-浏览,SEARCH-搜索,COLLECT-收藏,SHARE-分享,CHECKIN-打卡,CONSULT-咨询,FOLLOW-关注,UNFOLLOW-取消关注,FRIEND_ADD-添加好友,POST_PUBLISH-发布动态,POST_LIKE-动态点赞,POST_COMMENT-动态评论,POST_REPOST-动态转发,REPORT-举报,BLOCK-拉黑,COUPON_GIVE-赠送优惠券,COUPON_USE-使用优惠券,VOUCHER_GIVE-赠送代金券,VOUCHER_USE-使用代金券,PRICE_VIEW-价目表浏览,PRICE_SHARE-价目表分享,RATING-评价,APPEAL-申诉)',
+  `event_category` varchar(50) NOT NULL COMMENT '事件分类(TRAFFIC-流量数据,INTERACTION-互动数据,COUPON-优惠券,VOUCHER-代金券,SERVICE-服务质量,PRICE-价目表)',
+  `user_id` int(11) DEFAULT NULL COMMENT '用户ID',
+  `store_id` int(11) DEFAULT NULL COMMENT '店铺ID',
+  `target_id` int(11) DEFAULT NULL COMMENT '目标对象ID(如价目表ID、动态ID等)',
+  `target_type` varchar(50) DEFAULT NULL COMMENT '目标对象类型(PRICE-价目表,POST-动态,STORE-店铺等)',
+  `event_data` text COMMENT '事件附加数据(JSON格式)',
+  `amount` decimal(10,2) DEFAULT NULL COMMENT '金额(用于优惠券、代金券等)',
+  `duration` bigint(20) DEFAULT NULL COMMENT '时长(毫秒,用于访问时长等)',
+  `ip_address` varchar(50) DEFAULT NULL COMMENT 'IP地址',
+  `user_agent` varchar(500) DEFAULT NULL COMMENT '用户代理',
+  `device_type` varchar(20) DEFAULT NULL COMMENT '设备类型(IOS,ANDROID,WEB)',
+  `app_version` varchar(20) DEFAULT NULL COMMENT 'APP版本号',
+  `event_time` datetime NOT NULL COMMENT '事件发生时间',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `delete_flag` int(1) NOT NULL DEFAULT '0' COMMENT '删除标记(0:未删除,1:已删除)',
+  PRIMARY KEY (`id`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_event_type` (`event_type`),
+  KEY `idx_event_category` (`event_category`),
+  KEY `idx_event_time` (`event_time`),
+  KEY `idx_store_event_time` (`store_id`,`event_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='埋点事件表';
+```
+
+### 3.2 埋点统计数据表 (store_track_statistics)
+
+用于存储按店铺、日期聚合的统计数据,提升查询性能:
+
+```sql
+CREATE TABLE `store_track_statistics` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `store_id` int(11) NOT NULL COMMENT '店铺ID',
+  `stat_date` date NOT NULL COMMENT '统计日期',
+  `stat_type` varchar(50) NOT NULL COMMENT '统计类型(DAILY-日统计,WEEKLY-周统计,MONTHLY-月统计)',
+  `traffic_data` text COMMENT '流量数据(JSON格式)',
+  `interaction_data` text COMMENT '互动数据(JSON格式)',
+  `coupon_data` text COMMENT '优惠券数据(JSON格式)',
+  `voucher_data` text COMMENT '代金券数据(JSON格式)',
+  `service_data` text COMMENT '服务质量数据(JSON格式)',
+  `price_ranking_data` text COMMENT '价目表排名数据(JSON格式)',
+  `created_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_store_date_type` (`store_id`,`stat_date`,`stat_type`),
+  KEY `idx_store_id` (`store_id`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='埋点统计表';
+```
+
+## 四、后端实现
+
+### 4.1 埋点注解 (@TrackEvent)
+
+定义在 `alien-store/src/main/java/shop/alien/store/annotation/TrackEvent.java`
+
+### 4.2 AOP切面实现
+
+定义在 `alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java`
+
+### 4.3 Redis List 异步存储
+
+使用现有的 `BaseRedisService` 的 `setListRight` 方法将埋点数据写入Redis List
+
+### 4.4 定时任务消费
+
+定义在 `alien-store/src/main/java/shop/alien/store/service/TrackEventConsumer.java`
+
+使用 `@Scheduled` 注解定时从Redis List批量取出数据并写入数据库
+
+### 4.5 前端上报接口
+
+定义在 `alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java`
+
+### 4.6 统计查询接口
+
+定义在 `alien-store/src/main/java/shop/alien/store/controller/BusinessDataController.java`
+
+### 4.7 AI推荐接口
+
+定义在 `alien-store/src/main/java/shop/alien/store/controller/AIRecoveryController.java`
+
+## 五、前端联调
+
+### 5.1 前端需要的后端接口
+
+#### 5.1.1 埋点上报接口
+
+**接口路径**: `POST /track/event`
+
+**接口说明**: 前端主动上报埋点数据
+
+**请求参数**:
+```json
+{
+  "eventType": "VIEW",
+  "eventCategory": "TRAFFIC",
+  "storeId": 1001,
+  "targetId": 2001,
+  "targetType": "PRICE",
+  "eventData": "{}",
+  "amount": 100.00,
+  "duration": 3000
+}
+```
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "上报成功",
+  "data": null
+}
+```
+
+#### 5.1.2 经营数据查询接口
+
+**接口路径**: `GET /business/data`
+
+**接口说明**: 查询店铺的经营数据统计
+
+**请求参数**:
+- `storeId`: 店铺ID (必填)
+- `startDate`: 开始日期 (格式: yyyy-MM-dd)
+- `endDate`: 结束日期 (格式: yyyy-MM-dd)
+- `category`: 数据分类 (可选: TRAFFIC, INTERACTION, COUPON, VOUCHER, SERVICE, PRICE)
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "查询成功",
+  "data": {
+    "trafficData": {
+      "searchCount": 100,
+      "viewCount": 100,
+      "visitorCount": 70,
+      "newVisitorCount": 10,
+      "totalDuration": 373533,
+      "avgDuration": 213
+    },
+    "interactionData": {
+      "collectCount": 100,
+      "shareCount": 100,
+      "checkinCount": 100,
+      "consultCount": 100
+    }
+  }
+}
+```
+
+#### 5.1.3 数据对比接口
+
+**接口路径**: `GET /business/data/compare`
+
+**接口说明**: 对比两个时间段的数据
+
+**请求参数**:
+- `storeId`: 店铺ID (必填)
+- `startDate1`: 时间段1开始日期
+- `endDate1`: 时间段1结束日期
+- `startDate2`: 时间段2开始日期
+- `endDate2`: 时间段2结束日期
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "查询成功",
+  "data": {
+    "period1": {
+      "viewCount": 100,
+      "visitorCount": 70
+    },
+    "period2": {
+      "viewCount": 80,
+      "visitorCount": 70
+    },
+    "compare": {
+      "viewCountChange": 25.00,
+      "visitorCountChange": 0.00
+    }
+  }
+}
+```
+
+#### 5.1.4 AI推荐接口
+
+**接口路径**: `GET /business/ai/recommendation`
+
+**接口说明**: 获取基于埋点数据的AI推荐
+
+**请求参数**:
+- `storeId`: 店铺ID (必填)
+
+**响应示例**:
+```json
+{
+  "code": 200,
+  "success": true,
+  "msg": "查询成功",
+  "data": {
+    "summary": "相较于其他同星级的店铺,您价目表中的锅包肉和烤羊腿价格远高于其他商家",
+    "recommendations": [
+      {
+        "type": "PRICING",
+        "title": "价格优化建议",
+        "content": "寻找原材料更便宜的菜场、菜量降低、菜名突出特色,如锡林郭勒盟羔羊烤羊腿"
+      }
+    ]
+  }
+}
+```
+
+### 5.2 前端调用示例
+
+```javascript
+// 1. 上报浏览事件
+axios.post('/track/event', {
+  eventType: 'VIEW',
+  eventCategory: 'TRAFFIC',
+  storeId: 1001,
+  targetType: 'STORE',
+  duration: 3000
+});
+
+// 2. 查询经营数据
+axios.get('/business/data', {
+  params: {
+    storeId: 1001,
+    startDate: '2026-01-08',
+    endDate: '2026-01-14',
+    category: 'TRAFFIC'
+  }
+});
+
+// 3. 数据对比
+axios.get('/business/data/compare', {
+  params: {
+    storeId: 1001,
+    startDate1: '2026-01-08',
+    endDate1: '2026-01-14',
+    startDate2: '2026-01-01',
+    endDate2: '2026-01-07'
+  }
+});
+
+// 4. 获取AI推荐
+axios.get('/business/ai/recommendation', {
+  params: {
+    storeId: 1001
+  }
+});
+```
+
+## 六、实现细节
+
+### 6.1 埋点事件类型枚举
+
+```java
+public enum EventType {
+    VIEW("VIEW", "浏览"),
+    SEARCH("SEARCH", "搜索"),
+    COLLECT("COLLECT", "收藏"),
+    SHARE("SHARE", "分享"),
+    CHECKIN("CHECKIN", "打卡"),
+    CONSULT("CONSULT", "咨询"),
+    FOLLOW("FOLLOW", "关注"),
+    UNFOLLOW("UNFOLLOW", "取消关注"),
+    FRIEND_ADD("FRIEND_ADD", "添加好友"),
+    POST_PUBLISH("POST_PUBLISH", "发布动态"),
+    POST_LIKE("POST_LIKE", "动态点赞"),
+    POST_COMMENT("POST_COMMENT", "动态评论"),
+    POST_REPOST("POST_REPOST", "动态转发"),
+    REPORT("REPORT", "举报"),
+    BLOCK("BLOCK", "拉黑"),
+    COUPON_GIVE("COUPON_GIVE", "赠送优惠券"),
+    COUPON_USE("COUPON_USE", "使用优惠券"),
+    VOUCHER_GIVE("VOUCHER_GIVE", "赠送代金券"),
+    VOUCHER_USE("VOUCHER_USE", "使用代金券"),
+    PRICE_VIEW("PRICE_VIEW", "价目表浏览"),
+    PRICE_SHARE("PRICE_SHARE", "价目表分享"),
+    RATING("RATING", "评价"),
+    APPEAL("APPEAL", "申诉");
+}
+```
+
+### 6.2 Redis Key 设计
+
+- 埋点队列: `track:event:queue`
+- 消费锁: `track:event:consumer:lock`
+
+### 6.3 消费策略
+
+- 每次消费数量: 100条
+- 消费频率: 每10秒执行一次
+- 使用分布式锁防止多实例重复消费
+
+## 七、AI推荐实现
+
+### 7.1 AI推荐流程
+
+1. 收集店铺的埋点数据
+2. 调用AI服务分析数据
+3. 对比同行业、同星级店铺数据
+4. 生成推荐建议
+
+### 7.2 AI接口调用
+
+使用现有的 `AlienAIFeign` 调用AI服务,需要新增推荐接口。
+
+## 八、部署和运维
+
+### 8.1 配置项
+
+- Redis List最大长度: 100000
+- 批量消费大小: 100
+- 消费间隔: 10秒
+- 统计数据保留期: 2年
+
+### 8.2 监控指标
+
+- Redis List长度
+- 消费延迟
+- 消费失败率
+- 统计查询响应时间
+
+## 九、注意事项
+
+1. 埋点数据量可能很大,需要定期清理历史数据
+2. Redis List需要设置最大长度,防止内存溢出
+3. 消费服务需要异常处理和重试机制
+4. 统计数据建议使用定时任务预计算,提升查询性能
+5. AI推荐接口需要缓存,避免频繁调用AI服务

+ 4 - 0
alien-store/pom.xml

@@ -297,6 +297,10 @@
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-security</artifactId>
         </dependency>
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

+ 61 - 0
alien-store/src/main/java/shop/alien/store/annotation/TrackEvent.java

@@ -0,0 +1,61 @@
+package shop.alien.store.annotation;
+
+import java.lang.annotation.*;
+
+/**
+ * 埋点事件注解
+ * 用于标注需要埋点的方法
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface TrackEvent {
+
+    /**
+     * 事件类型
+     */
+    String eventType();
+
+    /**
+     * 事件分类
+     * TRAFFIC-流量数据
+     * INTERACTION-互动数据
+     * COUPON-优惠券
+     * VOUCHER-代金券
+     * SERVICE-服务质量
+     * PRICE-价目表
+     */
+    String eventCategory();
+
+    /**
+     * 店铺ID的SpEL表达式,如 "#storeId" 或 "#{#store.id}"
+     * 如果不指定,将从请求参数或Session中获取
+     */
+    String storeId() default "";
+
+    /**
+     * 目标对象ID的SpEL表达式,如 "#targetId" 或 "#{#price.id}"
+     */
+    String targetId() default "";
+
+    /**
+     * 目标对象类型
+     */
+    String targetType() default "";
+
+    /**
+     * 用户ID的SpEL表达式
+     * 如果不指定,将从JWT token中获取
+     */
+    String userId() default "";
+
+    /**
+     * 是否异步执行
+     * true: 异步执行,不阻塞主流程
+     * false: 同步执行
+     */
+    boolean async() default true;
+}

+ 557 - 0
alien-store/src/main/java/shop/alien/store/aspect/TrackEventAspect.java

@@ -0,0 +1,557 @@
+package shop.alien.store.aspect;
+
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.core.DefaultParameterNameDiscoverer;
+import org.springframework.core.annotation.Order;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+import shop.alien.entity.store.*;
+import shop.alien.mapper.*;
+import shop.alien.store.annotation.TrackEvent;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.util.common.JwtUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.lang.reflect.Method;
+import java.util.Date;
+
+/**
+ * 埋点事件切面
+ * 拦截标注了@TrackEvent注解的方法,自动收集埋点数据并写入Redis List
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Aspect
+@Component
+@Order(2)
+@RequiredArgsConstructor
+public class TrackEventAspect {
+
+    private final BaseRedisService baseRedisService;
+    private final LifeUserDynamicsMapper lifeUserDynamicsMapper;
+    private final CommonRatingMapper commonRatingMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final ObjectMapper objectMapper;
+    
+    private final SpelExpressionParser spelExpressionParser = new SpelExpressionParser();
+    private final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
+    
+    /**
+     * Redis List Key
+     */
+    private static final String REDIS_QUEUE_KEY = "track:event:queue";
+
+    /**
+     * 定义切点:所有标注了@TrackEvent注解的方法
+     */
+    @Pointcut("@annotation(shop.alien.store.annotation.TrackEvent)")
+    public void trackEventPointcut() {
+    }
+
+    /**
+     * 环绕通知:收集埋点数据并写入Redis List
+     */
+    @Around("trackEventPointcut()")
+    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        TrackEvent annotation = method.getAnnotation(TrackEvent.class);
+
+        if (annotation == null) {
+            return joinPoint.proceed();
+        }
+
+        // 执行目标方法
+        Object result = joinPoint.proceed();
+
+        try {
+            // 构建埋点事件对象
+            StoreTrackEvent trackEvent = buildTrackEvent(joinPoint, annotation, result);
+            
+            // 异步写入Redis List
+            if (annotation.async()) {
+                String eventJson = objectMapper.writeValueAsString(trackEvent);
+                baseRedisService.setListRight(REDIS_QUEUE_KEY, eventJson);
+            }
+        } catch (Exception e) {
+            // 埋点失败不应该影响主流程
+            log.error("收集埋点数据失败", e);
+        }
+
+        return result;
+    }
+
+    /**
+     * 构建埋点事件对象
+     */
+    private StoreTrackEvent buildTrackEvent(ProceedingJoinPoint joinPoint, TrackEvent annotation, Object result) {
+        StoreTrackEvent trackEvent = new StoreTrackEvent();
+        
+        // 设置基本事件信息
+        trackEvent.setEventType(annotation.eventType());
+        trackEvent.setEventCategory(annotation.eventCategory());
+        trackEvent.setTargetType(StringUtils.isNotBlank(annotation.targetType()) ? annotation.targetType() : null);
+        trackEvent.setEventTime(new Date());
+
+        // 创建SpEL表达式上下文
+        EvaluationContext context = createEvaluationContext(joinPoint, result);
+
+        // 解析SpEL表达式获取storeId
+        Integer storeId = parseSpEL(annotation.storeId(), context, Integer.class);
+        if (storeId == null && StringUtils.isBlank(annotation.storeId())) {
+            // 尝试从方法参数中查找storeId
+            storeId = extractStoreIdFromArgs(joinPoint);
+        }
+        // 如果storeId仍为空,根据接口类型和参数查询店铺ID
+        if (storeId == null) {
+            storeId = queryStoreIdByBusinessLogic(joinPoint, annotation, context);
+        }
+        
+        // 如果storeId仍为空,记录警告日志
+        if (storeId == null) {
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            String methodName = signature.getMethod().getName();
+            String className = joinPoint.getTarget().getClass().getSimpleName();
+            log.warn("无法获取storeId: className={}, methodName={}, storeId表达式={}", 
+                    className, methodName, annotation.storeId());
+        }
+        
+        trackEvent.setStoreId(storeId);
+
+        // 解析SpEL表达式获取userId
+        Integer userId = parseSpEL(annotation.userId(), context, Integer.class);
+        if (userId == null && StringUtils.isBlank(annotation.userId())) {
+            // 从JWT中获取用户ID
+            userId = getUserIdFromJWT();
+        }
+        trackEvent.setUserId(userId);
+
+        // 解析SpEL表达式获取targetId
+        Integer targetId = parseSpEL(annotation.targetId(), context, Integer.class);
+        trackEvent.setTargetId(targetId);
+
+        // 获取请求信息
+        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+        if (attributes != null) {
+            HttpServletRequest request = attributes.getRequest();
+            trackEvent.setIpAddress(getIpAddress(request));
+            String userAgent = request.getHeader("User-Agent");
+            trackEvent.setUserAgent(userAgent);
+            // 根据User-Agent解析设备类型
+            trackEvent.setDeviceType(shop.alien.store.util.UserAgentParserUtil.parseDeviceType(userAgent));
+        }
+
+        return trackEvent;
+    }
+
+    /**
+     * 创建SpEL表达式上下文
+     */
+    private EvaluationContext createEvaluationContext(ProceedingJoinPoint joinPoint, Object result) {
+        StandardEvaluationContext context = new StandardEvaluationContext();
+        
+        // 添加方法参数
+        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+        Method method = signature.getMethod();
+        Object[] args = joinPoint.getArgs();
+        String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
+        
+        if (parameterNames != null && args != null) {
+            for (int i = 0; i < parameterNames.length; i++) {
+                context.setVariable(parameterNames[i], args[i]);
+            }
+        }
+        
+        // 添加返回值
+        if (result != null) {
+            context.setVariable("result", result);
+        }
+
+        return context;
+    }
+
+    /**
+     * 解析SpEL表达式
+     */
+    private <T> T parseSpEL(String expression, EvaluationContext context, Class<T> clazz) {
+        if (StringUtils.isBlank(expression)) {
+            return null;
+        }
+
+        try {
+            // 如果表达式不包含#,直接作为字符串处理
+            if (!expression.contains("#")) {
+                if (clazz == String.class) {
+                    return clazz.cast(expression);
+                }
+                return null;
+            }
+
+            // 处理 #{#xxx} 格式,转换为 #xxx
+            String normalizedExpression = expression;
+            if (expression.startsWith("#{#") && expression.endsWith("}")) {
+                normalizedExpression = expression.substring(2, expression.length() - 1);
+            }
+            
+            // 解析SpEL表达式
+            Object value = spelExpressionParser.parseExpression(normalizedExpression).getValue(context);
+            if (value == null) {
+                return null;
+            }
+
+            if (clazz.isInstance(value)) {
+                return clazz.cast(value);
+            }
+            
+            // 类型转换
+            if (clazz == Integer.class) {
+                if (value instanceof Number) {
+                    return clazz.cast(((Number) value).intValue());
+                } else if (value instanceof String) {
+                    try {
+                        return clazz.cast(Integer.parseInt((String) value));
+                    } catch (NumberFormatException e) {
+                        log.debug("无法将字符串转换为Integer: {}", value);
+                        return null;
+                    }
+                }
+            }
+            
+            return null;
+        } catch (Exception e) {
+            log.debug("解析SpEL表达式失败: {}", expression, e);
+            return null;
+        }
+    }
+
+    /**
+     * 从方法参数中提取storeId
+     */
+    private Integer extractStoreIdFromArgs(ProceedingJoinPoint joinPoint) {
+        try {
+            Object[] args = joinPoint.getArgs();
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            String[] parameterNames = parameterNameDiscoverer.getParameterNames(signature.getMethod());
+
+            if (args != null && parameterNames != null) {
+                for (int i = 0; i < parameterNames.length; i++) {
+                    // 查找名为storeId的参数
+                    if ("storeId".equals(parameterNames[i]) && args[i] != null) {
+                        if (args[i] instanceof Integer) {
+                            return (Integer) args[i];
+                        } else if (args[i] instanceof String) {
+                            return Integer.parseInt((String) args[i]);
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug("从方法参数中提取storeId失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 从JWT中获取用户ID
+     */
+    private Integer getUserIdFromJWT() {
+        try {
+            JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null && userInfo.get("userId") != null) {
+                Object userIdObj = userInfo.get("userId");
+                if (userIdObj instanceof Integer) {
+                    return (Integer) userIdObj;
+                } else if (userIdObj instanceof String) {
+                    return Integer.parseInt((String) userIdObj);
+                }
+            }
+        } catch (Exception e) {
+            log.debug("从JWT获取用户ID失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 根据业务逻辑查询店铺ID
+     */
+    private Integer queryStoreIdByBusinessLogic(ProceedingJoinPoint joinPoint, TrackEvent annotation, EvaluationContext context) {
+        try {
+            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+            String methodName = signature.getMethod().getName();
+            String className = joinPoint.getTarget().getClass().getSimpleName();
+            
+            // 处理 /comment/like 接口(动态点赞)
+            if ("LifeCommentController".equals(className) && "like".equals(methodName)) {
+                return queryStoreIdForLike(context);
+            }
+            
+            // 处理 /commonComment/addComment 接口(动态回复)
+            if ("CommonCommentController".equals(className) && "addComment".equals(methodName)) {
+                return queryStoreIdForAddComment(context);
+            }
+            
+            // 处理 /user-violation/reporting 接口(举报)
+            if ("UserViolationController".equals(className) && "reporting".equals(methodName)) {
+                return queryStoreIdForReporting(context);
+            }
+            
+            // 处理 /life-blacklist/blackList 接口(拉黑)
+            if ("LifeBlacklistController".equals(className) && "blackList".equals(methodName)) {
+                return queryStoreIdForBlackList(context);
+            }
+            
+            // 处理 /userDynamics/addOrUpdate 接口(发布动态)
+            if ("LifeUserDynamicsController".equals(className) && "addOrUpdate".equals(methodName)) {
+                return queryStoreIdForAddOrUpdate(context);
+            }
+        } catch (Exception e) {
+            log.debug("根据业务逻辑查询店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为点赞接口查询店铺ID
+     */
+    private Integer queryStoreIdForLike(EvaluationContext context) {
+        try {
+            // 获取type和huifuId参数
+            Object typeObj = context.lookupVariable("type");
+            Object huifuIdObj = context.lookupVariable("huifuId");
+            
+            if (typeObj == null || huifuIdObj == null) {
+                return null;
+            }
+            
+            String type = String.valueOf(typeObj);
+            String huifuId = String.valueOf(huifuIdObj);
+            
+            // type=2 表示社区动态
+            if ("2".equals(type)) {
+                LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(Integer.parseInt(huifuId));
+                if (dynamics != null && dynamics.getPhoneId() != null) {
+                    // phoneId格式:store_手机号 或 user_手机号
+                    if (dynamics.getPhoneId().startsWith("store_")) {
+                        String phone = dynamics.getPhoneId().substring(6);
+                        LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                        wrapper.eq(StoreUser::getPhone, phone)
+                                .eq(StoreUser::getDeleteFlag, 0);
+                        StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                        if (storeUser != null && storeUser.getStoreId() != null) {
+                            return storeUser.getStoreId();
+                        }
+                    }
+                }
+            }
+            // type=1 表示评论,需要查询评论表获取店铺ID
+            else if ("1".equals(type)) {
+                // 这里可以根据评论ID查询评论表,但需要知道评论表的结构
+                // 暂时返回null,后续可以根据实际表结构补充
+            }
+        } catch (Exception e) {
+            log.debug("查询点赞接口的店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为添加评论接口查询店铺ID
+     */
+    private Integer queryStoreIdForAddComment(EvaluationContext context) {
+        try {
+            // 获取commonComment参数
+            Object commonCommentObj = context.lookupVariable("commonComment");
+            if (commonCommentObj == null || !(commonCommentObj instanceof CommonComment)) {
+                return null;
+            }
+            
+            CommonComment commonComment = (CommonComment) commonCommentObj;
+            
+            // sourceType=1 表示评价的评论
+            if (commonComment.getSourceType() != null && commonComment.getSourceType() == 1) {
+                // sourceId是rating.id,通过CommonRating查询businessId(店铺ID)
+                if (commonComment.getSourceId() != null) {
+                    CommonRating rating = commonRatingMapper.selectById(commonComment.getSourceId());
+                    if (rating != null && rating.getBusinessId() != null) {
+                        return rating.getBusinessId();
+                    }
+                }
+            }
+            // sourceType=2 表示社区动态
+            else if (commonComment.getSourceType() != null && commonComment.getSourceType() == 2) {
+                // sourceId是动态ID,通过LifeUserDynamics查询店铺ID
+                if (commonComment.getSourceId() != null) {
+                    LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(commonComment.getSourceId().intValue());
+                    if (dynamics != null && dynamics.getPhoneId() != null) {
+                        // phoneId格式:store_手机号 或 user_手机号
+                        if (dynamics.getPhoneId().startsWith("store_")) {
+                            String phone = dynamics.getPhoneId().substring(6);
+                            LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                            wrapper.eq(StoreUser::getPhone, phone)
+                                    .eq(StoreUser::getDeleteFlag, 0);
+                            StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                            if (storeUser != null && storeUser.getStoreId() != null) {
+                                return storeUser.getStoreId();
+                            }
+                        }
+                    }
+                }
+            }
+        } catch (Exception e) {
+            log.debug("查询添加评论接口的店铺ID失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 为举报接口查询店铺ID
+     */
+    private Integer queryStoreIdForReporting(EvaluationContext context) {
+        try {
+            // 获取lifeUserViolation参数
+            Object violationObj = context.lookupVariable("lifeUserViolation");
+            if (violationObj == null || !(violationObj instanceof LifeUserViolation)) {
+                return null;
+            }
+            
+            LifeUserViolation violation = (LifeUserViolation) violationObj;
+            
+            // 根据reportContextType查询店铺ID
+            String reportContextType = violation.getReportContextType();
+            
+            // reportContextType=2 表示动态
+            if ("2".equals(reportContextType) && violation.getDynamicsId() != null) {
+                LifeUserDynamics dynamics = lifeUserDynamicsMapper.selectById(Integer.parseInt(violation.getDynamicsId()));
+                if (dynamics != null && dynamics.getPhoneId() != null) {
+                    if (dynamics.getPhoneId().startsWith("store_")) {
+                        String phone = dynamics.getPhoneId().substring(6);
+                        LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                        wrapper.eq(StoreUser::getPhone, phone)
+                                .eq(StoreUser::getDeleteFlag, 0);
+                        StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                        if (storeUser != null && storeUser.getStoreId() != null) {
+                            return storeUser.getStoreId();
+                        }
+                    }
+                }
+            }
+            // reportContextType=3 表示评论
+            else if ("3".equals(reportContextType) && violation.getCommentId() != null) {
+                // 可以通过评论ID查询评论表,然后获取店铺ID
+                // 暂时返回null,后续可以根据实际表结构补充
+            }
+            // reportContextType=0 表示商户
+            else if ("0".equals(reportContextType) && violation.getBusinessId() != null) {
+                // businessId就是店铺ID
+                return violation.getBusinessId();
+            }
+        } catch (Exception e) {
+            log.debug("查询举报接口的店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为发布动态接口查询店铺ID
+     */
+    private Integer queryStoreIdForAddOrUpdate(EvaluationContext context) {
+        try {
+            // 获取lifeUserDynamics参数
+            Object dynamicsObj = context.lookupVariable("lifeUserDynamics");
+            if (dynamicsObj == null || !(dynamicsObj instanceof LifeUserDynamics)) {
+                return null;
+            }
+            
+            LifeUserDynamics dynamics = (LifeUserDynamics) dynamicsObj;
+            
+            // 如果phoneId以store_开头,通过phoneId查询店铺ID
+            if (dynamics.getPhoneId() != null && dynamics.getPhoneId().startsWith("store_")) {
+                String phone = dynamics.getPhoneId().substring(6);
+                LambdaQueryWrapper<StoreUser> wrapper = new LambdaQueryWrapper<>();
+                wrapper.eq(StoreUser::getPhone, phone)
+                        .eq(StoreUser::getDeleteFlag, 0);
+                StoreUser storeUser = storeUserMapper.selectOne(wrapper);
+                if (storeUser != null && storeUser.getStoreId() != null) {
+                    return storeUser.getStoreId();
+                }
+            }
+        } catch (Exception e) {
+            log.debug("查询发布动态接口的店铺ID失败", e);
+        }
+        return null;
+    }
+    
+    /**
+     * 为拉黑接口查询店铺ID
+     */
+    private Integer queryStoreIdForBlackList(EvaluationContext context) {
+        try {
+            // 获取lifeBlacklist参数
+            Object blacklistObj = context.lookupVariable("lifeBlacklist");
+            if (blacklistObj == null || !(blacklistObj instanceof LifeBlacklist)) {
+                return null;
+            }
+            
+            LifeBlacklist blacklist = (LifeBlacklist) blacklistObj;
+            
+            // 如果拉黑方是商户(blockerType=1),通过blockerId查询店铺ID
+            if ("1".equals(blacklist.getBlockerType()) && blacklist.getBlockerId() != null) {
+                try {
+                    Integer blockerId = Integer.parseInt(blacklist.getBlockerId());
+                    StoreUser storeUser = storeUserMapper.selectById(blockerId);
+                    if (storeUser != null && storeUser.getStoreId() != null) {
+                        return storeUser.getStoreId();
+                    }
+                } catch (NumberFormatException e) {
+                    log.debug("blockerId无法转换为Integer: {}", blacklist.getBlockerId());
+                }
+            }
+        } catch (Exception e) {
+            log.debug("查询拉黑接口的店铺ID失败", e);
+        }
+        return null;
+    }
+
+    /**
+     * 获取客户端IP地址
+     */
+    private String getIpAddress(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        // 如果是多级代理,取第一个IP
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        return ip;
+    }
+}

+ 2 - 0
alien-store/src/main/java/shop/alien/store/config/WebSocketProcess.java

@@ -245,4 +245,6 @@ public class WebSocketProcess implements ApplicationContextAware {
         }
     }
 
+
+
 }

+ 11 - 3
alien-store/src/main/java/shop/alien/store/controller/AiSearchController.java

@@ -24,6 +24,8 @@ import shop.alien.entity.store.StoreUser;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.mapper.StoreImgMapper;
 import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.annotation.TrackEvent;
+import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.CommonRatingService;
 import shop.alien.store.service.StoreImgService;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
@@ -57,6 +59,12 @@ public class AiSearchController {
     private final StoreImgService storeImgService;
     private final CommonRatingService commonRatingService;
 
+    @TrackEvent(
+            eventType = "SEARCH",
+            eventCategory = "TRAFFIC",
+            storeId = "#{#storeId}",
+            targetType = "STORE"
+    )
     @RequestMapping("/search")
     public R search(@RequestBody Map<String,String> map) {
 
@@ -135,7 +143,7 @@ public class AiSearchController {
 
             // 查找图片并设置到result中(图片类型1-入口图)
             fillStoreImages(result, 1);
-            
+
             // 填充评论总数
             fillRatingCount(result);
 
@@ -244,13 +252,13 @@ public class AiSearchController {
         if (result == null || result.isEmpty()) {
             return;
         }
-        
+
         for (StoreInfoVo storeInfo : result) {
             if (storeInfo.getId() != null) {
                 try {
                     // 调用评论服务获取评论总数,businessId传id,businessType传1
                     Object ratingCountObj = commonRatingService.getRatingCount(storeInfo.getId(), 1);
-                    
+
                     // 将返回的Object转换为Map
                     if (ratingCountObj instanceof Map) {
                         Map<String, Object> ratingCountMap = (Map<String, Object>) ratingCountObj;

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/CommentAppealController.java

@@ -10,6 +10,7 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommentAppeal;
 import shop.alien.entity.store.dto.AuditAppealRequestDto;
 import shop.alien.entity.store.vo.CommentAppealVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommentAppealService;
 
 import java.text.SimpleDateFormat;
@@ -33,6 +34,12 @@ public class CommentAppealController {
 
     private final CommentAppealService commentAppealService;
 
+    @TrackEvent(
+            eventType = "APPEAL",
+            eventCategory = "SERVICE",
+            storeId = "#{#commentAppeal.storeId}",
+            targetType = "COMMENT"
+    )
     @ApiOperation(value = "提交申诉", notes = "用户提交评论申诉")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({

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

@@ -1,13 +1,12 @@
 package shop.alien.store.controller;
 
-import io.swagger.annotations.Api;
-import io.swagger.annotations.ApiOperation;
-import io.swagger.annotations.ApiSort;
+import io.swagger.annotations.*;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonComment;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonCommentService;
 
 /**
@@ -59,7 +58,20 @@ public class CommonCommentController {
      //   "merchantId": 10086, 为2商户评论需要填
      }
      */
-    @ApiOperation(value = "新增评论", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
+    /**
+     * 新增评论
+     * @param commonComment 评论对象
+     * @return 0:成功, 1:失败, 2:文本内容异常, 4:字数超限(超过300字)
+     */
+    @TrackEvent(
+            eventType = "POST_COMMENT",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#commonComment.userId",
+            targetId = "#commonComment.sourceId",
+            targetType = "POST"
+    )
+    @ApiOperation(value = "新增评论", notes = "0:成功, 1:失败, 2:文本内容异常, 4:字数超限(超过300字)")
     @PostMapping("/addComment")
     public R addComment(@RequestBody CommonComment commonComment) {
         Integer addComment = commonCommentService.addComment(commonComment);
@@ -69,6 +81,9 @@ public class CommonCommentController {
         if (addComment == 2) {
             return R.fail("新增评论失败,评论内容包含敏感字符");
         }
+        if (addComment == 4) {
+            return R.fail("新增评论失败,评论内容超过300字限制");
+        }
         return R.fail("新增评论失败");
     }
 
@@ -86,5 +101,52 @@ public class CommonCommentController {
         return R.fail("删除评论失败");
     }
 
+
+    /**
+     * 根据动态类型和动态id查询评论列表
+     * @param sourceType
+     * @param sourceId
+     * @param pageNum
+     * @param pageSize
+     * @return
+     */
+    @ApiOperation(value = "动态评论")
+    @GetMapping("/getListBySourceType")
+    @ApiImplicitParams(value = {
+            @ApiImplicitParam(name = "sourceType", value = "类型", required = true, dataType = "Integer"),
+            @ApiImplicitParam(name = "sourceId", value = "id", required = true, dataType = "Integer"),
+            @ApiImplicitParam(name = "pageNum", value = "页码", required = true, dataType = "Integer"),
+            @ApiImplicitParam(name = "pageSize", value = "每页数量", required = true, dataType = "Integer"),
+            @ApiImplicitParam(name = "userId", value = "用户id", required = false, dataType = "Long")
+    })
+    public R getListBySourceType(@RequestParam Integer sourceType,
+                                 @RequestParam Integer sourceId,
+                                 @RequestParam(defaultValue = "1") Integer pageNum,
+                                 @RequestParam(defaultValue = "10") Integer pageSize,
+                                 @RequestParam(required = false) Long userId) {
+        return R.data(commonCommentService.getListBySourceType(sourceType, sourceId, pageNum, pageSize, userId));
+    }
+
+    /**
+     * 获取评论数量
+     * @param sourceId
+     * @param sourceType
+     * @param userId
+     * @return
+     */
+    @ApiOperation(value = "获取评论数量")
+    @GetMapping("/getCommitCount")
+    @ApiImplicitParams(value = {
+            @ApiImplicitParam(name = "sourceId", value = "id", required = true, dataType = "Integer"),
+            @ApiImplicitParam(name = "sourceType", value = "类型", required = true, dataType = "Integer"),
+            @ApiImplicitParam(name = "userId", value = "用户id", required = true, dataType = "String"),
+            @ApiImplicitParam(name = "userType", value = "用户类型,user,store", required = true, dataType = "String")
+    })
+    public R getCommitCount(@RequestParam Integer sourceId,
+                            @RequestParam Integer sourceType,
+                            @RequestParam String userId,
+                            @RequestParam String userType){
+        return R.data(commonCommentService.getCommitCount(sourceId, sourceType, userId, userType));
+    }
 }
 

+ 31 - 5
alien-store/src/main/java/shop/alien/store/controller/CommonRatingController.java

@@ -6,6 +6,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonRating;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.CommonCommentService;
 import shop.alien.store.service.CommonRatingService;
 
@@ -35,7 +36,10 @@ public class CommonRatingController {
             @ApiImplicitParam(name = "businessId", value = "业务关联ID", dataType = "Long", paramType = "query"),
             @ApiImplicitParam(name = "userId", value = "用户ID", dataType = "Long", paramType = "query"),
             @ApiImplicitParam(name = "auditStatus", value = "审核状态:0-待审核 1-通过 2-驳回", dataType = "Integer", paramType = "query"),
-            @ApiImplicitParam(name = "searchScore", value = "搜索评分:0-全部,1-好评 2-中评 3-差评,4-有图", dataType = "Integer", paramType = "query")
+            @ApiImplicitParam(name = "searchScore", value = "搜索评分:0-全部,1-好评 2-中评 3-差评,4-有图", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "days", value = "查询时间, 多少天前", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "replyStatus", value = "回复状态(0:全部, 1:已回复, 2:未回复)", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "tagId", value = "标签id", dataType = "Integer", paramType = "query")
     })
     @GetMapping("/getList")
     public R getList(
@@ -45,10 +49,13 @@ public class CommonRatingController {
             @RequestParam(required = false) Long businessId,
             @RequestParam(required = false) Long userId,
             @RequestParam(required = false) Integer auditStatus,
-            @RequestParam(defaultValue = "0") Integer searchScore) {
-        log.info("CommonRatingController.getList?pageNum={}&pageSize={}&businessType={}&businessId={}&userId={}&auditStatus={}&searchScore={}",
-                pageNum, pageSize, businessType, businessId, userId, auditStatus, searchScore);
-        return commonRatingService.getRatingList(pageNum, pageSize, businessType, businessId, userId, auditStatus, searchScore);
+            @RequestParam(defaultValue = "0") Integer searchScore,
+            @RequestParam(required = false) Integer days,
+            @RequestParam(required = false) Integer replyStatus,
+            @RequestParam(required = false) Integer tagId) {
+        log.info("CommonRatingController.getList?pageNum={}&pageSize={}&businessType={}&businessId={}&userId={}&auditStatus={}&searchScore={}&days={}&replyStatus={}&tagId={}",
+                pageNum, pageSize, businessType, businessId, userId, auditStatus, searchScore, days, replyStatus, tagId);
+        return commonRatingService.getRatingList(pageNum, pageSize, businessType, businessId, userId, auditStatus, searchScore, days, replyStatus, tagId);
     }
 
     @ApiOperation("获取全部评价数量(好评,中评,差评)")
@@ -61,6 +68,19 @@ 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)
@@ -70,6 +90,12 @@ public class CommonRatingController {
 //        return R.data(commonRatingService.getById(id));
 //    }
 //
+    @TrackEvent(
+            eventType = "RATING_ADD",
+            eventCategory = "SERVICE",
+            storeId = "#{#commonRating.businessId}",
+            targetType = "STORE"
+    )
     @ApiOperation(value = "新增评价", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @PostMapping("/addRating")
     public R<Integer> add(@RequestBody CommonRating commonRating) {

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

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeBlacklist;
 import shop.alien.entity.store.vo.LifeBlacklistVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeBlacklistService;
 
 import java.util.List;
@@ -33,6 +34,14 @@ public class LifeBlacklistController {
 
     private final LifeBlacklistService lifeBlacklistService;
 
+    @TrackEvent(
+            eventType = "BLOCK",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#lifeBlacklist.blockerId",
+            targetId = "#lifeBlacklist.blockedId",
+            targetType = "STORE"
+    )
     @ApiOperation("拉黑")
     @ApiOperationSupport(order = 1)
     @PostMapping("/blackList")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeCollectController.java

@@ -17,6 +17,7 @@ import shop.alien.entity.store.*;
 import shop.alien.entity.store.vo.StoreInfoVo;
 import shop.alien.mapper.*;
 import shop.alien.mapper.second.SecondGoodsMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.GaoDeMapUtil;
 import shop.alien.util.common.ListToPage;
 
@@ -203,6 +204,12 @@ public class LifeCollectController {
         return R.data(ListToPage.setPage(lifeCoupons, page, size));
     }
 
+    @TrackEvent(
+            eventType = "COLLECT",
+            eventCategory = "INTERACTION",
+            storeId = "#{#lifeCollect.storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("添加收藏")
     @ApiOperationSupport(order = 2)
     @PostMapping("addCollect")

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

@@ -10,6 +10,7 @@ import org.springframework.web.multipart.MultipartFile;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeComment;
 import shop.alien.entity.store.vo.LifePinglunVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeCommentService;
 import shop.alien.util.common.FileUpload;
 import shop.alien.util.common.ListToPage;
@@ -42,6 +43,14 @@ public class LifeCommentController {
      * @param type    点赞类型(1-评论 2-社区动态 3-活动 4-推荐菜 5-店铺打卡 6-二手商品 7-律师评分 8-点赞员工)
      * @return 点赞结果
      */
+    @TrackEvent(
+            eventType = "POST_LIKE",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#userId",
+            targetId = "#huifuId",
+            targetType = "POST"
+    )
     @ApiOperation("点赞")
     @ApiOperationSupport(order = 1)
     @ApiImplicitParams({

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

@@ -12,6 +12,7 @@ import shop.alien.entity.store.EssentialHolidayComparison;
 import shop.alien.entity.store.LifeCoupon;
 import shop.alien.entity.store.vo.LifeCouponStatusVo;
 import shop.alien.entity.store.vo.LifeCouponVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeCouponService;
 
 import java.util.Map;
@@ -96,6 +97,12 @@ public class LifeCouponController {
         }
     }
 
+    @TrackEvent(
+            eventType = "COUPON_USE",
+            eventCategory = "COUPON",
+            storeId = "#{#storeId}",
+            targetType = "COUPON"
+    )
     @ApiOperation("旧 核销订单")
     @ApiImplicitParams({@ApiImplicitParam(name = "storeId", value = "门店id", dataType = "Integer", paramType = "query", required = true), @ApiImplicitParam(name = "quanCode", value = "券码", dataType = "Integer", paramType = "query", required = true)})
     @GetMapping("/verify")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeDiscountCouponStoreFriendController.java

@@ -15,6 +15,7 @@ import shop.alien.entity.store.vo.LifeDiscountCouponFriendRuleDetailVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponFriendRuleVo;
 import shop.alien.entity.store.vo.LifeDiscountCouponStoreFriendVo;
 import shop.alien.entity.store.vo.LifeGroupBuyCountDateVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeDiscountCouponStoreFriendService;
 import shop.alien.util.common.TokenInfo;
 import springfox.documentation.annotations.ApiIgnore;
@@ -53,6 +54,12 @@ public class LifeDiscountCouponStoreFriendController {
         }
     }
 
+    @TrackEvent(
+            eventType = "COUPON_GIVE",
+            eventCategory = "COUPON",
+            storeId = "#{#lifeDiscountCouponStoreFriendDto.storeId}",
+            targetType = "COUPON"
+    )
     @ApiOperation("给好友发放优惠券")
     @ApiOperationSupport(order = 2)
     @PostMapping("/setFriendCoupon")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/LifeUserDynamicsController.java

@@ -13,6 +13,7 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.LifeUserDynamics;
 import shop.alien.entity.store.vo.LifePinglunVo;
 import shop.alien.entity.store.vo.LifeUserDynamicsVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeUserDynamicsService;
 import shop.alien.util.common.ListToPage;
 import shop.alien.util.common.safe.*;
@@ -73,6 +74,12 @@ public class LifeUserDynamicsController {
         return R.data(lifeUserDynamicsService.getUserDynamics(myselfPhoneId, phoneId, type, page, size));
     }
 
+    @TrackEvent(
+            eventType = "POST_PUBLISH",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            targetType = "POST"
+    )
 
     @ApiOperation(value = "发布动态社区", notes = "0:成功, 1:失败, 2:文本内容异常, 3:图片内容异常")
     @ApiOperationSupport(order = 2)

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreClockInController.java

@@ -8,6 +8,7 @@ import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreClockIn;
 import shop.alien.entity.store.vo.StoreClockInVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreClockInService;
 
 @Slf4j
@@ -20,6 +21,12 @@ public class StoreClockInController {
 
     private final StoreClockInService storeClockInService;
 
+    @TrackEvent(
+            eventType = "CHECKIN",
+            eventCategory = "INTERACTION",
+            storeId = "#{#storeClockIn.storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("打卡")
     @ApiOperationSupport(order = 1)
     @PostMapping("/addStoreClockIn")

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreCuisineController.java

@@ -15,6 +15,7 @@ import shop.alien.entity.store.StorePrice;
 import shop.alien.entity.store.dto.CuisineComboDto;
 import shop.alien.entity.store.dto.CuisineTypeResponseDto;
 import shop.alien.entity.store.vo.PriceListVo;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StoreCuisineService;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.store.util.ai.AiGetPriceUtil;
@@ -125,6 +126,12 @@ public class StoreCuisineController {
         return R.fail("操作失败");
     }
 
+    @TrackEvent(
+            eventType = "PRICE_VIEW",
+            eventCategory = "PRICE",
+            storeId = "#{#storeId}",
+            targetType = "PRICE"
+    )
     @ApiOperation("分页查询美食价目/通用价目")
     @ApiOperationSupport(order = 7)
     @ApiImplicitParams({

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

@@ -19,8 +19,10 @@ import shop.alien.entity.store.vo.*;
 import shop.alien.entity.storePlatform.StoreLicenseHistory;
 import shop.alien.mapper.*;
 import shop.alien.mapper.storePlantform.StoreLicenseHistoryMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.config.BaseRedisService;
 import shop.alien.store.service.StoreInfoService;
+import shop.alien.store.service.StoreQualificationService;
 
 import java.io.IOException;
 import java.util.*;
@@ -42,6 +44,8 @@ public class StoreInfoController {
 
     private final StoreInfoService storeInfoService;
 
+    private final StoreQualificationService storeQualificationService;
+
     private final TagsMainMapper tagsMainMapper;
 
     private final WebAuditMapper webAuditMapper;
@@ -1024,6 +1028,12 @@ public class StoreInfoController {
         return R.data(ocrData);
     }
 
+    @TrackEvent(
+            eventType = "VIEW",
+            eventCategory = "TRAFFIC",
+            storeId = "#{#id}",
+            targetType = "STORE"
+    )
     @ApiOperation(value = "获取店铺详情(用户端)")
     @ApiOperationSupport(order = 17)
     @GetMapping("/getClientStoreDetail")
@@ -1036,6 +1046,16 @@ public class StoreInfoController {
         StoreInfoVo storeDetail = storeInfoService.getClientStoreDetail(id, userId, jingdu, weidu);
         return R.data(storeDetail);
     }
+
+    @ApiOperation(value = "查询当前门店的资质信息", notes = "查询营业执照和其他资质证明图片")
+    @ApiOperationSupport(order = 18)
+    @GetMapping("/getStoreQualificationInfo")
+    @ResponseBody
+    public R<StoreQualificationInfoVo> getStoreQualificationInfo(@RequestParam("storeId") Integer storeId) {
+        log.info("StoreInfoController.getStoreQualificationInfo?storeId={}", storeId);
+        StoreQualificationInfoVo qualificationInfo = storeQualificationService.getStoreQualificationInfo(storeId);
+        return R.data(qualificationInfo);
+    }
     /**
      * 查询四种类型店铺(酒吧、ktv、洗浴汗蒸、按摩足浴)并按距离筛选
      *

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/StorePriceController.java

@@ -12,6 +12,7 @@ import shop.alien.entity.result.R;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StorePrice;
 import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.StorePriceService;
 import shop.alien.util.encryption.Decrypt;
 import shop.alien.util.encryption.Encrypt;
@@ -183,6 +184,12 @@ public class StorePriceController {
         return R.fail("批量删除失败");
     }
 
+    @TrackEvent(
+            eventType = "PRICE_VIEW",
+            eventCategory = "PRICE",
+            storeId = "#{#storeId}",
+            targetType = "PRICE"
+    )
     @ApiOperation("分页查询通用价目")
     @ApiOperationSupport(order = 6)
     @ApiImplicitParams({

+ 259 - 0
alien-store/src/main/java/shop/alien/store/controller/StoreRenovationRequirementController.java

@@ -0,0 +1,259 @@
+package shop.alien.store.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
+import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+import shop.alien.store.annotation.TrackEvent;
+import shop.alien.store.service.StoreRenovationBrowseRecordService;
+import shop.alien.store.service.StoreRenovationRequirementService;
+
+import java.util.List;
+
+/**
+ * 装修需求动态表 Controller
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Slf4j
+@Api(tags = {"装修需求动态"})
+@ApiSort(1)
+@CrossOrigin
+@RestController
+@RequestMapping("/renovation/requirement")
+@RequiredArgsConstructor
+public class StoreRenovationRequirementController {
+
+    private final StoreRenovationRequirementService requirementService;
+    private final StoreRenovationBrowseRecordService browseRecordService;
+
+    @ApiOperation("发布或更新装修需求")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/saveOrUpdate")
+    public R<Boolean> saveOrUpdate(@RequestBody StoreRenovationRequirementDto dto) {
+        log.info("StoreRenovationRequirementController.saveOrUpdate?dto={}", dto);
+        try {
+            boolean result = requirementService.saveOrUpdateRequirement(dto);
+            if (result) {
+                return R.success("操作成功");
+            }
+            return R.fail("操作失败");
+        } catch (Exception e) {
+            log.error("保存装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取装修需求详情")
+    @ApiOperationSupport(order = 2)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getDetail")
+    public R<StoreRenovationRequirementDto> getDetail(@RequestParam Integer id) {
+        log.info("StoreRenovationRequirementController.getDetail?id={}", id);
+        try {
+            // 增加浏览数
+            requirementService.incrementViewCount(id);
+            
+            StoreRenovationRequirementDto dto = requirementService.getRequirementDetail(id);
+            if (dto == null) {
+                return R.fail("装修需求不存在");
+            }
+            return R.data(dto);
+        } catch (Exception e) {
+            log.error("获取装修需求详情失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("分页查询装修需求列表")
+    @ApiOperationSupport(order = 3)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "page", value = "页码", dataType = "Integer", paramType = "query", defaultValue = "1"),
+            @ApiImplicitParam(name = "size", value = "每页数量", dataType = "Integer", paramType = "query", defaultValue = "10"),
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "city", value = "城市", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "renovationType", value = "装修类型(1:新房装修, 2:旧房改造, 3:局部装修)", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "status", value = "状态(0:草稿, 1:已发布, 2:已下架)", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "auditStatus", value = "审核状态(0:待审核, 1:审核通过, 2:审核失败)", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "sortType", value = "排序类型(1:最新发布, 2:价格最高, 3:面积最大)", dataType = "Integer", paramType = "query")
+    })
+    @GetMapping("/getPage")
+    public R<IPage<StoreRenovationRequirementDto>> getPage(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) Integer storeId,
+            @RequestParam(required = false) String city,
+            @RequestParam(required = false) Integer renovationType,
+            @RequestParam(required = false) Integer status,
+            @RequestParam(required = false) Integer auditStatus,
+            @RequestParam(required = false, defaultValue = "1") Integer sortType) {
+        log.info("StoreRenovationRequirementController.getPage?page={}, size={}, storeId={}, city={}, renovationType={}, status={}, auditStatus={}, sortType={}",
+                page, size, storeId, city, renovationType, status, auditStatus, sortType);
+        try {
+            Page<StoreRenovationRequirementDto> pageParam = new Page<>(page, size);
+            IPage<StoreRenovationRequirementDto> result = requirementService.getRequirementPage(
+                    pageParam, storeId, city, renovationType, status, auditStatus, sortType);
+            return R.data(result);
+        } catch (Exception e) {
+            log.error("分页查询装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("更新状态(草稿/发布/下架)")
+    @ApiOperationSupport(order = 4)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "status", value = "状态(0:草稿, 1:已发布, 2:已下架)", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/updateStatus")
+    public R<Boolean> updateStatus(@RequestParam Integer id, @RequestParam Integer status) {
+        log.info("StoreRenovationRequirementController.updateStatus?id={}, status={}", id, status);
+        try {
+            boolean result = requirementService.updateStatus(id, status);
+            if (result) {
+                return R.success("更新成功");
+            }
+            return R.fail("更新失败");
+        } catch (Exception e) {
+            log.error("更新状态失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("审核装修需求")
+    @ApiOperationSupport(order = 5)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "auditStatus", value = "审核状态(1:审核通过, 2:审核失败)", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "auditReason", value = "审核原因(审核失败时必填)", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "auditUserId", value = "审核人ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/audit")
+    public R<Boolean> audit(@RequestParam Integer id,
+                            @RequestParam Integer auditStatus,
+                            @RequestParam(required = false) String auditReason,
+                            @RequestParam Integer auditUserId) {
+        log.info("StoreRenovationRequirementController.audit?id={}, auditStatus={}, auditReason={}, auditUserId={}",
+                id, auditStatus, auditReason, auditUserId);
+        try {
+            boolean result = requirementService.auditRequirement(id, auditStatus, auditReason, auditUserId);
+            if (result) {
+                return R.success("审核成功");
+            }
+            return R.fail("审核失败");
+        } catch (Exception e) {
+            log.error("审核装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("删除装修需求")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/delete")
+    public R<Boolean> delete(@RequestParam Integer id) {
+        log.info("StoreRenovationRequirementController.delete?id={}", id);
+        try {
+            boolean result = requirementService.deleteRequirement(id);
+            if (result) {
+                return R.success("删除成功");
+            }
+            return R.fail("删除失败");
+        } catch (Exception e) {
+            log.error("删除装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("获取门店发布的装修需求列表")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "门店ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @GetMapping("/getListByStoreId")
+    public R<List<StoreRenovationRequirementDto>> getListByStoreId(@RequestParam Integer storeId) {
+        log.info("StoreRenovationRequirementController.getListByStoreId?storeId={}", storeId);
+        try {
+            List<StoreRenovationRequirementDto> list = requirementService.getRequirementListByStoreId(storeId);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("获取门店装修需求列表失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("增加咨询数(装修商家查看需求时调用)")
+    @ApiOperationSupport(order = 8)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "id", value = "需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/incrementInquiryCount")
+    public R<Boolean> incrementInquiryCount(@RequestParam Integer id) {
+        log.info("StoreRenovationRequirementController.incrementInquiryCount?id={}", id);
+        try {
+            requirementService.incrementInquiryCount(id);
+            return R.success("操作成功");
+        } catch (Exception e) {
+            log.error("增加咨询数失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @TrackEvent(
+            eventType = "CONSULT",
+            eventCategory = "INTERACTION",
+            targetId = "#{#requirementId}",
+            targetType = "STORE"
+    )
+    @ApiOperation("装修商铺咨询装修需求(记录浏览和咨询历史)")
+    @ApiOperationSupport(order = 9)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "requirementId", value = "装修需求ID", dataType = "Integer", paramType = "query", required = true)
+    })
+    @PostMapping("/consultRequirement")
+    public R<Boolean> consultRequirement(@RequestParam Integer requirementId) {
+        log.info("StoreRenovationRequirementController.consultRequirement?requirementId={}", requirementId);
+        try {
+            boolean result = browseRecordService.recordInquiryByCurrentUser(requirementId);
+            if (result) {
+                return R.success("咨询记录成功");
+            }
+            return R.fail("咨询记录失败");
+        } catch (Exception e) {
+            log.error("记录咨询失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+
+    @ApiOperation("查询装修商铺浏览过的普通商铺发布的装修需求列表")
+    @ApiOperationSupport(order = 10)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "renovationStoreTel", value = "装修商铺电话(store_info.store_tel)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "normalStoreTel", value = "普通商铺电话(store_info.store_tel,发布装修需求的店铺)", dataType = "String", paramType = "query", required = true)
+    })
+    @GetMapping("/getBrowsedRequirements")
+    public R<List<StoreRenovationBrowseRequirementDto>> getBrowsedRequirements(
+            @RequestParam String renovationStoreTel,
+            @RequestParam String normalStoreTel) {
+        log.info("StoreRenovationRequirementController.getBrowsedRequirements?renovationStoreTel={}, normalStoreTel={}", renovationStoreTel, normalStoreTel);
+        try {
+            List<StoreRenovationBrowseRequirementDto> list = browseRecordService.getBrowsedRequirementsByStoreTel(renovationStoreTel, normalStoreTel);
+            return R.data(list);
+        } catch (Exception e) {
+            log.error("查询浏览过的装修需求失败: {}", e.getMessage(), e);
+            return R.fail(e.getMessage());
+        }
+    }
+}
+

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

@@ -52,7 +52,8 @@ public class StoreStaffConfigController {
             @ApiImplicitParam(name = "size", value = "分页条数", dataType = "Integer", paramType = "query", required = false),
             @ApiImplicitParam(name = "status", value = "员工状态(0-待审核 1-审核通过 2-审核拒绝)", dataType = "String", paramType = "query", required = false),
             @ApiImplicitParam(name = "onlineStatus", value = "上线状态(0-上线 1-下线)", dataType = "Integer", paramType = "query", required = false),
-            @ApiImplicitParam(name = "staffPosition", value = "职位", dataType = "String", paramType = "query", required = false)
+            @ApiImplicitParam(name = "staffPosition", value = "职位", dataType = "String", paramType = "query", required = false),
+            @ApiImplicitParam(name = "staffName", value = "员工姓名", dataType = "String", paramType = "query", required = false),
     })
     @GetMapping("/getStaffConfigList")
     public R<IPage<StoreStaffConfig>> getStaffConfigList(StoreStaffConfigListQueryDto query) {

+ 11 - 0
alien-store/src/main/java/shop/alien/store/controller/TagCategoryController.java

@@ -11,6 +11,7 @@ import shop.alien.entity.store.vo.TagCategoryVo;
 import shop.alien.store.service.StoreTagBusinessRelationService;
 import shop.alien.store.service.StoreTagService;
 import shop.alien.store.service.TagStoreRelationService;
+import shop.alien.store.service.StoreTagDetailService;
 
 import java.util.List;
 
@@ -31,6 +32,7 @@ public class TagCategoryController {
     private final StoreTagService tagCategoryService;
     private final StoreTagBusinessRelationService storeTagBusinessRelationService;
     private final TagStoreRelationService tagStoreRelationService;
+    private final StoreTagDetailService storeTagDetailService;
 
     @ApiOperation("创建标签")
     @ApiOperationSupport(order = 1)
@@ -139,4 +141,13 @@ public class TagCategoryController {
         TagStoreRelation tagStoreRelation = tagStoreRelationService.getTagStoreRelationByStoreId(storeId);
         return R.data(tagStoreRelation);
     }
+
+    @ApiOperation(value = "店铺标签详情接口", notes = "根据门店id查询店铺标签详情(tag_store_relation)")
+    @ApiOperationSupport(order = 11)
+    @GetMapping("/getStoreTagDetail")
+    public R<TagStoreRelation> getStoreTagDetail(@RequestParam(required = true) Integer storeId) {
+        log.info("TagCategoryController.getStoreTagDetail?storeId={}", storeId);
+        TagStoreRelation tagStoreRelation = storeTagDetailService.getStoreTagDetailByStoreId(storeId);
+        return R.data(tagStoreRelation);
+    }
 }

+ 141 - 0
alien-store/src/main/java/shop/alien/store/controller/TrackEventController.java

@@ -0,0 +1,141 @@
+package shop.alien.store.controller;
+
+import com.alibaba.fastjson.JSONObject;
+import io.swagger.annotations.*;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.result.R;
+import shop.alien.entity.store.StoreTrackEvent;
+import shop.alien.store.service.TrackEventService;
+import shop.alien.util.common.JwtUtil;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Date;
+
+/**
+ * 埋点事件Controller
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Api(tags = {"埋点事件管理"})
+@ApiSort(20)
+@CrossOrigin
+@RestController
+@RequestMapping("/track")
+@RequiredArgsConstructor
+public class TrackEventController {
+
+    private final TrackEventService trackEventService;
+
+    @ApiOperation("上报埋点事件")
+    @ApiOperationSupport(order = 1)
+    @PostMapping("/event")
+    public R<String> reportEvent(@RequestBody StoreTrackEvent trackEvent, HttpServletRequest request) {
+        try {
+            // 设置默认值
+            if (trackEvent.getEventTime() == null) {
+                trackEvent.setEventTime(new Date());
+            }
+
+            // 从JWT中获取用户ID(如果未指定)
+            if (trackEvent.getUserId() == null) {
+                try {
+                    JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                    if (userInfo != null && userInfo.get("userId") != null) {
+                        trackEvent.setUserId(userInfo.getInteger("userId"));
+                    }
+                } catch (Exception e) {
+                    log.debug("无法从JWT获取用户ID", e);
+                }
+            }
+
+            // 获取IP地址
+            if (trackEvent.getIpAddress() == null) {
+                trackEvent.setIpAddress(getClientIpAddress(request));
+            }
+
+            // 获取User-Agent
+            if (trackEvent.getUserAgent() == null) {
+                trackEvent.setUserAgent(request.getHeader("User-Agent"));
+            }
+
+            // 根据User-Agent解析设备类型(如果未设置)
+            if (trackEvent.getDeviceType() == null && trackEvent.getUserAgent() != null) {
+                trackEvent.setDeviceType(shop.alien.store.util.UserAgentParserUtil.parseDeviceType(trackEvent.getUserAgent()));
+            }
+
+            // 异步保存埋点事件(写入Redis List)
+            trackEventService.saveTrackEvent(trackEvent);
+
+            return R.success("上报成功");
+        } catch (Exception e) {
+            log.error("上报埋点事件失败", e);
+            return R.fail("上报失败: " + e.getMessage());
+        }
+    }
+
+    @ApiOperation("手动触发统计数据计算(测试用)")
+    @ApiOperationSupport(order = 2)
+    @PostMapping("/statistics/calculate")
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "storeId", value = "店铺ID", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "statDate", value = "统计日期(格式:yyyy-MM-dd)", dataType = "String", paramType = "query", required = true),
+            @ApiImplicitParam(name = "statType", value = "统计类型(DAILY/WEEKLY/MONTHLY)", dataType = "String", paramType = "query", required = true)
+    })
+    public R<String> calculateStatistics(
+            @RequestParam("storeId") Integer storeId,
+            @RequestParam("statDate") String statDateStr,
+            @RequestParam("statType") String statType) {
+        try {
+            // 解析日期
+            java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd");
+            Date statDate = sdf.parse(statDateStr);
+            
+            // 验证统计类型
+            if (!"DAILY".equals(statType) && !"WEEKLY".equals(statType) && !"MONTHLY".equals(statType)) {
+                return R.fail("统计类型必须是 DAILY、WEEKLY 或 MONTHLY");
+            }
+            
+            // 调用统计方法
+            trackEventService.calculateAndSaveStatistics(storeId, statDate, statType);
+            
+            return R.success("统计数据计算成功");
+        } catch (java.text.ParseException e) {
+            log.error("日期格式错误: {}", statDateStr, e);
+            return R.fail("日期格式错误,请使用 yyyy-MM-dd 格式");
+        } catch (Exception e) {
+            log.error("计算统计数据失败", e);
+            return R.fail("计算统计数据失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取客户端IP地址
+     */
+    private String getClientIpAddress(HttpServletRequest request) {
+        String ip = request.getHeader("X-Forwarded-For");
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("WL-Proxy-Client-IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_CLIENT_IP");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+        }
+        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
+            ip = request.getRemoteAddr();
+        }
+        // 如果是多级代理,取第一个IP
+        if (ip != null && ip.contains(",")) {
+            ip = ip.split(",")[0].trim();
+        }
+        return ip;
+    }
+}

+ 7 - 0
alien-store/src/main/java/shop/alien/store/controller/UserStoreController.java

@@ -26,6 +26,7 @@ import shop.alien.entity.store.StoreCommentAppeal;
 import shop.alien.entity.store.vo.StoreCommentAppealVo;
 import shop.alien.mapper.StoreCommentAppealMapper;
 import shop.alien.mapper.StoreCommentMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeUserStoreService;
 import shop.alien.util.common.AlipayTradeAppPay;
 import shop.alien.util.common.ListToPage;
@@ -95,6 +96,12 @@ public class UserStoreController {
         return R.data(ListToPage.setPage(result, page, size));
     }
 
+    @TrackEvent(
+            eventType = "VIEW",
+            eventCategory = "TRAFFIC",
+            storeId = "#{#storeId}",
+            targetType = "STORE"
+    )
     @ApiOperation("查询商铺详情")
     @ApiOperationSupport(order = 2)
     @ApiImplicitParams({@ApiImplicitParam(name = "storeId", value = "商铺id", dataType = "String", paramType = "query"),

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

@@ -14,6 +14,7 @@ import shop.alien.entity.store.LifeUserViolation;
 import shop.alien.entity.store.UserLoginInfo;
 import shop.alien.entity.store.dto.LifeUserViolationDto;
 import shop.alien.mapper.LifeNoticeMapper;
+import shop.alien.store.annotation.TrackEvent;
 import shop.alien.store.service.LifeUserViolationService;
 import shop.alien.util.common.JwtUtil;
 import shop.alien.util.common.TokenInfo;
@@ -42,6 +43,14 @@ public class UserViolationController {
 
     private final LifeNoticeMapper lifeNoticeMapper;
 
+    @TrackEvent(
+            eventType = "REPORT",
+            eventCategory = "INTERACTION",
+            storeId = "",
+            userId = "#lifeUserViolation.reportingUserId",
+            targetId = "#lifeUserViolation.dynamicsId != null ? #lifeUserViolation.dynamicsId : #lifeUserViolation.commentId",
+            targetType = "STORE"
+    )
     @ApiOperation("举报")
     @ApiOperationSupport(order = 1)
     @PostMapping("/reporting")

+ 38 - 1
alien-store/src/main/java/shop/alien/store/service/CommonCommentService.java

@@ -2,6 +2,11 @@ package shop.alien.store.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.store.CommonComment;
+import shop.alien.entity.store.vo.CommonCommentVo;
+import shop.alien.entity.store.vo.CommonRatingVo;
+
+import java.util.List;
+import java.util.Map;
 
 /**
  * 评论表 服务类
@@ -10,7 +15,39 @@ import shop.alien.entity.store.CommonComment;
  * @since 2025-01-XX
  */
 public interface CommonCommentService extends IService<CommonComment> {
-
+    /**
+     * 新增评论
+     * @param commonComment
+     * @return
+     */
     Integer addComment(CommonComment commonComment);
+
+    /**
+     * 根据动态类型和动态id查询评论列表
+     * @param sourceType
+     * @param sourceId
+     * @param pageNum
+     * @param pageSize
+     * @param userId
+     * @return
+     */
+    List<CommonCommentVo> getListBySourceType(Integer sourceType, Integer sourceId, Integer pageNum, Integer pageSize, Long userId);
+
+
+    /**
+     *
+     * @param sourceType
+     * @param sourceId
+     * @param pageNum
+     * @param pageSize
+     * @param userId
+     * @param likeType
+     * @return
+     */
+    List<CommonCommentVo> getFirstLevelComment(Integer sourceType, Integer sourceId, Integer pageNum, Integer pageSize, Long userId, String likeType);
+
+    void getAllChildComment(Long userId, CommonRatingVo commonRatingVo, List<CommonCommentVo> commonComments, String likeType);
+
+    Map<String,Object> getCommitCount(Integer sourceId, Integer sourceType, String userId, String userType);
 }
 

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

@@ -20,7 +20,7 @@ public interface CommonRatingService extends IService<CommonRating> {
      */
     Integer saveCommonRating(CommonRating commonRating);
 
-/*        *
+    /**
      * 分页查询评价列表
      *
      * @param pageNum      页数
@@ -30,9 +30,12 @@ public interface CommonRatingService extends IService<CommonRating> {
      * @param userId       用户ID
      * @param auditStatus  审核状态:0-待审核 1-通过 2-驳回
      * @param searchScore  搜索评分:0-全部,1-好评 2-中评 3-差评,4-有图
+     * @param days         查询时间, 多少天前
+     * @param replyStatus  回复状态(0:全部, 1:已回复, 2:未回复)
+     * @param tagId        标签id
      * @return IPage<CommonRating>
      */
-    R getRatingList(Integer pageNum, Integer pageSize, Integer businessType, Long businessId, Long userId, Integer auditStatus, Integer searchScore);
+    R getRatingList(Integer pageNum, Integer pageSize, Integer businessType, Long businessId, Long userId, Integer auditStatus, Integer searchScore, Integer days, Integer replyStatus, Integer tagId);
 
      /**
      * 获取店铺评价数量(好评,中评,差评)
@@ -51,6 +54,15 @@ public interface CommonRatingService extends IService<CommonRating> {
      */
     Object getRatingDetail(Integer ratingId, Long userId);
 
+    /**
+     * 获取回复率和评价比例
+     *
+     * @param businessId   业务ID(店铺ID)
+     * @param businessType 业务类型:1-店铺评价
+     * @return 回复率和评价比例信息
+     */
+    shop.alien.entity.store.vo.RatingPercentVo getRatingPercent(Integer businessId, Integer businessType);
+
 
   /*  /**
      * 根据业务类型和业务ID获取平均评分

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

@@ -196,7 +196,7 @@ public class LifeCommentService {
                 return commonRatingMapper.update(null, new UpdateWrapper<CommonRating>()
                         .setSql("like_count = like_count + 1")
                         .eq("id", huifuId));
-            } else if (CommonConstant.COMMENT_LIKE.equals(type)) {
+            } else if (CommonConstant.COMMENT_LIKE.equals(type) || CommonConstant.DYNAMIC_LIKE.equals(type)) {
                 // 12-评论点赞:更新评论表点赞数
                 return commonCommentMapper.update(null, new UpdateWrapper<CommonComment>()
                         .setSql("like_count = like_count + 1")
@@ -364,7 +364,7 @@ public class LifeCommentService {
                 return commonRatingMapper.update(null, new UpdateWrapper<CommonRating>()
                         .setSql("like_count = like_count - 1")
                         .eq("id", huifuId));
-            } else if (CommonConstant.COMMENT_LIKE.equals(type)) {
+            } else if (CommonConstant.COMMENT_LIKE.equals(type) || CommonConstant.DYNAMIC_LIKE.equals(type)) {
                 // 12-评论点赞:更新评论表点赞数
                 return commonCommentMapper.update(null, new UpdateWrapper<CommonComment>()
                         .setSql("like_count = like_count - 1")

+ 21 - 14
alien-store/src/main/java/shop/alien/store/service/LifeUserDynamicsService.java

@@ -13,11 +13,9 @@ import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 import shop.alien.entity.store.*;
-import shop.alien.entity.store.vo.LifePinglunVo;
-import shop.alien.entity.store.vo.LifeUserDynamicsVo;
-import shop.alien.entity.store.vo.StoreCommentVo;
-import shop.alien.entity.store.vo.StoreUserVo;
+import shop.alien.entity.store.vo.*;
 import shop.alien.mapper.*;
+import shop.alien.util.common.constant.CommentSourceTypeEnum;
 
 import java.util.*;
 import java.util.stream.Collectors;
@@ -54,6 +52,8 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
 
     private final StoreInfoMapper storeInfoMapper;
 
+    private final CommonCommentMapper commonCommentMapper;
+
     public int addLiulanCount(String id) {
         LambdaUpdateWrapper<LifeUserDynamics> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
         lambdaUpdateWrapper.eq(LifeUserDynamics::getId, id);
@@ -199,8 +199,10 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
             lifeUserDynamicsVoList = lifeUserDynamicsVoList.stream().filter(item -> followList.contains(item.getPhoneId())).collect(Collectors.toList());
         }
 
-        List<StoreCommentVo> rootCommitCount = storeCommentMapper.getRootCommitCount(2, null);
-        List<StoreCommentVo> sonCommitCount = storeCommentMapper.getSonCommitCount(2, null);
+//        List<StoreCommentVo> rootCommitCount = storeCommentMapper.getRootCommitCount(2, null);
+//        List<StoreCommentVo> sonCommitCount = storeCommentMapper.getSonCommitCount(2, null);
+
+        List<CommonCommentVo> commonCommentVo= commonCommentMapper.getCommentCount(CommentSourceTypeEnum.DYNAMIC_COMMENT.getType());
 
         // 设置动态对象的状态信息:是否关注对方、是否被关注、是否点赞及评论数量
         // 设置.imagePath。视频为mp4+jpg格式,图片为jpg/png格式。
@@ -220,17 +222,21 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
             } else {
                 vo.setIsLike("0");
             }
-            List<StoreCommentVo> rootList = rootCommitCount.stream().filter(item -> Objects.equals(item.getBusinessId(), vo.getId())).collect(Collectors.toList());
-            if (rootList.isEmpty()) {
+            List<CommonCommentVo> collect = commonCommentVo.stream().filter(x -> x.getSourceId().equals(Long.parseLong(vo.getId().toString()))).collect(Collectors.toList());
+//            List<StoreCommentVo> rootList = rootCommitCount.stream().filter(item -> Objects.equals(item.getBusinessId(), vo.getId())).collect(Collectors.toList());
+            if (collect.isEmpty()) {
                 vo.setCommentCount(0);
             } else {
-                Integer count = rootList.size();
-                for (StoreCommentVo storeCommentVo : rootList) {
-                    List<StoreCommentVo> sonList = sonCommitCount.stream().filter(item -> Objects.equals(item.getReplyId(), storeCommentVo.getId())).collect(Collectors.toList());
-                    if (!sonList.isEmpty()) {
-                        count += sonList.get(0).getCommitCount();
-                    }
+                Integer count = collect.size();
+                for (CommonCommentVo commentVo : collect) {
+                    count += commentVo.getCommentCount();
                 }
+//                for (CommonCommentVo storeCommentVo : collect) {
+//                    List<StoreCommentVo> sonList = sonCommitCount.stream().filter(item -> Objects.equals(item.getReplyId(), storeCommentVo.getId())).collect(Collectors.toList());
+//                    if (!sonList.isEmpty()) {
+//                        count += sonList.get(0).getCommitCount();
+//                    }
+//                }
                 vo.setCommentCount(count);
             }
         }
@@ -440,6 +446,7 @@ public class LifeUserDynamicsService extends ServiceImpl<LifeUserDynamicsMapper,
             if (storeUser != null) {
                 if (storeUser.getStoreId() != null) {
                     StoreInfo storeInfo = storeInfoMapper.selectById(storeUser.getStoreId());
+                    resultMap.put("businessSection",storeInfo.getBusinessSection());
                     if (storeInfo != null && storeInfo.getStoreName() != null) {
                         // 使用店铺名称作为昵称
                         storeUser.setUserName(storeInfo.getStoreName());

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

@@ -145,6 +145,8 @@ public class LifeUserService extends ServiceImpl<LifeUserMapper, LifeUser> {
                 .like("a.storeUserName", vo.getSearchName())
                 .or()
                 .like("a.phoneId", vo.getSearchName())
+                .or()
+                .like("a.accountName", vo.getSearchName())
             );
         }
         IPage<LifeUserVo> voList = lifeUserMapper.getStoreAndUserByName(new Page<>(vo.getPage(), vo.getSize()), queryWrapper);

+ 21 - 0
alien-store/src/main/java/shop/alien/store/service/StoreQualificationService.java

@@ -0,0 +1,21 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.vo.StoreQualificationInfoVo;
+
+/**
+ * 门店资质信息服务接口
+ *
+ * @author system
+ * @since 2025-01-01
+ */
+public interface StoreQualificationService {
+    
+    /**
+     * 根据门店id查询资质信息
+     * 包括:营业执照、食品经营许可证、娱乐经营许可证
+     *
+     * @param storeId 门店id
+     * @return 门店资质信息
+     */
+    StoreQualificationInfoVo getStoreQualificationInfo(Integer storeId);
+}

+ 52 - 0
alien-store/src/main/java/shop/alien/store/service/StoreRenovationBrowseRecordService.java

@@ -0,0 +1,52 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreRenovationBrowseRecord;
+import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
+
+import java.util.List;
+
+/**
+ * 装修商铺访问装修需求浏览记录表 服务类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+public interface StoreRenovationBrowseRecordService extends IService<StoreRenovationBrowseRecord> {
+
+    /**
+     * 记录装修商铺浏览装修需求(仅浏览)
+     *
+     * @param renovationStoreId 装修商铺ID
+     * @param requirementId     装修需求ID
+     * @return 是否成功
+     */
+    boolean recordBrowse(Integer renovationStoreId, Integer requirementId);
+
+    /**
+     * 记录装修商铺咨询装修需求(浏览并咨询)
+     *
+     * @param renovationStoreId 装修商铺ID
+     * @param requirementId     装修需求ID
+     * @return 是否成功
+     */
+    boolean recordInquiry(Integer renovationStoreId, Integer requirementId);
+
+    /**
+     * 记录当前登录装修商铺咨询装修需求(自动获取当前用户的门店ID)
+     *
+     * @param requirementId 装修需求ID
+     * @return 是否成功
+     */
+    boolean recordInquiryByCurrentUser(Integer requirementId);
+
+    /**
+     * 查询装修商铺浏览过的普通商铺发布的装修需求列表
+     *
+     * @param renovationStoreTel 装修商铺电话(store_info.store_tel)
+     * @param normalStoreTel     普通商铺电话(store_info.store_tel,发布装修需求的店铺)
+     * @return 浏览过的装修需求列表(包含浏览信息)
+     */
+    List<StoreRenovationBrowseRequirementDto> getBrowsedRequirementsByStoreTel(String renovationStoreTel, String normalStoreTel);
+}
+

+ 105 - 0
alien-store/src/main/java/shop/alien/store/service/StoreRenovationRequirementService.java

@@ -0,0 +1,105 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import shop.alien.entity.store.StoreRenovationRequirement;
+import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+
+import java.util.List;
+
+/**
+ * 装修需求动态表 服务类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+public interface StoreRenovationRequirementService extends IService<StoreRenovationRequirement> {
+
+    /**
+     * 发布装修需求(保存或更新)
+     *
+     * @param dto 装修需求DTO
+     * @return 是否成功
+     */
+    boolean saveOrUpdateRequirement(StoreRenovationRequirementDto dto);
+
+    /**
+     * 根据ID获取装修需求详情
+     *
+     * @param id 需求ID
+     * @return 装修需求DTO
+     */
+    StoreRenovationRequirementDto getRequirementDetail(Integer id);
+
+    /**
+     * 分页查询装修需求列表
+     *
+     * @param page 分页对象
+     * @param storeId 门店ID(可选)
+     * @param city 城市(可选)
+     * @param renovationType 装修类型(可选,1:新房装修, 2:旧房改造, 3:局部装修)
+     * @param status 状态(可选,0:草稿, 1:已发布, 2:已下架)
+     * @param auditStatus 审核状态(可选,0:待审核, 1:审核通过, 2:审核失败)
+     * @param sortType 排序类型(1:最新发布, 2:价格最高, 3:面积最大),默认为1
+     * @return 分页结果
+     */
+    IPage<StoreRenovationRequirementDto> getRequirementPage(Page<StoreRenovationRequirementDto> page,
+                                                             Integer storeId,
+                                                             String city,
+                                                             Integer renovationType,
+                                                             Integer status,
+                                                             Integer auditStatus,
+                                                             Integer sortType);
+
+    /**
+     * 更新浏览数
+     *
+     * @param id 需求ID
+     */
+    void incrementViewCount(Integer id);
+
+    /**
+     * 更新咨询数
+     *
+     * @param id 需求ID
+     */
+    void incrementInquiryCount(Integer id);
+
+    /**
+     * 更新状态
+     *
+     * @param id 需求ID
+     * @param status 状态(0:草稿, 1:已发布, 2:已下架)
+     * @return 是否成功
+     */
+    boolean updateStatus(Integer id, Integer status);
+
+    /**
+     * 审核装修需求
+     *
+     * @param id 需求ID
+     * @param auditStatus 审核状态(1:审核通过, 2:审核失败)
+     * @param auditReason 审核原因(审核失败时必填)
+     * @param auditUserId 审核人ID
+     * @return 是否成功
+     */
+    boolean auditRequirement(Integer id, Integer auditStatus, String auditReason, Integer auditUserId);
+
+    /**
+     * 删除装修需求(逻辑删除)
+     *
+     * @param id 需求ID
+     * @return 是否成功
+     */
+    boolean deleteRequirement(Integer id);
+
+    /**
+     * 获取门店发布的装修需求列表
+     *
+     * @param storeId 门店ID
+     * @return 装修需求列表
+     */
+    List<StoreRenovationRequirementDto> getRequirementListByStoreId(Integer storeId);
+}
+

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

@@ -0,0 +1,20 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.TagStoreRelation;
+
+/**
+ * 店铺标签详情服务接口
+ *
+ * @author system
+ * @since 2025-01-01
+ */
+public interface StoreTagDetailService {
+    
+    /**
+     * 根据门店id查询店铺标签详情
+     *
+     * @param storeId 门店id
+     * @return 店铺标签详情
+     */
+    TagStoreRelation getStoreTagDetailByStoreId(Integer storeId);
+}

+ 84 - 0
alien-store/src/main/java/shop/alien/store/service/TrackEventConsumer.java

@@ -0,0 +1,84 @@
+package shop.alien.store.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import shop.alien.entity.store.StoreTrackEvent;
+import shop.alien.store.config.BaseRedisService;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * 埋点事件消费服务
+ * 定时从Redis List批量消费数据并写入数据库
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TrackEventConsumer {
+
+    private final BaseRedisService baseRedisService;
+    private final TrackEventService trackEventService;
+    private final ObjectMapper objectMapper;
+    
+    private static final String REDIS_QUEUE_KEY = "track:event:queue";
+    private static final String CONSUMER_LOCK_KEY = "track:event:consumer:lock";
+
+    /**
+     * 定时消费埋点数据
+     * 每10秒执行一次
+     */
+    @Scheduled(cron = "0/10 * * * * ?")
+    public void consumeTrackEvents() {
+        // 获取分布式锁
+        String lockId = baseRedisService.lock(CONSUMER_LOCK_KEY, 5000, 1000);
+        if (lockId == null) {
+            log.debug("获取消费锁失败,跳过本次消费");
+            return;
+        }
+
+        try {
+            // 批量从Redis List取出数据
+            List<String> eventList = baseRedisService.popBatchFromList(REDIS_QUEUE_KEY);
+            
+            if (eventList == null || eventList.isEmpty()) {
+                return;
+            }
+
+            log.debug("从Redis List取出{}条埋点数据", eventList.size());
+
+            // 转换为实体对象
+            List<StoreTrackEvent> events = eventList.stream()
+                    .map(json -> {
+                        try {
+                            return objectMapper.readValue(json, StoreTrackEvent.class);
+                        } catch (Exception e) {
+                            log.error("解析埋点数据JSON失败: {}", json, e);
+                            return null;
+                        }
+                    })
+                    .filter(event -> event != null)
+                    .collect(Collectors.toList());
+
+            if (events.isEmpty()) {
+                return;
+            }
+
+            // 批量保存到数据库
+            trackEventService.batchSaveTrackEvents(events);
+            
+            log.info("成功消费{}条埋点数据", events.size());
+        } catch (Exception e) {
+            log.error("消费埋点数据失败", e);
+        } finally {
+            // 释放锁
+            baseRedisService.unlock(CONSUMER_LOCK_KEY, lockId);
+        }
+    }
+}

+ 38 - 0
alien-store/src/main/java/shop/alien/store/service/TrackEventService.java

@@ -0,0 +1,38 @@
+package shop.alien.store.service;
+
+import shop.alien.entity.store.StoreTrackEvent;
+
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 埋点事件服务接口
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+public interface TrackEventService {
+
+    /**
+     * 保存埋点事件(异步写入Redis)
+     *
+     * @param trackEvent 埋点事件
+     */
+    void saveTrackEvent(StoreTrackEvent trackEvent);
+
+    /**
+     * 批量保存埋点事件(写入数据库)
+     *
+     * @param trackEvents 埋点事件列表
+     */
+    void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents);
+
+    /**
+     * 计算并保存统计数据
+     *
+     * @param id              店铺ID
+     * @param lastWeekMonday  上周一的日期
+     * @param weekly          统计类型(DAILY/WEEKLY/MONTHLY)
+     */
+    void calculateAndSaveStatistics(Integer id, Date lastWeekMonday, String weekly);
+}

+ 171 - 0
alien-store/src/main/java/shop/alien/store/service/TrackStatisticsScheduler.java

@@ -0,0 +1,171 @@
+package shop.alien.store.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.entity.store.StoreInfo;
+
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 统计数据定时任务
+ * 定时计算并保存统计数据到store_track_statistics表
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TrackStatisticsScheduler {
+    
+    private final TrackEventService trackEventService;
+    private final StoreInfoMapper storeInfoMapper;
+
+    /**
+     * 每天凌晨1点执行,计算前一天的统计数据
+     * cron表达式: 秒 分 时 日 月 周
+     */
+    @Scheduled(cron = "0 0 1 * * ?")
+    public void calculateDailyStatistics() {
+        log.info("开始执行日统计数据计算任务");
+        
+        try {
+            // 计算前一天的日期
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.DAY_OF_MONTH, -1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date yesterday = calendar.getTime();
+            
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            log.info("计算日期: {}", sdf.format(yesterday));
+            
+            // 查询所有店铺(只查询未删除的)
+            LambdaQueryWrapper<StoreInfo> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreInfo::getDeleteFlag, 0);
+            List<StoreInfo> stores = storeInfoMapper.selectList(wrapper);
+            log.info("共{}个店铺需要计算统计数据", stores.size());
+            
+            int successCount = 0;
+            int failCount = 0;
+            
+            for (StoreInfo store : stores) {
+                try {
+                    trackEventService.calculateAndSaveStatistics(store.getId(), yesterday, "DAILY");
+                    successCount++;
+                    log.debug("店铺{}的日统计数据计算成功", store.getId());
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("店铺{}的日统计数据计算失败", store.getId(), e);
+                }
+            }
+            
+            log.info("日统计数据计算任务完成: 成功{}, 失败{}", successCount, failCount);
+        } catch (Exception e) {
+            log.error("执行日统计数据计算任务失败", e);
+        }
+    }
+
+    /**
+     * 每周一凌晨2点执行,计算上一周的统计数据
+     */
+    @Scheduled(cron = "0 0 2 ? * MON")
+    public void calculateWeeklyStatistics() {
+        log.info("开始执行周统计数据计算任务");
+        
+        try {
+            // 计算上一周的日期(上周一)
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.WEEK_OF_YEAR, -1);
+            calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date lastWeekMonday = calendar.getTime();
+            
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            log.info("计算日期: {}", sdf.format(lastWeekMonday));
+            
+            // 查询所有店铺(只查询未删除的)
+            LambdaQueryWrapper<StoreInfo> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreInfo::getDeleteFlag, 0);
+            List<StoreInfo> stores = storeInfoMapper.selectList(wrapper);
+            log.info("共{}个店铺需要计算统计数据", stores.size());
+            
+            int successCount = 0;
+            int failCount = 0;
+            
+            for (StoreInfo store : stores) {
+                try {
+                    trackEventService.calculateAndSaveStatistics(store.getId(), lastWeekMonday, "WEEKLY");
+                    successCount++;
+                    log.debug("店铺{}的周统计数据计算成功", store.getId());
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("店铺{}的周统计数据计算失败", store.getId(), e);
+                }
+            }
+            
+            log.info("周统计数据计算任务完成: 成功{}, 失败{}", successCount, failCount);
+        } catch (Exception e) {
+            log.error("执行周统计数据计算任务失败", e);
+        }
+    }
+
+    /**
+     * 每月1号凌晨3点执行,计算上一个月的统计数据
+     */
+    @Scheduled(cron = "0 0 3 1 * ?")
+    public void calculateMonthlyStatistics() {
+        log.info("开始执行月统计数据计算任务");
+        
+        try {
+            // 计算上一个月的日期(上月1号)
+            Calendar calendar = Calendar.getInstance();
+            calendar.add(Calendar.MONTH, -1);
+            calendar.set(Calendar.DAY_OF_MONTH, 1);
+            calendar.set(Calendar.HOUR_OF_DAY, 0);
+            calendar.set(Calendar.MINUTE, 0);
+            calendar.set(Calendar.SECOND, 0);
+            calendar.set(Calendar.MILLISECOND, 0);
+            Date lastMonthFirstDay = calendar.getTime();
+            
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+            log.info("计算日期: {}", sdf.format(lastMonthFirstDay));
+            
+            // 查询所有店铺(只查询未删除的)
+            LambdaQueryWrapper<StoreInfo> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(StoreInfo::getDeleteFlag, 0);
+            List<StoreInfo> stores = storeInfoMapper.selectList(wrapper);
+            log.info("共{}个店铺需要计算统计数据", stores.size());
+            
+            int successCount = 0;
+            int failCount = 0;
+            
+            for (StoreInfo store : stores) {
+                try {
+                    trackEventService.calculateAndSaveStatistics(store.getId(), lastMonthFirstDay, "MONTHLY");
+                    successCount++;
+                    log.debug("店铺{}的月统计数据计算成功", store.getId());
+                } catch (Exception e) {
+                    failCount++;
+                    log.error("店铺{}的月统计数据计算失败", store.getId(), e);
+                }
+            }
+            
+            log.info("月统计数据计算任务完成: 成功{}, 失败{}", successCount, failCount);
+        } catch (Exception e) {
+            log.error("执行月统计数据计算任务失败", e);
+        }
+    }
+}

+ 117 - 8
alien-store/src/main/java/shop/alien/store/service/impl/BarPerformanceServiceImpl.java

@@ -18,6 +18,8 @@ import shop.alien.mapper.StoreStaffConfigMapper;
 import shop.alien.store.service.BarPerformanceService;
 import shop.alien.store.util.ai.AiContentModerationUtil;
 
+import java.time.LocalTime;
+import java.time.ZoneId;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -115,13 +117,22 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
                 if (barPerformance.getDailyStartDate().after(barPerformance.getDailyEndDate())) {
                     throw new IllegalArgumentException("开始日期不能晚于结束日期");
                 }
-                // 验证时间字段(复用single_start_datetime和single_end_datetime,只使用时间部分)
+                // 验证时间字段(使用single_start_datetime和single_end_datetime,提取时间部分)
                 if (barPerformance.getSingleStartDatetime() == null) {
                     throw new IllegalArgumentException("每天定时演出必须填写开始时间");
                 }
                 if (barPerformance.getSingleEndDatetime() == null) {
                     throw new IllegalArgumentException("每天定时演出必须填写结束时间");
                 }
+                // 从single_start_datetime和single_end_datetime中提取时间部分,存到daily_start_time和daily_end_time
+                LocalTime startTime = barPerformance.getSingleStartDatetime().toInstant()
+                        .atZone(ZoneId.systemDefault())
+                        .toLocalTime();
+                LocalTime endTime = barPerformance.getSingleEndDatetime().toInstant()
+                        .atZone(ZoneId.systemDefault())
+                        .toLocalTime();
+                barPerformance.setDailyStartTime(startTime);
+                barPerformance.setDailyEndTime(endTime);
                 // 每天定时不需要周天数据,清除周天字段(设置为空字符串,确保数据库插入时包含该字段)
                 barPerformance.setPerformanceWeek("");
                 break;
@@ -144,13 +155,22 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
                 if (StringUtils.isEmpty(barPerformance.getPerformanceWeek())) {
                     throw new IllegalArgumentException("每周定时演出必须选择演出日期");
                 }
-                // 验证时间字段(复用single_start_datetime和single_end_datetime,只使用时间部分)
+                // 验证时间字段(使用single_start_datetime和single_end_datetime,提取时间部分)
                 if (barPerformance.getSingleStartDatetime() == null) {
                     throw new IllegalArgumentException("每周定时演出必须填写开始时间");
                 }
                 if (barPerformance.getSingleEndDatetime() == null) {
                     throw new IllegalArgumentException("每周定时演出必须填写结束时间");
                 }
+                // 从single_start_datetime和single_end_datetime中提取时间部分,存到daily_start_time和daily_end_time
+                LocalTime weeklyStartTime = barPerformance.getSingleStartDatetime().toInstant()
+                        .atZone(ZoneId.systemDefault())
+                        .toLocalTime();
+                LocalTime weeklyEndTime = barPerformance.getSingleEndDatetime().toInstant()
+                        .atZone(ZoneId.systemDefault())
+                        .toLocalTime();
+                barPerformance.setDailyStartTime(weeklyStartTime);
+                barPerformance.setDailyEndTime(weeklyEndTime);
                 break;
             default:
                 throw new IllegalArgumentException("演出频次类型无效");
@@ -231,10 +251,6 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
             if (barPerformance.getDeleteFlag() == null) {
                 barPerformance.setDeleteFlag(0);
             }
-            // 设置默认状态为禁用(0)
-            if (barPerformance.getStatus() == null) {
-                barPerformance.setStatus(0);
-            }
             // 审核状态已在AI审核结果后设置(1-审核通过 或 2-审核拒绝)
             // 如果AI审核通过,reviewStatus已设置为1,并且onlineStatus已设置为1(上线)
             // 如果AI审核失败,reviewStatus已设置为2,onlineStatus保持为null或0(下线)
@@ -341,11 +357,34 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
             return null;
         }
 
+        // 检查并更新已删除的员工配置ID
+        String originalStaffConfigIds = barPerformance.getStaffConfigIds();
+        String updatedStaffConfigIds = checkAndUpdateDeletedStaff(originalStaffConfigIds);
+        
+        // 如果员工配置ID有变化,更新数据库
+        if (updatedStaffConfigIds != null && !updatedStaffConfigIds.equals(originalStaffConfigIds)) {
+            try {
+                LambdaUpdateWrapper<BarPerformance> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.eq(BarPerformance::getId, id);
+                updateWrapper.set(BarPerformance::getStaffConfigIds, updatedStaffConfigIds);
+                int updateResult = barPerformanceMapper.update(null, updateWrapper);
+                if (updateResult > 0) {
+                    // 更新成功,同步更新本地对象
+                    barPerformance.setStaffConfigIds(updatedStaffConfigIds);
+                    log.info("酒吧演出ID={}的演出嘉宾字段已更新,原值:{},新值:{}", 
+                            id, originalStaffConfigIds, updatedStaffConfigIds);
+                }
+            } catch (Exception e) {
+                log.error("更新酒吧演出ID={}的演出嘉宾字段失败,异常信息:{}", 
+                        id, e.getMessage(), e);
+            }
+        }
+
         // 转换为VO对象
         BarPerformanceDetailVo detailVo = new BarPerformanceDetailVo();
         BeanUtils.copyProperties(barPerformance, detailVo);
 
-        // 查询并设置表演嘉宾列表
+        // 查询并设置表演嘉宾列表(使用更新后的staffConfigIds)
         List<PerformerVo> performers = queryPerformersByStaffConfigIds(barPerformance.getStaffConfigIds());
         detailVo.setPerformers(performers);
 
@@ -353,6 +392,76 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
     }
 
     /**
+     * 检查并更新已删除的员工配置ID
+     * 如果发现员工被删除,从staffConfigIds中移除这些已删除的员工ID
+     * 
+     * @param staffConfigIds 员工配置ID字符串,逗号分隔,如 "41,25"
+     * @return 更新后的员工配置ID字符串,如果无变化则返回原值
+     */
+    private String checkAndUpdateDeletedStaff(String staffConfigIds) {
+        if (StringUtils.isEmpty(staffConfigIds)) {
+            return staffConfigIds;
+        }
+
+        try {
+            // 解析员工ID字符串
+            String[] idArray = staffConfigIds.split(",");
+            List<Integer> originalStaffIdList = new ArrayList<>();
+            
+            for (String idStr : idArray) {
+                if (StringUtils.isNotEmpty(idStr.trim())) {
+                    try {
+                        Integer staffId = Integer.parseInt(idStr.trim());
+                        if (staffId > 0) {
+                            originalStaffIdList.add(staffId);
+                        }
+                    } catch (NumberFormatException e) {
+                        log.warn("解析员工配置ID失败,无效的ID:{}", idStr);
+                    }
+                }
+            }
+
+            if (originalStaffIdList.isEmpty()) {
+                return staffConfigIds;
+            }
+
+            // 批量查询员工配置信息(MyBatis-Plus的selectBatchIds会自动过滤已删除的记录)
+            List<StoreStaffConfig> staffList = storeStaffConfigMapper.selectBatchIds(originalStaffIdList);
+            
+            // 获取实际查询到的员工ID列表(未删除的员工)
+            final List<Integer> existingStaffIdList;
+            if (staffList != null && !staffList.isEmpty()) {
+                existingStaffIdList = staffList.stream()
+                        .filter(staff -> staff != null && staff.getDeleteFlag() != null && staff.getDeleteFlag() == 0)
+                        .map(StoreStaffConfig::getId)
+                        .collect(Collectors.toList());
+            } else {
+                existingStaffIdList = new ArrayList<>();
+            }
+
+            // 比较原始ID列表和查询到的ID列表,找出被删除的员工ID
+            List<Integer> deletedStaffIdList = originalStaffIdList.stream()
+                    .filter(id -> !existingStaffIdList.contains(id))
+                    .collect(Collectors.toList());
+
+            // 如果有员工被删除,更新staffConfigIds字段
+            if (!deletedStaffIdList.isEmpty()) {
+                log.info("发现酒吧演出关联的员工被删除,员工ID列表:{}", deletedStaffIdList);
+                // 构建新的staffConfigIds字符串(只包含未删除的员工ID)
+                String updatedStaffConfigIds = existingStaffIdList.stream()
+                        .map(String::valueOf)
+                        .collect(Collectors.joining(","));
+                return updatedStaffConfigIds;
+            }
+        } catch (Exception e) {
+            log.error("检查并更新已删除的员工配置ID异常,staffConfigIds={},异常信息:{}", 
+                    staffConfigIds, e.getMessage(), e);
+        }
+
+        return staffConfigIds;
+    }
+
+    /**
      * 根据员工配置ID字符串查询表演嘉宾列表
      * 
      * @param staffConfigIds 员工配置ID字符串,逗号分隔,如 "41,25"
@@ -387,7 +496,7 @@ public class BarPerformanceServiceImpl implements BarPerformanceService {
                 return performerList;
             }
 
-            // 批量查询员工配置信息
+            // 批量查询员工配置信息(MyBatis-Plus的selectBatchIds会自动过滤已删除的记录)
             List<StoreStaffConfig> staffList = storeStaffConfigMapper.selectBatchIds(staffIdList);
             
             if (staffList != null && !staffList.isEmpty()) {

+ 10 - 3
alien-store/src/main/java/shop/alien/store/service/impl/CommentAppealServiceImpl.java

@@ -197,7 +197,7 @@ public class CommentAppealServiceImpl extends ServiceImpl<CommentAppealMapper, C
             lawyerUser.eq(LawyerUser::getId, appeal.getLawyerUserId());
             LawyerUser lifeUser = lawyerUserMapper.selectOne(lawyerUser);
             if (lifeUser == null) {
-                log.warn("评价用户不存在,userId={}", lifeUser.getId());
+                log.warn("律师用户不存在,lawyerUserId={}", appeal.getLawyerUserId());
                 return;
             }
             LambdaQueryWrapper<OrderReview> orderReviewLambdaQueryWrapper = new LambdaQueryWrapper<>();
@@ -547,16 +547,23 @@ public class CommentAppealServiceImpl extends ServiceImpl<CommentAppealMapper, C
                 pageNum, pageSize, status, lawyerUserId);
         List<CommentAppealVo> appealList = new ArrayList<>();
         //status 3 查全部
-        if (status == 3) {
+        if (status != null && status == 3) {
             appealList = baseMapper.getAppealHistoryList(null, lawyerUserId);
         } else {
             // 查询申诉历史列表
             appealList = baseMapper.getAppealHistoryList(status, lawyerUserId);
         }
 
+        // 防止空指针异常:如果查询结果为null,初始化为空列表
+        if (appealList == null) {
+            appealList = new ArrayList<>();
+        }
+
         // 处理数据转换(图片列表等)
         for (CommentAppealVo vo : appealList) {
-            processAppealVo(vo);
+            if (vo != null) {
+                processAppealVo(vo);
+            }
         }
 
         ListToPage.setPage(appealList, pageNum, pageSize);

+ 208 - 3
alien-store/src/main/java/shop/alien/store/service/impl/CommonCommentServiceImpl.java

@@ -1,17 +1,29 @@
 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.toolkit.CollectionUtils;
+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.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
-import shop.alien.entity.store.CommonComment;
-import shop.alien.mapper.CommonCommentMapper;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.vo.CommonCommentVo;
+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.util.common.constant.CommentSourceTypeEnum;
 import shop.alien.util.common.safe.TextModerationResultVO;
 import shop.alien.util.common.safe.TextModerationUtil;
 
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
 /**
  * 评论表 服务实现类
  *
@@ -27,19 +39,212 @@ public class CommonCommentServiceImpl extends ServiceImpl<CommonCommentMapper, C
     @Autowired
     private TextModerationUtil textModerationUtil;
 
+    private final CommonCommentMapper commonCommentMapper;
+    private final LifeUserDynamicsMapper lifeUserDynamicsMapper;
+    private final LifeLikeRecordMapper lifeLikeRecordMapper;
+    private final StoreUserMapper storeUserMapper;
+    private final LifeUserViolationMapper lifeUserViolationMapper;
+    private final LifeUserMapper lifeUserMapper;
+
+
+    /**
+     * 新增评论
+     *
+     * @param commonComment 评论对象
+     * @return 0:成功, 1:失败, 2:文本内容异常, 4:字数超限(超过300字)
+     */
     @Override
     public Integer addComment(CommonComment commonComment) {
+        // 校验评论内容字数限制(300字)
+        if (commonComment.getContent() != null && commonComment.getContent().length() > 300) {
+            log.warn("评论内容超过300字限制,content length={}", commonComment.getContent().length());
+            return 4; // 字数超限
+        }
+
+        // 文本内容审核
         TextModerationResultVO textCheckResult = null;
         try {
             textCheckResult = textModerationUtil.invokeFunction(commonComment.getContent(), CommonRatingServiceImpl.SERVICES_LIST);
             if ("high".equals(textCheckResult.getRiskLevel())) {
-                return 2;
+                return 2; // 文本内容异常(包含敏感词)
             }
             return this.save(commonComment) ? 0 : 1;
         } catch (Exception e) {
+            log.error("新增评论失败", e);
             return 1;
         }
     }
+
+    @Override
+    public List<CommonCommentVo> getListBySourceType(Integer sourceType, Integer sourceId, Integer pageNum, Integer pageSize, Long userId) {
+        String likeType = null;
+        if(sourceType == CommentSourceTypeEnum.STORE_COMMENT.getType()){
+            likeType = CommonConstant.COMMENT_LIKE;
+        } else if (sourceType == CommentSourceTypeEnum.DYNAMIC_COMMENT.getType()) {
+            likeType = CommonConstant.DYNAMIC_LIKE;
+        }
+        List<CommonCommentVo> firstLevelComment = getFirstLevelComment(sourceType, sourceId, pageNum, pageSize, userId,likeType);
+        CommonRatingVo commonRatingVo = new CommonRatingVo();
+        getAllChildComment(userId, commonRatingVo, firstLevelComment,likeType);
+        return commonRatingVo.getChildCommonComments();
+    }
+
+    @Override
+    public List<CommonCommentVo> getFirstLevelComment(Integer sourceType, Integer sourceId, Integer pageNum, Integer pageSize, Long userId, String likeType){
+        List<CommonCommentVo> commonComments = null;
+        QueryWrapper<CommonCommentVo> commentWrapper = new QueryWrapper<CommonCommentVo>();
+        if(sourceType == CommentSourceTypeEnum.STORE_COMMENT.getType()){
+            // 1查询店铺评价
+            commentWrapper.eq("cc.source_type", CommentSourceTypeEnum.STORE_COMMENT.getType());
+
+        } else if (sourceType == CommentSourceTypeEnum.DYNAMIC_COMMENT.getType()) {
+            // 2查询动态评论
+            commentWrapper.eq("cc.source_type", CommentSourceTypeEnum.DYNAMIC_COMMENT.getType());
+        }
+        commentWrapper.eq("cc.source_id", sourceId)
+                .eq("cc.parent_id", 0);
+        Page<CommonCommentVo> page = null;
+        if( null != pageNum && null != pageSize){
+            page = new Page<>(pageNum, pageSize);
+        }
+        commonComments = commonCommentMapper.selectALlComment(page, commentWrapper, likeType, userId);
+        return commonComments;
+    }
+
+    @Override
+    public void getAllChildComment(Long userId, CommonRatingVo commonRatingVo, List<CommonCommentVo> commonComments, String likeType) {
+        // 定义评论总数
+        AtomicReference<Long> count = new AtomicReference<>(0L);
+        count.updateAndGet(v -> v + commonComments.size());
+        List<CommonCommentVo> commonCommentVos = new ArrayList<>();
+        for (CommonCommentVo commonComment : commonComments) {
+//                CommonCommentVo commonCommentVo = new CommonCommentVo();
+//                BeanUtils.copyProperties(commonComment, commonCommentVo);
+            // 递归获取所有子评论(扁平化)
+            List<CommonCommentVo> allChildComments = getChildCommentsRecursively(commonComment.getId(), userId,likeType);
+            count.updateAndGet(v -> v + allChildComments.size());
+            // 一级评论本身的商家/用户标识和商家信息
+            // setStoreUserInfo(first);
+
+            // 按时间排序后绑定子评论列表
+            allChildComments.sort(Comparator.comparing(CommonCommentVo::getCreatedTime));
+
+            commonComment.setChildCommonComments(allChildComments);
+            commonCommentVos.add(commonComment);
+        }
+        commonRatingVo.setCommentCount(count.get());
+        commonRatingVo.setChildCommonComments(commonCommentVos);
+    }
+
+    /**
+     * 动态评论数量 目前给的是动态用的接口,可以根据sorceType返回不同数据
+     * @param sourceId
+     * @param sourceType
+     * @param userId
+     * @return
+     */
+    @Override
+    public Map<String, Object> getCommitCount(Integer sourceId, Integer sourceType, String userId, String userType) {
+        // 返回分享数,喜欢数,评论数
+        Map<String, Object> map = new HashMap<>();
+        // 计算评论数
+        QueryWrapper<CommonComment> queryWrapper = new QueryWrapper<>();
+        queryWrapper.eq(null != sourceId, "source_id", sourceId);
+        queryWrapper.eq(null != sourceType, "source_type", sourceType);
+        //通过当前登录人id和类型 查询举报业务id 不包含已举报的
+        List<LifeUserViolation> lifeUserViolations = new ArrayList<>();
+        if(sourceType == CommentSourceTypeEnum.DYNAMIC_COMMENT.getType() ){
+            if("user".equals(userType)){
+                LifeUser lifeUser = lifeUserMapper.selectOne(new LambdaQueryWrapper<LifeUser>().eq(LifeUser::getId,userId));
+                if(lifeUser!=null){
+                    LambdaQueryWrapper<LifeUserViolation> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+                    lambdaQueryWrapper.eq(LifeUserViolation::getReportingUserId,lifeUser.getId());
+                    lambdaQueryWrapper.eq(LifeUserViolation::getReportingUserType,2);
+                    lambdaQueryWrapper.ne(LifeUserViolation::getProcessingStatus,2);
+                    lifeUserViolations = lifeUserViolationMapper.selectList(lambdaQueryWrapper);
+                }
+            } else {
+                StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>().eq(StoreUser::getId,userId));
+                if(storeUser!=null){
+                    LambdaQueryWrapper<LifeUserViolation> lambdaQueryWrapper = new LambdaQueryWrapper<>();
+                    lambdaQueryWrapper.eq(LifeUserViolation::getReportingUserId,storeUser.getId());
+                    lambdaQueryWrapper.eq(LifeUserViolation::getReportingUserType,1);
+                    lambdaQueryWrapper.ne(LifeUserViolation::getProcessingStatus,2);
+                    lifeUserViolations = lifeUserViolationMapper.selectList(lambdaQueryWrapper);
+                }
+            }
+        }
+        List<Integer> businessIds = lifeUserViolations.stream()
+                .filter(Objects::nonNull)
+                .map(LifeUserViolation::getBusinessId)
+                .filter(id -> id != null && !id.toString().isEmpty())
+                .collect(Collectors.toList());
+        if(businessIds!=null && businessIds.size()>0){
+            queryWrapper.notIn("id",businessIds);
+        }
+
+        Integer i = commonCommentMapper.selectCount(queryWrapper);
+        map.put("commentCount", i);
+        getOtherDataWithSourceType(sourceId, sourceType, userId, map);
+        return map;
+    }
+
+    private void getOtherDataWithSourceType(Integer sourceId, Integer sourceType, String userId, Map<String, Object> map) {
+        if ( sourceType == CommentSourceTypeEnum.DYNAMIC_COMMENT.getType() ) {
+            // 计算分析数和喜欢数
+            LifeUserDynamics lifeUserDynamics = lifeUserDynamicsMapper.selectOne(new QueryWrapper<LifeUserDynamics>().eq("id", sourceId));
+            if(null != lifeUserDynamics){
+                map.put("likeCount",lifeUserDynamics.getDianzanCount());
+                map.put("transferCount",lifeUserDynamics.getTransferCount());
+            } else {
+                map.put("likeCount",0);
+                map.put("transferCount",0);
+            }
+            // 查询商家头像 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():"");
+            // 查询当前用户是否喜欢
+            LifeLikeRecord lifeLikeRecord = lifeLikeRecordMapper.selectOne(new QueryWrapper<LifeLikeRecord>()
+                    .eq("type", CommonConstant.LIKE_TYPE_DYNAMICS)
+                    .eq("dianzan_id", userId)
+                    .eq("huifu_id", sourceId)
+                    .eq("delete_flag", 0));
+            if(null != lifeLikeRecord){
+                map.put("isLike",1);
+            } else {
+                map.put("isLike",0);
+            }
+        }
+    }
+
+    private List<CommonCommentVo> getChildCommentsRecursively(Long id, Long userId, String likeType) {
+        List<CommonCommentVo> allChildComments = new ArrayList<>();
+
+        // 查询直接回复当前评论的所有记录
+        QueryWrapper<CommonCommentVo> wrapper = new QueryWrapper<>();
+        wrapper.eq("cc.delete_flag", 0)
+                .eq("cc.parent_id", id)
+                .orderByAsc("cc.created_time");
+        List<CommonCommentVo> directChildren = commonCommentMapper.selectALlComment(null,wrapper,likeType, userId);
+
+        if (CollectionUtils.isEmpty(directChildren)) {
+            return allChildComments;
+        }
+        // 处理每个直接子评论
+        for (CommonCommentVo child : directChildren) {
+            // 设置商家/用户标识
+//            setStoreUserInfo(child);
+            // 递归获取该子评论的所有子评论
+            List<CommonCommentVo> grandChildren = getChildCommentsRecursively(child.getId(), userId,likeType);
+            // 将当前子评论添加到结果列表
+            allChildComments.add(child);
+            // 将该子评论的所有子评论也添加到结果列表(扁平化)
+            allChildComments.addAll(grandChildren);
+        }
+
+        return allChildComments;
+    }
+
 //
 //    @Override
 //    public IPage<CommonComment> getCommentList(Integer pageNum, Integer pageSize, Integer sourceType, Long sourceId, Long parentId, Integer commentType, Integer auditStatus) {

+ 336 - 85
alien-store/src/main/java/shop/alien/store/service/impl/CommonRatingServiceImpl.java

@@ -21,10 +21,13 @@ 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.util.common.constant.CommentSourceTypeEnum;
@@ -32,6 +35,7 @@ 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;
@@ -66,6 +70,9 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
     private final LifeLikeRecordMapper lifeLikeRecordMapper;
     private final LifeCollectMapper lifeCollectMapper;
     private final LifeFansMapper lifeFansMapper;
+    private final TagsSynonymMapper tagsSynonymMapper;
+    private final CommonCommentService commonCommentService;
+
 
 
     public static final List<String> SERVICES_LIST = ImmutableList.of(
@@ -152,21 +159,24 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         }
 
     }
-
     @Override
-    public R getRatingList(Integer pageNum, Integer pageSize, Integer businessType, Long businessId, Long userId, Integer auditStatus, Integer searchScore) {
+    public R getRatingList(Integer pageNum, Integer pageSize, Integer businessType, Long businessId, Long userId, Integer auditStatus, Integer searchScore, Integer days, Integer replyStatus, Integer tagId) {
         Page<CommonRating> page = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<CommonRating> wrapper = new LambdaQueryWrapper<>();
         
+        // 业务类型筛选
         if (businessType != null) {
             wrapper.eq(CommonRating::getBusinessType, businessType);
         }
+        // 业务ID筛选
         if (businessId != null) {
             wrapper.eq(CommonRating::getBusinessId, businessId);
         }
+        // 审核状态筛选
         if (auditStatus != null) {
             wrapper.eq(CommonRating::getAuditStatus, auditStatus);
         }
+        // 评分等级筛选
         if (searchScore != null) {
             if(searchScore == 1){
                 // 1-好评
@@ -182,17 +192,89 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
             }else if(searchScore == 4){
                 // 4-有图
                 wrapper.isNotNull(CommonRating::getImageUrls);
-                // 2. 排除 空字符串 ""
                 wrapper.ne(CommonRating::getImageUrls, "");
-                // 3. 排除 纯空格字符串(如"   ")—— 用 TRIM 函数去掉首尾空格后判断非空
                 wrapper.apply("TRIM({0}) <> ''", "image_urls");
             }
         }
-        
+        // 时间范围筛选
+        if (days != null && days > 0) {
+            Date date = DateUtils.calcDays(new Date(), -days);
+            wrapper.ge(CommonRating::getCreatedTime, date);
+        }
+        // 回复状态筛选(使用EXISTS子查询在数据库层面筛选)
+        if (replyStatus != null && (replyStatus == 1 || replyStatus == 2)) {
+            if (replyStatus == 1) {
+                // 已回复:存在商户评论
+                wrapper.apply("EXISTS (SELECT 1 FROM common_comment cc WHERE cc.source_type = 1 " +
+                             "AND cc.source_id = common_rating.id AND cc.comment_type = 2 " +
+                             "AND cc.is_show = 1 AND cc.audit_status = 1 AND cc.delete_flag = 0)");
+            } else if (replyStatus == 2) {
+                // 未回复:不存在商户评论
+                wrapper.apply("NOT EXISTS (SELECT 1 FROM common_comment cc WHERE cc.source_type = 1 " +
+                             "AND cc.source_id = common_rating.id AND cc.comment_type = 2 " +
+                             "AND cc.is_show = 1 AND cc.audit_status = 1 AND cc.delete_flag = 0)");
+            }
+        }
+        // 标签筛选
+        if (tagId != null) {
+            // 1. 查询标签关联的评论ID列表(tags_synonym.comment_id 关联 common_comment.id)
+            LambdaQueryWrapper<TagsSynonym> tagWrapper = new LambdaQueryWrapper<>();
+            tagWrapper.eq(TagsSynonym::getMainTagId, tagId)
+                     .eq(TagsSynonym::getDeleteFlag, 0)
+                     .isNotNull(TagsSynonym::getCommentId);  // 确保comment_id不为空
+            List<TagsSynonym> tagsSynonymList = tagsSynonymMapper.selectList(tagWrapper);
+
+            if (CollectionUtils.isNotEmpty(tagsSynonymList)) {
+                // 2. 提取评论ID列表(common_comment.id)
+                List<Long> commentIdList = tagsSynonymList.stream()
+                        .filter(synonym -> synonym != null && synonym.getCommentId() != null)
+                        .map(synonym -> synonym.getCommentId().longValue())
+                        .distinct()
+                        .collect(Collectors.toList());
+
+                if (CollectionUtils.isNotEmpty(commentIdList)) {
+                    // 3. 通过评论ID查询common_comment表,获取source_id(评价ID)
+                    LambdaQueryWrapper<CommonComment> commentWrapper = new LambdaQueryWrapper<>();
+                    commentWrapper.in(CommonComment::getId, commentIdList)
+                                 .eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())  // source_type=1表示评价的评论
+                                 .eq(CommonComment::getIsShow, 1)
+                                 .eq(CommonComment::getAuditStatus, 1);
+                    List<CommonComment> comments = commonCommentMapper.selectList(commentWrapper);
+
+                    if (CollectionUtils.isNotEmpty(comments)) {
+                        // 4. 提取评价ID列表(common_comment.source_id = common_rating.id)
+                        List<Long> ratingIdList = comments.stream()
+                                .filter(comment -> comment.getSourceId() != null)
+                                .map(CommonComment::getSourceId)
+                                .distinct()
+                                .collect(Collectors.toList());
+
+                        if (CollectionUtils.isNotEmpty(ratingIdList)) {
+                            wrapper.in(CommonRating::getId, ratingIdList);
+                        } else {
+                            // 如果没有匹配的评价ID,返回空结果
+                            wrapper.eq(CommonRating::getId, -1);
+                        }
+                    } else {
+                        // 如果没有匹配的评论,返回空结果
+                        wrapper.eq(CommonRating::getId, -1);
+                    }
+                } else {
+                    // 如果没有评论ID,返回空结果
+                    wrapper.eq(CommonRating::getId, -1);
+                }
+            } else {
+                // 如果没有标签关联,返回空结果
+                wrapper.eq(CommonRating::getId, -1);
+            }
+        }
+
         wrapper.eq(CommonRating::getIsShow, 1);
         wrapper.orderByDesc(CommonRating::getId);
         IPage<CommonRating> page1 = this.page(page, wrapper);
-        return doListBusinessWithType(page1, businessType,userId);
+
+        // 处理回复状态筛选
+        return doListBusinessWithType(page1, businessType, userId, replyStatus);
     }
 
     @Override
@@ -211,6 +293,29 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         }
         // 获取评价统计信息(总评论数、有图评论数、好评数、中评数、差评数)
         ratingCount = commonRatingMapper.getRatingCount(new QueryWrapper<CommonRating>().in("id", collect));
+
+        // 计算好评、中评、差评占比
+        // 注意:数据库返回的 count 可能是 BigDecimal、Long 或 Integer 类型,需要安全转换
+        int goodCount = getIntValue(ratingCount.get("goodCount"));
+        int midCount = getIntValue(ratingCount.get("midCount"));
+        int badCount = getIntValue(ratingCount.get("badCount"));
+        int totalCount = goodCount + midCount + badCount;
+
+        if (totalCount > 0) {
+            // 计算占比(保留2位小数)
+            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;
+
+            ratingCount.put("goodPercent", goodPercent);
+            ratingCount.put("midPercent", midPercent);
+            ratingCount.put("badPercent", badPercent);
+        } else {
+            ratingCount.put("goodPercent", 0.0);
+            ratingCount.put("midPercent", 0.0);
+            ratingCount.put("badPercent", 0.0);
+        }
+
         if(RatingBusinessTypeEnum.STORE_RATING.getBusinessType() == businessType){
             // 1店铺评分
             StoreInfo storeInfo = storeInfoMapper.selectById(businessId);
@@ -236,7 +341,7 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
             List<Long> collect2 = commonRatings.stream().filter(i -> i.getScore() >= 4.5).map(CommonRating::getUserId).distinct().limit(6).collect(Collectors.toList());
             if(!collect2.isEmpty()) {
                 List<LifeUser> lifeUsers = lifeUserMapper.selectList(new QueryWrapper<LifeUser>().lambda().in(LifeUser::getId, collect2));
-                ratingCount.put("img", lifeUsers.stream().filter(x -> x.getUserImage() != null).map(LifeUser::getUserImage).collect(Collectors.toList()));
+                ratingCount.put("img", lifeUsers.stream().map(LifeUser::getUserImage).collect(Collectors.toList()));
             } else {
                 ratingCount.put("img", new ArrayList<>());
             }
@@ -333,62 +438,9 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 commonRatingVo.setIsCollect(0);
             }
             // 2查询一级评价
-            QueryWrapper<CommonCommentVo> commentWrapper = new QueryWrapper<CommonCommentVo>()
-                    .eq("cc.source_type", CommentSourceTypeEnum.STORE_COMMENT.getType())
-                    .eq("cc.source_id", ratingId)
-                    .eq("cc.parent_id", 0);
-            List<CommonCommentVo> commonComments = commonCommentMapper.selectALlComment(commentWrapper,CommonConstant.COMMENT_LIKE, userId);
-
-            // 定义评论总数
-            AtomicReference<Long> count = new AtomicReference<>(0L);
-            count.updateAndGet(v -> v + commonComments.size());
-            List<CommonCommentVo> commonCommentVos = new ArrayList<>();
-            for (CommonCommentVo commonComment : commonComments) {
-//                CommonCommentVo commonCommentVo = new CommonCommentVo();
-//                BeanUtils.copyProperties(commonComment, commonCommentVo);
-                // 递归获取所有子评论(扁平化)
-                List<CommonCommentVo> allChildComments = getChildCommentsRecursively(commonComment.getId(), userId);
-                count.updateAndGet(v -> v + allChildComments.size());
-                // 一级评论本身的商家/用户标识和商家信息
-                // setStoreUserInfo(first);
-
-                // 按时间排序后绑定子评论列表
-                allChildComments.sort(Comparator.comparing(CommonCommentVo::getCreatedTime));
-
-                commonComment.setChildCommonComments(allChildComments);
-                commonCommentVos.add(commonComment);
-            }
-            commonRatingVo.setCommentCount(count.get());
-            commonRatingVo.setChildCommonComments(commonCommentVos);
-        }
-    }
-
-    private List<CommonCommentVo> getChildCommentsRecursively(Long id, Long userId) {
-        List<CommonCommentVo> allChildComments = new ArrayList<>();
-
-        // 查询直接回复当前评论的所有记录
-        QueryWrapper<CommonCommentVo> wrapper = new QueryWrapper<>();
-        wrapper.eq("cc.delete_flag", 0)
-                .eq("cc.parent_id", id)
-                .orderByAsc("cc.created_time");
-        List<CommonCommentVo> directChildren = commonCommentMapper.selectALlComment(wrapper,CommonConstant.COMMENT_LIKE, userId);
-
-        if (CollectionUtils.isEmpty(directChildren)) {
-            return allChildComments;
+            List<CommonCommentVo> commonComments = commonCommentService.getFirstLevelComment(CommentSourceTypeEnum.STORE_COMMENT.getType(), ratingId,null,null, userId,CommonConstant.COMMENT_LIKE);
+            commonCommentService.getAllChildComment(userId, commonRatingVo, commonComments,CommonConstant.COMMENT_LIKE);
         }
-        // 处理每个直接子评论
-        for (CommonCommentVo child : directChildren) {
-            // 设置商家/用户标识
-//            setStoreUserInfo(child);
-            // 递归获取该子评论的所有子评论
-            List<CommonCommentVo> grandChildren = getChildCommentsRecursively(child.getId(), userId);
-            // 将当前子评论添加到结果列表
-            allChildComments.add(child);
-            // 将该子评论的所有子评论也添加到结果列表(扁平化)
-            allChildComments.addAll(grandChildren);
-        }
-
-        return allChildComments;
     }
 
     @NotNull
@@ -403,19 +455,29 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         }
     }
 
-    private R doListBusinessWithType(IPage<CommonRating> page1, Integer businessType, Long userId) {
+    /**
+     * 根据业务类型处理评价列表
+     *
+     * @param page1       评价分页数据
+     * @param businessType 业务类型
+     * @param userId      用户ID
+     * @param replyStatus 回复状态(0:全部, 1:已回复, 2:未回复)
+     * @return R<IPage<CommonRatingVo>>
+     */
+    private R<IPage<CommonRatingVo>> doListBusinessWithType(IPage<CommonRating> page1, Integer businessType, Long userId, Integer replyStatus) {
         if(businessType == RatingBusinessTypeEnum.STORE_RATING.getBusinessType()){
-            IPage<CommonRatingVo> result = new Page<>(page1.getPages(), page1.getSize(), page1.getTotal());
-            List resultList = new ArrayList();
+            IPage<CommonRatingVo> result = new Page<>(page1.getCurrent(), page1.getSize(), page1.getTotal());
+            List<CommonRatingVo> resultList = new ArrayList<>();
             if(page1.getRecords().isEmpty()){
                 result.setRecords(resultList);
                 return R.data(result);
             }
-            // 1查询评价用户信息
+
+            // 1. 查询评价用户信息
             Set<Long> userIdSet = page1.getRecords().stream()
                     .map(CommonRating::getUserId)
                     .collect(Collectors.toSet());
-            List<LifeUser> lifeUsers = new  ArrayList<>();
+            List<LifeUser> lifeUsers = new ArrayList<>();
             Map<Integer, LifeUser> lifeUserMap = new HashMap<>();
             if (!userIdSet.isEmpty()){
                 lifeUsers = lifeUserMapper.selectList(
@@ -424,49 +486,109 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
                 lifeUserMap = lifeUsers.stream()
                         .collect(Collectors.toMap(LifeUser::getId, Function.identity()));
             }
-            // 2查询当前用户点赞列表(仅评价)
-            List<LifeLikeRecord> lifeLikeRecords = lifeLikeRecordMapper.selectList(
-                    new QueryWrapper<LifeLikeRecord>().lambda()
-                    .eq(LifeLikeRecord::getDianzanId, userId)
-                    .eq(LifeLikeRecord::getType, CommonConstant.RATING_LIKE));
-            Map<String, LifeLikeRecord> likeRecordMap = lifeLikeRecords.stream()
-                    .collect(Collectors.toMap(LifeLikeRecord::getHuifuId, Function.identity()));
 
+            // 2. 查询当前用户点赞列表(仅评价)
+            List<LifeLikeRecord> lifeLikeRecords = new ArrayList<>();
+            Map<String, LifeLikeRecord> likeRecordMap = new HashMap<>();
+            if (userId != null) {
+                lifeLikeRecords = lifeLikeRecordMapper.selectList(
+                        new QueryWrapper<LifeLikeRecord>().lambda()
+                        .eq(LifeLikeRecord::getDianzanId, userId)
+                        .eq(LifeLikeRecord::getType, CommonConstant.RATING_LIKE));
+                likeRecordMap = lifeLikeRecords.stream()
+                        .collect(Collectors.toMap(LifeLikeRecord::getHuifuId, Function.identity()));
+            }
 
-            // 1.查询评价的评论的记录个数
+            // 3. 查询评价的评论记录个数和商户回复状态
             Set<Long> ratingIdSet = page1.getRecords().stream()
                     .map(CommonRating::getId)
                     .collect(Collectors.toSet());
-            LambdaQueryWrapper<CommonComment> commentWrapper = new LambdaQueryWrapper<CommonComment>()
-                    .eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())
-                    .in(CommonComment::getSourceId, ratingIdSet);
-            // 评价id对应的所有评论 定义Map存储「评价ID -> 该评价下的总评论数」
-            Map<Long, Long> ratingCommentCountMap = commonCommentMapper.selectList(commentWrapper).stream()
+
+            // 查询所有评论(用于统计评论数)
+            LambdaQueryWrapper<CommonComment> commentWrapper = new LambdaQueryWrapper<>();
+            commentWrapper.eq(CommonComment::getSourceType, CommentSourceTypeEnum.STORE_COMMENT.getType())
+                         .in(CommonComment::getSourceId, ratingIdSet)
+                         .eq(CommonComment::getIsShow, 1)
+                         .eq(CommonComment::getAuditStatus, 1);
+            List<CommonComment> allComments = commonCommentMapper.selectList(commentWrapper);
+
+            // 评价ID -> 该评价下的总评论数
+            Map<Long, Long> ratingCommentCountMap = allComments.stream()
                     .collect(Collectors.groupingBy(CommonComment::getSourceId, Collectors.counting()));
 
+            // 查询每个评价的最新一条商户回复(comment_type=2, parent_id=0)
+            Map<Long, CommonCommentVo> latestMerchantReplyMap = new HashMap<>();
+            if (!ratingIdSet.isEmpty()) {
+                QueryWrapper<CommonCommentVo> merchantReplyWrapper = new QueryWrapper<>();
+                merchantReplyWrapper.eq("cc.source_type", CommentSourceTypeEnum.STORE_COMMENT.getType())
+                                   .in("cc.source_id", ratingIdSet)
+                                   .eq("cc.comment_type", 2)  // 商户评论
+                                   .eq("cc.parent_id", 0)     // 直接回复评价的根评论
+                                   .eq("cc.is_show", 1)
+                                   .eq("cc.audit_status", 1)
+                                   .eq("cc.delete_flag", 0)
+                                   .orderByDesc("cc.created_time");
+
+                // 查询所有商户回复(包含用户信息)
+                List<CommonCommentVo> merchantReplies = commonCommentMapper.selectALlComment(
+                        null,
+                    merchantReplyWrapper,
+                    CommonConstant.COMMENT_LIKE,
+                    userId != null ? userId : 0L
+                );
+
+                // 按评价ID分组,每组只取最新的一条(已按时间倒序排序)
+                if (CollectionUtils.isNotEmpty(merchantReplies)) {
+                    for (CommonCommentVo reply : merchantReplies) {
+                        Long ratingId = reply.getSourceId();
+                        // 如果该评价还没有设置回复,则设置(因为已经按时间倒序,第一条就是最新的)
+                        if (ratingId != null && !latestMerchantReplyMap.containsKey(ratingId)) {
+                            latestMerchantReplyMap.put(ratingId, reply);
+                        }
+                    }
+                }
+            }
+
+            // 4. 组装评价列表数据
             for (CommonRating record : page1.getRecords()) {
                 CommonRatingVo commonRatingVo = new CommonRatingVo();
                 BeanUtil.copyProperties(record, commonRatingVo);
-                // 判断用户信息
+
+                // 设置用户信息
                 if(lifeUserMap.containsKey(Integer.parseInt(record.getUserId().toString()))){
                     LifeUser lifeUser = lifeUserMap.get(Integer.parseInt(record.getUserId().toString()));
-                    // 设置评论用户信息
                     commonRatingVo.setUserImage(lifeUser.getUserImage());
                     commonRatingVo.setUserName(lifeUser.getUserName());
                 }
-                // 判断当前登录人是否点赞过
+
+                // 设置点赞状态
                 commonRatingVo.setIsLike(0);
                 if(likeRecordMap.containsKey(record.getId().toString())){
                     commonRatingVo.setIsLike(1);
                 }
-                // 3.1 从映射中获取该评价的总评论数(默认0)
+
+                // 设置评论数
                 commonRatingVo.setCommentCount(ratingCommentCountMap.getOrDefault(record.getId(), 0L));
+
+                // 设置商户最新回复(同一评价下,商家只显示最新的一条回复)
+                CommonCommentVo latestMerchantReply = latestMerchantReplyMap.get(record.getId());
+                if (latestMerchantReply != null) {
+                    // 只设置一条最新的商户回复
+                    commonRatingVo.setChildCommonComments(Collections.singletonList(latestMerchantReply));
+                } else {
+                    // 没有商户回复,设置为空列表
+                    commonRatingVo.setChildCommonComments(new ArrayList<>());
+                }
+
                 resultList.add(commonRatingVo);
             }
+
             result.setRecords(resultList);
             return R.data(result);
         }
-        return null;
+        // 如果不是店铺评价类型,返回空结果
+        IPage<CommonRatingVo> emptyResult = new Page<>(page1.getCurrent(), page1.getSize(), 0);
+        return R.data(emptyResult);
     }
 /*
     @Override
@@ -495,5 +617,134 @@ public class CommonRatingServiceImpl extends ServiceImpl<CommonRatingMapper, Com
         
         return (long) this.count(wrapper);
     }*/
+
+    /**
+     * 安全地将数据库返回的数值类型转换为 int
+     * 支持 BigDecimal、Long、Integer、Number 等类型
+     *
+     * @param value 数据库返回的数值对象
+     * @return int 值,如果为 null 或无法转换则返回 0
+     */
+    private int getIntValue(Object value) {
+        if (value == null) {
+            return 0;
+        }
+
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+
+        // 尝试字符串转换
+        try {
+            if (value instanceof String) {
+                return Integer.parseInt((String) value);
+            }
+        } catch (NumberFormatException e) {
+            log.warn("无法将值转换为 int: {}", value, e);
+        }
+
+        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;
+    }
 }
 

+ 62 - 5
alien-store/src/main/java/shop/alien/store/service/impl/LicenseAuditAsyncService.java

@@ -203,22 +203,57 @@ public class LicenseAuditAsyncService {
                     log.info("{}AI审核拒绝,已删除store_img记录,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl);
                 } else if (needApprove) {
                     // 审核通过:需要检查所有图片是否都审核通过
-                    // 先查询当前记录,获取所有图片URL
+                    // 先查询当前记录,获取所有图片URL(查询状态为2或1的记录,因为第一张图片审核通过后状态会变为1)
                     StoreLicenseHistory currentHistory = licenseHistoryMapper.selectOne(
                             new LambdaQueryWrapper<StoreLicenseHistory>()
                                     .eq(StoreLicenseHistory::getStoreId, storeId)
                                     .eq(StoreLicenseHistory::getLicenseStatus, licenseStatus)
-                                    .eq(StoreLicenseHistory::getLicenseExecuteStatus, 2)
+                                    .in(StoreLicenseHistory::getLicenseExecuteStatus, 1, 2) // 查询状态为1或2的记录
                                     .like(StoreLicenseHistory::getImgUrl, imageUrl)
                                     .eq(StoreLicenseHistory::getDeleteFlag, 0)
+                                    .orderByDesc(StoreLicenseHistory::getCreatedTime) // 按创建时间倒序,获取最新的记录
                                     .last("LIMIT 1")
                     );
                     
                     if (currentHistory != null && StringUtils.isNotEmpty(currentHistory.getImgUrl())) {
-                        // 检查是否所有图片都已审核通过
-                        // 由于是异步审核,每张图片都会调用此方法,所以这里只记录单张图片的审核通过
-                        // 如果需要等所有图片都审核通过才更新状态,需要额外的机制来跟踪
+                        // 如果状态还是2(审核中),更新为1(审核通过)
+                        if (currentHistory.getLicenseExecuteStatus() == 2) {
+                            updateWrapper.set(StoreLicenseHistory::getLicenseExecuteStatus, 1); // 1-审核通过
+                            licenseHistoryMapper.update(null, updateWrapper);
+                        }
                         log.info("{}AI审核通过,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl);
+                        
+                        // 审核通过后,插入store_img表(检查是否已存在,避免重复插入)
+                        StoreImg existingImg = storeImgMapper.selectOne(
+                                new LambdaQueryWrapper<StoreImg>()
+                                        .eq(StoreImg::getStoreId, storeId)
+                                        .eq(StoreImg::getImgType, 35)
+                                        .eq(StoreImg::getImgUrl, imageUrl)
+                                        .eq(StoreImg::getDeleteFlag, 0)
+                        );
+                        if (existingImg == null) {
+                            // 根据imgUrl在allImgUrls中的位置确定imgSort
+                            String[] allImgUrls = currentHistory.getImgUrl().split(",");
+                            int imgSort = 1;
+                            for (int i = 0; i < allImgUrls.length; i++) {
+                                if (allImgUrls[i].equals(imageUrl)) {
+                                    imgSort = i + 1;
+                                    break;
+                                }
+                            }
+                            
+                            StoreImg storeImg = new StoreImg();
+                            storeImg.setStoreId(storeId);
+                            storeImg.setImgType(35); // 其他资质证明对应35
+                            storeImg.setImgDescription("其他资质证明");
+                            storeImg.setImgSort(imgSort);
+                            storeImg.setImgUrl(imageUrl);
+                            storeImg.setDeleteFlag(0);
+                            storeImgMapper.insert(storeImg);
+                            log.info("其他资质证明审核通过,已插入store_img表,门店ID:{},图片URL:{},排序:{}", storeId, imageUrl, imgSort);
+                        } else {
+                            log.info("其他资质证明审核通过,store_img表已存在该记录,门店ID:{},图片URL:{}", storeId, imageUrl);
+                        }
                     }
                 }
             } else {
@@ -251,6 +286,28 @@ public class LicenseAuditAsyncService {
                     updateWrapper.set(StoreLicenseHistory::getLicenseExecuteStatus, 1); // 1-审核通过
                     licenseHistoryMapper.update(null, updateWrapper);
                     log.info("{}AI审核通过,门店ID:{},图片URL:{}", licenseTypeName, storeId, imageUrl);
+                    
+                    // 审核通过后,插入store_img表(检查是否已存在,避免重复插入)
+                    StoreImg existingImg = storeImgMapper.selectOne(
+                            new LambdaQueryWrapper<StoreImg>()
+                                    .eq(StoreImg::getStoreId, storeId)
+                                    .eq(StoreImg::getImgType, 14)
+                                    .eq(StoreImg::getImgUrl, imageUrl)
+                                    .eq(StoreImg::getDeleteFlag, 0)
+                    );
+                    if (existingImg == null) {
+                        StoreImg storeImg = new StoreImg();
+                        storeImg.setStoreId(storeId);
+                        storeImg.setImgType(14); // 营业执照对应14
+                        storeImg.setImgDescription("营业执照");
+                        storeImg.setImgSort(0);
+                        storeImg.setImgUrl(imageUrl);
+                        storeImg.setDeleteFlag(0);
+                        storeImgMapper.insert(storeImg);
+                        log.info("营业执照审核通过,已插入store_img表,门店ID:{},图片URL:{}", storeId, imageUrl);
+                    } else {
+                        log.info("营业执照审核通过,store_img表已存在该记录,门店ID:{},图片URL:{}", storeId, imageUrl);
+                    }
                 }
             }
         } catch (Exception e) {

+ 75 - 1
alien-store/src/main/java/shop/alien/store/service/impl/LifeMessageServiceImpl.java

@@ -19,10 +19,13 @@ import shop.alien.mapper.*;
 import shop.alien.store.service.LifeMessageService;
 import shop.alien.store.service.LifeUserService;
 import shop.alien.util.common.JwtUtil;
+import shop.alien.util.common.constant.LawyerStatusEnum;
 
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.stream.Collectors;
 
@@ -42,10 +45,14 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
     private final LifeBlacklistMapper lifeBlacklistMapper;
     private final LifeNoticeMapper lifeNoticeMapper;
     private final LawyerUserMapper lawyerUserMapper;
+    private final LawyerConsultationOrderMapper lawyerConsultationOrderMapper;
 
     @Override
     public List<LifeMessageVo> getMessageList(String receiverId, int friendType, String search) throws Exception {
         try {
+            if (receiverId == null) {
+                return new ArrayList<>();
+            }
             String blockerType = "user".equals(Objects.requireNonNull(JwtUtil.getCurrentUserInfo()).getString("userType")) ? "2" : "1";
             String blockerId = JwtUtil.getCurrentUserInfo().getString("userId");
             QueryWrapper<LifeFansVo> wrapper = new QueryWrapper<>();
@@ -84,6 +91,21 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
             List<LifeMessageVo> lifeMessagePageList = lifeMessageMapper.getLifeMessagePageByPhoneId(receiverId, wrapper);
             List<LifeMessageVo> lifeMessagePageResultList = new ArrayList<>();
             if (!CollectionUtils.isEmpty(lifeMessagePageList)) {
+                boolean currentIsUser = receiverId != null && receiverId.startsWith("user_");
+                boolean currentIsLawyer = receiverId != null && receiverId.startsWith("lawyer_");
+                Integer currentUserId = null;
+                Integer currentLawyerId = null;
+                if (currentIsUser) {
+                    LifeUser currentUser = lifeUserMapper.selectOne(new QueryWrapper<LifeUser>()
+                            .eq("user_phone", receiverId.substring(5)));
+                    currentUserId = currentUser != null ? currentUser.getId() : null;
+                } else if (currentIsLawyer) {
+                    LawyerUser currentLawyer = lawyerUserMapper.selectOne(new QueryWrapper<LawyerUser>()
+                            .eq("phone", receiverId.substring(7)));
+                    currentLawyerId = currentLawyer != null ? currentLawyer.getId() : null;
+                }
+                Map<String, String> chatEnabledCache = new HashMap<>();
+
                 // 当前用户的所有关注
                 LambdaQueryWrapper<LifeFans> followWrapper = new LambdaQueryWrapper<>();
                 followWrapper.eq(LifeFans::getFansId, receiverId);
@@ -112,6 +134,28 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
                 List<String> notDisturbList = lifeMessageNotDisturbMapper.selectList(notDisturbWrapper).stream().map(LifeMessageNotDisturb::getNotDisturbId).collect(Collectors.toList());
 
                 for (LifeMessageVo messageVo : lifeMessagePageList) {
+                    messageVo.setCanChat("1");
+                    String messagePhoneId = messageVo.getPhoneId();
+                    if (messagePhoneId != null && (currentIsUser || currentIsLawyer)) {
+                        if (currentIsUser && messagePhoneId.startsWith("lawyer_")) {
+                            Integer lawyerId = messageVo.getUserId();
+                            if (lawyerId == null) {
+                                LawyerUser lawyerUser = lawyerUserMapper.selectOne(new QueryWrapper<LawyerUser>()
+                                        .eq("phone", messagePhoneId.substring(7)));
+                                lawyerId = lawyerUser != null ? lawyerUser.getId() : null;
+                            }
+                            messageVo.setCanChat(isChatEnabled(currentUserId, lawyerId, chatEnabledCache));
+                        } else if (currentIsLawyer && messagePhoneId.startsWith("user_")) {
+                            Integer userId = messageVo.getUserId();
+                            if (userId == null) {
+                                LifeUser lifeUser = lifeUserMapper.selectOne(new QueryWrapper<LifeUser>()
+                                        .eq("user_phone", messagePhoneId.substring(5)));
+                                userId = lifeUser != null ? lifeUser.getId() : null;
+                            }
+                            messageVo.setCanChat(isChatEnabled(userId, currentLawyerId, chatEnabledCache));
+                        }
+                    }
+
                     // 免打扰
                     if (notDisturbList.contains(messageVo.getPhoneId())) {
                         messageVo.setIsNotDisturb("1");
@@ -144,7 +188,6 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
                         messageVo.setIsMerchant("0");
                     }
 
-                    String senderId = messageVo.getPhoneId();
                     lifeMessagePageResultList.add(messageVo);
                 }
             }
@@ -156,6 +199,37 @@ public class LifeMessageServiceImpl extends ServiceImpl<LifeMessageMapper, LifeM
         }
     }
 
+    private String isChatEnabled(Integer userId, Integer lawyerId, Map<String, String> cache) {
+        if (userId == null || lawyerId == null) {
+            return "1";
+        }
+        String cacheKey = userId + "_" + lawyerId;
+        if (cache.containsKey(cacheKey)) {
+            return cache.get(cacheKey);
+        }
+        LambdaQueryWrapper<LawyerConsultationOrder> orderWrapper = new LambdaQueryWrapper<>();
+        orderWrapper.eq(LawyerConsultationOrder::getClientUserId, userId);
+        orderWrapper.eq(LawyerConsultationOrder::getLawyerUserId, lawyerId);
+        orderWrapper.eq(LawyerConsultationOrder::getDeleteFlag, 0);
+        orderWrapper.orderByDesc(LawyerConsultationOrder::getCreatedTime);
+        orderWrapper.last("limit 1");
+        LawyerConsultationOrder order = lawyerConsultationOrderMapper.selectOne(orderWrapper);
+        boolean enabled = true;
+        if (order != null) {
+            Integer status = order.getOrderStatus();
+            enabled = !Objects.equals(status, LawyerStatusEnum.COMPLETE.getStatus())
+                    && !Objects.equals(status, LawyerStatusEnum.CANCEL.getStatus())
+                    && !Objects.equals(status, LawyerStatusEnum.REFUNDED.getStatus());
+        }
+        String chatStr = "1";
+        if(!enabled){
+            chatStr = "0";
+        }
+
+        cache.put(cacheKey, chatStr);
+        return chatStr;
+    }
+
     @Override
     public LifeMessageVo getStrangerMessageNum(String receiverId) throws Exception {
         try {

+ 20 - 4
alien-store/src/main/java/shop/alien/store/service/impl/SportsEquipmentFacilityServiceImpl.java

@@ -1421,10 +1421,26 @@ public class SportsEquipmentFacilityServiceImpl extends ServiceImpl<SportsEquipm
             }
             
             if (!CollectionUtils.isEmpty(equipmentIdList)) {
-                // 查询设备信息(用于后续扩展,当前vo中没有equipmentList字段)
-                // List<FitnessEquipmentInfo> equipmentList = new ArrayList<>(
-                //         fitnessEquipmentInfoService.listByIds(equipmentIdList));
-                // 注意:如果需要返回设备信息,可以在vo中添加equipmentList字段
+                // 查询设备信息,设置设备相关字段(取第一个设备的信息)
+                List<FitnessEquipmentInfo> equipmentList = new ArrayList<>(
+                        fitnessEquipmentInfoService.listByIds(equipmentIdList));
+                if (!CollectionUtils.isEmpty(equipmentList)) {
+                    // 取第一个设备的信息(如果有多个设备,取第一个)
+                    FitnessEquipmentInfo firstEquipment = equipmentList.get(0);
+                    if (firstEquipment != null) {
+                        // 设置设备ID
+                        vo.setEquipmentId(firstEquipment.getId());
+                        // 设置设备名称
+                        vo.setEquipmentName(firstEquipment.getEquipmentName());
+                        // 设置设备数量
+                        vo.setEquipmentNums(firstEquipment.getEquipmentNums());
+                        // 设置设备图片URL
+                        vo.setEquipmentImage(firstEquipment.getEquipmentImage());
+                        log.info("设置设备信息到VO,equipmentId={},equipmentName={},equipmentImage={}", 
+                                firstEquipment.getId(), firstEquipment.getEquipmentName(), 
+                                firstEquipment.getEquipmentImage());
+                    }
+                }
             }
         }
         

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

@@ -82,6 +82,8 @@ public class StoreCommentServiceImpl extends ServiceImpl<StoreCommentMapper, Sto
 
     @Autowired
     private TextModerationUtil textModerationUtil;
+    @Autowired
+    private CommonCommentMapper commonCommentMapper;
 
     /**
      * 评论列表

+ 15 - 1
alien-store/src/main/java/shop/alien/store/service/impl/StoreCuisineServiceImpl.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import lombok.RequiredArgsConstructor;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -22,7 +23,6 @@ import shop.alien.store.service.StoreCuisineService;
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -49,6 +49,13 @@ public class StoreCuisineServiceImpl extends ServiceImpl<StoreCuisineMapper, Sto
      */
     @Override
     public boolean addCuisineCombo(CuisineComboDto cuisineComboDto) {
+        // 验证:如果需要预约,预约规则必填
+        if (cuisineComboDto.getNeedReserve() != null && cuisineComboDto.getNeedReserve() == 1) {
+            if (StringUtils.isBlank(cuisineComboDto.getReserveRule())) {
+                throw new IllegalArgumentException("需要预约时,预约规则不能为空");
+            }
+        }
+        
         // 1. 保存主表信息到 store_cuisine
         StoreCuisine cuisine = new StoreCuisine();
         BeanUtils.copyProperties(cuisineComboDto, cuisine);
@@ -70,6 +77,13 @@ public class StoreCuisineServiceImpl extends ServiceImpl<StoreCuisineMapper, Sto
      */
     @Override
     public boolean updateCuisineCombo(CuisineComboDto cuisineComboDto) {
+        // 验证:如果需要预约,预约规则必填
+        if (cuisineComboDto.getNeedReserve() != null && cuisineComboDto.getNeedReserve() == 1) {
+            if (StringUtils.isBlank(cuisineComboDto.getReserveRule())) {
+                throw new IllegalArgumentException("需要预约时,预约规则不能为空");
+            }
+        }
+        
         Integer comboId = cuisineComboDto.getId();
 
         // 1. 更新主表信息

+ 626 - 36
alien-store/src/main/java/shop/alien/store/service/impl/StoreInfoServiceImpl.java

@@ -128,6 +128,8 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
     private final StoreCommentMapper storeCommentMapper;
 
+    private final CommonRatingMapper commonRatingMapper;
+
     private final StoreInfoDraftMapper storeInfoDraftMapper;
 
     private final LifeNoticeMapper lifeNoticeMapper;
@@ -171,6 +173,8 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
      */
     private final LicenseAuditAsyncService licenseAuditAsyncService;
 
+    private final CommonRatingService commonRatingService;
+
     @Resource
     private StoreIncomeDetailsRecordService storeIncomeDetailsRecordService;
 
@@ -335,6 +339,15 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         } else {
             storeMainInfoVo.setDistance2(0);
         }
+
+        // 统计评价数量(common_rating 表,业务类型1=商铺评价)
+        LambdaQueryWrapper<CommonRating> ratingWrapper = new LambdaQueryWrapper<>();
+        ratingWrapper.eq(CommonRating::getBusinessType, 1); // 业务类型:1-商铺评价
+        ratingWrapper.eq(CommonRating::getBusinessId, id); // 门店ID
+        ratingWrapper.eq(CommonRating::getDeleteFlag, 0); // 未删除
+        Integer ratingCountLong = commonRatingMapper.selectCount(ratingWrapper);
+        storeMainInfoVo.setRatingCount(ratingCountLong != null ? ratingCountLong.intValue() : 0);
+
         return storeMainInfoVo;
     }
 
@@ -1663,6 +1676,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     public StoreInfoVo getStoreDetail(String storeId, String userId, String jingdu, String weidu) {
         StoreInfoVo result = new StoreInfoVo();
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            return null;
+        }
         BeanUtils.copyProperties(storeInfo, result);
         //将经营板块和种类拆分成集合
         String businessTypes = storeInfo.getBusinessTypes();
@@ -5473,6 +5489,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
     public StoreInfoVo getClientStoreDetail(String storeId, String userId, String jingdu, String weidu) {
         StoreInfoVo result = new StoreInfoVo();
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            return null;
+        }
         BeanUtils.copyProperties(storeInfo, result);
         //将经营板块和种类拆分成集合
         String businessTypes = storeInfo.getBusinessTypes();
@@ -5618,9 +5637,27 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
 
         Map<String, Object> commitCountAndScore = storeCommentService.getCommitCountAndScore(null, 5, Integer.parseInt(storeId), null, null);
-        result.setScore(Double.parseDouble(commitCountAndScore.get("score").toString()));
-        result.setCommitCount(commitCountAndScore.get("commitCount").toString());
+//        result.setScore(Double.parseDouble(commitCountAndScore.get("score").toString()));
+//        result.setCommitCount(commitCountAndScore.get("commitCount").toString());
+        Integer totalCount = 0;
+        double storeScore;
+        Object ratingObj =  commonRatingService.getRatingCount(storeInfo.getId(), 1);
+        if (ratingObj != null) {
+            Map<String, Object> ratingMap = (Map<String, Object>) ratingObj;
+            Object totalCountObj = ratingMap.get("totalCount");
+            if (totalCountObj != null) {
+                // 安全转换为整数
+                try {
+                    totalCount = Integer.parseInt(totalCountObj.toString().trim());
+                } catch (NumberFormatException e) {
+                    totalCount = 0; // 转换失败时默认值
+                }
+            } else {
+                totalCount = 0;
+            }
+        }
 
+        result.setCommitCount(totalCount.toString());
 
         // 在该店铺的打卡次数
         result.setClockInStoreNum(clockStoreList.size());
@@ -5694,35 +5731,348 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 //        result.setTotalDynamicsNum(storeDynamicslist.size());
 
         //营业时间
-        List<StoreBusinessInfo> storeBusinessInfos = storeBusinessInfoMapper.selectList(new LambdaQueryWrapper<StoreBusinessInfo>().eq(StoreBusinessInfo::getStoreId, storeId).eq(StoreBusinessInfo::getDeleteFlag, 0));
+        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);
-            StoreBusinessInfo storeBusinessInfo = result.getStoreBusinessInfos().stream().filter(item -> item.getBusinessType() == 1).findFirst().orElse(null);
-            if (ObjectUtils.isNotEmpty(storeBusinessInfo)) {
+        }
+        if(storeInfo.getBusinessStatus() == 0){
+            // 优先判断当前时间是否是特殊日期
+            LocalDate currentDate = LocalDate.now();
+            LocalTime currentTime = LocalTime.now();
 
-                Calendar calendar = Calendar.getInstance(); // 获取Calendar实例
-                int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); // 获取星期几,注意Calendar中的DAY_OF_WEEK是从1(代表星期天)开始的
-                String[] days = {"7", "1", "2", "3", "4", "5", "6"};
-                String day = days[dayOfWeek - 1];
-                if (storeBusinessInfo.getBusinessDate().contains(day)) {
-                    if (StringUtils.isNotEmpty(storeBusinessInfo.getStartTime()) && StringUtils.isNotEmpty(storeBusinessInfo.getEndTime())) {
-                        LocalTime now = LocalTime.now();
-                        List<String> startList = Arrays.asList(storeBusinessInfo.getStartTime().split(":"));
-                        List<String> endList = Arrays.asList(storeBusinessInfo.getEndTime().split(":"));
-                        LocalTime start = LocalTime.of(Integer.parseInt(startList.get(0)), Integer.parseInt(startList.get(1)));
-                        LocalTime end = LocalTime.of(Integer.parseInt(endList.get(0)), Integer.parseInt(startList.get(1)));
-                        if (now.isAfter(start) && now.isBefore(end)) {
-                            result.setYyFlag(1);
+            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());
+            }
+        } else {
+            log.info("店铺营业状态不为0,不判断营业时间 - businessStatus: {}", 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);
+//            StoreBusinessInfo storeBusinessInfo = result.getStoreBusinessInfos().stream().filter(item -> item.getBusinessType() == 1).findFirst().orElse(null);
+//            if (ObjectUtils.isNotEmpty(storeBusinessInfo)) {
+//
+//                Calendar calendar = Calendar.getInstance(); // 获取Calendar实例
+//                int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); // 获取星期几,注意Calendar中的DAY_OF_WEEK是从1(代表星期天)开始的
+//                String[] days = {"7", "1", "2", "3", "4", "5", "6"};
+//                String day = days[dayOfWeek - 1];
+//                if (storeBusinessInfo.getBusinessDate().contains(day)) {
+//                    if (StringUtils.isNotEmpty(storeBusinessInfo.getStartTime()) && StringUtils.isNotEmpty(storeBusinessInfo.getEndTime())) {
+//                        LocalTime now = LocalTime.now();
+//                        List<String> startList = Arrays.asList(storeBusinessInfo.getStartTime().split(":"));
+//                        List<String> endList = Arrays.asList(storeBusinessInfo.getEndTime().split(":"));
+//                        LocalTime start = LocalTime.of(Integer.parseInt(startList.get(0)), Integer.parseInt(startList.get(1)));
+//                        LocalTime end = LocalTime.of(Integer.parseInt(endList.get(0)), Integer.parseInt(startList.get(1)));
+//                        if (now.isAfter(start) && now.isBefore(end)) {
+//                            result.setYyFlag(1);
+//                        } else {
+//                            result.setYyFlag(0);
+//                        }
+//                    }
+//                } else {
+//                    result.setYyFlag(0);
+//                }
+//            }
+//        }
         LambdaQueryWrapper<StoreDictionary> storeDictionaryLambdaQueryWrapper = new LambdaQueryWrapper<>();
         storeDictionaryLambdaQueryWrapper.eq(StoreDictionary::getTypeName, "businessStatus")
                 .eq(StringUtils.isNotEmpty(result.getBusinessStatus().toString()), StoreDictionary::getDictId, result.getBusinessStatus());
@@ -5774,6 +6124,9 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
 
         StoreInfoVo result = new StoreInfoVo();
         StoreInfo storeInfo = storeInfoMapper.selectById(storeId);
+        if (storeInfo == null) {
+            return null;
+        }
         BeanUtils.copyProperties(storeInfo, result);
         //将经营板块和种类拆分成集合
         String businessTypes = storeInfo.getBusinessTypes();
@@ -6057,13 +6410,7 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
                 .set(StoreImg::getDeleteFlag, 1);
         storeImgMapper.update(null, deleteWrapper);
         
-        // 插入新的营业执照图片
-        storeImg.setImgType(14);
-        storeImg.setImgDescription("营业执照");
-        storeImg.setImgSort(0);
-        storeImg.setDeleteFlag(0);
-        storeImgMapper.insert(storeImg);
-        
+        // 不再立即插入新的营业执照图片,等审核通过后再插入
         // 插入营业执照历史记录(licenseStatus=1表示营业执照,licenseExecuteStatus=2表示审核中)
         StoreLicenseHistory licenseHistory = new StoreLicenseHistory();
         licenseHistory.setStoreId(storeImg.getStoreId());
@@ -6159,23 +6506,16 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         
         // 收集所有图片URL
         List<String> imgUrlList = new ArrayList<>();
-        
-        // 插入新的其他资质证明图片(最多9张)
+
+        // 不再立即插入新的其他资质证明图片,等审核通过后再插入
+        // 收集图片URL
         for (int i = 0; i < maxCount; i++) {
             StoreImg storeImg = storeImgList.get(i);
-            storeImg.setStoreId(storeId);
-            storeImg.setImgType(35); // 其他资质证明图片
-            storeImg.setImgDescription("其他资质证明");
-            storeImg.setImgSort(i + 1);
-            storeImg.setDeleteFlag(0);
-            storeImgMapper.insert(storeImg);
-            
-            // 收集图片URL
             if (StringUtils.isNotEmpty(storeImg.getImgUrl())) {
                 imgUrlList.add(storeImg.getImgUrl());
             }
         }
-        
+
         // 将所有图片URL合并存入一条store_license_history记录(用逗号分隔)
         if (!imgUrlList.isEmpty()) {
             String allImgUrls = String.join(",", imgUrlList);
@@ -6786,6 +7126,256 @@ public class StoreInfoServiceImpl extends ServiceImpl<StoreInfoMapper, StoreInfo
         return resultPage;
     }
 
+    /**
+     * 根据节假日名称获取当前年度的日期范围
+     *
+     * @param year 年份
+     * @param holidayNames 节假日名称,如"元旦、春节、儿童节"
+     * @return 节假日日期范围列表,每个元素是一个数组[开始日期, 结束日期]
+     */
+    private List<LocalDate[]> getHolidayDateRanges(int year, String holidayNames) {
+        List<LocalDate[]> ranges = new ArrayList<>();
+
+        if (StringUtils.isEmpty(holidayNames)) {
+            return ranges;
+        }
+
+        // 分割节假日名称(支持多种分隔符:、, ,)
+        String[] holidays = holidayNames.split("[、,,]");
+
+        for (String holiday : holidays) {
+            String holidayName = holiday.trim();
+            if (StringUtils.isEmpty(holidayName)) {
+                continue;
+            }
+
+            LocalDate[] range = getHolidayDateRange(year, holidayName);
+            if (range != null && range.length == 2) {
+                ranges.add(range);
+            }
+        }
+
+        return ranges;
+    }
+
+    /**
+     * 根据单个节假日名称获取当前年度的日期范围
+     *
+     * @param year 年份
+     * @param holidayName 节假日名称,如"元旦"、"春节"、"国庆"、"儿童节"等
+     * @return 节假日日期范围 [开始日期, 结束日期]
+     */
+    private LocalDate[] getHolidayDateRange(int year, String holidayName) {
+        String name = holidayName.trim();
+
+        try {
+            // 元旦:1月1日,通常3天假期(1月1日-1月3日)
+            if (name.contains("元旦")) {
+                return new LocalDate[]{
+                    LocalDate.of(year, 1, 1),
+                    LocalDate.of(year, 1, 3)
+                };
+            }
+
+            // 春节:农历正月初一,通常7天假期
+            if (name.contains("春节")) {
+                // 春节日期每年不同,这里提供近几年的数据,建议使用农历工具精确计算
+                // 2024年春节:2月10日(农历正月初一)-2月16日
+                // 2025年春节:1月29日(农历正月初一)-2月4日
+                // 2026年春节:2月17日(农历正月初一)-2月23日
+                if (year == 2024) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 2, 10),
+                        LocalDate.of(year, 2, 16)
+                    };
+                } else if (year == 2025) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 1, 29),
+                        LocalDate.of(year, 2, 4)
+                    };
+                } else if (year == 2026) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 2, 15),
+                        LocalDate.of(year, 2, 23)
+                    };
+                } else {
+                    // 默认:根据年份大致估算(建议使用农历工具精确计算)
+                    return new LocalDate[]{
+                        LocalDate.of(year, 2, 10),
+                        LocalDate.of(year, 2, 16)
+                    };
+                }
+            }
+
+            // 情人节:2月14日,1天
+            if (name.contains("情人节")) {
+                LocalDate date = LocalDate.of(year, 2, 14);
+                return new LocalDate[]{date, date};
+            }
+
+            // 元宵节:农历正月十五,1天(通常在2月或3月)
+            if (name.contains("元宵")) {
+                // 元宵节是春节后的第15天,这里简化处理
+                // 2024年元宵节:2月24日,2025年元宵节:2月12日,2026年元宵节:3月4日
+                if (year == 2024) {
+                    LocalDate date = LocalDate.of(year, 2, 24);
+                    return new LocalDate[]{date, date};
+                } else if (year == 2025) {
+                    LocalDate date = LocalDate.of(year, 2, 12);
+                    return new LocalDate[]{date, date};
+                } else if (year == 2026) {
+                    LocalDate date = LocalDate.of(year, 3, 4);
+                    return new LocalDate[]{date, date};
+                } else {
+                    // 默认:大致在2月底3月初
+                    LocalDate date = LocalDate.of(year, 2, 24);
+                    return new LocalDate[]{date, date};
+                }
+            }
+
+            // 清明节:通常4月4日或5日,通常3天假期
+            if (name.contains("清明")) {
+                // 清明节通常在4月4日或5日,这里以4月4日为基准,包含前后各1天
+                return new LocalDate[]{
+                    LocalDate.of(year, 4, 4),
+                    LocalDate.of(year, 4, 6)
+                };
+            }
+
+            // 劳动节:5月1日,通常5天假期(5月1日-5月5日)
+            if (name.contains("劳动") || name.contains("五一")) {
+                return new LocalDate[]{
+                    LocalDate.of(year, 5, 1),
+                    LocalDate.of(year, 5, 5)
+                };
+            }
+
+            // 儿童节:6月1日,1天
+            if (name.contains("儿童")) {
+                LocalDate date = LocalDate.of(year, 6, 1);
+                return new LocalDate[]{date, date};
+            }
+
+            // 端午节:农历五月初五,通常3天假期
+            if (name.contains("端午")) {
+                // 端午节通常在6月,这里提供近几年的数据
+                // 2024年端午节:6月10日,2025年端午节:5月31日,2026年端午节:6月19日
+                if (year == 2024) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 6, 10),
+                        LocalDate.of(year, 6, 12)
+                    };
+                } else if (year == 2025) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 5, 31),
+                        LocalDate.of(year, 6, 2)
+                    };
+                } else if (year == 2026) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 6, 19),
+                        LocalDate.of(year, 6, 21)
+                    };
+                } else {
+                    // 默认:大致在6月中旬
+                    return new LocalDate[]{
+                        LocalDate.of(year, 6, 10),
+                        LocalDate.of(year, 6, 12)
+                    };
+                }
+            }
+
+            // 七夕:农历七月初七,1天(通常在8月)
+            if (name.contains("七夕")) {
+                // 七夕通常在8月,这里提供近几年的数据
+                // 2024年七夕:8月10日,2025年七夕:7月31日,2026年七夕:8月20日
+                if (year == 2024) {
+                    LocalDate date = LocalDate.of(year, 8, 10);
+                    return new LocalDate[]{date, date};
+                } else if (year == 2025) {
+                    LocalDate date = LocalDate.of(year, 7, 31);
+                    return new LocalDate[]{date, date};
+                } else if (year == 2026) {
+                    LocalDate date = LocalDate.of(year, 8, 20);
+                    return new LocalDate[]{date, date};
+                } else {
+                    // 默认:大致在8月上旬
+                    LocalDate date = LocalDate.of(year, 8, 10);
+                    return new LocalDate[]{date, date};
+                }
+            }
+
+            // 中秋节:农历八月十五,通常3天假期
+            if (name.contains("中秋")) {
+                // 中秋节通常在9月或10月,这里提供近几年的数据
+                // 2024年中秋节:9月17日,2025年中秋节:10月6日,2026年中秋节:9月25日
+                if (year == 2024) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 9, 17),
+                        LocalDate.of(year, 9, 19)
+                    };
+                } else if (year == 2025) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 10, 6),
+                        LocalDate.of(year, 10, 8)
+                    };
+                } else if (year == 2026) {
+                    return new LocalDate[]{
+                        LocalDate.of(year, 9, 25),
+                        LocalDate.of(year, 9, 27)
+                    };
+                } else {
+                    // 默认:大致在9月下旬
+                    return new LocalDate[]{
+                        LocalDate.of(year, 9, 29),
+                        LocalDate.of(year, 10, 1)
+                    };
+                }
+            }
+
+            // 国庆节:10月1日,通常7天假期(10月1日-10月7日)
+            if (name.contains("国庆")) {
+                return new LocalDate[]{
+                    LocalDate.of(year, 10, 1),
+                    LocalDate.of(year, 10, 7)
+                };
+            }
+
+            // 冬至:通常12月21日或22日,1天
+            if (name.contains("冬至")) {
+                // 冬至通常在12月21日或22日,这里统一为12月22日
+                LocalDate date = LocalDate.of(year, 12, 22);
+                return new LocalDate[]{date, date};
+            }
+
+            // 平安夜:12月24日,1天
+            if (name.contains("平安夜")) {
+                LocalDate date = LocalDate.of(year, 12, 24);
+                return new LocalDate[]{date, date};
+            }
+
+            // 圣诞节:12月25日,1天
+            if (name.contains("圣诞")) {
+                LocalDate date = LocalDate.of(year, 12, 25);
+                return new LocalDate[]{date, date};
+            }
+
+        } catch (Exception e) {
+            log.warn("计算节假日日期范围失败 - 年份: {}, 节假日: {}", year, holidayName, e);
+        }
+
+        return null;
+    }
+
+    public boolean isBusinessTime(LocalTime currentTime, LocalTime openTime, LocalTime closeTime) {
+        if (openTime.isBefore(closeTime)) {
+            // 正常时间段(不跨天):09:00-18:00
+            return !currentTime.isBefore(openTime) && currentTime.isBefore(closeTime);
+        } else {
+            // 跨天时间段:14:00-03:00
+            return !currentTime.isBefore(openTime) || currentTime.isBefore(closeTime);
+        }
+    }
+
 }
 
 

+ 51 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreQualificationServiceImpl.java

@@ -0,0 +1,51 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.StoreImg;
+import shop.alien.entity.store.vo.StoreQualificationInfoVo;
+import shop.alien.mapper.StoreQualificationMapper;
+import shop.alien.store.service.StoreQualificationService;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 门店资质信息服务实现类
+ *
+ * @author system
+ * @since 2025-01-01
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StoreQualificationServiceImpl implements StoreQualificationService {
+    
+    private final StoreQualificationMapper storeQualificationMapper;
+    
+    @Override
+    public StoreQualificationInfoVo getStoreQualificationInfo(Integer storeId) {
+        log.info("StoreQualificationServiceImpl.getStoreQualificationInfo?storeId={}", storeId);
+        if (storeId == null) {
+            log.warn("门店ID为空");
+            return new StoreQualificationInfoVo();
+        }
+        
+        StoreQualificationInfoVo result = new StoreQualificationInfoVo();
+        
+        // 查询营业执照(img_type = 14)
+        List<StoreImg> businessLicenseList = storeQualificationMapper.selectStoreImgsByType(storeId, 14);
+        result.setBusinessLicenseList(businessLicenseList != null ? businessLicenseList : new ArrayList<>());
+        
+        // 查询其他资质证明图片(img_type = 35)
+        List<StoreImg> otherQualificationList = storeQualificationMapper.selectStoreImgsByType(storeId, 35);
+        result.setOtherQualificationList(otherQualificationList != null ? otherQualificationList : new ArrayList<>());
+        
+        log.info("查询门店资质信息完成 - 营业执照: {}条, 其他资质证明图片: {}条", 
+                result.getBusinessLicenseList().size(), 
+                result.getOtherQualificationList().size());
+        
+        return result;
+    }
+}

+ 307 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationBrowseRecordServiceImpl.java

@@ -0,0 +1,307 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.StoreRenovationBrowseRecord;
+import shop.alien.entity.store.StoreRenovationRequirement;
+import shop.alien.entity.store.StoreUser;
+import shop.alien.entity.store.dto.StoreRenovationBrowseRequirementDto;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.mapper.StoreRenovationBrowseRecordMapper;
+import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.service.StoreRenovationBrowseRecordService;
+import shop.alien.store.service.StoreRenovationRequirementService;
+import shop.alien.util.common.JwtUtil;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 装修商铺访问装修需求浏览记录表 服务实现类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class StoreRenovationBrowseRecordServiceImpl extends ServiceImpl<StoreRenovationBrowseRecordMapper, StoreRenovationBrowseRecord> implements StoreRenovationBrowseRecordService {
+
+    private final StoreRenovationRequirementService requirementService;
+    private final StoreUserMapper storeUserMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @Override
+    public boolean recordBrowse(Integer renovationStoreId, Integer requirementId) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.recordBrowse?renovationStoreId={}, requirementId={}", renovationStoreId, requirementId);
+        try {
+            StoreRenovationBrowseRecord record = new StoreRenovationBrowseRecord();
+            record.setRenovationStoreId(renovationStoreId);
+            record.setRequirementId(requirementId);
+            record.setBrowseTime(new Date());
+            record.setIsContacted(0); // 仅浏览,未咨询
+
+            // 获取当前登录用户ID
+            Integer currentUserId = null;
+            try {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                if (userInfo != null) {
+                    currentUserId = userInfo.getInteger("userId");
+                }
+            } catch (Exception e) {
+                log.warn("获取当前用户ID失败: {}", e.getMessage());
+            }
+            record.setCreatedUserId(currentUserId);
+
+            return this.save(record);
+        } catch (Exception e) {
+            log.error("记录浏览失败: {}", e.getMessage(), e);
+            throw new RuntimeException("记录浏览失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean recordInquiry(Integer renovationStoreId, Integer requirementId) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.recordInquiry?renovationStoreId={}, requirementId={}", renovationStoreId, requirementId);
+        try {
+            Date now = new Date();
+
+            // 检查是否已有浏览记录
+            LambdaQueryWrapper<StoreRenovationBrowseRecord> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(StoreRenovationBrowseRecord::getRenovationStoreId, renovationStoreId);
+            queryWrapper.eq(StoreRenovationBrowseRecord::getRequirementId, requirementId);
+            queryWrapper.eq(StoreRenovationBrowseRecord::getDeleteFlag, 0);
+            StoreRenovationBrowseRecord existingRecord = this.getOne(queryWrapper, false);
+
+            boolean isFirstContact = false;
+
+            if (existingRecord != null) {
+                // 如果已有记录但未咨询过,更新为已咨询
+                if (existingRecord.getIsContacted() == null || existingRecord.getIsContacted() == 0) {
+                    LambdaUpdateWrapper<StoreRenovationBrowseRecord> updateWrapper = new LambdaUpdateWrapper<>();
+                    updateWrapper.eq(StoreRenovationBrowseRecord::getId, existingRecord.getId());
+                    updateWrapper.set(StoreRenovationBrowseRecord::getIsContacted, 1);
+                    updateWrapper.set(StoreRenovationBrowseRecord::getContactTime, now);
+
+                    // 获取当前登录用户ID
+                    Integer currentUserId = null;
+                    try {
+                        com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                        if (userInfo != null) {
+                            currentUserId = userInfo.getInteger("userId");
+                        }
+                    } catch (Exception e) {
+                        log.warn("获取当前用户ID失败: {}", e.getMessage());
+                    }
+                    updateWrapper.set(StoreRenovationBrowseRecord::getUpdatedUserId, currentUserId);
+
+                    this.update(updateWrapper);
+                    isFirstContact = true;
+                }
+                // 如果已咨询过,不再重复记录,但可以记录新的浏览记录(可选)
+                // 这里选择不重复记录咨询,只更新浏览时间
+                existingRecord.setBrowseTime(now);
+                this.updateById(existingRecord);
+            } else {
+                // 如果没有记录,创建新记录(浏览+咨询)
+                StoreRenovationBrowseRecord record = new StoreRenovationBrowseRecord();
+                record.setRenovationStoreId(renovationStoreId);
+                record.setRequirementId(requirementId);
+                record.setBrowseTime(now);
+                record.setIsContacted(1); // 已咨询
+                record.setContactTime(now);
+
+                // 获取当前登录用户ID
+                Integer currentUserId = null;
+                try {
+                    com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                    if (userInfo != null) {
+                        currentUserId = userInfo.getInteger("userId");
+                    }
+                } catch (Exception e) {
+                    log.warn("获取当前用户ID失败: {}", e.getMessage());
+                }
+                record.setCreatedUserId(currentUserId);
+
+                this.save(record);
+                isFirstContact = true;
+            }
+
+            // 如果是首次咨询,增加需求的咨询数
+            if (isFirstContact) {
+                requirementService.incrementInquiryCount(requirementId);
+            }
+
+            return true;
+        } catch (Exception e) {
+            log.error("记录咨询失败: {}", e.getMessage(), e);
+            throw new RuntimeException("记录咨询失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public boolean recordInquiryByCurrentUser(Integer requirementId) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.recordInquiryByCurrentUser?requirementId={}", requirementId);
+        try {
+            // 获取当前登录用户信息
+            Integer currentUserId = null;
+            String phone = null;
+            try {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                if (userInfo != null) {
+                    currentUserId = userInfo.getInteger("userId");
+                    phone = userInfo.getString("phone");
+                }
+            } catch (Exception e) {
+                log.warn("获取当前用户信息失败: {}", e.getMessage());
+                throw new RuntimeException("获取当前用户信息失败,请先登录");
+            }
+
+            if (currentUserId == null) {
+                throw new RuntimeException("用户未登录");
+            }
+
+            // 根据用户ID或手机号查询 StoreUser,获取门店ID
+            StoreUser storeUser = null;
+            if (currentUserId != null) {
+                storeUser = storeUserMapper.selectById(currentUserId);
+            }
+            if (storeUser == null && phone != null) {
+                LambdaQueryWrapper<StoreUser> queryWrapper = new LambdaQueryWrapper<>();
+                queryWrapper.eq(StoreUser::getPhone, phone);
+                storeUser = storeUserMapper.selectOne(queryWrapper);
+            }
+
+            if (storeUser == null || storeUser.getStoreId() == null) {
+                throw new RuntimeException("未找到用户门店信息");
+            }
+
+            Integer renovationStoreId = storeUser.getStoreId();
+            return recordInquiry(renovationStoreId, requirementId);
+        } catch (Exception e) {
+            log.error("记录咨询失败: {}", e.getMessage(), e);
+            throw new RuntimeException(e.getMessage());
+        }
+    }
+
+    @Override
+    public List<StoreRenovationBrowseRequirementDto> getBrowsedRequirementsByStoreTel(String renovationStoreTel, String normalStoreTel) {
+        log.info("StoreRenovationBrowseRecordServiceImpl.getBrowsedRequirementsByStoreTel?renovationStoreTel={}, normalStoreTel={}", renovationStoreTel, normalStoreTel);
+        try {
+            // 0. 通过手机号查询门店ID
+            LambdaQueryWrapper<StoreUser> renovationStoreUserWrapper = new LambdaQueryWrapper<>();
+            renovationStoreUserWrapper.eq(StoreUser::getPhone, renovationStoreTel);
+            StoreUser renovationStoreUser = storeUserMapper.selectOne(renovationStoreUserWrapper);
+            if (renovationStoreUser == null || renovationStoreUser.getStoreId() == null) {
+                throw new RuntimeException("装修商铺不存在,手机号: " + renovationStoreTel);
+            }
+            Integer renovationStoreId = renovationStoreUser.getStoreId();
+
+            // 1. 通过手机号查询门店ID
+            LambdaQueryWrapper<StoreUser> normalStoreUserWrapper = new LambdaQueryWrapper<>();
+            normalStoreUserWrapper.eq(StoreUser::getPhone, normalStoreTel);
+            StoreUser normalStoreUser = storeUserMapper.selectOne(normalStoreUserWrapper);
+            if (normalStoreUser == null || normalStoreUser.getStoreId() == null) {
+                throw new RuntimeException("普通商铺不存在,门店电话: " + normalStoreTel);
+            }
+            Integer normalStoreId = normalStoreUser.getStoreId();
+
+            // 1. 查询普通商铺发布的所有装修需求ID
+            LambdaQueryWrapper<StoreRenovationRequirement> requirementWrapper = new LambdaQueryWrapper<>();
+            requirementWrapper.eq(StoreRenovationRequirement::getStoreId, normalStoreId);
+            requirementWrapper.eq(StoreRenovationRequirement::getDeleteFlag, 0);
+            List<StoreRenovationRequirement> requirements = requirementService.list(requirementWrapper);
+            
+            if (requirements.isEmpty()) {
+                return new ArrayList<>();
+            }
+            
+            List<Integer> requirementIds = requirements.stream()
+                    .map(StoreRenovationRequirement::getId)
+                    .collect(Collectors.toList());
+
+            // 2. 查询装修商铺浏览过哪些需求
+            LambdaQueryWrapper<StoreRenovationBrowseRecord> browseWrapper = new LambdaQueryWrapper<>();
+            browseWrapper.eq(StoreRenovationBrowseRecord::getRenovationStoreId, renovationStoreId);
+            browseWrapper.in(StoreRenovationBrowseRecord::getRequirementId, requirementIds);
+            browseWrapper.eq(StoreRenovationBrowseRecord::getDeleteFlag, 0);
+            browseWrapper.orderByDesc(StoreRenovationBrowseRecord::getBrowseTime);
+            List<StoreRenovationBrowseRecord> browseRecords = this.list(browseWrapper);
+
+            if (browseRecords.isEmpty()) {
+                return new ArrayList<>();
+            }
+
+            // 3. 创建需求ID到浏览记录的映射(取最新的浏览记录)
+            Map<Integer, StoreRenovationBrowseRecord> browseRecordMap = browseRecords.stream()
+                    .collect(Collectors.toMap(
+                            StoreRenovationBrowseRecord::getRequirementId,
+                            record -> record,
+                            (existing, replacement) -> existing.getBrowseTime().after(replacement.getBrowseTime()) ? existing : replacement
+                    ));
+
+            // 4. 创建需求ID到需求详情的映射
+            Map<Integer, StoreRenovationRequirement> requirementMap = requirements.stream()
+                    .collect(Collectors.toMap(StoreRenovationRequirement::getId, req -> req));
+
+            // 5. 组装返回数据(只返回浏览过的需求)
+            List<StoreRenovationBrowseRequirementDto> result = new ArrayList<>();
+            for (StoreRenovationBrowseRecord browseRecord : browseRecordMap.values()) {
+                StoreRenovationRequirement requirement = requirementMap.get(browseRecord.getRequirementId());
+                if (requirement != null) {
+                    StoreRenovationBrowseRequirementDto dto = new StoreRenovationBrowseRequirementDto();
+                    
+                    // 复制需求信息
+                    dto.setRequirementId(requirement.getId());
+                    dto.setRequirementTitle(requirement.getRequirementTitle());
+                    dto.setRenovationType(requirement.getRenovationType());
+                    dto.setHouseArea(requirement.getHouseArea());
+                    dto.setRenovationBudget(requirement.getRenovationBudget());
+                    dto.setDetailedRequirement(requirement.getDetailedRequirement());
+                    dto.setExpectedRenovationTime(requirement.getExpectedRenovationTime());
+                    dto.setContactName(requirement.getContactName());
+                    dto.setContactPhone(requirement.getContactPhone());
+                    dto.setCity(requirement.getCity());
+                    dto.setDetailedAddress(requirement.getDetailedAddress());
+                    dto.setCreatedTime(requirement.getCreatedTime());
+
+                    // 处理附件URL字符串:逗号拼接转List
+                    if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+                        List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+                        dto.setAttachmentUrls(attachmentUrls);
+                    } else {
+                        dto.setAttachmentUrls(new ArrayList<>());
+                    }
+
+                    // 复制浏览信息
+                    dto.setBrowseTime(browseRecord.getBrowseTime());
+                    dto.setIsContacted(browseRecord.getIsContacted());
+                    dto.setContactTime(browseRecord.getContactTime());
+
+                    result.add(dto);
+                }
+            }
+
+            // 按浏览时间倒序排序
+            result.sort((a, b) -> {
+                if (a.getBrowseTime() == null && b.getBrowseTime() == null) return 0;
+                if (a.getBrowseTime() == null) return 1;
+                if (b.getBrowseTime() == null) return -1;
+                return b.getBrowseTime().compareTo(a.getBrowseTime());
+            });
+
+            return result;
+        } catch (Exception e) {
+            log.error("查询浏览过的装修需求失败: {}", e.getMessage(), e);
+            throw new RuntimeException("查询浏览过的装修需求失败: " + e.getMessage());
+        }
+    }
+}
+

+ 666 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreRenovationRequirementServiceImpl.java

@@ -0,0 +1,666 @@
+package shop.alien.store.service.impl;
+
+import com.alibaba.fastjson2.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.store.*;
+import shop.alien.entity.store.dto.StoreRenovationRequirementDto;
+import shop.alien.entity.store.vo.WebSocketVo;
+import shop.alien.mapper.*;
+import shop.alien.store.config.WebSocketProcess;
+import shop.alien.store.service.StoreRenovationRequirementService;
+import shop.alien.store.util.ai.AiContentModerationUtil;
+import shop.alien.store.util.ai.AiVideoModerationUtil;
+import shop.alien.util.common.JwtUtil;
+
+import javax.annotation.PostConstruct;
+import javax.annotation.PreDestroy;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.stream.Collectors;
+
+/**
+ * 装修需求动态表 服务实现类
+ *
+ * @author auto-generated
+ * @since 2025-01-15
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional(rollbackFor = Exception.class)
+public class StoreRenovationRequirementServiceImpl extends ServiceImpl<StoreRenovationRequirementMapper, StoreRenovationRequirement> implements StoreRenovationRequirementService {
+
+    // 1. 自定义视频审核线程池(全局复用,避免频繁创建线程)
+    private ExecutorService videoAuditExecutor;
+
+    private final StoreInfoMapper storeInfoMapper;
+
+    private final LifeMessageMapper lifeMessageMapper;
+
+    private final LifeNoticeMapper lifeNoticeMapper;
+
+    private final WebSocketProcess webSocketProcess;
+
+    private final StoreUserMapper storeUserMapper;
+
+    private final AiContentModerationUtil aiContentModerationUtil;
+    private final AiVideoModerationUtil aiVideoModerationUtil;
+    // 定义图片后缀常量(不可修改,保证线程安全)
+    private static final Set<String> IMAGE_SUFFIXES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"))
+    );
+
+    // 定义视频后缀常量
+    private static final Set<String> VIDEO_SUFFIXES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(".mp4", ".avi", ".mov", ".flv", ".mkv", ".wmv", ".mpeg"))
+    );
+
+    // 初始化线程池
+    @PostConstruct
+    public void initExecutor() {
+        // 核心参数根据业务调整,IO密集型任务线程数可设为CPU核心数*2~4
+        int corePoolSize = Runtime.getRuntime().availableProcessors() * 2;
+        int maxPoolSize = Runtime.getRuntime().availableProcessors() * 4;
+        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) {
+                        log.error("视频审核任务被拒绝,队列已满!当前队列大小:{},活跃线程数:{}",
+                                e.getQueue().size(), e.getActiveCount());
+                        super.rejectedExecution(r, e);
+                    }
+                }
+        );
+    }
+
+    // 优雅关闭线程池
+    @PreDestroy
+    public void destroyExecutor() {
+        if (Objects.nonNull(videoAuditExecutor)) {
+            videoAuditExecutor.shutdown();
+            try {
+                if (!videoAuditExecutor.awaitTermination(30, TimeUnit.SECONDS)) {
+                    videoAuditExecutor.shutdownNow();
+                }
+            } catch (InterruptedException e) {
+                videoAuditExecutor.shutdownNow();
+                Thread.currentThread().interrupt();
+            }
+        }
+    }
+
+    @Override
+    public boolean saveOrUpdateRequirement(StoreRenovationRequirementDto dto) {
+        log.info("StoreRenovationRequirementServiceImpl.saveOrUpdateRequirement?dto={}", dto);
+        try {
+            StoreRenovationRequirement requirement = new StoreRenovationRequirement();
+            BeanUtils.copyProperties(dto, requirement);
+
+            // 处理附件URL列表:List转逗号拼接字符串
+            if (dto.getAttachmentUrls() != null && !dto.getAttachmentUrls().isEmpty()) {
+                String attachmentUrlsStr = String.join(",", dto.getAttachmentUrls());
+                requirement.setAttachmentUrls(attachmentUrlsStr);
+            }
+
+            // 获取当前登录用户ID
+            Integer currentUserId = null;
+            try {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                if (userInfo != null) {
+                    currentUserId = userInfo.getInteger("userId");
+                }
+            } catch (Exception e) {
+                log.warn("获取当前用户ID失败: {}", e.getMessage());
+            }
+
+            if (requirement.getId() == null) {
+                // 新增
+                requirement.setCreatedUserId(currentUserId);
+                requirement.setCreatedTime(new Date());
+                if (requirement.getStatus() == null) {
+                    requirement.setStatus(0); // 默认草稿
+                }
+                if (requirement.getViewCount() == null) {
+                    requirement.setViewCount(0);
+                }
+                if (requirement.getInquiryCount() == null) {
+                    requirement.setInquiryCount(0);
+                }
+                if (requirement.getAuditStatus() == null) {
+                    requirement.setAuditStatus(0); // 默认待审核
+                }
+            } else {
+                // 更新
+                requirement.setUpdatedUserId(currentUserId);
+                requirement.setUpdatedTime(new Date());
+            }
+            StoreUser storeUser = storeUserMapper.selectOne(new LambdaQueryWrapper<StoreUser>().eq(StoreUser::getStoreId, dto.getStoreId()).eq(StoreUser::getDeleteFlag, 0));
+
+            LifeNotice lifeMessage = new LifeNotice();
+            lifeMessage.setReceiverId("store_" + storeUser.getPhone());
+            String text = "您发布的装修动态已提交审核,请于1-3个工作日进行查看";
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("message", text);
+            lifeMessage.setContext(jsonObject.toJSONString());
+            lifeMessage.setTitle("审核通知");
+            lifeMessage.setSenderId("system");
+            lifeMessage.setIsRead(0);
+            lifeMessage.setNoticeType(1);
+            lifeNoticeMapper.insert(lifeMessage);
+
+            WebSocketVo websocketVo = new WebSocketVo();
+            websocketVo.setSenderId("system");
+            websocketVo.setReceiverId("store_" + storeUser.getPhone());
+            websocketVo.setCategory("notice");
+            websocketVo.setNoticeType("1");
+            websocketVo.setIsRead(0);
+            websocketVo.setText(JSONObject.from(lifeMessage).toJSONString());
+            webSocketProcess.sendMessage("store_" + storeUser.getPhone(), JSONObject.from(websocketVo).toJSONString());
+
+
+            // TODO 审核文本和图片内容是否违规
+            // 一次遍历完成分类,避免多次流式处理
+            Map<String, List<String>> urlCategoryMap = classifyUrls(dto.getAttachmentUrls());
+            List<String> videoUrls = urlCategoryMap.get("video");
+            // 1.调用文本+图片审核接口 ai为同步接口
+            AiContentModerationUtil.AuditResult auditResult = aiContentModerationUtil.auditContent(requirement.getDetailedRequirement(), urlCategoryMap.get("image"));
+            if (!auditResult.isPassed()) {
+                requirement.setAuditStatus(2);
+                requirement.setAuditReason(auditResult.getFailureReason());
+                // 发送审核失败通知(文本图片)
+                if (storeUser != null && storeUser.getPhone() != null) {
+                    sendAuditNotification(storeUser.getPhone(), 2, auditResult.getFailureReason(), "text_image");
+                }
+
+            } else {
+                // 如果文本/图片审核通过,且没有视频需要审核,直接设置为审核通过
+                if (videoUrls == null || videoUrls.isEmpty()) {
+                    requirement.setAuditStatus(1);
+                    requirement.setAuditReason(null);
+                    // 发送审核通过通知(文本图片,无视频)
+                    if (storeUser != null && storeUser.getPhone() != null) {
+                        sendAuditNotification(storeUser.getPhone(), 1, null, "text_image");
+                    }
+                } else {
+                    // 异步调用视频审核接口,图片审核通过后调用(核心优化)
+                    // 保存storeUser信息供异步任务使用
+                    final StoreUser finalStoreUser = storeUser;
+                    CompletableFuture.runAsync(() -> {
+                    AiVideoModerationUtil.VideoAuditResult videoAuditResult = null;
+                    try {
+                        // 调用审核接口,增加超时控制(避免接口挂死)
+                        videoAuditResult = CompletableFuture.supplyAsync(
+                                () -> aiVideoModerationUtil.auditVideos(urlCategoryMap.get("video")),
+                                videoAuditExecutor
+                        ).get();
+
+                        // 审核不通过则更新状态和原因
+                        if (Objects.nonNull(videoAuditResult) && !videoAuditResult.isPassed()) {
+                            // 重新查询最新的requirement,避免并发覆盖
+                            StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
+                            if (Objects.isNull(latestRequirement)) {
+                                log.error("视频审核后更新失败,requirement不存在,ID:{}", requirement.getId());
+                                return;
+                            }
+                            latestRequirement.setAuditStatus(2);
+                            latestRequirement.setAuditReason(videoAuditResult.getFailureReason());
+                            this.saveOrUpdate(latestRequirement);
+                            log.info("视频审核不通过,已更新状态,requirementID:{},原因:{}",
+                                    requirement.getId(), videoAuditResult.getFailureReason());
+                            // 发送审核不通过通知(视频)
+                            if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
+                                sendAuditNotification(finalStoreUser.getPhone(), 2, videoAuditResult.getFailureReason(), "video");
+                            }
+
+                        } else if (Objects.nonNull(videoAuditResult) && videoAuditResult.isPassed()) {
+                            // 审核通过也更新状态(可选,根据业务需求)
+                            StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
+                            if (Objects.nonNull(latestRequirement)) {
+                                latestRequirement.setAuditStatus(1);
+                                latestRequirement.setAuditReason(null);
+                                this.saveOrUpdate(latestRequirement);
+                                // 发送审核通过通知(视频)
+                                if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
+                                    sendAuditNotification(finalStoreUser.getPhone(), 1, null, "video");
+                                }
+
+                                log.info("视频审核通过,已更新状态,requirementID:{}", requirement.getId());
+                            }
+                        }
+                    } catch (Exception e) {
+                        log.error("视频审核接口调用异常,requirementID:{}", requirement.getId(), e);
+                        StoreRenovationRequirement latestRequirement = this.getById(requirement.getId());
+                        if (Objects.nonNull(latestRequirement)) {
+                            String exceptionMessage = "视频审核接口调用异常:" + e.getMessage();
+                            latestRequirement.setAuditStatus(2);
+                            latestRequirement.setAuditReason(exceptionMessage);
+                            this.saveOrUpdate(latestRequirement);
+                            // 发送审核异常通知(视频)
+                            if (finalStoreUser != null && finalStoreUser.getPhone() != null) {
+                                sendAuditNotification(finalStoreUser.getPhone(), 2, exceptionMessage, "video");
+                            }
+                        }
+                    }
+                    }, videoAuditExecutor);
+                }
+            }
+            requirement.setAuditTime(new Date());
+            boolean isSuccess  = this.saveOrUpdate(requirement);
+
+            return isSuccess;
+        } catch (Exception e) {
+            log.error("保存装修需求失败: {}", e.getMessage(), e);
+            throw new RuntimeException("保存装修需求失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 对URL列表进行分类(图片/视频/其他)
+     * @param attachmentUrls 原始URL列表
+     * @return 分类后的Map
+     */
+    private static Map<String, List<String>> classifyUrls(List<String> attachmentUrls) {
+        // 初始化分类容器
+        List<String> imageUrls = new ArrayList<>();
+        List<String> videoUrls = new ArrayList<>();
+        List<String> otherUrls = new ArrayList<>();
+
+        if (Objects.isNull(attachmentUrls) || attachmentUrls.isEmpty()) {
+            return buildResultMap(imageUrls, videoUrls, otherUrls);
+        }
+
+        for (String url : attachmentUrls) {
+            // 空URL直接归为其他
+            if (url == null || url.trim().isEmpty()) {
+                otherUrls.add(url);
+                continue;
+            }
+
+            String lowerUrl = url.toLowerCase().trim();
+            // 优先判断图片(避免URL中同时包含图片+视频后缀的极端情况)
+            boolean isImage = IMAGE_SUFFIXES.stream().anyMatch(lowerUrl::contains);
+            if (isImage) {
+                imageUrls.add(url); // 保留原始URL,而非小写后的
+                continue;
+            }
+
+            // 判断是否为视频
+            boolean isVideo = VIDEO_SUFFIXES.stream().anyMatch(lowerUrl::contains);
+            if (isVideo) {
+                videoUrls.add(url);
+            } else {
+                otherUrls.add(url);
+            }
+        }
+
+        return buildResultMap(imageUrls, videoUrls, otherUrls);
+    }
+
+    /**
+     * 构建分类结果Map
+     */
+    private 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));
+        resultMap.put("other", Collections.unmodifiableList(otherUrls));
+        return resultMap;
+    }
+
+    /**
+     * 发送审核结果通知
+     * @param storePhone 商铺电话
+     * @param auditStatus 审核状态:1-通过,2-不通过
+     * @param auditReason 审核原因(不通过时提供)
+     * @param auditType 审核类型:text_image-文本图片审核,video-视频审核
+     */
+    private void sendAuditNotification(String storePhone, Integer auditStatus, String auditReason, String auditType) {
+        try {
+            if (storePhone == null || storePhone.trim().isEmpty()) {
+                log.warn("发送审核通知失败,商铺电话为空");
+                return;
+            }
+
+            SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日");
+            String commonDate = sdf.format(new Date());
+
+            String title;
+            String message;
+            if (auditStatus == 1) {
+                // 审核通过
+                title = "审核通知";
+                if ("video".equals(auditType)) {
+                    message = "在" + commonDate + ",您发布的装修动态视频审核已通过。";
+                } else {
+                    message = "您发布的装修需求已通过审核,平台已将此需求发布,有意向的装修公司会与您联系";
+                }
+            } else {
+                // 审核不通过
+                title = "审核通知";
+//                String reasonText = auditReason != null && !auditReason.trim().isEmpty()
+//                    ? ",原因:" + auditReason
+//                    : "";
+                if ("video".equals(auditType)) {
+                    message = "在" + commonDate + ",您发布的装修动态视频审核未通过" + auditReason + ",请修改后重新提交。";
+                } else {
+                    message = "您发布的装修需求未通过审核.驳回原因:"+auditReason+"请您重新发布.";
+                }
+            }
+
+            LifeNotice lifeNotice = new LifeNotice();
+            lifeNotice.setReceiverId("store_" + storePhone);
+            JSONObject jsonObject = new JSONObject();
+            jsonObject.put("message", message);
+            lifeNotice.setContext(jsonObject.toJSONString());
+            lifeNotice.setTitle(title);
+            lifeNotice.setSenderId("system");
+            lifeNotice.setIsRead(0);
+            lifeNotice.setNoticeType(1);
+            lifeNoticeMapper.insert(lifeNotice);
+
+            WebSocketVo websocketVo = new WebSocketVo();
+            websocketVo.setSenderId("system");
+            websocketVo.setReceiverId("store_" + storePhone);
+            websocketVo.setCategory("notice");
+            websocketVo.setNoticeType("1");
+            websocketVo.setIsRead(0);
+            websocketVo.setText(JSONObject.from(lifeNotice).toJSONString());
+            webSocketProcess.sendMessage("store_" + storePhone, JSONObject.from(websocketVo).toJSONString());
+
+            log.info("审核通知发送成功,商铺电话:{},审核状态:{},审核类型:{}", storePhone, auditStatus, auditType);
+        } catch (Exception e) {
+            log.error("发送审核通知异常,商铺电话:{},审核状态:{},审核类型:{}", storePhone, auditStatus, auditType, e);
+        }
+    }
+
+
+    @Override
+    public StoreRenovationRequirementDto getRequirementDetail(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.getRequirementDetail?id={}", id);
+        StoreRenovationRequirement requirement = this.getById(id);
+        if (requirement == null) {
+            return null;
+        }
+
+        StoreRenovationRequirementDto dto = new StoreRenovationRequirementDto();
+        BeanUtils.copyProperties(requirement, dto);
+
+        // 处理附件URL字符串:逗号拼接转List
+        if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+            List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+            dto.setAttachmentUrls(attachmentUrls);
+        } else {
+            dto.setAttachmentUrls(new ArrayList<>());
+        }
+
+        return dto;
+    }
+
+    @Override
+    public IPage<StoreRenovationRequirementDto> getRequirementPage(Page<StoreRenovationRequirementDto> page,
+                                                                     Integer storeId,
+                                                                     String city,
+                                                                     Integer renovationType,
+                                                                     Integer status,
+                                                                     Integer auditStatus,
+                                                                     Integer sortType) {
+        log.info("StoreRenovationRequirementServiceImpl.getRequirementPage?storeId={}, city={}, renovationType={}, status={}, auditStatus={}, sortType={}",
+                storeId, city, renovationType, status, auditStatus, sortType);
+
+        LambdaQueryWrapper<StoreRenovationRequirement> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getDeleteFlag, 0);
+
+        if (storeId != null) {
+            wrapper.eq(StoreRenovationRequirement::getStoreId, storeId);
+        }
+        if (StringUtils.hasText(city)) {
+            wrapper.eq(StoreRenovationRequirement::getCity, city);
+        }
+        if (renovationType != null) {
+            wrapper.eq(StoreRenovationRequirement::getRenovationType, renovationType);
+        }
+        if (status != null) {
+            wrapper.eq(StoreRenovationRequirement::getStatus, status);
+        }
+        if (auditStatus != null) {
+            wrapper.eq(StoreRenovationRequirement::getAuditStatus, auditStatus);
+        }
+
+        // 排序处理:1-最新发布, 2-价格最高, 3-面积最大
+        if (sortType == null || sortType == 1) {
+            // 默认:最新发布(按创建时间降序)
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime);
+        } else if (sortType == 2) {
+            // 价格最高(按装修预算降序)
+            wrapper.orderByDesc(StoreRenovationRequirement::getRenovationBudget);
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime); // 价格相同则按时间排序
+        } else if (sortType == 3) {
+            // 面积最大(按房屋面积降序)
+            wrapper.orderByDesc(StoreRenovationRequirement::getHouseArea);
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime); // 面积相同则按时间排序
+        } else {
+            // 无效的排序类型,使用默认排序
+            wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime);
+        }
+
+        IPage<StoreRenovationRequirement> requirementPage = this.page(new Page<>(page.getCurrent(), page.getSize()), wrapper);
+
+        // 转换为DTO
+        IPage<StoreRenovationRequirementDto> dtoPage = requirementPage.convert(requirement -> {
+            StoreRenovationRequirementDto dto = new StoreRenovationRequirementDto();
+            BeanUtils.copyProperties(requirement, dto);
+
+            // 处理附件URL字符串:逗号拼接转List
+            if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+                List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+                dto.setAttachmentUrls(attachmentUrls);
+            } else {
+                dto.setAttachmentUrls(new ArrayList<>());
+            }
+
+            return dto;
+        });
+
+        // 批量查询商铺信息并填充到DTO
+        if (dtoPage.getRecords() != null && !dtoPage.getRecords().isEmpty()) {
+            // 收集所有的门店ID
+            List<Integer> storeIds = dtoPage.getRecords().stream()
+                    .map(StoreRenovationRequirementDto::getStoreId)
+                    .filter(id -> id != null)
+                    .distinct()
+                    .collect(Collectors.toList());
+
+            // 批量查询商铺信息
+            if (!storeIds.isEmpty()) {
+                List<StoreInfo> storeInfoList = storeInfoMapper.selectBatchIds(storeIds);
+                // 转换为Map,key为storeId,value为StoreInfo
+                Map<Integer, StoreInfo> storeInfoMap = storeInfoList.stream()
+                        .collect(Collectors.toMap(StoreInfo::getId, storeInfo -> storeInfo, (v1, v2) -> v1));
+
+                // 填充商铺信息到DTO
+                dtoPage.getRecords().forEach(dto -> {
+                    StoreInfo storeInfo = storeInfoMap.get(dto.getStoreId());
+                    if (storeInfo != null) {
+                        dto.setStoreName(storeInfo.getStoreName());
+                        dto.setStoreTel(storeInfo.getStoreTel());
+                        dto.setStoreAddress(storeInfo.getStoreAddress());
+                        dto.setStoreBlurb(storeInfo.getStoreBlurb());
+                    }
+                });
+            }
+
+            // 检查是否已与发布商铺发生沟通
+            String currentUserPhone = null;
+            try {
+                com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+                if (userInfo != null) {
+                    currentUserPhone = userInfo.getString("phone");
+                }
+            } catch (Exception e) {
+                log.debug("获取当前用户手机号失败(用户可能未登录): {}", e.getMessage());
+            }
+
+            if (currentUserPhone != null && StringUtils.hasText(currentUserPhone)) {
+                String currentUserPhoneId = "store_" + currentUserPhone;
+
+                // 批量查询沟通状态(优化:使用Map缓存结果,避免重复查询)
+                Map<String, Boolean> communicationStatusMap = new HashMap<>();
+
+                dtoPage.getRecords().forEach(dto -> {
+                    String storeTel = dto.getStoreTel();
+                    if (storeTel != null && StringUtils.hasText(storeTel)) {
+                        String publishingShopPhoneId = "store_" + storeTel;
+
+                        // 检查是否已查询过该商铺的沟通状态
+                        String communicationKey = currentUserPhoneId + "_" + publishingShopPhoneId;
+                        Boolean hasCommunicated = communicationStatusMap.get(communicationKey);
+
+                        if (hasCommunicated == null) {
+                            // 查询是否有消息记录(检查双向消息)
+                            LambdaQueryWrapper<LifeMessage> messageWrapper = new LambdaQueryWrapper<>();
+                            messageWrapper.eq(LifeMessage::getDeleteFlag, 0);
+                            messageWrapper.and(w -> w.and(w1 -> w1.eq(LifeMessage::getSenderId, currentUserPhoneId)
+                                            .eq(LifeMessage::getReceiverId, publishingShopPhoneId))
+                                    .or(w2 -> w2.eq(LifeMessage::getSenderId, publishingShopPhoneId)
+                                            .eq(LifeMessage::getReceiverId, currentUserPhoneId)));
+                            Integer messageCount = lifeMessageMapper.selectCount(messageWrapper);
+                            hasCommunicated = messageCount != null && messageCount > 0;
+                            communicationStatusMap.put(communicationKey, hasCommunicated);
+                        }
+
+                        dto.setHasCommunicated(hasCommunicated);
+                    } else {
+                        dto.setHasCommunicated(false);
+                    }
+                });
+            } else {
+                // 用户未登录,所有记录都设为未沟通
+                dtoPage.getRecords().forEach(dto -> dto.setHasCommunicated(false));
+            }
+        }
+
+        return dtoPage;
+    }
+
+    @Override
+    public void incrementViewCount(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.incrementViewCount?id={}", id);
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.setSql("view_count = view_count + 1");
+        this.update(wrapper);
+    }
+
+    @Override
+    public void incrementInquiryCount(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.incrementInquiryCount?id={}", id);
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.setSql("inquiry_count = inquiry_count + 1");
+        this.update(wrapper);
+    }
+
+    @Override
+    public boolean updateStatus(Integer id, Integer status) {
+        log.info("StoreRenovationRequirementServiceImpl.updateStatus?id={}, status={}", id, status);
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.set(StoreRenovationRequirement::getStatus, status);
+        wrapper.set(StoreRenovationRequirement::getUpdatedTime, new Date());
+
+        try {
+            com.alibaba.fastjson.JSONObject userInfo = JwtUtil.getCurrentUserInfo();
+            if (userInfo != null) {
+                wrapper.set(StoreRenovationRequirement::getUpdatedUserId, userInfo.getInteger("userId"));
+            }
+        } catch (Exception e) {
+            log.warn("获取当前用户ID失败: {}", e.getMessage());
+        }
+
+        return this.update(wrapper);
+    }
+
+    @Override
+    public boolean auditRequirement(Integer id, Integer auditStatus, String auditReason, Integer auditUserId) {
+        log.info("StoreRenovationRequirementServiceImpl.auditRequirement?id={}, auditStatus={}, auditReason={}, auditUserId={}",
+                id, auditStatus, auditReason, auditUserId);
+
+        if (auditStatus == 2 && !StringUtils.hasText(auditReason)) {
+            throw new RuntimeException("审核失败时必须填写审核原因");
+        }
+
+        LambdaUpdateWrapper<StoreRenovationRequirement> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getId, id);
+        wrapper.set(StoreRenovationRequirement::getAuditStatus, auditStatus);
+        wrapper.set(StoreRenovationRequirement::getAuditTime, new Date());
+        wrapper.set(StoreRenovationRequirement::getAuditUserId, auditUserId);
+        wrapper.set(StoreRenovationRequirement::getUpdatedTime, new Date());
+
+        if (auditStatus == 2) {
+            wrapper.set(StoreRenovationRequirement::getAuditReason, auditReason);
+        } else {
+            wrapper.set(StoreRenovationRequirement::getAuditReason, null);
+        }
+
+        return this.update(wrapper);
+    }
+
+    @Override
+    public boolean deleteRequirement(Integer id) {
+        log.info("StoreRenovationRequirementServiceImpl.deleteRequirement?id={}", id);
+        return this.removeById(id);
+    }
+
+    @Override
+    public List<StoreRenovationRequirementDto> getRequirementListByStoreId(Integer storeId) {
+        log.info("StoreRenovationRequirementServiceImpl.getRequirementListByStoreId?storeId={}", storeId);
+        LambdaQueryWrapper<StoreRenovationRequirement> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(StoreRenovationRequirement::getStoreId, storeId);
+        wrapper.eq(StoreRenovationRequirement::getDeleteFlag, 0);
+        wrapper.orderByDesc(StoreRenovationRequirement::getCreatedTime);
+
+        List<StoreRenovationRequirement> requirements = this.list(wrapper);
+
+        return requirements.stream().map(requirement -> {
+            StoreRenovationRequirementDto dto = new StoreRenovationRequirementDto();
+            BeanUtils.copyProperties(requirement, dto);
+
+            // 处理附件URL字符串:逗号拼接转List
+            if (StringUtils.hasText(requirement.getAttachmentUrls())) {
+                List<String> attachmentUrls = Arrays.asList(requirement.getAttachmentUrls().split(","));
+                dto.setAttachmentUrls(attachmentUrls);
+            } else {
+                dto.setAttachmentUrls(new ArrayList<>());
+            }
+
+            return dto;
+        }).collect(Collectors.toList());
+    }
+}
+

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

@@ -125,6 +125,7 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
         String status = query == null ? null : query.getStatus();
         Integer onlineStatus = query == null ? null : query.getOnlineStatus();
         String staffPosition = query == null ? null : query.getStaffPosition();
+        String staffName = query == null ? null : query.getStaffName();
 
         IPage<StoreStaffConfig> storePage = new Page<>(page, size);
         if (storeId == null) {
@@ -135,6 +136,7 @@ public class StoreStaffConfigServiceImpl implements StoreStaffConfigService {
         queryWrapper.eq(onlineStatus != null, "online_status", onlineStatus);
         queryWrapper.eq("store_id", storeId);
         queryWrapper.eq(StringUtils.isNotEmpty(staffPosition), "staff_position", staffPosition);
+        queryWrapper.eq(StringUtils.isNotEmpty(staffName), "name", staffName);
         // 只查询未删除的记录
         queryWrapper.eq("delete_flag", 0);
         // 排序规则:先按置顶状态降序(置顶的在前),再按置顶时间降序,最后按创建时间降序

+ 34 - 0
alien-store/src/main/java/shop/alien/store/service/impl/StoreTagDetailServiceImpl.java

@@ -0,0 +1,34 @@
+package shop.alien.store.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.store.TagStoreRelation;
+import shop.alien.mapper.StoreTagDetailMapper;
+import shop.alien.store.service.StoreTagDetailService;
+
+/**
+ * 店铺标签详情服务实现类
+ *
+ * @author system
+ * @since 2025-01-01
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class StoreTagDetailServiceImpl implements StoreTagDetailService {
+    
+    private final StoreTagDetailMapper storeTagDetailMapper;
+    
+    @Override
+    public TagStoreRelation getStoreTagDetailByStoreId(Integer storeId) {
+        log.info("StoreTagDetailServiceImpl.getStoreTagDetailByStoreId?storeId={}", storeId);
+        if (storeId == null) {
+            log.warn("店铺ID为空");
+            return null;
+        }
+        TagStoreRelation tagStoreRelation = storeTagDetailMapper.selectStoreTagDetailByStoreId(storeId);
+        log.info("查询店铺标签详情结果: {}", tagStoreRelation != null ? "查询成功" : "未找到数据");
+        return tagStoreRelation;
+    }
+}

+ 391 - 0
alien-store/src/main/java/shop/alien/store/service/impl/TrackEventServiceImpl.java

@@ -0,0 +1,391 @@
+package shop.alien.store.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.entity.store.StoreTrackEvent;
+import shop.alien.entity.store.StoreTrackStatistics;
+import shop.alien.mapper.StoreTrackEventMapper;
+import shop.alien.mapper.StoreTrackStatisticsMapper;
+import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.TrackEventService;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 埋点事件服务实现类
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+@Transactional
+public class TrackEventServiceImpl extends ServiceImpl<StoreTrackEventMapper, StoreTrackEvent> implements TrackEventService {
+
+    private final BaseRedisService baseRedisService;
+    private final StoreTrackStatisticsMapper trackStatisticsMapper;
+    private final ObjectMapper objectMapper;
+    
+    private static final String REDIS_QUEUE_KEY = "track:event:queue";
+
+    @Override
+    public void saveTrackEvent(StoreTrackEvent trackEvent) {
+        try {
+            String eventJson = objectMapper.writeValueAsString(trackEvent);
+            baseRedisService.setListRight(REDIS_QUEUE_KEY, eventJson);
+        } catch (Exception e) {
+            log.error("写入Redis List失败", e);
+            throw new RuntimeException("写入Redis List失败", e);
+        }
+    }
+
+    @Override
+    public void batchSaveTrackEvents(List<StoreTrackEvent> trackEvents) {
+        if (trackEvents == null || trackEvents.isEmpty()) {
+            return;
+        }
+
+        try {
+            boolean result = this.saveBatch(trackEvents, 100);
+            log.info("批量保存埋点事件: 总数={}, 成功={}", trackEvents.size(), result);
+        } catch (Exception e) {
+            log.error("批量保存埋点事件失败", e);
+            throw new RuntimeException("批量保存埋点事件失败", e);
+        }
+    }
+
+    @Override
+    public void calculateAndSaveStatistics(Integer storeId, Date statDate, String statType) {
+        log.info("开始计算统计数据: storeId={}, statDate={}, statType={}", storeId, statDate, statType);
+
+        try {
+            // 计算统计日期范围
+            Date startDate = statDate;
+            Date endDate = statDate;
+            
+            // 根据统计类型确定日期范围
+            if ("WEEKLY".equals(statType)) {
+                // 周统计:从周一(statDate)到周日
+                java.util.Calendar cal = java.util.Calendar.getInstance();
+                cal.setTime(statDate);
+                cal.set(java.util.Calendar.DAY_OF_WEEK, java.util.Calendar.MONDAY);
+                startDate = cal.getTime();
+                cal.add(java.util.Calendar.DAY_OF_WEEK, 6);
+                endDate = cal.getTime();
+            } else if ("MONTHLY".equals(statType)) {
+                // 月统计:从月初到月末
+                java.util.Calendar cal = java.util.Calendar.getInstance();
+                cal.setTime(statDate);
+                cal.set(java.util.Calendar.DAY_OF_MONTH, 1);
+                startDate = cal.getTime();
+                cal.add(java.util.Calendar.MONTH, 1);
+                cal.add(java.util.Calendar.DAY_OF_MONTH, -1);
+                endDate = cal.getTime();
+            }
+        
+        // 查询或创建统计记录
+        LambdaQueryWrapper<StoreTrackStatistics> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(StoreTrackStatistics::getStoreId, storeId)
+                .eq(StoreTrackStatistics::getStatDate, statDate)
+                .eq(StoreTrackStatistics::getStatType, statType);
+        
+        StoreTrackStatistics statistics = trackStatisticsMapper.selectOne(queryWrapper);
+        if (statistics == null) {
+            statistics = new StoreTrackStatistics();
+            statistics.setStoreId(storeId);
+            statistics.setStatDate(statDate);
+            statistics.setStatType(statType);
+        }
+        
+            // 从store_track_event表统计基础数据
+            LambdaQueryWrapper<StoreTrackEvent> eventWrapper = new LambdaQueryWrapper<>();
+            eventWrapper.eq(StoreTrackEvent::getStoreId, storeId)
+                    .ge(StoreTrackEvent::getEventTime, startDate)
+                    .le(StoreTrackEvent::getEventTime, endDate)
+                    .eq(StoreTrackEvent::getDeleteFlag, 0);
+
+            List<StoreTrackEvent> events = this.list(eventWrapper);
+
+            // 按事件分类统计(符合文档格式)
+            Map<String, Object> trafficData = calculateTrafficData(events, storeId, startDate);
+            Map<String, Object> interactionData = calculateInteractionData(events, storeId);
+            Map<String, Object> couponData = calculateCouponData(events, storeId);
+            Map<String, Object> voucherData = calculateVoucherData(events, storeId);
+            Map<String, Object> serviceData = calculateServiceData(events, storeId);
+            List<Map<String, Object>> priceData = calculatePriceRankingData(events);
+
+            // 设置统计数据(JSON格式)
+            statistics.setTrafficData(objectMapper.writeValueAsString(trafficData));
+            statistics.setInteractionData(objectMapper.writeValueAsString(interactionData));
+            statistics.setCouponData(objectMapper.writeValueAsString(couponData));
+            statistics.setVoucherData(objectMapper.writeValueAsString(voucherData));
+            statistics.setServiceData(objectMapper.writeValueAsString(serviceData));
+            statistics.setPriceRankingData(objectMapper.writeValueAsString(priceData));
+        
+        // 保存统计记录
+        if (statistics.getId() == null) {
+            trackStatisticsMapper.insert(statistics);
+                log.info("统计数据保存成功: storeId={}, statDate={}, statType={}", storeId, statDate, statType);
+        } else {
+            trackStatisticsMapper.updateById(statistics);
+                log.info("统计数据更新成功: storeId={}, statDate={}, statType={}", storeId, statDate, statType);
+            }
+        } catch (Exception e) {
+            log.error("计算统计数据失败: storeId={}, statDate={}, statType={}", storeId, statDate, statType, e);
+            throw new RuntimeException("计算统计数据失败", e);
+        }
+    }
+
+    /**
+     * 计算流量数据(符合文档格式)
+     */
+    private Map<String, Object> calculateTrafficData(List<StoreTrackEvent> events, Integer storeId, Date startDate) {
+        Map<String, Object> result = new java.util.HashMap<>();
+        
+        List<StoreTrackEvent> trafficEvents = events.stream()
+                .filter(e -> "TRAFFIC".equals(e.getEventCategory()))
+                .collect(java.util.stream.Collectors.toList());
+        
+        // 搜索量
+        long searchCount = trafficEvents.stream()
+                .filter(e -> "SEARCH".equals(e.getEventType()))
+                .count();
+        result.put("searchCount", searchCount);
+        
+        // 浏览量
+        long viewCount = trafficEvents.stream()
+                .filter(e -> "VIEW".equals(e.getEventType()))
+                .count();
+        result.put("viewCount", viewCount);
+        
+        // 访客数(去重userId)
+        long visitorCount = trafficEvents.stream()
+                .filter(e -> e.getUserId() != null)
+                .map(StoreTrackEvent::getUserId)
+                .distinct()
+                .count();
+        result.put("visitorCount", visitorCount);
+        
+        // 新增访客数(在统计日期之前没有访问记录的用户)
+        // TODO: 需要查询历史数据,暂时返回0
+        result.put("newVisitorCount", 0L);
+        
+        // 总访问时长(所有浏览事件duration字段的总和)
+        long totalDuration = trafficEvents.stream()
+                .filter(e -> "VIEW".equals(e.getEventType()) && e.getDuration() != null)
+                .mapToLong(StoreTrackEvent::getDuration)
+                .sum();
+        result.put("totalDuration", totalDuration);
+        
+        // 平均访问时长
+        long viewEventsWithDuration = trafficEvents.stream()
+                .filter(e -> "VIEW".equals(e.getEventType()) && e.getDuration() != null)
+                .count();
+        long avgDuration = viewEventsWithDuration > 0 ? totalDuration / viewEventsWithDuration : 0L;
+        result.put("avgDuration", avgDuration);
+        
+        return result;
+    }
+    
+    /**
+     * 计算互动数据(符合文档格式)
+     */
+    private Map<String, Object> calculateInteractionData(List<StoreTrackEvent> events, Integer storeId) {
+        Map<String, Object> result = new java.util.HashMap<>();
+        
+        List<StoreTrackEvent> interactionEvents = events.stream()
+                .filter(e -> "INTERACTION".equals(e.getEventCategory()))
+                .collect(java.util.stream.Collectors.toList());
+        
+        result.put("collectCount", countByEventType(interactionEvents, "COLLECT"));
+        result.put("shareCount", countByEventType(interactionEvents, "SHARE"));
+        result.put("checkinCount", countByEventType(interactionEvents, "CHECKIN"));
+        result.put("consultCount", countByEventType(interactionEvents, "CONSULT"));
+        result.put("postLikeCount", countByEventType(interactionEvents, "POST_LIKE"));
+        result.put("postCommentCount", countByEventType(interactionEvents, "POST_COMMENT"));
+        result.put("postRepostCount", countByEventType(interactionEvents, "POST_REPOST"));
+        result.put("reportCount", countByEventType(interactionEvents, "REPORT"));
+        result.put("blockCount", countByEventType(interactionEvents, "BLOCK"));
+        
+        // TODO: 需要从其他表查询的数据,暂时返回0
+        result.put("friendCount", 0L);
+        result.put("followCount", 0L);
+        result.put("fansCount", 0L);
+        result.put("postCount", 0L);
+        
+        return result;
+    }
+    
+    /**
+     * 计算优惠券数据(符合文档格式)
+     */
+    private Map<String, Object> calculateCouponData(List<StoreTrackEvent> events, Integer storeId) {
+        Map<String, Object> result = new java.util.HashMap<>();
+        
+        // TODO: 需要从 life_discount_coupon_user 等表查询,暂时返回0
+        result.put("giveToFriendCount", 0L);
+        result.put("giveToFriendAmount", java.math.BigDecimal.ZERO);
+        result.put("giveToFriendUseCount", 0L);
+        result.put("giveToFriendUseAmount", java.math.BigDecimal.ZERO);
+        result.put("giveToFriendUseAmountPercent", 0.0);
+        result.put("friendGiveCount", 0L);
+        result.put("friendGiveAmount", java.math.BigDecimal.ZERO);
+        result.put("friendGiveUseCount", 0L);
+        result.put("friendGiveUseAmount", java.math.BigDecimal.ZERO);
+        result.put("friendGiveUseAmountPercent", 0.0);
+        
+        return result;
+    }
+    
+    /**
+     * 计算代金券数据(符合文档格式)
+     */
+    private Map<String, Object> calculateVoucherData(List<StoreTrackEvent> events, Integer storeId) {
+        Map<String, Object> result = new java.util.HashMap<>();
+        
+        List<StoreTrackEvent> voucherEvents = events.stream()
+                .filter(e -> "VOUCHER".equals(e.getEventCategory()))
+                .collect(java.util.stream.Collectors.toList());
+        
+        // 赠送好友数量
+        long giveToFriendCount = countByEventType(voucherEvents, "VOUCHER_GIVE");
+        result.put("giveToFriendCount", giveToFriendCount);
+        
+        // 赠送好友金额合计
+        java.math.BigDecimal giveToFriendAmount = voucherEvents.stream()
+                .filter(e -> "VOUCHER_GIVE".equals(e.getEventType()) && e.getAmount() != null)
+                .map(StoreTrackEvent::getAmount)
+                .reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
+        result.put("giveToFriendAmount", giveToFriendAmount);
+        
+        // 赠送好友使用数量
+        long giveToFriendUseCount = countByEventType(voucherEvents, "VOUCHER_USE");
+        result.put("giveToFriendUseCount", giveToFriendUseCount);
+        
+        // 赠送好友使用金额合计
+        java.math.BigDecimal giveToFriendUseAmount = voucherEvents.stream()
+                .filter(e -> "VOUCHER_USE".equals(e.getEventType()) && e.getAmount() != null)
+                .map(StoreTrackEvent::getAmount)
+                .reduce(java.math.BigDecimal.ZERO, java.math.BigDecimal::add);
+        result.put("giveToFriendUseAmount", giveToFriendUseAmount);
+        
+        // 赠送好友使用金额占比
+        double giveToFriendUseAmountPercent = 0.0;
+        if (giveToFriendAmount.compareTo(java.math.BigDecimal.ZERO) > 0) {
+            giveToFriendUseAmountPercent = giveToFriendUseAmount.divide(giveToFriendAmount, 4, java.math.RoundingMode.HALF_UP)
+                    .multiply(new java.math.BigDecimal("100")).doubleValue();
+        }
+        result.put("giveToFriendUseAmountPercent", giveToFriendUseAmountPercent);
+        
+        // TODO: 需要从 life_discount_coupon_store_friend 表查询的数据,暂时返回0
+        result.put("friendGiveCount", 0L);
+        result.put("friendGiveAmount", java.math.BigDecimal.ZERO);
+        result.put("friendGiveUseCount", 0L);
+        result.put("friendGiveUseAmount", java.math.BigDecimal.ZERO);
+        result.put("friendGiveUseAmountPercent", 0.0);
+        
+        return result;
+    }
+    
+    /**
+     * 计算服务质量数据(符合文档格式)
+     */
+    private Map<String, Object> calculateServiceData(List<StoreTrackEvent> events, Integer storeId) {
+        Map<String, Object> result = new java.util.HashMap<>();
+        
+        List<StoreTrackEvent> serviceEvents = events.stream()
+                .filter(e -> "SERVICE".equals(e.getEventCategory()))
+                .collect(java.util.stream.Collectors.toList());
+        
+        // 差评申诉次数
+        long appealCount = countByEventType(serviceEvents, "APPEAL");
+        result.put("appealCount", appealCount);
+        
+        // TODO: 需要从 common_rating 和 store_comment_appeal 表查询的数据,暂时返回0或null
+        result.put("storeScore", null);
+        result.put("tasteScore", null);
+        result.put("environmentScore", null);
+        result.put("serviceScore", null);
+        result.put("ratingCount", 0L);
+        result.put("goodRatingCount", 0L);
+        result.put("midRatingCount", 0L);
+        result.put("badRatingCount", 0L);
+        result.put("badRatingPercent", 0.0);
+        result.put("appealSuccessCount", 0L);
+        result.put("appealSuccessPercent", 0.0);
+        
+        return result;
+    }
+    
+    /**
+     * 计算价目表排名数据(符合文档格式)
+     */
+    private List<Map<String, Object>> calculatePriceRankingData(List<StoreTrackEvent> events) {
+        List<StoreTrackEvent> priceEvents = events.stream()
+                .filter(e -> "PRICE".equals(e.getEventCategory()))
+                .collect(java.util.stream.Collectors.toList());
+        
+        // 按 targetId (priceId) 分组统计
+        Map<Integer, List<StoreTrackEvent>> eventsByPriceId = priceEvents.stream()
+                .filter(e -> e.getTargetId() != null)
+                .collect(java.util.stream.Collectors.groupingBy(StoreTrackEvent::getTargetId));
+        
+        List<Map<String, Object>> result = new java.util.ArrayList<>();
+        
+        for (Map.Entry<Integer, List<StoreTrackEvent>> entry : eventsByPriceId.entrySet()) {
+            Integer priceId = entry.getKey();
+            List<StoreTrackEvent> priceIdEvents = entry.getValue();
+            
+            Map<String, Object> priceData = new java.util.HashMap<>();
+            priceData.put("priceId", priceId);
+            
+            // 浏览量(PRICE_VIEW事件数量)
+            long viewCount = priceIdEvents.stream()
+                    .filter(e -> "PRICE_VIEW".equals(e.getEventType()))
+                    .count();
+            priceData.put("viewCount", (int) viewCount);
+            
+            // 访客数(去重后的用户ID数量)
+            long visitorCount = priceIdEvents.stream()
+                    .filter(e -> e.getUserId() != null)
+                    .map(StoreTrackEvent::getUserId)
+                    .distinct()
+                    .count();
+            priceData.put("visitorCount", (int) visitorCount);
+            
+            // 分享数(PRICE_SHARE事件数量)
+            long shareCount = priceIdEvents.stream()
+                    .filter(e -> "PRICE_SHARE".equals(e.getEventType()))
+                    .count();
+            priceData.put("shareCount", (int) shareCount);
+            
+            result.add(priceData);
+        }
+        
+        // 按 viewCount 降序排列
+        result.sort((a, b) -> {
+            Integer viewCountA = (Integer) a.get("viewCount");
+            Integer viewCountB = (Integer) b.get("viewCount");
+            return viewCountB.compareTo(viewCountA);
+        });
+        
+        return result;
+    }
+    
+    /**
+     * 统计指定事件类型的数量
+     */
+    private long countByEventType(List<StoreTrackEvent> events, String eventType) {
+        return events.stream()
+                .filter(e -> eventType.equals(e.getEventType()))
+                .count();
+    }
+}

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

@@ -111,4 +111,5 @@ public class CommonConstant {
     public static final String LIKE_TYPE_STORE = "9";
     public static final String RATING_LIKE = "11";
     public static final String COMMENT_LIKE = "12";
+    public static final String DYNAMIC_LIKE = "13";
 }

+ 189 - 0
alien-store/src/main/java/shop/alien/store/util/UserAgentParserUtil.java

@@ -0,0 +1,189 @@
+package shop.alien.store.util;
+
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * User-Agent解析工具类
+ * 用于从User-Agent字符串中解析设备类型
+ *
+ * @author system
+ * @since 2026-01-14
+ */
+@Slf4j
+public class UserAgentParserUtil {
+
+    /**
+     * 设备类型常量
+     */
+    public static final String DEVICE_TYPE_IOS = "iOS";
+    public static final String DEVICE_TYPE_ANDROID = "Android";
+    public static final String DEVICE_TYPE_HARMONY = "Harmony";
+    public static final String DEVICE_TYPE_PC = "PC";
+    public static final String DEVICE_TYPE_UNKNOWN = "Unknown";
+
+    /**
+     * 从User-Agent字符串中解析设备类型
+     * 优先返回移动端操作系统类型:iOS、Android、Harmony等
+     *
+     * @param userAgent User-Agent字符串
+     * @return 设备类型:iOS、Android、Harmony、PC、Unknown
+     */
+    public static String parseDeviceType(String userAgent) {
+        if (StringUtils.isBlank(userAgent)) {
+            return DEVICE_TYPE_UNKNOWN;
+        }
+
+        String ua = userAgent.toLowerCase();
+
+        // 判断是否为iOS设备(iPhone、iPad)
+        if (isIOS(ua)) {
+            return DEVICE_TYPE_IOS;
+        }
+
+        // 判断是否为HarmonyOS设备(鸿蒙)
+        if (isHarmonyOS(ua)) {
+            return DEVICE_TYPE_HARMONY;
+        }
+
+        // 判断是否为Android设备
+        if (isAndroid(ua)) {
+            return DEVICE_TYPE_ANDROID;
+        }
+
+        // 默认为PC
+        return DEVICE_TYPE_PC;
+    }
+
+    /**
+     * 判断是否为iOS设备(iPhone、iPad)
+     */
+    private static boolean isIOS(String ua) {
+        // iPhone
+        if (ua.contains("iphone")) {
+            return true;
+        }
+
+        // iPad
+        if (ua.contains("ipad")) {
+            return true;
+        }
+
+        // iOS标识
+        if (ua.contains("iphone os") || ua.contains("ios")) {
+            return true;
+        }
+
+        // iPod touch
+        if (ua.contains("ipod")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 判断是否为HarmonyOS设备(鸿蒙)
+     */
+    private static boolean isHarmonyOS(String ua) {
+        // HarmonyOS标识
+        if (ua.contains("harmonyos") || ua.contains("harmony os")) {
+            return true;
+        }
+
+        // 鸿蒙系统可能还包含其他标识
+        if (ua.contains("hmos")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 判断是否为Android设备
+     */
+    private static boolean isAndroid(String ua) {
+        // Android标识(包括手机和平板)
+        if (ua.contains("android")) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * 从User-Agent中提取浏览器信息(可选功能)
+     *
+     * @param userAgent User-Agent字符串
+     * @return 浏览器名称
+     */
+    public static String parseBrowser(String userAgent) {
+        if (StringUtils.isBlank(userAgent)) {
+            return "Unknown";
+        }
+
+        String ua = userAgent.toLowerCase();
+
+        if (ua.contains("chrome") && !ua.contains("edg")) {
+            return "Chrome";
+        } else if (ua.contains("safari") && !ua.contains("chrome")) {
+            return "Safari";
+        } else if (ua.contains("firefox")) {
+            return "Firefox";
+        } else if (ua.contains("edg")) {
+            return "Edge";
+        } else if (ua.contains("opera") || ua.contains("opr")) {
+            return "Opera";
+        } else if (ua.contains("msie") || ua.contains("trident")) {
+            return "IE";
+        } else if (ua.contains("micromessenger")) {
+            return "WeChat";
+        } else if (ua.contains("qqbrowser")) {
+            return "QQBrowser";
+        } else if (ua.contains("ucbrowser")) {
+            return "UCBrowser";
+        }
+
+        return "Unknown";
+    }
+
+    /**
+     * 从User-Agent中提取操作系统信息(可选功能)
+     *
+     * @param userAgent User-Agent字符串
+     * @return 操作系统名称
+     */
+    public static String parseOS(String userAgent) {
+        if (StringUtils.isBlank(userAgent)) {
+            return "Unknown";
+        }
+
+        String ua = userAgent.toLowerCase();
+
+        if (ua.contains("windows")) {
+            if (ua.contains("windows nt 10")) {
+                return "Windows 10";
+            } else if (ua.contains("windows nt 6.3")) {
+                return "Windows 8.1";
+            } else if (ua.contains("windows nt 6.2")) {
+                return "Windows 8";
+            } else if (ua.contains("windows nt 6.1")) {
+                return "Windows 7";
+            } else {
+                return "Windows";
+            }
+        } else if (ua.contains("mac os x") || ua.contains("macintosh")) {
+            return "macOS";
+        } else if (ua.contains("android")) {
+            return "Android";
+        } else if (ua.contains("iphone os") || ua.contains("ios")) {
+            return "iOS";
+        } else if (ua.contains("linux")) {
+            return "Linux";
+        } else if (ua.contains("unix")) {
+            return "Unix";
+        }
+
+        return "Unknown";
+    }
+}

+ 2 - 2
alien-util/src/main/java/shop/alien/util/common/constant/CommentSourceTypeEnum.java

@@ -2,8 +2,8 @@ package shop.alien.util.common.constant;
 
 
 public enum CommentSourceTypeEnum {
-    STORE_COMMENT(1, "店铺评价");
-
+    STORE_COMMENT(1, "店铺评价"),
+    DYNAMIC_COMMENT(2, "动态评论");
     private final Integer type;
     private final String info;