Compare commits

..

17 Commits

Author SHA1 Message Date
54130d4401 docs: 更新README文档和修复模型复数名称
- 更新README文档,添加新功能描述和API端点详情
- 修复TaskResult模型的verbose_name_plural字段
2026-01-28 16:47:17 +08:00
a8e24fad63 Merge branch 'main' of http://14.103.237.41:16001/xiaji/central-task 2025-12-12 12:02:59 +08:00
97c0e0db0d 当创建一个客户端的时候,自动创建一个API Token 2025-12-12 11:55:36 +08:00
c4086b9f46 删除 'db.sqlite3' 2025-12-11 15:12:02 +08:00
d7c1d00ba1 删除pyc文件 2025-12-11 15:06:16 +08:00
ddcd490575 删除 'task_center/__pycache__/wsgi.cpython-311.pyc' 2025-12-11 15:05:01 +08:00
c71f8f7740 删除 'task_center/__pycache__/urls.cpython-311.pyc' 2025-12-11 15:04:56 +08:00
de0af70958 删除 'task_center/__pycache__/settings.cpython-311.pyc' 2025-12-11 15:04:50 +08:00
2630405dac 删除 'task_center/__pycache__/__init__.cpython-311.pyc' 2025-12-11 15:04:43 +08:00
98925f4cce 更新了admin的显示内容 2025-12-11 14:33:32 +08:00
becca344bf Merge branch 'main' of http://14.103.237.41:16001/xiaji/central-task 2025-12-11 13:29:54 +08:00
527cb1dc3e 修改了admin的入口地址 2025-12-11 13:17:19 +08:00
066475c487 开发用的 2025-12-10 17:21:30 +08:00
fabc106396 提交忽略的git内容 2025-12-10 17:19:59 +08:00
d4bcf322d7 更新了settings.py的内容,在开发环境和生产环境统一 2025-12-10 17:11:43 +08:00
82c17639d7 增加依赖文件 2025-12-10 16:22:38 +08:00
f7e529c711 增加windows下的任务管理客户端 2025-12-08 13:29:43 +08:00
23 changed files with 955 additions and 126 deletions

58
.gitignore vendored Normal file
View File

@@ -0,0 +1,58 @@
# Virtual environments
.env
venv/
.venv/
env/
.virtualenv/
virtualenv/
# Python cache
__pycache__/
*.pyc
*.pyo
*.pyd
# Database
*.sqlite3
*.db
# Media files
media/
# Static files
static/
# Environment variables
.env
.env.local
.env.*.local
# Logs
*.log
logs/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Testing
.coverage
htmlcov/
.pytest_cache/
.tox/
# Build
build/
dist/
*.egg-info/
# Other
*.bak
*.tmp

View File

@@ -1,6 +1,7 @@
# 任务中心管理系统最终实现计划
## 项目结构设计
1. 创建Django项目`task_center`
2. 创建任务管理应用:`tasks`
3. 配置数据库为SQLite
@@ -10,41 +11,45 @@
## 数据库模型设计
### Client模型
| 字段名 | 类型 | 描述 |
|-------|------|------|
| id | AutoField | 客户端ID |
| name | CharField(unique=True) | 客户端标识 |
| token | CharField(max_length=128) | API Token |
| last_seen | DateTimeField | 最后活跃时间 |
| created_at | DateTimeField | 创建时间 |
| 字段名 | 类型 | 描述 |
| ----------- | -------------------------- | --------- |
| 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) | 完成时间 |
| 字段名 | 类型 | 描述 |
| ---------------- | ----------------------------------------------------- | ------------------ |
| 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 | 创建时间 |
| 字段名 | 类型 | 描述 |
| ------------ | -------------------------------------- | ----- |
| 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', '待分配'),
@@ -60,35 +65,53 @@ STATUS_CHOICES = [
## API接口设计
### 认证机制
- **所有API端点均需token认证**
- 客户端通过HTTP头`Authorization: Token <token>`进行身份验证
- 使用Django REST Framework的TokenAuthentication
- 未提供有效token的请求将返回401 Unauthorized
* **所有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>/` - 删除任务(需认证)
* `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/` - 上传任务结果(需认证,支持文件上传
* `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>/` - 获取客户端详情(需认证)
* `GET /api/clients/` - 获取客户端列表(需认证)
* `POST /api/clients/` - 创建客户端(需认证)
* `GET /api/clients/<id>/` - 获取客户端详情(需认证)
### 文件下载API
- `GET /api/task_results/<id>/download/` - 下载任务结果文件(需认证)
* `GET /api/task_results/<id>/download/` - 下载任务结果文件(需认证)
## 文件上传实现细节
### 1. Django媒体文件配置
```python
# settings.py
MEDIA_URL = '/media/'
@@ -96,28 +119,43 @@ 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`
* 使用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样式美化文件上传控件
- 支持进度条显示(可选)
* 使用HTML5的`input type="file"`元素
* 表单设置`enctype="multipart/form-data"`
* 使用Bootstrap样式美化文件上传控件
* 支持进度条显示(可选)
### 4. 文件存储策略
- 本地文件系统存储:`media/task_results/`目录
- 文件名自动生成,避免冲突
- 支持大文件上传通过Django默认配置
* 本地文件系统存储:`media/task_results/`目录
* 文件名自动生成,避免冲突
* 支持大文件上传通过Django默认配置
### 5. 文件下载实现
- API端点`GET /api/task_results/<id>/download/`
- 返回`Content-Disposition: attachment`头,触发浏览器下载
- 支持断点续传通过Django默认配置
* API端点`GET /api/task_results/<id>/download/`
* 返回`Content-Disposition: attachment`头,触发浏览器下载
* 支持断点续传通过Django默认配置
## 前端页面设计
1. 任务列表页:展示所有任务,支持筛选和搜索
2. 任务创建页:表单创建新任务
3. 任务详情页:查看任务详情和执行结果历史
@@ -128,26 +166,37 @@ MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
## 核心功能实现
### 1. 全API Token认证
- 所有API视图均使用`TokenAuthentication`
- 配置`DEFAULT_AUTHENTICATION_CLASSES``DEFAULT_PERMISSION_CLASSES`
* 所有API视图均使用`TokenAuthentication`
* 配置`DEFAULT_AUTHENTICATION_CLASSES``DEFAULT_PERMISSION_CLASSES`
### 2. 原子任务认领机制
- 使用数据库事务确保任务认领的原子性
- 客户端调用`/api/tasks/claim/`时,系统自动查找并分配可用任务
* 使用数据库事务确保任务认领的原子性
* 客户端调用`/api/tasks/claim/`时,系统自动查找并分配可用任务
### 3. 任务超时自动回收
- 使用Django管理命令定期检查超时任务
- 超过`timeout_seconds`的任务自动设置为`timeout`状态
- 管理命令:`python manage.py check_task_timeouts`
* 使用Django管理命令定期检查超时任务
* 超过`timeout_seconds`的任务自动设置为`timeout`状态
* 管理命令:`python manage.py check_task_timeouts`
### 4. 任务结果版本管理
- TaskResult模型记录每次执行结果
- 支持查看任务的完整执行历史
- 支持下载不同版本的结果文件
* TaskResult模型记录每次执行结果
* 支持查看任务的完整执行历史
* 支持下载不同版本的结果文件
## 测试用例设计
### 目录结构
```
task_center/
├── tasks/
@@ -164,14 +213,16 @@ task_center/
```
### 测试类型
1. **模型测试**:测试模型字段、方法和关系
2. **API测试**测试所有API端点的功能和认证机制
3. **文件上传测试**:测试文件上传和下载功能
4. **视图测试**:测试前端视图渲染
5. **集成测试**:测试完整的任务流程
6. **工厂测试**使用factory_boy创建测试数据
6. **工厂测试**使用factory\_boy创建测试数据
## 实现步骤
1. 创建Django项目和应用
2. 配置项目设置数据库、REST Framework、认证、媒体文件等
3. 实现数据库模型
@@ -183,18 +234,32 @@ task_center/
9. 测试API接口和功能
## 技术栈
- 后端Django 5.0.6 + Django REST Framework
- 端:HTML + Bootstrap 5
- 数据库SQLite
- 文件存储:本地文件系统
- 测试pytest + factory_boy
* 端:Django 5.0.6 + Django REST Framework
* 前端HTML + Bootstrap 5
* 数据库SQLite
* 文件存储:本地文件系统
* 测试pytest + factory\_boy
## 预期效果
- 所有API端点均需要token认证
- 支持结果文件的上传和下载功能
- 后台可通过admin或API创建和管理任务
- 客户端通过API进行身份验证原子认领任务
- 支持任务超时自动回收
- 完整的任务执行历史记录
- 简单大方的Bootstrap前端界面
- 全面的测试用例覆盖
* 所有API端点均需要token认证
* 支持结果文件的上传和下载功能
* 后台可通过admin或API创建和管理任务
* 客户端通过API进行身份验证原子认领任务
* 支持任务超时自动回收
* 完整的任务执行历史记录
* 简单大方的Bootstrap前端界面
* 全面的测试用例覆盖

View File

@@ -9,21 +9,30 @@
- 设置任务执行客户端(指定或自动分配)
- 配置任务超时时间
- 支持任务脚本存储
- 完整的任务生命周期管理
### 2. 客户端管理
- 客户端注册和身份验证
- 自动生成API Token
- 自动生成安全的API Token
- 客户端活跃状态跟踪
### 3. 任务执行流程
- **原子任务认领**:避免竞态条件
- **原子任务认领**使用数据库事务确保并发安全,避免竞态条件
- 任务状态跟踪:待分配、已分配、执行中、成功、失败、重试中、超时
- 任务结果上传和下载
- 完整的任务执行API认领、开始、完成
### 4. 系统管理
- 完整的后台管理界面
- 任务超时自动回收
- 结果文件管理
- API权限控制所有API端点均需要认证
### 5. 前端界面
- 响应式设计基于Bootstrap 5
- 任务管理:列表、创建、详情
- 客户端管理:列表、创建
- 结果查看和下载
## 技术栈
@@ -91,7 +100,7 @@ python manage.py runserver
#### 1.2 管理客户端
- 在左侧菜单栏点击 "客户端"
- 点击 "添加客户端" 创建新客户端
- 系统会自动生成API Token
- 系统会自动生成安全的API Token
#### 1.3 管理任务
- 在左侧菜单栏点击 "任务"
@@ -127,19 +136,30 @@ python manage.py runserver
Authorization: Token <your-token>
```
**注意**所有API端点均需要认证使用 `IsAuthenticated` 权限类控制。
#### 3.2 主要API端点
| 端点 | 方法 | 功能 |
|------|------|------|
| `/api/clients/` | GET | 获取客户端列表 |
| `/api/clients/` | POST | 创建客户端 |
| `/api/tasks/` | GET | 获取任务列表 |
| `/api/tasks/` | POST | 创建任务 |
| `/api/tasks/claim/` | POST | 客户端认领任务 |
| `/api/tasks/<id>/start/` | POST | 开始执行任务 |
| `/api/tasks/<id>/complete/` | POST | 完成任务 |
| `/api/task_results/` | POST | 上传任务结果 |
| `/api/task_results/<id>/download/` | GET | 下载结果文件 |
| 端点 | 方法 | 功能 | 权限 |
|------|------|------|------|
| `/api/clients/` | GET | 获取客户端列表 | 需要认证 |
| `/api/clients/` | POST | 创建客户端 | 需要认证 |
| `/api/clients/<id>/` | GET | 获取客户端详情 | 需要认证 |
| `/api/clients/<id>/` | PUT | 更新客户端 | 需要认证 |
| `/api/clients/<id>/` | DELETE | 删除客户端 | 需要认证 |
| `/api/tasks/` | GET | 获取任务列表 | 需要认证 |
| `/api/tasks/` | POST | 创建任务 | 需要认证 |
| `/api/tasks/<id>/` | GET | 获取任务详情 | 需要认证 |
| `/api/tasks/<id>/` | PUT | 更新任务 | 需要认证 |
| `/api/tasks/<id>/` | DELETE | 删除任务 | 需要认证 |
| `/api/tasks/claim/` | POST | 客户端认领任务 | 需要认证 |
| `/api/tasks/<id>/start/` | POST | 开始执行任务 | 需要认证 |
| `/api/tasks/<id>/complete/` | POST | 完成任务 | 需要认证 |
| `/api/task_results/` | GET | 获取任务结果列表 | 需要认证 |
| `/api/task_results/` | POST | 上传任务结果 | 需要认证 |
| `/api/task_results/<id>/` | GET | 获取任务结果详情 | 需要认证 |
| `/api/task_results/<id>/` | DELETE | 删除任务结果 | 需要认证 |
| `/api/task_results/<id>/download/` | GET | 下载结果文件 | 需要认证 |
#### 3.3 API使用示例
@@ -159,6 +179,20 @@ curl -X POST http://127.0.0.1:8000/api/tasks/claim/ \
-d '{"client_name":"client1"}'
```
##### 开始执行任务
```bash
curl -X POST http://127.0.0.1:8000/api/tasks/1/start/ \
-H "Authorization: Token <token>"
```
##### 完成任务
```bash
curl -X POST http://127.0.0.1:8000/api/tasks/1/complete/ \
-H "Authorization: Token <token>" \
-H "Content-Type: application/json" \
-d '{"status":"success","message":"任务执行成功"}'
```
##### 上传任务结果
```bash
curl -X POST http://127.0.0.1:8000/api/task_results/ \
@@ -170,22 +204,31 @@ curl -X POST http://127.0.0.1:8000/api/task_results/ \
-F "result_file=@result.txt"
```
##### 下载任务结果文件
```bash
curl -X GET http://127.0.0.1:8000/api/task_results/1/download/ \
-H "Authorization: Token <token>" \
-o result.txt
```
## 项目结构
```
task_center/
任务中心/
├── task_center/ # 项目配置
│ ├── settings.py # 项目设置
│ └── urls.py # 主URL配置
├── tasks/ # 任务应用
│ ├── models.py # 数据库模型
│ ├── serializers.py # API序列化器
│ ├── views.py # API视图
│ ├── views.py # API视图使用IsAuthenticated权限
│ ├── views_frontend.py # 前端视图
│ ├── urls.py # 应用URL配置
│ ├── templates/ # 前端模板
│ ├── tests/ # 测试用例
│ └── management/ # 管理命令
├── media/ # 媒体文件存储
│ └── task_results/ # 任务结果文件
└── manage.py # 项目入口
```
@@ -199,15 +242,20 @@ python manage.py check_task_timeouts
建议将此命令添加到系统定时任务中,例如每小时执行一次。
### 命令功能说明
- **check_task_timeouts**:检查所有执行中任务,将超过超时时间的任务标记为超时状态,使其可以被重新分配执行。
## 注意事项
1. **文件存储**:结果文件存储在 `media/task_results/` 目录下,请确保该目录有写入权限
2. **API Token安全**请妥善保管客户端API Token避免泄露
3. **生产环境配置**
3. **API权限控制**所有API端点均需要认证使用 `IsAuthenticated` 权限类控制
4. **生产环境配置**
- 建议使用PostgreSQL或MySQL数据库
- 配置合适的文件存储后端如S3
- 启用HTTPS
- 配置适当的日志记录
- 实现更细粒度的权限控制(如基于角色的权限)
## 许可证
@@ -233,6 +281,13 @@ python -m pytest --ds=task_center.settings
## 更新日志
### v1.1.0
- 增强API权限控制所有API端点均使用 `IsAuthenticated` 权限类
- 完善API文档更新API端点描述和使用示例
- 优化前端界面:改进任务和客户端管理功能
- 增强任务执行流程完善认领、开始、完成的API实现
- 改进项目结构文档:添加媒体文件存储目录说明
### v1.0.0
- 初始版本发布
- 完成任务管理核心功能

568
client/windows-client.py Normal file
View File

@@ -0,0 +1,568 @@
import sys
import json
import requests
from PySide6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget,
QGroupBox, QLabel, QLineEdit, QPushButton, QTextEdit, QTableWidget,
QTableWidgetItem, QComboBox, QSpinBox, QFileDialog, QMessageBox,
QStatusBar, QHeaderView, QFormLayout, QCheckBox
)
from PySide6.QtCore import Qt, QTimer, QThread, Signal
from PySide6.QtGui import QIcon
# API通信类
class ApiClient:
def __init__(self):
self.base_url = "http://127.0.0.1:8000/api"
self.token = ""
def set_server(self, base_url):
self.base_url = base_url.rstrip('/')
def set_token(self, token):
self.token = token
def _get_headers(self):
return {
"Authorization": f"Token {self.token}",
"Content-Type": "application/json"
}
def get_tasks(self):
try:
response = requests.get(f"{self.base_url}/tasks/", headers=self._get_headers())
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"获取任务失败: {str(e)}")
def create_task(self, name, client_name, script, timeout_seconds):
try:
data = {
"name": name,
"client_name": client_name or None,
"script": script or None,
"timeout_seconds": timeout_seconds
}
response = requests.post(f"{self.base_url}/tasks/", headers=self._get_headers(), json=data)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"创建任务失败: {str(e)}")
def claim_task(self, client_name):
try:
data = {"client_name": client_name}
response = requests.post(f"{self.base_url}/tasks/claim/", headers=self._get_headers(), json=data)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"认领任务失败: {str(e)}")
def start_task(self, task_id):
try:
response = requests.post(f"{self.base_url}/tasks/{task_id}/start/", headers=self._get_headers())
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"开始任务失败: {str(e)}")
def complete_task(self, task_id, status, message):
try:
data = {
"status": status,
"message": message
}
response = requests.post(f"{self.base_url}/tasks/{task_id}/complete/", headers=self._get_headers(), json=data)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"完成任务失败: {str(e)}")
def upload_result(self, task_id, client_id, status, message, result_file=None):
try:
headers = {
"Authorization": f"Token {self.token}"
}
data = {
"task": task_id,
"client": client_id,
"status": status,
"message": message
}
files = {}
if result_file:
files["result_file"] = open(result_file, "rb")
response = requests.post(f"{self.base_url}/task_results/", headers=headers, data=data, files=files)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"上传结果失败: {str(e)}")
# 任务列表更新线程
class TaskUpdateThread(QThread):
update_signal = Signal(list)
error_signal = Signal(str)
def __init__(self, api_client):
super().__init__()
self.api_client = api_client
self.running = True
def run(self):
while self.running:
try:
tasks = self.api_client.get_tasks()
self.update_signal.emit(tasks)
except Exception as e:
self.error_signal.emit(str(e))
self.sleep(5) # 每5秒更新一次
def stop(self):
self.running = False
self.wait()
# 主应用窗口
class TaskCenterClient(QMainWindow):
def __init__(self):
super().__init__()
self.api_client = ApiClient()
self.task_update_thread = None
self.init_ui()
def init_ui(self):
self.setWindowTitle("任务中心客户端")
self.setGeometry(100, 100, 1000, 700)
# 创建中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
# 创建标签页
self.tabs = QTabWidget()
main_layout.addWidget(self.tabs)
# 添加配置标签页
self.create_config_tab()
# 添加任务列表标签页
self.create_task_list_tab()
# 添加任务创建标签页
self.create_task_create_tab()
# 添加任务操作标签页
self.create_task_operation_tab()
# 创建状态栏
self.status_bar = QStatusBar()
self.setStatusBar(self.status_bar)
self.status_bar.showMessage("已连接到服务器")
def create_config_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# 服务器配置
server_group = QGroupBox("服务器配置")
server_layout = QFormLayout()
self.server_url_edit = QLineEdit("http://127.0.0.1:8000/api")
server_layout.addRow(QLabel("服务器地址:"), self.server_url_edit)
self.token_edit = QLineEdit()
self.token_edit.setEchoMode(QLineEdit.Password)
server_layout.addRow(QLabel("API Token:"), self.token_edit)
# 显示/隐藏Token按钮
self.token_visibility_btn = QPushButton("显示")
self.token_visibility_btn.clicked.connect(self.toggle_token_visibility)
server_layout.addRow(QLabel(" "), self.token_visibility_btn)
self.save_config_btn = QPushButton("保存配置")
self.save_config_btn.clicked.connect(self.save_config)
server_layout.addRow(QLabel(" "), self.save_config_btn)
server_group.setLayout(server_layout)
layout.addWidget(server_group)
# 测试连接按钮
self.test_connection_btn = QPushButton("测试连接")
self.test_connection_btn.clicked.connect(self.test_connection)
layout.addWidget(self.test_connection_btn)
# 连接状态
self.connection_status = QLabel("未连接")
self.connection_status.setStyleSheet("color: red;")
layout.addWidget(self.connection_status, alignment=Qt.AlignCenter)
self.tabs.addTab(tab, "配置")
def create_task_list_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# 刷新按钮
refresh_layout = QHBoxLayout()
self.refresh_tasks_btn = QPushButton("刷新任务列表")
self.refresh_tasks_btn.clicked.connect(self.refresh_tasks)
refresh_layout.addWidget(self.refresh_tasks_btn)
self.auto_refresh_checkbox = QCheckBox("自动刷新")
self.auto_refresh_checkbox.stateChanged.connect(self.toggle_auto_refresh)
refresh_layout.addWidget(self.auto_refresh_checkbox)
layout.addLayout(refresh_layout)
# 任务列表
self.task_table = QTableWidget()
self.task_table.setColumnCount(8)
self.task_table.setHorizontalHeaderLabels([
"ID", "名称", "指定客户端", "实际客户端", "状态",
"创建时间", "开始时间", "完成时间"
])
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)
self.task_table.horizontalHeader().setStretchLastSection(True)
self.task_table.cellClicked.connect(self.on_task_selected)
layout.addWidget(self.task_table)
# 任务详情
self.task_detail = QTextEdit()
self.task_detail.setReadOnly(True)
self.task_detail.setPlaceholderText("点击任务查看详情")
layout.addWidget(QLabel("任务详情:"))
layout.addWidget(self.task_detail)
self.tabs.addTab(tab, "任务列表")
def create_task_create_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# 任务创建表单
form_layout = QFormLayout()
self.task_name_edit = QLineEdit()
form_layout.addRow(QLabel("任务名称:"), self.task_name_edit)
self.task_client_edit = QLineEdit()
form_layout.addRow(QLabel("指定客户端:"), self.task_client_edit)
self.task_script_edit = QTextEdit()
self.task_script_edit.setPlaceholderText("输入执行脚本(可选)")
form_layout.addRow(QLabel("执行脚本:"), self.task_script_edit)
self.task_timeout_spin = QSpinBox()
self.task_timeout_spin.setMinimum(60)
self.task_timeout_spin.setMaximum(86400)
self.task_timeout_spin.setValue(3600)
form_layout.addRow(QLabel("超时时间(秒):"), self.task_timeout_spin)
self.create_task_btn = QPushButton("创建任务")
self.create_task_btn.clicked.connect(self.create_task)
form_layout.addRow(QLabel(" "), self.create_task_btn)
layout.addLayout(form_layout)
# 创建结果
self.create_result = QTextEdit()
self.create_result.setReadOnly(True)
self.create_result.setPlaceholderText("创建任务结果将显示在这里")
layout.addWidget(QLabel("创建结果:"))
layout.addWidget(self.create_result)
self.tabs.addTab(tab, "创建任务")
def create_task_operation_tab(self):
tab = QWidget()
layout = QVBoxLayout(tab)
# 任务选择
task_select_layout = QHBoxLayout()
self.selected_task_id = QLineEdit()
self.selected_task_id.setPlaceholderText("输入任务ID")
task_select_layout.addWidget(QLabel("任务ID:"))
task_select_layout.addWidget(self.selected_task_id)
self.select_from_list_btn = QPushButton("从列表选择")
self.select_from_list_btn.clicked.connect(self.select_task_from_list)
task_select_layout.addWidget(self.select_from_list_btn)
layout.addLayout(task_select_layout)
# 操作按钮组
operations_layout = QHBoxLayout()
self.claim_task_btn = QPushButton("认领任务")
self.claim_task_btn.clicked.connect(self.claim_task)
operations_layout.addWidget(self.claim_task_btn)
self.start_task_btn = QPushButton("开始任务")
self.start_task_btn.clicked.connect(self.start_task)
operations_layout.addWidget(self.start_task_btn)
self.complete_task_btn = QPushButton("完成任务")
self.complete_task_btn.clicked.connect(self.complete_task)
operations_layout.addWidget(self.complete_task_btn)
layout.addLayout(operations_layout)
# 完成任务表单
complete_layout = QFormLayout()
self.complete_status_combo = QComboBox()
self.complete_status_combo.addItems(["success", "failed"])
complete_layout.addRow(QLabel("执行状态:"), self.complete_status_combo)
self.complete_message_edit = QTextEdit()
self.complete_message_edit.setPlaceholderText("输入执行结果消息")
complete_layout.addRow(QLabel("执行消息:"), self.complete_message_edit)
# 文件上传
self.result_file_edit = QLineEdit()
self.result_file_edit.setReadOnly(True)
browse_btn = QPushButton("浏览")
browse_btn.clicked.connect(self.browse_result_file)
complete_layout.addRow(QLabel("结果文件:"), self.result_file_edit)
complete_layout.addRow(QLabel(" "), browse_btn)
layout.addLayout(complete_layout)
# 操作结果
self.operation_result = QTextEdit()
self.operation_result.setReadOnly(True)
self.operation_result.setPlaceholderText("操作结果将显示在这里")
layout.addWidget(QLabel("操作结果:"))
layout.addWidget(self.operation_result)
self.tabs.addTab(tab, "任务操作")
def toggle_token_visibility(self):
if self.token_edit.echoMode() == QLineEdit.Password:
self.token_edit.setEchoMode(QLineEdit.Normal)
self.token_visibility_btn.setText("隐藏")
else:
self.token_edit.setEchoMode(QLineEdit.Password)
self.token_visibility_btn.setText("显示")
def save_config(self):
server_url = self.server_url_edit.text()
token = self.token_edit.text()
if not server_url or not token:
QMessageBox.warning(self, "警告", "服务器地址和API Token不能为空")
return
# 保存配置到文件
try:
config = {
"server_url": server_url,
"token": token
}
with open("client_config.json", "w") as f:
json.dump(config, f)
QMessageBox.information(self, "成功", "配置已保存!")
self.api_client.set_server(server_url)
self.api_client.set_token(token)
except Exception as e:
QMessageBox.critical(self, "错误", f"保存配置失败: {str(e)}")
def test_connection(self):
server_url = self.server_url_edit.text()
token = self.token_edit.text()
if not server_url or not token:
QMessageBox.warning(self, "警告", "服务器地址和API Token不能为空")
return
try:
# 测试连接
temp_client = ApiClient()
temp_client.set_server(server_url)
temp_client.set_token(token)
tasks = temp_client.get_tasks()
QMessageBox.information(self, "成功", f"连接成功!找到 {len(tasks)} 个任务")
self.connection_status.setText("已连接")
self.connection_status.setStyleSheet("color: green;")
except Exception as e:
QMessageBox.critical(self, "错误", f"连接失败: {str(e)}")
self.connection_status.setText(f"连接失败: {str(e)}")
self.connection_status.setStyleSheet("color: red;")
def refresh_tasks(self):
try:
tasks = self.api_client.get_tasks()
self.update_task_table(tasks)
self.status_bar.showMessage(f"刷新完成,共 {len(tasks)} 个任务")
except Exception as e:
QMessageBox.critical(self, "错误", str(e))
self.status_bar.showMessage(f"刷新失败: {str(e)}")
def update_task_table(self, tasks):
self.task_table.setRowCount(len(tasks))
for row, task in enumerate(tasks):
self.task_table.setItem(row, 0, QTableWidgetItem(str(task["id"])))
self.task_table.setItem(row, 1, QTableWidgetItem(task["name"]))
self.task_table.setItem(row, 2, QTableWidgetItem(task["client_name"] or "未指定"))
self.task_table.setItem(row, 3, QTableWidgetItem(task["assigned_to"] or "未分配"))
self.task_table.setItem(row, 4, QTableWidgetItem(task["status"]))
self.task_table.setItem(row, 5, QTableWidgetItem(task["created_at"]))
self.task_table.setItem(row, 6, QTableWidgetItem(task["started_at"] or "-"))
self.task_table.setItem(row, 7, QTableWidgetItem(task["completed_at"] or "-"))
def toggle_auto_refresh(self, state):
if state == Qt.Checked:
# 启动自动刷新线程
self.task_update_thread = TaskUpdateThread(self.api_client)
self.task_update_thread.update_signal.connect(self.update_task_table)
self.task_update_thread.error_signal.connect(self.show_thread_error)
self.task_update_thread.start()
self.status_bar.showMessage("已启动自动刷新")
else:
# 停止自动刷新线程
if self.task_update_thread:
self.task_update_thread.stop()
self.task_update_thread = None
self.status_bar.showMessage("已停止自动刷新")
def show_thread_error(self, error_msg):
self.status_bar.showMessage(f"自动刷新失败: {error_msg}")
def on_task_selected(self, row, column):
task_id = self.task_table.item(row, 0).text()
self.selected_task_id.setText(task_id)
# 显示任务详情
task_name = self.task_table.item(row, 1).text()
client_name = self.task_table.item(row, 2).text()
assigned_to = self.task_table.item(row, 3).text()
status = self.task_table.item(row, 4).text()
created_at = self.task_table.item(row, 5).text()
started_at = self.task_table.item(row, 6).text()
completed_at = self.task_table.item(row, 7).text()
detail = f"任务ID: {task_id}\n"
detail += f"任务名称: {task_name}\n"
detail += f"指定客户端: {client_name}\n"
detail += f"实际客户端: {assigned_to}\n"
detail += f"状态: {status}\n"
detail += f"创建时间: {created_at}\n"
detail += f"开始时间: {started_at}\n"
detail += f"完成时间: {completed_at}\n"
self.task_detail.setPlainText(detail)
def toggle_token_visibility(self):
if self.token_edit.echoMode() == QLineEdit.Password:
self.token_edit.setEchoMode(QLineEdit.Normal)
self.token_visibility_btn.setText("隐藏")
else:
self.token_edit.setEchoMode(QLineEdit.Password)
self.token_visibility_btn.setText("显示")
def create_task(self):
name = self.task_name_edit.text()
client_name = self.task_client_edit.text()
script = self.task_script_edit.toPlainText()
timeout = self.task_timeout_spin.value()
if not name:
QMessageBox.warning(self, "警告", "任务名称不能为空!")
return
try:
result = self.api_client.create_task(name, client_name, script, timeout)
self.create_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False))
QMessageBox.information(self, "成功", "任务创建成功!")
# 清空表单
self.task_name_edit.clear()
self.task_client_edit.clear()
self.task_script_edit.clear()
self.task_timeout_spin.setValue(3600)
except Exception as e:
self.create_result.setPlainText(str(e))
QMessageBox.critical(self, "错误", str(e))
def claim_task(self):
task_id = self.selected_task_id.text()
if not task_id:
QMessageBox.warning(self, "警告", "请输入任务ID")
return
try:
result = self.api_client.claim_task(self.task_client_edit.text() or "default_client")
self.operation_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False))
QMessageBox.information(self, "成功", "任务认领成功!")
except Exception as e:
self.operation_result.setPlainText(str(e))
QMessageBox.critical(self, "错误", str(e))
def start_task(self):
task_id = self.selected_task_id.text()
if not task_id:
QMessageBox.warning(self, "警告", "请输入任务ID")
return
try:
result = self.api_client.start_task(task_id)
self.operation_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False))
QMessageBox.information(self, "成功", "任务开始成功!")
except Exception as e:
self.operation_result.setPlainText(str(e))
QMessageBox.critical(self, "错误", str(e))
def complete_task(self):
task_id = self.selected_task_id.text()
if not task_id:
QMessageBox.warning(self, "警告", "请输入任务ID")
return
status = self.complete_status_combo.currentText()
message = self.complete_message_edit.toPlainText()
try:
result = self.api_client.complete_task(task_id, status, message)
self.operation_result.setPlainText(json.dumps(result, indent=2, ensure_ascii=False))
QMessageBox.information(self, "成功", "任务完成成功!")
except Exception as e:
self.operation_result.setPlainText(str(e))
QMessageBox.critical(self, "错误", str(e))
def select_task_from_list(self):
selected_rows = self.task_table.selectedItems()
if selected_rows:
task_id = selected_rows[0].text()
self.selected_task_id.setText(task_id)
def browse_result_file(self):
file_path, _ = QFileDialog.getOpenFileName(self, "选择结果文件")
if file_path:
self.result_file_edit.setText(file_path)
def load_config(self):
try:
with open("client_config.json", "r") as f:
config = json.load(f)
self.server_url_edit.setText(config.get("server_url", "http://127.0.0.1:8000/api"))
self.token_edit.setText(config.get("token", ""))
self.api_client.set_server(config.get("server_url", "http://127.0.0.1:8000/api"))
self.api_client.set_token(config.get("token", ""))
except FileNotFoundError:
pass # 配置文件不存在,使用默认值
except Exception as e:
QMessageBox.warning(self, "警告", f"加载配置失败: {str(e)}")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = TaskCenterClient()
window.load_config()
window.show()
sys.exit(app.exec())

Binary file not shown.

15
requirements.txt Normal file
View File

@@ -0,0 +1,15 @@
# Django and REST Framework
django==5.0.6
djangorestframework==3.15.2
# Testing
pytest==8.3.2
pytest-django==4.11.1
factory_boy==3.3.3
# Client dependencies
pyside6==6.9.1
requests==2.32.3
# Other dependencies
python-dotenv==1.0.1

View File

@@ -10,7 +10,12 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.0/ref/settings/
"""
import os
from pathlib import Path
from dotenv import load_dotenv
# 加载.env文件
load_dotenv()
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -19,13 +24,14 @@ 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'
SECRET_KEY = os.getenv('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
DEBUG = os.getenv('DEBUG', 'True') == 'True'
ALLOWED_HOSTS = []
ALLOWED_HOSTS = os.getenv('ALLOWED_HOSTS', '').split(',')
# Application definition
@@ -105,9 +111,9 @@ AUTH_PASSWORD_VALIDATORS = [
# Internationalization
# https://docs.djangoproject.com/en/5.0/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'zh-hans'
TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Shanghai'
USE_I18N = True
@@ -117,7 +123,8 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.0/howto/static-files/
STATIC_URL = 'static/'
STATIC_URL = '/static/'
STATIC_ROOT = os.getenv('STATIC_ROOT', BASE_DIR / 'static')
# Default primary key field type
# https://docs.djangoproject.com/en/5.0/ref/settings/#default-auto-field
@@ -133,7 +140,7 @@ REST_FRAMEWORK = {
# Media files settings
MEDIA_URL = '/media/'
MEDIA_ROOT = BASE_DIR / 'media'
MEDIA_ROOT = os.getenv('MEDIA_ROOT', BASE_DIR / 'media')
# Test settings
# TEST_RUNNER = 'pytest_django.runner.DjangoPytestTestRunner' # Commented out as it's not needed for pytest

View File

@@ -14,15 +14,15 @@ 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'),
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('houtai/', 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)

View File

@@ -1,3 +1,56 @@
from django.contrib import admin
# Register your models here.
from django.contrib import admin
from .models import Client, Task, TaskResult
@admin.register(Client)
class ClientAdmin(admin.ModelAdmin):
"""客户端管理类"""
list_display = ('name', 'token', 'last_seen', 'created_at')
list_filter = ('created_at', 'last_seen')
search_fields = ('name',)
readonly_fields = ('token', 'created_at', 'last_seen')
@admin.register(Task)
class TaskAdmin(admin.ModelAdmin):
"""任务管理类"""
list_display = ('name', 'client_name', 'status', 'timeout_seconds', 'created_at', 'updated_at', 'assigned_to')
list_filter = ('status', 'created_at', 'updated_at', 'started_at', 'completed_at')
search_fields = ('name', 'client_name', 'assigned_to')
readonly_fields = ('created_at', 'updated_at', 'started_at', 'completed_at')
fieldsets = (
(None, {
'fields': ('name', 'client_name', 'script', 'status', 'timeout_seconds')
}),
('执行信息', {
'fields': ('assigned_to', 'started_at', 'completed_at'),
'classes': ('collapse',)
}),
('时间信息', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)
@admin.register(TaskResult)
class TaskResultAdmin(admin.ModelAdmin):
"""任务结果管理类"""
list_display = ('task', 'client', 'status', 'created_at')
list_filter = ('status', 'created_at', 'client')
search_fields = ('task__name', 'client__name', 'message')
readonly_fields = ('created_at',)
fieldsets = (
(None, {
'fields': ('task', 'client', 'status', 'message')
}),
('结果文件', {
'fields': ('result_file',),
'classes': ('collapse',)
}),
('时间信息', {
'fields': ('created_at',),
'classes': ('collapse',)
}),
)

View File

@@ -1,5 +1,6 @@
from django.db import models
from django.utils import timezone
import secrets
# Status choices for tasks
STATUS_CHOICES = [
@@ -20,6 +21,13 @@ class Client(models.Model):
def __str__(self):
return self.name
def save(self, *args, **kwargs):
# Generate a unique token if this is a new client
if not self.pk and not self.token:
# Use secrets module to generate a secure random token
self.token = secrets.token_urlsafe(64) # 64 bytes -> ~86 characters
super().save(*args, **kwargs)
class Meta:
verbose_name = '客户端'
@@ -57,4 +65,4 @@ class TaskResult(models.Model):
class Meta:
verbose_name = '任务结果'
verbose_name_plural = '任务结果'
verbose_name_plural = '任务结果'

View File

@@ -28,7 +28,7 @@
<a class="nav-link" href="{% url 'client_list' %}">客户端管理</a>
</li>
<li class="nav-item">
<a class="nav-link" href="/admin/">后台管理</a>
<a class="nav-link" href="/houtai/">后台管理</a>
</li>
</ul>
</div>
@@ -50,7 +50,7 @@
</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>
<a class="btn btn-secondary btn-lg" href="/houtai/" role="button">后台管理</a>
</div>
</div>
@@ -78,7 +78,7 @@
<div class="card-body">
<h5 class="card-title">后台管理</h5>
<p class="card-text">使用Django Admin进行系统管理包括用户、任务和客户端。</p>
<a href="/admin/" class="btn btn-primary">进入</a>
<a href="/houtai/" class="btn btn-primary">进入</a>
</div>
</div>
</div>