Просмотр исходного кода

推送列表 推送漏斗 报表中心统计接口

qinxuyang 13 часов назад
Родитель
Сommit
53ef61c60a

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

@@ -117,7 +117,7 @@ public class CommonPushTask implements Serializable {
 
     @ApiModelProperty("任务状态(状态机)")
     @TableField("status")
-    private String status;
+    private Integer status;
 
     @ApiModelProperty("运营内部备注")
     @TableField("remark")

+ 36 - 0
alien-entity/src/main/java/shop/alien/entity/store/dto/CommonPushTaskStatsDto.java

@@ -0,0 +1,36 @@
+package shop.alien.entity.store.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 推送任务统计聚合原始数据
+ */
+@Data
+public class CommonPushTaskStatsDto {
+
+    private Long id;
+
+    private String taskNo;
+
+    private String title;
+
+    private Integer pushType;
+
+    private String status;
+
+    private String channels;
+
+    private Integer estimatedCount;
+
+    private Date createdTime;
+
+    private Long sentCount;
+
+    private Long deliveredCount;
+
+    private Long clickCount;
+
+    private Long showCount;
+}

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

@@ -0,0 +1,32 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 推送漏斗单步数据
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "CommonPushFunnelStepVo", description = "推送漏斗单步数据")
+public class CommonPushFunnelStepVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("步骤编码:planned/actual/arrived/show/click")
+    private String stepCode;
+
+    @ApiModelProperty("步骤名称")
+    private String stepName;
+
+    @ApiModelProperty("数量")
+    private Long count;
+
+    @ApiModelProperty("相对上一步转化率(%)")
+    private BigDecimal conversionRate;
+}

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonPushFunnelVo.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 推送漏斗数据
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "CommonPushFunnelVo", description = "推送漏斗数据")
+public class CommonPushFunnelVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("推送任务ID,为空表示汇总")
+    private Long pushTaskId;
+
+    @ApiModelProperty("漏斗各步骤")
+    private List<CommonPushFunnelStepVo> steps;
+}

+ 38 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonPushReportTopItemVo.java

@@ -0,0 +1,38 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 报表中心 Top 推送项
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "CommonPushReportTopItemVo", description = "报表中心Top推送项")
+public class CommonPushReportTopItemVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("排名")
+    private Integer rank;
+
+    @ApiModelProperty("推送任务ID")
+    private Long pushTaskId;
+
+    @ApiModelProperty("推送标题")
+    private String title;
+
+    @ApiModelProperty("点击率(%)")
+    private BigDecimal clickRate;
+
+    @ApiModelProperty("送达数")
+    private Long deliveredCount;
+
+    @ApiModelProperty("点击数")
+    private Long clickCount;
+}

+ 46 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonPushReportVo.java

@@ -0,0 +1,46 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 报表中心数据
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "CommonPushReportVo", description = "报表中心数据")
+public class CommonPushReportVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("报表类型:1-日报 2-周报 3-月报")
+    private Integer reportType;
+
+    @ApiModelProperty("报表日期")
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
+    private Date reportDate;
+
+    @ApiModelProperty("报表标题,如:推送日报 — 2026-06-02 (周一)")
+    private String reportTitle;
+
+    @ApiModelProperty("统计开始时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date startTime;
+
+    @ApiModelProperty("统计结束时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date endTime;
+
+    @ApiModelProperty("核心指标概览")
+    private CommonPushStatisticsSummaryVo summary;
+
+    @ApiModelProperty("Top5推送(按点击率排序)")
+    private List<CommonPushReportTopItemVo> topList;
+}

+ 38 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonPushStatisticsSummaryVo.java

@@ -0,0 +1,38 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+
+/**
+ * 推送统计概览指标
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "CommonPushStatisticsSummaryVo", description = "推送统计概览指标")
+public class CommonPushStatisticsSummaryVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("推送量(status=0或1)")
+    private Long pushVolume;
+
+    @ApiModelProperty("送达数(status=1)")
+    private Long deliveredCount;
+
+    @ApiModelProperty("点击数(user_add=1)")
+    private Long clickCount;
+
+    @ApiModelProperty("送达率(送达数/推送量,%)")
+    private BigDecimal deliveryRate;
+
+    @ApiModelProperty("点击率(点击数/送达数,%)")
+    private BigDecimal clickRate;
+
+    @ApiModelProperty("转化率(点击数/推送量,%)")
+    private BigDecimal conversionRate;
+}

+ 65 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonPushTaskStatisticsItemVo.java

@@ -0,0 +1,65 @@
+package shop.alien.entity.store.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 推送任务统计列表项
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "CommonPushTaskStatisticsItemVo", description = "推送任务统计列表项")
+public class CommonPushTaskStatisticsItemVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("任务ID")
+    private Long id;
+
+    @ApiModelProperty("任务编号")
+    private String taskNo;
+
+    @ApiModelProperty("推送标题")
+    private String title;
+
+    @ApiModelProperty("推送类型:1-交易类 2-系统类 3-运营类 4-社交类")
+    private Integer pushType;
+
+    @ApiModelProperty("任务状态")
+    private String status;
+
+    @ApiModelProperty("所选渠道")
+    private String channels;
+
+    @ApiModelProperty("目标用户数")
+    private Integer estimatedCount;
+
+    @ApiModelProperty("实际发送数(status=0或1)")
+    private Long sentCount;
+
+    @ApiModelProperty("送达数(status=1)")
+    private Long deliveredCount;
+
+    @ApiModelProperty("点击数(user_add=1)")
+    private Long clickCount;
+
+    @ApiModelProperty("推送率(实际发送数/目标用户数,%)")
+    private BigDecimal pushRate;
+
+    @ApiModelProperty("送达率(送达数/实际发送数,%)")
+    private BigDecimal deliveryRate;
+
+    @ApiModelProperty("点击率(点击数/送达数,%)")
+    private BigDecimal clickRate;
+
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private Date createdTime;
+}

+ 26 - 0
alien-entity/src/main/java/shop/alien/entity/store/vo/CommonPushTaskStatisticsPageVo.java

@@ -0,0 +1,26 @@
+package shop.alien.entity.store.vo;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 推送任务统计分页列表
+ */
+@Data
+@JsonInclude
+@ApiModel(value = "CommonPushTaskStatisticsPageVo", description = "推送任务统计分页列表")
+public class CommonPushTaskStatisticsPageVo implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty("概览指标")
+    private CommonPushStatisticsSummaryVo summary;
+
+    @ApiModelProperty("分页列表")
+    private IPage<CommonPushTaskStatisticsItemVo> page;
+}

+ 88 - 0
alien-entity/src/main/java/shop/alien/mapper/CommonPushTaskMapper.java

@@ -1,7 +1,95 @@
 package shop.alien.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+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 org.apache.ibatis.annotations.Select;
 import shop.alien.entity.store.CommonPushTask;
+import shop.alien.entity.store.dto.CommonPushTaskStatsDto;
 
+import java.util.Date;
+import java.util.List;
+
+@Mapper
 public interface CommonPushTaskMapper extends BaseMapper<CommonPushTask> {
+
+    String TASK_STATS_SELECT = "SELECT t.id, t.task_no AS taskNo, t.title, t.push_type AS pushType, t.status, " +
+            "t.channels, t.estimated_count AS estimatedCount, t.created_time AS createdTime, " +
+            "IFNULL(SUM(CASE WHEN u.status IN (2, 3) THEN 1 ELSE 0 END), 0) AS sentCount, " +
+            "IFNULL(SUM(CASE WHEN u.status = 3 THEN 1 ELSE 0 END), 0) AS deliveredCount, " +
+            "IFNULL(SUM(CASE WHEN u.show_info = 1 THEN 1 ELSE 0 END), 0) AS showCount, " +
+            "IFNULL(SUM(CASE WHEN u.user_add = 1 THEN 1 ELSE 0 END), 0) AS clickCount ";
+
+    String TASK_STATS_FROM = "FROM common_push_task t " +
+            "LEFT JOIN common_push_task_user u ON u.push_task_id = t.id AND u.delete_flag = 0 ";
+
+    String TASK_STATS_WHERE = "WHERE t.delete_flag = 0 " +
+            "<if test='taskNo != null and taskNo != \"\"'>AND t.task_no = #{taskNo} </if>" +
+            "<if test='keyword != null and keyword != \"\"'>AND (t.title LIKE CONCAT('%', #{keyword}, '%') OR t.task_no LIKE CONCAT('%', #{keyword}, '%')) </if>" +
+            "<if test='status != null and status != \"\"'>AND t.status = #{status} </if>" +
+            "<if test='pushType != null'>AND t.push_type = #{pushType} </if>" +
+            "<if test='channel != null and channel != \"\"'>AND t.channels LIKE CONCAT('%', #{channel}, '%') </if>" +
+            "<if test='startTime != null'>AND t.created_time &gt;= #{startTime} </if>" +
+            "<if test='endTime != null'>AND t.created_time &lt; #{endTime} </if>";
+
+    @Select("<script>" + TASK_STATS_SELECT + TASK_STATS_FROM + TASK_STATS_WHERE +
+            "GROUP BY t.id, t.task_no, t.title, t.push_type, t.status, t.channels, t.estimated_count, t.created_time " +
+            "ORDER BY t.created_time DESC" +
+            "</script>")
+    IPage<CommonPushTaskStatsDto> selectStatisticsPage(Page<CommonPushTaskStatsDto> page,
+                                                       @Param("taskNo") String taskNo,
+                                                       @Param("keyword") String keyword,
+                                                       @Param("status") String status,
+                                                       @Param("pushType") Integer pushType,
+                                                       @Param("channel") String channel,
+                                                       @Param("startTime") Date startTime,
+                                                       @Param("endTime") Date endTime);
+
+    @Select("<script>" + TASK_STATS_SELECT + TASK_STATS_FROM + TASK_STATS_WHERE +
+            "GROUP BY t.id, t.task_no, t.title, t.push_type, t.status, t.channels, t.estimated_count, t.created_time " +
+            "</script>")
+    List<CommonPushTaskStatsDto> selectStatisticsList(@Param("taskNo") String taskNo,
+                                                      @Param("keyword") String keyword,
+                                                      @Param("status") String status,
+                                                      @Param("pushType") Integer pushType,
+                                                      @Param("channel") String channel,
+                                                      @Param("startTime") Date startTime,
+                                                      @Param("endTime") Date endTime);
+
+    @Select("<script>" +
+            "SELECT " +
+            "IFNULL(SUM(CASE WHEN u.status IN (2, 3) THEN 1 ELSE 0 END), 0) AS sentCount, " +
+            "IFNULL(SUM(CASE WHEN u.status = 3 THEN 1 ELSE 0 END), 0) AS deliveredCount, " +
+            "IFNULL(SUM(CASE WHEN u.show_info = 1 THEN 1 ELSE 0 END), 0) AS showCount, " +
+            "IFNULL(SUM(CASE WHEN u.user_add = 1 THEN 1 ELSE 0 END), 0) AS clickCount " +
+            "FROM common_push_task_user u " +
+            "WHERE u.delete_flag = 0 " +
+            "<if test='pushTaskId != null'>AND u.push_task_id = #{pushTaskId} </if>" +
+            "<if test='startTime != null'>AND u.created_time &gt;= #{startTime} </if>" +
+            "<if test='endTime != null'>AND u.created_time &lt; #{endTime} </if>" +
+            "</script>")
+    CommonPushTaskStatsDto selectUserStats(@Param("pushTaskId") Long pushTaskId,
+                                           @Param("startTime") Date startTime,
+                                           @Param("endTime") Date endTime);
+
+    @Select("<script>" +
+            "SELECT t.id, t.title, " +
+            "IFNULL(SUM(CASE WHEN u.status IN (2, 3) THEN 1 ELSE 0 END), 0) AS sentCount, " +
+            "IFNULL(SUM(CASE WHEN u.status = 3 THEN 1 ELSE 0 END), 0) AS deliveredCount, " +
+            "IFNULL(SUM(CASE WHEN u.user_add = 1 THEN 1 ELSE 0 END), 0) AS clickCount " +
+            "FROM common_push_task t " +
+            "INNER JOIN common_push_task_user u ON u.push_task_id = t.id AND u.delete_flag = 0 " +
+            "WHERE t.delete_flag = 0 " +
+            "<if test='startTime != null'>AND u.created_time &gt;= #{startTime} </if>" +
+            "<if test='endTime != null'>AND u.created_time &lt; #{endTime} </if>" +
+            "GROUP BY t.id, t.title " +
+            "HAVING deliveredCount &gt; 0 " +
+            "ORDER BY (clickCount / deliveredCount) DESC " +
+            "LIMIT #{limit}" +
+            "</script>")
+    List<CommonPushTaskStatsDto> selectTopByClickRate(@Param("startTime") Date startTime,
+                                                      @Param("endTime") Date endTime,
+                                                      @Param("limit") Integer limit);
 }

+ 60 - 0
alien-store/src/main/java/shop/alien/store/controller/CommonPushTaskController.java

@@ -11,8 +11,13 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonPushTask;
+import shop.alien.entity.store.vo.CommonPushFunnelVo;
+import shop.alien.entity.store.vo.CommonPushReportVo;
+import shop.alien.entity.store.vo.CommonPushTaskStatisticsPageVo;
 import shop.alien.store.service.CommonPushTaskService;
 
+import java.util.Date;
+
 @Api(tags = {"推送任务管理"})
 @Slf4j
 @RestController
@@ -77,4 +82,59 @@ public class CommonPushTaskController {
             @RequestParam(required = false) Integer pushType) {
         return commonPushTaskService.list(pageNum, pageSize, taskNo, title, status, pushType);
     }
+
+    @ApiOperation("推送统计列表(含概览指标)")
+    @ApiOperationSupport(order = 6)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pageNum", value = "页码", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "pageSize", value = "每页数量", dataType = "Integer", paramType = "query", required = true),
+            @ApiImplicitParam(name = "taskNo", value = "任务编号", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "keyword", value = "搜索标题或编号", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "status", value = "任务状态", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "pushType", value = "推送类型:1-交易类 2-系统类 3-运营类 4-社交类", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "channel", value = "推送渠道,如 notification/inapp", dataType = "String", paramType = "query"),
+            @ApiImplicitParam(name = "startTime", value = "开始时间", dataType = "Date", paramType = "query"),
+            @ApiImplicitParam(name = "endTime", value = "结束时间", dataType = "Date", paramType = "query")
+    })
+    @GetMapping("/statisticsList")
+    public R<CommonPushTaskStatisticsPageVo> statisticsList(
+            @RequestParam Integer pageNum,
+            @RequestParam Integer pageSize,
+            @RequestParam(required = false) String taskNo,
+            @RequestParam(required = false) String keyword,
+            @RequestParam(required = false) String status,
+            @RequestParam(required = false) Integer pushType,
+            @RequestParam(required = false) String channel,
+            @RequestParam(required = false) Date startTime,
+            @RequestParam(required = false) Date endTime) {
+        return commonPushTaskService.statisticsList(pageNum, pageSize, taskNo, keyword, status, pushType, channel, startTime, endTime);
+    }
+
+    @ApiOperation("推送漏斗统计")
+    @ApiOperationSupport(order = 7)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "pushTaskId", value = "推送任务ID,为空则统计全部", dataType = "Long", paramType = "query"),
+            @ApiImplicitParam(name = "startTime", value = "开始时间", dataType = "Date", paramType = "query"),
+            @ApiImplicitParam(name = "endTime", value = "结束时间", dataType = "Date", paramType = "query")
+    })
+    @GetMapping("/funnel")
+    public R<CommonPushFunnelVo> funnel(
+            @RequestParam(required = false) Long pushTaskId,
+            @RequestParam(required = false) Date startTime,
+            @RequestParam(required = false) Date endTime) {
+        return commonPushTaskService.funnel(pushTaskId, startTime, endTime);
+    }
+
+    @ApiOperation("报表中心(日报/周报/月报)")
+    @ApiOperationSupport(order = 8)
+    @ApiImplicitParams({
+            @ApiImplicitParam(name = "reportType", value = "报表类型:1-日报 2-周报 3-月报", dataType = "Integer", paramType = "query"),
+            @ApiImplicitParam(name = "reportDate", value = "报表日期,日报传当天,周报传周内任意一天,月报传月内任意一天", dataType = "Date", paramType = "query")
+    })
+    @GetMapping("/report")
+    public R<CommonPushReportVo> report(
+            @RequestParam(required = false) Integer reportType,
+            @RequestParam(required = false) Date reportDate) {
+        return commonPushTaskService.report(reportType, reportDate);
+    }
 }

+ 13 - 0
alien-store/src/main/java/shop/alien/store/service/CommonPushTaskService.java

@@ -4,6 +4,11 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonPushTask;
+import shop.alien.entity.store.vo.CommonPushFunnelVo;
+import shop.alien.entity.store.vo.CommonPushReportVo;
+import shop.alien.entity.store.vo.CommonPushTaskStatisticsPageVo;
+
+import java.util.Date;
 
 public interface CommonPushTaskService extends IService<CommonPushTask> {
 
@@ -16,4 +21,12 @@ public interface CommonPushTaskService extends IService<CommonPushTask> {
     R<CommonPushTask> getInfoById(Long id);
 
     R<IPage<CommonPushTask>> list(Integer pageNum, Integer pageSize, String taskNo, String title, String status, Integer pushType);
+
+    R<CommonPushTaskStatisticsPageVo> statisticsList(Integer pageNum, Integer pageSize, String taskNo, String keyword,
+                                                     String status, Integer pushType, String channel,
+                                                     Date startTime, Date endTime);
+
+    R<CommonPushFunnelVo> funnel(Long pushTaskId, Date startTime, Date endTime);
+
+    R<CommonPushReportVo> report(Integer reportType, Date reportDate);
 }

+ 268 - 0
alien-store/src/main/java/shop/alien/store/service/impl/CommonPushTaskServiceImpl.java

@@ -10,14 +10,26 @@ import org.apache.commons.lang3.StringUtils;
 import org.springframework.stereotype.Service;
 import shop.alien.entity.result.R;
 import shop.alien.entity.store.CommonPushTask;
+import shop.alien.entity.store.dto.CommonPushTaskStatsDto;
+import shop.alien.entity.store.vo.*;
 import shop.alien.mapper.CommonPushTaskMapper;
 import shop.alien.store.service.CommonPushTaskService;
 
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+
 @Slf4j
 @Service
 @RequiredArgsConstructor
 public class CommonPushTaskServiceImpl extends ServiceImpl<CommonPushTaskMapper, CommonPushTask> implements CommonPushTaskService {
 
+    private static final String[] WEEKDAY_NAMES = {"周日", "周一", "周二", "周三", "周四", "周五", "周六"};
+
     @Override
     public R<String> add(CommonPushTask task) {
         log.info("CommonPushTaskServiceImpl.add, param={}", task);
@@ -69,4 +81,260 @@ public class CommonPushTaskServiceImpl extends ServiceImpl<CommonPushTaskMapper,
         wrapper.orderByDesc(CommonPushTask::getUpdatedTime);
         return R.data(this.page(page, wrapper));
     }
+
+    @Override
+    public R<CommonPushTaskStatisticsPageVo> statisticsList(Integer pageNum, Integer pageSize, String taskNo, String keyword,
+                                                            String status, Integer pushType, String channel,
+                                                            Date startTime, Date endTime) {
+        log.info("CommonPushTaskServiceImpl.statisticsList, pageNum={}, pageSize={}, taskNo={}, keyword={}, status={}, pushType={}, channel={}, startTime={}, endTime={}",
+                pageNum, pageSize, taskNo, keyword, status, pushType, channel, startTime, endTime);
+        Page<CommonPushTaskStatsDto> page = new Page<>(pageNum, pageSize);
+        IPage<CommonPushTaskStatsDto> statsPage = baseMapper.selectStatisticsPage(page, taskNo, keyword, status, pushType, channel, startTime, endTime);
+
+        List<CommonPushTaskStatsDto> allStats = baseMapper.selectStatisticsList(taskNo, keyword, status, pushType, channel, startTime, endTime);
+
+        CommonPushTaskStatisticsPageVo result = new CommonPushTaskStatisticsPageVo();
+        result.setSummary(buildSummary(allStats));
+        result.setPage(statsPage.convert(this::toStatisticsItem));
+        return R.data(result);
+    }
+
+    @Override
+    public R<CommonPushFunnelVo> funnel(Long pushTaskId, Date startTime, Date endTime) {
+        log.info("CommonPushTaskServiceImpl.funnel, pushTaskId={}, startTime={}, endTime={}", pushTaskId, startTime, endTime);
+        CommonPushTaskStatsDto stats = baseMapper.selectUserStats(pushTaskId, startTime, endTime);
+        if (stats == null) {
+            stats = new CommonPushTaskStatsDto();
+            stats.setSentCount(0L);
+            stats.setDeliveredCount(0L);
+            stats.setShowCount(0L);
+            stats.setClickCount(0L);
+        }
+
+        Long plannedCount = null;
+        if (pushTaskId != null) {
+            CommonPushTask task = this.getById(pushTaskId);
+            if (task != null) {
+                plannedCount = task.getEstimatedCount() != null ? task.getEstimatedCount().longValue() : null;
+            }
+        }
+
+        CommonPushFunnelVo funnelVo = new CommonPushFunnelVo();
+        funnelVo.setPushTaskId(pushTaskId);
+        funnelVo.setSteps(buildFunnelSteps(plannedCount, stats));
+        return R.data(funnelVo);
+    }
+
+    @Override
+    public R<CommonPushReportVo> report(Integer reportType, Date reportDate) {
+        log.info("CommonPushTaskServiceImpl.report, reportType={}, reportDate={}", reportType, reportDate);
+        if (reportType == null) {
+            reportType = 1;
+        }
+        if (reportDate == null) {
+            reportDate = truncateToDay(new Date());
+        } else {
+            reportDate = truncateToDay(reportDate);
+        }
+
+        Date[] range = resolveReportRange(reportType, reportDate);
+        Date startTime = range[0];
+        Date endTime = range[1];
+
+        CommonPushTaskStatsDto stats = baseMapper.selectUserStats(null, startTime, endTime);
+        if (stats == null) {
+            stats = new CommonPushTaskStatsDto();
+            stats.setSentCount(0L);
+            stats.setDeliveredCount(0L);
+            stats.setClickCount(0L);
+        }
+
+        CommonPushReportVo reportVo = new CommonPushReportVo();
+        reportVo.setReportType(reportType);
+        reportVo.setReportDate(reportDate);
+        reportVo.setStartTime(startTime);
+        reportVo.setEndTime(endTime);
+        reportVo.setReportTitle(buildReportTitle(reportType, reportDate));
+        reportVo.setSummary(buildUserStatsSummary(stats));
+        reportVo.setTopList(buildTopList(startTime, endTime));
+        return R.data(reportVo);
+    }
+
+    private CommonPushTaskStatisticsItemVo toStatisticsItem(CommonPushTaskStatsDto dto) {
+        CommonPushTaskStatisticsItemVo item = new CommonPushTaskStatisticsItemVo();
+        item.setId(dto.getId());
+        item.setTaskNo(dto.getTaskNo());
+        item.setTitle(dto.getTitle());
+        item.setPushType(dto.getPushType());
+        item.setStatus(dto.getStatus());
+        item.setChannels(dto.getChannels());
+        item.setEstimatedCount(dto.getEstimatedCount());
+        item.setCreatedTime(dto.getCreatedTime());
+        item.setSentCount(defaultCount(dto.getSentCount()));
+        item.setDeliveredCount(defaultCount(dto.getDeliveredCount()));
+        item.setClickCount(defaultCount(dto.getClickCount()));
+        item.setPushRate(calcRate(item.getSentCount(), dto.getEstimatedCount() != null ? dto.getEstimatedCount().longValue() : 0L));
+        item.setDeliveryRate(calcRate(item.getDeliveredCount(), item.getSentCount()));
+        item.setClickRate(calcRate(item.getClickCount(), item.getDeliveredCount()));
+        return item;
+    }
+
+    private CommonPushStatisticsSummaryVo buildSummary(List<CommonPushTaskStatsDto> statsList) {
+        long sentCount = 0L;
+        long deliveredCount = 0L;
+        long clickCount = 0L;
+        if (statsList != null) {
+            for (CommonPushTaskStatsDto dto : statsList) {
+                sentCount += defaultCount(dto.getSentCount());
+                deliveredCount += defaultCount(dto.getDeliveredCount());
+                clickCount += defaultCount(dto.getClickCount());
+            }
+        }
+        return buildSummary(sentCount, deliveredCount, clickCount);
+    }
+
+    private CommonPushStatisticsSummaryVo buildUserStatsSummary(CommonPushTaskStatsDto stats) {
+        return buildSummary(defaultCount(stats.getSentCount()), defaultCount(stats.getDeliveredCount()), defaultCount(stats.getClickCount()));
+    }
+
+    private CommonPushStatisticsSummaryVo buildSummary(long sentCount, long deliveredCount, long clickCount) {
+        CommonPushStatisticsSummaryVo summary = new CommonPushStatisticsSummaryVo();
+        summary.setPushVolume(sentCount);
+        summary.setDeliveredCount(deliveredCount);
+        summary.setClickCount(clickCount);
+        summary.setDeliveryRate(calcRate(deliveredCount, sentCount));
+        summary.setClickRate(calcRate(clickCount, deliveredCount));
+        summary.setConversionRate(calcRate(clickCount, sentCount));
+        return summary;
+    }
+
+    private List<CommonPushFunnelStepVo> buildFunnelSteps(Long plannedCount, CommonPushTaskStatsDto stats) {
+        long actualSend = defaultCount(stats.getSentCount());
+        long arrived = defaultCount(stats.getDeliveredCount());
+        long show = defaultCount(stats.getShowCount());
+        long click = defaultCount(stats.getClickCount());
+
+        List<CommonPushFunnelStepVo> steps = new ArrayList<>();
+
+        CommonPushFunnelStepVo planned = new CommonPushFunnelStepVo();
+        planned.setStepCode("planned");
+        planned.setStepName("计划发送");
+        planned.setCount(plannedCount);
+        planned.setConversionRate(plannedCount != null && plannedCount > 0 ? BigDecimal.valueOf(100) : null);
+        steps.add(planned);
+
+        CommonPushFunnelStepVo actual = new CommonPushFunnelStepVo();
+        actual.setStepCode("actual");
+        actual.setStepName("实际发送");
+        actual.setCount(actualSend);
+        actual.setConversionRate(calcRate(actualSend, plannedCount != null ? plannedCount : 0L));
+        steps.add(actual);
+
+        CommonPushFunnelStepVo arrivedStep = new CommonPushFunnelStepVo();
+        arrivedStep.setStepCode("arrived");
+        arrivedStep.setStepName("抵达设备");
+        arrivedStep.setCount(arrived);
+        arrivedStep.setConversionRate(calcRate(arrived, actualSend));
+        steps.add(arrivedStep);
+
+        CommonPushFunnelStepVo showStep = new CommonPushFunnelStepVo();
+        showStep.setStepCode("show");
+        showStep.setStepName("展示通知");
+        showStep.setCount(show);
+        showStep.setConversionRate(calcRate(show, arrived));
+        steps.add(showStep);
+
+        CommonPushFunnelStepVo clickStep = new CommonPushFunnelStepVo();
+        clickStep.setStepCode("click");
+        clickStep.setStepName("用户点击");
+        clickStep.setCount(click);
+        clickStep.setConversionRate(calcRate(click, show));
+        steps.add(clickStep);
+
+        return steps;
+    }
+
+    private List<CommonPushReportTopItemVo> buildTopList(Date startTime, Date endTime) {
+        List<CommonPushTaskStatsDto> topStats = baseMapper.selectTopByClickRate(startTime, endTime, 5);
+        List<CommonPushReportTopItemVo> topList = new ArrayList<>();
+        if (topStats == null) {
+            return topList;
+        }
+        int rank = 1;
+        for (CommonPushTaskStatsDto dto : topStats) {
+            CommonPushReportTopItemVo item = new CommonPushReportTopItemVo();
+            item.setRank(rank++);
+            item.setPushTaskId(dto.getId());
+            item.setTitle(dto.getTitle());
+            item.setDeliveredCount(defaultCount(dto.getDeliveredCount()));
+            item.setClickCount(defaultCount(dto.getClickCount()));
+            item.setClickRate(calcRate(item.getClickCount(), item.getDeliveredCount()));
+            topList.add(item);
+        }
+        return topList;
+    }
+
+    private String buildReportTitle(Integer reportType, Date reportDate) {
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
+        String dateStr = dateFormat.format(reportDate);
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(reportDate);
+        String weekday = WEEKDAY_NAMES[calendar.get(Calendar.DAY_OF_WEEK) - 1];
+        if (reportType == 2) {
+            return "推送周报 — " + dateStr;
+        }
+        if (reportType == 3) {
+            SimpleDateFormat monthFormat = new SimpleDateFormat("yyyy-MM");
+            return "推送月报 — " + monthFormat.format(reportDate);
+        }
+        return "推送日报 — " + dateStr + " (" + weekday + ")";
+    }
+
+    private Date[] resolveReportRange(Integer reportType, Date reportDate) {
+        Calendar start = Calendar.getInstance();
+        start.setTime(reportDate);
+        start.set(Calendar.HOUR_OF_DAY, 0);
+        start.set(Calendar.MINUTE, 0);
+        start.set(Calendar.SECOND, 0);
+        start.set(Calendar.MILLISECOND, 0);
+
+        Calendar end = (Calendar) start.clone();
+        if (reportType == 2) {
+            int dayOfWeek = start.get(Calendar.DAY_OF_WEEK);
+            int diffToMonday = (dayOfWeek + 5) % 7;
+            start.add(Calendar.DAY_OF_MONTH, -diffToMonday);
+            end.setTime(start.getTime());
+            end.add(Calendar.DAY_OF_MONTH, 7);
+        } else if (reportType == 3) {
+            start.set(Calendar.DAY_OF_MONTH, 1);
+            end.setTime(start.getTime());
+            end.add(Calendar.MONTH, 1);
+        } else {
+            end.add(Calendar.DAY_OF_MONTH, 1);
+        }
+        return new Date[]{start.getTime(), end.getTime()};
+    }
+
+    private Date truncateToDay(Date date) {
+        Calendar calendar = Calendar.getInstance();
+        calendar.setTime(date);
+        calendar.set(Calendar.HOUR_OF_DAY, 0);
+        calendar.set(Calendar.MINUTE, 0);
+        calendar.set(Calendar.SECOND, 0);
+        calendar.set(Calendar.MILLISECOND, 0);
+        return calendar.getTime();
+    }
+
+    private long defaultCount(Long count) {
+        return count == null ? 0L : count;
+    }
+
+    private BigDecimal calcRate(long numerator, long denominator) {
+        if (denominator <= 0) {
+            return BigDecimal.ZERO;
+        }
+        return BigDecimal.valueOf(numerator)
+                .multiply(BigDecimal.valueOf(100))
+                .divide(BigDecimal.valueOf(denominator), 1, RoundingMode.HALF_UP);
+    }
 }