简介:基于Java+SSM+Maven+MySQL+Bootstrap构建的学校教务管理系统,面向Java Web初学者与毕业设计场景,围绕管理员对课程、学生、教师的增删改查、教师给学生打分、学生自主选课等核心功能展开,帮助掌握SSM整合开发与Maven工程构建。压缩包17.82MB,共393个文件,其中以55个Java源码、58个JSP视图、58个XML配置、30个JAR依赖和SQL数据库脚本为主体,附有CSS/JS样式文件与说明文档,导入Maven工程即可运行。项目通过MyBatis逆向工程自动生成Example条件查询类,配合数据库建表脚本与分层包结构,可直接用于学习SSM底层原理、前端表单交互和通用增删改查模板,且系统经过多轮测试运行稳定。目前已有1333人学习使用,适合作为课程设计、实训项目或二次开发的基础源码。
1. SSM 教务管理系统这个资源:骨架完整,适合动手复现
拿这套 Java+SSM+Maven+MySQL+Bootstrap 写的学校教务管理系统去实际部署,最先打脸的往往不是业务逻辑,而是环境。有人把源码包导进去后连 Tomcat 都起不来,反复报 ClassNotFound,最后才发现是 Maven 本地仓库和 JDK 版本没对齐。这个资源覆盖三类角色:管理员能对课程、学生、教师做增删改查,教师能给学生打分,学生能完成选课。它不算花哨,却把 SSM 的核心链路完整走了一遍:Controller 转发、Service 事务、Mapper 写 SQL、JSP 渲染页面。适合正在学 SSM、想拿完整案例练手的人,也适合改造成课设或毕设骨架的同学。下文从选型开始,把环境先配通,再拆数据库表与三个核心流程,最后把最常见的坑逐个排掉。
2. 选型与环境:先把 Maven 拧紧,项目才跑得起来
2.1 五个组件各自管什么,为什么这么配
SSM 不是三个框架的简单叠加,而是一套约定成熟的协作链。Spring 负责对象创建依赖注入,也管事务边界;SpringMVC 负责前端请求到达 Controller、参数绑定、结果转发到 JSP;MyBatis 把 Java 接口和 SQL 映射起来,替代手工写 JDBC 的样板代码。Maven 管的是依赖版本和打包,MySQL 管持久化,Bootstrap 则让后台页面不至于裸奔。很多同学一开始把注意力全放在增删改查上,结果卡在依赖冲突上,所以顺序上我会先讲环境。
| 组件 | 常见版本 | 在工程中的角色 | 最容易出问题的地方 |
|---|---|---|---|
| JDK | 1.8 | 编译与运行环境 | IDE 编译级别和 Maven 的 source/target 不一致 |
| Maven | 3.6.x | 依赖管理与 war 打包 | 本地仓库路径、镜像源、lastUpdated 缓存 |
| MySQL | 5.7 / 8.0 | 数据持久化 | 驱动类名、时区参数、建表字符集 |
| Spring | 5.2.x | IoC 容器与事务 | 传递依赖版本冲突 |
| MyBatis | 3.5.x | SQL 映射 | Mapper XML 是否进入 classpath |
| Bootstrap | 3.x / 4.x | 后台 UI | 静态资源路径和项目 context path |
这套组合为什么到现在还有参考价值?Spring Boot 把什么都自动配好了,反而让一批人说不清 Controller、Service、Mapper 是怎么串起来的。SSM 的优势是配置暴露在明处,你能亲眼看到数据源怎么注入、事务切到哪个 Service 方法、一个请求从 DispatcherServlet 到 JSP 的完整路径。对新手来说,这种“看得见接线”的项目反而比一键生成的工程更能建立系统观。
2.2 把 pom.xml 和 jdbc.properties 先配好
拿到源码包,我一般不会急着点运行按钮,而是把依赖文件先过一遍。pom.xml 里最重要的不是依赖多,而是版本是否互相兼容。SSM 的老资源常常用 Spring 4.x,如果你现在用的是 JDK 17,那基本必翻车。我的建议是保持 JDK 8 + Spring 5.2.x + MyBatis 3.5.x,这是这套架构最稳的区间。
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.22.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>这里的前四行决定了编译基线。maven.compiler.source和 target 都写 1.8,是为了防止 IDEA 默认用当前 JDK 版本去编译老代码,编译时出现source option 7 is no longer supported之类的问题。spring-webmvc 会连带引入 spring-context、spring-web、spring-beans,所以不需要单独逐个添加。mybatis-spring 是把 SqlSessionFactory 交给 Spring 管理的桥梁,没有它,Mapper 接口扫描就无从谈起。mysql-connector-java 的版本要看你的数据库,若是 MySQL 5.7 用 5.1.49 也能跑,但配 MySQL 8.0 就必须用带cj的驱动类。
数据源配置集中在 jdbc.properties 里,编码和时区是我必查的两行。
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/edu_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456useUnicode=true&characterEncoding=utf8解决中文写入乱码,serverTimezone=Asia/Shanghai解决 MySQL 8 的时间偏移报错。老资源里写的是com.mysql.jdbc.Driver,那是 5.x 驱动的写法,换成 8.x 驱动后不改正会导致启动时驱动类加载失败。密码不要用 123456 这种明摆着给别人看的,本地测试无所谓,交付前必须改成环境变量或加密配置。
2.3 Maven 构建命令与配置顺序
配置完依赖,接下来是所有资源落地最容易被忽略的一步:本地仓库。先用mvn -v确认 Maven 版本和仓库位置,再执行构建。
mvn -v mvn clean install -DskipTests mvn dependency:tree -Dincludes=org.springframeworkmvn clean install -DskipTests会先清掉 target,再把项目打成本地包,跳过测试能省下编译测试类的时间。dependency:tree -Dincludes=org.springframework是我用来查版本冲突的常用命令,它会把 spring 相关的依赖树全部列出来。如果看到一个工程里同时出现 spring 4.3 和 spring 5.2,那基本是某个第三方依赖传递进来的,必须用<exclusions>排除,否则启动时会出现各种NoSuchMethodError,这类问题最玄学,看报错完全看不出是版本冲突。
还有一点常见的坑:IDEA 里 File → Project Structure → Project SDK 要选 1.8,Modules 的 Language level 也要对应,Maven 的 Runner 里 JRE 选 1.8。三个地方设置不一致,Maven 编译一个版本、IDEA 编译一个版本,运行时就等着看UnsupportedClassVersionError。这不算系统的 bug,但十个跑不起来的工程里,有五个是栽在这种地方。
3. 数据库设计与工程结构:四张表把教务系统立住
3.1 表结构:学生、教师、课程、选课关系
教务系统的数据模型并不复杂,核心只有四个实体:学生、教师、课程、选课记录。难点在关系:学生和课程是多对多,需要中间表;教师和课程是一对多,课程表里挂一个 teacher_id 外键。很多翻车事故都出在删除动作上,所以建表时我会把外键和唯一索引一并写清楚。
CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL DEFAULT '123456', gender TINYINT DEFAULT 0 COMMENT '0男 1女', class_name VARCHAR(50) COMMENT '班级', phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE teacher ( id INT PRIMARY KEY AUTO_INCREMENT, teacher_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL DEFAULT '123456', title VARCHAR(20) COMMENT '职称', department VARCHAR(50) COMMENT '院系' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT '课程号', name VARCHAR(50) NOT NULL, credit DECIMAL(3,1) NOT NULL, teacher_id INT NOT NULL, capacity INT NOT NULL DEFAULT 60 COMMENT '容量', selected_count INT NOT NULL DEFAULT 0 COMMENT '已选人数', CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE student_course ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2) COMMENT '成绩', selected_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_sc_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_sc_course FOREIGN KEY (course_id) REFERENCES course(id), CONSTRAINT uk_student_course UNIQUE (student_id, course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;四张表里,student_no、teacher_no、course_no都用了业务唯一键,而不是把自增主键暴露给用户。这样做的价值在于:登录页输入学号、导入 Excel 按学号匹配、页面上显示的都是业务号,自增 id 只作为内部外键引用,避免业务数据迁移时 id 变更引起一串外键问题。
student_course表是整个系统最关键的关联表。先删课程再删学生、先删教师再删课程,都必须考虑这张表上的外键。uk_student_course唯一索引是防止重复选课的最后一道防线,哪怕程序里漏判,数据库也能兜住。selected_count是一个冗余字段,目的在于课程列表页直接显示已选人数,不需要每次 join student_course 做 count,查询性能会好很多,但代价是选课和退课的事务里必须同步更新这个字段。
| 字段 | 类型 | 说明 |
|---|---|---|
| score | DECIMAL(5,2) | 0.00 ~ 100.00,教师打分写入 |
| student_id | INT | 学生主键,外键指向 student.id |
| course_id | INT | 课程主键,外键指向 course.id |
| selected_time | DATETIME | 选课时间,默认当前时间 |
| id | INT | 关联记录主键 |
3.2 工程目录与 MyBatis XML 的存放位置
拿到打包好的源码,我首先看目录结构,而不是看代码。SSM 工程的规范程度基本靠目录就能判断。一个标准的分层结构应该长这样:
src ├── main │ ├── java │ │ └── com/sample/edu │ │ ├── controller # 处理请求,返回页面或跳转 │ │ ├── service # 业务逻辑与事务边界 │ │ ├── mapper # MyBatis 接口 │ │ └── entity # 对应的实体类 │ ├── resources │ │ ├── mapper # Mapper XML 文件 │ │ ├── jdbc.properties │ │ └── applicationContext.xml │ └── webapp │ ├── WEB-INF │ │ └── jsp # JSP 页面 │ └── static # Bootstrap、CSS、JS分层的基本原则是 Controller 不直接写 SQL,Service 不直接处理 HttpServletRequest。Controller 只做参数接收和视图转发,Service 负责业务规则和事务,Mapper 接口只声明方法,真正 SQL 在 XML 里。如果某个资源把 SQL 写在 Controller 里,或者 Service 类里大量操作 HttpSession,那这个工程的可维护性基本为零,建议当参考骨架来用而不是直接交付。
Mapper XML 的位置是我每次必查的地方。常见做法是把 XML 放在src/main/resources/mapper目录下,并在 applicationContext.xml 里指定classpath:mapper/*.xml。但有些老工程喜欢把 XML 和接口放在同一个包下,那就必须在 pom.xml 里显式声明资源目录,否则打包后 Mapper XML 不会出现在 classpath 中,运行期会报“Invalid bound statement”。
<build> <resources> <resource> <directory>src/main/resources</directory> </resource> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> </resource> </resources> </build>这段配置把src/main/java下的 XML 也当作资源打入最终产物。很多同学在 IDEA 里跑时没问题,因为 IDEA 编译时会把源码目录下的 xml 拷贝到 target,可一执行mvn clean package打 war 包,到 Tomcat 里就找不到 XML,这就是经典的环境差异。我把这个配置叫作“后悔药”:遇到 Invalid bound statement 时先检查 target/classes 里有没有对应 xml,没有就是资源打包配置的问题。
4. 三个核心流程的实现:增删改查、打分、选课
4.1 管理员对课程的增删改查:完整链路这样串
管理员的课程管理是这套系统里最典型的 CRUD 场景。它的价值不在于三个方法有多难写,而在于你能否分清哪段代码该放在哪一层。Controller 里不该出现 SQL,Service 里不该直接写HttpSession,Mapper XML 里只放最纯粹的 SQL,这是 SSM 面试里最常被考察的点。
@Controller @RequestMapping("/admin/course") public class CourseController { @Autowired private CourseService courseService; @GetMapping("/list") public String list(Model model) { model.addAttribute("courseList", courseService.findAll()); return "admin/course_list"; } @PostMapping("/add") public String add(Course course) { courseService.add(course); return "redirect:/admin/course/list"; } @GetMapping("/delete") public String delete(@RequestParam("id") Integer id) { courseService.deleteById(id); return "redirect:/admin/course/list"; } }@GetMapping和@PostMapping是 Spring 5 的写法,老资源里如果用的是@RequestMapping也可以,但建议统一成这种更明确的映射。Course对象直接作为方法入参,SpringMVC 会把表单里的courseNo、name、credit等字段自动绑定到实体属性,省去手动request.getParameter的重复劳动。redirect:/admin/course/list这个写法是 PRG 模式,POST 提交后重定向到列表页,避免用户刷新浏览器时重复提交表单。
添加课程后,下一步是删除课程的坑。课程被选课后,student_course表里已经有记录,直接 DELETE 会触发外键约束报错。所以 Service 层必须处理关联数据:
@Service public class CourseServiceImpl implements CourseService { @Autowired private CourseMapper courseMapper; @Autowired private StudentCourseMapper studentCourseMapper; @Transactional public void deleteById(Integer courseId) { studentCourseMapper.deleteByCourseId(courseId); courseMapper.deleteByPrimaryKey(courseId); } }@Transactional保证两步操作要么都成功,要么都回滚。如果先删除选课记录、再删除课程,第二步失败了,前面的删除也能回滚,不会出现课程还在但选课记录被清掉的脏数据。Mapper XML 里删除选课记录对应一条简单的语句:
<delete id="deleteByCourseId" parameterType="int"> DELETE FROM student_course WHERE course_id = #{courseId} </delete>4.2 教师打分:更新分数还是保留历史成绩
教师打分功能在这类系统中通常有两种做法:一种是在student_course表里直接更新score字段,另一种是单独建一张成绩表。大多数课设和简单教务系统用的是第一种,因为它够直观,页面也简单。但你要清楚它的边界:如果老师给同一学生多次打分,旧分数会被覆盖,系统不会有任何历史痕迹。
@Transactional public void scoreStudent(Integer studentCourseId, BigDecimal score) { if (score == null || score.compareTo(BigDecimal.ZERO) < 0 || score.compareTo(new BigDecimal("100")) > 0) { throw new RuntimeException("分数必须在0到100之间"); } studentCourseMapper.updateScore(studentCourseId, score); }使用BigDecimal而不是Float或Double,是成绩系统的入门常识。浮点数算 0.1 加 0.2 都会出现精度问题,保存考试成绩时绝不能冒这个险。后端校验必须做,前端只是体验优化,别人绕过页面直接 POST 一个 120 分的成绩,后端无法拦截的话就成了漏洞。
<update id="updateScore" parameterType="map"> UPDATE student_course SET score = #{score} WHERE id = #{studentCourseId} </update>这里parameterType用了map,因为入参是两个值,Controller 把studentCourseId和score传进 Service,Service 再组装成 Map 传给 Mapper。另一种更规范的做法是定义一个 ScoreParam 对象或直接用@Param注解。如果你打算把这个教务系统做成能长期维护的产品,我会建议把成绩历史单独拆一张score_record表,记录分数、时间、录入人,student_course里的 score 只保留当前成绩。那样教师打分就不是覆盖,而是新建记录同时回写当前分数。
4.3 学生选课:唯一索引和事务控制缺一不可
选课是整个系统里最值得花时间看的功能,因为它不仅是简单的 INSERT,还涉及容量校验、重复校验和并发问题。学生端先看到一个课程列表,点选课后提交课程 id 和学生 id,Service 层按“查课程 → 校验容量 → 插入选课记录 → 更新已选人数”的顺序处理。
@Transactional public void enroll(Integer studentId, Integer courseId) { Course course = courseMapper.selectByPrimaryKey(courseId); if (course == null) { throw new RuntimeException("课程不存在"); } if (course.getSelectedCount() >= course.getCapacity()) { throw new RuntimeException("该课程人数已满"); } int result = studentCourseMapper.insert(studentId, courseId); if (result == 0) { throw new RuntimeException("你已经选过这门课,不要重复提交"); } courseMapper.increaseSelectedCount(courseId); }这段逻辑里有三个细节值得注意。第一,先查课程再判断容量,这是一种常见的读取方式,但遇到两个学生同时抢最后一门课的场景,两个请求可能都读到selected_count = 59、capacity = 60,然后都通过校验并 INSERT 成功,最终人数变成 61。真正严谨的做法是对 course 行加SELECT ... FOR UPDATE悲观锁,或者在 UPDATE 已选人数时带上WHERE selected_count < capacity条件。简单系统里未必会做到这一层,但你得知道边界在哪。第二,insert返回 0 的情况依赖数据库层的uk_student_course唯一索引,程序还没判断重复前数据库已经拒绝了重复插入,这是最好的兜底。第三,插入和更新人数必须放在同一个事务里,否则会出现选课记录有了,但列表上人数没变的情况。
对应的 Mapper XML 只有两条语句:
<insert id="insert" parameterType="map"> INSERT INTO student_course(student_id, course_id) VALUES(#{studentId}, #{courseId}) </insert> <update id="increaseSelectedCount" parameterType="int"> UPDATE course SET selected_count = selected_count + 1 WHERE id = #{courseId} </update>increaseSelectedCount用selected_count = selected_count + 1这种原子自增,而不是先查出来再回写,是因为它能在并发下少发生一次脏读。这个细节老工程师一眼就能看出来,面试或者答辩时主动提出来,比背一百遍 MVC 概念有用得多。
5. 避坑与常见问题排查:五条血泪经验
5.1 Maven 仓库配置错了,够你折腾一晚上
现象:项目导入后 IDEA 一直转圈,jar 包下载到一半停止,或者明明本地有依赖还反复报 “Cannot resolve symbol”,执行mvn clean install时提示某个插件找不到。
原因:Maven 默认中央仓库在外网,访问速度不稳定;另一个坑是本地仓库里留有.lastUpdated后缀的失败缓存文件,Maven 看到这个文件会认为依赖下载失败,不再重试。多个镜像源混用时,不同 jar 可能从不同仓库下载,元数据互相冲突,也会出现怪异报错。
解决:打开 Maven 安装目录下conf/settings.xml,配一个稳定镜像源<mirror>,同时检查<localRepository>路径,确保不能带空格。然后删除本地仓库下所有*.lastUpdated文件,重新执行mvn clean compile。如果你不知道本地仓库在哪,命令行里执行mvn help:evaluate -Dexpression=settings.localRepository -q -DforceStdout,它会直接打印路径,省去翻 config 的时间。
5.2 Tomcat 与 JDK 版本不匹配,Tomcat 直接翻车
现象:启动 Tomcat 后控制台一堆ClassNotFoundException: javax.servlet.*或者ClassCastException: jakarta.servlet.ServletRequest cannot be cast,项目页面全是 500。
原因:Spring 5.x 底层用的是javax.servlet命名空间,而 Tomcat 10 之后全面切换到jakarta.servlet命名空间。老 SSM 源码大多是针对 Tomcat 8/9 写的,硬塞进 Tomcat 10 就像把汽油发动机塞进柴油车,接口都不一样。
解决:最简单的方案是用 Tomcat 8.5 或 Tomcat 9.0 运行,JDK 用 1.8。如果你已经在用 Tomcat 10,也不是不能修,但要全局替换javax.servlet为jakarta.servlet,并且把 Spring 升到 6.x,涉及改动面很大。对这种 SSM 老资源,回归到 Tomcat 8.5 是性价比最高的做法,不要盲目追新版本。
5.3 MyBatis 报 Invalid bound statement:XML 根本没进 classpath
现象:项目能启动,Controller 也能跳转,一执行需要查数据库的方法就报org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): ...CourseMapper.findAll。IDE 里直接运行没报错,打成 war 包部署到 Tomcat 才出现。
原因:这基本是 Mapper XML 没有出现在target/classes或 war 包的WEB-INF/classes里。IDE 的增量编译有时会帮把src/main/java下的 xml 一并输出到 target,所以开发环境正常;但mvn clean package是全量构建,没有在 pom.xml 里声明src/main/java下的 xml 为资源,干净构建后它们就被丢弃了。
解决:参考上一章给出的<build><resources>配置,把源码目录下的 xml 也纳入资源打包;同时检查 applicationContext.xml 里的mapperLocations是否写对路径。然后再检查接口的 namespace 是不是填成了mapper.CourseMapper而不是全限定名com.sample.edu.mapper.CourseMapper。用jar tf target/xxx.war | grep xml能直接验证 war 里到底有没有这个文件,这是最快速的定位手段。
5.4 外键约束挡删除:关联数据没清理干净
现象:管理员在页面上删除一门课程或一个教师时,页面报 500,控制台出现Cannot delete or update a parent row: a foreign key constraint fails,甚至提示fk_sc_course。
原因:数据库中确实配置了外键,student_course表里还有记录引用着课程 id。直接删除主表记录,数据库出于完整性拒绝执行。不少课设为了省事不建外键,但资源里既然建了外键,删除操作就必须先处理关联表。
解决:在 Service 里把删除主记录前先删子记录的逻辑补齐,放在同一个事务里。更稳妥的做法是不做物理删除,给课程表加一个status TINYINT字段,删除时执行UPDATE course SET status = 0 WHERE id = ?,所有列表查询强制带WHERE status = 1。逻辑删除的代价是查询要记得过滤,但好处是不会触发外键问题,也能留操作痕迹。如果你要改造这套系统,我建议把课程和教师删除都改成逻辑删除,这是能快速提升系统完成度的低风险改动。
5.5 中文乱码:三个环节的编码必须统一
现象:JSP 页面显示的中文变成??,输入中文保存后数据库里是乱码,从数据库读出来又是乱码,三种位置的表现还不一样。
原因:中文从浏览器到 Tomcat、再到 MySQL,要穿过三条链路。浏览器页面编码、Tomcat 的 URI 编码、JDBC 连接参数、MySQL 表字符集,任何一个环节不是 UTF-8,就会出现乱码。最常见的是 jdbc.url 少了characterEncoding=utf8,或者建表时用了默认的 latin1。
解决:按三层排查。第一层,JSP 页面头部加<%@ page contentType="text/html;charset=UTF-8" %>,静态页面里加<meta charset="UTF-8">。第二层,Tomcat 的 server.xml 里给 Connector 加URIEncoding="UTF-8"。第三层,数据库连接 url 带上useUnicode=true&characterEncoding=utf8,建表统一用utf8mb4。如果项目里已经进了乱码数据,用ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4转一下你会发现旧数据无法自动恢复,所以这个问题的重点是前置预防。顺手在 web.xml 加一个CharacterEncodingFilter强制设置请求编码,几乎能挡住一半的中文问题。
6. 从打包到验收:五步验证这套教务系统真的能跑
拿到资源后我最厌恶的状态是“明明编译过了却不敢交付”。所以我现在有一套固定的验收流程,不管给谁接手我都会走一遍。第一步,执行mvn clean package -DskipTests打出干净 war 包,用ls -lh target/*.war确认产物存在。第二步,用jar tf target/xxx.war | grep 'mapper/.*xml'检查 Mapper XML 是否被打进 war,这步能提前暴露第 5.3 节说的 Invalid bound statement。第三步,把 war 复制到 Tomcat 的 webapps 目录,启动前先确认配置文件里的数据库密码和本地一致。
第四步是核心验证动作,跑一遍完整的业务冒烟用例:
| 验证动作 | 预期结果 |
|---|---|
| 管理员登录 | 进入后台首页,能看菜单 |
| 新增一门课程 | 课程列表出现新记录,学生端课程列表同步可见 |
| 学生选课 | 选课记录新增一条,课程列表已选人数加一 |
| 教师打分 | 分数写回选课记录,列表展示成绩 |
| 删除课程 | 不报外键错误,关联选课记录被清理 |
第五步,故意做一次容错测试:让同一学生重复选同一门课程,页面必须给出提示而不是直接报 500。这条过了,说明数据库唯一索引和 Service 层校验都正常。如果这套系统还要继续扩展,我会建议加一个登录拦截器,在 SpringMVC 配置里注册一个 HandlerInterceptor,拦截所有业务路径,检查 Session 里有没有登录用户,没有就跳回登录页。这个小功能大约二十行代码,能让系统从“能跑”变成“像个产品”。
从那以后,我每次把一套 SSM 工程交付给别人,都会强制走一遍这五步验证流程:看一眼 war 落盘,看一眼 Mapper XML 是否在 war 里,看一遍数据库脚本,再做一轮新增课程、学生选课、教师打分的冒烟测试,最后故意触发一次重复选课确认兜底逻辑存在。这个习惯让我少挨了不少骂,也让很多原本躺在压缩包里的代码真正变成能用的系统。希望帮到你。
本文还有配套的精品资源,点击获取