每年毕业设计选题的时候,SpringBoot、Vue、MySQL这三个词几乎成了计算机类专业本科毕设的标配组合。考试信息报名系统更是在各种选题清单里反复出现,因为它足够贴近真实业务:有用户登录、有角色权限、有列表查询、有数据写入,更重要的是有并发边界。多数人以为这是最稳妥的“安全牌”,结果交上来的系统只有三层——用户注册、考试列表、报名按钮,答辩老师随便追问一句“同一场考试名额只剩一个时,两个学生同时点报名会发生什么”就答不上来。这篇文章不是从零教你怎么定义一个实体类,而是把我自己从选题、建表、写接口、写页面到联调部署的完整过程梳理出来,重点说清楚哪些设计决定了系统能不能撑住真实使用,哪些坑是我实际踩过、后来在答辩场合反而成了加分项的。内容适合正在做教务管理类毕设的同学,也适合刚工作不久、想补一补前后端分离项目完整链路的新人。
1. 选型背后的三道分水岭:为什么同样用这套组合,有人能答辩有人只能演示
1.1 这套组合考察的不是三个框架,而是它们之间的衔接
很多同学以为毕业设计选SpringBoot+Vue+MySQL是因为“网上资料多、好抄”,这个想法大错特错。我做完这个项目之后最大的体会是:单看每一个技术点都很基础,SpringBoot的Controller怎么写、Vue的组件怎么挂载、MySQL的建表语句怎么敲,这些课程设计阶段就已经练过了。但把它们拼在一起之后,问题就变得完全不基础了。
比如后端的接口数据格式怎么设计,前端才能少写一堆if else?登录之后的用户身份怎么在每次请求里传递,总不能每次都查一遍数据库?前端页面跳转时怎么判断当前用户有没有权限进入管理员页面?这些问题既不属于SpringBoot,也不属于Vue,更不属于MySQL,而是三者之间的衔接层。毕业设计真正考察的就是这个衔接层,而不是某个框架本身用过没有。
这个考试信息报名系统恰好把衔接层的关键点全部覆盖了:用户登录涉及到Token的签发与校验,角色权限涉及到管理员接口和普通学生接口的隔离,考试列表涉及到分页查询的数据格式约定,报名操作涉及到事务和并发控制。把这几个环节打通了,哪怕界面做得朴素一些,答辩老师也能看出你是真的理解了前后端分离项目的运转方式。
1.2 选型前必须想清楚的三个错误心态
我在带毕设小组的过程中见过三类特别典型的心态,基本可以提前预判答辩结果:
| 错误心态 | 表面做法 | 实际后果 |
|---|---|---|
| “能跑就行” | 一个登录页,所有用户登录后看到的功能完全一样 | 答辩老师必问角色权限,三句话就卡壳 |
| “表能存数据就行” | 考试信息和报名信息全部挂在一个字段里,用逗号分隔 | 数据无法统计,接口逻辑越写越乱 |
| “数据库用什么无所谓” | 为了部署方便用SQLite替代MySQL | 避开了数据库设计的核心考察点,老师不认可选题的工程价值 |
这三个心态本质上是同一个问题:把毕业设计当成“交作业”,而不是当成“做项目”。考试信息报名系统虽然技术上不复杂,但它具备一个真实系统该有的全部要素:用户体系、权限体系、核心业务、数据一致性、前端状态同步。如果你在选型阶段就把这些要素当成分内事,后面的实现思路会清晰非常多。
1.3 版本选择上落地的建议
版本选型曾经是个大坑,不是最新版就一定好用。我现在做这个项目的标准搭配是:
- 后端:SpringBoot 2.7.x + JDK 8 或 JDK 17,两者兼容性都很好
- 数据库:MySQL 8.0,注意驱动用
com.mysql.cj.jdbc.Driver - 前端:Vue 3 + Vite 5 + Element Plus + Pinia,不要再用Vue 2
- ORM:MyBatis-Plus,版本用3.5.x,自带分页插件
SpringBoot 3.x 也可以,但Java版本要求17以上,如果你的JDK还没升级,用2.7.x不会在答辩时减分。重要的是把核心逻辑做对,而不是追新版本。
2. 数据库是地基:考试报名系统的表结构与字段设计细节
2.1 用户表:一张表解决三种角色,而不是拆三张表
考试报名系统里至少有学生、教师/管理员两类角色,有些系统还可能加一个超级管理员。很多同学的直觉是“角色不同就应该建不同的表”,这在真实企业项目里往往是错的,在这个毕设项目里更是平白增加复杂度。
我的设计是只建一张sys_user表,用一个role字段区分角色,值分别是1(管理员)、2(教师)、3(学生)。为什么一张表就够了?因为这些用户的核心属性是重合的:账号、密码、姓名、学号/工号、创建时间。拆成三张表意味着登录接口要查询三张表再合并,权限逻辑要写三套分支,完全是给自己挖坑。
CREATE TABLE `sys_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `student_no` varchar(30) DEFAULT NULL COMMENT '学号/工号', `role` tinyint(4) NOT NULL DEFAULT '3' COMMENT '角色:1管理员,2教师,3学生', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`), UNIQUE KEY `uk_student_no` (`student_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有两个字段细节特别容易踩坑。第一是password字段长度不要建varchar(20),我用BCrypt加密密码,加密结果固定60个字符,建小了密码根本存不进去,只能换回明文存储。第二是username必须加唯一索引,这是登录功能的第一道防线,防止数据库层面出现重复账号。role字段用tinyint而不是字符串,看起来可读性差一点,但查询效率高、前后端枚举映射方便,加个注释就清楚了。
2.2 考试表:容量字段直接决定并发报名逻辑怎么写
考试信息表是整个系统的业务核心。除了考试名称、考试时间、考试地点这些常规字段之外,有两个字段决定了报名的并发逻辑能不能成立:total_quota和registered_count。
CREATE TABLE `exam` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `exam_name` varchar(100) NOT NULL COMMENT '考试名称', `exam_date` datetime DEFAULT NULL COMMENT '考试时间', `location` varchar(200) DEFAULT NULL COMMENT '考试地点', `total_quota` int(11) NOT NULL DEFAULT '0' COMMENT '总名额', `registered_count` int(11) NOT NULL DEFAULT '0' COMMENT '已报名人数', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0未发布,1报名中,2已结束', `description` text COMMENT '考试说明', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;很多同学会把“已报名人数”设计成每次查询时count(registration表)来统计,这在数据量小的时候完全没问题,但到了并发场景就成了性能瓶颈。考试人数本身就是一条高频更新的数据,直接冗余在考试表里,报名操作加一、取消报名减一,查询考试列表时连表都不需要,一条SQL就带出来了。status字段建议用数字枚举,0表示未发布、1表示报名中、2表示已结束,列表页根据这个字段决定显示哪些考试。
2.3 报名表:唯一索引是防重复报名的最后底线
报名表是连接用户和考试的桥梁表,结构看起来简单,但设计不好会出大问题。
CREATE TABLE `registration` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `user_id` bigint(20) NOT NULL COMMENT '用户ID', `exam_id` bigint(20) NOT NULL COMMENT '考试ID', `register_time` datetime DEFAULT NULL COMMENT '报名时间', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1正常,0已取消', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_exam` (`user_id`, `exam_id`), KEY `idx_exam_id` (`exam_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;重点在UNIQUE KEY uk_user_exam (user_id, exam_id)这一行。业务代码里无论你怎么做校验,都拦不住两个请求同时到达的情况。数据库的唯一索引是最后一道物理屏障,同一个用户对同一场考试的报名请求,哪怕同时发过来两个,也只会有一条插入成功,另一条直接抛DuplicateKeyException。我见过太多项目把这个约束漏掉,然后在Service里写一堆if判断,看起来逻辑正确,实际上并发下全是漏洞。
2.4 字符集、逻辑删除和时间字段的统一约定
建表的时候统一用utf8mb4字符集,不要问Why,MySQL 8.0默认就是utf8mb4,但如果你直接复制了老项目的建表语句,可能还是utf8,会导致有些中文和表情符号存不进去。时间字段统一用datetime,配合后端的LocalDateTime使用,避免timestamp在2038年之后出问题(虽然毕设演示不会等到那一年,但面试时可以提一嘴)。
3. 后端核心:SpringBoot里报名功能的正确写法
3.1 统一返回体和全局异常处理,先省下一半联调时间
做前后端分离项目,第一个要定的不是接口名,而是接口返回的数据格式。我用的统一返回结构是:
{ "code": 200, "message": "success", "data": {} }前端不管请求成功还是失败,只用看code。如果业务层抛出了异常,全局异常处理器把它转成对应的code和message,前端统一弹提示。这样前端不用在每个接口里各自处理错误分支,后端也不用在Controller里写大量try catch。
@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static <T> Result<T> error(Integer code, String message) { Result<T> r = new Result<>(); r.setCode(code); r.setMessage(message); return r; } }全局异常处理用@RestControllerAdvice,核心是捕获BizException(业务异常)和DuplicateKeyException(唯一索引冲突)。报名接口里最怕的就是唯一索引冲突异常裸奔到前端,前端拿到一堆看不懂的英文报错。在全局异常里专门处理它,返回“请勿重复报名”,体验一下子就正常了。
3.2 JWT登录与ThreadLocal用户上下文:一个过滤器贯穿全流程
考试报名系统的登录方案我选了JWT,不使用Session。原因很简单:前后端分离部署时,后端接口可能被多个前端调用(网页端、管理端),Session天然不友好,而JWT是无状态的,前端每次请求在请求头带上Token,后端验签通过就知道是谁。
登录接口的核心逻辑很简单:查用户、验密码、生成Token。
public String login(String username, String password) { SysUser user = userMapper.selectByUsername(username); if (user == null) { throw new BizException(400, "用户不存在"); } if (!BCrypt.checkpw(password, user.getPassword())) { throw new BizException(400, "密码错误"); } // 生成JWT,有效期2小时 return JwtUtil.generateToken(user.getId(), user.getRole()); }Token生成之后,前端每次请求带上Authorization: Bearer xxx,后端用一个拦截器统一解析Token。
public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token != null && token.startsWith("Bearer ")) { token = token.substring(7); Claims claims = JwtUtil.parseToken(token); if (claims != null) { UserContext.setUserId(claims.get("userId", Long.class)); UserContext.setRole(claims.get("role", Integer.class)); } } return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }UserContext本质是一个ThreadLocal,请求进来放用户信息,请求结束清除。这样做的好处是Controller和Service里到处都能拿当前登录用户ID,不用每次从Token里解析。但注意afterCompletion一定要清除,否则线程池复用时会串号,这是个隐蔽的坑。
3.3 报名接口:事务、乐观控制、唯一索引的三层组合拳
报名是整个系统最核心的接口,也是答辩老师几乎必问的地方。先看错误写法:
@Transactional public Result<Void> register(Long examId) { Exam exam = examMapper.selectById(examId); if (exam.getRegisteredCount() >= exam.getTotalQuota()) { throw new BizException("考试名额已满"); } registrationMapper.insert(new Registration(userId, examId)); exam.setRegisteredCount(exam.getRegisteredCount() + 1); examMapper.updateById(exam); return Result.ok(); }这段代码在单用户、低并发场景下完全没问题,但两个学生同时点报名时必然出错。两个请求同时执行selectById,都看到名额还剩1,都通过检查,都执行insert,都执行update,最终已报名人数变成满额+1。
正确写法是先使用数据库原子操作扣减名额,根据受影响行数判断是否满额,再插入报名记录:
@Transactional public Result<Void> register(Long examId) { Long userId = UserContext.getUserId(); int updated = examMapper.deductQuota(examId); if (updated == 0) { throw new BizException("考试名额已满"); } try { registrationMapper.insert(new Registration(userId, examId)); } catch (DuplicateKeyException e) { throw new BizException("请勿重复报名"); } return Result.ok(); }对应的SQL语句是:
UPDATE exam SET registered_count = registered_count + 1 WHERE id = #{examId} AND registered_count < total_quota这条UPDATE语句是原子的,数据库行锁保证同一时间只有一个请求能把registered_count加一。如果registered_count < total_quota不成立,受影响行数为0,事务回滚,就不会出现超卖。接下来插入报名记录时,唯一索引兜底防止同一用户重复报名。
3.4 考试列表分页与“我是否已报名”的数据联想
考试列表接口的难点不在于分页,而在于每条数据要告诉前端“当前登录用户是否已经报过这场考试”。最直接的做法是查出考试列表后,再查一遍当前用户的所有报名记录,用Set<Long>记录已报名的考试ID,遍历列表打标记。
public PageResult<ExamVO> page(int pageNum, int pageSize, String keyword) { Page<Exam> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Exam> wrapper = new LambdaQueryWrapper<>(); wrapper.like(StringUtils.isNotBlank(keyword), Exam::getExamName, keyword) .orderByDesc(Exam::getExamDate); examMapper.selectPage(page, wrapper); Long userId = UserContext.getUserId(); List<ExamVO> voList = page.getRecords().stream().map(exam -> { ExamVO vo = new ExamVO(); BeanUtils.copyProperties(exam, vo); int count = registrationMapper.countByUserIdAndExamId(userId, exam.getId()); vo.setRegistered(count > 0); vo.setFull(exam.getRegisteredCount() >= exam.getTotalQuota()); return vo; }).collect(Collectors.toList()); return new PageResult<>(voList, page.getTotal()); }这里用countByUserIdAndExamId而不是直接查询列表,是因为数量级小的时候这样写最简单直观,而且不会出现内存中列表和数据库脱节的问题。
4. 前端落地:Vue3项目管理、路由守卫和报名状态联动
4.1 Vite + Vue3的项目结构与依赖安装
前端我用了Vite创建Vue3项目,Node版本建议16以上,否则Vite 5跑不起来。创建命令是:
npm create vite@latest exam-frontend -- --template vue进入项目后安装依赖:
npm install npm install vue-router@4 pinia axios element-plus目录结构按功能模块划分,而不是按文件类型堆在一起:
src/ ├── api/ # 接口请求封装 │ ├── auth.js # 登录相关接口 │ ├── exam.js # 考试相关接口 │ └── registration.js # 报名相关接口 ├── views/ # 页面组件 │ ├── Login.vue │ ├── ExamList.vue │ ├── AdminExam.vue │ └── Profile.vue ├── router/ # 路由配置 ├── stores/ # Pinia状态管理 ├── utils/ # axios封装、工具函数 └── App.vue这个结构的好处是每个页面涉及的API、状态、组件都在离它最近的地方,不会出现api目录里几百个文件互相找不到的情况。毕设项目代码量不大,但结构清爽在答辩时很加分。
4.2 Axios封装:Token注入、统一错误提示一个都不能少
前端请求后端接口,最基础的要求是每个请求自动带上Token,返回401时自动跳转登录页。我在utils/request.js里封装了axios实例:
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') router.push('/login') } ElMessage.error('网络请求失败') return Promise.reject(error) } )一个关键约定是开发环境下baseURL写/api,不要写http://localhost:8080。这样配合Vite代理才能绕开跨域问题,具体原理我在后面的排错环节详细讲。
4.3 考试列表页与报名按钮的状态联动
考试列表页面最核心的逻辑是:按钮的显示状态要跟后端数据保持一致,而且不能每次点完都重新刷新整个页面。
我的做法是维护三个响应式变量:examList(考试列表)、registeredExamIds(已报名的考试ID集合)、loading(加载状态)。
const examList = ref([]) const registeredExamIds = ref(new Set()) const loading = ref(false) const loadExamList = async () => { loading.value = true const res = await examApi.getPage({ pageNum: 1, pageSize: 10 }) examList.value = res.data.list const regs = await registrationApi.getMyRegisteredIds() registeredExamIds.value = new Set(regs.data) loading.value = false } const handleRegister = async (examId) => { await registrationApi.register(examId) registeredExamIds.value.add(examId) ElMessage.success('报名成功') }模板层怎么做按钮状态:
<el-button v-if="!registeredExamIds.has(item.id) && !item.full" type="primary" @click="handleRegister(item.id)" >立即报名</el-button> <el-button v-else-if="registeredExamIds.has(item.id)" disabled>已报名</el-button> <el-button v-else disabled>名额已满</el-button>这里没有在报名成功后重新请求列表,而是直接更新本地的Set。这样页面上所有引用registeredExamIds的地方都会自动更新。如果你在报名成功后loadExamList()重新拉全量数据,在网络稍慢的情况下,报名按钮会闪烁一下,体验很差。
4.4 路由守卫与管理员权限控制
前端路由守卫的作用是拦截未登录用户和没有权限的用户。Vue Router 4里用beforeEach实现:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') return } const role = Number(localStorage.getItem('role')) if (to.meta.roles && !to.meta.roles.includes(role)) { next('/403') return } next() })路由配置里对应的写法是:
{ path: '/admin/exam', component: AdminExam, meta: { requiresAuth: true, roles: [1, 2] } }有一个细节:localStorage里存的角色信息在用户手动修改后,前端权限就失效了。所以管理员相关的接口,后端还必须再校验一次JWT里的角色。前端路由守卫只是用户体验层面的控制,真正的安全边界永远在后端。
5. 联调排错实录:跨域、时区和重复提交三个典型事故
5.1 跨域问题:Vite代理配置对了,为什么还是报跨域?
联调阶段遇到的第一个问题就是跨域。我的开发环境前端跑在http://localhost:5173,后端跑在http://localhost:8080。浏览器看到请求源不同,就会拦截。很多同学遇到跨域第一反应是去后端配CORS过滤器,这没问题,但在这个项目里更推荐用Vite代理。
Vite代理的思路是:前端请求不是直接发给后端,而是发给Vite的开发服务器,由开发服务器转发给后端。这样浏览器看到的始终是同源请求,根本没有跨域。
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这里最常见的坑是:配置了代理,但前端请求里写的是完整地址http://localhost:8080/api/login,代理完全没生效,因为代理只匹配以/api开头的相对地址。正确写法是request.post('/login'),axios实例里的baseURL会自动拼成/api/login。
排查这个问题的时候,我记得当时开了浏览器Network面板看了半天请求状态,又去后端看日志,最后才发现是前端自己的URL写错了,代理根本没接管。记住一个原则:开发环境统一走代理,生产环境统一走Nginx反向代理,后端CORS过滤器只在特殊场景才用。
5.2 数据库时间慢了8小时:一个配置两个位置都要改
第二个典型的坑是时间问题。前端页面显示create_time总是比实际时间少8小时,查了数据库里的数据发现是对的,那问题一定出在数据传输链路上。
一步步排查下来,发现两个位置都要配置。第一,MySQL连接URL必须指定时区:
spring: datasource: url: jdbc:mysql://localhost:3306/exam?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/ShanghaiMySQL 8.0驱动默认时区是UTC,如果不指定serverTimezone,查询出来直接按UTC时间返回,换算到东八区就差了8小时。第二,Jackson序列化LocalDateTime时也要指定时区:
spring: jackson: time-zone: Asia/Shanghai date-format: yyyy-MM-dd HH:mm:ss这个坑的隐蔽性在于:你单独看数据库、单独看后端日志,时间都是对的,但返回给前端就错了。其实是因为后端在把LocalDateTime对象转成JSON字符串时,用了JVM默认时区,而有的云服务器默认时区是UTC。统一把这两处配置配好,不要去前端写什么补丁函数强行加8小时,那种写法在业务复杂的项目里迟早出乱子。
5.3 并发重复报名:为什么事务没挡住,唯一索引却挡住了
最后一个也是最重要的排错经历:用两个浏览器同时登录不同账号,在同一场考试还剩最后一个名额时同时点报名,发现数据库里registered_count变成了满额加一。这明显是超卖了。
我当时的排查链路是这样的:
第一步,先看后端日志,确认两个请求确实同时进入了Service方法。第二步,看执行的SQL,发现两个请求都执行了UPDATE exam SET registered_count = registered_count + 1 WHERE id = ?,没有带registered_count < total_quota条件。原因是早期版本我写的是selectById查一次名额,再updateById覆盖更新,两个请求都查到了满额前的数据,然后各自加一写回,最后一条写回把前一条覆盖了。
第三步,就是改成前面提到的原子UPDATE方式。这条SQL天然带资格校验,数据库行锁保证同一时间只有一个请求能更新成功。第四步,加上唯一索引防止同一用户在同一毫秒内发两次请求产生的重复记录。
这个问题的根因不在事务,而在于“先查后写”的竞态。事务保证的是要么全成功要么全失败,但没办法阻止两个并发事务读到相同值。数据库的原子更新和唯一索引才是真正解决并发的底层机制。这个排查经历非常典型,也成了我答辩时的核心亮点。
6. 答辩前的代码自查与演示节奏安排
6.1 答辩老师最爱追问的三个问题怎么答
答辩环节老师一般不会让你当场写代码,而是围绕你项目里的关键设计追问。考试报名系统最常被问到的问题有这三个:
第一,“一个考生重复提交报名,系统怎么防?”回答思路:前后端双重校验,前端通过已报名状态禁用按钮,后端用数据库唯一索引兜底,重复插入时捕获异常并返回友好提示。
第二,“名额只剩一个时,两个考生同时报名会发生什么?”回答思路:这是并发下的超卖问题。如果先查再写会超卖,所以直接用带条件的UPDATE原子扣减名额,受影响行数为0就说明满额,事务回滚。
第三,“管理员新增考试后,学生端什么时候能看到?”回答思路:考试表有status字段,管理员录入时默认未发布,点击发布后状态变成报名中,学生端的列表页只查status=1的考试,通过状态字段控制可见性,而不是通过延迟或刷新机制。
这三个问题答好了,哪怕其他细节答辩时有点小瑕疵,分数也不会差。
6.2 代码提交前的六项自查清单
每次让学弟学妹提交毕设前,我建议按这个清单过一遍:
| 检查项 | 正确做法 | 常见错误 |
|---|---|---|
| 密码存储 | BCrypt加密 | 明文存储或MD5 |
| 参数校验 | @Validated+ 注解 | Controller里自己写一串if |
| 异常处理 | 全局异常处理器 | 每个Controller单独 try catch |
| 数据库并发 | 原子UPDATE + 唯一索引 | select再update |
| 前端请求 | 统一Axios实例 | 每个页面新建axios |
| SQL注入 | MyBatis-Plus预编译 | 字符串拼接SQL |
第六项特别容易忽略。用MyBatis-Plus的LambdaQueryWrapper默认预编译,基本没有SQL注入风险,但如果自己在XML里写了${}拼接,答辩老师一旦问到你搜索功能怎么实现的,就可能暴露风险。
6.3 演示数据准备与操作节奏
演示环节翻车的人太多了,基本都是因为没提前准备数据。我当时准备了两个账号:一个管理员账号、一个学生账号,加上一组特意构造的考试数据——其中一场名额设为1,专门用来演示并发下的满额提示。
演示顺序建议:先用管理员登录,发布一场新考试,设置名额为3;换学生账号登录,进入考试列表页,报名,观察按钮变成“已报名”;再换另一个学生账号,把名额报满,第三个学生点击时看到满额提示;最后切回管理员,在后台看到报名人数和报名明细。这个流程不到五分钟,但把登录、权限、列表、报名、状态变化、数据统计全部串起来了。
写在最后的一点个人体会
做完这个考试信息报名系统,我最大的感觉是:毕业设计的技术难度从来不是瓶颈,瓶颈在于有没有把每个环节之间的衔接想明白。数据库设计没想好,接口写起来就难受;接口格式没约定好,前端就到处打补丁;并发问题没想清楚,演示阶段就可能当场翻车。这恰恰是这一类选题最值得投入时间的地方。如果时间充裕,你还可以在这个基础上扩展成绩查询、准考证下载、数据统计导出等功能,每个扩展点都能成为答辩时的加分项。希望这篇实战记录能帮你少走一些我走过的弯路。