news 2026/9/11 12:20:16

SpringBoot+Vue前后端分离的医院急诊资源调度与可视化大屏系统实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue前后端分离的医院急诊资源调度与可视化大屏系统实践

这几年的毕业设计和实际落地项目里,SpringBoot + Vue 这套前后端分离组合基本成了标配。但大多数项目停在增删改查的层面,一旦涉及“资源调度”和“可视化大屏”,很多同学就卡住了。医院急诊资源调度与床位管理系统正好是这两点的典型结合体:既要处理急诊患者分诊、床位分配、流转记录这类核心业务,又要把数据实时汇总到可视化大屏上供值班人员决策。我基于自己做过的一个类似项目,把这个系统的搭建思路、表结构设计、调度逻辑和数据链路完整拆开讲一遍,顺便把踩过的坑也一并交代。

1. 急诊科的“白板困境”:先想清楚系统要解决什么

做系统最忌讳一上来就建表。我见过不少项目,表建了二十多张,代码写了一万多行,最后做出来的东西,护士站根本不用。核心原因只有一个:没有理解急诊科的真实工作场景。

急诊科和门诊、住院部最大的区别在于节奏。门诊是预约制,患者按时来,医生按时看,流程是线性的。住院部是计划制,患者病情相对稳定,床位周转有规律。急诊不是这样,患者的到达是随机的,病情严重程度差别极大,可能同时来一个胸痛的患者和一个手指划伤的患者,前者需要立刻进抢救室,后者只需要清创缝合。传统的白板管理模式靠护士手工记录床位占用情况,信息滞后、容易出错,碰上突发事件整个急诊科就乱了。

所以这个系统的核心价值不是“管理数据”,而是“辅助决策”。

时间就是生命,在急诊场景下不是一句口号。系统需要回答几个问题:

  • 当前还有多少空床?分布在哪个区?
  • 抢救室、观察室、留观区分别有多少患者?
  • 哪些患者已经等了很久,需要优先处理?
  • 哪个区域的压力最大,需要调配资源?

可视化大屏在这个系统里不是装饰品,而是调度指令的载体。大屏上显示的是实时数据,背后的逻辑是:护士站分诊台录入患者信息后,系统自动判断危重等级,推荐安置区域;医生在处置过程中更新患者状态,床位自动释放或占用;大屏上的数字实时刷新,管理人员一眼就能看出全局态势。

这套系统的技术栈选择其实非常务实。

层次技术选型选择理由
后端SpringBoot 2.x / 3.x生态成熟,快速开发,内置Tomcat
前端Vue 3 + Element Plus组件化开发,表格和表单效率高
可视化ECharts 5开源免费,图表类型全,实时刷新方便
数据库MySQL 8.x关系型数据模型清晰,事务支持好
权限Spring Security + JWT前后端分离场景下的标准方案

这套组合的优点是上手快、坑少、资料多。不管是毕设还是真实项目,遇到问题基本都能搜到解决方案。

2. SpringBoot + Vue 前后端分离的项目骨架搭建

项目骨架的搭建本身不复杂,但有几个细节值得注意。我先说后端,再说前端,最后说两者怎么对接。

2.1 SpringBoot 后端:从依赖到分层

创建 SpringBoot 项目,除了基础的 Web 依赖,还需要引入 MyBatis-Plus、MySQL 驱动、Lombok、JWT 相关依赖。MyBatis-Plus 在这个项目里能省很多事,尤其是分页查询和条件构造器,写起来非常顺手。依赖版本上建议不要追求最新,SpringBoot 2.7.x 搭配 MyBatis-Plus 3.5.x 是验证过的稳定组合。

分层结构按照惯例来:controller、service、mapper、entity、dto、vo、config、utils。很多初学者分不清 DTO 和 VO 的区别,我这里简单说清楚:DTO 是前端传给后端的参数对象,VO 是后端返回给前端的响应对象,两者不能混用。比如分诊登记接口,前端传过来的是一个包含患者姓名、身份证号、主诉、初步评估等级的 DTO,后端经过业务处理后,返回的是一个包含挂号单号、预检分级、建议安置区域、当前候诊人数的 VO。

后端接口设计遵循 RESTful 风格,资源用名词复数,操作通过 HTTP 方法区分。比如:

GET /api/beds # 获取床位列表(支持条件查询) POST /api/patients/registration # 急诊分诊登记 PUT /api/beds/{id}/allocate # 分配床位 PUT /api/beds/{id}/release # 释放床位 GET /api/dashboard/overview # 可视化大屏汇总数据 GET /api/dashboard/trend # 近24小时急诊量趋势

统一返回结果用 Result 对象包装,包含 code、message、data 三个字段。这样前端可以统一处理响应,不需要每个接口单独判断。异常处理用 @RestControllerAdvice 全局捕获,业务异常用自定义 BizException,参数校验用 javax.validation 注解。这一整套下来,代码会非常干净。

2.2 Vue3 前端:从脚手架到路由和状态管理

前端我用 Vue 3 + Vite 而不是 Vue CLI,原因很简单:Vite 启动速度快了不止一个量级,开发体验好太多。创建项目之后,需要安装 vue-router、pinia、axios、element-plus、echarts 这几个核心依赖。

项目目录结构按功能模块划分,和传统的前端目录不太一样。因为这个系统有普通业务页面和可视化大屏两种形态,所以我把两者分开:

src/ ├── api/ # 接口请求封装 │ ├── bed.js │ ├── patient.js │ └── dashboard.js ├── views/ # 页面 │ ├── register/ # 分诊登记 │ ├── bed/ # 床位管理 │ ├── dispatch/ # 调度台 │ └── screen/ # 可视化大屏 ├── components/ # 公共组件 ├── stores/ # Pinia 状态 └── router/

路由配置这里有一个大屏场景的特殊处理。可视化大屏通常是投到电视或大显示器上的,不需要左侧菜单和顶部导航,是全屏沉浸式的。所以大屏路由放在一个单独的 layout 下,不继承主布局组件,并且设置meta: { fullscreen: true }

axios 封装的关键是拦截器:请求拦截器带上 JWT Token,响应拦截器统一处理 401 跳转登录、业务错误提示。大屏页面用的数据接口要单独封装一个轮询请求,后面我会专门讲。

2.3 前后端联调:CORS 和代理配置

前后端分离项目第一个遇到的坑必然是跨域。SpringBoot 后端要开启 CORS 配置,允许指定来源的跨域请求:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOriginPattern("*"); config.addAllowedMethod("*"); config.addAllowedHeader("*"); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

这里要注意setAllowCredentials(true)的情况下,addAllowedOrigin不能写*,要用addAllowedOriginPattern("*")或者指定具体域名。否则前端带着 Cookie 请求的时候会报错,这个坑我踩过不止一次。

前端开发环境下用 Vite 的代理配置就行,把/api开头的请求转发到后端地址,既避免了跨域,也让代码里不会写死接口地址:

// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样生产部署的时候,只改一行代理配置或者用 Nginx 转发,前端代码完全不用动。

3. 核心数据模型:患者、床位、调度这条链怎么建表

数据模型是整个系统里最需要花心思的部分。表结构设计得好不好,直接决定后面业务逻辑怎么走。我按业务链的顺序来拆:先患者,再床位,最后是两者的关联——调度记录。

3.1 患者主表和分诊信息分开:身份信息和医疗过程解耦

患者信息我拆成了两张表:patienttriage_record。原因是同一患者可能多次来急诊,如果每次来都复制一遍姓名、身份证、联系方式,数据冗余不说,后续统计历史就诊次数也会出错。

patient 表的核心字段:

CREATE TABLE `patient` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL COMMENT '姓名', `gender` TINYINT COMMENT '性别 1男 2女 0未知', `phone` VARCHAR(20) COMMENT '联系电话', `id_card` VARCHAR(18) COMMENT '身份证号', `birth_date` DATE COMMENT '出生日期', `blood_type` VARCHAR(10) COMMENT '血型', `allergy_history` VARCHAR(255) COMMENT '过敏史', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_id_card` (`id_card`) ) COMMENT='患者基础信息表';

身份证号做了唯一索引,这是患者主索引的自然选择。但要注意,急诊场景下经常有没带身份证的患者,所以身份证允许为空,业务上通过姓名+手机号匹配历史记录,匹配不上就新建。

triage_record 表是急诊的核心表:

CREATE TABLE `triage_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `patient_id` BIGINT NOT NULL COMMENT '患者ID', `visit_no` VARCHAR(32) NOT NULL COMMENT '就诊流水号', `triage_level` TINYINT NOT NULL COMMENT '分诊级别 1-4级', `chief_complaint` VARCHAR(255) COMMENT '主诉', `current_illness` TEXT COMMENT '现病史', `vital_signs` VARCHAR(500) COMMENT '生命体征 JSON', `arrival_time` DATETIME NOT NULL COMMENT '到诊时间', `triage_time` DATETIME NOT NULL COMMENT '分诊时间', `triage_nurse` VARCHAR(50) COMMENT '分诊护士', `status` TINYINT DEFAULT 0 COMMENT '状态 0待诊 1已就诊 2已离院 3已入院 4已转院', `suggested_area` VARCHAR(20) COMMENT '建议安置区域', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, KEY `idx_patient_time` (`patient_id`, `arrival_time`), KEY `idx_triage_time` (`triage_time`) ) COMMENT='急诊分诊记录表';

分诊级别 1-4 级对应医疗上通用的急诊预检分诊规则:1级是濒危(需要立即抢救),2级是危重(需要尽快处置),3级是急症(可以等待但需要优先),4级是非急症(可以按顺序等待)。这个字段是整个调度逻辑的核心输入,后面我会详细讲怎么用它驱动床位分配。

就诊流水号visit_no我用了时间戳加随机数的生成方式,格式类似ER20250210143000123,保证唯一性和可读性。

3.2 床位表:状态机和区域划分是调度的基础

床位表的设计关键在于状态机。一张床从“空闲”到“被占用”再到“释放”,中间要经过哪些状态,直接影响业务流程和统计口径。

CREATE TABLE `bed` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `bed_code` VARCHAR(20) NOT NULL COMMENT '床位编号', `area` VARCHAR(20) NOT NULL COMMENT '所属区域:抢救室/观察室/留观区', `bed_type` VARCHAR(20) COMMENT '床位类型:普通床/抢救床/监护床', `status` TINYINT DEFAULT 0 COMMENT '状态 0空闲 1占用 2清扫中 3维修中 4预留', `current_patient_id` BIGINT COMMENT '当前占用患者ID', `current_record_id` BIGINT COMMENT '当前占用记录ID', `equipment_tags` VARCHAR(100) COMMENT '设备标签:心电监护/呼吸机/吸氧', `remark` VARCHAR(255) COMMENT '备注', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY `uk_bed_code` (`bed_code`), KEY `idx_area_status` (`area`, `status`) ) COMMENT='床位信息表';

我把“清扫中”作为一个独立的中间状态,是因为急诊床位的周转效率直接决定科室的接诊能力。患者刚走的时候,床位物理上是空的,但还不能马上接收新患者,需要消毒、更换床单,这个过程通常要 10-15 分钟。如果不区分这个状态,大屏上显示的空床数就会虚高,护士安排患者后发现床还没清理完,调度就乱了。

区域的划分要结合实际科室布局。一般急诊科分为红区(抢救区)、黄区(观察区)、绿区(留观区),对应不同的病情严重程度和监护级别。床位的区域和类型字段可以用来实现“按需分配”的策略。

3.3 调度记录表:每一次床位变动都留痕

调度记录表是整个系统的“账本”,记录每一次床位分配、释放、转区的操作。这张表的价值有两个:一是流程追溯,出了问题能定位是谁在什么时间做了什么操作;二是统计分析,床位的周转率、平均占用时长、各区域的使用率都是从这张表计算出来的。

CREATE TABLE `bed_allocation_record` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `record_no` VARCHAR(32) NOT NULL COMMENT '调度记录编号', `triage_record_id` BIGINT NOT NULL COMMENT '分诊记录ID', `patient_id` BIGINT NOT NULL COMMENT '患者ID', `bed_id` BIGINT NOT NULL COMMENT '床位ID', `area` VARCHAR(20) COMMENT '安置区域', `action_type` TINYINT COMMENT '操作类型 1分配 2释放 3转区 4锁定预留', `from_area` VARCHAR(20) COMMENT '来源区域(转区时用)', `from_bed_id` BIGINT COMMENT '来源床位(转区时用)', `operator` VARCHAR(50) COMMENT '操作人', `operator_role` VARCHAR(20) COMMENT '操作人角色:护士/医生/管理员', `operation_time` DATETIME COMMENT '操作时间', `remark` VARCHAR(255) COMMENT '备注', KEY `idx_bed_time` (`bed_id`, `operation_time`), KEY `idx_patient` (`patient_id`), KEY `idx_triage` (`triage_record_id`) ) COMMENT='床位调度记录表';

这套表的关联逻辑是:患者到急诊 → 分诊产生 triage_record → 分配床位写入 bed_allocation_record 且更新 bed 表状态 → 患者离院或转科释放床位再次写入记录。任何一个时间点,都能通过 bed 表的 current_record_id 反查当前占用记录,也能通过 allocation_record 表还原一张床的完整生命周期。

3.4 为什么要加 equipment_tags 字段:急诊的“床位”不只是床

这个字段的设计来自实际观察:急诊科的抢救床位往往配备心电监护仪、呼吸机、吸氧装置等设备。同样是“空床”,带呼吸机的床和不带呼吸机的床,接收患者的能力完全不同。可视化大屏上如果只显示“有空床”,不足以支撑调度决策。

所以我在分配算法里加了一个匹配逻辑:分诊级别为 1 级的患者优先分配带完整抢救设备且位于抢救区的床;分诊级别为 3 级的患者分配到观察区的普通床。设备标签用逗号分隔存储,查询时用FIND_IN_SET或者直接 LIKE 匹配即可。这种设计虽然不够规范化,但业务上足够实用,也减少了关联表的复杂度。

4. 分诊与调度:把医疗规则落成可执行代码

分诊是急诊信息系统的灵魂。这一块如果只做简单的增删改查,系统就没有灵魂。我花了不少时间研究怎么把分诊规则和床位分配逻辑落到代码里,分享下关键思路。

4.1 四级分诊规则的前端表达和后端校验

分诊这件事,规则本身不复杂,难的是怎么落地。前端的表达用表单加颜色标识:1级红色、2级橙色、3级黄色、4级绿色,护士一眼就能识别。后端要做的是基于关键指标的自动分级辅助,主要看生命体征里面的体温、心率、呼吸频率、血压、血氧饱和度几项。

可用条件判断的方式做自动分级。比如收缩压低于 80mmHg 或血氧饱和度低于 90%,至少判为 2 级;意识模糊判为 1 级或 2 级;单纯的手指划伤但生命体征平稳,判为 4 级。规则代码大致如下:

public TriageLevel autoTriage(TriageRequest request) { VitalSigns vitalSigns = request.getVitalSigns(); String complaint = request.getChiefComplaint(); // 意识状态判断 if ("昏迷".equals(request.getConsciousness()) || "嗜睡".equals(request.getConsciousness())) { return TriageLevel.LEVEL_1; } // 生命体征阈值判断 if (vitalSigns.getSpo2() != null && vitalSigns.getSpo2() < 90) { return TriageLevel.LEVEL_1; } if (vitalSigns.getSystolic() != null && vitalSigns.getSystolic() < 80) { return TriageLevel.LEVEL_1; } if (vitalSigns.getHeartRate() != null && vitalSigns.getHeartRate() > 140) { return TriageLevel.LEVEL_2; } // 主诉关键词判断 if (complaint.contains("胸痛") || complaint.contains("呼吸困难")) { return TriageLevel.LEVEL_2; } // 默认级 return TriageLevel.LEVEL_3; }

注意这是“辅助”而不是“替代”,最终分诊级别还是要护士确认。自动分诊的作用是兜底,防止护士在忙乱中漏掉危重患者。

4.2 床位分配算法:从“先来先得”到“分级优先”

急诊场景下床位分配必须“分级优先,兼顾等待时间”。同样是等床位的患者,1 级的必须优先于 4 级的,但 3 级患者如果等了很久,权重也应该提高,防止“插队”导致医患矛盾。

我设计了一个积分排序模型。每个候诊患者计算一个优先级积分:

priorityScore = (6 - triageLevel) * 100 + waitMinutes * 0.5

1 级患者天生拿 500 分,2 级拿 400 分,3 级拿 300 分,4 级拿 200 分,等待时间每分钟加 0.5 分。这样 3 级患者等 400 分钟能追上 2 级的初始分,但永远不会超过 1 级患者,因为 1 级的初始分太高了。这个算法既保证危重优先,又兼顾公平。

分配床位时按优先级积分从高到低遍历候诊列表,对每个患者找到匹配的床位。匹配逻辑分三步:

  1. 过滤:筛出当前状态为空闲且区域、类型满足患者分级的床位
  2. 排序:按设备匹配度降序,带完整设备的优先
  3. 兜底:同级区域内没有合适的床,向上寻找可用资源

这一步用 SQL + Java 双重过滤:先用 MyBatis-Plus 的条件构造器把 SQL 层的区域和状态条件筛掉,再在 Java 层用 Stream 做设备匹配和排序。代码量不大,但逻辑清晰,调试也方便。

4.3 并发分配:一次性的事务和行锁问题

多护士同时操作的时候,不能出现“同一张床被分配给两个患者”的情况。Spring 事务可以保证原子性,但单纯的@Transactional不能防止并发条件下的“查-改-写”竞争。我用的方案是乐观锁 + 条件更新

分配床位的 SQL 更新带上状态条件:

UPDATE bed SET status = 1, current_patient_id = ?, current_record_id = ? WHERE id = ? AND status = 0

如果影响行数为 0,说明床位已被其他人抢走了,重新选床即可。这是数据库层的原子操作,比 Java 层加锁更可靠,也不会扛住性能。结合事务里的SELECT ... FOR UPDATE锁定分诊记录,整个分配流程就是安全的。

4.4 调度逻辑的异常处理:没有床位的兜底方案

真实世界里床位就是可能满的。调度系统必须设计“没有床位怎么办”的兜底方案。我在系统里做了三个动作:

  1. 提示候诊队列中当前最高优先级的患者及等待时间,建议医生评估是否转入其他区域
  2. 生成“床位紧张预警”,在大屏上以橙色高亮显示当前区域压力
  3. 自动检查观察区是否有“可以转入住院部但尚未办理手续”的患者,提示护士尽快完成院内流转

第三个动作的实质是分析 triage_record 中状态为“已就诊”且停留时间超过 8 小时的患者,把这类患者标记为“滞留”,因为他们已经不需要急诊资源但还占着床位。这个数据的价值很大,能帮管理员找到流程中的堵点。

5. 可视化大屏:从数据库到图表的一条龙数据链路

可视化大屏是这个项目的加分项,也是最容易做得“看起来很炫但没什么用”的部分。我的原则是:每一个数字都要有业务含义,每一个图表都要能支撑调度决策

5.1 大屏布局:看什么、在哪看、多大字号

大屏的使用场景是挂在急诊护士站或者科主任办公室的墙上,观看距离在 2-5 米。这个距离下人眼对小字号完全不敏感,所以大屏的设计规范和普通后台页面完全不同:

  • 背景深色(深蓝或深灰),降低长时间注视的视觉疲劳
  • 主标题字号 24-32px,指标数字字号 40-48px,图例文字 16-20px
  • 色彩用亮色点缀,但同一屏不超过 3 个主色调
  • 布局用栅格系统,宽度按 1920 设计,必要时做等比缩放

我的大屏分了四个区域:顶部是标题和当前时间,左侧是急诊量趋势和分诊级别分布,中间是核心指标(当前空床数、候诊人数、平均候诊时间、最大滞留时长)和床位使用情况热力图,右侧是各区域床位状态列表和待分配患者队列。信息密度适中,值班人员扫一眼就能掌握全局。

5.2 数据接口设计:一个接口还是多个接口?

大屏的数据获取我用了多个接口而不是一个大聚合接口。原因是每个图表的数据刷新频率不同:核心指标和床位状态 10 秒刷一次,趋势图 5 分钟刷一次,患者队列 30 秒刷一次。如果合在一个接口里,刷新频率必须取最小值,后端压力会大不少。

接口设计按业务拆:

@RestController @RequestMapping("/api/dashboard") public class DashboardController { @GetMapping("/overview") public Result<OverviewVO> overview() { // 返回核心指标:总床位数、空床数、候诊人数、平均候诊时长 } @GetMapping("/area-status") public Result<List<AreaStatusVO>> areaStatus() { // 返回各区域的床位状态统计 } @GetMapping("/trend") public Result<TrendVO> trend(@RequestParam Integer hours) { // 返回指定小时范围内的急诊量和入观量 } @GetMapping("/waiting-queue") public Result<List<WaitingPatientVO>> waitingQueue() { // 返回当前候诊患者列表,按优先级排序 } }

这套接口拿到数据后,前端各自维护定时器,互不干扰。前端轮询代码写在每个图表组件自己的生命周期里,组件销毁时清掉定时器,避免内存泄漏。

5.3 ECharts 核心图表的实践

大屏上用到最多的五个图表:折线图(急诊量趋势)、环图(分诊级别分布)、横向条形图(各区域床位占用)、数字翻牌器(核心指标)、热力图或地图(地区分布,如果数据允许)。

折线图我推荐用堆叠面积图,把“新到达”“已接诊”“已离院”三条线画在同一张图上,能直观看出急诊科的处理效率是否跟得上到达量。实现时注意 ECharts 的updateData方法不要整个setOption,否则图表会闪烁,体验很差。用myChart.setOption(option, true)的第二个参数控制是否合并更新。

床位使用情况建议用“数字翻牌器 + 进度条”的组合。空床数用翻牌器突出显示,各区域的占用率用横向进度条表示,超过 90% 自动变红。这个组合比单纯一个饼图实用得多,因为调度人员需要看到的是“哪里快满了”,而不是“哪里占了多大比例”。

3D 地区的可视化大屏虽然视觉效果出色,但数据准备工作量大,而且没有真实的地理数据支撑,做出来往往是空壳。我建议这个项目聚焦内部调度,3D 地图的说法看看就好,别当真往项目里塞。

5.4 大屏实时刷新的技术选型:轮询 vs WebSocket

大屏数据实时性用什么方案?我分别说下这两种方案的适用性:

轮询(Polling):实现简单,代码量少,适合数据变化不频繁的场景。10 秒一次的轮询对后端几乎没有压力,MySQL 查几个聚合字段也就几毫秒的事。缺点是实时性稍差,但急诊科的大屏 10 秒延迟完全能接受。

WebSocket:实时性好,但需要后端维护长连接,代码复杂度高。如果项目里已经有消息推送的需求,可以一起用;如果只是为了大屏,有点杀鸡用牛刀。

我的选择是先用轮询,等项目跑起来再考虑要不要升级 WebSocket。这不是技术妥协,而是务实的决策逻辑:轮询能解决 90% 的问题,剩下 10% 的极端实时性需求在当前场景下并不存在。

用轮询还有一个额外的好处:接口天然是 HTTP 的,测试和排查问题非常方便,直接浏览器里打开GET /api/dashboard/overview就能看数据是否正确。

前端轮询的代码封装成自定义 Hook :

import { onMounted, onUnmounted, ref } from 'vue' export function usePolling(fetcher, interval = 10000) { const data = ref(null) let timer = null onMounted(async () => { await load() timer = setInterval(load, interval) }) const load = async () => { data.value = await fetcher() } onUnmounted(() => { clearInterval(timer) }) return { data, reload: load } }

用的是 Vue 3 的组合式 API,每个图表组件里面调用这个 Hook,传进去对应的 fetcher 和刷新间隔就完事了。

5.5 大屏适配:从 1920 到任意分辨率

大屏的适配方案我推荐用 CSS3 的transform: scale()配合设计稿 1920×1080 来实现。先把大屏容器固定成 1920×1080,然后用 JS 计算实际屏幕和设计稿的比例,对容器做等比缩放:

function scaleScreen() { const designWidth = 1920 const designHeight = 1080 const ratio = Math.min( window.innerWidth / designWidth, window.innerHeight / designHeight ) document.querySelector('.screen-wrapper').style.transform = `scale(${ratio})` } window.addEventListener('resize', scaleScreen)

这种方案的优点是开发时完全按 1920×1080 做,不用写一堆媒体查询和 rem 计算,显示效果稳定。缺点是屏幕比例和 16:9 差太远时,上下或左右会有留白,不过对医院这种标准 16:9 显示器适配足够。

6. 上线前避坑:CORS、时区、并发分配这些老问题

项目做得差不多要部署上线了,但这个阶段恰恰是最容易出问题的时候。我把自己踩过的一些坑整理一下,给后面做类似项目的人做个参考。

6.1 时区问题导致的时间错乱

第一个坑在 MySQL 连接串上。如果connectionTimeZone没有设置成Asia/Shanghai,入库的时间会自动被转成 UTC,查出来的时候少了 8 小时。大屏上显示的时间比实际时间早 8 小时,值班护士一看就会质疑数据准确性。

MySQL 连接串建议这样写:

jdbc:mysql://localhost:3306/emergency_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false

后端接收时间参数时统一用 LocalDateTime 而不是 Date,JSON 格式化时指定格式yyyy-MM-dd HH:mm:ss,避免前后端时间格式不一致导致的解析错误。

6.2 并发环境下床位状态的一致性

前面提到用条件更新解决并发分配的问题,这里再补充两个场景。

第一个是释放床位。患者离院后释放床位,也要用条件更新:WHERE id = ? AND status = 1,防止重复释放。

第二个是转区操作。转区本质上是“释放旧床 + 分配新床”,必须放在同一个事务里。如果分开操作,中间有一个失败就会造成数据不一致,旧床释放了但新床没分配上。事务方法如下:

@Transactional(rollbackOn = Exception.class) public void transferBed(Long allocationRecordId, Long targetBedId, String operator) { BedAllocationRecord record = allocationRecordMapper.selectById(allocationRecordId); releaseBed(record); // 释放旧床 allocateBed(record, targetBedId); // 分配新床 }

6.3 大屏数据缓存与一致性

大屏接口每 10 秒查一次数据库,如果每次都直接查原始表,数据库压力会随着接口增加而上升。我的做法是在 SpringBoot 里加了一层简单的内存缓存,用 Caffeine 做一个短时缓存,缓存过期时间设成 8 秒,保证最多只有 2 秒的数据延迟,同时能把数据库查询频率降低一个量级。

关键代码实现:

@Configuration public class CacheConfig { @Bean public Cache<String, Object> dashboardCache() { return Caffeine.newBuilder() .expireAfterWrite(Duration.ofSeconds(8)) .maximumSize(100) .build(); } }

大屏接口读取时先查缓存,判断业务触发的数据变更要不要主动失效缓存。分诊登记、床位分配这类写操作走完事务后,主动cache.invalidate("dashboard:overview"),保证下一次查询拿到最新数据。

6.4 前端组件卸载清理

Vue 大屏页面如果忘了在onUnmounted里清理定时器和监听事件,页面切走再切回来,定时器会叠加,后端会被请求轰炸。我一般写一个公共的清理逻辑,用上述的usePollingHook 统一处理,尽量不让定时器散落在各个组件里。

6.5 权限测试和脱敏准备

如果这个系统是给医院真实使用的,权限控制不能只做前端路由守卫。后端接口必须校验权限,否则绕过前端直接调 API 就没有任何防线了。Spring Security 配置里至少要保证写操作接口需要ROLE_DOCTORROLE_NURSE,管理端接口需要ROLE_ADMIN

另外,如果系统里涉及患者身份证号、手机号等敏感信息,要注意脱敏。接口返回给前端时,中间四位用*替代。脱敏逻辑放在 VO 的 getter 里做就行,不用改数据库里的数据。

7. 扩展思路:再接一步可以怎么玩

核心功能跑通之后,这套系统还有很多可以继续深化的方向。我梳理了几个上手快、价值明确的扩展点。

自动分区分流:在现有床位分配的基础上,增加一条规则引擎,根据当前各区域占用率和患者分诊级别自动平衡区域负载。比如抢救室爆满而观察室有空床时,系统可以提示将部分 2 级患者转入观察室进行密切观察,而不是死等抢救室。

护工任务联动:床位释放后自动生成“清扫任务”,推送给对应区域的护工。大屏上显示“3 床待清扫”状态,替代当前打电话叫人的方式。这个扩展点虽然业务逻辑简单,但实际价值非常大,因为它打通了调度到执行的最后一环。

候诊时长报警:为候诊队列增加一个监控,4 级患者候诊超过 2 小时或者 3 级患者候诊超过 90 分钟时,系统自动提醒分诊台护士主动评估二次分诊,防止漏掉病情变化。

趋势预测:基于历史数据预测量,结合天气和节假日因素,预测未来 4 小时的急诊就诊量,帮助排班。这个效果做起来复杂,但可以考虑先做了按小时维度的历史趋势对比,辅助人工判断。

移动端签到付款:给患者回执上生成二维码,线上补缴费用,减少收费窗口排队时间。这一块虽然不属于核心调度,但对改善就医体验有直接作用。

这些扩展方向没有改变系统的核心架构,只是在现有模块上叠加逻辑。SpringBoot + Vue 这套架构的扩展性完全能支撑。唯一需要提前规划的是一些基础数据维度的完整性,比如在 patient 表里保留足够的字段、在 triage_record 表里记录详细的到达时间属性,这些现在用不到但以后做分析都会用到。

我在实际开发中的体会是,医院类系统的难点从来不在技术选型,而在于对业务流程的理解。同样是床位管理,住院部的床位管理简单得多,因为患者来了住下,几天内不会大动。急诊的床位管理难就难在周转快、变化多、容错率低。做这个项目最大的收获,不只是练会了 SpringBoot 和 Vue 的搭配,而是真正理解了对“时效性”这件事的工程化表达。数据模型要能反映真实世界的状态流转, 코드要能处理并发和异常,大屏展示要能辅助决策而不是制造噪音。把这些想通了,系统自然就好用了。

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

BK3633 BLE开发实战:Keil5工程重建与低功耗调试

简介&#xff1a;本资源是面向嵌入式蓝牙开发工程师与IoT硬件初学者的BK3633蓝牙SoC实战开发套件&#xff0c;聚焦BLE无线通信应用落地&#xff0c;解决Keil5环境下SDK适配难、烧录调试流程不透明、蓝牙OTA及外设驱动集成无参考等典型痛点。压缩包共1450个文件&#xff0c;主体…

作者头像 李华
网站建设 2026/9/11 12:13:59

Redis高级数据结构实战:GEO、BitMap与HyperLogLog在项目中的应用

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/11 12:13:37

安卓10系统定制:彻底隐藏设备信息菜单的ROM修改指南

1. 项目背景与需求解析 最近在折腾一台老旧的安卓10.0设备时&#xff0c;遇到了一个很有意思的需求&#xff1a;客户需要隐藏系统设置中的"我的设备"菜单选项。这个需求在商用设备定制、企业终端管理等领域其实很常见——比如共享设备厂商不希望用户查看硬件配置&…

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

生物智能与AI融合:技术边界模糊的挑战与机遇

1. 项目概述&#xff1a;当技术边界模糊的临界点2003年波士顿动力公司成立时&#xff0c;大多数人还认为双足机器人行走是天方夜谭。如今当Atlas机器人完成后空翻的瞬间&#xff0c;我们突然意识到&#xff1a;那个机器人只能呆在工厂流水线上的时代正在终结。这个标题揭示的正…

作者头像 李华
网站建设 2026/9/11 12:11:11

项目管理深度解析(三十六)——项目怎么估算活动资源

摘要&#xff1a;本文系统讲解项目管理中如何估算活动资源&#xff0c;涵盖估算的输入信息、常用工具与技术、输出成果及实践建议。文章先厘清活动资源估算的概念与作用&#xff0c;再梳理项目管理计划、项目文件、事业环境因素等输入&#xff0c;重点对比专家判断、自下而上估…

作者头像 李华