ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM酒店客房管理系统实战:从数据库设计到并发控制

SSM酒店客房管理系统实战:从数据库设计到并发控制 马上要交课程设计了选题还没定或者定了但不知道从哪下手如果你正在Java方向找管理系统类课题酒店客房管理系统其实被很多人低估了。别把它想象成普通的学生信息增删改查——真实的酒店业务里房间状态要不停流转客人住店时间会互相重叠房费结算也分好几种规则。把这些业务规则用SSMSpring SpringMVC MyBatis框架落到代码里一个学期的Java知识基本就串起来了。这篇文章不打算写成课程设计说明书而是以“java_ssm62酒店客房管理系统”为例讲清楚这类系统从数据库设计、后端分层、并发控制到答辩回答的完整思路。文中所有代码和方案都来自我实际做过、调试过的项目适合正在做Java课程设计的学生也适合想用SSM把CRUD项目做出层次感的开发者。不管你是刚开始学Java基础、还没把框架集成跑通还是已经被数据一致性、并发这些概念绕晕这篇文章都能给你一套能直接照做的方案。1. 为什么选酒店客房管理做课程设计业务模型比CRUD值钱1.1 看似简单的管理系统藏着完整的状态机我见过太多人做“学生管理系统”或“图书管理系统”最后的成果就是四个操作加一条、删一条、改一条、按关键字查一条。不能说没用但面试官听完基本记不住任何亮点。酒店客房管理不一样它的核心业务天然带着状态变化一间房从空闲变成已预订客人到店后从已预订变成已入住退房结账后又回到空闲中间可能还穿插维修、清洁等状态。这其实就是一个小型状态机。你不需要专门学状态机理论但把这个业务实现一遍你会自然理解“状态流转需要约束、需要校验、需要事务保护”。比如你不可能让一间已入住的房间被另一个客户直接预订也不可能让一间维修中的房间分配给前台。这些规则写进代码项目就有了“业务逻辑”而不是单纯的“数据操作”。另外这个课题还能锻炼面向对象建模能力。房型、房间、客户、预订单、入住单、退房记录每张表对应一个Java实体类实体之间的关系对应外键或业务关联字段。用Java面向对象编程的思路去设计这些类后面写Service和Controller会顺手很多。1.2 技术路线为什么用SSM而不是直接手写JSPServlet有些课程设计还在用JSP文件里直接写Java脚本的方式页面里塞一堆%和%请求处理靠Servlet硬编码。这种写法能运行但代码全揉在一起改一个功能要翻好几个文件答辩时也很难说清楚分层思想。SSM的价值在于三个层次各管一摊Spring负责对象管理IoC和事务管理AOPService、Mapper这些对象由容器创建和注入你不用到处new。SpringMVC负责Web层处理请求映射、参数绑定、返回视图或JSON数据。MyBatis负责数据库访问把SQL写在XML映射文件里和Java代码分离。如果你以后要学Spring BootSSM正好是它的底层框架。学SSM期间你手动配置过数据源、事务管理器、MyBatis扫描这些再去理解Spring Boot的自动配置就轻松很多。所以课程设计选SSM不是走弯路是给后面铺路。1.3 功能清单哪些必须有哪些是加分项做这类系统最忌讳一上来就打开IDE写代码。先定功能范围我建议按这个优先级来功能模块优先级说明管理员登录/权限必须有至少做简单登录认证区分管理员和前台房型管理必须有房型名称、价格、床型、面积、最大入住人数房间管理必须有房间号、楼层、所属房型、房间状态、清洁状态客房预订必须有客户信息 房型 入住/离店日期 间数入住登记必须有到店后把预订或直接散客分配合适房间退房结账必须有根据价格、会员折扣、超时情况计算最终费用订单查询必须有按日期/手机号/状态查订单列表统计报表建议做月度入住率、营收统计用SQL分组实现会员等级加分项不同会员折扣不同配合策略模式计算房费钟点房计费加分项按小时计费和全天入住区分开房间清洁状态加分项退房后待打扫打扫完再变为可售核心闭环是“房型→房间→预订→入住→退房→统计”先把这条线跑通再考虑加分项。我当年就是先写预订后来发现退房还要算钱、算完还要改房态被迫回头改表结构前前后后改了三轮所以强烈建议前期把状态关联设计放第一位。2. 数据库建模房态、客户、订单三块表怎么关联2.1 五张核心表两张扩展表酒店客房管理系统的数据库设计我最终定下来是这么几张表t_admin管理员账号表字段有 id、username、password、real_name、role、create_time。t_room_type房型表字段有 id、type_name、base_price、bed_num、area、max_people、remark。t_room房间表字段有 id、room_no、floor、room_type_id、room_status、clean_status、version、create_time。t_customer客户表字段有 id、name、id_card、phone、member_level、discount_rate。t_reserve预订表字段有 id、reserve_no、customer_id、room_type_id、room_num、in_date、out_date、status、create_time。t_checkin入住登记表字段有 id、reserve_id、customer_id、room_id、in_date、expect_out_date、deposit、status。t_checkout退房记录表字段有 id、checkin_id、out_date、total_amount、actual_out_date。可选扩展表还有 t_member会员表、t_fee_rule费率规则表。课程设计阶段建议至少把前五张表和退房记录表建好会员和费率规则作为加分项。这里要注意一个重要关系预订表存的是“房型 间数”不直接指定具体房间号。因为客人预订时酒店还没决定具体给哪一间只是锁定额外房间数量。入住登记时才把具体房间号 t_checkin.room_id 定下来。这一点答辩常被追问如果你把 t_reserve 里直接存 room_id反而暴露了业务理解不够的问题。2.2 房态字段别用魔法数字用枚举t_room 表里的 room_status 字段我建议用 tinyint 存 0、1、2、3分别表示空闲、已预订、已入住、维修。但代码里不要到处写数字推荐定义一个枚举类public enum RoomStatus { FREE(0, 空闲), RESERVED(1, 已预订), CHECKED_IN(2, 已入住), MAINTENANCE(3, 维修); private final int code; private final String desc; RoomStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }用枚举的好处是代码里写RoomStatus.FREE.getCode()比直接写0可读性强很多也不会出现“0到底是空闲还是维修”这种迷惑。数据库里存数字没问题那是为了查询和索引效率但Java代码层面必须语义化。这也是“说人话”在代码里的体现。另外一个容易踩坑的点清洁状态clean_status一定要和房间状态分开。之前有同学把“已打扫”和“空闲”合成一个字段结果前台想分配房间时系统无法区分“空闲但还没打扫”和“空闲且已打扫”最后只能把所有房间都当成可售卫生问题完全没法管理。两个维度就是两个字段房间状态管能不能住清洁状态管能不能卖。2.3 外键和索引课程设计阶段我建议加很多生产项目强调性能会故意去掉外键由应用层保证数据一致性。但课程设计阶段我建议保留外键。理由很实际答辩时你能对着表结构清楚说明“t_room.room_type_id 关联到 t_room_type.id是一对多关系”而且 MySQL 的 InnoDB 在删除父表记录时如果存在子表引用会报错相当于数据库层面多了道保护防止你误删已经关联的房间类型。索引方面至少给这些字段建索引t_room.room_no唯一索引房间号不能重复。t_reserve.customer_id 和 t_reserve.status组合索引或单列索引因为查询订单常按客户和时间过滤。t_checkin.room_id查询某房间历史入住记录会用到。t_reserve.in_date / out_date查“某个时间段内哪些房型被占用”是核心查询。2.4 初始化SQL片段这里给一段建核心表的SQL你可以直接改库名使用。字段类型我按 MySQL 5.7/8.0 写CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE hotel_db; CREATE TABLE t_room_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL COMMENT 房型名称, base_price DECIMAL(10,2) NOT NULL COMMENT 基础价格, bed_num INT DEFAULT 1 COMMENT 床数, area DECIMAL(6,1) DEFAULT 0 COMMENT 面积平米, max_people INT DEFAULT 2 COMMENT 最大入住人数, remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT房型表; CREATE TABLE t_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE COMMENT 房间号, floor INT NOT NULL COMMENT 楼层, room_type_id INT NOT NULL COMMENT 房型id, room_status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1已预订 2已入住 3维修, clean_status TINYINT NOT NULL DEFAULT 1 COMMENT 0待打扫 1已打扫, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_room_type FOREIGN KEY (room_type_id) REFERENCES t_room_type(id) ) ENGINEInnoDB COMMENT房间表; CREATE TABLE t_customer ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, id_card VARCHAR(18) NOT NULL COMMENT 身份证号, phone VARCHAR(20), member_level TINYINT DEFAULT 0 COMMENT 0普通 1银卡 2金卡, discount_rate DECIMAL(4,2) DEFAULT 1.00 COMMENT 折扣率0.85表示85折, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT客户表; CREATE TABLE t_reserve ( id INT PRIMARY KEY AUTO_INCREMENT, reserve_no VARCHAR(30) NOT NULL UNIQUE COMMENT 预订单号, customer_id INT NOT NULL, room_type_id INT NOT NULL, room_num INT NOT NULL DEFAULT 1 COMMENT 预订间数, in_date DATE NOT NULL COMMENT 入住日期, out_date DATE NOT NULL COMMENT 离店日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 0已预订 1已入住 2已取消 3已离店, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_reserve_customer FOREIGN KEY (customer_id) REFERENCES t_customer(id), CONSTRAINT fk_reserve_room_type FOREIGN KEY (room_type_id) REFERENCES t_room_type(id) ) ENGINEInnoDB COMMENT预订表;注意 DECIMAL 字段尤其是价格千万别用 DOUBLE。后面第6节会专门讲为什么简单说就是二进制浮点数算钱会产生精度错误课程设计里这算一个很值钱的细节。3. SSM三层调用链一份预订请求从前端到数据库的完整旅程3.1 Controller层先做参数收口和格式统一以“用户提交预订”为例前端表单提交客户姓名、手机号、身份证、房型ID、入住日期、离店日期、间数。Controller 层要做的事有三件接收参数、校验参数、调用Service。日期格式是这里最常见的坑。前端传过来的入住日期是字符串2025-06-01Java 里不要用java.util.Date去接更不要用SimpleDateFormat到处解析直接用LocalDate加DateTimeFormat注解Controller RequestMapping(/reserve) public class ReserveController { Autowired private ReserveService reserveService; PostMapping(/add) ResponseBody public Result add(ReserveAddDTO dto) { // DTO里已经用 DateTimeFormat(pattern yyyy-MM-dd) 接收 LocalDate if (dto.getOutDate().isBefore(dto.getInDate())) { return Result.error(离店日期必须晚于入住日期); } reserveService.createReserve(dto); return Result.success(); } }参数校验在课程设计阶段至少做到非空判断、离店日期大于入住日期、间数大于0。用 Hibernate Validator 的NotNull、Future注解会更正规但手写判断也够用关键是别跳过。Controller 里尽量不要出现 SQL、不要直接操作数据库对象它只做“消息翻译”把HTTP参数转成DTO把Service返回结果包装成前端要的JSON。这个习惯坚持住项目结构会清爽很多。3.2 Service层业务规则必须在这里事务边界也在这里这是SSM分层里最核心的一环。预订业务在Service层至少要做这几步根据 roomTypeId 查房型确认房型存在。根据入住、离店日期查询该房型在时间段内的可售房间数量。如果可用数量大于等于预订间数插入 t_reserve 记录。把对应房型下空闲状态的房间标记为已预订或者至少锁定额外数量。所有步骤要么全成功要么全失败。第5点靠的就是事务。在实现类方法上加TransactionalService public class ReserveServiceImpl implements ReserveService { Autowired private ReserveMapper reserveMapper; Autowired private RoomMapper roomMapper; Override Transactional(rollbackFor Exception.class) public void createReserve(ReserveAddDTO dto) { // 1. 查询房型 // 2. 查询可售房间数不足就抛业务异常 // 3. 插入预订记录 // 4. 更新房间状态 } }这里有个容易被忽视的技术细节Transactional默认只对 RuntimeException 和 Error 回滚。如果你的业务异常是自定义的BizException extends Exception事务不会回滚数据就处于“预订记录存在但房间状态没改”的中间状态。所以要么让自定义异常继承 RuntimeException要么在注解里显式写rollbackFor Exception.class。我建议两种都了解答辩能说清楚区别面试也很爱问。事务放在Service实现类上不要放在Controller上也不建议只放在Mapper接口上。Mapper层的每个方法都是独立SQL如果只给Mapper加事务一个业务跨多个Mapper方法时就管不住了。放Controller上则事务范围太宽而且SpringMVC的Controller默认是单例业务边界不清晰。放在Service实现类上是最合适的粒度。3.3 Mapper层与XML动态SQL解决多条件组合查询MyBatis的Mapper接口写法大家都熟但XML里动态SQL才是真正值钱的地方。比如订单查询页面用户可能按“手机号”“日期范围”“状态”“房型”任意组合筛选如果每个条件都写一个SQL方法组合会爆炸正确做法是动态拼接select idselectReserveList resultTypecom.hotel.entity.Reserve SELECT id, reserve_no, customer_id, room_type_id, room_num, in_date, out_date, status, create_time FROM t_reserve where if testphone ! null and phone ! AND customer_id IN (SELECT id FROM t_customer WHERE phone #{phone}) /if if teststatus ! null AND status #{status} /if if testroomTypeId ! null AND room_type_id #{roomTypeId} /if if teststartDate ! null AND out_date gt; #{startDate} /if if testendDate ! null AND in_date lt; #{endDate} /if /where ORDER BY create_time DESC /selectwhere标签会自动去掉第一个多余的 ANDif判断条件是否拼进来。这样一套逻辑搞定所有组合查询不用写十几个方法。实体映射方面有个高频坑如果表字段是room_type_id实体属性是roomTypeIdMyBatis默认不会自动转驼峰。必须在配置文件里开启setting namemapUnderscoreToCamelCase valuetrue/不开启的话查出来的 roomTypeId 全是 null页面一片空白排查半天找不到原因。这类问题我在第7节会再展开讲。4. 房间预订与入住的时间冲突判断容易被忽视的边界条件4.1 重叠区间判断的标准写法做酒店客房管理系统最大的业务陷阱是“同一间房在同一个时间段能不能被两个人预订”。很多人第一反应是写“新的入住日期不能在已有预订的开始和结束之间”但这个条件不完整。正确判断两个时间段[a, b)和[c, d)是否重叠数学条件是a d 且 c b翻译成业务语言新预订的入住日期要早于已有预订的离店日期且已有预订的入住日期要早于新预订的离店日期。SQL 里查某个房型在指定时间段内被占用的数量核心片段是这样SELECT COUNT(*) FROM t_reserve WHERE room_type_id #{roomTypeId} AND status IN (0, 1) AND in_date #{outDate} AND out_date #{inDate}拿实际例子验证一下已有预订是6月1日到6月3日。新预订6月3日到6月5日in_date outDate即6月1日6月5日成立out_date inDate即6月3日6月3日不成立等值不算重叠所以不冲突可以订。这符合酒店惯例6月3日中午退房新房客6月3日中午后入住完全没问题。但如果你写成out_date inDate就会把6月3日入住的客人拒之门外其实6月3日只是和退房客人在同一天交接并不冲突。很多课程设计在这里写错入住率和客流都会出问题。4.2 跨天、钟点房、续住的边界日期字段比较只是第一层实际业务还有几类场景必须单独处理跨天住宿客人晚上22点到店订的是当天入住次天离店。预订记录里的 in_date 和 out_date 都是日期没问题但退房截止时间通常是每天中午12点。如果只按日期算费用容易把“住一晚”和“住两天”算错。钟点房有些客房按小时卖入住时间是“2025-06-01 14:00”离店时间是“2025-06-01 18:00”这两个时间在同一天但区间只有4小时。如果用 DATE 类型存系统会认为这单占用了一整天导致很多本可以接的钟点房被锁死。解决方法是给预订表加一个is_hourly标志钟点房单走一套时间逻辑用 DATETIME 类型。续住客人到店后想多住一天不能直接改 out_date要先检查该房间后续时间段是否已有别的预订。这个检查和预订时的区间判断完全一样只是操作对象从“房型房间数量”变成“具体某一间房”。4.3 我踩过的坑不要用字符串比日期有一版功能我图省事直接用前端传的字符串2025-06-01和数据库层的字符串字段比较当月测试一切正常结果跨月就出了诡异bug。原因是2025-07-01和2025-06-15做字符串比较时字符 6 大于 7 吗不6 比 7 小所以2025-06-15 2025-07-01按字典序反而成立看起来“6月15日在7月1日之前”是对的但换成2025-10-01和2025-09-15时字符串比较09和10又出问题了。日期这类时间语义永远要转成 LocalDate 或专门的日期类型再比较别信字符串的字典序。4.4 状态机流转约束房间状态的流转不是随便改的得按业务规则来。我总结成下面这条线空闲 - 已预订客人提交预订成功。已预订 - 已入住客人到店办理入住。已入住 - 空闲客人退房结账完成。空闲/已预订 - 维修管理员报修房间。维修 - 空闲维修完成恢复可售。已入住 - 维修一般不允许客人还没走不能抢修只能等退房。这些约束放在Service层统一封装比如RoomStateService.checkIn(roomId)、checkOut(roomId)。犯错的人通常是直接写一个roomMapper.updateRoomStatus(roomId, status)到处传值结果页面里一个按钮就能把已入住的房间改成维修业务彻底乱掉。正确做法是每个状态变更都是一个独立的业务方法方法里先做状态校验再执行更新。5. 并发与数据一致性课程设计里也躲不开的两个关键词5.1 典型场景两个人同时订了同一个房间很多课程设计做完看起来功能都正常但一开两个浏览器窗口同时提交预订就会出现超卖——一个只剩2间标准间两个客户同时下单各订1间系统可能两张单都成功但实际上只有1间可售了。这不是Java代码逻辑写得对不对的问题而是并发时序问题两个请求同时查库存查到的都是“还有2间”然后各自写各自的预订记录谁也没拦住谁。面试官问“你怎么保证数据一致性”指的就是这类场景。不管是单机项目还是微服务只要多个线程同时读写共享数据就必须考虑并发控制。5.2 第一道防线事务约束事务能保证“先查后写”的原子性让多个SQL操作要么全部成功、要么全部回滚。比如预订单插入和房间状态更新放在同一个事务里就不会出现“订单建了但房间状态没改”的情况。但事务本身解决不了“两个请求同时查到库存2间”的问题因为两个查询在时间上完全交错必须配合锁机制。5.3 乐观锁version字段的完整写法课程设计阶段最优雅、也最容易讲清楚的方案是乐观锁。给 t_room 表加一个version字段每次更新房间状态时带上版本号UPDATE t_room SET room_status 1, version version 1 WHERE id #{roomId} AND version #{oldVersion}Java 侧的逻辑是这样public boolean tryLockRoom(Integer roomId) { Room room roomMapper.selectRoomForUpdate(roomId); int oldVersion room.getVersion(); int rows roomMapper.updateStatusWithVersion(roomId, RoomStatus.RESERVED.getCode(), oldVersion); return rows 1; }如果rows 0说明在执行 UPDATE 之前这条记录的 version 已经被别的线程改掉了也就是房间被别人抢了。这时程序要提示用户“该房间刚刚被预订请重新选择”而不是继续往下走。乐观锁的适用前提是“冲突不频繁”读了数据的人能接受偶尔更新失败重试。酒店预订显然符合这个特征而且实现简单不用长时间占用数据库行锁性能也好。答辩被问到并发控制时能把这个原理讲清楚已经比大多数课程设计项目高一个档次。5.4 悲观锁SELECT FOR UPDATE 什么时候用和乐观锁对应的是悲观锁查询时直接把行锁住其他事务必须等待SELECT * FROM t_room WHERE id #{roomId} FOR UPDATE;在事务里执行这条语句后其他线程对同一行的 UPDATE 或 SELECT FOR UPDATE 都会阻塞直到当前事务提交或回滚。悲观锁写起来更直白逻辑上最安全但缺点是并发性能差行锁等待时间一长容易拖垮数据库。我的建议是课程设计优先做乐观锁把version字段和更新失败重试的逻辑写完整面试时再补一句“如果并发量高到乐观锁重试频繁才会考虑悲观锁或队列化处理”这样既落地了方案又能体现你懂取舍。说到锁如果你后续去看 JUC 源码会发现 ReentrantLock、Semaphore、CountDownLatch 这些工具底层都依赖 AQSAbstractQueuedSynchronizer。课程设计阶段不会直接用 AQS但理解它之后再看锁的设计会通透很多。6. 计费模块设计用策略模式把房费算明白6.1 需求先拆解普通会员钟点房三种计费规则如果只做一个“单价乘天数”酒店管理系统就少了一个很大的亮点。真实的计费至少要覆盖三种情况普通入住基础价格 × 住宿天数。会员入住基础价格 × 天数 × 会员折扣。钟点房按小时累计通常是首小时一口价超时部分按每小时价格累加。再加上退房超时的场景标准离店时间是中午12点如果客人下午14点才退房有些酒店会加收半天房费。这些规则叠在一起如果用一堆 if-else 写在退房方法里代码会越来越难维护。这里很适合用设计模式里的策略模式。6.2 策略接口与三个实现类先定义一个策略接口public interface FeeStrategy { BigDecimal calculate(FeeContext context); }FeeContext 里放好计算所需的数据基础价格、入住天数、钟点时长、会员折扣、超时小时数等。然后三个实现类public class NormalFeeStrategy implements FeeStrategy { Override public BigDecimal calculate(FeeContext context) { return context.getBasePrice() .multiply(BigDecimal.valueOf(context.getDays())); } } public class MemberFeeStrategy implements FeeStrategy { Override public BigDecimal calculate(FeeContext context) { return context.getBasePrice() .multiply(BigDecimal.valueOf(context.getDays())) .multiply(context.getDiscountRate()); } } public class HourlyFeeStrategy implements FeeStrategy { Override public BigDecimal calculate(FeeContext context) { BigDecimal firstHourPrice context.getBasePrice(); BigDecimal extraHourPrice context.getBasePrice() .multiply(new BigDecimal(0.5)); long hours context.getHourlyDuration(); if (hours 1) { return firstHourPrice; } return firstHourPrice.add(extraHourPrice.multiply(BigDecimal.valueOf(hours - 1))); } }再写一个FeeContextFactory或FeeStrategyFactory根据客户类型和订单类型返回对应策略。这样退房结账的代码几乎没有 if-else将来新增“团队协议价”“节假日价”只需要加实现类不改核心逻辑。面试问“设计模式你怎么用的”这就是一个真实的、能说透使用场景的例子比背概念强。6.3 费率数据放数据库不要写死在代码里策略类只负责“怎么算”具体单价、折扣率应该从数据库查出来。房型表里存 base_price客户表里存 discount_rate钟点房加价规则可以放费率规则表 t_fee_rule。这样酒店调整价格时改数据库就行不用重新编译部署。答辩时这个细节非常加分它体现的是可维护性思维。我在实际项目里见过有人把价格直接写在 Java 常量类里后来改价只能改代码重新打包这显然不符合真实酒店需求。放数据库一开始多写一次查询长远省很多事。6.4 BigDecimal 精度坑金额计算必须用 BigDecimal这是铁律。两个注意点第一创建 BigDecimal 不要用构造函数传 doublenew BigDecimal(0.1)得到的不是精确的0.1而是0.1000000000000000055511151231257827021181583404541015625。正确写法是new BigDecimal(0.1)或者用BigDecimal.valueOf(0.1)。第二除法时要指定精度和舍入模式否则除不尽会抛 ArithmeticException。比如算日均费用BigDecimal daily totalAmount.divide( BigDecimal.valueOf(days), 2, RoundingMode.HALF_UP );用 double 算钱0.1加0.2都可能不等于0.3更不用说累加房费、折扣、押金。这一节虽然基础但你在答辩现场说出“金额用 BigDecimal不能使用浮点数”考官基本会点头。7. 前端联动与调试经验让表单不吵架让列表不白屏7.1 前端选型JSPBootstrap 还是前后端分离课程设计阶段我推荐用 JSP Bootstrap。理由很直接业务量不大JSP 服务端渲染能顺带把数据显示完Bootstrap 样式可以让页面不至于太难看而且服务器端渲染避免了前后端分离带来的跨域配置、接口联调这些额外成本。你不需要证明自己会微服务你要证明的是业务闭环完整。如果已经会 Vue也可以用 Vue axios后端返回 JSON。但一定要注意SpringMVC 返回对象时要加ResponseBody或RestController不然 Spring 会把返回值当视图名解析页面就白屏了。7.2 核心联动场景选房型、选日期、查可订前端最典型的交互是“预订表单联动”。我的页面布局是房型下拉框、入住日期、离店日期、间数四个字段变化后后台实时返回可订数量并显示价格。实现逻辑不复杂用原生 JS 或 jQuery 发一个 ajax 请求function checkAvailable() { var roomTypeId $(#roomTypeId).val(); var inDate $(#inDate).val(); var outDate $(#outDate).val(); if (!roomTypeId || !inDate || !outDate) return; if (outDate inDate) { $(#tip).text(离店日期必须晚于入住日期); return; } $.get(/reserve/available, { roomTypeId: roomTypeId, inDate: inDate, outDate: outDate }, function (data) { if (data.code 0) { $(#tip).text(可订 data.data.available 间价格 data.data.totalPrice); } else { $(#tip).text(data.msg); } }); }后端对应接口要重新走一遍“区间重叠判断”前端提示只能作为体验优化后端校验才是业务保障。如果列表需要过滤、排序Java 8 的 Stream 和 Lambda 很常用比如按创建时间排序ListReserve sorted list.stream() .sorted(Comparator.comparing(Reserve::getCreateTime).reversed()) .collect(Collectors.toList());能写 SQL 时就写 SQL少在内存里做过滤数据量一大性能差别很明显。面试被问到排序算法时冒泡排序、快排这些基础要会手写但实际业务排序优先交给数据库。7.3 列表空白或乱码三个方向排查页面数据不对是最消耗耐心的事。我的排查链路固定为三条第一先看浏览器 Network 面板请求返回状态码是 200 还是 500。500 就去翻 Tomcat 日志优先看最底层的 Caused by不要盯着前面一长串堆栈发愣。第二确认接口返回的是 JSON 还是视图名。如果 Controller 方法没加ResponseBody返回一个对象时 Spring 会尝试找同名 JSP 页面找不到就报 404 或白屏。这个错误太常见了。第三检查数据库连接串。MySQL 8 之前版本时区问题会导致日期字段返回少 8 小时连接串要带上jdbc:mysql://localhost:3306/hotel_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai编码问题则是统一入口JSP 页面顶部pageEncodingUTF-8响应头ContentTypetext/html;charsetUTF-8数据库连接串characterEncodingutf8三处一致中文十有八九就不会乱码。7.4 日志与调试用对工具能少熬一个通宵很多课程设计代码里全是System.out.println控制台一滚屏啥都看不清。建议接一个 slf4j logback 的组合代码里用占位符输出没有字符串拼接损耗。拼接大量日志时用 StringBuilder 而不是一堆字符串号这是 Java 基础里经常被问到的点实际开发也真是这么用的Logger log LoggerFactory.getLogger(ReserveServiceImpl.class); log.info(创建预订 | 客户id:{} | 房型id:{} | 日期:{}~{}, dto.getCustomerId(), dto.getRoomTypeId(), dto.getInDate(), dto.getOutDate());再把 MyBatis 的 SQL 日志级别调到 DEBUG每个 SQL 的执行参数就都能看到。排查“cond 查出来是 null”这类问题看 SQL 日志比盲改代码快得多。8. 答辩复盘考官追问最多的八类问题8.1 高频问题清单与回答方向做了几十个类似项目梳理后答辩现场被问到的高频问题基本是这八类提前准备能稳很多表之间是什么关系房型对房间是一对多客户对预订单是一对多预订到入住再对退房是逐级关联。讲的时候先画概念关系别埋头念字段。为什么要加外键课程设计阶段外键能保证数据完整性防止删除房型时留下悬空房间。同时提一句生产环境可能为了性能和分库分表去掉外键由应用层保证一致性。房间状态是谁维护的Service 层通过独立方法维护Controller 不直接改房间状态字段。要展示 checkIn、checkOut 这些方法里的状态校验逻辑。同一房间重复预订怎么防讲乐观锁 version 机制再说 SELECT FOR UPDATE 作为备选方案。事务加在哪层为什么Service 实现类方法上因为一个业务方法往往跨多个 Mapper 操作事务粒度要覆盖完整业务单元。注意补充自定义异常要继承 RuntimeException或者显式配置 rollbackFor。如果真上线哪里要改数据库肯定要加索引、去掉外键约束、连接池配置、密码加密存储日志接入完整监控房间状态变更增加操作记录表操作日志、审计字段。这些是很好的加分答案。房费怎么算普通、会员、钟点房用策略模式费率配置在数据库里金额用 BigDecimal 计算。报表怎么统计按月统计入住率用 SQL 分组和条件聚合比如按年月分组统计已入住房晚数除以总房晚数。给一个简单示例题思路不用现场完全写出来。8.2 不会答的问题比硬编答案更安全答辩不是要你满分项目真实度比完美度重要。遇到不清楚的问题最稳妥的回答方式是“这块我当时做了简化处理没有考虑到 XX。如果现在重新设计我会往 XX 方向改进。” 这个回答模板听起来像反思不像编造。最怕的是现场开始胡编一个自己都没跑通过的方案考官多追问两句直接露馅那比承认不足严重得多。8.3 我个人的课程设计心得这个项目做下来我最深的体会是数据库设计花的时间决定了后面编码是不是顺畅。我第一版直接对着页面写表结果做到退房结账时发现缺少会员折扣字段又回去改表、改实体、改Mapper前后返工了很久。后来学乖了先把业务状态流转、计费规则、并发场景都列出来再去定表结构一切顺多了。你可能觉得课程设计只是个作业能跑就行。但如果你肯把上面这些细节都做进去这个项目完全可以写进简历面试时作为“能讲清楚业务、能应对并发追问、用了设计模式”的项目案例。SSM 这个技术栈看起来不算最新但底子打得越扎实后面学 Spring Boot、微服务的时候就越不吃力。酒店客房管理系统这个题目不算新鲜但你能把它做深一层它就能比别人多值很多分。
返回列表