JavaWeb学生选课系统毕设全攻略:从JDBC连接池到事务并发 简介一套基于JavaWeb实现的学生选课系统完整项目包专为计算机相关专业正在准备毕业设计的学生及需要Java项目实战练习的开发者打造解决了从选课、考勤到成绩管理等高校常见业务需求。系统采用Servlet、JSP、JDBC、DbUtils作为后台框架UI层使用EasyUI、jQuery与Ajax数据库采用MySQL整体包含学生、教师、系统管理员三类用户角色并针对各角色分别设计了学生信息、班级信息、教师信息、课程信息、选课信息、考勤信息、请假信息、成绩信息以及系统管理等模块功能覆盖全面且界面简洁易用。资源包共13个文件压缩包大小7.67MB内含可直接运行的完整项目源码zip包、MySQL数据库脚本sql、项目文档pdf与md、运行截图png以及软件工具下载地址txt可清晰对照部署环境与二次开发。到目前为止已有6131人学习/下载项目经严格调试可直接运行附带的文档与脚本可帮助快速搭建系统是完成毕设或积累项目经验的可靠参考。1. 学生选课系统为什么是JavaWeb毕设的“安全牌”技术栈、工作量与答辩要点做JavaWeb方向的本科毕设学生选课系统是出现频率最高、也最稳妥的选题之一它业务边界清晰涉及登录鉴权、数据建模、事务处理、页面交互这些JavaWeb课程的核心知识点踩坑可控工作量适中一个人在三到六周内完全能做完。更重要的是答辩时老师一定会追问“你为什么要这样设计”选课系统天然具备能自圆其说的业务逻辑——选课冲突、余量控制、课程与学生的多对多关系每一个都能展开讲。这篇笔记按我自己做毕设带项目的路径来写从ER图设计、JDBC连接池配置到IDEA里把源码跑通再到答辩演示的技巧读者按顺序走完就能复现一版能上台演示的项目。2. 从ER图到建表选课系统的核心表设计与JDBC连接池参数2.1 学生、课程、选课记录三张表的关系设计与外键取舍选课系统的数据模型不复杂核心就是学生和课程之间的多对多关系。常见做法是三张表student学生、course课程、select_course选课记录表。我见过有不少人只建两张表在课程表里存一个选课学生ID的逗号分隔字段当时能跑通答辩时被老师追问数据一致性就答不上来。正确做法是把关联关系单独落一张表这样才方便统计选课人数、实现余量控制。student表不需要设计得太复杂主键用学号还是自增ID要看场景我更推荐业务学号做主键因为教务系统里学号本身唯一且长期不变。course表要带课程容量、已选人数、授课教师、上课时间这些选课业务必需的字段其中课程容量和已选人数这两个字段是后续余量判断的基础。select_course表保存学生ID、课程ID、选课时间并给(student_id, course_id)加唯一索引这一步能直接从数据库层面挡住一个学生重复选同一门课。CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, password VARCHAR(64) NOT NULL COMMENT MD5加盐后的密码, name VARCHAR(32) NOT NULL, major VARCHAR(64) COMMENT 专业, class_name VARCHAR(32) COMMENT 班级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(64) NOT NULL COMMENT 课程名称, teacher VARCHAR(32) NOT NULL COMMENT 授课教师, capacity INT NOT NULL COMMENT 课程容量, selected_count INT DEFAULT 0 COMMENT 已选人数, schedule VARCHAR(64) COMMENT 上课时间/地点, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE select_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生ID, course_id BIGINT NOT NULL COMMENT 课程ID, select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段DDL有两个值得注意的细节。第一字符集用了utf8mb4而不是utf8BMP之外的生僻字和特殊符号在utf8mb4下不会变成问号清华大学的“”这类字也能正常存。第二selected_count字段在course表里冗余存储是为了避免每次选课都COUNT(*)扫选课表但冗余字段带来的风险是可能和选课记录表不一致事务里必须同步更新它。2.2 用Druid连接池连接MySQL最小配置文件与关键参数JavaWeb项目里直接DriverManager.getConnection()拿连接的做法只适合课设Demo一旦选课高峰有几十个并发请求频繁创建和销毁连接会直接拖垮MySQL。常见做法是引入连接池组件Druid是使用最方便的一个而且它的监控页面在答辩时能现场展示并发情况是很好的加分点。用Druid只需要一个配置文件加一个工具类。# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/course_select?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue usernameroot passwordyour_password initialSize5 maxActive20 minIdle5 maxWait5000 validationQuerySELECT 1 testWhileIdletrue timeBetweenEvictionRunsMillis60000url里必带的三组参数是characterEncodingutf8、serverTimezoneAsia/Shanghai和useSSLfalse。不带时区参数MySQL 8.0 加上JDBC驱动8.x会直接启动失败报错里会有Server time zone关键字不带SSL关闭参数控制台会刷出很长的SSL握手日志虽然不影响运行但看着很糟心。maxWait5000是拿连接的最大等待时间这个参数默认值太大选课时并发上来等待超时的线程会堆积调成5000毫秒可以让失败快速暴露。public class DruidUtil { private static DruidDataSource dataSource; static { try { Properties props new Properties(); props.load(DruidUtil.class.getClassLoader().getResourceAsStream(druid.properties)); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(DruidDataSource init failed: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static DruidDataSource getDataSource() { return dataSource; } }这段工具类用静态代码块在类加载时初始化连接池getConnection()返回的是池化的连接而不是新建连接。配置文件里的initialSize5是启动时预创建的物理连接数maxActive20是最大连接数这两个值按课程设计项目的并发量设置就够了不要贪大连接池开太大会占MySQL的连接数上限。2.3 为什么选ServletJSP而不是纯Spring Boot毕设答辩的自证逻辑这几年很多人的毕设直接上手Spring Boot甚至有一条命令生成前后端分离项目的脚手架思路。但做学生选课系统这个题目我的建议是本科阶段使用Servlet JSP JDBC这套经典JavaWeb技术栈来打底。答辩时老师问“框架解决了什么问题”用Servlet能讲清楚HTTP请求从进入Tomcat到Servlet再到DAO的完整链路而用Spring Boot只能讲“自动配置帮我把Tomcat内嵌了”两者在答辩深度上的差异是很明显的。技术选型上还有个折中方案主体用Servlet JSP再把Druid连接池、BeanUtils、JSTL这些工具库引进来。这样既避开了Spring全家桶的复杂配置又能体现你对基础设施组件的掌握。整个JavaWeb项目完整案例的经典结构就是src/main/javaController、Service、DAO、src/main/webappJSP页面、静态资源加src/main/resources配置文件三块这个结构在IDEA里对应度最高也符合大多数任课老师对“JavaWeb项目”的预期。3. 核心业务怎么落地登录鉴权、选课事务与冲突检测3.1 登录与SessionMD5加盐和验证码的取舍登录模块是选课系统的门面也是答辩时老师最容易上手操作的功能。最简单的实现是把明文密码存数据库登录时直接比对但这样在答辩场上会被问“密码泄露怎么办”。我建议至少做两层密码不存明文用MD5加盐登录成功后把用户信息放进Session后续请求都校验Session。加盐的常见做法是固定业务盐加用户名组合后再做MD5。固定盐不要写死在代码里放在配置文件config.properties中用saltcourse_select_2024这类形式配置。前端页面把用户输入的密码原样传到ServletServlet里再做加盐哈希这样数据库泄露时攻击者拿到的也不是可直接登录的原始密码。public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 检查固定验证码简单场景或集成Kaptcha String code req.getParameter(captcha); HttpSession session req.getSession(); String sessionCode (String) session.getAttribute(captcha); if (sessionCode null || !sessionCode.equalsIgnoreCase(code)) { req.setAttribute(error, 验证码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } UserService userService new UserService(); AppUser user userService.login(username, password); if (user null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } session.setAttribute(loginUser, user); session.setAttribute(studentId, user.getId()); resp.sendRedirect(req.getContextPath() /course/list); } }这里sendRedirect用的是req.getContextPath()拼接的路径原因是项目部署时上下文根不一定叫course_select。如果写死/course_select/course/list换了一台机器部署时路径一变就会404用getContextPath()动态获取可以避免这个坑。验证码有两种选择自己写一个CaptchaServlet生成图片或者引入Kaptcha第三方库后者代码量更少但多一个依赖自己能写出简易版验证码在答辩时也是加分项。3.2 选课的事务边界数据库锁解决超选与重复选课选课的并发冲突是这个系统最核心的难点。两个学生同时选同一门余量只剩1的课如果代码里没有锁两个请求都读到selected_count49容量50各自加1写回数据库里就变成了已选51人超员了。解决方式从三个层面做数据库唯一索引挡住重复选课SELECT ... FOR UPDATE锁住课程行挡住并发修改事务保证选课记录和人数增减要么同时成功要么同时失败。public boolean selectCourse(Long studentId, Long courseId) { String lockSql SELECT id, capacity, selected_count FROM course WHERE id ? FOR UPDATE; String insertSql INSERT INTO select_course(student_id, course_id) VALUES(?, ?); String updateSql UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity; Connection conn DruidUtil.getConnection(); try { conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); PreparedStatement lockPs conn.prepareStatement(lockSql); lockPs.setLong(1, courseId); ResultSet rs lockPs.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int capacity rs.getInt(capacity); int selectedCount rs.getInt(selected_count); if (selectedCount capacity) { conn.rollback(); return false; } PreparedStatement insertPs conn.prepareStatement(insertSql); insertPs.setLong(1, studentId); insertPs.setLong(2, courseId); int inserted insertPs.executeUpdate(); PreparedStatement updatePs conn.prepareStatement(updateSql); updatePs.setLong(1, courseId); int updated updatePs.executeUpdate(); if (inserted 1 updated 1) { conn.commit(); return true; } conn.rollback(); return false; } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ignored) {} return false; } finally { try { conn.setAutoCommit(true); } catch (SQLException ignored) {} try { conn.close(); } catch (SQLException ignored) {} } }这里我采用手动事务而不是依赖框架是因为JDBC的setAutoCommit(false)能最清晰地展示事务边界答辩时这段代码可以反复讲。锁的选择上FOR UPDATE锁的是课程表的行后续所有针对同一门课的事务都要等锁释放这保证了余量判断和人数更新之间没有被插入的窗口。但要注意锁必须在事务内使用代码里commit或rollback之后锁才释放事务里任何一步失败都要rollback否则锁会一直持有。3.3 退课、课表展示与分页查询JSPJSTL的常见写法退课的逻辑是选课的逆向操作先删选课记录再把课程表的selected_count减一这两步同样要放在事务里。退课的核心坑是谁可以退——是学生本人只能退自己的课代码里删除条件必须带student_id否则就可能发生越权操作。课表展示的页面形态是一个二维表格横向是星期一到星期五纵向是节次。实现的朴素思路是把选课记录查出来后在JSP里循环判断上课时间拼接表格单元格但这样JSP里会塞大量Java代码。我习惯在Servlet里预先组装一个MapString, ListCoursekey是周一第1节这样的字符串JSP里用JSTL的c:forEach遍历生成表格。分页查询选课时用LIMIT ?, ?配合PreparedStatement设参数pageNum从请求参数取pageSize固定10或15。div classcourse-table table border1 cellspacing0 cellpadding6 tr th节次/th th周一/th th周二/th th周三/th th周四/th th周五/th /tr c:forEach items${scheduleMap} varentry varStatusidx tr td第${idx.index 1}节/td c:forEach items${entry.value} varcourse td${course.courseName}br${course.teacher}/td /c:forEach /tr /c:forEach /table /divvarStatusidx的index从0开始显示节次时要加1。这里有个细节${entry.value}如果某个时间段没课会输出空白需要在Servlet里保证每个星期几的key都存在否则c:forEach遍历不到对应的列表格会出现错位。这个在真正的项目中很容易遇到属于典型的“看着对、跑起来乱”的问题。4. 把源码跑起来IDEA配置Tomcat与MySQL的全流程4.1 环境选型JDK、Tomcat、MySQL版本怎么配才不打架学生选课系统这种经典项目强烈建议使用JDK 8、Tomcat 8.5或9.0、MySQL 5.7或8.0的组合。这个组合是网上绝大多数JavaWeb项目完整案例的默认环境教程覆盖率高遇到问题搜得到答案。JDK 8因为javax.servlet命名空间和Tomcat 8.5的兼容性都很成熟不要在这个项目上尝试JDK 21加Tomcat 11的组合新版本把javax.servlet改成了jakarta.servlet旧源码直接编译失败。MySQL版本上5.7和8.0都行但驱动类名不同。MySQL 5.7配com.mysql.jdbc.DriverMySQL 8.0配com.mysql.cj.jdbc.Driver。用错了驱动类启动时会报ClassNotFoundException。我自己习惯直接用8.0因为5.7在很多机器上已经不再预装但配置文件里千万不要漏了serverTimezone参数。Tomcat的下载和安装不做展开但要提醒一点Tomcat目录不能放在有空格的路径下比如D:\Program Files\apache-tomcat-8.5.98在调用catalina.bat时可能因为路径分割出问题。放在D:\dev\tomcat8这类纯英文无空格路径最省心。4.2 IDEA配置JavaWeb项目运行环境Artifact、Deployment与路径映射IDEA里运行JavaWeb项目核心是配置Tomcat的Deployment。新建Web项目时选择Java Enterprise或Web Application模板IDEA会自动生成web/WEB-INF目录结构。配置Tomcat的入口在Run - Edit Configurations - Tomcat Server - Local要点如下第一Tomcat Server页面选择本地Tomcat路径JRE选择已安装的JDK 8。第二切到Deployment选项卡点击选择Artifact项目的war exploded格式。第三Application context填/course_select这是浏览器访问时的根路径与前面代码里的getContextPath()对应。!-- project/web/WEB-INF/web.xml -- ?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 display-namecourse_select/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list resource-ref descriptionDruid DataSource/description res-ref-namejdbc/courseSelect/res-ref-name res-typejavax.sql.DataSource/res-type res-authContainer/res-auth /resource-ref /web-appwar exploded是开发模式IDEA会把编译后的类和JSP页面直接映射到Tomcat的webapps下改代码后点击Update即可热更新不需要每次重启Tomcat。resource-ref是JNDI数据源的声明方式如果你选择在context.xml里配置JNDI数据源而不是用Druid工具类这里才是入口。4.3 初始化脚本与数据从SQL文件到第一条选课记录拿到一个JavaWeb源码项目第一件事是找sql或db目录下的初始化脚本。一般项目都附带course_select.sql里面包含建库、建表、插入管理员和学生初始数据。执行SQL的常见方式有两种Navicat或DataGrip里直接运行SQL文件或者MySQL命令行source。mysql -u root -p -e CREATE DATABASE IF NOT EXISTS course_select DEFAULT CHARACTER SET utf8mb4; mysql -u root -p course_select course_select.sql-- course_select.sql 关键初始化数据 INSERT INTO student (student_no, password, name, major, class_name) VALUES (2024010101, e10adc3949ba59abbe56e057f20f883e, 张伟, 软件工程, 软工2401), (2024010102, e10adc3949ba59abbe56e057f20f883e, 李娜, 软件工程, 软工2401); INSERT INTO course (course_no, course_name, teacher, capacity, selected_count, schedule, credit) VALUES (CS101, JavaWeb程序设计, 王强, 50, 0, 周一第1-2节, 3.0), (CS102, 数据库原理, 刘敏, 45, 0, 周二第3-4节, 3.5), (CS103, 数据结构, 陈磊, 40, 0, 周三第1-2节, 4.0);e10adc3949ba59abbe56e057f20f883e是明文密码123456的MD5值插入初始化数据时直接用预计算的哈希不要在SQL里调MD5()函数因为MySQL的函数在不同版本行为有差异。执行完SQL后启动Tomcat浏览器访问http://localhost:8080/course_select/应该能看到登录页。此时先用2024010101登录走一遍选课流程确认能正常进入课程列表页并完成选课环境就算跑通了。5. 选课系统避坑5个让毕设翻车的细节与排查方法5.1 中文乱码请求、响应、数据库三层编码不一致现象是登录后页面上的课程名称显示成问号或者提交中文姓名后数据库里存的是乱码。原因是三层编码没对齐浏览器发出请求时用的编码、Tomcat解析请求时用的编码、MySQL存储时用的字符集任意一层不一致就会乱码。最常见的是JSP页面没有声明pageEncodingUTF-8以及Servlet里没有调用req.setCharacterEncoding(UTF-8)。解决方法是三处同时设置JSP头部写% page contentTypetext/html;charsetUTF-8 languagejava %Servlet的doPost方法第一行加req.setCharacterEncoding(UTF-8)MySQL表字符集统一utf8mb4。如果还乱检查druid.properties里的url是否带了characterEncodingutf8。这个坑几乎每个做JavaWeb项目的人都会遇到一次踩坑记录里永远排第一。5.2 数据库连接池启动失败时区、驱动版本与allowPublicKeyRetrieval现象是Tomcat启动时控制台报Cannot create PoolableConnectionFactory后跟Could not create connection to database server。原因是连接池在初始化时检测不到MySQL服务或参数不对。三个高频触发点MySQL 8.0 的url没写serverTimezone驱动版本和数据库版本不匹配比如5.7的数据库用了8.x驱动MySQL 8.0 默认使用caching_sha2_password认证旧驱动无法通过认证。解决方法是url中补上serverTimezoneAsia/Shanghai把驱动升级到mysql-connector-java-8.0.x以上并在url末尾追加allowPublicKeyRetrievaltrue。如果用的是MySQL 5.7驱动保持在5.1.x即可不要混用。5.3 Session失效与重复提交选课按钮连点的坑现象是学生端点击选课按钮第一次成功再点一次提示重复选课但有时候提示不弹页面卡死。原因是两个问题叠加一是选课提交后没有做重定向跳转用户按F5刷新导致请求重复提交二是select_course表没加唯一索引时连点两下会插入两条相同记录。解决方法是选课成功后在Servlet里执行resp.sendRedirect(req.getContextPath() /course/list)提交方式改为POST后重定向这是PRG模式同时在select_course表加UNIQUE KEY uk_student_course (student_id, course_id)。5.4 Tomcat部署路径导致的404Application context与getContextPath不一致现象是IDEA里启动Tomcat后浏览器访问http://localhost:8080/course_select/login.jsp显示404但直接访问根路径却能打开访问Tomcat首页。原因是IDEA的Application context配置的和代码里实际写的路径不一致。代码里写死了/course_select但IDEA的Deployment标签里改成了/course或者sendRedirect跳转时用了绝对路径而没有带上下文根。解决方法是启动时看IDEA控制台输出的日志找到Deploying web application archive ...后面的路径名再把浏览器访问地址和代码里的getContextPath()对齐。5.5 外键约束引发的删除失败级联删除与选课记录清理现象是管理员想在后台删除一个学生报错Cannot delete or update a parent row: a foreign key constraint fails。原因是select_course表中外键fk_sc_student引用了student.id直接删除学生时母表中的行被选课记录引用数据库拒绝执行。解决方法是先清选课记录再删学生或者建表时指定ON DELETE CASCADE。对选课系统来说建议用第一种显式删除的方式因为业务上删除学生前应该保留操作日志。管理员删除学生的业务代码里要先执行DELETE FROM select_course WHERE student_id ?再执行DELETE FROM student WHERE id ?两步放同一事务里。6. 答辩加分项做一个可以演示的自选课监控与导出功能6.1 用Ajax在不刷新页面的情况下检查课程余量答辩现场最多的情况是评委老师想实际点一下选课按钮看效果。如果选课后页面刷新互动性会比较弱。我建议在选课按钮上挂一个Ajax请求点击后先提示“正在锁定余量”再用异步方式调用一个独立的检查接口返回剩余名额。实现上是写一个CheckStockServlet查course.selected_count配合前端JavaScript的fetch调用。6.2 把选课结果导出ExcelPOI的一段可用代码背包里可以预置一个导出课表的功能答辩开场先演示这个能让评委觉得项目完整度超出预期。导出用Apache POI的XSSFWorkbook代码量不大但要注意导出的文件流需要设置响应头Content-Disposition否则浏览器可能直接打开乱码。public class ExportServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { Long studentId (Long) req.getSession().getAttribute(studentId); ListCourse courses new CourseService().listSelectedCourses(studentId); XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(我的课表); Row header sheet.createRow(0); header.createCell(0).setCellValue(课程编号); header.createCell(1).setCellValue(课程名称); header.createCell(2).setCellValue(教师); for (int i 0; i courses.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(courses.get(i).getCourseNo()); row.createCell(1).setCellValue(courses.get(i).getCourseName()); row.createCell(2).setCellValue(courses.get(i).getTeacher()); } resp.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); resp.setHeader(Content-Disposition, attachment;filenamemy_courses.xlsx); workbook.write(resp.getOutputStream()); workbook.close(); } }这里用了XSSFWorkbook而不是HSSFWorkbook前者生成xlsx对课程数据量没有上限压力。注意这个Servlet的doGet没有做登录校验实际项目里一定要在doGet开头检查Session里有没有loginUser否则任何人直接访问导出接口就能拿到所有课表。6.3 给答辩的演示脚本从清空数据到恢复数据的顺序答辩前花半小时准备一份演示脚本至少能保证流程不翻车。我自己的习惯是准备两个SQL文件reset_demo.sql清空选课记录并把course.selected_count归零seed_demo.sql插入演示用的学生和课程数据。演示时按照“空白状态 → 选课 → 查看课表 → 导出Excel → 退课”的顺序走每个环节控制在三十秒以内。走完这五步评委对项目的业务完整度基本有一个确定的认识。这个项目做到这里其实已经比大部分停留在“能跑”层面的毕设往前多走了一步有事务锁处理并发、有唯一索引防重复、有导出功能负责交付。我自己在做项目时吃过亏的地方就是一开始总想着加功能结果核心选课流程的边界没处理好反而被追问时卡了壳。写完这篇希望你能少走这一步弯路把基础链路做扎实再来谈加分项。希望帮到你。本文还有配套的精品资源点击获取