
简介这是一套基于SSM的高校选课系统完整源码面向Java初学者、毕业设计学生以及需要快速搭建校内课务管理平台的开发者。系统基于Spring、SpringMVC、MyBatis分层架构覆盖学生管理、课程发布、在线选课、管理员审批、成绩录入等核心业务并提供选课冲突检测与权限控制能力可直接作为课程设计或毕业设计的功能蓝本。压缩包共911个文件约11.32MB包含120个Java源码、55个JSP页面、21个XML配置和2个SQL脚本前端配有224个JS、102个CSS及Bootstrap样式资源另有大量图片、字体和部分.bak备份文件便于参考界面效果与对比原始修改。文件类型覆盖后端逻辑、动态页面、样式交互、数据库初始化与项目配置适合按模块查阅。当前已有131人学习下载。项目采用分层结构与前台页面分离的方式组织目录并附带数据库初始化脚本部署到主流JavaWeb环境后即可体验完整的选课流程也适合在此基础上二次开发或进行答辩讲解。1. 从SSM选课系统的.bak文件说起这套源码真正值钱的部分先把结论放在前面对于一个用户量在几千到几万、峰值选课并发不超过几十 TPS 的高校选课系统SSMSpring SpringMVC MyBatis不是“过时技术”而是刚好够用的工程方案。难点不在框架选型而在选课这种“先验证再写入”的业务怎么把事务边界划对怎么在并发下拦住同一门课被超选。这套源码的价值也不是代码量而是它把一个 Web 工程的完整形态摆在你面前——包括那些容易忽略的残留文件。打开压缩包你会先看到 styles.css.bak、index.jsp.bak、setMenu.js.bak、topNav.jsp.bak、.classpath、org.eclipse.wst.common.component 这类文件这已经透露了两个信息项目由 Eclipse 导出页面层经历过一次改动并保留了备份。对正在做 Java 毕业设计或者接手维护的人来说这些 .bak 恰恰是理解项目演进过程的入口。2. SSM框架分层与选课数据库表设计先把项目骨架拆明白2.1 从残留文件判断项目结构风格.classpath和org.eclipse.wst.common.component是 Eclipse Dynamic Web Project 的标记文件前者记录源码目录和 JRE 版本后者声明 Web 模块的部署路径。如果你准备用 IDEA 打开这套源码这两个文件不会参与构建但别着急删。打开.classpath能看到classpathentry kindsrc pathsrc/这类条目它告诉你源码根目录是src而非 Maven 的src/main/java也说明项目是基于传统 Web 工程目录而非 Maven 结构。常见做法是先保留这些文件确认编译输出目录和依赖库的引入方式再决定是否迁移到 Maven。.bak文件则是上一轮修改者留下的“后悔药”比如index.jsp.bak说明首页 JSP 被动过topNav.jsp.bak说明公共导航栏也被改过。这类文件在 IDE 里不会报错但放在 WEB-INF 下会被认为普通 JSP 文件继续参与编译如果里面混入了旧版页面代码很容易出现“页面显示的还是老样子”的错觉。2.2 三张核心表建模高校选课系统围绕“学生—课程—选课记录”三个实体展开。先看课程表字段要覆盖开课学期、教师、容量和已选人数已选人数是判断是否满员的前台依据。学生表主要存放学号、姓名、学院选课记录表则负责关联两者并记录选课时间与状态。下面这三张表是这套场景下最常见的落法CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(32) NOT NULL UNIQUE, course_name VARCHAR(128) NOT NULL, teacher_name VARCHAR(64), credit DECIMAL(3,1) DEFAULT 2.0, capacity INT NOT NULL DEFAULT 60, selected INT NOT NULL DEFAULT 0, semester VARCHAR(32) NOT NULL, schedule VARCHAR(255), is_active TINYINT DEFAULT 1 ); CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(32) NOT NULL UNIQUE, student_name VARCHAR(64), college VARCHAR(128) ); CREATE TABLE t_selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1, UNIQUE KEY uk_student_course (student_id, course_id) );这里需要说明两个容易被忽略的设计点。t_selection上的联合唯一约束是并发防重复选课的第一道闸门即使 Service 层代码写漏了数据库也会在第二次插入同一对student_id course_id时报 DuplicateKeyException不会产生脏数据。t_course.selected字段作为已选人数缓存目的是避免每次都要COUNT(*)全表扫选课记录但它的值必须与t_selection的真实插入保持一致否则会出现页面显示未满员、实际无法选课的矛盾。2.3 MyBatis与Spring的初始化要点这套源码既然是 Web 工程而非 Maven 工程MyBatis 的引入方式必然是 jar 包 Spring 配置文件。常见做法是在applicationContext.xml里配置SqlSessionFactoryBean同时指定 mapper 文件位置。关键配置如下bean iddataSource classorg.apache.commons.dbcp.BasicDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/course_db?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath*:com/course/dao/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.course.dao/ /beanmapperLocations指向 dao 包下的 XML 文件MapperScannerConfigurer会扫描接口并生成代理实现。如果这套源码里 mapper XML 放在src/com/course/dao/下路径字符串一定要写对最常见的问题就是 mapper 接口与 XML 文件名不一致导致启动时提示Invalid bound statement。2.4 分层代码组织源码包内代码通常按 controller、service、dao、entity 四层组织。Controller 层只做参数接收和视图跳转Service 层处理事务和业务校验DAO 层通过 MyBatis 与 MySQL 交互Entity 层与表字段一一对应。如果发现项目里 entity 类的属性名与数据库列名不一致需要在 MyBatis 映射文件中配置resultMap做映射别图省事直接依赖驼峰自动转换MySQL 配置和 MyBatis 版本不同的时候很容易踩坑。3. 选课核心链路SpringMVC控制器、MyBatis动态SQL与事务兜底3.1 CourseController与请求映射选课动作的入口是 SpringMVC 的 Controller。对 JSP 页面来说选课通常会走一个带ResponseBody的接口前端用 Ajax 提交返回 JSON 而不是直接刷新页面。这样做的原因是选课结果需要即时提示“选课成功”或“容量已满”整页刷新会丢失操作上下文。Controller RequestMapping(/course) public class CourseController { Autowired private CourseService courseService; RequestMapping(value /select, method RequestMethod.POST) ResponseBody public MapString, Object selectCourse(Integer courseId, Integer studentId) { MapString, Object result new HashMapString, Object(); try { boolean ok courseService.selectCourse(studentId, courseId); result.put(code, ok ? 200 : 500); result.put(msg, ok ? 选课成功 : 选课失败容量已满或重复选课); } catch (Exception e) { result.put(code, 500); result.put(msg, 选课异常 e.getMessage()); } return result; } }这个接口返回的 Map 会被 Jackson 序列化成 JSON前端拿到code200再做 DOM 更新。参数courseId和studentId是前端传过来的主键值实际开发中 studentId 应从 session 里取不能信任页面提交的参数否则他人可以伪造学号替别人选课。这里为了示例简化直接接收源码里如果也是这样写接手后建议改成从登录态获取。3.2 Service层事务与容量校验选课的核心逻辑必须放在 Service 层并用事务控制。一个完整的选课动作包含三步查课程当前容量、校验是否冲突、写入选课记录并更新已选人数。这三步只要中间失败一步数据库就应回到操作前状态否则会出现“选课记录没插进去但人数增加了”的极端情况。Service Transactional(rollbackFor Exception.class) public class CourseServiceImpl implements CourseService { Autowired private CourseDao courseDao; Autowired private SelectionDao selectionDao; Override public boolean selectCourse(Integer studentId, Integer courseId) { Course course courseDao.findById(courseId); if (course null || course.getSelected() course.getCapacity()) { return false; } int rows selectionDao.insert(studentId, courseId); if (rows 1) { courseDao.increaseSelected(courseId); return true; } return false; } }Transactional(rollbackFor Exception.class)指定所有异常都回滚包括 RuntimeException 和受检异常。这里的逻辑顺序是先查课程再插入选课记录最后更新已选人数。注意insert依赖t_selection表上的唯一约束如果学生已经选过这门课数据库会直接抛异常事务回滚后selected字段不会被误更新。这种写法比“先查出所有选课记录再遍历判断”更可靠后者在并发场景下必然漏判。3.3 MyBatis动态SQL与课程查询课程列表页通常需要按学期、课程名、授课教师做组合查询条件不确定数量MyBatis 的动态 SQL 正好解决这个问题。where标签会自动去掉多余的 ANDif标签按条件拼接片段。select idfindByCondition parameterTypemap resultTypecom.course.entity.Course SELECT id, course_no, course_name, teacher_name, credit, capacity, selected, semester, schedule FROM t_course where if testsemester ! null and semester ! AND semester #{semester} /if if testcourseName ! null and courseName ! AND course_name LIKE CONCAT(%, #{courseName}, %) /if if testteacherName ! null and teacherName ! AND teacher_name #{teacherName} /if AND is_active 1 /where ORDER BY id /select#{semester}是预编译参数MyBatis 会把它转成?占位符避免 SQL 注入。CONCAT(%, #{courseName}, %)实现模糊匹配不直接写%${courseName}%是因为${}是字符串拼接存在注入风险。传参的 Map 可以只放需要过滤的字段空值的if条件不会进入 SQL。3.4 并发场景下的超选兜底前面 Service 代码在单线程下没问题但如果两个学生同时点击选课事务隔离级别默认下可能出现两人同时读到selected59然后都通过校验最终超出容量。常见做法是给t_course.selected的更新语句加条件int rows courseDao.increaseSelectedWithLimit(courseId, capacity);对应的 SQL 是UPDATE t_course SET selected selected 1 WHERE id #{courseId} AND selected #{capacity}。rows0表示更新失败说明选课人数已达上限。这一招把校验和更新压缩成一条原子语句比加 synchronized 锁或分布式锁都轻量也更适合这套源码所在的单体应用场景。事务仍然保留保证increaseSelectedWithLimit和selectionDao.insert要么都成功要么都回滚。4. JSP与Bootstrap前端改造index.jsp.bak和topNav.jsp.bak的取舍4.1 备份文件的处理策略这套源码里出现index.jsp.bak、topNav.jsp.bak、setMenu.js.bak、styles.css.bak说明上一个维护者改版时直接复制了一份旧文件做备份。既然源码已经交付这些 .bak 就有两种处理方式。第一种是确认新版 JSP 正常运行后直接删除减少 Web 容器扫描文件数量第二种是如果新版页面出现问题把 .bak 改名回 .jsp 再对比差异。我的建议是先做对比再删。比如index.jsp.bak和index.jsp同时存在时用 diff 工具查看两个文件的差异能直观看到改了哪些模块这比看 Git 历史还要直接。4.2 Bootstrap引入与静态资源映射目录下的bootstrap.min.css是压缩版bootstrap.css是未压缩版style.css是自定义样式。JSP 页面引用这些 CSS 时必须确认 SpringMVC 放行了静态资源目录否则所有样式都会 404。常见配置如下mvc:resources mapping/static/** location/static// mvc:annotation-driven/在 JSP 里引用 CSS 的习惯写法是link relstylesheet href${pageContext.request.contextPath}/static/bootstrap/css/bootstrap.min.css link relstylesheet href${pageContext.request.contextPath}/static/css/style.css${pageContext.request.contextPath}会输出项目根路径避免部署到不同路径时 CSS 链接失效。如果少了这一步本地调试没问题部署到 Tomcat 的 ROOT 之外就会出现样式全部丢失。检查源码时要留意 JSP head 区写的是相对路径还是带 contextPath 的绝对路径这是最容易被忽略的坑。4.3 用Ajax对接选课接口选课页面一般用 jQuery 发起异步请求setMenu.js.bak里大概率也封装了菜单切换逻辑。选课按钮可以参考下面这段function doSelect(courseId) { $.ajax({ url: contextPath /course/select, type: POST, data: {courseId: courseId, studentId: $(#studentId).val()}, dataType: json, success: function (resp) { if (resp.code 200) { alert(选课成功); location.reload(); } else { alert(resp.msg); } }, error: function () { alert(网络异常请重试); } }); }前端拿到code200后刷新页面重新渲染课程卡片的剩余名额失败则弹出后端返回的msg。这里的data参数是请求体courseId从当前课程行的 data 属性读取studentId在真实项目中应改为从 session 或隐藏域中获取。如果后端返回的字段名与resp.code对不上检查 Controller 返回的 Map key 是否写成了status或success前后端字段名不一致是最常见的联调问题。5. 选课系统源码排错与交付前检查5.1 高频报错对照表拿到源码包后按下面的对照表排查能解决九成以上的启动和运行问题。报错现象根因处理方式启动报Invalid bound statementmapper XML 与接口不在同一包或文件名不一致检查mapperLocations路径与 XML 文件实际位置页面样式全无SpringMVC 拦截了静态资源请求添加mvc:resources映射并保证 JSP 使用 contextPath访问 JSP 404WEB-INF 下视图解析器前缀配置错误确认InternalResourceViewResolver的 prefix 指向/WEB-INF/views/选课提示 DuplicateKeyException同一学生重复提交用唯一约束兜底前端按钮提交后置灰中文乱码JSP 页面编码与 MySQL 连接编码不一致统一使用 UTF-8并在 JDBC URL 加characterEncodingutf85.2 MyBatis SQL日志与Druid连接池参数排错时先看到 SQL 执行情况能省大量时间。这取决于日志框架的级别配置。以 log4j 为例在log4j.properties中单独为 MyBatis 打开 SQL 日志log4j.logger.com.course.daoDEBUG log4j.logger.org.mybatisINFO log4j.logger.org.springframework.jdbcDEBUGcom.course.dao是 mapper 接口所在包控制台会打印每个接口方法对应的 SQL、参数和影响行数。如果源码连接池用的是 DBCP建议把maxActive、maxWait两个参数调出来防止高并发抢不到连接导致前端长时间等待property namemaxActive value20/ property namemaxIdle value5/ property namemaxWait value10000/maxActive20表示最大活跃连接数选课类系统这个值不宜设太大MySQL 服务端默认连接数有限设成 50 就可能把其他系统拖垮。maxWait10000表示连接池耗尽后最多等 10 秒超过直接抛异常避免页面无限转圈。5.3 交付前文件清理清单确认全部功能跑通后整理源码包按下面几步走。第一步删除或归档所有.bak文件前提是当前版本的 JSP、CSS、JS 都经过验证第二步保留.classpath和org.eclipse.wst.common.component它们对 Eclipse 用户是友好的对 IDEA 用户也无害第三步检查 WEB-INF/lib 下是否存在重复版本的 jar 包比如同时有mysql-connector-java-5.1.x.jar和mysql-connector-java-8.0.x.jar这种组合轻则警告重则启动冲突只留一个第四步确认所有代码文件编码统一为 UTF-8排除 Windows 下默认 GBK 造成的注释乱码。最后重新用干净环境部署一遍验证从首页到选课、退课、后台管理的主流程再压缩交付。本文还有配套的精品资源点击获取