资讯详情

资讯详情

酒店管理系统课程设计:房态日历、订单状态机与并发防超卖

简介酒店管理系统是一套面向课程设计与毕业设计场景的数据库管理类项目适合计算机专业学生用来理解从需求调查、逻辑模型设计到数据库搭建、编码与测试的完整开发流程。压缩包共收录52个文件整体约10.63MB以C源码为主体包含17个h头文件与16个cpp实现文件配合sln、vcproj等Visual Studio工程文件可直接组织编译另有软件工程报告书、分工说明等doc与txt文档以及rc资源、ico图标和db数据库文件覆盖界面资源与数据存储环节。项目围绕客房管理、入住登记、住客查询、退房、房间预订和系统用户管理等功能展开代码按登录、房态查询、预订入住、退房结算、房间与房型维护等对话框模块拆分便于对照软件工程报告梳理业务流程与数据库表关系。已有151人学习适合需要课程设计参考、功能模块拆解或C数据库项目练手的读者。1. 一间房被卖两次酒店管理系统真正要守的那条线答辩现场评委经常不问功能只挑一个最不起眼的点两位前台同时给最后一间大床房下单会怎样。酒店管理系统的功能列表看着平平无奇——房型、房间、订单、入住、退房、账单可真要交出一份能过课程设计、也能当毕业设计底稿的完整方案决定分数高低的恰恰是这类状态一致性问题。这套方案面向三类人两周内要交课程设计的同学、想把选题做深一层的毕业设计者以及要给小型门店交付一套轻量管理系统的开发者。它不追求做成连锁集团的中央预订系统而是围绕「一间房在任意时刻只属于一个有效订单」这条主线把需求边界、表结构、状态机、核心代码和验收标准串成一条能复现的路径。2. 需求边界与角色权限从一张房态表倒推功能清单很多同学写需求文档是从「管理员可以增删改查」开始的写到最后发现做了二十个页面答辩时讲不出主线。我一般反过来做先把房态这件事说清楚——一间房在某个日期区间里是空闲、被预订、还是在住——然后问一句「谁有权改变这个状态」功能清单自然就长出来了。这一章不堆功能点只把角色、动线、边界和权限四件事定下来后面所有表结构和接口都从这里推导。2.1 三类角色与一条贯穿全局的业务动线小体量酒店管理系统的角色不用设计得很复杂三个就够前台接待、客房服务员、店长管理员。如果毕业设计想加分可以再加一个客人自助端但那属于扩展不是主线。真正的主线是一条从预订到回置的动线客人询价 → 前台查房态 → 下单预订 → 到店排房 → 办理入住、收押金 → 在住期间可能续住或换房 → 退房结算 → 房态置为待打扫 → 客房打扫完成 → 房态回置为空闲。这条动线上每一个箭头都是一次状态变更也都对应一条权限。把这条动线画在纸上你会发现系统的核心不是「房间表」而是「房间 日期」的组合。房间表只说明 8801 这间房存在房态表才说明它在 3 月 5 日晚上有没有人住。这个认知很关键它直接决定了后面是写一个能跑通的课设还是写一个演示时会被问穿的课设。角色的职责划分建议这样落角色核心职责可见菜单关键限制前台接待查房态、建订单、办理入住退房、收银房态看板、订单、入住、账单不能删除已入住订单不能改房价客房服务员接收打扫任务、回置房态待打扫列表、打扫记录只能改房态看不到客人手机号和金额店长/管理员房型房价维护、员工账号、报表全部 统计报表删除订单需二次确认并留日志客人可选查询、预订、查看订单移动端页面只能操作自己的订单2.2 功能清单与「不做清单」课程设计和毕业设计最大的坑不是做不出来是想做的太多最后每样都半成品。我通常会给学生一张三段式清单必做、加分、明确不做。必做部分是保证系统闭环的最小集合加分部分是能在答辩时讲出技术含量的模块不做部分要写进文档里并说明理由——评委看到你有边界意识比看到你做了一堆半成品印象好得多。必做模块包括房型与房价维护、房间管理、房态日历看板、订单的创建/取消/查询、入住登记、换房与续住、退房结算与账单、简单报表入住率、营收。这几块合起来就是一个完整闭环演示时从下单一路走到退房中间不断线。加分模块建议挑一到两个做深比如「房态日历的可视化看板」或者「订单状态机 操作日志」。做过的人都知道答辩时最容易被追问的是状态流转而不是页面好不好看。把状态机做扎实比多写五个 CRUD 页面有用。明确不做的部分OTA 渠道对接、电子发票、门锁硬件对接、身份证读卡器、多门店连锁。理由要写清楚——这些要么依赖外部资质和硬件要么需要真实的商户账号课程设计阶段做不了只能做假数据而假数据在答辩现场是撑不住追问的。把这些写进「范围外说明」反而是加分项。提示需求文档里一定要有一节叫「范围外」写清楚哪些功能不做、为什么不做。这一节通常只占半页纸但能挡掉答辩时至少两个问题。2.3 权限模型RBAC 表结构与接口级拦截权限这块很多课设只做到「登录后进后台」菜单全给等于没有权限。稍微像样一点的做法是标准 RBAC用户、角色、权限三张主表加两张关联表。权限码不要用数字用「模块:动作」的字符串比如order:cancel、room:status:update写日志和排查时一眼能看懂。表名作用关键字段sys_user员工账号id、username、passwordBCrypt、real_name、statussys_role角色id、role_code、role_namesys_permission权限点id、perm_code、perm_name、type菜单/按钮sys_user_role用户-角色user_id、role_idsys_role_permission角色-权限role_id、perm_id真正的拦截放在接口层不是放在页面上。页面隐藏按钮只是体验接口不校验才是漏洞。用一个自定义注解加拦截器就能做到比全套 Spring Security 轻得多课设完全够用// 1) 权限注解标在 Controller 方法上值为权限码 Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequiresPerm { String value(); // 例如 order:cancel }// 2) 拦截器登录成功后把权限码集合放进 ThreadLocal请求进来先比对 public class PermInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) { if (!(handler instanceof HandlerMethod hm)) return true; // 静态资源放行 RequiresPerm perm hm.getMethodAnnotation(RequiresPerm.class); if (perm null) return true; // 未标注则不校验 SetString owned LoginContext.current().getPerms(); // 登录时查库装载 if (!owned.contains(perm.value())) { throw new BizException(403, 无权限 perm.value()); // 由全局异常处理器转 JSON } return true; } }这段代码有两个设计点值得在答辩时讲。第一权限码在登录时一次性查出来放进上下文避免每个请求都联表查五次这是典型的空间换时间。第二校验失败抛的是业务异常而不是返回false因为返回false浏览器只会看到一个空白页排查时容易陷入困惑抛异常能统一走全局处理器前端拿到明确的 403 和提示语。线程上下文记得在afterCompletion里remove()否则线程池复用时会串号这个问题在本地单线程测试时永远发现不了一上压测就出问题。3. 总体架构与数据库设计五张核心表撑起房态与订单架构这一章最容易写成名词罗列把 SpringBoot、Vue、Redis、Nginx 全列一遍就算完事。但评委真正想听的是「你为什么这么选」。一家几十间客房的小酒店日均订单量可能不到一百这种情况下上微服务、上消息队列不是技术先进是给自己找麻烦。这一章把选型理由、表结构、状态机和接口约定讲清楚后面写代码才有依据。3.1 分层架构与技术选型为什么不做微服务技术栈我一般定成后端 Spring Boot 3 JDK 17 MyBatis-Plus MySQL 8前端 Vue 3 Vite Element Plus Pinia Axios鉴权用 JWT部署用 jar 包 Nginx 静态资源。这套组合的资料最多出问题最好查课程设计阶段这是最重要的考量不是哪个框架更时髦。不做微服务的理由要能说出口酒店管理系统的业务边界高度内聚订单、房态、账单三者强关联拆开就要处理分布式事务而分布式事务的复杂度远超这个系统的全部业务复杂度。单体应用加清晰的分层已经足够支撑这个量级。分层上走经典的 Controller → Service → Mapper但有一条约定Controller 不写业务判断Service 不拼 SQLMapper 不掺业务逻辑。目录结构大致这样hotel-server/ ├── src/main/java/com/example/hotel/ │ ├── controller/ # 只做参数接收、校验、调用 Service │ ├── service/ # 业务编排事务边界在这一层 │ ├── mapper/ # MyBatis-Plus 接口 XML │ ├── entity/ # 与表一一对应的实体 │ ├── dto/ # 入参对象和 entity 分开 │ ├── vo/ # 出参对象避免把密码等字段带出去 │ ├── common/ # Result、BizException、全局异常处理 │ └── config/ # 拦截器、跨域、MyBatis-Plus 配置 └── src/main/resources/ ├── mapper/ # XML 映射文件 └── application.ymlDTO 和 VO 分开这件事很多课设图省事直接用实体收参、用实体返回结果前端拿到了password字段的哈希值。分开写虽然多几个类但它是一条不用返工的规矩。3.2 核心表结构与字段取舍表不用多五张主表加两张辅助表就能撑起整个系统。设计时抓住一个原则凡是会影响历史账单的数据一律存快照不做联表取当前值。房价会调整如果账单金额靠实时联表算去年住过的订单金额今年就变了这是财务类系统的大忌。表名作用关键字段关键索引room_type房型与挂牌价id、type_name、price、capacity—room物理房间id、room_no、room_type_id、floor、statusuk_room_no、idx_type_statushotel_order订单主表id、order_no、room_id、check_in、check_out、price_snapshot、status、deposituk_order_no、idx_status_checkinroom_stay_calendar房间占用日历id、room_id、stay_date、order_iduk_room_datebill账单id、order_id、room_fee、extra_fee、deposit、totaluk_order_idconsume_item在住消费迷你吧等id、order_id、item_name、amountidx_orderoper_log操作日志id、user_id、action、target_id、before、afteridx_targetroom_stay_calendar是整套设计的核心用「一间房一天一行」的方式记录占用。住 3 晚就写 3 行退房那天不写。这样任意日期区间的可用性判断就退化成一次NOT EXISTS查询简单且不会错。CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(16) NOT NULL COMMENT 房号如 8801, room_type_id BIGINT NOT NULL COMMENT 房型ID, floor TINYINT NOT NULL COMMENT 楼层用于看板分组, status TINYINT NOT NULL DEFAULT 0 COMMENT 0空闲 1已预订 2在住 3待打扫 4维修 5停用, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_room_no (room_no), KEY idx_type_status (room_type_id, status) ) COMMENT 房间; CREATE TABLE hotel_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务单号展示用, room_id BIGINT NOT NULL, guest_name VARCHAR(32) NOT NULL, guest_phone VARCHAR(20) NOT NULL, check_in DATE NOT NULL COMMENT 入住日, check_out DATE NOT NULL COMMENT 离店日当天不计费, price_snapshot DECIMAL(10,2) NOT NULL COMMENT 下单时的房价快照, deposit DECIMAL(10,2) NOT NULL DEFAULT 0.00, status VARCHAR(16) NOT NULL COMMENT BOOKED/CHECKED_IN/CHECKED_OUT/CANCELLED, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_status_checkin (status, check_in) ) COMMENT 订单;金额字段一律DECIMAL(10,2)不要用DOUBLE。浮点数在金额上会出现0.1 0.2 ! 0.3这类问题单笔看不出来做日月汇总报表时差几分钱对账时很难查。3.3 房态与订单状态机两个状态不要混为一谈初学者常犯的错是把房态和订单状态合并成一个字段结果发现「已预订」和「在住」既描述房间又描述订单谁都对不上。正确的做法是拆成两套订单状态描述这笔生意走到哪一步房态描述这间房当下能不能卖。订单状态流转BOOKED已预订→CHECKED_IN在住→CHECKED_OUT已退房BOOKED可以转CANCELLED已取消。除了这四条边其他转移一律非法。房态流转空闲 ↔ 已预订 ↔ 在住 → 待打扫 → 空闲维修和停用是人工干预状态不参与自动流转。两套状态之间需要联动但联动方向是单向的订单状态变更触发房态变更反过来不允许。比如订单转CHECKED_IN房态置为在住订单转CHECKED_OUT房态置为待打扫。这样房态永远是订单状态的投影出问题时只需要查订单。订单状态触发动作房态变化日历占用BOOKED创建订单空闲 → 已预订写入 [入住, 离店) 区间CANCELLED取消订单已预订 → 空闲删除该订单全部日历行CHECKED_IN办理入住已预订 → 在住不变CHECKED_OUT退房结算在住 → 待打扫保持不变历史留痕—打扫完成待打扫 → 空闲不变3.4 接口约定与统一返回体接口不做统一约定前端每个页面都要写一套解析逻辑改一处错一片。统一返回体我一般只留三个字段code、msg、data。code用 0 表示成功业务错误用 4xx 段系统异常用 5xx 段HTTP 状态码保持 200让前端只判断业务码。// 统一返回体code0 成功4xx 业务错误5xx 系统异常 public record ResultT(int code, String msg, T data) { public static T ResultT ok(T data) { return new Result(0, ok, data); } public static T ResultT fail(int code, String msg) { return new Result(code, msg, null); } }配套要有全局异常处理器把BizException、参数校验失败、唯一索引冲突分别映射成不同业务码。没有这层前端拿到的永远是 500 和一堆堆栈信息排查全靠猜。核心接口清单大致是这些写文档时把请求方式、路径、权限码列成一张表前后端对接会顺很多POST /api/orders创建订单order:create、PUT /api/orders/{id}/cancel取消order:cancel、PUT /api/orders/{id}/checkin入住order:checkin、PUT /api/orders/{id}/checkout退房order:checkout、GET /api/rooms/calendar房态日历room:view、PUT /api/rooms/{id}/status房态回置room:status:update。4. 核心业务落地预订锁房、入住换房、退房结算的代码实现前面三章定的是「做什么」和「长什么样」这一章开始写「怎么保证它对」。酒店管理系统最容易出问题的三个地方并发下单超卖、换房时旧房日历没释放、退房重复结算。这三个问题在本地单线程点鼠标时永远复现不了但答辩演示只要开两个浏览器窗口就能撞上。下面按业务流程给出关键实现。4.1 预订锁房唯一索引兜底事务保证原子锁房这件事网上的方案很多有人用SELECT ... FOR UPDATE锁全表有人上 Redis 分布式锁。前者在并发稍高时会把整张房间表锁住后者对一个课设来说属于杀鸡用牛刀还要额外维护 Redis 可用性。我的做法是让数据库自己保证在room_stay_calendar上建(room_id, stay_date)唯一索引插入冲突就说明这间房那天被占了直接回滚重选。Service public class OrderService { Transactional(rollbackFor Exception.class) public Long create(OrderCreateDTO dto) { // 1) 参数与日期校验离店必须晚于入住且不允许预订过去日期 if (!dto.getCheckOut().isAfter(dto.getCheckIn())) { throw new BizException(400, 离店日期必须晚于入住日期); } // 2) 房价快照下单瞬间的挂牌价写进订单后续调价不影响历史单 RoomType rt roomTypeMapper.selectById(dto.getRoomTypeId()); BigDecimal price rt.getPrice(); // 3) 选房挑一间在 [入住, 离店) 区间内没有任何日历行的房间 Long roomId roomMapper.pickFreeRoom(dto.getRoomTypeId(), dto.getCheckIn(), dto.getCheckOut()); if (roomId null) { throw new BizException(409, 所选日期该房型已满); } // 4) 落订单 Order order Order.of(dto, roomId, price); orderMapper.insert(order); // 5) 占日历唯一索引冲突说明并发被抢先转成业务异常并回滚 try { calendarMapper.batchOccupy(roomId, dto.getCheckIn(), dto.getCheckOut(), order.getId()); } catch (DuplicateKeyException e) { throw new BizException(409, 房间刚被占用请重新选择); } return order.getId(); } }选房的 SQL 是这段逻辑的关键NOT EXISTS比LEFT JOIN ... IS NULL在小数据量下更直观也更容易讲清楚SELECT r.id FROM room r WHERE r.room_type_id #{typeId} AND r.status IN (0, 1) -- 空闲或已预订维修/停用房不参与 AND NOT EXISTS ( SELECT 1 FROM room_stay_calendar c WHERE c.room_id r.id AND c.stay_date #{checkIn} -- 含入住日 AND c.stay_date #{checkOut} -- 不含离店日 ) ORDER BY r.room_no LIMIT 1;三个参数要注意checkIn和checkOut都是DATE类型Java 侧用LocalDate不要用Date带时分秒否则和的边界会飘。checkOut不参与占用因为客人当天中午退房晚上这间房可以再卖这也是为什么区间是左闭右开。LIMIT 1是为了避免同一房型下把所有可用房都捞出来前端只需要一间。DuplicateKeyException必须在这里捕获并转成BizException不能让它冒到全局处理器。原因是它属于「预期内的并发失败」业务语义是「房间被抢了请重选」不是系统故障。如果直接抛 500前端只能弹一个「服务器错误」用户一脸茫然。注意取消订单时必须物理删除该订单的日历行不能只把状态改成「已释放」。因为唯一索引(room_id, stay_date)不包含状态字段留着旧行会让这间房在那些日期永远卖不出去。这个坑我在别人的项目里见过两次表现是「取消完订单房态看板显示空闲但下单时报已满」。4.2 入住与换房状态机校验放在 Service 层入住本身不复杂难的是防止非法流转。比如一个已取消的订单被人点了「入住」或者一个已退房的订单被重复退房。这类问题靠前端隐藏按钮挡不住必须在 Service 层做校验。我一般用一个枚举自带流转表比一堆if-else好维护。public enum OrderStatus { BOOKED, CHECKED_IN, CHECKED_OUT, CANCELLED; // 允许的流转关系不在表里的转移一律拒绝 private static final MapOrderStatus, SetOrderStatus FLOW Map.of( BOOKED, Set.of(CHECKED_IN, CANCELLED), CHECKED_IN, Set.of(CHECKED_OUT), CHECKED_OUT, Set.of(), CANCELLED, Set.of() ); public boolean canGo(OrderStatus next) { return FLOW.getOrDefault(this, Set.of()).contains(next); } }Service 里只写一句if (!order.getStatus().canGo(CHECKED_IN)) throw new BizException(400, 当前订单状态不允许入住);逻辑集中加新状态时也只改一处。换房是这套系统里最容易被忽略的操作。客人住了两晚想换到另一间房正确的处理是三件事放在同一个事务里释放旧房剩余日期从今天到原离店日的日历行、占用新房同区间的日历行、更新订单的room_id。顺序不能反先占后放否则中间有一瞬间两间房都被占着看板上会闪一下。如果新房在某个日期已被占用事务回滚旧房日历自动恢复客人这边只会看到一句「目标房间该日期不可用」。续住更简单只做占用把原离店日到新离店日之间的日历行补上同时更新订单的check_out。补的时候同样会撞唯一索引撞了就说明这间房后面已经被别人订了只能建议客人换房。Transactional(rollbackFor Exception.class) public void extend(Long orderId, LocalDate newCheckOut) { Order o orderMapper.selectById(orderId); if (o.getStatus() ! OrderStatus.CHECKED_IN) { throw new BizException(400, 只有在住订单可以续住); } // 从原离店日开始补到新离店日原离店日当天不占 calendarMapper.batchOccupy(o.getRoomId(), o.getCheckOut(), newCheckOut, orderId); o.setCheckOut(newCheckOut); orderMapper.updateById(o); }4.3 退房结算金额计算、幂等与房态回置退房是整条动线的收口也是唯一涉及钱的地方。计算规则要写死在文档里房费 房价快照 × nightsnights 用ChronoUnit.DAYS.between(checkIn, checkOut)计算天然不含离店日在住消费从consume_item汇总应付 房费 消费 − 押金负数表示需要退还客人。Transactional(rollbackFor Exception.class) public Bill checkout(Long orderId) { Order o orderMapper.selectById(orderId); if (!o.getStatus().canGo(OrderStatus.CHECKED_OUT)) { throw new BizException(400, 订单不在住无法退房); } // 1) 间夜数离店日不计费所以直接按日期差算 long nights ChronoUnit.DAYS.between(o.getCheckIn(), o.getCheckOut()); BigDecimal roomFee o.getPriceSnapshot().multiply(BigDecimal.valueOf(nights)); // 2) 在住消费迷你吧、洗衣等没有则返回 0 BigDecimal extraFee consumeItemMapper.sumByOrder(orderId); // 3) 账单落库bill.order_id 建唯一索引重复退房会直接失败 Bill bill Bill.of(orderId, roomFee, extraFee, o.getDeposit()); try { billMapper.insert(bill); } catch (DuplicateKeyException e) { throw new BizException(409, 该订单已结算请勿重复操作); } // 4) 订单状态与房态联动房态置为待打扫等客房回置 o.setStatus(OrderStatus.CHECKED_OUT); orderMapper.updateById(o); roomMapper.updateStatus(o.getRoomId(), RoomStatus.TO_CLEAN); return bill; }幂等这件事很多人靠「前端按钮置灰三秒」来解决答辩演示时双击一下就露馅。真正的幂等必须落在数据库bill.order_id建唯一索引第二次插入必然冲突捕获后返回明确提示。这样即使接口被重复调用十次账单也只有一张。房态回置为「待打扫」而不是「空闲」是一个容易被质疑但很合理的设计。客人刚退房房间还没打扫直接标空闲会被前台误卖。让客房服务员在移动端点一下「打扫完成」房态才回到空闲。这一步在小系统里常常被省掉但它体现的是对业务动线的理解答辩时值得单独讲一页。5. 常见问题与排查上线前最容易翻车的五个点代码写完能跑通只是走完了一半。剩下的一半是把那些「本地不会出现、演示一定出现」的问题提前堵住。下面五条是我带项目和看别人答辩时反复见到的按「现象 → 原因 → 解决」整理建议对着自己的系统逐条过一遍。5.1 超卖唯一索引建了但没生效现象两个人同时下最后一间房两边都提示下单成功房态看板上同一间房出现两个在住订单。原因通常有三种。一是唯一索引建在了id上或者建的是普通索引KEY而不是UNIQUE KEY冲突根本不会触发。二是插入日历行的代码写在事务外面或者事务方法被同类内部调用导致Transactional失效冲突异常抛出后订单已经提交。三是批量插入用了INSERT IGNORE冲突被静默吞掉程序以为成功了。解决先用SHOW INDEX FROM room_stay_calendar确认uk_room_date的Non_unique为 0再检查下单入口是不是从 Controller 直接调到 Service 的Transactional方法同类内部调用不走 AOP这是很常见的错误最后把批量插入语句改成普通INSERT让冲突正常抛出。5.2 房态和订单对不上双写没有对账现象订单显示已退房房态看板上那间房还挂着「在住」刷新也不变只能手动改数据库。原因房态是订单状态的投影但代码里两处更新没放进同一个事务。比如orderMapper.updateById成功roomMapper.updateStatus抛了异常事务没覆盖房态就停在上一个状态。还有一种是换房时只更新了订单没更新房态看板自然对不上。解决所有涉及状态联动的方法统一加上Transactional(rollbackFor Exception.class)注意是Exception.class默认只回滚运行时异常。另外建议加一个定时对账任务每天凌晨扫一遍「状态为在住但房态不为在住」的订单输出到日志或告警表。这个任务不修数据只报告让问题自己浮出来。提示对账任务的价值不在于自动修复而在于让不一致变得可见。自动修复会把真正的 bug 藏起来答辩时被问到「你怎么保证数据一致」反而答不上来。5.3 金额用 double报表差几分钱现象单笔账单看着没问题月度营收报表和各房间账单加起来对不上差几分到几毛。原因金额字段用了DOUBLE或 Java 里的double浮点累加产生精度误差。另一种是计算过程中用new BigDecimal(0.1)这种从 double 构造的方式误差从一开始就带进去了。解决数据库统一DECIMAL(10,2)Java 统一BigDecimal构造时用字符串new BigDecimal(0.1)或BigDecimal.valueOf(0.1)。除法涉及金额时明确指定精度和舍入模式比如divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP)不指定会抛ArithmeticException。5.4 跨天与时间边界退房日到底算不算一晚现象3 月 5 日入住、3 月 6 日离店系统算出两晚或者算出零晚。原因间夜数计算用错了类型。用Date带时分秒做减法再除以 86400000遇到夏令时或跨月会得到 0.99 天取整就变成 0。用Period.between得到的是年月日三元组不是总天数。日历占用区间写成了闭区间[入住, 离店]导致离店日也被占第二天卖不出去。解决全链路用LocalDate间夜数用ChronoUnit.DAYS.between(checkIn, checkOut)日历占用统一左闭右开[checkIn, checkOut)。SQL 里对应stay_date #{checkIn} AND stay_date #{checkOut}两个边界符号不要写反。5.5 越权与日志缺失前台能删订单现象演示时用前台账号登录发现菜单里没有「删除订单」但改一下 URL 或者用接口工具直接调还是能删。原因权限只做在菜单渲染层后端接口没有校验。菜单隐藏是体验不是安全边界。另一种情况是校验了但没记日志出了问题查不到是谁操作的。解决按第 2.3 节的注解方式在接口上加权限码删除、改价、退款这类敏感操作额外要求二次确认。同时把写操作统一记进oper_log记录操作人、动作、目标 ID 和变更前后值。日志表不需要复杂四个字段就够定位问题但一定要在 Service 层统一拦截着记不要靠开发自觉在每个方法里写一行。6. 验收与答辩把「能跑」讲成「能交付」的检查方法验收这件事我一般不让别人凭感觉说「差不多了」。感觉是最不可靠的东西尤其是自己写的代码。做法是提前列一张可判定的清单每条都写成「做什么操作 → 期望看到什么结果」能勾选才算过。下面这张表是我常用的版本八条覆盖了这套系统的关键路径答辩前照着走一遍基本能排掉大部分演示事故。序号操作期望结果1用两个浏览器同时预订最后一间同房型房一个成功另一个提示「已满」房态看板只有一条占用2下单后立即取消再重新预订同一日期第二次能成功说明日历行已释放3走完预订 → 入住 → 退房全流程订单状态依次变更房态最终为待打扫4重复点击退房按钮只生成一张账单第二次提示「已结算」5换房后查看两间房的日历旧房剩余日期释放新房对应日期占用6用前台账号直接调删除订单接口返回 403订单未被删除日志有记录73 月 5 日入住、3 月 6 日离店间夜数为 1房费等于单价 × 18月度营收报表与逐笔账单求和金额完全一致无小数误差答辩被追问的高频问题也就那么几个提前准备好回答思路比背 PPT 有用得多。第一个是「你怎么防止一间房卖两次」。回答不要只说「加了锁」要说清楚用的是数据库唯一索引兜底索引建在(room_id, stay_date)上配合事务冲突时转业务异常并回滚。如果能补一句「没有用 Redis 分布式锁因为当前并发量下一个数据库约束就够多引入一个组件反而增加故障面」会显得你在做取舍而不是抄方案。第二个是「订单状态为什么不用一个字段表示」。回答要点是房态描述房间、订单状态描述交易两者生命周期不同混在一起会出现「已取消但在住」这种无法表达的状态。第三个是「金额为什么用 DECIMAL」。这题几乎是送分题答浮点精度损失、给出0.1 0.2的例子即可能顺带说出BigDecimal构造要用字符串就更完整。第四个是「如果酒店有一百家分店你这套还能用吗」。不要硬答能正确姿势是承认不能直接扩展然后指出瓶颈在哪单库单表、房态看板全量查询、没有租户隔离。再给出改造方向加hotel_id做数据隔离、日历表按酒店分片、看板改增量刷新。承认边界并提出路径比硬撑得分高。注意答辩演示前把数据库重置成一份干净的初始化脚本只留几条演示数据。带着几十条测试脏数据上台房态看板会乱成一团第一印象就没了。从那以后我每次带人做这套系统交付前都强制走一遍这张八条清单一条不过就不许演示。有一次某位同学嫌麻烦跳过了第 4 条结果答辩现场评委让他再点一次退房屏幕上跳出两张账单后面所有问题都围绕这个展开。这套表结构、接口清单和可运行的源码我整理成了一份完整资料需要照着交课程设计或改造成自己毕业设计的可以拿去对着改重点是先跑通第 1 章那条主线再往里加功能。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →