package controllers import ( "path/filepath" "runtime" "strconv" "encoding/json" "fmt" "net/http" "os" "strings" "time" "github.com/engigu/taskpool/internal/constant" "github.com/engigu/taskpool/internal/database" "github.com/engigu/taskpool/internal/eventbus" "github.com/engigu/taskpool/internal/models" "github.com/engigu/taskpool/internal/models/vo" "github.com/engigu/taskpool/internal/services" "github.com/engigu/taskpool/internal/services/tasks" "github.com/engigu/taskpool/internal/tunnel" "github.com/engigu/taskpool/internal/utils" "github.com/gin-gonic/gin" "github.com/shirou/gopsutil/v3/process" ) type SettingsController struct { userService *services.UserService settingsService *services.SettingsService loginLogService *services.LoginLogService backupService *services.BackupService executorService *tasks.ExecutorService } func NewSettingsController(userService *services.UserService, loginLogService *services.LoginLogService, executorService *tasks.ExecutorService) *SettingsController { return &SettingsController{ userService: userService, settingsService: services.NewSettingsService(), loginLogService: loginLogService, backupService: services.NewBackupService(), executorService: executorService, } } // ChangePassword 修改密码及账号信息 func (sc *SettingsController) ChangePassword(c *gin.Context) { // 演示模式下禁止修改 if constant.DemoMode { utils.BadRequest(c, "演示模式下不能修改账号或密码") return } var req struct { OldUsername string `json:"old_username"` Username string `json:"username"` OldPassword string `json:"old_password" binding:"required"` NewPassword string `json:"new_password"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "参数错误") return } userID := c.GetString("userID") var user *models.User res := database.DB.Where("id = ?", userID).Limit(1).Find(&user) if res.Error != nil || res.RowsAffected == 0 { utils.NotFound(c, "用户不存在") return } // 统一校验原账密 if req.OldUsername != "" && req.OldUsername != user.Username { utils.BadRequest(c, "原账号不正确") return } if !sc.userService.AuthenticateUser(user.Username, req.OldPassword) { utils.BadRequest(c, "原密码错误") return } var updated bool var logoutRequired bool // 1. 处理用户名修改 if req.Username != "" && req.Username != user.Username { if err := sc.userService.UpdateAccount(user.ID, req.Username); err != nil { utils.BadRequest(c, err.Error()) return } updated = true logoutRequired = true } // 2. 处理密码修改 if req.NewPassword != "" { if len(req.NewPassword) < 6 { utils.BadRequest(c, "新密码至少6位") return } if err := sc.userService.UpdatePassword(user.ID, req.NewPassword); err != nil { utils.ServerError(c, "修改密码失败") return } updated = true logoutRequired = true } if !updated { utils.SuccessMsg(c, "未检测到变更内容") return } eventbus.DefaultBus.Publish(eventbus.Event{ Type: constant.EventPasswordChanged, Payload: map[string]interface{}{ "username": user.Username, }, }) msg := "保存成功" if logoutRequired { msg += ",请重新登录" } utils.SuccessMsg(c, msg) } // CleanLogs 清理日志 - 已移除,改为任务级别的日志清理配置 // GetSiteSettings 获取站点设置 func (sc *SettingsController) GetSiteSettings(c *gin.Context) { settings := sc.settingsService.GetSection(constant.SectionSite) // 纠正数据库中的空值,防止因配置冲突被意外置空 if settings[constant.KeyTitle] == "" { settings[constant.KeyTitle] = "任务池" sc.settingsService.Set(constant.SectionSite, constant.KeyTitle, "任务池") } if settings[constant.KeySubtitle] == "" { settings[constant.KeySubtitle] = "极致轻量、高性能的自动化任务调度平台" sc.settingsService.Set(constant.SectionSite, constant.KeySubtitle, "极致轻量、高性能的自动化任务调度平台") } if settings[constant.KeyIcon] == "" { settings[constant.KeyIcon] = constant.DefaultIcon sc.settingsService.Set(constant.SectionSite, constant.KeyIcon, constant.DefaultIcon) } // 解析 JSON 格式的 OpenAPI Token if tokenJson, ok := settings[constant.KeyOpenapiToken]; ok && tokenJson != "" { var tokenConfig vo.TokenConfig if err := json.Unmarshal([]byte(tokenJson), &tokenConfig); err == nil { settings["openapi_token"] = tokenConfig.Token settings["openapi_token_expire"] = tokenConfig.ExpireAt if tokenConfig.Enabled { settings["openapi_enabled"] = "true" } else { settings["openapi_enabled"] = "false" } } } // 获取日志清理配置 settings["system_notice_days"] = sc.settingsService.Get(constant.SectionSystem, constant.KeySystemNoticeDays) settings["system_notice_max_count"] = sc.settingsService.Get(constant.SectionSystem, constant.KeySystemNoticeMaxCount) settings["push_log_days"] = sc.settingsService.Get(constant.SectionSystem, constant.KeyPushLogDays) settings["push_log_max_count"] = sc.settingsService.Get(constant.SectionSystem, constant.KeyPushLogMaxCount) settings["login_log_days"] = sc.settingsService.Get(constant.SectionSystem, constant.KeyLoginLogDays) settings["login_log_max_count"] = sc.settingsService.Get(constant.SectionSystem, constant.KeyLoginLogMaxCount) settings["scheduler_log_days"] = sc.settingsService.Get(constant.SectionSystem, constant.KeySchedulerLogDays) settings["scheduler_log_max_count"] = sc.settingsService.Get(constant.SectionSystem, constant.KeySchedulerLogMaxCount) utils.Success(c, settings) } // GetPublicSiteSettings 获取公开的站点设置(无需认证) func (sc *SettingsController) GetPublicSiteSettings(c *gin.Context) { settings := sc.settingsService.GetSection(constant.SectionSite) title := settings[constant.KeyTitle] if title == "" { title = "任务池" } subtitle := settings[constant.KeySubtitle] if subtitle == "" { subtitle = "极致轻量、高性能的自动化任务调度平台" } icon := settings[constant.KeyIcon] if icon == "" { icon = constant.DefaultIcon } // 只返回公开信息 utils.Success(c, gin.H{ constant.KeyTitle: title, constant.KeySubtitle: subtitle, constant.KeyIcon: icon, "demo_mode": constant.DemoMode, }) } // UpdateSiteSettings 更新站点设置 func (sc *SettingsController) UpdateSiteSettings(c *gin.Context) { var req struct { Title string `json:"title"` Subtitle string `json:"subtitle"` Icon string `json:"icon"` PageSize string `json:"page_size"` CookieDays string `json:"cookie_days"` OpenapiEnabled bool `json:"openapi_enabled"` OpenapiToken string `json:"openapi_token"` OpenapiTokenExpire string `json:"openapi_token_expire"` SystemNoticeDays string `json:"system_notice_days"` SystemNoticeMaxCount string `json:"system_notice_max_count"` PushLogDays string `json:"push_log_days"` PushLogMaxCount string `json:"push_log_max_count"` LoginLogDays string `json:"login_log_days"` LoginLogMaxCount string `json:"login_log_max_count"` SchedulerLogDays string `json:"scheduler_log_days"` SchedulerLogMaxCount string `json:"scheduler_log_max_count"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "参数错误") return } openapiTokenJson := "" if req.OpenapiToken != "" || req.OpenapiTokenExpire != "" || req.OpenapiEnabled { tokenConfig := vo.TokenConfig{ Enabled: req.OpenapiEnabled, Token: req.OpenapiToken, ExpireAt: req.OpenapiTokenExpire, } if b, err := json.Marshal(tokenConfig); err == nil { openapiTokenJson = string(b) } } values := map[string]string{ constant.KeyTitle: req.Title, constant.KeySubtitle: req.Subtitle, constant.KeyIcon: req.Icon, constant.KeyPageSize: req.PageSize, constant.KeyCookieDays: req.CookieDays, constant.KeyOpenapiToken: openapiTokenJson, } if err := sc.settingsService.SetSection(constant.SectionSite, values); err != nil { utils.ServerError(c, "保存失败") return } // 保存日志清理配置 sc.settingsService.Set(constant.SectionSystem, constant.KeySystemNoticeDays, req.SystemNoticeDays) sc.settingsService.Set(constant.SectionSystem, constant.KeySystemNoticeMaxCount, req.SystemNoticeMaxCount) sc.settingsService.Set(constant.SectionSystem, constant.KeyPushLogDays, req.PushLogDays) sc.settingsService.Set(constant.SectionSystem, constant.KeyPushLogMaxCount, req.PushLogMaxCount) sc.settingsService.Set(constant.SectionSystem, constant.KeyLoginLogDays, req.LoginLogDays) sc.settingsService.Set(constant.SectionSystem, constant.KeyLoginLogMaxCount, req.LoginLogMaxCount) sc.settingsService.Set(constant.SectionSystem, constant.KeySchedulerLogDays, req.SchedulerLogDays) sc.settingsService.Set(constant.SectionSystem, constant.KeySchedulerLogMaxCount, req.SchedulerLogMaxCount) utils.SuccessMsg(c, "保存成功") } // GenerateOpenapiToken 随机生成OpenAPI Token func (sc *SettingsController) GenerateOpenapiToken(c *gin.Context) { utils.Success(c, gin.H{ "token": strings.ToLower(utils.RandomString(32)), }) } // GetSchedulerSettings 获取调度设置 func (sc *SettingsController) GetSchedulerSettings(c *gin.Context) { settings := sc.settingsService.GetSection(constant.SectionScheduler) utils.Success(c, settings) } // UpdateSchedulerSettings 更新调度设置 func (sc *SettingsController) UpdateSchedulerSettings(c *gin.Context) { var req struct { WorkerCount string `json:"worker_count"` QueueSize string `json:"queue_size"` RateInterval string `json:"rate_interval"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "参数错误") return } var workerCount int if _, err := fmt.Sscanf(req.WorkerCount, "%d", &workerCount); err != nil || workerCount < 1 || workerCount > 1000 { utils.BadRequest(c, "工作线程数必须在 1 至 1000 之间") return } var queueSize int if _, err := fmt.Sscanf(req.QueueSize, "%d", &queueSize); err != nil || queueSize < 1 || queueSize > 50000 { utils.BadRequest(c, "等待队列容量必须在 1 至 50000 之间") return } var rateInterval int if _, err := fmt.Sscanf(req.RateInterval, "%d", &rateInterval); err != nil || rateInterval < 1 { utils.BadRequest(c, "限频间隔必须为正整数") return } values := map[string]string{ constant.KeyWorkerCount: req.WorkerCount, constant.KeyQueueSize: req.QueueSize, constant.KeyRateInterval: req.RateInterval, } if err := sc.settingsService.SetSection(constant.SectionScheduler, values); err != nil { utils.ServerError(c, "保存失败") return } // 重新加载 executor service if sc.executorService != nil { sc.executorService.Reload() } utils.SuccessMsg(c, "保存成功") } // GetPaths 获取系统路径信息 func (sc *SettingsController) GetPaths(c *gin.Context) { absScriptsDir, _ := filepath.Abs(constant.ScriptsWorkDir) utils.Success(c, gin.H{ "scripts_dir": absScriptsDir, }) } // GetAbout 获取关于信息 func (sc *SettingsController) GetAbout(c *gin.Context) { var taskCount, logCount, envCount int64 database.DB.Model(&models.Task{}).Count(&taskCount) database.DB.Model(&models.TaskLog{}).Count(&logCount) database.DB.Model(&models.EnvironmentVariable{}).Count(&envCount) // 内存使用 memUsage := "N/A" if p, err := process.NewProcess(int32(os.Getpid())); err == nil { if memInfo, err := p.MemoryInfo(); err == nil { memUsage = formatBytes(memInfo.RSS) } } // 运行时间 uptime := formatDuration(time.Since(constant.StartTime)) // 获取远程最新版本 remoteVersion := "" client := &http.Client{Timeout: 2 * time.Second} req, err := http.NewRequest("GET", "https://api.github.com/repos/engigu/taskpool/releases/latest", nil) if err == nil { req.Header.Set("User-Agent", "taskpool") if resp, err := client.Do(req); err == nil { defer resp.Body.Close() var release struct { TagName string `json:"tag_name"` } if err := json.NewDecoder(resp.Body).Decode(&release); err == nil { remoteVersion = release.TagName } } } utils.Success(c, gin.H{ "version": constant.Version, "remote_version": remoteVersion, "build_time": constant.BuildTime, "mem_usage": memUsage, "goroutines": runtime.NumGoroutine(), "uptime": uptime, "task_count": taskCount, "log_count": logCount, "env_count": envCount, }) } // GetChangelog 获取更新日志 func (sc *SettingsController) GetChangelog(c *gin.Context) { content, err := os.ReadFile("docs/guide/changelog.md") if err != nil { utils.Success(c, "暂无更新日志") return } utils.Success(c, string(content)) } // formatBytes 格式化字节数 func formatBytes(bytes uint64) string { const unit = 1024 if bytes < unit { return fmt.Sprintf("%d B", bytes) } div, exp := uint64(unit), 0 for n := bytes / unit; n >= unit; n /= unit { div *= unit exp++ } return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) } // formatDuration 格式化时间间隔 func formatDuration(d time.Duration) string { days := int(d.Hours()) / 24 hours := int(d.Hours()) % 24 minutes := int(d.Minutes()) % 60 seconds := int(d.Seconds()) % 60 if days > 0 { return fmt.Sprintf("%d天%d小时%d分钟%d秒", days, hours, minutes, seconds) } if hours > 0 { return fmt.Sprintf("%d小时%d分钟%d秒", hours, minutes, seconds) } if minutes > 0 { return fmt.Sprintf("%d分钟%d秒", minutes, seconds) } return fmt.Sprintf("%d秒", seconds) } // GetLoginLogs 获取登录日志 func (sc *SettingsController) GetLoginLogs(c *gin.Context) { page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "10")) username := c.Query("username") if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 10 } logs, total, err := sc.loginLogService.List(page, pageSize, username) if err != nil { utils.ServerError(c, "获取登录日志失败") return } // 将 AppLog 转换为 LoginLogVO 返回,保持前端兼容性 vos := make([]*vo.LoginLogVO, len(logs)) for i, log := range logs { vos[i] = &vo.LoginLogVO{ ID: log.ID, Username: log.Title, IP: log.RefID, UserAgent: string(log.Content), Status: log.Status, Message: string(log.ErrorMsg), CreatedAt: log.CreatedAt, } } utils.Success(c, utils.PaginationData{ Data: vos, Total: total, Page: page, PageSize: pageSize, }) } // CreateBackup 创建备份 func (sc *SettingsController) CreateBackup(c *gin.Context) { _, err := sc.backupService.CreateBackup() if err != nil { utils.ServerError(c, "创建备份失败: "+err.Error()) return } utils.SuccessMsg(c, "备份创建成功") } // GetBackupStatus 获取备份状态 func (sc *SettingsController) GetBackupStatus(c *gin.Context) { filePath := sc.backupService.GetBackupFile() var backupTime string if filePath != "" { if info, err := os.Stat(filePath); err == nil { backupTime = info.ModTime().Format("2006-01-02 15:04:05") } } utils.Success(c, gin.H{ "has_backup": filePath != "", "backup_time": backupTime, }) } // DownloadBackup 下载备份文件 func (sc *SettingsController) DownloadBackup(c *gin.Context) { filePath := sc.backupService.GetBackupFile() if filePath == "" { utils.NotFound(c, "没有可下载的备份") return } // 检查文件是否存在 if _, err := os.Stat(filePath); os.IsNotExist(err) { sc.backupService.ClearBackup() utils.NotFound(c, "备份文件不存在") return } // 设置响应头 c.Header("Content-Disposition", "attachment; filename="+filepath.Base(filePath)) c.Header("Content-Type", "application/zip") c.File(filePath) // 下载后清除备份记录和文件 go func() { time.Sleep(time.Minute * 5) // 等待下载完成 sc.backupService.ClearBackup() }() } // RestoreBackup 恢复备份 func (sc *SettingsController) RestoreBackup(c *gin.Context) { file, err := c.FormFile("file") if err != nil { utils.BadRequest(c, "请上传备份文件") return } // 保存上传的文件 tempPath := filepath.Join(os.TempDir(), file.Filename) if err := c.SaveUploadedFile(file, tempPath); err != nil { utils.ServerError(c, "保存文件失败") return } defer os.Remove(tempPath) // 恢复备份 if err := sc.backupService.Restore(tempPath); err != nil { utils.ServerError(c, "恢复失败: "+err.Error()) return } utils.SuccessMsg(c, "恢复成功") } // GetSectionSettings 获取指定 section 的所有设置 func (sc *SettingsController) GetSectionSettings(c *gin.Context) { section := c.Param("section") if section == "" { utils.BadRequest(c, "参数错误") return } settings := sc.settingsService.GetSection(section) utils.Success(c, settings) } // UpdateSectionSettings 批量更新指定 section 的设置 func (sc *SettingsController) UpdateSectionSettings(c *gin.Context) { section := c.Param("section") if section == "" { utils.BadRequest(c, "参数错误") return } var values map[string]string if err := c.ShouldBindJSON(&values); err != nil { utils.BadRequest(c, "参数错误") return } if err := sc.settingsService.SetSection(section, values); err != nil { utils.ServerError(c, "更新失败") return } // 当互联配置发生改变时,通知 tunnel 模块立刻应用新角色,启动或停止相关的后台协程 if section == constant.SectionInterconnect { if role, ok := values[constant.KeyInterconnectRole]; ok { tunnel.ApplyRole(role) } } utils.SuccessMsg(c, "保存成功") } // GetSetting 获取单个设置值 func (sc *SettingsController) GetSetting(c *gin.Context) { section := c.Param("section") key := c.Param("key") if section == "" || key == "" { utils.BadRequest(c, "参数错误") return } value := sc.settingsService.Get(section, key) utils.Success(c, value) } // GenerateSettingToken 为指定设置生成随机token func (sc *SettingsController) GenerateSettingToken(c *gin.Context) { section := c.Param("section") key := c.Param("key") if section == "" || key == "" { utils.BadRequest(c, "参数错误") return } // 生成32位随机token token := strings.ToLower(utils.RandomString(32)) // 保存到数据库 if err := sc.settingsService.Set(section, key, token); err != nil { utils.ServerError(c, "保存失败") return } utils.Success(c, token) }