从毕业设计管理系统看Spring Boot全流程开发与答辩实践
简介:本资源是面向高校计算机专业师生的毕业设计全流程数字化管理平台,聚焦选题分配、任务书审核、开题评审、中期检查、论文查重与答辩组织等核心环节,解决传统人工管理效率低、流程脱节、过程难追溯等痛点,适用于本科毕业设计教学管理与信息化系统开发实践。压缩包共2005个文件,涵盖1198个JavaScript前端逻辑、198个HTML页面结构、161个JSON配置与接口数据、155个Markdown文档说明及140个CSS样式文件,辅以PHP后端脚本、SQL数据库脚本和PDF/DOCX规范文档,整体23.57MB,结构完整、模块清晰,便于二次开发与教学部署。已有67人学习下载,提供含‘附赠资源’与详细说明文件的完整工程包,包含FastAdmin框架定制后台、Bootstrap响应式前端及Pimple轻量依赖注入实现,可直接运行并深入理解高校教务类系统的典型分层架构与业务闭环设计。 每年到三四月份,我这边就会集中收到一批计算机专业学生的求助,问题几乎都长一个样:毕设做什么题目才能既体现工作量,又不至于答辩现场被老师两句话问住?说实话,“毕业设计管理系统”这个选题放在前几年不算稀奇,但最近两年反而成了香饽饽,原因是它天然长在真实业务上——选题、任务书、开题报告、中期检查、论文查重、答辩评分,一整套流程全是硬场景。做电商你绕不开商品订单,做管理系统你也绕不开角色权限和多流程状态流转,但它比博客、商城多了一个优势:每一块功能都能在答辩时讲出“为什么这么设计”。这篇文章我就基于这个计算机系毕业设计管理系统,把从需求分析、数据库设计到核心代码实现、部署演示的全过程完整拆一遍,争取让你看完之后不仅能复现,还能在答辩时讲得头头是道。
1. 项目背景与核心需求拆解
1.1 高校毕业设计管理中的真实痛点
在系统立项之前,一定要先搞清楚线下流程到底痛在哪。很多高校的毕业设计管理还停留在“Excel表格 + 微信群通知 + 邮箱收文件”的阶段,我见过最夸张的情况是:选题汇总表在导师、教学秘书、系主任三个人手里来回传了五版,最终版本到底是谁改的已经没人能说清。学生提交开题报告用的是 Word 文档,文件名从“开题报告_最终版.docx”改到“开题报告_最终版2_真的不改了.docx”,导师下载之后还得自己手动改文件名才能区分。
这种模式下有四个核心痛点:
- 选题阶段靠抢,学生盯着群消息“拼手速”,导师这边却不知道谁抢到了、哪个课题被选满了,经常出现一个课题报了十几个人,另一个课题无人问津。
- 过程文件分散在各个私聊记录和邮箱附件里,导师想整体查看学生的进度,根本无从下手。
- 审核没有留痕,导师口头说“可以”,过几天学生改了内容,到底哪版通过过,双方都记不清。
- 中期检查和答辩环节需要大量人工统计,教学秘书熬夜汇总成绩、算加权平均,还容易算错。
这个系统要解决的核心问题,就是把这些无序的线下流程变成“每个环节都有记录、每个步骤都可追溯、每个角色都能看到自己关心的数据”的线上闭环。对计算机专业的学生来说,这个场景的价值还在于:业务需求足够复杂,可以自然引出权限管理、状态机、并发控制、文件存储、定时任务这些硬核技术点,全做下来简历和答辩都有东西可讲。
1.2 角色梳理与功能边界
做管理系统最忌讳一上来就写代码,先把角色画清楚,后面的路才会顺。这个系统我拆成了三个大角色,外加一个“隐形角色”:
| 角色 | 核心诉求 | 主要操作 |
|---|---|---|
| 学生 | 快速选题、按时提交材料、随时看审核结果 | 浏览课题、选择课题、填写任务书回执、提交开题报告、填写周报、提交论文、查看查重结果 |
| 导师 | 少做重复劳动、高效审批、掌握学生动态 | 发布课题、审核选题、审核开题/任务书、查看学生进度、评阅论文、参与答辩评分 |
| 管理员(教学秘书/系主任) | 掌控整个流程、配置时间窗口、处理异常 | 维护师生账号、配置选题开放时间、管理答辩分组、导入导出统计报表 |
| 督导/评阅老师 | 只关注评阅和答辩环节 | 评阅论文、录入答辩分数 |
功能边界的划分原则是:学生管“提交”,导师管“审核”,管理员管“规则”。不要在学生的页面里放任何审核功能,也不要在导师的页面里放账号管理功能。角色功能一旦越界,后面做权限控制会非常痛苦。
对系统设计而言,角色梳理还直接决定了数据库表结构和接口权限方案。我的建议是权限模型采用经典的 RBAC(基于角色的访问控制),先建用户表、角色表、用户角色关联表,再在接口层用注解控制访问权限,这样后面加一个“督导”角色,只需要往角色表里插数据、给接口配权限,完全不用改主体代码。
2. 技术选型与整体架构设计
2.1 后端与前端的技术栈
这套系统我推荐采用前后端分离架构,后端用 Spring Boot 2.7.x + MyBatis-Plus + MySQL 8.0,前端用 Vue 3 + Element Plus + Axios + Vite。
选这个组合不是因为它最“高级”,而是因为它最稳。Spring Boot 是 Java 系做毕设和中小型管理系统的事实标准,资料多,遇到问题随便搜都有答案;MyBatis-Plus 把单表 CRUD 封装得非常舒服,内置分页插件可以直接用,不用手写一堆 XML;Vue 3 + Element Plus 组件齐全,做后台管理界面基本就是“搭积木”,表格、表单、弹窗、上传组件都有现成的,能省大量时间。
如果前端基础比较弱,不想碰 Node.js 那套工具链,也可以退一步用 Thymeleaf 做服务端渲染。但是我从实际答辩效果来说,还是建议做前后端分离,因为现在高校老师对“前后端分离”这个概念非常买账,答辩时你可以理直气壮地说“前端部署在 Nginx、后端打包成 jar,两边通过 RESTful API 交互”,这句话本身就是加分项。
整个系统的架构可以概括为:浏览器访问 Vue 页面,Vue 通过 Axios 调后端 RESTful 接口,后端按业务模块分包处理,数据存到 MySQL,上传的文件落本地磁盘并通过 Nginx 做静态映射。视频演示的时候,这套流程可以当成“系统总体架构”直接画在白板上。
2.2 数据库设计:核心表结构和字段说明
数据库设计是这个系统能不能撑起“全流程管理”说法的关键。我列一下完整的数据表清单和核心用途:
| 表名 | 中文含义 | 核心用途 |
|---|---|---|
| sys_user | 用户表 | 存所有人的登录账号、密码、姓名、角色标识 |
| sys_role | 角色表 | 角色基础信息,如学生/导师/管理员 |
| sys_user_role | 用户角色关联表 | 用户和角色多对多关系 |
| student | 学生扩展表 | 学号、班级、方向、指导老师ID |
| teacher | 导师扩展表 | 工号、职称、研究方向、可带学生数上限 |
| topic | 课题表 | 课题名称、简介、要求、创建人、开放人数 |
| selection_record | 选题记录表 | 谁选了哪个课题、状态(待审核/通过/驳回) |
| task_book | 任务书表 | 任务内容、提交文件路径、审核状态 |
| proposal_report | 开题报告表 | 报告内容、提交文件路径、评审意见 |
| weekly_report | 周报表 | 每周进展、存在问题、撰写日期 |
| midterm_check | 中期检查表 | 总体进度、是否延期、检查结果 |
| thesis | 论文表 | 论文文件、版本号、提交状态 |
| plagiarism_record | 查重记录表 | 查重报告、相似度、查重时间 |
| defense_group | 答辩分组表 | 组名、组长、组员、房间、时间 |
| defense_score | 答辩评分表 | 学生ID、评委ID、各维度分数、总分 |
| notice | 通知公告表 | 系统内公告、流程节点提醒 |
几个关键的字段设计思路:
- 每张业务表都建议带
status字段,用 int 类型存状态码,比如 0=草稿、1=待审核、2=通过、3=驳回。为什么不用 varchar 直接存中文?因为状态码可扩展、可比较、数据库体积小,翻译成中文这件事交给前端枚举或者后端字典就好。 - 选题记录表必须加唯一约束
(student_id),一个学生同一时间只能有一条有效选题记录;同时加一个topic_id + status的组合索引,用来快速查某个课题当前有多少人处于“已通过”状态。 - 论文表单独加一个
version字段,学生每次重新提交自动加一,既保历史版本,又能在答辩时展示“我迭代了三次”的过程。
2.3 文件存储与上传方案
学生提交任务书、开题报告、论文,本质上都是文件上传。我见过有人在这个环节引入 FastDFS、MinIO 甚至阿里云 OSS,对毕设项目来说其实没必要。最务实的方案是:文件直接存到服务器本地磁盘的一个约定目录,比如/data/graduation/upload/,然后按模块分子目录,再通过 Nginx 把这个目录映射成静态资源路径。好处有三个:
- 零额外成本,不需要申请云存储服务,也不用配对象存储的密钥。
- 方便打包演示,整个项目压缩之后,数据文件跟着走,答辩现场不会出现“文件打不开”的尴尬。
- 下载预览简单,后端只需要返回文件的相对路径,前端拼上静态资源前缀就能在浏览器里打开。
要特别注意文件名的处理。用户上传的文件名中文居多,而且可能重名,落盘时必须重命名。我的习惯是用UUID + 时间戳 + 原文件后缀作为新文件名,原始文件名单独存在数据库字段里,下载时通过接口把原始文件名作为Content-Disposition返回。这样既避免文件名冲突,也避免中文文件名在 URL 传输过程中的编码问题。
3. 核心功能模块的落地实现
3.1 选题管理:并发与去重设计
选题环节是整个系统第一个容易翻车的地方。业务流程是:导师发布课题,管理员审核通过后上线,学生在规定时间内选择一个课题提交申请,导师确认后选题生效。
这里有两层并发问题:
- 两个学生同时选同一个课题,而该课题只剩 1 个名额,怎么保证不会超选?
- 同一个学生重复提交选题请求,怎么保证不会生成两条记录?
第一个问题的最优解是加唯一约束加乐观锁。selection_record表给student_id加唯一索引可以防重复提交;防超选要在更新课题剩余名额时加条件判断,比如:
UPDATE topic SET selected_count = selected_count + 1 WHERE id = #{topicId} AND selected_count < max_student这样“读-改-写”三步变成了一条原子 SQL,只要更新影响行数为 1,就说明抢选成功;影响行数为 0,说明名额已经被占满,直接返回“课题已满员”。我建议后端 Service 层在事务里先执行这条更新,再插入选题记录,很多并发问题都能被数据库本身拦掉。
第二个问题除了唯一索引之外,前端也要做交互层提示,比如学生一旦选过课题,对应的按钮就置灰。但前端限制只是体验优化,真正兜底的还是数据库的唯一约束。
3.2 任务书与开题报告的状态流转
任务书和开题报告这两个模块本质是同一个模式:“学生提交文件 -> 导师审核 -> 通过或驳回”。这个模式最应该做的事,是把状态转换的规则固化下来,而不是放任每个接口自己改状态。
我强烈建议定义一个状态机。拿开题报告举例,状态有DRAFT(0)、SUBMITTED(1)、APPROVED(2)、REJECTED(3)。合法的流转路径是:
- DRAFT -> SUBMITTED:学生提交
- SUBMITTED -> APPROVED:导师通过
- SUBMITTED -> REJECTED:导师驳回
- REJECTED -> SUBMITTED:学生修改后重新提交
如果代码里没有状态校验,学生把一份驳回的报告反复提交、导师把已通过的状态改回待审核,这种操作就会出现,最后数据全乱。实现上可以在 Service 层写一个状态转换校验方法,每次更新前先装载旧记录,再校验当前状态和目标状态是否在允许的转换矩阵里。
我实际写代码时喜欢用一个枚举类把所有状态转换规则表描述出来,这样不仅校验逻辑清晰,答辩时也可以说“我用了状态机的思想管理流程”,这比单纯堆 CRUD 高级不少。
3.3 中期检查与进度预警
中期检查模块要解决的问题是:导师怎么知道学生到底有没有在推进?很多学生交了开题报告就人间蒸发,直到最后一周才开始写论文。这里我设计了两个机制:
第一个是周报机制。学生每周提交一篇简单的工作记录,内容包括本周进展、遇到的问题、下周计划。导师在首页就能看到所有指导学生的最近周报时间,超过 7 天没有提交的学生,系统自动标记成“进度预警”。
第二个是中期检查节点。管理员可以配置中期检查的起止时间,在这个时间窗口内,学生提交中期检查表,导师审核后给出“按计划进行/存在风险/严重滞后”三个结论。这里需要引入 Spring 的定时任务@Scheduled,每天凌晨扫描一次,把超过预警阈值的学生统计出来,插入到通知表里,导师一登录就能看到。
定时任务的写法很简单,核心逻辑是查weekly_report表里每个学生最新的提交时间,再和学生表里的指导老师 ID 关联,最后生成提醒数据。注意定时任务要加分布式锁或者至少保证单实例执行,否则部署多副本的时候会因为重复执行产生重复通知。
3.4 论文提交、查重与版本管理
论文提交模块最容易被忽略的是“版本管理”。学生不是一次性交终稿的,很多人会反复修改,如果系统只有一个“上传论文”按钮,每次上传都覆盖旧文件,学生想找上一版回来就没戏了。我在thesis表里设计了一个version字段,每次提交时后端读取当前最大版本号并加一,所有历史文件都保留在磁盘里,数据库里每条记录对应一个版本。
查重模块是答辩提问的高发区。学生自己很难接触学校官方的查重系统,所以要做一个查重模块的合理方案是:设计一个plagiarism_record表,前端提供一个“提交查重”按钮,后端调用一个查重接口。如果做模拟,后端可以生成一个随机相似度并返回一份模拟报告,帮助演示流程;如果对接真实API,可以在代码里预留PlagiarismService接口,用 HTTP 客户端调用第三方服务,存储返回的报告编号和相似度,等学校提供接口时再替换实现。
务必注意,查重是敏感操作,真实场景中论文内容不能随便传给没有资质的外部服务。在系统的交互文案里,要把查重结果标注为“模拟演示数据”,避免导师误判。
3.5 答辩分组与评分统计
答辩模块可以做成两部分:分组管理和评分录入。
分组管理由管理员操作,根据学生所在班级、课题方向,把学生随机分到若干个答辩组,每个组关联一个组长、若干评委、答辩时间、答辩地点。这个功能可以用一个简单的分页列表加一个“自动分组”按钮实现,自动分组的算法甚至可以就是按学生 ID 取模,真实业务里更复杂的分组算法反而没必要在毕设阶段做。
评分表的字段要体现“维度化”。比如总分 100 分,拆成“论文规范性 20 分”“表达能力 20 分”“技术创新性 30 分”“回答准确性 30 分”。评委登录系统后,只看到分配给自己的学生列表,录入每个维度的分数。最终成绩由后端统一计算加权和,再汇总到成绩统计页面。
一个小的经验:评分之前要注意去重校验,一个评委对同一个学生只能评分一次,否则改来改去会出现多次评分记录。实现方式就是defense_score表加联合唯一索引(student_id, teacher_id)。
4. 实操过程与关键代码示例
4.1 项目初始化与依赖配置
假设你拿到的压缩包解压之后是这个结构:前端一个目录,后端一个目录,外加一个 readme。先看后端。使用 IDEA 打开项目之后,首先要确认pom.xml里的依赖是否齐全。我这里给一个核心依赖清单:
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>application.yml里最需要注意的几项是数据库连接、MyBatis-Plus 配置和文件上传大小限制:
spring: datasource: url: jdbc:mysql://localhost:3306/graduation_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 100MB max-request-size: 200MB mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 sa-token: token-name: satoken timeout: 2592000 is-concurrent: true is-share: true这里有两个细节:数据库连接串里必须加characterEncoding=utf8和serverTimezone=Asia/Shanghai,否则中文乱码、时间差 8 小时的问题会让你查一个晚上;文件上传大小限制必须调大,因为学生开放的论文往往有几十兆,Spring Boot 默认只允许 1MB。
4.2 基于 Sa-Token 的登录鉴权与角色控制
登录鉴权我推荐用 Sa-Token,而不是 Spring Security。原因很简单:Sa-Token 开箱即用,API 非常直白,不需要写一堆 SecurityConfig 的过滤器链,对毕设这种体量的项目完全够用,而且答辩时你能说清每个注解是干嘛的。
登录接口的核心逻辑是:根据用户名查出用户,校验密码,登录成功后调用StpUtil.login(userId),再把用户角色信息写入会话:
@PostMapping("/login") public ApiResult login(@RequestBody @Valid LoginDTO dto) { SysUser user = userService.findByUsername(dto.getUsername()); if (user == null || !PasswordUtil.matches(dto.getPassword(), user.getPassword())) { return ApiResult.error("用户名或密码错误"); } StpUtil.login(user.getId()); StpUtil.getSession().set("role", user.getRole()); return ApiResult.success(StpUtil.getTokenInfo()); }接口权限控制直接用注解:
@SaCheckRole("teacher") @PostMapping("/topic") public ApiResult createTopic(@RequestBody Topic topic) { topicService.save(topic); return ApiResult.success(); }这里有一个很实用的小知识:Sa-Token 的@SaCheckRole注解走的是拦截器配置,你需要在启动类或配置类里注册SaInterceptor,否则注解不生效。我见过很多人把依赖加上之后发现怎么鉴权不生效,就是漏了这一步。另外,前端拿到 token 之后,每次请求要在 header 里带satoken这个字段,前后端对应的字段名必须一致。
4.3 选题接口的完整实现
选题接口是整个系统里最需要小心写的接口。我直接给一个可落地的版本。先定义 Service 接口:
public interface TopicSelectionService { /** * 学生选择课题 */ void selectTopic(Long studentId, Long topicId); }实现类里做三步:校验、占名额、插入记录:
@Override @Transactional(rollbackFor = Exception.class) public void selectTopic(Long studentId, Long topicId) { // 1. 校验学生是否已有有效选题 Long count = selectionRecordMapper.selectCount( new LambdaQueryWrapper<SelectionRecord>() .eq(SelectionRecord::getStudentId, studentId) .ne(SelectionRecord::getStatus, 3)); if (count > 0) { throw new BizException("你已存在一条待处理或已通过的选题申请"); } // 2. 占住名额,通过 update 的条件判断保证不超选 int rows = topicMapper.occupySeat(topicId); if (rows == 0) { throw new BizException("该课题名额已满"); } // 3. 插入选题记录 SelectionRecord record = new SelectionRecord(); record.setStudentId(studentId); record.setTopicId(topicId); record.setStatus(1); // 待导师审核 selectionRecordMapper.insert(record); }topicMapper.occupySeat对应的 XML 是:
<update id="occupySeat"> UPDATE topic SET selected_count = selected_count + 1 WHERE id = #{topicId} AND selected_count < max_student </update>注意这里rollbackFor = Exception.class必须写,Spring 默认只回滚 RuntimeException,如果你抛的是自定义Exception而不指定类型,占住的名额就回滚不掉,会出现“没有选上但名额少了”的诡异 bug。这一步我踩过坑,印象特别深。
4.4 文件上传与在线预览
文件上传的后端代码不复杂,核心是落盘路径和返回 URL。我习惯把文件保存到可配置目录,通过配置文件里的upload.path指定:
@PostMapping("/upload") public ApiResult upload(@RequestParam("file") MultipartFile file, @RequestParam("module") String module) { if (file.isEmpty()) { return ApiResult.error("上传文件不能为空"); } String originalFilename = file.getOriginalFilename(); String ext = StringUtils.getFilenameExtension(originalFilename); String newFileName = UUID.randomUUID().toString().replace("-", "") + "." + ext; String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); File dir = new File(uploadPath + "/" + module + "/" + datePath); if (!dir.exists()) { dir.mkdirs(); } File dest = new File(dir, newFileName); try { file.transferTo(dest); } catch (IOException e) { throw new BizException("文件保存失败"); } String relativePath = "/" + module + "/" + datePath + "/" + newFileName; return ApiResult.success(relativePath); }文件预览有个麻烦点:如果是 PDF 可以浏览器直接打开,但如果是 Word 文档,直接在浏览器里没法看。两个思路:一是把上传入口限制成“上转 PDF”,在系统提示里写明“请将 Word 转成 PDF 后上传”;二是引入 LibreOffice 做在线转换,但这对毕设来说太重了。我推荐第一种,简单省事,答辩演示也不会出问题。前端接入 Element Plus 的el-upload组件时,注意在请求头里带上 token:
<el-upload :action="uploadUrl" :headers="{ satoken: getToken() }" name="file" :data="{ module: 'proposal' }"> <el-button type="primary">上传开题报告</el-button> </el-upload>4.5 查重模块的模拟与对接
查重模块我建议做成接口抽象 + 默认模拟实现。定义接口:
public interface PlagiarismCheckService { /** * 提交查重,返回一个任务id */ String submitCheck(Long thesisId); /** * 查询结果 */ PlagiarismResult queryResult(String taskId); }模拟实现里可以让线程睡几秒钟,模拟查重需要时间,再生成一个 5% 到 30% 之间的相似度:
@Service public class MockPlagiarismCheckServiceImpl implements PlagiarismCheckService { @Override public String submitCheck(Long thesisId) { String taskId = UUID.randomUUID().toString(); // 实际开发中可以存入数据库 return taskId; } @Override public PlagiarismResult queryResult(String taskId) { int similarity = 5 + (int) (Math.random() * 25); PlagiarismResult result = new PlagiarismResult(); result.setTaskId(taskId); result.setSimilarity(similarity); result.setStatus(1); return result; } }前端拿到 taskId 之后,用轮询方式每隔几秒调一次queryResult,直到 status 变成完成。把这段逻辑讲清楚,答辩时可以说“我了解文本查重的异步处理模型,模拟了提交任务和轮询结果的完整链路”,这个回答方向很加分。
5. 常见问题与排查实录
5.1 并发选题导致超选
现象:两个学生同时点击选题,数据库里同一个课题通过的人数超过了max_student。
排查思路:先看 SQL 日志,确认更新名额的 SQL 是否带上了selected_count < max_student条件。我遇到过有人把这条条件写错了,大于小于号方向搞反,或者直接在 Java 代码里做了 if 判断而不是用数据库原子更新,并发下必出问题。
解决方案就是前面讲过的:把“检查 + 更新”合并成一条带条件 UPDATE,把并发控制交给数据库。另外要在selection_record表加上唯一索引兜底,双保险。
5.2 文件上传超限与预览失败
现象:上传大一点的文件就报FileSizeLimitExceededException。
排查思路:先看 Spring 配置的max-file-size,再看 Nginx 的client_max_body_size。这两个都要调大,任何一个默认限制都会被卡住。
预览失败的另一个常见原因是文件路径不对。Nginx 映射静态目录时,location /upload/的 alias 路径必须和实际落盘路径对应。举个例子,如果文件存在/data/graduation/upload/proposal/2024/05/task.pdf,Nginx 配置应该是:
location /upload/ { alias /data/graduation/upload/; }少了末尾斜杠,或者路径写错,浏览器访问就是 404。
5.3 流程状态跳级问题
现象:学生提交的任务书还没审核通过,就能直接提交开题报告;或者开题报告被驳回了,学生还能进入中期检查。
排查思路:系统缺少状态流转的强校验。每个流程的接口入口处,都要先查上一环节的状态是否满足条件。
我在做流程控制时喜欢写一个“流程校验工具类”,比如FlowCheckUtil.checkProposalCanSubmit(studentId),里面查任务书状态和开题报告当前状态,不满足就抛异常。这样在多个接口里复用,不会出现同样的校验逻辑写两遍、最后改了一处漏了另一处的情况。
5.4 数据库中文乱码与时间时区
现象:页面上提交的中文说明,库里存成了问号,或者时间字段差了 8 小时。
排查思路:先看数据库连接串是否带characterEncoding=utf8,再看 MySQL 建表时的默认字符集是不是utf8mb4,最后看服务器和数据库所在的时区。
连接串带serverTimezone=Asia/Shanghai之后,绝大多数时区问题都能解决。建表时我建议统一在application.yml里配上:
CREATE DATABASE graduation_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;如果已经建了表,也可以执行ALTER TABLE xxx CONVERT TO CHARACTER SET utf8mb4;修复旧数据。
5.5 演示环境准备不充分
现象:答辩现场演示的时候,登录不进去,或者列表是空的,或者某个按钮报了“系统错误”。
排查思路:这不是代码问题,是演示环境没准备好。不要现场新建账号、现场走全流程,一定要提前准备好“预制数据”。
我的建议是准备三个演示账号,分别对应学生、导师、管理员,提前在库里造好完整过程数据:学生 A 已选题、任务书已提交、开题报告已通过、周报有 3 条、中期检查已完成、论文已提交、查重结果有相似度、答辩组已分配。演示的时候按这个精心设计好的顺序走,每一个页面都有数据可看。这样导师会觉得这个系统是真在跑业务,而不是个空壳子。
6. 部署上线与答辩演示技巧
6.1 本地打包含部署
项目完成后要能在 Linux 服务器上跑起来。后端先 Maven 打包:
mvn clean package -DskipTests打包完成后target目录下会生成一个 jar 文件,直接启动:
nohup java -jar graduation-system-1.0.0.jar --spring.profiles.active=prod > app.log 2>&1 &前端先构建生产包:
npm run build构建产物在dist目录,把这个目录拷贝到 Nginx 的 html 目录下,然后配置反向代理。如果前后端都在同一台服务器,前端请求 API 时要把/api前缀代理到后端的 8080 端口:
location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }6.2 使用 Docker Compose 一键启动
如果你的毕设要体现“工程化能力”,Docker Compose 是加分项。至少可以把 MySQL 和 Redis 用容器跑起来,后端 jar 包也可以打成镜像。一个简单的docker-compose.yml长这样:
version: '3' services: mysql: image: mysql:8.0 container_name: graduation-mysql environment: MYSQL_ROOT_PASSWORD: 123456 MYSQL_DATABASE: graduation_manage ports: - "3306:3306" volumes: - ./mysql/data:/var/lib/mysql - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql backend: build: ./backend container_name: graduation-backend depends_on: - mysql ports: - "8080:8080" frontend: image: nginx:stable container_name: graduation-frontend ports: - "80:80" volumes: - ./frontend/dist:/usr/share/nginx/html - ./frontend/nginx.conf:/etc/nginx/conf.d/default.conf depends_on: - backend把init.sql放到 mysql 初始化目录里,MySQL 容器第一次启动时会自动建表、导数据,这一点非常方便。写进文档里,评委老师如果现场想看环境部署,可以直接敲docker compose up -d,对工程能力的评价会高很多。
6.3 答辩演示的准备工作
答辩演示这个环节,准备得好不好,效果差别非常大。我总结了一个清单,照着准备基本不会冷场:
- 准备三套演示账号,提前截图记录密码,不要现场输入。
- 完整走一遍学生提交材料、导师审核、管理员分组的流程,把每一步可能出现的弹窗和异常都提前过一遍。
- 电脑充好电,浏览器提前把系统首页开好,收藏好登录地址。
- 打开开发者工具也没关系,但不要现场改代码、不要现场重启项目。
- 如果演示中途报错,不要慌,先看一眼是否是网络或环境问题;如果是代码 bug,直接说“这个边界情况我下来再处理”,不要在现场调试。
有一个技巧一定要用上:把“选题并发控制”“文件版本管理”“状态机流转”“定时任务预警”这四个点做成能力清单,答辩的时候导师不论问到哪个模块,你都能往这四件事上靠。这四件事代表着数据库设计能力、并发控制能力、流程建模能力和工程化能力,正好覆盖了计算机专业毕业设计最看重的几个维度。
这套系统我前后带学生做过好几个版本,最深的体会是:不要一上来就写代码,先把状态流转图和角色权限图画明白。你要是把流程梳理清楚了,后面的代码其实就是往框架里填逻辑。最后再分享一个小技巧,答辩前把演示账号的首页数据、选题记录、各环节审核都提前造好,别现场临时注册新账号跑流程,一旦某个状态没对上,冷场五分钟,分数就直接往下掉了。祝你的毕设顺利过审。
本文还有配套的精品资源,点击获取
