ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot+Vue实战:大学生竞赛管理系统从设计到部署

Spring Boot+Vue实战:大学生竞赛管理系统从设计到部署 1. 起点大学生竞赛管理系统这个毕设题有多大的含金量我接触过不少准备做毕业设计的同学很多人一听到“管理系统”三个字就觉得没有技术含量脑子里先冒出“不就是增删改查么”这个想法。但实际做完“基于Spring Boot Vue的大学生竞赛管理系统”这个题目之后我的观点变了管理系统类题目不是没含金量而是绝大多数人只做了表层的增删改查没把业务逻辑和工程化细节做透。这个题真正的难点在于如何把一场竞赛从“发布通知”到“报名”再到“作品提交”“评委打分”“获奖排名”的完整闭环在系统里跑顺并且让管理员、学生、评委三种角色各司其职。如果你能把这个闭环讲清楚代码实现规范答辩时老师很难挑出大毛病。先把这个系统的边界定清楚。大学生竞赛管理系统本质上是一个多角色、多流程的信息化平台。它的核心服务对象是三类用户管理员创建竞赛、审核竞赛、管理参赛队伍、分配评委、发布公告、设置奖项比例学生/参赛选手查看竞赛列表、在线报名、组队、上传作品、查看成绩评委/指导教师对学生提交的作品进行评审打分查看评分汇总。如果再拆细一点系统还应该包含学院的维度比如按学院统计参赛人数、按竞赛类型分析获奖率。这些数据在后端数据库设计阶段就提前预留字段后面做统计报表的时候会省很多事。那为什么偏偏选 Spring Boot Vue我自己的判断是这套组合在当前环境下有三个很实际的优势。第一Spring Boot 把 Spring 家族繁杂的 XML 配置砍掉了大半一个依赖加一个启动类就能跑起来对没接触过企业级开发的本科生来说学习曲线相对平缓。第二Vue 是目前前端生态里上手成本最低的框架之一配合 Element Plus 这类组件库能很快搭建出符合学校系统审美的管理界面。第三网上可参考的案例和问答极其丰富遇到问题搜一下基本都能找到解决方案这对毕设周期卡得很紧的学生来说非常重要。当然这套组合也是国内很多中小型公司实际在用的技术栈所以做完这个项目不仅是为了答辩写在简历上也有说服力。2. 数据库设计先行表结构决定开发会不会返工我见过太多人一上来就写 Controller写到一半发现缺字段又回去改表反复横跳浪费大量时间。做管理系统数据库设计一定要走在前头。尤其是毕业设计这种一个人要干完全部活的项目表关系如果理不顺后面每个功能模块都会跟着难受。2.1 核心实体与关系梳理大学生竞赛管理系统里最基本的实体有这几个用户、竞赛、报名记录、作品、评分记录、公告。如果你把奖励机制也算进去还应该有奖项配置表。以竞赛为主线来看这些实体的关系一个管理员可以发布多个竞赛所以用户和竞赛是 1 对 N一个学生可以报名多个竞赛一个竞赛可以被多个学生报名所以用户和竞赛之间是多对多关系中间需要有报名表来承接一次报名对应一个作品所以报名记录和作品是 1 对 1 的关系按团队参赛时一个团队一份作品一个竞赛对应多条评分记录因为每个评委都会对同一份作品打分所以竞赛和评分记录是 1 对 N作品和评分记录也是 1 对 N公告则相对独立属于管理员批量的内容发布。多对多关系一定要通过中间表转化这是数据库设计的基本素养也是答辩提问率极高的点。中间表不能只是简单地存两个外键报名状态、报名时间、团队名称这些字段都应该放进报名表里否则后面查“某竞赛报名了多少人、哪些人还在审核中”这类问题时你要么写很复杂的关联查询要么就得冗余字段都不划算。2.2 建表的核心 SQL我用 MySQL 来设计字符集统一用 utf8mb4不要问为什么不用 utf8等你哪天遇到用户提交的作品说明里带了一个 emoji 表情导致存储报错的时候就会感谢这个选择了。下面是几张核心表的精简版结构保留了最重要字段方便你理解整体设计。用户表CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT 密码密文, real_name VARCHAR(64) DEFAULT NULL COMMENT 姓名, role TINYINT NOT NULL COMMENT 角色: 1管理员 2学生 3评委, college VARCHAR(128) DEFAULT NULL COMMENT 学院, student_no VARCHAR(64) DEFAULT NULL COMMENT 学号, email VARCHAR(128) DEFAULT NULL, phone VARCHAR(32) DEFAULT NULL, status TINYINT DEFAULT 1 COMMENT 1启用 0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;竞赛表CREATE TABLE competition ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL COMMENT 竞赛名称, type VARCHAR(64) DEFAULT NULL COMMENT 竞赛类型, level VARCHAR(32) DEFAULT NULL COMMENT 校级/省级/国家级, description TEXT COMMENT 竞赛简介, start_time DATETIME DEFAULT NULL COMMENT 报名开始时间, end_time DATETIME DEFAULT NULL COMMENT 报名截止时间, status TINYINT DEFAULT 0 COMMENT 0草稿 1报名中 2评审中 3已结束, create_by BIGINT DEFAULT NULL COMMENT 创建人, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_type (type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT竞赛表;报名表和评分表CREATE TABLE competition_entry ( id BIGINT NOT NULL AUTO_INCREMENT, competition_id BIGINT NOT NULL COMMENT 竞赛id, student_id BIGINT NOT NULL COMMENT 学生id, team_name VARCHAR(128) DEFAULT NULL COMMENT 队伍名称, member_names VARCHAR(500) DEFAULT NULL COMMENT 团队成员姓名逗号分隔, status TINYINT DEFAULT 0 COMMENT 0待审核 1通过 2驳回, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_competition_id (competition_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT报名表; CREATE TABLE score_record ( id BIGINT NOT NULL AUTO_INCREMENT, competition_id BIGINT NOT NULL, entry_id BIGINT NOT NULL COMMENT 报名id, judge_id BIGINT NOT NULL COMMENT 评委用户id, score DECIMAL(5,2) NOT NULL COMMENT 评分, comment VARCHAR(500) DEFAULT NULL COMMENT 评语, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_entry_judge (entry_id, judge_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评分记录表;这里有一个容易被忽略的细节评分记录表的唯一索引。评委点击提交评分时后端如果没做幂等处理用户双击按钮就可能产生两条评分记录。加了uk_entry_judgeentry_id, judge_id之后数据库层面就直接兜底了同一评委对同一作品只能存在一条评分记录这比在 Service 层写一堆 if 判断要可靠得多。2.3 数据库设计中的几个典型教训讲几个我实际踩过、也见别人踩过的坑。第一个不要把所有跟用户相关的信息都塞进一张表。比如管理员可能需要维护评委的职称信息学生需要维护指导老师信息如果全部用一张用户表加一个 role 字段硬扛后期扩展字段时这张表会被改成“大杂烩”。我见过有人把“指导老师姓名”和“学生学号”放在同一行的查询时各种为 null维护起来极度痛苦。正确做法是用户表只保留通用字段再针对不同角色用扩展表或模块级字段去承接特有信息。第二个时间字段一定要用 datetime 而不是 varchar。别小看这个问题报名截止判断在系统里是一个非常核心的逻辑。如果时间存成字符串你会在比较“是否已截止”时到处写字符串转日期的代码而且一旦格式不统一数据直接没法比较。直接用 datetime配合 SQL 的原生比较和索引区块排序、筛选都干净利落。第三个状态字段建议用 TINYINT 加注释而不是用字符串枚举。虽然硬编码的 0、1、2 可读性确实差但在 Java 枚举类里做一层映射之后代码可读性就回来了。数据库里用数字存状态查询性能和存储占用都更好而且不容易因为一个多打一个空格导致查不出数据。3. 后端开发的节奏与细节数据库设计完成后端开发才算真正有了图纸。这个阶段不要急着写功能代码先把工程结构和依赖搭好。Spring Boot 版本的选择上我建议用 2.7.x 而不是刚出的 3.x。不是新版本不好而是在毕业设计这个场景下你大概率会找很多网上的教程和开源代码参考而老版本生态下的示例代码一搜一大把遇到诡异问题很容易查到解决方案。这一点在“Spring Boot 版本太高”导致依赖不兼容的求助帖里体现得淋漓尽致。Java 用 8 或者 11 都行JDK 8 稳JDK 11 也不差看你自己机器上装了什么别折腾。3.1 项目初始化与依赖选择创建一个 Spring Boot 项目时核心依赖我建议这样选spring-boot-starter-web提供 MVC 能力Controller 开发的基础mybatis-plus-boot-starterMyBatis Plus 封装了单表 CRUD能省掉大量样板代码mysql-connector-javaMySQL 驱动jjwt 或 hutool-jwt做登录令牌lombok简化实体类 getter/setterspring-boot-starter-validation参数校验。MyBatis Plus 是我在后端开发里强烈推荐的组件。它在 MyBatis 基础上封装了 BaseMapper像单表查询、分页查询、条件构造器这些高频操作都能直接调用现成方法不用手写 XML。手写 SQL 的场景通常只出现在多表关联查询里比如查“竞赛报名人数排行”或者“评委已评分数量”这种时候再用Select注解或 XML 都不迟。做配置的时候application.yml里几个点需要注意server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/contest_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case一定要打开这样数据库的create_time才能自动映射到 Java 里实体类的createTime字段不用每个字段都去写TableField注解。3.2 登录鉴权用 JWT 而不是 Session管理系统必然涉及权限控制我也强烈建议用 JWT 而不是传统的 Session。原因很简单JWT 天然适合前后端分离项目。用户登录成功后后端返回一个加密的 token前端存到 localStorage 里之后每次请求都在请求头带上Authorization: Bearer token后端拦截器校验 token 解析出用户身份不需要在服务端保存会话状态部署和横向扩展都方便。核心代码大致是这样的逻辑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.startsWith(Bearer )) { token token.substring(7); // 解析token校验签名和过期时间 Claims claims JwtUtil.parseToken(token); request.setAttribute(userId, claims.get(userId)); request.setAttribute(role, claims.get(role)); return true; } // 未登录返回401 response.setStatus(401); return false; } }需要注意的是拦截器只管“有没有登录”和“token 是否合法”具体的角色权限判断要再写一层。最简单的思路是从 token 里取出角色信息在 Controller 层用自定义注解或手动判断进行控制。比如管理员的接口只有 role 等于 1 的请求才能调用否则统一返回无权限。3.3 核心接口设计与实现后端接口设计要想清楚两个问题谁能调用返回什么。我列一下这套系统最核心的接口清单功能模块请求方式路径说明登录认证POST/api/auth/login登录成功返回 token 和用户信息竞赛管理GET/api/competition/page分页查询竞赛列表竞赛管理POST/api/competition新建竞赛竞赛管理PUT/api/competition/{id}修改竞赛竞赛管理DELETE/api/competition/{id}删除竞赛报名管理POST/api/entry学生报名参赛报名管理GET/api/entry/my查看我的报名记录作品管理POST/api/work/upload上传作品附件评分管理POST/api/score评委提交评分评分管理GET/api/score/result/{competitionId}查看竞赛成绩排行公告管理GET/api/notice/list查看公告列表接口路径统一以/api开头方便后续做统一的前缀处理或者网关转发。分页查询统一返回自定义数据结构比如ResultT实体里面放 code、message、data 三样东西。这里有一个很实际的建议分页参数用current和size而不是pageNum和pageSize因为 MyBatis Plus 的Page对象默认就是用这两个字段命名的。3.4 评分业务逻辑去掉最高最低分的计算评分功能是竞赛管理系统的业务重点也是你在答辩时可以重点展开讲的地方。它的逻辑不仅仅是往score_record表里插一条数据还要考虑几个实际问题报名人数很多时评委按什么顺序评分一个人评完能不能修改最终成绩怎么算我建议的实现方式是评委在评审页面看到分配给自己的待评分列表点击某个作品之后进入评分详情页填写分数和评语提交后数据落在评分表里。因为前面已经建了唯一索引同一个评委重复提交同一份作品只会触发更新不会重复插入。最终成绩计算一般竞赛会采用“去掉一个最高分、去掉一个最低分然后取平均”的规则。这个逻辑用 SQL 写比较绕但在 Java 里实现很直观public BigDecimal calculateFinalScore(Long entryId) { ListScoreRecord scores scoreRecordMapper.selectList( new LambdaQueryWrapperScoreRecord() .eq(ScoreRecord::getEntryId, entryId) ); if (scores.isEmpty()) { return BigDecimal.ZERO; } BigDecimal sum scores.stream() .map(ScoreRecord::getScore) .reduce(BigDecimal.ZERO, BigDecimal::add); if (scores.size() 3) { BigDecimal max scores.stream().map(ScoreRecord::getScore).max(BigDecimal::compareTo).get(); BigDecimal min scores.stream().map(ScoreRecord::getScore).min(BigDecimal::compareTo).get(); return sum.subtract(max).subtract(min) .divide(BigDecimal.valueOf(scores.size() - 2), 2, RoundingMode.HALF_UP); } return sum.divide(BigDecimal.valueOf(scores.size()), 2, RoundingMode.HALF_UP); }这个逻辑里一个值得注意的地方是BigDecimal的精度处理。数据库里我用DECIMAL(5,2)存储Java 里就不能再用double运算否则会出现 10.0 - 9.3 0.699999 这种经典浮点问题。用BigDecimal并设置保留两位小数既符合实际业务需求也避免被测试数据打脸。4. Vue 前端的开发与踩坑后端把接口调通了前端才能真正进入开发状态。我见过不少同学喜欢前端做一半、后端做一半然后两边对接时发现接口入参对不上来回改。我的习惯是后端先把 Controller 写完接口文档用 Swagger 或者 Apifox 导出前端按文档去对接配合效率高得多。4.1 前端工程化和组件库选择前端这里我建议直接用 Vue 3 Vite Element Plus 的组合。相比 Vue 2 的 Options APIVue 3 的 Composition API 在代码组织上更有条理而且 Vite 的开发服务器冷启动速度比 webpack 快很多调试体验不是一个级别。当然如果你对 Vue 2 的 Options API 更熟或者网上找的模板是基于 Vue 2 Element UI 的沿用也没问题技术选型只要能支撑项目完成就是合理的。创建项目npm create vitelatest contest-front cd contest-front npm install npm install vue-router4 pinia axios element-plus这里插一句“npm install 报错”“vue 安装依赖失败”是网上出现频率极高的问题。绝大多数情况是网络问题或 node 版本问题。解决思路很简单先node -v确认版本再设置 npm 源为国内镜像最后再删除 node_modules 重新安装。前端的依赖问题百分之七八十靠这三板斧能解决。4.2 路由与动态菜单设计前端路由按照角色来划分是比较合理的。系统里有管理员、学生、评委三种角色路由虽然写在同一份配置里但要根据登录用户的角色做过滤避免学生在地址栏手动输入/admin/competition/manage就能打开管理员页面。简单的做法是在路由元信息里声明meta.roles{ path: /admin/competition/manage, name: CompetitionManage, component: () import(/views/admin/CompetitionManage.vue), meta: { roles: [ADMIN] } }然后在全局路由守卫里检查router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); return; } const role localStorage.getItem(role); if (to.meta.roles !to.meta.roles.includes(role)) { next(/403); return; } next(); });这段代码的逻辑虽然简单但是把“未登录拦截”和“越权访问拦截”都处理到了。前端路由守卫是最后一道展示层的防线真正的权限控制还是在后端接口级别前后端双重校验才能拿得出手。4.3 axios 封装与响应拦截前端和后端交互我习惯把 axios 封装成一个独立模块统一处理 baseURL、token 注入、错误提示三个问题。下面是封装的核心逻辑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 error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) } ) export default request把 token 的注入统一放到拦截器里而不是在每次请求里手动添加是一个很重要的工程化习惯。这样即使后面新增几十个接口也不容易出现“某个请求忘了带 token 导致 401”的低级错误。401 跳回登录页的逻辑放在响应拦截器里也集中且可控。4.4 作品上传组件的前端细节竞赛管理系统里面作品上传是使用频率很高的功能。Element Plus 的el-upload组件默认走的是 FormData 文件上传你需要设定action属性指向上传接口的地址同时把 token 放入 headers 才能让上传请求通过后端拦截器校验。有一点很容易踩坑el-upload的on-success回调里的返回值并不一定就是后端返回的原始数据结构因为 Element Plus 内部可能对响应做了处理。稳妥的做法是在on-success里打印出来确认一层再往下写业务逻辑不要凭感觉认为response.data.url就一定存在。文件上传的两种常用处理方式前端把文件传到后端服务器后端存储到本地磁盘或云存储返回文件访问 URL再把 URL 存到数据库作品表里前端直接把文件转成 Base64 传到后端由后端保存。这种方式只适合小型文件图片还可以视频和压缩包几 MB 甚至几十 MB直接 Base64 会拖垮请求体完全不建议。5. 前后端联调时最容易翻车的场景到了联调阶段你会把前面所有模块拼在一起跑这时候的问题往往不是功能逻辑本身而是环境、路径、部署相关的“低级问题”。但恰恰是这些低级问题最容易卡住人半天时间。5.1 跨域问题前后端分离的第一个拦路虎前端跑在http://localhost:5173后端跑在http://localhost:8080浏览器默认会拦截跨域请求这就是所谓的 CORS 问题。解决思路有两个二选一即可。方案一是后端开启 CORS 配置在 Spring Boot 里加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }方案二是在前端开发环境配置代理在 Vite 的vite.config.js里server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }我个人更推荐方案二。因为后端一旦把 CORS 配成allowedOriginPatterns(*)虽然开发时很爽但放到生产环境上等于允许任意域名跨域调用接口。用前端代理的话生产环境通常直接由 Nginx 做反向代理前后端天然同源根本不存在跨域问题配置也更安全。5.2 Vue 打包后布局异常是怎么回事很多热词里“vue 打包后布局异常”是被搜索频率很高的问题我自己第一次部署这个项目时也遇到过。前端开发环境一切正常npm run build之后把 dist 文件扔到服务器打开页面发现样式完全乱了图标也不显示。这个问题大部分原因出在静态资源路径上。Vite 打包后的默认资源路径是相对路径还是绝对路径取决于base配置。如果你把前端文件部署在一个子路径下比如http://localhost:8080/contest但 base 默认是/那么所有资源都会从根路径去找自然会 404样式也就丢了。解决办法很简单在vite.config.js里根据部署路径设置 baseexport default defineConfig({ base: /contest/ })如果前端文件打算直接丢进 Spring Boot 的static目录和系统保持同域同端口base 设成./相对路径往往是最省心的选择。不过相对路径也有它的副作用比如 Vue Router 使用 HTML5 history 模式时刷新子路径页面会 404这时候要么改用 hash 模式要么让后端把所有请求都转发到 index.html。5.3 打包部署的完整过程部署方案我推荐两种你自己按场景选。第一种是把 Vue 打包产物放进 Spring Boot 项目里直接打成一个 jar 包运行。操作步骤是前端执行npm run build生成 dist 目录把 dist 目录里的文件全部复制到src/main/resources/static目录下后端执行mvn clean package -DskipTests打成 jar 包java -jar contest-system.jar启动浏览器直接访问 8080 端口前端页面和后端接口都在同一个服务上。这种方式对毕业设计演示和答辩非常方便一个 jar 包搞定全部老师想看什么功能你打开浏览器就能演示。缺点是每次改前端代码都得重新打包复制迭代起来比较烦。第二种是前后端分离部署Vue 打包产物扔到 Nginx 的 html 目录后端接口通过 Nginx 反向代理转发。这种方式更接近真实企业环境配置好后前端访问/api会被 Nginx 转发到后端的 8080 端口。我建议有条件的同学优先选这种方案因为部署过程涉及 Linux 命令、Nginx 配置这些都可以写进文档里作为“系统部署”章节能明显加分。5.4 文件上传路径的环境差异作品上传功能的文件存储路径在 Windows 本地开发时你可能会写D:/upload/works但部署到 Linux 服务器后这个路径就不存在了。正确的做法是把上传根目录配置到application.yml里添加上传文件的静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceLocations(file: uploadDir /); } }这样文件上传后返回给前端的 URL 就是/files/xxx.jpg前端能直接访问而物理文件实际存到的是你配置的目录。这个做法相当于把“业务访问路径”和“物理存储路径”解耦了换一台服务器部署时只需要改配置不用改代码。6. 性能优化与功能增强系统做完能跑了其实只算完成了 60%。如果要让它在答辩时经得起追问让老师觉得你考虑问题深入还需要做一些性能和功能层面的增强。这部分的投入产出比其实挺高的。6.1 分页查询与索引优化管理系统里最常用的查询就是分页查竞赛列表。如果不在后端做分页而是把全部数据查出来交给前端去切片那么数据量一旦上千条接口响应就会明显变慢前端渲染也会卡顿。MyBatis Plus 提供了分页插件配置之后只需要在 Service 层传入 Page 对象即可public PageCompetitionVO pageCompetitions(int current, int size, String keyword) { PageCompetition page new Page(current, size); LambdaQueryWrapperCompetition wrapper new LambdaQueryWrapper(); if (StringUtils.hasText(keyword)) { wrapper.like(Competition::getTitle, keyword); } wrapper.orderByDesc(Competition::getCreateTime); return competitionMapper.selectPage(page, wrapper); }光分页还不够数据库索引要跟上。竞赛表的status字段承担了大量查询筛选工作报名表的competition_id、student_id也经常出现在 where 条件里。这些字段如果不建索引数据量上来之后全表扫描的速度会让人崩溃。在正式数据库设计里就给这些高频查询字段建立索引是性价比极高的优化手段。6.2 数据库备份与迁移策略写毕业论文时“数据库设计”这一章除了表结构最好再提一下数据库的备份与恢复方案。备份并不复杂用 MySQL 自带命令就能完成mysqldump -u root -p contest_db contest_db_backup.sql恢复时执行mysql -u root -p contest_db contest_db_backup.sql学校实验室的机器环境经常不固定今天在这个电脑上开发明天换一台电脑继续写。这时候有个完整备份文件在手就不会担心环境迁移丢数据。你也可以顺便提一下用 Navicat 或者数据库同步工具做定时同步这些在实际工作中会接触到写上说明文档也会让老师觉得你不止停留在课本知识层面。不过要记住任何同步工具都不应该替代每天写代码之前的本地备份习惯我见过太多人同步配置没配对反而把线上数据覆盖了。6.3 主动加分的扩展功能方向这里分享几个我在原有系统上扩展过、也确实在答辩时被老师肯定的功能你可以根据自己的时间安排选择性实现。第一个是 Excel 批量导入导出。管理端把学生名单导入系统或者把竞赛成绩导出成 Excel 给学院存档用的是 EasyExcel 库几行代码就能实现。这个功能很接地气学校教务处的老师会喜欢。第二个是邮件通知。竞赛报名审核通过后自动发邮件提醒学生确认。尤其是有多个竞赛同时进行时系统在后台将结果汇总并发送邮件体验会好很多。Spring Boot 的spring-boot-starter-mail封装得很完善QQ 邮箱或学校邮箱配置成 SMTP 就能用。第三个是作品视频在线预览。很多创意类、视频类竞赛提交的作品是视频文件如果前端能直接播放视频会显得系统很完整。这里有一个技术点原始视频 mp4 格式大文件在浏览器里播放效率不高更专业的做法是转成 m3u8 分片流前端用 video.js 或 hls.js 播放。虽然这个功能开发成本稍微高一点但一旦做出来整体项目的完整度和技术深度会高出不少也正好踩中当下前后端视频处理的实操方向。7. 文档、答辩和我翻过的车毕设不只是写代码文档和答辩占的分数比重甚至比代码还高。很多同学代码功能都实现了结果文档写得像流水账答辩被老师一问就卡壳最后分数反而不如代码写得少但能说会道的同学。这一节我就专门聊聊文档和答辩这两件事。7.1 毕业论文和设计文档怎么写才不虚文档的核心目标只有一句话让老师快速看到“你做了什么、为什么这么做、效果怎么样”。框架我推荐按这种顺序走绪论背景、国内外现状、研究意义需求分析系统参与者、功能需求、非功能需求配合用例图总体设计系统架构图、功能模块划分、技术选型理由详细设计数据库设计表结构、E-R 图、核心接口设计、关键功能流程图系统实现每张核心页面截图 对应代码块 一句话实现思路系统测试测试环境、功能测试用例表、测试结果总结与展望。有一个观点我得强调需求分析绝对不只是为了凑字数它是整个系统开发的指南针。你写“学生可以查看竞赛信息并报名”就对应了后端的竞赛查询接口和报名接口你写“管理员可以审核报名”就对应了报名状态的更新逻辑。文档里的每一个需求点都要能在代码里找到落点这样答辩时老师问你“这个需求怎么实现的”你才能对答如流。系统的关键界面截图要放高清大图代码只贴最核心的部分不要让文档变成代码堆积。老师看论文的时间有限他要看到的是你的结构能力和总结能力而不是复制粘贴的代码搬运能力。7.2 答辩高频提问与应对话术以我参与过答辩现场的经验看老师对管理系统类项目的高频问题集中在这几个第一个为什么选 Spring Boot Vue我的建议不是背概念而是用“解决了什么问题”的角度去答。比如 Spring Boot 解决了传统 Spring 配置繁琐、部署不便的问题Vue 解决了前端页面复用和交互复杂度的问题前后端分离可以让开发和维护更灵活。第二个Spring Boot 自动装配的原理是什么这个问题很多同学答不好其实核心就一句话Spring Boot 通过EnableAutoConfiguration引入大量AutoConfiguration类依据 classpath 中的依赖和application.yml里的配置在条件注解的控制下自动完成 Bean 的注册。你讲清楚“条件装配”这个概念老师就会觉得你理解了本质。第三个如果报名人数暴增系统会出现什么瓶颈怎么处理这是扩展性问题。你可以从两个层面答数据库层面加索引、加缓存应用层面对报名接口做幂等处理防止重复提交。如果你还能提到把对象存储从本地磁盘换成云存储再引入消息队列削峰那就更出彩了实际做没做先不讨论至少证明你有架构视野。第四个数据库设计范式的理解回答里带上三个要点第一范式要求字段不可再分第二范式要求非主键字段完全依赖主键第三范式要求消除传递依赖。在这个系统里把用户信息拆到多张表就是第三范式的体现但又因为查询性能部分统计字段保留了少量冗余这是范式和反范式的取舍。这样答有理论有实践。7.3 我翻过的三个经典亏第一个是密码明文存储。第一版系统我开发时图省事密码直接拿明文存进了数据库后来在演示阶段用测试账号登录Navicat 打开用户表时全被旁边同学看见了场面一度十分尴尬。后来我改成 BCrypt 加密存储Spring Security 里现成的BCryptPasswordEncoder加盐哈希安全性提升一大截。毕业后复盘这件事我得出一个结论做毕设也不能降低安全底线密码加密这种动作成本极低收益却极高答辩时也值得写在创新点里。第二个是异常处理不规范。系统第一版写 Controller 时到处 try-catch但很多 catch 之后把异常吞掉了前端永远只能收到一个 200 但 data 为空的响应。后来我统一用RestControllerAdvice做全局异常处理业务异常抛自定义的BusinessException未知异常由兜底处理器捕获并记录日志前端才能可靠地拿到错误提示调试效率终于正常了。第三个是文件上传后删除用户导致的脏数据。学生报名并上传作品后管理员把该学生账号禁用了结果作品表里还残留着这个人的数据导致评分列表出现查不到用户名的情况。这个问题后来我通过外键逻辑约束和 Service 层校验来解决删除用户前先查关联数据有关联就提示“该用户已存在报名记录不允许直接删除”而不是放出一个数据库底层的外键报错给用户看。这种细节在答辩时反而很加分因为说明你考虑过业务闭环而不是只会照着 CRUD 模板写代码。最后的个人体会是借这个毕设把 Spring Boot 和 Vue 这套技术栈从头到尾走一遍收益远比想象中要大。当你真正把一个系统从数据库设计、后端接口、前端页面、部署上线到文档撰写完整地做下来你学到的已经不只是教科书上的语法而是“如何把一个模糊的需求变成可运行的产品”的能力。这套系统做完以后你完全可以在这个基础上往里面加功能、换技术组件让它变成自己简历上真正讲得出来的经历。
返回列表