diff --git a/internal/controllers/task_controller.go b/internal/controllers/task_controller.go
index 5ceb34f..8fa9e8b 100644
--- a/internal/controllers/task_controller.go
+++ b/internal/controllers/task_controller.go
@@ -23,9 +23,11 @@ func NewTaskController(taskService *services.TaskService, cronService *services.
func (tc *TaskController) CreateTask(c *gin.Context) {
var req struct {
- Name string `json:"name" binding:"required"`
- Command string `json:"command" binding:"required"`
- Schedule string `json:"schedule" binding:"required"`
+ Name string `json:"name" binding:"required"`
+ Command string `json:"command" binding:"required"`
+ Schedule string `json:"schedule" binding:"required"`
+ Timeout int `json:"timeout"`
+ CleanConfig string `json:"clean_config"`
}
if err := c.ShouldBindJSON(&req); err != nil {
@@ -38,7 +40,7 @@ func (tc *TaskController) CreateTask(c *gin.Context) {
return
}
- task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule)
+ task := tc.taskService.CreateTask(req.Name, req.Command, req.Schedule, req.Timeout, req.CleanConfig)
tc.cronService.AddTask(task)
utils.Success(c, task)
@@ -76,10 +78,12 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
}
var req struct {
- Name string `json:"name"`
- Command string `json:"command"`
- Schedule string `json:"schedule"`
- Enabled bool `json:"enabled"`
+ Name string `json:"name"`
+ Command string `json:"command"`
+ Schedule string `json:"schedule"`
+ Timeout int `json:"timeout"`
+ CleanConfig string `json:"clean_config"`
+ Enabled bool `json:"enabled"`
}
if err := c.ShouldBindJSON(&req); err != nil {
@@ -94,7 +98,7 @@ func (tc *TaskController) UpdateTask(c *gin.Context) {
}
}
- task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Enabled)
+ task := tc.taskService.UpdateTask(id, req.Name, req.Command, req.Schedule, req.Timeout, req.CleanConfig, req.Enabled)
if task == nil {
utils.NotFound(c, "任务不存在")
return
diff --git a/internal/models/task.go b/internal/models/task.go
index 36cf9f1..f7156ad 100644
--- a/internal/models/task.go
+++ b/internal/models/task.go
@@ -6,19 +6,26 @@ import (
"gorm.io/gorm"
)
+// CleanConfig 清理配置结构
+type CleanConfig struct {
+ Type string `json:"type"` // "day" 或 "count"
+ Keep int `json:"keep"` // 保留天数或条数
+}
+
// Task represents a scheduled task
type Task struct {
- ID uint `json:"id" gorm:"primaryKey"`
- Name string `json:"name" gorm:"size:255;not null"`
- Command string `json:"command" gorm:"type:text;not null"`
- Schedule string `json:"schedule" gorm:"size:100"` // cron expression
- Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟
- Enabled bool `json:"enabled" gorm:"default:true"`
- LastRun *LocalTime `json:"last_run"`
- NextRun *LocalTime `json:"next_run"`
- CreatedAt LocalTime `json:"created_at"`
- UpdatedAt LocalTime `json:"updated_at"`
- DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
+ ID uint `json:"id" gorm:"primaryKey"`
+ Name string `json:"name" gorm:"size:255;not null"`
+ Command string `json:"command" gorm:"type:text;not null"`
+ Schedule string `json:"schedule" gorm:"size:100"` // cron expression
+ Timeout int `json:"timeout" gorm:"default:30"` // 超时时间(分钟),默认30分钟
+ CleanConfig string `json:"clean_config" gorm:"size:255;default:''"` // 清理配置 JSON
+ Enabled bool `json:"enabled" gorm:"default:true"`
+ LastRun *LocalTime `json:"last_run"`
+ NextRun *LocalTime `json:"next_run"`
+ CreatedAt LocalTime `json:"created_at"`
+ UpdatedAt LocalTime `json:"updated_at"`
+ DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
}
func (Task) TableName() string {
@@ -39,4 +46,4 @@ type TaskLog struct {
func (TaskLog) TableName() string {
return constant.TablePrefix + "task_logs"
-}
\ No newline at end of file
+}
diff --git a/internal/services/executor_service.go b/internal/services/executor_service.go
index b90e105..ea61824 100644
--- a/internal/services/executor_service.go
+++ b/internal/services/executor_service.go
@@ -3,6 +3,7 @@ package services
import (
"bytes"
"context"
+ "encoding/json"
"os/exec"
"sync"
"time"
@@ -47,6 +48,7 @@ func NewExecutorService(taskService *TaskService) *ExecutorService {
// 注册默认回调
es.RegisterCallback(es.saveTaskLogCallback)
es.RegisterCallback(es.updateStatsCallback)
+ es.RegisterCallback(es.cleanLogsCallback)
return es
}
@@ -112,6 +114,53 @@ func (es *ExecutorService) updateStatsCallback(taskID uint, _ string, result *Ex
}
}
+// CleanConfig 清理配置结构
+type CleanConfig struct {
+ Type string `json:"type"` // "day" 或 "count"
+ Keep int `json:"keep"` // 保留天数或条数
+}
+
+// cleanLogsCallback 清理日志的回调
+func (es *ExecutorService) cleanLogsCallback(taskID uint, _ string, _ *ExecutionResult) {
+ task := es.taskService.GetTaskByID(int(taskID))
+ if task == nil || task.CleanConfig == "" {
+ return
+ }
+
+ var config CleanConfig
+ if err := json.Unmarshal([]byte(task.CleanConfig), &config); err != nil {
+ logger.Errorf("Failed to parse clean config: %v", err)
+ return
+ }
+
+ if config.Keep <= 0 {
+ return
+ }
+
+ var deleted int64
+ switch config.Type {
+ case "day":
+ // 按天清理:删除 N 天前的日志
+ cutoff := time.Now().AddDate(0, 0, -config.Keep)
+ result := database.DB.Where("task_id = ? AND created_at < ?", taskID, cutoff).Delete(&models.TaskLog{})
+ deleted = result.RowsAffected
+ case "count":
+ // 按条数清理:使用子查询删除超出保留数量的旧日志
+ // 先获取第 N 条的 ID 作为边界
+ var boundaryLog models.TaskLog
+ err := database.DB.Where("task_id = ?", taskID).Order("id DESC").Offset(config.Keep - 1).Limit(1).First(&boundaryLog).Error
+ if err == nil {
+ // 删除 ID 小于边界的所有日志
+ result := database.DB.Where("task_id = ? AND id < ?", taskID, boundaryLog.ID).Delete(&models.TaskLog{})
+ deleted = result.RowsAffected
+ }
+ }
+
+ if deleted > 0 {
+ logger.Infof("Cleaned %d logs for task %d", deleted, taskID)
+ }
+}
+
// ExecuteTask executes a task by ID
func (es *ExecutorService) ExecuteTask(taskID int) *ExecutionResult {
task := es.taskService.GetTaskByID(taskID)
diff --git a/internal/services/task_service.go b/internal/services/task_service.go
index 70e155c..1e997f3 100644
--- a/internal/services/task_service.go
+++ b/internal/services/task_service.go
@@ -11,12 +11,14 @@ func NewTaskService() *TaskService {
return &TaskService{}
}
-func (ts *TaskService) CreateTask(name, command, schedule string) *models.Task {
+func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, cleanConfig string) *models.Task {
task := &models.Task{
- Name: name,
- Command: command,
- Schedule: schedule,
- Enabled: true,
+ Name: name,
+ Command: command,
+ Schedule: schedule,
+ Timeout: timeout,
+ CleanConfig: cleanConfig,
+ Enabled: true,
}
database.DB.Create(task)
return task
@@ -52,7 +54,7 @@ func (ts *TaskService) GetTaskByID(id int) *models.Task {
return &task
}
-func (ts *TaskService) UpdateTask(id int, name, command, schedule string, enabled bool) *models.Task {
+func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, cleanConfig string, enabled bool) *models.Task {
var task models.Task
if err := database.DB.First(&task, id).Error; err != nil {
return nil
@@ -60,6 +62,8 @@ func (ts *TaskService) UpdateTask(id int, name, command, schedule string, enable
task.Name = name
task.Command = command
task.Schedule = schedule
+ task.Timeout = timeout
+ task.CleanConfig = cleanConfig
task.Enabled = enabled
database.DB.Save(&task)
return &task
diff --git a/web/src/api/index.ts b/web/src/api/index.ts
index c805b64..0289864 100644
--- a/web/src/api/index.ts
+++ b/web/src/api/index.ts
@@ -187,6 +187,7 @@ export interface Task {
command: string
schedule: string
timeout: number
+ clean_config: string
enabled: boolean
last_run: string
next_run: string
diff --git a/web/src/components/ui/select/Select.vue b/web/src/components/ui/select/Select.vue
new file mode 100644
index 0000000..c94bbe8
--- /dev/null
+++ b/web/src/components/ui/select/Select.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectContent.vue b/web/src/components/ui/select/SelectContent.vue
new file mode 100644
index 0000000..adf04ec
--- /dev/null
+++ b/web/src/components/ui/select/SelectContent.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectGroup.vue b/web/src/components/ui/select/SelectGroup.vue
new file mode 100644
index 0000000..e981c6c
--- /dev/null
+++ b/web/src/components/ui/select/SelectGroup.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectItem.vue b/web/src/components/ui/select/SelectItem.vue
new file mode 100644
index 0000000..9371764
--- /dev/null
+++ b/web/src/components/ui/select/SelectItem.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectItemText.vue b/web/src/components/ui/select/SelectItemText.vue
new file mode 100644
index 0000000..b6700b1
--- /dev/null
+++ b/web/src/components/ui/select/SelectItemText.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectLabel.vue b/web/src/components/ui/select/SelectLabel.vue
new file mode 100644
index 0000000..5b6650c
--- /dev/null
+++ b/web/src/components/ui/select/SelectLabel.vue
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectScrollDownButton.vue b/web/src/components/ui/select/SelectScrollDownButton.vue
new file mode 100644
index 0000000..7dc7670
--- /dev/null
+++ b/web/src/components/ui/select/SelectScrollDownButton.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectScrollUpButton.vue b/web/src/components/ui/select/SelectScrollUpButton.vue
new file mode 100644
index 0000000..07fe87e
--- /dev/null
+++ b/web/src/components/ui/select/SelectScrollUpButton.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectSeparator.vue b/web/src/components/ui/select/SelectSeparator.vue
new file mode 100644
index 0000000..4b5c885
--- /dev/null
+++ b/web/src/components/ui/select/SelectSeparator.vue
@@ -0,0 +1,19 @@
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectTrigger.vue b/web/src/components/ui/select/SelectTrigger.vue
new file mode 100644
index 0000000..667908b
--- /dev/null
+++ b/web/src/components/ui/select/SelectTrigger.vue
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/SelectValue.vue b/web/src/components/ui/select/SelectValue.vue
new file mode 100644
index 0000000..d5ce58b
--- /dev/null
+++ b/web/src/components/ui/select/SelectValue.vue
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
diff --git a/web/src/components/ui/select/index.ts b/web/src/components/ui/select/index.ts
new file mode 100644
index 0000000..96eae60
--- /dev/null
+++ b/web/src/components/ui/select/index.ts
@@ -0,0 +1,11 @@
+export { default as Select } from "./Select.vue"
+export { default as SelectContent } from "./SelectContent.vue"
+export { default as SelectGroup } from "./SelectGroup.vue"
+export { default as SelectItem } from "./SelectItem.vue"
+export { default as SelectItemText } from "./SelectItemText.vue"
+export { default as SelectLabel } from "./SelectLabel.vue"
+export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"
+export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"
+export { default as SelectSeparator } from "./SelectSeparator.vue"
+export { default as SelectTrigger } from "./SelectTrigger.vue"
+export { default as SelectValue } from "./SelectValue.vue"
diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue
index 320339a..ad32d22 100644
--- a/web/src/views/tasks/Tasks.vue
+++ b/web/src/views/tasks/Tasks.vue
@@ -1,10 +1,11 @@