在 HL7 噩梦中生存:将现代 SaaS 与传统医院系统解耦的策略

发布: (2025年12月28日 GMT+8 00:00)
9 min read
原文: Dev.to

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(最小下层协议)。这意味着你必须:

  1. 打开一个原始 TCP 套接字。
  2. 监听特定的起始字节(0x0B)。
  3. 读取直到结束字节(0x1C 0x0D)。
  4. 立即发送确认(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 类。你的 admissionControllerPatientAdmission 模型保持不变。
  • 可测试性: 你可以使用示例文本文件对 Translator 进行单元测试,而无需真实的 TCP 连接。
  • 健壮性: 你的核心业务逻辑不再充斥着 split('^')PID.5 的引用。

关于“购买 vs. 自建”的说明

虽然上面的代码写起来很有趣,但在生产环境中处理 MLLP 套接字相当棘手(超时、缓冲区碎片、VPN 隧道)。

在真实的企业场景中,你可能会把 ACL 的 基础设施 部分视为购买的服务。像 Mirth Connect(NextGen Connect) 这样的工具,或 AWS HealthLakeGoogle Cloud Healthcare API 等云服务,都可以充当你的物理 ACL。它们会接收原始的 MLLP 并将干净的 JSON 推送到你的 HTTP webhook。

然而,即使你收到了 JSON,你仍然需要一个逻辑 ACL 来将它们的模式映射到你的模式。绝不要盲目信任外部模式!

总结

与传统医院系统的集成是健康科技开发者的必经之路。过程可能会很混乱,但只要有稳固的反腐层(Anti‑Corruption Layer),就不必导致系统被破坏。将混乱隔离,只翻译一次,并保持你的领域模型清晰。

我在个人博客中写了更多关于具体工程模式和集成策略的内容,欢迎查看更多技术指南,如果你正面临类似的架构挑战。

祝编码愉快,愿你的套接字连接永远保持打开状态!

Back to Blog

相关文章

阅读更多 »