简介这套数据库课程设计资料包以宾馆管理系统为题面向需要完成数据库课程项目或学习Python/PyQt桌面开发的学生提供从数据库设计、业务逻辑到界面呈现的完整参考。压缩包共包含490个文件大小约14.06MB以316个py源码文件为主同时包含PyQt的ui布局文件、编译后的pyd/pyc模块、exe可执行文件、png/jpg界面素材以及sql数据库脚本、cfg/txt配置说明等基本覆盖课程设计所需的代码、界面、脚本与运行环境依赖。包中带有Python 3.7虚拟环境相关组件便于快速恢复依赖、运行或二次调试项目。目前已有948人学习下载适合准备数据库课程设计答辩、需要借鉴桌面管理端开发思路的同学使用通过宾馆房间、客户入住、预订结算等常见业务模块可系统理解表结构设计、界面交互与数据访问流程并基于现有工程继续扩展功能。整体目录组织清晰可按源码、界面、脚本、运行库分类查阅对课程实践和期末项目演示均有较高参考价值。1. 数据库课程设计做成宾馆管理系统为什么说这个课题是练增删改查最好的样例数据库课程设计年年有人选“宾馆管理系统”看起来老套实际上它把数据库课里最难讲清楚的部分全串起来了多张表之间的主外键关系、房间状态随预订和退房不断流转、入住和结账之间的金额计算、以及并发场景下同一间房被两个人同时预订的冲突。用 Python PyQt 做界面再用 SQLite 或 MySQL 承载数据是高校课设最稳妥的组合代码量可控演示效果好答辩时也有东西可讲。这套《数据库课程设计-宾馆管理系统.zip》提供的就是这样一个完整工程后端有建表 SQL前端有 PyQt 界面中间是增删改查和预订、退房、结账的完整流程。适合正在做课设、想快速跑通一个完整系统的学生也适合想看看别人怎么组织 DAO 层代码的开发者。这篇文章按我拆这个资源的顺序来写先讲表和关系再讲 PyQt 如何对接数据库最后把最容易翻车的地方逐个列出来。2. 先看懂系统数据表、关系与 PyQt 的模块结构拿到一个课设压缩包第一件事不是急着运行而是先把目录和 SQL 文件过一遍。宾馆管理系统的核心数据模型并不复杂但关系设计的好坏直接决定后面写 DAO 层顺手不顺手。2.1 从需求到表结构六张核心表与主外键关系这个项目里的表结构是典型的“客户—房间—订单”三件套外加辅助表。我展开说一下设计逻辑这对答辩很关键为什么必须有这几张表为什么某些字段要设成外键。表名核心字段作用说明guestguest_id, guest_name, phone, id_card客户信息身份证号要设唯一约束roomroom_id, room_type, price, status房间信息status 标记可预订/入住/打扫bookingbooking_id, guest_id, room_id, check_in_date, check_out_date预订记录核心业务表check_incheck_in_id, booking_id, room_id, actual_check_in_time实际入住记录与预订分离check_outcheck_out_id, check_in_id, total_amount, pay_method退房结账记录adminadmin_id, username, password_hash登录用户表关键设计点在于 booking 和 check_in 拆成两张表。很多课设会把“预订”和“入住”混在一张表里用一个状态字段区分这样写起来少一张表但到了结账统计时会很别扭预订时间、实际入住时间、实际退房时间挤在一起查询历史记录时没法区分“预订过没来”和“住了一晚”。拆开之后预订失败、预订未到、入住中、已退房四种状态都能通过表间关系表达清楚不需要在应用层维护一个极其脆弱的状态枚举。外键关系是booking 表通过 guest_id 引用 guest 表通过 room_id 引用 room 表check_in 通过 booking_id 引用 booking 表check_out 通过 check_in_id 引用 check_in 表。建议在 SQL 里显式声明外键而不是只靠应用层保证数据一致性。SQLite 默认外键约束是关闭的需要在连接后执行PRAGMA foreign_keys ON;这点很多人在课设答辩时会被问到后面避坑章节会细说。2.2 PyQt 界面模块划分与数据库连接方式这个资源的界面层是 PyQt5目录结构大致是主窗口文件、各个业务对话框、数据库访问模块、入口文件。不要把 UI 逻辑和数据库操作混在一起写这是这个项目最值得借鉴的地方。常见做法是把数据库访问封装在一个db_helper.py或dao.py模块里界面文件只负责拿数据和展示。每个业务实体对应一个 DAO 类比如GuestDAO、RoomDAO、BookingDAO。界面层的按钮事件调用 DAO 方法DAO 返回列表或字典界面再填充到表格控件中。数据库连接这里有个选择直接用 SQLite 文件还是配 MySQL。做课设演示SQLite 够用而且交作业时不用让对方装数据库服务但如果老师明确要求用 MySQL 或 SQL Server那就把连接字符串换一下SQL 语句基本不用改。下面给出 SQLite 方式的基础连接写法也是这个项目默认的方式import sqlite3 from contextlib import closing DB_PATH hotel.db def get_connection(): conn sqlite3.connect(DB_PATH) conn.row_factory sqlite3.Row conn.execute(PRAGMA foreign_keys ON) return conn这里两行关键的设置row_factory sqlite3.Row让查询结果可以通过字段名访问而不是只能按数字下标取界面代码可读性会好很多PRAGMA foreign_keys ON开启外键约束否则你在代码里删一个还有入住记录的客户SQLite 不会报错数据直接变成孤儿数据。这两行是整个连接代码里最容易被忽略但最重要的部分。3. 把增删改查写成可维护的 DAO 层连接、事务与参数绑定课设代码最怕的就是把 SQL 语句直接散落在按钮事件里几十行 return 嵌套看都看不完。可维护的写法是统一走 DAO 层每张表对应一个类每个类只暴露业务需要的方法。这一章我把这个项目里最核心的几个操作拆开讲。3.1 数据库连接与事务提交的取舍SQLite 默认是自动提交模式每次执行execute后自动 commit。这对查询没问题但对“预订房间”这种涉及多步写的操作就危险了先插入 booking 记录再修改 room 表状态两步之间如果第二步失败第一步已经提交数据就残了。这个项目里的做法是手动控制事务我强烈建议你在自己的课设里也这样做def create_booking(guest_id, room_id, check_in_date, check_out_date): booking_id None conn get_connection() try: conn.execute(BEGIN) cur conn.execute( INSERT INTO booking (guest_id, room_id, check_in_date, check_out_date, status) VALUES (?, ?, ?, ?, ?), (guest_id, room_id, check_in_date, check_out_date, reserved) ) booking_id cur.lastrowid conn.execute( UPDATE room SET status reserved WHERE room_id ? AND status available, (room_id,) ) conn.commit() except Exception: conn.rollback() raise finally: conn.close() return booking_id参数说明SQL 语句里用?占位不管用户输入什么传给execute的元组会经过驱动转义避免 SQL 注入。cur.lastrowid是 SQLite 生成的最近一次插入行的自增主键我们拿它当预订编号返回。整个流程先BEGIN再在结束时commit一旦中间任何一步抛异常就rollback房间状态和预订记录保持一致。为什么要把room.status available写进 UPDATE 条件里这是一种乐观锁的简易实现如果这间房已经被别人订走了status 不再是 available这条 UPDATE 影响行数为 0事务提交后我们检查影响行数就能发现冲突。这在单机课设里看起来没必要但答辩时老师几乎必问“并发预订怎么办”你答出这一句就能拉开差距。3.2 查询与统计模糊搜索与日期区间课程设计的演示环节一定会用到查询尤其是模糊搜索客户和按日期统计入住率。这个项目里提供了一个通用的查询方法把不同的查询条件拼成动态 SQL。这里要注意一个坑动态拼接 SQL 时条件部分不能直接字符串格式化值部分必须用参数占位。def search_guests(keyword, date_from, date_to): sql SELECT * FROM guest WHERE 1 1 params [] if keyword: sql AND (guest_name LIKE ? OR phone LIKE ?) like f%{keyword}% params.extend([like, like]) if date_from: sql AND create_date ? params.append(date_from) if date_to: sql AND create_date ? params.append(date_to) conn get_connection() with closing(conn.cursor()) as cur: cur.execute(sql, params) rows cur.fetchall() conn.close() return [dict(row) for row in rows]逻辑说明先写一个恒真的1 1后面每有一个条件就追加一段 SQL 和对应参数这样代码顺序清楚不需要单独维护一个“是否第一个条件”的布尔标记。closing确保游标关闭但注意closing不会关连接所以最后单独conn.close()。这里有个小习惯查询函数里用closing包游标写操作的函数里用try/finally包连接读多写少两种方式各取所需。日期区间查询的关键是“date 类型按字符串比较会出问题”。SQLite 没有专门的 date 类型存的是字符串格式统一成YYYY-MM-DD才能按字典序比较。如果你的日期值里有时间部分比如2025-06-01 14:30:00那 date_to会匹配不到当天晚些时候的记录。做法是存值时一律只存日期部分或者查询时给 date_to 拼上23:59:59。3.3 退房结账金额计算放在数据库还是 Python结账涉及住宿天数计算和总金额汇总这里有个设计选择用 SQL 函数算还是用 Python 算。这个项目的做法是在 Python 里算天数然后调 SQL 查出房间单价再相乘。我给你看退房的完整流程def checkout(check_in_id, pay_method): conn get_connection() try: conn.execute(BEGIN) row conn.execute( SELECT ci.room_id, ci.check_in_time, r.price, b.check_out_date FROM check_in ci JOIN booking b ON ci.booking_id b.booking_id JOIN room r ON ci.room_id r.room_id WHERE ci.check_in_id ? AND ci.status in_house, (check_in_id,) ).fetchone() if not row: raise ValueError(该入住记录不存在或已结账) check_in_time row[check_in_time][:10] check_out_date row[check_out_date] days (datetime.strptime(check_out_date, %Y-%m-%d) - datetime.strptime(check_in_time, %Y-%m-%d)).days if days 1: days 1 total days * row[price] conn.execute( UPDATE check_in SET status checked_out, total_amount ? WHERE check_in_id ?, (total, check_in_id) ) conn.execute( UPDATE room SET status cleaning WHERE room_id ?, (row[room_id],) ) conn.execute( INSERT INTO check_out (check_in_id, total_amount, pay_method, check_out_time) VALUES (?, ?, ?, datetime(now, localtime)), (check_in_id, total, pay_method) ) conn.commit() return total except Exception: conn.rollback() raise finally: conn.close()天数计算用 Python 的datetime.strptime两个日期字符串转成 datetime 对象做差得到timedelta取.days。要留意的是如果当天入住当天退房天数差为 0业务上至少按一天收所以这里强制设成 1。房间状态改成cleaning而不是直接回到available意思是保洁完成前不能再次预订这个状态流转比“退房直接可订”更真实答辩时可以说这是参考了酒店 PMS 系统的房态管理逻辑。4. PyQt 界面绑定与数据刷新从表格控件到事件处理的完整链路数据层写完后界面层就是把 DAO 返回的数据填到表格里再把按钮和事件绑起来。这个项目的界面结构不复杂但有几个细节直接关系到演示顺不顺畅。4.1 QTableWidget 填充与刷新时机界面里最常用的是QTableWidget它的数据填充方式有两种setItem逐格设置或者setRowCount后循环。这里我给出项目里刷新客房列表的标准写法注意列头是固定设置的数据行每次刷新前要清空旧数据def refresh_room_table(self, rooms): self.table_room.setRowCount(0) self.table_room.setRowCount(len(rooms)) for i, room in enumerate(rooms): self.table_room.setItem(i, 0, QTableWidgetItem(str(room[room_id]))) self.table_room.setItem(i, 1, QTableWidgetItem(room[room_type])) self.table_room.setItem(i, 2, QTableWidgetItem(f{room[price]:.2f})) status_text {available: 可预订, reserved: 已预订, in_house: 入住中, cleaning: 保洁中}.get( room[status], room[status]) self.table_room.setItem(i, 3, QTableWidgetItem(status_text))刷新前先setRowCount(0)清空旧数据再设置新行数避免残留行。状态字段在数据库里存英文在界面显示中文这个映射放在界面对应的函数里而不是数据库层因为同一份数据未来可能换不同的展示语言。价格用f{price:.2f}格式化成两位小数避免出现 98.0 这种显示。QTableWidget 的单元格默认不可编辑但如果在表格里做行选中需要拿到当前选中的行号这就要绑定currentCellChanged信号。项目里常用的做法是用户点击某行后把该行的 room_id 存到一个成员变量里后续点击“预订”按钮时读取这个变量。4.2 信号槽绑定按钮事件与数据校验PyQt 的事件绑定很直接button.clicked.connect(self.on_some_action)。但这里要留意一个常见问题在循环里给多个按钮绑定信号时lambda 表达式会捕获循环变量导致所有按钮触发同样的行为。这个项目的按钮都是单一存在的不涉及这个问题但如果你自己扩展成“列表里每行一个操作按钮”就会踩到。下面这段代码是预订按钮的处理逻辑展示了界面层如何调用 DAO 层并处理错误def on_booking_clicked(self): guest_id self.line_edit_guest_id.text().strip() room_id self.current_room_id check_in self.date_edit_check_in.date().toString(yyyy-MM-dd) check_out self.date_edit_check_out.date().toString(yyyy-MM-dd) if not guest_id or room_id is None: QMessageBox.warning(self, 提示, 请先选择客户和房间) return if check_in check_out: QMessageBox.warning(self, 提示, 退房日期必须晚于入住日期) return try: booking_id booking_dao.create_booking( int(guest_id), room_id, check_in, check_out) except ValueError as e: QMessageBox.critical(self, 错误, str(e)) return QMessageBox.information(self, 成功, f预订成功编号{booking_id}) self.refresh_room_table(room_dao.list_all())line_edit.text().strip()是清掉用户误输入的空格否则查询条件和主键比较都会出问题。日期从QDateEdit控件读取时要显式转成yyyy-MM-dd字符串日期格式不统一是课设里特别常见的低级错误——界面上显示正常但存进数据库后查询对不上。这里在调 DAO 之前先做了两个校验客户和房间不能为空、退房日期必须晚于入住日期。校验放在界面层而不是数据库层因为这类业务规则与用户交互直接相关尽早拦下来比数据库抛一个看不懂的异常更友好。4.3 登录窗口与主窗口的跳转项目里还有一个登录窗口验证 admin 表里的用户名和密码。这里我提一个安全层面的点密码不要明文存储。课设里密码表如果用明文答辩时老师可能会追问。最低要求是存哈希值Python 标准库里有hashlib甚至低配的 SHA-256 都行import hashlib def hash_password(password): salt hotel_salt return hashlib.sha256((salt password).encode(utf-8)).hexdigest()登录验证时把用户输入的密码做同样的哈希再和数据库里的字段比对。注意我这个写法里的 salt 是硬编码的真正的生产系统应该每个用户一个随机 salt但课设里能做到“不存明文”已经是合格线。演示时也可以顺带展示数据库中存的是哈希串说明你至少在考虑安全性。5. 避坑这门课设里最容易翻车的 5 个数据库问题课设翻车往往不是功能没写完而是写完后在演示和答辩的几分钟里崩溃。下面的现象都是实际运行中常见的每一条都是我拆这个项目或者给学弟学妹调试时见过的真实问题。问题 1房间显示“已预订”但实际没人预订现象界面上房间状态为 reservedbooking 表里没有对应记录。原因代码里的预订流程是先插入 booking再更新 room 状态两步之间没有事务包裹。如果客户信息非法导致第二步失败第一步已经提交房间状态就成了孤儿。解决把整个预订操作放在一个事务里任何一步失败都 rollback。检查所有写操作的地方凡是涉及两张表以上状态变更的一律用BEGINcommitrollback。问题 2删除客户时报外键错误但界面上明明删的是没人关联的客户现象删除 guest 记录时抛FOREIGN KEY constraint failed。原因SQLite 默认没开外键约束所以即使你建表时写了FOREIGN KEY也没生效。而且PRAGMA foreign_keys是一个连接级别的设置没在每次建连接时执行就静默关闭。解决在get_connection()里强制conn.execute(PRAGMA foreign_keys ON)每次连接都执行不要只在初始化时执行一次。问题 3日期查询少了当天数据现象按退房日期筛选 6 月 10 日退房的记录结果只有 6 月 10 日以前的6 月 10 日当天的数据不出现。原因存储的日期带时间部分WHERE check_out_date 2025-06-10实际比较的是2025-06-10 08:30:00 2025-06-10字典序比较时字符串更长的值排后面所以当天数据被排除。解决查询时给结束日期拼上23:59:59或者统一在存取时去掉时间部分。比较稳妥的做法是WHERE check_out_date date_from AND check_out_date date(date_to, 1 day)用半开区间避开边界问题。问题 4QTableWidget 刷新后之前选中的行还在现象操作完一个客户后刷新列表旧客户的行仍然高亮。原因setRowCount(0)只是清空行数据没有清除选中状态。解决刷新后调用table.clearSelection()清空选中。问题 5打包成 exe 后数据库文件写不进去现象在 PyCharm 里能正常写入数据打包后用PyInstaller生成的 exe 却无法创建或写入数据库文件。原因打包后的程序工作目录是临时解压目录相对路径hotel.db指向临时目录没有写权限或路径不对。解决数据库路径不要用相对路径用os.path.dirname(os.path.abspath(__file__))拼出数据库文件位置或者直接放在用户目录下。代码里每次用到DB_PATH的地方都检查一遍这个路径是否是绝对路径。6. 演示与答辩的系统化验证把课设从能跑变成能讲功能写完只是第一步答辩前一定要按业务流程把系统过一遍不是随便点几下而是按真实酒店操作顺序走一遍。我习惯用一套固定的演练脚本新建客户→预订房间→查看房间状态→办理入住→退房结账→确认房间变保洁→保洁完成变可预订→查询客户历史记录。每一步都验证数据库里的对应变化。这里我用一段独立于业务代码的验证脚本直接查看数据库里的数据流转是否符合预期def pre_demo_check(): conn get_connection() sqls [ (客户数, SELECT COUNT(*) AS c FROM guest), (空闲房, SELECT COUNT(*) AS c FROM room WHERE statusavailable), (在住记录, SELECT COUNT(*) AS c FROM check_in WHERE statusin_house), (待处理预订, SELECT COUNT(*) AS c FROM booking WHERE statusreserved), ] for name, sql in sqls: row conn.execute(sql).fetchone() print(f{name}: {row[c]}) conn.close()这段代码的价值不是功能本身而是提供一种“业务闭环校验”的思路跑完一个完整流程后用几条 SQL 检查数据是否一致。如果房间状态和预订记录对不上说明事务或状态流转有 bug趁答辩前修掉。关于打包PyInstaller是这个项目推荐的做法命令是pyinstaller --windowed --onefile main.py。要注意--onefile模式下程序启动会慢因为每次运行都要解压到临时目录这正常不是死循环。还有一个技巧把数据库建表 SQL 在程序启动时自动执行一次如果数据库不存在就自动创建这样交付给对方时不需要手动导入 schema双击就能跑。这属于课设加分项。另外答辩时大概率会问你“数据库并发访问如何处理”从这个项目的代码出发你可以回答两层第一层是事务加乐观锁就是前面UPDATE room SET status reserved WHERE status available这种写法通过影响行数判断冲突第二层是针对 SQLite 写并发受限如果换成 MySQL 可以开启事务配合行级锁。你不需要真的改成 MySQL但要把设计意图讲清楚。从那以后我每次做课设都会在答辩前用这个固定脚本把整个业务流程走一遍再打开数据库文件逐个表确认数据和界面一致宁可多花二十分钟也不让演示环节出意外。希望帮到你这套宾馆管理系统的源码包里包含完整的建表 SQL、PyQt 界面和 DAO 层代码下载后先按第 2 章的目录结构熟悉一遍再动手去改功能会比直接跑起来更有收获。本文还有配套的精品资源点击获取