我在 payever 待了两个月。2020 年 2 月和 3 月——你可能还记得,这正好是疫情把一切锁死、所有人计划全部作废的前两个月。短暂的差事,具体的工作。这篇就是写那段工作的。

payever 做的是电商基础设施——让商家从一个平台统一管理多渠道销售的产品。交给我的具体问题:为接入 Mobile.de、Amazon.de 和 eBay 搭建后端 microservices。三个 marketplace。三套截然不同的数据模型。一个让平台能一致工作的统一领域表示层。

你认识几个工程师,能从亲身经历描述 Mobile.de 车辆 listing API 的?就是这种稀有程度。

三个 marketplace,三种世界观

理解 marketplace 集成最重要的一件事:每个 marketplace 不只是一套不同的 API。它是一套对”商业是什么”的不同概念模型。

Amazon 把 catalog 和库存分开来想。一个商品有 ASIN——Amazon 对世界上某样东西的权威标识符。你的 listing 不是商品本身,而是你向 Amazon 提出的”我要卖这个 Amazon 已知商品”的报价。这个区别对你如何建模更新有深远影响:改价格,你改的是报价;改商品描述,你可能在试图修改 Amazon 对这件商品的权威记录——Amazon 对这件事有很强的意见。基于 feed 的更新流(当时是 MWS,现在是 SP-API)是异步的、基于确认的。你提交 feed,拿到 submission ID,然后轮询结果。错误处理不是一个简单的 HTTP status code,而是一个你得自己解析的结构化 feed 结果文档。

eBay 更像直接的拍卖/listing 模型。你完全拥有自己的 listing——没有 catalog 消歧义层。这听起来更简单,某种程度上也确实如此;代价是你的 listing 是一座孤岛,你得对它的完整数据负责。集成复杂度转移到了订单流:eBay 的订单模型有它自己的买家消息、反馈、纠纷解决和退货发起等概念,都要映射到平台内部的订单模型上。

Mobile.de 是那个在饭局上最有趣可以聊的——因为德国以外几乎没人知道它,软件圈里直接接触过它的人也寥寥无几。它是德国最大的二手车 marketplace。库存模型高度垂直——一辆车的 listing 有品牌、型号、年款、配置版本、里程、VIN、一套特定的车况分类体系,还有一组可搜索属性(燃料类型、变速箱、排放等级),这些属性在德国市场有其特殊意义,而一个通用电商平台天生不理解这些。API 文档写得很好,但对于只接触过横向零售集成的人来说,概念模型相当陌生。

统一领域模型问题

平台的职责是给商家一个界面统一管理这一切。这意味着后端的职责是维护一套内部领域模型,既能忠实表示三个平台上的 listing,又能在它们之间双向同步更新。

这才是真正难的地方。你的内部模型里的”商品”必须包含足够多的信息,以便能够:

  • 以 feed entry 的形式提交给 Amazon,对应到已知的 ASIN;或者在没有 ASIN 时触发新商品创建请求
  • 在 eBay 上发布 listing,完整拥有 listing 数据,包括类目、商品属性和运费政策
  • 在 Mobile.de 上发布 listing,填入正确的车辆分类字段——这些字段在 Amazon 和 eBay 上完全不需要

最简单粗暴的做法是一个超大商品 schema,把三个 marketplace 的所有字段都塞进去。这是错的。这些字段不只是不同,而是语义上不同。eBay 的”Condition”(全新、二手、零件或无法使用等)和 Mobile.de 的车况不是同一个概念——Mobile.de 有自己的评级词汇。一个试图把这些差异压平的统一模型,产出的是一个撒谎的 schema。

正确做法是建立一个核心领域模型,承载真正通用的字段(标题、价格、库存数量、媒体文件),同时为每个 marketplace 做类型化的扩展,和核心记录一起存储。发布到某个 marketplace 时,把核心记录和对应的扩展组合起来;从 marketplace 收到更新时,再拆解回来。MongoDB 的文档模型很适合这个场景,因为扩展字段因 marketplace 而异,不存在一个固定 schema 能在关系表里好好放的。

NestJS 作为接缝层

服务层用的是 NestJS——TypeScript on Node.js,带有 Angular 风格的模块组织。对于这类集成工作,模块边界真的很有用:Amazon 模块负责 Amazon wire format,eBay 模块负责 eBay wire format,核心商品模块负责领域模型。依赖方向清晰。Amazon 模块里没有任何东西依赖 eBay 怎么表示一个 listing。

RabbitMQ 处理异步工作:需要时间处理的 marketplace feed、每个 marketplace 进来的订单通知、商家改价时需要广播到所有激活渠道的库存更新。这里的 exchange routing 很直接——每个 marketplace 一个 exchange 用于入站事件,一个 exchange 用于内部商品更新并扇出到所有已启用的渠道发布者。发布者就是那些知道如何把内部更新翻译成 marketplace 专属 wire format 的模块。

八周,值多少

两个月时间不多,我不打算装作集成已经完整。我们在这个窗口里交付的是三个 marketplace 的核心商品推送路径和入站订单同步。退货流程、消息集成、完整的错误对账循环——那些还在 roadmap 上。

两个月足够做到的是:深入理解这个领域,形成有价值的判断。我知道 Mobile.de 的车辆分类体系从内部看是什么样的。我知道 Amazon 的异步 feed 确认流程在 feed 校验失败时是什么样子。我知道 eBay 的类目专属商品属性要求怎样决定一个 listing 能不能上线。这些不是抽象的架构概念——它们是我读日志时在日志里出现的具体失败模式。

这种集成考古能力,大多数工程师需要比两个月更长的时间才能形成判断,因为大多数工程师从来不会同时接触这三个系统。压缩的时间线逼着你想清楚什么是重要的,什么是偶发复杂度。

我一直在把集成纪律注入 Fulcrum:领域之间的类型化契约、稳定的事件 schema、可组合的外部系统 adapter——这些思路直接适用于 agent 编排。问题的韵律相同,领域不同。而且和 payever 的合约不同,Fulcrum 的时间线不是两个月。