疫情管理系统是个很典型的SpringBootVue全栈练手项目也是后台管理类系统的集大成者——单表CRUD、多表关联、文件上传、权限区分、数据可视化、定时任务一个项目能把你日常开发会用到的技术点串起一大半。这套基于SpringBootVueMyBatisMySQL搭建的疫情管理系统属于比较完整且可以直接跑起来的版本源码结构清晰拿来当项目经验补充、毕业设计参考或者自己改写扩展都很合适。这篇就把系统的完整设计思路、数据库表结构、前后端核心代码逻辑、踩坑记录和排查经验全部拆开讲一遍从环境准备到最终能独立部署尽量写成一套可以照着上手的完整闭环。1. 项目概述与整体设计思路1.1 疫情管理系统到底解决了什么问题先说业务侧的需求。学校、园区、社区这类人员密集型场景日常需要收集每个人的健康状态、体温数据、行程轨迹、健康码颜色等信息。早期靠微信接龙、Excel表格收集效率低而且不好回溯。管理员要花大量时间做汇总统计有异常记录时排查起来更是头疼。这个系统的核心价值就是把“上报-汇总-审核-通知”这条链路完整搬到线上。系统按角色划分为两类使用人群权限边界清晰普通用户注册登录后每日提交健康打卡填写体温、是否接触过确诊人员、当前所在地、健康码颜色等关键信息。管理员登录后台查看全员打卡记录按日期、班级或部门、健康状态筛选数据具备发布公告、维护疫情信息、标记异常记录的操作权限。整个系统不仅解决了数据收集的效率问题还通过统一的数据存储和可视化的统计展示让管理者能从全局掌握人员健康态势算是这类管理系统的标准功能骨架。1.2 技术栈选型为什么是这四件套SpringBoot省掉了大量繁琐的XML配置内嵌Tomcat项目打包成jar就能直接运行。起步依赖和自动配置让团队从项目初始化到第一个接口上线时间控制在分钟级别。Vue前端采用渐进式框架配合Element UI组件库开发效率极高。数据的双向绑定省掉了手动操作DOM的麻烦组件化机制让页面拆分成独立模块维护起来相当顺手。MyBatisSQL由开发者完全掌控业务涉及多表关联和动态条件查询时MyBatis写起来非常灵活。尤其像疫情管理这种政策调整频繁的业务场景字段和查询条件经常要改动半自动化的MyBatis只需要改SQL和映射关系就行不用重编译Java代码。MySQL开源免费、稳定可靠在中小型管理系统领域占有率极高配合Navicat、DataGrip等工具做数据管理非常顺手。从团队协作和人才储备角度看这套技术栈的生态极其成熟遇到的问题几乎都能找到现成案例。对新手来说这是一条友好度很高的技术路线成功率和参考价值都高于冷门技术组合。2. 核心功能模块与数据库设计2.1 功能模块拆解结合实际业务场景系统划分为六大功能模块每个模块承担独立职责登录认证模块负责用户注册、密码加密存储、登录状态校验和Token签发。方案用的是JWT令牌登录成功后后端签发Token前端存到localStorage每次请求带上后端通过拦截器统一校验。用户管理模块管理用户账号信息、角色权限、所属班级或部门。管理员可对用户进行增删改查和状态变更。健康打卡模块整个系统业务逻辑最核心的部分。每个用户每天只能提交一条打卡记录提交内容包括体温数值、是否接触确诊人员、当前所在地、健康码颜色等。当天已打卡则拦截重复提交。疫情信息管理模块管理员维护病例信息、风险区域、防控政策等数据支持增删改查系统首页会展示关键统计指标。公告管理模块管理员发布、编辑、下线公告内容。普通用户登录后在信息看板查看最新公告。数据统计模块基于ECharts实现可视化图表展示每日打卡完成率、体温异常人数趋势、健康状态分布等。2.2 数据库表结构设计数据库名采用health_system核心表共八张这里挑重点讲解sys_user用户表。字段包括id、username、password、real_name、role0普通用户1管理员、dept_id关联部门或班级、status账号状态、create_time。密码字段存的是BCrypt加密后的哈希值不存明文。health_record健康打卡记录表。字段包括id、user_id、temperature、is_contact是否接触确诊人员、current_location、health_code_color健康码颜色、remark备注、create_date。针对每日一条的业务约束我建了user_id和create_date的联合唯一索引在数据库层面兜底防重。epidemic_info疫情信息表。字段包括id、case_name、location、type确诊/疑似/密接、status、publish_date。notice公告表。字段包括id、title、content、publisher_id、create_time、status上架/下架。dept部门或班级表。字段包括id、dept_name、parent_id。用于用户维度的组织管理。dict_type和dict_data数据字典相关表用于维护性别、证件类型、健康状态等可扩展的枚举字段。sys_login_log登录日志表。记录用户登录时间和IP便于安全审计。设计表结构时有几个经验值得说明。日期字段一定要用DATE或DATETIME类型不要图省事存字符串。虽然字符串也能存日期但做范围查询、排序、聚合统计时原生日期类型无论在性能上还是函数支持上都明显占优。所有表统一加上create_time和update_time字段后续排查数据问题时能少很多麻烦。所有表统一使用InnoDB引擎字符集utf8mb4排序规则utf8mb4_general_ci。3. 环境准备与项目初始化3.1 后端环境准备后端开发环境建议如下JDK1.8或更高版本。推荐JDK 8运行稳定与大多数依赖版本兼容性最好。Maven3.6以上用于依赖管理和项目构建。MySQL5.7或8.0版本。IDE推荐IDEA社区版或旗舰版均可。创建SpringBoot项目在pom.xml引入核心依赖。这里直接列出关键的几个dependencies !-- Web -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis 整合 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.0/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- JWT -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency !-- Lombok -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies数据库连接配置文件application.yml有几个关键配置项需要注意server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/health_system?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的数据库密码 servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.healthsystem.entity configuration: map-underscore-to-camel-case: true logging: level: com.healthsystem.mapper: debug解释几个容易踩坑的点。useSSLfalse不加的话高版本MySQL连接时经常报SSL警告虽然能跑但看着烦。serverTimezoneAsia/Shanghai必须配否则系统时间和数据库时间会出现8小时的时差。allowPublicKeyRetrievaltrue是MySQL 8.0配合非SSL连接时的必要设置。map-underscore-to-camel-casetrue开启后数据库的user_name字段可以自动映射到实体类的userName属性省掉大量resultMap配置。3.2 前端环境准备前端基于Vue 2 Element UI构建开发环境需要Node.js 14以上版本建议使用npm或yarn管理依赖。# 安装Vue CLI npm install -g vue/cli # 创建项目选择vue2模板 vue create health-system-front # 进入项目 cd health-system-front # 安装核心依赖 npm install vue-router3 axios element-ui echarts --save需要说明的是Vue 2和Vue 3的生态差异比较大这个项目采用的是Vue 2配套的Element UI版本是2.x。如果你个人更偏好Vue 3也可以使用Element Plus但组件API有一些差异到时候照着项目源码比对调整即可。前端项目的核心目录结构设计如下src/ ├── api/ # 接口封装统一管理 │ ├── login.js # 登录相关接口 │ ├── health.js # 打卡相关接口 │ └── admin.js # 管理端接口 ├── router/ # 路由配置 │ └── index.js # 路由表与权限守卫 ├── store/ # Vuex状态管理 │ └── modules/ │ └── user.js # 用户状态与Token ├── views/ # 页面组件 │ ├── Login.vue # 登录页 │ ├── Dashboard.vue # 首页看板 │ ├── HealthCheck.vue # 健康打卡页 │ └── admin/ # 管理后台页面 └── utils/ └── request.js # Axios封装拦截器接口单独抽一层页面里不直接写请求这是后期维护的核心保障。改一个接口地址只需要变动一个文件全局生效。4. 核心代码实现与细节解析4.1 后端基于三层架构的落地实现后端按照Controller、Service、Mapper三层标准结构组织职责清晰。这里拿登录流程和健康打卡这两个核心业务做详细拆解。登录流程完整逻辑如下Controller层接收前端请求进行基础参数校验将请求交给Service层。RestController RequestMapping(/auth) public class AuthController { Resource private AuthService authService; PostMapping(/login) public Result login(RequestBody Valid LoginDTO dto) { return authService.login(dto); } PostMapping(/register) public Result register(RequestBody Valid RegisterDTO dto) { return authService.register(dto); } }Service层负责核心业务逻辑根据用户名查询用户比对BCrypt加密密码验证通过后生成JWT Token。Service public class AuthServiceImpl implements AuthService { Resource private UserMapper userMapper; Resource private JwtUtil jwtUtil; Override public Result login(LoginDTO dto) { User user userMapper.selectByUsername(dto.getUsername()); if (user null) { return Result.error(用户不存在); } if (user.getStatus() 0) { return Result.error(账号已被禁用); } if (!BCrypt.checkpw(dto.getPassword(), user.getPassword())) { return Result.error(用户名或密码错误); } String token jwtUtil.generateToken(user.getId(), user.getRole()); MapString, Object data new HashMap(); data.put(token, token); data.put(userInfo, user); return Result.success(data); } }这里有个关键安全细节实体类里不要默认查询密码字段。常规的selectByUsername会把整行数据查出来但密码只在登录入口需要其他场景一概不返回。所以我在Mapper里写了两个查询方法一个是登录专用的selectByUsernameWithPassword一个是通用查询的selectByUsername后者在SQL层面就直接排除password字段。密码加密存储使用BCrypt算法加盐哈希即使是相同的明文密码每次生成的哈希值也完全不同能有效抵御彩虹表攻击。注册用户时调用BCrypt.hashpw(rawPassword, BCrypt.gensalt())完成加密。4.2 打卡业务的幂等处理健康打卡是整个系统业务逻辑的核心也是最容易出bug的地方。接口设计为每天每人只能提交一条记录。如果只在Controller层简单判断一次在并发场景下两个请求同时通过校验就会插入两条数据破坏业务约束。为了解决这个问题我做了三道保障第一道前端按钮加loading状态提交后立即禁用防止用户重复点击。第二道后端Service层在事务内先查再插判断user_id和create_date是否已有记录。Override Transactional(rollbackFor Exception.class) public Result submitHealthCheck(HealthRecordDTO dto, Long userId) { LocalDate today LocalDate.now(); HealthRecord existRecord healthRecordMapper.selectByUserIdAndDate(userId, today); if (existRecord ! null) { return Result.error(今日已打卡请勿重复提交); } HealthRecord record new HealthRecord(); BeanUtils.copyProperties(dto, record); record.setUserId(userId); record.setCreateDate(today); healthRecordMapper.insert(record); return Result.success(打卡成功); }第三道数据库层面加联合唯一索引。即使前两道都被绕过数据库也会在插入时直接报唯一约束异常保证数据绝对不会重复。这三道保障意味着什么就是你在前端狂点、用脚本刷接口、甚至两个请求同时打过来最终数据库里也只会有一条记录。这种多端联动的防重设计是这类业务的基本功。4.3 MyBatis的XML映射细节MyBatis在本项目中使用XML方式为主因为业务涉及动态SQL和复杂多表关联XML的可读性和维护性明显好于注解方式。以健康打卡记录的统计查询为例select idselectDailyReport resultTypemap SELECT DATE_FORMAT(create_date, %Y-%m-%d) AS date, COUNT(*) AS total_count, SUM(CASE WHEN temperature lt; 37.3 THEN 1 ELSE 0 END) AS normal_count, SUM(CASE WHEN temperature gt; 37.3 THEN 1 ELSE 0 END) AS abnormal_count FROM health_record WHERE create_date BETWEEN #{startDate} AND #{endDate} GROUP BY DATE_FORMAT(create_date, %Y-%m-%d) ORDER BY date /select注意XML里小于号需要用转义否则XML解析直接报错。这个细节新手很容易踩坑。关联查用户信息时用resultMap完成字段映射resultMap idrecordWithUserMap typecom.healthsystem.entity.HealthRecord id propertyid columnid/ result propertyuserId columnuser_id/ result propertytemperature columntemperature/ association propertyuser javaTypecom.healthsystem.entity.User id propertyid columnuid/ result propertyrealName columnreal_name/ result propertydeptName columndept_name/ /association /resultMapMapper接口与XML文件怎么对应关键在于Mapper接口的完全限定名必须与XML的namespace一致方法名必须与XML中select或insert的id一致。比如com.healthsystem.mapper.HealthRecordMapper对应mapper/HealthRecordMapper.xml两者名字一一对应项目启动时MyBatis才能正确绑定。4.4 前端核心实现思路前端的技术重点是三块axios请求封装、路由权限控制、页面组件构建。axios统一封装在utils/request.js核心逻辑就是请求拦截和响应拦截import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带Token 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) { Message.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error(error.message || 网络错误) return Promise.reject(error) } ) export default service这段代码里最关键的是401统一处理。Token过期或非法时后端返回401前端直接跳转登录页并清空本地Token。这样做可以避免用户在一个失效会话里继续操作不断请求后端、报错、清理这种体验极差的情况。路由权限控制使用了Vue Router的全局前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (token) { if (to.path /login) { next(/) } else if (to.meta.role to.meta.role ! role) { next(/403) } else { next() } } else { if (to.path /login) { next() } else { next(/login) } } })这种基于角色路由守卫的实现方式简单有效。普通用户和管理员的路由表分离管理员能访问的页面普通用户即使手动输入URL也会被拦截到无权限页面从入口层面保障了安全性。前端页面组件的选择上登录页用的Element UI表单组件健康打卡页根据“今日是否已打卡”状态动态切换已打卡则回显记录并禁用表单未打卡则渲染完整表单并提交。这种状态驱动的UI逻辑配合后端返回的code值来实现整体代码量适中且维护方便。5. 常见问题与排查技巧实录5.1 环境启动阶段的高频问题问题1SpringBoot项目启动失败提示端口被占用。SpringBoot默认端口8080本地开发时很常见。解决办法有两种第一种是找到占用进程并关闭它第二种是直接改server.port配置。我的建议是本地业务开发时用随机或者指定不常用端口比如8081可以避免和其他项目冲突。问题2数据库连接失败报Communications link failure。排查顺序固定MySQL服务是否启动、用户名密码是否正确、URL中的数据库名是否存在、端口是否被改过。按这个顺序检查90%的问题都在前两项。还有个小细节如果MySQL是Docker容器部署的检查端口映射是否正确。问题3Maven依赖下载慢或失败。国内网络环境下载中央仓库依赖速度不稳定。解决方案是把Maven仓库地址改为阿里云镜像mirrors mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror /mirrors配置在maven的settings.xml中全局生效。改完之后依赖下载速度能从几分钟压缩到几十秒。5.2 前后端联调阶段的问题问题4浏览器跨域错误前端8081访问后端8080报CORS错误。开发环境下解决方式是在后端加一个全局CORS配置。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }生产环境推荐使用Nginx反向代理把前后端统一到同一个域名下从源头避免跨域。问题5登录后接口数据拿不到控制台显示401。有三个排查重点登录是否成功返回Token、Token是否有效未过期、请求头是否正确携带Authorization信息。我建议在浏览器开发者工具Network面板中查看具体请求的Request Headers基本上能一眼定位到问题所在。问题6时间字段在数据库和页面之间相差8小时。这通常是JDBC连接URL缺少serverTimezone配置导致的。MySQL驱动在无法确定时区时会使用服务器默认时区导致时间偏差。配置Asia/Shanghai时区后问题解决。5.3 业务功能逻辑的坑问题7打卡记录重复。前端防重复点击、后端事务查重、数据库唯一索引三层保障缺一不可。前端防重复只能解决正常用户操作后端查重能解决大部分请求数据库约束是最后的兜底。少了任何一层在特定场景下都可能出现脏数据。问题8删除管理员账号失败。删除用户时要检查是否有相关联的业务数据。我的方案是逻辑删除给sys_user表加了一个deleted字段删除操作改成update语句置deleted1。这样用户历史打卡记录和操作日志全部保留只是在登录校验时多一个状态判断。问题9SQL注入风险。MyBatis的#{}是预编译占位符能有效防止SQL注入但${}是字符串拼接极容易注入。项目里所有动态条件查询都用#{}表名和列名这类无法预编译的固定场景才允许用${}而且必须经过白名单校验。6. 项目扩展优化方向疫情管理系统的核心功能做完之后还可以从性能、安全性、使用体验三个维度继续演进。引入Redis缓存将Token令牌、公告列表、热点统计数据缓存起来。用户登录后Token不用每次查询数据库验证直接从Redis读取响应速度和系统并发能力都会明显提升。引入Spring Security安全框架目前项目用的是自己写的拦截器校验Token核心链路是通的但安全深度有上限。整合Spring Security后可以补充更细粒度的权限注解、方法级授权、安全响应头等功能。集成定时任务使用Spring Boot自带的Scheduled注解开发每日打卡提醒功能。每天上午定时通知未打卡用户到设定时间点自动汇总当日数据生成报告这类功能很实用。数据可视化增强ECharts接地图组件展示不同区域的疫情分布再配合时间轴滑块做趋势回放。当前系统的统计图以折线图和饼图为主后期可以丰富图表类型。接口文档自动化集成Knife4j自动生成在线接口文档。前后端分离开发时接口文档的实时性和准确性直接影响协作效率。容器化部署写Dockerfile和docker-compose配置文件将MySQL、后端服务、前端Nginx一键编排启动。部署环节从手工操作变成一条命令在团队协作和持续交付场景下价值很高。扩展的优先级建议Redis大于安全框架大于部署容器化这三步对系统整体水平的提升最明显也最能体现在简历和项目文档中。我个人的体会是这类管理系统的核心价值不在于用了多少技术栈而在于是否把一条完整的业务链路真正跑通并做到可维护。我搭建这套系统前后大概用了一周最深的感受是表结构设计决定了后面写代码的顺畅程度表设计得不好后面每个功能写起来都别扭。如果你准备拿这套源码上手或改造建议不要急着敲代码先把sys_user、health_record这几张核心表的关系理清再动手改需求你会少走很多弯路。另外强烈建议本地准备一套独立的MySQL环境专门做项目开发别跟生产数据混在同一个库这个习惯培养起来多年开发生涯都受益。