ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue+MyBatis企业级疾控管理系统源码实战与优化复盘

SpringBoot+Vue+MyBatis企业级疾控管理系统源码实战与优化复盘 这套系统是我前两年实际交付过的一套企业级疾病防控综合管理系统的完整源码复盘技术栈就是标题里的SpringBoot Vue MyBatis架构数据库用MySQL前后端分离的经典组合。做这类系统最大的感受是它表面上是一个增删改查的管理后台真正做进去才发现业务规则、状态流转、权限矩阵、数据统计每一块都比想象中复杂。如果你正在做医疗公卫类信息化项目或者想找一套结构完整、能直接二次开发的全栈源码来学习这篇文章应该能帮你节省不少时间。我会从业务拆解、技术选型、后端落地、前端实现、数据库调优、环境部署到实战踩坑把整个项目的设计思路和关键细节讲清楚。1. 疾控系统到底在管什么业务模块与角色权限拆解先别急着看代码。做企业级系统最忌讳的就是拿到需求就建表我在这套项目上踩过最大的坑就是前期业务分析不够后面反复改表结构。疾病防控综合管理系统之所以叫综合是因为它服务的不是一个科室而是疾控中心内部多个业务线同时还要对接医疗机构、基层社区卫生服务中心和被管理人群。1.1 从业务痛点反推系统模块疾控中心的日常工作大致可以拆成这么几条线传染病监测与报告、病例管理和流行病学调查、疫苗接种管理、重点场所卫生监督、应急物资保障以及面向社会层面的健康状态申报。过去这些工作大量依赖Excel和微信群数据散落在不同人手里一旦需要汇总统计几个科室的人要折腾好几天。所以我做这套系统时第一件事不是画原型而是把业务模块拆成了七个边界清晰的功能域模块核心功能关键数据对象传染病监测报告病例直报、审核、订正、查重报告卡、病种字典病例个案与流调管理个案建档、流调信息采集、密接管理个案档案、密接人员疫苗接种管理预约登记、疫苗库存、接种记录疫苗批次、接种档案健康状态申报居民自助申报、社区审核申报记录应急物资管理物资出入库、库存预警物资台账、出入库单统计分析大屏日报周报、地区分布、趋势图统计结果集系统权限管理用户、角色、菜单、操作日志用户表、角色表每个模块都不是孤立存在的。比如传染病监测模块里一个病例报告被审核通过后会自动关联生成一条待流调任务流调完成后再生成密接人员管理记录整个链路是通的。这也是我把它设计成综合系统而不是几个独立小系统的原因。1.2 角色权限矩阵与业务状态流转这套系统的用户角色有五类权限设计上采取RBAC模型菜单和按钮都挂在角色上。我直接给出一份权限矩阵参考角色数据范围核心权限超级管理员全局系统配置、用户管理、所有模块疾控中心业务员本区域病例审核、流调录入、物资管理医疗机构上报医生本单位病例直报、修订基层网格员街道/社区健康申报审核、密接随访普通市民本人健康申报填报、记录查询角色设计直接影响后端的接口鉴权策略。数据范围这一列非常关键疾控中心业务员只能看本区域数据市级的能看全部区县。这意味着所有列表查询接口都必须带上行政区划编码的过滤条件不是简单登录之后就能查全库。状态流转方面病例报告模块我设计了这样一个状态机待审核、审核通过、调查中、已结案以及异常分支已作废、已订正。每个状态变更都会写一条审计日志。这类业务状态流转代码看起来不起眼但比CRUD复杂得多也恰恰是企业版源码和教学demo的差距所在。2. 技术栈选型复盘为什么这套组合最适合企业级交付技术选型这部分我可以说是在交付压力下被逼出来的结论。市面上各种框架层出不穷但真正拿到疾控中心这种企业级项目里稳定、可维护、团队能上手才是第一位的。2.1 单体优先企业级内部系统的架构克制这套源码没有上微服务架构就是一个SpringBoot单体应用加上Vue前端。很多人可能会觉得企业级就该是Spring Cloud全家桶我一开始也差点这么干后来想通了疾控系统的真实并发量根本没那么夸张更多是几百个工作人员在上班时间集中录入和审核单体应用配合MySQL完全扛得住。微服务带来的服务拆分、分布式事务、链路追踪、部署运维成本在这个业务场景里全是负资产。所以我坚持单体优先保留清晰的模块分包将来如果某个模块需要独立拆分代码层面也是现成的。这也是我想给做同类项目的人一个建议先评估业务规模和团队运维能力再决定要不要分布式。2.2 MyBatis与MySQL把复杂查询握在自己手里持久层我没有用JPA选了MyBatis。原因非常直接疾控系统里的统计报表太多了。什么本月手足口病报告数按区县分布、疫苗接种率按月趋势、密接者转归情况统计这类查询的SQL非常复杂动辄多表联查加条件聚合用JPA的Criteria API写起来能写到怀疑人生而且生成的SQL很难优化。MyBatis的XML里写SQLDBA同事也能直接review索引怎么走一目了然。配合MyBatis-Plus提供的基础CRUD和分页插件简单的单表操作用BaseMapper复杂的统计查询用自定义XML开发效率很高。MySQL的选择更简单团队熟、运维熟、云上数据库兼容性好。用得最多的就是InnoDB引擎事务、行级锁、外键约束都靠谱。要注意的是字符集必须用utf8mb4而不是utf8不然疾控系统里录入了生僻字或者特殊符号比如体温符号℃、特殊标点会直接报错。2.3 前端框架选型与版本搭配的细节前端我选了Vue3 Element Plus Vite。Vue3的组合式API在维护复杂表单逻辑时确实比Options API顺手Element Plus的组件库覆盖了后台管理系统90%的场景表格、表单、弹窗、树形控件都有不需要自己造轮子。后端用的是SpringBoot 2.7.x配JDK 8没有盲目上SpringBoot 3。为什么SpringBoot 3强制要求JDK 17但很多单位的服务器上还跑着JDK 8升级会引入兼容性问题。MyBatis-Plus用的3.5.x版本对SpringBoot 2.x支持得最好。整个项目的版本搭配如下组件版本选择说明JDK1.8服务器兼容性最好SpringBoot2.7.18稳定维护版支持JDK8MyBatis-Plus3.5.3增强CRUD与分页MySQL5.7 / 8.0幂等兼容Vue3.x组合式APIElement Plus2.x后台组件库这套版本组合经过了实际生产环境验证踩坑最少网上能查到的资料也最全。技术选型就是这么回事与其追新版本不如选最适合团队维护的组合。3. 后端落地从表结构到核心接口的实现笔记后端这部分是整个系统的心脏。我不会把全部源码贴出来那没有意义我把最关键的表结构设计、接口规划和鉴权链路拿出来讲你拿到源码后就能对上号。3.1 核心表结构与业务状态流转疾病防控系统的数据库表核心的几张表我列一下看完你就知道为什么说这不是简单CRUD了。以传染病报告卡为例建表DDL关键字段如下CREATE TABLE case_report ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, case_no varchar(32) NOT NULL COMMENT 报告卡编号, patient_name varchar(64) DEFAULT NULL COMMENT 患者姓名, patient_id_card varchar(64) DEFAULT NULL COMMENT 身份证号(加密存储), disease_code varchar(20) NOT NULL COMMENT 病种字典编码, onset_date date DEFAULT NULL COMMENT 发病日期, diagnosis_date datetime DEFAULT NULL COMMENT 诊断日期, report_level tinyint(4) DEFAULT NULL COMMENT 报告级别, report_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态:0待审核 1审核通过 2调查中 3已结案, region_code varchar(12) NOT NULL COMMENT 行政区划编码, reporter_id bigint(20) DEFAULT NULL COMMENT 报告人ID, created_at datetime DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_case_no (case_no), KEY idx_region_date (region_code, onset_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT传染病报告卡;注意几个细节身份证号必须加密存储这是疾控数据的合规要求地区编码region_code单独建索引并且和日期组合成联合索引因为这类系统最高频的查询条件就是某地区某时间段内的报告数报告状态用tinyint数字字典而不是直接用字符串既省空间又方便扩展。状态流转我把它做成了枚举类配合状态机服务每个状态变更都走统一入口自动校验前置状态是否合法。比如已结案的病例不能直接改回待审核否则数据在流转链路上就乱套了。这些逻辑放在service层用Spring的事务注解保证状态更新和日志写入要么都成功要么都失败。3.2 接口设计与鉴权链路后端整体采用标准的controller-service-mapper三层结构包名按com.disease.xxx划分看源码时路径很清晰。接口设计遵循RESTful风格核心接口大致这么规划方法路径说明POST/api/case/report新发病例直报GET/api/case/page病例分页查询PUT/api/case/{id}/audit病例审核POST/api/vaccine/appointment疫苗接种预约GET/api/vaccine/stock疫苗库存查询GET/api/statistics/trend趋势统计POST/api/materials/inbound物资入库鉴权用的是Spring Security JWT。登录成功后签发token前端把它存在本地每次请求放进Authorization头。拦截器里做两件事校验token是否有效校验当前用户是否拥有访问接口所需的权限标识。// 核心权限校验逻辑 PreAuthorize(hasAuthority(case:audit)) PutMapping(/{id}/audit) public ResultVoid audit(PathVariable Long id, RequestBody AuditDTO dto) { // 只有具备病例审核权限的角色才能调用 return caseService.audit(id, dto); }数据权限这块容易被忽略。同样是病例列表市级用户要看所有区县区级用户只能看本辖区。我在查询条件里统一拼接了regionCode的过滤逻辑从JWT里解析出当前用户的区域编码不让前端传区域参数。这样前端怎么改请求都越权不了数据安全才真正落地。4. 前端Vue工程实践动态路由、权限菜单与数据可视化前端是整个系统的门面疾控中心的工作人员天天对着这些页面录入数据不好用就会被天天吐槽。Vue侧我重点做了三个事动态路由、axios封装、可视化大屏。4.1 动态路由与菜单权限的实现方式后台管理系统的菜单不同角色进来看到的不一样。如果所有路由都写死在代码里那前端就得做一堆v-if判断很臃肿。我的方案是动态路由登录成功后从后端拉取当前用户的菜单和权限标识用addRoute动态注册。// 登录成功后动态注册路由 const menuList await getMenuList(); const routes generateRoutes(menuList); routes.forEach(route router.addRoute(route));generateRoutes函数把后端返回的菜单树转换成Vue Router的RouteRecordRaw数组每个菜单节点对应一个组件路径。这样菜单配置、路由注册、页面渲染三者是一一对应的新增一个菜单只需在数据库里加一条记录完全不用改前端代码。路由守卫也很关键。我在全局前置守卫里做了三件事判断本地有没有token没有就跳登录页有token但当前路由不在已注册列表里重新拉取菜单并addRoute每次路由跳转前校验目标路由所需的权限标识。router.beforeEach((to, from, next) { if (!getToken()) { next(/login); return; } if (!hasRoutes()) { // 刷新页面后路由丢失重新拉取 initDynamicRoutes().then(() next({ ...to, replace: true })); return; } if (to.meta.permission !hasPermission(to.meta.permission)) { next(/403); return; } next(); });刷新页面后动态路由丢失这个问题几乎所有做动态路由的项目都会遇到我直接在上面代码里做了重新拉取处理。这个细节如果你不做用户按一下F5就直接白屏了。4.2 axios封装、跨域代理和报表展示axios封装是每个前端项目的基础工程。我在请求拦截器里自动带上token在响应拦截器里统一处理HTTP错误码和业务错误码特别是401状态码后端返回这个说明token过期了直接清理本地状态并跳转重新登录。业务错误码非0的统一用Element Plus的Message提示前端不用在每个请求里写一堆错误处理。跨域问题在开发环境用Vite的proxy解决// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }生产环境部署时我用Nginx转发或直接把前端dist放进SpringBoot的static目录就都不存在跨域问题了这个部署细节放到后面章节详细说。报表和大屏用的是ECharts。疾控大屏上最常放的图表有近30天报告数趋势折线图、各区县病例分布柱状图、病种占比饼图。ECharts通过npm安装按需引入打包体积控制在合理范围。数据从统计分析接口异步加载接口返回结构统一为日期、区域、数量、占比字段图表配置完全数据驱动。5. 数据库与MyBatis的调优实践慢SQL、缓存坑、批量插入疾控系统上线运行半年后问题开始浮出水面。这个阶段数据库层面的优化是最有价值的我总结了三个典型的调优方向。5.1 统计报表慢SQL定位与索引优化系统刚上线时统计报表接口偶尔会慢到几秒钟。我先在MySQL里开启了慢查询日志定位到最慢的一条SQL是就诊趋势统计它在case_report表上同时做了日期过滤、地区分组和病种关联三件事。explain一看病种字典表走了全表扫描。优化手段就是加联合索引。我在病例表上建了(region_code, onset_date, disease_code)联合索引统计查询直接在索引上完成过滤不需要回表取完整行数据。这条SQL从1.8秒降到了80毫秒左右。另一条高频查询是疫苗接种预约记录查询我在预约时间字段和疫苗批次ID上加了普通索引效果非常明显。给张表加索引谁都会但要知道给哪些字段建组合索引、建完之后explain的type是ref还是index这才是实践里的真功夫。5.2 MyBatis缓存引发的数据一致性事故这个坑我必须详细说下否则很多人会中招。MyBatis有一级缓存和二级缓存一级缓存默认开启作用域是同一个SqlSession内二级缓存需要手工开启作用域是namespace级别。那次事故是这样的疫苗接种记录模块我在Mapper上开了二级缓存应用启动后前几次查询很快但没过多久就发现某个批次的库存扣减之后重新查询还是显示旧库存。原因就是二级缓存在namespace下缓存了查询结果而库存变更走的是另一个Mapper的方法MyBatis不知道要清哪个缓存于是读到了脏数据。排查链路就是先确认不是MySQL事务问题然后在SQL日志里发现同样的查询没有真正执行直接命中了缓存才定位到二级缓存上。修复方案很简单完全关闭这个模块的二级缓存或者把库存类实时性要求高的查询设置为useCachefalse。我最终的方案是干脆关闭全局二级缓存只保留一级缓存。像疾控这种数据实时性要求高的系统缓存的收益远小于数据不一致的风险。5.3 批量插入与分页查询的性能优化系统里最常见的两个大数据量场景一是大批量导入历史病例数据二是分页查询大量密接人员记录。批量插入如果不做优化几千条数据一条条insert能慢到让人怀疑人生。两个关键配置jdbc:mysql://localhost:3306/disease_control?rewriteBatchedStatementstrue加上这个参数后MyBatis批量插入才能真正走JDBC的batch模式而不是模拟执行。另一个是useServerPrepStmtstrue允许服务端预编译也能提升执行效率。分页查询我用的MyBatis-Plus的分页插件底层是物理分页自动拼接LIMIT语句。需要注意大偏移量问题比如查看第10000页、每页20条MySQL要扫描前面20万条数据再丢弃性能很差。这种场景我改成基于游标的分页用上一页最后一条记录的ID作为查询条件性能提升非常明显。6. 环境搭建与源码运行MySQL、SpringBoot、Vue联调全流程拿到源码第一件事就是跑起来这一步我见过太多人在环境上卡住。我把完整的运行流程捋一遍按照这个顺序操作基本不会出问题。6.1 MySQL安装与初始化配置MySQL的安装方式Windows上推荐直接下载zip免安装版。步骤其实很简单下载zip包解压到指定目录在根目录建一个my.ini配置文件指定datadir、端口、字符集然后用mysqld --initialize-insecure初始化最后用net start方式注册成Windows服务。配置文件里有几个生产环境必须注意的点[mysqld] port3306 character-set-serverutf8mb4 collation-serverutf8mb4_general_ci default-time-zone08:00 max_connections500default-time-zone这个参数非常重要不设置的话SpringBoot连接时用serverTimezoneAsia/Shanghai能连上但数据库内部的时间函数返回值可能是UTC时间导致疾控报表里今日新增病例数永远对不上。数据库初始化完成后创建项目数据库并导入源码自带的SQL脚本mysql -uroot -p -e CREATE DATABASE disease_control DEFAULT CHARACTER SET utf8mb4 mysql -uroot -p disease_control disease_control.sqlSQL脚本里包含了所有表结构和初始化数据包括用户表、菜单表、字典表直接用管理员账号登录就能看到完整的页面结构。6.2 SpringBoot启动配置与常见报错处理后端启动前修改application.yml数据源配置spring: datasource: url: jdbc:mysql://localhost:3306/disease_control?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 server: port: 8080 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpllog-impl配置成StdOutImpl后控制台会打印每一条执行的SQL和参数调试阶段几乎离不开。端口修改直接改server.port如果你本地的8080被占用了改成8081或其他端口都行但前端代理和目标地址要对应调整。最常见的启动报错有这么几类数据库连接失败检查MySQL服务启动了没有、密码对不对Invalid bound statementMapper接口找不到XML检查mapper-locations路径端口占用被别的进程占了改端口。这些基本都是环境问题按上面的配置逐项排查很快就能解决。6.3 Vue打包放进SpringBoot的两种部署方式前端开发模式跑起来很简单npm install装依赖然后npm run dev。但真实交付时前端要打包部署。这里有两种成熟方案。第一种是双服务部署Vue打包生成dist目录扔给NginxNginx把/api开头的请求反向代理到SpringBoot端口。这种方案前后端完全隔离适合前端、后端分别部署在独立服务器上的场景。Nginx配置需要特别注意history路由模式下要配置try_files规则location / { try_files $uri $uri/ /index.html; }第二种是把Vue打包产物放进SpringBoot的static目录。把dist目录下的文件复制到src/main/resources/static下重新打包SpringBoot的jar访问同一个端口就能同时提供页面展示和接口服务。这种方式适合单机部署、不想额外装Nginx的小团队场景。需要注意的坑是如果你的Vue路由用了history模式SpringBoot需要把非接口请求转发到index.html否则直接刷新某个二级页面会返回404。我在项目里加了一个WebMvcConfigurer把非/api路径的页面请求forward到/index.html问题就解决了。7. 上线后遇到的坑完整的排查链路复盘这个章节我挑三个最典型的线上问题完整还原当时的排查过程。这些问题不会写在官方文档里但做企业级系统迟早会遇到。7.1 大批量导出Excel导致内存溢出现象是疾控中心的工作人员导出全年病例Excel报表时应用之前是个SpringBoot服务直接OOM崩掉了。第一反应是JVM内存不够把堆内存调大后重新触发还是崩。定位分析后发现是Excel导出工具的问题我用的是Apache POI的HSSFWorkbook它会把所有数据行全部加载到内存里全年几万条记录加上合并单元格样式内存直接爆掉。排查链路是先看GC日志发现频繁Full GC抓了堆转储文件用MAT分析定位到org.apache.poi.hssf包下的对象占用了80%的堆内存。根因清楚了修复方案是改用SXSSFWorkbook这个类专门用于流式导出内存里只保留最近100行数据其余全部刷到磁盘临时文件。上线后同样的导出操作内存占用降到了原来的十分之一OOM问题彻底解决。7.2 定时统计任务里的事务失效系统有一个每天凌晨的定时任务负责把前一天的各区域上报数据汇总到统计表。上线后某天发现统计表里有几条重复数据排查后发现定时任务所在的方法被Scheduled注解调用但方法内部的事务没有生效。根因是Spring的Transactional注解是通过AOP代理实现的而定时任务在同一个类的内部方法之间调用时不会经过代理对象事务自然就失效了。排查链路是这样的先在数据库日志里看有没有BEGIN/COMMIT语句发现没有说明事务注解根本没起作用。解决办法有两种一是把定时任务调用的方法拆到另一个Service类里让外部调用走代理二是在定时任务方法里手动获取事务管理器用编程式事务包裹业务逻辑。我选了第一种因为代码改起来最清晰。7.3 前端路由刷新404与打包体积过大前端部署上线后用户反馈在列表页刷新一下浏览器就404了。这个问题根因就是前面说的history路由模式刷新时浏览器直接向服务器请求当前URL对应的路径而服务器上并没有这个文件。排查链路是先用浏览器开发者工具看请求URL发现请求直接到了NginxNginx返回404。修复就是在Nginx配置里加try_files把请求回退到index.html或者后端SpringBoot做转发。两种方案我都采用了双服务部署的走Nginx配置单jar部署的走后端转发。打包体积过大其实也是上线后才暴露的问题。整个Element Plus直接全量引入打包出来2MB多首屏加载白屏好几秒。优化方案是按需引入组件Vite配合unplugin-vue-components插件自动处理组件和对应样式的按需引入ECharts也改成按模块引入用到的图表类型才打包。最后打包体积从2.2MB降到500KB左右首屏加载时间快了很多。8. 源码二次开发的建议最后说点实际的建议。这套系统源码我在设计之初就刻意把业务模块和基础框架剥离开来如果要在它基础上做二次开发按照我的经验你可能会遇到的问题主要集中在这几个方向新业务模块怎么挂上去、数据字典怎么扩、现有接口满足不了新需求怎么办。扩展新模块时后端照着现有的controller-service-mapper结构复制一套前端在菜单表里加一条记录不用改权限框架和路由代码就能把新模块挂进去。这是这套源码最大的增量价值。数据字典扩展更简单所有下拉选项都走字典表改数据库即可前端不需要硬编码。如果觉得现有接口返回的字段不够用优先选择新增接口而不是改老接口因为老接口可能已经被报表、大屏、移动端多处绑定改字段很容易引发连锁问题。每次改完接口用Swagger的接口文档对照检查一遍能省掉很多联调时来回扯皮的麻烦。我个人实际做这套系统的过程中最大的体会是这类企业级系统的难点从来不在某个技术点上而在于你怎么把散乱的需求整理成清晰的模块边界再通过合理的表结构和接口设计让整套系统在三年内都经得起业务变化的折腾。如果你准备在这个源码基础上做医疗公卫领域的项目建议第一件事就是把病种字典和行政区划编码表梳理清楚这两张表是整个系统所有统计报表的数据地基。
返回列表