体育用品销售数据可视化分析系统 — 完整技术文档

版本:v1.0 | 最后更新:2026-05-20


目录

  1. 项目概述
  2. 技术栈与依赖
  3. 项目目录结构
  4. 数据模型
  5. 后端架构设计
  6. 前端架构设计
  7. API 接口规范
  8. 前端模块详细设计
  9. 样式系统与响应式设计
  10. 核心算法详解
  11. 关键实现细节
  12. 启动与部署
  13. 扩展建议

一、项目概述

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 依赖安装

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 顶部)

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 数据加载层

@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 筛选引擎

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 数据持久化

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) 函数实现:

  1. 更新页面标题、描述(从 pageMeta 映射表读取)
  2. 切换侧边栏导航的 .active
  3. 切换对应的 .page-section.active 类(CSS 控制显隐)
  4. 更新 window.location.hash
  5. 对数据页面隐藏/显示筛选面板和导出按钮
  6. 切换到数据管理页时自动加载记录

pageMeta 映射表:

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 函数):

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 状态管理

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

{
  "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 — 筛选选项

返回所有下拉框的可选值。

响应结构:

{
  "date_min": "2024-01-01",
  "date_max": "2024-12-31",
  "categories": ["健身器材", "球类", "运动服饰", "运动鞋", "..."],
  "regions": ["华东", "华南", "华北", "华中", "西南", "西北", "东北"],
  "provinces": ["上海", "江苏", "浙江", "..."],
  "customer_types": ["个人", "企业", "团购"]
}
GET /api/overview — 经营总览

返回 KPI 指标、月度趋势、分组统计和热力矩阵。

响应结构:

{
  "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 种图表所需的数据集。

响应结构:

{
  "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 — 深度分析

返回洞察卡片、排行榜和预警数据。

响应结构:

{
  "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}
  ]
}

洞察卡片生成逻辑:

  1. 核心品类:取销售额最高的品类
  2. 重点区域:取销售额最高的区域
  3. 利润效率:取利润率最高的区域
  4. 整体健康度:综合利润率 + 总订单数 + 总销售额
GET /api/forecast — 销售预测

请求参数:

参数 类型 默认 范围 说明
steps int 6 1-12 预测期数

响应结构:

{
  "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 筛选引擎

响应结构:

{
  "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):

{
  "销售日期": "2024-06-01",
  "产品类别": "运动鞋",
  "产品名称": "跑步鞋A",
  "销售区域": "华东",
  "所属省份": "上海",
  "客户类型": "个人",
  "订单数量": 2,
  "单位成本": 200,
  "销售单价": 500
}

处理流程:

  1. parse_record_body() 解析 9 个基础字段
  2. compute_derived() 计算 4 个派生字段
  3. next_order_id() 自动生成订单 ID
  4. 拼接到现有 DataFrame 末尾
  5. save_data() 写入 Excel 并清除缓存

响应:

{"success": true, "订单ID": "ORD001001", "total": 1001}
PUT /api/records/{id} — 编辑订单

请求体: 同 POST,包含要更新的字段。

处理流程:

  1. 定位目标行(df[订单ID] == record_id
  2. 解析并计算派生字段
  3. 更新 DataFrame 中对应行的所有字段
  4. 重新计算 月份销售日期文本
  5. save_data() 写入

响应:

{"success": true, "订单ID": "ORD001001"}
DELETE /api/records/{id} — 删除订单

处理: 过滤掉目标行 → save_data() 写入。

响应:

{"success": true, "total": 999}
POST /api/import — 批量导入

请求: multipart/form-data

字段 类型 说明
file File .xlsx / .xls / .csv 文件
mode string append(追加)或 replace(覆盖)

处理流程:

  1. 根据文件扩展名选择 pd.read_csvpd.read_excel
  2. 校验必需列
  3. 类型转换(日期、数值)
  4. 追加模式:与现有数据合并;替换模式:直接使用上传数据
  5. 按订单ID去重(drop_duplicates,保留最后一条)
  6. 计算派生字段
  7. save_data() 写入

响应:

{"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 自定义属性)

: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 两栏

.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 无障碍 禁用所有 transitionanimation

9.4 图表样式

SVG 图表通用样式:

.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):

.donut {
  width: 200px; height: 200px;
  border-radius: 50%;
  background: conic-gradient(...);
  /* 内圈白色圆形通过伪元素或内层 div 实现 */
}

热力图(CSS Grid):

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

.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 线性回归 + 移动平均混合预测

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

置信度衰减:

confidence = max(0.62, 0.9 - index * 0.04)
  • 第 1 期:0.90
  • 第 2 期:0.86
  • 第 3 期:0.82
  • 第 7 期及以后:0.62(下限)

10.2 订单 ID 生成算法

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 派生字段计算

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 折线图坐标计算

// 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 气泡图半径映射

// 气泡半径 = 基础半径 + 比例缩放
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 热力图颜色强度

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() 函数转义:

function escapeHtml(value) {
  return String(value ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

所有 API 返回的数据在渲染到 HTML 前都经过此函数处理。

11.4 CSV 导出 BOM 编码

csv_text = output.to_csv(index=False, encoding="utf-8-sig")

使用 utf-8-sig 编码自动添加 BOM 头(\xEF\xBB\xBF),确保 Microsoft Excel 正确识别中文编码。

11.5 数据导入去重

result = result.drop_duplicates(subset=["订单ID"], keep="last").reset_index(drop=True)
  • 按订单ID 去重
  • keep="last":保留最后一条(即新导入的数据覆盖旧数据)
  • 适用于追加模式下的数据更新场景

11.6 表单实时预览

[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 安装与启动

# 1. 进入项目目录
cd code

# 2. 安装依赖
pip install -r requirements.txt

# 3. 启动服务
python app.py

默认监听 http://127.0.0.1:5050

12.3 环境变量

变量 默认值 说明
PORT 5050 服务监听端口
# 自定义端口
PORT=8080 python app.py

12.4 数据文件要求

确保 体育用品销售数据_1000行.xlsx 位于项目根目录(与 app.py 同级)。系统首次启动时自动加载并缓存。

12.5 生产部署建议

# 使用 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 模块,记录访问日志和错误日志
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐