forked from erp-dev/erp
426 lines
9.1 KiB
Markdown
426 lines
9.1 KiB
Markdown
# 文件上传接口文档
|
||
|
||
## 概述
|
||
|
||
通用文件上传接口,用于上传无法归类到具体业务的文件。
|
||
|
||
**基础路径**: `/api/v1/upload/`
|
||
|
||
**认证要求**: 所有接口都需要 JWT Token 认证
|
||
|
||
**内容格式**: `multipart/form-data` (上传时) / `application/json` (响应)
|
||
|
||
**注意事项**:
|
||
- 不支持列表查询(list)
|
||
- 不支持修改操作(PUT/PATCH)
|
||
- 仅支持单个文件查询、上传、删除操作
|
||
|
||
---
|
||
|
||
## 数据模型
|
||
|
||
### UploadedFile
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | integer | 文件ID |
|
||
| path | string | 文件存储路径(随机文件名) |
|
||
| file_url | string | 文件访问URL |
|
||
| owner | integer | 上传者用户ID |
|
||
| owner_username | string | 上传者用户名 |
|
||
| is_deleted | boolean | 是否已删除(软删除标记) |
|
||
| original_filename | string | 原始文件名 |
|
||
| file_size | integer | 文件大小(字节) |
|
||
| content_type | string | MIME类型(如 image/jpeg) |
|
||
| created_at | datetime | 创建时间 |
|
||
| updated_at | datetime | 更新时间 |
|
||
|
||
---
|
||
|
||
## 接口列表
|
||
|
||
### 1. 上传文件
|
||
|
||
**请求**
|
||
```
|
||
POST /api/v1/upload/
|
||
Content-Type: multipart/form-data
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**请求参数**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| file | file | 是 | 要上传的文件(最大100MB) |
|
||
|
||
**请求示例**
|
||
```bash
|
||
curl -X POST http://localhost:8000/api/v1/upload/ \
|
||
-H "Authorization: Bearer YOUR_TOKEN" \
|
||
-F "file=@/path/to/your/file.pdf"
|
||
```
|
||
|
||
**成功响应** (201 Created)
|
||
```json
|
||
{
|
||
"id": 1,
|
||
"path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||
"file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||
"owner": 1,
|
||
"owner_username": "admin",
|
||
"is_deleted": false,
|
||
"original_filename": "document.pdf",
|
||
"file_size": 1048576,
|
||
"content_type": "application/pdf",
|
||
"created_at": "2025-11-17T10:30:00Z",
|
||
"updated_at": "2025-11-17T10:30:00Z"
|
||
}
|
||
```
|
||
|
||
**错误响应** (400 Bad Request)
|
||
```json
|
||
{
|
||
"file": ["未上传文件"]
|
||
}
|
||
```
|
||
|
||
```json
|
||
{
|
||
"file": ["文件大小不能超过100MB"]
|
||
}
|
||
```
|
||
|
||
**安全特性**:
|
||
- 文件名使用 UUID 随机化,防止文件名冲突和路径遍历攻击
|
||
- 原始文件名保存在数据库中,不影响存储安全
|
||
- 自动记录上传者信息
|
||
|
||
---
|
||
|
||
### 2. 获取文件信息
|
||
|
||
**请求**
|
||
```
|
||
GET /api/v1/upload/{id}/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**路径参数**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| id | integer | 是 | 文件ID |
|
||
|
||
**请求示例**
|
||
```bash
|
||
curl -X GET http://localhost:8000/api/v1/upload/1/ \
|
||
-H "Authorization: Bearer YOUR_TOKEN"
|
||
```
|
||
|
||
**成功响应** (200 OK)
|
||
```json
|
||
{
|
||
"id": 1,
|
||
"path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||
"file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||
"owner": 1,
|
||
"owner_username": "admin",
|
||
"is_deleted": false,
|
||
"original_filename": "document.pdf",
|
||
"file_size": 1048576,
|
||
"content_type": "application/pdf",
|
||
"created_at": "2025-11-17T10:30:00Z",
|
||
"updated_at": "2025-11-17T10:30:00Z"
|
||
}
|
||
```
|
||
|
||
**错误响应** (404 Not Found)
|
||
```json
|
||
{
|
||
"detail": "未找到"
|
||
}
|
||
```
|
||
|
||
**注意**: 已软删除的文件无法通过此接口查询
|
||
|
||
---
|
||
|
||
### 3. 软删除文件
|
||
|
||
**请求**
|
||
```
|
||
DELETE /api/v1/upload/{id}/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**路径参数**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| id | integer | 是 | 文件ID |
|
||
|
||
**请求示例**
|
||
```bash
|
||
curl -X DELETE http://localhost:8000/api/v1/upload/1/ \
|
||
-H "Authorization: Bearer YOUR_TOKEN"
|
||
```
|
||
|
||
**成功响应** (200 OK)
|
||
```json
|
||
{
|
||
"detail": "文件已标记为删除"
|
||
}
|
||
```
|
||
|
||
**说明**:
|
||
- 软删除不会物理删除文件,只是标记为已删除
|
||
- 软删除后的文件无法通过常规接口查询
|
||
- 可以通过恢复接口恢复文件
|
||
|
||
---
|
||
|
||
### 4. 恢复已删除文件
|
||
|
||
**请求**
|
||
```
|
||
POST /api/v1/upload/{id}/restore/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**路径参数**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| id | integer | 是 | 文件ID |
|
||
|
||
**请求示例**
|
||
```bash
|
||
curl -X POST http://localhost:8000/api/v1/upload/1/restore/ \
|
||
-H "Authorization: Bearer YOUR_TOKEN"
|
||
```
|
||
|
||
**成功响应** (200 OK)
|
||
```json
|
||
{
|
||
"detail": "文件已恢复",
|
||
"data": {
|
||
"id": 1,
|
||
"path": "uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||
"file_url": "/media/uploads/2025/11/17/a1b2c3d4e5f6...hex.pdf",
|
||
"owner": 1,
|
||
"owner_username": "admin",
|
||
"is_deleted": false,
|
||
"original_filename": "document.pdf",
|
||
"file_size": 1048576,
|
||
"content_type": "application/pdf",
|
||
"created_at": "2025-11-17T10:30:00Z",
|
||
"updated_at": "2025-11-17T10:30:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**错误响应** (400 Bad Request)
|
||
```json
|
||
{
|
||
"detail": "文件未被删除,无需恢复"
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 5. 永久删除文件
|
||
|
||
**请求**
|
||
```
|
||
DELETE /api/v1/upload/{id}/permanent_delete/
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
**路径参数**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| id | integer | 是 | 文件ID |
|
||
|
||
**请求示例**
|
||
```bash
|
||
curl -X DELETE http://localhost:8000/api/v1/upload/1/permanent_delete/ \
|
||
-H "Authorization: Bearer YOUR_TOKEN"
|
||
```
|
||
|
||
**成功响应** (204 No Content)
|
||
```json
|
||
{
|
||
"detail": "文件已永久删除"
|
||
}
|
||
```
|
||
|
||
**说明**:
|
||
- 永久删除会物理删除文件和数据库记录
|
||
- 此操作不可恢复,请谨慎使用
|
||
- 建议仅在确认不需要时使用
|
||
|
||
---
|
||
|
||
## 使用示例
|
||
|
||
### Python (requests)
|
||
|
||
```python
|
||
import requests
|
||
|
||
# 配置
|
||
BASE_URL = "http://localhost:8000/api/v1"
|
||
TOKEN = "your_jwt_token"
|
||
headers = {"Authorization": f"Bearer {TOKEN}"}
|
||
|
||
# 1. 上传文件
|
||
with open('document.pdf', 'rb') as f:
|
||
files = {'file': f}
|
||
response = requests.post(
|
||
f"{BASE_URL}/upload/",
|
||
headers=headers,
|
||
files=files
|
||
)
|
||
file_data = response.json()
|
||
file_id = file_data['id']
|
||
print(f"上传成功,文件ID: {file_id}")
|
||
|
||
# 2. 获取文件信息
|
||
response = requests.get(
|
||
f"{BASE_URL}/upload/{file_id}/",
|
||
headers=headers
|
||
)
|
||
print(f"文件信息: {response.json()}")
|
||
|
||
# 3. 软删除文件
|
||
response = requests.delete(
|
||
f"{BASE_URL}/upload/{file_id}/",
|
||
headers=headers
|
||
)
|
||
print(f"软删除: {response.json()}")
|
||
|
||
# 4. 恢复文件
|
||
response = requests.post(
|
||
f"{BASE_URL}/upload/{file_id}/restore/",
|
||
headers=headers
|
||
)
|
||
print(f"恢复文件: {response.json()}")
|
||
|
||
# 5. 永久删除
|
||
response = requests.delete(
|
||
f"{BASE_URL}/upload/{file_id}/permanent_delete/",
|
||
headers=headers
|
||
)
|
||
print(f"永久删除完成")
|
||
```
|
||
|
||
### JavaScript (Axios)
|
||
|
||
```javascript
|
||
const axios = require('axios');
|
||
const FormData = require('form-data');
|
||
const fs = require('fs');
|
||
|
||
const BASE_URL = 'http://localhost:8000/api/v1';
|
||
const TOKEN = 'your_jwt_token';
|
||
const headers = { Authorization: `Bearer ${TOKEN}` };
|
||
|
||
// 1. 上传文件
|
||
async function uploadFile() {
|
||
const formData = new FormData();
|
||
formData.append('file', fs.createReadStream('document.pdf'));
|
||
|
||
const response = await axios.post(
|
||
`${BASE_URL}/upload/`,
|
||
formData,
|
||
{ headers: { ...headers, ...formData.getHeaders() } }
|
||
);
|
||
|
||
console.log('上传成功:', response.data);
|
||
return response.data.id;
|
||
}
|
||
|
||
// 2. 获取文件信息
|
||
async function getFileInfo(fileId) {
|
||
const response = await axios.get(
|
||
`${BASE_URL}/upload/${fileId}/`,
|
||
{ headers }
|
||
);
|
||
console.log('文件信息:', response.data);
|
||
}
|
||
|
||
// 3. 软删除
|
||
async function softDelete(fileId) {
|
||
const response = await axios.delete(
|
||
`${BASE_URL}/upload/${fileId}/`,
|
||
{ headers }
|
||
);
|
||
console.log('软删除:', response.data);
|
||
}
|
||
|
||
// 4. 恢复文件
|
||
async function restore(fileId) {
|
||
const response = await axios.post(
|
||
`${BASE_URL}/upload/${fileId}/restore/`,
|
||
{},
|
||
{ headers }
|
||
);
|
||
console.log('恢复:', response.data);
|
||
}
|
||
|
||
// 5. 永久删除
|
||
async function permanentDelete(fileId) {
|
||
const response = await axios.delete(
|
||
`${BASE_URL}/upload/${fileId}/permanent_delete/`,
|
||
{ headers }
|
||
);
|
||
console.log('永久删除完成');
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 错误码说明
|
||
|
||
| HTTP状态码 | 说明 |
|
||
|-----------|------|
|
||
| 200 | 成功 |
|
||
| 201 | 创建成功 |
|
||
| 204 | 删除成功(无内容) |
|
||
| 400 | 请求参数错误 |
|
||
| 401 | 未认证或认证失败 |
|
||
| 403 | 无权限 |
|
||
| 404 | 资源不存在 |
|
||
| 413 | 文件过大 |
|
||
| 500 | 服务器内部错误 |
|
||
|
||
---
|
||
|
||
## 最佳实践
|
||
|
||
1. **文件大小限制**: 单文件最大100MB,超过此限制会返回400错误
|
||
2. **文件命名**: 系统自动使用UUID生成随机文件名,原始文件名保存在`original_filename`字段
|
||
3. **软删除策略**: 建议先使用软删除,确认不需要后再使用永久删除
|
||
4. **文件访问**: 使用返回的`file_url`字段访问文件
|
||
5. **权限控制**: 所有接口都需要认证,上传的文件自动关联当前用户
|
||
|
||
---
|
||
|
||
## 注意事项
|
||
|
||
1. **不支持的操作**:
|
||
- ❌ 列表查询 (`GET /api/v1/upload/`)
|
||
- ❌ 批量上传
|
||
- ❌ 修改文件 (`PUT/PATCH /api/v1/upload/{id}/`)
|
||
|
||
2. **文件存储**:
|
||
- 文件按日期组织:`uploads/YYYY/MM/DD/`
|
||
- 文件名使用32位十六进制UUID
|
||
- 保留原始文件扩展名
|
||
|
||
3. **查询限制**:
|
||
- 默认查询会过滤掉已软删除的文件
|
||
- 要访问已删除文件,需要通过Django Admin或直接数据库查询
|
||
|
||
4. **安全考虑**:
|
||
- 所有文件名随机化,防止路径遍历攻击
|
||
- 需要JWT认证
|
||
- 自动记录上传者信息
|