简介:本资源是一套面向高校数据库课程学习者的Python酒店管理系统完整实践项目,适用于期末大作业、课程设计等场景,特别适合数据库原理与Python开发初学者快速上手并获得高分。项目采用MySQL+PyQt5技术栈,涵盖用户登录、客房管理、员工信息、报表生成等核心功能模块,代码结构清晰、注释详尽,配套E-R图、功能结构图、系统设计报告及课程设计要求文档,便于理解数据库建模与前后端协同逻辑。压缩包共61个文件,含18个核心Python源码(如Main.py、room.py、staff.py)、8个PyQt界面文件(.ui)、3个SQL脚本(含hotelManagement.sql建库建表语句)、2份PDF报告及多张设计示意图,整体8.3MB,轻量易部署。目前已有281人学习下载,读者可直接运行调试、复现高分实现路径,并参考导师认可的98分项目范式完成自身课程任务。
1. 这不是又一个“酒店管理系统Demo”:它是一份能直接交作业、能现场答辩、能改出三个变体的数据库课程实战包
你是不是也经历过——花三天搭了个Flask+SQLite的“酒店前台”,结果导师一句“ER图没标基数”就打回重做;或者PyQt界面调得挺顺,一跑SQL就报no such table: room,翻遍hotelManagement.sql才发现建表语句里staff_id少了个下划线;更别提文档里写“系统支持多角色登录”,实际代码里LoginUI.ui压根没连权限校验逻辑……这份基于Python酒店管理系统的资源,恰恰卡在课程设计最痛的几个节点上:它不是玩具项目,而是某高校《数据库应用》课真实交付的98分高分作业,所有模块都经得起现场提问——E-R图用draw.io导出高清JPG,功能结构图标注了三层数据流走向,SQL脚本按CREATE TABLE → INSERT INIT DATA → CREATE INDEX分段注释,连ModifyPwd.py里密码加密都明确写了hashlib.pbkdf2_hmac('sha256', ...)而非简单md5()。它适合两类人:一类是下周就要交终稿、想避开DDL语法错误和外键约束翻车的新手;另一类是已写完基础版、需要快速叠加“入住统计报表”或“员工排班视图”等加分项的进阶者。核心价值不在“能跑”,而在“每行代码都能讲清为什么这么写”。
2. 从解压到登录:五步完成本地部署,关键在SQL初始化与UI资源路径绑定
这个项目本质是“PyQt5 + SQLite3 + 面向过程逻辑”的轻量组合,没有Docker、不依赖云服务,但对文件路径和数据库状态极其敏感。很多同学卡在第一步——双击Main.py报错ModuleNotFoundError: No module named 'PyQt5',或启动后界面空白。这不是代码问题,而是环境链断裂。下面步骤必须严格按顺序执行,跳过任意一步都可能触发后续玄学报错。
2.1 环境准备:只装两个包,但版本有硬性要求
提示:不要用
pip install pyqt5无脑安装!该资源实测兼容PyQt5.15.0~5.15.9,高版本(如6.x)会因QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)移除导致界面缩放异常。
# 创建干净虚拟环境(避免污染全局pip) python -m venv hotel_env hotel_env\Scripts\activate # Windows # 或 source hotel_env/bin/activate # macOS/Linux # 安装指定版本PyQt5(关键!) pip install PyQt5==5.15.9 # 安装数据库驱动(SQLite3内置,无需额外安装pysqlite3) pip install --upgrade pip参数说明:PyQt5==5.15.9是硬性约束。某次我帮A同学调试时发现,他用pip install pyqt5默认装了6.4.0,结果MainUI.ui加载后所有按钮文字变成方块——查源码发现uic.loadUi()在6.x中对.ui文件编码解析逻辑变更,而本项目.ui文件由Qt Designer 5.15生成,存在二进制头标识不兼容。降级后立即恢复。
2.2 数据库初始化:必须手动执行SQL,不能依赖程序自动建表
项目中的hotelManagement.sql不是示例脚本,而是完整生产级建表语句。注意:Main.py里没有create_all_tables_if_not_exists()逻辑,所有表必须提前导入。常见错误是直接双击运行Main.py,程序因找不到room表而崩溃。
# 步骤1:用命令行进入项目根目录(含hotelManagement.sql的同级目录) cd /path/to/your/unzipped/folder # 步骤2:启动SQLite3并导入SQL(Windows/macOS/Linux通用) sqlite3 hotel.db < hotelManagement.sql # 验证是否成功(应返回3张表名) sqlite3 hotel.db ".tables" # 输出示例:room staff user逻辑说明:hotelManagement.sql包含三部分:
CREATE TABLE语句(含PRIMARY KEY,FOREIGN KEY(room_id) REFERENCES room(id)等完整约束)INSERT INTO初始数据(如预置3个房间、2个员工账号)CREATE INDEX加速查询(如CREATE INDEX idx_room_status ON room(status);)
若跳过此步,room.py中get_available_rooms()函数执行SELECT * FROM room WHERE status='空闲'时必然报错。这是课程设计中最常被忽略的“数据库先行”原则。
2.3 UI资源路径修正:PyQt5加载.ui文件的绝对路径陷阱
项目中所有.ui文件(如LoginUI.ui,MainUI.ui)在代码里通过uic.loadUi("LoginUI.ui")加载。但PyQt5默认从当前工作目录读取,而非Main.py所在目录。若你在桌面双击Main.py,工作目录是C:\Users\XXX\Desktop,自然找不到LoginUI.ui。
# 修改Main.py开头的UI加载逻辑(原文件第12行附近) # 原始错误写法(删掉!): # ui = uic.loadUi("LoginUI.ui") # 替换为以下健壮写法: import os from pathlib import Path # 获取Main.py所在目录(绝对路径) BASE_DIR = Path(__file__).parent.absolute() # 动态拼接UI文件路径 login_ui_path = os.path.join(BASE_DIR, "LoginUI.ui") ui = uic.loadUi(login_ui_path)参数说明:Path(__file__).parent.absolute()获取Main.py的父目录(即解压后的根文件夹),os.path.join()确保跨平台路径分隔符正确。某次某导师抽查代码,发现学生直接写死"D:/project/LoginUI.ui",换台电脑就崩——这种硬编码在课程答辩中属于典型扣分点。
2.4 启动主程序:绕过IDE缓存,用终端直启验证
不要用PyCharm点击绿色三角形运行!.idea文件夹里的缓存可能导致__pycache__加载旧字节码,出现“修改了staff.py但界面没变化”的假象。
# 确保在项目根目录下(含Main.py的目录) # 激活虚拟环境后执行 python Main.py现象验证:
- 首次启动弹出
LoginUI.ui登录窗口 - 输入默认账号
admin/密码123456(见hotelManagement.sql中INSERT INTO user语句) - 成功跳转至
MainUI.ui主界面,顶部显示“欢迎,管理员” - 点击“房间管理”标签页,列表应显示3条预置房间数据
若卡在登录页,检查LoginUI.ui中登录按钮的objectName是否为loginBtn(原项目是),因为Main.py里事件绑定写死self.loginBtn.clicked.connect(self.handle_login)。
2.5 快速验证数据联动:用SQL命令反向检验业务逻辑
登录后,在主界面操作“新增房间”,填入房号1004、类型豪华单人间、价格380,点击保存。此时不要只信界面提示“添加成功”,立刻用SQLite验证:
# 在项目根目录执行 sqlite3 hotel.db "SELECT * FROM room WHERE id=1004;" # 应返回:1004|豪华单人间|380|空闲|NULL # 再测试关联删除:删除员工ID=1的记录 sqlite3 hotel.db "DELETE FROM staff WHERE id=1;" # 检查是否级联删除其管理的房间?(原设计未设ON DELETE CASCADE,应保留房间记录) sqlite3 hotel.db "SELECT * FROM room WHERE manager_id=1;" # 应返回空,证明`manager_id`字段未被误设为外键强制约束为什么这么做:课程设计评分标准里,“数据完整性”占20%。导师常现场执行SELECT查数据一致性,而非只看界面。这个步骤帮你提前暴露room.py中add_room()函数是否漏写conn.commit(),或staff.py里delete_staff()是否忘了更新room.manager_id为NULL。
3. 代码层深度拆解:四个核心模块如何用纯Python实现数据库CRUD,避开ORM幻觉
很多同学一上来就想用SQLAlchemy,结果在课程答辩被问“session.add()底层怎么发SQL?”答不上来。本项目坚持用sqlite3原生API,每个模块都暴露SQL执行细节,这才是数据库课要考察的本质能力。我们逐个击破。
3.1room.py:房间管理模块的事务边界设计
该模块处理房间增删改查,关键在update_room_status()函数——它模拟真实酒店“入住/退房”场景,必须保证状态变更与时间戳同步。
# room.py 第45行起(已加关键注释) def update_room_status(room_id, new_status, check_time=None): """ 更新房间状态,同时记录操作时间 :param room_id: 房间ID(整数) :param new_status: 新状态(字符串,如'入住中'/'空闲') :param check_time: 操作时间(字符串格式'YYYY-MM-DD HH:MM:SS',为空则用当前时间) """ conn = sqlite3.connect("hotel.db") cursor = conn.cursor() # 【重点】显式开启事务,防止状态更新与时间戳不同步 conn.execute("BEGIN TRANSACTION") try: # 步骤1:更新状态 cursor.execute( "UPDATE room SET status = ? WHERE id = ?", (new_status, room_id) ) # 步骤2:更新最后操作时间(注意:这里用datetime.now()而非SQL的datetime('now')) # 原因:课程要求日志精确到毫秒,SQLite datetime('now')只到秒级 if check_time is None: from datetime import datetime check_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cursor.execute( "UPDATE room SET last_update = ? WHERE id = ?", (check_time, room_id) ) # 【关键】提交事务,两步操作原子性保证 conn.commit() return True except sqlite3.Error as e: # 回滚事务,避免部分更新 conn.rollback() print(f"更新房间状态失败:{e}") return False finally: conn.close()参数说明:
BEGIN TRANSACTION显式声明事务边界,这是课程设计高分关键点。某次某高校答辩题:“如果更新状态成功但时间戳失败,数据库会怎样?”答案必须是“用ROLLBACK回滚,保持数据一致”。datetime.now().strftime()而非datetime('now'),因课程报告要求“操作日志精度达秒级”,而SQLite内置函数不支持毫秒。conn.close()放在finally块,确保连接释放,避免too many connections错误。
3.2staff.py:员工管理中的外键约束实践
员工表staff与房间表room通过manager_id关联,但项目未设置ON DELETE CASCADE,而是用Python逻辑处理级联。这符合课程要求“理解外键行为,而非依赖数据库自动处理”。
# staff.py 第78行起 def delete_staff(staff_id): """ 删除员工,同时将其管理的房间manager_id置为NULL :param staff_id: 员工ID """ conn = sqlite3.connect("hotel.db") cursor = conn.cursor() try: # 步骤1:先解除房间表对该员工的引用(关键!) cursor.execute( "UPDATE room SET manager_id = NULL WHERE manager_id = ?", (staff_id,) ) # 步骤2:再删除员工记录 cursor.execute( "DELETE FROM staff WHERE id = ?", (staff_id,) ) conn.commit() return True except sqlite3.Error as e: conn.rollback() print(f"删除员工失败:{e}") return False finally: conn.close()为什么这样设计:
- 若直接
DELETE FROM staff,因room.manager_id是外键且无ON DELETE SET NULL,SQLite会报FOREIGN KEY constraint failed。 - 课程设计报告中明确要求“描述外键约束的处理策略”,此处用Python显式置空,比数据库级联更体现设计思考。
- 注意
UPDATE在DELETE之前,顺序颠倒会导致外键冲突。
3.3report.py:统计报表模块的SQL聚合技巧
report.py生成“各房型入住率统计”,不用Pandas,纯用SQLGROUP BY+COUNT,直击数据库课核心考点。
# report.py 第22行起 def get_room_type_occupancy(): """ 统计各房型入住率:(入住中房间数 / 该房型总房间数) * 100 返回列表:[{'type': '标准间', 'rate': 66.67}, ...] """ conn = sqlite3.connect("hotel.db") cursor = conn.cursor() # 【核心SQL】用子查询计算分母(各房型总数),JOIN计算分子(各房型入住中数) sql = """ SELECT t1.type, ROUND(CAST(t2.occupied_count AS REAL) * 100 / t1.total_count, 2) AS rate FROM ( -- 子查询1:各房型总房间数 SELECT type, COUNT(*) AS total_count FROM room GROUP BY type ) AS t1 LEFT JOIN ( -- 子查询2:各房型入住中房间数 SELECT type, COUNT(*) AS occupied_count FROM room WHERE status = '入住中' GROUP BY type ) AS t2 ON t1.type = t2.type ORDER BY rate DESC """ cursor.execute(sql) results = cursor.fetchall() conn.close() # 转为字典列表,适配UI显示 return [{"type": row[0], "rate": row[1]} for row in results]参数说明:
CAST(... AS REAL)确保除法结果为浮点数,避免SQLite整数除法截断(如2/3=0)。ROUND(..., 2)保留两位小数,符合报表展示规范。LEFT JOIN保证即使某房型无入住中房间,仍显示rate=0.0,而非被过滤掉。- 课程设计评分细则中,“复杂查询能力”项明确要求“使用子查询或JOIN完成多表统计”。
3.4ModifyPwd.py:密码安全的最小可行实现
课程未要求OAuth或JWT,但明确要求“密码不可明文存储”。本项目用pbkdf2_hmac加盐哈希,比MD5/SHA1更符合现代安全观。
# ModifyPwd.py 第33行起 import hashlib import os def hash_password(password, salt=None): """ 使用PBKDF2-HMAC-SHA256加盐哈希密码 :param password: 明文密码(字符串) :param salt: 盐值(bytes,为空则生成新盐) :return: (hash_bytes, salt_bytes) """ if salt is None: salt = os.urandom(32) # 生成32字节随机盐 # 100000次迭代,提高暴力破解成本 key = hashlib.pbkdf2_hmac( 'sha256', password.encode('utf-8'), salt, 100000 ) return key, salt def verify_password(stored_hash, stored_salt, input_password): """ 验证密码:对输入密码用相同盐值哈希,比对结果 :param stored_hash: 数据库存储的哈希值(bytes) :param stored_salt: 数据库存储的盐值(bytes) :param input_password: 用户输入的明文密码 :return: bool """ key, _ = hash_password(input_password, stored_salt) return key == stored_hash关键参数:
100000次迭代是平衡安全与性能的合理值(课程设计报告中需说明选择依据)。os.urandom(32)生成密码学安全随机盐,避免random模块的可预测性。- 密码验证时必须复用原盐值,否则永远返回
False——这是新手最常踩的坑。
4. 避坑指南:课程设计答辩高频翻车点与血泪解决方案(附真实报错日志)
别等答辩当天被导师一句“你这外键怎么没生效?”问懵。以下是我在帮十多位同学调试该项目时,总结出的5个最高频、最致命的翻车点,每一条都对应真实报错日志和可复制的修复命令。
4.1 现象:启动Main.py报错AttributeError: 'NoneType' object has no attribute 'clicked'
原因:LoginUI.ui中登录按钮的objectName被误改为非loginBtn(如pushButton),而Main.py第89行硬编码self.loginBtn.clicked.connect(...)。Qt Designer默认命名不统一,导出.ui时可能重命名。
解决:
- 用文本编辑器打开
LoginUI.ui,搜索<widget class="QPushButton",找到<property name="objectName">标签 - 确保其值为
<string>loginBtn</string>,若为<string>pushButton</string>,则改为loginBtn - 保存后,在
Main.py中确认第89行是self.loginBtn.clicked.connect(self.handle_login)
提示:Qt Designer中右键按钮→“更改objectName”可直接修改,比手动改XML安全。
4.2 现象:登录后点击“房间管理”标签页,列表为空,控制台无报错
原因:room.py中get_all_rooms()函数连接的是hotel.db,但当前工作目录下不存在该文件,或文件名大小写错误(如Hotel.db)。SQLite不会报错,只会静默创建空库。
解决:
# 终端进入项目根目录,执行 ls -la | grep "hotel.db" # Linux/macOS # 或 dir | findstr "hotel.db" # Windows若无输出,说明hotel.db缺失。立即执行:
sqlite3 hotel.db < hotelManagement.sql验证:sqlite3 hotel.db "SELECT COUNT(*) FROM room;"应返回3。
4.3 现象:修改密码后,用新密码无法登录,旧密码仍可用
原因:ModifyPwd.py中hash_password()生成的哈希值存入数据库时,未将bytes类型转换为hex字符串。SQLite的BLOB字段存储bytes,但user表password字段是TEXT类型,直接存bytes会转成b'\x01\x02...'字符串,验证时比对失败。
解决:
修改ModifyPwd.py中密码更新逻辑(原第62行):
# 原错误写法(删掉!): # cursor.execute("UPDATE user SET password = ? WHERE id = ?", (hashed_pwd, user_id)) # 替换为: cursor.execute( "UPDATE user SET password = ? WHERE id = ?", (hashed_pwd.hex(), user_id) # .hex()转为十六进制字符串 )验证:sqlite3 hotel.db "SELECT password FROM user WHERE username='admin';"应返回64位十六进制字符串(如a1b2c3...)。
4.4 现象:在“员工管理”页新增员工,点击保存后报错sqlite3.IntegrityError: NOT NULL constraint failed: staff.phone
原因:staff.py中add_staff()函数插入语句未包含phone字段,但staff表定义中phone TEXT NOT NULL(见hotelManagement.sql)。课程设计要求字段非空,但UI未提供手机号输入框。
解决:
- 打开
staff.ui,用Qt Designer添加QLineEdit控件,objectName设为phoneInput - 修改
staff.py中add_staff()函数,提取self.phoneInput.text()并加入SQL:
cursor.execute( "INSERT INTO staff (name, gender, phone, hire_date) VALUES (?, ?, ?, ?)", (name, gender, phone, hire_date) )注意:hire_date字段在hotelManagement.sql中为TEXT,需传入'2023-01-01'格式字符串,不能传datetime对象。
4.5 现象:生成报表时,report.py报错sqlite3.OperationalError: no such column: t2.occupied_count
原因:SQL中子查询别名t2未在SELECT中定义occupied_count,或WHERE status = '入住中'的字符串引号为中文全角引号‘入住中’。
解决:
- 检查
report.py中SQL字符串,确保所有单引号为英文半角 - 确认子查询2的
SELECT包含COUNT(*) AS occupied_count - 手动执行SQL验证:
sqlite3 hotel.db "SELECT type, COUNT(*) AS occupied_count FROM room WHERE status = '入住中' GROUP BY type;"若报错,说明status字段值不是'入住中'(可能是'已入住'或'Occupied'),需查hotelManagement.sql中INSERT INTO room语句确认原始值。
5. 高分进阶技巧:三步改造出“入住趋势图”模块,让导师眼前一亮
课程设计终稿想拿95+,光跑通基础功能不够,得有“可演示的增值点”。我帮A同学在原项目上加了“近7天入住趋势折线图”,从开发到答辩只用了2小时,导师当场问“这个图表数据怎么来的?”,他指着report.py里新加的SQL回答,拿了满分。下面是可直接抄的三步法。
5.1 数据层:扩展room表,增加check_in_time字段记录入住时间
原hotelManagement.sql中room表无时间字段,需手动升级。这不是破坏原有设计,而是课程允许的“需求演进”。
-- 在项目根目录执行,为room表添加check_in_time字段 sqlite3 hotel.db "ALTER TABLE room ADD COLUMN check_in_time TEXT;"为什么加这个字段:
- 原设计只有
status(空闲/入住中),无法统计“哪天入住多少间” TEXT类型存'2023-10-01 14:30:00'格式,兼容SQLite日期函数- 不用
DATETIME类型,因课程要求“字段类型选择理由”,TEXT更易解释(避免julianday()等复杂函数)
5.2 逻辑层:在room.py中新增record_check_in()函数,绑定入住操作
当用户在“房间管理”页点击“入住”按钮时,不仅要改status,还要记时间。
# room.py 末尾新增函数 from datetime import datetime def record_check_in(room_id): """ 记录房间入住时间,仅当状态为'入住中'时生效 :param room_id: 房间ID """ conn = sqlite3.connect("hotel.db") cursor = conn.cursor() try: # 先检查当前状态是否为'入住中' cursor.execute("SELECT status FROM room WHERE id = ?", (room_id,)) result = cursor.fetchone() if result and result[0] == '入住中': # 记录当前时间 now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") cursor.execute( "UPDATE room SET check_in_time = ? WHERE id = ?", (now, room_id) ) conn.commit() return True else: print(f"房间{room_id}当前状态非'入住中',不记录入住时间") return False except sqlite3.Error as e: conn.rollback() print(f"记录入住时间失败:{e}") return False finally: conn.close()关键点:
- 加
if判断确保只在status='入住中'时记录,避免退房后误写时间 - 时间格式与
strftime("%Y-%m-%d %H:%M:%S")严格匹配,方便后续SQL按日分组
5.3 展示层:用Matplotlib嵌入PyQt5,生成动态趋势图
report.py中新增get_weekly_occupancy()函数,并在MainUI.ui中加QVBoxLayout容器放图表。
# report.py 新增函数 import matplotlib.pyplot as plt from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas from PyQt5.QtWidgets import QVBoxLayout, QWidget def get_weekly_occupancy(): """ 查询近7天每日入住房间数 返回:[(date_str, count), ...] 如 [('2023-10-01', 2), ('2023-10-02', 5)] """ conn = sqlite3.connect("hotel.db") cursor = conn.cursor() # SQLite日期函数:date('now', '-6 days')获取7天前日期 sql = """ SELECT SUBSTR(check_in_time, 1, 10) AS date_day, COUNT(*) AS count FROM room WHERE status = '入住中' AND check_in_time >= date('now', '-6 days') GROUP BY date_day ORDER BY date_day """ cursor.execute(sql) results = cursor.fetchall() conn.close() return results # 在MainUI.ui的“报表”标签页中,添加一个QWidget(objectName设为chartWidget) # 然后在Main.py中绑定图表(示例代码) def show_trend_chart(self): """在chartWidget中显示趋势图""" data = get_weekly_occupancy() if not data: print("暂无入住数据") return dates = [row[0] for row in data] counts = [row[1] for row in data] # 创建matplotlib figure fig, ax = plt.subplots(figsize=(8, 4)) ax.plot(dates, counts, marker='o', linewidth=2, markersize=6) ax.set_title("近7天入住趋势", fontsize=14) ax.set_xlabel("日期") ax.set_ylabel("入住房间数") ax.grid(True, alpha=0.3) # 嵌入PyQt5 canvas = FigureCanvas(fig) layout = QVBoxLayout(self.chartWidget) # chartWidget是UI中定义的容器 layout.addWidget(canvas) canvas.draw()参数说明:
SUBSTR(check_in_time, 1, 10)提取日期部分('2023-10-01 14:30:00'→'2023-10-01'),比date()函数更稳定date('now', '-6 days')是SQLite内置函数,无需Python处理,减少逻辑耦合FigureCanvasQTAgg是PyQt5官方推荐的Matplotlib嵌入方式,比QPixmap截图更专业
从那以后我每次帮同学改课程设计,都强制走一遍“加字段→写SQL→嵌图表”三步流程。不是为了炫技,而是让数据库课回归本质:数据怎么存、怎么查、怎么用。当你在答辩时,导师问“这个趋势图的数据源头在哪?”,你能指着room.py里record_check_in()说“每次入住操作都会写入时间戳,报表SQL直接聚合”,而不是支吾“呃…应该是后台算的…”,那种笃定感,就是高分的底气。希望帮到你。
本文还有配套的精品资源,点击获取