SpringBoot+Vue+MyBatis高校疫情防控管理系统设计实战
做管理系统的项目很多时候不是技术难而是业务乱、角色多、数据散。疫情类系统的难点更特殊使用频率高、上报时间集中、数据准确性要求高、还要应对各种临时统计需求。这篇文章把我做过的这套基于SpringBootVueMyBatis架构、MySQL数据库的高校疫情防控管理系统的核心设计思路和实操过程完整拆出来从需求梳理、表结构设计到前后端关键实现再到上线后排查过的问题一次性讲清楚。1. 项目定位与需求拆解1.1 这个系统到底解决什么问题高校疫情防控系统的核心不是“上报体温”这一个动作而是围绕学生健康数据建立一套完整的管理闭环学生日常健康上报、出入校登记、异常情况预警、辅导员批量处理、校级数据汇总统计。我们当时接到的需求看似简单等真正梳理完才发现涉及的角色至少有五类学生、辅导员、院系管理员、校医院、校级领导。每类角色眼里的“系统”都是不一样的东西。学生在乎的是“快”——打开页面填体温、传绿码其实就是上传一个状态码、点提交十秒内完成辅导员在乎的是“准”——班里谁没上报、谁体温异常一眼能看到校级领导在乎的是“全”——全校今天上报率多少、异常人数多少、趋势怎么走。一台系统要同时满足这几类完全不同的使用习惯就必须在功能设计上做明确分层不能简单堆菜单。还有个容易忽略的点这类系统要应对的是“高并发集中提交”场景。比如早上八点到九点是上课前的高峰几千人同时提交如果表结构或接口设计不合理很容易出现慢查询甚至超时。当时我们做压测就遇到过类似问题后面细说。1.2 为什么选这套技术栈而不是别的SpringBoot Vue MyBatis MySQL这个组合乍看没有什么新奇但它在这个场景下有几个很实在的优点第一SpringBoot让后端开发效率很高。不用像老Spring项目那样配置一堆XML内嵌Tomcat启动也方便打包成jar就能跑。对于这种需要频繁迭代的校内系统改完功能重新打包部署的成本很低。第二Vue做后台管理界面非常合适。项目用Vue 2带Element UI或Vue 3带Element Plus都可以组件化开发让页面复用率很高。比如学生列表、上报记录列表这些页面结构高度相似抽象成公共组件后开发速度会快很多。第三MyBatis的关系映射灵活适合写复杂统计SQL。这类系统的统计报表非常多学院上报率、班级异常数、每日趋势每张报表的SQL差异都很大。MyBatis可以让你直接控制SQL语句而不是像JPA那样在实体关系映射里绕圈子。我们实际做下来复杂查询用MyBatis明显更顺手。第四MySQL足够承载校园级别数据量。一个万人规模的高校日报表数据量一天产生一万多条一年累计几百万条配上合适的索引和分表策略MySQL完全扛得住。没必要一上来就上重型中间件轻量可靠才是校内系统的首选。提示技术选型不是越新越好而是要看团队熟悉度和运维成本。校内系统通常由教务处或信息中心的老师维护技术栈越常见后续接手成本越低。2. 系统架构与核心功能设计2.1 前后端分离下的模块划分整个系统采用标准的前后端分离架构前端是Vue工程跑在Nginx上后端是SpringBoot的jar包跑在服务器上数据库用MySQL。前后端通过RESTful接口通信使用JSON传递数据。后端按业务模块分包而不是按技术层分包这是我们实践下来比较舒服的组织方式com.xxx.health │ ├── common // 公共类统一返回结果、异常处理、工具类 ├── config // 配置类跨域、拦截器、定时任务 ├── controller // 控制层接收请求 ├── service // 业务层处理核心逻辑 ├── mapper // 数据访问层MyBatis接口 ├── entity // 实体类 ├── dto // 入参/出参对象 └── vo // 视图对象给前端的数据结构这样分包的好处是一个模块比如健康上报从Controller到Mapper的所有代码都聚在一起改需求的时候不用在多个技术包之间跳来跳去。不同模块之间通过Service接口交互尽量避免一个模块直接调用另一个模块的Mapper。前端部分按路由划分页面组登录注册页、学生端工作台、辅导员管理页、校级数据大屏、系统管理页。每块功能独立成视图文件夹配上对应的API请求文件。有点要注意的是做这类多角色系统前端路由和后端权限要双重控制。前端控制只是提升体验——咱不显示无权限的菜单但真正的数据安全必须靠后端接口鉴权。否则别人猜到接口地址直接发起请求就能越权拿到数据。2.2 核心业务模块拆解整个系统的功能围绕“报、查、管、统”四个字展开报学生每日健康上报包括体温、健康状况、是否在校、行程状态。这是数据入口也是最核心的高频操作。查各级管理人员查询本组织范围内的人员健康数据。辅导员查班级院系管理员查全系校级管理员查全部。管对异常数据的管理包括体温异常标记、疑似症状登记、隔离观察人员台账、出入校审批。统维度汇总统计按学院、班级、时间三个维度做上报率、异常率、趋势分析。这四个字对应到系统菜单就是学生端的三五个功能和后台管理的七八个功能模块不再堆砌更多花哨功能。我们当时犯过的错是把功能设计得很“全”结果很多菜单长期闲置还分散了开发精力。后来砍掉一批低频率功能系统反而更好用了。2.3 角色权限设计的关键思路权限设计是这类系统的命根子。我们的方案是RBAC基于角色的访问控制但做了一层简化不搞细粒度到按钮级别的权限只控制到菜单和接口粗粒度。具体来说四类角色各有一套可见菜单和可用接口学生端个人信息、每日上报、出入校申请、我的上报记录。 辅导员端班级学生列表、上报情况总览、异常学生处理、请假/出入校审批。 院系管理员院系内各班级数据汇总、异常人员台账、导出报表。 校级管理员全校数据看板、各院系数据对比、基础数据管理学院、专业、班级、用户、系统参数配置。后端在拦截器里校验Token携带的角色信息再判断当前请求路径的权限标识。我们给每个接口定义权限码比如report:student:submit表示学生提交上报在配置类里做路径与权限码的映射实现起来不复杂、维护也方便。值得留意角色与用户的关系建议做成关联表而不是在用户表里写死一个角色字段。因为实际情况中会出现“学生兼任辅导员助理”这类跨角色需求关联表更灵活。我们后来就是因为一开始写死了角色改动时多花了不少时间。3. 数据库设计与关键表结构3.1 核心数据表清单整个系统我梳理下来核心表控制在12张左右就够了。当时团队里有同学一口气设计出30多张表很多表之间关系冗余反而给后续开发添乱。我们用到的核心表如下表名用途说明sys_user用户表统一存放所有人员包含工号/学号、姓名、密码、类型sys_role角色表定义角色类型sys_user_role用户角色关联表支持一人多角色bus_academy学院表学院基础信息bus_major专业表关联学院bus_class班级表关联专业bus_student学生信息表拓展的学生档案关联sys_userbus_report健康上报表每日上报记录核心数据表bus_temperature体温明细表记录多时段体温值bus_leave出入校申请表请假/出入校登记bus_abnormal异常台账表异常人员跟踪记录bus_notice通知公告表系统公告推送sys_user和bus_student拆成两张表一开始有同学不理解觉得学生信息里直接放登录账号不就行了。但实际使用中账号信息密码、状态和档案信息班级、学号、宿舍变更频率完全不同拆开后互不影响做账号启停用的时候也不用连带更新档案。3.2 健康上报表的核心字段设计健康上报表是整个系统的心脏字段设计直接决定后续统计功能的开发成本。我们经过两个版本迭代最终保留的核心字段如下id varchar(36) 主键如有需要不会用数据库自增避免分表扩容困难 student_id varchar(36) 学生档案ID user_id varchar(36) 用户ID方便关联查询 report_date date 上报日期 temperature decimal(4,1) 当日最高体温 health_status tinyint 健康状况1正常 2可疑症状 3确诊/密接等 is_on_campus tinyint 是否在校1在校 0校外 is_vaccinated tinyint 接种状态 location varchar(200) 当前位置 remark varchar(500) 备注 create_time datetime 提交时间 check_status tinyint 审核状态0待审核 1通过 2异常待处理 check_remark varchar(200) 审核备注这里有两个容易被踩坑的点一是report_date和create_time要分开。上报日期是业务时间创建时间是系统时间。有同学图省事只保留create_time结果遇到学生深夜提交凌晨的体温时统计口径就乱了。二是健康状态和审核状态要分开。学生可能填报体温正常但处于隔离期此时系统应该置为“异常待处理”但又不能简单覆盖学生的健康状态字段。独立审核状态位让审核链路更清晰。3.3 索引设计与事务处理心得健康上报表用的最多的是两个查询查某人某天上没上报和按日期查全班/全院的上报记录。针对这两个高频查询我们建了组合索引ALTER TABLE bus_report ADD INDEX idx_student_date (student_id, report_date); ALTER TABLE bus_report ADD INDEX idx_date_class (report_date, class_id);查某人某天是否已上报时走第一个索引命中的记录数最多一两行速度极快。而每天统计上报率时各班级的查询都走第二个索引避免全表扫描。事务处理集中在两个地方一个是提交上报时同时插入主表和明细表需要保证原子性另一个是批量导入学生数据时需要一批全部成功或全部回滚。我们统一在Service层加了Transactional注解同时注意不要把大循环套在事务里——一次导入一万条数据逐条Insert再加上事务数据库压力会非常大。我们后来改成批量插入每500条提交一次整体耗时降了接近一个数量级。注意MyBatis的批量插入有两种方式用foreach标签拼接SQL或者使用ExecutorType.BATCH的批处理模式。前者SQL字符串会很长受max_allowed_packet限制后者是JDBC级别的批处理性能更优。我们实际项目中推荐后者。4. 后端SpringBoot实现要点4.1 统一返回结果与异常处理后端接口设计最重要的一件事是把返回格式统一。我们定义了一个Result 类所有接口返回值都包一层code、message、datapublic class ResultT { private Integer code; // 200成功其他为失败 private String message; // 提示信息 private T data; // 业务数据 }对应地在Controller层不直接捕获业务异常而是抛给全局异常处理器统一处理。我们用RestControllerAdvice做了一层拦截把校验异常、业务异常、系统异常分别映射到不同的业务状态码。这样做的好处是前端拿到响应时只用判断一个code不用为每个接口单独写错误处理逻辑。实际开发中我们遇到过最头疼的返回格式问题是在分页查询上——有同事把total和records拆开放在两个字段里前端每次都要做特殊处理。后来我们把分页结果统一成PageResult 结构包含total、pages、list三个字段全项目只维护这一套新来的开发也能很快上手。4.2 登录鉴权与Token机制登录这块我们没用Spring Security配置学习成本偏高对校内系统有点重而是用拦截器JWT的轻量方案。用户登录成功后后端签发一个Token里面加密存储userId、roleCode、expireTime三个关键信息返回给前端。前端把Token存在localStorage里每次请求在请求头里带上。后端拦截器做两件事先校验Token有效性再通过自定义注解RequirePermission校验权限码。Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); // 1. 校验Token if (!jwtUtil.validateToken(token)) { throw new AuthException(登录已过期请重新登录); } // 2. 解析用户角色与权限 Claims claims jwtUtil.parseToken(token); // 3. 校验当前接口所需权限码 if (handler instanceof HandlerMethod) { HandlerMethod hm (HandlerMethod) handler; RequirePermission annotation hm.getMethodAnnotation(RequirePermission.class); if (annotation ! null) { String[] needCodes annotation.value(); // 对比当前用户拥有权限码集合 if (!permissionService.hasPermissions(claims.get(userId), needCodes)) { throw new AuthException(无权限访问); } } } return true; }这套方案有几个好处实现难度低一个拦截器加一个工具类就够跟业务解耦不下沉到数据层每个接口通过注解声明权限码接口权限一目了然。当时还遇到一个坑Token有效期设太短学生早上登录中午刚提交上报就提示过期体验很差。后来把有效期调到12小时并做了“记住我”的延长逻辑问题解决。这类系统的用户体验优先级其实很高因为每天都要用任何一点卡顿都会被放大。4.3 健康上报接口的防重复设计健康上报有个核心需求每人每天只能报一次。很多初级开发者会直接在Service里加个判断先查表存在就不让提交。但这样在高并发下会出现“同时查不到、同时都插入”的竞态问题。我们当时采用的方案是在数据库层面做约束同时利用唯一索引来兜底给bus_report表加一个(student_id, report_date)的唯一索引然后在Service里显式捕获DuplicateKeyException转成友好的提示返回给前端。Transactional(rollbackFor Exception.class) public void submitReport(ReportSubmitDTO dto) { try { reportMapper.insertReport(dto); } catch (DuplicateKeyException e) { throw new BizException(今天已经提交过上报请勿重复提交); } }这个方案干净利落前端拦截只是提体验数据库约束才是最终防线。后来压测时并发一百个请求同时提交最终也只有一条数据插进去这在生产环境验证过是可靠的。4.4 定时任务与数据统计疫情期间每天要把各学院上报率汇总给校办人工统计太痛苦。我们在后端配置了定时任务每天凌晨一点生成前一天的统计快照存到统计表里。同时提供实时统计接口给管理端大屏用。这里分享一个判断统计功能建议“实时查询定时快照”双轨并行。实时查询用于白天看板更新数据最新定时快照用于生成报表和领导汇报数据固定不会变。如果只做实时查询遇到今天数据量大、查询节点重合时大屏接口和报表导出会互相争抢数据库资源。5. 前端Vue实现要点5.1 路由与菜单动态化前端这边学生端和后台管理共用一套Vue工程通过路由懒加载和动态路由做模块隔离。登录完成后前端根据当前用户角色请求后端接口动态生成可以访问的菜单和路由。动态路由的实现要特别注意刷新页面后路由丢失的问题。我们一开始没做持久化用户一刷新页面就白屏。后来把路由生成的逻辑收敛到一个统一方法里并在路由守卫中检测如果当前路由不在已注册的动态路由列表中就重新拉取用户信息并重新注册路由。5.2 接口封装与状态管理前端所有API请求都走统一的request模块内部基于axios二次封装。封装时做三件事请求拦截器里自动带Token、响应拦截器里统一处理业务错误码并弹出提示、遇到401状态码自动跳转登录页。// 统一请求处理 service.interceptors.request.use(config { config.headers[Authorization] getToken() return config }) service.interceptors.response.use(response { const res response.data if (res.code ! 200) { Message.error(res.message) if (res.code 401) { userStore.resetAndRedirectLogin() } return Promise.reject(new Error(res.message)) } return res.data }, error { // 网络异常统一处理 Message.error(网络异常请稍后重试) return Promise.reject(error) })状态管理层我们用了当前主流的Pinia。全局只维护三个store用户信息、上报状态、应用配置。不把每个页面数据都放store里避免状态满天飞、调试困难的局面。表格数据尽量在页面组件内部维护只有在多个页面之间共享的数据才提升到store。5.3 报表可视化与导出功能管理端的数据看板用ECharts做图表展示覆盖上报率趋势、各学院异常统计、体温分布直方图几个核心场景。这里有一个实际经验不要把图表初始化逻辑直接写在页面里抽成一个ChartCard组件传入配置项统一管理图表的加载、销毁和自适应。导出功能当时也踩了坑。后端直接用POI导出Excel前端通过Blob方式下载文件。最开始遇到的问题是文件流没有附带响应头导致前端无法从response header里拿到文件名。后来前后端约定文件名编码放在Content-Disposition里前端做一层解析问题解决。6. 联调、部署与问题排查6.1 前后端联调踩坑记录联调阶段最常见的问题集中在跨域、参数格式、时间格式三块跨域我们采用后端配置跨域过滤器的方式解决前端Nginx做反向代理时再配一层双保险。参数格式方面前端传后端的时间字段一开始是带T的ISO字符串后端LocalDateTime默认解析不了。统一在全局做了Jackson配置指定时间格式为yyyy-MM-dd HH:mm:ss问题消除。还有一次很典型的联调问题前端页面显示列表数据正常但导出Excel时始终报错。排查半天发现是接口返回的Excel文件流被axios默认当成JSON解析了。解决方式是在response拦截器里判断响应类型如果是Blob直接返回。6.2 部署上线要点部署方案可以灵活但一定要保证发布过程可回滚。我们最终的方案是Java后端打jar包用systemd服务管理每次发布前备份旧包发布新包后校验启动日志里没有报错再切换流量。Vue前端打包成dist目录放到Nginx的站点目录下同样是先备份再替换。数据库变更单独准备SQL脚本每次升级按版本号顺序执行不直接手工改表。提示校内系统经常出现“功能改了但用户说没变”的情况多半是浏览器缓存。前端发布时让Nginx对静态资源设置带hash的文件名并关闭入口index.html的缓存能大幅减少这类反馈。6.3 常见错误与解决方案速查表现象可能原因解决方案学生提交上报提示重复前端重复点击提交按钮加loading禁用态后端唯一索引兜底页面列表加载慢单表数据量大缺少索引检查高频查询条件补充组合索引批量导入学生数据超时单条插入循环改为JDBC批量插入模式看板数字和报表对不上实时数据和快照差异明确两套数据的使用场景不混用某个接口偶尔报500日志定位为空指针检查多表关联查询中关联字段是否有null导出文件乱码文件编码不一致统一导出文件为UTF-8并带BOM头整条链路做下来我最大的体会是这种系统真正花时间打磨的地方不在技术难点而在于把每个角色每天要用到的操作做到极致顺手。上报快、查询快、导出快、权限不出错、数据不丢这五件事做好了系统就成功了八成。后续做类似管理系统时这套SpringBootVueMyBatisMySQL的组合方案可以低成本复用只需在业务表和权限模型上做调整即可开发周期能压缩到相当短的程度。