【实用程序】电商销售分析仪表盘 — 从零搭建一个AI参与的全栈数据洞察系统

本项目本站内源代码下载地址

快速搭建一个电商数据分析看板,却苦于后端数据准备、清洗逻辑、图表展示和 AI 解读的繁琐流程?本项目就是一个开箱即用 的全栈解决方案------基于 Next.js 16 构建,涵盖了数据生成、自动清洗、多维度可视化分析,甚至集成了兼容 OpenAI 的大模型智能解读。最重要的是,它无需任何外部数据库,启动即用,所有数据通过内存模拟生成且每次运行结果一致,非常适合演示、教学或作为真实项目的前置原型。

接下来,将一步步拆解这个仪表盘的设计思想、核心原理、技术选型、部署方式,并在关键环节配上流程图和架构图,让你不仅会用,更懂得为什么这么设计。


1. 项目概览:

  • 模拟真实数据:自动生成 3000 条订单(含价格≤0、数量≤0 的异常值)和 500 条用户资料(含缺失年龄),每次请求数据完全相同,便于调试和演示。
  • 智能数据清洗:过滤无效订单 → 计算年龄中位数 → 填充缺失年龄 → 订单与用户左连接,最终得到高质量分析宽表。
  • 四大分析维度:月度/季度趋势、品类×地区交叉分析、RFM 用户价值分群、年龄分布与消费相关性。
  • 十多种图表:柱状图、折线图、饼图、箱线图、组合图、热力图等,全部用 Recharts 实现。
  • AI 智能分析:支持任何 OpenAI 兼容 API,提供 5 种分析预设(整体概览、品类深度、RFM 用户、趋势预测、自定义提问),并实时流式响应(SSE)。
  • 简单部署:Next.js standalone 输出 + Caddy 反向代理 + SQLite 持久化(可选),一键构建打包。

接下来,我们从最上层的系统架构开始,逐步深入到每一块的设计细节。


2. 系统架构:

下图展示了从用户浏览器到后端服务的完整请求链路:
#mermaid-svg-dbipAg6NIqRFrOae{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-dbipAg6NIqRFrOae .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-dbipAg6NIqRFrOae .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-dbipAg6NIqRFrOae .error-icon{fill:#552222;}#mermaid-svg-dbipAg6NIqRFrOae .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-dbipAg6NIqRFrOae .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-dbipAg6NIqRFrOae .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-dbipAg6NIqRFrOae .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-dbipAg6NIqRFrOae .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-dbipAg6NIqRFrOae .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-dbipAg6NIqRFrOae .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-dbipAg6NIqRFrOae .marker{fill:#333333;stroke:#333333;}#mermaid-svg-dbipAg6NIqRFrOae .marker.cross{stroke:#333333;}#mermaid-svg-dbipAg6NIqRFrOae svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-dbipAg6NIqRFrOae p{margin:0;}#mermaid-svg-dbipAg6NIqRFrOae .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-dbipAg6NIqRFrOae .cluster-label text{fill:#333;}#mermaid-svg-dbipAg6NIqRFrOae .cluster-label span{color:#333;}#mermaid-svg-dbipAg6NIqRFrOae .cluster-label span p{background-color:transparent;}#mermaid-svg-dbipAg6NIqRFrOae .label text,#mermaid-svg-dbipAg6NIqRFrOae span{fill:#333;color:#333;}#mermaid-svg-dbipAg6NIqRFrOae .node rect,#mermaid-svg-dbipAg6NIqRFrOae .node circle,#mermaid-svg-dbipAg6NIqRFrOae .node ellipse,#mermaid-svg-dbipAg6NIqRFrOae .node polygon,#mermaid-svg-dbipAg6NIqRFrOae .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-dbipAg6NIqRFrOae .rough-node .label text,#mermaid-svg-dbipAg6NIqRFrOae .node .label text,#mermaid-svg-dbipAg6NIqRFrOae .image-shape .label,#mermaid-svg-dbipAg6NIqRFrOae .icon-shape .label{text-anchor:middle;}#mermaid-svg-dbipAg6NIqRFrOae .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-dbipAg6NIqRFrOae .rough-node .label,#mermaid-svg-dbipAg6NIqRFrOae .node .label,#mermaid-svg-dbipAg6NIqRFrOae .image-shape .label,#mermaid-svg-dbipAg6NIqRFrOae .icon-shape .label{text-align:center;}#mermaid-svg-dbipAg6NIqRFrOae .node.clickable{cursor:pointer;}#mermaid-svg-dbipAg6NIqRFrOae .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-dbipAg6NIqRFrOae .arrowheadPath{fill:#333333;}#mermaid-svg-dbipAg6NIqRFrOae .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-dbipAg6NIqRFrOae .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-dbipAg6NIqRFrOae .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dbipAg6NIqRFrOae .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-dbipAg6NIqRFrOae .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dbipAg6NIqRFrOae .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-dbipAg6NIqRFrOae .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-dbipAg6NIqRFrOae .cluster text{fill:#333;}#mermaid-svg-dbipAg6NIqRFrOae .cluster span{color:#333;}#mermaid-svg-dbipAg6NIqRFrOae div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-dbipAg6NIqRFrOae .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-dbipAg6NIqRFrOae rect.text{fill:none;stroke-width:0;}#mermaid-svg-dbipAg6NIqRFrOae .icon-shape,#mermaid-svg-dbipAg6NIqRFrOae .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-dbipAg6NIqRFrOae .icon-shape p,#mermaid-svg-dbipAg6NIqRFrOae .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-dbipAg6NIqRFrOae .icon-shape .label rect,#mermaid-svg-dbipAg6NIqRFrOae .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-dbipAg6NIqRFrOae .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-dbipAg6NIqRFrOae .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-dbipAg6NIqRFrOae :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP / SSE
默认转发
XTransformPort 动态路由
读取/写入
流式请求
SSE 流式响应
用户浏览器

React 19 SPA
Caddy 反向代理

监听 :81
Next.js 16 Server

监听 localhost:3000
API Routes

/api/analysis

/api/ai/test

/api/ai/analyze
数据清洗与分析引擎

内存模拟数据生成

RFM/箱线图/相关性
SQLite 数据库

可选持久化
外部 LLM API

OpenAI 兼容

核心要点

  • Caddy 作为反向代理,统一接收前端请求。它支持通过查询参数 XTransformPort 动态转发到不同本地服务(为 WebSocket 等扩展预留)。
  • Next.js 承担全栈角色:既提供前端页面(page.tsx),又通过 API Route 提供数据分析与 AI 接口。
  • 所有分析数据在内存中实时生成并计算,不依赖数据库,但同时也保留了 Prisma + SQLite 的扩展能力,随时可以切换为真实数据库。

3. 数据如何流动?

数据从原始生成到最终输出 JSON 的完整流水线如下:
#mermaid-svg-kQ2EHXFjaMLfO8Wq{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .error-icon{fill:#552222;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .marker{fill:#333333;stroke:#333333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .marker.cross{stroke:#333333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq p{margin:0;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .cluster-label text{fill:#333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .cluster-label span{color:#333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .cluster-label span p{background-color:transparent;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .label text,#mermaid-svg-kQ2EHXFjaMLfO8Wq span{fill:#333;color:#333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .node rect,#mermaid-svg-kQ2EHXFjaMLfO8Wq .node circle,#mermaid-svg-kQ2EHXFjaMLfO8Wq .node ellipse,#mermaid-svg-kQ2EHXFjaMLfO8Wq .node polygon,#mermaid-svg-kQ2EHXFjaMLfO8Wq .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .rough-node .label text,#mermaid-svg-kQ2EHXFjaMLfO8Wq .node .label text,#mermaid-svg-kQ2EHXFjaMLfO8Wq .image-shape .label,#mermaid-svg-kQ2EHXFjaMLfO8Wq .icon-shape .label{text-anchor:middle;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .rough-node .label,#mermaid-svg-kQ2EHXFjaMLfO8Wq .node .label,#mermaid-svg-kQ2EHXFjaMLfO8Wq .image-shape .label,#mermaid-svg-kQ2EHXFjaMLfO8Wq .icon-shape .label{text-align:center;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .node.clickable{cursor:pointer;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .arrowheadPath{fill:#333333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-kQ2EHXFjaMLfO8Wq .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kQ2EHXFjaMLfO8Wq .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-kQ2EHXFjaMLfO8Wq .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .cluster text{fill:#333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .cluster span{color:#333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-kQ2EHXFjaMLfO8Wq rect.text{fill:none;stroke-width:0;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .icon-shape,#mermaid-svg-kQ2EHXFjaMLfO8Wq .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .icon-shape p,#mermaid-svg-kQ2EHXFjaMLfO8Wq .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .icon-shape .label rect,#mermaid-svg-kQ2EHXFjaMLfO8Wq .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-kQ2EHXFjaMLfO8Wq .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-kQ2EHXFjaMLfO8Wq .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-kQ2EHXFjaMLfO8Wq :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 分析阶段
清洗阶段
生成阶段
生成原始订单

3000条, 含异常
生成原始用户

500条, 含缺失年龄
Step 1: 过滤无效订单

price>0 & quantity>0
Step 2: 计算年龄中位数
Step 3: 用中位数填充缺失年龄
Step 4: 左连接订单与用户
整体概览 KPI
月度/季度趋势 + 环比
品类 / 地区 / 交叉表
箱线图五数概括
RFM 用户分群
年龄分布 & 相关性矩阵
返回完整 JSON

清洗逻辑原理解释

  • 为什么先过滤再填充? 因为异常订单(价格或数量≤0)本身是错误数据,不应该参与后续任何统计,包括计算中位数。先过滤能保证后续步骤基于"干净"的数据集。
  • 年龄缺失填充为何用中位数而不是均值? 年龄分布往往右偏(年轻人多),中位数更能代表"典型用户",且不受极端值影响。本项目动态计算所有非空年龄的中位数,确保填充值符合当前数据分布。
  • 左连接的目的 :保留所有订单,即使找不到对应用户(实际数据中每个订单都有有效 userId,但扩展性考虑)。连接后增加 totalAmount = price × quantity 作为核心消费指标。

4. 技术栈:

层级 技术 版本 选择理由
框架 Next.js 16.1.1 App Router 支持服务端组件与 API 路由同项目,standalone 输出部署极简。
前端库 React 19.0.0 客户端组件 (use client) 保证图表和交互流畅。
样式 Tailwind CSS + shadcn/ui 4.x / new-york 原子化 CSS 快速构建一致界面,shadcn 提供高质量无头组件。
图表 Recharts 2.15.4 声明式 React 图表,与 React 生命周期完美结合。
动画 Framer Motion 12.23.2 实现 KPI 卡片交错入场等细腻动效。
ORM Prisma + SQLite 6.11.1 轻量嵌入式数据库,无外部依赖;Prisma 提供类型安全的数据库操作,未来可无缝切换 PostgreSQL。
运行时 Bun 1.3.4 比 Node.js 快 4 倍以上,自带包管理器和打包器,兼容 npm 生态。
反向代理 Caddy 2.x 自动 HTTPS、配置极简,支持动态端口转发。
AI 集成 OpenAI 兼容 API - 标准 SSE 流式接口,可接入任何大模型服务。
状态管理 zustand 5.0.6 极简 API,管理 AI 配置、分析历史等前端状态。

5. 目录结构:

复制代码
电商销售分析仪表盘/
├── .zscripts/                # 构建与启动脚本(build.sh, start.sh, dev.sh)
├── agent-ctx/                # Agent 上下文描述文档
├── db/                       # SQLite 数据库文件(生产环境)
├── examples/websocket/       # WebSocket 扩展示例(Socket.IO)
├── mini-services/            # 预留微服务目录
├── prisma/
│   └── schema.prisma         # 数据库 Schema(用户/文章模型框架)
├── public/                   # 静态资源(logo, robots.txt)
├── src/
│   ├── app/
│   │   ├── api/
│   │   │   ├── analysis/route.ts    # 数据生成+清洗+分析 API
│   │   │   └── ai/
│   │   │       ├── test/route.ts    # LLM 连接测试
│   │   │       └── analyze/route.ts # 流式 AI 分析(SSE)
│   │   ├── globals.css
│   │   ├── layout.tsx
│   │   └── page.tsx                  # 主页面(5个 Tab)
│   ├── components/ui/               # 50+ shadcn 组件
│   ├── hooks/                       # use-mobile, use-toast
│   └── lib/                         # db.ts(Prisma 单例), utils.ts
├── Caddyfile                        # Caddy 反向代理配置
├── next.config.ts
├── package.json
└── tailwind.config.ts

关键点 :所有前端 UI 代码全部集中在 page.tsx 中。这样做虽然文件较大,但减少了网络请求(所有数据一次获取),保证多个 Tab 的数据一致性,也方便快速迭代。如果项目进一步膨胀,可以按 Tab 拆分为独立组件。


6. 核心模块深度解析

6.1 数据生成模块:

/api/analysis/route.ts 中,我们使用种子随机数(seeded random)来生成订单和用户数据。例如:

typescript 复制代码
const seed = 12345;
const rng = () => { /* 确定性的伪随机函数 */ };

这样做的好处是:无论在开发、测试还是演示环境,每次刷新页面或调用 API 都会得到完全相同的数据集(包括异常值的数量和位置)。这避免了"随机性"导致的难以复现问题,也让截图和文档保持一致。

订单数据特点

  • 3000 条订单,日期覆盖 2024 全年。
  • 6 个品类,每个品类下 6 个商品,价格区间差异明显(电子产品 199~8999 元,图书文具 9~299 元)。
  • 故意注入异常:约 3% 订单 price ≤ 0,约 2% 订单 quantity ≤ 0,用于测试清洗逻辑。

用户数据特点

  • 500 人,年龄 18~64 岁。
  • 约 8% 用户年龄为 null,模拟真实场景中的字段缺失。

6.2 清洗模块:处理异常与缺失的完整逻辑

清洗流程严格按照以下顺序(不可颠倒):

  1. 过滤订单 :只保留 price > 0 && quantity > 0 的记录。这一步移除了约 150 条无效订单。
  2. 计算年龄中位数 :从所有有效年龄 (非 null)的用户中取中位数。例如,若有效年龄为 [22, 24, 35, 35, 40],中位数为 35。
  3. 填充缺失年龄 :将所有 agenull 的用户行设置为该中位数。
  4. 左连接 :以 userId 为键,将用户表的年龄、性别、城市附加到订单表,并计算 totalAmount = price * quantity

为什么中位数不在过滤订单之前计算? 因为年龄中位数只与用户表有关,而用户表独立于订单表,所以先后顺序不影响。但放在过滤之后更符合"先处理各自表内异常"的原则。

6.3 分析引擎:每个指标背后的计算方法

月度趋势 + 环比增长率
  • 按月份(1-12)聚合所有订单的 totalAmount
  • 环比增长率 = (本月销售额 - 上月销售额) / 上月销售额 × 100%。首月(1月)环比为 null,前端不显示。
RFM 模型
  • R(Recency):最近一次订单日期距离 2024-12-31 的天数(越近越好)。
  • F(Frequency):该用户的订单总数。
  • M(Monetary):该用户的累计消费金额。
  • 对每个维度按数值大小分为 5 档(五分位数),得到 1~5 分。
  • 总分 = R分 + F分 + M分(范围 3~15),然后按阈值分群:
    • ≥12 → 高价值用户(绿色)
    • 9~11 → 潜力用户(蓝色)
    • 6~8 → 一般用户(橙色)
    • ≤5 → 流失风险用户(红色)
箱线图(五数概括)
  • 按品类分组,对每个品类内的订单金额排序。
  • 计算:最小值、第一四分位数(Q1,25%)、中位数(50%)、第三四分位数(Q3,75%)、最大值,以及均值。
  • 使用线性插值法 计算分位数((n+1)*p 位置),避免简单取整带来的偏差。
相关性矩阵

选取 5 个特征:消费金额、购买频次、最近购买间隔、年龄、平均客单价。计算两两之间的皮尔逊相关系数,生成 5×5 热力图数据。前端使用颜色深浅表示相关性强弱,正相关为青色,负相关为红色。

6.4 前端仪表盘:5 个 Tab 的内容与设计

页面采用 Tabs 组件组织,每个 Tab 独立且共享同一份从 /api/analysis 获取的数据。
#mermaid-svg-UwK9847DIvpX99I9{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UwK9847DIvpX99I9 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UwK9847DIvpX99I9 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UwK9847DIvpX99I9 .error-icon{fill:#552222;}#mermaid-svg-UwK9847DIvpX99I9 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UwK9847DIvpX99I9 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UwK9847DIvpX99I9 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UwK9847DIvpX99I9 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UwK9847DIvpX99I9 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UwK9847DIvpX99I9 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UwK9847DIvpX99I9 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UwK9847DIvpX99I9 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UwK9847DIvpX99I9 .marker.cross{stroke:#333333;}#mermaid-svg-UwK9847DIvpX99I9 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UwK9847DIvpX99I9 p{margin:0;}#mermaid-svg-UwK9847DIvpX99I9 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UwK9847DIvpX99I9 .cluster-label text{fill:#333;}#mermaid-svg-UwK9847DIvpX99I9 .cluster-label span{color:#333;}#mermaid-svg-UwK9847DIvpX99I9 .cluster-label span p{background-color:transparent;}#mermaid-svg-UwK9847DIvpX99I9 .label text,#mermaid-svg-UwK9847DIvpX99I9 span{fill:#333;color:#333;}#mermaid-svg-UwK9847DIvpX99I9 .node rect,#mermaid-svg-UwK9847DIvpX99I9 .node circle,#mermaid-svg-UwK9847DIvpX99I9 .node ellipse,#mermaid-svg-UwK9847DIvpX99I9 .node polygon,#mermaid-svg-UwK9847DIvpX99I9 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UwK9847DIvpX99I9 .rough-node .label text,#mermaid-svg-UwK9847DIvpX99I9 .node .label text,#mermaid-svg-UwK9847DIvpX99I9 .image-shape .label,#mermaid-svg-UwK9847DIvpX99I9 .icon-shape .label{text-anchor:middle;}#mermaid-svg-UwK9847DIvpX99I9 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UwK9847DIvpX99I9 .rough-node .label,#mermaid-svg-UwK9847DIvpX99I9 .node .label,#mermaid-svg-UwK9847DIvpX99I9 .image-shape .label,#mermaid-svg-UwK9847DIvpX99I9 .icon-shape .label{text-align:center;}#mermaid-svg-UwK9847DIvpX99I9 .node.clickable{cursor:pointer;}#mermaid-svg-UwK9847DIvpX99I9 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UwK9847DIvpX99I9 .arrowheadPath{fill:#333333;}#mermaid-svg-UwK9847DIvpX99I9 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UwK9847DIvpX99I9 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UwK9847DIvpX99I9 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UwK9847DIvpX99I9 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UwK9847DIvpX99I9 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UwK9847DIvpX99I9 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UwK9847DIvpX99I9 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UwK9847DIvpX99I9 .cluster text{fill:#333;}#mermaid-svg-UwK9847DIvpX99I9 .cluster span{color:#333;}#mermaid-svg-UwK9847DIvpX99I9 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UwK9847DIvpX99I9 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UwK9847DIvpX99I9 rect.text{fill:none;stroke-width:0;}#mermaid-svg-UwK9847DIvpX99I9 .icon-shape,#mermaid-svg-UwK9847DIvpX99I9 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UwK9847DIvpX99I9 .icon-shape p,#mermaid-svg-UwK9847DIvpX99I9 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UwK9847DIvpX99I9 .icon-shape .label rect,#mermaid-svg-UwK9847DIvpX99I9 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UwK9847DIvpX99I9 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UwK9847DIvpX99I9 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UwK9847DIvpX99I9 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} page.tsx 主页面
shadcn Tabs
数据总览
销售分析
用户分析
数据清洗
AI 分析
4个KPI卡片: 销售额/订单数/用户数/客单价
月度趋势组合图: Bar+Line+环比
季度柱状图
品类销售额水平柱状图
地区销售环形饼图
品类订单金额箱线图
地区×品类交叉热力表
RFM分群饼图
年龄分布与消费组合图
高价值用户列表Top20
相关性热力图
5步清洗流程可视化
数据质量指标进度条
清洗后样本数据表
LLM配置面板: URL/Key/Model + 测试
5种分析预设按钮
自定义提问框
流式响应显示 + Markdown渲染
最近10次分析历史

设计规范速览

  • 主色调 #3AAFA9(Teal Cyan)用于强调和图标背景。
  • 图表色板采用 Paul Tol 色板#0077BB, #33BBEE, #009988 等),对色盲友好且专业。
  • RFM 分群配色:高价值 #009988,潜力 #0077BB,一般 #EE7733,流失 #CC3311
  • 响应式布局:移动端 1 列 → 平板 2 列 → 桌面 4 列(使用 Tailwind grid)。

6.5 AI 分析模块:让大模型帮你解读数据

工作流程
  1. 用户在前端配置 LLM 的 API URL、Key 和模型 ID(配置保存到 localStorage)。
  2. 点击"测试连接",调用 POST /api/ai/test 验证配置是否正确。
  3. 选择分析预设(或自定义提问),前端将当前仪表盘的数据摘要化为一个结构化的 Markdown 文本(包含 KPI、月度趋势、品类排名、RFM 分群等)。
  4. 发送 POST /api/ai/analyze 请求,携带配置、分析类型和数据摘要。
  5. 后端根据分析类型构造 system prompt 和 user prompt,调用 LLM 的流式 API,并将响应的每个 chunk 通过 SSE 推送到前端。
  6. 前端使用 react-markdown 实时渲染,并自动滚动到底部。
为什么需要数据摘要而不是直接把原始 JSON 给 AI?
  • Token 限制:原始 JSON 可能很大(包含所有订单明细),直接发送会超出上下文窗口且浪费成本。
  • 聚焦关键:我们只提取已经聚合好的统计结果,让 AI 直接进行业务解读,而不是让它做计算。
  • 格式友好:Markdown 表格和列表更容易被 LLM 理解。
五种预设的 Prompt 设计思路
类型 System Prompt 角色 User Prompt 重点
overview 资深电商数据分析专家 核心指标解读、环比变化、整体健康度、行动建议
category 电商品类管理专家 品类排名、结构占比、交叉洞察、增长机会
rfm 用户运营专家 用户分层、高价值用户画像、流失预警、转化策略
trend 时间序列分析专家 季节性规律、关键拐点、未来趋势预判
custom 同 overview 直接提问,不加额外模板

模型参数固定为 temperature: 0.7(保持一定创意但不离谱),max_tokens: 4096,超时 120 秒。


7. 部署方案:从开发到生产的完整路径

7.1 开发环境(本地调试)

前置条件:安装 Bun(>=1.3.4)

bash 复制代码
# 一键启动(推荐)
bash .zscripts/dev.sh

# 或手动执行
bun install
bun run db:push   # 初始化 SQLite schema
bun run dev       # 启动 Next.js 在 http://localhost:3000

dev.sh 脚本还会自动启动 mini-services(如有)并进行健康检查,确保所有依赖就绪。

7.2 生产构建:standalone 打包

执行 bash .zscripts/build.sh,构建脚本会:

  1. 安装依赖并执行 next build
  2. 复制 .next/staticpublic.next/standalone 目录下。
  3. 将所有产物(standalone 文件夹 + db 文件 + Caddyfile + 启动脚本)打包成一个 tar.gz
  4. 生成唯一的构建 ID,例如 /tmp/build_fullstack_xxx.tar.gz

为什么使用 standalone 模式? 它只包含运行所需的必要文件(不含 node_modules 中的开发依赖),体积小,且可以直接用 node server.js 启动,不依赖 next 命令行。

7.3 生产运行:Caddy + Next.js 组合

解压构建包到目标目录(如 /app),设置环境变量:

bash 复制代码
export PORT=3000
export HOSTNAME=0.0.0.0
export DATABASE_URL="file:/app/db/custom.db"
bash /app/start.sh

start.sh 会依次启动:

  • Next.js 服务器(bun server.js)作为后台进程。
  • 可选的 mini-services。
  • 前台运行 Caddy(caddy run --config Caddyfile),确保 Caddy 退出时整个服务停止。

Caddyfile 配置解析

caddyfile 复制代码
:81 {
    @transform_port_query {
        query XTransformPort=*
    }
    handle @transform_port_query {
        reverse_proxy localhost:{query.XTransformPort}
    }
    handle {
        reverse_proxy localhost:3000
    }
}
  • 监听 81 端口(避免与常见 Web 服务冲突)。
  • 如果请求携带 ?XTransformPort=3003,则动态转发到 localhost:3003(例如 WebSocket 服务)。
  • 其他所有请求默认转发到 Next.js 的 3000 端口。

7.4 数据库管理(可选持久化)

虽然核心分析不依赖数据库,但项目保留了 Prisma Schema 用于扩展。常用命令:

bash 复制代码
bun run db:push     # 将 schema 同步到数据库(无迁移文件)
bun run db:generate # 生成 Prisma Client
bun run db:migrate  # 创建迁移文件
bun run db:reset    # 重置数据库

8. 关键设计决策背后的原理

8.1 为什么所有数据都在内存中生成,而非真实数据库?

  • 零配置启动 :用户 clone 项目后,只需 bun install && bun run dev 即可看到完整仪表盘,无需安装 MySQL/PostgreSQL。
  • 确定性:seeded random 保证每次运行结果一致,方便对比不同版本的前端或 AI 提示词效果。
  • 演示友好:在任何地方(包括在线代码沙盒)都能直接运行,无需额外服务。
  • 可切换:一旦需要真实数据,只需修改 API 路由中的数据源为数据库查询,Prisma 已做好类型支持。

8.2 为什么选择 Next.js standalone 输出?

  • 最小化部署 :standalone 目录只包含必要的代码,不会把整个 node_modules 打包进去,镜像或压缩包体积小。
  • 独立运行 :生产环境不需要 next CLI,直接用 Node/Bun 执行 server.js,减少了依赖和潜在版本冲突。
  • 容器友好:适合 Docker 多阶段构建,最终镜像可以控制在 200MB 以内。

8.3 Caddy 相比 Nginx 的优势是什么?

  • 自动 HTTPS:Caddy 默认自动从 Let's Encrypt 获取证书,无需手动配置 certbot。
  • 配置极简:几十行配置就能实现反向代理、动态路由、负载均衡,而 Nginx 需要更多指令。
  • 动态端口转发XTransformPort 特性允许前端通过参数选择后端服务端口,非常适合微服务或 WebSocket 网关场景。

8.4 Bun 比 Node.js 更适合这个项目吗?

  • 启动速度:Bun 启动 Next.js 开发服务器比 Node.js 快约 4 倍,提升开发体验。
  • 一体化 :不需要单独安装 npmnpx,Bun 自带包管理器和运行器。
  • 兼容性:可以无缝运行现有的 Next.js 项目,所有 Node.js API 都支持。

8.5 为什么将全部 UI 代码塞进一个 page.tsx

  • 减少请求 :所有图表共享一次 /api/analysis 请求,避免每个 Tab 单独加载数据导致不一致。
  • 状态简单 :数据只需 useEffect 一次,全局可用,无需跨组件传递或使用复杂状态库。
  • 便于快速迭代:在项目初期,集中式开发可以更快调整布局。当代码超过 2000 行后,可随时按 Tab 拆分为独立组件,重构成本低。

9. 性能与体验优化要点

前端层面

  • 条件渲染:Tabs 内容只有在对应 Tab 被激活时才渲染 DOM,减少初始加载开销。
  • 骨架屏 :数据加载时显示 Skeleton 组件,避免白屏闪烁。
  • 滚动条美化 :表格区域使用 scrollbar-width: thin 优化视觉效果。
  • 字体优化 :通过 next/font 加载 Geist 字体,自动子集化,减少首屏 CSS 体积。

后端层面

  • 单次聚合:所有分析计算在一次 API 调用中完成,避免多次数据库或内存操作。
  • 内存操作:数据清洗和分析全程在 Node/Bun 内存中进行,无磁盘 I/O(除了可选的 SQLite 读/写)。
  • 流式 AI:SSE 流式传输让用户更早看到部分响应,避免长时间等待一个完整的 JSON。
  • 超时控制:AI 分析请求设置 120 秒超时,并支持前端中断。

部署层面

  • Standalone 输出:减少最终部署包的体积,加快传输和启动。
  • gzip 压缩 :构建脚本自动将产物打包为 .tar.gz
  • Caddy 静态资源缓存 :可配置 Cache-Control 头来缓存图表库和字体文件。

10. 常见问题与排查

问题现象 常见原因 解决方法
bun: command not found Bun 未安装 `curl -fsSL https://bun.sh/install
端口 3000 已被占用 其他进程(如旧 Next 实例) lsof -i :3000kill 相应进程
页面空白且控制台报错 /api/analysis 返回非 200 检查终端日志,确认数据生成是否抛出异常
生产环境 server.js 找不到 未执行 bun run build 或构建不完整 重新运行 bash .zscripts/build.sh
AI 分析无响应或超时 LLM 配置错误、网络不通或 API 不兼容 使用 /api/ai/test 接口测试,检查返回的错误信息
Caddy 启动失败 端口 81 被占用或 Caddy 未安装 修改 Caddyfile 中的端口,或安装 Caddy:sudo apt install caddy

日志查看:

  • 开发环境:tail -f dev.log
  • 生产环境:所有输出到 stdout,可使用 journalctl(systemd)或 docker logs

11. 如何扩展你的仪表盘?

11.1 接入真实数据库(例如 PostgreSQL)

  1. 修改 prisma/schema.prisma 中的 datasource

    prisma 复制代码
    datasource db {
      provider = "postgresql"
      url      = env("DATABASE_URL")
    }
  2. .env 中设置真实的连接字符串。

  3. 运行 bun run db:migrate -- --name init 创建迁移。

  4. 修改 /api/analysis/route.ts,将数据生成逻辑替换为从数据库查询真实订单和用户表的代码。

11.2 增加新的分析维度(例如"促销活动效果")

  • analyzeData 函数中添加新的聚合逻辑,返回字段。
  • page.tsxAnalysisData 接口中添加类型定义。
  • 在对应 Tab 下增加新的图表组件(复用 ChartContainer 模式)。
  • 若需要 AI 分析支持,在 ANALYSIS_PROMPTS 中添加新的预设,并在前端 ANALYSIS_PRESETS 数组中增加选项。

11.3 添加新的 AI 分析预设

只需两步:

  1. 在前端 page.tsx 中找到 ANALYSIS_PRESETS 数组,追加 { id: "newType", label: "新预设名称" }
  2. 在后端 /api/ai/analyze/route.tsANALYSIS_PROMPTS 对象中添加同名的 systemPromptuserPromptTemplate

前端会自动出现新按钮,无需修改其他逻辑。

11.4 启用多语言(i18n)

项目已集成 next-intl,具体步骤:

  • 创建 messages/zh.jsonmessages/en.json
  • next.config.ts 中配置 nextIntl 插件。
  • 在页面中使用 useTranslations() 替换硬编码文本。

12. API 接口文档速查

12.1 GET /api/analysis

功能:执行完整的数据生成、清洗、分析流程,返回所有看板所需的数据。

响应结构(节选):

json 复制代码
{
  "cleaningStats": { "originalCount": 3000, "validCount": 2850, "medianAge": 35 },
  "analysis": {
    "overview": { "totalSales": 12345678.90, "totalOrders": 2850 },
    "monthlySales": [ { "month": 1, "sales": 980000, "growth": null } ],
    "rfmSegmentCount": [ { "segment": "高价值用户", "count": 68 } ],
    "correlationMatrix": [[1, 0.3, -0.2], [0.3, 1, 0.1], ...]
  }
}

12.2 POST /api/ai/test

请求体

json 复制代码
{
  "url": "https://api.openai.com/v1",
  "apiKey": "sk-xxx",
  "modelId": "gpt-4o"
}

成功响应{ "success": true, "latency": 342, "model": "gpt-4o-2024-05-13" }

失败响应{ "success": false, "error": "连接失败: 401 Unauthorized" }

12.3 POST /api/ai/analyze(SSE 流式)

请求体

json 复制代码
{
  "config": { "url": "...", "apiKey": "...", "modelId": "..." },
  "analysisType": "overview",
  "dataSummary": "## 电商销售数据摘要..."
}

响应Content-Type: text/event-stream

复制代码
data: {"content":"## "}
data: {"content":"核心指标"}
data: {"content":"解读\n\n"}
data: [DONE]

前端通过 EventSourcefetch + ReadableStream 消费。


结语

从数据生成、清洗到多维度分析和 AI 解读,这个电商销售分析仪表盘展示了现代全栈 Web 应用的一个完整范例。它既可以用作学习 React + Next.js 数据可视化的实战项目,也可以作为真实商业智能系统的快速原型。通过本文的章节拆解和流程图辅助,相信你已经对每个模块的设计意图运作原理有了清晰的认识。现在,你可以 clone 代码,亲自运行看看效果,并基于此扩展出属于你自己的数据分析平台。

相关推荐
枫糖浆AI1 小时前
openclaw页面无法访问解决方法
人工智能
浩子coding2 小时前
通过 Spring AI Alibaba 源码,看如何玩转 ReAct 智能体范式
人工智能·后端
卡梅德生物科技小能手2 小时前
卡梅德生物科普CD124(IL-4Rα):2型免疫炎症的核心调控靶点
人工智能·经验分享·深度学习
垂钓的小鱼12 小时前
TRIZ理论是什么?萃智引擎如何将它变为工程师的AI创新助手
人工智能·microsoft
咋吃都不胖lyh2 小时前
DBSCAN(基于密度的空间聚类应用与噪声)算法
人工智能·机器学习
诸葛务农2 小时前
涡喷式发烟机施放粉末状烟剂成烟面积的计算:烟剂材料特性的影响
人工智能
ken22322 小时前
在 Libreoffice Calc中输入自定义表情字符时,需要保存之后,才能正常显示
学习
云烟成雨TD2 小时前
Agent Scope Java 2.x 系列【10】技能(Skill)
java·人工智能·agent
GDAL2 小时前
书签栏的 AI 转型:用 bge-small-zh-v1.5 重塑书签管理
人工智能·书签栏