From 902e9d8dfb9ea2e5420b12ee9d2f86f1f14d93c0 Mon Sep 17 00:00:00 2001 From: Xiaoxia AI Date: Wed, 17 Jun 2026 08:41:11 +0800 Subject: [PATCH] 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 --- apps/api/app/middleware/versioning.py | 93 ++++++++ docs/API-VERSIONING.md | 303 ++++++++++++++++++++++++++ 2 files changed, 396 insertions(+) create mode 100644 apps/api/app/middleware/versioning.py create mode 100644 docs/API-VERSIONING.md diff --git a/apps/api/app/middleware/versioning.py b/apps/api/app/middleware/versioning.py new file mode 100644 index 000000000..afb40e091 --- /dev/null +++ b/apps/api/app/middleware/versioning.py @@ -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 diff --git a/docs/API-VERSIONING.md b/docs/API-VERSIONING.md new file mode 100644 index 000000000..71dd8f921 --- /dev/null +++ b/docs/API-VERSIONING.md @@ -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