☰
基于javaEE+MySQL的酒店管理系统毕业设计实战指南
2026/9/26 11:27:36 网站建设 项目流程

简介:这套基于JavaEE与MySQL的酒店管理系统毕业设计资料包,内容围绕完整的客房业务场景设计,适合具备一定编程基础的学生和开发者将其作为毕业设计、课程设计或工程实训的参考项目。压缩包为ZIP格式,整体大小约186.81MB,涵盖完整的JavaEE项目源码、MySQL数据库初始化脚本、毕业论文、答辩PPT以及演示视频等主要文件类型,便于从开发、文档到答辩全流程使用。项目功能覆盖客户管理、客房管理、菜品管理、餐桌预定和餐饮消费管理,并给出了JDK与Tomcat以及MySQL环境下的具体部署方案,降低了复现与二次开发难度。数据库脚本可独立导入MySQL,快速搭建可运行环境。已有156人浏览学习,借助源码、论文和视频可以掌握酒店管理系统的模块划分、数据库表结构设计与业务实现思路,同时适合作为论文写作和答辩准备的参考模板。

1. 基于javaEE+MySQL的酒店管理系统:毕设选题里的老牌刚需

选题时最怕的是“听起来很新,做起来没底”。酒店管理系统在 javaEE 方向里属于典型的老牌刚需:需求不复杂,但房费计算、订单状态、会员折扣这些业务逻辑能把 CRUD 做出真实感,而且数据库表关系复杂度适中,拿来当毕业设计正好。标题里那套源码、数据库 SQL、论文、答辩 PPT 和演示视频之所以成为经典配置,是因为大多数人的痛点不是写不出代码,而是不知道业务链路怎么闭环、答辩怎么讲。这篇笔记把主题锁定在基于 javaEE+MySql 酒店管理系统的完整落地路径上:从技术选型、建库建表、核心功能实现,到部署排查和答辩亮点,按我实际带项目的顺序写给你。适合正在做酒店管理系统毕业设计的人,也适合想用这个老题目补齐分层架构和数据库基本功的初级开发者。

2. 技术选型与项目骨架:javaEE+MySQL的组合为什么最稳

2.1 三种方案的分寸感:Servlet/JSP、SSH、SSM到底该选谁

拿到这个标题,第一个要拍板的就是主框架。市面上这类系统的主流做法有三种:纯 Servlet + JSP + JDBC、SSH(Struts2 + Spring + Hibernate)、SSM(Spring + SpringMVC + MyBatis)。不少同学一看 SSH 是“当年企业标配”就扑上去,结果被 Struts2 的拦截器配置和 Hibernate 的懒加载坑到怀疑人生。我的建议很直接:论文题目写的是 javaEE 而不是 SpringBoot,说明评审期待的是你能讲清分层架构和 Java Web 原生运行机制,纯 Servlet + JSP + JDBC/连接池反而是最稳妥的答案。

为什么这么说?酒店管理系统的核心是订单和房态,这类强业务逻辑场景里,Servlet 负责接收请求、调用 Service、控制页面跳转,JSP 只负责渲染,这个 MVC 结构在答辩时三句话就能讲完。SSH 里的 Hibernate 映射关系在学习成本上占比太高,Struts2 的 valueStack 又是个黑匣子,一旦页面取值取不到,排查链路非常长。SSM 当然也可以,但 MyBatis 的 XML 映射文件会分散你对业务设计的注意力,而且框架版本之间经常出现 jar 包冲突,不少人的毕设时间就耗在“启动报错—搜依赖—换版本”这个循环里。

用原生 Servlet 还有一个隐藏优势:它最能体现你理解 HTTP 请求的生命周期。SpringBoot 固然是当前主流,但如果你在毕业设计里用 SpringBoot,评委大概率会追问“那 javaEE 的 Servlet、Filter、Listener 你还熟吗”,到时候你如果只能说“框架封装了”,场面会比较被动。我的做法是:主链路用 Servlet + JSP + JDBC + Druid 连接池,把 Spring 的 IoC 思想用在 Service 层的接口与实现分离上,这样既有原生味道,又能体现设计意识。

2.2 从 JDK 到 Tomcat:跑通项目前的三个环境检查

环境准备这一步看着基础,但翻车率极高。我见过太多次“代码没问题,环境不对”的案例,尤其是 MySQL 8 和 Tomcat 版本组合出的幺蛾子。下面这套检查顺序是我每次搭 javaEE 环境都会走的:

java -version mysql -u root -p -e "status;" cd /path/to/tomcat/bin && ./catalina.sh run

第一条确认 JDK 版本,javaEE 项目用 JDK 8 或 11 最稳,JDK 17 配老 Tomcat 偶尔会碰到模块访问限制。第二条确认 MySQL 服务是真的活着,而不是只在安装向导里“看起来装好了”。第三条直接前台启动 Tomcat,能看到完整日志,比双击 startup.bat 一闪而过强太多。如果你用 VSCode 配 javaEE 语言环境,记得把 Tomcat 的 lib 目录加进 referenced libraries,否则 Servlet 类会飘红。

Tomcat 版本这里提前打个预防针:如果用 Tomcat 10 或更高,Servlet 的包名已经从 javax.servlet 换成了 jakarta.servlet,很多老源码直接跑会 ClassNotFound。这篇标题对应的项目源码大概率是按 Tomcat 8/9 写的,所以我建议你直接用 Tomcat 9,省去一批包名改造工作。项目放对位置也很关键,webapps/ROOT还是webapps/你的项目名会影响访问路径,我习惯放在webapps/hotel下,然后通过http://localhost:8080/hotel访问,这样后续部署到云服务器时路径不用改。

2.3 web.xml 是入口说明书:Servlet 映射与全局配置

打开 javaEE 老项目的第一件事,先看 web.xml。这个文件决定了哪些请求进哪个 Servlet、静态资源怎么放行、默认首页是谁。下面是我常用的最小配置:

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="3.1"> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.hotel.web.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login.do</url-pattern> </servlet-mapping> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

这里有两处值得注意。servlet-class必须和源码里的包名完全一致,大小写都不能错,否则启动时不报错,访问时才 404。url-pattern尽量用/login.do这种精确匹配,不要图省事写/*,因为/*会拦截所有请求,包括 JSP 页面本身的渲染请求,造成“所有页面白屏只有 Servlet 正常”的诡异现象。Servlet 版本 3.1 对应 Tomcat 8/9,如果你用注解@WebServlet("/login.do")替代 web.xml 配置,也可以,但论文里最好把 web.xml 的写法也放上去,因为它是 javaEE 规范的直观体现。

2.4 数据库连接配置:Druid 连接池与 jdbc.properties

数据库连接这块,最忌讳在 DAO 里DriverManager.getConnection()每次现连。酒店管理系统要扛住前台高频查询,连接池是必须的。常见做法是用 Druid,配置写在jdbc.properties里,便于替换环境:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hotel_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456 jdbc.maxActive=10 jdbc.initialSize=2

URL 里的参数每一个都是血泪教训。serverTimezone=Asia/Shanghai不写,MySQL 8 会直接报时区错误;useSSL=false是关掉本地开发环境的 SSL 证书校验,避免驱动每次连接都去验证证书;allowPublicKeyRetrieval=true解决 MySQL 8 的 caching_sha2_password 认证插件在非安全连接下取公钥的问题。characterEncoding=utf8必须写在 URL 里,不然哪怕数据库和页面都是 UTF-8,JDBC 传输过程中也会乱码。

配套的工具类一般是静态代码块加载配置并创建数据源:

public class DbUtil { private static DruidDataSource ds; static { try { Properties p = new Properties(); p.load(DbUtil.class.getClassLoader() .getResourceAsStream("jdbc.properties")); ds = (DruidDataSource) DruidDataSourceFactory.createDataSource(p); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConn() throws SQLException { return ds.getConnection(); } }

ResourceAsStream的路径是 classpath 根目录,所以jdbc.properties必须放在 src 根目录或 resources 目录下,放错位置会在静态块里抛空指针,这种启动报错最容易让人误判成“包没导全”。连接池的maxActive不要贪大,本地开发 10 个足够,设成 50 反而会让 MySQL 的线程数飙升,拖慢整机响应。

3. 数据库设计先行:把酒店业务钉进 MySQL 的 7 张核心表

3.1 从业务流程到表结构:先理清角色和状态再建表

酒店管理系统的数据库设计,核心是回答四个问题:谁在用系统、房间有什么状态、订单怎么流转、退房怎么算钱。我习惯把角色拆成三类:系统管理员管房型和房间信息,前台负责预订、入住、退房和结账,经理看营业报表。围绕这些角色,核心表可以收敛为 7 张:系统用户表sys_user、会员表t_member、房型表t_room_type、房间表t_room、预订订单表t_order、入住登记表t_checkin、消费明细表t_consume。

房型和房间为什么要拆两张表?因为“价格”和“房间”是一对多的关系,如果把价格直接写进房间表,改一次房价就要 UPDATE 几十行,而且容易漏。下面是一个精简但完整的建表片段:

CREATE TABLE t_room_type ( id INT PRIMARY KEY AUTO_INCREMENT, type_name VARCHAR(20) NOT NULL COMMENT '单人间/双人间/套房', base_price DECIMAL(10,2) NOT NULL COMMENT '基准价/晚', hourly_price DECIMAL(10,2) DEFAULT 0 COMMENT '钟点房价/小时', bed_num TINYINT DEFAULT 1 COMMENT '床位数', breakfast TINYINT DEFAULT 0 COMMENT '1含早 0不含早', status TINYINT DEFAULT 1 COMMENT '1启用 0停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(10) NOT NULL UNIQUE COMMENT '门牌号', room_type_id INT NOT NULL, floor_no TINYINT COMMENT '所在楼层', room_status TINYINT DEFAULT 0 COMMENT '0空闲 1已预订 2已入住 3维修 4脏房', remark VARCHAR(255), FOREIGN KEY (room_type_id) REFERENCES t_room_type(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

价格字段用DECIMAL(10,2)而不是 FLOAT,是因为浮点数在计算房费时会出现 0.1+0.2 不等于 0.3 的问题,金额出错在酒店这种场景里非常致命。room_status用 TINYINT 存数字而不是直接存中文,是为了后续状态流转时用 int 比较和更新更高效,中文状态值应该由前端字典翻译。UNIQUE约束加在room_no上,防止重复门牌号。所有表统一ENGINE=InnoDB,因为订单和入住登记需要事务支持,MyISAM 不支持行级锁和事务回滚。

接下来是订单表,它的设计决定了预订、入住、取消这条主链路的复杂度:

CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '订单号,格式:日期+随机数', room_id INT NOT NULL, cust_name VARCHAR(30) NOT NULL, cust_phone VARCHAR(20), member_id INT COMMENT '会员ID,可为空', plan_in_date DATE COMMENT '预计入住日期', plan_out_date DATE COMMENT '预计离店日期', hours INT DEFAULT 0 COMMENT '钟点房小时数', order_status TINYINT DEFAULT 0 COMMENT '0已预订 2已入住 3已取消 4已离店', pre_amount DECIMAL(10,2) COMMENT '预付金额', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_room_status (room_id, order_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表里我把预计入住和预计离店拆成两个 DATE 字段,而不是一个时间段字段,这样查“明天有哪些房要退”只需WHERE plan_out_date = DATE_ADD(CURDATE(), INTERVAL 1 DAY),索引也能用上。order_no不要用自增 ID 直接展示给客户,酒店行业习惯看到日期开头的单号,我一般用yyyyMMddHHmmss + 三位随机数生成。最后那行INDEX很重要,房态列表页最频繁的查询就是“按房间和时间过滤订单”,没有这个联合索引,数据量过万后会明显变慢。

3.2 房态流转别用字符串裸奔:状态机与条件更新

房间状态是整个系统的命脉。前台操作的每一步都在改t_room.room_status:空闲 → 已预订(客人下单)→ 已入住(客人到店)→ 脏房(退房后保洁)→ 空闲。如果代码里到处直接写UPDATE t_room SET room_status = 2 WHERE id = 1,不出一个星期就会出现“房间明明被预订了,另一个前台还能开出去”的并发问题。

我的做法是在状态流转处统一走 Service 方法,并且用条件 UPDATE 实现乐观锁:

START TRANSACTION; UPDATE t_room SET room_status = 2 WHERE id = #{roomId} AND room_status = 1; -- 受影响行数为 0 说明房间不是“已预订”状态,下单失败 INSERT INTO t_checkin (order_id, room_id, cust_name, real_in_time) VALUES (#{orderId}, #{roomId}, #{custName}, NOW()); COMMIT;

这段 SQL 的精髓在AND room_status = 1。如果两个前台同时操作同一间房,MySQL 的行锁会让后到的 UPDATE 等待,但等锁释放后,它的 WHERE 条件已经匹配不上最新状态,受影响行数为 0,代码里就能捕获到这个失败并提示“房间状态已变化”。这就是用数据库自身的能力做并发控制,比在 Java 代码里加 synchronized 靠谱得多,也更容易在答辩时讲出亮点。

另外强调一个容易忽略的细节:脏房状态一定要有。客人退房后房间需要保洁,如果直接置为“空闲”,下一个客人入住时发现房间没打扫就是事故。我见过不少毕设把状态只做成“已入住/空闲”两态,业务上其实是不完整的。哪怕你这个系统的保洁模块只是一个按钮,也要有“一键标记脏房”和“保洁完成恢复空闲”这两个动作,这会让评审觉得你想到了真实场景。

3.3 导入导出 SQL 脚本:导入前先建库,外键别硬刚

源码包里通常附带一个hotel.sql或db.sql,这是整个项目的数据库资产。拿到手先别急着source,我一般按下面这套流程走:

mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS hotel_db DEFAULT CHARACTER SET utf8mb4;" mysql -u root -p hotel_db < hotel_db.sql

第二条命令前必须先建库,因为脚本里多数只包含表和数据的 INSERT,不会帮你建库。如果脚本没有指定USE hotel_db;,导入时会一直报“No database selected”。另外很多脚本开头有DROP TABLE IF EXISTS,这本身没问题,但如果表之间有外键约束,DROP 父表时会失败。遇到这种情况先执行一句SET FOREIGN_KEY_CHECKS=0;再导,导完再设回 1。这个坑在 Workbench 里特别常见,明明点了 Forward Engineer,生成的脚本里 DROP 顺序却是乱的。

导出备份时用mysqldump更专业一些,尤其答辩前一定要备份一次:

mysqldump -u root -p hotel_db > hotel_db_backup.sql

如果是用 MySQL Workbench 管理表结构,注意导出时勾选 “Dump Stored Procedures and Functions” 和 “Dump Events”,否则你在 Workbench 里建的存储过程不会进到脚本里。字符集方面,建议建库时一律utf8mb4,不要用utf8,MySQL 的 utf8 实际是 utf8mb3,存不了 emoji 和部分生僻汉字,客人姓名里有生僻字时页面就会显示成问号。

4. 代码主线:从登录到退房结账的完整链路实现

4.1 登录与权限:一个 Session 撑起的最简 RBAC

登录是整个系统的大门,也是答辩时评委必看的第一段代码。常见做法是在login.jsp收集用户名和密码,提交到LoginServlet,验证成功后把用户信息放进 Session。注意两点:密码不能明文存库,前端传过来的密码要做哈希后比对;DAO 层必须用 PreparedStatement 防 SQL 注入。核心代码如下:

@WebServlet("/login.do") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = Md5Util.hash(req.getParameter("password")); SysUser user = new UserDao().findByNameAndPwd(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); session.setAttribute("role", user.getRole()); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } else { req.setAttribute("msg", "账号或密码不正确"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }

sendRedirect和forward的区别要搞清楚:登录成功后重定向,是为了避免刷新页面时重复提交表单;登录失败后转发,是为了把msg带回到登录页显示。DAO 层里的查询语句务必用占位符:

String sql = "SELECT * FROM sys_user WHERE username=? AND password=?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { ... } } }

为什么不直接String sql = "SELECT * FROM sys_user WHERE username='" + username + "'"?因为前台在用户名框里输入' OR '1'='1就能绕过密码直接登录,这在答辩现场是致命伤。做了 PreparedStatement 之后,评委如果追问安全加固,你还可以补一句“我对密码做了加盐哈希,并对 Session 里的角色做了 Filter 拦截”。

4.2 预订到入住:事务边界和房间锁定的实战写法

预订到入住这条链路,是最能体现工程能力的部分。业务流程是:前台选择房型 → 提交订单 → 系统把房间从“空闲”改为“已预订” → 客人到店后“入住登记” → 订单状态从“已预订”改为“已入住”。这两步必须在一个事务里,否则会出现订单插入成功但房间状态没变,或者房间被占但订单丢失的情况。

public boolean createOrder(Order order) { Connection conn = null; try { conn = DbUtil.getConn(); conn.setAutoCommit(false); RoomDao roomDao = new RoomDao(); int rows = roomDao.updateStatus(2, 1, order.getRoomId()); if (rows == 0) { conn.rollback(); return false; } orderDao.insert(order); conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException("预订下单失败", e); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { } } } }

这段代码里roomDao.updateStatus(2, 1, order.getRoomId())对应的是UPDATE t_room SET room_status = 2 WHERE id = ? AND room_status = 1,受影响行数为 0 说明房间状态已经不是“空闲”,直接回滚,避免脏数据。事务边界要覆盖“改状态”和“插订单”两个动作,少了任何一个都不完整。最后在 finally 里把autoCommit恢复成 true 再关连接,是因为连接池里的连接是复用的,如果不重置状态,下一次拿到这条连接的人会在一个残留的事务里执行 SQL,查不到数据时很难排查。

4.3 退房结账算法:统一按自然日计费,别用浮点数算钱

退房结账是酒店管理系统里最有“业务含金量”的部分。房费怎么算?业界常见做法是“按自然日计费”,即入住当天算一天,离店日的中午 12 点前退房不额外收费,超过 12 点加收半天或一天房费。你不需要做得和真实 PMS 一样复杂,但至少要把“天数 × 房价 + 消费 - 折扣”这条公式写对。

public BigDecimal calcAmount(Order order, List<ConsumeItem> items) { long days = ChronoUnit.DAYS.between( order.getPlanInDate(), order.getPlanOutDate()); BigDecimal roomFee = order.getBasePrice() .multiply(BigDecimal.valueOf(days)); BigDecimal total = roomFee; for (ConsumeItem item : items) { total = total.add(item.getAmount()); } if (order.getMember() != null) { total = total.multiply(order.getMember().getDiscount()); } return total.setScale(2, RoundingMode.HALF_UP); }

ChronoUnit.DAYS.between计算两个 LocalDate 之间的天数,注意这个方法返回的是 long,如果退房日等于入住日,返回 0,分钟房订单要走单独的分支。所有金额运算必须用BigDecimal,乘法之后调用setScale(2, RoundingMode.HALF_UP)做四舍五入,否则会出现 113.500000001 这种结果。会员折扣如果存在t_member表里是 0.9 这种小数,做百分比换算时也要用 BigDecimal,不能转 double 后再回来。这段算法建议单独抽一个PriceCalculator类,不要写在 JSP 里,面试时把类名报出来,会比“我的代码都写在 Servlet 里”听起来专业得多。

5. 避坑清单:本地能跑不代表答辩能过

5.1 JDBC 连 MySQL 8 报 SSL 与时区错误

现象:项目启动后第一次访问数据库,控制台抛The server time zone value '�й���ʱ��' is unrecognized或者Establishing SSL connection without server's identity verification is not recommended,前端页面上表现为登录按钮点了没反应,后台却能看到 SQLException。

原因:MySQL 8 的 JDBC 驱动com.mysql.cj.jdbc.Driver默认要求带时区参数,而连接串里没写;同时驱动默认会去校验 SSL 证书,本地自建库没有合理证书就会报警告甚至连接失败。

解决:在jdbc.url后追加useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true。useSSL=false的意思是本地开发环境不走证书验证,只要不是公网传输敏感数据,这个参数没问题。如果用的是 MySQL 5.7,驱动可以换com.mysql.jdbc.Driver或不加 serverTimezone,但为了兼容性,我统一按 8 的写法来。另外确认mysql-connector-java的 jar 包版本和 MySQL 服务端版本不要差太远,5.x 的驱动去连 8.x 服务端会直接报Unable to load authentication plugin 'caching_sha2_password'。

5.2 连不上本地 MySQL:socket 文件、端口和认证插件三连

现象:命令行执行mysql -u root -p报ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock',或者 Java 代码里报Communications link failure。

原因:这条报错的意思是客户端默认通过 unix socket 文件连本地库,但/tmp/mysql.sock不存在,最直接的原因是服务没启动,其次是 MySQL 的 socket 文件路径被改过。如果你在 Java 里用localhost连接,驱动也会优先走 socket 而不是 TCP。

解决:先确认服务真的活着:

systemctl status mysql mysqladmin -u root -p status

如果服务活着但 socket 路径不对,一条路是找到my.cnf里socket=的路径,另一条更省事的办法是在 JDBC URL 里把localhost改成127.0.0.1,强制走 TCP 端口,绕开 socket 文件。按下葫芦浮起瓢的情况也有:明明 MySQL 起来了,端口却是 3307,这种多实例安装导致的坑,用netstat -tlnp | grep 3306看端口占用就能定位。最后还有一种排查点:root 账号在 MySQL 8 默认用caching_sha2_password认证,老驱动不支持时,可以考虑把账号改成mysql_native_password,这属于常规兼容操作。

5.3 中文乱码:从过滤器、连接参数到建表字符集四层排查

现象:登录页面输入中文用户名,提交后变成“???”,或数据库表里看中文是乱码,又或者页面能显示中文但查询条件带中文查不到数据。

原因:乱码是四层字符集不一致叠加的结果,谁都不能独善其身。POST 请求体没有设置 UTF-8;JSP 页面本身pageEncoding不对;JDBC 连接串里没带characterEncoding=utf8;建表时用了latin1默认字符集。任何一层断了,最终表现都是乱码。

解决:从上到下一层层堵漏。最省事的做法是加一个全局编码过滤器,覆盖所有请求:

@WebFilter("/*") public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html; charset=UTF-8"); chain.doFilter(req, resp); } }

有了这个过滤器,POST 请求体里的中文基本稳了。连接串里的characterEncoding=utf8负责 JDBC 传输层,建库时DEFAULT CHARSET=utf8mb4负责存储层,JSP 头部的pageEncoding="UTF-8"负责视图层。四层都对齐后,把表里已存在的乱码数据 DELETE 掉重新录入,不要想着 UPDATE 能救回来,乱码数据通常是不可逆的。

5.4 Tomcat 10 换包名导致 404 与 ClassNotFound:javax 和 jakarta 的分水岭

现象:同一套源码,照着教程配 Tomcat 10,项目能启动,但访问 Servlet 路径时 404,后台报java.lang.NoClassDefFoundError: javax/servlet/ServletException或ClassNotFoundException: javax.servlet.http.HttpServlet。

原因:Tomcat 10 开始全面迁移到 Jakarta EE 9,Servlet API 的包名从javax.servlet改成了jakarta.servlet。老项目源码里的import javax.servlet.*在 Tomcat 10 的 classpath 里找不到对应类,但 Tomcat 本身又用了一些兼容机制,所以启动阶段不一定报错,直到真正加载 Servlet 类才炸。

解决:两条路。第一,直接换回 Tomcat 9,这是最不折腾的方案,源码里所有javax.servlet都不用动,JDK 8 或 11 都支持。第二,坚持用 Tomcat 10 的话,用 IDEA 或 VSCode 的全局替换,把所有import javax.servlet.改成import jakarta.servlet.,同时把 web.xml 里的xmlns命名空间也改成 Jakarta EE 的版本。对毕业设计来说,我强烈建议选第一条路,因为你现在的时间应该花在业务和答辩上,而不是给 Servlet 包名做迁移。顺带提醒,源码包里提供的readme.txt如果写了“运行环境:Tomcat 8/9”,就老老实实按它说的配,别自己升级到 Tomcat 10 再花两天排错。

6. 答辩展示与项目进化:把演示视频和存储过程变成加分项

6.1 演示视频也是技术资产:按业务剧本录,别录操作流水账

标题里那套视频资源,别只把它当作“给老师看一眼的东西”。我的经验是:越是这种成套交付的毕设,越要自己重录一版演示视频,因为别人的视频里项目名可能和你的论文题目不一致。录制时按“预订 → 入住登记 → 消费记账 → 退房结账 → 营业报表”这条业务链来剪,每个步骤 3 到 5 秒的停留即可,后台代码不用录进去,但数据库表要切出来看一眼,让评委知道数据是真实落库的。视频里最好单独录一段异常操作:比如重复预订同一间房时系统的报错提示,这在答辩时主动讲出来,比被动回答“并发怎么办”更有说服力。

6.2 加一个 MySQL 存储过程做日报汇总,马上拔高项目档次

如果你的代码包里没有存储过程,强烈建议加一个。酒店管理系统天然适合用存储过程做夜间统计,因为房间数有限,统计逻辑固定,不需要高并发,数据库端跑批量任务正合适。比如生成每日营业报表:

DELIMITER $$ CREATE PROCEDURE proc_daily_report(IN p_date DATE) BEGIN INSERT INTO t_report (report_date, order_count, room_income, consume_income) SELECT p_date, COUNT(DISTINCT order_id), SUM(room_fee), SUM(consume_fee) FROM t_checkin WHERE DATE(create_time) = p_date; END$$ DELIMITER ;

在答辩时讲清楚“我为什么不用 Java 在代码里统计”:因为报表统计要扫大量订单数据,放到数据库端执行可以减少网络传输,而且可以挂在 MySQL 事件调度器里每天凌晨自动跑。就这一句话,立刻让你的项目从“CRUD 管理系统”变成“有真实工程考虑的系统”。另一个低成本亮点是用第四章讲的乐观锁房态流转,把同一间房并发预订的失败场景截图放进论文,配上行的锁机制解释,这是评委最想看的“异常处理”素材。

最后说个我做毕设辅导时的习惯:每次答辩前,我会把项目当产品重新过一遍,专门挑那些“正常人不会点”的操作去试,比如在预订页直接改成房号 999,看看系统有没有报错提示;把系统日期改到跨月,看看报表统计是否正常。这些不是刁难自己,而是提前把黑匣子摸清,总比你站在台上被评委问到哑口无言强。希望这篇笔记能帮你把 javaEE+MySQL 这个经典组合做成一份经得起追问的作品。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询