医院急诊系统源码落地实战:分诊分级、医嘱状态机与数据一致性
简介这是一套面向Java Web开发学习者与团队的医院急诊系统完整源码适合毕业生做课程设计或毕设起步、团队寻找稳定模板加速产品开发、教育机构用于教学演示以及创业公司快速搭建可用的MVP。后端基于Spring Boot、Shiro、MyBatis构建数据库采用MySQL前端使用Vue、Bootstrap、jQuery等技术前后端分离结构清晰便于二次开发与功能扩展。压缩包共828个文件约30.99MB其中Java源码125个、Vue组件70个、JavaScript文件158个、HTML页面44个、CSS样式50个另含SVG图标、GIF与PNG图片、XML配置、SQL脚本及说明文档覆盖从界面到业务逻辑的完整工程。资源还附带安装、构建、运行等批处理脚本可帮助读者快速完成环境部署与项目启动。目前已有59人学习关注适合需要一套可直接运行、结构规范的Java Web项目作为参考或开发起点的开发者。1. 急诊系统源码落地从「能跑起来」到「敢给护士用」差在哪医院急诊系统跟普通 CRUD 后台最大的区别是它永远处在「人比代码急」的状态。分诊台护士要在 30 秒内完成一次分级抢救室医生要在一屏里看到生命体征、过敏史、既往用药挂号收费还得跟 HIS 对得上账。所以拿到一份「Java系统源码医院急诊系统」的压缩包第一件事不是急着mvn spring-boot:run而是判断这套代码到底覆盖了急诊的哪几条主链路——分诊、抢救、留观、医嘱、计费缺一条都上不了线。这篇笔记面向两类人一类是拿着课程设计案例源码想改成能演示的毕设或作品集另一类是真要在科室里试点的工程师。前者关心怎么把源码跑通、把数据造得像样后者关心权限、并发、数据一致性这些上线才暴露的问题。两条路我都走过血泪经验是急诊系统最贵的不是功能是「错一次就没人再信你」的信任成本。下面按「先立住模型、再动手复现、最后讲坑」的顺序拆开讲。2. 急诊业务建模先想清楚分诊分级和医嘱状态机2.1 急诊四级分诊为什么不能只存一个 int急诊分诊常见做法是按病情危重程度分四级Ⅰ 濒危、Ⅱ 危重、Ⅲ 急症、Ⅳ 非急症很多课程设计源码里就一个triage_level字段存 1 到 4。真用起来会翻车同一个病人在等待期间病情会变化分级要能改改了要留痕还要能追溯是谁在几点几分改的。所以数据模型至少要有三张表分诊记录主表、分级变更流水表、生命体征采集表。-- 分诊记录主表一个就诊号对应一条当前有效分诊 CREATE TABLE triage_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visit_no VARCHAR(32) NOT NULL COMMENT 就诊流水号关联挂号, patient_id BIGINT NOT NULL, current_level TINYINT NOT NULL COMMENT 当前分级 1-4, chief_complaint VARCHAR(500) COMMENT 主诉, triage_nurse BIGINT NOT NULL COMMENT 分诊护士ID, triage_time DATETIME NOT NULL, status TINYINT DEFAULT 1 COMMENT 1待诊 2就诊中 3已离院, UNIQUE KEY uk_visit (visit_no) ) COMMENT 急诊分诊主表; -- 分级变更流水只追加不修改用于追溯和质控 CREATE TABLE triage_level_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, visit_no VARCHAR(32) NOT NULL, from_level TINYINT, to_level TINYINT NOT NULL, reason VARCHAR(200), operator_id BIGINT NOT NULL, op_time DATETIME NOT NULL, KEY idx_visit_time (visit_no, op_time) ) COMMENT 分诊分级变更流水;主表存「当前状态」是为了查询快流水表存「变更历史」是为了质控和纠纷追溯这是急诊系统里反复出现的模式——当前态 流水态双写。参数上注意current_level用 TINYINT 而不是 ENUM因为分级标准各地会微调ENUM 改起来要动 DDL。visit_no建唯一索引防止同一就诊号被重复分诊这是并发下最容易出的脏数据。2.2 医嘱状态机别用布尔字段表达「已执行」医嘱是急诊系统里状态最复杂的对象。一份医嘱要经历「开立 → 审核 → 收费 → 执行 → 完成/作废」中间还可能退回。我见过太多源码用is_paid、is_executed两个布尔字段硬扛结果退费、部分执行、执行后作废这些场景全乱套。正确做法是显式状态机状态流转用一张表记录业务表只存当前状态。public enum OrderStatus { CREATED(0, 已开立), AUDITED(1, 已审核), CHARGED(2, 已收费), EXECUTING(3, 执行中), FINISHED(4, 已完成), CANCELLED(9, 已作废); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } } // 状态流转校验只允许合法迁移非法迁移直接抛业务异常 public class OrderStateMachine { private static final MapOrderStatus, SetOrderStatus ALLOWED Map.of( OrderStatus.CREATED, Set.of(OrderStatus.AUDITED, OrderStatus.CANCELLED), OrderStatus.AUDITED, Set.of(OrderStatus.CHARGED, OrderStatus.CANCELLED), OrderStatus.CHARGED, Set.of(OrderStatus.EXECUTING, OrderStatus.CANCELLED), OrderStatus.EXECUTING, Set.of(OrderStatus.FINISHED), OrderStatus.FINISHED, Set.of(), OrderStatus.CANCELLED, Set.of() ); public static void check(OrderStatus from, OrderStatus to) { if (!ALLOWED.getOrDefault(from, Set.of()).contains(to)) { throw new BizException(医嘱状态不允许从 from 变更为 to); } } }这段代码的关键不是枚举本身而是ALLOWED这张迁移表——它把「什么情况下能改状态」这件事从散落各处的 if-else 收敛到一个地方。参数说明FINISHED和CANCELLED是终态出边为空集合意味着完成后不能再改要改只能新开一份医嘱。这是急诊质控的硬要求执行完的医嘱被偷偷改掉是重大事故。实际项目里这张迁移表建议放配置中心或数据库因为不同医院对「已收费能否直接作废」的规定不一样。2.3 用 MyBatis 把分诊和医嘱串起来的最小查询急诊医生最常用的界面是「按分级排序的待诊列表」这个查询要同时拿到分诊信息、最近一次生命体征、未完成医嘱数。用 MyBatis 写一个聚合查询比在 Java 里循环调三次接口靠谱得多。select idlistWaitingByLevel resultMapWaitingVO SELECT t.visit_no, t.current_level, t.chief_complaint, t.triage_time, v.sbp, v.dbp, v.temperature, v.heart_rate, (SELECT COUNT(*) FROM medical_order o WHERE o.visit_no t.visit_no AND o.status lt; 4) AS pending_orders FROM triage_record t LEFT JOIN vital_sign v ON v.visit_no t.visit_no AND v.record_time (SELECT MAX(record_time) FROM vital_sign WHERE visit_no t.visit_no) WHERE t.status 1 ORDER BY t.current_level ASC, t.triage_time ASC /select逻辑说明主表按status 1待诊过滤ORDER BY current_level让 Ⅰ 级病人永远排最前同级别按分诊时间先到先看。生命体征用相关子查询取「最近一次」避免 JOIN 出多条记录导致行数膨胀。pending_orders用标量子查询算未完成医嘱数比 LEFT JOIN GROUP BY 更直观。参数上要注意vital_sign表如果数据量大record_time上必须有(visit_no, record_time)联合索引否则这个子查询会全表扫。这套查询在几千条待诊数据下响应在 50ms 内够急诊用。3. 把源码跑起来环境、依赖和造数据的完整路径3.1 从零到能登录的启动步骤拿到源码先别改代码按标准路径跑通再说。常见的技术栈是 Spring Boot MyBatis MySQL Redis前端可能是 Vue 或 Thymeleaf。下面是我一般会走的启动流程命令按顺序执行。# 1. 确认 JDK 版本急诊系统源码多为 JDK 8 或 11 java -version # 2. 建库并导入初始化脚本脚本一般在 src/main/resources/sql 下 mysql -uroot -p -e CREATE DATABASE emergency DEFAULT CHARSET utf8mb4; mysql -uroot -p emergency sql/schema.sql mysql -uroot -p emergency sql/data.sql # 3. 改配置数据库、Redis、端口 # application-dev.yml 里改 datasource 和 redis 的 host/port/password # 4. 编译打包跳过测试先跑通 mvn clean package -DskipTests # 5. 启动 java -jar target/emergency-*.jar --spring.profiles.activedev逻辑说明先java -version是因为 JDK 17 跑 JDK 8 编译的字节码有时会因模块化报错版本对不上后面全是玄学问题。建库用utf8mb4而不是utf8因为病人姓名、主诉里可能有生僻字和 emoji家属在备注里打表情的情况真不少。-DskipTests是权宜之计跑通后再单独跑测试。启动参数--spring.profiles.activedev指定环境别硬编码在 jar 里。提示如果启动报Table xxx doesnt exist八成是data.sql里的建表语句和schema.sql重复或顺序反了先只导 schema 再导 data。3.2 造一批像样的急诊测试数据空库跑起来的系统没法演示但随手 INSERT 几条又太假。急诊数据要满足几个特征分级分布符合真实Ⅳ 级最多、Ⅰ 级最少、时间集中在近几小时、生命体征在合理区间。用一段 Java 或 SQL 批量造。// 用 JDBC 批量插入模拟过去 6 小时的急诊流量 String sql INSERT INTO triage_record(visit_no, patient_id, current_level, chief_complaint, triage_nurse, triage_time, status) VALUES(?,?,?,?,?,?,1); try (PreparedStatement ps conn.prepareStatement(sql)) { Random rnd new Random(42); // 固定种子保证每次造的数据一致 String[] complaints {发热, 腹痛, 胸痛, 外伤, 头晕, 呕吐}; for (int i 0; i 200; i) { ps.setString(1, V System.currentTimeMillis() i); ps.setLong(2, 10000L i); // 分级分布Ⅰ级2% Ⅱ级13% Ⅲ级35% Ⅳ级50% int r rnd.nextInt(100); int level r 2 ? 1 : r 15 ? 2 : r 50 ? 3 : 4; ps.setInt(3, level); ps.setString(4, complaints[rnd.nextInt(complaints.length)]); ps.setLong(5, 1L); // 护士ID // 时间落在过去 6 小时内 ps.setTimestamp(6, new Timestamp(System.currentTimeMillis() - rnd.nextInt(6 * 60) * 60_000L)); ps.addBatch(); } ps.executeBatch(); }逻辑说明固定随机种子42是为了让每次生成的数据可复现方便对比测试。分级分布按真实急诊比例设置Ⅰ 级只占 2%这样列表排序效果才明显。时间用「当前时间减去 0 到 360 分钟」生成保证数据落在近期不会出现「2020 年的病人还在待诊」这种穿帮。参数上visit_no用时间戳加序号保证唯一patient_id从 10000 起避免和真实数据冲突。3.3 权限和角色急诊系统绕不开的 RBAC急诊系统至少四类角色分诊护士、急诊医生、抢救室护士、收费员权限边界很清楚。源码里如果只有一张user表加一个role字符串后期加角色会痛不欲生。标准做法是用户、角色、权限三张表加两张关联表。表名作用关键字段sys_user用户账号id, username, password, dept_idsys_role角色定义id, role_code, role_namesys_permission权限点id, perm_code, perm_name, typesys_user_role用户角色关联user_id, role_idsys_role_permission角色权限关联role_id, perm_id权限点建议按「资源:操作」命名比如triage:create、order:cancel、vital:view。急诊里有个特殊点抢救室护士要能看全科病人普通分诊护士只能看自己分诊的这种数据级权限光靠角色控制不住得在查询里加数据范围条件。常见做法是在用户表挂一个data_scope字段本人/本科室/全院Service 层拼 SQL 时按范围加 where。4. 急诊系统上线前必须过的四道坎4.1 并发抢号两个护士同时分诊同一个病人现象分诊台两台电脑同时打开同一个病人的分诊页都点了提交结果triage_record里出现两条记录或者后提交的把先提交的覆盖了。原因是没有做并发控制visit_no的唯一索引如果没建数据库层面也拦不住。解决分两层。数据库层给visit_no建唯一索引这是最后一道防线。应用层用乐观锁或分布式锁我一般用「先查后插 唯一索引兜底」的简单方案插入捕获DuplicateKeyException后提示「该病人已被分诊」。try { triageMapper.insert(record); } catch (DuplicateKeyException e) { throw new BizException(该就诊号已完成分诊请刷新页面); }注意别用SELECT ... FOR UPDATE锁整张表急诊高峰期会把分诊台卡死唯一索引 异常捕获足够。4.2 医嘱和收费的数据一致性现象医生开了医嘱收费系统扣了费但医嘱状态没更新护士看不到该执行的医嘱。原因是医嘱服务和收费服务是两个事务中间任何一步失败都会不一致。热词里常问的「java怎么保证数据一致性」在急诊场景就是这个问题。解决本地消息表 定时补偿。医嘱状态变更时在同一个本地事务里往message表插一条待发送消息然后异步通知收费系统收费系统处理成功后回调更新医嘱状态。如果回调失败定时任务扫message表重发。别一上来就上分布式事务框架急诊系统数据量不大本地消息表够用且好排查。4.3 生命体征采集的时间精度现象同一分钟内采集了两次血压查询「最近一次」时取到的不是真正最新的。原因是record_time用了DATETIME精度只到秒两次采集时间戳相同MAX(record_time)取出来是随机的。解决时间字段用DATETIME(3)精确到毫秒或者加一个自增的seq字段取最近一次时按seq排序。急诊抢救时一分钟内多次测量很常见这个坑不踩一次想不到。4.4 分页查询在数据量大时变慢现象待诊列表前几页很快翻到后面越来越慢甚至超时。原因是用了LIMIT offset, sizeoffset 很大时 MySQL 要扫描并丢弃前面所有行。解决改成游标分页用上一页最后一条的triage_time和id作为游标。SELECT * FROM triage_record WHERE status 1 AND (triage_time, id) (?, ?) ORDER BY triage_time ASC, id ASC LIMIT 20;参数说明(triage_time, id)是上一页最后一条记录的值MySQL 支持行值比较能直接走联合索引。急诊待诊列表其实很少翻很多页但留观病人列表可能上千条这个优化值得做。5. 让急诊系统经得起用的两个进阶技巧5.1 用状态机 事件驱动解耦医嘱流转前面讲的医嘱状态机如果只在 Service 里校验随着业务变复杂会越来越臃肿。进阶做法是把状态变更发布成领域事件各下游模块订阅。比如医嘱变为CHARGED时收费模块、药房模块、护理模块各自订阅处理互不阻塞。// 状态变更后发布事件事务提交后再投递 Transactional public void changeStatus(String orderNo, OrderStatus to) { MedicalOrder order orderMapper.selectByNo(orderNo); OrderStateMachine.check(order.getStatus(), to); orderMapper.updateStatus(orderNo, to.getCode()); // 注册事务同步回调确保事务提交后才发事件 TransactionSynchronizationManager.registerSynchronization( new TransactionSynchronization() { Override public void afterCommit() { eventPublisher.publishEvent(new OrderStatusChangedEvent(orderNo, to)); } }); }逻辑说明关键是afterCommit里发事件如果事务回滚了事件不会发出去避免「医嘱没改成但通知已经发了」的脏事件。参数上OrderStatusChangedEvent里带上订单号和目标状态订阅方按需处理。这套模式在急诊里特别适合「医嘱执行后自动生成护理记录」这类联动。5.2 用影子表做上线前的数据校验急诊系统最怕上线后数据对不上。我的习惯是上线前建一套影子表把老系统的数据同步过来新系统跑一段时间后对比两边数据。对比脚本按就诊号逐条比对分诊分级、医嘱状态、费用金额差异超过阈值就报警。-- 对比新旧系统分诊分级差异 SELECT n.visit_no, o.current_level AS old_level, n.current_level AS new_level FROM triage_record n JOIN triage_record_shadow o ON n.visit_no o.visit_no WHERE n.current_level ! o.current_level;这个技巧不新鲜但急诊场景下特别值——因为急诊数据错了直接影响诊疗宁可上线慢一周也别让护士在抢救时发现系统里的分级是错的。我自己踩过的最大坑就是跳过影子表直接切结果一批历史病人的过敏史没同步过来幸好是试运行阶段发现的。做医疗系统后悔药永远比预防贵。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →