2020 年 4 月,德国每一家医疗 startup 突然变得重要了起来。药店不堪重负。医生们第一次开始做视频问诊。患者在邮寄纸质处方。整个国家猛然意识到,处方流转——从医生开方到患者拿药的全过程——几乎还是纯模拟的。在汉堡,我加入了 meinrezept.online,帮他们搭建嵌入这个流程中间的后端。

这篇不是”我们救了人”的帖子。这是一篇”工程实际长什么样”的帖子,因为这段工程工作真的很有意思,而且从外面完全看不见。

处方格式问题

在把处方路由到药店之前,你得先能读出来。2020 年的德国,这意味着要处理多种格式:Muster-16 纸质表格、还未标准化的早期数字处方格式、传真扫描件,以及医生诊所用各种诊所管理软件生成的 PDF。

每种格式都有不同的提取路径。纸质表格有已知布局,但以扫描件的形式进来。数字格式有结构化数据,但不是所有药店都能处理所有格式。诊所管理软件生成的 PDF 是最糟糕的情况:结构化数据被锁在一个从没想过被程序解析的文档格式里。

后端的职责是把这一切归一化为一个统一内部表示,让系统的其他部分——药店路由层、订单跟踪服务、患者通知服务——无需关心输入格式就能消费。这是个出奇难的问题,尤其是当输入格式还在实时变化的时候——2020 年就是这样,德国政府正在积极推进 Elektronisches Rezept(电子处方)的落地。

药店集成天生就是遗留重灾区

从后端的视角看药店集成是这个样子:每家大型连锁药店都有自己的订购系统,很多独立药店在跑 WINAPO、ProFit 或 ADG——这些都是几十年前的诊所管理系统,暴露的订购接口用的是你在现代后端语境下根本没听说过的协议。HTTP/JSON 不是主流的 wire format。EDIFACT 和 HL7 变体到处都是。有些药店有个网页门户,还有人逆向工程出来了非官方 API。有些什么都没有,你只能发结构化邮件。

这不是抱怨。这就是面向消费者端的医疗集成长什么样。我在其他地方做过的医院级 EHR 工作(AFAQ、HAKEEM)运作在不同的抽象层——你用 HL7 FHIR,你有专用集成引擎,你在药店或实验室有技术对接人。在消费者药店 app 这个规模,你往往要对接的是药店实际拥有的东西,那通常远比你期望的少。

RabbitMQ 在这里做消息 broker,在这个规模下很合适。我们没有 Kafka 级别的体量;我们处理的是数量可控的服务之间的可靠投递,需要 retry 语义、死信处理,以及把入站订单接收和出站药店 dispatch 解耦的能力。RabbitMQ 处理这些很干净。exchange 拓扑——按药店地区、处方类型和配送路径做 topic exchange routing——让我们在不动每个 consumer 的情况下增加路由逻辑。

三种语言、六周、Microservices

技术栈是 PHP 7、Python 和 Node.js。这就是现实世界。你继承了第一个服务的语言,为下一个问题挑了最合适的工具(Python 做提取工作,Node.js 做实时状态 API),最后得到一个靠服务间消息契约而非语言一致性维系的多语言 codebase。

说实话,消息契约纪律比语言一致性更重要。如果每个服务都对 order.received 事件长什么样达成共识——schema、版本化、必填字段——那么 producer 是 PHP 还是 consumer 是 Node.js,只是实现细节。危险的失败模式是让每个服务各自演化对领域的理解,导致 order.received 对药店 dispatch 服务和对患者通知服务来说意思略有不同。你会在最糟糕的时候发现这一点——患者打来电话说收到了”处方已发货”通知,但药店表示没有这笔订单的记录。

前端是 ReactJS,不是我主要负责的部分,但它通过状态 API 连到同一套领域模型。患者在 UI 看到的内容和后端事件日志里记录的一致性,是一个被低估的集成关切——在疫情期间的订单量把我们没经过规模测试的 timing gap 全部暴露出来之后,这一点变得非常显眼。

疫情时间线把一切都放大了

2020 年 4 月入职,意味着加入一个产品突然被真实用户需求进行负载测试的团队——而且是任何 staging 环境都没有预料到的规模。医疗焦虑是真实的流量驱动因素。入站订单量在任何新闻事件周围都会出现峰值——封城通知、药店营业时间变化、关于药品短缺的新闻报道。你会很快搞清楚你服务的哪些部分是有状态的、哪些不是,因为有状态的部分就是在负载峰值时以不优雅的方式挂掉的那些。

这以非常具体的方式教会了我:提取 pipeline 需要和订单接收路径解耦。一个慢的药店集成不应该阻止患者处方被接收和确认。确认和履约是不同的关切,应该独立失败。我们是在线上流量下做的这个改造——不理想,但这是真实的工程记忆。

跨医疗领域的跨度

我在两个截然不同的规模上交付过医疗软件:大型企业级 EHR 和临床工作流系统(医院级别,数十万患者记录,复杂的临床决策支持),以及面向消费者的医疗 app(处方订购、患者通知、药店路由)。它们共享监管要求——患者数据处理、处方有效性、审计追踪——但几乎没有其他共同之处。

面向消费者的一侧迭代周期更快,和集成伙伴的关系没那么正式,还有更大的压力去处理数据的长尾边缘情况(奇怪的处方格式、周末不响应的药店)。企业侧有更结构化的集成,正式的 HL7 和 FHIR 契约,变化周期更慢。两边都难。这个跨度是有价值的。

我一直在构建 Fulcrum——一个 local-first 的 agent 控制面——部分原因是我在分布式医疗后端学到的协调模式直接适用于 agent 编排。可靠投递、死信处理、schema 版本化的事件契约:问题是一样的,领域不同。