☰
数据库课程设计选宾馆管理系统:从ER图到JDBC事务一网打尽
2026/10/13 8:31:38 网站建设 项目流程

简介:面向计算机相关专业学生的宾馆管理系统课程设计资料包,是一套可直接参考的完整项目方案。系统以宾馆日常运营为业务背景,围绕客房、客户、预订、入住、退房等核心数据对象,实现了房间信息维护、客户登记、订单管理、结算统计等典型功能模块,业务逻辑连贯且贴近实际工程场景。该方案曾获98分,源码采用Java编写,数据库脚本预置了多张关联数据表及初始数据,配套报告则完整记录了需求分析、E-R设计、关系模式、功能结构、界面截图与测试过程,能够帮助学习者快速把握课程设计从设计到成文的完整脉络。压缩包大小约23.53MB,主要文件类型包括Java源文件、SQL脚本、Word版报告等,整体结构清晰、便于按文档索引查阅。目前已有88人学习下载,尤其适合计算机专业本科生用于期末大作业、课程设计答辩准备或数据库项目实践训练,既可对照实现功能,也可借鉴规范化文档写法,具有较高的完成度和实用价值。

1. 数据库课程设计选宾馆管理系统:一门课就把增删改查、事务、报表全练了

数据库课程设计最常翻车的并不是代码本身,而是“选了个需要造一堆假数据的题材,结果交上去的老是拼凑出来的功能”。宾馆管理系统之所以常年霸榜,是因为它业务闭环清晰:订房、入住、退房、结账、房态统计,每一步都天然对应数据库课程里必须考的知识点——增删改查、实体关系建模、事务边界、视图和触发器。这套题目拿 95 分以上的关键也在于此:功能量不需要多,但要让老师看到你“把数据真正管起来了”。

这份笔记面向正在做课程设计、需要尽快交源码和报告的人。我会从表结构设计、JDBC 核心业务实现、报告评分点、答辩演示到常见翻车现场,按一套可以直接落地的路径写透。

2. 先做数据库设计而不是先写代码:ER图、关系模式与表的落法

2.1 需求怎么拆:把“宾馆”拆成五个业务对象

拿到题目先别急着建表。课程设计的评分重点往往不是界面多漂亮,而是“你的数据模型能不能说清楚”。宾馆管理系统常见的实体有五个:房型、房间、顾客、预订单、账单。房型和房间是一对多,房间和预订单是一对多,顾客和预订单是一对多,预订单和账单是一对一。把这些先画成简单的 E-R 图,等于把整篇报告的第二章“需求分析”提前写完了。

拆需求的时候有一个容易被忽略的点:预订单本身是有状态的。很多初学者把“预订”和“入住”当成两张表,结果退房的时候数据对不上,报表统计更是无从下手。实际上预订单表里加一个状态字段就够了:booked(已预订)、in_use(入住中)、checked_out(已退房)、cancelled(已取消)。这样从预订到退房的整条生命周期都在一行记录里流转,写代码和写报告都省事。

另外别把“顾客信息”和“预订记录”合并。有的同学为了省事,把顾客姓名、电话直接塞进预订表,最后老师一问“同一个顾客第二次入住,怎么查出他的历史记录”就答不上来。顾客单独建表,证件号做唯一键,这是宾馆管理系统里最能体现规范化设计的地方。

2.2 关系模式设计:三张核心表把预订状态机立住

关系模式是报告的加分枢纽。如果你只交一张“总表”,评语大概率是“未体现关系数据库设计”。按下述方案,关系模式可以写成四句话:

  • 房型(房型编号,房型名称,基准价格,可住人数)
  • 房间(房间编号,房型编号,楼层,房态)
  • 顾客(顾客编号,姓名,证件号,电话)
  • 预订单(订单编号,顾客编号,房间编号,状态,入住时间,退房时间)
  • 账单(账单编号,订单编号,金额,创建时间)

加下划线的主键,带(房型编号)之类的是外键。把这套关系模式写进报告“逻辑结构设计”一节,已经比多数提交版本高一个档次。

这里有一个加分细节:预订表里的“入住时间”“退房时间”用 DATETIME,不要拆成日期和时间两个字段。一来聚合函数可以直接算入住时长,二来触发器生成账单时可以省掉一次字符串拼接。我自己见过不少代码把日期和时间分开存,结果算房费时先拼字符串再转类型,多做一步还容易乱。

2.3 用 SQL 把表建出来:字段类型、默认值与索引的取舍

表结构按下面的 SQL 在 MySQL 8.0 里可以一次跑通。字符集统一用utf8mb4,这是中文乱码最常见的一道防线。

CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hotel_db; CREATE TABLE room_type ( id INT AUTO_INCREMENT PRIMARY KEY, type_name VARCHAR(20) NOT NULL UNIQUE COMMENT '房型名称', base_price DECIMAL(10,2) NOT NULL COMMENT '基准价格', capacity INT NOT NULL DEFAULT 2 COMMENT '可住人数' ) ENGINE=InnoDB; CREATE TABLE room ( id INT AUTO_INCREMENT PRIMARY KEY, room_number VARCHAR(10) NOT NULL UNIQUE COMMENT '门牌号', type_id INT NOT NULL, floor INT NOT NULL COMMENT '入住楼层', status ENUM('available','booked','occupied','maintenance') NOT NULL DEFAULT 'available' COMMENT '房间状态', CONSTRAINT fk_room_type FOREIGN KEY (type_id) REFERENCES room_type(id) ) ENGINE=InnoDB; CREATE TABLE customer ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL, id_card VARCHAR(18) NOT NULL UNIQUE COMMENT '证件号,一般用身份证', phone VARCHAR(20) ) ENGINE=InnoDB; CREATE TABLE booking ( id INT AUTO_INCREMENT PRIMARY KEY, customer_id INT NOT NULL, room_id INT NOT NULL, status ENUM('booked','in_use','checked_out','cancelled') NOT NULL DEFAULT 'booked' COMMENT '预订单状态', check_in_time DATETIME, check_out_time DATETIME, CONSTRAINT fk_booking_customer FOREIGN KEY (customer_id) REFERENCES customer(id), CONSTRAINT fk_booking_room FOREIGN KEY (room_id) REFERENCES room(id) ) ENGINE=InnoDB;

字段类型的选择有三个值得说的地方。第一,DECIMAL(10,2)而不是FLOAT,金额计算不出现浮点尾巴。第二,ENUM存状态字段,写代码时不容易传错字符串,报告里也能解释成“用约束保证数据合法性”。第三,room_number加UNIQUE,这是业务上“一间房不能有两个房号”的底线约束,也方便后面做演示数据的可读性。

索引建议在booking表上加一个组合索引:(room_id, status)。这是查询“某个房间现在是否可订”“哪些房间正在入住”时的高频路径,后面讲 EXPLAIN 验证时会再提到它。而customer.id_card的唯一索引天然就是索引,不需要额外建。

3. 用 Java JDBC 实现核心业务:从“能连库”到“不丢订单”

3.1 一套连库配置:JDBC 连接 MySQL 的最小工程

课程设计的常见技术栈是 Java + MySQL,用 JDBC 直连不引入 MyBatis,反而容易在答辩时讲清楚每一条 SQL 的行为。先用一个工具类把连接管起来:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DbUtil { private static final String URL = "jdbc:mysql://localhost:3306/hotel_db" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

参数说明里最容易被漏掉的是characterEncoding=utf8。漏掉它,你会在插入中文顾客姓名时看到一堆问号,而且这个 bug 是后面所有功能跑起来之后才出现的,排查时绕一圈还找不到根源。serverTimezone=Asia/Shanghai是 MySQL 8.0 与 JDBC 驱动的时区对齐,不加会在连库时直接抛异常。端口没改过的机器用 3306,改了的话记得同步。

3.2 预订到入住的状态流转:事务边界写在哪

宾馆管理系统最容易出现的数据问题,是两段代码分别改了room.status和booking.status:房间改成“入住中”了,订单状态还是“已预订”,后来统计入住率时永远少一行。正确的做法是让两个状态在同一个事务里更新。

以“办理入住”为例,核心代码如下:

public boolean checkIn(int bookingId, int roomId) throws SQLException { String updateRoomSql = "UPDATE room SET status = 'occupied' WHERE id = ? AND status = 'booked'"; String updateBookingSql = "UPDATE booking SET status = 'in_use', check_in_time = NOW() " + "WHERE id = ? AND status = 'booked'"; Connection conn = DbUtil.getConnection(); conn.setAutoCommit(false); try { PreparedStatement psRoom = conn.prepareStatement(updateRoomSql); psRoom.setInt(1, roomId); int roomRows = psRoom.executeUpdate(); if (roomRows != 1) { conn.rollback(); return false; // 房间状态不是 booked,说明房态已被改动 } PreparedStatement psBooking = conn.prepareStatement(updateBookingSql); psBooking.setInt(1, bookingId); int bookingRows = psBooking.executeUpdate(); if (bookingRows != 1) { conn.rollback(); return false; } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }

这段代码里最关键的地方是WHERE ... AND status = 'booked'。同一时刻只有一个请求能把booked改成occupied,天然挡住了“两拨客人同时入住同一间房”的并发问题。如果改成先SELECT查状态、再用UPDATE改,两步之间有窗口期,并发一上来就会超卖。学会在面试时把这个“条件更新代替先查后改”的思路讲出来,比堆十个功能点都管用。

3.3 避免 SQL 注入与参数字段对不齐:PreparedStatement 的使用习惯

课程设计源码的常见扣分点之一是字符串拼接 SQL:

String sql = "SELECT * FROM booking WHERE customer_id = " + customerId;

这种写法既容易注入,也经不起留心眼的老师一句“如果我传一个1 OR 1=1你会怎样”。换成PreparedStatement只需要改半段代码:

public List<Booking> listByCustomer(int customerId) throws SQLException { String sql = "SELECT id, room_id, status, check_in_time, check_out_time " + "FROM booking WHERE customer_id = ? ORDER BY check_in_time DESC"; List<Booking> result = new ArrayList<>(); try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, customerId); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Booking b = new Booking(); b.setId(rs.getInt("id")); b.setRoomId(rs.getInt("room_id")); b.setStatus(rs.getString("status")); b.setCheckInTime(rs.getTimestamp("check_in_time")); b.setCheckOutTime(rs.getTimestamp("check_out_time")); result.add(b); } } } return result; }

两个容易踩的细节:第一,setInt的下标从 1 开始,不是 0,写多参数 SQL 时经常有人在这个地方对不齐;第二,try-with-resources会自动关闭Connection、PreparedStatement、ResultSet,顺序是反着的,别手动关Connection又去关ResultSet。课程设计不要求性能优化,但连接用完就关是好习惯,答辩时提一句“我保证了连接生命周期受控”,效果比空谈设计模式实在。

4. 报告与源码怎么准备才够“95 分”:评分点是写出来的

4.1 课程设计报告的结构与评分点分布

不少同学把源码写完,报告当晚拼凑,结果代码 90 分、报告 60 分,总评被拉下来。从老师评分角度看,一份数据库课程设计报告通常按以下比例给分:

报告章节约占分常见丢分点
需求分析10%只写“系统有登录和增删改查”,没有业务流程描述
概念结构设计(E-R 图)20%实体间缺关系、不标主外键、不写联系类型
逻辑结构设计(关系模式)15%只有表名,没有主外键和约束说明
数据字典10%字段名和代码对不上、类型不一致
功能实现说明20%只说“实现了”,不贴关键 SQL 和代码截图
测试与运行15%没有测试数据和测试用例描述
总结与心得10%一句话带过

这里做个算术你就能明白:报告里的概念结构和逻辑结构加起来占 35 分,比功能实现的 20 分还高。也就是说,即便你的界面朴素一点,只要数据模型画得清楚、说得明白,及格线已经过了。

4.2 数据字典这一节要写细:字段名、类型、约束、说明一个都不能少

数据字典是最容易被抄模板、也是最好写出区分度的部分。每张表现成的写法是:表名、字段名、数据类型、是否为空、默认值、约束说明,六列一张表,像下面这样描述booking表:

字段名类型可空默认值说明
idINT否自增订单编号,主键
customer_idINT否无顾客编号,外键引用 customer.id
room_idINT否无房间编号,外键引用 room.id
statusENUM否booked状态流转:booked/in_use/checked_out/cancelled
check_in_timeDATETIME是NULL实际入住时间
check_out_timeDATETIME是NULL实际退房时间

写数据字典有一个隐藏的要求:字段名要和源码里的实体类字段、JSP/前端表单里的 name 对齐。老师拿到报告抽查代码时,最喜欢做的一件事就是拿着数据字典去源码里对比字段。如果你报告里写customer_id,代码里写custId,当场就会被扣“文档与实现不一致”的分数。这个细节不需要任何技巧,就看你有没有认真检查一遍。

4.3 答辩演示脚本:按角色演示比按功能演示更出效果

答辩时间通常只有五分钟。按“游客入住流程”串起来演示,比按“登录、查询、删除、退出”这种菜单顺序演示要顺得多。我会把演示脚本写成四条场景:

  • 场景一:前台登记一位新顾客,录入姓名、证件号、电话,系统自动创建顾客档案;
  • 场景二:为顾客预订一间 5 楼的标间,此时房间状态由 available 变为 booked;
  • 场景三:顾客到店,办理入住,演示订单状态变为 in_use,房间状态变为 occupied;
  • 场景四:退房结账,展示账单自动生成,金额和入住时长对得上,订单状态变为 checked_out。

这个脚本的好处是把状态机完整走了一遍。老师看到的不再是孤立的增删改查,而是一条连贯的业务链路。演示时还要准备好“看完一条数据后打开 SQL 或 Navicat 秀一下对应的行”,这能证明数据是真实落库的,不是写死在界面上。

5. 常见问题与避坑清单:从选题到答辩,五个真实翻车现场

5.1 中文乱码:连接串里少写了 characterEncoding

现象:界面输入中文姓名,保存到数据库变成“?”,查询回来也是问号;但在 Navicat 里手动插入的中文却能正常显示。

原因:Java 连接 MySQL 时,驱动默认使用的字符集和表字符集不一致。表用了utf8mb4,但连接串里没有声明characterEncoding=utf8,导致字节流在传输过程中被错误解码。

解决:在 JDBC URL 中补上characterEncoding=utf8,并确保数据库连接、表结构、页面编码三处统一为 UTF-8。如果用的是 MySQL 5.x,还要检查连接串里是否漏了useUnicode=true。这个问题最坑的是它不会报错,只在数据入库时悄悄损坏。

5.2 预订状态不闭环:今天该退房的订单查不出来

现象:退房功能做完了,但“今日应退房列表”一直只有一部分数据,统计报表里的入住率对不上。

原因:退房时只改了room.status为 available,没有同步把booking.status改成 checked_out,或者反过来了。订单状态和房间状态失去同步,后续所有基于状态的查询都会出错。

解决:把状态流转收敛到一个事务里,就像第 3.2 节那样,一条路径同时更新两张表;更进一步,可以在booking上加一个“状态机”约束思路:只允许booked -> in_use -> checked_out,取消只在booked状态发生,用应用层代码保证,不依赖数据库触发器。写报告时把这个过程描述成“事务边界设计”,是加分说法。

5.3 外键约束删不掉:删除房型时被约束拦截

现象:想删掉一个已有房间引用的房型,删除按钮点击后报错,页面直接抛异常。

原因:room.type_id外键指向room_type.id,数据库在ON DELETE默认行为下拒绝删除被引用的父表记录。很多课程设计报告里根本没提这个异常,答辩被问到时支支吾吾。

解决:先处理子表,再删除父表。业务上正确顺序是:下架相关房间、删除或转移房间记录,最后删除房型。也可以在建表时把外键声明为ON DELETE RESTRICT,然后在报告里专门讲一小段“为什么选择禁止级联删除”——因为宾馆数据需要保留入住历史,防止误删账目。这个回答反而是加分项。

5.4 房价用 float 存储:账单金额对不上

现象:房价是 128.00,入住两晚应收 256.00,报表里却显示 255.9999998。

原因:FLOAT/DOUBLE是浮点数,二进制无法精确表示部分十进制小数,进行乘法聚合时会累积误差。金额字段用了 float,是一切“钱数不对”的源头。

解决:表结构中的金额字段一律改为DECIMAL(10,2),Java 侧对应使用BigDecimal而不是double。门票类似的还有“平均房价”这种统计指标,在 SQL 里用CAST(SUM(amount) / COUNT(*) AS DECIMAL(10,2))收口。把这一点写进报告“设计约束”小节,说明你考虑了浮点精度问题,属于一眼就能发现的细节分。

5.5 MySQL 8.0 连接报错 caching_sha2_password

现象:用 JDBC 连接 MySQL 8.0 时报Unable to load authentication plugin 'caching_sha2_password',换了驱动版本也没解决。

原因:MySQL 8.0 默认认证插件是caching_sha2_password,旧版 JDBC 驱动不认识。课程设计的机器上常装的是旧驱动,或者从网上下了一个过时 jar。

解决:优先把mysql-connector-java换成 8.x 版本,并在连接串里继续保持SSL关闭。如果实验环境不允许动驱动,可以用管理员账号执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '密码';把认证插件退回去,但这句话不用写进报告,答辩时提“我用的是新版驱动兼容 8.0 认证方式”就够了。

6. 从“能跑”到“95 分”:三个加分项和一套验收演示

6.1 用 EXPLAIN 证明你的索引优化有效

报告里写了索引,老师不一定会信。最直接的验证方式是执行EXPLAIN SELECT * FROM booking WHERE room_id = 1 AND status = 'in_use';,把执行计划截图贴进报告:type显示ref、possible_keys命中了组合索引、key不是 NULL,这几行就是最硬的证据。

常见的翻车是索引建了但 SQL 写法让索引失效,比如在WHERE里对索引列做函数运算WHERE DATE(check_in_time) = CURDATE(),这会让索引失效。改成WHERE check_in_time >= CURDATE() AND check_in_time < CURDATE() + INTERVAL 1 DAY,索引才能生效。把这个前后对比写进测试小节,效果远好于一句“我建了索引”。

6.2 触发器自动生成账单:让报告多一个设计亮点

课程设计不用触发器也能及格,但用了明显是个加分项。以“退房时自动计算房费”为例,MySQL 8.0 里可以这样写:

CREATE TRIGGER trg_booking_after_update AFTER UPDATE ON booking FOR EACH ROW BEGIN IF NEW.status = 'checked_out' AND OLD.status <> 'checked_out' THEN INSERT INTO billing(booking_id, amount, created_at) SELECT NEW.id, TIMESTAMPDIFF(HOUR, NEW.check_in_time, NEW.check_out_time) * rt.base_price / 24, NOW() FROM room r JOIN room_type rt ON r.type_id = rt.id WHERE r.id = NEW.room_id; END IF; END;

这段逻辑说清楚了三件事:数据库中billing表的金额来源、避免应用层漏算、状态变更与账单生成的一致性。要注意不同数据库的触发器语法差别很大,SQLite 写AFTER UPDATE OF status,SQL Server 用INSERTED表,如果课程设计指定了别的数据库,提前确认语法。另外触发器建议单独写在报告“高级设计”一节,不要在答辩现场临时展示建触发器的过程。

6.3 验收演示套路:从菜单演示升级为数据流演示

最后一次自测不要按菜单点,要按数据流走:开两个终端窗口,一边开 Java 程序界面,另一边开 Navicat 或 MySQL 命令行。每执行一步操作,就切到数据库端查一次对应表的记录变化。查room、booking、billing三张表的行,能干净地看到状态从booked到in_use再到checked_out的流转,账单金额也对应生成,这个演示的说服力是最强的。

再准备一个“后悔药”演示:取消一笔状态还停留在booked的订单,把房间恢复为 available,然后指出“已入住的订单不允许取消,在业务代码中已经限制了状态机的合法跳转”。这句话不大,但能显示你思考过异常流程。

我做课程设计时最深的教训是:把八成时间花在数据库设计和状态一致性上,而不是拼命加功能按钮。能跑通的页面一抓一大把,状态不乱的系统才是少数。按这个思路把表设计、事务、报告逻辑串起来,95 分以上不是玄学,是每一步都有据可查的结果。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询