ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Python点餐系统实训:数据库设计与Flask接口实现

Python点餐系统实训:数据库设计与Flask接口实现 简介面向Python初学者及需要完成毕业设计、期末大作业的学生一套可直接运行的高分点餐系统实训项目源码正适合作为课程设计与毕业设计的参考模板。系统基于Python语言开发并配套数据库文件覆盖菜单管理、桌台点餐、订单结算等常见业务场景前端采用Bootstrap、AdminLTE等框架整体界面简洁美观操作流程清晰。压缩包共包含2031个文件、约70.17MB其中以1262个JavaScript文件、377个CSS样式表和131个HTML页面构成主要前端资源48个Python文件组织后端业务逻辑另有95个JSON配置、72个Markdown说明文档及13个文本文档便于快速理解项目结构与部署步骤。代码注释详细可简单部署后直接运行且已经过严格调试功能稳定据作者介绍为98分高分项目得到导师认可实际应用价值高。目前已有324人学习适合用作实训参考、期末大作业或毕业设计的基础源码。1. 实训选点餐系统而不是图书管理系统赢在哪实训周报选题时多数人第一反应是图书管理系统或学生管理系统而我会直接选点餐系统。理由很直接点餐系统把 Python 基本语法、MySQL 建表、增删改查、事务、HTTP 接口五个能力串在一条业务链里做完一遍数据库课程设计要考的东西基本都过了。它不是一个纯 CRUD 演示菜单、库存、订单、金额之间有真实的业务约束代码里自然会出现外键、事务回滚、Decimal 精度这类问题。网上搜免费 Python 源码大全点餐系统不少但大多停在控制台输入输出能把数据库表和订单流程完整跑通的不多。这篇按一个能当场演示、能拿高分的方案往下讲数据库表怎么设计、Python 端点单流程怎么写、运行时报错怎么处理最后给一份答辩前的自查技巧。适合要交 Python 实训报告或数据库课程设计、需要源码和数据库配套的同学也适合想用一个小项目把 MySQL 和 Python 真正串通的开发者。2. 数据库设计点餐系统的菜品表、订单主表和明细表一个点餐系统的源码会被拆开看两部分Python 代码和数据库脚本。很多同学花大量时间写界面数据库只给几张空表这是明显的丢分点。数据库脚本直接反映你对业务的理解比如谁和谁是一对多、库存扣减放在哪一层、历史订单价格怎么保留。下面这套表结构按堂食点餐最常见的需求设计如果要改造成外卖点餐系统只需在 orders 表上加 customer_name、phone、address 三个字段其他表基本不动。2.1 菜品表 menu_item 字段设计与建表 SQL先看第一张表 menu_item它保存菜单上所有可售商品。设计这张表时有两个高频错误一个是把价格字段设成 FLOAT后面算金额会出现 0.30000000000000004 这类小数误差另一个是把下架菜品直接从表里删掉导致历史订单明细失去关联数据。正确做法是用 DECIMAL 存金额用 is_available 控制上下架菜品记录永远保留。CREATE DATABASE IF NOT EXISTS ordering_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE ordering_system; CREATE TABLE menu_item ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL COMMENT 菜品名称, category VARCHAR(20) NOT NULL COMMENT 分类热菜/凉菜/汤类/主食, price DECIMAL(10, 2) NOT NULL COMMENT 单价保留两位小数, stock INT NOT NULL DEFAULT 100 COMMENT 可售库存, is_available TINYINT(1) NOT NULL DEFAULT 1 COMMENT 1上架 0下架 ) ENGINEInnoDB;建表语句里值得说明的几个点ENGINEInnoDB 必须写后面 Python 端要用到事务和行锁MyISAM 不支持这些特性COMMENT 不只是注释把 schema.sql 交给评阅老师看时字段含义一目了然DECIMAL(10,2) 表示整数部分最多 8 位、小数部分 2 位餐厅菜价完全够用而且不会产生浮点误差。2.2 订单主表和明细表拆分的原因及外键设置点餐系统里最容易让初学者困惑的是订单表要不要拆。我的做法是拆成 orders 和 order_item 两张。orders 存一次点单的“头”桌号、订单状态、总金额、下单时间order_item 存这次点单的“行”每道菜、数量、当时的价格。如果强行把明细塞进 orders同一桌点五个菜就要写五行重复的桌号和状态后续改状态要改五条记录查询也麻烦。CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, table_no VARCHAR(10) NOT NULL COMMENT 桌号, status ENUM(pending, paid, cancelled) NOT NULL DEFAULT pending COMMENT 订单状态, total_amount DECIMAL(10, 2) NOT NULL DEFAULT 0.00 COMMENT 订单总金额, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间 ) ENGINEInnoDB; CREATE TABLE order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL COMMENT 所属订单, menu_id INT NOT NULL COMMENT 菜品ID, quantity INT NOT NULL DEFAULT 1 COMMENT 购买数量, price DECIMAL(10, 2) NOT NULL COMMENT 下单时价格快照, CONSTRAINT fk_order_id FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE, CONSTRAINT fk_menu_id FOREIGN KEY (menu_id) REFERENCES menu_item(id) ) ENGINEInnoDB;order_item 里的 price 是价格快照不是当前菜单价格。菜品后来改价历史订单依然按旧价统计这个细节在做数据分析时很关键。外键约束保证了不会出现明细指向不存在的订单这种脏数据ON DELETE CASCADE 表示删除订单时自动删除它的所有明细。三张表的分工可以用下面这张表概括。表名关键字段用途menu_itemprice, stock, is_available菜单展示、库存判断、上下架orderstable_no, status, total_amount一次点单的主记录order_itemorder_id, menu_id, quantity, price订单中的每道菜明细建表顺序也值得注意先建 menu_item再建 orders最后建 order_item。order_item 的外键依赖前两张表顺序反了 MySQL 会直接报错。2.3 种子数据与数据库增删改查验证表建好之后先插入几行测试数据再手动跑几条增删改查确认表结构和外键没问题再开始写 Python 代码。这一步能省下大量调试时间数据库层面的问题如果拖到界面阶段才暴露排错成本会翻倍。INSERT INTO menu_item (name, category, price, stock) VALUES (宫保鸡丁, 热菜, 28.00, 50), (番茄炒蛋, 热菜, 18.00, 50), (紫菜蛋花汤, 汤类, 12.00, 30), (米饭, 主食, 2.00, 200); SELECT id, name, price, stock FROM menu_item WHERE is_available 1 ORDER BY category; UPDATE menu_item SET stock stock - 2 WHERE id 1; -- 模拟卖出两份宫保鸡丁这些命令就是常说的数据库增删改查。SELECT 验证上架菜品能查出来UPDATE 模拟库存扣减DELETE 放在一个专门重置数据的脚本里。等 Python 端写完后订单数据的落库正确性也要回到 SQL 里验证比如关联查询看某个订单点了哪些菜。3. Python 点餐流程实现连接数据库、菜单列表、生成订单3.1 技术选型Flask PyMySQL 还是纯 Python 脚本网上大量点餐系统源码是纯控制台版input() 输入桌号和菜品编号print() 打印结果。这种写法实现简单但和前端交互基本绝缘答辩演示效果一般。我一般用 Flask PyMySQLFlask 负责提供 HTTP 接口PyMySQL 负责和 MySQL 通信。前端可以是简陋的 HTML 页面甚至直接用 curl 访问接口都比控制台版更能体现“系统”的形态。这里有个常见对比在线点餐系统有人用 Spring Boot Vue那套技术栈适合做分布式外卖平台作为 Python 实训项目Flask 加原生 SQL 已经把数据库课程设计的核心覆盖干净写起来也快。选型先想清楚实训要考核哪几项再决定没必要为了一次展示把 Vue 和 Spring Boot 全拉进来。3.2 菜单列表接口和订单生成接口安装依赖这一步很简单两个包就够了。pip install flask pymysql先看菜单查询接口。它做的事是把 menu_item 表的上架菜品查出来转成 JSON 返回给前端。# app.py from flask import Flask, jsonify, request import pymysql app Flask(__name__) DB_CONFIG { host: 127.0.0.1, port: 3306, user: root, password: 123456, database: ordering_system, charset: utf8mb4, } def get_conn(): return pymysql.connect(cursorclasspymysql.cursors.DictCursor, **DB_CONFIG) app.get(/menu) def menu_list(): conn get_conn() try: with conn.cursor() as cur: cur.execute( SELECT id, name, category, price, stock FROM menu_item WHERE is_available 1 ORDER BY category, id ) rows cur.fetchall() return jsonify({code: 0, data: rows}) finally: conn.close()这里用 conn.cursor() 的上下文管理器管理游标但连接 conn 要自己在 finally 里关闭因为 pymysql 的连接不会自动回收。DB_CONFIG 里的 charset 必须写 utf8mb4少了这个参数中文菜名很可能乱码。DictCursor 让每行结果以字典形式返回前端拿到 JSON 可以直接渲染省去解析元组的麻烦。接着是下订单接口这段代码和数据库交互最密切。app.post(/order) def create_order(): payload request.get_json() table_no payload.get(table_no, A01) lines payload.get(items, []) if not lines: return jsonify({code: 1, msg: 订单明细不能为空}), 400 conn get_conn() try: conn.begin() # 开启显式事务下面所有语句要么一起成功要么一起回滚 with conn.cursor() as cur: total 0 validated [] for line in lines: # FOR UPDATE 锁住这一行防止两个请求同时抢最后一份库存 cur.execute( SELECT price, stock FROM menu_item WHERE id %s AND is_available 1 FOR UPDATE, (line[menu_id],), ) menu cur.fetchone() if menu is None or menu[stock] line[quantity]: raise RuntimeError(f菜品 {line[menu_id]} 库存不足) total menu[price] * line[quantity] validated.append( (line[menu_id], menu[price], line[quantity]) ) cur.execute( INSERT INTO orders (table_no, total_amount) VALUES (%s, %s), (table_no, total), ) order_id cur.lastrowid for menu_id, price, quantity in validated: cur.execute( INSERT INTO order_item (order_id, menu_id, quantity, price) VALUES (%s, %s, %s, %s), (order_id, menu_id, quantity, price), ) cur.execute( UPDATE menu_item SET stock stock - %s WHERE id %s, (quantity, menu_id), ) conn.commit() return jsonify({code: 0, order_id: order_id, total: str(total)}) except Exception as exc: conn.rollback() return jsonify({code: 1, msg: str(exc)}), 400 finally: conn.close()下订单接口的逻辑分三步先逐行检查菜品库存并锁定对应行再算出总价并插入 orders 主记录最后把明细逐条写入 order_item 并同步扣减库存。raise RuntimeError 触发异常后except 块里的 rollback() 会撤销之前所有写操作不会出现“库存扣了但订单没建成”的中间状态。FOR UPDATE 是 InnoDB 的行锁语法并发场景下两个请求抢同一道菜的最后一份库存时第二个请求会等第一个提交或回滚后再执行避免超卖。total_amount 在响应里用 str(total) 转成字符串是因为 JSON 序列化时对 Decimal 类型不友好直接返回会报 TypeError。3.3 金额计算用 Decimal 不用 float 的原因与写法上面代码里 price 从 MySQL 查出来就是 Decimal 对象乘以整数 quantity 后结果依然是 Decimal全程没有转成 float所以不会出现小数误差。很多初学者图省事写成 float(price)结果 28.00 乘以 0.9 变成 25.200000000000003。把这段对比写进代码注释答辩时可以直接当亮点讲。from decimal import Decimal price Decimal(28.00) quantity 2 print(price * quantity) # 输出 Decimal(56.00) # 错误示范一旦转成 float精度就失控 print(float(price) * 0.9) # 25.200000000000003如果菜品参与打折折扣率也建议用 Decimal 表示并把结果量化到两位再写入数据库。Decimal 的量化写法是total.quantize(Decimal(0.01))比 round() 更符合财务计算规范。价格快照同样用 Decimal 存进 order_item 表历史订单金额才能保持准确。4. 本地运行配置、连接参数与常见报错排错4.1 运行环境和依赖安装代码写完后完整启动流程包括建虚拟环境、装依赖、导入数据库表、启动服务四步。虚拟环境不是图好看是避免本机其他 Python 项目把 Flask 或 PyMySQL 的版本互相弄坏。python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install flask pymysql mysql -u root -p schema.sql python app.pymysql 命令执行 schema.sql 时脚本里已经带了 CREATE DATABASE 和 USE执行完可以直接登录 MySQL 看表是否创建成功。python app.py 默认跑在 5000 端口看到 Flask 启动日志并且没有报错再访问接口。如果打开页面超时多半是 app.py 里少了 app.run()或者 5000 端口已被占用。4.2 数据库连接参数说明及环境变量配置连接 MySQL 的参数集中在 DB_CONFIG 字典里实训项目可以直接写死但更正规的写法是从环境变量读取换一台机器跑时不用改源码。比如学校 MySQL 的密码改了只需要改环境变量。下表里的每个参数都可能成为排错点。参数示例值说明host127.0.0.1MySQL 所在地址本机建议用 127.0.0.1port3306MySQL 默认端口改过 my.cnf 才需要变user / passwordroot / 123456建议单独建一个 ordering_user别用 root 跑演示databaseordering_system必须和 schema.sql 里的库名一致charsetutf8mb4连接级字符集防止中文乱码推荐把配置挪到 config.py 里用 os.getenv 读取默认值。import os DB_CONFIG { host: os.getenv(DB_HOST, 127.0.0.1), port: int(os.getenv(DB_PORT, 3306)), user: os.getenv(DB_USER, root), password: os.getenv(DB_PASS, 123456), database: os.getenv(DB_NAME, ordering_system), charset: utf8mb4, }os.getenv 第二个参数表示环境变量没设置时的默认值。这样在答辩现场即使评委要求换数据库你只需要在启动前执行 export DB_PASS新密码源码不用改一行。int() 包裹 DB_PORT 是因为 os.getenv 返回的永远是字符串直接拼给 pymysql 会报端口类型错误。4.3 三个高频报错认证插件、字符集、端口占用新手跑这个项目时报错集中在启动阶段和访问阶段。下面这张对照表按现象、原因、处理方式排列照着比对自己代码更快定位。报错现象原因快速处理Authentication plugin caching_sha2_password cannot be loadedMySQL 8 默认认证插件与旧版 PyMySQL 不兼容升级 PyMySQL或执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456;中文存入数据库变成???建库、连接任意一层没设 utf8mb4建库语句补上 CHARACTER SET utf8mb4连接参数加 charsetutf8mb4Cant connect to MySQL server (2003)MySQL 服务没启动或 3306 端口被占用Windows 服务管理器启动 MySQL或执行netstat -ano | grep 3306查占用进程接口写完先用 curl 做冒烟验证比打开浏览器更直接。下面两条命令先拉菜单再提交一单两个菜观察返回里有没有 order_id 和 total。curl http://127.0.0.1:5000/menu curl -X POST http://127.0.0.1:5000/order \ -H Content-Type: application/json \ -d {table_no:A01,items:[{menu_id:1,quantity:2},{menu_id:2,quantity:1}]}POST 请求的 -d 参数是 JSON 字符串菜单里宫保鸡丁 id 为 1番茄炒蛋 id 为 2所以这单点了两份宫保鸡丁和一份番茄炒蛋。返回 code 为 0 并带 order_id说明订单写库成功。返回 400 时错误信息要么是库存不足要么是 JSON 格式问题把 payload 打印出来逐项比对就能定位。5. 答辩演示前的高分检查清单和方法5.1 一分钟重置演示数据正式演示前最怕上次运行留下脏数据。做法是把 schema.sql 重新导入一次再执行一条 UPDATE 恢复初始库存数据库回到刚建完表的状态演示逻辑可重复。答辩前把这几条命令固定在一个脚本里现场跑一条命令完成重置。mysql -u root -p schema.sql mysql -u root -p -e UPDATE menu_item SET stock 100 WHERE id 4; python app.py5.2 用并发请求展示事务为什么不超卖把宫保鸡丁的库存改成 3再用 xargs 并发发起 10 个请求每个请求都点 3 份。没有事务和锁时最终库存可能变成负数。加上 FOR UPDATE 和事务后只有一个请求能成功其他请求返回库存不足。这个现象先录成短视频放进报告答辩时播放一遍比对着 PPT 讲原理有力得多。mysql -u root -p -e UPDATE menu_item SET stock 3 WHERE id 1; seq 10 | xargs -P 10 -I{} \ curl -s -X POST http://127.0.0.1:5000/order \ -H Content-Type: application/json \ -d {table_no:A01,items:[{menu_id:1,quantity:3}]}5.3 用 matplotlib 输出最近 7 天营业额统计图点餐系统的价值不止于点单还能从 orders 表里统计营业额。下面这段统计脚本把最近一周每天的营业额拉出来生成柱状图保存到本地。它调用数据库的方式和业务接口一致等于在同一个项目里多做了一层数据分析展示。# stats.py import pymysql import matplotlib.pyplot as plt conn pymysql.connect( host127.0.0.1, userroot, password123456, databaseordering_system, charsetutf8mb4, cursorclasspymysql.cursors.DictCursor, ) with conn.cursor() as cur: cur.execute( SELECT DATE(created_at) AS d, SUM(total_amount) AS revenue FROM orders WHERE status paid AND created_at DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(created_at) ORDER BY d ) rows cur.fetchall() conn.close() dates [r[d].strftime(%m-%d) for r in rows] revenues [float(r[revenue]) for r in rows] plt.figure(figsize(8, 4)) plt.bar(dates, revenues) plt.title(最近 7 天营业额) plt.tight_layout() plt.savefig(sales_report.png)执行pip install matplotlib python stats.py后同目录生成 sales_report.png。答辩前先在库里 UPDATE 几条 orders 的状态为 paid 并改 created_at保证图表有数据。最后核对 orders.total_amount 与 order_item 明细求和一致再打开 sales_report.png 那一页开始讲解这套点餐系统从数据库、事务到数据分析的完整度就和纯增删改查的代码拉开差距了。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进