# API 设计规范 > **文档版本**:v1.0 > **最后更新**:2025-01-27 --- ## 目录 1. [API 设计原则](#api-设计原则) 2. [路由结构](#路由结构) 3. [认证授权](#认证授权) 4. [请求响应格式](#请求响应格式) 5. [错误处理](#错误处理) 6. [API 版本管理](#api-版本管理) --- ## API 设计原则 ### 1. RESTful 风格 遵循 REST 架构风格,使用标准 HTTP 方法: | 方法 | 用途 | 示例 | | ------ | ------------ | ------------------------------ | | GET | 获取资源 | `GET /api/v1/projects` | | POST | 创建资源 | `POST /api/v1/projects` | | PUT | 完整更新资源 | `PUT /api/v1/projects/{id}` | | PATCH | 部分更新资源 | `PATCH /api/v1/projects/{id}` | | DELETE | 删除资源 | `DELETE /api/v1/projects/{id}` | ### 2. 资源命名 - 使用复数名词:`/projects` 而非 `/project` - 使用小写字母和连字符:`/project-members` 而非 `/projectMembers` - 避免动词:`POST /projects` 而非 `/createProject` ### 3. 嵌套资源 ``` GET /api/v1/projects/{project_id}/storyboards POST /api/v1/projects/{project_id}/storyboards GET /api/v1/projects/{project_id}/storyboards/{storyboard_id} PUT /api/v1/projects/{project_id}/storyboards/{storyboard_id} DELETE /api/v1/projects/{project_id}/storyboards/{storyboard_id} ``` --- ## 路由结构 ### 完整 API 路由表 ``` /api/v1 ├── /auth │ ├── POST /register # 用户注册 │ ├── POST /login # 用户登录 │ ├── POST /logout # 用户登出 │ └── POST /refresh # 刷新 Token │ ├── /folders │ ├── GET / # 获取文件夹列表 │ ├── POST / # 创建文件夹 │ ├── GET /{id} # 获取文件夹详情 │ ├── PUT /{id} # 更新文件夹 │ ├── DELETE /{id} # 删除文件夹 │ ├── POST /{id}/move # 移动文件夹 │ └── GET /tree # 获取文件夹树 │ ├── /projects │ ├── GET / # 获取项目列表 │ ├── POST / # 创建项目 │ ├── GET /{id} # 获取项目详情 │ ├── PUT /{id} # 更新项目 │ ├── DELETE /{id} # 删除项目 │ ├── POST /{id}/move # 移动项目到文件夹 │ └── GET /{id}/members # 获取项目成员 │ ├── /scripts │ ├── GET / # 获取剧本列表 │ ├── POST / # 创建剧本 │ ├── GET /{id} # 获取剧本详情 │ ├── PUT /{id} # 更新剧本 │ ├── DELETE /{id} # 删除剧本 │ ├── POST /{id}/approve # 审批剧本 │ ├── GET /{id}/versions # 获取版本历史 │ ├── POST /{id}/characters # 添加角色 │ └── POST /{id}/scenes # 添加场景 │ ├── /attachments │ ├── POST / # 上传附件 │ ├── GET /{id} # 获取附件详情 │ ├── GET /{id}/download # 获取下载链接 │ └── DELETE /{id} # 删除附件 │ ├── /storyboards │ ├── GET / # 获取分镜列表 │ ├── POST / # 创建分镜 │ ├── GET /{id} # 获取分镜详情 │ ├── PUT /{id} # 更新分镜 │ └── DELETE /{id} # 删除分镜 │ ├── /resources │ ├── GET / # 获取资源列表 │ ├── POST /upload # 上传资源 │ ├── GET /{id} # 获取资源详情 │ └── DELETE /{id} # 删除资源 │ ├── /videos │ ├── GET / # 获取视频列表 │ ├── POST / # 创建视频 │ ├── GET /{id} # 获取视频详情 │ └── DELETE /{id} # 删除视频 │ ├── /ai │ ├── POST /generate-image # 生成图片 │ ├── POST /generate-video # 生成视频 │ ├── POST /generate-sound # 生成音效 │ ├── POST /generate-voice # 生成配音 │ └── GET /jobs/{id} # 查询任务状态 │ └── /export ├── POST / # 导出项目 └── GET /jobs/{id} # 查询导出状态 ``` --- ## 认证授权 ### JWT Token 认证 **流程**: ``` 1. 用户登录 → 服务器返回 access_token 和 refresh_token 2. 客户端在请求头中携带 access_token 3. 服务器验证 token,返回数据 4. token 过期后,使用 refresh_token 刷新 ``` **实现代码**: ```python # app/core/security.py from datetime import datetime, timedelta from typing import Optional from jose import JWTError, jwt from passlib.context import CryptContext from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer SECRET_KEY = "your-secret-key" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): """创建访问令牌""" to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=15) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) return encoded_jwt async def get_current_user(token: str = Depends(oauth2_scheme)): """获取当前用户""" credentials_exception = HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) try: payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) user_id: str = payload.get("sub") if user_id is None: raise credentials_exception except JWTError: raise credentials_exception # 从数据库获取用户 user = get_user(user_id) if user is None: raise credentials_exception return user ``` ### 权限控制 基于角色的访问控制(RBAC): ```python # app/core/security.py from enum import Enum class Role(str, Enum): OWNER = "owner" EDITOR = "editor" VIEWER = "viewer" def require_role(required_role: Role): """权限检查装饰器""" async def role_checker( current_user = Depends(get_current_user), project_id: str = None ): # 检查用户在项目中的角色 user_role = get_user_role(current_user.id, project_id) role_hierarchy = { Role.OWNER: 3, Role.EDITOR: 2, Role.VIEWER: 1 } if role_hierarchy[user_role] < role_hierarchy[required_role]: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions" ) return current_user return role_checker ``` **使用示例**: ```python @router.delete("/{project_id}") async def delete_project( project_id: str, current_user = Depends(require_role(Role.OWNER)) ): """删除项目(仅 owner 可操作)""" pass ``` --- ## 请求响应格式 ### 请求格式 **查询参数**(GET 请求): ``` GET /api/v1/projects?type=mine&page=1&page_size=20 ``` **请求体**(POST/PUT 请求): ```json { "name": "我的项目", "description": "项目描述", "type": "mine", "folder_id": "folder-123" } ``` ### 响应格式 **成功响应**: ```json { "id": "project-123", "name": "我的项目", "description": "项目描述", "type": "mine", "owner_id": "user-456", "created_at": "2025-01-27T10:00:00Z", "updated_at": "2025-01-27T10:00:00Z" } ``` **列表响应**(带分页): ```json { "items": [ { "id": "project-123", "name": "项目1" }, { "id": "project-456", "name": "项目2" } ], "total": 100, "page": 1, "page_size": 20, "total_pages": 5 } ``` --- ## 错误处理 ### 标准错误响应 ```json { "error": { "code": "RESOURCE_NOT_FOUND", "message": "项目不存在", "details": { "project_id": "project-123" } } } ``` ### HTTP 状态码 | 状态码 | 说明 | 使用场景 | | ------ | --------------------- | ---------------------- | | 200 | OK | 请求成功 | | 201 | Created | 资源创建成功 | | 204 | No Content | 删除成功(无返回内容) | | 400 | Bad Request | 请求参数错误 | | 401 | Unauthorized | 未认证 | | 403 | Forbidden | 无权限 | | 404 | Not Found | 资源不存在 | | 409 | Conflict | 资源冲突 | | 422 | Unprocessable Entity | 数据验证失败 | | 500 | Internal Server Error | 服务器错误 | ### 错误码定义 ```python # app/core/error_codes.py class ErrorCode: # 通用错误 INTERNAL_ERROR = "INTERNAL_ERROR" INVALID_REQUEST = "INVALID_REQUEST" # 认证错误 UNAUTHORIZED = "UNAUTHORIZED" INVALID_TOKEN = "INVALID_TOKEN" TOKEN_EXPIRED = "TOKEN_EXPIRED" # 权限错误 FORBIDDEN = "FORBIDDEN" INSUFFICIENT_PERMISSIONS = "INSUFFICIENT_PERMISSIONS" # 资源错误 RESOURCE_NOT_FOUND = "RESOURCE_NOT_FOUND" RESOURCE_ALREADY_EXISTS = "RESOURCE_ALREADY_EXISTS" # 业务错误 INVALID_PROJECT_TYPE = "INVALID_PROJECT_TYPE" FOLDER_CYCLE_DETECTED = "FOLDER_CYCLE_DETECTED" ``` --- ## API 版本管理 ### URL 版本控制 ``` /api/v1/projects # 版本 1 /api/v2/projects # 版本 2 ``` ### 版本兼容性 - **向后兼容**:新版本应尽量保持向后兼容 - **废弃通知**:在响应头中添加废弃警告 ```python @router.get("/projects") async def get_projects(response: Response): response.headers["X-API-Deprecated"] = "This endpoint will be removed in v2" return projects ``` ### 版本迁移 在文档中明确说明版本差异: ```markdown ## v1 → v2 迁移指南 ### 变更内容 - `type` 字段重命名为 `project_type` - 新增 `folder_id` 字段 - 移除 `thumbnail_url` 字段(改用 `/attachments` 接口) ### 迁移步骤 1. 更新客户端代码,使用新字段名 2. 调用 `/api/v2/projects/migrate` 迁移数据 3. 切换到 v2 API ``` --- ## API 文档 ### 自动生成文档 FastAPI 自动生成 OpenAPI 文档: - **Swagger UI**:`http://localhost:8000/api/docs` - **ReDoc**:`http://localhost:8000/api/redoc` ### 文档配置 ```python # app/main.py from fastapi import FastAPI app = FastAPI( title="Jointo API", description="AI 驱动的视频创作平台", version="1.0.0", docs_url="/api/docs", redoc_url="/api/redoc", openapi_url="/api/openapi.json" ) ``` --- ## 相关文档 - [系统架构设计](./03-system-design.md) - [数据库设计](./04-database-design.md) - [核心服务设计](./04-services/) --- **文档版本**:v1.0 **最后更新**:2025-01-27