轻易云
注册体验

多平台对账的"统一抽象 vs 平台特化":架构选择的工程哲学

· 陈洁琳· AI 财务对账· 27 次浏览· 约 14 分钟读完
统一抽象平台特化架构选择分层决策抽象基类约定规范平台聚合范式多平台治理

多平台对账的"统一抽象 vs 平台特化":架构选择的工程哲学

摘要:「多平台对账系统应该抽一个统一抽象基类,还是让每个平台各自实现?」——这是每个架构师都会撞上的问题,标准答案通常分两派:「统一抽象派」认为代码复用 > 一切,「平台特化派」认为业务差异 > 一切。本文给出第三种答案:这不是一道是非题,是 6 道分层选择题。数据层必须平台特化(账单物理结构决定)、契约层必须统一抽象(用户期望决定)、判定层必须平台特化(算法依赖结构)、差异语义层必须统一抽象(机读码需要可比)、执行层必须统一抽象(沙箱、队列、状态机)、扩展机制必须平台特化(每平台独立演进)。选错层的代价不是代码冗余、是指数级返工成本——v4.0 → v3.0 重构那次,我们为了把"统一抽象"从数据层挪走,烧了 6 周 × 3 人。

技术评审会上那个永不消失的问题

2026 年 5 月,我们启动 v4.0 → v3.0 重构。第一次架构评审会上,一位资深工程师提出了那个永远会出现在多平台系统评审里的问题:

"5 个平台 × 4 个账单类型 = 20 个 BillRow 模型,字段重合度 60%+,要不要抽一个 AbstractBillRow 基类?"

紧接着是对账侧:"要不要抽 IReconciliationStrategy 接口统一 5 个平台的判定算法?" 转化侧:"要不要抽 ITransformRule 把 164 条规则的平台特化部分挤掉?"

三个问题问完,会议室安静了 30 秒。我当时的回答是:先别回答。这不是一道是非题,是 6 道选择题——每一层都有不同的答案。

那天的结论是"先按平台特化写出 5 个最小可用版本,再回头看哪些层真的需要统一抽象"。6 周后我们回头看的时候,发现 6 个层里只有 2 层需要统一抽象,4 层必须平台特化——这个比例不是我事先猜到的,是 5 个平台真实业务逼出来的。

下面这张图就是 5 个已落地平台 + 3 个最近接入平台的全景,每个平台的对账特征都不一样——这种"业务侧自然就长得不一样"的现状,是讨论"统一 vs 特化"的唯一起点:

多平台接入架构图:5 个已落地平台(京东 POP 整单轧差 8 步骤 / 抖店五轮匹配 / 支付宝/天猫三表 / 亚马逊分向不轧差 / 速卖通 5 billType + JIT)+ 3 个最近接入(拼多多 / 视频号 / 小红书)+ 5 个未来扩展平台

一、6 层架构 × 6 个分层决策

多平台对账系统不是一个单一系统,是 6 个层次的叠加。每一层回答的问题不一样,需要的"统一抽象率"也不一样。架构师的工作不是做一道统一抽象 vs 平台特化的二选一,是给 6 个层分别选:

层级回答的核心问题决策理由
L1 数据层"账单物理结构长什么样?"平台特化账单结构是业务世界的物理事实,不是工程选择
L2 契约层"调用方期望拿到什么?"统一抽象用户期望"调一次拿到所有平台",契约必须统一
L3 判定层"差异怎么算出来?"平台特化算法依赖账单结构(整单轧差 vs 分向不轧差)
L4 语义层"差异怎么命名、怎么机读?"统一抽象标签需要跨平台可比、跨报表可聚合
L5 执行层"脚本怎么跑、日志怎么落?"统一抽象沙箱、队列、状态机是基础设施,与平台无关
L6 扩展层"新平台怎么接进来?"平台特化每平台独立演进,扩展点必须平台隔离

把这张表背下来。当你面对"要不要抽基类"的问题时,第一步是问自己"这个问题在 6 层里属于哪一层"——答案错了,怎么选都是债。

下面这张图是 v4.0 旧版与 v3.0 新版的对比,左边红色的是"统一抽象派"的代价——20 张含抽象基类的表 + tenantId 多租户 + deletedAt 软删除 + 5 层沙箱防护 + 抽象基类继承;右边绿色的是 v3 重构后的形态——24 张独立表 + 约定规范 10 条 + 平台聚合范式 + 双层沙箱 + 23 个 diffReason 标签:

v3 重构 v6 拍扁架构对比图:左侧红色 v4.0 旧版列出 5 大过度设计问题(20 张表含抽象基类、tenantId 多租户、deletedAt 软删除、5 层沙箱防护、抽象基类 + 5 子表继承),右侧绿色 v3 新版展示 24 张独立表 + 约定规范 10 条 + 平台聚合范式

二、L1 数据层:必须平台特化("基类 = 债")

L1 数据层的答案永远是平台特化,没有例外。原因是账单物理结构是业务世界本来就长得不一样,不是工程选择:

  • 京东 POP 账户流水(gb18030 编码、前 6 行汇总、第 7 行表头):核心字段是「商户号 + 账户代码 + 收支方向」
  • 亚马逊 Transaction(UTF-8 BOM、前 9 行元数据、第 10 行表头、15 个金额列宽表):核心字段是「settlement id + type + sku + 15 个 fee 列」
  • 支付宝账户流水(utf-8-bom、含 20+ 列资金流字段):核心字段是「支付宝交易号 + 流水号 + 对方账户 + 业务基础订单号」
  • 抖店 account_flow:核心字段是「子行 rowSeq + 主行/子行拆分 + 拆行注释」
  • 拼多多 capital:核心字段是「资金类型 + 店铺编号」

如果硬抽基类,必然出现 4 种事故场景:

  1. 基类被迫加 60 个字段——为了覆盖所有平台特有字段,基类变成"全字段字典",每个 BillRow 只填其中 1/3,其余 2/3 是 null;
  2. 基类一次改动 = 全平台编译失败——亚马逊加一个 settlement_id 字段,京东 / 抖店 / 支付宝的 BillRow 都要跟着加,否则 TS 报错"未实现父类抽象字段";
  3. 基类的 aggregate() 模板方法——5 个平台的聚合逻辑(亚马逊按腿聚合 vs 京东整单轧差 vs 抖店五轮匹配 vs 支付宝三表对账 vs 拼多多单腿)根本无法用同一个模板描述,最后基类的 aggregate() 退化成 if (platform === "AMAZON") ... else if ... 的另一种耦合;
  4. 新人 onboarding 时被迫读基类——新人想了解京东聚合,必须先读 200 行的 AbstractBillRow 基类代码、再读 200 行的京东子类,无法独立读懂京东。

我们当前的实际状态是 23 张完全独立的 BillRow 表(apps/api/prisma/schema/jdpop.prisma × 3 + douyin.prisma × 1 + amazon.prisma × 2 + alipay.prisma × 3 + sph.prisma × 2 + xiaohongshu.prisma × 2 + pdd.prisma × 1 + independent-site.prisma × 4 + aliexpress.prisma × 5 = 23),分布在 9 个平台专属的 .prisma 文件里。这些表之间没有任何 extends / interface 关系,没有任何外键关联(BillRow 之间的关联是通过 businessOrderNo 业务字段跨表 join,不是通过 SQL 外键)。

数据层的硬约束:3 条

约束出处违反后果
不抽抽象基类23 张表实际状态基类变成"全字段字典"
跨表关联走业务字段businessOrderNo 业务层 join加索引成本陡增、外键循环依赖
字段命名靠 10 条约定CAT.* / SR.* / ZC.* 命名规范命名混乱、grep 失效

[来源:2026-05 v3 业务规范数据模型定稿记录]

三、L2 契约层:必须统一抽象(用户期望的"一次接入")

L2 契约层的答案永远是统一抽象。原因是用户的期望是"调一次接口拿 5 个平台"——契约层不统一,整个产品就是个 5 个工具的拼盘。这也是轻易云多平台治理哲学在契约层的核心:用户面对的是「一个对账系统」,不是「5 套对账工具」。

平台聚合范式:2 个标准方法 + 3 个 TypeScript 接口 + 1 个注册表

契约面的核心是 apps/api/src/biz_reconciliation/common/platform-aggregation-registry.ts 一个文件,4 个 TypeScript 定义撑起所有平台的对外契约:

typescript
// apps/api/src/biz_reconciliation/common/platform-aggregation-registry.ts

export interface PlatformAggregation {
  aggregateIncome(input: IncomeAggregationInput): Promise<IncomeAggregationRow[]>;
  aggregateExpense(input: ExpenseAggregationInput): Promise<ExpenseAggregationRow[]>;
  /** 无收入订单费用(可选):订单账期内无主营收入的费用行按 订单 × 核算项目 汇总 */
  aggregateNoIncomeFees?(input: ExpenseAggregationInput): Promise<NoIncomeFeeRow[]>;
}

const FACTORIES: Partial<Record<PlatformEnum, (prisma: PrismaClient) => PlatformAggregation>> = {
  [PlatformEnum.JD_POP]: createJdpopAggregation,
  [PlatformEnum.AMAZON]: createAmazonAggregation,
  // ... 8 个平台注册项
};

这是 L2 统一抽象的典型形态——接口用 TypeScript interface 声明、注册表用 Partial<Record<PlatformEnum, Factory>> 静态映射。IncomeReconciliationService.startReconcile() 调用方一行代码不区分平台:

typescript
const aggregation = FACTORIES[platform]!(prisma);
const rows = await aggregation.aggregateIncome(input);

这个范式从 2026-07-17 落地到 2026-09-20 接入第 7 平台 XHS 小红书,一共加过 4 次平台(PDD / INDEPENDENT_SITE / ALIEXPRESS 速卖通 / XHS 小红书)、加过 1 个可选第三方法 aggregateNoIncomeFees,没有一次动过注册表或调用方代码——这就是「约定规范代替抽象基类」的力量。

[来源:2026-09-20 第 7 平台接入实测 + 平台聚合注册表调用链路]

契约层的硬约束:3 条

约束出处违反后果
接口签名 6 个月内不允许改第 7 平台接入 0 改动实测调用方编译失败
新方法必须可选(? 后缀)aggregateNoIncomeFees 范例老平台被迫实现空方法
注册表只允许加行不允许减行v3 以来 0 减行平台下线时全调用方报错

下面这张图是业务模块架构全景——上半部是平台基座(统一抽象:Auth / Gateway / Queue / Settings / Prisma / Redis),下半部是 biz_reconciliation(平台聚合范式承载多平台治理):

业务功能模块架构图:上半部平台基座(Auth JWT + App-Key、Gateway 限流、Queue BullMQ、Settings 系统设置、Prisma 多文件 schema、Redis 缓存+队列)+ 下半部 biz_reconciliation 业务域(账单导入、解析沙箱、收入对账、费用对账、集成转换、集成中心、AI Agent、知识库 RAG、报表分析、数据安全)

四、L3 判定层:必须平台特化(算法依赖账单结构)

L3 判定层的答案是平台特化,且这一层的特化是最有业务深度、最不能合并的特化。原因是判定算法依赖账单的物理结构——账单结构不一样,算法就不一样,没有"通用算法"这回事。

京东整单轧差 vs 亚马逊分向不轧差是最经典的对照案例(详见《整单轧差 vs 分向不轧差:京东 / 亚马逊的两种对账策略》一文)。根因在账单层:

  • 京东账单按商家业务订单号归集——同一订单的收款 + 退款落在同一组体行、能合并成净额;
  • 亚马逊账单按平台单号归集——同一订单的收款和退款是两条独立体行、天然分向。

这两件事不是工程选择,是物理事实。强行合并判定算法会出现「if (platform === "AMAZON") ... else if ...」的反模式——基类退化成另一种耦合。

判定层的硬约束:3 条

约束出处违反后果
每个平台一个 .ts 文件jdpop.reconciliation.ts × 5合并文件后 5000 行
严格相等无容差为原则2026-08-04 v3.0.0 定稿容差模糊券差异、补差价
承载行机制强制京东 v3.3.1 + 抖店 v3.6.3 同构互抵单被闸门误判

我们当前 5 个平台收入对账脚本的实际长度:京东 v3.4.2 1058 行、抖店 v3.6.3 777 行、支付宝 v3.6.5 1109 行、亚马逊 v3.9.6 932 行、速卖通 v3.x.x 856 行——5 个平台 × 平均 950 行 = 4750 行的"代码即业务"。这 4750 行不能合并,也不应该合并——每行都是该平台独有的判定逻辑,合并后就是 5000 行的"伪基类",新人 onboarding 时不知道从哪读起。

五、L4 语义层:必须统一抽象(标签需要跨平台可比)

L4 语义层的答案是统一抽象。虽然判定算法平台特化,但判定结果的命名必须统一——因为 C 端的报表、跨平台分析、自动化处置都要求差异标签跨平台可比。

23 个 diffReason 机读码就是 L4 统一抽象的产物:

类别标签数代表码跨平台复用
取消/退货类4CANCELLED_UNSHIPPED / AFTER_SALE_SERVICE_DIFF / GIFT_WIPES5 平台共用
金额差异类5AMOUNT_DIFF / PRICE_DIFF_ORDER / PRICE_PROTECT_DIFF4 平台共用
部分结算类3PARTIAL_SETTLEMENT / DIRECT_COMPENSATION3 平台共用
平台特化类7FBA_REIMBURSEMENT / INSTANT_REFUND / JIT_AMOUNT_DIFF1-2 平台专用
其他4OTHER + 自定义兜底全部

[来源:INCOME_DIFF_REASON_LABELS 机读码 + 中文标签映射]

L4 的关键纪律是"机读码与人读标签分离"——AMOUNT_DIFF 是机读码(机器判定、自动化处置脚本消费),"金额差异" 是人读标签(业务人员理解)。两者 1:1 映射、互相冗余、永远同步。

语义层的硬约束:3 条

约束出处违反后果
标签机读码不可改自 2026-09 起冻结已有数据失读、自动化脚本失效
新增机读码走 10 处同步链platform-script-onboarding 同步链条章单边遗漏导致报表不准
中文标签可改但需留 alias历史 联合优惠券/立减差异 多版本老数据需重新映射

六、L5 执行层 + L6 扩展层:执行统一 / 扩展特化

L5 执行层必须统一抽象。原因是沙箱、队列、状态机是基础设施,与平台无关。沙箱代码只关心"安全隔离 IPC + 30 秒超时 + SIGKILL",队列只关心"并发数 + 重试 + 失败兜底",状态机只关心"7+5+3+4+4 态迁移规则"——这些代码0 行业务字、0 行平台特化,天然就是统一抽象的最佳土壤。

apps/api/src/queue/queue.constants.ts 是 L5 的典型——5 个平台 × 5 个队列 + 4 个 worker + JobTask 4 态状态机,完全没有平台特化代码:

typescript
// apps/api/src/queue/queue.constants.ts
export const QUEUE_BILL_IMPORT_JD_POP = 'queue:bill:import:jd_pop';
export const QUEUE_BILL_IMPORT_DOUYIN = 'queue:bill:import:douyin';
export const QUEUE_BILL_IMPORT_ALIPAY = 'queue:bill:import:alipay';
export const QUEUE_BILL_IMPORT_AMAZON = 'queue:bill:import:amazon';
export const QUEUE_BILL_IMPORT_ALIEXPRESS = 'queue:bill:import:aliexpress';

export const QUEUE_INCOME_RECONCILE = 'queue:income:reconcile';
export const QUEUE_EXPENSE_ALLOCATE = 'queue:expense:allocate';

L6 扩展层必须平台特化。每平台独立演进,扩展点必须平台隔离。新平台接入流程是 L6 的黄金案例——8 步 checklist(聚合注册 → 核算项目 → 解析脚本 → 对账脚本 → 转换脚本 → 规则 seed → 知识库 / help → 验证)每一步都是平台独立的:

步骤平台特化体现接入实测耗时
聚合注册1 个 .ts + 1 行注册表0.5 天
核算项目 seed平台专属 L3 叶子0.5 天
解析脚本平台专属 BillRow + 解析函数2 天
对账脚本平台专属判定算法(按标准骨架)4 天
转换脚本收入 + 费用各一平台专属转换逻辑3 天
差异机读码优先复用,新增走 10 处同步1 天
知识库 + help 主题平台专属文档1 天
验证 + 批次轧平平台专属 harness 回放2 天

5 + 3 + 独立站 = 8 个平台平均接入耗时 14 天 → 第 7 平台 XHS 小红书接入耗时降到 9 天(复用既有 diffReason + 既有核算项目)——这就是 L6 平台特化的成本红利。

[来源:2026-09 新平台接入 8 步 checklist + 5 平台版本演进史]

下面这张图是多店多平台统一对账的 hub-and-spoke 视图——中心是统一对账引擎(聚合范式承载),周围是 38+ 店铺(5 平台),每个店铺走独立的解析+对账+转换路径,但被同一套调度、同一套状态机、同一套差异标签统一管理:

多店多平台统一对账流程图:中心是轻易云统一对账引擎(teal 色 hub),周围 38+ 店铺按平台分组(京东 POP 10 店、抖店 20 店、支付宝 5 店、亚马逊 3 站),每个店铺走独立的解析+对账+转换路径,但被同一套调度、同一套状态机、同一套差异标签统一管理

七、错层的代价:v4.0 → v3.0 那 6 周的复盘

为了给"分层决策"提供反向证据,我把 v4.0 时代选错层的代价摆在下面——这不是代码冗余的成本,是指数级返工的成本:

选错层的场景v4.0 时代真实代价v3.0 修复方法
数据层抽 AbstractBillRow 基类200 行基类 + 5 子类继承,亚马逊加字段全平台编译失败删基类、23 张独立表、约定规范 10 条
契约层漏统一(5 个平台 5 套 API)前端要写 5 套适配层平台聚合范式(4 interface + 1 注册表)
判定层强行模板方法200 行 aggregate() 退化为 if-else 链5 个平台 5 个独立 .ts、严格标准骨架
语义层每个平台自己造码5 套差异标签无法跨平台报表23 个 diffReason 机读码统一
执行层 5 套沙箱5 套 IPC + 5 套超时配置,运维不可观测双层沙箱 + 队列统一抽象
扩展层没有约定新平台接入 30 天起8 步 checklist + 复用既有 diffReason 降到 9 天

[来源:2026-05-04 至 2026-06-15 v4.0 至 v3.0 重构记录]

八、3 条心法(架构师的判别准则)

把 6 层决策浓缩成 3 条心法,架构师面对"要不要抽基类"问题时按顺序问自己:

心法 1:"业务的物理事实"先于"工程美感"

字段重合度 60%+ 是物理事实造成的吗?是 → 平台特化。是因为历史命名混乱造成的吗?→ 约定规范治理。在 L1 数据层,账单物理结构永远是业务世界的真,工程美感是次要的——宁可 23 张独立表难看,不可 1 个基类债重。

心法 2:"用户期望"决定 L2 契约是否统一

用户期望"调一次拿到所有平台" → L2 必须统一抽象;用户期望"按平台独立调用" → L2 可以平台特化。L2 抽象率过高 = 浪费,过低 = 难用。当前 5 个平台的 aggregateIncome / aggregateExpense 抽象率 = 100%(每平台必须实现 2 个方法),第 3 方法 aggregateNoIncomeFees 抽象率 = 25%(仅亚马逊实现)。

心法 3:"扩展成本"决定 L6 是否平台隔离

如果新平台接入耗时是 30 天起 → 扩展层出了问题(要么 L2 抽象过紧、要么 L3 骨架不清晰);如果新平台接入耗时降到 9 天 → L6 平台隔离做对了。L6 是架构决策的"延迟验证"——3 个月后回头看接入耗时就知道了。

九、整体架构视角:4 层分层 × 6 层决策

把 6 层决策叠到 4 层架构上(表现层 / 业务层 / 数据层 / 基础设施层),你能看到统一抽象和平台特化是交错出现的,不是泾渭分明的两层:

整体技术架构图:4 层分层架构——表现层(Next.js 15 App Router + RSC + Tailwind + VTable)、业务层(biz_reconciliation 多平台治理)、数据层(PostgreSQL 多文件 schema + pgvector + Redis)、基础设施层(NestJS 沙箱 + 队列 + 集成中心 + AI Agent)

统一抽象带在"基座"上(沙箱 / 队列 / 状态机 / Prisma / Redis),平台特化带在"业务层内部"(数据层 + 判定层 + 扩展层)。这就是分层决策的可视化结果——架构图不再是非此即彼的"统一 vs 特化",而是6 个有界面的层、每个层有自己明确的抽象率。

十、给架构师的 take-away

回到开篇的问题:「多平台对账系统应该抽一个统一抽象基类,还是让每个平台各自实现?」

答案是:先别回答,问自己 6 个分层问题:

  1. 这是数据层吗?→ 平台特化(账单物理结构是事实)
  2. 这是契约层吗?→ 统一抽象(用户期望)
  3. 这是判定层吗?→ 平台特化(算法依赖结构)
  4. 这是语义层吗?→ 统一抽象(标签需要可比)
  5. 这是执行层吗?→ 统一抽象(基础设施无关业务)
  6. 这是扩展层吗?→ 平台特化(每平台独立演进)

6 个问题问完,6 个答案几乎自动出现——数据层 23 张独立表、契约层 4 interface + 1 注册表、判定层 5 个独立 .ts、语义层 23 个 diffReason 统一码、执行层双层沙箱 + 队列、扩展层 8 步 checklist。这不是一道抽象 vs 特化的二选一,是6 道分层选择 + 每层独立的最优解。

这套工程哲学我们叫它「分层抽象率」——不追求单一层的最高抽象率,追求 6 层的最适配分布。它帮我们把第 5 平台支付宝接入耗时压到 9 天,把第 7 平台小红书接入耗时至 9 天(复用既有 diffReason + 核算项目)——这就是把抽象成本花在"对的位置"的回报。

如果你想在自己的对账系统里复用这套分层决策法,完整工程范式沉淀在项目内的 platform-script-onboarding skill 中——6 层决策的每层硬约束、错层代价的真实数据、新平台接入的 8 步 checklist,都在那里整理好了。

架构决策不追求"全局最优抽象",追求"每一层的最适配抽象率"——这就是多平台对账系统统一抽象 vs 平台特化的工程哲学答案。

本文为原创内容,转载请注明出处:https://www.qeasy.cloud/insights/reconciliation/4-6-4-unified-abstraction-vs-platform-specialization

评论