
简介这套基于SpringBoot与Vue的酒店客房管理系统源码主要面向毕业设计、Java全栈开发练习及课程实践。系统涵盖客房信息管理、预订入住、用户权限等核心模块前后端分离采用MySQL数据库与MyBatisPlus持久层框架适合需要完整项目参考的初学者及二次开发者。压缩包内共904个文件大小约18.44MB包含178个Java后端逻辑文件、60个Vue组件、153个JS脚本及HTML页面另有CSS样式、图标图片、XML配置与SQL数据库脚本等资源类型齐全便于直接导入IDE运行调试。已附带安装运行批处理脚本、环境配置文件及常见目录结构可帮助使用者快速搭建环境。目前已有69人学习下载对于需要完成酒店管理类课题或希望了解SpringBootVue整合方式的人群是一份可直接参考的实战代码包。1. 这类Java Web酒店客房系统源码跑通之前先看懂三件事这类以“酒店客房系统”为标题的Java Web项目几乎每年都会在毕业设计和课程设计里出现一轮。它的定位很明确一个基于 Web 的轻量业务系统把散客在线选房、提交预订、前台入住登记、退房结账、房态维护这些环节串成一条完整的数据流。你拿到源码的第一反应不应该是双击导入然后跑起来而是先问自己这个项目用了几张表、几个角色、几条主流程如果答不上来那这套系统在你手里只是个黑匣子答辩时一问就露馅。这篇笔记就按“需求拆解 → 表结构 → 预订主流程 → 页面联调 → 排错 → 答辩加分”的顺序把基于Web的酒店客房系统到底怎么做讲清楚。适合正在做Java Web毕设的学生也适合想拿完整业务练手Spring Boot和MyBatis的新人。2. 先想清楚表再写页面酒店客房系统的需求拆解与数据库设计2.1 角色与用例管理员、前台、散客各管什么做酒店客房系统的第一步不是开IDEA而是先把“谁在用这个系统”说清楚。我经手的这类基于Web的项目角色一般切三份管理员管房型、房价、员工账号和经营统计前台管入住登记、退房结账和换房散客则通过网页浏览空房、提交预订、查看自己的订单。这个划分直接决定后面所有页面的数量和菜单结构也决定数据库里要不要单独放一张员工表。很多源码翻车就翻在这个阶段比如把管理员和前台混成同一个角色导致退房结算权限谁都拦不住再比如把散客在线预订和前台直接开单走同一条代码路径用一个status字段硬撑最后订单状态彻底乱掉。你看别人源码时第一步就是对着功能列表反向画出角色用例。如果角色少于三个这个系统多半是阉割版答辩时撑不过五分钟。角色核心用例对应页面或接口散客查空房、提交预订、查自己订单首页、预订页、我的订单前台入住登记、退房结算、换房前台工作台、结算页管理员房型房价维护、订单管理、统计报表后台管理页、报表页从页面角度看角色用例就是菜单树游客首页加预订页前台工作台包含今日入住、今日退房、房态总览后台管理包含房型房价、订单列表、统计报表。建议你先把菜单树手写在纸上再打开源码找到对应页面。找不到页面的对应功能基本就是没实现或者实现得不成型。2.2 核心表结构room_type、room、room_order 怎么建数据库设计是整个客房系统的地基。我一般会建四张核心表房型表、房间表、订单表、入住登记表。下面这份SQL是去掉无关字段后的最小可用版本足够支撑客房预订和前台结算两条主流程。-- 房型表放房型的公共属性避免每间房重复存价格 CREATE TABLE room_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL, base_price DECIMAL(10,2) NOT NULL COMMENT 门市价下单时快照进订单, bed_count TINYINT DEFAULT 1, area INT DEFAULT 20 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房型表; -- 房间表一张表对应一个物理房号状态用数字枚举 CREATE TABLE room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE, type_id INT NOT NULL, floor TINYINT DEFAULT 1, status TINYINT DEFAULT 0 COMMENT 0空闲 1入住 2脏房 3维修, CONSTRAINT fk_room_type FOREIGN KEY (type_id) REFERENCES room_type(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT房间表; -- 订单表散客预订和前台开单统一落这里 CREATE TABLE room_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, room_id INT NOT NULL, customer_name VARCHAR(50) NOT NULL, customer_phone VARCHAR(20) NOT NULL, check_in_date DATE NOT NULL COMMENT 预计入住日期, check_out_date DATE NOT NULL COMMENT 预计离店日期, price DECIMAL(10,2) NOT NULL COMMENT 成交价快照改价不影响历史, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2已入住 3已离店 4已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_order_date (check_in_date, check_out_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这份建表脚本里藏着三个关键设计面试官通常逮着这几点问。第一房间状态是room表自己的字段status用0空闲、1入住、2脏房、3维修的枚举而不是靠“订单没结束就说明占用”这种推理——因为脏房状态在订单结束后仍然存在必须有个字段单独表达。第二订单表里的price必须在下单那一刻把成交价快照下来房型表改价后历史订单的金额不能跟着变这是财务口径的硬要求。第三日期字段用DATE而不是DATETIME客房按自然日结算具体几点入住由check_in_record表记录。参数方面DECIMAL(10,2)对日均房价和押金足够别用FLOAT算钱TINYINT比INT省空间而且状态值0到4完全够用表字符集统一用utf8mb4只写utf8的话遇到特殊字符直接报错。索引idx_order_date建在check_in_date和check_out_date上是因为可用房间查询会按日期区间过滤联合索引能降低回表成本。如果嫌自增ID不够专业可以换雪花ID但单机演示的课设项目自增ID反而更直观。再补一张入住登记表专门记录“实际入住时间、实际离店时间、押金”订单表只管预订状态两件事分开后面做日报表会非常舒服。-- 入住登记表与订单一对一记录实际到店离店时间 CREATE TABLE check_in_record ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL UNIQUE, actual_check_in DATETIME NOT NULL COMMENT 实际入住时间, actual_check_out DATETIME DEFAULT NULL COMMENT 实际离店时间, deposit DECIMAL(10,2) DEFAULT 0 COMMENT 押金, operator VARCHAR(20) COMMENT 前台操作人 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入住登记表;2.3 两个关键取舍价格快照与多间房订单分组继续说设计里容易想不透的两个取舍。第一个是价格快照。房型表base_price是当前门市价订单表price是成交价两者必须分开。如果下单时只是引用房型价格三个月后房价一调所有历史订单的报表金额全部变味这在财务上是事故。下单当年我做课设时直接读了base_price被老师一句话问住“客人三个月前订的房今天退房按哪个价收”从此我把“快照”两个字刻进了所有交易表。第二个是“一间房一张订单”的限制。散客一次订两间代码里最常见的处理是生成两条订单加一个group_no字段把它们关联起来。这样每个订单对象始终只对应一个room_id办理入住时逻辑简单房态也跟得清楚。group_no可以用下单时间加随机数生成前端“我的订单”页按group_no折叠展示一眼就能看出这个客人订了几间房、住了几天。还有一种情况要在设计阶段就表态钟点房要不要做我的建议是课设阶段先不做但要在答辩时主动说“我知道这里是状态机扩展钟点房需要在订单上加时段字段我留了口子”。比起把一堆功能堆成半成品明确说出边界更让老师认可。3. 预订主流程实现Spring Boot MyBatis 怎么查出空房并安全下单3.1 技术选型为什么这类Web项目普遍用Spring Boot MyBatis现在网上能下载到的酒店客房系统源码主流组合就是Spring Boot加MyBatis加MySQL老一点的会碰到JSP加Servlet或者SSH。我的建议是认准Spring Boot MyBatis这套Spring Boot内嵌Tomcat打包成一个jar就能跑不依赖外部Web服务器MyBatis把SQL写在Mapper里连表查询和日期区间一眼能看懂这正好是java面试题里反复考察的MyBatis绑定机制和SQL功底。至于MyBatis-Plus它能根据Java实体类生成建表SQL省手写建表但这种课程设计反而推荐手写CREATE TABLE因为你在建表SQL里能控制索引和字段注释MyBatis-Plus自动生成的那份SQL还要你回头改一大轮省下的时间又还回去了。工程创建不需要去网上找模板。用IDEA 2024版本创建web项目时选Spring InitializrJava 8对应Spring Boot 2.7Java 17对应Spring Boot 3.x依赖勾Spring Web、MyBatis、MySQL Driver、Lombok就够。注意Spring Boot 3.x用的是jakarta.servlet命名空间网上很多老代码还是javax直接搬过来会编译不过。对刚起步的java工程师来说我建议用Spring Boot 2.7毕业设计生态最成熟遇到问题搜得到答案。内嵌Tomcat虽然省事但线上部署时还是要记得设置服务端口和访问日志这是web服务器安全的基本配置别裸奔。3.2 查可用房间的SQL日期交叠条件才是核心难点空房查询是整套系统最容易翻车的SQL。初级写法是先查所有status0的房间然后在Java里循环判断日期是否冲突。这套写法在数据量小的课堂演示里能跑通但每次查询都要把所有空闲房捞进内存而且从“查列表”到“提交订单”之间的空窗期同一间房可能被第二个客人抢走。真正合理的做法是把日期交叠判断下推到SQL的NOT EXISTS子查询里。-- 查询某个日期区间内可用的房间 SELECT r.id, r.room_no, t.type_name, t.base_price FROM room r JOIN room_type t ON r.type_id t.id WHERE r.status 0 AND NOT EXISTS ( SELECT 1 FROM room_order o WHERE o.room_id r.id AND o.status IN (1, 2) -- 已确认、已入住的订单才算占用 AND o.check_in_date #{checkOut} AND o.check_out_date #{checkIn} ) ORDER BY r.room_no;关键在两个不等号o.check_in_date #{checkOut} 且 o.check_out_date #{checkIn}都没有等号。举个例子现有订单是8月1日入住、8月2日离店新请求查8月2日到8月3日的可用性现有订单的入住日8月1日小于我的离店日8月3日这成立但现有订单的离店日8月2日并不大于我的入住日8月2日所以整个子查询不命中这间房在8月2日释放给新房客。这正符合酒店“中午离店下午让新房客入住”的规则。谁要是手欠把大于号改成大于等于房间就会在离店日空置一整天损失就出来了。对应的Mapper可以这样写Select({ script, SELECT r.id, r.room_no, t.type_name, t.base_price, FROM room r JOIN room_type t ON r.type_id t.id, WHERE r.status 0, AND NOT EXISTS (, SELECT 1 FROM room_order o, WHERE o.room_id r.id, AND o.status IN (1, 2), AND o.check_in_date lt; #{checkOut}, AND o.check_out_date gt; #{checkIn}, ), ORDER BY r.room_no, /script }) ListRoomVO listAvailable(Param(checkIn) LocalDate checkIn, Param(checkOut) LocalDate checkOut);参数说明checkIn和checkOut用LocalDate类型MyBatis可以直接映射到SQL的DATE字段不需要额外的类型转换。XML里的和是小于号和大于号的转义写法写在注解里时必须转义否则XML解析直接报错。为什么用NOT EXISTS而不是NOT IN因为NOT IN在子查询结果含NULL时整体返回空集这是一个经典坑。NOT EXISTS只关心有没有命中行语义更安全性能也好。3.3 提交订单事务注解、订单号与状态机查完空房之后提交订单是第二个重头戏。这一步要把“库存扣减”和“订单生成”变成原子操作否则并发下两个人同时抢最后一间房数据库里会出现两条订单。核心方法是Service方法加事务注解并且在插入订单前再把可用性检查一遍不能信任前端页面上的结果。Override Transactional(rollbackFor Exception.class) public String createOrder(OrderRequest req) { // 1. 校验日期离店必须晚于入住这是业务底线 if (!req.getCheckOutDate().isAfter(req.getCheckInDate())) { throw new BizException(离店日期必须晚于入住日期); } // 2. 提交订单时重新查一次可用性防并发超卖 int count roomMapper.countAvailable(req.getRoomId(), req.getCheckInDate(), req.getCheckOutDate()); if (count 0) { throw new BizException(该时段房间已被预订); } // 3. 生成订单号日期时间 随机4位 房间号后3位 String orderNo LocalDateTime.now() .format(DateTimeFormatter.ofPattern(yyyyMMddHHmmss)) String.format(%04d, ThreadLocalRandom.current().nextInt(10000)) (req.getRoomId() % 1000); // 4. 插入订单记录状态默认1已确认 roomOrderMapper.insert(req, orderNo); return orderNo; }逻辑说明第一步校验日期是防御性编程防止接口被绕过页面直接调用时传入非法日期第二步的countAvailable和列表查询是同一套SQL逻辑但这里只需要返回数量命中0就抛异常终止事务。订单号用“时间戳随机数房间号尾巴”拼接长度够、可读性好ThreadLocalRandom在高并发下比Math.random()更合理。Transactional(rollbackFor Exception.class) 表示任何异常都触发回滚Spring默认只回滚RuntimeException如果自定义的BizException没有继承RuntimeException不加这个参数的话事务就不会回滚。订单状态机也要提前定好0待确认、1已确认、2已入住、3已离店、4已取消。散客提交后可以是0或1前台确认变成1到店办理入住变成2退房结账变成3未到店或取消变成4。状态转换不要写成到处可改集中在Service里提供confirm、checkIn、checkOut、cancel几个方法每个方法只允许特定状态跳转这样订单表不会出现“已取消还能办入住”的荒唐数据。4. Web页面与接口联调选房页、预订表单、后台权限控制4.1 JSP还是前后端分离先定页面渲染方案页面层的关键决策是先想清楚用哪种渲染方案。老源码里常见JSP加JSTLJSP能直接在页面里写Java代码但对新手很不友好调试时报错看不懂。新一点的做法是后端接口返回JSON前端用Vue或原生JS渲染。我个人的偏好是如果要快速复现用Spring Boot直接托管静态HTML加原生JS前后端通过接口数据交互如果这个项目你打算长期迭代把前后端彻底拆开更清爽。但不要一个项目里又用JSP又用接口混在一起会让你排查问题时精神分裂。这里我采用“后端返回JSON 前端静态页面”的方案。Controller只负责业务处理不再返回视图路径。一个典型的查询接口长这样RestController RequestMapping(/api/room) public class RoomApiController { GetMapping(/available) public ResultListRoomVO available( RequestParam String checkIn, RequestParam String checkOut) { LocalDate in LocalDate.parse(checkIn); LocalDate out LocalDate.parse(checkOut); ListRoomVO list roomService.listAvailable(in, out); return Result.success(list); } }参数说明checkIn和checkOut用yyyy-MM-dd格式字符串传入正好是HTML日期控件的默认格式LocalDate.parse不用指定格式就能解析。返回统一用Result包装类里面放code、message、data三个字段这是企业级Web开发里的常见约定前端只需要判断code是否为0不用每次处理不同的返回结构。4.2 在线预订页日期控件参数设置与前后端双重校验在线预订页的核心交互是选入住日期和离店日期。常见做法是用日期插件联动选完入住日后离店日控件的最小可选日期自动变成入住日次日同时刷新空房列表。下面以flatpickr为例// 入住日控件不能选今天之前选中后联动刷新空房 flatpickr(#checkIn, { minDate: today, onChange: function (selectedDates) { // 构造入住日次日的时间戳作为离店最小日期 const nextDay selectedDates[0].getTime() 24 * 60 * 60 * 1000; checkout.set(minDate, nextDay); fetchAvailableRooms(); } }); // 离店日控件 const checkout flatpickr(#checkOut, { minDate: today });参数说明minDate设为today保证不能订昨天的房onChange回调里把离店控件的最小日期推到入住日次日避免出现“入住8月1日、离店8月1日”这种无效组合。fetchAvailableRooms是封装好的请求函数把两个日期拼到查询参数里请求第4.1节那个接口。这里有一个常见的边界跨天才是合法预订所以离店最小日期一定要加一天而不是等于入住日。前端限制只影响体验真正的校验必须放后端。我在3.3节写的日期判断即使前端被绕过也能拦住非法请求。前后端各守一道才符合web服务的基本安全习惯。后端校验通过后在Service里抛BizException前端拿到code非0的结果时弹出错误提示而不是往table里硬塞数据。4.3 用拦截器把后台管理页和游客页隔开客房系统的后台管理页不能对游客开放。实现方式有两种Servlet里的Filter或者Spring MVC的HandlerInterceptor。我一般用HandlerInterceptor因为能拿到Controller的handler对象适合做页面级权限控制比如只拦截路径前缀是/admin/的请求。public class AdminInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { HttpSession session req.getSession(); Object admin session.getAttribute(admin); if (admin null) { resp.sendRedirect(req.getContextPath() /login); return false; } return true; } }注册到配置类里指定拦截范围和放行范围Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AdminInterceptor()) .addPathPatterns(/admin/**) .excludePathPatterns(/admin/login, /static/**, /error); } }参数说明addPathPatterns指定拦截所有/admin开头路径excludePathPatterns有三个值必须放行——登录页本身、静态资源目录、Spring Boot默认错误页。新手最容易漏的是放行静态资源导致后台页面打开了但样式和JS全被拦截页面光秃秃一片还以为是路径写错。另外拦截的是“后台管理”功能前台预订页和首页不应该被挡所以它们的路径不要放在/admin下这也是一个设计约定。5. 客房系统常见问题排查乱码、404、时间边界三个高频坑5.1 中文乱码页面显示正常但库里全是问号现象页面上客人姓名显示正常但存进MySQL后变成“???”或者从数据库查出来的中文在页面上变成乱码。这是Java Web新手遇到的第一个见鬼问题很多人的第一反应是重装MySQL其实跟MySQL没关系。原因从浏览器到Servlet再到MySQL编码要在多个环节保持一致。最常见的一处是JDBC连接串没指定characterEncoding数据库默认用了latin1第二处是请求和响应的编码没有统一为UTF-8。Spring Boot自动配了CharacterEncodingFilter但如果没有把force打开它只会处理请求体URL里的参数照样乱。解决把连接串和Spring配置一起改掉。# application.properties 数据库连接串 spring.datasource.urljdbc:mysql://localhost:3306/hotel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai # 强制请求和响应都使用 UTF-8 server.servlet.encoding.charsetUTF-8 server.servlet.encoding.enabledtrue server.servlet.encoding.forcetrue参数说明useUnicode和characterEncoding必须成对出现只写一个不生效serverTimezoneAsia/Shanghai同时解决日期少8小时的问题forcetrue表示即使客户端没声明编码也强制按UTF-8处理。建表时再用utf8mb4字符集这一整套串起来中文才保得住。排查顺序建议是先看数据库连接串再看表和字段字符集最后看页面meta标签这三处都一致的话乱码基本能绝迹。5.2 页面404与静态资源加载失败现象访问首页正常但点进后台管理页直接404或者页面出来了CSS和JS全部失效整个页面像没穿衣服。原因第一类404是Controller映射路径写错比如把available写成了avaliable单词拼错这种低级坑。第二类是返回视图名和模板实际路径对不上Thymeleaf的视图解析器默认去templates目录下找同名文件多一层少一层都不行。第三类是拦截器把静态资源拦了样式请求返回403或404。解决打开浏览器开发者工具看Network面板里具体是哪个URL返回404然后用这个URL反推。如果是静态资源检查它是否放在classpath:/static/下以及路径里有没有拼错目录如果URL正确还是404检查拦截器配置把/static/**加进excludePathPatterns。别靠眼睛盯代码直接看浏览器请求链路这是最快的办法。我见过有人为了修一个CSS 404把整个静态资源目录换了个位置结果越改越乱——其实只是拦截器少放行了一个路径。5.3 离店日当天房间重复预订时间边界写错了现象客人A订了8月1日到8月2日客人B订8月2日到8月3日系统提示B预订失败理由是房间被占用。按酒店规则8月2日中午前A退房这间房晚上可以给B入住不应该冲突。原因SQL里的日期比较用了小于等于或大于等于把离店日当天也算成了房间占用时间。也就是说系统认为8月2日这天A和B同时都在住这自然就撞上了。还有一种隐含问题离店时间在业务上约定为当天中午但订单表存的是DATE具体几点退房并没有记录。解决把日期判断统一改为半开区间现有订单的离店日严格小于新订单的入住日时才释放o.check_in_date #{checkOut} AND o.check_out_date #{checkIn}这行规则必须写进代码注释因为后面维护的人大概率会手欠把它改成和一改就原地翻车。同时退房结算时要把房间状态置为2脏房而不是直接回到0空闲等保洁完成再改成0这样房态图才真实。提示日期区间判断是订单类系统的通用逻辑换成会议室预订、车辆预订也一样成立。最好把它抽成一个工具方法或统一SQL片段避免在多个地方重复写改边界时只改一处。5.4 下单报错但订单还是插入成功事务没回滚现象调用下单接口返回500但数据库里订单表还是多出一条记录甚至房间状态也被改了。客人没付钱房却占上了。原因Transactional失效。最常见的三种一是同类内部自调用比如OrderService里的方法A调用同一个类里的方法BB上有Transactional但Spring事务是基于动态代理实现的内部调用绕过了代理对象注解形同虚设二是方法被final修饰CGLIB代理无法重写事务注解同样不生效三是方法里catch了所有异常并吞掉不往外抛Spring只对“方法正常结束”或“抛出指定异常”两种结局判断是提交还是回滚异常被吞了就无法触发回滚。解决把事务方法放到另一个Service里或者让入口方法持有事务保证方法是public且非final异常要重新抛出不要catch后默默返回null。验证方法很简单在下单逻辑里故意抛一个BizException调用后看日志里有没有“Rolling back”行再看库里有没有脏数据。这个验证动作我在第6章还会讲答辩时它就是你的实战证据。6. 给客房系统加一个答辩级亮点营收统计口径与事务验证前面把主流程跑通后系统已经能用了但离“答辩能讲出东西”还差一步。我最推荐加的功能是每日营收统计。别小看这张报表它能把订单表、入住登记表、事务正确性三个点串起来而且实现成本很低。统计口径一定要定义清楚只统计“已离店且完成结算”的订单也就是room_order.status等于3并且check_in_record里有实际离店时间。如果把未入住的预订单也算进去报表数字会虚高老师追问一句“你这营收包含明天才入住的订单吗”就露馅了。SELECT DATE_FORMAT(c.actual_check_out, %Y-%m-%d) AS day, COUNT(*) AS settled_orders, SUM(o.price) AS revenue FROM room_order o JOIN check_in_record c ON o.id c.order_id WHERE o.status 3 GROUP BY day ORDER BY day DESC;GROUP BY day按天聚合SUM(o.price)用的是订单快照价DATE_FORMAT把datetime转成yyyy-MM-dd。这张表能回答三个经营问题哪几天生意好、平均房价多少、总共确认了多少间夜。配合一个简单的柱状图或表格它就成为一个完整的后台审计视图。事务验证也值得作为演示环节。写一个临时测试入口在insert订单之前故意抛一个BizException调用后用SQL查库确认room_order没有新增、room.status没有变化日志里出现Rolling back。把这个验证过程在答辩现场演示比任何截图都管用因为它是“人还在操作、数据没坏”的直接证明。我早年做这类Web项目时习惯先把成功路径跑通然后截图放进PPT里就以为万事大吉结果答辩时被问“你的下单接口如果中途断网了会怎样”我只能愣在台上。后来我养成一个习惯每次写完一个涉及金额或库存的接口先写一条必然失败的测试用例再写成功用例。这个习惯帮我躲过了不少线上翻车也让我在讲项目时心里有底敢主动聊边界和异常场景而不是等着老师来挑毛病。希望帮到你。本文还有配套的精品资源点击获取