news 2026/9/29 18:28:17

JavaWeb选课系统源码解析:Servlet+JSP+MySQL三层架构与事务实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb选课系统源码解析:Servlet+JSP+MySQL三层架构与事务实战

简介:这是一套基于 Servlet+JSP 实现的学生选课管理系统完整源码包,面向计算机相关专业正在准备毕业设计的学生,以及需要项目实战练习的 Java 学习者。系统覆盖管理员、教师、学生三种角色:管理员维护学生、教师与课程信息,教师可查看课程、查看学生并录入成绩,学生能浏览课程、在线选课并查询成绩。技术栈为 JavaWeb、Servlet、MySQL,前端使用 JSP、CSS、Bootstrap、jQuery,开发环境适用 IDEA/Eclipse 与 Navicat,JDK 1.8 即可运行。压缩包共 112 个文件,大小仅 2.39MB,包含 22 个 Java 源文件、22 个 class 编译文件、21 个 JSP 页面、10 个 jar 依赖库,以及 sql 数据库脚本、css/js 样式交互文件等,结构清晰,导入后可直接部署调试。资源经过严格调试,确保能运行,可作为毕业设计或课程设计的核心参照。目前已有 553 人学习下载,适合需要完整后端逻辑与基础前端页面配合的初学者快速上手。

1. 学生选课管理系统:一份能直接复现的 JavaWeb 完整源码

「学生选课管理系统」在高校 JavaWeb 课程设计清单里几乎年年出现,需求也高度固定:学生登录后能选课、退课、查成绩,管理员登录后能开课、调容量、录成绩。这套完整源码的价值不在于功能花哨,而在于把 Servlet + JSP + MySQL + Tomcat 这条经典链路完整走通。解压、建库、启动 Tomcat,十分钟左右就能在浏览器里看到登录页。它适合两类人:一类是刚开始写 JavaWeb、想找个完整工程对照着理解请求转发和事务提交的新手;另一类是课程设计时间紧、需要先跑通闭环再逐步改界面的同学。跟着这篇把环境搭起来,把核心代码过一遍,再动自己的需求,比自己从零敲省力得多。

2. 先看清这份资源的内部结构:三层架构与四张表

拿到源码压缩包先别急着点 Run。先花十分钟看目录结构,能少走很多弯路。JavaWeb 课程设计源码通常不是 Spring Boot 那种自动化工程,而是手写 Servlet + JSP 的老三样,包结构基本是 controller / service / dao 三层。我一般先扫一眼包名和 web 目录,就能判断它是不是纯 Servlet 实现,以及登录跳转大概是怎么写的。

2.1 从工程目录看请求链路的职责划分

代码包解压后,典型的目录结构长这样:

src/com/course ├── controller # Servlet:接收请求、校验参数、跳转 JSP ├── service # 业务:事务控制、容量判断、成绩录入 ├── dao # 持久化:JDBC 增删改查 ├── entity # User、Course、CourseSelection 实体类 └── util # DBUtil、MD5Util 等工具类 web ├── admin # 管理员页面 JSP ├── student # 学生页面 JSP ├── login.jsp └── WEB-INF/web.xml sql └── course_system.sql

先看 controller 和 jsp 的对应关系。controller 里每个 Servlet 通常对应一个功能页面,比如 LoginServlet 对应 login.jsp,CourseListServlet 对应 student/courseList.jsp。service 层是重头戏,选课防超员、退课回退人数的逻辑都在这层,dao 层只负责把 SQL 发出去。下面是这份源码里几个关键文件的作用:

目录或文件职责改动时注意
controller 包接收请求、解析参数、跳转页面与 JSP 表单的 action 必须一一对应
service 包业务规则、事务提交回滚选课并发和容量判断都在这层
dao 包封装 JDBC 操作表结构变了这里要同步改
web/admin管理员端页面访问控制靠 role 判断
web/student学生端页面同一份代码按角色渲染菜单
sql/course_system.sql建库建表并预置数据启动前必须执行,且要先看一眼

这套结构的好处是:功能点多了以后,新增模块只需照着已有的 Servlet 复制一个,再在 dao 里加对应方法即可。改 JSP 的时候不会误伤业务代码,查问题也知道去哪一层看。

2.2 web.xml 与 URL 映射:注解还是配置

Servlet 3.0 之后,每个 Servlet 可以用 @WebServlet 注解直接声明 URL,不一定非要在 web.xml 里配 servlet-mapping。这几年写的课程设计源码,web.xml 里往往只剩欢迎页和会话配置:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>

这里主要看两点。一是 session-timeout,默认 30 分钟,课程设计演示时如果嫌频繁重新登录,可以调大或改成 60。二是 welcome-file,如果访问根路径直接跳 login.jsp,入口就没有问题。有的老资源还在用 web.xml 里配 servlet-mapping 的方式,那么在 2.1 的 controller 包里是看不到 @WebServlet 注解的,URL 映射集中在 web.xml 的 servlet 和 servlet-mapping 标签里,改 URL 时需要两处同步改。

判断方法很简单:在 IDEA 里打开 controller 任意一个 Servlet,看类名上方有没有 @WebServlet("/xxx")。有,就以注解为准;没有,去 web.xml 里搜同一个类名。很多 404 问题就是注解路径和 JSP 里 form action 路径没对上。

2.3 SQL 建表脚本:四张表怎么关联

数据库脚本是这份资源里最值得先看的东西。学生选课的核心数据模型就是用户表、课程表、选课记录表三张表。常见做法是把学生和管理员都放进 user 表,用 role 字段区分,省去复杂的关联:

CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4; CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(64) NOT NULL, `role` VARCHAR(20) NOT NULL DEFAULT 'student' COMMENT 'admin-管理员 student-学生', `real_name` VARCHAR(50) ) ENGINE=InnoDB; CREATE TABLE `course` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `course_code` VARCHAR(20) NOT NULL, `course_name` VARCHAR(50) NOT NULL, `credit` DECIMAL(2,1) DEFAULT 2.0, `teacher_name` VARCHAR(50), `max_capacity` INT DEFAULT 60, `selected_count` INT DEFAULT 0, `course_time` VARCHAR(100) ) ENGINE=InnoDB; CREATE TABLE `course_selection` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `student_id` INT NOT NULL, `course_id` INT NOT NULL, `select_time` DATETIME, `score` DECIMAL(5,2) DEFAULT NULL, KEY `idx_course` (`course_id`), KEY `idx_student` (`student_id`) ) ENGINE=InnoDB;

user 表负责登录和角色判断,course 表存课程基本信息,course_selection 是中间表,把学生和课程做多对多关联。score 字段放在中间表而不是 course 表,因为成绩描述的是「某个学生对某门课」的结果,不是课程本身的属性。max_capacity 是整门课容量,selected_count 是当前已选人数。

这里必须说明:selected_count 是一个冗余字段。冗余的目的是列表页直接显示「已选人数 / 容量」,不用每次用 COUNT(*) 聚合。代价是每次选课、退课都要同步 UPDATE 这个字段,忘了更新就会出现人数对不上的问题。第 4 章会专门讲这个 UPDATE 的事务写法。另外,脚本结尾的预置数据非常重要。很多号称完整的源码,表结构没问题,但 INSERT 里没插 admin 账号,拿到手登录页面怎么输入都进不去,最后才发现要手动补一条管理员数据。

3. 本地部署把系统跑起来:JDK、Tomcat 与 MySQL 的版本搭配

复现的第一步是版本对齐。这个项目是 Servlet + JSP 的 Web 工程,不是 Spring Boot,不需要 Maven 拉一大串依赖,但 Tomcat 版本必须选对。JavaWeb 项目里版本问题占了一半以上的报错,剩下的才是代码问题。

3.1 版本组合:为什么我推荐 JDK 1.8 + Tomcat 9 + MySQL 8.0

课程设计源码大多是在 JDK 1.8 时代写的,JDK 8 兼容性最好,Tomcat 9 对应 Servlet 4.0,还沿用 javax.servlet 包名。Tomcat 10 起改成了 jakarta.servlet,直接运行老代码会报 NoClassDefFoundError,这个坑第 5 章细讲。MySQL 用 8.0 的话,驱动类要选 com.mysql.cj.jdbc.Driver。

组件推荐版本说明
JDK1.8与老代码的编译级别一致
Tomcat9.0.x支持 javax.servlet,避开包名迁移问题
MySQL8.0 或 5.78.0 需用新驱动类名
JDBC 驱动mysql-connector-java 8.0.x同时兼容 5.7 和 8.0

数据库连接信息通常在 util 包或 src 根目录的 db.properties 里。常见的配置长这样:

driver=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/course_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username=root password=123456

url 里的参数每一个都是经验值。characterEncoding=utf8 保证中文不乱码;serverTimezone=Asia/Shanghai 解决 MySQL 8.0 的时区报错;useSSL=false 关掉 SSL 警告;allowPublicKeyRetrieval=true 配合 MySQL 8.0 的 caching_sha2_password 认证方式,不加它有时会报 Public Key Retrieval is not allowed。如果数据库是 5.7,最后两个参数可以去掉,但留着也不影响。

如果这份源码的 util 包里用的是最朴素的 DriverManager,核心就是一个 DBUtil:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/course_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "123456"; public static Connection getConnection() throws Exception { Class.forName("com.mysql.cj.jdbc.Driver"); return DriverManager.getConnection(URL, USER, PASSWORD); } }

逻辑说明:Class.forName 加载驱动类,DriverManager.getConnection 建立连接。这个类在课程设计里足够用。如果想升级成连接池,把 getConnection 方法体换成 dataSource.getConnection(),dao 层其他代码完全不用改。连接池的 jar 包放到 web/WEB-INF/lib 下,非 Maven 工程手动加 jar 就是这个路径。

3.2 IDEA 导入工程与 Tomcat 启动配置

IDEA 里跑 JavaWeb 项目配置,核心是把源码识别成 Web 工程,再关联到本地 Tomcat。步骤按顺序来:

  1. File → Open,选到解压后的工程目录。如果里面有 .idea 或 .iml,直接打开;没有的话 IDEA 会按普通工程识别。
  2. File → Project Structure → Project,SDK 选 JDK 1.8,语言级别选 8。
  3. Project Structure → Facets,点 + 号,选 Web,把 web 目录指定为 Web 资源目录。
  4. Project Structure → Artifacts,点 + 号,选 Web Application: Exploded,把刚才的 Web Facet 加进去。
  5. Run → Edit Configurations,点 + 号,选 Tomcat Server → Local,指定 Tomcat 目录。
  6. 在 Deployment 页签把这个 Artifact 加进去,Application context 写 /course_system,或按源码里的跳转路径来定。
  7. 启动 Tomcat,浏览器访问 http://localhost:8080/course_system/。

最后一步最容易卡住的是 Application context。如果源码里所有 sendRedirect 都带 request.getContextPath(),那么 context 叫什么都能自适应;如果源码里写死 sendRedirect("/login"),context 就必须是 /,否则跳转全是 404。遇到白屏先看地址栏路径,再查 Servlet 里的重定向写法。

# Linux / macOS 下查看 Tomcat 实时日志 tail -f $CATALINA_HOME/logs/catalina.out # Windows 下直接在 IDEA 的 Run 窗口看输出 # 出现 Starting ProtocolHandler 表示端口和连接器已就绪

日志里搜关键字 Caused by,比从头读堆栈高效。如果报端口占用,说明 8080 被占,改 Tomcat 配置里的 port 或杀掉占用进程。

提示:Tomcat 端口被占用时,优先改 server.xml 的 Connector port,不要盲目杀进程,否则可能误伤其他服务。

3.3 初始化数据库并验证预置数据

数据库初始化有两种方式。工具类图形界面比较简单:打开 Navicat,左上角新建连接,host 填 localhost,端口 3306,用户名 root,密码按本地 MySQL 的设置填,然后右键运行 SQL 文件,选 sql/course_system.sql。命令行方式则是:

mysql -u root -p < sql/course_system.sql

执行完先看两张关键表里有没有数据:

USE course_system; SELECT id, username, password, role FROM user; SELECT COUNT(*) AS total_course FROM course;

user 表至少要有一条管理员数据。如果脚本里没有预置 INSERT,登录就会一直是失败的。此时手动补一条测试数据:

INSERT INTO user(username, password, role, real_name) VALUES ('admin', '123456', 'admin', '系统管理员'), ('stu001', '123456', 'student', '张三');

注意:如果源码的登录逻辑里用了 MD5 工具类,password 字段就不能直接插明文,得先用 MD5Util 加密一次再插入。判断方法还是去登录 Servlet 里看 userService.login 的查询语句,查的是 password = ? 还是 password = MD5(?)。这一步对了,登录页才能真正进去。

4. 核心功能逐段拆解:登录、选课防超选与成绩录入

整个系统的核心逻辑集中在三件事上:登录后按角色分流、选课时防止超出课程容量、退课和成绩录入时保持数据一致。这三段代码看得懂,后续改功能基本没有障碍。

4.1 登录与角色分流:Session 和重定向路径的配合

登录模块是所有页面的入口。用户提交表单后,LoginServlet 接收 username 和 password,调 service 层查询 user 表,返回一个 User 对象:

@WebServlet("/login") public class LoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); UserService userService = new UserService(); User user = userService.login(username, password); if (user == null) { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } HttpSession session = request.getSession(); session.setAttribute("loginUser", user); if ("admin".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/admin/courseList"); } else { response.sendRedirect(request.getContextPath() + "/student/courseList"); } } }

逻辑说明:登录失败用 forward 转发回 login.jsp,而不是重定向。这样 errorMsg 能带到页面显示,并且地址栏不会跳走,用户可以直接重新输入。登录成功用 sendRedirect,浏览器地址变成新的 URL,刷新页面时不会重复提交表单。role 字段决定跳转方向,同时被放进 session,后续每个 JSP 都可以通过 session 里的 loginUser 判断应该渲染哪些菜单。

参数说明:request.getContextPath() 返回当前应用的部署上下文,比如 /course_system。把它拼在重定向路径前面,工程部署到任何 context 下都不会因为路径写死而 404。这是一个值得养成的习惯,包括 JSP 里的静态资源引用,最好都用 ${pageContext.request.contextPath} 拼路径。

4.2 选课防超选:FOR UPDATE 行锁与事务边界

选课是并发场景最集中的地方。两个学生在同一秒提交选课,如果代码只是先查 selected_count 再判断是否小于 max_capacity,两个请求都可能读到同一个旧值,最后课程超员。标准做法是让查询和更新处于同一个事务,并给课程行加锁:

public boolean selectCourse(int studentId, int courseId) { Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 锁住课程行,防止其他事务同时修改 course String lockSql = "SELECT max_capacity, selected_count FROM course WHERE id = ? FOR UPDATE"; ps = conn.prepareStatement(lockSql); ps.setInt(1, courseId); rs = ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int maxCapacity = rs.getInt("max_capacity"); int selectedCount = rs.getInt("selected_count"); if (selectedCount >= maxCapacity) { conn.rollback(); return false; // 课程已满 } // 2. 插入选课记录 String insertSql = "INSERT INTO course_selection(student_id, course_id, select_time) VALUES (?, ?, NOW())"; ps = conn.prepareStatement(insertSql); ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); // 3. 更新已选人数 String updateSql = "UPDATE course SET selected_count = selected_count + 1 WHERE id = ?"; ps = conn.prepareStatement(updateSql); ps.setInt(1, courseId); ps.executeUpdate(); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { // 按 rs、ps、conn 的顺序关闭,连接归还连接池 } }

逻辑说明:SELECT ... FOR UPDATE 是 InnoDB 的行锁语法。第一个事务锁住课程 id 对应的行,第二个事务执行同样的 SELECT 时会被阻塞,直到第一个事务 commit。这样第二个事务读到的一定是更新后的 selected_count,从根上杜绝超选。事务边界从 setAutoCommit(false) 开始,到 commit 或 rollback 结束,中间任何异常都能整体回滚,不会出现选课记录插进去了、人数却没加的情况。

参数说明:setInt(1, studentId) 是 JDBC 的预编译占位参数,第一个 ? 对 studentId,第二个 ? 对 courseId。用 PreparedStatement 而不是拼 SQL 字符串,既能防止 SQL 注入,又让 MySQL 可以复用执行计划。代码里返回 false 的场景,前端 JSP 一般会弹提示或者显示「课程已满」,具体效果取决于资源里前端部分的写法。总容量和已选人数的校验放在 service 层,是为了让这段逻辑能同时被 Servlet 和可能的定时任务调用。

4.3 退课与成绩录入:UPDATE 的顺序和 WHERE 条件

退课是选课的逆操作,但顺序有讲究。常见做法是先删 course_selection 里的记录,再回退 course 表的 selected_count:

public boolean cancelCourse(int studentId, int courseId) { Connection conn = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); // 1. 先删选课记录 String deleteSql = "DELETE FROM course_selection WHERE student_id = ? AND course_id = ?"; PreparedStatement ps = conn.prepareStatement(deleteSql); ps.setInt(1, studentId); ps.setInt(2, courseId); ps.executeUpdate(); // 2. 再回退已选人数 String updateSql = "UPDATE course SET selected_count = selected_count - 1 WHERE id = ?"; ps = conn.prepareStatement(updateSql); ps.setInt(1, courseId); ps.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } }

逻辑说明:course_selection 里的记录是业务事实,selected_count 只是冗余统计字段。先删事实,再调统计值,即使第二步 UPDATE 失败,记录已经删掉了,退课成立,最多是人数多算一个,可以靠脚本校准。反过来如果先减人数、DELETE 失败,就会出现记录还在、人数却少了,学生其实没退成课,数据更难看。

成绩录入是另一个容易出事的地方。核心 SQL 是:

String sql = "UPDATE course_selection SET score = ? WHERE course_id = ? AND student_id = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setBigDecimal(1, score); ps.setInt(2, courseId); ps.setInt(3, studentId); int rows = ps.executeUpdate(); if (rows == 0) { // 提示:该学生没有选这门课,不要静默失败 }

注意 WHERE 条件必须同时带 course_id 和 student_id。如果只写 course_id,会把这门课所有学生的成绩一次性改成同一个分数,这是成绩录入模块最常见的事故。更新行数 rows 一定要判断,等于 0 时说明选课记录不存在,应该提示操作者重新核对,不能假装成功。

5. 复现避坑:四个高频翻车现场与排查步骤

这一章把本地复现时最容易卡住的问题集中列一遍。每一条都是实际运行过的坑,按「现象 → 原因 → 解决」的顺序写,遇到问题可以直接按图索骥。

5.1 中文乱码:从 JSP 到数据库的四个设置点

现象:页面上课程名称显示成问号,或者往数据库插中文时 MySQL 报 Incorrect string value: '\xE8\xAF\xBE...'。

原因:字符集在 JSP 文件、请求接收、JDBC 传输、数据库存储四个环节不一致。最常见的是 JSP 页面是 UTF-8,数据库表却是 latin1,或者 Tomcat 接收 POST 请求时默认用 ISO-8859-1 解析。

解决:四个地方逐一确认。JSP 顶部要写 <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>;web.xml 里加一个编码过滤器;JDBC URL 带上 characterEncoding=utf8;建库时用 utf8mb4。Tomcat 自带的过滤器可以直接复用:

<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</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>

这个过滤器只处理请求编码。响应编码最好在每个 JSP 的 page 指令里声明确认,因为有的 Servlet 直接 write 字符串时不经过 JSP。IDEA 的 Tomcat 控制台如果出现中文乱码,一般不影响功能,但看着难受,可以在 Tomcat 的 catalina.bat 里加 -Dfile.encoding=UTF-8 解决。

5.2 Tomcat 10 导致 NoClassDefFoundError: javax/servlet/xxx

现象:Tomcat 启动后访问页面,报 java.lang.NoClassDefFoundError: javax/servlet/ServletException,或者 ClassNotFoundException: javax.servlet.Filter。

原因:Tomcat 10 开始把 Servlet API 从 javax.servlet 迁移到 jakarta.servlet。老项目 import javax.servlet.*,Tomcat 10 的类库已经不带这些包了。

解决:最省事的方案是换回 Tomcat 9,在 IDEA 的 Run Configuration 里把 Tomcat Server 换成 9.0.x,代码一行不用改。如果机器上只有 Tomcat 10,也可以用 IDEA 的 Edit → Find → Replace in Files,把 javax.servlet 全局替换成 jakarta.servlet,同时把 web.xml 的命名空间换成 jakarta 版本:

<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd" version="5.0">

替换的时候注意范围,只替换 import 和 web.xml 头部,不要动业务包名。替换完重新编译,如果工程里有第三方库内置了 javax.servlet 的类,还可能冲突,所以我的建议是:课程设计阶段直接用 Tomcat 9,不值得在这上面浪费时间。

5.3 MySQL 8 连不上:驱动类、时区、公钥检索三个参数

现象:启动后页面报 Communications link failure,或者 Public Key Retrieval is not allowed,也可能直接 Access denied for user 'root'@'localhost'。

原因:MySQL 8.0 默认用 caching_sha2_password 认证,老版本的 5.1.x 驱动不认识;同时新版驱动要求 URL 里明确时区,不写 serverTimezone 就可能报错;allowPublicKeyRetrieval 不开启时,第一条查询会被拒绝。

解决:先把 3.1 节 db.properties 里的完整 URL 复制过去,驱动类用 com.mysql.cj.jdbc.Driver。如果这样还报 Access denied,多半是密码本身不对,要么是 root 密码没改成配置里的值,要么是 MySQL 用户用了别的 host。作为兼容手段,也可以把 root 的认证方式临时改回 mysql_native_password:

ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;

这条命令在开发环境有效,生产环境不建议这样改。MySQL 8 的 caching_sha2_password 其实更安全,只是因为老驱动不支持才需要降级。排查时优先看完整堆栈的第一行 Caused by,不要只看末尾的 MySQLConnection 字样。

5.4 删除课程失败与已选人数对不上

现象:管理员删除一门课时 MySQL 报 Cannot delete or update a parent row: a foreign key constraint fails;或者选课后列表里人数不增加、退课后人数不减少。

原因:如果 course_selection 表建了外键指向 course 表,course 表有子记录时直接删主记录必然被外键拦住。人数对不上,则是 selected_count 冗余字段没有和选课记录同步更新,或者同步更新不在同一个事务里。

解决:删课之前先删选课记录,再删课程。如果建表脚本里没写外键,业务上也要按这个顺序,否则会留下孤儿记录。人数对不上时,先别急着改代码,用一条子查询校准:

UPDATE course c SET c.selected_count = ( SELECT COUNT(*) FROM course_selection s WHERE s.course_id = c.id );

这条 SQL 以 course_selection 的真实记录数为准,一次性修正所有课程的已选人数。跑完之后再看页面,如果人数还是不对,就是缓存或者页面没刷新。业务代码里彻底解决,要回到第 4 章的事务写法,让选课、退课和人数更新要么一起成功,要么一起回滚。

6. 把管理系统当骨架复用:最小改动加一个功能

课程设计最后大多要求「至少有一个自己扩展的功能」,最常见的加分项是修改密码。这个功能不动表结构、不引入新依赖、不改已有流程,是最小改动路径的典型代表。

6.1 最小改动路径:修改密码功能怎么加

改动只涉及三处:新建一个 Servlet、加一个 JSP 表单、在页面导航里加一个入口。Servlet 的核心逻辑是校验旧密码、更新新密码:

@WebServlet("/student/updatePassword") public class UpdatePasswordServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { User loginUser = (User) request.getSession().getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } String oldPwd = request.getParameter("oldPassword"); String newPwd = request.getParameter("newPassword"); // WHERE 里带旧密码,更新成功说明旧密码正确 String sql = "UPDATE user SET password = ? WHERE id = ? AND password = ?"; // 执行后如果更新行数为 0,提示旧密码错误,不直接修改 } }

逻辑说明:UPDATE 语句把旧密码校验直接写进 WHERE,更新行数为 1 说明旧密码正确且已改密,为 0 说明旧密码不对或用户不存在,不需要先 SELECT 再 UPDATE。密码字段如果源码里用的是 MD5,新密码在 executeUpdate 之前要先走一遍同一个工具类,否则登录时对不上。session 里的 loginUser 不包含密码,改密后无需销毁 session,保持登录状态即可。

参数说明:newPwd 一定要加长度校验和字符校验,至少不能为空、不能短于 6 位。明文密码在任何课程设计源码里都是减分项,如果原工程登录已经是 MD5,扩展功能时保持同样的处理方式,不要破坏一致性。

6.2 自己验证一遍:手工回归的固定套路

改完功能别只点一下新页面就交差。JavaWeb 工程页面跳转多,任何一个 context 路径改错都会白屏,所以我每轮改动后都固定走一遍完整链路:

步骤操作预期结果
1admin 登录,新增一门课程course 表多一条记录,selected_count 为 0
2学生登录,选择这门课course_selection 多一条记录,selected_count 变为 1
3学生执行退课选课记录删除,selected_count 回到 0
4admin 给学生录入成绩score 字段有值,学生端能查到

每步都要同时看页面和数据库两处。页面跳转对了但数据没变,问题在 Servlet 或 dao;数据变了但页面没显示,问题在 JSP 渲染或重定向路径。确认这四步都通,再继续加新功能,可以省掉大量返工时间。

有一回我为了赶进度,直接改了一个 JSP 里的跳转地址,没同步改 Servlet 的重定向路径,结果页面白屏了一下午,最后发现只是 context 拼写问题。从那以后,我每次动这类 JavaWeb 工程,都强制把数据库先备份一份,再按登录、选课、退课、录成绩的顺序手动走一遍,确认页面和数据两边完全一致才敢继续。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 18:27:49

企业IM客服系统源码落地:ThinkPHP5+FastAdmin+Swoole实战指南

简介&#xff1a;这份资源是一套基于ThinkPHP5、FastAdmin与Swoole构建的企业IM客服系统PHP源码&#xff0c;面向需要独立部署即时通讯与在线客服能力的中小企业、开发者及运维人员&#xff0c;帮助解决多站点统一客服、会员与游客实时沟通等需求。压缩包共约2000个文件&#x…

作者头像 李华
网站建设 2026/9/29 18:26:38

大模型学习路线V1.0:从环境搭建到微调部署的实战指南

1. 大模型学习路线全景拆解先说个实在话&#xff1a;现在网上的“大模型学习路线”动不动就是一张几十个节点的知识图谱&#xff0c;从Transformer原理一路画到分布式训练框架&#xff0c;看着什么都全了&#xff0c;实际上没几个人能照着走完。我自己的经验是&#xff0c;大模…

作者头像 李华
网站建设 2026/9/29 18:26:32

Prompt模板管理与Agent编排实战:从零散提示词到工程化

提示词写多了之后&#xff0c;你会发现单条 prompt 写得再漂亮&#xff0c;一旦涉及多个场景、多个智能体协同&#xff0c;立刻就会失控。我自己的项目从十几个零散提示词膨胀到上百个之后&#xff0c;第一次真切感受到什么叫“提示词也需要管理”。这篇是系列第七篇&#xff0…

作者头像 李华
网站建设 2026/9/29 18:25:56

AgentScope多智能体实战课深度评测:从Demo到企业级落地

最近有个朋友跑来问我&#xff0c;说他准备买一门叫做"AgentScope 企业级多智能体实战课"的课程&#xff0c;问我值不值得。我正好系统性地把市面上跟 AgentScope 相关的东西过了一遍&#xff0c;也深度体验过这类实战课的内容设计&#xff0c;所以干脆把对这门课的评…

作者头像 李华
网站建设 2026/9/29 18:25:56

UE5 UMG与Slate底层原理:从UI失效到性能优化的全链路解析

1. 项目概述&#xff1a;这不是又一个UI框架教程&#xff0c;而是UE5里“画布”如何真正被你握在手里的实操笔记 如果你最近在UE5项目里拖了一个Button&#xff0c;改了十次颜色却始终不生效&#xff1b;或者写了个UMG Widget&#xff0c;打包后发现所有绑定的文本全变成问号&a…

作者头像 李华
网站建设 2026/9/29 18:25:13

理解部署本质:从脚本到K8s的决策逻辑与工程实践

1. 这不是“部署”教程&#xff0c;而是你第一次真正理解部署本质的现场复盘很多人把“部署”当成一个终点——写完代码&#xff0c;跑个python main.py&#xff0c;再扔到服务器上nohup python app.py &&#xff0c;就宣告胜利。我见过太多团队在上线前夜&#xff0c;因为…

作者头像 李华