feat: 支持创建任务时自定义crontab并清理冗余代码

- 添加创建任务时的crontab编辑控件
- 修复创建任务按钮状态重置问题
- 创建任务后自动加载到调度器
- 删除废弃的手动创建任务API和相关代码
This commit is contained in:
2026-01-05 20:53:25 +08:00
parent 0fd21960e8
commit a5de813c82
8 changed files with 40 additions and 77 deletions
+2 -32
View File
@@ -5,7 +5,7 @@ from datetime import datetime, timedelta
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from backend.models import get_db, User from backend.models import get_db, User
from backend.schemas.task import TaskCreate, TaskUpdate, TaskResponse from backend.schemas.task import TaskUpdate, TaskResponse
from backend.services.task_service import TaskService from backend.services.task_service import TaskService
from backend.dependencies import get_current_user from backend.dependencies import get_current_user
@@ -16,37 +16,7 @@ class CronValidateRequest(BaseModel):
"""Cron 表达式验证请求""" """Cron 表达式验证请求"""
cron_expression: str = Field(..., min_length=9, description="Crontab 表达式") cron_expression: str = Field(..., min_length=9, description="Crontab 表达式")
# create_task_from_template: 已在 templates.py 中定义
@router.post("/", response_model=TaskResponse, status_code=status.HTTP_201_CREATED, summary="创建打卡任务")
async def create_task(
task_data: TaskCreate,
current_user: User = Depends(get_current_user),
db: Session = Depends(get_db)
):
"""
创建新的打卡任务(基于模板)
现在的任务创建流程:
1. 管理员在后台创建模板(包含完整的 payload_config
2. 用户基于模板创建任务,填写字段值
3. 系统自动生成完整的 payload_config
注意:直接创建任务的方式已废弃,请使用模板接口。
"""
try:
task = TaskService.create_task(current_user.id, task_data, db)
return task
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"创建任务失败: {str(e)}"
)
@router.get("/", response_model=List[TaskResponse], summary="获取当前用户的任务列表") @router.get("/", response_model=List[TaskResponse], summary="获取当前用户的任务列表")
async def get_tasks( async def get_tasks(
+3 -1
View File
@@ -200,6 +200,7 @@ async def create_task_from_template(
- **thread_id**: 接龙项目 ID - **thread_id**: 接龙项目 ID
- **field_values**: 用户填写的字段值 - **field_values**: 用户填写的字段值
- **task_name**: 任务名称(可选) - **task_name**: 任务名称(可选)
- **cron_expression**: Cron 表达式(可选,默认每天 20:00)
""" """
task = TemplateService.create_task_from_template( task = TemplateService.create_task_from_template(
template_id=request.template_id, template_id=request.template_id,
@@ -207,6 +208,7 @@ async def create_task_from_template(
field_values=request.field_values, field_values=request.field_values,
user_id=current_user.id, user_id=current_user.id,
task_name=request.task_name, task_name=request.task_name,
db=db db=db,
cron_expression=request.cron_expression
) )
return task return task
+1
View File
@@ -137,6 +137,7 @@ class TaskFromTemplateRequest(BaseModel):
thread_id: str = Field(..., min_length=1, description="接龙项目 ID") thread_id: str = Field(..., min_length=1, description="接龙项目 ID")
field_values: Dict[str, Any] = Field(default_factory=dict, description="用户填写的字段值") field_values: Dict[str, Any] = Field(default_factory=dict, description="用户填写的字段值")
task_name: Optional[str] = Field(None, max_length=100, description="任务名称(可选)") task_name: Optional[str] = Field(None, max_length=100, description="任务名称(可选)")
cron_expression: Optional[str] = Field("0 20 * * *", description="Cron 表达式(可选,默认每天 20:00)")
class TemplatePreviewResponse(BaseModel): class TemplatePreviewResponse(BaseModel):
+12 -3
View File
@@ -503,7 +503,8 @@ class TemplateService:
field_values: Dict[str, Any], field_values: Dict[str, Any],
user_id: int, user_id: int,
task_name: Optional[str], task_name: Optional[str],
db: Session db: Session,
cron_expression: Optional[str] = "0 20 * * *"
) -> CheckInTask: ) -> CheckInTask:
""" """
从模板创建打卡任务 从模板创建打卡任务
@@ -515,6 +516,7 @@ class TemplateService:
user_id: 用户 ID user_id: 用户 ID
task_name: 任务名称(可选) task_name: 任务名称(可选)
db: 数据库会话 db: 数据库会话
cron_expression: Cron 表达式(可选,默认每天 20:00)
Returns: Returns:
创建的任务对象 创建的任务对象
@@ -544,19 +546,26 @@ class TemplateService:
signature = payload.get('Signature', 'Unknown') signature = payload.get('Signature', 'Unknown')
task_name = f"{template.name} - {signature}" task_name = f"{template.name} - {signature}"
# 创建任务(只存储 payload_config,不再需要 thread_id 和 email # 创建任务(包含 cron_expression
try: try:
task = CheckInTask( task = CheckInTask(
user_id=user_id, user_id=user_id,
payload_config=json.dumps(payload, ensure_ascii=False), payload_config=json.dumps(payload, ensure_ascii=False),
name=task_name, name=task_name,
is_active=True is_active=True,
cron_expression=cron_expression or "0 20 * * *"
) )
db.add(task) db.add(task)
db.commit() db.commit()
db.refresh(task) db.refresh(task)
logger.info(f"从模板创建任务成功: {task.name} (ID: {task.id}, 模板: {template.name}, ThreadId: {thread_id})") logger.info(f"从模板创建任务成功: {task.name} (ID: {task.id}, 模板: {template.name}, ThreadId: {thread_id})")
# 如果任务启用且包含 cron_expression,立即添加到调度器
if task.is_scheduled_enabled:
from backend.services.task_service import TaskService
TaskService._reload_scheduler_for_task(task, db)
return task return task
except Exception as e: except Exception as e:
-5
View File
@@ -89,11 +89,6 @@ export const taskAPI = {
return client.get('/api/tasks', { params }); return client.get('/api/tasks', { params });
}, },
// 创建任务
createTask: taskData => {
return client.post('/api/tasks', taskData);
},
// 获取任务详情 // 获取任务详情
getTask: taskId => { getTask: taskId => {
return client.get(`/api/tasks/${taskId}`); return client.get(`/api/tasks/${taskId}`);
-19
View File
@@ -46,25 +46,6 @@ export const useTaskStore = defineStore('task', {
} }
}, },
// 创建新任务
async createTask(taskData) {
this.loading = true;
this.error = null;
try {
const newTask = await api.task.createTask(taskData);
this.tasks.unshift(newTask); // 添加到列表开头
return newTask;
} catch (error) {
// 解析后端错误信息
let errorMsg = error.message || '创建任务失败';
this.error = errorMsg;
throw new Error(errorMsg);
} finally {
this.loading = false;
}
},
// 更新任务 // 更新任务
async updateTask(taskId, taskData) { async updateTask(taskId, taskData) {
this.loading = true; this.loading = true;
+2 -1
View File
@@ -132,7 +132,7 @@ export const useTemplateStore = defineStore('template', {
} }
}, },
async createTaskFromTemplate(templateId, threadId, fieldValues, taskName = null) { async createTaskFromTemplate(templateId, threadId, fieldValues, taskName = null, cronExpression = '0 20 * * *') {
this.loading = true; this.loading = true;
this.error = null; this.error = null;
try { try {
@@ -141,6 +141,7 @@ export const useTemplateStore = defineStore('template', {
thread_id: threadId, thread_id: threadId,
field_values: fieldValues, field_values: fieldValues,
task_name: taskName, task_name: taskName,
cron_expression: cronExpression,
}); });
return task; return task;
} catch (error) { } catch (error) {
+20 -16
View File
@@ -13,7 +13,7 @@
type="primary" type="primary"
size="large" size="large"
class="shadow-md3-3" class="shadow-md3-3"
@click="showCreateDialog = true" @click="openCreateDialog"
> >
<template #icon> <template #icon>
<PlusOutlined /> <PlusOutlined />
@@ -99,7 +99,7 @@
<p class="text-on-surface-variant mb-6"> <p class="text-on-surface-variant mb-6">
点击右上角的"创建任务"按钮开始添加您的第一个打卡任务 点击右上角的"创建任务"按钮开始添加您的第一个打卡任务
</p> </p>
<a-button type="primary" @click="showCreateDialog = true"> 创建第一个任务 </a-button> <a-button type="primary" @click="openCreateDialog"> 创建第一个任务 </a-button>
</a-card> </a-card>
<a-row v-else :gutter="[16, 16]"> <a-row v-else :gutter="[16, 16]">
@@ -263,6 +263,10 @@
<a-input v-model:value="templateTaskForm.thread_id" placeholder="请输入接龙项目 ID" /> <a-input v-model:value="templateTaskForm.thread_id" placeholder="请输入接龙项目 ID" />
</a-form-item> </a-form-item>
<a-form-item label="打卡时间表">
<CrontabEditor v-model="templateTaskForm.cron_expression" />
</a-form-item>
<a-divider orientation="left">填写字段信息</a-divider> <a-divider orientation="left">填写字段信息</a-divider>
<!-- Dynamic Fields --> <!-- Dynamic Fields -->
@@ -419,19 +423,18 @@ const templateFormRef = ref(null);
const checkInLoading = ref({}); const checkInLoading = ref({});
// Template mode // Template mode
const createMode = ref('template'); // 'template' or 'manual'
const loadingTemplates = ref(false); const loadingTemplates = ref(false);
const activeTemplates = ref([]); const activeTemplates = ref([]);
const selectedTemplate = ref(null); const selectedTemplate = ref(null);
const templatePreview = ref(null); // 存储从 preview 接口获取的合并后配置 const templatePreview = ref(null); // 存储从 preview 接口获取的合并后配置
// Manual create form // Edit task form (仅用于编辑任务)
const taskForm = reactive({ const taskForm = reactive({
name: '', name: '',
thread_id: '', thread_id: '',
is_active: true, is_active: true,
payload_config: '', payload_config: '',
cron_expression: '0 20 * * *', // 新增:Crontab 表达式,默认每天 20:00 cron_expression: '0 20 * * *',
}); });
// Template create form // Template create form
@@ -439,6 +442,7 @@ const templateTaskForm = reactive({
task_name: '', task_name: '',
thread_id: '', thread_id: '',
field_values: {}, field_values: {},
cron_expression: '0 20 * * *',
}); });
const taskRules = { const taskRules = {
@@ -750,7 +754,7 @@ const handleSubmit = async () => {
message.success('任务更新成功'); message.success('任务更新成功');
} }
// Create from template // Create from template
else if (createMode.value === 'template') { else {
if (!selectedTemplate.value) { if (!selectedTemplate.value) {
message.warning('请选择一个模板'); message.warning('请选择一个模板');
return; return;
@@ -765,19 +769,12 @@ const handleSubmit = async () => {
selectedTemplate.value.id, selectedTemplate.value.id,
templateTaskForm.thread_id, templateTaskForm.thread_id,
templateTaskForm.field_values, templateTaskForm.field_values,
templateTaskForm.task_name || null templateTaskForm.task_name || null,
templateTaskForm.cron_expression || '0 20 * * *'
); );
message.success('任务创建成功'); message.success('任务创建成功');
} }
// Create manually
else {
if (!taskFormRef.value) return;
await taskFormRef.value.validate();
await taskStore.createTask(taskForm);
message.success('任务创建成功');
}
showCreateDialog.value = false; showCreateDialog.value = false;
resetForm(); resetForm();
@@ -793,22 +790,29 @@ const handleSubmit = async () => {
const resetForm = () => { const resetForm = () => {
editingTask.value = null; editingTask.value = null;
selectedTemplate.value = null; selectedTemplate.value = null;
createMode.value = 'template';
Object.assign(taskForm, { Object.assign(taskForm, {
name: '', name: '',
thread_id: '', thread_id: '',
is_active: true, is_active: true,
payload_config: '', payload_config: '',
cron_expression: '0 20 * * *',
}); });
templateTaskForm.task_name = ''; templateTaskForm.task_name = '';
templateTaskForm.thread_id = ''; templateTaskForm.thread_id = '';
templateTaskForm.field_values = {}; templateTaskForm.field_values = {};
templateTaskForm.cron_expression = '0 20 * * *';
taskFormRef.value?.resetFields(); taskFormRef.value?.resetFields();
}; };
// 打开创建任务对话框
const openCreateDialog = () => {
resetForm(); // 重置表单状态,确保不会显示编辑界面
showCreateDialog.value = true;
};
// Watch dialog open to load templates // Watch dialog open to load templates
watch(showCreateDialog, isOpen => { watch(showCreateDialog, isOpen => {
if (isOpen && !editingTask.value) { if (isOpen && !editingTask.value) {