ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Flask+Vue大型超市生鲜数据处理系统:库存、损耗与看板实战

Flask+Vue大型超市生鲜数据处理系统:库存、损耗与看板实战 生鲜这块业务是所有超市里最难管的一条线。不是说“进出货”有多难而是“数据”太容易失真今天到货的青菜晚上损耗多少、卖了多少、剩了多少如果全靠人脑子记到月底盘完亏了都不知道亏在哪。我做这个“Python flask 大型超市生鲜数据处理系统”的初衷就是用 Flask 撑后端接口、Vue 撑前端页面把采购入库、库存变动、销售流水、损耗报损这一整套流程里的数据沉淀下来再通过图表看板把“损耗率、售罄率、品类占比”这些关键指标直接摆到屏幕上。这套系统适合谁如果你正在做一个前后端分离的课设、毕设或者想给一家小型超市门店做一套内部管理系统那本文的思路、表结构、踩坑记录基本可以照抄。我会尽量把每个设计决策的“为什么”也讲清楚而不是直接贴一堆代码就完事。1. 项目整体设计思路拆解1.1 为什么是 Flask Vue 这对组合先说结论选Flask不是因为“不会用别的框架”而是“没必要用更重的框架”。这个系统的核心任务不是处理百万级高并发而是把生鲜超市日常产生的数据整理成可分析、可看板、可追溯的记录。Flask 的轻量特性在这里正好——你想把 Python 的数据处理能力pandas、numpy、统计分析直接塞进接口逻辑里Flask 是最顺手的容器。Vue 作为前端则是冲着“看板体验”去的。生鲜超市的管理者需要在一个页面上快速看到今天卖了多少、什么品类滞销、哪个供应商的货损耗特别大。Vue 的组件化加上ECharts做一个数据大屏比传统服务端渲染的 jQuery 页面要舒服得多切换页面不用整页刷新操作起来更像一个“正经软件”。还有一个很实际的理由这个项目如果放进简历里前后端分离是一个加分项。前端 Vue 后端 Flask RESTful API MySQL这组技术栈是现在小型企业管理系统里最常见的搭档。做毕设也好、做个人项目也罢面试官看到这套组合不会觉得你在玩玩具因为它涵盖了完整的开发链路。1.2 生鲜业务需求与常规进销存系统的差异普通超市的进销存系统核心是“账实相符”——入库、出库、盘点把库存数量管住就行。但生鲜数据处理系统多了一个极其关键的需求损耗管理。我在设计初期最深刻的体会是生鲜品和日用百货完全不是一个物种。比如一箱饮料放三个月库存数字不会变但一筐草莓放一天损耗就开始了。生鲜的损耗来源非常杂自然失水、磕碰变质、超过保质期、加工切配损耗、顾客挑选导致的损耗……如果系统里不做专门记录这些损耗最后全都变成了“销售额低”的模糊解释。所以这套系统在传统的进销存基础上必须增加三块内容报损单登记记录每种生鲜每天的报损数量与报损原因这是分析损耗率的数据源头。时效追踪生鲜商品有保质期字段库存列表里按剩余保质期排序快到期的自动标黄绝不依赖人肉记忆。价格调整记录生鲜价格波动大晚上促销打折、早起新鲜上架价高每次调价都要留痕迹否则毛利分析就会失真。另一个差异是数据量级。一个中型超市的生鲜区SKU 可能有三四百个每天每个 SKU 都可能发生采购、销售、报损三条记录一天就是上千条数据。这不至于压垮数据库但如果你用“每笔查询都全表扫描”的写法页面会明显变卡。所以在表设计和查询逻辑里从一开始就要考虑按日聚合、按品类分组的统计分析。1.3 功能模块划分与权限设计我最终把所有功能收敛成了七个模块每个模块对应一组接口、一组前端页面模块核心功能面向角色用户认证登录、登出、角色校验所有用户商品管理生鲜商品 CRUD、保质期配置管理员、采购员供应商管理供应商信息与供货记录管理员、采购员库存管理入库、出库、报损、库存预警仓管员销售管理销售流水录入、查询、商品售罄标记收银员、店长数据分析销售趋势、品类占比、损耗率、毛利统计店长、经理系统设置用户管理、角色权限分配管理员这里有个细节值得展开不要把每个角色都做成独立的页面。很多新手做系统会给管理员、店长、收银员各写一套前端页面最后维护起来想死。正确的做法是同一套页面通过角色控制“可见按钮”和“可访问路由”。举个例子收银员登录后能看到销售录入和库存查询但页面上的“报损审批”“供应商管理”菜单对他们不可见管理员登录后则全部放开。这个能力在 Vue 里通过路由守卫加一个角色字段就能做到Flask 端用装饰器校验 JWT 里的角色标识两边配合把权限卡的明明白白。2. 数据库设计与API结构规划2.1 核心数据表结构与字段选型数据库设计决定这个项目是“作业”还是“作品”。我见过太多同学把商品表、库存表做成一张表结果报损、调价、出入库记录全没地方存最后数据乱成一锅粥。我的做法是拆层一张主数据表 多张流水表。主数据表管“静态信息”比如products表只存商品的基础属性字段类型说明idINT 自增主键商品IDcategory_idINT关联分类表nameVARCHAR(50)商品名称unitVARCHAR(10)单位斤/盒/袋purchase_priceDECIMAL(10,2)采购价sale_priceDECIMAL(10,2)当前售价shelf_lifeINT保质期天数supplier_idINT关联供应商表warning_stockINT库存预警阈值statusTINYINT1上架 0下架流水表则记录“每一次变化”。比如stock_records表专门存入库、出库、报损的动作字段类型说明idINT 自增主键记录IDproduct_idINT商品IDrecord_typeVARCHAR(20)in/out/lossquantityINT变动数量正数reasonVARCHAR(50)报损原因等operator_idINT操作人created_atDATETIME操作时间为什么要单独拆一张流水表而不是直接在inventory表里改数量因为“现在有多少”只是结果“发生了什么”才是分析的关键。你只有拿到完整的流水才能算出某个时间段内每种生鲜的损耗率、售罄率、周转天数。inventory表里的库存数量每次出入库时实时更新即可它只是一个余额快照。金额字段一律用DECIMAL(10,2)不用float。这是一个非常容易踩的坑Python 的float在计算金额时会出现 0.1 0.2 0.30000000000000004 这类精度问题。如果你的代码里有sale_price * quantity这种运算累计下来可能差出好几块钱生鲜单价低但量大误差会被放大。2.2 后端API路由设计与统一响应格式Flask 后端我采用了蓝图的组织方式按模块拆路由文件而不是全部堆在app.py里。如果项目规模不大你确实可以把所有路由写在一个文件里快速跑通但后续每加一个功能就要改这个文件git 冲突概率直接拉满。蓝图拆分后每个模块自己管自己的路由代码可读性也好很多。路由设计遵循 RESTful 风格POST /api/auth/login登录获取 tokenGET /api/products商品列表分页查询POST /api/products新增商品PUT /api/products/id修改商品信息DELETE /api/products/id下架商品GET /api/stock/records库存流水查询POST /api/stock/in入库登记POST /api/stock/loss报损登记GET /api/analytics/sales-trend销售趋势数据GET /api/analytics/loss-rate损耗率统计所有接口统一返回格式这一点对前后端联调至关重要。我用的格式是{ code: 200, message: success, data: {} }错误时返回{ code: 400, message: 商品名称不能为空, data: null }前端 axios 拦截器里统一判断code等于 200 才取data否则弹message。这样前端不用每个接口单独写错误分支代码量能省三分之一。2.3 前端Vue项目结构与页面规划Vue 端我用的是 Vue 3 Vite Element Plus 这个组合。Vite 的启动速度比 Webpack 快一个数量级开发体验非常好。Element Plus 的表格、表单、弹窗组件直接覆盖了后台管理系统 80% 的界面需求不用自己从零写下拉框和日期选择器。前端目录结构我习惯按“功能模块”划分而不是按“文件类型”划分src/ api/ # 每个模块的接口封装 components/ # 公共组件图表卡片、弹窗 router/ # 路由配置 views/ dashboard/ # 数据分析看板 product/ # 商品管理页面 stock/ # 库存管理页面 sales/ # 销售管理页面 loss/ # 报损管理页面 supplier/ # 供应商管理页面 login/ # 登录页 store/ # 用户状态管理路由这块有个经验后台管理系统不需要把每个菜单都写死在路由表里。因为不同角色能看的页面不同可以写一个createRouter函数在用户登录成功后根据其角色动态添加可用路由。这样一来前端路由表里只预留一个空壳登录后再往里塞路由配合后端的接口权限校验等于上了两把锁。3. 实操过程从零搭建到功能跑通3.1 环境准备Python、Node、MySQL先说 Python 环境。我自己用的是 Python 3.10下载安装时一定要勾选“Add Python to PATH”这个勾选没勾后面 pip 命令全在系统里找不到。安装完在命令行里执行python --version验证一下。如果你机器上装过多个 Python 版本建议给项目单独建虚拟环境避免包版本冲突python -m venv venv source venv/bin/activate # Windows 是 venv\Scripts\activateNode.js 我用的 LTS 版本直接用官网安装包自带 npm。Vue 项目的脚手架用 Vite 创建npm create vitelatest frontend -- --template vueMySQL 装 8.0 版本就好本地开发用 root 账号建一个supermarket_fresh数据库CREATE DATABASE supermarket_fresh DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;utf8mb4这个字符集要注意它能存表情符号和特殊字符避免后续导入数据时出现乱码问题。3.2 后端Flask工程搭建与配置后端工程结构是按模块拆分的核心依赖有四个flaskWeb 框架本身flask-sqlalchemyORM 操作数据库flask-jwt-extendedJWT 认证flask-cors解决跨域安装依赖时如果你用的 pip 下载速度慢可以临时切成清华或阿里源这个不用多说网上一搜就有。关键是别等下载到一半卡住然后怀疑人生。app.py入口文件里核心配置如下from flask import Flask from flask_cors import CORS from flask_jwt_extended import JWTManager from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] mysqlpymysql://root:passwordlocalhost:3306/supermarket_fresh app.config[SQLALCHEMY_TRACK_MODIFICATIONS] False app.config[JWT_SECRET_KEY] your-secret-key app.config[JWT_ACCESS_TOKEN_EXPIRES] timedelta(hours12) CORS(app, resources{r/api/*: {origins: *}}) db SQLAlchemy(app) jwt JWTManager(app)JWT 的过期时间我设成了 12 小时正好覆盖一个班次。如果用户下班前系统还在用12 小时不重新登录是合理的但别设成 7 天登录态过期太久对系统安全是个隐患。另外CORS的origins参数开发时用*没问题但部署到正式环境务必改成前端实际域名不然任何网站都能跨域请求你的接口。这个习惯要早点养成。3.3 核心模块代码实现登录认证是所有模块里最重要的后续所有接口都靠 JWT 来识别用户身份。Flask 端的登录接口逻辑很简单app.route(/api/auth/login, methods[POST]) def login(): username request.json.get(username) password request.json.get(password) user User.query.filter_by(usernameusername).first() if user and check_password_hash(user.password_hash, password): token create_access_token(identityuser.id, additional_claims{role: user.role}) return jsonify(code200, data{token: token, role: user.role}) return jsonify(code401, message用户名或密码错误)密码不能存明文werkzeug.security的generate_password_hash和check_password_hash就是干这个的。损耗率计算是这套系统的灵魂。所谓损耗率指的是在统计周期内报损数量占全部可售数量的比例。公式很简单损耗率 报损数量 / (期初库存 采购入库数量)后端用一条聚合查询就能拿到app.route(/api/analytics/loss-rate) jwt_required() def loss_rate(): results db.session.execute( text( SELECT p.category_id, SUM(sr.quantity) AS loss_qty, (SUM(p.initial_qty) SUM(p.purchase_qty)) AS available_qty FROM stock_records sr JOIN products p ON sr.product_id p.id WHERE sr.record_type loss GROUP BY p.category_id ) ).fetchall()这个接口返回的数据会直接变成前端的柱状图让你一眼看出哪个品类损耗最严重。销售趋势查询按天聚合app.route(/api/analytics/sales-trend) jwt_required() def sales_trend(): results db.session.execute( text( SELECT DATE(sale_time) AS day, SUM(quantity) AS total_qty, SUM(price * quantity) AS total_amount FROM sales WHERE sale_time DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(sale_time) ORDER BY day ) ).fetchall()注意这里SUM(price * quantity)用的是销售单价乘数量不是商品当前售价乘数量。为什么要单独存一个price字段因为生鲜价格经常变如果销售记录只存商品 ID过了一周你根本查不出当时到底卖了几块钱。价格快照是销售流水表里最容易漏但最关键的一个字段。3.4 前端Vue页面与ECharts图表接入前端登录页没什么好说的一个表单调一下接口存 token 就行。重点在于登录成功后怎么拿用户信息和角色。我的做法是登录接口返回 token 和 roletoken 存 localStoragerole 存 PiniaVue 的状态管理库。路由守卫里判断有没有 token没有就强制跳登录页有 token 再判断当前用户角色是否能访问目标路由。库存页面是数据交互最密集的一个页面。表格展示所有生鲜商品的实时库存、当前售价、保质期剩余天数并在剩余天数少于保质期三分之一时把这一行标记成橙色。这用 Element Plus 表格的row-class-name回调就能实现核心逻辑是const rowClassName ({ row }) { const remainRatio (row.expiry_date - Date.now()) / (row.shelf_life * 86400000); if (remainRatio 0.33) return warning-row; return ; };这种小细节就是用了心的表现管理者打开页面第一眼就知道哪些货要赶紧处理不用挨个点开看。数据看板页面接入 ECharts 的方式很简单先npm install echarts然后封装一个图表组件接收 options 属性组件内部负责初始化实例和自适应 resize。我通常把趋势图、饼图、柱状图封装成三个组件页面里直接传数据就行。要记住给每个图表容器一个固定的height不然 echarts 初始化会拿到 0 高度页面上一片空白。看板页面我会放四张卡片今日销售额、今日销售件数、今日报损数、库存预警数加两张图近 7 日销售趋势折线图、品类销售占比环形图加一张损耗率柱状图。这些数据全部来自dashboard/overview接口后端一次把聚合结果返回前端不用发多个请求。3.5 前后端联调与本地部署联调阶段最舒服的做法是让 Vite 代理 API 请求。vite.config.js 里配置server: { proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } }这样前端代码里所有请求都写/api/xxxx开发时 Vite 会自动转发到 Flask 的 5000 端口绕开了浏览器跨域限制。这个模式的体验是你不用开两个地址来回切换一切请求看起来都是同源的。项目完成后部署到本地或服务器把 Vue 项目执行npm run build生成dist目录Flask 增加一个通配路由把静态文件服务起来app.route(/, defaults{path: }) app.route(/path:path) def catch_all(path): if path.startswith(api/): abort(404) return send_from_directory(dist, index.html)注意这段要写在其他路由最后保证/api开头的请求先被 API 路由接管。用户访问http://localhost:5000/就能看到整个系统不需要单独开一个 Nginx 服务静态文件。4. 常见问题与排查技巧实录4.1 跨域CORS与预检请求“接口明明在浏览器直接访问能通前端 axios 请求就报跨域错误”——这是前后端分离项目最经典的开局坑。根源在于浏览器默认只允许同源页面发起跨站请求解决思路就两条要么后端开 CORS要么开发环境用代理。我两个都推荐开发时用 Vite 代理部署时后端开 CORS 指定白名单。还有一个细节PUT、DELETE请求会触发浏览器的OPTIONS预检。如果你后端只配了 CORS 但没正确处理OPTIONS请求前端一样报错。flask-cors库默认会处理预检不需要额外写路由但你要确保没有在请求拦截器里把OPTIONS请求也带上自定义 Header否则预检就挂了。4.2 日期时间序列化与前端展示不一致datetime类型直接放进jsonify会报一个很经典的错Object of type datetime is not JSON serializable。解决方式分两种第一种是简单粗暴在查出来后用strftime手动转字符串。第二种是自定义 JSON 编码器from datetime import datetime from flask.json import JSONEncoder class CustomJSONEncoder(JSONEncoder): def default(self, obj): if isinstance(obj, datetime): return obj.strftime(%Y-%m-%d %H:%M:%S) return super().default(obj) app.json_encoder CustomJSONEncoder配置一次所有接口统一生效。前端拿到的是格式化好的字符串展示展示直接插入模板不需要再调处理函数。4.3 数据库连接池与并发读写Flask SQLAlchemy 在本地开发时往往一切正常一旦部署到线上访问量稍微上来一点可能会冒出MySQL server has gone away的报错。这是连接池里某些连接被 MySQL 服务端断开了但 SQLAlchemy 还在用它。解决方式是在数据库配置里加两个参数app.config[SQLALCHEMY_ENGINE_OPTIONS] { pool_size: 10, pool_recycle: 3600, pool_pre_ping: True }pool_pre_ping会在每次取连接时先探测连接是否存活失效就重新建立。这个参数虽然有一点点性能开销但换来的稳定性绝对值。4.4 Vue打包后的静态资源404前端打包后部署到 Flask 上刷新某个子路由页面会出现 404。原因很简单Vue Router 默认用的是 HTML5 History 模式URL 路径是/dashboard这种形式服务器默认找不到这个路径对应的文件。Flask 端加一个 fallback 路由把所有非 API 请求都指向index.html即可就是上文catch_all那段代码。还有一个连带坑如果你把 Vue 部署在子路径下比如http://server/supermarket/需要同时修改 Vite 的base配置、Vue Router 的createWebHistory(process.env.BASE_URL)以及 Flask 静态文件路径三者必须对齐少一个页面就是白屏。我建议如果没有特殊要求直接部署在根路径省掉这一堆麻烦。5. 如何从项目原型走向真实应用5.1 当前版本与生产环境之间的差距这套系统做完解决的是“从无到有”的问题。但真实的大型超市里生鲜数据不仅仅来自人工录入还来自收银 POS 机、电子秤、供应链 ERP 系统。你系统的销售数据如果要靠收银员一笔一笔录进去那不会有人愿意用的。所以想把它推向真实应用第一步一定是做数据对接POS 机导出的数据能批量导入或者直接通过接口接收。另外我强烈建议把“数据导入导出”做成完整闭环。生鲜超市每天需要给财务发日报表、给采购发进货建议如果这些都要在系统里手动复制粘贴 Excel那这套系统的价值就打了折扣。加一个 Excel 导出功能技术难度不高但实用价值非常高。5.2 数据智能化扩展方向目前系统的“数据处理”更多是描述性统计也就是告诉你发生了什么。再往下走有两件事可以做第一是智能订货建议。基于历史销售数据、周末效应、节假日效应、天气数据预测未来几天每种生鲜的销量结合当前库存和保质期给出“明天该订多少货”的建议。这一步能真正帮超市把缺货率和损耗率同时降下来算法上用时间序列或者梯度提升树就能做一个雏形。第二是自动调价提醒。当某种商品的库存积压且临近保质期时系统自动计算打折清库存的推荐价格。比如还剩两天保质期的猪肉是打七折卖完损失小还是等报废损失小用毛利模型一算就能给出建议。这两个方向做出来系统就从“记录工具”升级成了“决策工具”。这也正是生鲜数据处理系统真正该有的商业价值。这个项目做完我最大的变化是养成了一个习惯任何系统在动手写代码之前先把“要分析什么数据”想清楚。功能可以后面加但数据如果从一开始就没采到后面的分析全是空中楼阁。生鲜超市一天下来的销售额、损耗量、库存周转这些看着不起眼的数字攒起来就是优化利润的底气。如果你也在做类似的系统记住先把表结构和流水打好再考虑页面渲染这个顺序颠倒过来你会返工到崩溃。
返回列表