体育用品销售数据可视化分析系统 --- 完整技术文档
版本:v1.0 | 最后更新:2026-05-20
目录
- 项目概述
- 技术栈与依赖
- 项目目录结构
- 数据模型
- 后端架构设计
- 前端架构设计
- [API 接口规范](#API 接口规范)
- 前端模块详细设计
- 样式系统与响应式设计
- 核心算法详解
- 关键实现细节
- 启动与部署
- 扩展建议
一、项目概述
1.1 系统简介
本系统是一个面向体育用品销售场景的 数据可视化分析平台,采用 Python Flask 后端 + 原生 HTML/CSS/JS 前端的轻量级架构。系统从 Excel 文件加载 1000 条销售订单数据,提供七大功能模块:
| 模块 | 功能定位 | 核心能力 |
|---|---|---|
| 经营总览 | Dashboard | KPI 指标、趋势图、品类/区域/客户摘要 |
| 深度分析 | Analysis | 洞察卡片、利润排行、利润率排行、客户价值、低利润预警 |
| 销售预测 | Forecast | 线性回归+移动平均混合预测,置信度衰减 |
| 可视图表 | Charts | 12 种独立图表面板(柱状图、环形图、气泡图、热力图等) |
| 订单明细 | Records | 可排序、可分页、支持筛选联动的订单列表 |
| 数据管理 | Data | CRUD 操作、批量导入、Excel/CSV 导出 |
| 个人中心 | Profile | 用户偏好、筛选快照、退出登录 |
1.2 设计目标
- 零外部前端依赖:不使用 ECharts、Chart.js、D3 等第三方图表库,所有图表通过原生 SVG 字符串拼接实现
- 单文件后端 :全部后端逻辑集中在
app.py一个文件中,便于理解和维护 - Excel 即数据库 :以
.xlsx文件作为唯一数据存储,降低部署门槛 - 筛选全局联动:顶部筛选栏的所有控件变更后,全页面数据同步刷新















二、技术栈与依赖
2.1 后端技术栈
| 技术 | 版本要求 | 用途 |
|---|---|---|
| Python | >= 3.8 | 运行环境 |
| Flask | >= 3.1.0 | Web 框架,路由、模板渲染、JSON API |
| pandas | >= 2.2.0 | 数据加载、筛选、聚合、类型转换 |
| openpyxl | >= 3.1.0 | Excel 文件读写引擎 |
2.2 前端技术栈
| 技术 | 用途 |
|---|---|
| HTML5 | 页面结构,Jinja2 模板引擎渲染 |
| CSS3 | 样式系统,CSS Grid 布局,CSS 自定义属性(设计令牌),conic-gradient 环形图 |
| ES6+ JavaScript | 业务逻辑,Fetch API 异步请求,DOM 操作 |
| SVG | 所有图表渲染(polyline、rect、circle、path 等原生元素) |
2.3 依赖安装
bash
pip install -r requirements.txt
requirements.txt 内容:
Flask>=3.1.0
pandas>=2.2.0
openpyxl>=3.1.0
三、项目目录结构
code/
├── app.py # Flask 后端主程序(908 行)
├── requirements.txt # Python 依赖声明
├── 体育用品销售数据_1000行.xlsx # 数据源文件(1000 行 × 14 列)
├── README.md # 项目说明文档
├── 技术文档.md # 本文档
├── .gitignore # Git 忽略规则
├── templates/ # Jinja2 模板目录
│ ├── index.html # 主仪表盘页面(607 行)
│ └── auth.html # 登录注册页面(89 行)
├── static/ # 静态资源目录
│ ├── css/
│ │ └── app.css # 全局样式表(1972 行)
│ └── js/
│ ├── app.js # 主应用逻辑(1401 行)
│ └── auth.js # 登录注册逻辑(145 行)
└── server*.log # 运行日志(已 gitignore)
文件职责划分:
| 文件 | 行数 | 职责 |
|---|---|---|
app.py |
908 | 路由定义、数据加载、筛选引擎、聚合函数、预测算法、CRUD 操作、错误处理 |
index.html |
607 | 主页面 HTML 结构,7 个 page-section、2 个 modal、筛选面板 |
auth.html |
89 | 登录/注册双面板页面,Tab 切换 |
app.css |
1972 | 设计令牌、Grid 布局、响应式断点、图表样式、动画、Toast 通知 |
app.js |
1401 | 页面路由、数据请求、图表渲染、CRUD 交互、用户认证、筛选联动 |
auth.js |
145 | 登录注册表单处理、localStorage 读写、密码编码 |
四、数据模型
4.1 数据源
- 文件名 :
体育用品销售数据_1000行.xlsx - 格式 :Microsoft Excel
.xlsx(由 openpyxl 引擎读写) - 规模:1000 行 × 14 列
- 角色:系统唯一数据存储,无数据库
4.2 字段定义
| 字段名 | 含义 | 类型 | 示例 | 说明 |
|---|---|---|---|---|
| 订单ID | 订单编号 | 文本 | ORD000001 | 唯一标识,格式 ORD + 6 位数字 |
| 销售日期 | 下单日期 | 日期 | 2024-03-15 | YYYY-MM-DD 格式 |
| 产品类别 | 商品分类 | 文本 | 运动鞋 | 如运动鞋、健身器材、球类等 |
| 产品名称 | 具体商品 | 文本 | 跑步鞋A | 产品级别标识 |
| 单位成本 | 采购单价 | 数值 | 200.00 | 元,进货成本 |
| 销售单价 | 零售单价 | 数值 | 500.00 | 元,对外售价 |
| 订单数量 | 购买件数 | 整数 | 3 | --- |
| 销售总金额 | 订单金额 | 数值 | 1500.00 | 派生:销售单价 × 订单数量 |
| 总成本 | 订单成本 | 数值 | 600.00 | 派生:单位成本 × 订单数量 |
| 总利润 | 订单利润 | 数值 | 900.00 | 派生:销售总金额 − 总成本 |
| 利润率 | 利润占比 | 数值 | 0.6000 | 派生:总利润 / 销售总金额 |
| 销售区域 | 大区 | 文本 | 华东 | 如华东、华南、华北、华中、西南、西北、东北 |
| 所属省份 | 省份 | 文本 | 上海 | --- |
| 客户类型 | 客户分类 | 文本 | 企业 | 如个人、企业、团购等 |
4.3 派生字段
系统在数据加载时自动计算两个额外字段(不写入 Excel):
| 字段 | 计算方式 | 用途 |
|---|---|---|
月份 |
销售日期.dt.to_period("M").astype(str) → 如 2024-03 |
月度聚合分组键 |
销售日期文本 |
销售日期.dt.strftime("%Y-%m-%d") → 如 2024-03-15 |
前端展示 |
4.4 数据加载流程
load_sales_data()(带 @lru_cache 缓存)
├── 读取 Excel 文件 (pd.read_excel)
├── 校验必需列是否存在(缺失则抛 ValueError)
├── 日期列 → pd.to_datetime(无效值变 NaT)
├── 数值列 → pd.to_numeric + fillna(0)
├── 文本列 → fillna("未知").astype(str).strip()
├── 丢弃日期为空的行 (dropna)
├── 计算「月份」和「销售日期文本」
└── 返回 DataFrame
五、后端架构设计
5.1 整体架构
┌─────────────────────────────────────────────────┐
│ Flask App │
├──────────┬──────────┬──────────┬─────────────────┤
│ 页面路由 │ 数据 API │ CRUD API │ 错误处理器 │
│ 3 个 GET │ 8 个 GET │ 5 个 │ 2 个 │
├──────────┴──────────┴──────────┴─────────────────┤
│ 筛选引擎 apply_filters() │
├──────────────────────────────────────────────────┤
│ 数据层 load_sales_data() + lru_cache │
├──────────────────────────────────────────────────┤
│ Excel 文件 (.xlsx) │
└──────────────────────────────────────────────────┘
5.2 常量定义(app.py 顶部)
python
BASE_DIR = Path(__file__).resolve().parent
DATA_FILE = BASE_DIR / "体育用品销售数据_1000行.xlsx"
DATE_COLUMN = "销售日期"
NUMERIC_COLUMNS = ["单位成本", "销售单价", "订单数量", "销售总金额", "总成本", "总利润", "利润率"]
FILTER_COLUMNS = {
"category": "产品类别",
"region": "销售区域",
"province": "所属省份",
"customer_type": "客户类型",
}
TABLE_COLUMNS = [
"订单ID", "销售日期", "产品类别", "产品名称", "销售区域", "所属省份",
"客户类型", "订单数量", "销售总金额", "总成本", "总利润", "利润率",
]
SORTABLE_COLUMNS = set(TABLE_COLUMNS) | {"单位成本", "销售单价"}
REQUIRED_COLUMNS = set(TABLE_COLUMNS) | {"单位成本", "销售单价"}
5.3 数据加载层
python
@lru_cache(maxsize=1)
def load_sales_data() -> pd.DataFrame:
- 使用
functools.lru_cache(maxsize=1)实现单例缓存 - 首次调用读取 Excel 文件,后续调用直接返回内存中的 DataFrame
- 所有 CRUD 操作后调用
load_sales_data.cache_clear()强制刷新
5.4 筛选引擎
python
def apply_filters(df: pd.DataFrame) -> pd.DataFrame:
从 request.args 读取 URL 查询参数,依次应用以下筛选:
| 参数 | 筛选逻辑 | 示例 |
|---|---|---|
start_date |
df[销售日期] >= pd.to_datetime(start_date) |
2024-01-01 |
end_date |
df[销售日期] <= pd.to_datetime(end_date) |
2024-12-31 |
category |
df[产品类别] == value 精确匹配 |
运动鞋 |
region |
df[销售区域] == value 精确匹配 |
华东 |
province |
df[所属省份] == value 精确匹配 |
上海 |
customer_type |
df[客户类型] == value 精确匹配 |
企业 |
keyword |
订单ID / 产品名称 / 所属省份 三列模糊搜索(str.contains) |
跑步 |
5.5 核心聚合函数
| 函数 | 签名 | 输出 |
|---|---|---|
grouped_metrics(df, column, limit, ascending) |
分组列名 + 可选限制数 | [{name, sales, profit, quantity, orders, margin}] |
monthly_trend(df) |
--- | [{month, sales, profit, quantity, orders}] |
monthly_growth(df) |
--- | [{month, sales, profit, orders, sales_growth, profit_growth, order_growth}] |
weekday_metrics(df) |
--- | [{name, sales, profit, quantity, orders}](周一~周日) |
price_band_metrics(df) |
--- | [{name, sales, profit, quantity, orders, margin}](6 个价格带) |
category_efficiency(df) |
--- | [{name, sales, profit, quantity, orders, avg_price, margin}] |
product_profit_matrix(df, limit) |
取前 N 条 | [{name, category, sales, profit, quantity, orders, margin}] |
region_category_stack(df) |
--- | {regions, categories, series} |
customer_value_metrics(df) |
--- | [{name, sales, profit, quantity, orders, aov, margin}] |
category_region_matrix(df) |
--- | {categories, regions, values} |
linear_forecast(values, steps) |
历史序列 + 步数 | [预测值, ...] |
5.6 数据持久化
python
def save_data(df: pd.DataFrame) -> None:
output = df.copy()
output[DATE_COLUMN] = pd.to_datetime(output[DATE_COLUMN], errors="coerce")
output.to_excel(DATA_FILE, index=False, engine="openpyxl")
load_sales_data.cache_clear()
- 所有写操作(新增、编辑、删除、导入)最终调用
save_data() - 写入后自动清除
lru_cache,确保下次读取为最新数据 - 订单 ID 自动生成逻辑(
next_order_id):扫描所有现有 ID 的数字部分,取最大值 +1,格式化为ORD000001
5.7 辅助工具函数
| 函数 | 用途 |
|---|---|
money(value) |
保留 2 位小数的浮点数格式化 |
ratio(value) |
保留 4 位小数的比率格式化 |
growth_ratio(current, previous) |
计算环比增长率,前值为 0 时返回 0 |
query_int(name, default, minimum, maximum) |
从 URL 参数读取整数,带默认值和范围校验 |
parse_record_body() |
从 JSON 请求体解析 9 个订单字段 |
compute_derived(row) |
根据基础字段计算 4 个派生字段(销售总金额、总成本、总利润、利润率) |
table_rows(df) |
将 DataFrame 转换为前端可用的字典列表 |
六、前端架构设计
6.1 整体架构:服务端渲染 SPA
首次加载 ──→ Flask 渲染 index.html(Jinja2)──→ 浏览器加载 HTML + CSS + JS
│
页面切换 ──→ JS 切换 .page-section.active ──→ 无刷新跳转
│
数据加载 ──→ Fetch API 请求 /api/* ──→ JSON 响应 ──→ DOM 渲染
│
URL 同步 ──→ window.location.hash ──→ 支持浏览器前进/后退
6.2 页面路由机制
在 app.js 中通过 setActivePage(page) 函数实现:
- 更新页面标题、描述(从
pageMeta映射表读取) - 切换侧边栏导航的
.active类 - 切换对应的
.page-section的.active类(CSS 控制显隐) - 更新
window.location.hash - 对数据页面隐藏/显示筛选面板和导出按钮
- 切换到数据管理页时自动加载记录
pageMeta 映射表:
javascript
const pageMeta = {
overview: ["Dashboard", "销售经营总览", "查看核心 KPI、销售趋势和筛选范围内的整体表现。"],
analysis: ["Analysis", "销售经营分析", "按品类、区域、客户和产品利润效率拆解经营表现。"],
forecast: ["Forecast", "销售趋势预测", "基于历史月份走势预估未来 6 个月的销售额、利润和订单量。"],
charts: ["Charts", "图表中心", "集中查看品类、区域、省份、产品和热力矩阵图表。"],
records: ["Records", "订单明细", "检索、排序和导出筛选范围内的订单数据。"],
data: ["Data", "数据管理", "新增、编辑、删除、导入和导出销售订单数据。"],
profile: ["Profile", "个人中心", "管理账户偏好和当前分析上下文。"],
auth: ["Account", "登录注册", "进入系统或创建新的演示账户。"],
};
6.3 全局筛选联动
所有数据页面共享顶部筛选栏,包含 7 个控件:
| 控件 | HTML 元素 | 触发方式 | 联动行为 |
|---|---|---|---|
| 开始日期 | <input type="date"> |
change 事件 |
立即刷新全部数据 |
| 结束日期 | <input type="date"> |
change 事件 |
立即刷新全部数据 |
| 产品类别 | <select> |
change 事件 |
立即刷新全部数据 |
| 销售区域 | <select> |
change 事件 |
立即刷新全部数据 |
| 所属省份 | <select> |
change 事件 |
立即刷新全部数据 |
| 客户类型 | <select> |
change 事件 |
立即刷新全部数据 |
| 关键词 | <input type="search"> |
Enter 键 |
触发刷新 |
重置按钮恢复默认值后刷新,刷新按钮手动触发刷新。
刷新流程(refreshAll 函数):
javascript
async function refreshAll(resetPage = true) {
if (resetPage) { state.page = 1; state.dmPage = 1; }
await Promise.all([
loadOverview(), // /api/overview
loadAnalysis(), // /api/analysis
loadForecast(), // /api/forecast
loadRecords(), // /api/records
loadCharts(), // /api/charts
]);
if (state.activePage === "data") await loadDmRecords();
renderFilterSnapshot();
}
使用 Promise.all 并行请求 5 个 API 端点,最大化加载效率。
6.4 状态管理
javascript
const state = {
page: 1, // 订单明细当前页码
pageSize: 12, // 订单明细每页条数
options: null, // /api/options 响应缓存
activePage: "overview", // 当前激活页面
recordTotal: 0, // 订单总数
user: null, // 当前用户对象
dmPage: 1, // 数据管理当前页码
dmPageSize: 15, // 数据管理每页条数
dmTotal: 0, // 数据管理总数
};
6.5 用户认证方案
采用 纯客户端认证(演示用途):
存储结构(localStorage):
json
{
"name": "张三",
"account": "zhangsan",
"password": "base64编码后的密码",
"role": "体育用品销售运营",
"defaultPage": "overview"
}
流程:
- 注册 :用户名 + 密码 →
btoa(unescape(encodeURIComponent(password)))编码 → 存入 localStorage → 自动登录 - 登录:读取 localStorage → 比对账号和编码后的密码 → 匹配则登录成功
- 会话 :
state.user对象 + localStorage 持久化,刷新页面不丢失 - 退出 :清除
state.user+ 移除 localStorage 项
6.6 工具函数
| 函数 | 用途 |
|---|---|
escapeHtml(value) |
XSS 防护,转义 & < > " ' |
showToast(message, type) |
底部浮层通知,success(绿) / error(红),2.5 秒自动消失 |
money(value) |
格式化为人民币(如 ¥1,500) |
number(value) |
千分位格式化(如 1,000) |
percent(value) |
百分比格式化(如 60.00%) |
shortMoney(value) |
简写金额(如 12.3万、1.5亿) |
fillSelect(select, values, placeholder) |
填充下拉选项 |
getFilters() |
从筛选控件读取当前值,构建 URLSearchParams |
fetchJson(url) |
Fetch + JSON 解析 + 错误处理 |
setLoading(container) |
显示"加载中"占位 |
setEmpty(container, text) |
显示"暂无数据"占位 |
七、API 接口规范
7.1 页面路由(GET,返回 HTML)
| 方法 | 路由 | 函数 | 说明 |
|---|---|---|---|
| GET | / |
index() |
渲染 index.html 主仪表盘 |
| GET | /login |
login() |
渲染 auth.html(mode="login") |
| GET | /register |
register() |
渲染 auth.html(mode="register") |
7.2 数据 API(GET,返回 JSON)
GET /api/options --- 筛选选项
返回所有下拉框的可选值。
响应结构:
json
{
"date_min": "2024-01-01",
"date_max": "2024-12-31",
"categories": ["健身器材", "球类", "运动服饰", "运动鞋", "..."],
"regions": ["华东", "华南", "华北", "华中", "西南", "西北", "东北"],
"provinces": ["上海", "江苏", "浙江", "..."],
"customer_types": ["个人", "企业", "团购"]
}
GET /api/overview --- 经营总览
返回 KPI 指标、月度趋势、分组统计和热力矩阵。
响应结构:
json
{
"kpis": {
"sales": 1234567.89,
"profit": 234567.89,
"cost": 1000000.00,
"orders": 1000,
"quantity": 2500,
"average_order_value": 1234.57,
"profit_margin": 0.1900
},
"trend": [
{"month": "2024-01", "sales": 100000, "profit": 20000, "quantity": 200, "orders": 80}
],
"category": [
{"name": "运动鞋", "sales": 500000, "profit": 100000, "quantity": 800, "orders": 300, "margin": 0.20}
],
"region": ["..."],
"province_top": ["..."],
"product_top": ["..."],
"customer_type": ["..."],
"category_region": {
"categories": ["运动鞋", "健身器材"],
"regions": ["华东", "华南"],
"values": [
{"category": "运动鞋", "region": "华东", "sales": 50000}
]
}
}
GET /api/charts --- 图表数据
返回 7 种图表所需的数据集。
响应结构:
json
{
"monthly_growth": [
{"month": "2024-01", "sales": 100000, "profit": 20000, "orders": 80,
"sales_growth": 0, "profit_growth": 0, "order_growth": 0}
],
"weekday": [
{"name": "周一", "sales": 50000, "profit": 10000, "quantity": 100, "orders": 40}
],
"price_band": [
{"name": "0-100", "sales": 30000, "profit": 5000, "quantity": 300, "orders": 150, "margin": 0.17}
],
"category_efficiency": [
{"name": "运动鞋", "sales": 500000, "profit": 100000, "quantity": 800, "orders": 300, "avg_price": 450, "margin": 0.20}
],
"product_profit_matrix": [
{"name": "跑步鞋A", "category": "运动鞋", "sales": 50000, "profit": 10000, "quantity": 100, "orders": 50, "margin": 0.20}
],
"region_category_stack": {
"regions": ["华东", "华南"],
"categories": ["运动鞋", "健身器材"],
"series": [
{"region": "华东", "values": [{"category": "运动鞋", "sales": 50000, "share": 0.45}]}
]
},
"customer_type": ["..."]
}
GET /api/analysis --- 深度分析
返回洞察卡片、排行榜和预警数据。
响应结构:
json
{
"monthly_compare": {
"current_month": "2024-12",
"previous_month": "2024-11",
"sales_growth": 0.05,
"profit_growth": 0.03,
"order_growth": 0.02
},
"insights": [
{
"title": "核心品类",
"value": "运动鞋",
"detail": "贡献销售额 500,000,利润率 20.00%。",
"tone": "primary"
},
{
"title": "重点区域",
"value": "华东",
"detail": "区域销售额 400,000,订单 300 单。",
"tone": "success"
},
{
"title": "利润效率",
"value": "华南",
"detail": "该区域利润率 22.50%,适合优先复盘打法。",
"tone": "warning"
},
{
"title": "整体健康度",
"value": "19.00%",
"detail": "筛选范围内共 1000 单,销售额 1,234,567,利润 234,567。",
"tone": "neutral"
}
],
"category_profit": ["..."],
"region_margin": ["..."],
"customer_value": [
{"name": "企业", "sales": 500000, "profit": 100000, "quantity": 500, "orders": 200, "aov": 2500, "margin": 0.20}
],
"low_margin_products": [
{"name": "瑜伽垫B", "sales": 20000, "profit": 500, "quantity": 200, "orders": 100, "margin": 0.025}
]
}
洞察卡片生成逻辑:
- 核心品类:取销售额最高的品类
- 重点区域:取销售额最高的区域
- 利润效率:取利润率最高的区域
- 整体健康度:综合利润率 + 总订单数 + 总销售额
GET /api/forecast --- 销售预测
请求参数:
| 参数 | 类型 | 默认 | 范围 | 说明 |
|---|---|---|---|---|
steps |
int | 6 | 1-12 | 预测期数 |
响应结构:
json
{
"history": [
{"month": "2024-01", "sales": 100000, "profit": 20000, "orders": 80}
],
"forecast": [
{"month": "2025-01", "sales": 110000, "profit": 22000, "orders": 85, "confidence": 0.90},
{"month": "2025-02", "sales": 112000, "profit": 22500, "orders": 86, "confidence": 0.86}
],
"summary": {
"last_month": "2024-12",
"next_month": "2025-01",
"next_month_sales": 110000,
"estimated_growth": 0.05,
"method": "近月移动均值与线性趋势混合预测"
}
}
GET /api/records --- 订单列表
请求参数:
| 参数 | 类型 | 默认 | 说明 |
|---|---|---|---|
page |
int | 1 | 页码(从 1 开始) |
page_size |
int | 12 | 每页条数(5-100) |
sort_by |
string | 销售日期 | 排序列(见 SORTABLE_COLUMNS) |
sort_order |
string | desc | 排序方向(asc/desc) |
| + 所有筛选参数 | 见 5.4 筛选引擎 |
响应结构:
json
{
"page": 1,
"page_size": 12,
"total": 1000,
"pages": 84,
"rows": [
{
"订单ID": "ORD001000",
"销售日期": "2024-12-31",
"产品类别": "运动鞋",
"产品名称": "跑步鞋A",
"销售区域": "华东",
"所属省份": "上海",
"客户类型": "个人",
"订单数量": 2,
"销售总金额": 1000.00,
"总成本": 400.00,
"总利润": 600.00,
"利润率": 0.6000
}
]
}
GET /api/export.csv --- CSV 导出
- 返回 UTF-8 BOM 编码的 CSV 文件(
前缀确保 Excel 正确识别中文) - 文件名:
sales_filtered.csv - 按销售日期降序排列
GET /api/export.xlsx --- Excel 导出
- 返回
.xlsx格式文件 - 文件名:
sales_filtered.xlsx - 使用 openpyxl 引擎写入
BytesIO缓冲区
7.3 CRUD API
GET /api/records/{id} --- 查询单条记录
响应: 完整记录对象(14 个字段),日期格式化为 YYYY-MM-DD。
错误: 404 {"error": "未找到记录"}
POST /api/records --- 新增订单
请求体(JSON):
json
{
"销售日期": "2024-06-01",
"产品类别": "运动鞋",
"产品名称": "跑步鞋A",
"销售区域": "华东",
"所属省份": "上海",
"客户类型": "个人",
"订单数量": 2,
"单位成本": 200,
"销售单价": 500
}
处理流程:
parse_record_body()解析 9 个基础字段compute_derived()计算 4 个派生字段next_order_id()自动生成订单 ID- 拼接到现有 DataFrame 末尾
save_data()写入 Excel 并清除缓存
响应:
json
{"success": true, "订单ID": "ORD001001", "total": 1001}
PUT /api/records/{id} --- 编辑订单
请求体: 同 POST,包含要更新的字段。
处理流程:
- 定位目标行(
df[订单ID] == record_id) - 解析并计算派生字段
- 更新 DataFrame 中对应行的所有字段
- 重新计算
月份和销售日期文本 save_data()写入
响应:
json
{"success": true, "订单ID": "ORD001001"}
DELETE /api/records/{id} --- 删除订单
处理: 过滤掉目标行 → save_data() 写入。
响应:
json
{"success": true, "total": 999}
POST /api/import --- 批量导入
请求: multipart/form-data
| 字段 | 类型 | 说明 |
|---|---|---|
file |
File | .xlsx / .xls / .csv 文件 |
mode |
string | append(追加)或 replace(覆盖) |
处理流程:
- 根据文件扩展名选择
pd.read_csv或pd.read_excel - 校验必需列
- 类型转换(日期、数值)
- 追加模式:与现有数据合并;替换模式:直接使用上传数据
- 按订单ID去重(
drop_duplicates,保留最后一条) - 计算派生字段
save_data()写入
响应:
json
{"success": true, "imported": 50, "total": 1050}
7.4 错误处理
| 错误处理器 | 触发条件 | 响应 |
|---|---|---|
@app.errorhandler(FileNotFoundError) |
数据文件不存在 | 500 + 错误信息 |
@app.errorhandler(ValueError) |
数据文件缺少必需列 | 500 + 错误信息 |
八、前端模块详细设计
8.1 经营总览(overview)
布局结构:
┌─────────────────────────────────────────┐
│ KPI 卡片区(8 张,4×2 Grid) │
│ 销售总额 │ 订单数量 │ 客单价 │ 利润率 │
│ 品类数量 │ 客户类型 │ 最佳品类│ 核心区域 │
├──────────────┬──────────────┬────────────┤
│ 品类销售TOP5 │ 区域销售分布 │ 客户类型 │
│ (进度条) │ (SVG环形图) │ (进度条) │
├──────────────┴──────────────┴────────────┤
│ 经营洞察卡片(3 列) │
│ 核心品类 │ 重点区域 │ 利润效率 │
├─────────────────────────────────────────┤
│ 月度销售与利润趋势(SVG 折线图) │
└─────────────────────────────────────────┘
KPI 卡片数据映射:
| 卡片 | 主值 | 副值 | 色调 |
|---|---|---|---|
| 销售总额 | money(kpis.sales) |
利润 money(kpis.profit) |
primary (蓝) |
| 订单数量 | number(kpis.orders) |
商品件数 number(kpis.quantity) |
success (绿) |
| 平均客单价 | money(kpis.average_order_value) |
总成本 money(kpis.cost) |
warning (橙) |
| 综合利润率 | percent(kpis.profit_margin) |
按销售额加权计算 | neutral (灰) |
| 品类数量 | N 个 | 覆盖 N 个区域 | primary |
| 客户类型 | N 类 | 产品 N 种 | success |
| 最佳品类 | 品类名 | 销售额 ¥XXX | warning |
| 核心区域 | 区域名 | 利润率 XX% | neutral |
区域销售分布 SVG 环形图实现:
使用自定义 SVG 弧形路径(arcPath 函数)绘制,而非 CSS conic-gradient:
- 通过极坐标转直角坐标计算弧形起点和终点
- 使用
<path d="M...L...A...Z">绘制扇形 - 中心
<circle>创建环形效果 - 中心文字显示总额
8.2 深度分析(analysis)
布局结构:
┌─────────────────────────────────────────┐
│ 洞察卡片(4 列 Grid) │
│ 核心品类 │ 重点区域 │ 利润效率 │ 整体健康 │
├──────────────┬──────────────────────────┤
│ 品类利润排行 │ 区域利润率排行 │
│ (水平柱状图) │ (排名列表) │
├──────────────┼──────────────────────────┤
│ 客户价值分析 │ 低利润产品关注 │
│ (表格) │ (排名列表) │
└──────────────┴──────────────────────────┘
洞察卡片渲染:
每张卡片包含:
- SVG 图标(折线图/勾号/感叹号/笑脸,根据 tone 选择)
- 标题(如"核心品类")
- 主值(如"运动鞋")
- 描述(如"贡献销售额 500,000,利润率 20.00%。")
- 装饰性背景元素
.insight-deco
8.3 销售预测(forecast)
布局结构:
┌─────────────────────────────────────────┐
│ 预测摘要卡片(4 张) │
│ 预测月份 │ 预计销售额 │ 预计增长 │ 方法 │
├─────────────────────────────────────────┤
│ 预测趋势图(SVG 折线图) │
│ 历史实线 + 预测虚线 │
├─────────────────────────────────────────┤
│ 预测明细表 │
│ 月份 │ 预计销售额 │ 预计利润 │ 订单 │ 置信│
└─────────────────────────────────────────┘
预测图表实现:
- 历史数据:蓝色实线 (
line-sales) - 预测数据:橙色虚线 (
line-forecast) - 预测线从历史最后一个点开始,形成连续效果
- 使用
buildTrendSvg()统一渲染
8.4 可视图表(charts)
包含 12 个独立图表面板,采用 2 列 Grid 布局(panel-wide 占满整行):
| 图表 | 渲染函数 | 类型 | 技术实现 |
|---|---|---|---|
| 品类贡献 | renderBars() |
水平柱状图 | HTML <div> 进度条 |
| 区域销售结构 | renderDonut() |
环形图 | CSS conic-gradient |
| 省份销售 TOP 12 | renderBars() |
水平柱状图 | HTML <div> 进度条 |
| 热销产品 TOP 10 | renderBars() |
水平柱状图 | HTML <div> 进度条 |
| 月度增长趋势 | renderMonthlyGrowth() |
分组柱状图 | SVG <rect> 双条 |
| 星期分布 | renderWeekdayChart() |
垂直柱状图 | SVG <rect> |
| 价格带分析 | renderPriceBandChart() |
水平柱状图 | SVG <rect> |
| 产品利润矩阵 | renderProductProfit() |
气泡图 | SVG <circle> |
| 客户类型对比 | renderCustomerTypeChart() |
水平柱状图 | SVG <rect> |
| 品类效率分析 | renderCategoryEfficiency() |
双层条形图 | SVG <rect> 双层 |
| 区域品类结构 | renderRegionCategoryStack() |
堆叠柱状图 | SVG <rect> 拼接 |
| 品类×区域热力 | renderHeatmap() |
热力图 | CSS Grid + 背景色 |
8.5 订单明细(records)
布局结构:
┌─────────────────────────────────────────┐
│ 标题 + 记录数 + 排序控件 │
├─────────────────────────────────────────┤
│ 订单表格(11 列) │
│ 订单ID│日期│品类│产品│区域│省份│客户│数量││
│ 销售额│利润│利润率 │
├─────────────────────────────────────────┤
│ 分页控件:上一页 │ 页码信息 │ 下一页 │
└─────────────────────────────────────────┘
排序支持:销售日期、销售总金额、总利润、利润率、订单数量,升降序可选。
8.6 数据管理(data)
功能清单:
- 查看:与订单明细类似的分页表格,但操作列替代利润率列
- 新增:弹窗表单(10 个字段),订单ID 自动生成,实时计算预计销售额和利润
- 编辑:弹窗表单,预填现有数据
- 删除:确认对话框后删除
- 导入:弹窗,支持 .xlsx/.xls/.csv,追加/替换两种模式
- 导出 :按钮直接打开
/api/export.xlsx下载
新增/编辑弹窗表单字段:
| 字段 | 输入类型 | 必填 | 说明 |
|---|---|---|---|
| 订单ID | readonly | --- | 新增时显示"自动生成",编辑时显示现有ID |
| 销售日期 | date | ✓ | --- |
| 产品类别 | text | ✓ | --- |
| 产品名称 | text | ✓ | --- |
| 销售区域 | text | ✓ | --- |
| 所属省份 | text | ✓ | --- |
| 客户类型 | text | ✓ | --- |
| 订单数量 | number (min=1) | ✓ | 默认 1 |
| 单位成本 | number (step=0.01) | ✓ | --- |
| 销售单价 | number (step=0.01) | ✓ | --- |
实时预览: updateFormPreview() 监听数量、成本、单价的 input 事件,实时计算并显示预计销售额和利润。
8.7 个人中心(profile)
布局结构:
┌──────────────────┬──────────────────────────┐
│ 用户头像 │ 账户偏好设置 │
│ 姓名、角色 │ 昵称、角色、默认页面 │
│ 订单数、模块数 │ 保存按钮、退出按钮 │
├──────────────────┴──────────────────────────┤
│ 当前筛选快照(3×2 Grid) │
│ 日期范围 │ 产品类别 │ 销售区域 │
│ 所属省份 │ 客户类型 │ 关键词 │
└─────────────────────────────────────────────┘
九、样式系统与响应式设计
9.1 设计令牌(CSS 自定义属性)
css
:root {
--primary: #1e40af; /* 主色蓝 */
--teal: #0d9488; /* 辅助青 */
--green: #059669; /* 成功绿 */
--amber: #d97706; /* 警告橙 */
--red: #dc2626; /* 错误红 */
--surface: #ffffff; /* 卡片背景 */
--text: #0f172a; /* 主文字 */
--muted: #64748b; /* 次要文字 */
--line: #e2e8f0; /* 边框线 */
--shadow: 0 1px 3px ...; /* 默认阴影 */
--shadow-strong: 0 4px ...;/* 悬停阴影 */
--radius: 12px; /* 圆角 */
}
9.2 布局体系
整体布局: CSS Grid 两栏
css
.app-shell {
display: grid;
grid-template-columns: 268px 1fr;
}
各区域 Grid 布局:
| 区域 | Grid 定义 | 说明 |
|---|---|---|
| KPI 卡片 | grid-template-columns: repeat(4, 1fr) |
4 列等宽 |
| 洞察卡片 | grid-template-columns: repeat(4, 1fr) |
4 列等宽 |
| 概览摘要 | grid-template-columns: repeat(3, 1fr) |
3 列等宽 |
| 图表面板 | grid-template-columns: repeat(2, 1fr) |
2 列,.panel-wide 跨 2 列 |
| 筛选面板 | grid-template-columns: repeat(6, 1fr) |
6 列,关键词跨 2 列 |
| 个人中心 | grid-template-columns: 280px 1fr 1fr |
3 列不等宽 |
9.3 响应式断点
| 断点 | 适配设备 | 主要变化 |
|---|---|---|
<= 1180px |
平板 | KPI/洞察 2 列,图表单列,摘要单列 |
<= 760px |
手机 | 侧边栏隐藏(display: none),所有网格单列 |
<= 520px |
小屏手机 | 筛选面板单列,字体缩小 |
prefers-reduced-motion |
无障碍 | 禁用所有 transition 和 animation |
9.4 图表样式
SVG 图表通用样式:
css
.grid-line { stroke: #edf2f7; stroke-width: 1; }
.axis-line { stroke: #cfd8e3; stroke-width: 1; }
.line-sales { fill: none; stroke: #1e40af; stroke-width: 2.5; }
.line-profit { fill: none; stroke: #15803d; stroke-width: 2.5; }
.line-forecast { fill: none; stroke: #f59e0b; stroke-width: 2.5; stroke-dasharray: 8 4; }
环形图(CSS conic-gradient):
css
.donut {
width: 200px; height: 200px;
border-radius: 50%;
background: conic-gradient(...);
/* 内圈白色圆形通过伪元素或内层 div 实现 */
}
热力图(CSS Grid):
css
.heatmap-grid {
display: grid;
grid-template-columns: 150px repeat(N, minmax(96px, 1fr));
}
.heatmap-cell {
background: rgba(30, 64, 175, 0.18~1.0); /* 根据数值调整透明度 */
}
9.5 通知系统(Toast)
css
.toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
opacity: 0; transition: opacity 0.3s;
pointer-events: none;
}
.toast-show { opacity: 1; }
.toast-success { background: #059669; }
.toast-error { background: #dc2626; }
十、核心算法详解
10.1 线性回归 + 移动平均混合预测
python
def linear_forecast(values: list[float], steps: int) -> list[float]:
# 1. 计算线性回归斜率
count = len(values)
x_avg = (count - 1) / 2
y_avg = sum(values) / count
slope = sum((i - x_avg) * (v - y_avg) for i, v in enumerate(values)) / sum((i - x_avg)**2 for i in range(count))
# 2. 计算最近 3 期移动平均
moving_average = sum(values[-3:]) / min(len(values), 3)
# 3. 混合预测
last_value = values[-1]
predictions = []
for step in range(1, steps + 1):
trend_value = last_value + slope * step # 线性趋势
blended = trend_value * 0.65 + moving_average * 0.35 # 65% 趋势 + 35% 均值
predictions.append(max(blended, 0)) # 负值截断为 0
return predictions
置信度衰减:
python
confidence = max(0.62, 0.9 - index * 0.04)
- 第 1 期:0.90
- 第 2 期:0.86
- 第 3 期:0.82
- ...
- 第 7 期及以后:0.62(下限)
10.2 订单 ID 生成算法
python
def next_order_id(df: pd.DataFrame) -> str:
if df.empty:
return "ORD000001"
existing = df["订单ID"].astype(str).tolist()
max_num = 0
for oid in existing:
digits = "".join(c for c in oid if c.isdigit())
if digits:
max_num = max(max_num, int(digits))
return f"ORD{max_num + 1:06d}"
- 遍历所有现有 ID,提取数字部分
- 取最大值 +1
- 格式化为
ORD+ 6 位零填充数字
10.3 派生字段计算
python
def compute_derived(row: dict) -> dict:
quantity = int(row.get("订单数量", 1))
unit_cost = float(row.get("单位成本", 0))
unit_price = float(row.get("销售单价", 0))
row["销售总金额"] = round(unit_price * quantity, 2)
row["总成本"] = round(unit_cost * quantity, 2)
row["总利润"] = round(row["销售总金额"] - row["总成本"], 2)
row["利润率"] = round(row["总利润"] / row["销售总金额"], 4) if row["销售总金额"] else 0
return row
10.4 SVG 折线图坐标计算
javascript
// buildTrendSvg() 核心坐标映射
const x = (index) => pad.left + index * xStep;
const y = (value) => pad.top + plotH - (Number(value) / maxValue) * plotH;
// 网格线(5 条:0%, 25%, 50%, 75%, 100%)
const gridLines = [0, 0.25, 0.5, 0.75, 1].map((ratioValue) => {
const yy = pad.top + plotH * ratioValue;
const label = shortMoney(maxValue * (1 - ratioValue));
return `<line .../><text ...>${label}</text>`;
});
// 数据点连线
const points = item.values.map((value, index) => svgPoint(x(index), y(value))).join(" ");
return `<polyline points="${points}"></polyline>`;
10.5 气泡图半径映射
javascript
// 气泡半径 = 基础半径 + 比例缩放
const ratio = qMax > qMin ? (Number(d.quantity) - qMin) / (qMax - qMin) : 0.5;
const radius = 6 + ratio * 14; // 范围:6px ~ 20px
// 颜色映射(利润率阈值)
const color = Number(d.margin) >= 0.4 ? "#15803d" // 绿:高利润
: Number(d.margin) >= 0.2 ? "#f59e0b" // 橙:中利润
: "#dc2626"; // 红:低利润
10.6 热力图颜色强度
javascript
const intensity = value / maxValue;
const bg = `rgba(30, 64, 175, ${0.18 + intensity * 0.82})`;
- 最小值:
rgba(30, 64, 175, 0.18)--- 几乎透明 - 最大值:
rgba(30, 64, 175, 1.0)--- 完全不透明深蓝
十一、关键实现细节
11.1 无第三方图表库的 SVG 图表
所有图表通过 JavaScript 字符串拼接生成 SVG 标记,优势:
- 零外部依赖,页面加载无需下载额外 JS/CSS
- 完全可控的样式和交互
- 通过
<title>元素实现原生浏览器 tooltip - 图表尺寸通过
viewBox+width="100%"实现响应式
11.2 LRU 缓存策略
load_sales_data() 使用 functools.lru_cache(maxsize=1):
- 读操作:命中缓存,毫秒级响应,适合高频查询场景
- 写操作 :CRUD/导入后调用
cache_clear()强制刷新 - 限制:单进程、内存级缓存,适合中小数据量(本系统 1000 行)
- 注意:多进程部署时每个进程有独立缓存,需改用 Redis 等外部缓存
11.3 XSS 防护
前端所有动态内容通过 escapeHtml() 函数转义:
javascript
function escapeHtml(value) {
return String(value ?? "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
所有 API 返回的数据在渲染到 HTML 前都经过此函数处理。
11.4 CSV 导出 BOM 编码
python
csv_text = output.to_csv(index=False, encoding="utf-8-sig")
使用 utf-8-sig 编码自动添加 BOM 头(\xEF\xBB\xBF),确保 Microsoft Excel 正确识别中文编码。
11.5 数据导入去重
python
result = result.drop_duplicates(subset=["订单ID"], keep="last").reset_index(drop=True)
- 按订单ID 去重
keep="last":保留最后一条(即新导入的数据覆盖旧数据)- 适用于追加模式下的数据更新场景
11.6 表单实时预览
javascript
[els.formQuantity, els.formUnitCost, els.formUnitPrice].forEach((el) => {
el.addEventListener("input", updateFormPreview);
});
function updateFormPreview() {
var sales = price * qty;
var profit = (price - cost) * qty;
els.previewSales.textContent = money(sales);
els.previewProfit.textContent = money(profit);
}
用户输入数量、成本、单价时,实时计算并显示预计销售额和利润。
十二、启动与部署
12.1 环境要求
| 要求 | 版本 |
|---|---|
| Python | >= 3.8 |
| pip | 最新版 |
| 操作系统 | Windows / macOS / Linux |
12.2 安装与启动
bash
# 1. 进入项目目录
cd code
# 2. 安装依赖
pip install -r requirements.txt
# 3. 启动服务
python app.py
默认监听 http://127.0.0.1:5050。
12.3 环境变量
| 变量 | 默认值 | 说明 |
|---|---|---|
PORT |
5050 | 服务监听端口 |
bash
# 自定义端口
PORT=8080 python app.py
12.4 数据文件要求
确保 体育用品销售数据_1000行.xlsx 位于项目根目录(与 app.py 同级)。系统首次启动时自动加载并缓存。
12.5 生产部署建议
bash
# 使用 Gunicorn(Linux/macOS)
pip install gunicorn
gunicorn -w 4 -b 0.0.0.0:5050 app:app
# 使用 waitress(Windows)
pip install waitress
waitress-serve --port=5050 app:app
注意事项:
- LRU 缓存在多 worker 模式下每个进程独立,需改用 Redis
- Excel 文件写入不支持并发,多进程写入可能导致数据损坏
- 建议在 Nginx 反向代理后部署
十三、扩展建议
| 方向 | 现状 | 建议 |
|---|---|---|
| 数据存储 | Excel 文件 | 迁移至 SQLite(轻量)或 PostgreSQL(生产级),支持并发写入 |
| 用户认证 | localStorage 客户端认证 | 引入 Flask-Login + 服务端 Session + 密码哈希(bcrypt) |
| 图表交互 | 静态 SVG + 原生 tooltip | 集成 ECharts 或 Chart.js 实现缩放、钻取、动画 |
| 缓存 | LRU 内存缓存 | 使用 Redis 替代,支持多进程/多服务器共享 |
| 部署 | 单进程 Flask 开发服务器 | Gunicorn + Nginx 反向代理 + Docker 容器化 |
| 测试 | 无 | 添加 pytest 单元测试覆盖聚合函数和 API 端点 |
| API 安全 | 无校验 | 增加分页参数校验、速率限制、CORS 配置、API Key 认证 |
| 数据量 | 1000 行 | 分页加载 + 数据库索引 + 后端分页优化 |
| 国际化 | 中文硬编码 | 提取文案到配置文件,支持多语言切换 |
| 日志 | 无结构化日志 | 引入 Python logging 模块,记录访问日志和错误日志 |