ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SSM框架实战:在线考试系统从零到部署全解析

SSM框架实战:在线考试系统从零到部署全解析 简介这是一套面向计算机专业本科生的SSM架构毕业设计级学生在线考试系统适用于期末大作业、课程设计及毕业设计场景帮助学习者掌握企业级Web应用开发全流程。资源包共7008个文件涵盖3798张系统界面与分析图含用例图、ER图、数据库表设计图、916个前端页面HTML/JSP、816个样式文件CSS、530个交互脚本JS、69个核心Java业务类如ExamServiceImpl、ExamController等以及配套的XML配置、SQL建库脚本、Redis缓存配置和Shiro权限控制代码整体压缩包达763.9MB。已有1068人学习下载资源附带详细运行指导视频MP4、全套开发工具安装包含MySQL5.7、Redis5.0.5、Tomcat8.5等所有Java类均含完整注释模块划分清晰权限控制与考试流程逻辑完备是理解SSM整合、Shiro安全框架与Redis缓存实践的高质量参考实现。1. 项目概述与整体设计思路1.1 为什么还在做SSM项目而不是直接上Spring Boot先开门见山说结论SSMSpring Spring MVC MyBatis这套组合虽然看起来“老”但它依然是理解Java Web后端技术原理的最佳切入路径。这几年我带过不少新人很多人一上来就学Spring Boot跟着视频敲了几个demo能跑起来就觉得自己会了。结果面试一问DispatcherServlet怎么工作的、MyBatis的Mapper代理是怎么生成的、Spring的事务拦截器在哪一层生效基本都答不上来。而SSM项目的好处恰恰在于它逼你去面对那些被Spring Boot“藏起来”的细节。比如你要手动配置web.xml、要自己写SqlMapConfig.xml的Mapper扫描路径、要理解Spring容器和Spring MVC容器之间的父子关系。这些脏活累活在Spring Boot里全被自动配置替代了但如果你没亲手配过一遍出了问题排查起来就会很被动。那“学生在线考试系统”这个选题为什么经典因为它麻雀虽小、五脏俱全业务上天然覆盖了Web开发最常见的几大类问题用户体系学生、教师、管理员三类角色权限不能乱串数据关联题目、试卷、考试记录、成绩涉及多表关联查询核心业务逻辑自动组卷、定时交卷、客观题自动判分、主观题人工复核状态与并发同一场考试多个学生同时交卷数据库写入不能乱前后端交互考试倒计时、答题缓存、异常断线恢复这些交互细节特别考验接口设计。换句话说做完这个项目你不是只会“CRUD搬运工”而是把Spring的IOC/AOP、MyBatis的动态SQL、Spring MVC的请求流转、以及数据库设计的基本功全部串起来了。这也是我至今仍推荐拿它当练手项目的原因。1.2 系统角色与核心功能模块拆解在动代码之前先花时间想清楚权限边界和功能边界这一步能帮你省掉后期大量的返工。我们这个系统的角色分三类账号体系建议做在一张表里用role字段区分而不是傻乎乎建三张用户表。学生端登录、查看考试列表只显示已发布且在有效期内的考试、进入考试答题倒计时、题面渲染、交卷自动判分后立即看到客观题得分、查看历史成绩和试卷详情含正确答案对照。教师端题库管理单选、多选、判断题的CRUD、手动组卷按题型和知识点筛选题目选完生成试卷、自动组卷按难度比例和题型数量随机抽题、阅卷主观题打分、考试成绩导出。管理员端用户管理教师和学生账号的创建、禁用、重置密码、考试发布管理安排考试时间、时长、监考策略、数据统计参考人数、平均分、及格率、分数段分布。模块拆到这里数据库的表结构其实已经能推出来了。我把表结构设计放在下一节细讲但这里想先强调一个容易被忽略的点“试卷”和“考试”要拆成两个概念。试卷是一组题目的集合可以被复用考试是一次具体的活动安排包含起止时间、时长、参加学生范围、关联哪份试卷。很多第一次做这个系统的人会混在一起结果后面想“同一套卷子安排两场考试”时就傻眼了。1.3 技术选型的理由与备选方案对比技术组件本项目选择选型理由备选方案后端框架Spring Spring MVC学习价值高手动配置能理解Web应用底层机制Spring Boot配置少但原理容易被忽略ORM框架MyBatis动态SQL灵活适合题库这种多条件组合查询场景MyBatis-Plus省事但不利于理解XML绑定前端方案AJSP JSTL Bootstrap传统方案适合快速出效果、学习成本低Thymeleaf、FreeMarker前端方案BVue 3 Element Plus前后端分离体验好贴近企业实际开发React、Angular数据库MySQL 5.7免费、通用性强面试和企业都在用PostgreSQL数据库连接池Druid自带监控页面能实时看SQL执行情况HikariCP构建工具Maven依赖管理直观SSM项目最成熟的构建方式Gradle这里需要单独说一下前端方案。如果你是想快速熟悉SSM全流程、把主要精力放在后端逻辑上那就选JSP但如果你希望这个项目能放进简历作为展示项目我强烈建议做前后端分离用Vue 3来写前端。后面第6节我会专门讲Vue 3如何和SSM后端连接包括跨域、axios封装、动态路由权限这些实操细节。2. 数据库设计与表关系核心要点2.1 表结构设计八张核心表数据库设计是这类系统最重要的地基我直接把设计好的表结构贴出来并逐个说明设计意图。-- 1. 用户表教师、学生、管理员共用一个账号体系 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 加密存储推荐BCrypt, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, role TINYINT NOT NULL COMMENT 1-管理员 2-教师 3-学生, phone VARCHAR(20) DEFAULT NULL, email VARCHAR(50) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1-启用 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT系统用户表; -- 2. 题目表题干、选项、答案、解析、知识点、难度 CREATE TABLE question ( id BIGINT NOT NULL AUTO_INCREMENT, question_type TINYINT NOT NULL COMMENT 1-单选 2-多选 3-判断, content TEXT NOT NULL COMMENT 题干支持富文本/图片路径, options TEXT DEFAULT NULL COMMENT JSON数组如[A.选项1,B.选项2], answer VARCHAR(50) NOT NULL COMMENT 单选填A多选填ACD判断填T/F, analysis TEXT COMMENT 答案解析, subject VARCHAR(50) DEFAULT NULL COMMENT 所属科目, knowledge_point VARCHAR(100) DEFAULT NULL COMMENT 知识点用于组卷筛选, difficulty TINYINT DEFAULT 1 COMMENT 1-易 2-中 3-难, creator_id BIGINT DEFAULT NULL COMMENT 录入人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_type_diff (question_type, difficulty) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT题库表;这里有一个关键技术点想单独提出来说选项字段用TEXT类型存JSON而不是单独建一张选项表这是有意为之。因为考试系统的题目选项天然是并列的、没有独立业务属性的如果为每个选项建一行记录查询时要拼行转列反而把简单问题复杂化了。用JSON存储在Java端用一个泛型List接收即可配合MyBatis的TypeHandler或Jackson反序列化非常灵活。-- 3. 试卷表存储一份试卷的基本信息 CREATE TABLE exam_paper ( id BIGINT NOT NULL AUTO_INCREMENT, paper_name VARCHAR(100) NOT NULL COMMENT 试卷名称, total_score INT DEFAULT 100, duration_minutes INT DEFAULT 60 COMMENT 建议时长发布时可覆盖, creator_id BIGINT COMMENT 创建人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 0 COMMENT 0-草稿 1-已发布, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT试卷表; -- 4. 试卷题目关联表试卷和题目是多对多关系 CREATE TABLE exam_paper_question ( id BIGINT NOT NULL AUTO_INCREMENT, paper_id BIGINT NOT NULL COMMENT 试卷ID, question_id BIGINT NOT NULL COMMENT 题目ID, score INT NOT NULL COMMENT 该题分值, sort_order INT DEFAULT 0 COMMENT 题号排序, PRIMARY KEY (id), KEY idx_paper (paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT试卷题目关联表;题目表、试卷表、关联表这三张表做完整份卷子的结构就已经清晰了。为什么要单独建关联表因为同一道题可以出现在多份试卷里而且在不同试卷里分值可能不同比如同样的选择题A卷里3分B卷里5分。这种“对象本身属性”和“关联关系属性”并存的情况正是中间表存在的意义。再补上考试记录表和答题明细表-- 5. 考试表一场具体的考试安排 CREATE TABLE exam ( id BIGINT NOT NULL AUTO_INCREMENT, exam_name VARCHAR(100) NOT NULL, paper_id BIGINT NOT NULL COMMENT 关联试卷, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, duration_minutes INT NOT NULL DEFAULT 60, total_score INT NOT NULL DEFAULT 100, pass_score INT NOT NULL DEFAULT 60, publish_status TINYINT DEFAULT 0 COMMENT 0-未发布 1-已发布, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试表; -- 6. 学生考试记录表记录谁参加了哪场考试 CREATE TABLE exam_record ( id BIGINT NOT NULL AUTO_INCREMENT, exam_id BIGINT NOT NULL, student_id BIGINT NOT NULL, paper_id BIGINT NOT NULL, start_time DATETIME NOT NULL COMMENT 实际开始时间, submit_time DATETIME DEFAULT NULL COMMENT 交卷时间, objective_score DECIMAL(5,1) DEFAULT 0.0 COMMENT 客观题得分, subjective_score DECIMAL(5,1) DEFAULT 0.0 COMMENT 主观题得分, total_score DECIMAL(5,1) DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT 0-考试中 1-已交卷 2-已阅卷, ip_address VARCHAR(50) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_exam_student (exam_id, student_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT考试记录表; -- 7. 答题明细表记录学生每一道题的作答 CREATE TABLE exam_answer_detail ( id BIGINT NOT NULL AUTO_INCREMENT, record_id BIGINT NOT NULL COMMENT 考试记录ID, question_id BIGINT NOT NULL, student_answer TEXT COMMENT 学生答案多选题存ACD, is_correct TINYINT DEFAULT NULL COMMENT 客观题是否正确 1-正确 0-错误, score DECIMAL(5,1) DEFAULT 0.0 COMMENT 此题得分, PRIMARY KEY (id), KEY idx_record (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT答题明细表;2.2 核心设计决策为什么答案用字符串而不是布尔值你可能会注意到题目答案字段answer设计成了VARCHAR(50)而不是更简洁的布尔值或单字符。这是我在实际项目中踩过坑之后特意改的。第一多选题的存在让答案天然变成了一个集合例如正确答案是A、C、D如果字段设计太窄存不下这种多选情况。用VARCHAR存字符串“ACD”在判分时做字符串分割和比对简单高效。第二判断题答案用“T/F”而不是BIT类型在校验时更直观也避免前端拿布尔值做各种类型转换的麻烦。第三如果你后续想扩展填空题、简答题答案字段的兼容性也更强。这里额外提醒一句所有用于判断的答案字段在入库时必须统一大小写并trim空字符否则就会出现“学生填了A答案是a”这种低级但让人血压飙升的误判。2.3 数据库优化心得索引怎么建才不翻车SSM项目一般不会遇到大数据量瓶颈但合理的索引设计仍然是一个专业开发者应有的基本素养而且面试时一定会被问到。我这套表的索引设计原则可以总结为三条第一条外键关联字段建立普通索引比如exam_record表中的exam_id和student_id、exam_answer_detail表中的record_id、关联表中的paper_id。因为在线考试系统最频繁的操作是“查询某个学生的某场考试记录”和“查询某份试卷的全部题目”这些字段没有索引会全表扫描。第二条状态字段和常用条件字段建立联合索引比如题目表经常要按照question_type和difficulty组合筛选题目来组卷这两个字段建立联合索引后自动组卷的查询效率会明显提升。第三条唯一索引保护核心业务约束exam_record表里的uk_exam_student十分关键它保证了同一场考试同一个学生只有一条记录防止前端重复交卷时产生脏数据。数据库层级的约束比你在Java代码里写一百行if判断都要可靠。这一点我在第7节讲“重复交卷”问题时还会再次强调。3. 后端核心模块实现与分析3.1 三层架构与代码包结构规范项目采用经典的三层架构我建议的包结构如下。这个结构是我从多个生产项目中归纳出来的既不过度设计又有清晰的边界。com.example.exam ├── Controller // 控制层负责接收请求、参数校验、返回结果 ├── Service // 业务层接口 impl核心业务逻辑都在这里 ├── Mapper // 数据访问层接口 XML ├── Model // 实体类与数据库表一一对应 │ ├── entity │ ├── dto // 接收前端参数的对象 │ └── vo // 返回前端的数据对象 ├── Common │ ├── Result // 统一返回结果包装 │ ├── PageResult // 分页结果包装 │ └── GlobalExceptionHandler // 全局异常处理 └── Config // 配置类这里我想重点聊聊dto和vo的分层。很多初学者图省事直接拿实体类entity去接收前端参数、再直接返回给前端。短期看代码量少了但后续维护非常痛苦。原因是数据库表结构和前端页面字段天然不是一一对应的。比如新增题目时前端要传options数组JSON同时还要传answer字符串而查询题目列表时前端需要的是options解析后的列表以及一个“试题难度的中文说明”。这些字段组装逻辑放实体类里太脏放Controller层又太厚正确做法就是分别在dto和vo里规范定义。3.2 核心功能一基于JWT的用户登录与权限拦截因为要支持Vue 3前端传统Session方案需要处理跨域携带Cookie的问题比较麻烦。所以这里我使用JWT方案来做登录态管理。RestController RequestMapping(/api/auth) public class AuthController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO loginDTO) { // 1. 查用户 User user userService.findByUsername(loginDTO.getUsername()); if (user null) { return Result.error(用户不存在); } // 2. 校验密码 if (!BCrypt.checkpw(loginDTO.getPassword(), user.getPassword())) { return Result.error(密码错误); } // 3. 校验状态 if (user.getStatus() 0) { return Result.error(账号已被禁用请联系管理员); } // 4. 生成 token String token JwtUtil.createToken(user.getId(), user.getRole()); MapString, Object data new HashMap(); data.put(token, token); data.put(realName, user.getRealName()); data.put(role, user.getRole()); return Result.success(data); } }登录接口的逻辑很清晰查用户 → 校验密码 → 检查状态 → 发token。密码存储使用BCrypt加密而不是MD5。原因也很简单MD5是摘要算法加盐不当很容易被彩虹表反查而BCrypt是适应性哈希算法内部自动随机盐并且可以通过调整strength参数来控制计算成本是目前业界最主流的密码存储方案。权限拦截这块用Spring MVC的HandlerInterceptor实现public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 String uri request.getRequestURI(); if (uri.contains(/api/auth/login)) { return true; } // 获取 token String token request.getHeader(Authorization); // 校验 token try { Claims claims JwtUtil.parseToken(token.replace(Bearer , )); // 将用户信息放入 request后续Controller直接获取 request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\token无效或已过期\}); return false; } } }这个实现的亮点在于将用户信息放入request属性后后续所有Controller都能通过参数解析拿到当前登录人省去重复解析token的操作。我通常在项目的WebMvcConfig里再定制一个CurrentUser注解实现参数解析这样业务代码里直接写User currentUser作为参数就能拿人体验非常好。3.3 核心功能二自动组卷算法与动态SQL实现手动组卷的逻辑相对简单教师从题库里勾选题目循环插入关联表就行。真正有技术含量的是自动组卷。假设需求是“组一份总分100分的试卷单选题15道每题2分多选题5道每题4分判断题10道每题3分总分刚好是 15×2 5×4 10×3 100”。同时要求题目难度覆盖比例为容易40%、中等40%、较难20%。这种需求用代码实现思路如下Transactional public Long autoGeneratePaper(AutoPaperDTO dto) { // 1. 创建试卷 ExamPaper paper new ExamPaper(); paper.setPaperName(dto.getPaperName()); paper.setTotalScore(dto.getTotalScore()); examPaperMapper.insert(paper); int sortOrder 0; // 2. 遍历每个题型的抽取要求 for (QuestionTypeConfig config : dto.getConfigs()) { // 3. 查询符合条件的题目列表这里使用MyBatis动态SQL ListQuestion pool questionMapper.selectForAutoGenerate( config.getQuestionType(), config.getDifficulty(), config.getSubject()); // 4. 随机抽取指定数量 Collections.shuffle(pool); int needCount config.getCount(); if (pool.size() needCount) { throw new BusinessException(题库数量不足 config.getQuestionTypeDesc() 需要 needCount 道实际只有 pool.size() 道); } // 5. 插入关联表 for (int i 0; i needCount; i) { ExamPaperQuestion paperQuestion new ExamPaperQuestion(); paperQuestion.setPaperId(paper.getId()); paperQuestion.setQuestionId(pool.get(i).getId()); paperQuestion.setScore(config.getScore()); paperQuestion.setSortOrder(sortOrder); examPaperQuestionMapper.insert(paperQuestion); } } return paper.getId(); }这里有几个细节非常关键。第一Transactional事务不能少否则组卷中途出错会出现“试卷有了但题目没插全”的半成品数据。第二随机抽题不要直接在SQL里写ORDER BY RAND()当题库数据量到十万级的时候这个操作会让数据库CPU瞬间飙高。更合理的做法是先把候选题目ID查出来放内存里再用Collections.shuffle进行随机。当然如果题库数据量确实大几十万条级别也可以考虑“先按条件查出一个ID列表再用WHERE id IN (...) ORDER BY RAND() LIMIT n”至少在MySQL 8.0版本中这是比较折中且稳定的方案。组卷对应的动态SQL也一并贴出来select idselectForAutoGenerate resultTypecom.example.exam.model.entity.Question SELECT * FROM question where if testquestionType ! null AND question_type #{questionType} /if if testdifficulty ! null AND difficulty #{difficulty} /if if testsubject ! null and subject ! AND subject #{subject} /if /where /selectMyBatis动态SQL的核心价值在于查询条件是可组合的教师勾选“只看单选题、难度中等”和“只看判断题、所有难度”是两个完全不同的查询用if标签拼装SQL比在一行SQL里写死多个AND条件要优雅得多。3.4 核心功能三考试中自动交卷与客观题判分自动判分逻辑是考试系统最核心的价值所在。学生交卷后后端需要遍历答题明细比对正确答案计算得分并汇总出客观题成绩。这一段代码我给出完整实现Transactional public void submitExam(SubmitExamDTO dto) { // 1. 获取考试记录 ExamRecord record examRecordMapper.selectById(dto.getRecordId()); if (record null) { throw new BusinessException(考试记录不存在); } if (record.getStatus() ! 0) { throw new BusinessException(考试已交卷请勿重复操作); } // 2. 校验是否超过考试时间防止前端倒计时被篡改 Exam exam examMapper.selectById(record.getExamId()); Date now new Date(); if (now.after(exam.getEndTime())) { // 超时强制交卷 record.setStatus(1); record.setSubmitTime(exam.getEndTime()); } else { record.setStatus(1); record.setSubmitTime(now); } // 3. 获取试卷所有题目 ListExamPaperQuestion paperQuestions examPaperQuestionMapper.selectByPaperId(record.getPaperId()); MapLong, Integer scoreMap new HashMap(); for (ExamPaperQuestion pq : paperQuestions) { scoreMap.put(pq.getQuestionId(), pq.getScore()); } // 4. 遍历答题明细客观题自动判分 double objectiveScore 0; for (ExamAnswerDetail detail : dto.getAnswers()) { Question question questionMapper.selectById(detail.getQuestionId()); if (question.getQuestionType() 3) { // 主观题暂不判分分值记为0待教师人工阅卷 detail.setScore(0.0); detail.setIsCorrect(null); } else { // 客观题判断题答案比对 boolean correct normalizeAnswer(detail.getStudentAnswer()) .equals(normalizeAnswer(question.getAnswer())); detail.setIsCorrect(correct ? 1 : 0); double s correct ? scoreMap.getOrDefault(question.getId(), 0) : 0.0; detail.setScore(s); if (correct) { objectiveScore s; } } examAnswerDetailMapper.insert(detail); } // 5. 更新考试记录总成绩 record.setObjectiveScore(objectiveScore); record.setTotalScore(objectiveScore); // 人工阅卷后再更新 examRecordMapper.updateById(record); }这段代码有几个对错逻辑值得展开说明。多选题判分策略是一个容易纠结的点。业界一般有两种策略一种叫“全对才得分”即必须和标准答案完全一致选多、选少、选错都不得分另一种叫“部分给分”即少选的情况下每个正确选项给部分分。我在代码里采用了“全对才得分”策略原因是用户可接受度更好实现也简单。如果你要做“部分给分”就必须指定“漏选给一半分”的规则那对应的代码逻辑要改成集合交集计算这也并不难看需求取舍。防作弊的细节前端倒计时归零后自动调用交卷接口这个逻辑只能作为用户体验辅助不能作为后端信任的凭据。因为前端的代码可以被绕过一个懂技术的学生完全可以通过浏览器开发者工具修改倒计时变量让自己“永远不超时”。所以后端一定要在交卷接口再做一次时间校验就是我代码里第2步做的事。记住一个核心原则所有安全相关的判断必须发生在服务端前端只是UI辅助。4. 前端设计Vue 3连接SSM后端的完整实践4.1 Vite开发服务器代理解决跨域问题如果你选了前后端分离方案第一个要面对的问题就是跨域。Vue 3前端开发环境默认跑在http://localhost:5173后端Spring MVC跑在http://localhost:8080两个端口不同浏览器会拦截前端发起的Ajax请求。解决方案有两个方向。方向一是后端开启CORS在Spring配置里加一个CorsFilter或使用CrossOrigin注解方向二是在前端Vite配置代理把/api开头的请求代理到后端地址。实际开发中我推荐两件事同时做但理由是反直觉的开发环境用代理生产环境用CORS。开发环境用代理的好处是你在代码里写的请求路径是完整的项目路径比如/api/exam/list不需要拼接http://localhost:8080这样的绝对地址代码更干净健壮生产环境把前后端项目部署在同一域名的不同路径下比如后端/api前端根路径代理同样可行。CORS则作为拓展兼容方案尤其是你联调时要用Postman或者别的前端项目临时调用时打开CORS能省很多事。Vite代理配置如下// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端Controller映射没有/api前缀这里做路径重写 // rewrite: (path) path.replace(/^\/api/, ) } } } })4.2 axios封装与请求拦截器axios是Vue项目中最主流的HTTP库针对这个考试系统我封装了一个统一的请求模块。这里把核心代码贴出来你可以直接抄到自己的项目里// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 30000, }) // 请求拦截器 request.interceptors.request.use( (config) { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, (error) { return Promise.reject(error) } ) // 响应拦截器 request.interceptors.response.use( (response) { const res response.data // 后端返回结构统一为 { code, msg, data } if (res.code ! 200) { ElMessage.error(res.msg || 请求失败) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.msg)) } return res.data }, (error) { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request这个封装的精妙之处在响应拦截器里业务代码只需要关心data部分code和msg的处理被统一消化掉了。比如调用const examList await getExamList()拿到的直接就是数组或对象不需要每处都写res.data.data这种冗长代码。4.3 考试页面的核心交互倒计时、答案暂存、异常恢复考试页面是前端最复杂的部分。我给读者一个完整的交互体验设计方案倒计时与到时自动交卷进入考试后后端返回考试截止时间从后端数据库读取前端依据此时间倒计时而不是在前端定义一个60分钟定时器从0开始减。这个设计有讲究。因为如果学生中途刷新页面前端定义在内存里的倒计时会归零重置学生就有了“重新获得满满时间”的漏洞而依据截止时间倒计时刷新后倒计时依然准确这正是后端时间戳校验思想的延伸。答案暂存防丢失即使是本机不刷新也建议在每次切换题目时将当前作答答案存入sessionStorage。这样万一用户不小心关闭浏览器再开重新进入考试页面时可以提示“检测到未提交的答案是否恢复”体验会好很多。异常断线恢复考试过程中如果网络断开前端应当在axios错误捕获时提示“网络异常答题进度已自动保存请勿关闭页面”并停止自动交卷倒计时等网络恢复后提示重新连接。防止学生在断网期间无法操作。// 倒计时核心逻辑 const startCountdown (deadline) { const timer setInterval(() { const remain new Date(deadline).getTime() - Date.now() if (remain 0) { clearInterval(timer) submitExam() // 调用交卷接口 return } const minutes Math.floor(remain / 60000) const seconds Math.floor((remain % 60000) / 1000) countdown.value ${String(minutes).padStart(2, 0)}:${String(seconds).padStart(2, 0)} }, 1000) }4.4 权限控制动态菜单与按钮级权限前端权限控制核心思想就是根据登录人的角色动态渲染菜单和按钮。我建议把角色信息放在store里持久化页面路由控制则通过Vue Router的beforeEach守卫实现。// 路由守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.path /login) { next() return } if (!token) { next(/login) return } // 根据角色判断是否有权限访问该页面 if (to.meta.roles to.meta.roles.indexOf(Number(role)) -1) { ElMessage.error(无权访问该页面) next(/dashboard) return } next() })5. 项目落地与部署5.1 项目初始化与Maven依赖配置SSM项目的初始化核心在于Maven的pom.xml依赖配置。我直接给出一份可用的依赖清单对照使用即可dependencies !-- Spring核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version5.3.20/version /dependency !-- MyBatis -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.10/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency !-- MySQL驱动 Druid连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.8/version /dependency !-- JWT 密码加密 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId version5.6.2/version /dependency !-- Servlet API -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- Jackson -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version2.13.0/version /dependency !-- 其他工具类 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-lang3/artifactId version3.12.0/version /dependency /dependencies注意几个依赖版本选择背后的考量Spring用了5.3.x是因为这个版本对Servlet API 4.0和Java 8都有良好的兼容性而且当前主流Spring Boot 2.x底层也是用的这个版本的Spring核心学完以后迁移到Spring Boot几乎没有障碍。MyBatis和mybatis-spring的版本必须搭配它们各自的3.5.x和2.0.x是官方推荐配套版本用错了会出现MapperScannerConfigurer初始化异常这种莫名其妙的错误。5.2 部署方式与上线避坑项目开发完成后部署到服务器推荐用外置Tomcat以war包方式发布。用Maven打包时记得先执行mvn clean package跳过测试避免测试类干扰打包mvn clean package -DskipTests打出的war包复制到Tomcat的webapps目录即可。这里有几个部署期的难点值得提一下第一数据库连接配置要外置。生产环境的数据库地址、账号密码通常和本地不同我习惯做法是在jdbc.properties里配置占位符部署时直接改配置文件而不是重新打包。第二服务器时区问题。MySQL连接串一定要加serverTimezoneAsia/Shanghai否则存的日期会差8个小时。这个坑非常经典我第一次部署时前端显示考试时间总比预期晚8小时查了半天才发现是连接串缺了时区参数。第三静态资源与后端接口的路径规划。如果是前后端分离部署需要把Vue构建出来的静态文件交给Nginx托管Nginx配置将/api路径代理到Tomcat端口。如果你打算用spring-mvc.xml里的default-servlet-handler同时处理静态资源要小心路径冲突。6. 常见问题与排查技巧实录6.1 问题速查表我把这个项目开发过程中最常遇到的10个问题整理成了表格每一条都是真实踩过的坑问题表现根本原因解决方案页面报404但Controller路径看起来没问题没有加ResponseBody或Controller不在Spring扫描包下检查context:component-scan的包名范围前端传的JSON参数为null缺少Jackson依赖或实体类缺少无参构造加jackson-databind依赖检查实体类MyBatis提示Invalid bound statement (not found)Mapper接口和XML的namespace匹配失败检查XML中的namespace和接口全限定名是否一致分页查询失效返回了所有数据没有使用PageHelper依赖或拦截器配置遗漏添加PageHelper依赖并在MyBatis配置中注册拦截器交卷时数据重复插入前端重复点击交卷按钮前端按钮防抖 后端唯一索引兜底考试时间快了8小时MySQL连接串缺少时区参数URL添加serverTimezoneAsia/Shanghai文件上传中文文件名乱码Tomcat默认编码不是UTF-8修改server.xml配置URIEncodingUTF-8答案保存不成功但其他接口正常参数对象名和前端字段名不一致前端统一下划线命名后端dto用驼峰加JsonProperty自动组卷偶尔出现空卷没有校验题库数量随机抽取时池子为空抽题前先检查候选池数量不足则抛业务异常学生能同时打开两个浏览器参加考试没有做同一账号考试互斥后端记录考试开始状态检查现有进行中的记录6.2 排查技巧看日志与慢SQL定位瓶颈实际排查问题我自己的流程很固定先看日志、再看SQL、最后看代码。SSM项目里启动日志和运行日志会打印很多关键信息比如Spring初始化时扫描了哪些Bean、MyBatis加载了哪些Mapper XML。如果某个接口报500错误先打开日志找到异常堆栈对照堆栈的行号定位到具体的Mapper方法或Service方法再打开MyBatis的SQL日志确认实际执行的SQL语句。慢SQL问题推荐使用Druid连接池自带的监控页面配置好StatFilter和StatViewServlet后在http://localhost:8080/druid/index.html能看到每条SQL的执行次数、耗时、最慢SQL的详细语句。我实际优化过的一个案例是考试列表页每次刷新需要1.5秒监控发现它在循环查询学生的每场考试成绩产生了典型的N1查询后面改成JOIN查询加一次批量查询耗时降到了50毫秒以内。6.3 安全加固SQL注入与XSS防御实操在线考试系统涉及大量查询和展示安全上最容易出问题的就是SQL注入和XSS攻击。SQL注入方面MyBatis的#{}预编译机制已经能挡住绝大多数注入攻击但如果你在SQL里用了${}拼接字段比如动态排序的ORDER BY ${orderByColumn}就会留下注入漏洞。这个地方的防御方案是白名单校验在Java端判断传入的列名是否在允许的排序字段集合里不在就拒绝。XSS攻击方面题目或用户名这类用户输入内容保存时必须做HTML标签转义展示时利用模板引擎的默认转义机制。我建议给全文配一个基于Jackson的XssFilter过滤器在请求进入Controller前统一清洗参数防止script标签入库。7. 个人经验总结与进阶建议这个项目从零到完整上线我前前后后做过两版。第一版用JSP花了大概两周第二版改成Vue 3前后端分离又花了一周调试联调。回头看的体会是SSM真正的价值不在框架本身而在于它逼着你把Web开发的底层链路完整走了一遍。你会理解请求从浏览器到Controller再到Service和Mapper的完整流转能说清楚Spring容器和Spring MVC容器的父子关系知道事务代理是怎么拦截的也知道MyBatis的Mapper接口是怎么通过JDK动态代理生成实现类的。这些知识在面试时、在排查Spring Boot项目的诡异问题时都会变成你的底气和直觉。最后再分享一个小技巧做完这个项目建议顺手把它改造成Spring Boot版本做一次“同一套业务逻辑两套技术栈实现”的对照。改造过程中你会很容易发现Spring Boot自动配置到底帮你做了什么同时也能理解为什么Spring Boot能成为当下生产环境的主流选择。而这种“先学原理、再上高速”的学习路径对我带过的新人来说是见效最快的一条路。如果你在实操中遇到什么奇怪的问题欢迎按上面的排查思路去定位。尤其是数据库连接串时区、N1查询、事务不回滚这三大经典坑碰上一次就会记忆深刻。本文还有配套的精品资源点击获取
返回列表