CPS统计数据汇总 实现计划
For agentic workers: Use
subagent-driven-developmentorexecuting-plansto 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
页面包含三个区域:
- 指标卡片区(今日数据 + 环比箭头):订单数、佣金、返利、利润、活跃会员
- 趋势折线图:时间范围选择器 + 平台切换 + 折线图(ECharts)
- 平台占比饼图:各平台订单/佣金占比(ECharts)
关键实现要点:
- 使用
echarts/core+ 按需引入(与 iot/home 组件保持一致) - 环比增减用绿/红箭头标注
- 折线图支持选择显示指标(订单/佣金/返利/利润)
- 时间选择器默认最近 7 天,快捷选项:7天/30天/本月
Task 11:前端代码检查
- 确认 Task 9、10 新增文件无明显语法错误(人工 review 或 pnpm ts:check)