ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue+MySQL考试报名系统实战:从设计到部署全流程

SpringBoot+Vue+MySQL考试报名系统实战:从设计到部署全流程 每年毕业设计季总有人来问我做个管理系统选什么技术栈不容易翻车我的回答一直是 Java 后端配 Vue 前端再加一个 MySQL 数据库这个组合甭管是考试报名系统、教室预约系统还是资料管理系统都能稳稳撑住。今天这篇就把这套考试信息报名系统从设计到落地的完整过程拆给你看包括数据库怎么建、后端接口怎么写、前端页面怎么接以及那些文档里找不到的坑。适合马上要开题、或者已经动工但卡在某一步的同学参考。先说下项目背景。很多同学毕设选题会选“XX管理系统”但往往卡在同一个地方需求没想清楚就开始写代码写到一半发现表结构不对、接口设计缺东少西再回头改就是灾难。所以这篇我会按我平时带项目时的顺序来走先讲整体设计再讲后端再讲前端最后讲数据库和联调部署。内容偏实战代码片段可以直接抄但更重要的是理解每一步为什么这么做。1. 考试报名系统的整体设计选型、角色与流程1.1 为什么偏偏是 SpringBoot Vue MySQL先说选型逻辑。SpringBoot 是目前 Java 后端的事实标准它的自动配置和 starter 机制省掉了大量 XML 配置一个人开发效率很高。Vue 则胜在上手门槛低组件化开发对单人项目友好尤其 Element Plus 这类组件库把表格、表单、弹窗都封装好了写后台管理界面跟拼积木似的。MySQL 就更不用说了免费、稳定、资料最多大学机房和服务器基本都装过答辩环境不会出幺蛾子。关键在于这个组合不是最新的但它是就业市场的绝对主流。我见过有人毕设选了 Rust 后端论文写得非常费劲答辩老师还不一定吃得下。SpringBoot Vue MySQL 的好处是随便一搜就是几千篇参考博客从环境配置到 bug 排查都有现成答案遇到问题你能自己扛住。对毕设来说稳比炫技更重要。1.2 角色与功能模块拆解考试报名系统一听名字就知道核心业务是“报名”。但完整做下来至少要覆盖三类角色的诉求学生前台用户注册登录、浏览考试列表、查看考试详情、报名考试、取消报名、查看自己的报名记录和考试成绩。管理员后台用户管理学生账号、发布考试信息、设置报名起止时间和人数上限、查看报名数据、录入或导入成绩。教师可选角色操作相对简单主要做成绩录入也可以用管理员的子模块替代。建议第一次做的时候把权限分两级学生和管理员就够了。教师角色如果硬加会牵扯到账号分配和权限配置的额外工作量对毕设来说性价比不高。把学生端和管理员端做扎实论文里把扩展方案写清楚就行。这里我把功能模块用表格列一下你直接照这个去设计页面菜单和接口就行角色功能模块核心操作学生账号管理注册、登录、个人信息修改学生考试浏览分页查询、按名称关键词搜索、查看详情学生报名管理报名、取消报名、查看我的报名列表学生成绩查询查看成绩发布结果管理员用户管理学生账号的增删改查、状态禁用管理员考试管理发布、编辑、上下架考试信息管理员报名统计统计各考试报名人数、导出列表管理员成绩管理录入成绩、发布/隐藏成绩1.3 报名流程先画清楚再动手我在带人的时候发现一个现象很多人接口写了一半才意识到报名的业务规则没有梳理完整。考试报名不是简单地往表里插一条记录它必须经历一串校验用户是否已登录。考试信息是否存在且处于“报名中”状态。当前时间是否在报名起止时间范围内。用户是否已经报过名防重复报名。当前报名人数是否已满防超卖。这五步缺一不可。尤其是第 4、5 步是答辩时评审老师最喜欢追问的点“同一时间 50 个人报名你怎么保证不会报超”这个问题我会在后面的并发章节专门展开。现在你只需要记住设计阶段把流程图画清楚后面写代码是水到渠成的事。另外还要设计好“状态机”的概念。一场考试的 status 字段不能只用一个布尔值存“上架/下架”至少要区分“未开始报名”“报名中”“已满员”“已结束”四种状态。状态变化要跟时间联动前端展示按钮时才不会出现逻辑混乱。2. 后端从零搭建SpringBoot 核心模块实战2.1 环境准备与项目初始化我先说一下版本组合这是经验之谈。SpringBoot 2.7.14 JDK 8 是最推荐的毕设组合因为网上的教程、博客、代码示例绝大多数都是这个体系你遇到任何问题都能搜到答案。SpringBoot 3.x 虽然新但它强制要求 JDK 17很多老版本的依赖会踩坑排查成本对毕设来说不值得。创建项目用 IDEA 的 Spring Initializr 就行。需要注意如果你的 IDEA 创建项目时一直超时那是网络问题可以换成阿里云的 Spring Initializr 地址https://start.aliyun.com。勾选的依赖只需要三样Spring WebMySQL DriverLombokMyBatis-Plus 需要手动加到pom.xml因为默认的依赖列表里没有。我建议毕设直接用 MyBatis-Plus它在 MyBatis 基础上封装了通用 CRUD单表操作不用写 SQL能把开发时间压缩至少三分之一。核心依赖如下dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.2/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdcn.hutool/groupId artifactIdhutool-all/artifactId version5.8.22/version /dependencyHutool 是个人开发的神器里面有空值判断、日期处理、加密工具一大堆毕设用它能省太多事。2.2 application.yml 配置连接 MySQL 的经典坑配置文件的写法本身不复杂但 MySQL 8.0 版本有几个坑必须提前避开。先给一份可以直接用的配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/exam_system?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意看 driver-class-name 那行MySQL 8.0 的驱动类是com.mysql.cj.jdbc.Driver不是以前那个com.mysql.jdbc.Driver。少了中间的 cj 会直接报驱动加载失败。serverTimezoneAsia/Shanghai不加会提示 CST 时区错误allowPublicKeyRetrievaltrue是 MySQL 8 使用 caching_sha2_password 认证方式时经常会需要的否则连不上。还有一个加分项面试或答辩喜欢追问安全性的话可以在配置文件里把数据库密码加密。用 Jasypt 插件引入依赖后在配置里写ENC(加密串)启动时它会自动解密。毕设能做到这一步评委印象分会明显不同。2.3 用户认证JWT 方案怎么落地登录注册是几乎所有系统的第一道门槛。我推荐用 JWTJSON Web Token方案而不是传统的 Session。理由很简单前后端分离项目里Session 要处理跨域携带 Cookie 的问题麻烦JWT 是无状态的后端不用存登录状态前端把 token 放在请求头里就行扩展性也更好。具体实现分四步用户注册时密码不要明文存数据库用 BCrypt 加密。Spring Security 里自带 BCryptPasswordEncoder但为了不引入整个安全框架直接用 Hutool 的 BCrypt 工具类也行。登录成功后把 userId、用户名、角色放进 JWT 的载荷部分生成 token 返回前端。写一个拦截器AuthInterceptor实现HandlerInterceptor接口在preHandle里从请求头获取 token解析失败就返回 401成功就把 userId 放入 request 的 Attribute 供后续使用。在 WebMvcConfigurer 里注册拦截器放行登录、注册等公开接口其他接口全部校验。贴一个 JwtUtil 核心代码你可以直接改成自己的public class JwtUtil { private static final String SECRET your-secret-key-please-change-at-least-32-bytes; public static String generateToken(Long userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 24)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }注意 jjwt 0.9.1 版本里日期过期校验是自动的token 过期会直接抛异常拦截器里捕获后统一返回“登录已过期请重新登录”。有些博客推荐 jjwt 0.11.5 以后的新 API但 0.9.1 对你写论文时的代码解释更友好别在版本问题上花太多时间。2.4 核心报名接口防重复、防超卖的一次实践报名接口是整个系统的核心也是毕设含金量最高的地方。前面说过报名不能只是插一条记录要做五步校验。这里我重点讲防重复和防超卖。防重复报名最简单的办法给报名表加唯一索引(user_id, exam_id)这是数据库层面的兜底。同时在业务代码里先查一次用户是否已经报名双重保险。唯一索引还有个好处——并发情况下即使业务代码查漏了数据库也会直接拒绝重复插入。防超卖是这个系统最容易被追问的技术点。场景是这样的一场考试限制 100 人现在已报名 100 人但第 101 个人发起了报名请求。如果你先查人数、再判断是否小于上限、最后更新人数并发情况下就会出现两个请求同时查出人数为 99然后同时通过校验最终报成 101 人。这就是超卖。解决方案是用一条有条件的 UPDATE 语句做原子操作UPDATE exam_info SET registered registered 1 WHERE id #{examId} AND registered max_people AND status 1这条 SQL 的意思是只有当当前报名人数小于上限时才加 1。数据库的行锁会保证同一时间只有一个请求能成功执行这次更新。MyBatis 的 update 方法返回受影响行数如果返回 0说明没更新成功要么人数已满要么考试状态不对这时直接抛出业务异常提示“报名人数已满”。对应 Mapper 写法Mapper public interface ExamInfoMapper extends BaseMapperExamInfo { Update(UPDATE exam_info SET registered registered 1 WHERE id #{examId} AND registered max_people AND status 1) int increaseRegistered(Long examId); }然后是完整的报名事务方法Transactional(rollbackFor Exception.class) public void register(Long userId, Long examId) { ExamInfo exam examInfoMapper.selectById(examId); if (exam null) { throw new BusinessException(考试不存在); } // 校验报名时间 Date now new Date(); if (now.before(exam.getRegisterStart()) || now.after(exam.getRegisterEnd())) { throw new BusinessException(不在报名时间范围内); } // 防重复报名 Long count registrationMapper.selectCount(new LambdaQueryWrapperExamRegistration() .eq(ExamRegistration::getUserId, userId) .eq(ExamRegistration::getExamId, examId)); if (count 0) { throw new BusinessException(您已报名该考试); } // 原子更新报名人数 int updated examInfoMapper.increaseRegistered(examId); if (updated 0) { throw new BusinessException(报名人数已满); } // 写入报名记录 ExamRegistration registration new ExamRegistration(); registration.setUserId(userId); registration.setExamId(examId); registration.setStatus(1); registration.setRegisterTime(now); registrationMapper.insert(registration); }这段代码是整套系统的灵魂建议你把他背下来。答辩的时候能把“先查再更新会导致超卖用条件更新做原子操作”这句话讲清楚技术分基本就稳了。2.5 分页查询与条件搜索考试列表必须支持分页不然前端一次加载几百条数据体验很差。MyBatis-Plus 的分页插件配置一下就行Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }查询接口用 LambdaQueryWrapper支持按考试名称模糊搜索、按状态筛选、按时间排序public PageExamInfo pageExams(int page, int size, String keyword, Integer status) { PageExamInfo pageInfo new Page(page, size); LambdaQueryWrapperExamInfo wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), ExamInfo::getExamName, keyword) .eq(status ! null, ExamInfo::getStatus, status) .orderByDesc(ExamInfo::getCreateTime); return examInfoMapper.selectPage(pageInfo, wrapper); }有过开发经验的同学会发现这套逻辑是通用的换成任何管理系统的列表页都一样。事实上毕设项目就是这么一个套路登录鉴权 CRUD 一对多关联查询把基础打扎实比什么都强。3. 前端页面实战Vue3 报名与后台管理界面3.1 用 Vite 搭建 Vue3 项目前端我推荐 Vue 3 Vite Element Plus Pinia Vue Router 这套组合这是当前社区最主流的配置。老项目的 Vue CLI 已经处于维护模式别再用vue create了。创建项目命令npm create vitelatest exam-front -- --template vue cd exam-front npm install npm install axios vue-router pinia element-plus这里有一个环境要求Node.js 必须 16 以上如果版本太低Vite 会直接报错。安装依赖慢的话记得先设置淘宝镜像源npm config set registry https://registry.npmmirror.comElement Plus 如果按需引入比较麻烦我建议毕设项目直接全量引入代码简单打包体积大一点也无所谓。在main.js里import { createApp } from vue import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router import pinia from ./stores const app createApp(App) app.use(ElementPlus) app.use(router) app.use(pinia) app.mount(#app)组件库选 Element Plus 还是 Ant Design Vue纯个人偏好。考试报名系统这类后台管理界面用的最多的组件就是表格、表单、弹窗、日期选择器这两套组件库都做得很好。考虑到 Vue 和 Element 都是尤雨溪团队生态我习惯用 Element Plus。3.2 开发环境的跨域代理前后端分离开发时第一个拦路虎就是跨域。前端跑在 5173 端口后端跑在 8080 端口浏览器会拦截跨域请求。解决办法有两个后端加 CORS 配置或者前端开代理。我推荐用 Vite 的代理开发时最省事配置在vite.config.jsexport default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/auth/login时Vite 会把请求转发到http://localhost:8080/api/auth/login浏览器里看到的始终是同源请求完全没有跨域问题。部署时用 Nginx 也能配置同样效果。不推荐谷歌浏览器关掉安全策略来绕过跨域演示的时候换个环境就露馅了。3.3 Axios 封装与 Token 注入Axios 必须封装一层不然每个页面都写一遍axios.get再处理 401 错误会累死。我一般会在src/utils/request.js里做统一处理import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response?.status 401) { localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push(/login) ElMessage.error(登录已过期请重新登录) } else { ElMessage.error(error.message || 网络异常) } return Promise.reject(error) } ) export default request后端返回格式统一用{ code: 200, message: success, data: ... }这样前端拦截器只用判断一层结构。token 过期统一跳转登录页这个细节做得好演示时不会出现页面白屏的尴尬。3.4 路由守卫与考试列表页路由配置按模块拆开考试列表、我的报名、管理后台分开写const routes [ { path: /login, component: Login }, { path: /, component: Layout, children: [ { path: exam, component: ExamList, meta: { auth: true } }, { path: my-registration, component: MyRegistration, meta: { auth: true } }, { path: admin, component: AdminDashboard, meta: { auth: true, role: ADMIN } } ]} ]路由守卫的逻辑是访问需要登录的页面先检查本地有没有 token没有就踢回登录页访问管理页再检查用户角色是不是管理员router.beforeEach((to, from, next) { const token localStorage.getItem(token) const user JSON.parse(localStorage.getItem(userInfo) || {}) if (to.meta.auth !token) { next(/login) } else if (to.meta.role user.role ! to.meta.role) { ElMessage.error(没有权限访问) next(/exam) } else { next() } })这里涉及一个细节路由传参。列表页跳详情页时可以用router.push({ path: /exam/detail, query: { id: row.id } })目标页用route.query.id接收。query 参数刷新后还在比用 params 更稳。考试列表页是学生端最核心的页面。表格里要显示考试名称、考试时间、报名起止时间、报名人数/上限、状态和操作按钮。按钮状态是动态的要根据几个字段联合判断已登录、在报名时间内、未报名、人数未满 → 显示“立即报名”已经报名 → 显示“已报名”禁用按钮不在报名时间 → 显示“未开始”或“已截止”禁用人数已满 → 显示“已满员”禁用每个按钮的点击事件都要二次确认用 Element Plus 的ElMessageBox.confirm防止用户误触const handleRegister (row) { ElMessageBox.confirm(确定报名${row.examName}吗, 提示, { type: warning }) .then(async () { await request.post(/registration, { examId: row.id }) ElMessage.success(报名成功) loadList() }) .catch(() {}) }报名成功后刷新列表按钮状态自动变成“已报名”。整个过程不需要手动刷新页面交互非常顺滑。3.5 管理端界面表格 弹窗的组合拳管理端的考试信息管理页面核心就是一张表格加一个弹窗表单。表格展示所有考试操作列提供“编辑”“上架/下架”“查看报名”按钮。新增和编辑共用同一个弹窗区别是表单是否有初始值。弹窗表单里有一个字段容易忽略报名起止时间。Element Plus 的日期时间选择器用el-date-picker设置typedatetimerange可以一次选两个时间但是绑定值是一个数组提交时要把数组拆成registerStart和registerEnd两个字段注意格式化为yyyy-MM-dd HH:mm:ssconst form reactive({ examName: , maxPeople: 100, timeRange: [] }) const handleSubmit () { const [registerStart, registerEnd] form.timeRange const params { examName: form.examName, maxPeople: form.maxPeople, registerStart: formatDate(registerStart), registerEnd: formatDate(registerEnd) } // 提交接口 }前端日期格式化建议用 dayjsVite 项目里默认没有需要npm install dayjs。后端 Jackson 已经配好了日期格式但前端传字符串时还是要注意统一成yyyy-MM-dd HH:mm:ss格式否则后端 LocalDateTime 解析会报 400 错误。4. 数据库设计表结构、索引与并发防超卖4.1 核心表结构四张表撑起一个系统数据库是整套系统的地基表设计得好代码写起来就顺手。我建议用四张核心表用户表 sys_userCREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) COMMENT 真实姓名, student_no VARCHAR(30) COMMENT 学号, role VARCHAR(20) DEFAULT STUDENT COMMENT STUDENT/ADMIN, phone VARCHAR(20), email VARCHAR(100), deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;考试信息表 exam_infoCREATE TABLE exam_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_name VARCHAR(200) NOT NULL COMMENT 考试名称, description TEXT COMMENT 考试介绍, exam_time DATETIME COMMENT 考试时间, register_start DATETIME COMMENT 报名开始时间, register_end DATETIME COMMENT 报名结束时间, max_people INT DEFAULT 0 COMMENT 人数上限, registered INT DEFAULT 0 COMMENT 已报名人数, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, deleted TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;报名记录表 exam_registrationCREATE TABLE exam_registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, exam_id BIGINT NOT NULL, status TINYINT DEFAULT 1 COMMENT 1已报名 0已取消, register_time DATETIME DEFAULT CURRENT_TIMESTAMP, cancel_time DATETIME, UNIQUE KEY uk_user_exam (user_id, exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;成绩表 exam_scoreCREATE TABLE exam_score ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, exam_id BIGINT NOT NULL, score DECIMAL(5,2), rank_no INT, publish_status TINYINT DEFAULT 0 COMMENT 0未发布 1已发布, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_exam_score (user_id, exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个细节值得注意。一是所有表都加了deleted字段做逻辑删除配合 MyBatis-Plus 的全局配置如果删除一条考试记录底层执行的是 UPDATE 而不是 DELETE这样数据不会物理消失方便论文里写“基于逻辑删除的数据安全策略”。二是主键用BIGINT虽然毕设数据量不大但这是良好的编码习惯也是面试官和答辩老师会关注的点。4.2 自动建表与初始化数据很多同学在运行项目时经常卡在“手动建表”这一步因为每个人本地的数据库可能不一样。用 Spring Boot 的 SQL 初始化机制可以解决这个问题。在src/main/resources下放一个schema.sql和一个data.sql然后在配置里开启spring: sql: init: mode: always schema-locations: classpath:schema.sql >EXPLAIN SELECT * FROM exam_info WHERE status 1 AND register_start NOW() AND register_end NOW();能看到查询走了哪个索引、扫描了多少行。你能说出“用 EXPLAIN 分析慢查询”这在毕设答辩里就是一个真实的亮点说明你不只是会 CRUD还懂一点调优。存储过程在毕设里是一个可选加分项。一个典型场景是统计每个考试的报名率用一条 SQL 就能实现不需要写存储过程但如果要做一个复杂的报表比如按学院统计报名人数、平均成绩、通过率用存储过程封装起来会显得更专业。我给一个示例CREATE PROCEDURE stat_exam_report() BEGIN SELECT e.exam_name, COUNT(r.id) AS register_count, e.max_people, ROUND(COUNT(r.id) / e.max_people * 100, 2) AS register_rate FROM exam_info e LEFT JOIN exam_registration r ON e.id r.exam_id AND r.status 1 WHERE e.deleted 0 GROUP BY e.id; END;当然不要为了用存储过程而用。如果业务逻辑本身很简单强行塞进存储过程会让代码可读性变差答辩时也不好解释。把它作为扩充点写进论文的“系统展望”里就够了。4.4 并发控制为什么不用悲观锁前面在接口实现里提到了条件更新防超卖这里再深入讲一下。有些同学会想到用SELECT ... FOR UPDATE对查询加悲观锁也就是在读取记录时就锁定这一行其他事务必须等锁释放才能操作。这在原理上没问题但会带来两个问题一是事务时间稍长就会造成锁等待并发能力下降二是FOR UPDATE要配合事务使用如果代码里事务范围没有控制好锁会一直不释放系统直接被拖垮。我用的条件更新属于乐观锁思想的一种简化实现不加锁只是通过 UPDATE 语句自身判断条件是否满足。它的优势是性能好、代码简单适合报名系统这种“并发集中在人数扣减”的业务场景。如果未来业务扩展成“同一资源多条件校验需要保证原子性”再考虑引入真正的乐观锁版本号机制。这些内容在论文里作为扩展点提一下很加分。5. 联调部署与答辩准备5.1 接口文档先约定联调少踩坑前后端并行开发时最怕的就是后端改个字段名前端再改一遍。我的经验是写代码前先花一个小时把接口文档定出来就算不用 Swagger也要在文档里写清楚每个接口的路径、请求方法、请求参数、返回结构。这里列出核心接口清单接口路径方法功能请求参数返回结果/api/auth/registerPOST注册username, password, realNamecode, message/api/auth/loginPOST登录username, passwordtoken, userInfo/api/exam/listGET考试分页列表page, size, keyword, statusrecords, total/api/exam/detailGET考试详情idexamInfo/api/registrationPOST报名examIdcode, message/api/registration/myGET我的报名列表page, sizerecords, total/api/registration/cancelDELETE取消报名idcode, message/api/admin/examPOST新增考试examInfocode, message/api/admin/examPUT修改考试examInfocode, message/api/admin/examDELETE删除考试idcode, message/api/admin/scorePOST录入成绩userId, examId, scorecode, message返回结构统一是{ code, message, data }。如果项目时间充裕接入 Swagger 或 Knife4j 自动生成接口文档答辩时打开页面展示清晰的接口列表也是一个加分项。5.2 打包与本地演示毕设演示通常是在一台电脑上完成不需要复杂的分布式部署。后端在 IDEA 里可以直接运行但为了稳妥建议打包成 jarmvn clean package -DskipTests java -jar target/exam-system-0.0.1.jar前端打包npm run build它会生成dist目录。如果想让演示过程更酷一点可以把前端打包产物复制到后端的src/main/resources/static目录下再重新打包后端这样启动后端一个进程就能同时提供前端页面和接口服务访问http://localhost:8080就能看到系统界面。临时演示非常方便不用同时开两个窗口也不怕端口冲突。正式一点的做法是用 Nginx 提供前端静态文件反向代理后端接口但毕业设计演示环境没必要搞这么复杂。你把单端口方案跑通论文里把 Nginx 部署作为生产环境扩展略加说明就足够了。5.3 答辩前必须能讲清楚的三个技术问题根据我带毕设的经验答辩老师对管理系统项目的提问高度集中在几个点上你最好提前组织好语言第一JWT 的完整校验流程是什么从用户登录生成 token到前端存储再到请求拦截器解析 token最后到 token 过期处理整条链路要能流畅讲出来。第二如何防止同一用户重复报名同一场考试答案是数据库唯一索引 业务层查询校验的双保险。第三如何防止报名人数超出上限答案是条件 UPDATE 原子操作用受影响行数判断是否成功配合事务保证数据一致性。把这三个问题练熟了答辩环节基本稳过。别忘了准备一句“我用 EXPLAIN 分析过查询语句确认走对了索引。”这句话一定能拉住评委的注意力。6. 高频问题排查速查与避坑指南毕设项目从搭建到跑通最容易卡壳的就是环境问题和配置问题。我把这些年见过的高频问题整理成一张速查表你可以直接收藏遇到问题翻出来对照。问题现象根本原因解决方案连接 MySQL 报时区异常缺少 serverTimezone在 JDBC URL 加 serverTimezoneAsia/Shanghai驱动类找不到MySQL 8 驱动类名变了用 com.mysql.cj.jdbc.Driver创建 SpringBoot 项目超时IDEA 默认源连不上换成阿里云 start.aliyun.com前端 npm install 失败网络原因配置淘宝镜像 registry.npmmirror.com前端请求接口 404Vite 代理路径或 Controller 映射不一致检查 /api 前缀是否对齐浏览器提示 CORS 跨域前后端不同源开发环境用 Vite proxy部署配 NginxVue 路由 history 模式刷新 404服务端没配 fallback用 hash 模式或 Nginx try_files数据库插入中文乱码建库没指定字符集建库用 utf8mb4连接加 characterEncodingutf8Element Plus 图标不显示图标组件未注册全量引入 Element Plus 或单独注册图标JWT 登录后每次请求都 401前端没把 token 放入请求头在 Axios 拦截器里统一加 AuthorizationIDEA 运行项目端口被占用8080 被其他进程占用换端口或杀掉占用进程 lsof -i:8080MySQL Workbench 连不上本地数据库root 用户认证方式问题确认密码正确必要时用 ALTER USER 改认证方式这里多说一句关于 SpringBoot 版本的问题。如果你下载的依赖版本太高比如 SpringBoot 3.x那么javax.servlet包名变成了jakarta.servlet很多旧代码的 import 全部要改MyBatis-Plus 也要用专门的适配版本。对毕设来说这不是必要的学习成本强烈建议固定在 2.7.x。再补充一个开发效率小技巧后端项目里开启 MyBatis-Plus 控制台 SQL 日志打印配置在application.yml的log-impl那行。调试接口时可以直接看到 SQL 语句定位问题速度翻倍。前端调试就用浏览器自带的开发者工具安装 Vue Devtools 插件可以看到组件的响应式数据但别用检查元素的方式去判断请求看 Network 面板里的请求和响应才是最快的。还有一个小东西SpringBoot 启动时那个 Banner 可以用 banner 生成器在线制作把班级学号或项目名写成 ASCII 艺术字教学检查和答辩开场都能增加一点仪式感这个不影响功能纯属乐趣。最后再聊聊我的实际体会做完这个考试报名系统我最大的感受是毕设项目不需要堆砌多少高级技术把 CRUD 做扎实、把并发边界处理好、把异常情况考虑周全已经能打败一大半同龄人了。我在做报名接口时最初也只是简单地“查一下人数再插入”直到自己写了个并发测试脚本模拟 200 个同时请求亲眼看到了超卖现象才真正理解为什么网上都在强调原子操作。所以建议你完成项目后也写个简单脚本去压一下报名接口实践出来的理解跟背八股完全是两回事。另外别把这个项目当作终点。这套 SpringBoot Vue MySQL 的骨架非常通用以后想往上面加 Redis 缓存、加 RabbitMQ 异步通知、加 Spring Security 权限框架都是顺理成章的事。甚至把这个报名逻辑改一改就变成了秒杀系统、预约系统、选课系统。基础打好了扩展只是时间问题。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进