学校选课查成绩背后 JSP技术如何驱动教育系统 真实案例解析
你点的每一”选”,都在和服务器对话
每天早上七点半,宿舍楼里的手机就开始疯狂震动。不是闹钟,是选课系统开放的提醒。你揉着惺忪的睡眼点开网页,输入账号密码,盯着那些课程名疯狂点击——加选、退选、查看成绩,整个过程行云流水。
但你有没有想过,你点击鼠标的那一刻,到底发生了什么?
今天,我就带你揭开这个神秘的面纱。我们将深入到一个真实的教育系统,看看JSP技术是如何在后台默默支撑起每一次选课、每一次查分、每一次成绩的录入。
一、先认识一下JSP——Java Server Pages
JSP,全称是Java Server Pages,翻译过来就是”Java服务器页面”。听起来很复杂?别急,我给你打个比方。
想象一下,你是一个餐厅老板(服务器),顾客(用户)通过点菜单(网页)下单。JSP就像是那个能同时写菜单、下单、传菜的全能服务员。它既能让页面显示出来(HTML),又能处理业务逻辑(Java代码)。
具体来说,JSP做了三件事:
- 接收请求:当你在浏览器输入网址,JSP页面就会”醒来”处理这个请求。
- 处理业务:比如查询你选了哪些课、成绩是多少。
- 返回结果:把处理好的数据封装成HTML,发回给你。
为什么教育系统偏爱JSP?
- 安全:Java本身的特性,比PHP、ASP更稳定安全
- 跨平台:服务器用Linux、Windows都能跑
- 复用性强:代码写一次,到处都能用
- 生态成熟:十几年发展,组件丰富到离谱
二、真实案例:某高校教务系统的选课架构
让我给你讲一个真实的故事。三年前,我参与了一家教育科技公司的项目——为某理工大学开发教务系统。这个学校有2.8万名学生,900多名教师,开设1200多门课程。
选课开放的那一瞬间,峰值QPS(每秒查询率)达到500+,也就是说每秒有500个人在同时操作。如果系统扛不住,后果就是——所有人都在骂街。
系统整体架构
┌─────────────────────────────────────────────────────────────┐
│ 用户层(浏览器) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 学生端 │ │ 教师端 │ │ 管理员端 │ │ 家长端 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │ │
└───────┼─────────────┼─────────────┼─────────────┼───────────┘
│ │ │ │
┌───────▼─────────────▼─────────────▼─────────────▼───────────┐
│ 表示层(JSP + Servlet) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ 选课模块 成绩模块 课程管理模块 │ │
│ │ StudentCourseSelect.jsp 对应Servlet: CourseAction │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 业务逻辑层(Java Bean / Service) │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │CourseService │ │GradeService │ │StudentService │ │
│ │ 选课业务逻辑 │ │ 成绩计算逻辑 │ │ 学生信息管理 │ │
│ └──────┬──────┘ └──────┬──────┘ └────────┬─────────┘ │
└─────────┼────────────────┼───────────────────┼─────────────┘
│ │ │
┌─────────▼────────────────▼───────────────────▼─────────────┐
│ 数据访问层(DAO + JDBC) │
│ ┌─────────────┐ ┌─────────────┐ ┌──────────────────┐ │
│ │CourseDAO │ │GradeDAO │ │StudentDAO │ │
│ │ 课程数据访问 │ │ 成绩数据访问 │ │ 学生数据访问 │ │
│ └──────┬──────┘ └──────┬──────┘ └────────┬─────────┘ │
└─────────┼────────────────┼───────────────────┼─────────────┘
│ │ │
┌─────────▼────────────────▼───────────────────▼─────────────┐
│ 数据库层(MySQL 5.7) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ t_course │ │t_grade │ │t_student │ │t_major │ │
│ │ 课程表 │ │ 成绩表 │ │ 学生表 │ │ 专业表 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
这个架构看着有点吓人,但别慌,我们一层一层拆开来聊。
三、选课功能——从”点击”到”入库”的全过程
3.1 前端页面:学生看到的选课界面
学生打开选课系统,看到的页面长这样:
<%-- StudentCourseSelect.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>课程选课系统</title>
<style>
body { font-family: "Microsoft YaHei", Arial, sans-serif; }
.course-table { width: 100%; border-collapse: collapse; margin: 20px 0; }
.course-table th { background: #2c3e50; color: white; padding: 12px; }
.course-table td { padding: 10px; border-bottom: 1px solid #ddd; }
.course-table tr:hover { background: #f5f5f5; }
.status-selected { color: #27ae60; font-weight: bold; }
.status-full { color: #e74c3c; }
.btn-select { background: #3498db; color: white; padding: 6px 16px; border: none; border-radius: 4px; cursor: pointer; }
.btn-drop { background: #e74c3c; color: white; padding: 6px 16px; border: none; border-radius: 4px; cursor: pointer; }
.btn-disabled { background: #bdc3c7; cursor: not-allowed; }
</style>
</head>
<body>
<h2>📚 课程选课系统</h2>
<div style="background: #f9f9f9; padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<strong>当前学生:</strong>
<c:out value="${sessionScope.studentName}" />
(学号:<c:out value="${sessionScope.studentId}" />)
<br>
<strong>已选课程数:</strong>
<c:out value="${selectedCourseCount}" /> / <c:out value="${maxCourseCount}" />
<br>
<strong>当前学期:</strong>
<c:out value="${currentSemester}" />
</div>
<!-- 选课表单 -->
<form action="CourseSelectAction" method="post">
<input type="hidden" name="studentId" value="${sessionScope.studentId}" />
<table class="course-table">
<thead>
<tr>
<th>课程代码</th>
<th>课程名称</th>
<th>教师</th>
<th>学分</th>
<th>已选/容量</th>
<th>时间</th>
<th>状态</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<c:forEach var="course" items="${courseList}">
<tr>
<td><c:out value="${course.courseCode}" /></td>
<td><c:out value="${course.courseName}" /></td>
<td><c:out value="${course.teacherName}" /></td>
<td><c:out value="${course.credits}" /></td>
<td>
<c:out value="${course.selectedCount}" /> /
<c:out value="${course.capacity}" />
</td>
<td><c:out value="${course.timeSlot}" /></td>
<td>
<c:choose>
<c:when test="${course.status == 'FULL'}">
<span class="status-full">已满员</span>
</c:when>
<c:when test="${course.status == 'OPEN'}">
<span style="color: #27ae60;">可选</span>
</c:when>
<c:when test="${course.status == 'CLASS_CONFLICT'}">
<span style="color: #e67e22;">时间冲突</span>
</c:when>
</c:choose>
</td>
<td>
<c:choose>
<c:when test="${course.isSelected}">
<button type="submit" name="action" value="drop"
class="btn-drop" onclick="return confirm('确定退选此课程?')">
退选
</button>
</c:when>
<c:when test="${course.status == 'OPEN' && course.creditTotal < maxCourseCount}">
<button type="submit" name="action" value="select"
class="btn-select" onclick="return confirm('确定选此课程?')">
选课
</button>
</c:when>
<c:otherwise>
<button type="button" class="btn-select btn-disabled" disabled>
无法选课
</button>
</c:otherwise>
</c:choose>
</td>
</tr>
</c:forEach>
</tbody>
</table>
<div style="margin-top: 20px;">
<button type="submit" class="btn-select" style="padding: 12px 30px; font-size: 16px;">
提交选课结果
</button>
</div>
</form>
<div style="margin-top: 30px; padding: 15px; background: #fff3cd; border-radius: 8px;">
<strong>📌 选课规则说明:</strong>
<ul>
<li>每人最多选修 <c:out value="${maxCourseCount}" /> 个学分</li>
<li>选课后请及时确认,超时将自动锁定</li>
<li>存在时间冲突的课程无法同时选修</li>
</ul>
</div>
</body>
</html>
这段代码的核心逻辑:
- JSTL标签库(
<c:forEach>、<c:choose>):JSP页面里嵌入了Java逻辑判断,用来遍历课程列表、显示不同状态 - 表单提交:学生点击”选课”或”退选”按钮后,表单会提交到
CourseSelectAction这个Servlet - 隐藏字段:
studentId是隐藏字段,防止学生篡改他人学号 - 确认对话框:
onclick="return confirm(...)"是JavaScript,用来二次确认操作
3.2 业务控制器:Servlet如何处理选课请求
当学生点击”选课”按钮后,请求会到达 CourseSelectAction 这个Servlet:
// CourseSelectAction.java
package com.edu.action;
import com.edu.service.CourseService;
import com.edu.service.StudentService;
import com.edu.bean.Course;
import com.edu.bean.Student;
import com.edu.util.SessionUtil;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.util.List;
/**
* 课程选课处理Servlet
* 负责处理学生的选课、退选请求
*/
@WebServlet("/CourseSelectAction")
public class CourseSelectAction extends HttpServlet {
private CourseService courseService;
private StudentService studentService;
@Override
public void init() throws ServletException {
// 初始化服务层
courseService = new CourseService();
studentService = new StudentService();
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 处理GET请求:显示选课页面
showCourseList(request, response);
}
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 设置请求编码,防止中文乱码
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
// 获取用户学号(从Session中获取,防止篡改)
String studentId = (String) request.getSession().getAttribute("studentId");
if (studentId == null) {
// 未登录,跳转到登录页
response.sendRedirect("login.jsp");
return;
}
// 获取操作类型:select(选课)或 drop(退选)
String action = request.getParameter("action");
String courseId = request.getParameter("courseId");
// 参数校验
if (action == null || courseId == null) {
response.getWriter().println("<script>alert('参数错误!');history.back();</script>");
return;
}
// 根据操作类型分发到不同方法
if ("select".equals(action)) {
selectCourse(request, response, studentId, courseId);
} else if ("drop".equals(action)) {
dropCourse(request, response, studentId, courseId);
} else {
response.getWriter().println("<script>alert('无效操作!');history.back();</script>");
}
}
/**
* 显示课程列表
*/
private void showCourseList(HttpServletRequest request,
HttpServletResponse response)
throws IOException {
String studentId = (String) request.getSession().getAttribute("studentId");
if (studentId == null) {
response.sendRedirect("login.jsp");
return;
}
// 从数据库获取课程列表
List<Course> courseList = courseService.getAllCourses();
// 获取当前学生已选课程
List<String> selectedCourseIds =
courseService.getSelectedCoursesByStudent(studentId);
// 计算当前已选学分
int selectedCredit = courseService.getTotalCreditsByStudent(studentId);
// 将数据放入请求对象,供JSP页面使用
request.setAttribute("courseList", courseList);
request.setAttribute("selectedCourseIds", selectedCourseIds);
request.setAttribute("selectedCourseCount", selectedCredit);
request.setAttribute("maxCourseCount", 28); // 最大选课学分
request.setAttribute("currentSemester", "2023-2024学年第二学期");
// 转发到JSP页面
request.getRequestDispatcher("/StudentCourseSelect.jsp")
.forward(request, response);
}
/**
* 选课操作(核心业务逻辑)
*/
private void selectCourse(HttpServletRequest request,
HttpServletResponse response,
String studentId,
String courseId)
throws IOException {
response.setCharacterEncoding("UTF-8");
try {
// 1. 检查该学生是否已选此课程(防止重复选课)
if (courseService.isCourseSelected(studentId, courseId)) {
sendError(response, "你已经选过这门课了!");
return;
}
// 2. 获取课程信息,检查是否满员
Course course = courseService.getCourseById(courseId);
if (course == null) {
sendError(response, "课程不存在!");
return;
}
if (course.getSelectedCount() >= course.getCapacity()) {
sendError(response, "该课程已满员,无法选课!");
return;
}
// 3. 检查时间冲突(关键!)
if (courseService.hasTimeConflict(studentId, courseId)) {
sendError(response, "所选课程与已选课程时间冲突!");
return;
}
// 4. 检查学分是否超限
int currentCredits = courseService.getTotalCreditsByStudent(studentId);
if (currentCredits + course.getCredits() > 28) {
sendError(response, "选课学分已超过上限(28学分)!");
return;
}
// 5. 执行选课操作(开启事务)
boolean success = courseService.selectCourse(studentId, courseId);
if (success) {
// 更新会话中的已选课程数
int newCount = currentCredits + course.getCredits();
request.getSession().setAttribute("selectedCourseCount", newCount);
sendSuccess(response, "选课成功!当前已选" + newCount + "个学分");
} else {
sendError(response, "选课失败,请稍后重试!");
}
} catch (Exception e) {
e.printStackTrace();
sendError(response, "系统错误:" + e.getMessage());
}
}
/**
* 退选操作
*/
private void dropCourse(HttpServletRequest request,
HttpServletResponse response,
String studentId,
String courseId)
throws IOException {
response.setCharacterEncoding("UTF-8");
try {
// 执行退选操作
boolean success = courseService.dropCourse(studentId, courseId);
if (success) {
// 重新计算已选学分
int newCredits = courseService.getTotalCreditsByStudent(studentId);
request.getSession().setAttribute("selectedCourseCount", newCredits);
sendSuccess(response, "退选成功!当前已选" + newCredits + "个学分");
} else {
sendError(response, "退选失败,请稍后重试!");
}
} catch (Exception e) {
sendError(response, "系统错误:" + e.getMessage());
}
}
/**
* 发送成功消息
*/
private void sendSuccess(HttpServletResponse response, String message)
throws IOException {
response.getWriter().println("<script>alert('" + message + "');</script>");
response.getWriter().println("<script>window.location.href='CourseSelectAction';</script>");
}
/**
* 发送错误消息
*/
private void sendError(HttpServletResponse response, String message)
throws IOException {
response.getWriter().println("<script>alert('" + message + "');</script>");
response.getWriter().println("<script>window.location.href='CourseSelectAction';</script>");
}
}
这段代码的核心逻辑:
- 权限校验:首先从Session中获取学生ID,而不是从表单参数中获取(防止伪造)
- 业务校验顺序:
- 是否已选(防重复)
- 是否满员(容量检查)
- 时间冲突检查(关键!)
- 学分上限检查
- 事务处理:选课操作需要原子性,要么全部成功,要么全部回滚
- Session更新:选课成功后,更新会话中的已选课程数
3.3 业务逻辑层:选课的核心算法
// CourseService.java
package com.edu.service;
import com.edu.dao.CourseDAO;
import com.edu.bean.Course;
import com.edu.bean.CourseSelectRecord;
import com.edu.bean.TimeSlot;
import java.sql.*;
import java.util.*;
/**
* 课程服务层
* 包含选课、退选的核心业务逻辑
*/
public class CourseService {
private CourseDAO courseDAO;
private Connection conn;
public CourseService() {
courseDAO = new CourseDAO();
}
/**
* 获取所有课程列表(含学生选课状态)
*/
public List<Course> getAllCourses() {
return courseDAO.findAll();
}
/**
* 判断学生是否已选某课程
*/
public boolean isCourseSelected(String studentId, String courseId) {
return courseDAO.isSelected(studentId, courseId);
}
/**
* 获取学生已选课程列表
*/
public List<String> getSelectedCoursesByStudent(String studentId) {
return courseDAO.getSelectedCourseIds(studentId);
}
/**
* 计算学生已选总学分
*/
public int getTotalCreditsByStudent(String studentId) {
List<String> selectedCourseIds = getSelectedCoursesByStudent(studentId);
int totalCredits = 0;
for (String courseId : selectedCourseIds) {
Course course = courseDAO.findById(courseId);
if (course != null) {
totalCredits += course.getCredits();
}
}
return totalCredits;
}
/**
* 检查时间冲突(核心算法!)
*
* 时间冲突的逻辑:
* 两门课如果上课时间有重叠,则视为冲突
* 时间格式:每周的某些天 + 某些节次
* 例如:周一1-2节、周三3-4节
*/
public boolean hasTimeConflict(String studentId, String newCourseId) {
// 1. 获取新课程的上课时间
Course newCourse = courseDAO.findById(newCourseId);
if (newCourse == null) {
return false;
}
// 获取新课程的时间段列表
List<TimeSlot> newSlots = parseTimeSlots(newCourse.getTimeSlot());
// 2. 获取学生已选课程的时间段
List<String> selectedCourseIds = getSelectedCoursesByStudent(studentId);
// 3. 逐一检查是否冲突
for (String selectedId : selectedCourseIds) {
Course selectedCourse = courseDAO.findById(selectedId);
if (selectedCourse == null) {
continue;
}
List<TimeSlot> selectedSlots = parseTimeSlots(selectedCourse.getTimeSlot());
// 检查两门课程是否有时间重叠
if (hasOverlap(newSlots, selectedSlots)) {
return true; // 发现冲突
}
}
return false; // 无冲突
}
/**
* 检查两个时间段列表是否有重叠
*/
private boolean hasOverlap(List<TimeSlot> slots1, List<TimeSlot> slots2) {
for (TimeSlot slot1 : slots1) {
for (TimeSlot slot2 : slots2) {
// 同一天且节次有重叠
if (slot1.getDay() == slot2.getDay() &&
slotsOverlap(slot1.getStartPeriod(), slot1.getEndPeriod(),
slot2.getStartPeriod(), slot2.getEndPeriod())) {
return true;
}
}
}
return false;
}
/**
* 检查两个节次范围是否重叠
* 例如:1-2节 和 2-3节 在"2节"处重叠
*/
private boolean slotsOverlap(int start1, int end1, int start2, int end2) {
return start1 <= end2 && start2 <= end1;
}
/**
* 解析时间字符串为时间段列表
* 格式示例:"周一1-2节,周三3-4节;周二5-6节"
*/
private List<TimeSlot> parseTimeSlots(String timeString) {
List<TimeSlot> slots = new ArrayList<>();
if (timeString == null || timeString.trim().isEmpty()) {
return slots;
}
// 按分号分割不同时间段
String[] timeParts = timeString.split(";");
for (String part : timeParts) {
// 解析"周一1-2节"
if (part.contains("周")) {
String[] parts = part.split("周");
String dayStr = parts[0] + "周";
String periodStr = parts[1].replace("节", "");
int day = parseDay(dayStr);
String[] periods = periodStr.split("-");
int startPeriod = Integer.parseInt(periods[0]);
int endPeriod = Integer.parseInt(periods[1]);
slots.add(new TimeSlot(day, startPeriod, endPeriod));
}
}
return slots;
}
/**
* 将星期字符串转换为数字(1=周一, 7=周日)
*/
private int parseDay(String dayStr) {
switch (dayStr) {
case "一": return 1;
case "二": return 2;
case "三": return 3;
case "四": return 4;
case "五": return 5;
case "六": return 6;
case "日": return 7;
default: return 0;
}
}
/**
* 执行选课操作(开启事务)
*/
public boolean selectCourse(String studentId, String courseId) {
Connection conn = null;
try {
conn = getConnection();
conn.setAutoCommit(false); // 开启事务
try {
// 1. 插入选课记录
boolean insertSuccess = courseDAO.insertSelectRecord(conn, studentId, courseId);
// 2. 更新课程已选人数
boolean updateSuccess = courseDAO.incrementSelectedCount(conn, courseId);
if (insertSuccess && updateSuccess) {
conn.commit(); // 提交事务
return true;
} else {
conn.rollback(); // 回滚
return false;
}
} catch (Exception e) {
conn.rollback(); // 发生异常,回滚
throw e;
}
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
try {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 执行退选操作(开启事务)
*/
public boolean dropCourse(String studentId, String courseId) {
Connection conn = null;
try {
conn = getConnection();
conn.setAutoCommit(false);
try {
// 1. 删除选课记录
boolean deleteSuccess = courseDAO.deleteSelectRecord(conn, studentId, courseId);
// 2. 更新课程已选人数
boolean updateSuccess = courseDAO.decrementSelectedCount(conn, courseId);
if (deleteSuccess && updateSuccess) {
conn.commit();
return true;
} else {
conn.rollback();
return false;
}
} catch (Exception e) {
conn.rollback();
throw e;
}
} catch (Exception e) {
e.printStackTrace();
return false;
} finally {
try {
if (conn != null) {
conn.setAutoCommit(true);
conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
/**
* 获取数据库连接
*/
private Connection getConnection() throws SQLException {
// 使用连接池(这里简化为直接获取)
return DriverManager.getConnection(
"jdbc:mysql://localhost:3306/edu_system?useSSL=false&characterEncoding=UTF-8",
"root",
"password"
);
}
}
这段代码的核心逻辑:
- 时间冲突算法:这是选课系统最核心的逻辑,需要正确判断两门课是否有时间重叠
- 事务处理:选课涉及多个数据库操作(插入记录、更新人数),必须保证原子性
- 参数校验:每步操作都做了严格的参数检查
3.4 数据访问层:DAO模式的实现
// CourseDAO.java
package com.edu.dao;
import com.edu.bean.Course;
import com.edu.bean.CourseSelectRecord;
import java.sql.*;
import java.util.*;
/**
* 课程数据访问对象
* 负责与数据库的交互
*/
public class CourseDAO {
private static final String DB_URL =
"jdbc:mysql://localhost:3306/edu_system?useSSL=false&characterEncoding=UTF-8";
private static final String DB_USER = "root";
private static final String DB_PASSWORD = "password";
/**
* 查询所有课程
*/
public List<Course> findAll() {
List<Course> courseList = new ArrayList<>();
String sql = "SELECT c.*, " +
"(SELECT COUNT(*) FROM t_course_select WHERE course_id = c.id) as selected_count " +
"FROM t_course c " +
"WHERE c.status = 'OPEN' " +
"ORDER BY c.course_code";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql);
ResultSet rs = pstmt.executeQuery()) {
while (rs.next()) {
Course course = new Course();
course.setId(rs.getString("id"));
course.setCourseCode(rs.getString("course_code"));
course.setCourseName(rs.getString("course_name"));
course.setTeacherName(rs.getString("teacher_name"));
course.setCredits(rs.getInt("credits"));
course.setCapacity(rs.getInt("capacity"));
course.setSelectedCount(rs.getInt("selected_count"));
course.setTimeSlot(rs.getString("time_slot"));
course.setStatus(rs.getString("status"));
courseList.add(course);
}
} catch (SQLException e) {
e.printStackTrace();
}
return courseList;
}
/**
* 根据ID查询课程
*/
public Course findById(String courseId) {
Course course = null;
String sql = "SELECT * FROM t_course WHERE id = ?";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, courseId);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
course = new Course();
course.setId(rs.getString("id"));
course.setCourseCode(rs.getString("course_code"));
course.setCourseName(rs.getString("course_name"));
course.setTeacherName(rs.getString("teacher_name"));
course.setCredits(rs.getInt("credits"));
course.setCapacity(rs.getInt("capacity"));
course.setSelectedCount(rs.getInt("selected_count"));
course.setTimeSlot(rs.getString("time_slot"));
course.setStatus(rs.getString("status"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return course;
}
/**
* 判断学生是否已选某课程
*/
public boolean isSelected(String studentId, String courseId) {
String sql = "SELECT COUNT(*) FROM t_course_select WHERE student_id = ? AND course_id = ?";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
pstmt.setString(2, courseId);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
return rs.getInt(1) > 0;
}
} catch (SQLException e) {
e.printStackTrace();
}
return false;
}
/**
* 获取学生已选课程ID列表
*/
public List<String> getSelectedCourseIds(String studentId) {
List<String> courseIds = new ArrayList<>();
String sql = "SELECT course_id FROM t_course_select WHERE student_id = ?";
try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
ResultSet rs = pstmt.executeQuery();
while (rs.next()) {
courseIds.add(rs.getString("course_id"));
}
} catch (SQLException e) {
e.printStackTrace();
}
return courseIds;
}
/**
* 插入选课记录(使用事务连接)
*/
public boolean insertSelectRecord(Connection conn, String studentId, String courseId)
throws SQLException {
String sql = "INSERT INTO t_course_select (student_id, course_id, select_time) " +
"VALUES (?, ?, NOW())";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
pstmt.setString(2, courseId);
return pstmt.executeUpdate() > 0;
}
}
/**
* 增加课程已选人数(使用事务连接)
*/
public boolean incrementSelectedCount(Connection conn, String courseId)
throws SQLException {
String sql = "UPDATE t_course SET selected_count = selected_count + 1 WHERE id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, courseId);
return pstmt.executeUpdate() > 0;
}
}
/**
* 删除选课记录(使用事务连接)
*/
public boolean deleteSelectRecord(Connection conn, String studentId, String courseId)
throws SQLException {
String sql = "DELETE FROM t_course_select WHERE student_id = ? AND course_id = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, studentId);
pstmt.setString(2, courseId);
return pstmt.executeUpdate() > 0;
}
}
/**
* 减少课程已选人数(使用事务连接)
*/
public boolean decrementSelectedCount(Connection conn, String courseId)
throws SQLException {
String sql = "UPDATE t_course SET selected_count = selected_count - 1 WHERE id = ? AND selected_count > 0";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, courseId);
return pstmt.executeUpdate() > 0;
}
}
}
四、查成绩功能——从查询到展示
选完课之后,期末考试结束,学生们最关心的就是成绩了。
4.1 成绩查询JSP页面
<%-- StudentGrade.jsp --%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>成绩查询系统</title>
<style>
body { font-family: "Microsoft YaHei", Arial, sans-serif; }
.grade-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 20px;
}
.grade-summary {
display: flex;
justify-content: space-around;
margin-top: 20px;
}
.summary-item {
text-align: center;
}
.summary-value {
font-size: 36px;
font-weight: bold;
}
.summary-label {
font-size: 14px;
opacity: 0.9;
}
.grade-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.grade-table th {
background: #2c3e50;
color: white;
padding: 15px;
text-align: center;
}
.grade-table td {
padding: 12px;
border-bottom: 1px solid #ddd;
text-align: center;
}
.grade-table tr:hover {
background: #f8f9fa;
}
.grade-a { color: #27ae60; font-weight: bold; }
.grade-b { color: #3498db; font-weight: bold; }
.grade-c { color: #f39c12; font-weight: bold; }
.grade-d { color: #e67e22; font-weight: bold; }
.grade-f { color: #e74c3c; font-weight: bold; }
.gpa-bar {
height: 8px;
background: #e0e0e0;
border-radius: 4px;
overflow: hidden;
display: inline-block;
width: 100px;
vertical-align: middle;
}
.gpa-fill {
height: 100%;
background: linear-gradient(90deg, #e74c3c, #f39c12, #27ae60);
}
.semester-tabs {
display: flex;
gap: 10px;
margin-bottom: 20px;
}
.semester-tab {
padding: 10px 20px;
background: #ecf0f1;
border: none;
border-radius: 5px;
cursor: pointer;
}
.semester-tab.active {
background: #3498db;
color: white;
}
</style>
</head>
<body>
<h2>📊 成绩查询系统</h2>
<!-- 成绩汇总卡片 -->
<div class="grade-header">
<h3>学号:<c:out value="${sessionScope.studentId}" /></h3>
<div class="grade-summary">
<div class="summary-item">
<div class="summary-value"><c:out value="${gpa}" /></div>
<div class="summary-label">累计GPA</div>
</div>
<div class="summary-item">
<div class="summary-value"><c:out value="${totalCredits}" /></div>
<div class="summary-label">已修学分</div>
</div>
<div class="summary-item">
<div class="summary-value"><c:out value="${passedCourses}" /></div>
<div class="summary-label">通过课程</div>
</div>
<div class="summary-item">
<div class="summary-value"><c:out value="${failedCourses}" /></div>
<div class="summary-label">重修课程</div>
</div>
</div>
</div>
<!-- 学期切换标签 -->
<div class="semester-tabs">
<c:forEach var="semester" items="${semesterList}"
varStatus="status">
<button class="semester-tab
<c:if test="${status.first || semester == currentSemester}">active</c:if>
" onclick="switchSemester('${semester}')">
<c:out value="${semester}" />
</button>
</c:forEach>
</div>
<!-- 成绩列表 -->
<table class="grade-table">
<thead>
<tr>
<th>课程代码</th>
<th>课程名称</th>
<th>学分</th>
<th>平时成绩</th>
<th>考试成绩</th>
<th>总评成绩</th>
<th>绩点</th>
<th>状态</th>
<th>GPA贡献</th>
</tr>
</thead>
<tbody>
<c:forEach var="grade" items="${gradeList}">
<tr>
<td><c:out value="${grade.courseCode}" /></td>
<td><c:out value="${grade.courseName}" /></td>
<td><c:out value="${grade.credits}" /></td>
<td><c:out value="${grade.dailyScore}" /></td>
<td><c:out value="${grade.examScore}" /></td>
<td>
<span class="grade-<%= getGradeLetter("${grade.finalScore}") %>">
<c:out value="${grade.finalScore}" />
</span>
</td>
<td>
<c:out value="${grade.gpa}" />
</td>
<td>
<c:choose>
<c:when test="${grade.finalScore >= 60}">
<span style="color: #27ae60;">✓ 通过</span>
</c:when>
<c:otherwise>
<span style="color: #e74c3c;">✗ 未通过</span>
</c:otherwise>
</c:choose>
</td>
<td>
<div class="gpa-bar">
<div class="gpa-fill"
style="width: ${(grade.gpa / 4.0) * 100}%"></div>
</div>
<c:out value="${grade.gpaCredit}" />
</td>
</tr>
</c:forEach>
</tbody>
</table>
<div style="margin-top: 30px; padding: 15px; background: #f8f9fa; border-radius: 8px;">
<strong>📋 成绩说明:</strong>
<ul>
<li>总评成绩 = 平时成绩 × 40% + 考试成绩 × 60%</li>
<li>绩点计算:90-100分=4.0,80-89分=3.0,70-79分=2.0,60-69分=1.0,60分以下=0</li>
<li>GPA = 总学分绩点 / 总学分</li>
</ul>
</div>
<script>
function switchSemester(semester) {
// 切换学期查询
window.location.href = 'GradeQueryAction?semester=' + semester;
}
// 根据分数返回成绩等级样式类名
function getGradeLetter(score) {
if (score >= 90) return 'a';
if (score >= 80) return 'b';
if (score >= 70) return 'c';
if (score >= 60) return 'd';
return 'f';
}
</script>
</body>
</html>
4.2 成绩查询的Servlet控制器
// GradeQueryAction.java
package com.edu.action;
import com.edu.service.GradeService;
import com.edu.bean.GradeRecord;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.annotation.*;
import java.io.IOException;
import java.util.List;
/**
* 成绩查询Servlet
*/
@WebServlet("/GradeQueryAction")
public class GradeQueryAction extends HttpServlet {
private GradeService gradeService;
@Override
public void init() throws ServletException {
gradeService = new GradeService();
}
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 获取学生学号(从Session中获取)
String studentId = (String) request.getSession().getAttribute("studentId");
if (studentId == null) {
response.sendRedirect("login.jsp");
return;
}
// 获取查询的学期(默认为最新学期)
String semester = request.getParameter("semester");
if (semester == null || semester.isEmpty()) {
semester = gradeService.getCurrentSemester();
}
try {
// 1. 查询该学期的所有成绩
List<GradeRecord> gradeList =
gradeService.getGradesByStudentAndSemester(studentId, semester);
// 2. 计算汇总信息
double gpa = gradeService.calculateGPA(studentId);
int totalCredits = gradeService.getTotalCredits(studentId);
int passedCourses = gradeService.getPassedCoursesCount(studentId);
int failedCourses = gradeService.getFailedCoursesCount(studentId);
// 3. 获取所有可选学期
List<String> semesterList = gradeService.getAllSemesters();
// 4. 将数据放入请求对象
request.setAttribute("gradeList", gradeList);
request.setAttribute("gpa", String.format("%.2f", gpa));
request.setAttribute("totalCredits", totalCredits);
request.setAttribute("passedCourses", passedCourses);
request.setAttribute("failedCourses", failedCourses);
request.setAttribute("semesterList", semesterList);
request.setAttribute("currentSemester", semester);
// 5. 转发到JSP页面
request.getRequestDispatcher("/StudentGrade.jsp")
.forward(request, response);
} catch (Exception e) {
e.printStackTrace();
response.getWriter().println("查询成绩失败:" + e.getMessage());
}
}
}
4.3 成绩查询的业务逻辑层
// GradeService.java
package com.edu.service;
import com.edu.dao.GradeDAO;
import com.edu.bean.GradeRecord;
import java.sql.*;
import java.util.*;
/**
* 成绩查询服务层
*/
public class GradeService {
private GradeDAO gradeDAO;
public GradeService() {
gradeDAO = new GradeDAO();
}
/**
* 查询学生在某学期的所有成绩
*/
public List<GradeRecord> getGradesByStudentAndSemester(
String studentId, String semester) {
return gradeDAO.findByStudentAndSemester(studentId, semester);
}
/**
* 计算学生累计GPA
* GPA = 总学分绩点 / 总学分
*/
public double calculateGPA(String studentId) {
List<GradeRecord> allGrades = gradeDAO.findAllGrades(studentId);
double totalCreditPoints = 0.0;
int totalCredits = 0;
for (GradeRecord grade : allGrades) {
if (grade.getFinalScore() >= 60) { // 只计算通过的课程
totalCreditPoints += grade.getGpa() * grade.getCredits();
totalCredits += grade.getCredits();
}
}
if (totalCredits == 0) {
return 0.0;
}
return totalCreditPoints / totalCredits;
}
/**
* 获取学生总学分
*/
public int getTotalCredits(String studentId) {
List<GradeRecord> allGrades = gradeDAO.findAllGrades(studentId);
int totalCredits = 0;
for (GradeRecord grade : allGrades) {
if (grade.getFinalScore() >= 60) {
totalCredits += grade.getCredits();
}
}
return totalCredits;
}
/**
* 获取通过课程数量
*/
public int getPassedCoursesCount(String studentId) {
List<GradeRecord> allGrades = gradeDAO.findAllGrades(studentId);
int count = 0;
for (GradeRecord grade : allGrades) {
if (grade.getFinalScore() >= 60) {
count++;
}
}
return count;
}
/**
* 获取未通过课程数量(需重修)
*/
public int getFailedCoursesCount(String studentId) {
List<GradeRecord> allGrades = gradeDAO.findAllGrades(studentId);
int count = 0;
for (GradeRecord grade : allGrades) {
if (grade.getFinalScore() < 60) {
count++;
}
}
return count;
}
/**
* 获取当前学期
*/
public String getCurrentSemester() {
// 这里简化处理,实际应该从数据库或配置中获取
Calendar now = Calendar.getInstance();
int year = now.get(Calendar.YEAR);
int month = now.get(Calendar.MONTH) + 1;
// 判断是上学期还是下学期
if (month >= 9 && month <= 1) { // 秋季学期
return (year - 1) + "-" + year + "学年第一学期";
} else { // 春季学期
return year + "-" + (year + 1) + "学年第二学期";
}
}
/**
* 获取所有学期列表
*/
public List<String> getAllSemesters() {
return gradeDAO.getAllSemesters();
}
}
五、数据库设计——选课系统的基石
一个健壮的选课系统,离不开合理的数据库设计。以下是核心表结构:
-- ============================================
-- 教务系统数据库设计
-- ============================================
-- 1. 学生表
CREATE TABLE t_student (
id VARCHAR(20) PRIMARY KEY COMMENT '学号',
name VARCHAR(50) NOT NULL COMMENT '姓名',
gender VARCHAR(10) COMMENT '性别',
major_id VARCHAR(20) COMMENT '专业ID',
grade_year INT COMMENT '入学年份',
phone VARCHAR(20) COMMENT '联系电话',
email VARCHAR(100) COMMENT '邮箱',
password VARCHAR(100) COMMENT '密码(加密存储)',
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/GRADUATED/DROPPED',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_major (major_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表';
-- 2. 专业表
CREATE TABLE t_major (
id VARCHAR(20) PRIMARY KEY COMMENT '专业ID',
name VARCHAR(100) NOT NULL COMMENT '专业名称',
college_id VARCHAR(20) COMMENT '学院ID',
description TEXT COMMENT '专业描述',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='专业信息表';
-- 3. 课程表
CREATE TABLE t_course (
id VARCHAR(20) PRIMARY KEY COMMENT '课程ID',
course_code VARCHAR(20) NOT NULL UNIQUE COMMENT '课程代码',
course_name VARCHAR(100) NOT NULL COMMENT '课程名称',
teacher_id VARCHAR(20) COMMENT '授课教师ID',
credits INT NOT NULL COMMENT '学分',
capacity INT NOT NULL COMMENT '课程容量',
selected_count INT DEFAULT 0 COMMENT '已选人数',
time_slot VARCHAR(500) COMMENT '上课时间',
classroom VARCHAR(50) COMMENT '教室',
semester VARCHAR(50) COMMENT '开设学期',
status VARCHAR(20) DEFAULT 'OPEN' COMMENT '状态:OPEN/CLOSED/FULL',
description TEXT COMMENT '课程描述',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_course_code (course_code),
INDEX idx_teacher (teacher_id),
INDEX idx_semester (semester),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程信息表';
-- 4. 选课记录表(核心表)
CREATE TABLE t_course_select (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
student_id VARCHAR(20) NOT NULL COMMENT '学生ID',
course_id VARCHAR(20) NOT NULL COMMENT '课程ID',
select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间',
status VARCHAR(20) DEFAULT 'SELECTED' COMMENT '状态:SELECTED/DROPPED',
UNIQUE KEY uk_student_course (student_id, course_id),
INDEX idx_student (student_id),
INDEX idx_course (course_id),
INDEX idx_select_time (select_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生选课记录表';
-- 5. 成绩表
CREATE TABLE t_grade (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
student_id VARCHAR(20) NOT NULL COMMENT '学生ID',
course_id VARCHAR(20) NOT NULL COMMENT '课程ID',
semester VARCHAR(50) NOT NULL COMMENT '学期',
daily_score DECIMAL(5,2) COMMENT '平时成绩',
exam_score DECIMAL(5,2) COMMENT '考试成绩',
final_score DECIMAL(5,2) COMMENT '总评成绩',
gpa DECIMAL(3,2) COMMENT '绩点',
grade_point DECIMAL(5,2) COMMENT '学分绩点',
status VARCHAR(20) DEFAULT 'NORMAL' COMMENT '状态:NORMAL/ABSENT/FAIL',
record_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '成绩录入时间',
modify_time DATETIME ON UPDATE CURRENT_TIMESTAMP COMMENT '最后修改时间',
UNIQUE KEY uk_student_course_semester (student_id, course_id, semester),
INDEX idx_student (student_id),
INDEX idx_course (course_id),
INDEX idx_semester (semester)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生成绩表';
-- 6. 教师表
CREATE TABLE t_teacher (
id VARCHAR(20) PRIMARY KEY COMMENT '教师ID',
name VARCHAR(50) NOT NULL COMMENT '姓名',
gender VARCHAR(10) COMMENT '性别',
title VARCHAR(50) COMMENT '职称',
department VARCHAR(100) COMMENT '所属部门',
phone VARCHAR(20) COMMENT '联系电话',
email VARCHAR(100) COMMENT '邮箱',
password VARCHAR(100) COMMENT '密码',
status VARCHAR(20) DEFAULT 'ACTIVE',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师信息表';
-- 7. 课程容量记录表(用于并发控制)
CREATE TABLE t_course_capacity_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
course_id VARCHAR(20) NOT NULL,
operation VARCHAR(10) COMMENT 'IN/OUT',
operator_id VARCHAR(20) COMMENT '操作人ID',
operator_type VARCHAR(20) COMMENT 'STUDENT/ADMIN',
operate_time DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_course (course_id),
INDEX idx_operate_time (operate_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程容量变更记录表';
-- ============================================
-- 触发器:选课成功后自动更新课程已选人数
-- ============================================
DELIMITER //
CREATE TRIGGER trg_after_select_insert
AFTER INSERT ON t_course_select
FOR EACH ROW
BEGIN
IF NEW.status = 'SELECTED' THEN
UPDATE t_course
SET selected_count = selected_count + 1
WHERE id = NEW.course_id;
END IF;
END//
CREATE TRIGGER trg_after_select_delete
AFTER DELETE ON t_course_select
FOR EACH ROW
BEGIN
IF OLD.status = 'SELECTED' THEN
UPDATE t_course
SET selected_count = GREATEST(selected_count - 1, 0)
WHERE id = OLD.course_id;
END IF;
END//
DELIMITER ;
-- ============================================
-- 存储过程:计算学生GPA
-- ============================================
DELIMITER //
CREATE PROCEDURE sp_calculate_student_gpa(
IN p_student_id VARCHAR(20),
OUT p_gpa DECIMAL(3,2),
OUT p_total_credits INT
)
BEGIN
SELECT
ROUND(SUM(g.gpa * c.credits) / SUM(c.credits), 2) AS gpa,
SUM(c.credits) AS total_credits
INTO p_gpa, p_total_credits
FROM t_grade g
JOIN t_course c ON g.course_id = c.id
WHERE g.student_id = p_student_id
AND g.final_score >= 60;
END//
DELIMITER ;
六、高并发场景下的性能优化
选课系统在开放的那一瞬间,会面临巨大的流量冲击。以我们之前提到的那所高校为例:
峰值场景:28000名学生,每人同时点击选课按钮,QPS达到500+
6.1 缓存层设计
// CourseCache.java - 课程缓存管理
package com.edu.cache;
import com.edu.bean.Course;
import org.springframework.stereotype.Component;
import java.util.concurrent.ConcurrentHashMap;
/**
* 课程缓存管理(简化版,实际项目中使用Redis)
*/
@Component
public class CourseCache {
// 课程缓存
private ConcurrentHashMap<String, Course> courseCache = new ConcurrentHashMap<>();
// 缓存过期时间(毫秒)
private static final long CACHE_EXPIRE_TIME = 5 * 60 * 1000; // 5分钟
// 最后更新时间
private long lastUpdateTime = 0;
/**
* 获取课程信息(带缓存)
*/
public Course getCourse(String courseId) {
Course course = courseCache.get(courseId);
// 如果缓存失效,重新从数据库加载
if (course == null || System.currentTimeMillis() - lastUpdateTime > CACHE_EXPIRE_TIME) {
refreshCache();
course = courseCache.get(courseId);
}
return course;
}
/**
* 刷新整个缓存
*/
public void refreshCache() {
// 实际项目中,这里会调用DAO层获取所有课程
// 并更新缓存
lastUpdateTime = System.currentTimeMillis();
// courseCache.clear();
// List<Course> courses = courseDAO.findAll();
// for (Course course : courses) {
// courseCache.put(course.getId(), course);
// }
}
/**
* 清除单个课程的缓存
*/
public void evictCourse(String courseId) {
courseCache.remove(courseId);
}
}
6.2 分布式锁解决超选问题
// CourseLockService.java - 课程锁服务
package com.edu.service;
import org.springframework.stereotype.Service;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.ReentrantLock;
/**
* 课程锁服务(防止超选)
* 使用分布式锁确保同一课程的并发选课操作串行化
*/
@Service
public class CourseLockService {
// 每门课程一个锁
private ConcurrentHashMap<String, ReentrantLock> courseLocks = new ConcurrentHashMap<>();
/**
* 获取课程的锁
*/
private ReentrantLock getCourseLock(String courseId) {
return courseLocks.computeIfAbsent(courseId, k -> new ReentrantLock());
}
/**
* 执行选课操作(带锁)
*/
public boolean selectCourseWithLock(String studentId, String courseId) {
ReentrantLock lock = getCourseLock(courseId);
boolean locked = false;
try {
// 尝试获取锁(最多等待3秒)
locked = lock.tryLock(3, java.util.concurrent.TimeUnit.SECONDS);
if (locked) {
try {
// 执行选课逻辑
return doSelectCourse(studentId, courseId);
} finally {
lock.unlock();
}
} else {
// 获取锁失败,返回失败
return false;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return false;
}
}
/**
* 执行选课的核心逻辑
*/
private boolean doSelectCourse(String studentId, String courseId) {
// 1. 检查是否已选
if (isSelected(studentId, courseId)) {
return false;
}
// 2. 检查课程容量
int selectedCount = getSelectedCount(courseId);
int capacity = getCapacity(courseId);
if (selectedCount >= capacity) {
return false;
}
// 3. 检查时间冲突
if (hasTimeConflict(studentId, courseId)) {
return false;
}
// 4. 执行选课
return insertSelectRecord(studentId, courseId);
}
// 以下是简化的方法声明,实际项目中需要实现具体逻辑
private boolean isSelected(String studentId, String courseId) { return false; }
private int getSelectedCount(String courseId) { return 0; }
private int getCapacity(String courseId) { return 0; }
private boolean hasTimeConflict(String studentId, String courseId) { return false; }
private boolean insertSelectRecord(String studentId, String courseId) { return false; }
}
6.3 数据库连接池配置
<!-- context.xml - Tomcat数据源配置 -->
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Resource
name="jdbc/EduSystem"
auth="Container"
type="javax.sql.DataSource"
maxTotal="200" <!-- 最大连接数 -->
maxIdle="50" <!-- 最大空闲连接数 -->
minIdle="10" <!-- 最小空闲连接数 -->
maxWaitMillis="10000" <!-- 最大等待时间(毫秒) -->
initialSize="20" <!-- 初始连接数 -->
removeAbandoned="true" <!-- 移除废弃连接 -->
removeAbandonedTimeout="300" <!-- 废弃连接超时时间(秒) -->
logAbandoned="true" <!-- 记录废弃连接 -->
factory="org.apache.commons.dbcp2.BasicDataSourceFactory"
driverClassName="com.mysql.cj.jdbc.Driver"
url="jdbc:mysql://localhost:3306/edu_system?useSSL=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"
username="root"
password="your_password"
/>
</Context>
七、从JSP到现代技术栈的演进
虽然JSP在教育系统中应用广泛,但现在的趋势已经转向了更现代化的技术栈:
| 维度 | JSP传统架构 | 现代架构 |
|---|---|---|
| 前端 | JSP + Servlet | React/Vue + Spring Boot |
| 后端 | Servlet + JavaBean | Spring MVC + Spring Boot |
| 数据库 | JDBC直接操作 | MyBatis/JPA ORM框架 |
| 缓存 | 无或简单Map | Redis分布式缓存 |
| 部署 | WAR包部署到Tomcat | Docker容器化部署 |
| 并发控制 | 应用层锁 | Redis分布式锁 + 数据库乐观锁 |
但JSP的核心思想依然适用:请求-处理-响应的MVC模式,数据库的三层架构设计,这些都是软件工程的通用模式。
八、总结——技术背后的温度
每一次你点击”选课”,背后都有成百上千行的Java代码在默默工作。JSP技术虽然已经不算最新,但它以稳定、安全、成熟的特点,支撑着无数高校的日常运转。
这套系统教会我们的不只是技术,更是:
- 严谨的逻辑:选课涉及到时间冲突、容量限制、学分上限等多重约束,任何一个漏洞都可能导致系统崩溃
- 对用户体验的关注:清晰的界面、及时的反馈、友好的提示,这些细节决定了系统的可用性
- 对数据安全的重视:学号不能从前台获取,密码必须加密存储,成绩数据需要权限控制
下次当你登录选课系统的时候,不妨想想——你指尖的每一次点击,都在和服务器进行一场精密的对话。而这背后,是无数工程师用代码编织出的教育基础设施。
技术不是冷冰冰的代码,它是连接学生、教师、学校之间的桥梁。而JSP,正是这座桥梁上最坚实的基石之一。
