web-analytics-design.md 19 KB

Web 端统计分析功能设计

基于现有 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(核心统计)

  1. 日期范围筛选(今日/近 7 天/近 30 天/本月/自定义)
  2. 收入趋势折线图(按日/周/月聚合)
  3. 用户增长柱状图(新增用户/VIP 转化)
  4. 订单统计(支付转化率/产品类型分布)

Phase 2(高级分析)

  1. 转化漏斗(注册→付费→升级→能量师)
  2. 分销网络分析(团队层级/佣金分布/Top10 推广者)
  3. 提现统计(提现金额/通道分布/失败率)
  4. 数据导出(CSV/Excel)

三、后端设计

3.1 新增统计 Service

文件: 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 顶部卡片
    }
}

3.2 AdminController 扩展

文件: 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) {
    // 支持日期范围
}

3.3 Repository 扩展(如需要)

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);

四、前端设计

4.1 新增页面路由

文件: 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>

4.2 统计分析页面

文件: 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>

4.3 API 封装

文件: 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 },
    })
  },
}

五、实施计划

Phase 1:核心统计(2-3 天)

任务 文件 预估工时
后端
创建 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 天

Phase 2:高级分析(2-3 天)

任务 文件 预估工时
后端
实现 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 天

六、验收标准

Phase 1

  • 日期范围选择器正常工作(快捷选项 + 自定义)
  • 收入趋势折线图正确显示(单位:元,保留 2 位小数)
  • 用户增长柱状图正确显示(新增用户/VIP 转化分层)
  • 核心指标卡片数据准确(总收入/订单数/新增用户/转化率)
  • 分组维度切换正常(按日/周/月)

Phase 2

  • 转化漏斗图正确显示(注册→付费→升级→能量师)
  • 分销 Top10 表格数据准确
  • 数据导出功能正常(CSV 格式)
  • 提现统计图表正确(通道分布/成功率)

七、技术注意事项

  1. 金额单位转换:后端返回分(Integer),前端需除以 100 显示为元
  2. 时区处理:所有时间使用 ISO 8601 格式,后端解析为 LocalDateTime
  3. 性能优化:大数据量时使用数据库聚合(GROUP BY),避免内存计算
  4. 缓存策略:统计数据可缓存 5-10 分钟,避免频繁查询
  5. 图表响应式:ECharts 实例需监听窗口 resize 事件
  6. 空数据处理:无数据时显示友好提示,不渲染空图表

八、扩展方向(未来)

  1. 实时数据:WebSocket 推送实时订单/用户数据
  2. 自定义报表:用户自选指标 + 图表类型
  3. 预警系统:收入/用户异常波动告警
  4. A/B 测试分析:不同定价策略转化对比
  5. RFM 模型:用户价值分层分析