某高校教务系统采用JSP技术运行三年零故障 学生选课教师排课家长查询成绩全流程线上化 常见问题解决方案汇总
说实话,把教务系统做成”三年零故障”这种成果,在高校信息化领域真的不算多见。
我们这套系统从上线到现在,已经稳稳跑过了三个完整的学年周期,覆盖学生六千多人、教师四百余人,日常峰值并发接近两千——选课季那几天,系统照样没出过任何严重的线上事故。回头看这三年积累的经验,值得写出来跟大家聊聊。
技术选型为什么还是JSP
很多人看到”JSP”可能会觉得老气。确实,现在Spring Boot、Vue、微服务满天飞,但在我们当时做选型的时候,JSP + Servlet + JDBC的组合是最务实的决定。
原因很简单:我们当时的技术团队只有三个人,没有专职的后端开发,也没有运维人员。学校服务器的硬件条件一般,预算也有限。在这种背景下,过度追求新技术反而容易翻车。
JSP的优势在于:
- 学习曲线低,团队成员都能快速上手
- 部署简单,Tomcat一键启动
- 与MySQL的配合非常成熟,资料丰富
- 出错时排查路径清晰,不会陷入框架黑盒的困境
当然,JSP也有缺点,比如前后端耦合、代码维护性一般、容易出现N+1查询等问题。但这些问题都可以通过良好的编码规范和架构约束来解决,并不影响系统的稳定性。
系统整体架构
我们的系统采用经典的三层架构,结构非常清晰:
┌─────────────────────────────────────┐
│ 表现层 │
│ JSP页面 + JavaScript + CSS │
├─────────────────────────────────────┤
│ 控制层 │
│ Servlet + Filter + 拦截器 │
├─────────────────────────────────────┤
│ 业务层 │
│ Service层 + 事务管理 │
├─────────────────────────────────────┤
│ 数据访问层 │
│ DAO层 + JDBC + 连接池 │
├─────────────────────────────────────┤
│ 数据库层 │
│ MySQL 5.7 + 索引优化 │
└─────────────────────────────────────┘
核心文件结构如下:
src/
├── com/edu/scheduler/
│ ├── servlet/ // Servlet控制器
│ │ ├── StudentServlet.java
│ │ ├── TeacherServlet.java
│ │ ├── AdminServlet.java
│ │ └── ParentServlet.java
│ ├── service/ // 业务逻辑层
│ │ ├── CourseService.java
│ │ ├── ScheduleService.java
│ │ ├── GradeService.java
│ │ └── UserService.java
│ ├── dao/ // 数据访问层
│ │ ├── CourseDAO.java
│ │ ├── ScheduleDAO.java
│ │ ├── GradeDAO.java
│ │ └── BaseDAO.java
│ ├── model/ // 实体类
│ │ ├── Student.java
│ │ ├── Teacher.java
│ │ ├── Course.java
│ │ └── Grade.java
│ ├── filter/ // 过滤器
│ │ └── AuthFilter.java
│ └── util/ // 工具类
│ ├── DBUtil.java
│ ├── PageUtil.java
│ └── StringUtil.java
├── web/
│ ├── jsp/ // 页面
│ ├── css/ // 样式
│ └── js/ // 脚本
└── WEB-INF/
├── web.xml // 配置文件
└── lib/ // 依赖JAR
数据库设计要点
数据库设计是系统稳定运行的基础。我们花了大量时间做表结构优化,下面是核心表的设计思路。
学生表 student
CREATE TABLE student (
id VARCHAR(20) PRIMARY KEY COMMENT '学号',
name VARCHAR(50) NOT NULL COMMENT '姓名',
gender CHAR(1) COMMENT '性别',
major_id VARCHAR(20) COMMENT '专业ID',
class_id VARCHAR(20) COMMENT '班级ID',
password VARCHAR(64) NOT NULL COMMENT '密码(MD5)',
phone VARCHAR(20) COMMENT '手机号',
email VARCHAR(100) COMMENT '邮箱',
status TINYINT DEFAULT 1 COMMENT '状态:1正常 0冻结',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_major (major_id),
INDEX idx_class (class_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表';
课程表 course
CREATE TABLE course (
id INT AUTO_INCREMENT PRIMARY KEY COMMENT '课程ID',
course_no VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号',
course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
credits DECIMAL(3,1) NOT NULL COMMENT '学分',
total_hours INT NOT NULL COMMENT '总学时',
major_id VARCHAR(20) COMMENT '所属专业',
semester INT NOT NULL COMMENT '学期',
capacity INT NOT NULL COMMENT '最大容量',
current_count INT DEFAULT 0 COMMENT '当前选课人数',
status TINYINT DEFAULT 1 COMMENT '状态:1开放 0关闭',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_course_no (course_no),
INDEX idx_major_semester (major_id, semester)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';
选课记录表 course_selection
CREATE TABLE course_selection (
id INT AUTO_INCREMENT PRIMARY KEY,
student_id VARCHAR(20) NOT NULL COMMENT '学号',
course_id INT NOT NULL COMMENT '课程ID',
teacher_id VARCHAR(20) COMMENT '授课教师',
class_time VARCHAR(50) COMMENT '上课时间',
classroom VARCHAR(50) COMMENT '教室',
select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间',
status TINYINT DEFAULT 1 COMMENT '状态:1已选 0已退',
INDEX idx_student (student_id),
INDEX idx_course (course_id),
UNIQUE KEY uk_student_course (student_id, course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课记录表';
成绩表 grade
CREATE TABLE grade (
id INT AUTO_INCREMENT PRIMARY KEY,
student_id VARCHAR(20) NOT NULL COMMENT '学号',
course_id INT NOT NULL COMMENT '课程ID',
final_score DECIMAL(5,2) COMMENT '期末成绩',
total_score DECIMAL(5,2) COMMENT '总成绩',
grade_point DECIMAL(3,2) COMMENT '绩点',
status TINYINT DEFAULT 1 COMMENT '状态:1有效 0作废',
submit_time DATETIME COMMENT '提交时间',
INDEX idx_student_course (student_id, course_id),
INDEX idx_course (course_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='成绩表';
核心功能实现
学生选课模块
选课环节是整个系统压力最大的地方。我们的处理思路是:先做前端校验,再做后端校验,最后写库。
前端选课界面(选课页面核心片段):
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.edu.scheduler.model.Course" %>
<%@ page import="java.util.List" %>
<%@ page import="com.edu.scheduler.service.CourseService" %>
<%
String studentId = (String) session.getAttribute("userId");
if (studentId == null) {
response.sendRedirect("/login.jsp");
return;
}
CourseService courseService = new CourseService();
List<Course> courses = courseService.getByMajor(studentId);
%>
<div class="course-select-container">
<h3>本学期可选课程</h3>
<div class="tips-box">
<p>温馨提示:选课时间为每周三下午14:00-16:00,请准时操作。每人最多选18学分。</p>
<p>当前已选学分:<span id="selectedCredits">0</span> / 18</p>
</div>
<table class="course-table">
<thead>
<tr>
<th>课程编号</th>
<th>课程名称</th>
<th>学分</th>
<th>授课教师</th>
<th>上课时间</th>
<th>已选/容量</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<% for (Course course : courses) { %>
<tr>
<td><%= course.getCourseNo() %></td>
<td><%= course.getCourseName() %></td>
<td><%= course.getCredits() %></td>
<td><%= course.getTeacherName() %></td>
<td><%= course.getClassTime() %></td>
<td>
<span class="count-selected"><%= course.getCurrentCount() %></span>/
<span class="count-capacity"><%= course.getCapacity() %></span>
</td>
<td>
<% if (course.getCurrentCount() < course.getCapacity()) { %>
<% if (course.getStatus() == 1) { %>
<button onclick="selectCourse(<%= course.getId() %>)"
class="btn-select">选课</button>
<% } else { %>
<button disabled class="btn-disabled">已关闭</button>
<% } %>
<% } else { %>
<button disabled class="btn-disabled">已满员</button>
<% } %>
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
<script>
// 前端防止重复点击
function selectCourse(courseId) {
var btn = event.target;
btn.disabled = true;
btn.textContent = '提交中...';
// 发送AJAX请求
fetch('/scheduler/selectCourse', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'courseId=' + courseId + '&studentId=<%= studentId %>'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('选课成功!');
location.reload();
} else {
alert('选课失败:' + data.message);
btn.disabled = false;
btn.textContent = '选课';
}
})
.catch(error => {
alert('网络错误,请稍后重试');
btn.disabled = false;
btn.textContent = '选课';
});
}
</script>
后端选课Servlet(关键逻辑):
package com.edu.scheduler.servlet;
import com.edu.scheduler.dao.CourseDAO;
import com.edu.scheduler.dao.CourseSelectionDAO;
import com.edu.scheduler.model.Course;
import com.edu.scheduler.model.Student;
import com.google.gson.Gson;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.HashMap;
import java.util.Map;
@WebServlet("/selectCourse")
public class SelectCourseServlet extends HttpServlet {
private CourseDAO courseDAO = new CourseDAO();
private CourseSelectionDAO selectionDAO = new CourseSelectionDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
String studentId = request.getParameter("studentId");
String courseIdStr = request.getParameter("courseId");
// 基础参数校验
if (studentId == null || studentId.isEmpty()
|| courseIdStr == null || courseIdStr.isEmpty()) {
jsonResponse(response, false, "参数不能为空");
return;
}
int courseId = 0;
try {
courseId = Integer.parseInt(courseIdStr);
} catch (NumberFormatException e) {
jsonResponse(response, false, "课程ID格式错误");
return;
}
// 查询学生信息
Student student = courseDAO.getStudentById(studentId);
if (student == null) {
jsonResponse(response, false, "学生信息不存在");
return;
}
// 查询课程信息
Course course = courseDAO.getById(courseId);
if (course == null) {
jsonResponse(response, false, "课程不存在");
return;
}
// 检查是否已选
if (selectionDAO.exists(studentId, courseId)) {
jsonResponse(response, false, "您已选修该课程");
return;
}
// 检查是否超学分
int currentCredits = selectionDAO.getTotalCredits(studentId);
if (currentCredits + course.getCredits() > 18) {
jsonResponse(response, false, "选课学分超限(最多18学分)");
return;
}
// 检查课程是否满员
if (course.getCurrentCount() >= course.getCapacity()) {
jsonResponse(response, false, "该课程已满员");
return;
}
// 使用事务保证数据一致性
Connection conn = null;
try {
conn = com.edu.scheduler.util.DBUtil.getConnection();
conn.setAutoCommit(false);
// 插入选课记录
selectionDAO.insert(conn, studentId, courseId);
// 更新课程已选人数
courseDAO.updateCount(conn, courseId, course.getCurrentCount() + 1);
conn.commit();
jsonResponse(response, true, "选课成功");
} catch (Exception e) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); }
}
jsonResponse(response, false, "选课失败,请稍后重试");
} finally {
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
}
private void jsonResponse(HttpServletResponse response, boolean success, String message)
throws IOException {
Map<String, Object> result = new HashMap<>();
result.put("success", success);
result.put("message", message);
response.getWriter().write(new Gson().toJson(result));
}
}
教师排课模块
教师排课是教务系统中最复杂的环节之一,涉及到教室资源、时间冲突检测等多个维度。
排课冲突检测算法:
package com.edu.scheduler.service;
import com.edu.scheduler.dao.ScheduleDAO;
import com.edu.scheduler.model.Course;
import com.edu.scheduler.model.Teacher;
import com.edu.scheduler.model.Schedule;
import java.sql.*;
import java.util.*;
public class ScheduleService {
private ScheduleDAO scheduleDAO = new ScheduleDAO();
/**
* 检查时间冲突
*/
public boolean checkTimeConflict(String teacherId, String classroom,
int weekDay, int startTime, int endTime) {
// 检查教师时间冲突
List<Schedule> teacherSchedules = scheduleDAO.getByTeacher(teacherId);
for (Schedule existing : teacherSchedules) {
if (overlap(existing.getWeekDay(), existing.getStartTime(),
existing.getEndTime(), weekDay, startTime, endTime)) {
return true;
}
}
// 检查教室时间冲突
List<Schedule> classroomSchedules = scheduleDAO.getByClassroom(classroom);
for (Schedule existing : classroomSchedules) {
if (overlap(existing.getWeekDay(), existing.getStartTime(),
existing.getEndTime(), weekDay, startTime, endTime)) {
return true;
}
}
return false;
}
/**
* 判断两个时间段是否重叠
*/
private boolean overlap(int day1, int start1, int end1,
int day2, int start2, int end2) {
if (day1 != day2) {
return false;
}
// 时间重叠判断:start1 < end2 && end1 > start2
return start1 < end2 && end1 > start2;
}
/**
* 批量排课并自动检测冲突
*/
public Map<String, Object> batchSchedule(List<Schedule> scheduleList) {
Map<String, Object> result = new HashMap<>();
List<String> conflicts = new ArrayList<>();
int successCount = 0;
Connection conn = null;
try {
conn = com.edu.scheduler.util.DBUtil.getConnection();
conn.setAutoCommit(false);
for (Schedule schedule : scheduleList) {
// 检查时间冲突
boolean hasConflict = checkTimeConflict(
schedule.getTeacherId(),
schedule.getClassroom(),
schedule.getWeekDay(),
schedule.getStartTime(),
schedule.getEndTime()
);
if (hasConflict) {
conflicts.add("课程[" + schedule.getCourseName()
+ "]与已有排课存在时间冲突");
continue;
}
// 检查教室容量
if (schedule.getClassroomCapacity() < schedule.getStudentCount()) {
conflicts.add("课程[" + schedule.getCourseName()
+ "]教室容量不足");
continue;
}
// 插入排课记录
scheduleDAO.insert(conn, schedule);
successCount++;
}
conn.commit();
} catch (Exception e) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); }
}
result.put("success", false);
result.put("message", "排课失败:" + e.getMessage());
return result;
} finally {
if (conn != null) {
try { conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
result.put("success", true);
result.put("successCount", successCount);
result.put("conflictCount", conflicts.size());
result.put("conflicts", conflicts);
return result;
}
}
排课管理页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.edu.scheduler.service.ScheduleService" %>
<%@ page import="com.edu.scheduler.model.Course" %>
<%@ page import="java.util.List" %>
<%
String teacherId = (String) session.getAttribute("userId");
if (teacherId == null) {
response.sendRedirect("/login.jsp");
return;
}
ScheduleService scheduleService = new ScheduleService();
List<Course> myCourses = scheduleService.getTeacherCourses(teacherId);
%>
<div class="schedule-container">
<h3>我的课程排课管理</h3>
<div class="schedule-form">
<form id="scheduleForm" onsubmit="return submitSchedule()">
<div class="form-group">
<label>选择课程:</label>
<select id="courseSelect" name="courseId" required>
<% for (Course course : myCourses) { %>
<option value="<%= course.getId() %>">
<%= course.getCourseName() %>
(<%= course.getCourseNo() %>)
</option>
<% } %>
</select>
</div>
<div class="form-group">
<label>上课星期:</label>
<select id="weekDay" name="weekDay" required>
<option value="1">周一</option>
<option value="2">周二</option>
<option value="3">周三</option>
<option value="4">周四</option>
<option value="5">周五</option>
<option value="6">周六</option>
</select>
</div>
<div class="form-group">
<label>开始时间:</label>
<select id="startTime" name="startTime" required>
<option value="1">第1-2节 (8:00-9:40)</option>
<option value="3">第3-4节 (10:00-11:40)</option>
<option value="5">第5-6节 (14:00-15:40)</option>
<option value="7">第7-8节 (16:00-17:40)</option>
<option value="9">第9-10节 (19:00-20:40)</option>
</select>
</div>
<div class="form-group">
<label>教室:</label>
<select id="classroom" name="classroom" required>
<option value="A101">A101 (容量:120)</option>
<option value="A102">A102 (容量:80)</option>
<option value="B201">B201 (容量:150)</option>
<option value="B202">B202 (容量:60)</option>
<option value="C301">C301 (容量:200)</option>
</select>
</div>
<button type="submit" class="btn-primary">提交排课</button>
</form>
</div>
<div class="schedule-list">
<h4>已排课程</h4>
<table class="schedule-table">
<thead>
<tr>
<th>课程名称</th>
<th>星期</th>
<th>时间</th>
<th>教室</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<%
List<Schedule> schedules = scheduleService.getTeacherSchedules(teacherId);
for (Schedule schedule : schedules) {
%>
<tr>
<td><%= schedule.getCourseName() %></td>
<td><%= getWeekDayName(schedule.getWeekDay()) %></td>
<td><%= schedule.getStartTime() %>-<%= schedule.getEndTime() %></td>
<td><%= schedule.getClassroom() %></td>
<td>
<button onclick="deleteSchedule(<%= schedule.getId() %>)">删除</button>
</td>
</tr>
<% } %>
</tbody>
</table>
</div>
</div>
<script>
function submitSchedule() {
var form = document.getElementById('scheduleForm');
var formData = new FormData(form);
fetch('/scheduler/batchSchedule', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('排课成功!成功' + data.successCount + '条,' + data.conflictCount + '条存在冲突');
if (data.conflicts.length > 0) {
alert('冲突详情:' + data.conflicts.join('\n'));
}
location.reload();
} else {
alert('排课失败:' + data.message);
}
})
.catch(error => {
alert('网络错误,请稍后重试');
});
return false;
}
</script>
家长查询成绩模块
家长端的设计重点是信息安全,成绩查询必须经过严格的身份验证。
成绩查询接口:
package com.edu.scheduler.servlet;
import com.edu.scheduler.dao.GradeDAO;
import com.edu.scheduler.model.Grade;
import com.edu.scheduler.model.Student;
import com.google.gson.Gson;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@WebServlet("/parent/queryGrade")
public class ParentQueryGradeServlet extends HttpServlet {
private GradeDAO gradeDAO = new GradeDAO();
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=UTF-8");
String parentId = request.getParameter("parentId");
String studentId = request.getParameter("studentId");
String idCard = request.getParameter("idCard");
String semester = request.getParameter("semester");
// 1. 验证家长身份
if (parentId == null || parentId.isEmpty()
|| studentId == null || studentId.isEmpty()
|| idCard == null || idCard.isEmpty()) {
jsonResponse(response, false, "参数不完整");
return;
}
// 2. 验证身份证号与学号匹配
Student student = gradeDAO.getStudentById(studentId);
if (student == null) {
jsonResponse(response, false, "学号不存在");
return;
}
// 3. 验证家长与学生的绑定关系
if (!parentId.equals(student.getParentId())) {
jsonResponse(response, false, "您无权查询该学生的成绩");
return;
}
// 4. 查询成绩
List<Grade> grades = gradeDAO.getByStudent(studentId, semester);
Map<String, Object> result = new HashMap<>();
result.put("success", true);
result.put("studentName", student.getName());
result.put("grades", grades);
result.put("totalCredits", student.getTotalCredits());
result.put("totalScore", student.getTotalScore());
result.put("averageScore", student.getAverageScore());
response.getWriter().write(new Gson().toJson(result));
}
private void jsonResponse(HttpServletResponse response, boolean success, String message)
throws IOException {
Map<String, Object> result = new HashMap<>();
result.put("success", success);
result.put("message", message);
response.getWriter().write(new Gson().toJson(result));
}
}
家长查询页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="com.edu.scheduler.model.Grade" %>
<%@ page import="java.util.List" %>
<%
String parentId = (String) session.getAttribute("userId");
if (parentId == null) {
response.sendRedirect("/login.jsp");
return;
}
%>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>家长成绩查询</title>
<style>
.query-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
.query-form {
background: #f5f5f5;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input, .form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.btn-query {
background: #4CAF50;
color: white;
padding: 10px 30px;
border: none;
border-radius: 4px;
cursor: pointer;
}
.btn-query:hover {
background: #45a049;
}
.grade-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.grade-table th, .grade-table td {
border: 1px solid #ddd;
padding: 12px;
text-align: center;
}
.grade-table th {
background: #4CAF50;
color: white;
}
.grade-table tr:nth-child(even) {
background: #f9f9f9;
}
.summary-box {
background: #e8f5e9;
padding: 15px;
border-radius: 8px;
margin-top: 20px;
}
.error-msg {
color: red;
padding: 10px;
background: #ffebee;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="query-container">
<h2>家长成绩查询</h2>
<div class="query-form">
<div class="form-group">
<label>学生学号:</label>
<input type="text" id="studentId" placeholder="请输入学生学号">
</div>
<div class="form-group">
<label>身份证号:</label>
<input type="password" id="idCard" placeholder="请输入家长身份证号">
</div>
<div class="form-group">
<label>查询学期:</label>
<select id="semester">
<option value="2023-2024-1">2023-2024学年第一学期</option>
<option value="2023-2024-2">2023-2024学年第二学期</option>
<option value="2024-2025-1">2024-2025学年第一学期</option>
<option value="2024-2025-2">2024-2025学年第二学期</option>
</select>
</div>
<button class="btn-query" onclick="queryGrade()">查询成绩</button>
</div>
<div id="resultArea"></div>
</div>
<script>
function queryGrade() {
var studentId = document.getElementById('studentId').value;
var idCard = document.getElementById('idCard').value;
var semester = document.getElementById('semester').value;
if (!studentId || !idCard) {
document.getElementById('resultArea').innerHTML =
'<div class="error-msg">请输入完整的查询信息</div>';
return;
}
// 发送查询请求
var formData = new FormData();
formData.append('parentId', '<%= parentId %>');
formData.append('studentId', studentId);
formData.append('idCard', idCard);
formData.append('semester', semester);
fetch('/scheduler/parent/queryGrade', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.success) {
displayGrades(data);
} else {
document.getElementById('resultArea').innerHTML =
'<div class="error-msg">' + data.message + '</div>';
}
})
.catch(error => {
document.getElementById('resultArea').innerHTML =
'<div class="error-msg">查询失败,请稍后重试</div>';
});
}
function displayGrades(data) {
var html = '<div class="summary-box">';
html += '<h4>' + data.studentName + '的成绩报告</h4>';
html += '<p>总学分:' + data.totalCredits + '</p>';
html += '<p>总绩点:' + data.totalScore + '</p>';
html += '<p>平均成绩:' + data.averageScore + '</p>';
html += '</div>';
html += '<table class="grade-table">';
html += '<thead><tr><th>课程名称</th><th>课程编号</th><th>成绩</th><th>学分</th><th>绩点</th></tr></thead>';
html += '<tbody>';
for (var i = 0; i < data.grades.length; i++) {
var grade = data.grades[i];
html += '<tr>';
html += '<td>' + grade.courseName + '</td>';
html += '<td>' + grade.courseNo + '</td>';
html += '<td>' + grade.finalScore + '</td>';
html += '<td>' + grade.credit + '</td>';
html += '<td>' + grade.gradePoint + '</td>';
html += '</tr>';
}
html += '</tbody></table>';
document.getElementById('resultArea').innerHTML = html;
}
</script>
</body>
</html>
过滤器与权限控制
权限控制是系统安全的第一道防线。我们的认证过滤器覆盖了所有核心接口。
package com.edu.scheduler.filter;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
public class AuthFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化代码
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String uri = httpRequest.getRequestURI();
// 跳过静态资源
if (uri.contains(".css") || uri.contains(".js") ||
uri.contains(".png") || uri.contains(".jpg")) {
chain.doFilter(request, response);
return;
}
// 跳过登录页面
if (uri.equals("/login.jsp") || uri.equals("/register.jsp")) {
chain.doFilter(request, response);
return;
}
// 检查用户是否已登录
HttpSession session = httpRequest.getSession(false);
if (session == null || session.getAttribute("userId") == null) {
httpResponse.sendRedirect("/login.jsp");
return;
}
// 检查角色权限
String role = (String) session.getAttribute("role");
// 学生只能访问学生相关接口
if ("student".equals(role) && uri.contains("/admin")) {
httpResponse.sendError(403, "无权访问");
return;
}
// 教师不能访问管理员接口
if (("teacher".equals(role) || "parent".equals(role)) && uri.contains("/admin")) {
httpResponse.sendError(403, "无权访问");
return;
}
// 通过验证,继续请求
chain.doFilter(request, response);
}
@Override
public void destroy() {
// 销毁代码
}
}
web.xml配置:
<filter>
<filter-name>AuthFilter</filter-name>
<filter-class>com.edu.scheduler.filter.AuthFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>AuthFilter</filter-name>
<url-pattern>/scheduler/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>EncodingFilter</filter-name>
<filter-class>com.edu.scheduler.filter.EncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>EncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
常见问题解决方案汇总
三年运行下来,我们遇到过不少问题。这里把典型的几类问题整理出来,希望能帮到正在做类似项目的团队。
问题一:高并发下的选课超时
现象: 选课高峰期,大量用户同时点击选课,导致数据库连接耗尽,请求超时。
解决方案:
- 引入连接池管理,使用Druid连接池:
<!-- pom.xml依赖 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.8</version>
</dependency>
package com.edu.scheduler.util;
import com.alibaba.druid.pool.DruidDataSourceFactory;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;
public class DBUtil {
private static DataSource dataSource;
static {
try {
Properties props = new Properties();
props.load(DBUtil.class.getClassLoader().getResourceAsStream("db.properties"));
dataSource = DruidDataSourceFactory.createDataSource(props);
} catch (Exception e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
public static void closeConnection(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
# db.properties
driverClassName=com.mysql.cj.jdbc.Driver
url=jdbc:mysql://localhost:3306/scheduler?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username=root
password=your_password
initialSize=5
maxActive=50
maxWait=3000
minIdle=3
validationQuery=SELECT 1
testWhileIdle=true
testOnBorrow=false
testOnReturn=false
- 增加选课队列机制,避免瞬时峰值:
package com.edu.scheduler.service;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
public class CourseSelectQueue {
private static BlockingQueue<SelectTask> taskQueue = new LinkedBlockingQueue<>(1000);
public static boolean submitTask(SelectTask task) {
try {
// 如果队列已满,直接拒绝
if (!taskQueue.offer(task)) {
return false;
}
return true;
} catch (Exception e) {
return false;
}
}
public static SelectTask takeTask() throws InterruptedException {
return taskQueue.take();
}
public static int size() {
return taskQueue.size();
}
}
class SelectTask {
private String studentId;
private int courseId;
private long timestamp;
// 构造方法和getter/setter省略
}
- 增加Redis缓存,减少数据库压力:
package com.edu.scheduler.util;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisUtil {
private static JedisPool jedisPool;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(100);
config.setMaxIdle(10);
config.setMinIdle(5);
jedisPool = new JedisPool(config, "localhost", 6379, 2000);
}
public static Jedis getJedis() {
return jedisPool.getResource();
}
public static void closeJedis(Jedis jedis) {
if (jedis != null) {
jedis.close();
}
}
}
package com.edu.scheduler.service;
import com.edu.scheduler.dao.CourseDAO;
import com.edu.scheduler.model.Course;
import com.edu.scheduler.util.RedisUtil;
import redis.clients.jedis.Jedis;
public class CacheService {
private CourseDAO courseDAO = new CourseDAO();
/**
* 获取课程信息(优先从缓存)
*/
public Course getCourse(int courseId) {
String cacheKey = "course:" + courseId;
Jedis jedis = RedisUtil.getJedis();
try {
// 先查缓存
String cached = jedis.get(cacheKey);
if (cached != null && !cached.equals("null")) {
return parseCourse(cached);
}
// 缓存未命中,查数据库
Course course = courseDAO.getById(courseId);
if (course != null) {
// 写入缓存,过期时间30分钟
jedis.setex(cacheKey, 1800, serializeCourse(course));
}
return course;
} finally {
RedisUtil.closeJedis(jedis);
}
}
/**
* 更新课程缓存
*/
public void updateCourseCache(int courseId) {
String cacheKey = "course:" + courseId;
Jedis jedis = RedisUtil.getJedis();
try {
Course course = courseDAO.getById(courseId);
if (course != null) {
jedis.setex(cacheKey, 1800, serializeCourse(course));
} else {
jedis.del(cacheKey);
}
} finally {
RedisUtil.closeJedis(jedis);
}
}
// 序列化和反序列化方法省略
}
问题二:时间冲突检测不准确
现象: 排课系统偶尔出现时间冲突遗漏,导致同一教师、同一教室在同一时间段被安排了多门课程。
解决方案:
优化冲突检测算法,增加边界条件处理:
package com.edu.scheduler.service;
import com.edu.scheduler.dao.ScheduleDAO;
import com.edu.scheduler.model.Schedule;
import java.sql.*;
import java.util.*;
public class ScheduleService {
private ScheduleDAO scheduleDAO = new ScheduleDAO();
/**
* 检查时间冲突(优化版)
*/
public boolean checkTimeConflict(String teacherId, String classroom,
int weekDay, int startTime, int endTime) {
// 边界条件处理
if (startTime >= endTime) {
return true; // 无效的时间段
}
// 查询所有同星期、有时间重叠的记录
List<Schedule> overlappingSchedules =
scheduleDAO.getOverlappingSchedules(weekDay, startTime, endTime);
for (Schedule existing : overlappingSchedules) {
// 检查教师冲突
if (teacherId.equals(existing.getTeacherId())) {
return true;
}
// 检查教室冲突
if (classroom.equals(existing.getClassroom())) {
return true;
}
}
return false;
}
}
package com.edu.scheduler.dao;
import com.edu.scheduler.model.Schedule;
import com.edu.scheduler.util.DBUtil;
import java.sql.*;
import java.util.*;
public class ScheduleDAO {
/**
* 查询可能存在时间冲突的记录
*/
public List<Schedule> getOverlappingSchedules(int weekDay, int startTime, int endTime) {
List<Schedule> result = new ArrayList<>();
// 时间重叠条件:existingStartTime < newEndTime AND existingEndTime > newStartTime
String sql = "SELECT * FROM schedule WHERE week_day = ? AND status = 1 " +
"AND start_time < ? AND end_time > ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, weekDay);
pstmt.setInt(2, endTime);
pstmt.setInt(3, startTime);
rs = pstmt.executeQuery();
while (rs.next()) {
Schedule schedule = new Schedule();
schedule.setId(rs.getInt("id"));
schedule.setTeacherId(rs.getString("teacher_id"));
schedule.setCourseId(rs.getInt("course_id"));
schedule.setClassroom(rs.getString("classroom"));
schedule.setWeekDay(rs.getInt("week_day"));
schedule.setStartTime(rs.getInt("start_time"));
schedule.setEndTime(rs.getInt("end_time"));
result.add(schedule);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
return result;
}
}
问题三:成绩数据安全
现象: 成绩泄露事件时有发生,家长担心孩子成绩被其他人看到。
解决方案:
- 增加数据加密:
package com.edu.scheduler.util;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
public class AESUtil {
private static final String ALGORITHM = "AES";
private static final String TRANSFORMATION = "AES/CBC/PKCS5Padding";
/**
* AES加密
*/
public static String encrypt(String data, String key) throws Exception {
// 生成密钥
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
keyGen.init(128);
SecretKey secretKey = new SecretKeySpec(key.getBytes(), ALGORITHM);
// 初始化加密器
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(key.getBytes()));
// 加密
byte[] encrypted = cipher.doFinal(data.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* AES解密
*/
public static String decrypt(String encryptedData, String key) throws Exception {
KeyGenerator keyGen = KeyGenerator.getInstance(ALGORITHM);
keyGen.init(128);
SecretKey secretKey = new SecretKeySpec(key.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(key.getBytes()));
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decrypted, "UTF-8");
}
}
- 增加查询日志审计:
package com.edu.scheduler.filter;
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.Date;
public class LogFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
long startTime = System.currentTimeMillis();
// 执行请求
chain.doFilter(request, response);
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 记录日志
String userId = (String) httpRequest.getSession().getAttribute("userId");
String role = (String) httpRequest.getSession().getAttribute("role");
String uri = httpRequest.getRequestURI();
String method = httpRequest.getMethod();
String queryString = httpRequest.getQueryString();
// 异步写入日志表
new Thread(() -> {
try {
insertLog(userId, role, uri, method, queryString, costTime);
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
private void insertLog(String userId, String role, String uri,
String method, String queryString, long costTime) {
String sql = "INSERT INTO access_log (user_id, role, uri, method, query_string, cost_time, access_time) " +
"VALUES (?, ?, ?, ?, ?, ?, ?)";
Connection conn = null;
PreparedStatement pstmt = null;
try {
conn = com.edu.scheduler.util.DBUtil.getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, userId);
pstmt.setString(2, role);
pstmt.setString(3, uri);
pstmt.setString(4, method);
pstmt.setString(5, queryString);
pstmt.setLong(6, costTime);
pstmt.setTimestamp(7, new Timestamp(System.currentTimeMillis()));
pstmt.executeUpdate();
} catch (SQLException e) {
e.printStackTrace();
} finally {
try { if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
}
}
问题四:数据库性能瓶颈
现象: 系统运行一段时间后,查询响应时间逐渐变长,尤其是选课和成绩查询环节。
解决方案:
- 定期分析慢查询:
-- 开启慢查询日志
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 2;
-- 查看慢查询日志文件
SHOW VARIABLES LIKE 'slow_query_log_file';
- 添加必要的索引:
-- 查看未使用索引的表
SELECT
TABLE_SCHEMA,
TABLE_NAME,
INDEX_NAME,
CARDINALITY
FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = 'scheduler'
ORDER BY TABLE_NAME, INDEX_NAME;
-- 添加缺失的索引
ALTER TABLE course_selection ADD INDEX idx_select_time (select_time);
ALTER TABLE grade ADD INDEX idx_submit_time (submit_time);
ALTER TABLE schedule ADD INDEX idx_teacher_weekday (teacher_id, week_day);
- 分页优化:
package com.edu.scheduler.util;
import java.sql.*;
import java.util.*;
public class PageUtil {
/**
* 高效分页查询
*/
public static <T> List<T> queryPage(Connection conn, String sql,
Object[] params, int page, int pageSize,
RowMapper<T> rowMapper) throws SQLException {
List<T> result = new ArrayList<>();
// 计算偏移量
int offset = (page - 1) * pageSize;
// 查询总数
String countSql = "SELECT COUNT(*) FROM (" + sql + ") AS total_count";
PreparedStatement countStmt = conn.prepareStatement(countSql);
for (int i = 0; i < params.length; i++) {
countStmt.setObject(i + 1, params[i]);
}
ResultSet countRs = countStmt.executeQuery();
int total = 0;
if (countRs.next()) {
total = countRs.getInt(1);
}
countRs.close();
countStmt.close();
// 分页查询
String pagedSql = sql + " LIMIT ? OFFSET ?";
PreparedStatement pstmt = conn.prepareStatement(pagedSql);
for (int i = 0; i < params.length; i++) {
pstmt.setObject(i + 1, params[i]);
}
pstmt.setInt(params.length + 1, pageSize);
pstmt.setInt(params.length + 2, offset);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
result.add(rowMapper.mapRow(rs));
}
rs.close();
pstmt.close();
return result;
}
}
问题五:JSP页面内存泄漏
现象: 系统运行几个月后,服务器内存占用逐渐升高,最终导致OutOfMemoryError。
解决方案:
- 避免在JSP中使用静态变量:
<%-- 错误示例:静态变量会导致内存泄漏 --%>
<%@ page import="java.util.HashMap" %>
<%@ page import="java.util.Map" %>
<%!
// 绝对不要在JSP中使用静态变量存储状态
private static Map<String, Object> cache = new HashMap<>();
%>
<%-- 正确做法:使用会话或应用域 --%>
- 及时关闭资源:
package com.edu.scheduler.dao;
import com.edu.scheduler.util.DBUtil;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
public class StudentDAO {
/**
* 正确关闭资源的示例
*/
public List<Student> getAllStudents() {
List<Student> students = new ArrayList<>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DBUtil.getConnection();
pstmt = conn.prepareStatement("SELECT * FROM student WHERE status = 1");
rs = pstmt.executeQuery();
while (rs.next()) {
Student student = new Student();
student.setId(rs.getString("id"));
student.setName(rs.getString("name"));
student.setMajorId(rs.getString("major_id"));
students.add(student);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
// 按顺序关闭资源
try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (pstmt != null) pstmt.close(); } catch (SQLException e) { e.printStackTrace(); }
try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
}
return students;
}
}
- 配置Tomcat内存参数:
# setenv.sh (Linux/Mac)
CATALINA_OPTS="-Xms512m -Xmx2048m -XX:MaxMetaspaceSize=256m"
# setenv.bat (Windows)
set CATALINA_OPTS=-Xms512m -Xmx2048m -XX:MaxMetaspaceSize=256m
系统监控与日志
为了让系统稳定运行,我们建立了完善的监控机制。
监控指标采集:
package com.edu.scheduler.monitor;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
public class MonitorUtil {
private static ConcurrentHashMap<String, AtomicInteger> counters =
new ConcurrentHashMap<>();
private static ConcurrentHashMap<String, Long> responseTimes =
new ConcurrentHashMap<>();
/**
* 记录请求
*/
public static void recordRequest(String uri) {
counters.computeIfAbsent(uri, k -> new AtomicInteger(0)).incrementAndGet();
}
/**
* 记录响应时间
*/
public static void recordResponseTime(String uri, long costTime) {
responseTimes.put(uri, costTime);
}
/**
* 获取请求统计
*/
public static String getStats() {
StringBuilder sb = new StringBuilder();
sb.append("=== 系统监控统计 ===\n");
for (String uri : counters.keySet()) {
sb.append(uri).append(": ")
.append(counters.get(uri).get()).append(" 次请求\n");
}
sb.append("\n--- 响应时间(最近)---\n");
for (String uri : responseTimes.keySet()) {
sb.append(uri).append(": ")
.append(responseTimes.get(uri)).append(" ms\n");
}
return sb.toString();
}
}
日志配置:
# log4j.properties
log4j.rootLogger=INFO, FILE, CONSOLE
# 文件输出
log4j.appender.FILE=org.apache.log4j.RollingFileAppender
log4j.appender.FILE.File=${catalina.home}/logs/scheduler.log
log4j.appender.FILE.MaxFileSize=10MB
log4j.appender.FILE.MaxBackupIndex=10
log4j.appender.FILE.layout=org.apache.log4j.PatternLayout
log4j.appender.FILE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c - %m%n
# 控制台输出
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c - %m%n
# 只记录scheduler包的日志
log4j.logger.com.edu.scheduler=INFO
三年运行的经验总结
回顾这三年的运行历程,有几个关键点值得总结:
1. 稳定性优先于功能
初期我们追求功能丰富,后来发现系统的稳定性才是第一位的。JSP技术虽然”古老”,但它的成熟度、文档完备性和问题排查路径都非常清晰。相比之下,盲目追求新技术可能带来不可控的风险。
2. 数据备份是底线
我们建立了每日全量备份 + 每小时增量备份的机制,备份文件同时存储在服务器本地和云端。这个习惯让我们避免了多次潜在的灾难性数据丢失风险。
# 备份脚本示例
#!/bin/bash
BACKUP_DIR="/backup/daily"
DATE=$(date +%Y%m%d_%H%M%S)
DB_NAME="scheduler"
# 全量备份
mysqldump -u root -p password $DB_NAME > $BACKUP_DIR/full_$DATE.sql
# 增量备份(binlog)
mysqlbinlog --start-datetime="$DATE" /var/lib/mysql/scheduler-bin.000001 > $BACKUP_DIR/increment_$DATE.sql
# 上传到云端
rsync -avz $BACKUP_DIR/ backup@cloud:/backup/scheduler/
# 清理30天前的备份
find $BACKUP_DIR -name "*.sql" -mtime +30 -delete
3. 用户体验不可忽视
虽然系统稳定很重要,但如果用户用起来不舒服,系统再好也会被抱怨。我们在选课页面增加了实时提示、在成绩查询页面增加了成绩分析图表、在排课页面增加了冲突预览,这些改进让用户满意度提升了几个台阶。
4. 定期维护比紧急救火更重要
每周一次的代码审查、每月一次的数据库优化、每季度的服务器维护,这些例行工作让我们避免了大部分潜在问题。系统出问题往往是长期积累的结果,定期的维护可以有效预防。
写到这里,三年来的点点滴滴历历在目。从最初的团队摸索,到后来的稳定运行,这套系统承载了学校的教学管理需求,也见证了信息化建设的意义。希望这篇分享能给正在做类似项目的你一些参考。
如果你正在规划自己的教务系统,建议先想清楚自己的需求场景,选择合适的技术方案,不要盲目追新。稳定性、可维护性、可扩展性,这三者之间的平衡,才是系统能够长期稳定运行的关键。
