news 2026/9/16 21:00:10

Python实训项目:Flask+MySQL点餐系统设计与事务实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python实训项目:Flask+MySQL点餐系统设计与事务实现

简介:一份面向Python实训、课程设计和毕业设计的点餐系统源码项目,完整包含前端页面、后端逻辑与数据库文件。代码注释清晰,新手也能快速读懂,作者标注为98分高分项目,导师评价较高,适合作为期末大作业或课程设计参考。资源包总计2031个文件,压缩后约70MB,包括1262个JavaScript脚本、377个CSS样式、131个HTML页面、48个Python源码文件,以及JSON配置文件、Markdown说明文档等;前端基于Bootstrap和AdminLTE框架,后台管理界面美观实用。当前已有324人学习下载;项目经过严格调试,简单部署即可运行,功能模块覆盖点餐、订单、管理等常见流程,并附带数据库脚本和清晰目录结构,既方便二次开发,也可直接用于项目演示或答辩展示。无论是巩固Python Web开发知识,还是快速搭建一个完整的实训项目,这套源码都能提供有效支撑。

1. 从课程设计到答辩:为什么点餐系统最适合当 Python 实训项目

每年数据库课程设计交上来的点餐系统少说也有上百份,但大部分成品都卡在同一个位置:菜单页能展示、按钮能加购,一到“下单”就只是把购物车数据拼成一条 INSERT 塞进 orders 表。老师随便问一句“库存不足时这次下单会不会把订单写进去”就答不上来。这套题看着简单,实际是围绕数据库事务、状态流转、分页查询的小型业务系统,正好是 Python 实训周里最值得精做的对象。这篇内容面向正在选课题的学生,也适合需要快速理解“点餐系统源码+数据库”该如何组装的从业者,从选型开始走到答辩前一刻,给你一条能直接复现的路线。

2. 技术选型与三层结构:Flask + MySQL 的组合依据

2.1 为什么不用 Django,也不走 SpringBoot 前后端分离

点餐系统的业务规模,用重型框架是杀鸡用牛刀。Django 自带 ORM、Admin 后台和用户认证,确实能省不少事,但实训考核看的是你对 SQL 和业务逻辑的控制力。Admin 把增删改查全代劳之后,老师翻开源码看到的全是框架的默认能力,不是你针对点餐场景做的设计。Flask 只保留路由和请求处理,把数据库访问、业务校验、事务控制全部暴露在业务代码里,更贴合“源码+数据库”这个交付物。

另一条常见路线是基于 SpringBoot + Vue 的在线点餐系统,它适合前后端分离课程,但要同时维护两套工程和一份接口文档,实训周期通常不够。Python 技术栈的实训课,Flask + PyMySQL + MySQL 是最稳妥的组合,既能单独说清后端逻辑,又不引入太多框架魔法。

方案学习成本考核可见度适合场景
Flask + PyMySQLSQL 与业务全可见Python 实训、数据库课程设计
Django + ORMORM 屏蔽 SQLWeb 开发课
SpringBoot + Vue前后端分离切分清晰综合项目实训

有人会用 Flask-SQLAlchemy 替代 PyMySQL,这在实训场景下我不太推荐。ORM 在开发时写得更快,但增删改查的 SQL 就藏到模型类后面去了。答辩被问“这条 UPDATE 有没有 WHERE 条件”时,你翻开源码给他看的是一个 save() 方法,解释成本很高。自己手写 SQL,能让所有查询路径肉眼可见,分数反而好拿。

2.2 项目目录怎么分,能让老师一眼看懂分层

写点餐系统不要把所有代码堆在一个 app.py 里。我一般会拆成 routes、services、dao、database 四个层级:路由层负责接收参数和返回 JSON,service 层写下单、付款这类业务逻辑,dao 层只做 SQL 语句执行,database 模块负责连接管理。这样分层的直接好处是答辩时能对着目录讲出“路由层不写 SQL、DAO 层不写业务”这句话,这本身就是加分项。

ordering-system/ ├── app.py # 应用入口与路由注册 ├── config.py # 数据库连接参数、分页默认值 ├── database.py # PyMySQL 连接封装 ├── services/ │ ├── order_service.py # 下单、订单状态流转业务 │ └── dish_service.py # 菜单查询与分页 ├── dao/ │ ├── dish_dao.py # dish 表 SQL │ └── order_dao.py # orders/order_item 表 SQL └── resources/ ├── templates/ # 页面模板 └── static/ # 前端资源
# config.py HOST = "127.0.0.1" PORT = 3306 USER = "root" PASSWORD = "your_password" DATABASE = "ordering" DEFAULT_PAGE_SIZE = 10 MAX_PAGE_SIZE = 50

连接参数集中在 config.py 里,部署到其他机器只需要改这一处。PASSWORD 不要在代码里写死成长字符串,实训演示可以接受明文,但要在答辩时主动提一句“正式项目会用环境变量代替”。

2.3 最小可运行的 Flask 骨架

先把连接参数和入口跑起来,再去填业务。下面这段代码里不涉及任何业务功能,只做一件事:验证 Flask 能启动、MySQL 连接能建立。

# app.py from flask import Flask, jsonify import database app = Flask(__name__) app.config.from_object("config") @app.route("/api/health") def health(): conn = database.get_connection() with conn.cursor() as cursor: cursor.execute("SELECT 1") result = cursor.fetchone() return jsonify({"status": "ok", "db": result["1"]}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000, debug=True)

database.py 不直接把连接对象暴露给路由层,请求结束后的归还和异常处理集中在同一个地方。host 写 127.0.0.1 是因为本机演示最稳定;如果连接时报“Access denied”,先检查 USER 的密码和允许登录的 host,而不是去改 MySQL 的 bind-address。debug=True 开发时开着,答辩演示前建议关掉,否则报错页会把 SQL 语句连同参数一起展示出来。

# database.py import pymysql from pymysql.cursors import DictCursor import config def get_connection(): return pymysql.connect( host=config.HOST, port=config.PORT, user=config.USER, password=config.PASSWORD, database=config.DATABASE, charset="utf8mb4", cursorclass=DictCursor, autocommit=False, )

charset 必须和建库字符集保持一致,否则写入 emoji 或生僻字会直接报错。cursorclass 用 DictCursor,查询结果返回字典列表,在 service 层取值用 result["status"],比默认元组的 result[3] 可读性好得多。autocommit 关掉是给第 4 章的下单事务做准备,事务控制权必须留在业务代码手里。

3. 点餐系统数据库设计:从 ER 图到建表 SQL

3.1 六张核心表与字段设计

点餐系统的核心实体是用户、菜品、分类、购物车、订单。只做增删改查的话两张表就够,但答辩要想站得住,必须把业务状态放进表设计里。我常用的表结构如下。

表名核心字段关键约束作用
userid, username, password_hash, roleusername 唯一顾客和店长共用,role 区分
categoryid, name, sortsort 默认 0菜单分组
dishid, category_id, name, price, stock, statusprice DECIMAL(10,2)价格不能用 float
cartid, user_id, dish_id, quantity(user_id, dish_id) 唯一同一道菜只保留一行
ordersid, order_no, user_id, total_amount, statusorder_no 唯一订单主表
order_itemid, order_id, dish_id, name, price, quantity冗余菜品快照菜品改价不影响历史订单

字段设计的三个要点。第一,价格用 DECIMAL(10,2) 而不用 FLOAT,因为金额带精度,float 算总价会出现 0.1+0.2=0.30000000000000004 的经典问题。第二,order_item 里冗余一份 name 和 price,菜品价格会随时间调整,订单明细必须记录下单那一刻的快照,否则以后一改菜品价格,历史账单全部错乱。第三,user 表不存明文密码,只存 password_hash,为第 5 章的密码处理留好位置。

3.2 建表 SQL 与数据库修改结构

MySQL 建库建表 SQL 如下。存储引擎全部用 InnoDB,订单表的 user_id 和明细表的 order_id 建索引,否则联表查询在数据量变大后会全表扫描。

CREATE DATABASE IF NOT EXISTS ordering DEFAULT CHARSET utf8mb4; USE ordering; CREATE TABLE user ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL, password_hash VARCHAR(255) NOT NULL, role TINYINT NOT NULL DEFAULT 0 COMMENT '0=顾客 1=管理员', phone VARCHAR(20) DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB COMMENT='用户表'; CREATE TABLE category ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, sort INT NOT NULL DEFAULT 0 ) ENGINE=InnoDB COMMENT='菜品分类'; CREATE TABLE dish ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, category_id INT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT UNSIGNED NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1=上架 0=下架', description VARCHAR(255) DEFAULT NULL, KEY idx_category (category_id), KEY idx_status (status) ) ENGINE=InnoDB COMMENT='菜品表'; CREATE TABLE cart ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, dish_id INT UNSIGNED NOT NULL, quantity INT UNSIGNED NOT NULL DEFAULT 1, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_dish (user_id, dish_id), KEY idx_user (user_id) ) ENGINE=InnoDB COMMENT='购物车表'; CREATE TABLE orders ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL, user_id INT UNSIGNED NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT '0=待支付 1=已支付 2=已完成 3=已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINE=InnoDB COMMENT='订单表'; CREATE TABLE order_item ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id INT UNSIGNED NOT NULL, dish_id INT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL, KEY idx_order (order_id) ) ENGINE=InnoDB COMMENT='订单明细表';

课程里常说的数据库增删改查,在这个项目里的体现就是上面六张表。如果中途发现 cart 表漏加了唯一索引,不需要重建表,用 ALTER 也能补:

ALTER TABLE cart ADD UNIQUE KEY uk_user_dish (user_id, dish_id);

这条语句也是实训报告里值得写进“数据库修改结构”一节的内容。改表之前先确认没有脏数据,否则唯一索引会创建失败。

执行完建表语句后,插入几条测试数据,方便后面功能调试:

INSERT INTO category (name, sort) VALUES ('招牌菜', 1), ('下饭菜', 2), ('饮品', 3); INSERT INTO dish (category_id, name, price, stock, status) VALUES (1, '宫保鸡丁', 28.00, 30, 1), (1, '水煮鱼', 58.00, 20, 1), (2, '鱼香肉丝', 32.00, 25, 1);

3.3 用 PyMySQL 封装数据库连接层,注意字符集与自动提交

上一章的 database.py 已经有连接函数,这里补充两个容易被扣分的细节。第一,每张表都定义 created_at 或 add_time,用 MySQL 的 DEFAULT CURRENT_TIMESTAMP 自动填充,不要在 Python 代码里手动传 datetime.now(),时间基准统一由数据库保证。第二,所有 INSERT/UPDATE 用 %s 占位符传参,不要用 f-string 拼接。

字符集方面,建库用了 utf8mb4,连接参数里的 charset 也必须是 utf8mb4。如果只改库不改连接,存中文没问题,一旦写入特殊符号就报“Incorrect string value”。数据库字符集修改可以通过一条 SQL 快速验证:

SHOW VARIABLES LIKE 'character_set%%';

autocommit=False 意味着每条 DML 都需要显式 commit 才会落盘。这一节先记住结论:下单业务里的事务处理依赖这个设置,务必保留。

4. 核心业务功能实现:菜单展示、加购与下单流程

4.1 菜单列表的分页查询:服务端分页而不是一次全查

点餐系统的菜单数量超过 30 条之后,一次全查就不是好选择了。菜单查询必须做服务端分页,前端传 page 和 page_size,后端返回当前页数据。

# dish_dao.py def list_dishes(cursor, page, page_size): offset = (page - 1) * page_size cursor.execute( """SELECT id, name, price, stock, description FROM dish WHERE status = %s ORDER BY id LIMIT %s OFFSET %s""", (1, page_size, offset), ) return cursor.fetchall()

PyMySQL 的 %s 占位符会由驱动处理转义,SQL 注入在这个接口里被天然挡掉了。注意 LIMIT 和 OFFSET 也要作为参数传入,而不是用 f-string 把 page 拼进 SQL,否则分页参数就成了新注入点。OFFSET 分页在实训项目的数据量下完全够用,不需要引入 keyset 分页,答辩也不用给这个简单项目加复杂度。

4.2 购物车与订单状态的状态机设计

订单不是一条记录加上一个数字这么简单。从用户点击下单到最终上菜,订单要经历待支付、已支付、已完成、已取消四个状态。定义一个常量类,比在业务代码里到处写 0、1、2、3 安全得多。

# order_service.py class OrderStatus: PENDING = 0 # 待支付 PAID = 1 # 已支付 FINISHED = 2 # 已完成 CANCELLED = 3 # 已取消
状态值常量名业务含义页面展示
0PENDING待支付去支付
1PAID已支付备餐中
2FINISHED已完成交易成功
3CANCELLED已取消订单关闭

购物车表反而不能加状态字段,它只是一个临时集合。用户提交订单之后,购物车中对应记录会被一次性删除,保留它反而会出现“订单已支付但购物车里还有这个菜”的数据不一致。如果做的是外卖点餐系统,需要在订单表额外增加 address 和 phone,状态流转仍然不变。

4.3 下单事务:为什么必须用 FOR UPDATE 和 rollback

下单不是一条 INSERT,而是“查购物车—扣库存—写订单—写明细—清空购物车”五步的组合操作。任一步失败,其余步骤都不能提交。下面的 create_order 函数就是整套源码里最值得答辩的一段。

# order_service.py from database import get_connection from order_dao import generate_order_no def create_order(user_id): conn = get_connection() try: conn.begin() with conn.cursor() as cursor: cursor.execute( "SELECT dish_id, quantity FROM cart WHERE user_id=%s", (user_id,), ) cart_items = cursor.fetchall() if not cart_items: raise ValueError("购物车为空") total = 0.0 items_for_order = [] for item in cart_items: dish_id = item["dish_id"] quantity = item["quantity"] cursor.execute( "SELECT price, stock, status FROM dish WHERE id=%s FOR UPDATE", (dish_id,), ) dish = cursor.fetchone() if not dish or dish["status"] != 1: raise ValueError(f"菜品 {dish_id} 已下架") if dish["stock"] < quantity: raise ValueError(f"菜品 {dish_id} 库存不足") cursor.execute( "UPDATE dish SET stock=stock-%s WHERE id=%s", (quantity, dish_id), ) total += dish["price"] * quantity items_for_order.append( (dish_id, dish["name"], dish["price"], quantity) ) order_no = generate_order_no() cursor.execute( "INSERT INTO orders (order_no, user_id, total_amount, status)" " VALUES (%s, %s, %s, %s)", (order_no, user_id, total, OrderStatus.PENDING), ) order_id = cursor.lastrowid for item in items_for_order: cursor.execute( """INSERT INTO order_item (order_id, dish_id, name, price, quantity, subtotal) VALUES (%s, %s, %s, %s, %s, %s)""", (order_id, item[0], item[1], item[2], item[3], item[2] * item[3]), ) cursor.execute("DELETE FROM cart WHERE user_id=%s", (user_id,)) conn.commit() except Exception: conn.rollback() raise finally: conn.close()

代码逻辑按五个阶段推进:先查出用户购物车里的所有菜品;再逐行读取菜品价格和库存,用 FOR UPDATE 把该行锁住;然后扣减库存、累计总价;随后写入订单主表和订单明细表;最后清空购物车。整个过程在一个事务里,任何一步抛异常都会触发 rollback,库存扣减、订单写入、购物车清空会一起回滚。

FOR UPDATE 是这段代码的关键。它会给命中行加排他锁,事务提交或回滚后才释放。两个用户同时买同一道最后一份菜时,第二个会被阻塞到第一个事务结束,然后重新读到新库存,避免超卖。如果去掉 FOR UPDATE,两份订单都可能读到 stock=1,并把库存扣成负数,这就是典型的并发写问题。

代码里的 generate_order_no 建议用时间戳加用户编号生成,比如 time.strftime("%Y%m%d%H%M%S") + str(user_id),保证唯一性即可。不要用自增 id 当订单号展示给用户,会暴露平台单量。

提示:如果漏掉 conn.begin(),PyMySQL 在 autocommit=False 下不会自动开启事务吗?实际上每条 DML 都会在隐式事务里执行,但后续 commit 才生效。想保证多语句原子性,仍然必须显式 begin。最容易出现的错误是 delete cart 那一步单独 commit,下单失败时库存回滚了,购物车却被清空了。

5. 高分点在哪:答辩演示时的加分细节与常见坑

5.1 演示前必查的数据一致性

答辩演示最容易翻车的不是功能跑不起来,而是数据自相矛盾。演示前重点检查三项:在售菜品的 stock 必须大于 0;购物车里的数量不能超过菜品库存;orders.total_amount 和 order_item 明细之和必须一致。第三条可以用一条 SQL 快速验证:

SELECT o.id, SUM(oi.subtotal) AS calc_total, o.total_amount FROM orders o JOIN order_item oi ON o.id = oi.order_id GROUP BY o.id, o.total_amount HAVING calc_total <> o.total_amount;

有返回结果说明某张订单的金额算错了。这个动作即使查不出问题,也可以当场演示给老师看,因为它展示了你对“总价冗余”这个设计后果的认知。

5.2 值得主动展示的代码设计点

答辩时主动把下面两处代码翻给老师看,比 PPT 讲十页有用。

第一处,密码不存明文。user 表的 password_hash 用 werkzeug 的 generate_password_hash 生成,而不是 md5(password)。

from werkzeug.security import generate_password_hash, check_password_hash password_hash = generate_password_hash("123456") check_password_hash(password_hash, "123456") # True

werkzeug 默认使用加盐的哈希算法,同样的密码每次生成的哈希值都不同。第二处,所有 SQL 都用 %s 占位符,翻源码能看到 WHERE id=%s 这种写法,而不是 f-string 拼接。这两点能直接回答“用户数据安全怎么做”这类高频提问。

5.3 预留三个可答的扩展方向

老师问“这个项目还能怎么扩展”时,按这个顺序回答。菜品按销量排序,只需要在 order_item 上做 GROUP BY 然后与 dish 关联,不需要改表;营业额日报,用 DATE(create_time) 按天聚合 orders 表;支付超时自动取消,增加一个定时任务扫描 pending 超过 30 分钟的订单并把状态置为 CANCELLED。三个方向都建立在现有表结构之上,能当场说出具体 SQL 或代码路径,远比一句“我打算引入消息队列”可靠。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 20:56:20

MTK DRM显示驱动初始化全解析:从KMS到组件框架

MTK平台的显示驱动&#xff0c;说穿了就是一套Linux原生的DRM/KMS实现&#xff0c;但第一次打开mtk_drm_drv.c的时候&#xff0c;我确实懵了几天——正常的DRM驱动是platform_driver直接probe完set up&#xff0c;而MTK这边到处是component_add、component_match_add、componen…

作者头像 李华
网站建设 2026/9/16 20:54:34

text-to-cad并发构建保护机制:锁等待与竞态处理的工程设计细节

text-to-cad并发构建保护机制&#xff1a;锁等待与竞态处理的工程设计细节 【免费下载链接】text-to-cad A library of agent skills for CAD, CAE and CAM 项目地址: https://gitcode.com/GitHub_Trending/tex/text-to-cad text-to-cad 是一个面向 CAD、CAE 和 CAM 的 …

作者头像 李华
网站建设 2026/9/16 20:53:11

OpenXCAP not yet configured?TaoToken 这样给 Codex 换通道再排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华