JavaEE酒店管理系统毕业设计:数据库、状态流转与联调部署实战 简介面向JavaEE毕业设计与SSH框架入门实践的小型酒店管理系统适合需要完整项目案例的高校学生、初级开发者及课程设计选题者参考。资源包共262个文件以49个Java源码、54个class编译文件、54个jar依赖包和25个JSP页面为主另含XML配置、properties及图片等静态资源压缩包约18.69MB。项目围绕酒店业务组织用户与员工权限、客房类型与状态、预订入住退房、收费账单和报表统计等模块可帮助读者梳理Struts请求流转、Spring依赖注入与事务管理、Hibernate实体映射的协作方式。包内还保留测试类与配置文件便于对照实体类、DAO、Service、Action各层职责理解从JSP表单到数据库的完整链路。已有1695人学习下载适合用于课程设计选题、代码结构参考和SSH分层实现思路的对照学习。1. 从答辩现场倒推JavaEE 小型酒店管理系统到底该先跑哪条链路答辩前一周还在改登录页是 JavaEE 小型酒店管理系统毕业设计里很常见的翻车现场。这套系统本质是把“房型—房间—预订—入住—退房—账单”串成一条可演示的业务链路再用 JSP/SSM 或 Spring Boot 把权限、分页、状态流转撑起来。它适合拿来交毕业设计也适合刚接触 JavaWeb 的开发者练手但不适合直接当生产级酒店系统。真正决定能不能跑通的不是页面多漂亮而是数据库表关系、登录会话、订单状态和部署路径这四件事。先把启动链路和核心表跑通再回头补页面细节比一上来改 UI 稳得多。2. 数据库与实体映射七张核心表怎么拆字段类型别乱省小型酒店管理系统的数据库不算大但特别容易在“表拆得不够”上吃亏。常见做法是先定业务状态再拆表房型管价格和容量房间管具体房号预订管占房日期入住管实际到店账单管结算金额用户表管登录和角色。把房型和房间合成一张表换房时就要改历史数据把预订和入住合成一张表前台演示“先预订、后入住、再退房”时状态会绕成一团。这一章按可复现的顺序把核心表、建表 SQL、实体映射和字段参数拆开讲。2.1 先定状态再拆表房型、房间、预订、入住、账单的关系房型和房间是一对多一个房型可以有多个具体房间价格放在房型上房号放在房间上。房间和预订是一对多同一房间在不同日期可以有多条预订记录但同一日期段不能重叠。预订和入住常见做法是一对一预订确认后到店办理入住生成入住记录退房时再生成账单。用户表和预订、入住之间通过操作人字段关联方便答辩时讲“谁办的入住、谁结的账”。状态字段要提前定好不然后面接口会互相打架。房间状态我一般用四个值空闲、已预订、在住、维修。预订状态用五个值待入住、已入住、已退房、已取消、已过期。账单状态用未结、已结、已退款。状态值不要用中文直接存用TINYINT存数字前端用字典翻译。这样查询快后面加状态也不用改表结构。常见错误是把状态写成VARCHAR再存“已入住”这种中文联调时大小写、空格、编码都能让人排查半天。表关系定完后索引要跟着查询走。房间表按room_no建唯一索引预订表按room_id check_in_date check_out_date建普通索引或唯一索引账单表按checkin_id建索引。外键约束在毕业设计环境里不是必须常见做法是加索引不做强外键少一层级联报错如果导师明确要求外键再补上也不迟。这里没有绝对标准关键是答辩时能说清为什么这样选。2.2 建表 SQL 与字段参数金额、时间、状态别省下面这段 SQL 是七张核心表的精简版字段够用命名统一方便后面实体类和 Mapper 对上。金额统一用DECIMAL(10,2)日期用DATE操作时间用DATETIME状态用TINYINT。表名和字段名都用了小写下划线后面靠 MyBatis 的下划线转驼峰配置映射到 Java 属性。-- 用户表登录、角色、状态 CREATE TABLE hotel_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, login_name VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), role_type TINYINT NOT NULL DEFAULT 1 COMMENT 1前台 2管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_login_name (login_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房型表价格、容量、上下架 CREATE TABLE room_type ( id BIGINT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, capacity INT NOT NULL DEFAULT 2, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 房间表房号、所属房型、房态 CREATE TABLE room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(20) NOT NULL, type_id BIGINT NOT NULL, floor_no INT, status TINYINT NOT NULL DEFAULT 1 COMMENT 1空闲 2已预订 3在住 4维修, UNIQUE KEY uk_room_no (room_no), KEY idx_type_id (type_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 客人表入住人信息 CREATE TABLE guest ( id BIGINT PRIMARY KEY AUTO_INCREMENT, guest_name VARCHAR(50) NOT NULL, id_card VARCHAR(30), phone VARCHAR(20), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 预订表占房日期、状态、操作人 CREATE TABLE reservation ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, guest_id BIGINT NOT NULL, check_in_date DATE NOT NULL, check_out_date DATE NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待入住 2已入住 3已退房 4已取消, created_by BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_room_date (room_id, check_in_date, check_out_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 入住表实际到店、押金 CREATE TABLE checkin ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reservation_id BIGINT NOT NULL, room_id BIGINT NOT NULL, guest_id BIGINT NOT NULL, check_in_time DATETIME NOT NULL, check_out_time DATETIME, deposit DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 1 COMMENT 1在住 2已退房, KEY idx_reservation_id (reservation_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 账单表结算金额、支付方式 CREATE TABLE bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, checkin_id BIGINT NOT NULL, room_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, other_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, pay_type TINYINT COMMENT 1现金 2扫码 3挂账, status TINYINT NOT NULL DEFAULT 1 COMMENT 1未结 2已结 3已退款, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_checkin_id (checkin_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 SQL 的逻辑是先把“静态资源”和“动态流水”分开房型、房间、用户属于静态或半静态数据预订、入住、账单属于流水数据。参数上DECIMAL(10,2)能覆盖酒店房价和押金不用FLOAT是血泪经验浮点数在金额累加时会出现199.999999这种显示问题DATE和DATETIME分开是为了让预订按天比较、入住按时间点记录TINYINT状态字段给前端字典留了空间。索引先按查询条件建不追求大而全。提示建表脚本跑完后先插一条房型、一条房间、一条用户不要等页面写完再补基础数据否则联调时列表页空白会误判成接口坏了。2.3 实体映射与结果集字段名对不上时先查三处实体类不用追求和表字段完全同名但映射规则要统一。常见做法是开启下划线转驼峰把room_no映射到roomNo把type_name映射到typeName。如果开了配置仍然查出来是null先查三处实体属性名是否写错、SQL 是否用了别名、resultMap是否覆盖了自动映射。下面这段 Mapper 查询把房间和房型联起来返回RoomVO字段别名写得很直白减少猜的成本。!-- RoomMapper.xml分页查询房间联表取房型名和价格 -- select idpageRoom resultTypecom.example.hotel.vo.RoomVO SELECT r.id, r.room_no AS roomNo, r.floor_no AS floorNo, r.status, t.type_name AS typeName, t.price FROM room r JOIN room_type t ON t.id r.type_id WHERE 1 1 if testkeyword ! null and keyword ! AND (r.room_no LIKE CONCAT(%, #{keyword}, %) OR t.type_name LIKE CONCAT(%, #{keyword}, %)) /if if teststatus ! null AND r.status #{status} /if ORDER BY r.room_no LIMIT #{offset}, #{limit} /select这段查询的逻辑是列表页只取展示需要的字段不把整张房间表和房型表都查出来。参数上keyword支持房号和房型名模糊搜status用来筛房态offset和limit由分页参数换算offset (page - 1) * limit。如果实体类用BigDecimal接price就不要用Double否则前端toFixed(2)之前可能已经丢精度。日期字段用LocalDate接DATE用LocalDateTime接DATETIME别混用。注意resultType自动映射依赖列名和属性名规则联表查询出现同名字段时一定要起别名比如两张表都有status不写别名就可能把房型状态映射到房间状态上。3. 后端分层与接口从登录拦截到退房结算的四个可抄步骤JavaEE 毕业设计最容易写成“Controller 里塞所有逻辑”页面能点但答辩时一问事务边界和状态流转就露馅。合理的分层不是摆样子Controller 负责接参、登录态和返回格式Service 负责业务规则和事务Mapper 负责 SQL。小型酒店管理系统的接口顺序也有讲究先登录再房态列表再预订再入住再退房结算。这一章按这个顺序把可抄的代码和参数说明拆开。3.1 分层不是摆样子Controller、Service、Mapper 的边界Controller 不要写业务判断尤其不要把“房间是否可订”写在 Controller 里。Controller 只做三件事从HttpSession取当前用户、把前端参数转成 DTO、调用 Service 并包装返回结果。Service 承担校验、事务和状态变更。Mapper 只做数据访问不写业务分支。这样后面加“取消预订”或“换房”时不用在多个 Controller 里找重复代码。RestController RequestMapping(/api/reservation) public class ReservationController { Resource private ReservationService reservationService; // 创建预订登录态从 Session 取业务参数从 DTO 取 PostMapping(/create) public Result create(RequestBody ReservationDTO dto, HttpSession session) { Long userId (Long) session.getAttribute(LOGIN_USER_ID); if (userId null) { return Result.fail(登录已失效请重新登录); } return reservationService.createReservation(dto, userId); } }这段代码的逻辑是把“登录态”和“业务参数”分开session里只取LOGIN_USER_ID不信任前端传的操作人 IDReservationDTO只接roomId、guestId、checkInDate、checkOutDate、deposit这类业务字段。参数上RequestBody要求前端发application/json如果页面用表单提交要改成ModelAttribute或RequestParam。返回统一用Result前端判断code字段不要直接返回字符串。Service 里再写事务。事务边界放在 Service 方法上是因为一次预订可能同时写预订表、改房态甚至写操作日志放在 Controller 里太散放在 Mapper 里又太细。下面这段是预订创建的核心逻辑省略了日志和消息但保留了日期校验、冲突校验、房态校验和写入顺序。Service public class ReservationService { Resource private ReservationMapper reservationMapper; Resource private RoomMapper roomMapper; Transactional(rollbackFor Exception.class) public Result createReservation(ReservationDTO dto, Long userId) { // 1. 日期合法性离店必须晚于入住 if (!dto.getCheckOutDate().isAfter(dto.getCheckInDate())) { return Result.fail(离店日期必须晚于入住日期); } // 2. 同一房间日期不能重叠 int overlap reservationMapper.countOverlap( dto.getRoomId(), dto.getCheckInDate(), dto.getCheckOutDate()); if (overlap 0) { return Result.fail(该房间在所选日期已被预订); } // 3. 房间必须存在且不是维修状态 Room room roomMapper.selectById(dto.getRoomId()); if (room null || room.getStatus() 4) { return Result.fail(房间不可用); } // 4. 写预订再把房态改成已预订 reservationMapper.insert(dto, userId); roomMapper.updateStatus(dto.getRoomId(), 2); return Result.ok(预订成功); } }这段逻辑的关键参数是checkInDate和checkOutDate用LocalDate比较不要用字符串比较。countOverlap的 SQL 条件常见写法是check_in_date #{checkOutDate} AND check_out_date #{checkInDate}这样边界日期不会误判。roomMapper.updateStatus放在同一个事务里预订写失败时房态回滚。并发场景下先查冲突再插入仍有竞态常见补救是给房间行加FOR UPDATE锁或者在预订表上建唯一索引加捕获重复键异常毕业设计里至少要在答辩时能说出这个边界。3.2 登录与会话拦截用户名、密码、Session 的三个参数登录接口不要直接把密码明文比对哪怕是小系统也建议存一次哈希。毕业设计里常见做法是用MD5加盐或BCrypt前者好讲后者更稳。下面这段登录代码只保留核心参数loginName、password、session。登录成功后把用户 ID 和用户名放进 Session并设置过期时间避免答辩演示到一半会话失效。PostMapping(/login) public Result login(RequestParam String loginName, RequestParam String password, HttpSession session) { UserVO user userService.checkPassword(loginName, password); if (user null) { return Result.fail(账号或密码错误); } session.setAttribute(LOGIN_USER_ID, user.getId()); session.setAttribute(LOGIN_USER_NAME, user.getLoginName()); // 30 分钟无操作失效答辩演示够用 session.setMaxInactiveInterval(30 * 60); return Result.ok(user); }参数上loginName要做去空格password不要回显到日志里。setMaxInactiveInterval的单位是秒写30 * 60比写1800可读。拦截器里要区分页面请求和 AJAX 请求页面请求可以重定向到登录页AJAX 请求应返回401或统一 JSON否则前端表格会把登录页 HTML 当数据解析报一个很难懂的解析错误。public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { Object uid req.getSession().getAttribute(LOGIN_USER_ID); if (uid null) { // 判断是否为 AJAX是则返回 JSON不是则跳登录页 String requestedWith req.getHeader(X-Requested-With); if (XMLHttpRequest.equals(requestedWith)) { resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write({\code\:401,\msg\:\登录已失效\}); } else { resp.sendRedirect(req.getContextPath() /login.jsp); } return false; } return true; }这段拦截器的逻辑是先看 Session 里有没有LOGIN_USER_ID没有就按请求类型分流。参数上req.getContextPath()会带上部署上下文比如/hotel不写的话重定向可能落到根路径。X-Requested-With不是所有前端库都带Layui 的$.ajax默认会带原生fetch不一定所以前端统一封装请求头更稳。3.3 预订到退房的状态流转接口顺序和事务边界退房结算不要在一个接口里既改入住状态、又改房间状态、又生成账单逻辑会变得很难排。常见做法是拆成三个动作办理入住、退房、结算。入住时把预订状态改成已入住房间状态改成在住退房时写入离店时间房间状态改成空闲结算时生成账单并更新账单状态。每一步都有事务接口顺序固定答辩演示就不会乱。Transactional(rollbackFor Exception.class) public Result checkout(Long checkinId, LocalDateTime checkoutTime) { Checkin checkin checkinMapper.selectById(checkinId); if (checkin null || checkin.getStatus() 2) { return Result.fail(入住记录不存在或已退房); } // 1. 更新入住记录离店时间、状态 checkinMapper.checkout(checkinId, checkoutTime); // 2. 释放房间 roomMapper.updateStatus(checkin.getRoomId(), 1); // 3. 生成账单金额由入住天数和房型价格计算 billService.createBill(checkinId); return Result.ok(退房成功); }这段逻辑的参数是checkinId和checkoutTimecheckoutTime用服务端时间更稳前端传时间只做展示。事务边界覆盖入住更新、房态释放、账单生成三步任何一步失败都回滚。注意账单金额不要用前端传的总价后端按check_in_time和check_out_time计算天数再乘房型价格押金和其他消费单独累加。这样答辩时被问“金额怎么来的”能直接指到 Service。提示状态流转接口先画一张状态表再写代码把“待入住→已入住→已退房”和“待入住→已取消”写清楚后面加“换房”时不容易把房态改错。4. 前端页面与联调JSP/Layui 表格请求怎么对上后端分页页面能打开不等于联调成功。小型酒店管理系统的前端常见组合是 JSP Layui 或 JSP Bootstrap表格分页、表单提交、日期选择器是最容易对不上的三块。联调时先看浏览器 Network 里的请求参数再看后端接收到的参数名最后看返回 JSON 的字段名。这一章按“表格分页—上下文路径—表单日期”的顺序把前端请求和后端接收对齐。4.1 JSP 页面怎么拿到后端分页数据参数名先对齐Layui 表格默认用page和limit传分页后端如果写成current和size就会出现“第一页正常、点第二页还是第一页”的假分页。常见做法是前端不改后端用RequestParam(defaultValue 1) Integer page和RequestParam(defaultValue 10) Integer limit接。下面这段前端代码把ctx拼在接口前面where里放筛选条件templet处理金额显示。layui.use([table, form], function () { var table layui.table; table.render({ elem: #roomTable, url: ctx /api/room/page, // ctx 由 JSP 注入避免上下文路径写死 method: get, where: { keyword: , status: 1 }, page: true, limit: 10, cols: [[ { field: roomNo, title: 房间号, width: 100 }, { field: typeName, title: 房型, width: 140 }, { field: price, title: 价格, width: 100, templet: function (d) { return Number(d.price).toFixed(2); }}, { field: status, title: 房态, width: 100, templet: function (d) { return d.status 1 ? 空闲 : (d.status 2 ? 已预订 : (d.status 3 ? 在住 : 维修)); }} ]] }); });这段请求的逻辑是url带上上下文路径where里的keyword和status会作为查询参数拼到 URL 上page和limit由 Layui 自动带。参数上limit要和后端分页大小一致templet里用Number(d.price).toFixed(2)防止后端返回字符串时出现199.0这种显示。后端接收分页的代码要返回 Layui 认识的格式常见是code、msg、count、data四个字段count是总条数不是当前页条数。GetMapping(/page) public Result page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer limit, RequestParam(defaultValue ) String keyword, RequestParam(required false) Integer status) { int offset (page - 1) * limit; ListRoomVO rows roomMapper.pageRoom(offset, limit, keyword, status); long count roomMapper.countRoom(keyword, status); return Result.table(count, rows); }这段后端代码的逻辑是把page换算成offset再查列表和总数。参数上page从 1 开始limit不要允许前端传太大常见做法是后端限制最大 100 条防止一次拉全表。status用required false不传就查全部房态。Result.table里把count和data对上前端表格才能正确显示分页条。4.2 静态资源与上下文路径/hotel 和 / 的差别JSP 项目部署到 Tomcat 后访问路径常常带项目名比如http://127.0.0.1:8080/hotel/login.jsp。页面里如果写死/static/layui/layui.js浏览器会去http://127.0.0.1:8080/static/layui/layui.js找结果 404。正确做法是用pageContext.request.contextPath拼上下文或者在一个全局 script 里定义ctx。下面这段 JSP 把 CSS、JS 和全局ctx放在一起后面所有接口都以ctx开头。% page contentTypetext/html;charsetUTF-8 languagejava % html head link relstylesheet href${pageContext.request.contextPath}/static/layui/css/layui.css script // 全局上下文路径页面内所有接口和静态资源都用它拼 var ctx ${pageContext.request.contextPath}; /script script src${pageContext.request.contextPath}/static/layui/layui.js/script /head body table idroomTable lay-filterroomTable/table /body /html这段 JSP 的逻辑是把上下文路径在页面加载时注入成 JS 变量静态资源用 EL 表达式拼。参数上contentType要写text/html;charsetUTF-8否则中文页面可能乱码ctx在 JSP 里是字符串到了 JS 里就是普通变量。如果项目部署在根路径ctx是空字符串拼出来是/api/...也不会多一个斜杠。注意静态资源 404 时先看 Tomcat 部署的 Application Context再看 Spring MVC 有没有拦截/static/**。拦截器配了addPathPatterns(/**)却忘了排除静态资源CSS 和 JS 会被登录拦截器挡住页面样式全丢。4.3 表单校验与日期格式前端传 yyyy-MM-dd后端别收成带 T 的字符串日期是联调里最容易出后悔药的地方。Layui 日期选择器默认返回yyyy-MM-dd后端用LocalDate接一般没问题如果前端用了new Date().toISOString()会变成2025-01-01T00:00:00.000Z后端按yyyy-MM-dd解析直接报错。常见做法是前端统一格式化后端用DateTimeFormat指定格式。金额输入框限制两位小数提交前parseFloat再toFixed(2)不要直接把带货币符号的字符串传到后端。// 表单提交前统一处理日期和金额 form.on(submit(saveReservation), function (data) { var formData data.field; formData.roomId Number(formData.roomId); formData.guestId Number(formData.guestId); formData.deposit Number(formData.deposit || 0).toFixed(2); // checkInDate/checkOutDate 由日期选择器产出 yyyy-MM-dd $.ajax({ url: ctx /api/reservation/create, type: post, contentType: application/json;charsetUTF-8, data: JSON.stringify(formData), success: function (res) { if (res.code 200) { layer.msg(预订成功); table.reload(roomTable); } else { layer.msg(res.msg); } } }); return false; });这段提交逻辑的关键是contentType用application/json并且data用JSON.stringify因为后端 Controller 用了RequestBody。参数上roomId和guestId转成数字deposit保留两位小数字符串或数字具体看 DTO 类型。日期字段不要在前端做时区转换yyyy-MM-dd原样传后端用DateTimeFormat(pattern yyyy-MM-dd)接LocalDate。public class ReservationDTO { private Long roomId; private Long guestId; DateTimeFormat(pattern yyyy-MM-dd) private LocalDate checkInDate; DateTimeFormat(pattern yyyy-MM-dd) private LocalDate checkOutDate; private BigDecimal deposit; }这段 DTO 的逻辑是把前端字段一对一接住日期格式显式声明金额用BigDecimal。参数上DateTimeFormat对表单和查询参数生效如果请求体是 JSON常见做法是用JsonFormat(pattern yyyy-MM-dd)。两个注解别混用混用后会出现“表单能传、JSON 不能传”的怪问题。5. 联调与部署排查五个高频翻车点的现象、原因和解决这套系统在本地跑通、换台机器就翻车通常不是代码逻辑突然坏了而是环境、路径、编码、连接串这些“黑匣子”参数没对齐。排查顺序我一般从启动日志开始再看数据库连接再看浏览器 Network最后才改业务代码。下面五个点按出现频率排基本覆盖小型酒店管理系统从启动到演示的主要翻车场景。5.1 启动与连接类排查端口、库账号、编码和时区现象一启动报端口占用或者访问 404。原因常见是 Tomcat 的 8080 被别的进程占了或者项目部署的上下文不是根路径访问地址少了项目名。解决是先看启动日志里的Port already in use换端口或结束占用进程再看 Tomcat 部署后的 Application Context访问时带上/hotel这类前缀。不要一看到 404 就改 Controller 映射先确认请求有没有打到项目里。现象二数据库连接失败日志里是Access denied或Unknown database。原因一般是连接串里的库名、用户名、密码和本机 MySQL 不一致或者 MySQL 没启动。解决是把连接串单独拿出来用客户端先连一次确认库存在、账号有权限。下面这段连接串是常见写法useUnicode和characterEncoding解决中文serverTimezone解决时间差 8 小时。# 连接串示例库名、账号、密码、编码、时区逐项对齐 jdbc.urljdbc:mysql://127.0.0.1:3306/hotel?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernamehotel_user jdbc.passwordhotel_pass这段配置的逻辑是把连接参数集中管理不在 Java 代码里散落。参数上hotel是库名hotel_user和hotel_pass是本机账号serverTimezone要和数据库服务器时区一致。如果启动日志里时间比实际少 8 小时先查这里再查实体类时间字段类型。现象三页面中文乱码或者保存后数据库里是问号。原因可能是 JSP 页面没写charsetUTF-8、请求过滤器没设编码、数据库表字符集不是utf8mb4、连接串没带characterEncoding。解决是按“页面—请求—连接—表”四层排查JSP 加contentTypeweb 层加编码过滤器连接串带characterEncodingutf8建表用utf8mb4。四层缺一层都可能乱码不要只改一处就重启。// 编码过滤器放在过滤器链最前面 public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); chain.doFilter(req, resp); } }这段过滤器的逻辑是统一请求和响应编码参数写死UTF-8即可。注意setCharacterEncoding要在读取参数之前调用过滤器顺序要配在登录拦截器前面。如果用了 Spring 的CharacterEncodingFilter就不要重复造轮子确认forceEncoding打开。5.2 业务与页面类排查重复预订、金额精度和静态资源现象四同一房间同一日期出现两条预订演示时被问住。原因常见是先查冲突再插入两个请求几乎同时进来都查到 0 条冲突然后都插入成功。解决分两层业务层用SELECT ... FOR UPDATE锁住房间行或者在预订表上建唯一索引兜底。唯一索引不能完全表达日期区间重叠但能防完全相同的日期段重复插入答辩时能讲清“数据库兜底 业务校验”的组合。-- 兜底索引防完全相同的房间和日期段重复插入 ALTER TABLE reservation ADD UNIQUE KEY uk_room_date (room_id, check_in_date, check_out_date);这段 SQL 的逻辑是给预订表加一个唯一约束参数是房间 ID 和入住、离店日期。注意这个索引只能防“三段完全相同”的重复防不住1-3和2-4这种交叉重叠。交叉重叠仍要靠业务查询check_in_date 离店 AND check_out_date 入住判断必要时在事务里锁房间行。演示前把这条边界讲清楚比只贴代码更加分。现象五金额显示199.999999或者退房结算多出几分钱。原因多是金额用了double或float或者在 Java 里做了浮点累加。解决是数据库用DECIMAL(10,2)实体用BigDecimal加法用add()乘法用multiply()最后setScale(2, RoundingMode.HALF_UP)。前端展示用toFixed(2)只解决显示不解决计算精度计算必须放在后端。// 账单金额计算天数 * 房价 其他消费最后保留两位 BigDecimal roomAmount roomPrice.multiply(new BigDecimal(days)); BigDecimal total roomAmount.add(otherAmount) .setScale(2, RoundingMode.HALF_UP);这段计算的逻辑是先算房费再加其他消费最后统一保留两位。参数上days用ChronoUnit.DAYS.between(checkIn, checkOut)算至少为 1RoundingMode.HALF_UP是常见四舍五入规则。不要在前端算总价再传给后端后端要按自己的数据重算防止演示时改前端数字对不上账。现象六静态资源 404页面样式全丢。原因常见是上下文路径写死、拦截器拦截了/static/**、资源目录不在webapp下。解决是先看浏览器请求的完整 URL再对比项目部署路径最后看拦截器排除规则。JSP 里用pageContext.request.contextPath拼Spring MVC 配置里排除静态资源Tomcat 部署时确认webapp/static或 resources本文还有配套的精品资源点击获取