SSM+Vue员工培训考试系统源码解析与部署避坑指南

简介:这是一款基于SSM的员工知识培训考试系统完整源码包,主要面向需要进行Java课程设计、毕业设计或搭建企业培训考试平台的开发者。系统围绕员工培训与在线考试场景,后端整合Spring、SpringMVC与MyBatisPlus技术栈,前端使用Vue和ElementUI组件库,数据层采用MySQL,整体基于B/S架构,源码中已实现用户信息管理、图片素材管理与视频素材管理等核心模块,并附带数据库脚本,导入即可运行和二次扩展。资源压缩包约16.12MB,共包含846个文件,其中Java源文件128个、Vue组件52个、JavaScript文件167个、CSS样式表55个、HTML页面43个,另外还有SQL数据库脚本、XML配置文件和项目说明文档,工程结构完整,适合在IDEA或Eclipse中直接打开学习。包内同时提供系统设计文档,包含绪论、相关技术介绍、系统分析等章节,能够帮助读者理解整体设计思路、业务操作流程和前后端交互方式。目前已有194人浏览学习,可作为课程设计选题或实际项目开发的参考模板。整体代码风格清晰、注释适中,既适合中等水平Java开发者练习,也能为员工培训考试系统的快速建站提供基础。

1. 员工知识培训考试系统源码:为什么我建议你先打开那三个 .bat

拿到这套基于 SSM 的员工知识培训考试系统源码,第一眼印象跟大多数 Java 课程设计源码不一样:没有冗长的 README,目录里是一堆.vue.bak文件和三个编号明确的批处理脚本1-install.bat、2-run.bat、3-build.bat。这不是一份只能看的半成品,而是一套能直接跑起来的企业内部培训考试平台——后端用 spring + springmvc + MyBatisPlus 撑接口,前端用 Vue + ElementUI 画页面,MySQL 存全部业务数据。它的价值在于把「员工信息管理、题库维护、在线考试、自动判分、成绩查询」这条链子完整串了一遍,不是只给你一个登录页加一张表。适合两类人:做员工知识培训考试系统设计与实现相关课设、毕设的学生,以及公司内网急需一套考试系统、想拿现成代码做二次开发的 Java 工程师。我先从项目骨架说起。

2. 骨架认知:从.vue.bak反推前端结构,从BaseMapper反推后端分层

2.1 从.bak后缀反推前端工程结构

这套源码里最扎眼的是一批.vue.bak文件:IndexMain.vue.bak、IndexAsideStatic.vue.bak、BreadCrumbs.vue.bak、IndexHeader.vue.bak、update-password.vue.bak。.bak是备份文件后缀,出现这种命名通常只有一种可能:原作者在生产部署目录里直接拷贝了前端构建前的源码,顺手把改动前的版本存了个备份。这反而是好事——说明你拿到的是真正的 Vue 源码,不是打包压缩后的dist,你可以直接改组件再重新构建,不用对着压缩代码猜逻辑。

从文件名能反推这套前端是一个标准的管理后台布局:IndexMain.vue是主框架容器,IndexAsideStatic.vue是左侧静态菜单栏,IndexHeader.vue是顶部导航栏,BreadCrumbs.vue是面包屑导航,update-password.vue是用户修改密码页面。这套组件划分跟 vue-element-admin 这类后台模板的思路完全一致:框架负责布局,子组件负责功能模块。你在二次开发时新增一个「试卷管理」页面,只需要复制IndexMain.vue的路由结构,再写一个业务组件挂进router即可。

这里要注意一个细节:资源里还提到了「图片素材」「视频素材」这两类数据。在培训考试系统里,它们通常有两种挂载方式:一种是作为考题的题干附件(比如看一段安全培训视频后答题),另一种是作为培训资料存在素材库,考试时随机抽取关联。无论哪种,前端都需要一个素材上传组件。如果你看到的是.bak版本,说明原作者的素材管理模块还没完全整理干净,二次开发时优先梳理这块的字段关联。

2.2 后端分层:SSM 框架 + MyBatisPlus 的组合逻辑

后端技术栈是 spring + springmvc + MyBatisPlus,这是 SSM 框架的常见变体。跟传统 SSM 的区别在于:传统 SSM 用 MyBatis 写一堆 XML 映射文件,而这个项目用 MyBatisPlus 做单表 CRUD 的自动封装,只有多表关联查询才需要手写 SQL。我拆项目时看到 Mapper 层通常是这种写法的:

import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface EmployeeMapper extends BaseMapper<Employee> { // 单表 CRUD 由 MyBatisPlus 自动实现,不需要写 XML // 多表查询时可以在这里自定义方法,配合 @Select 注解或 XML }

逻辑说明:BaseMapper<Employee>是 MyBatisPlus 提供的通用泛型接口,里面内置了selectById、insert、updateById、deleteById等基础方法。@Mapper注解让 Spring 容器在启动时扫描到该接口并生成代理实现类。这样做的好处是:员工表的增删改查你一行 SQL 都不用写,直接把焦点放在考试业务上。

参数说明:泛型里的Employee必须对应数据库中的员工表实体类,实体类上用@TableName("employee")指定表名,用@TableId(type = IdType.AUTO)标注主键策略。如果实体类属性名跟数据库字段名不一致(比如userName对应user_name),MyBatisPlus 默认开启驼峰映射,但保险做法是在application.yml里显式配置map-underscore-to-camel-case: true。

Service 层的写法也很有 MyBatisPlus 风格:

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; @Service public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> { // IService 内置 saveBatch、listByIds、page 等批量方法 public Page<Employee> searchByKeyword(String keyword, long current, long size) { LambdaQueryWrapper<Employee> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(keyword), Employee::getEmpName, keyword) .orderByDesc(Employee::getCreateTime); return this.page(new Page<>(current, size), wrapper); } }

逻辑说明:ServiceImpl<EmployeeMapper, Employee>把 Mapper 和实体类绑定,调用this.page()可以直接返回分页对象。LambdaQueryWrapper是 MyBatisPlus 的条件构造器,用 lambda 方式指定查询字段,避免硬编码字符串字段名。这套代码里凡是涉及员工列表、题库分页、考试记录查询的接口,基本都是这种模板,你理解一个就能读懂其余全部。

2.3 数据表设计:四张核心表 + 两张素材表

根据「员工信息、图片素材、视频素材」这几个摘要关键词,结合考试系统的业务闭环,我拆出来的核心表结构大致如下。先说员工和考试这两组必不可少的表:

-- 员工表 CREATE TABLE `employee` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `emp_no` varchar(32) NOT NULL COMMENT '工号', `emp_name` varchar(64) NOT NULL COMMENT '姓名', `password` varchar(128) NOT NULL COMMENT '登录密码(MD5或BCrypt)', `department` varchar(64) DEFAULT NULL COMMENT '所属部门', `position` varchar(64) DEFAULT NULL COMMENT '岗位', `role` tinyint(1) DEFAULT '0' COMMENT '角色: 0员工 1管理员', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_emp_no` (`emp_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表'; -- 题库表 CREATE TABLE `question_bank` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `subject` varchar(64) DEFAULT NULL COMMENT '科目/分类', `q_type` tinyint(1) DEFAULT '0' COMMENT '题型: 0单选 1多选 2判断', `content` text COMMENT '题干', `option_a` varchar(255) DEFAULT NULL, `option_b` varchar(255) DEFAULT NULL, `option_c` varchar(255) DEFAULT NULL, `option_d` varchar(255) DEFAULT NULL, `answer` varchar(10) DEFAULT NULL COMMENT '标准答案', `score` int(11) DEFAULT '2' COMMENT '分值', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='题库表';

字段说明:q_type用数字区分题型,避免字符串带来的脏数据;answer存字母组合(单选题存A,多选题存ABD),判分时直接字符串比对;score控制每题分值,方便组卷时调整试卷总分。emp_no加唯一索引,这是员工登录账号的常见设计,避免用自增 id 做账号导致泄露员工数量。

考试和成绩记录表是这套系统的业务核心:

CREATE TABLE `exam_record` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exam_id` bigint(20) NOT NULL COMMENT '考试场次ID', `emp_id` bigint(20) NOT NULL COMMENT '员工ID', `total_score` int(11) DEFAULT '0' COMMENT '总分', `duration` int(11) DEFAULT '0' COMMENT '用时(秒)', `submit_time` datetime DEFAULT NULL COMMENT '交卷时间', PRIMARY KEY (`id`), KEY `idx_exam_id` (`exam_id`), KEY `idx_emp_id` (`emp_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试记录表';

这四张表构成了最小可用的考试系统:员工发起考试 → 从题库抽题 → 提交后生成考试记录 → 查分时按记录关联明细。至于图片素材和视频素材,常见做法是单独建image_material和video_material两张表,字段大致是id、material_name、file_path、uploader_id、create_time,通过subject字段跟题库的科目关联,实现「看视频再答对应题目」的培训闭环。如果你拿到的源码里这几张表还没建全,那正是二次开发要补的第一个短板。

3. 本地跑起来:三个批处理脚本的顺序、参数与黑匣子

3.1 三个 bat 的分工逻辑

这套资源里最实用的设计就是三个编号脚本,它们的命名直接暴露了执行的先后逻辑:1-install.bat负责安装依赖和初始化环境,2-run.bat负责启动后端服务,3-build.bat负责打包构建。从.classpath文件可以判断这原本是个 Eclipse 工程,但脚本本身是跨 IDE 的,IDEA、VSCode 都能用。我把常见的部署脚本内容展开来说:

@echo off REM 1-install.bat - 安装依赖并初始化数据库 echo [STEP 1/3] Initialize Maven dependencies... call mvn clean install -DskipTests if %errorlevel% neq 0 ( echo [ERROR] Maven install failed. Check your Maven settings. pause exit /b 1 ) echo [STEP 2/3] Initialize database... mysql -uroot -p123456 -e "CREATE DATABASE IF NOT EXISTS exam_db DEFAULT CHARSET utf8mb4;" mysql -uroot -p123456 exam_db < sql/init.sql echo [STEP 3/3] Install complete. pause

逻辑说明:第一段mvn clean install -DskipTests把后端项目编译并打包到本地 Maven 仓库,-DskipTests跳过测试用例加快速度,这一步是在确保你本机装了 JDK 和 Maven 的前提下才能成功。第二段用 MySQL 命令行创建数据库并导入初始化 SQL——注意这条命令里的-uroot -p123456是写死的,如果你的 MySQL 密码不是123456,必须改掉,否则会报Access denied。%errorlevel% neq 0是批处理里的错误捕获,任何一步失败都会暂停并提示,方便你定位问题。

参数说明:errorlevel是 Windows 批处理的环境变量,存的是上一条命令的退出码,0 代表成功,非 0 代表失败。"CREATE DATABASE IF NOT EXISTS exam_db DEFAULT CHARSET utf8mb4;"里的utf8mb4是必选项,MySQL 的utf8最多存 3 字节,存不了 emoji 和一些生僻汉字,考试题干里如果没有 utf8mb4,后续导入含有特殊符号的题目时大概率乱码。

3.2 配置文件核对:跑不起来先查这三处

按我的习惯,拿到项目先不急着点脚本,而是先把核心配置文件过一遍。SSM 项目最常见的配置文件是application.yml或application.properties,这套资源的配置文件通常在src/main/resources下:

server: port: 8080 servlet: context-path: /exam spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/exam_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 50MB max-request-size: 100MB mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

参数说明:context-path: /exam意味着所有接口访问都要加/exam前缀,比如登录接口实际地址是http://localhost:8080/exam/login,前端 Vue 的 axios 请求路径必须跟这个前缀匹配,否则会 404。serverTimezone=Asia/Shanghai是 MySQL 8.x 的强制要求,不写它启动时会报 CST 时区错误。max-file-size和max-request-size直接关系到图片素材、视频素材能否上传成功,默认 1MB 肯定不够用,50MB/100MB 是培训视频素材的下限配置。

log-impl: StdOutImpl这行建议保留,它能让你在控制台看到每条 SQL 的执行语句和参数,排查「数据查不出来」类问题全靠它。如果你用的是 MyBatis 而不是 MyBatisPlus,对应的配置是mybatis.mapper-locations,注意前缀区别。

第三处要看的是pom.xml里的依赖版本。这套资源用的是 spring + springmvc + MyBatisPlus 的组合,MyBatisPlus 对不同 Spring 版本有兼容要求。如果项目里用的是mybatis-plus-boot-starter3.x,而 Spring Boot 还是 1.x,启动时大概率会报NoSuchBeanDefinitionException。我见过的血泪经验是:Spring Boot 2.x 配 MyBatisPlus 3.4.x 最稳,Spring Boot 1.5.x 就老实换mybatis-plus-boot-starter2.x,别用最新版硬怼。

3.3 启动与接口自测:别急着点页面

环境配好后不要直接双击2-run.bat,先在命令行手动执行一次,让日志留在当前窗口里,这样能看到完整启动过程。启动成功的标志不是控制台打印 Spring logo,而是出现Tomcat started on port(s): 8080或Started Application in x.xxx seconds。看到这行后再验证接口:

# 第一步:验证服务是否存活 curl http://localhost:8080/exam/ # 第二步:模拟管理员登录(账号密码以你初始化SQL为准) curl -X POST http://localhost:8080/exam/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 第三步:带 Cookie 访问受保护接口,验证 Session 是否生效 curl -b cookies.txt -c cookies.txt http://localhost:8080/exam/employee/list

逻辑说明:第一步curl访问根路径,如果返回 404 是正常的,因为根路径没有映射;如果返回连接拒绝,说明 Spring Boot 根本没起来。第二步用 POST 提交登录信息,-c cookies.txt把服务端返回的会话 Cookie 存到本地文件。第三步用-b cookies.txt携带 Cookie 访问受保护的员工列表接口,如果能返回 JSON 数据,说明登录鉴权链路通畅;如果返回 401 或跳转到登录页,说明拦截器配置有问题。这套自测方法能帮你区分「后端没起来」「接口报错」「前端编译问题」三种故障类型,比先开浏览器再抓包高效得多。

4. 核心业务链路:登录鉴权、随机组卷、自动判分是怎么串起来的

4.1 登录与权限控制:Session 方案的前后端配合

这套系统用的是最经典的 Session 鉴权方案。后端通过 HandlerInterceptor 拦截请求,前端 Vue 通过 axios 的withCredentials属性携带 Cookie。这里有一处特别容易翻车的细节,我展开讲:

import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/logout", "/static/**", "/api/material/**"); } }

逻辑说明:addPathPatterns("/**")拦截所有路径,excludePathPatterns放行登录、登出、静态资源和部分公开素材接口。这里最容易踩的坑是把/static/**漏了——Vue 打包后的 JS、CSS 文件都挂在/static下,拦截器如果把静态资源也挡了,前端页面会白屏且控制台报 401。另一个坑是放行/login却忘了放行/api/material/**,导致员工考试时加载题图素材失败。

前端配合的代码在 axios 封装里:

// utils/request.js import axios from 'axios' const request = axios.create({ baseURL: '/exam', // 对应后端 context-path timeout: 15000, withCredentials: true // 关键:跨域或非同源下携带 Cookie }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { window.location.href = '/login' } return Promise.reject(error) } ) export default request

参数说明:baseURL必须跟后端的server.servlet.context-path保持一致,如果后端是 8080 端口加/exam前缀,而你前端请求的是 8081 端口或没加前缀,就会出现「前端发请求成功但后端不认识」的尴尬。withCredentials: true在前后端分离部署时是必需的,不设置的话浏览器不会携带 Session Cookie,后端拦截器永远拿不到会话,于是不断跳回登录页——这是「登录成功后跳转又回到登录页」问题的头号元凶。timeout: 15000设 15 秒比较合理,考试系统涉及图片素材加载,太短容易把正常请求误判为超时。

4.2 随机组卷:从题库抽取题目的一种稳妥写法

考试系统的核心体验在于组卷。这套系统基于 SSM 实现,组卷逻辑通常放在 Service 层。对于员工培训场景,题量一般不会特别大,用 MySQL 的ORDER BY RAND()做随机抽取是够用的:

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import org.springframework.stereotype.Service; import javax.annotation.Resource; import java.util.ArrayList; import java.util.List; @Service public class ExamPaperServiceImpl { @Resource private QuestionBankMapper questionBankMapper; public List<QuestionBank> generatePaper(Long examId, int singleCount, int multiCount) { List<QuestionBank> paper = new ArrayList<>(); // 抽取单选题:按科目过滤,随机排序后截取指定数量 LambdaQueryWrapper<QuestionBank> singleWrapper = new LambdaQueryWrapper<>(); singleWrapper.eq(QuestionBank::getQType, 0) .last("ORDER BY RAND() LIMIT " + singleCount); paper.addAll(questionBankMapper.selectList(singleWrapper)); // 抽取多选题 LambdaQueryWrapper<QuestionBank> multiWrapper = new LambdaQueryWrapper<>(); multiWrapper.eq(QuestionBank::getQType, 1) .last("ORDER BY RAND() LIMIT " + multiCount); paper.addAll(questionBankMapper.selectList(multiWrapper)); return paper; } }

逻辑说明:eq(QuestionBank::getQType, 0)按题型过滤,last("ORDER BY RAND() LIMIT " + singleCount)是 MyBatisPlus 的尾巴拼接,会在 SQL 末尾追加排序和限量语句。ORDER BY RAND()的本质是给每行生成随机数再排序,数据量小时简单直接,数据量超过万级时性能会明显下降,那时候就要改成先查 id 集合再WHERE id IN (...)的两步法。

参数说明:singleCount和multiCount是前端传过来的组卷参数,后端要校验这两个值不能超过对应题型的总量,否则LIMIT超出实际行数虽然不报错,但会破坏试卷的分数配比。组出来的paper列表只是题目,不能直接返回给前端,通常要转成 DTO 去掉answer字段——否则前端调试工具里一眼就能看到标准答案,这是考试系统的基本素养。

4.3 自动判分:答案比对与成绩回写

判分逻辑不复杂,但边界不少。单选题比对一个字符就行,多选题就麻烦一点——得考虑考生答案顺序、漏选、错选三种情况:

import org.springframework.stereotype.Service; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; @Service public class ExamJudgeServiceImpl { /** * 判分单题 * @param standardAnswer 标准答案,如 "ABD" * @param userAnswer 考生答案,如 "BAD" * @return 是否得分 */ public boolean judge(String standardAnswer, String userAnswer, int qType) { if (standardAnswer == null || userAnswer == null) { return false; } // 判断题和单选题:直接忽略大小写比对 if (qType == 0 || qType == 2) { return standardAnswer.trim().equalsIgnoreCase(userAnswer.trim()); } // 多选题:转为有序字符集合再比对 char[] std = standardAnswer.trim().toUpperCase().toCharArray(); char[] usr = userAnswer.trim().toUpperCase().toCharArray(); Set<Character> stdSet = Arrays.stream(new String(std).split("")) .map(s -> s.charAt(0)).collect(Collectors.toSet()); Set<Character> usrSet = Arrays.stream(new String(usr).split("")) .map(s -> s.charAt(0)).collect(Collectors.toSet()); return stdSet.equals(usrSet); } }

逻辑说明:单选题和判断题用equalsIgnoreCase直接比较字符串,多选题需要把"ABD"拆成字符集合再比较,这样考生回复"BAD"也能得分。注意这里用的是「全对才得分」的策略,如果你要的是「漏选得一半分」,那就需要先判断usrSet是否是stdSet的子集再加分,逻辑要对应改。

参数说明:standardAnswer.trim()是为了去掉数据库里可能的空格,toUpperCase()统一转大写避免大小写干扰。这套代码里标准答案的存储格式是"ABD"这种紧凑字符串,如果数据库里存的是"A,B,D"带逗号格式,判分前要先replace(",", "")处理。成绩回写逻辑通常放在ExamRecordServiceImpl里,累加每题得分后更新exam_record表的total_score字段。

4.4 前端考试页与后端接口的联调时序

前端 Vue 页面拿到试卷后的典型流程是:页面加载时调exam/detail接口拿题目,用户逐题点击选项,点击交卷按钮后收集所有答案成一个数组,POST 到exam/submit。看一段交卷的 axios 调用:

// views/exam/preview.vue 内的交卷方法 submitExam() { const answers = this.questionList.map(q => ({ questionId: q.id, answer: q.userAnswer || '' })) request({ url: '/exam/submit', method: 'post', data: answers }).then(res => { if (res.code === 200) { this.result = res.data this.$message.success('交卷成功,得分:' + res.data.totalScore) } }) }

逻辑说明:前端把每道题的用户答案包装成{ questionId, answer }的结构提交,后端拿到后逐题判分、累加、回写考试记录。this.result = res.data把后端返回的得分存到组件 data 里,用于在结果页展示。这里要注意:后端返回的res.data.totalScore是你自己定义的返回结构,不是 axios 默认的response.data——如果后端返回体是{ code: 200, data: {...} }这种统一格式,命名要对齐,否则undefined会显示到页面上。

这段时序里常见的联调问题有两个:一个是前端点击交卷时网络超时,多半是后端判分逻辑里不小心做了全表查询;另一个是交卷后页面没有跳转到结果页,检查 Vue 路由是否配置了/exam/result/:recordId的动态路由,以及跳转传参是否用了query而不是params。

5. 避坑排查:复现这套 SSM 考试系统时我踩过的五个坑

5.1 MySQL 8.0 连不上:驱动类名和加密协议不匹配

现象:启动2-run.bat后控制台报Access denied for user 'root'@'localhost',但用 Navicat 用同样账号密码能连上。

原因:MySQL 8.0 默认的密码加密方式是caching_sha2_password,而 Spring Boot 1.x 或某些旧版本 MySQL Connector/J 用的是mysql_native_password,导致认证协议不兼容。这套资源的driver-class-name如果还是com.mysql.jdbc.Driver,在 MySQL 8.0 下会直接报驱动类找不到。

解决:把配置里的驱动类改成com.mysql.cj.jdbc.Driver(注意多了一个.cj),同时执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';来兼容旧驱动。如果你用的是 MySQL 5.7,就保持原来的驱动类不变,别乱改。

5.2 前端页面白屏:静态资源被拦截器挡了

现象:后端启动成功,登录页能打开,但登录之后跳转主页,整个页面空白,F12 控制台一堆 401。

原因:拦截器的excludePathPatterns里没放行/static/**。SSM 项目的前端资源一般放在src/main/resources/static或通过资源映射挂在/static路径下,拦截器把所有请求都挡了,CSS、JS 加载不到,页面自然白屏。

解决:检查WebConfig里的放行清单,确认/static/**、/favicon.ico、/index.html都在排除列表里。如果页面引用了图片素材,还要单独放行素材目录。可以临时把拦截器注释掉再启动,快速验证是不是拦截器导致的白屏。

5.3 上传视频素材报 413:文件大小限制没调

现象:上传图片素材正常,上传视频素材时网络请求直接失败,后端日志出现MaxUploadSizeExceededException。

原因:Spring Boot 默认的单个文件上传上限是 1MB,视频素材随便一个培训视频就是几十 MB。前端虽然传完了,但后端在 multipart 解析阶段直接抛异常拒绝了。

解决:在application.yml里把spring.servlet.multipart.max-file-size调大到50MB或更高,max-request-size调到100MB。注意如果你的系统是 Nginx 反代,还要同步改 Nginx 的client_max_body_size,否则前端会看到 413 而不是后端返回的 JSON 错误。两个位置漏一个,问题都不会消失。

5.4 考试页面加载慢:题库表没有索引

现象:题库里加了三千道题之后,组卷接口响应时间从 200ms 涨到 3 秒多,考试时考生明显感觉卡顿。

原因:ORDER BY RAND()需要全表扫描临时排序,题量涨上去之后性能直线下降。同时exam_record表按emp_id查询时没有索引,导致查成绩也变慢。

解决:给question_bank表的q_type加普通索引,给exam_record表的emp_id和exam_id加联合索引。组卷 SQL 里对q_type走索引过滤后再随机排序,能快一个数量级。如果题量继续增长,换用「先查MIN(id)到MAX(id)区间再随机取 id」的两步法,避免全表随机排序。

5.5 中文乱码:连接参数和数据库字符集脱节

现象:题库录入的中文题干在页面显示为???,或者从 Excel 导入题目时中文全部丢失。

原因:数据库连接 URL 里没有加characterEncoding=utf8,或者数据库本身建库时没指定utf8mb4字符集。MySQL 的默认字符集在有些系统上是latin1,只能存英文字符,中文进去就变成问号。

解决:三步检查——先看jdbc:mysql连接串有没有useUnicode=true&characterEncoding=utf8;再看建库语句有没有DEFAULT CHARSET utf8mb4;最后看表的字符集。如果数据已经录入成问号,删掉重建才有救,所以第一次建库时这两处配置必须一次到位。另外一个容易忽略的点:前端index.html要加<meta charset="utf-8">,否则高版本浏览器从旧页面跳转过来也会以gbk解析页面,导致显示乱码。

6. 进阶技巧:把 Excel 题库批量导入做进这套系统

6.1 用 Apache POI 解析 Excel 批量入库

这套系统如果只靠后台一个个手工录题,题库永远厚不起来。真实企业场景里,培训管理员手里多半已经有一份 Excel 题库,最省事的落地方式就是加一个批量导入接口。常见做法是用 Apache POI 读.xlsx文件,逐行映射到QuestionBank实体再批量插入:

import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.web.multipart.MultipartFile; import java.util.ArrayList; import java.util.List; public List<QuestionBank> parseExcel(MultipartFile file) throws Exception { List<QuestionBank> list = new ArrayList<>(); try (Workbook workbook = new XSSFWorkbook(file.getInputStream())) { Sheet sheet = workbook.getSheetAt(0); // 从第 2 行开始遍历,跳过表头 for (int i = 1; i <= sheet.getLastRowNum(); i++) { Row row = sheet.getRow(i); if (row == null) continue; QuestionBank q = new QuestionBank(); q.setQType((int) row.getCell(0).getNumericCellValue()); q.setContent(row.getCell(1).getStringCellValue()); q.setOptionA(row.getCell(2).getStringCellValue()); q.setOptionB(row.getCell(3).getStringCellValue()); q.setOptionC(row.getCell(4).getStringCellValue()); q.setOptionD(row.getCell(5).getStringCellValue()); q.setAnswer(row.getCell(6).getStringCellValue()); q.setScore((int) row.getCell(7).getNumericCellValue()); list.add(q); } } return list; }

逻辑说明:XSSFWorkbook只负责解析.xlsx格式,如果你手里的文件是老的.xls,得换成HSSFWorkbook,或者统一用WorkbookFactory.create(inputStream)让它自动判断格式。代码里用getNumericCellValue取数字型单元格,用getStringCellValue取文本型,混合类型的 Excel 文件在这一步最容易抛Cannot get a STRING value from a NUMERIC cell异常,导入前最好让题库模板的列格式统一。

参数说明:getCell(0)到getCell(7)对应 Excel 的 A 到 H 列,列的顺序跟你导出的模板必须一致。我把题型列设为数字 0/1/2,是因为 POI 读文本型数字容易带出科学计数法,直接用数字列更稳。注意row.getCell(0).getNumericCellValue()返回的是double,转int前要确认单元格里真的是整数。如果导入经常翻车,可以按题量分批把list切成每 500 条一组,用 MyBatisPlus 的saveBatch分批插入,避免大事务锁表。

6.2 用成绩导出来验证整个链路的正确性

加了导入还不够,考试之后管理员需要看到成绩报表。我一般会提供一个简单的成绩导出接口,生成 CSV 而不是 Excel,因为 CSV 不用额外引入导出库,浏览器直接就能打开:

public void exportCsv(HttpServletResponse response, Long examId) throws IOException { List<ExamRecordVO> records = examRecordMapper.selectExamResult(examId); response.setContentType("text/csv;charset=utf-8"); response.setHeader("Content-Disposition", "attachment;filename=exam_result.csv"); StringBuilder sb = new StringBuilder(); sb.append("\uFEFF"); // BOM 头,防止 Excel 打开 CSV 中文乱码 sb.append("工号,姓名,部门,得分,用时,交卷时间\n"); for (ExamRecordVO r : records) { sb.append(r.getEmpNo()).append(',') .append(r.getEmpName()).append(',') .append(r.getDepartment()).append(',') .append(r.getTotalScore()).append(',') .append(r.getDuration()).append(',') .append(r.getSubmitTime()).append('\n'); } response.getWriter().write(sb.toString()); }

逻辑说明:selectExamResult是ExamRecordMapper里的一个自定义联表查询,把exam_record和employee表 join 起来取工号、姓名、部门。CSV 拼接时数字和字符串在 Excel 里可能被自动转换格式(比如工号变科学计数法),字段前面可以拼一个单引号或设单元格格式,实际生产里我会老老实实用 POI 出 xlsx。\uFEFF这个 BOM 头是经验之谈——不加它,你用 Excel 打开导出的 CSV,中文全是乱码。

从那次被乱码折磨之后,我拿到任何一套 SSM 源码,都会强制走一遍固定流程:先查连接串字符集,再审拦截器放行列表,再配好file.max-file-size,最后用最小数据集把「登录 → 组卷 → 判分 → 导出」这条链路完整跑通,再往里面填真实业务。这套员工知识培训考试系统的源码结构其实很标准,你按这个顺序拆,半天就能把它变成一套能交付的考试平台。希望帮到你。

本文还有配套的精品资源,点击获取