|
|
@@ -0,0 +1,911 @@
|
|
|
+# 标注平台对外 API 接口设计文档
|
|
|
+
|
|
|
+> 基于《标注平台提供API接口需求文档》编写
|
|
|
+> 版本:v1.0
|
|
|
+> 日期:2026-05-17
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 目录
|
|
|
+
|
|
|
+- [1. 概述](#1-概述)
|
|
|
+- [2. 架构设计](#2-架构设计)
|
|
|
+- [3. 认证与授权](#3-认证与授权)
|
|
|
+- [4. 接口列表](#4-接口列表)
|
|
|
+- [5. 接口详细设计](#5-接口详细设计)
|
|
|
+- [6. 数据模型](#6-数据模型)
|
|
|
+- [7. 错误码定义](#7-错误码定义)
|
|
|
+- [8. 文件组织结构](#8-文件组织结构)
|
|
|
+- [9. 安全设计](#9-安全设计)
|
|
|
+- [10. 调用时序图](#10-调用时序图)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 概述
|
|
|
+
|
|
|
+### 1.1 背景
|
|
|
+
|
|
|
+标注平台提供标准化的 RESTful API 接口,供**模型训练平台**(外部系统)调用,实现以下能力:
|
|
|
+
|
|
|
+1. 查询标注项目列表
|
|
|
+2. 按项目 ID 和数据集格式下载标注数据集
|
|
|
+3. 通过 API Key + Secret 签名机制完成认证与授权
|
|
|
+
|
|
|
+### 1.2 设计原则
|
|
|
+
|
|
|
+- **与现有代码风格一致**:复用项目已有的 FastAPI + Pydantic + PyMySQL 技术栈
|
|
|
+- **独立路由前缀**:所有对外 API 使用 `/api/v1/open` 前缀,与内部 API(`/api/projects` 等)区分
|
|
|
+- **独立认证机制**:不依赖现有 SSO/JWT 体系,采用 API Key + Secret 签名
|
|
|
+- **数据格式统一**:遵循项目已有的响应包装模式
|
|
|
+
|
|
|
+### 1.3 业务流程
|
|
|
+
|
|
|
+```
|
|
|
+模型训练平台 标注平台
|
|
|
+ | |
|
|
|
+ | 1. 获取 Access Token |
|
|
|
+ |------------------------------>|
|
|
|
+ | |
|
|
|
+ | 2. 查询项目列表 |
|
|
|
+ |------------------------------>|
|
|
|
+ | |
|
|
|
+ | 3. 选择项目,下载数据集 |
|
|
|
+ |------------------------------>|
|
|
|
+ | |
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 2. 架构设计
|
|
|
+
|
|
|
+### 2.1 技术栈
|
|
|
+
|
|
|
+| 组件 | 技术 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| Web 框架 | FastAPI 0.109.0 | 与项目保持一致 |
|
|
|
+| 数据校验 | Pydantic 2.5.3 | 请求/响应模型 |
|
|
|
+| 数据库 | MySQL (PyMySQL 1.1.0) | 项目已有数据库 |
|
|
|
+| 签名算法 | HMAC-SHA256 | API Key + Secret 签名 |
|
|
|
+| Token 格式 | JWT (PyJWT 2.8.0) | Access Token |
|
|
|
+
|
|
|
+### 2.2 认证流程设计
|
|
|
+
|
|
|
+```
|
|
|
+┌─────────────────────────────────────────────────────────┐
|
|
|
+│ 认证与授权流程 │
|
|
|
+│ │
|
|
|
+│ 模型训练平台 标注平台 │
|
|
|
+│ │ │ │
|
|
|
+│ │ POST /api/v1/open/auth/token │ │
|
|
|
+│ │ Header: X-Api-Key: {app_id} │ │
|
|
|
+│ │ Header: X-Signature: {hmac_sig} │ │
|
|
|
+│ │ Header: X-Timestamp: {timestamp} │ │
|
|
|
+│ │ Header: X-Nonce: {nonce} │ │
|
|
|
+│ │------------------------------------>│ │
|
|
|
+│ │ │ │
|
|
|
+│ │ 验证 app_id │ │
|
|
|
+│ │ 验证 timestamp │ │
|
|
|
+│ │ 验证 signature │ │
|
|
|
+│ │ 生成 JWT Token │ │
|
|
|
+│ │ │ │
|
|
|
+│ │ 200 { access_token, expires_in } │ │
|
|
|
+│ │<------------------------------------│ │
|
|
|
+│ │ │ │
|
|
|
+│ │ 后续请求携带: │ │
|
|
|
+│ │ Authorization: Bearer {token} │ │
|
|
|
+│ │------------------------------------>│ │
|
|
|
+└─────────────────────────────────────────────────────────┘
|
|
|
+```
|
|
|
+
|
|
|
+### 2.3 签名算法
|
|
|
+
|
|
|
+```
|
|
|
+signature = HMAC-SHA256(
|
|
|
+ key = app_secret,
|
|
|
+ message = app_id + timestamp + nonce
|
|
|
+)
|
|
|
+```
|
|
|
+
|
|
|
+- `timestamp`:当前 Unix 时间戳(秒),有效期 ±5 分钟
|
|
|
+- `nonce`:随机字符串(16 位),防止重放攻击
|
|
|
+- `signature`:十六进制编码的 HMAC-SHA256 签名
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 3. 认证与授权
|
|
|
+
|
|
|
+### 3.1 凭证管理
|
|
|
+
|
|
|
+#### 3.1.1 凭证分配
|
|
|
+
|
|
|
+每个接入系统在标注平台注册后,分配一对凭证:
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `app_id` | string | 应用唯一标识,注册时生成 |
|
|
|
+| `app_secret` | string | 应用密钥,仅初始化时展示一次,需妥善保存 |
|
|
|
+
|
|
|
+#### 3.1.2 数据库设计
|
|
|
+
|
|
|
+新增 `api_applications` 表:
|
|
|
+
|
|
|
+```sql
|
|
|
+CREATE TABLE api_applications (
|
|
|
+ id VARCHAR(36) PRIMARY KEY,
|
|
|
+ app_id VARCHAR(64) NOT NULL UNIQUE,
|
|
|
+ app_name VARCHAR(128) NOT NULL COMMENT '应用名称',
|
|
|
+ app_secret VARCHAR(128) NOT NULL COMMENT '应用密钥(bcrypt 哈希存储)',
|
|
|
+ secret_plain VARCHAR(128) DEFAULT NULL COMMENT '明文密钥(仅在创建时返回,后续不存储)',
|
|
|
+ status VARCHAR(20) NOT NULL DEFAULT 'active' COMMENT '状态: active/disabled',
|
|
|
+ description TEXT DEFAULT NULL COMMENT '应用描述',
|
|
|
+ created_by VARCHAR(36) DEFAULT NULL COMMENT '创建人用户ID',
|
|
|
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
|
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
|
|
+ last_used_at TIMESTAMP NULL COMMENT '最后一次使用时间',
|
|
|
+ INDEX idx_app_id (app_id),
|
|
|
+ INDEX idx_status (status)
|
|
|
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='API 接入应用管理';
|
|
|
+```
|
|
|
+
|
|
|
+#### 3.1.3 Token 设计
|
|
|
+
|
|
|
+| 字段 | 说明 |
|
|
|
+|------|------|
|
|
|
+| `access_token` | JWT 格式,有效期 2 小时 |
|
|
|
+| `token_type` | 固定值 `Bearer` |
|
|
|
+| `expires_in` | 过期时间(秒),默认 7200 |
|
|
|
+
|
|
|
+JWT Payload:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "sub": "app_id_xxxx",
|
|
|
+ "app_id": "app_id_xxxx",
|
|
|
+ "app_name": "模型训练平台",
|
|
|
+ "iat": 1716000000,
|
|
|
+ "exp": 1716007200,
|
|
|
+ "type": "open_api_access"
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 4. 接口列表
|
|
|
+
|
|
|
+所有对外 API 统一前缀:`/api/v1/open`
|
|
|
+
|
|
|
+| 序号 | 方法 | 路径 | 说明 | 认证 |
|
|
|
+|------|------|------|------|------|
|
|
|
+| 1 | POST | `/api/v1/open/auth/token` | 获取 Access Token | API Key 签名 |
|
|
|
+| 2 | POST | `/api/v1/open/auth/refresh` | 刷新 Access Token | Bearer Token |
|
|
|
+| 3 | GET | `/api/v1/open/projects` | 查询项目列表 | Bearer Token |
|
|
|
+| 4 | GET | `/api/v1/open/projects/{project_id}` | 查询项目详情 | Bearer Token |
|
|
|
+| 5 | POST | `/api/v1/open/projects/{project_id}/datasets/download` | 数据集下载 | Bearer Token |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 5. 接口详细设计
|
|
|
+
|
|
|
+### 5.1 获取 Access Token
|
|
|
+
|
|
|
+**接口路由**:`POST /api/v1/open/auth/token`
|
|
|
+
|
|
|
+**功能说明**:通过 API Key + Secret 签名获取访问令牌
|
|
|
+
|
|
|
+**请求头**:
|
|
|
+
|
|
|
+| Header | 类型 | 必录 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| `X-Api-Key` | string | 是 | 应用 app_id |
|
|
|
+| `X-Signature` | string | 是 | HMAC-SHA256 签名值(十六进制) |
|
|
|
+| `X-Timestamp` | string | 是 | Unix 时间戳(秒) |
|
|
|
+| `X-Nonce` | string | 是 | 16 位随机字符串 |
|
|
|
+
|
|
|
+**请求体**:无
|
|
|
+
|
|
|
+**响应(200)**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": {
|
|
|
+ "access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
|
+ "token_type": "Bearer",
|
|
|
+ "expires_in": 7200
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**错误响应**:
|
|
|
+
|
|
|
+| HTTP 状态码 | error_code | 说明 |
|
|
|
+|-------------|-----------|------|
|
|
|
+| 401 | INVALID_API_KEY | app_id 不存在 |
|
|
|
+| 401 | APP_DISABLED | 应用已被禁用 |
|
|
|
+| 401 | INVALID_SIGNATURE | 签名验证失败 |
|
|
|
+| 401 | TIMESTAMP_EXPIRED | 时间戳过期(超过 ±5 分钟) |
|
|
|
+| 401 | NONCE_USED | Nonce 已被使用(重放攻击) |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.2 刷新 Access Token
|
|
|
+
|
|
|
+**接口路由**:`POST /api/v1/open/auth/refresh`
|
|
|
+
|
|
|
+**功能说明**:使用有效的 Access Token 换取新的 Token,延长有效期
|
|
|
+
|
|
|
+**请求头**:
|
|
|
+
|
|
|
+| Header | 类型 | 必录 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
|
|
|
+
|
|
|
+**请求体**:无
|
|
|
+
|
|
|
+**响应(200)**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": {
|
|
|
+ "access_token": "eyJhbGciOiJIUzI1NiIs...",
|
|
|
+ "token_type": "Bearer",
|
|
|
+ "expires_in": 7200
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.3 查询项目列表
|
|
|
+
|
|
|
+**接口路由**:`GET /api/v1/open/projects`
|
|
|
+
|
|
|
+**功能说明**:查询标注平台的项目列表,支持按项目名称筛选
|
|
|
+
|
|
|
+**请求头**:
|
|
|
+
|
|
|
+| Header | 类型 | 必录 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
|
|
|
+
|
|
|
+**查询参数**:
|
|
|
+
|
|
|
+| 参数 | 类型 | 必录 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `name` | string | 否 | 项目名称(模糊匹配) |
|
|
|
+| `type` | string | 否 | 项目类型:`image` / `text` |
|
|
|
+| `status` | string | 否 | 项目状态筛选 |
|
|
|
+| `page` | int | 否 | 页码,默认 1 |
|
|
|
+| `page_size` | int | 否 | 每页数量,默认 20,最大 100 |
|
|
|
+
|
|
|
+**响应(200)**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": {
|
|
|
+ "items": [
|
|
|
+ {
|
|
|
+ "project_id": "proj_abc123def456",
|
|
|
+ "project_name": "商品图片分类标注",
|
|
|
+ "description": "对电商平台商品图片进行分类标注",
|
|
|
+ "project_type": "image",
|
|
|
+ "task_type": "image_classification",
|
|
|
+ "status": "in_progress",
|
|
|
+ "created_by": "admin",
|
|
|
+ "created_at": "2026-05-10T10:30:00",
|
|
|
+ "updated_at": "2026-05-15T14:20:00",
|
|
|
+ "task_count": 500,
|
|
|
+ "completed_task_count": 350
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "total": 1,
|
|
|
+ "page": 1,
|
|
|
+ "page_size": 20,
|
|
|
+ "total_pages": 1,
|
|
|
+ "has_next": false,
|
|
|
+ "has_prev": false
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**字段说明**:
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `project_id` | string | 项目唯一标识 |
|
|
|
+| `project_name` | string | 项目名称 |
|
|
|
+| `description` | string | 项目描述 |
|
|
|
+| `project_type` | string | 项目类型:`image`(图片)/ `text`(文本) |
|
|
|
+| `task_type` | string | 具体任务类型,见下方映射表 |
|
|
|
+| `status` | string | 项目状态:`draft` / `configuring` / `ready` / `in_progress` / `completed` |
|
|
|
+| `created_by` | string | 项目创建人用户名 |
|
|
|
+| `created_at` | string | 创建时间(ISO 8601) |
|
|
|
+| `updated_at` | string | 最后更新时间(ISO 8601) |
|
|
|
+| `task_count` | int | 总任务数 |
|
|
|
+| `completed_task_count` | int | 已完成任务数 |
|
|
|
+
|
|
|
+**项目类型映射**:
|
|
|
+
|
|
|
+| `task_type`(数据库) | `project_type`(API 返回) | 说明 |
|
|
|
+|----------------------|--------------------------|------|
|
|
|
+| `text_classification` | `text` | 文本分类 |
|
|
|
+| `ner` | `text` | 命名实体识别 |
|
|
|
+| `image_classification` | `image` | 图片分类 |
|
|
|
+| `object_detection` | `image` | 目标检测 |
|
|
|
+| `polygon` | `image` | 多边形标注 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.4 查询项目详情
|
|
|
+
|
|
|
+**接口路由**:`GET /api/v1/open/projects/{project_id}`
|
|
|
+
|
|
|
+**功能说明**:根据项目 ID 查询项目详细信息
|
|
|
+
|
|
|
+**路径参数**:
|
|
|
+
|
|
|
+| 参数 | 类型 | 必录 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `project_id` | string | 是 | 项目 ID |
|
|
|
+
|
|
|
+**请求头**:
|
|
|
+
|
|
|
+| Header | 类型 | 必录 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
|
|
|
+
|
|
|
+**响应(200)**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": {
|
|
|
+ "project_id": "proj_abc123def456",
|
|
|
+ "project_name": "商品图片分类标注",
|
|
|
+ "description": "对电商平台商品图片进行分类标注",
|
|
|
+ "project_type": "image",
|
|
|
+ "task_type": "image_classification",
|
|
|
+ "status": "in_progress",
|
|
|
+ "created_by": "admin",
|
|
|
+ "created_at": "2026-05-10T10:30:00",
|
|
|
+ "updated_at": "2026-05-15T14:20:00",
|
|
|
+ "task_count": 500,
|
|
|
+ "completed_task_count": 350,
|
|
|
+ "assigned_task_count": 500,
|
|
|
+ "completion_percentage": 70.0
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**错误响应**:
|
|
|
+
|
|
|
+| HTTP 状态码 | error_code | 说明 |
|
|
|
+|-------------|-----------|------|
|
|
|
+| 404 | PROJECT_NOT_FOUND | 项目不存在 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.5 数据集下载
|
|
|
+
|
|
|
+**接口路由**:`POST /api/v1/open/projects/{project_id}/datasets/download`
|
|
|
+
|
|
|
+**功能说明**:根据项目 ID 和指定的数据集格式,导出并下载标注数据集
|
|
|
+
|
|
|
+**路径参数**:
|
|
|
+
|
|
|
+| 参数 | 类型 | 必录 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `project_id` | string | 是 | 项目 ID |
|
|
|
+
|
|
|
+**请求头**:
|
|
|
+
|
|
|
+| Header | 类型 | 必录 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
|
|
|
+
|
|
|
+**请求体**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "format": "coco",
|
|
|
+ "completed_only": true
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| 参数 | 类型 | 必录 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `format` | string | 是 | 数据集格式,见格式说明表 |
|
|
|
+| `completed_only` | boolean | 否 | 是否只导出已完成的任务,默认 `true` |
|
|
|
+
|
|
|
+**支持的格式**:
|
|
|
+
|
|
|
+| `format` 值 | 数据类型 | 格式说明 | 对应后端方法 |
|
|
|
+|-------------|---------|---------|-------------|
|
|
|
+| `alpaca` | 文本 | Alpaca 指令微调格式 | `export_to_alpaca` |
|
|
|
+| `sharegpt` | 文本 | ShareGPT 对话格式 | `export_to_sharegpt` |
|
|
|
+| `json` | 图片 | 平台原生 JSON 格式 | `export_to_json` |
|
|
|
+| `csv` | 图片 | CSV 表格格式 | `export_to_csv` |
|
|
|
+| `coco` | 图片 | COCO 目标检测格式 | `export_to_coco` |
|
|
|
+| `yolo` | 图片 | YOLO 目标检测格式 | `export_to_yolo` |
|
|
|
+| `pascal_voc` | 图片 | PascalVOC XML 格式 | `export_to_pascal_voc` |
|
|
|
+
|
|
|
+**响应(200)**:
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": {
|
|
|
+ "project_id": "proj_abc123def456",
|
|
|
+ "format": "coco",
|
|
|
+ "total_exported": 350,
|
|
|
+ "file_url": "/api/v1/open/datasets/downloads/dl_abc123",
|
|
|
+ "file_name": "proj_abc123def456_coco_20260517_143000.json",
|
|
|
+ "file_size": 2048576,
|
|
|
+ "expires_at": "2026-05-17T16:30:00",
|
|
|
+ "status": "completed"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**字段说明**:
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `project_id` | string | 项目 ID |
|
|
|
+| `format` | string | 导出格式 |
|
|
|
+| `total_exported` | int | 导出的任务数量 |
|
|
|
+| `file_url` | string | 文件下载链接(相对路径) |
|
|
|
+| `file_name` | string | 文件名 |
|
|
|
+| `file_size` | int | 文件大小(字节) |
|
|
|
+| `expires_at` | string | 下载链接过期时间(ISO 8601),2 小时后过期 |
|
|
|
+| `status` | string | 导出状态:`completed` / `failed` |
|
|
|
+
|
|
|
+**下载文件**:
|
|
|
+
|
|
|
+模型训练平台拿到 `file_url` 后,通过 GET 请求下载文件:
|
|
|
+
|
|
|
+```
|
|
|
+GET /api/v1/open/datasets/downloads/{download_token}
|
|
|
+Authorization: Bearer {access_token}
|
|
|
+```
|
|
|
+
|
|
|
+**错误响应**:
|
|
|
+
|
|
|
+| HTTP 状态码 | error_code | 说明 |
|
|
|
+|-------------|-----------|------|
|
|
|
+| 400 | INVALID_FORMAT | 不支持的导出格式 |
|
|
|
+| 400 | FORMAT_NOT_COMPATIBLE | 格式与项目类型不兼容(如文本项目请求 COCO 格式) |
|
|
|
+| 404 | PROJECT_NOT_FOUND | 项目不存在 |
|
|
|
+| 404 | NO_DATA_AVAILABLE | 项目中没有可导出的数据 |
|
|
|
+| 500 | EXPORT_FAILED | 导出失败 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+### 5.6 下载文件获取
|
|
|
+
|
|
|
+**接口路由**:`GET /api/v1/open/datasets/downloads/{download_token}`
|
|
|
+
|
|
|
+**功能说明**:根据下载令牌获取实际的数据集文件
|
|
|
+
|
|
|
+**路径参数**:
|
|
|
+
|
|
|
+| 参数 | 类型 | 必录 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `download_token` | string | 是 | 下载令牌(5.5 接口返回的 file_url 中的 token) |
|
|
|
+
|
|
|
+**请求头**:
|
|
|
+
|
|
|
+| Header | 类型 | 必录 | 说明 |
|
|
|
+|--------|------|------|------|
|
|
|
+| `Authorization` | string | 是 | `Bearer {access_token}` |
|
|
|
+
|
|
|
+**响应**:
|
|
|
+
|
|
|
+- `Content-Type: application/octet-stream` 或对应格式的 MIME type
|
|
|
+- `Content-Disposition: attachment; filename="..."`
|
|
|
+- 响应体为实际文件内容
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 6. 数据模型
|
|
|
+
|
|
|
+### 6.1 统一响应格式
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": { ... }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+| 字段 | 类型 | 说明 |
|
|
|
+|------|------|------|
|
|
|
+| `code` | int | 业务状态码,0 表示成功,非 0 表示失败 |
|
|
|
+| `message` | string | 提示信息 |
|
|
|
+| `data` | object | 业务数据 |
|
|
|
+
|
|
|
+### 6.2 统一错误响应格式
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 1001,
|
|
|
+ "message": "签名验证失败",
|
|
|
+ "data": null,
|
|
|
+ "error_code": "INVALID_SIGNATURE",
|
|
|
+ "error_details": {
|
|
|
+ "field": "X-Signature",
|
|
|
+ "reason": "HMAC signature mismatch"
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.3 分页响应格式
|
|
|
+
|
|
|
+```json
|
|
|
+{
|
|
|
+ "code": 0,
|
|
|
+ "message": "success",
|
|
|
+ "data": {
|
|
|
+ "items": [...],
|
|
|
+ "total": 100,
|
|
|
+ "page": 1,
|
|
|
+ "page_size": 20,
|
|
|
+ "total_pages": 5,
|
|
|
+ "has_next": true,
|
|
|
+ "has_prev": false
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+### 6.4 Pydantic Schema 定义
|
|
|
+
|
|
|
+#### 6.4.1 认证相关 Schema
|
|
|
+
|
|
|
+```python
|
|
|
+# schemas/open_auth.py
|
|
|
+
|
|
|
+class TokenRequest(BaseModel):
|
|
|
+ """Token 请求(签名信息通过 Header 传递)"""
|
|
|
+ pass # 无请求体,签名信息在 Header 中
|
|
|
+
|
|
|
+class TokenResponseData(BaseModel):
|
|
|
+ access_token: str = Field(..., description="JWT 访问令牌")
|
|
|
+ token_type: str = Field(default="Bearer", description="令牌类型")
|
|
|
+ expires_in: int = Field(default=7200, description="过期时间(秒)")
|
|
|
+
|
|
|
+class TokenResponse(BaseModel):
|
|
|
+ code: int = Field(default=0, description="状态码")
|
|
|
+ message: str = Field(default="success", description="提示信息")
|
|
|
+ data: TokenResponseData
|
|
|
+
|
|
|
+class RefreshTokenResponse(BaseModel):
|
|
|
+ code: int = Field(default=0)
|
|
|
+ message: str = Field(default="success")
|
|
|
+ data: TokenResponseData
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.4.2 项目相关 Schema
|
|
|
+
|
|
|
+```python
|
|
|
+# schemas/open_project.py
|
|
|
+
|
|
|
+class ProjectType(str, Enum):
|
|
|
+ IMAGE = "image"
|
|
|
+ TEXT = "text"
|
|
|
+
|
|
|
+class OpenProjectItem(BaseModel):
|
|
|
+ project_id: str = Field(..., description="项目ID")
|
|
|
+ project_name: str = Field(..., description="项目名称")
|
|
|
+ description: str = Field(default="", description="项目描述")
|
|
|
+ project_type: ProjectType = Field(..., description="项目类型")
|
|
|
+ task_type: str = Field(..., description="任务类型")
|
|
|
+ status: str = Field(..., description="项目状态")
|
|
|
+ created_by: str = Field(..., description="创建人")
|
|
|
+ created_at: datetime = Field(..., description="创建时间")
|
|
|
+ updated_at: Optional[datetime] = Field(None, description="更新时间")
|
|
|
+ task_count: int = Field(default=0, description="总任务数")
|
|
|
+ completed_task_count: int = Field(default=0, description="已完成任务数")
|
|
|
+
|
|
|
+class OpenProjectListData(BaseModel):
|
|
|
+ items: List[OpenProjectItem]
|
|
|
+ total: int
|
|
|
+ page: int
|
|
|
+ page_size: int
|
|
|
+ total_pages: int
|
|
|
+ has_next: bool
|
|
|
+ has_prev: bool
|
|
|
+
|
|
|
+class OpenProjectListResponse(BaseModel):
|
|
|
+ code: int = Field(default=0)
|
|
|
+ message: str = Field(default="success")
|
|
|
+ data: OpenProjectListData
|
|
|
+
|
|
|
+class OpenProjectDetailResponse(BaseModel):
|
|
|
+ code: int = Field(default=0)
|
|
|
+ message: str = Field(default="success")
|
|
|
+ data: OpenProjectItem # 扩展字段在 service 层组装
|
|
|
+```
|
|
|
+
|
|
|
+#### 6.4.3 数据集下载 Schema
|
|
|
+
|
|
|
+```python
|
|
|
+# schemas/open_dataset.py
|
|
|
+
|
|
|
+class DatasetFormat(str, Enum):
|
|
|
+ # 文本格式
|
|
|
+ ALPACA = "alpaca"
|
|
|
+ SHAREGPT = "sharegpt"
|
|
|
+ # 图片格式
|
|
|
+ JSON = "json"
|
|
|
+ CSV = "csv"
|
|
|
+ COCO = "coco"
|
|
|
+ YOLO = "yolo"
|
|
|
+ PASCAL_VOC = "pascal_voc"
|
|
|
+
|
|
|
+TEXT_FORMATS = {DatasetFormat.ALPACA, DatasetFormat.SHAREGPT}
|
|
|
+IMAGE_FORMATS = {DatasetFormat.JSON, DatasetFormat.CSV, DatasetFormat.COCO,
|
|
|
+ DatasetFormat.YOLO, DatasetFormat.PASCAL_VOC}
|
|
|
+
|
|
|
+class DatasetDownloadRequest(BaseModel):
|
|
|
+ format: DatasetFormat = Field(..., description="数据集格式")
|
|
|
+ completed_only: bool = Field(default=True, description="是否只导出已完成的任务")
|
|
|
+
|
|
|
+class DatasetDownloadResponseData(BaseModel):
|
|
|
+ project_id: str = Field(..., description="项目ID")
|
|
|
+ format: str = Field(..., description="导出格式")
|
|
|
+ total_exported: int = Field(..., description="导出任务数")
|
|
|
+ file_url: str = Field(..., description="下载链接")
|
|
|
+ file_name: str = Field(..., description="文件名")
|
|
|
+ file_size: Optional[int] = Field(None, description="文件大小(字节)")
|
|
|
+ expires_at: Optional[datetime] = Field(None, description="链接过期时间")
|
|
|
+ status: str = Field(default="completed", description="导出状态")
|
|
|
+
|
|
|
+class DatasetDownloadResponse(BaseModel):
|
|
|
+ code: int = Field(default=0)
|
|
|
+ message: str = Field(default="success")
|
|
|
+ data: DatasetDownloadResponseData
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 7. 错误码定义
|
|
|
+
|
|
|
+### 7.1 通用错误码
|
|
|
+
|
|
|
+| code | error_code | HTTP 状态码 | 说明 |
|
|
|
+|------|-----------|------------|------|
|
|
|
+| 0 | SUCCESS | 200 | 请求成功 |
|
|
|
+| 1000 | INTERNAL_ERROR | 500 | 服务器内部错误 |
|
|
|
+| 1001 | INVALID_REQUEST | 400 | 请求参数无效 |
|
|
|
+| 1002 | UNAUTHORIZED | 401 | 未认证或认证过期 |
|
|
|
+
|
|
|
+### 7.2 认证错误码
|
|
|
+
|
|
|
+| code | error_code | HTTP 状态码 | 说明 |
|
|
|
+|------|-----------|------------|------|
|
|
|
+| 2001 | INVALID_API_KEY | 401 | app_id 不存在 |
|
|
|
+| 2002 | APP_DISABLED | 401 | 应用已被禁用 |
|
|
|
+| 2003 | INVALID_SIGNATURE | 401 | 签名验证失败 |
|
|
|
+| 2004 | TIMESTAMP_EXPIRED | 401 | 时间戳过期(超过 ±5 分钟) |
|
|
|
+| 2005 | NONCE_USED | 401 | Nonce 已被使用(重放攻击) |
|
|
|
+| 2006 | TOKEN_EXPIRED | 401 | Access Token 已过期 |
|
|
|
+| 2007 | TOKEN_INVALID | 401 | Access Token 无效 |
|
|
|
+
|
|
|
+### 7.3 业务错误码
|
|
|
+
|
|
|
+| code | error_code | HTTP 状态码 | 说明 |
|
|
|
+|------|-----------|------------|------|
|
|
|
+| 3001 | PROJECT_NOT_FOUND | 404 | 项目不存在 |
|
|
|
+| 3002 | INVALID_FORMAT | 400 | 不支持的导出格式 |
|
|
|
+| 3003 | FORMAT_NOT_COMPATIBLE | 400 | 格式与项目类型不兼容 |
|
|
|
+| 3004 | NO_DATA_AVAILABLE | 404 | 项目中没有可导出的数据 |
|
|
|
+| 3005 | EXPORT_FAILED | 500 | 导出失败 |
|
|
|
+| 3006 | DOWNLOAD_EXPIRED | 410 | 下载链接已过期 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 8. 文件组织结构
|
|
|
+
|
|
|
+基于现有项目结构,新增以下文件:
|
|
|
+
|
|
|
+```
|
|
|
+backend/
|
|
|
+├── routers/
|
|
|
+│ ├── api/
|
|
|
+│ │ └── v1/
|
|
|
+│ │ ├── __init__.py
|
|
|
+│ │ ├── open_auth_view.py # 认证相关 API 路由
|
|
|
+│ │ └── open_project_view.py # 项目和数据集 API 路由
|
|
|
+│
|
|
|
+├── services/
|
|
|
+│ ├── api/
|
|
|
+│ │ ├── __init__.py
|
|
|
+│ │ ├── open_auth_service.py # 认证业务逻辑(Token 生成、签名验证)
|
|
|
+│ │ └── open_project_service.py # 项目查询和数据集导出业务逻辑
|
|
|
+│ │
|
|
|
+│ └── open_middleware.py # 开放 API 认证中间件
|
|
|
+│
|
|
|
+├── schemas/
|
|
|
+│ ├── open_auth.py # 认证相关 Pydantic 模型
|
|
|
+│ ├── open_project.py # 项目相关 Pydantic 模型
|
|
|
+│ └── open_dataset.py # 数据集下载 Pydantic 模型
|
|
|
+│
|
|
|
+└── database.py # 需增加 api_applications 表创建逻辑
|
|
|
+```
|
|
|
+
|
|
|
+### 8.1 main.py 修改
|
|
|
+
|
|
|
+在 `main.py` 中注册新的路由:
|
|
|
+
|
|
|
+```python
|
|
|
+from routers.api.v1 import open_auth_view, open_project_view
|
|
|
+
|
|
|
+# Include open API routers
|
|
|
+app.include_router(open_auth_view.router)
|
|
|
+app.include_router(open_project_view.router)
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 9. 安全设计
|
|
|
+
|
|
|
+### 9.1 签名验证
|
|
|
+
|
|
|
+1. **时间戳校验**:请求时间与服务端时间差超过 5 分钟则拒绝
|
|
|
+2. **Nonce 去重**:使用过的 nonce 在有效期内不允许重复使用(存储于 Redis 或内存缓存)
|
|
|
+3. **HMAC-SHA256**:使用 app_secret 对 `app_id + timestamp + nonce` 进行签名
|
|
|
+
|
|
|
+### 9.2 Token 安全
|
|
|
+
|
|
|
+- Access Token 有效期 2 小时,过期后需重新通过签名获取
|
|
|
+- 提供刷新接口,持有有效 Token 时可延长有效期
|
|
|
+- Token 绑定到具体 app_id,不可跨应用使用
|
|
|
+
|
|
|
+### 9.3 限流策略
|
|
|
+
|
|
|
+- 获取 Token 接口:10 次/分钟(按 app_id 限流)
|
|
|
+- 数据集下载接口:5 次/分钟(按 app_id 限流)
|
|
|
+- 项目列表查询:30 次/分钟(按 app_id 限流)
|
|
|
+
|
|
|
+### 9.4 数据访问控制
|
|
|
+
|
|
|
+- 开放 API 只能访问 `status` 为 `ready` / `in_progress` / `completed` 的项目
|
|
|
+- 无法访问 `draft` / `configuring` 状态的项目
|
|
|
+- 不返回 XML 配置等内部敏感字段
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 10. 调用时序图
|
|
|
+
|
|
|
+### 10.1 完整业务流程
|
|
|
+
|
|
|
+```
|
|
|
+模型训练平台 标注平台
|
|
|
+ | |
|
|
|
+ | ① 获取 Token |
|
|
|
+ | POST /api/v1/open/auth/token
|
|
|
+ | Headers: |
|
|
|
+ | X-Api-Key: app_id_xxx |
|
|
|
+ | X-Signature: hmac... |
|
|
|
+ | X-Timestamp: 1716xxx |
|
|
|
+ | X-Nonce: rand16chars |
|
|
|
+ |------------------------->|
|
|
|
+ | | 验证签名
|
|
|
+ | | 生成 JWT
|
|
|
+ | |
|
|
|
+ | ① Response 200 |
|
|
|
+ | { access_token, ... } |
|
|
|
+ |<-------------------------|
|
|
|
+ | |
|
|
|
+ | |
|
|
|
+ | ② 查询项目列表 |
|
|
|
+ | GET /api/v1/open/projects?name=xxx
|
|
|
+ | Authorization: Bearer xxx
|
|
|
+ |------------------------->|
|
|
|
+ | | 验证 Token
|
|
|
+ | | 查询数据库
|
|
|
+ | |
|
|
|
+ | ② Response 200 |
|
|
|
+ | { items: [...], ... } |
|
|
|
+ |<-------------------------|
|
|
|
+ | |
|
|
|
+ | |
|
|
|
+ | ③ 下载数据集 |
|
|
|
+ | POST /api/v1/open/projects/{id}/datasets/download
|
|
|
+ | Authorization: Bearer xxx
|
|
|
+ | Body: { format: "coco" } |
|
|
|
+ |------------------------->|
|
|
|
+ | | 验证 Token
|
|
|
+ | | 执行导出
|
|
|
+ | | 生成下载链接
|
|
|
+ | |
|
|
|
+ | ③ Response 200 |
|
|
|
+ | { file_url: "/..." } |
|
|
|
+ |<-------------------------|
|
|
|
+ | |
|
|
|
+ | |
|
|
|
+ | ④ 下载文件 |
|
|
|
+ | GET /api/v1/open/datasets/downloads/{token}
|
|
|
+ | Authorization: Bearer xxx
|
|
|
+ |------------------------->|
|
|
|
+ | | 验证 Token
|
|
|
+ | | 返回文件流
|
|
|
+ | |
|
|
|
+ | ④ Response 200 |
|
|
|
+ | Content-Type: application/json
|
|
|
+ | [文件内容] |
|
|
|
+ |<-------------------------|
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 附录
|
|
|
+
|
|
|
+### A. 签名生成示例(Python)
|
|
|
+
|
|
|
+```python
|
|
|
+import time
|
|
|
+import secrets
|
|
|
+import hmac
|
|
|
+import hashlib
|
|
|
+
|
|
|
+def generate_signature(app_id: str, app_secret: str) -> dict:
|
|
|
+ """生成 API 请求签名"""
|
|
|
+ timestamp = str(int(time.time()))
|
|
|
+ nonce = secrets.token_hex(8) # 16 位随机字符串
|
|
|
+
|
|
|
+ message = f"{app_id}{timestamp}{nonce}"
|
|
|
+ signature = hmac.new(
|
|
|
+ app_secret.encode('utf-8'),
|
|
|
+ message.encode('utf-8'),
|
|
|
+ hashlib.sha256
|
|
|
+ ).hexdigest()
|
|
|
+
|
|
|
+ return {
|
|
|
+ "X-Api-Key": app_id,
|
|
|
+ "X-Signature": signature,
|
|
|
+ "X-Timestamp": timestamp,
|
|
|
+ "X-Nonce": nonce,
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+### B. 调用示例(curl)
|
|
|
+
|
|
|
+```bash
|
|
|
+# 1. 获取 Token
|
|
|
+curl -X POST http://localhost:8003/api/v1/open/auth/token \
|
|
|
+ -H "X-Api-Key: app_id_xxx" \
|
|
|
+ -H "X-Signature: abc123..." \
|
|
|
+ -H "X-Timestamp: 1716000000" \
|
|
|
+ -H "X-Nonce: a1b2c3d4e5f6g7h8"
|
|
|
+
|
|
|
+# 2. 查询项目列表
|
|
|
+curl -X GET "http://localhost:8003/api/v1/open/projects?page=1&page_size=20" \
|
|
|
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."
|
|
|
+
|
|
|
+# 3. 下载数据集
|
|
|
+curl -X POST http://localhost:8003/api/v1/open/projects/proj_xxx/datasets/download \
|
|
|
+ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
|
|
|
+ -H "Content-Type: application/json" \
|
|
|
+ -d '{"format": "coco", "completed_only": true}'
|
|
|
+```
|
|
|
+
|
|
|
+### C. 与现有 External API 的关系
|
|
|
+
|
|
|
+| 维度 | External API (`/api/external`) | Open API (`/api/v1/open`) |
|
|
|
+|------|-------------------------------|--------------------------|
|
|
|
+| 用途 | 样本中心主动推送数据到标注平台 | 模型训练平台拉取标注结果 |
|
|
|
+| 认证 | 现有 JWT / Admin Token | API Key + Secret 签名 |
|
|
|
+| 方向 | 推模式(Push) | 拉模式(Pull) |
|
|
|
+| 操作 | 创建项目、查询进度、回调导出 | 查询列表、下载数据集 |
|
|
|
+| 权限 | 需要管理员权限 | 应用级权限,无角色概念 |
|
|
|
+
|
|
|
+两者认证机制独立,互不影响。Open API 是全新的独立接口体系。
|