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

157
handlers/api.go Normal file
View File

@@ -0,0 +1,157 @@
package handlers
import (
"net/http"
"strconv"
"tonav-go/database"
"tonav-go/models"
"github.com/gin-gonic/gin"
)
// API 响应结构
type Response struct {
Success bool `json:"success"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// GetServices 获取所有服务
func GetServices(c *gin.Context) {
var services []models.Service
database.DB.Order("category_id asc, sort_order desc").Find(&services)
c.JSON(http.StatusOK, Response{Success: true, Data: services})
}
// SaveService 创建或更新服务
func SaveService(c *gin.Context) {
var service models.Service
if err := c.ShouldBindJSON(&service); err != nil {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "请求数据格式错误: " + err.Error()})
return
}
// 验证必填字段
if service.Name == "" {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "服务名称不能为空"})
return
}
if service.URL == "" {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "服务地址不能为空"})
return
}
// 如果 URL 路径中有 id使用路径参数
if idStr := c.Param("id"); idStr != "" {
id, err := strconv.ParseUint(idStr, 10, 32)
if err == nil {
service.ID = uint(id)
}
}
if service.ID > 0 {
// 更新时只更新指定字段,避免覆盖 created_at 等
result := database.DB.Model(&models.Service{}).Where("id = ?", service.ID).Updates(map[string]interface{}{
"name": service.Name,
"url": service.URL,
"description": service.Description,
"icon": service.Icon,
"category_id": service.CategoryID,
"tags": service.Tags,
"is_enabled": service.IsEnabled,
"sort_order": service.SortOrder,
"health_check_url": service.HealthCheckURL,
"health_check_enabled": service.HealthCheckEnabled,
})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "更新失败: " + result.Error.Error()})
return
}
} else {
if err := database.DB.Create(&service).Error; err != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "创建失败: " + err.Error()})
return
}
}
c.JSON(http.StatusOK, Response{Success: true, Message: "保存成功", Data: service})
}
// DeleteService 删除服务
func DeleteService(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "缺少服务ID"})
return
}
if err := database.DB.Delete(&models.Service{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, Response{Success: true, Message: "删除成功"})
}
// GetCategories 获取所有分类
func GetCategories(c *gin.Context) {
var categories []models.Category
database.DB.Order("sort_order desc").Find(&categories)
c.JSON(http.StatusOK, Response{Success: true, Data: categories})
}
// SaveCategory 保存分类
func SaveCategory(c *gin.Context) {
var category models.Category
if err := c.ShouldBindJSON(&category); err != nil {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "请求数据格式错误: " + err.Error()})
return
}
if category.Name == "" {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "分类名称不能为空"})
return
}
// 如果 URL 路径中有 id
if idStr := c.Param("id"); idStr != "" {
id, err := strconv.ParseUint(idStr, 10, 32)
if err == nil {
category.ID = uint(id)
}
}
if category.ID > 0 {
result := database.DB.Model(&models.Category{}).Where("id = ?", category.ID).Updates(map[string]interface{}{
"name": category.Name,
"sort_order": category.SortOrder,
})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "更新失败: " + result.Error.Error()})
return
}
} else {
if err := database.DB.Create(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, Response{Success: false, Message: "创建失败: " + err.Error()})
return
}
}
c.JSON(http.StatusOK, Response{Success: true, Message: "保存成功", Data: category})
}
// DeleteCategory 删除分类
func DeleteCategory(c *gin.Context) {
id := c.Param("id")
if id == "" {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "缺少分类ID"})
return
}
// 检查是否有服务属于该分类
var count int64
database.DB.Model(&models.Service{}).Where("category_id = ?", id).Count(&count)
if count > 0 {
c.JSON(http.StatusBadRequest, Response{Success: false, Message: "该分类下仍有服务,无法删除"})
return
}
database.DB.Delete(&models.Category{}, id)
c.JSON(http.StatusOK, Response{Success: true, Message: "删除成功"})
}