每一位学Java的人应该都绕不过这个坎——期末要做一个小型管理系统。我当时纠结了很久最后选定的题目是“基于Java的酒店预定管理系统设计与实现”。选它倒不是因为多高深而是因为它足够有画面感登录、查房、下单、结算每个环节都看得见摸得着做出来的东西也能真跑、真演示、真答辩。更关键的是一套酒店预定系统几乎把Java SE阶段该用的东西全串起来了对象建模、集合、文件读写、JDBC数据库访问、Swing事件处理、日期时间操作哪一样都躲不掉。老实讲把“订酒店”这件事做成能用的桌面程序比做图书管理、成绩管理那些“老熟人”有意思得多也更能体现一个人到底有没有动手能力。这篇文章我不打算按答辩PPT那套“研究背景、需求分析、详细设计、测试结论”来写而是直接按我实际动手的顺序把从需求盘到技术选型从数据库建表到核心模块实现再到那些没写在报告里的踩坑记录一次性摊开。适合正要选课设题目、或者已经选了酒店管理类项目、正愁代码怎么写的人参考。你不需要先看完一遍Java教程只需要跟着这条线走完你就能理解一个完整的管理系统是怎么从零长出来的。1. 先把需求盘清楚这套系统到底要管什么1.1 角色与核心业务流做系统之前第一件事不是写代码而是把“酒店到底怎么运转”这件事问明白。我当时去模拟了一套流程顾客想订房要么到现场、要么打电话前台先查哪几天有空房然后按房型报价顾客认可就下单到店登记入住走的时候退房结账。如果把管理员一个人干了所有的活那系统就退化成一个记事本没有设计味道。所以标准做法是拆两类角色普通用户前台操作员/顾客自助和管理员老板/店长。普通用户登录注册、浏览房型、按日期查空闲房间、发起预订、查看自己的订单、办理入住登记、退房结算。管理员维护房型与房间信息、查看全部订单、处理入住与退房操作、简单的营收数据统计。我把这套流程画成一条业务线查房 - 下单 - 支付/登记 - 入住 - 退房。后面所有表结构和代码都是围绕这条线展开的。1.2 容易被忽略的边界场景“系统设计”这一章能不能拿高分往往看你能不能说出几个业务里的边界场景。我当时在需求评审时被某位导师追问了一句“那顾客订了两天第二天想续住怎么办隔壁房间临时装修不能卖怎么办”当场有点卡壳后来一想其实是好事这些问题逼我把规则补全了。常见的边界规则至少要覆盖这几点订单可以取消但已经入住的订单不能直接取消只能走退房退房时间如果超过当天中午12点的约定算不算加收半天房费系统要不要处理同一个房间在同一时间段内不能被重复预订房型价格调整之后历史订单应该显示下单时的价格而不是最新价格。这些规则听起来是“细节”但写进设计文档和代码之后整个系统的深度立刻不一样了。2. 技术选型为什么Java SE JDBC Swing就够用别一上来就上框架2.1 备选方案对比很多同学一看到“管理系统”三个字第一反应是“那我是不是得用Spring Boot Vue来做”。如果在学校课程设计的时间窗内纯Java桌面方案显然更可控。我列过一张对比表最后给自己找了一个不折腾的理由方案复杂度演示效果踩坑风险我的评价Java SE Swing JDBC MySQL低桌面窗口直观较低课程设计首选Java SE Swing MyBatis中同上中引入XML映射配置反而增加学习成本Spring Boot Thymeleaf MyBatis高浏览器访问高适合毕业设计不是课设首选Spring Boot Vue 前后端分离很高现代感强很高只推荐基础很好的同学否则时间不够为什么我会劝退框架方案因为框架解决的核心问题不是“课程设计”的诉求。Spring Boot 的价值在于自动化配置、生态整合、微服务能力而一个单机酒店管理系统根本没有多个服务要协调也没有高并发要撑住。强行上框架你反而要先搞懂依赖注入、ORM映射、自动配置这些概念等于在做一个管理系统之前先给自己加了一门框架课。当然如果你目标是毕业设计或者实习作品展示Spring Boot这类的商业级路线再合适不过。这里要区分清楚场景。2.2 分层架构设计技术选型定了之后最怕的是把所有代码堆在JFrame里一个类两千行“一套代码打天下”。我当时给自己定了一个最小分层标准就三层界面层UI、业务逻辑层Service、数据访问层DAO。UI层放JFrame、JPanel、JTable这些窗口和组件只负责展示和收集用户输入不写SQL。Service层处理业务规则比如价格计算、时间碰撞校验、订单状态校验。这里是把“规则”变成“代码”的地方。DAO层只负责数据库的增删改查用JDBC执行SQL把结果集转换为实体对象。这样做的好处非常直接哪天想换数据库或者把Swing换成控制台输出只需要改动对应层不用把整个项目推倒重来。我在答辩时被问到“如果要把界面从Swing换成Web怎么做”因为提前按分层写了回答起来非常顺——换UI层就可以Service和DAO复用。2.3 包结构与代码组织具体的包名我当时是按这个来组织的你可以直接抄作业com.hotel.entity实体类对应数据表比如User、RoomType、Room、Ordercom.hotel.dao数据访问接口和实现比如UserDao、RoomDao、OrderDaocom.hotel.service业务逻辑比如LoginService、ReservationService、CheckoutServicecom.hotel.ui窗口类LoginFrame、MainFrame、RoomPanel、OrderPanelcom.hotel.util工具类DBUtil数据库连接、DateUtil日期转换、MD5Util密码加密这种结构非常朴素但它是Java学习阶段最重要的“工程感”。等以后真的用Spring框架你会发现 Controller、Service、Repository 这些注解本质上就是把这三层固定成了规范。现在把分层的肌肉记忆练出来后面上手框架会快很多。3. 数据库设计订单表才是整个系统的灵魂3.1 五张核心表的结构设计酒店预定系统的表结构网上随便一搜就能找到一堆但很多都是“用户表、订单表、房间表”三张表糊弄完事。我的经验是房型一定要单独一张表因为房间的“单价”不是挂在具体房间上的而是挂在房型上的。同一个房型下面有多个房间比如豪华大床房可能有三间价格一样但房间号不同。如果把价格挂在每间房上以后调价要改好几行数据逻辑上也讲不通。我设计的核心表如下用户表t_user字段类型说明idINT PK AI主键usernameVARCHAR(50) UNIQUE登录名passwordVARCHAR(100)加密后的密码saltVARCHAR(50)加盐值roleTINYINT0-管理员 1-普通用户nicknameVARCHAR(50)显示名phoneVARCHAR(20)手机号create_timeDATETIME注册时间房型表t_room_type字段类型说明idINT PK AI主键type_nameVARCHAR(50)房型名标准大床房/豪华双床房priceDECIMAL(10,2)门市价bed_typeVARCHAR(20)床型areaDOUBLE面积max_peopleINT可住人数descriptionVARCHAR(255)描述房间表t_room字段类型说明idINT PK AI主键room_noVARCHAR(20)房间号 101/201type_idINT FK关联房型表floorINT楼层statusTINYINT0-空闲 1-已入住 2-维护中remarkVARCHAR(255)备注订单表t_order字段类型说明idINT PK AI主键order_noVARCHAR(30)订单号user_idINT FK下单人room_idINT FK房间type_nameVARCHAR(50)下单时的房型名冗余priceDECIMAL(10,2)下单时的单价冗余checkin_dateDATE入住日期checkout_dateDATE退房日期nightsINT入住晚数total_priceDECIMAL(10,2)总价statusTINYINT订单状态create_timeDATETIME下单时间remarkVARCHAR(255)备注3.2 为什么要把type_name和price冗余进订单表刚学数据库的时候老师反复强调“规范化”所以很多学生一看到订单表里的 type_name 和 price 就会皱眉这不冗余了吗要查房型名联表查不就行了但实际酒店业务里房型价格是会变的。假设顾客三天前预订的时候房型价格是368元今天到店退房结算结果房型价格已经调到398元。如果你只存了房间ID价格靠联表现查那退房结算时就说不清到底按哪个价格算。把下单时的单价和房型名直接快照进订单表这个问题就解决了订单显示的是下单那一刻的价格跟后续调价无关。这就是典型的“以空间换时间、用冗余保一致”的思路。做课程设计的时候能说出这个理由数据库设计直接上一个档次。另外订单号我建议不要用自增ID直接展示给用户而是拼接一个业务号。我的做法是yyyyMMddHHmmss 用户ID 四位随机数比如20250615143012_1001_8572。这样做的好处是订单号本身就能看出下单时间而且避免用户根据ID号猜测系统总共有多少订单带点商业系统的感觉。3.3 日期与状态字段的约定日期类型我用的是DATE不是VARCHAR。很多初学同学喜欢把日期存成字符串“2025-06-15”表面上看着方便但到了要比较日期前后顺序、计算入住天数的时候还得先转格式纯属给自己埋坑。Java侧对应使用LocalDate配合java.sql.Date做JDBC参数绑定非常顺手。订单状态我用了整数枚举这套状态以后在代码里就是一个常量类的事状态值含义0已预订待入住1已入住已办理入住2已退房已完成3已取消房间表的 status 跟订单表的 status 是两套概念。房间状态表示物理状态订单状态表示交易状态。这两者容易混我在代码里专门分了一个常量类去隔离它们避免业务代码里魔法数字满天飞。4. 五个核心模块的实现思路与关键代码4.1 登录认证与权限控制登录模块是所有管理系统的门面。我当时为了方便演示早期直接把密码明文存库。后来看某段课程视频讲到密码安全立刻意识到这是演示时会被当场问住的点——“你的密码凭什么不加密”于是改成MD5 随机盐的方案。流程是注册时生成一个随机盐比如UUID前8位把盐和密码拼起来做MD5盐和hash一起入库。登录时用输入的密码去匹配。在这个项目里你可以用MessageDigest做摘要代码量很小。如果未来想更安全可以升级为BCrypt这类专门算法但对于课程设计展示加密思想MD5盐已经足够说明你已经考虑过安全问题。登录之后我用一个全局的Session概念其实就是静态变量存当前用户对象来控制权限。管理员登录后主界面的“房间管理”“订单管理”按钮可用普通用户登录后这些按钮置灰或直接隐藏。Swing里就是在按钮上做setVisible(false)或者setEnabled(false)配合角色值判断非常简单。4.2 空闲房间查询与时间碰撞检测查空闲房间是整个系统最核心的业务逻辑。你光查“状态为0的空闲房间”是不够的因为一个房间即使当前显示空闲也可能已经有未来日期的预订。必须把“房间在指定日期区间内是否被占用”作为过滤条件。判断两个时间段是否重叠有一个非常经典的条件。假设已有订单的入住日期是existing_checkin退房日期是existing_checkout用户想订的区间是[checkin, checkout)。那么两者冲突等价于checkin existing_checkout AND checkout existing_checkin这个条件初看有点绕我当时也想了很久。我自己的理解方式是画时间轴如果新订单开始日期早于已有订单的结束日期并且新订单结束日期晚于已有订单的开始日期这两个区间就一定重叠了。注意边界处理我约定入住当天中午12点算入住退房当天中午12点前算退房所以日期区间是左闭右开检查条件严格成立。下面是查询可用房间的核心SQL这段代码也是整个项目价值最高的部分之一SELECT r.*, rt.type_name, rt.price FROM t_room r JOIN t_room_type rt ON r.type_id rt.id WHERE r.status 0 AND r.id NOT IN ( SELECT o.room_id FROM t_order o WHERE o.status IN (0, 1) AND #{checkoutDate} o.checkin_date AND #{checkinDate} o.checkout_date )前端把用户选的入住日期、退房日期当作参数传入SQL直接返回所有在该时间段内可订的房间。这个查询既考虑了订单状态已预订和已入住的都算占用又考虑了时间区间属于整个系统的“题眼”。4.3 预订下单与事务一致性下单不仅仅是一次 INSERT 操作。它要做的事情包括生成订单号、计算晚数和总价、检查房间是否仍可预订、写入订单记录。最怕的是A用户查询完房间还没下单B用户抢先订了同一间房。为了尽量降低这个风险我在Service层的预定方法里用的是“先判断再插入”的事务方式。什么叫事务就是一组操作要么全部成功要么全部失败不留下半成品数据。在JDBC里事务控制的套路是固定的Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 再次校验房间在该时间段是否被占用 // 2. INSERT 订单记录 conn.commit(); } catch (Exception e) { if (conn ! null) { conn.rollback(); } e.printStackTrace(); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } }你需要注意一个细节查询校验和插入操作必须放在同一个事务里并且查询要使用当前连接而不是新开一个连接。如果校验用一个连接插入用另一个连接那它们就是两套独立操作事务根本管不住。我当时就踩过这个坑后面第5部分会详细说。另外总价的计算我推荐放在Service层完成而不是在SQL里算。因为晚数的计算逻辑、优惠逻辑都是业务规则放Service层更好维护、更好写单元测试。晚数计算就一行代码long nights ChronoUnit.DAYS.between(checkinDate, checkoutDate);这个API是Java 8引入的比SimpleDateFormat算日期差可靠得多不会踩格式匹配的坑。4.4 订单状态流转与退房结算酒店系统的订单跟电商订单不一样它有一个可感知的线下履约过程。我把状态流转严格定成这几条合法路径已预订 - 已入住办理入住已预订 - 已取消用户取消已入住 - 已退房办理退房结算在代码层面我用了一个非常朴素的OrderStateMachine工具类本质就是一组状态判断的 if-elsepublic static boolean canTransition(int from, int to) { switch (from) { case OrderStatus.BOOKED: return to OrderStatus.CHECKED_IN || to OrderStatus.CANCELLED; case OrderStatus.CHECKED_IN: return to OrderStatus.CHECKED_OUT; default: return false; } }状态在数据库里改起来很容易但“状态乱跳”会造成数据混乱比如订单从“已取消”直接变成“已退房”这在业务上是不可能的。有了这个工具类的校验所有Service层改状态的地方都要先过canTransition这一关。退房结算的另一个重点是入住期间如果产生了其他消费比如mini吧、餐费要不要记录作为课设最朴素的方案是在订单表加一个other_fee字段退房时手动填然后total_price nights * 单价 other_fee。这个扩展虽然简单但在演示时能展示出“你考虑过真实酒店的复杂场景”属于加分项。4.5 后台房型与房间管理后台管理模块其实是一组通用的CRUD操作核心难点在于Swing表格组件的刷新机制。我用 JTable DefaultTableModel 来做展示添加或修改数据之后必须刷新表格模型否则界面上看不到变化。我封装了一个比较省心的模式每次打开管理面板时重新查数据库把数据封装为二维数组tableModel.setDataVector(...)重新填充。很多人喜欢用局部刷新的方式来“只更新变化行”但在数据量不大的课设场景中全量刷新更简单也更不容易出错。等你以后真的做大数据量的商品列表时再考虑分页与局部刷新也不迟。房型管理里有一个联动逻辑删除房型时如果该房型下面还有房间必须阻止删除或给出提示修改房型价格时只影响之后的订单历史订单仍然显示下单时的快照价格。这个联动规则说到底是外键约束和业务约束的配合。数据库层面可以不加物理外键有些课设不喜欢外键因为删除麻烦但Service层一定要做检查避免出现“孤儿房间”。5. 踩坑记录这四类错误我强烈推荐你做之前先看一遍5.1 SQL注入拼字符串一时爽查完数据库火葬场最开始的版本我的登录SQL是这样写的String sql SELECT * FROM t_user WHERE username username AND password password ;运行没问题但如果用户名输入admin --SQL就变成SELECT * FROM t_user WHERE username admin -- AND password xxx注释符把后面条件全干掉攻击者只靠一个用户名就能登录。这在演示现场被人提一句场面会非常尴尬。修复方式很简单所有用户输入拼到SQL里的场景一律用预编译占位符。String sql SELECT * FROM t_user WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, md5WithSalt(password, salt));除了安全性预编译还有一个性能上的好处SQL结构固定数据库可以复用执行计划。我后来接受某位前辈的建议把整个项目里所有SQL都改成了PreparedStatement随手一搜String 拼接SQL的地方全部肃清。这一点写进代码里会在答辩时成为非常加分的谈资。5.2 同一房间同一时间被订两次事务与状态检查的配合最早我的预定逻辑是先查空闲房间列表用户选定房间后执行 INSERT 订单中间没有任何防并发的保护。结果测试阶段我自己开两个窗口对同一房间同一时间段下单两次都成功了房间被同时订给两位“顾客”数据直接拍脸。后来加了两层保险第一层是上面说的事务内再次校验第二层是订单表对(room_id, checkin_date, checkout_date, status)这类字段的逻辑约束在代码里就是插入之前重新执行空闲房间查询。虽然纯JDBC课程设级项目并发访问量很低这两层保护足够抵挡99%的误操作。5.3 Swing界面卡死数据库操作别直接放事件回调里Swing是单线程模型所有界面更新必须在事件分发线程EDT上执行。如果我在按钮点击事件里直接执行一个耗时3秒的SQL查询界面上整个窗口都会“假死”鼠标点哪儿都没反应像程序崩溃一样。解决方式有两种。简单方式课程设计演示阶段保证本机MySQL性能正常查询都在毫秒级卡顿不明显进阶方式使用SwingWorker把耗时操作放到后台线程再通过done()回调把结果送回事件线程更新界面。我给的建议是如果时间紧张就保证数据库可用、说明“因数据量小目前采用同步查询未来可优化为SwingWorker”如果时间充裕把SwingWorker的用法写出来并封装成一个TableRefreshWorker工具类作为性能优化的加分项。重点在于你必须知道Swing线程模型的存在答辩时这一句话就能证明你不是只会拖控件。5.4 数据库连接泄漏finally关闭是底线我的项目早期出现过一个问题打开多个窗口后操作变慢MySQL报“Too many connections”。原因是DAO层每次getConnection()之后没有代码关闭连接。JDBC的DriverManager.getConnection()拿到的连接如果不手动close()进程结束前不会自动归还。修复方式就是在 finally 块里把 conn、ps、rs 三个资源逐个关闭。Java 7之后有 try-with-resources可以直接关闭AutoCloseable对象try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ResultSet rs ps.executeQuery(); // ... } catch (SQLException e) { e.printStackTrace(); }说实话课程设计阶段不需要引入连接池但如果你未来做真实项目连接池比如HikariCP会接管连接的创建与回收。不过连接池不是重点重点是“用完就关”这个习惯。我在代码里写了一个DBUtil.close(conn, ps, rs)的辅助方法所有DAO层统一调用从那之后“连接数超限”的问题再也没出现过。最后再分享一点我的实际体会整套系统从需求梳理到能跑通演示我大概花了不到三周。回头看不难但有几个点你要有心理准备第一个是SQL的时间区间条件理解了左闭右开的区间重叠你的系统就立住了第二个是状态流转宁可多写几个常量类也别在Service里到处填数字第三个是分层哪怕再简单的项目也要逼自己把UI、Service、DAO分开写这个习惯比系统本身值钱。如果你还要继续扩展我推荐两个方向一是把这个桌面版改造成Web版把Swing层换掉Service和DAO非常容易迁到Spring Boot上二是在订单流程里引入“支付状态”把它从课设系统推进一步变成像模像样的商业原型。但无论如何先把这篇文章里的表结构、空闲房间查询、事务下单这三样吃透你的酒店预定管理系统就已经能站在及格线之上了。事实上这三样东西放到任何带“预约资源”的业务里——会议室预定、球场订场、车辆调度——都是一模一样的套路一通百通。