ARTICLE DETAIL

资讯详情

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

Python餐饮管理系统全解:从Flask架构到Linux部署实战

Python餐饮管理系统全解:从Flask架构到Linux部署实战 简介这套Python开发的餐饮管理系统源码包面向餐饮企业技术人员、Python学习者及软件工程课程设计人群覆盖订单管理、库存控制、菜单管理、客户关系、员工管理和财务报表等核心模块。系统基于MVC模式采用Flask/Django构建前端界面结合MySQL/SQLite数据库并集成支付宝/微信支付、数据分析报表与权限安全控制适合用于二次开发或毕业设计参考。资源包共5400个文件约23.53MB。主要包含1210个Python源码文件py、1100余个翻译文件po/mo、900余个编译缓存pyc以及HTML、JavaScript、CSS等前端资源另有少量配置文件与可执行程序。其中庞大的时区数据文件支撑系统多时区处理能力整体目录结构完整便于按模块查看与调试。目前已有113人学习下载。通过它可获得完整的餐饮管理系统源代码、前后端实现逻辑、数据库表结构设计以及支付与安全模块的封装方式尤其适合希望快速理解Python Web开发全流程并在此基础上扩展功能的读者。1. 为什么一套 Python 餐饮管理系统比单个 demo 更值得拆一个用 Python 开发的餐饮管理系统拆开来看并没有多高深订单、菜品、库存三张表加上 Flask 提供接口再加一个支付回调就撑起了八成业务。我拿到这套源码时先不急着跑起来而是先读目录结构和模型定义之后花一个晚上把核心链路点餐 - 扣库存 - 支付回调 - 报表在本地跑通。它的价值在于完整覆盖了一个小餐饮企业的日常流程前台点餐、后厨同步、库存预警、财务统计。对正在做课程设计、毕设或者小餐馆数字化改造的开发者来说它是比零散 demo 合适得多的参照物对想验证 Python 基础语法和框架用法的读者也能找到可以直接抄的代码。2. 从 MVC 到三张表餐饮系统的核心架构与数据库设计2.1 用 MVC 划清订单、菜品、库存的边界这套源码采用标准的 MVC 分层Model 定义业务实体View 处理页面和用户交互Controller在 Flask 里就是蓝图和视图函数接收请求并调度业务逻辑。我第一次打开目录时先看 models 和 views 的命名就能猜出系统支持哪些功能。目录结构大致如下restaurant/ ├── app/ │ ├── models/ # SQLAlchemy 模型 │ ├── views/ # Flask 蓝图 │ ├── templates/ # Jinja2 页面 │ └── static/ # JS / CSS ├── config.py ├── requirements.txt └── run.pymodels里每一张表对应一个类views里每个蓝图对应一类操作。这样拆的好处是改菜单上下架不会碰订单逻辑改库存扣减不会影响支付回调。很多课程设计把 SQL 直接写在路由函数里接口一多就开始互相埋坑而这份源码至少把边界画清楚了。各模块的职责可以梳理成一张表模块核心职责关键文件订单创建订单、更新支付状态、查询流水app/views/order.py菜品菜品增删改查、上下架、分类app/views/dish.py库存扣减、回补、低库存预警app/views/inventory.py报表销售统计、热门菜品排名app/views/report.py设计接口前先列清楚这张表后面写代码时就不会出现「报表接口去改菜品表」这种越界操作。模块之间的依赖方向也应该固定订单依赖菜品和库存库存不反向依赖订单报表只读取订单和订单明细。这样的单向依赖是 MVC 能跑得顺的基础。2.2 数据库模型订单头、订单明细与库存表的关系餐饮系统最核心的模型是订单。一个订单包含多条菜品明细所以要把订单头orders和订单明细order_items分开。菜品表dish保存当前价格和上下架状态库存表stock保存可售数量。为了处理并发下单stock 里加了 version 字段做乐观锁。下面是简化后的 SQLAlchemy 模型from datetime import datetime from sqlalchemy import Column, Integer, String, Float, ForeignKey, DateTime from app import db class Dish(db.Model): __tablename__ dish id Column(Integer, primary_keyTrue) name Column(String(50), nullableFalse) price Column(Float, nullableFalse) category Column(String(20), indexTrue) status Column(Integer, default1, indexTrue) # 1 上架 0 下架 class Stock(db.Model): __tablename__ stock id Column(Integer, primary_keyTrue) dish_id Column(Integer, ForeignKey(dish.id), uniqueTrue) quantity Column(Integer, default0) version Column(Integer, default0) # 乐观锁版本号 class Order(db.Model): __tablename__ orders id Column(Integer, primary_keyTrue) table_no Column(String(10), nullableFalse) status Column(Integer, default0, indexTrue) # 0 待支付 1 已支付 2 已取消 created_at Column(DateTime, defaultdatetime.utcnow) class OrderItem(db.Model): __tablename__ order_items id Column(Integer, primary_keyTrue) order_id Column(Integer, ForeignKey(orders.id), indexTrue) dish_id Column(Integer) dish_name Column(String(50)) # 菜品名称快照 price Column(Float) # 下单时价格快照 quantity Column(Integer, nullableFalse)有几个设计点值得关注。第一order_items里存了dish_name和price的快照而不是下单时去关联 dish 表这样菜品改名或调价后历史账单仍然能还原当时的交易内容。第二stock通过uniqueTrue保证一个菜品只有一条库存记录避免重复数据。第三status和category加上 index因为订单查询和菜品分类统计是大头不加索引的数据表在数据量到几万行时报表查询会明显变慢。用 SQL 原语句看更直接把上面的模型翻译成建表语句索引部分是这样的CREATE INDEX idx_order_status ON orders(status); CREATE INDEX idx_order_created_at ON orders(created_at); CREATE INDEX idx_order_item_order ON order_items(order_id); CREATE INDEX idx_dish_category ON dish(category);idx_order_created_at是给日报表按天分组用的这个索引经常被漏掉。如果报表页还有「按时间范围查流水」的功能建议连status created_at的联合索引也加上。2.3 SQLite 还是 MySQL按门店规模选才对最开始拿到项目我直接用config.py里的默认 SQLite 跑通。SQLite 的好处是零配置文件即数据库适合本地调试和演示。但真实门店是多台收银机和后厨大屏同时访问SQLite 在写并发和网络访问上都不合适建议上线前换成 MySQL。切换只需要改连接串模型完全不用动# config.py import os class Config: SQLALCHEMY_DATABASE_URI os.getenv( DATABASE_URL, sqlite:///restaurant.db ) SQLALCHEMY_TRACK_MODIFICATIONS False部署时设置环境变量DATABASE_URLmysqlpymysql://user:pass127.0.0.1/restaurant?charsetutf8mb4即可。选择可以按下面的表来定场景数据库原因本地调试、课程设计SQLite单文件、零配置单店 2~10 台终端MySQL 8.x行锁、事务、并发稳定多店连锁PostgreSQL复杂聚合分析和分区表更顺手不建议一上来就上 MQ 或微服务。餐饮系统的核心是「点餐不出错、库存不超卖、账能对上」这三件事用当前这套关系模型加事务就够。等到有了外卖平台对接、多店数据同步的硬需求再考虑拆服务也不迟。提示SQLite 对并发的支持远弱于 MySQL课程设计可以用但如果要演示多台收银机同时下单最好把测试环境切换到 MySQL。3. 订单流转与库存扣减Flask 接口层实战写法3.1 蓝图规划把订单、支付、报表拆成独立模块Flask 默认允许把所有路由放在 app.py但餐饮系统至少有收银、点餐、支付、库存、报表 5 组接口全放一起连找路由都得翻半天。源码把订单接口单独放在app/views/order.py用蓝图注册到 app 上这就是 MVC 中 View 和 Controller 的实现载体。注册方式和 URL 前缀如下# run.py from flask import Flask from app.views.order import order_bp from app.views.dish import dish_bp from app.views.pay import pay_bp def create_app(): app Flask(__name__) app.config.from_object(config.Config) app.register_blueprint(order_bp, url_prefix/api/order) app.register_blueprint(dish_bp, url_prefix/api/dish) app.register_blueprint(pay_bp, url_prefix/api/pay) return app app create_app()url_prefix让蓝图内的每个路由再拼接一层前缀这样 order 蓝图中写order_bp.route(/create)最终对外是/api/order/create语义清晰也好做 nginx 转发。订单相关的接口可以先列成一张表作为后续开发的契约方法路径关键参数返回POST/api/order/createtable_no, itemsorder_idPOST/api/order/payorder_id, pay_channel支付跳转地址GET/api/order/detailorder_id订单明细POST/api/order/cancelorder_id, reason取消结果接口表定好后前端同事可以并行开发页面你只需要保证返回的 JSON 结构不变。字段最好统一用code、msg、data三层包裹方便前端统一处理错误。3.2 创建订单时扣库存要放进事务并加行锁点餐链路里最容易出错的就是库存扣减。两个顾客同时点最后一份菜如果代码先SELECT quantity再UPDATE两个请求都会读到同一个库存数最后超卖。常见做法是把「校验库存并扣减」放在同一个数据库事务里并且对库存行加锁。下面是创建订单的视图函数from flask import jsonify, request from sqlalchemy.exc import OperationalError from app import db from app.models import Order, OrderItem, Stock order_bp.route(/create, methods[POST]) def create_order(): data request.get_json() table_no data.get(table_no) items data.get(items) # [{dish_id: 1, qty: 2}] if not items: return jsonify({code: 1, msg: items 不能为空}) try: order Order(table_notable_no, status0) db.session.add(order) db.session.flush() # 在事务内拿到 order.id for it in items: stock db.session.query(Stock).filter( Stock.dish_id it[dish_id] ).with_for_update().first() if stock is None or stock.quantity it[qty]: db.session.rollback() return jsonify({code: 2, msg: 菜品库存不足, dish_id: it[dish_id]}) stock.quantity - it[qty] db.session.add(OrderItem( order_idorder.id, dish_idit[dish_id], dish_nameit.get(dish_name, ), priceit.get(price, 0), quantityit[qty] )) db.session.commit() except OperationalError as e: db.session.rollback() return jsonify({code: 3, msg: 数据库繁忙请重试}) return jsonify({code: 0, data: {order_id: order.id}})with_for_update()在 MySQL InnoDB 下会锁住命中行直到事务提交或回滚。第二个请求只能等第一个请求提交后才能读到最新库存因此超卖被挡住。db.session.flush()的作用是让order.id在事务内生成后续写 order_items 时才能引用。注意 SQLite 不支持FOR UPDATE本地跑的时候该语句会被忽略所以测试并发建议直接上 MySQL。如果不想用行锁也可以改成条件更新UPDATE stock SET quantity quantity - :qty, version version 1 WHERE dish_id :dish_id AND version :old_version AND quantity :qty受影响行数为 0 就说明有人改过库存或库存不足回滚订单即可。这种乐观锁方式在读多写少的场景下性能更好但代码里需要处理重试逻辑。课程设计用第一种更直观面试聊到并发时能说出乐观锁和悲观锁的取舍就足够了。注意with_for_update()在 SQLite 下会被静默忽略本地调试时看不出行锁效果建议用 MySQL 容器做联调。3.3 前端 AJAX 下单与重复点击防护前端页面用 jQuery 的$.ajax向后端提交订单同时维护一个简单的「提交中」状态。这样能挡住大多数手抖双击但后端仍然要假设所有请求都可能是重复的。下面是简化后的下单脚本// static/js/order.js $(#submitOrder).on(click, function () { if (this.disabled) return; this.disabled true; setTimeout(() (this.disabled false), 1500); const items []; $(.cart-item).each(function () { items.push({ dish_id: $(this).data(dish-id), qty: parseInt($(this).data(qty), 10) }); }); $.ajax({ url: /api/order/create, method: POST, contentType: application/json, data: JSON.stringify({ table_no: A01, items }), success: function (res) { if (res.code 0) { location.href /pay.html?order_id res.data.order_id; } else { alert(res.msg); } }, error: function () { $(#msg).text(网络异常请重试); } }); });按钮禁用和定时器只是障眼法真正的幂等要放在后端。我一般会在订单请求里加一个client_token由前端生成唯一编号后端以(table_no, client_token)建唯一索引重复请求直接返回第一次的order_id。这样即使有人绕过页面用 curl 连续 POST也不会生成重复订单。前端 AJAX 断点调试时在浏览器开发者工具的 Network 面板里能看到完整的请求和响应 JSON配合后端日志排查参数问题最容易。4. 支付回调与销售报表对接支付宝沙箱和 Pandas 统计4.1 支付宝电脑网站支付的接入流程与沙箱配置支付集成是整个系统里最容易被「能通就行」带偏的部分。支付宝电脑网站支付的完整链路包括后端创建网关订单 - 返回跳转表单 - 用户支付 - 支付宝异步通知服务端 - 服务端验签并更新订单。本地开发不能真的付款所以要申请支付宝沙箱应用获取沙箱的 app_id、应用私钥和支付宝公钥。申请好后用python-alipay-sdk构造支付跳转链接from alipay import AliPay alipay AliPay( appidALIPAY_APP_ID, app_notify_urlhttp://your-domain/api/pay/callback, app_private_key_stringopen(private_key.pem).read(), alipay_public_key_stringopen(alipay_public_key.pem).read(), sign_typeRSA2, debugTrue # 沙箱环境必须开启 ) def gen_pay_url(order_id, amount): order_string alipay.api_alipay_trade_page_pay( out_trade_nostr(order_id), total_amountf{amount:.2f}, subject餐饮订单, return_urlhttp://your-domain/pay/return ) return https://openapi-sandbox.dl.alipaydev.com/gateway.do? order_stringtotal_amount必须是字符串而且最多保留两位小数直接用f{amount:.2f}格式化。debugTrue只用于沙箱上线必须改成False否则会请求沙箱网关导致用户付不了款。异步通知地址app_notify_url必须公网可访问本地调试可以用内网穿透工具但上线前要确认该地址未暴露日志。微信支付的小程序支付流程类似只是回调参数和验签方式不同设计思路上可以复用同一套订单状态机。支付状态主要有三种后端只需要针对成功状态做处理支付状态含义处理方式WAIT_BUYER_PAY交易创建等待付款不处理TRADE_SUCCESS支付成功更新订单状态为已支付TRADE_FINISHED交易完成且不可退款视同成功4.2 异步通知验签与状态更新的正确姿势支付宝的异步通知是表单 POST服务端必须立即返回success纯文本否则支付宝会按一定频率重复通知。很多项目在回调函数里写了一大堆业务逻辑一个异常就丢掉通知订单状态就永远卡在未支付。下面是稳健的回调处理pay_bp.route(/callback, methods[POST]) def pay_callback(): data request.form.to_dict() sign data.pop(sign, ) if not alipay.verify(data, sign): return failure trade_status data.get(trade_status) if trade_status TRADE_SUCCESS: order_id data.get(out_trade_no) order Order.query.filter_by(idorder_id).first() if order and order.status 0: order.status 1 db.session.commit() return success验签必须用支付宝公钥不是应用公钥也不是应用私钥。如果验签不通过直接返回failure不要做任何数据库更新。out_trade_no对应我们创建订单时传入的order_id这里直接用自增主键当订单号没问题但正式环境建议单独生成业务单号避免被人枚举。回调接口不能套用 Flask-WTF 的全局 CSRF 保护否则支付宝的通知会被拒绝需要在 CSRF 配置里把这个 URL 排除掉。注意支付宝回调接口不要套用全局 CSRF 保护否则验签通知会被 Flask-WTF 拦截。4.3 用 Pandas 生成日报表和热门菜品排名报表部分最适合体现 Python 数据分析的优势。系统里所有流水都落在 orders 和 order_items 表用一条带聚合的 SQL 就能把每日销售数据拉出来再用 Pandas 做透视和绘图。代码里推荐用pandas.read_sql直接读数据库避免自己拼 CSVimport pandas as pd from sqlalchemy import text from app import db def load_sales_data(start_date): sql text( SELECT date(o.created_at) AS day, d.category, oi.dish_name, SUM(oi.quantity) AS qty, SUM(oi.price * oi.quantity) AS amount FROM orders o JOIN order_items oi ON oi.order_id o.id JOIN dish d ON d.id oi.dish_id WHERE o.status 1 AND o.created_at :start GROUP BY day, d.category, oi.dish_name ) return pd.read_sql(sql, db.session.bind, params{start: start_date}) def hot_dishes(df, top_n10): return (df.groupby(dish_name)[qty] .sum() .sort_values(ascendingFalse) .head(top_n))params里传递的:start是绑定参数能防止注入也能让数据库缓存执行计划。read_sql返回的 DataFrame 可以直接做 groupby比在视图函数里写一堆循环干净得多。生成图表时最常遇到两个问题中文显示成方块以及日期横坐标挤成一条黑线。中文问题可以设置 Matplotlib 的中文字体plt.rcParams[font.sans-serif] [WenQuanYi Zen Hei] plt.rcParams[axes.unicode_minus] FalseLinux 服务器上一般装fonts-wqy-zenhei就有了。横坐标太密集时不要把每天都画出来按周聚合df[day] pd.to_datetime(df[day]) weekly (df.set_index(day) .resample(W-MON)[qty] .sum())按周重采样后横坐标就只剩每周一个点图面立刻干净。报表接口不要做太重建议在营业结束后用离线任务计算好结果再让页面读缓存而不是每次打开报表都全表聚合。5. 权限控制与 Linux 部署的小技巧5.1 用 Flask-Security 把收银和后厨分开餐饮系统至少有三种角色收银员能下单和退款后厨只能看单出菜店长能看报表和改菜单。前端隐藏按钮只是体验问题后端必须用装饰器做接口级校验。Flask-Security 封装了用户、角色、登录态和密码哈希最简单的用法如下from flask_security import roles_required order_bp.route(/refund, methods[POST]) roles_required(manager) def refund(): order_id request.get_json().get(order_id) # 退款业务... return jsonify({code: 0})注意roles_required只判断当前用户角色不判断资源归属所以退款接口里还要校验该订单是否属于当前收银员操作的订单。权限模型不要做太复杂先满足「店长能改价、收银员不能」这条最容易被审计的规则。5.2 Linux 上从 Python 环境到 Nginx 的部署部署到 Linux 时如果机器上没有现成的 Python 环境先用系统包管理器安装再创建虚拟环境避免污染系统 Pythonsudo apt update sudo apt install -y python3.11 python3.11-venv nginx python3.11 -m venv .venv source .venv/bin/activate pip install -r requirements.txt .venv/bin/gunicorn -w 3 -b 127.0.0.1:8000 run:app-w 3表示启动 3 个 worker但要注意 SQLite 不适合多 worker 并发写生产环境一定要换成 MySQL。run:app是 run.py 中的 Flask 实例。Nginx 负责接收 80 端口请求并转发给 gunicorn同时处理静态文件。部署后可以用systemctl守护 gunicorn 进程这里不再展开服务配置细节。5.3 一个值得养成的小习惯用 crontab 缓存热门菜品报表页最怕每次打开都全表聚合。我习惯用 crontab 在每天午市前预生成一份热菜榜单写入 Redis 或本地 JSON前端只读缓存0 11 * * * cd /var/www/restaurant .venv/bin/python scripts/refresh_hot_cache.py logs/hot.log 21脚本里读取已有的 Pandas 聚合结果把 top10 菜品序列化到缓存。这样营业高峰期报表接口不会跟着点餐接口抢数据库连接。这个习惯同样适用于库存预警每天开店前跑一次低库存清单发到钉钉或企业微信群里比让收银员自己记高效得多。本文还有配套的精品资源点击获取
返回列表