写在前面
SSM(Spring + 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存用户信息 + 用户角色字段(role=1管理员 / role=0考生)。为什么不用Shiro或者Security?课设项目讲究的是“把核心逻辑讲清楚”,拦截器加角色的方案代码量小、逻辑透明,老师问起来也容易答。当然如果你想更专业一点,也可以引入Shiro,但我觉得对这个体量的项目来说没太大必要。
1.3 数据库设计要点
数据库是这类系统的地基,我初版设计时踩过不少坑,比如直接把“试卷”和“题目”设计成一对多,结果想做“一套卷子在不同时间重复考”的功能时就傻了。后来老老实实拆了中间表,话不多说直接看核心表结构:
用户表(sys_user)
CREATE 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;题目表(exam_question)
CREATE 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 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;试卷表(exam_paper)和试卷题目关系表(exam_paper_question)
CREATE 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_answer)
CREATE 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实现SQL,Entity是数据载体。这里有一个原则:Controller里不要写业务逻辑,Service里不要出现SQL语句。分层清楚,答辩和后期维护都轻松。
2.2 pom.xml 依赖清单
给出核心依赖,版本号都是实际跑通过的,直接抄:
<properties> <spring.version>5.2.9.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> </properties> <dependencies> <!-- Spring核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis与Spring整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> <!-- 数据库连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency> <!-- JSON处理 --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.11.4</version> </dependency> <!-- 验证码 --> <dependency> <groupId>com.github.penggle</groupId> <artifactId>kaptcha</artifactId> <version>2.3.2</version> </dependency> <!-- 工具包 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.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-package="com.exam.controller"/> <mvc:default-servlet-handler/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean>applicationContext.xml管业务和数据层:扫描Service和DAO、配置Druid数据源、配置SqlSessionFactoryBean、配置MapperScannerConfigurer、开启事务管理。
<context:component-scan base-package="com.exam.service"/> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mybatis/mapper/*.xml"/> <property name="typeAliasesPackage" value="com.exam.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.exam.dao"/> </bean> <tx:annotation-driven/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean>web.xml是总入口,负责加载这两个配置文件、配置Druid的Servlet(监控页面)和字符编码过滤器。
注意:
jdbc.url里连接MySQL 8以上必须加时区参数,比如jdbc:mysql://localhost:3306/exam_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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;charset=utf-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;charset=utf-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 class="com.exam.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这个方案有个典型坑:如果路径放行配置没写好,会出现“验证码能显示、CSS加载不出来”的情况,一个原因是静态资源被拦截了。把mvc:default-servlet-handler和排除映射都配上,基本能解决。
3.3 模拟考试:组卷与自动判分的实现
自动组卷是核心业务。我提供两种模式:
模式一:随机按比例抽题调用方传入试卷ID,随机抽取题目,先按题型分组,再从每组中按比例选数量。
@Override public List<Question> generateRandomPaper(Integer paperId) { ExamPaper paper = paperMapper.selectById(paperId); List<Question> questions = new ArrayList<>(); // 按题型分组随机抽取,例如:10道单选、5道多选、5道判断 Integer singleCount = 10; List<Question> singles = questionMapper.selectRandomByType(1, singleCount); Integer multiCount = 5; List<Question> multis = questionMapper.selectRandomByType(2, multiCount); Integer judgeCount = 5; List<Question> judges = questionMapper.selectRandomByType(3, judgeCount); questions.addAll(singles); questions.addAll(multis); questions.addAll(judges); return questions; }对应的Mapper SQL:
<select id="selectRandomByType" resultType="com.exam.entity.Question"> SELECT * FROM exam_question WHERE question_type = #{type} ORDER BY RAND() LIMIT #{count} </select>
ORDER BY RAND()在题目量超过几千条时性能会下降,但课设数据量几百上千条规模完全没压力。真要做优化,可以用 “MAX(ID) * RAND()” 或洗牌算法,但别过度设计。
模式二:固定卷在exam_paper_question表中配置好题目ID和分值,按sort_order排序查询即可。这种模式适合做“真题卷”。
交卷自动判分是考试系统的核心体验。设计思路是:考生每答一题,前端立刻把答案写入exam_record_answer(草稿状态),交卷时统一算分。
@Transactional public int submitExam(Integer recordId, Map<Integer, String> answers) { ExamRecord record = examRecordMapper.selectById(recordId); if (record == null || record.getStatus() == 1) { throw new RuntimeException("试卷不存在或已交卷"); } // 获取该试卷所有题目(含正确答案和分值) List<PaperQuestionVO> 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) { // 多选:无序比对 Set<String> userSet = new HashSet<>(Arrays.asList(userAnswer.split(","))); Set<String> correctSet = new HashSet<>(Arrays.asList(correctAnswer.split(","))); return userSet.equals(correctSet); } return userAnswer.trim().equalsIgnoreCase(correctAnswer.trim()); }如果需支持漏选得分,就按“得分 = 正确交集数 / 标准答案数 × 题目分值”算,但这样逻辑复杂一点,课设用“全对得分”最清晰。
3.4 考试过程中的状态控制
很多人在考试模块做得太“静态”:进考场把题全部查出来渲染在页面上,交卷时挨个比对。这样有两个问题:一是答题进度没法中途保存,刷新就全丢了;二是倒计时误差大。
我的做法是:
- 进入考试时,先创建一条
exam_record记录(status=0),拿到recordId。 - 每答一题,前端通过
navigator.sendBeacon或者Ajax把答案存到后台草稿表。这个防刷新丢答案。 - 前端倒计时用
sessionStorage记录剩余时间起点,而不是依赖服务器时间。交卷时再把耗时和服务端时间做一次校准。 - 考试中接口防重入:同一个用户同一套试卷只能有一条status=0的记录。如果已有未交卷记录,直接恢复那场考试。
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 id="selectScoreDistribution" resultType="map"> 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 Tomcat,Deployment里加
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.BeanCreationException | spring-mvc.xml和applicationContext.xml里的扫描包范围有重合或漏配置 | 检查component-scan路径,确保Controller和Service分别在对应配置文件中被扫描到;观察异常信息是否指向某个Bean,再反向查配置 |
| 访问接口404,但页面能打开 | 请求的URL和@RequestMapping不一致,或忘了加@RequestBody/@ResponseBody | 打开浏览器F12看Network里的实际请求URL,对照Controller里的映射路径;加了@RestController就不需要每个方法都写@ResponseBody |
MyBatis报Invalid bound statement (not found) | Mapper接口和Mapper.xml没有绑定成功 | 确认接口全限定名和XML的namespace一致;确认mapperLocations配置的路径和XML实际位置一致 |
| 查询中文乱码 | 数据库连接URL没带编码参数,或页面编码不对 | 连接URL加useUnicode=true&characterEncoding=utf8;检查JSP/HTML的charset=utf-8,并在web.xml配置CharacterEncodingFilter |
交卷时java.sql.SQLException: Incorrect string value | 答案/题干中存储了emoji或特殊字符,而表是utf8 | 建库建表统一用utf8mb4,连接URL加characterEncoding=utf8。后续数据入库没问题 |
| Druid监控页能打开,但数据源报timezone错误 | MySQL 8与旧驱动的时区处理差异 | JDBC URL加serverTimezone=Asia/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 Boot:SSM整合逻辑迁移到Spring Boot其实就是“删配置、加starter”,数据层和前端基本不用动。对想学Spring Boot的人来说,这是一次很好的对照学习。
- 引入Redis:把验证码、Session、热点排行榜数据放进Redis,顺便学习缓存穿透和过期策略。
- 增加Excel批量导入题库:用POI或EasyExcel解析xlsx,管理端上传表格一键导入几百道题。
- 增加在线阅卷:主观题答案用文本域采集,后台人工打分,总分=客观分+主观分。
- 前端升级:Vue3 + Element Plus 做单页应用,后端只出JSON,顺便把前后端分离的姿势学会。
从就业角度看,这个项目覆盖了SSM、MySQL、Maven、Tomcat、ECharts、JSON交互这些经典技术点,写在简历的项目经历里,面试官问“讲一个你最熟悉的项目”,你能把从数据库建模到权限控制再到考试状态机一条线讲清楚,就已经赢了很多人。
最后分享一个我的建议
这种管理系统项目,真正拉开差距的往往不是页面长什么样,而是你有没有把“边界情况”想清楚:用户重复交卷怎么办、考试超时怎么处理、多选题答案顺序不同怎么判、历史成绩会不会被后续修改的题目影响。这些细节,做过了才知道坑在哪。
如果你正在赶课设,我的建议是从数据库脚本和登录模块入手跑通全流程,再加考试模块,不要一上来就想所有功能全做完。一个能跑通的半成品,永远比一堆没跑通的代码有价值。项目代码的每个模块我都尽量按清晰分层来写,直接照着结合你自己的需求改,速度会比从零开始快很多。