feat: ToNav-go v1.0.0 - 内部服务导航系统

功能:
- 前台导航: 分类Tab切换、实时搜索、健康状态指示、响应式适配
- 后台管理: 服务/分类CRUD、系统设置、登录认证(bcrypt)
- 健康检查: 定时检测(5min)、独立检查URL、三态指示(在线/离线/未检测)
- 云端备份: WebDAV上传/下载/恢复/删除、定时自动备份、本地备份管理

技术栈: Go + Gin + GORM + SQLite
This commit is contained in:
2026-02-14 05:09:23 +08:00
commit efaf787981
23 changed files with 2735 additions and 0 deletions

221
handlers/settings.go Normal file
View File

@@ -0,0 +1,221 @@
package handlers
import (
"fmt"
"log"
"net/http"
"path/filepath"
"time"
"tonav-go/database"
"tonav-go/models"
config "tonav-go/utils"
"github.com/gin-gonic/gin"
)
// GetSettings 获取设置
func GetSettings(c *gin.Context) {
var settings []models.Setting
database.DB.Find(&settings)
res := make(map[string]string)
for _, s := range settings {
res[s.Key] = s.Value
}
c.JSON(http.StatusOK, res)
}
// SaveSettings 保存设置
func SaveSettings(c *gin.Context) {
var input map[string]string
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: err.Error()})
return
}
for k, v := range input {
database.DB.Save(&models.Setting{Key: k, Value: v})
}
c.JSON(http.StatusOK, Response{Success: true, Message: "设置已保存"})
}
// getWebDAVClient 从数据库配置构建 WebDAV 客户端
func getWebDAVClient() (*config.WebDAVClient, error) {
var url, user, pass models.Setting
database.DB.Where("key = ?", "webdav_url").First(&url)
database.DB.Where("key = ?", "webdav_user").First(&user)
database.DB.Where("key = ?", "webdav_password").First(&pass)
if url.Value == "" {
return nil, fmt.Errorf("未配置 WebDAV URL")
}
return config.NewWebDAVClient(url.Value, user.Value, pass.Value), nil
}
// RunCloudBackup 执行云端备份
func RunCloudBackup(c *gin.Context) {
client, err := getWebDAVClient()
if err != nil {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: err.Error()})
return
}
// 获取数据库路径
cfg := config.LoadConfig()
backupPath, err := config.CreateBackup(cfg.DBPath)
if err != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "本地备份失败: " + err.Error()})
return
}
// 上传时只用文件名,不带路径
remoteName := filepath.Base(backupPath)
err = client.Upload(backupPath, remoteName)
if err != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "云端上传失败: " + err.Error()})
return
}
// 清理本地旧备份保留最近5份
config.CleanOldBackups(5)
c.JSON(http.StatusOK, Response{Success: true, Message: "备份成功: " + remoteName})
}
// ListCloudBackups 列出云端备份
func ListCloudBackups(c *gin.Context) {
client, err := getWebDAVClient()
if err != nil {
c.JSON(http.StatusOK, gin.H{"files": []interface{}{}})
return
}
files, err := client.List()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"files": files})
}
// DeleteCloudBackup 删除云端备份
func DeleteCloudBackup(c *gin.Context) {
name := c.Query("name")
if name == "" {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "缺少备份文件名"})
return
}
client, err := getWebDAVClient()
if err != nil {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: err.Error()})
return
}
if err := client.Delete(name); err != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "删除失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, Response{Success: true, Message: "已删除: " + name})
}
// RestoreCloudBackup 从云端备份恢复
func RestoreCloudBackup(c *gin.Context) {
name := c.Query("name")
if name == "" {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "缺少备份文件名"})
return
}
client, err := getWebDAVClient()
if err != nil {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: err.Error()})
return
}
cfg := config.LoadConfig()
// 恢复前先备份当前数据库
preBackup, err := config.CreateBackup(cfg.DBPath)
if err != nil {
log.Printf("恢复前备份失败: %v", err)
} else {
log.Printf("恢复前已备份当前数据库: %s", preBackup)
}
// 下载云端备份到临时文件
tmpPath := "backups/restore_tmp.db"
if err := client.Download(name, tmpPath); err != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "下载备份失败: " + err.Error()})
return
}
// 关闭当前数据库连接
sqlDB, err := database.DB.DB()
if err == nil {
sqlDB.Close()
}
// 用下载的备份替换当前数据库
if err := config.ReplaceDB(tmpPath, cfg.DBPath); err != nil {
// 尝试重新连接原数据库
database.InitDB(cfg.DBPath)
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "替换数据库失败: " + err.Error()})
return
}
// 重新初始化数据库连接
database.InitDB(cfg.DBPath)
c.JSON(http.StatusOK, Response{Success: true, Message: fmt.Sprintf("已从 %s 恢复,恢复前备份: %s", name, filepath.Base(preBackup))})
}
// StartAutoBackup 启动定时自动备份
func StartAutoBackup() {
go func() {
for {
// 计算距离下一个凌晨3点的时间
now := time.Now()
next := time.Date(now.Year(), now.Month(), now.Day(), 3, 0, 0, 0, now.Location())
if next.Before(now) {
next = next.Add(24 * time.Hour)
}
timer := time.NewTimer(next.Sub(now))
<-timer.C
// 检查是否启用了自动备份
var setting models.Setting
database.DB.Where("key = ?", "auto_backup").First(&setting)
if setting.Value != "true" {
continue
}
log.Println("开始执行自动备份...")
doAutoBackup()
}
}()
}
func doAutoBackup() {
client, err := getWebDAVClient()
if err != nil {
log.Printf("自动备份失败WebDAV未配置: %v", err)
return
}
cfg := config.LoadConfig()
backupPath, err := config.CreateBackup(cfg.DBPath)
if err != nil {
log.Printf("自动备份失败(本地备份): %v", err)
return
}
remoteName := filepath.Base(backupPath)
if err := client.Upload(backupPath, remoteName); err != nil {
log.Printf("自动备份失败(上传): %v", err)
return
}
config.CleanOldBackups(5)
log.Printf("自动备份成功: %s", remoteName)
}