news 2026/9/28 12:50:21

急诊科信息系统开发实战:Spring Boot+Vue实现分诊、看板与数据建模

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
急诊科信息系统开发实战:Spring Boot+Vue实现分诊、看板与数据建模

1. 急诊科的真实业务痛点与系统边界:为什么我要做这件事

我先说个背景。上半年帮一家市级医院做信息化改造,他们的急诊科还在用一套从门诊系统改过来的老系统,分诊靠护士手写小票,留观病人床位状态靠白板贴标签,检验危急值靠电话通知。听起来有点原始,但这是很多医院急诊科的真实状态。院方信息科提的需求很直接:能不能搞一套真正贴合急诊流程的系统,技术栈限定 Spring Boot + Vue,前后端分离,我这边负责整体架构和核心模块落地。

在动手之前,我先花了两周时间蹲在急诊科观察他们的工作流。这事比写代码重要得多,因为急诊科跟普通门诊的业务节奏完全是两个概念。门诊是一个病人一个号,按顺序叫号就行,但急诊讲究两个字:分级。来的人有濒危的、有危重的、有急症的、有非急症的,处置优先级完全不同。分诊护士要在几分钟内完成评估、分级、建档、分配诊区,系统如果在这一步拖后腿,那是要出事的。

再比如留观床位。急诊留观病区少则二三十张床,多则上百张,病人随时可能转住院、转ICU、离院,床位状态变化极快。护士长告诉我,老系统里床位是整个病区一个大列表,护士只能靠记忆判断哪张床是空的,经常出现两个护士同时安排同一张床的情况。所以新系统里,床位管理我直接做成了一张看板,按区、按房间、按床位逐级展开,每个床位卡片实时显示占用状态、病人病情分级、留观时长、医嘱执行情况,用颜色区分危急程度。这个设计思路,后面是沿着整个系统往下贯穿的。

医院急诊系统的核心价值,不是说把门诊系统换个皮,而是要把"分诊—就诊—留观—抢救—转归"这条链路上的特殊规则做进去。比如绿色通道管理,比如抢救室床位状态,比如危重病人转科交接记录,比如检验危急值提醒——这些是急诊独有或者特别强调的能力。本文把我实际落地的方案完整拆出来,包括数据库表设计、后端接口逻辑、前端页面组织、部署方案,按我踩过的坑和验证过的做法来写,给正在做同类项目的同学参考。

2. 项目工程规划与核心表结构:先把急诊特殊规则落到数据模型里

技术栈本身没什么新鲜的,Spring Boot 2.7 + MyBatis-Plus + MySQL 8 + Redis,前端 Vue 3 + Element Plus + ECharts + Vite。真正花心思的地方,是在数据模型上怎么体现急诊业务规则。数据模型如果设计得不对,后面写多少代码都白搭。

2.1 分诊记录表的设计:把分级评估结果变成可量化数据

分诊是急诊系统的源头,整条链路的数据都是从分诊记录开始的。我的triage_record表结构大体是这样的:

CREATE TABLE `triage_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', `visit_code` VARCHAR(32) NOT NULL COMMENT '就诊流水号,格式:JZ+yyyyMMdd+序号', `patient_name` VARCHAR(32) NOT NULL COMMENT '患者姓名', `patient_gender` TINYINT COMMENT '性别 1男 2女', `patient_age` INT COMMENT '年龄', `patient_phone` VARCHAR(20) COMMENT '联系电话', `chief_complaint` VARCHAR(255) COMMENT '主诉', `triage_level` TINYINT COMMENT '分级:1濒危 2危重 3急症 4非急症', `triage_score` INT COMMENT '分级评分', `arrival_time` DATETIME COMMENT '到诊时间', `triage_time` DATETIME COMMENT '分诊完成时间', `nurse_id` BIGINT COMMENT '分诊护士ID', `visit_status` TINYINT COMMENT '就诊状态:1待诊 2就诊中 3留观 4抢救 5转住院 6离院', `created_at` DATETIME, `updated_at` DATETIME ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个要点我需要专门说明一下。

第一,visit_code不用自增ID当流水号。急诊科医生和护士习惯了看"挂号编号"喊人,一个纯数字的自增ID放在场景里既不好记,也容易暴露当天接诊量。我用JZ + 日期 + 当日序号的格式,比如JZ20240612018,对医护人员来说扫一眼就能知道是今天的第18个病人,很直观。生成方式是在 Redis 里用INCR对当天的 key 自增,避免数据库层并发冲突。

第二,triage_level和triage_score分开存。分级是结果,评分是依据。分诊护士做评估的时候用的是《急诊预检分诊专家共识》里的标准,包含意识状态、呼吸、循环、疼痛、体温等维度的打分,最后加权得到一个分数,再映射到对应的等级。把分数存下来,后续如果要审计分诊质量、校对分级是否合理,都有据可查。系统的分级评分界面是把量表做成步骤条,每一维度做完勾选自动累加,最终护士确认后再落库。

第三,visit_status是整条就诊链路的状态机。从待诊到离院,每个环节流转都有专门的状态修改接口控制,不允许前端直接改这个字段。比如一个濒危分级的病人,分诊完成后系统自动生成一条抢救室占用记录,状态直接置为"抢救中"而不是"待诊",因为濒危病人不存在排队候诊这回事。这个规则是用一个策略类在服务端做的,避免有人误操作把危重病人置成普通候诊。

2.2 急诊看板相关的核心表:床位、设备与护士站的可视化基础

急诊护士站看板是这个系统里使用频率最高的页面,几乎永远挂在护士站的电视大屏上。它要展示三类核心信息:候诊区队列、抢救室床位状态、留观病区床位状态。支撑这些看板的表,除了前面说的triage_record,还有床位表sickbed和床位占用记录表bed_usage_record。

CREATE TABLE `sickbed` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `bed_code` VARCHAR(32) NOT NULL COMMENT '床位编码,如 LQ-B-01 表示留观B区01床', `bed_type` TINYINT COMMENT '床位类型:1抢救床位 2留观床位', `area_code` VARCHAR(32) COMMENT '所属分区编码', `use_status` TINYINT COMMENT '使用状态:0空闲 1占用 2消毒中 3停用', `department_id` BIGINT COMMENT '所属科室', `created_at` DATETIME ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `bed_usage_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `bed_id` BIGINT NOT NULL, `visit_code` VARCHAR(32) NOT NULL COMMENT '占用该床位的就诊流水号', `start_time` DATETIME COMMENT '占用开始时间', `end_time` DATETIME COMMENT '释放时间', `start_reason` VARCHAR(255) COMMENT '占用原因:如抢救、留观观察', `end_reason` VARCHAR(255) COMMENT '释放原因:如转住院、离院', `operator_id` BIGINT COMMENT '操作人员ID' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这块有两个实践细节我重点说一下。

一是床位占用和释放必须走事务,并且记录完整的生命周期。刚开始我只在sickbed表上做了一个简单的状态字段,后来发现运营分析需要知道"这张床这个月被占用了多少次、平均留观时长多久",但数据已经被后续操作覆盖掉了,根本查不出来。加上bed_usage_record之后,每一次占用和释放都有开始时间、结束时间、原因、操作人,既能支撑管理报表,也能做误操作追踪。查某个病人某段时间住过哪张床,直接搜visit_code就能拿到完整记录。

二是看板数据要用接口聚合,不要在小程序端或页面端拼接。前端的护士站看板是一个轮询接口,每30秒拉一次。这个接口在 Service 层做聚合,把候诊队列、抢救床位占用、留观床位占用、护士交班统计一次性拼好返回,前端拿到之后直接渲染,不在浏览器里做多接口并发汇总。原因是急诊大屏使用的终端机型配置普遍不高,有的一体机还是多年前的触屏款,前端做太多计算容易卡顿,而服务端聚合是几毫秒的事。这样虽然单次返回的 JSON 比较大,但实际渲染起来反而流畅。

2.3 医嘱与危急值模块:最容易低估复杂度的两张表

急诊的医嘱模块和住院医嘱有区别,急诊医嘱以临时医嘱为主,长期医嘱相对少,而且执行节奏极快,在抢救场景下医生开完医嘱,护士几乎是同时执行。我的emergency_order表设计简化如下:

CREATE TABLE `emergency_order` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `visit_code` VARCHAR(32), `order_no` VARCHAR(40) NOT NULL COMMENT '医嘱编号', `order_type` TINYINT COMMENT '医嘱类型:1药品 2检验 3检查 4治疗 5护理', `order_content` VARCHAR(500) COMMENT '医嘱内容', `status` TINYINT COMMENT '状态:1已开具 2已审核 3执行中 4已完成 5已作废 6已退费', `urgency_flag` TINYINT COMMENT '加急标记:0普通 1加急', `doctor_id` BIGINT, `nurse_id` BIGINT COMMENT '执行护士', `prescribed_at` DATETIME COMMENT '开立时间', `executed_at` DATETIME COMMENT '完成时间', `note` VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里需要提一个设计上的选择:为什么急诊医嘱不做成完整的住院医嘱套件?因为急诊的系统定位是"快",一个抢救病人可能在半小时内开十几条临时医嘱,每条从开具到执行只有几分钟。如果引入住院系统那种长期医嘱定期审核、拆分批次执行的重流程,急诊护士会疯掉。所以我做的是轻量模式:医生开立 -> 护士审核 -> 执行完成,三步,没有批次概念。如果有病人转住院,医嘱数据通过接口推送到住院系统,由住院端做转换。

危急值模块是有专门的回调接口的。检验设备出结果后,LIS(实验室信息系统)会把危急值结果推送到急诊系统的服务端接口,系统根据visit_code找到对应病人,生成一条危急值提醒记录,同时在护士站看板上弹窗闪烁,直到有医生或护士手动确认。这个确认动作在critical_value_record表里记录时间、确认人和处理意见,如果超过10分钟没人确认,系统会给护士长推送一条催办。这是急诊本职工作的一部分,不是说做个数据库表就够了,还要设计对应的提醒机制。

2.4 绿色通道和转归记录:被很多外包项目忽略的细节

绿色通道是急诊特有场景:胸痛、卒中、创伤等病种需要先救治后付费,所有检查和取药都走加急通道。我的设计中,分诊记录表增加了green_channel_type字段,当护士勾选"胸痛中心绿色通道"时,系统自动给这个病人的所有医嘱加上urgency_flag = 1,并在检验申请单、检查申请单上打上绿色通道标识,后续环节的系统(LIS、RIS)就能识别并优先处理。这个逻辑不是简单在业务层判断一下就行,还要考虑后续和院内集成平台对接时的数据映射,所以我把绿色通道类型做成独立的字典表,而不是硬编码成数字塞在代码里。

转归记录表visit_finish_record记录了每个病人离开急诊的方式:转住院、留观后离院、抢救无效死亡、自动离院(也就是患者自己走了)等场景。这张表的字段除了基本的visit_code、finish_type、finish_time,我还加了next_department_id(转入科室)、transfer_reson(转科原因)、operator_id。当时院方信息科要求转住院必须有明确的去向科室,否则财务结算和病历归档对不上账,这张表的存在让「一个急诊病人最终去了哪里」这件事变得可追踪、可统计。

字段编号的避坑:急诊科表单里大量使用"1、2、3"这种数值来代表业务状态,但直接在代码里用 magic number 非常容易出错。我的做法是全部走枚举类或者字典表,尤其在triage_level、visit_status、bed_type这三个核心字段上。MyBatis-Plus 可以用@EnumValue注解做枚举映射,但项目里老同事更习惯直接存TINYINT,代码里用常量类管理。两种方式都可以,看团队习惯,核心是禁止在 SQL 里裸写WHERE triage_level = 1这种没有语义的魔法数。

3. 后端接口层的设计思路与核心逻辑拆解

后端接口设计上,我没有采用市面上常见的纯 CRUD 风格,而是针对急诊的业务特征做了几个专门的接口聚合。原因很简单:急诊的页面大多是大屏展示,一次渲染需要的信息量大,如果让前端十几个接口来回调,网络开销和等待时间都不可控。

3.1 护士站看板聚合接口:一次返回整个急诊态势

这个接口路径是GET /api/emergency/board/overview,返回结构大致是:

{ "waitingQueue": [ { "visitCode": "JZ20240612018", "patientName": "张**", "triageLevel": 3, "waitingMinutes": 18, "chiefComplaint": "腹痛2小时" } ], "rescueBeds": [ { "bedCode": "RQ-01", "useStatus": 1, "visitCode": "JZ20240612010", "patientName": "李**", "triageLevel": 1, "doctorName": "王医生", "admissionTime": "2024-06-12 09:15:00" } ], "observationBeds": [ { "bedCode": "LQ-B-03", "useStatus": 0 } ], "criticalValueCount": 2, "unfinishedOrderCount": 5 }

服务端实现用的是CompletableFuture并行查询 + 聚合。这个接口需要同时查候诊表、抢救床位占用表、留观床位表、危急值表、医嘱执行表,如果把Redis缓存引入,还能进一步降低数据库压力。为了提高实时性,我加了@Scheduled定时任务,每10秒刷新一次 Redis 中的看板数据,前端轮询读到的其实已经是缓存数据,数据库压力很小。

CompletableFuture这块有一个容易踩的坑:并行查询要注意线程池的隔离。Spring Boot 默认的@Async线程池是SimpleAsyncTaskExecutor,每来一个任务就创建一个新线程,高并发下会炸。我单独定义了一个带ThreadPoolTaskExecutor的 Bean,核心线程数8、最大线程数16、队列容量50,并且设置了拒绝策略CallerRunsPolicy,意思是如果线程池满了,就退回到调用线程执行,保证不会丢任务。

3.2 分诊评分与就诊流转:策略模式处理不同分级的不同路径

分诊评分逻辑是一个典型的策略模式场景。不同分级对应的后续动作不一样:

  • 濒危(1级):立即进入抢救室,分配抢救床位,主责医生和护士接收站内信推送,状态置为抢救中。
  • 危重(2级):进入抢救区或优先就诊队列,系统自动排在候诊队列最前面。
  • 急症(3级):进入正常就诊队列,按到达时间排序。
  • 非急症(4级):进入普通就诊队列,如果候诊人数过多,会自动提醒分诊护士是否建议患者改挂门诊。

这个逻辑我用TriageLevelHandler接口加四个实现类来做的。Spring 启动后把四个实现类注入一个Map<String, TriageLevelHandler>,key 就是 level 对应的枚举名,调用时直接从 Map 里取对应的处理器,而不是写一长串 if-else。

后端的实现逻辑大概是这样:

@Service public class TriageService { private final Map<TriageLevelEnum, TriageLevelHandler> handlerMap; public TriageService(List<TriageLevelHandler> handlers) { this.handlerMap = handlers.stream() .collect(Collectors.toMap(TriageLevelHandler::level, Function.identity())); } @Transactional public TriageResult submitTriage(TriageRequest request) { TriageRecord record = buildRecord(request); triageRecordMapper.insert(record); TriageLevelHandler handler = handlerMap.get(record.getTriageLevel()); return handler.handleAfterTriage(record); } }

这样加新的分级规则时,只需要多写一个 Handler 类,不用改老代码,符合开闭原则。当然,对于中小项目,if-else 也不是不可以,但如果后续规则多起来了,策略模式的重构成本就很低了。

3.3 号源队列的公平性算法:急诊不是完全的先到先得

急诊候诊队列的排序规则其实比普通门诊复杂。非急症病人理论上按先来后到,但同一时刻可能既有非急症复诊病人,也有紧急程度稍高的病人,两者的排序需要动态权衡。查分护士在分诊时已经给了一个级别分,系统做队列排序时不直接用triage_level,而是用一个"动态优先级"字段,它在当前时间和分级分数的基础上计算出来:

priority = triage_score * 1000 - waiting_minutes

这个公式的思路是:分级分数是主导因素,级别高的永远排前面,等待时间只是同级别内部的次序参考。比如一个分级3分、已经等了30分钟的病人,和另一个分级4分、刚到5分钟的病人,前者的priority是 3000 - 30 = 2970,后者是 4000 - 5 = 3995,后者还是排在前面。如果两个病人是同分,那等待时间久的人得以及时被叫到,不至于长时间苦等。

这么做有一个好处:候诊队列里不会出现"一个危重病人来了,还必须等前面的非急症病人看完才轮到自己"这情况。当然,这是急诊系统特有的排序规则,普通门诊千万别这么干,会出纠纷。

为了保证队列长度不要无限堆积,每个医生的并行处理数量是有限的。系统在医生端展示候诊人员时,只推送当前医生负责队列中优先级最高的前10个患者,医生看诊完一个,下一顺位自然补上。这要求后端在状态流转时主动做一次队列推送,而不是等前端自己刷新。

3.4 MyBatis-Plus 分页插件的正确用法

项目里候选人列表、医嘱记录、交班记录这些页面都涉及分页。MyBatis-Plus 的PaginationInnerInterceptor是我用的分页插件,配置很简单:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); pageInterceptor.setMaxLimit(500L); pageInterceptor.setOverflow(false); interceptor.addInnerInterceptor(pageInterceptor); return interceptor; } }

有两个细节值得说。

第一,maxLimit必须设置。医院急诊的医嘱记录量不大,但作预览时稍不小心就会把整天的记录全部拉出来。设置maxLimit = 500L是一种兜底,前端页面最大一页显示50条,正常不会触到这个限制,但如果有人偷偷在请求里把 size 改成 99999,也不会把数据库拖垮。

第二,分页查询不要跟联表查询滥用。MyBatis-Plus 的分页插件对单表查询很高效,但一旦涉及多表 join,自动生成的 count 语句可能非常慢,甚至出现笛卡尔积导致 count 不准。我的做法是:涉及多表关联的查询,单独手写Page<XxxVO> selectXxxPage(Page<?> page, @Param("query") XxxQuery query)在 XML 里实现,专门写优化好的 count 语句,不让插件自动生成。急诊快节奏场景下,SQL 性能至关重要,多花几分钟手写是值得的。

4. 前端工程组织与 Vue 项目的落地做法

前端部分我用的是 Vue 3 组合式 API + Element Plus + Pinia + Vue Router。页面列表包括:分诊台(PC触屏)、护士站大屏、医生工作站、留观病区管理、绿色通道管理、报表中心、系统设置,总共约40个页面组件。

4.1 基于角色的动态路由与菜单权限

医院系统对权限要求严格:分诊护士不能打开医生工作站,护士长能看全部床位管理,科主任能看报表但操作不了医嘱。我的做法是在用户登录后,后端返回该用户的角色列表和对应的可访问路由名称集合,前端用router.addRoute()动态添加路由。

// 路由守卫中加载动态路由 router.beforeEach(async (to, from, next) => { const userStore = useUserStore(); if (!userStore.token) { next('/login'); return; } if (!userStore.routesLoaded) { const userInfo = await userStore.fetchUserInfo(); const routes = await userStore.fetchAccessRoutes(); routes.forEach(route => { router.addRoute(route); }); userStore.routesLoaded = true; next({ ...to, replace: true }); } else { next(); } });

动态路由有个隐藏问题:刷新页面时 Pinia 里的路由信息会丢,所以在App.vue的onMounted里加一个router.isReady()之后再挂载应用,避免首屏白屏。另外一个做法是直接把可访问路由存到localStorage里,刷新后先从本地读一遍,再向后端校验。二选一都行,反正核心是刷新后不能丢权限。

菜单权限和按钮权限是分开控制的。菜单用v-if+ 动态路由解决,按钮级别我用了一个自定义指令v-permission,比如"删除医嘱"这个按钮只有护士长和科主任能看见,普通护士看不到,以防止既有权限又误操作的风险。

4.2 数据可视化和大屏适配的踩坑记录

护士站大屏和医生工作站大屏是我重点投入的前端页面。数据可视化我用的是 ECharts,主要展示科室的候诊人数趋势、留观床位利用率、分级比例分布。ECharts 的init需要在容器宽高确定之后执行,这个常识大家都懂,但实际用的时候最麻烦的是大屏终端的分辨率不太统一——有的是1920×1080,有的是 3840×1080(双屏拼接)。我用的是scale适配方案,大屏页面根组件按 1920 × 1080 设计,然后用 transform 的 scale 等比缩放适配任意屏幕。

function screenAdapter() { const designWidth = 1920; const designHeight = 1080; const scaleX = window.innerWidth / designWidth; const scaleY = window.innerHeight / designHeight; const scale = Math.min(scaleX, scaleY); document.getElementById('board-root').style.transform = `scale(${scale})`; }

这里特别要提醒一个点:用 scale 缩放大屏时,页面里的坐标点击事件偶尔会有偏移。尤其是点位命中区域,如果缩了 scale,内部基于MouseEvent计算坐标的逻辑很可能错位。我踩过一次,后来直接把所有的点击命中区域都改成"用被缩放的根元素坐标做适配",避免用全局视口坐标。如果不想做这种复杂适配,也可以用viewport方案让页面实际渲染分辨率与视觉分辨率一致,但在双屏场景下 CSS 写法更绕。实测下来我选的scale方案护理人员和医生给出的反馈是"屏幕字够大了",效果符合预期。

前端播放视频监控的模块,当时也遇到了电子大屏接入的问题,具体是用video.js播 m3u8 流。医院急诊大屏要接走廊、抢救室的实时监控画面,摄像头流是 HLS 协议的 m3u8。Vue 里接 m3u8 流需要装video.js和他对应的videojs-contrib-hls插件,如果你们的流是通过 CDN 分发的,还要注意Cross-Origin Resource Sharing跨域的问题,后端网关要把媒体服务器的响应头配好,不然视频加载到一半就断掉了。

4.3 前后端分离部署中的环境配置与 Nginx 反代

项目使用标准的前后端分离架构,后端服务跑在 8080 端口,前端静态资源由 Nginx 托管,/api开头的请求反向代理到后端服务。生产环境的 Nginx 配置我贴一段核心的:

server { listen 80; server_name your-hospital-domain.com; root /opt/emergency-front/dist; index index.html; location / { try_files $uri $uri/ /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; proxy_set_header X-Forwarded-Proto $scheme; proxy_connect_timeout 60s; proxy_read_timeout 120s; } }

正确设置try_files是为了支持 Vue Router 的 history 模式,刷新页面不会 404。proxy_read_timeout 120s是因为急诊系统有些接口(比如报表导出Excel)耗时较长,默认的 60 秒不够用。还有一个细节:患者照片上传接口我单独放在/upload/路径下,不走/api反代,直接由 Nginx 静态目录处理,不过上传必须先通过后端的Spring Security会话认证,这个在上传组件的请求头里带了 token,由后端校验后才能写入。

4.4 关于 XSS 防护的一个特殊场景

项目里有一个文件上传模块,急诊医生有时会上传 PDF 病历或检验报告。有次安全测试的同事提醒我,上传的 PDF 文件内容里可能携带恶意脚本(PDF 里嵌 JavaScript),如果直接在浏览器预览时,可能触发 XSS。这个场景在处理时要注意两点:

一是后端全局过滤器需要对上传文件类型做白名单校验,不只校验扩展名,还要校验文件的 MIME 类型和文件内容的前几个字节(Magic Number),防止把 HTML 改名成 PDF 传上来。

二是前端预览 PDF 时,不要用iframe直接打开后端文件地址,因为 PDF 解析器对嵌入脚本的处理有很大差异,而是用pdf.js这类解析库在沙箱里渲染,不让 PDF 直接执行脚本。这个是安全测试那边给的建议,我如实记录在这里,给做类似项目的同学提个醒。

5. 部署、性能优化和运维层面的实战笔记

5.1 Docker Compose 编排:MySQL + Redis + 后端 + Nginx

医院现场交付时,我用的是一台物理服务器,系统是 CentOS 7,装了 Docker Compose 来做整套环境的编排。在 docs 目录下维护一个docker-compose.yml整体启动整套服务:

version: "3.8" services: mysql: image: mysql:8.0 container_name: emergency-mysql environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: emergency_db MYSQL_USER: emergency MYSQL_PASSWORD: emergency_pass volumes: - ./mysql-data:/var/lib/mysql - ./sql/init.sql:/docker-entrypoint-initdb.d/init.sql ports: - "3306:3306" redis: image: redis:7.0 container_name: emergency-redis ports: - "6379:6379" backend: build: ./backend container_name: emergency-backend depends_on: - mysql - redis environment: SPRING_PROFILES_ACTIVE: prod DB_HOST: mysql DB_PORT: 3306 DB_NAME: emergency_db DB_USER: emergency DB_PASSWORD: emergency_pass REDIS_HOST: redis ports: - "8080:8080" nginx: image: nginx:1.24 container_name: emergency-nginx ports: - "80:80" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf - ./dist:/usr/share/nginx/html depends_on: - backend

Docker Compose 虽然配置繁琐,但好处是医院信息科接手维护时,一条docker compose up -d就能把整个环境拉起来,不需要一个个装依赖。生产环境的 MySQL 数据卷要定期备份,我用的是一个 crontab 定时任务,每天凌晨两点执行一次 mysqldump 全量备份,保留最近7天,同时用 rsync 把备份文件同步到另一台备份机上。

另外还要注意一点:Docker Compose 的容器名不要随便改。医院内网环境可能同时跑着其他系统,如果容器名跟别的容器冲突,应用起不来还不好排查。我在交付文档里专门让信息科确认并对容器名做了统一前缀,比如em-mysql、em-backend,这套规范在维护时非常有用。

5.2 Spring Boot 配置的外部化:一份配置走遍开发与生产

医院开发环境、测试环境、生产环境至少要三套配置。我不建议把生产数据库密码写在application.yml里提交到 Git,我们采用的方式是application-{profile}.yml+ 环境变量覆盖的组合。

spring: datasource: url: jdbc:mysql://${DB_HOST:127.0.0.1}:${DB_PORT:3306}/${DB_NAME:emergency_db}?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false username: ${DB_USER:emergency} password: ${DB_PASSWORD:emergency_pass} redis: host: ${REDIS_HOST:127.0.0.1} port: ${REDIS_PORT:6379}

Spring 读取环境变量时如果取不到就用后面的默认值,这个机制很好用。开发环境不用配置任何环境变量就能直接跑起来(连本机 MySQL),生产环境用 Docker Compose 的 environment 段传入真实值,一份配置走遍所有环境。密码这类敏感信息还可以配置为从环境变量读取,不要在文件里明文。

启动时指定 profile,生产环境用:

java -jar emergency.jar --spring.profiles.active=prod

5.3 医院内网的 SQL Server / 金仓数据库兼容问题

项目需求虽然写的是 MySQL,但国内医院信息化建设有一个很常见的情况——部分区域的大型三甲医院用的是达梦、金仓这类国产数据库,或者历史遗留的 SQL Server。这个项目实际交付时没有遇到国产库的问题,但我提前在 MyBatis 层做了方言隔离,分页插件配置里只声明数据库类型为 MySQL,相关代码没有直接拼数据库特定的 SQL 语法。如果后续有内网部署到金仓数据库的需求,最稳妥的方式是单独维护一套针对金仓的 Mapper XML 和分页配置,不要指望一套代码自动兼容所有数据库。这部分我虽然没全做,但给团队留下了扩展点。

如果真要到国产数据库做迁移,我个人建议从这几个方面考虑:

  • 字段类型映射:MySQL 的DATETIME对应金仓的TIMESTAMP,TEXT对应CLOB。
  • 自增主键:MySQL 的AUTO_INCREMENT在金仓里要改成SERIAL或序列。
  • 分页语法:金仓支持LIMIT/OFFSET的写法跟 MySQL 基本一致,但部分版本要确认。

这些内容要提前做评估并写进技术方案里,避免交付时突然发现兼容性问题导致延期。

6. 测试与上线阶段的事项:急诊系统容不得含糊

6.1 模拟分诊演练:至少完整跑一遍"急救全流程"

项目功能开发完成后,我和医院的护士长、信息科、急诊科主任一起做了三轮模拟演练,分别按胸痛、脑卒中和多发性创伤三个场景走全流程。演练的核心不是验证功能存在,而是验证每个环节的状态流转、消息推送、医嘱联动是否符合真实业务时间预期。

胸痛场景的流程大概是:患者由120急救车送到 -> 分诊护士扫码读取患者基本信息 -> 初步评估为2级 -> 系统自动分配抢救床位并标记绿色通道 -> 主诊医生开心电图医嘱(加急) -> 护士执行 -> 系统推送给心电图室 -> 结果回报 -> 医生开溶栓医嘱 -> 护士核对执行 -> 记录时间 -> 稳定后转入留观室观察。

这里发现了一个重要问题: 最初的系统里,分诊护士给病人建档时必须先扫码读取急诊号,但120送来的胸痛病人往往没有带任何身份凭证,身份信息只能靠口头问。系统应该允许分诊护士先以"无名氏"身份建临时档案,快速开始抢救,后续再补录真实身份。这个需求在常规流程设计里没考虑到,但却是急诊系统的刚需。我后来在分诊表里加了is_temporary标志位,临时档案也能正常开医嘱、分配床位,等家属或病人确认身份后补录信息,后台做身份合并时工作上还不算太复杂。

6.2 并发与性能压测:护士站大屏不能卡

急诊科高峰时段可能有几十个病人同时在候诊和接受处置,护士站大屏必须稳定。我用 JMeter 做了一轮基准压测,主要关注两个接口:POST /api/emergency/triage(分诊提交)和GET /api/emergency/board/overview(看板聚合)。压测参数:200 并发线程,每线程循环 20 次,TPS 约 400。

分诊提交接口在无缓存情况下,TPS 大概 1200 左右,但看板聚合接口在没有 Redis 缓存之前是瓶颈,因为每次要 join 四张表,TPS 只有 50 上下,刚好符合大屏轮询的预计压力。加了 Redis 缓存(10秒过期)后,看板接口虽然 TPS 大幅提升,但存在一个隐患:极端情况下缓存可能为空(Redis 重启),此时瞬时所有轮询请求同时打到数据库,造成抖动。

我的解决方案是加了一层"缓存空窗保护":定时任务每5秒主动刷新一份看板数据到 Redis,前端请求直接读缓存,即使前端请求频率很高,数据库也毫无压力。如果 Redis 挂了,就降级到直接查数据库,用@Cacheable注解的 condition 属性做粗粒度的开关控制。

6.3 上线后的稳定性建议:不是交付完就没事了

项目上线后的前两周,我和医院信息科保持每日沟通,重点看两个指标:接口响应时间趋势和数据库连接数占用。急诊系统半夜也有病患流量,所以告警监控要 7×24 小时。

我配置了 Spring Boot Actuator 的/actuator/health探活,并在服务器上加了脚本把健康状态同步到监控平台。如果探活连续失败,自动重启容器并给值班人员发短信通知。医院内网环境如果要接入外部的监控 SaaS 平台比较麻烦,可以退而求其次,用 Grafana + Prometheus 做内网监控,覆盖指标包括 JVM 内存、GC 时间、接口响应时间、数据库连接池使用率等。

数据库连接池配置这块我说一个经验参数:HikariCP 的连接池 maximum-pool-size 不要盲目设大,MySQL 默认连接数上限是 151,设太大反而可能导致数据库端连接数打满。对急诊这种中低并发系统,我设置的 maximum-pool-size=20,minimum-idle=5,实测在压测和日常运行中都够用。

7. 我对这类项目的复盘与经验沉淀

做完这个项目,我自己最大的感受是:**技术难点其实不在 Spring Boot 和 Vue 本身,而在把医院急诊科的业务规则用代码准确表达出来。**技术层面遇到的所有问题都能在网上找到答案,但"分诊分级后不同级别病人应该走什么样的路径""绿色通道在数据上如何体现""留观床位状态变化如何保证不冲突""无名氏如何建立临时档案"这些问题,是网上搜不到现成方案的,只能深入现场、跟医护人员反复确认。

下面几条是我这次最想留给同行的经验:

第一,做医疗系统一定要留出"现场跟随调研"的时间。我两周的蹲点观察产生了十几个需求点,其中包括无名氏临时档案、动态优先级队列、床位占用生命周期记录这些关键设计要素,全是坐在电脑前拍脑袋想不出来的。如果你做的项目是医疗信息化方向,强烈建议至少安排一次完整的科室跟班,亲眼看看护士和医生是怎么工作的,哪怕多花几天也值得。

第二,急诊系统的模块边界要克制。我可以做完整电子病历、可以做复杂的排班系统、可以做药品库存管理,但实际交付范围是经过跟院方反复对齐的。急诊系统的核心是"分诊->就诊->留观->转归"链路,其他功能能集成就集成,不能集成的留好接口就行,不要试图做一个包罗万象的超级系统。功能边界清晰,交付才可控。

第三,接口设计时离业务操作越近,越要往"防错"的方向做。比如床位占用接口,除了判断床位是否空闲,还要校验当前占用记录是否已经正确关闭、是否有未完成的医嘱挂在这个病人名下。这类接口属于核心操作接口,不要过度信任调用方的参数,后端必须做完整的状态校验。有些开发同学喜欢追求代码简洁,觉得前端已经做过校验后端可以少写点,但在医疗场景下,后端是最后一道防线,多一点防御逻辑,上线后就少一分风险。

第四,代码规范层面,接口返回结构必须统一。我使用的是Result<T>统一返回对象,包含code、message、data三个字段,前端 axios 请求拦截器统一处理错误码。这个得益于团队的统一约定,很多小项目不重视这些,前后端联调时各写各的,调试和排查成本直线上升。

第五,关于 Spring Boot 自身的理解。网上关于 Spring Boot 自动装配原理的面试题很多,我实际开发中也确实从 Configure、Condition 注解的使用里得到了不少启发。比如我在这个项目的全局配置类里用@ConditionalOnProperty控制缓存开关:当emergency.cache.enabled=true时加载缓存相关配置,否则走直连数据库模式。这样做的好处是,在演示环境或者资源受限的内网测试机里,可以随时关掉缓存环节,少一个依赖就少一份风险。熟练掌握自动装配的思维模式,对做这些灵活配置特别有用。

这个项目从需求分析到上线,整体耗时大约三个月,后面我还会继续跟进移动端护理操作和医护排班的深度集成。如果你正好在做一个基于 Spring Boot + Vue 的医院相关项目,希望这篇文章里的表设计、接口思路、部署方案和那些踩出来的坑,能帮你少走几步弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 12:49:07

输电线路过热检测实战:YOLO11与YOLOv8双模型融合及RK3588部署

简介&#xff1a;这份资源面向电力巡检与目标检测方向的开发者、学生及科研人员&#xff0c;提供一套基于YOLO11与YOLOv8的输电线路过热检测完整方案&#xff0c;可用于大作业、课程设计或工程原型验证。压缩包共2000个文件&#xff0c;约407.5MB&#xff0c;包含1331个txt标注…

作者头像 李华
网站建设 2026/9/28 12:48:42

从零吃透ESKF:IMU状态估计的工程实践与C++实现

1. 从零吃透 ESKF&#xff1a;为什么 IMU 状态估计非它不可搞过 IMU 姿态解算的人都有一个共同的痛&#xff1a;原始陀螺仪积分漂得亲妈都不认识&#xff0c;加速度计噪声大得跟菜市场一样&#xff0c;磁力计在室内基本废掉。你拿这些数据直接做姿态融合&#xff0c;要么响应慢…

作者头像 李华
网站建设 2026/9/28 12:48:18

SVM面部识别实战:LBP特征+RBF核调参指南

简介&#xff1a;本资源是一份基于支持向量机&#xff08;SVM&#xff09;实现面部识别的完整实践项目&#xff0c;面向机器学习初学者与算法实践者&#xff0c;聚焦图像分类中的经典监督学习任务&#xff0c;尤其适用于人脸特征建模与小样本身份判别场景。压缩包共11个文件&am…

作者头像 李华
网站建设 2026/9/28 12:46:53

千笔AI降AIGC实操指南:从检测原理到改写技巧全拆解

标题是"直接上结论"&#xff0c;那我也就不绕弯子&#xff1a;对专科生来说&#xff0c;千笔AI是目前我用下来比较省心的降AIGC网站之一&#xff0c;但前提是你得明白降AIGC的原理、会用正确的姿势操作&#xff0c;而不是把全文甩进去点一下"开始"就完事。…

作者头像 李华
网站建设 2026/9/28 12:46:21

ViT/DeiT/SwinT量化加速:PTQ避坑与INT8部署实战

简介&#xff1a;面向需要将视觉Transformer模型部署到受限环境中的深度学习开发者&#xff0c;该资源针对ViT、DeiT与SwinT推理耗时长、显存占用高的问题&#xff0c;提供一套完整的PTQ后训练量化加速方案。包内含量化后的模型权重、可复现的量化流程教程与可运行项目源码&…

作者头像 李华
网站建设 2026/9/28 12:45:23

Allegro铜皮过期问题全解析:快速定位与清除Out of Date Shape实战指南

1. 铜皮过期问题到底是怎么回事1.1 从一个让人抓狂的场景说起做PCB Layout的朋友大概率都遇到过这种情况&#xff1a;板子改了好几轮&#xff0c;DRC也跑过了&#xff0c;光绘也出了&#xff0c;结果板厂反馈说某层铜皮和线路短路。回头一查&#xff0c;发现是一块早就该被删掉…

作者头像 李华