简介这是一套面向Java Web初学者与课程设计者的会议室预约系统完整源码基于JSPServletJavaBean技术栈实现可帮助读者理解企业级资源管理系统的开发流程。系统区分管理员与员工两类角色管理员负责部门、员工账户、会议室信息及公告的增删改查与权限分配员工则可查看公告、按日期时间预订或取消会议室覆盖了权限控制、数据库CRUD、表单验证与session安全等典型知识点。压缩包共744个文件约5.01MB以gif、js、html、png、css等前端资源为主另含jsp页面、jar依赖、sql脚本及少量java源码便于直接部署到Tomcat运行调试。目前已有1657人学习下载适合作为毕业设计、课程作业或JSP入门练手项目读者可从中获取完整的目录结构、数据库设计与前后端交互实现思路。1. 会议室预约系统从 Excel 排期到 JSP 全栈落地的真实距离行政群里又炸了锅——三楼那间能坐 12 个人的会议室同时被三个部门写进了各自的日程表。这种场景你大概率不陌生共享表格靠自觉日历工具各用各的最后总有人抱着电脑在门口干等。这套 JAVA JSP 会议室预约系统要解决的就是把「谁在什么时候占了哪间房」这件事从口头约定变成数据库里的事务。它适合两类人一是需要交课程设计或毕业设计的同学二是小团队里想搭个内部预约工具、又不想引入太重框架的开发者。技术栈是经典的 JSP Servlet JDBC MySQL部署在 Tomcat 上没有 Spring 那套全家桶读起来不绕。下面我按「能跑起来、能改得动、能避开坑」的顺序把这份资源拆开讲。2. 环境搭建与数据库设计让项目在你机器上先跑通拿到一个 JSP 项目最怕的就是「代码看着都对一跑全是 500」。这一章先把运行环境和表结构钉死后面调试才有参照系。2.1 JDK、Tomcat、MySQL 的版本对齐JSP 项目对版本比想象中敏感。JDK 用 8 或 11 都行但别上 17——老版本 Tomcat 对高版本 JDK 的模块化限制处理得不好容易在启动时抛ClassNotFoundException。Tomcat 选 8.5 或 9.0这两个版本对 JSP 2.3 和 Servlet 3.1 支持完整。MySQL 用 5.7 或 8.0注意 8.0 的驱动类名和连接串参数跟 5.x 不一样这是新手翻车最多的地方。我一般会先确认三件事java -version输出是不是 1.8 或 11Tomcat 的webapps目录能不能正常写MySQL 服务是不是开机自启。这三步过了再谈导入项目。# 检查 JDK 版本确认是 1.8 或 11 java -version # 检查 MySQL 是否在运行Linux 下 systemctl status mysql # 查看 Tomcat 端口占用默认 8080 netstat -tlnp | grep 8080这几条命令的作用分别是确认 JDK 大版本、确认数据库服务状态、确认 Tomcat 端口没被别的进程抢占。端口冲突是启动失败的常见原因改server.xml里的Connector port即可但改完记得同步改项目里所有硬编码的访问地址。2.2 数据库表结构与字段含义这套系统的核心表通常围绕「用户—会议室—预约记录」三张主表展开。下面是我根据这类项目的通用设计整理的表结构字段名和类型可以直接照着建。表名字段类型说明useridint(11)主键自增userusernamevarchar(50)登录名唯一索引userpasswordvarchar(64)建议存 MD5 或 SHA-256userroletinyint(1)0 普通用户1 管理员meeting_roomidint(11)主键自增meeting_roomroom_namevarchar(50)如「三楼小会议室」meeting_roomcapacityint(11)容纳人数meeting_roomstatustinyint(1)0 可用1 维护中reservationidint(11)主键自增reservationuser_idint(11)外键关联 user.idreservationroom_idint(11)外键关联 meeting_room.idreservationstart_timedatetime预约开始时间reservationend_timedatetime预约结束时间reservationstatustinyint(1)0 待审核1 已通过2 已取消建表时有两个细节值得注意。第一reservation表上要建(room_id, start_time, end_time)的联合索引否则查冲突时全表扫描数据一多就慢。第二时间字段用datetime而不是varchar虽然存字符串也能跑但后面做时间区间比较时字符串比较会出玄学问题——比如9:00和10:00按字典序比是对的但9:00和09:00就乱了。-- 创建数据库字符集用 utf8mb4 避免中文乱码 CREATE DATABASE meeting_booking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 预约表加联合索引加速冲突检测查询 ALTER TABLE reservation ADD INDEX idx_room_time (room_id, start_time, end_time);第一条语句指定字符集是为了让会议室名称、用户名里的中文正常存取。第二条语句加的联合索引是给后面「查某房间某时段是否已被占用」的查询用的。没有这个索引每次预约都要扫全表几十条数据感觉不出来上千条就是秒级和毫秒级的差别。2.3 导入项目与连接池配置项目导入 IDE 后第一件事是改数据库连接配置。这类 JSP 项目通常把连接信息放在db.properties或c3p0-config.xml里。找到jdbcUrl、user、password三项改成你本地的。# db.properties 示例 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/meeting_booking?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password你的密码com.mysql.cj.jdbc.Driver是 MySQL 8.0 的驱动类如果你用的是 5.7要改成com.mysql.jdbc.Driver。serverTimezone参数在 8.0 里必须加否则连接时会报时区错误。useSSLfalse是关掉 SSL 警告本地开发用不上加密连接。这几行配置改错一个字符启动就是Communications link failure所以改完先单独写个 main 方法测一下连接别急着启动 Tomcat。3. 预约冲突检测整个系统最该较真的那段逻辑会议室预约系统的核心不是增删改查是「同一房间同一时段不能有两条有效预约」。这段逻辑写不对系统就是个摆设。3.1 冲突判断的 SQL 写法判断冲突的本质是区间重叠检测。两条预约[start1, end1)和[start2, end2)重叠的条件是start1 end2 AND start2 end1。注意这里用的是半开区间结束时间等于开始时间不算冲突这样 9:00-10:00 和 10:00-11:00 可以无缝衔接。-- 查询指定房间在指定时段内是否已有有效预约 SELECT COUNT(*) FROM reservation WHERE room_id ? AND status IN (0, 1) AND start_time ? AND end_time ?;参数依次是房间 ID、新预约的结束时间、新预约的开始时间。status IN (0, 1)表示只统计待审核和已通过的记录已取消的不算。这条 SQL 返回大于 0就说明有冲突直接拒绝新预约。我见过有人用BETWEEN来写结果边界情况全乱套——BETWEEN是闭区间会把 10:00 结束和 10:00 开始判成冲突用户会觉得莫名其妙。3.2 并发下的重复预约问题上面那条 SQL 在单线程下没问题但两个人同时点「提交」时可能都查到 0 条冲突然后都插入成功。这就是典型的竞态条件。解决办法有两种一是给reservation表加唯一约束但区间重叠没法用唯一索引直接表达二是在插入前用SELECT ... FOR UPDATE锁住相关行。// 在 Service 层用事务包裹「查冲突 插入」 Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 先锁住该房间的预约记录 String lockSql SELECT id FROM reservation WHERE room_id ? FOR UPDATE; PreparedStatement lockStmt conn.prepareStatement(lockSql); lockStmt.setInt(1, roomId); lockStmt.executeQuery(); // 再查冲突 // ... 冲突检测逻辑 // 无冲突则插入 conn.commit(); } catch (SQLException e) { conn.rollback(); } finally { conn.setAutoCommit(true); conn.close(); }FOR UPDATE会对查询到的行加排他锁第二个事务必须等第一个提交后才能继续。这样就把并发提交串行化了。代价是性能下降但会议室预约这种低频场景完全够用。注意setAutoCommit(false)和commit()必须成对出现漏了rollback会导致连接一直挂着锁后面所有请求都卡死——这个坑我踩过排查了半天才发现是异常分支没回滚。3.3 时间格式与前端传参前端传过来的时间通常是字符串后端要转成java.sql.Timestamp再塞进 SQL。格式不统一是另一个高频翻车点。// 接收前端参数并转换 String startStr request.getParameter(startTime); // 格式 2025-03-20T09:00 String endStr request.getParameter(endTime); DateTimeFormatter formatter DateTimeFormatter.ofPattern(yyyy-MM-ddTHH:mm); LocalDateTime start LocalDateTime.parse(startStr, formatter); LocalDateTime end LocalDateTime.parse(endStr, formatter); // 基本校验结束必须晚于开始 if (!end.isAfter(start)) { response.getWriter().write({\code\:400,\msg\:\结束时间必须晚于开始时间\}); return; } Timestamp startTs Timestamp.valueOf(start); Timestamp endTs Timestamp.valueOf(end);DateTimeFormatter的模式串必须和前端input typedatetime-local的输出格式一致那个控件默认就是yyyy-MM-ddTHH:mm中间有个T。如果前端用的是别的日期库格式可能带秒或带时区这时候要么统一前端格式要么后端做兼容解析。我一般会在后端加一层「格式尝试」逻辑先按标准格式解析失败再试带秒的格式避免因为一个格式问题让整个预约提交失败。4. 权限控制与页面跳转别让普通用户看到管理后台JSP 项目最容易忽略的就是权限校验。很多人只在登录时判断了角色登录后所有页面都能直接输 URL 访问普通用户改个地址就进了管理后台。4.1 基于 Session 的登录拦截标准做法是写一个Filter拦截所有需要登录的路径检查 Session 里有没有用户信息。// LoginFilter.java 核心逻辑 public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); String uri request.getRequestURI(); // 放行登录页、静态资源 if (uri.contains(login.jsp) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); }getSession(false)表示不自动创建新 Session避免给未登录用户也发一个空 Session。放行列表里一定要包含登录页和静态资源否则登录页自己都被拦截就死循环了。这个 Filter 在web.xml里配置url-pattern/*/url-pattern或者用WebFilter注解。4.2 管理员权限的二次校验登录拦截只保证「登录了」不保证「有权限」。管理后台的 Servlet 里还要再查一次角色。// AdminServlet 里的权限检查 HttpSession session request.getSession(false); User user (User) session.getAttribute(user); if (user null || user.getRole() ! 1) { response.sendError(403, 无权访问); return; }这里用sendError(403)而不是跳转是为了让前端能明确区分「没登录」和「没权限」。有些项目偷懒只在前端隐藏菜单后端不校验结果普通用户用 Postman 直接调接口就能删数据。权限校验必须落在服务端前端隐藏只是体验优化不是安全措施。4.3 JSP 页面里的角色渲染JSP 里可以用 JSTL 标签根据角色决定显示什么。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:if test${sessionScope.user.role 1} a hrefadmin/roomList.jsp会议室管理/a a hrefadmin/auditList.jsp预约审核/a /c:ifsessionScope.user对应 Session 里存的 User 对象.role调的是 getter 方法。JSTL 的c:if在服务端执行不会把管理菜单的 HTML 发给普通用户。注意taglib指令要放在页面顶部且项目WEB-INF/lib下要有jstl.jar和standard.jar缺了会报NoClassDefFoundError。5. 避坑与排查那些让项目「跑不起来」的细节这一章记录几个我实际调试时反复遇到的问题每个都按「现象 → 原因 → 解决」写清楚。5.1 中文乱码从请求到响应全链路现象会议室名称在列表页显示成??????或者提交表单后数据库里存的是乱码。原因乱码可能出现在三个环节——请求编码、响应编码、数据库连接编码。JSP 页面默认用 ISO-8859-1 解码请求参数MySQL 表如果没指定 utf8mb4 也会存乱码。解决三处一起改。请求端在 Filter 里加request.setCharacterEncoding(UTF-8)响应端在 JSP 顶部加% page contentTypetext/html;charsetUTF-8 %数据库连接串加characterEncodingutf8建库时用utf8mb4。三处缺一处都可能乱码别只改一处就以为好了。5.2 数据库连接池耗尽现象系统跑一段时间后所有请求都卡住日志里报Could not get a connection。原因Connection 用完没关闭或者异常分支里漏了close()。连接池默认最大连接数通常只有 10 到 20泄漏几次就耗尽了。解决所有Connection、PreparedStatement、ResultSet都放在try-with-resources里让 JVM 自动关闭。如果用的是手动关闭确保finally块里也写了close()。另外连接池配置里把maxPoolSize调到 50 左右给突发流量留余量。5.3 时间区间边界判断错误现象9:00-10:00 和 10:00-11:00 被判定为冲突用户无法连续预约。原因用了BETWEEN或、做区间比较把边界点算成了重叠。解决统一用半开区间逻辑SQL 里写start_time 新结束 AND end_time 新开始Java 里用isBefore和isAfter而不是compareTo的等号判断。这个规则要在代码注释里写清楚不然下次改代码的人又会改回去。5.4 Tomcat 启动报 404 但项目没报错现象Tomcat 启动日志正常访问http://localhost:8080/项目名/却是 404。原因项目没有部署到webapps目录或者web.xml里welcome-file配置的首页文件不存在。解决检查 Tomcat 的webapps下有没有你的项目文件夹检查web.xml里welcome-file-list写的文件名通常是index.jsp或login.jsp在项目根目录下确实存在。如果是 IDE 里配置的 Tomcat还要看 Deployment 里有没有把 Artifact 加进去。6. 进阶技巧把预约冲突检测做成可复用的工具方法前面几章的冲突检测逻辑散在 Service 里如果以后要加「批量预约」或「周期性预约」代码会重复。我一般会把它抽成一个静态工具方法参数是房间 ID、开始时间、结束时间返回布尔值。这样单元测试也好写。public class ReservationValidator { /** * 检测指定房间在指定时段是否可预约 * param roomId 房间 ID * param start 开始时间 * param end 结束时间 * param excludeReservationId 排除的预约 ID修改预约时排除自身 * return true 表示可预约 */ public static boolean isAvailable(int roomId, Timestamp start, Timestamp end, Integer excludeReservationId) throws SQLException { String sql SELECT COUNT(*) FROM reservation WHERE room_id ? AND status IN (0,1) AND start_time ? AND end_time ?; if (excludeReservationId ! null) { sql AND id ! ?; } try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, roomId); ps.setTimestamp(2, end); ps.setTimestamp(3, start); if (excludeReservationId ! null) { ps.setInt(4, excludeReservationId); } try (ResultSet rs ps.executeQuery()) { rs.next(); return rs.getInt(1) 0; } } } }这个方法的关键设计点有三个。第一excludeReservationId参数让「修改预约」场景可以排除自身否则改时间时会把原记录也算成冲突。第二SQL 拼接只在excludeReservationId非空时加条件避免多写一个无用的AND id ! -1。第三用try-with-resources确保连接和语句自动关闭不会泄漏。验证方法很简单写一个 JUnit 测试先插入一条 9:00-10:00 的预约然后分别测 8:00-9:00应可用、9:30-10:30应冲突、10:00-11:00应可用。三个用例过了基本逻辑就稳了。我还会额外测一个「结束时间早于开始时间」的非法输入确保方法返回 false 而不是抛异常。从那以后我每次改预约相关代码都强制走一遍这三个边界用例再跑一次并发提交的模拟测试。会议室预约这种系统功能不复杂但时间边界和并发这两处一旦出错用户信任就没了。希望帮到你。本文还有配套的精品资源点击获取