简介:这是一套面向Java Web初学者与课程设计者的会议室预约系统完整源码,基于JSP+Servlet+JavaBean技术栈实现,可帮助读者理解企业级资源管理系统的开发流程。系统区分管理员与员工两类角色:管理员负责部门、员工账户、会议室信息及公告的增删改查与权限分配,员工则可查看公告、按日期时间预订或取消会议室,覆盖了权限控制、数据库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 或 11;Tomcat 的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 数据库表结构与字段含义
这套系统的核心表通常围绕「用户—会议室—预约记录」三张主表展开。下面是我根据这类项目的通用设计整理的表结构,字段名和类型可以直接照着建。
| 表名 | 字段 | 类型 | 说明 |
|---|---|---|---|
| user | id | int(11) | 主键,自增 |
| user | username | varchar(50) | 登录名,唯一索引 |
| user | password | varchar(64) | 建议存 MD5 或 SHA-256 |
| user | role | tinyint(1) | 0 普通用户,1 管理员 |
| meeting_room | id | int(11) | 主键,自增 |
| meeting_room | room_name | varchar(50) | 如「三楼小会议室」 |
| meeting_room | capacity | int(11) | 容纳人数 |
| meeting_room | status | tinyint(1) | 0 可用,1 维护中 |
| reservation | id | int(11) | 主键,自增 |
| reservation | user_id | int(11) | 外键,关联 user.id |
| reservation | room_id | int(11) | 外键,关联 meeting_room.id |
| reservation | start_time | datetime | 预约开始时间 |
| reservation | end_time | datetime | 预约结束时间 |
| reservation | status | tinyint(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.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/meeting_booking?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 jdbc.username=root jdbc.password=你的密码com.mysql.cj.jdbc.Driver是 MySQL 8.0 的驱动类,如果你用的是 5.7,要改成com.mysql.jdbc.Driver。serverTimezone参数在 8.0 里必须加,否则连接时会报时区错误。useSSL=false是关掉 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-dd'T'HH: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 type="datetime-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 prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:if test="${sessionScope.user.role == 1}"> <a href="admin/roomList.jsp">会议室管理</a> <a href="admin/auditList.jsp">预约审核</a> </c:if>sessionScope.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 contentType="text/html;charset=UTF-8" %>;数据库连接串加characterEncoding=utf8,建库时用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 而不是抛异常。
从那以后我每次改预约相关代码,都强制走一遍这三个边界用例,再跑一次并发提交的模拟测试。会议室预约这种系统,功能不复杂,但时间边界和并发这两处一旦出错,用户信任就没了。希望帮到你。
本文还有配套的精品资源,点击获取