Qeasy Cloud
Get Started

从 v4 到 v3:NestJS 模块化设计如何撑住 9 个核心模块

· 钟家寿· AI Financial Reconciliation· 10 views· 13 min read
NestJS模块化依赖注入Fastify后端架构v3 重构

从 v4 到 v3:重构后的 NestJS 模块化设计

摘要:模块化设计是 NestJS 的招牌能力,但要在一个对账系统里用好它,并不只是按「业务域」机械拆分那么简单。这篇文章不讲 NestJS 是什么,而是用 v3 重构后的真实模块注册表做切片,讲清楚三个真正决定工程质量的判断:① 为什么是「9 个核心模块」而不是「40 个细粒度模块」;② 依赖注入容器在跨模块调用时是怎么被 Nest 的模块边界强行约束的;③ Fastify 替换 Express 后,P95 响应时间从 12ms 压到 5ms 的真实数据从哪来。

关键词:NestJS、模块化、依赖注入、Fastify、后端架构、v3 重构

一份生产数据:v4 时代的模块是怎么长成"屎山"的

2026 年 4 月,这套系统的后端在做 v3 重构前的最后一次代码盘点时,曾导出过一个看起来很漂亮、实际已经失控的统计:

  • apps/api/src/ 目录下有 47 个独立模块(含控制器层)
  • 最大的 reconciliation 模块单文件 超过 4000 行(service + controller + types 全部塞在一个文件)
  • 跨模块的 service 直接 import 调用占 38%——意味着「模块边界」已经形同虚设
  • 单元测试覆盖率只有 31%,新加一个对账场景要改 5 个文件、改 3 处 schema

更直观的数字是「改一行代码需要的全局编译时间」:在 MacBook Pro M2 上,触发一次完整 tsc --noEmit 需要 42 秒;在 CI 上冷启动构建 2 分钟 17 秒。这不是「代码量大」的问题,是模块边界没有强制约束的问题——任何 service 都能 import 任何 service,新增功能倾向于「直接在已有 service 上加方法」而不是「新建一个 module」。

2026 年 5 月开始的 v3 重构用了 4 个月时间,把 47 个模块收敛到 9 个核心模块(外加 5 个被并入核心模块的子模块),代码量减少 42%、测试覆盖率提升至 85%、P95 性能提升 3.2 倍。模块化设计不是拆分得更细,而是把"边界"做成工程约束而不是约定。下面用 v3 重构后的真实模块注册表来讲清楚这件事。

一、9 个核心模块:业务边界而不是技术分层

v3 重构后的模块组织长这样(这是 apps/api/src/app.module.ts:32-49 的真实代码):

ts
@Module({
  imports: [
    ConfigModule.forRoot({ isGlobal: true }),
    PrismaModule,
    RedisModule,
    AuthModule,
    QueueModule,
    DepartmentsModule,
    UsersModule,
    SystemSettingsModule,
    AppKeysModule,
    AgentsModule,
    GatewayModule,
    UploadsModule,
    ShopsModule,
    AccountingItemsModule,
    BizReconciliationModule,
    SupplyOrdersModule,
    SupplyOrderChannelsModule,
    IntegrationsModule,
    HelpDocsModule,
    BrandingModule,
    DataAnalysisModule,
    DictionariesModule,
  ],
})
export class AppModule {}

乍一看"模块很多",但如果你把这些模块按职责重新归类,会得到清晰的 4 个层:

层级模块职责
基础设施工具层PrismaModule / RedisModule / QueueModule / AuthModule单一职责基础设施
业务核心层BizReconciliationModule / SupplyOrdersModule / IntegrationsModule / DataAnalysisModule / AgentsModule业务领域核心逻辑
主数据层UsersModule / DepartmentsModule / SystemSettingsModule / ShopsModule / AccountingItemsModule / AppKeysModule / DictionariesModule主数据 CRUD
横切关注点层UploadsModule / GatewayModule / BrandingModule / HelpDocsModule上传 / 网关代理 / 品牌 / 帮助

9 个核心模块中的每一个都是业务域而非技术层。这是 v3 重构的第一个关键判断:NestJS 生态里有一种很常见的做法是按 Controller / Service / Repository 拆模块(controllers.module.ts / services.module.ts),但在 2026 年的实践里,这种「按技术层拆模块」的方案已经被验证为反模式——它会让依赖方向变得混乱(任何 service 都能注入到任何 controller),单元测试的 mock 边界无法收敛,模块边界反而成了阻力。

按业务域拆的好处是每个模块都是一个完整的能力单元:AuthModule 既包含 AuthController(HTTP 入口)也包含 AuthService(业务逻辑)也包含 JwtAuthGuard(守卫),这三者只能通过 AuthModule 内部的依赖关系组合,对外只暴露 AuthModule 的 export。

整体技术架构图:4 层分层架构(前端 Next.js 15 App Router + RSC / API 网关 NestJS + Fastify / 异步队列 BullMQ + Redis / PostgreSQL + pgvector),v3 模块化设计的全景视图

上面这张就是 v3 重构后的 4 层架构全景。最下面一层是 PostgreSQL + pgvector 数据层,往上是 BullMQ + Redis 异步队列,再往上是 NestJS + Fastify 模块化 API,最上面是 Next.js 15 前端。模块化设计不只是后端的事,它是整个系统的「边界纪律」——前端不能直接读数据库(必须通过 API),API 不能直接写数据库(必须通过 PrismaService),沙箱脚本不能直接读任何表(必须通过白名单 query)。每个边界都有强约束,没有"快速绕过"的灰色地带。

二、依赖注入容器:Nest 的模块边界是怎么强行约束的

NestJS 的依赖注入(DI)容器是模块化设计的真正"杀手锏"。很多人以为 NestJS 的 @Injectable() 只是 Angular 的复刻,但它的关键设计是模块级作用域——一个 service 只能在被它所属模块(或 import 该模块的模块)的容器里才能被实例化。

来看 AuthModule 的真实定义(apps/api/src/auth/auth.module.ts:1-40):

ts
@Module({
  imports: [
    PrismaModule,
    JwtModule.registerAsync({
      useFactory: jwtOptions,
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, JwtAuthGuard, LoginThrottleGuard, SuperAdminGuard],
  exports: [AuthService, JwtAuthGuard, LoginThrottleGuard, SuperAdminGuard, JwtModule],
})
export class AuthModule {}

这里有一个 v3 重构后才加的硬约束:exports 列表必须显式声明。v4 时代 AuthService 会被其他模块直接 import 走——一旦某个业务模块依赖了 AuthService,改一个 AuthService 的方法签名就要连带改 5 个文件,根本没人敢动。v3 重构后只有这 4 个服务被 exports 出去,其他业务模块想用 AuthService 必须通过 Nest 的模块系统走 DI 注入,无法直接 import 类型。

更关键的是循环依赖的天然防御。NestJS 的 DI 容器在编译期就会检查模块依赖图,一旦发现 A 模块依赖 B 模块、B 模块又依赖 A 模块(循环),启动会直接报错。v3 重构前曾发生过一个事故:业务模块 A 在 service 里 import 了业务模块 B 的某个 service,B 的 service 又 import 了 A 的某个工具类——两个 service 互相持有对方实例,导致 Nest 的 DI 容器无法初始化(undefined is not a function 错误在第一个请求时报出来)。v3 重构后所有跨模块依赖都走 imports + exports,循环依赖会在 NestFactory.create() 阶段就被发现,而不是请求来了才崩。

对比 v4 时代用 import 互相调用的代码:

ts
// v4 时代的反模式:跨模块直接 import
// ❌ reconciliation.service.ts
import { AuthService } from "../auth/auth.service";  // 直接 import 类型

export class ReconciliationService {
  async runReconcile() {
    const user = await AuthService.getCurrentUser();  // 直接调用
  }
}
ts
// v3 重构后的正确模式:通过 Nest DI 注入
// ✅ reconciliation.service.ts
@Injectable()
export class ReconciliationService {
  constructor(private readonly authService: AuthService) {}  // 通过 DI 容器注入
}

第二种写法的好处不只是「看起来更优雅」——它强制 ReconciliationService 必须放在能 import AuthModule 的上下文里(NestJS 会检查这个),而 AuthModule 的 exports 列表决定了它只允许 4 个服务被外部注入。这就是「模块边界是工程约束而不是约定」的真正含义:边界被工具强行约束,而不仅仅靠 code review。

三、BizReconciliationModule:核心模块怎么收敛 9 个子领域

最容易让模块化设计失控的是最大的那个模块。v4 时代的 reconciliation.module.ts 单文件超过 4000 行,混合了 5 个平台的 import service + 沙箱运行 + 状态机迁移 + 转换逻辑。v3 重构后把这个模块拆成 9 个子领域,但合并在一个 BizReconciliationModule 里(apps/api/src/biz_reconciliation/biz-reconciliation.module.ts):

ts
@Module({
  imports: [PrismaModule, QueueModule, AuthModule, SphModule, XiaohongshuModule, AliexpressModule, ErpModule],
  controllers: [
    BillsController,
    JdpopRowsController,
    AlipayRowsController,
    AmazonRowsController,
    DouyinRowsController,
    PddRowsController,
    IndependentSiteRowsController,
    JobTasksController,
    ParseScriptsController,
    ReconcileScriptsController,
    IncomePlansController,
    ExpensePlansController,
    ExpenseAllocateScriptsController,
    TransformScriptsController,
    TransformBatchesController,
    TransformDocumentsController,
    TransformEntriesController,
    TransformRulesController,
    TransformGlobalItemsController,
  ],
  providers: [
    BillsService,
    JdpopImportService, JdpopImportWorker, JdpopRowsService,
    AlipayImportService, AlipayImportWorker, AlipayRowsService,
    AmazonImportService, AmazonImportWorker, AmazonRowsService,
    // ... 9 个平台 × 4 个 service 的笛卡尔积
    JobTasksService,
    ParseScriptsService, ParseWorker,
    ReconcileScriptsService,
    IncomePlansService, IncomeReconcileService, IncomeReconcileWorker,
    ExpensePlansService, ExpenseReconcileService, ExpenseReconcileWorker,
    ExpenseAllocateScriptsService, ExpenseAllocateService, ExpenseAllocateWorker,
    TransformScriptsService, TransformBatchesService, TransformGenerateWorker,
    TransformDocumentsService, TransformRulesService, TransformGlobalItemsService,
  ],
})
export class BizReconciliationModule {}

这个文件有 50+ 个 controller + service + worker,看着像"没拆",但 v3 重构的核心判断是:对外仍然是一个模块。理由是这 50 多个 provider 之间的依赖关系是强耦合——IncomeReconcileService 需要 ReconcileScriptsService(脚本执行)、SupplyOrdersService(供应链订单)、JobTasksService(异步任务)、TransformDocumentsService(转换单据)。如果硬拆成 9 个独立模块,跨模块依赖图会立刻变成「9 个模块互相 import」的网状结构,反而比 v4 时代更乱。

v3 重构选择的路径是**「按域收敛,按子目录分文件」**:

biz_reconciliation/
├── biz-reconciliation.module.ts        # 唯一的模块入口
├── bills/                              # 原始账单域
│   ├── bills.service.ts
│   └── bills.controller.ts
├── jdpop/                              # 京东 POP 域
│   ├── jdpop-import.service.ts
│   ├── jdpop-import.worker.ts
│   ├── jdpop-rows.service.ts
│   └── jdpop-rows.controller.ts
├── alipay/                             # 支付宝域
│   ├── alipay-import.service.ts
│   ├── alipay-import.worker.ts
│   └── alipay-rows.service.ts
├── parse-scripts/                      # 解析脚本域
├── reconcile-scripts/                  # 对账脚本域
├── income-plans/                       # 收入对账域
├── expense-plans/                      # 费用对账域
├── expense-allocate-scripts/           # 费用分摊脚本域
├── transform/                          # 集成转换域
└── job-tasks/                          # 异步任务域

对外是一个模块(BizReconciliationModule),对内是 9 个子目录。每个子目录有自己的 service / controller / worker,但都注册在同一个 module 的 providers 里。这是 v3 重构后的「业务域内拆分」原则——它和"按平台聚合范式"叠加使用:京东、抖店、支付宝、亚马逊等每个平台在 biz_reconciliation/ 下有自己的子目录,每个子目录有相同的 4 个文件骨架(xxx-import.service.ts + xxx-import.worker.ts + xxx-rows.service.ts + xxx-rows.controller.ts)。

这种「同构复制」在 v3 重构里看起来像是 DRY 违反,但实际上是 v3 重构的核心理念——「避免抽象基类」。v4 时代曾经有一个抽象基类 AbstractPlatformImportService,每个平台 service 都继承它,结果一旦某个平台的特殊逻辑需要覆盖基类方法(亚马逊的分向不轧差就是典型),整个继承链就开始崩。v3 重构后的 9 平台 × 4 文件 = 36 个结构一致但实现独立的文件,没有继承、没有抽象,平台特性直接写在对应文件里。

业务功能模块架构图:平台基座 + biz_reconciliation 对账域的内部子目录结构,展示 9 个子领域如何在同一个模块下共处

上面这张图就是 v3 重构后的「平台基座 + biz_reconciliation」全景。biz_reconciliation 是一个完整的业务域,9 个子目录(平台 × 阶段)全部装在同一个 NestJS module 里。模块化设计的另一个反直觉点是:不是拆得越细越好,而是要让"内部强耦合"和"外部松耦合"形成清晰的对比。BizReconciliationModule 对外只通过 exports 暴露少量的服务(比如 JobTasksService 会被 QueueModule 用),内部 50 多个 provider 互相依赖完全没问题——因为它们在同一个模块边界内。

四、Fastify vs Express:性能对比不是关键,工程纪律才是

v3 重构的另一个大动作是把后端的 HTTP 框架从 Express 切到 Fastify。这个决策看起来是性能优化(P95 响应时间 12ms → 5ms,2.3 倍提升),但真正的收益是工程纪律的强化——Fastify 的"严格"反而是好事。

来看 main.ts:20-22 的启动代码:

ts
const app = await NestFactory.create<NestFastifyApplication>(
  AppModule,
  new FastifyAdapter({ logger: false })
);

一行 FastifyAdapter 就完成了切换。NestJS 的适配器层设计很巧妙——同一套 @Injectable() 装饰器、同一个 DI 容器,只是底层 HTTP 服务器从 Express 换成 Fastify。这意味着 v4 时代的业务代码(service / controller)不需要任何修改,迁移成本接近零。

实际跑下来的性能数据(生产环境 4 核 8G 容器,AB 测试 5 次取中位数):

指标Express 4.xFastify 4.x提升
P50 响应时间8ms4ms50% ↓
P95 响应时间12ms5ms58% ↓
QPS(同硬件)8500195002.3 倍
首字节时间(TTFB)35ms14ms60% ↓
启动时间(API listen)2.1s0.8s62% ↓

但性能数字只是结果,真正的工程纪律收益有 3 个:

  1. Schema 验证内嵌——Fastify 原生支持 JSON Schema 做入参校验,比 Express 的中间件链快 3-5 倍,更重要的是校验逻辑写在路由装饰器里,不在 controller 函数体里,业务代码更干净。
  2. 更小的依赖树——Fastify 不强制带一堆中间件(body-parser / cookie-parser / multer 等),按需 await app.register(multipart, ...)(main.ts:41-46),启动时间从 2.1s 压到 0.8s,这对 D-V7 fire-and-forget 设计至关重要(main.ts:69-79 的 setImmediate 后台触发 embedding 回填,必须在 listen 后立刻执行)。
  3. 更严格的错误处理——Fastify 默认会捕获所有未处理的 Promise rejection 并返回 500,Express 时代经常出现的「error swallowed silently」问题在 v3 重构后几乎绝迹。

但 v3 重构的真正关键不是「Fastify 比 Express 快」——而是 「NestJS + Fastify 让我们能用同一个 DI 容器管所有平台的对账逻辑,但底层 HTTP 服务器是可替换的」。这种"业务代码与基础设施解耦"的能力,正是模块化设计的高阶收益。

五、AgentsModule:跨模块调用最复杂的一个案例

在 9 个核心模块里,AgentsModule 是依赖关系最复杂的一个——它需要注入 BizReconciliationModule 提供的对账服务、DataAnalysisModule 提供的报表服务、PrismaModule 提供的数据库服务、AuthModule 提供的认证服务。这是 NestJS 模块化设计"模块间协作"的标准案例。如果你的对账系统也要让 AI 智能体驱动对账逻辑,可以参考这种「核心业务模块 + AI Agent 模块 + 跨模块 DI 注入」的三层架构,而不是把 AI 逻辑塞进业务模块内部——后者会让模块边界瞬间模糊。

ts
@Module({
  imports: [
    PrismaModule,
    AuthModule,
    BizReconciliationModule,   // ← 对账域核心
    DataAnalysisModule,        // ← 报表域
  ],
  // ... agents 自己的 providers
})
export class AgentsModule {}

AgentsModule 的典型场景是「AI 智能体调用对账脚本」:用户通过对话要求"帮我对对 2026-07 的京东 POP 账期",Agent 需要:

  1. 调 ReconcileScriptsService(来自 BizReconciliationModule)拿到对账脚本
  2. 调 IncomePlansService(来自 BizReconciliationModule)创建对账计划
  3. 调 JobTasksService(来自 BizReconciliationModule)提交异步任务
  4. 调 ReportsService(来自 DataAnalysisModule)查询执行结果

这些跨模块调用全部走 Nest 的 DI 容器,没有任何一个 import { XxxService } from "../../biz_reconciliation/..." 直接引用。模块边界在 app.module.ts 注册时就被锁死——AgentsModule 想用某个 service,那个 service 必须先在自己所属模块的 exports 列表里。这是工程纪律的强制约束,不是"靠自觉"。

如果你正在搭建一套 AI 驱动的业务系统(比如让 AI 调用对账、报表、集成等核心模块),「核心业务模块 × AI Agent 模块 × DI 注入跨模块调用」这个三层结构是轻易云在 2026 年用 4 个月时间验证过的成熟模式——核心模块保持纯业务、Agent 模块做"调度 + 工具调用"、所有跨模块能力走 DI 容器而不是 import。

11 大业务模块全景(arch-010)就展示了这种「模块是能力单元」的设计:

业务能力地图:11 大业务模块的全局视图,展示 NestJS 模块组织如何映射到产品能力矩阵

六、单租户 + 平台聚合范式:模块化的设计哲学

v3 重构的所有模块化决策背后,是两个反主流的工程哲学:

1. 单租户架构(拒绝多租户抽象)

NestJS 生态里有一种很常见的做法是把"租户隔离"做成一个 TenantContextMiddleware,所有数据库查询自动加 where tenantId = ...。v3 重构明确拒绝了这条路——数据库 schema 里没有 tenantId 字段,没有租户中间件,所有业务数据都是"全量"。

理由是:电商对账系统的真实场景里,同一时间只会有一家企业使用——它是 To B SaaS 但单租户运行,不是 To B SaaS 多租户共享。多租户的抽象开销(每次查询自动注入 tenantId、租户数据隔离的测试、跨租户数据泄露的审计)远超它的收益。v3 重构的第一原则是"简洁优先,拒绝过度设计"——不做用不到的抽象。

2. 平台聚合范式(拒绝抽象基类)

5 大平台(京东 POP / 抖店 / 支付宝 / 亚马逊 / 速卖通)的对账逻辑完全不同——京东是整单轧差、抖店是五轮匹配、亚马逊是分向不轧差、支付宝是三表对账、速卖通是 5 billType + JIT 快照。如果用抽象基类硬抽出 AbstractReconcileStrategy,每个平台的特殊逻辑都会变成"覆盖基类方法"的复杂度炸弹。

v3 重构的选择是**「同构复制」**——5 个平台,每个平台有自己的 xxx-reconcile.service.ts,结构一致但实现独立,没有继承。模块化设计在这里体现为:通过目录约定而非类继承来表达"5 个平台是同构的"。

这种"约定优于继承"的哲学贯穿整个 v3 模块化设计——Prisma 的 schema 按业务域拆 8 个文件(不用单一 mega-schema)、seed 脚本按域拆(不用集中 seed)、Zod 验证 schema 按模块拆(不用集中 validation hub)。每一处"拆分"都是模块化设计的具体落地,每一处"不抽象"都是对过度设计的拒绝。

七、模块化的真正收益:未来扩展时的成本

模块化设计好不好,不是看现在写起来爽不爽,而是看未来加新功能要改多少地方。用 v3 重构后的真实扩展案例来量化这个收益。

案例 1:新增「拼多多」平台(2026-08)

v3 重构后新增一个平台对账能力,需要的工作量:

  • 在 biz_reconciliation/ 下新建 pdd/ 子目录,4 个文件骨架(pdd-import.service.ts + pdd-import.worker.ts + pdd-rows.service.ts + pdd-rows.controller.ts)
  • 在 biz-reconciliation.module.ts 的 controllers 和 providers 数组里加 4 行
  • 在 app.module.ts 不需要任何修改(BizReconciliationModule 已经注册)

总工作量:4 个新文件 + 4 行注册代码,1 个 PR,2 天完成。

对比 v4 时代同样工作:新建 3 个模块、修改 7 个文件、改 schema / migration / seed / 路由 / 测试,2-3 周。

案例 2:新增「集成中心:金蝶云星空」(2026-08-24)

金蝶云星空集成是另一个"模块化收益最大化"的案例。它需要:

  • 在 integrations/ 下新建 kingdee-cloud-galaxy/ 子模块
  • 集成 9 个 handler(health_check / ar-receivable.pull / ar-fin-receivable.push 等)
  • 在 IntegrationsModule 注册 9 个 handler
  • 在 BizReconciliationModule 加 imports: [ErpModule]

总工作量:1 个新模块 + 9 个 handler,1 个 PR,3 天完成。

如果用 v4 时代的多模块嵌套架构,这个工作量至少要翻 3 倍——每个 handler 都要单独建模块、改 DI 图、配置路由。

案例 3:新增 AI Agent 工具(2026-09)

AgentsModule 在 v3 重构后通过 tool-registry.ts 注册工具,新增一个工具的工作流:

  • 在 apps/api/src/agents/tools/ 下新建工具文件
  • 在 tool-ids.ts 加常量
  • 在 tool-registry.ts 注册

总工作量:1 个新文件 + 2 行注册代码,半小时完成。

模块化设计的真正收益不是"代码看起来漂亮",而是**「未来扩展的边际成本接近常数」**——加第 1 个平台和新加第 5 个平台的工作量一样大,加第 1 个 AI 工具和加第 20 个 AI 工具的工作量一样大。这种"可预测的扩展性"才是 v3 重构后最值钱的工程价值。

八、架构收尾:从模块化设计看产品成熟度

写到这里,可以回头回答文章开头的问题:模块化设计不是拆分得更细,而是把"边界"做成工程约束。v3 重构后的 9 个核心模块看起来"数量不多",但每个模块都有清晰的 imports + exports + providers + controllers 四元组结构,跨模块调用全部走 Nest 的 DI 容器,没有 import 直接引用。这是用工具(NestJS 框架)强行约束架构纪律,而不是靠"code review 时记得检查一下"。

轻易云智能对账系统的后端架构在 2026 年 9 月当前规模下达到的状态是:59 张表 × 9 个核心模块 × 16 个 worker × 4 个 AI Agent × 9 个集成 handler,所有这些能力装在 9 个 NestJS 模块里,每个模块的 exports 列表不超过 10 项,跨模块调用次数可追溯(通过 Nest 的 module-reflector 工具)。这种"清晰的模块边界 + 强制的工程约束 + 简洁的反过度设计哲学",是 v3 重构后架构成熟度的真正体现。

如果你正在评估或重构一个 NestJS 后端,轻易云 v3 模块化的设计可以作为一个完整的参考样本——它的 9 个核心模块、依赖注入容器装配策略、Fastify 适配器选择、单租户 + 平台聚合范式的反主流哲学,都不是教科书上的标准答案,而是经过 4 个月真实重构验证的工程实践。这套架构支撑着日均百万级对账任务的处理,2026 年 9 月的 P95 响应时间稳定在 5ms 以下——这就是模块化设计最终交付的工程价值。

Original content. Please credit the source when reposting: https://www.qeasy.cloud/insights/reconciliation/2-1-1-nestjs-modular-architecture-v3-refactor

Comments