Explorar el Código

埋点历史统计接口 以及图表接口 开发

lutong hace 1 día
padre
commit
21b73f154a
Se han modificado 75 ficheros con 4411 adiciones y 594 borrados
  1. 1 1
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsAiChatStat.java
  2. 3 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsAiChatStatHistory.java
  3. 3 1
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsCategoryDaily.java
  4. 5 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsCategoryDailyToday.java
  5. 7 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsContentStatHistory.java
  6. 9 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsEventCode.java
  7. 24 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsEventSubtype.java
  8. 5 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsMerchantStatToday.java
  9. 3 1
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsReportRecord.java
  10. 3 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsReportRecordHistory.java
  11. 13 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsScene.java
  12. 31 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsStatPeriod.java
  13. 1 1
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsUserStat.java
  14. 5 2
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsUserStatToday.java
  15. 9 0
      alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsFrontReportDTO.java
  16. 6 0
      alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsTrackEventDTO.java
  17. 9 0
      alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsUserRegisterDTO.java
  18. 26 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsAuditTrendVo.java
  19. 31 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentInteractionRankVo.java
  20. 32 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportChartsVo.java
  21. 32 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportDetailVo.java
  22. 33 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportSummaryVo.java
  23. 17 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsAvgDurationTrendVo.java
  24. 18 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsContentTrendVo.java
  25. 18 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsConversationTrendVo.java
  26. 36 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsDashboardChartsVo.java
  27. 36 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsDashboardSummaryVo.java
  28. 24 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsFunnelStageVo.java
  29. 21 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsRetentionPointVo.java
  30. 20 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsTrendPointVo.java
  31. 21 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsUserActiveTrendVo.java
  32. 21 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsAvgOrderTrendVo.java
  33. 18 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsGmvTrendVo.java
  34. 30 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantGmvRankVo.java
  35. 31 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantReportChartsVo.java
  36. 36 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantReportDetailVo.java
  37. 36 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantReportSummaryVo.java
  38. 24 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsDistributionItemVo.java
  39. 38 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserReportChartsVo.java
  40. 35 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserReportDetailVo.java
  41. 36 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserReportSummaryVo.java
  42. 27 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserTypeRatioVo.java
  43. 4 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsAiRequestMapper.java
  44. 31 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsContentReportMapper.java
  45. 45 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsDashboardMapper.java
  46. 19 2
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsEventMapper.java
  47. 34 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantReportMapper.java
  48. 14 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantStatMapper.java
  49. 34 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsUserReportMapper.java
  50. 446 0
      alien-entity/src/main/resources/db/migration/analytics_schema.sql
  51. 5 240
      alien-entity/src/main/resources/db/migration/analytics_tables.sql
  52. 64 45
      alien-entity/src/main/resources/db/migration/analytics_tables_dashboard_upgrade.sql
  53. 2 262
      alien-entity/src/main/resources/db/migration/analytics_today_history_tables.sql
  54. 107 0
      alien-entity/src/main/resources/mapper/AnalyticsContentReportMapper.xml
  55. 127 0
      alien-entity/src/main/resources/mapper/AnalyticsDashboardMapper.xml
  56. 130 0
      alien-entity/src/main/resources/mapper/AnalyticsMerchantReportMapper.xml
  57. 110 0
      alien-entity/src/main/resources/mapper/AnalyticsUserReportMapper.xml
  58. 8 4
      alien-store/doc/analytics-front-sdk.js
  59. 135 25
      alien-store/doc/前端埋点接入指南.md
  60. 83 0
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsContentReportController.java
  61. 96 0
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsDashboardController.java
  62. 89 0
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsMerchantReportController.java
  63. 102 0
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsUserReportController.java
  64. 24 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsContentReportService.java
  65. 27 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsDashboardService.java
  66. 24 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsMerchantReportService.java
  67. 31 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsUserReportService.java
  68. 293 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsContentReportServiceImpl.java
  69. 464 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDashboardServiceImpl.java
  70. 353 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsMerchantReportServiceImpl.java
  71. 164 8
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatisticsServiceImpl.java
  72. 67 2
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsTrackServiceImpl.java
  73. 316 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsUserReportServiceImpl.java
  74. 86 0
      alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsPeriodContext.java
  75. 43 0
      alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsTrendFillUtil.java

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

@@ -12,7 +12,7 @@ import java.util.Date;
 /**
  * AI 对话明细统计实体。
  *
- * <p>表名:analytics_ai_chat_stat</p>
+ * <p>表名:analytics_ai_chat_stat_today(零点归档至 analytics_ai_chat_stat_history)</p>
  * <p>字段:对话ID、用户ID、开始时间、消息数、AI响应时长(ms)</p>
  */
 @Data

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

@@ -7,6 +7,9 @@ import lombok.Data;
 
 import java.util.Date;
 
+/**
+ * AI对话明细-历史表(今日表见 AnalyticsAiChatStat)。
+ */
 @Data
 @JsonInclude
 @TableName("analytics_ai_chat_stat_history")

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

@@ -11,7 +11,9 @@ import java.math.BigDecimal;
 import java.util.Date;
 
 /**
- * 经营品类日统计(商家 GMV 占比、内容品类分布趋势)。
+ * 经营品类日统计-历史表(今日表见 AnalyticsCategoryDailyToday)。
+ *
+ * <p>表名:analytics_category_daily_history</p>
  */
 @Data
 @JsonInclude

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

@@ -2,13 +2,18 @@ package shop.alien.entity.analytics;
 
 import com.baomidou.mybatisplus.annotation.*;
 import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
 import lombok.Data;
 
 import java.math.BigDecimal;
 
+/**
+ * 经营品类日统计-今日表(零点归档至 analytics_category_daily_history)。
+ */
 @Data
 @JsonInclude
 @TableName("analytics_category_daily_today")
+@ApiModel(value = "AnalyticsCategoryDailyToday", description = "经营品类日统计-今日")
 public class AnalyticsCategoryDailyToday {
 
     @TableId(value = "id", type = IdType.AUTO)

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

@@ -3,13 +3,20 @@ 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 lombok.Data;
 
 import java.util.Date;
 
+/**
+ * 内容明细-历史表(今日表见 AnalyticsContentStat)。
+ *
+ * <p>表名:analytics_content_stat_history</p>
+ */
 @Data
 @JsonInclude
 @TableName("analytics_content_stat_history")
+@ApiModel(value = "AnalyticsContentStatHistory", description = "内容明细-历史")
 public class AnalyticsContentStatHistory {
 
     @TableId(value = "id", type = IdType.AUTO)

+ 9 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsEventCode.java

@@ -10,10 +10,18 @@ public final class AnalyticsEventCode {
 
     public static final String USER_LAUNCH = "user.launch";
     public static final String USER_REGISTER = "user.register";
+    public static final String USER_REGISTER_PAGE = "user.register.page";
+    public static final String USER_REGISTER_PHONE = "user.register.phone";
+    public static final String USER_REGISTER_OTP = "user.register.otp";
+    public static final String USER_REGISTER_PASSWORD = "user.register.password";
     public static final String USER_LOGIN = "user.login";
     public static final String USER_LOGOUT = "user.logout";
     public static final String USER_HEARTBEAT = "user.heartbeat";
 
+    public static final String MERCHANT_EXPOSE = "merchant.expose";
+    public static final String MERCHANT_CLICK = "merchant.click";
+    public static final String MERCHANT_DETAIL = "merchant.detail";
+    public static final String MERCHANT_CONTACT = "merchant.contact";
     public static final String MERCHANT_VIEW = "merchant.view";
     public static final String MERCHANT_VERIFY = "merchant.verify";
 
@@ -24,6 +32,7 @@ public final class AnalyticsEventCode {
 
     public static final String AUDIT_SUBMIT = "audit.submit";
     public static final String AUDIT_PASS = "audit.pass";
+    public static final String AUDIT_REJECT = "audit.reject";
 
     public static final String REPORT_SUBMIT = "report.submit";
     public static final String REPORT_HANDLE = "report.handle";

+ 24 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsEventSubtype.java

@@ -0,0 +1,24 @@
+package shop.alien.entity.analytics;
+
+/**
+ * 埋点事件子类型(配合 event_subtype 字段)
+ */
+public final class AnalyticsEventSubtype {
+
+    private AnalyticsEventSubtype() {
+    }
+
+    /** 内容互动-点赞 */
+    public static final String INTERACT_LIKE = "like";
+    /** 内容互动-评论 */
+    public static final String INTERACT_COMMENT = "comment";
+    /** 内容互动-分享 */
+    public static final String INTERACT_SHARE = "share";
+
+    /** 审核-AI通过 */
+    public static final String AUDIT_AI_PASS = "ai_pass";
+    /** 审核-人工通过 */
+    public static final String AUDIT_MANUAL_PASS = "manual_pass";
+    /** 审核-驳回 */
+    public static final String AUDIT_REJECT = "reject";
+}

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

@@ -3,14 +3,19 @@ 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 lombok.Data;
 
 import java.math.BigDecimal;
 import java.util.Date;
 
+/**
+ * 商户明细-今日表(零点归档至 analytics_merchant_stat_history)。
+ */
 @Data
 @JsonInclude
 @TableName("analytics_merchant_stat_today")
+@ApiModel(value = "AnalyticsMerchantStatToday", description = "商户明细-今日")
 public class AnalyticsMerchantStatToday {
 
     @TableId(value = "id", type = IdType.AUTO)

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

@@ -10,7 +10,9 @@ import lombok.Data;
 import java.util.Date;
 
 /**
- * 举报处理明细(内容报表列表)。
+ * 举报处理明细-今日表(零点归档至 analytics_report_record_history)。
+ *
+ * <p>表名:analytics_report_record_today</p>
  */
 @Data
 @JsonInclude

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

@@ -7,6 +7,9 @@ import lombok.Data;
 
 import java.util.Date;
 
+/**
+ * 举报明细-历史表(今日表见 AnalyticsReportRecord)。
+ */
 @Data
 @JsonInclude
 @TableName("analytics_report_record_history")

+ 13 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsScene.java

@@ -21,9 +21,18 @@ public enum AnalyticsScene {
     USER_LOGOUT("USER_LOGOUT", "用户登出", AnalyticsEventCode.USER_LOGOUT, "USER"),
     USER_HEARTBEAT("USER_HEARTBEAT", "在线心跳", AnalyticsEventCode.USER_HEARTBEAT, "USER"),
 
+    MERCHANT_EXPOSE("MERCHANT_EXPOSE", "商户曝光", AnalyticsEventCode.MERCHANT_EXPOSE, "MERCHANT"),
+    MERCHANT_CLICK("MERCHANT_CLICK", "商户点击", AnalyticsEventCode.MERCHANT_CLICK, "MERCHANT"),
+    MERCHANT_DETAIL("MERCHANT_DETAIL", "商户详情", AnalyticsEventCode.MERCHANT_DETAIL, "MERCHANT"),
+    MERCHANT_CONTACT("MERCHANT_CONTACT", "电话/导航", AnalyticsEventCode.MERCHANT_CONTACT, "MERCHANT"),
     MERCHANT_VIEW("MERCHANT_VIEW", "访问商户", AnalyticsEventCode.MERCHANT_VIEW, "MERCHANT"),
     MERCHANT_VERIFY("MERCHANT_VERIFY", "商户核销", AnalyticsEventCode.MERCHANT_VERIFY, "MERCHANT"),
 
+    REGISTER_PAGE("REGISTER_PAGE", "进入注册页", AnalyticsEventCode.USER_REGISTER_PAGE, "USER"),
+    REGISTER_PHONE("REGISTER_PHONE", "提交手机号", AnalyticsEventCode.USER_REGISTER_PHONE, "USER"),
+    REGISTER_OTP("REGISTER_OTP", "验证码通过", AnalyticsEventCode.USER_REGISTER_OTP, "USER"),
+    REGISTER_PASSWORD("REGISTER_PASSWORD", "设置密码", AnalyticsEventCode.USER_REGISTER_PASSWORD, "USER"),
+
     CONTENT_PUBLISH("CONTENT_PUBLISH", "发布内容", AnalyticsEventCode.CONTENT_PUBLISH, "CONTENT"),
     CONTENT_INTERACT("CONTENT_INTERACT", "内容互动", AnalyticsEventCode.CONTENT_INTERACT, "CONTENT"),
 
@@ -31,6 +40,7 @@ public enum AnalyticsScene {
 
     AUDIT_SUBMIT("AUDIT_SUBMIT", "提交审核", AnalyticsEventCode.AUDIT_SUBMIT, "SYSTEM"),
     AUDIT_PASS("AUDIT_PASS", "审核通过", AnalyticsEventCode.AUDIT_PASS, "SYSTEM"),
+    AUDIT_REJECT("AUDIT_REJECT", "审核驳回", AnalyticsEventCode.AUDIT_REJECT, "SYSTEM"),
 
     REPORT_SUBMIT("REPORT_SUBMIT", "提交举报", AnalyticsEventCode.REPORT_SUBMIT, "SYSTEM"),
     REPORT_HANDLE("REPORT_HANDLE", "举报处理", AnalyticsEventCode.REPORT_HANDLE, "SYSTEM"),
@@ -104,6 +114,9 @@ public enum AnalyticsScene {
         fields.put("deviceType", "设备");
         fields.put("channel", "渠道");
         fields.put("city", "城市");
+        fields.put("eventSubtype", "事件子类型(内容互动like/comment/share;审核ai_pass/manual_pass/reject)");
+        fields.put("businessCategory", "经营品类(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)");
+        fields.put("shopType", "店铺类型(1美食2休闲娱乐3生活服务)");
         return Collections.unmodifiableMap(fields);
     }
 }

+ 31 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsStatPeriod.java

@@ -0,0 +1,31 @@
+package shop.alien.entity.analytics;
+
+/**
+ * 看板统计周期
+ */
+public final class AnalyticsStatPeriod {
+
+    private AnalyticsStatPeriod() {
+    }
+
+    public static final String TODAY = "TODAY";
+    public static final String YESTERDAY = "YESTERDAY";
+    public static final String LAST_7D = "LAST_7D";
+    public static final String LAST_30D = "LAST_30D";
+
+    public static String normalize(String period) {
+        if (period == null || period.trim().isEmpty()) {
+            return TODAY;
+        }
+        String p = period.trim().toUpperCase();
+        switch (p) {
+            case TODAY:
+            case YESTERDAY:
+            case LAST_7D:
+            case LAST_30D:
+                return p;
+            default:
+                throw new IllegalArgumentException("不支持的 period: " + period + ",可选 TODAY/YESTERDAY/LAST_7D/LAST_30D");
+        }
+    }
+}

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

@@ -80,7 +80,7 @@ public class AnalyticsUserStat {
     @TableField("channel")
     private String channel;
 
-    @ApiModelProperty("是否新用户(7天内注册)")
+    @ApiModelProperty("是否当日新注册用户(0否1是)")
     @TableField("is_new_user")
     private Integer isNewUser;
 

+ 5 - 2
alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsUserStatToday.java

@@ -9,16 +9,17 @@ import lombok.Data;
 
 import java.util.Date;
 
-/** 用户明细-今日表(零点归档至 history) */
+/** 用户明细-今日表(零点归档至 analytics_user_stat_history) */
 @Data
 @JsonInclude
 @TableName("analytics_user_stat_today")
-@ApiModel("AnalyticsUserStatToday")
+@ApiModel(value = "AnalyticsUserStatToday", description = "用户明细-今日")
 public class AnalyticsUserStatToday {
 
     @TableId(value = "id", type = IdType.AUTO)
     private Long id;
 
+    @ApiModelProperty("用户ID")
     @TableField("user_id")
     private Long userId;
 
@@ -58,9 +59,11 @@ public class AnalyticsUserStatToday {
     @TableField("channel")
     private String channel;
 
+    @ApiModelProperty("是否当日新注册用户(0否1是)")
     @TableField("is_new_user")
     private Integer isNewUser;
 
+    @ApiModelProperty("当日在线时长(分钟)")
     @TableField("online_duration_min")
     private Integer onlineDurationMin;
 }

+ 9 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsFrontReportDTO.java

@@ -51,6 +51,15 @@ public class AnalyticsFrontReportDTO {
     @ApiModelProperty("城市")
     private String city;
 
+    @ApiModelProperty("事件子类型(互动like/comment/share;审核reject等)")
+    private String eventSubtype;
+
+    @ApiModelProperty("经营品类(1-7)")
+    private Integer businessCategory;
+
+    @ApiModelProperty("店铺类型(1美食2休闲娱乐3生活服务)")
+    private Integer shopType;
+
     @ApiModelProperty("事件时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date eventTime;

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

@@ -38,6 +38,12 @@ public class AnalyticsTrackEventDTO {
     @ApiModelProperty("经营品类")
     private Integer businessCategory;
 
+    @ApiModelProperty("店铺类型")
+    private Integer shopType;
+
+    @ApiModelProperty("事件子类型")
+    private String eventSubtype;
+
     @ApiModelProperty("金额")
     private BigDecimal amount;
 

+ 9 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsUserRegisterDTO.java

@@ -33,6 +33,15 @@ public class AnalyticsUserRegisterDTO {
     @ApiModelProperty("城市")
     private String city;
 
+    @ApiModelProperty("性别(0未知1男2女)")
+    private Integer gender;
+
+    @ApiModelProperty("年龄")
+    private Integer age;
+
+    @ApiModelProperty("年龄段")
+    private String ageGroup;
+
     @ApiModelProperty("事件唯一ID(幂等,可选)")
     private String eventId;
 }

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsAuditTrendVo.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.analytics.vo.contentreport;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsTrendPointVo;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("内容审核趋势点")
+public class AnalyticsAuditTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("提交审核")
+    private Integer submitCount;
+
+    @ApiModelProperty("审核通过")
+    private Integer passCount;
+
+    @ApiModelProperty("未通过")
+    private Integer failCount;
+
+    @ApiModelProperty("驳回")
+    private Integer rejectCount;
+}

+ 31 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentInteractionRankVo.java

@@ -0,0 +1,31 @@
+package shop.alien.entity.analytics.vo.contentreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+@Data
+@ApiModel("内容互动排行")
+public class AnalyticsContentInteractionRankVo {
+
+    @ApiModelProperty("排名")
+    private Integer rank;
+
+    @ApiModelProperty("内容ID")
+    private Long contentId;
+
+    @ApiModelProperty("内容标题")
+    private String contentTitle;
+
+    @ApiModelProperty("浏览/互动总量")
+    private Integer interactionCount;
+
+    @ApiModelProperty("点赞数")
+    private Integer likeCount;
+
+    @ApiModelProperty("评论数")
+    private Integer commentCount;
+
+    @ApiModelProperty("分享数")
+    private Integer shareCount;
+}

+ 32 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportChartsVo.java

@@ -0,0 +1,32 @@
+package shop.alien.entity.analytics.vo.contentreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+
+import java.util.List;
+
+@Data
+@ApiModel("内容报表汇总")
+public class AnalyticsContentReportChartsVo {
+
+    @ApiModelProperty("统计周期")
+    private String period;
+
+    @ApiModelProperty("顶部指标")
+    private AnalyticsContentReportSummaryVo summary;
+
+    @ApiModelProperty("内容分类分布")
+    private List<AnalyticsDistributionItemVo> categoryDistribution;
+
+    @ApiModelProperty("内容审核趋势")
+    private List<AnalyticsAuditTrendVo> auditTrend;
+
+    @ApiModelProperty("内容互动TOP10")
+    private List<AnalyticsContentInteractionRankVo> interactionTop10;
+
+    @ApiModelProperty("近期审核通过列表(默认第1页)")
+    private IPage<AnalyticsContentReportDetailVo> auditPassedPage;
+}

+ 32 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportDetailVo.java

@@ -0,0 +1,32 @@
+package shop.alien.entity.analytics.vo.contentreport;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("近期审核通过内容")
+public class AnalyticsContentReportDetailVo {
+
+    @ApiModelProperty("展示内容ID(如R001)")
+    private String displayContentId;
+
+    @ApiModelProperty("内容ID")
+    private Long contentId;
+
+    @ApiModelProperty("内容标题")
+    private String contentTitle;
+
+    @ApiModelProperty("内容类型名称")
+    private String contentTypeName;
+
+    @ApiModelProperty("状态(已通过/发布中)")
+    private String status;
+
+    @ApiModelProperty("审核/发布时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date auditTime;
+}

+ 33 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportSummaryVo.java

@@ -0,0 +1,33 @@
+package shop.alien.entity.analytics.vo.contentreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("内容报表顶部指标")
+public class AnalyticsContentReportSummaryVo {
+
+    @ApiModelProperty("内容发布数")
+    private Integer publishCount;
+
+    @ApiModelProperty("内容发布数环比变化(%)")
+    private BigDecimal publishCountChangeRate;
+
+    @ApiModelProperty("审核通过率(%)")
+    private BigDecimal auditPassRate;
+
+    @ApiModelProperty("审核通过率环比变化(百分点)")
+    private BigDecimal auditPassRateDelta;
+
+    @ApiModelProperty("内容互动/访问数")
+    private Integer visitCount;
+
+    @ApiModelProperty("内容互动数环比变化(%)")
+    private BigDecimal visitCountChangeRate;
+
+    @ApiModelProperty("审核处理率(%)")
+    private BigDecimal auditProcessRate;
+}

+ 17 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsAvgDurationTrendVo.java

@@ -0,0 +1,17 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("人均使用时长趋势")
+public class AnalyticsAvgDurationTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("人均使用时长(分钟)")
+    private BigDecimal avgDurationMin;
+}

+ 18 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsContentTrendVo.java

@@ -0,0 +1,18 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("内容互动趋势")
+public class AnalyticsContentTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("发布量")
+    private Integer publishCount;
+
+    @ApiModelProperty("互动量")
+    private Integer interactionCount;
+}

+ 18 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsConversationTrendVo.java

@@ -0,0 +1,18 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("对话数据趋势")
+public class AnalyticsConversationTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("对话次数")
+    private Integer chatCount;
+
+    @ApiModelProperty("对话人数")
+    private Integer chatUserCount;
+}

+ 36 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsDashboardChartsVo.java

@@ -0,0 +1,36 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@ApiModel("数据看板六图汇总")
+public class AnalyticsDashboardChartsVo {
+
+    @ApiModelProperty("统计周期")
+    private String period;
+
+    @ApiModelProperty("顶部指标卡片")
+    private AnalyticsDashboardSummaryVo summary;
+
+    @ApiModelProperty("用户活跃趋势")
+    private List<AnalyticsUserActiveTrendVo> userActiveTrend;
+
+    @ApiModelProperty("对话数据趋势")
+    private List<AnalyticsConversationTrendVo> conversationTrend;
+
+    @ApiModelProperty("内容互动趋势")
+    private List<AnalyticsContentTrendVo> contentTrend;
+
+    @ApiModelProperty("转化漏斗")
+    private List<AnalyticsFunnelStageVo> conversionFunnel;
+
+    @ApiModelProperty("用户留存率")
+    private List<AnalyticsRetentionPointVo> userRetention;
+
+    @ApiModelProperty("人均使用时长")
+    private List<AnalyticsAvgDurationTrendVo> avgUsageDuration;
+}

+ 36 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsDashboardSummaryVo.java

@@ -0,0 +1,36 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("数据看板顶部指标")
+public class AnalyticsDashboardSummaryVo {
+
+    @ApiModelProperty("DAU")
+    private Integer dau;
+
+    @ApiModelProperty("新增用户数")
+    private Integer newUserCount;
+
+    @ApiModelProperty("对话次数")
+    private Integer aiChatCount;
+
+    @ApiModelProperty("内容发布数")
+    private Integer contentPublishCount;
+
+    @ApiModelProperty("商家访问UV")
+    private Integer merchantVisitUv;
+
+    @ApiModelProperty("AI平均响应时间(毫秒)")
+    private Long aiAvgResponseMs;
+
+    @ApiModelProperty("当前在线人数(仅今日/昨日有效)")
+    private Integer onlineUserCount;
+
+    @ApiModelProperty("订单转化率(%)")
+    private BigDecimal conversionRate;
+}

+ 24 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsFunnelStageVo.java

@@ -0,0 +1,24 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("转化漏斗阶段")
+public class AnalyticsFunnelStageVo {
+
+    @ApiModelProperty("阶段编码")
+    private String stageCode;
+
+    @ApiModelProperty("阶段名称")
+    private String stageName;
+
+    @ApiModelProperty("数量")
+    private Long count;
+
+    @ApiModelProperty("相对上一阶段转化率(%)")
+    private BigDecimal conversionRate;
+}

+ 21 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsRetentionPointVo.java

@@ -0,0 +1,21 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("用户留存点")
+public class AnalyticsRetentionPointVo {
+
+    @ApiModelProperty("留存日(1/2/3/4/5/6/7/14/30)")
+    private Integer dayIndex;
+
+    @ApiModelProperty("留存率(%)")
+    private BigDecimal retentionRate;
+
+    @ApiModelProperty("留存用户数")
+    private Integer retainedCount;
+}

+ 20 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsTrendPointVo.java

@@ -0,0 +1,20 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("趋势图数据点")
+public class AnalyticsTrendPointVo {
+
+    @ApiModelProperty("横轴标签(日期或小时)")
+    private String label;
+
+    @ApiModelProperty("统计日期(按日粒度时有值)")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date statDate;
+}

+ 21 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsUserActiveTrendVo.java

@@ -0,0 +1,21 @@
+package shop.alien.entity.analytics.vo.dashboard;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("用户活跃趋势")
+public class AnalyticsUserActiveTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("DAU")
+    private Integer dau;
+
+    @ApiModelProperty("WAU(当日往前7日去重)")
+    private Integer wau;
+
+    @ApiModelProperty("MAU(当日往前30日去重)")
+    private Integer mau;
+}

+ 21 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsAvgOrderTrendVo.java

@@ -0,0 +1,21 @@
+package shop.alien.entity.analytics.vo.merchantreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsTrendPointVo;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("商家平均单量趋势点")
+public class AnalyticsAvgOrderTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("客单价(元)")
+    private BigDecimal avgOrderAmount;
+
+    @ApiModelProperty("场均支付笔数")
+    private BigDecimal avgPayCount;
+}

+ 18 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsGmvTrendVo.java

@@ -0,0 +1,18 @@
+package shop.alien.entity.analytics.vo.merchantreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsTrendPointVo;
+
+import java.math.BigDecimal;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ApiModel("GMV趋势点")
+public class AnalyticsGmvTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("GMV(元)")
+    private BigDecimal gmv;
+}

+ 30 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantGmvRankVo.java

@@ -0,0 +1,30 @@
+package shop.alien.entity.analytics.vo.merchantreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("商家GMV排行")
+public class AnalyticsMerchantGmvRankVo {
+
+    @ApiModelProperty("排名")
+    private Integer rank;
+
+    @ApiModelProperty("商家ID")
+    private Long merchantId;
+
+    @ApiModelProperty("商家名称")
+    private String merchantName;
+
+    @ApiModelProperty("分类名称")
+    private String categoryName;
+
+    @ApiModelProperty("店铺类型名称")
+    private String shopTypeName;
+
+    @ApiModelProperty("GMV(元)")
+    private BigDecimal gmv;
+}

+ 31 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantReportChartsVo.java

@@ -0,0 +1,31 @@
+package shop.alien.entity.analytics.vo.merchantreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+
+import java.util.List;
+
+@Data
+@ApiModel("商家报表汇总")
+public class AnalyticsMerchantReportChartsVo {
+
+    @ApiModelProperty("统计周期")
+    private String period;
+
+    @ApiModelProperty("顶部指标")
+    private AnalyticsMerchantReportSummaryVo summary;
+
+    @ApiModelProperty("GMV趋势")
+    private List<AnalyticsGmvTrendVo> gmvTrend;
+
+    @ApiModelProperty("各类商家GMV占比")
+    private List<AnalyticsDistributionItemVo> gmvCategoryDistribution;
+
+    @ApiModelProperty("商家平均单量趋势")
+    private List<AnalyticsAvgOrderTrendVo> avgOrderTrend;
+
+    @ApiModelProperty("商家GMV TOP10")
+    private List<AnalyticsMerchantGmvRankVo> gmvTop10;
+}

+ 36 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantReportDetailVo.java

@@ -0,0 +1,36 @@
+package shop.alien.entity.analytics.vo.merchantreport;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@ApiModel("入驻商家明细")
+public class AnalyticsMerchantReportDetailVo {
+
+    @ApiModelProperty("展示商家ID(如M1001)")
+    private String displayMerchantId;
+
+    @ApiModelProperty("商家ID")
+    private Long merchantId;
+
+    @ApiModelProperty("商家名称")
+    private String merchantName;
+
+    @ApiModelProperty("分类名称")
+    private String categoryName;
+
+    @ApiModelProperty("入驻时间")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date settleTime;
+
+    @ApiModelProperty("周期内GMV(元)")
+    private BigDecimal periodGmv;
+
+    @ApiModelProperty("状态(营业中/暂停营业等)")
+    private String status;
+}

+ 36 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantReportSummaryVo.java

@@ -0,0 +1,36 @@
+package shop.alien.entity.analytics.vo.merchantreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("商家报表顶部指标")
+public class AnalyticsMerchantReportSummaryVo {
+
+    @ApiModelProperty("入驻商家总数")
+    private Integer totalSettledMerchantCount;
+
+    @ApiModelProperty("入驻商家较上期变化")
+    private Integer totalSettledMerchantDelta;
+
+    @ApiModelProperty("GMV(元)")
+    private BigDecimal gmv;
+
+    @ApiModelProperty("GMV环比变化(%)")
+    private BigDecimal gmvChangeRate;
+
+    @ApiModelProperty("核销率(%)")
+    private BigDecimal verifyRate;
+
+    @ApiModelProperty("核销率环比变化(百分点)")
+    private BigDecimal verifyRateDelta;
+
+    @ApiModelProperty("客单价(元)")
+    private BigDecimal avgOrderAmount;
+
+    @ApiModelProperty("客单价较上期变化(元)")
+    private BigDecimal avgOrderAmountDelta;
+}

+ 24 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsDistributionItemVo.java

@@ -0,0 +1,24 @@
+package shop.alien.entity.analytics.vo.userreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("分布项(性别/年龄/地域)")
+public class AnalyticsDistributionItemVo {
+
+    @ApiModelProperty("编码")
+    private String code;
+
+    @ApiModelProperty("名称")
+    private String name;
+
+    @ApiModelProperty("数量")
+    private Long count;
+
+    @ApiModelProperty("占比(%)")
+    private BigDecimal ratio;
+}

+ 38 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserReportChartsVo.java

@@ -0,0 +1,38 @@
+package shop.alien.entity.analytics.vo.userreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsFunnelStageVo;
+
+import java.util.List;
+
+@Data
+@ApiModel("用户报表汇总")
+public class AnalyticsUserReportChartsVo {
+
+    @ApiModelProperty("统计周期")
+    private String period;
+
+    @ApiModelProperty("顶部指标")
+    private AnalyticsUserReportSummaryVo summary;
+
+    @ApiModelProperty("注册转化漏斗")
+    private List<AnalyticsFunnelStageVo> registerFunnel;
+
+    @ApiModelProperty("性别分布")
+    private List<AnalyticsDistributionItemVo> genderDistribution;
+
+    @ApiModelProperty("年龄分布")
+    private List<AnalyticsDistributionItemVo> ageDistribution;
+
+    @ApiModelProperty("新/老用户占比")
+    private AnalyticsUserTypeRatioVo userTypeRatio;
+
+    @ApiModelProperty("地域分布TOP10")
+    private List<AnalyticsDistributionItemVo> geoTop10;
+
+    @ApiModelProperty("新注册用户明细(默认第1页)")
+    private IPage<AnalyticsUserReportDetailVo> newUserPage;
+}

+ 35 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserReportDetailVo.java

@@ -0,0 +1,35 @@
+package shop.alien.entity.analytics.vo.userreport;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@ApiModel("新注册用户明细行")
+public class AnalyticsUserReportDetailVo {
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("展示用户ID(如U1001)")
+    private String displayUserId;
+
+    @ApiModelProperty("脱敏手机号")
+    private String maskedPhone;
+
+    @ApiModelProperty("注册时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date registerTime;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("渠道")
+    private String channel;
+
+    @ApiModelProperty("状态(正常/待验证/已注销/封禁)")
+    private String status;
+}

+ 36 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserReportSummaryVo.java

@@ -0,0 +1,36 @@
+package shop.alien.entity.analytics.vo.userreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("用户报表顶部指标")
+public class AnalyticsUserReportSummaryVo {
+
+    @ApiModelProperty("累计注册用户")
+    private Integer totalRegisterUserCount;
+
+    @ApiModelProperty("累计注册用户较上期变化")
+    private Integer totalRegisterUserDelta;
+
+    @ApiModelProperty("近7日新增用户")
+    private Integer last7dNewUserCount;
+
+    @ApiModelProperty("近7日新增用户较上期变化")
+    private Integer last7dNewUserDelta;
+
+    @ApiModelProperty("近30日活跃用户")
+    private Integer last30dActiveUserCount;
+
+    @ApiModelProperty("近30日活跃用户较上期变化")
+    private Integer last30dActiveUserDelta;
+
+    @ApiModelProperty("次日留存率(%)")
+    private BigDecimal nextDayRetentionRate;
+
+    @ApiModelProperty("次日留存率较上期变化(百分点)")
+    private BigDecimal nextDayRetentionRateDelta;
+}

+ 27 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserTypeRatioVo.java

@@ -0,0 +1,27 @@
+package shop.alien.entity.analytics.vo.userreport;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+@Data
+@ApiModel("新/老用户占比")
+public class AnalyticsUserTypeRatioVo {
+
+    @ApiModelProperty("新用户数")
+    private Long newUserCount;
+
+    @ApiModelProperty("活跃用户数(含新老)")
+    private Long activeUserCount;
+
+    @ApiModelProperty("老用户数")
+    private Long oldUserCount;
+
+    @ApiModelProperty("新用户占比(%)")
+    private BigDecimal newUserRatio;
+
+    @ApiModelProperty("老用户占比(%)")
+    private BigDecimal oldUserRatio;
+}

+ 4 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsAiRequestMapper.java

@@ -14,4 +14,8 @@ public interface AnalyticsAiRequestMapper extends BaseMapper<AnalyticsAiRequest>
     @Select("SELECT COALESCE(SUM(response_duration_ms), 0) FROM analytics_ai_request " +
             "WHERE created_time >= #{startTime} AND created_time < #{endTime}")
     Long sumResponseDuration(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
+    @Select("SELECT COUNT(*) FROM analytics_ai_request " +
+            "WHERE created_time >= #{startTime} AND created_time < #{endTime}")
+    Long countRequests(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
 }

+ 31 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsContentReportMapper.java

@@ -0,0 +1,31 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.analytics.AnalyticsContentStat;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface AnalyticsContentReportMapper {
+
+    Map<String, Object> sumContentMetrics(@Param("startDate") Date startDate,
+                                          @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> countCategoryDistribution(@Param("startDate") Date startDate,
+                                                        @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> listAuditTrend(@Param("startDate") Date startDate,
+                                             @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> listInteractionTop10(@Param("startDate") Date startDate,
+                                                     @Param("endDate") Date endDate);
+
+    IPage<AnalyticsContentStat> pageAuditPassed(Page<AnalyticsContentStat> page,
+                                                @Param("startTime") Date startTime,
+                                                @Param("endTime") Date endTime);
+}

+ 45 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsDashboardMapper.java

@@ -0,0 +1,45 @@
+package shop.alien.mapper;
+
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.analytics.AnalyticsDailySummary;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface AnalyticsDashboardMapper {
+
+    List<AnalyticsDailySummary> listDailySummaryBetween(@Param("startDate") Date startDate,
+                                                        @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> countDauByHour(@Param("startTime") Date startTime,
+                                             @Param("endTime") Date endTime);
+
+    List<Map<String, Object>> countEventByHour(@Param("eventCode") String eventCode,
+                                               @Param("startTime") Date startTime,
+                                               @Param("endTime") Date endTime);
+
+    List<Map<String, Object>> countDistinctUserByEventByHour(@Param("eventCode") String eventCode,
+                                                             @Param("startTime") Date startTime,
+                                                             @Param("endTime") Date endTime);
+
+    List<Map<String, Object>> countContentInteractByHour(@Param("startTime") Date startTime,
+                                                         @Param("endTime") Date endTime);
+
+    List<Map<String, Object>> avgHeartbeatMinutesByHour(@Param("startTime") Date startTime,
+                                                       @Param("endTime") Date endTime);
+
+    Map<String, Object> sumMerchantFunnel(@Param("startDate") Date startDate,
+                                        @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> avgOnlineDurationByDayFromHistory(@Param("startDate") Date startDate,
+                                                                @Param("endDate") Date endDate);
+
+    java.math.BigDecimal avgOnlineDurationFromToday();
+
+    Long countRetainedOnDayOffset(@Param("cohortStart") Date cohortStart,
+                                  @Param("cohortEnd") Date cohortEnd,
+                                  @Param("dayOffset") int dayOffset);
+}

+ 19 - 2
alien-entity/src/main/java/shop/alien/mapper/AnalyticsEventMapper.java

@@ -55,13 +55,30 @@ public interface AnalyticsEventMapper extends BaseMapper<AnalyticsEvent> {
     List<Map<String, Object>> aggregateUserDaily(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
 
     @Select("SELECT merchant_id AS merchantId, " +
+            "COUNT(CASE WHEN event_code = 'merchant.expose' THEN 1 END) AS exposeCount, " +
+            "COUNT(CASE WHEN event_code = 'merchant.click' THEN 1 END) AS clickCount, " +
+            "COUNT(CASE WHEN event_code IN ('merchant.detail', 'merchant.view') THEN 1 END) AS detailViewCount, " +
+            "COUNT(CASE WHEN event_code = 'merchant.contact' THEN 1 END) AS contactCount, " +
             "COUNT(CASE WHEN event_code = 'merchant.view' THEN 1 END) AS visitPv, " +
             "COUNT(DISTINCT CASE WHEN event_code = 'merchant.view' AND user_id IS NOT NULL THEN user_id END) AS visitUv, " +
             "COUNT(CASE WHEN event_code = 'merchant.verify' THEN 1 END) AS verifyCount, " +
-            "COUNT(DISTINCT CASE WHEN event_code = 'merchant.view' AND user_id IS NOT NULL THEN user_id END) AS visitUserCount " +
-            "FROM analytics_event WHERE event_time >= #{startTime} AND event_time < #{endTime} AND merchant_id IS NOT NULL GROUP BY merchant_id")
+            "COUNT(DISTINCT CASE WHEN event_code = 'merchant.view' AND user_id IS NOT NULL THEN user_id END) AS visitUserCount, " +
+            "COALESCE(SUM(CASE WHEN event_code = 'pay.success' THEN amount ELSE 0 END), 0) AS gmv, " +
+            "COUNT(CASE WHEN event_code = 'pay.success' THEN 1 END) AS payCount " +
+            "FROM analytics_event WHERE event_time >= #{startTime} AND event_time < #{endTime} AND merchant_id IS NOT NULL " +
+            "GROUP BY merchant_id")
     List<Map<String, Object>> aggregateMerchantDaily(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
 
+    @Select("SELECT business_category AS businessCategory, " +
+            "COALESCE(SUM(CASE WHEN event_code = 'pay.success' THEN amount ELSE 0 END), 0) AS gmv, " +
+            "COUNT(CASE WHEN event_code = 'pay.success' THEN 1 END) AS payCount, " +
+            "COUNT(DISTINCT CASE WHEN event_code = 'merchant.view' AND user_id IS NOT NULL THEN user_id END) AS merchantVisitUv, " +
+            "COUNT(CASE WHEN event_code = 'content.publish' THEN 1 END) AS contentPublishCount, " +
+            "COUNT(CASE WHEN event_code = 'content.interact' THEN 1 END) AS contentInteractionCount " +
+            "FROM analytics_event WHERE event_time >= #{startTime} AND event_time < #{endTime} " +
+            "AND business_category IS NOT NULL GROUP BY business_category")
+    List<Map<String, Object>> aggregateCategoryDaily(@Param("startTime") Date startTime, @Param("endTime") Date endTime);
+
     @Select("SELECT COUNT(DISTINCT r.user_id) FROM analytics_event r " +
             "INNER JOIN analytics_event a ON r.user_id = a.user_id " +
             "WHERE r.event_code = 'user.register' AND r.event_time >= #{prevStart} AND r.event_time < #{prevEnd} " +

+ 34 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantReportMapper.java

@@ -0,0 +1,34 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.analytics.AnalyticsMerchantStat;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface AnalyticsMerchantReportMapper {
+
+    Map<String, Object> sumMerchantMetrics(@Param("startDate") Date startDate,
+                                           @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> listGmvTrend(@Param("startDate") Date startDate,
+                                           @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> sumGmvByCategory(@Param("startDate") Date startDate,
+                                             @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> listAvgOrderTrend(@Param("startDate") Date startDate,
+                                                @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> listMerchantGmvTop10(@Param("startDate") Date startDate,
+                                                  @Param("endDate") Date endDate);
+
+    IPage<AnalyticsMerchantStat> pageSettledMerchantDetail(Page<AnalyticsMerchantStat> page,
+                                                           @Param("startDate") Date startDate,
+                                                           @Param("endDate") Date endDate);
+}

+ 14 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantStatMapper.java

@@ -7,6 +7,7 @@ import org.apache.ibatis.annotations.Select;
 import shop.alien.entity.analytics.AnalyticsMerchantStat;
 
 import java.util.Date;
+import java.util.Map;
 
 @Mapper
 public interface AnalyticsMerchantStatMapper extends BaseMapper<AnalyticsMerchantStat> {
@@ -18,4 +19,17 @@ public interface AnalyticsMerchantStatMapper extends BaseMapper<AnalyticsMerchan
             "SELECT merchant_id FROM analytics_merchant_stat_today WHERE settle_status = 1" +
             ") t")
     Long countSettledMerchants(@Param("statDate") Date statDate);
+
+    @Select("SELECT COALESCE(SUM(expose_count), 0) AS exposeCount, " +
+            "COALESCE(SUM(click_count), 0) AS clickCount, " +
+            "COALESCE(SUM(detail_view_count), 0) AS detailCount, " +
+            "COALESCE(SUM(contact_count), 0) AS contactCount " +
+            "FROM (" +
+            "SELECT expose_count, click_count, detail_view_count, contact_count " +
+            "FROM analytics_merchant_stat_history WHERE stat_date = #{statDate} " +
+            "UNION ALL " +
+            "SELECT expose_count, click_count, detail_view_count, contact_count " +
+            "FROM analytics_merchant_stat_today WHERE #{statDate} = CURDATE()" +
+            ") t")
+    Map<String, Object> sumFunnelByStatDate(@Param("statDate") Date statDate);
 }

+ 34 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsUserReportMapper.java

@@ -0,0 +1,34 @@
+package shop.alien.mapper;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import shop.alien.entity.analytics.AnalyticsUserStat;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface AnalyticsUserReportMapper {
+
+    Map<String, Object> sumRegisterFunnel(@Param("startDate") Date startDate,
+                                          @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> countGenderDistribution(@Param("startDate") Date startDate,
+                                                      @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> countAgeDistribution(@Param("startDate") Date startDate,
+                                                   @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> countCityTop10(@Param("startDate") Date startDate,
+                                             @Param("endDate") Date endDate);
+
+    Map<String, Object> countUserTypeRatio(@Param("startDate") Date startDate,
+                                           @Param("endDate") Date endDate);
+
+    IPage<AnalyticsUserStat> pageNewUsersInRange(Page<AnalyticsUserStat> page,
+                                                 @Param("startTime") Date startTime,
+                                                 @Param("endTime") Date endTime);
+}

+ 446 - 0
alien-entity/src/main/resources/db/migration/analytics_schema.sql

@@ -0,0 +1,446 @@
+-- =============================================================================
+-- 平台埋点统计系统 - 完整建表脚本(最终版)
+-- =============================================================================
+-- 【新环境】仅需执行本文件一次即可。
+--
+-- 表结构说明:
+--   A. 基础表(4张)  事件日志、AI请求、日汇总KPI、任务日志
+--   B. 明细表(12张)  用户/商户/内容/品类/AI对话/举报 × (今日表+历史表)
+--
+-- 数据流:
+--   白天埋点/统计 → 写入 *_today
+--   零点 analyticsArchiveDaily → 归档至 *_history(stat_date=昨日)
+--                          → TRUNCATE *_today → 删除30天前历史
+--
+-- XXL-JOB 调度建议:
+--   analyticsArchiveDaily              0 0 0 * * ?   零点归档
+--   analyticsCalculateTodayHourly      0 0 * * * ?   每小时汇总当日
+--   analyticsCalculateYesterdayDaily   0 0 2 * * ?   凌晨2点汇总前日
+--   analyticsCalculateRetention        0 0 3 * * ?   凌晨3点计算留存
+--
+-- 经营品类 business_category / shop_type 枚举:
+--   1美食 2休闲娱乐 3生活服务 4旅游 5酒店 6购物 7其他
+--
+-- 实体对照:
+--   analytics_event                    → AnalyticsEvent
+--   analytics_ai_request               → AnalyticsAiRequest
+--   analytics_daily_summary            → AnalyticsDailySummary
+--   analytics_stat_job_log             → AnalyticsStatJobLog
+--   analytics_user_stat_today          → AnalyticsUserStatToday
+--   analytics_user_stat_history        → AnalyticsUserStat
+--   analytics_merchant_stat_today      → AnalyticsMerchantStatToday
+--   analytics_merchant_stat_history    → AnalyticsMerchantStat
+--   analytics_content_stat_today       → AnalyticsContentStat
+--   analytics_content_stat_history     → AnalyticsContentStatHistory
+--   analytics_category_daily_today     → AnalyticsCategoryDailyToday
+--   analytics_category_daily_history   → AnalyticsCategoryDaily
+--   analytics_ai_chat_stat_today       → AnalyticsAiChatStat
+--   analytics_ai_chat_stat_history     → AnalyticsAiChatStatHistory
+--   analytics_report_record_today      → AnalyticsReportRecord
+--   analytics_report_record_history    → AnalyticsReportRecordHistory
+--
+-- 【旧环境升级】勿直接执行本文件,请参考:
+--   analytics_tables_dashboard_upgrade.sql(v1→v2增量)
+--   脚本末尾「旧库迁移」RENAME 说明(单表明细 → today/history)
+-- =============================================================================
+
+SET NAMES utf8mb4;
+
+-- #############################################################################
+-- PART A. 基础表
+-- #############################################################################
+
+-- -----------------------------------------------------------------------------
+-- A1. 埋点事件表(原始日志,日汇总数据源)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_event` (
+  `id`                bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `event_id`          varchar(64)   NOT NULL COMMENT '事件唯一ID(幂等,前端UUID)',
+  `event_code`        varchar(64)   NOT NULL COMMENT '事件编码(见 AnalyticsEventCode)',
+  `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(如内容ID、订单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 '设备类型(IOS/ANDROID/H5等)',
+  `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`),
+  KEY `idx_business_category_time` (`business_category`, `event_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='埋点事件表(原始日志)';
+
+-- -----------------------------------------------------------------------------
+-- A2. AI 请求明细(单次 API 调用耗时,不按今日/历史拆分)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_ai_request` (
+  `id`                   bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `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 '响应时长(毫秒)',
+  `is_timeout`           tinyint      NOT NULL DEFAULT 0 COMMENT '是否超时(0否1是)',
+  `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请求明细(单次调用)';
+
+-- -----------------------------------------------------------------------------
+-- A3. 平台日统计主表(四页看板 KPI,按自然日一行)
+-- -----------------------------------------------------------------------------
+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)',
+  `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 '当日转化率(%)',
+  `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 '核销率(%)',
+  `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='平台日统计主表(看板KPI)';
+
+-- -----------------------------------------------------------------------------
+-- A4. 统计任务执行日志
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_stat_job_log` (
+  `id`           bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `job_id`       varchar(64)   NOT NULL COMMENT '任务实例ID(UUID)',
+  `stat_date`    date          NOT NULL COMMENT '统计目标日期',
+  `scope`        varchar(32)   NOT NULL COMMENT '统计范围(ALL/USER/MERCHANT/CONTENT/DAILY等,见 AnalyticsStatScope)',
+  `trigger_type` varchar(16)   NOT NULL COMMENT '触发方式(SCHEDULE定时/MANUAL手动)',
+  `status`       tinyint       NOT NULL DEFAULT 0 COMMENT '状态(0执行中1成功2失败)',
+  `start_time`   datetime      NOT NULL COMMENT '开始时间',
+  `end_time`     datetime      DEFAULT NULL COMMENT '结束时间',
+  `error_msg`    varchar(1000) DEFAULT NULL COMMENT '失败错误信息',
+  `created_time` datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_job_id` (`job_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统计任务执行日志';
+
+-- #############################################################################
+-- PART B. 明细表(今日表 + 历史表,历史保留至少30天)
+-- #############################################################################
+
+-- -----------------------------------------------------------------------------
+-- B1. 用户明细(今日表 uk_user_id;历史表 uk_date_user)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_user_stat_today` (
+  `id`                  bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_id`             bigint       NOT NULL COMMENT '用户ID',
+  `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 '设备类型(IOS 17/Android 14等)',
+  `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 '年龄段(18-24/25-29等)',
+  `register_time`       datetime     DEFAULT NULL COMMENT '注册时间',
+  `channel`             varchar(64)  DEFAULT NULL COMMENT '注册/获客渠道',
+  `is_new_user`         tinyint      NOT NULL DEFAULT 0 COMMENT '是否当日新注册用户(0否1是)',
+  `online_duration_min` int          NOT NULL DEFAULT 0 COMMENT '当日累计在线时长(分钟)',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_id` (`user_id`),
+  KEY `idx_last_active` (`last_active_time`),
+  KEY `idx_register_time` (`register_time`),
+  KEY `idx_is_new_user` (`is_new_user`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户明细-今日(DAU/新增/在线)';
+
+CREATE TABLE IF NOT EXISTS `analytics_user_stat_history` (
+  `id`                  bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `stat_date`           date         NOT NULL COMMENT '统计日期(归档日)',
+  `user_id`             bigint       NOT NULL COMMENT '用户ID',
+  `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 '是否当日新注册用户(0否1是)',
+  `online_duration_min` int          NOT NULL DEFAULT 0 COMMENT '当日在线时长(分钟)',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_user` (`stat_date`, `user_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_last_active` (`last_active_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户明细-历史(保留30天)';
+
+-- -----------------------------------------------------------------------------
+-- B2. 商户明细(今日表 uk_merchant_id;历史表 uk_date_merchant)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_merchant_stat_today` (
+  `id`                     bigint        NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `merchant_id`            bigint        NOT NULL COMMENT '商家/店铺ID',
+  `shop_type`              tinyint       DEFAULT NULL COMMENT '店铺类型(1美食2休闲娱乐3生活服务)',
+  `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 '入驻状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_merchant_id` (`merchant_id`),
+  KEY `idx_shop_type` (`shop_type`),
+  KEY `idx_visit_pv` (`visit_pv`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商户明细-今日(访问UV/PV/转化)';
+
+CREATE TABLE IF NOT EXISTS `analytics_merchant_stat_history` (
+  `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生活服务)',
+  `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 '入驻状态',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_merchant` (`stat_date`, `merchant_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_merchant_id` (`merchant_id`),
+  KEY `idx_gmv` (`stat_date`, `gmv`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商户明细-历史(保留30天)';
+
+-- -----------------------------------------------------------------------------
+-- B3. 内容明细(今日表 uk_content_type_id;历史表 uk_date_content)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_content_stat_today` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `content_id`        bigint       NOT NULL COMMENT '内容ID',
+  `content_type`      tinyint      NOT NULL COMMENT '内容形态(1动态2打卡3二手商品)',
+  `business_category` tinyint      DEFAULT NULL COMMENT '经营品类(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)',
+  `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 '互动总数(点赞+评论+分享等)',
+  `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 '审核状态',
+  `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_interaction` (`interaction_count`),
+  KEY `idx_publish_time` (`publish_time`),
+  KEY `idx_business_category` (`business_category`),
+  KEY `idx_author` (`author_type`, `author_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='内容明细-今日(发布/互动)';
+
+CREATE TABLE IF NOT EXISTS `analytics_content_stat_history` (
+  `id`                bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `stat_date`         date         NOT NULL COMMENT '统计日期(归档日)',
+  `content_id`        bigint       NOT NULL COMMENT '内容ID',
+  `content_type`      tinyint      NOT NULL COMMENT '内容形态(1动态2打卡3二手商品)',
+  `business_category` tinyint      DEFAULT NULL COMMENT '经营品类(1美食2休闲娱乐3生活服务4旅游5酒店6购物7其他)',
+  `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 '互动总数',
+  `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 '审核状态',
+  `audit_type`        tinyint      DEFAULT NULL COMMENT '审核方式(1AI2人工)',
+  `audit_time`        datetime     DEFAULT NULL COMMENT '审核时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_content` (`stat_date`, `content_type`, `content_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_interaction` (`interaction_count`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='内容明细-历史(保留30天)';
+
+-- -----------------------------------------------------------------------------
+-- B4. 经营品类日统计(今日表 uk_category;历史表 uk_date_category)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_category_daily_today` (
+  `id`                        bigint        NOT NULL AUTO_INCREMENT 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_category` (`business_category`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='经营品类日统计-今日(看板饼图)';
+
+CREATE TABLE IF NOT EXISTS `analytics_category_daily_history` (
+  `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='经营品类日统计-历史(保留30天)';
+
+-- -----------------------------------------------------------------------------
+-- B5. AI 对话明细(今日表 uk_chat_id;历史表 uk_date_chat)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_ai_chat_stat_today` (
+  `id`                      bigint      NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `chat_id`                 varchar(64) NOT NULL COMMENT '对话ID(幂等)',
+  `user_id`                 bigint      NOT NULL COMMENT '用户ID',
+  `start_time`              datetime    NOT NULL COMMENT '对话开始时间',
+  `message_count`           int         NOT NULL DEFAULT 0 COMMENT '消息条数',
+  `ai_response_duration_ms` bigint      NOT NULL DEFAULT 0 COMMENT 'AI累计响应时长(毫秒)',
+  PRIMARY KEY (`id`),
+  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对话明细-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_ai_chat_stat_history` (
+  `id`                      bigint      NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `stat_date`               date        NOT NULL COMMENT '统计日期(归档日)',
+  `chat_id`                 varchar(64) NOT NULL COMMENT '对话ID',
+  `user_id`                 bigint      NOT NULL COMMENT '用户ID',
+  `start_time`              datetime    NOT NULL COMMENT '对话开始时间',
+  `message_count`           int         NOT NULL DEFAULT 0 COMMENT '消息条数',
+  `ai_response_duration_ms` bigint      NOT NULL DEFAULT 0 COMMENT 'AI累计响应时长(毫秒)',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_date_chat` (`stat_date`, `chat_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_user_id` (`user_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='AI对话明细-历史(保留30天)';
+
+-- -----------------------------------------------------------------------------
+-- B6. 举报明细(今日表 uk_report_id;历史表 uk_date_report)
+-- -----------------------------------------------------------------------------
+CREATE TABLE IF NOT EXISTS `analytics_report_record_today` (
+  `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='举报明细-今日';
+
+CREATE TABLE IF NOT EXISTS `analytics_report_record_history` (
+  `id`               bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `stat_date`        date         NOT NULL 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_date_report` (`stat_date`, `report_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_report_time` (`report_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='举报明细-历史(保留30天)';
+
+-- #############################################################################
+-- 旧库迁移说明(已有 v1 单表明细时,备份后手动执行,勿与新环境混用)
+-- #############################################################################
+-- 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;
+-- 然后 CREATE 缺失的 *_today 表(见上文 PART B,仅执行 today 部分)。

+ 5 - 240
alien-entity/src/main/resources/db/migration/analytics_tables.sql

@@ -1,242 +1,7 @@
 -- =============================================================================
--- 平台埋点统计系统 - 建表脚本 v2(含四页看板字段)
+-- 【已合并】请使用最终版建表脚本:
+--   analytics_schema.sql
+--
+-- 新环境:仅执行 analytics_schema.sql 一次即可(含基础表 + 今日表 + 历史表)。
+-- 旧环境升级:见 analytics_tables_dashboard_upgrade.sql
 -- =============================================================================
--- 新环境:直接执行本文件
--- 旧环境:执行 analytics_tables.sql 后再执行 analytics_tables_dashboard_upgrade.sql
--- =============================================================================
-
-SET NAMES utf8mb4;
-
-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 '事件编码',
-  `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`),
-  KEY `idx_business_category_time` (`business_category`, `event_time`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='埋点事件表';
-
-CREATE TABLE IF NOT EXISTS `analytics_user_stat` (
-  `id`                  bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
-  `stat_date`           date         NOT NULL COMMENT '统计日期',
-  `user_id`             bigint       NOT NULL COMMENT '用户ID',
-  `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_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='用户明细统计';
-
-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生活服务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 '入驻状态',
-  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_gmv` (`stat_date`, `gmv`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商户明细统计';
-
-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二手)',
-  `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 '互动总数',
-  `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 '审核状态',
-  `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='内容明细统计';
-
-CREATE TABLE IF NOT EXISTS `analytics_ai_chat_stat` (
-  `id`                      bigint      NOT NULL AUTO_INCREMENT COMMENT '主键',
-  `chat_id`                 varchar(64) NOT NULL COMMENT '对话ID',
-  `user_id`                 bigint      NOT NULL COMMENT '用户ID',
-  `start_time`              datetime    NOT NULL COMMENT '开始时间',
-  `message_count`           int         NOT NULL DEFAULT 0 COMMENT '消息数',
-  `ai_response_duration_ms` bigint      NOT NULL DEFAULT 0 COMMENT 'AI响应时长(ms)',
-  PRIMARY KEY (`id`),
-  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对话明细';
-
-CREATE TABLE IF NOT EXISTS `analytics_ai_request` (
-  `id`                   bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
-  `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 '是否超时',
-  `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请求明细';
-
-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',
-  `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 '今日转化率',
-  `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 '核销率',
-  `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='平台日统计主表';
-
-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 '统计范围',
-  `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 '错误信息',
-  `created_time` datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  PRIMARY KEY (`id`),
-  KEY `idx_stat_date` (`stat_date`),
-  KEY `idx_job_id` (`job_id`)
-) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='统计任务日志';

+ 64 - 45
alien-entity/src/main/resources/db/migration/analytics_tables_dashboard_upgrade.sql

@@ -1,14 +1,20 @@
 -- =============================================================================
--- 平台埋点 - 四页看板表结构升级(在 analytics_tables.sql 已执行基础上增量执行
+-- 平台埋点 - 旧库增量升级(v1 → v2 → v3
 -- =============================================================================
--- 数据看板 / 用户报表 / 内容报表 / 商家报表
+-- 【新环境】请勿执行本文件,直接执行:analytics_schema.sql
+--
+-- 【旧环境】已存在 analytics_user_stat 等旧单表时:
+--   1. 执行本文件(v1→v2 看板字段增量)
+--   2. 按 analytics_schema.sql 末尾 RENAME 说明迁移为 today/history
+--   3. 执行 analytics_schema.sql 中缺失的 *_today 建表语句(或全量对照补表)
 -- =============================================================================
 
 SET NAMES utf8mb4;
 
 -- -----------------------------------------------------------------------------
 -- 1. analytics_event  【数据看板-漏斗】【内容/商家-品类】
---    新增:event_subtype / business_category / shop_type
+--     新增:event_subtype / business_category / shop_type
+--     实体:AnalyticsEvent
 -- -----------------------------------------------------------------------------
 ALTER TABLE `analytics_event`
     ADD COLUMN `event_subtype` varchar(32) DEFAULT NULL COMMENT '事件子类型(如审核ai_pass/manual_pass/reject;注册漏斗步骤等)' AFTER `event_code`,
@@ -18,41 +24,44 @@ ALTER TABLE `analytics_event`
     ADD KEY `idx_business_category_time` (`business_category`, `event_time`);
 
 -- -----------------------------------------------------------------------------
--- 2. analytics_user_stat  【用户报表-画像/地域/新老用户】
+-- 2. analytics_user_stat  【用户报表-画像/地域/新老用户】(旧单表,升级后建议迁移为 today/history)
+--     实体:升级阶段仍用 AnalyticsUserStat;迁移后历史表为 analytics_user_stat_history
 -- -----------------------------------------------------------------------------
 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 COLUMN `is_new_user` tinyint NOT NULL DEFAULT 0 COMMENT '是否当日注册用户(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/明细/转化漏斗】
+-- 3. analytics_merchant_stat  【商家报表-GMV/转化漏斗】(旧单表)
+--     实体:升级阶段 AnalyticsMerchantStat;迁移后 analytics_merchant_stat_history
 -- -----------------------------------------------------------------------------
 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 `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/审核】
+-- 4. analytics_content_stat  【内容报表-品类/互动/审核】(旧单表)
+--     实体:迁移后今日表 analytics_content_stat_today → AnalyticsContentStat
 -- -----------------------------------------------------------------------------
 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 `content_title` varchar(256) DEFAULT NULL COMMENT '内容标题快照(发布明细展示)' 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`,
@@ -61,22 +70,23 @@ ALTER TABLE `analytics_content_stat`
     ADD KEY `idx_interaction` (`interaction_count`);
 
 -- -----------------------------------------------------------------------------
--- 5. analytics_ai_chat_stat  【数据看板-对话趋势】无需改表,按 start_time 聚合即可
--- 6. analytics_ai_request   【数据看板-AI响应时间】无需改表
--- 7. analytics_stat_job_log   运维表,无需改表
+-- 5. analytics_ai_chat_stat   【数据看板-对话趋势】无需改表,按 start_time 聚合
+-- 6. analytics_ai_request     【数据看板-AI响应时间】无需改表
+-- 7. analytics_stat_job_log   【运维】无需改表,实体 AnalyticsStatJobLog
 -- -----------------------------------------------------------------------------
 
 -- -----------------------------------------------------------------------------
 -- 8. analytics_daily_summary  【四页顶部 KPI + 漏斗 + MAU + 审核细分】
+--     实体:AnalyticsDailySummary
 -- -----------------------------------------------------------------------------
 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_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 `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`,
@@ -87,43 +97,52 @@ ALTER TABLE `analytics_daily_summary`
     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`;
+    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占比】【内容报表-品类分布】  新表
+-- 9. analytics_category_daily  【商家/内容报表-品类占比】新表(旧环境单表)
+--     迁移后拆为 analytics_category_daily_today / _history
+--     实体:AnalyticsCategoryDaily → history;AnalyticsCategoryDailyToday → today
 -- -----------------------------------------------------------------------------
 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 '品类内容互动数',
+  `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='经营品类日统计(看板饼图/占比)';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='经营品类日统计(旧单表,建议迁移为today/history)';
 
 -- -----------------------------------------------------------------------------
--- 10. analytics_report_record  【内容报表-举报处理列表】  新表
+-- 10. analytics_report_record  【内容报表-举报列表】新表(旧环境单表)
+--      迁移后拆为 analytics_report_record_today / _history
+--      实体:AnalyticsReportRecord → today;AnalyticsReportRecordHistory → history
 -- -----------------------------------------------------------------------------
 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 '入库时间',
+  `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='举报处理明细(内容报表列表)';
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='举报处理明细(旧单表,建议迁移为today/history)';
+
+-- -----------------------------------------------------------------------------
+-- 11. 今日表 + 历史表架构(v3)
+--     参考 analytics_schema.sql PART B 及文件末尾「旧库迁移」RENAME 说明
+-- -----------------------------------------------------------------------------

+ 2 - 262
alien-entity/src/main/resources/db/migration/analytics_today_history_tables.sql

@@ -1,264 +1,4 @@
 -- =============================================================================
--- 埋点明细:今日表 + 历史表(历史保留至少30天,由定时任务零点归档)
---
--- 调度建议(XXL-JOB):
---   analyticsArchiveDaily        0 0 0 * * ?  零点归档昨日今日表 → 历史表
---   analyticsCalculateTodayHourly 0 0 * * * ?  每小时汇总当日
---   analyticsCalculateYesterdayDaily 0 0 2 * * ? 凌晨2点汇总前日(归档后)
+-- 【已合并】请使用最终版建表脚本:
+--   analytics_schema.sql
 -- =============================================================================
-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='举报明细-历史';

+ 107 - 0
alien-entity/src/main/resources/mapper/AnalyticsContentReportMapper.xml

@@ -0,0 +1,107 @@
+<?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.AnalyticsContentReportMapper">
+
+    <sql id="contentUnion">
+        SELECT content_id, content_type, content_title, interaction_count, like_count, comment_count,
+               share_count, audit_status, audit_time, publish_time, status
+        FROM analytics_content_stat_history
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        UNION ALL
+        SELECT content_id, content_type, content_title, interaction_count, like_count, comment_count,
+               share_count, audit_status, audit_time, publish_time, status
+        FROM analytics_content_stat_today
+        WHERE #{endDate} &gt;= CURDATE()
+          AND #{startDate} &lt;= CURDATE()
+    </sql>
+
+    <select id="sumContentMetrics" resultType="java.util.HashMap">
+        SELECT COALESCE(SUM(content_publish_count), 0)     AS publishCount,
+               COALESCE(SUM(content_interaction_count), 0) AS interactionCount,
+               COALESCE(SUM(audit_submit_count), 0)        AS auditSubmitCount,
+               COALESCE(SUM(audit_pass_count), 0)          AS auditPassCount,
+               COALESCE(SUM(audit_reject_count), 0)        AS auditRejectCount
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+    </select>
+
+    <select id="countCategoryDistribution" resultType="java.util.HashMap">
+        SELECT content_type AS code,
+               COUNT(DISTINCT content_id) AS cnt
+        FROM (
+            <include refid="contentUnion"/>
+        ) t
+        WHERE publish_time IS NOT NULL
+        GROUP BY content_type
+        ORDER BY cnt DESC
+    </select>
+
+    <select id="listAuditTrend" resultType="java.util.HashMap">
+        SELECT stat_date AS statDate,
+               DATE_FORMAT(stat_date, '%m-%d') AS label,
+               COALESCE(audit_submit_count, 0) AS submitCount,
+               COALESCE(audit_pass_count, 0)   AS passCount,
+               COALESCE(audit_reject_count, 0) AS rejectCount
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        ORDER BY stat_date ASC
+    </select>
+
+    <select id="listInteractionTop10" resultType="java.util.HashMap">
+        SELECT content_id AS contentId,
+               MAX(content_title) AS contentTitle,
+               SUM(interaction_count) AS interactionCount,
+               SUM(like_count) AS likeCount,
+               SUM(comment_count) AS commentCount,
+               SUM(share_count) AS shareCount
+        FROM (
+            <include refid="contentUnion"/>
+        ) t
+        GROUP BY content_id
+        ORDER BY interactionCount DESC
+        LIMIT 10
+    </select>
+
+    <select id="pageAuditPassed" resultType="shop.alien.entity.analytics.AnalyticsContentStat">
+        SELECT t.content_id AS contentId,
+               t.content_type AS contentType,
+               t.content_title AS contentTitle,
+               t.audit_time AS auditTime,
+               t.status,
+               t.audit_status AS auditStatus
+        FROM (
+            SELECT content_id, content_type, content_title, audit_time, status, audit_status
+            FROM analytics_content_stat_history
+            WHERE audit_status = 1
+              AND audit_time &gt;= #{startTime}
+              AND audit_time &lt; #{endTime}
+            UNION ALL
+            SELECT content_id, content_type, content_title, audit_time, status, audit_status
+            FROM analytics_content_stat_today
+            WHERE audit_status = 1
+              AND audit_time &gt;= #{startTime}
+              AND audit_time &lt; #{endTime}
+        ) t
+        INNER JOIN (
+            SELECT content_id, MAX(audit_time) AS max_audit_time
+            FROM (
+                SELECT content_id, audit_time
+                FROM analytics_content_stat_history
+                WHERE audit_status = 1
+                  AND audit_time &gt;= #{startTime}
+                  AND audit_time &lt; #{endTime}
+                UNION ALL
+                SELECT content_id, audit_time
+                FROM analytics_content_stat_today
+                WHERE audit_status = 1
+                  AND audit_time &gt;= #{startTime}
+                  AND audit_time &lt; #{endTime}
+            ) u
+            GROUP BY content_id
+        ) latest ON t.content_id = latest.content_id AND t.audit_time = latest.max_audit_time
+        ORDER BY t.audit_time DESC
+    </select>
+</mapper>

+ 127 - 0
alien-entity/src/main/resources/mapper/AnalyticsDashboardMapper.xml

@@ -0,0 +1,127 @@
+<?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.AnalyticsDashboardMapper">
+
+    <select id="listDailySummaryBetween" resultType="shop.alien.entity.analytics.AnalyticsDailySummary">
+        SELECT *
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        ORDER BY stat_date ASC
+    </select>
+
+    <select id="countDauByHour" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(event_time, '%H:00') AS label,
+               COUNT(DISTINCT user_id) AS cnt
+        FROM analytics_event
+        WHERE event_time &gt;= #{startTime}
+          AND event_time &lt; #{endTime}
+          AND user_id IS NOT NULL
+        GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+        ORDER BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+    </select>
+
+    <select id="countEventByHour" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(event_time, '%H:00') AS label,
+               COUNT(*) AS cnt
+        FROM analytics_event
+        WHERE event_code = #{eventCode}
+          AND event_time &gt;= #{startTime}
+          AND event_time &lt; #{endTime}
+        GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+        ORDER BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+    </select>
+
+    <select id="countDistinctUserByEventByHour" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(event_time, '%H:00') AS label,
+               COUNT(DISTINCT user_id) AS cnt
+        FROM analytics_event
+        WHERE event_code = #{eventCode}
+          AND event_time &gt;= #{startTime}
+          AND event_time &lt; #{endTime}
+          AND user_id IS NOT NULL
+        GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+        ORDER BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+    </select>
+
+    <select id="countContentInteractByHour" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(event_time, '%H:00') AS label,
+               COUNT(*) AS cnt
+        FROM analytics_event
+        WHERE event_code = 'content.interact'
+          AND event_time &gt;= #{startTime}
+          AND event_time &lt; #{endTime}
+        GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+        ORDER BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+    </select>
+
+    <select id="avgHeartbeatMinutesByHour" resultType="java.util.HashMap">
+        SELECT DATE_FORMAT(event_time, '%H:00') AS label,
+               ROUND(
+                   SUM(CASE WHEN event_code = 'user.heartbeat' AND duration_ms IS NOT NULL THEN duration_ms ELSE 0 END)
+                   / NULLIF(COUNT(DISTINCT CASE WHEN user_id IS NOT NULL THEN user_id END), 0) / 60000,
+                   2
+               ) AS avgMin
+        FROM analytics_event
+        WHERE event_time &gt;= #{startTime}
+          AND event_time &lt; #{endTime}
+        GROUP BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+        ORDER BY DATE_FORMAT(event_time, '%Y-%m-%d %H')
+    </select>
+
+    <select id="sumMerchantFunnel" resultType="java.util.HashMap">
+        SELECT COALESCE(SUM(expose_count), 0)      AS exposeCount,
+               COALESCE(SUM(click_count), 0)       AS clickCount,
+               COALESCE(SUM(detail_view_count), 0) AS detailCount,
+               COALESCE(SUM(contact_count), 0)     AS contactCount,
+               COALESCE(SUM(verify_count), 0)      AS verifyCount
+        FROM (
+            SELECT expose_count, click_count, detail_view_count, contact_count, verify_count
+            FROM analytics_merchant_stat_history
+            WHERE stat_date &gt;= #{startDate}
+              AND stat_date &lt;= #{endDate}
+            UNION ALL
+            SELECT expose_count, click_count, detail_view_count, contact_count, verify_count
+            FROM analytics_merchant_stat_today
+            WHERE #{endDate} &gt;= CURDATE()
+              AND #{startDate} &lt;= CURDATE()
+        ) t
+    </select>
+
+    <select id="avgOnlineDurationByDayFromHistory" resultType="java.util.HashMap">
+        SELECT stat_date AS statDate,
+               DATE_FORMAT(stat_date, '%m.%d') AS label,
+               ROUND(AVG(online_duration_min), 2) AS avgMin
+        FROM analytics_user_stat_history
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+          AND online_duration_min IS NOT NULL
+          AND online_duration_min &gt; 0
+        GROUP BY stat_date
+        ORDER BY stat_date ASC
+    </select>
+
+    <select id="avgOnlineDurationFromToday" resultType="java.math.BigDecimal">
+        SELECT ROUND(AVG(online_duration_min), 2)
+        FROM analytics_user_stat_today
+        WHERE online_duration_min IS NOT NULL
+          AND online_duration_min &gt; 0
+    </select>
+
+    <select id="countRetainedOnDayOffset" resultType="java.lang.Long">
+        SELECT COUNT(DISTINCT r.user_id)
+        FROM analytics_event r
+        WHERE r.event_code = 'user.register'
+          AND r.event_time &gt;= #{cohortStart}
+          AND r.event_time &lt; #{cohortEnd}
+          AND r.user_id IS NOT NULL
+          AND EXISTS (
+            SELECT 1
+            FROM analytics_event a
+            WHERE a.user_id = r.user_id
+              AND a.user_id IS NOT NULL
+              AND a.event_time &gt;= DATE_ADD(DATE(r.event_time), INTERVAL #{dayOffset} DAY)
+              AND a.event_time &lt; DATE_ADD(DATE(r.event_time), INTERVAL #{dayOffset} + 1 DAY)
+        )
+    </select>
+</mapper>

+ 130 - 0
alien-entity/src/main/resources/mapper/AnalyticsMerchantReportMapper.xml

@@ -0,0 +1,130 @@
+<?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.AnalyticsMerchantReportMapper">
+
+    <sql id="merchantUnion">
+        SELECT merchant_id, shop_type, gmv, pay_count, verify_count, settle_time, settle_status
+        FROM analytics_merchant_stat_history
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        UNION ALL
+        SELECT merchant_id, shop_type, gmv, pay_count, verify_count, settle_time, settle_status
+        FROM analytics_merchant_stat_today
+        WHERE #{endDate} &gt;= CURDATE()
+          AND #{startDate} &lt;= CURDATE()
+    </sql>
+
+    <select id="sumMerchantMetrics" resultType="java.util.HashMap">
+        SELECT COALESCE(SUM(today_gmv), 0) AS totalGmv,
+               COALESCE(AVG(avg_order_amount), 0) AS avgOrderAmount,
+               COALESCE(AVG(verify_rate), 0) AS verifyRate
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+    </select>
+
+    <select id="listGmvTrend" resultType="java.util.HashMap">
+        SELECT stat_date AS statDate,
+               DATE_FORMAT(stat_date, '%m-%d') AS label,
+               COALESCE(today_gmv, 0) AS gmv
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        ORDER BY stat_date ASC
+    </select>
+
+    <select id="sumGmvByCategory" resultType="java.util.HashMap">
+        SELECT business_category AS code,
+               COALESCE(SUM(gmv), 0) AS gmv
+        FROM (
+            SELECT business_category, gmv
+            FROM analytics_category_daily_history
+            WHERE stat_date &gt;= #{startDate}
+              AND stat_date &lt;= #{endDate}
+            UNION ALL
+            SELECT business_category, gmv
+            FROM analytics_category_daily_today
+            WHERE #{endDate} &gt;= CURDATE()
+              AND #{startDate} &lt;= CURDATE()
+        ) t
+        GROUP BY business_category
+        ORDER BY gmv DESC
+    </select>
+
+    <select id="listAvgOrderTrend" resultType="java.util.HashMap">
+        SELECT d.stat_date AS statDate,
+               DATE_FORMAT(d.stat_date, '%m-%d') AS label,
+               COALESCE(d.avg_order_amount, 0) AS avgOrderAmount,
+               COALESCE(m.avgPayCount, 0) AS avgPayCount
+        FROM analytics_daily_summary d
+        LEFT JOIN (
+            SELECT stat_date,
+                   ROUND(SUM(pay_count) / NULLIF(COUNT(DISTINCT CASE WHEN pay_count &gt; 0 THEN merchant_id END), 0), 2) AS avgPayCount
+            FROM (
+                SELECT stat_date, merchant_id, pay_count
+                FROM analytics_merchant_stat_history
+                WHERE stat_date &gt;= #{startDate}
+                  AND stat_date &lt;= #{endDate}
+                UNION ALL
+                SELECT CURDATE() AS stat_date, merchant_id, pay_count
+                FROM analytics_merchant_stat_today
+                WHERE #{endDate} &gt;= CURDATE()
+                  AND #{startDate} &lt;= CURDATE()
+            ) pay
+            GROUP BY stat_date
+        ) m ON d.stat_date = m.stat_date
+        WHERE d.stat_date &gt;= #{startDate}
+          AND d.stat_date &lt;= #{endDate}
+        ORDER BY d.stat_date ASC
+    </select>
+
+    <select id="listMerchantGmvTop10" resultType="java.util.HashMap">
+        SELECT merchant_id AS merchantId,
+               MAX(shop_type) AS shopType,
+               COALESCE(SUM(gmv), 0) AS gmv
+        FROM (
+            <include refid="merchantUnion"/>
+        ) t
+        GROUP BY merchant_id
+        ORDER BY gmv DESC
+        LIMIT 10
+    </select>
+
+    <select id="pageSettledMerchantDetail" resultType="shop.alien.entity.analytics.AnalyticsMerchantStat">
+        SELECT base.merchant_id AS merchantId,
+               base.shop_type AS shopType,
+               base.settle_time AS settleTime,
+               base.settle_status AS settleStatus,
+               COALESCE(period.gmv, 0) AS gmv
+        FROM (
+            SELECT merchant_id,
+                   MAX(shop_type) AS shop_type,
+                   MAX(settle_time) AS settle_time,
+                   MAX(settle_status) AS settle_status
+            FROM (
+                SELECT merchant_id, shop_type, settle_time, settle_status
+                FROM analytics_merchant_stat_history
+                WHERE settle_status = 1
+                  AND settle_time IS NOT NULL
+                  AND settle_time &gt;= #{startDate}
+                  AND settle_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+                UNION ALL
+                SELECT merchant_id, shop_type, settle_time, settle_status
+                FROM analytics_merchant_stat_today
+                WHERE settle_status = 1
+                  AND settle_time IS NOT NULL
+                  AND settle_time &gt;= #{startDate}
+                  AND settle_time &lt; DATE_ADD(#{endDate}, INTERVAL 1 DAY)
+            ) settled
+            GROUP BY merchant_id
+        ) base
+        LEFT JOIN (
+            SELECT merchant_id, COALESCE(SUM(gmv), 0) AS gmv
+            FROM (
+                <include refid="merchantUnion"/>
+            ) t
+            GROUP BY merchant_id
+        ) period ON base.merchant_id = period.merchant_id
+        ORDER BY base.settle_time DESC
+    </select>
+</mapper>

+ 110 - 0
alien-entity/src/main/resources/mapper/AnalyticsUserReportMapper.xml

@@ -0,0 +1,110 @@
+<?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.AnalyticsUserReportMapper">
+
+    <sql id="activeUserUnion">
+        SELECT user_id, gender, age_group, city, is_new_user, register_time, user_phone, channel
+        FROM analytics_user_stat_history
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        UNION ALL
+        SELECT user_id, gender, age_group, city, is_new_user, register_time, user_phone, channel
+        FROM analytics_user_stat_today
+        WHERE #{endDate} &gt;= CURDATE()
+          AND #{startDate} &lt;= CURDATE()
+    </sql>
+
+    <select id="sumRegisterFunnel" resultType="java.util.HashMap">
+        SELECT COALESCE(SUM(register_page_view_count), 0)      AS pageViewCount,
+               COALESCE(SUM(register_phone_submit_count), 0)   AS phoneSubmitCount,
+               COALESCE(SUM(register_otp_pass_count), 0)       AS otpPassCount,
+               COALESCE(SUM(register_password_set_count), 0)   AS passwordSetCount,
+               COALESCE(SUM(register_success_count), 0)        AS successCount,
+               COALESCE(SUM(new_user_count), 0)                AS newUserCount
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+    </select>
+
+    <select id="countGenderDistribution" resultType="java.util.HashMap">
+        SELECT gender AS code,
+               COUNT(DISTINCT user_id) AS cnt
+        FROM (
+            <include refid="activeUserUnion"/>
+        ) t
+        GROUP BY gender
+        ORDER BY cnt DESC
+    </select>
+
+    <select id="countAgeDistribution" resultType="java.util.HashMap">
+        SELECT COALESCE(NULLIF(age_group, ''), '未知') AS code,
+               COUNT(DISTINCT user_id) AS cnt
+        FROM (
+            <include refid="activeUserUnion"/>
+        ) t
+        GROUP BY COALESCE(NULLIF(age_group, ''), '未知')
+        ORDER BY cnt DESC
+    </select>
+
+    <select id="countCityTop10" resultType="java.util.HashMap">
+        SELECT COALESCE(NULLIF(city, ''), '未知') AS code,
+               COUNT(DISTINCT user_id) AS cnt
+        FROM (
+            <include refid="activeUserUnion"/>
+        ) t
+        GROUP BY COALESCE(NULLIF(city, ''), '未知')
+        ORDER BY cnt DESC
+        LIMIT 10
+    </select>
+
+    <select id="countUserTypeRatio" resultType="java.util.HashMap">
+        SELECT COUNT(DISTINCT user_id) AS activeUserCount,
+               COUNT(DISTINCT CASE WHEN is_new_user = 1 THEN user_id END) AS newUserCount
+        FROM (
+            <include refid="activeUserUnion"/>
+        ) t
+    </select>
+
+    <select id="pageNewUsersInRange" resultType="shop.alien.entity.analytics.AnalyticsUserStat">
+        SELECT t.user_id AS userId,
+               t.user_phone AS userPhone,
+               t.register_time AS registerTime,
+               t.city,
+               t.channel,
+               t.gender,
+               t.age,
+               t.age_group AS ageGroup,
+               t.is_new_user AS isNewUser
+        FROM (
+            SELECT user_id, user_phone, register_time, city, channel, gender, age, age_group, is_new_user
+            FROM analytics_user_stat_history
+            WHERE register_time &gt;= #{startTime}
+              AND register_time &lt; #{endTime}
+              AND is_new_user = 1
+            UNION ALL
+            SELECT user_id, user_phone, register_time, city, channel, gender, age, age_group, is_new_user
+            FROM analytics_user_stat_today
+            WHERE register_time &gt;= #{startTime}
+              AND register_time &lt; #{endTime}
+              AND is_new_user = 1
+        ) t
+        INNER JOIN (
+            SELECT user_id, MAX(register_time) AS max_register_time
+            FROM (
+                SELECT user_id, register_time
+                FROM analytics_user_stat_history
+                WHERE register_time &gt;= #{startTime}
+                  AND register_time &lt; #{endTime}
+                  AND is_new_user = 1
+                UNION ALL
+                SELECT user_id, register_time
+                FROM analytics_user_stat_today
+                WHERE register_time &gt;= #{startTime}
+                  AND register_time &lt; #{endTime}
+                  AND is_new_user = 1
+            ) u
+            GROUP BY user_id
+        ) latest ON t.user_id = latest.user_id AND t.register_time = latest.max_register_time
+        ORDER BY t.register_time DESC
+    </select>
+</mapper>

+ 8 - 4
alien-store/doc/analytics-front-sdk.js

@@ -103,9 +103,11 @@ const Analytics = {
     return request('/analytics/front/batch', { events: list });
   },
 
-  /** 在线心跳,durationMs 为距上次心跳的毫秒数 */
+  /** 在线心跳,durationMs 为距上次心跳的毫秒数;userId 必填 */
   heartbeat(durationMs, extra = {}) {
+    const userId = typeof extra.userId === 'function' ? extra.userId() : extra.userId;
     return request('/analytics/front/heartbeat', {
+      userId,
       durationMs,
       deviceType: extra.deviceType || getDeviceType(),
       channel: extra.channel || config.channel,
@@ -230,21 +232,23 @@ const Analytics = {
       eventId: data.eventId || uuid(),
       contentId: data.contentId,
       contentType: data.contentType,
+      contentTitle: data.contentTitle,
+      businessCategory: data.businessCategory,
       authorType: data.authorType,
       authorId: data.authorId,
       publishTime: data.publishTime,
     });
   },
 
-  /** 启动心跳定时器,默认60秒 */
-  startHeartbeat(intervalMs = 60000) {
+  /** 启动心跳定时器,默认60秒;extra.userId 可传固定值或 getter 函数 */
+  startHeartbeat(intervalMs = 60000, extra = {}) {
     if (this._heartbeatTimer) {
       clearInterval(this._heartbeatTimer);
     }
     let last = Date.now();
     this._heartbeatTimer = setInterval(() => {
       const now = Date.now();
-      this.heartbeat(now - last).catch(() => {});
+      this.heartbeat(now - last, extra).catch(() => {});
       last = now;
     }, intervalMs);
   },

+ 135 - 25
alien-store/doc/前端埋点接入指南.md

@@ -46,19 +46,22 @@ Analytics.init({
 });
 
 Analytics.onLaunch();
-Analytics.startHeartbeat(60000);
+Analytics.startHeartbeat(60000, { userId: () => uni.getStorageSync('userId') });
 ```
 
 ### 3. 用户注册 / 登录 / 登出
 
 ```javascript
-// 注册成功
+// 用户注册(建议补充性别/年龄段,用于用户报表分布)
 Analytics.userRegister({
   userId: 10001,
   userPhone: '13800138000',
   registerTime: '2026-06-15 10:00:00',
   channel: 'app_store',
   city: '北京',
+  gender: 1,        // 0未知 1男 2女
+  age: 28,
+  ageGroup: '25-29', // 18-24|25-29|30-34|35-39|40-49|50+
 });
 
 // 登录成功
@@ -94,8 +97,10 @@ Analytics.aiChatEnd({
 ```javascript
 Analytics.contentPublish({
   contentId: 90001,
-  contentType: 1,   // 1动态 2打卡 3二手商品
-  authorType: 1,    // 1用户 2商家
+  contentType: 1,        // 1动态 2打卡 3二手商品
+  contentTitle: '大连最好吃的5家日料店推荐',
+  businessCategory: 1,   // 1美食 2休闲娱乐 3生活服务 4旅游 5酒店 6购物 7其他
+  authorType: 1,         // 1用户 2商家
   authorId: 10001,
   publishTime: '2026-06-15 16:00:00',
 });
@@ -104,12 +109,13 @@ Analytics.contentPublish({
 ### 6. 内容互动(评论 / 点赞 / 收藏)
 
 ```javascript
-// 点赞 +1
+// 点赞 +1(建议传 eventSubtype 便于拆分点赞/评论/分享)
 Analytics.contentInteract({
   contentId: 90001,
   contentType: 1,
   increment: 1,
   userId: 10001,
+  eventSubtype: 'like',   // like | comment | share
 });
 
 // 取消点赞 -1
@@ -136,6 +142,18 @@ Analytics.merchantView({
   userId: 10001,
 });
 
+// 商户转化漏斗(列表曝光 → 点击 → 详情 → 电话/导航)
+Analytics.report('MERCHANT_EXPOSE', { merchantId: 20001, shopType: 1 });
+Analytics.report('MERCHANT_CLICK', { merchantId: 20001, shopType: 1 });
+Analytics.report('MERCHANT_DETAIL', { merchantId: 20001, shopType: 1 });
+Analytics.report('MERCHANT_CONTACT', { merchantId: 20001, shopType: 1 });
+
+// 注册转化漏斗
+Analytics.report('REGISTER_PAGE', { channel: 'app_store' });
+Analytics.report('REGISTER_PHONE', { userPhone: '13800138000' });
+Analytics.report('REGISTER_OTP', { userPhone: '13800138000' });
+Analytics.report('REGISTER_PASSWORD', { userPhone: '13800138000' });
+
 // 兼容旧写法(需传 shopType、visitPv、visitUv)
 Analytics.onMerchantView(20001, {
   shopType: 1,
@@ -158,8 +176,13 @@ Analytics.onMerchantView(merchantId, {
 // 内容互动(contentType: 1动态 2打卡 3二手商品)
 Analytics.onContentInteract(contentId, 1);
 
-// 支付成功
-Analytics.report('PAY_SUCCESS', { amount: 99.00, targetId: orderId });
+// 支付成功(建议传 merchantId、businessCategory 支撑商家 GMV 分类)
+Analytics.report('PAY_SUCCESS', {
+  amount: 99.00,
+  targetId: orderId,
+  merchantId: 20001,
+  businessCategory: 1,
+});
 
 // AI 请求耗时
 Analytics.aiRequest({
@@ -178,6 +201,8 @@ Analytics.aiRequest({
 |------|------|
 | userId / merchantId / targetId | 用户、商户、目标对象 |
 | contentType | 1动态 2打卡 3二手商品 |
+| eventSubtype | 事件子类型(见下方枚举) |
+| businessCategory | 经营品类(见下方枚举,内容发布/支付时建议传) |
 | amount / durationMs | 金额、时长 |
 | deviceType / channel / city | 设备、渠道、城市 |
 
@@ -188,17 +213,49 @@ Analytics.aiRequest({
 | `APP_LAUNCH` | App 启动 | channel, city |
 | `APP_REGISTER` | 用户注册 | - |
 | `USER_HEARTBEAT` | 在线心跳 | durationMs |
+| `MERCHANT_EXPOSE` | 商户曝光 | merchantId, shopType |
+| `MERCHANT_CLICK` | 商户点击 | merchantId, shopType |
+| `MERCHANT_DETAIL` | 商户详情 | merchantId, shopType |
+| `MERCHANT_CONTACT` | 电话/导航 | merchantId, shopType |
 | `MERCHANT_VIEW` | 访问商户 | merchantId |
 | `MERCHANT_VERIFY` | 商户核销 | merchantId, amount |
+| `REGISTER_PAGE` | 进入注册页 | channel |
+| `REGISTER_PHONE` | 提交手机号 | userPhone |
+| `REGISTER_OTP` | 验证码通过 | userPhone |
+| `REGISTER_PASSWORD` | 设置密码 | userPhone |
 | `CONTENT_PUBLISH` | 发布内容 | targetId, contentType |
-| `CONTENT_INTERACT` | 内容互动 | targetId, contentType |
-| `PAY_SUCCESS` | 支付成功 | targetId, amount |
+| `CONTENT_INTERACT` | 内容互动 | targetId, contentType, eventSubtype |
+| `PAY_SUCCESS` | 支付成功 | targetId, amount, merchantId, businessCategory |
 | `AI_CHAT_START` | AI 对话开始 | - |
 | `REPORT_SUBMIT` | 提交举报 | targetId |
 | `AUDIT_SUBMIT` | 提交审核 | targetId, contentType |
+| `AUDIT_REJECT` | 审核驳回 | targetId, contentType, eventSubtype=reject |
 
 完整列表调用:`GET /analytics/front/scenes`
 
+### eventSubtype 子类型枚举
+
+| 值 | 含义 | 适用 scene |
+|----|------|------------|
+| `like` | 点赞 | CONTENT_INTERACT |
+| `comment` | 评论 | CONTENT_INTERACT |
+| `share` | 分享 | CONTENT_INTERACT |
+| `ai_pass` | AI审核通过 | AUDIT_SUBMIT |
+| `manual_pass` | 人工审核通过 | AUDIT_SUBMIT |
+| `reject` | 审核驳回 | AUDIT_REJECT |
+
+### businessCategory 经营品类枚举
+
+| 值 | 含义 |
+|----|------|
+| 1 | 美食 |
+| 2 | 休闲娱乐 |
+| 3 | 生活服务 |
+| 4 | 旅游 |
+| 5 | 酒店 |
+| 6 | 购物 |
+| 7 | 其他 |
+
 ## 五、请求示例
 
 ### 统一上报
@@ -225,10 +282,15 @@ Content-Type: application/json
 POST /analytics/front/heartbeat
 
 {
-  "durationMs": 60000
+  "userId": 10001,
+  "durationMs": 60000,
+  "deviceType": "iOS 17",
+  "city": "大连"
 }
 ```
 
+> `userId` **必填**(用于写入当日用户明细、支撑「当前在线用户」看板)。除事件表外,会同步更新 `analytics_user_stat_today` 的 `last_active_time` 与 `online_duration_min`。
+
 ### 用户注册
 
 ```http
@@ -243,6 +305,8 @@ POST /analytics/front/user/register
 }
 ```
 
+> 注册成功后会写入当日用户明细,并标记 `is_new_user = 1`。
+
 ### 用户登录
 
 ```http
@@ -292,12 +356,16 @@ POST /analytics/front/content/publish
 {
   "contentId": 90001,
   "contentType": 1,
+  "contentTitle": "大连最好吃的5家日料店推荐",
+  "businessCategory": 1,
   "authorType": 1,
   "authorId": 10001,
   "publishTime": "2026-06-15 16:00:00"
 }
 ```
 
+> `contentTitle`、`businessCategory` 用于内容发布明细看板,**建议发布时必传**。
+
 ### 内容互动
 
 ```http
@@ -311,7 +379,7 @@ POST /analytics/front/content/interact
 }
 ```
 
-> `increment` 由前端决定增减幅度:评论/点赞/收藏传正数,取消操作传负数。会实时累加到 `analytics_content_stat.interaction_count`。
+> `increment` 由前端决定增减幅度:评论/点赞/收藏传正数,取消操作传负数。会实时累加到 `analytics_content_stat_today.interaction_count`。
 
 ### 商家浏览
 
@@ -360,37 +428,79 @@ POST /analytics/front/ai-request
 }
 ```
 
-## 六、幂等与防重复
+## 六、明细存储说明(今日表 + 历史表)
+
+埋点实时写入 **今日明细表**,每天零点由定时任务归档到历史表(保留 30 天):
+
+| 维度 | 今日表 | 历史表 | 唯一键(今日) |
+|------|--------|--------|----------------|
+| 用户 | `analytics_user_stat_today` | `analytics_user_stat_history` | `user_id`(每用户每日一行) |
+| 商户 | `analytics_merchant_stat_today` | `analytics_merchant_stat_history` | `merchant_id`(每商家每日一行) |
+| 内容 | `analytics_content_stat_today` | `analytics_content_stat_history` | `content_type + content_id` |
+
+- 用户注册会标记 `is_new_user = 1`,用于「新增用户明细」查询。
+- 展示字段(用户昵称、商家名称、作者名称、手机号脱敏)**不入库**,由查询接口按 ID 回填。
+
+建表脚本(新环境执行一次):
+
+```
+alien-entity/src/main/resources/db/migration/analytics_schema.sql
+```
+
+旧环境升级见同目录 `analytics_tables_dashboard_upgrade.sql`。
+
+## 七、幂等与防重复
 
 - 每条上报建议传 `eventId`(前端 UUID)
 - 相同 `eventId` 重复提交会被服务端忽略,不会重复计数
 
-## 七、管理端统计
+## 八、管理端统计与查询
 
-建表脚本:`alien-entity/src/main/resources/db/migration/analytics_tables.sql`
+埋点写明细与事件,看板 KPI 由 **alien-job** 定时任务或手动接口汇总到 `analytics_daily_summary`。
 
-埋点只写明细,看板数据由 **alien-job** 模块 XXL-JOB 定时任务或手动接口触发汇总:
+### 定时任务(XXL-JOB)
 
-| XXL-JOB Handler | Cron 建议 | 说明 |
-|-----------------|-----------|------|
+| Handler | Cron 建议 | 说明 |
+|---------|-----------|------|
+| `analyticsArchiveDaily` | `0 0 0 * * ?` | **零点**将昨日今日表明细归档到历史表 |
 | `analyticsCalculateTodayHourly` | `0 0 * * * ?` | 每小时汇总当日 |
-| `analyticsCalculateYesterdayDaily` | `0 0 2 * * ?` | 每天凌晨2点汇总前日 |
-| `analyticsCalculateRetention` | `0 0 3 * * ?` | 每天凌晨3点计算留存 |
+| `analyticsCalculateYesterdayDaily` | `0 0 2 * * ?` | 凌晨 2 点汇总前日(在归档之后) |
+| `analyticsCalculateRetention` | `0 0 3 * * ?` | 凌晨 3 点计算留存 |
 
-手动触发:
+### 手动触发
 
 ```http
 POST /analytics/stat/calculate
 { "statDate": "2026-06-15", "scope": "ALL" }
+
+POST /analytics/stat/archive
 ```
 
-查询看板:
+### 看板查询(管理端)
 
-```http
-GET /analytics/stat/daily?statDate=2026-06-15
-```
+**周期参数**(四个报表模块通用):`period=TODAY|YESTERDAY|LAST_7D|LAST_30D`
+
+| 模块 | 汇总接口 | 说明 |
+|------|----------|------|
+| 数据看板 | `GET /analytics/dashboard/charts?period=` | 含顶部 KPI + 6 张图表 |
+| 数据看板 | `GET /analytics/dashboard/summary?period=` | 仅顶部 KPI |
+| 用户报表 | `GET /analytics/user-report/charts?period=&detailPage=1&detailSize=10` | 含新注册用户明细分页 |
+| 内容报表 | `GET /analytics/content-report/charts?period=&detailPage=1&detailSize=10` | 含审核通过列表分页 |
+| 商家报表 | `GET /analytics/merchant-report/charts?period=` | GMV/分类/客单价趋势等 |
+
+| 接口 | 说明 |
+|------|------|
+| `GET /analytics/stat/daily?statDate=` | 平台日统计 KPI |
+| `GET /analytics/stat/user/page?statDate=` | DAU 用户明细 |
+| `GET /analytics/stat/user/new/page?statDate=` | 新增用户明细 |
+| `GET /analytics/stat/user/online/page?onlineWithinMin=5` | 当前在线用户(仅今日) |
+| `GET /analytics/stat/merchant/page?statDate=` | 商家访问 UV 明细 |
+| `GET /analytics/stat/content/page?statDate=&contentType=` | 内容发布明细 |
+| `GET /analytics/stat/conversion/page?startDate=&endDate=` | 转化率日明细 |
+
+> 查询历史日期时读历史表;`statDate` 为空或当天时读今日表。
 
-## 八、代码目录(与旧埋点完全隔离)
+## 、代码目录(与旧埋点完全隔离)
 
 ```
 alien-store/src/main/java/shop/alien/store/

+ 83 - 0
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsContentReportController.java

@@ -0,0 +1,83 @@
+package shop.alien.store.controller.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
+import shop.alien.entity.analytics.vo.contentreport.*;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.analytics.AnalyticsContentReportService;
+
+import java.util.List;
+
+@Slf4j
+@Api(tags = {"平台埋点-内容报表"})
+@ApiSort(25)
+@CrossOrigin
+@RestController
+@RequestMapping("/analytics/content-report")
+@RequiredArgsConstructor
+public class AnalyticsContentReportController {
+
+    private final AnalyticsContentReportService contentReportService;
+
+    @ApiOperation("内容报表汇总(一次加载全部图表,含底部分页)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/charts")
+    public R<AnalyticsContentReportChartsVo> loadAllCharts(
+            @ApiParam(value = "统计周期", allowableValues = "TODAY,YESTERDAY,LAST_7D,LAST_30D")
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long detailPage,
+            @RequestParam(defaultValue = "10") long detailSize) {
+        return R.data(contentReportService.loadAllCharts(period, detailPage, detailSize));
+    }
+
+    @ApiOperation("顶部指标卡片")
+    @ApiOperationSupport(order = 2)
+    @GetMapping("/summary")
+    public R<AnalyticsContentReportSummaryVo> summary(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(contentReportService.summary(period));
+    }
+
+    @ApiOperation("内容分类分布")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/category-distribution")
+    public R<List<AnalyticsDistributionItemVo>> categoryDistribution(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(contentReportService.categoryDistribution(period));
+    }
+
+    @ApiOperation("内容审核趋势")
+    @ApiOperationSupport(order = 4)
+    @GetMapping("/audit-trend")
+    public R<List<AnalyticsAuditTrendVo>> auditTrend(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_7D) String period) {
+        return R.data(contentReportService.auditTrend(period));
+    }
+
+    @ApiOperation("内容互动TOP10")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/interaction-top10")
+    public R<List<AnalyticsContentInteractionRankVo>> interactionTop10(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_7D) String period) {
+        return R.data(contentReportService.interactionTop10(period));
+    }
+
+    @ApiOperation("近期审核通过列表(分页)")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/audit-passed/page")
+    public R<IPage<AnalyticsContentReportDetailVo>> pageAuditPassed(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_7D) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(contentReportService.pageAuditPassed(period, page, size));
+    }
+}

+ 96 - 0
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsDashboardController.java

@@ -0,0 +1,96 @@
+package shop.alien.store.controller.analytics;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
+import shop.alien.entity.analytics.vo.dashboard.*;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.analytics.AnalyticsDashboardService;
+
+import java.util.List;
+
+/**
+ * 平台埋点 - 数据看板图表
+ */
+@Slf4j
+@Api(tags = {"平台埋点-数据看板"})
+@ApiSort(23)
+@CrossOrigin
+@RestController
+@RequestMapping("/analytics/dashboard")
+@RequiredArgsConstructor
+public class AnalyticsDashboardController {
+
+    private final AnalyticsDashboardService dashboardService;
+
+    @ApiOperation("六图汇总(一次加载全部图表,含顶部 KPI)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/charts")
+    public R<AnalyticsDashboardChartsVo> loadAllCharts(
+            @ApiParam(value = "统计周期", allowableValues = "TODAY,YESTERDAY,LAST_7D,LAST_30D")
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.loadAllCharts(period));
+    }
+
+    @ApiOperation("顶部 KPI 指标卡片")
+    @ApiOperationSupport(order = 2)
+    @GetMapping("/summary")
+    public R<AnalyticsDashboardSummaryVo> summary(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.summary(period));
+    }
+
+    @ApiOperation("用户活跃趋势(DAU/WAU/MAU)")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/user-active-trend")
+    public R<List<AnalyticsUserActiveTrendVo>> userActiveTrend(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.userActiveTrend(period));
+    }
+
+    @ApiOperation("对话数据趋势(对话次数/对话人数)")
+    @ApiOperationSupport(order = 4)
+    @GetMapping("/conversation-trend")
+    public R<List<AnalyticsConversationTrendVo>> conversationTrend(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.conversationTrend(period));
+    }
+
+    @ApiOperation("内容互动趋势(发布量/互动量)")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/content-trend")
+    public R<List<AnalyticsContentTrendVo>> contentTrend(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.contentTrend(period));
+    }
+
+    @ApiOperation("转化漏斗(曝光→点击→详情→电话/导航→到店)")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/conversion-funnel")
+    public R<List<AnalyticsFunnelStageVo>> conversionFunnel(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.conversionFunnel(period));
+    }
+
+    @ApiOperation("用户留存率(Day1~Day30)")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/user-retention")
+    public R<List<AnalyticsRetentionPointVo>> userRetention(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.userRetention(period));
+    }
+
+    @ApiOperation("人均使用时长趋势(分钟)")
+    @ApiOperationSupport(order = 8)
+    @GetMapping("/avg-usage-duration")
+    public R<List<AnalyticsAvgDurationTrendVo>> avgUsageDuration(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(dashboardService.avgUsageDuration(period));
+    }
+}

+ 89 - 0
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsMerchantReportController.java

@@ -0,0 +1,89 @@
+package shop.alien.store.controller.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
+import shop.alien.entity.analytics.vo.merchantreport.*;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.analytics.AnalyticsMerchantReportService;
+
+import java.util.List;
+
+@Slf4j
+@Api(tags = {"平台埋点-商家报表"})
+@ApiSort(26)
+@CrossOrigin
+@RestController
+@RequestMapping("/analytics/merchant-report")
+@RequiredArgsConstructor
+public class AnalyticsMerchantReportController {
+
+    private final AnalyticsMerchantReportService merchantReportService;
+
+    @ApiOperation("商家报表汇总(一次加载全部图表)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/charts")
+    public R<AnalyticsMerchantReportChartsVo> loadAllCharts(
+            @ApiParam(value = "统计周期", allowableValues = "TODAY,YESTERDAY,LAST_7D,LAST_30D")
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
+        return R.data(merchantReportService.loadAllCharts(period));
+    }
+
+    @ApiOperation("顶部指标卡片")
+    @ApiOperationSupport(order = 2)
+    @GetMapping("/summary")
+    public R<AnalyticsMerchantReportSummaryVo> summary(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
+        return R.data(merchantReportService.summary(period));
+    }
+
+    @ApiOperation("GMV趋势")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/gmv-trend")
+    public R<List<AnalyticsGmvTrendVo>> gmvTrend(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
+        return R.data(merchantReportService.gmvTrend(period));
+    }
+
+    @ApiOperation("各类商家GMV占比")
+    @ApiOperationSupport(order = 4)
+    @GetMapping("/gmv-category-distribution")
+    public R<List<AnalyticsDistributionItemVo>> gmvCategoryDistribution(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
+        return R.data(merchantReportService.gmvCategoryDistribution(period));
+    }
+
+    @ApiOperation("商家平均单量/客单价趋势")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/avg-order-trend")
+    public R<List<AnalyticsAvgOrderTrendVo>> avgOrderTrend(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
+        return R.data(merchantReportService.avgOrderTrend(period));
+    }
+
+    @ApiOperation("商家GMV TOP10")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/gmv-top10")
+    public R<List<AnalyticsMerchantGmvRankVo>> gmvTop10(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
+        return R.data(merchantReportService.gmvTop10(period));
+    }
+
+    @ApiOperation("入驻商家明细分页")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/settled/page")
+    public R<IPage<AnalyticsMerchantReportDetailVo>> pageSettledMerchant(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(merchantReportService.pageSettledMerchant(period, page, size));
+    }
+}

+ 102 - 0
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsUserReportController.java

@@ -0,0 +1,102 @@
+package shop.alien.store.controller.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiOperationSupport;
+import io.swagger.annotations.ApiParam;
+import io.swagger.annotations.ApiSort;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsFunnelStageVo;
+import shop.alien.entity.analytics.vo.userreport.*;
+import shop.alien.entity.result.R;
+import shop.alien.store.service.analytics.AnalyticsUserReportService;
+
+import java.util.List;
+
+/**
+ * 平台埋点 - 用户报表
+ */
+@Slf4j
+@Api(tags = {"平台埋点-用户报表"})
+@ApiSort(24)
+@CrossOrigin
+@RestController
+@RequestMapping("/analytics/user-report")
+@RequiredArgsConstructor
+public class AnalyticsUserReportController {
+
+    private final AnalyticsUserReportService userReportService;
+
+    @ApiOperation("用户报表汇总(一次加载全部图表,含底部分页)")
+    @ApiOperationSupport(order = 1)
+    @GetMapping("/charts")
+    public R<AnalyticsUserReportChartsVo> loadAllCharts(
+            @ApiParam(value = "统计周期", allowableValues = "TODAY,YESTERDAY,LAST_7D,LAST_30D")
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @RequestParam(defaultValue = "1") long detailPage,
+            @RequestParam(defaultValue = "10") long detailSize) {
+        return R.data(userReportService.loadAllCharts(period, detailPage, detailSize));
+    }
+
+    @ApiOperation("顶部指标卡片")
+    @ApiOperationSupport(order = 2)
+    @GetMapping("/summary")
+    public R<AnalyticsUserReportSummaryVo> summary(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(userReportService.summary(period));
+    }
+
+    @ApiOperation("注册转化漏斗")
+    @ApiOperationSupport(order = 3)
+    @GetMapping("/register-funnel")
+    public R<List<AnalyticsFunnelStageVo>> registerFunnel(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(userReportService.registerFunnel(period));
+    }
+
+    @ApiOperation("性别分布")
+    @ApiOperationSupport(order = 4)
+    @GetMapping("/gender-distribution")
+    public R<List<AnalyticsDistributionItemVo>> genderDistribution(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(userReportService.genderDistribution(period));
+    }
+
+    @ApiOperation("年龄分布")
+    @ApiOperationSupport(order = 5)
+    @GetMapping("/age-distribution")
+    public R<List<AnalyticsDistributionItemVo>> ageDistribution(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(userReportService.ageDistribution(period));
+    }
+
+    @ApiOperation("新/老用户占比")
+    @ApiOperationSupport(order = 6)
+    @GetMapping("/user-type-ratio")
+    public R<AnalyticsUserTypeRatioVo> userTypeRatio(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(userReportService.userTypeRatio(period));
+    }
+
+    @ApiOperation("地域分布TOP10")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/geo-top10")
+    public R<List<AnalyticsDistributionItemVo>> geoTop10(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
+        return R.data(userReportService.geoTop10(period));
+    }
+
+    @ApiOperation("新注册用户明细分页")
+    @ApiOperationSupport(order = 8)
+    @GetMapping("/new-user/page")
+    public R<IPage<AnalyticsUserReportDetailVo>> pageNewUserDetail(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_7D) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(userReportService.pageNewUserDetail(period, page, size));
+    }
+}

+ 24 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsContentReportService.java

@@ -0,0 +1,24 @@
+package shop.alien.store.service.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.vo.contentreport.*;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+
+import java.util.List;
+
+public interface AnalyticsContentReportService {
+
+    AnalyticsContentReportChartsVo loadAllCharts(String period);
+
+    AnalyticsContentReportChartsVo loadAllCharts(String period, long detailPage, long detailSize);
+
+    AnalyticsContentReportSummaryVo summary(String period);
+
+    List<AnalyticsDistributionItemVo> categoryDistribution(String period);
+
+    List<AnalyticsAuditTrendVo> auditTrend(String period);
+
+    List<AnalyticsContentInteractionRankVo> interactionTop10(String period);
+
+    IPage<AnalyticsContentReportDetailVo> pageAuditPassed(String period, long page, long size);
+}

+ 27 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsDashboardService.java

@@ -0,0 +1,27 @@
+package shop.alien.store.service.analytics;
+
+import shop.alien.entity.analytics.vo.dashboard.*;
+
+import java.util.List;
+
+/**
+ * 数据看板图表查询
+ */
+public interface AnalyticsDashboardService {
+
+    AnalyticsDashboardChartsVo loadAllCharts(String period);
+
+    AnalyticsDashboardSummaryVo summary(String period);
+
+    List<AnalyticsUserActiveTrendVo> userActiveTrend(String period);
+
+    List<AnalyticsConversationTrendVo> conversationTrend(String period);
+
+    List<AnalyticsContentTrendVo> contentTrend(String period);
+
+    List<AnalyticsFunnelStageVo> conversionFunnel(String period);
+
+    List<AnalyticsRetentionPointVo> userRetention(String period);
+
+    List<AnalyticsAvgDurationTrendVo> avgUsageDuration(String period);
+}

+ 24 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsMerchantReportService.java

@@ -0,0 +1,24 @@
+package shop.alien.store.service.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.vo.merchantreport.*;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+
+import java.util.List;
+
+public interface AnalyticsMerchantReportService {
+
+    AnalyticsMerchantReportChartsVo loadAllCharts(String period);
+
+    AnalyticsMerchantReportSummaryVo summary(String period);
+
+    List<AnalyticsGmvTrendVo> gmvTrend(String period);
+
+    List<AnalyticsDistributionItemVo> gmvCategoryDistribution(String period);
+
+    List<AnalyticsAvgOrderTrendVo> avgOrderTrend(String period);
+
+    List<AnalyticsMerchantGmvRankVo> gmvTop10(String period);
+
+    IPage<AnalyticsMerchantReportDetailVo> pageSettledMerchant(String period, long page, long size);
+}

+ 31 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsUserReportService.java

@@ -0,0 +1,31 @@
+package shop.alien.store.service.analytics;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsFunnelStageVo;
+import shop.alien.entity.analytics.vo.userreport.*;
+
+import java.util.List;
+
+/**
+ * 用户报表图表查询
+ */
+public interface AnalyticsUserReportService {
+
+    AnalyticsUserReportChartsVo loadAllCharts(String period);
+
+    AnalyticsUserReportChartsVo loadAllCharts(String period, long detailPage, long detailSize);
+
+    AnalyticsUserReportSummaryVo summary(String period);
+
+    List<AnalyticsFunnelStageVo> registerFunnel(String period);
+
+    List<AnalyticsDistributionItemVo> genderDistribution(String period);
+
+    List<AnalyticsDistributionItemVo> ageDistribution(String period);
+
+    AnalyticsUserTypeRatioVo userTypeRatio(String period);
+
+    List<AnalyticsDistributionItemVo> geoTop10(String period);
+
+    IPage<AnalyticsUserReportDetailVo> pageNewUserDetail(String period, long page, long size);
+}

+ 293 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsContentReportServiceImpl.java

@@ -0,0 +1,293 @@
+package shop.alien.store.service.analytics.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.analytics.AnalyticsContentStat;
+import shop.alien.entity.analytics.AnalyticsEventCode;
+import shop.alien.entity.analytics.vo.contentreport.*;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+import shop.alien.mapper.AnalyticsContentReportMapper;
+import shop.alien.mapper.AnalyticsEventMapper;
+import shop.alien.store.service.analytics.AnalyticsContentReportService;
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
+import shop.alien.store.util.analytics.AnalyticsPeriodContext;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AnalyticsContentReportServiceImpl implements AnalyticsContentReportService {
+
+    private final AnalyticsContentReportMapper contentReportMapper;
+    private final AnalyticsEventMapper eventMapper;
+
+    @Override
+    public AnalyticsContentReportChartsVo loadAllCharts(String period) {
+        return loadAllCharts(period, 1, 10);
+    }
+
+    @Override
+    public AnalyticsContentReportChartsVo loadAllCharts(String period, long detailPage, long detailSize) {
+        AnalyticsContentReportChartsVo vo = new AnalyticsContentReportChartsVo();
+        vo.setPeriod(AnalyticsPeriodContext.resolve(period).getPeriod());
+        vo.setSummary(summary(period));
+        vo.setCategoryDistribution(categoryDistribution(period));
+        vo.setAuditTrend(auditTrend(period));
+        vo.setInteractionTop10(interactionTop10(period));
+        vo.setAuditPassedPage(pageAuditPassed(period, detailPage, detailSize));
+        return vo;
+    }
+
+    @Override
+    public AnalyticsContentReportSummaryVo summary(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        AnalyticsPeriodContext prevCtx = previousContext(ctx);
+
+        Map<String, Object> now = loadMetrics(ctx);
+        Map<String, Object> prev = loadMetrics(prevCtx);
+
+        AnalyticsContentReportSummaryVo vo = new AnalyticsContentReportSummaryVo();
+        int publishNow = intValue(now.get("publishCount"));
+        int publishPrev = intValue(prev.get("publishCount"));
+        vo.setPublishCount(publishNow);
+        vo.setPublishCountChangeRate(calcChangeRate(publishNow, publishPrev));
+
+        int visitNow = intValue(now.get("interactionCount"));
+        int visitPrev = intValue(prev.get("interactionCount"));
+        vo.setVisitCount(visitNow);
+        vo.setVisitCountChangeRate(calcChangeRate(visitNow, visitPrev));
+
+        BigDecimal passRateNow = calcPassRate(now);
+        BigDecimal passRatePrev = calcPassRate(prev);
+        vo.setAuditPassRate(passRateNow);
+        vo.setAuditPassRateDelta(subtractRate(passRateNow, passRatePrev));
+        vo.setAuditProcessRate(calcProcessRate(now));
+        return vo;
+    }
+
+    @Override
+    public List<AnalyticsDistributionItemVo> categoryDistribution(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = contentReportMapper.countCategoryDistribution(ctx.getStartDate(), ctx.getEndDate());
+        long total = rows.stream().mapToLong(row -> longValue(row.get("cnt"))).sum();
+        return rows.stream().map(row -> {
+            AnalyticsDistributionItemVo item = new AnalyticsDistributionItemVo();
+            String code = String.valueOf(row.get("code"));
+            item.setCode(code);
+            item.setName(resolveContentTypeName(code));
+            long count = longValue(row.get("cnt"));
+            item.setCount(count);
+            item.setRatio(calcRatio(count, total));
+            return item;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<AnalyticsAuditTrendVo> auditTrend(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = contentReportMapper.listAuditTrend(ctx.getStartDate(), ctx.getEndDate());
+        if (rows.isEmpty()) {
+            return buildAuditTrendFromEvents(ctx);
+        }
+        return rows.stream().map(row -> {
+            AnalyticsAuditTrendVo vo = new AnalyticsAuditTrendVo();
+            Object statDate = row.get("statDate");
+            if (statDate instanceof Date) {
+                vo.setStatDate((Date) statDate);
+            }
+            vo.setLabel(stringValue(row.get("label")));
+            int submit = intValue(row.get("submitCount"));
+            int pass = intValue(row.get("passCount"));
+            int reject = intValue(row.get("rejectCount"));
+            vo.setSubmitCount(submit);
+            vo.setPassCount(pass);
+            vo.setRejectCount(reject);
+            vo.setFailCount(Math.max(submit - pass - reject, 0));
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<AnalyticsContentInteractionRankVo> interactionTop10(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = contentReportMapper.listInteractionTop10(ctx.getStartDate(), ctx.getEndDate());
+        List<AnalyticsContentInteractionRankVo> result = new ArrayList<>();
+        int rank = 1;
+        for (Map<String, Object> row : rows) {
+            AnalyticsContentInteractionRankVo vo = new AnalyticsContentInteractionRankVo();
+            vo.setRank(rank++);
+            vo.setContentId(longValue(row.get("contentId")));
+            vo.setContentTitle(stringValue(row.get("contentTitle")));
+            vo.setInteractionCount(intValue(row.get("interactionCount")));
+            vo.setLikeCount(intValue(row.get("likeCount")));
+            vo.setCommentCount(intValue(row.get("commentCount")));
+            vo.setShareCount(intValue(row.get("shareCount")));
+            result.add(vo);
+        }
+        return result;
+    }
+
+    @Override
+    public IPage<AnalyticsContentReportDetailVo> pageAuditPassed(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsContentStat> raw = contentReportMapper.pageAuditPassed(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
+        return raw.convert(this::toDetailVo);
+    }
+
+    private AnalyticsContentReportDetailVo toDetailVo(AnalyticsContentStat stat) {
+        AnalyticsContentReportDetailVo vo = new AnalyticsContentReportDetailVo();
+        vo.setContentId(stat.getContentId());
+        vo.setDisplayContentId(stat.getContentId() != null ? "R" + stat.getContentId() : null);
+        vo.setContentTitle(stat.getContentTitle());
+        vo.setContentTypeName(resolveContentTypeName(String.valueOf(stat.getContentType())));
+        vo.setAuditTime(stat.getAuditTime());
+        vo.setStatus(resolveContentStatus(stat));
+        return vo;
+    }
+
+    private String resolveContentStatus(AnalyticsContentStat stat) {
+        if (stat.getStatus() != null && stat.getStatus() == 1) {
+            return "发布中";
+        }
+        return "已通过";
+    }
+
+    private Map<String, Object> loadMetrics(AnalyticsPeriodContext ctx) {
+        Map<String, Object> metrics = contentReportMapper.sumContentMetrics(ctx.getStartDate(), ctx.getEndDate());
+        if (metrics == null) {
+            metrics = new HashMap<>();
+        }
+        if (intValue(metrics.get("publishCount")) <= 0) {
+            metrics.put("publishCount", intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.CONTENT_PUBLISH, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        }
+        if (intValue(metrics.get("interactionCount")) <= 0) {
+            metrics.put("interactionCount", intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.CONTENT_INTERACT, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        }
+        if (intValue(metrics.get("auditSubmitCount")) <= 0) {
+            metrics.put("auditSubmitCount", intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.AUDIT_SUBMIT, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+            metrics.put("auditPassCount", intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.AUDIT_PASS, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+            metrics.put("auditRejectCount", intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.AUDIT_REJECT, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        }
+        return metrics;
+    }
+
+    private List<AnalyticsAuditTrendVo> buildAuditTrendFromEvents(AnalyticsPeriodContext ctx) {
+        Date cursor = ctx.getStartDate();
+        List<AnalyticsAuditTrendVo> result = new ArrayList<>();
+        while (!cursor.after(ctx.getEndDate())) {
+            Date dayStart = AnalyticsDateUtil.dayStart(cursor);
+            Date dayEnd = AnalyticsDateUtil.dayEndExclusive(cursor);
+            int submit = intValue(eventMapper.countByEventCode(AnalyticsEventCode.AUDIT_SUBMIT, dayStart, dayEnd));
+            int pass = intValue(eventMapper.countByEventCode(AnalyticsEventCode.AUDIT_PASS, dayStart, dayEnd));
+            int reject = intValue(eventMapper.countByEventCode(AnalyticsEventCode.AUDIT_REJECT, dayStart, dayEnd));
+            AnalyticsAuditTrendVo vo = new AnalyticsAuditTrendVo();
+            vo.setStatDate(cursor);
+            vo.setLabel(new SimpleDateFormat("M-d").format(cursor));
+            vo.setSubmitCount(submit);
+            vo.setPassCount(pass);
+            vo.setRejectCount(reject);
+            vo.setFailCount(Math.max(submit - pass - reject, 0));
+            result.add(vo);
+            cursor = AnalyticsDateUtil.addDays(cursor, 1);
+        }
+        return result;
+    }
+
+    private AnalyticsPeriodContext previousContext(AnalyticsPeriodContext ctx) {
+        long days = daysBetween(ctx.getStartDate(), ctx.getEndDate());
+        Date prevEnd = AnalyticsDateUtil.addDays(ctx.getStartDate(), -1);
+        Date prevStart = AnalyticsDateUtil.addDays(prevEnd, -(int) days);
+        return new AnalyticsPeriodContext(
+                ctx.getPeriod(),
+                prevStart,
+                prevEnd,
+                AnalyticsDateUtil.dayStart(prevStart),
+                AnalyticsDateUtil.dayEndExclusive(prevEnd),
+                false,
+                prevStart
+        );
+    }
+
+    private long daysBetween(Date start, Date end) {
+        return (AnalyticsDateUtil.dayStart(end).getTime() - AnalyticsDateUtil.dayStart(start).getTime()) / 86400000L;
+    }
+
+    private BigDecimal calcPassRate(Map<String, Object> metrics) {
+        return AnalyticsDateUtil.calcRate(intValue(metrics.get("auditPassCount")), intValue(metrics.get("auditSubmitCount")));
+    }
+
+    private BigDecimal calcProcessRate(Map<String, Object> metrics) {
+        long submit = longValue(metrics.get("auditSubmitCount"));
+        long processed = longValue(metrics.get("auditPassCount")) + longValue(metrics.get("auditRejectCount"));
+        return calcRatio(processed, submit);
+    }
+
+    private String resolveContentTypeName(String code) {
+        if (code == null) {
+            return "其他";
+        }
+        switch (code) {
+            case "1": return "动态";
+            case "2": return "打卡";
+            case "3": return "二手商品";
+            default: return "其他";
+        }
+    }
+
+    private BigDecimal calcChangeRate(long current, long previous) {
+        if (previous <= 0) {
+            return current > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf((current - previous) * 100.0 / previous).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal calcRatio(long part, long total) {
+        if (total <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf(part * 100.0 / total).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal subtractRate(BigDecimal current, BigDecimal previous) {
+        if (current == null || previous == null) {
+            return null;
+        }
+        return current.subtract(previous).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private int intValue(Object value) {
+        if (value == null) {
+            return 0;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        return 0;
+    }
+
+    private long longValue(Object value) {
+        if (value == null) {
+            return 0L;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        return 0L;
+    }
+
+    private String stringValue(Object value) {
+        return value != null ? String.valueOf(value) : "";
+    }
+}

+ 464 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDashboardServiceImpl.java

@@ -0,0 +1,464 @@
+package shop.alien.store.service.analytics.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import shop.alien.entity.analytics.AnalyticsDailySummary;
+import shop.alien.entity.analytics.AnalyticsEventCode;
+import shop.alien.entity.analytics.vo.dashboard.*;
+import shop.alien.mapper.AnalyticsDashboardMapper;
+import shop.alien.mapper.AnalyticsEventMapper;
+import shop.alien.store.service.analytics.AnalyticsDashboardService;
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
+import shop.alien.store.util.analytics.AnalyticsPeriodContext;
+import shop.alien.store.util.analytics.AnalyticsTrendFillUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService {
+
+    private static final int[] RETENTION_DAY_OFFSETS = {1, 2, 3, 4, 5, 6, 7, 14, 30};
+
+    private final AnalyticsDashboardMapper dashboardMapper;
+    private final AnalyticsEventMapper eventMapper;
+
+    @Override
+    public AnalyticsDashboardChartsVo loadAllCharts(String period) {
+        AnalyticsDashboardChartsVo vo = new AnalyticsDashboardChartsVo();
+        vo.setPeriod(AnalyticsPeriodContext.resolve(period).getPeriod());
+        vo.setSummary(summary(period));
+        vo.setUserActiveTrend(userActiveTrend(period));
+        vo.setConversationTrend(conversationTrend(period));
+        vo.setContentTrend(contentTrend(period));
+        vo.setConversionFunnel(conversionFunnel(period));
+        vo.setUserRetention(userRetention(period));
+        vo.setAvgUsageDuration(avgUsageDuration(period));
+        return vo;
+    }
+
+    @Override
+    public AnalyticsDashboardSummaryVo summary(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<AnalyticsDailySummary> rows = dashboardMapper.listDailySummaryBetween(ctx.getStartDate(), ctx.getEndDate());
+
+        AnalyticsDashboardSummaryVo vo = new AnalyticsDashboardSummaryVo();
+        int dayCount = dayCount(ctx.getStartDate(), ctx.getEndDate());
+
+        if (rows.isEmpty()) {
+            fillSummaryFromEvents(vo, ctx);
+            return vo;
+        }
+
+        if (dayCount == 1) {
+            AnalyticsDailySummary s = rows.get(0);
+            vo.setDau(resolveDau(s, ctx));
+            vo.setOnlineUserCount(defaultInt(s.getOnlineUserCount()));
+        } else {
+            vo.setDau((int) Math.round(rows.stream().mapToInt(s -> resolveDau(s, ctx)).average().orElse(0)));
+            vo.setOnlineUserCount(null);
+        }
+
+        vo.setNewUserCount(rows.stream().mapToInt(s -> defaultInt(s.getNewUserCount())).sum());
+        vo.setAiChatCount(rows.stream().mapToInt(s -> defaultInt(s.getAiChatCount())).sum());
+        vo.setContentPublishCount(rows.stream().mapToInt(s -> defaultInt(s.getContentPublishCount())).sum());
+        vo.setMerchantVisitUv(rows.stream().mapToInt(s -> defaultInt(s.getMerchantVisitUv())).sum());
+
+        long totalAiMs = rows.stream()
+                .mapToLong(s -> s.getAiResponseDurationTotalMs() != null ? s.getAiResponseDurationTotalMs() : 0L)
+                .sum();
+        int totalAiReq = rows.stream().mapToInt(s -> defaultInt(s.getAiRequestCount())).sum();
+        vo.setAiAvgResponseMs(totalAiReq > 0 ? totalAiMs / totalAiReq : 0L);
+        vo.setConversionRate(calcWeightedConversionRate(rows));
+
+        if (vo.getDau() == null || vo.getDau() == 0) {
+            fillSummaryFromEvents(vo, ctx);
+        }
+        return vo;
+    }
+
+    @Override
+    public List<AnalyticsUserActiveTrendVo> userActiveTrend(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        if (ctx.isHourly()) {
+            return buildHourlyUserActive(ctx);
+        }
+        return buildDailyUserActive(ctx);
+    }
+
+    @Override
+    public List<AnalyticsConversationTrendVo> conversationTrend(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        if (ctx.isHourly()) {
+            List<Map<String, Object>> chatRows = dashboardMapper.countEventByHour(
+                    AnalyticsEventCode.AI_CHAT_MESSAGE, ctx.getStartTime(), ctx.getEndTimeExclusive());
+            List<Map<String, Object>> userRows = dashboardMapper.countDistinctUserByEventByHour(
+                    AnalyticsEventCode.AI_CHAT_START, ctx.getStartTime(), ctx.getEndTimeExclusive());
+            Map<String, Long> chatMap = toCountMap(chatRows);
+            Map<String, Long> userMap = toCountMap(userRows);
+            Set<String> labels = new TreeSet<>();
+            labels.addAll(chatMap.keySet());
+            labels.addAll(userMap.keySet());
+            return labels.stream().map(label -> {
+                AnalyticsConversationTrendVo vo = new AnalyticsConversationTrendVo();
+                vo.setLabel(label);
+                vo.setChatCount(intValue(chatMap.get(label)));
+                vo.setChatUserCount(intValue(userMap.get(label)));
+                return vo;
+            }).collect(Collectors.toList());
+        }
+        return fillConversationTrend(ctx, dashboardMapper.listDailySummaryBetween(ctx.getStartDate(), ctx.getEndDate()).stream().map(s -> {
+            AnalyticsConversationTrendVo vo = new AnalyticsConversationTrendVo();
+            vo.setStatDate(s.getStatDate());
+            vo.setLabel(formatDayLabel(s.getStatDate()));
+            vo.setChatCount(resolveChatCount(s));
+            vo.setChatUserCount(resolveChatUserCount(s));
+            return vo;
+        }).collect(Collectors.toList()));
+    }
+
+    @Override
+    public List<AnalyticsContentTrendVo> contentTrend(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        if (ctx.isHourly()) {
+            List<Map<String, Object>> publishRows = dashboardMapper.countEventByHour(
+                    AnalyticsEventCode.CONTENT_PUBLISH, ctx.getStartTime(), ctx.getEndTimeExclusive());
+            List<Map<String, Object>> interactRows = dashboardMapper.countContentInteractByHour(
+                    ctx.getStartTime(), ctx.getEndTimeExclusive());
+            Map<String, Long> publishMap = toCountMap(publishRows);
+            Map<String, Long> interactMap = toCountMap(interactRows);
+            Set<String> labels = new TreeSet<>();
+            labels.addAll(publishMap.keySet());
+            labels.addAll(interactMap.keySet());
+            return labels.stream().map(label -> {
+                AnalyticsContentTrendVo vo = new AnalyticsContentTrendVo();
+                vo.setLabel(label);
+                vo.setPublishCount(intValue(publishMap.get(label)));
+                vo.setInteractionCount(intValue(interactMap.get(label)));
+                return vo;
+            }).collect(Collectors.toList());
+        }
+        return fillContentTrend(ctx, dashboardMapper.listDailySummaryBetween(ctx.getStartDate(), ctx.getEndDate()).stream().map(s -> {
+            AnalyticsContentTrendVo vo = new AnalyticsContentTrendVo();
+            vo.setStatDate(s.getStatDate());
+            vo.setLabel(formatDayLabel(s.getStatDate()));
+            vo.setPublishCount(defaultInt(s.getContentPublishCount()));
+            vo.setInteractionCount(defaultInt(s.getContentInteractionCount()));
+            return vo;
+        }).collect(Collectors.toList()));
+    }
+
+    @Override
+    public List<AnalyticsFunnelStageVo> conversionFunnel(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        Map<String, Object> funnel = dashboardMapper.sumMerchantFunnel(ctx.getStartDate(), ctx.getEndDate());
+        long expose = longValue(funnel.get("exposeCount"));
+        long click = longValue(funnel.get("clickCount"));
+        long detail = longValue(funnel.get("detailCount"));
+        long contact = longValue(funnel.get("contactCount"));
+        long storeVisit = longValue(funnel.get("verifyCount"));
+
+        List<AnalyticsFunnelStageVo> stages = new ArrayList<>();
+        stages.add(buildFunnelStage("EXPOSE", "曝光", expose, null));
+        stages.add(buildFunnelStage("CLICK", "点击", click, expose));
+        stages.add(buildFunnelStage("DETAIL", "详情页", detail, click));
+        stages.add(buildFunnelStage("CONTACT", "电话/导航", contact, detail));
+        stages.add(buildFunnelStage("STORE_VISIT", "到店", storeVisit, contact));
+        return stages;
+    }
+
+    @Override
+    public List<AnalyticsRetentionPointVo> userRetention(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        Date cohortStart = ctx.getStartTime();
+        Date cohortEnd = ctx.getEndTimeExclusive();
+        Long cohortSize = eventMapper.countRegisterUsers(cohortStart, cohortEnd);
+        long size = cohortSize != null ? cohortSize : 0L;
+        if (size <= 0) {
+            return Arrays.stream(RETENTION_DAY_OFFSETS).mapToObj(day -> {
+                AnalyticsRetentionPointVo vo = new AnalyticsRetentionPointVo();
+                vo.setDayIndex(day);
+                vo.setRetainedCount(0);
+                vo.setRetentionRate(BigDecimal.ZERO);
+                return vo;
+            }).collect(Collectors.toList());
+        }
+        List<AnalyticsRetentionPointVo> result = new ArrayList<>();
+        for (int dayOffset : RETENTION_DAY_OFFSETS) {
+            Long retained = dashboardMapper.countRetainedOnDayOffset(cohortStart, cohortEnd, dayOffset);
+            long retainedCount = retained != null ? retained : 0L;
+            AnalyticsRetentionPointVo vo = new AnalyticsRetentionPointVo();
+            vo.setDayIndex(dayOffset);
+            vo.setRetainedCount((int) retainedCount);
+            vo.setRetentionRate(BigDecimal.valueOf(retainedCount * 100.0 / size).setScale(2, RoundingMode.HALF_UP));
+            result.add(vo);
+        }
+        return result;
+    }
+
+    @Override
+    public List<AnalyticsAvgDurationTrendVo> avgUsageDuration(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        if (ctx.isHourly()) {
+            List<Map<String, Object>> rows = dashboardMapper.avgHeartbeatMinutesByHour(
+                    ctx.getStartTime(), ctx.getEndTimeExclusive());
+            return rows.stream().map(row -> {
+                AnalyticsAvgDurationTrendVo vo = new AnalyticsAvgDurationTrendVo();
+                vo.setLabel(stringValue(row.get("label")));
+                vo.setAvgDurationMin(toBigDecimal(row.get("avgMin")));
+                return vo;
+            }).collect(Collectors.toList());
+        }
+        Date today = AnalyticsDateUtil.truncateToDate(new Date());
+        List<AnalyticsAvgDurationTrendVo> result = new ArrayList<>();
+        if (!ctx.getEndDate().before(today) && !ctx.getStartDate().after(today)) {
+            Date historyEnd = AnalyticsDateUtil.addDays(today, -1);
+            if (!ctx.getStartDate().after(historyEnd)) {
+                result.addAll(mapAvgDurationRows(
+                        dashboardMapper.avgOnlineDurationByDayFromHistory(ctx.getStartDate(), historyEnd)));
+            }
+            BigDecimal todayAvg = dashboardMapper.avgOnlineDurationFromToday();
+            if (todayAvg != null) {
+                AnalyticsAvgDurationTrendVo vo = new AnalyticsAvgDurationTrendVo();
+                vo.setStatDate(today);
+                vo.setLabel(formatDayLabel(today));
+                vo.setAvgDurationMin(todayAvg);
+                result.add(vo);
+            }
+        } else {
+            result.addAll(mapAvgDurationRows(
+                    dashboardMapper.avgOnlineDurationByDayFromHistory(ctx.getStartDate(), ctx.getEndDate())));
+        }
+        return result;
+    }
+
+    private List<AnalyticsUserActiveTrendVo> buildHourlyUserActive(AnalyticsPeriodContext ctx) {
+        List<Map<String, Object>> dauRows = dashboardMapper.countDauByHour(ctx.getStartTime(), ctx.getEndTimeExclusive());
+        int wau = intValue(eventMapper.countActiveUsersInRange(
+                AnalyticsDateUtil.addDays(ctx.getEndDate(), -6), ctx.getEndTimeExclusive()));
+        int mau = intValue(eventMapper.countActiveUsersInRange(
+                AnalyticsDateUtil.addDays(ctx.getEndDate(), -29), ctx.getEndTimeExclusive()));
+        return dauRows.stream().map(row -> {
+            AnalyticsUserActiveTrendVo vo = new AnalyticsUserActiveTrendVo();
+            vo.setLabel(stringValue(row.get("label")));
+            vo.setDau(intValue(row.get("cnt")));
+            vo.setWau(wau);
+            vo.setMau(mau);
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    private List<AnalyticsUserActiveTrendVo> buildDailyUserActive(AnalyticsPeriodContext ctx) {
+        List<AnalyticsDailySummary> summaries = dashboardMapper.listDailySummaryBetween(ctx.getStartDate(), ctx.getEndDate());
+        Map<Date, AnalyticsDailySummary> summaryMap = summaries.stream()
+                .collect(Collectors.toMap(s -> AnalyticsDateUtil.truncateToDate(s.getStatDate()), s -> s, (a, b) -> a));
+        List<AnalyticsUserActiveTrendVo> result = new ArrayList<>();
+        Date cursor = ctx.getStartDate();
+        while (!cursor.after(ctx.getEndDate())) {
+            AnalyticsUserActiveTrendVo vo = new AnalyticsUserActiveTrendVo();
+            vo.setStatDate(cursor);
+            vo.setLabel(formatDayLabel(cursor));
+            AnalyticsDailySummary summary = summaryMap.get(cursor);
+            if (summary != null) {
+                vo.setDau(defaultInt(summary.getDau()));
+                vo.setMau(defaultInt(summary.getMau()));
+            } else {
+                Date dayStart = AnalyticsDateUtil.dayStart(cursor);
+                Date dayEnd = AnalyticsDateUtil.dayEndExclusive(cursor);
+                vo.setDau(intValue(eventMapper.countDau(dayStart, dayEnd)));
+                vo.setMau(intValue(eventMapper.countActiveUsersInRange(
+                        AnalyticsDateUtil.addDays(cursor, -29), dayEnd)));
+            }
+            Date wauStart = AnalyticsDateUtil.addDays(cursor, -6);
+            Date wauEnd = AnalyticsDateUtil.dayEndExclusive(cursor);
+            vo.setWau(intValue(eventMapper.countActiveUsersInRange(wauStart, wauEnd)));
+            result.add(vo);
+            cursor = AnalyticsDateUtil.addDays(cursor, 1);
+        }
+        return result;
+    }
+
+    private int resolveChatCount(AnalyticsDailySummary summary) {
+        if (summary.getAiChatCount() != null && summary.getAiChatCount() > 0) {
+            return summary.getAiChatCount();
+        }
+        Date day = AnalyticsDateUtil.truncateToDate(summary.getStatDate());
+        return intValue(eventMapper.countByEventCode(
+                AnalyticsEventCode.AI_CHAT_MESSAGE,
+                AnalyticsDateUtil.dayStart(day),
+                AnalyticsDateUtil.dayEndExclusive(day)));
+    }
+
+    private int resolveChatUserCount(AnalyticsDailySummary summary) {
+        if (summary.getAiChatUserCount() != null && summary.getAiChatUserCount() > 0) {
+            return summary.getAiChatUserCount();
+        }
+        Date day = AnalyticsDateUtil.truncateToDate(summary.getStatDate());
+        return intValue(eventMapper.countDistinctUserByEventCode(
+                AnalyticsEventCode.AI_CHAT_START,
+                AnalyticsDateUtil.dayStart(day),
+                AnalyticsDateUtil.dayEndExclusive(day)));
+    }
+
+    private List<AnalyticsConversationTrendVo> fillConversationTrend(
+            AnalyticsPeriodContext ctx, List<AnalyticsConversationTrendVo> existing) {
+        Map<Date, AnalyticsConversationTrendVo> map = existing.stream()
+                .filter(vo -> vo.getStatDate() != null)
+                .collect(Collectors.toMap(
+                        vo -> AnalyticsDateUtil.truncateToDate(vo.getStatDate()),
+                        vo -> vo,
+                        (a, b) -> a));
+        return AnalyticsTrendFillUtil.fillDailyRange(
+                ctx.getStartDate(), ctx.getEndDate(), map,
+                AnalyticsConversationTrendVo::new,
+                (vo, date) -> {
+                    vo.setChatCount(0);
+                    vo.setChatUserCount(0);
+                });
+    }
+
+    private List<AnalyticsContentTrendVo> fillContentTrend(
+            AnalyticsPeriodContext ctx, List<AnalyticsContentTrendVo> existing) {
+        Map<Date, AnalyticsContentTrendVo> map = existing.stream()
+                .filter(vo -> vo.getStatDate() != null)
+                .collect(Collectors.toMap(
+                        vo -> AnalyticsDateUtil.truncateToDate(vo.getStatDate()),
+                        vo -> vo,
+                        (a, b) -> a));
+        return AnalyticsTrendFillUtil.fillDailyRange(
+                ctx.getStartDate(), ctx.getEndDate(), map,
+                AnalyticsContentTrendVo::new,
+                (vo, date) -> {
+                    vo.setPublishCount(0);
+                    vo.setInteractionCount(0);
+                });
+    }
+
+    private void fillSummaryFromEvents(AnalyticsDashboardSummaryVo vo, AnalyticsPeriodContext ctx) {
+        vo.setDau(intValue(eventMapper.countDau(ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        vo.setNewUserCount(intValue(eventMapper.countRegisterUsers(ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        vo.setAiChatCount(intValue(eventMapper.countByEventCode(
+                AnalyticsEventCode.AI_CHAT_MESSAGE, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        vo.setContentPublishCount(intValue(eventMapper.countByEventCode(
+                AnalyticsEventCode.CONTENT_PUBLISH, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        vo.setMerchantVisitUv(intValue(eventMapper.countDistinctUserByEventCode(
+                AnalyticsEventCode.MERCHANT_VIEW, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        vo.setAiAvgResponseMs(0L);
+        vo.setConversionRate(BigDecimal.ZERO);
+        if (ctx.isHourly()) {
+            vo.setOnlineUserCount(vo.getDau());
+        }
+    }
+
+    private int resolveDau(AnalyticsDailySummary summary, AnalyticsPeriodContext ctx) {
+        if (summary.getDau() != null && summary.getDau() > 0) {
+            return summary.getDau();
+        }
+        Date day = AnalyticsDateUtil.truncateToDate(summary.getStatDate());
+        return intValue(eventMapper.countDau(
+                AnalyticsDateUtil.dayStart(day), AnalyticsDateUtil.dayEndExclusive(day)));
+    }
+
+    private BigDecimal calcWeightedConversionRate(List<AnalyticsDailySummary> rows) {
+        BigDecimal weighted = BigDecimal.ZERO;
+        int weight = 0;
+        for (AnalyticsDailySummary s : rows) {
+            if (s.getConversionRate() != null && s.getDau() != null && s.getDau() > 0) {
+                weighted = weighted.add(s.getConversionRate().multiply(BigDecimal.valueOf(s.getDau())));
+                weight += s.getDau();
+            }
+        }
+        if (weight <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return weighted.divide(BigDecimal.valueOf(weight), 2, RoundingMode.HALF_UP);
+    }
+
+    private int dayCount(Date start, Date end) {
+        return (int) ((AnalyticsDateUtil.dayStart(end).getTime() - AnalyticsDateUtil.dayStart(start).getTime()) / 86400000L) + 1;
+    }
+
+    private AnalyticsFunnelStageVo buildFunnelStage(String code, String name, long count, Long previous) {
+        AnalyticsFunnelStageVo vo = new AnalyticsFunnelStageVo();
+        vo.setStageCode(code);
+        vo.setStageName(name);
+        vo.setCount(count);
+        if (previous != null) {
+            vo.setConversionRate(AnalyticsDateUtil.calcRate(count, previous));
+        }
+        return vo;
+    }
+
+    private List<AnalyticsAvgDurationTrendVo> mapAvgDurationRows(List<Map<String, Object>> rows) {
+        return rows.stream().map(row -> {
+            AnalyticsAvgDurationTrendVo vo = new AnalyticsAvgDurationTrendVo();
+            Object statDate = row.get("statDate");
+            if (statDate instanceof Date) {
+                vo.setStatDate((Date) statDate);
+            }
+            vo.setLabel(stringValue(row.get("label")));
+            vo.setAvgDurationMin(toBigDecimal(row.get("avgMin")));
+            return vo;
+        }).collect(Collectors.toList());
+    }
+
+    private Map<String, Long> toCountMap(List<Map<String, Object>> rows) {
+        Map<String, Long> map = new LinkedHashMap<>();
+        if (rows == null) {
+            return map;
+        }
+        for (Map<String, Object> row : rows) {
+            map.put(stringValue(row.get("label")), longValue(row.get("cnt")));
+        }
+        return map;
+    }
+
+    private String formatDayLabel(Date date) {
+        return new SimpleDateFormat("M.d").format(date);
+    }
+
+    private int defaultInt(Integer value) {
+        return value != null ? value : 0;
+    }
+
+    private int intValue(Object value) {
+        if (value == null) {
+            return 0;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        return 0;
+    }
+
+    private long longValue(Object value) {
+        if (value == null) {
+            return 0L;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        return 0L;
+    }
+
+    private String stringValue(Object value) {
+        return value != null ? String.valueOf(value) : "";
+    }
+
+    private BigDecimal toBigDecimal(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return BigDecimal.valueOf(((Number) value).doubleValue());
+        }
+        return null;
+    }
+}

+ 353 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsMerchantReportServiceImpl.java

@@ -0,0 +1,353 @@
+package shop.alien.store.service.analytics.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.analytics.AnalyticsMerchantStat;
+import shop.alien.entity.analytics.vo.merchantreport.*;
+import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
+import shop.alien.entity.store.StoreInfo;
+import shop.alien.mapper.AnalyticsMerchantReportMapper;
+import shop.alien.mapper.AnalyticsMerchantStatMapper;
+import shop.alien.mapper.StoreInfoMapper;
+import shop.alien.store.service.analytics.AnalyticsMerchantReportService;
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
+import shop.alien.store.util.analytics.AnalyticsPeriodContext;
+import shop.alien.store.util.analytics.AnalyticsTrendFillUtil;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AnalyticsMerchantReportServiceImpl implements AnalyticsMerchantReportService {
+
+    private final AnalyticsMerchantReportMapper merchantReportMapper;
+    private final AnalyticsMerchantStatMapper merchantStatMapper;
+    private final StoreInfoMapper storeInfoMapper;
+
+    @Override
+    public AnalyticsMerchantReportChartsVo loadAllCharts(String period) {
+        AnalyticsMerchantReportChartsVo vo = new AnalyticsMerchantReportChartsVo();
+        vo.setPeriod(AnalyticsPeriodContext.resolve(period).getPeriod());
+        vo.setSummary(summary(period));
+        vo.setGmvTrend(gmvTrend(period));
+        vo.setGmvCategoryDistribution(gmvCategoryDistribution(period));
+        vo.setAvgOrderTrend(avgOrderTrend(period));
+        vo.setGmvTop10(gmvTop10(period));
+        return vo;
+    }
+
+    @Override
+    public AnalyticsMerchantReportSummaryVo summary(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        AnalyticsPeriodContext prevCtx = previousContext(ctx);
+
+        int settledNow = countSettled(ctx.getEndDate());
+        int settledPrev = countSettled(prevCtx.getEndDate());
+
+        Map<String, Object> metricsNow = merchantReportMapper.sumMerchantMetrics(ctx.getStartDate(), ctx.getEndDate());
+        Map<String, Object> metricsPrev = merchantReportMapper.sumMerchantMetrics(prevCtx.getStartDate(), prevCtx.getEndDate());
+
+        AnalyticsMerchantReportSummaryVo vo = new AnalyticsMerchantReportSummaryVo();
+        vo.setTotalSettledMerchantCount(settledNow);
+        vo.setTotalSettledMerchantDelta(settledNow - settledPrev);
+
+        BigDecimal gmvNow = toBigDecimal(metricsNow.get("totalGmv"));
+        BigDecimal gmvPrev = toBigDecimal(metricsPrev.get("totalGmv"));
+        vo.setGmv(gmvNow);
+        vo.setGmvChangeRate(calcChangeRate(gmvNow, gmvPrev));
+
+        BigDecimal verifyNow = toBigDecimal(metricsNow.get("verifyRate"));
+        BigDecimal verifyPrev = toBigDecimal(metricsPrev.get("verifyRate"));
+        vo.setVerifyRate(verifyNow);
+        vo.setVerifyRateDelta(subtractRate(verifyNow, verifyPrev));
+
+        BigDecimal avgNow = toBigDecimal(metricsNow.get("avgOrderAmount"));
+        BigDecimal avgPrev = toBigDecimal(metricsPrev.get("avgOrderAmount"));
+        vo.setAvgOrderAmount(avgNow);
+        if (avgNow != null && avgPrev != null) {
+            vo.setAvgOrderAmountDelta(avgNow.subtract(avgPrev).setScale(2, RoundingMode.HALF_UP));
+        }
+        return vo;
+    }
+
+    @Override
+    public List<AnalyticsGmvTrendVo> gmvTrend(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<AnalyticsGmvTrendVo> existing = merchantReportMapper.listGmvTrend(ctx.getStartDate(), ctx.getEndDate()).stream().map(row -> {
+            AnalyticsGmvTrendVo vo = new AnalyticsGmvTrendVo();
+            Object statDate = row.get("statDate");
+            if (statDate instanceof Date) {
+                vo.setStatDate((Date) statDate);
+            }
+            vo.setLabel(stringValue(row.get("label")));
+            vo.setGmv(toBigDecimal(row.get("gmv")));
+            return vo;
+        }).collect(Collectors.toList());
+        Map<Date, AnalyticsGmvTrendVo> map = existing.stream()
+                .filter(vo -> vo.getStatDate() != null)
+                .collect(Collectors.toMap(
+                        vo -> AnalyticsDateUtil.truncateToDate(vo.getStatDate()),
+                        vo -> vo,
+                        (a, b) -> a));
+        return AnalyticsTrendFillUtil.fillDailyRange(
+                ctx.getStartDate(), ctx.getEndDate(), map,
+                AnalyticsGmvTrendVo::new,
+                (vo, date) -> vo.setGmv(BigDecimal.ZERO));
+    }
+
+    @Override
+    public List<AnalyticsDistributionItemVo> gmvCategoryDistribution(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = merchantReportMapper.sumGmvByCategory(ctx.getStartDate(), ctx.getEndDate());
+        BigDecimal total = rows.stream()
+                .map(row -> toBigDecimal(row.get("gmv")))
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        return rows.stream().map(row -> {
+            AnalyticsDistributionItemVo item = new AnalyticsDistributionItemVo();
+            String code = String.valueOf(row.get("code"));
+            item.setCode(code);
+            item.setName(resolveBusinessCategoryName(code));
+            BigDecimal gmv = toBigDecimal(row.get("gmv"));
+            item.setCount(gmv != null ? gmv.longValue() : 0L);
+            item.setRatio(calcRatio(gmv, total));
+            return item;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<AnalyticsAvgOrderTrendVo> avgOrderTrend(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<AnalyticsAvgOrderTrendVo> existing = merchantReportMapper.listAvgOrderTrend(ctx.getStartDate(), ctx.getEndDate()).stream().map(row -> {
+            AnalyticsAvgOrderTrendVo vo = new AnalyticsAvgOrderTrendVo();
+            Object statDate = row.get("statDate");
+            if (statDate instanceof Date) {
+                vo.setStatDate((Date) statDate);
+            }
+            vo.setLabel(stringValue(row.get("label")));
+            vo.setAvgOrderAmount(toBigDecimal(row.get("avgOrderAmount")));
+            vo.setAvgPayCount(toBigDecimal(row.get("avgPayCount")));
+            return vo;
+        }).collect(Collectors.toList());
+        Map<Date, AnalyticsAvgOrderTrendVo> map = existing.stream()
+                .filter(vo -> vo.getStatDate() != null)
+                .collect(Collectors.toMap(
+                        vo -> AnalyticsDateUtil.truncateToDate(vo.getStatDate()),
+                        vo -> vo,
+                        (a, b) -> a));
+        return AnalyticsTrendFillUtil.fillDailyRange(
+                ctx.getStartDate(), ctx.getEndDate(), map,
+                AnalyticsAvgOrderTrendVo::new,
+                (vo, date) -> {
+                    vo.setAvgOrderAmount(BigDecimal.ZERO);
+                    vo.setAvgPayCount(BigDecimal.ZERO);
+                });
+    }
+
+    @Override
+    public List<AnalyticsMerchantGmvRankVo> gmvTop10(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = merchantReportMapper.listMerchantGmvTop10(ctx.getStartDate(), ctx.getEndDate());
+        List<AnalyticsMerchantGmvRankVo> result = new ArrayList<>();
+        int rank = 1;
+        for (Map<String, Object> row : rows) {
+            AnalyticsMerchantGmvRankVo vo = new AnalyticsMerchantGmvRankVo();
+            vo.setRank(rank++);
+            Long merchantId = longValue(row.get("merchantId"));
+            vo.setMerchantId(merchantId);
+            vo.setGmv(toBigDecimal(row.get("gmv")));
+            Integer shopType = intValue(row.get("shopType"));
+            vo.setShopTypeName(resolveShopTypeName(shopType));
+            fillMerchantInfo(vo, merchantId, shopType);
+            result.add(vo);
+        }
+        return result;
+    }
+
+    @Override
+    public IPage<AnalyticsMerchantReportDetailVo> pageSettledMerchant(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsMerchantStat> raw = merchantReportMapper.pageSettledMerchantDetail(
+                new Page<>(page, size), ctx.getStartDate(), ctx.getEndDate());
+        return raw.convert(this::toDetailVo);
+    }
+
+    private AnalyticsMerchantReportDetailVo toDetailVo(AnalyticsMerchantStat stat) {
+        AnalyticsMerchantReportDetailVo vo = new AnalyticsMerchantReportDetailVo();
+        vo.setMerchantId(stat.getMerchantId());
+        vo.setDisplayMerchantId(stat.getMerchantId() != null ? "M" + stat.getMerchantId() : null);
+        vo.setSettleTime(stat.getSettleTime());
+        vo.setPeriodGmv(stat.getGmv());
+        fillMerchantDetailInfo(vo, stat.getMerchantId());
+        vo.setStatus(resolveMerchantStatus(stat.getMerchantId()));
+        return vo;
+    }
+
+    private void fillMerchantInfo(AnalyticsMerchantGmvRankVo vo, Long merchantId, Integer shopType) {
+        if (merchantId == null) {
+            return;
+        }
+        StoreInfo store = storeInfoMapper.selectById(merchantId.intValue());
+        if (store != null) {
+            vo.setMerchantName(store.getStoreName());
+            vo.setCategoryName(firstNonBlank(store.getBusinessCategoryName(), store.getBusinessTypeName(),
+                    resolveShopTypeName(shopType)));
+        }
+    }
+
+    private void fillMerchantDetailInfo(AnalyticsMerchantReportDetailVo vo, Long merchantId) {
+        if (merchantId == null) {
+            return;
+        }
+        StoreInfo store = storeInfoMapper.selectById(merchantId.intValue());
+        if (store != null) {
+            vo.setMerchantName(store.getStoreName());
+            vo.setCategoryName(firstNonBlank(store.getBusinessCategoryName(), store.getBusinessTypeName()));
+        }
+    }
+
+    private String resolveMerchantStatus(Long merchantId) {
+        if (merchantId == null) {
+            return "营业中";
+        }
+        StoreInfo store = storeInfoMapper.selectById(merchantId.intValue());
+        if (store == null || store.getBusinessStatus() == null) {
+            return "营业中";
+        }
+        switch (store.getBusinessStatus()) {
+            case 0: return "营业中";
+            case 1: return "暂停营业";
+            case 2: return "筹建中";
+            case -1: return "注销中";
+            case 99: return "永久关门";
+            default: return "营业中";
+        }
+    }
+
+    private int countSettled(Date endDate) {
+        Long count = merchantStatMapper.countSettledMerchants(endDate);
+        return count != null ? count.intValue() : 0;
+    }
+
+    private AnalyticsPeriodContext previousContext(AnalyticsPeriodContext ctx) {
+        long days = daysBetween(ctx.getStartDate(), ctx.getEndDate());
+        Date prevEnd = AnalyticsDateUtil.addDays(ctx.getStartDate(), -1);
+        Date prevStart = AnalyticsDateUtil.addDays(prevEnd, -(int) days);
+        return new AnalyticsPeriodContext(
+                ctx.getPeriod(),
+                prevStart,
+                prevEnd,
+                AnalyticsDateUtil.dayStart(prevStart),
+                AnalyticsDateUtil.dayEndExclusive(prevEnd),
+                false,
+                prevStart
+        );
+    }
+
+    private long daysBetween(Date start, Date end) {
+        return (AnalyticsDateUtil.dayStart(end).getTime() - AnalyticsDateUtil.dayStart(start).getTime()) / 86400000L;
+    }
+
+    private String resolveBusinessCategoryName(String code) {
+        if (code == null) {
+            return "其他";
+        }
+        switch (code) {
+            case "1": return "美食";
+            case "2": return "休闲娱乐";
+            case "3": return "生活服务";
+            case "4": return "旅游";
+            case "5": return "酒店";
+            case "6": return "购物";
+            case "7": return "其他";
+            default: return "其他";
+        }
+    }
+
+    private String resolveShopTypeName(Integer shopType) {
+        if (shopType == null) {
+            return null;
+        }
+        switch (shopType) {
+            case 1: return "美食";
+            case 2: return "休闲娱乐";
+            case 3: return "生活服务";
+            default: return "未知";
+        }
+    }
+
+    private BigDecimal calcChangeRate(BigDecimal current, BigDecimal previous) {
+        if (current == null || previous == null || previous.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return current.subtract(previous)
+                .multiply(BigDecimal.valueOf(100))
+                .divide(previous, 2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal calcRatio(BigDecimal part, BigDecimal total) {
+        if (part == null || total == null || total.compareTo(BigDecimal.ZERO) <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return part.multiply(BigDecimal.valueOf(100)).divide(total, 2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal subtractRate(BigDecimal current, BigDecimal previous) {
+        if (current == null || previous == null) {
+            return null;
+        }
+        return current.subtract(previous).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private String firstNonBlank(String... values) {
+        for (String value : values) {
+            if (StringUtils.hasText(value)) {
+                return value;
+            }
+        }
+        return null;
+    }
+
+    private int intValue(Object value) {
+        if (value == null) {
+            return 0;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        return 0;
+    }
+
+    private long longValue(Object value) {
+        if (value == null) {
+            return 0L;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        return 0L;
+    }
+
+    private BigDecimal toBigDecimal(Object value) {
+        if (value == null) {
+            return BigDecimal.ZERO;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return BigDecimal.valueOf(((Number) value).doubleValue());
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private String stringValue(Object value) {
+        return value != null ? String.valueOf(value) : "";
+    }
+}

+ 164 - 8
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatisticsServiceImpl.java

@@ -30,6 +30,8 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
     private final AnalyticsDailySummaryMapper dailySummaryMapper;
     private final AnalyticsStatJobLogMapper jobLogMapper;
     private final AnalyticsMerchantStatMapper merchantStatHistoryMapper;
+    private final AnalyticsCategoryDailyTodayMapper categoryDailyTodayMapper;
+    private final AnalyticsCategoryDailyMapper categoryDailyHistoryMapper;
     private final BaseRedisService baseRedisService;
 
     @Override
@@ -58,6 +60,7 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             }
             if (AnalyticsStatScope.ALL.equals(normalizedScope) || AnalyticsStatScope.MERCHANT.equals(normalizedScope)) {
                 calculateMerchantStat(day);
+                calculateCategoryDaily(day);
             }
             if (AnalyticsStatScope.ALL.equals(normalizedScope) || AnalyticsStatScope.CONTENT.equals(normalizedScope)) {
                 calculateContentStat(day);
@@ -161,6 +164,13 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
 
             stat.setVisitPv(Math.max(defaultInt(stat.getVisitPv()), toInt(row.get("visitPv"))));
             stat.setVisitUv(Math.max(defaultInt(stat.getVisitUv()), toInt(row.get("visitUv"))));
+            stat.setExposeCount(Math.max(defaultInt(stat.getExposeCount()), toInt(row.get("exposeCount"))));
+            stat.setClickCount(Math.max(defaultInt(stat.getClickCount()), toInt(row.get("clickCount"))));
+            stat.setDetailViewCount(Math.max(defaultInt(stat.getDetailViewCount()), toInt(row.get("detailViewCount"))));
+            stat.setContactCount(Math.max(defaultInt(stat.getContactCount()), toInt(row.get("contactCount"))));
+            stat.setVerifyCount(Math.max(defaultInt(stat.getVerifyCount()), toInt(row.get("verifyCount"))));
+            stat.setPayCount(Math.max(defaultInt(stat.getPayCount()), toInt(row.get("payCount"))));
+            stat.setGmv(toBigDecimal(row.get("gmv")));
             int verifyCount = toInt(row.get("verifyCount"));
             int visitUserCount = toInt(row.get("visitUserCount"));
             stat.setVerifyConversionRate(AnalyticsDateUtil.calcRate(verifyCount, visitUserCount));
@@ -185,16 +195,25 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
                 .ge(AnalyticsEvent::getEventTime, start)
                 .lt(AnalyticsEvent::getEventTime, end);
         List<AnalyticsEvent> interactEvents = eventMapper.selectList(interactWrapper);
-        Map<String, Integer> interactCountMap = new HashMap<>();
+        Map<String, ContentInteractAgg> interactAggMap = new HashMap<>();
         for (AnalyticsEvent event : interactEvents) {
             if (event.getTargetId() == null || event.getContentType() == null) {
                 continue;
             }
             String key = event.getContentType() + ":" + event.getTargetId();
-            interactCountMap.merge(key, 1, Integer::sum);
+            ContentInteractAgg agg = interactAggMap.computeIfAbsent(key, k -> new ContentInteractAgg());
+            agg.total++;
+            String subtype = event.getEventSubtype();
+            if (AnalyticsEventSubtype.INTERACT_LIKE.equalsIgnoreCase(subtype)) {
+                agg.like++;
+            } else if (AnalyticsEventSubtype.INTERACT_COMMENT.equalsIgnoreCase(subtype)) {
+                agg.comment++;
+            } else if (AnalyticsEventSubtype.INTERACT_SHARE.equalsIgnoreCase(subtype)) {
+                agg.share++;
+            }
         }
 
-        for (Map.Entry<String, Integer> entry : interactCountMap.entrySet()) {
+        for (Map.Entry<String, ContentInteractAgg> entry : interactAggMap.entrySet()) {
             String[] parts = entry.getKey().split(":");
             upsertContentInteraction(statDate, Integer.parseInt(parts[0]), Long.parseLong(parts[1]), entry.getValue());
         }
@@ -212,7 +231,86 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         }
 
         log.info("内容明细统计完成: statDate={}, interactGroups={}, publishEvents={}",
-                statDate, interactCountMap.size(), publishEvents.size());
+                statDate, interactAggMap.size(), publishEvents.size());
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    protected void calculateCategoryDaily(Date statDate) {
+        Date start = AnalyticsDateUtil.dayStart(statDate);
+        Date end = AnalyticsDateUtil.dayEndExclusive(statDate);
+        List<Map<String, Object>> aggregates = eventMapper.aggregateCategoryDaily(start, end);
+        Map<Integer, Map<String, Object>> aggregateMap = new HashMap<>();
+        for (Map<String, Object> row : aggregates) {
+            Object categoryObj = row.get("businessCategory");
+            if (categoryObj == null) {
+                continue;
+            }
+            int category = toInt(categoryObj);
+            if (category <= 0) {
+                continue;
+            }
+            aggregateMap.put(category, row);
+        }
+
+        for (int category = AnalyticsBusinessCategory.FOOD; category <= AnalyticsBusinessCategory.OTHER; category++) {
+            Map<String, Object> row = aggregateMap.get(category);
+            BigDecimal gmv = row != null ? toBigDecimal(row.get("gmv")) : BigDecimal.ZERO;
+            int payCount = row != null ? toInt(row.get("payCount")) : 0;
+            int visitUv = row != null ? toInt(row.get("merchantVisitUv")) : 0;
+            int publishCount = row != null ? toInt(row.get("contentPublishCount")) : 0;
+            int interactionCount = row != null ? toInt(row.get("contentInteractionCount")) : 0;
+            upsertCategoryDaily(statDate, category, gmv, payCount, visitUv, publishCount, interactionCount);
+        }
+        log.info("品类日统计完成: statDate={}, categories={}", statDate, aggregateMap.size());
+    }
+
+    private void upsertCategoryDaily(Date statDate, Integer businessCategory, BigDecimal gmv, int payCount,
+                                     int visitUv, int publishCount, int interactionCount) {
+        if (detailStoreService.isToday(statDate)) {
+            LambdaQueryWrapper<AnalyticsCategoryDailyToday> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AnalyticsCategoryDailyToday::getBusinessCategory, businessCategory);
+            AnalyticsCategoryDailyToday existing = categoryDailyTodayMapper.selectOne(wrapper);
+            if (existing == null) {
+                AnalyticsCategoryDailyToday row = new AnalyticsCategoryDailyToday();
+                row.setBusinessCategory(businessCategory);
+                row.setGmv(gmv);
+                row.setPayCount(payCount);
+                row.setMerchantVisitUv(visitUv);
+                row.setContentPublishCount(publishCount);
+                row.setContentInteractionCount(interactionCount);
+                categoryDailyTodayMapper.insert(row);
+                return;
+            }
+            existing.setGmv(gmv);
+            existing.setPayCount(payCount);
+            existing.setMerchantVisitUv(visitUv);
+            existing.setContentPublishCount(publishCount);
+            existing.setContentInteractionCount(interactionCount);
+            categoryDailyTodayMapper.updateById(existing);
+            return;
+        }
+        LambdaQueryWrapper<AnalyticsCategoryDaily> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsCategoryDaily::getStatDate, statDate)
+                .eq(AnalyticsCategoryDaily::getBusinessCategory, businessCategory);
+        AnalyticsCategoryDaily existing = categoryDailyHistoryMapper.selectOne(wrapper);
+        if (existing == null) {
+            AnalyticsCategoryDaily row = new AnalyticsCategoryDaily();
+            row.setStatDate(statDate);
+            row.setBusinessCategory(businessCategory);
+            row.setGmv(gmv);
+            row.setPayCount(payCount);
+            row.setMerchantVisitUv(visitUv);
+            row.setContentPublishCount(publishCount);
+            row.setContentInteractionCount(interactionCount);
+            categoryDailyHistoryMapper.insert(row);
+            return;
+        }
+        existing.setGmv(gmv);
+        existing.setPayCount(payCount);
+        existing.setMerchantVisitUv(visitUv);
+        existing.setContentPublishCount(publishCount);
+        existing.setContentInteractionCount(interactionCount);
+        categoryDailyHistoryMapper.updateById(existing);
     }
 
     @Transactional(rollbackFor = Exception.class)
@@ -233,7 +331,8 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
 
         summary.setDau(toInt(eventMapper.countDau(start, end)));
         summary.setNewUserCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.USER_REGISTER, start, end)));
-        summary.setAiChatCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.AI_CHAT_START, start, end)));
+        summary.setAiChatCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.AI_CHAT_MESSAGE, start, end)));
+        summary.setAiChatUserCount(toInt(eventMapper.countDistinctUserByEventCode(AnalyticsEventCode.AI_CHAT_START, start, end)));
         summary.setContentPublishCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.CONTENT_PUBLISH, start, end)));
         summary.setContentInteractionCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.CONTENT_INTERACT, start, end)));
         summary.setMerchantVisitUv(toInt(eventMapper.countMerchantVisitUv(start, end)));
@@ -241,9 +340,29 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         summary.setTodayGmv(defaultDecimal(eventMapper.sumPayAmount(AnalyticsEventCode.PAY_SUCCESS, start, end)));
         summary.setAuditPassCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.AUDIT_PASS, start, end)));
         summary.setAuditSubmitCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.AUDIT_SUBMIT, start, end)));
+        summary.setAuditRejectCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.AUDIT_REJECT, start, end)));
         summary.setReportHandleCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.REPORT_HANDLE, start, end)));
         summary.setReportSubmitCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.REPORT_SUBMIT, start, end)));
         summary.setAiResponseDurationTotalMs(defaultLong(aiRequestMapper.sumResponseDuration(start, end)));
+        summary.setAiRequestCount(toInt(aiRequestMapper.countRequests(start, end)));
+
+        Date mauStart = AnalyticsDateUtil.addDays(statDate, -29);
+        summary.setMau(toInt(eventMapper.countActiveUsersInRange(mauStart, end)));
+        summary.setLast30dActiveUserCount(summary.getMau());
+
+        summary.setRegisterPageViewCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.USER_REGISTER_PAGE, start, end)));
+        summary.setRegisterPhoneSubmitCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.USER_REGISTER_PHONE, start, end)));
+        summary.setRegisterOtpPassCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.USER_REGISTER_OTP, start, end)));
+        summary.setRegisterPasswordSetCount(toInt(eventMapper.countByEventCode(AnalyticsEventCode.USER_REGISTER_PASSWORD, start, end)));
+        summary.setRegisterSuccessCount(summary.getNewUserCount());
+
+        Map<String, Object> merchantFunnel = merchantStatHistoryMapper.sumFunnelByStatDate(statDate);
+        if (merchantFunnel != null) {
+            summary.setMerchantExposeCount(longValue(merchantFunnel.get("exposeCount")));
+            summary.setMerchantClickCount(longValue(merchantFunnel.get("clickCount")));
+            summary.setMerchantDetailViewCount(longValue(merchantFunnel.get("detailCount")));
+            summary.setMerchantContactCount(longValue(merchantFunnel.get("contactCount")));
+        }
 
         summary.setTotalRegisterUserCount(toInt(eventMapper.countTotalRegisterUsers(end)));
         summary.setLast7dNewUserCount(toInt(eventMapper.countNewUsersInRange(last7Start, end)));
@@ -265,6 +384,7 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         summary.setTodayConversionRate(AnalyticsDateUtil.calcRate(paySuccessCount, summary.getDau()));
         summary.setConversionRate(summary.getTodayConversionRate());
         summary.setAuditPassRate(AnalyticsDateUtil.calcRate(summary.getAuditPassCount(), summary.getAuditSubmitCount()));
+        summary.setReportHandleRate(AnalyticsDateUtil.calcRate(summary.getReportHandleCount(), summary.getReportSubmitCount()));
 
         AnalyticsDailySummary yesterdaySummary = getDailySummary(prevDate);
         if (yesterdaySummary != null) {
@@ -291,7 +411,7 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         return summary;
     }
 
-    private void upsertContentInteraction(Date statDate, Integer contentType, Long contentId, int eventDayCount) {
+    private void upsertContentInteraction(Date statDate, Integer contentType, Long contentId, ContentInteractAgg agg) {
         AnalyticsContentStat stat = detailStoreService.findContentStat(statDate, contentType, contentId);
         if (stat == null) {
             stat = new AnalyticsContentStat();
@@ -299,11 +419,17 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             stat.setContentId(contentId);
             stat.setAuthorType(1);
             stat.setAuthorId(0L);
-            stat.setInteractionCount(eventDayCount);
+            stat.setInteractionCount(agg.total);
+            stat.setLikeCount(agg.like);
+            stat.setCommentCount(agg.comment);
+            stat.setShareCount(agg.share);
             detailStoreService.insertContentStat(statDate, stat);
             return;
         }
-        stat.setInteractionCount(Math.max(defaultInt(stat.getInteractionCount()), eventDayCount));
+        stat.setInteractionCount(Math.max(defaultInt(stat.getInteractionCount()), agg.total));
+        stat.setLikeCount(Math.max(defaultInt(stat.getLikeCount()), agg.like));
+        stat.setCommentCount(Math.max(defaultInt(stat.getCommentCount()), agg.comment));
+        stat.setShareCount(Math.max(defaultInt(stat.getShareCount()), agg.share));
         detailStoreService.updateContentStat(statDate, stat);
     }
 
@@ -415,6 +541,36 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         return StringUtils.hasText(current) ? current : incoming;
     }
 
+    private BigDecimal toBigDecimal(Object value) {
+        if (value == null) {
+            return BigDecimal.ZERO;
+        }
+        if (value instanceof BigDecimal) {
+            return (BigDecimal) value;
+        }
+        if (value instanceof Number) {
+            return BigDecimal.valueOf(((Number) value).doubleValue());
+        }
+        return BigDecimal.ZERO;
+    }
+
+    private long longValue(Object value) {
+        if (value == null) {
+            return 0L;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        return 0L;
+    }
+
+    private static final class ContentInteractAgg {
+        private int total;
+        private int like;
+        private int comment;
+        private int share;
+    }
+
     private String truncateError(String message) {
         if (message == null) {
             return null;

+ 67 - 2
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsTrackServiceImpl.java

@@ -45,6 +45,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         AnalyticsTrackEventDTO eventDto = convertFrontReport(dto);
         AnalyticsFrontHelper.enrichTrackEvent(eventDto, request);
         insertEvent(eventDto);
+        syncContentAuditFromFront(dto, eventDto.getEventCode());
     }
 
     @Override
@@ -96,6 +97,8 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         event.setChannel(dto.getChannel());
         event.setCity(dto.getCity());
         event.setBusinessCategory(dto.getBusinessCategory());
+        event.setShopType(dto.getShopType());
+        event.setEventSubtype(dto.getEventSubtype());
         event.setEventTime(dto.getEventTime() != null ? dto.getEventTime() : new Date());
 
         try {
@@ -126,6 +129,9 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         eventDto.setDeviceType(dto.getDeviceType());
         eventDto.setChannel(dto.getChannel());
         eventDto.setCity(dto.getCity());
+        eventDto.setEventSubtype(dto.getEventSubtype());
+        eventDto.setBusinessCategory(dto.getBusinessCategory());
+        eventDto.setShopType(dto.getShopType());
         eventDto.setEventTime(dto.getEventTime());
         return eventDto;
     }
@@ -255,12 +261,16 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
             stat.setLastActiveTime(registerTime);
             stat.setChannel(dto.getChannel());
             stat.setCity(dto.getCity());
+            stat.setGender(dto.getGender());
+            stat.setAge(dto.getAge());
+            stat.setAgeGroup(dto.getAgeGroup());
             stat.setIsNewUser(1);
             stat.setOnlineDurationMin(0);
             detailStoreService.insertUserStat(statDate, stat);
             return;
         }
-        applyUserRegisterMerge(existing, dto.getUserPhone(), registerTime, dto.getChannel(), dto.getCity());
+        applyUserRegisterMerge(existing, dto.getUserPhone(), registerTime, dto.getChannel(), dto.getCity(),
+                dto.getGender(), dto.getAge(), dto.getAgeGroup());
         detailStoreService.updateUserStat(statDate, existing);
     }
 
@@ -499,7 +509,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
     }
 
     private void applyUserRegisterMerge(AnalyticsUserStat target, String userPhone, Date registerTime,
-                                        String channel, String city) {
+                                        String channel, String city, Integer gender, Integer age, String ageGroup) {
         if (StringUtils.hasText(userPhone)) {
             target.setUserPhone(userPhone);
         }
@@ -516,9 +526,52 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         if (StringUtils.hasText(city)) {
             target.setCity(city);
         }
+        if (gender != null) {
+            target.setGender(gender);
+        }
+        if (age != null) {
+            target.setAge(age);
+        }
+        if (StringUtils.hasText(ageGroup)) {
+            target.setAgeGroup(ageGroup);
+        }
         target.setIsNewUser(1);
     }
 
+    private void syncContentAuditFromFront(AnalyticsFrontReportDTO dto, String eventCode) {
+        if (dto.getTargetId() == null || dto.getContentType() == null) {
+            return;
+        }
+        if (!AnalyticsEventCode.AUDIT_PASS.equals(eventCode)
+                && !AnalyticsEventCode.AUDIT_REJECT.equals(eventCode)
+                && !AnalyticsEventCode.AUDIT_SUBMIT.equals(eventCode)) {
+            return;
+        }
+        Date statDate = AnalyticsDateUtil.truncateToDate(dto.getEventTime() != null ? dto.getEventTime() : new Date());
+        AnalyticsContentStat stat = detailStoreService.findContentStat(statDate, dto.getContentType(), dto.getTargetId());
+        if (stat == null) {
+            stat = new AnalyticsContentStat();
+            stat.setContentType(dto.getContentType());
+            stat.setContentId(dto.getTargetId());
+            stat.setAuthorType(1);
+            stat.setAuthorId(0L);
+        }
+        if (AnalyticsEventCode.AUDIT_SUBMIT.equals(eventCode)) {
+            stat.setAuditStatus(0);
+        } else if (AnalyticsEventCode.AUDIT_PASS.equals(eventCode)) {
+            stat.setAuditStatus(1);
+            stat.setAuditTime(dto.getEventTime() != null ? dto.getEventTime() : new Date());
+        } else if (AnalyticsEventCode.AUDIT_REJECT.equals(eventCode)) {
+            stat.setAuditStatus(2);
+            stat.setAuditTime(dto.getEventTime() != null ? dto.getEventTime() : new Date());
+        }
+        if (stat.getId() == null) {
+            detailStoreService.insertContentStat(statDate, stat);
+        } else {
+            detailStoreService.updateContentStat(statDate, stat);
+        }
+    }
+
     private void applyUserSessionMerge(AnalyticsUserStat target, Date firstLaunchTime,
                                        Date defaultFirstLaunch, Date lastActiveTime, String city,
                                        String deviceName, Integer addOnlineDurationMin) {
@@ -550,6 +603,9 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         stat.setCity(dto.getCity());
         stat.setDeviceType(dto.getDeviceType());
         stat.setChannel(dto.getChannel());
+        stat.setGender(dto.getGender());
+        stat.setAge(dto.getAge());
+        stat.setAgeGroup(dto.getAgeGroup());
         stat.setIsNewUser(dto.getIsNewUser());
         stat.setOnlineDurationMin(defaultInt(dto.getOnlineDurationMin()));
         return stat;
@@ -564,6 +620,9 @@ 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.getGender() != null) target.setGender(dto.getGender());
+        if (dto.getAge() != null) target.setAge(dto.getAge());
+        if (StringUtils.hasText(dto.getAgeGroup())) target.setAgeGroup(dto.getAgeGroup());
         if (dto.getIsNewUser() != null) target.setIsNewUser(dto.getIsNewUser());
         if (dto.getOnlineDurationMin() != null) target.setOnlineDurationMin(dto.getOnlineDurationMin());
     }
@@ -600,6 +659,9 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         stat.setBusinessCategory(dto.getBusinessCategory());
         stat.setPublishTime(dto.getPublishTime());
         stat.setInteractionCount(defaultInt(dto.getInteractionCount()));
+        stat.setLikeCount(defaultInt(dto.getLikeCount()));
+        stat.setCommentCount(defaultInt(dto.getCommentCount()));
+        stat.setShareCount(defaultInt(dto.getShareCount()));
         stat.setStatus(dto.getStatus());
         stat.setAuditUserId(dto.getAuditUserId());
         stat.setAuditStatus(dto.getAuditStatus());
@@ -614,6 +676,9 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
         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.getLikeCount() != null) target.setLikeCount(dto.getLikeCount());
+        if (dto.getCommentCount() != null) target.setCommentCount(dto.getCommentCount());
+        if (dto.getShareCount() != null) target.setShareCount(dto.getShareCount());
         if (dto.getStatus() != null) target.setStatus(dto.getStatus());
         if (dto.getAuditUserId() != null) target.setAuditUserId(dto.getAuditUserId());
         if (dto.getAuditStatus() != null) target.setAuditStatus(dto.getAuditStatus());

+ 316 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsUserReportServiceImpl.java

@@ -0,0 +1,316 @@
+package shop.alien.store.service.analytics.impl;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+import shop.alien.entity.analytics.AnalyticsDailySummary;
+import shop.alien.entity.analytics.AnalyticsEventCode;
+import shop.alien.entity.analytics.AnalyticsUserStat;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsFunnelStageVo;
+import shop.alien.entity.analytics.vo.userreport.*;
+import shop.alien.entity.store.LifeUser;
+import shop.alien.mapper.AnalyticsDashboardMapper;
+import shop.alien.mapper.AnalyticsEventMapper;
+import shop.alien.mapper.AnalyticsUserReportMapper;
+import shop.alien.mapper.LifeUserMapper;
+import shop.alien.store.service.analytics.AnalyticsUserReportService;
+import shop.alien.store.util.analytics.AnalyticsDateUtil;
+import shop.alien.store.util.analytics.AnalyticsPeriodContext;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AnalyticsUserReportServiceImpl implements AnalyticsUserReportService {
+
+    private static final List<String> AGE_ORDER = Arrays.asList(
+            "18-24", "25-29", "30-34", "35-39", "40-49", "50+", "未知"
+    );
+
+    private final AnalyticsUserReportMapper userReportMapper;
+    private final AnalyticsDashboardMapper dashboardMapper;
+    private final AnalyticsEventMapper eventMapper;
+    private final LifeUserMapper lifeUserMapper;
+
+    @Override
+    public AnalyticsUserReportChartsVo loadAllCharts(String period) {
+        return loadAllCharts(period, 1, 10);
+    }
+
+    @Override
+    public AnalyticsUserReportChartsVo loadAllCharts(String period, long detailPage, long detailSize) {
+        AnalyticsUserReportChartsVo vo = new AnalyticsUserReportChartsVo();
+        vo.setPeriod(AnalyticsPeriodContext.resolve(period).getPeriod());
+        vo.setSummary(summary(period));
+        vo.setRegisterFunnel(registerFunnel(period));
+        vo.setGenderDistribution(genderDistribution(period));
+        vo.setAgeDistribution(ageDistribution(period));
+        vo.setUserTypeRatio(userTypeRatio(period));
+        vo.setGeoTop10(geoTop10(period));
+        vo.setNewUserPage(pageNewUserDetail(period, detailPage, detailSize));
+        return vo;
+    }
+
+    @Override
+    public AnalyticsUserReportSummaryVo summary(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        Date anchorEnd = ctx.getEndDate();
+        Date prevEnd = ctx.previousEndDate();
+
+        AnalyticsUserReportSummaryVo vo = new AnalyticsUserReportSummaryVo();
+        int totalNow = countTotalRegister(anchorEnd);
+        int totalPrev = countTotalRegister(prevEnd);
+        vo.setTotalRegisterUserCount(totalNow);
+        vo.setTotalRegisterUserDelta(totalNow - totalPrev);
+
+        int new7Now = countNewUsersInWindow(anchorEnd, 6);
+        int new7Prev = countNewUsersInWindow(prevEnd, 6);
+        vo.setLast7dNewUserCount(new7Now);
+        vo.setLast7dNewUserDelta(new7Now - new7Prev);
+
+        int active30Now = countActiveUsersInWindow(anchorEnd, 29);
+        int active30Prev = countActiveUsersInWindow(prevEnd, 29);
+        vo.setLast30dActiveUserCount(active30Now);
+        vo.setLast30dActiveUserDelta(active30Now - active30Prev);
+
+        BigDecimal retentionNow = calcNextDayRetentionRate(anchorEnd);
+        BigDecimal retentionPrev = calcNextDayRetentionRate(prevEnd);
+        vo.setNextDayRetentionRate(retentionNow);
+        vo.setNextDayRetentionRateDelta(subtractRate(retentionNow, retentionPrev));
+        return vo;
+    }
+
+    @Override
+    public List<AnalyticsFunnelStageVo> registerFunnel(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        Map<String, Object> funnel = userReportMapper.sumRegisterFunnel(ctx.getStartDate(), ctx.getEndDate());
+
+        long pageView = longValue(funnel.get("pageViewCount"));
+        long phoneSubmit = longValue(funnel.get("phoneSubmitCount"));
+        long otpPass = longValue(funnel.get("otpPassCount"));
+        long passwordSet = longValue(funnel.get("passwordSetCount"));
+        long success = longValue(funnel.get("successCount"));
+        if (success <= 0) {
+            success = longValue(funnel.get("newUserCount"));
+        }
+        if (success <= 0) {
+            success = longValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.USER_REGISTER, ctx.getStartTime(), ctx.getEndTimeExclusive()));
+        }
+
+        List<AnalyticsFunnelStageVo> stages = new ArrayList<>();
+        stages.add(buildFunnelStage("REGISTER_PAGE", "进入注册页", pageView, null));
+        stages.add(buildFunnelStage("PHONE_SUBMIT", "提交手机号", phoneSubmit, pageView));
+        stages.add(buildFunnelStage("OTP_PASS", "验证码通过", otpPass, phoneSubmit));
+        stages.add(buildFunnelStage("PASSWORD_SET", "设置密码", passwordSet, otpPass));
+        stages.add(buildFunnelStage("REGISTER_SUCCESS", "注册成功", success, passwordSet > 0 ? passwordSet : otpPass));
+        return stages;
+    }
+
+    @Override
+    public List<AnalyticsDistributionItemVo> genderDistribution(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = userReportMapper.countGenderDistribution(ctx.getStartDate(), ctx.getEndDate());
+        return toDistributionItems(rows, this::resolveGenderName);
+    }
+
+    @Override
+    public List<AnalyticsDistributionItemVo> ageDistribution(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = userReportMapper.countAgeDistribution(ctx.getStartDate(), ctx.getEndDate());
+        List<AnalyticsDistributionItemVo> items = toDistributionItems(rows, code -> String.valueOf(code));
+        items.sort(Comparator.comparingInt(item -> {
+            int idx = AGE_ORDER.indexOf(item.getCode());
+            return idx >= 0 ? idx : AGE_ORDER.size();
+        }));
+        return items;
+    }
+
+    @Override
+    public AnalyticsUserTypeRatioVo userTypeRatio(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        Map<String, Object> row = userReportMapper.countUserTypeRatio(ctx.getStartDate(), ctx.getEndDate());
+        long active = longValue(row.get("activeUserCount"));
+        long newUser = longValue(row.get("newUserCount"));
+        long oldUser = Math.max(active - newUser, 0);
+
+        AnalyticsUserTypeRatioVo vo = new AnalyticsUserTypeRatioVo();
+        vo.setActiveUserCount(active);
+        vo.setNewUserCount(newUser);
+        vo.setOldUserCount(oldUser);
+        vo.setNewUserRatio(calcRatio(newUser, active));
+        vo.setOldUserRatio(calcRatio(oldUser, active));
+        return vo;
+    }
+
+    @Override
+    public List<AnalyticsDistributionItemVo> geoTop10(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<Map<String, Object>> rows = userReportMapper.countCityTop10(ctx.getStartDate(), ctx.getEndDate());
+        return toDistributionItems(rows, code -> String.valueOf(code));
+    }
+
+    @Override
+    public IPage<AnalyticsUserReportDetailVo> pageNewUserDetail(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsUserStat> raw = userReportMapper.pageNewUsersInRange(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
+        return raw.convert(this::toDetailVo);
+    }
+
+    private AnalyticsUserReportDetailVo toDetailVo(AnalyticsUserStat stat) {
+        AnalyticsUserReportDetailVo vo = new AnalyticsUserReportDetailVo();
+        vo.setUserId(stat.getUserId());
+        vo.setDisplayUserId(stat.getUserId() != null ? "U" + stat.getUserId() : null);
+        vo.setMaskedPhone(maskPhone(stat.getUserPhone()));
+        vo.setRegisterTime(stat.getRegisterTime());
+        vo.setCity(stat.getCity());
+        vo.setChannel(stat.getChannel());
+        vo.setStatus(resolveUserStatus(stat.getUserId(), stat.getUserPhone()));
+        return vo;
+    }
+
+    private String resolveUserStatus(Long userId, String phone) {
+        if (!StringUtils.hasText(phone)) {
+            return "待验证";
+        }
+        if (userId == null) {
+            return "正常";
+        }
+        LifeUser user = lifeUserMapper.selectById(userId.intValue());
+        if (user == null) {
+            return "正常";
+        }
+        if (user.getLogoutFlag() != null && user.getLogoutFlag() == 1) {
+            return "已注销";
+        }
+        if (user.getIsBanned() != null && user.getIsBanned() == 1) {
+            return "封禁";
+        }
+        if (!StringUtils.hasText(user.getUserPhone())) {
+            return "待验证";
+        }
+        return "正常";
+    }
+
+    private int countTotalRegister(Date endDate) {
+        Date endExclusive = AnalyticsDateUtil.dayEndExclusive(endDate);
+        return intValue(eventMapper.countTotalRegisterUsers(endExclusive));
+    }
+
+    private int countNewUsersInWindow(Date endDate, int daysBack) {
+        Date start = AnalyticsDateUtil.addDays(endDate, -daysBack);
+        Date endExclusive = AnalyticsDateUtil.dayEndExclusive(endDate);
+        return intValue(eventMapper.countNewUsersInRange(
+                AnalyticsDateUtil.dayStart(start), endExclusive));
+    }
+
+    private int countActiveUsersInWindow(Date endDate, int daysBack) {
+        Date start = AnalyticsDateUtil.addDays(endDate, -daysBack);
+        Date endExclusive = AnalyticsDateUtil.dayEndExclusive(endDate);
+        return intValue(eventMapper.countActiveUsersInRange(
+                AnalyticsDateUtil.dayStart(start), endExclusive));
+    }
+
+    private BigDecimal calcNextDayRetentionRate(Date anchorEnd) {
+        List<AnalyticsDailySummary> summaries = dashboardMapper.listDailySummaryBetween(anchorEnd, anchorEnd);
+        if (!summaries.isEmpty() && summaries.get(0).getNextDayRetentionRate() != null) {
+            return summaries.get(0).getNextDayRetentionRate();
+        }
+        Date cohortDay = AnalyticsDateUtil.addDays(anchorEnd, -1);
+        Date cohortStart = AnalyticsDateUtil.dayStart(cohortDay);
+        Date cohortEnd = AnalyticsDateUtil.dayEndExclusive(cohortDay);
+        Date statStart = AnalyticsDateUtil.dayStart(anchorEnd);
+        Date statEnd = AnalyticsDateUtil.dayEndExclusive(anchorEnd);
+        Long retained = eventMapper.countNextDayRetained(cohortStart, cohortEnd, statStart, statEnd);
+        Long cohortSize = eventMapper.countRegisterUsers(cohortStart, cohortEnd);
+        return AnalyticsDateUtil.calcRate(retained, cohortSize);
+    }
+
+    private List<AnalyticsDistributionItemVo> toDistributionItems(
+            List<Map<String, Object>> rows, java.util.function.Function<Object, String> nameResolver) {
+        long total = rows.stream().mapToLong(row -> longValue(row.get("cnt"))).sum();
+        return rows.stream().map(row -> {
+            AnalyticsDistributionItemVo item = new AnalyticsDistributionItemVo();
+            Object code = row.get("code");
+            item.setCode(code != null ? String.valueOf(code) : "未知");
+            item.setName(nameResolver.apply(code));
+            long count = longValue(row.get("cnt"));
+            item.setCount(count);
+            item.setRatio(calcRatio(count, total));
+            return item;
+        }).collect(Collectors.toList());
+    }
+
+    private String resolveGenderName(Object code) {
+        if (code == null) {
+            return "未知";
+        }
+        switch (String.valueOf(code)) {
+            case "1":
+                return "男";
+            case "2":
+                return "女";
+            case "0":
+            default:
+                return "未知";
+        }
+    }
+
+    private AnalyticsFunnelStageVo buildFunnelStage(String code, String name, long count, Long previous) {
+        AnalyticsFunnelStageVo vo = new AnalyticsFunnelStageVo();
+        vo.setStageCode(code);
+        vo.setStageName(name);
+        vo.setCount(count);
+        if (previous != null) {
+            vo.setConversionRate(AnalyticsDateUtil.calcRate(count, previous));
+        }
+        return vo;
+    }
+
+    private BigDecimal calcRatio(long part, long total) {
+        if (total <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf(part * 100.0 / total).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    private BigDecimal subtractRate(BigDecimal current, BigDecimal previous) {
+        if (current == null || previous == null) {
+            return null;
+        }
+        return current.subtract(previous).setScale(2, RoundingMode.HALF_UP);
+    }
+
+    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 int intValue(Object value) {
+        if (value == null) {
+            return 0;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).intValue();
+        }
+        return 0;
+    }
+
+    private long longValue(Object value) {
+        if (value == null) {
+            return 0L;
+        }
+        if (value instanceof Number) {
+            return ((Number) value).longValue();
+        }
+        return 0L;
+    }
+}

+ 86 - 0
alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsPeriodContext.java

@@ -0,0 +1,86 @@
+package shop.alien.store.util.analytics;
+
+import lombok.Getter;
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
+
+import java.util.Date;
+
+/**
+ * 看板查询周期上下文
+ */
+@Getter
+public class AnalyticsPeriodContext {
+
+    private final String period;
+    private final Date startDate;
+    private final Date endDate;
+    private final Date startTime;
+    private final Date endTimeExclusive;
+    private final boolean hourly;
+    private final Date cohortDate;
+
+    public AnalyticsPeriodContext(String period, Date startDate, Date endDate,
+                                  Date startTime, Date endTimeExclusive,
+                                  boolean hourly, Date cohortDate) {
+        this.period = period;
+        this.startDate = startDate;
+        this.endDate = endDate;
+        this.startTime = startTime;
+        this.endTimeExclusive = endTimeExclusive;
+        this.hourly = hourly;
+        this.cohortDate = cohortDate;
+    }
+
+    public static AnalyticsPeriodContext resolve(String period) {
+        String normalized = AnalyticsStatPeriod.normalize(period);
+        Date today = AnalyticsDateUtil.truncateToDate(new Date());
+        Date startDate;
+        Date endDate;
+        boolean hourly;
+
+        switch (normalized) {
+            case AnalyticsStatPeriod.TODAY:
+                startDate = today;
+                endDate = today;
+                hourly = true;
+                break;
+            case AnalyticsStatPeriod.YESTERDAY:
+                startDate = AnalyticsDateUtil.addDays(today, -1);
+                endDate = startDate;
+                hourly = true;
+                break;
+            case AnalyticsStatPeriod.LAST_7D:
+                startDate = AnalyticsDateUtil.addDays(today, -6);
+                endDate = today;
+                hourly = false;
+                break;
+            case AnalyticsStatPeriod.LAST_30D:
+                startDate = AnalyticsDateUtil.addDays(today, -29);
+                endDate = today;
+                hourly = false;
+                break;
+            default:
+                throw new IllegalArgumentException("不支持的 period: " + period);
+        }
+
+        Date startTime = AnalyticsDateUtil.dayStart(startDate);
+        Date endTimeExclusive = AnalyticsDateUtil.dayEndExclusive(endDate);
+        Date cohortDate = startDate;
+        return new AnalyticsPeriodContext(normalized, startDate, endDate, startTime, endTimeExclusive, hourly, cohortDate);
+    }
+
+    /** 上一对比周期的结束日(用于环比) */
+    public Date previousEndDate() {
+        switch (period) {
+            case AnalyticsStatPeriod.TODAY:
+            case AnalyticsStatPeriod.YESTERDAY:
+                return AnalyticsDateUtil.addDays(startDate, -1);
+            case AnalyticsStatPeriod.LAST_7D:
+                return AnalyticsDateUtil.addDays(startDate, -1);
+            case AnalyticsStatPeriod.LAST_30D:
+                return AnalyticsDateUtil.addDays(startDate, -1);
+            default:
+                return AnalyticsDateUtil.addDays(startDate, -1);
+        }
+    }
+}

+ 43 - 0
alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsTrendFillUtil.java

@@ -0,0 +1,43 @@
+package shop.alien.store.util.analytics;
+
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsTrendPointVo;
+
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.function.BiConsumer;
+import java.util.function.Supplier;
+
+/**
+ * 按日趋势图补全空白天
+ */
+public final class AnalyticsTrendFillUtil {
+
+    private AnalyticsTrendFillUtil() {
+    }
+
+    public static <T extends AnalyticsTrendPointVo> List<T> fillDailyRange(
+            Date startDate, Date endDate,
+            Map<Date, T> existing,
+            Supplier<T> factory,
+            BiConsumer<T, Date> filler) {
+        List<T> result = new ArrayList<>();
+        Date cursor = AnalyticsDateUtil.truncateToDate(startDate);
+        Date end = AnalyticsDateUtil.truncateToDate(endDate);
+        SimpleDateFormat labelFormat = new SimpleDateFormat("M.d");
+        while (!cursor.after(end)) {
+            T point = existing.get(cursor);
+            if (point == null) {
+                point = factory.get();
+                point.setStatDate(cursor);
+                point.setLabel(labelFormat.format(cursor));
+                filler.accept(point, cursor);
+            }
+            result.add(point);
+            cursor = AnalyticsDateUtil.addDays(cursor, 1);
+        }
+        return result;
+    }
+}