ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JavaWeb学生选课管理系统:表结构、事务与防超选实现

JavaWeb学生选课管理系统:表结构、事务与防超选实现 简介一份基于Servletjsp实现的学生选课管理系统完整源码面向计算机相关专业正在准备毕业设计的学生以及需要JavaWeb项目实战练习的初学者。系统涵盖管理员、教师、学生三类角色实现学生信息管理、课程信息维护、教师成绩录入、学生选课与成绩查询等核心功能后端采用Servlet与MySQL前端使用JSP、CSS、Bootstrap与jQuery可在IDEA或Eclipse中配合Navicat和JDK1.8直接运行部署。压缩包共112个文件包含22个Java源文件、22个class编译文件、21个JSP页面、相关JAR依赖库及SQL数据库脚本等整体大小仅2.39MB结构清晰便于导入和二次开发。该源码项目经过调试可直接作为毕业设计使用也适合在学习中对照理解JavaWeb分层开发流程。目前已获得553人学习浏览是一个值得参考的完整实战案例。1. 学生选课管理系统一套能直接跑起来的 JavaWeb 课设样板期末前两周课设题目里十个有四个是学生选课管理系统。这个标题描述的交付物就是把这个经典业务做成了完整工程JSP Servlet JDBC MySQL 的 javaWeb 项目附带建库脚本和初始数据导入 IDEA 配好 Tomcat 就能跑通从登录到选课到成绩查询的整条链路。它对三类人有直接价值准备交课程设计的学生想快速看懂一个真实 JavaWeb 业务怎么分层的初学者以及需要一套带数据库脚本的演示工程去改造成自己系统的开发者。注意这不是 Spring Boot 微服务而是传统 JavaWeb 项目先确认技术栈再决定要不要用。2. 先定数据再写代码五张核心表的结构与建库脚本选课管理系统的业务本质是“用户、课程、选课关系”三件事。源码里代码可以换写法但表结构基本跑不出这个范围。我习惯先讲表设计因为字段定错后面改代码的代价远大于改一句 SQL。2.1 三种角色与权限边界最常见的角色划分是管理员、教师、学生。课程设计里我见过两种建模方式一种是给每个角色单独建表登录时分别查三张表另一种是共用一张 user 表用 role 字段区分再通过关联字段挂到各自的明细表。后一种更常见也更好扩展因为登录入口只有一个Session 里存的对象也统一。sys_user 表只负责账号、密码、角色学生姓名、班级、学号这些信息放到 student 表教师职称、系别放到 teacher 表。这里的关联字段叫 user_ref_id它指向 student.id 或 teacher.id。管理员没有对应明细user_ref_id 置 0 即可。这个设计的好处是以后要加一个“教务员”角色只需要在 sys_user 里新增一个 role 枚举值再决定要不要新建一张明细表不需要动登录逻辑。权限边界的控制也依赖 role 字段。管理员能访问用户管理和课程管理页面教师能开课和录成绩学生只能选课、退课、查成绩。页面展示和 Filter 拦截都以这个字段为准后面第 4 章会给出具体代码。如果角色设计得再简单一些比如只要学生和管理员那张 sys_user 表就可以少一个枚举值但表结构不用改。2.2 五张核心表的字段设计表结构建议如下字段名可以直接对应到项目里的实体类属性。表名用途关键字段sys_user登录账号与角色id、username、password、role、user_ref_idstudent学生基本信息id、user_id、student_no、name、class_name、majorteacher教师基本信息id、user_id、teacher_no、name、dept、titlecourse课程信息与容量id、course_no、course_name、credit、teacher_id、capacity、selected_count、time_placeelective选课与成绩记录id、course_id、student_id、status、score、create_time这里有一个容易忽略的点course 表里的 capacity 和 selected_count 是容量控制的两个关键字段。selected_count 不是纯冗余字段它的作用是让选课操作可以用一条 UPDATE 语句同时完成“容量判断 人数递增”避免先 SELECT 再 INSERT 带来的并发超选问题。elective 表里的 score 字段为什么提前留出来是因为成绩录入本来就和选课记录绑定在一起教师录成绩时直接按 course_id 更新这一列即可不需要额外建一张成绩表。字段类型上student_no、teacher_no、course_no 这类编号字段全部用 VARCHAR不要用 INT。原因是编号可能带前导零或字母用 INT 会静默丢失格式。credit 用 DECIMAL(3,1) 而不是 FLOAT因为浮点类型在计算总学分时容易出现 2.99999 这种精度问题课程设计阶段看不出差别答辩时一算总分会很尴尬。2.3 建库脚本DDL 与初始化数据把下面的 SQL 存成 init.sql在 Navicat 或命令行里执行即可。需要注意 utf8mb4 编码和唯一索引这两处是最容易在运行阶段踩坑的地方。CREATE DATABASE IF NOT EXISTS course_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE course_system; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role ENUM(ADMIN, STUDENT, TEACHER) NOT NULL, user_ref_id INT NOT NULL DEFAULT 0, INDEX idx_role (role) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, class_name VARCHAR(50), major VARCHAR(50), INDEX idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, teacher_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, dept VARCHAR(50), title VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, teacher_id INT NOT NULL, capacity INT NOT NULL DEFAULT 60, selected_count INT NOT NULL DEFAULT 0, time_place VARCHAR(200), INDEX idx_teacher (teacher_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE elective ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, student_id INT NOT NULL, status TINYINT DEFAULT 1, score DECIMAL(5,1) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_course_student (course_id, student_id), INDEX idx_student (student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段 DDL 里有几个参数值得说明。password 字段用 VARCHAR(64)是因为 md5 哈希后是 32 位预留长度是为了将来换 SHA-256 或加盐后不需要改表。role 用 ENUM 而不是 VARCHAR能在数据库层限制非法角色程序里传一个不在枚举里的值会直接报错这比在 Java 代码里写 if 判断更早暴露问题。ENGINE 全部指定 InnoDB是因为选课和人数递增需要行级锁和事务回滚MyISAM 不支持这些换成 MyISAM 后第 4 章的防超选写法就失效了。elective 表的联合唯一索引 uk_course_student 是防重复选课的最后一道防线。即使 Controller 层和 Service 层都漏了查重数据库也会拒绝插入第二条 course_id student_id 相同的记录。底层会报 Duplicate entry 错误业务层再把这个异常转成“你已经选过这门课程”的提示。再插一段初始化数据密码统一用 md5(123456) 生成学生、教师、课程三张表各给一条INSERT INTO sys_user (username, password, role, user_ref_id) VALUES (admin, MD5(123456), ADMIN, 0), (stu001, MD5(123456), STUDENT, 1), (tea001, MD5(123456), TEACHER, 1); INSERT INTO student (user_id, student_no, name, class_name, major) VALUES (2, 20240001, 张明, 计算机2401班, 软件工程); INSERT INTO teacher (user_id, teacher_no, name, dept, title) VALUES (3, T1001, 李华, 计算机学院, 副教授); INSERT INTO course (course_no, course_name, credit, teacher_id, capacity, selected_count, time_place) VALUES (C001, JavaWeb程序设计, 3.0, 1, 2, 0, 周一 3-4节 教学楼A201);注意课程容量故意写成 2而不是真实的 60。这么做的目的是让演示时能快速触发“课程已满”的分支不 demo 到第三个人就已经能看到完整逻辑。很多课设源码默认容量 60演示到最后也没人把课选满容量判断的代码就像黑匣子一样没被验证过。初始化数据的细节还有一层含义admin 的 user_ref_id 是 0表示管理员不关联任何业务表而 stu001 和 tea001 通过 user_id 关联到各自详情表的主键sys_user.id 与 student.id 并不相等查询时要用关联字段不要直接用 userId 去当作 studentId 用。3. 用 IDEA 把项目跑起来导入、Tomcat 配置与数据源连接拿到源码之后的第一件事不是读代码而是让它先跑起来。很多人在这一步就被卡住因为 IDEA 里 JavaWeb 项目的运行配置比 Spring Boot 复杂要装 Tomcat、要配置 Artifact、要建数据源。按下面的顺序做十分钟内能启动。3.1 导入工程与 JDK 和 Tomcat 配置多数课设源码是 Maven 工程目录下有 pom.xmlIDEA 直接用 Open 选择该目录等 Maven 导入完成即可。如果不是 Maven 结构而是 WebContent 或 webapp 目录就按普通 JavaWeb 项目打开手动把 jar 包放进 WEB-INF/lib。Maven 工程里最关键的依赖如下dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies这里有两个参数要解释。servlet-api 的 scope 设为 provided意思是编译时需要、运行时交给 Tomcat 提供避免和 Tomcat 自带的 servlet 实现冲突。mysql-connector-java 用 8.0.28对应的驱动类名是 com.mysql.cj.jdbc.Driver如果你本机是 MySQL 5.7用这个驱动也能连但连接串里必须带 serverTimezone否则会报时区异常。导入完成后配置 Tomcat。IDEA 右上角下拉框选 Edit Configurations点加号找 Tomcat Server Local。Name 随意选本地 Tomcat 安装目录JRE 保持默认。切到 Deployment 标签页把当前项目的 war exploded 加进去Application context 我习惯直接改成 /这样访问时不带项目名路径问题会少很多。war exploded 和 war 的区别在于前者是解压目录支持 JSP 修改后直接刷新不用重启 Tomcat后者是打包后的压缩包适合部署到服务器。本地开发选 war exploded 就够了。3.2 数据库连接池配置Druid 的参数含义项目里如果用的 Druid通常有一个 db.properties 或 druid.properties 放在 src/main/resources 下。核心内容如下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/course_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 minIdle5 maxWait60000 validationQuerySELECT 1几个参数的调整逻辑initialSize 是连接池启动时创建的连接数开发环境 5 就够改成 0 也行只是第一次请求会慢一点。maxActive 是连接池能同时持有的最大连接数课程设计这个量级 20 绰绰有余不要盲目调大每个连接都会占用 MySQL 的服务端内存。maxWait 是拿不到连接时的等待毫秒数60000 表示 60 秒超时。如果经常报连接等待超时优先检查是不是代码里拿到 Connection 没关闭而不是先加大 maxWait用连接池时连接关闭写错位置内存被拖垮只是时间问题。选择 Druid 而不是裸 JDBC是因为它自带连接复用和监控db.properties 不用跟着每个 DAO 方法重复加载。早期课设源码里经常看到每个 DAO 方法里 Class.forName 一次那在并发表面上没问题但每次连接都重新认证性能很差。数据库连接池这类基础组件属于“平时看不见一旦访问量上来就翻车”直接用成熟方案省心。url 里的 characterEncodingutf8 必须和数据库字符集一致。前面建库用的是 utf8mb4MySQL 驱动能识别 utf8 这个别名。serverTimezone 是 MySQL 8 驱动强制要求的参数不写的报错信息非常长最后一行才是 the server time zone看到这里别慌加上 Asia/Shanghai 就能过。3.3 启动与验证在 IDEA 里点击运行后Tomcat 日志出现一行类似 Server startup in 的信息说明容器起来了。浏览器访问 http://localhost:8080/正常情况下会跳到登录页 login.jsp。用第 2 章插入的 admin / 123456 登录能进管理员页面说明 JDBC 连接、Session、页面转发这三层都通了。如果看到 404 或 500先别急着看业务代码。404 多半是 Application context 没改成 /或者页面访问路径写死500 大概率是数据库连接失败或 JSP 编译错误。看 IDEA 的 console 标签页把堆栈第一行读出来80% 的启动问题都能在这里定位。比如出现了 Access denied for user说明 db.properties 账号密码不对别去改代码出现了 ClassNotFoundException才需要回到 Maven 依赖和 Artifact 配置上。提示项目运行起来后先看一眼登录成功后跳转的 URL 里有没有项目名。如果地址栏是 http://localhost:8080/course_system_war_exploded/说明 Deployment 里没改 context后续每个链接都要带一长串前缀早点统一成 / 再继续。4. 核心功能代码怎么读登录鉴权、事务选课与防超选项目跑起来以后要做的是把登录、选课、退课这几条主流程对应的代码找出来理解它的分层和几个关键写法。这既是为了应付答辩提问也是你把它改成自己系统的前提。代码无非是数据库增删改查加页面跳转但增删改查之间的顺序和边界决定了数据对不对。4.1 登录与鉴权从 LoginServlet 到 Filter 拦截登录逻辑一般由 LoginServlet 接收表单提交的 username 和 password查库校验通过后把用户对象放进 Session。一个典型的实现如下WebServlet(/login) public class LoginServlet extends HttpServlet { private SysUserDao userDao new SysUserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password MD5Utils.md5(req.getParameter(password)); SysUser user userDao.findByUsernameAndPassword(username, password); if (user null) { req.setAttribute(error, 账号或密码错误); req.getRequestDispatcher(login.jsp).forward(req, resp); return; } req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /index.jsp); } }这段代码里值得注意的点有三个。第一request.setCharacterEncoding(UTF-8) 必须放在读取参数之前否则 POST 提交的中文会乱码。第二密码在 DAO 层做比对但加密动作发生在 Servlet 里也就是说数据库里存的永远是哈希值。这个约定要全项目统一注册、初始化数据、登录三处都必须走 MD5Utils任何一处漏了都会出现“数据库能看到密码但程序登不进去”的怪现象。第三重定向用 req.getContextPath() 拼路径而不是硬编码 /项目名/index.jsp这样改应用上下文时不会牵连所有跳转。外观上这种写法比直接在 JSP 里写数据库查询要清晰但还有个明显缺点md5 并没有加盐撞库成本很低。课程设计阶段用它是常规操作因为代码短、好解释如果你的题目要求更高把 MD5Utils 替换成 BCrypt 即可原理一样只是哈希值更长。光登录还不行受保护页面需要拦截。常见的做法是一个 Filter 检查 Session 里的用户和角色WebFilter(/admin/*) public class AdminFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); SysUser user session null ? null : (SysUser) session.getAttribute(loginUser); if (user null || !ADMIN.equals(user.getRole())) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(req, response); } }这个拦截器解决的是越权问题。注意 getSession(false) 的写法当前没有 Session 就返回 null而不是主动创建一个新 Session能避免未登录用户每次访问都生成无意义的 Session 对象。角色判断放在 Filter 里可以实现即使有人直接输入 admin/index.jsp 的地址也会被拦回登录页。学生和教师模块的拦截逻辑同理只是判断条件换成对应的 role 值。4.2 选课业务在事务里完成三条 SQL 的联动选课不是一条 INSERT 那么简单。一次完整的选课要检查课程是否存在、是否已满、学生是否已选过然后插入选课记录再把 course 表的 selected_count 加一。这三步必须在一个事务里否则会出现“课表里有一条选课记录但课程已选人数没变”的数据不一致。这也是数据库事务最典型的应用场景。核心代码大致如下public void selectCourse(Connection conn, int studentId, int courseId) throws SQLException { try { conn.setAutoCommit(false); CourseDao courseDao new CourseDao(); Course course courseDao.findById(conn, courseId); if (course null) { throw new BizException(课程不存在); } ElectiveDao electiveDao new ElectiveDao(); if (electiveDao.exists(conn, courseId, studentId)) { throw new BizException(不能重复选课); } electiveDao.insert(conn, courseId, studentId); int rows courseDao.increaseSelectedCount(conn, courseId); if (rows 0) { throw new BizException(课程已满); } conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }这一段把事务的关键动作写全了。setAutoCommit(false) 开启手动提交commit 和 rollback 分别处理成功和失败两个分支。increaseSelectedCount 这个方法是容量判断的核心下一节单独说。还有一层细节Service 层和 DAO 层的 Connection 要传参共享而不是每个方法内部单独拿连接。如果 DAO 里自己用 Druid 工具类拿连接那么这里的事务控制全部失效。课程设计里出现“选科成功但人数不变”这种问题十有八九是连接没传下去各层各用各的。退课就是反向操作删除 elective 记录再把 selected_count 减一。同样要放在一个事务里并且删除时也要带 course_id 和 student_id 两个条件防止误删别人的记录。退课的容量判断简单一些因为不会发生超退但事务仍然需要。4.3 用一条 UPDATE 挡住超选防超选最稳的写法不是“先 SELECT 课程容量再判断是否小于已选人数然后 UPDATE”而是把判断条件写进 UPDATE 的 WHERE 子句public int increaseSelectedCount(Connection conn, int courseId) throws SQLException { String sql UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, courseId); return ps.executeUpdate(); } }这段 SQL 让数据库在更新时原子地完成“检查容量 递增人数”两个动作。如果返回影响行数为 0说明此刻课程已满直接回滚如果返回 1说明这次选课合法。即使两个学生同时在最后一秒提交选课请求MySQL 的行锁和 WHERE 条件也能保证最多一个人选上不会出现先查后写导致的超卖。常见的错误写法是先查询再更新select capacity, selected_count from course where id ? if (selected_count capacity) { insert into elective ... update course set selected_count selected_count 1 where id ? }这种写法在单用户测试时完全正常但并发请求一来就出问题。原因在于查询和更新之间有时间差两个请求可以先后读到同一个已选人数然后都判断可以选最终超出容量。所以记住一个原则容量判断尽量让数据库的原子 UPDATE 来做。selectCourse 方法里的查重也是同理数据库层的联合唯一索引兜底代码层的 exists 只是为了让提示更友好。5. 移植到自己的项目连接池参数调整与四个高频避坑点源码能跑通只算入门。往自己的课设题目上迁移比如把“学生选课”改成“实验室预约”或“教材订购”表结构要改、页面要换、参数也要调。这一章讲迁移时一定要动的参数以及几个我见的比较多的翻车现场。5.1 迁移时必调的三个参数位置第一个是 db.properties 里的连接串。把 course_system 换成你自己建的库名username 和 password 换成自己本机 MySQL 的账号。url 里的 characterEncoding 和 serverTimezone 保留去掉必踩中文乱码坑。第二个是初始化脚本里的初始数据。管理员账号、测试学生、示范课程都要按新业务改一遍否则演示时页面上还是“张明选 JavaWeb”这种旧数据显得很假。第三个是 Tomcat 的 Application context。部署名如果带了项目路径所有 sendRedirect 和 JSP 里的相对链接都可能 404最简单的办法是把 context 改成 /。参数参考值如下按课程设计的并发量级完全够用参数推荐值说明initialSize5启动即建立的连接数minIdle5最小空闲连接数maxActive20最大活跃连接数maxWait60000获取连接最大等待毫秒数validationQuerySELECT 1校验连接是否有效注意这些参数没有唯一正确答案。如果你本机内存只有 4GmaxActive 降到 10 更稳妥如果你用的是 c3p0 而不是 Druid配置项名称会变成 jdbc 开头的那一套例如 jdbc.maxPoolSize。核心思路是连接数宁少勿多等待超时宁可报错也别让请求无限挂起。5.2 避坑一中文全部变成问号现象页面能登录能选课但学生姓名、课程名在页面上显示为 ????写入数据库的也是乱码。 原因三层编码不统一。JSP 页面可能是 ISO-8859-1Servlet 没设置请求编码数据库连接串也没指定 utf8。 解决三层各写一处。JSP 顶部加 % page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %Servlet 的 doPost 第一行加 req.setCharacterEncoding(UTF-8)数据库连接串保留 characterEncodingutf8。如果这三层都改了还是乱码把表 DROP 后用 utf8mb4 重建因为表创建时的字符集可能在迁移过程中被覆盖成了 latin1。5.3 避坑二启动报端口被占用现象IDEA 运行项目console 里出现 Address already in use: JVM_Bind或者 Tomcat 启动失败。 原因8080 端口被其他程序占用或者上一个 Tomcat 进程没有完全退出。 解决Windows 下命令行执行 netstat -ano | findstr 8080找出占用进程的 PID到任务管理器结束它也可以改 Tomcat 的 conf/server.xml把 8080 改成 8083。我一般优先改端口因为杀进程容易误伤。改完端口后访问地址跟着变记得让演示机上也一致。5.4 避坑三ClassNotFoundException: com.mysql.jdbc.Driver现象页面加载或启动时报找不到 MySQL JDBC 驱动类。 原因mysql-connector-java 8.x 的合法驱动类名是 com.mysql.cj.jdbc.Driver旧代码或旧笔记里写的是 com.mysql.jdbc.Driver还有一种情况是 Maven 依赖在编译期存在但运行时没有打进部署目录。 解决先确认依赖版本。8.x 就把 driverClassName 改成 com.mysql.cj.jdbc.Driver5.x 保持旧类名。然后打开 Project Structure - Artifacts展开 WEB-INF/lib确认 mysql 驱动 jar 在里面。IDEA 里 Maven 依赖如果没勾选源码编译能过运行时却找不到类这个坑很隐蔽排查顺序放在最前面。5.5 避坑四选课记录重复但页面没提示现象同一个学生反复点击选课按钮数据库里出现两条相同 course_id student_id 的记录或者直接报 Duplicate entry 异常。 原因页面按钮没做防重复提交Service 层也没有查重逻辑兜底。 解决elective 表的联合唯一索引是底线已经加了就不会真正出现重复数据。再在 Service 层捕获重复键异常转成友好提示try { electiveDao.insert(conn, courseId, studentId); } catch (SQLIntegrityConstraintViolationException e) { throw new BizException(你已经选过这门课程); }注意这个 catch 要在事务回滚之后抛业务异常否则提示虽然友好但事务把其他 SQL 也回滚了状态会变得不确定。如果源码里没有统一异常处理至少保证这个业务异常能被外层 catch 到并回滚。6. 用一条验收 SQL 加三个冒烟用例证明项目可用课设答辩或交付演示时对方问得最多的往往不是代码细节而是“你怎么证明系统是对的”。建议准备两个工具一条数据一致性 SQL和三组手工冒烟用例。先给数据库加一条校验 SQL专门查 selected_count 和选课明细是否一致SELECT c.course_name, c.capacity, c.selected_count AS 记录人数, (SELECT COUNT(*) FROM elective e WHERE e.course_id c.id) AS 实际选课数, CASE WHEN c.selected_count (SELECT COUNT(*) FROM elective e WHERE e.course_id c.id) THEN 一致 ELSE 不一致 END AS 校验结果 FROM course c;跑出全部行的校验结果都是“一致”时说明选课事务没有留下连带副作用。这条 SQL 会暴露一个非常隐蔽的问题如果曾经手动在 elective 表插过测试数据而没有同步更新 course.selected_count业务代码里看着一切正常但数据层已经不自洽了。用这条 SQL 能在演示前发现这类隐患。冒烟用例按角色分三组用 stu001 登录选一门容量为 2 的课程确认选课数加一再选一次同一门课确认被拦截退课后再选确认能恢复。用 tea001 登录给选课学生录成绩确认学生端能查到。用 admin 登录新增一门课确认容量字段不能为空。三组都过了核心链路就算验证完成。我自己的教训是不要在演示当天临时改容量参数。有一次为了演示顺畅我把 capacity 改成了 100结果忘记了 selected_count 里已经残留旧数据页面显示人数和明细对不上临时排查了几分钟才定位到是初始化数据没同步。后来养成了一个习惯每次移植后都先把 course 表重置容量写小跑一遍验收 SQL 再上台。这个习惯帮我省了不少时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表