简介:本资源是一套完整的基于Spring Boot的医院急诊系统毕业设计级源码,面向计算机专业本科生、Java全栈初学者及医疗信息化项目实践者,解决急诊业务流程数字化、前后端分离开发与MySQL数据管理等典型需求。压缩包共852个文件,含138个Java后端逻辑文件、50个Vue组件页面、153个JS交互脚本、44个CSS样式文件及1个建库SQL脚本,辅以SVG图标、GIF动效与JPG/PNG素材,完整覆盖系统启动、用户认证、分诊调度、病历管理等核心模块;整体大小为17.7MB。已有67人学习下载,资源附带详细部署说明文档,并包含.bat启动脚本、.yml配置文件及.bak备份模板,便于快速导入IDEA/Eclipse环境、执行Navicat数据库初始化与本地调试,特别适合课程设计答辩与毕设二次开发。
1. 项目概述与核心价值
最近在整理过往项目资料时,翻出了一个几年前深度参与开发的医院急诊系统源码。这套系统基于当时主流的 SpringBoot + Vue 技术栈构建,后端用 Java,前端用 Vue,数据库是 MySQL,算是一个比较经典的“前后端分离”单体应用。虽然现在微服务、云原生概念满天飞,但回过头看,这个项目的架构设计和业务逻辑实现,对于想深入理解医疗信息化、特别是急诊业务流程的开发者来说,依然有很高的参考价值。它不是一个简单的“增删改查”Demo,而是包含了从分诊、抢救、留观到费用结算的完整急诊闭环管理。如果你正在学习 SpringBoot 全栈开发,或者对医疗软件感兴趣,想了解一个真实业务系统是如何从设计到落地的,这套代码和附带的说明文档应该能给你不少启发。
急诊系统的核心价值在于“急”和“序”。它需要在极短的时间内,处理信息不全、病情多变的患者,同时确保救治流程规范、资源调度有序、信息记录准确。这对软件系统的实时性、稳定性和业务逻辑的严谨性提出了极高要求。我们当时的设计目标,就是通过技术手段,将急诊科的标准化作业流程(SOP)固化到系统中,减少人为差错,提升救治效率。
2. 技术栈选型与架构设计解析
2.1 为什么是 SpringBoot + Vue + MySQL?
这个技术组合在几年前乃至现在,都是企业中快速开发业务系统的“黄金搭档”。选型背后的逻辑很实际:
- 后端 SpringBoot: 它极大地简化了 Spring 应用的初始搭建和开发过程。急诊系统后台需要处理复杂的业务逻辑、权限控制、事务管理以及与各种硬件设备(如监护仪、打印机)的接口对接。SpringBoot 的“约定大于配置”理念,让我们能快速集成 MyBatis(数据访问)、Spring Security(安全)、Redis(缓存)、RabbitMQ(消息队列,用于如检验报告推送)等关键组件。内嵌的 Tomcat 服务器也省去了复杂的部署配置,通过一个
jar包就能独立运行,这对于需要快速部署和更新的医院环境很友好。 - 前端 Vue.js: 急诊系统的前端界面需要高度的交互性和实时性。比如,分诊大屏需要实时刷新候诊队列,医生工作站需要快速切换患者、填写电子病历。Vue 的响应式数据绑定和组件化开发,使得构建这类动态复杂的单页面应用(SPA)变得高效且易于维护。相较于当时的 Angular 和 React,Vue 的学习曲线更平缓,对于医院内部可能存在的技术栈转型团队更友好。我们使用了 Vue CLI 搭建工程,配合 Vue Router 管理路由,Vuex 做状态管理,Element UI 作为基础组件库,快速构建了清晰、易用的操作界面。
- 数据库 MySQL: 选择 MySQL 是基于其成熟、稳定、开源且生态完善。急诊系统的数据关系虽然复杂(患者、医嘱、病历、费用等),但事务强一致性的要求很高(例如扣费、发药必须保证原子性)。MySQL 的 InnoDB 存储引擎提供了可靠的 ACID 事务支持。同时,考虑到医院数据量的长期增长(一个三甲医院急诊科年接诊量可达数十万人次),我们在设计之初就考虑了分表和历史数据归档策略。MySQL 丰富的监控和优化工具,也便于 DBA 进行性能调优。
注意: 技术选型没有绝对的好坏,只有是否适合当前场景。对于初创团队或项目,SpringBoot 和 Vue 的“快”是首要优势。如果项目规模极大,并发量超高,或许需要考虑 Spring Cloud 微服务或更前沿的技术,但那会带来巨大的复杂度和运维成本。这个急诊系统项目证明了,经典技术栈足以支撑起一个核心业务系统的稳定运行。
2.2 整体架构设计思路
系统采用典型的前后端分离架构。前端 Vue 项目独立部署,通过 Nginx 提供静态文件服务并配置反向代理;后端 SpringBoot 项目提供 RESTful API;两者通过 HTTP/HTTPS 协议进行 JSON 格式的数据通信。
后端分层架构:
- Controller 层: 接收前端请求,进行参数校验(我们使用了 Hibernate Validator),并调用对应的 Service 方法。这一层很“薄”,只负责协议转换和流量分发。
- Service 层: 业务逻辑的核心所在地。所有急诊相关的业务规则,如分诊级别的自动判定逻辑、抢救记录的生成规则、药品库存的校验与扣减等,都在这里实现。为了保证事务,我们通常在 Service 方法上使用
@Transactional注解。 - Mapper 层(DAO层): 使用 MyBatis 框架,通过 XML 映射文件或注解方式,定义数据库的 CRUD 操作。这里会编写复杂的动态 SQL 来应对多条件查询,比如根据时间、科室、医生、患者姓名等多维度组合查询急诊记录。
- Entity 层(Model层): 与数据库表结构对应的 Java 实体类。我们使用了 Lombok 插件来简化 Getter/Setter 等样板代码的编写。
前端模块化设计: 前端项目按功能模块划分:
views/triage/: 分诊台相关页面(患者登记、分诊评估、队列管理)。views/doctor/: 医生工作站页面(病历书写、医嘱开具、检查申请)。views/nurse/: 护士工作站页面(执行医嘱、护理记录、费用记账)。views/charge/: 收费处页面(预交金管理、结算、发票打印)。views/board/: 急诊大屏展示页面(候诊队列、抢救室状态、医生排班)。api/: 集中管理所有向后端发送请求的接口函数。store/modules/: Vuex 状态管理模块,用于共享跨组件的状态,如当前登录用户信息、全局字典数据。
3. 核心业务模块深度解析
3.1 急诊预检分诊模块
这是急诊的“第一道关卡”,直接关系到患者能否得到及时、恰当的救治。系统实现了标准的分诊流程。
业务流程:
- 患者登记: 通过读取身份证、医保卡或手动输入,快速创建患者基本信息。对于“三无”患者(无身份、无家属、无经费),系统支持创建临时档案。
- 分诊评估: 护士根据患者的主诉、生命体征(体温、脉搏、呼吸、血压、血氧饱和度)、疼痛评分等,填写结构化分诊评估单。系统内置了常见的分诊工具逻辑,如改良早期预警评分(MEWS)或急诊严重指数(ESI)的自动计算辅助。
- 分级与分区: 根据评估结果,系统自动建议分诊级别(如濒危、危重、急症、非急症),并推荐就诊区域(抢救室、诊室、观察区)。护士可以确认或调整。患者被分配到一个唯一的急诊号,并进入对应级别的候诊队列。
- 队列管理与大屏展示: 分诊台和大屏实时同步显示各队列的患者列表、等待时间、当前接诊医生等信息。系统支持“过号召回”、“优先安排”等队列调整操作。
技术实现要点:
- 实时推送: 大屏的实时更新是通过 WebSocket 实现的。当分诊台新增患者或队列状态变化时,后端通过 WebSocket 主动推送消息给所有连接的大屏客户端。
- 并发控制: 分诊高峰期可能出现多名护士同时操作。关键操作如“分配医生”使用了数据库乐观锁(通过版本号字段)或 Redis 分布式锁,防止同一患者被重复分配。
- 字典管理: 症状、体征、分诊级别等所有下拉选项,均通过后端字典表统一管理,前端通过接口动态获取,保证了数据的规范性和可维护性。
3.2 急诊电子病历与医嘱模块
这是医生工作的核心,要求快速、规范、可追溯。
病历书写: 我们采用了结构化病历模板与自由文本相结合的方式。对于现病史、既往史等,提供丰富的模板和常用短语库,支持点选和快速录入。体格检查部分则是高度结构化的表单,确保关键体征不遗漏。所有病历修改均留有操作日志。
医嘱管理: 医嘱分为长期医嘱和临时医嘱。系统实现了完整的医嘱生命周期管理:
- 开具: 医生开立医嘱,系统会实时进行合理性校验,如药品的剂量、频次、配伍禁忌(基于内置的合理用药知识库)、库存状态、患者过敏史等。
- 核对: 护士对医嘱进行核对,确认无误后执行。
- 执行: 护士执行医嘱(发药、治疗、检查),并在系统中记录执行时间和执行人。
- 停止: 医生停止医嘱。
技术实现要点:
- 前端富文本编辑器: 病历的自由文本部分,我们集成了
Quill编辑器,并对其进行了定制,防止 XSS 攻击,并支持保存为 HTML 或纯文本两种格式。 - 医嘱校验引擎: 这是一个相对独立的服务模块。它将药品、检查、治疗项目及其规则(成人/儿童剂量、性别限制、相互作用等)配置在数据库中。当医生保存医嘱时,Service 层会调用校验引擎,传入患者信息和医嘱内容,引擎返回校验结果和提示信息。这种设计使得业务规则易于维护和扩展。
- 事务一致性: 一个“开立医嘱并扣减库存”的操作,必须在一个事务内完成。我们使用 Spring 的
@Transactional来保证,如果扣减库存失败,则整个医嘱开立操作回滚。
3.3 急诊留观与费用管理模块
留观管理: 对于需要留观的患者,系统提供床位管理、留观病历、护理计划等功能。床位状态(空床、占用、待消毒)实时可视。护士可以排班并记录护理记录。
费用管理: 急诊费用产生点分散(药房、检验科、治疗室),需要实时汇总。
- 自动记账: 医嘱执行时(如发药、做检查),系统自动触发记账,将费用项目关联到患者账户。
- 预交金管理: 患者就诊时缴纳预交金,系统支持多次缴纳。所有消费实时冲抵预交金,余额不足时系统会对医生和护士进行提醒。
- 结算与发票: 支持医保结算、自费结算等多种方式。与市医保平台通过 HTTPS 接口进行实时对接,完成费用上传和医保报销计算。结算后,系统调用热敏打印机或针式打印机打印发票和费用清单。
技术实现要点:
- 分布式事务的妥协: 严格来说,“执行医嘱”和“记账”应该是一个分布式事务(涉及业务系统和财务系统)。但在实际中,我们采用了“最终一致性”的补偿机制。先确保医嘱执行成功,然后异步发送消息到消息队列(RabbitMQ),由独立的记账服务消费消息并完成记账。如果记账失败,会有告警并触发人工干预对账。这是在高性能和高一致性之间做的典型权衡。
- 接口集成: 与医保、LIS(检验系统)、PACS(影像系统)的接口是重点。我们为每个外部系统定义了清晰的接口协议(通常是 WebService 或 HTTP+XML/JSON),并使用 Apache HttpClient 或 FeignClient 进行调用。接口调用均有超时、重试和日志记录,确保稳定性。
- 数据一致性校验: 每日定时任务会跑批核对医嘱执行记录、费用明细和库存变动,生成对账报表,这是保障系统数据准确性的最后一道防线。
4. 数据库设计与关键表结构
数据库设计遵循第三范式,同时针对高频查询做了适当的反范式优化(如增加冗余字段以减少关联查询)。以下是几个核心表及其作用:
1.er_patient_visit(急诊患者就诊表)这是整个急诊流程的主线索表,每一条记录代表一次急诊就诊。
CREATE TABLE `er_patient_visit` ( `visit_id` varchar(32) NOT NULL COMMENT '急诊就诊ID,主键', `patient_id` varchar(32) NOT NULL COMMENT '患者ID,关联患者主索引', `triage_level` tinyint(4) NOT NULL COMMENT '分诊级别:1濒危,2危重,3急症,4非急症', `chief_complaint` varchar(500) DEFAULT NULL COMMENT '主诉', `visit_time` datetime NOT NULL COMMENT '就诊时间(分诊时间)', `doctor_id` varchar(32) DEFAULT NULL COMMENT '接诊医生ID', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1候诊,2就诊中,3留观,4离院,5死亡', `prepay_balance` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '预交金余额', `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`visit_id`), KEY `idx_patient_id` (`patient_id`), KEY `idx_visit_time` (`visit_time`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='急诊就诊记录';设计心得:
visit_id使用了雪花算法生成的分布式ID,避免自增ID在分库分表时的麻烦。status字段是驱动整个流程状态机的关键,所有业务操作都会校验当前状态是否允许。
2.er_medical_order(急诊医嘱表)
CREATE TABLE `er_medical_order` ( `order_id` varchar(32) NOT NULL, `visit_id` varchar(32) NOT NULL, `order_type` tinyint(4) NOT NULL COMMENT '医嘱类型:1药品,2检验,3检查,4治疗', `order_content` varchar(1000) NOT NULL COMMENT '医嘱内容(JSON格式,存储药品规格、剂量等详细信息)', `order_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1已开立,2已核对,3已执行,4已停止', `frequency` varchar(50) DEFAULT NULL COMMENT '频次(如BID)', `doctor_id` varchar(32) NOT NULL COMMENT '开嘱医生', `nurse_id` varchar(32) DEFAULT NULL COMMENT '执行护士', `create_time` datetime NOT NULL, `update_time` datetime NOT NULL, PRIMARY KEY (`order_id`), KEY `idx_visit_id` (`visit_id`), KEY `idx_status` (`order_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='急诊医嘱';设计心得:
order_content使用 JSON 格式存储动态的医嘱详情,这样在新增医嘱项目类型时,无需频繁修改表结构,提高了灵活性。但缺点是查询和统计时需要解析 JSON,对数据库有一定压力。对于需要复杂查询的字段,我们仍会拆分成单独的列。
3.er_fee_detail(急诊费用明细表)
CREATE TABLE `er_fee_detail` ( `fee_id` varchar(32) NOT NULL, `visit_id` varchar(32) NOT NULL, `order_id` varchar(32) DEFAULT NULL COMMENT '关联的医嘱ID', `fee_item_code` varchar(50) NOT NULL COMMENT '收费项目编码', `fee_item_name` varchar(200) NOT NULL COMMENT '收费项目名称', `unit_price` decimal(10,2) NOT NULL COMMENT '单价', `quantity` decimal(10,3) NOT NULL COMMENT '数量', `total_amount` decimal(10,2) NOT NULL COMMENT '总金额', `charge_time` datetime NOT NULL COMMENT '记账时间', `charge_status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1已记账,2已结算,3已退费', PRIMARY KEY (`fee_id`), KEY `idx_visit_id` (`visit_id`), KEY `idx_order_id` (`order_id`), KEY `idx_charge_time` (`charge_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='急诊费用明细';设计心得: 这张表是写多读多的表。除了常规索引,我们还按
charge_time做了按月分表(例如er_fee_detail_202401),以应对数据量的快速增长。查询时,由中间件或应用层根据时间范围路由到对应的物理表。
5. 关键代码实现与避坑指南
5.1 后端:基于SpringBoot的RESTful API设计
我们遵循 RESTful 风格设计 API,力求资源定义清晰,HTTP 方法使用得当。
示例:患者就诊查询接口
@RestController @RequestMapping("/api/er/visits") @Api(tags = "急诊就诊管理") public class ErVisitController { @Autowired private ErVisitService erVisitService; /** * 分页条件查询急诊就诊记录 * @param queryDTO 查询条件(患者姓名、时间范围、状态等) * @param page 页码 * @param size 每页大小 * @return 分页结果 */ @GetMapping @ApiOperation("分页查询就诊记录") public CommonResult<PageInfo<ErVisitVO>> queryVisitList(ErVisitQueryDTO queryDTO, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { // 1. 参数校验(使用JSR-303注解在DTO上已完成基础校验) // 2. 构造分页对象 PageHelper.startPage(page, size); // 3. 调用Service查询 List<ErVisitVO> list = erVisitService.queryVisitList(queryDTO); // 4. 包装分页结果 PageInfo<ErVisitVO> pageInfo = new PageInfo<>(list); return CommonResult.success(pageInfo); } /** * 根据ID获取就诊详情(包含患者信息、分诊信息等) * @param visitId 就诊ID * @return 就诊详情VO */ @GetMapping("/{visitId}") @ApiOperation("获取就诊详情") public CommonResult<ErVisitDetailVO> getVisitDetail(@PathVariable String visitId) { // 业务校验:visitId是否存在,当前用户是否有权限查看 ErVisitDetailVO detail = erVisitService.getVisitDetailById(visitId); if (detail == null) { return CommonResult.failed("就诊记录不存在"); } return CommonResult.success(detail); } /** * 更新就诊状态(如:开始就诊、留观、离院) * @param visitId 就诊ID * @param statusUpdateDTO 状态更新请求体 * @return 操作结果 */ @PutMapping("/{visitId}/status") @ApiOperation("更新就诊状态") public CommonResult updateVisitStatus(@PathVariable String visitId, @Valid @RequestBody VisitStatusUpdateDTO statusUpdateDTO) { // 此处包含复杂的业务逻辑和状态机校验 boolean success = erVisitService.updateVisitStatus(visitId, statusUpdateDTO); return success ? CommonResult.success(null) : CommonResult.failed("状态更新失败"); } }避坑指南:
- DTO/VO 分离: 坚决不要用 Entity 对象直接接收前端请求或返回给前端。我们定义了
QueryDTO用于接收查询参数,UpdateDTO用于接收更新请求,VO(View Object) 用于返回给前端。这保证了领域模型的纯净性和 API 的稳定性。- 全局异常处理: 使用
@ControllerAdvice和@ExceptionHandler定义全局异常处理器。将业务异常(如BusinessException)、参数校验异常、系统异常统一捕获,并转换为结构化的错误信息(CommonResult.failed(message))返回给前端。这避免了在 Controller 里写大量的try-catch。- 接口文档: 使用 Swagger2/3(通过
springfox或springdoc-openapi)自动生成 API 文档。在 Controller 和 Model 上使用@Api,@ApiOperation,@ApiModelProperty等注解,极大提升了前后端协作效率。
5.2 前端:Vue组件化开发与状态管理
示例:分诊队列组件 (TriageQueue.vue)
<template> <div class="triage-queue"> <el-table :data="queueData" style="width: 100%" stripe @row-click="handleRowClick"> <el-table-column prop="queueNumber" label="队列号" width="100"></el-table-column> <el-table-column prop="patientName" label="姓名" width="120"></el-table-column> <el-table-column prop="triageLevel" label="分级" width="80"> <template slot-scope="scope"> <el-tag :type="getLevelTagType(scope.row.triageLevel)"> {{ getLevelText(scope.row.triageLevel) }} </el-tag> </template> </el-table-column> <el-table-column prop="waitingTime" label="等待时间" width="100"> <template slot-scope="scope"> {{ formatWaitingTime(scope.row.waitingTime) }} </template> </el-table-column> <el-table-column prop="chiefComplaint" label="主诉" show-overflow-tooltip></el-table-column> <el-table-column label="操作" width="150"> <template slot-scope="scope"> <el-button size="mini" @click.stop="callPatient(scope.row)">呼叫</el-button> <el-button size="mini" type="primary" @click.stop="assignDoctor(scope.row)">分诊</el-button> </template> </el-table-column> </el-table> <!-- 分诊对话框 --> <triage-dialog :visible.sync="dialogVisible" :patient-info="selectedPatient" @success="onTriageSuccess"/> </div> </template> <script> import { mapState, mapActions } from 'vuex'; import TriageDialog from './components/TriageDialog.vue'; import { formatDuration } from '@/utils/date'; export default { name: 'TriageQueue', components: { TriageDialog }, data() { return { selectedPatient: null, dialogVisible: false }; }, computed: { // 从 Vuex store 中获取实时队列数据 ...mapState('triage', ['queueData']) }, mounted() { // 组件挂载时,开始轮询或建立WebSocket连接获取最新队列 this.fetchQueueData(); this.startPolling(); }, beforeDestroy() { this.stopPolling(); }, methods: { ...mapActions('triage', ['fetchQueueData', 'callPatient']), // 格式化等待时间 formatWaitingTime(seconds) { return formatDuration(seconds); }, // 获取分诊级别对应的标签类型和文本 getLevelTagType(level) { const map = { 1: 'danger', 2: 'warning', 3: 'primary', 4: 'info' }; return map[level] || 'info'; }, getLevelText(level) { const map = { 1: '濒危', 2: '危重', 3: '急症', 4: '非急症' }; return map[level] || '未知'; }, // 分诊操作 assignDoctor(row) { this.selectedPatient = row; this.dialogVisible = true; }, onTriageSuccess() { this.dialogVisible = false; this.fetchQueueData(); // 刷新队列 this.$message.success('分诊成功'); }, // 简单的轮询机制(实际项目可能用WebSocket) startPolling() { this.pollingTimer = setInterval(() => { this.fetchQueueData(); }, 10000); // 每10秒刷新一次 }, stopPolling() { if (this.pollingTimer) { clearInterval(this.pollingTimer); } } } }; </script>避坑指南:
- 组件拆分: 保持组件单一职责。
TriageQueue只负责展示队列和触发操作,具体的“分诊”弹窗逻辑封装在子组件TriageDialog中。这样父子组件通过props和$emit通信,结构清晰。- 状态管理: 像“分诊队列数据”这种多个组件都需要访问的全局状态,我们将其放在 Vuex 的
triage模块中。避免了复杂的组件间事件传递($emit/$on)。- 性能优化: 对于实时性要求高的大屏,轮询(Polling)不是最佳选择,它会给服务器带来不必要的压力。强烈推荐使用 WebSocket进行服务端推送。我们在生产环境就使用了
SockJS+Stomp来实现。示例中的轮询仅作演示。- 防抖与节流: 对于搜索框输入、窗口 resize 等频繁触发的事件,一定要使用防抖(debounce)或节流(throttle)函数,例如 Lodash 的
_.debounce,以提升性能。
5.3 前后端交互与安全
1. 认证与授权我们采用JWT (JSON Web Token)进行无状态认证。
- 登录: 用户输入工号密码,后端验证后,生成一个包含用户ID、角色等信息的 JWT Token 返回给前端。
- 后续请求: 前端将 Token 放在 HTTP 请求头的
Authorization: Bearer <token>字段中。 - 后端校验: 通过一个 Spring Security 的过滤器(或拦截器)来校验 Token 的有效性和权限。
权限控制在多个层面实现:
- 接口层面: 使用
@PreAuthorize("hasRole('DOCTOR')")或@PreAuthorize("hasAuthority('triage:write')")注解。 - 前端菜单/按钮层面: 根据用户角色动态生成路由和菜单,按钮通过
v-if="hasPermission('triage:write')"控制显示。
2. API 安全
- SQL 注入: 坚持使用 MyBatis 的
#{}预编译占位符,杜绝拼接 SQL 字符串。 - XSS 攻击: 后端对用户输入的富文本内容,使用 Jsoup 等库进行 HTML 过滤(白名单机制)。前端在显示时,使用 Vue 的
v-html要格外小心,或者对内容进行转义。 - CSRF 攻击: 由于我们采用前后端分离且使用 JWT,CSRF 风险较低,但仍在关键操作(如修改密码)上增加了额外的验证码或二次确认。
- 数据脱敏: 返回给前端的患者敏感信息(如身份证号、手机号)在序列化时进行部分脱敏处理(如
510***********1234)。
6. 部署、监控与性能调优
6.1 部署架构
生产环境部署采用经典的三层架构:
- 负载均衡层: 使用 Nginx 做反向代理和负载均衡,将请求分发到多个后端 SpringBoot 应用实例。同时,Nginx 也托管前端 Vue 的静态资源(
dist目录)。 - 应用层: 多个 SpringBoot 应用实例部署在独立的 Tomcat 或直接以
java -jar方式运行。通过 Nginx 的upstream实现负载均衡。 - 数据层: MySQL 采用主从复制(一主一从或多从),读写分离。应用层通过中间件(如 Sharding-JDBC)或配置多数据源来区分读写操作。Redis 作为缓存和会话存储(如果不用 JWT 无状态会话)。
6.2 监控与日志
- 应用监控: 集成 Spring Boot Actuator,暴露
/actuator/health,/actuator/metrics等端点,配合 Prometheus 和 Grafana 监控应用状态(JVM 内存、GC、线程池、接口 QPS、响应时间等)。 - 业务日志: 使用 SLF4J + Logback,将日志按级别(INFO, ERROR)和模块输出到不同文件。关键业务操作(如开立医嘱、结算)必须打印操作日志,包含操作人、时间、对象ID、前后状态变化,便于审计和问题追溯。
- 链路追踪: 在微服务化之前,我们通过在每个请求的 MDC (Mapped Diagnostic Context) 中放入一个唯一的
traceId,并在日志中打印该traceId,可以将一个用户请求在整个系统中的所有日志串联起来,极大方便了问题排查。
6.3 性能调优实战记录
问题1: 分诊大屏列表查询缓慢
- 现象: 高峰期,分诊大屏刷新列表的接口响应时间超过 2 秒。
- 排查:
- 查看慢查询日志,发现
SELECT * FROM er_patient_visit WHERE status = 1 ORDER BY triage_level ASC, visit_time ASC这条 SQL 在数据量达到 10 万条后变得很慢。 - 使用
EXPLAIN分析,发现虽然status和visit_time有索引,但ORDER BY涉及两个字段,且triage_level区分度不高(只有4个值),导致大量文件排序(filesort)。
- 查看慢查询日志,发现
- 解决方案:
- 查询优化: 大屏通常只关心当前活跃(状态为候诊、就诊中)的患者,且数量有限。修改查询为
SELECT ... WHERE status IN (1,2) ORDER BY visit_time ASC LIMIT 100。status IN (1,2)能有效利用索引,LIMIT减少了排序和传输的数据量。 - 引入缓存: 这个列表数据实时性要求高,但变化频率相对可接受(每分钟几次)。我们将查询结果放入 Redis 缓存,设置 30 秒过期。前端请求先读缓存,同时后端有一个定时任务每 20 秒刷新一次缓存。这样 99% 的请求都命中缓存,响应时间降到毫秒级。
- 索引优化: 为
(status, visit_time)建立了联合索引,覆盖了查询和排序条件。
- 查询优化: 大屏通常只关心当前活跃(状态为候诊、就诊中)的患者,且数量有限。修改查询为
问题2: 医嘱保存时,合理性校验耗时过长
- 现象: 医生保存复杂医嘱(包含十几种药品)时,页面会“卡住”几秒钟。
- 排查: 发现校验逻辑中,对每种药品都需要查询一次药品信息表、一次库存表、一次患者过敏史表,并且要进行复杂的规则匹配计算。串行执行导致总耗时累加。
- 解决方案:
- 批量查询: 将多个药品的查询条件合并,改为
SELECT * FROM drug WHERE id IN (?,?,...)和SELECT * FROM stock WHERE drug_id IN (?,?,...),将 N 次数据库交互减少为 2 次。 - 缓存预热: 将不经常变动的数据(如药品基本信息、配伍禁忌规则)在应用启动时加载到本地内存(如 Guava Cache)或 Redis 中,校验时直接内存查询,避免了数据库 IO。
- 异步与补偿: 对于最耗时的“合理用药知识库”深度校验,我们将其改为异步操作。医嘱先保存成功,然后发送一个消息到消息队列,由后台服务进行深度校验。如果校验出严重问题,再通过系统消息通知医生进行修正。这是一种“先通过,后严审”的折中方案,保证了开医嘱流程的流畅性。
- 批量查询: 将多个药品的查询条件合并,改为
7. 常见问题排查与解决方案速查
在实际开发和运维中,我们遇到了各种各样的问题。下面这个表格总结了一些典型问题及其排查思路和解决方案,希望能帮你少走弯路。
| 问题现象 | 可能原因 | 排查步骤 | 解决方案 |
|---|---|---|---|
| 前端页面打开空白,控制台报404 | 1. Nginx配置错误,未正确代理前端或后端请求。 2. 后端服务未启动。 3. 前端路由模式为 history,但Nginx未配置try_files。 | 1. 检查浏览器开发者工具Network面板,看具体哪个资源(JS/CSS/API)404。 2. 检查Nginx的 error.log和access.log。3. 直接访问后端API地址,看是否通。 | 1. 修正Nginx配置,确保静态资源路径和API代理路径正确。 2. 启动后端服务。 3. 对于Vue history模式,在Nginx location / 中添加try_files $uri $uri/ /index.html;。 |
| 登录成功,但后续接口请求返回401/403 | 1. 前端未正确携带Token(存储或发送问题)。 2. Token已过期。 3. 用户权限不足。 | 1. 检查浏览器Application/Local Storage或Cookie中Token是否存在。 2. 检查请求头 Authorization是否正确。3. 查看后端日志,确认JWT解析是否失败或权限校验未通过。 | 1. 确保登录后正确存储Token,并在axios拦截器中统一设置请求头。 2. 实现Token自动刷新逻辑(使用refresh token)。 3. 检查用户角色和接口要求的权限是否匹配。 |
| 页面数据不更新,或显示旧数据 | 1. 浏览器缓存了静态资源或API响应。 2. Vuex状态未及时更新。 3. 组件 key未正确设置,导致Vue复用错误组件。 | 1. 打开开发者工具Network,勾选Disable cache,刷新看是否正常。2. 检查Vue Devtools,查看Vuex中对应state的值。 3. 检查列表渲染时,是否为每个项设置了唯一的 :key。 | 1. 为Webpack输出文件添加hash后缀([name].[contenthash].js)。2. 在获取新数据后,正确提交mutation更新Vuex state。 3. 确保列表项的 key是唯一且稳定的(如ID),而不是数组索引。 |
| 数据库CPU或连接数异常升高 | 1. 存在慢查询。 2. 数据库连接未正确关闭(连接泄漏)。 3. 遭遇SQL注入攻击或恶意爬虫。 | 1. 开启MySQL慢查询日志,分析long_query_time以上的SQL。2. 使用 SHOW PROCESSLIST;查看当前连接和执行状态。3. 检查应用连接池配置(如HikariCP)和使用情况。 | 1. 优化慢SQL,添加合适索引,避免SELECT *,优化子查询。2. 确保所有数据库操作都在 try-with-resources(Java)或finally块中关闭连接/会话。3. 配置防火墙规则,限制非法IP访问;加强入参校验。 |
| 事务不回滚 | 1. 异常未被Spring捕获(如非RuntimeException)。2. 方法内部try-catch吞掉了异常。 3. 方法不是 public的,导致AOP代理失效。 | 1. 检查方法上是否有@Transactional注解。2. 检查抛出的异常是否是 RuntimeException或Error,或者是否在@Transactional(rollbackFor=Exception.class)中指定。3. 检查方法访问权限。 | 1. 确保抛出的是RuntimeException,或在注解中指定rollbackFor。2. 在catch块中,如果需要回滚,手动抛出异常或调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。3. 将事务方法定义为 public。 |
| 文件上传失败或大小受限 | 1. Spring Boot默认文件上传大小限制(1MB)。 2. Nginx客户端请求体大小限制。 3. 磁盘空间不足。 | 1. 查看后端日志,是否有MaxUploadSizeExceededException。2. 查看Nginx错误日志 ( error.log)。3. 检查服务器磁盘使用情况。 | 1. 在application.yml中配置spring.servlet.multipart.max-file-size和max-request-size。2. 在Nginx配置中增加 client_max_body_size 20m;。3. 清理磁盘或增加存储空间。 |
这个项目从零到一的过程,充满了挑战也收获颇丰。最大的体会是,技术永远是为业务服务的。再炫酷的技术,如果不能稳定、高效地支撑起“分秒必争”的急诊业务,都是空中楼阁。在编码之前,花足够的时间去理解业务流程、与医护人员沟通、设计表结构和接口,往往比后期埋头调优更重要。这套源代码和文档,希望能为你打开一扇窗,看到如何将 SpringBoot、Vue 这些技术,实实在在地应用到一个复杂且重要的行业系统中。如果你在运行或研究代码时遇到任何问题,欢迎随时交流讨论。
本文还有配套的精品资源,点击获取