feat(api): add API versioning and deprecation strategy
- Implement API version management middleware - Add version lifecycle management (dev/stable/maintenance/deprecated/sunset) - Add deprecation warning headers (X-API-Deprecated, X-API-Sunset-Date) - Add version tracking headers (X-API-Version) - Handle sunset versions with 410 Gone response - Comprehensive API versioning documentation - Include migration guide and best practices - Support gradual version rollout Phase 4 Task 44/68 completed
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
"""
|
||||
API 版本管理中间件
|
||||
"""
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class APIVersionMiddleware(BaseHTTPMiddleware):
|
||||
"""API 版本管理中间件"""
|
||||
|
||||
# 版本配置
|
||||
VERSIONS = {
|
||||
"v1": {
|
||||
"status": "stable",
|
||||
"deprecated": False,
|
||||
"sunset_date": None,
|
||||
"release_date": "2026-06-17",
|
||||
},
|
||||
"v2": {
|
||||
"status": "development",
|
||||
"deprecated": False,
|
||||
"sunset_date": None,
|
||||
"release_date": None,
|
||||
},
|
||||
}
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# 提取版本号
|
||||
version = self._extract_version(request.url.path)
|
||||
|
||||
# 处理请求
|
||||
response = await call_next(request)
|
||||
|
||||
# 添加版本信息头
|
||||
if version:
|
||||
response.headers["X-API-Version"] = version
|
||||
|
||||
# 添加弃用警告
|
||||
version_info = self.VERSIONS.get(version, {})
|
||||
if version_info.get("deprecated"):
|
||||
response.headers["X-API-Deprecated"] = "true"
|
||||
|
||||
sunset_date = version_info.get("sunset_date")
|
||||
if sunset_date:
|
||||
response.headers["X-API-Sunset-Date"] = sunset_date
|
||||
|
||||
response.headers["X-API-Deprecation-Info"] = (
|
||||
f"https://docs.xiaoxia-saas.com/api/deprecation/{version}"
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def _extract_version(self, path: str) -> str:
|
||||
"""从路径中提取版本号"""
|
||||
parts = path.split("/")
|
||||
for part in parts:
|
||||
if part.startswith("v") and part[1:].isdigit():
|
||||
return part
|
||||
return None
|
||||
|
||||
|
||||
class VersionNotFoundMiddleware(BaseHTTPMiddleware):
|
||||
"""处理已下线的 API 版本"""
|
||||
|
||||
SUNSET_VERSIONS = [] # 已下线的版本列表
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
version = self._extract_version(request.url.path)
|
||||
|
||||
if version in self.SUNSET_VERSIONS:
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse(
|
||||
status_code=410,
|
||||
content={
|
||||
"error": {
|
||||
"code": "API_VERSION_SUNSET",
|
||||
"message": f"API {version} has been sunset and is no longer available",
|
||||
"sunset_date": "2028-07-01",
|
||||
"migration_guide": f"https://docs.xiaoxia-saas.com/api/migration/{version}"
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
def _extract_version(self, path: str) -> str:
|
||||
"""从路径中提取版本号"""
|
||||
parts = path.split("/")
|
||||
for part in parts:
|
||||
if part.startswith("v") and part[1:].isdigit():
|
||||
return part
|
||||
return None
|
||||
@@ -0,0 +1,303 @@
|
||||
# API 版本管理指南
|
||||
|
||||
## 📋 概述
|
||||
|
||||
小虾 SaaS 使用 URL 路径版本管理策略,确保 API 的向后兼容性。
|
||||
|
||||
---
|
||||
|
||||
## 🔢 版本命名规则
|
||||
|
||||
### 当前版本:v1
|
||||
|
||||
```
|
||||
/api/v1/auth/login
|
||||
/api/v1/workspaces
|
||||
/api/v1/projects
|
||||
```
|
||||
|
||||
### 版本号规则
|
||||
|
||||
- **v1, v2, v3...** - 主版本号
|
||||
- 破坏性变更才升级主版本
|
||||
- 非破坏性变更在当前版本迭代
|
||||
|
||||
---
|
||||
|
||||
## 📝 版本策略
|
||||
|
||||
### 什么时候升级版本?
|
||||
|
||||
**需要升级(破坏性变更):**
|
||||
- ✅ 修改响应结构
|
||||
- ✅ 删除字段
|
||||
- ✅ 修改字段类型
|
||||
- ✅ 修改认证方式
|
||||
- ✅ 修改 HTTP 方法
|
||||
|
||||
**不需要升级(兼容性变更):**
|
||||
- ✅ 添加新字段
|
||||
- ✅ 添加新端点
|
||||
- ✅ 添加可选参数
|
||||
- ✅ 优化性能
|
||||
- ✅ 修复 Bug
|
||||
|
||||
---
|
||||
|
||||
## 🔄 版本生命周期
|
||||
|
||||
### 阶段 1: 开发中(Development)
|
||||
- 版本:`v2-beta`
|
||||
- 状态:不稳定,可能变更
|
||||
- 使用:仅内部测试
|
||||
|
||||
### 阶段 2: 稳定版(Stable)
|
||||
- 版本:`v2`
|
||||
- 状态:稳定,推荐使用
|
||||
- 支持:完整支持,持续更新
|
||||
|
||||
### 阶段 3: 维护中(Maintenance)
|
||||
- 版本:`v1`(当 v2 发布后)
|
||||
- 状态:只修复安全问题和严重 Bug
|
||||
- 支持:12 个月维护期
|
||||
|
||||
### 阶段 4: 已弃用(Deprecated)
|
||||
- 版本:`v1`(维护期结束后)
|
||||
- 状态:仍可访问,显示弃用警告
|
||||
- 支持:6 个月弃用期
|
||||
|
||||
### 阶段 5: 下线(Sunset)
|
||||
- 版本:`v1`(弃用期结束后)
|
||||
- 状态:返回 410 Gone
|
||||
- 支持:无
|
||||
|
||||
---
|
||||
|
||||
## 📅 版本时间线示例
|
||||
|
||||
```
|
||||
2026-06-01: v1 发布 (Stable)
|
||||
2027-01-01: v2 发布 (Stable)
|
||||
2027-01-01: v1 进入维护期 (Maintenance, 12个月)
|
||||
2028-01-01: v1 进入弃用期 (Deprecated, 6个月)
|
||||
2028-07-01: v1 下线 (Sunset)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 弃用通知
|
||||
|
||||
### 响应头
|
||||
|
||||
```http
|
||||
HTTP/1.1 200 OK
|
||||
X-API-Version: v1
|
||||
X-API-Deprecated: true
|
||||
X-API-Sunset-Date: 2028-07-01
|
||||
X-API-Deprecation-Info: https://docs.xiaoxia-saas.com/api/deprecation/v1
|
||||
```
|
||||
|
||||
### 响应体(可选)
|
||||
|
||||
```json
|
||||
{
|
||||
"data": { ... },
|
||||
"_meta": {
|
||||
"deprecated": true,
|
||||
"sunset_date": "2028-07-01",
|
||||
"migration_guide": "https://docs.xiaoxia-saas.com/api/migration/v1-to-v2"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 客户端处理
|
||||
|
||||
### 检测弃用
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
response = requests.get("https://api.xiaoxia-saas.com/api/v1/workspaces")
|
||||
|
||||
if response.headers.get("X-API-Deprecated") == "true":
|
||||
sunset_date = response.headers.get("X-API-Sunset-Date")
|
||||
print(f"警告: API v1 将在 {sunset_date} 下线")
|
||||
```
|
||||
|
||||
### 自动迁移
|
||||
|
||||
```python
|
||||
class APIClient:
|
||||
def __init__(self, version="v2"):
|
||||
self.version = version
|
||||
self.base_url = f"https://api.xiaoxia-saas.com/api/{version}"
|
||||
|
||||
def fallback_to_v1(self, endpoint):
|
||||
"""自动降级到 v1"""
|
||||
try:
|
||||
return self.request(endpoint)
|
||||
except APINotFoundError:
|
||||
# v2 不存在,尝试 v1
|
||||
self.version = "v1"
|
||||
return self.request(endpoint)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📚 迁移指南
|
||||
|
||||
### v1 → v2 迁移示例
|
||||
|
||||
**变更 1: 登录响应结构**
|
||||
|
||||
v1:
|
||||
```json
|
||||
{
|
||||
"token": "abc123",
|
||||
"user_id": "user-123"
|
||||
}
|
||||
```
|
||||
|
||||
v2:
|
||||
```json
|
||||
{
|
||||
"access_token": "abc123",
|
||||
"refresh_token": "def456",
|
||||
"expires_in": 1800,
|
||||
"user": {
|
||||
"id": "user-123",
|
||||
"email": "user@example.com"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**迁移代码:**
|
||||
```python
|
||||
# v1
|
||||
token = response.json()["token"]
|
||||
|
||||
# v2
|
||||
token = response.json()["access_token"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 实现方式
|
||||
|
||||
### FastAPI 版本路由
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter
|
||||
|
||||
# v1 路由
|
||||
api_v1 = APIRouter(prefix="/api/v1")
|
||||
|
||||
@api_v1.get("/workspaces")
|
||||
async def list_workspaces_v1():
|
||||
return {"workspaces": []}
|
||||
|
||||
# v2 路由
|
||||
api_v2 = APIRouter(prefix="/api/v2")
|
||||
|
||||
@api_v2.get("/workspaces")
|
||||
async def list_workspaces_v2():
|
||||
return {
|
||||
"data": [],
|
||||
"pagination": {"page": 1, "total": 0}
|
||||
}
|
||||
|
||||
# 注册到主应用
|
||||
app.include_router(api_v1)
|
||||
app.include_router(api_v2)
|
||||
```
|
||||
|
||||
### 弃用中间件
|
||||
|
||||
```python
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
|
||||
class DeprecationMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
response = await call_next(request)
|
||||
|
||||
if request.url.path.startswith("/api/v1"):
|
||||
response.headers["X-API-Deprecated"] = "true"
|
||||
response.headers["X-API-Sunset-Date"] = "2028-07-01"
|
||||
|
||||
return response
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 版本使用统计
|
||||
|
||||
### 监控指标
|
||||
|
||||
```python
|
||||
from prometheus_client import Counter
|
||||
|
||||
api_version_counter = Counter(
|
||||
'api_requests_by_version',
|
||||
'API requests by version',
|
||||
['version']
|
||||
)
|
||||
|
||||
@app.middleware("http")
|
||||
async def track_version(request: Request, call_next):
|
||||
version = extract_version(request.url.path)
|
||||
api_version_counter.labels(version=version).inc()
|
||||
return await call_next(request)
|
||||
```
|
||||
|
||||
### 查看统计
|
||||
|
||||
```bash
|
||||
# v1 使用量
|
||||
curl http://localhost:8000/metrics | grep api_requests_by_version{version="v1"}
|
||||
|
||||
# v2 使用量
|
||||
curl http://localhost:8000/metrics | grep api_requests_by_version{version="v2"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 最佳实践
|
||||
|
||||
### 1. 提前通知
|
||||
|
||||
- 至少提前 6 个月通知弃用
|
||||
- 通过邮件、公告、响应头多渠道通知
|
||||
- 提供详细的迁移指南
|
||||
|
||||
### 2. 保持兼容
|
||||
|
||||
- 尽可能添加而不是修改
|
||||
- 使用可选参数而不是必填
|
||||
- 保留旧字段,添加新字段
|
||||
|
||||
### 3. 渐进式迁移
|
||||
|
||||
```
|
||||
v1 (100%) → v1 (80%) + v2 (20%) → v1 (20%) + v2 (80%) → v2 (100%)
|
||||
```
|
||||
|
||||
### 4. 文档优先
|
||||
|
||||
- 版本变更先更新文档
|
||||
- 提供完整的迁移指南
|
||||
- 包含代码示例
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关资源
|
||||
|
||||
- [语义化版本规范](https://semver.org/)
|
||||
- [API 版本管理最佳实践](https://restfulapi.net/versioning/)
|
||||
- [Stripe API 版本管理](https://stripe.com/docs/api/versioning)
|
||||
|
||||
---
|
||||
|
||||
**最后更新:** 2026-06-17
|
||||
Reference in New Issue
Block a user