forked from erp-dev/erp
feat: big version, added tasks for backup_database and stock change, added health check api, approve sse (support channel via merchant)
This commit is contained in:
76
docs/business_purchase.md
Normal file
76
docs/business_purchase.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Business 模块采购单 API
|
||||
|
||||
## 创建采购单并触发入库
|
||||
|
||||
- **URL**: `POST /api/v1/purchase-orders/`
|
||||
- **权限**: 需要登录且具备员工身份
|
||||
- **描述**: 创建业务模块的 `PurchaseOrder`,并异步触发库存入库任务(通过 `stock.services.StockFlowService.stock_in` 实现)。
|
||||
|
||||
### 请求体
|
||||
|
||||
```json
|
||||
{
|
||||
"supplier": 1,
|
||||
"warehouse": 2,
|
||||
"order_date": "2025-11-26",
|
||||
"total_amount": "1200.50",
|
||||
"remarks": "测试采购单",
|
||||
"items": [
|
||||
{
|
||||
"product_id": 10,
|
||||
"quantities": ["10.5", "6.0"]
|
||||
},
|
||||
{
|
||||
"product_id": 18,
|
||||
"quantities": ["3.25"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| supplier | integer | ✅ | 供应商 ID,必须隶属于当前商户 |
|
||||
| warehouse | integer | ✅ | 入库仓库 ID |
|
||||
| order_date | string (date) | ✅ | 订单日期(`YYYY-MM-DD`) |
|
||||
| total_amount | string/number | ✅ | 采购总金额 |
|
||||
| remarks | string | 否 | 备注 |
|
||||
| items | array | ✅ | 入库明细,结构需满足 `StockFlowService` 的要求(目前仅支持严谨模式:product_id + quantities) |
|
||||
|
||||
> `items` 内部字段会被直接传给 `StockFlowService`,因此:
|
||||
> - 严谨/严进模式:`{'product_id': 1, 'quantities': ['10', '5']}`
|
||||
> - 宽松模式/严进严出出库暂未开放,后续按需扩展。
|
||||
|
||||
### 响应
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 35,
|
||||
"message": "采购单创建成功,入库任务已排队"
|
||||
}
|
||||
```
|
||||
|
||||
创建成功即刻返回,实际入库明细由后台 Celery 任务生成,可在日志或 `stock_change` 记录中查看。
|
||||
|
||||
### 错误示例
|
||||
|
||||
| 状态码 | 示例 | 说明 |
|
||||
|--------|------|------|
|
||||
| 400 | `{"error": "缺少供应商 ID"}` | 请求缺失关键字段 |
|
||||
| 400 | `{"error": "供应商 99 不存在"}` | 提供的供应商不属于当前商户 |
|
||||
| 403 | `{"error": "无权限访问"}` | 当前用户无员工信息 |
|
||||
|
||||
### 关联任务(business/tasks.py)
|
||||
|
||||
`create_purchase_order_stock_entries` 任务会接收 `purchase_order_id`、`warehouse_id`、`items` 等信息,并通过 `StockFlowService.stock_in` 创建入库记录。
|
||||
|
||||
- 任务日志示例:`采购单 35 入库任务完成`
|
||||
- 返回 payload 包含 `stock_change_record_id`、`created_details_count`
|
||||
|
||||
### 测试
|
||||
|
||||
`api_v1/tests.py` 中新增 `PurchaseOrderAPITestCase`,通过 eager Celery 设置验证:
|
||||
|
||||
1. API 请求返回 201
|
||||
2. Celery 任务被成功调用(patch `create_purchase_order_stock_entries.delay` 断言参数)
|
||||
|
||||
108
docs/celery_testing.md
Normal file
108
docs/celery_testing.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Celery 任务测试指南
|
||||
|
||||
## 1. 自动化测试(单元测试/CI)
|
||||
|
||||
项目新增了三个演示型任务(`api_v1/tasks.py`):
|
||||
|
||||
| 任务 | 功能 |
|
||||
|------|------|
|
||||
| `ping_task(message='ping')` | 打印并返回时间戳,用于快速验证 worker 是否可接收/返回结果 |
|
||||
| `merchant_product_count(merchant_id)` | 统计某商户下产品数量,示范如何在任务中访问数据库 |
|
||||
| `backup_database(output_dir=None, filename_prefix='db-backup')` | 将当前数据库导出为 `.sql` 文件(数据备份),默认保存到 `BASE_DIR/data-bak/` |
|
||||
|
||||
对应的单元测试位于 `api_v1/tests.py`(类 `CeleryTasksTestCase`),通过
|
||||
`@override_settings(CELERY_TASK_ALWAYS_EAGER=True)` 让任务在测试进程内同步执行。
|
||||
|
||||
运行方式:
|
||||
|
||||
```bash
|
||||
uv run manage.py test api_v1.tests.CeleryTasksTestCase
|
||||
```
|
||||
|
||||
若要在其他测试中调用任务,只需在测试类上使用同样的 `override_settings`
|
||||
即可保证 Celery 在没有 worker 的情况下仍能同步执行。
|
||||
|
||||
---
|
||||
|
||||
## 2. 本地验证真实 worker(RabbitMQ + Celery)
|
||||
|
||||
### 2.1 启动依赖服务
|
||||
|
||||
```bash
|
||||
# 启动基础设施(Postgres/Redis/Rabbit/Celery worker/web)
|
||||
docker compose up -d postgres redis rabbitmq
|
||||
|
||||
# 启动 web(Django)
|
||||
docker compose up -d web
|
||||
|
||||
# 启动 Celery worker(如已运行,可跳过)
|
||||
docker compose up -d celery_worker
|
||||
# 或者手动:uv run celery -A flower worker -l info
|
||||
```
|
||||
|
||||
> 若 worker 容器异常,可通过 `docker compose restart celery_worker` 重启。
|
||||
|
||||
### 2.2 发送测试任务
|
||||
|
||||
在另一个终端进入容器或宿主项目目录执行:
|
||||
|
||||
```bash
|
||||
uv run python manage.py shell
|
||||
```
|
||||
|
||||
```python
|
||||
from api_v1.tasks import ping_task, merchant_product_count, backup_database
|
||||
from basic_info.models import Merchant
|
||||
|
||||
# 发送心跳任务
|
||||
async_result = ping_task.delay('hello celery')
|
||||
print(async_result.get(timeout=10))
|
||||
|
||||
# 发送统计任务(示例:使用 ID=1 的商户)
|
||||
merchant = Merchant.objects.first()
|
||||
result = merchant_product_count.delay(merchant.id)
|
||||
print(result.get(timeout=10))
|
||||
|
||||
# 触发数据库备份(输出到默认 data-bak)
|
||||
backup = backup_database.delay(filename_prefix='manual-backup')
|
||||
print(backup.get(timeout=30)) # payload 中包含 backup_path,文件为 .sql
|
||||
```
|
||||
|
||||
### 2.3 观察执行结果
|
||||
|
||||
1. **Worker 日志**:`docker compose logs -f celery_worker` \
|
||||
可查看任务被消费、日志输出等信息。
|
||||
2. **RabbitMQ 控制台**:访问 `http://localhost:15672`(默认账号 guest/guest),
|
||||
观察队列长度是否回落到 0。
|
||||
3. **Task Result**:上面的 `result.get()` 会在任务完成时返回 payload,
|
||||
若超时或无法连接则会抛出异常,帮助定位问题。
|
||||
|
||||
---
|
||||
|
||||
## 3. 失败排查 Checklist
|
||||
|
||||
1. **环境变量**:`CELERY_BROKER_URL` 和 `CELERY_RESULT_BACKEND` 是否指向
|
||||
正在运行的 RabbitMQ/Redis?
|
||||
2. **Worker 进程**:`docker compose ps` 确认 `celery_worker` 状态为 Up。
|
||||
3. **队列阻塞**:RabbitMQ 控制台查看是否有大量消息处于 `Unacked`。
|
||||
4. **日志级别**:测试中需要捕获日志时,可使用
|
||||
`with self.assertLogs('api_v1.tasks', level='INFO')`.
|
||||
5. **自动化测试**:若任务在测试里需要真实队列,请去掉
|
||||
`CELERY_TASK_ALWAYS_EAGER` 覆盖;否则保持默认值即可同步执行。
|
||||
6. **数据库备份依赖**:`backup_database` 在 PostgreSQL 场景下需要系统可执行 `pg_dump`,
|
||||
请确保容器/宿主机已安装 PostgreSQL 客户端工具;SQLite 则会使用内置 `iterdump` 生成 `.sql`。
|
||||
|
||||
---
|
||||
|
||||
## 4. 常用命令速查
|
||||
|
||||
| 操作 | 命令 |
|
||||
|------|------|
|
||||
| 启动 worker | `docker compose up -d celery_worker` |
|
||||
| 查看 worker 日志 | `docker compose logs -f celery_worker` |
|
||||
| 重启 worker | `docker compose restart celery_worker` |
|
||||
| 清空 RabbitMQ 队列 | `docker compose exec rabbitmq rabbitmqctl purge_queue flower`(示例) |
|
||||
| 运行单个任务测试 | `uv run python manage.py shell` -> `ping_task.delay()` |
|
||||
|
||||
通过以上步骤,即可确认 Celery + RabbitMQ 在本地能够成功执行任务,并在自动化测试里保持覆盖。若需要新增业务任务,可参考 `api_v1/tasks.py` 的写法:使用 `@shared_task`,在任务内进行必要的日志记录,便于问题排查。***
|
||||
|
||||
217
docs/sse.md
Normal file
217
docs/sse.md
Normal file
@@ -0,0 +1,217 @@
|
||||
# SSE (Server-Sent Events) 模块
|
||||
|
||||
基于 Django 和 Django REST Framework 的服务器推送事件实现,支持多商户隔离。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 使用 DRF 处理请求和响应(非流式端点)
|
||||
- ✅ 支持多种请求格式(JSON、Form Data、Multipart)
|
||||
- ✅ 自动数据验证和序列化
|
||||
- ✅ 异步支持,高并发处理
|
||||
- ✅ 心跳机制保持连接活跃
|
||||
- ✅ 自动清理断开的连接
|
||||
- ✅ 连接状态监控
|
||||
- ✅ 多商户消息隔离
|
||||
- ✅ JWT认证和商户验证
|
||||
|
||||
## API 端点
|
||||
|
||||
### 1. 订阅 SSE 事件流
|
||||
|
||||
**端点**: `GET /sse/`
|
||||
|
||||
客户端连接此端点保持长连接,接收服务器推送的事件。
|
||||
|
||||
**认证要求**: 需要JWT认证,且用户必须有关联的商户
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
# 需要提供JWT token
|
||||
curl -N -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:8000/sse/
|
||||
```
|
||||
|
||||
**JavaScript 示例**:
|
||||
```javascript
|
||||
// 需要在连接时提供认证头
|
||||
const eventSource = new EventSource('/sse/', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${yourJwtToken}`
|
||||
}
|
||||
});
|
||||
|
||||
eventSource.onmessage = function(event) {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('收到消息:', data);
|
||||
console.log('商户ID:', data.merchant_id);
|
||||
};
|
||||
```
|
||||
|
||||
**注意**: 浏览器原生的EventSource不支持自定义请求头,因此推荐使用EventSource polyfill或fetch实现。
|
||||
|
||||
---
|
||||
|
||||
### 2. 推送事件到当前商户的客户端
|
||||
|
||||
**端点**: `POST /sse/push/`
|
||||
|
||||
向当前用户所属商户的所有已连接客户端广播消息。
|
||||
|
||||
**认证要求**: 需要JWT认证,且用户必须有关联的商户
|
||||
|
||||
**请求参数**:
|
||||
- 自动使用当前用户的商户ID
|
||||
- 固定发送测试消息:'订单已支付'
|
||||
- 固定事件类型:'order_paid'
|
||||
- 固定对象ID:12345
|
||||
|
||||
**请求示例**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/sse/push/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "Test event broadcasted to your merchant",
|
||||
"merchant_id": 1,
|
||||
"clients": 3
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 获取连接状态
|
||||
|
||||
**端点**: `GET /sse/status/`
|
||||
|
||||
查询当前 SSE 服务器的状态和连接数。
|
||||
|
||||
**认证要求**: 需要JWT认证,且用户必须有关联的商户
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
curl -H "Authorization: Bearer YOUR_JWT_TOKEN" http://localhost:8000/sse/status/
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"status": "running",
|
||||
"total_clients": 10,
|
||||
"merchant_clients": 3,
|
||||
"merchant_id": 1,
|
||||
"message": "SSE server is running with 10 total connections, 3 for your merchant"
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 关闭商户连接
|
||||
|
||||
**端点**: `POST /sse/shutdown/`
|
||||
|
||||
关闭当前用户所属商户的所有SSE连接。
|
||||
|
||||
**认证要求**: 需要JWT认证,且用户必须有关联的商户
|
||||
|
||||
**示例**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8000/sse/shutdown/ \
|
||||
-H "Authorization: Bearer YOUR_JWT_TOKEN"
|
||||
```
|
||||
|
||||
**响应示例**:
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"message": "Shutdown signal sent to your merchant's SSE connections",
|
||||
"merchant_id": 1,
|
||||
"clients": 3
|
||||
}
|
||||
```
|
||||
|
||||
## 启动服务器
|
||||
|
||||
使用 Uvicorn (ASGI 服务器) 启动:
|
||||
|
||||
```bash
|
||||
# 开发环境
|
||||
uvicorn flower.asgi:application --reload --host 0.0.0.0 --port 8000
|
||||
|
||||
# 生产环境
|
||||
uvicorn flower.asgi:application --host 0.0.0.0 --port 8000 --workers 4
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
打开 `sse_test.html` 在浏览器中测试:
|
||||
|
||||
1. 点击"连接 SSE"建立连接
|
||||
2. 输入消息
|
||||
3. 点击"发送 (JSON)"或"发送 (Form Data)"测试不同格式
|
||||
4. 点击"获取连接状态"查看当前连接数
|
||||
5. 打开多个浏览器标签测试广播功能
|
||||
|
||||
## Python 客户端示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
import sseclient # pip install sseclient-py
|
||||
|
||||
# 订阅事件
|
||||
response = requests.get('http://localhost:8000/sse/', stream=True)
|
||||
client = sseclient.SSEClient(response)
|
||||
|
||||
for event in client.events():
|
||||
print(f'收到消息: {event.data}')
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
- **流式响应**: 使用 `StreamingHttpResponse` 实现 SSE 连接
|
||||
- **队列机制**: 每个连接对应一个 `queue.Queue`,按商户组织
|
||||
- **心跳**: 30 秒超时,自动发送心跳保持连接
|
||||
- **DRF 集成**: 非流式端点使用 DRF 的 `@api_view` 和序列化器
|
||||
- **多格式支持**: 自动解析 JSON、Form Data、Multipart 等格式
|
||||
- **商户隔离**: 所有消息按商户隔离,确保数据安全
|
||||
- **JWT认证**: 使用 DRF Simple JWT 进行身份验证
|
||||
|
||||
## 消息结构
|
||||
|
||||
### 连接成功消息
|
||||
```json
|
||||
{
|
||||
"type": "connected",
|
||||
"message": "SSE connection established",
|
||||
"merchant_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 业务事件消息
|
||||
```json
|
||||
{
|
||||
"mode": "simple_message",
|
||||
"type": "order_paid",
|
||||
"message": "订单已支付",
|
||||
"object_id": 12345,
|
||||
"merchant_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 服务器关闭消息
|
||||
```json
|
||||
{
|
||||
"type": "server_shutdown",
|
||||
"message": "Server shutting down your connections, please reconnect later"
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 必须使用 ASGI 服务器(如 Uvicorn、Daphne)运行
|
||||
2. 不支持使用传统的 WSGI 服务器(如 Gunicorn + WSGI)
|
||||
3. 如果使用 Nginx,需要禁用缓冲:`X-Accel-Buffering: no`
|
||||
4. 所有SSE端点都需要JWT认证,且用户必须有关联的商户
|
||||
5. 浏览器原生的EventSource不支持自定义请求头,推荐使用polyfill或fetch实现
|
||||
6. SSE 使用 GET 请求,注意 CORS 配置
|
||||
425
docs/upload.md
Normal file
425
docs/upload.md
Normal file
@@ -0,0 +1,425 @@
|
||||
# 文件上传接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
通用文件上传接口,用于上传无法归类到具体业务的文件。
|
||||
|
||||
**基础路径**: `/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认证
|
||||
- 自动记录上传者信息
|
||||
Reference in New Issue
Block a user