从 v4 到 v3:NestJS 模块化设计如何撑住 9 个核心模块
从 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 的真实代码):
@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。
上面这张就是 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):
@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 互相调用的代码:
// v4 时代的反模式:跨模块直接 import
// ❌ reconciliation.service.ts
import { AuthService } from "../auth/auth.service"; // 直接 import 类型
export class ReconciliationService {
async runReconcile() {
const user = await AuthService.getCurrentUser(); // 直接调用
}
}
// 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):
@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 个结构一致但实现独立的文件,没有继承、没有抽象,平台特性直接写在对应文件里。
上面这张图就是 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 的启动代码:
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.x | Fastify 4.x | 提升 |
|---|---|---|---|
| P50 响应时间 | 8ms | 4ms | 50% ↓ |
| P95 响应时间 | 12ms | 5ms | 58% ↓ |
| QPS(同硬件) | 8500 | 19500 | 2.3 倍 |
| 首字节时间(TTFB) | 35ms | 14ms | 60% ↓ |
| 启动时间(API listen) | 2.1s | 0.8s | 62% ↓ |
但性能数字只是结果,真正的工程纪律收益有 3 个:
- Schema 验证内嵌——Fastify 原生支持 JSON Schema 做入参校验,比 Express 的中间件链快 3-5 倍,更重要的是校验逻辑写在路由装饰器里,不在 controller 函数体里,业务代码更干净。
- 更小的依赖树——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 后立刻执行)。 - 更严格的错误处理——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 逻辑塞进业务模块内部——后者会让模块边界瞬间模糊。
@Module({
imports: [
PrismaModule,
AuthModule,
BizReconciliationModule, // ← 对账域核心
DataAnalysisModule, // ← 报表域
],
// ... agents 自己的 providers
})
export class AgentsModule {}
AgentsModule 的典型场景是「AI 智能体调用对账脚本」:用户通过对话要求"帮我对对 2026-07 的京东 POP 账期",Agent 需要:
- 调
ReconcileScriptsService(来自BizReconciliationModule)拿到对账脚本 - 调
IncomePlansService(来自BizReconciliationModule)创建对账计划 - 调
JobTasksService(来自BizReconciliationModule)提交异步任务 - 调
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)就展示了这种「模块是能力单元」的设计:
六、单租户 + 平台聚合范式:模块化的设计哲学
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 以下——这就是模块化设计最终交付的工程价值。