最近好几个读者私信我问课程设计想做一个会议室管理系统用 JSPServletMySQL 到底能不能行。我的回答是太能行了。会议室管理系统是我见过的最适合当 JavaWeb 入门实战的题目之一需求一听就懂业务不绕弯但背后又牵扯到用户权限、会议室状态、预订冲突、审批流程这些真实系统里躲不开的细节。今天我就把自己做过的一个基于 javaweb 和 mysql 的 jspservlet 会议室管理系统完整复盘一遍从数据库设计到页面展示再到打包部署全程只用 javajsphtmlservletmysql 这五样东西。如果你是正在做课程设计或者刚学完 Servlet 想练手按这篇文章的思路走绝对比自己闷头写省事得多。这个项目技术栈确实不算新但老有老的好处每一个请求从浏览器到 Servlet再到数据库全程透明可见没有框架替你把这些细节藏起来。做完它以后你再去看 SpringBoot 里那些“约定大于配置”的东西会顺手很多。这篇就把功能拆解、表结构、搭建步骤、核心代码、常见坑一条条理清楚你可以直接当成一份参考答案来用。1. 项目拆解会议室管理系统到底在管什么1.1 核心业务需求与功能边界会议室管理系统的目标很明确解决企业里订会议室靠口头沟通、时间撞车、资源浪费的问题。围绕这个目标功能可以拆成几个大块。第一块是用户管理。系统至少要有两类角色普通员工和管理员。普通员工能注册、登录、修改自己的信息管理员能查看用户列表、启用或禁用账号。这里要注意做课程设计的时候不必把用户管理做得太复杂像密码找回、邮箱验证这类功能能省就省但登录和角色权限必须有否则后面会议室预订的“审批”就没法闭环。第二块是会议室信息维护。管理员要能新增会议室维护房间编号、名称、位置、容纳人数、设备信息还能启用或停用某个会议室。普通员工则只能查看这些信息不能修改。有的同学会把会议室增删改查写成四个独立的 Servlet当然没问题但是更好的做法是统一用一个 RoomServlet 加一个 type 参数区分动作代码量能少不少。第三块是预订管理。这是整个系统的核心也最值得花精力。员工提交预订申请时需要选择会议室、预订日期、开始时间、结束时间还要填用途提交后进入待审批状态。管理员看到申请后可以同意或拒绝。预订状态一般分四种待审批、已通过、已拒绝、已取消。这里我建议把“已取消”也做进去因为员工临时改期取消会议很常见没有这个状态数据就会出现“明明取消了会议室还被占着”的尴尬情况。第四块是查询和统计。员工可以看自己提交过的预订记录管理员可以看所有记录也可以按日期、状态、会议室去筛选。统计报表属于加分项比如当月各会议室使用率、最抢手的时间段做出来能明显提升项目评分但基础查询必须先做好。总结下来这是一个非常标准的“权限 资源 预约”业务模型。没有复杂算法没有高并发压力但数据表之间的关系、事务操作、时间重叠判断这些真实开发里天天打交道的知识点全都有。这也是我为什么推荐它作为课程设计或 JavaWeb 练手项目的原因工作量适中但技术覆盖面很全。1.2 基于JSPServletMySQL的选型理由很多人会问现在都 2025 年了为什么还要用 JSPServlet 这种老组合直接上 SpringBoot MyBatis Plus 不香吗这个问题我在带新人的时候经常被问到我的回答是学习阶段看的是“掌握原理”生产阶段才看“开发效率”。JSPServlet 是 JavaWeb 的地基。Servlet 让你理解 HTTP 请求怎么被接收、参数怎么解析、响应怎么返回JSP 让你理解动态页面是怎么被翻译成 Servlet 然后输出的JDBC 让你理解数据库连接从哪来、SQL 怎么执行、结果集怎么遍历。这些底层机制在 SpringBoot 里全被封装了你写起来确实快但出了问题往往一头雾水。比如你在 SpringBoot 里遇到一个“数据库连接失败”你很难说清楚是驱动问题、连接池问题还是配置问题而在纯 JSPServlet 项目里你亲手写一遍 DBUtil自然就明白“驱动注册—连接创建—语句执行”这条链路。再者这个技术栈部署简单。一个 Tomcat一个 war 包丢进去就能跑。不依赖 Maven 中央仓库不依赖复杂的构建流程非常适合课程设计答辩现场演示。如果你在答辩时能用白话说清楚 Filter 是怎么拦截请求的Servlet 生命周期有哪几步老师一般会另眼相看。那 JSPServlet 有没有缺点当然有。页面和 Java 代码混在一起维护起来比较难受没有自动装配配置全靠 web.xmlJDBC 的样板代码太多写业务很啰嗦。所以做这个项目的时候我建议你在代码分层上多下功夫用 DAO、Service、Servlet 三层结构把业务逻辑从 Servlet 里抽出来这样既保留传统技术栈的学习价值又不至于把代码写成一团乱麻。2. 数据库设计与表结构规划2.1 关键数据表与字段设计数据库设计是会议室管理系统里最不能省的一步。我见过不少同学上来就写代码写到后面发现表缺字段、字段类型不对又回来改反而耽误时间。这套项目用三张核心表就够了用户表、会议室表、预订表。先看用户表CREATE DATABASE meeting_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE meeting_system; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(64) NOT NULL COMMENT 密码存哈希值不存明文, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色1普通员工2管理员, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个设计细节要注意。第一username 必须加 UNIQUE 约束否则注册时还得先查一遍而且并发下可能重复。第二password 字段长度我特意写成 64因为不管是 MD5、SHA-256 还是加盐哈希摘要长度都在这附近。数据库里存明文密码是非常坏的习惯做课程设计也千万别图省事。再看会议室表CREATE TABLE t_room ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 会议室ID, room_no VARCHAR(20) NOT NULL UNIQUE COMMENT 房间编号如 M201, room_name VARCHAR(100) NOT NULL COMMENT 会议室名称, location VARCHAR(100) COMMENT 所在位置如南区3楼, capacity INT NOT NULL DEFAULT 0 COMMENT 容纳人数, equipment VARCHAR(255) COMMENT 设备说明如投影仪、视频会议终端, status TINYINT NOT NULL DEFAULT 1 COMMENT 1可用0停用, description VARCHAR(500) COMMENT 备注 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;会议室表本身不复杂但 status 字段别漏了。管理员把某个会议室临时停用后普通员工应该看不到它或者预订时提示不可选。这个状态位可以通过查询条件直接过滤比删除记录好很多。预订表是最核心的一张表CREATE TABLE t_booking ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 预订ID, room_id INT NOT NULL COMMENT 会议室ID, user_id INT NOT NULL COMMENT 申请人ID, book_date DATE NOT NULL COMMENT 预订日期, start_time TIME NOT NULL COMMENT 开始时间, end_time TIME NOT NULL COMMENT 结束时间, purpose VARCHAR(255) COMMENT 预订用途, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待审批2已通过3已拒绝4已取消, approver_id INT NULL COMMENT 审批人ID, approve_time DATETIME NULL COMMENT 审批时间, remark VARCHAR(500) COMMENT 备注, KEY idx_room_date (room_id, book_date), KEY idx_user_booking (user_id, book_date), CONSTRAINT fk_booking_room FOREIGN KEY (room_id) REFERENCES t_room(id), CONSTRAINT fk_booking_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;时间字段我拆成了 book_date、start_time、end_time 三个而不是把一个时间段放在一个 VARCHAR 字段里。这样做的好处是 SQL 可以直接用比较运算符判断时间重叠查询效率更高代码也更清晰。状态字段我用 TINYINT 而不是 VARCHAR因为数字状态更容易扩展占用空间也小。如果你需要给人看“当前是待审批”可以在 JSP 里通过 JSTL 判断。2.2 外键、索引与数据完整性数据完整性这块要在业务需求和实现复杂度之间找平衡。我在这个项目里给 t_booking 加了外键目的是防止出现预订记录指向不存在的会议室或用户。不过外键在真实的高并发系统里经常被刻意不用因为会影响插入性能和删除灵活性。课程设计阶段用外键没问题能体现你对数据一致性的理解。索引方面不要乱加。这里的高频查询场景是查某个会议室在某个日期有没有被预订。所以我在 room_id 和 book_date 上建了联合索引。用户查看“我的预订”也是高频操作所以在 user_id 和 book_date 上再建一个索引。其他字段暂时不建索引因为数据量小索引太多反而浪费维护成本。数据库层面最难保证的是“同一会议室同一时间段不能重复预订”。这个约束理论上可以用触发器或唯一约束实现但时间段是区间实现起来很绕。更常见的做法是在应用层做判断先查询冲突记录没有再插入。不过这会有并发风险两个用户同时提交相同时间段可能出现查的时候都为空插入时两条都成功。解决方案有两个方向一是给该会议室当天的记录加 SELECT...FOR UPDATE 锁二是用事务把“冲突检查 插入”包起来配合乐观锁或唯一业务键。后面在核心代码部分我会详细讲怎么落地。顺便再提一下数据初始化的建议。给系统准备的初始数据不要只放一个管理员账号还要放两条会议室数据和几条预订记录方便你调试页面。我习惯把所有建表和初始化语句写在一个 init.sql 脚本里按照 用户-会议室-预订 的顺序执行。这样项目换一台电脑也能快速跑起来不用担心手动点鼠标把数据造得乱七八糟。3. 开发环境与项目搭建3.1 工具链准备JDK、Tomcat、MySQL、IDEA开始写代码前先把环境整理干净。我的推荐版本组合是 JDK 1.8 Tomcat 9 MySQL 8.0 IDEA 社区版这一套在 Windows 上最容易踩坑但资料也多遇到问题随便一搜就有答案。如果你已经装了 JDK 17 也没关系Tomcat 9 兼容 JDK 8 以上版本但要注意不要选 Tomcat 10 或 11因为 Tomcat 10 开始把 javax.servlet 包改成了 jakarta.servlet传统 JSPServlet 项目的依赖需要跟着换课程设计没必要给自己添乱。MySQL 的安装这里不展开讲只提醒几个关键点。第一MySQL 8 以后的驱动类名是 com.mysql.cj.jdbc.Driver不是以前的 com.mysql.jdbc.Driver很多同学在这卡半天。第二连接 URL 里要加 serverTimezoneAsia/Shanghai 和 useSSLfalse否则要么报时区错误要么报 SSL 连接问题。第三密码别用太复杂的学习项目里面 root 密码设成 123456 都行但要记住它是写在代码里的项目打包部署后别把真实生产环境密码也这么搞。IDEA 里配置 Tomcat 是新手最容易懵的一步。操作是Run - Edit Configurations - 左上角加号 - Tomcat Server - Local。在 Server 页签里 Application server 选到你本地 Tomcat 解压目录然后在 Deployment 页签点加号选择 Artifact。这里有两种 Artifact推荐用“war exploded”模式意思是不打成压缩包直接运行编译后的目录这样改 JSP 后刷新页面就能看到效果调试效率高很多。Application context 建议改成 /meeting这样访问地址就是 http://localhost:8080/meeting/比默认带一堆随机字符的 url 看着舒服。还有一个常见问题是把 Tomcat 装了以后端口被占用。启动时报 8080 端口被占用要么是之前有进程没杀掉要么是其他服务占用了。可以在 Tomcat 的 conf/server.xml 里改端口也可以直接找到占用进程杀掉。我一般建议改端口比如改成 8088一劳永逸因为被杀掉的进程可能过一会儿又起来。3.2 从空白目录搭建一个可运行的JavaWeb项目我不是很推荐用 IDEA 自带的 Java Enterprise 模板创建项目因为默认生成的目录结构比较复杂对新手不友好。我更常用的是最笨的方法先建一个普通 Java 项目然后手动添加 Web 支持。在建好项目后右键模块 - Add Framework Support - 勾选 Web Application。IDEA 会自动帮你生成 web 目录然后在 web 目录下建 WEB-INF 子目录再把 web.xml 放进去。如果你下载的项目源码没有 war 包这种方式最快能跑起来。项目整体目录结构我建议这样分meeting-room-system/ ├── src/ │ └── com/meeting/ │ ├── dao/ # 数据访问层 │ ├── service/ # 业务逻辑层 │ ├── servlet/ # 控制器 │ ├── filter/ # 过滤器 │ ├── util/ # 工具类 │ └── entity/ # 实体类 ├── web/ │ ├── static/ # css/js/images │ ├── WEB-INF/ │ │ ├── web.xml │ │ └── lib/ # 放 JDBC 驱动、JSTL 依赖 │ ├── login.jsp │ ├── index.jsp │ └── ... └── src/main/resources/ └── db.properties新手经常犯一个错误把 JDBC 驱动 jar 包放在桌面上然后类加载死活找不到驱动。记住只要是 Web 项目外部 jar 包必须放在 WEB-INF/lib 目录下Tomcat 启动时才会把它们加载到应用的类路径里。如果项目里用了 JSTL也需要把 jstl 和 standard 两个 jar 包放在这个目录。写完目录结构后最重要的一步是配置 web.xml。我一般会在里面配三样东西编码过滤器、会话过期时间、Servlet 映射。字符编码过滤器我会单独写一个类放到 filter 包里避免在每个 JSP 里重复写编码声明。web.xml 的版本声明建议用 3.1兼容 Tomcat 9又不会引入太新的语法。?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 filter filter-nameencodingFilter/filter-name filter-classcom.meeting.filter.EncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping session-config session-timeout30/session-timeout /session-config /web-app有的同学喜欢用 WebServlet 注解而不是在 web.xml 里配置那也可以。但从“理解 URL 映射”的角度我建议至少把核心的几个 Servlet 在 web.xml 里配一遍你会对 Servlet 的注册和映射关系有更直观的认识。等配完这些项目就已经是一个能被 Tomcat 加载的空壳了。下一步就是连接数据库把第一行数据查出来。JDBC 工具类我上面已经提过这里再说一个细节从字节码里加载配置文件比如 db.properties建议放在 src 目录下用DBUtil.class.getClassLoader().getResourceAsStream(db.properties)读取而不是用文件路径去读。因为打包成 war 后文件会被放进 WEB-INF/classes 下用文件路径很容易找不到。4. 核心功能实现与代码思路4.1 用户登录与权限控制登录是整个系统第一个要写的功能也是一个 Filter 的最佳实践场景。我先把逻辑走一遍登录页提交用户名和密码LoginServlet 接收到参数后调 UserDao 查库。这里有一个特别容易踩的坑密码必须做哈希后再比对不能把明文密码拼到 SQL 里。我用的是 MD5虽然 MD5 本身已经被认为不安全但在这个教学项目里够用而且代码简洁。更讲究一点可以用 SHA-256 加盐不过没必要在课程设计里过度设计。核心代码大概是这样的protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password DigestUtils.md5Hex(req.getParameter(password)); User user userDao.findByUsername(username); if (user ! null user.getPassword().equals(password)) { req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } }注意登录成功后我用的是重定向而不是 forward。为什么因为如果用 forward用户按 F5 刷新浏览器时表单会重复提交。重定向会把浏览器地址变成新页面再刷新只是刷新这个新页面不会重复交登录请求。这个细节我经常强调是很实用的经验。登录后的页面权限靠 Filter 统一控制。我写了一个 AuthFilter拦截所有需要登录才能访问的路径放行登录页和静态资源。判断逻辑很简单从 session 里取 loginUser取不到就跳回 login.jsp。要注意的一点是 filter 拦截范围不要过宽。如果项目里有 css、js、图片这些静态资源别把整个 /* 都拦住否则页面加载时样式全丢还找不出原因。我的做法是判断请求路径凡是 /login 开头、/static/ 开头或者以 .css / .js / .png 结尾的直接放行。管理员权限可以再细分。比如添加会议室、审批预订这些操作普通员工肯定不能访问。我一般会在 Servlet 里抽一个方法private void checkAdmin(HttpServletRequest req) throws BusinessException { User user (User) req.getSession().getAttribute(loginUser); if (user null || !2.equals(user.getRole())) { throw new BusinessException(无权限访问); } }然后在 doPost 开头调用。这样写比每个方法里都来一遍 if 判断要干净。4.2 会议室预订冲突判断与事务预订是系统里最有技术含量的功能因为它必须保证同一会议室在同一时间段不能被两个人订走。我之前在学校项目里看到过很多错误写法最典型的就是只在页面端做一个“时间段是否可选”的校验或者在后端查一次没问题就插入完全没考虑并发。这个功能你不能只把它当增删改查来做。我推荐的后端逻辑分成三步第一步拿到表单提交的 roomId、bookDate、startTime、endTime第二步事务内锁住该会议室当天的预订记录第三步做重叠查询如果没重叠才执行插入。时间重叠的判断 SQL 是关键。假设新预订是从 startNew 到 endNew已有预订是从 startOld 到 endOld判断两者是否重叠的条件是SELECT COUNT(*) FROM t_booking WHERE room_id ? AND book_date ? AND status IN (1, 2) AND start_time ? AND end_time ?这个 SQL 里的两个参数分别是新的结束时间和新的开始时间。为什么是 start_time 新结束时间 并且 end_time 新开始时间因为两个区间重叠的必要条件就是旧的开始时间早于新结束时间且旧结束时间晚于新开始时间。这个判断我在白纸上画过很多次时间轴你最好也画一遍比死记硬背可靠。然后加上事务和锁Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 锁住该会议室当天的记录防止并发提交 bookingDao.lockRoomForDate(conn, roomId, bookDate); int count bookingDao.countConflict(conn, roomId, bookDate, startTime, endTime); if (count 0) { throw new BusinessException(当前时间段已被预订请换一个时间); } bookingDao.insert(conn, booking); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(null, null, conn); }lockRoomForDate 的实现用的是SELECT ... FOR UPDATESELECT id FROM t_booking WHERE room_id ? AND book_date ? FOR UPDATE;如果是 InnoDB在 room_id 和 book_date 有索引的情况下这会对相关记录加锁避免两个事务同时插入重叠记录。教学项目里并发量不会很大但写出这个方案至少说明你理解事务和锁的概念面试也是加分项。如果你不想用锁那么重也可以做一个乐观锁方案在 t_booking 表加一个 version 字段插入前查当前版本号插入时带上 version 条件更新影响行数为 0 就说明冲突了。不过这个方案实现起来更绕我建议还是用事务加 SELECT FOR UPDATE思路清晰代码好写。另外还有一个管理端功能很值得做审批。审批本身就是一次 UPDATE 操作把 status 从 1 改成 2 或 3同时记录审批人 ID 和审批时间。审批通过后会议室该时间段就不可用了。要注意的是审批拒绝的时候最好让管理员填一下拒绝原因存到 remark 字段里员工那边能看见体验会好很多。4.3 页面展示与JSP复用传统 JSP 项目最容易写烂的就是页面因为可以在一个 JSP 里同时写 HTML、Java 代码、SQL 查询最后变成一锅粥。我的经验是JSP 里尽量不要出现大段的 Java 代码能用 JSTL 和 EL 表达式解决的问题就用它们解决。比如会议室列表页面我会先从 RoomServlet 查好数据放进 request然后让 JSP 来展示。Servlet 里一般是ListRoom roomList roomService.getAllRooms(); request.setAttribute(roomList, roomList); request.getRequestDispatcher(/room_list.jsp).forward(request, response);JSP 页面的主体用c:forEach遍历配合${room.roomName}这种 EL 表达式取字段代码干净很多% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body table tr th会议室/th th容量/th th设备/th th状态/th th操作/th /tr c:forEach items${roomList} varroom tr td${room.roomName}/td td${room.capacity}/td td${room.equipment}/td td${room.status 1 ? 可用 : 停用}/td tda href${pageContext.request.contextPath}/room/detail?id${room.id}详情/a/td /tr /c:forEach /table /body /html这里必须强调${pageContext.request.contextPath}这个用法。很多新手写静态资源路径时直接写/static/css/style.css结果部署到不同 context 下就 404。把它拼上 contextPath代码在任意路径下都能正常工作。这是个良好的习惯从一开始就要养成。页面复用方面导航栏、页脚这种每个页面都有的部分用jsp:include pageheader.jsp /抽出去。头信息、登录状态判断也都可以放进去。如果你不想写重复的head内容可以把它们抽到一个 head.jsp 里。我见过很多课程设计项目十个页面复制了十份导航栏改个名字要改十个文件这种代码评分的时候很掉价。前端交互方面不需要引入特别重的框架。我用的是原生 HTML 加少量 JavaScript。预订页面可以写一个简单的时间段选择用两个input typetime然后在前端校验结束时间必须大于开始时间。注意 HTML5 的time输入类型在浏览器里渲染很友好但提交的数据格式是字符串后端还是要用 LocalTime 或 Time 类型来解析。5. 部署、打包与常见问题排查5.1 本地部署和war包发布项目写完以后最终要能在你自己机器上跑最好还能打包成一个 war 包放到别的 Tomcat 里也能跑。打包之前先确认三件事数据库脚本能不能从头执行一遍、db.properties 里的数据库密码是否正确、项目里有没有引用绝对的磁盘路径。我一般会在项目交付前把数据库删掉重建一次再执行 init.sql 脚本保证新环境可以复现。在 IDEA 里打 war 包很简单File - Project Structure - Artifacts - 点加号 - Web Application: Archive。注意这里要保证 Output Layout 里有编译后的 classes 和所有 jar 包WEB-INF/lib、WEB-INF/classes 都要在。然后 Build - Build Artifacts - Rebuildwar 包就会生成到 out/artifacts 目录下。把 war 包复制到 Tomcat 的 webapps 目录启动 Tomcat它会自动解压部署。此时访问地址要注意war 包文件名就是上下文路径。比如你打包成 meeting.war访问地址就是 http://localhost:8080/meeting/。如果访问根路径出现 404 或者跳转到 Tomcat 首页多半是上下文路径没写对不要慌检查一下 war 包名字就行。还有一个小技巧开发调试时建议用 IDEA 的 Tomcat 集成运行用 exploded 模式。这样改 JSP 页面后按 CtrlF10 或者直接刷新浏览器在设置了更新资源自动热部署的前提下就能看到效果不用每次重启 Tomcat。改 Java 代码则需要重新编译并重启应用但也比手动重新部署快很多。5.2 高频Bug与避坑清单下面这个表是我做这个项目过程中还有带学生做类似项目时踩过的高频问题清理成了速查表。每一条都是真实遇到过的不是网上抄来的。现象可能原因解决办法启动 Tomcat 提示 8080 端口被占用有别的服务或残留进程占用了端口换端口conf/server.xml 里改 8080 为其他端口ClassNotFoundException: com.mysql.jdbc.Driver用了 MySQL 8 驱动但还写旧驱动类名改成 com.mysql.cj.jdbc.Driver或换旧版本驱动并改回数据库连接报 SSL 或时区错误MySQL 8 默认要求 SSL系统时区未设置JDBC URL 加 useSSLfalseserverTimezoneAsia/Shanghai页面中文全部变成问号页面编码、请求编码、数据库编码不一致JSP 头声明 UTF-8EncodingFilter 设为 UTF-8URL 加 characterEncodingutf8建库用 utf8mb4登录成功页面刷新后重复提交Servlet forward 到了登录后才能看到的页面登录成功用 sendRedirect 重定向不要 forward404 访问不到页面或 Servlet上下文路径写错或 Artifact 没部署访问用 http://localhost:8080/meeting/xxxIDEA 里检查 Deployment所有页面样式丢失静态资源路径没有加 contextPath引入 css/js 时使用 ${pageContext.request.contextPath}查询报空指针ResultSet 没有先 next() 就直接 getXxx先 if (rs.next()) 判断再取值或者用 while 循环时间能提交成功但冲突判断失效前端传的时间格式与数据库不一致后端统一用字符串转 LocalTime或直接用 SQL 时间参数重复点击预订按钮插入多条记录前端没防重后端也没做冲突事务前端提交后 disable 按钮后端事务加 SELECT FOR UPDATE这里面最值得展开说的是数据库连接 URL。我好几次看到同学从网上复制的旧配置写成 jdbc:mysql://localhost:3306/meeting_system 没有参数报错后一脸茫然。MySQL 8 必须有 serverTimezone不写会报 CST 相关异常SSL 虽然不强制关但本地开发没配证书写上 useSSLfalse 省得看警告。这些参数以后在 SpringBoot 里一样会用到现在记住了不吃亏。另外有一个安全习惯必须提写 SQL 的时候永远不要用字符串拼接用户输入。比如登录查询你千万别写String sql SELECT * FROM t_user WHERE username username ;这样写不光会让系统直接暴露在 SQL 注入风险下而且代码风格也很糟糕。正确做法是用 PreparedStatement 占位符String sql SELECT * FROM t_user WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery();这样做的好处是参数会被数据库驱动自动转义输入里带引号、分号都不会破坏 SQL 结构。这也是面试时必问的一个点项目里用了 PreparedStatement就能把这个知识点讲得很扎实。做完这个项目我个人最深的体会是传统 JSPServlet 项目虽然看着“土”但反而能用最少的知识依赖把一个完整业务流程撑起来。你做一遍以后脑子里会死死记住什么叫事务、什么叫请求转发和重定向的区别、什么叫 Session 和 Cookie 的配合这些才是后面学任何框架都绕不开的核心。如果再让我给这个项目选一个最重要的功能我会首选预订冲突处理因为你把时间重叠判断和事务提交写好之后整系统的含金量立刻就不一样了。最后分享一个小技巧开发时别忘了在控制台打印执行的 SQL。最简单的方式是在 DAO 层用 System.out.println 把 PreparedStatement 的 SQL 和传入参数打出来虽然不优雅但排查问题非常快。等你哪天觉得手写 JDBC 太啰嗦了再换成 MyBatis会发现 console 里打印的正是你当时手写的那条 SQL。