package controllers import ( "path/filepath" "runtime" "strconv" "encoding/json" "fmt" "os" "strings" "time" "github.com/engigu/baihu-panel/internal/constant" "github.com/engigu/baihu-panel/internal/database" "github.com/engigu/baihu-panel/internal/eventbus" "github.com/engigu/baihu-panel/internal/models" "github.com/engigu/baihu-panel/internal/models/vo" "github.com/engigu/baihu-panel/internal/services" "github.com/engigu/baihu-panel/internal/services/tasks" "github.com/engigu/baihu-panel/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 { OldPassword string `json:"old_password" binding:"required"` NewPassword string `json:"new_password" binding:"required,min=6"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "参数错误") return } userID := c.GetString("userID") var user *models.User if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil { utils.NotFound(c, "用户不存在") return } if !sc.userService.ValidatePassword(user, req.OldPassword) { utils.BadRequest(c, "原密码错误") return } if err := sc.userService.UpdatePassword(user.ID, req.NewPassword); err != nil { utils.ServerError(c, "修改密码失败") return } eventbus.DefaultBus.Publish(eventbus.Event{ Type: constant.EventPasswordChanged, Payload: map[string]interface{}{ "username": user.Username, }, }) utils.SuccessMsg(c, "密码修改成功") } // CleanLogs 清理日志 - 已移除,改为任务级别的日志清理配置 // GetSiteSettings 获取站点设置 func (sc *SettingsController) GetSiteSettings(c *gin.Context) { settings := sc.settingsService.GetSection(constant.SectionSite) // 解析 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) utils.Success(c, settings) } // GetPublicSiteSettings 获取公开的站点设置(无需认证) func (sc *SettingsController) GetPublicSiteSettings(c *gin.Context) { settings := sc.settingsService.GetSection(constant.SectionSite) // 只返回公开信息 utils.Success(c, gin.H{ constant.KeyTitle: settings[constant.KeyTitle], constant.KeySubtitle: settings[constant.KeySubtitle], constant.KeyIcon: settings[constant.KeyIcon], "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"` } 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) 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 } 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)) utils.Success(c, gin.H{ "version": constant.Version, "build_time": constant.BuildTime, "mem_usage": memUsage, "goroutines": runtime.NumGoroutine(), "uptime": uptime, "task_count": taskCount, "log_count": logCount, "env_count": envCount, }) } // 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, "恢复成功") } // 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) }