架构 - 后端从 flat routes/ 重构为 modules/<domain>/ 模块化结构(8个模块) - 四层架构:Route -> Service -> Repository -> Prisma - 新增 shared/ 基础设施(AppError 异常体系、ALS 上下文、prom-client 指标) - 前端 Toast/Skeleton/Alert 组件基建 + formulaService 模板 安全 - JWT 签名算法修复(HS256 用 createHmac 而非 createHash) - 密码哈希 async scrypt + timingSafeEqual - API Key 从 localStorage 迁移至服务端 runtime/config.json - Helmet 安全头 + rate-limit 全局限流 100 req/min - 全局 auth preHandler + RBAC + Ownership 中间件 颜色引擎 - 色匹配切换为 cube 粗筛 + CIEDE2000 精排 - PantoneColor 表 + 种子数据 + 搜索端点 - AI 配色 Prompt 注入成分库 colorant 列表 配方推演 - 本地优化引擎(同 category 替换 + 成本排序) - baseFormulaId 支持 + Pareto 散点图 文档 - ADR-0003 四层架构、ADR-0004 RBAC 授权模型 - 更新 ADR-0001/0002 - api-reference.md(29端点)、project-overview.md 部署 - Dockerfile * 2 + nginx.conf + docker-compose.prod.yml - 健康探针 + 优雅关闭 + pg_dump 备份脚本 - ESLint + Prettier + tsconfig strict
4.4 KiB
4.4 KiB
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']自动发现