在 HL7 噩梦中生存:将现代 SaaS 与传统医院系统解耦的策略
Source: Dev.to
请提供您希望翻译的文章正文内容,我将为您翻译成简体中文。
介绍
如果你是一名现代开发者,你的世界可能围绕着 REST、GraphQL、JSON,甚至还有一点点 gRPC(如果你想显得更高级的话)。然后,有一天你接到一家医院或医疗初创公司的合同。你想:“太好了!我只要调用他们的 API 就能获取患者入院数据。”
于是他们把规范发给你。
这 不是 JSON。也 不是 XML。它是一串用管道符(|)分隔的文本,看起来像是猫在键盘上走了一遍,并且通过一个永不关闭的原始 TCP 套接字传输。欢迎来到 HL7 v2——这个已经有 30 年历史的标准仍在支撑全球的医疗基础设施。
对于现代 SaaS 工程师来说,集成它可能感觉像是一场噩梦。但如果你让这套遗留的烂摊子泄漏进你干净、现代的代码库,你就在制造会困扰你多年的技术债务。
解决方案?反腐层(Anti‑Corruption Layer,ACL)。
在本文中,我们将探讨如何使用 ACL 模式在不失去理智(或干净架构)的情况下,生存于 HL7 接口之中。
怪物:HL7 v2 与 MLLP
在我们与怪物搏斗之前,必须先了解它。HL7 v2 消息的格式如下:
MSH|^~\&|EPIC|HOSPITAL|MYAPP|SaaS|202501011230||ADT^A01|MSG00001|P|2.3
PID|1||12345^^^MRN||DOE^JOHN^^^^||19800101|M
PV1|1|I|200^Bed1^Room2||||1234^Doctor^Smith
更糟的是,这些消息并不是通过 HTTP 发送的。它们使用 MLLP(最小下层协议)。这意味着你必须:
- 打开一个原始 TCP 套接字。
- 监听特定的起始字节(
0x0B)。 - 读取直到结束字节(
0x1C 0x0D)。 - 立即发送确认(ACK),否则医院系统会报错。
如果你在主业务逻辑控制器中直接编写解析 PID|1||… 的代码,就已经成功地破坏了你的领域模型。
策略:反腐层 (ACL)
反腐层 是领域驱动设计(Domain‑Driven Design,DDD)中的一种模式。它在你的下游系统(医院)和上游系统(你闪亮的 SaaS)之间创建了一个防御性的边界。
你的内部系统 绝不 应该了解 HL7 消息的具体样子。它只应使用你清晰的内部领域语言进行交流。
HL7 ACL 的组件
- Facade(摄取) – 处理丑陋的 MLLP 套接字连接。
- Adapter(解析) – 将管道分隔的文本转换为可用的对象。
- Translator(映射) – 最重要的部分。将 HL7 对象转换为 你的 领域模型。
让我们构建它(Node.js 示例)
我们希望在医院发送 ADT^A01(入院)消息时创建一个 PatientAdmission 事件。
我们会使用轻量级的 HL7 解析器(例如 node-hl7-client)来进行底层字符串拆分,但真正关键的是整体架构。
步骤 1:定义干净的领域模型
// domain/models.ts
// 这里是纯粹的业务模型,没有任何 HL7 垃圾代码。
export interface PatientAdmission {
patientId: string;
fullName: string;
admittedAt: Date;
location: string;
attendingPhysician: string;
}
步骤 2:腐化层(HL7 输入)
const rawHL7 = "MSH|^~\\&|...|ADT^A01|...\rPID|1||12345^^^MRN||DOE^JOHN...\rPV1|..."
步骤 3:翻译服务
这里是魔法发生的地方。此函数是代码库中唯一可以了解 “PID‑5” 或 “PV1‑3” 的位置。
// services/AntiCorruptionLayer.ts
import { parse } from 'some-hl7-library';
import { PatientAdmission } from '../domain/models';
export class HL7ToDomainTranslator {
static translateAdmission(rawMessage: string): PatientAdmission {
const hl7 = parse(rawMessage);
// 验证消息是否为入院(ADT A01)
if (hl7.get('MSH.9.1') !== 'ADT' || hl7.get('MSH.9.2') !== 'A01') {
throw new Error('Message is not an admission event');
}
// 将旧字段映射到现代领域模型
return {
patientId: hl7.get('PID.3.1').toString(), // MRN
fullName: `${hl7.get('PID.5.2')} ${hl7.get('PID.5.1')}`, // Family^Given
admittedAt: this.parseHL7Date(hl7.get('MSH.7').toString()),
location: hl7.get('PV1.3.1').toString(), // 护理单元
attendingPhysician: `${hl7.get('PV1.7.2')} ${hl7.get('PV1.7.1')}`
};
}
// 处理 HL7 那种奇怪的 YYYYMMDDHHMM 格式的辅助方法
private static parseHL7Date(dateString: string): Date {
// … 专门的日期解析逻辑
return new Date(); // 为演示简化
}
}
步骤 4:门面(基础设施层)
现在把它们连起来。你的主业务逻辑只会收到一个干净的 PatientAdmission 对象。
// infrastructure/TcpServer.ts
import net from 'net';
import { HL7ToDomainTranslator } from '../services/AntiCorruptionLayer';
import { admissionController } from '../controllers/admissionController';
function stripMLLP(message: string): string {
// 去除起始(0x0B)和结束(0x1C 0x0D)帧字符
return message.replace(/^\x0b/, '').replace(/\x1c\x0d$/, '');
}
function createAck(originalMessage: string): Buffer {
// 构建最小的 ACK 消息——细节此处省略
const ack = `MSH|^~\\&|...|ACK|...|${Date.now()}|...`;
return Buffer.from(`\x0b${ack}\x1c\x0d`);
}
const server = net.createServer((socket) => {
socket.on('data', (data) => {
try {
// 1️⃣ 接收(去除 MLLP 帧)
const rawMessage = stripMLLP(data.toString());
// 2️⃣ 翻译(ACL 正在工作!)
const cleanEvent = HL7ToDomainTranslator.translateAdmission(rawMessage);
// 3️⃣ 交给现代业务逻辑处理
console.log('New clean event:', cleanEvent);
admissionController.handleNewAdmission(cleanEvent);
// 4️⃣ 确认(HL7 要求的 ACK)
socket.write(createAck(rawMessage));
} catch (err) {
console.error('Failed to process HL7', err);
// TODO: 在此发送 NACK(否定 ACK)
}
});
});
server.listen(5000, () => console.log('Listening for Hospital Data on port 5000'));
回顾
- Facade – 处理原始的 TCP/MLLP。
- Adapter – 将管道分隔的字符串解析为结构化对象。
- Translator – 将该对象映射到干净的领域模型,保持 HL7 知识的隔离。
通过将所有 HL7 特定的逻辑限制在 ACL 中,你可以保护其余代码库免受遗留问题的侵蚀,保持架构整洁,并避免堆积如山的技术债务。 🚀
全屏控制
- 进入全屏模式
- 退出全屏模式
为什么这样更好
- 解耦: 如果医院明年从 HL7 v2 切换到 FHIR,你只需要重写
Translator类。你的admissionController和PatientAdmission模型保持不变。 - 可测试性: 你可以使用示例文本文件对
Translator进行单元测试,而无需真实的 TCP 连接。 - 健壮性: 你的核心业务逻辑不再充斥着
split('^')和PID.5的引用。
关于“购买 vs. 自建”的说明
虽然上面的代码写起来很有趣,但在生产环境中处理 MLLP 套接字相当棘手(超时、缓冲区碎片、VPN 隧道)。
在真实的企业场景中,你可能会把 ACL 的 基础设施 部分视为购买的服务。像 Mirth Connect(NextGen Connect) 这样的工具,或 AWS HealthLake、Google Cloud Healthcare API 等云服务,都可以充当你的物理 ACL。它们会接收原始的 MLLP 并将干净的 JSON 推送到你的 HTTP webhook。
然而,即使你收到了 JSON,你仍然需要一个逻辑 ACL 来将它们的模式映射到你的模式。绝不要盲目信任外部模式!
总结
与传统医院系统的集成是健康科技开发者的必经之路。过程可能会很混乱,但只要有稳固的反腐层(Anti‑Corruption Layer),就不必导致系统被破坏。将混乱隔离,只翻译一次,并保持你的领域模型清晰。
我在个人博客中写了更多关于具体工程模式和集成策略的内容,欢迎查看更多技术指南,如果你正面临类似的架构挑战。
祝编码愉快,愿你的套接字连接永远保持打开状态!