Kaynağa Gözat

统计整理 添加表字段 维护代码

lutong 2 gün önce
ebeveyn
işleme
e268ed98b7
46 değiştirilmiş dosya ile 2196 ekleme ve 235 silme
  1. 1 1
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsAiChatStat.java
  2. 37 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsAiChatStatHistory.java
  3. 18 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsBusinessCategory.java
  4. 53 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsCategoryDaily.java
  5. 34 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsCategoryDailyToday.java
  6. 26 2
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsContentStat.java
  7. 71 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsContentStatHistory.java
  8. 72 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsDailySummary.java
  9. 12 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsEvent.java
  10. 42 2
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsMerchantStat.java
  11. 70 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsMerchantStatToday.java
  12. 69 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsReportRecord.java
  13. 57 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsReportRecordHistory.java
  14. 22 2
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsUserStat.java
  15. 66 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsUserStatToday.java
  16. 7 1
      alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsContentPublishDTO.java
  17. 1 1
      alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsHeartbeatDTO.java
  18. 3 0
      alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsTrackEventDTO.java
  19. 3 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/AnalyticsContentStatVo.java
  20. 3 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/AnalyticsUserStatVo.java
  21. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsAiChatStatHistoryMapper.java
  22. 46 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsArchiveMapper.java
  23. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsCategoryDailyMapper.java
  24. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsCategoryDailyTodayMapper.java
  25. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsContentStatHistoryMapper.java
  26. 6 2
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantStatMapper.java
  27. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantStatTodayMapper.java
  28. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsReportRecordHistoryMapper.java
  29. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsReportRecordMapper.java
  30. 9 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsUserStatTodayMapper.java
  31. 140 105
      alien-entity/src/main/resources/db/migration/analytics_tables.sql
  32. 129 0
      alien-entity/src/main/resources/db/migration/analytics_tables_dashboard_upgrade.sql
  33. 264 0
      alien-entity/src/main/resources/db/migration/analytics_today_history_tables.sql
  34. 164 0
      alien-entity/src/main/resources/mapper/AnalyticsArchiveMapper.xml
  35. 6 0
      alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java
  36. 17 0
      alien-job/src/main/java/shop/alien/job/store/AnalyticsStatisticsJob.java
  37. 11 0
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsJobController.java
  38. 51 9
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsStatController.java
  39. 14 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsArchiveService.java
  40. 47 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsDetailStoreService.java
  41. 8 1
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsStatEnrichService.java
  42. 57 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsArchiveServiceImpl.java
  43. 291 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDetailStoreServiceImpl.java
  44. 59 30
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatEnrichServiceImpl.java
  45. 61 34
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatisticsServiceImpl.java
  46. 86 45
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsTrackServiceImpl.java

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsAiChatStat.java

@@ -17,7 +17,7 @@ import java.util.Date;
  */
 @Data
 @JsonInclude
-@TableName("analytics_ai_chat_stat")
+@TableName("analytics_ai_chat_stat_today")
 @ApiModel(value = "AnalyticsAiChatStat", description = "AI对话明细统计")
 public class AnalyticsAiChatStat {
 

+ 37 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsAiChatStatHistory.java

@@ -0,0 +1,37 @@
+package shop.alien.entity.analytics;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@JsonInclude
+@TableName("analytics_ai_chat_stat_history")
+public class AnalyticsAiChatStatHistory {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("stat_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date statDate;
+
+    @TableField("chat_id")
+    private String chatId;
+
+    @TableField("user_id")
+    private Long userId;
+
+    @TableField("start_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date startTime;
+
+    @TableField("message_count")
+    private Integer messageCount;
+
+    @TableField("ai_response_duration_ms")
+    private Long aiResponseDurationMs;
+}

+ 18 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsBusinessCategory.java

@@ -0,0 +1,18 @@
+package shop.alien.entity.analytics;
+
+/**
+ * 经营品类(看板饼图/GMV占比统一枚举)
+ */
+public final class AnalyticsBusinessCategory {
+
+    private AnalyticsBusinessCategory() {
+    }
+
+    public static final int FOOD = 1;
+    public static final int LEISURE = 2;
+    public static final int LIFE_SERVICE = 3;
+    public static final int TRAVEL = 4;
+    public static final int HOTEL = 5;
+    public static final int SHOPPING = 6;
+    public static final int OTHER = 7;
+}

+ 53 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsCategoryDaily.java

@@ -0,0 +1,53 @@
+package shop.alien.entity.analytics;
+
+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;
+
+/**
+ * 经营品类日统计(商家 GMV 占比、内容品类分布趋势)。
+ */
+@Data
+@JsonInclude
+@TableName("analytics_category_daily_history")
+@ApiModel(value = "AnalyticsCategoryDaily", description = "经营品类日统计")
+public class AnalyticsCategoryDaily {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("统计日期")
+    @TableField("stat_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date statDate;
+
+    @ApiModelProperty("经营品类(见 AnalyticsBusinessCategory)")
+    @TableField("business_category")
+    private Integer businessCategory;
+
+    @ApiModelProperty("品类GMV")
+    @TableField("gmv")
+    private BigDecimal gmv;
+
+    @ApiModelProperty("支付笔数")
+    @TableField("pay_count")
+    private Integer payCount;
+
+    @ApiModelProperty("商家访问UV")
+    @TableField("merchant_visit_uv")
+    private Integer merchantVisitUv;
+
+    @ApiModelProperty("内容发布数")
+    @TableField("content_publish_count")
+    private Integer contentPublishCount;
+
+    @ApiModelProperty("内容互动数")
+    @TableField("content_interaction_count")
+    private Integer contentInteractionCount;
+}

+ 34 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsCategoryDailyToday.java

@@ -0,0 +1,34 @@
+package shop.alien.entity.analytics;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@JsonInclude
+@TableName("analytics_category_daily_today")
+public class AnalyticsCategoryDailyToday {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("business_category")
+    private Integer businessCategory;
+
+    @TableField("gmv")
+    private BigDecimal gmv;
+
+    @TableField("pay_count")
+    private Integer payCount;
+
+    @TableField("merchant_visit_uv")
+    private Integer merchantVisitUv;
+
+    @TableField("content_publish_count")
+    private Integer contentPublishCount;
+
+    @TableField("content_interaction_count")
+    private Integer contentInteractionCount;
+}

+ 26 - 2
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsContentStat.java

@@ -12,12 +12,12 @@ import java.util.Date;
 /**
  * 内容明细统计实体。
  *
- * <p>表名:analytics_content_stat</p>
+ * <p>表名:analytics_content_stat_today(零点归档至 history)</p>
  * <p>名称类字段不入库,查询时按 ID 回填或推导</p>
  */
 @Data
 @JsonInclude
-@TableName("analytics_content_stat")
+@TableName("analytics_content_stat_today")
 @ApiModel(value = "AnalyticsContentStat", description = "内容明细统计")
 public class AnalyticsContentStat {
 
@@ -32,6 +32,10 @@ public class AnalyticsContentStat {
     @TableField("content_type")
     private Integer contentType;
 
+    @ApiModelProperty("经营品类(见 AnalyticsBusinessCategory)")
+    @TableField("business_category")
+    private Integer businessCategory;
+
     @ApiModelProperty("作者类型(1用户2商家)")
     @TableField("author_type")
     private Integer authorType;
@@ -40,6 +44,10 @@ public class AnalyticsContentStat {
     @TableField("author_id")
     private Long authorId;
 
+    @ApiModelProperty("内容标题快照")
+    @TableField("content_title")
+    private String contentTitle;
+
     @ApiModelProperty("发布时间")
     @TableField("publish_time")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@@ -49,6 +57,18 @@ public class AnalyticsContentStat {
     @TableField("interaction_count")
     private Integer interactionCount;
 
+    @ApiModelProperty("点赞数")
+    @TableField("like_count")
+    private Integer likeCount;
+
+    @ApiModelProperty("评论数")
+    @TableField("comment_count")
+    private Integer commentCount;
+
+    @ApiModelProperty("分享数")
+    @TableField("share_count")
+    private Integer shareCount;
+
     @ApiModelProperty("状态")
     @TableField("status")
     private Integer status;
@@ -61,6 +81,10 @@ public class AnalyticsContentStat {
     @TableField("audit_status")
     private Integer auditStatus;
 
+    @ApiModelProperty("审核方式(1AI2人工)")
+    @TableField("audit_type")
+    private Integer auditType;
+
     @ApiModelProperty("审核时间")
     @TableField("audit_time")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")

+ 71 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsContentStatHistory.java

@@ -0,0 +1,71 @@
+package shop.alien.entity.analytics;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@JsonInclude
+@TableName("analytics_content_stat_history")
+public class AnalyticsContentStatHistory {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("stat_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date statDate;
+
+    @TableField("content_id")
+    private Long contentId;
+
+    @TableField("content_type")
+    private Integer contentType;
+
+    @TableField("business_category")
+    private Integer businessCategory;
+
+    @TableField("author_type")
+    private Integer authorType;
+
+    @TableField("author_id")
+    private Long authorId;
+
+    @TableField("content_title")
+    private String contentTitle;
+
+    @TableField("publish_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date publishTime;
+
+    @TableField("interaction_count")
+    private Integer interactionCount;
+
+    @TableField("like_count")
+    private Integer likeCount;
+
+    @TableField("comment_count")
+    private Integer commentCount;
+
+    @TableField("share_count")
+    private Integer shareCount;
+
+    @TableField("status")
+    private Integer status;
+
+    @TableField("audit_user_id")
+    private Long auditUserId;
+
+    @TableField("audit_status")
+    private Integer auditStatus;
+
+    @TableField("audit_type")
+    private Integer auditType;
+
+    @TableField("audit_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date auditTime;
+}

+ 72 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsDailySummary.java

@@ -34,14 +34,42 @@ public class AnalyticsDailySummary {
     @TableField("dau")
     private Integer dau;
 
+    @ApiModelProperty("MAU(近30日快照)")
+    @TableField("mau")
+    private Integer mau;
+
     @ApiModelProperty("新增用户数")
     @TableField("new_user_count")
     private Integer newUserCount;
 
+    @ApiModelProperty("注册漏斗-进入注册页")
+    @TableField("register_page_view_count")
+    private Integer registerPageViewCount;
+
+    @ApiModelProperty("注册漏斗-提交手机号")
+    @TableField("register_phone_submit_count")
+    private Integer registerPhoneSubmitCount;
+
+    @ApiModelProperty("注册漏斗-验证码通过")
+    @TableField("register_otp_pass_count")
+    private Integer registerOtpPassCount;
+
+    @ApiModelProperty("注册漏斗-设置密码")
+    @TableField("register_password_set_count")
+    private Integer registerPasswordSetCount;
+
+    @ApiModelProperty("注册漏斗-注册成功")
+    @TableField("register_success_count")
+    private Integer registerSuccessCount;
+
     @ApiModelProperty("今日AI对话次数")
     @TableField("ai_chat_count")
     private Integer aiChatCount;
 
+    @ApiModelProperty("对话用户数")
+    @TableField("ai_chat_user_count")
+    private Integer aiChatUserCount;
+
     @ApiModelProperty("今日内容发布数量")
     @TableField("content_publish_count")
     private Integer contentPublishCount;
@@ -50,10 +78,30 @@ public class AnalyticsDailySummary {
     @TableField("merchant_visit_uv")
     private Integer merchantVisitUv;
 
+    @ApiModelProperty("商家曝光次数")
+    @TableField("merchant_expose_count")
+    private Long merchantExposeCount;
+
+    @ApiModelProperty("商家点击次数")
+    @TableField("merchant_click_count")
+    private Long merchantClickCount;
+
+    @ApiModelProperty("商家详情浏览次数")
+    @TableField("merchant_detail_view_count")
+    private Long merchantDetailViewCount;
+
+    @ApiModelProperty("商家电话/导航次数")
+    @TableField("merchant_contact_count")
+    private Long merchantContactCount;
+
     @ApiModelProperty("AI响应时间总数(ms)")
     @TableField("ai_response_duration_total_ms")
     private Long aiResponseDurationTotalMs;
 
+    @ApiModelProperty("AI请求次数")
+    @TableField("ai_request_count")
+    private Integer aiRequestCount;
+
     @ApiModelProperty("当前在线总人数")
     @TableField("online_user_count")
     private Integer onlineUserCount;
@@ -86,6 +134,10 @@ public class AnalyticsDailySummary {
     @TableField("last_7d_active_user_count")
     private Integer last7dActiveUserCount;
 
+    @ApiModelProperty("近30日活跃用户数量")
+    @TableField("last_30d_active_user_count")
+    private Integer last30dActiveUserCount;
+
     @ApiModelProperty("首日注册用户次日有日活数")
     @TableField("next_day_retained_count")
     private Integer nextDayRetainedCount;
@@ -102,6 +154,18 @@ public class AnalyticsDailySummary {
     @TableField("audit_submit_count")
     private Integer auditSubmitCount;
 
+    @ApiModelProperty("AI审核通过次数")
+    @TableField("audit_ai_pass_count")
+    private Integer auditAiPassCount;
+
+    @ApiModelProperty("人工审核通过次数")
+    @TableField("audit_manual_pass_count")
+    private Integer auditManualPassCount;
+
+    @ApiModelProperty("审核驳回次数")
+    @TableField("audit_reject_count")
+    private Integer auditRejectCount;
+
     @ApiModelProperty("审核通过率(%)")
     @TableField("audit_pass_rate")
     private BigDecimal auditPassRate;
@@ -122,6 +186,10 @@ public class AnalyticsDailySummary {
     @TableField("yesterday_report_handle_rate")
     private BigDecimal yesterdayReportHandleRate;
 
+    @ApiModelProperty("当日举报处理率(%)")
+    @TableField("report_handle_rate")
+    private BigDecimal reportHandleRate;
+
     @ApiModelProperty("入驻商家总数")
     @TableField("total_settled_merchant_count")
     private Integer totalSettledMerchantCount;
@@ -133,4 +201,8 @@ public class AnalyticsDailySummary {
     @ApiModelProperty("核销率(%)")
     @TableField("verify_rate")
     private BigDecimal verifyRate;
+
+    @ApiModelProperty("商家评价率(%)")
+    @TableField("merchant_review_rate")
+    private BigDecimal merchantReviewRate;
 }

+ 12 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsEvent.java

@@ -35,6 +35,10 @@ public class AnalyticsEvent {
     @TableField("event_code")
     private String eventCode;
 
+    @ApiModelProperty("事件子类型(审核/注册漏斗等)")
+    @TableField("event_subtype")
+    private String eventSubtype;
+
     /** 用户ID */
     @ApiModelProperty("用户ID")
     @TableField("user_id")
@@ -55,6 +59,14 @@ public class AnalyticsEvent {
     @TableField("content_type")
     private Integer contentType;
 
+    @ApiModelProperty("经营品类(见 AnalyticsBusinessCategory)")
+    @TableField("business_category")
+    private Integer businessCategory;
+
+    @ApiModelProperty("商户店铺类型(冗余)")
+    @TableField("shop_type")
+    private Integer shopType;
+
     /** 金额 */
     @ApiModelProperty("金额")
     @TableField("amount")

+ 42 - 2
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsMerchantStat.java

@@ -13,12 +13,12 @@ import java.util.Date;
 /**
  * 商户明细统计实体。
  *
- * <p>表名:analytics_merchant_stat</p>
+ * <p>表名:analytics_merchant_stat_history</p>
  * <p>商家名称不入库;shopType 由浏览埋点上报入库,查询时可推导 shopTypeName</p>
  */
 @Data
 @JsonInclude
-@TableName("analytics_merchant_stat")
+@TableName("analytics_merchant_stat_history")
 @ApiModel(value = "AnalyticsMerchantStat", description = "商户明细统计")
 public class AnalyticsMerchantStat {
 
@@ -46,6 +46,46 @@ public class AnalyticsMerchantStat {
     @TableField("visit_pv")
     private Integer visitPv;
 
+    @ApiModelProperty("当日GMV")
+    @TableField("gmv")
+    private BigDecimal gmv;
+
+    @ApiModelProperty("支付笔数")
+    @TableField("pay_count")
+    private Integer payCount;
+
+    @ApiModelProperty("支付用户数")
+    @TableField("pay_user_count")
+    private Integer payUserCount;
+
+    @ApiModelProperty("核销次数")
+    @TableField("verify_count")
+    private Integer verifyCount;
+
+    @ApiModelProperty("曝光次数")
+    @TableField("expose_count")
+    private Integer exposeCount;
+
+    @ApiModelProperty("点击次数")
+    @TableField("click_count")
+    private Integer clickCount;
+
+    @ApiModelProperty("详情浏览次数")
+    @TableField("detail_view_count")
+    private Integer detailViewCount;
+
+    @ApiModelProperty("电话/导航次数")
+    @TableField("contact_count")
+    private Integer contactCount;
+
+    @ApiModelProperty("评价数")
+    @TableField("review_count")
+    private Integer reviewCount;
+
+    @ApiModelProperty("评价率(%)")
+    @TableField("review_rate")
+    private BigDecimal reviewRate;
+
     @ApiModelProperty("核销转化率(%)")
     @TableField("verify_conversion_rate")
     private BigDecimal verifyConversionRate;

+ 70 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsMerchantStatToday.java

@@ -0,0 +1,70 @@
+package shop.alien.entity.analytics;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@JsonInclude
+@TableName("analytics_merchant_stat_today")
+public class AnalyticsMerchantStatToday {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("merchant_id")
+    private Long merchantId;
+
+    @TableField("shop_type")
+    private Integer shopType;
+
+    @TableField("visit_uv")
+    private Integer visitUv;
+
+    @TableField("visit_pv")
+    private Integer visitPv;
+
+    @TableField("gmv")
+    private BigDecimal gmv;
+
+    @TableField("pay_count")
+    private Integer payCount;
+
+    @TableField("pay_user_count")
+    private Integer payUserCount;
+
+    @TableField("verify_count")
+    private Integer verifyCount;
+
+    @TableField("expose_count")
+    private Integer exposeCount;
+
+    @TableField("click_count")
+    private Integer clickCount;
+
+    @TableField("detail_view_count")
+    private Integer detailViewCount;
+
+    @TableField("contact_count")
+    private Integer contactCount;
+
+    @TableField("review_count")
+    private Integer reviewCount;
+
+    @TableField("review_rate")
+    private BigDecimal reviewRate;
+
+    @TableField("verify_conversion_rate")
+    private BigDecimal verifyConversionRate;
+
+    @TableField("settle_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date settleTime;
+
+    @TableField("settle_status")
+    private Integer settleStatus;
+}

+ 69 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsReportRecord.java

@@ -0,0 +1,69 @@
+package shop.alien.entity.analytics;
+
+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;
+
+/**
+ * 举报处理明细(内容报表列表)。
+ */
+@Data
+@JsonInclude
+@TableName("analytics_report_record_today")
+@ApiModel(value = "AnalyticsReportRecord", description = "举报处理明细")
+public class AnalyticsReportRecord {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("举报单号")
+    @TableField("report_id")
+    private String reportId;
+
+    @ApiModelProperty("内容ID")
+    @TableField("content_id")
+    private Long contentId;
+
+    @ApiModelProperty("内容类型")
+    @TableField("content_type")
+    private Integer contentType;
+
+    @ApiModelProperty("内容标题快照")
+    @TableField("content_title")
+    private String contentTitle;
+
+    @ApiModelProperty("举报类型")
+    @TableField("report_type")
+    private String reportType;
+
+    @ApiModelProperty("状态(0处理中1已处理)")
+    @TableField("status")
+    private Integer status;
+
+    @ApiModelProperty("举报人用户ID")
+    @TableField("reporter_user_id")
+    private Long reporterUserId;
+
+    @ApiModelProperty("举报时间")
+    @TableField("report_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date reportTime;
+
+    @ApiModelProperty("处理时间")
+    @TableField("handle_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date handleTime;
+
+    @ApiModelProperty("处理人ID")
+    @TableField("handle_user_id")
+    private Long handleUserId;
+
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+}

+ 57 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsReportRecordHistory.java

@@ -0,0 +1,57 @@
+package shop.alien.entity.analytics;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@JsonInclude
+@TableName("analytics_report_record_history")
+public class AnalyticsReportRecordHistory {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("stat_date")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date statDate;
+
+    @TableField("report_id")
+    private String reportId;
+
+    @TableField("content_id")
+    private Long contentId;
+
+    @TableField("content_type")
+    private Integer contentType;
+
+    @TableField("content_title")
+    private String contentTitle;
+
+    @TableField("report_type")
+    private String reportType;
+
+    @TableField("status")
+    private Integer status;
+
+    @TableField("reporter_user_id")
+    private Long reporterUserId;
+
+    @TableField("report_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date reportTime;
+
+    @TableField("handle_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date handleTime;
+
+    @TableField("handle_user_id")
+    private Long handleUserId;
+
+    @TableField(value = "created_time", fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+}

+ 22 - 2
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsUserStat.java

@@ -12,12 +12,12 @@ import java.util.Date;
 /**
  * 用户明细统计实体。
  *
- * <p>表名:analytics_user_stat</p>
+ * <p>表名:analytics_user_stat_history</p>
  * <p>用户名称不入库,查询时按 userId 回填</p>
  */
 @Data
 @JsonInclude
-@TableName("analytics_user_stat")
+@TableName("analytics_user_stat_history")
 @ApiModel(value = "AnalyticsUserStat", description = "用户明细统计")
 public class AnalyticsUserStat {
 
@@ -47,6 +47,10 @@ public class AnalyticsUserStat {
     @TableField("city")
     private String city;
 
+    @ApiModelProperty("省份")
+    @TableField("province")
+    private String province;
+
     @ApiModelProperty("设备")
     @TableField("device_type")
     private String deviceType;
@@ -55,6 +59,18 @@ public class AnalyticsUserStat {
     @TableField("user_phone")
     private String userPhone;
 
+    @ApiModelProperty("性别(0未知1男2女)")
+    @TableField("gender")
+    private Integer gender;
+
+    @ApiModelProperty("年龄")
+    @TableField("age")
+    private Integer age;
+
+    @ApiModelProperty("年龄段")
+    @TableField("age_group")
+    private String ageGroup;
+
     @ApiModelProperty("注册时间")
     @TableField("register_time")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@@ -64,6 +80,10 @@ public class AnalyticsUserStat {
     @TableField("channel")
     private String channel;
 
+    @ApiModelProperty("是否新用户(7天内注册)")
+    @TableField("is_new_user")
+    private Integer isNewUser;
+
     @ApiModelProperty("在线时长(分)")
     @TableField("online_duration_min")
     private Integer onlineDurationMin;

+ 66 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsUserStatToday.java

@@ -0,0 +1,66 @@
+package shop.alien.entity.analytics;
+
+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;
+
+/** 用户明细-今日表(零点归档至 history) */
+@Data
+@JsonInclude
+@TableName("analytics_user_stat_today")
+@ApiModel("AnalyticsUserStatToday")
+public class AnalyticsUserStatToday {
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @TableField("user_id")
+    private Long userId;
+
+    @TableField("first_launch_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date firstLaunchTime;
+
+    @TableField("last_active_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastActiveTime;
+
+    @TableField("city")
+    private String city;
+
+    @TableField("province")
+    private String province;
+
+    @TableField("device_type")
+    private String deviceType;
+
+    @TableField("user_phone")
+    private String userPhone;
+
+    @TableField("gender")
+    private Integer gender;
+
+    @TableField("age")
+    private Integer age;
+
+    @TableField("age_group")
+    private String ageGroup;
+
+    @TableField("register_time")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date registerTime;
+
+    @TableField("channel")
+    private String channel;
+
+    @TableField("is_new_user")
+    private Integer isNewUser;
+
+    @TableField("online_duration_min")
+    private Integer onlineDurationMin;
+}

+ 7 - 1
alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsContentPublishDTO.java

@@ -11,7 +11,7 @@ import java.util.Date;
  * 内容发布埋点上报。
  *
  * <p>接口:POST /analytics/front/content/publish</p>
- * <p>落库:analytics_event、analytics_content_stat</p>
+ * <p>落库:analytics_event、analytics_content_stat_today</p>
  */
 @Data
 @ApiModel("内容发布埋点上报")
@@ -33,6 +33,12 @@ public class AnalyticsContentPublishDTO {
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date publishTime;
 
+    @ApiModelProperty("内容标题")
+    private String contentTitle;
+
+    @ApiModelProperty("经营品类(见 AnalyticsBusinessCategory)")
+    private Integer businessCategory;
+
     @ApiModelProperty("事件唯一ID(幂等,可选)")
     private String eventId;
 }

+ 1 - 1
alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsHeartbeatDTO.java

@@ -8,7 +8,7 @@ import lombok.Data;
  * 在线心跳上报。
  *
  * <p>接口:POST /analytics/front/heartbeat</p>
- * <p>落库:analytics_event(事件码 user.heartbeat)</p>
+ * <p>落库:analytics_event(事件码 user.heartbeat)、analytics_user_stat_today</p>
  */
 @Data
 @ApiModel("在线心跳")

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsTrackEventDTO.java

@@ -35,6 +35,9 @@ public class AnalyticsTrackEventDTO {
     @ApiModelProperty("内容分类(1/2/3)")
     private Integer contentType;
 
+    @ApiModelProperty("经营品类")
+    private Integer businessCategory;
+
     @ApiModelProperty("金额")
     private BigDecimal amount;
 

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/AnalyticsContentStatVo.java

@@ -19,6 +19,9 @@ public class AnalyticsContentStatVo extends AnalyticsContentStat {
     @ApiModelProperty("内容分类名称(按contentType推导)")
     private String contentTypeName;
 
+    @ApiModelProperty("经营品类名称(按businessCategory推导)")
+    private String businessCategoryName;
+
     @ApiModelProperty("作者名称(按authorType+authorId查询)")
     private String authorName;
 

+ 3 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/AnalyticsUserStatVo.java

@@ -18,4 +18,7 @@ public class AnalyticsUserStatVo extends AnalyticsUserStat {
 
     @ApiModelProperty("用户名称(按userId查询)")
     private String userName;
+
+    @ApiModelProperty("脱敏手机号")
+    private String maskedPhone;
 }

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsAiChatStatHistoryMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsAiChatStatHistory;
+
+@Mapper
+public interface AnalyticsAiChatStatHistoryMapper extends BaseMapper<AnalyticsAiChatStatHistory> {
+}

+ 46 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsArchiveMapper.java

@@ -0,0 +1,46 @@
+package shop.alien.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.Date;
+
+@Mapper
+public interface AnalyticsArchiveMapper {
+
+    int archiveUserStat(@Param("archiveDate") Date archiveDate);
+
+    int archiveMerchantStat(@Param("archiveDate") Date archiveDate);
+
+    int archiveContentStat(@Param("archiveDate") Date archiveDate);
+
+    int archiveCategoryDaily(@Param("archiveDate") Date archiveDate);
+
+    int archiveAiChatStat(@Param("archiveDate") Date archiveDate);
+
+    int archiveReportRecord(@Param("archiveDate") Date archiveDate);
+
+    int purgeUserStatHistory(@Param("beforeDate") Date beforeDate);
+
+    int purgeMerchantStatHistory(@Param("beforeDate") Date beforeDate);
+
+    int purgeContentStatHistory(@Param("beforeDate") Date beforeDate);
+
+    int purgeCategoryDailyHistory(@Param("beforeDate") Date beforeDate);
+
+    int purgeAiChatStatHistory(@Param("beforeDate") Date beforeDate);
+
+    int purgeReportRecordHistory(@Param("beforeDate") Date beforeDate);
+
+    int truncateUserStatToday();
+
+    int truncateMerchantStatToday();
+
+    int truncateContentStatToday();
+
+    int truncateCategoryDailyToday();
+
+    int truncateAiChatStatToday();
+
+    int truncateReportRecordToday();
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsCategoryDailyMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsCategoryDaily;
+
+@Mapper
+public interface AnalyticsCategoryDailyMapper extends BaseMapper<AnalyticsCategoryDaily> {
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsCategoryDailyTodayMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsCategoryDailyToday;
+
+@Mapper
+public interface AnalyticsCategoryDailyTodayMapper extends BaseMapper<AnalyticsCategoryDailyToday> {
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsContentStatHistoryMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsContentStatHistory;
+
+@Mapper
+public interface AnalyticsContentStatHistoryMapper extends BaseMapper<AnalyticsContentStatHistory> {
+}

+ 6 - 2
alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantStatMapper.java

@@ -11,7 +11,11 @@ import java.util.Date;
 @Mapper
 public interface AnalyticsMerchantStatMapper extends BaseMapper<AnalyticsMerchantStat> {
 
-    @Select("SELECT COUNT(DISTINCT merchant_id) FROM analytics_merchant_stat " +
-            "WHERE stat_date <= #{statDate} AND settle_status = 1")
+    @Select("SELECT COUNT(DISTINCT merchant_id) FROM (" +
+            "SELECT merchant_id FROM analytics_merchant_stat_history " +
+            "WHERE stat_date <= #{statDate} AND settle_status = 1 " +
+            "UNION ALL " +
+            "SELECT merchant_id FROM analytics_merchant_stat_today WHERE settle_status = 1" +
+            ") t")
     Long countSettledMerchants(@Param("statDate") Date statDate);
 }

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantStatTodayMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsMerchantStatToday;
+
+@Mapper
+public interface AnalyticsMerchantStatTodayMapper extends BaseMapper<AnalyticsMerchantStatToday> {
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsReportRecordHistoryMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsReportRecordHistory;
+
+@Mapper
+public interface AnalyticsReportRecordHistoryMapper extends BaseMapper<AnalyticsReportRecordHistory> {
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsReportRecordMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsReportRecord;
+
+@Mapper
+public interface AnalyticsReportRecordMapper extends BaseMapper<AnalyticsReportRecord> {
+}

+ 9 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsUserStatTodayMapper.java

@@ -0,0 +1,9 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import shop.alien.entity.analytics.AnalyticsUserStatToday;
+
+@Mapper
+public interface AnalyticsUserStatTodayMapper extends BaseMapper<AnalyticsUserStatToday> {
+}

+ 140 - 105
alien-entity/src/main/resources/db/migration/analytics_tables.sql

@@ -1,45 +1,39 @@
 -- =============================================================================
--- 平台埋点统计系统 - 最终建表脚本
+-- 平台埋点统计系统 - 建表脚本 v2(含四页看板字段)
 -- =============================================================================
--- 说明:
---   1. 表尚未创建时,直接执行本文件即可
---   2. 字段与业务需求清单严格对齐,明细表 + 日汇总表 + 内部事件表
---   3. 明细由 /analytics/detail/* 同步,日汇总由定时任务或 /analytics/stat/calculate 生成
+-- 新环境:直接执行本文件
+-- 旧环境:执行 analytics_tables.sql 后再执行 analytics_tables_dashboard_upgrade.sql
 -- =============================================================================
 
 SET NAMES utf8mb4;
 
--- -----------------------------------------------------------------------------
--- 1. analytics_event  行为事件表(内部汇总用,非业务展示)
---    前端 /analytics/front/* 上报写入,供 DAU/留存/核销率等聚合计算
--- -----------------------------------------------------------------------------
 CREATE TABLE IF NOT EXISTS `analytics_event` (
-  `id`           bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
-  `event_id`     varchar(64)   NOT NULL COMMENT '事件唯一ID(幂等)',
-  `event_code`   varchar(64)   NOT NULL COMMENT '事件编码,如 user.launch / merchant.view',
-  `user_id`      bigint        DEFAULT NULL COMMENT '用户ID',
-  `merchant_id`  bigint        DEFAULT NULL COMMENT '商户ID',
-  `target_id`    bigint        DEFAULT NULL COMMENT '目标ID(内容ID/订单ID等)',
-  `content_type` tinyint       DEFAULT NULL COMMENT '内容分类(1动态2打卡3二手商品)',
-  `amount`       decimal(14,2) DEFAULT NULL COMMENT '金额(元)',
-  `duration_ms`  bigint        DEFAULT NULL COMMENT '时长(毫秒)',
-  `device_type`  varchar(16)   DEFAULT NULL COMMENT '设备',
-  `channel`      varchar(64)   DEFAULT NULL COMMENT '渠道',
-  `city`         varchar(64)   DEFAULT NULL COMMENT '城市',
-  `event_time`   datetime(3)   NOT NULL COMMENT '事件时间',
-  `created_time` datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间',
+  `id`                bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `event_id`          varchar(64)   NOT NULL COMMENT '事件唯一ID(幂等)',
+  `event_code`        varchar(64)   NOT NULL COMMENT '事件编码',
+  `event_subtype`     varchar(32)   DEFAULT NULL COMMENT '事件子类型(审核ai_pass/manual_pass/reject等)',
+  `user_id`           bigint        DEFAULT NULL COMMENT '用户ID',
+  `merchant_id`       bigint        DEFAULT NULL COMMENT '商户ID',
+  `target_id`         bigint        DEFAULT NULL COMMENT '目标ID',
+  `content_type`      tinyint       DEFAULT NULL COMMENT '内容形态(1动态2打卡3二手商品)',
+  `business_category` tinyint       DEFAULT NULL COMMENT '经营品类(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)',
+  `shop_type`         tinyint       DEFAULT NULL COMMENT '商户类型(冗余,便于聚合)',
+  `amount`            decimal(14,2) DEFAULT NULL COMMENT '金额(元)',
+  `duration_ms`       bigint        DEFAULT NULL COMMENT '时长(毫秒)',
+  `device_type`       varchar(16)   DEFAULT NULL COMMENT '设备',
+  `channel`           varchar(64)   DEFAULT NULL COMMENT '渠道',
+  `city`              varchar(64)   DEFAULT NULL COMMENT '城市',
+  `event_time`        datetime(3)   NOT NULL COMMENT '事件时间',
+  `created_time`      datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间',
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_event_id` (`event_id`),
   KEY `idx_event_code_time` (`event_code`, `event_time`),
+  KEY `idx_event_code_subtype_time` (`event_code`, `event_subtype`, `event_time`),
   KEY `idx_user_time` (`user_id`, `event_time`),
-  KEY `idx_merchant_time` (`merchant_id`, `event_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='埋点事件表(汇总用)';
+  KEY `idx_merchant_time` (`merchant_id`, `event_time`),
+  KEY `idx_business_category_time` (`business_category`, `event_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='埋点事件表';
 
--- -----------------------------------------------------------------------------
--- 2. analytics_user_stat  用户明细统计
---    字段:用户ID / 首次启动时间 / 最后活跃时间 / 城市 / 设备 /
---          手机号 / 注册时间 / 渠道 / 在线时长(分)(用户名称按userId查询)
--- -----------------------------------------------------------------------------
 CREATE TABLE IF NOT EXISTS `analytics_user_stat` (
   `id`                  bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
   `stat_date`           date         NOT NULL COMMENT '统计日期',
@@ -47,66 +41,79 @@ CREATE TABLE IF NOT EXISTS `analytics_user_stat` (
   `first_launch_time`   datetime     DEFAULT NULL COMMENT '首次启动时间',
   `last_active_time`    datetime     DEFAULT NULL COMMENT '最后活跃时间',
   `city`                varchar(64)  DEFAULT NULL COMMENT '城市',
+  `province`            varchar(64)  DEFAULT NULL COMMENT '省份',
   `device_type`         varchar(16)  DEFAULT NULL COMMENT '设备',
   `user_phone`          varchar(20)  DEFAULT NULL COMMENT '手机号',
+  `gender`              tinyint      DEFAULT NULL COMMENT '性别(0未知1男2女)',
+  `age`                 int          DEFAULT NULL COMMENT '年龄',
+  `age_group`           varchar(16)  DEFAULT NULL COMMENT '年龄段',
   `register_time`       datetime     DEFAULT NULL COMMENT '注册时间',
   `channel`             varchar(64)  DEFAULT NULL COMMENT '渠道',
+  `is_new_user`         tinyint      NOT NULL DEFAULT 0 COMMENT '是否新用户(7天内注册)',
   `online_duration_min` int          NOT NULL DEFAULT 0 COMMENT '在线时长(分)',
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_date_user` (`stat_date`, `user_id`),
   KEY `idx_user_id` (`user_id`),
-  KEY `idx_last_active` (`last_active_time`)
+  KEY `idx_last_active` (`last_active_time`),
+  KEY `idx_gender` (`gender`),
+  KEY `idx_age_group` (`age_group`),
+  KEY `idx_city` (`city`),
+  KEY `idx_register_time` (`register_time`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户明细统计';
 
--- -----------------------------------------------------------------------------
--- 3. analytics_merchant_stat  商户明细统计
---    字段:商家ID / 商家店铺类型 / 访问UV / 访问PV / 核销转化率 / 商家入驻时间 / 商家入驻状态
---          (商家名称按merchantId查询)
--- -----------------------------------------------------------------------------
 CREATE TABLE IF NOT EXISTS `analytics_merchant_stat` (
   `id`                     bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
   `stat_date`              date          NOT NULL COMMENT '统计日期',
   `merchant_id`            bigint        NOT NULL COMMENT '商家ID',
-  `shop_type`              tinyint       DEFAULT NULL COMMENT '商家店铺类型(1美食2休闲娱乐3生活服务)',
+  `shop_type`              tinyint       DEFAULT NULL COMMENT '店铺类型(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)',
   `visit_uv`               int           NOT NULL DEFAULT 0 COMMENT '访问UV',
   `visit_pv`               int           NOT NULL DEFAULT 0 COMMENT '访问PV',
+  `gmv`                    decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '当日GMV',
+  `pay_count`              int           NOT NULL DEFAULT 0 COMMENT '支付笔数',
+  `pay_user_count`         int           NOT NULL DEFAULT 0 COMMENT '支付用户数',
+  `verify_count`           int           NOT NULL DEFAULT 0 COMMENT '核销次数',
+  `expose_count`           int           NOT NULL DEFAULT 0 COMMENT '曝光次数',
+  `click_count`            int           NOT NULL DEFAULT 0 COMMENT '点击次数',
+  `detail_view_count`      int           NOT NULL DEFAULT 0 COMMENT '详情浏览次数',
+  `contact_count`          int           NOT NULL DEFAULT 0 COMMENT '电话/导航次数',
+  `review_count`           int           NOT NULL DEFAULT 0 COMMENT '评价数',
+  `review_rate`            decimal(10,4) DEFAULT NULL COMMENT '评价率(%)',
   `verify_conversion_rate` decimal(10,4) DEFAULT NULL COMMENT '核销转化率(%)',
-  `settle_time`            datetime      DEFAULT NULL COMMENT '商家入驻时间',
-  `settle_status`          tinyint       DEFAULT NULL COMMENT '商家入驻状态(0待审核1已入驻2驳回3退出)',
+  `settle_time`            datetime      DEFAULT NULL COMMENT '入驻时间',
+  `settle_status`          tinyint       DEFAULT NULL COMMENT '入驻状态',
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_date_merchant` (`stat_date`, `merchant_id`),
   KEY `idx_merchant_id` (`merchant_id`),
   KEY `idx_shop_type` (`shop_type`),
-  KEY `idx_settle_status` (`settle_status`, `stat_date`)
+  KEY `idx_gmv` (`stat_date`, `gmv`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商户明细统计';
 
--- -----------------------------------------------------------------------------
--- 4. analytics_content_stat  内容明细统计
---    字段:内容ID / 内容分类 / 作者类型 / 作者ID / 发布时间 / 互动数 /
---          状态 / 审核人ID / 审核状态 / 审核时间(名称类字段按ID查询或推导)
--- -----------------------------------------------------------------------------
 CREATE TABLE IF NOT EXISTS `analytics_content_stat` (
   `id`                bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
   `content_id`        bigint       NOT NULL COMMENT '内容ID',
-  `content_type`      tinyint      NOT NULL COMMENT '内容分类(1动态2打卡3二手商品)',
+  `content_type`      tinyint      NOT NULL COMMENT '内容形态(1动态2打卡3二手)',
+  `business_category` tinyint      DEFAULT NULL COMMENT '经营品类',
   `author_type`       tinyint      NOT NULL COMMENT '作者类型(1用户2商家)',
   `author_id`         bigint       NOT NULL COMMENT '作者ID',
+  `content_title`     varchar(256) DEFAULT NULL COMMENT '标题快照',
   `publish_time`      datetime     DEFAULT NULL COMMENT '发布时间',
-  `interaction_count` int          NOT NULL DEFAULT 0 COMMENT '互动数',
+  `interaction_count` int          NOT NULL DEFAULT 0 COMMENT '互动总数',
+  `like_count`        int          NOT NULL DEFAULT 0 COMMENT '点赞数',
+  `comment_count`     int          NOT NULL DEFAULT 0 COMMENT '评论数',
+  `share_count`       int          NOT NULL DEFAULT 0 COMMENT '分享数',
   `status`            tinyint      DEFAULT NULL COMMENT '状态',
   `audit_user_id`     bigint       DEFAULT NULL COMMENT '审核人ID',
-  `audit_status`      tinyint      DEFAULT NULL COMMENT '审核状态(0审核中1通过2驳回)',
+  `audit_status`      tinyint      DEFAULT NULL COMMENT '审核状态',
+  `audit_type`        tinyint      DEFAULT NULL COMMENT '审核方式(1AI2人工)',
   `audit_time`        datetime     DEFAULT NULL COMMENT '审核时间',
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_content` (`content_type`, `content_id`),
   KEY `idx_author` (`author_type`, `author_id`),
+  KEY `idx_business_category` (`business_category`),
+  KEY `idx_interaction` (`interaction_count`),
   KEY `idx_publish_time` (`publish_time`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='内容明细统计';
 
--- -----------------------------------------------------------------------------
--- 5. analytics_ai_chat_stat  AI对话明细统计
---    字段:对话ID / 用户ID / 开始时间 / 消息数 / AI响应时长(ms)
--- -----------------------------------------------------------------------------
 CREATE TABLE IF NOT EXISTS `analytics_ai_chat_stat` (
   `id`                      bigint      NOT NULL AUTO_INCREMENT COMMENT '主键',
   `chat_id`                 varchar(64) NOT NULL COMMENT '对话ID',
@@ -118,78 +125,113 @@ CREATE TABLE IF NOT EXISTS `analytics_ai_chat_stat` (
   UNIQUE KEY `uk_chat_id` (`chat_id`),
   KEY `idx_user_id` (`user_id`),
   KEY `idx_start_time` (`start_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI对话明细统计';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI对话明细';
 
--- -----------------------------------------------------------------------------
--- 6. analytics_ai_request  AI请求明细统计
---    字段:AI接口名称 / AI接口地址 / 响应时长(ms) / 是否超时
--- -----------------------------------------------------------------------------
 CREATE TABLE IF NOT EXISTS `analytics_ai_request` (
   `id`                   bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
-  `request_id`           varchar(64)  NOT NULL COMMENT '请求唯一ID(幂等)',
+  `request_id`           varchar(64)  NOT NULL COMMENT '请求唯一ID',
   `api_name`             varchar(128) NOT NULL COMMENT 'AI接口名称',
   `api_url`              varchar(512) NOT NULL COMMENT 'AI接口地址',
   `response_duration_ms` bigint       NOT NULL DEFAULT 0 COMMENT '响应时长(ms)',
-  `is_timeout`           tinyint      NOT NULL DEFAULT 0 COMMENT '是否超时(0否1是)',
-  `created_time`         datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间(日汇总筛选用)',
+  `is_timeout`           tinyint      NOT NULL DEFAULT 0 COMMENT '是否超时',
+  `created_time`         datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间',
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_request_id` (`request_id`),
   KEY `idx_api_name` (`api_name`),
   KEY `idx_created_time` (`created_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI请求明细统计';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI请求明细';
 
--- -----------------------------------------------------------------------------
--- 7. analytics_daily_summary  通用日统计主表
---    字段:日期 / DAU / 新增用户数 / 今日AI对话次数 / 今日内容发布数量 /
---          商家访问UV / AI响应时间总数 / 当前在线总人数 / 支付用户数 / 转化率 /
---          客单价 / 今日转化率 / 累计注册用户 / 近7日新用户 / 近7日活跃用户 /
---          次日留存数 / 次日留存率 / 审核通过次数 / 审核提交次数 / 审核通过率 /
---          今日内容互动数 / 举报处理次数 / 举报提交次数 / 昨日举报处理率 /
---          入驻商家总数 / 今日GMV / 核销率
--- -----------------------------------------------------------------------------
 CREATE TABLE IF NOT EXISTS `analytics_daily_summary` (
   `id`                            bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
   `stat_date`                     date          NOT NULL COMMENT '日期',
   `dau`                           int           NOT NULL DEFAULT 0 COMMENT 'DAU',
-  `new_user_count`                int           NOT NULL DEFAULT 0 COMMENT '新增用户数',
-  `ai_chat_count`                 int           NOT NULL DEFAULT 0 COMMENT '今日AI对话次数',
-  `content_publish_count`         int           NOT NULL DEFAULT 0 COMMENT '今日内容发布数量',
-  `merchant_visit_uv`             int           NOT NULL DEFAULT 0 COMMENT '商家访问UV数量',
-  `ai_response_duration_total_ms` bigint        NOT NULL DEFAULT 0 COMMENT 'AI响应时间总数(ms)',
-  `online_user_count`             int           NOT NULL DEFAULT 0 COMMENT '当前在线总人数',
+  `mau`                           int           NOT NULL DEFAULT 0 COMMENT 'MAU(近30日快照)',
+  `new_user_count`                int           NOT NULL DEFAULT 0 COMMENT '新增用户',
+  `register_page_view_count`      int           NOT NULL DEFAULT 0 COMMENT '注册漏斗-进入注册页',
+  `register_phone_submit_count`   int           NOT NULL DEFAULT 0 COMMENT '注册漏斗-提交手机号',
+  `register_otp_pass_count`       int           NOT NULL DEFAULT 0 COMMENT '注册漏斗-验证码通过',
+  `register_password_set_count`   int           NOT NULL DEFAULT 0 COMMENT '注册漏斗-设置密码',
+  `register_success_count`        int           NOT NULL DEFAULT 0 COMMENT '注册漏斗-注册成功',
+  `ai_chat_count`                 int           NOT NULL DEFAULT 0 COMMENT 'AI对话次数',
+  `ai_chat_user_count`            int           NOT NULL DEFAULT 0 COMMENT '对话用户数',
+  `content_publish_count`         int           NOT NULL DEFAULT 0 COMMENT '内容发布数',
+  `merchant_visit_uv`             int           NOT NULL DEFAULT 0 COMMENT '商家访问UV',
+  `merchant_expose_count`         bigint        NOT NULL DEFAULT 0 COMMENT '商家曝光',
+  `merchant_click_count`          bigint        NOT NULL DEFAULT 0 COMMENT '商家点击',
+  `merchant_detail_view_count`    bigint        NOT NULL DEFAULT 0 COMMENT '商家详情浏览',
+  `merchant_contact_count`        bigint        NOT NULL DEFAULT 0 COMMENT '电话/导航',
+  `ai_response_duration_total_ms` bigint        NOT NULL DEFAULT 0 COMMENT 'AI响应总时长',
+  `ai_request_count`              int           NOT NULL DEFAULT 0 COMMENT 'AI请求次数',
+  `online_user_count`             int           NOT NULL DEFAULT 0 COMMENT '在线人数',
   `pay_user_count`                int           NOT NULL DEFAULT 0 COMMENT '支付用户数',
   `conversion_rate`               decimal(10,4) DEFAULT NULL COMMENT '转化率(%)',
-  `avg_order_amount`              decimal(10,2) DEFAULT NULL COMMENT '客单价(元)',
-  `today_conversion_rate`         decimal(10,4) DEFAULT NULL COMMENT '今日转化率(支付成功数/DAU*100%)',
-  `total_register_user_count`     int           NOT NULL DEFAULT 0 COMMENT '累计注册用户数量',
-  `last_7d_new_user_count`        int           NOT NULL DEFAULT 0 COMMENT '近7日新用户数量',
-  `last_7d_active_user_count`     int           NOT NULL DEFAULT 0 COMMENT '近7日活跃用户数量',
-  `next_day_retained_count`       int           NOT NULL DEFAULT 0 COMMENT '首日注册用户次日有日活数',
-  `next_day_retention_rate`       decimal(10,4) DEFAULT NULL COMMENT '次日留存率(%)',
-  `audit_pass_count`              int           NOT NULL DEFAULT 0 COMMENT '审核通过次数',
-  `audit_submit_count`            int           NOT NULL DEFAULT 0 COMMENT '审核提交次数',
-  `audit_pass_rate`               decimal(10,4) DEFAULT NULL COMMENT '审核通过率(%)',
-  `content_interaction_count`     int           NOT NULL DEFAULT 0 COMMENT '今日内容互动数',
-  `report_handle_count`           int           NOT NULL DEFAULT 0 COMMENT '举报处理次数',
-  `report_submit_count`           int           NOT NULL DEFAULT 0 COMMENT '举报提交次数',
-  `yesterday_report_handle_rate`  decimal(10,4) DEFAULT NULL COMMENT '昨日举报处理率(%)',
+  `avg_order_amount`              decimal(10,2) DEFAULT NULL COMMENT '客单价',
+  `today_conversion_rate`         decimal(10,4) DEFAULT NULL COMMENT '今日转化率',
+  `total_register_user_count`     int           NOT NULL DEFAULT 0 COMMENT '累计注册用户',
+  `last_7d_new_user_count`        int           NOT NULL DEFAULT 0 COMMENT '近7日新用户',
+  `last_7d_active_user_count`     int           NOT NULL DEFAULT 0 COMMENT '近7日活跃用户',
+  `last_30d_active_user_count`    int           NOT NULL DEFAULT 0 COMMENT '近30日活跃用户',
+  `next_day_retained_count`       int           NOT NULL DEFAULT 0 COMMENT '次日留存数',
+  `next_day_retention_rate`       decimal(10,4) DEFAULT NULL COMMENT '次日留存率',
+  `audit_pass_count`              int           NOT NULL DEFAULT 0 COMMENT '审核通过总数',
+  `audit_submit_count`            int           NOT NULL DEFAULT 0 COMMENT '审核提交数',
+  `audit_ai_pass_count`           int           NOT NULL DEFAULT 0 COMMENT 'AI审核通过',
+  `audit_manual_pass_count`       int           NOT NULL DEFAULT 0 COMMENT '人工审核通过',
+  `audit_reject_count`            int           NOT NULL DEFAULT 0 COMMENT '审核驳回',
+  `audit_pass_rate`               decimal(10,4) DEFAULT NULL COMMENT '审核通过率',
+  `content_interaction_count`     int           NOT NULL DEFAULT 0 COMMENT '内容互动数',
+  `report_handle_count`           int           NOT NULL DEFAULT 0 COMMENT '举报处理数',
+  `report_submit_count`           int           NOT NULL DEFAULT 0 COMMENT '举报提交数',
+  `yesterday_report_handle_rate`  decimal(10,4) DEFAULT NULL COMMENT '昨日举报处理率',
+  `report_handle_rate`            decimal(10,4) DEFAULT NULL COMMENT '当日举报处理率',
   `total_settled_merchant_count`  int           NOT NULL DEFAULT 0 COMMENT '入驻商家总数',
-  `today_gmv`                     decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '今日GMV',
-  `verify_rate`                   decimal(10,4) DEFAULT NULL COMMENT '核销率(%)',
+  `today_gmv`                     decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT 'GMV',
+  `verify_rate`                   decimal(10,4) DEFAULT NULL COMMENT '核销率',
+  `merchant_review_rate`          decimal(10,4) DEFAULT NULL COMMENT '商家评价率',
   PRIMARY KEY (`id`),
   UNIQUE KEY `uk_stat_date` (`stat_date`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台日统计主表';
 
--- -----------------------------------------------------------------------------
--- 8. analytics_stat_job_log  统计任务日志(运维辅助,非业务展示字段)
--- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_category_daily` (
+  `id`                        bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `stat_date`                 date          NOT NULL COMMENT '统计日期',
+  `business_category`         tinyint       NOT NULL COMMENT '经营品类',
+  `gmv`                       decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '品类GMV',
+  `pay_count`                 int           NOT NULL DEFAULT 0 COMMENT '支付笔数',
+  `merchant_visit_uv`         int           NOT NULL DEFAULT 0 COMMENT '访问UV',
+  `content_publish_count`     int           NOT NULL DEFAULT 0 COMMENT '内容发布数',
+  `content_interaction_count` int           NOT NULL DEFAULT 0 COMMENT '内容互动数',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_category` (`stat_date`, `business_category`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='经营品类日统计';
+
+CREATE TABLE IF NOT EXISTS `analytics_report_record` (
+  `id`               bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `report_id`        varchar(64)  NOT NULL COMMENT '举报单号',
+  `content_id`       bigint       DEFAULT NULL COMMENT '内容ID',
+  `content_type`     tinyint      DEFAULT NULL COMMENT '内容类型',
+  `content_title`    varchar(256) DEFAULT NULL COMMENT '内容标题',
+  `report_type`      varchar(32)  NOT NULL COMMENT '举报类型',
+  `status`           tinyint      NOT NULL DEFAULT 0 COMMENT '0处理中1已处理',
+  `reporter_user_id` bigint       DEFAULT NULL COMMENT '举报人',
+  `report_time`      datetime     NOT NULL COMMENT '举报时间',
+  `handle_time`      datetime     DEFAULT NULL COMMENT '处理时间',
+  `handle_user_id`   bigint       DEFAULT NULL COMMENT '处理人',
+  `created_time`     datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_report_id` (`report_id`),
+  KEY `idx_report_time` (`report_time`),
+  KEY `idx_status_time` (`status`, `report_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='举报处理明细';
+
 CREATE TABLE IF NOT EXISTS `analytics_stat_job_log` (
   `id`           bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
   `job_id`       varchar(64)   NOT NULL COMMENT '任务ID',
   `stat_date`    date          NOT NULL COMMENT '统计日期',
-  `scope`        varchar(32)   NOT NULL COMMENT '统计范围(ALL/USER/MERCHANT/CONTENT/AI_CHAT/DAILY)',
-  `trigger_type` varchar(16)   NOT NULL COMMENT '触发方式(SCHEDULE/MANUAL)',
-  `status`       tinyint       NOT NULL DEFAULT 0 COMMENT '状态(0进行中1成功2失败)',
+  `scope`        varchar(32)   NOT NULL COMMENT '统计范围',
+  `trigger_type` varchar(16)   NOT NULL COMMENT '触发方式',
+  `status`       tinyint       NOT NULL DEFAULT 0 COMMENT '状态',
   `start_time`   datetime      NOT NULL COMMENT '开始时间',
   `end_time`     datetime      DEFAULT NULL COMMENT '结束时间',
   `error_msg`    varchar(1000) DEFAULT NULL COMMENT '错误信息',
@@ -198,10 +240,3 @@ CREATE TABLE IF NOT EXISTS `analytics_stat_job_log` (
   KEY `idx_stat_date` (`stat_date`),
   KEY `idx_job_id` (`job_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统计任务日志';
-
--- -----------------------------------------------------------------------------
--- 增量变更(表已存在时按需执行)
--- -----------------------------------------------------------------------------
--- ALTER TABLE `analytics_merchant_stat`
---   ADD COLUMN `shop_type` tinyint DEFAULT NULL COMMENT '商家店铺类型(1美食2休闲娱乐3生活服务)' AFTER `merchant_id`,
---   ADD KEY `idx_shop_type` (`shop_type`);

+ 129 - 0
alien-entity/src/main/resources/db/migration/analytics_tables_dashboard_upgrade.sql

@@ -0,0 +1,129 @@
+-- =============================================================================
+-- 平台埋点 - 四页看板表结构升级(在 analytics_tables.sql 已执行基础上增量执行)
+-- =============================================================================
+-- 数据看板 / 用户报表 / 内容报表 / 商家报表
+-- =============================================================================
+
+SET NAMES utf8mb4;
+
+-- -----------------------------------------------------------------------------
+-- 1. analytics_event  【数据看板-漏斗】【内容/商家-品类】
+--    新增:event_subtype / business_category / shop_type
+-- -----------------------------------------------------------------------------
+ALTER TABLE `analytics_event`
+    ADD COLUMN `event_subtype` varchar(32) DEFAULT NULL COMMENT '事件子类型(如审核ai_pass/manual_pass/reject;注册漏斗步骤等)' AFTER `event_code`,
+    ADD COLUMN `business_category` tinyint DEFAULT NULL COMMENT '经营品类(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)' AFTER `content_type`,
+    ADD COLUMN `shop_type` tinyint DEFAULT NULL COMMENT '商户店铺类型(与business_category一致,支付/浏览事件冗余)' AFTER `business_category`,
+    ADD KEY `idx_event_code_subtype_time` (`event_code`, `event_subtype`, `event_time`),
+    ADD KEY `idx_business_category_time` (`business_category`, `event_time`);
+
+-- -----------------------------------------------------------------------------
+-- 2. analytics_user_stat  【用户报表-画像/地域/新老用户】
+-- -----------------------------------------------------------------------------
+ALTER TABLE `analytics_user_stat`
+    ADD COLUMN `gender` tinyint DEFAULT NULL COMMENT '性别(0未知1男2女)' AFTER `user_phone`,
+    ADD COLUMN `age` int DEFAULT NULL COMMENT '年龄' AFTER `gender`,
+    ADD COLUMN `age_group` varchar(16) DEFAULT NULL COMMENT '年龄段(18-24/25-29/30-34/35-39/40-49/50+)' AFTER `age`,
+    ADD COLUMN `province` varchar(64) DEFAULT NULL COMMENT '省份' AFTER `city`,
+    ADD COLUMN `is_new_user` tinyint NOT NULL DEFAULT 0 COMMENT '是否新用户(注册7天内:0否1是)' AFTER `channel`,
+    ADD KEY `idx_gender` (`gender`),
+    ADD KEY `idx_age_group` (`age_group`),
+    ADD KEY `idx_city` (`city`),
+    ADD KEY `idx_register_time` (`register_time`);
+
+-- -----------------------------------------------------------------------------
+-- 3. analytics_merchant_stat  【商家报表-GMV TOP10/明细/转化漏斗】
+-- -----------------------------------------------------------------------------
+ALTER TABLE `analytics_merchant_stat`
+    ADD COLUMN `gmv` decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '当日GMV(元)' AFTER `visit_pv`,
+    ADD COLUMN `pay_count` int NOT NULL DEFAULT 0 COMMENT '当日支付笔数' AFTER `gmv`,
+    ADD COLUMN `pay_user_count` int NOT NULL DEFAULT 0 COMMENT '当日支付用户数(去重)' AFTER `pay_count`,
+    ADD COLUMN `verify_count` int NOT NULL DEFAULT 0 COMMENT '当日核销次数' AFTER `pay_user_count`,
+    ADD COLUMN `expose_count` int NOT NULL DEFAULT 0 COMMENT '曝光次数(漏斗)' AFTER `verify_count`,
+    ADD COLUMN `click_count` int NOT NULL DEFAULT 0 COMMENT '点击次数(漏斗)' AFTER `expose_count`,
+    ADD COLUMN `detail_view_count` int NOT NULL DEFAULT 0 COMMENT '详情页浏览次数(漏斗)' AFTER `click_count`,
+    ADD COLUMN `contact_count` int NOT NULL DEFAULT 0 COMMENT '电话/导航次数(漏斗)' AFTER `detail_view_count`,
+    ADD COLUMN `review_count` int NOT NULL DEFAULT 0 COMMENT '当日评价数' AFTER `contact_count`,
+    ADD COLUMN `review_rate` decimal(10,4) DEFAULT NULL COMMENT '评价率(%)' AFTER `review_count`,
+    ADD KEY `idx_gmv` (`stat_date`, `gmv`);
+
+-- -----------------------------------------------------------------------------
+-- 4. analytics_content_stat  【内容报表-品类分布/互动TOP10/审核】
+-- -----------------------------------------------------------------------------
+ALTER TABLE `analytics_content_stat`
+    ADD COLUMN `business_category` tinyint DEFAULT NULL COMMENT '经营品类(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)' AFTER `content_type`,
+    ADD COLUMN `content_title` varchar(256) DEFAULT NULL COMMENT '内容标题快照(互动TOP10展示)' AFTER `author_id`,
+    ADD COLUMN `like_count` int NOT NULL DEFAULT 0 COMMENT '点赞数' AFTER `interaction_count`,
+    ADD COLUMN `comment_count` int NOT NULL DEFAULT 0 COMMENT '评论数' AFTER `like_count`,
+    ADD COLUMN `share_count` int NOT NULL DEFAULT 0 COMMENT '分享数' AFTER `comment_count`,
+    ADD COLUMN `audit_type` tinyint DEFAULT NULL COMMENT '审核方式(1AI2人工)' AFTER `audit_status`,
+    ADD KEY `idx_business_category` (`business_category`),
+    ADD KEY `idx_interaction` (`interaction_count`);
+
+-- -----------------------------------------------------------------------------
+-- 5. analytics_ai_chat_stat  【数据看板-对话趋势】无需改表,按 start_time 聚合即可
+-- 6. analytics_ai_request   【数据看板-AI响应时间】无需改表
+-- 7. analytics_stat_job_log   运维表,无需改表
+-- -----------------------------------------------------------------------------
+
+-- -----------------------------------------------------------------------------
+-- 8. analytics_daily_summary  【四页顶部 KPI + 漏斗 + MAU + 审核细分】
+-- -----------------------------------------------------------------------------
+ALTER TABLE `analytics_daily_summary`
+    ADD COLUMN `mau` int NOT NULL DEFAULT 0 COMMENT '月活MAU(近30日去重,写入当日快照)' AFTER `dau`,
+    ADD COLUMN `ai_request_count` int NOT NULL DEFAULT 0 COMMENT 'AI请求次数(计算平均响应时长分母)' AFTER `ai_response_duration_total_ms`,
+    ADD COLUMN `ai_chat_user_count` int NOT NULL DEFAULT 0 COMMENT '对话用户数(去重)' AFTER `ai_chat_count`,
+    ADD COLUMN `merchant_expose_count` bigint NOT NULL DEFAULT 0 COMMENT '商家曝光(漏斗)' AFTER `merchant_visit_uv`,
+    ADD COLUMN `merchant_click_count` bigint NOT NULL DEFAULT 0 COMMENT '商家点击(漏斗)' AFTER `merchant_expose_count`,
+    ADD COLUMN `merchant_detail_view_count` bigint NOT NULL DEFAULT 0 COMMENT '商家详情浏览(漏斗)' AFTER `merchant_click_count`,
+    ADD COLUMN `merchant_contact_count` bigint NOT NULL DEFAULT 0 COMMENT '商家电话/导航(漏斗)' AFTER `merchant_detail_view_count`,
+    ADD COLUMN `register_page_view_count` int NOT NULL DEFAULT 0 COMMENT '进入注册页(注册漏斗)' AFTER `new_user_count`,
+    ADD COLUMN `register_phone_submit_count` int NOT NULL DEFAULT 0 COMMENT '提交手机号(注册漏斗)' AFTER `register_page_view_count`,
+    ADD COLUMN `register_otp_pass_count` int NOT NULL DEFAULT 0 COMMENT '验证码通过(注册漏斗)' AFTER `register_phone_submit_count`,
+    ADD COLUMN `register_password_set_count` int NOT NULL DEFAULT 0 COMMENT '设置密码(注册漏斗)' AFTER `register_otp_pass_count`,
+    ADD COLUMN `register_success_count` int NOT NULL DEFAULT 0 COMMENT '注册成功(注册漏斗)' AFTER `register_password_set_count`,
+    ADD COLUMN `audit_ai_pass_count` int NOT NULL DEFAULT 0 COMMENT 'AI审核通过次数' AFTER `audit_submit_count`,
+    ADD COLUMN `audit_manual_pass_count` int NOT NULL DEFAULT 0 COMMENT '人工审核通过次数' AFTER `audit_ai_pass_count`,
+    ADD COLUMN `audit_reject_count` int NOT NULL DEFAULT 0 COMMENT '审核驳回次数' AFTER `audit_manual_pass_count`,
+    ADD COLUMN `report_handle_rate` decimal(10,4) DEFAULT NULL COMMENT '当日举报处理率(%)' AFTER `report_submit_count`,
+    ADD COLUMN `merchant_review_rate` decimal(10,4) DEFAULT NULL COMMENT '商家评价率(%)' AFTER `verify_rate`,
+    ADD COLUMN `last_30d_active_user_count` int NOT NULL DEFAULT 0 COMMENT '近30日活跃用户数量' AFTER `last_7d_active_user_count`;
+
+-- -----------------------------------------------------------------------------
+-- 9. analytics_category_daily  【商家报表-品类GMV占比】【内容报表-品类分布】  新表
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_category_daily` (
+  `id`                      bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `stat_date`               date          NOT NULL COMMENT '统计日期',
+  `business_category`       tinyint       NOT NULL COMMENT '经营品类(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)',
+  `gmv`                     decimal(14,2) NOT NULL DEFAULT 0.00 COMMENT '品类GMV',
+  `pay_count`               int           NOT NULL DEFAULT 0 COMMENT '支付笔数',
+  `merchant_visit_uv`       int           NOT NULL DEFAULT 0 COMMENT '品类商家访问UV',
+  `content_publish_count`   int           NOT NULL DEFAULT 0 COMMENT '品类内容发布数',
+  `content_interaction_count` int         NOT NULL DEFAULT 0 COMMENT '品类内容互动数',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_category` (`stat_date`, `business_category`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='经营品类日统计(看板饼图/占比)';
+
+-- -----------------------------------------------------------------------------
+-- 10. analytics_report_record  【内容报表-举报处理列表】  新表
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_report_record` (
+  `id`              bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `report_id`       varchar(64)  NOT NULL COMMENT '举报单号(幂等)',
+  `content_id`      bigint       DEFAULT NULL COMMENT '被举报内容ID',
+  `content_type`    tinyint      DEFAULT NULL COMMENT '内容类型(1动态2打卡3二手)',
+  `content_title`   varchar(256) DEFAULT NULL COMMENT '内容标题快照',
+  `report_type`     varchar(32)  NOT NULL COMMENT '举报类型(不实信息/色情低俗/垃圾广告/侵权等)',
+  `status`          tinyint      NOT NULL DEFAULT 0 COMMENT '状态(0处理中1已处理)',
+  `reporter_user_id` bigint      DEFAULT NULL COMMENT '举报人用户ID',
+  `report_time`     datetime     NOT NULL COMMENT '举报时间',
+  `handle_time`     datetime     DEFAULT NULL COMMENT '处理时间',
+  `handle_user_id`  bigint       DEFAULT NULL COMMENT '处理人ID',
+  `created_time`    datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_report_id` (`report_id`),
+  KEY `idx_report_time` (`report_time`),
+  KEY `idx_status_time` (`status`, `report_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='举报处理明细(内容报表列表)';

+ 264 - 0
alien-entity/src/main/resources/db/migration/analytics_today_history_tables.sql

@@ -0,0 +1,264 @@
+-- =============================================================================
+-- 埋点明细:今日表 + 历史表(历史保留至少30天,由定时任务零点归档)
+--
+-- 调度建议(XXL-JOB):
+--   analyticsArchiveDaily        0 0 0 * * ?  零点归档昨日今日表 → 历史表
+--   analyticsCalculateTodayHourly 0 0 * * * ?  每小时汇总当日
+--   analyticsCalculateYesterdayDaily 0 0 2 * * ? 凌晨2点汇总前日(归档后)
+-- =============================================================================
+SET NAMES utf8mb4;
+
+-- 若旧表存在,重命名为历史表(仅首次升级执行)
+-- RENAME TABLE analytics_user_stat TO analytics_user_stat_history;
+-- RENAME TABLE analytics_merchant_stat TO analytics_merchant_stat_history;
+-- RENAME TABLE analytics_content_stat TO analytics_content_stat_today;
+-- RENAME TABLE analytics_category_daily TO analytics_category_daily_history;
+-- RENAME TABLE analytics_ai_chat_stat TO analytics_ai_chat_stat_today;
+-- RENAME TABLE analytics_report_record TO analytics_report_record_today;
+
+-- -----------------------------------------------------------------------------
+-- 1. 用户明细
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_user_stat_today` (
+  `id`                  bigint       NOT NULL AUTO_INCREMENT,
+  `user_id`             bigint       NOT NULL COMMENT '用户ID',
+  `first_launch_time`   datetime     DEFAULT NULL,
+  `last_active_time`    datetime     DEFAULT NULL,
+  `city`                varchar(64)  DEFAULT NULL,
+  `province`            varchar(64)  DEFAULT NULL,
+  `device_type`         varchar(16)  DEFAULT NULL,
+  `user_phone`          varchar(20)  DEFAULT NULL,
+  `gender`              tinyint      DEFAULT NULL,
+  `age`                 int          DEFAULT NULL,
+  `age_group`           varchar(16)  DEFAULT NULL,
+  `register_time`       datetime     DEFAULT NULL,
+  `channel`             varchar(64)  DEFAULT NULL,
+  `is_new_user`         tinyint      NOT NULL DEFAULT 0,
+  `online_duration_min` int          NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_id` (`user_id`),
+  KEY `idx_last_active` (`last_active_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户明细-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_user_stat_history` (
+  `id`                  bigint       NOT NULL AUTO_INCREMENT,
+  `stat_date`           date         NOT NULL COMMENT '统计日期',
+  `user_id`             bigint       NOT NULL,
+  `first_launch_time`   datetime     DEFAULT NULL,
+  `last_active_time`    datetime     DEFAULT NULL,
+  `city`                varchar(64)  DEFAULT NULL,
+  `province`            varchar(64)  DEFAULT NULL,
+  `device_type`         varchar(16)  DEFAULT NULL,
+  `user_phone`          varchar(20)  DEFAULT NULL,
+  `gender`              tinyint      DEFAULT NULL,
+  `age`                 int          DEFAULT NULL,
+  `age_group`           varchar(16)  DEFAULT NULL,
+  `register_time`       datetime     DEFAULT NULL,
+  `channel`             varchar(64)  DEFAULT NULL,
+  `is_new_user`         tinyint      NOT NULL DEFAULT 0,
+  `online_duration_min` int          NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_user` (`stat_date`, `user_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户明细-历史';
+
+-- -----------------------------------------------------------------------------
+-- 2. 商户明细
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_merchant_stat_today` (
+  `id`                     bigint        NOT NULL AUTO_INCREMENT,
+  `merchant_id`            bigint        NOT NULL,
+  `shop_type`              tinyint       DEFAULT NULL,
+  `visit_uv`               int           NOT NULL DEFAULT 0,
+  `visit_pv`               int           NOT NULL DEFAULT 0,
+  `gmv`                    decimal(14,2) NOT NULL DEFAULT 0.00,
+  `pay_count`              int           NOT NULL DEFAULT 0,
+  `pay_user_count`         int           NOT NULL DEFAULT 0,
+  `verify_count`           int           NOT NULL DEFAULT 0,
+  `expose_count`           int           NOT NULL DEFAULT 0,
+  `click_count`            int           NOT NULL DEFAULT 0,
+  `detail_view_count`      int           NOT NULL DEFAULT 0,
+  `contact_count`          int           NOT NULL DEFAULT 0,
+  `review_count`           int           NOT NULL DEFAULT 0,
+  `review_rate`            decimal(10,4) DEFAULT NULL,
+  `verify_conversion_rate` decimal(10,4) DEFAULT NULL,
+  `settle_time`            datetime      DEFAULT NULL,
+  `settle_status`          tinyint       DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_merchant_id` (`merchant_id`),
+  KEY `idx_shop_type` (`shop_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户明细-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_merchant_stat_history` (
+  `id`                     bigint        NOT NULL AUTO_INCREMENT,
+  `stat_date`              date          NOT NULL,
+  `merchant_id`            bigint        NOT NULL,
+  `shop_type`              tinyint       DEFAULT NULL,
+  `visit_uv`               int           NOT NULL DEFAULT 0,
+  `visit_pv`               int           NOT NULL DEFAULT 0,
+  `gmv`                    decimal(14,2) NOT NULL DEFAULT 0.00,
+  `pay_count`              int           NOT NULL DEFAULT 0,
+  `pay_user_count`         int           NOT NULL DEFAULT 0,
+  `verify_count`           int           NOT NULL DEFAULT 0,
+  `expose_count`           int           NOT NULL DEFAULT 0,
+  `click_count`            int           NOT NULL DEFAULT 0,
+  `detail_view_count`      int           NOT NULL DEFAULT 0,
+  `contact_count`          int           NOT NULL DEFAULT 0,
+  `review_count`           int           NOT NULL DEFAULT 0,
+  `review_rate`            decimal(10,4) DEFAULT NULL,
+  `verify_conversion_rate` decimal(10,4) DEFAULT NULL,
+  `settle_time`            datetime      DEFAULT NULL,
+  `settle_status`          tinyint       DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_merchant` (`stat_date`, `merchant_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_merchant_id` (`merchant_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商户明细-历史';
+
+-- -----------------------------------------------------------------------------
+-- 3. 内容明细
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_content_stat_today` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `content_id`        bigint       NOT NULL,
+  `content_type`      tinyint      NOT NULL,
+  `business_category` tinyint      DEFAULT NULL,
+  `author_type`       tinyint      NOT NULL,
+  `author_id`         bigint       NOT NULL,
+  `content_title`     varchar(256) DEFAULT NULL,
+  `publish_time`      datetime     DEFAULT NULL,
+  `interaction_count` int          NOT NULL DEFAULT 0,
+  `like_count`        int          NOT NULL DEFAULT 0,
+  `comment_count`     int          NOT NULL DEFAULT 0,
+  `share_count`       int          NOT NULL DEFAULT 0,
+  `status`            tinyint      DEFAULT NULL,
+  `audit_user_id`     bigint       DEFAULT NULL,
+  `audit_status`      tinyint      DEFAULT NULL,
+  `audit_type`        tinyint      DEFAULT NULL,
+  `audit_time`        datetime     DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_content` (`content_type`, `content_id`),
+  KEY `idx_interaction` (`interaction_count`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='内容明细-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_content_stat_history` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT,
+  `stat_date`         date         NOT NULL,
+  `content_id`        bigint       NOT NULL,
+  `content_type`      tinyint      NOT NULL,
+  `business_category` tinyint      DEFAULT NULL,
+  `author_type`       tinyint      NOT NULL,
+  `author_id`         bigint       NOT NULL,
+  `content_title`     varchar(256) DEFAULT NULL,
+  `publish_time`      datetime     DEFAULT NULL,
+  `interaction_count` int          NOT NULL DEFAULT 0,
+  `like_count`        int          NOT NULL DEFAULT 0,
+  `comment_count`     int          NOT NULL DEFAULT 0,
+  `share_count`       int          NOT NULL DEFAULT 0,
+  `status`            tinyint      DEFAULT NULL,
+  `audit_user_id`     bigint       DEFAULT NULL,
+  `audit_status`      tinyint      DEFAULT NULL,
+  `audit_type`        tinyint      DEFAULT NULL,
+  `audit_time`        datetime     DEFAULT NULL,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_content` (`stat_date`, `content_type`, `content_id`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='内容明细-历史';
+
+-- -----------------------------------------------------------------------------
+-- 4. 品类日统计
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_category_daily_today` (
+  `id`                        bigint        NOT NULL AUTO_INCREMENT,
+  `business_category`         tinyint       NOT NULL,
+  `gmv`                       decimal(14,2) NOT NULL DEFAULT 0.00,
+  `pay_count`                 int           NOT NULL DEFAULT 0,
+  `merchant_visit_uv`         int           NOT NULL DEFAULT 0,
+  `content_publish_count`     int           NOT NULL DEFAULT 0,
+  `content_interaction_count` int           NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_category` (`business_category`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品类日统计-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_category_daily_history` (
+  `id`                        bigint        NOT NULL AUTO_INCREMENT,
+  `stat_date`                 date          NOT NULL,
+  `business_category`         tinyint       NOT NULL,
+  `gmv`                       decimal(14,2) NOT NULL DEFAULT 0.00,
+  `pay_count`                 int           NOT NULL DEFAULT 0,
+  `merchant_visit_uv`         int           NOT NULL DEFAULT 0,
+  `content_publish_count`     int           NOT NULL DEFAULT 0,
+  `content_interaction_count` int           NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_category` (`stat_date`, `business_category`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='品类日统计-历史';
+
+-- -----------------------------------------------------------------------------
+-- 5. AI 对话明细
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_ai_chat_stat_today` (
+  `id`                      bigint      NOT NULL AUTO_INCREMENT,
+  `chat_id`                 varchar(64) NOT NULL,
+  `user_id`                 bigint      NOT NULL,
+  `start_time`              datetime    NOT NULL,
+  `message_count`           int         NOT NULL DEFAULT 0,
+  `ai_response_duration_ms` bigint      NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_chat_id` (`chat_id`),
+  KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_ai_chat_stat_history` (
+  `id`                      bigint      NOT NULL AUTO_INCREMENT,
+  `stat_date`               date        NOT NULL,
+  `chat_id`                 varchar(64) NOT NULL,
+  `user_id`                 bigint      NOT NULL,
+  `start_time`              datetime    NOT NULL,
+  `message_count`           int         NOT NULL DEFAULT 0,
+  `ai_response_duration_ms` bigint      NOT NULL DEFAULT 0,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_chat` (`stat_date`, `chat_id`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI对话-历史';
+
+-- -----------------------------------------------------------------------------
+-- 6. 举报明细
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_report_record_today` (
+  `id`               bigint       NOT NULL AUTO_INCREMENT,
+  `report_id`        varchar(64)  NOT NULL,
+  `content_id`       bigint       DEFAULT NULL,
+  `content_type`     tinyint      DEFAULT NULL,
+  `content_title`    varchar(256) DEFAULT NULL,
+  `report_type`      varchar(32)  NOT NULL,
+  `status`           tinyint      NOT NULL DEFAULT 0,
+  `reporter_user_id` bigint       DEFAULT NULL,
+  `report_time`      datetime     NOT NULL,
+  `handle_time`      datetime     DEFAULT NULL,
+  `handle_user_id`   bigint       DEFAULT NULL,
+  `created_time`     datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_report_id` (`report_id`),
+  KEY `idx_report_time` (`report_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='举报明细-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_report_record_history` (
+  `id`               bigint       NOT NULL AUTO_INCREMENT,
+  `stat_date`        date         NOT NULL,
+  `report_id`        varchar(64)  NOT NULL,
+  `content_id`       bigint       DEFAULT NULL,
+  `content_type`     tinyint      DEFAULT NULL,
+  `content_title`    varchar(256) DEFAULT NULL,
+  `report_type`      varchar(32)  NOT NULL,
+  `status`           tinyint      NOT NULL DEFAULT 0,
+  `reporter_user_id` bigint       DEFAULT NULL,
+  `report_time`      datetime     NOT NULL,
+  `handle_time`      datetime     DEFAULT NULL,
+  `handle_user_id`   bigint       DEFAULT NULL,
+  `created_time`     datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_report` (`stat_date`, `report_id`),
+  KEY `idx_stat_date` (`stat_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='举报明细-历史';

+ 164 - 0
alien-entity/src/main/resources/mapper/AnalyticsArchiveMapper.xml

@@ -0,0 +1,164 @@
+<?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.AnalyticsArchiveMapper">
+
+    <insert id="archiveUserStat">
+        INSERT INTO analytics_user_stat_history (
+            stat_date, user_id, first_launch_time, last_active_time, city, province,
+            device_type, user_phone, gender, age, age_group, register_time, channel,
+            is_new_user, online_duration_min
+        )
+        SELECT #{archiveDate}, user_id, first_launch_time, last_active_time, city, province,
+               device_type, user_phone, gender, age, age_group, register_time, channel,
+               is_new_user, online_duration_min
+        FROM analytics_user_stat_today
+        ON DUPLICATE KEY UPDATE
+            first_launch_time = VALUES(first_launch_time),
+            last_active_time = VALUES(last_active_time),
+            city = VALUES(city),
+            province = VALUES(province),
+            device_type = VALUES(device_type),
+            user_phone = VALUES(user_phone),
+            gender = VALUES(gender),
+            age = VALUES(age),
+            age_group = VALUES(age_group),
+            register_time = VALUES(register_time),
+            channel = VALUES(channel),
+            is_new_user = VALUES(is_new_user),
+            online_duration_min = VALUES(online_duration_min)
+    </insert>
+
+    <insert id="archiveMerchantStat">
+        INSERT INTO analytics_merchant_stat_history (
+            stat_date, merchant_id, shop_type, visit_uv, visit_pv, gmv, pay_count,
+            pay_user_count, verify_count, expose_count, click_count, detail_view_count,
+            contact_count, review_count, review_rate, verify_conversion_rate,
+            settle_time, settle_status
+        )
+        SELECT #{archiveDate}, merchant_id, shop_type, visit_uv, visit_pv, gmv, pay_count,
+               pay_user_count, verify_count, expose_count, click_count, detail_view_count,
+               contact_count, review_count, review_rate, verify_conversion_rate,
+               settle_time, settle_status
+        FROM analytics_merchant_stat_today
+        ON DUPLICATE KEY UPDATE
+            shop_type = VALUES(shop_type),
+            visit_uv = VALUES(visit_uv),
+            visit_pv = VALUES(visit_pv),
+            gmv = VALUES(gmv),
+            pay_count = VALUES(pay_count),
+            pay_user_count = VALUES(pay_user_count),
+            verify_count = VALUES(verify_count),
+            expose_count = VALUES(expose_count),
+            click_count = VALUES(click_count),
+            detail_view_count = VALUES(detail_view_count),
+            contact_count = VALUES(contact_count),
+            review_count = VALUES(review_count),
+            review_rate = VALUES(review_rate),
+            verify_conversion_rate = VALUES(verify_conversion_rate),
+            settle_time = VALUES(settle_time),
+            settle_status = VALUES(settle_status)
+    </insert>
+
+    <insert id="archiveContentStat">
+        INSERT INTO analytics_content_stat_history (
+            stat_date, content_id, content_type, business_category, author_type, author_id,
+            content_title, publish_time, interaction_count, like_count, comment_count,
+            share_count, status, audit_user_id, audit_status, audit_type, audit_time
+        )
+        SELECT #{archiveDate}, content_id, content_type, business_category, author_type, author_id,
+               content_title, publish_time, interaction_count, like_count, comment_count,
+               share_count, status, audit_user_id, audit_status, audit_type, audit_time
+        FROM analytics_content_stat_today
+        ON DUPLICATE KEY UPDATE
+            business_category = VALUES(business_category),
+            author_type = VALUES(author_type),
+            author_id = VALUES(author_id),
+            content_title = VALUES(content_title),
+            publish_time = VALUES(publish_time),
+            interaction_count = VALUES(interaction_count),
+            like_count = VALUES(like_count),
+            comment_count = VALUES(comment_count),
+            share_count = VALUES(share_count),
+            status = VALUES(status),
+            audit_user_id = VALUES(audit_user_id),
+            audit_status = VALUES(audit_status),
+            audit_type = VALUES(audit_type),
+            audit_time = VALUES(audit_time)
+    </insert>
+
+    <insert id="archiveCategoryDaily">
+        INSERT INTO analytics_category_daily_history (
+            stat_date, business_category, gmv, pay_count, merchant_visit_uv,
+            content_publish_count, content_interaction_count
+        )
+        SELECT #{archiveDate}, business_category, gmv, pay_count, merchant_visit_uv,
+               content_publish_count, content_interaction_count
+        FROM analytics_category_daily_today
+        ON DUPLICATE KEY UPDATE
+            gmv = VALUES(gmv),
+            pay_count = VALUES(pay_count),
+            merchant_visit_uv = VALUES(merchant_visit_uv),
+            content_publish_count = VALUES(content_publish_count),
+            content_interaction_count = VALUES(content_interaction_count)
+    </insert>
+
+    <insert id="archiveAiChatStat">
+        INSERT INTO analytics_ai_chat_stat_history (
+            stat_date, chat_id, user_id, start_time, message_count, ai_response_duration_ms
+        )
+        SELECT #{archiveDate}, chat_id, user_id, start_time, message_count, ai_response_duration_ms
+        FROM analytics_ai_chat_stat_today
+        ON DUPLICATE KEY UPDATE
+            user_id = VALUES(user_id),
+            start_time = VALUES(start_time),
+            message_count = VALUES(message_count),
+            ai_response_duration_ms = VALUES(ai_response_duration_ms)
+    </insert>
+
+    <insert id="archiveReportRecord">
+        INSERT INTO analytics_report_record_history (
+            stat_date, report_id, content_id, content_type, content_title, report_type,
+            status, reporter_user_id, report_time, handle_time, handle_user_id, created_time
+        )
+        SELECT #{archiveDate}, report_id, content_id, content_type, content_title, report_type,
+               status, reporter_user_id, report_time, handle_time, handle_user_id, created_time
+        FROM analytics_report_record_today
+        ON DUPLICATE KEY UPDATE
+            content_id = VALUES(content_id),
+            content_type = VALUES(content_type),
+            content_title = VALUES(content_title),
+            report_type = VALUES(report_type),
+            status = VALUES(status),
+            reporter_user_id = VALUES(reporter_user_id),
+            report_time = VALUES(report_time),
+            handle_time = VALUES(handle_time),
+            handle_user_id = VALUES(handle_user_id),
+            created_time = VALUES(created_time)
+    </insert>
+
+    <delete id="purgeUserStatHistory">
+        DELETE FROM analytics_user_stat_history WHERE stat_date &lt; #{beforeDate}
+    </delete>
+    <delete id="purgeMerchantStatHistory">
+        DELETE FROM analytics_merchant_stat_history WHERE stat_date &lt; #{beforeDate}
+    </delete>
+    <delete id="purgeContentStatHistory">
+        DELETE FROM analytics_content_stat_history WHERE stat_date &lt; #{beforeDate}
+    </delete>
+    <delete id="purgeCategoryDailyHistory">
+        DELETE FROM analytics_category_daily_history WHERE stat_date &lt; #{beforeDate}
+    </delete>
+    <delete id="purgeAiChatStatHistory">
+        DELETE FROM analytics_ai_chat_stat_history WHERE stat_date &lt; #{beforeDate}
+    </delete>
+    <delete id="purgeReportRecordHistory">
+        DELETE FROM analytics_report_record_history WHERE stat_date &lt; #{beforeDate}
+    </delete>
+
+    <update id="truncateUserStatToday">TRUNCATE TABLE analytics_user_stat_today</update>
+    <update id="truncateMerchantStatToday">TRUNCATE TABLE analytics_merchant_stat_today</update>
+    <update id="truncateContentStatToday">TRUNCATE TABLE analytics_content_stat_today</update>
+    <update id="truncateCategoryDailyToday">TRUNCATE TABLE analytics_category_daily_today</update>
+    <update id="truncateAiChatStatToday">TRUNCATE TABLE analytics_ai_chat_stat_today</update>
+    <update id="truncateReportRecordToday">TRUNCATE TABLE analytics_report_record_today</update>
+</mapper>

+ 6 - 0
alien-job/src/main/java/shop/alien/job/feign/AlienStoreFeign.java

@@ -112,6 +112,12 @@ public interface AlienStoreFeign {
     R<String> pollStoreCommentAppealSupplementCompletedResult();
 
     /**
+     * 平台埋点:零点归档昨日明细
+     */
+    @PostMapping("/analytics/job/archiveDaily")
+    R<String> archiveAnalyticsDaily();
+
+    /**
      * 平台埋点:每小时汇总当日数据
      */
     @PostMapping("/analytics/job/calculateTodayHourly")

+ 17 - 0
alien-job/src/main/java/shop/alien/job/store/AnalyticsStatisticsJob.java

@@ -12,6 +12,7 @@ import shop.alien.job.feign.AlienStoreFeign;
  * <p>
  * XXL-JOB 调度建议:
  * <ul>
+ *   <li>analyticsArchiveDaily:0 0 0 * * ?(每天零点归档)</li>
  *   <li>analyticsCalculateTodayHourly:0 0 * * * ?(每小时)</li>
  *   <li>analyticsCalculateYesterdayDaily:0 0 2 * * ?(每天凌晨2点)</li>
  *   <li>analyticsCalculateRetention:0 0 3 * * ?(每天凌晨3点)</li>
@@ -25,6 +26,22 @@ public class AnalyticsStatisticsJob {
     private final AlienStoreFeign alienStoreFeign;
 
     /**
+     * 每天零点将今日表明细归档至历史表
+     */
+    @XxlJob("analyticsArchiveDaily")
+    public void archiveDaily() {
+        log.info("【定时任务】平台埋点明细-零点归档:开始执行");
+        try {
+            R<String> result = alienStoreFeign.archiveAnalyticsDaily();
+            log.info("【定时任务】平台埋点明细-零点归档:执行完成,结果={}",
+                    result != null ? result.getData() : null);
+        } catch (Exception e) {
+            log.error("【定时任务】平台埋点明细-零点归档:执行异常", e);
+            throw e;
+        }
+    }
+
+    /**
      * 每小时汇总当日数据(准实时看板)
      */
     @XxlJob("analyticsCalculateTodayHourly")

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

@@ -9,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 import shop.alien.entity.analytics.AnalyticsStatScope;
 import shop.alien.entity.result.R;
+import shop.alien.store.service.analytics.AnalyticsArchiveService;
 import shop.alien.store.service.analytics.AnalyticsStatisticsService;
 import shop.alien.store.util.analytics.AnalyticsDateUtil;
 
@@ -28,6 +29,16 @@ public class AnalyticsJobController {
     private static final String TRIGGER_SCHEDULE = "SCHEDULE";
 
     private final AnalyticsStatisticsService statisticsService;
+    private final AnalyticsArchiveService archiveService;
+
+    @ApiOperation("零点归档昨日明细至历史表")
+    @PostMapping("/archiveDaily")
+    public R<String> archiveDaily() {
+        log.info("analytics job: archiveDaily 开始");
+        archiveService.archiveYesterday();
+        log.info("analytics job: archiveDaily 结束");
+        return R.success("归档完成");
+    }
 
     @ApiOperation("每小时汇总当日数据(准实时看板)")
     @PostMapping("/calculateTodayHourly")

+ 51 - 9
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsStatController.java

@@ -19,6 +19,7 @@ import shop.alien.entity.analytics.vo.AnalyticsMerchantStatVo;
 import shop.alien.entity.analytics.vo.AnalyticsUserStatVo;
 import shop.alien.entity.result.R;
 import shop.alien.mapper.*;
+import shop.alien.store.service.analytics.AnalyticsArchiveService;
 import shop.alien.store.service.analytics.AnalyticsStatEnrichService;
 import shop.alien.store.service.analytics.AnalyticsStatisticsService;
 
@@ -40,6 +41,7 @@ public class AnalyticsStatController {
 
     private final AnalyticsStatisticsService statisticsService;
     private final AnalyticsStatEnrichService statEnrichService;
+    private final AnalyticsArchiveService archiveService;
     private final AnalyticsAiChatStatMapper aiChatStatMapper;
     private final AnalyticsAiRequestMapper aiRequestMapper;
     private final AnalyticsStatJobLogMapper jobLogMapper;
@@ -56,16 +58,24 @@ public class AnalyticsStatController {
         return R.success("统计完成");
     }
 
-    @ApiOperation("查询平台日统计")
+    @ApiOperation("手动触发昨日明细归档(运维补跑)")
     @ApiOperationSupport(order = 2)
+    @PostMapping("/archive")
+    public R<String> archiveYesterday() {
+        archiveService.archiveYesterday();
+        return R.success("归档完成");
+    }
+
+    @ApiOperation("查询平台日统计")
+    @ApiOperationSupport(order = 3)
     @GetMapping("/daily")
     public R<AnalyticsDailySummary> getDailySummary(
             @ApiParam("统计日期") @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") Date statDate) {
         return R.data(statisticsService.getDailySummary(statDate));
     }
 
-    @ApiOperation("分页查询用户明细")
-    @ApiOperationSupport(order = 3)
+    @ApiOperation("分页查询用户明细(DAU)")
+    @ApiOperationSupport(order = 4)
     @GetMapping("/user/page")
     public R<IPage<AnalyticsUserStatVo>> pageUserStat(
             @RequestParam(defaultValue = "1") long page,
@@ -74,8 +84,28 @@ public class AnalyticsStatController {
         return R.data(statEnrichService.pageUserStat(page, size, statDate));
     }
 
+    @ApiOperation("分页查询新增用户明细")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/user/new/page")
+    public R<IPage<AnalyticsUserStatVo>> pageNewUserStat(
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date statDate) {
+        return R.data(statEnrichService.pageNewUserStat(page, size, statDate));
+    }
+
+    @ApiOperation("分页查询当前在线用户明细")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/user/online/page")
+    public R<IPage<AnalyticsUserStatVo>> pageOnlineUserStat(
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size,
+            @RequestParam(defaultValue = "5") int onlineWithinMin) {
+        return R.data(statEnrichService.pageOnlineUserStat(page, size, onlineWithinMin));
+    }
+
     @ApiOperation("分页查询商户明细")
-    @ApiOperationSupport(order = 4)
+    @ApiOperationSupport(order = 7)
     @GetMapping("/merchant/page")
     public R<IPage<AnalyticsMerchantStatVo>> pageMerchantStat(
             @RequestParam(defaultValue = "1") long page,
@@ -85,17 +115,29 @@ public class AnalyticsStatController {
     }
 
     @ApiOperation("分页查询内容明细")
-    @ApiOperationSupport(order = 5)
+    @ApiOperationSupport(order = 8)
     @GetMapping("/content/page")
     public R<IPage<AnalyticsContentStatVo>> pageContentStat(
             @RequestParam(defaultValue = "1") long page,
             @RequestParam(defaultValue = "10") long size,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date statDate,
             @RequestParam(required = false) Integer contentType) {
-        return R.data(statEnrichService.pageContentStat(page, size, contentType));
+        return R.data(statEnrichService.pageContentStat(page, size, statDate, contentType));
+    }
+
+    @ApiOperation("分页查询转化率日明细")
+    @ApiOperationSupport(order = 9)
+    @GetMapping("/conversion/page")
+    public R<IPage<AnalyticsDailySummary>> pageConversionDaily(
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
+            @RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate) {
+        return R.data(statEnrichService.pageConversionDaily(page, size, startDate, endDate));
     }
 
     @ApiOperation("分页查询AI对话明细")
-    @ApiOperationSupport(order = 6)
+    @ApiOperationSupport(order = 10)
     @GetMapping("/ai-chat/page")
     public R<IPage<AnalyticsAiChatStat>> pageAiChatStat(
             @RequestParam(defaultValue = "1") long page,
@@ -106,7 +148,7 @@ public class AnalyticsStatController {
     }
 
     @ApiOperation("分页查询AI请求明细")
-    @ApiOperationSupport(order = 7)
+    @ApiOperationSupport(order = 11)
     @GetMapping("/ai-request/page")
     public R<IPage<AnalyticsAiRequest>> pageAiRequest(
             @RequestParam(defaultValue = "1") long page,
@@ -117,7 +159,7 @@ public class AnalyticsStatController {
     }
 
     @ApiOperation("分页查询统计任务日志")
-    @ApiOperationSupport(order = 8)
+    @ApiOperationSupport(order = 12)
     @GetMapping("/job-log/page")
     public R<IPage<AnalyticsStatJobLog>> pageJobLog(
             @RequestParam(defaultValue = "1") long page,

+ 14 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsArchiveService.java

@@ -0,0 +1,14 @@
+package shop.alien.store.service.analytics;
+
+import java.util.Date;
+
+/**
+ * 埋点明细零点归档:今日表 → 历史表,并清理 30 天前数据
+ */
+public interface AnalyticsArchiveService {
+
+    /**
+     * 将昨日今日表数据归档至历史表,清空今日表,并删除 30 天前历史
+     */
+    void archiveYesterday();
+}

+ 47 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsDetailStoreService.java

@@ -0,0 +1,47 @@
+package shop.alien.store.service.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import shop.alien.entity.analytics.*;
+
+import java.util.Date;
+
+/**
+ * 埋点明细表路由:今日表 vs 历史表读写
+ */
+public interface AnalyticsDetailStoreService {
+
+    Date currentStatDate();
+
+    boolean isToday(Date statDate);
+
+    AnalyticsUserStat findUserStat(Date statDate, Long userId);
+
+    void insertUserStat(Date statDate, AnalyticsUserStat stat);
+
+    void updateUserStat(Date statDate, AnalyticsUserStat stat);
+
+    AnalyticsMerchantStat findMerchantStat(Date statDate, Long merchantId);
+
+    void insertMerchantStat(Date statDate, AnalyticsMerchantStat stat);
+
+    void updateMerchantStat(Date statDate, AnalyticsMerchantStat stat);
+
+    AnalyticsContentStat findContentStat(Date statDate, Integer contentType, Long contentId);
+
+    void insertContentStat(Date statDate, AnalyticsContentStat stat);
+
+    void updateContentStat(Date statDate, AnalyticsContentStat stat);
+
+    IPage<AnalyticsUserStat> pageUserStat(Page<AnalyticsUserStat> page, Date statDate);
+
+    /** 当日新增用户(is_new_user=1) */
+    IPage<AnalyticsUserStat> pageNewUserStat(Page<AnalyticsUserStat> page, Date statDate);
+
+    /** 当前在线用户(仅今日表,按最后活跃时间筛选) */
+    IPage<AnalyticsUserStat> pageOnlineUserStat(Page<AnalyticsUserStat> page, int onlineWithinMin);
+
+    IPage<AnalyticsMerchantStat> pageMerchantStat(Page<AnalyticsMerchantStat> page, Date statDate);
+
+    IPage<AnalyticsContentStat> pageContentStat(Page<AnalyticsContentStat> page, Date statDate, Integer contentType);
+}

+ 8 - 1
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsStatEnrichService.java

@@ -1,6 +1,7 @@
 package shop.alien.store.service.analytics;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.AnalyticsDailySummary;
 import shop.alien.entity.analytics.vo.AnalyticsContentStatVo;
 import shop.alien.entity.analytics.vo.AnalyticsMerchantStatVo;
 import shop.alien.entity.analytics.vo.AnalyticsUserStatVo;
@@ -11,7 +12,13 @@ public interface AnalyticsStatEnrichService {
 
     IPage<AnalyticsUserStatVo> pageUserStat(long page, long size, Date statDate);
 
+    IPage<AnalyticsUserStatVo> pageNewUserStat(long page, long size, Date statDate);
+
+    IPage<AnalyticsUserStatVo> pageOnlineUserStat(long page, long size, int onlineWithinMin);
+
     IPage<AnalyticsMerchantStatVo> pageMerchantStat(long page, long size, Date statDate);
 
-    IPage<AnalyticsContentStatVo> pageContentStat(long page, long size, Integer contentType);
+    IPage<AnalyticsContentStatVo> pageContentStat(long page, long size, Date statDate, Integer contentType);
+
+    IPage<AnalyticsDailySummary> pageConversionDaily(long page, long size, Date startDate, Date endDate);
 }

+ 57 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsArchiveServiceImpl.java

@@ -0,0 +1,57 @@
+package shop.alien.store.service.analytics.impl;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import shop.alien.mapper.AnalyticsArchiveMapper;
+import shop.alien.store.service.analytics.AnalyticsArchiveService;
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
+
+import java.util.Calendar;
+import java.util.Date;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AnalyticsArchiveServiceImpl implements AnalyticsArchiveService {
+
+    private static final int HISTORY_RETAIN_DAYS = 30;
+
+    private final AnalyticsArchiveMapper archiveMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void archiveYesterday() {
+        Calendar calendar = Calendar.getInstance();
+        calendar.add(Calendar.DAY_OF_MONTH, -1);
+        Date archiveDate = AnalyticsDateUtil.truncateToDate(calendar.getTime());
+        Date purgeBefore = AnalyticsDateUtil.addDays(AnalyticsDateUtil.truncateToDate(new Date()), -HISTORY_RETAIN_DAYS);
+
+        log.info("埋点明细归档开始: archiveDate={}, purgeBefore={}", archiveDate, purgeBefore);
+
+        int userRows = archiveMapper.archiveUserStat(archiveDate);
+        int merchantRows = archiveMapper.archiveMerchantStat(archiveDate);
+        int contentRows = archiveMapper.archiveContentStat(archiveDate);
+        int categoryRows = archiveMapper.archiveCategoryDaily(archiveDate);
+        int aiChatRows = archiveMapper.archiveAiChatStat(archiveDate);
+        int reportRows = archiveMapper.archiveReportRecord(archiveDate);
+
+        archiveMapper.purgeUserStatHistory(purgeBefore);
+        archiveMapper.purgeMerchantStatHistory(purgeBefore);
+        archiveMapper.purgeContentStatHistory(purgeBefore);
+        archiveMapper.purgeCategoryDailyHistory(purgeBefore);
+        archiveMapper.purgeAiChatStatHistory(purgeBefore);
+        archiveMapper.purgeReportRecordHistory(purgeBefore);
+
+        archiveMapper.truncateUserStatToday();
+        archiveMapper.truncateMerchantStatToday();
+        archiveMapper.truncateContentStatToday();
+        archiveMapper.truncateCategoryDailyToday();
+        archiveMapper.truncateAiChatStatToday();
+        archiveMapper.truncateReportRecordToday();
+
+        log.info("埋点明细归档完成: archiveDate={}, user={}, merchant={}, content={}, category={}, aiChat={}, report={}",
+                archiveDate, userRows, merchantRows, contentRows, categoryRows, aiChatRows, reportRows);
+    }
+}

+ 291 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDetailStoreServiceImpl.java

@@ -0,0 +1,291 @@
+package shop.alien.store.service.analytics.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.BeanUtils;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.analytics.*;
+import shop.alien.mapper.*;
+import shop.alien.store.service.analytics.AnalyticsDetailStoreService;
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
+
+import java.util.Calendar;
+import java.util.Date;
+
+@Service
+@RequiredArgsConstructor
+public class AnalyticsDetailStoreServiceImpl implements AnalyticsDetailStoreService {
+
+    private final AnalyticsUserStatMapper userStatHistoryMapper;
+    private final AnalyticsUserStatTodayMapper userStatTodayMapper;
+    private final AnalyticsMerchantStatMapper merchantStatHistoryMapper;
+    private final AnalyticsMerchantStatTodayMapper merchantStatTodayMapper;
+    private final AnalyticsContentStatMapper contentStatTodayMapper;
+    private final AnalyticsContentStatHistoryMapper contentStatHistoryMapper;
+
+    @Override
+    public Date currentStatDate() {
+        return AnalyticsDateUtil.truncateToDate(new Date());
+    }
+
+    @Override
+    public boolean isToday(Date statDate) {
+        if (statDate == null) {
+            return true;
+        }
+        return AnalyticsDateUtil.truncateToDate(statDate).equals(currentStatDate());
+    }
+
+    @Override
+    public AnalyticsUserStat findUserStat(Date statDate, Long userId) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate != null ? statDate : new Date());
+        if (isToday(day)) {
+            LambdaQueryWrapper<AnalyticsUserStatToday> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AnalyticsUserStatToday::getUserId, userId);
+            AnalyticsUserStatToday today = userStatTodayMapper.selectOne(wrapper);
+            return toUserStat(day, today);
+        }
+        LambdaQueryWrapper<AnalyticsUserStat> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsUserStat::getStatDate, day).eq(AnalyticsUserStat::getUserId, userId);
+        return userStatHistoryMapper.selectOne(wrapper);
+    }
+
+    @Override
+    public void insertUserStat(Date statDate, AnalyticsUserStat stat) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate);
+        if (isToday(day)) {
+            userStatTodayMapper.insert(toUserStatToday(stat));
+            return;
+        }
+        stat.setStatDate(day);
+        userStatHistoryMapper.insert(stat);
+    }
+
+    @Override
+    public void updateUserStat(Date statDate, AnalyticsUserStat stat) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate);
+        if (isToday(day)) {
+            AnalyticsUserStatToday today = toUserStatToday(stat);
+            today.setId(stat.getId());
+            userStatTodayMapper.updateById(today);
+            return;
+        }
+        stat.setStatDate(day);
+        userStatHistoryMapper.updateById(stat);
+    }
+
+    @Override
+    public AnalyticsMerchantStat findMerchantStat(Date statDate, Long merchantId) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate != null ? statDate : new Date());
+        if (isToday(day)) {
+            LambdaQueryWrapper<AnalyticsMerchantStatToday> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AnalyticsMerchantStatToday::getMerchantId, merchantId);
+            AnalyticsMerchantStatToday today = merchantStatTodayMapper.selectOne(wrapper);
+            return toMerchantStat(day, today);
+        }
+        LambdaQueryWrapper<AnalyticsMerchantStat> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsMerchantStat::getStatDate, day).eq(AnalyticsMerchantStat::getMerchantId, merchantId);
+        return merchantStatHistoryMapper.selectOne(wrapper);
+    }
+
+    @Override
+    public void insertMerchantStat(Date statDate, AnalyticsMerchantStat stat) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate);
+        if (isToday(day)) {
+            merchantStatTodayMapper.insert(toMerchantStatToday(stat));
+            return;
+        }
+        stat.setStatDate(day);
+        merchantStatHistoryMapper.insert(stat);
+    }
+
+    @Override
+    public void updateMerchantStat(Date statDate, AnalyticsMerchantStat stat) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate);
+        if (isToday(day)) {
+            AnalyticsMerchantStatToday today = toMerchantStatToday(stat);
+            today.setId(stat.getId());
+            merchantStatTodayMapper.updateById(today);
+            return;
+        }
+        stat.setStatDate(day);
+        merchantStatHistoryMapper.updateById(stat);
+    }
+
+    @Override
+    public AnalyticsContentStat findContentStat(Date statDate, Integer contentType, Long contentId) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate != null ? statDate : new Date());
+        if (isToday(day)) {
+            LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AnalyticsContentStat::getContentType, contentType)
+                    .eq(AnalyticsContentStat::getContentId, contentId);
+            return contentStatTodayMapper.selectOne(wrapper);
+        }
+        LambdaQueryWrapper<AnalyticsContentStatHistory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsContentStatHistory::getStatDate, day)
+                .eq(AnalyticsContentStatHistory::getContentType, contentType)
+                .eq(AnalyticsContentStatHistory::getContentId, contentId);
+        AnalyticsContentStatHistory history = contentStatHistoryMapper.selectOne(wrapper);
+        return toContentStat(history);
+    }
+
+    @Override
+    public void insertContentStat(Date statDate, AnalyticsContentStat stat) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate);
+        if (isToday(day)) {
+            contentStatTodayMapper.insert(stat);
+            return;
+        }
+        contentStatHistoryMapper.insert(toContentStatHistory(day, stat));
+    }
+
+    @Override
+    public void updateContentStat(Date statDate, AnalyticsContentStat stat) {
+        Date day = AnalyticsDateUtil.truncateToDate(statDate);
+        if (isToday(day)) {
+            contentStatTodayMapper.updateById(stat);
+            return;
+        }
+        AnalyticsContentStatHistory history = toContentStatHistory(day, stat);
+        history.setId(stat.getId());
+        contentStatHistoryMapper.updateById(history);
+    }
+
+    @Override
+    public IPage<AnalyticsUserStat> pageUserStat(Page<AnalyticsUserStat> page, Date statDate) {
+        Date day = statDate != null ? AnalyticsDateUtil.truncateToDate(statDate) : currentStatDate();
+        if (isToday(day)) {
+            LambdaQueryWrapper<AnalyticsUserStatToday> wrapper = new LambdaQueryWrapper<>();
+            wrapper.orderByDesc(AnalyticsUserStatToday::getLastActiveTime);
+            IPage<AnalyticsUserStatToday> raw = userStatTodayMapper.selectPage(
+                    new Page<>(page.getCurrent(), page.getSize()), wrapper);
+            return raw.convert(t -> toUserStat(day, t));
+        }
+        LambdaQueryWrapper<AnalyticsUserStat> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsUserStat::getStatDate, day)
+                .orderByDesc(AnalyticsUserStat::getLastActiveTime);
+        return userStatHistoryMapper.selectPage(page, wrapper);
+    }
+
+    @Override
+    public IPage<AnalyticsUserStat> pageNewUserStat(Page<AnalyticsUserStat> page, Date statDate) {
+        Date day = statDate != null ? AnalyticsDateUtil.truncateToDate(statDate) : currentStatDate();
+        if (isToday(day)) {
+            LambdaQueryWrapper<AnalyticsUserStatToday> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AnalyticsUserStatToday::getIsNewUser, 1)
+                    .orderByDesc(AnalyticsUserStatToday::getRegisterTime);
+            IPage<AnalyticsUserStatToday> raw = userStatTodayMapper.selectPage(
+                    new Page<>(page.getCurrent(), page.getSize()), wrapper);
+            return raw.convert(t -> toUserStat(day, t));
+        }
+        LambdaQueryWrapper<AnalyticsUserStat> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsUserStat::getStatDate, day)
+                .eq(AnalyticsUserStat::getIsNewUser, 1)
+                .orderByDesc(AnalyticsUserStat::getRegisterTime);
+        return userStatHistoryMapper.selectPage(page, wrapper);
+    }
+
+    @Override
+    public IPage<AnalyticsUserStat> pageOnlineUserStat(Page<AnalyticsUserStat> page, int onlineWithinMin) {
+        int withinMin = onlineWithinMin > 0 ? onlineWithinMin : 5;
+        Date day = currentStatDate();
+        Calendar threshold = Calendar.getInstance();
+        threshold.add(Calendar.MINUTE, -withinMin);
+        Date since = threshold.getTime();
+
+        LambdaQueryWrapper<AnalyticsUserStatToday> wrapper = new LambdaQueryWrapper<>();
+        wrapper.ge(AnalyticsUserStatToday::getLastActiveTime, since)
+                .orderByDesc(AnalyticsUserStatToday::getLastActiveTime);
+        IPage<AnalyticsUserStatToday> raw = userStatTodayMapper.selectPage(
+                new Page<>(page.getCurrent(), page.getSize()), wrapper);
+        return raw.convert(t -> toUserStat(day, t));
+    }
+
+    @Override
+    public IPage<AnalyticsMerchantStat> pageMerchantStat(Page<AnalyticsMerchantStat> page, Date statDate) {
+        Date day = statDate != null ? AnalyticsDateUtil.truncateToDate(statDate) : currentStatDate();
+        if (isToday(day)) {
+            LambdaQueryWrapper<AnalyticsMerchantStatToday> wrapper = new LambdaQueryWrapper<>();
+            wrapper.orderByDesc(AnalyticsMerchantStatToday::getVisitPv);
+            IPage<AnalyticsMerchantStatToday> raw = merchantStatTodayMapper.selectPage(
+                    new Page<>(page.getCurrent(), page.getSize()), wrapper);
+            return raw.convert(t -> toMerchantStat(day, t));
+        }
+        LambdaQueryWrapper<AnalyticsMerchantStat> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsMerchantStat::getStatDate, day)
+                .orderByDesc(AnalyticsMerchantStat::getVisitPv);
+        return merchantStatHistoryMapper.selectPage(page, wrapper);
+    }
+
+    @Override
+    public IPage<AnalyticsContentStat> pageContentStat(Page<AnalyticsContentStat> page, Date statDate, Integer contentType) {
+        Date day = statDate != null ? AnalyticsDateUtil.truncateToDate(statDate) : currentStatDate();
+        if (isToday(day)) {
+            LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
+            if (contentType != null) {
+                wrapper.eq(AnalyticsContentStat::getContentType, contentType);
+            }
+            wrapper.orderByDesc(AnalyticsContentStat::getPublishTime);
+            return contentStatTodayMapper.selectPage(page, wrapper);
+        }
+        LambdaQueryWrapper<AnalyticsContentStatHistory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsContentStatHistory::getStatDate, day);
+        if (contentType != null) {
+            wrapper.eq(AnalyticsContentStatHistory::getContentType, contentType);
+        }
+        wrapper.orderByDesc(AnalyticsContentStatHistory::getPublishTime);
+        IPage<AnalyticsContentStatHistory> raw = contentStatHistoryMapper.selectPage(
+                new Page<>(page.getCurrent(), page.getSize()), wrapper);
+        return raw.convert(this::toContentStat);
+    }
+
+    private AnalyticsUserStat toUserStat(Date statDate, AnalyticsUserStatToday today) {
+        if (today == null) {
+            return null;
+        }
+        AnalyticsUserStat stat = new AnalyticsUserStat();
+        BeanUtils.copyProperties(today, stat);
+        stat.setStatDate(statDate);
+        return stat;
+    }
+
+    private AnalyticsUserStatToday toUserStatToday(AnalyticsUserStat stat) {
+        AnalyticsUserStatToday today = new AnalyticsUserStatToday();
+        BeanUtils.copyProperties(stat, today);
+        return today;
+    }
+
+    private AnalyticsMerchantStat toMerchantStat(Date statDate, AnalyticsMerchantStatToday today) {
+        if (today == null) {
+            return null;
+        }
+        AnalyticsMerchantStat stat = new AnalyticsMerchantStat();
+        BeanUtils.copyProperties(today, stat);
+        stat.setStatDate(statDate);
+        return stat;
+    }
+
+    private AnalyticsMerchantStatToday toMerchantStatToday(AnalyticsMerchantStat stat) {
+        AnalyticsMerchantStatToday today = new AnalyticsMerchantStatToday();
+        BeanUtils.copyProperties(stat, today);
+        return today;
+    }
+
+    private AnalyticsContentStat toContentStat(AnalyticsContentStatHistory history) {
+        if (history == null) {
+            return null;
+        }
+        AnalyticsContentStat stat = new AnalyticsContentStat();
+        BeanUtils.copyProperties(history, stat);
+        return stat;
+    }
+
+    private AnalyticsContentStatHistory toContentStatHistory(Date statDate, AnalyticsContentStat stat) {
+        AnalyticsContentStatHistory history = new AnalyticsContentStatHistory();
+        BeanUtils.copyProperties(stat, history);
+        history.setStatDate(statDate);
+        return history;
+    }
+}

+ 59 - 30
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatEnrichServiceImpl.java

@@ -7,21 +7,18 @@ import lombok.RequiredArgsConstructor;
 import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.util.StringUtils;
-import shop.alien.entity.analytics.AnalyticsContentStat;
-import shop.alien.entity.analytics.AnalyticsMerchantStat;
-import shop.alien.entity.analytics.AnalyticsUserStat;
+import shop.alien.entity.analytics.*;
 import shop.alien.entity.analytics.vo.AnalyticsContentStatVo;
 import shop.alien.entity.analytics.vo.AnalyticsMerchantStatVo;
 import shop.alien.entity.analytics.vo.AnalyticsUserStatVo;
 import shop.alien.entity.store.LifeUser;
 import shop.alien.entity.store.StoreInfo;
 import shop.alien.entity.store.StoreUser;
-import shop.alien.mapper.AnalyticsContentStatMapper;
-import shop.alien.mapper.AnalyticsMerchantStatMapper;
-import shop.alien.mapper.AnalyticsUserStatMapper;
+import shop.alien.mapper.AnalyticsDailySummaryMapper;
 import shop.alien.mapper.LifeUserMapper;
 import shop.alien.mapper.StoreInfoMapper;
 import shop.alien.mapper.StoreUserMapper;
+import shop.alien.store.service.analytics.AnalyticsDetailStoreService;
 import shop.alien.store.service.analytics.AnalyticsStatEnrichService;
 import shop.alien.store.util.analytics.AnalyticsDateUtil;
 
@@ -31,46 +28,53 @@ import java.util.Date;
 @RequiredArgsConstructor
 public class AnalyticsStatEnrichServiceImpl implements AnalyticsStatEnrichService {
 
-    private final AnalyticsUserStatMapper userStatMapper;
-    private final AnalyticsMerchantStatMapper merchantStatMapper;
-    private final AnalyticsContentStatMapper contentStatMapper;
+    private final AnalyticsDetailStoreService detailStoreService;
+    private final AnalyticsDailySummaryMapper dailySummaryMapper;
     private final LifeUserMapper lifeUserMapper;
     private final StoreInfoMapper storeInfoMapper;
     private final StoreUserMapper storeUserMapper;
 
     @Override
     public IPage<AnalyticsUserStatVo> pageUserStat(long page, long size, Date statDate) {
-        LambdaQueryWrapper<AnalyticsUserStat> wrapper = new LambdaQueryWrapper<>();
-        if (statDate != null) {
-            wrapper.eq(AnalyticsUserStat::getStatDate, AnalyticsDateUtil.truncateToDate(statDate));
-        }
-        wrapper.orderByDesc(AnalyticsUserStat::getLastActiveTime);
-        IPage<AnalyticsUserStat> raw = userStatMapper.selectPage(new Page<>(page, size), wrapper);
+        IPage<AnalyticsUserStat> raw = detailStoreService.pageUserStat(new Page<>(page, size), statDate);
+        return raw.convert(this::toUserVo);
+    }
+
+    @Override
+    public IPage<AnalyticsUserStatVo> pageNewUserStat(long page, long size, Date statDate) {
+        IPage<AnalyticsUserStat> raw = detailStoreService.pageNewUserStat(new Page<>(page, size), statDate);
+        return raw.convert(this::toUserVo);
+    }
+
+    @Override
+    public IPage<AnalyticsUserStatVo> pageOnlineUserStat(long page, long size, int onlineWithinMin) {
+        IPage<AnalyticsUserStat> raw = detailStoreService.pageOnlineUserStat(new Page<>(page, size), onlineWithinMin);
         return raw.convert(this::toUserVo);
     }
 
     @Override
     public IPage<AnalyticsMerchantStatVo> pageMerchantStat(long page, long size, Date statDate) {
-        LambdaQueryWrapper<AnalyticsMerchantStat> wrapper = new LambdaQueryWrapper<>();
-        if (statDate != null) {
-            wrapper.eq(AnalyticsMerchantStat::getStatDate, AnalyticsDateUtil.truncateToDate(statDate));
-        }
-        wrapper.orderByDesc(AnalyticsMerchantStat::getVisitPv);
-        IPage<AnalyticsMerchantStat> raw = merchantStatMapper.selectPage(new Page<>(page, size), wrapper);
+        IPage<AnalyticsMerchantStat> raw = detailStoreService.pageMerchantStat(new Page<>(page, size), statDate);
         return raw.convert(this::toMerchantVo);
     }
 
     @Override
-    public IPage<AnalyticsContentStatVo> pageContentStat(long page, long size, Integer contentType) {
-        LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
-        if (contentType != null) {
-            wrapper.eq(AnalyticsContentStat::getContentType, contentType);
-        }
-        wrapper.orderByDesc(AnalyticsContentStat::getPublishTime);
-        IPage<AnalyticsContentStat> raw = contentStatMapper.selectPage(new Page<>(page, size), wrapper);
+    public IPage<AnalyticsContentStatVo> pageContentStat(long page, long size, Date statDate, Integer contentType) {
+        IPage<AnalyticsContentStat> raw = detailStoreService.pageContentStat(new Page<>(page, size), statDate, contentType);
         return raw.convert(this::toContentVo);
     }
 
+    @Override
+    public IPage<AnalyticsDailySummary> pageConversionDaily(long page, long size, Date startDate, Date endDate) {
+        Date start = startDate != null ? AnalyticsDateUtil.truncateToDate(startDate) : AnalyticsDateUtil.addDays(new Date(), -7);
+        Date end = endDate != null ? AnalyticsDateUtil.truncateToDate(endDate) : AnalyticsDateUtil.truncateToDate(new Date());
+        LambdaQueryWrapper<AnalyticsDailySummary> wrapper = new LambdaQueryWrapper<>();
+        wrapper.ge(AnalyticsDailySummary::getStatDate, start)
+                .le(AnalyticsDailySummary::getStatDate, end)
+                .orderByDesc(AnalyticsDailySummary::getStatDate);
+        return dailySummaryMapper.selectPage(new Page<>(page, size), wrapper);
+    }
+
     private AnalyticsUserStatVo toUserVo(AnalyticsUserStat stat) {
         AnalyticsUserStatVo vo = new AnalyticsUserStatVo();
         BeanUtils.copyProperties(stat, vo);
@@ -80,6 +84,7 @@ public class AnalyticsStatEnrichServiceImpl implements AnalyticsStatEnrichServic
                 vo.setUserName(user.getUserName());
             }
         }
+        vo.setMaskedPhone(maskPhone(stat.getUserPhone()));
         return vo;
     }
 
@@ -101,6 +106,7 @@ public class AnalyticsStatEnrichServiceImpl implements AnalyticsStatEnrichServic
         AnalyticsContentStatVo vo = new AnalyticsContentStatVo();
         BeanUtils.copyProperties(stat, vo);
         vo.setContentTypeName(resolveContentTypeName(stat.getContentType()));
+        vo.setBusinessCategoryName(resolveBusinessCategoryName(stat.getBusinessCategory()));
         vo.setAuthorName(resolveAuthorName(stat.getAuthorType(), stat.getAuthorId()));
         if (stat.getAuditUserId() != null) {
             LifeUser auditor = lifeUserMapper.selectById(stat.getAuditUserId());
@@ -111,6 +117,13 @@ public class AnalyticsStatEnrichServiceImpl implements AnalyticsStatEnrichServic
         return vo;
     }
 
+    private String maskPhone(String phone) {
+        if (!StringUtils.hasText(phone) || phone.length() < 7) {
+            return phone;
+        }
+        return phone.substring(0, 3) + "****" + phone.substring(phone.length() - 4);
+    }
+
     private String resolveAuthorName(Integer authorType, Long authorId) {
         if (authorType == null || authorId == null || authorId <= 0) {
             return null;
@@ -139,6 +152,22 @@ public class AnalyticsStatEnrichServiceImpl implements AnalyticsStatEnrichServic
         }
     }
 
+    private String resolveBusinessCategoryName(Integer businessCategory) {
+        if (businessCategory == null) {
+            return null;
+        }
+        switch (businessCategory) {
+            case AnalyticsBusinessCategory.FOOD: return "美食";
+            case AnalyticsBusinessCategory.LEISURE: return "休闲娱乐";
+            case AnalyticsBusinessCategory.LIFE_SERVICE: return "生活服务";
+            case AnalyticsBusinessCategory.TRAVEL: return "旅游";
+            case AnalyticsBusinessCategory.HOTEL: return "酒店";
+            case AnalyticsBusinessCategory.SHOPPING: return "购物";
+            case AnalyticsBusinessCategory.OTHER: return "其他";
+            default: return "未知";
+        }
+    }
+
     private String resolveShopTypeName(Integer shopType) {
         if (shopType == null) {
             return null;
@@ -151,7 +180,7 @@ public class AnalyticsStatEnrichServiceImpl implements AnalyticsStatEnrichServic
         }
     }
 
-    private String firstNonBlank(String first, String second) {
-        return StringUtils.hasText(first) ? first : second;
+    private String firstNonBlank(String a, String b) {
+        return StringUtils.hasText(a) ? a : b;
     }
 }

+ 61 - 34
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatisticsServiceImpl.java

@@ -9,6 +9,7 @@ import org.springframework.util.StringUtils;
 import shop.alien.entity.analytics.*;
 import shop.alien.mapper.*;
 import shop.alien.store.config.BaseRedisService;
+import shop.alien.store.service.analytics.AnalyticsDetailStoreService;
 import shop.alien.store.service.analytics.AnalyticsStatisticsService;
 import shop.alien.store.util.analytics.AnalyticsDateUtil;
 
@@ -25,11 +26,10 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
 
     private final AnalyticsEventMapper eventMapper;
     private final AnalyticsAiRequestMapper aiRequestMapper;
-    private final AnalyticsUserStatMapper userStatMapper;
-    private final AnalyticsMerchantStatMapper merchantStatMapper;
-    private final AnalyticsContentStatMapper contentStatMapper;
+    private final AnalyticsDetailStoreService detailStoreService;
     private final AnalyticsDailySummaryMapper dailySummaryMapper;
     private final AnalyticsStatJobLogMapper jobLogMapper;
+    private final AnalyticsMerchantStatMapper merchantStatHistoryMapper;
     private final BaseRedisService baseRedisService;
 
     @Override
@@ -106,7 +106,7 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             }
             Long userId = ((Number) userIdObj).longValue();
 
-            AnalyticsUserStat stat = findUserStat(statDate, userId);
+            AnalyticsUserStat stat = detailStoreService.findUserStat(statDate, userId);
             if (stat == null) {
                 stat = new AnalyticsUserStat();
                 stat.setStatDate(statDate);
@@ -125,11 +125,14 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             if (stat.getRegisterTime() == null && countUserEvent(userId, AnalyticsEventCode.USER_REGISTER, start, end) > 0) {
                 stat.setRegisterTime(findFirstEventTimeInRange(userId, AnalyticsEventCode.USER_REGISTER, start, end));
             }
+            if (countUserEvent(userId, AnalyticsEventCode.USER_REGISTER, start, end) > 0) {
+                stat.setIsNewUser(1);
+            }
 
             if (stat.getId() == null) {
-                userStatMapper.insert(stat);
+                detailStoreService.insertUserStat(statDate, stat);
             } else {
-                userStatMapper.updateById(stat);
+                detailStoreService.updateUserStat(statDate, stat);
             }
         }
 
@@ -149,7 +152,7 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             }
             Long merchantId = ((Number) merchantIdObj).longValue();
 
-            AnalyticsMerchantStat stat = findMerchantStat(statDate, merchantId);
+            AnalyticsMerchantStat stat = detailStoreService.findMerchantStat(statDate, merchantId);
             if (stat == null) {
                 stat = new AnalyticsMerchantStat();
                 stat.setStatDate(statDate);
@@ -163,9 +166,9 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             stat.setVerifyConversionRate(AnalyticsDateUtil.calcRate(verifyCount, visitUserCount));
 
             if (stat.getId() == null) {
-                merchantStatMapper.insert(stat);
+                detailStoreService.insertMerchantStat(statDate, stat);
             } else {
-                merchantStatMapper.updateById(stat);
+                detailStoreService.updateMerchantStat(statDate, stat);
             }
         }
 
@@ -193,10 +196,23 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
 
         for (Map.Entry<String, Integer> entry : interactCountMap.entrySet()) {
             String[] parts = entry.getKey().split(":");
-            upsertContentInteraction(Integer.parseInt(parts[0]), Long.parseLong(parts[1]), entry.getValue());
+            upsertContentInteraction(statDate, Integer.parseInt(parts[0]), Long.parseLong(parts[1]), entry.getValue());
+        }
+
+        LambdaQueryWrapper<AnalyticsEvent> publishWrapper = new LambdaQueryWrapper<>();
+        publishWrapper.eq(AnalyticsEvent::getEventCode, AnalyticsEventCode.CONTENT_PUBLISH)
+                .ge(AnalyticsEvent::getEventTime, start)
+                .lt(AnalyticsEvent::getEventTime, end);
+        List<AnalyticsEvent> publishEvents = eventMapper.selectList(publishWrapper);
+        for (AnalyticsEvent event : publishEvents) {
+            if (event.getTargetId() == null || event.getContentType() == null) {
+                continue;
+            }
+            upsertContentPublish(statDate, event);
         }
 
-        log.info("内容明细统计完成: statDate={}, interactGroups={}", statDate, interactCountMap.size());
+        log.info("内容明细统计完成: statDate={}, interactGroups={}, publishEvents={}",
+                statDate, interactCountMap.size(), publishEvents.size());
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -242,7 +258,7 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         Long verifyCount = eventMapper.countMerchantVerify(start, end);
         Long visitUsers = eventMapper.countMerchantViewUsers(start, end);
         summary.setVerifyRate(AnalyticsDateUtil.calcRate(verifyCount, visitUsers));
-        summary.setTotalSettledMerchantCount(toInt(merchantStatMapper.countSettledMerchants(statDate)));
+        summary.setTotalSettledMerchantCount(toInt(merchantStatHistoryMapper.countSettledMerchants(statDate)));
 
         Long paySuccessCount = eventMapper.countByEventCode(AnalyticsEventCode.PAY_SUCCESS, start, end);
         summary.setAvgOrderAmount(calcAvgAmount(summary.getTodayGmv(), toInt(paySuccessCount)));
@@ -275,26 +291,8 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         return summary;
     }
 
-    private AnalyticsUserStat findUserStat(Date statDate, Long userId) {
-        LambdaQueryWrapper<AnalyticsUserStat> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AnalyticsUserStat::getStatDate, statDate).eq(AnalyticsUserStat::getUserId, userId);
-        return userStatMapper.selectOne(wrapper);
-    }
-
-    private AnalyticsMerchantStat findMerchantStat(Date statDate, Long merchantId) {
-        LambdaQueryWrapper<AnalyticsMerchantStat> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AnalyticsMerchantStat::getStatDate, statDate).eq(AnalyticsMerchantStat::getMerchantId, merchantId);
-        return merchantStatMapper.selectOne(wrapper);
-    }
-
-    /**
-     * 从当日事件汇总互动数。取明细上报值与事件汇总值的较大者,避免定时任务重复执行时累加翻倍。
-     */
-    private void upsertContentInteraction(Integer contentType, Long contentId, int eventDayCount) {
-        LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AnalyticsContentStat::getContentType, contentType)
-                .eq(AnalyticsContentStat::getContentId, contentId);
-        AnalyticsContentStat stat = contentStatMapper.selectOne(wrapper);
+    private void upsertContentInteraction(Date statDate, Integer contentType, Long contentId, int eventDayCount) {
+        AnalyticsContentStat stat = detailStoreService.findContentStat(statDate, contentType, contentId);
         if (stat == null) {
             stat = new AnalyticsContentStat();
             stat.setContentType(contentType);
@@ -302,11 +300,40 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             stat.setAuthorType(1);
             stat.setAuthorId(0L);
             stat.setInteractionCount(eventDayCount);
-            contentStatMapper.insert(stat);
+            detailStoreService.insertContentStat(statDate, stat);
             return;
         }
         stat.setInteractionCount(Math.max(defaultInt(stat.getInteractionCount()), eventDayCount));
-        contentStatMapper.updateById(stat);
+        detailStoreService.updateContentStat(statDate, stat);
+    }
+
+    private void upsertContentPublish(Date statDate, AnalyticsEvent event) {
+        Integer contentType = event.getContentType();
+        Long contentId = event.getTargetId();
+        AnalyticsContentStat stat = detailStoreService.findContentStat(statDate, contentType, contentId);
+        if (stat == null) {
+            stat = new AnalyticsContentStat();
+            stat.setContentType(contentType);
+            stat.setContentId(contentId);
+            stat.setAuthorType(event.getMerchantId() != null ? 2 : 1);
+            stat.setAuthorId(event.getMerchantId() != null ? event.getMerchantId() : defaultLong(event.getUserId()));
+            stat.setPublishTime(event.getEventTime());
+            stat.setBusinessCategory(event.getBusinessCategory());
+            stat.setInteractionCount(0);
+            detailStoreService.insertContentStat(statDate, stat);
+            return;
+        }
+        if (stat.getPublishTime() == null) {
+            stat.setPublishTime(event.getEventTime());
+        }
+        if (stat.getBusinessCategory() == null && event.getBusinessCategory() != null) {
+            stat.setBusinessCategory(event.getBusinessCategory());
+        }
+        if (stat.getAuthorId() == null || stat.getAuthorId() <= 0) {
+            stat.setAuthorType(event.getMerchantId() != null ? 2 : 1);
+            stat.setAuthorId(event.getMerchantId() != null ? event.getMerchantId() : defaultLong(event.getUserId()));
+        }
+        detailStoreService.updateContentStat(statDate, stat);
     }
 
     private int countOnlineUsers(Date start, Date end) {

+ 86 - 45
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsTrackServiceImpl.java

@@ -10,6 +10,7 @@ import org.springframework.util.StringUtils;
 import shop.alien.entity.analytics.*;
 import shop.alien.entity.analytics.dto.*;
 import shop.alien.mapper.*;
+import shop.alien.store.service.analytics.AnalyticsDetailStoreService;
 import shop.alien.store.service.analytics.AnalyticsTrackService;
 import shop.alien.store.util.analytics.AnalyticsDateUtil;
 import shop.alien.store.util.analytics.AnalyticsFrontHelper;
@@ -25,9 +26,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
 
     private final AnalyticsEventMapper eventMapper;
     private final AnalyticsAiRequestMapper aiRequestMapper;
-    private final AnalyticsUserStatMapper userStatMapper;
-    private final AnalyticsMerchantStatMapper merchantStatMapper;
-    private final AnalyticsContentStatMapper contentStatMapper;
+    private final AnalyticsDetailStoreService detailStoreService;
     private final AnalyticsAiChatStatMapper aiChatStatMapper;
 
     @Override
@@ -68,6 +67,10 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         if (dto.getDurationMs() == null || dto.getDurationMs() <= 0) {
             throw new IllegalArgumentException("durationMs 必须大于0");
         }
+        if (dto.getUserId() == null) {
+            throw new IllegalArgumentException("userId 不能为空");
+        }
+        Date heartbeatTime = new Date();
         AnalyticsFrontReportDTO report = new AnalyticsFrontReportDTO();
         report.setScene(AnalyticsScene.USER_HEARTBEAT.getScene());
         report.setUserId(dto.getUserId());
@@ -76,6 +79,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         report.setChannel(dto.getChannel());
         report.setCity(dto.getCity());
         trackFromFront(report, request);
+        upsertUserHeartbeat(dto, heartbeatTime);
     }
 
     private void insertEvent(AnalyticsTrackEventDTO dto) {
@@ -91,6 +95,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         event.setDeviceType(dto.getDeviceType());
         event.setChannel(dto.getChannel());
         event.setCity(dto.getCity());
+        event.setBusinessCategory(dto.getBusinessCategory());
         event.setEventTime(dto.getEventTime() != null ? dto.getEventTime() : new Date());
 
         try {
@@ -155,13 +160,13 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         Date statDate = dto.getStatDateOverride() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDateOverride())
                 : (dto.getStatDate() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDate()) : AnalyticsDateUtil.truncateToDate(new Date()));
 
-        AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
+        AnalyticsUserStat existing = detailStoreService.findUserStat(statDate, dto.getUserId());
         if (existing == null) {
-            userStatMapper.insert(buildUserStat(dto, statDate));
+            detailStoreService.insertUserStat(statDate, buildUserStat(dto, statDate));
             return;
         }
         mergeUserStat(existing, dto, statDate);
-        userStatMapper.updateById(existing);
+        detailStoreService.updateUserStat(statDate, existing);
     }
 
     @Override
@@ -173,13 +178,13 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         Date statDate = dto.getStatDateOverride() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDateOverride())
                 : (dto.getStatDate() != null ? AnalyticsDateUtil.truncateToDate(dto.getStatDate()) : AnalyticsDateUtil.truncateToDate(new Date()));
 
-        AnalyticsMerchantStat existing = findMerchantStat(statDate, dto.getMerchantId());
+        AnalyticsMerchantStat existing = detailStoreService.findMerchantStat(statDate, dto.getMerchantId());
         if (existing == null) {
-            merchantStatMapper.insert(buildMerchantStat(dto, statDate));
+            detailStoreService.insertMerchantStat(statDate, buildMerchantStat(dto, statDate));
             return;
         }
         mergeMerchantStat(existing, dto);
-        merchantStatMapper.updateById(existing);
+        detailStoreService.updateMerchantStat(statDate, existing);
     }
 
     @Override
@@ -189,17 +194,17 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             throw new IllegalArgumentException("contentId 与 contentType 不能为空");
         }
 
-        LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AnalyticsContentStat::getContentType, dto.getContentType())
-                .eq(AnalyticsContentStat::getContentId, dto.getContentId());
-        AnalyticsContentStat existing = contentStatMapper.selectOne(wrapper);
+        Date statDate = dto.getPublishTime() != null
+                ? AnalyticsDateUtil.truncateToDate(dto.getPublishTime())
+                : AnalyticsDateUtil.truncateToDate(new Date());
 
+        AnalyticsContentStat existing = detailStoreService.findContentStat(statDate, dto.getContentType(), dto.getContentId());
         if (existing == null) {
-            contentStatMapper.insert(buildContentStat(dto));
+            detailStoreService.insertContentStat(statDate, buildContentStat(dto));
             return;
         }
         mergeContentStat(existing, dto);
-        contentStatMapper.updateById(existing);
+        detailStoreService.updateContentStat(statDate, existing);
     }
 
     @Override
@@ -239,7 +244,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         insertEvent(eventDto);
 
         Date statDate = AnalyticsDateUtil.truncateToDate(registerTime);
-        AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
+        AnalyticsUserStat existing = detailStoreService.findUserStat(statDate, dto.getUserId());
         if (existing == null) {
             AnalyticsUserStat stat = new AnalyticsUserStat();
             stat.setStatDate(statDate);
@@ -250,12 +255,13 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             stat.setLastActiveTime(registerTime);
             stat.setChannel(dto.getChannel());
             stat.setCity(dto.getCity());
+            stat.setIsNewUser(1);
             stat.setOnlineDurationMin(0);
-            userStatMapper.insert(stat);
+            detailStoreService.insertUserStat(statDate, stat);
             return;
         }
         applyUserRegisterMerge(existing, dto.getUserPhone(), registerTime, dto.getChannel(), dto.getCity());
-        userStatMapper.updateById(existing);
+        detailStoreService.updateUserStat(statDate, existing);
     }
 
     @Override
@@ -277,7 +283,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         insertEvent(eventDto);
 
         Date statDate = AnalyticsDateUtil.truncateToDate(activeTime);
-        AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
+        AnalyticsUserStat existing = detailStoreService.findUserStat(statDate, dto.getUserId());
         if (existing == null) {
             AnalyticsUserStat stat = new AnalyticsUserStat();
             stat.setStatDate(statDate);
@@ -287,12 +293,12 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             stat.setCity(dto.getCity());
             stat.setDeviceType(dto.getDeviceName());
             stat.setOnlineDurationMin(0);
-            userStatMapper.insert(stat);
+            detailStoreService.insertUserStat(statDate, stat);
             return;
         }
         applyUserSessionMerge(existing, dto.getFirstLaunchTime(), firstLaunch,
                 activeTime, dto.getCity(), dto.getDeviceName(), null);
-        userStatMapper.updateById(existing);
+        detailStoreService.updateUserStat(statDate, existing);
     }
 
     @Override
@@ -318,7 +324,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         insertEvent(eventDto);
 
         Date statDate = AnalyticsDateUtil.truncateToDate(activeTime);
-        AnalyticsUserStat existing = findUserStat(statDate, dto.getUserId());
+        AnalyticsUserStat existing = detailStoreService.findUserStat(statDate, dto.getUserId());
         if (existing == null) {
             AnalyticsUserStat stat = new AnalyticsUserStat();
             stat.setStatDate(statDate);
@@ -328,12 +334,12 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             stat.setCity(dto.getCity());
             stat.setDeviceType(dto.getDeviceName());
             stat.setOnlineDurationMin(sessionMin);
-            userStatMapper.insert(stat);
+            detailStoreService.insertUserStat(statDate, stat);
             return;
         }
         applyUserSessionMerge(existing, dto.getFirstLaunchTime(), activeTime,
                 activeTime, dto.getCity(), dto.getDeviceName(), sessionMin);
-        userStatMapper.updateById(existing);
+        detailStoreService.updateUserStat(statDate, existing);
     }
 
     @Override
@@ -409,6 +415,9 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             eventDto.setMerchantId(dto.getAuthorId());
         }
         eventDto.setEventTime(dto.getPublishTime());
+        if (dto.getBusinessCategory() != null) {
+            eventDto.setBusinessCategory(dto.getBusinessCategory());
+        }
         insertEvent(eventDto);
 
         AnalyticsContentDetailDTO detail = new AnalyticsContentDetailDTO();
@@ -417,6 +426,8 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         detail.setAuthorType(dto.getAuthorType());
         detail.setAuthorId(dto.getAuthorId());
         detail.setPublishTime(dto.getPublishTime());
+        detail.setContentTitle(dto.getContentTitle());
+        detail.setBusinessCategory(dto.getBusinessCategory());
         upsertContentDetail(detail);
     }
 
@@ -505,6 +516,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         if (StringUtils.hasText(city)) {
             target.setCity(city);
         }
+        target.setIsNewUser(1);
     }
 
     private void applyUserSessionMerge(AnalyticsUserStat target, Date firstLaunchTime,
@@ -527,18 +539,6 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         }
     }
 
-    private AnalyticsUserStat findUserStat(Date statDate, Long userId) {
-        LambdaQueryWrapper<AnalyticsUserStat> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AnalyticsUserStat::getStatDate, statDate).eq(AnalyticsUserStat::getUserId, userId);
-        return userStatMapper.selectOne(wrapper);
-    }
-
-    private AnalyticsMerchantStat findMerchantStat(Date statDate, Long merchantId) {
-        LambdaQueryWrapper<AnalyticsMerchantStat> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AnalyticsMerchantStat::getStatDate, statDate).eq(AnalyticsMerchantStat::getMerchantId, merchantId);
-        return merchantStatMapper.selectOne(wrapper);
-    }
-
     private AnalyticsUserStat buildUserStat(AnalyticsUserDetailDTO dto, Date statDate) {
         AnalyticsUserStat stat = new AnalyticsUserStat();
         stat.setStatDate(statDate);
@@ -550,6 +550,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         stat.setCity(dto.getCity());
         stat.setDeviceType(dto.getDeviceType());
         stat.setChannel(dto.getChannel());
+        stat.setIsNewUser(dto.getIsNewUser());
         stat.setOnlineDurationMin(defaultInt(dto.getOnlineDurationMin()));
         return stat;
     }
@@ -563,6 +564,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         if (StringUtils.hasText(dto.getCity())) target.setCity(dto.getCity());
         if (StringUtils.hasText(dto.getDeviceType())) target.setDeviceType(dto.getDeviceType());
         if (StringUtils.hasText(dto.getChannel())) target.setChannel(dto.getChannel());
+        if (dto.getIsNewUser() != null) target.setIsNewUser(dto.getIsNewUser());
         if (dto.getOnlineDurationMin() != null) target.setOnlineDurationMin(dto.getOnlineDurationMin());
     }
 
@@ -594,6 +596,8 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         stat.setContentType(dto.getContentType());
         stat.setAuthorType(dto.getAuthorType() != null ? dto.getAuthorType() : 1);
         stat.setAuthorId(dto.getAuthorId() != null ? dto.getAuthorId() : 0L);
+        stat.setContentTitle(dto.getContentTitle());
+        stat.setBusinessCategory(dto.getBusinessCategory());
         stat.setPublishTime(dto.getPublishTime());
         stat.setInteractionCount(defaultInt(dto.getInteractionCount()));
         stat.setStatus(dto.getStatus());
@@ -606,6 +610,8 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
     private void mergeContentStat(AnalyticsContentStat target, AnalyticsContentDetailDTO dto) {
         if (dto.getAuthorType() != null) target.setAuthorType(dto.getAuthorType());
         if (dto.getAuthorId() != null) target.setAuthorId(dto.getAuthorId());
+        if (StringUtils.hasText(dto.getContentTitle())) target.setContentTitle(dto.getContentTitle());
+        if (dto.getBusinessCategory() != null) target.setBusinessCategory(dto.getBusinessCategory());
         if (dto.getPublishTime() != null) target.setPublishTime(dto.getPublishTime());
         if (dto.getInteractionCount() != null) target.setInteractionCount(dto.getInteractionCount());
         if (dto.getStatus() != null) target.setStatus(dto.getStatus());
@@ -615,10 +621,8 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
     }
 
     private void addContentInteraction(Integer contentType, Long contentId, int increment) {
-        LambdaQueryWrapper<AnalyticsContentStat> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(AnalyticsContentStat::getContentType, contentType)
-                .eq(AnalyticsContentStat::getContentId, contentId);
-        AnalyticsContentStat existing = contentStatMapper.selectOne(wrapper);
+        Date statDate = AnalyticsDateUtil.truncateToDate(new Date());
+        AnalyticsContentStat existing = detailStoreService.findContentStat(statDate, contentType, contentId);
         if (existing == null) {
             AnalyticsContentStat stat = new AnalyticsContentStat();
             stat.setContentType(contentType);
@@ -626,16 +630,53 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             stat.setAuthorType(1);
             stat.setAuthorId(0L);
             stat.setInteractionCount(Math.max(0, increment));
-            contentStatMapper.insert(stat);
+            detailStoreService.insertContentStat(statDate, stat);
             return;
         }
         int next = defaultInt(existing.getInteractionCount()) + increment;
         existing.setInteractionCount(Math.max(0, next));
-        contentStatMapper.updateById(existing);
+        detailStoreService.updateContentStat(statDate, existing);
+    }
+
+    private void upsertUserHeartbeat(AnalyticsHeartbeatDTO dto, Date heartbeatTime) {
+        Date statDate = AnalyticsDateUtil.truncateToDate(heartbeatTime);
+        int addMin = (int) (dto.getDurationMs() / 60000);
+        if (addMin <= 0) {
+            addMin = 1;
+        }
+
+        AnalyticsUserStat existing = detailStoreService.findUserStat(statDate, dto.getUserId());
+        if (existing == null) {
+            AnalyticsUserStat stat = new AnalyticsUserStat();
+            stat.setUserId(dto.getUserId());
+            stat.setFirstLaunchTime(heartbeatTime);
+            stat.setLastActiveTime(heartbeatTime);
+            stat.setCity(dto.getCity());
+            stat.setDeviceType(dto.getDeviceType());
+            stat.setChannel(dto.getChannel());
+            stat.setOnlineDurationMin(addMin);
+            detailStoreService.insertUserStat(statDate, stat);
+            return;
+        }
+        if (existing.getFirstLaunchTime() == null) {
+            existing.setFirstLaunchTime(heartbeatTime);
+        }
+        existing.setLastActiveTime(heartbeatTime);
+        if (StringUtils.hasText(dto.getCity())) {
+            existing.setCity(dto.getCity());
+        }
+        if (StringUtils.hasText(dto.getDeviceType())) {
+            existing.setDeviceType(dto.getDeviceType());
+        }
+        if (StringUtils.hasText(dto.getChannel())) {
+            existing.setChannel(dto.getChannel());
+        }
+        existing.setOnlineDurationMin(defaultInt(existing.getOnlineDurationMin()) + addMin);
+        detailStoreService.updateUserStat(statDate, existing);
     }
 
     private void addMerchantVisit(Date statDate, Long merchantId, Integer shopType, int visitUvDelta, int visitPvDelta) {
-        AnalyticsMerchantStat existing = findMerchantStat(statDate, merchantId);
+        AnalyticsMerchantStat existing = detailStoreService.findMerchantStat(statDate, merchantId);
         if (existing == null) {
             AnalyticsMerchantStat stat = new AnalyticsMerchantStat();
             stat.setStatDate(statDate);
@@ -643,7 +684,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             stat.setShopType(shopType);
             stat.setVisitUv(Math.max(0, visitUvDelta));
             stat.setVisitPv(Math.max(0, visitPvDelta));
-            merchantStatMapper.insert(stat);
+            detailStoreService.insertMerchantStat(statDate, stat);
             return;
         }
         if (shopType != null) {
@@ -651,7 +692,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         }
         existing.setVisitUv(defaultInt(existing.getVisitUv()) + visitUvDelta);
         existing.setVisitPv(defaultInt(existing.getVisitPv()) + visitPvDelta);
-        merchantStatMapper.updateById(existing);
+        detailStoreService.updateMerchantStat(statDate, existing);
     }
 
     private AnalyticsAiChatStat buildAiChatStat(AnalyticsAiChatDetailDTO dto) {