尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

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

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 PyMySQL低SQL 与业务全可见Python 实训、数据库课程设计Django ORM中ORM 屏蔽 SQLWeb 开发课SpringBoot Vue高前后端分离切分清晰综合项目实训有人会用 Flask-SQLAlchemy 替代 PyMySQL这在实训场景下我不太推荐。ORM 在开发时写得更快但增删改查的 SQL 就藏到模型类后面去了。答辩被问“这条 UPDATE 有没有 WHERE 条件”时你翻开源码给他看的是一个 save() 方法解释成本很高。自己手写 SQL能让所有查询路径肉眼可见分数反而好拿。2.2 项目目录怎么分能让老师一眼看懂分层写点餐系统不要把所有代码堆在一个 app.py 里。我一般会拆成 routes、services、dao、database 四个层级路由层负责接收参数和返回 JSONservice 层写下单、付款这类业务逻辑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(host0.0.0.0, port5000, debugTrue)database.py 不直接把连接对象暴露给路由层请求结束后的归还和异常处理集中在同一个地方。host 写 127.0.0.1 是因为本机演示最稳定如果连接时报“Access denied”先检查 USER 的密码和允许登录的 host而不是去改 MySQL 的 bind-address。debugTrue 开发时开着答辩演示前建议关掉否则报错页会把 SQL 语句连同参数一起展示出来。# database.py import pymysql from pymysql.cursors import DictCursor import config def get_connection(): return pymysql.connect( hostconfig.HOST, portconfig.PORT, userconfig.USER, passwordconfig.PASSWORD, databaseconfig.DATABASE, charsetutf8mb4, cursorclassDictCursor, autocommitFalse, )charset 必须和建库字符集保持一致否则写入 emoji 或生僻字会直接报错。cursorclass 用 DictCursor查询结果返回字典列表在 service 层取值用 result[status]比默认元组的 result[3] 可读性好得多。autocommit 关掉是给第 4 章的下单事务做准备事务控制权必须留在业务代码手里。3. 点餐系统数据库设计从 ER 图到建表 SQL3.1 六张核心表与字段设计点餐系统的核心实体是用户、菜品、分类、购物车、订单。只做增删改查的话两张表就够但答辩要想站得住必须把业务状态放进表设计里。我常用的表结构如下。表名核心字段关键约束作用userid, username, password_hash, roleusername 唯一顾客和店长共用role 区分categoryid, name, sortsort 默认 0菜单分组dishid, category_id, name, price, stock, statusprice DECIMAL(10,2)价格不能用 floatcartid, 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.10.20.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) ) ENGINEInnoDB COMMENT用户表; CREATE TABLE category ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL, sort INT NOT NULL DEFAULT 0 ) ENGINEInnoDB 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) ) ENGINEInnoDB 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) ) ENGINEInnoDB 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) ) ENGINEInnoDB 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) ) ENGINEInnoDB 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%%;autocommitFalse 意味着每条 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 stockstock-%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两份订单都可能读到 stock1并把库存扣成负数这就是典型的并发写问题。代码里的 generate_order_no 建议用时间戳加用户编号生成比如 time.strftime(%Y%m%d%H%M%S) str(user_id)保证唯一性即可。不要用自增 id 当订单号展示给用户会暴露平台单量。提示如果漏掉 conn.begin()PyMySQL 在 autocommitFalse 下不会自动开启事务吗实际上每条 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) # Truewerkzeug 默认使用加盐的哈希算法同样的密码每次生成的哈希值都不同。第二处所有 SQL 都用 %s 占位符翻源码能看到 WHERE id%s 这种写法而不是 f-string 拼接。这两点能直接回答“用户数据安全怎么做”这类高频提问。5.3 预留三个可答的扩展方向老师问“这个项目还能怎么扩展”时按这个顺序回答。菜品按销量排序只需要在 order_item 上做 GROUP BY 然后与 dish 关联不需要改表营业额日报用 DATE(create_time) 按天聚合 orders 表支付超时自动取消增加一个定时任务扫描 pending 超过 30 分钟的订单并把状态置为 CANCELLED。三个方向都建立在现有表结构之上能当场说出具体 SQL 或代码路径远比一句“我打算引入消息队列”可靠。本文还有配套的精品资源点击获取
返回列表