ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue疫情隔离管理系统实战:状态机、权限与部署全解析

SpringBoot+Vue疫情隔离管理系统实战:状态机、权限与部署全解析 这两年只要聊到“管理系统”类的毕业设计或企业内部工具十有八九绕不开SpringBootVue这套组合。我自己前后帮人看过、改过不少这类项目疫情隔离管理系统算是一个很典型的全栈实战题目它既有常规增删改查又牵扯到人员流转、健康监测、床位分配这类相对复杂的业务状态用来练手或者接私活都挺合适。这篇文章就围绕这个项目把从架构设计、表结构到核心流程、部署避坑的完整链路拆开揉碎讲清楚重点说一些我实际踩过、改过的细节让你不光能跑起来还能知道为什么这么设计。1. 项目整体设计与拆解1.1 这道题到底在考什么先说结论这个项目表面上是“隔离人员管理”本质考的是两件事一是状态机的设计能力二是多角色权限下的数据流转。隔离管理系统和普通的后台管理系统有个关键区别它的核心实体“隔离人员”不是静态的而是有一个明确的生命周期。从申请隔离、安排床位、每日健康监测、核酸记录到最后解除隔离每一步都对应不同的状态和可操作动作。如果只是建一张表、写几个接口那和图书馆管理系统没区别但一旦涉及“这个人当前处于什么状态、谁能操作、操作后数据如何联动”项目的复杂度就上来了。跟我之前接触过的仓库管理系统做个对比你会更清楚维度仓库管理系统隔离管理系统核心实体物品相对静态人员有生命周期状态流转入库/出库相对简单登记→隔离→监测→解除多步流转权限重点操作员/管理员管理员/医护/隔离人员多角色数据特点库存数量准确性优先时序记录每日体温、症状优先扩展点报表、预警统计、密接追踪、物资联动所以做这个项目第一步不是写代码而是先把状态机和角色矩阵画清楚。这一点想透后面的表结构和接口设计会顺很多。1.2 技术选型的取舍理由这套项目用的是SpringBoot Vue MySQL MyBatis放在今天来看确实是稳妥到有些保守的组合但正因为保守才适合教学、适合快速交付、适合让维护的人不骂娘。SpringBoot解决的问题是零配置快速启动。以前用SSM整合光XML配置文件就能写好几页现在SpringBoot的自动配置机制把这些都藏起来了你只管写业务代码。MyBatis则是半自动ORM里最灵活的一个复杂查询写SQL比JPA那种全自动的直观性能也更好掌控。Vue这边我多说两句。很多人选Vue是冲着响应式数据绑定去的但在这个项目里Vue另一个优势往往被忽略组件化拆分的便利性。隔离管理系统的页面虽然不多但每个页面都有共性模块比如人员卡片、状态标签、表单弹窗用Vue的组件机制抽出来之后开发效率提升非常明显。1.3 功能模块如何划分我把这个系统拆成了六个功能域这个划分方式不是拍脑袋而是按“隔离业务有人进入、有人管理、有人出”这条主线来的系统登录与权限管理JWT 拦截器区分管理员/医护/隔离人员人员登记与状态管理隔离申请、审批、解除隔离床位/房间分配管理房间容量控制、床位状态健康监测模块每日体温上报、症状记录、异常预警核酸检测记录检测批次、结果登记数据统计与报表在隔人数、转阴率、物资消耗趋势这里有个容易忽略的地方隔离人员登录后能看到什么。这个角色不应该看到后台管理页面他能看到的应该是自己的健康上报页面、核酸记录查询和隔离倒计时。所以权限校验不只是页面级更应该是接口级别的这也是为什么JWT要在后端统一校验的原因。2. 数据库设计与核心表结构2.1 库表设计思路这个项目的表不算多核心表大概六七张但表之间的关系需要反复推敲。我建议按“人员-行为-资源”三条线来梳理人员线隔离人员表、管理员/医护账号表。行为线体温/症状记录表、核酸记录表、出入记录表。资源线房间表、床位表。这三条线交汇的地方就是“隔离记录”这个主表它记录的是“某个人从哪天开始在哪个房间隔离状态是什么”。这个设计类似订单中心的结构思路——订单主表加明细表只是这里的“明细”更多是时间线上的行为记录。为什么不用一张大表把人员信息和隔离信息放一起我跟你说个真实遭遇。之前接过一个半成品项目就是一张表存全量字段结果隔离人员隔离结束后registryState改了但历史体温记录里查不到当时他住哪个房间数据追溯全断了。所以记住一个原则会随时间变化的数据要拆开独立存储不要和静态的档案信息混在一张表里。2.2 核心DDL参考模板给你一份我调过多次的核心表结构直接把最关键的部分贴出来。人员表就不放了重点看隔离主表和健康记录表-- 隔离记录主表 CREATE TABLE isolation_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, person_id BIGINT NOT NULL COMMENT 关联隔离人员ID, room_id BIGINT NOT NULL COMMENT 关联房间ID, bed_id BIGINT NOT NULL COMMENT 关联床位ID, start_date DATE NOT NULL COMMENT 隔离开始日期, expected_end_date DATE COMMENT 预计解除日期, actual_end_date DATE COMMENT 实际解除日期, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态0待入住 1隔离中 2已解除 3异常转移, isolation_type TINYINT COMMENT 隔离类型0集中隔离 1居家隔离, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_person (person_id), INDEX idx_status (status), INDEX idx_room (room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT隔离记录表;-- 健康监测记录表 CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, isolation_id BIGINT NOT NULL COMMENT 关联隔离记录ID, record_date DATE NOT NULL COMMENT 记录日期, body_temperature DECIMAL(4,1) COMMENT 体温, is_cough TINYINT DEFAULT 0 COMMENT 是否咳嗽0否 1是, is_fatigue TINYINT DEFAULT 0 COMMENT 是否乏力, other_symptom VARCHAR(255) COMMENT 其他症状描述, record_time DATETIME NOT NULL, UNIQUE KEY uk_iso_date (isolation_id, record_date), INDEX idx_record_date (record_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT健康监测记录表;两点说明一是status字段用TINYINT不用字符串省空间还好扩展只需在枚举类里做映射二是健康记录表里加了唯一索引isolation_id record_date这个约束能从数据库层防止同一个人同一天重复上报比在代码里if判断可靠得多。2.3 关于字段设计的三个经验第一凡是金额、体温、百分比这类数据一律用DECIMAL别用FLOAT或者DOUBLE。体温显示出来是36.5℃但DB里可能出现36.50000000001这种脏数据排查起来很痛苦。第二逻辑删除和物理删除要想清楚。用户管理、隔离记录这种核心数据建议加deleted字段做逻辑删除因为牵扯到审计追溯。但健康监测记录这种时序流水数据几乎不需要“反悔”直接物理删除就行不用全部一刀切。第三时间字段建议用datetime而不是timestamp。timestamp有2038年问题而且受时区影响生产环境踩过的坑不少。datetime虽然省不了多少空间但胜在省心。3. 后端核心实现与业务逻辑3.1 SpringBoot项目的分层结构这个项目我推荐按四层结构组织别搞花活com.example.isolation ├── controller // 接口层 ├── service // 业务逻辑层 │ └── impl ├── mapper // MyBatis数据访问层 ├── entity // 数据库实体 ├── dto // 入参/出参封装 ├── vo // 视图对象 ├── common // 通用返回结果、异常、枚举 ├── config // JWT拦截器、跨域等配置 └── utils // 日期处理等工具类Controller层只管接收参数和返回结果Service层只写业务逻辑Mapper层只写SQL。这个规矩看着简单但很多人都破功。我见过最离谱的代码是把SQL拼接逻辑写在了Controller里后边想复用接口都没法下手。3.2 关键业务代码走读拿“隔离人员登记审批”这个场景举例这是整个系统里状态流转最完整的环节Service public class IsolationServiceImpl implements IsolationService { Autowired private IsolationRecordMapper isolationRecordMapper; Autowired private RoomBedMapper roomBedMapper; Transactional(rollbackFor Exception.class) public Long registerIsolation(IsolationRegisterDTO dto) { // 1. 校验床位当前状态 BedVO bed roomBedMapper.getBedWithStatus(dto.getBedId()); if (!AVAILABLE.equals(bed.getStatus())) { throw new BizException(该床位当前不可用); } // 2. 创建隔离记录状态为待入住 IsolationRecord record new IsolationRecord(); record.setPersonId(dto.getPersonId()); record.setRoomId(bed.getRoomId()); record.setBedId(dto.getBedId()); record.setStartDate(LocalDate.now()); record.setExpectedEndDate(LocalDate.now().plusDays(dto.getIsolationDays())); record.setStatus(IsolationStatus.PENDING.getCode()); isolationRecordMapper.insert(record); // 3. 占用床位防止其他人再分配 roomBedMapper.updateStatus(dto.getBedId(), OCCUPIED); return record.getId(); } }这个方法的细节全在Transactional上。登记和床位状态变更必须是原子操作否则可能出现“记录建好了床位状态没改”或者反过来这种数据不一致在隔离场景下很致命。从SpringBoot 2.x开始事务默认只在RuntimeException下回滚所以业务异常一定记得继承RuntimeException或者明确指定rollbackFor Exception.class。3.3 健康上报接口的防重设计健康上报是隔离人员每天最常用的功能这里有一个很经典的并发坑。假设隔离人员连续点了两次提交会怎样两次请求都通过了IF判断就会产生两条记录。解决办法有两个层面数据库层面就是前面那张表里加了唯一索引uk_iso_date谁都没法插入重复数据。应用层面提交时先SELECT COUNT判断是否已上报双保险。不过还有一个更隐蔽的问题隔离第7天要生成第8天的报备提醒这个不是实时计算的而是用定时任务去扫描。SpringBoot自带的Scheduled就能做但要注意分布式环境下的任务重复执行问题。如果系统部署在多节点定时任务会重复执行要么用XXL-Job这类分布式调度框架要么给任务加数据库锁。单机部署的话原生Scheduled足够了。3.4 权限控制的落地姿势我不建议在Controller里用一堆if (admin.equals(role))硬编码去判断权限后期维护会非常痛苦。这个项目用JWT做登录态拦截器做统一鉴权就够了public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains(/auth/)) { return true; } String token request.getHeader(Authorization); if (token null || !JwtUtil.verify(token)) { response.setStatus(401); return false; } // 解析角色并存入ThreadLocal或Request Attribute request.setAttribute(role, JwtUtil.getRole(token)); return true; } }配置拦截器注册时记得放行静态资源不然前端打包后的JS和CSS全被拦了页面白屏排查半天才发现是拦截器的问题这个坑我自己踩过。如果要更精细的权限粒度比如医护只能看自己负责楼层的记录推荐在PreAuthorize注解里写SpEL表达式或者在查询SQL层通过角色拼接条件。SQL层控制数据权限其实比代码层更靠谱这个思路是跟阿里Java开发手册学的。4. Vue前端实现与前后端联调4.1 前端项目结构Vue这边我用的是Vue CLI创建的标准结构但实际开发时没有死板地用三层目录而是按业务模块组织的src ├── api // 接口请求封装 │ ├── isolation.js │ └── health.js ├── views // 页面组件 │ ├── admin // 管理员端页面 │ ├── doctor // 医护端页面 │ └── user // 隔离人员端页面 ├── components // 公共组件 │ ├── StatusTag.vue │ └── PersonCard.vue ├── router ├── store └── utils └── request.js // axios封装这种组织方式对多人协作是有好处的——后端定好接口后前端开发直接按模块新建对应的api文件各写各的页面合代码时冲突少很多。4.2 axios封装的必要动作axios请求不能直接用至少要做三层处理。第一层设置baseURL和超时时间第二层拦截response把后端的统一返回结构解开只返回data部分第三层统一处理Http状态码和业务状态码例如token过期时自动跳转登录页。service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 系统异常) if (res.code 401) { router.push(/login) } return Promise.reject(new Error(res.msg)) } return res.data }, error { Message.error(error.message) return Promise.reject(error) } )这样前端业务代码里就不会到处散落res.code 200的判断了。隔离人员提交健康上报时代码只需要关心成功后的数据刷新逻辑。4.3 动态路由与权限菜单隔离系统里不同角色看到的菜单差别很大我一开始用静态路由每个人登录后都去route里判断v-if控制菜单显示但这样有个问题用户其实可以手动输入URL绕过菜单进入无权页面。所以后来改成按角色动态渲染路由// 根据登录用户的角色从后端拉取可访问的路由配置 const asyncRoutes await getMenuList() router.addRoutes(asyncRoutes)router.addRoutes这个方法Vue Router 4里被废弃了换成了addRoute动态逐个添加但核心思路是一样的。动态路由的好处是后端返回什么菜单前端就注册什么路由用户没法越权访问。4.4 前后端联调的命门跨域与时间格式联调阶段90%的问题集中在跨域。SpringBoot后端建议直接配置全局跨域映射Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }如果前端用Vite开发也可以配置server.proxy做代理转发二选一即可不要两边同时都放开反而会出现重复响应头的问题。时间格式的坑也很典型。后端返回LocalDateTime默认序列化成2024-06-18T15:30:00Vue组件里显示时格式不对。一劳永逸的办法是在配置文件中统一指定spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这么配好了之后全项目的时间字段都统一格式不用每个类加JsonFormat注解。5. 部署发布与环境配置5.1 本地开发环境准备Java环境建议JDK 8或11这两个版本对SpringBoot 2.x的兼容性最好。别用JDK 17跑老项目Commons-CSV这些老库可能直接报模块访问错误。MySQL装5.7或8.0都行但注意8.0的mysql-connector-java驱动包名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver配置不对会启动报错。Maven项目建议在pom.xml中锁定SpringBoot版本避免依赖冲突后找半天问题parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent2.7.x是目前最稳的版本线社区资料多遇到问题网上基本都是这个版本的解决方案。5.2 前后端分离部署方案生产环境有两种部署思路一是前后端完全分离后端跑8080端口前端打包后放在Nginx里监听80端口并配置反向代理转发API请求二是把Vue打包后的dist目录直接塞进SpringBoot的resources/static下打成单Jar运行。小项目的话方案二更省事一个jar包全搞定。Vue Router还要注意一个事如果用的是history模式打包后部署到Nginx会404因为服务端没有配置try_files回退到index.html。开发环境看不出来部署上去才发现。解决方法是Nginx配置location里加上try_files $uri $uri/ /index.html;或者前端直接用hash模式免去这个烦恼。hash模式URL带个#号不好看但胜在省心。5.3 数据库初始化与版本管理多环境部署时数据库表结构的老是手工同步会出大事。这个项目建议直接用spring.sql.init.schema-locations加载启动SQL文件或者集成Flyway做版本管理。Flyway用了就离不开了每次表结构变更写一个V2__xxx.sql任何环境执行顺序都一样不会出现开发库多一张表、生产库少一张表的尴尬。6. 常见问题与避坑实录6.1 典型问题速查表现象根本原因解决方案前端登录后调用接口全部401JWT拦截器放行了登录接口但后续请求没带token检查axios是否在请求头里统一加Authorization列表数据能查出来但前端全是undefined后端返回字段是驼峰命名前端用了下划线命名统一启用map-underscore-to-camel-case前端也统一命名规范上传接口报413Nginx默认请求体大小限制1M在Nginx配置client_max_body_size 20mMyBatis查询结果中文乱码连接URL缺少characterEncodingutf8JDBC连接串补上参数端口被占用启动失败上一次的进程没有终止Windows下netstat -ano找PID杀掉打包后dist访问白屏Vue Router history模式刷新404Nginx配置try_files或改用hash模式6.2 排查问题的方法论碰到问题不要瞎猜按三层顺序排查效率会高很多。第一层看浏览器Network面板API请求是否发出去了状态码是多少第二层看后端日志SpringBoot的启动日志和请求日志有Full堆栈比看前端报错更接近真相第三层才是看代码逻辑。这里特别建议在application.yml里把MyBatis的SQL日志打开mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这个配置会直接打印每个Mapper里执行的完整SQL和参数值80%的SQL问题都能从这个日志里肉眼定位到。生产环境记得关掉日志打太多会影响性能。6.3 一个让我头疼很久的隐性bug这个项目里隐藏最深的一个坑是数据库连接时区问题。本地开发一切正常部署到服务器上发现所有新增记录的时间都比北京时间少了8个小时。排查半天最后发现是MySQL连接串里没有设置serverTimezoneAsia/Shanghai。这问题不大但是一旦出问题现象特别像“系统逻辑错了”误导性很强。后来我在交付检查清单里固定加了一条连接串必须显式声明时区。7. 写在最后的体会做这个项目时我最大的感受是SpringBootVue这类技术在2025年已经非常成熟了框架本身的差异早已不是项目的核心竞争力真正的门槛在业务抽象能力和数据一致性设计上。隔离管理系统的“隔离记录-健康记录-床位资源”三张表的关系设计才是整个项目里最需要花心思的部分技术实现反而是相对机械的活。我个人复盘时发现的规律是凡是让我改稿超过三轮的地方都不是功能跑不通而是业务角色之间的前后token能推进——比如隔离人员提交体温、医护人员看到数据、管理员看汇总报表这三个角色的数据通道如果一开始没有在设计阶段理清开发阶段就会反复返工。所以给接手类似项目的朋友一个建议代码可以写得快但设计不能想得快。拿到需求先画用例图、梳理状态流转、列角色权限矩阵这步做完再动手写代码整体效率只高不低。我见过太多人一上来就建表结果表建了十几张业务一跑发现逻辑上就缺字段推倒重来这学费交得太不值了。
返回列表