python大作业
2025春季学期《Python程序设计》大作业
系统说明报告
编制: 马瑞鑫(20234303)
审查: 专 业 软件工程
班 级 信2305-3
目 录
1项目目的与意义 3
1.1项目背景说明 3
1.2项目目的与意义 3
2 软件开发环境与技术说明 4
2.1软件开发环境 4
2.2软件开发技术描述 4
3系统分析与设计 5
3.1项目需求分析说明 5
3.2系统设计方案 5
4系统源代码 6
4.1系统源代码文件说明 6
4.2源代码 6
5系统使用说明书 7
6参考资料 8
7附件说明 9
1项目目的与意义
1.1项目背景说明
随着餐饮行业的快速发展和数字化转型,传统人工点餐模式暴露出效率低、信息管理混乱、易出错等问题。尤其在客流量较大的场景中,人工记录菜品信息、计算价格、处理订单等操作难以满足实时性和准确性需求16。同时,消费者对点餐便捷性、菜品信息透明化(如价格、烹调时间、图片展示)的要求日益提高11。在此背景下,基于Python的餐厅点餐管理系统应运而生。该系统利用Python语言的高效开发能力与丰富的图形界面库(如Tkinter、PyQt),结合数据库技术(如MySQL或本地文件存储),实现菜品全生命周期管理(增删改查)、动态定价、订单处理等功能。通过图形化界面设计,系统简化了操作流程,降低了员工培训成本,同时为消费者提供直观的点餐体验17。此外,系统的数据持久化功能(保存与读取)可有效避免信息丢失,保障餐厅运营的连续性618。
1.2项目目的与意义
1.提升餐饮管理效率 系统通过集中化管理菜品信息(编号、名称、价格、图片、烹调时间等),实现快速查询与动态更新,减少人工记录错误 。 自动化订单处理与价格计算功能可显著缩短结账时间,降低人力成本,尤其适用于高峰时段的订单处理 。 2. 优化用户体验 图形界面直观展示菜品图片与详情,帮助消费者快速决策,提升点餐效率 。 支持实时菜品状态查询(如烹调进度),增强服务透明度和客户满意度 。 3. 推动技术实践与创新 通过Python面向对象编程实现模块化设计(如MenuItem类、Order类),提升代码复用性和可维护性 。 结合数据库技术(如MySQL或SQLite),探索数据持久化存储方案,为后续功能扩展(如销售分析、用户行为追踪)奠定基础 。 4. 促进餐饮行业数字化转型 系统可作为中小型餐厅低成本数字化转型的解决方案,帮助其适应“互联网+餐饮”趋势 。 通过积累的菜品销售数据,为餐厅优化菜单结构、调整定价策略提供数据支撑。 5. 教学与科研价值 项目涵盖Python核心语法、GUI开发、数据库操作等知识点,可作为软件工程与算法设计的实践案例 。 为后续智能化扩展(如推荐算法、智能库存管理)提供研究基础 。
2 软件开发环境与技术说明
2.1软件开发环境
python环境要求
Python3.8
Window10
Python依赖包
Python pillow
Mysql-connector-python
数据库环境
Mysql8.0
2.2软件开发技术描述
点菜系统开发技术说明
- 界面设计技术 GUI框架:基于 Tkinter(Python标准GUI库)实现窗口、按钮、表格等组件。 布局管理:使用 pack()、grid() 进行控件排版,结合 Frame 容器划分功能区域。 数据展示:通过 ttk.Treeview 展示菜品列表和购物车,支持列排序和动态更新。 图片交互:利用 Pillow(PIL) 库处理图片(如调整大小、格式转换),并通过 Label 控件显示图片预览。 事件绑定:使用 bind() 方法实现菜单项选中、按钮点击等事件的响应逻辑。 用户体验优化: 模态窗口(Toplevel)用于编辑菜品或支付确认。 图片预览功能动态加载本地图片路径。 购物车实时更新总价和数量。
- 数据库连接技术 数据库类型:MySQL 作为后端数据库,存储菜品信息和图片路径。 连接管理: 使用 mysql-connector-python 驱动进行数据库操作。 单例模式(DatabaseManager 类)确保全局共享一个连接,避免资源浪费。 参数化查询(cursor.execute(query, params))防止 SQL 注入攻击。 自动重连机制:在执行查询前检查连接状态(is_connected())。 表结构设计:
SQL CREATE TABLE dishes ( id VARCHAR(50) PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, cook_time VARCHAR(50), image_path VARCHAR(255) ); image_path
字段存储本地图片路径,通过 get_dish_image() 方法动态加载。 - 网络连接技术 数据库网络访问: 支持本地或远程 MySQL 服务,通过配置 host、user、password 参数连接。 需确保防火墙开放 MySQL 端口(默认3306),并授予用户远程访问权限(若使用远程数据库)。 模拟支付功能: 当前支付流程为本地模拟(现金、微信、支付宝),未集成真实支付接口。 扩展建议: 集成第三方支付 API(如支付宝/微信支付的 SDK)。 使用 requests 库发起 HTTPS 请求与支付网关交互。 通过 WebSocket 实现订单状态实时推送。
- 数据验证与字符串处理 输入验证: 使用 try-except 捕获数据类型转换异常(如价格必须为浮点数)。 通过 StringVar 和 Entry 控件限制用户输入格式(如价格字段仅允许数字)。 空值检查:提交表单时验证必填字段是否为空。 正则表达式应用(潜在场景): Python import re # 验证价格格式(如 12.34) if not re.match(r"^\d+(.\d{1,2})?$", price_input): messagebox.showerror("错误", "价格格式无效!") # 验证菜品ID格式(如字母+数字组合) if not re.match(r"[1]+$", dish_id): messagebox.showerror("错误", "ID只能包含字母、数字、下划线和短横线!") SQL模糊查询: 使用 LIKE %s 和通配符 % 实现菜品名称或ID的模糊搜索。
- 其他关键技术 图片管理: 通过 os 模块操作文件系统,自动创建 dish_images 目录。 文件名冲突时自动添加后缀(如 image_1.jpg)。 使用绝对路径或相对路径确保跨平台兼容性。 异常处理: 数据库操作异常捕获(如连接失败、查询错误)。 文件操作异常处理(如图片路径不存在时抛出 FileNotFoundError)。 状态持久化: 购物车数据暂存于内存列表,关闭窗口后清空。 扩展建议:将购物车数据持久化到临时文件或数据库,支持用户会话恢复。
3系统分析与设计
3.1项目需求分析说明
3.2系统设计方案
系统采用 分层架构,分为以下三个核心层级:
- 表现层(UI):基于 Tkinter 的图形界面,提供菜单管理、点餐、支付等功能入口。 业务逻辑层:处理核心业务流程(如菜品增删改查、购物车管理、支付逻辑)。数据访问层:通过 DatabaseManager 类封装 MySQL 数据库操作,实现数据持久化。
- 功能模块划分 模块名称 功能描述 主界面模块 提供系统入口,支持跳转到菜单管理和点菜界面。 菜单管理模块 实现菜品信息的增删改查,支持图片上传、预览和数据库同步。 点菜系统模块 展示菜单列表,支持购物车管理(添加、删除、调整数量)和模拟支付流程。 数据库模块 封装 MySQL 连接与 CRUD 操作,确保数据安全性和一致性。 图片管理模块 处理图片选择、尺寸调整、本地存储,支持动态加载预览。
- 核心数据流 管理员操作流程: Plaintext Tkinter界面 → 输入菜品信息 → 上传图片 → 保存到dish_images目录 → 写入dishes表 顾客操作流程: Plaintext 浏览菜单 → 选择菜品 → 加入购物车 → 计算总价 → 模拟支付 → 清空购物车
二、数据库设计方案 - 数据库表结构 表名:dishes(菜品表) 字段名 类型 约束 描述 id VARCHAR(50) PRIMARY KEY 菜品唯一标识(如D001) name VARCHAR(100) NOT NULL 菜品名称 price DECIMAL(10,2) NOT NULL 价格(保留两位小数) cook_time VARCHAR(50) DEFAULT NULL 烹饪时间(如“15分钟”) image_path VARCHAR(255) DEFAULT NULL 本地图片路径(可为空)
- 关键设计说明 主键设计:id 字段作为业务主键,需保证唯一性(例如 D001)。 图片存储:图片路径存储在 image_path 字段,实际文件保存在本地 dish_images 目录。 扩展性考虑: 可新增 category 字段支持菜品分类。 可添加 status 字段管理菜品上下架状态。
- 数据交互流程 数据写入: Python # 示例:添加菜品到数据库 db.add_dish_with_image(("D001", "宫保鸡丁", 38.0, "20分钟", "dish_images/d001.jpg")) 数据读取: Python # 示例:获取所有菜品 dishes = db.get_all_dishes() # 返回格式: [(id, name, price, cook_time, image_path), ...]
4系统源代码
4.1系统源代码文件说明
文件作用列表 文件名 核心功能
database.py - 封装 MySQL 数据库连接(单例模式)- 提供菜品表的 CRUD 操作(增删改查)- 防止 SQL 注入的通用查询方法 e
xecute_query image_utils.py - 图片选择对话框(支持 JPG/PNG 格式)- 调整图片尺寸以适应界面显示- 保存图片到 dish_images 目录(自动处理文件名冲突)
main.py - 系统主界面入口- 提供导航按钮跳转到菜单管理或点菜系统- 管理子窗口的打开与关闭逻辑
menu_manager.py - 管理员界面:通过 Treeview 展示菜品列表(含图片状态)- 支持菜品增删改查和图片上传- 实时预览选中菜品的图片
order_system.py - 顾客界面:展示菜单列表和图片预览- 购物车管理(添加/删除/调整数量)- 模拟支付流程(现金、微信、支付宝)
关键依赖关系 数据库交互: menu_manager.py 和 order_system.py 依赖 database.py 读写菜品数据。 图片处理: menu_manager.py 调用 image_utils.py 上传和调整图片。 order_system.py 通过 database.py 获取图片路径后动态加载图片。 界面导航: main.py 作为入口,通过按钮跳转到 menu_manager.py(管理员)或 order_system.py(顾客)。 备注 dish_images 目录:程序首次运行时自动创建,用于存储上传的菜品图片。 单例模式:database.py 中通过 db = DatabaseManager() 全局共享数据库连接。 安全性:所有 SQL 查询均使用参数化(execute_query 方法)防止注入攻击。 通过此结构,系统实现了模块化设计,便于扩展新功能(如订单历史、用户权限管理)。
4.2源代码
数据库代码database.py:
import mysql.connector
from mysql.connector import Error
import os
class DatabaseManager:
def init(self, host="localhost", user="root", password="1234", database="restaurant"):
"""初始化数据库连接配置"""
self.host = host
self.user = user
self.password = password
self.database = database
self.connection = None
def add_dish_with_image(self, dish_data_with_image):
"""
参数: dish_data_with_image 应包含5个元素的元组
(id, name, price, cook_time, image_path)
"""
query = """
INSERT INTO dishes (id, name, price, cook_time, image_path)
VALUES (%s, %s, %s, %s, %s)
"""
# 检查图片路径是否存在
if not os.path.exists(dish_data_with_image[4]):
raise FileNotFoundError(f"图片文件不存在: {dish_data_with_image[4]}")
return self.execute_query(query, dish_data_with_image, fetch=False)
def get_dish_image(self, dish_id):
"""获取菜品图片路径"""
query = "SELECT image_path FROM dishes WHERE id = %s"
result = self.execute_query(query, (dish_id,))
return result[0][0] if result else None
def update_dish_with_image(self, dish_data):
"""更新带图片的菜品"""
query = """
UPDATE dishes
SET name=%s, price=%s, cook_time=%s, image_path=%s
WHERE id=%s
"""
# 检查图片路径是否存在(如果不是空路径)
if dish_data[3] and not os.path.exists(dish_data[3]):
raise FileNotFoundError(f"图片文件不存在: {dish_data[3]}")
return self.execute_query(query, dish_data, fetch=False)
def connect(self):
"""建立数据库连接"""
try:
self.connection = mysql.connector.connect(
host=self.host,
user=self.user,
password=self.password,
database=self.database
)
print("数据库连接成功!")
return True
except Error as e:
print(f"数据库连接失败: {e}")
return False
def is_connected(self):
"""检查连接是否有效"""
return self.connection is not None and self.connection.is_connected()
def disconnect(self):
"""关闭数据库连接"""
if self.connection and self.connection.is_connected():
self.connection.close()
print("数据库连接已关闭。")
def execute_query(self, query, params=None, fetch=True):
cursor = None
try:
cursor = self.connection.cursor()
cursor.execute(query, params or ())
if fetch:
result = cursor.fetchall()
return result
else:
self.connection.commit()
return cursor.rowcount # 总是返回受影响行数
except Error as e:
# 将错误信息转换为字符串
error_msg = f"SQL执行错误: {e}"
print(error_msg)
# 抛出异常让上层处理
raise Exception(error_msg)
finally:
if cursor:
cursor.close()
# ---------- 针对菜品表的专用方法 ----------
def get_all_dishes(self):
"""获取所有菜品(包含图片路径)"""
query = "SELECT id, name, price, cook_time, image_path FROM dishes"
return self.execute_query(query)
def add_dish(self, dish_data):
"""添加菜品"""
query = """
INSERT INTO dishes (id, name, price, cook_time)
VALUES (%s, %s, %s, %s)
"""
return self.execute_query(query, dish_data, fetch=False)
def update_dish(self, dish_data):
"""更新菜品"""
query = """
UPDATE dishes
SET name=%s, price=%s, cook_time=%s
WHERE id=%s
"""
return self.execute_query(query, dish_data, fetch=False)
def delete_dish(self, dish_id):
"""删除菜品"""
query = "DELETE FROM dishes WHERE id=%s"
return self.execute_query(query, (dish_id,), fetch=False)
def search_dishes(self, keyword):
"""搜索菜品(按ID或名称模糊匹配)"""
query = """
SELECT id, name, price, cook_time
FROM dishes
WHERE id LIKE %s OR name LIKE %s
"""
return self.execute_query(query, (f"%{keyword}%", f"%{keyword}%"))
单例模式:全局共享一个数据库连接
db = DatabaseManager()
if name == "main":
# 测试数据库连接
if db.connect():
print("测试查询结果:", db.get_all_dishes())
db.disconnect()处理图像的工具类image_utils.py
:
image_utils.py
import os
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import filedialog
class ImageManager:
@staticmethod
def resize_image(image_path, size=(200, 200)):
"""调整图片大小"""
img = Image.open(image_path)
img = img.resize(size, Image.LANCZOS)
return ImageTk.PhotoImage(img)
@staticmethod
def select_image():
"""选择图片文件对话框"""
filetypes = (
('图片文件', '*.jpg *.jpeg *.png'),
('所有文件', '*.*')
)
filename = filedialog.askopenfilename(
title='选择菜品图片',
filetypes=filetypes
)
return filename if filename else None
@staticmethod
def save_image_to_folder(image_path, target_folder="dish_images"):
"""将图片保存到指定文件夹"""
if not os.path.exists(target_folder):
os.makedirs(target_folder)
filename = os.path.basename(image_path)
target_path = os.path.join(target_folder, filename)
# 避免文件名冲突
counter = 1
while os.path.exists(target_path):
name, ext = os.path.splitext(filename)
target_path = os.path.join(target_folder, f"{name}_{counter}{ext}")
counter += 1
img = Image.open(image_path)
img.save(target_path)
return target_path
菜单面理类import os
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
from database import db # 导入数据库管理器
from image_utils import ImageManager
from PIL import Image, ImageTk
class RestaurantSystem:
import os;
def init(self, root):
self.root = root
if not db.connect(): # 启动时连接数据库
self.root.destroy() # 连接失败则关闭窗口
return
self.setup_ui()
self.root.protocol("WM_DELETE_WINDOW", self.on_close) # 窗口关闭事件
self.current_image = None # 保持图片引用
self.setup_image_preview() # 初始化图片预览区域
def setup_image_preview(self):
"""设置图片预览区域"""
self.preview_frame = tk.Frame(self.root)
self.preview_frame.pack(side=tk.RIGHT, fill=tk.BOTH, padx=10)
self.image_label = tk.Label(self.preview_frame)
self.image_label.pack()
# 绑定选中事件
self.tree.bind('<<TreeviewSelect>>', self.show_selected_image)
def show_selected_image(self, event):
"""显示选中菜品的图片"""
if not hasattr(self, 'image_label') or not self.image_label.winfo_exists():
return # 防止访问已销毁的组件
selected = self.tree.selection()
if not selected:
self.image_label.config(image=None)
return
try:
dish_id = self.tree.item(selected[0])['values'][0]
query = "SELECT image_path FROM dishes WHERE id = %s"
result = db.execute_query(query, (dish_id,))
if result and result[0][0]:
image_path = result[0][0]
try:
img = Image.open(image_path)
img = img.resize((200, 200), Image.LANCZOS)
self.current_image = ImageTk.PhotoImage(img)
self.image_label.config(image=self.current_image)
self.image_label.image = self.current_image # 保持引用
except Exception as e:
print(f"加载图片失败: {e}")
self.image_label.config(image=None)
else:
self.image_label.config(image=None)
except Exception as e:
print(f"显示图片时出错: {e}")
def show_edit_window(self, dish_data=None):
"""显示编辑窗口(带图片功能)"""
win = tk.Toplevel(self.root)
win.title("修改菜品" if dish_data else "添加菜品")
# 图片显示区域
self.current_image = None
image_frame = tk.Frame(win)
image_frame.grid(row=0, column=2, rowspan=4, padx=10)
self.image_label = tk.Label(image_frame)
self.image_label.pack()
# 图片选择按钮
tk.Button(
image_frame,
text="选择图片",
command=lambda: self.select_image(win)
).pack(pady=5)
# 其他表单控件(保持不变)...
# 只需要调整grid布局,给图片留出空间
# 如果有菜品数据,加载图片
if dish_data and dish_data.get('image_path'):
self.load_image(dish_data['image_path'])
def select_image(self, parent):
"""选择菜品图片"""
image_path = ImageManager.select_image()
if image_path:
try:
# 保存图片到项目目录
saved_path = ImageManager.save_image_to_folder(image_path)
self.load_image(saved_path)
self.temp_image_path = saved_path # 临时存储,等确认后再更新数据库
except Exception as e:
messagebox.showerror("错误", f"图片处理失败: {e}")
def load_image(self, image_path):
"""加载并显示图片"""
try:
# 保持对图片对象的引用
self.current_image = ImageManager.resize_image(image_path)
self.image_label.config(image=self.current_image)
self.image_label.image = self.current_image # 关键:保持引用
except Exception as e:
messagebox.showerror("错误", f"无法加载图片: {e}")
def setup_ui(self):
"""初始化用户界面"""
self.root.title("菜单管理")
# 菜单列表 (Treeview)
self.tree = ttk.Treeview(
self.root,
columns=('ID', 'Name', 'Price', 'Time', 'Image'),
show='headings'
)
self.tree.heading('Image', text='图片')
self.tree.heading('ID', text='菜品编号')
self.tree.heading('Name', text='菜品名称')
self.tree.heading('Price', text='价格(元)')
self.tree.heading('Time', text='烹饪时间(分钟)')
self.tree.column('ID', width=100, anchor='center')
self.tree.column('Name', width=150, anchor='center')
self.tree.column('Price', width=80, anchor='center')
self.tree.column('Time', width=120, anchor='center')
self.tree.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 功能按钮区域
btn_frame = tk.Frame(self.root)
btn_frame.pack(pady=(0, 10))
btn_style = {'width': 10, 'padx': 5}
tk.Button(btn_frame, text="添加菜品", command=self.add_dish, **btn_style).grid(row=0, column=0)
tk.Button(btn_frame, text="修改菜品", command=self.edit_dish, **btn_style).grid(row=0, column=1)
tk.Button(btn_frame, text="删除菜品", command=self.del_dish, **btn_style).grid(row=0, column=2)
tk.Button(btn_frame, text="搜索菜品", command=self.search_dish, **btn_style).grid(row=0, column=3)
tk.Button(btn_frame, text="刷新列表", command=self.refresh_list, **btn_style).grid(row=0, column=4)
# 初始加载数据
self.refresh_list()
def refresh_list(self):
"""刷新菜品列表显示"""
for item in self.tree.get_children():
self.tree.delete(item)
dishes = self.load_data()
for dish in dishes:
# 显示所有信息,包括图片状态
self.tree.insert('', tk.END, values=(
dish['id'],
dish['name'],
f"{dish['price']:.2f}",
dish['cook_time'],
"✓" if dish['image_path'] else "✗" # 显示图片状态
))
def load_data(self):
"""从数据库加载菜品数据(包含图片处理)"""
dishes = db.get_all_dishes()
return [
{
"id": d[0],
"name": d[1],
"price": float(d[2]),
"cook_time": d[3],
"image_path": d[4] if d[4] else None # 处理可能的NULL值
}
for d in dishes
] if dishes else []
def add_dish(self):
"""添加新菜品"""
self.show_edit_window()
def edit_dish(self):
"""修改选中菜品"""
selected = self.tree.selection()
if not selected:
messagebox.showwarning("提示", "请先选择要修改的菜品")
return
# 获取选中行的ID(确保是第一列)
dish_id = self.tree.item(selected[0])['values'][0]
# 直接从数据库获取完整数据(确保包含image_path)
query = "SELECT id, name, price, cook_time, image_path FROM dishes WHERE id = %s"
result = db.execute_query(query, (dish_id,))
if not result:
messagebox.showerror("错误", "找不到选中的菜品")
return
dish_data = {
'id': result[0][0],
'name': result[0][1],
'price': float(result[0][2]),
'cook_time': result[0][3],
'image_path': result[0][4] if result[0][4] else None
}
self.show_edit_window(dish_data)
def del_dish(self):
"""删除选中菜品"""
selected = self.tree.selection()
if not selected:
messagebox.showwarning("提示", "请先选择要删除的菜品")
return
dish_id = self.tree.item(selected[0])['values'][0]
if messagebox.askyesno("确认", "确定要删除该菜品吗?"):
if db.delete_dish(dish_id):
messagebox.showinfo("成功", "菜品删除成功!")
self.refresh_list()
else:
messagebox.showerror("错误", "删除失败,请检查数据库!")
def search_dish(self):
"""搜索菜品"""
keyword = simpledialog.askstring("搜索", "输入菜品编号或名称:")
if not keyword:
return
results = db.search_dishes(keyword)
if not results:
messagebox.showinfo("提示", "未找到匹配菜品")
return
# 清空并显示搜索结果
for item in self.tree.get_children():
self.tree.delete(item)
for dish in results:
self.tree.insert('', tk.END, values=(
dish[0], dish[1], f"{float(dish[2]):.2f}", dish[3]
))
def _find_dish_by_id(self, dish_id):
"""根据ID查找菜品(返回字典格式)"""
dishes = self.load_data()
for dish in dishes:
if dish['id'] == dish_id:
return dish
return None
def show_edit_window(self, dish_data=None):
"""显示编辑窗口(带图片功能)"""
win = tk.Toplevel(self.root)
win.title("修改菜品" if dish_data else "添加菜品")
win.grab_set() # 模态窗口
# 使用局部变量存储图片引用,避免与主窗口冲突
current_image = None
temp_image_path = dish_data.get('image_path') if dish_data else None
# 图片显示区域
image_frame = tk.Frame(win)
image_frame.grid(row=0, column=2, rowspan=4, padx=10)
image_label = tk.Label(image_frame)
image_label.pack()
def load_image(image_path):
"""加载并显示图片(局部函数)"""
nonlocal current_image
try:
if image_path and os.path.exists(image_path):
current_image = ImageManager.resize_image(image_path)
image_label.config(image=current_image)
image_label.image = current_image # 保持引用
else:
image_label.config(image=None)
except Exception as e:
print(f"加载图片失败: {e}")
image_label.config(image=None)
# 图片选择按钮
def select_image():
"""选择图片文件"""
nonlocal temp_image_path
image_path = ImageManager.select_image()
if image_path:
try:
saved_path = ImageManager.save_image_to_folder(image_path)
load_image(saved_path)
temp_image_path = saved_path
except Exception as e:
messagebox.showerror("错误", f"图片处理失败: {e}")
tk.Button(
image_frame,
text="选择图片",
command=select_image
).pack(pady=5)
# 如果有菜品数据,加载图片
if dish_data and dish_data.get('image_path'):
load_image(dish_data['image_path'])
# 表单控件
tk.Label(win, text="菜品编号:").grid(row=0, column=0, padx=5, pady=5, sticky='e')
tk.Label(win, text="菜品名称:").grid(row=1, column=0, padx=5, pady=5, sticky='e')
tk.Label(win, text="价格(元):").grid(row=2, column=0, padx=5, pady=5, sticky='e')
tk.Label(win, text="烹饪时间(分钟):").grid(row=3, column=0, padx=5, pady=5, sticky='e')
id_var = tk.StringVar(value=dish_data['id'] if dish_data else '')
name_var = tk.StringVar(value=dish_data['name'] if dish_data else '')
price_var = tk.StringVar(value=f"{dish_data['price']:.2f}" if dish_data else '')
time_var = tk.StringVar(value=dish_data['cook_time'] if dish_data else '')
# 编号框在修改时禁用
id_entry = tk.Entry(win, textvariable=id_var, state='readonly' if dish_data else 'normal')
id_entry.grid(row=0, column=1, padx=5, pady=5)
tk.Entry(win, textvariable=name_var).grid(row=1, column=1, padx=5, pady=5)
tk.Entry(win, textvariable=price_var).grid(row=2, column=1, padx=5, pady=5)
tk.Entry(win, textvariable=time_var).grid(row=3, column=1, padx=5, pady=5)
def confirm():
try:
# 收集数据
new_data = {
'id': id_var.get().strip(),
'name': name_var.get().strip(),
'price': float(price_var.get()),
'cook_time': time_var.get().strip(),
'image_path': temp_image_path if temp_image_path else (
dish_data.get('image_path') if dish_data else None)
}
# 数据验证
if not all([new_data['name'], str(new_data['price']), new_data['cook_time']]):
messagebox.showwarning("警告", "菜品名称、价格和烹饪时间必须填写!")
return
if new_data['price'] <= 0:
messagebox.showwarning("警告", "价格必须大于0!")
return
# 执行数据库操作
try:
if dish_data: # 修改模式
affected_rows = db.update_dish_with_image(
(new_data['name'], new_data['price'],
new_data['cook_time'], new_data['image_path'],
new_data['id'])
)
action = "修改"
else: # 添加模式
affected_rows = db.add_dish_with_image(
(new_data['id'], new_data['name'],
new_data['price'], new_data['cook_time'],
new_data['image_path'])
)
action = "添加"
if affected_rows > 0:
messagebox.showinfo("成功", f"菜品{action}成功!")
self.refresh_list()
win.destroy()
else:
messagebox.showerror("错误", f"菜品{action}失败!数据库返回0行受影响")
except FileNotFoundError as e:
messagebox.showerror("错误", f"图片文件未找到: {str(e)}")
except Exception as e:
messagebox.showerror("错误", f"操作异常: {str(e)}")
except ValueError:
messagebox.showwarning("警告", "请输入有效的价格数字!")
tk.Button(win, text="确认", command=confirm, width=10).grid(row=4, columnspan=2, pady=10)
def on_close(self):
"""关闭窗口时的清理操作"""
db.disconnect()
self.root.destroy()
独立运行时的测试代码
if name == "main":
root = tk.Tk()
app = RestaurantSystem(root)
root.mainloop()
点菜类order_system.py:
import tkinter as tk
from tkinter import ttk, messagebox
from database import db
from image_utils import ImageManager
import os
from PIL import Image, ImageTk
class OrderSystem:
def init(self, root):
self.root = root
if not db.is_connected() and not db.connect(): # 确保连接有效
messagebox.showerror("错误", "无法连接数据库,请检查配置!")
self.root.destroy()
return
self.root.title("点菜系统")
self.cart = [] # 购物车列表
self.current_image = None # 保持图片引用
self.setup_ui()
def load_menu(self):
"""从数据库加载菜单(带错误处理)"""
try:
dishes = db.get_all_dishes()
if dishes is None: # 查询失败
raise RuntimeError("获取菜品数据失败")
for dish in dishes:
self.tree_menu.insert('', tk.END, values=(dish[1], f"{float(dish[2]):.2f}"))
except Exception as e:
messagebox.showerror("错误", f"加载菜单失败: {e}")
self.root.destroy()
def setup_ui(self):
"""初始化界面(带图片预览)"""
# 主框架
main_frame = tk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True)
# 左侧菜单列表
left_frame = tk.Frame(main_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 右侧图片预览和购物车
right_frame = tk.Frame(main_frame, width=300)
right_frame.pack(side=tk.RIGHT, fill=tk.Y)
# 菜单列表
self.tree_menu = ttk.Treeview(
left_frame,
columns=('ID', 'Name', 'Price'),
show='headings'
)
self.tree_menu.heading('ID', text='编号')
self.tree_menu.heading('Name', text='菜品名称')
self.tree_menu.heading('Price', text='价格')
self.tree_menu.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
# 图片预览区域
self.image_frame = tk.Frame(right_frame)
self.image_frame.pack(pady=10)
self.image_label = tk.Label(self.image_frame)
self.image_label.pack()
# 绑定菜单选择事件
self.tree_menu.bind('<<TreeviewSelect>>', self.show_dish_image)
# 加载菜单数据
# 购物车区域 ======================================
cart_frame = tk.Frame(right_frame)
cart_frame.pack(fill=tk.X, pady=10)
# 购物车标题
tk.Label(cart_frame, text="购物车", font=('Arial', 12, 'bold')).pack()
# 购物车列表
self.tree_cart = ttk.Treeview(
cart_frame,
columns=('Name', 'Price', 'Qty'),
show='headings',
height=6
)
self.tree_cart.heading('Name', text='菜品')
self.tree_cart.heading('Price', text='单价')
self.tree_cart.heading('Qty', text='数量')
self.tree_cart.column('Name', width=100)
self.tree_cart.column('Price', width=60)
self.tree_cart.column('Qty', width=40)
self.tree_cart.pack(fill=tk.X)
# 总价显示
self.label_total = tk.Label(
cart_frame,
text="总价: 0.00 元",
font=('Arial', 10, 'bold')
)
self.label_total.pack(pady=5)
# 操作按钮
btn_frame = tk.Frame(cart_frame)
btn_frame.pack(fill=tk.X)
tk.Button(
btn_frame,
text="加入购物车",
command=self.add_to_cart
).pack(side=tk.LEFT, padx=2)
tk.Button(
btn_frame,
text="支付",
command=self.checkout,
bg='#4CAF50',
fg='white'
).pack(side=tk.RIGHT, padx=2)
self.load_menu()
def show_dish_image(self, event):
"""显示选中菜品的图片"""
selected = self.tree_menu.selection()
if not selected:
return
dish_id = self.tree_menu.item(selected[0])['values'][0]
image_path = db.get_dish_image(dish_id) # 需要实现这个方法
if image_path and os.path.exists(image_path):
try:
img = Image.open(image_path)
img = img.resize((250, 200), Image.LANCZOS)
self.current_image = ImageTk.PhotoImage(img)
self.image_label.config(image=self.current_image)
self.image_label.image = self.current_image # 保持引用
except Exception as e:
print(f"图片加载失败: {e}")
self.image_label.config(image=None)
else:
self.image_label.config(image=None)
def show_selected_dish_image(self, event):
"""显示选中菜品的图片"""
selected = self.tree_menu.selection()
if not selected:
return
dish_id = self.tree_menu.item(selected[0])['values'][0]
image_path = db.get_dish_image(dish_id)
if image_path:
try:
img = ImageManager.resize_image(image_path, (300, 200))
self.dish_image_label.config(image=img)
self.dish_image_label.image = img # 保持引用
except Exception as e:
print(f"无法加载图片: {e}")
self.dish_image_label.config(image=None)
def load_menu(self):
"""从数据库加载菜单数据"""
for item in self.tree_menu.get_children():
self.tree_menu.delete(item)
dishes = db.get_all_dishes() # 确保这个方法返回image_path
for dish in dishes:
self.tree_menu.insert('', tk.END, values=(
dish[0], # ID
dish[1], # Name
f"{float(dish[2]):.2f}" # Price
))
def add_to_cart(self):
"""将选中菜品加入购物车"""
selected = self.tree_menu.selection()
if not selected:
messagebox.showwarning("提示", "请先选择菜品")
return
# 获取菜品ID、名称和价格
dish_id, dish_name, dish_price = self.tree_menu.item(selected[0])['values']
dish_price = float(dish_price)
# 检查是否已在购物车
for item in self.cart:
if item['id'] == dish_id: # 改用ID判断更准确
item['qty'] += 1
self.update_cart_display()
return
# 新增到购物车
self.cart.append({
'id': dish_id, # 添加ID字段
'name': dish_name,
'price': dish_price,
'qty': 1
})
self.update_cart_display()
def update_cart_display(self):
"""更新购物车显示和总价"""
# 清空当前显示
for item in self.tree_cart.get_children():
self.tree_cart.delete(item)
# 重新加载购物车
total = 0.0
for item in self.cart:
self.tree_cart.insert('', tk.END, values=(
item['name'],
f"{item['price']:.2f}",
item['qty']
))
total += item['price'] * item['qty']
# 更新总价
self.label_total.config(text=f"总价: {total:.2f} 元")
def checkout(self):
"""支付功能"""
if not self.cart:
messagebox.showwarning("提示", "购物车为空!")
return
total = sum(item['price'] * item['qty'] for item in self.cart)
# 创建支付确认窗口
pay_win = tk.Toplevel(self.root)
pay_win.title("支付确认")
tk.Label(pay_win, text=f"总计: {total:.2f} 元", font=('Arial', 14)).pack(pady=10)
# 支付方式选择
pay_method = tk.StringVar(value="cash")
tk.Radiobutton(pay_win, text="现金", variable=pay_method, value="cash").pack()
tk.Radiobutton(pay_win, text="微信", variable=pay_method, value="wechat").pack()
tk.Radiobutton(pay_win, text="支付宝", variable=pay_method, value="alipay").pack()
def confirm_pay():
messagebox.showinfo("成功", f"{pay_method.get()}支付成功!")
self.cart.clear()
self.update_cart_display()
pay_win.destroy()
tk.Button(pay_win, text="确认支付", command=confirm_pay, bg='green', fg='white').pack(pady=10)
if name == "main":
root = tk.Tk()
app = OrderSystem(root)
root.mainloop()
5系统使用说明书
系统用户角色:工作人员和顾客
工作人员可以增删查改菜品并且保存到数据库
顾客可以点菜
1.进入主页面后有三个选项,工作人员点击菜单管理
之后来到菜品管理页面可以对菜品进行增删改查
修改菜品:
删除菜品:
搜索菜品:
- 顾客点击点菜进入点菜页面:
点完之后结账:
结账成功:
6参考资料
7附件说明
列出本文档要求的附件资料,主要包括项目源代码(包含支持类库、数据库备份)、可执行文件、运行环境说明、开发环境说明等资料。这些附件文件已压缩包的形式压缩上交。
A-Za-z0-9_- ↩︎

浙公网安备 33010602011771号