
1. 为什么高校疫情管理需要一套独立系统项目背景与选型逻辑高校的疫情防控和其他场景不太一样核心差异在于人员密度高、流动性大、身份主体明确。一个校区动辄上万名学生加上教职工、后勤人员、临时访客每天的健康数据、出入记录、异常上报如果靠微信群接龙或者Excel表格汇总信息滞后不说还容易漏报错报。疫情管理系统的核心价值不是做了个网页而是把健康状态、活动轨迹、异常处置这三件事串成一条完整的业务闭环。很多刚接触Java Web开发的同学第一反应是这不就是一个CRUD系统吗。这话对了一半疫情管理系统确实大量依赖增删改查但难点在数据之间的关联关系一个学生的健康打卡记录需要关联他的所属学院、班级、宿舍楼栋一次异常上报需要联动生成隔离观察记录同时通知辅导员每天的出入校审批需要校验当日健康状态是否符合通行条件。这些业务约束才是系统真正的复杂度来源也是面试官或答辩老师最感兴趣的部分。选型方面这套系统用的是SpringBoot2 Vue3 MyBatis-Plus MySQL8.0属于目前Java Web项目里非常主流、也非常稳妥的组合。为什么说稳妥因为这几个技术栈在社区里的资料最多、踩坑记录最全对于课程设计和毕业设计而言遇到问题能搜到解决方案比什么都重要。SpringBoot2负责后端接口的快速搭建Vue3负责前端页面的交互呈现MyBatis-Plus把最常见的单表操作省到极致MySQL8.0提供稳定的事务支持和JSON字段能力。每个组件都不过度复杂但组合起来恰好够用。这套系统适合谁准备毕业设计的计算机相关专业学生、想快速上手前后端分离项目的初级开发、以及需要给学校做一个轻量管理后台的运维同学。下面我会把系统的功能设计、技术细节、部署过程和踩坑经验完整拆开来讲照着做基本能跑通。2. 功能地图与数据模型先想清楚要管什么再写代码2.1 三类角色和各自的核心操作高校疫情管理的用户身份天然分成三层学生、教职工辅导员/教师、系统管理员。三种角色看到的界面和能做的操作完全不同这是系统设计的第一步。学生端的核心操作是每日健康打卡填报体温、健康状况、是否接触过风险区域、出入校申请离校报备、返校申请、查看个人健康状态和异常通知。教职工端的核心操作是查看所带班级/学院学生的健康数据、处理异常上报、审批出入校申请、查看统计报表。管理端的核心操作是用户管理、楼栋/宿舍信息维护、健康数据全量查看、公告发布、基础参数配置比如风险区域名单、打卡时间窗口。这里有个设计细节值得注意打卡数据要区分已提交和未提交而不仅仅是存一个打卡表。系统需要每天定时统计哪些人没有打卡生成未打卡名单推送给辅导员。这就意味着数据库设计时不能只设计一张打卡记录表还需要一个定时任务去比对用户表和打卡表。2.2 核心表的字段设计与关联关系我按实际开发时的习惯把表拆成四组用户体系、健康业务、出入管理、系统支撑。用户体系包括用户表user、学院表college、班级表clazz、宿舍楼栋表building、宿舍表dormitory。用户表除了账号密码还要挂上学院ID、班级ID、宿舍ID这样查一个人的完整信息时能顺藤摸瓜关联出一串数据。健康业务包括每日打卡表health_check、异常上报表abnormal_report、隔离观察表isolation_record。打卡表的核心字段是用户ID、打卡日期、体温值、是否咳嗽/乏力用int类型0或1存储、是否接触风险区域、健康码颜色、打卡时间。业务上记得加唯一约束user_id check_date防止同一个人同一天重复提交。出入管理包括出入校申请表access_apply、审批记录表approval_record。申请表的状态字段是关键待审批、辅导员通过、辅导员驳回、管理员最终确认。审批记录表不仅能追溯每一步谁做了什么操作也方便前端展示审批进度。系统支撑包括公告表notice、操作日志表operation_log。操作日志很容易被忽略但答辩时老师很喜欢问如何追溯某个管理员改了哪些数据有一张日志表就能从容应对。2.3 状态流转的设计思路业务上最核心的状态流转是异常处理闭环学生打卡时体温异常或勾选咳嗽等症状 - 系统自动生成异常上报记录 - 通知辅导员核实 - 辅导员确认后进入隔离观察流程 - 隔离期满且连续打卡正常后解除。每一步对应一个状态值表设计时用status字段标记前端根据状态值渲染不同的操作按钮。这个状态机设计在答辩时很有讲头建议在文档里画一个状态流转表。异常上报状态0-待核实1-核实中2-确认异常3-排除异常4-解除观察3. 后端落地SpringBoot2与MyBatis-Plus的工程化实践3.1 项目分层结构与包命名规范拿到源码后第一步不是急着启动而是先看包结构。合理的分层决定了后续维护体验。这个项目的后端遵循标准的三层架构Controller接口层- Service业务层- Mapper数据层外加entity实体类、dto传输对象、vo视图对象、config配置类、common通用类、utils工具类。commons包下放统一返回结果类Result、异常处理类GlobalExceptionHandler、业务异常类BusinessException。这三个类值得所有Java Web项目都配一套。统一返回结构最简单也最实用的是code状态码、message提示信息、data数据体。前端axios拦截器统一判断code是否为200省得每个接口单独处理异常。GlobalExceptionHandler用RestControllerAdvice注解能捕获Service层抛出的业务异常转成统一格式返回。这个小设计能让Controller代码干净很多业务里只需要throw new BusinessException(该日期已打卡)即可。3.2 MyBatis-Plus的常规操作与自定义SQL边界MyBatis-Plus对单表CRUD的简化非常夸张。继承BaseMapper 后insert、deleteById、selectById、selectList等常用方法直接可用。配合ServiceImpl和IService接口Service层也省了大部分基础增删改查代码。实际开发里我总结了一个经验能用BaseMapper解决的操作绝不自写SQL只有多表关联查询或复杂统计时才上自定义XML。以每日打卡为例单表插入就是一行healthCheckMapper.insert(healthCheck);但查询某学院今日未打卡学生名单就需要多表关联了。这个SQL我建议写在XML文件里Mapper接口只定义方法。select idselectUncheckedStudents resultTypecom.example.entity.UserInfo SELECT u.id, u.real_name, u.college_id FROM user u WHERE u.college_id #{collegeId} AND u.role_type student AND u.id NOT IN ( SELECT hc.user_id FROM health_check hc WHERE hc.check_date #{today} ) /selectMyBatis-Plus还有一个容易被低估的功能条件构造器Wrapper。动态条件查询用lambdaQuery()非常舒服不用写SQL也不怕字符串拼错。官方建议在service层用LambdaQueryWrapper做动态条件组装比如出入校申请按状态筛选LambdaQueryWrapperAccessApply wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(status), AccessApply::getStatus, status) .eq(userId ! null, AccessApply::getUserId, userId);这段代码的精髓在eq方法的第一个参数当条件为false时这个查询条件自动跳过天然支持了前端没传这个筛选条件就不加这个where。比一个个if判断写起来干净得多。3.3 定时任务的必要性与实现每天的健康打卡统计离不开定时任务。SpringBoot2自带Scheduled注解不需要引入额外的Quartz依赖就能实现每天23:00扫描一遍全量学生找出当天未打卡的人生成待办消息推给对应辅导员。定时任务里最关键的坑是时区问题服务器默认时区和北京时间不一致导致Cron表达式触发的时刻不对。解决办法是在启动类或配置文件中指定时区最简单的方式是配置项加上spring.jackson.time-zoneGMT8同时在定时任务中显式使用localDateTime.now()配合ZoneId指定国内时区。实际开发中这个任务还会遇到性能问题上万名学生一次性查出来循环比对虽然MySQL8.0可以扛住但更好的做法是定时任务里分批处理每批500人配合PageHelper或MyBatis-Plus的分页查询避免一次性加载大量数据造成内存压力。4. 前端落地Vue3组合式API与后台管理界面4.1 为什么这个阶段我强烈建议Vite而不是vue-cli很多教程还在教vue-cli创建Vue3项目但我建议新项目直接用Vite。核心原因就一个快。Vite基于ESModule开发环境下冷启动是秒级热更新也是毫秒级响应。而vue-cli的Webpack方案每改一行代码都要重新打包开发体验差距太大。另外这个项目既然用了Vue3搭配Vite是官方主推的路线遇到问题社区解决方案也多。创建项目的命令很简单npm create vitelatest epidemic-admin -- --template vue cd epidemic-admin npm install npm run dev4.2 组合式API是Vue3的核心分水岭Vue3相比Vue2最大的变化就是组合式API也就是setup语法。我在给学生讲的时候经常用一个类比Vue2的Options API像把一份报告分成背景、数据、方法三个固定章节一个功能的代码被拆散在不同区域Vue3的Composition API则像按项目归档每个功能的变量、方法、生命周期全放在一起。实际编码中健康打卡页面的逻辑大概长这样script setup import { ref, reactive, onMounted } from vue import { submitHealthCheck, getTodayRecord } from /api/health const todayRecord ref(null) const form reactive({ temperature: 36.5, cough: 0, fatigue: 0, contactRisk: 0 }) const loadToday async () { todayRecord.value await getTodayRecord() } const submit async () { await submitHealthCheck(form) ElMessage.success(打卡成功) loadToday() } onMounted(loadToday) /script表单校验是另一个Vue3的核心场景比如体温必须填写且在35.5到42之间这个范围。Vue3里用Element Plus的rules配置很方便但要注意一个细节自定义校验函数必须执行callback否则表单会一直卡在校验中状态。这是我踩过的坑也见过不少新手栽在里面。4.3 前端调用后端的接口协议与axios封装前后端分离项目必须解决的第一个问题是接口对接。前端封装axios时统一处理baseURL、token、响应拦截器。建议在src/utils/request.js里封装一个实例import axios from axios 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.data }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } ) export default request这里要注意跨域问题。本地开发最常见的方案是在Vite配置文件里配proxy代理而不是在后端开CORS。后端的CORS配置如果放开所有域名生产环境会引入风险比如携带着用户token的请求可以被任意第三方网站发起。Vite proxy配置如下server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }前端所有请求都写在/api下面代理自动转发到8080端口浏览器看到的还是同源请求绕开了跨域限制。4.4 权限路由的实现思路管理后台几乎必然需要权限控制学生登录后看不到管理员的菜单和页面。Vue3实现路由守卫配合本地存储或Pinia状态管理能解决大部分需求。我建议的方案是登录成功后将用户角色信息存入Pinia在路由配置里用meta字段标记哪些路由需什么角色router.beforeEach里做校验if (to.meta.role to.meta.role ! store.user.role) { ElMessage.warning(无权访问该页面) return / }比把所有页面全部注册成静态路由再在页面里做按钮级控制要省事得多菜单也能直接根据角色动态生成。5. 数据库环境MySQL8.0的安装配置与SQL细节5.1 MySQL8.0值得注意的几个改变MySQL8.0相比5.7有几个对开发和部署影响明显的区别我在第一次使用8.0时也踩过坑。第一是默认认证插件变了。8.0起默认使用caching_sha2_password而很多老工具或旧版驱动尤其是一些图形化客户端和服务端程序只支持mysql_native_password导致连接时报错。解决办法要么升级驱动连接器要么在建用户时指定认证插件CREATE USER epidemic% IDENTIFIED WITH mysql_native_password BY yourpassword; GRANT ALL PRIVILEGES ON epidemic.* TO epidemic%; FLUSH PRIVILEGES;第二是SQL关键字更敏感了。在8.0里rank、window、groups这些都成了保留字建表时字段名尽量避免这些词汇。疫情管理系统的表中status、type这种字段名完全安全但不要用rank做字段名。第三是group by的行为更严格了。MySQL8.0默认开启ONLY_FULL_GROUP_BY查询SELECT和GROUP BY的字段必须严格匹配。聚合查询时不要图省事直接select所有字段而要明确指定分组字段否则直接报错。5.2 数据库初始化脚本的设计源码自带的SQL脚本建议先完整过一遍不要盲目执行。重点检查三件事字符集是否统一为utf8mb4否则emoji和生僻字会变成乱码、是否存在外键约束导致表创建先后顺序问题可以先创建全部表再加外键、是否设置了合理的索引用户表的college_id、打卡表的user_idcheck_date都要建索引。我整理了一个建库模板供参考CREATE DATABASE IF NOT EXISTS epidemic DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE epidemic; DROP TABLE IF EXISTS user; CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 账号, password VARCHAR(100) NOT NULL COMMENT 密码(MD5加盐), real_name VARCHAR(30) NOT NULL COMMENT 姓名, role_type TINYINT NOT NULL COMMENT 角色:1-学生 2-教职工 3-管理员, college_id BIGINT COMMENT 学院ID, class_id BIGINT COMMENT 班级ID, dormitory_id BIGINT COMMENT 宿舍ID, phone VARCHAR(20) COMMENT 联系电话, status TINYINT DEFAULT 1 COMMENT 状态:1-正常 0-禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_college (college_id), KEY idx_class (class_id) ) ENGINEInnoDB COMMENT用户表; DROP TABLE IF EXISTS health_check; CREATE TABLE health_check ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, check_date DATE NOT NULL, temperature DECIMAL(3,1) NOT NULL, cough TINYINT DEFAULT 0, fatigue TINYINT DEFAULT 0, contact_risk TINYINT DEFAULT 0, health_code TINYINT DEFAULT 1 COMMENT 1-绿码 2-黄码 3-红码, remark VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_date (user_id, check_date) ) ENGINEInnoDB COMMENT每日健康打卡;注意到create_time和update_time我用了DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP这样插入和更新时不需要手动维护时间字段省了很多样板代码。5.3 Docker安装MySQL8.0的快速方式如果本机不想直接安装MySQLDocker是最省事的方案。一条命令就能拉起一个8.0实例docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORDroot123 \ -e TZAsia/Shanghai \ -v /opt/mysql-data:/var/lib/mysql \ mysql:8.0这里有两个参数值得说明。-e TZAsia/Shanghai是因为容器默认时区是UTCSpringBoot连接后时间会偏移8小时。-v /opt/mysql-data是数据持久化否则容器销毁后数据全丢这在开发机上跑的时候最容易忽略。6. 从源码到可演示项目部署运行的全过程与常见问题排查6.1 本地环境跑通的四个步骤拿到源码后我推荐按这个顺序操作能规避九成以上的启动问题。第一步是统一JDK版本。SpringBoot2要求JDK8及以上我用的是JDK8或JDK11都没有问题。如果本机装了多个JDKIDEA里Project Structure和Settings里都要确认用的是同一个版本两处不一致会出现错误: 无效的源发行版。第二步是配置MySQL账号信息。打开application.yml确认数据库连接串、用户名、密码与本地一致spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/epidemic?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: root123serverTimezoneAsia/Shanghai和useSSLfalse这两个参数尤其重要前者避免日期写入数据库时出现8小时偏差后者避免本机没有SSL证书时连接报错。allowPublicKeyRetrievaltrue是配合MySQL8.0的caching_sha2_password认证方式用的防止连接时报出public key retrieval not allowed的错。第三步是执行数据库脚本。用Navicat或命令行执行项目doc目录下的init.sql。执行时如果遇到Unknown database之类的错误先手动创建数据库或把脚本开头的CREATE DATABASE语句单独执行。第四步是先启动后端再启动前端。后端启动看日志里出现Started Application in X seconds说明接口层跑通了。前端npm run dev启动后浏览器访问localhost:5173能看到登录页就说明前后端已经连上了。用默认管理员账号登录后第一件事是检查首页的统计数字是否能正常加载这一步能确认数据库连接和第一张业务表的读写都正常。6.2 最常见的启动失败问题排查我在带学生跑这类项目时总结出几个高频问题。问题一接口请求404。前端能打开登录页但点击登录时请求报404。绝大多数情况是代理没配对检查前端vite.config.ts里的proxy配置target是否真的指向了后端启动的端口。还有一种情况是后端项目没在8080端口启动而是随机分配了一个端口。问题二MyBatis-Plus的Mapper扫描不到。SpringBoot启动时报Invalid bound statement (not found)。需要在启动类上加MapperScan注解或者用Mapper注解标记每个Mapper接口。尤其注意如果Mapper接口和XML文件不在同一个包路径下还需要在application.yml里指定XML文件的位置mybatis-plus: mapper-locations: classpath:mapper/*.xml问题三数据库连接失败。报错信息若包含Access denied for user检查密码或用户名是否打错包含Unknown database则说明数据库还没创建包含Communications link failure则大概率是MySQL服务没启动Linux环境可以用service mysql status检查Windows环境看任务管理器里的服务进程。问题四前端npm install失败。这类问题多半是镜像源访问慢导致的。切换成国内镜像源npm config set registry https://registry.npmmirror.com之后再重新install成功率会提升非常多。6.3 演示时的加分细节这个项目最容易被评委关注的点有三个建议演示前做好准备。第一是权限控制的演示。分别用学生账号和管理员账号登录展示同一个环境下不同角色看到的菜单差异比只演示管理员账号更有说服力。第二是异常流程的闭环演示。先模拟提交一次体温异常的打卡去辅导员账号里查看待处理记录处理成确认异常后再去管理员端看到的状态变化和隔离记录。这个流程走通说明系统不是简单的增删改查。第三是统计报表的直观化。如果前端已经集成了ECharts把首页的今日打卡率、各学院打卡率对比图展示出来。答辩时数据可视化页面永远是最抓眼球的部分。7. 源码中含文档的价值毕设文档比代码更值钱7.1 文档里应该包含哪些核心内容这个项目附带文档这点对做毕业设计的同学来说非常关键。一套完整的毕设文档我认为至少要覆盖这些内容需求分析部分系统背景、用户角色、功能用例图、业务流程说明。这部分要能回答为什么做这个系统。系统设计部分总体架构图、功能模块划分、数据库ER图、核心表结构说明。这部分要能回答系统怎么搭出来的。系统实现部分每个模块的代码思路、核心类的职责说明、关键技术点的实现分析。这部分是硬货直接展示工作量。系统测试部分测试用例表格、功能测试结果、性能测试简述。哪怕只做一轮简单的功能测试记录也比缺失这部分有说服力。7.2 文档与源码如何对照阅读我见过太多学生拿到源码后一头扎进代码里结果看三天还是一头雾水。更高效的方式是文档先行先读需求分析里的功能列表再对照数据库表结构搞清楚每张表对应哪个功能模块然后顺着一条业务链路走一遍代码比如登录-首页-打卡-提交把这几个接口的Controller、Service、Mapper全部跟一遍项目的整体脉络就清楚了。顺带说一个拆解技巧在IDEA里按CtrlShiftF全局搜索关键词比如搜索health_check能快速定位到所有操作这张表的代码搜索accessApply能定位到出入校申请的相关代码。这比手动翻目录高效得多。8. 项目还能怎么扩展几个值得尝试的方向如果不想止步于跑通一个毕业设计这套系统还有几个不错的扩展角度能显著增加项目的完整度和技术含金量。接入消息通知。目前系统里审批和异常通知基本靠站内消息扩展方向是接入邮件或短信服务学生提交出校申请后辅导员实时收到通知。SpringBoot里集成JavaMail或者阿里云短信都不复杂费用也很低。增加数据可视化大屏。在管理端首页接入ECharts展示今日打卡率、各学院对比、近7天体温异常趋势。这部分代码量不大但视觉效果提升非常明显。引入Redis缓存。健康打卡的当日记录是高频读取数据把当日统计结果缓存到Redis设置过期时间到第二天凌晨能显著降低数据库压力。把这一步做掉系统架构就多了一个缓存层答辩时能讲的技术深度完全不一样。我在实际使用中发现这类项目最大的敌人不是技术难点而是需求梳理不清导致代码反复修改。功能边界没定好就急着写SQL建表后面改起来非常痛苦。建议动手之前先把用户角色和核心流程在纸上画清楚建表顺序按用户-业务-流程-统计逐层推进每一步都明确依赖关系后期的返工成本会小很多。最后再分享一个对新手价值很高的小技巧在本地跑通项目之后找一条最核心的业务链路比如学生打卡到辅导员审核的完整过程从头到尾读一遍对应代码然后在代码里打上详细的注释拿这套代码去讲给同学听。能把别人讲明白说明你离真正掌握这套系统就不远了。