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:
Xiaoxia AI
2026-06-17 08:41:11 +08:00
parent 81aaef4fb9
commit c23922faab
2 changed files with 396 additions and 0 deletions
+93
View File
@@ -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
+303
View File
@@ -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