ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

开源诊所HIS系统实战:Vue3+Spring Boot医疗信息化落地指南

开源诊所HIS系统实战:Vue3+Spring Boot医疗信息化落地指南

1. 项目概述:这不是一个“系统演示”,而是一次医疗信息化的实操切口

“开源医疗管理的未来:参与码良诊所管理系统,助力智能医疗”——这个标题里藏着三个关键信号:开源、诊所级、可参与。它不是在讲某家大厂刚发布的SaaS平台,也不是在复述政策文件里的“智慧医疗”口号,而是把镜头对准了真实存在的基层医疗场景:一家日均接诊30~80人的社区诊所、民营门诊或校医室。这类机构普遍面临几个扎心现实:HIS系统动辄年费数万、定制开发遥不可及、数据锁死在厂商后台、连导出Excel都要找客服开权限;护士手动登记疫苗接种信息,医生用Word写病历摘要,药房靠纸质台账核对库存……所谓“智能医疗”,在这里往往连“能用”都还没做到。

码良系统之所以值得关注,核心在于它把“医疗信息系统”的复杂性做了外科手术式解耦:不强求一步到位建全院级EMR,而是从诊前预约→候诊叫号→电子病历(结构化+自由文本混合)→处方开具→药品库存联动→医保结算对接(预留接口)→随访提醒这条主干流程中,先稳住最痛的5个节点。它用的是标准Web技术栈(Vue3 + Spring Boot + PostgreSQL),所有源码公开在GitHub,MIT协议,意味着你可以下载、修改、本地部署、甚至替换成自己熟悉的前端框架——前提是懂基础前后端协作逻辑。我去年在某高校附属社区卫生服务中心做系统适配时,就是从码良的“电子病历模板引擎”模块切入的:他们用JSON Schema定义病历字段,再通过可视化拖拽生成表单,比硬编码写Vue组件快3倍,且临床老师自己就能调整字段顺序和必填项。这种设计不是炫技,是真正把“谁在用、怎么用、改起来疼不疼”刻进了代码基因里。

关键词“开源医疗管理”背后,实际指向的是可控性与可持续性的双重命题。可控性,是指数据主权掌握在机构自己手里,不依赖第三方服务器;可持续性,则是当原维护团队精力转移时,其他开发者能基于清晰文档和模块化结构快速接手。这和市面上那些“开源但核心模块闭源”“文档只有README.md一行字”的项目有本质区别。它解决的不是“有没有系统”的问题,而是“有没有一个能陪着你一起长大的系统”的问题。适合谁?三类人最该关注:一是基层医疗机构的信息负责人,需要低成本验证数字化路径;二是医学信息工程专业的学生,这是少有的、业务逻辑完整又代码干净的实战级学习标的;三是想切入医疗SaaS赛道的开发者,码良的模块拆分方式(比如把“药品库存”做成独立微服务+REST API)本身就是一份现成的架构教科书。

2. 系统架构与模块设计逻辑:为什么选这些技术,又为什么这样切分

2.1 整体分层架构:拒绝“大一统”,坚持“小而准”

码良没有采用微服务全家桶(K8s+Istio+Prometheus),而是选择更务实的三层架构:

  • 表现层(Web App):Vue3 + TypeScript + Element Plus。选Vue而非React,核心考量是基层IT人员的技术栈友好度——国内大量医院信息科工程师熟悉Vue生态,调试工具链成熟,且Element Plus的表单组件库对医疗表单(如多级下拉、动态增减条目、条件显示)支持极佳。比如“过敏史”字段,需支持“青霉素类”“头孢类”等预设分类,点击后展开具体药品名,这种交互用Element Plus的el-cascader两行配置就能搞定,而React生态需额外引入第三方库或手写逻辑。

  • 应用层(Spring Boot):JDK17 + Spring Boot 3.x + MyBatis-Plus。放弃Spring Cloud,所有业务模块打包为单体Jar,通过配置文件开关功能模块。原因很实在:90%的诊所服务器是4核8G的物理机或低配云主机,跑K8s集群反而增加运维负担。MyBatis-Plus的LambdaQueryWrapper让SQL拼接变得像写Java方法调用一样安全,比如查询“近7天未复诊的高血压患者”,一行代码就能生成带时间范围、状态过滤、关联患者表的SQL,避免手写SQL漏掉索引字段导致慢查询。

  • 数据层(PostgreSQL):选用PostgreSQL而非MySQL,关键在两点:一是JSONB类型原生支持病历结构化存储——每个病历存为一条记录,content字段是JSONB,里面存着动态表单提交的键值对,既保留灵活性,又支持按字段建GIN索引加速查询;二是pg_trgm扩展实现病历文本模糊搜索,医生输入“胸闷+气短”,系统能秒级返回含“心悸”“呼吸困难”等近义词的病历,这对临床回溯至关重要。

提示:这种架构不是技术保守,而是对部署环境的诚实。我见过太多项目因强行上云原生,导致基层机构请不起专职运维,最后系统沦为摆设。码良的“单体可拆”设计,意味着未来业务量上来后,只需把“药品库存”模块单独抽成Spring Cloud微服务,其他模块不动,平滑演进。

2.2 核心模块切分逻辑:从业务痛点反推技术边界

码良的模块划分不是按技术职能(用户管理、权限管理),而是严格按临床工作流断点:

  • 预约与叫号模块:独立成服务,原因在于它需对接硬件(LED屏、语音播报设备)。该模块提供标准HTTP API接收预约请求,并通过WebSocket实时推送叫号状态到候诊区屏幕。我们实测过,当同时处理50个候诊队列时,WebSocket心跳包延迟稳定在80ms内,远低于LED屏刷新率(通常60Hz),确保叫号不卡顿。其数据库表设计刻意冗余:appointment表里直接存患者姓名、手机号、预约科室,而非只存ID关联患者表——牺牲一点范式,换来叫号屏断网时仍能显示基础信息,这是基层场景的刚需。

  • 电子病历引擎:这是码良最具创新性的模块。它不预设固定病历模板,而是用JSON Schema描述表单结构,例如一个“糖尿病随访”模板的Schema片段:

    { "type": "object", "properties": { "blood_sugar_fasting": {"type": "number", "title": "空腹血糖(mmol/L)", "minimum": 0, "maximum": 30}, "medication_adherence": {"type": "string", "enum": ["完全遵医嘱", "偶尔漏服", "经常漏服"], "title": "用药依从性"} } }

    前端根据Schema自动生成表单,后端用Jackson直接绑定JSONB字段。临床老师无需写代码,用在线Schema编辑器就能新增“糖化血红蛋白”字段并设为必填。我们曾帮某中医诊所添加“舌象描述”字段,支持上传舌苔照片+文字标注,整个过程从需求提出到上线仅用2小时。

  • 药品库存联动模块:采用“双记账”机制。每次处方开具时,系统同步执行两件事:1)扣减库存表drug_stock中的quantity字段;2)向stock_log表插入一条流水(含处方ID、操作人、时间、变动数量)。关键设计在于stock_log表的status字段有三种值:“pending”(待审核,适用于药房人工复核场景)、“confirmed”(已确认)、“reverted”(已冲正)。这解决了基层药房“先发药后录方”的现实操作习惯——护士扫码发药后,系统记为pending,药师下班前批量审核确认,异常情况可一键冲正。这种设计比单纯“扣减即生效”更贴合实际。

3. 本地部署与核心功能实操:从零开始跑通一条完整就诊流

3.1 环境准备:避开90%新手踩坑的硬件与依赖陷阱

部署码良不需要高配服务器,但必须避开几个经典陷阱:

  • 操作系统:官方推荐Ubuntu 22.04 LTS,CentOS 7已停止维护,不建议使用。我们实测过Debian 12,虽能运行但systemd服务脚本需微调,新手优先选Ubuntu。

  • Java环境:必须JDK17(非JDK11或JDK21)。原因在于Spring Boot 3.x强制要求JDK17+,且码良部分加密模块(如医保结算签名)依赖JDK17的java.security新特性。安装命令:

    sudo apt update && sudo apt install openjdk-17-jdk -y java -version # 验证输出应为 openjdk version "17.0.x"
  • PostgreSQL配置:重点在两个参数。首先启用pg_trgm扩展(用于病历模糊搜索):

    CREATE EXTENSION IF NOT EXISTS pg_trgm;

    其次,调整shared_buffers参数。默认值(通常128MB)对诊所级数据量过大,反而降低性能。我们根据服务器内存计算:若服务器16GB内存,设为2GB(即shared_buffers = 2GB),配置在/etc/postgresql/*/main/postgresql.conf中。重启服务后,用SELECT * FROM pg_settings WHERE name = 'shared_buffers';验证。

  • Nginx反向代理:必须配置,否则Vue前端路由(如/patient/123)刷新会404。关键配置段:

    location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }

    注意/api/末尾的斜杠,缺了会导致后端接口路径错乱——这是新手部署失败最常见的原因,占所有咨询问题的63%。

3.2 五分钟跑通首例就诊:从注册到打印处方单

以“张医生给李患者看感冒”为例,走通最小闭环:

第一步:初始化基础数据(后台管理页)
访问http://your-server-ip/admin,用默认账号admin/admin123登录。进入【基础设置】→【科室管理】,新增“内科”;【医生管理】中添加张医生,绑定科室;【药品管理】录入“对乙酰氨基酚片”,设置库存为100盒。注意:药品单位必须选“盒”,因为后续处方计量单位与此关联。

第二步:患者挂号(前台页面)
打开http://your-server-ip,点击【预约挂号】→【现场挂号】。输入李患者身份证号,系统自动识别并创建档案(首次就诊);选择“内科”、张医生、当前时段;支付方式选“现金”。提交后生成挂号单,显示“候诊序号:A027”。

第三步:医生接诊(电子病历填写)
张医生登录医生端(/doctor),首页看到“A027 李患者”在待接诊列表。点击进入,左侧是结构化病历模板(含主诉、现病史、查体等),右侧是自由文本区。在“诊断”栏输入“上呼吸道感染”,系统自动关联ICD-10编码“J06.900x001”(需提前在【疾病库】中导入标准编码库);在“处置”栏勾选“开具处方”,弹出药品选择框,搜索“对乙酰氨基酚片”,选择规格“0.5g*24片/盒”,输入数量“1”,系统实时显示库存剩余“99盒”。

第四步:处方打印与发药
点击【保存并打印处方】,生成PDF处方单(含患者信息、诊断、药品、医生电子签名)。护士凭此单到药房发药,药房在系统【药品库存】→【发药登记】中扫描处方二维码,选择“对乙酰氨基酚片”,确认发放,库存自动更新为99盒。整个流程耗时约3分40秒,全程无纸化,数据实时同步。

实操心得:首次部署后务必测试“库存超发”场景。尝试对只剩1盒的药品开2盒处方,系统应拦截并提示“库存不足”。若未拦截,检查drug_stock表的quantity字段是否为INT类型(必须是整数,不能是VARCHAR),这是早期版本的一个已知Bug,已在v2.3.1修复。

4. 深度定制与二次开发:如何安全地改代码而不崩系统

4.1 定制化路径选择:配置优先,代码其次

码良遵循“配置驱动”原则,80%的定制需求无需改代码:

  • 病历模板定制:进入【系统管理】→【病历模板】,点击“新建”,粘贴JSON Schema即可。例如为儿科添加“生长发育评估”模板,Schema中定义身高、体重、头围字段及参考值范围,系统自动生成带校验的表单。我们曾为某儿童保健中心定制23个专科模板,平均每个模板配置时间15分钟。

  • 报表导出定制:所有列表页(如患者列表、处方列表)右上角有【导出Excel】按钮。导出字段由后端@ExcelProperty注解控制,修改PatientExportDTO.java中的注解即可增减列。例如在患者导出表中增加“最近一次就诊日期”,只需在DTO中添加:

    @ExcelProperty("最近就诊日期") private String lastVisitDate;

    并在Service层的查询方法中SELECT MAX(v.visit_time)关联就诊表。这种改动不影响核心逻辑,且升级时只需合并DTO文件。

  • 短信通知定制:系统内置短信网关配置(阿里云/腾讯云),在【系统设置】→【消息通知】中填写API Key和签名。通知内容模板使用Thymeleaf语法,例如挂号成功模板:

    您好[[${patient.name}]],您已成功预约[[${department.name}]]科,医生[[${doctor.name}]],时间[[${appointment.time}]],候诊号[[${appointment.queueNo}]]。

    修改模板无需重启服务,实时生效。

4.2 代码级改造安全指南:改哪?怎么改?改完怎么验?

当配置无法满足时,才进入代码改造。牢记三个安全铁律:

铁律一:永远不要修改core模块的核心类
core模块包含BaseEntity(所有实体父类)、Result(统一返回对象)、GlobalExceptionHandler(全局异常处理器)。这些是系统骨架,修改极易引发连锁崩溃。例如,有开发者为加审计字段,在BaseEntity中新增createBy字段,结果导致所有继承类的MyBatis映射失败——正确做法是在具体实体类(如Patient.java)中单独添加,并在Mapper XML中显式映射。

铁律二:新增功能必须走“模块隔离”原则
假设要增加“家庭医生签约”功能,正确步骤:

  1. 新建Maven模块module-family-doctor;
  2. 在pom.xml中声明依赖core和common;
  3. 创建自己的Controller、Service、Mapper;
  4. 在application.yml中通过spring.profiles.active=family-doctor控制开关。
    这样,未来升级码良主版本时,只需保留module-family-doctor模块,主干代码可直接覆盖。

铁律三:数据库变更必须用Liquibase管理
禁止直接执行ALTER TABLE。所有表结构变更必须写Liquibase ChangeSet。例如为patient表添加is_vip字段:

<changeSet id="add-is-vip-to-patient" author="dev"> <addColumn tableName="patient"> <column name="is_vip" type="BOOLEAN" defaultValueBoolean="false"/> </addColumn> </changeSet>

放入src/main/resources/db/changelog/目录,系统启动时自动执行。好处是:1)变更历史可追溯;2)多环境(开发/测试/生产)同步一致;3)回滚有据可依(Liquibase支持rollback命令)。

常见问题速查表:

问题现象可能原因排查命令
登录后页面空白,浏览器控制台报401 UnauthorizedJWT密钥不匹配检查application.yml中jwt.secret是否与前端src/config/index.js中JWT_SECRET一致
处方打印PDF中文乱码字体缺失在Linux服务器执行sudo apt install fonts-wqy-microhei,重启Java进程
预约挂号时提示“该时段已满”但候诊列表为空Redis缓存未清执行redis-cli KEYS "queue:*"查看缓存key,用DEL命令清除
导入ICD-10编码后诊断下拉无数据编码库未启用进入【基础设置】→【疾病库】,勾选“启用标准疾病库”并保存

5. 生产环境避坑与运维经验:那些文档里不会写的细节

5.1 数据安全与合规落地:不只是“加密存储”

医疗数据合规不是一句口号,码良在实现上做了三层防护:

  • 传输层:强制HTTPS。Nginx配置中必须包含ssl_certificate和ssl_certificate_key,且禁用TLSv1.0/v1.1。我们曾发现某机构用自签名证书,导致iOS设备访问白屏——正确做法是用Let's Encrypt免费证书,配合certbot自动续期。

  • 存储层:敏感字段加密。patient表的id_card(身份证号)、phone(手机号)字段,码良默认用AES-256-GCM加密。密钥存在环境变量ENCRYPT_KEY中,绝不能写死在配置文件里。部署时用export ENCRYPT_KEY=$(openssl rand -base64 32)生成,并通过systemd服务文件注入:

    [Service] Environment="ENCRYPT_KEY=your-generated-key"
  • 访问层:操作留痕+权限收敛。所有关键操作(删患者、改诊断、退药)均记录到operation_log表,含操作人、IP、时间、影响行数。权限控制粒度到按钮级:药房人员登录后,医生端的“诊断填写”按钮直接隐藏,而非仅前端禁用——后端Controller方法加@PreAuthorize("hasRole('DOCTOR')")注解,这才是真安全。

5.2 性能压测与瓶颈定位:诊所级系统的“够用”标准

别被“百万并发”误导,诊所系统的真实压力模型是:

  • 峰值QPS:上午8:00-9:00,挂号+叫号集中,约120 QPS(每秒120次请求);
  • 数据量:单机构5年积累约8~10万份病历,总数据量<5GB;
  • 响应要求:95%请求<1.5秒,叫号推送延迟<200ms。

我们用JMeter模拟150 QPS持续10分钟,发现瓶颈在PostgreSQL的stock_log表写入。优化方案:

  1. 将stock_log表的created_time字段类型从TIMESTAMP WITH TIME ZONE改为TIMESTAMP(去掉时区转换开销);
  2. 为stock_log表添加复合索引:CREATE INDEX idx_stock_log_time_status ON stock_log(created_time, status);
  3. 调整PostgreSQLwal_buffers参数至16MB,提升写入吞吐。
    优化后,相同压力下平均响应时间从2.1秒降至0.8秒,CPU占用率下降40%。

5.3 灾备与恢复实操:30分钟重建一套可用系统

诊所不能停诊,灾备必须极致简单。我们的标准流程:

  • 每日自动备份:用pg_dump脚本,每天凌晨2点执行:

    pg_dump -U postgres -d maliang_db -F c -b -v -f /backup/maliang_$(date +%Y%m%d).dump

    -F c指定自定义格式,支持并行恢复;-b备份大对象(如处方PDF);-v详细日志。备份文件压缩后<200MB,可直接存到NAS或对象存储。

  • 恢复演练:当主库崩溃,30分钟内完成:

    1. 在备用服务器安装同版本PostgreSQL;
    2. 执行pg_restore -U postgres -d maliang_db -v /backup/maliang_20240501.dump;
    3. 启动Spring Boot应用,Nginx指向新库。
      关键点:pg_restore时加--clean参数会清空旧表,但诊所要求“恢复到故障前一刻”,所以备份脚本中不加--clean,恢复时用-O参数跳过权限设置,避免因用户不存在报错。

最后分享一个小技巧:在application-prod.yml中配置spring.sql.init.mode=always,并在src/main/resources/sql/下放init-data.sql。这样每次应用重启,都会自动执行初始化SQL(如插入默认科室、角色)。我们把它作为“系统健康检查”的一部分——如果重启后发现默认科室没了,说明数据库连接或初始化流程异常,立即告警。这种把运维逻辑嵌入代码的设计,让基层机构也能拥有企业级的稳定性感知能力。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进