bindbox-game/.claude/plan/channel-stats-frontend.md
win 8d1eef2f7f fix(channel): 修复渠道统计GMV重复计数和商城直购误计入
1. 排除商城直购(source_type=1):GMV和成本过滤条件从IN(1,2,3,4)改为IN(2,3,4)
2. 排除次卡免费使用订单(actual_amount=0):避免购买次卡和使用次卡双重计入GMV
   - source_type=4 一番赏使用次卡:1578单 44032元重复
   - source_type=3 对对碰使用次卡:422单 7042元重复
   - 合计去除51074元虚增GMV(29.1%)
3. 成本过滤条件同步修正:source_type IN(2,3,4),total_amount>0

修正后:GMV从175600降至124527元,毛利率从37.4%回到真实的11.8%
2026-03-16 21:41:39 +08:00

7.4 KiB
Raw Permalink Blame History

渠道统计 — 前端盈亏展示

📋 实施计划:渠道统计页面新增成本/盈亏展示

任务类型

  • 前端
  • 后端
  • 全栈

需求概述

后端 /admin/channels/:id/stats 接口已新增以下字段:

Overview 新增

字段 类型 说明
total_cost_cents number 总成本(分)
total_profit_cents number 盈亏(分) = paid - cost
total_cost number 总成本(元)
total_profit number 盈亏(元)

趋势图每日新增

字段 类型 说明
cost_cents number 当日成本(分)
profit_cents number 当日盈亏(分)

技术方案

UI 设计

Overview 区域

  • 现有 3 个卡片(用户、订单、实付金额)→ 扩展为 5 个卡片
  • 新增:总成本 卡片 + 盈亏 卡片
  • 布局:从 grid-cols-3 改为 grid-cols-5(或在移动端自适应 grid-cols-2 md:grid-cols-5
  • 盈亏卡片需根据正/负值显示不同颜色(盈利=绿色,亏损=红色)

趋势图区域

  • 现有 2 个 Tab用户增长、付费数据→ 新增第 3 个 Tab盈亏分析
  • 盈亏分析 Tab 包含 3 条曲线:实付金额、成本、盈亏
  • 盈亏曲线可使用虚线区分

实施步骤

Step 1: 更新 TypeScript 类型定义

文件web/admin/src/api/channels.ts

StatsOverview 接口新增:

export interface StatsOverview {
  total_users: number
  total_orders: number
  total_gmv: number
  total_paid_cents?: number
  // 新增
  total_cost_cents?: number    // 总成本(分)
  total_profit_cents?: number  // 盈亏(分)
  total_cost?: number          // 总成本(元)
  total_profit?: number        // 盈亏(元)
}

StatsDailyItem 接口新增:

export interface StatsDailyItem {
  date: string
  user_count: number
  order_count: number
  gmv: number
  paid_cents?: number
  // 新增
  cost_cents?: number    // 当日成本(分)
  profit_cents?: number  // 当日盈亏(分)
}

Step 2: 更新 Overview 卡片区域

文件web/admin/src/views/operations/channels/index.vue

2.1 布局从 grid-cols-3 改为 grid-cols-5

2.2 新增两个 ArtStatsCard

<!-- 总成本 -->
<ArtStatsCard
    title="总成本"
    :count="totalCostYuan"
    :decimals="2"
    icon="ri:funds-line"
    box-style="bg-purple-50"
    text-color="#7C3AED"
    icon-style="bg-purple-500"
    description="总奖品成本"
/>

<!-- 盈亏 -->
<ArtStatsCard
    title="盈亏"
    :count="totalProfitYuan"
    :decimals="2"
    icon="ri:bar-chart-2-line"
    :box-style="profitCardStyle"
    :text-color="profitTextColor"
    :icon-style="profitIconStyle"
    :description="profitDescription"
/>

2.3 新增 computed 属性:

const totalCostYuan = computed(() => {
    const cents = statsData.value.overview.total_cost_cents
    if (typeof cents === 'number') {
        return Number((cents / 100).toFixed(2))
    }
    return 0
})

const totalProfitYuan = computed(() => {
    const cents = statsData.value.overview.total_profit_cents
    if (typeof cents === 'number') {
        return Number((cents / 100).toFixed(2))
    }
    return 0
})

const profitCardStyle = computed(() =>
    totalProfitYuan.value >= 0 ? 'bg-green-50' : 'bg-red-50'
)

const profitTextColor = computed(() =>
    totalProfitYuan.value >= 0 ? '#10B981' : '#EF4444'
)

const profitIconStyle = computed(() =>
    totalProfitYuan.value >= 0 ? 'bg-green-500' : 'bg-red-500'
)

const profitDescription = computed(() =>
    totalProfitYuan.value >= 0 ? '盈利' : '亏损'
)

Step 3: 更新趋势图 Tab

文件web/admin/src/views/operations/channels/index.vue

3.1el-radio-group 新增 Tab

<el-radio-group v-model="statsTab" size="small">
    <el-radio-button label="growth">用户增长</el-radio-button>
    <el-radio-button label="revenue">付费数据</el-radio-button>
    <el-radio-button label="profit">盈亏分析</el-radio-button>
</el-radio-group>

3.2chartData computed 中新增 profit 分支:

const chartData = computed(() => {
    if (statsTab.value === 'growth') {
        return [
            { name: '新增用户', data: statsData.value.daily.map(i => i.user_count), smooth: true, color: '#409EFF' }
        ]
    } else if (statsTab.value === 'revenue') {
        return [
            { name: '订单数', data: statsData.value.daily.map(i => i.order_count), smooth: true, color: '#67C23A' },
            { name: '实付金额', data: statsData.value.daily.map(i => getDailyPaidYuan(i)), smooth: true, color: '#E6A23C' }
        ]
    } else {
        // profit tab
        return [
            { name: '实付(元)', data: statsData.value.daily.map(i => getDailyPaidYuan(i)), smooth: true, color: '#E6A23C' },
            { name: '成本(元)', data: statsData.value.daily.map(i => getDailyCostYuan(i)), smooth: true, color: '#7C3AED' },
            { name: '盈亏(元)', data: statsData.value.daily.map(i => getDailyProfitYuan(i)), smooth: true, color: '#10B981' }
        ]
    }
})

3.3 新增辅助函数:

function getDailyCostYuan(item: { cost_cents?: number }) {
    if (typeof item.cost_cents === 'number') {
        return Number((item.cost_cents / 100).toFixed(2))
    }
    return 0
}

function getDailyProfitYuan(item: { profit_cents?: number }) {
    if (typeof item.profit_cents === 'number') {
        return Number((item.profit_cents / 100).toFixed(2))
    }
    return 0
}

Step 4: 更新 statsData 初始值

文件web/admin/src/views/operations/channels/index.vue

const statsData = ref<ChannelStatsRes>({
    overview: {
        total_users: 0, total_orders: 0, total_gmv: 0, total_paid_cents: 0,
        total_cost_cents: 0, total_profit_cents: 0, total_cost: 0, total_profit: 0
    },
    daily: []
})

关键文件

文件 操作 说明
web/admin/src/api/channels.ts:L68-86 修改 扩展 StatsOverview 和 StatsDailyItem 接口
web/admin/src/views/operations/channels/index.vue:L155-184 修改 Overview 卡片区域新增成本/盈亏卡
web/admin/src/views/operations/channels/index.vue:L202-205 修改 趋势图新增盈亏分析 Tab
web/admin/src/views/operations/channels/index.vue:L482-558 修改 新增 computed 属性和辅助函数

风险与缓解

风险 严重程度 缓解措施
5列卡片在窄屏溢出 使用响应式 grid-cols-2 md:grid-cols-5,必要时改为 grid-cols-3 + 第二行 grid-cols-2
后端字段为空(旧数据) 已解决 所有新字段使用 ? 可选computed 中做 typeof 检查,默认 0
ArtStatsCard 不支持负数展示 ArtCountTo 组件底层支持负数(基于 countUp.js无需额外处理
盈亏曲线可能有负值 ECharts 原生支持负值 Y 轴,图表会自动适配

验收标准

  • TypeScript 类型定义包含新字段
  • Overview 展示 5 个卡片(用户、订单、实付、成本、盈亏)
  • 盈亏卡片根据正/负值动态切换颜色(绿/红)
  • 趋势图新增"盈亏分析"Tab
  • 盈亏分析 Tab 展示 3 条曲线(实付、成本、盈亏)
  • pnpm build 编译通过
  • pnpm type-check 类型检查通过

SESSION_ID供 /ccg:execute 使用)

  • CODEX_SESSION: N/A
  • GEMINI_SESSION: N/A