毕业设计选了“高校疫情防疫管理系统”这个题目的同学,我猜你多半是被这套组合吸引过来的:技术栈足够经典(Java + Spring Boot + MySQL),业务场景足够完整(上报、审批、统计、预警一条龙)。哪怕疫情已经翻篇,这个题目依然是练手Spring Boot+MySQL的绝佳载体——它的角色边界清晰、流程闭环完整、可演示的点很多,答辩时不容易被问倒。
这篇文章我会按真实的毕设开发顺序来写:从需求拆解、技术选型、数据库设计,到核心功能实现,再到我这几年带毕设踩过的坑和排查思路。文中的表结构、代码、接口设计都不是PPT级的空话,照着抄就能跑通。适合正在选题、或者已经在开发路上卡住的同学参考。
1. 需求分析与功能模块拆解
1.1 角色与权限:先搞清楚谁在用什么
做毕设最容易犯的错就是一上来写代码,写到一半发现“这个功能到底给谁用”都没想清楚。高校防疫管理系统的核心在于“多角色协同”,我从用户侧说起。
系统至少要划分四种角色:
- 超级管理员:负责系统配置、账号管理、学院与班级维护,以及全量数据的总览。
- 辅导员/院系管理员:管理本学院或本班级的学生,查看学生上报情况,处理异常预警,审核学生的出入校申请。
- 校医院/医务人员:接收异常上报信息,对体温异常或症状异常的学生进行跟踪记录,标记隔离观察状态。
- 学生:每日提交健康状况(体温、症状、行程轨迹/接触史),提交出入校申请。
这四种角色不是拍脑袋定的,它们对应了防疫管理的真实工作流:学生是数据来源,辅导员是信息审核节点,校医院是专业处置节点,超级管理员是系统维护者。你把这个模型理清楚,后面做权限控制、菜单动态显示、数据权限隔离都顺理成章。
1.2 核心业务流程:三个闭环必须能跑通
需求分析阶段建议先把流程图手画一遍,不一定要用专业工具,白板或纸笔都行。这个系统真正要跑通的流程有三条:
流程一:每日健康上报闭环。学生登录后填写体温、是否有咳嗽/乏力等症状、近期是否接触过异常人员、当前所在地。提交后系统自动判断是否为异常数据:体温超过37.3℃或症状字段非空即标记为异常,并推送提醒给对应班级的辅导员。
流程二:异常处置闭环。辅导员看到异常预警后,进行初步核实,必要时转交校医院。校医院建立跟踪记录,标记是否需要隔离观察,填写处理意见,直到学生状态恢复正常才关闭这个跟踪单。
流程三:出入校审批闭环。学生提交出入校申请(事由、目的地、离校时间、预计返校时间),辅导员审批通过或拒绝。通过后学生在门岗处出示审批结果,管理员可按时间段查询出入记录。
这三个闭环就是系统的核心价值。答辩时如果有人问你“这个系统解决了什么问题”,你可以直接说:它把线下纸质填报、电话通知、人工审批的流程搬到了线上,并加入自动预警和统计报表,让信息流转效率提升,数据可追溯。这是很实在的亮点。
1.3 功能清单:模块划分决定开发量
我列一个可以直接当需求文档用的功能清单:
| 模块 | 核心功能 | 涉及的页面 |
|---|---|---|
| 登录与权限 | 登录、登出、JWT鉴权、菜单动态展示 | 登录页、主框架页 |
| 用户管理 | 学生/辅导员/校医院账号的增删改查、重置密码 | 用户列表、用户表单 |
| 基础数据管理 | 学院管理、班级管理、学号导入 | 学院列表、班级列表 |
| 健康上报 | 学生每日上报、历史记录查询、今日上报状态 | 上报页、记录列表 |
| 异常预警管理 | 异常数据展示、预警提醒、跟踪处置 | 预警列表、跟踪表单 |
| 出入校管理 | 学生申请、辅导员审批、出入记录查询 | 申请页、审批列表 |
| 数据统计 | 上报率统计、异常趋势、学院排名 | 统计看板、图表页 |
按这个清单估算,后端接口大约30个左右,前端页面15个左右。对毕设来说工作量适中,即使是一个人全程开发,按每天4-6小时算,4到6周能做出一个完整可演示的版本。
2. 技术选型与核心架构设计
2.1 为什么是Spring Boot + MySQL,而不是其他组合
直接说结论:这个组合是毕设领域“性价比”最高的方案,没有之一。
先看Spring Boot。它的核心优势是“约定优于配置”——你不需要像传统SSM项目那样写一堆XML配置文件,也不需要关心Spring和Spring MVC版本是否兼容的问题。一个spring-boot-starter-web依赖就能把Web开发环境搭起来,内嵌Tomcat容器意味着你本地跑mvn spring-boot:run就能启动项目,部署也是打一个jar包就完事。
再对比其他框架组合:
- 如果选SSM(Spring + Spring MVC + MyBatis)手动整合,你要处理jar包冲突、写web.xml、配置事务管理,一套下来至少消耗3天时间,而且这些工作跟业务无关,纯粹是配置环境。对毕设来说这个时间花得不值。
- 如果选更重的Spring Cloud微服务体系,你需要在Eureka/Nacos、Feign、Gateway这些组件上花大量时间,而业务本身就是一个单体系统,引入微服务只会增加演示时出问题的概率。
- 如果选非Java方案,比如Node.js + Express或Python + Flask,虽然写起来快,但“Java后端”这个技术要求在很多学校的毕设选题里是硬性条件,而且Java生态在找工作面试时也更对口。
数据库选MySQL的理由同样实在:它免费、跨平台、资料多,更重要的是MySQL 8.0支持窗口函数、公用表表达式(CTE)等高级特性,写统计类SQL会比老版本优雅不少。整个互联网上关于MySQL的坑和解决方案早就被踩平了,你遇到任何报错,复制错误信息到搜索引擎基本都能找到答案,这对毕设阶段的学生来说太重要了。
2.2 前后端分离还是服务端渲染:我给的建议
这个决策会影响你后面一个月的开发节奏,得认真想清楚。
方案A是前后端分离:前端用Vue 3 + Element Plus + Axios,后端提供JSON接口。这个方案的优点是演示效果好看,页面交互流畅,接口文档写出来也像模像样;缺点是你要同时维护两套项目,还要处理跨域、Token存储、打包部署的问题,前端知识对纯后端学生来说有一定门槛。
方案B是服务端渲染:后端用Thymeleaf模板引擎,页面由后端渲染,前端只用Bootstrap写基础样式。优点是只需要维护一个项目,技术栈统一,打开页面就能直接测;缺点是页面交互效果弱,前后端分离的“现代感”不足,而且Thymeleaf写复杂的前端交互时会让你怀疑人生。
我的个人建议:如果你是Java后端方向的学生,且时间在4周以内,选方案B,效率优先,把一个项目做完整比做两个半吊子项目强得多。如果你已经学过Vue,或者有前端基础,选方案A,答辩演示的加分效果明显。我接下来的讲解以后端为核心,前端方案你可以根据自己情况替换。
2.3 项目目录结构设计:一套能撑到答辩的工程布局
我使用的基础包结构如下:
com.example.epidemic ├── common # 通用类:统一返回结果、异常处理、常量 ├── config # 配置类:MyBatis-Plus、跨域、拦截器注册 ├── controller # 控制层:接收请求、参数校验 ├── service # 业务层:核心逻辑实现 │ └── impl # 业务实现类 ├── mapper # 数据访问层:MyBatis-Plus接口 + 自定义SQL ├── entity # 实体类:与数据库表对应 ├── dto # 数据传输对象:接收前端参数、返回视图数据 ├── util # 工具类:JWT工具、Excel导入工具 └── interceptor # 拦截器:登录鉴权、角色校验这里说一下每个分层的职责边界,这是很多同学写代码时容易乱的:Controller只做参数接收和结果包装,不写业务逻辑;Service层专注业务规则,比如“判断今天是否已上报”“审批通过后修改申请状态”这类逻辑必须放在Service;Mapper层只做数据读写,不写if else判断。遵守这个约定,你的代码量会减少很多——不信可以试试把一段业务逻辑塞进Controller,后面每次改动都会很痛苦。
统一返回类和全局异常处理是你一定要提前做好的基建。返回类用泛型设计:
@Data public class Result<T> { private Integer code; // 200成功,500失败,401未登录 private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }配合@RestControllerAdvice做全局异常捕捉,前端就不用每个接口都写一大段错误处理逻辑了。
3. 数据库设计实战:核心表结构拆解
3.1 数据表清单与职责划分
数据库设计直接决定你后面编码的顺畅程度。我先给完整的表清单:
| 表名 | 表职责 | 关键关系 |
|---|---|---|
| sys_user | 用户表(学生/辅导员/校医院/超管) | 唯一主表 |
| sys_role | 角色表 | 用户-角色多对一 |
| college | 学院表 | 用户所属学院 |
| classes | 班级表 | 学院下多个班级 |
| health_report | 健康上报记录表 | 关联sys_user |
| access_apply | 出入校申请表 | 关联sys_user, approver |
| abnormal_track | 异常跟踪记录表 | 关联health_report |
| sys_log | 操作日志表 | 记录关键操作 |
一张表只干一件事,这是设计原则。比如把学生信息和健康上报记录混在一张表里,字段会越加越多,查询也会越来越慢,还容易产生数据冗余。
3.2 核心表结构与字段设计
用户表是所有功能的起点,字段设计如下:
CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(100) NOT NULL COMMENT 'BCrypt加密后的密码', real_name VARCHAR(30) NOT NULL COMMENT '姓名', student_no VARCHAR(20) COMMENT '学号(学生必填)', role_id TINYINT NOT NULL DEFAULT 3 COMMENT '角色1超管2辅导员3校医院4学生', college_id BIGINT COMMENT '所属学院', class_id BIGINT COMMENT '所属班级', phone VARCHAR(20) COMMENT '联系电话', status TINYINT DEFAULT 0 COMMENT '0正常,1禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT '用户表';注意几个细节:密码字段长度设到100,因为BCrypt加密后的字符串有60个字符,用VARCHAR(50)会存不下,这是一个很容易踩的坑。student_no加唯一索引,避免同一学号被导入两次。role_id用TINYINT就够了,别用VARCHAR存角色名,后面改角色名称时需要逐条更新数据,非常麻烦。
健康上报表是每天写入频率最高的表,设计时优先考虑“查询效率”和“防止重复上报”:
CREATE TABLE health_report ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT '上报学生ID', report_date DATE NOT NULL COMMENT '上报日期', temperature DECIMAL(3,1) NOT NULL COMMENT '体温', is_abnormal TINYINT DEFAULT 0 COMMENT '0正常1异常', symptom VARCHAR(255) COMMENT '症状描述', contact_history TINYINT DEFAULT 0 COMMENT '是否有异常接触史0无1有', location VARCHAR(100) COMMENT '当前所在地', remark VARCHAR(255) COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, report_date) ) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COMMENT '健康上报表';UNIQUE KEY uk_user_date (user_id, report_date)是防重复上报的数据库兜底手段。你可以在Service层写“先查今天是否已上报,如果已上报就拒绝”,但并发情况下两个请求同时查到“未上报”然后同时插入,还是会产生两条数据。加了这个唯一索引,数据库层直接拒绝重复插入,后端只需要捕获异常转成友好提示就行。这就是我常说的“前端控制体验,后端保证安全,数据库兜底”。
3.3 设计要点与避坑
字段冗余是提升查询效率的常用手段,比如access_apply表中直接保存学生的姓名、学号和审批人的姓名,查询的时候不需要再去联表拿。这违反了传统范式,但在这个项目里是合理的,因为查询出入校记录时每次都JOIN用户表会增加开销,而且一旦学生改名字,历史申请记录应该保持原样。
时间字段统一用DATETIME,不要用TIMESTAMP。TIMESTAMP的范围到2038年就会溢出,虽然看起来离我们很远,但MySQL 8官方文档也推荐DATETIME做业务时间。Java侧对应LocalDateTime,MyBatis-Plus会自动处理映射,不会有时区问题。
逻辑删除是一个加分点。在实体类的删除标记字段上加上@TableLogic注解:
@TableLogic private Integer deleted;这样MyBatis-Plus执行delete操作时会自动转成update语句,把deleted置为1。学生删除后数据保留在库里,统计报表不会因为“删除了几个人”而数据对不上,答辩的时候这是一个很好的“数据安全意识”展示点。
4. 核心功能实现与代码实战
4.1 登录认证与JWT:不用Spring Security也能做好鉴权
登录模块很多同学纠结“要不要用Spring Security”。我的建议是:如果你对Spring Security不熟,用JWT + 拦截器完全够用,而且更容易展示你对原理的理解。
登录流程是:接收用户名密码 → 查库并校验BCrypt密码 → 生成JWT返回给前端 → 前端后续请求在Header里带上Authorization: Bearer token→ 后端拦截器解析token并放行。
生成JWT的代码:
@Component public class JwtUtil { // 密钥,正式项目必须放到配置文件中,不要硬编码 private final String secret = "your-secret-key"; public String generateToken(Long userId, String username, Integer roleId) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("username", username) .claim("roleId", roleId) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } }Token有效期设12小时,学生一天只上报一次,这个时长刚好覆盖一天的正常使用。密钥放到application.yml里,通过@Value注入,不要硬编码在类里。
拦截器负责解析token并存入ThreadLocal:
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { response.setStatus(401); response.getWriter().write("未登录"); return false; } // 解析token,异常说明失效 ... UserContext.set(userId); return true; } @Override public void afterCompletion(...) { UserContext.clear(); // 防止内存泄漏 } }再说一个容易被问到的点:为什么不用Session?Session需要服务端存储状态,如果部署多台服务器就涉及session共享问题。JWT把用户信息编码进token,服务端无状态,天然适合前后端分离。你把这个解释放答辩里,面试官会认为你是真的理解而不是背概念。
4.2 每日健康上报接口:业务规则写在Service层
学生上报接口是系统的核心写接口,逻辑包括:参数校验(体温范围、症状长度)、判断今日是否已上报、计算是否异常、插入记录并处理异常提醒。
@Service public class HealthReportServiceImpl implements HealthReportService { @Override @Transactional public Result<?> report(ReportDTO dto, Long userId) { // 1. 校验体温范围 if (dto.getTemperature() < 35.0 || dto.getTemperature() > 42.0) { return Result.error("体温数据异常,请重新测量"); } // 2. 查询今日是否已上报(逻辑删除外) LocalDate today = LocalDate.now(); HealthReport exist = healthReportMapper.selectOne( new LambdaQueryWrapper<HealthReport>() .eq(HealthReport::getUserId, userId) .eq(HealthReport::getReportDate, today)); if (exist != null) { return Result.error("今日已上报,请勿重复操作"); } // 3. 判断异常 boolean abnormal = dto.getTemperature() > 37.3 || StringUtils.hasText(dto.getSymptom()) || dto.getContactHistory() == 1; // 4. 插入上报记录 HealthReport report = new HealthReport(); ... // 组装字段 healthReportMapper.insert(report); // 5. 异常则生成预警提醒 if (abnormal) { alertService.createAlert(report.getId(), userId); } return Result.success(null); } }注意@Transactional注解。上报和生成预警分属两张表的写操作,要么全部成功要么全部回滚,不能让“上报成功但预警没生成”这种情况发生。事务没加的后果是:学生在演示时上报一条异常数据,然后你发现预警列表里是空的,现场特别尴尬。
前端建议做一层“今日已上报就禁用按钮”的交互,但这只是体验优化,真正的防重复依赖的是数据库唯一索引。我在实操中就遇到过一个学生用两次浏览器tab同时提交,靠前端判断挡不住,靠后端的select先查也经不住并发,最后是唯一索引兜住并返回了数据库唯一键冲突异常,把异常捕获后转成“今日已上报”的提示,问题才彻底解决。
4.3 出入校审批:用状态机思想管理数据流转
出入校申请的状态流转是:待审批 → 通过 / 拒绝 / 撤销。千万不要在业务代码里放任意的if-else嵌套去做状态更新,用状态机思想会清晰很多。我给你一个简化但能跑通的设计:
public enum ApplyStatus { PENDING(0, "待审批"), APPROVED(1, "已通过"), REJECTED(2, "已拒绝"), CANCELED(3, "已撤销"); private final int code; private final String desc; } // 审批操作 public class ApplyAction { public static final String SUBMIT = "submit"; public static final String APPROVE = "approve"; public static final String REJECT = "reject"; public static final String CANCEL = "cancel"; }在Service层做状态流转校验:
@Transactional public Result<?> approve(Long applyId, Long approverId, String remark) { AccessApply apply = accessApplyMapper.selectById(applyId); if (apply == null) { return Result.error("申请不存在"); } if (apply.getStatus() != ApplyStatus.PENDING.getCode()) { return Result.error("该申请已处理,请勿重复审批"); } apply.setStatus(ApplyStatus.APPROVED.getCode()); apply.setApproverId(approverId); apply.setApproveRemark(remark); apply.setApproveTime(LocalDateTime.now()); accessApplyMapper.updateById(apply); return Result.success(null); }这样做的收益是:状态变更路径可控,不会出现“把已拒绝的申请又改成已通过”这种非法操作。你还可以在类里写一个静态方法校验“当前状态是否允许该操作”,把状态机的规则集中管理。
4.4 数据统计报表:SQL聚合让答辩有亮点
统计功能是答辩时最直观的加分项。至少要做三个指标:今日上报率、异常人数趋势、各学院上报率排名。这里用MySQL聚合函就可以实现。
今日上报率的计算分为两步,分子是今天已上报的人数,分母是正常状态的学生总数:
public Map<String, Object> getTodayStats() { // 已上报人数 Long reported = healthReportMapper.selectCount( new LambdaQueryWrapper<HealthReport>() .eq(HealthReport::getReportDate, LocalDate.now())); // 应报人数(用户表中所有启用状态学生) Long total = userMapper.selectCount( new LambdaQueryWrapper<User>() .eq(User::getRoleId, 4) .eq(User::getStatus, 0)); // 计算比例 ... }异常趋势可以按日期分组统计:
SELECT report_date, COUNT(*) AS abnormal_count FROM health_report WHERE is_abnormal = 1 AND report_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY report_date ORDER BY report_date;前端用ECharts的折线图展示,一周的数据趋势立刻就有视觉冲击力。值得提醒的是统计查询时要关注索引,report_date字段单独建索引,否则数据量大了以后全表扫描会明显变慢。虽然毕设数据量可能只有几百条感觉不出来,但答辩时如果有老师问“数据量大怎么办”,你可以顺势说出“对日期字段建立索引、按月分表”的优化思路,这是非常加分的回答。
5. 常见问题排查与避坑实录
5.1 环境与部署类问题
问题1:MySQL 8连接报Public Key Retrieval is not allowed。这是MySQL 8默认使用caching_sha2_password认证插件导致的,JDBC连接时加上allowPublicKeyRetrieval=true参数就能解决。我见过不下5个同学卡在这个报错上,还以为是用户名密码错了。
问题2:Spring Boot版本和MySQL驱动版本不匹配。如果你用的是Spring Boot 2.7.x,MySQL驱动版本一般由starter管理;如果升级到Spring Boot 3.x,JDK必须17以上,javax包名会变成jakarta,这些迁移点得提前查清楚。毕设求稳的话,我建议用Spring Boot 2.7.x + JDK 8 + MySQL 8.0组合,教程最多、报错最少。
问题3:端口被占用。Spring Boot默认8080端口,如果本地有其他服务占用,启动会直接报错。在application.yml里换成别的端口即可,或者用kill -9杀掉占用进程。
5.2 代码层面的典型报错
问题4:LocalDateTime序列化格式不对。接口返回的时间是2025-01-01T10:00:00这种带T的格式,前端显示不友好。在application.yml里统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8问题5:MyBatis-Plus分页查询不生效。很多同学导入分页插件依赖后直接跑,查询出来却是全部数据。你需要单独配置MybatisPlusInterceptor并加入PaginationInnerInterceptor,这是MyBatis-Plus分页的常见遗漏项。
问题6:前端请求跨域。前后端分离项目必踩的坑。配置一个全局CORS跨域类:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(InterceptorRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }5.3 答辩高频提问与应对
把你自己的项目吃透,比背一百道面试题更重要。这几类问题几乎必问:
问题1:数据安全性怎么保证?回答要点:密码使用BCrypt加盐加密存储,不能明文保存;JWT做身份认证,设置12小时有效期;SQL使用MyBatis-Plus参数绑定,有效防止SQL注入;关键操作记录操作日志,方便追溯。
问题2:并发情况下如何保证数据一致性?回答要点:以健康上报为例,数据库层用唯一索引兜底,业务层做幂等校验,事务机制保证上报与预警的原子性。虽然这个系统并发量不大,但设计上已经考虑了临界情况。
问题3:系统后续如何扩展?回答要点:可以引入消息队列实现预警通知的异步推送,比如RabbitMQ或Kafka;如果需要对接第三方健康码或行程数据,可以在Service层预留适配接口。别把话说完,留一点“可扩展空间”会让答辩老师觉得你有思考深度。
我在实际带毕设过程中的体会是,这个题目真正的价值不在功能有多炫,而在于让你完整走一遍“需求分析 → 数据库设计 → 后端开发 → 测试部署”的流程。很多同学到答辩前夜还在改表结构,根本原因就是前面需求没理清就动手写代码。你把这篇文章里的表结构、接口流程、状态流转关系先画出来,再来写代码,至少能省一半返工时间。
最后再分享一个小技巧:做完系统后,准备一个模拟演示数据集,包含10个学生、3个辅导员、2个校医院账号,以及过去7天的上报数据和几条异常记录。答辩演示时打开数据统计页,趋势图直接就有内容,比现场输入数据或者展示空页面有力得多。祝你把毕设做出一个能挺直腰板演示的作品。