新闻详情

新闻详情

首页 / 资讯中心 / 详情

Spring Boot 3+Vue 3在线考试系统实战:数据库设计与JWT认证

发布时间:2026/9/5 18:17:59来源:尧图网络
Spring Boot 3+Vue 3在线考试系统实战:数据库设计与JWT认证
在线考试系统是 Java 全栈学习里很适合做完整闭环的项目它不只是登录加两张表而是包含了考试建模、题目管理、在线答题、自动评分、成绩查询这一条完整业务链路。很多初学者做这个项目时容易把重心放在“页面多不多、按钮全不全”上结果到了设计试卷、提交答案、算分、判断重复提交这些环节才发现代码结构没想清楚事务也不知道该放在哪一层。这篇教程以 Java 17、Spring Boot 3、Vue 3、MySQL 8 为例从数据库设计讲到前后端联调最后给出常见报错的排查路径。示例代码用于说明设计思路实际项目需要结合自己的包名、路径、版本和教研要求再调整。1. 在线考试系统的核心链路与技术选型1.1 先画出最小可运行的业务闭环在设计页面和接口之前先想清楚一条最小闭环能走通哪些功能。在线考试系统建议先定义两种角色教师或管理员负责组卷、发布考试、查看统计学生负责查看可参加的考试、开始答题、提交试卷、查看成绩。最小闭环可以这样走教师创建一场考试并录入题目发布考试学生看到考试列表学生点击开始后进入答题页提交后服务端自动判分并生成成绩学生刷新成绩页可以看到分数。这个闭环不需要一开始就做题库导入、随机组卷、班级管理、防作弊等功能但它的成功打通意味着项目的主链路已经成立。如果一开始就把用户管理、考试管理、组织管理、题库管理、监考设置全部堆在一起前后端联调时基本无法定位问题是出在哪一层。先跑通一个最小闭环再逐步加功能是更适合课程设计和工程实践的顺序。1.2 技术栈分工为什么是 Spring Boot 3、Vue 3、MySQL在线考试系统采用前后端分离时Spring Boot 3 负责 REST API、认证、业务事务和评分逻辑Vue 3 负责登录、答题、管理等页面交互MySQL 负责持久化用户、考试、题目和成绩数据。这里有一个容易被忽略的版本前提Spring Boot 3 是基于 Jakarta EE 9 规范实现的包名从javax.*迁移到了jakarta.*并且运行环境要求 Java 17 及以上。网上大量旧教程基于 Spring Boot 2.7 和 Java 8里面的依赖坐标、包名写法并不适合直接套用到 Spring Boot 3。Vue 3 配合 Vite 开发时Node.js 版本也有下限要求建议使用 Node.js 18 以上版本。MySQL 使用 8.x 会更合适因为 8.x 对 JSON、窗口函数、utf8mb4 的支持更完整也更匹配当前主流开发环境。版本环境可以整理成一张速查表项目落地前先对照检查组件学习环境建议说明JDK17Spring Boot 3 最低要求不要用 Java 8Maven3.8 以上用于构建后端和管理依赖Spring Boot3.2.x 为例示范代码中会用到 jakarta 命名空间Node.js18 以上Vite 和 Vue 3 常用环境要求MySQL8.0使用 utf8mb4 字符集前端构建Vite Vue 3推荐 create-vite 初始化如果你的本机还是 Java 8 环境先完成 JDK 17 安装和环境变量配置再开始创建 Spring Boot 3 项目否则会出现“Unsupported class file major version”这类错误。安装 MySQL 时需要记清楚 root 密码、端口和连接参数后面配置spring.datasource.url会用到。1.3 ORM 选型这阶段不必纠结 JPA 还是 MyBatisSpring Boot 3 下可以选用 Spring Data JPA也可以选用 MyBatis-Plus。对于题库、试卷、答题记录这种表关系并不复杂的系统Spring Data JPA 可以让实体、仓储和事务代码更集中也比较容易读。初学者不需要为了“显得专业”而在第一版就引入复杂的查询框架。如果团队已经有 MyBatis 习惯完全可以在建表完成之后把代码改成对应的 Mapper 实现。关键是明确一点ORM 只负责对象与数据库表之间的映射真正需要你认真思考的是表结构、状态流、事务边界和接口校验。这篇教程的示例使用 Spring Data JPA。2. 数据库设计把考试、答题、评分拆成这几张表2.1 不要先写代码先确认状态和关系在线考试系统的数据库设计需要回答几个问题一个用户可以参加多场考试吗一场考试可以包含多少道题目一次提交之后还可以重复提交吗学生提交试卷后服务端要存哪些数据才能算分和复查课程设计阶段可以先按最常用的“单场考试、多个题目、一次有效提交”来设计先把数据表拆成四张核心表sys_user系统用户包含教师和学生。exam考试主表保存考试时间、时长、总分、发布状态。question题目表保存题目所属考试、题型、选项、标准答案和分值。exam_record考试记录表表示某位学生参加某场考试的答题会话。answer_record答题明细表保存每一道题的学生答案和判分结果。这里把question.exam_id直接指向exam.id是为了让第一版项目更容易理解。如果后面要做题库复用、随机抽题、不同考试复用同一道题再拆出独立的题库表和试卷题目关联表。2.2 初始化库表 SQL在 MySQL 中执行下面的建表脚本先建立一个独立库避免和其他项目表浑浊在一起CREATE DATABASE IF NOT EXISTS online_exam DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE online_exam; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) DEFAULT , role VARCHAR(20) NOT NULL DEFAULT STUDENT, status TINYINT NOT NULL DEFAULT 1, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE exam ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description VARCHAR(500) DEFAULT , start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, duration_minutes INT NOT NULL DEFAULT 60, total_score INT NOT NULL DEFAULT 0, pass_score INT NOT NULL DEFAULT 60, editor_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, type VARCHAR(20) NOT NULL COMMENT SINGLE / MULTIPLE / JUDGE, content VARCHAR(500) NOT NULL, options_json VARCHAR(1000) DEFAULT NULL, answer VARCHAR(200) NOT NULL, score INT NOT NULL DEFAULT 5, sort_no INT NOT NULL DEFAULT 0 ) ENGINEInnoDB; CREATE TABLE exam_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, user_id BIGINT NOT NULL, score INT DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT IN_PROGRESS, start_time DATETIME NOT NULL, submit_time DATETIME DEFAULT NULL, UNIQUE KEY uk_exam_user (exam_id, user_id) ) ENGINEInnoDB; CREATE TABLE answer_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, record_id BIGINT NOT NULL, question_id BIGINT NOT NULL, user_answer VARCHAR(200) DEFAULT , is_correct TINYINT DEFAULT 0, got_score INT DEFAULT 0, UNIQUE KEY uk_record_question (record_id, question_id) ) ENGINEInnoDB;上述设计的几个关键点如下status字段用数字表示草稿、已发布等状态比用字符串更适合与前端下拉选项对接。exam_record上加UNIQUE KEY uk_exam_user表示同一学生只能保留一条记录重复点击开始或提交时会被数据库拦截。answer_record上加UNIQUE KEY uk_record_question可以防止同一道题被重复写入或重复判分。2.3 为什么不直接把答案字段写死在题目表里题目表使用options_json存选项使用answer存标准答案。这种写法看起来简单但要注意考试发布后答案字段不能原样返回给前端页面。学生提交答案后服务端拿着answer字段与学生答案比较而学生端看到的接口必须把标准答案移除。把答案设计成字符串还有一个好处单选和多选可以统一处理。单选用A多选用A,B判断题用TRUE或FALSE。评分时先做归一化再比较字符串即可。如果答案经常变化或需要统计每个选项的答题情况后续可以升级成独立题目答案表。系统初始化时不要直接往sys_user表里插入明文密码推荐在 Spring Boot 启动时以 Java 方式创建内置账号密码使用 BCrypt 加密。因为后续登录校验会使用BCryptPasswordEncoder如果手写明文密码登录永远无法匹配。3. 后端工程搭建与认证链路3.1 创建 Spring Boot 3 项目并确认 Java 17后端工程可以使用 Spring Initializr 创建也可以直接在已有 Maven 工程中补全依赖。这里给出一个简化版的pom.xml关键部分parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.security/groupId artifactIdspring-security-crypto/artifactId /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency /dependencies这里的版本号是示例不是固定推荐值。Spring Boot 3.x 的小版本会持续更新落地前应通过 Maven 依赖树确认实际使用的版本。在 Spring Boot 3 中实体和 Java EE 接口的包名发生了变化例如 Spring MVC 的Controller、RestController来自org.springframework.stereotype但 Servlet API 从javax.servlet变成了jakarta.servlet。JPA 实体注解也统一使用jakarta.persistence.*。如果你的旧代码里写的是javax.persistence.Entity在 Spring Boot 3 下会直接编译失败。核心application.yml可以这样配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/online_exam?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: update show-sql: true open-in-view: false exam: jwt: secret: online-exam-jwt-secret-key-please-change-in-prod-123456 expire-hours: 12allowPublicKeyRetrievaltrue和useSSLfalse是本地开发中便于连接 MySQL 8 使用caching_sha2_password加密插件时的常见参数。生产环境应使用useSSLtrue或配置 TLS 证书而不是直接复制这段配置。3.2 内置账号初始化与密码加密因为表里没有明文密码这里使用一个启动初始化类创建教师和学生账号。如果用户名已存在就不重复创建Component public class DataInitializer implements ApplicationRunner { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; public DataInitializer(UserRepository userRepository, PasswordEncoder passwordEncoder) { this.userRepository userRepository; this.passwordEncoder passwordEncoder; } Override public void run(ApplicationArguments args) { createUserIfAbsent(teacher, teacher_real, 123456, TEACHER); createUserIfAbsent(student, student_real, 123456, STUDENT); createUserIfAbsent(admin, admin_real, 123456, ADMIN); } private void createUserIfAbsent(String username, String realName, String rawPassword, String role) { if (userRepository.findByUsername(username).isPresent()) { return; } SysUser user new SysUser(); user.setUsername(username); user.setPassword(passwordEncoder.encode(rawPassword)); user.setRealName(realName); user.setRole(role); userRepository.save(user); } }同时把BCryptPasswordEncoder注册为 Spring BeanConfiguration public class SecurityBeanConfig { Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }为什么要单独引入spring-security-crypto而不是直接使用spring-boot-starter-security课程设计阶段如果接入完整 Spring Security默认会拦截所有请求需要花时间理解过滤器链、认证管理器、用户详情服务等概念。先只用BCryptPasswordEncoder做密码加密再手写 JWT 拦截器处理登录态业务链路更清楚。生产项目如果要求细粒度权限再引入完整 Spring Security 或 Sa-Token。3.3 JWT 令牌生成与校验JWT 的逻辑是用户登录成功后服务端签发一个带过期时间的令牌后续请求在请求头Authorization: Bearer token中携带它。服务端解析令牌确认用户身份再允许访问对应资源。示例工具类如下使用 jjwt 0.11.5 版本编写Component public class JwtUtil { Value(${exam.jwt.secret}) private String secret; Value(${exam.jwt.expire-hours:12}) private int expireHours; private SecretKey getKey() { return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } public String createToken(Long userId, String username, String role) { Date now new Date(); Date expireAt new Date(now.getTime() expireHours * 3600L * 1000L); return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(username, username) .claim(role, role) .setIssuedAt(now) .setExpiration(expireAt) .signWith(getKey(), SignatureAlgorithm.HS256) .compact(); } public Claims parseToken(String token) { return Jwts.parserBuilder() .setSigningKey(getKey()) .build() .parseClaimsJws(token) .getBody(); } }这里最容易踩的坑有两个。第一secret必须足够长HMAC-SHA256 的密钥长度至少 32 字节否则Keys.hmacShaKeyFor会抛异常。第二jjwt 不同版本的 API 差异很大。0.11.x 使用parserBuilder()而 0.12.x 已将 API 调整为parser().verifyWith(...)。复制代码前先看自己项目里的 jar 版本不要拿旧代码硬编译。登录接口很简单根据用户名找到用户用 BCrypt 校验密码校验成功后创建令牌PostMapping(/login) public ResultLoginResponse login(RequestBody LoginRequest request) { SysUser user userRepository.findByUsername(request.getUsername()) .orElseThrow(() - new BizException(用户名或密码错误)); if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) { throw new BizException(用户名或密码错误); } String token jwtUtil.createToken(user.getId(), user.getUsername(), user.getRole()); return Result.ok(new LoginResponse(token, user.getRealName(), user.getRole())); }3.4 用拦截器保存用户上下文而不是每个方法重复解析为了防止每个 Controller 都调用 parseToken可以定义一个简单拦截器。拦截器负责放行登录、注册接口其余接口要求请求头携带合法 JWT并把解析出来的用户信息放进ThreadLocal。Component public class AuthInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; public AuthInterceptor(JwtUtil jwtUtil) { this.jwtUtil jwtUtil; } Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String auth request.get
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Qt5.15.16 ARM64静态库构建与部署实战指南 2026/9/5 18:54:04

Qt5.15.16 ARM64静态库构建与部署实战指南

简介:本资源为已编译完成的Qt 5.15.16静态库(ARM64架构),专为国产化信创环境下的嵌入式或桌面级Linux应用开发设计,面向使用麒麟V10(Kylin 202101-aarch64)等ARM64平台的C/Qt开发者,…

阅读更多 →
MoneyPrinterTurbo 安装部署完全指南:从 config.toml 配置到 Docker 与手动部署的 API/WebUI 双接口实践 2026/9/5 18:54:04

MoneyPrinterTurbo 安装部署完全指南:从 config.toml 配置到 Docker 与手动部署的 API/WebUI 双接口实践

MoneyPrinterTurbo 安装部署完全指南:从 config.toml 配置到 Docker 与手动部署的 API/WebUI 双接口实践 【免费下载链接】MoneyPrinterTurbo 利用 AI 大模型和自动化工作流,根据主题或关键词一键生成高清短视频。Generate HD short videos from a topic…

阅读更多 →
DataEase 内网部署指南:开源 BI 工具离线安装完整流程 2026/9/5 18:54:04

DataEase 内网部署指南:开源 BI 工具离线安装完整流程

DataEase 内网部署指南:开源 BI 工具离线安装完整流程 【免费下载链接】dataease 🔥 人人可用的开源 BI 工具,数据可视化神器。An open-source BI tool alternative to Tableau. 项目地址: https://gitcode.com/GitHub_Trending/da/dataeas…

阅读更多 →
Svelte animate: 指令详解:keyed each 列表重排动画的 FLIP 原理与自定义动画函数 2026/9/5 18:54:04

Svelte animate: 指令详解:keyed each 列表重排动画的 FLIP 原理与自定义动画函数

Svelte animate: 指令详解:keyed each 列表重排动画的 FLIP 原理与自定义动画函数 【免费下载链接】svelte web development for the rest of us 项目地址: https://gitcode.com/GitHub_Trending/sv/svelte 本文基于 Svelte 官方模板语法文档 documentation/…

阅读更多 →
Pixelle-Video 五分钟出片:AI 短视频生成上手指南 2026/9/5 18:54:04

Pixelle-Video 五分钟出片:AI 短视频生成上手指南

Pixelle-Video 五分钟出片:AI 短视频生成上手指南 【免费下载链接】Pixelle-Video 🚀 AI 全自动短视频引擎 | AI Fully Automated Short Video Engine 项目地址: https://gitcode.com/GitHub_Trending/pi/Pixelle-Video Pixelle-Video 是一款 AI …

阅读更多 →
基于SpringBoot的毕业设计选题系统:从需求到部署的实战指南 2026/9/5 18:51:04

基于SpringBoot的毕业设计选题系统:从需求到部署的实战指南

简介:这是一套面向高校计算机专业本科生与Java初学者的毕业设计选题管理实战项目,基于SpringBoot快速构建,解决院系师生在毕设选题阶段存在的信息不对称、流程不透明、状态难追踪等实际问题。资源包共135个文件,涵盖49个核心Java业…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞