
写在前面SSMSpring Spring MVC MyBatis考试模拟系统算得上Java Web课程设计里的“常青树”项目。最近整理代码库翻出了一套之前帮学生改过的“资格认证考试模拟系统”正好是SSM架构带用户端、后台管理端、题库刷题、模拟考试、自动判分、成绩统计这些完整闭环直接作为课设交上去完全够用也能当毕业设计的前身版本。这套系统我前前后后改过好几版也踩了不少坑把核心代码结构和能直接复现的细节整理出来给正在做课设、准备毕设或者想用传统SSM练一遍Java Web基本功的朋友参考。这套东西的价值不在于页面多炫而在于技术栈经典、功能闭环完整、可扩展性强。如果你是那种“项目文档都读完了但真正动手从零整合SSM框架还是会卡住”的人这篇文章就是照着抄作业的路子。下面会从整体设计、数据库建模、核心模块实现、部署排坑几个角度拆开讲代码层面能给的都会给到。1. 系统整体设计与技术选型1.1 为什么选SSM而不是Spring Boot先聊一个很多人纠结的问题现在新项目都直接Spring Boot了课设为什么还要用SSM原因很现实很多高校的Java Web课程和课设要求里明确写了“基于SSM框架”。而且从学习角度讲SSM是理解Spring Boot的“前置课”——Spring Boot的本质是“约定大于配置”把Spring MVC和MyBatis的繁琐配置自动化了。但如果你连Spring容器怎么装配Bean、Spring MVC的DispatcherServlet怎么拦截请求、MyBatis的Mapper怎么和接口绑定都没见过直接上手Spring Boot出了问题会非常被动。这套系统用的组合是Spring 5.x Spring MVC 5.x MyBatis 3.x MySQL 5.7 Maven Tomcat 8。没有引入过多花哨组件就靠原生的SSM整合把整个请求链路跑通浏览器发起请求 → DispatcherServlet接收 → HandlerMapping找到Controller → Service处理业务 → Mapper操作数据库 → 返回JSON → 前端渲染。这套链路跑通一遍Spring IoC、AOP、MVC核心组件、MyBatis动态SQL这些知识点基本全都覆盖了答辩的时候老师怎么问都不慌。1.2 功能模块怎么拆这类考试模拟系统的业务逻辑其实高度相似核心都是“考生 → 题库 → 试卷 → 考试 → 成绩”。我按角色拆成两大端每端再按业务域细分这样代码结构清晰也方便后面扩展角色功能域核心功能点普通用户考生账号与登录注册、登录、密码MD5加盐存储、验证码校验普通用户考生模拟考试按科目/题型随机组卷、倒计时、交卷自动判分普通用户考生题目练习分科目刷题、答题反馈、错题本回顾普通用户考生成绩中心历史成绩查询、成绩趋势统计、试卷详情回顾管理员系统管理用户管理启用/禁用、数据字典维护管理员题库管理题目增删改查、按题型/难度筛选、批量导入管理员试卷管理手动组卷/随机组卷、试卷上下线、考试时长设置管理员成绩统计参考人数、平均分、及格率、分数段分布权限模型这块没有用Spring Security而是用了最简单可靠的方案一个基于HandlerInterceptor的登录拦截器 Session存用户信息 用户角色字段role1管理员 / role0考生。为什么不用Shiro或者Security课设项目讲究的是“把核心逻辑讲清楚”拦截器加角色的方案代码量小、逻辑透明老师问起来也容易答。当然如果你想更专业一点也可以引入Shiro但我觉得对这个体量的项目来说没太大必要。1.3 数据库设计要点数据库是这类系统的地基我初版设计时踩过不少坑比如直接把“试卷”和“题目”设计成一对多结果想做“一套卷子在不同时间重复考”的功能时就傻了。后来老老实实拆了中间表话不多说直接看核心表结构用户表sys_userCREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 用户名/学号, password VARCHAR(100) NOT NULL COMMENT MD5加盐密码, salt VARCHAR(32) DEFAULT NULL COMMENT 盐值, real_name VARCHAR(50) DEFAULT NULL COMMENT 姓名, role TINYINT DEFAULT 0 COMMENT 角色 0-考生 1-管理员, status TINYINT DEFAULT 1 COMMENT 账号状态 1-正常 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;题目表exam_questionCREATE TABLE exam_question ( id INT PRIMARY KEY AUTO_INCREMENT, subject_id INT NOT NULL COMMENT 所属科目, question_type TINYINT NOT NULL COMMENT 1-单选 2-多选 3-判断, difficulty TINYINT DEFAULT 1 COMMENT 难度 1简单 2中等 3困难, content TEXT NOT NULL COMMENT 题干, options TEXT COMMENT 选项JSON串, answer VARCHAR(20) NOT NULL COMMENT 标准答案, analysis TEXT COMMENT 答案解析, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;试卷表exam_paper和试卷题目关系表exam_paper_questionCREATE TABLE exam_paper ( id INT PRIMARY KEY AUTO_INCREMENT, paper_name VARCHAR(100) NOT NULL, subject_id INT NOT NULL, duration INT DEFAULT 60 COMMENT 考试时长分钟, total_score INT DEFAULT 100, status TINYINT DEFAULT 0 COMMENT 0-未发布 1-已发布, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE exam_paper_question ( id INT PRIMARY KEY AUTO_INCREMENT, paper_id INT NOT NULL, question_id INT NOT NULL, score INT DEFAULT 5 COMMENT 该题分值, sort_order INT DEFAULT 0 COMMENT 题目顺序 );考试记录表exam_record和答题明细表exam_record_answerCREATE TABLE exam_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, paper_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, score DECIMAL(5,1) DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0-考试中 1-已交卷, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE exam_record_answer ( id INT PRIMARY KEY AUTO_INCREMENT, record_id INT NOT NULL, question_id INT NOT NULL, user_answer VARCHAR(20) DEFAULT NULL, is_correct TINYINT DEFAULT NULL COMMENT 0-错误 1-正确, score DECIMAL(5,1) DEFAULT 0 );有几个设计细节值得说一下试卷题目关系表必须单独建。这样一套试卷可以稳定复考同一个题目也能出现在多套试卷里典型多对多关系。答案选项用JSON串存储不要为每个选项单独建列。因为题目类型会有调整比如判断题只有A/B两个选项JSON方案灵活Java端用Jackson或者Fastjson一解析就行。答题明细表里的score字段要冗余一个快照值。因为题目正确答案或者分值以后可能会改但历史考试记录不能跟着变否则成绩就对不上了。这就是“写时快照”思维虽然课设阶段不一定暴露问题但答辩时说出来会很加分。2. 环境搭建与工程骨架2.1 一个能直接跑起来的SSM项目结构很多人卡住不是不会写代码而是不知道项目结构该怎么摆。我用的标准Maven war包结构直接照着建就行exam-system ├── pom.xml ├── src/main/java │ └── com/exam │ ├── controller │ │ ├── UserController.java │ │ ├── QuestionController.java │ │ ├── PaperController.java │ │ ├── ExamController.java │ │ └── AdminController.java │ ├── service │ │ ├── UserService.java │ │ ├── QuestionService.java │ │ ├── PaperService.java │ │ ├── ExamService.java │ │ └── StatsService.java │ ├── dao │ │ ├── UserMapper.java │ │ ├── QuestionMapper.java │ │ ├── PaperMapper.java │ │ ├── ExamRecordMapper.java │ │ └── ... │ ├── entity │ │ ├── User.java │ │ ├── Question.java │ │ ├── Paper.java │ │ └── ... │ ├── interceptor │ │ └── LoginInterceptor.java │ ├── common │ │ ├── Result.java │ │ └── PageResult.java │ └── util │ ├── Md5Util.java │ └── JwtUtil.java (选配) ├── src/main/resources │ ├── spring │ │ ├── applicationContext.xml │ │ └── spring-mvc.xml │ ├── mybatis │ │ ├── mybatis-config.xml │ │ └── mapper │ │ ├── UserMapper.xml │ │ ├── QuestionMapper.xml │ │ └── ... │ ├── jdbc.properties │ └── log4j.properties └── src/main/webapp ├── WEB-INF │ └── web.xml └── static ├── css ├── js └── pagesController层只负责参数接收和结果返回Service层做事务边界DAO层用MyBatis接口XML实现SQLEntity是数据载体。这里有一个原则Controller里不要写业务逻辑Service里不要出现SQL语句。分层清楚答辩和后期维护都轻松。2.2 pom.xml 依赖清单给出核心依赖版本号都是实际跑通过的直接抄properties spring.version5.2.9.RELEASE/spring.version mybatis.version3.5.6/mybatis.version /properties dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency 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/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdcom.github.pagehelper/groupId artifactIdpagehelper/artifactId version5.2.0/version /dependency !-- 数据库连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.26/version /dependency !-- JSON处理 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.11.4/version /dependency !-- 验证码 -- dependency groupIdcom.github.penggle/groupId artifactIdkaptcha/artifactId version2.3.2/version /dependency !-- 工具包 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.11/version /dependency /dependencies分页插件PageHelper强烈建议加上课设里列表页太多手写LIMIT烦且容易出错。用PageHelper一行开启分页PageHelper.startPage(pageNum, pageSize);后面紧跟的查询就自动带上了LIMIT非常省事。2.3 配置文件的核心逻辑SSM整合之所以让新手头大就是三个配置文件职责要拎清spring-mvc.xml只管Web层开启Spring MVC注解驱动、扫描Controller包、配置视图解析器、配置静态资源放行、配置JSON消息转换器。mvc:annotation-driven/ context:component-scan base-packagecom.exam.controller/ mvc:default-servlet-handler/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/pages// property namesuffix value.jsp/ /beanapplicationContext.xml管业务和数据层扫描Service和DAO、配置Druid数据源、配置SqlSessionFactoryBean、配置MapperScannerConfigurer、开启事务管理。context:component-scan base-packagecom.exam.service/ context:property-placeholder locationclasspath:jdbc.properties/ bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName value${jdbc.driver}/ property nameurl value${jdbc.url}/ property nameusername value${jdbc.username}/ property namepassword value${jdbc.password}/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mybatis/mapper/*.xml/ property nametypeAliasesPackage valuecom.exam.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.exam.dao/ /bean tx:annotation-driven/ bean idtransactionManager classorg.springframework.jdbc.datasource.DataSourceTransactionManager property namedataSource refdataSource/ /beanweb.xml是总入口负责加载这两个配置文件、配置Druid的Servlet监控页面和字符编码过滤器。注意jdbc.url里连接MySQL 8以上必须加时区参数比如jdbc:mysql://localhost:3306/exam_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai。不加这个连数据库时会被 “The server time zone value” 的报错卡到怀疑人生。3. 核心功能实现与关键代码3.1 登录与验证码核心链路演示登录逻辑看似简单但有几个细节能拉开差距。使用Kaptcha生成验证码配置一个验证码Servlet然后在登录页展示。核心校验代码如下Controller public class UserController { Resource private UserService userService; PostMapping(/doLogin) ResponseBody public Result doLogin(String username, String password, String verifyCode, HttpSession session) { // 1. 验证码校验 Object sessionCode session.getAttribute(kaptchaCode); if (sessionCode null || !sessionCode.toString().equalsIgnoreCase(verifyCode)) { return Result.error(验证码错误); } // 验证码用完即焚防止重复利用 session.removeAttribute(kaptchaCode); // 2. 查询用户service里做加盐和MD5比对 User loginUser userService.login(username, password); if (loginUser null) { return Result.error(用户名或密码错误); } if (loginUser.getStatus() 0) { return Result.error(账号已被禁用); } // 3. 写入Session拦截器以此判断登录状态 session.setAttribute(loginUser, loginUser); session.setMaxInactiveInterval(30 * 60); // 30分钟超时 return Result.success(登录成功, loginUser); } }密码存储用的是MD5加盐注册时生成一个随机盐值存密码的MD5(密码盐)。为什么不能存明文因为哪怕只是课程设计只要涉及用户数据就必须养成“不存明文密码”的习惯这个点答辩时也是亮点。public class Md5Util { public static String md5WithSalt(String password, String salt) { String raw password salt; // 实际可用DigestUtils.md5Hex(raw)或自写MessageDigest return DigestUtils.md5Hex(raw); } public static String generateSalt() { return UUID.randomUUID().toString().replace(-, ).substring(0, 8); } }3.2 登录拦截器权限控制的低成本方案写一个HandlerInterceptor在preHandle方法里判断Session是否有登录用户再判断访问路径是否属于管理员接口public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User loginUser (User) session.getAttribute(loginUser); if (loginUser null) { // 前端Ajax请求和页面请求区分处理 if (XMLHttpRequest.equals(request.getHeader(X-Requested-With))) { response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:401,\msg\:\未登录\}); } else { response.sendRedirect(request.getContextPath() /login.html); } return false; } // 管理员接口校验 String uri request.getRequestURI(); if (uri.contains(/admin/) loginUser.getRole() ! 1) { response.setContentType(application/json;charsetutf-8); response.getWriter().write({\code\:403,\msg\:\无权限\}); return false; } return true; } }这个拦截器在spring-mvc.xml里注册注意要放行登录、注册、静态资源这几个路径mvc:interceptors mvc:interceptor mvc:mapping path/**/ mvc:exclude-mapping path/login.html/ mvc:exclude-mapping path/doLogin/ mvc:exclude-mapping path/register/ mvc:exclude-mapping path/css/**/ mvc:exclude-mapping path/js/**/ mvc:exclude-mapping path/images/**/ mvc:exclude-mapping path/kaptcha/**/ bean classcom.exam.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors这个方案有个典型坑如果路径放行配置没写好会出现“验证码能显示、CSS加载不出来”的情况一个原因是静态资源被拦截了。把mvc:default-servlet-handler和排除映射都配上基本能解决。3.3 模拟考试组卷与自动判分的实现自动组卷是核心业务。我提供两种模式模式一随机按比例抽题调用方传入试卷ID随机抽取题目先按题型分组再从每组中按比例选数量。Override public ListQuestion generateRandomPaper(Integer paperId) { ExamPaper paper paperMapper.selectById(paperId); ListQuestion questions new ArrayList(); // 按题型分组随机抽取例如10道单选、5道多选、5道判断 Integer singleCount 10; ListQuestion singles questionMapper.selectRandomByType(1, singleCount); Integer multiCount 5; ListQuestion multis questionMapper.selectRandomByType(2, multiCount); Integer judgeCount 5; ListQuestion judges questionMapper.selectRandomByType(3, judgeCount); questions.addAll(singles); questions.addAll(multis); questions.addAll(judges); return questions; }对应的Mapper SQLselect idselectRandomByType resultTypecom.exam.entity.Question SELECT * FROM exam_question WHERE question_type #{type} ORDER BY RAND() LIMIT #{count} /selectORDER BY RAND()在题目量超过几千条时性能会下降但课设数据量几百上千条规模完全没压力。真要做优化可以用 “MAX(ID) * RAND()” 或洗牌算法但别过度设计。模式二固定卷在exam_paper_question表中配置好题目ID和分值按sort_order排序查询即可。这种模式适合做“真题卷”。交卷自动判分是考试系统的核心体验。设计思路是考生每答一题前端立刻把答案写入exam_record_answer草稿状态交卷时统一算分。Transactional public int submitExam(Integer recordId, MapInteger, String answers) { ExamRecord record examRecordMapper.selectById(recordId); if (record null || record.getStatus() 1) { throw new RuntimeException(试卷不存在或已交卷); } // 获取该试卷所有题目含正确答案和分值 ListPaperQuestionVO paperQuestions paperMapper.selectQuestionsByPaper(record.getPaperId()); int totalScore 0; for (PaperQuestionVO pq : paperQuestions) { String userAnswer answers.get(pq.getQuestionId()); if (userAnswer null || userAnswer.isEmpty()) { continue; // 没作答0分 } // 判断题/单选题直接等值比对多选题需要拆分后比对集合 boolean correct checkAnswer(userAnswer, pq.getAnswer(), pq.getQuestionType()); if (correct) { totalScore pq.getScore(); } ExamRecordAnswer answer new ExamRecordAnswer(); answer.setRecordId(recordId); answer.setQuestionId(pq.getQuestionId()); answer.setUserAnswer(userAnswer); answer.setIsCorrect(correct ? 1 : 0); answer.setScore(correct ? pq.getScore() : 0); examRecordAnswerMapper.insert(answer); } // 更新考试记录 record.setScore(totalScore); record.setStatus(1); record.setEndTime(new Date()); examRecordMapper.update(record); return totalScore; }多选题判分是个细节坑用户答案是“A,C,D”标准答案是“A,B,D”就不能简单equals。我的做法是把两边逗号拆成Set再比对同时注意顺序差异private boolean checkAnswer(String userAnswer, String correctAnswer, Integer type) { if (type 2) { // 多选无序比对 SetString userSet new HashSet(Arrays.asList(userAnswer.split(,))); SetString correctSet new HashSet(Arrays.asList(correctAnswer.split(,))); return userSet.equals(correctSet); } return userAnswer.trim().equalsIgnoreCase(correctAnswer.trim()); }如果需支持漏选得分就按“得分 正确交集数 / 标准答案数 × 题目分值”算但这样逻辑复杂一点课设用“全对得分”最清晰。3.4 考试过程中的状态控制很多人在考试模块做得太“静态”进考场把题全部查出来渲染在页面上交卷时挨个比对。这样有两个问题一是答题进度没法中途保存刷新就全丢了二是倒计时误差大。我的做法是进入考试时先创建一条exam_record记录status0拿到recordId。每答一题前端通过navigator.sendBeacon或者Ajax把答案存到后台草稿表。这个防刷新丢答案。前端倒计时用sessionStorage记录剩余时间起点而不是依赖服务器时间。交卷时再把耗时和服务端时间做一次校准。考试中接口防重入同一个用户同一套试卷只能有一条status0的记录。如果已有未交卷记录直接恢复那场考试。public ExamRecord startOrResumeExam(Integer userId, Integer paperId) { ExamRecord record examRecordMapper.selectActiveRecord(userId, paperId); if (record null) { record new ExamRecord(); record.setUserId(userId); record.setPaperId(paperId); record.setStartTime(new Date()); record.setStatus(0); examRecordMapper.insert(record); } return record; }这个细节在答辩时如果被问到“考生中途退出怎么办”你只要答出来“支持断点续考”老师基本就满意了。3.5 成绩统计与可视化成绩报表不要做得太复杂但至少要有一个统计页。我用ECharts画出两个图参考人数趋势折线图、分数段占比饼图。后端的统计SQL!-- 各分数段人数统计 -- select idselectScoreDistribution resultTypemap SELECT SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS excellent, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) AS good, SUM(CASE WHEN score 60 AND score 80 THEN 1 ELSE 0 END) AS pass, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS fail FROM exam_record WHERE paper_id #{paperId} /select有一点要注意SQL别名别用中文。虽然MySQL支持但Java端用Map接收时中文key容易踩编码坑。我改成了英文别名excellent、good、pass、fail前端做映射显示中文。4. 完整部署步骤与常见报错排查4.1 从零到跑通的部署记录这套系统从IDEA到浏览器能访问按下面几步走总共也就十分钟的事创建数据库exam_db用Navicat或命令行执行项目里的init.sql。注意执行前先SET NAMES utf8mb4防止建表注释乱码。修改jdbc.properties把数据库名、用户名、密码换成自己的。IDEA配置Tomcat选择Local TomcatDeployment里加war exploded类型Application context填/exam。启动项目访问http://localhost:8080/exam/login.html。如果看到登录页说明基本环境OK。用初始化SQL里自带的账号一般是admin/123456登录后台。如果是打包成war部署到独立Tomcat则用mvn clean package生成.war丢到Tomcat的webapps目录启动后自动解压部署。4.2 高频报错定位与解决下面这些是我多次见到的错误按出现频率排个序并给出排查路径报错现象根因解决方式启动时报org.springframework.beans.factory.BeanCreationExceptionspring-mvc.xml和applicationContext.xml里的扫描包范围有重合或漏配置检查component-scan路径确保Controller和Service分别在对应配置文件中被扫描到观察异常信息是否指向某个Bean再反向查配置访问接口404但页面能打开请求的URL和RequestMapping不一致或忘了加RequestBody/ResponseBody打开浏览器F12看Network里的实际请求URL对照Controller里的映射路径加了RestController就不需要每个方法都写ResponseBodyMyBatis报Invalid bound statement (not found)Mapper接口和Mapper.xml没有绑定成功确认接口全限定名和XML的namespace一致确认mapperLocations配置的路径和XML实际位置一致查询中文乱码数据库连接URL没带编码参数或页面编码不对连接URL加useUnicodetruecharacterEncodingutf8检查JSP/HTML的charsetutf-8并在web.xml配置CharacterEncodingFilter交卷时java.sql.SQLException: Incorrect string value答案/题干中存储了emoji或特殊字符而表是utf8建库建表统一用utf8mb4连接URL加characterEncodingutf8。后续数据入库没问题Druid监控页能打开但数据源报timezone错误MySQL 8与旧驱动的时区处理差异JDBC URL加serverTimezoneAsia/Shanghai并确认驱动版本是mysql-connector-java 8.x有一个很容易被忽略的坑IDEA的Tomcat热部署常常不生效。改完Mapper.xml后经常出现“改了SQL但不起作用”的情况这是MyBatis的XML没有重新加载。我的土办法是每次改完XML重启一下Tomcat不要信什么热部署稳才是效率。4.3 性能和小细节优化数据量不大不代表可以不优化有些习惯在课设里养成很有价值列表页查询必须加上合理索引。比如exam_record表的user_id、paper_id字段加普通索引成绩查询会快很多。考试记录表数据会积累分页查询时不要SELECT *只查出需要展示的列。Session存用户对象时不要存密码字段设成null再存这是安全习惯。数据库连接池用Druid并在druid监控页面里看看慢SQL答辩时能说出“我通过Druid监控发现了某条慢查询并做了索引优化”这种话非常加分。5. 答辩亮点与扩展方向建议5.1 如果老师问“你的项目有什么亮点”我建议从以下角度准备随口就能答出东西来答题草稿与断点续考考试过程中刷新不丢答案靠的是做题过程实时写库而不是交卷时一次性采集。试卷与题目的多对多建模一套卷子可以复考题目可以复用历史和最新数据通过快照分值隔离。安全细节MD5加盐存储、验证码一次性失效、Session超时设置、拦截器做权限控制。代码分层Controller只做参数解析、Service管业务事务、DAO专注SQL扩展时改动范围小。这几个点拿出来哪怕代码本身不算复杂答辩的“技术深度”评分也会上去。5.2 这套系统的可扩展迁移路径这类考试模拟系统的价值在于可以轻松换皮。把“考试科目、考生来源、题目内容”换掉它就变成各种资格认证考试模拟系统。如果后续想提升可以往这几个方向走升级Spring BootSSM整合逻辑迁移到Spring Boot其实就是“删配置、加starter”数据层和前端基本不用动。对想学Spring Boot的人来说这是一次很好的对照学习。引入Redis把验证码、Session、热点排行榜数据放进Redis顺便学习缓存穿透和过期策略。增加Excel批量导入题库用POI或EasyExcel解析xlsx管理端上传表格一键导入几百道题。增加在线阅卷主观题答案用文本域采集后台人工打分总分客观分主观分。前端升级Vue3 Element Plus 做单页应用后端只出JSON顺便把前后端分离的姿势学会。从就业角度看这个项目覆盖了SSM、MySQL、Maven、Tomcat、ECharts、JSON交互这些经典技术点写在简历的项目经历里面试官问“讲一个你最熟悉的项目”你能把从数据库建模到权限控制再到考试状态机一条线讲清楚就已经赢了很多人。最后分享一个我的建议这种管理系统项目真正拉开差距的往往不是页面长什么样而是你有没有把“边界情况”想清楚用户重复交卷怎么办、考试超时怎么处理、多选题答案顺序不同怎么判、历史成绩会不会被后续修改的题目影响。这些细节做过了才知道坑在哪。如果你正在赶课设我的建议是从数据库脚本和登录模块入手跑通全流程再加考试模块不要一上来就想所有功能全做完。一个能跑通的半成品永远比一堆没跑通的代码有价值。项目代码的每个模块我都尽量按清晰分层来写直接照着结合你自己的需求改速度会比从零开始快很多。