
从 Excel 记账到能用的管理系统中间其实只差一个 Flask 项目。今天要写的这套超市员工供应采购管理系统就是用 Python 的 Flask 框架搭建的覆盖了员工档案、供应商管理、采购申请、入库登记、库存查询这些核心流程适合课程设计、毕业设计也适合小型超市或便利店直接拿去二次开发。我会把从需求拆解到数据库设计再到功能实现的全过程讲清楚包含可以直接复制的代码片段和我在实际开发中踩过的坑。1. 需求建模超市供应采购的业务流与角色拆解先别急着写代码。任何管理系统第一步都是搞清楚「谁在用什么流程操作哪些数据」。超市供应采购不同于普通电商它涉及内部员工、外部供应商、商品库存三个核心维度而且每个维度之间都有数据联动关系。1.1 业务对象与关系超市供应采购系统里最少需要有这几类业务对象员工系统操作用户分为管理员、采购员、仓管员、店长等角色不同角色能做不同操作。供应商给超市供货的外部商家需要记录联系人、电话、供货品类。商品超市里在售或可采购的商品包含分类、规格、单位、采购价、售价。采购订单某个员工向某供应商发出的采购请求包含商品明细。入库单供应商送货后仓库员工验收并入库产生的记录。库存流水每次入库、出库、退货都产生一条流水用来追踪库存变化。这些对象之间不是孤立的。一个采购订单会关联多个商品一个供应商可以对应多张采购单一张入库单会同时扣减采购单的待入库数量并增加商品库存。理解了这个关系才能设计出不会出现数据矛盾的表结构。1.2 三步主流程整套系统的主流程可以压缩成三步采购申请采购员选择供应商和商品填写数量和预期单价生成采购申请单。审核确认店长或管理员审核采购单确认无误后状态改为「已审核」此时才允许供应商送货。入库登记仓管员根据送货单做出入库登记系统自动增加对应商品的可售库存并生成一条入库流水。你可能会问为什么采购单不直接入库因为在实际业务中审核和入库往往不是同一个人而且供应商送货数量可能和申请数量有出入。所以把「审核」和「入库」拆成两个独立环节数据才会真实可信。这也是很多毕设项目最容易做模糊的地方——采购单生成后直接改库存逻辑上虽然简单但跟真实业务相去甚远。1.3 角色与权限边界权限设计直接决定系统能不能真正落地。在这个超市管理场景里我建议至少划分四类角色系统管理员维护员工账号、供应商资料、商品基础数据拥有全部权限。采购员创建采购申请单、查看自己发起的采购单状态。仓管员执行入库操作、查看库存与流水、登记损耗。店长/经理审核采购单、查看采购与库存统计报表、导出数据。权限边界体现在两个层面一个是功能访问权限比如仓管员不能点击「审核采购单」按钮另一个是数据范围权限比如采购员只能看到自己创建的采购单店长能看到全部。后者经常被忽略但对于评分和真实使用来说都很重要。2. 技术选型与系统架构为什么是 Flask这套系统在很多替代方案里选了 Flask不是因为它最新最潮而是因为它在「项目结构清晰」「开发效率高」「学习曲线平缓」三者之间平衡得最好。2.1 Flask 对比 Django 和 FastAPI很多人在启动项目时会纠结框架选择这里直接说我的结论。如果目标是快速开发一个内部管理系统Flask 是够用的它没有强制目录结构自由度大适合中小型项目扩展生态很完整SQLAlchemy、Flask-Login、Flask-WTF、Flask-Admin 这些插件能覆盖系统开发九成以上的需要。Django 适合大型、功能高度标准化的项目自带 Admin 后台和 ORM但它的「全家桶」风格有时候反而是负担尤其当你只想做一个灵活的管理系统时重写默认配置的时间可能比写业务功能还多。FastAPI 性能确实强异步模型也很现代但它更适合 API 服务而不是返回大量页面模板的传统 Web 管理端。如果你的需求是给超市员工用浏览器点按钮而不是给外部平台提供高并发接口FastAPI 的优势在这里并不明显。所以在这类管理系统里Flask 是一个务实的选择。它可以让你把精力集中在业务逻辑上而不是跟框架本身较劲。2.2 三层架构与项目目录我在项目里采用的是经典的「视图层-业务层-数据层」三层结构而不是把所有路由堆在单个 app.py 里。单文件在 Demo 阶段没问题但一旦加上采购审批、库存流水、报表统计这些功能单文件很快会膨胀到几千行后面想改权限都找不到地方。项目目录结构我习惯这样组织supply_management/ ├── app.py # 应用入口创建 Flask 实例 ├── extensions.py # 存放 db、login_manager 等扩展实例 ├── models/ │ ├── __init__.py │ ├── user.py # 员工用户模型 │ ├── supplier.py # 供应商模型 │ ├── product.py # 商品模型 │ ├── purchase.py # 采购单与采购明细模型 │ └── stock.py # 入库单与库存流水模型 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录、退出、修改密码 │ ├── employee.py # 员工管理 │ ├── supplier.py # 供应商管理 │ ├── product.py # 商品管理 │ ├── purchase.py # 采购申请与审核 │ └── stock.py # 入库与库存查询 ├── templates/ │ ├── base.html # 基础布局模板 │ ├── auth/ │ ├── purchase/ │ └── stock/ ├── static/ │ ├── css/ │ └── js/ ├── config.py # 配置文件 └── requirements.txtextensions.py 单独放 db 和 login_manager 的实例是为了避免循环导入。因为 models 和 views 都需要用到这些扩展对象如果直接写在 app.py 里models 再导入 app.py 就会出现 ImportError。这是一个新手特别容易掉进去的坑提前把扩展独立出来后面会很舒服。2.3 技术栈清单Python 3.10Flask 3.xFlask-SQLAlchemy 3.xORMFlask-Login会话与登录状态Flask-WTF表单与 CSRF 防护SQLite开发环境 / MySQL生产环境Jinja2 模板Flask 内置Bootstrap 5前端布局CDN 引入即可这套组合不需要额外安装 Node.js 之类的前端环节重点全在后端业务逻辑上对一个人完成整个项目来说非常友好。3. 数据库设计每个表字段背后的业务含义数据库设计是最能体现系统成熟度的部分。很多人在这个环节草草建四五张表结果写功能时到处凑数据。我按照前面梳理的业务对象把表设计成了 8 张每一张都有明确的业务意义。3.1 用户与角色表员工表我命名为users而不是employee因为后续做 Flask-Login 登录时它需要的就是 users 表。表中关键字段如下字段类型说明idInteger 主键自增usernameString(50) 唯一登录账号password_hashString(255)只存哈希不存明文密码real_nameString(50)员工真实姓名roleString(20)admin / buyer / keeper / managerphoneString(20)联系电话is_activeBoolean是否可登录created_atDateTime创建时间password_hash 这一列开发时如果用明文密码项目在答辩或交付时会被一眼看穿不专业。正确做法是用 Werkzeug 自带的generate_password_hash和check_password_hash。3.2 供应商与商品表供应商表suppliers保持轻量核心用来做采购单的外键关联和通讯录维护id、name、contact_name、phone、address 这些常规字段之外我额外加了一个status字段标记启用/停用。停用的供应商在前台采购选择时直接过滤掉避免采购员误选已停止合作的供应商。商品表products是库存管理的核心字段类型说明idInteger 主键自增nameString(100)商品名称categoryString(50)分类如饮料、粮油、日化specString(100)规格如 500ml/瓶unitString(20)计量单位如箱、瓶、袋purchase_priceNumeric(10,2)最近采购单价sale_priceNumeric(10,2)超市售价stockInteger当前可用库存min_stockInteger库存预警下限supplier_idInteger 外键主要供货商其中min_stock是容易被忽略但非常实用的字段。库存低于这个值在系统首页和商品列表里高亮预警提醒采购员该补货了。这个设计虽然简单但会让系统看起来「真的有在用」。3.3 采购单、采购明细与库存流水表采购主表purchase_orders存一次采购的整体信息order_no生成的采购单号如 CG20250607001不直接用自增 id 当单号因为单号需要给供应商看需要有一定的规则感。supplier_id外键关联供应商applicant_id谁创建的采购单total_amount采购单总金额可以由明细自动汇总再写入status待审核 / 已审核 / 已入库 / 已取消apply_time、audit_time、audit_byremark备注采购明细表purchase_items存采购单里的商品行项目order_id 外键关联采购主表product_id 外键quantity、price、subtotal库存流水表stock_movements是保证数据可追溯的关键。每次入库、出库、盘亏都写一条记录product_id 外键change_typein / out / adjustquantity变动数量入库为正、出库为负ref_type 和 ref_id关联来源比如 purchase_order / stock_in / manualoperator_id操作人created_at操作时间remark备注有流水表之后库存数不再是一个被直接改写的字段而是通过流水汇总来核对。如果运营中发现商品库存对不上就能精确查到是什么时候、谁、通过哪张单子改的。这种设计思路比单纯 update products set stock stock 1 要严谨得多。3.4 建表初始化与种子数据模型在 Flask-SQLAlchemy 里直接用 class 定义以商品模型为例from extensions import db class Product(db.Model): __tablename__ products id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(100), nullableFalse) category db.Column(db.String(50), indexTrue) spec db.Column(db.String(100)) unit db.Column(db.String(20), default件) purchase_price db.Column(db.Numeric(10, 2), default0) sale_price db.Column(db.Numeric(10, 2), default0) stock db.Column(db.Integer, default0) min_stock db.Column(db.Integer, default10) supplier_id db.Column(db.Integer, db.ForeignKey(suppliers.id)) created_at db.Column(db.DateTime, defaultdatetime.utcnow) supplier db.relationship(Supplier, backrefproducts)在第一次运行时执行db.create_all()建表同时用脚本写入一个默认管理员账号和一个演示供应商。开发期这样做很方便但后面如果要换 MySQL 生产库建议改成 Alembic 迁移方案这个放到部署部分再讲。4. 核心业务功能实现从登录到入库全链路这一部分直接给出系统关键功能的实现思路与核心代码保证你能照着写出来。4.1 登录与会话管理登录模块用 Flask-Login 管理用户会话。使用方式非常简单from flask_login import login_user, logout_user, login_required, current_user auth_bp.route(/login, methods[GET, POST]) def login(): form LoginForm() if form.validate_on_submit(): user User.query.filter_by(usernameform.username.data).first() if user and user.check_password(form.password.data) and user.is_active: login_user(user) return redirect(request.args.get(next) or url_for(purchase.index)) flash(账号或密码错误, danger) return render_template(auth/login.html, formform)注意几个细节登录成功后跳转的目标要优先取request.args.get(next)保证用户在未登录时点击某个功能被拦下来登录后能回到原来的页面。login_user(user)之后不要手动写 sessionFlask-Login 内部会维护。退出时调用logout_user()顺带清除 session。角色权限我用一个自定义装饰器实现。虽然 Flask-Principal 这类扩展可以做更复杂的权限体系但对当前系统装饰器足够清晰from functools import wraps from flask import abort from flask_login import current_user def role_required(*roles): def decorator(f): wraps(f) def wrapper(*args, **kwargs): if not current_user.is_authenticated: abort(401) if current_user.role not in roles: abort(403) return f(*args, **kwargs) return wrapper return decorator使用stock_bp.route(/stock/in, methods[GET, POST]) login_required role_required(keeper, admin, manager) def stock_in(): # 入库登记逻辑 pass这里把 keeper、admin、manager 三种角色都允许入库是因为实际场景里小店可能没有专职仓管员店长也能代为入库。权限不是越严越好而是要匹配业务流程。4.2 采购申请单与审核流程采购申请是系统中交互最复杂的部分。用户需要选择供应商然后在一个页面里动态添加多个商品明细最后提交生成采购单。我实现的思路是前端页面上有一个商品明细表格用户可以选择商品、填写数量、单价通过 JavaScript 动态增删行。提交时把多条明细数据以items[0][product_id]这种列表形式 POST 到后端。后端用PurchaseOrderItemForm这套嵌套表单来接收数据。后端接收明细并创建采购单的简化代码如下access_bp.route(/purchase/apply, methods[GET, POST]) login_required def purchase_apply(): form PurchaseOrderForm() suppliers Supplier.query.filter_by(statusactive).all() if form.validate_on_submit(): order PurchaseOrder( order_nogenerate_order_no(), supplier_idform.supplier_id.data, applicant_idcurrent_user.id, remarkform.remark.data, statuspending ) db.session.add(order) db.session.flush() # 提前拿到 order.id total 0 for item in form.items: product Product.query.get(item.product_id.data) subtotal item.quantity.data * item.price.data order.items.append(PurchaseItem( product_idproduct.id, quantityitem.quantity.data, priceitem.price.data, subtotalsubtotal )) total subtotal order.total_amount total db.session.commit() flash(采购申请单已提交等待审核, success) return redirect(url_for(access.purchase_list)) return render_template(purchase/apply.html, formform, supplierssuppliers)这里有一个值得注意的细节db.session.flush()后order.id 会提前生成但事务并没有提交。这样可以在同一个事务里继续创建关联明细最后统一 commit。如果中途报错整个事务一起回滚不会出现一张主表有记录、明细表空着的情况。审核操作更简单本质上就是一个权限校验后的字段更新access_bp.route(/purchase/audit/int:order_id, methods[POST]) login_required role_required(manager, admin) def audit_order(order_id): order PurchaseOrder.query.get_or_404(order_id) if order.status ! pending: flash(该采购单已审核过, warning) return redirect(url_for(access.purchase_detail, order_idorder.id)) action request.form.get(action) if action approve: order.status approved order.audit_by current_user.id order.audit_time datetime.now() flash(采购单已通过审核, success) elif action reject: order.status rejected order.audit_by current_user.id order.audit_time datetime.now() flash(采购单已驳回, warning) db.session.commit() return redirect(url_for(access.purchase_detail, order_idorder.id))审核按钮在页面上只对 manager 和 admin 角色显示Jinja2 模板里可以用current_user.role判断。但要记住前端隐藏按钮只是用户体验优化真正的权限控制必须靠后端的role_required装饰器否则别人直接构造 POST 请求就能越权操作。4.3 入库登记与库存联动供应商送货到超市后仓管员在系统里找到「已审核」的采购单执行入库。入库的核心逻辑是每条明细都要更新对应商品库存同时写一条库存流水。stock_bp.route(/stock/in/int:order_id, methods[POST]) login_required role_required(keeper, admin, manager) def stock_in(order_id): order PurchaseOrder.query.get_or_404(order_id) if order.status ! approved: flash(只有已审核的采购单才能入库, warning) return redirect(url_for(access.purchase_detail, order_idorder.id)) for item in order.items: product item.product prev_stock product.stock product.stock prev_stock item.quantity movement StockMovement( product_idproduct.id, change_typein, quantityitem.quantity, ref_typepurchase_order, ref_idorder.id, operator_idcurrent_user.id, remarkf采购单 {order.order_no} 入库 ) db.session.add(movement) order.status done db.session.commit() flash(入库完成库存已更新, success) return redirect(url_for(stock.index))在修改库存之前可以做一个库存校验如果item.quantity 0或者商品已被禁用需要中断入库。另外并发情况下可能出现两个仓管员同时给同一商品入库这里 SQLite 下简单的做法是给商品行加锁或者使用数据库事务隔离。对于中小型超市管理系统理解的顺序是先保证逻辑正确、有流水可查再考虑极端并发。5. 页面交互与前端细节让系统真正可用的关键点很多人写管理系统后端接口通了一看页面就想摔键盘。一套内部管理系统如果页面难用员工会直接弃用宁可继续用 Excel。前端不用做得多炫但交互逻辑要顺。5.1 模板继承与导航布局我在基础模板base.html里用 Jinja2 的{% block %}机制划分区域左侧是导航菜单右侧是内容区。导航菜单根据角色动态渲染{% if current_user.role in [admin, manager] %} lia href{{ url_for(access.purchase_audit_list) }}采购审核/a/li {% endif %}这样做的好处是不同角色登录后看到的菜单不一样产品上就叫「菜单级权限」。5.2 表单处理与消息闪现所有表单都用 Flask-WTF 定义和校验。以登录表单为例from flask_wtf import FlaskForm from wtforms import StringField, PasswordField, SubmitField from wtforms.validators import DataRequired, Length class LoginForm(FlaskForm): username StringField(账号, validators[DataRequired(), Length(1, 50)]) password PasswordField(密码, validators[DataRequired()]) submit SubmitField(登录)CSRF 防护默认由 Flask-WTF 开启表单里只要渲染了一次form.hidden_tag()就会自动带上 CSRF token。这个细节对毕设或正式项目都很重要因为它能防止跨站请求伪造攻击。页面顶部显示操作结果用flash()消息与前端的 Bootstrap alert 结合{% with messages get_flashed_messages(with_categoriestrue) %} {% for category, message in messages %} div classalert alert-{{ category }} alert-dismissible fade show{{ message }}/div {% endfor %} {% endwith %}5.3 低库存预警与首页统计首页设计成一张「管理驾驶舱」式的面板顶部显示几个关键数字今日采购单数量、待审核数量、低库存商品数量、供应商总数。低库存商品列表单独列出一张表按缺口从大到小排序。库存吃紧的判断条件Product.query.filter(Product.stock Product.min_stock).order_by(Product.stock - Product.min_stock).all()如果想让效果更进一步可以在首页引入 Chart.js用 JavaScript 直接读后端传过来的 JSON 数据渲染柱状图或饼图。比如把「各分类采购金额占比」展示出来。实现方式很简单后端写一个返回 JSON 的接口前端用fetch获取数据再渲染。这一步不算难但给评审或老板演示时的观感会明显上升。6. 本地运行与生产部署别让系统只活在开发端口里很多项目跑在127.0.0.1:5000上没问题一换环境就崩。Deploy 部署这块我单独说清楚。6.1 本地跑起来的最快路径项目 requirements.txt 内容如下Flask3.0.0 Flask-SQLAlchemy3.1.2 Flask-Login0.6.3 Flask-WTF1.2.1 Werkzeug3.0.1安装依赖后初始化数据库pip install -r requirements.txt python init_db.py python app.py访问 http://127.0.0.1:5000默认管理员账号 admin密码 admin123登录后第一件事就是去员工管理里改密码。开发服务器默认只监听本机。如果想让超市里其他员工电脑也能访问启动时改成app.run(host0.0.0.0, port5000, debugTrue)这样同一局域网内的设备就能通过这台电脑的局域网 IP 访问。但debugTrue绝不能用于生产环境它会在报错时暴露完整堆栈而且允许任意代码执行非常危险。6.2 切换生产数据库SQLite 适合开发和演示但正式使用建议换成 MySQL 或 PostgreSQL。切换时只需要改配置里的数据库连接串# config.py SQLALCHEMY_DATABASE_URI mysqlpymysql://user:passwordlocalhost/supply_db?charsetutf8mb4 SQLALCHEMY_TRACK_MODIFICATIONS False同时把SECRET_KEY从默认值改成环境变量读取import os SECRET_KEY os.environ.get(SECRET_KEY, dev-key-change-me)生产环境下建议关掉 debug使用 waitressWindows或 gunicornLinux启动。以 waitress 为例pip install waitress waitress-serve --host0.0.0.0 --port8000 app:appgunicorn 启动方式类似。Nginx 反代放到前面静态资源由 Nginx 直接服务动态请求转发给 Flask 端口这是最标准的小型系统部署架构。6.3 数据备份与可维护性内部管理系统最怕数据丢。SQLite 版备份很简单直接复制数据库文件但要注意在写入高峰时复制可能出现脏数据。更稳妥的做法是定期停止写入的凌晨用 sqlite3 的.backup命令或者换 MySQL 后用 mysqldump 定时导出。sqlite3 supply.db .backup backups/supply_20250607.db7. 踩坑复盘我这个项目里最值得说的几个问题最后这部分是我实际开发中遇到过的坑有的折腾了好几个小时才定位到原因。写出来希望能帮你少走弯路。7.1 SQLAlchemy 循环导入与上下文问题我第一次写这个系统时把db定义在 app.py 里然后在 models 中导入 app.py结果一运行就报ImportError或circular import。后来把数据库实例单独抽到extensions.py里models、views、app.py 三方都只依赖 extensions问题立即消失。另一个常见问题是在非请求上下文环境里用db.session。比如有段时间我想在测试脚本里直接调模型方法没在app.app_context()里包裹就报 Working outside of application context。处理办法也很简单with app.app_context(): db.create_all() admin User.query.filter_by(usernameadmin).first()7.2 模板中直接调用外键关联带来的 N1 查询我在商品列表页面展示「供应商名称」时最初直接写product.supplier.name看起来很简单但每一行商品都要多执行一次供应商的查询。当商品几百条时页面明显变卡。解决办法是查询时使用joinedload或selectinload一次性把关联数据查出来from sqlalchemy.orm import joinedload products Product.query.options(joinedload(Product.supplier)).all()修改前后SQL 数量从 N1 条变成 1 条页面的响应速度是肉眼可见的提升。管理系统中类似的 N1 问题很容易出现建议从第一个列表页就开始使用joinedload养成习惯。7.3 角色判断写在模板里的隐患我一直强调权限控制必须以后端装饰器为准模板里的if current_user.role admin只作为用户体验优化。因为模板是可以被前端源码直接看到并绕过逻辑的如果有人用工具直接 POST 到审核接口那后端没有校验就完了。所以凡涉及写操作的接口必须挂上role_required。7.4 金额字段用浮点数的后果超市商品单价如果直接用 Python float 来存累计采购金额很可能出现 0.30000000000000004 这类诡异小数。解决办法是模型里统一用db.Numeric(10, 2)前端显示时保留两位小数计算时用 Decimal。一句话总结钱的问题不要用浮点数用定点数。7.5 从「能跑」到「好用」还能怎么扩展这套系统做完核心流程之后如果你想继续打磨我建议从以下方向入手增加供应商结算模块把采购入库数据变成每月账单自动统计应付金额对接供应商对账。增加商品条码扫描PDA 或手机扫码入库、盘点减少手工录入错误。增加预测补货建议根据近三个月销售数据和当前库存、在途库存自动生成建议采购量和采购时间。增加操作日志记录谁在什么时候改了什么关键数据出现纠纷时有据可查。这些扩展并不会推翻现有的表结构因为当初设计时库存流水和采购状态已经为这些场景留好了接口。这也是为什么我说数据库设计阶段多想一步后面扩展能省非常多的力气。整套系统做下来我的最大感受是管理系统的难点从来不在某个技术点而在于梳理清楚业务流程并把流程忠实地转化成数据流转。Flask 只是把这一切串起来的工具真正让系统值钱的是你对「超市怎么采购、怎么入库、怎么对账」这件事的理解。如果你现在正准备动手做类似项目建议先把业务模型画清楚再开始写代码顺序反过来的时候你多半会推倒重来一遍。