ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Flask和ECharts的餐饮销售趋势可视化大屏实现

基于Flask和ECharts的餐饮销售趋势可视化大屏实现 在接手这套基于 Flask 的餐饮管理系统之前我一直觉得可视化大屏这个词离传统餐饮店很遥远。直到帮一个做连锁快餐的朋友做门店运营诊断看到他每天靠 Excel 表格手工对比各时段的营业额、逐个菜品翻销量我才意识到很多餐饮老板真正缺的不是收银系统而是一眼看懂销售趋势的能力。所以这套系统的核心没有放在菜品管理、桌台管理这些常规功能上而是把销售趋势分析和可视化大屏作为主战场。整套项目用 Python 生态里的 Flask 框架承载后端接口和页面渲染配合 ECharts 做数据可视化大屏从数据库设计到接口封装再到前端图表的联动刷新完整走通了一条餐饮销售数据从存储到决策的链路。这篇内容我会把整个项目的完整拆解过程写出来包括需求分析角度、Flask 在中小型管理系统里的真实定位、销售趋势数据表的建模细节、核心接口的 SQL 聚合写法、大屏布局和图表选型逻辑以及我在部署和联调阶段踩过的坑。适合正在做 Python Web 项目、打算用 Flask 做管理后台或者想入门数据可视化大屏的开发者参考也适合刚学完 Flask 基础、不知道从哪下手找项目的朋友。1. 为什么餐饮管理系统最该做销售趋势这个模块传统餐饮管理系统的菜单里通常堆着一大堆功能会员管理、库存管理、员工排班、采购进货、财务对账。功能看着很全但老板真正每天打开系统的第一件事往往只是想知道三件事今天卖了多少、现在是什么趋势、哪个菜品该补货或该下架。销售趋势模块的价值就在这里它把散落在订单表里的数据转化成可以直接指导经营决策的信息。1.1 从店长和老板的日常痛点反推功能清单我在项目启动前做了一轮现场调研记录下来的核心问题比较集中。店长最关心的是某个菜品在这一天中的销量分布比如午市和晚市各占多少好决定备货量。老板最关心的是本周和上周相比营业额涨了还是跌了以及涨跌是出在哪个时间段、哪个品类上。采购的人则更关注菜品排行以便调整采购计划。这些痛点最后落在功能清单上就变得很清晰营业总览今日营业额、订单数、客单价、翻台率等核心指标卡时段趋势分析按小时维度展示营业额和订单量的波动曲线跨天趋势对比最近 7 天、30 天的日营业额趋势支持按周维度对比菜品销售排行按销售额、销量两个维度排序的 Top10 菜品菜品分类占比不同品类热菜、凉菜、饮品、主食的销售结构星期时段热力图一周七天 x 营业时段 的订单热度分布用来辅助排班和营销这套功能组合的思路是先给整体结论再看时间维度然后落到具体菜品。老板大屏上只需要看一眼就能做出判断不需要自己去翻 Excel 或者问店长。可视化大屏解决的不是有没有数据的问题而是数据能不能被人快速读懂的问题。1.2 业务边界只做销售趋势不做全链路确定功能边界这件事很容易被忽视。刚开始我也考虑过把进销存、会员储值、营销活动全做进去后来跟实际用户反复确认后砍掉了。原因很简单销售趋势分析的底层数据来源是订单和订单明细这两张表的数据质量足够支撑决策而库存和采购涉及的操作链路太长做进去会拖慢项目节奏。所以这个系统的数据边界最终限定为三块基础档案数据菜品分类、菜品、交易流水数据订单主表、订单明细、时间维度数据。会员信息和员工信息暂时不纳入趋势分析最多在订单表里预留一个 member_id 字段用于后续扩展。定位清晰之后数据库设计和接口设计就比较聚焦了也不会出现一张大宽表装所有字段的情况。2. 技术选型与系统骨架Flask ECharts 的配合逻辑选型这件事没有什么神秘的关键在于你的项目体量和团队的熟悉程度。我之前也做过 Spring Boot 的管理系统也用过 Django但回到餐饮销售趋势这个场景Flask 反而是更合适的选择。为什么这么说下面拆开来讲。2.1 Flask 在中小型管理系统里的真实定位Flask 的核心优势就是轻和灵活。轻指的是整个框架本身不带强制约束你可以自由选择用不用 ORM、用不用蓝图、用不用 SQLAlchemy。灵活指的是它的扩展生态足够丰富需要什么装什么不需要的不会出现在项目里。对于一套面向中小餐饮门店的数据管理后台Flask 的启动速度和开发节奏都能很好地匹配需求。我用一张表来说清楚常见 Web 框架在这个场景下的取舍框架适用规模学习曲线模板渲染生态情况我的评价Flask中小型系统、数据可视化后台平缓内置 Jinja2丰富扩展自由组合上手快改造成本低Django中大型系统、功能全面的 CMS较陡自带模板系统全家桶很多内置功能对于趋势大屏过于重量级FastAPI高并发 API、前后端分离中等不擅长模板渲染现代异步生态适合纯接口项目不适合快速渲染后端页面Spring Boot企业级大规模系统陡峭Thymeleaf 等极其庞大餐饮小系统用它属于杀鸡用牛刀这个项目里我选择 Flask 还有一个很重要的原因可视化大屏页面本身需要服务端渲染首页模板同时又要提供 JSON 接口给前端图表异步取数。Flask 天然支持这两种模式一个 render_template 搞定首屏一组 app.route 装饰器搞定 API不需要单独起 Node 服务开发调试成本明显下降。2.2 前后端配合方式模板渲染与异步接口并存很多刚开始接触 Flask 的人会被Flask 如何绑定到网页元素这个问题卡住。实际上 Flask 本身并不直接操作网页元素它只负责把数据传出去。数据的传递有两种方式。第一种是服务端渲染在视图函数里通过 render_template 将变量传给 Jinja2 模板模板里用 {{ variable }} 或者 {% for %} 语法把数据渲染进 HTML。这种方式适合页面结构固定、数据更新不频繁的页面比如菜品管理的列表页。第二种是异步接口模式前端页面通过 JavaScript 里的 fetch 或 axios 请求 Flask 提供的 API 路由后端返回 JSON前端再把数据填充到图表组件里。可视化大屏上需要更新的数据基本都是用这种方式。一个容易让新手迷惑的细节是Flask 路由既可以返回 HTML 页面也可以返回 JSON 数据这完全取决于视图函数里 return 的内容。返回字符串、字典、Response 对象都可以。我在大屏项目里定义了统一约定凡是 /page/ 开头的路由都返回模板页面凡是 /api/ 开头的路由都返回 JSON开发的时候路径清晰不容易混乱。3. 数据库设计支撑销售趋势分析的表结构拆分销售趋势分析的核心是聚合查询而聚合查询最怕的是表结构设计不合理。如果一张订单表里既存菜品信息又存订单金额还存了分类和门店信息那查询的时候虽然 Join 少了但数据冗余和维护成本会很高。我最终把核心表拆分成了五张菜品分类表、菜品表、订单主表、订单明细表以及一个可选的日期维表。3.1 订单主表与订单明细表拆分的原因先说订单主表和订单明细表为什么要拆分。一个订单会包含多个菜品如果把菜品名称、数量、金额直接塞进订单主表那同一笔订单在不同菜品之间会产生大量重复的主表字段。以一份点了三个菜的订单为例订单号、支付时间、门店编号这些字段会在表里重复三行既浪费存储又很容易在统计时算错订单数。拆分之后订单主表保存一笔订单的公共信息比如订单号、门店编号、桌台号、支付金额、支付时间、订单状态。订单明细表则保存每个菜品在订单中的具体行比如菜品 ID、菜品名称、单价、数量、行金额。统计某个菜品的总销量时只需要扫明细表统计营业额时只需要聚合主表需要分析客单价时用主表订单数做除法。职责清晰索引也好建。下面是核心建表结构我按实际项目整理了一个精简版本CREATE TABLE goods_category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort_order INT DEFAULT 0 COMMENT 排序权重 ); CREATE TABLE goods ( id INT AUTO_INCREMENT PRIMARY KEY, category_id INT NOT NULL COMMENT 所属分类ID, name VARCHAR(100) NOT NULL COMMENT 菜品名称, price DECIMAL(10,2) NOT NULL COMMENT 销售单价, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id) ); CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT 订单号, store_id INT NOT NULL COMMENT 门店ID, table_no VARCHAR(20) COMMENT 桌台号, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单原价总额, pay_amount DECIMAL(10,2) NOT NULL COMMENT 实付金额, order_status TINYINT NOT NULL DEFAULT 1 COMMENT 订单状态: 1已完成 0已取消, pay_time DATETIME DEFAULT NULL COMMENT 支付完成时间, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_store_paytime (store_id, pay_time), INDEX idx_paytime_status (pay_time, order_status) ); CREATE TABLE order_items ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL COMMENT 关联订单主表ID, goods_id INT NOT NULL COMMENT 菜品ID, goods_name VARCHAR(100) NOT NULL COMMENT 冗余菜品名称, price DECIMAL(10,2) NOT NULL COMMENT 下单时单价, quantity INT NOT NULL COMMENT 数量, amount DECIMAL(10,2) NOT NULL COMMENT 行小计金额, INDEX idx_order (order_id), INDEX idx_goods (goods_id) );3.2 时间字段和索引设计是趋势查询的关键销售趋势分析的所有语句几乎都离不开时间维度所以时间字段的设计我特别谨慎。orders 表里我同时保留了 created_at 和 pay_time 两个时间字段这是有讲究的。created_at 是订单创建时间适合分析下单行为pay_time 是支付完成时间只有支付完成的订单才算有效营业额。统计趋势时默认用 pay_time同时过滤 order_status 1避免把用户取消的订单算进营收。索引设计上最常用到的查询条件是门店 时间范围内 状态有效所以建了一个联合索引 idx_store_paytime (store_id, pay_time)。联合索引里字段的顺序是有讲究的因为 shop 查询基本是等值条件pay_time 是范围条件把等值字段放在范围字段前面索引才能最大化生效。另外还有一个常用的状态过滤条件所以我又补了 idx_paytime_status (pay_time, order_status)。实际跑下来几十万级别的订单量在这种索引下聚合查询基本都是毫秒级响应。日期维表是另一个容易被忽略的优化点。如果系统要支持未来扩展跨年、跨季度的复杂对比基于日期维表做 LEFT JOIN 可以方便地补全没有订单的日期避免折线图出现断点。但我这套系统第一版没有单独做日期维表而是在接口层用 Python 生成连续的日期序列来补零这样做更轻量也少维护一张表。后面如果数据量上去了再考虑落一张 date_dim 表也不迟。4. 后端接口实现趋势数据怎么从 SQL 变成 JSON数据库设计得再合理最终也是要通过接口把数据送给前端的。Flask 后端的核心工作就是把 SQL 聚合结果转换成前端图表能直接消费的 JSON 结构。这一节我把项目里最核心的三个趋势接口拿出来拆解。4.1 三个最核心的销售趋势接口第一个接口是营业趋势接口 /api/sales/trend它返回指定时间范围内、按小时或按天聚合的营业额和订单数。前端折线图和柱状图都依赖这个接口。SQL 里用 DATE_FORMAT 对 pay_time 做格式化再按维度分组。app.route(/api/sales/trend) def sales_trend(): granularity request.args.get(granularity, hour) start_date request.args.get(start_date, ) end_date request.args.get(end_date, ) params [] where WHERE order_status 1 if start_date: where AND pay_time %s params.append(start_date) if end_date: where AND pay_time %s params.append(end_date 23:59:59) if granularity hour: sql SELECT DATE_FORMAT(pay_time, %Y-%m-%d %H:00) AS time_point, SUM(pay_amount) AS amount, COUNT(*) AS order_count FROM orders {where} GROUP BY DATE_FORMAT(pay_time, %Y-%m-%d %H:00) ORDER BY time_point .format(wherewhere) else: sql SELECT DATE_FORMAT(pay_time, %Y-%m-%d) AS time_point, SUM(pay_amount) AS amount, COUNT(*) AS order_count FROM orders {where} GROUP BY DATE_FORMAT(pay_time, %Y-%m-%d) ORDER BY time_point .format(wherewhere) cursor db.cursor() cursor.execute(sql, tuple(params)) rows cursor.fetchall() return jsonify({ code: 0, data: { time_points: [r[time_point] for r in rows], amounts: [float(r[amount]) for r in rows], order_counts: [r[order_count] for r in rows] } })第二个接口是菜品排行接口 /api/sales/top_dishes用于大屏右侧的 Top10 榜单。这个聚合要跨订单主表和订单明细表把已完成的订单关联后按菜品分组排序。app.route(/api/sales/top_dishes) def top_dishes(): limit int(request.args.get(limit, 10)) days int(request.args.get(days, 7)) start datetime.now() - timedelta(daysdays) sql SELECT gi.goods_name, SUM(gi.quantity) AS total_quantity, SUM(gi.amount) AS total_amount FROM order_items gi INNER JOIN orders o ON gi.order_id o.id WHERE o.order_status 1 AND o.pay_time %s GROUP BY gi.goods_id, gi.goods_name ORDER BY total_amount DESC LIMIT %s cursor db.cursor() cursor.execute(sql, (start, limit)) rows cursor.fetchall() return jsonify({ code: 0, data: { names: [r[goods_name] for r in rows], amounts: [float(r[total_amount]) for r in rows], quantities: [r[total_quantity] for r in rows] } })第三个接口是分类占比接口 /api/sales/category_ratio前端用环形图展示热菜、凉菜、饮品、主食的销售额占比。这里要注意过滤掉菜品已删除的情况避免分类关联不上导致的数据空洞。4.2 调试接口时最容易忽略的数据类型细节Flask 接口开发完成后联调阶段最容易出问题的不是路由写错而是数据类型的隐性坑。我在调试时就踩过几次这里帮大家提前避一下。第一个坑是 Decimal 类型无法被 jsonify 直接序列化。数据库里的金额字段 DECIMAL 类型取出来后是 Decimal 对象直接返回给 jsonify 会报 TypeError。解决办法是在返回之前统一转换成 float或者在 JSON 序列化器里注册一个 Decimal 转换函数。我建议在项目里直接定义一个通用的 JSONEncoder 子类后续所有接口都不用担心这个。第二个坑是 datetime 类型在 JSON 序列化时也需要处理。如果 SQL 直接返回原始的时间字段jsonify 同样会报错。所以我在接口层会刻意用 DATE_FORMAT 把时间转成字符串或者在序列化时做默认参数处理。第三个坑来自flask 查看从客户端获取的变量数据类型这个高频问题。排查接口参数时很多人会下意识以为 request.args.get 返回的是对应类型其实 Flask 拿到的全都是字符串。比如前端传 granularity7request.args.get 拿到的是 7如果你在代码里做数字运算直接报错。调试时最直接的办法就是 print(type(request.args.get(days)))确认之后再用 int() 转换。我习惯在进入正式逻辑前对每个参数做一次显式类型转换并且用 try/except 捕获异常返回参数错误提示给前端避免 500 错误。4.3 封装公共查询函数减少重复代码三个核心接口写完后我发现 SQL 里有大量重复的时间范围拼接逻辑。如果每个接口都写一遍 start_date 判断后面维护成本很高。所以我把公共逻辑抽成了一个函数输入时间范围、门店 ID、是否只算有效订单输出 WHERE 条件子句和参数列表。def build_date_filter(start_date, end_date, store_idNone): where [order_status 1] params [] if start_date: where.append(pay_time %s) params.append(start_date) if end_date: where.append(pay_time %s) params.append(end_date 23:59:59) if store_id: where.append(store_id %s) params.append(store_id) return AND .join(where), params这个函数看起来简单却让后续新增接口的效率高了很多。所有趋势类查询都基于它拼接 WHERE 条件参数化查询也顺带解决了 SQL 注入问题。写接口的时候我始终坚持一个原则任何外部传入的参数都不直接拼接到 SQL 字符串里必须走参数化。哪怕是一个整型参数也要走 %s 占位这个习惯能避免绝大多数安全隐患。5. 可视化大屏从数据到图表布局与色彩如何服务于决策后端接口就绪后大屏本体的设计才是真正出效果的地方。很多人做可视化大屏容易陷入图表堆砌的状态什么图都往上放结果全是信息噪声。我的经验是大屏的布局必须围绕决策路径来组织。老板看大屏的顺序应当是从宏观到微观从趋势到明细。5.1 大屏布局的黄金结构与图表选型逻辑我用的是一种比较经典的三栏式布局适配 1920x1080 的分辨率。顶部是标题栏和当前时间整个大屏最显眼的位置放营业总览指标卡。左侧从上到下依次放菜品销售排行和订单量时段分布右侧放品类销售占比和星期时段热力图。中间主区域给营业趋势总览这是整个大屏信息量最大、最核心的位置。图表选型上有一条很实用的匹配逻辑我每次做可视化都会先做这个判断要表达随着时间变化的趋势就用折线图要对比分类别的大小差异就用柱状图要呈现占比关系就用饼图或环形图要表达两个维度的分布强度就用热力图。这套系统里的四类图表正好对应这四种场景。营业趋势总览折线图展示营业额和订单数双 Y 轴随时间变化菜品销售排行横向柱状图方便直接对比菜名对应的销售额高低品类销售占比环形图中间放总营业额数字视觉冲击力强星期时段热力图热力图X 轴是营业时段Y 轴是星期颜色深浅表达订单密度大屏的配色我建议以深色背景为主因为大屏通常放在门店或办公室深色背景更容易突出数据的光影效果也避免长时间观看造成视觉疲劳。图表主色用高饱和度的亮色比如青色、橙色、黄色与深色背景形成对比。数据指标卡用较大字号的关键数字配合小的描述文字保证远距离也能看清。5.2 数据刷新与 ECharts 联动更新的实现细节大屏不可能只显示静态数据所以定时刷新是标配。我在前端用一个 setInterval 定时器每 60 秒请求一次最新数据接口拿到新数据后通过 ECharts 实例的 setOption 方法更新图表。这里有一个关键参数需要说明setOption 默认是 merge 模式如果直接传入新的 series 数组可能会和旧的 series 合并导致数据错乱。所以我在更新图表时会显式传入 notMerge 参数保证每次用新数据整体替换旧数据。async function loadTrendData() { const res await fetch(/api/sales/trend?granularityhour); const json await res.json(); if (json.code 0) { trendChart.setOption({ xAxis: { data: json.data.time_points }, series: [ { name: 营业额, type: line, data: json.data.amounts }, { name: 订单量, type: line, data: json.data.order_counts } ] }, true); } } setInterval(loadTrendData, 60000); loadTrendData();初次加载时页面可能还没准备好所以我会把图表初始化放在 window.onload 里执行。还要注意图表实例的销毁问题大屏页面如果涉及切换路由或关闭页面需要在 beforeDestroy 或卸载时调用 chart.dispose() 释放内存否则频繁刷新会积累内存泄漏。这个细节浏览器短时间看不出差别但大屏通常 7x24 小时运行跑一两天就能感觉到页面变得卡顿。5.3 前后端联调时避免常见的 404 和样式丢失大屏页面做好之后联调阶段最常见的坑是静态资源 404。因为 Flask 默认的模板目录和静态目录是有约定的如果模板文件放在 templates 目录下、静态文件放在 static 目录下那使用 url_for(static, filenamejs/app.js) 能正常生成路径。但有些人会把前端构建后的 dist 目录直接挂进 Flask这时候就要配置 StaticFolder 和 TemplateFolder 指向 dist 目录否则浏览器控制台会报一堆 CSS、JS 加载失败。我在这套系统里采用了比较简单的组织方式没有引入前端构建工具直接使用原生 HTML JavaScript ECharts静态文件按 js 和 css 分别存放。这样做的原因是大屏页面的交互复杂度不高原生 JS 完全够用还省去了 webpack、vite 等构建工具的配置时间。如果你喜欢用 Vue 或 React 搭建大屏也可以采用前后端分离的方式但那样就需要处理跨域问题在 Flask 端配置 CORS 响应头。我建议先把前后端耦合的版本跑通再考虑拆前端工程。6. 部署与踩坑经验从本地开发到服务器运行的注意事项很多 Flask 项目在本地跑得好好的一部署到服务器就出各种问题。我自己第一次部署就遇到过附件路径错误和静态资源 404排查了很久才发现是工作目录和路径拼接方式的问题。这一节分享一些能直接落地的部署经验。6.1 本地开发环境最容易忽略的配置项在本地开发时Flask 默认的 app.run() 可以直接运行但这套默认配置是有隐患的。它默认只监听 127.0.0.1端口是 5000而且 debug 模式关闭。本地调试时通常你会开启 debugTrue这样可以查看异常堆栈和自动重载但部署时一定要记得关闭 debug否则会暴露详细的错误信息甚至有远程执行代码的风险。环境隔离也很重要。我强烈建议在项目根目录创建虚拟环境用 venv 或 pipenv 把依赖隔离在项目内部不要把这些依赖安装到系统 Python 环境里。然后生成一个 requirements.txt把 Flask、Flask-CORS、PyMySQL、gunicorn 等依赖固定版本写进去。这样换一台服务器部署时一行 pip install -r requirements.txt 就能还原环境。如果你的本地环境是 VSCode记得先配置好 Python 解释器路径指向虚拟环境里的 python。很多新人卡在代码在终端跑不起来的问题往往就是因为解释器指向了系统自带的 Python找不到项目里安装的 Flask。6.2 服务器部署的三层坑路径、静态资源、进程管理第一层是最容易出问题的路径坑。Flask 项目里如果有文件读写操作比如保存菜品图片、导出 Excel 报表千万不要直接用相对路径。因为在本地开发时程序工作目录通常就是项目根目录相对路径看着没毛病但部署到服务器上以后工作目录可能是启动命令所在的目录或者 systemd 服务指定的目录相对路径就会指向错误的位置。如果你在项目里写过类似 open(uploads/a.jpg, wb) 这样的代码部署后大概率会报附件路径找不到或者 Permission Denied。正确做法是永远基于file来推导项目根目录import os BASE_DIR os.path.dirname(os.path.dirname(os.path.abspath(__file__))) UPLOAD_FOLDER os.path.join(BASE_DIR, uploads)或者用 Flask 自带的 instance_pathUPLOAD_FOLDER os.path.join(app.instance_path, uploads)我个人更推荐基于file的方式因为它无论从哪里启动都能准确定位到项目目录不会受到当前工作目录的影响。这是我在部署踩坑后总结出来的硬经验。第二层是静态资源问题。本地访问 http://127.0.0.1:5000 一切正常部署到服务器后 CSS、JS 加载不出来通常有两个原因。一是项目使用了 Nginx 反向代理但没有给静态资源配置合适的 location 规则二是 Flask 在非 debug 模式下默认不会主动服务项目根目录下的文件你需要确保所有静态资源都放到了 Flask 的 static 目录里并使用 url_for(static, ...) 生成路径。为了减少服务器压力我一般会在 Nginx 里单独配置静态资源路径让 Nginx 直接返回文件而不是经过 Flask 进程处理。第三层是进程管理。Flask 自带的 app.run() 开启的开发服务器性能很差不适合直接在生产环境运行。我的选择是使用 gunicorn 作为 WSGI 服务器配合多 worker 提升并发能力。启动命令如下gunicorn -w 4 -b 127.0.0.1:8000 app:app如果服务器是 Windows 而不是 Linuxgunicorn 就不可用了可以改用 waitresswaitress-serve --listen127.0.0.1:8000 app:app进程管理方面Linux 下我会写一个 systemd 服务单元文件配置好启动脚本、工作目录和日志输出然后通过 systemctl 管理服务的启停和开机自启。Nginx 反向代理到 127.0.0.1:8000对外暴露 80 端口。这样整个服务才算是生产可用的状态。6.3 大屏系统的性能优化与数据量增长后的对策餐饮系统的订单数据增长很快一个生意好的门店一天能产生上千笔订单一年下来就是几十万行。如果大屏每次刷新都实时扫全表聚合数据量大了以后响应时间会明显上升。我实际验证过在订单量超过 50 万行之后没有预聚合的按小时聚合查询会从毫秒级退化到秒级大屏刷新就会出现卡顿感。针对这个问题我常用三个层面的优化手段。第一是索引优化确保 pay_time 和 order_status 有联合索引这是成本最低的优化。第二是预聚合表在 MySQL 中建立一张 sales_daily_summary 表每天凌晨用定时任务把前一天的数据按天、按小时、按菜品分类聚合好写入表中大屏查询直接读预聚合结果不再实时扫订单明细。第三是加一层 Redis 缓存把热点接口的查询结果缓存 60 秒大屏定时刷新期间直接命中缓存数据库压力能降一个量级。这三个手段按需叠加使用基本能让大屏在数据量翻倍后依然保持流畅。结尾一点真实的项目心得回头再看这个项目我最想强调的倒不是某个语法或某个框架特性而是先想清楚业务到底需要看什么再动手写代码。我在做销售趋势大屏之前最大的误区是一上来就研究各种炫酷图表后来发现如果数据模型设计得不好图表再漂亮也只是花瓶。把订单主表和订单明细表拆分好把 pay_time 和 order_status 的索引建对把接口返回结构统一好大屏的效果自然就出来了。另外说句实在的这套系统用到的 Flask 知识点并不难难度更多集中在如何把一份业务数据从数据库里准确、高效地聚合出来再转换成图表能用的结构。这个过程涉及 SQL 聚合、接口设计、前端渲染、部署运维等多个环节每一步都有值得琢磨的小细节。我个人建议你在复刻的时候先跑通一个最小闭环建表、写一个趋势接口、用一个 ECharts 折线图展示然后再逐步叠加其他图表和功能模块。先让数据流动起来再谈美化。
返回列表