JSP+SQL Server实现开放性实验管理系统:从表设计到并发预约 📅 发布时间:2026/9/16 0:50:29 👁 浏览次数: 简介面向高校实验教学管理场景的毕业设计资源基于JSPSQL Server实现开放实验室管理系统采用B/S模式涵盖实验室信息管理、实验信息管理、网上预约实验等核心功能适合作为计算机相关专业毕设、课程设计或工程实训参考。整套资料共108个文件压缩包约2.72MB主体为59个JSP页面及配套Java类、CSS/JS前端文件同时包含SQL Server数据库文件MDF/LDF、毕业设计论文、开题报告、中英文文献和答辩PPT从代码到文档形成完整闭环。已有63人学习下载。资源不仅提供可运行的系统代码还给出论文写作框架、文献翻译与答辩展示材料能帮助读者快速理解系统设计思路、数据库表结构及预约流程并辅助完成毕业设计各阶段文档准备。代码需在理解基础上自行调试适合有一定JSP基础的学习者。1. 用JSPSQL Server做开放性实验管理系统到底在做什么你在答辩PPT里可以写“系统采用JSPServletSQL Server三层架构”但这句免费的话不会让你多拿一分。指导老师真正会追问的是这个系统开放在哪里。开放性实验管理系统的核心不是维护实验名称和成绩而是处理一组有容量上限、有时间窗口的资源允许学生自由挑选、教师在线审批、管理员做统计整个过程里JSP负责把页面吐回浏览器Servlet处理请求流转SQL Server把每次预约和审批都落到表里。本文按最常见的毕业设计实现顺序写先谈需求怎么拆成表结构再写JDBC连接SQL Server 2019的坑然后处理预约时的并发条件和状态流转最后聊Tomcat部署和答辩时容易被追问的几个点。学生照着走能跑通全流程已经工作的人可以跳过建库直接看后面的并发SQL、JSP缓存和防越权。2. 开放性业务怎么拆从需求到表结构2.1 开放性体现在三个地方项目库、时间窗口、审批流传统的实验课是排课式的班级、时间、内容全部固定系统只需要一张成绩表。开放性实验管理系统的差异在于规则变了实验项目库由管理员动态维护教师可以发布新的项目同一项目在一段时间内重复开放学生选某个时段报名名额有限先到先得教师对预约结果有审批权审批不通过要能释放名额成绩在实验完成后由教师录入。这个场景下页面上的增删改查只是表象真正要设计好的是一个“状态驱动的预约流程”。我在做同类系统时一般会把业务实体收敛为四个维度人、实验项目、预约单、公告或附件。人包括学生、教师、管理员用一条用户表加角色字段搞定不做三张表分表实验项目保存开放时间和容量预约单是核心交易表状态字段驱动审批和归档公告表用于发布实验通知至于统计报表直接按这三张表聚合就能算出来不需要额外建宽表。2.2 为什么还在用JSPServlet而不是Spring Boot这是毕设答辩第一问。技术选型要站得住脚不能只说“学校教的”。JSPServlet是Java Web的基础能清楚展示请求从浏览器到Tomcat再到数据库的完整路径Spring Boot把这些过程自动化了反而很难在论文里展开讲原理。SQL Server在市场占有率上不如MySQL但事务处理、存储过程、图形化管理工具SSMS在工程教育里有优势而且在Windows Server实训环境里部署维护成本低。从复现角度看这个组合对机器要求也不高。一台4G内存的Windows机器就能跑SQL Server 2019 Express和Tomcat 9学生自己电脑和学校机房都能满足。我做选型对比时常用下面这个表格对比项JSP Servlet SQL ServerSpring Boot MySQL逻辑层划分手工控制类路径看得见自动装配黑盒部署依赖Tomcat JDBC驱动Maven 内嵌Tomcat数据库事务教学手动commit/rollback直观注解声明式事务答辩被追问风险低原理全部可答高容易答不上自动配置同样功能代码量多约30%但论文好写少但细节容易被忽略2.3 用SQL Server建出四张核心表用户、项目、预约、公告很多人一上来就建十几张表结果数据都是空的。开放性实验管理系统的表不在多而在约束到位。我按下面的DDL建库使用INT IDENTITY(1,1)做主键自增时间字段用DATETIME状态用TINYINT节省空间。-- 1. 用户表学生、教师、管理员共用一个表用 role_code 区分 CREATE TABLE t_user ( user_id INT IDENTITY(1,1) PRIMARY KEY, user_no VARCHAR(20) NOT NULL UNIQUE, -- 学号/工号 user_name NVARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL, -- 加盐后的散列值 role_code VARCHAR(10) NOT NULL DEFAULT STU, -- STU/TEA/ADMIN dept_name NVARCHAR(50) NULL ); -- 2. 实验项目表 CREATE TABLE t_experiment ( exp_id INT IDENTITY(1,1) PRIMARY KEY, exp_no VARCHAR(20) NOT NULL UNIQUE, exp_name NVARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 1.0, -- 学分 teacher_id INT NOT NULL REFERENCES t_user(user_id), begin_time DATETIME NOT NULL, -- 开放时段开始 end_time DATETIME NOT NULL, -- 开放时段结束 capacity INT NOT NULL DEFAULT 30, -- 总容量 remain INT NOT NULL DEFAULT 30, -- 剩余名额 status TINYINT NOT NULL DEFAULT 1, -- 1未开始 2进行中 3已结束 summary NVARCHAR(500) NULL ); -- 3. 预约表约束针对业务场景 CREATE TABLE t_reservation ( resv_id INT IDENTITY(1,1) PRIMARY KEY, exp_id INT NOT NULL REFERENCES t_experiment(exp_id), student_id INT NOT NULL REFERENCES t_user(user_id), status TINYINT NOT NULL DEFAULT 0, -- 0待审 1通过 2拒绝 3已归档 apply_time DATETIME NOT NULL DEFAULT GETDATE(), audit_time DATETIME NULL, audit_by INT NULL REFERENCES t_user(user_id), score DECIMAL(5,2) NULL, remark NVARCHAR(200) NULL, CONSTRAINT uq_exp_student UNIQUE (exp_id, student_id) -- 同一项目不能重复申请 ); -- 4. 公告表动态发布通知 CREATE TABLE t_notice ( notice_id INT IDENTITY(1,1) PRIMARY KEY, title NVARCHAR(100) NOT NULL, content NVARCHAR(MAX) NOT NULL, create_by INT NOT NULL REFERENCES t_user(user_id), create_time DATETIME NOT NULL DEFAULT GETDATE() );这里字段设计有几个要点值得在论文里解释remain是预留的冗余字段每次预约先UPDATE t_experiment SET remain remain - 1 WHERE exp_id ? AND remain 0再插入预约记录两步放在同一事务里避免超卖uq_exp_student唯一约束做兜底即使用户在页面连点两次提交数据库层也会拒绝重复预约audit_by记录了是谁审批的属于审计字段答辩时能体现设计完整性。2.4 预约状态机七个状态撑起一个审批闭环预约单不能只有“已通过/未通过”两个状态。实际业务里学生提交、教师通过、实验完成、成绩录入是不同时间点发生的事状态字段需要覆盖全生命周期状态码含义触发动作0待审批学生提交预约扣减remain1已通过教师审批通过2已拒绝教师拒绝恢复remain 13已归档实验结束教师录入成绩4已取消学生在未审批前取消预约5已过期实验结束仍未处理的预约定时任务批量置为过期6异常数据不一致时人工标记状态变更不要散落在JSP页面里全部收敛到一个Servlet的switch中用一个状态流转方法统一处理。public boolean changeStatus(Connection conn, int resvId, int fromStatus, int toStatus) throws SQLException { String sql UPDATE t_reservation SET status ? WHERE resv_id ? AND status ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, toStatus); ps.setInt(2, resvId); ps.setInt(3, fromStatus); return ps.executeUpdate() 1; } }带fromStatus的条件更新是一种乐观锁思想。3. 在SQL Server 2019上建库建表并让JDBC连上3.1 安装和首次连接最容易踩的两个配置SQL Server 2019安装教程一搜一大把大部分人卡在SSMS里能打开、JDBC连不上的环节。第一个坑是身份验证模式。安装时如果选了“Windows身份验证模式”JDBC用用户名密码登录会直接报Login failed for user sa。解决方法是安装完成后打开SSMS用Windows身份登录执行下面的SQL把身份验证改成混合模式EXEC xp_instance_regwrite NHKEY_LOCAL_MACHINE, NSoftware\Microsoft\MSSQLServer\MSSQLServer, NLoginMode, REG_DWORD, 2; -- 1Windows only, 2混合模式运行完成后还要去SQL Server 配置管理器里把SQL Server 网络配置下的TCP/IP协议启用双击进入属性在“IP 地址”标签页把IPALL的TCP 端口设为1433然后重启服务。第二个坑是JDBC连接串里没写加密相关参数。SQL Server 2019默认强制加密连接Java 8以上环境需要额外传encrypttrue;trustServerCertificatetrue否则会报证书相关的SSL错误。3.2 DBUtil类与连接串参数说明我把数据库连接封装在一个静态工具类里项目里所有业务类都调用它拿连接。package com.lab.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { // 注意ServerName写本机或IP不要用localhost以外的别名避免SQL Server在解析实例名时卡住 private static final String URL jdbc:sqlserver://localhost:1433;DatabaseNamelab_open; encrypttrue;trustServerCertificatetrue;loginTimeout5; private static final String USER lab_user; private static final String PASSWORD Lab2024; // 密码单独配置不要硬编码在生产环境 static { try { Class.forName(com.microsoft.sqlserver.jdbc.SQLServerDriver); } catch (ClassNotFoundException e) { throw new RuntimeException(缺少SQL Server JDBC驱动jar, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }参数说明DatabaseName指定默认数据库连上后USE语句都可以省掉loginTimeout5是登录超时单位为秒不设的话默认能卡到30秒以上trustServerCertificatetrue表示信任服务器端SSL证书开发环境使用生产环境应该把证书换成正规CA证书。驱动jar文件mssql-jdbc从9.4版开始支持JDBC 4.2把jar放到WEB-INF/lib目录下Tomcat启动时自动加载不需要额外配classpath。3.3 SQL Server时间函数在报表页里的实际用法报表是评审必看的功能点。SQL Server的时间函数和MySQL语法不完全一样三个要会用GETDATE()取当前时间CONVERT()做格式转换DATEPART()取年月日等部分值。统计每天预约人数的查询SELECT CONVERT(varchar(10), apply_time, 120) AS apply_day, -- 120格式输出yyyy-MM-dd COUNT(*) AS app_count FROM t_reservation GROUP BY CONVERT(varchar(10), apply_time, 120) ORDER BY apply_day DESC;分析哪个时段最受欢迎可以按小时分组SELECT DATEPART(HOUR, apply_time) AS hour_no, COUNT(*) AS cnt FROM t_reservation GROUP BY DATEPART(HOUR, apply_time) ORDER BY hour_no;CONVERT(varchar(10), apply_time, 120)里的120是样式代码不是数字怪癖SQL Server里有二十多种预定义格式120对应可排序的yyyy-MM-dd HH:mm:ss取前10位就是日期这样写比SUBSTRING(CONVERT(varchar, apply_time), 1, 10)更稳定。这两个查询做完之后图表页面直接拿JSON返回给前端就是一张完整的实验热度报表。注意时间字段的索引问题如果预约数据量超过十万行GROUP BY CONVERT(varchar(10), apply_time, 120)会让索引失效生产环境建议再冗余一个apply_date AS DATE的持久化计算列。4. Servlet处理业务JSP渲染视图4.1 登录与角色权限校验JSP页面不能直接放业务逻辑否则数据流会绕到页面里后续改权限和加安全校验非常痛苦。常见做法是页面表单提交到LoginServlet先做登录名和密码校验成功后在session里保存一个loginUser对象和roleCode之后所有请求进入系统前都过一个权限过滤器。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String userNo request.getParameter(userNo); String password request.getParameter(password); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( SELECT user_id, user_name, user_no, role_code, password FROM t_user WHERE user_no ?)) { ps.setString(1, userNo); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) { request.setAttribute(errMsg, 账号不存在); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } String salt rs.getString(salt); // 加盐字段 String pwdHash DigestUtils.sha256Hex(salt password); if (!pwdHash.equals(rs.getString(password))) { request.setAttribute(errMsg, 密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); return; } request.getSession().setAttribute(loginUser, rs.getString(user_name)); request.getSession().setAttribute(roleCode, rs.getString(role_code)); response.sendRedirect(request.getContextPath() /index.jsp); } } catch (SQLException e) { throw new ServletException(数据库异常, e); } } }密码比较用的是加盐后的SHA-256值明文密码不许进数据库。需要注意forward和sendRedirect的区别登录失败用forward因为要保留错误提示登录成功用重定向避免浏览器刷新时重复提交表单。对应的权限过滤器只需要几行拦截除/login和/register以外的所有路径WebFilter(/*) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; String uri request.getRequestURI(); if (uri.contains(/login) || uri.endsWith(.css) || uri.endsWith(.js)) { chain.doFilter(req, res); return; } if (request.getSession().getAttribute(roleCode) null) { ((HttpServletResponse) res).sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, res); } }4.2 预约提交与名额扣减的事务边界预约是并发场景学生可能在同一个瞬间抢夺最后一个名额。如果先查remain再判断再更新会出现两个线程读到相同剩余名额然后同时写库。解决办法是把“扣减落单”放在一个事务里用一条带条件的UPDATE做原子扣减Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 原子扣减影响行数为0说明已无名额 String updateRemainSql UPDATE t_experiment SET remain remain - 1 WHERE exp_id ? AND remain 0; PreparedStatement updatePs conn.prepareStatement(updateRemainSql); updatePs.setInt(1, expId); int updated updatePs.executeUpdate(); if (updated 0) { conn.rollback(); request.setAttribute(errMsg, 该实验名额已满); request.getRequestDispatcher(/reserve.jsp).forward(request, response); return; } // 2. 插入预约单 String insertSql INSERT INTO t_reservation(exp_id, student_id, status, apply_time) VALUES (?, ?, 0, GETDATE()); PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setInt(1, expId); insertPs.setInt(2, studentId); insertPs.executeUpdate(); conn.commit(); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new ServletException(预约失败, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); } catch (SQLException ignored) {} try { conn.close(); } catch (SQLException ignored) {} } }同时在密码上设置增强校验密码最少8位必须包含字母和数字。4.3 用JSTL把JSP页面里的Java代码清掉很多学生写的JSP页面里塞满% %脚本片段代码混在一起越写到后面越乱。推荐用JSTL核心标签库和格式化标签库。在WEB-INF/lib下放jstl-1.2.jar页面顶部加两行taglib指令预约列表页的例子% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table thead trth实验名称/thth申请时间/thth状态/th/tr /thead tbody c:forEach items${reservationList} varr tr td${r.expName}/td td fmt:formatDate value${r.applyTime} patternyyyy-MM-dd HH:mm / /td td c:choose c:when test${r.status 0}待审批/c:when c:when test${r.status 1}已通过/c:when c:when test${r.status 2}已拒绝/c:when c:otherwise未知/c:otherwise /c:choose /td /tr /c:forEach /tbody /table推荐在Servlet里先把ListReservationVO查好放到request作用域JSP页面里只负责显示。${r.expName}会调用JavaBean的getter方法因此ReservationVO的属性名要和页面对应字段一致。用c:forEach遍历从1开始不是从0开始页面上那个varStatusloop可以拿到循环序号渲染排名的时候才用得到。4.4 JSP编译后的class文件在哪页面不生效怎么查idea创建jsp文件后JSP并不是直接执行的Tomcat会把JSP翻译成Java源文件再编译成class。部署当Tomcat运行起来后JSP的class文件在Tomcat的work目录下。路径是# Tomcat根目录 ../apache-tomcat-9.0.xx/work/Catalina/localhost/你的工程名/org/apache/jsp/比如登录页login.jsp会生成login_jsp.java和login_jsp.class。这个目录一个很有用的排查技巧是页面报500的时候Tomcat日志里的报错只会指向这个生成的Java文件行号并不指向JSP原始行号这时候打开这个_jsp.java定位到对应行就能反推出JSP里是哪个标签或脚本出了问题。这个路径也解释了另一个常见现象改完JSP页面重新部署后不生效大部分是因为Tomcat缓存了旧的class手动清空work目录再重启或者直接在Tomcat控制台点redeploy就能恢复。5. 部署、答辩与安全加固的三条捷径5.1 把SQL Server驱动打进war包如果直接在IDEA里运行Tomcat跑程序一般很顺问题出在打包部署。用IDEA打war包时默认不会把Tomcat的lib目录里的驱动拷过去所以SQL Server的mssql-jdbc-x.x.x.jre8.jar必须放到WEB-INF/lib下面而不是放在Tomcat的lib里。否则你在自己电脑上跑正常换个环境就报ClassNotFoundException: com.microsoft.sqlserver.jdbc.SQLServerDriver原因就是war包里压根没带驱动。检查war包内容可以用这个命令jar tf target/xxx.war | grep sqlserver如果输出里没有WEB-INF/lib/mssql-jdbc-xxx.jar说明驱动没打进去。5.2 答辩追问三连的预备答案第一个问题为什么用SQL Server不用MySQL回答思路是课程积累是Windows环境SQL Server的SSMS图形化工具在展示表关系、生成ER图时比命令行方便而且SQL Server自带的事务日志能客观说明崩溃恢复场景论文里不需要解释底层实现也能讲清楚。第二个问题两名学生同时预约最后一个名额你的系统凭什么不超卖这时直接把4.2节的SQL贴出来强调条件更新和事务。第三个问题密码为什么用SHA-256加盐而不是MD5答案简单直接MD5有彩虹表风险加盐后每个用户盐值不同相同的明文密码散列结果也不一样。5.3 三个能写进论文的安全细节第一个细节是权限校验不止应放在过滤器JSP页面里的操作按钮也应该按角色渲染否则教师在页面上看到“删除用户”按钮并且点了之后被过滤器拦下来体验很糟。这个用c:if就能解决。第三个细节是操作日志。建一张t_log表在Filter里统一记录用户请求的URI、参数排除password字段、耗时的毫秒数表格结构也很简单。CREATE TABLE t_log ( log_id INT IDENTITY(1,1) PRIMARY KEY, user_id INT NULL, req_uri VARCHAR(255) NOT NULL, req_param NVARCHAR(500) NULL, -- 半结构化文本 cost_ms INT NOT NULL, create_time DATETIME DEFAULT GETDATE() );执行完写日志不需要事务占用时间极其短不会影响主流程。日志数据还能当作答辩时的“系统具备可审计行为”的佐证材料成本很低收益很高。本文还有配套的精品资源点击获取