feat: add retry times and backup users

This commit is contained in:
engigu
2026-02-28 14:44:32 +08:00
parent 06cdc58a72
commit 69cb727b87
15 changed files with 175 additions and 46 deletions
+4
View File
@@ -42,6 +42,7 @@ type tableConfig struct {
func (s *BackupService) getTableConfigs() []tableConfig {
return []tableConfig{
{"users.json", s.exportTable(&[]models.User{}, true), s.restoreTable(&[]models.User{}, true)},
{"tasks.json", s.exportTable(&[]models.Task{}, true), s.restoreTable(&[]models.Task{}, true)},
{"task_logs.json", s.exportTable(&[]models.TaskLog{}, false), s.restoreTable(&[]models.TaskLog{}, false)},
{"envs.json", s.exportTable(&[]models.EnvironmentVariable{}, true), s.restoreTable(&[]models.EnvironmentVariable{}, true)},
@@ -199,6 +200,7 @@ func (s *BackupService) Restore(zipPath string) error {
// 开启全局事务
return database.DB.Transaction(func(tx *gorm.DB) error {
// 1. 清空现有数据(物理删除)
tx.Unscoped().Where("1=1").Delete(&models.User{})
tx.Unscoped().Where("1=1").Delete(&models.Task{})
tx.Unscoped().Where("1=1").Delete(&models.TaskLog{})
tx.Unscoped().Where("1=1").Delete(&models.EnvironmentVariable{})
@@ -281,6 +283,8 @@ func (s *BackupService) restoreFromZipFile(tx *gorm.DB, f *zip.File, filename st
}
switch filename {
case "users.json":
return restoreStreamBatch[models.User](tx, decoder)
case "tasks.json":
return restoreStreamBatch[models.Task](tx, decoder)
case "task_logs.json":
+2 -2
View File
@@ -102,8 +102,8 @@ func LoadConfig(path string) (*AppConfig, error) {
// 设置表前缀到 constant 包
constant.TablePrefix = Config.Database.TablePrefix
// 设置 Secret constant
constant.Secret = Config.Security.Secret
// 暂存旧的 Secret,不再直接给 constant 赋值(改为到 settings 初始化时判断)
// constant.Secret = Config.Security.Secret
// 设置演示模式
if v := os.Getenv("BH_DEMO_MODE"); v == "true" || v == "1" {
+20
View File
@@ -5,6 +5,7 @@ import (
"github.com/engigu/baihu-panel/internal/constant"
"github.com/engigu/baihu-panel/internal/database"
"github.com/engigu/baihu-panel/internal/models"
"github.com/engigu/baihu-panel/internal/utils"
)
type SettingsService struct{}
@@ -26,6 +27,25 @@ func (s *SettingsService) InitSettings() error {
}
}
}
// 初始化或获取 JWT Secret 密码
var secCount int64
database.DB.Model(&models.Setting{}).Where("section = ? AND `key` = ?", constant.SectionSecurity, constant.KeySecret).Count(&secCount)
var secretValue string
if secCount == 0 {
// 先尝试从配置文件读取遗留下来的旧设
if Config != nil && Config.Security.Secret != "" {
secretValue = Config.Security.Secret
} else {
secretValue = utils.RandomString(32)
}
if err := database.DB.Create(&models.Setting{Section: constant.SectionSecurity, Key: constant.KeySecret, Value: secretValue}).Error; err != nil {
return err
}
} else {
secretValue = s.Get(constant.SectionSecurity, constant.KeySecret)
}
constant.Secret = secretValue
cache.LoadSiteCache()
return nil
}
+54 -12
View File
@@ -141,10 +141,7 @@ func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest)
return nil, nil, fmt.Errorf("任务并发限制: %v", err)
}
if req.Metadata == nil {
req.Metadata = make(map[string]interface{})
}
req.Metadata["goid"] = goid
req.Metadata.GoID = goid
// 3. 创建 TinyLog 实时日志收集器
tl, err := NewTinyLog(taskLog.ID)
@@ -161,6 +158,10 @@ func (h *ServerSchedulerHandler) OnTaskExecuting(req *executor.ExecutionRequest)
StartTime: time.Now(),
})
if req.Metadata.RetryIndex > 0 {
tl.Write([]byte(fmt.Sprintf("\n[System] 此为任务失败后的第 %d 次重试执行...\n\n", req.Metadata.RetryIndex)))
}
// 对于本地任务,Scheduler 会通过返回的 Writer 写入日志
// 对于远程任务,Scheduler 不会写入任何内容(由 Agent 推送至此 TL)
return tl, tl, nil
@@ -235,10 +236,8 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
}
// 移除运行记录
if req.Metadata != nil {
if goid, ok := req.Metadata["goid"].(int64); ok {
h.es.RemoveRunningGo(task.ID, goid)
}
if req.Metadata.GoID != 0 {
h.es.RemoveRunningGo(task.ID, req.Metadata.GoID)
}
// 处理任务完成(更新统计、清理旧日志等)
@@ -246,6 +245,9 @@ func (h *ServerSchedulerHandler) OnTaskCompleted(req *executor.ExecutionRequest,
// 更新内存缓冲
h.es.UpdateResult(*result)
// ======= 重试逻辑 =======
h.es.HandleTaskRetry(task, req, result.Success, result.Status, result.ExitCode)
}
func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, err error) {
@@ -257,10 +259,8 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
fmt.Sscanf(req.TaskID, "%d", &taskID)
// 移除运行记录
if req.Metadata != nil {
if goid, ok := req.Metadata["goid"].(int64); ok {
h.es.RemoveRunningGo(taskID, goid)
}
if req.Metadata.GoID != 0 {
h.es.RemoveRunningGo(taskID, req.Metadata.GoID)
}
// 构造错误日志
@@ -305,6 +305,48 @@ func (h *ServerSchedulerHandler) OnTaskFailed(req *executor.ExecutionRequest, er
StartTime: time.Now(),
EndTime: time.Now(),
})
// ======= 重试逻辑 =======
h.es.HandleTaskRetry(task, req, false, constant.TaskStatusFailed, 1)
}
// HandleTaskRetry 处理任务失败重试逻辑
func (es *ExecutorService) HandleTaskRetry(task *models.Task, req *executor.ExecutionRequest, isSuccess bool, status string, exitCode int) {
if task == nil {
return
}
if !isSuccess || status == constant.TaskStatusFailed || status == constant.TaskStatusTimeout || exitCode != 0 {
retryIndex := req.Metadata.RetryIndex
if retryIndex < task.RetryCount {
retryIndex++
logger.Infof("[Executor] 任务 #%d 执行失败/出错,将在 %d 秒后进行第 %d/%d 次重试...", task.ID, task.RetryInterval, retryIndex, task.RetryCount)
es.scheduler.EnqueueDelayed(time.Duration(task.RetryInterval)*time.Second, func() *executor.ExecutionRequest {
latestTask := es.taskService.GetTaskByID(int(task.ID))
if latestTask == nil || !latestTask.Enabled {
return nil
}
newEnvs := es.loadEnvVars(latestTask.Envs)
return &executor.ExecutionRequest{
TaskID: req.TaskID,
Name: latestTask.Name,
Command: latestTask.Command,
WorkDir: latestTask.WorkDir,
Envs: newEnvs,
Timeout: latestTask.Timeout,
Languages: latestTask.Languages,
UseMise: latestTask.UseMise(),
Type: executor.TaskTypeManual,
Metadata: executor.ExecutionMetadata{
RetryIndex: retryIndex,
},
}
})
}
}
}
func (h *ServerSchedulerHandler) OnCronNextRun(req *executor.ExecutionRequest, nextRun time.Time) {
+9 -5
View File
@@ -12,7 +12,7 @@ func NewTaskService() *TaskService {
return &TaskService{}
}
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string) *models.Task {
func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, workDir, cleanConfig, envs, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int) *models.Task {
if taskType == "" {
taskType = "task"
}
@@ -31,9 +31,11 @@ func (ts *TaskService) CreateTask(name, command, schedule string, timeout int, w
WorkDir: workDir,
CleanConfig: cleanConfig,
Envs: envs,
Languages: languages,
AgentID: agentID,
Enabled: true,
Languages: languages,
AgentID: agentID,
Enabled: true,
RetryCount: retryCount,
RetryInterval: retryInterval,
}
if triggerType != constant.TriggerTypeCron {
task.NextRun = nil
@@ -81,7 +83,7 @@ func (ts *TaskService) GetTaskByID(id int) *models.Task {
return &task
}
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string) *models.Task {
func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeout int, workDir, cleanConfig, envs string, enabled bool, taskType, config string, agentID *uint, languages []map[string]string, triggerType string, tags string, retryCount int, retryInterval int) *models.Task {
var task models.Task
if err := database.DB.First(&task, id).Error; err != nil {
return nil
@@ -97,6 +99,8 @@ func (ts *TaskService) UpdateTask(id int, name, command, schedule string, timeou
task.Enabled = enabled
task.AgentID = agentID
task.Languages = languages
task.RetryCount = retryCount
task.RetryInterval = retryInterval
if taskType != "" {
task.Type = taskType
}