轻易云
注册体验

轻易云 v3 重构:去过度设计的 16 项关键决策

· 何海波· AI 财务对账· 60 次浏览· 约 15 分钟读完
技术哲学v3 重构去过度设计领域驱动设计单租户平台聚合范式

轻易云 v3 重构:去过度设计的 16 项关键决策

摘要:每一家做企业级 SaaS 的公司都声称自己在做「简化架构」。但真正把简化做到位的产品,背后一定有 16 项「反主流」的硬决策。本文不讲什么是好的架构原则,而是把 v3 重构(2026 年 5–8 月,4 个月时间)的 16 项关键决策完整摊开:为什么删 Python 沙箱只留 JS、为什么删软删除字段、为什么删抽象基类采用平台聚合范式、为什么删租户隔离采用单租户架构。这 16 项决策的共同母题只有一句话——「简洁优先,拒绝过度设计」。决策背后的真实收益是:代码量减少 42%、测试覆盖率提升至 85%、P95 性能提升 3.2 倍。

关键词:技术哲学、v3 重构、去过度设计、领域驱动设计、单租户

一、把「过度设计」当头号敌人,是 CTO 必修的工程判断

2026 年初,这套系统的代码盘点报告里有一组刺眼的数字:47 个 NestJS 模块、单模块最大 4,000+ 行、跨模块直接 import 调用占比 38%、单元测试覆盖率 31%、一次完整 tsc --noEmit 在 MacBook Pro M2 上需要 42 秒、CI 冷启动构建 2 分 17 秒。

这并不是「代码量大」的问题——是模块边界没有强制约束的问题。任何 service 都能直接 import 任何 service,跨模块依赖图变成乱麻,新人对账场景要改 5 个文件、改 3 处 schema。那时候整个团队的共识是「我们需要更严格的抽象、更细的层级、更全的中间件」。但当我们回看 v4.0 的实际运行情况时发现——真正阻塞生产力的不是抽象不够,而是抽象过度。

「抽象」这个东西,对账系统很容易走向几个误区:

  • 反正多租户是 To B 的标配,先把 tenantId 加到所有表里
  • 反正软删除比物理删除安全,先把所有表都加 deletedAt 字段
  • 反正 5 个平台逻辑有共性,先抽一个 AbstractPlatformImportService 基类
  • 反正异步任务要可靠,自己造一套 5 层沙箱防护

每一项单独看都很合理。但 16 项「合理」叠加在一起,就变成了一台谁都不敢动的代码机器。

2026 年 5 月,团队启动了 v3 重构(是的,版本号故意往回退——因为它代表「返璞归真」)。4 个月时间,把 47 个模块收敛到 9 个核心模块,代码量减少 42%,测试覆盖率从 31% 提升到 85%,P95 性能提升 3.2 倍。这篇文章把 v3 重构背后的 16 项关键决策完整摊开,让同行看完之后能直接对照自家系统做一次「过度设计体检」。

下面把这 16 项决策按删除类、简化类、纪律类三类逐项展开。

二、v3 vs v4:一张图看清过度设计的代价

v3 重构的第一周,我们就画了一张「v4 过度设计」全景图。它后来被收进了 v3 vs v4 架构对比图,作为整个项目的「反面教材」挂在新人入职培训的第一页。

轻易云 v3 重构 v6 拍扁架构对比图:左侧红色 v4.0 旧版列出 5 大过度设计问题(20 张表含抽象基类、tenantId 多租户、deletedAt 软删除、5 层沙箱防护、抽象基类 + 5 子表继承),右侧绿色 v3 新版展示收敛后的模块化结构

这张图把 v4.0 时代最严重的 5 类过度设计摆在左边:20 张表(含抽象基类)、tenantId 多租户、deletedAt 软删除、5 层自研沙箱防护、抽象基类 + 5 子表继承。每一条都在告诉我们——抽象是有负债的。

右边是 v3 重构后的形态:24 张表(每平台独立 + 双子计划 + 桥表 + 三层费用对账)、单租户架构(无 tenantId 字段)、物理删除(无 deletedAt)、isolated-vm + child_process 双层沙箱(用成熟开源组件)、无继承(约定规范 10 条 + 平台聚合范式)。

从 v4 到 v3 的代码量减少不是靠「删功能」实现的——v3 的功能集比 v4 还多(多了双子对账计划 + 集成转换 + AI Agent)。减少的 42% 全部来自「删抽象」。我们没有变弱,而是变轻了。

三、16 项关键决策:分三类讲透

16 项决策不是 16 个并列条目,按性质可以归为三类:

  • 删除类(5 项):把不该存在的代码、字段、抽象从系统中移除
  • 简化类(5 项):用更简单的方案替换更复杂的方案
  • 纪律类(6 项):用规则代替经验,把「靠人记」变成「靠工具约束」

下面按这三类逐一展开。

3.1 删除类(5 项):让代码库瘦下来的关键

决策 1:删除 Python 沙箱,只保留 JS

v4 时代做法:ScriptLanguage 枚举同时支持 JAVASCRIPT 和 PYTHON,沙箱分别用两个运行时实现。开发团队每接一个新平台,要写两个版本的脚本解析、两个版本的错误处理、两套调试工具链。

v3 重构做法:枚举里只剩 JAVASCRIPT(业务约定规范,2026-05 起),整个沙箱体系只剩一套 JS 实现。统一用 isolated-vm 隔离代码执行、child_process 处理进程级崩溃防护。

决策理由:Python 沙箱的覆盖率不足 12%(4 个平台用 JS,0.5 个平台半心半意用 Python),但维护成本是 JS 沙箱的 2 倍。一个 To B 系统「支持两种语言」的吸引力,远低于「只把一种语言做到极致」的工程纪律。

决策 2:删除软删除字段,物理删除

v4 时代做法:所有业务表都带 deletedAt + isDeleted 字段,软删除成为默认行为。代码里充斥着「先查 deletedAt: null、再考虑业务字段」的双重过滤逻辑。

v3 重构做法:schema 里完全没有 deletedAt / isDeleted 字段(v3 命名禁止项 §1.3),需要删除就 DELETE FROM ... WHERE id = ?。如果担心误删,由调用方在应用层做快照。

决策理由:软删除带来 5 个隐藏成本——① 查询默认带 deletedAt: null 过滤,索引设计被锁死;② 数据库膨胀;③ 「恢复」功能 99% 没人用;④ 字段不同步导致数据不一致;⑤ 审计日志应由专门事件表承担,不是业务表兼任。

决策 3:删除抽象基类,采用平台聚合范式

v4 时代做法:AbstractPlatformImportService 基类,5 个平台 service 继承。亚马逊分向不轧差、京东整单轧差、抖店五轮匹配这些平台特性都靠「覆盖基类方法」实现。

v3 重构做法:5 个平台 × 4 个文件(import service / import worker / rows service / rows controller)= 20 个结构一致但实现独立的文件,没有继承。京东在 jdpop/,亚马逊在 amazon/,平台特性直接写在对应文件里。

决策理由:当 5 个平台的「差异」比「共性」更多时,抽象基类就是一个负债。基类的每一次演进都要考虑「会不会破坏其他平台」,没人敢动基类。约定规范 10 条替代了继承——文件结构、字段命名、错误处理都按规范来,实现可以完全不同。

决策 4:删除租户隔离,采用单租户架构

v4 时代做法:TenantAwareModel 基类 + tenantId 字段 + TenantContextMiddleware 中间件 + 所有 Prisma 查询自动注入 where tenantId = ...。

v3 重构做法:schema 里完全没有 tenantId 字段(v3 命名禁止项 §1.3),没有租户中间件,所有业务数据全量。

决策理由:这套系统是 To B SaaS,但单租户运行(同一时间一家企业)。多租户的抽象开销——自动注入 tenantId、跨租户数据隔离测试、泄露审计——远超收益。NestJS 生态里常见的「TenantContextMiddleware 自动注入」方案,在我们真实场景里是过度设计。v3 重构的第一原则是「不做用不到的抽象」。轻易云在最初版本也走过「先做多租户、再考虑简化」的弯路,v3 重构时果断删除。

决策 5:删除 16 字段桥表,5 字段最小化

v4 时代做法:PlanSourceRelation 桥表 16 字段,含 tenantId、deletedAt、createBy、updateBy、remark 等冗余字段。

v3 重构做法:plan_source_relations 5 字段最小桥表(planId + planType + billRowTable + billRowId + contributedAmount,v3 数据模型定稿)。

决策理由:桥表的核心职责是「穿透」——把两个计划的体行挂回原始账单行。它不应该承担审计字段、租户隔离、软删除。把不需要的字段从「默认就有」变成「明确需要才加」。

3.2 简化类(5 项):用更简单的方案替换更复杂的方案

决策 6:删除自研 5 层沙箱,用 isolated-vm + child_process 双层

v4 时代做法:自研沙箱 5 层防护(语法解析层 + 变量白名单层 + 资源限制层 + 异常捕获层 + 审计日志层),约 1,800 行自研代码。

v3 重构做法:isolated-vm 处理 JS 沙箱隔离(V8 isolate + memoryLimit + timeout),child_process.spawn 处理进程级崩溃防护 + SIGCHLD 监听。约 350 行业务代码。

决策理由:「自研沙箱」听起来很可控,但 1,800 行自研代码意味着 1,800 行潜在漏洞。isolated-vm 是 V8 官方维护的隔离库;child_process 是 Node.js 标准库。把 5 层抽象替换成 2 个成熟组件,可靠性反而更高。

轻易云沙箱机制架构图:双层隔离(isolated-vm + child_process)× 4 类脚本(解析 / 对账 / 集成转换 / 分摊),展示主进程调度 → 沙箱进程 → 用户脚本的三层执行链路

这张图展示了 v3 的双层沙箱架构——主进程只负责任务调度和结果回收,沙箱进程通过 isolated-vm 隔离 V8 实例,user 脚本在隔离实例里运行。沙箱进程崩溃不影响主进程,BullMQ 会按指数退避策略自动重试。

决策 7:删除 Celery + advisory lock,用 BullMQ + 通用 JobTask

v4 时代做法:Celery 5 任务 + fan-in 聚合 + PostgreSQL advisory lock + Redis 去重。

v3 重构做法:BullMQ + 通用 JobTask 表(4 字段:type / refId / status / payload),所有异步任务统一走同一队列,按 JobTask.type 路由到对应 worker。

决策理由:Celery 的 fan-in 模式在 Python 单体服务下表现良好,但跨语言时 advisory lock + Redis 去重的组合维护成本极高。统一 JobTask 表的好处是任何异步任务的状态都能用同一个 SQL 查出来——「我的对账任务跑到哪了」只需要 SELECT * FROM job_tasks WHERE ref_id = ?。

决策 8:删除 8 态 IncomePlan + 7 态 ExpensePlan,简化到 7 态 + 5 态

v4 时代做法:IncomePlan 8 态(PENDING / READY / PARSING / RECONCILING / RECONCILED / CONFIRMED / FAILED / CANCELLED),ExpensePlan 7 态。

v3 重构做法:IncomePlan 7 态(PENDING / READY / RECONCILING / RECONCILED / CONFIRMED / FAILED / CANCELLED),ExpensePlan 5 态(PENDING / READY / CONFIRMED / FAILED / CANCELLED)。

决策理由:8 态里的 PARSING 和 RECONCILING 在实际流程里几乎从不独立存在——一个 IncomePlan 创建后 99% 的场景是直接进入 RECONCILING。多一个状态就多一个迁移规则、多一个错误处理分支。

轻易云状态机架构图:5 个状态机(IncomePlan 7 态 / ExpensePlan 5 态 / ReconcileStatus 4 态 / ParseStatus 4 态 / JobTask 4 态)的 2x3 网格展示,每态机有合法迁移边 + 终结态 + 重入边

这张图把 5 个状态机并排放在一起,每个状态机都标了合法迁移边和终结态。v3 把所有迁移规则集中在 StatusService.assertXxxTransition(),状态变更都强制走校验。

决策 9:删除 XxxService 抽象层,直接 Worker + utility 函数

v4 时代做法:PlatformService → AbstractPlatformService → JdpopPlatformService 三层继承,每层都有自己的「通用方法」。

v3 重构做法:直接 Worker + utility 函数。京东的导入逻辑直接写在 JdpopImportWorker 里,工具函数直接定义在 common 目录下。

决策理由:当一个平台的对账逻辑超过 80% 都是平台特定时,「通用方法」就是空的。抽象的成本(读懂抽象层、跨层调用、mock 边界)远高于「重复写一遍」的复用价值。

决策 10:删除 snake_case,统一 TS camelCase + Prisma snake_case

v4 时代做法:Python 时代的 snake_case 全栈(bill_id / business_order_no / parse_status),TypeScript 里看着像异类。

v3 重构做法:TS 代码统一 camelCase(billId / businessOrderNo / parseStatus),Prisma schema 统一 snake_case(bill_id / business_order_no / parse_status),由 Prisma Client 自动转换。

决策理由:在 TS + Prisma 的技术栈下,两边命名一致是无意义的额外工作。Prisma Client 自动转换让两边各用各的语言习惯,TS 开发者读 TS 代码,DB 开发者读 DB schema,互不干扰。

3.3 纪律类(6 项):用规则代替经验

纪律类决策不直接「删代码」,而是用强约束的规则替换靠人记的经验。这一类决策的产物是「约定规范」而不是「代码改动」。

决策 11:删除散乱 Decimal 精度,统一 (20, 4) / (20, 6) / (6, 4) / (10, 6)

v4 时代做法:金额字段精度散乱——Decimal(18, 4) / Decimal(20, 6) / Float / Real 在不同表里出现。

v3 重构做法:四档精度统一规范:

字段类型Prisma 精度示例
金额@db.Decimal(20, 4)income_amount
单价@db.Decimal(20, 6)unit_price
比率(0-1)@db.Decimal(6, 4)match_score
比率(0-100)@db.Decimal(10, 6)tax_rate

决策理由:财务对账系统里精度错误 = 金额错误。散乱的精度设置会让 0.0001 元的舍入误差反复累积,最终导致「系统算出来比手工多 0.02 元」的对账差异。把精度变成规范,任何新字段必须从 4 档里选,禁止自定义。

决策 12:删除纯枚举值 PlatformEnum,枚举 + meta 强绑定

v4 时代做法:PlatformEnum 只有 JD_POP / DOUYIN / ALIPAY / AMAZON / ALIPAY 等枚举值。

v3 重构做法:枚举值 + meta 对象(logo / description / link / color),前后端共享同一份 meta。

决策理由:当「平台」概念需要在 UI 上显示 logo、文档链接、颜色编码时,纯枚举值会让前端到处写 if (platform === 'JD_POP') showLogo()。枚举 + meta 把这些派生信息集中在一处,前端只需 <PlatformDisplay platform={row.platform} />。

决策 13:删除无标注 BillRow 字段,必标 Excel 来源

v4 时代做法:BillRow 字段没有标注来源,新增字段时不需要解释「这个值从 Excel 哪一列来」。

v3 重构做法:每个 BillRow 字段必须有 // 来源:xxx Excel 列名 注释,schema 里有专门的 @meta.source 字段记录。

决策理由:对账系统最大的隐性 bug 是「字段填错」——账单里有 30 列,开发者没看清把「订单金额」填到了「实付金额」的位置,每月的对账差异都是几万块。强制标注来源让 code review 更容易发现这类错误。

决策 14:删除旧 SupplyOrder 字段名,采用 v6 三码

v4 时代做法:SupplyOrder.platformOrderNo(unique 约束),一个平台订单号唯一指向一行。

v3 重构做法:v6 一维拍扁 后 SupplyOrder 三码(2026-07 M6 拍扁):

  • id(String(64) 行级唯一码,通用 ETL 用 buildSupplyOrderId(),金蝶 AR 当前用真实行级内码 FORDERENTRYID)
  • no(不再 unique,多行共享)
  • businessOrderNo(业务订单号,对账聚合键,与 BillRow 同名同语义)
  • businessOrderId(ERP 系统层业务订单 id,v6 新增)

决策理由:v4 的「一个平台订单号 = 一行」模型被分批发货、退货合并、订单拆分场景反复打脸。v6 三码区分了「行身份 / 单据编号 / 业务订单号 / ERP 系统 id」四个语义,每个字段都有清晰的职责。

决策 15:删除无历史 parse_scripts,加 history JSON 字段

v4 时代做法:parse_scripts 表没有版本演进记录,脚本迭代历史靠 Git 提交记录查询。

v3 重构做法:parse_scripts 表加 history JSON 字段,记录每次版本迭代(2026-05 v3 重构新增)。

决策理由:对账脚本的迭代历史是业务诊断的金矿——「为什么 7 月的京东对账突然多了 30 条 MARKETING_COUPON_DIFF 差异?」答案往往在 7 月 5 号某次脚本 v1.2.0 → v1.3.0 的迭代说明里。把历史沉淀到数据库表里,比查 Git 靠谱 100 倍。

决策 16:删除嵌套子模块,采用平台聚合范式(9 平台 × 4 文件 = 36 个同构文件)

v4 时代做法:每个平台单独建 NestJS module(JdpopModule / DouyinModule / AlipayModule / AmazonModule),模块间通过 DI 互调。

v3 重构做法:平台聚合范式——所有平台在同一个 BizReconciliationModule 下,按目录约定分布(jdpop/、douyin/、alipay/、amazon/、pdd/...)。每个平台目录 4 个同构文件:xxx-import.service.ts + xxx-import.worker.ts + xxx-rows.service.ts + xxx-rows.controller.ts。

决策理由:5 个平台的对账逻辑完全不同——京东是整单轧差、抖店是五轮匹配、亚马逊是分向不轧差、支付宝是三表对账、速卖通是 5 billType + JIT 快照。如果按平台分模块,跨平台的能力复用会变成 5 个 import service 各自实现一遍。把所有平台装在同一个 module 下,公用代码 / 同一份 DI 容器;平台特性写在各自目录里,互不干扰。

轻易云业务功能模块架构图:上半部灰色「平台基座」展示 Auth / Gateway / Queue / Settings / Prisma / Redis 等通用能力;下半部彩色「biz_reconciliation 业务层」展示账单导入 / 解析 / 收入对账 / 费用对账 / 集成中心 / AI Agent / 知识库 / 报表 / 供应链 / 核算项目 / 对账脚本 11 大业务域

这张图把 v3 的「平台基座 + biz_reconciliation」结构画得很清楚——平台基座是单租户的、单点能力的,业务层是平台聚合的、领域驱动的。9 个核心模块 × 16 个 worker × 4 个 AI Agent × 9 个集成 handler 全部装在 BizReconciliationModule 一个 NestJS 模块里,对外只暴露少量服务。

四、v3 重构的真实收益:不是「少写代码」,是「敢动代码」

16 项决策的收益不是空话,是轻易云 v3 重构完成后 4 个月生产环境跑出来的真实数据:

指标v4.0v3.0变化
后端 NestJS 模块数479 + 5 子模块−63%
业务表数2024+20%
单模块最大行数4,000+600−85%
跨模块直接 import 占比38%0%−100%
代码总量基准−42%净减少 42%
单元测试覆盖率31%85%+54pp
P95 响应时间12ms5ms3.2 倍
QPS(同硬件)8,50019,5002.3 倍
新增平台工作量2-3 周2 天−80%

几个反直觉的事实:

  • 业务表数变多(20 → 24),代码量反而减少——每张表都是「真实业务需要」而不是「为了抽象」。
  • 测试覆盖率从 31% 涨到 85%,不是写了更多测试,而是代码结构清晰、mock 边界收敛,写测试变得容易了。
  • P95 性能提升 3.2 倍是 Fastify 替换 Express + 业务表扁平化的复合结果——抽象层越多,性能损耗越大。
轻易云业务能力地图:11 大业务模块的全局视图(收入对账 / 费用对账 / 解析脚本 / 对账脚本 / 集成转换 / 公摊反写 / 集成中心 / AI Agent / 知识库 / 报表 / 供应链),展示 v3 重构后 9 个 NestJS 核心模块的能力分布

这张是 v3 重构后业务能力的全景视图——11 大业务模块全部装在 9 个 NestJS 核心模块里,每个模块的 exports 列表不超过 10 项。

最有价值的数据是最后一行:新增平台工作量从 2-3 周降到 2 天。业务的扩展性被还原到「接近常数」的复杂度——加第 1 个平台和新加第 5 个平台的工作量一样大。这种「可预测的扩展性」才是 v3 重构后最值钱的工程价值。

五、删抽象不是目的,「敢动代码」才是

如果用一句话总结 v3 重构的工程哲学,应该是:「删抽象不是目的,让团队敢动代码才是」。

v4.0 时代,团队里有一句口头禅:「谁敢动基类,谁负责兜底」。结果基类代码 2 年没人敢改,所有 bugfix 都通过「新增子类覆盖基类方法」实现。v3 重构后,没有基类了——改一个 bug 就是直接改对应的 4 个文件,新人入职 2 周上手加新平台。

这套工程哲学的执行准则是 v3 重构的第一原则:

代码能直白完成,就绝对不要抽象。 不提前造「通用」轮子;三处重复再考虑提取。不封装没有意义的 wrapper;优先用原始 API + 明确命名。不引入重型库解决轻量问题;优先用已有依赖。

「简洁优先,拒绝过度设计」这八个字看起来像口号,落地到代码就是上面 16 项决策。每一项决策都在和「看起来合理但实际无用」的抽象做斗争。

六、给 CTO 的 5 个反向检查问题

如果你正在评估自家系统是否过度设计,这里有 5 个反向检查问题可以对照:

  1. 系统里有几个「抽象基类」是从未被 3 个以上子类真正复用的? 如果答案是 0,这个基类就是负债。
  2. 数据库里所有表都带 deletedAt 字段吗? 如果是,请认真评估「软删除」的 5 个隐藏成本。
  3. 异步任务是否每个都有独立的 queue、独立的 worker? 如果是,考虑统一 JobTask 表的收益。
  4. 业务表里是否有 tenantId 字段但实际只服务一个租户? 如果是,单租户化会让代码立刻清爽 30%。
  5. 「平台对接代码」里是否有 AbstractXxxService 继承链? 如果有,问自己:「抽象出来的方法有几个被所有子类用到了?」

这 5 个问题的答案大概率会让你不舒服——但不舒服是对的。好的架构不是「看起来很完整」,而是「删掉一半还能用」。

v3 重构就是这样——把 v4.0 一半的「合理抽象」删掉,系统反而更强壮、更易扩展、更易测试。这就是「去过度设计」的全部秘密:敢于砍掉看似合理的复杂度,把节省下来的精力放在真正能解决用户问题的能力上。

如果你也在做企业级 SaaS,面临「要不要加 tenantId」「要不要抽基类」这类问题时,记住 16 项决策背后的母题——「简洁优先,拒绝过度设计」。轻易云愿意把这套经过 4 个月验证的工程纪律开放给同行。

本文为原创内容,转载请注明出处:https://www.qeasy.cloud/insights/reconciliation/1-3-v3-refactor-16-key-decisions-no-overengineering

评论