在高校教学与科研活动中,实验室作为重要的实践场所,其管理效率直接影响着资源利用率和师生体验。传统依赖纸质登记、电话沟通或线下审批的实验室管理模式,普遍存在信息更新不及时、预约流程繁琐、资源冲突频发、数据统计困难等痛点。为了应对这些挑战,一个集中化、自动化、可视化的线上管理系统显得尤为必要。本系统正是在此背景下应运而生,旨在为高校构建一个高效、透明、易用的实验室资源调度与管理平台。
系统采用经典的JSP+Servlet+JavaBean技术架构,严格遵循MVC设计模式,实现了业务逻辑、数据模型和用户界面的有效分离。Servlet作为控制器负责接收并处理所有HTTP请求,调用相应的JavaBean(模型)进行业务处理和数据库操作,最后将结果转发至JSP页面(视图)进行渲染展示。数据持久层使用JDBC直接连接MySQL数据库,确保了数据操作的稳定性和效率。前端界面综合运用了HTML、CSS和JavaScript,辅以JSP的动态内容生成能力,为用户提供了直观友好的交互体验。整个系统通过Session机制管理用户登录状态,并利用Java的异常处理机制保障核心业务流程的健壮性。
数据库架构设计与核心表解析
一个稳健的后台数据模型是系统高效运行的基石。本系统数据库共设计了6张核心数据表,以下是其中几个关键表的结构分析,体现了其在实体关系映射和数据完整性方面的细致考量。
1. 实验室信息表 (laboratory)
该表是系统的核心资源表,定义了实验室的基本属性和状态。
CREATE TABLE `laboratory` (
`lab_id` int(11) NOT NULL AUTO_INCREMENT,
`lab_name` varchar(100) NOT NULL,
`location` varchar(200) DEFAULT NULL,
`capacity` int(11) DEFAULT NULL COMMENT '可容纳人数',
`equipment_info` text COMMENT '主要设备描述',
`status` enum('available','maintenance','unavailable') DEFAULT 'available',
`admin_id` int(11) DEFAULT NULL,
PRIMARY KEY (`lab_id`),
KEY `admin_id` (`admin_id`),
CONSTRAINT `laboratory_ibfk_1` FOREIGN KEY (`admin_id`) REFERENCES `admin` (`admin_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
设计亮点分析:
- 状态枚举约束:
status字段使用MySQL的ENUM类型,严格限定其取值只能为 'available'(可用)、'maintenance'(维护中)或 'unavailable'(不可用)。这种设计在数据库层面避免了无效状态值的插入,保证了数据的一致性,并为前端界面提供了清晰的状态标识。 - 外键关联管理:通过
admin_id字段与管理员表(admin)建立外键约束,明确了每个实验室的责任管理员。这不仅实现了数据关联,也便于后续的审计和责任追溯。 - 容量与设备信息:
capacity和equipment_info字段详细描述了实验室的物理属性,为师生选择合适的实验室提供了关键决策依据。
2. 预约记录表 (reservation)
此表是系统业务流的核心,记录了所有的预约申请及其生命周期状态。
CREATE TABLE `reservation` (
`reservation_id` int(11) NOT NULL AUTO_INCREMENT,
`lab_id` int(11) NOT NULL,
`applicant_id` int(11) NOT NULL COMMENT '申请人ID,关联student或teacher表',
`applicant_type` enum('student','teacher') NOT NULL,
`start_time` datetime NOT NULL,
`end_time` datetime NOT NULL,
`purpose` text NOT NULL,
`status` enum('pending','approved','rejected','completed','cancelled') DEFAULT 'pending',
`admin_notes` text COMMENT '管理员审批意见',
`submit_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`reservation_id`),
KEY `lab_id` (`lab_id`),
KEY `applicant_id` (`applicant_id`),
CONSTRAINT `reservation_ibfk_1` FOREIGN KEY (`lab_id`) REFERENCES `laboratory` (`lab_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
设计亮点分析:
- 多态申请人设计:通过
applicant_id和applicant_type两个字段的组合,巧妙地实现了对学生和教师两种不同身份申请人的统一管理。这种设计避免了为不同身份用户创建冗余的表结构,增强了系统的扩展性。 - 完整的预约生命周期管理:
status字段的枚举值覆盖了预约从提交('pending')、审批('approved'/'rejected')、执行('completed')到取消('cancelled')的全过程,清晰地定义了业务状态流。 - 时间戳与审计追踪:
submit_time使用CURRENT_TIMESTAMP作为默认值,自动记录申请提交时间。admin_notes字段则用于记录审批理由,满足了管理上的审计需求。
3. 用户表结构(以学生表为例)
用户身份管理是系统安全的基础。
CREATE TABLE `student` (
`student_id` int(11) NOT NULL AUTO_INCREMENT,
`student_number` varchar(20) NOT NULL UNIQUE,
`name` varchar(50) NOT NULL,
`password` varchar(255) NOT NULL COMMENT '加密存储',
`class_id` int(11) DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`phone` varchar(20) DEFAULT NULL,
PRIMARY KEY (`student_id`),
KEY `class_id` (`class_id`),
CONSTRAINT `student_ibfk_1` FOREIGN KEY (`class_id`) REFERENCES `class` (`class_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
设计亮点分析:学号(student_number)字段设置了UNIQUE约束,保证了用户唯一性。密码字段预留了255的长度,为使用安全的哈希算法(如bcrypt)进行加密存储做好了准备。与班级表(class)的外键关联,方便了按班级进行用户组织和管理。
核心功能模块实现解析
1. 用户认证与会话管理
用户登录是系统访问的入口,安全可靠的认证机制至关重要。系统通过Servlet处理登录请求,验证凭证后建立Session。
登录核心Servlet代码 (LoginServlet.java):
@WebServlet("/LoginServlet")
public class LoginServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String userType = request.getParameter("userType");
String username = request.getParameter("username");
String password = request.getParameter("password");
UserService userService = new UserService();
Object user = null;
boolean isValidLogin = false;
try {
switch (userType) {
case "student":
user = userService.authenticateStudent(username, password);
break;
case "teacher":
user = userService.authenticateTeacher(username, password);
break;
case "admin":
user = userService.authenticateAdmin(username, password);
break;
}
isValidLogin = (user != null);
} catch (SQLException e) {
e.printStackTrace();
request.setAttribute("errorMessage", "系统错误,请稍后重试。");
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
if (isValidLogin) {
HttpSession session = request.getSession();
session.setAttribute("user", user);
session.setAttribute("userType", userType);
session.setMaxInactiveInterval(30 * 60);
String redirectPage = "";
switch (userType) {
case "student": redirectPage = "/student/dashboard.jsp"; break;
case "teacher": redirectPage = "/teacher/dashboard.jsp"; break;
case "admin": redirectPage = "/admin/dashboard.jsp"; break;
}
response.sendRedirect(request.getContextPath() + redirectPage);
} else {
request.setAttribute("errorMessage", "用户名或密码错误!");
request.getRequestDispatcher("/login.jsp").forward(request, response);
}
}
}
代码解析:该Servlet根据userType参数区分用户角色,调用相应的UserService方法进行认证。认证成功后,将用户对象和角色类型存入Session,并设置会话超时时间为30分钟。通过response.sendRedirect实现登录后跳转到对应的主面板,避免了重复提交表单的问题。异常捕获确保了系统在数据库访问异常时能给出友好提示而非崩溃。

2. 实验室预约流程
预约功能是系统的核心,涉及复杂的业务逻辑,如时间冲突检测、状态管理等。
预约提交Servlet核心代码 (MakeReservationServlet.java):
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
int labId = Integer.parseInt(request.getParameter("labId"));
String applicantType = (String) session.getAttribute("userType");
int applicantId = getApplicantId(session, applicantType);
String startTimeStr = request.getParameter("startTime");
String endTimeStr = request.getParameter("endTime");
String purpose = request.getParameter("purpose");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm");
Reservation reservation = new Reservation();
try {
Date startTime = sdf.parse(startTimeStr);
Date endTime = sdf.parse(endTimeStr);
if (endTime.before(startTime) || startTime.before(new Date())) {
request.setAttribute("errorMessage", "时间选择无效!");
request.getRequestDispatcher("/reservation-form.jsp").forward(request, response);
return;
}
ReservationService resService = new ReservationService();
boolean isConflict = resService.checkTimeConflict(labId, startTime, endTime);
if (isConflict) {
request.setAttribute("errorMessage", "该时间段已被预约,请选择其他时间。");
request.getRequestDispatcher("/reservation-form.jsp").forward(request, response);
return;
}
reservation.setLabId(labId);
reservation.setApplicantId(applicantId);
reservation.setApplicantType(applicantType);
reservation.setStartTime(new Timestamp(startTime.getTime()));
reservation.setEndTime(new Timestamp(endTime.getTime()));
reservation.setPurpose(purpose);
reservation.setStatus("pending");
boolean success = resService.createReservation(reservation);
if (success) {
request.setAttribute("successMessage", "预约申请提交成功,等待管理员审核。");
} else {
request.setAttribute("errorMessage", "提交失败,请重试。");
}
} catch (ParseException e) {
request.setAttribute("errorMessage", "日期格式错误!");
} catch (SQLException e) {
e.printStackTrace();
request.setAttribute("errorMessage", "系统繁忙,请稍后重试。");
}
request.getRequestDispatcher("/reservation-result.jsp").forward(request, response);
}
private int getApplicantId(HttpSession session, String userType) {
switch (userType) {
case "student":
Student student = (Student) session.getAttribute("user");
return student.getStudentId();
case "teacher":
Teacher teacher = (Teacher) session.getAttribute("user");
return teacher.getTeacherId();
default:
return 0;
}
}
代码解析:此段代码完整展示了预约业务的核心逻辑。首先进行会话验证,确保用户已登录。然后解析前端传递的参数,并进行严格的业务规则校验:检查时间是否有效(结束时间不能早于开始时间,开始时间不能是过去时)、检测目标实验室在选定时间段内是否已被预约(通过checkTimeConflict方法)。所有校验通过后,才构建Reservation对象并持久化到数据库,状态初始化为'pending'(待审核)。整个流程通过异常处理机制保障了系统的稳定性。

3. 预约审批管理
管理员对预约申请的审批是实验室资源合理分配的关键环节。
审批操作Servlet代码 (ProcessReservationServlet.java):
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
int reservationId = Integer.parseInt(request.getParameter("reservationId"));
String action = request.getParameter("action"); // 'approve' or 'reject'
String adminNotes = request.getParameter("adminNotes");
HttpSession session = request.getSession(false);
Admin admin = (session != null) ? (Admin) session.getAttribute("user") : null;
if (admin == null) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
ReservationService resService = new ReservationService();
try {
boolean success = false;
String newStatus = "";
if ("approve".equals(action)) {
// 再次冲突检测,防止审批期间出现新的冲突
Reservation res = resService.getReservationById(reservationId);
boolean isStillAvailable = !resService.checkTimeConflict(res.getLabId(), res.getStartTime(), res.getEndTime(), reservationId);
if (isStillAvailable) {
success = resService.approveReservation(reservationId, admin.getId(), adminNotes);
newStatus = "approved";
} else {
request.setAttribute("errorMessage", "该时间段现已被占用,无法批准。");
}
} else if ("reject".equals(action)) {
success = resService.rejectReservation(reservationId, admin.getId(), adminNotes);
newStatus = "rejected";
}
if (success) {
request.setAttribute("message", "预约已成功" + ("approve".equals(action) ? "批准" : "拒绝") + "。");
// 记录操作日志
LogService.logAction(admin.getId(), "reservation_" + action, "Reservation ID: " + reservationId);
} else if (!"approve".equals(action) || !isStillAvailable) {
request.setAttribute("errorMessage", "操作失败。");
}
} catch (SQLException e) {
e.printStackTrace();
request.setAttribute("errorMessage", "数据库操作异常。");
}
request.getRequestDispatcher("/admin/reservation-management.jsp").forward(request, response);
}
代码解析:审批逻辑不仅简单地更新状态。在批准操作前,代码会再次执行时间冲突检测(checkTimeConflict方法传入了当前预约ID以排除自身),这是一个关键细节,确保了在用户提交预约到管理员审批这个时间窗口内,即使出现了新的冲突预约,审批操作也能保持数据一致性。审批操作会记录下管理员ID和审批意见,并触发操作日志的记录,满足了管理审计的要求。

4. 个人预约视图与状态跟踪
为用户提供清晰的个人预约记录视图是提升用户体验的重要功能。
个人预约查询JSP片段 (my-reservations.jsp):
<%@ page import="com.lab.model.Reservation, com.lab.model.Laboratory, java.util.List" %>
<%
List<Reservation> myReservations = (List<Reservation>) request.getAttribute("myReservations");
%>
<table class="table table-striped">
<thead>
<tr>
<th>实验室</th>
<th>预约时间</th>
<th>结束时间</th>
<th>用途</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% for (Reservation res : myReservations) { %>
<tr>
<td><%= res.getLaboratory().getLabName() %></td>
<td><%= new SimpleDateFormat("yyyy-MM-dd HH:mm").format(res.getStartTime()) %></td>
<td><%= new SimpleDateFormat("yyyy-MM-dd HH:mm").format(res.getEndTime()) %></td>
<td><%= res.getPurpose() %></td>
<td>
<span class="badge badge-<%=
"pending".equals(res.getStatus()) ? "warning" :
"approved".equals(res.getStatus()) ? "success" :
"rejected".equals(res.getStatus()) ? "danger" :
"completed".equals(res.getStatus()) ? "info" : "secondary"
%>">
<%=
"pending".equals(res.getStatus()) ? "待审核" :
"approved".equals(res.getStatus()) ? "已批准" :
"rejected".equals(res.getStatus()) ? "已拒绝" :
"completed".equals(res.getStatus()) ? "已完成" : res.getStatus()
%>
</span>
</td>
<td>
<% if ("pending".equals(res.getStatus())) { %>
<button class="btn btn-sm btn-outline-danger" onclick="cancelReservation(<%= res.getReservationId() %>)">取消</button>
<% } %>
</td>
</tr>
<% } %>
</tbody>
</table>
<script>
function cancelReservation(reservationId) {
if (confirm('确定要取消此预约吗?')) {
fetch('/CancelReservationServlet?reservationId=' + reservationId, { method: 'POST' })
.then(response => response.json())
.then(data => {
if (data.success) {
alert('取消成功!');
location.reload();
} else {
alert('取消失败:' + data.message);
}
});
}
}
</script>
代码解析:此JSP页面动态生成用户的预约列表。核心在于使用JSP脚本片段和表达式从请求属性中获取数据并循环渲染。状态字段通过条件判断动态赋予不同的CSS类(如badge-warning, badge-success),直观地以颜色区分不同状态。对于状态为“待审核”的预约,提供取消按钮,并通过JavaScript调用后端取消接口,实现了无需刷新页面的交互体验。

实体模型与业务逻辑封装
系统通过JavaBean对核心实体进行建模,封装属性和简单行为,并通过Service类