mirror of
https://github.com/Cccc-owo/CheckInApp.git
synced 2026-06-17 05:56:29 +00:00
feat: 支持创建任务时自定义crontab并清理冗余代码
- 添加创建任务时的crontab编辑控件 - 修复创建任务按钮状态重置问题 - 创建任务后自动加载到调度器 - 删除废弃的手动创建任务API和相关代码
This commit is contained in:
+2
-32
@@ -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(
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user