首页 / 资讯中心 / 文章详情

SpringBoot+Vue+MySQL考试系统源码解析:架构、部署与实战

SpringBoot+Vue+MySQL考试系统源码解析:架构、部署与实战 ★ FEATURED ARTICLE
从网上下载一份标注着“可直接运行”的考试系统源码时我通常不会急着双击启动而是先花半小时确认三件事数据库脚本是否完整、JDK和Node版本是否和本地匹配、前端依赖能否顺利装完。这三件里有任何一件对不上“可直接运行”就会变成连环报错现场。这套SpringBoot后端加Vue前端加MySQL的考试系统信息管理系统正好就是这样一个典型项目——技术栈不复杂但真正跑通它、看懂它、改明白它需要把前后端的数据流、登录鉴权、组卷逻辑、阅卷规则和部署细节全部串起来。这篇文章就围绕这套考试系统源码展开后端怎么分层、前端怎么组织、数据库五张核心表怎么设计以及我从零把它跑起来的过程中踩过的坑。对正在做课程设计、毕业设计或者想快速搭一套考试系统的开发者来说看完这篇可以直接照做少走很多弯路。1. 拆项目一份“可直接运行”的源码先看清前后端的家底拿到任何前后端分离项目第一件事不是运行而是读目录。目录结构就是项目的骨架骨架理解透了后面排查问题才有方向感。这套考试系统的源码结构非常典型后端是标准的SpringBoot工程前端是Vue单页应用两者通过JSON格式的REST接口通信数据最终落到MySQL。1.1 主目录结构后端三层与前端组件各管一摊后端工程的主目录一般是这样exam-system-backend ├── src/main/java/com/exam │ ├── controller # 接口层接收前端请求 │ ├── service # 业务层写核心逻辑 │ ├── dao/mapper # 数据访问层操作数据库 │ ├── entity # 实体类对应数据库表 │ ├── config # 配置类比如跨域、拦截器 │ └── utils # 工具类JWT生成、密码加密等 ├── src/main/resources │ ├── application.yml # 数据源、端口、日志配置 │ └── mapper # MyBatis的XML映射文件 └── pom.xml # Maven依赖清单这是标准的Controller-Service-Mapper三层架构。Controller只负责接收参数、调用Service、返回结果不写业务逻辑Service是核心业务所在比如登录校验、组卷抽题、阅卷计分Mapper封装对数据库的操作。实体类则是一张张表的Java映射字段名和数据库列名一一对应。前端工程的主目录结构如下exam-system-frontend ├── src │ ├── api # 按业务模块封装的请求接口 │ ├── assets # 静态资源 │ ├── components # 通用组件比如倒计时、答题卡 │ ├── router # 前端路由表 │ ├── store # 全局状态管理存用户登录信息 │ ├── views # 页面比如登录页、考试页、管理后台 │ ├── App.vue │ └── main.js ├── package.json # 前端依赖清单 └── vue.config.js # webpack配置包含开发代理前端这块看点主要在router和store。router管的是页面跳转权限哪些页面需要登录后才能访问哪些页面只有管理员能进store存的是登录用户的token和基本信息刷新页面时用来恢复登录态。1.2 技术选型背后的真实理由这套项目选SpringBoot而不是SpringMVC或者SSH核心原因是SpringBoot把大量繁琐配置变成了“约定大于配置”。内嵌Tomcat、自动装配数据源、starter机制一键引入依赖配合MyBatis-Plus使用单表增删改查几乎不用写SQL。对考试系统这种以增删改查为主的业务系统来说开发效率非常高。Vue这边选Vue 2还是Vue 3要看源码里的package.json但无论哪个版本组件化开发对考试系统的适配度都很高——登录页是一个组件、答题页是一个组件、管理后台的题库管理又是一个组件页面间通过路由跳转状态通过Vuex或Pinia管理数据驱动视图更新开发体验比原生JavaScript写DOM操作强太多。MySQL是这三个组合里最容易被忽略但最不该被忽略的一环。考试系统的数据量不大但表结构之间的关联关系很清晰事务要求也高——比如提交答卷时考试记录和答题详情必须同时写入不能出现记录写了一半的情况。MySQL的InnoDB引擎天然支持事务配合Spring的Transactional注解可以非常方便地保证数据一致性。提示看一份源码之前先看它的pom.xml和package.json里依赖版本的组合再对照自己本地的JDK、Node、MySQL版本。版本差一代踩坑的概率至少翻一倍。2. 考试系统数据库建模角色、题库、试卷、答题四类核心表数据库设计是这套系统里最见功底的部分。考试业务说到底就是一个闭环用户登录、管理员维护题库和试卷、考生答题、系统自动阅卷、成绩落库。整个闭环涉及的数据对象不多但表之间的关联关系需要讲清楚。2.1 用户与角色表权限的落地方式用户表是系统的入口最简单的设计是这样的CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT 密码MD5加盐存储, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, role_type tinyint(1) DEFAULT NULL COMMENT 1管理员 2教师 3考生, status tinyint(1) DEFAULT 1 COMMENT 1正常 0禁用, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个细节值得注意。第一密码不存明文而是用MD5加盐或BCrypt加密后存储防止数据库泄露导致密码裸奔。第二角色用数字类型而不是字符串枚举方便后端代码里用整数比较控制权限。第三加了status字段做逻辑禁用而不是直接删除记录这样即使要封号也不影响历史考试记录。在实际项目里管理员和考生可能还需要单独的扩展信息表比如考生有班级和学号管理员有部门但这套简化设计的核心思路是“用户只存公共字段扩展信息按角色拆分表”避免一张表字段过于冗余。2.2 题库表与组卷中间表一道题怎么进一张卷题库表的设计直接决定组卷逻辑的复杂度。核心字段包括题目类型、题干、选项、正确答案、分值、解析CREATE TABLE exam_question ( id int(11) NOT NULL AUTO_INCREMENT, question_type tinyint(1) DEFAULT NULL COMMENT 1单选 2多选 3判断 4主观, 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 答案多选用逗号分隔如A,B,C, score decimal(5,1) DEFAULT NULL COMMENT 分值, analysis text COMMENT 题目解析, creator_id int(11) DEFAULT NULL COMMENT 出题人, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;判断题的选项可以留空答案字段存“正确”或“错误”主观题的四个选项也留空答案字段存参考要点具体得分由人工在后台复核时给出。这种“一张表通吃多种题型”的设计简洁且扩展性好——以后如果要增加填空题型只需要约定答案格式即可不需要改表结构。试卷和题目的关系是一张独立的中间表而不是在试卷表里存一个题目ID列表。原因很简单关系型数据库不适合存列表如果一列存多个题目ID后面想调整题目顺序、统计每道题的分值分布都会非常难受。正确的做法CREATE TABLE exam_paper_question ( id int(11) NOT NULL AUTO_INCREMENT, paper_id int(11) NOT NULL COMMENT 试卷ID, question_id int(11) NOT NULL COMMENT 题目ID, sort_no int(11) DEFAULT NULL COMMENT 题目顺序号, PRIMARY KEY (id), KEY idx_paper_id (paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;中间表的sort_no字段往往被初学者忽略但它很重要。试卷上题目的顺序是固定的A卷第一题是单选第1题第二题是单选第2题这个顺序必须显式存储。如果靠查询结果的默认顺序一旦数据插入顺序变化或者后续调整试卷结构就乱了。试卷主表只需要存与题目无关的信息CREATE TABLE exam_paper ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL COMMENT 试卷名称, duration int(11) DEFAULT NULL COMMENT 考试时长单位分钟, total_score decimal(5,1) DEFAULT NULL COMMENT 总分, start_time datetime DEFAULT NULL COMMENT 开考时间, end_time datetime DEFAULT NULL COMMENT 结束时间, status tinyint(1) DEFAULT 0 COMMENT 0未发布 1已发布, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;total_score是一个冗余字段理论上可以通过中间表关联所有题目后的分值汇总得出。但保留冗余字段可以避免每次都做聚合查询而且发布试卷时校验总分是否符合预期也更方便。这类冗余在业务系统里很常见只要保证创建和编辑试卷时同步更新即可。2.3 考试记录与答题详情从开考到交卷的状态流转考试记录表是考生和试卷之间的桥梁记录一次考试的整体状态CREATE TABLE exam_record ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 考生ID, paper_id int(11) NOT NULL COMMENT 试卷ID, total_score decimal(5,1) DEFAULT NULL COMMENT 最终得分, start_time datetime DEFAULT NULL COMMENT 开考时间, submit_time datetime DEFAULT NULL COMMENT 交卷时间, status tinyint(1) DEFAULT 0 COMMENT 0考试中 1已交卷 2异常终止, PRIMARY KEY (id), KEY idx_user_paper (user_id, paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;state字段的流转逻辑很简单点“开始考试”时创建一条status0的记录交卷时更新状态和总分。如果考生中途关闭了浏览器start_time已经写入后端可以根据它和考试时长判断是否需要由管理员手动收卷或者设置一个定时任务自动提交。答题详情表记录考生对每道题的具体作答CREATE TABLE answer_detail ( id int(11) NOT NULL AUTO_INCREMENT, record_id int(11) NOT NULL COMMENT 考试记录ID, question_id int(11) NOT NULL COMMENT 题目ID, user_answer varchar(10) DEFAULT NULL COMMENT 考生答案多选用逗号分隔, is_correct tinyint(1) DEFAULT NULL COMMENT 1正确 0错误, score decimal(5,1) DEFAULT NULL COMMENT 本题得分, PRIMARY KEY (id), KEY idx_record_id (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里要注意阅卷完成后得到的得分和是否正确最好落库存储而不是每次查询时再算一遍。原因有两点一是如果考试规则后续调整历史考试成绩不应受影响二是成绩查询页面往往需要高频访问直接读库比实时计算高效得多。这套表结构整体上没有物理外键。不是说外键无用而是互联网应用和海量表结构的项目里物理外键会带来两个问题删除数据时要考虑外键约束顺序后续分库分表时外键失效。核心做法是在SQL查询时用JOIN关联逻辑外键在代码层面保证数据完整性。3. 后端SpringBoot登录鉴权、组卷抽题、自动阅卷三个关键链路后端代码量不小但真正值得掰开看的就三块登录鉴权、组卷、自动阅卷。把这三块的代码逻辑理清楚整个系统的核心就掌握了。3.1 JWT登录鉴权与全局拦截器考试系统有管理员、教师、考生三种角色不同角色能访问的接口必须做权限控制。最常见的实现是JWT配合拦截器。登录接口的逻辑是接收用户名密码查询数据库比对密码通过后生成一个带用户信息的JWT令牌返回给前端。前端把令牌存到localStorage后续所有请求在请求头里带上tokenRestController RequestMapping(/api/auth) public class AuthController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO loginDTO) { User user userService.login(loginDTO.getUsername(), loginDTO.getPassword()); // 登录成功后生成JWT令牌有效期设置2小时 String token JwtUtil.generateToken(user.getId(), user.getUsername(), user.getRoleType()); MapString, Object data new HashMap(); data.put(token, token); data.put(userInfo, user); return Result.success(data); } }JWT令牌的生成逻辑也不复杂常见的内部实现是使用io.jsonwebtoken库载荷中放入用户ID、用户名、角色ID签名密钥放在配置文件中过期时间一般设为2小时public static String generateToken(Integer userId, String username, Integer roleType) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .claim(roleType, roleType) .setExpiration(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); }有了令牌后端需要一个全局拦截器在请求进入Controller之前校验token的有效性。SpringBoot中实现HandlerInterceptor即可Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || token.isEmpty()) { throw new BusinessException(401, 请先登录); } Claims claims JwtUtil.parseToken(token); if (claims null) { throw new BusinessException(401, 登录已过期); } request.setAttribute(userId, claims.get(userId)); return true; } }拦截器注册到WebMvcConfig中并指定放行那些不需要登录就能访问的接口比如登录接口、注册接口其他的全部拦截。管理员接口再通过自定义注解加一重角色校验比如RequireAdmin注解里判断token解析出来的roleType是否为1。3.2 组卷逻辑中间的sort_no与随机打散组卷是教师端的核心操作。一种方式是教师创建试卷时手工从题库里挑题另一种是系统随机抽题。这套源码里两种逻辑同时存在手工挑题时往中间表插入记录并记录sort_no随机抽题时先按题型比例从题库里抽取题目再写入中间表。随机抽题的核心SQL利用ORDER BY RAND()实现数据量几百道题时性能没问题上万道题时才需要考虑优化方案public ListExamQuestion generateQuestionsByType(Integer questionType, Integer count) { return examQuestionMapper.selectRandomQuestions(questionType, count); }对应的XML查询select idselectRandomQuestions resultTypeExamQuestion SELECT * FROM exam_question WHERE question_type #{questionType} ORDER BY RAND() LIMIT #{count} /select组卷完成后前端发起交卷时后端返回的题目列表顺序由中间表的sort_no升序排列。这里有一个容易被忽略的问题每次打开试卷进入答题时题目顺序不能变化否则考生记忆题号位置的体验会很差所以查询时必须ORDER BY sort_no不能依赖数据库的默认返回顺序。GO题库查询如果遇到排序混乱的情况基本都是忘了加这一行。3.3 自动阅卷选择题的精确匹配与主观题的兜底方案自动阅卷的逻辑是整个考试系统最有业务复杂度的地方。单选题、多选题、判断题的判分规则不同主观题则需要人工介入。核心代码如下public BigDecimal calculateScore(AnswerDetail detail, ExamQuestion question) { if (question.getQuestionType() 1) { // 单选题答案精确匹配 return detail.getUserAnswer().equals(question.getAnswer()) ? question.getScore() : BigDecimal.ZERO; } if (question.getQuestionType() 2) { // 多选题全对得满分漏选错选均不得分 SetString userSet new HashSet(Arrays.asList(detail.getUserAnswer().split(,))); SetString answerSet new HashSet(Arrays.asList(question.getAnswer().split(,))); return userSet.equals(answerSet) ? question.getScore() : BigDecimal.ZERO; } if (question.getQuestionType() 3) { // 判断题匹配正确或错误 return detail.getUserAnswer().equals(question.getAnswer()) ? question.getScore() : BigDecimal.ZERO; } // 主观题需要人工评分得分先置为0 return BigDecimal.ZERO; }多选题判分这里特别提一下。很多考试会采用“漏选得部分分、错选不得分”的规则但这套简化的系统采用的是更加严格的全对才得分。如果要改成部分得分则需要把规则改成“用户选项集合是正确答案集合的子集时按比例得分”Z集合isEmpty的判断和比例计算都不复杂。实际开发中规则怎么定要跟业务方确认清楚这是最容易产生需求返工的点。整个阅卷流程在考生点击交卷时一次性触发系统遍历答题详情逐题计分累加得到总分然后事务性地更新exam_record记录和所有answer_detail记录的得分。Transactional public void submitExam(Integer recordId) { ExamRecord record examRecordMapper.findById(recordId); ListAnswerDetail detailList answerDetailMapper.findByRecordId(recordId); BigDecimal totalScore BigDecimal.ZERO; for (AnswerDetail detail : detailList) { ExamQuestion question examQuestionMapper.findById(detail.getQuestionId()); BigDecimal score calculateScore(detail, question); detail.setScore(score); detail.setIsCorrect(score.compareTo(BigDecimal.ZERO) 0 ? 1 : 0); totalScore totalScore.add(score); answerDetailMapper.update(detail); } record.setTotalScore(totalScore); record.setStatus(1); record.setSubmitTime(new Date()); examRecordMapper.update(record); }Transactional这个注解必须加原因很直接如果中途有一道题更新失败了总分和每题得分可能会出现不一致的状态。事务保证所有写入要么全部成功要么全部回滚考试数据的准确性是底线。提示交卷接口存在并发提交的可能——考生双击交卷按钮、或者重复刷新提交会触发同一recordId的多次执行。处理方式有两种前端置灰按钮加loading后端在交卷前先检查record的status如果已经是1则直接返回“已交卷”两重保险缺一不可。4. Vue端实现登录、答题、后台管理三端页面骨架后端接口再齐全最终还是要通过前端页面跟用户交互。这套考试系统的前端页面可以分为三大块考生登录及考试页、管理后台的题库和试卷管理页、公共布局和状态管理。我重点说三个直接关系到系统能不能用顺的细节。4.1 路由守卫与登录态恢复前端路由表一般分成两级基础路由放登录页受保护的路由放在一个需要鉴权的父级路由下面。Vue Router配置里最常见的写法const router new VueRouter({ mode: hash, // 也可以改成history但部署时要注意404问题 routes: [ { path: /login, component: Login }, { path: /, component: Layout, redirect: /exam/list, meta: { requiresAuth: true }, children: [ { path: exam/list, component: ExamList, meta: { requiresAuth: true } }, { path: exam/start/:id, component: ExamDetail, meta: { requiresAuth: true } } ] }, { path: /admin, component: AdminLayout, meta: { requiresAuth: true, role: 1 }, children: [] } ] })路由守卫是Vue端的权限防线。每次跳转前检查meta.requiresAuth没有token就拦到登录页有token但访问了role为管理员的页面就弹出“无权限”提示router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) return } const userInfo JSON.parse(localStorage.getItem(userInfo) || {}) if (to.meta.role userInfo.roleType ! to.meta.role) { next(/exam/list) return } next() })这里有一个体验细节刷新页面时Vue实例重新创建localStorage里的token和userInfo还在所以路由守卫直接读localStorage是可行的。但比localStorage更稳妥的是把userInfo存到Vuex里在应用启动时读取一次localStorage并同步到Vuex后续组件都从store里获取用户角色信息避免到处解析localStorage。4.2 请求封装、跨域代理与统一错误码Axios封装几乎是Vue项目的标配。核心意图只有一个把所有公共逻辑集中到一个地方处理。请求拦截器统一挂token响应拦截器统一处理错误码和401跳转import axios from axios const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data // 后端统一返回 { code: 200, message: success, data: {} } if (res.code ! 200) { if (res.code 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(new Error(res.message)) } return res }, error { return Promise.reject(error) } ) export default service开发环境的跨域问题是后端同学和前端同学最容易争论的地方。解决方式有两种一是后端加CrossOrigin注解或CorsFilter全局配置二是前端在vue.config.js里配devServer的proxy代理。更推荐的方式是代理因为生产环境前后端往往是同域部署开发环境通过代理模拟同域请求代码里不需要任何后端环境相关的配置module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: /api } } } } }4.3 计时交卷与答题卡交互考试页是考生体验的核心。这一类页面的关键交互有三个倒计时、答案暂存、交卷确认。倒计时的实现可以借助setInterval读取试卷返回的duration字段从开考时间开始计算剩余时间最后几分钟做红色高亮提示。答题卡交互是另一个值得注意的细节。考生每答完一道题左侧题号应该改变背景色标记这道题“已答”。组件的实现思路是把当前答卷映射为一个对象key是questionIdvalue是答案字符串通过监听对象的变化来动态计算题号的状态渲染。交卷前必须弹二次确认框确认框除了提示“确认交卷”之外最好顺便统计“已答X题、未答Y题”因为很多考生会因为漏答题而发现问题。交卷接口调用成功后跳转到成绩页或试卷回看页清空本地的答题暂存。如果这套源码的答题页出现倒计时不准的问题多半是用了系统时间而不是服务器时间。前端本地时间可以被篡改严谨的做法是页面加载时同步服务器时间戳然后在前端做递减。不过大多数课程设计和内部考试系统直接用new Date().getTime()也够用关键是要明白这个偏差的存在。5. 从零跑通JDK、MySQL、Node三件套准备与执行步骤前面讲了架构和核心代码现在进入“能不能跑起来”的关键环节。标题里写“可直接运行”但运行的前提是底层的运行环境已经就绪。本地的软件环境、数据库初始化、项目配置调整这三个环节任何一个出问题都会中断流程。5.1 环境版本清单与常见组合跑这套考试系统建议准备以下软件环境软件建议版本说明JDK1.8 或 11SpringBoot 2.x版本对应JDK8即可3.x需要JDK17Maven3.6.x 或 3.8.x用于后端依赖下载和启动MySQL5.7 或 8.0注意驱动类名差异和服务器时区设置Node.js14.x 或 16.x具体看package.json里node-sass版本npm/yarnnpm 6以上随Node自动安装IDEA 或 VS Code任意新版本后端推荐IDEA前端VS Code够用拿到源码后第一件事是打开pom.xml看SpringBoot版本。如果是2.x用JDK8毫无压力如果是3.xJDK8肯定跑不起来必须换JDK17。Node版本的匹配则要看package.json里有没有node-sass或sass-loader有的话版本锁得非常死直接按下面这个对照关系来node-sass版本对应的Node版本4.14.xNode 146.0.xNode 167.0.xNode 188.0.xNode 205.2 数据库初始化命令行脚本导入与可视化工具导入找到项目里的exam_system.sql或者database目录下的脚本这是数据库初始化的关键文件。确认本机MySQL服务已经启动后命令行导入的方法如下mysql -u root -p Enter password: ****** # 创建数据库注意字符集 CREATE DATABASE exam_system DEFAULT CHARACTER SET utf8mb4; # 退出当前交互执行导入 mysql -u root -p exam_system exam_system.sql可视化工具比如Navicat导入更简单新建数据库exam_system字符集选utf8mb4然后右键运行SQL文件选中脚本执行即可。这里有个高频问题SQL脚本执行时报错报错位置往往卡在表名的反引号或某个字符集设置上。多数情况都是因为SQL文件本身编码不对——用记事本打开后另存为UTF-8编码再执行基本能解决。导入完成后可以用SHOW TABLES;验证一下有哪些表确保核心的几张大表都在。好习惯是改一下MySQL的默认认证插件尤其是MySQL 8.0用Navicat连接时会遇到 caching_sha2_password 报错ALTER USER改成 mysql_native_password 再刷新权限即可。5.3 后端启动和前端启动的标准操作数据库初始化完成后改后端配置文件application.yml核心就三项数据源URL、用户名、密码server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver后端启动方式二选一。第一种是命令行mvn spring-boot:run第二种是IDEA里找到Application启动类右键直接Run。启动成功后控制台会看到SpringBoot的横幅和Tomcat started on port(s): 8080的日志。前端启动方式如下cd exam-system-frontend # 安装依赖这一步耗时最长 npm install # 启动开发服务器 npm run dev启动成功后控制台会显示一个本地地址一般是 http://localhost:8081。浏览器打开这个地址能看到登录页说明前后端联通成功。用管理员账号登录后可以进入到管理后台创建题库和试卷再用普通考生账号参加考试整个链路就完整了。6. 实际运行中容易翻车的五个细节这部分是我实际运行这类项目时反复踩过的坑记录下来给后来人省点时间。每一个都真实发生过并且产生过至少半小时的排查成本。6.1 MySQL8的驱动类名、时区与SSL连接报错MySQL 5.7时代的驱动类名是com.mysql.jdbc.Driver从MySQL 8.0开始改成了com.mysql.cj.jdbc.Driver。pom.xml里如果引入的是mysql-connector-java 8.x版本却还沿用老的驱动类名启动时会直接报ClassNotFoundException。MySQL 8.0还会强制要求设置时区。连接串里不添加serverTimezoneAsia/Shanghai大概率报连接超时或者The server time zone value错误。另外MySQL 8默认启用SSL自动配置可能导致本地出现SSL connection报错虽然不是致命错误但日志里会很碍眼可以在URL后面加上useSSLfalse消除。url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse6.2 CORS跨域与webpack代理冲突前端开发服务器跑在8081端口后端跑在8080端口前端请求 /api/report 经过代理转发到后端这是标准方案。但如果后端代码里又加了CrossOrigin配置同时前端代理也做了有时会出现二次转发或者预检请求OPTIONS无法处理的情况。我的建议是二选一开发阶段优先用前端代理后端不写跨域配置生产阶段前后端部署到同一个域两级路径区分根本不存在跨域。混用两个方案反而容易出一些隐蔽问题。如果一定要后端支持跨域就需要在Spring的配置类里单独写CorsFilter只放行指定来源不要直接allowAllOrigins。6.3 Node版本和node-sass锁死的兼容关系npm install是前端环境里最脆弱的环节报错最多的就是node-gyp rebuild和node-sass相关错误。本质原因是node-sass是一个原生模块必须与Node二进制版本严格匹配。Node版本和node-sass版本不匹配时下载下来的二进制文件无法加载启动时直接报模块错误。查看当前node版本可以执行node -v再对照package.json里的node-sass版本决定是换Node版本还是换node-sass版本。现在的新项目早就用sass替代node-sass了旧项目如果卡在这里最快的解法就是用nvm切换Node版本而不是硬编node-sass因为前者一分钟能解决后者半小时起步。6.4 后端端口被占用与启动顺序后端明明配置了8080端口启动却提示端口已占用。排查方法很简单命令行执行netstat -ano | findstr 8080找到占用端口的PID确认不是重要进程后结束它。改端口是最后的备选方案因为前端代理配置里target的地址也要一起改改动面会变大。启动顺序也有讲究。必须先启动MySQL再启动后端最后启动前端。如果后端启动时MySQL没起来会马上报数据库连接失败。但这里有一个特性SpringBoot数据源初始化失败会导致整个应用启动失败后端必须先保证数据库可用。6.5 前端刷新404与路由history模式Vue Router如果用的是history模式本地npm run dev不会有问题但打包上线后刷新非首页路由会404。本质原因是开发服务器对任意路径都会回退到index.html而Nginx或Tomacat等静态资源服务器不会自动做这个回退。解决方案有两种。一是路由改回hash模式路径里会带#简单但不够美观。二是在Nginx配置里加try_files指令location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }这样刷新任何前端路由都会重新加载index.html再由前端路由接管。我在实际运行这套项目时还有最后一个体会源码跑通只是第一步真正把它变成自己的东西一定要动手改一个完整的功能链路。比如把单选题的随机抽题改成按难度系数抽题或者给考试记录增加一个成绩导出功能。改一次才能真正理解表结构为什么这么设计、后端接口为什么返回这个格式、前端为什么在那个位置调用。这份考试系统源码最大的价值不在于“能运行”而在于它是一张非常标准的学习地图值得好好拆一遍。
阅读完成 · 觉得有帮助?
咨询建站