ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SSM校园教务管理系统实战:环境搭建、数据库设计与选课并发控制

SSM校园教务管理系统实战:环境搭建、数据库设计与选课并发控制 简介这份资源是基于SSM框架的校园教务管理系统完整设计包面向计算机相关专业做毕业设计、课程设计或期末大作业的学生也适合想练手Spring、SpringMVC与MyBatis整合开发的初学者。系统围绕学生、教师、课程与成绩等教务核心业务展开采用分层设计将数据访问层、业务逻辑层与服务层分离兼顾功能完整性、操作便捷性与数据安全性。压缩包共469个文件约10.61MB其中135个java文件承载后端业务逻辑50个vue文件与19个js文件构成前端页面另有23个xml配置、2个sql脚本及svg、png、jpg等静态资源并附bat启动脚本与doc说明文档目录结构清晰便于按模块查阅与二次开发。目前已有41人学习下载。读者可据此获得一套可直接参考的教务系统实现方案涵盖数据库设计、前后端交互与部署维护思路适合作为项目搭建与排错复盘的实践素材。1. 从一份课程表说起SSM 校园教务管理系统到底在解决什么每学期开学前两周教务处的老师几乎都在做同一件事把几百个班级、上千门课程、几十间教室塞进一张时间表里还要避开教师冲突、教室冲突、班级冲突。手工排课的时代早就过去了但很多学校用的系统要么是十几年前的老古董要么是买来的成品软件改都改不动。这就是为什么「基于 SSM 的校园教务管理系统」一直是毕业设计和二次开发的热门选题——它踩中了真实需求技术栈又足够成熟。SSM 指的是 Spring SpringMVC MyBatis 这套组合在国内 Java Web 开发里统治了将近十年。虽然现在 SpringBoot 更流行但 SSM 的分层结构清晰、配置显式、调试直观特别适合拿来理解一个 Web 系统从请求到数据库的完整链路。校园教务管理系统的核心业务无非几块学生信息管理、教师信息管理、课程管理、选课管理、成绩管理、排课管理。听起来简单但每一块都有坑——选课并发、成绩录入权限、排课冲突检测随便一个都能让新手翻车。这篇文章面向两类人一是要基于 SSM 从零搭建教务系统的开发者二是手里已经有一个 SSM 项目但跑不起来、想搞懂每一层怎么配的人。我会按「环境搭建 → 数据库设计 → 核心功能实现 → 排错」的顺序讲每个步骤都给可复现的命令和代码参数怎么调、为什么这么设都会说清楚。不堆概念直接上能抄的作业。2. SSM 环境搭建与项目骨架从零到能跑的最小闭环2.1 版本选型为什么我建议锁死在 Spring 5.3 MyBatis 3.5SSM 项目最容易翻车的地方不是代码写错而是版本对不上。Spring 4.x 和 5.x 的包名有变化MyBatis 3.4 和 3.5 对 JDK 版本要求不同MySQL 8 的驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver。我一般会锁死这套组合JDK 8 或 11、Spring 5.3.x、SpringMVC 5.3.x、MyBatis 3.5.x、MySQL 8.0、Druid 1.2.x、Maven 3.8。这套组合在 Tomcat 9 下跑得最稳社区资料也最全。如果你用 JDK 17Spring 5.3 虽然能跑但反射相关的警告会很多MyBatis 某些版本还会报InaccessibleObjectException。新手没必要给自己找麻烦JDK 8 就够了。Maven 的pom.xml里重点管好三块Spring 的spring-webmvc、spring-jdbc、spring-txMyBatis 的mybatis和mybatis-spring以及数据库的mysql-connector-java和连接池druid。!-- pom.xml 关键依赖版本统一管理 -- properties spring.version5.3.30/spring.version mybatis.version3.5.13/mybatis.version /properties dependencies !-- Spring 核心 MVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 整合包版本必须匹配 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.2/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency !-- MySQL 8 驱动注意 groupId 已变更 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies这段配置里最容易错的是mybatis-spring的版本。2.0.x 对应 Spring 51.3.x 对应 Spring 4写错了启动就报NoClassDefFoundError。另外 MySQL 8 的驱动groupId从mysql变成了com.mysql老教程里还是旧的复制过来直接红。2.2 三层配置文件web.xml、spring-mvc.xml、applicationContext.xml 各管什么SSM 的配置分散在三个文件里很多人搞不清谁加载谁。简单说web.xml是入口负责启动 Spring 容器和 SpringMVC 容器applicationContext.xml管 Service 和 DAO 层包括数据源、事务、MyBatis 的SqlSessionFactoryspring-mvc.xml管 Controller 层包括注解驱动、视图解析器、静态资源放行。两个容器是父子关系子容器能拿到父容器的 Bean反过来不行。!-- web.xml 核心片段注册两个容器 -- context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingContextLoaderListener加载父容器DispatcherServlet加载子容器。这里有个经典坑如果把Service注解的类扫描到spring-mvc.xml里事务会失效因为子容器里的事务管理器不生效。所以spring-mvc.xml的context:component-scan一定要加use-default-filtersfalse只扫Controller。!-- spring-mvc.xml只扫 Controller避免事务失效 -- context:component-scan base-packagecom.campus.controller use-default-filtersfalse context:include-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan mvc:annotation-driven/ mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /beanapplicationContext.xml里则要配数据源、SqlSessionFactoryBean、MapperScannerConfigurer和事务管理器。数据源用 Druid 时initialSize设 5、maxActive设 20 对校园系统足够validationQuery写SELECT 1否则 MySQL 8 小时断连后第一次请求会报错。2.3 用 Maven 命令跑通第一个接口骨架搭好后用一条命令验证环境是否通mvn clean package -DskipTests # 把 target 下的 war 包丢进 Tomcat webapps启动后访问 curl http://localhost:8080/campus/test/ping对应的 Controller 写一个最简单的接口Controller RequestMapping(/test) public class PingController { RequestMapping(/ping) ResponseBody public String ping() { return pong; } }如果返回pong说明 SpringMVC 容器起来了。如果 404先看web.xml的url-pattern是不是/再看 Tomcat 日志里DispatcherServlet有没有初始化成功。如果报NoClassDefFoundError九成是依赖冲突用mvn dependency:tree查一下有没有重复的 Spring 包。这一步跑通后面才有意义。3. 教务数据库设计七张核心表与选课冲突的字段级处理3.1 学生、教师、课程、选课四张主表怎么建教务系统的数据库设计直接决定后面代码好不好写。我一般会建七张表student、teacher、course、class班级、sc选课成绩、schedule排课、user登录账号。核心是sc表它同时承担选课记录和成绩记录两个职责字段包括student_id、course_id、score、select_time。student_id和course_id建联合唯一索引防止重复选课。CREATE TABLE student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, class_id BIGINT, major VARCHAR(50), phone VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE COMMENT 课程号, cname VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, teacher_id BIGINT, capacity INT DEFAULT 60 COMMENT 容量上限, selected INT DEFAULT 0 COMMENT 已选人数 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sc ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, course_id BIGINT NOT NULL, score DECIMAL(5,1) DEFAULT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_course (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;course表里的capacity和selected是选课并发的关键。不要每次选课都去COUNT(sc)那样在高并发下会锁表。正确做法是在course表里维护一个selected字段选课时用UPDATE course SET selected selected 1 WHERE id ? AND selected capacity靠数据库的行锁保证不超卖。3.2 排课冲突检测时间片编码与 SQL 层面的互斥判断排课是教务系统里最烧脑的部分。我的做法是把一周的时间切成时间片比如周一到周五、每天 5 个大节用day_of_week1-5和section1-5两个字段表示。schedule表存course_id、classroom、day_of_week、section、weeks周次范围。冲突检测就是查同一时间片、同一周次范围内是否已有相同教师、相同班级或相同教室的记录。-- 检测某教师在某时间片是否已有课 SELECT COUNT(*) FROM schedule s JOIN course c ON s.course_id c.id WHERE c.teacher_id #{teacherId} AND s.day_of_week #{day} AND s.section #{section} AND s.weeks #{weeks};这条 SQL 返回大于 0 就说明冲突。教室冲突同理把teacher_id换成classroom字段即可。班级冲突需要schedule表再关联class表因为一个班可能有多门课。实际写的时候我会把三种冲突检测封装成一个checkConflict方法返回具体是哪种冲突前端好提示。注意weeks字段如果存的是1-16这种字符串SQL 里没法直接比较。要么拆成start_week和end_week两个整型字段要么用 MySQL 的FIND_IN_SET配合逗号分隔。我推荐前者查询效率高得多。3.3 用 MyBatis 逆向工程生成实体和 Mapper手写实体类和 Mapper 太慢用 MyBatis Generator 自动生成。在pom.xml里加mybatis-generator-maven-plugin配一个generatorConfig.xml指定数据库连接和生成路径然后跑mvn mybatis-generator:generate生成的StudentMapper.xml里会有基础的增删改查但教务系统的查询往往需要多表关联比如查学生选课列表要带课程名和教师名。这时候不要改生成的 XML而是新建一个StudentCourseMapper.xml写自定义 SQL避免重新生成时被覆盖。自定义查询示例select idselectMyCourses resultTypemap SELECT c.cname, t.name AS teacherName, s.score, s.select_time FROM sc s JOIN course c ON s.course_id c.id LEFT JOIN teacher t ON c.teacher_id t.id WHERE s.student_id #{studentId} /select返回resultTypemap适合前端直接渲染表格不用再建 VO 类。如果字段多、需要类型转换就建一个CourseVO类把resultType改成全限定类名。4. 选课与成绩模块的落地Service 事务、并发控制与权限校验4.1 选课接口的完整链路从 Controller 到数据库行锁选课是教务系统里唯一有并发压力的场景。每学期开放选课的那几分钟几百个学生同时点如果代码写不好要么超卖要么死锁。我的实现分三步Controller 接收courseId从 Session 拿studentIdService 先查是否已选再执行带条件的UPDATE扣减名额最后插入sc记录整个过程加Transactional。Service public class SelectCourseService { Autowired private CourseMapper courseMapper; Autowired private ScMapper scMapper; Transactional(rollbackFor Exception.class) public String select(Long studentId, Long courseId) { // 1. 查是否已选联合唯一索引兜底 int exists scMapper.countByStuAndCourse(studentId, courseId); if (exists 0) { return 已选过该课程; } // 2. 带条件的原子扣减selected capacity 才更新 int rows courseMapper.decreaseCapacity(courseId); if (rows 0) { return 课程名额已满; } // 3. 插入选课记录 scMapper.insert(studentId, courseId); return 选课成功; } }对应的 Mapper SQLupdate iddecreaseCapacity UPDATE course SET selected selected 1 WHERE id #{courseId} AND selected lt; capacity /updatedecreaseCapacity返回受影响行数0 表示名额已满。这条UPDATE在 InnoDB 下会对该行加排他锁并发时后到的请求会等锁等到了发现selected capacity就返回 0。这样既不会超卖也不会因为SELECT ... FOR UPDATE造成大范围锁等待。Transactional保证扣减和插入要么都成功要么都回滚。4.2 成绩录入的权限校验用拦截器区分教师和管理员成绩录入不能谁都能调。我的做法是写一个AuthInterceptor在spring-mvc.xml里配置拦截/score/**路径检查 Session 里的role字段。教师只能录自己教的课管理员可以录所有课。拦截器里拿到courseId后查course表的teacher_id是否等于当前用户 ID。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { HttpSession session req.getSession(); String role (String) session.getAttribute(role); if (admin.equals(role)) { return true; } if (teacher.equals(role)) { Long teacherId (Long) session.getAttribute(userId); String courseIdStr req.getParameter(courseId); if (courseIdStr null) { resp.sendError(403, 缺少课程参数); return false; } Long courseId Long.parseLong(courseIdStr); // 查课程归属判断是否本人授课 Long ownerId courseMapper.selectTeacherId(courseId); if (teacherId.equals(ownerId)) { return true; } } resp.sendError(403, 无权限); return false; } }拦截器注册在spring-mvc.xml的mvc:interceptors里mvc:mapping path/score/**/指定拦截范围。这里有个细节preHandle里如果直接resp.sendError前端拿到的是 Tomcat 的错误页体验不好。更好的做法是返回 JSON但需要手动设Content-Type和写response.getWriter()。新手先用sendError跑通后面再优化。4.3 成绩批量导入POI 解析 Excel 与事务回滚成绩录入如果一条条填太慢教务老师通常会给一个 Excel。用 Apache POI 解析每行读学号、课程号、分数然后批量更新sc表。关键是事务如果第 50 行学号不存在前面 49 行要不要回滚我的选择是全部回滚让老师改完 Excel 重新传避免部分成功导致数据对不上。Transactional(rollbackFor Exception.class) public MapString, Object importScores(MultipartFile file) throws Exception { ListString errors new ArrayList(); try (Workbook wb WorkbookFactory.create(file.getInputStream())) { Sheet sheet wb.getSheetAt(0); for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) continue; String sno row.getCell(0).getStringCellValue().trim(); String cno row.getCell(1).getStringCellValue().trim(); double score row.getCell(2).getNumericCellValue(); int updated scMapper.updateScoreBySnoAndCno(sno, cno, score); if (updated 0) { errors.add(第 (i 1) 行学号或课程号不存在); } } } if (!errors.isEmpty()) { throw new RuntimeException(String.join(, errors)); } return Collections.singletonMap(success, true); }WorkbookFactory.create同时支持.xls和.xlsx不用分别写HSSFWorkbook和XSSFWorkbook。updateScoreBySnoAndCno返回 0 说明sc表里没有对应记录可能是学生没选这门课。抛异常触发回滚前端拿到错误信息让老师修正。注意 POI 的getNumericCellValue对文本格式的分数会抛异常导入前要提醒老师把分数列设成数值格式。5. 部署与排错Tomcat 日志、中文乱码与连接池的四个高频坑5.1 启动报 NoClassDefFoundError 或 ClassNotFoundException现象Tomcat 启动时控制台刷java.lang.NoClassDefFoundError: org/springframework/context/ApplicationContext或者ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因依赖没打进WEB-INF/lib或者 Maven 依赖冲突导致某个包被排除。常见于手动改过pom.xml的exclusions或者用了provided作用域但 Tomcat 里没有对应 jar。解决先跑mvn dependency:tree -Dverbose看有没有omitted for conflict的包。如果是 MySQL 驱动找不到检查pom.xml里mysql-connector-j的scope是不是provided改成默认的compile。如果是 Spring 包缺失在 IDEA 的 Project Structure 里看 Artifact 的WEB-INF/lib下有没有对应的 jar没有就重新mvn package。5.2 中文乱码从 JSP 到数据库的四个编码点现象页面输入的中文存到数据库变成???或者数据库里正常但页面显示乱码。原因编码链路有四个点——JSP 页面、请求体、数据库连接、数据库表。任何一个没设 UTF-8 都会乱。解决JSP 头部加% page contentTypetext/html;charsetUTF-8 %web.xml加CharacterEncodingFilterforceEncoding设trueJDBC URL 加useUnicodetruecharacterEncodingutf8建表时指定DEFAULT CHARSETutf8mb4。四个点都对齐后中文就不会出问题。MySQL 8 默认字符集已经是utf8mb4但老库迁移过来可能还是latin1用SHOW CREATE TABLE student确认一下。5.3 连接池报错Druid 的 validationQuery 与 MySQL 8 小时断连现象系统跑一晚上第二天早上第一个请求报CommunicationsException: The last packet successfully received from the server was xxx milliseconds ago。原因MySQL 默认wait_timeout是 8 小时连接池里的空闲连接被服务端断开但客户端不知道拿起来用就报错。解决Druid 配validationQuery: SELECT 1、testWhileIdle: true、timeBetweenEvictionRunsMillis: 60000让连接池定期检测空闲连接。另外minEvictableIdleTimeMillis设成比wait_timeout小比如 5 分钟主动淘汰旧连接。这三个参数配上基本不会再出现隔夜断连。5.4 事务不生效Transactional 失效的三种典型场景现象Service 方法上加了Transactional但抛异常后数据没回滚。原因一是方法不是publicSpring AOP 代理不生效二是同类内部方法直接调用没走代理三是spring-mvc.xml扫了 Service 包导致事务管理器在子容器里找不到。解决Transactional只加在public方法上如果 Service 内部要调另一个事务方法通过ApplicationContext拿代理对象再调或者拆到另一个 Servicespring-mvc.xml的component-scan严格只扫ControllerService 和 DAO 交给applicationContext.xml。这三条是血泪经验每一条我都见过有人踩。6. 从能跑到好用分页插件、缓存与接口压测的三个进阶技巧系统跑起来只是第一步真正交付前还得解决分页、缓存和性能验证。分页用 PageHelper 最省事在pom.xml加pagehelper依赖Service 里一行PageHelper.startPage(pageNum, pageSize)放在查询前返回的List会被自动包装成PageInfo。注意startPage只对紧接着的第一条查询生效中间不要插别的查询。参数上pageSize建议限制在 50 以内防止前端传个 10000 把数据库拖死。public PageInfoMapString, Object listStudents(int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListMapString, Object list studentMapper.selectAllWithClass(); return new PageInfo(list); }缓存方面课程列表、班级列表这种变动少的数据用 Spring Cache Redis 或者 Ehcache 都行。如果不想引入 Redis用ConcurrentHashMap做个简单的本地缓存也能顶一阵但要注意加过期时间否则数据改了页面不刷新。我一般会在 Service 的查询方法上加Cacheable更新方法上加CacheEvict配置一个CacheManager就行。压测是验证选课接口的关键。用 JMeter 或ab命令模拟 200 个并发选同一门课看selected有没有超过capacity。命令示例ab -n 500 -c 200 -p select.json -T application/json \ http://localhost:8080/campus/course/selectselect.json里放{courseId: 1}。跑完看数据库course表的selected字段如果等于capacity且sc表记录数一致说明并发控制没问题。如果selected超了检查decreaseCapacity的WHERE条件是不是漏了selected capacity。压测时把 Tomcat 的maxThreads调到 300acceptCount调到 200否则请求会堆在连接队列里。最后说个习惯每次改完 Mapper XML我都会重启 Tomcat 后先访问一次相关页面看控制台有没有BindingException。MyBatis 的 XML 里方法名和id对不上、resultType写错包名这些错误启动时不报第一次调用才炸。提前点一遍比等用户反馈强。希望帮到你。本文还有配套的精品资源点击获取
返回列表