CPS统计数据汇总 实现计划

CPS统计数据汇总 实现计划

For agentic workers: Use subagent-driven-development or executing-plans to implement this plan.

Goal: 实现 CPS 运营数据统计体系,包含定时汇总 Job、看板 API、趋势 API 和前端统计页面。

Architecture:

  • 后端:定时任务每日凌晨聚合 yudao_cps_statistics 表;API 层提供"实时看板"(直接聚合原始表查今日/昨日数据)和"历史趋势"(读统计表)两套接口;前端用 ECharts 渲染折线+饼图。
  • 统计粒度:(stat_date, platform_code) 唯一组合,platform_code='total' 代表全平台汇总。
  • "实时看板"(今日数据)走实时 SQL 聚合,不依赖定时任务,保证数据即时性。

Tech Stack: Java 17 / Spring Boot 3.x / MyBatis Plus / Quartz / Vue3 / ECharts 5


Task 1:数据库建表 + 补充 SQL 脚本

Files:

  • Modify: backend/sql/module/cps-all-in-one.sql(追加 yudao_cps_statistics 表定义)

  • cps-all-in-one.sql 末尾(SET FOREIGN_KEY_CHECKS = 1 之前)追加如下建表 SQL:

sql 复制代码
-- ----------------------------
-- 8. CPS统计数据表(Phase5新增)
-- ----------------------------
DROP TABLE IF EXISTS `yudao_cps_statistics`;
CREATE TABLE `yudao_cps_statistics` (
  `id`                        bigint        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `stat_date`                 date          NOT NULL COMMENT '统计日期',
  `platform_code`             varchar(32)   NOT NULL DEFAULT 'total' COMMENT '平台编码(total=全平台)',
  `order_count`               int           NOT NULL DEFAULT '0' COMMENT '订单总数',
  `new_order_count`           int           NOT NULL DEFAULT '0' COMMENT '新增订单数(不含退款)',
  `order_amount`              decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '订单总金额',
  `commission_amount`         decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '佣金总额',
  `settled_commission_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '已结算佣金',
  `pending_commission_amount` decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '待结算佣金',
  `rebate_amount`             decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '返利总额',
  `profit_amount`             decimal(14,2) NOT NULL DEFAULT '0.00' COMMENT '平台利润(佣金-返利)',
  `active_member_count`       int           NOT NULL DEFAULT '0' COMMENT '活跃会员数(当日有下单)',
  `creator`                   varchar(64)            DEFAULT NULL COMMENT '创建人',
  `create_time`               datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `updater`                   varchar(64)            DEFAULT NULL COMMENT '更新人',
  `update_time`               datetime      NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  `deleted`                   bit(1)                 DEFAULT b'0' COMMENT '是否删除',
  `tenant_id`                 bigint        NOT NULL DEFAULT '0' COMMENT '租户编号',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_stat_date_platform` (`stat_date`, `platform_code`, `tenant_id`) USING BTREE,
  KEY `idx_stat_date` (`stat_date`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='CPS统计数据表';

Task 2:扩展 CpsStatisticsMapper(支持统计查询)

Files:

  • Modify: backend/yudao-module-cps/yudao-module-cps-biz/src/main/java/cn/iocoder/yudao/module/cps/dal/mysql/statistics/CpsStatisticsMapper.java

  • 新增以下查询方法(Lambda + default method):

    • selectByDateAndPlatform(LocalDate date, String platformCode) --- 按日期+平台精确查
    • selectTrendList(LocalDate startDate, LocalDate endDate, String platformCode) --- 日期区间趋势
    • selectPlatformSummary(LocalDate startDate, LocalDate endDate) --- 各平台聚合饼图
java 复制代码
@Mapper
public interface CpsStatisticsMapper extends BaseMapperX<CpsStatisticsDO> {

    default CpsStatisticsDO selectByDateAndPlatform(LocalDate date, String platformCode) {
        return selectOne(new LambdaQueryWrapperX<CpsStatisticsDO>()
                .eq(CpsStatisticsDO::getStatDate, date)
                .eq(CpsStatisticsDO::getPlatformCode, platformCode));
    }

    default List<CpsStatisticsDO> selectTrendList(LocalDate startDate, LocalDate endDate,
                                                   String platformCode) {
        return selectList(new LambdaQueryWrapperX<CpsStatisticsDO>()
                .eq(CpsStatisticsDO::getPlatformCode, platformCode)
                .ge(CpsStatisticsDO::getStatDate, startDate)
                .le(CpsStatisticsDO::getStatDate, endDate)
                .orderByAsc(CpsStatisticsDO::getStatDate));
    }

    default List<CpsStatisticsDO> selectPlatformSummary(LocalDate startDate, LocalDate endDate) {
        return selectList(new LambdaQueryWrapperX<CpsStatisticsDO>()
                .ne(CpsStatisticsDO::getPlatformCode, "total")
                .ge(CpsStatisticsDO::getStatDate, startDate)
                .le(CpsStatisticsDO::getStatDate, endDate));
    }
}

Task 3:扩展 CpsOrderMapper(统计聚合查询)

Files:

  • Modify: backend/yudao-module-cps/.../dal/mysql/order/CpsOrderMapper.java

  • 新增 XML Mapper 文件用于复杂聚合 SQL(按日期+平台分组)

新建 CpsOrderMapper.xml(路径:resources/mapper/order/CpsOrderMapper.xml):

xml 复制代码
<?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="cn.iocoder.yudao.module.cps.dal.mysql.order.CpsOrderMapper">

    <!-- 统计指定日期各平台订单数据 -->
    <select id="selectDailyStatsByDate" resultType="map">
        SELECT
            platform_code,
            COUNT(*) AS order_count,
            SUM(CASE WHEN order_status NOT IN ('refunded','invalid') THEN 1 ELSE 0 END) AS new_order_count,
            COALESCE(SUM(final_price), 0) AS order_amount,
            COALESCE(SUM(commission_amount), 0) AS commission_amount,
            COALESCE(SUM(CASE WHEN order_status IN ('settled','credited') THEN commission_amount ELSE 0 END), 0) AS settled_commission_amount,
            COALESCE(SUM(CASE WHEN order_status IN ('ordered','paid','received') THEN commission_amount ELSE 0 END), 0) AS pending_commission_amount,
            COALESCE(SUM(real_rebate), 0) AS rebate_amount,
            COUNT(DISTINCT member_id) AS active_member_count
        FROM yudao_cps_order
        WHERE DATE(create_time) = #{statDate}
          AND deleted = 0
          AND tenant_id = #{tenantId}
        GROUP BY platform_code
    </select>

    <!-- 实时看板:今日 vs 昨日数据(用于趋势对比) -->
    <select id="selectRealtimeDashboard" resultType="map">
        SELECT
            COUNT(*) AS order_count,
            COALESCE(SUM(final_price), 0) AS order_amount,
            COALESCE(SUM(commission_amount), 0) AS commission_amount,
            COALESCE(SUM(real_rebate), 0) AS rebate_amount,
            COALESCE(SUM(commission_amount) - SUM(real_rebate), 0) AS profit_amount,
            COUNT(DISTINCT member_id) AS active_member_count
        FROM yudao_cps_order
        WHERE DATE(create_time) = #{statDate}
          AND deleted = 0
          AND tenant_id = #{tenantId}
    </select>

</mapper>
  • CpsOrderMapper.java 中声明对应接口:
java 复制代码
List<Map<String, Object>> selectDailyStatsByDate(@Param("statDate") LocalDate statDate,
                                                  @Param("tenantId") Long tenantId);

Map<String, Object> selectRealtimeDashboard(@Param("statDate") LocalDate statDate,
                                             @Param("tenantId") Long tenantId);

Task 4:CpsStatisticsService 接口 + 实现

Files:

  • Create: backend/.../service/statistics/CpsStatisticsService.java

  • Create: backend/.../service/statistics/CpsStatisticsServiceImpl.java

  • 创建 CpsStatisticsService.java

java 复制代码
public interface CpsStatisticsService {
    /** 汇总指定日期的统计数据(供Job调用) */
    void aggregateDailyStatistics(LocalDate date);

    /** 运营看板:实时今日数据 + 历史昨日数据(用于环比) */
    CpsDashboardRespVO getDashboard();

    /** 趋势数据:按日返回指定时间段的统计指标 */
    List<CpsStatisticsDO> getTrend(LocalDate startDate, LocalDate endDate, String platformCode);

    /** 各平台占比汇总(饼图数据) */
    List<CpsPlatformSummaryVO> getPlatformSummary(LocalDate startDate, LocalDate endDate);
}
  • 创建 CpsStatisticsServiceImpl.java,关键逻辑:
java 复制代码
@Service
@Validated
public class CpsStatisticsServiceImpl implements CpsStatisticsService {

    @Resource private CpsStatisticsMapper statisticsMapper;
    @Resource private CpsOrderMapper orderMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void aggregateDailyStatistics(LocalDate date) {
        Long tenantId = TenantContextHolder.getTenantId();
        // 1. 查各平台明细
        List<Map<String, Object>> platformStats =
            orderMapper.selectDailyStatsByDate(date, tenantId);

        // 2. 计算全平台汇总
        BigDecimal totalOrderAmount = BigDecimal.ZERO;
        BigDecimal totalCommission = BigDecimal.ZERO;
        BigDecimal totalRebate = BigDecimal.ZERO;
        int totalOrderCount = 0;
        int totalNewOrderCount = 0;
        int totalActiveMembers = 0;

        for (Map<String, Object> row : platformStats) {
            String platformCode = (String) row.get("platform_code");
            CpsStatisticsDO stat = buildStatDO(row, date, platformCode);
            // upsert(INSERT ON DUPLICATE KEY UPDATE)
            upsertStatistics(stat);
            // 累加汇总
            totalOrderAmount = totalOrderAmount.add(nvl(stat.getOrderAmount()));
            totalCommission   = totalCommission.add(nvl(stat.getCommissionAmount()));
            totalRebate       = totalRebate.add(nvl(stat.getRebateAmount()));
            totalOrderCount  += safe(stat.getOrderCount());
            totalNewOrderCount += safe(stat.getNewOrderCount());
            totalActiveMembers += safe(stat.getActiveMemberCount());
        }

        // 3. 写全平台汇总行 platform_code='total'
        CpsStatisticsDO totalStat = CpsStatisticsDO.builder()
            .statDate(date).platformCode("total")
            .orderCount(totalOrderCount).newOrderCount(totalNewOrderCount)
            .orderAmount(totalOrderAmount).commissionAmount(totalCommission)
            .rebateAmount(totalRebate).profitAmount(totalCommission.subtract(totalRebate))
            .activeMemberCount(totalActiveMembers)
            .build();
        upsertStatistics(totalStat);
    }

    @Override
    public CpsDashboardRespVO getDashboard() {
        Long tenantId = TenantContextHolder.getTenantId();
        LocalDate today = LocalDate.now();
        LocalDate yesterday = today.minusDays(1);
        Map<String, Object> todayData = orderMapper.selectRealtimeDashboard(today, tenantId);
        Map<String, Object> yestData  = orderMapper.selectRealtimeDashboard(yesterday, tenantId);
        // 同时查历史待结算
        CpsStatisticsDO histTotal = statisticsMapper.selectByDateAndPlatform(yesterday, "total");
        return buildDashboardVO(todayData, yestData, histTotal);
    }

    /** upsert: 存在则 updateById,不存在则 insert */
    private void upsertStatistics(CpsStatisticsDO stat) {
        CpsStatisticsDO existing = statisticsMapper.selectByDateAndPlatform(
                stat.getStatDate(), stat.getPlatformCode());
        if (existing != null) {
            stat.setId(existing.getId());
            statisticsMapper.updateById(stat);
        } else {
            statisticsMapper.insert(stat);
        }
    }
}

Task 5:VO 类(看板 + 平台汇总)

Files:

  • Create: backend/.../controller/admin/statistics/vo/CpsDashboardRespVO.java

  • Create: backend/.../controller/admin/statistics/vo/CpsTrendRespVO.java

  • Create: backend/.../controller/admin/statistics/vo/CpsPlatformSummaryVO.java

  • CpsDashboardRespVO.java(今日指标 + 昨日环比):

java 复制代码
@Schema(description = "CPS运营数据看板 Response VO")
@Data
public class CpsDashboardRespVO {
    @Schema(description = "今日订单数")       private Integer todayOrderCount;
    @Schema(description = "今日佣金(元)")   private BigDecimal todayCommission;
    @Schema(description = "今日返利(元)")   private BigDecimal todayRebate;
    @Schema(description = "今日利润(元)")   private BigDecimal todayProfit;
    @Schema(description = "今日活跃会员数")   private Integer todayActiveMembers;
    @Schema(description = "昨日订单数")       private Integer yesterdayOrderCount;
    @Schema(description = "昨日佣金(元)")   private BigDecimal yesterdayCommission;
    @Schema(description = "昨日返利(元)")   private BigDecimal yesterdayRebate;
    @Schema(description = "昨日利润(元)")   private BigDecimal yesterdayProfit;
    @Schema(description = "累计待结算佣金")   private BigDecimal totalPendingCommission;
    @Schema(description = "累计已结算佣金")   private BigDecimal totalSettledCommission;
}
  • CpsTrendRespVO.java(趋势折线图所需):
java 复制代码
@Schema(description = "CPS趋势数据 Response VO")
@Data
public class CpsTrendRespVO {
    @Schema(description = "日期列表(YYYY-MM-DD)") private List<String> dates;
    @Schema(description = "订单数序列")             private List<Integer> orderCounts;
    @Schema(description = "佣金序列(元)")          private List<BigDecimal> commissions;
    @Schema(description = "返利序列(元)")          private List<BigDecimal> rebates;
    @Schema(description = "利润序列(元)")          private List<BigDecimal> profits;
}
  • CpsPlatformSummaryVO.java(平台饼图):
java 复制代码
@Schema(description = "CPS平台汇总 Response VO")
@Data
public class CpsPlatformSummaryVO {
    @Schema(description = "平台编码") private String platformCode;
    @Schema(description = "平台名称") private String platformName;
    @Schema(description = "订单数")   private Integer orderCount;
    @Schema(description = "佣金(元)") private BigDecimal commissionAmount;
    @Schema(description = "返利(元)") private BigDecimal rebateAmount;
    @Schema(description = "利润(元)") private BigDecimal profitAmount;
}

Task 6:CpsStatisticsController(管理端 API)

Files:

  • Create: backend/.../controller/admin/statistics/CpsStatisticsController.java

  • 实现如下接口:

java 复制代码
@Tag(name = "管理后台 - CPS数据统计")
@RestController
@RequestMapping("/admin-api/cps/statistics")
@Validated
public class CpsStatisticsController {

    @Resource private CpsStatisticsService statisticsService;

    /** 运营看板:实时今日数据 */
    @GetMapping("/dashboard")
    @Operation(summary = "运营数据看板")
    @PreAuthorize("@ss.hasPermission('cps:statistics:query')")
    public CommonResult<CpsDashboardRespVO> getDashboard() {
        return success(statisticsService.getDashboard());
    }

    /** 趋势图表:折线图数据 */
    @GetMapping("/trend")
    @Operation(summary = "趋势图表数据")
    @PreAuthorize("@ss.hasPermission('cps:statistics:query')")
    public CommonResult<CpsTrendRespVO> getTrend(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate,
            @RequestParam(defaultValue = "total") String platformCode) {
        List<CpsStatisticsDO> list = statisticsService.getTrend(startDate, endDate, platformCode);
        return success(buildTrendVO(list));
    }

    /** 平台占比:饼图数据 */
    @GetMapping("/platform-summary")
    @Operation(summary = "平台占比统计")
    @PreAuthorize("@ss.hasPermission('cps:statistics:query')")
    public CommonResult<List<CpsPlatformSummaryVO>> getPlatformSummary(
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
            @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
        return success(statisticsService.getPlatformSummary(startDate, endDate));
    }
}

Task 7:统计汇总定时 Job

Files:

  • Create: backend/.../job/CpsStatisticsAggregateJob.java

  • 创建定时任务(每日凌晨 1 点执行,汇总昨日数据):

java 复制代码
/**
 * CPS 统计数据汇总定时任务
 *
 * <h3>Quartz 注册方式</h3>
 * 处理器名字:cpsStatisticsAggregateJob
 * CRON 表达式:0 0 1 * * ?(每日凌晨 1 点执行)
 * 处理器参数示例:{"date":"2026-04-06"}(不填则默认昨日)
 */
@Slf4j
@Component("cpsStatisticsAggregateJob")
public class CpsStatisticsAggregateJob implements JobHandler {

    @Resource private CpsStatisticsService statisticsService;

    @Override
    public String execute(String param) throws Exception {
        LocalDate date = parseDate(param);
        log.info("[CpsStatisticsAggregateJob] 开始汇总 {} 统计数据", date);
        statisticsService.aggregateDailyStatistics(date);
        log.info("[CpsStatisticsAggregateJob] 完成汇总 {} 统计数据", date);
        return "汇总完成,日期=" + date;
    }

    private LocalDate parseDate(String param) {
        if (StrUtil.isNotBlank(param)) {
            try {
                Map<String, String> map = JsonUtils.parseObject(param, Map.class);
                if (map != null && map.containsKey("date")) {
                    return LocalDate.parse(map.get("date"));
                }
            } catch (Exception ignored) {}
        }
        return LocalDate.now().minusDays(1); // 默认昨日
    }
}

Task 8:编译验证

  • 执行编译命令,确认无错误:
bash 复制代码
cd backend
mvn compile -pl yudao-module-cps/yudao-module-cps-biz -am -q

期望:无错误输出


Task 9:前端 API 层

Files:

  • Create: frontend/admin-vue3/src/api/cps/statistics.ts

  • 创建统计 API 文件:

typescript 复制代码
import request from '@/config/axios'

export interface CpsDashboardVO {
  todayOrderCount: number
  todayCommission: number
  todayRebate: number
  todayProfit: number
  todayActiveMembers: number
  yesterdayOrderCount: number
  yesterdayCommission: number
  yesterdayRebate: number
  yesterdayProfit: number
  totalPendingCommission: number
  totalSettledCommission: number
}

export interface CpsTrendVO {
  dates: string[]
  orderCounts: number[]
  commissions: number[]
  rebates: number[]
  profits: number[]
}

export interface CpsPlatformSummaryVO {
  platformCode: string
  platformName: string
  orderCount: number
  commissionAmount: number
  rebateAmount: number
  profitAmount: number
}

export const CpsStatisticsApi = {
  getDashboard: () =>
    request.get<CpsDashboardVO>({ url: '/admin-api/cps/statistics/dashboard' }),

  getTrend: (params: { startDate: string; endDate: string; platformCode?: string }) =>
    request.get<CpsTrendVO>({ url: '/admin-api/cps/statistics/trend', params }),

  getPlatformSummary: (params: { startDate: string; endDate: string }) =>
    request.get<CpsPlatformSummaryVO[]>({ url: '/admin-api/cps/statistics/platform-summary', params }),
}

Task 10:前端统计看板页面

Files:

  • Create: frontend/admin-vue3/src/views/cps/statistics/index.vue

页面包含三个区域:

  1. 指标卡片区(今日数据 + 环比箭头):订单数、佣金、返利、利润、活跃会员
  2. 趋势折线图:时间范围选择器 + 平台切换 + 折线图(ECharts)
  3. 平台占比饼图:各平台订单/佣金占比(ECharts)

关键实现要点:

  • 使用 echarts/core + 按需引入(与 iot/home 组件保持一致)
  • 环比增减用绿/红箭头标注
  • 折线图支持选择显示指标(订单/佣金/返利/利润)
  • 时间选择器默认最近 7 天,快捷选项:7天/30天/本月

Task 11:前端代码检查

  • 确认 Task 9、10 新增文件无明显语法错误(人工 review 或 pnpm ts:check)
相关推荐
onebyte8bits3 小时前
NestJS 系列教程(十八):文件上传与对象存储架构(Multer + S3/OSS + 访问控制)
前端·架构·node.js·状态模式·nestjs
前端不太难4 小时前
深度解析:OpenClaw 多智能体系统四大支柱
人工智能·状态模式·openclaw
前端不太难5 小时前
鸿蒙游戏开发的正确分层方式
华为·状态模式·harmonyos
兄弟加油,别颓废了。5 小时前
【无标题】
状态模式
SuperEugene6 小时前
Python 函数与模块化:前端工程化思维完全通用| 基础篇
前端·python·状态模式
前端不太难1 天前
推理+护栏:OpenClaw的信任双保险
状态模式·openclaw
观无2 天前
.NET Core + Ocelot 网关 跨域 (CORS) 配置
状态模式·.netcore
前端不太难2 天前
鸿蒙游戏如何接入支付 / 排行榜 / 社交
游戏·状态模式·harmonyos
前端不太难3 天前
OpenClaw:探索未知的多智能体中枢
状态模式·openclaw