# ADR-0003: 后端四层模块化架构 > **状态**: 已决议 > **日期**: 2026-05-21 > **父决策**: ADR-0001(整体技术栈) > **决策者**: 架构评审 --- ## 上下文 项目初期路由层直接操作 Prisma,业务逻辑与 HTTP 适配耦合。随着模块增长(8 个领域模块,29 个 API 端点),缺乏分层导致: - 业务逻辑无法脱离 HTTP 环境做单元测试 - Prisma 调用散落在 8 个路由文件中,查询逻辑难以复用 - 横切关注点(日志、审计、错误处理)没有统一注入点 - 单文件超长(formulas 路由 295 行)难以维护 需对后端架构做分层设计,支持企业级扩展。 --- ## 决策 **选择四层模块化架构**:Route → Service → Repository → Prisma,按领域模块聚合文件。 ### 目录结构 ``` src/ ├── modules/ │ ├── formulas/ │ │ ├── formulas.route.ts # Fastify 路由注册 + 参数提取 │ │ ├── formulas.service.ts # 纯业务逻辑 + 审计埋点 │ │ ├── formulas.repository.ts # Prisma 数据访问 │ │ ├── formulas.schema.ts # Zod 验证 schema │ │ └── formulas.test.ts # 集成测试 │ ├── ingredients/ # 同上 │ ├── projects/ # 同上 │ ├── color/ # 同上 │ ├── ai/ # 同上 │ ├── auth/ # 同上 │ ├── config/ # 同上 │ └── health/ # 同上 └── shared/ # 跨模块共享 ├── errors/ # AppError 异常体系 ├── logging/ # AsyncLocalStorage 上下文 ├── middleware/ # RBAC / Ownership 中间件 ├── metrics/ # Prometheus 指标 └── audit/ # 审计服务 ``` ### 对比方案 | 方案 | 优势 | 劣势 | 结论 | |------|------|------|------| | **四层模块化(选)** | 按领域聚合,改动一个功能不需跨目录跳转;Service/Repository 可独立单元测试;横切关注点通过 shared/ 统一注入 | 小模块(如 health)文件数多 | ✅ | | 三层(Route→Service→Prisma) | 简单直接 | Service 与持久化耦合,不含 Repository 则 Prisma mock 困难 | ❌ | | 按层分目录(routes/ / services/ / repositories/) | 层边界清晰 | 同功能文件分散在 4 个目录,开发时频繁切换 | ❌ | | Clean/六边形 | 核心领域零框架依赖 | 过度工程化;当前仅 Web 端,无需端口-适配器抽象 | ❌ | --- ## 层职责 | 层 | 职责 | 依赖 | 测试方式 | |----|------|------|----------| | **Route** | Fastify 注册、参数提取(req→纯数据)、preHandler 挂载 | Controller/Service + Zod | `app.inject()` 集成测试 | | **Service** | 纯业务逻辑、审计埋点、百分比验证 | Repository + AuditService | 单元测试(mock repository) | | **Repository** | Prisma 查询封装、事务管理 | Prisma | Testcontainers 集成测试 | | **Schema** | Zod 验证定义、TypeScript 类型导出 | Zod | 不需要测试(声明式) | --- ## 横切关注点 | 关注点 | 实现方式 | 注入点 | |--------|----------|--------| | **认证** | JWT preHandler(app.ts 全局) | onRequest | | **授权** | `requireRole()` / `requireFormulaOwnership()` | Route preHandler | | **错误处理** | AppError 子类(ValidationError/NotFoundError 等) | 全局 `setErrorHandler` | | **日志** | pino 结构化日志 + AsyncLocalStorage context | `app.log.child()` | | **审计** | AuditService(pino 输出,action/resource/userId) | Service 层显式调用 | | **指标** | prom-client(http_requests_total, app_errors_total, ai_requests_total) | 全局 handler / AI Service | | **输入校验** | `validateOrReply()` — Zod 解析,失败时 400 | Route 层 | | **API 文档** | `@fastify/swagger` + `zod-to-json-schema` | Route schema 定义 | --- ## 后果 - 新增模块需创建 4 个文件(route/service/repository/schema),模板明确 - Service 和 Repository 可脱离 HTTP 环境做纯函数测试 - 跨模块共享逻辑必须放在 `shared/` 下,不能在模块间直接 import - 所有错误必须使用 AppError 子类,不可裸抛 `new Error()` - 模块测试文件与源文件同目录,vitest `include: ['src/**/*.test.ts']` 自动发现