简介面向数据库系统课程设计的酒店管理系统完整项目基于PythonPyQt5MySQL搭建包含可直接运行的源代码、数据库脚本与课程设计报告适合计算机及相关专业学生参考、复用或进行功能扩展。系统围绕酒店日常运营实现员工管理含权限分级与增删改查、客房管理个人/团队/预订入住、退房、房间维护和客户管理入住记录存档与分析等核心业务界面全部采用Qt Designer绘制业务逻辑与界面分离便于深入学习GUI编程和MySQL数据库交互。资源包共61个文件涵盖18个Python源码文件、8个UI界面文件、3个SQL数据库脚本、2份PDF课程设计文档另附E-R图、功能结构图等设计素材整体大小仅8.28MB目录结构清晰易检索。已有209人学习下载凭借完整的设计文档和可直接运行的代码可显著节省课设开发时间同时为理解软件分层架构与数据库落地应用提供真实案例。1. 课设标题里藏着三重门槛GUI、数据库、可运行把“基于 PythonPyQt5MySQL 的酒店管理系统”这个课设标题拆开看真正卡住人的从来不是 Python 语法而是三件事要同时交差界面能点、数据能查、交上去的压缩包解压后能跑。前两件靠一个下午就能打通第三件才是课程设计的高分分水岭——开发机上能启动的程序换到机房电脑往往崩在 MySQL 连接、PyQt5 插件或编码上。下面按课设交付顺序写先用 MySQL 把实体和存储过程钉死再用 PyQt5 搭出前台窗口最后补上“可直接运行”的两个落地细节顺带覆盖课程设计报告里最常被追问的事务、时序图和界面截图从哪来。适合正在做课设的学生也适合帮人审课设代码的工程师快速定位问题。2. 选型定了先别写代码把 MySQL 表结构与存储过程钉死2.1 房间、客户、订单三张核心表的实体关系酒店管理系统做课程设计规模不需要上微服务但关系型数据库该有的味道要出来。常见做法是围绕“房间-客户-订单”三条主线建表不要一上来就建十几张表把外键绕成蜘蛛网。核心关系是一个客户可以有多张订单一张订单对着一间房房间状态由订单驱动变化。会员信息可以单独拆 guest 表也可以并入客户表课设建议拆开因为结账、报表都要按客户维度统计。这里有个建模选择房间的状态是“存在性字段”还是“派生字段”。把rooms.status做成独立字段查询房态最方便但必须靠代码或存储过程保证它与订单状态一致如果完全由订单表实时计算房态就少一个字段但每次刷新房态都要 JOIN。课设阶段选前者配合事务把两处更新绑在同一提交里实现上最直观答辩时也能讲清楚状态一致性怎么保证。2.2 可直接执行的建库建表 SQL约束与索引一次写对下面是这套系统最常见的一套建库脚本按课程设计默认场景设计单机开发root 账号InnoDB 引擎。注意字符集MySQL 8.0 默认 utf8mb4但建库时显式指定更稳避免 Windows 下控制台导入 SQL 文件时注释中文变乱码。CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hotel_db; CREATE TABLE rooms ( room_id INT AUTO_INCREMENT PRIMARY KEY, room_no VARCHAR(10) NOT NULL UNIQUE COMMENT 房号, room_type ENUM(单人间,双人间,家庭房) NOT NULL, price_per_night DECIMAL(10,2) NOT NULL DEFAULT 0, status ENUM(空闲,入住,维修) NOT NULL DEFAULT 空闲, remark VARCHAR(255) DEFAULT ) ENGINEInnoDB; CREATE TABLE guests ( guest_id INT AUTO_INCREMENT PRIMARY KEY, guest_name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE, phone VARCHAR(20) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, room_id INT NOT NULL, guest_id INT NOT NULL, check_in DATETIME NOT NULL, check_out DATETIME DEFAULT NULL, nights INT NOT NULL DEFAULT 1, total_amount DECIMAL(10,2) DEFAULT 0, status ENUM(入住,退房,取消) NOT NULL DEFAULT 入住, KEY idx_status (status), CONSTRAINT fk_order_room FOREIGN KEY (room_id) REFERENCES rooms(room_id), CONSTRAINT fk_order_guest FOREIGN KEY (guest_id) REFERENCES guests(guest_id) ) ENGINEInnoDB;这段 SQL 有三个值得在报告里展开的设计点。第一rooms.status用 ENUM 限死取值范围界面下拉框可以照着枚举填查询统计时不会出现“可住”“空房”“空闲”三种写法并存。第二orders.check_out允许 NULL退房前不填这是“当前在住订单”的自然表达如果建表就 NOT NULL入住登记时反而要先编一个假的退房时间。第三外键写在订单表上约束住“订单必须关联存在的房间和客户”虽然代码里照样要判断但数据库这层兜底能拦住批量导入带来的脏数据。字段设计要点课设常见错误rooms.status用 ENUM 限死枚举值手输“可住/空房/满房”统计口径混乱orders.check_out允许 NULL退房前不填NOT NULL 导致入住登记要先编造退房时间orders.order_id自增主键与业务无关拿 room_id 当订单主键一间房只能住一次guests.id_card建 UNIQUE 唯一约束同一身份证重复开单无法拦截字段设计的常见误区顺带提一下拿 room_id 当订单主键是最典型的错一间房多次入住时主键冲突直接崩掉电话号码只做普通索引即可身份证号才值得建 UNIQUE。order_no留成字符串是给“显示用单号”留余地比如20250601-001这种格式方便前台对单。2.3 用 MySQL 存储过程收拢“退房结算”业务规则退房结算是酒店管理系统的核心业务课设报告里最常画的就是这张图。结算规则一句话房费等于实际住宿天数乘以房价不足一天按一天算。把这句规则写进 MySQL 存储过程而不是散落在按钮的点击事件里答辩时被问“业务规则在哪一层”就能直接回答。DELIMITER // CREATE PROCEDURE checkout_room( IN p_order_id INT, IN p_leave_time DATETIME, OUT p_amount DECIMAL(10,2) ) BEGIN DECLARE v_room_id INT; DECLARE v_check_in DATETIME; DECLARE v_price DECIMAL(10,2); SELECT room_id, check_in, price_per_night INTO v_room_id, v_check_in, v_price FROM orders JOIN rooms USING (room_id) WHERE order_id p_order_id AND status 入住 FOR UPDATE; IF v_room_id IS NULL THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT order not found or already checked out; END IF; SET p_amount GREATEST(DATEDIFF(p_leave_time, v_check_in), 1) * v_price; UPDATE orders SET status退房, check_outp_leave_time, total_amountp_amount WHERE order_id p_order_id; UPDATE rooms SET status空闲 WHERE room_id v_room_id; END // DELIMITER ;参数设计上把退房时间p_leave_time作为入参而不是直接调 NOW()好处是补单、改单时能传历史时间单纯演示退房结算时传NOW()也行。FOR UPDATE锁住订单行防止两个窗口同时对这个订单点退房。GREATEST(..., 1)处理“当天入住当天退”的边界否则DATEDIFF会算出 0 元账单。存储过程的验证方式很简单导完脚本后执行CALL checkout_room(1, NOW(), amt); SELECT amt;看到房间状态同时被改掉就说明逻辑通。3. PyQt5 GUI 编程骨架连接层、主窗口与信号槽3.1 为什么用 pymysql 封装单例连接而不是每个按钮各连一次Python 连 MySQL 的方案很多SQLAlchemy 功能强但课设没必要pymysql 是纯 Python 实现装上就能用不用像 mysql-connector 那样配一堆驱动。常见做法是不引入 ORM直接用 SQL理由很现实答辩抽问存储过程、事务、索引时用裸 SQL 回答起来最不心虚。连接不建议每个按钮创建一个演示时点一次卡一次。用一个类级别的单例包住连接项目里所有窗口共享这一条连接即可。# db.py —— 课程设计级的数据库连接封装 import pymysql class DB: _conn None classmethod def connect(cls, hostlocalhost, port3306, userroot, password123456, databasehotel_db): cls._conn pymysql.connect( hosthost, portport, useruser, passwordpassword, databasedatabase, charsetutf8mb4, autocommitFalse, cursorclasspymysql.cursors.DictCursor ) return cls._conn classmethod def query(cls, sql, argsNone): cur cls._conn.cursor() cur.execute(sql, args or ()) rows cur.fetchall() cur.close() return rows classmethod def execute(cls, sql, argsNone): cur cls._conn.cursor() affected cur.execute(sql, args or ()) cur.close() cls._conn.commit() return affectedcursorclass用DictCursor查出来的行是字典row[room_no]比row[0]可读性强得多也避免 SELECT 字段顺序一变代码就跟着改。autocommitFalse是刻意的入住登记这类操作需要两个 UPDATE 同生共死靠手动 COMMIT 控制事务边界。查询走query写操作用execute事务内需要手写 COMMIT/ROLLBACK 时不走execute直接拿DB._conn操作这也是课设代码里最常见的分层习惯。3.2 主窗口沿用 QMainWindow房态视图用 QTableWidget主窗口布局课设常见做法是左边 QListWidget 做功能导航中间 QTableWidget 放房态或订单明细底部 QStatusBar 显示数据库连接状态和提示。QTableWidget 比 QTableView 适合课设因为数据量小不需要自定义 Model/View 就有现成的填格能力要注意它默认允许双击编辑管理员界面里最好关掉避免演示时手滑把单价改了。控件适合场景课设建议QTableWidget数据量小、需要直接填格房态列表、订单列表首选用它QTableView QSqlTableModel数据量大、需要排序过滤课设没必要引入徒增复杂度QTreeWidget树形导航、分类展示放左侧功能菜单顺手# main_window.py —— 主窗口骨架 from PyQt5.QtWidgets import (QMainWindow, QWidget, QTableWidget, QTableWidgetItem, QPushButton, QVBoxLayout) from db import DB class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(酒店管理系统 - 前台) self.resize(860, 540) self.table QTableWidget(0, 4) self.table.setHorizontalHeaderLabels([房号, 类型, 单价, 状态]) self.table.setEditTriggers(QTableWidget.NoEditTriggers) btn QPushButton(刷新房态) btn.clicked.connect(self.refresh_rooms) box QVBoxLayout() box.addWidget(self.table) box.addWidget(btn) center QWidget(self) center.setLayout(box) self.setCentralWidget(center) def refresh_rooms(self): rows DB.query( SELECT room_no, room_type, price_per_night, status FROM rooms ORDER BY room_no) self.table.setRowCount(len(rows)) for i, row in enumerate(rows): values [row[room_no], row[room_type], f{row[price_per_night]:.2f}, row[status]] for j, value in enumerate(values): self.table.setItem(i, j, QTableWidgetItem(value))setRowCount(len(rows))配合循环setItem是性价比最高的填表方式另一种insertRow逐行插入会在数据量上来后明显卡顿而且行数多了容易漏删。NoEditTriggers保证表格只读所有修改都走弹窗表单数据校验集中在同一处。单价转成f{...:.2f}再显示能顺手解决浮点数显示成100.0的问题。3.3 信号槽驱动的“查询-填充”最小闭环PyQt5 里按钮事件、窗口关闭、下拉选择都是信号槽函数就是普通 Python 方法。上面的btn.clicked.connect(self.refresh_rooms)就是最小闭环点击按钮触发查询查询结果填回表格。这个模式后面所有功能都在复用区别只是 SQL 和控件不同。运行时先连数据库再弹主界面是另一个值得养成的顺序习惯——MySQL 没启动时程序要有明确报错而不是黑框闪退。if __name__ __main__: import sys from PyQt5.QtWidgets import QApplication, QMessageBox app QApplication(sys.argv) try: DB.connect() except Exception as exc: QMessageBox.critical(None, 连接失败, f请检查 MySQL 是否启动:\n{exc}) sys.exit(1) win MainWindow() win.show() sys.exit(app.exec_())这段启动代码把“数据库不可用”从“程序崩溃”变成了“弹窗提示”答辩演示时即使忘开 MySQL 也不会当场翻车。QMessageBox.critical属于模态对话框把str(exc)接进去后机房电脑连不上库时不用开调试器就能判断是密码错还是服务没启。4. 入住登记与退房结算PyQt5 界面 MySQL 事务的联动实现4.1 入住登记用 FOR UPDATE 锁行房间与订单同事务提交入住登记是酒店管理系统的并发敏感点同一间房的最后一个空闲状态可能被两个前台窗口同时抢到。课设虽然不会真有并发压力但事务写法必须正确报告里的 E-R 图、时序图、事务说明都指望这段代码。实现上先查房间状态并加排他锁确认空闲后插入订单、改房间状态最后一次性提交。from datetime import datetime def check_in(self, room_id, guest_id, nights): if nights 0: raise ValueError(入住天数必须大于 0) conn DB.connect() cur conn.cursor() try: cur.execute(SELECT status FROM rooms WHERE room_id%s FOR UPDATE, (room_id,)) row cur.fetchone() if row is None or row[status] ! 空闲: raise RuntimeError(该房间不可入住) order_no f{datetime.now():%Y%m%d%H%M%S}-{room_id} cur.execute( INSERT INTO orders (order_no, room_id, guest_id, check_in, nights, status) VALUES (%s, %s, %s, NOW(), %s, 入住), (order_no, room_id, guest_id, nights)) cur.execute(UPDATE rooms SET status%s WHERE room_id%s, (入住, room_id)) conn.commit() except Exception: conn.rollback() raise finally: cur.close()FOR UPDATE的意义是在这事务提交前其他连接执行同一条 SELECT 会被阻塞等到提交后才能读到新状态。order_no拼上时间戳和房间号保证同一秒对同一房间重复提交也不会撞唯一索引。异常时rollback是关键否则会出现“房间状态已改、订单没插入”的中间态这也是课程设计报告里“事务一致性”一节最直接的证据。4.2 退房结算用 callproc 调用存储过程并回读金额退房按钮要做的事从当前订单列表选中一条在住订单调用checkout_room把返回金额显示出来同时刷新房态表。点按钮之前要把订单状态筛选好只让“入住”状态的订单进入退房流程避免重复退房。def checkout_selected(self, order_id): conn DB.connect() with conn.cursor() as cur: cur.callproc(checkout_room, (order_id, datetime.now(), -1)) cur.execute(SELECT _checkout_room_2 AS amount) row cur.fetchone() conn.commit() if row: QMessageBox.information(self, 退房成功, f应收金额{row[amount]} 元) self.refresh_rooms() self.refresh_order_table()pymysql 调用存储过程时OUT 参数不直接返回而是存到用户变量里命名规则是_存储过程名_参数序号。这里checkout_room的第三个参数序号是 2所以读出_checkout_room_2。先SELECT再commit确保金额是在事务内读的。如果存储过程走的是SIGNAL报错Python 这边会抛异常界面上用 try 包一层弹窗即可不必让程序退出。4.3 QComboBox 联动过滤房型刷新动作收拢成一个方法房态界面光有“刷新”按钮不够课设演示时评审大概率会点一下房型下拉框看筛选是否生效。常见做法是房型下拉框的选项写死三类房型加上“全部”选择变化时重新查房间表并按类型过滤。def on_type_changed(self): room_type self.combo_type.currentText() if room_type 全部: rows DB.query(SELECT room_no, room_type, price_per_night, status FROM rooms ORDER BY room_no) else: rows DB.query(SELECT room_no, room_type, price_per_night, status FROM rooms WHERE room_type%s ORDER BY room_no, (room_type,)) self.fill_table(rows)下拉框的currentTextChanged信号连接这个函数每次选择变化自动刷新表格这就是 GUI 编程里典型的联动。容易漏掉的一点入住、退房成功后要同时刷新房间表和订单表而不是只刷其中一个。房态统计区如果显示“当前在住 X 间”也应在这个时间点一起重算。把三个刷新动作收进一个refresh_all()方法是避免“界面和数据库状态不一致”最笨也最有效的办法。动作订单状态房间状态需要刷新入住成功入住入住房间表、订单表、统计退房成功退房空闲房间表、订单表、统计取消订单取消空闲房间表、订单表5. 报表与可视化把查询结果渲染进 PyQt5 界面5.1 用 QTextBrowser 的 setHtml 直接渲染营收报表课程设计报告要截图如果报表做成打印样式的 HTML截图效果远好于 QTableView 里挤成一行行的数据。QTextBrowser 支持富文本和网页样式不需要引入 WebEngine 就能显示表格。热门检索里总有人问 PyQt5 显示 HTML 怎么处理实际上setHtml对课设报表够用且打包体积远小于 QWebEngineView。def show_income_report(self): rows DB.query( SELECT DATE(check_out) AS d, COUNT(*) AS cnt, SUM(total_amount) AS amt FROM orders WHERE status退房 GROUP BY DATE(check_out) ORDER BY d DESC LIMIT 30) html_parts [h3近 30 日退房营收/h3 table border1 cellspacing0 cellpadding6 width100% trth日期/thth退房单数/thth营收(元)/th/tr] for row in rows: html_parts.append( ftrtd{row[d]}/tdtd{row[cnt]}/td ftd{float(row[amt]):,.2f}/td/tr) html_parts.append(/table) self.text_report.setHtml(.join(html_parts))GROUP BY DATE(check_out)把退房时间按天聚合注意营收按退房时间而不是入住时间统计这是业务口径报告里可以专门写一句。金额格式化用:,.2f会带千分位逗号比裸数字更像正式报表。QTextBrowser的默认字体在 Windows 下偏小可以加一行self.text_report.document().setDefaultStyleSheet(body{font-size:14px})统一调。5.2 把 matplotlib 图表嵌进 PyQt5 画布房态占比一眼看清GUI 编程的加分项是带图。matplotlib 有专门的 Qt5 后端把 Figure 塞进FigureCanvasQTAgg就能和 QWidget 一起布局。房态占比饼图是最不容易出错的选型因为只查一个分组统计。from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from matplotlib.figure import Figure def refresh_pie(self): rows DB.query(SELECT status, COUNT(*) AS c FROM rooms GROUP BY status) if not rows: return fig Figure(figsize(4, 3), dpi100) ax fig.add_subplot(111) labels [r[status] for r in rows] values [r[c] for r in rows] ax.pie(values, labelslabels, autopct%1.1f%%) self._pie_canvas FigureCanvas(fig) self.layout_pie.addWidget(self._pie_canvas)FigureCanvas是 QWidget 子类能直接 addWidget刷新前要把旧画布从布局里摘掉否则多张饼图叠在一起。dpi100是清晰度和打包体积的平衡点。这里不用每次重建 Figure 也行但要清空坐标轴重画课设代码怎么直接怎么来重建 Figure 反而简单。5.3 报表导出 CSV 与打印的一条可行路径评审要“导出报表”功能时别用 xlwt 老库直接用csv标准库最稳。编码指定utf-8-sigWindows 上用 Excel 打开才不会乱码。import csv with open(income.csv, w, newline, encodingutf-8-sig) as f: writer csv.writer(f) writer.writerow([日期, 退房单数, 营收]) for row in rows: writer.writerow([row[d], row[cnt], float(row[amt])])utf-8-sig会写 BOM 头Excel 才能识别 UTF-8newline防止 Windows 下每行多一个空行。如果还要打印QTextBrowser 从 QTextEdit 继承了print_()接一个 QPrinter 就能直接走系统打印对话框报表截图和打印可以共用一份 HTML 数据。6. 让“可直接运行”成立PyInstaller 打包与验证清单6.1 打包参数、资源路径与 PyQt5 插件坑“可直接运行”五个字在标题里是最重的一条承诺。开发机python main.py能跑不算数交付物要么是main.py requirements.txt要么是 exe。课设通常两者都交源码包给老师查exe 用于演示。打包用 PyInstallerWindows 上命令是pip install pyinstaller pymysql PyQt5 pyinstaller -F -w -n hotel_system main.py-F打单文件-w去掉控制台黑窗-n指定程序名。资源文件要用--add-data带进包Windows 分隔符是分号Linux 是冒号。程序里读资源路径时用sys._MEIPASS判断是否在打包环境import sys, os def resource_path(name): base getattr(sys, _MEIPASS, os.path.dirname(os.path.abspath(__file__))) return os.path.join(base, name)打包常见的坑多进程或多线程下-F单文件会导致临时目录被提前清理课设单窗口场景基本不受影响matplotlib 打包体积会多十几 MB报告里写明即可。PyQt5 偶发启动时报 Qt platform plugin 找不到常见做法是把PyQt5-Qt5固定回 5.15.2。6.2 交付前对照清单自查三行命令验证数据库与打包作为帮人审课设代码的人最常看到的问题是zip 解压后README 说“导入 hotel_db.sql”但源文件里根本没有 sql或者密码写死成自己机器上的 root 密码换台机器连不上。交付前至少跑一遍下面三条命令mysql -u root -p sql/init.sql python -c from db import DB; DB.connect(); print(DB.query(SELECT COUNT(*) c FROM rooms)) pyinstaller -F -w --add-data resources;resources main.py第一条重建数据库第二条验证 pymysql 连接和查询第三条出 exe。DB.connect()的密码参数在课设里写死可以理解但要把密码提成config.py里的常量README 里用一行话说清怎么改。提示exe 打包好后在干净的 Windows 虚拟机上验证一次比在开发机上点十次都有说服力。最后在无 Python 环境的干净机器上双击 exe能看到主窗口和房态数据才算真正达成“可直接运行”。本文还有配套的精品资源点击获取
