diff --git a/internal/database/migrate.go b/internal/database/migrate.go index 08f5954..b4d3768 100644 --- a/internal/database/migrate.go +++ b/internal/database/migrate.go @@ -13,5 +13,6 @@ func Migrate() error { &models.EnvironmentVariable{}, &models.Setting{}, &models.LoginLog{}, + &models.SendStats{}, ) } diff --git a/internal/models/send_stats.go b/internal/models/send_stats.go new file mode 100644 index 0000000..68b6f11 --- /dev/null +++ b/internal/models/send_stats.go @@ -0,0 +1,20 @@ +package models + +import ( + "time" + + "baihu/internal/constant" +) + +// SendStats 任务执行统计 +type SendStats struct { + ID uint `json:"id" gorm:"primaryKey"` + TaskID uint `json:"task_id" gorm:"index"` + Status string `json:"status" gorm:"size:20;not null"` // success, failed + Num int `json:"num" gorm:"default:0"` + CreatedAt time.Time `json:"created_at"` +} + +func (SendStats) TableName() string { + return constant.TablePrefix + "send_stats" +} diff --git a/internal/router/router.go b/internal/router/router.go index c5b89f9..b4dea7f 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -151,7 +151,7 @@ func Setup(c *Controllers) *gin.Engine { files.POST("/delete", c.File.DeleteFile) files.POST("/rename", c.File.RenameFile) files.POST("/upload", c.File.UploadArchive) - files.POST("/upload-files", c.File.UploadFiles) + files.POST("/uploadfiles", c.File.UploadFiles) } // Log routes @@ -169,11 +169,11 @@ func Setup(c *Controllers) *gin.Engine { settings := authorized.Group("/settings") { settings.POST("/password", c.Settings.ChangePassword) - settings.POST("/clean-logs", c.Settings.CleanLogs) + settings.POST("/cleanlogs", c.Settings.CleanLogs) settings.GET("/site", c.Settings.GetSiteSettings) settings.PUT("/site", c.Settings.UpdateSiteSettings) settings.GET("/about", c.Settings.GetAbout) - settings.GET("/login-logs", c.Settings.GetLoginLogs) + settings.GET("/loginlogs", c.Settings.GetLoginLogs) } } } diff --git a/internal/services/executor_service.go b/internal/services/executor_service.go index 48564e5..b90e105 100644 --- a/internal/services/executor_service.go +++ b/internal/services/executor_service.go @@ -24,20 +24,91 @@ type ExecutionResult struct { End time.Time } +// ExecutionCallback 任务执行完成后的回调函数类型 +type ExecutionCallback func(taskID uint, command string, result *ExecutionResult) + // ExecutorService handles task execution type ExecutorService struct { taskService *TaskService results []ExecutionResult - runningTasks map[int]bool // 正在运行的任务 + runningTasks map[int]bool + callbacks []ExecutionCallback mu sync.RWMutex } // NewExecutorService creates a new executor service func NewExecutorService(taskService *TaskService) *ExecutorService { - return &ExecutorService{ + es := &ExecutorService{ taskService: taskService, results: make([]ExecutionResult, 0), runningTasks: make(map[int]bool), + callbacks: make([]ExecutionCallback, 0), + } + // 注册默认回调 + es.RegisterCallback(es.saveTaskLogCallback) + es.RegisterCallback(es.updateStatsCallback) + return es +} + +// RegisterCallback 注册执行完成回调 +func (es *ExecutorService) RegisterCallback(cb ExecutionCallback) { + es.mu.Lock() + es.callbacks = append(es.callbacks, cb) + es.mu.Unlock() +} + +// executeCallbacks 执行所有回调 +func (es *ExecutorService) executeCallbacks(taskID uint, command string, result *ExecutionResult) { + es.mu.RLock() + callbacks := make([]ExecutionCallback, len(es.callbacks)) + copy(callbacks, es.callbacks) + es.mu.RUnlock() + + for _, cb := range callbacks { + cb(taskID, command, result) + } +} + +// saveTaskLogCallback 保存任务日志的回调 +func (es *ExecutorService) saveTaskLogCallback(taskID uint, command string, result *ExecutionResult) { + output := result.Output + if result.Error != "" { + output += "\n[ERROR]\n" + result.Error + } + + compressed, err := utils.CompressToBase64(output) + if err != nil { + logger.Errorf("Failed to compress log: %v", err) + compressed = "" + } + + status := "success" + if !result.Success { + status = "failed" + } + + taskLog := &models.TaskLog{ + TaskID: taskID, + Command: command, + Output: compressed, + Status: status, + Duration: result.End.Sub(result.Start).Milliseconds(), + } + + if err := database.DB.Create(taskLog).Error; err != nil { + logger.Errorf("Failed to save task log: %v", err) + } +} + +// updateStatsCallback 更新统计数据的回调 +func (es *ExecutorService) updateStatsCallback(taskID uint, _ string, result *ExecutionResult) { + status := "success" + if !result.Success { + status = "failed" + } + sendStatsService := NewSendStatsService() + if err := sendStatsService.IncrementStats(taskID, status); err != nil { + logger.Errorf("Failed to update stats: %v", err) } } @@ -72,8 +143,8 @@ func (es *ExecutorService) ExecuteTask(taskID int) *ExecutionResult { delete(es.runningTasks, taskID) es.mu.Unlock() - // Save log to database - es.saveTaskLog(uint(taskID), task.Command, result) + // 执行回调 + es.executeCallbacks(uint(taskID), task.Command, result) return result } @@ -97,11 +168,9 @@ func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout tim Start: time.Now(), } - // Create a context with timeout ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - // Execute the command shell, args := utils.GetShellCommand(command) cmd := exec.CommandContext(ctx, shell, args...) var stdout, stderr bytes.Buffer @@ -111,7 +180,6 @@ func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout tim err := cmd.Run() result.End = time.Now() - // Process results result.Output = stdout.String() if err != nil { if ctx.Err() == context.DeadlineExceeded { @@ -123,10 +191,8 @@ func (es *ExecutorService) ExecuteCommandWithTimeout(command string, timeout tim result.Success = true } - // Store result es.mu.Lock() es.results = append(es.results, *result) - // Keep only the last 100 results to prevent memory issues if len(es.results) > 100 { es.results = es.results[1:] } @@ -149,35 +215,3 @@ func (es *ExecutorService) GetLastResults(count int) []ExecutionResult { copy(results, es.results[start:]) return results } - -// saveTaskLog saves execution log to database with gzip+base64 compression -func (es *ExecutorService) saveTaskLog(taskID uint, command string, result *ExecutionResult) { - output := result.Output - if result.Error != "" { - output += "\n[ERROR]\n" + result.Error - } - - // Compress output - compressed, err := utils.CompressToBase64(output) - if err != nil { - logger.Errorf("Failed to compress log: %v", err) - compressed = "" - } - - status := "success" - if !result.Success { - status = "failed" - } - - taskLog := &models.TaskLog{ - TaskID: taskID, - Command: command, - Output: compressed, - Status: status, - Duration: result.End.Sub(result.Start).Milliseconds(), - } - - if err := database.DB.Create(taskLog).Error; err != nil { - logger.Errorf("Failed to save task log: %v", err) - } -} diff --git a/internal/services/send_stats_service.go b/internal/services/send_stats_service.go new file mode 100644 index 0000000..6e7e2f0 --- /dev/null +++ b/internal/services/send_stats_service.go @@ -0,0 +1,63 @@ +package services + +import ( + "time" + + "baihu/internal/database" + "baihu/internal/models" +) + +type SendStatsService struct{} + +func NewSendStatsService() *SendStatsService { + return &SendStatsService{} +} + +// IncrementStats 增加任务执行统计 +func (s *SendStatsService) IncrementStats(taskID uint, status string) error { + today := time.Now().Format("2006-01-02") + startOfDay, _ := time.ParseInLocation("2006-01-02", today, time.Local) + + var stats models.SendStats + result := database.DB.Where("task_id = ? AND status = ? AND created_at >= ?", taskID, status, startOfDay).First(&stats) + + if result.Error != nil { + // 不存在则创建 + stats = models.SendStats{ + TaskID: taskID, + Status: status, + Num: 1, + CreatedAt: time.Now(), + } + return database.DB.Create(&stats).Error + } + + // 存在则增加计数 + return database.DB.Model(&stats).Update("num", stats.Num+1).Error +} + +// GetStatsByTaskID 获取任务的统计数据 +func (s *SendStatsService) GetStatsByTaskID(taskID uint) []models.SendStats { + var stats []models.SendStats + database.DB.Where("task_id = ?", taskID).Order("created_at DESC").Find(&stats) + return stats +} + +// GetTodayStats 获取今日统计 +func (s *SendStatsService) GetTodayStats() []models.SendStats { + today := time.Now().Format("2006-01-02") + startOfDay, _ := time.ParseInLocation("2006-01-02", today, time.Local) + + var stats []models.SendStats + database.DB.Where("created_at >= ?", startOfDay).Find(&stats) + return stats +} + +// GetRecentStats 获取最近N天的统计 +func (s *SendStatsService) GetRecentStats(days int) []models.SendStats { + startDate := time.Now().AddDate(0, 0, -days) + + var stats []models.SendStats + database.DB.Where("created_at >= ?", startDate).Order("created_at DESC").Find(&stats) + return stats +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 05e4f41..2f4e335 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -108,7 +108,7 @@ export const api = { changePassword: (data: { old_password: string; new_password: string }) => request('/settings/password', { method: 'POST', body: JSON.stringify(data) }), cleanLogs: (days: number) => - request<{ deleted: number }>('/settings/clean-logs', { method: 'POST', body: JSON.stringify({ days }) }), + request<{ deleted: number }>('/settings/cleanlogs', { method: 'POST', body: JSON.stringify({ days }) }), getSite: () => request('/settings/site'), getPublicSite: () => request<{ title: string; subtitle: string; icon: string }>('/settings/public'), updateSite: (data: SiteSettings) => @@ -119,7 +119,7 @@ export const api = { if (params?.page) query.set('page', String(params.page)) if (params?.page_size) query.set('page_size', String(params.page_size)) if (params?.username) query.set('username', params.username) - return request(`/settings/login-logs?${query}`) + return request(`/settings/loginlogs?${query}`) } }, files: { @@ -157,7 +157,7 @@ export const api = { } if (targetPath) formData.append('path', targetPath) - const res = await fetch(`${BASE_URL}/files/upload-files`, { + const res = await fetch(`${BASE_URL}/files/uploadfiles`, { method: 'POST', credentials: 'include', body: formData diff --git a/web/src/layouts/MainLayout.vue b/web/src/layouts/MainLayout.vue index 516c560..f041c02 100644 --- a/web/src/layouts/MainLayout.vue +++ b/web/src/layouts/MainLayout.vue @@ -18,7 +18,7 @@ const navItems = [ { to: '/history', icon: ScrollText, label: '执行历史', exact: true }, { to: '/environments', icon: Variable, label: '环境变量', exact: true }, { to: '/terminal', icon: Terminal, label: '终端命令', exact: true }, - { to: '/login-logs', icon: KeyRound, label: '登录日志', exact: true }, + { to: '/loginlogs', icon: KeyRound, label: '登录日志', exact: true }, { to: '/settings', icon: Settings, label: '系统设置', exact: true }, ] diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 0f07ac1..015de15 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -20,7 +20,7 @@ const router = createRouter({ { path: 'editor/:path(.*)?', name: 'editor', component: () => import('@/views/editor/Editor.vue') }, { path: 'environments', name: 'environments', component: () => import('@/views/environments/Environments.vue') }, { path: 'history', name: 'history', component: () => import('@/views/history/History.vue') }, - { path: 'login-logs', name: 'login-logs', component: () => import('@/views/login-logs/LoginLogs.vue') }, + { path: 'loginlogs', name: 'loginlogs', component: () => import('@/views/loginlogs/LoginLogs.vue') }, { path: 'terminal', name: 'terminal', component: () => import('@/views/terminal/Terminal.vue') }, { path: 'settings', name: 'settings', component: () => import('@/views/settings/Settings.vue') } ] diff --git a/web/src/views/history/History.vue b/web/src/views/history/History.vue index 1705f21..54944b0 100644 --- a/web/src/views/history/History.vue +++ b/web/src/views/history/History.vue @@ -117,12 +117,12 @@ onMounted(loadLogs)
-
+
ID 任务名称 - 命令 + 命令 状态 耗时 执行时间 @@ -143,7 +143,7 @@ onMounted(loadLogs) > #{{ log.id }} {{ log.task_name }} - {{ log.command }} + {{ log.command }} diff --git a/web/src/views/login-logs/LoginLogs.vue b/web/src/views/loginlogs/LoginLogs.vue similarity index 86% rename from web/src/views/login-logs/LoginLogs.vue rename to web/src/views/loginlogs/LoginLogs.vue index c913b0f..0ef7467 100644 --- a/web/src/views/login-logs/LoginLogs.vue +++ b/web/src/views/loginlogs/LoginLogs.vue @@ -58,15 +58,6 @@ function handlePageChange(page: number) { loadLogs() } -function getBrowserInfo(userAgent: string): string { - if (!userAgent) return '未知' - if (userAgent.includes('Chrome')) return 'Chrome' - if (userAgent.includes('Firefox')) return 'Firefox' - if (userAgent.includes('Safari')) return 'Safari' - if (userAgent.includes('Edge')) return 'Edge' - return '其他' -} - onMounted(loadLogs) @@ -98,9 +89,8 @@ onMounted(loadLogs)
用户名 IP 地址 - 浏览器 状态 - 消息 + User Agent 时间
@@ -115,13 +105,12 @@ onMounted(loadLogs) > {{ log.username }} {{ log.ip }} - {{ getBrowserInfo(log.user_agent) }} {{ log.status === 'success' ? '成功' : '失败' }} - {{ log.message }} + {{ log.user_agent || '-' }} {{ log.created_at }}
diff --git a/web/src/views/tasks/Tasks.vue b/web/src/views/tasks/Tasks.vue index 2fb9f47..320339a 100644 --- a/web/src/views/tasks/Tasks.vue +++ b/web/src/views/tasks/Tasks.vue @@ -139,7 +139,7 @@ onMounted(loadTasks) ID 名称 命令 - 定时规则 + 定时规则 上次执行 下次执行 状态 @@ -158,7 +158,7 @@ onMounted(loadTasks) #{{ task.id }} {{ task.name }} {{ task.command }} - {{ task.schedule }} + {{ task.schedule }} {{ task.last_run || '-' }} {{ task.next_run || '-' }}