本项目本站内源代码下载地址
快速搭建一个电商数据分析看板,却苦于后端数据准备、清洗逻辑、图表展示和 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 清洗模块:处理异常与缺失的完整逻辑
清洗流程严格按照以下顺序(不可颠倒):
- 过滤订单 :只保留
price > 0 && quantity > 0的记录。这一步移除了约 150 条无效订单。 - 计算年龄中位数 :从所有有效年龄 (非 null)的用户中取中位数。例如,若有效年龄为
[22, 24, 35, 35, 40],中位数为 35。 - 填充缺失年龄 :将所有
age为null的用户行设置为该中位数。 - 左连接 :以
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 分析模块:让大模型帮你解读数据



工作流程
- 用户在前端配置 LLM 的 API URL、Key 和模型 ID(配置保存到
localStorage)。 - 点击"测试连接",调用
POST /api/ai/test验证配置是否正确。 - 选择分析预设(或自定义提问),前端将当前仪表盘的数据摘要化为一个结构化的 Markdown 文本(包含 KPI、月度趋势、品类排名、RFM 分群等)。
- 发送
POST /api/ai/analyze请求,携带配置、分析类型和数据摘要。 - 后端根据分析类型构造 system prompt 和 user prompt,调用 LLM 的流式 API,并将响应的每个 chunk 通过 SSE 推送到前端。
- 前端使用
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,构建脚本会:
- 安装依赖并执行
next build。 - 复制
.next/static、public到.next/standalone目录下。 - 将所有产物(standalone 文件夹 + db 文件 + Caddyfile + 启动脚本)打包成一个
tar.gz。 - 生成唯一的构建 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打包进去,镜像或压缩包体积小。 - 独立运行 :生产环境不需要
nextCLI,直接用 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 倍,提升开发体验。
- 一体化 :不需要单独安装
npm、npx,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 :3000 并 kill 相应进程 |
| 页面空白且控制台报错 | /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)
-
修改
prisma/schema.prisma中的datasource:prismadatasource db { provider = "postgresql" url = env("DATABASE_URL") } -
在
.env中设置真实的连接字符串。 -
运行
bun run db:migrate -- --name init创建迁移。 -
修改
/api/analysis/route.ts,将数据生成逻辑替换为从数据库查询真实订单和用户表的代码。
11.2 增加新的分析维度(例如"促销活动效果")
- 在
analyzeData函数中添加新的聚合逻辑,返回字段。 - 在
page.tsx的AnalysisData接口中添加类型定义。 - 在对应 Tab 下增加新的图表组件(复用
ChartContainer模式)。 - 若需要 AI 分析支持,在
ANALYSIS_PROMPTS中添加新的预设,并在前端ANALYSIS_PRESETS数组中增加选项。
11.3 添加新的 AI 分析预设
只需两步:
- 在前端
page.tsx中找到ANALYSIS_PRESETS数组,追加{ id: "newType", label: "新预设名称" }。 - 在后端
/api/ai/analyze/route.ts的ANALYSIS_PROMPTS对象中添加同名的systemPrompt和userPromptTemplate。
前端会自动出现新按钮,无需修改其他逻辑。
11.4 启用多语言(i18n)
项目已集成 next-intl,具体步骤:
- 创建
messages/zh.json和messages/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]
前端通过 EventSource 或 fetch + ReadableStream 消费。
结语
从数据生成、清洗到多维度分析和 AI 解读,这个电商销售分析仪表盘展示了现代全栈 Web 应用的一个完整范例。它既可以用作学习 React + Next.js 数据可视化的实战项目,也可以作为真实商业智能系统的快速原型。通过本文的章节拆解和流程图辅助,相信你已经对每个模块的设计意图 和运作原理有了清晰的认识。现在,你可以 clone 代码,亲自运行看看效果,并基于此扩展出属于你自己的数据分析平台。