创建中心任务的管理系统

This commit is contained in:
2025-12-05 13:45:16 +08:00
commit 2ca8b8d181
62 changed files with 1611 additions and 0 deletions

View File

@@ -0,0 +1,200 @@
# 任务中心管理系统最终实现计划
## 项目结构设计
1. 创建Django项目`task_center`
2. 创建任务管理应用:`tasks`
3. 配置数据库为SQLite
4. 集成Bootstrap前端框架
5. 配置pytest测试框架
## 数据库模型设计
### Client模型
| 字段名 | 类型 | 描述 |
|-------|------|------|
| id | AutoField | 客户端ID |
| name | CharField(unique=True) | 客户端标识 |
| token | CharField(max_length=128) | API Token |
| last_seen | DateTimeField | 最后活跃时间 |
| created_at | DateTimeField | 创建时间 |
### Task模型
| 字段名 | 类型 | 描述 |
|-------|------|------|
| id | AutoField | 任务ID |
| name | CharField | 任务名称 |
| client_name | CharField(null=True, blank=True) | 指定执行客户端 |
| script | TextField(null=True, blank=True) | 执行脚本 |
| status | CharField(choices=STATUS_CHOICES, default='pending') | 任务状态 |
| timeout_seconds | IntegerField(default=259200) | 超时时间默认3天=259200秒 |
| created_at | DateTimeField | 创建时间 |
| updated_at | DateTimeField | 更新时间 |
| assigned_to | CharField(null=True, blank=True) | 实际执行客户端 |
| started_at | DateTimeField(null=True, blank=True) | 开始执行时间 |
| completed_at | DateTimeField(null=True, blank=True) | 完成时间 |
### TaskResult模型
| 字段名 | 类型 | 描述 |
|-------|------|------|
| id | AutoField | 结果ID |
| task | ForeignKey(Task) | 关联任务 |
| client | ForeignKey(Client) | 执行客户端 |
| result_file | FileField(upload_to='task_results/') | 结果文件 |
| status | CharField(choices=STATUS_CHOICES) | 执行状态 |
| message | TextField(null=True, blank=True) | 执行消息 |
| created_at | DateTimeField | 创建时间 |
## 状态定义
```python
STATUS_CHOICES = [
('pending', '待分配'),
('assigned', '已分配'),
('running', '执行中'),
('success', '成功'),
('failed', '失败'),
('retrying', '重试中'),
('timeout', '超时,关闭'),
]
```
## API接口设计
### 认证机制
- **所有API端点均需token认证**
- 客户端通过HTTP头`Authorization: Token <token>`进行身份验证
- 使用Django REST Framework的TokenAuthentication
- 未提供有效token的请求将返回401 Unauthorized
### 任务管理API
- `GET /api/tasks/` - 获取任务列表(需认证)
- `GET /api/tasks/<id>/` - 获取任务详情(需认证)
- `POST /api/tasks/` - 创建任务(需认证)
- `PUT /api/tasks/<id>/` - 更新任务(需认证)
- `DELETE /api/tasks/<id>/` - 删除任务(需认证)
### 客户端API
- `POST /api/tasks/claim/` - 客户端原子认领任务(需认证)
- `POST /api/tasks/<id>/start/` - 客户端开始执行任务(需认证)
- `POST /api/tasks/<id>/complete/` - 客户端完成任务(需认证)
- `POST /api/task_results/` - 上传任务结果(需认证,支持文件上传)
### 客户端管理API
- `GET /api/clients/` - 获取客户端列表(需认证)
- `POST /api/clients/` - 创建客户端(需认证)
- `GET /api/clients/<id>/` - 获取客户端详情(需认证)
### 文件下载API
- `GET /api/task_results/<id>/download/` - 下载任务结果文件(需认证)
## 文件上传实现细节
### 1. Django媒体文件配置
```python
# settings.py
MEDIA_URL = '/media/'
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
```
### 2. 文件上传API实现
- 使用Django REST Framework的`MultiPartParser``FormParser`
- API端点`POST /api/task_results/`
- 支持`multipart/form-data`格式上传文件
- 上传字段:`task_id``status``message``result_file`
### 3. 前端文件上传实现
- 使用HTML5的`input type="file"`元素
- 表单设置`enctype="multipart/form-data"`
- 使用Bootstrap样式美化文件上传控件
- 支持进度条显示(可选)
### 4. 文件存储策略
- 本地文件系统存储:`media/task_results/`目录
- 文件名自动生成,避免冲突
- 支持大文件上传通过Django默认配置
### 5. 文件下载实现
- API端点`GET /api/task_results/<id>/download/`
- 返回`Content-Disposition: attachment`头,触发浏览器下载
- 支持断点续传通过Django默认配置
## 前端页面设计
1. 任务列表页:展示所有任务,支持筛选和搜索
2. 任务创建页:表单创建新任务
3. 任务详情页:查看任务详情和执行结果历史
4. 客户端管理页:管理客户端列表
5. 结果文件上传页:支持文件上传和状态更新
6. 结果文件下载功能:点击即可下载
## 核心功能实现
### 1. 全API Token认证
- 所有API视图均使用`TokenAuthentication`
- 配置`DEFAULT_AUTHENTICATION_CLASSES``DEFAULT_PERMISSION_CLASSES`
### 2. 原子任务认领机制
- 使用数据库事务确保任务认领的原子性
- 客户端调用`/api/tasks/claim/`时,系统自动查找并分配可用任务
### 3. 任务超时自动回收
- 使用Django管理命令定期检查超时任务
- 超过`timeout_seconds`的任务自动设置为`timeout`状态
- 管理命令:`python manage.py check_task_timeouts`
### 4. 任务结果版本管理
- TaskResult模型记录每次执行结果
- 支持查看任务的完整执行历史
- 支持下载不同版本的结果文件
## 测试用例设计
### 目录结构
```
task_center/
├── tasks/
│ ├── tests/
│ │ ├── __init__.py
│ │ ├── test_models.py
│ │ ├── test_views.py
│ │ ├── test_api.py
│ │ ├── test_integration.py
│ │ └── test_factories.py
│ └── ...
├── conftest.py
└── pytest.ini
```
### 测试类型
1. **模型测试**:测试模型字段、方法和关系
2. **API测试**测试所有API端点的功能和认证机制
3. **文件上传测试**:测试文件上传和下载功能
4. **视图测试**:测试前端视图渲染
5. **集成测试**:测试完整的任务流程
6. **工厂测试**使用factory_boy创建测试数据
## 实现步骤
1. 创建Django项目和应用
2. 配置项目设置数据库、REST Framework、认证、媒体文件等
3. 实现数据库模型
4. 实现API视图和序列化器包括文件上传功能
5. 配置URL路由
6. 实现前端模板,包括文件上传表单
7. 实现任务超时管理命令
8. 编写测试用例
9. 测试API接口和功能
## 技术栈
- 后端Django 5.0.6 + Django REST Framework
- 前端HTML + Bootstrap 5
- 数据库SQLite
- 文件存储:本地文件系统
- 测试pytest + factory_boy
## 预期效果
- 所有API端点均需要token认证
- 支持结果文件的上传和下载功能
- 后台可通过admin或API创建和管理任务
- 客户端通过API进行身份验证原子认领任务
- 支持任务超时自动回收
- 完整的任务执行历史记录
- 简单大方的Bootstrap前端界面
- 全面的测试用例覆盖

BIN
db.sqlite3 Normal file

Binary file not shown.

22
manage.py Normal file
View File

@@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'task_center.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
task_center/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
task_center/asgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
ASGI config for task_center project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'task_center.settings')
application = get_asgi_application()

139
task_center/settings.py Normal file
View File

@@ -0,0 +1,139 @@
"""
Django settings for task_center project.
Generated by 'django-admin startproject' using Django 5.0.6.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.0/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-&_@v3e5@!x+fa5273@v=^&02p@(#b89=p^06ifule17*p+g@u8'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'tasks',
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'task_center.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'task_center.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.0/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.0/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# REST Framework settings
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny',
],
}
# Media files settings
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
# Test settings
# TEST_RUNNER = 'pytest_django.runner.DjangoPytestTestRunner' # Commented out as it's not needed for pytest

28
task_center/urls.py Normal file
View File

@@ -0,0 +1,28 @@
"""
URL configuration for task_center project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.0/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
from django.conf import settings
from django.conf.urls.static import static
from tasks.views_frontend import index
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include('tasks.urls')),
# Root URL points to home page
path('', index, name='index'),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

16
task_center/wsgi.py Normal file
View File

@@ -0,0 +1,16 @@
"""
WSGI config for task_center project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'task_center.settings')
application = get_wsgi_application()

0
tasks/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
tasks/admin.py Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
tasks/apps.py Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TasksConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'tasks'

View File

Binary file not shown.

View File

View File

@@ -0,0 +1,37 @@
from django.core.management.base import BaseCommand
from django.utils import timezone
from tasks.models import Task
class Command(BaseCommand):
help = 'Check and update tasks that have timed out'
def handle(self, *args, **kwargs):
now = timezone.now()
# Get all tasks that are in progress (assigned, running, retrying)
in_progress_tasks = Task.objects.filter(
status__in=['assigned', 'running', 'retrying']
)
timeout_count = 0
for task in in_progress_tasks:
# Calculate when the task should timeout
if task.started_at:
# If task has started, calculate from started_at
timeout_time = task.started_at + timezone.timedelta(seconds=task.timeout_seconds)
else:
# If task hasn't started, calculate from created_at
timeout_time = task.created_at + timezone.timedelta(seconds=task.timeout_seconds)
# Check if task has timed out
if now > timeout_time:
task.status = 'timeout'
task.save()
timeout_count += 1
self.stdout.write(self.style.WARNING(f'Task {task.id} "{task.name}" has timed out'))
if timeout_count > 0:
self.stdout.write(self.style.SUCCESS(f'Successfully updated {timeout_count} timed out tasks'))
else:
self.stdout.write(self.style.INFO('No tasks have timed out'))

View File

@@ -0,0 +1,65 @@
# Generated by Django 5.0.6 on 2025-12-05 03:40
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Client',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='客户端标识')),
('token', models.CharField(max_length=128, verbose_name='API Token')),
('last_seen', models.DateTimeField(auto_now=True, verbose_name='最后活跃时间')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
],
options={
'verbose_name': '客户端',
'verbose_name_plural': '客户端',
},
),
migrations.CreateModel(
name='Task',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='任务名称')),
('client_name', models.CharField(blank=True, max_length=100, null=True, verbose_name='指定执行客户端')),
('script', models.TextField(blank=True, null=True, verbose_name='执行脚本')),
('status', models.CharField(choices=[('pending', '待分配'), ('assigned', '已分配'), ('running', '执行中'), ('success', '成功'), ('failed', '失败'), ('retrying', '重试中'), ('timeout', '超时,关闭')], default='pending', max_length=20, verbose_name='任务状态')),
('timeout_seconds', models.IntegerField(default=259200, verbose_name='超时时间(秒)')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('assigned_to', models.CharField(blank=True, max_length=100, null=True, verbose_name='实际执行客户端')),
('started_at', models.DateTimeField(blank=True, null=True, verbose_name='开始执行时间')),
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='完成时间')),
],
options={
'verbose_name': '任务',
'verbose_name_plural': '任务',
},
),
migrations.CreateModel(
name='TaskResult',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('result_file', models.FileField(blank=True, null=True, upload_to='task_results/', verbose_name='结果文件')),
('status', models.CharField(choices=[('pending', '待分配'), ('assigned', '已分配'), ('running', '执行中'), ('success', '成功'), ('failed', '失败'), ('retrying', '重试中'), ('timeout', '超时,关闭')], max_length=20, verbose_name='执行状态')),
('message', models.TextField(blank=True, null=True, verbose_name='执行消息')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tasks.client', verbose_name='执行客户端')),
('task', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='results', to='tasks.task', verbose_name='关联任务')),
],
options={
'verbose_name': '任务结果',
'verbose_name_plural': '任务结果',
},
),
]

View File

Binary file not shown.

60
tasks/models.py Normal file
View File

@@ -0,0 +1,60 @@
from django.db import models
from django.utils import timezone
# Status choices for tasks
STATUS_CHOICES = [
('pending', '待分配'),
('assigned', '已分配'),
('running', '执行中'),
('success', '成功'),
('failed', '失败'),
('retrying', '重试中'),
('timeout', '超时,关闭'),
]
class Client(models.Model):
name = models.CharField(max_length=100, unique=True, verbose_name='客户端标识')
token = models.CharField(max_length=128, verbose_name='API Token')
last_seen = models.DateTimeField(auto_now=True, verbose_name='最后活跃时间')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
def __str__(self):
return self.name
class Meta:
verbose_name = '客户端'
verbose_name_plural = '客户端'
class Task(models.Model):
name = models.CharField(max_length=200, verbose_name='任务名称')
client_name = models.CharField(max_length=100, null=True, blank=True, verbose_name='指定执行客户端')
script = models.TextField(null=True, blank=True, verbose_name='执行脚本')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending', verbose_name='任务状态')
timeout_seconds = models.IntegerField(default=259200, verbose_name='超时时间(秒)') # 默认3天
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
assigned_to = models.CharField(max_length=100, null=True, blank=True, verbose_name='实际执行客户端')
started_at = models.DateTimeField(null=True, blank=True, verbose_name='开始执行时间')
completed_at = models.DateTimeField(null=True, blank=True, verbose_name='完成时间')
def __str__(self):
return self.name
class Meta:
verbose_name = '任务'
verbose_name_plural = '任务'
class TaskResult(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE, related_name='results', verbose_name='关联任务')
client = models.ForeignKey(Client, on_delete=models.CASCADE, verbose_name='执行客户端')
result_file = models.FileField(upload_to='task_results/', null=True, blank=True, verbose_name='结果文件')
status = models.CharField(max_length=20, choices=STATUS_CHOICES, verbose_name='执行状态')
message = models.TextField(null=True, blank=True, verbose_name='执行消息')
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
def __str__(self):
return f"{self.task.name} - {self.client.name} - {self.status}"
class Meta:
verbose_name = '任务结果'
verbose_name_plural = '任务结果'

30
tasks/serializers.py Normal file
View File

@@ -0,0 +1,30 @@
from rest_framework import serializers
from .models import Client, Task, TaskResult
class ClientSerializer(serializers.ModelSerializer):
class Meta:
model = Client
fields = ['id', 'name', 'token', 'last_seen', 'created_at']
read_only_fields = ['token', 'last_seen', 'created_at']
class TaskSerializer(serializers.ModelSerializer):
class Meta:
model = Task
fields = ['id', 'name', 'client_name', 'script', 'status', 'timeout_seconds', 'created_at', 'updated_at', 'assigned_to', 'started_at', 'completed_at']
read_only_fields = ['created_at', 'updated_at', 'assigned_to', 'started_at', 'completed_at']
class TaskResultSerializer(serializers.ModelSerializer):
class Meta:
model = TaskResult
fields = ['id', 'task', 'client', 'result_file', 'status', 'message', 'created_at']
read_only_fields = ['created_at']
class TaskClaimSerializer(serializers.Serializer):
client_name = serializers.CharField(max_length=100)
class TaskStartSerializer(serializers.Serializer):
pass
class TaskCompleteSerializer(serializers.Serializer):
status = serializers.ChoiceField(choices=[('success', '成功'), ('failed', '失败')])
message = serializers.CharField(allow_blank=True, required=False)

View File

@@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务中心</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'task_list' %}">任务中心</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link" href="{% url 'task_list' %}">任务列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'task_create' %}">创建任务</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'client_list' %}">客户端管理</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-4">
{% block content %}
{% endblock %}
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,25 @@
{% extends 'tasks/base.html' %}
{% block content %}
<h1 class="mb-4">创建客户端</h1>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="name" class="form-label">客户端名称</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="alert alert-info" role="alert">
创建后将自动生成API Token用于客户端认证。
</div>
<button type="submit" class="btn btn-primary">创建客户端</button>
<a href="{% url 'client_list' %}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends 'tasks/base.html' %}
{% block content %}
<h1 class="mb-4">客户端列表</h1>
<div class="card mb-4">
<div class="card-header">
<a href="{% url 'client_create' %}" class="btn btn-primary">创建新客户端</a>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>客户端名称</th>
<th>API Token</th>
<th>最后活跃时间</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
{% for client in clients %}
<tr>
<td>{{ client.id }}</td>
<td>{{ client.name }}</td>
<td>
<code>{{ client.token }}</code>
</td>
<td>{{ client.last_seen|date:'Y-m-d H:i:s' }}</td>
<td>{{ client.created_at|date:'Y-m-d H:i:s' }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>任务中心 - 首页</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
<div class="container">
<a class="navbar-brand" href="{% url 'index' %}">任务中心</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav">
<li class="nav-item">
<a class="nav-link active" href="{% url 'index' %}">首页</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'task_list' %}">任务列表</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'task_create' %}">创建任务</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{% url 'client_list' %}">客户端管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/">后台管理</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container mt-5">
<div class="jumbotron bg-light p-5 rounded-3">
<h1 class="display-4">欢迎使用任务中心</h1>
<p class="lead">一个基于Django的任务管理系统支持客户端任务认领、执行和结果上传。</p>
<hr class="my-4">
<p>系统功能:</p>
<ul>
<li>创建和管理任务</li>
<li>客户端原子认领任务</li>
<li>任务执行状态跟踪</li>
<li>结果文件上传下载</li>
<li>客户端管理和认证</li>
</ul>
<div class="mt-4">
<a class="btn btn-primary btn-lg" href="{% url 'task_list' %}" role="button">查看任务</a>
<a class="btn btn-secondary btn-lg" href="/admin/" role="button">后台管理</a>
</div>
</div>
<div class="row mt-5">
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">任务管理</h5>
<p class="card-text">创建、查看和管理任务,支持设置执行客户端和超时时间。</p>
<a href="{% url 'task_list' %}" class="btn btn-primary">进入</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">客户端管理</h5>
<p class="card-text">管理客户端列表自动生成API Token用于客户端认证。</p>
<a href="{% url 'client_list' %}" class="btn btn-primary">进入</a>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card h-100">
<div class="card-body">
<h5 class="card-title">后台管理</h5>
<p class="card-text">使用Django Admin进行系统管理包括用户、任务和客户端。</p>
<a href="/admin/" class="btn btn-primary">进入</a>
</div>
</div>
</div>
</div>
</div>
<footer class="mt-5 py-4 bg-dark text-white">
<div class="container text-center">
<p>&copy; 2025 任务中心管理系统</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@@ -0,0 +1,37 @@
{% extends 'tasks/base.html' %}
{% block content %}
<h1 class="mb-4">创建任务</h1>
<div class="card">
<div class="card-body">
<form method="post">
{% csrf_token %}
<div class="mb-3">
<label for="name" class="form-label">任务名称</label>
<input type="text" class="form-control" id="name" name="name" required>
</div>
<div class="mb-3">
<label for="client_name" class="form-label">指定执行客户端(可选)</label>
<input type="text" class="form-control" id="client_name" name="client_name">
</div>
<div class="mb-3">
<label for="script" class="form-label">执行脚本(可选)</label>
<textarea class="form-control" id="script" name="script" rows="5"></textarea>
</div>
<div class="mb-3">
<label for="timeout_seconds" class="form-label">超时时间(秒)</label>
<input type="number" class="form-control" id="timeout_seconds" name="timeout_seconds" value="259200">
<div class="form-text">默认3天259200秒</div>
</div>
<button type="submit" class="btn btn-primary">创建任务</button>
<a href="{% url 'task_list' %}" class="btn btn-secondary">取消</a>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,98 @@
{% extends 'tasks/base.html' %}
{% block content %}
<h1 class="mb-4">任务详情</h1>
<div class="card mb-4">
<div class="card-header">
<h5>{{ task.name }}</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<p><strong>ID:</strong> {{ task.id }}</p>
<p><strong>指定客户端:</strong> {{ task.client_name|default:'未指定' }}</p>
<p><strong>实际执行客户端:</strong> {{ task.assigned_to|default:'未分配' }}</p>
<p><strong>状态:</strong>
<span class="badge bg-{{
'success' if task.status == 'success' else
'danger' if task.status in ['failed', 'timeout'] else
'warning' if task.status in ['assigned', 'running', 'retrying'] else
'info'
}}">
{{ task.get_status_display }}
</span>
</p>
</div>
<div class="col-md-6">
<p><strong>创建时间:</strong> {{ task.created_at|date:'Y-m-d H:i:s' }}</p>
<p><strong>更新时间:</strong> {{ task.updated_at|date:'Y-m-d H:i:s' }}</p>
<p><strong>开始执行时间:</strong> {{ task.started_at|default:'未开始'|date:'Y-m-d H:i:s' }}</p>
<p><strong>完成时间:</strong> {{ task.completed_at|default:'未完成'|date:'Y-m-d H:i:s' }}</p>
<p><strong>超时时间:</strong> {{ task.timeout_seconds }} 秒</p>
</div>
</div>
{% if task.script %}
<div class="mt-4">
<h6>执行脚本:</h6>
<pre class="bg-light p-3 rounded">{{ task.script }}</pre>
</div>
{% endif %}
</div>
</div>
<h2 class="mb-4">执行结果</h2>
<div class="card">
<div class="card-body">
{% if results %}
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>执行客户端</th>
<th>状态</th>
<th>执行消息</th>
<th>创建时间</th>
<th>结果文件</th>
</tr>
</thead>
<tbody>
{% for result in results %}
<tr>
<td>{{ result.id }}</td>
<td>{{ result.client.name }}</td>
<td>
<span class="badge bg-{{
'success' if result.status == 'success' else
'danger' if result.status in ['failed', 'timeout'] else
'warning' if result.status in ['assigned', 'running', 'retrying'] else
'info'
}}">
{{ result.get_status_display }}
</span>
</td>
<td>{{ result.message|default:'无消息' }}</td>
<td>{{ result.created_at|date:'Y-m-d H:i:s' }}</td>
<td>
{% if result.result_file %}
<a href="/api/task_results/{{ result.id }}/download/" class="btn btn-sm btn-success">下载</a>
{% else %}
无文件
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% else %}
<p class="text-center text-muted">暂无执行结果</p>
{% endif %}
</div>
</div>
<div class="mt-4">
<a href="{% url 'task_list' %}" class="btn btn-secondary">返回任务列表</a>
</div>
{% endblock %}

View File

@@ -0,0 +1,48 @@
{% extends 'tasks/base.html' %}
{% block content %}
<h1 class="mb-4">任务列表</h1>
<div class="card mb-4">
<div class="card-header">
<a href="{% url 'task_create' %}" class="btn btn-primary">创建新任务</a>
</div>
<div class="card-body">
<table class="table table-striped">
<thead>
<tr>
<th>ID</th>
<th>任务名称</th>
<th>指定客户端</th>
<th>状态</th>
<th>创建时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{% for task in tasks %}
<tr>
<td>{{ task.id }}</td>
<td>{{ task.name }}</td>
<td>{{ task.client_name|default:'未指定' }}</td>
<td>
<span class="badge bg-{{
'success' if task.status == 'success' else
'danger' if task.status in ['failed', 'timeout'] else
'warning' if task.status in ['assigned', 'running', 'retrying'] else
'info'
}}">
{{ task.get_status_display }}
</span>
</td>
<td>{{ task.created_at|date:'Y-m-d H:i' }}</td>
<td>
<a href="{% url 'task_detail' task.id %}" class="btn btn-sm btn-info">详情</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

3
tasks/tests.py Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

0
tasks/tests/__init__.py Normal file
View File

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

128
tasks/tests/test_api.py Normal file
View File

@@ -0,0 +1,128 @@
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APIClient
from tasks.models import Client, Task, TaskResult
from tasks.tests.test_factories import ClientFactory, TaskFactory, TaskResultFactory
class TaskAPITest(TestCase):
def setUp(self):
self.client = APIClient()
# Create a test client with token
self.test_client = ClientFactory()
self.token = self.test_client.token
# Authenticate the client
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}')
def test_task_list(self):
"""Test that authenticated users can list tasks"""
# Create some test tasks
TaskFactory.create_batch(3)
url = reverse('task-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 3)
def test_task_create(self):
"""Test that authenticated users can create tasks"""
url = reverse('task-list')
data = {
'name': 'test_task',
'client_name': 'test_client',
'script': 'echo "Hello World"',
'timeout_seconds': 3600
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Task.objects.count(), 1)
self.assertEqual(Task.objects.get().name, 'test_task')
def test_task_claim(self):
"""Test that clients can claim available tasks"""
# Create a pending task
TaskFactory(client_name='test_client')
url = reverse('task-claim')
data = {
'client_name': 'test_client'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data['status'], 'assigned')
self.assertEqual(response.data['assigned_to'], 'test_client')
def test_unauthenticated_access(self):
"""Test that unauthenticated users cannot access API endpoints"""
# Create a new client without authentication
unauth_client = APIClient()
url = reverse('task-list')
response = unauth_client.get(url)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
class ClientAPITest(TestCase):
def setUp(self):
self.client = APIClient()
# Create a test client with token
self.test_client = ClientFactory()
self.token = self.test_client.token
# Authenticate the client
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}')
def test_client_list(self):
"""Test that authenticated users can list clients"""
# Create some test clients
ClientFactory.create_batch(3)
url = reverse('client-list')
response = self.client.get(url)
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(len(response.data), 4) # Including the test client
def test_client_create(self):
"""Test that authenticated users can create clients"""
url = reverse('client-list')
data = {
'name': 'new_client'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(Client.objects.count(), 2) # Including the test client
self.assertEqual(Client.objects.get(id=response.data['id']).name, 'new_client')
class TaskResultAPITest(TestCase):
def setUp(self):
self.client = APIClient()
# Create a test client with token
self.test_client = ClientFactory()
self.token = self.test_client.token
# Authenticate the client
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}')
# Create a test task
self.task = TaskFactory()
def test_task_result_create(self):
"""Test that authenticated users can create task results"""
url = reverse('taskresult-list')
data = {
'task': self.task.id,
'client': self.test_client.id,
'status': 'success',
'message': 'Task completed successfully'
}
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(TaskResult.objects.count(), 1)
self.assertEqual(TaskResult.objects.get().status, 'success')

View File

@@ -0,0 +1,38 @@
import factory
from django.utils import timezone
from tasks.models import Client, Task, TaskResult
class ClientFactory(factory.django.DjangoModelFactory):
class Meta:
model = Client
name = factory.Sequence(lambda n: f"client_{n}")
token = factory.Faker('uuid4')
last_seen = timezone.now()
created_at = timezone.now()
class TaskFactory(factory.django.DjangoModelFactory):
class Meta:
model = Task
name = factory.Sequence(lambda n: f"task_{n}")
client_name = factory.Sequence(lambda n: f"client_{n}")
script = factory.Faker('text')
status = 'pending'
timeout_seconds = 3600 # 1 hour
created_at = timezone.now()
updated_at = timezone.now()
assigned_to = None
started_at = None
completed_at = None
class TaskResultFactory(factory.django.DjangoModelFactory):
class Meta:
model = TaskResult
task = factory.SubFactory(TaskFactory)
client = factory.SubFactory(ClientFactory)
result_file = None
status = 'success'
message = factory.Faker('text')
created_at = timezone.now()

View File

@@ -0,0 +1,89 @@
from django.test import TestCase
from django.urls import reverse
from django.utils import timezone
from rest_framework import status
from rest_framework.test import APIClient
from tasks.models import Client, Task, TaskResult
from tasks.tests.test_factories import ClientFactory, TaskFactory
class TaskFlowIntegrationTest(TestCase):
def setUp(self):
self.client = APIClient()
# Create a test client with token
self.test_client = ClientFactory()
self.token = self.test_client.token
# Authenticate the client
self.client.credentials(HTTP_AUTHORIZATION=f'Token {self.token}')
def test_full_task_flow(self):
"""Test the full task flow: create → claim → start → complete"""
# 1. Create a task
create_url = reverse('task-list')
create_data = {
'name': 'integration_test_task',
'client_name': self.test_client.name,
'script': 'echo "Integration Test"',
'timeout_seconds': 3600
}
create_response = self.client.post(create_url, create_data, format='json')
self.assertEqual(create_response.status_code, status.HTTP_201_CREATED)
task_id = create_response.data['id']
# 2. Claim the task
claim_url = reverse('task-claim')
claim_data = {
'client_name': self.test_client.name
}
claim_response = self.client.post(claim_url, claim_data, format='json')
self.assertEqual(claim_response.status_code, status.HTTP_200_OK)
self.assertEqual(claim_response.data['status'], 'assigned')
self.assertEqual(claim_response.data['assigned_to'], self.test_client.name)
# 3. Start the task
start_url = reverse('task-start', args=[task_id])
start_response = self.client.post(start_url, format='json')
self.assertEqual(start_response.status_code, status.HTTP_200_OK)
self.assertEqual(start_response.data['status'], 'running')
self.assertIsNotNone(start_response.data['started_at'])
# 4. Complete the task
complete_url = reverse('task-complete', args=[task_id])
complete_data = {
'status': 'success',
'message': 'Task completed successfully'
}
complete_response = self.client.post(complete_url, complete_data, format='json')
self.assertEqual(complete_response.status_code, status.HTTP_200_OK)
self.assertEqual(complete_response.data['status'], 'success')
self.assertIsNotNone(complete_response.data['completed_at'])
# 5. Verify the task status in database
task = Task.objects.get(id=task_id)
self.assertEqual(task.status, 'success')
self.assertEqual(task.assigned_to, self.test_client.name)
self.assertIsNotNone(task.started_at)
self.assertIsNotNone(task.completed_at)
def test_task_result_upload(self):
"""Test that a client can upload task results"""
# Create a test task
task = TaskFactory()
# Upload a task result
upload_url = reverse('taskresult-list')
upload_data = {
'task': task.id,
'client': self.test_client.id,
'status': 'success',
'message': 'Result uploaded successfully'
}
upload_response = self.client.post(upload_url, upload_data, format='json')
self.assertEqual(upload_response.status_code, status.HTTP_201_CREATED)
# Verify the result exists in database
self.assertEqual(TaskResult.objects.count(), 1)
result = TaskResult.objects.get()
self.assertEqual(result.task, task)
self.assertEqual(result.client, self.test_client)
self.assertEqual(result.status, 'success')

109
tasks/tests/test_models.py Normal file
View File

@@ -0,0 +1,109 @@
from django.test import TestCase
from django.utils import timezone
from tasks.models import Client, Task, TaskResult
class ClientModelTest(TestCase):
def test_client_creation(self):
"""Test that a client can be created with all required fields"""
client = Client.objects.create(
name="test_client",
token="test_token_12345"
)
self.assertEqual(client.name, "test_client")
self.assertEqual(client.token, "test_token_12345")
self.assertIsNotNone(client.created_at)
self.assertIsNotNone(client.last_seen)
def test_client_str(self):
"""Test that the client string representation is correct"""
client = Client.objects.create(
name="test_client",
token="test_token_12345"
)
self.assertEqual(str(client), "test_client")
class TaskModelTest(TestCase):
def test_task_creation(self):
"""Test that a task can be created with all required fields"""
task = Task.objects.create(
name="test_task",
client_name="test_client",
script="echo 'Hello World'",
timeout_seconds=3600
)
self.assertEqual(task.name, "test_task")
self.assertEqual(task.client_name, "test_client")
self.assertEqual(task.script, "echo 'Hello World'")
self.assertEqual(task.status, "pending")
self.assertEqual(task.timeout_seconds, 3600)
self.assertIsNotNone(task.created_at)
self.assertIsNotNone(task.updated_at)
def test_task_str(self):
"""Test that the task string representation is correct"""
task = Task.objects.create(
name="test_task"
)
self.assertEqual(str(task), "test_task")
def test_task_status_choices(self):
"""Test that task status choices are correctly implemented"""
from tasks.models import STATUS_CHOICES
status_choices = [choice[0] for choice in STATUS_CHOICES]
self.assertIn("pending", status_choices)
self.assertIn("assigned", status_choices)
self.assertIn("running", status_choices)
self.assertIn("success", status_choices)
self.assertIn("failed", status_choices)
self.assertIn("retrying", status_choices)
self.assertIn("timeout", status_choices)
class TaskResultModelTest(TestCase):
def test_task_result_creation(self):
"""Test that a task result can be created with all required fields"""
client = Client.objects.create(
name="test_client",
token="test_token_12345"
)
task = Task.objects.create(
name="test_task"
)
result = TaskResult.objects.create(
task=task,
client=client,
status="success",
message="Task completed successfully"
)
self.assertEqual(result.task, task)
self.assertEqual(result.client, client)
self.assertEqual(result.status, "success")
self.assertEqual(result.message, "Task completed successfully")
self.assertIsNotNone(result.created_at)
def test_task_result_str(self):
"""Test that the task result string representation is correct"""
client = Client.objects.create(
name="test_client",
token="test_token_12345"
)
task = Task.objects.create(
name="test_task"
)
result = TaskResult.objects.create(
task=task,
client=client,
status="success"
)
# Expected status display is in Chinese
self.assertEqual(str(result), f"{task.name} - {client.name} - 成功")

60
tasks/tests/test_views.py Normal file
View File

@@ -0,0 +1,60 @@
from django.test import TestCase
from django.urls import reverse
from tasks.models import Client, Task
from tasks.tests.test_factories import ClientFactory, TaskFactory
class TaskViewTest(TestCase):
def test_task_list_view(self):
"""Test that the task list view renders correctly"""
# Create some test tasks
TaskFactory.create_batch(3)
url = reverse('task_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tasks/task_list.html')
self.assertContains(response, '任务列表')
def test_task_create_view(self):
"""Test that the task create view renders correctly"""
url = reverse('task_create')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tasks/task_create.html')
self.assertContains(response, '创建任务')
def test_task_detail_view(self):
"""Test that the task detail view renders correctly"""
# Create a test task
task = TaskFactory()
url = reverse('task_detail', args=[task.id])
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tasks/task_detail.html')
self.assertContains(response, task.name)
class ClientViewTest(TestCase):
def test_client_list_view(self):
"""Test that the client list view renders correctly"""
# Create some test clients
ClientFactory.create_batch(3)
url = reverse('client_list')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tasks/client_list.html')
self.assertContains(response, '客户端列表')
def test_client_create_view(self):
"""Test that the client create view renders correctly"""
url = reverse('client_create')
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, 'tasks/client_create.html')
self.assertContains(response, '创建客户端')

23
tasks/urls.py Normal file
View File

@@ -0,0 +1,23 @@
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from .views import ClientViewSet, TaskViewSet, TaskResultViewSet
from .views_frontend import (
index, task_list, task_create, task_detail,
client_list, client_create
)
router = DefaultRouter()
router.register(r'clients', ClientViewSet)
router.register(r'tasks', TaskViewSet)
router.register(r'task_results', TaskResultViewSet)
urlpatterns = [
path('', include(router.urls)),
# Frontend views
path('index/', index, name='index'),
path('tasks/list/', task_list, name='task_list'),
path('tasks/create/', task_create, name='task_create'),
path('tasks/<int:task_id>/', task_detail, name='task_detail'),
path('clients/list/', client_list, name='client_list'),
path('clients/create/', client_create, name='client_create'),
]

105
tasks/views.py Normal file
View File

@@ -0,0 +1,105 @@
from django.shortcuts import get_object_or_404
from django.utils import timezone
from rest_framework import viewsets, status
from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
from .models import Client, Task, TaskResult
from .serializers import (
ClientSerializer,
TaskSerializer,
TaskResultSerializer,
TaskClaimSerializer,
TaskStartSerializer,
TaskCompleteSerializer
)
class ClientViewSet(viewsets.ModelViewSet):
queryset = Client.objects.all()
serializer_class = ClientSerializer
permission_classes = [IsAuthenticated]
class TaskViewSet(viewsets.ModelViewSet):
queryset = Task.objects.all()
serializer_class = TaskSerializer
permission_classes = [IsAuthenticated]
@action(detail=False, methods=['post'], serializer_class=TaskClaimSerializer)
def claim(self, request):
"""Client claims an available task"""
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
client_name = serializer.validated_data['client_name']
# Try to find a pending task, either assigned to this client or unassigned
with self.queryset.model.objects._base_manager._db.transaction.atomic():
# First try to find tasks assigned to this client
task = Task.objects.filter(
status='pending',
client_name=client_name
).select_for_update().first()
# If no task assigned to this client, try to find unassigned tasks
if not task:
task = Task.objects.filter(
status='pending',
client_name__isnull=True
).select_for_update().first()
if not task:
return Response({'detail': 'No available tasks'}, status=status.HTTP_404_NOT_FOUND)
# Assign the task to the client
task.status = 'assigned'
task.assigned_to = client_name
task.save()
return Response(TaskSerializer(task).data, status=status.HTTP_200_OK)
@action(detail=True, methods=['post'], serializer_class=TaskStartSerializer)
def start(self, request, pk=None):
"""Client starts a task"""
task = self.get_object()
if task.status != 'assigned':
return Response({'detail': 'Task is not assigned'}, status=status.HTTP_400_BAD_REQUEST)
task.status = 'running'
task.started_at = timezone.now()
task.save()
return Response(TaskSerializer(task).data, status=status.HTTP_200_OK)
@action(detail=True, methods=['post'], serializer_class=TaskCompleteSerializer)
def complete(self, request, pk=None):
"""Client completes a task"""
task = self.get_object()
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
if task.status != 'running':
return Response({'detail': 'Task is not running'}, status=status.HTTP_400_BAD_REQUEST)
task.status = serializer.validated_data['status']
task.completed_at = timezone.now()
task.save()
return Response(TaskSerializer(task).data, status=status.HTTP_200_OK)
class TaskResultViewSet(viewsets.ModelViewSet):
queryset = TaskResult.objects.all()
serializer_class = TaskResultSerializer
permission_classes = [IsAuthenticated]
parser_classes = [MultiPartParser, FormParser]
@action(detail=True, methods=['get'])
def download(self, request, pk=None):
"""Download task result file"""
task_result = self.get_object()
if not task_result.result_file:
return Response({'detail': 'No file available'}, status=status.HTTP_404_NOT_FOUND)
# Use Django's built-in FileResponse for downloading
from django.http import FileResponse
return FileResponse(task_result.result_file.open('rb'), as_attachment=True)

54
tasks/views_frontend.py Normal file
View File

@@ -0,0 +1,54 @@
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from .models import Task, Client, TaskResult
# Home view
def index(request):
return render(request, 'tasks/index.html')
# Task views
def task_list(request):
tasks = Task.objects.all()
return render(request, 'tasks/task_list.html', {'tasks': tasks})
def task_create(request):
if request.method == 'POST':
name = request.POST['name']
client_name = request.POST.get('client_name', '')
script = request.POST.get('script', '')
timeout_seconds = int(request.POST.get('timeout_seconds', 259200))
Task.objects.create(
name=name,
client_name=client_name if client_name else None,
script=script if script else None,
timeout_seconds=timeout_seconds
)
messages.success(request, '任务创建成功!')
return redirect('task_list')
return render(request, 'tasks/task_create.html')
def task_detail(request, task_id):
task = get_object_or_404(Task, id=task_id)
results = task.results.all()
return render(request, 'tasks/task_detail.html', {'task': task, 'results': results})
# Client views
def client_list(request):
clients = Client.objects.all()
return render(request, 'tasks/client_list.html', {'clients': clients})
def client_create(request):
if request.method == 'POST':
name = request.POST['name']
# Generate a simple token for demo purposes
import secrets
token = secrets.token_urlsafe(32)
Client.objects.create(
name=name,
token=token
)
messages.success(request, '客户端创建成功!')
return redirect('client_list')
return render(request, 'tasks/client_create.html')