Bläddra i källkod

代码维护 参照原型设计

lutong 8 timmar sedan
förälder
incheckning
88b0273f78
42 ändrade filer med 1223 tillägg och 75 borttagningar
  1. 1 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsEventCode.java
  2. 1 0
      alien-entity/src/main/java/shop/alien/entity/analytics/AnalyticsScene.java
  3. 9 0
      alien-entity/src/main/java/shop/alien/entity/analytics/dto/AnalyticsFrontReportDTO.java
  4. 3 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportChartsVo.java
  5. 39 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportRecordVo.java
  6. 6 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportSummaryVo.java
  7. 38 9
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsDashboardSummaryVo.java
  8. 38 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsFunnelDetailVo.java
  9. 42 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsKpiCardVo.java
  10. 42 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantProfileVo.java
  11. 3 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantReportChartsVo.java
  12. 18 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsReviewRateTrendVo.java
  13. 45 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserProfileVo.java
  14. 6 0
      alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserReportSummaryVo.java
  15. 5 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsContentReportMapper.java
  16. 1 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsEventMapper.java
  17. 19 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsFunnelMapper.java
  18. 3 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsMerchantReportMapper.java
  19. 2 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsUserReportMapper.java
  20. 7 0
      alien-entity/src/main/java/shop/alien/mapper/AnalyticsUserStatTodayMapper.java
  21. 29 1
      alien-entity/src/main/resources/mapper/AnalyticsContentReportMapper.xml
  22. 23 0
      alien-entity/src/main/resources/mapper/AnalyticsFunnelMapper.xml
  23. 10 0
      alien-entity/src/main/resources/mapper/AnalyticsMerchantReportMapper.xml
  24. 22 0
      alien-entity/src/main/resources/mapper/AnalyticsUserReportMapper.xml
  25. 15 5
      alien-store/doc/前端埋点接入指南.md
  26. 14 2
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsContentReportController.java
  27. 15 2
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsDashboardController.java
  28. 19 2
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsMerchantReportController.java
  29. 13 1
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsStatController.java
  30. 20 0
      alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsUserReportController.java
  31. 5 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsContentReportService.java
  32. 3 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsDashboardService.java
  33. 4 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsMerchantReportService.java
  34. 5 0
      alien-store/src/main/java/shop/alien/store/service/analytics/AnalyticsUserReportService.java
  35. 54 1
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsContentReportServiceImpl.java
  36. 279 52
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDashboardServiceImpl.java
  37. 68 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsMerchantReportServiceImpl.java
  38. 5 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatisticsServiceImpl.java
  39. 104 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsTrackServiceImpl.java
  40. 88 0
      alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsUserReportServiceImpl.java
  41. 84 0
      alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsFunnelStageCodes.java
  42. 16 0
      alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsPeriodContext.java

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

@@ -24,6 +24,7 @@ public final class AnalyticsEventCode {
     public static final String MERCHANT_CONTACT = "merchant.contact";
     public static final String MERCHANT_VIEW = "merchant.view";
     public static final String MERCHANT_VERIFY = "merchant.verify";
+    public static final String MERCHANT_REVIEW = "merchant.review";
 
     public static final String CONTENT_PUBLISH = "content.publish";
     public static final String CONTENT_INTERACT = "content.interact";

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

@@ -27,6 +27,7 @@ public enum AnalyticsScene {
     MERCHANT_CONTACT("MERCHANT_CONTACT", "电话/导航", AnalyticsEventCode.MERCHANT_CONTACT, "MERCHANT"),
     MERCHANT_VIEW("MERCHANT_VIEW", "访问商户", AnalyticsEventCode.MERCHANT_VIEW, "MERCHANT"),
     MERCHANT_VERIFY("MERCHANT_VERIFY", "商户核销", AnalyticsEventCode.MERCHANT_VERIFY, "MERCHANT"),
+    MERCHANT_REVIEW("MERCHANT_REVIEW", "商户评价", AnalyticsEventCode.MERCHANT_REVIEW, "MERCHANT"),
 
     REGISTER_PAGE("REGISTER_PAGE", "进入注册页", AnalyticsEventCode.USER_REGISTER_PAGE, "USER"),
     REGISTER_PHONE("REGISTER_PHONE", "提交手机号", AnalyticsEventCode.USER_REGISTER_PHONE, "USER"),

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

@@ -63,4 +63,13 @@ public class AnalyticsFrontReportDTO {
     @ApiModelProperty("事件时间")
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date eventTime;
+
+    @ApiModelProperty("内容标题快照(举报等)")
+    private String contentTitle;
+
+    @ApiModelProperty("举报类型(不实信息/色情低俗等,也可用eventSubtype)")
+    private String reportType;
+
+    @ApiModelProperty("操作人ID(举报处理人等)")
+    private Long operatorId;
 }

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

@@ -29,4 +29,7 @@ public class AnalyticsContentReportChartsVo {
 
     @ApiModelProperty("近期审核通过列表(默认第1页)")
     private IPage<AnalyticsContentReportDetailVo> auditPassedPage;
+
+    @ApiModelProperty("近期举报处理列表(默认第1页)")
+    private IPage<AnalyticsContentReportRecordVo> reportPage;
 }

+ 39 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/contentreport/AnalyticsContentReportRecordVo.java

@@ -0,0 +1,39 @@
+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 AnalyticsContentReportRecordVo {
+
+    @ApiModelProperty("举报单号")
+    private String reportId;
+
+    @ApiModelProperty("展示举报ID(如R001)")
+    private String displayReportId;
+
+    @ApiModelProperty("内容ID")
+    private Long contentId;
+
+    @ApiModelProperty("内容标题")
+    private String contentTitle;
+
+    @ApiModelProperty("举报类型")
+    private String reportType;
+
+    @ApiModelProperty("处理状态(处理中/已处理)")
+    private String statusLabel;
+
+    @ApiModelProperty("举报时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date reportTime;
+
+    @ApiModelProperty("处理时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date handleTime;
+}

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

@@ -30,4 +30,10 @@ public class AnalyticsContentReportSummaryVo {
 
     @ApiModelProperty("审核处理率(%)")
     private BigDecimal auditProcessRate;
+
+    @ApiModelProperty("举报处理率(%)")
+    private BigDecimal reportHandleRate;
+
+    @ApiModelProperty("举报处理率环比变化(百分点)")
+    private BigDecimal reportHandleRateDelta;
 }

+ 38 - 9
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsDashboardSummaryVo.java

@@ -7,30 +7,59 @@ import lombok.Data;
 import java.math.BigDecimal;
 
 @Data
-@ApiModel("数据看板顶部指标")
+@ApiModel("数据看板顶部指标(8张卡片)")
 public class AnalyticsDashboardSummaryVo {
 
-    @ApiModelProperty("DAU")
+    @ApiModelProperty("统计周期")
+    private String period;
+
+    @ApiModelProperty("活跃用户(DAU/近N日活跃)")
+    private AnalyticsKpiCardVo activeUser;
+
+    @ApiModelProperty("新增用户")
+    private AnalyticsKpiCardVo newUser;
+
+    @ApiModelProperty("对话次数")
+    private AnalyticsKpiCardVo aiChat;
+
+    @ApiModelProperty("内容发布")
+    private AnalyticsKpiCardVo contentPublish;
+
+    @ApiModelProperty("商家访问UV")
+    private AnalyticsKpiCardVo merchantVisitUvCard;
+
+    @ApiModelProperty("AI平均响应时间")
+    private AnalyticsKpiCardVo aiResponse;
+
+    @ApiModelProperty("当前在线(实时)")
+    private AnalyticsKpiCardVo onlineUser;
+
+    @ApiModelProperty("转化率")
+    private AnalyticsKpiCardVo conversionRateCard;
+
+    // ---- 兼容旧字段(与卡片 value 同步) ----
+
+    @ApiModelProperty("DAU/周期活跃(兼容)")
     private Integer dau;
 
-    @ApiModelProperty("新增用户数")
+    @ApiModelProperty("新增用户数(兼容)")
     private Integer newUserCount;
 
-    @ApiModelProperty("对话次数")
+    @ApiModelProperty("对话次数(兼容)")
     private Integer aiChatCount;
 
-    @ApiModelProperty("内容发布数")
+    @ApiModelProperty("内容发布数(兼容)")
     private Integer contentPublishCount;
 
-    @ApiModelProperty("商家访问UV")
+    @ApiModelProperty("商家访问UV(兼容)")
     private Integer merchantVisitUv;
 
-    @ApiModelProperty("AI平均响应时间(毫秒)")
+    @ApiModelProperty("AI平均响应时间毫秒(兼容)")
     private Long aiAvgResponseMs;
 
-    @ApiModelProperty("当前在线人数(仅今日/昨日有效)")
+    @ApiModelProperty("当前在线人数(兼容)")
     private Integer onlineUserCount;
 
-    @ApiModelProperty("订单转化率(%)")
+    @ApiModelProperty("订单转化率%(兼容)")
     private BigDecimal conversionRate;
 }

+ 38 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsFunnelDetailVo.java

@@ -0,0 +1,38 @@
+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 AnalyticsFunnelDetailVo {
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("展示用户ID")
+    private String displayUserId;
+
+    @ApiModelProperty("商户ID(商家漏斗)")
+    private Long merchantId;
+
+    @ApiModelProperty("展示商户ID")
+    private String displayMerchantId;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("设备")
+    private String deviceType;
+
+    @ApiModelProperty("渠道")
+    private String channel;
+
+    @ApiModelProperty("最近事件时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date eventTime;
+}

+ 42 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/dashboard/AnalyticsKpiCardVo.java

@@ -0,0 +1,42 @@
+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("看板KPI指标卡片")
+public class AnalyticsKpiCardVo {
+
+    @ApiModelProperty("指标编码(DAU/NEW_USER/AI_CHAT/CONTENT_PUBLISH/MERCHANT_UV/AI_RESPONSE/ONLINE/CONVERSION)")
+    private String metricCode;
+
+    @ApiModelProperty("展示标题(随period变化,如今日DAU/近7日活跃)")
+    private String label;
+
+    @ApiModelProperty("当前值")
+    private BigDecimal value;
+
+    @ApiModelProperty("较上期绝对变化(可为负)")
+    private BigDecimal delta;
+
+    @ApiModelProperty("较上期变化率(%)")
+    private BigDecimal changeRate;
+
+    @ApiModelProperty("单位(ms/%/空)")
+    private String unit;
+
+    @ApiModelProperty("数值类型(COUNT/RATE/DURATION)")
+    private String valueType;
+
+    @ApiModelProperty("环比文案(较昨日/较上期)")
+    private String compareLabel;
+
+    @ApiModelProperty("是否实时指标(当前在线)")
+    private Boolean realTime;
+
+    @ApiModelProperty("数值越低越好(AI响应时间)")
+    private Boolean lowerIsBetter;
+}

+ 42 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsMerchantProfileVo.java

@@ -0,0 +1,42 @@
+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 AnalyticsMerchantProfileVo {
+
+    @ApiModelProperty("商家ID")
+    private Long merchantId;
+
+    @ApiModelProperty("展示商家ID")
+    private String displayMerchantId;
+
+    @ApiModelProperty("商家名称")
+    private String merchantName;
+
+    @ApiModelProperty("分类")
+    private String categoryName;
+
+    @ApiModelProperty("入驻时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date settleTime;
+
+    @ApiModelProperty("周期GMV(元)")
+    private BigDecimal periodGmv;
+
+    @ApiModelProperty("核销率(%)")
+    private BigDecimal verifyRate;
+
+    @ApiModelProperty("评价率(%)")
+    private BigDecimal reviewRate;
+
+    @ApiModelProperty("状态")
+    private String status;
+}

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

@@ -26,6 +26,9 @@ public class AnalyticsMerchantReportChartsVo {
     @ApiModelProperty("商家平均单量趋势")
     private List<AnalyticsAvgOrderTrendVo> avgOrderTrend;
 
+    @ApiModelProperty("商家评价率趋势")
+    private List<AnalyticsReviewRateTrendVo> reviewRateTrend;
+
     @ApiModelProperty("商家GMV TOP10")
     private List<AnalyticsMerchantGmvRankVo> gmvTop10;
 }

+ 18 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/merchantreport/AnalyticsReviewRateTrendVo.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("商家评价率趋势点")
+public class AnalyticsReviewRateTrendVo extends AnalyticsTrendPointVo {
+
+    @ApiModelProperty("评价率(%)")
+    private BigDecimal reviewRate;
+}

+ 45 - 0
alien-entity/src/main/java/shop/alien/entity/analytics/vo/userreport/AnalyticsUserProfileVo.java

@@ -0,0 +1,45 @@
+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 AnalyticsUserProfileVo {
+
+    @ApiModelProperty("用户ID")
+    private Long userId;
+
+    @ApiModelProperty("展示用户ID")
+    private String displayUserId;
+
+    @ApiModelProperty("脱敏手机号")
+    private String maskedPhone;
+
+    @ApiModelProperty("注册时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date registerTime;
+
+    @ApiModelProperty("最近活跃")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date lastActiveTime;
+
+    @ApiModelProperty("城市")
+    private String city;
+
+    @ApiModelProperty("渠道")
+    private String channel;
+
+    @ApiModelProperty("性别")
+    private String genderLabel;
+
+    @ApiModelProperty("年龄段")
+    private String ageGroup;
+
+    @ApiModelProperty("状态")
+    private String status;
+}

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

@@ -22,6 +22,12 @@ public class AnalyticsUserReportSummaryVo {
     @ApiModelProperty("近7日新增用户较上期变化")
     private Integer last7dNewUserDelta;
 
+    @ApiModelProperty("近7日活跃用户(WAU)")
+    private Integer last7dActiveUserCount;
+
+    @ApiModelProperty("近7日活跃用户较上期变化")
+    private Integer last7dActiveUserDelta;
+
     @ApiModelProperty("近30日活跃用户")
     private Integer last30dActiveUserCount;
 

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

@@ -5,6 +5,7 @@ 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 shop.alien.entity.analytics.AnalyticsReportRecord;
 
 import java.util.Date;
 import java.util.List;
@@ -28,4 +29,8 @@ public interface AnalyticsContentReportMapper {
     IPage<AnalyticsContentStat> pageAuditPassed(Page<AnalyticsContentStat> page,
                                                 @Param("startTime") Date startTime,
                                                 @Param("endTime") Date endTime);
+
+    IPage<AnalyticsReportRecord> pageReportRecords(Page<AnalyticsReportRecord> page,
+                                                   @Param("startTime") Date startTime,
+                                                   @Param("endTime") Date endTime);
 }

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

@@ -63,6 +63,7 @@ public interface AnalyticsEventMapper extends BaseMapper<AnalyticsEvent> {
             "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(CASE WHEN event_code = 'merchant.review' THEN 1 END) AS reviewCount, " +
             "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, " +

+ 19 - 0
alien-entity/src/main/java/shop/alien/mapper/AnalyticsFunnelMapper.java

@@ -0,0 +1,19 @@
+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 java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface AnalyticsFunnelMapper {
+
+    IPage<Map<String, Object>> pageFunnelUsers(Page<?> page,
+                                               @Param("startTime") Date startTime,
+                                               @Param("endTime") Date endTime,
+                                               @Param("eventCodes") List<String> eventCodes);
+}

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

@@ -31,4 +31,7 @@ public interface AnalyticsMerchantReportMapper {
     IPage<AnalyticsMerchantStat> pageSettledMerchantDetail(Page<AnalyticsMerchantStat> page,
                                                            @Param("startDate") Date startDate,
                                                            @Param("endDate") Date endDate);
+
+    List<Map<String, Object>> listReviewRateTrend(@Param("startDate") Date startDate,
+                                                  @Param("endDate") Date endDate);
 }

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

@@ -31,4 +31,6 @@ public interface AnalyticsUserReportMapper {
     IPage<AnalyticsUserStat> pageNewUsersInRange(Page<AnalyticsUserStat> page,
                                                  @Param("startTime") Date startTime,
                                                  @Param("endTime") Date endTime);
+
+    AnalyticsUserStat findLatestUserStat(@Param("userId") Long userId);
 }

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

@@ -2,8 +2,15 @@ package shop.alien.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 import shop.alien.entity.analytics.AnalyticsUserStatToday;
 
+import java.util.Date;
+
 @Mapper
 public interface AnalyticsUserStatTodayMapper extends BaseMapper<AnalyticsUserStatToday> {
+
+    @Select("SELECT COUNT(*) FROM analytics_user_stat_today WHERE last_active_time >= #{since}")
+    Long countOnlineSince(@Param("since") Date since);
 }

+ 29 - 1
alien-entity/src/main/resources/mapper/AnalyticsContentReportMapper.xml

@@ -21,7 +21,9 @@
                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
+               COALESCE(SUM(audit_reject_count), 0)        AS auditRejectCount,
+               COALESCE(SUM(report_submit_count), 0)       AS reportSubmitCount,
+               COALESCE(SUM(report_handle_count), 0)       AS reportHandleCount
         FROM analytics_daily_summary
         WHERE stat_date &gt;= #{startDate}
           AND stat_date &lt;= #{endDate}
@@ -104,4 +106,30 @@
         ) latest ON t.content_id = latest.content_id AND t.audit_time = latest.max_audit_time
         ORDER BY t.audit_time DESC
     </select>
+
+    <select id="pageReportRecords" resultType="shop.alien.entity.analytics.AnalyticsReportRecord">
+        SELECT report_id AS reportId,
+               content_id AS contentId,
+               content_type AS contentType,
+               content_title AS contentTitle,
+               report_type AS reportType,
+               status,
+               reporter_user_id AS reporterUserId,
+               report_time AS reportTime,
+               handle_time AS handleTime
+        FROM (
+            SELECT report_id, content_id, content_type, content_title, report_type, status,
+                   reporter_user_id, report_time, handle_time
+            FROM analytics_report_record_today
+            WHERE report_time &gt;= #{startTime}
+              AND report_time &lt; #{endTime}
+            UNION ALL
+            SELECT report_id, content_id, content_type, content_title, report_type, status,
+                   reporter_user_id, report_time, handle_time
+            FROM analytics_report_record_history
+            WHERE report_time &gt;= #{startTime}
+              AND report_time &lt; #{endTime}
+        ) t
+        ORDER BY t.report_time DESC
+    </select>
 </mapper>

+ 23 - 0
alien-entity/src/main/resources/mapper/AnalyticsFunnelMapper.xml

@@ -0,0 +1,23 @@
+<?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.AnalyticsFunnelMapper">
+
+    <select id="pageFunnelUsers" resultType="java.util.HashMap">
+        SELECT e.user_id AS userId,
+               MAX(e.merchant_id) AS merchantId,
+               MAX(e.city) AS city,
+               MAX(e.device_type) AS deviceType,
+               MAX(e.channel) AS channel,
+               MAX(e.event_time) AS eventTime
+        FROM analytics_event e
+        WHERE e.event_time &gt;= #{startTime}
+          AND e.event_time &lt; #{endTime}
+          AND e.user_id IS NOT NULL
+          AND e.event_code IN
+          <foreach collection="eventCodes" item="code" open="(" separator="," close=")">
+              #{code}
+          </foreach>
+        GROUP BY e.user_id
+        ORDER BY eventTime DESC
+    </select>
+</mapper>

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

@@ -126,4 +126,14 @@
         ) period ON base.merchant_id = period.merchant_id
         ORDER BY base.settle_time DESC
     </select>
+
+    <select id="listReviewRateTrend" resultType="java.util.HashMap">
+        SELECT stat_date AS statDate,
+               DATE_FORMAT(stat_date, '%m-%d') AS label,
+               COALESCE(merchant_review_rate, 0) AS reviewRate
+        FROM analytics_daily_summary
+        WHERE stat_date &gt;= #{startDate}
+          AND stat_date &lt;= #{endDate}
+        ORDER BY stat_date ASC
+    </select>
 </mapper>

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

@@ -107,4 +107,26 @@
         ) latest ON t.user_id = latest.user_id AND t.register_time = latest.max_register_time
         ORDER BY t.register_time DESC
     </select>
+
+    <select id="findLatestUserStat" resultType="shop.alien.entity.analytics.AnalyticsUserStat">
+        SELECT user_id AS userId,
+               user_phone AS userPhone,
+               register_time AS registerTime,
+               last_active_time AS lastActiveTime,
+               city,
+               channel,
+               gender,
+               age_group AS ageGroup
+        FROM (
+            SELECT user_id, user_phone, register_time, last_active_time, city, channel, gender, age_group, stat_date
+            FROM analytics_user_stat_history
+            WHERE user_id = #{userId}
+            UNION ALL
+            SELECT user_id, user_phone, register_time, last_active_time, city, channel, gender, age_group, CURDATE() AS stat_date
+            FROM analytics_user_stat_today
+            WHERE user_id = #{userId}
+        ) t
+        ORDER BY stat_date DESC, last_active_time DESC
+        LIMIT 1
+    </select>
 </mapper>

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

@@ -227,8 +227,11 @@ Analytics.aiRequest({
 | `CONTENT_INTERACT` | 内容互动 | targetId, contentType, eventSubtype |
 | `PAY_SUCCESS` | 支付成功 | targetId, amount, merchantId, businessCategory |
 | `AI_CHAT_START` | AI 对话开始 | - |
-| `REPORT_SUBMIT` | 提交举报 | targetId |
+| `REPORT_SUBMIT` | 提交举报 | targetId, contentType, contentTitle, reportType/eventSubtype |
+| `REPORT_HANDLE` | 举报处理 | eventSubtype=原举报单号(reportId), operatorId |
+| `MERCHANT_REVIEW` | 商户评价 | merchantId |
 | `AUDIT_SUBMIT` | 提交审核 | targetId, contentType |
+| `AUDIT_PASS` | 审核通过 | targetId, contentType, eventSubtype |
 | `AUDIT_REJECT` | 审核驳回 | targetId, contentType, eventSubtype=reject |
 
 完整列表调用:`GET /analytics/front/scenes`
@@ -483,10 +486,10 @@ POST /analytics/stat/archive
 | 模块 | 汇总接口 | 说明 |
 |------|----------|------|
 | 数据看板 | `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/dashboard/summary?period=` | 顶部 8 张 KPI 卡片(含 value/delta/changeRate) |
+| 用户报表 | `GET /analytics/user-report/charts?period=&detailPage=1&detailSize=10` | 含新注册用户明细分页;summary 含近7日活跃 |
+| 内容报表 | `GET /analytics/content-report/charts?period=&detailPage=1&detailSize=10&reportPage=1&reportSize=10` | 含审核通过列表 + 举报处理列表 |
+| 商家报表 | `GET /analytics/merchant-report/charts?period=` | GMV/分类/评价率趋势/客单价趋势等 |
 
 | 接口 | 说明 |
 |------|------|
@@ -497,6 +500,13 @@ POST /analytics/stat/archive
 | `GET /analytics/stat/merchant/page?statDate=` | 商家访问 UV 明细 |
 | `GET /analytics/stat/content/page?statDate=&contentType=` | 内容发布明细 |
 | `GET /analytics/stat/conversion/page?startDate=&endDate=` | 转化率日明细 |
+| `GET /analytics/stat/report/page?period=` | 举报处理明细分页 |
+| `GET /analytics/dashboard/conversion-funnel/detail?period=&stageCode=` | 商家漏斗下钻(EXPOSE/CLICK/DETAIL/CONTACT/STORE_VISIT) |
+| `GET /analytics/user-report/register-funnel/detail?period=&stageCode=` | 注册漏斗下钻 |
+| `GET /analytics/user-report/user/{userId}` | 用户详情抽屉 |
+| `GET /analytics/merchant-report/merchant/{merchantId}?period=` | 商家详情抽屉 |
+| `GET /analytics/content-report/report/page?period=` | 举报处理列表 |
+| `GET /analytics/merchant-report/review-rate-trend?period=` | 商家评价率趋势 |
 
 > 查询历史日期时读历史表;`statDate` 为空或当天时读今日表。
 

+ 14 - 2
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsContentReportController.java

@@ -35,8 +35,10 @@ public class AnalyticsContentReportController {
             @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));
+            @RequestParam(defaultValue = "10") long detailSize,
+            @RequestParam(defaultValue = "1") long reportPage,
+            @RequestParam(defaultValue = "10") long reportSize) {
+        return R.data(contentReportService.loadAllCharts(period, detailPage, detailSize, reportPage, reportSize));
     }
 
     @ApiOperation("顶部指标卡片")
@@ -80,4 +82,14 @@ public class AnalyticsContentReportController {
             @RequestParam(defaultValue = "10") long size) {
         return R.data(contentReportService.pageAuditPassed(period, page, size));
     }
+
+    @ApiOperation("近期举报处理列表(分页)")
+    @ApiOperationSupport(order = 7)
+    @GetMapping("/report/page")
+    public R<IPage<AnalyticsContentReportRecordVo>> pageReportRecords(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_7D) String period,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(contentReportService.pageReportRecords(period, page, size));
+    }
 }

+ 15 - 2
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsDashboardController.java

@@ -1,5 +1,6 @@
 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;
@@ -78,8 +79,20 @@ public class AnalyticsDashboardController {
         return R.data(dashboardService.conversionFunnel(period));
     }
 
-    @ApiOperation("用户留存率(Day1~Day30)")
+    @ApiOperation("商家转化漏斗阶段下钻明细分页")
     @ApiOperationSupport(order = 7)
+    @GetMapping("/conversion-funnel/detail")
+    public R<IPage<AnalyticsFunnelDetailVo>> pageConversionFunnelDetail(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @ApiParam(value = "阶段编码", allowableValues = "EXPOSE,CLICK,DETAIL,CONTACT,STORE_VISIT")
+            @RequestParam String stageCode,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(dashboardService.pageConversionFunnelDetail(period, stageCode, page, size));
+    }
+
+    @ApiOperation("用户留存率(Day1~Day30)")
+    @ApiOperationSupport(order = 8)
     @GetMapping("/user-retention")
     public R<List<AnalyticsRetentionPointVo>> userRetention(
             @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {
@@ -87,7 +100,7 @@ public class AnalyticsDashboardController {
     }
 
     @ApiOperation("人均使用时长趋势(分钟)")
-    @ApiOperationSupport(order = 8)
+    @ApiOperationSupport(order = 9)
     @GetMapping("/avg-usage-duration")
     public R<List<AnalyticsAvgDurationTrendVo>> avgUsageDuration(
             @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period) {

+ 19 - 2
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsMerchantReportController.java

@@ -69,8 +69,16 @@ public class AnalyticsMerchantReportController {
         return R.data(merchantReportService.avgOrderTrend(period));
     }
 
-    @ApiOperation("商家GMV TOP10")
+    @ApiOperation("商家评价率趋势")
     @ApiOperationSupport(order = 6)
+    @GetMapping("/review-rate-trend")
+    public R<List<AnalyticsReviewRateTrendVo>> reviewRateTrend(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
+        return R.data(merchantReportService.reviewRateTrend(period));
+    }
+
+    @ApiOperation("商家GMV TOP10")
+    @ApiOperationSupport(order = 7)
     @GetMapping("/gmv-top10")
     public R<List<AnalyticsMerchantGmvRankVo>> gmvTop10(
             @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period) {
@@ -78,7 +86,7 @@ public class AnalyticsMerchantReportController {
     }
 
     @ApiOperation("入驻商家明细分页")
-    @ApiOperationSupport(order = 7)
+    @ApiOperationSupport(order = 8)
     @GetMapping("/settled/page")
     public R<IPage<AnalyticsMerchantReportDetailVo>> pageSettledMerchant(
             @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period,
@@ -86,4 +94,13 @@ public class AnalyticsMerchantReportController {
             @RequestParam(defaultValue = "10") long size) {
         return R.data(merchantReportService.pageSettledMerchant(period, page, size));
     }
+
+    @ApiOperation("商家详情(抽屉)")
+    @ApiOperationSupport(order = 9)
+    @GetMapping("/merchant/{merchantId}")
+    public R<AnalyticsMerchantProfileVo> merchantProfile(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_30D) String period,
+            @PathVariable Long merchantId) {
+        return R.data(merchantReportService.getMerchantProfile(period, merchantId));
+    }
 }

+ 13 - 1
alien-store/src/main/java/shop/alien/store/controller/analytics/AnalyticsStatController.java

@@ -20,6 +20,7 @@ import shop.alien.entity.analytics.vo.AnalyticsUserStatVo;
 import shop.alien.entity.result.R;
 import shop.alien.mapper.*;
 import shop.alien.store.service.analytics.AnalyticsArchiveService;
+import shop.alien.store.service.analytics.AnalyticsContentReportService;
 import shop.alien.store.service.analytics.AnalyticsStatEnrichService;
 import shop.alien.store.service.analytics.AnalyticsStatisticsService;
 
@@ -41,6 +42,7 @@ public class AnalyticsStatController {
 
     private final AnalyticsStatisticsService statisticsService;
     private final AnalyticsStatEnrichService statEnrichService;
+    private final AnalyticsContentReportService contentReportService;
     private final AnalyticsArchiveService archiveService;
     private final AnalyticsAiChatStatMapper aiChatStatMapper;
     private final AnalyticsAiRequestMapper aiRequestMapper;
@@ -136,8 +138,18 @@ public class AnalyticsStatController {
         return R.data(statEnrichService.pageConversionDaily(page, size, startDate, endDate));
     }
 
-    @ApiOperation("分页查询AI对话明细")
+    @ApiOperation("分页查询举报处理明细")
     @ApiOperationSupport(order = 10)
+    @GetMapping("/report/page")
+    public R<IPage<shop.alien.entity.analytics.vo.contentreport.AnalyticsContentReportRecordVo>> pageReportRecords(
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size,
+            @RequestParam(defaultValue = AnalyticsStatPeriod.LAST_7D) String period) {
+        return R.data(contentReportService.pageReportRecords(period, page, size));
+    }
+
+    @ApiOperation("分页查询AI对话明细")
+    @ApiOperationSupport(order = 11)
     @GetMapping("/ai-chat/page")
     public R<IPage<AnalyticsAiChatStat>> pageAiChatStat(
             @RequestParam(defaultValue = "1") long page,

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

@@ -10,6 +10,7 @@ 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.AnalyticsFunnelDetailVo;
 import shop.alien.entity.analytics.vo.dashboard.AnalyticsFunnelStageVo;
 import shop.alien.entity.analytics.vo.userreport.*;
 import shop.alien.entity.result.R;
@@ -99,4 +100,23 @@ public class AnalyticsUserReportController {
             @RequestParam(defaultValue = "10") long size) {
         return R.data(userReportService.pageNewUserDetail(period, page, size));
     }
+
+    @ApiOperation("注册漏斗阶段下钻明细分页")
+    @ApiOperationSupport(order = 9)
+    @GetMapping("/register-funnel/detail")
+    public R<IPage<AnalyticsFunnelDetailVo>> pageRegisterFunnelDetail(
+            @RequestParam(defaultValue = AnalyticsStatPeriod.TODAY) String period,
+            @ApiParam(value = "阶段编码", allowableValues = "REGISTER_PAGE,PHONE_SUBMIT,OTP_PASS,PASSWORD_SET,REGISTER_SUCCESS")
+            @RequestParam String stageCode,
+            @RequestParam(defaultValue = "1") long page,
+            @RequestParam(defaultValue = "10") long size) {
+        return R.data(userReportService.pageRegisterFunnelDetail(period, stageCode, page, size));
+    }
+
+    @ApiOperation("用户详情(抽屉)")
+    @ApiOperationSupport(order = 10)
+    @GetMapping("/user/{userId}")
+    public R<AnalyticsUserProfileVo> userProfile(@PathVariable Long userId) {
+        return R.data(userReportService.getUserProfile(userId));
+    }
 }

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

@@ -12,6 +12,9 @@ public interface AnalyticsContentReportService {
 
     AnalyticsContentReportChartsVo loadAllCharts(String period, long detailPage, long detailSize);
 
+    AnalyticsContentReportChartsVo loadAllCharts(String period, long detailPage, long detailSize,
+                                                 long reportPage, long reportSize);
+
     AnalyticsContentReportSummaryVo summary(String period);
 
     List<AnalyticsDistributionItemVo> categoryDistribution(String period);
@@ -21,4 +24,6 @@ public interface AnalyticsContentReportService {
     List<AnalyticsContentInteractionRankVo> interactionTop10(String period);
 
     IPage<AnalyticsContentReportDetailVo> pageAuditPassed(String period, long page, long size);
+
+    IPage<AnalyticsContentReportRecordVo> pageReportRecords(String period, long page, long size);
 }

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

@@ -1,5 +1,6 @@
 package shop.alien.store.service.analytics;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import shop.alien.entity.analytics.vo.dashboard.*;
 
 import java.util.List;
@@ -21,6 +22,8 @@ public interface AnalyticsDashboardService {
 
     List<AnalyticsFunnelStageVo> conversionFunnel(String period);
 
+    IPage<AnalyticsFunnelDetailVo> pageConversionFunnelDetail(String period, String stageCode, long page, long size);
+
     List<AnalyticsRetentionPointVo> userRetention(String period);
 
     List<AnalyticsAvgDurationTrendVo> avgUsageDuration(String period);

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

@@ -20,5 +20,9 @@ public interface AnalyticsMerchantReportService {
 
     List<AnalyticsMerchantGmvRankVo> gmvTop10(String period);
 
+    List<AnalyticsReviewRateTrendVo> reviewRateTrend(String period);
+
     IPage<AnalyticsMerchantReportDetailVo> pageSettledMerchant(String period, long page, long size);
+
+    AnalyticsMerchantProfileVo getMerchantProfile(String period, Long merchantId);
 }

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

@@ -1,6 +1,7 @@
 package shop.alien.store.service.analytics;
 
 import com.baomidou.mybatisplus.core.metadata.IPage;
+import shop.alien.entity.analytics.vo.dashboard.AnalyticsFunnelDetailVo;
 import shop.alien.entity.analytics.vo.dashboard.AnalyticsFunnelStageVo;
 import shop.alien.entity.analytics.vo.userreport.*;
 
@@ -28,4 +29,8 @@ public interface AnalyticsUserReportService {
     List<AnalyticsDistributionItemVo> geoTop10(String period);
 
     IPage<AnalyticsUserReportDetailVo> pageNewUserDetail(String period, long page, long size);
+
+    IPage<AnalyticsFunnelDetailVo> pageRegisterFunnelDetail(String period, String stageCode, long page, long size);
+
+    AnalyticsUserProfileVo getUserProfile(Long userId);
 }

+ 54 - 1
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsContentReportServiceImpl.java

@@ -6,6 +6,7 @@ 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.AnalyticsReportRecord;
 import shop.alien.entity.analytics.vo.contentreport.*;
 import shop.alien.entity.analytics.vo.userreport.AnalyticsDistributionItemVo;
 import shop.alien.mapper.AnalyticsContentReportMapper;
@@ -29,11 +30,17 @@ public class AnalyticsContentReportServiceImpl implements AnalyticsContentReport
 
     @Override
     public AnalyticsContentReportChartsVo loadAllCharts(String period) {
-        return loadAllCharts(period, 1, 10);
+        return loadAllCharts(period, 1, 10, 1, 10);
     }
 
     @Override
     public AnalyticsContentReportChartsVo loadAllCharts(String period, long detailPage, long detailSize) {
+        return loadAllCharts(period, detailPage, detailSize, 1, 10);
+    }
+
+    @Override
+    public AnalyticsContentReportChartsVo loadAllCharts(String period, long detailPage, long detailSize,
+                                                        long reportPage, long reportSize) {
         AnalyticsContentReportChartsVo vo = new AnalyticsContentReportChartsVo();
         vo.setPeriod(AnalyticsPeriodContext.resolve(period).getPeriod());
         vo.setSummary(summary(period));
@@ -41,6 +48,7 @@ public class AnalyticsContentReportServiceImpl implements AnalyticsContentReport
         vo.setAuditTrend(auditTrend(period));
         vo.setInteractionTop10(interactionTop10(period));
         vo.setAuditPassedPage(pageAuditPassed(period, detailPage, detailSize));
+        vo.setReportPage(pageReportRecords(period, reportPage, reportSize));
         return vo;
     }
 
@@ -68,6 +76,11 @@ public class AnalyticsContentReportServiceImpl implements AnalyticsContentReport
         vo.setAuditPassRate(passRateNow);
         vo.setAuditPassRateDelta(subtractRate(passRateNow, passRatePrev));
         vo.setAuditProcessRate(calcProcessRate(now));
+
+        BigDecimal reportRateNow = calcReportHandleRate(now);
+        BigDecimal reportRatePrev = calcReportHandleRate(prev);
+        vo.setReportHandleRate(reportRateNow);
+        vo.setReportHandleRateDelta(subtractRate(reportRateNow, reportRatePrev));
         return vo;
     }
 
@@ -163,6 +176,34 @@ public class AnalyticsContentReportServiceImpl implements AnalyticsContentReport
         return raw.convert(this::toDetailVo);
     }
 
+    @Override
+    public IPage<AnalyticsContentReportRecordVo> pageReportRecords(String period, long page, long size) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<AnalyticsReportRecord> raw = contentReportMapper.pageReportRecords(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive());
+        return raw.convert(this::toReportVo);
+    }
+
+    private AnalyticsContentReportRecordVo toReportVo(AnalyticsReportRecord record) {
+        AnalyticsContentReportRecordVo vo = new AnalyticsContentReportRecordVo();
+        vo.setReportId(record.getReportId());
+        vo.setDisplayReportId(record.getReportId());
+        vo.setContentId(record.getContentId());
+        vo.setContentTitle(record.getContentTitle());
+        vo.setReportType(record.getReportType());
+        vo.setStatusLabel(resolveReportStatus(record.getStatus()));
+        vo.setReportTime(record.getReportTime());
+        vo.setHandleTime(record.getHandleTime());
+        return vo;
+    }
+
+    private String resolveReportStatus(Integer status) {
+        if (status != null && status == 1) {
+            return "已处理";
+        }
+        return "处理中";
+    }
+
     private AnalyticsContentReportDetailVo toDetailVo(AnalyticsContentStat stat) {
         AnalyticsContentReportDetailVo vo = new AnalyticsContentReportDetailVo();
         vo.setContentId(stat.getContentId());
@@ -202,6 +243,12 @@ public class AnalyticsContentReportServiceImpl implements AnalyticsContentReport
             metrics.put("auditRejectCount", intValue(eventMapper.countByEventCode(
                     AnalyticsEventCode.AUDIT_REJECT, ctx.getStartTime(), ctx.getEndTimeExclusive())));
         }
+        if (intValue(metrics.get("reportSubmitCount")) <= 0) {
+            metrics.put("reportSubmitCount", intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.REPORT_SUBMIT, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+            metrics.put("reportHandleCount", intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.REPORT_HANDLE, ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        }
         return metrics;
     }
 
@@ -234,6 +281,12 @@ public class AnalyticsContentReportServiceImpl implements AnalyticsContentReport
         return calcRatio(processed, submit);
     }
 
+    private BigDecimal calcReportHandleRate(Map<String, Object> metrics) {
+        return AnalyticsDateUtil.calcRate(
+                intValue(metrics.get("reportHandleCount")),
+                intValue(metrics.get("reportSubmitCount")));
+    }
+
     private String resolveContentTypeName(String code) {
         if (code == null) {
             return "其他";

+ 279 - 52
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsDashboardServiceImpl.java

@@ -1,14 +1,21 @@
 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.AnalyticsDailySummary;
 import shop.alien.entity.analytics.AnalyticsEventCode;
+import shop.alien.entity.analytics.AnalyticsStatPeriod;
 import shop.alien.entity.analytics.vo.dashboard.*;
+import shop.alien.mapper.AnalyticsAiRequestMapper;
 import shop.alien.mapper.AnalyticsDashboardMapper;
 import shop.alien.mapper.AnalyticsEventMapper;
+import shop.alien.mapper.AnalyticsFunnelMapper;
+import shop.alien.mapper.AnalyticsUserStatTodayMapper;
 import shop.alien.store.service.analytics.AnalyticsDashboardService;
 import shop.alien.store.util.analytics.AnalyticsDateUtil;
+import shop.alien.store.util.analytics.AnalyticsFunnelStageCodes;
 import shop.alien.store.util.analytics.AnalyticsPeriodContext;
 import shop.alien.store.util.analytics.AnalyticsTrendFillUtil;
 
@@ -26,6 +33,11 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
 
     private final AnalyticsDashboardMapper dashboardMapper;
     private final AnalyticsEventMapper eventMapper;
+    private final AnalyticsFunnelMapper funnelMapper;
+    private final AnalyticsAiRequestMapper aiRequestMapper;
+    private final AnalyticsUserStatTodayMapper userStatTodayMapper;
+
+    private static final int ONLINE_WINDOW_MIN = 5;
 
     @Override
     public AnalyticsDashboardChartsVo loadAllCharts(String period) {
@@ -44,45 +56,256 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
     @Override
     public AnalyticsDashboardSummaryVo summary(String period) {
         AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
-        List<AnalyticsDailySummary> rows = dashboardMapper.listDailySummaryBetween(ctx.getStartDate(), ctx.getEndDate());
+        AnalyticsPeriodContext prevCtx = AnalyticsPeriodContext.buildPreviousPeriod(ctx);
+        String compareLabel = resolveCompareLabel(ctx.getPeriod());
+
+        PeriodMetrics now = loadPeriodMetrics(ctx);
+        PeriodMetrics prev = loadPeriodMetrics(prevCtx);
 
         AnalyticsDashboardSummaryVo vo = new AnalyticsDashboardSummaryVo();
-        int dayCount = dayCount(ctx.getStartDate(), ctx.getEndDate());
+        vo.setPeriod(ctx.getPeriod());
+
+        vo.setActiveUser(buildCountCard("DAU", resolveActiveUserLabel(ctx.getPeriod()),
+                now.activeUsers, prev.activeUsers, compareLabel));
+        vo.setNewUser(buildCountCard("NEW_USER", resolvePeriodPrefix(ctx.getPeriod()) + "新增用户",
+                now.newUsers, prev.newUsers, compareLabel));
+        vo.setAiChat(buildCountCard("AI_CHAT", resolvePeriodPrefix(ctx.getPeriod()) + "对话次数",
+                now.aiChatCount, prev.aiChatCount, compareLabel));
+        vo.setContentPublish(buildCountCard("CONTENT_PUBLISH", resolvePeriodPrefix(ctx.getPeriod()) + "内容发布",
+                now.contentPublishCount, prev.contentPublishCount, compareLabel));
+        vo.setMerchantVisitUvCard(buildCountCard("MERCHANT_UV", resolvePeriodPrefix(ctx.getPeriod()) + "商家访问UV",
+                now.merchantVisitUv, prev.merchantVisitUv, compareLabel));
+        vo.setAiResponse(buildDurationCard(resolvePeriodPrefix(ctx.getPeriod()) + "AI响应时间",
+                now.aiAvgResponseMs, prev.aiAvgResponseMs, compareLabel));
+        vo.setOnlineUser(buildOnlineCard(ctx, now.onlineUsers, prev.onlineUsers, compareLabel));
+        vo.setConversionRateCard(buildRateCard(resolvePeriodPrefix(ctx.getPeriod()) + "转化率",
+                now.conversionRate, prev.conversionRate, compareLabel));
+
+        syncLegacyFields(vo, now);
+        return vo;
+    }
 
-        if (rows.isEmpty()) {
-            fillSummaryFromEvents(vo, ctx);
-            return vo;
+    private PeriodMetrics loadPeriodMetrics(AnalyticsPeriodContext ctx) {
+        PeriodMetrics m = new PeriodMetrics();
+        List<AnalyticsDailySummary> rows = dashboardMapper.listDailySummaryBetween(
+                ctx.getStartDate(), ctx.getEndDate());
+
+        m.activeUsers = intValue(eventMapper.countActiveUsersInRange(
+                ctx.getStartTime(), ctx.getEndTimeExclusive()));
+        m.newUsers = sumInt(rows, AnalyticsDailySummary::getNewUserCount);
+        if (m.newUsers <= 0) {
+            m.newUsers = intValue(eventMapper.countRegisterUsers(ctx.getStartTime(), ctx.getEndTimeExclusive()));
         }
 
-        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);
+        m.aiChatCount = sumInt(rows, AnalyticsDailySummary::getAiChatCount);
+        if (m.aiChatCount <= 0) {
+            m.aiChatCount = intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.AI_CHAT_END, ctx.getStartTime(), ctx.getEndTimeExclusive()));
         }
 
-        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());
-        if (dayCount == 1) {
-            vo.setMerchantVisitUv(defaultInt(rows.isEmpty() ? 0 : rows.get(0).getMerchantVisitUv()));
-        } else {
-            vo.setMerchantVisitUv(intValue(eventMapper.countMerchantVisitUv(ctx.getStartTime(), ctx.getEndTimeExclusive())));
+        m.contentPublishCount = sumInt(rows, AnalyticsDailySummary::getContentPublishCount);
+        if (m.contentPublishCount <= 0) {
+            m.contentPublishCount = intValue(eventMapper.countByEventCode(
+                    AnalyticsEventCode.CONTENT_PUBLISH, ctx.getStartTime(), ctx.getEndTimeExclusive()));
         }
 
+        m.merchantVisitUv = intValue(eventMapper.countMerchantVisitUv(
+                ctx.getStartTime(), ctx.getEndTimeExclusive()));
+
         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 (totalAiReq <= 0) {
+            totalAiMs = defaultLong(aiRequestMapper.sumResponseDuration(ctx.getStartTime(), ctx.getEndTimeExclusive()));
+            totalAiReq = intValue(aiRequestMapper.countRequests(ctx.getStartTime(), ctx.getEndTimeExclusive()));
+        }
+        m.aiAvgResponseMs = totalAiReq > 0 ? totalAiMs / totalAiReq : 0L;
+
+        m.conversionRate = calcPeriodConversionRate(ctx, rows, m.activeUsers);
 
-        if (vo.getDau() == null || vo.getDau() == 0) {
-            fillSummaryFromEvents(vo, ctx);
+        if (isCurrentDayPeriod(ctx)) {
+            m.onlineUsers = countRealtimeOnline();
+        } else if (!rows.isEmpty()) {
+            m.onlineUsers = defaultInt(rows.get(rows.size() - 1).getOnlineUserCount());
         }
-        return vo;
+
+        return m;
+    }
+
+    private AnalyticsKpiCardVo buildCountCard(String code, String label, int value, int previous, String compareLabel) {
+        AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
+        card.setMetricCode(code);
+        card.setLabel(label);
+        card.setValue(BigDecimal.valueOf(value));
+        card.setDelta(BigDecimal.valueOf(value - previous));
+        card.setChangeRate(calcChangeRate(value, previous));
+        card.setCompareLabel(compareLabel);
+        card.setValueType("COUNT");
+        card.setRealTime(false);
+        card.setLowerIsBetter(false);
+        return card;
+    }
+
+    private AnalyticsKpiCardVo buildDurationCard(String label, long value, long previous, String compareLabel) {
+        AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
+        card.setMetricCode("AI_RESPONSE");
+        card.setLabel(label);
+        card.setValue(BigDecimal.valueOf(value));
+        card.setDelta(BigDecimal.valueOf(value - previous));
+        card.setChangeRate(calcChangeRate(value, previous));
+        card.setCompareLabel(compareLabel);
+        card.setValueType("DURATION");
+        card.setUnit("ms");
+        card.setRealTime(false);
+        card.setLowerIsBetter(true);
+        return card;
+    }
+
+    private AnalyticsKpiCardVo buildRateCard(String label, BigDecimal value, BigDecimal previous, String compareLabel) {
+        BigDecimal current = value != null ? value : BigDecimal.ZERO;
+        BigDecimal prev = previous != null ? previous : BigDecimal.ZERO;
+        AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
+        card.setMetricCode("CONVERSION");
+        card.setLabel(label);
+        card.setValue(current);
+        card.setDelta(current.subtract(prev).setScale(2, RoundingMode.HALF_UP));
+        card.setChangeRate(calcChangeRate(current.doubleValue(), prev.doubleValue()));
+        card.setCompareLabel(compareLabel);
+        card.setValueType("RATE");
+        card.setUnit("%");
+        card.setRealTime(false);
+        card.setLowerIsBetter(false);
+        return card;
+    }
+
+    private AnalyticsKpiCardVo buildOnlineCard(AnalyticsPeriodContext ctx, int value, int previous, String compareLabel) {
+        AnalyticsKpiCardVo card = new AnalyticsKpiCardVo();
+        card.setMetricCode("ONLINE");
+        card.setLabel("当前在线");
+        card.setValue(BigDecimal.valueOf(value));
+        card.setValueType("COUNT");
+        card.setUnit(null);
+        if (isCurrentDayPeriod(ctx)) {
+            card.setRealTime(true);
+            card.setDelta(BigDecimal.valueOf(value - previous));
+            card.setChangeRate(calcChangeRate(value, previous));
+            card.setCompareLabel(compareLabel);
+        } else {
+            card.setRealTime(false);
+            card.setDelta(null);
+            card.setChangeRate(null);
+            card.setCompareLabel(null);
+        }
+        card.setLowerIsBetter(false);
+        return card;
+    }
+
+    private void syncLegacyFields(AnalyticsDashboardSummaryVo vo, PeriodMetrics metrics) {
+        vo.setDau(metrics.activeUsers);
+        vo.setNewUserCount(metrics.newUsers);
+        vo.setAiChatCount(metrics.aiChatCount);
+        vo.setContentPublishCount(metrics.contentPublishCount);
+        vo.setMerchantVisitUv(metrics.merchantVisitUv);
+        vo.setAiAvgResponseMs(metrics.aiAvgResponseMs);
+        vo.setOnlineUserCount(metrics.onlineUsers);
+        vo.setConversionRate(metrics.conversionRate);
+    }
+
+    private BigDecimal calcPeriodConversionRate(AnalyticsPeriodContext ctx,
+                                                List<AnalyticsDailySummary> rows,
+                                                int activeUsers) {
+        if (!rows.isEmpty()) {
+            BigDecimal weighted = calcWeightedConversionRate(rows);
+            if (weighted != null && weighted.compareTo(BigDecimal.ZERO) > 0) {
+                return weighted;
+            }
+        }
+        Long payCount = eventMapper.countByEventCode(
+                AnalyticsEventCode.PAY_SUCCESS, ctx.getStartTime(), ctx.getEndTimeExclusive());
+        return AnalyticsDateUtil.calcRate(payCount, activeUsers);
+    }
+
+    private int countRealtimeOnline() {
+        Calendar threshold = Calendar.getInstance();
+        threshold.add(Calendar.MINUTE, -ONLINE_WINDOW_MIN);
+        Long count = userStatTodayMapper.countOnlineSince(threshold.getTime());
+        return count != null ? count.intValue() : 0;
+    }
+
+    private boolean isCurrentDayPeriod(AnalyticsPeriodContext ctx) {
+        Date today = AnalyticsDateUtil.truncateToDate(new Date());
+        return AnalyticsStatPeriod.TODAY.equals(ctx.getPeriod())
+                || (ctx.getEndDate() != null && AnalyticsDateUtil.truncateToDate(ctx.getEndDate()).equals(today)
+                && ctx.getStartDate() != null && AnalyticsDateUtil.truncateToDate(ctx.getStartDate()).equals(today));
+    }
+
+    private String resolvePeriodPrefix(String period) {
+        switch (period) {
+            case AnalyticsStatPeriod.TODAY:
+                return "今日";
+            case AnalyticsStatPeriod.YESTERDAY:
+                return "昨日";
+            case AnalyticsStatPeriod.LAST_7D:
+                return "近7日";
+            case AnalyticsStatPeriod.LAST_30D:
+                return "近30日";
+            default:
+                return "";
+        }
+    }
+
+    private String resolveActiveUserLabel(String period) {
+        switch (period) {
+            case AnalyticsStatPeriod.TODAY:
+                return "今日 DAU";
+            case AnalyticsStatPeriod.YESTERDAY:
+                return "昨日 DAU";
+            case AnalyticsStatPeriod.LAST_7D:
+                return "近7日活跃";
+            case AnalyticsStatPeriod.LAST_30D:
+                return "近30日活跃";
+            default:
+                return "活跃用户";
+        }
+    }
+
+    private String resolveCompareLabel(String period) {
+        switch (period) {
+            case AnalyticsStatPeriod.TODAY:
+                return "较昨日";
+            case AnalyticsStatPeriod.YESTERDAY:
+                return "较前日";
+            default:
+                return "较上期";
+        }
+    }
+
+    private BigDecimal calcChangeRate(double current, double previous) {
+        if (previous <= 0) {
+            return current > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf((current - previous) * 100.0 / previous)
+                .setScale(1, RoundingMode.HALF_UP);
+    }
+
+    private int sumInt(List<AnalyticsDailySummary> rows, java.util.function.Function<AnalyticsDailySummary, Integer> getter) {
+        return rows.stream().mapToInt(s -> defaultInt(getter.apply(s))).sum();
+    }
+
+    private long defaultLong(Long value) {
+        return value != null ? value : 0L;
+    }
+
+    private static final class PeriodMetrics {
+        private int activeUsers;
+        private int newUsers;
+        private int aiChatCount;
+        private int contentPublishCount;
+        private int merchantVisitUv;
+        private long aiAvgResponseMs;
+        private int onlineUsers;
+        private BigDecimal conversionRate = BigDecimal.ZERO;
     }
 
     @Override
@@ -176,6 +399,38 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
     }
 
     @Override
+    public IPage<AnalyticsFunnelDetailVo> pageConversionFunnelDetail(String period, String stageCode, long page, long size) {
+        List<String> eventCodes = AnalyticsFunnelStageCodes.merchantStageEvents(stageCode);
+        if (eventCodes.isEmpty()) {
+            throw new IllegalArgumentException("未知漏斗阶段: " + stageCode);
+        }
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<Map<String, Object>> raw = funnelMapper.pageFunnelUsers(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive(), eventCodes);
+        return raw.convert(this::toFunnelDetailVo);
+    }
+
+    private AnalyticsFunnelDetailVo toFunnelDetailVo(Map<String, Object> row) {
+        AnalyticsFunnelDetailVo vo = new AnalyticsFunnelDetailVo();
+        Long userId = longValue(row.get("userId"));
+        vo.setUserId(userId);
+        vo.setDisplayUserId(userId != null ? "U" + userId : null);
+        Long merchantId = longValue(row.get("merchantId"));
+        if (merchantId != null && merchantId > 0) {
+            vo.setMerchantId(merchantId);
+            vo.setDisplayMerchantId("M" + merchantId);
+        }
+        vo.setCity(stringValue(row.get("city")));
+        vo.setDeviceType(stringValue(row.get("deviceType")));
+        vo.setChannel(stringValue(row.get("channel")));
+        Object eventTime = row.get("eventTime");
+        if (eventTime instanceof Date) {
+            vo.setEventTime((Date) eventTime);
+        }
+        return vo;
+    }
+
+    @Override
     public List<AnalyticsRetentionPointVo> userRetention(String period) {
         AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
         Date cohortStart = AnalyticsDateUtil.dayStart(ctx.getStartDate());
@@ -342,30 +597,6 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
                 });
     }
 
-    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_END, ctx.getStartTime(), ctx.getEndTimeExclusive())));
-        vo.setContentPublishCount(intValue(eventMapper.countByEventCode(
-                AnalyticsEventCode.CONTENT_PUBLISH, ctx.getStartTime(), ctx.getEndTimeExclusive())));
-        vo.setMerchantVisitUv(intValue(eventMapper.countMerchantVisitUv(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;
@@ -381,10 +612,6 @@ public class AnalyticsDashboardServiceImpl implements AnalyticsDashboardService
         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);

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

@@ -41,6 +41,7 @@ public class AnalyticsMerchantReportServiceImpl implements AnalyticsMerchantRepo
         vo.setGmvTrend(gmvTrend(period));
         vo.setGmvCategoryDistribution(gmvCategoryDistribution(period));
         vo.setAvgOrderTrend(avgOrderTrend(period));
+        vo.setReviewRateTrend(reviewRateTrend(period));
         vo.setGmvTop10(gmvTop10(period));
         return vo;
     }
@@ -174,6 +175,32 @@ public class AnalyticsMerchantReportServiceImpl implements AnalyticsMerchantRepo
     }
 
     @Override
+    public List<AnalyticsReviewRateTrendVo> reviewRateTrend(String period) {
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        List<AnalyticsReviewRateTrendVo> existing = merchantReportMapper.listReviewRateTrend(
+                ctx.getStartDate(), ctx.getEndDate()).stream().map(row -> {
+            AnalyticsReviewRateTrendVo vo = new AnalyticsReviewRateTrendVo();
+            Object statDate = row.get("statDate");
+            if (statDate instanceof Date) {
+                vo.setStatDate((Date) statDate);
+            }
+            vo.setLabel(stringValue(row.get("label")));
+            vo.setReviewRate(toBigDecimal(row.get("reviewRate")));
+            return vo;
+        }).collect(Collectors.toList());
+        Map<Date, AnalyticsReviewRateTrendVo> 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,
+                AnalyticsReviewRateTrendVo::new,
+                (vo, date) -> vo.setReviewRate(BigDecimal.ZERO));
+    }
+
+    @Override
     public IPage<AnalyticsMerchantReportDetailVo> pageSettledMerchant(String period, long page, long size) {
         AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
         IPage<AnalyticsMerchantStat> raw = merchantReportMapper.pageSettledMerchantDetail(
@@ -181,6 +208,47 @@ public class AnalyticsMerchantReportServiceImpl implements AnalyticsMerchantRepo
         return raw.convert(this::toDetailVo);
     }
 
+    @Override
+    public AnalyticsMerchantProfileVo getMerchantProfile(String period, Long merchantId) {
+        if (merchantId == null) {
+            return null;
+        }
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        AnalyticsMerchantProfileVo vo = new AnalyticsMerchantProfileVo();
+        vo.setMerchantId(merchantId);
+        vo.setDisplayMerchantId("M" + merchantId);
+
+        StoreInfo store = storeInfoMapper.selectById(merchantId.intValue());
+        if (store != null) {
+            vo.setMerchantName(store.getStoreName());
+            vo.setCategoryName(firstNonBlank(store.getBusinessCategoryName(), store.getBusinessTypeName()));
+        }
+        vo.setStatus(resolveMerchantStatus(merchantId));
+
+        List<Map<String, Object>> rows = merchantReportMapper.listMerchantGmvTop10(ctx.getStartDate(), ctx.getEndDate());
+        for (Map<String, Object> row : rows) {
+            if (merchantId.equals(longValue(row.get("merchantId")))) {
+                vo.setPeriodGmv(toBigDecimal(row.get("gmv")));
+                break;
+            }
+        }
+        if (vo.getPeriodGmv() == null) {
+            vo.setPeriodGmv(BigDecimal.ZERO);
+        }
+        vo.setVerifyRate(calcPeriodVerifyRate(ctx));
+        Long reviewCount = eventMapper.countByEventCode(
+                AnalyticsEventCode.MERCHANT_REVIEW, ctx.getStartTime(), ctx.getEndTimeExclusive());
+        Long verifyCount = eventMapper.countMerchantVerify(ctx.getStartTime(), ctx.getEndTimeExclusive());
+        vo.setReviewRate(AnalyticsDateUtil.calcRate(reviewCount, verifyCount));
+
+        if (store != null && store.getReviewDate() != null) {
+            vo.setSettleTime(store.getReviewDate());
+        } else if (store != null) {
+            vo.setSettleTime(store.getCreatedTime());
+        }
+        return vo;
+    }
+
     private AnalyticsMerchantReportDetailVo toDetailVo(AnalyticsMerchantStat stat) {
         AnalyticsMerchantReportDetailVo vo = new AnalyticsMerchantReportDetailVo();
         vo.setMerchantId(stat.getMerchantId());

+ 5 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsStatisticsServiceImpl.java

@@ -178,12 +178,15 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
             stat.setPayCount(toInt(row.get("payCount")));
             stat.setPayUserCount(toInt(row.get("payUserCount")));
             stat.setGmv(toBigDecimal(row.get("gmv")));
+            int reviewCount = toInt(row.get("reviewCount"));
+            stat.setReviewCount(reviewCount);
             if (row.get("shopType") != null) {
                 stat.setShopType(toInt(row.get("shopType")));
             }
             int verifyCount = toInt(row.get("verifyCount"));
             int visitUserCount = toInt(row.get("visitUserCount"));
             stat.setVerifyConversionRate(AnalyticsDateUtil.calcRate(verifyCount, visitUserCount));
+            stat.setReviewRate(AnalyticsDateUtil.calcRate(reviewCount, verifyCount));
 
             if (stat.getId() == null) {
                 detailStoreService.insertMerchantStat(statDate, stat);
@@ -385,6 +388,8 @@ public class AnalyticsStatisticsServiceImpl implements AnalyticsStatisticsServic
         Long verifyCount = eventMapper.countMerchantVerify(start, end);
         Long visitUsers = eventMapper.countMerchantViewUsers(start, end);
         summary.setVerifyRate(AnalyticsDateUtil.calcRate(verifyCount, visitUsers));
+        Long reviewCount = eventMapper.countByEventCode(AnalyticsEventCode.MERCHANT_REVIEW, start, end);
+        summary.setMerchantReviewRate(AnalyticsDateUtil.calcRate(reviewCount, verifyCount));
         summary.setTotalSettledMerchantCount(toInt(merchantStatHistoryMapper.countSettledMerchants(statDate)));
 
         Long paySuccessCount = eventMapper.countByEventCode(AnalyticsEventCode.PAY_SUCCESS, start, end);

+ 104 - 0
alien-store/src/main/java/shop/alien/store/service/analytics/impl/AnalyticsTrackServiceImpl.java

@@ -27,6 +27,7 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
 
     private final AnalyticsEventMapper eventMapper;
     private final AnalyticsAiRequestMapper aiRequestMapper;
+    private final AnalyticsReportRecordMapper reportRecordMapper;
     private final AnalyticsDetailStoreService detailStoreService;
     private final AnalyticsAiChatStatMapper aiChatStatMapper;
 
@@ -590,6 +591,109 @@ public class AnalyticsTrackServiceImpl implements AnalyticsTrackService {
                 addContentInteraction(dto.getContentType(), dto.getTargetId(), increment, dto.getEventSubtype());
             }
         }
+        syncReportFromFront(dto, eventCode);
+    }
+
+    private void syncReportFromFront(AnalyticsFrontReportDTO dto, String eventCode) {
+        if (AnalyticsEventCode.REPORT_SUBMIT.equals(eventCode)) {
+            upsertReportSubmit(dto);
+        } else if (AnalyticsEventCode.REPORT_HANDLE.equals(eventCode)) {
+            upsertReportHandle(dto);
+        }
+    }
+
+    private void upsertReportSubmit(AnalyticsFrontReportDTO dto) {
+        String reportId = resolveReportId(dto);
+        if (!StringUtils.hasText(reportId)) {
+            return;
+        }
+        Date reportTime = dto.getEventTime() != null ? dto.getEventTime() : new Date();
+        LambdaQueryWrapper<AnalyticsReportRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsReportRecord::getReportId, reportId);
+        AnalyticsReportRecord existing = reportRecordMapper.selectOne(wrapper);
+        if (existing != null) {
+            return;
+        }
+        AnalyticsReportRecord record = new AnalyticsReportRecord();
+        record.setReportId(reportId);
+        record.setContentId(dto.getTargetId());
+        record.setContentType(dto.getContentType());
+        record.setContentTitle(dto.getContentTitle());
+        record.setReportType(resolveReportType(dto));
+        record.setStatus(0);
+        record.setReporterUserId(dto.getUserId());
+        record.setReportTime(reportTime);
+        try {
+            reportRecordMapper.insert(record);
+        } catch (DuplicateKeyException e) {
+            log.debug("举报记录已存在: reportId={}", reportId);
+        }
+    }
+
+    private void upsertReportHandle(AnalyticsFrontReportDTO dto) {
+        String reportId = resolveHandledReportId(dto);
+        if (!StringUtils.hasText(reportId)) {
+            return;
+        }
+        Date handleTime = dto.getEventTime() != null ? dto.getEventTime() : new Date();
+        LambdaQueryWrapper<AnalyticsReportRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(AnalyticsReportRecord::getReportId, reportId);
+        AnalyticsReportRecord record = reportRecordMapper.selectOne(wrapper);
+        if (record == null) {
+            record = new AnalyticsReportRecord();
+            record.setReportId(reportId);
+            record.setContentId(dto.getTargetId());
+            record.setContentType(dto.getContentType());
+            record.setContentTitle(dto.getContentTitle());
+            record.setReportType(resolveReportType(dto));
+            record.setReporterUserId(dto.getUserId());
+            record.setReportTime(handleTime);
+        }
+        record.setStatus(1);
+        record.setHandleTime(handleTime);
+        record.setHandleUserId(dto.getOperatorId());
+        if (record.getId() == null) {
+            try {
+                reportRecordMapper.insert(record);
+            } catch (DuplicateKeyException e) {
+                AnalyticsReportRecord again = reportRecordMapper.selectOne(wrapper);
+                if (again != null) {
+                    again.setStatus(1);
+                    again.setHandleTime(handleTime);
+                    again.setHandleUserId(dto.getOperatorId());
+                    reportRecordMapper.updateById(again);
+                }
+            }
+        } else {
+            reportRecordMapper.updateById(record);
+        }
+    }
+
+    private String resolveReportId(AnalyticsFrontReportDTO dto) {
+        if (StringUtils.hasText(dto.getEventId())) {
+            return dto.getEventId();
+        }
+        return null;
+    }
+
+    private String resolveHandledReportId(AnalyticsFrontReportDTO dto) {
+        if (StringUtils.hasText(dto.getEventSubtype())) {
+            return dto.getEventSubtype();
+        }
+        if (StringUtils.hasText(dto.getEventId())) {
+            return dto.getEventId();
+        }
+        return null;
+    }
+
+    private String resolveReportType(AnalyticsFrontReportDTO dto) {
+        if (StringUtils.hasText(dto.getReportType())) {
+            return dto.getReportType();
+        }
+        if (StringUtils.hasText(dto.getEventSubtype())) {
+            return dto.getEventSubtype();
+        }
+        return "其他";
     }
 
     private void syncContentAuditFromFront(AnalyticsFrontReportDTO dto, String eventCode) {

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

@@ -8,15 +8,18 @@ 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.AnalyticsFunnelDetailVo;
 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.AnalyticsFunnelMapper;
 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.AnalyticsFunnelStageCodes;
 import shop.alien.store.util.analytics.AnalyticsPeriodContext;
 
 import java.math.BigDecimal;
@@ -35,6 +38,7 @@ public class AnalyticsUserReportServiceImpl implements AnalyticsUserReportServic
     private final AnalyticsUserReportMapper userReportMapper;
     private final AnalyticsDashboardMapper dashboardMapper;
     private final AnalyticsEventMapper eventMapper;
+    private final AnalyticsFunnelMapper funnelMapper;
     private final LifeUserMapper lifeUserMapper;
 
     @Override
@@ -73,6 +77,11 @@ public class AnalyticsUserReportServiceImpl implements AnalyticsUserReportServic
         vo.setLast7dNewUserCount(new7Now);
         vo.setLast7dNewUserDelta(new7Now - new7Prev);
 
+        int active7Now = countActiveUsersInWindow(anchorEnd, 6);
+        int active7Prev = countActiveUsersInWindow(prevEnd, 6);
+        vo.setLast7dActiveUserCount(active7Now);
+        vo.setLast7dActiveUserDelta(active7Now - active7Prev);
+
         int active30Now = countActiveUsersInWindow(anchorEnd, 29);
         int active30Prev = countActiveUsersInWindow(prevEnd, 29);
         vo.setLast30dActiveUserCount(active30Now);
@@ -163,6 +172,81 @@ public class AnalyticsUserReportServiceImpl implements AnalyticsUserReportServic
         return raw.convert(this::toDetailVo);
     }
 
+    @Override
+    public IPage<AnalyticsFunnelDetailVo> pageRegisterFunnelDetail(String period, String stageCode, long page, long size) {
+        List<String> eventCodes = AnalyticsFunnelStageCodes.registerStageEvents(stageCode);
+        if (eventCodes.isEmpty()) {
+            throw new IllegalArgumentException("未知漏斗阶段: " + stageCode);
+        }
+        AnalyticsPeriodContext ctx = AnalyticsPeriodContext.resolve(period);
+        IPage<Map<String, Object>> raw = funnelMapper.pageFunnelUsers(
+                new Page<>(page, size), ctx.getStartTime(), ctx.getEndTimeExclusive(), eventCodes);
+        return raw.convert(this::toFunnelDetailVo);
+    }
+
+    @Override
+    public AnalyticsUserProfileVo getUserProfile(Long userId) {
+        if (userId == null) {
+            return null;
+        }
+        AnalyticsUserProfileVo vo = new AnalyticsUserProfileVo();
+        vo.setUserId(userId);
+        vo.setDisplayUserId("U" + userId);
+
+        LifeUser user = lifeUserMapper.selectById(userId.intValue());
+        if (user != null) {
+            vo.setMaskedPhone(maskPhone(user.getUserPhone()));
+            vo.setRegisterTime(user.getCreatedTime());
+            vo.setCity(user.getCity());
+            vo.setGenderLabel(user.getUserSex());
+            vo.setStatus(resolveUserStatus(userId, user.getUserPhone()));
+        }
+
+        AnalyticsUserStat stat = userReportMapper.findLatestUserStat(userId);
+        if (stat != null) {
+            if (stat.getRegisterTime() != null) {
+                vo.setRegisterTime(stat.getRegisterTime());
+            }
+            vo.setLastActiveTime(stat.getLastActiveTime());
+            if (!StringUtils.hasText(vo.getCity())) {
+                vo.setCity(stat.getCity());
+            }
+            if (!StringUtils.hasText(vo.getChannel())) {
+                vo.setChannel(stat.getChannel());
+            }
+            if (!StringUtils.hasText(vo.getMaskedPhone())) {
+                vo.setMaskedPhone(maskPhone(stat.getUserPhone()));
+            }
+            if (!StringUtils.hasText(vo.getGenderLabel())) {
+                vo.setGenderLabel(resolveGenderName(stat.getGender()));
+            }
+            if (!StringUtils.hasText(vo.getAgeGroup())) {
+                vo.setAgeGroup(stat.getAgeGroup());
+            }
+        }
+        return vo;
+    }
+
+    private AnalyticsFunnelDetailVo toFunnelDetailVo(Map<String, Object> row) {
+        AnalyticsFunnelDetailVo vo = new AnalyticsFunnelDetailVo();
+        Long userId = longValue(row.get("userId"));
+        vo.setUserId(userId);
+        vo.setDisplayUserId(userId != null ? "U" + userId : null);
+        Long merchantId = longValue(row.get("merchantId"));
+        if (merchantId != null && merchantId > 0) {
+            vo.setMerchantId(merchantId);
+            vo.setDisplayMerchantId("M" + merchantId);
+        }
+        vo.setCity(stringValue(row.get("city")));
+        vo.setDeviceType(stringValue(row.get("deviceType")));
+        vo.setChannel(stringValue(row.get("channel")));
+        Object eventTime = row.get("eventTime");
+        if (eventTime instanceof Date) {
+            vo.setEventTime((Date) eventTime);
+        }
+        return vo;
+    }
+
     private AnalyticsUserReportDetailVo toDetailVo(AnalyticsUserStat stat) {
         AnalyticsUserReportDetailVo vo = new AnalyticsUserReportDetailVo();
         vo.setUserId(stat.getUserId());
@@ -313,4 +397,8 @@ public class AnalyticsUserReportServiceImpl implements AnalyticsUserReportServic
         }
         return 0L;
     }
+
+    private String stringValue(Object value) {
+        return value != null ? String.valueOf(value) : "";
+    }
 }

+ 84 - 0
alien-store/src/main/java/shop/alien/store/util/analytics/AnalyticsFunnelStageCodes.java

@@ -0,0 +1,84 @@
+package shop.alien.store.util.analytics;
+
+import shop.alien.entity.analytics.AnalyticsEventCode;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 漏斗阶段编码 → 埋点事件编码
+ */
+public final class AnalyticsFunnelStageCodes {
+
+    private AnalyticsFunnelStageCodes() {
+    }
+
+    public static List<String> merchantStageEvents(String stageCode) {
+        if (stageCode == null) {
+            return Collections.emptyList();
+        }
+        switch (stageCode.toUpperCase()) {
+            case "EXPOSE":
+                return Collections.singletonList(AnalyticsEventCode.MERCHANT_EXPOSE);
+            case "CLICK":
+                return Collections.singletonList(AnalyticsEventCode.MERCHANT_CLICK);
+            case "DETAIL":
+                return Arrays.asList(AnalyticsEventCode.MERCHANT_DETAIL, AnalyticsEventCode.MERCHANT_VIEW);
+            case "CONTACT":
+                return Collections.singletonList(AnalyticsEventCode.MERCHANT_CONTACT);
+            case "STORE_VISIT":
+                return Collections.singletonList(AnalyticsEventCode.MERCHANT_VERIFY);
+            default:
+                return Collections.emptyList();
+        }
+    }
+
+    public static List<String> registerStageEvents(String stageCode) {
+        if (stageCode == null) {
+            return Collections.emptyList();
+        }
+        switch (stageCode.toUpperCase()) {
+            case "REGISTER_PAGE":
+                return Collections.singletonList(AnalyticsEventCode.USER_REGISTER_PAGE);
+            case "PHONE_SUBMIT":
+                return Collections.singletonList(AnalyticsEventCode.USER_REGISTER_PHONE);
+            case "OTP_PASS":
+                return Collections.singletonList(AnalyticsEventCode.USER_REGISTER_OTP);
+            case "PASSWORD_SET":
+                return Collections.singletonList(AnalyticsEventCode.USER_REGISTER_PASSWORD);
+            case "REGISTER_SUCCESS":
+                return Collections.singletonList(AnalyticsEventCode.USER_REGISTER);
+            default:
+                return Collections.emptyList();
+        }
+    }
+
+    public static String merchantStageName(String stageCode) {
+        if (stageCode == null) {
+            return "";
+        }
+        switch (stageCode.toUpperCase()) {
+            case "EXPOSE": return "曝光";
+            case "CLICK": return "点击";
+            case "DETAIL": return "详情页";
+            case "CONTACT": return "电话/导航";
+            case "STORE_VISIT": return "到店";
+            default: return stageCode;
+        }
+    }
+
+    public static String registerStageName(String stageCode) {
+        if (stageCode == null) {
+            return "";
+        }
+        switch (stageCode.toUpperCase()) {
+            case "REGISTER_PAGE": return "进入注册页";
+            case "PHONE_SUBMIT": return "提交手机号";
+            case "OTP_PASS": return "验证码通过";
+            case "PASSWORD_SET": return "设置密码";
+            case "REGISTER_SUCCESS": return "注册成功";
+            default: return stageCode;
+        }
+    }
+}

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

@@ -69,6 +69,22 @@ public class AnalyticsPeriodContext {
         return new AnalyticsPeriodContext(normalized, startDate, endDate, startTime, endTimeExclusive, hourly, cohortDate);
     }
 
+    /** 等长上一对比周期(如今日对昨日、近7日对前7日) */
+    public static AnalyticsPeriodContext buildPreviousPeriod(AnalyticsPeriodContext ctx) {
+        long days = dayCount(ctx.getStartDate(), ctx.getEndDate());
+        Date prevEnd = AnalyticsDateUtil.addDays(ctx.getStartDate(), -1);
+        Date prevStart = AnalyticsDateUtil.addDays(prevEnd, -(int) (days - 1));
+        Date prevStartTime = AnalyticsDateUtil.dayStart(prevStart);
+        Date prevEndTimeExclusive = AnalyticsDateUtil.dayEndExclusive(prevEnd);
+        boolean hourly = days == 1 && ctx.isHourly();
+        return new AnalyticsPeriodContext(
+                ctx.getPeriod(), prevStart, prevEnd, prevStartTime, prevEndTimeExclusive, hourly, prevStart);
+    }
+
+    private static int dayCount(Date start, Date end) {
+        return (int) ((AnalyticsDateUtil.dayStart(end).getTime() - AnalyticsDateUtil.dayStart(start).getTime()) / 86400000L) + 1;
+    }
+
     /** 上一对比周期的结束日(用于环比) */
     public Date previousEndDate() {
         switch (period) {