chore: import zh skill fullstack-dev

This commit is contained in:
wehub-skill-sync
2026-07-13 21:35:57 +08:00
commit 2e61c0357c
10 changed files with 3857 additions and 0 deletions
+9
View File
@@ -0,0 +1,9 @@
# WeHub 来源说明
- Skill 名称:`fullstack-dev`
- 中文类目:通用后端系统设计与全栈脚手架
- 上游仓库:`minimax-ai__skills`
- 上游路径:`skills/fullstack-dev/SKILL.md`
- 上游链接:https://github.com/minimax-ai/skills/blob/HEAD/skills/fullstack-dev/SKILL.md
- 本仓库为 WeHub 中文 Skill 汉化包,基于 skill 市场筛选 Top200 清单整理
- 原作者、版权和许可证信息以上游仓库为准
+1038
View File
File diff suppressed because it is too large Load Diff
+444
View File
@@ -0,0 +1,444 @@
---
name: fullstack-dev-api-design
description: "API 设计模式与最佳实践。适用于创建端点、选择方法/状态码、实现分页或编写 OpenAPI 规范。可避免常见的 REST/GraphQL/gRPC 错误。"
license: MIT
metadata:
version: "2.0.0"
sources:
- Microsoft REST API Guidelines
- Google API Design Guide
- Zalando RESTful API Guidelines
- JSON:API Specification
- RFC 9457 (Problem Details for HTTP APIs)
- RFC 9110 (HTTP Semantics)
---
# API 设计指南
面向后端与全栈工程师的框架无关 API 设计指南。涵盖 10 大类共 50 余条规则,按影响程度排序。覆盖 REST、GraphQL 和 gRPC。
## 适用范围
**使用此技能的场景:**
- 设计新 API 或添加端点
- 审查 API 拉取请求
- 在 REST / GraphQL / gRPC 之间做选择
- 编写 OpenAPI 规范
- 迁移或对现有 API 进行版本管理
**不适用的场景:**
- 框架特定的实现细节(请使用对应框架的技能/文档)
- 前端数据获取模式(请使用 React Query / SWR 文档)
- 认证实现细节(请使用认证库的文档)
- 数据库 schema 设计(→ `database-schema-design`
## 所需上下文
在应用此技能前,请收集以下信息:
| 必填项 | 可选项 |
|----------|----------|
| 目标消费者(浏览器、移动端、服务) | 项目中已有的 API 约定 |
| 预期请求量(RPS 预估) | 当前的 OpenAPI / Swagger 规范 |
| 认证方式(JWT、API Key、OAuth | 限流需求 |
| 数据模型 / 领域实体 | 缓存策略 |
---
## 快速开始清单
新建 API 端点?写代码前逐一检查:
- [ ] 资源命名为**复数名词**`/orders`,而不是 `/getOrders`
- [ ] URL 使用**短横线命名**,请求体字段使用**驼峰命名**
- [ ] 使用正确的 **HTTP 方法**GET=读取,POST=创建,PUT=全量替换,PATCH=部分更新,DELETE=删除)
- [ ] 使用正确的**状态码**201 Created、422 Validation、404 Not Found……)
- [ ] 错误响应遵循 **RFC 9457** 格式
- [ ] 所有列表端点实现**分页**(默认 20,最多 100)
- [ ] 要求**认证**Bearer Token,而非查询参数)
- [ ] 响应头中包含**请求 ID**`X-Request-Id`
- [ ] 包含**限流**响应头
- [ ] 端点已在 **OpenAPI 规范**中记录
---
## 快速导航
| 需要…… | 跳转至 |
|----------|---------|
| 命名资源 URL | [1. 资源建模](#1-资源建模关键) |
| 选择 HTTP 方法与状态码 | [3. HTTP 方法与状态码](#3-http-方法与状态码关键) |
| 格式化错误响应 | [4. 错误处理](#4-错误处理高优先级) |
| 添加分页或过滤 | [6. 分页与过滤](#6-分页与过滤高优先级) |
| 选择 API 风格(REST vs GraphQL vs gRPC | [10. API 风格决策](#10-api-风格决策树) |
| 对现有 API 进行版本管理 | [7. 版本管理](#7-版本管理中高优先级) |
| 避免常见错误 | [反模式清单](#反模式清单) |
---
## 1. 资源建模(关键)
### 核心规则
```
✅ /users — 复数名词
✅ /users/{id}/orders — 1 层嵌套
✅ /reviews?orderId={oid} — 使用查询参数展平深层嵌套
❌ /getUsers — URL 中包含动词
❌ /user — 单数
❌ /users/{uid}/orders/{oid}/items/{iid}/reviews — 超过 3 层嵌套
```
**最大嵌套层级:2 层。** 超过此限制时,提升为顶层资源并通过过滤参数查询。
### 领域对齐
资源映射到**领域概念**,而非数据库表:
```
✅ /checkout-sessions (领域聚合)
✅ /shipping-labels (领域概念)
❌ /tbl_order_header (数据库表泄露)
❌ /join_user_role (内部 schema 泄露)
```
---
## 2. URL 与命名(关键)
| 上下文 | 规范 | 示例 |
|---------|-----------|---------|
| URL 路径 | 短横线命名 | `/order-items` |
| JSON 请求体字段 | 驼峰命名 | `{ "firstName": "Jane" }` |
| 查询参数 | 驼峰命名或下划线命名(保持一致) | `?sortBy=createdAt` |
| 请求头 | 连字符大写命名 | `X-Request-Id` |
**Python 例外:** 如果整个技术栈都是 Python/下划线命名,可以在 JSON 中使用 `snake_case`——但必须在**所有端点间保持一致**。
```
✅ GET /users ❌ GET /users/
✅ GET /reports/annual ❌ GET /reports/annual.json
✅ POST /users ❌ POST /users/create
```
---
## 3. HTTP 方法与状态码(关键)
### 方法语义
| 方法 | 语义 | 幂等 | 安全 | 请求体 |
|--------|-----------|-----------|------|-------------|
| GET | 读取 | ✅ | ✅ | ❌ 从不 |
| POST | 创建 / 动作 | ❌ | ❌ | ✅ 始终 |
| PUT | 全量替换 | ✅ | ❌ | ✅ 始终 |
| PATCH | 部分更新 | ❌* | ❌ | ✅ 始终 |
| DELETE | 删除 | ✅ | ❌ | ❌ 极少 |
### 状态码快速参考
**成功类:**
| 状态码 | 使用时机 | 响应体 |
|------|------|--------------|
| 200 OK | GET、PUT、PATCH 成功 | 资源 / 结果 |
| 201 Created | POST 创建了资源 | 已创建的资源 + `Location` 请求头 |
| 202 Accepted | 异步操作已启动 | 任务 ID / 状态 URL |
| 204 No Content | DELETE 成功、PUT 无请求体 | 无 |
**客户端错误类:**
| 状态码 | 使用时机 | 关键区别 |
|------|------|-----------------|
| 400 Bad Request | 语法格式错误 | 无法解析 |
| 401 Unauthorized | 缺少或无效的认证凭据 | "你是谁?" |
| 403 Forbidden | 已认证但无权限 | "我认识你,但不行" |
| 404 Not Found | 资源不存在 | 也可用于隐藏 403 |
| 409 Conflict | 重复、版本不匹配 | 状态冲突 |
| 422 Unprocessable | 语法正确但验证失败 | 语义错误 |
| 429 Too Many Requests | 触发限流 | 包含 `Retry-After` |
**服务端错误类:** 500(意外错误)、502(上游失败)、503(过载)、504(上游超时)
---
## 4. 错误处理(高优先级)
### 标准错误格式(RFC 9457
所有错误响应均使用此格式:
```json
{
"type": "https://api.example.com/errors/insufficient-funds",
"title": "余额不足",
"status": 422,
"detail": "账户余额 $10.00 低于提现金额 $50.00。",
"instance": "/transactions/txn_abc123",
"request_id": "req_7f3a8b2c",
"errors": [
{ "field": "amount", "message": "超出余额", "code": "INSUFFICIENT_BALANCE" }
]
}
```
### 多语言实现
**TypeScriptExpress):**
```typescript
class AppError extends Error {
constructor(
public readonly title: string,
public readonly status: number,
public readonly detail: string,
public readonly code: string,
) { super(detail); }
}
// 中间件
app.use((err, req, res, next) => {
if (err instanceof AppError) {
return res.status(err.status).json({
type: `https://api.example.com/errors/${err.code}`,
title: err.title, status: err.status,
detail: err.detail, request_id: req.id,
});
}
res.status(500).json({ title: '内部错误', status: 500, request_id: req.id });
});
```
**PythonFastAPI):**
```python
from fastapi import Request
from fastapi.responses import JSONResponse
class AppError(Exception):
def __init__(self, title: str, status: int, detail: str, code: str):
self.title, self.status, self.detail, self.code = title, status, detail, code
@app.exception_handler(AppError)
async def app_error_handler(request: Request, exc: AppError):
return JSONResponse(status_code=exc.status, content={
"type": f"https://api.example.com/errors/{exc.code}",
"title": exc.title, "status": exc.status,
"detail": exc.detail, "request_id": request.state.request_id,
})
```
### 铁律
```
✅ 所有错误均返回 RFC 9457 错误格式
✅ 每个错误响应都包含 request_id
✅ 字段级验证错误在 `errors` 数组中返回
❌ 绝不在生产环境暴露堆栈信息
❌ 绝不为错误返回 200 状态码
❌ 绝不静默吞掉错误
```
---
## 5. 认证与授权(高优先级)
```
✅ Authorization: Bearer eyJhbGci... (请求头)
❌ GET /users?token=eyJhbGci... URL——会出现在日志中)
✅ 401 → "你是谁?" (缺少或无效的凭据)
✅ 403 → "你不能这样做" (已认证,无权限)
✅ 404 → 隐藏资源存在性 (需要时用来替代 403)
```
**限流响应头(始终包含):**
```
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1625097600
Retry-After: 30
```
---
## 6. 分页与过滤(高优先级)
### 游标分页 vs 偏移分页
| 策略 | 适用场景 | 优点 | 缺点 |
|----------|------|------|------|
| **游标分页**(推荐) | 大型/动态数据集 | 数据一致,无跳变 | 无法跳转到第 N 页 |
| **偏移分页** | 小型/稳定数据集、管理后台 | 简单,支持页面跳转 | 插入/删除时数据漂移 |
**游标分页响应:**
```json
{
"data": [...],
"pagination": { "next_cursor": "eyJpZCI6MTIwfQ", "has_more": true }
}
```
**偏移分页响应:**
```json
{
"data": [...],
"pagination": { "page": 3, "per_page": 20, "total": 256, "total_pages": 13 }
}
```
**始终执行:** 默认 20 条,最多 100 条。
### 标准过滤模式
```
GET /orders?status=shipped&created_after=2025-01-01&sort=-created_at&fields=id,status
```
| 模式 | 规范 |
|---------|-----------|
| 精确匹配 | `?status=shipped` |
| 范围查询 | `?price_gte=10&price_lte=100` |
| 日期范围 | `?created_after=2025-01-01&created_before=2025-12-31` |
| 排序 | `?sort=field`(升序)、`?sort=-field`(降序) |
| 稀疏字段 | `?fields=id,name,email` |
| 搜索 | `?q=搜索+关键词` |
---
## 7. 版本管理(中高优先级)
| 策略 | 格式 | 最佳适用 |
|----------|--------|----------|
| **URL 路径**(推荐) | `/v1/users` | 公开 API |
| **请求头** | `Api-Version: 2` | 内部 API |
| **查询参数** | `?version=2` | 遗留系统(避免使用) |
**非破坏性变更(无需升级版本号):** 新增可选的响应字段、新增端点、新增可选参数。
**破坏性变更(需新版本):** 删除/重命名字段、更改类型、收紧验证规则、删除端点。
**弃用响应头:**
```
Sunset: Sat, 01 Mar 2026 00:00:00 GMT
Deprecation: true
Link: <https://api.example.com/v2/users>; rel="successor-version"
```
---
## 8. 请求/响应设计(中优先级)
### 统一的响应格式
```json
{
"data": { "id": "ord_123", "status": "pending", "total": 99.50 },
"meta": { "request_id": "req_abc123", "timestamp": "2025-06-15T10:30:00Z" }
}
```
### 关键规则
| 规则 | 正确 | 错误 |
|------|---------|-------|
| 时间戳 | `"2025-06-15T10:30:00Z"`ISO 8601 | `"06/15/2025"``1718447400` |
| 公开 ID | UUID `"550e8400-..."` | 自增 `42` |
| Null 与缺省(PATCH | `{ "nickname": null }` = 清空字段 | 缺省字段 = 不做更改 |
| HATEOAS(公开 API | `"links": { "cancel": "/orders/123/cancel" }` | 无可发现性 |
---
## 9. 文档——OpenAPI(中优先级)
**设计优先的工作流程:**
```
1. 编写 OpenAPI 3.1 规范
2. 与相关方审查规范
3. 生成服务端桩代码 + 客户端 SDK
4. 实现处理器
5. 在 CI 中对照规范验证响应
```
每个端点均需记录:摘要、所有参数、请求体及示例、所有响应码及 schema、认证要求。
---
## 10. API 风格决策树
```
什么样的 API
├─ 浏览器 + 移动端客户端,需要灵活查询
│ └─ GraphQL
│ 规则:DataLoader(避免 N+1)、深度限制 ≤7、Relay 分页
├─ 标准 CRUD,面向公开消费者,缓存很重要
│ └─ REST(本指南)
│ 规则:资源、HTTP 方法、状态码、OpenAPI
├─ 服务间通信,高吞吐量,强类型
│ └─ gRPC
│ 规则:Protobuf schema、大数据流式传输、截止时间
├─ 全栈 TypeScript,同一团队拥有客户端和服务端
│ └─ tRPC
│ 规则:共享类型,无需代码生成
└─ 实时双向通信
└─ WebSocket / SSE
规则:心跳、重连、消息排序
```
---
## 反模式清单
| # | ❌ 不要这样做 | ✅ 应该这样做 |
|---|---------|--------------|
| 1 | URL 中包含动词(`/getUser`) | HTTP 方法 + 名词资源 |
| 2 | 错误时返回 200 | 正确的 4xx/5xx 状态码 |
| 3 | 混合使用多种命名风格 | 每个上下文保持一种规范 |
| 4 | 暴露数据库 ID | 使用 UUID 作为公开标识符 |
| 5 | 列表不进行分页 | 始终分页(默认 20 条) |
| 6 | 静默吞掉错误 | 结构化的 RFC 9457 错误 |
| 7 | Token 放在 URL 查询参数中 | Authorization 请求头 |
| 8 | 深层嵌套(3 层以上) | 使用查询参数展平 |
| 9 | 破坏性变更不升级版本 | 保持兼容或使用版本管理 |
| 10 | 不设限流 | 实现限流并通过请求头告知 |
| 11 | 无请求 ID | 每个响应都包含 `X-Request-Id` |
| 12 | 生产环境暴露堆栈信息 | 安全的错误消息 + 内部日志 |
---
## 常见问题
### 问题 1:"这应该是新资源还是子资源?"
**现象:** URL 路径不断增长(`/users/{id}/orders/{id}/items/{id}/reviews`
**规则:** 如果子实体本身有意义,则提升为顶层资源。如果它只在父级上下文中存在,则保持嵌套(最多 2 层)。
```
/reviews?orderId=123 ✅ reviews 独立存在)
/orders/{id}/items ✅ items 属于 orders1 层)
```
### 问题 2"PUT 还是 PATCH"
**现象:** 团队对更新语义无法达成一致。
**规则:**
- PUT = 客户端发送**完整**资源(缺失字段 → 设为默认值/null)
- PATCH = 客户端只发送**发生变化的字段**(缺失字段 → 保持不变)
- 不确定时 → **PATCH**(更安全,更少意外)
### 问题 3"400 还是 422"
**现象:** 验证错误码不一致。
**规则:**
- 400 = 完全无法解析请求(格式错误的 JSON、错误的 content-type
- 422 = 解析成功,但值未通过验证(无效的 email、负数数量)
+172
View File
@@ -0,0 +1,172 @@
---
name: auth-flow-patterns
description: 完整的认证流程模式,涵盖前端与后端
metadata:
type: reference
---
# 认证流程模式
前端和后端的完整认证流程。涵盖 JWT Bearer 流程、自动令牌刷新、Next.js 服务端认证、RBAC 以及后端中间件顺序。
---
## JWT Bearer 流程(最常见)
```
1. 登录
客户端 → POST /api/auth/login { email, password }
服务端 → { accessToken15分钟), refreshToken7天,httpOnly cookie }
2. 认证请求
客户端 → GET /api/orders Authorization: Bearer <accessToken>
服务端 → 验证 JWT → 返回数据
3. 令牌刷新(无感透明)
客户端 → 收到 401 → POST /api/auth/refreshcookie 自动发送)
服务端 → 新的 accessToken
客户端 → 使用新令牌重试原始请求
4. 登出
客户端 → POST /api/auth/logout
服务端 → 使 refresh token 失效 → 清除 cookie
```
---
## 前端:自动令牌刷新
```typescript
// lib/api-client.ts — 添加到现有的 fetch 封装中
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;
}
}
```
---
## Next.js:服务端认证(App Router
```typescript
// middleware.ts — 在服务端保护路由
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
export function middleware(request: NextRequest) {
const token = request.cookies.get('session')?.value;
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
// app/dashboard/page.tsx — 带认证的服务端组件
import { cookies } from 'next/headers';
export default async function Dashboard() {
const token = (await cookies()).get('session')?.value;
const user = await fetch(`${process.env.API_URL}/api/me`, {
headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json());
return <DashboardContent user={user} />;
}
```
---
## 后端:标准中间件顺序
```
请求 → 1.请求ID → 2.日志 → 3.CORS → 4.限流 → 5.请求体解析
→ 6.认证 → 7.授权 → 8.校验 → 9.处理器 → 10.错误处理 → 响应
```
---
## 后端:JWT 规则
```
✅ 短时效的 access token15分钟)+ refresh token(服务端存储)
✅ 最小化声明: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);
```
---
## 认证方式决策表
| 方法 | 适用场景 | 前端 |
|--------|------|----------|
| Session | 同域、SSR、Django 模板 | Django templates / htmx |
| JWT | 跨域、SPA、移动端 | React, Vue, 移动端应用 |
| OAuth2 | 第三方登录、API 消费者 | 任何 |
---
## 铁律
```
✅ Access token:短时效(15分钟),存于内存
✅ Refresh tokenhttpOnly cookieXSS 安全)
✅ 收到 401 时自动无感刷新
✅ 刷新失败时重定向到登录页
❌ 切勿将令牌存储在 localStorage 中(存在 XSS 风险)
❌ 切勿在 URL 查询参数中传递令牌(会被记录)
❌ 切勿仅依赖客户端认证检查(服务端必须验证)
```
---
## 常见问题
### 问题 1:「页面加载时认证正常,导航后失效」
**原因:** 令牌存储在组件状态中(组件卸载后丢失)。
**解决方案:** 将 access token 存储在持久化位置:
- React Context(导航后仍存活,页面刷新后丢失)
- Cookie(刷新后仍存活)
- React Query 缓存,对 session 设置 `staleTime: Infinity`
### 问题 2:「认证请求出现 CORS 错误」
**原因:** 前端缺少 `credentials: 'include'` 或后端 CORS 配置中缺少 `credentials: true`
**解决方案:**
1. 前端:`fetch(url, { credentials: 'include' })`
2. 后端:`cors({ origin: 'https://your-frontend.com', credentials: true })`
3. 后端:使用凭据时必须指定明确的 origin(不能使用 `*`
+705
View File
@@ -0,0 +1,705 @@
---
name: fullstack-dev-db-schema
description: "数据库表结构设计与迁移。适用于创建表、定义 ORM 模型、添加索引或设计关系。涵盖零停机迁移与多租户。"
license: MIT
metadata:
version: "1.0.0"
sources:
- PostgreSQL 官方文档
- Use The Index, Luke (use-the-index-luke.com)
- 设计数据密集型应用(Martin Kleppmann
- 数据库可靠性工程(Laine Campbell & Charity Majors
---
# 数据库表结构设计
ORM 无关的关系型数据库表结构设计指南。涵盖数据建模、规范化、索引、迁移、多租户以及常见应用模式。主要面向 PostgreSQL,但原则同样适用于 MySQL/MariaDB。
## 适用范围
**使用该技能的场景:**
- 为新项目或新功能设计表结构
- 在规范化和反规范化之间做决策
- 选择需要创建的索引
- 规划在线数据库的零停机迁移
- 实现多租户数据隔离
- 添加审计追踪、软删除或版本控制
- 诊断因表结构问题导致的慢查询
**不适用的场景:**
- 选择使用哪种数据库技术(→ `technology-selection`
- PostgreSQL 特定的查询调优(请使用 PostgreSQL 性能文档)
- ORM 特定的配置(→ `django-best-practices` 或对应 ORM 的文档)
- 应用层缓存(→ `fullstack-dev-practices`
## 所需上下文
| 必需 | 可选 |
|----------|----------|
| 数据库引擎(PostgreSQL / MySQL | 预期数据量(行数、增长率) |
| 领域实体及其关系 | 读写比例 |
| 关键访问模式(查询) | 多租户需求 |
---
## 快速入门清单
设计新表结构:
- [ ] **识别领域实体**——每个实体映射一张表(而非每个类一张表)
- [ ] **主键**:公开 ID 使用 UUID,仅内部使用使用 serial/bigserial
- [ ] **外键**带有显式的 `ON DELETE` 行为
- [ ] **默认 NOT NULL**——仅在业务逻辑要求时才设为可空
- [ ] **时间戳**:每张表都包含 `created_at` + `updated_at`
- [ ] **索引**:为每个 WHERE、JOIN、ORDER BY 列创建索引
- [ ] **不提前反规范化**——从规范化开始,在有测量依据时才反规范化
- [ ] **命名约定**保持一致:`snake_case`,表名使用复数
---
## 快速导航
| 需要… | 跳转至 |
|----------|---------|
| 建模实体与关系 | [1. 数据建模](#1-数据建模关键) |
| 决定规范化还是反规范化 | [2. 规范化](#2-规范化-vs-反规范化关键) |
| 选择合适的索引 | [3. 索引](#3-索引策略关键) |
| 在在线数据库上安全运行迁移 | [4. 迁移](#4-零停机迁移高) |
| 设计多租户表结构 | [5. 多租户](#5-多租户设计高) |
| 添加软删除/审计追踪 | [6. 常见模式](#6-常见表结构模式中) |
| 对大表进行分区 | [7. 分区](#7-表分区中) |
| 查看反模式 | [反模式](#反模式) |
---
## 核心原则(7 条规则)
```
1. ✅ 从规范化(3NF)开始——只有在有测量证据时才反规范化
2. ✅ 每张表都有主键、created_at、updated_at
3. ✅ 对外公开的 ID 使用 UUID,内部连接键使用 serial
4. ✅ 默认 NOT NULL——null 是业务决策,而非偷懒的默认值
5. ✅ 为每个用在 WHERE、JOIN、ORDER BY 中的列创建索引
6. ✅ 外键在数据库中强制执行(而非仅在应用代码中)
7. ✅ 迁移是增量的——绝不在生产中一步完成删除/重命名,必须有多步骤计划
```
---
## 1. 数据建模(关键)
### 表命名
```sql
-- ✅ 复数、snake_case
CREATE TABLE orders (...);
CREATE TABLE order_items (...);
CREATE TABLE user_profiles (...);
-- ❌ 单数、大小写混用
CREATE TABLE Order (...);
CREATE TABLE OrderItem (...);
CREATE TABLE tbl_usr_prof (...); -- 晦涩的缩写
```
### 主键
| 策略 | 适用场景 | 优点 | 缺点 |
|----------|------|------|------|
| `bigserial`(自增) | 内部表、外键连接 | 紧凑、连接快 | 可枚举,不适合公开 ID |
| `uuid`(v4 随机) | 面向公开的资源 | 不可猜测、全局唯一 | 体积更大(16 字节)、B-Tree 随机 I/O |
| `uuid` v7(按时间排序) | 公开 + 需要排序 | 不可猜测 + 插入友好 | 较新,生态系统支持较少 |
| `text` slug | URL 友好的资源 | 人类可读 | 必须强制唯一性,更新成本高 |
**推荐的默认方案:**
```sql
CREATE TABLE orders (
id bigserial PRIMARY KEY, -- 内部外键目标
public_id uuid NOT NULL DEFAULT gen_random_uuid() UNIQUE, -- API 对外
-- ...
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
```
### 关系
```sql
-- 一对多:user → orders
CREATE TABLE orders (
id bigserial PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
-- ...
);
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- 多对多:orders ↔ products(通过连接表)
CREATE TABLE order_items (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id) ON DELETE CASCADE,
product_id bigint NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
quantity int NOT NULL CHECK (quantity > 0),
unit_price numeric(10,2) NOT NULL,
UNIQUE (order_id, product_id) -- 防止重复的行项目
);
-- 一对一:user → profile
CREATE TABLE user_profiles (
user_id bigint PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
bio text,
avatar_url text,
-- ...
);
```
### ON DELETE 行为
| 行为 | 适用场景 | 示例 |
|----------|------|---------|
| `CASCADE` | 子表数据脱离父表无意义 | 删除订单时同时删除 order_items |
| `RESTRICT` | 防止意外删除 | 被 order_items 引用的 products |
| `SET NULL` | 保留子表数据,清除引用 | 员工离职时 orders.assigned_to 置空 |
| `SET DEFAULT` | 回退为默认值 | 较少使用,用于状态列 |
---
## 2. 规范化 vs 反规范化(关键)
### 从规范化开始(3NF
**实际中的范式:**
| 范式 | 规则 | 违反示例 |
|------|------|-------------------|
| 1NF | 无重复组、原子值 | 一列中存 `tags = "go,python,rust"` |
| 2NF | 无部分依赖(组合键) | `order_items.product_name` 仅依赖于 `product_id` |
| 3NF | 无传递依赖 | `orders.customer_city` 依赖于 `customer_id` 而非 `order_id` |
**1NF 违反修复:**
```sql
-- ❌ 标签作为逗号分隔的字符串
CREATE TABLE posts (id serial, tags text); -- tags = "go,python"
-- ✅ 单独的表(若简单也可用数组或 JSONB)
CREATE TABLE post_tags (
post_id bigint REFERENCES posts(id) ON DELETE CASCADE,
tag_id bigint REFERENCES tags(id) ON DELETE CASCADE,
PRIMARY KEY (post_id, tag_id)
);
-- ✅ 替代方案:PostgreSQL 数组(如果标签仅是字符串,无元数据)
CREATE TABLE posts (id serial, tags text[] NOT NULL DEFAULT '{}');
CREATE INDEX idx_posts_tags ON posts USING GIN(tags);
```
### 何时反规范化
**仅在以下条件同时满足时才反规范化:**
1. 你已经**测量到**性能问题(EXPLAIN ANALYZE,而非「我觉得慢」)
2. 反规范化后的数据是**读密集**型(读写比例 > 100:1
3. 你接受**一致性维护成本**(触发器、应用逻辑或物化视图)
**安全的反规范化模式:**
```sql
-- 模式 1:物化视图(计算后、可刷新)
CREATE MATERIALIZED VIEW order_summary AS
SELECT o.id, o.user_id, o.total,
COUNT(oi.id) AS item_count,
u.email AS user_email
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN users u ON u.id = o.user_id
GROUP BY o.id, u.email;
REFRESH MATERIALIZED VIEW CONCURRENTLY order_summary; -- 非阻塞
-- 模式 2:缓存聚合列(由应用程序维护)
ALTER TABLE orders ADD COLUMN item_count int NOT NULL DEFAULT 0;
-- 通过触发器或在 order_item 插入/删除时的应用代码来更新
-- 模式 3:JSONB 快照(写入时冻结)
-- 在购买时存储产品信息的副本
CREATE TABLE order_items (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id),
product_id bigint REFERENCES products(id),
quantity int NOT NULL,
unit_price numeric(10,2) NOT NULL, -- 冻结的价格
product_snapshot jsonb NOT NULL -- 冻结的名称、描述、图片
);
```
---
## 3. 索引策略(关键)
### 索引类型(PostgreSQL
| 类型 | 适用场景 | 示例 |
|------|------|---------|
| **B-Tree**(默认) | 等值、范围、ORDER BY | `WHERE status = 'active'``WHERE created_at > '2025-01-01'` |
| **Hash** | 仅等值(很少使用,B-Tree 通常更好) | `WHERE id = 123`(大表,Postgres 10+ |
| **GIN** | 数组、JSONB、全文搜索 | `WHERE tags @> '{go}'``WHERE data->>'key' = 'val'` |
| **GiST** | 几何、范围、最近邻 | PostGIS、tsrange、ltree |
| **BRIN** | 带有自然顺序的超大表 | 按时间戳排序的时间序列数据 |
### 索引决策规则
```
规则 1:为 WHERE 子句中的每个列创建索引
规则 2:为 JOIN ON 条件中的每个列创建索引
规则 3:为 ORDER BY 中的每个列创建索引(如果与 LIMIT 一起查询)
规则 4:多列 WHERE 使用复合索引(最左前缀规则)
规则 5:仅过滤子集时使用部分索引(例如,仅活跃记录)
规则 6:使用覆盖索引(INCLUDE)以避免回表查询
规则 7:不要单独为低基数列建索引(例如布尔值)
```
### 复合索引:列顺序很重要
```sql
-- 查询:WHERE user_id = ? AND status = ? ORDER BY created_at DESC
-- ✅ 最优:从左到右匹配查询模式
CREATE INDEX idx_orders_user_status_created
ON orders(user_id, status, created_at DESC);
-- ❌ 顺序错误:无法高效用于该查询
CREATE INDEX idx_orders_created_user_status
ON orders(created_at DESC, user_id, status);
```
**最左前缀规则:** `(A, B, C)` 上的索引支持对 `(A)``(A, B)``(A, B, C)` 的查询,但不支持 `(B)``(C)``(B, C)` 的查询。
### 部分索引(仅索引所需数据)
```sql
-- 只有 5% 的订单是 'pending',但频繁查询
CREATE INDEX idx_orders_pending
ON orders(created_at DESC)
WHERE status = 'pending';
-- 只有活跃用户对登录有意义
CREATE INDEX idx_users_active_email
ON users(email)
WHERE is_active = true;
```
### 覆盖索引(避免回表查询)
```sql
-- 查询只需要 id 和 status,无需读取表行
CREATE INDEX idx_orders_user_covering
ON orders(user_id) INCLUDE (status, total);
-- 现在该查询是仅索引查询:
SELECT status, total FROM orders WHERE user_id = 123;
```
### 何时不要建索引
```
❌ 很少用于 WHERE/JOIN/ORDER BY 的列
❌ 行数 < 1000 的表(顺序扫描更快)
❌ 单独来看基数值很低的列(例如布尔值 is_active)
❌ 写密集的表,索引维护成本超过读收益
❌ 重复索引(检查 pg_stat_user_indexes 中的未使用索引)
```
---
## 4. 零停机迁移(高)
### 黄金法则
```
绝不要在一个步骤中做破坏性变更。
始终:添加 → 迁移数据 → 移除旧数据(分多次部署)。
```
### 安全的迁移模式
**重命名列(3 次部署):**
```
部署 1:添加新列
ALTER TABLE users ADD COLUMN full_name text;
UPDATE users SET full_name = name; -- 回填
-- 应用同时写入 name 和 full_name
部署 2:将读取切换到新列
-- 应用从 full_name 读取,仍然同时写入两个字段
部署 3:删除旧列
ALTER TABLE users DROP COLUMN name;
-- 应用仅使用 full_name
```
**添加 NOT NULL 列(2 次部署):**
```sql
-- 部署 1:添加可空列,回填
ALTER TABLE orders ADD COLUMN currency text; -- 先设为可空
UPDATE orders SET currency = 'USD' WHERE currency IS NULL; -- 回填
-- 部署 2:添加约束(所有行回填完成后)
ALTER TABLE orders ALTER COLUMN currency SET NOT NULL;
ALTER TABLE orders ALTER COLUMN currency SET DEFAULT 'USD';
```
**无锁添加索引:**
```sql
-- ✅ CONCURRENTLY:不锁表,可在在线数据库上运行
CREATE INDEX CONCURRENTLY idx_orders_status ON orders(status);
-- ❌ 不加 CONCURRENTLY:建索引期间锁住写入
CREATE INDEX idx_orders_status ON orders(status);
```
### 迁移安全清单
```
✅ 迁移在生产数据量级上运行时间 < 30 秒
✅ 无排他表锁(索引使用 CONCURRENTLY
✅ 回滚方案已记录并测试
✅ 回填分批执行(而非一次巨型 UPDATE)
✅ 新列先以可空方式添加,稍后添加约束
✅ 旧列保留到所有代码引用移除为止
❌ 绝不在一次部署中重命名/删除列
❌ 绝不在大表上不做时间测试就执行 ALTER TYPE
❌ 绝不在事务中运行数据回填(大表上会 OOM)
```
### 分批回填模板
```sql
-- 以 10,000 行为一批进行回填(避免长时间运行的事务)
DO $$
DECLARE
batch_size int := 10000;
affected int;
BEGIN
LOOP
UPDATE orders
SET currency = 'USD'
WHERE id IN (
SELECT id FROM orders WHERE currency IS NULL LIMIT batch_size
);
GET DIAGNOSTICS affected = ROW_COUNT;
RAISE NOTICE 'Updated % rows', affected;
EXIT WHEN affected = 0;
PERFORM pg_sleep(0.1); -- 短暂暂停以降低负载
END LOOP;
END $$;
```
---
## 5. 多租户设计(高)
### 三种方案
| 方案 | 隔离性 | 复杂度 | 适用场景 |
|----------|-----------|------------|------|
| **行级**(共享表 + `tenant_id` | 低 | 低 | SaaS MVP,租户数 < 1000 |
| **每租户独立 Schema** | 中 | 中 | 受监管行业,中等规模 |
| **每租户独立数据库** | 高 | 高 | 企业级,严格数据隔离 |
### 行级租户(最常见)
```sql
-- 每张表都有 tenant_id
CREATE TABLE orders (
id bigserial PRIMARY KEY,
tenant_id bigint NOT NULL REFERENCES tenants(id),
user_id bigint NOT NULL REFERENCES users(id),
total numeric(10,2) NOT NULL,
-- ...
);
-- 复合索引:租户在前(大多数查询按租户过滤)
CREATE INDEX idx_orders_tenant_user ON orders(tenant_id, user_id);
CREATE INDEX idx_orders_tenant_status ON orders(tenant_id, status);
-- 行级安全(PostgreSQL
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON orders
USING (tenant_id = current_setting('app.tenant_id')::bigint);
```
**应用层强制:**
```typescript
// 中间件:在每个请求上设置租户上下文
app.use((req, res, next) => {
const tenantId = req.headers['x-tenant-id'];
if (!tenantId) return res.status(400).json({ error: '缺少租户标识' });
req.tenantId = tenantId;
next();
});
// 仓库层:始终按租户过滤
async findOrders(tenantId: string, userId: string) {
return db.order.findMany({
where: { tenantId, userId }, // ← 每条查询中都有 tenant_id
});
}
```
### 规则
```
✅ 每张包含租户数据的表都有 tenant_id
✅ tenant_id 作为每个复合索引的第一列
✅ 应用中间件强制租户上下文
✅ 使用 RLS(PostgreSQL)作为纵深防御,而非唯一保护
✅ 用 2 个以上的租户进行测试以验证隔离
❌ 绝不允许在应用代码中进行跨租户查询
❌ 绝不在 WHERE 子句中遗漏 tenant_id(即便在管理工具中)
```
---
## 6. 常见表结构模式(中)
### 软删除
```sql
ALTER TABLE orders ADD COLUMN deleted_at timestamptz;
-- 所有查询过滤已删除记录
CREATE VIEW active_orders AS
SELECT * FROM orders WHERE deleted_at IS NULL;
-- 部分索引:仅索引未删除的行
CREATE INDEX idx_orders_active_status
ON orders(status, created_at DESC)
WHERE deleted_at IS NULL;
```
**ORM 集成:**
```typescript
// Prisma 中间件:自动过滤软删除记录
prisma.$use(async (params, next) => {
if (params.action === 'findMany' || params.action === 'findFirst') {
params.args.where = { ...params.args.where, deletedAt: null };
}
return next(params);
});
```
### 审计追踪
```sql
-- 方案 A:每张表上的审计列
ALTER TABLE orders ADD COLUMN created_by bigint REFERENCES users(id);
ALTER TABLE orders ADD COLUMN updated_by bigint REFERENCES users(id);
-- 方案 B:独立的审计日志表(更详细)
CREATE TABLE audit_log (
id bigserial PRIMARY KEY,
table_name text NOT NULL,
record_id bigint NOT NULL,
action text NOT NULL CHECK (action IN ('INSERT', 'UPDATE', 'DELETE')),
old_data jsonb,
new_data jsonb,
changed_by bigint REFERENCES users(id),
changed_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX idx_audit_table_record ON audit_log(table_name, record_id);
CREATE INDEX idx_audit_changed_at ON audit_log(changed_at DESC);
```
### 枚举列
```sql
-- 方案 APostgreSQL 枚举类型(严格,但 ALTER TYPE 很麻烦)
CREATE TYPE order_status AS ENUM ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled');
ALTER TABLE orders ADD COLUMN status order_status NOT NULL DEFAULT 'pending';
-- 方案 B:文本 + CHECK 约束(更容易迁移)
ALTER TABLE orders ADD COLUMN status text NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'confirmed', 'shipped', 'delivered', 'cancelled'));
-- 方案 C:查找表(最灵活,适合 UI 驱动的列表)
CREATE TABLE order_statuses (
id serial PRIMARY KEY,
name text UNIQUE NOT NULL,
label text NOT NULL -- 显示名称
);
```
**推荐:** 大多数情况下使用方案 B(文本 + CHECK)。如果状态由非开发人员管理,则使用方案 C。
### 多态关联
```sql
-- ❌ 反模式:多态外键(无参照完整性)
CREATE TABLE comments (
id bigserial PRIMARY KEY,
commentable_type text, -- 'Post' 或 'Photo'
commentable_id bigint, -- 无法建立外键约束!
body text
);
-- ✅ 模式 A:单独的外键列(可空)
CREATE TABLE comments (
id bigserial PRIMARY KEY,
post_id bigint REFERENCES posts(id) ON DELETE CASCADE,
photo_id bigint REFERENCES photos(id) ON DELETE CASCADE,
body text NOT NULL,
CHECK (
(post_id IS NOT NULL AND photo_id IS NULL) OR
(post_id IS NULL AND photo_id IS NOT NULL)
)
);
-- ✅ 模式 B:独立的表(最清晰,适合不同表结构)
CREATE TABLE post_comments (..., post_id bigint REFERENCES posts(id));
CREATE TABLE photo_comments (..., photo_id bigint REFERENCES photos(id));
```
### JSONB 列(半结构化数据)
```sql
-- 好的用途:元数据、设置、灵活属性
CREATE TABLE products (
id bigserial PRIMARY KEY,
name text NOT NULL,
price numeric(10,2) NOT NULL,
attributes jsonb NOT NULL DEFAULT '{}' -- 颜色、尺寸、重量…
);
-- JSONB 查询的索引
CREATE INDEX idx_products_attrs ON products USING GIN(attributes);
-- 查询
SELECT * FROM products WHERE attributes->>'color' = 'red';
SELECT * FROM products WHERE attributes @> '{"size": "XL"}';
```
```
✅ 对真正灵活/可选的数据(元数据、设置、偏好)使用 JSONB
✅ 在查询 JSONB 列时使用 GIN 索引
❌ 绝不对本应作为列的数据(email、status、price)使用 JSONB
❌ 绝不用 JSONB 来逃避表结构设计(它不是 PostgreSQL 里的 MongoDB
```
---
## 7. 表分区(中)
### 何时分区
```
✅ 表行数 > 1 亿且仍在增长
✅ 大多数查询按分区键过滤(日期范围、租户)
✅ 旧数据可按分区删除/归档(高效的 DELETE)
❌ 表行数 < 1000 万(开销不值得)
❌ 查询不按分区键过滤(扫描所有分区)
```
### 范围分区(时间序列)
```sql
CREATE TABLE events (
id bigserial,
tenant_id bigint NOT NULL,
event_type text NOT NULL,
payload jsonb,
created_at timestamptz NOT NULL DEFAULT now()
) PARTITION BY RANGE (created_at);
-- 按月分区
CREATE TABLE events_2025_01 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE events_2025_02 PARTITION OF events
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- 使用 pg_partman 或 cron 自动创建分区
```
### 列表分区(多租户)
```sql
CREATE TABLE orders (
id bigserial,
tenant_id bigint NOT NULL,
total numeric(10,2)
) PARTITION BY LIST (tenant_id);
CREATE TABLE orders_tenant_1 PARTITION OF orders FOR VALUES IN (1);
CREATE TABLE orders_tenant_2 PARTITION OF orders FOR VALUES IN (2);
```
---
## 反模式
| # | ❌ 不要这样做 | ✅ 而应该这样做 |
|---|---------|--------------|
| 1 | 提前反规范化 | 从 3NF 开始,有测量依据时再反规范化 |
| 2 | 自增 ID 作为公开 API 标识符 | 公开用 UUID,内部用 serial |
| 3 | 无外键约束 | 始终在数据库中强制执行外键 |
| 4 | 默认可为空 | 默认 NOT NULL,需要时再设为可空 |
| 5 | 外键列上无索引 | 为每个外键列创建索引 |
| 6 | 一步完成破坏性迁移 | 分多次部署完成:添加 → 迁移 → 移除 |
| 7 | 不加 `CONCURRENTLY``CREATE INDEX` | 在线表上始终使用 `CONCURRENTLY` |
| 8 | 多态外键(`commentable_type + commentable_id` | 单独的外键列或独立的表 |
| 9 | 什么都用 JSONB | 灵活数据用 JSONB,结构化数据用列 |
| 10 | 没有 `created_at` / `updated_at` | 每张表都配有时间戳对 |
| 11 | 一列中存逗号分隔的值 | 单独的表或 PostgreSQL 数组 |
| 12 | 不带长度验证的 `text` | CHECK 约束或应用层验证 |
---
## 常见问题
### 问题 1:「查询很慢,但我已经有索引了」
**症状:** 尽管已有索引,`EXPLAIN ANALYZE` 仍然显示顺序扫描。
**原因:**
1. **索引列顺序错误**——复合索引 `(A, B)``WHERE B = ?` 无效
2. **选择性低**——布尔列上的索引(50% 的行匹配),优化器更倾向于顺序扫描
3. **统计信息过时**——运行 `ANALYZE table_name;`
4. **类型不匹配**——用 `integer` 参数比较 `varchar` 列 → 无法使用索引
**修复方法:** 检查 `EXPLAIN (ANALYZE, BUFFERS)`,确认索引与查询模式匹配,运行 `ANALYZE`
### 问题 2:「迁移锁住表数分钟」
**症状:** 执行期间 `ALTER TABLE` 阻塞所有写入。
**原因:** 添加 NOT NULL 约束、更改列类型、或者创建索引时未使用 `CONCURRENTLY`
**修复方法:**
```sql
-- 无锁添加索引
CREATE INDEX CONCURRENTLY idx_name ON table(col);
-- 无锁添加 NOT NULL 约束(Postgres 12+
ALTER TABLE t ADD CONSTRAINT t_col_nn CHECK (col IS NOT NULL) NOT VALID;
ALTER TABLE t VALIDATE CONSTRAINT t_col_nn; -- 非阻塞验证
```
### 问题 3:「多少个索引算太多?」
**经验法则:**
- 读密集表(报表、产品目录):5-10 个索引没问题
- 写密集表(事件、日志):最多 2-3 个索引
- 通过 `pg_stat_user_indexes` 监控——删除 `idx_scan = 0` 的索引
```sql
-- 查找未使用的索引
SELECT schemaname, relname, indexrelname, idx_scan
FROM pg_stat_user_indexes
WHERE idx_scan = 0 AND indexrelname NOT LIKE '%pkey%'
ORDER BY pg_relation_size(indexrelid) DESC;
+466
View File
@@ -0,0 +1,466 @@
# Django 最佳实践
适用于 Django 5.x 和 Django REST Framework 的生产级指南。涵盖 8 个类别共 40 余条规则。
## 核心原则(7 条)
```
1. ✅ 在首次迁移前配置自定义用户模型(之后无法更改)
2. ✅ 每个领域概念对应一个 Django 应用(用户、订单、支付)
3. ✅ 胖模型、瘦视图——业务逻辑放在模型/管理器里,而不是视图里
4. ✅ 始终使用 select_related/prefetch_related(避免 N+1
5. ✅ 按环境拆分配置(base + dev + prod
6. ✅ 使用 pytest-django + factory_boy 进行测试(而不是 fixtures
7. ✅ 生产环境绝不使用 runserver(使用 Gunicorn + Nginx
```
---
## 1. 项目结构(关键)
### 按领域划分应用
```
myproject/
├── config/ # 项目配置
│ ├── __init__.py
│ ├── settings/
│ │ ├── base.py # 共享配置
│ │ ├── dev.py # DEBUG=True,可接受 SQLite
│ │ └── prod.py # DEBUG=FalsePostgresHTTPS
│ ├── urls.py
│ ├── wsgi.py
│ └── asgi.py
├── apps/
│ ├── users/ # 自定义用户模型
│ │ ├── models.py
│ │ ├── serializers.py
│ │ ├── views.py
│ │ ├── urls.py
│ │ ├── admin.py
│ │ ├── services.py # 业务逻辑
│ │ ├── selectors.py # 复杂查询
│ │ └── tests/
│ │ ├── test_models.py
│ │ ├── test_views.py
│ │ └── factories.py
│ ├── orders/
│ └── payments/
├── manage.py
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ └── prod.txt
└── docker-compose.yml
```
### 规则
```
✅ 一个应用 = 一个限界上下文(用户、订单、支付)
✅ 业务逻辑放在 services.py / selectors.py 中,而不是视图里
✅ 每个应用拥有自己的 urls.py、admin.py、tests/
❌ 绝不要把一切塞进同一个应用
❌ 绝不在模型层面跨应用边界导入(使用 ID)
❌ 绝不要把业务逻辑放在视图或序列化器里
```
---
## 2. 模型与迁移(关键)
### 自定义用户模型(第一天就要做!)
```python
# apps/users/models.py
from django.contrib.auth.models import AbstractUser
from django.db import models
import uuid
class User(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(unique=True)
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = ['username']
class Meta:
db_table = 'users'
# config/settings/base.py
AUTH_USER_MODEL = 'users.User'
```
**这必须在 `migrate` 之前完成。之后无法更改。**
### 模型最佳实践
```python
class TimeStampedModel(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Order(TimeStampedModel):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='orders')
status = models.CharField(max_length=20, choices=OrderStatus.choices, default=OrderStatus.PENDING, db_index=True)
total = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
db_table = 'orders'
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'status']),
]
def can_cancel(self) -> bool:
return self.status in [OrderStatus.PENDING, OrderStatus.CONFIRMED]
def cancel(self):
if not self.can_cancel():
raise ValueError(f"Cannot cancel order in {self.status} status")
self.status = OrderStatus.CANCELLED
self.save(update_fields=['status', 'updated_at'])
```
### 迁移规则
```
✅ 审查迁移 SQLpython manage.py sqlmigrate app_name 0001
✅ 为迁移起描述性名称:--name add_status_index_to_orders
✅ 将数据迁移与 schema 迁移分开
✅ 先非破坏性操作:添加列 → 回填数据 → 移除旧列
❌ 绝不编辑或删除已应用的迁移
❌ 绝不使用不带 reverse 函数的 RunPython
```
---
## 3. 视图与序列化器——DRF(高优先级)
### 服务层模式
```python
# apps/orders/services.py
from django.db import transaction
class OrderService:
@staticmethod
@transaction.atomic
def create_order(user, items_data: list[dict]) -> Order:
total = sum(item['price'] * item['quantity'] for item in items_data)
order = Order.objects.create(user=user, total=total)
OrderItem.objects.bulk_create([
OrderItem(order=order, **item) for item in items_data
])
return order
@staticmethod
def cancel_order(order_id: str, user) -> Order:
order = Order.objects.select_for_update().get(id=order_id, user=user)
order.cancel()
return order
```
### 序列化器
```python
class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer(many=True, read_only=True)
class Meta:
model = Order
fields = ['id', 'status', 'total', 'items', 'created_at']
read_only_fields = ['id', 'total', 'created_at']
class CreateOrderSerializer(serializers.Serializer):
"""仅用于输入的序列化器——与输出序列化器分开。"""
items = serializers.ListField(
child=serializers.DictField(), min_length=1, max_length=50,
)
def validate_items(self, items):
for item in items:
if item.get('quantity', 0) < 1:
raise serializers.ValidationError("Quantity must be at least 1")
return items
```
### 视图(保持轻薄!)
```python
@api_view(['POST'])
@permission_classes([IsAuthenticated])
def create_order(request):
serializer = CreateOrderSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
order = OrderService.create_order(request.user, serializer.validated_data['items'])
return Response({'data': OrderSerializer(order).data}, status=status.HTTP_201_CREATED)
```
### 规则
```
✅ 将输入序列化器与输出序列化器分开
✅ 视图只做:校验 → 调用服务 → 序列化 → 响应
✅ 多模型写入使用 @transaction.atomic
❌ 绝不把业务逻辑放在视图或序列化器里
❌ 绝不使用 ModelSerializer 进行写操作(过于隐式)
```
---
## 4. 认证(高优先级)
| 方法 | 适用场景 | 前端 |
|--------|------|----------|
| Session | 同域、SSR、Django 模板 | Django 模板 / htmx |
| JWT | 不同域、SPA、移动端 | React、Vue、移动应用 |
| OAuth2 | 第三方登录、API 消费者 | 任意 |
### JWT 配置(djangorestframework-simplejwt
```python
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=15),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': True,
'BLACKLIST_AFTER_ROTATION': True,
}
```
---
## 5. 性能优化(高优先级)
### 预防 N+1 查询
```python
# ❌ N+1:1 次查询获取订单 + N 次查询获取用户
orders = Order.objects.all()
for o in orders:
print(o.user.email) # 每次循环都访问数据库
# ✅ select_related(外键/一对一——JOIN
orders = Order.objects.select_related('user').all()
# ✅ prefetch_related(多对多/反向外键——2 次查询)
orders = Order.objects.prefetch_related('items').all()
# ✅ 组合使用
orders = Order.objects.select_related('user').prefetch_related('items').all()
```
### 查询优化工具集
```python
# 只获取需要的列
User.objects.values('id', 'email')
User.objects.values_list('email', flat=True)
# 使用注解代替 Python 循环
from django.db.models import Count, Sum
Order.objects.annotate(item_count=Count('items'), revenue=Sum('items__price'))
# 批量操作
OrderItem.objects.bulk_create([...])
Order.objects.filter(status='pending').update(status='cancelled')
# 数据库索引
class Meta:
indexes = [
models.Index(fields=['user', 'status']),
models.Index(fields=['-created_at']),
models.Index(fields=['email'], condition=Q(is_active=True)),
]
# 分页
from rest_framework.pagination import CursorPagination
class OrderPagination(CursorPagination):
page_size = 20
ordering = '-created_at'
```
### 缓存
```python
from django.core.cache import cache
def get_product(product_id: str):
cache_key = f'product:{product_id}'
product = cache.get(cache_key)
if product is None:
product = Product.objects.get(id=product_id)
cache.set(cache_key, product, timeout=300)
return product
```
---
## 6. 测试(中高优先级)
### pytest-django + factory_boy
```python
# conftest.py
@pytest.fixture
def api_client():
return APIClient()
@pytest.fixture
def authenticated_client(api_client, user_factory):
user = user_factory()
api_client.force_authenticate(user=user)
return api_client
```
```python
# factories.py
class UserFactory(factory.django.DjangoModelFactory):
class Meta:
model = User
email = factory.Sequence(lambda n: f'user{n}@example.com')
username = factory.Sequence(lambda n: f'user{n}')
class OrderFactory(factory.django.DjangoModelFactory):
class Meta:
model = 'orders.Order'
user = factory.SubFactory(UserFactory)
total = factory.Faker('pydecimal', left_digits=3, right_digits=2, positive=True)
```
```python
# test_views.py
@pytest.mark.django_db
class TestListOrders:
def test_returns_user_orders(self, authenticated_client):
OrderFactory.create_batch(3, user=authenticated_client.handler._force_user)
response = authenticated_client.get('/api/orders/')
assert response.status_code == 200
assert len(response.data['data']) == 3
def test_requires_authentication(self, api_client):
response = api_client.get('/api/orders/')
assert response.status_code == 401
```
---
## 7. 管理后台定制(中等优先级)
```python
class OrderItemInline(admin.TabularInline):
model = OrderItem
extra = 0
readonly_fields = ['price']
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ['id', 'user', 'status', 'total', 'created_at']
list_filter = ['status', 'created_at']
search_fields = ['user__email', 'id']
readonly_fields = ['id', 'created_at', 'updated_at']
inlines = [OrderItemInline]
date_hierarchy = 'created_at'
def get_queryset(self, request):
return super().get_queryset(request).select_related('user')
```
---
## 8. 生产部署(中等优先级)
### 安全配置
```python
# settings/prod.py
DEBUG = False
ALLOWED_HOSTS = ['example.com', 'www.example.com']
CSRF_TRUSTED_ORIGINS = ['https://example.com']
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 31536000
```
### 部署架构
```
Nginx → Gunicorn → Django
PostgreSQL + Redis (cache)
Celery (background tasks)
```
```bash
gunicorn config.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 4 \
--timeout 120 \
--access-logfile -
```
### 静态文件——WhiteNoise
```python
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'whitenoise.middleware.WhiteNoiseMiddleware', # 紧跟在 SecurityMiddleware 之后
...
]
STATICFILES_STORAGE = 'whitenoise.storage.CompressedManifestStaticFilesStorage'
```
### 规则
```
✅ Gunicorn + Nginx(或 Cloud Run / Railway
✅ PostgreSQL(不要用 SQLite
✅ python manage.py check --deploy
✅ 使用 Sentry 进行错误追踪
❌ 生产环境绝不使用 runserver
❌ 生产环境绝不使用 DEBUG=True
❌ 生产环境绝不使用 SQLite
```
---
## 反模式
| # | ❌ 不要这样做 | ✅ 应该这样做 |
|---|---------|--------------|
| 1 | 在视图中写业务逻辑 | 服务层(`services.py` |
| 2 | 一个巨型应用 | 按领域划分应用 |
| 3 | 使用默认 User 模型 | 首次迁移前配置自定义 User 模型 |
| 4 | 不使用 `select_related` | 始终预加载关联对象 |
| 5 | 使用 Django fixtures 做测试 | 使用 `factory_boy` 工厂 |
| 6 | 单一 `settings.py` 文件 | 拆分:base + dev + prod |
| 7 | 生产环境使用 `runserver` | Gunicorn + Nginx |
| 8 | 生产环境使用 SQLite | PostgreSQL |
| 9 | 使用 `ModelSerializer` 进行写操作 | 显式的输入序列化器 |
| 10 | 在视图中写原生 SQL | ORM querysets + `selectors.py` |
---
## 常见问题
### 问题 1:「首次迁移后无法更改 User 模型」
**解决方案:** 如果从零开始:删除所有迁移文件 + 数据库,设置自定义 User 模型,重新迁移。如果已有数据:需进行复杂迁移(使用 `django-allauth` 或增量字段迁移)。
### 问题 2:「序列化器在处理大型查询集时速度太慢」
**解决方案:** 缺少 `select_related` / `prefetch_related` → 导致 N+1 查询。
```python
queryset = Order.objects.select_related('user').prefetch_related('items')
```
### 问题 3:「应用之间存在循环导入」
**解决方案:** 使用字符串引用:`models.ForeignKey('orders.Order', ...)` 代替导入模型类。对于服务层,在函数内部进行导入。
+78
View File
@@ -0,0 +1,78 @@
# 环境与 CORS 管理
跨前后端技术栈管理环境变量、API URL 和 CORS 配置的模式。
---
## 标准环境模式
```
# .env.local(被 gitignore,用于本地开发)
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_WS_URL=ws://localhost:3001
# 预发布环境(在 Vercel/CI 中设置)
NEXT_PUBLIC_API_URL=https://api-staging.example.com
# 生产环境(在 Vercel/CI 中设置)
NEXT_PUBLIC_API_URL=https://api.example.com
```
---
## 环境变量规则
```
✅ API 基础地址来自环境变量——绝不硬编码
✅ 客户端变量前缀使用 NEXT_PUBLIC_Next.js)或 VITE_Vite
✅ 后端 URL = 仅服务端使用的环境变量(用于 SSR 调用,不暴露给浏览器)
✅ 后端 CORS:按环境明确列出允许的域名列表
❌ 生产构建中绝不使用 localhost URL
❌ 绝不使用 NEXT_PUBLIC_ 前缀暴露仅后端使用的密钥
❌ 绝不提交 .env.local(提交含占位符的 .env.example
```
---
## CORS 配置
```typescript
// 后端:感知环境的 CORS
const ALLOWED_ORIGINS = {
development: ['http://localhost:3000', 'http://localhost:5173'],
staging: ['https://staging.example.com'],
production: ['https://example.com', 'https://www.example.com'],
};
app.use(cors({
origin: ALLOWED_ORIGINS[process.env.NODE_ENV || 'development'],
credentials: true, // 需要用于 cookie(身份认证)
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
}));
```
---
## 常见问题
### 问题 1:「浏览器报 CORS 错误,但 Postman 能正常访问」
**原因:** CORS 是浏览器的安全机制。Postman/curl 会跳过它。
**修复方法:**
1. 后端必须返回 `Access-Control-Allow-Origin: https://your-frontend.com`
2. 涉及 Cookie/身份认证时,两端都需要设置 `credentials: true`
3. 确认预检 `OPTIONS` 请求返回了正确的响应头
### 问题 2:「浏览器中环境变量为 undefined」
**原因:** 缺少客户端访问所需的 `NEXT_PUBLIC_``VITE_` 前缀。
**修复方法:** 客户端变量必须带有框架前缀。添加新环境变量后需要重新构建(它们在构建时被嵌入)。
### 问题 3:「本地能正常工作,预发布环境却失败」
**原因:** 域名不同,缺少预发布域的 CORS 配置。
**修复方法:** 将预发布域名添加到 `ALLOWED_ORIGINS`,确认部署平台已设置相应的环境变量。
+285
View File
@@ -0,0 +1,285 @@
---
name: release-acceptance-checklist
description: 发布与验收检查清单——后端与全栈应用的 6 道关卡发布检查清单
metadata:
type: reference
---
# 发布与验收检查清单
适用于后端和全栈应用的 6 道关卡发布检查清单。防止出现「在我机器上能跑」和「我们忘了检查 X」之类的故障。
**铁律:未通过所有关卡,不得发布。**
---
## 发布关卡概览
```
功能完成
关卡 1:功能验收 → 功能是否达到预期?
关卡 2:非功能验收 → 是否快速、可靠、可观测?
关卡 3:安全审查 → 是否安全?
关卡 4:部署就绪 → 能否安全部署并回滚?
关卡 5:发布执行 → 采用金丝雀部署 + 监控上线
关卡 6:发布后验证 → 在生产环境是否真正生效?
```
---
## 关卡 1:功能验收
**问题:功能是否满足需求文档的要求?**
- [ ] 所有来自工单/PRD 的验收条件均有通过测试
- [ ] 正常路径端到端可用
- [ ] 边界情况已测试(空输入、最大长度、Unicode)
- [ ] 错误情况已测试(无效输入、未找到、超时)
- [ ] 数据完整性已验证(CRUD 周期产生正确状态)
- [ ] 向后兼容性已确认(现有客户端未被破坏)
- [ ] API 契约与 OpenAPI 规范一致
- [ ] 幂等性已验证(重试不会产生重复数据)
### 证据模板
| 需求 | 测试 | 状态 | 备注 |
|------|------|------|------|
| 用户可创建订单 | `orders.api.test:creates order` | ✅ 通过 | |
| 空购物车 → 报错 | `orders.api.test:rejects empty` | ✅ 通过 | |
| 支付失败已处理 | `payments.test:handles decline` | ✅ 通过 | |
---
## 关卡 2:非功能验收
**问题:是否快速、可靠、可观测?**
### 性能
- [ ] 响应时间在预算范围内(p95 < ___ms)——已实测,非假设
- [ ] 无 N+1 查询(通过查询日志检查)
- [ ] 新查询使用索引(`EXPLAIN ANALYZE`
- [ ] 大数据集支持分页
- [ ] 缓存生效(命中率 > 80%)
- [ ] 连接池在负载下运行正常
### 可靠性
- [ ] 依赖失败时优雅降级(断路器)
- [ ] 瞬态故障的重试逻辑正常工作
- [ ] 所有外部调用均设置了超时
- [ ] 速率限制正确返回 429
- [ ] 健康检查端点已验证(`/health``/ready`
### 可观测性
- [ ] 结构化日志包含请求 ID(非 `console.log`
- [ ] 指标已暴露(请求数、延迟、错误率)
- [ ] 告警已配置(错误突增、延迟突增)
- [ ] 请求追踪端到端可用
- [ ] 新功能对应的仪表盘已更新
### 证据
| 指标 | 目标 | 实际值 | 状态 |
|------|------|--------|------|
| p95 响应时间 | < 500ms | ___ms | ✅/❌ |
| p99 响应时间 | < 1000ms | ___ms | ✅/❌ |
| 负载下错误率 | < 0.1% | ___% | ✅/❌ |
| 吞吐量 | > ___ RPS | ___ RPS | ✅/❌ |
---
## 关卡 3:安全审查
**问题:是否引入了安全漏洞?**
### 输入与输出
- [ ] 所有输入在服务端验证(永远不要信任客户端)
- [ ] 已防止 SQL 注入(仅使用参数化查询)
- [ ] 已防止 XSS(输出编码)
- [ ] 文件上传已验证(类型、大小、文件名已净化)
- [ ] 敏感端点已限流(登录、重置、API)
### 认证与数据
- [ ] 受保护端点需要有效凭据
- [ ] 用户只能访问自己的资源
- [ ] 管理后台路由需要管理员角色
- [ ] 令牌有过期机制(短生命周期访问令牌 + 刷新令牌)
- [ ] 密码已哈希(bcrypt/argon2,非 MD5/SHA
- [ ] 敏感数据未记入日志(密码、令牌、PII)
- [ ] 密钥存储在环境变量中(非硬编码)
- [ ] 错误消息不泄露内部信息
### 依赖
- [ ] 无已知漏洞(`npm audit` / `pip audit` / `govulncheck`
- [ ] 依赖版本已锁定在 lockfile 中
- [ ] 未使用的依赖已移除
---
## 关卡 4:部署就绪
**问题:能否安全部署,并在必要时回滚?**
### 代码
- [ ] 所有测试在 CI 中通过(而非「本地通过了」)
- [ ] 代码检查通过,构建成功
- [ ] 代码已审查并批准
- [ ] 无未解决的 TODO/FIXME/HACK
### 数据库
- [ ] 迁移在预发布环境使用类生产数据已测试
- [ ] 回滚迁移可正常工作(已测试!)
- [ ] 迁移不会造成破坏(仅新增)
- [ ] 已根据生产数据规模预估迁移耗时
- [ ] 数据回填方案已记录(如需)
### 配置
- [ ] 新增环境变量已在 `.env.example` 中记录
- [ ] 环境变量已在预发布环境设置并验证
- [ ] 环境变量已在生产环境设置
- [ ] 功能开关已配置(如适用)
### 回滚计划模板
```markdown
## 回滚计划:[功能]
### 触发回滚的条件
- 错误率 > 1% 持续 5 分钟
- p99 延迟 > 3000ms 持续 10 分钟
- 关键业务功能被破坏
### 步骤
1. 回退部署:[命令]
2. 回滚迁移(如已执行):[命令]
3. 清除缓存:[命令]
4. 通知团队:#incidents 频道
5. 验证回滚:[验证步骤]
### 预计耗时:[X 分钟]
### 数据恢复:[数据被修改时的恢复方案]
```
---
## 关卡 5:发布执行
### 部署顺序
```
1. 📢 在发布频道中公告
2. 🗄️ 数据库——执行迁移
- 运行迁移
- 验证完成
- 检查数据完整性
3. 🚀 部署——发布代码
- 先金丝雀发布(10% 流量)
- 监控 5 分钟
- 如正常 → 50% → 监控 → 100%
- 如不正常 → 立即停止
4. 🔍 冒烟测试
- 健康检查 → 200
- 登录正常
- 核心操作正常
- 无错误突增
5. ✅ 公告「发布完成。持续监控 30 分钟。」
```
### 金丝雀决策表
| 指标 | 基准值 | 金丝雀正常 | 停止 | 回滚 |
|------|--------|-----------|------|------|
| 错误率 | 0.05% | < 0.1% | 0.5% | > 1% |
| p95 延迟 | 300ms | < 500ms | 700ms | > 1000ms |
---
## 关卡 6:发布后验证
### 即时(0-30 分钟)
- [ ] 所有实例的健康检查均为绿色
- [ ] 错误率在正常范围内
- [ ] 延迟正常(p95、p99
- [ ] 核心用户流程已手动测试
- [ ] 日志正常——无意外错误
- [ ] 告警保持静默
### 短期(1-24 小时)
- [ ] 无客户投诉
- [ ] 业务指标稳定(转化率、收入、注册数)
- [ ] 内存/CPU 稳定(无缓慢增长)
- [ ] 队列积压已清除
- [ ] 数据库性能稳定
### 发布后报告模板
```markdown
## 发布报告:[功能]
- 部署时间:[时间戳] 由 @[工程师]
- 耗时:[分钟]
| 检查项 | 状态 | 备注 |
|--------|------|------|
| 健康检查 | ✅ | 全部正常 |
| 错误率 | ✅ | 0.03%(基准值:0.05% |
| p95 延迟 | ✅ | 310ms(基准值:300ms |
| 核心流程 | ✅ | 订单创建已验证 |
发现的问题:无 / [详情]
是否回滚:否 / 是:[原因]
```
---
## 发布就绪评分
每道关卡评分 **0-2**:(0 = 未检查,1 = 部分检查,2 = 已全面验证并有证据)
| 关卡 | 评分 |
|------|------|
| 1. 功能验收 | /2 |
| 2. 非功能验收 | /2 |
| 3. 安全审查 | /2 |
| 4. 部署就绪 | /2 |
| 5. 发布执行计划 | /2 |
| 6. 发布后验证计划 | /2 |
| **总分** | **/12** |
**决策:**
- **12/12** → 上线 ✅
- **10-11** → 上线,附带已记录的例外事项 + 指定负责人
- **< 10** → 不得发布。先修复差距。
---
## 常见借口
| ❌ 借口 | ✅ 事实 |
|--------|--------|
| 「这只是一个小改动」 | 小改动每天都会导致故障 |
| 「我们在本地测过了」 | 本地 ≠ 生产环境 |
| 「出问题了再修」 | 你会在凌晨 3 点修。现在就预防。 |
| 「今天就是截止日期」 | 出错的代码比延期交付成本更高 |
| 「CI 通过了」 | CI 并不检查所有内容。执行检查清单。 |
| 「我们随时可以回滚」 | 只有当你有计划并测试过回滚才行 |
| 「上次这么干没问题」 | 幸存者偏差。每次都要走检查清单。 |
+254
View File
@@ -0,0 +1,254 @@
# 技术选型框架
后端与全栈技术选型的结构化决策框架。防止分析瘫痪,同时确保严谨评估。
**铁律:未经明确的取舍分析,不得做出任何技术选择。**
"我喜欢"和"这很热门"都不是工程论证。
---
## 阶段一:先定需求,再选技术
### 非功能性需求(量化!)
| 维度 | 问题 | 糟糕的回答 | 好的回答 |
|-----------|----------|-----------|-------------|
| 规模 | 多少并发用户? | "很多" | "1K 并发,500 RPS 峰值" |
| 延迟 | 可接受的 p99 响应时间? | "快" | "API < 200ms,报告 < 2s" |
| 可用性 | 需要的正常运行时间? | "永远在线" | "99.9%(年停机 8.7 小时)" |
| 数据量 | 预计存储增长? | "很多" | "100GB/年,1000 万行" |
| 一致性 | 强一致还是最终一致? | "一致" | "支付强一致,信息流最终一致" |
| 合规性 | 监管要求? | "有一些" | "GDPR 数据驻留欧盟,SOC 2 Type II" |
### 团队约束
- 团队规模与资深程度
- 团队已熟练掌握的技术
- 能否为此技术栈招到人?(查看招聘市场)
- 时间压力(几天还是几个月上线)
- 许可、基础设施、培训的预算
---
## 阶段二:评估矩阵
对每个选项按加权标准打分 1-5 分:
| 评估标准 | 权重 | 选项 A | 选项 B | 选项 C |
|-----------|--------|----------|----------|----------|
| 满足功能性需求 | 5× | _ | _ | _ |
| 满足非功能性需求 | 5× | _ | _ | _ |
| 团队经验 / 学习曲线 | 4× | _ | _ | _ |
| 生态成熟度(库、工具) | 3× | _ | _ | _ |
| 社区与长期 viability | 3× | _ | _ | _ |
| 运维复杂度 | 3× | _ | _ | _ |
| 招聘人才池 availability | 2× | _ | _ | _ |
| 成本(许可 + 基础设施 + 培训) | 2× | _ | _ | _ |
| **加权总分** | | _ | _ | _ |
**规则:**
- 任何选项在 **5× 标准上得 1 分** → 自动淘汰
- 选项间差距在 **10% 以内** → 选团队最熟悉的
- 选项间差距在 **15% 以内** → 进行**有时间限制的概念验证**(最长 2-5 天)
---
## 阶段三:决策树
### 后端语言 / 框架
```
什么类型的项目?
├─ REST/GraphQL API,快速开发
│ ├─ 团队熟悉 TypeScript → Node.js
│ │ ├─ 功能完备,企业级模式 → NestJS
│ │ ├─ 轻量灵活 → Fastify / Hono / Express
│ │ └─ 全栈 + React → Next.js API routes
│ ├─ 团队熟悉 Python
│ │ ├─ 高性能异步 API → FastAPI
│ │ ├─ 全栈,后台管理繁重 → Django
│ │ └─ 轻量级 → Flask / Litestar
│ └─ 团队熟悉 Java/Kotlin
│ ├─ 企业级,大型团队 → Spring Boot
│ └─ 轻量级,启动快 → Quarkus / Ktor
├─ 高并发,系统级
│ ├─ 微服务,网络编程 → Go
│ ├─ 极致性能,安全性 → Rust (Axum / Actix)
│ └─ 容错性 → Elixir (Phoenix)
├─ 实时(WebSocket、流式)
│ ├─ Node.js 生态 → Socket.io / ws
│ ├─ 可扩展的发布/订阅 → Elixir Phoenix
│ └─ 低延迟 → Go / Rust
└─ 机器学习 / 数据密集
└─ Python (FastAPI + ML libs)
```
### 数据库
```
什么数据模型?
├─ 结构化,关系型,ACID
│ ├─ 通用 → PostgreSQL ← 默认选择
│ ├─ 读密集,MySQL 生态 → MySQL / MariaDB
│ └─ 嵌入式 / 无服务器边缘 → SQLite / Turso / D1
├─ 半结构化,灵活 schema
│ ├─ 文档型 → MongoDB
│ ├─ 无服务器文档 → DynamoDB / Firestore
│ └─ 搜索密集 → Elasticsearch / OpenSearch
├─ 键值 / 缓存
│ ├─ 内存 + 数据结构 → Redis / Valkey
│ └─ 星球级 KV → DynamoDB / Cassandra
├─ 时间序列 → TimescaleDB / ClickHouse / InfluxDB
├─ 图 → Neo4j / Apache AGE (Postgres 扩展)
└─ 向量(AI 嵌入) → pgvector / Pinecone / Qdrant
```
**默认:** 从 PostgreSQL 开始。它能处理 80% 的使用场景。
### 缓存策略
| 模式 | 技术 | 何时使用 |
|---------|-----------|------|
| 应用缓存 | Redis / Valkey | 会话、频繁读取、限流 |
| HTTP 缓存 | CDN (Cloudflare/Vercel) | 静态资源、公开 API 响应 |
| 查询缓存 | 物化视图 | 复杂聚合、仪表盘 |
| 进程内缓存 | LRU(内存) | 配置、小型查找表 |
| 边缘缓存 | Cloudflare KV / Vercel KV | 全球低延迟读取 |
### 消息队列 / 事件流
| 模式 | 技术 | 何时使用 |
|---------|-----------|------|
| 任务队列(后台作业) | BullMQ / Celery / SQS | 邮件、导出、支付 |
| 事件流(回放、审计) | Kafka / Redpanda | 事件溯源、实时管道 |
| 轻量发布/订阅 | Redis Streams / NATS | 简单通知、广播 |
| 请求-回复(异步同步化) | NATS / RabbitMQ RPC | 内部服务调用 |
### 托管 / 部署
| 模式 | 技术 | 何时使用 |
|-------|-----------|------|
| 无服务器(自动扩缩) | Vercel / Cloudflare Workers / Lambda | 流量波动、按量付费 |
| 容器(可预测) | Cloud Run / Render / Railway / Fly.io | 流量稳定、运维简单 |
| Kubernetes(大规模) | EKS / GKE / AKS | 10+ 服务、团队有 K8s 经验 |
| VPS(完全控制) | DigitalOcean / Hetzner / EC2 | 工作负载可预测、对成本敏感 |
---
## 阶段四:决策文档
### ADR(架构决策记录)模板
```markdown
# ADR-{NNN}: {标题}
## 状态:提议 | 已接受 | 已废弃 | 被 ADR-{NNN} 取代
## 背景
我们在解决什么问题?有哪些因素在起作用?
## 决策
我们选择了什么以及为什么?
## 评估
| 标准 | 权重 | 选中方案 | 备选方案 |
|-----------|--------|--------|-----------|
## 影响
- 正面:...
- 负面:...
- 风险:...
## 已拒绝的备选方案
- 选项 B:因...被拒绝
- 选项 C:因...被拒绝
```
---
## 常见技术栈模板
### A:初创 / MVP(速度优先)
| 层 | 选择 | 理由 |
|-------|--------|-----|
| 语言 | TypeScript | 前后端统一语言 |
| 框架 | Next.js(全栈)或 NestJSAPI | 快速迭代 |
| 数据库 | PostgreSQL (Supabase / Neon) | 托管服务,慷慨的免费额度 |
| 认证 | Better Auth / Clerk | 无需维护认证代码 |
| 缓存 | Redis (Upstash) | 对无服务器友好 |
| 托管 | Vercel / Railway | 零配置部署 |
### BSaaS / 商业应用(平衡)
| 层 | 选择 | 理由 |
|-------|--------|-----|
| 语言 | TypeScript 或 Python | 团队偏好 |
| 框架 | NestJS 或 FastAPI | 结构化、可测试 |
| 数据库 | PostgreSQL | 可靠、功能丰富 |
| 队列 | BullMQ (Redis) | 简单的后台任务 |
| 认证 | OAuth 2.0 + JWT | 标准、灵活 |
| 托管 | AWS ECS / Cloud Run | 可扩缩容器 |
| 监控 | Datadog / Grafana + Prometheus | 完整可观测性 |
### C:高性能(规模优先)
| 层 | 选择 | 理由 |
|-------|--------|-----|
| 语言 | Go 或 Rust | 最大吞吐量、低延迟 |
| 数据库 | PostgreSQL + Redis + ClickHouse | OLTP + 缓存 + 分析 |
| 队列 | Kafka / Redpanda | 高吞吐量流处理 |
| 托管 | Kubernetes (EKS/GKE) | 细粒度扩缩 |
| 监控 | Prometheus + Grafana + Jaeger | 指标 + 追踪 |
### DAI / 机器学习应用
| 层 | 选择 | 理由 |
|-------|--------|-----|
| 语言 | PythonAPI+ TypeScript(前端) | 机器学习库 + 现代 UI |
| 框架 | FastAPI + Next.js | 异步 + SSR |
| 数据库 | PostgreSQL + pgvector | 关系型 + 嵌入 |
| 队列 | Celery + Redis | 机器学习任务处理 |
| 托管 | Modal / AWS GPU / Replicate | GPU 访问 |
---
## 反模式
| # | ❌ 不要做 | ✅ 应该做 |
|---|---------|--------------|
| 1 | "X 在 HN 上火了" | 对照**你自己的需求**来评估 |
| 2 | 简历驱动开发 | 选团队能维护的 |
| 3 | "必须支持 100 万用户"(第一天) | 按当前需求的 10 倍构建,而非 1000 倍 |
| 4 | 评估好几周 | 限制在 3-5 天,然后做决定 |
| 5 | 不做决策文档 | 为每个重大选择写 ADR |
| 6 | 忽略运维成本 | 将部署、监控、调试成本纳入考量 |
| 7 | "以后重写" | 假设不会重写。慎重选择。 |
| 8 | 默认微服务 | 从单体开始,需要时再拆分 |
| 9 | 每个服务一个不同数据库(第一天) | 用一个数据库,有充分理由再拆分 |
| 10 | "谷歌就是这么做的" | 你不是谷歌。按**你自己的场景**来扩缩。 |
---
## 常见问题
### 问题 1:"团队无法就框架达成一致"
**解决方法:** 限制在 3 天内。填写评估矩阵。如果分数相差在 10% 以内,选多数人熟悉的。写入 ADR。继续推进。
### 问题 2:"我们选了 X,但它不合适"
**解决方法:** 检查沉没成本谬误。如果投入不到 2 周,立即切换。如果已超过 2 周,记录痛点并规划分阶段迁移。
### 问题 3"我们需要微服务吗?"
**解决方法:** 几乎肯定不需要。从一个结构良好的单体开始。仅在以下情况才提取为服务:(a) 不同扩缩需求,(b) 不同团队所有权,(c) 不同部署节奏。
+406
View File
@@ -0,0 +1,406 @@
```markdown
# 后端测试策略
面向后端与全栈应用的全面测试指南。涵盖完整的测试金字塔,重点深入 API 集成测试、数据库测试、契约测试与性能测试。
## 快速启动清单
- [ ] **测试运行器已配置**Jest/Vitest、Pytest、Go test
- [ ] **测试数据库**就绪(Docker 容器或内存数据库)
- [ ] **每次测试的数据库隔离**(事务回滚或截断表)
- [ ] **测试工厂**用于常见实体(用户、订单、商品)
- [ ] **认证助手**用于生成测试令牌
- [ ] **CI 流水线**使用真实数据库服务运行测试
- [ ] **覆盖率阈值**已设定(≥ 80%)
---
## 测试金字塔
```
╱╲ E2E(少量、慢速)——跨服务的完整流程
╱────╲ 集成测试(适中)——API + 数据库 + 外部服务
╱────────╲ 单元测试(大量、快速)——纯业务逻辑
__________╲
```
| 层级 | 测试内容 | 速度 | 数量 |
|-------|------|-------|-------|
| 单元测试 | 纯函数、业务逻辑、无 I/O | < 10ms | 占总测试的 70% 以上 |
| 集成测试 | API 路由 + 真实数据库 + 模拟外部服务 | 50-500ms | 约 20% |
| E2E 测试 | 跨已部署服务的完整用户流程 | 1-30s | 约 10% |
| 契约测试 | 服务间的 API 兼容性 | < 100ms | 每个 API 边界 |
| 性能测试 | 负载、压力、浸泡测试 | 数分钟 | 每个关键路径 |
---
## 1. API 集成测试(关键)
### 每个端点需要测试的内容
| 方面 | 需编写的测试 |
|--------|---------------|
| 正常路径 | 正确的输入 → 预期的响应 + 正确的数据库状态 |
| 认证 | 无令牌 → 401,错误令牌 → 401,过期令牌 → 401 |
| 授权 | 错误角色 → 403,非所有者 → 403 |
| 校验 | 缺少字段 → 422,错误类型 → 422,边界值 |
| 未找到 | 无效 ID → 404,已删除资源 → 404 |
| 冲突 | 重复创建 → 409,过期更新 → 409 |
| 幂等性 | 相同请求两次 → 相同结果 |
| 副作用 | 数据库状态变更、事件被触发、缓存失效 |
| 错误格式 | 所有错误均符合 RFC 9457 信封格式 |
### TypeScriptJest + Supertest
```typescript
describe('POST /api/orders', () => {
let token: string;
let product: Product;
beforeAll(async () => {
await resetDatabase();
const user = await createTestUser({ role: 'customer' });
token = await getAuthToken(user);
product = await createTestProduct({ price: 29.99, stock: 10 });
});
it('创建订单 → 201 + 正确的数据库状态', async () => {
const res = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send({ items: [{ productId: product.id, quantity: 2 }] });
expect(res.status).toBe(201);
expect(res.body.data.total).toBe(59.98);
const updated = await db.product.findUnique({ where: { id: product.id } });
expect(updated!.stock).toBe(8);
});
it('无认证时拒绝 → 401', async () => {
const res = await request(app).post('/api/orders').send({ items: [] });
expect(res.status).toBe(401);
});
it('空商品列表时拒绝 → 422', async () => {
const res = await request(app)
.post('/api/orders')
.set('Authorization', `Bearer ${token}`)
.send({ items: [] });
expect(res.status).toBe(422);
expect(res.body.errors[0].field).toBe('items');
});
});
```
### PythonPytest + FastAPI TestClient
```python
@pytest.fixture
def client(db_session):
def override_get_db():
yield db_session
app.dependency_overrides[get_db] = override_get_db
yield TestClient(app)
app.dependency_overrides.clear()
def test_create_order_success(client, auth_headers, test_product):
response = client.post("/api/orders", json={
"items": [{"product_id": test_product.id, "quantity": 2}]
}, headers=auth_headers)
assert response.status_code == 201
assert response.json()["data"]["total"] == 59.98
def test_create_order_no_auth(client):
response = client.post("/api/orders", json={"items": []})
assert response.status_code == 401
def test_create_order_empty_items(client, auth_headers):
response = client.post("/api/orders", json={"items": []}, headers=auth_headers)
assert response.status_code == 422
```
---
## 2. 数据库测试(高优先级)
### 测试隔离策略
| 策略 | 速度 | 真实度 | 使用时机 |
|----------|-------|---------|------|
| **事务回滚** | ⚡ 最快 | 中等 | 单元测试 + 集成测试的默认方式 |
| **截断表** | 快 | 高 | 事务回滚不可用时 |
| **测试容器** | 启动慢 | 最高 | CI 流水线、完整集成测试 |
**事务回滚(推荐默认方式):**
```typescript
let tx: Transaction;
beforeEach(async () => { tx = await db.beginTransaction(); });
afterEach(async () => { await tx.rollback(); });
```
**Docker 测试容器(CI):**
```yaml
# docker-compose.test.yml
services:
test-db:
image: postgres:16-alpine
tmpfs: /var/lib/postgresql/data # RAM 磁盘以提升速度
environment:
POSTGRES_DB: myapp_test
```
### 测试工厂(而非原始 SQL
```typescript
// factories/user.factory.ts
import { faker } from '@faker-js/faker';
export function buildUser(overrides: Partial<User> = {}): CreateUserDTO {
return {
email: faker.internet.email(),
firstName: faker.person.firstName(),
role: 'customer',
...overrides,
};
}
export async function createUser(overrides = {}) {
return db.user.create({ data: buildUser(overrides) });
}
```
```python
# factories/user_factory.py
import factory
from faker import Faker
class UserFactory(factory.Factory):
class Meta:
model = User
email = factory.LazyAttribute(lambda _: Faker().email())
first_name = factory.LazyAttribute(lambda _: Faker().first_name())
role = "customer"
```
---
## 3. 外部服务测试(高优先级)
### HTTP 级别模拟(而非函数级别模拟)
**TypeScriptnock):**
```typescript
import nock from 'nock';
it('成功处理支付', async () => {
nock('https://api.stripe.com')
.post('/v1/charges')
.reply(200, { id: 'ch_123', status: 'succeeded', amount: 5000 });
const result = await paymentService.charge({ amount: 50.00, currency: 'usd' });
expect(result.status).toBe('succeeded');
});
it('处理支付超时', async () => {
nock('https://api.stripe.com').post('/v1/charges').delay(10000).reply(200);
await expect(paymentService.charge({ amount: 50, currency: 'usd' }))
.rejects.toThrow('timeout');
});
```
**Pythonresponses):**
```python
import responses
@responses.activate
def test_payment_success():
responses.post("https://api.stripe.com/v1/charges",
json={"id": "ch_123", "status": "succeeded"}, status=200)
result = payment_service.charge(amount=50.00, currency="usd")
assert result.status == "succeeded"
```
### 用于基础设施的测试容器
```typescript
import { PostgreSqlContainer } from '@testcontainers/postgresql';
import { RedisContainer } from '@testcontainers/redis';
beforeAll(async () => {
const pg = await new PostgreSqlContainer('postgres:16').start();
process.env.DATABASE_URL = pg.getConnectionUri();
await runMigrations();
}, 60000);
```
---
## 4. 契约测试(中高优先级)
### 消费者驱动的契约(Pact
**消费者(OrderService 调用 UserService):**
```typescript
it('可以根据 ID 获取用户', async () => {
await pact.addInteraction()
.given('用户 usr_123 存在')
.uponReceiving('GET /users/usr_123')
.withRequest('GET', '/api/users/usr_123')
.willRespondWith(200, (b) => {
b.jsonBody({ data: { id: MatchersV3.string(), email: MatchersV3.email() } });
})
.executeTest(async (mockserver) => {
const user = await new UserClient(mockserver.url).getUser('usr_123');
expect(user.id).toBeDefined();
});
});
```
**提供者在 CI 中验证:**
```typescript
await new Verifier({
providerBaseUrl: 'http://localhost:3001',
pactBrokerUrl: process.env.PACT_BROKER_URL,
provider: 'UserService',
}).verifyProvider();
```
---
## 5. 性能测试(中优先级)
### k6 负载测试
```javascript
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '30s', target: 20 }, // 逐步增加
{ duration: '1m', target: 100 }, // 持续维持
{ duration: '30s', target: 0 }, // 逐步减少
],
thresholds: {
http_req_duration: ['p(95)<500', 'p(99)<1000'],
http_req_failed: ['rate<0.01'],
},
};
export default function () {
const res = http.get(`${__ENV.BASE_URL}/api/orders`);
check(res, { 'status 200': (r) => r.status === 200 });
sleep(1);
}
```
### 性能预算
| 指标 | 目标值 | 超出后措施 |
|--------|--------|--------------------|
| p95 响应时间 | < 500ms | 优化查询/缓存 |
| p99 响应时间 | < 1000ms | 检查异常查询 |
| 错误率 | < 0.1% | 排查异常尖峰 |
| 数据库查询时间 | 每次 < 100ms | 添加索引 |
### 何时运行
| 触发时机 | 测试类型 |
|---------|-----------|
| 重大版本发布前 | 完整负载测试 |
| 新增数据库查询/索引 | 查询基准测试 |
| 基础设施变更 | 基线对比 |
| 每周(CI) | 冒烟负载测试 |
---
## 测试文件组织
```
tests/
unit/ # 纯逻辑,模拟依赖
order.service.test.ts
integration/ # API + 真实数据库
orders.api.test.ts
auth.api.test.ts
contracts/ # 消费者驱动的契约
user-service.consumer.pact.ts
performance/ # 负载测试
load-test.js
fixtures/
factories/ # 测试数据工厂
user.factory.ts
seeds/
test-data.ts
helpers/
setup.ts # 全局测试配置
auth.helper.ts # 令牌生成
db.helper.ts # 数据库清理
```
---
## 反模式
| # | ❌ 不要这样做 | ✅ 应该这样做 |
|---|---------|--------------|
| 1 | 仅测试正常路径 | 测试错误、认证、校验、边界情况 |
| 2 | 什么都模拟(无真实数据库) | 使用测试容器或测试数据库 |
| 3 | 测试依赖执行顺序 | 每个测试自行设置/清理自身状态 |
| 4 | 硬编码测试数据 | 使用工厂(faker + 覆盖参数) |
| 5 | 测试实现细节 | 测试行为:输入 → 输出 |
| 6 | 共享可变状态 | 每次测试隔离(事务回滚) |
| 7 | 在 CI 中跳过迁移测试 | 在 CI 中从头运行迁移 |
| 8 | 发布前不做性能测试 | 每个重大版本都做负载测试 |
| 9 | 对生产数据做测试 | 仅使用生成的测试数据 |
| 10 | 测试套件超过 10 分钟 | 并行化、使用 RAM 磁盘、优化设置 |
---
## 常见问题
### 问题 1:「测试单独通过,一起运行时失败」
**原因:** 测试之间共享了数据库状态。缺少清理操作。
**修复方法:**
```typescript
beforeEach(async () => { await db.raw('TRUNCATE orders, users CASCADE'); });
// 或者每次测试使用事务回滚
```
### 问题 2:「测试运行结束后 Jest 没有在一秒内退出」
**原因:** 数据库连接或 HTTP 服务器未关闭。
**修复方法:**
```typescript
afterAll(async () => {
await db.destroy();
await server.close();
});
```
### 问题 3:「异步回调未在超时时间内被调用」
**原因:** 缺少 `async/await` 或未处理的 promise。
**修复方法:**
```typescript
// ❌ Promise 未被等待
it('should work', () => { request(app).get('/users'); });
// ✅ 正确等待
it('should work', async () => { await request(app).get('/users'); });
```
### 问题 4:「集成测试在 CI 中太慢」
**修复方法:**
1. 对 PostgreSQL 数据目录使用 `tmpfs`RAM 磁盘)
2.`beforeAll` 中运行一次迁移,在 `beforeEach` 中截断表
3. 使用 `--maxWorkers` 并行化测试套件
4. 特性分支跳过性能测试(仅在主分支运行)
```