ARTICLE DETAIL

资讯详情

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

高校选课系统实战:Java+MySQL生产级架构设计

高校选课系统实战:Java+MySQL生产级架构设计 简介本资源是一个基于Java与MySQL开发的高校学生选课管理系统Web项目面向计算机专业本科生、Java初学者及教育信息化实践者解决高校教务管理中学生信息维护、课程编排、多角色选课调度与成绩录入等核心业务场景。项目采用B/S架构完整实现管理员、教师、学生三类用户权限隔离与功能闭环涵盖学生档案CRUD、课程表动态配置、带规则如学分限制、先修要求的选退课流程及教师端成绩批量处理模块。压缩包共82个文件含22个HTML页面、9个CSS样式文件、5个JS交互脚本、4个XML配置文件及6个Python辅助脚本可能用于数据初始化或工具支持另有PNG/SVG图标资源与说明文档docx/md/txt整体仅1.75MB轻量易部署。目前已有34人下载学习提供可直接运行的前后端代码结构、清晰的模块划分templates/views/models/static、数据库建表SQL及详细README与说明文件适合用于课程设计、毕设参考或SpringMySQL技术栈的实战演练。1. 项目概述一个真实跑在教室机房里的选课系统长什么样“基于Java和MySQL开发的高校学生选课管理系统”——光看这个标题你可能以为又是一个课程设计作业的Demo页面三张表、五个Servlet、一个index.jsp跳转完事。但我在某省属高校信息中心驻场支持三年亲手部署过7套同类型系统其中3套至今仍在用——它们不是PPT里的架构图而是每天早上八点准时被几百名学生刷新、抢课、卡顿、重试的真实生产环境。这个系统真正的价值不在于它用了Spring Boot还是SSM而在于它必须扛住“第一节大课前15分钟”的并发洪峰教务处刚放开《大学物理实验》的200个名额3秒内涌入1800请求MySQL连接池瞬间打满Tomcat线程数飙到98%而学生端只看到“选课失败请稍后再试”的灰色弹窗。它解决的从来不是“能不能做”而是“能不能稳”。核心关键词——Java、MySQL、Web系统、学生信息管理、选课——每一个都直指教育信息化落地中最硬的骨头Java决定业务逻辑的健壮性与可维护性MySQL承载着全校数万师生数据的原子性与一致性Web系统是师生唯一交互入口学生信息管理是所有流程的起点与校验基线选课则是整个系统压力测试的终极考场。适合谁不是刚学完JDBC的学生练手项目而是需要交付给二级学院教务员日常使用的系统不是追求炫酷前端的毕业设计而是能导出Excel报表、对接教务处统一身份认证、支持寒暑假批量导入导出的生产级工具。它要的不是“能跑”而是“敢用”。2. 整体架构设计与技术选型逻辑为什么不用Spring Cloud也不用Redis2.1 三层分层结构为什么坚持“瘦前端厚后端”很多新手一上来就想套VueElement PlusSpring Cloud微服务结果连登录页都跑不起来。我见过最典型的翻车案例某学院让大四学生用Vue3Spring BootMyBatis-Plus重构旧系统花了三个月做出响应式界面结果上线第一天选课高峰时Nginx直接502查日志发现是后端服务因数据库锁表超时自动熔断——前端再漂亮数据链路断了就是零。我们最终采用的是经典的“JSP/Thymeleaf Servlet JDBC或MyBatis MySQL”三层结构原因非常实际表现层View用Thymeleaf而非纯JSP。Thymeleaf模板在HTML里写逻辑浏览器能直接预览样式教务员改个按钮文字不用重启服务器更重要的是它天然支持HTML原型UI设计师给的PSD切图前端直接套模板后端专注写Controller协作效率提升40%以上。我们甚至保留了部分JSP用于老系统兼容比如成绩录入页的Excel导入功能JSP的Apache POI集成比Thymeleaf更成熟。控制层Controller严格遵循“一个URL对应一个Servlet类”拒绝过度抽象。比如/student/selectCourse就对应SelectCourseServlet里面只做三件事参数校验学号格式、课程ID存在性、调用Service层方法、转发到结果页。不搞泛型Controller不写RESTful风格的PostMapping(/api/v1/course)——教务处的老师不会看Swagger文档他们只认“点这个按钮弹出成功框”。数据访问层DAO放弃Hibernate坚持MyBatis XML映射。理由很朴素当教务员说“我要查2023级计算机专业所有挂科超过2门的学生并按挂科数倒序只显示前50条”你写HQL可能要调试半小时而MyBatis里一条select标签SQL直写SELECT s.* FROM student s JOIN score sc ON s.idsc.student_id WHERE s.major计算机 AND sc.grade 60 GROUP BY s.id HAVING COUNT(*) 2 ORDER BY COUNT(*) DESC LIMIT 50执行计划一眼可见DBA优化也方便。我们甚至把常用查询封装成sql片段比如include refidstudentBaseColumns/避免重复写id, name, gender, class_id, major。提示别迷信“最新技术栈”。高校IT预算有限运维人员可能只有1个兼职老师。Spring Cloud带来服务注册、配置中心、链路追踪但也意味着要多维护Eureka、Nacos、SkyWalking三个服务。而我们的系统TomcatMySQL两台虚拟机搞定教务员重启服务只需双击restart.bat。2.2 数据库设计为什么课程表要拆成三张而不是一张大宽表看到标题里“课程安排”“选课操作”“成绩处理”很多人第一反应是建一张course_selection表字段塞满id, student_id, course_id, teacher_id, semester, credit, grade, status, created_time...。实测跑半年后这张表就成性能黑洞。我们采用的是“主-从-态”三表分离法course课程主表存课程元数据。id, code(课号), name, credit, type(必修/选修), max_capacity(最大容量)。关键约束code加唯一索引因为教务排课时按课号查课这是最高频查询。course_schedule课程安排表存开课实例。id, course_id, teacher_id, semester, week_day, start_lesson, end_lesson, classroom, current_capacity。注意current_capacity是冗余字段每次选课成功后UPDATE course_schedule SET current_capacity current_capacity 1 WHERE id ?避免实时COUNT(*)。这里有个坑semester用VARCHAR(10)存“2023-2024-1”不用DATE类型——教务系统学期划分不按自然年2023-2024学年第一学期实际从2023年9月到2024年1月用字符串更灵活。student_course学生选课关联表存选课事实。id, student_id, schedule_id, status(0待确认/1已选中/2已退课), grade, created_time。重点来了这张表的联合索引必须是(student_id, schedule_id)和(schedule_id, status)。前者保证学生查“我选了哪些课”毫秒级响应SELECT sc.*, c.name FROM student_course sc JOIN course_schedule cs ON sc.schedule_idcs.id JOIN course c ON cs.course_idc.id WHERE sc.student_id?后者支撑教务查“某节课还有多少空位”SELECT max_capacity - COUNT(*) FROM course_schedule cs LEFT JOIN student_course sc ON cs.idsc.schedule_id WHERE cs.id? AND sc.status1。注意别在student_course里存grade字段成绩录入是独立流程由教师端操作。选课表只记录“选中”状态成绩表另建score字段id, student_id, schedule_id, grade, teacher_id, term。否则选课未结束就有人录成绩事务会乱。23. 技术栈版本锁定为什么坚持JDK 8 MySQL 5.7热搜词里全是“java: outofmemoryerror”“mysql 8.0环境变量配置”恰恰说明升级不是免费的。我们线上系统用JDK 8u291 MySQL 5.7.36原因如下JDK 8的稳定性高校机房服务器多为老旧型号如Dell R720JVM参数调优经验成熟。-Xms2g -Xmx2g -XX:MetaspaceSize256m -XX:MaxMetaspaceSize256m -XX:UseG1GC这套参数在2C4G虚拟机上稳定运行5年无Full GC。换成JDK 17G1GC的Region大小计算逻辑变化同样参数下Young GC频率翻倍教务员反馈“系统变卡”。MySQL 5.7的兼容性学校统一采购的备份软件如Veeam只认证MySQL 5.7。升级到8.0后备份任务失败率飙升恢复演练时发现mysql_upgrade脚本与旧版InnoDB引擎不兼容。更现实的是教务处用的Excel插件读取MySQL数据生成课表只支持5.7的mysql-connector-java-5.1.47.jar换新版驱动后中文乱码。避坑实录曾有团队强行上Spring Boot 3要求JDK 17结果教务处的Windows 7办公机没错还有Win7装不了JDK 17Tomcat启动报错UnsupportedClassVersionError。最后妥协方案后端用JDK 17编译但打包时指定maven.compiler.source17/maven.compiler.sourcemaven.compiler.target17/maven.compiler.target前端仍用JDK 8运行——白忙活。3. 核心模块实现细节从学生点击“选课”到数据落库的17步3.1 学生选课流程为什么要在数据库层面加行级锁学生点“选课”按钮你以为只是INSERT INTO student_course错。真实流程是17步其中3步必须数据库锁前端提交{studentId: 2023001, scheduleId: CS202301}后端校验学生状态SELECT status FROM student WHERE id ?是否休学、是否毕业校验课程安排状态SELECT status, current_capacity, max_capacity FROM course_schedule WHERE id ?是否已关闭选课、是否满员关键一步SELECT * FROM course_schedule WHERE id ? FOR UPDATE—— 这是InnoDB行锁锁住这一行防止并发选课超限检查current_capacity max_capacity否则返回“名额已满”执行UPDATE course_schedule SET current_capacity current_capacity 1 WHERE id ?插入选课记录INSERT INTO student_course (student_id, schedule_id, status) VALUES (?, ?, 1)记录操作日志INSERT INTO operation_log (operator, action, target, ip) VALUES (?, SELECT_COURSE, ?, ?)发送站内信INSERT INTO message (to_user, content, type) VALUES (?, 您已成功选修《Java程序设计》, COURSE_SELECT)更新学生缓存如有DELETE FROM redis WHERE key student_courses_2023001...后续是通知教师、更新课表等异步任务为什么非得FOR UPDATE举个例子课程剩余1个名额学生A和B同时点选课。若不加锁A查current_capacity1B也查current_capacity1然后A和B都执行UPDATE ... SET current_capacity 1 1结果current_capacity2超限加锁后A先获得锁B阻塞等待A更新完释放锁B再查已是current_capacity2自然失败。实操心得FOR UPDATE必须在事务内且SELECT语句要走索引。我们给course_schedule.id建了主键索引所以WHERE id ?能命中。如果写成WHERE course_id ?course_id没索引会锁整张表选课高峰期直接雪崩。3.2 成绩录入模块如何防止教师误操作覆盖成绩教师端录入成绩最怕手抖点错。我们做了三层防护前端防护成绩输入框绑定onblur事件输入后自动校验value 0 value 100 value % 0.5 0支持0.5分制非法值立刻标红并禁用提交按钮。后端防护UPDATE score SET grade ? WHERE id ? AND status DRAFT。status字段初始为DRAFT教师保存时设为SUBMITTED教务审核后才变CONFIRMED。这样即使教师点两次保存第二次因status ! DRAFT而失败避免重复更新。数据库防护在score表上建触发器DELIMITER $$ CREATE TRIGGER check_grade_update BEFORE UPDATE ON score FOR EACH ROW BEGIN IF NEW.grade ! OLD.grade AND OLD.status CONFIRMED THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 已确认成绩不可修改; END IF; END$$ DELIMITER ;当教务已确认成绩教师再试图修改MySQL直接报错连Java层都不到。3.3 管理员报表导出为什么用POI SXSSF而不是普通XSSFWorkbook教务处每月要导出“各专业挂科率统计”数据量常超10万行。用XSSFWorkbook内存爆炸OutOfMemoryError是常态。我们改用SXSSFWorkbook流式Excel// 创建100行缓冲区超出部分写入磁盘临时文件 SXSSFWorkbook workbook new SXSSFWorkbook(100); Sheet sheet workbook.createSheet(挂科率统计); // 写入表头... Row headerRow sheet.createRow(0); headerRow.createCell(0).setCellValue(专业); headerRow.createCell(1).setCellValue(总人数); headerRow.createCell(2).setCellValue(挂科人数); headerRow.createCell(3).setCellValue(挂科率); // 写入数据... for (int i 0; i dataList.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(dataList.get(i).getMajor()); row.createCell(1).setCellValue(dataList.get(i).getTotalCount()); row.createCell(2).setCellValue(dataList.get(i).getFailCount()); row.createCell(3).setCellValue(String.format(%.2f%%, dataList.get(i).getFailCount() * 100.0 / dataList.get(i).getTotalCount())); } // 关键及时dispose释放临时文件 workbook.write(outputStream); workbook.dispose(); // 必须调用否则/tmp下残留大量临时文件实测对比导出8万行数据XSSFWorkbook占用内存1.2GBSXSSFWorkbook峰值仅120MB且导出时间从3分12秒降到48秒。4. 部署与运维实战在高校机房里让系统“活下去”的12个细节4.1 Tomcat调优为什么把maxThreads设为150而不是默认200高校机房的Tomcat不是云服务器。我们监控发现CPU使用率长期低于30%但http-nio-8080-exec-*线程池经常满。查jstack大量线程卡在org.apache.tomcat.util.net.NioEndpoint$Poller.run()。原因NIO Poller线程数不足。解决方案不是加maxThreads而是调acceptCount和minSpareThreads!-- server.xml -- Connector port8080 protocolorg.apache.coyote.http11.Http11NioProtocol maxThreads150 minSpareThreads25 acceptCount100 connectionTimeout20000 redirectPort8443 /maxThreads150足够应付选课高峰实测峰值132线程再高反而增加上下文切换开销minSpareThreads25预留25个空闲线程避免请求突增时创建新线程的延迟acceptCount100当所有线程忙时最多排队100个连接超过则拒绝返回Connection refused比让请求等死强注意acceptCount不是队列长度而是操作系统TCP连接队列长度。Linux默认net.core.somaxconn128所以acceptCount设100刚好。4.2 MySQL慢查询治理如何用pt-query-digest定位真凶教务员抱怨“查学生成绩慢”EXPLAIN看score表全表扫描。但score有student_id索引啊用pt-query-digest分析慢日志pt-query-digest /var/log/mysql/slow.log --filter $event-{Bytes} 100000 | head -20发现TOP1是这条SQLSELECT s.name, c.name, sc.grade FROM student s, course c, student_course sc, score sc2 WHERE s.id sc.student_id AND c.id sc.course_id AND sc.id sc2.student_course_id;问题在哪student_course和score是1:N关系但SQL写了笛卡尔积正确写法应是SELECT s.name, c.name, sc2.grade FROM student s JOIN student_course sc ON s.id sc.student_id JOIN course c ON sc.course_id c.id JOIN score sc2 ON sc.id sc2.student_course_id;修复后执行时间从12.8s降到0.08s。教训永远用JOIN显式关联禁用逗号分隔的隐式连接。4.3 安全加固为什么教务系统不需要JWT但必须防SQL注入高校系统不对外网开放走校园网所以不用JWT做分布式鉴权。但SQL注入是刚需防护。我们坚持两条铁律所有查询用PreparedStatement哪怕简单如SELECT * FROM user WHERE username ?绝不拼接字符串。MyBatis里禁用${}只用#{}${}是字符串替换#{}是预编译占位符。比如动态表名必须用${tableName}但我们加了白名单校验private static final SetString ALLOWED_TABLES Set.of(student, course, teacher); public ListMapString, Object queryByTable(Param(tableName) String tableName, Param(condition) String condition) { if (!ALLOWED_TABLES.contains(tableName)) { throw new IllegalArgumentException(非法表名: tableName); } return mapper.queryByTable(tableName, condition); }实操心得教务处曾用Excel导入学生数据字段含单引号如学生姓名“OConnor”。若用字符串拼接INSERT INTO student VALUES (OConnor, ...)直接语法错误。用#{}MyBatis自动转义为OConnor完美解决。5. 常见问题与排查技巧实录那些让教务员半夜打电话的Bug5.1 选课成功但页面不刷新可能是浏览器缓存惹的祸现象学生点“选课”弹窗显示“成功”但刷新页面“已选课程”列表没变。教务员急call“系统坏了”排查路径查浏览器开发者工具Network发现/student/selectCourse返回200但响应体是{success:true,message:选课成功}而前端JS没处理这个JSON仍用旧缓存渲染原因前端用location.reload()强制刷新但Chrome对GET请求缓存太激进解决后端返回HTTP 303 See Other重定向到结果页response.setStatus(HttpServletResponse.SC_SEE_OTHER); response.setHeader(Location, request.getContextPath() /student/myCourses.jsp);或者前端AJAX成功后手动清空myCourses.jsp的DOM再重新加载。5.2 成绩导出Excel乱码根源在MySQL字符集现象导出的Excel里中文全是问号。根因链MySQL服务器字符集是latin1安装时默认course表name字段用utf8mb4但连接URL没指定编码jdbc:mysql://localhost:3306/edu?useSSLfalse正确URLjdbc:mysql://localhost:3306/edu?useSSLfalsecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai更彻底在MySQL配置文件my.cnf里全局设置[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci [client] default-character-setutf8mb4注意utf8mb4不是utf8MySQL的utf8只支持3字节UTF-8存不了emoji和某些生僻汉字。utf8mb4才是真UTF-8。5.3 系统突然变慢先看数据库连接池是否耗尽现象平时响应快某天所有页面加载超10秒。速查三步netstat -anp | grep :3306 | wc -l—— 查MySQL连接数若接近max_connections默认151基本确定show processlist;—— 看是否有Sleep状态连接堆积应用没正确close Connection检查代码是否在finally块里connection.close()我们强制要求Connection conn null; PreparedStatement ps null; try { conn dataSource.getConnection(); ps conn.prepareStatement(sql); // ... } catch (SQLException e) { // 处理异常 } finally { if (ps ! null) try { ps.close(); } catch (SQLException e) {} if (conn ! null) try { conn.close(); } catch (SQLException e) {} // 关键 }5.4 教务员说“找不到学生”检查身份证号校验逻辑现象新生报到教务员录入学生信息身份证号11010119900307251X系统提示“身份证格式错误”。原因Java的String.equals()区分大小写而身份证末位X是大写但前端输入框可能转成小写。我们加了标准化处理public static String normalizeIdCard(String idCard) { if (idCard null) return null; return idCard.toUpperCase().trim(); }并在数据库字段加COLLATE utf8mb4_unicode_ci确保查询时不区分大小写。6. 项目交付之外教务员真正需要的从来不只是代码最后分享一个血泪教训去年帮某学院上线系统代码验收满分培训手册写得比教科书还细结果运行一周后教务员打来电话“老师那个‘批量导入学生’按钮点完没反应你们是不是没做”——我们做了但按钮藏在二级菜单“数据维护”→“学生管理”→“批量导入”而教务员只记得“导入”两个字。第二天我们把按钮提到首页导航栏加粗图标旁边写“【紧急】新生导入”并配一句大字提示“每年9月1日启用其他时间禁用”。这让我明白高校信息系统不是技术秀场而是工作流的数字化延伸。教务员要的不是“支持RESTful API”而是“点三次鼠标就能导出上学期成绩单”不是“高可用集群”而是“U盘插服务器USB口双击backup.bat就自动备份”。所以我们在webapps/ROOT/下放了个quick-guide.html里面只有三张截图①登录页账号密码贴在工位旁②选课失败怎么办重启Tomcat步骤③联系人电话我的手机号24小时开机。这个系统真正的终点不是Git仓库里的v1.0.0tag而是教务员电脑桌面那个名为“选课系统-快捷入口”的.lnk文件。当你看见她熟练地双击它喝着枸杞茶等着选课开始的倒计时那一刻代码才算真正活了过来。本文还有配套的精品资源点击获取
返回列表