资讯动态

JSP会议室预约系统:Servlet+JDBC三层架构与时间冲突检测

发布时间:2026/10/29 20:10:35 来源:尧图企业网站定制
简介面向企业或组织内部会议室管理场景的Java Web项目源码包适合Java Web课程设计、毕业设计及JSP/Servlet入门者参考。系统围绕管理员与员工两类角色展开管理员可维护部门信息、员工账户与权限、会议室名称容量及设备配置并发布系统公告员工能查看公告按日期与时间段选择会议室提交预约也可取消预订以释放资源覆盖资源冲突规避等典型业务。压缩包共744个文件约5.01MB以gif、js、html等前端与素材文件为主并含20个jsp页面、4个java源文件、15个jar依赖、1个sql脚本及xml配置等其中SQL脚本可用于存储部门、员工、会议室和公告等数据并据此梳理JSP、Servlet、JavaBean与数据库增删改查的实现脉络。资源已有1659人学习适合对照角色权限、预约流程、session安全控制和Tomcat部署结构理解完整JSP会议室预约系统的模块划分、页面组织与后台处理逻辑。1. 会议扎堆的早上这套 JSP 预约系统填的是哪个具体的坑早上九点半行政群里第 N 次刷出同一条消息三楼那间能坐八个人的会议室到底谁在用。有人拿着笔记本站在门口里面的人在开会外面的人在等最后靠一句“我十分钟就好”草草收场。这种场景在小团队里几乎每天上演靠吼、靠群公告、靠一张贴在门上的 Excel 打印纸本质上都没有解决同一个问题——同一间房、同一段时间只能被一个人占住。这套 JAVA JSP 会议室预约系统干的就是这件事把会议室台账、预约申请、冲突校验、审批流转搬到一个 Web 界面上谁在什么时候用哪间房一条记录说话。它适合三类人做课程设计或毕设需要一套完整可跑的学生项目、小团队想搭个内部工具又不想上云服务、以及想把 Servlet JSP 这套经典三层亲手拆一遍的开发者。下面按能跑起来、能看懂、能改造的顺序拆。2. 技术栈与工程骨架Servlet JSP JDBC 的三层怎么切这套系统的技术选型没什么花活就是 Java Web 的经典老组合。有人会觉得 JSP 已经过时但对一个会议室预约这种 CRUD 密集、页面不多、部署环境受限的内部系统来说这套组合反而省事不用装 Node、不用配前端构建、一个 WAR 包丢进 Tomcat 就能跑运维同事看得懂出了问题日志也好找。2.1 为什么是这套“老派”组合分层上是标准的三层JSP 负责视图渲染Servlet 当控制器接请求、做参数校验和跳转DAO 用原生 JDBC 拼 SQL 访问 MySQL。中间没有 Spring 的依赖注入也没有 MyBatis 的映射文件所有的Connection、PreparedStatement、ResultSet都手写。这种写法在工程上确实啰嗦但好处是链路上没有任何黑匣子——一个请求从进来到返回每一步都能在代码里指出来这对学习和二次开发反而是优势。实际工程里我一般会在 JDBC 之上加两个东西来兜住复杂度一个是连接池用 Druid 或者 Tomcat 自带的 JNDI 数据源避免每次请求都DriverManager.getConnection造成连接风暴另一个是BaseDao抽象类把关闭资源、执行更新、查询单条、查询列表这几个高频动作收进去子类只写 SQL 和参数绑定。少了这两样代码量会翻倍而且finally块里关连接漏一个就是慢性连接泄漏。会话管理走HttpSession登录成功后把userId、role、realName塞进 session后面每个受保护的 Servlet 和 JSP 都从 session 里取当前用户。权限控制用一个Filter拦/admin/*和/reservation/*前缀未登录直接重定向到登录页角色不够返回 403 页面。这套做法比在每个 Servlet 里写if (session.getAttribute(user) null)要干净得多也不容易漏。前端不用框架JSP 里嵌 JSTL 的c:forEach渲染列表样式手写一份 CSS日历视图用一个简单的表格按小时切片画出来。表单提交走同步 POST成功后sendRedirect回列表页。这套交互在今天看略显笨重但它对浏览器零要求内网老机器也能开。2.2 目录结构与分层职责工程是标准 Maven Web 项目结构打成 WAR 部署。目录大致长这样meeting-room/ ├── pom.xml └── src/main/ ├── java/com/example/meeting/ │ ├── controller/ # Servlet 层接请求、校验、跳转 │ │ ├── LoginServlet.java │ │ ├── RoomServlet.java │ │ ├── ReservationServlet.java │ │ └── ApprovalServlet.java │ ├── service/ # 业务层冲突检测、状态流转放这里 │ │ └── ReservationService.java │ ├── dao/ # 数据访问层 │ │ ├── BaseDao.java │ │ ├── RoomDao.java │ │ └── ReservationDao.java │ ├── entity/ # 与表一一对应的 POJO │ │ ├── User.java │ │ ├── MeetingRoom.java │ │ └── Reservation.java │ ├── filter/ # 登录与编码过滤器 │ │ ├── AuthFilter.java │ │ └── EncodingFilter.java │ └── util/ # 连接池、日期工具 │ ├── JdbcUtils.java │ └── DateUtils.java └── webapp/ ├── WEB-INF/ │ ├── web.xml │ └── views/ # 放在 WEB-INF 下防止被直接访问 │ ├── login.jsp │ ├── room_list.jsp │ ├── reservation_form.jsp │ └── approval_list.jsp ├── css/ js/ └── index.jsp有两个地方值得强调。views目录放在WEB-INF下面浏览器直接敲 URL 是访问不到的只能通过 Servlet 的RequestDispatcher.forward进去这样页面就绕不开权限检查属于低成本但很有效的一层防护。另外service层不要省很多人图快直接在 Servlet 里调 DAO冲突检测和状态校验全堆在控制器里等到要加“会议室停用后自动取消未来预约”这种规则时就会发现改一处崩三处。2.3 表结构与核心字段数据库就三张主表加一张用户表字段设计直接决定了后面冲突检测 SQL 好不好写。表名作用关键字段sys_user用户与角色id、username、password、real_name、role0 普通 / 1 管理员、statusmeeting_room会议室台账id、room_name、location、capacity、device、status1 可用 / 0 停用reservation预约单id、room_id、user_id、subject、start_time、end_time、status、create_timeapproval_log审批留痕id、reservation_id、operator_id、action、remark、create_timereservation.status是整个系统的状态机核心取值我习惯定成 0 待审批、1 已通过、2 已驳回、3 已取消。注意reservation表上要建(room_id, start_time, end_time)的联合索引否则冲突检测那条 SQL 在数据量涨到几千条后就会开始变慢——这是最容易忽略、也最容易在演示时翻车的地方后面避坑章节还会提到。密码别存明文sys_user.password里存MD5加盐或者BCrypt的结果都行登录时比对哈希。别看是内部系统真部署到内网里被人拿弱口令扫一遍的事故不在少数。3. 本地跑通从建库建表到 Tomcat 部署的一整套动作这一章的目标很明确从一台干净机器开始把项目跑到能登录、能预约、能审批。下面的版本组合是这类老项目最常见的搭法实际导入时如果 IDE 有版本提示按提示降级 JDK 就行。3.1 环境准备与版本搭配需要的四样东西JDK 8、Maven 3.6 以上、Tomcat 8.5 或 9、MySQL 5.7 或 8。JDK 别上 17JSP 里如果用了老版本的 JSTL 会出javax与jakarta包名不兼容的问题报错信息还特别难懂。Tomcat 10 之后 Servlet API 全部迁到jakarta.servlet老代码的javax.servlet.*导入会整片飘红所以要么用 9 要么把导入全改一遍这是第一个要提前决定的事。确认环境java -version # 期望 1.8.x mvn -v # 期望 3.6 mysql --version # 期望 5.7 或 8.xpom.xml里的依赖一共四五个重点是javax.servlet-api用provided作用域因为 Tomcat 自带打进去反而会冲突MySQL 驱动注意 8.x 版本类名是com.mysql.cj.jdbc.Driver且 URL 要带时区参数5.x 是com.mysql.jdbc.Driver混用会直接连不上。dependencies !-- 由 Tomcat 提供绝不能打进 WAR -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTLJSP 页面里的 c:forEach 靠它 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 8 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency !-- 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.16/version /dependency /dependencies参数说明上scopeprovided/scope的含义是编译期可见、打包不包含这是 Servlet API 的唯一正确写法jstl版本必须是 1.21.1 的 URI 是http://java.sun.com/jstl/core1.2 才改成http://java.sun.com/jsp/jstl/core标签不生效十有八九是这里写错。3.2 建库建表与初始化数据先建库字符集统一utf8mb4别用utf8——会议室名称里带个生僻字或者 emoji 就会写入失败。CREATE DATABASE meeting_room DEFAULT CHARACTER SET utf8mb4; USE meeting_room; CREATE TABLE meeting_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(50) NOT NULL COMMENT 会议室名称, location VARCHAR(100) NOT NULL COMMENT 所在楼层/位置, capacity INT NOT NULL DEFAULT 1 COMMENT 容纳人数, device VARCHAR(200) DEFAULT COMMENT 投影/白板等设备, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 可用 0 停用 ); CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL, user_id INT NOT NULL, subject VARCHAR(100) NOT NULL COMMENT 会议主题, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1通过 2驳回 3取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 冲突检测和日历查询都靠这个索引 KEY idx_room_time (room_id, start_time, end_time), KEY idx_user (user_id) );初始化数据至少塞四条一个管理员账号、两个普通账号、三间会议室。账号密码存哈希值别图省事存明文再在代码里做对比迁移到生产时改起来很麻烦。插入历史预约数据时记得让时间落在未来否则 macOS 或者 Windows 上的时间格式处理差异会让你排查半天。3.3 数据源配置与部署连接池配置我一般放在src/main/resources/druid.properties用JdbcUtils静态块加载全应用共享一个DataSourcepublic class JdbcUtils { private static final DataSource DS; static { try { Properties p new Properties(); // 从 classpath 读连接池配置 p.load(JdbcUtils.class.getClassLoader() .getResourceAsStream(druid.properties)); DS DruidDataSourceFactory.createDataSource(p); } catch (Exception e) { // 静态块里抛异常会导致类加载失败必须显式终止 throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DS.getConnection(); } }这段代码的关键点在于异常处理静态初始化块里如果吞掉异常后续每次getConnection都会返回 null报错会延后到业务代码里堆栈完全指不到根因。用ExceptionInInitializerError包一层Tomcat 启动日志里会立刻出现ExceptionInInitializerError加上原始 cause属于花十秒就能定位问题的写法。对应的druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/meeting_room?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的密码 initialSize2 maxActive10 maxWait3000serverTimezone必须显式指定MySQL 8 驱动不写这个参数会抛The server time zone value is unrecognizedmaxActive给 10 足够小团队用给太大反而容易在数据库端打满连接数。打包部署三步mvn clean package -DskipTests # 产出 target/meeting-room.war cp target/meeting-room.war $CATALINA_HOME/webapps/ $CATALINA_HOME/bin/startup.sh # Windows 用 startup.bat-DskipTests是因为这类项目自带的测试类往往依赖测试库先跳过保证能出包。WAR 包名就是访问路径丢进去后等 Tomcat 解压完成浏览器打开http://localhost:8080/meeting-room/就进登录页了。3.4 冒烟验证清单部署完别急着交付按这份清单走一遍任何一步不通过都别往下继续登录页能打开且样式正常 —— 样式丢了说明css/路径用了相对路径在转发场景下会解析错。用管理员账号登录成功右上角显示真实姓名 —— 说明 session 写入正常。会议室列表有数据 —— 说明连接池和 DAO 查询通了。提交一条明天上午 10:00 到 11:00 的预约列表里出现“待审批”。用同一间房、同一时段再提交一次应该被拦下并提示时间冲突。管理员进入审批页通过第一条申请状态变为“已通过”。退出后不登录直接访问/admin/approval_list应跳回登录页而不是报 500。第 7 条经常被跳过但它是权限过滤器到底有没有生效的唯一证据。我见过不止一次过滤器在web.xml里过滤路径写成了/admin而不是/admin/*结果所有管理页面裸奔。4. 预约主链路拆解一次提交从表单校验到落库的完整路径这一条链路是整个系统的主心骨也是最值得逐段读的部分。一次预约提交要依次过前端防呆、Servlet 参数校验、业务层时间冲突检测、DAO 落库、状态机初始化。任何一环漏掉都会在生产里变成“两拨人在同一间会议室门口对峙”的真实事故。4.1 表单与前端防呆reservation_form.jsp里有两个datetime-local输入框加一段提交前的 JS 校验。前端校验的意义不是安全是减少无效请求和提升体验后端必须再校验一遍。document.getElementById(reserveForm).addEventListener(submit, function (e) { const start new Date(document.getElementById(startTime).value); const end new Date(document.getElementById(endTime).value); const now new Date(); if (isNaN(start) || isNaN(end)) { alert(请选择完整的开始和结束时间); e.preventDefault(); return; } // 不允许预约过去的时间 if (start now) { alert(开始时间不能早于当前时间); e.preventDefault(); return; } // 结束必须晚于开始且不短于 15 分钟 if (end - start 15 * 60 * 1000) { alert(会议时长至少 15 分钟); e.preventDefault(); return; } });datetime-local提交上来的字符串格式是2024-06-01T10:00中间是字母T后端用SimpleDateFormat(yyyy-MM-ddTHH:mm)解析。这一点如果没对齐ParseException会在服务器端抛而前端看起来一切正常属于典型的“现象在浏览器、原因在服务器”的排查场景。我的习惯是让前端提交前把T换成空格后端统一按yyyy-MM-dd HH:mm解析省掉两边格式不一致的麻烦。4.2 Servlet 控制层的处理顺序ReservationServlet只处理/reservation/submit这个路径业务判断全部下沉到ReservationService。控制器的职责就四件事取参数、转对象、调服务、决定跳哪。WebServlet(/reservation/submit) public class ReservationServlet extends HttpServlet { private final ReservationService service new ReservationService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session req.getSession(false); // 过滤器已拦过一次这里再兜一层防止直接转发进来 if (session null || session.getAttribute(userId) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } try { int userId (Integer) session.getAttribute(userId); int roomId Integer.parseInt(req.getParameter(roomId)); String subject req.getParameter(subject).trim(); Date start DateUtils.parse(req.getParameter(startTime)); Date end DateUtils.parse(req.getParameter(endTime)); if (subject.isEmpty()) { throw new BizException(会议主题不能为空); } // 冲突检测 落库都在 service 里控制器不碰 SQL service.createReservation(userId, roomId, subject, start, end); resp.sendRedirect(req.getContextPath() /reservation/list?ok1); } catch (BizException e) { // 业务异常回填到页面保留用户已填内容 req.setAttribute(error, e.getMessage()); req.getRequestDispatcher(/WEB-INF/views/reservation_form.jsp) .forward(req, resp); } catch (Exception e) { log(预约提交失败, e); req.setAttribute(error, 系统繁忙请稍后重试); req.getRequestDispatcher(/WEB-INF/views/reservation_form.jsp) .forward(req, resp); } } }几个细节值得说。getSession(false)不新建会话避免匿名请求凭空创建 session 占内存业务异常用自定义的BizException包裹错误文案页面直接显示给用户非业务异常统一转成“系统繁忙”把堆栈打到日志里而不是浏览器上——这是防止 SQL 报错信息泄露表结构的底线。另外成功之后用sendRedirect而不是forward是为了避免刷新页面重复提交这个坑后面还会碰到。4.3 DAO 层的时间区间冲突检测这是整个系统最核心的一段 SQL。判断两个时间段是否重叠标准写法是SELECT COUNT(1) FROM reservation WHERE room_id ? AND status IN (0, 1) -- 待审批和已通过的都算占用 AND start_time ? -- 新预约的 end_time AND end_time ? -- 新预约的 start_time AND id ? -- 编辑场景排除自身为什么是和而不是、因为区间按左闭右开处理10:00-11:00 和 11:00-12:00 是两场会议首尾相接不算冲突。如果写成所有相邻时段都会被误判成冲突用户会觉得这系统“什么都约不上”这种玄学体感问题追到最后往往就是两个不等号的差别。封装到 DAO 里public boolean hasConflict(Connection conn, int roomId, Date start, Date end, Integer excludeId) throws SQLException { String sql SELECT COUNT(1) FROM reservation WHERE room_id ? AND status IN (0,1) AND start_time ? AND end_time ? (excludeId null ? : AND id ?); try (PreparedStatement ps conn.prepareStatement(sql)) { int i 1; ps.setInt(i, roomId); ps.setTimestamp(i, new Timestamp(end.getTime())); // 注意顺序先 end ps.setTimestamp(i, new Timestamp(start.getTime())); // 再 start if (excludeId ! null) ps.setInt(i, excludeId); try (ResultSet rs ps.executeQuery()) { return rs.next() rs.getInt(1) 0; } } }参数绑定顺序是最容易写反的地方SQL 里第一个?对应新预约的结束时间第二个对应开始时间和人的直觉相反。写完一定要拿“完全重叠”“部分重叠”“首尾相接”“完全错开”四种数据各测一遍别只看一种情况过了就认为对了。还有并发问题。两个用户几乎同时提交同一时段的预约“先查后插”之间存在窗口期可能两条都通过检查后都插进去。小团队数据量下撞上的概率不高但血泪经验是真正撞上一次会议室门口就是四个人。兜底方案有两种一是在reservation上做数据库层的唯一约束但对区间无能为力二是把检测和插入放进同一个事务并给meeting_room那一行加SELECT ... FOR UPDATE行锁代价是并发性能下降但正确。我用的是第二种事务粒度控制在一个会议室上实际影响可以忽略。4.4 状态流转与审批ReservationService里的状态流转要写成显式判断不要用if (status ! 0) return这种糊弄写法因为驳回、取消、重新提交的规则各不相同。public void approve(int reservationId, int operatorId, boolean pass, String remark) throws BizException { try (Connection conn JdbcUtils.getConnection()) { conn.setAutoCommit(false); // 审批要动两张表必须开事务 try { Reservation r dao.findById(conn, reservationId); if (r null) throw new BizException(预约不存在); if (r.getStatus() ! 0) throw new BizException(该申请已被处理); int newStatus pass ? 1 : 2; dao.updateStatus(conn, reservationId, newStatus); logDao.insert(conn, reservationId, operatorId, pass ? PASS : REJECT, remark); conn.commit(); } catch (Exception e) { conn.rollback(); throw e instanceof BizException ? (BizException) e : new BizException(审批失败); } finally { conn.setAutoCommit(true); // 归还连接前恢复否则池里全是脏连接 } } }setAutoCommit(true)放在finally里恢复这一步很多示例代码会漏。连接池复用连接一个没恢复自动提交的连接回到池里下一个拿到它的请求会莫名其妙地每条 SQL 都即时提交事务形同虚设排查起来极其折磨。状态机整体是0 待审批 → 1 通过 / 2 驳回0 或 1 → 3 取消且取消只有本人能操作管理员除外。取消已通过的预约要不要通知参会人取决于你有没有接消息通道没有的话至少在列表页用醒目颜色标出来。5. 避坑与排查部署联调阶段五个真实翻车现场这一类项目代码本身不复杂出问题基本都出在环境和数据上。下面五条是按踩坑频率排的每一条我都实际遇到过至少一次。5.1 启动与部署阶段的两类报错现象一访问任何 JSP 都返回 500日志里是java.lang.ClassNotFoundException: javax.servlet.jsp.jstl.core.Config。原因jstl依赖没打进 WAR或者版本用了 1.1 但页面里的 taglib URI 写的是 1.2 的地址。解决确认pom.xml里有jstl:1.2且作用域是默认的 compile然后mvn dependency:tree看一眼有没有被其他依赖排除掉同时检查每个 JSP 顶部的% taglib urihttp://java.sun.com/jsp/jstl/core prefixc %是否完全一致。现象二Tomcat 启动正常但登录后刷新页面就退出登录session 像凭空丢了。原因多半是web.xml里的session-config超时设得过短或者项目路径变了导致浏览器 Cookie 的 Path 不匹配还有一种更隐蔽的情况是在context.xml里配置了sessionCookiePath却和实际部署路径不一致。解决先把超时改到 30 分钟排除第一种再打开浏览器开发者工具看JSESSIONID这个 Cookie 的 Path 是不是/meeting-room不对就调context.xml。现象三MySQL 8 环境下启动报Public Key Retrieval is not allowed。原因MySQL 8 默认使用caching_sha2_password认证插件JDBC 驱动在不加密连接下需要显式允许取公钥。解决在 URL 后面追加allowPublicKeyRetrievaltrueuseSSLfalse只在本地开发环境这么做正式环境应该配好证书走加密。5.2 预约逻辑与数据层的三类坑现象四明明没有冲突提交时却提示“该时段已被占用”或者反过来真有冲突却能提交成功。原因前者是把区间判断写成了/相邻时段被误判后者是 SQL 里status条件写成了只查status 1漏掉了待审批的 0导致两条待审批可以占同一时段。解决把冲突查询的四种边界数据完全重叠、部分重叠、首尾相接、完全错开各造一条测试数据跑一遍同时在 SQL 里明确写status IN (0,1)把驳回和取消排除在占用之外。这类问题事后看都很简单但当场没有测试数据撑着只能靠猜。现象五列表页翻到第二页数据重复或丢失总数还对不上。原因分页 SQL 用了LIMIT offset, size但ORDER BY的字段不唯一比如按create_time排序同秒创建的多条记录顺序在两次查询间不稳定翻页时就会重复或跳过。解决排序字段后面补一个唯一列比如ORDER BY create_time DESC, id DESC保证排序结果确定。这个坑和数据量无关两条同秒数据就能复现。提示以上五条里有三条在本地开发环境永远不会出现MySQL 8 认证、Cookie Path、并发窗口所以上线前一定要在一台干净的机器上从零部署一遍别拿自己的开发机当验收环境。6. 二次开发把“能预约”改造成“敢上生产”的三个方向这套系统跑通之后多数人的第一反应是加功能但我建议先想清楚往哪个方向加。按投入产出比排优先级大概是下面这样改造方向具体动作难度收益稳定性冲突检测加行锁事务、连接池监控、全局异常页中高直接消灭最痛的事故可用性周视图日历、我的预约一键取消、邮件或站内提醒中高用户感知最明显扩展性DAO 换 MyBatis、接口改 REST、拆出独立前端高中视后续需求而定先说稳定性里最值得做的一件事把第 4 章那段“先查后插”改成带锁的事务。具体做法是在 service 方法开头执行SELECT id FROM meeting_room WHERE id ? FOR UPDATE把这一行锁住后续的冲突检测和插入都在锁的保护下完成提交后释放。这样两个并发请求会被串行化第二个请求进来时能看到第一个已提交的记录冲突检测就准确了。代价是同一间会议室的并发预约会排队但会议室场景本来就不存在高并发这笔交易很划算。再说可用性。日历视图是这类系统最容易被要求的功能实现上不用引第三方组件按天查出一间房当天的所有status IN (0,1)的预约在 JSP 里用一张 24 行 × 房间数列的表格染色即可。核心是把时间区间映射到行号// 把一条预约映射成日历上的起始行和跨行数每格代表 30 分钟 int startSlot (int) ((r.getStartTime().getTime() - dayStart) / (30 * 60 * 1000)); int endSlot (int) Math.ceil((r.getEndTime().getTime() - dayStart) / (30 * 60 * 1000.0)); int rowSpan Math.max(1, endSlot - startSlot);rowSpan用Math.max(1, ...)兜底是为了防止有人提交了一个 10 分钟的预约前端拦了但接口可能被直接调用算出 0 行导致整个表格错位。这类边界兜底不写测试时用正常数据永远发现不了。最后说扩展性。如果你的目标是把它当成一个正经内部系统长期维护我的建议是把 DAO 层换成 MyBatis不是因为 JDBC 不好而是动态条件查询按房间、按人、按状态、按时间段组合筛选用字符串拼 SQL 太容易出错一个漏掉空格的WHERE AND就能让整个列表页 500。换 ORM 之后if testroomId ! null这种写法能省掉大量手工拼接。至于拆前后端除非你打算做移动端否则没必要JSP 的渲染速度在内网环境完全够用。从一个具体教训收尾我早期改这类项目时习惯直接在reservation_form.jsp上加字段改完在浏览器里点两下看着没问题就提交了结果忘了对应的实体类和 DAO 的INSERT语句——页面填了值数据库里那一列永远是 null直到用户投诉“填的会议主题不见了”才发现。从那以后我每次加字段都强制走一遍“建表语句 → 实体类 → DAO → Servlet 取参 → JSP 渲染”这五步核对一步不落。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑