基于现有 Admin SPA(Vue 3 + Element Plus + ECharts)扩展统计分析功能
状态:v1.0 Draft
后端(num-server)
| 组件 | 位置 | 功能 | 状态 |
|------|------|------|------|
| AdminController | /api/admin/stats | Dashboard 基础统计 | ✅ 已有 |
| CommissionService | getStats(userId) | 个人佣金统计 | ✅ 已有 |
| OrderRepository | findByStatusAndPaidAtBetween | 时间范围订单查询 | ✅ 已有 |
| UserRepository | countByVipEndTimeAfter | VIP 用户统计 | ✅ 已有 |
前端(admin/)
| 组件 | 位置 | 功能 | 状态 |
|------|------|------|------|
| Dashboard 页面 | admin/src/views/dashboard/index.vue | 4 个统计卡片 + 柱状图 + 饼图 | ✅ 已有 |
| ECharts | ^5.4.0 | 图表库 | ✅ 已安装 |
| Element Plus | ^2.4.0 | UI 组件库 | ✅ 已安装 |
| Axios | admin/src/utils/request.ts | HTTP 客户端 | ✅ 已有 |
| 类别 | 缺失内容 | 优先级 |
|---|---|---|
| 后端 | 时间序列统计接口(按日/周/月分组) | ⭐⭐⭐ |
| 后端 | 用户增长趋势(按注册时间分组) | ⭐⭐ |
| 后端 | 收入趋势(按支付时间分组) | ⭐⭐⭐ |
| 后端 | 转化漏斗(普通用户→C 端→能量师) | ⭐⭐ |
| 后端 | 分销网络统计(团队层级/佣金分布) | ⭐⭐ |
| 前端 | 日期范围选择器(快捷 + 自定义) | ⭐⭐⭐ |
| 前端 | 收入趋势折线图 | ⭐⭐⭐ |
| 前端 | 用户增长柱状图 | ⭐⭐ |
| 前端 | 订单状态时间序列(堆叠面积图) | ⭐ |
| 前端 | 数据导出功能(CSV/Excel) | ⭐ |
Phase 1(核心统计)
Phase 2(高级分析)
文件: num-server/src/main/java/com/etotem/num/service/AnalyticsService.java
package com.etotem.num.service;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* Web 端统计分析服务。
* 提供时间序列统计、趋势分析、转化漏斗等功能。
*/
@Service
public class AnalyticsService {
// Dependencies: OrderRepository, UserRepository, CommissionRepository, WithdrawRepository
/**
* 收入时间序列统计。
* @param startDate 开始时间
* @param endDate 结束时间
* @param groupBy 分组维度:"day" | "week" | "month"
* @return List<Map> 每个时间点的数据:{date: "2026-06-01", revenue: 13100, orderCount: 5}
*/
public List<Map<String, Object>> getRevenueTrend(LocalDateTime startDate,
LocalDateTime endDate,
String groupBy) {
// 1. 查询时间范围内所有已支付订单
// 2. 按 groupBy 分组(day/week/month)
// 3. 每组计算:总金额(分)、订单数、平均客单价
// 4. 返回按时间排序的列表
}
/**
* 用户增长时间序列。
* @return List<Map> {date: "2026-06-01", newUsers: 10, newVip: 3, newPractitioner: 1}
*/
public List<Map<String, Object>> getUserGrowthTrend(LocalDateTime startDate,
LocalDateTime endDate,
String groupBy) {
// 1. 查询时间范围内所有新用户(按 created_at)
// 2. 按 vipType 分类统计
// 3. 返回按时间排序的列表
}
/**
* 转化漏斗统计。
* @return Map{registered: 1000, paidC: 200, paidSuper: 50, practitioner: 30, conversionRate: 3%}
*/
public Map<String, Object> getConversionFunnel() {
// 1. 统计总用户数(registered)
// 2. 统计 C 端付费用户数(paidC)
// 3. 统计超级会员数(paidSuper)
// 4. 统计能量师数(practitioner)
// 5. 计算转化率:practitioner / registered
}
/**
* 分销网络统计。
* @return Map{totalPromoters: 50, topPromoters: List[top10], totalCommission: 500000, avgCommission: 10000}
*/
public Map<String, Object> getDistributionStats() {
// 1. 统计有推广记录的用户数(invitedBy IS NOT NULL)
// 2. 查询 Top10 推广者(按 directCount + indirectCount 排序)
// 3. 统计总佣金(settled + available)
// 4. 计算人均佣金
}
/**
* 提现统计。
* @return Map{totalWithdraw: 300000, successRate: 95%, byChannel: {REFUND: 100000, WECHAT: 200000}}
*/
public Map<String, Object> getWithdrawStats(LocalDateTime startDate,
LocalDateTime endDate) {
// 1. 统计提现总额
// 2. 按通道分组统计
// 3. 计算成功率(success / total)
}
/**
* 核心指标汇总(支持日期范围)。
* @return Map{totalRevenue: 1000000, totalOrders: 100, totalUsers: 500, newUsers: 50, ...}
*/
public Map<String, Object> getSummaryStats(LocalDateTime startDate,
LocalDateTime endDate) {
// 综合统计,用于 Dashboard 顶部卡片
}
}
文件: num-server/src/main/java/com/etotem/num/controller/AdminController.java
新增端点:
@PostMapping("/analytics/revenue-trend")
public Result<List<Map<String, Object>>> revenueTrend(@RequestBody Map<String, String> body) {
String startDate = body.get("startDate"); // ISO 8601: "2026-06-01T00:00:00"
String endDate = body.get("endDate"); // ISO 8601: "2026-06-30T23:59:59"
String groupBy = body.get("groupBy"); // "day" | "week" | "month"
LocalDateTime start = LocalDateTime.parse(startDate);
LocalDateTime end = LocalDateTime.parse(endDate);
return Result.success(analyticsService.getRevenueTrend(start, end, groupBy));
}
@PostMapping("/analytics/user-growth")
public Result<List<Map<String, Object>>> userGrowth(@RequestBody Map<String, String> body) {
// 同上
}
@PostMapping("/analytics/conversion-funnel")
public Result<Map<String, Object>> conversionFunnel() {
return Result.success(analyticsService.getConversionFunnel());
}
@PostMapping("/analytics/distribution")
public Result<Map<String, Object>> distributionStats() {
return Result.success(analyticsService.getDistributionStats());
}
@PostMapping("/analytics/summary")
public Result<Map<String, Object>> summaryStats(@RequestBody Map<String, String> body) {
// 支持日期范围
}
OrderRepository 新增方法:
@Query("SELECT DATE(o.paidAt) as date, SUM(o.totalFee) as revenue, COUNT(o) as count " +
"FROM Order o WHERE o.status = 'paid' AND o.paidAt BETWEEN :start AND :end " +
"GROUP BY DATE(o.paidAt) ORDER BY date")
List<Object[]> findRevenueByDate(@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
UserRepository 新增方法:
@Query("SELECT DATE(u.createdAt) as date, COUNT(u) as count " +
"FROM User u WHERE u.createdAt BETWEEN :start AND :end " +
"GROUP BY DATE(u.createdAt) ORDER BY date")
List<Object[]> findUserGrowthByDate(@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
文件: admin/src/router/index.ts
{
path: '/analytics',
name: 'Analytics',
component: () => import('@/views/analytics/index.vue'),
meta: { title: '统计分析', icon: 'trend-charts' }
}
文件: admin/src/layout/index.vue
<el-menu-item index="/analytics">
<el-icon><TrendCharts /></el-icon>
<span>统计分析</span>
</el-menu-item>
文件: admin/src/views/analytics/index.vue
页面结构:
<template>
<div class="analytics-container">
<!-- 1. 日期范围选择器 -->
<el-card class="filter-card">
<el-row :gutter="16" align="middle">
<el-col :span="8">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:shortcuts="dateShortcuts"
@change="fetchData"
/>
</el-col>
<el-col :span="4">
<el-select v-model="groupBy" placeholder="分组维度" @change="fetchData">
<el-option label="按日" value="day" />
<el-option label="按周" value="week" />
<el-option label="按月" value="month" />
</el-select>
</el-col>
<el-col :span="4">
<el-button type="primary" @click="fetchData">查询</el-button>
<el-button @click="exportData">导出</el-button>
</el-col>
</el-row>
</el-card>
<!-- 2. 核心指标卡片(4 列) -->
<el-row :gutter="16" class="metrics-row">
<el-col :span="6">
<el-card shadow="hover">
<div class="metric-item">
<div class="label">总收入</div>
<div class="value">¥{{ summary.totalRevenue / 100 }}</div>
<div class="trend positive">↑ 12%</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<!-- 订单总数 -->
</el-col>
<el-col :span="6">
<!-- 新增用户 -->
</el-col>
<el-col :span="6">
<!-- VIP 转化率 -->
</el-col>
</el-row>
<!-- 3. 收入趋势图(折线图) -->
<el-card class="chart-card">
<template #header>
<div class="card-header">
<span>收入趋势</span>
<el-radio-group v-model="groupBy" size="small" @change="fetchRevenueTrend">
<el-radio-button label="day">按日</el-radio-button>
<el-radio-button label="week">按周</el-radio-button>
<el-radio-button label="month">按月</el-radio-button>
</el-radio-group>
</div>
</template>
<div ref="revenueChartRef" class="chart-container"></div>
</el-card>
<!-- 4. 用户增长图(柱状图 + 折线图组合) -->
<el-card class="chart-card">
<template #header>用户增长</template>
<div ref="userGrowthChartRef" class="chart-container"></div>
</el-card>
<!-- 5. 转化漏斗(漏斗图) -->
<el-card class="chart-card">
<template #header>转化漏斗</template>
<div ref="funnelChartRef" class="chart-container"></div>
</el-card>
<!-- 6. 分销 Top10(表格) -->
<el-card class="table-card">
<template #header>
<div class="card-header">
<span>推广者 Top10</span>
<el-button type="primary" size="small">查看全部</el-button>
</div>
</template>
<el-table :data="topPromoters" stripe>
<el-table-column prop="nickname" label="用户" />
<el-table-column prop="directCount" label="直接推广" />
<el-table-column prop="indirectCount" label="间接推广" />
<el-table-column prop="totalCommission" label="总佣金(¥)" />
</el-table>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import { analyticsApi } from '@/api/analytics'
// 日期范围
const dateRange = ref<[Date, Date]>()
const groupBy = ref('day')
// 日期快捷选项
const dateShortcuts = [
{ text: '今日', value: () => [new Date(), new Date()] },
{ text: '近 7 天', value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 7)
return [start, end]
}},
{ text: '近 30 天', value: () => {
const end = new Date()
const start = new Date()
start.setTime(start.getTime() - 3600 * 1000 * 24 * 30)
return [start, end]
}},
{ text: '本月', value: () => {
const now = new Date()
const start = new Date(now.getFullYear(), now.getMonth(), 1)
return [start, now]
}},
]
// 核心指标
const summary = ref({
totalRevenue: 0,
totalOrders: 0,
newUsers: 0,
vipConversionRate: 0,
})
// Top 推广者
const topPromoters = ref([])
// 图表实例
const revenueChartRef = ref<HTMLElement>()
const userGrowthChartRef = ref<HTMLElement>()
const funnelChartRef = ref<HTMLElement>()
let revenueChart: echarts.ECharts
let userGrowthChart: echarts.ECharts
let funnelChart: echarts.ECharts
// 获取数据
const fetchData = async () => {
await fetchSummary()
await fetchRevenueTrend()
await fetchUserGrowth()
await fetchConversionFunnel()
await fetchTopPromoters()
}
const fetchSummary = async () => {
const res = await analyticsApi.getSummary({
startDate: dateRange.value?.[0].toISOString(),
endDate: dateRange.value?.[1].toISOString(),
})
summary.value = res.data
}
const fetchRevenueTrend = async () => {
const res = await analyticsApi.getRevenueTrend({
startDate: dateRange.value?.[0].toISOString(),
endDate: dateRange.value?.[1].toISOString(),
groupBy: groupBy.value,
})
// 渲染折线图
const option = {
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: res.data.map((item: any) => item.date),
},
yAxis: {
type: 'value',
axisLabel: { formatter: '¥{value}' },
},
series: [{
type: 'line',
data: res.data.map((item: any) => item.revenue / 100),
smooth: true,
areaStyle: { opacity: 0.3 },
}],
}
revenueChart.setOption(option)
}
const fetchUserGrowth = async () => {
// 类似实现柱状图 + 折线图组合
}
const fetchConversionFunnel = async () => {
// 漏斗图实现
}
const fetchTopPromoters = async () => {
// 表格数据
}
const exportData = () => {
// CSV/Excel 导出逻辑
}
onMounted(() => {
revenueChart = echarts.init(revenueChartRef.value!)
userGrowthChart = echarts.init(userGrowthChartRef.value!)
funnelChart = echarts.init(funnelChartRef.value!)
fetchData()
})
</script>
<style lang="scss" scoped>
.analytics-container {
padding: 20px;
}
.filter-card {
margin-bottom: 20px;
}
.metrics-row {
margin-bottom: 20px;
}
.metric-item {
.label {
font-size: 14px;
color: #909399;
}
.value {
font-size: 24px;
font-weight: bold;
color: #303133;
margin: 8px 0;
}
.trend {
font-size: 12px;
&.positive { color: #67c23a; }
&.negative { color: #f56c6c; }
}
}
.chart-card {
margin-bottom: 20px;
.chart-container {
height: 400px;
}
}
</style>
文件: admin/src/api/analytics.ts
import request from '@/utils/request'
export const analyticsApi = {
/** 核心指标汇总 */
getSummary(params: { startDate?: string; endDate?: string }) {
return request({
url: '/api/admin/analytics/summary',
method: 'post',
data: params,
})
},
/** 收入趋势 */
getRevenueTrend(params: { startDate: string; endDate: string; groupBy: string }) {
return request({
url: '/api/admin/analytics/revenue-trend',
method: 'post',
data: params,
})
},
/** 用户增长 */
getUserGrowth(params: { startDate: string; endDate: string; groupBy: string }) {
return request({
url: '/api/admin/analytics/user-growth',
method: 'post',
data: params,
})
},
/** 转化漏斗 */
getConversionFunnel() {
return request({
url: '/api/admin/analytics/conversion-funnel',
method: 'post',
})
},
/** 分销统计 */
getDistributionStats() {
return request({
url: '/api/admin/analytics/distribution',
method: 'post',
})
},
/** Top 推广者 */
getTopPromoters(limit = 10) {
return request({
url: '/api/admin/analytics/top-promoters',
method: 'post',
data: { limit },
})
},
}
| 任务 | 文件 | 预估工时 |
|---|---|---|
| 后端 | ||
| 创建 AnalyticsService | service/AnalyticsService.java |
4h |
| 实现 getRevenueTrend | 同上 | 2h |
| 实现 getUserGrowthTrend | 同上 | 2h |
| 实现 getSummaryStats | 同上 | 1h |
| AdminController 新增端点 | controller/AdminController.java |
1h |
| 前端 | ||
| 创建 analytics 路由 | router/index.ts |
0.5h |
| 新增 Analytics 页面 | views/analytics/index.vue |
4h |
| API 封装 | api/analytics.ts |
1h |
| 日期范围选择器 | 同上 | 1h |
| 收入趋势折线图 | 同上 | 2h |
| 用户增长柱状图 | 同上 | 2h |
| 合计 | 20.5h ≈ 2.5 天 |
| 任务 | 文件 | 预估工时 |
|---|---|---|
| 后端 | ||
| 实现 getConversionFunnel | service/AnalyticsService.java |
2h |
| 实现 getDistributionStats | 同上 | 2h |
| 实现 getWithdrawStats | 同上 | 1h |
| Repository 扩展方法 | OrderRepository.java, UserRepository.java |
2h |
| 前端 | ||
| 转化漏斗图 | views/analytics/index.vue |
2h |
| 分销 Top10 表格 | 同上 | 1h |
| 数据导出功能(CSV) | 同上 | 2h |
| 合计 | 14h ≈ 1.5 天 |