先说句实在话,看到“疫情防控管理系统”这个标题,很多人的第一反应是“这玩意儿还有人在做?”。但真去扒一遍需求你会发现,这个领域远比表面上复杂。它不是简单的增删改查,而是典型的多角色、多流程、数据实时联动的业务系统,涉及用户端、管理端、审核端、统计端四个层面的交互。用SpringBoot+Vue+MyBatis+MySQL这套组合来落地,在国内的技术环境下确实是性价比最高的选择——生态成熟、招人好招、部署文档遍地都是,出了问题Stack Overflow一搜全有答案。
我这篇文章不打算写那种“从零开始敲代码”的教程,那没意义,系统源码又不是只有一份逻辑。我要拆的是这个项目背后真正的设计思路、技术选型的理由、核心模块的实现方式,以及那些代码里看不出来的坑。不管你是刚入行的应届生想拿个完整项目练手,还是公司内部要快速搭一套类似的公共卫生应急管理流程,这篇文章都能给你一个比较完整的参照系。
1. 内容整体设计与思路拆解
1.1 核心需求解析:防疫系统到底在管什么
很多人把这类系统想简单了,以为就是“登记一下体温”完事。实际业务场景拆开了看,至少要覆盖五个核心维度:
- 人员管理:常住人口、流动人口、重点观察人群的分类管控,每一种人群的权限和可见数据都不一样。
- 健康监测:每日体温上报、症状打卡、行程轨迹记录,这些数据要能按时间轴回溯。
- 核酸管理:检测记录登记、检测结果同步、未检人员提醒、周期统计。
- 隔离管理:隔离人员信息、隔离起止时间、隔离状态流转(待隔离/隔离中/已解除)。
- 物资调度:防护物资库存、出入库记录、申领审批流程。
这五个维度不是孤立的,它们通过人员ID这条主线串联在一起。一个重点观察人员从“健康打卡异常”到“核酸复核”再到“转入隔离”再到“解除隔离”,是一条完整的状态机流转链路。系统设计最核心的一点,就是要把这条链路的状态流转设计清楚,而不是把每个模块做成信息孤岛。
1.2 方案选型:为什么是这套技术栈
选SpringBoot+Vue这套组合,说白了就是三个字:稳、快、省。
后端SpringBoot的优势不在技术多新,在于它把Spring生态那些繁琐的XML配置全部干掉,用自动配置和Starter机制让你五分钟拉起一个Web服务。配合Maven或Gradle做依赖管理,项目结构清晰,部署产物就是一个可执行的Jar包,扔到服务器上java -jar就完事。
前端Vue目前主流版本是Vue 3.x,配合Element Plus组件库,后台管理界面的开发效率极高。表格、表单、弹窗、分页这些高频组件全部现成,项目里百分之六十的页面都是“表格+搜索条件+弹窗表单”的组合模式,用Element Plus真的可以做到半天出一个模块。
MyBatis是另一个争议点。有人觉得JPA更省事,但国内绝大多数老项目都是MyBatis系,你拿到的这套源码大概率也是MyBatis-Plus——它比原生MyBatis多了个MyBatis-Plus,内置了单表CRUD的通用方法,连SQL都不用写,只管写Mapper接口继承BaseMapper<T>就行。复杂查询再手写XML,灵活性和效率平衡得非常舒服。
MySQL作为存储层就不多吹了,关系型数据库里最普及的选择,5.7和8.0两个版本都稳如老狗。这套系统的事务场景不少(比如物资出库要同时扣库存和加流水),MySQL的InnoDB引擎事务支持完全够用。
注意:如果你拿到的源码是SpringBoot 2.x版本,建议直接用JDK 8跑,千万别图新鲜上JDK 17,不然一堆依赖兼容问题能折腾你一下午。SpringBoot 3.x则需要JDK 17及以上,这个要看项目里
pom.xml的父版本号说话。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心功能模块与数据库设计解析
2.1 用户角色权限设计:不止是“管理员”和“普通用户”
这套系统的权限设计比一般的后台管理系统要细得多,直接照搬RABC(基于角色的访问控制)三张表模型虽然也能跑,但实际业务中你会发现有些特殊场景需要更灵活的处理。
角色至少要拆成四类:
- 系统管理员:管人、管角色、管配置,拥有全部权限。
- 社区/单位管理员:管理自己辖区内的数据,只能看本级及下级的数据。
- 医护人员/检测人员:登记核酸记录、录入隔离信息、更新人员状态。
- 普通用户:每日打卡、查看自己的记录。
数据库层面就是经典的五张表:user(用户表)、role(角色表)、permission(权限表)、user_role(用户角色关联表)、role_permission(角色权限关联表)。
但这里有个细节很多入门项目容易忽略——数据权限。比如一个社区管理员登录后,他只能看到自己社区的居民数据,而不是全量数据。这类数据权限光靠RBAC的菜单权限控制不了,需要在业务SQL里面加一个community_id的过滤条件,在Service层做数据隔离。市面上做SaaS系统的框架会用MyBatis-Plus的DataPermissionInterceptor做自动拦截,有这个配置的源码质量会高一个档次,拿到手可以先找找看。
2.2 核心表结构设计:关键字段一个都不能少
我大概梳理了这套系统的主表结构,字段设计上有几个地方特别值得参考:
用户信息表(user)
id、username、password(BCrypt加密存储,不允许明文)real_name、id_card、phone、address、community_idhealth_status(健康状态:正常/异常)is_deleted(逻辑删除标记,这个字段一定要有,人员数据不能物理删除)create_time、update_time
健康打卡表(health_check)
id、user_id、temperature、symptom(症状描述)check_date(打卡日期,和user_id做唯一索引,防止同一天重复提交)location(打卡地点,记录定位信息)remark
核酸记录表(nucleic_acid_record)
id、user_id、test_date、test_result(阴性/阳性)、test_org(检测机构)report_time、report_url(结果报告文件路径)
隔离管理表(isolation_record)
id、user_id、start_date、end_date、isolation_type(居家/集中)status(隔离状态)location、reason(隔离原因)release_time(解除时间)
物资管理表(supplies_stock / supplies_apply)
- 库存表:
supply_name、category、stock_count、unit、safety_stock(安全库存阈值) - 申领表:
apply_user_id、supply_id、apply_count、status(待审批/已通过/已驳回)、apply_time、approve_time
光是看这几个表的字段,你就能感觉到这套系统的业务深度了。“安全库存阈值”这个字段特别实用,库存低于阈值自动提醒补货;“隔离状态流转”保证了从隔离开始到解除的完整生命周期记录。
2.3 数据库索引与性能优化策略
数据量不大的时候索引随便建都行,但防疫系统的打卡数据是天天涨的,一年下来一个中等城市的数据量轻松上百万。索引策略就变得很重要了:
- 健康打卡表的联合索引必须建:
(user_id, check_date),这是查询频率最高的组合。 - 核酸记录表的单列索引:
(user_id)、(test_date),如果要做全城核酸统计,后者的区分度很高,索引收益明显。 - 物资申领表的联合索引:
(status, apply_time),后台审批列表基本按这两个字段筛。
提醒:MySQL 8.0以上建议直接给时间字段用
DATETIME(3)类型保留毫秒精度,统一用数据库服务器的当前时间(DEFAULT CURRENT_TIMESTAMP)写入,避免因为部署服务器时区不一致导致的时间错乱。这个坑我踩过不止一次,前端页面显示的时间和数据库实际写入时间差了8小时,查了半天最后发现是连接串里没加serverTimezone=Asia/Shanghai。
3. 前后端核心实现与实操过程
3.1 后端工程结构与分层规范
这套源码的后端目录结构基本是行业标准模板,拿到手先看结构,心里就有数了:
code复制src/main/java/com/example/epidemic/
├── config/ // 配置类:跨域、拦截器、MyBatis-Plus分页插件
├── controller/ // 控制层:接收参数、调用service、返回Result
├── service/ // 业务层:接口+实现类,业务逻辑在这一层
├── mapper/ // 持久层:接口,继承BaseMapper
├── entity/ // 实体类:对应数据库表
├── vo/ // 视图对象:给前端传参用的DTO
├── common/ // 公共类:统一返回结果、异常处理、工具类
└── EpidemicApplication.java // 启动类
最值得点赞的是它有**统一返回结果Result**类,所有接口都返回{ code: 200, message: "success", data: ... }这种格式,前端处理起来非常统一。有些垃圾项目每个接口返回格式都不一样,前端拿数据还要分情况判断,别提多难受。
还有全局异常处理器,用@RestControllerAdvice注解统一捕获业务异常和系统异常,不会出现报错直接把堆栈信息甩给前端的情况。代码里一般会定义BizException运行时异常,业务校验不通过就throw new BizException("xxx不能为空"),配合全局异常处理直接返回友好提示,这套机制值得所有后台项目借鉴。
3.2 核心接口逻辑:健康打卡与审批流转
挑两个核心接口看一下实现思路。
健康打卡接口(POST /api/healthCheck/submit)的关键代码逻辑:
java复制@Override
public Result submit(HealthCheckDTO dto, Long userId) {
// 1.校验当天是否已打卡
LambdaQueryWrapper<HealthCheck> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(HealthCheck::getUserId, userId)
.eq(HealthCheck::getCheckDate, LocalDate.now());
Long count = this.baseMapper.selectCount(wrapper);
if (count > 0) {
throw new BizException("今日已打卡,请勿重复提交");
}
// 2.组装数据并判断健康状况
HealthCheck entity = new HealthCheck();
BeanUtils.copyProperties(dto, entity);
entity.setUserId(userId);
entity.setCheckDate(LocalDate.now());
// 体温大于等于37.3就标记为异常
if (dto.getTemperature() != null && dto.getTemperature() >= 37.3) {
entity.setHealthStatus(1);
// 同时更新用户主表状态
userService.updateHealthStatus(userId, 1);
}
this.save(entity);
return Result.success();
}
代码不多,但把业务规则落地得很明确:重复提交的幂等校验、体温异常的联动更新,逻辑清晰不拖泥带水。很多做得差的系统,打卡和用户状态是两个独立模块,数据对不上,查问题能查疯。
物资申领审批接口(POST /api/supply/approve)的逻辑就体现了事务的用法:
java复制@Transactional(rollbackFor = Exception.class)
public Result approveApply(Long applyId, Integer status) {
// 1.校验申请表是否存在
SuppliesApply apply = this.getById(applyId);
if (apply == null) {
throw new BizException("申请记录不存在");
}
// 2.修改状态
apply.setStatus(status);
this.updateById(apply);
// 3.如果审批通过,扣减库存
if (status == 1) {
SuppliesStock stock = stockMapper.selectById(apply.getSupplyId());
if (stock.getStockCount() < apply.getApplyCount()) {
throw new BizException("库存不足,无法通过审批");
}
stock.setStockCount(stock.getStockCount() - apply.getApplyCount());
// 可在此处加入库存变更流水记录
stockMapper.updateById(stock);
}
return Result.success();
}
@Transactional保证“更新审批状态”和“扣减库存”这两个操作要么都成功,要么都回滚,绝对不会出现审批通过了但库存没减的脏数据。库存不足的判断放在事务里,并发情况下也能基本保证数据一致性。
3.3 前端Vue3核心实现与组件封装
前端部分,Vue3的组合式API已经是标配。项目的目录结构大概是:
code复制src/
├── api/ // 接口请求封装
├── router/ // 路由配置
├── store/ // Pinia状态管理
├── views/ // 页面组件
├── components/ // 公共组件
├── utils/ // 工具函数(request.js axios封装)
└── App.vue
axios请求封装这个环节,核心在于请求拦截器统一携带Token,响应拦截器统一处理错误码和HTTP状态异常。
javascript复制// utils/request.js
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
timeout: 10000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
})
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message)
if (res.code === 401) {
router.push('/login')
}
return Promise.reject(res)
}
return res
},
error => {
ElMessage.error(error.message || '网络异常,请稍后重试')
return Promise.reject(error)
}
)
这个封装做完之后,前端业务代码里请求就变成了:
javascript复制// 登录接口
const submitForm = () => {
login(form.value).then(res => {
localStorage.setItem('token', res.data.token)
router.push('/dashboard')
})
}
权限控制这块,Vue Router提供了beforeEach路由守卫,可以在路由跳转之前判断用户角色,不满足条件的直接拦下来。动态路由控制(根据用户角色动态添加路由表)在这个项目里算高级功能,但基础版的addRoute方案也够用。
角色配置为“系统管理员”时全部菜单可见,社区管理员只见自己和下属社区的功能模块。这些配置在后端返回的菜单树里提前过滤好,前端只管渲染,不越权。
3.4 前端高频页面:表格+弹窗+表单三板斧
这类管理系统百分之七十的页面都可以用“搜索条件区 + 表格区 + 分页条 + 弹窗表单”这套模式快速搭建。以核酸记录管理页为例:
vue复制<template>
<div class="container">
<!-- 搜索条件 -->
<el-form :inline="true">
<el-form-item label="姓名">
<el-input v-model="queryParams.realName" placeholder="请输入姓名" clearable />
</el-form-item>
<el-form-item label="检测结果">
<el-select v-model="queryParams.testResult" clearable placeholder="请选择">
<el-option label="阴性" value="阴性" />
<el-option label="阳性" value="阳性" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleQuery">搜索</el-button>
<el-button @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 表格 -->
<el-table :data="tableData" border>
<el-table-column prop="realName" label="姓名" />
<el-table-column prop="idCard" label="身份证号" />
<el-table-column prop="testDate" label="检测日期" />
<el-table-column prop="testResult" label="结果">
<template #default="{ row }">
<el-tag :type="row.testResult === '阳性' ? 'danger' : 'success'">
{{ row.testResult }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button link type="primary" @click="handleView(row)">详情</el-button>
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<el-pagination
v-model:current-page="queryParams.pageNum"
v-model:page-size="queryParams.pageSize"
:total="total"
layout="total, prev, pager, next, sizes"
@change="getList"
/>
</div>
</template>
3.4.1 数据可视化:ECharts统计报表模块
报表统计模块是这个系统少有的亮色。用ECharts基于打卡数据渲染折线图(趋势变化)、饼图(各社区完成率占比)、柱状图(物资消耗对比),后端提供一个组装好的统计接口,返回结构如下:
json复制{
"dates": ["2025-04-01", "2025-04-02"],
"normalCount": [998, 1001],
"abnormalCount": [2, 1]
}
前端直接chart.setOption渲染,不需要额外处理,做起来很简单。
注意:ECharts按需引入模块可以大幅压缩打包体积,项目里只引了
LineChart、PieChart、BarChart三个核心图表组件就够了,别图省事直接全量引入,打包出来文件大一圈加载还慢。
4. 环境搭建与部署实操
4.1 本地开发环境准备
拿到源码第一件事,先把环境对齐,不然根本跑不起来。这套系统需要的东西:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8(SpringBoot 2.x) | 别用太新版本,稳字第一 |
| Maven | 3.6+ | 依赖管理必备 |
| Node.js | 16.x 或 18.x LTS | 太新版本对老前端项目不友好 |
| MySQL | 5.7 或 8.0 | 8.0建议用mysql-connector-java 8.0+驱动 |
| Redis | 有就装,没有也行 | 主要用于缓存验证码和Token,看项目是否引用 |
| IDE | IntelliJ IDEA + VSCode | 后端IDEA,前端VSCode,各干各的 |
数据库脚本一般放在项目根目录的sql/文件夹里,里面有建库、建表、初始数据的SQL文件,直接用Navicat或命令行导入就行。特别注意:先看脚本里面有没有默认账号,一般都会预置一个admin/admin123之类的管理员账号,方便首次登录。
4.2 后端启动三步走
第一步,修改application.yml里的数据库连接:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/epidemic_system?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
第二步,Maven打包或直接在IDEA里启动EpidemicApplication主类。
第三步,看控制台日志,出现Tomcat started on port(s): 8080就说明起来了。
4.3 前端启动步骤
bash复制# 安装依赖
npm install
# 如果项目是Vue2用npm run serve;Vue3项目通常用vite,命令是npm run dev
npm run serve
# 或者
npm run dev
启动成功后访问http://localhost:5173(Vite默认端口),看到登录页就说明前后端连上了。
注意:前端启动最常见的坑就是跨域报错。开发环境下,前端端口是5173,后端是8080,浏览器会拦截跨域请求。解决办法有两种:后端
WebMvcConfigurer里配置addCorsMappings允许跨域,或者前端在vite.config.js里配置proxy代理转发。推荐后者,原因是上线后前后端部署在同域下,不带跨域头更安全。
4.4 生产环境部署方案
生产环境我强烈推荐最朴素但也最稳定的方案:
| 组件 | 部署方式 | 说明 |
|---|---|---|
| 后端 | Docker容器或Systemd托管Jar包 | 二选一,看运维习惯 |
| 前端 | Nginx托管静态文件 | 打包后dist目录,配反向代理 |
| MySQL | 独立服务器 | 别和Web应用抢资源 |
| Redis | 独立或和MySQL一起 | 看项目实际是否引入 |
前端打包:
bash复制npm run build
# 生成dist目录,把dist里的文件拷贝到Nginx的html目录下
Nginx配置示例:
nginx复制server {
listen 80;
server_name your-domain.com;
root /var/www/dist;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location / {
try_files $uri $uri/ /index.html;
}
}
这个配置解决两个关键问题:静态资源直接走Nginx文件系统不经过Java应用,性能好很多;/api/开头的请求反向代理到后端服务,前端请求地址写/api/xxx就行,不用关心后端到底在哪台机器上。try_files配置了前端路由的history模式回退,刷新页面不会出现404。
部署完之后还有一个容易忽略的点:MySQL连接池优化。application.yml里DataSource的HikariCP连接池配置,可以根据实际并发调一下maximum-pool-size,一般默认10在你本地够用,但生产上来了几十个用户并发操作,10个池子可能会排队,建议调整到20~50之间,同时把connection-timeout配成30000毫秒避峰。
5. 常见问题与排查技巧实录
5.1 前端启动后页面白屏,控制台全是报错
八成是后端没起来,或者后端接口地址没对上。先在后端控制台看有没有正常启动日志,再看前端request.js里的baseURL是不是写对了。如果后端确认没问题,再打开浏览器F12看网络请求,看看是404还是500,404就是路径不对,500就是后端代码执行出错,把后端日志拉出来看具体异常堆栈。
5.2 数据库连接报错 Access denied for user
这个常见得不能再常见。检查三样东西:用户名密码是否写错、该用户是否有远程访问权限(MySQL默认root只能localhost登录)、连接串里的useSSL=false是否配置。开发环境建议直接用root,图省事;生产环境单独建个账号,授权最小权限更安全。
5.3 登录后Token失效,接口一直401
JWT(JSON Web Token)的坑主要在密钥和过期时间。先查配置文件里的jwt.secret是否一致(前后端拿到的是不是同一个签名密钥),过期时间是不是太短(开发阶段建议把过期时间设长点,比如24小时,不然一小时崩一次心态爆炸)。再检查一下系统时间和服务器时间差,JWT的exp过期校验用的是当前时间,时间不准会导致Token“提前过期”。
5.4 MyBatis查询结果字段全是null
这个坑老手也经常踩。核心原因是数据库下划线字段(create_time)和Java实体类驼峰属性(createTime)映射不上。
解决办法在application.yml里开启下划线转驼峰:
yaml复制mybatis-plus:
configuration:
map-underscore-to-camel-case: true
如果用的是纯MyBatis不是Plus版本,还要额外加@MapperScan注解扫描Mapper接口,不然启动就报找不到Bean。
5.5 Vue前端编译报错 This dependency was not found
换个依赖版本或者删掉整个node_modules目录,重新执行npm install。这类问题绝大多数是依赖版本不匹配导致,前端生态这两年更新太快,老项目的锁定版本在npm源里可能已经被替换。npm install完还是报错的话,试试npm install --legacy-peer-deps,ESLint(代码检查工具)新版对插件版本要求很严,很多人就是卡在这里。
5.6 系统卡顿,接口响应慢
先看数据库慢查询日志,再看后端接口耗时,最后看是不是全表扫描。大概率是前面说的索引没建好,或者关联查询没走索引。还有可能是前端getList方法在页面初始化时被重复调用,一次性查出全量数据,数据量一大就卡。再就是初始化时给pageSize设一个相对保守的值比如10,给用户后续自己切换的余地。
6. 实操心得与经验总结
说了这么多,最后把个人实操中最重要的几个体会分享出来。
第一个是不要在源码上直接改需求。拿到任何一套源码,我的习惯是先花半天把所有代码读一遍,画出角色权限矩阵和业务流程时序图,完全搞清楚状态机和数据流转后再动手改,避免越改越乱。尤其是这种带有状态流转的系统,隔离记录从“待观察”到“已解除”中间到底经过哪些状态、谁有权限变更状态、状态变更后哪些关联数据要同步更新,这些搞不清就去写需求,后面一定会翻车。
第二个是学习这套源码,上手最重要的地方不是CRUD写法,而是业务抽象能力。你仔细读Service层会发现,它把重复的业务逻辑抽得非常干净,比如“查询当前用户的数据权限范围”封装成一个工具类,所有Service都复用。这种抽象能力是两三年经验的坎,跨过去了,看任何业务系统都不再复杂。
第三个是项目跑通了之后,一定要亲手做一次数据迁移演练。把MySQL里的数据用mysqldump导出再导入另一个库,验证一下生产环境的备份恢复流程。就算自动化部署再顺畅,数据丢了就是事故,这件事不能省。
这套系统的意义不只是“又写了一个后台管理”,它的完整度完全可以当作毕业设计、求职项目,或者作为一个公共应急管理系统的底座模板来复用。把里面的业务模块改成社区团购、党建管理、企业内部行政系统,核心架构基本都不用大动。
一个小建议:真正吃透这套系统之后,可以尝试把其中一个模块升级成规则引擎思路——用配置化的方式控制健康异常等级和处置流程,脱离硬编码,比如库存阈值、异常温度阈值全部放到配置表里,这样换一个城市对接,不用改代码就能适应全新的规则。这是从“会用”到“会设计”的关键一步,值得花时间研究。
