1039 lines
34 KiB
Markdown
1039 lines
34 KiB
Markdown
---
|
||
|
||
name: fullstack-dev
|
||
description: |
|
||
全栈后端架构与前后端集成指南。
|
||
触发条件:构建全栈应用、创建带前端的 REST API、搭建后端服务、
|
||
构建 todo 应用、构建 CRUD 应用、构建实时应用、构建聊天应用、
|
||
Express + React、Next.js API、Node.js 后端、Python 后端、Go 后端、
|
||
设计服务层、实现错误处理、管理配置/认证、
|
||
设置 API 客户端、实现认证流程、处理文件上传、
|
||
添加实时功能(SSE/WebSocket)、为生产环境加固。
|
||
不触发条件:纯前端 UI 工作、纯 CSS/样式、仅数据库模式。
|
||
license: MIT
|
||
metadata:
|
||
category: full-stack
|
||
version: "1.0.0"
|
||
sources:
|
||
- The Twelve-Factor App (12factor.net)
|
||
- Clean Architecture (Robert C. Martin)
|
||
- Domain-Driven Design (Eric Evans)
|
||
- Patterns of Enterprise Application Architecture (Martin Fowler)
|
||
- Martin Fowler (Testing Pyramid, Contract Tests)
|
||
- Google SRE Handbook (Release Engineering)
|
||
- ThoughtWorks Technology Radar
|
||
---
|
||
|
||
# 全栈开发实践
|
||
|
||
## 强制工作流程——请按顺序执行以下步骤
|
||
|
||
**当此技能被触发时,在编写任何代码之前,你必须遵循此工作流程。**
|
||
|
||
### 第 0 步:收集需求
|
||
|
||
在搭建任何内容之前,请让用户澄清(或从上下文中推断):
|
||
|
||
1. **技术栈**:后端和前端的语言/框架(例如 Express + React、Django + Vue、Go + HTMX)
|
||
2. **服务类型**:仅 API、全栈单体应用还是微服务?
|
||
3. **数据库**:SQL(PostgreSQL、SQLite、MySQL)还是 NoSQL(MongoDB、Redis)?
|
||
4. **集成方式**:REST、GraphQL、tRPC 还是 gRPC?
|
||
5. **实时性**:是否需要?如果需要——SSE、WebSocket 还是轮询?
|
||
6. **认证**:是否需要?如果需要——JWT、Session、OAuth 还是第三方(Clerk、Auth.js)?
|
||
|
||
如果用户已在请求中指定了以上内容,则跳过询问,直接继续。
|
||
|
||
### 第 1 步:架构决策
|
||
|
||
基于需求,在编码之前做出并声明以下决策:
|
||
|
||
| 决策项 | 可选方案 | 参考章节 |
|
||
|--------|---------|---------|
|
||
| 项目结构 | 按功能优先(推荐)vs 按层优先 | [第 1 节](#1-项目结构与分层关键) |
|
||
| API 客户端方式 | 类型化 fetch / React Query / tRPC / OpenAPI 代码生成 | [第 5 节](#5-api-客户端模式中等) |
|
||
| 认证策略 | JWT + 刷新 / Session / 第三方 | [第 6 节](#6-认证与中间件高) |
|
||
| 实时方案 | 轮询 / SSE / WebSocket | [第 11 节](#11-实时模式中等) |
|
||
| 错误处理 | 类型化错误层级 + 全局处理器 | [第 3 节](#3-错误处理与弹性高) |
|
||
|
||
简要说明每个选择的原因(每项一句话)。
|
||
|
||
### 第 2 步:使用检查清单搭建
|
||
|
||
使用下方相应的检查清单。确保所有已勾选的项目均已实现——不得跳过任何项。
|
||
|
||
### 第 3 步:按照模式实现
|
||
|
||
按照本文档中的模式编写代码。在实现每个部分时,引用相应的具体章节。
|
||
|
||
### 第 4 步:测试与验证
|
||
|
||
实现完成后,在声称完成之前运行以下检查:
|
||
|
||
1. **构建检查**:确保后端和前端均编译无误
|
||
```bash
|
||
# 后端
|
||
cd server && npm run build
|
||
# 前端
|
||
cd client && npm run build
|
||
```
|
||
2. **启动与冒烟测试**:启动服务器,验证关键端点返回预期的响应
|
||
```bash
|
||
# 启动服务器,然后测试
|
||
curl http://localhost:3000/health
|
||
curl http://localhost:3000/api/<resource>
|
||
```
|
||
3. **集成检查**:验证前端能够连接到后端(CORS、API 基础 URL、认证流程)
|
||
4. **实时性检查**(如适用):打开两个浏览器标签页,验证变更能够同步
|
||
|
||
如果任何检查失败,请在继续之前修复问题。
|
||
|
||
### 第 5 步:交接总结
|
||
|
||
向用户提供一个简要总结:
|
||
|
||
- **已构建的内容**:已实现的功能和端点列表
|
||
- **如何运行**:启动后端和前端的准确命令
|
||
- **缺失项/后续步骤**:任何延期事项、已知限制或建议的改进
|
||
- **关键文件**:列出用户应该了解的最重要文件
|
||
|
||
---
|
||
|
||
## 适用范围
|
||
|
||
**在以下情况使用此技能:**
|
||
- 构建全栈应用(后端 + 前端)
|
||
- 搭建新的后端服务或 API
|
||
- 设计服务层和模块边界
|
||
- 实现数据库访问、缓存或后台任务
|
||
- 编写错误处理、日志记录或配置管理
|
||
- 审查后端代码的架构问题
|
||
- 为生产环境加固
|
||
- 设置 API 客户端、认证流程、文件上传或实时功能
|
||
|
||
**不适用于:**
|
||
- 纯前端/UI 相关事项(请查阅你的前端框架文档)
|
||
- 脱离后端上下文的纯数据库模式设计
|
||
|
||
---
|
||
|
||
## 快速入门——新后端服务检查清单
|
||
|
||
- [ ] 使用**按功能优先**结构搭建项目
|
||
- [ ] 配置**集中化**,环境变量在启动时**验证**(快速失败)
|
||
- [ ] 定义**类型化错误层级**(而非通用 `Error`)
|
||
- [ ] **全局错误处理器**中间件
|
||
- [ ] **结构化 JSON 日志**,带请求 ID 传递
|
||
- [ ] 数据库:设置**迁移**,配置**连接池**
|
||
- [ ] 所有端点的**输入验证**(Zod / Pydantic / Go 验证器)
|
||
- [ ] **认证中间件**就位
|
||
- [ ] **健康检查**端点(`/health`、`/ready`)
|
||
- [ ] **优雅关闭**处理(SIGTERM)
|
||
- [ ] **CORS** 已配置(显式来源,而非 `*`)
|
||
- [ ] **安全头部**(helmet 或等效方案)
|
||
- [ ] 提交 `.env.example`(不含真实密钥)
|
||
|
||
## 快速入门——前后端集成检查清单
|
||
|
||
- [ ] **API 客户端**已配置(类型化 fetch 封装、React Query、tRPC 或 OpenAPI 生成)
|
||
- [ ] **基础 URL** 来自环境变量(而非硬编码)
|
||
- [ ] **认证令牌**自动附加到请求中(拦截器/中间件)
|
||
- [ ] **错误处理**——API 错误映射为用户可读的消息
|
||
- [ ] **加载状态**已处理(骨架屏/旋转图标,而非空白屏幕)
|
||
- [ ] **跨边界类型安全**(共享类型、OpenAPI 或 tRPC)
|
||
- [ ] **CORS** 已配置,使用显式来源(生产环境不使用 `*`)
|
||
- [ ] **刷新令牌**流程已实现(httpOnly cookie + 401 时透明重试)
|
||
|
||
---
|
||
|
||
## 快速导航
|
||
|
||
| 需要…… | 跳转到 |
|
||
|--------|--------|
|
||
| 组织项目文件夹 | [1. 项目结构](#1-项目结构与分层关键) |
|
||
| 管理配置 + 密钥 | [2. 配置](#2-配置与环境关键) |
|
||
| 正确处理错误 | [3. 错误处理](#3-错误处理与弹性高) |
|
||
| 编写数据库代码 | [4. 数据库访问模式](#4-数据库访问模式高) |
|
||
| 从前端设置 API 客户端 | [5. API 客户端模式](#5-api-客户端模式中等) |
|
||
| 添加认证中间件 | [6. 认证与中间件](#6-认证与中间件高) |
|
||
| 设置日志记录 | [7. 日志记录与可观测性](#7-日志记录与可观测性中高) |
|
||
| 添加后台任务 | [8. 后台任务](#8-后台任务与异步中等) |
|
||
| 实现缓存 | [9. 缓存模式](#9-缓存模式中等) |
|
||
| 上传文件(预签名 URL、multipart) | [10. 文件上传模式](#10-文件上传模式中等) |
|
||
| 添加实时功能(SSE、WebSocket) | [11. 实时模式](#11-实时模式中等) |
|
||
| 在前端 UI 中处理 API 错误 | [12. 跨边界错误处理](#12-跨边界错误处理中等) |
|
||
| 为生产环境加固 | [13. 生产环境加固](#13-生产环境加固中等) |
|
||
| 设计 API 端点 | [API 设计](references/api-design.md) |
|
||
| 设计数据库模式 | [数据库模式](references/db-schema.md) |
|
||
| 认证流程(JWT、刷新、Next.js SSR、RBAC) | [references/auth-flow.md](references/auth-flow.md) |
|
||
| CORS、环境变量、环境管理 | [references/environment-management.md](references/environment-management.md) |
|
||
|
||
---
|
||
|
||
## 核心原则(7 条铁律)
|
||
|
||
```
|
||
1. ✅ 按功能组织,而非按技术层
|
||
2. ✅ 控制器绝不包含业务逻辑
|
||
3. ✅ 服务绝不导入 HTTP 请求/响应类型
|
||
4. ✅ 所有配置来自环境变量,启动时验证,快速失败
|
||
5. ✅ 每个错误都有类型、被记录,并返回一致的格式
|
||
6. ✅ 所有输入在边界处验证——不信任来自客户端的任何内容
|
||
7. ✅ 使用带请求 ID 的结构化 JSON 日志——而非 console.log
|
||
```
|
||
|
||
---
|
||
|
||
## 1. 项目结构与分层(关键)
|
||
|
||
### 按功能优先的组织方式
|
||
|
||
```
|
||
✅ 按功能优先 ❌ 按层优先
|
||
src/ src/
|
||
orders/ controllers/
|
||
order.controller.ts order.controller.ts
|
||
order.service.ts user.controller.ts
|
||
order.repository.ts services/
|
||
order.dto.ts order.service.ts
|
||
order.test.ts user.service.ts
|
||
users/ repositories/
|
||
user.controller.ts ...
|
||
user.service.ts
|
||
shared/
|
||
database/
|
||
middleware/
|
||
```
|
||
|
||
### 三层架构
|
||
|
||
```
|
||
控制器(HTTP)→ 服务(业务逻辑)→ 仓库(数据访问)
|
||
```
|
||
|
||
| 层 | 职责 | ❌ 绝不 |
|
||
|------|--------|--------|
|
||
| 控制器 | 解析请求、验证、调用服务、格式化响应 | 业务逻辑、数据库查询 |
|
||
| 服务 | 业务规则、编排、事务管理 | HTTP 类型(req/res)、直接访问数据库 |
|
||
| 仓库 | 数据库查询、外部 API 调用 | 业务逻辑、HTTP 类型 |
|
||
|
||
### 依赖注入(所有语言)
|
||
|
||
**TypeScript:**
|
||
```typescript
|
||
class OrderService {
|
||
constructor(
|
||
private readonly orderRepo: OrderRepository, // ✅ 注入接口
|
||
private readonly emailService: EmailService,
|
||
) {}
|
||
}
|
||
```
|
||
|
||
**Python:**
|
||
```python
|
||
class OrderService:
|
||
def __init__(self, order_repo: OrderRepository, email_service: EmailService):
|
||
self.order_repo = order_repo # ✅ 注入
|
||
self.email_service = email_service
|
||
```
|
||
|
||
**Go:**
|
||
```go
|
||
type OrderService struct {
|
||
orderRepo OrderRepository // ✅ 接口
|
||
emailService EmailService
|
||
}
|
||
|
||
func NewOrderService(repo OrderRepository, email EmailService) *OrderService {
|
||
return &OrderService{orderRepo: repo, emailService: email}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 2. 配置与环境(关键)
|
||
|
||
### 集中化、类型化、快速失败
|
||
|
||
**TypeScript:**
|
||
```typescript
|
||
const config = {
|
||
port: parseInt(process.env.PORT || '3000', 10),
|
||
database: { url: requiredEnv('DATABASE_URL'), poolSize: intEnv('DB_POOL_SIZE', 10) },
|
||
auth: { jwtSecret: requiredEnv('JWT_SECRET'), expiresIn: process.env.JWT_EXPIRES_IN || '1h' },
|
||
} as const;
|
||
|
||
function requiredEnv(name: string): string {
|
||
const value = process.env[name];
|
||
if (!value) throw new Error(`Missing required env var: ${name}`); // 快速失败
|
||
return value;
|
||
}
|
||
```
|
||
|
||
**Python:**
|
||
```python
|
||
from pydantic_settings import BaseSettings
|
||
|
||
class Settings(BaseSettings):
|
||
database_url: str # 必填——没有它应用不会启动
|
||
jwt_secret: str # 必填
|
||
port: int = 3000 # 可选,有默认值
|
||
db_pool_size: int = 10
|
||
class Config:
|
||
env_file = ".env"
|
||
|
||
settings = Settings() # 如果缺少 DATABASE_URL 则快速失败
|
||
```
|
||
|
||
### 规则
|
||
|
||
```
|
||
✅ 所有配置通过环境变量(十二因素应用)
|
||
✅ 在启动时验证必填变量——快速失败
|
||
✅ 在配置层进行类型转换,而非在使用处
|
||
✅ 提交 .env.example,使用虚拟值
|
||
|
||
❌ 绝不硬编码密钥、URL 或凭据
|
||
❌ 绝不提交 .env 文件
|
||
❌ 绝不在代码中散落 process.env / os.environ
|
||
```
|
||
|
||
---
|
||
|
||
## 3. 错误处理与弹性(高)
|
||
|
||
### 类型化错误层级
|
||
|
||
```typescript
|
||
// 基类(TypeScript)
|
||
class AppError extends Error {
|
||
constructor(
|
||
message: string,
|
||
public readonly code: string,
|
||
public readonly statusCode: number,
|
||
public readonly isOperational: boolean = true,
|
||
) { super(message); }
|
||
}
|
||
class NotFoundError extends AppError {
|
||
constructor(resource: string, id: string) {
|
||
super(`${resource} not found: ${id}`, 'NOT_FOUND', 404);
|
||
}
|
||
}
|
||
class ValidationError extends AppError {
|
||
constructor(public readonly errors: FieldError[]) {
|
||
super('Validation failed', 'VALIDATION_ERROR', 422);
|
||
}
|
||
}
|
||
```
|
||
|
||
```python
|
||
# 基类(Python)
|
||
class AppError(Exception):
|
||
def __init__(self, message: str, code: str, status_code: int):
|
||
self.message, self.code, self.status_code = message, code, status_code
|
||
|
||
class NotFoundError(AppError):
|
||
def __init__(self, resource: str, id: str):
|
||
super().__init__(f"{resource} not found: {id}", "NOT_FOUND", 404)
|
||
```
|
||
|
||
### 全局错误处理器
|
||
|
||
```typescript
|
||
// TypeScript(Express)
|
||
app.use((err, req, res, next) => {
|
||
if (err instanceof AppError && err.isOperational) {
|
||
return res.status(err.statusCode).json({
|
||
title: err.code, status: err.statusCode,
|
||
detail: err.message, request_id: req.id,
|
||
});
|
||
}
|
||
logger.error('Unexpected error', { error: err.message, stack: err.stack, request_id: req.id });
|
||
res.status(500).json({ title: 'Internal Error', status: 500, request_id: req.id });
|
||
});
|
||
```
|
||
|
||
### 规则
|
||
|
||
```
|
||
✅ 类型化、领域特定的错误类
|
||
✅ 全局错误处理器捕获所有异常
|
||
✅ 操作性错误→结构化响应
|
||
✅ 编程错误→记录日志 + 通用 500
|
||
✅ 对临时故障使用指数退避重试
|
||
|
||
❌ 绝不捕获并静默忽略错误
|
||
❌ 绝不向客户端返回堆栈跟踪
|
||
❌ 绝不抛出通用的 Error('something')
|
||
```
|
||
|
||
---
|
||
|
||
## 4. 数据库访问模式(高)
|
||
|
||
### 始终使用迁移
|
||
|
||
```bash
|
||
# TypeScript(Prisma) # Python(Alembic) # Go(golang-migrate)
|
||
npx prisma migrate dev alembic revision --autogenerate migrate -source file://migrations
|
||
npx prisma migrate deploy alembic upgrade head migrate -database $DB up
|
||
```
|
||
|
||
```
|
||
✅ 通过迁移进行模式变更,绝不手动执行 SQL
|
||
✅ 迁移必须是可逆的
|
||
✅ 在应用到生产环境前审查迁移 SQL
|
||
❌ 绝不手动修改生产环境模式
|
||
```
|
||
|
||
### N+1 预防
|
||
|
||
```typescript
|
||
// ❌ N+1:1 次查询 + N 次查询
|
||
const orders = await db.order.findMany();
|
||
for (const o of orders) { o.items = await db.item.findMany({ where: { orderId: o.id } }); }
|
||
|
||
// ✅ 单次 JOIN 查询
|
||
const orders = await db.order.findMany({ include: { items: true } });
|
||
```
|
||
|
||
### 多步写入使用事务
|
||
|
||
```typescript
|
||
await db.$transaction(async (tx) => {
|
||
const order = await tx.order.create({ data: orderData });
|
||
await tx.inventory.decrement({ productId, quantity });
|
||
await tx.payment.create({ orderId: order.id, amount });
|
||
});
|
||
```
|
||
|
||
### 连接池
|
||
|
||
连接池大小 = `(CPU 核心数 × 2) + 磁盘轴数`(从 10-20 开始)。始终设置连接超时。对于无服务器场景使用 PgBouncer。
|
||
|
||
---
|
||
|
||
## 5. API 客户端模式(中等)
|
||
|
||
连接前后端的"胶水层"。选择适合你的团队和技术栈的方式。
|
||
|
||
### 选项 A:类型化 Fetch 封装(简单,无依赖)
|
||
|
||
```typescript
|
||
// lib/api-client.ts
|
||
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
|
||
|
||
class ApiError extends Error {
|
||
constructor(public status: number, public body: any) {
|
||
super(body?.detail || body?.message || `API error ${status}`);
|
||
}
|
||
}
|
||
|
||
async function api<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||
const token = getAuthToken(); // 来自 cookie / 内存 / context
|
||
|
||
const res = await fetch(`${BASE_URL}${path}`, {
|
||
...options,
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||
...options.headers,
|
||
},
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const body = await res.json().catch(() => null);
|
||
throw new ApiError(res.status, body);
|
||
}
|
||
|
||
if (res.status === 204) return undefined as T;
|
||
return res.json();
|
||
}
|
||
|
||
export const apiClient = {
|
||
get: <T>(path: string) => api<T>(path),
|
||
post: <T>(path: string, data: unknown) => api<T>(path, { method: 'POST', body: JSON.stringify(data) }),
|
||
put: <T>(path: string, data: unknown) => api<T>(path, { method: 'PUT', body: JSON.stringify(data) }),
|
||
patch: <T>(path: string, data: unknown) => api<T>(path, { method: 'PATCH', body: JSON.stringify(data) }),
|
||
delete: <T>(path: string) => api<T>(path, { method: 'DELETE' }),
|
||
};
|
||
```
|
||
|
||
### 选项 B:React Query + 类型化客户端(React 推荐)
|
||
|
||
```typescript
|
||
// hooks/use-orders.ts
|
||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||
import { apiClient } from '@/lib/api-client';
|
||
|
||
interface Order { id: string; total: number; status: string; }
|
||
interface CreateOrderInput { items: { productId: string; quantity: number }[] }
|
||
|
||
export function useOrders() {
|
||
return useQuery({
|
||
queryKey: ['orders'],
|
||
queryFn: () => apiClient.get<{ data: Order[] }>('/api/orders'),
|
||
staleTime: 1000 * 60, // 1 分钟
|
||
});
|
||
}
|
||
|
||
export function useCreateOrder() {
|
||
const queryClient = useQueryClient();
|
||
return useMutation({
|
||
mutationFn: (data: CreateOrderInput) =>
|
||
apiClient.post<{ data: Order }>('/api/orders', data),
|
||
onSuccess: () => {
|
||
queryClient.invalidateQueries({ queryKey: ['orders'] });
|
||
},
|
||
});
|
||
}
|
||
|
||
// 在组件中使用:
|
||
function OrdersPage() {
|
||
const { data, isLoading, error } = useOrders();
|
||
const createOrder = useCreateOrder();
|
||
if (isLoading) return <Skeleton />;
|
||
if (error) return <ErrorBanner error={error} />;
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 选项 C:tRPC(同一团队同时维护两端)
|
||
|
||
```typescript
|
||
// server: trpc/router.ts
|
||
export const appRouter = router({
|
||
orders: router({
|
||
list: publicProcedure.query(async () => {
|
||
return db.order.findMany({ include: { items: true } });
|
||
}),
|
||
create: protectedProcedure
|
||
.input(z.object({ items: z.array(orderItemSchema) }))
|
||
.mutation(async ({ input, ctx }) => {
|
||
return orderService.create(ctx.user.id, input);
|
||
}),
|
||
}),
|
||
});
|
||
export type AppRouter = typeof appRouter;
|
||
|
||
// client:自动类型安全,无需代码生成
|
||
const { data } = trpc.orders.list.useQuery();
|
||
const createOrder = trpc.orders.create.useMutation();
|
||
```
|
||
|
||
### 选项 D:OpenAPI 生成客户端(公共/多消费者 API)
|
||
|
||
```bash
|
||
npx openapi-typescript-codegen \
|
||
--input http://localhost:3001/api/openapi.json \
|
||
--output src/generated/api \
|
||
--client axios
|
||
```
|
||
|
||
### 决策:选择哪个 API 客户端?
|
||
|
||
| 方式 | 适用场景 | 类型安全 | 工作量 |
|
||
|--------|--------|----------|--------|
|
||
| 类型化 fetch 封装 | 简单应用、小团队 | 手动类型 | 低 |
|
||
| React Query + fetch | React 应用、服务端状态 | 手动类型 | 中等 |
|
||
| tRPC | 同一团队,两端均为 TypeScript | 自动 | 低 |
|
||
| OpenAPI 生成 | 公共 API、多消费者 | 自动 | 中等 |
|
||
| GraphQL codegen | GraphQL API | 自动 | 中等 |
|
||
|
||
---
|
||
|
||
## 6. 认证与中间件(高)
|
||
|
||
> **完整参考:** [references/auth-flow.md](references/auth-flow.md)——JWT Bearer 流程、自动令牌刷新、Next.js 服务端认证、RBAC 模式、后端中间件顺序。
|
||
|
||
### 标准中间件顺序
|
||
|
||
```
|
||
请求 → 1.请求ID → 2.日志 → 3.CORS → 4.限流 → 5.请求体解析
|
||
→ 6.认证 → 7.鉴权 → 8.验证 → 9.处理器 → 10.错误处理器 → 响应
|
||
```
|
||
|
||
### JWT 规则
|
||
|
||
```
|
||
✅ 短期访问令牌(15 分钟)+ 刷新令牌(服务端存储)
|
||
✅ 最小化声明:userId、roles(而非整个用户对象)
|
||
✅ 定期轮换签名密钥
|
||
|
||
❌ 绝不将令牌存储在 localStorage 中(XSS 风险)
|
||
❌ 绝不通过 URL 查询参数传递令牌
|
||
```
|
||
|
||
### RBAC 模式
|
||
|
||
```typescript
|
||
function authorize(...roles: Role[]) {
|
||
return (req, res, next) => {
|
||
if (!req.user) throw new UnauthorizedError();
|
||
if (!roles.some(r => req.user.roles.includes(r))) throw new ForbiddenError();
|
||
next();
|
||
};
|
||
}
|
||
router.delete('/users/:id', authenticate, authorize('admin'), deleteUser);
|
||
```
|
||
|
||
### 认证令牌自动刷新
|
||
|
||
```typescript
|
||
// lib/api-client.ts——在 401 时透明刷新
|
||
async function apiWithRefresh<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||
try {
|
||
return await api<T>(path, options);
|
||
} catch (err) {
|
||
if (err instanceof ApiError && err.status === 401) {
|
||
const refreshed = await api<{ accessToken: string }>('/api/auth/refresh', {
|
||
method: 'POST',
|
||
credentials: 'include', // 发送 httpOnly cookie
|
||
});
|
||
setAuthToken(refreshed.accessToken);
|
||
return api<T>(path, options); // 重试
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 7. 日志记录与可观测性(中高)
|
||
|
||
### 结构化 JSON 日志
|
||
|
||
```typescript
|
||
// ✅ 结构化——可解析、可过滤、可告警
|
||
logger.info('Order created', {
|
||
orderId: order.id, userId: user.id, total: order.total,
|
||
items: order.items.length, duration_ms: Date.now() - startTime,
|
||
});
|
||
// 输出:{"level":"info","msg":"Order created","orderId":"ord_123",...}
|
||
|
||
// ❌ 非结构化——在大规模下毫无用处
|
||
console.log(`Order created for user ${user.id} with total ${order.total}`);
|
||
```
|
||
|
||
### 日志级别
|
||
|
||
| 级别 | 适用场景 | 生产环境? |
|
||
|------|---------|-----------|
|
||
| error | 需要立即关注 | ✅ 始终 |
|
||
| warn | 意外但已处理 | ✅ 始终 |
|
||
| info | 正常操作、审计追踪 | ✅ 始终 |
|
||
| debug | 开发调试 | ❌ 仅开发环境 |
|
||
|
||
### 规则
|
||
|
||
```
|
||
✅ 每条日志条目包含请求 ID(通过中间件传播)
|
||
✅ 在层边界处记录日志(请求进入、响应返回、外部调用)
|
||
❌ 绝不记录密码、令牌、PII 或密钥
|
||
❌ 绝不在生产代码中使用 console.log
|
||
```
|
||
|
||
---
|
||
|
||
## 8. 后台任务与异步(中等)
|
||
|
||
### 规则
|
||
|
||
```
|
||
✅ 所有任务必须是幂等的(同一任务执行两次 = 相同结果)
|
||
✅ 失败任务→重试(最多 3 次)→死信队列→告警
|
||
✅ 工作进程作为独立进程运行(而非 API 服务器中的线程)
|
||
|
||
❌ 绝不在请求处理器中放置长时间运行的任务
|
||
❌ 绝不假设任务只执行一次
|
||
```
|
||
|
||
### 幂等任务模式
|
||
|
||
```typescript
|
||
async function processPayment(data: { orderId: string }) {
|
||
const order = await orderRepo.findById(data.orderId);
|
||
if (order.paymentStatus === 'completed') return; // 已处理
|
||
await paymentGateway.charge(order);
|
||
await orderRepo.updatePaymentStatus(order.id, 'completed');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 9. 缓存模式(中等)
|
||
|
||
### 旁路缓存(懒加载)
|
||
|
||
```typescript
|
||
async function getUser(id: string): Promise<User> {
|
||
const cached = await redis.get(`user:${id}`);
|
||
if (cached) return JSON.parse(cached);
|
||
|
||
const user = await userRepo.findById(id);
|
||
if (!user) throw new NotFoundError('User', id);
|
||
|
||
await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 900); // 15 分钟 TTL
|
||
return user;
|
||
}
|
||
```
|
||
|
||
### 规则
|
||
|
||
```
|
||
✅ 始终设置 TTL——绝不做无过期时间的缓存
|
||
✅ 在写入时失效(更新后删除缓存键)
|
||
✅ 缓存用于读取,绝不用于权威状态
|
||
|
||
❌ 绝不缓存而不设 TTL(过期数据比慢数据更糟糕)
|
||
```
|
||
|
||
| 数据类型 | 建议 TTL |
|
||
|-----------|--------|
|
||
| 用户资料 | 5-15 分钟 |
|
||
| 产品目录 | 1-5 分钟 |
|
||
| 配置/功能开关 | 30-60 秒 |
|
||
| 会话 | 与会话时长一致 |
|
||
|
||
---
|
||
|
||
## 10. 文件上传模式(中等)
|
||
|
||
### 选项 A:预签名 URL(大文件推荐)
|
||
|
||
```
|
||
客户端 → GET /api/uploads/presign?filename=photo.jpg&type=image/jpeg
|
||
服务端 → { uploadUrl: "https://s3.../presigned", fileKey: "uploads/abc123.jpg" }
|
||
客户端 → PUT uploadUrl(直接上传至 S3,绕过你的服务器)
|
||
客户端 → POST /api/photos { fileKey: "uploads/abc123.jpg" }(保存引用)
|
||
```
|
||
|
||
**后端:**
|
||
```typescript
|
||
app.get('/api/uploads/presign', authenticate, async (req, res) => {
|
||
const { filename, type } = req.query;
|
||
const key = `uploads/${crypto.randomUUID()}-${filename}`;
|
||
const url = await s3.getSignedUrl('putObject', {
|
||
Bucket: process.env.S3_BUCKET, Key: key,
|
||
ContentType: type, Expires: 300, // 5 分钟
|
||
});
|
||
res.json({ uploadUrl: url, fileKey: key });
|
||
});
|
||
```
|
||
|
||
**前端:**
|
||
```typescript
|
||
async function uploadFile(file: File) {
|
||
const { uploadUrl, fileKey } = await apiClient.get<PresignResponse>(
|
||
`/api/uploads/presign?filename=${file.name}&type=${file.type}`
|
||
);
|
||
await fetch(uploadUrl, { method: 'PUT', body: file, headers: { 'Content-Type': file.type } });
|
||
return apiClient.post('/api/photos', { fileKey });
|
||
}
|
||
```
|
||
|
||
### 选项 B:Multipart(小于 10MB 的小文件)
|
||
|
||
```typescript
|
||
// 前端
|
||
const formData = new FormData();
|
||
formData.append('file', file);
|
||
formData.append('description', 'Profile photo');
|
||
const res = await fetch('/api/upload', { method: 'POST', body: formData });
|
||
// 注意:不要设置 Content-Type 头部——浏览器会自动设置 boundary
|
||
```
|
||
|
||
### 决策
|
||
|
||
| 方式 | 文件大小 | 服务器负载 | 复杂度 |
|
||
|--------|---------|-----------|--------|
|
||
| 预签名 URL | 任意(推荐 > 5MB) | 无(直接上传至存储) | 中等 |
|
||
| Multipart | < 10MB | 高(流经服务器) | 低 |
|
||
| 分块/断点续传 | > 100MB | 中等 | 高 |
|
||
|
||
---
|
||
|
||
## 11. 实时模式(中等)
|
||
|
||
### 选项 A:服务器推送事件(SSE)——单向服务端 → 客户端
|
||
|
||
最适合:通知、实时推送、AI 响应流式输出。
|
||
|
||
**后端(Express):**
|
||
```typescript
|
||
app.get('/api/events', authenticate, (req, res) => {
|
||
res.writeHead(200, {
|
||
'Content-Type': 'text/event-stream',
|
||
'Cache-Control': 'no-cache',
|
||
Connection: 'keep-alive',
|
||
});
|
||
const send = (event: string, data: unknown) => {
|
||
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
||
};
|
||
const unsubscribe = eventBus.subscribe(req.user.id, (event) => {
|
||
send(event.type, event.payload);
|
||
});
|
||
req.on('close', () => unsubscribe());
|
||
});
|
||
```
|
||
|
||
**前端:**
|
||
```typescript
|
||
function useServerEvents(userId: string) {
|
||
useEffect(() => {
|
||
const source = new EventSource(`/api/events?userId=${userId}`);
|
||
source.addEventListener('notification', (e) => {
|
||
showToast(JSON.parse(e.data).message);
|
||
});
|
||
source.onerror = () => { source.close(); setTimeout(() => /* 重连 */, 3000); };
|
||
return () => source.close();
|
||
}, [userId]);
|
||
}
|
||
```
|
||
|
||
### 选项 B:WebSocket——双向
|
||
|
||
最适合:聊天、协同编辑、游戏。
|
||
|
||
**后端(ws 库):**
|
||
```typescript
|
||
import { WebSocketServer } from 'ws';
|
||
const wss = new WebSocketServer({ server: httpServer, path: '/ws' });
|
||
wss.on('connection', (ws, req) => {
|
||
const userId = authenticateWs(req);
|
||
if (!userId) { ws.close(4001, 'Unauthorized'); return; }
|
||
ws.on('message', (raw) => handleMessage(userId, JSON.parse(raw.toString())));
|
||
ws.on('close', () => cleanupUser(userId));
|
||
const interval = setInterval(() => ws.ping(), 30000);
|
||
ws.on('pong', () => { /* 活跃 */ });
|
||
ws.on('close', () => clearInterval(interval));
|
||
});
|
||
```
|
||
|
||
**前端:**
|
||
```typescript
|
||
function useWebSocket(url: string) {
|
||
const [ws, setWs] = useState<WebSocket | null>(null);
|
||
useEffect(() => {
|
||
const socket = new WebSocket(url);
|
||
socket.onopen = () => setWs(socket);
|
||
socket.onclose = () => setTimeout(() => /* 重连 */, 3000);
|
||
return () => socket.close();
|
||
}, [url]);
|
||
const send = useCallback((data: unknown) => ws?.send(JSON.stringify(data)), [ws]);
|
||
return { ws, send };
|
||
}
|
||
```
|
||
|
||
### 选项 C:轮询(最简单,无需基础设施)
|
||
|
||
```typescript
|
||
function useOrderStatus(orderId: string) {
|
||
return useQuery({
|
||
queryKey: ['order-status', orderId],
|
||
queryFn: () => apiClient.get<Order>(`/api/orders/${orderId}`),
|
||
refetchInterval: (query) => {
|
||
if (query.state.data?.status === 'completed') return false;
|
||
return 5000;
|
||
},
|
||
});
|
||
}
|
||
```
|
||
|
||
### 决策
|
||
|
||
| 方式 | 方向 | 复杂度 | 适用场景 |
|
||
|--------|---------|--------|---------|
|
||
| 轮询 | 客户端 → 服务端 | 低 | 简单的状态检查,少于 10 个客户端 |
|
||
| SSE | 服务端 → 客户端 | 中等 | 通知、推送、AI 流式输出 |
|
||
| WebSocket | 双向 | 高 | 聊天、协作、游戏 |
|
||
|
||
---
|
||
|
||
## 12. 跨边界错误处理(中等)
|
||
|
||
### API 错误 → 面向用户的消息
|
||
|
||
```typescript
|
||
// lib/error-handler.ts
|
||
export function getErrorMessage(error: unknown): string {
|
||
if (error instanceof ApiError) {
|
||
switch (error.status) {
|
||
case 401: return '请登录后继续。';
|
||
case 403: return '你没有执行此操作的权限。';
|
||
case 404: return '你查找的项目不存在。';
|
||
case 409: return '与已有项目冲突。';
|
||
case 422:
|
||
const fields = error.body?.errors;
|
||
if (fields?.length) return fields.map((f: any) => f.message).join('。');
|
||
return '请检查你的输入。';
|
||
case 429: return '请求过于频繁,请稍后再试。';
|
||
default: return '出错了,请重试。';
|
||
}
|
||
}
|
||
if (error instanceof TypeError && error.message === 'Failed to fetch') {
|
||
return '无法连接到服务器,请检查你的网络连接。';
|
||
}
|
||
return '发生了意外错误。';
|
||
}
|
||
```
|
||
|
||
### React Query 全局错误处理器
|
||
|
||
```typescript
|
||
const queryClient = new QueryClient({
|
||
defaultOptions: {
|
||
mutations: { onError: (error) => toast.error(getErrorMessage(error)) },
|
||
queries: {
|
||
retry: (failureCount, error) => {
|
||
if (error instanceof ApiError && error.status < 500) return false;
|
||
return failureCount < 3;
|
||
},
|
||
},
|
||
},
|
||
});
|
||
```
|
||
|
||
### 规则
|
||
|
||
```
|
||
✅ 将每个 API 错误代码映射为人类可读的消息
|
||
✅ 在表单输入旁显示字段级别的验证错误
|
||
✅ 5xx 自动重试(最多 3 次,带退避),4xx 绝不重试
|
||
✅ 401 时重定向到登录页面(刷新尝试失败后)
|
||
✅ 当 fetch 因 TypeError 失败时显示"离线"横幅
|
||
|
||
❌ 绝不向用户显示原始 API 错误消息("NullPointerException")
|
||
❌ 绝不静默吞噬错误(显示 toast 或记录日志)
|
||
❌ 绝不重试 4xx 错误(客户端有误,重试无济于事)
|
||
```
|
||
|
||
### 集成决策树
|
||
|
||
```
|
||
同一团队维护前端 + 后端?
|
||
│
|
||
├─ 是,且两端均为 TypeScript
|
||
│ └─ tRPC(端到端类型安全,零代码生成)
|
||
│
|
||
├─ 是,但语言不同
|
||
│ └─ OpenAPI 规范 → 生成客户端(通过代码生成实现类型安全)
|
||
│
|
||
├─ 否,公共 API
|
||
│ └─ REST + OpenAPI → 为消费者生成 SDK
|
||
│
|
||
└─ 复杂数据需求,多个前端
|
||
└─ GraphQL + codegen(每个客户端的灵活查询)
|
||
|
||
需要实时功能?
|
||
│
|
||
├─ 仅服务端 → 客户端(通知、推送、AI 流式输出)
|
||
│ └─ SSE(最简单,自动重连,可穿透代理)
|
||
│
|
||
├─ 双向(聊天、协作)
|
||
│ └─ WebSocket(需要心跳 + 重连逻辑)
|
||
│
|
||
└─ 简单的状态轮询(少于 10 个客户端)
|
||
└─ React Query refetchInterval(无需基础设施)
|
||
```
|
||
|
||
---
|
||
|
||
## 13. 生产环境加固(中等)
|
||
|
||
### 健康检查
|
||
|
||
```typescript
|
||
app.get('/health', (req, res) => res.json({ status: 'ok' })); // 存活检查
|
||
app.get('/ready', async (req, res) => { // 就绪检查
|
||
const checks = {
|
||
database: await checkDb(), redis: await checkRedis(),
|
||
};
|
||
const ok = Object.values(checks).every(c => c.status === 'ok');
|
||
res.status(ok ? 200 : 503).json({ status: ok ? 'ok' : 'degraded', checks });
|
||
});
|
||
```
|
||
|
||
### 优雅关闭
|
||
|
||
```typescript
|
||
process.on('SIGTERM', async () => {
|
||
logger.info('SIGTERM received');
|
||
server.close(); // 停止新连接
|
||
await drainConnections(); // 完成正在处理的请求
|
||
await closeDatabase();
|
||
process.exit(0);
|
||
});
|
||
```
|
||
|
||
### 安全检查清单
|
||
|
||
```
|
||
✅ CORS:显式来源(生产环境绝不用 '*')
|
||
✅ 安全头部(helmet / 等效方案)
|
||
✅ 对公共端点进行限流
|
||
✅ 所有端点进行输入验证(不信任任何内容)
|
||
✅ 强制使用 HTTPS
|
||
❌ 绝不向客户端暴露内部错误
|
||
```
|
||
|
||
---
|
||
|
||
## 反模式
|
||
|
||
| # | ❌ 不要做 | ✅ 应该做 |
|
||
|---|---------|---------|
|
||
| 1 | 在路由/控制器中编写业务逻辑 | 移到服务层 |
|
||
| 2 | `process.env` 散落在各处 | 集中化的类型化配置 |
|
||
| 3 | 使用 `console.log` 记录日志 | 结构化 JSON 日志记录器 |
|
||
| 4 | 通用的 `Error('oops')` | 类型化错误层级 |
|
||
| 5 | 在控制器中直接调用数据库 | 仓库模式 |
|
||
| 6 | 没有输入验证 | 在边界处验证(Zod/Pydantic) |
|
||
| 7 | 静默捕获错误 | 记录日志 + 重新抛出或返回错误 |
|
||
| 8 | 没有健康检查端点 | `/health` + `/ready` |
|
||
| 9 | 硬编码配置/密钥 | 环境变量 |
|
||
| 10 | 没有优雅关闭 | 正确处理 SIGTERM |
|
||
| 11 | 在前端硬编码 API URL | 环境变量(`NEXT_PUBLIC_API_URL`) |
|
||
| 12 | 将 JWT 存储在 localStorage 中 | 内存 + httpOnly 刷新 cookie |
|
||
| 13 | 向用户显示原始 API 错误 | 映射为人类可读的消息 |
|
||
| 14 | 重试 4xx 错误 | 仅重试 5xx(服务端故障) |
|
||
| 15 | 跳过加载状态 | 获取数据时使用骨架屏/旋转图标 |
|
||
| 16 | 通过 API 服务器上传大文件 | 预签名 URL → 直接上传至 S3 |
|
||
| 17 | 轮询获取实时数据 | SSE 或 WebSocket |
|
||
| 18 | 前端 + 后端重复类型 | 共享类型、tRPC 或 OpenAPI codegen |
|
||
|
||
---
|
||
|
||
## 常见问题
|
||
|
||
### 问题 1:"这条业务规则应该放哪里?"
|
||
|
||
**规则:** 如果涉及 HTTP(请求解析、状态码、头部)→ 放控制器。如果涉及业务决策(定价、权限、规则)→ 放服务。如果涉及数据库 → 放仓库。
|
||
|
||
### 问题 2:"服务层变得太大了"
|
||
|
||
**症状:** 一个服务文件超过 500 行,包含 20 多个方法。
|
||
|
||
**修复:** 按子领域拆分。`OrderService` → `OrderCreationService` + `OrderFulfillmentService` + `OrderQueryService`。每个专注于一个工作流。
|
||
|
||
### 问题 3:"测试很慢,因为它们要访问数据库"
|
||
|
||
**修复:** 单元测试模拟仓库层(速度快)。集成测试使用测试容器或事务回滚(真实数据库,仍然快速)。绝不在集成测试中模拟服务层。
|
||
|
||
---
|
||
|
||
## 参考文档
|
||
|
||
此技能包含针对专门主题的深入参考文档。当你需要详细指导时,请阅读相应的参考文档。
|
||
|
||
| 需要…… | 参考文档 |
|
||
|---------|---------|
|
||
| 编写后端测试(单元、集成、端到端、契约、性能) | [references/testing-strategy.md](references/testing-strategy.md) |
|
||
| 在部署前验证发布(6 道关卡检查清单) | [references/release-checklist.md](references/release-checklist.md) |
|
||
| 选择技术栈(语言、框架、数据库、基础设施) | [references/technology-selection.md](references/technology-selection.md) |
|
||
| 使用 Django / DRF 构建(模型、视图、序列化器、管理后台) | [references/django-best-practices.md](references/django-best-practices.md) |
|
||
| 设计 REST/GraphQL/gRPC 端点(URL、状态码、分页) | [references/api-design.md](references/api-design.md) |
|
||
| 设计数据库模式、索引、迁移、多租户 | [references/db-schema.md](references/db-schema.md) |
|
||
| 认证流程(JWT Bearer、令牌刷新、Next.js SSR、RBAC、中间件顺序) | [references/auth-flow.md](references/auth-flow.md) |
|
||
| CORS 配置、各环境的环境变量、常见 CORS 问题 | [references/environment-management.md](references/environment-management.md) |
|