ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

WEB电子病历系统架构设计与实战:从B/S选型到部署排障

WEB电子病历系统架构设计与实战:从B/S选型到部署排障 1. 为什么说WEB架构是电子病历系统的必然选择真正上手做过医疗信息化项目的人都绕不开一个核心系统电子病历EMR。前些年很多医院还在用C/S架构的老病历系统客户端装在医生工作站上每次升级都要逐台机器去更新部门之间的数据还经常不同步。后来行业整体转向WEB电子病历不是说换个浏览器访问就叫WEB化而是从部署方式、交互模式、数据流转到维护链路整个重做了一遍。现在你打开浏览器输入医院内网的地址就能完成从患者建档、书写病程、开立医嘱到提交病案的完整流程这才是WEB电子病历该有的样子。这套系统能解决什么问题最直观的是把病历从“写在纸上的记录”变成“能在浏览器里实时读写和流转的临床数据资产”。医生不用再等检验结果打印出来才知道数值护士在病区录入的体温单、血糖值医生端几秒内就能看到。对信息科来说WEB架构最大的价值是“一份代码全院内网都能跑”不用再维护几百台客户端的运行环境。对医院管理者来说病历数据集中存储在服务器端做质控、统计、科研检索都变得高效。这篇内容适合谁看如果你是正在做医疗信息化项目的开发人员、准备从零搭建电子病历系统的架构师或者想把自己医院的病历系统从C/S迁移到WEB的技术负责人这篇东西能给你省不少踩坑的时间。我不会只讲概念会把设计思路、表结构、接口逻辑、部署细节和排障经验全部拆开来说。2. 整体设计与选型思路拆解2.1 为什么选B/S架构而不是继续用C/S先说一个常识B/S架构的本质是把业务逻辑全部收敛到服务器端浏览器只负责渲染和交互。电子病历这个场景对部署一致性要求极高因为临床科室的电脑配置参差不齐有十年前的老机器也有新配的国产终端如果做C/S客户端光是兼容不同操作系统、不同显卡、不同办公软件环境就能让人崩溃。用WEB之后只要机器能跑浏览器系统就能用兼容性问题大幅减少。另外还有升级成本。按照卫健委对电子病历分级评价的要求系统需要不断迭代功能比如新增过敏史结构化的录入、增加护理计划模块、调整病历模板。WEB架构下重启一下后端服务或者让前端重新构建部署全院终端下一次刷新页面就是新版本不需要逐台机器去装补丁。这一点在落地时节省的时间成本是实打实的。有人会担心浏览器的复杂表格编辑能力。早期确实有这个问题一个病历文书里动辄几十个字段还有签名、打印、续打等需求。但在现在的技术条件下用富文本编辑器加上自研的表格控件配合打印样式控制体验已经可以做到和原生桌面应用很接近。我见过不少团队在这个点上纠结太久其实不如先跑通一个完整流程再慢慢优化。2.2 前后端分离还是服务端渲染这是做WEB电子病历必须回答的问题。市面上很多老牌厂商用的是服务端渲染方案后端用JSP或者模板引擎直接出页面好处是SEO友好但这玩意儿对电子病历来说意义不大毕竟系统在内网跑不需要搜索引擎收录。坏处反而明显前后端代码耦合严重改一个输入框的样式往往要动到后端的Java或者Python代码联调效率低。我推荐前后端分离方案。前端用Vue或React做单页应用后端只提供纯粹的RESTful API或JSON-RPC接口。电子病历的场景里有大量的状态流转、弹窗确认、异步校验前端的交互复杂度非常高分离架构可以让前端团队专注做交互体验后端团队专注做业务逻辑和数据库操作。具体的分工是前端仓库负责登录页、工作台、病历文书编辑器、医嘱录入界面、模板管理界面等后端服务负责患者索引、病历文档存储、医嘱处理、权限校验、签名验证、质控规则引擎等数据库层MySQL或PostgreSQL存业务数据Redis存会话和缓存Elasticsearch可以做病历全文检索前端构建后用Nginx托管静态文件反向代理到后端接口。后端接口统一走HTTPS协议内网虽然不一定需要公网证书但至少要用自签证书保证传输加密防止局域网内的明文嗅探。2.3 关键模块怎么划分一套完整可用的WEB电子病历至少要包含以下模块患者主索引统一管理患者的身份信息、住院号、门诊号、历次就诊记录病历文书门急诊病历、住院病历、病程记录、手术记录、护理记录、知情同意书等医嘱管理长期医嘱、临时医嘱的录入、审核、执行、停止、作废模板与知识库病种模板、常用词库、临床路径让医生不需要从零打字质控管理时限质控、完整性质控、合理性质控比如“入院记录必须在入院后24小时内完成”这种规则电子签名与CA对接实现医护人员的身份认证和操作防抵赖打印与PDF输出病历文书要能按标准格式打印要能归档成PDF存证检索与统计支持按诊断、按医生、按时间范围检索病历用于科研和报表核心流程是医生登录系统后进入工作台选择患者系统自动带出患者基本信息医生根据病程阶段选择对应的病历模板填写完成后提交系统做必填项校验进入质控环节最后归档。所有操作都会被审计谁在什么时间改了哪个字段都有记录。设计的时候最忌讳贪大求全。我见过有团队一上来就想做AI辅助诊断、知识图谱、自动编码结果主流程都没跑通。正确的做法是先做医疗文书的“增删改查签”把数据模型稳定下来再逐步叠加能力。3. 核心实现数据模型与接口设计3.1 病历文档的数据结构怎么设计电子病历不像普通的表单数据一份门诊病历可能只有几个字段但一份住院大病历有现病史、既往史、个人史、家族史、体格检查、辅助检查、初步诊断、治疗意见等等字段是树形的而且不同科室的侧重点完全不同。所以数据模型不能做成统一的宽表否则会有几十上百个字段大部分是空的维护起来非常痛苦。实践中推荐混合方案结构化索引字段患者ID、就诊ID、文书类型、填写医生ID、记录时间、最后修改时间、状态等放在关系型数据库的表里方便查询和统计非结构化文档内容把整个文书的表单数据序列化为JSON存入一个文档字段或者直接存MongoDB。这样做的好处是模板灵活新增一个科室字段不用改表结构以PostgreSQL为例可以直接用JSONB类型。又想要关系型查询能力又想要文档型灵活性JSONB是电子病历场景下的很好的折中。需要快速检索的时候对JSONB字段建立GIN索引对索引字段建立普通索引。一个简化版本的表结构是这样CREATE TABLE emr_document ( id BIGSERIAL PRIMARY KEY, patient_id VARCHAR(32) NOT NULL, encounter_id VARCHAR(32) NOT NULL, doc_type_code VARCHAR(32) NOT NULL, doc_status SMALLINT NOT NULL DEFAULT 0, content JSONB NOT NULL, created_by VARCHAR(32) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_by VARCHAR(32), updated_at TIMESTAMPTZ, submitted_at TIMESTAMPTZ, signed_at TIMESTAMPTZ, deleted_flag SMALLINT NOT NULL DEFAULT 0 ); CREATE INDEX idx_emr_doc_patient ON emr_document(patient_id); CREATE INDEX idx_emr_doc_encounter ON emr_document(encounter_id); CREATE INDEX idx_emr_doc_type ON emr_document(doc_type_code); CREATE INDEX idx_emr_doc_status ON emr_document(doc_status); CREATE INDEX idx_emr_doc_created ON emr_document(created_at);字段先别急着定死等运行一段时间再根据实际查询需求做优化。我这里特意用了deleted_flag做逻辑删除因为病历是医疗文书物理删除会引出合规和审计问题哪怕是误操作也必须走“作废”流程而不是直接删除数据。3.2 一个核心接口的设计示例病历保存接口是写入频率最高的接口也是最容易出问题的接口。很多新手在设计时直接把前端表单的JSON原样POST到后端后端也不做校验就存库。一旦前端被篡改或者保存了脏数据整个病历就废了。我的方案是前后端都校验前端做交互提示后端做强制校验。以保存一份病程记录为例后端接口会做三层校验# 伪代码示例实际项目里用FastAPI/Flask都可以实现 def save_progress_note(payload): # 第一层基础参数校验 if not payload.get(patient_id) or not payload.get(encounter_id): raise BizException(缺少患者或就诊信息) if not payload.get(content): raise BizException(病历内容不能为空) # 第二层文书状态校验防止覆盖已提交的记录 doc get_document(payload[id]) if payload.get(id) else None if doc and doc.status DOC_STATUS.SUBMITTED: raise BizException(该文书已提交不允许直接修改) # 第三层业务规则校验 required_fields get_template_required_fields(payload[doc_type_code]) missing [field for field in required_fields if not payload[content].get(field)] if missing: raise BizException(f缺少必填项: {, .join(missing)}) doc_id save_document(payload) add_audit_log(user_id, actionSAVE_PROGRESS_NOTE, doc_iddoc_id) return {doc_id: doc_id}需要注意电子病历的“保存”有两个层次。一个是草稿保存医生写着写着先存起来方便下次继续编辑另一个是提交保存表示这份病历已经完成进入质控流程。提交流程需要做更严格的校验而且一旦提交医生再想修改就必须走“申请修改—上级审批—修改留痕”的路径设计时一定要把这个状态机理清楚。3.3 权限与审计医疗数据的高压线医疗数据不同于普通业务数据它的敏感程度非常高。WEB电子病历系统在权限设计上至少要落实以下几层数据范围权限医生只能看自己负责的患者的病历护士只能看自己病区的患者功能权限住院医生能写医嘱但未必能开停某些特殊药物护士能执行医嘱但不能修改医嘱内容字段级权限部分敏感信息如HIV检测结果、精神科诊断需要对非相关医护人员做脱敏处理操作审计每次查看、修改、打印、导出都要记录操作日志权限模型可以用经典的RBAC基于角色的访问控制再结合数据归属校验。特别需要提醒的是无论前端怎么隐藏按钮后端接口都必须做二次鉴权。很多电子病历安全事故不是系统被外部攻破而是内部人员越权访问或者测试接口没有关闭被人用Postman直接调用。审计日志我建议单独建表不要和业务日志混在一起。字段至少要包含操作人、操作时间、操作类型、目标对象、IP/MAC地址、操作前后的数据快照或摘要。用数据快照的成本较高但遇到医疗纠纷时这是最有力的举证材料。我在实际项目里会把修改前的JSON快照压缩之后存到独立的审计归档表不影响主流程的性能。4. 部署实战从开发到内网上线的完整过程4.1 Nginx反向代理与前后端联调配置很多团队用Nginx只是做静态文件托管其实在WEB电子病历项目里Nginx承担的职责要重得多HTTPS终结、反向代理、静态资源缓存、请求体大小限制、超时控制。下面是我在项目中验证过的一套核心配置不涉及敏感内容仅做技术参考server { listen 443 ssl; server_name emr.hospital.internal; ssl_certificate /etc/nginx/certs/emr.crt; ssl_certificate_key /etc/nginx/certs/emr.key; # 前端静态资源 root /data/emr-frontend/dist; index index.html; # 单页应用路由处理 location / { try_files $uri $uri/ /index.html; } # 后端API代理 location /api/ { proxy_pass http://127.0.0.1:8000/; 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; # 病历文书的请求体可能比较大必须调大限制 client_max_body_size 20m; # 病历保存是耗时操作要保证超时时间足够 proxy_connect_timeout 60s; proxy_read_timeout 120s; } # 静态资源缓存 location ~* \.(js|css|png|jpg|svg|woff2)$ { expires 7d; add_header Cache-Control public, immutable; } }一个比较容易踩的坑是client_max_body_size。默认只有1MB如果病历文书里嵌入了大量的base64图片有些医院要求在病历中插入患者照片、检查图片请求体很容易超过1MB然后前端就报“413 Request Entity Too Large”但医生不知道是Nginx拦截的只会觉得系统坏了。我建议在配置里明确调大同时从产品上引导用户尽量上传压缩后的图片。4.2 后端服务的环境配置与启动后端服务我优先推荐使用FastAPI或者Flask配合SQLAlchemy操作数据库。FastAPI自带OpenAPI文档调试接口非常方便而且异步性能好可以配合数据库连接池扛住早高峰的并发访问。下面是一份虚拟环境下的依赖清单和启动方式# 创建虚拟环境 python3 -m venv venv source venv/bin/activate # 安装依赖按需增减 pip install fastapi uvicorn sqlalchemy psycopg2-binary redis pydantic pydantic-settings # 启动服务监听内网地址 uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4--workers 4的意思是开4个worker进程具体数值要看服务器的CPU核心数一般取CPU核数的1到2倍。如果数据库查询写得不高效开再多的worker也是白搭反而会因为进程间抢占连接池把数据库拖垮。所以接口性能优化重点要放在SQL语句的索引命中和减少不必要的N1查询上。实际使用中还建议用systemd托管后端进程避免终端退出后服务也跟着退出。一个简化的systemd服务文件如下[Unit] DescriptionEMR Backend Service Afternetwork.target postgresql.service redis.service [Service] Useremr Groupemr WorkingDirectory/data/emr-backend ExecStart/data/emr-backend/venv/bin/uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4 Restartalways RestartSec5 EnvironmentPYTHONUNBUFFERED1 [Install] WantedBymulti-user.target用Restartalways可以保证后端进程意外崩溃时自动拉起。医院信息科通常不会时刻盯着服务器自动重启机制是救命的底线。4.3 一份可复用的部署检查清单三年的时间里我反复用一张清单来检查部署是否到位每次帮医院上线或迁移系统都能派上用场服务器时钟是否同步病历记录时间必须统一用NTP同步到同一时间源数据库备份是否配置至少每日全量备份加实时WAL归档备份要定期演练恢复HTTPS证书是否有效自签证书要安排在每一台接入终端上安装信任根证书会话超时时间是否合理医生写病历可能会长时间挂机超时时间太短会丢内容太长又不安全我一般设置为30分钟无操作后弹窗提醒、60分钟后强制重新登录病毒软件是否添加白名单有些医院终端装了安全软件可能会拦截前端JS的缓存写入导致页面加载异常同一内网下的其他系统是否冲突特别是80/443端口如果医院内网已经有OA或LIS系统占用了端口Nginx配置里就要用其他端口电子病历上线当天我建议信息科和技术支持团队提前准备好一张应急联系电话表并且安排人员在重点科室急诊科、内科病区、外科病区现场蹲点处理医生在使用中的即时反馈。系统上线初期医生的抵抗情绪是最重的现场有人能解答问题可以大大降低上线阻力。4.4 浏览器兼容性与服务器资源估算WEB电子病历对浏览器环境的依赖比较大。医院内网的终端浏览器五花八门有IE内核的、Chrome、Edge、国产浏览器还有各种安全浏览器。我在项目中会明确要求使用基于Chromium内核的浏览器版本并在登录页做自动检测如果检测到低版本或不兼容的浏览器提示用户下载指定的浏览器版本。资源估算方面一个500张床位的二级医院日常同时在线的医生和护士大约150到200人高峰期早上查房后集中开医嘱可能到300人。按这个规模后端服务器配置8核16G内存就够用数据库单独一台物理机或虚机4核8G开始起步。如果条件允许把前端静态资源放到CDN或者独立的Nginx上虽然是在内网也能减轻单台服务器的压力。5. 落地实战中的难点从Web页面到一份合规的病历5.1 病历文书编辑器为什么不能直接用现成富文本这是整个系统里技术含量最高、也是最能看出团队功力的部分。普通的富文本编辑器比如常见的CKEditor、TinyMCE拿来写文章没问题但病历文书有两个特殊要求一是“结构化”二是“不可随意更改”。“结构化”是说病历的每个段落应该是独立的元素比如主诉、现病史、既往史、体格检查每个段落既要有文本内容还要有字段标识这样后期做质控、统计、科研检索的时候才能按字段抽取数据。如果全都混在一个HTML大字符串里后期想提取“所有高血压患者的既往史描述”就麻烦了。所以我的方案是基于Slate.js或ProseMirror这类底层编辑器框架自己封装一套病历块编辑器。每个块Block对应一种类型的病历内容——普通段落、结构化字段、表格、签名区域等。保存的时候把这些块序列化成JSON而不是纯HTML。渲染的时候再根据JSON渲染成适合打印的版式。这个开发量不小但当你看到医生用结构化模板一键生成格式规范的入院记录时会觉得一切都值得。“不可随意更改”不是说不能让医生修改而是说编辑过程中要保留痕迹。我的方案是草稿状态可以自由改提交后进入留痕状态任何修改都生成一条变更记录新旧版本对比一目了然。要实现版本对比最简单的办法是用类似diff算法比较两次JSON的差异然后把差异点渲染给质控人员看。5.2 打印与PDF导出隐藏的大坑电子病历最终是要打印归档的。很多团队在屏幕上调试得很完美一打印就乱行间距不对、页边距变了、分页把表格劈成两半。这个问题我在项目里处理了很长时间总结下来核心就三件事打印样式独立编写用media print控制打印时隐藏按钮、导航栏强制用A4纸型表格和块级元素设置break-inside: avoid防止跨页断裂打印字体统一使用中文字体集宋体、仿宋、黑体按需设置同时确保服务器或终端安装了对应字体PDF导出建议用无头浏览器方案比如Puppeteer加载前端页面然后生成PDF。这样导出的PDF和屏幕显示、打印出的效果完全一致避免了另写一套渲染模板还要保持两边同步的坑。生成PDF的接口独立出来放在后端定时任务里执行医生点击“打印”后系统异步生成好PDF推送到前端避免请求等待太久。5.3 病案归档与质控规则病历写完了不意味着流程结束。按管理要求出院病历要在规定时间内完成归档。系统里要有归档模块病历主索引关联所有住院期间的文书、医嘱、检验检查报告归档时做一次全量完整性检查缺哪份文书就提示哪份。质控规则我会拆成“硬质控”和“软质控”。硬质控比如“入院记录在入院后24小时内完成”“抢救记录在抢救后6小时内完成”这是红线规则不满足直接拦截提交。软质控比如“主诉少于20个字”“现病史里有疑似复制粘贴的字段”规则不满足只提示不阻断以免影响医生的工作效率。这里有一个经验质控规则要尽量做到可配置。不要每次改规则都发版把规则存到数据库表里后台管理员可以自己增删改规则引擎在提交时读取生效的规则并执行。我们当时是把规则做成了JSON格式的声明式配置类似{field: admission_time, max_hours: 24}提高了迭代效率。6. 安全防线与常见问题排查实录6.1 WEB电子病历的安全防护重点医疗系统是网络攻击的重点目标等保测评也单独对医疗行业有严格要求。WEB电子病历在安全建设上至少要做这几件事身份认证对密码强度做限制开启登录失败锁定策略防止暴力破解访问控制所有接口都要校验JWT或会话禁止未登录访问重要操作要二次验证PIN码传输加密全站HTTPS不允许HTTP明文回退输入校验后端必须对每个参数做类型、长度、格式校验防止SQL注入和XSS攻击数据防泄漏病历数据导出要审批留痕后台管理员账号要绑定操作人真实身份禁止共用维护账号另外还建议在Nginx层加基础的防护策略比如限制单IP的请求速率、禁止通过IP直接访问后端端口、隐藏后端服务的版本信息。这些操作虽然简单但能挡住大量自动化的扫描探测。具体到代码里SQL操作一定要用参数化查询不要用字符串拼接。前端渲染医生填写的内容时要转义HTML避免存储型XSS——想象一下医生在病历里填了一个script标签所有打开这份病历的电脑都执行了恶意脚本这个场景是很恐怖的。6.2 高频故障排查技巧我在落地过程中遇到了大量问题整理出下面几个最常遇到的可以作为排查手册来参考问题1页面能打开但接口请求一直转圈排查思路先看浏览器F12里的Network请求看接口返回的状态码。如果是404大概率是Nginx的proxy_pass路径配错了/api/反代到后端时前缀是否被正确剥离如果是504后端服务可能挂了或者超时检查后端进程是否存活、数据库连接是否正常问题2医生反馈“保存成功”但重新打开内容丢失排查思路检查前端保存时是否真的调用成功接口。很多次是前端在表单校验失败时拦截了请求但页面提示语写得不够明确让医生以为保存成功了。建议在前端明确区分“草稿保存中”“已保存”“保存失败”三种状态用不同颜色标识问题3同一时间大量人员登录系统响应很慢排查思路这种通常是数据库连接池被打满或者接口存在慢查询。先看后端日志里的SQL执行时间重点排查病历列表查询、患者检索这类高频接口的索引使用情况。必要时对高频查询增加Redis缓存问题4其他科室系统通过API调用电子病历的数据偶尔超时排查思路外部系统调用内部接口时一定要设置合理的超时时间而且调用方要加熔断机制。曾出现过LIS系统批量同步检验结果时一次性把上千条数据POST过来后端又没有限制请求体大小直接把服务拖垮的案例。建议所有对接接口统一使用消息队列做异步削峰问题5系统提示“Web视图加载错误”排查思路如果医生用的是定制浏览器或老旧终端可能是浏览器内核版本过低无法支持现代JavaScript语法。解决办法是在构建前端时设置browserslist兼容目标把ES6代码转译到ES5同时滚动升级终端浏览器版本6.3 浏览器兼容矩阵这里有必要单独拿一个小节来说因为医疗内网的终端环境真的比公网复杂得多。我在项目中维护了一张兼容性矩阵每次前端发布前会按这个表格过一遍浏览器环境兼容级别说明Chrome 90 / Edge 90完全支持开发主目标推荐医生使用国产浏览器Chromium内核完全支持只要内核版本在90以上基本无问题Firefox 最新版基本支持偶有打印样式微调需要回归测试IE11不支持已停止适配登录页做拦截提示移动端浏览器PAD部分支持可用于查看病历不建议书写复杂文书做兼容的时候最大的坑是“医生说自己的浏览器是最新版”实际上打开的是系统自带的旧版IE兼容模式外壳。所以登录页要显示浏览器内核版本号信息科远程指导时一眼就能判断问题所在。7. 一些实战后的心得体会做WEB电子病历这个方向踩过的坑比做过的功能还多。如果让我给刚入行的团队一句建议我会说先跑通最核心的“医生写一份病历并完成提交”这个闭环再谈其他的加分功能。很多厂商在宣传时把界面做得花团锦簇但实际部署后医生最常用的功能就是开单和书写病程这些地方不流畅其他都是空中楼阁。病历数据的结构化程度决定了系统的上限。从数据建模的第一天起就要把“这份数据将来要用于检索和统计分析”这个念头刻在脑子里。我见过不少病历系统半年后想统计“全院糖尿病患者使用二甲双胍的比例”结果发现诊断和用药信息都以大段文本形式躺在数据库里根本无法准确统计只能人工翻阅纸质病历这完全是设计阶段的失误。在部署层面再啰嗦一句上线前一定要做流量演练。别看医院内网不像互联网有几十万并发但早交班后那半小时的集中开医嘱请求以及出院的集中打印请求很容易让没有经过压测的服务直接挂掉。至少用压测工具模拟200个用户同时保存病历观察后端接口的响应时间超过3秒就不要强行上线。最后分享一个小经验系统上线后不要急着加新功能先用一个月时间把使用中暴露出来的性能和易用性问题修复完。医疗系统的连续性比什么都重要一个稳定运行的基础版本远比一堆华而不实的功能更有价值。
返回列表