├── Dockerfile ├── README.md ├── backend-deployment.yaml ├── backend ├── .env ├── config │ └── config.go ├── controllers │ ├── application.go │ ├── auth.go │ ├── hitokoto.go │ ├── hotrank.go │ ├── menu.go │ ├── scrapers.go │ ├── settings.go │ ├── sync_icon.go │ └── user.go ├── database │ └── database.go ├── main.go ├── middlewares │ └── auth.go ├── models │ ├── application.go │ ├── menu.go │ ├── migrate.go │ ├── settings.go │ └── user.go ├── routes │ └── routes.go ├── services │ ├── Hot_Rank.go │ ├── application_service.go │ ├── user_service.go │ ├── web_scraper.go │ └── wotd_hitokoto.go └── utils │ └── jwt.go ├── background.png ├── docker-compose.build.yml ├── docker-compose.image.yml ├── foreground.png ├── frontend-deployment.yaml ├── frontend ├── .env ├── .eslintrc.js ├── .gitignore ├── Dockerfile ├── nginx.conf ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ └── index.html ├── src │ ├── App.vue │ ├── api │ │ └── axios.js │ ├── babel.config.js │ ├── main.js │ ├── router │ │ └── index.js │ ├── store │ │ └── index.js │ └── views │ │ ├── Admin.vue │ │ ├── AppManagement.vue │ │ ├── AppNavigation.vue │ │ ├── Dashboard.vue │ │ ├── Login.vue │ │ ├── MenuManagement.vue │ │ └── SiteSettings.vue └── vue.config.js ├── go.mod ├── initdb ├── 01-init.sql └── 02-znav.sql ├── mysql-deployment.yaml ├── mysql-pv.yaml └── znav-ingress.yaml /Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 Go 官方镜像作为基础镜像 2 | FROM golang:1.20-alpine 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 设置 Go Modules 代理,避免网络问题 8 | ENV GOPROXY=https://goproxy.cn,direct 9 | 10 | # 将当前目录的所有文件复制到容器中 11 | COPY . . 12 | 13 | # 复制 .env 文件到容器中 14 | COPY ./backend/.env .env 15 | 16 | # 进入 backend 目录,并下载安装 Go module 依赖 17 | WORKDIR /app/backend 18 | 19 | # 下载安装 Go module 依赖 20 | RUN go mod tidy 21 | 22 | # 编译 Go 应用为二进制文件 23 | RUN go build -o main . 24 | 25 | # 暴露服务端口(假设 Go 服务运行在 8080 端口) 26 | EXPOSE 8080 27 | 28 | # 设置启动命令 29 | CMD ["./main"] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # znav 2 | ## Docker部署 3 | ```shell 4 | ### 1.拉取项目 5 | mkdir /data && cd /data 6 | git clone https://github.com/zhanghao123321/nav.git 7 | cd nav 8 | ### 2.按需修改docker-compose.image.yml文件 9 | 10 | ### 3.运行打包镜像 11 | docker-compose -f docker-compose.image.yml up -d 12 | 13 | ### 4.重新构建镜像 14 | docker-compose -f docker-compose.build.yml up --build -d 15 | 16 | ### web访问: 17 | http://你的IP 18 | admin 19 | admin 20 | 21 | ``` 22 | 前台: 23 | ![image](https://github.com/zhanghao123321/nav/blob/main/foreground.png) 24 | 25 | 后台: 26 | ![image](https://github.com/zhanghao123321/nav/blob/main/background.png) 27 | 28 | ## K8S部署 29 | ```shell 30 | ### 1.构建服务 31 | mkdir /data && cd /data 32 | git clone https://github.com/zhanghao123321/nav.git 33 | cd nav 34 | kubectl apply -f mysql-pv.yaml 35 | kubectl apply -f mysql-deployment.yaml 36 | kubectl apply -f backend-deployment.yaml 37 | kubectl apply -f frontend-deployment.yaml 38 | kubectl apply -f znav-ingress.yaml 39 | 40 | ### 2.查看服务 41 | kubectl get pod,svc,ingress -n production 42 | ``` 43 | -------------------------------------------------------------------------------- /backend-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: znav-backend 5 | namespace: production 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: znav-backend 11 | template: 12 | metadata: 13 | labels: 14 | app: znav-backend 15 | spec: 16 | containers: 17 | - name: znav-backend 18 | image: registry.cn-shanghai.aliyuncs.com/hooz/nav:backend # 替换为你的后端镜像 19 | env: 20 | - name: DB_USER 21 | value: "root" 22 | - name: DB_PASSWORD 23 | value: "123456" 24 | - name: DB_HOST 25 | value: "znav-mysql" 26 | - name: DB_PORT 27 | value: "3306" 28 | - name: DB_NAME 29 | value: "znav" 30 | - name: JWT_SECRET 31 | value: "supersecret" 32 | ports: 33 | - containerPort: 8080 34 | --- 35 | apiVersion: v1 36 | kind: Service 37 | metadata: 38 | name: znav-backend 39 | namespace: production 40 | spec: 41 | ports: 42 | - port: 8080 43 | targetPort: 8080 44 | selector: 45 | app: znav-backend 46 | -------------------------------------------------------------------------------- /backend/.env: -------------------------------------------------------------------------------- 1 | DB_USER=root 2 | DB_PASSWORD=123456 3 | DB_HOST=192.168.222.173 4 | DB_PORT=3306 5 | DB_NAME=znav 6 | JWT_SECRET="zsadwqdssaf" -------------------------------------------------------------------------------- /backend/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/joho/godotenv" 7 | ) 8 | 9 | func LoadConfig() { 10 | if err := godotenv.Load("D:/GO/znav/backend/.env"); err != nil { 11 | log.Println("No .env file found") 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /backend/controllers/application.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "log" 6 | "net/http" 7 | "strconv" 8 | "znav/backend/database" 9 | "znav/backend/models" 10 | ) 11 | 12 | func CreateApplication(c *gin.Context) { 13 | var app models.Application 14 | if err := c.ShouldBindJSON(&app); err != nil { 15 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 16 | return 17 | } 18 | 19 | if app.MenuID == 0 { 20 | c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid Menu ID"}) 21 | return 22 | } 23 | 24 | db := database.GetDB() 25 | if err := db.Create(&app).Error; err != nil { 26 | log.Println("Error creating application:", err) 27 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create application"}) 28 | return 29 | } 30 | 31 | c.JSON(http.StatusOK, app) 32 | } 33 | 34 | func GetApplications(c *gin.Context) { 35 | var applications []models.Application 36 | // 获取筛选参数 37 | title := c.Query("title") 38 | status := c.Query("status") 39 | menuIdStr := c.Query("menuId") 40 | 41 | // 获取分页参数 42 | pageStr := c.Query("page") 43 | pageSizeStr := c.Query("pageSize") 44 | 45 | // 默认分页参数 46 | page := 1 47 | pageSize := 10 48 | 49 | // 将参数转换为整数 50 | if pageStr != "" { 51 | page, _ = strconv.Atoi(pageStr) 52 | } 53 | if pageSizeStr != "" { 54 | pageSize, _ = strconv.Atoi(pageSizeStr) 55 | } 56 | 57 | // 计算偏移量 58 | offset := (page - 1) * pageSize 59 | 60 | // 查询条件 61 | db := database.GetDB() 62 | query := db.Model(&models.Application{}) 63 | 64 | if title != "" { 65 | query = query.Where("title LIKE ?", "%"+title+"%") 66 | } 67 | if status != "" { 68 | query = query.Where("status = ?", status) 69 | } 70 | 71 | if menuIdStr != "" { 72 | menuID, err := strconv.Atoi(menuIdStr) 73 | if err == nil { 74 | // 获取指定菜单ID以及其子菜单的ID 75 | var menuIDs []uint 76 | menuIDs = append(menuIDs, uint(menuID)) 77 | var subMenus []models.Menu 78 | db.Where("parent_id = ?", menuID).Find(&subMenus) 79 | for _, subMenu := range subMenus { 80 | menuIDs = append(menuIDs, subMenu.ID) 81 | } 82 | query = query.Where("menu_id IN ?", menuIDs) 83 | } 84 | } 85 | 86 | // 查询总数 87 | var total int64 88 | query.Count(&total) 89 | 90 | // 分页查询 91 | if err := query.Limit(pageSize).Offset(offset).Find(&applications).Error; err != nil { 92 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 93 | return 94 | } 95 | 96 | c.JSON(http.StatusOK, gin.H{ 97 | "applications": applications, 98 | "total": total, 99 | }) 100 | } 101 | 102 | func UpdateApplication(c *gin.Context) { 103 | var app models.Application 104 | id := c.Param("id") 105 | 106 | db := database.GetDB() 107 | if err := db.First(&app, id).Error; err != nil { 108 | c.JSON(http.StatusNotFound, gin.H{"error": "Application not found"}) 109 | return 110 | } 111 | 112 | if err := c.ShouldBindJSON(&app); err != nil { 113 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 114 | return 115 | } 116 | 117 | db.Save(&app) 118 | c.JSON(http.StatusOK, app) 119 | } 120 | 121 | func DeleteApplication(c *gin.Context) { 122 | id := c.Param("id") 123 | db := database.GetDB() 124 | if err := db.Delete(&models.Application{}, id).Error; err != nil { 125 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 126 | return 127 | } 128 | 129 | c.JSON(http.StatusOK, gin.H{"message": "Application deleted successfully"}) 130 | } 131 | 132 | func BatchDeleteApplications(c *gin.Context) { 133 | var request struct { 134 | Ids []uint `json:"ids"` 135 | } 136 | 137 | if err := c.ShouldBindJSON(&request); err != nil { 138 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 139 | return 140 | } 141 | 142 | db := database.GetDB() 143 | 144 | if err := db.Where("id IN ?", request.Ids).Delete(&models.Application{}).Error; err != nil { 145 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 146 | return 147 | } 148 | 149 | c.JSON(http.StatusOK, gin.H{"message": "Applications deleted successfully"}) 150 | } 151 | 152 | // 获取应用总数 153 | func GetTotalApplications(c *gin.Context) { 154 | var count int64 155 | db := database.GetDB() 156 | db.Model(&models.Application{}).Count(&count) 157 | c.JSON(http.StatusOK, gin.H{"total": count}) 158 | } 159 | 160 | // 获取最近新增的应用 161 | func GetRecentApplications(c *gin.Context) { 162 | var applications []models.Application 163 | db := database.GetDB() 164 | db.Order("created_at desc").Limit(5).Find(&applications) 165 | c.JSON(http.StatusOK, gin.H{"applications": applications}) 166 | } 167 | -------------------------------------------------------------------------------- /backend/controllers/auth.go: -------------------------------------------------------------------------------- 1 | // controllers/auth.go 2 | 3 | package controllers 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "strings" 9 | "znav/backend/database" 10 | "znav/backend/models" 11 | "znav/backend/utils" 12 | ) 13 | 14 | func Login(c *gin.Context) { 15 | var loginData struct { 16 | Username string `json:"username"` 17 | Password string `json:"password"` 18 | } 19 | 20 | if err := c.ShouldBindJSON(&loginData); err != nil { 21 | c.JSON(http.StatusBadRequest, gin.H{"error": "请求数据无效"}) 22 | return 23 | } 24 | 25 | var user models.User 26 | db := database.GetDB() 27 | if err := db.Where("username = ?", loginData.Username).First(&user).Error; err != nil { 28 | // 用户未注册 29 | c.JSON(http.StatusUnauthorized, gin.H{"error": "用户未注册"}) 30 | return 31 | } 32 | 33 | // 检查用户状态,确保用户是启用状态才能登录 34 | if user.Status == "disabled" { 35 | c.JSON(http.StatusForbidden, gin.H{"error": "用户已停用"}) 36 | return 37 | } 38 | 39 | // 验证密码 40 | if !utils.CheckPasswordHash(loginData.Password, user.Password) { 41 | c.JSON(http.StatusUnauthorized, gin.H{"error": "密码验证失败"}) 42 | return 43 | } 44 | 45 | // 生成 JWT token,设定24小时的过期时间 46 | token, err := utils.GenerateJWT(user.Username) 47 | if err != nil { 48 | c.JSON(http.StatusInternalServerError, gin.H{"error": "生成令牌失败"}) 49 | return 50 | } 51 | 52 | // 将 token 保存到用户记录中 53 | user.Token = token 54 | if err := db.Save(&user).Error; err != nil { 55 | c.JSON(http.StatusInternalServerError, gin.H{"error": "保存令牌失败"}) 56 | return 57 | } 58 | 59 | // 返回 token 给前端 60 | c.JSON(http.StatusOK, gin.H{"token": token}) 61 | } 62 | 63 | func Logout(c *gin.Context) { 64 | token := c.GetHeader("Authorization") 65 | 66 | // 去除 Bearer 前缀 67 | if strings.HasPrefix(token, "Bearer ") { 68 | token = strings.TrimPrefix(token, "Bearer ") 69 | } 70 | 71 | username, err := utils.GetUsernameFromJWT(token) 72 | if err != nil { 73 | c.JSON(http.StatusUnauthorized, gin.H{"error": "无效的令牌"}) 74 | return 75 | } 76 | 77 | // 查找用户并清除 token 78 | db := database.GetDB() 79 | var user models.User 80 | if err := db.Where("username = ?", username).First(&user).Error; err != nil { 81 | c.JSON(http.StatusInternalServerError, gin.H{"error": "用户未找到"}) 82 | return 83 | } 84 | 85 | // 清除用户的 token 86 | user.Token = "" 87 | if err := db.Save(&user).Error; err != nil { 88 | c.JSON(http.StatusInternalServerError, gin.H{"error": "注销失败"}) 89 | return 90 | } 91 | 92 | c.JSON(http.StatusOK, gin.H{"message": "注销成功"}) 93 | } 94 | -------------------------------------------------------------------------------- /backend/controllers/hitokoto.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | "znav/backend/services" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func HitokotoHandler(c *gin.Context) { 11 | // 调用 services.GetHitokoto 函数 12 | hitokoto, err := services.GetHitokoto() 13 | if err != nil { 14 | // 返回错误信息给前端 15 | c.JSON(http.StatusInternalServerError, gin.H{ 16 | "error": "Failed to fetch hitokoto", 17 | "details": err.Error(), 18 | }) 19 | return 20 | } 21 | 22 | // 成功时返回每日一言内容 23 | c.JSON(http.StatusOK, gin.H{ 24 | "hitokoto": hitokoto, 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /backend/controllers/hotrank.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | "znav/backend/services" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | // HotRankHandler 处理热榜请求 11 | func HotRankHandler(c *gin.Context) { 12 | // 获取前端传来的热榜类别 13 | category := c.Query("category") 14 | if category == "" { 15 | c.JSON(http.StatusBadRequest, gin.H{ 16 | "error": "Category is required", 17 | }) 18 | return 19 | } 20 | 21 | // 调用 services 层获取热榜数据 22 | hotRankData, err := services.GetHotRank(category) 23 | if err != nil { 24 | c.JSON(http.StatusInternalServerError, gin.H{ 25 | "error": "Failed to fetch hot rank", 26 | "details": err.Error(), 27 | }) 28 | return 29 | } 30 | 31 | // 返回热榜数据给前端 32 | c.JSON(http.StatusOK, hotRankData) 33 | } 34 | -------------------------------------------------------------------------------- /backend/controllers/menu.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | "strconv" 7 | "znav/backend/database" 8 | "znav/backend/models" 9 | ) 10 | 11 | func CreateMenu(c *gin.Context) { 12 | var menu models.Menu 13 | if err := c.ShouldBindJSON(&menu); err != nil { 14 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 15 | return 16 | } 17 | 18 | db := database.GetDB() 19 | if err := db.Create(&menu).Error; err != nil { 20 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 21 | return 22 | } 23 | 24 | c.JSON(http.StatusOK, menu) 25 | } 26 | 27 | func GetMenus(c *gin.Context) { 28 | title := c.Query("title") 29 | status := c.Query("status") 30 | pageStr := c.Query("page") 31 | pageSizeStr := c.Query("pageSize") 32 | 33 | page := 1 34 | pageSize := 10 35 | 36 | if pageStr != "" { 37 | page, _ = strconv.Atoi(pageStr) 38 | } 39 | if pageSizeStr != "" { 40 | pageSize, _ = strconv.Atoi(pageSizeStr) 41 | } 42 | 43 | offset := (page - 1) * pageSize 44 | 45 | var menus []models.Menu 46 | var total int64 47 | 48 | db := database.GetDB() 49 | query := db.Model(&models.Menu{}) 50 | 51 | if title != "" { 52 | query = query.Where("title LIKE ?", "%"+title+"%") 53 | } 54 | if status != "" { 55 | query = query.Where("status = ?", status) 56 | } 57 | 58 | query.Count(&total) 59 | 60 | query.Preload("Apps").Order("order_id ASC").Limit(pageSize).Offset(offset).Find(&menus) 61 | 62 | c.JSON(http.StatusOK, gin.H{ 63 | "menus": menus, 64 | "total": total, 65 | }) 66 | } 67 | 68 | func UpdateMenu(c *gin.Context) { 69 | var menu models.Menu 70 | id := c.Param("id") 71 | 72 | db := database.GetDB() 73 | if err := db.First(&menu, id).Error; err != nil { 74 | c.JSON(http.StatusNotFound, gin.H{"error": "Menu not found"}) 75 | return 76 | } 77 | 78 | if err := c.ShouldBindJSON(&menu); err != nil { 79 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 80 | return 81 | } 82 | 83 | db.Save(&menu) 84 | c.JSON(http.StatusOK, menu) 85 | } 86 | 87 | func DeleteMenu(c *gin.Context) { 88 | id := c.Param("id") 89 | db := database.GetDB() 90 | if err := db.Delete(&models.Menu{}, id).Error; err != nil { 91 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 92 | return 93 | } 94 | 95 | c.JSON(http.StatusOK, gin.H{"message": "Menu deleted successfully"}) 96 | } 97 | 98 | func BatchDeleteMenus(c *gin.Context) { 99 | var request struct { 100 | Ids []uint `json:"ids"` 101 | } 102 | 103 | if err := c.ShouldBindJSON(&request); err != nil { 104 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 105 | return 106 | } 107 | 108 | db := database.GetDB() 109 | 110 | if err := db.Where("id IN ?", request.Ids).Delete(&models.Menu{}).Error; err != nil { 111 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 112 | return 113 | } 114 | 115 | c.JSON(http.StatusOK, gin.H{"message": "Menus deleted successfully"}) 116 | } 117 | 118 | // 获取菜单总数 119 | func GetTotalMenus(c *gin.Context) { 120 | var count int64 121 | db := database.GetDB() 122 | db.Model(&models.Menu{}).Count(&count) 123 | c.JSON(http.StatusOK, gin.H{"total": count}) 124 | } 125 | -------------------------------------------------------------------------------- /backend/controllers/scrapers.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | "znav/backend/services" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func ScrapeWebsiteHandler(c *gin.Context) { 11 | url := c.Query("url") 12 | if url == "" { 13 | c.JSON(http.StatusBadRequest, gin.H{"error": "URL is required"}) 14 | return 15 | } 16 | 17 | data, err := services.ScrapeWebsite(url) 18 | if err != nil { 19 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 20 | return 21 | } 22 | 23 | c.JSON(http.StatusOK, data) 24 | } 25 | -------------------------------------------------------------------------------- /backend/controllers/settings.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "net/http" 5 | "znav/backend/database" 6 | "znav/backend/models" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func GetSiteSettings(c *gin.Context) { 12 | var settings models.SiteSettings 13 | db := database.GetDB() 14 | 15 | // 使用固定的 ID 查询 16 | if err := db.First(&settings, 1).Error; err != nil { 17 | c.JSON(http.StatusNotFound, gin.H{"error": "Settings not found"}) 18 | return 19 | } 20 | 21 | c.JSON(http.StatusOK, settings) 22 | } 23 | 24 | func UpdateSiteSettings(c *gin.Context) { 25 | var settings models.SiteSettings 26 | db := database.GetDB() 27 | 28 | // 使用固定的 ID 查询 29 | if err := db.First(&settings, 1).Error; err != nil { 30 | // 如果记录不存在,创建新记录并设置 ID 为 1 31 | settings.ID = 1 32 | if err := c.ShouldBindJSON(&settings); err != nil { 33 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 34 | return 35 | } 36 | if err := db.Create(&settings).Error; err != nil { 37 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 38 | return 39 | } 40 | } else { 41 | // 更新现有的记录 42 | if err := c.ShouldBindJSON(&settings); err != nil { 43 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 44 | return 45 | } 46 | settings.ID = 1 // 确保 ID 不变 47 | if err := db.Save(&settings).Error; err != nil { 48 | c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) 49 | return 50 | } 51 | } 52 | 53 | c.JSON(http.StatusOK, settings) 54 | } 55 | -------------------------------------------------------------------------------- /backend/controllers/sync_icon.go: -------------------------------------------------------------------------------- 1 | // controllers/controllers.go 2 | 3 | package controllers 4 | 5 | import ( 6 | "bytes" 7 | "encoding/json" 8 | "fmt" 9 | "io" 10 | "mime/multipart" 11 | "net/http" 12 | "time" 13 | "znav/backend/database" 14 | "znav/backend/models" 15 | 16 | "github.com/gin-gonic/gin" 17 | ) 18 | 19 | func SyncIconHandler(c *gin.Context) { 20 | var requestData struct { 21 | IconURL string `json:"icon_url"` 22 | Token string `json:"token"` 23 | } 24 | 25 | if err := c.ShouldBindJSON(&requestData); err != nil { 26 | c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"}) 27 | return 28 | } 29 | 30 | if requestData.IconURL == "" { 31 | c.JSON(http.StatusBadRequest, gin.H{"error": "Icon URL is required"}) 32 | return 33 | } 34 | 35 | if requestData.Token == "" { 36 | c.JSON(http.StatusBadRequest, gin.H{"error": "Token is required"}) 37 | return 38 | } 39 | 40 | // 下载图标 41 | iconData, err := downloadImage(requestData.IconURL) 42 | if err != nil { 43 | c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to download icon: %v", err)}) 44 | return 45 | } 46 | 47 | // 上传图标到图床 48 | uploadedURL, err := uploadImage(iconData, requestData.Token) 49 | if err != nil { 50 | c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to upload icon: %v", err)}) 51 | return 52 | } 53 | 54 | // 成功时,返回图床上传成功后的 URL 55 | c.JSON(http.StatusOK, gin.H{"icon_url": uploadedURL}) 56 | } 57 | 58 | func uploadImage(imageData []byte, token string) (string, error) { 59 | uploadURL := "https://img.ink/api/upload" 60 | 61 | var body bytes.Buffer 62 | writer := multipart.NewWriter(&body) 63 | 64 | // 设置图片文件的字段 65 | part, err := writer.CreateFormFile("image", "icon.png") 66 | if err != nil { 67 | return "", fmt.Errorf("创建文件字段失败: %w", err) 68 | } 69 | 70 | _, err = part.Write(imageData) 71 | if err != nil { 72 | return "", fmt.Errorf("写入图片数据失败: %w", err) 73 | } 74 | 75 | // 设置文件夹字段为 "icofolder" 76 | err = writer.WriteField("folder", "icofolder") 77 | if err != nil { 78 | return "", fmt.Errorf("写入 folder 字段失败: %w", err) 79 | } 80 | 81 | err = writer.Close() 82 | if err != nil { 83 | return "", fmt.Errorf("关闭写入器失败: %w", err) 84 | } 85 | 86 | // 创建请求 87 | req, err := http.NewRequest("POST", uploadURL, &body) 88 | if err != nil { 89 | return "", fmt.Errorf("创建上传请求失败: %w", err) 90 | } 91 | 92 | // 设置 token 头,而不是 Authorization 93 | req.Header.Set("token", token) 94 | req.Header.Set("Content-Type", writer.FormDataContentType()) 95 | 96 | client := &http.Client{Timeout: 10 * time.Second} 97 | resp, err := client.Do(req) 98 | if err != nil { 99 | return "", fmt.Errorf("上传请求失败: %w", err) 100 | } 101 | defer resp.Body.Close() 102 | 103 | respBody, err := io.ReadAll(resp.Body) 104 | if err != nil { 105 | return "", fmt.Errorf("读取响应体失败: %w", err) 106 | } 107 | 108 | // 打印图床返回的状态码和响应体 109 | //fmt.Printf("图床返回的状态码: %d\n", resp.StatusCode) 110 | //fmt.Printf("图床返回的响应体: %s\n", string(respBody)) 111 | 112 | if resp.StatusCode != http.StatusOK { 113 | return "", fmt.Errorf("上传失败,状态码: %d,响应: %s", resp.StatusCode, string(respBody)) 114 | } 115 | 116 | // 解析图床返回的结果 117 | var result struct { 118 | Code int `json:"code"` 119 | Msg string `json:"msg"` 120 | Data struct { 121 | URL string `json:"url"` 122 | } `json:"data"` 123 | } 124 | 125 | err = json.Unmarshal(respBody, &result) 126 | if err != nil { 127 | return "", fmt.Errorf("解析上传响应失败: %w", err) 128 | } 129 | 130 | if result.Code != 200 { 131 | return "", fmt.Errorf("上传失败: %s", result.Msg) 132 | } 133 | 134 | return result.Data.URL, nil 135 | } 136 | 137 | func downloadImage(imageURL string) ([]byte, error) { 138 | client := &http.Client{ 139 | Timeout: 10 * time.Second, 140 | } 141 | 142 | req, err := http.NewRequest("GET", imageURL, nil) 143 | if err != nil { 144 | return nil, fmt.Errorf("创建请求失败: %w", err) 145 | } 146 | req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Scraper/1.0)") 147 | 148 | resp, err := client.Do(req) 149 | if err != nil { 150 | return nil, fmt.Errorf("请求失败: %w", err) 151 | } 152 | defer resp.Body.Close() 153 | 154 | if resp.StatusCode != http.StatusOK { 155 | return nil, fmt.Errorf("请求失败: %d %s", resp.StatusCode, resp.Status) 156 | } 157 | 158 | imageData, err := io.ReadAll(resp.Body) 159 | if err != nil { 160 | return nil, fmt.Errorf("读取图片数据失败: %w", err) 161 | } 162 | 163 | return imageData, nil 164 | } 165 | 166 | // 保存图床 Token 的接口 167 | func SaveImageHostTokenHandler(c *gin.Context) { 168 | var requestData struct { 169 | Token string `json:"token"` 170 | } 171 | 172 | if err := c.ShouldBindJSON(&requestData); err != nil { 173 | c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request data"}) 174 | return 175 | } 176 | 177 | if requestData.Token == "" { 178 | c.JSON(http.StatusBadRequest, gin.H{"error": "Token is required"}) 179 | return 180 | } 181 | 182 | db := database.GetDB() 183 | 184 | var settings models.SiteSettings 185 | if err := db.First(&settings, 1).Error; err != nil { 186 | c.JSON(http.StatusInternalServerError, gin.H{"error": "无法获取站点设置"}) 187 | return 188 | } 189 | 190 | settings.ImageHostToken = requestData.Token 191 | 192 | if err := db.Save(&settings).Error; err != nil { 193 | c.JSON(http.StatusInternalServerError, gin.H{"error": "无法保存 Token"}) 194 | return 195 | } 196 | 197 | c.JSON(http.StatusOK, gin.H{"message": "Token 保存成功"}) 198 | } 199 | -------------------------------------------------------------------------------- /backend/controllers/user.go: -------------------------------------------------------------------------------- 1 | // controllers/users.go 2 | 3 | package controllers 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "regexp" 9 | "time" 10 | "znav/backend/database" 11 | "znav/backend/models" 12 | "znav/backend/utils" 13 | ) 14 | 15 | // 获取用户列表 16 | func GetUsers(c *gin.Context) { 17 | // 从中间件中获取当前用户 18 | currentUser, exists := c.Get("user") 19 | if !exists { 20 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 21 | return 22 | } 23 | 24 | currentUserData, ok := currentUser.(models.User) 25 | if !ok { 26 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current user"}) 27 | return 28 | } 29 | 30 | var users []models.User 31 | db := database.GetDB() 32 | 33 | if currentUserData.IsAdmin { 34 | // 管理员用户,返回所有用户 35 | if err := db.Find(&users).Error; err != nil { 36 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) 37 | return 38 | } 39 | } else { 40 | // 普通用户,只返回自己的信息 41 | if err := db.Where("id = ?", currentUserData.ID).Find(&users).Error; err != nil { 42 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"}) 43 | return 44 | } 45 | } 46 | 47 | c.JSON(http.StatusOK, gin.H{"users": users}) 48 | } 49 | 50 | // 获取当前登录用户信息 51 | func GetCurrentUser(c *gin.Context) { 52 | // 从中间件中获取用户信息 53 | user, exists := c.Get("user") 54 | if !exists { 55 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 56 | return 57 | } 58 | 59 | // 返回用户信息 60 | c.JSON(http.StatusOK, gin.H{"user": user}) 61 | } 62 | 63 | // 创建用户 64 | func CreateUser(c *gin.Context) { 65 | // 从中间件中获取当前用户 66 | currentUser, exists := c.Get("user") 67 | if !exists { 68 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 69 | return 70 | } 71 | 72 | currentUserData, ok := currentUser.(models.User) 73 | if !ok { 74 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current user"}) 75 | return 76 | } 77 | 78 | // 仅管理员可以创建新用户 79 | if !currentUserData.IsAdmin { 80 | c.JSON(http.StatusForbidden, gin.H{"error": "Only admin users can create new users"}) 81 | return 82 | } 83 | 84 | var user models.User 85 | if err := c.ShouldBindJSON(&user); err != nil { 86 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 87 | return 88 | } 89 | 90 | // 验证用户名格式: 只允许英文和数字 91 | match, _ := regexp.MatchString(`^[a-zA-Z0-9]+$`, user.Username) 92 | if !match { 93 | c.JSON(http.StatusBadRequest, gin.H{"error": "Username can only contain letters and numbers"}) 94 | return 95 | } 96 | 97 | // 验证密码长度: 至少5个字符 98 | if len(user.Password) < 5 { 99 | c.JSON(http.StatusBadRequest, gin.H{"error": "Password must be at least 5 characters long"}) 100 | return 101 | } 102 | 103 | // 对密码进行哈希处理 104 | hashedPassword, err := utils.HashPassword(user.Password) 105 | if err != nil { 106 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) 107 | return 108 | } 109 | user.Password = hashedPassword 110 | 111 | // 设置创建时间 112 | now := time.Now() 113 | user.CreatedAt = now 114 | user.LoginAt = &now 115 | 116 | db := database.GetDB() 117 | if err := db.Create(&user).Error; err != nil { 118 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"}) 119 | return 120 | } 121 | 122 | c.JSON(http.StatusOK, user) 123 | } 124 | 125 | // 更新用户密码 126 | func UpdateUserPassword(c *gin.Context) { 127 | var req struct { 128 | ID uint `json:"id"` 129 | CurrentPassword string `json:"current_password"` 130 | NewPassword string `json:"new_password"` 131 | } 132 | 133 | if err := c.ShouldBindJSON(&req); err != nil { 134 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 135 | return 136 | } 137 | 138 | // 获取当前登录用户 139 | currentUser, exists := c.Get("user") 140 | if !exists { 141 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 142 | return 143 | } 144 | 145 | currentUserData, ok := currentUser.(models.User) 146 | if !ok { 147 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current user"}) 148 | return 149 | } 150 | 151 | // 检查权限:用户只能修改自己的密码,管理员可以修改任何用户的密码 152 | if currentUserData.ID != req.ID && !currentUserData.IsAdmin { 153 | c.JSON(http.StatusForbidden, gin.H{"error": "You can only change your own password"}) 154 | return 155 | } 156 | 157 | // 获取要修改的用户 158 | var user models.User 159 | db := database.GetDB() 160 | if err := db.Where("id = ?", req.ID).First(&user).Error; err != nil { 161 | c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) 162 | return 163 | } 164 | 165 | // 如果不是管理员,需要验证原密码 166 | if !currentUserData.IsAdmin { 167 | if err := utils.CheckPassword(req.CurrentPassword, user.Password); err != nil { 168 | // 返回明确的错误信息 169 | c.JSON(http.StatusBadRequest, gin.H{"error": "原密码不正确"}) 170 | return 171 | } 172 | } 173 | 174 | // 更新密码 175 | hashedPassword, err := utils.HashPassword(req.NewPassword) 176 | if err != nil { 177 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) 178 | return 179 | } 180 | 181 | if err := db.Model(&user).Update("password", hashedPassword).Error; err != nil { 182 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update password"}) 183 | return 184 | } 185 | 186 | c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) 187 | } 188 | 189 | func UpdateUserStatus(c *gin.Context) { 190 | var req struct { 191 | ID uint `json:"id"` 192 | Status string `json:"status"` 193 | } 194 | 195 | if err := c.ShouldBindJSON(&req); err != nil { 196 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 197 | return 198 | } 199 | 200 | // 验证当前用户是否为管理员 201 | currentUser, exists := c.Get("user") 202 | if !exists { 203 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 204 | return 205 | } 206 | 207 | currentUserData, ok := currentUser.(models.User) 208 | if !ok { 209 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current user"}) 210 | return 211 | } 212 | 213 | if !currentUserData.IsAdmin { 214 | c.JSON(http.StatusForbidden, gin.H{"error": "Only administrators can change user status"}) 215 | return 216 | } 217 | 218 | // 不允许管理员修改自己的状态 219 | if currentUserData.ID == req.ID { 220 | c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change your own status"}) 221 | return 222 | } 223 | 224 | // 更新用户状态 225 | var user models.User 226 | db := database.GetDB() 227 | if err := db.First(&user, req.ID).Error; err != nil { 228 | c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) 229 | return 230 | } 231 | 232 | user.Status = req.Status 233 | if err := db.Save(&user).Error; err != nil { 234 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user status"}) 235 | return 236 | } 237 | 238 | c.JSON(http.StatusOK, gin.H{"message": "User status updated successfully"}) 239 | } 240 | 241 | func UpdateUserAdminStatus(c *gin.Context) { 242 | var req struct { 243 | ID uint `json:"id"` 244 | IsAdmin bool `json:"is_admin"` 245 | } 246 | 247 | if err := c.ShouldBindJSON(&req); err != nil { 248 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 249 | return 250 | } 251 | 252 | // 验证当前用户是否为管理员 253 | currentUser, exists := c.Get("user") 254 | if !exists { 255 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 256 | return 257 | } 258 | 259 | currentUserData, ok := currentUser.(models.User) 260 | if !ok { 261 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current user"}) 262 | return 263 | } 264 | 265 | if !currentUserData.IsAdmin { 266 | c.JSON(http.StatusForbidden, gin.H{"error": "Only administrators can change admin status"}) 267 | return 268 | } 269 | 270 | // 不允许管理员修改自己的管理员权限 271 | if currentUserData.ID == req.ID { 272 | c.JSON(http.StatusBadRequest, gin.H{"error": "Cannot change your own admin status"}) 273 | return 274 | } 275 | 276 | // 更新用户的管理员权限 277 | var user models.User 278 | db := database.GetDB() 279 | if err := db.First(&user, req.ID).Error; err != nil { 280 | c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) 281 | return 282 | } 283 | 284 | user.IsAdmin = req.IsAdmin 285 | if err := db.Save(&user).Error; err != nil { 286 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user admin status"}) 287 | return 288 | } 289 | 290 | c.JSON(http.StatusOK, gin.H{"message": "User admin status updated successfully"}) 291 | } 292 | 293 | // 删除用户 294 | func DeleteUser(c *gin.Context) { 295 | id := c.Param("id") 296 | 297 | // 获取当前登录用户 298 | currentUser, exists := c.Get("user") 299 | if !exists { 300 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 301 | return 302 | } 303 | 304 | currentUserData, ok := currentUser.(models.User) 305 | if !ok { 306 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current user"}) 307 | return 308 | } 309 | 310 | // 仅管理员可以删除用户 311 | if !currentUserData.IsAdmin { 312 | c.JSON(http.StatusForbidden, gin.H{"error": "Only admin users can delete users"}) 313 | return 314 | } 315 | 316 | // 获取数据库连接 317 | db := database.GetDB() 318 | var user models.User 319 | 320 | // 查找用户 321 | if err := db.First(&user, id).Error; err != nil { 322 | c.JSON(http.StatusNotFound, gin.H{"error": "User not found"}) 323 | return 324 | } 325 | 326 | // 检查是否是当前登录用户 327 | if currentUserData.ID == user.ID { 328 | c.JSON(http.StatusForbidden, gin.H{"error": "Cannot delete your own account"}) 329 | return 330 | } 331 | 332 | // 尝试删除用户 333 | if err := db.Delete(&user).Error; err != nil { 334 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"}) 335 | return 336 | } 337 | 338 | c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"}) 339 | } 340 | 341 | // 更新用户信息 342 | func UpdateUser(c *gin.Context) { 343 | var updatedUser models.User 344 | 345 | // 获取当前登录用户 346 | currentUser, exists := c.Get("user") 347 | if !exists { 348 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 349 | return 350 | } 351 | 352 | currentUserData, ok := currentUser.(models.User) 353 | if !ok { 354 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve current user"}) 355 | return 356 | } 357 | 358 | // 仅管理员可以更新用户信息 359 | if !currentUserData.IsAdmin { 360 | c.JSON(http.StatusForbidden, gin.H{"error": "Only admin users can update user information"}) 361 | return 362 | } 363 | 364 | // 绑定更新的数据 365 | if err := c.ShouldBindJSON(&updatedUser); err != nil { 366 | c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) 367 | return 368 | } 369 | 370 | // 防止修改自己的管理员状态 371 | if currentUserData.ID == updatedUser.ID && currentUserData.IsAdmin != updatedUser.IsAdmin { 372 | c.JSON(http.StatusForbidden, gin.H{"error": "You cannot change your own admin status"}) 373 | return 374 | } 375 | 376 | // 调试:打印接收到的数据 377 | // log.Printf("Updating user ID: %d, IsAdmin: %v", updatedUser.ID, updatedUser.IsAdmin) 378 | 379 | // 准备要更新的数据 380 | updateData := map[string]interface{}{ 381 | "Status": updatedUser.Status, 382 | "IsAdmin": updatedUser.IsAdmin, 383 | } 384 | 385 | // 如果密码不为空,则对密码进行哈希处理并添加到更新数据中 386 | if updatedUser.Password != "" { 387 | hashedPassword, err := utils.HashPassword(updatedUser.Password) 388 | if err != nil { 389 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"}) 390 | return 391 | } 392 | updateData["Password"] = hashedPassword 393 | } 394 | 395 | // 更新用户信息 396 | db := database.GetDB() 397 | if err := db.Model(&models.User{}).Where("id = ?", updatedUser.ID).Updates(updateData).Error; err != nil { 398 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"}) 399 | return 400 | } 401 | 402 | c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"}) 403 | } 404 | 405 | // 获取用户总数 406 | func GetTotalUsers(c *gin.Context) { 407 | var count int64 408 | db := database.GetDB() 409 | db.Model(&models.User{}).Count(&count) 410 | c.JSON(http.StatusOK, gin.H{"total": count}) 411 | } 412 | -------------------------------------------------------------------------------- /backend/database/database.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "fmt" 5 | "github.com/joho/godotenv" // 导入 godotenv 用于读取 .env 文件 6 | "gorm.io/driver/mysql" 7 | "gorm.io/gorm" 8 | "log" 9 | "os" 10 | "znav/backend/models" 11 | ) 12 | 13 | var DB *gorm.DB 14 | 15 | func InitDB() { 16 | // 加载 .env 文件中的环境变量 17 | err := godotenv.Load() 18 | if err != nil { 19 | log.Println("No .env file found or unable to load .env file.") 20 | } 21 | 22 | // 从环境变量中读取数据库配置信息 23 | dbUser := os.Getenv("DB_USER") 24 | dbPassword := os.Getenv("DB_PASSWORD") 25 | dbHost := os.Getenv("DB_HOST") 26 | dbPort := os.Getenv("DB_PORT") 27 | dbName := os.Getenv("DB_NAME") 28 | 29 | // 拼接成 DSN (Data Source Name) 30 | dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Asia%%2FShanghai", dbUser, dbPassword, dbHost, dbPort, dbName) 31 | 32 | // 打印调试信息 33 | log.Println("Connecting to database with DSN:", dsn) 34 | 35 | // 连接数据库 36 | DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{}) 37 | if err != nil { 38 | log.Fatal("Failed to connect to database: ", err) 39 | } 40 | 41 | // 自动迁移数据库 42 | models.Migrate(DB) 43 | } 44 | 45 | func GetDB() *gorm.DB { 46 | return DB 47 | } 48 | -------------------------------------------------------------------------------- /backend/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | "znav/backend/config" 8 | "znav/backend/database" 9 | "znav/backend/models" 10 | "znav/backend/routes" 11 | "znav/backend/utils" 12 | ) 13 | 14 | func init() { 15 | loc, err := time.LoadLocation("Asia/Shanghai") 16 | if err != nil { 17 | log.Fatalf("Failed to load location: %v", err) 18 | } 19 | time.Local = loc 20 | } 21 | 22 | func main() { 23 | // 加载配置 24 | config.LoadConfig() 25 | 26 | // 初始化数据库连接 27 | database.InitDB() 28 | db := database.GetDB() 29 | 30 | // 确保数据库迁移已经执行 31 | models.Migrate(db) 32 | 33 | now := time.Now() // 先将当前时间保存到变量中 34 | 35 | // 确保默认的 admin 用户存在 36 | var user models.User 37 | if err := db.Where("username = ?", "admin").First(&user).Error; err != nil { 38 | // 如果找不到用户,则创建默认的 admin 用户 39 | hashedPassword, err := utils.HashPassword("admin") 40 | if err != nil { 41 | log.Fatalf("Failed to hash default admin password: %v", err) 42 | } 43 | // 创建默认的 admin 用户 44 | user = models.User{ 45 | Username: "admin", 46 | Password: hashedPassword, 47 | LoginAt: &now, 48 | Status: "enabled", // 默认启用 49 | IsAdmin: true, // 设置为管理员 50 | } 51 | 52 | if err := db.Create(&user).Error; err != nil { 53 | log.Fatalf("Failed to create default admin user: %v", err) 54 | } 55 | log.Println("Initialized default admin user with username: admin and password: admin") 56 | } else { 57 | // 如果用户已经存在,确保其 IsAdmin 字段为 true 58 | if !user.IsAdmin { 59 | user.IsAdmin = true 60 | if err := db.Save(&user).Error; err != nil { 61 | log.Fatalf("Failed to update admin user to administrator: %v", err) 62 | } 63 | log.Println("Updated existing admin user to have administrator privileges") 64 | } else { 65 | log.Println("Admin user already exists and is an administrator") 66 | } 67 | } 68 | 69 | // 设置路由 70 | router := routes.SetupRouter() 71 | 72 | // 启动 HTTP 服务器 73 | log.Println("Starting server on :8080") 74 | if err := http.ListenAndServe(":8080", router); err != nil { 75 | log.Fatalf("Server failed to start: %v", err) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /backend/middlewares/auth.go: -------------------------------------------------------------------------------- 1 | // middlewares/auth.go 2 | 3 | package middlewares 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "strings" 9 | "znav/backend/database" 10 | "znav/backend/models" 11 | "znav/backend/utils" 12 | ) 13 | 14 | func AuthMiddleware() gin.HandlerFunc { 15 | return func(c *gin.Context) { 16 | token := c.GetHeader("Authorization") 17 | 18 | if token == "" || !strings.HasPrefix(token, "Bearer ") { 19 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"}) 20 | c.Abort() 21 | return 22 | } 23 | 24 | token = strings.TrimPrefix(token, "Bearer ") 25 | 26 | // 验证 JWT 27 | username, err := utils.GetUsernameFromJWT(token) 28 | if err != nil { 29 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"}) 30 | c.Abort() 31 | return 32 | } 33 | 34 | // 从数据库获取用户信息 35 | var user models.User 36 | db := database.GetDB() 37 | if err := db.Where("username = ?", username).First(&user).Error; err != nil { 38 | c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"}) 39 | c.Abort() 40 | return 41 | } 42 | 43 | // 检查用户状态 44 | if user.Status == "disabled" { 45 | c.JSON(http.StatusUnauthorized, gin.H{"error": "Your account has been disabled"}) 46 | c.Abort() 47 | return 48 | } 49 | 50 | // 将用户信息存储到上下文中 51 | c.Set("user", user) 52 | 53 | c.Next() 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /backend/models/application.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | type Application struct { 9 | ID uint `json:"id" gorm:"primary_key"` 10 | Title string `json:"title"` 11 | Icon string `json:"icon"` 12 | IconColor string `json:"icon_color"` // 添加这个字段 13 | Link string `json:"link"` 14 | Description string `json:"description"` 15 | Status string `json:"status"` 16 | MenuID uint `json:"menu_id"` 17 | OrderID int `json:"order_id" gorm:"default:0"` // 设置默认值为0 18 | CreatedAt time.Time `json:"created_at"` 19 | UpdatedAt time.Time `json:"updated_at"` 20 | } 21 | 22 | func (app *Application) BeforeCreate(tx *gorm.DB) (err error) { 23 | loc, _ := time.LoadLocation("Asia/Shanghai") // 设置时区为 Asia/Shanghai 24 | app.CreatedAt = time.Now().In(loc) // 使用当前时区的时间 25 | app.UpdatedAt = time.Now().In(loc) 26 | return 27 | } 28 | 29 | func (app *Application) BeforeUpdate(tx *gorm.DB) (err error) { 30 | loc, _ := time.LoadLocation("Asia/Shanghai") // 确保更新时间也使用正确的时区 31 | app.UpdatedAt = time.Now().In(loc) 32 | return 33 | } 34 | -------------------------------------------------------------------------------- /backend/models/menu.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | type Menu struct { 9 | ID uint `json:"id" gorm:"primary_key"` 10 | Title string `json:"title"` 11 | Icon string `json:"icon"` 12 | IconColor string `json:"icon_color"` // 添加这个字段 13 | Status string `json:"status"` 14 | OrderID int `json:"order_id" gorm:"default:0"` // 设置默认值为0 15 | ParentID *uint `json:"parent_id"` // 新增父级菜单字段,允许为 null 16 | Apps []Application `json:"apps" gorm:"foreignkey:MenuID"` 17 | CreatedAt time.Time `json:"created_at"` 18 | UpdatedAt time.Time `json:"updated_at"` 19 | gorm.Model `gorm:"-"` 20 | } 21 | 22 | // BeforeCreate 钩子,在创建数据之前执行 23 | func (menu *Menu) BeforeCreate(tx *gorm.DB) (err error) { 24 | loc, _ := time.LoadLocation("Asia/Shanghai") // 设置时区为 Asia/Shanghai 25 | menu.CreatedAt = time.Now().In(loc) // 使用当前时区的时间 26 | menu.UpdatedAt = time.Now().In(loc) 27 | return 28 | } 29 | 30 | // BeforeUpdate 钩子,在更新数据之前执行 31 | func (menu *Menu) BeforeUpdate(tx *gorm.DB) (err error) { 32 | loc, _ := time.LoadLocation("Asia/Shanghai") // 确保更新时间也使用正确的时区 33 | menu.UpdatedAt = time.Now().In(loc) 34 | return 35 | } 36 | -------------------------------------------------------------------------------- /backend/models/migrate.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | ) 6 | 7 | func Migrate(db *gorm.DB) { 8 | db.AutoMigrate(&User{}, &Application{}, &Menu{}, &SiteSettings{}) 9 | } 10 | -------------------------------------------------------------------------------- /backend/models/settings.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type SiteSettings struct { 8 | ID uint `json:"id" gorm:"primaryKey"` 9 | Title string `json:"title"` 10 | Icon string `json:"icon"` 11 | IconColor string `json:"icon_color"` 12 | Footer string `json:"footer"` 13 | Icp string `json:"icp"` 14 | ImageHostToken string `json:"image_host_token"` // 保留图床 Token 15 | CreatedAt time.Time `json:"created_at"` 16 | UpdatedAt time.Time `json:"updated_at"` 17 | } 18 | -------------------------------------------------------------------------------- /backend/models/user.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import ( 4 | "gorm.io/gorm" 5 | "time" 6 | ) 7 | 8 | type User struct { 9 | ID uint `json:"id" gorm:"primaryKey"` 10 | Username string `json:"username"` 11 | Password string `json:"password"` 12 | LoginAt *time.Time `json:"login_at,omitempty"` // 确保这个字段的类型正确 13 | Status string `json:"status"` // 新增字段,表示启用或停用状态 14 | IsAdmin bool `json:"is_admin"` // 新增的字段,是否启动管理员 15 | Token string `json:"token"` 16 | CreatedAt time.Time `json:"created_at"` 17 | UpdatedAt time.Time `json:"updated_at"` 18 | DeletedAt gorm.DeletedAt `json:"deleted_at,omitempty" gorm:"index"` 19 | } 20 | 21 | // BeforeCreate 钩子 22 | func (user *User) BeforeCreate(tx *gorm.DB) (err error) { 23 | loc, _ := time.LoadLocation("Asia/Shanghai") 24 | user.CreatedAt = time.Now().In(loc) 25 | user.UpdatedAt = time.Now().In(loc) 26 | return 27 | } 28 | 29 | // BeforeUpdate 钩子 30 | func (user *User) BeforeUpdate(tx *gorm.DB) (err error) { 31 | loc, _ := time.LoadLocation("Asia/Shanghai") 32 | user.UpdatedAt = time.Now().In(loc) 33 | return 34 | } 35 | -------------------------------------------------------------------------------- /backend/routes/routes.go: -------------------------------------------------------------------------------- 1 | // routes/routes.go 2 | 3 | package routes 4 | 5 | import ( 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-gonic/gin" 8 | "znav/backend/controllers" 9 | "znav/backend/middlewares" 10 | ) 11 | 12 | func SetupRouter() *gin.Engine { 13 | router := gin.Default() 14 | 15 | // 配置 CORS 中间件 16 | config := cors.DefaultConfig() 17 | config.AllowAllOrigins = true 18 | config.AllowMethods = []string{"GET", "POST", "PUT", "DELETE"} 19 | config.AllowHeaders = []string{"Origin", "Authorization", "Content-Type"} 20 | 21 | router.Use(cors.New(config)) 22 | 23 | public := router.Group("/api") 24 | public.POST("/login", controllers.Login) 25 | public.GET("/applications", controllers.GetApplications) // 公开访问 26 | public.GET("/menus", controllers.GetMenus) // 公开访问 27 | public.GET("/settings", controllers.GetSiteSettings) 28 | 29 | // 注册每日一言的路由 30 | public.GET("/hitokoto", controllers.HitokotoHandler) 31 | // 注册热榜路由 32 | public.GET("/hotrank", controllers.HotRankHandler) 33 | 34 | // 采集网站 35 | public.GET("/scrape-website", controllers.ScrapeWebsiteHandler) 36 | // 同步图标到图床 37 | public.POST("/sync-icon", controllers.SyncIconHandler) 38 | // 保存图床 Token 39 | public.POST("/save-token", controllers.SaveImageHostTokenHandler) 40 | 41 | private := router.Group("/api") 42 | private.Use(middlewares.AuthMiddleware()) 43 | 44 | // 应用管理 45 | private.POST("/applications", controllers.CreateApplication) 46 | private.PUT("/applications/:id", controllers.UpdateApplication) 47 | private.DELETE("/applications/:id", controllers.DeleteApplication) 48 | 49 | // 菜单管理 50 | private.POST("/menus", controllers.CreateMenu) 51 | private.PUT("/menus/:id", controllers.UpdateMenu) 52 | private.DELETE("/menus/:id", controllers.DeleteMenu) 53 | 54 | // 批量删除 55 | private.POST("/menus/batch_delete", controllers.BatchDeleteMenus) 56 | private.POST("/applications/batch_delete", controllers.BatchDeleteApplications) 57 | 58 | // 退出登录 59 | private.POST("/logout", controllers.Logout) 60 | 61 | // 站点设置 62 | private.PUT("/settings", controllers.UpdateSiteSettings) 63 | 64 | // 获取数据统计 65 | private.GET("/applications/total", controllers.GetTotalApplications) // 获取总应用数量 66 | private.GET("/applications/recent", controllers.GetRecentApplications) // 获取最近新增应用 67 | private.GET("/menus/total", controllers.GetTotalMenus) // 获取总菜单数量 68 | private.GET("/users/total", controllers.GetTotalUsers) // 获取总用户数量 69 | 70 | // 用户管理 71 | private.GET("/users", controllers.GetUsers) // 获取用户列表 72 | private.GET("/user", controllers.GetCurrentUser) // 获取当前登录用户信息 73 | private.POST("/users", controllers.CreateUser) // 创建用户 74 | private.PUT("/users/password", controllers.UpdateUserPassword) // 更新用户密码 75 | private.PUT("/users/status", controllers.UpdateUserStatus) // 更新用户状态 76 | private.PUT("/users/admin", controllers.UpdateUserAdminStatus) // 更新用户管理员权限 77 | private.PUT("/users", controllers.UpdateUser) 78 | private.DELETE("/users/:id", controllers.DeleteUser) // 删除用户 79 | 80 | return router 81 | } 82 | -------------------------------------------------------------------------------- /backend/services/Hot_Rank.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io/ioutil" 8 | "log" 9 | "net/http" 10 | "sort" 11 | "time" 12 | ) 13 | 14 | // 定义结构体来解析热榜 API 响应 15 | type HotItem struct { 16 | Title string `json:"title"` 17 | Hot float64 `json:"hot"` // 修改为 float64 类型来处理数字 18 | URL string `json:"url"` 19 | } 20 | 21 | type HotRankResponse struct { 22 | Code int `json:"code"` 23 | Message string `json:"message"` 24 | Title string `json:"title"` 25 | Subtitle string `json:"subtitle"` 26 | From string `json:"from"` 27 | Total int `json:"total"` 28 | UpdateTime string `json:"updateTime"` 29 | Data []HotItem `json:"data"` 30 | } 31 | 32 | // GetHotRank 获取指定热榜的内容 33 | func GetHotRank(category string) ([]HotItem, error) { 34 | // 构建请求 URL 35 | apiURL := fmt.Sprintf("https://api.pearktrue.cn/api/dailyhot/?title=%s", category) 36 | 37 | // 创建 HTTP 客户端并设置超时时间 38 | client := &http.Client{ 39 | Timeout: 60 * time.Second, 40 | } 41 | 42 | // 发起 GET 请求 43 | resp, err := client.Get(apiURL) 44 | if err != nil { 45 | return nil, fmt.Errorf("failed to fetch hot rank: %w", err) 46 | } 47 | defer resp.Body.Close() 48 | 49 | // 检查 HTTP 状态码是否为 200 OK 50 | if resp.StatusCode != http.StatusOK { 51 | return nil, errors.New("received non-200 response code") 52 | } 53 | 54 | // 读取响应数据 55 | body, err := ioutil.ReadAll(resp.Body) 56 | if err != nil { 57 | log.Printf("Failed to read response body: %v", err) 58 | return nil, fmt.Errorf("failed to read response body: %w", err) 59 | } 60 | 61 | // 解析 JSON 数据 62 | var response HotRankResponse 63 | if err := json.Unmarshal(body, &response); err != nil { 64 | log.Printf("Failed to parse JSON: %v", err) 65 | return nil, fmt.Errorf("failed to parse JSON: %w", err) 66 | } 67 | 68 | // 排序并格式化数据 69 | hotItems := response.Data 70 | sort.Slice(hotItems, func(i, j int) bool { 71 | return hotItems[i].Hot > hotItems[j].Hot 72 | }) 73 | 74 | // 只保留前 50 条数据 75 | if len(hotItems) > 50 { 76 | hotItems = hotItems[:50] 77 | } 78 | 79 | return hotItems, nil 80 | } 81 | -------------------------------------------------------------------------------- /backend/services/application_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | // 这里可以实现应用服务的业务逻辑 4 | -------------------------------------------------------------------------------- /backend/services/user_service.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | // 这里可以实现用户服务逻辑,比如用户注册、登录等 4 | -------------------------------------------------------------------------------- /backend/services/web_scraper.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | "time" 10 | 11 | "github.com/PuerkitoBio/goquery" 12 | ) 13 | 14 | type ScrapedData struct { 15 | Title string `json:"title"` 16 | LogoLink string `json:"logo_link"` 17 | Description string `json:"description"` 18 | } 19 | 20 | func ScrapeWebsite(websiteURL string) (*ScrapedData, error) { 21 | // 创建带有超时设置的 HTTP 客户端 22 | client := &http.Client{ 23 | Timeout: 10 * time.Second, // 设置超时时间为 5 秒 24 | } 25 | 26 | // 发起 HTTP 请求 27 | req, err := http.NewRequest("GET", websiteURL, nil) 28 | if err != nil { 29 | return nil, fmt.Errorf("创建请求失败: %w", err) 30 | } 31 | req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Scraper/1.0)") 32 | 33 | res, err := client.Do(req) 34 | if err != nil { 35 | return nil, fmt.Errorf("请求失败: %w", err) 36 | } 37 | defer res.Body.Close() 38 | 39 | if res.StatusCode != http.StatusOK { 40 | return nil, fmt.Errorf("请求失败: %d %s", res.StatusCode, res.Status) 41 | } 42 | 43 | // 解析 HTML 44 | doc, err := goquery.NewDocumentFromReader(res.Body) 45 | if err != nil { 46 | return nil, fmt.Errorf("解析 HTML 失败: %w", err) 47 | } 48 | 49 | // 获取标题 50 | title := doc.Find("title").Text() 51 | 52 | // 获取描述 53 | description, _ := doc.Find("meta[name='description']").Attr("content") 54 | 55 | // 获取 Logo 链接 56 | var logolink string 57 | 58 | // 定义可能的选择器和对应的属性 59 | type selectorAttr struct { 60 | selector string 61 | attr string 62 | } 63 | 64 | logoSelectors := []selectorAttr{ 65 | {"link[rel~='icon']", "href"}, 66 | {"link[rel~='shortcut icon']", "href"}, 67 | {"link[rel~='apple-touch-icon']", "href"}, 68 | {"meta[property='og:image']", "content"}, 69 | } 70 | 71 | for _, sa := range logoSelectors { 72 | selection := doc.Find(sa.selector) 73 | if selection.Length() > 0 { 74 | link, exists := selection.First().Attr(sa.attr) 75 | if exists && link != "" { 76 | logolink = link 77 | break 78 | } 79 | } 80 | } 81 | 82 | // 如果未找到图标链接,尝试使用默认的 /favicon.ico 83 | if logolink == "" { 84 | log.Println("Logo 链接未在 HTML 中找到,尝试获取默认的 /favicon.ico") 85 | logolink = resolveURL(websiteURL, "/favicon.ico") 86 | } else { 87 | // 处理相对路径和协议相对路径的情况 88 | logolink = resolveURL(websiteURL, logolink) 89 | } 90 | 91 | // 检查 favicon 是否存在 92 | if !urlExists(logolink, client) { 93 | log.Printf("无法访问图标链接: %s", logolink) 94 | logolink = "" 95 | } 96 | 97 | data := &ScrapedData{ 98 | Title: strings.TrimSpace(title), 99 | LogoLink: logolink, 100 | Description: strings.TrimSpace(description), 101 | } 102 | 103 | return data, nil 104 | } 105 | 106 | // 解析相对 URL 107 | func resolveURL(baseURL, relativeURL string) string { 108 | base, err := url.Parse(baseURL) 109 | if err != nil { 110 | return relativeURL 111 | } 112 | ref, err := url.Parse(relativeURL) 113 | if err != nil { 114 | return relativeURL 115 | } 116 | resolvedURL := base.ResolveReference(ref) 117 | return resolvedURL.String() 118 | } 119 | 120 | // 检查 URL 是否存在,使用带超时的客户端 121 | func urlExists(u string, client *http.Client) bool { 122 | req, err := http.NewRequest("HEAD", u, nil) 123 | if err != nil { 124 | return false 125 | } 126 | req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; Scraper/1.0)") 127 | 128 | resp, err := client.Do(req) 129 | if err != nil { 130 | return false 131 | } 132 | defer resp.Body.Close() 133 | return resp.StatusCode == http.StatusOK 134 | } 135 | -------------------------------------------------------------------------------- /backend/services/wotd_hitokoto.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "net/http" 7 | "time" 8 | ) 9 | 10 | // GetHitokoto 发送 HTTP 请求获取每日一言并返回纯文本内容 11 | func GetHitokoto() (string, error) { 12 | // 创建 HTTP 客户端并设置超时时间 13 | client := &http.Client{ 14 | Timeout: 10 * time.Second, // 设置超时时间为 10 秒 15 | } 16 | 17 | // 发起 GET 请求 18 | resp, err := client.Get("https://api.pearktrue.cn/api/hitokoto/") 19 | if err != nil { 20 | // 打印并返回错误信息 21 | fmt.Printf("Error fetching hitokoto: %v\n", err) 22 | return "", err 23 | } 24 | defer resp.Body.Close() 25 | 26 | // 检查 HTTP 状态码是否为 200 OK 27 | if resp.StatusCode != http.StatusOK { 28 | fmt.Printf("Error: received non-200 status code %d\n", resp.StatusCode) 29 | return "", fmt.Errorf("non-200 response code: %d", resp.StatusCode) 30 | } 31 | 32 | // 读取响应数据(假设是纯文本) 33 | body, err := ioutil.ReadAll(resp.Body) 34 | if err != nil { 35 | fmt.Printf("Error reading response body: %v\n", err) 36 | return "", err 37 | } 38 | 39 | // 返回每日一言的文本内容 40 | return string(body), nil 41 | } 42 | -------------------------------------------------------------------------------- /backend/utils/jwt.go: -------------------------------------------------------------------------------- 1 | // utils/jwt.go 2 | 3 | package utils 4 | 5 | import ( 6 | "github.com/golang-jwt/jwt/v4" 7 | "golang.org/x/crypto/bcrypt" 8 | "time" 9 | ) 10 | 11 | var jwtKey = []byte("gdwerxzsderg") 12 | 13 | type Claims struct { 14 | Username string `json:"username"` 15 | jwt.StandardClaims 16 | } 17 | 18 | // 生成 JWT,并设置 24 小时过期 19 | func GenerateJWT(username string) (string, error) { 20 | expirationTime := time.Now().Add(24 * time.Hour) // 设置为24小时过期 21 | claims := &Claims{ 22 | Username: username, 23 | StandardClaims: jwt.StandardClaims{ 24 | ExpiresAt: expirationTime.Unix(), 25 | }, 26 | } 27 | 28 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 29 | return token.SignedString(jwtKey) 30 | } 31 | 32 | // 验证 JWT 是否有效 33 | func ValidateJWT(tokenString string) bool { 34 | claims := &Claims{} 35 | 36 | token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 37 | return jwtKey, nil 38 | }) 39 | 40 | return err == nil && token.Valid 41 | } 42 | 43 | func GetUsernameFromJWT(tokenString string) (string, error) { 44 | claims := &Claims{} 45 | 46 | token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 47 | return jwtKey, nil 48 | }) 49 | 50 | if err != nil || !token.Valid { 51 | return "", err 52 | } 53 | 54 | return claims.Username, nil 55 | } 56 | 57 | func HashPassword(password string) (string, error) { 58 | bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) // 密码哈希,cost值为14 59 | return string(bytes), err 60 | } 61 | 62 | func CheckPasswordHash(password, hash string) bool { 63 | err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) 64 | return err == nil 65 | } 66 | 67 | // CheckPassword 验证密码是否正确 68 | func CheckPassword(password, hashedPassword string) error { 69 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 70 | } 71 | -------------------------------------------------------------------------------- /background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhanghao123321/nav/6a5e55d2adb09bf8152ff5d666788069ea942d71/background.png -------------------------------------------------------------------------------- /docker-compose.build.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | backend: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile # 指向后端的 Dockerfile 7 | container_name: znav-backend 8 | environment: 9 | - DB_USER=root # 数据库用户 10 | - DB_PASSWORD=123456 # 数据库密码 11 | - DB_HOST=mysql # 数据库容器 12 | - DB_PORT=3306 # 数据库端口 13 | - DB_NAME=znav # zanv数据库 14 | - JWT_SECRET=supersecret 15 | ports: 16 | - "8080:8080" # 暴露后端的 8080 端口 17 | networks: 18 | - znav_network 19 | depends_on: 20 | - mysql # 等待 MySQL 启动 21 | restart: always 22 | 23 | frontend: 24 | build: 25 | context: ./frontend 26 | dockerfile: Dockerfile # 指向前端的 Dockerfile 27 | container_name: znav-frontend 28 | ports: 29 | - "80:80" # 暴露前端的 80 端口 30 | depends_on: 31 | - backend # 等待 backend 服务启动 32 | networks: 33 | - znav_network 34 | restart: always 35 | 36 | mysql: 37 | image: mysql:8.4.0-oraclelinux8 38 | container_name: znav-mysql 39 | environment: 40 | - MYSQL_ROOT_PASSWORD=123456 41 | - MYSQL_DATABASE=znav 42 | - TZ=Asia/Shanghai 43 | ports: 44 | - "3306:3306" 45 | volumes: 46 | - ./initdb:/docker-entrypoint-initdb.d 47 | networks: 48 | - znav_network 49 | healthcheck: 50 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 51 | interval: 10s 52 | retries: 3 53 | restart: always 54 | 55 | networks: 56 | znav_network: 57 | driver: bridge 58 | -------------------------------------------------------------------------------- /docker-compose.image.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | backend: 4 | image: registry.cn-shanghai.aliyuncs.com/hooz/nav:backend 5 | container_name: znav-backend 6 | environment: 7 | - DB_USER=root # 数据库用户 8 | - DB_PASSWORD=123456 # 数据库密码 9 | - DB_HOST=mysql # 数据库容器 10 | - DB_PORT=3306 # 数据库端口 11 | - DB_NAME=znav # zanv数据库 12 | - JWT_SECRET=supersecret 13 | ports: 14 | - "8080:8080" # 暴露后端的 8080 端口 15 | networks: 16 | - znav_network 17 | depends_on: 18 | - mysql # 等待 MySQL 启动 19 | restart: always 20 | 21 | frontend: 22 | image: registry.cn-shanghai.aliyuncs.com/hooz/nav:frontend 23 | container_name: znav-frontend 24 | ports: 25 | - "80:80" # 暴露前端的 80 端口 26 | depends_on: 27 | - backend # 等待 backend 服务启动 28 | networks: 29 | - znav_network 30 | restart: always 31 | 32 | mysql: 33 | image: mysql:8.4.0-oraclelinux8 34 | container_name: znav-mysql 35 | environment: 36 | - MYSQL_ROOT_PASSWORD=123456 37 | - MYSQL_DATABASE=znav 38 | - TZ=Asia/Shanghai 39 | ports: 40 | - "3306:3306" 41 | volumes: 42 | - ./initdb:/docker-entrypoint-initdb.d 43 | networks: 44 | - znav_network 45 | healthcheck: 46 | test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] 47 | interval: 10s 48 | retries: 3 49 | restart: always 50 | 51 | networks: 52 | znav_network: 53 | driver: bridge 54 | -------------------------------------------------------------------------------- /foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhanghao123321/nav/6a5e55d2adb09bf8152ff5d666788069ea942d71/foreground.png -------------------------------------------------------------------------------- /frontend-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: znav-frontend 5 | namespace: production 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: znav-frontend 11 | template: 12 | metadata: 13 | labels: 14 | app: znav-frontend 15 | spec: 16 | containers: 17 | - name: znav-frontend 18 | image: registry.cn-shanghai.aliyuncs.com/hooz/nav:frontend 19 | ports: 20 | - containerPort: 80 21 | --- 22 | apiVersion: v1 23 | kind: Service 24 | metadata: 25 | name: znav-frontend 26 | namespace: production 27 | spec: 28 | ports: 29 | - port: 80 30 | targetPort: 80 31 | selector: 32 | app: znav-frontend 33 | -------------------------------------------------------------------------------- /frontend/.env: -------------------------------------------------------------------------------- 1 | VUE_APP_API_URL=/api -------------------------------------------------------------------------------- /frontend/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended' 9 | ], 10 | parserOptions: { 11 | ecmaVersion: 2020 12 | }, 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 16 | 'vue/multi-word-component-names': 'off', 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /dist/ 3 | -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用 Node.js 官方镜像构建前端应用 2 | FROM node:18 as build 3 | 4 | # 设置工作目录 5 | WORKDIR /app 6 | 7 | # 复制 frontend 文件夹的内容到容器中 8 | COPY . /app 9 | 10 | # 安装依赖 11 | RUN npm --registry https://registry.npmmirror.com/ install 12 | 13 | # 构建前端应用 14 | RUN npm --registry https://registry.npmmirror.com/ run build 15 | 16 | # 使用 Nginx 来部署前端应用 17 | FROM nginx:alpine 18 | 19 | # 拷贝 Nginx 配置文件 20 | COPY nginx.conf /etc/nginx/nginx.conf 21 | 22 | # 复制前端构建产物到 Nginx 的默认目录 23 | COPY --from=build /app/dist /usr/share/nginx/html 24 | 25 | # 暴露 Nginx 默认的 80 端口 26 | EXPOSE 80 27 | 28 | # 启动 Nginx 29 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { 4 | worker_connections 1024; 5 | } 6 | 7 | http { 8 | include /etc/nginx/mime.types; 9 | default_type application/octet-stream; 10 | server_tokens off; 11 | client_max_body_size 20m; 12 | client_body_buffer_size 20m; 13 | keepalive_timeout 65; 14 | sendfile on; 15 | tcp_nodelay on; 16 | ssl_prefer_server_ciphers on; 17 | ssl_session_cache shared:SSL:2m; 18 | gzip on; 19 | gzip_static on; 20 | gzip_types text/plain application/json application/javascript application/x-javascript text/css application/xml text/javascript; 21 | gzip_proxied any; 22 | gzip_vary on; 23 | gzip_comp_level 6; 24 | gzip_buffers 16 8k; 25 | gzip_http_version 1.0; 26 | server { 27 | listen 80; 28 | 29 | server_name localhost; 30 | 31 | # Serve frontend files 32 | location / { 33 | root /usr/share/nginx/html; 34 | try_files $uri /index.html; 35 | } 36 | 37 | # Proxy API requests to the Go backend 38 | location /api/ { 39 | proxy_pass http://znav-backend:8080; 40 | proxy_set_header Host $host; 41 | proxy_set_header X-Real-IP $remote_addr; 42 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 43 | proxy_set_header X-Forwarded-Proto $scheme; 44 | } 45 | 46 | # 定义 404 页面 47 | error_page 404 /404.html; 48 | location = /404.html { 49 | root /usr/share/nginx/html; 50 | } 51 | 52 | # 定义 50x 页面 53 | error_page 500 502 503 504 /50x.html; 54 | location = /50x.html { 55 | root /usr/share/nginx/html; 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "1.0.0", 4 | "description": "Vue.js frontend for the backend management system", 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build", 8 | "lint": "vue-cli-service lint" 9 | }, 10 | "dependencies": { 11 | "@iconify/vue": "^4.1.2", 12 | "axios": "^0.21.1", 13 | "chart.js": "^4.4.4", 14 | "core-js": "^3.6.5", 15 | "element-plus": "^2.8.0", 16 | "portal-vue": "^3.0.0", 17 | "vue": "^3.0.0", 18 | "vue-chartjs": "^5.3.1", 19 | "vue-router": "^4.0.0", 20 | "vuex": "^4.0.0" 21 | }, 22 | "devDependencies": { 23 | "@babel/plugin-proposal-optional-chaining": "^7.12.0", 24 | "@babel/plugin-transform-optional-chaining": "^7.24.8", 25 | "@vue/cli-plugin-babel": "~4.5.0", 26 | "@vue/cli-plugin-eslint": "~4.5.0", 27 | "@vue/cli-service": "~4.5.0", 28 | "@vue/compiler-sfc": "^3.0.0", 29 | "babel-eslint": "^10.1.0", 30 | "echarts": "^5.5.1", 31 | "eslint-plugin-vue": "^9.28.0", 32 | "vue-echarts": "^7.0.3" 33 | }, 34 | "browserslist": [ 35 | "> 1%", 36 | "last 2 versions", 37 | "not dead", 38 | "not ie <= 11" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | // 如使用了其他插件,请在此添加 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | zh导航 7 | 8 | 9 | 12 |
13 | 14 |
15 | 16 | 29 | 30 | 65 | -------------------------------------------------------------------------------- /frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 209 | 210 | 369 | 370 | 607 | -------------------------------------------------------------------------------- /frontend/src/api/axios.js: -------------------------------------------------------------------------------- 1 | // src/api/axios.js 2 | 3 | import axios from 'axios'; 4 | import store from '../store'; // 引入Vuex Store 5 | 6 | // 创建一个Axios实例 7 | const apiClient = axios.create({ 8 | baseURL: process.env.VUE_APP_BASE_API, // 你可以在.env文件中设置API的基础URL 9 | timeout: 10000, // 请求超时时间 10 | }); 11 | 12 | // 请求拦截器 13 | apiClient.interceptors.request.use(config => { 14 | const token = store.state.token; 15 | if (token) { 16 | config.headers.Authorization = `Bearer ${token}`; 17 | } 18 | return config; 19 | }, error => { 20 | return Promise.reject(error); 21 | }); 22 | 23 | // 响应拦截器 24 | apiClient.interceptors.response.use(response => { 25 | return response; 26 | }, error => { 27 | if (error.response && error.response.status === 401) { 28 | store.commit('clearToken'); // 清除无效的Token 29 | alert('您的登录已过期,请重新登录。'); 30 | // 可以在此处重定向到登录页面,或者触发重新登录的流程 31 | } 32 | return Promise.reject(error); 33 | }); 34 | 35 | export default apiClient; 36 | -------------------------------------------------------------------------------- /frontend/src/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": ["@babel/preset-env"], 3 | "plugins": ["@babel/plugin-proposal-optional-chaining"] 4 | }; -------------------------------------------------------------------------------- /frontend/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | import ElementPlus from 'element-plus'; 6 | import 'element-plus/dist/index.css'; 7 | import { getIcon } from '@iconify/vue'; 8 | import 'element-plus/theme-chalk/index.css'; 9 | 10 | const app = createApp(App); 11 | 12 | app.use(store); 13 | app.use(router); 14 | app.use(ElementPlus); 15 | 16 | app.mount('#app'); 17 | 18 | 19 | 20 | 21 | 22 | // 定义 isUrl 方法 23 | function isUrl(string) { 24 | return string.startsWith('http://') || string.startsWith('https://'); 25 | } 26 | 27 | // 定义 isIconifyIcon 方法,判断是否为 :icon 格式的 Iconify 图标 28 | function isIconifyIcon(string) { 29 | return string && string.includes(':'); 30 | } 31 | 32 | // 动态更新页面标题和图标 33 | store.dispatch('fetchSiteSettings').then(() => { 34 | const siteSettings = store.getters.siteSettings; 35 | updateIconAndTitle(siteSettings); 36 | 37 | // 监听 store 里的设置变化并动态更新图标 38 | store.watch( 39 | (state) => state.siteSettings, 40 | (newSettings) => { 41 | updateIconAndTitle(newSettings); 42 | } 43 | ); 44 | }).catch(() => { 45 | // 获取失败时,使用默认标题和图标 46 | updateIconAndTitle({ 47 | title: window.location.pathname === '/' ? 'zh导航' : '管理系统', 48 | icon: window.location.pathname === '/' 49 | ? 'https://img1.baidu.com/it/u=1217061905,2277984247&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500' 50 | : '' 51 | }); 52 | }); 53 | 54 | // 更新图标和标题 55 | function updateIconAndTitle(settings) { 56 | let siteIcon = settings.icon; 57 | 58 | if (isUrl(siteIcon)) { 59 | // 如果是 URL 图标,直接使用 URL 更新 favicon 60 | setFavicon(siteIcon); 61 | } else if (isIconifyIcon(siteIcon)) { 62 | // 如果是 Iconify 图标,通过 Iconify 处理 63 | setIconifyIcon(siteIcon); 64 | } else { 65 | // 默认使用一个图标 66 | setFavicon('https://img1.baidu.com/it/u=1217061905,2277984247&fm=253&fmt=auto&app=138&f=JPEG?w=500&h=500'); 67 | } 68 | 69 | // 更新标题 70 | if (window.location.pathname === '/') { 71 | document.title = settings.title || 'zh导航'; 72 | } else { 73 | document.title = '管理系统'; 74 | } 75 | } 76 | 77 | // 设置 URL 图标为 favicon 78 | function setFavicon(iconUrl) { 79 | const link = document.querySelector("link[rel~='icon']"); 80 | if (link) { 81 | link.href = iconUrl; 82 | } else { 83 | const newLink = document.createElement('link'); 84 | newLink.rel = 'icon'; 85 | newLink.href = iconUrl; 86 | document.head.appendChild(newLink); 87 | } 88 | } 89 | 90 | // 设置 Iconify 图标为 favicon 91 | function setIconifyIcon(iconName) { 92 | try { 93 | // 获取 Iconify 图标的 SVG 数据 94 | const iconData = getIcon(iconName); 95 | 96 | if (!iconData) { 97 | console.error('Icon not found'); 98 | return; 99 | } 100 | 101 | // 构造 SVG 图标,并确保图标居中 102 | const svg = ` 103 | 107 | 108 | ${iconData.body} 109 | 110 | 111 | `; 112 | 113 | const svgUrl = `data:image/svg+xml,${encodeURIComponent(svg)}`; 114 | 115 | // 设置生成的 SVG 图标为 favicon 116 | setFavicon(svgUrl); 117 | } catch (error) { 118 | console.error('Error loading icon:', error); 119 | } 120 | } -------------------------------------------------------------------------------- /frontend/src/router/index.js: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router' 2 | import Login from '../views/Login.vue' 3 | import Dashboard from '../views/Dashboard.vue' 4 | import AppManagement from '../views/AppManagement.vue' 5 | import AppNavigation from '../views/AppNavigation.vue' 6 | import MenuManagement from '../views/MenuManagement.vue' 7 | import SiteSettings from '../views/SiteSettings.vue' 8 | import Admin from '../views/Admin.vue'; 9 | 10 | const routes = [ 11 | { path: '/login', component: Login }, 12 | { path: '/dashboard', component: Dashboard, meta: { requiresAuth: true } }, 13 | { path: '/apps', component: AppManagement, meta: { requiresAuth: true } }, 14 | { path: '/menus', component: MenuManagement, meta: { requiresAuth: true } }, 15 | { path: '/admin', component: Admin, meta: { requiresAuth: true } }, 16 | { path: '/settings', component: SiteSettings, meta: { requiresAuth: true } }, 17 | { path: '/', component: AppNavigation }, 18 | ] 19 | 20 | const router = createRouter({ 21 | history: createWebHistory(process.env.BASE_URL), 22 | routes, 23 | }) 24 | 25 | router.beforeEach((to, from, next) => { 26 | const loggedIn = !!localStorage.getItem('token'); 27 | if (to.matched.some(record => record.meta.requiresAuth) && !loggedIn) { 28 | next('/login'); 29 | } else { 30 | // 延迟导航,确保 Vuex 状态同步后再跳转 31 | setTimeout(() => { 32 | next(); 33 | }, 200); // 延迟200ms,确保渲染完成 34 | } 35 | }); 36 | 37 | 38 | export default router 39 | -------------------------------------------------------------------------------- /frontend/src/store/index.js: -------------------------------------------------------------------------------- 1 | import { createStore } from 'vuex'; 2 | import axios from 'axios'; 3 | 4 | export default createStore({ 5 | state: { 6 | token: localStorage.getItem('token') || null, 7 | user: { 8 | username: '', 9 | }, 10 | siteSettings: { 11 | id: null, 12 | title: '', 13 | icon: '', 14 | icon_color: '', 15 | footerInfo: '', 16 | recordNumber: '', 17 | image_host_token: '' 18 | }, 19 | }, 20 | mutations: { 21 | setToken(state, token) { 22 | state.token = token; 23 | localStorage.setItem('token', token); 24 | }, 25 | setUser(state, user) { 26 | state.user = user; // 更新 Vuex 中的用户数据 27 | }, 28 | clearToken(state) { 29 | state.token = null; 30 | state.user = {}; // 清空用户数据 31 | localStorage.removeItem('token'); 32 | }, 33 | setSiteSettings(state, settings) { 34 | state.siteSettings = settings; 35 | }, 36 | updateSiteSetting(state, { key, value }) { 37 | state.siteSettings[key] = value; 38 | } 39 | }, 40 | actions: { 41 | fetchUserData({ commit, state, dispatch }) { 42 | return new Promise((resolve, reject) => { 43 | if (!state.token) { 44 | reject(new Error('No token available')); 45 | return; 46 | } 47 | 48 | axios.get(`${process.env.VUE_APP_API_URL}/user`, { 49 | headers: { 50 | Authorization: `Bearer ${state.token}` 51 | } 52 | }).then(response => { 53 | commit('setUser', response.data.user); // 提交提取后的 user 对象 54 | resolve(response.data.user); // 确保返回的是用户数据 55 | }).catch(error => { 56 | if (error.response && error.response.status === 404) { 57 | console.error('User data not found.'); 58 | } 59 | if (error.response && error.response.status === 401) { 60 | dispatch('logout'); // token 过期,执行登出操作 61 | } 62 | reject(error); 63 | }); 64 | }); 65 | }, 66 | fetchSiteSettings({ commit, state }) { 67 | return axios.get(`${process.env.VUE_APP_API_URL}/settings`, { 68 | headers: { Authorization: `Bearer ${state.token}` } 69 | }) 70 | .then(response => { 71 | commit('setSiteSettings', response.data); 72 | }) 73 | .catch(error => { 74 | console.error('Failed to fetch site settings:', error); 75 | }); 76 | }, 77 | updateSiteSettings({ commit, state }, settings) { 78 | return axios.put(`${process.env.VUE_APP_API_URL}/settings`, settings, { 79 | headers: { Authorization: `Bearer ${state.token}` } 80 | }) 81 | .then(response => { 82 | commit('setSiteSettings', response.data); 83 | }) 84 | .catch(error => { 85 | console.error('Failed to update site settings:', error); 86 | throw error; 87 | }); 88 | }, 89 | logout({ commit }) { 90 | commit('clearToken'); 91 | this.$router.push('/login'); // 跳转到登录页面 92 | } 93 | }, 94 | getters: { 95 | isAuthenticated: state => !!state.token, 96 | userData: state => state.user, // 返回用户的所有信息 97 | siteSettings: state => state.siteSettings, 98 | username: state => state.user.username || '管理员', // 从 state.user 中返回用户名 99 | }, 100 | }); 101 | -------------------------------------------------------------------------------- /frontend/src/views/Admin.vue: -------------------------------------------------------------------------------- 1 | 210 | 211 | 212 | 549 | 550 | 551 | -------------------------------------------------------------------------------- /frontend/src/views/AppManagement.vue: -------------------------------------------------------------------------------- 1 | 483 | 484 | 898 | 899 | 1140 | -------------------------------------------------------------------------------- /frontend/src/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 85 | 86 | 243 | 244 | 245 | 257 | -------------------------------------------------------------------------------- /frontend/src/views/Login.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 124 | 125 | 154 | -------------------------------------------------------------------------------- /frontend/src/views/MenuManagement.vue: -------------------------------------------------------------------------------- 1 | 412 | 413 | 665 | 666 | 667 | 924 | -------------------------------------------------------------------------------- /frontend/src/views/SiteSettings.vue: -------------------------------------------------------------------------------- 1 | 84 | 85 | 146 | 147 | 236 | -------------------------------------------------------------------------------- /frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | 3 | 4 | module.exports = { 5 | lintOnSave: false, 6 | devServer: { 7 | proxy: { 8 | '/api': { 9 | target: 'http://localhost:8080', 10 | changeOrigin: true, 11 | }, 12 | }, 13 | }, 14 | css: { 15 | extract: true, // 确保 CSS 被正确提取到单独的文件 16 | sourceMap: false, // 在生产环境中关闭 CSS source map 17 | }, 18 | chainWebpack: config => { 19 | config.module 20 | .rule('js') 21 | .include 22 | .add(/node_modules\/vue-echarts/) 23 | .add(/node_modules\/echarts/) 24 | .end() 25 | .use('babel-loader') 26 | .loader('babel-loader') 27 | .tap(options => { 28 | options = options || {}; 29 | options.plugins = (options.plugins || []).concat(['@babel/plugin-transform-optional-chaining']); 30 | return options; 31 | }); 32 | }, 33 | configureWebpack: { 34 | plugins: [ 35 | new webpack.DefinePlugin({ 36 | __VUE_OPTIONS_API__: true, // 如果你使用了 options API,请设置为 true 37 | __VUE_PROD_DEVTOOLS__: false, // 生产环境中不需要 devtools 38 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false // 生产环境中不需要 hydration mismatch 细节 39 | }) 40 | ] 41 | } 42 | }; 43 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module znav 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.9.2 7 | github.com/gin-contrib/cors v1.7.2 8 | github.com/gin-gonic/gin v1.10.0 9 | github.com/golang-jwt/jwt/v4 v4.5.0 10 | github.com/joho/godotenv v1.5.1 11 | golang.org/x/crypto v0.23.0 12 | golang.org/x/image v0.20.0 13 | gorm.io/driver/mysql v1.5.7 14 | gorm.io/gorm v1.25.11 15 | ) 16 | 17 | require ( 18 | github.com/andybalholm/cascadia v1.3.2 // indirect 19 | github.com/bytedance/sonic v1.11.6 // indirect 20 | github.com/bytedance/sonic/loader v0.1.1 // indirect 21 | github.com/cloudwego/base64x v0.1.4 // indirect 22 | github.com/cloudwego/iasm v0.2.0 // indirect 23 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 24 | github.com/gin-contrib/sse v0.1.0 // indirect 25 | github.com/go-playground/locales v0.14.1 // indirect 26 | github.com/go-playground/universal-translator v0.18.1 // indirect 27 | github.com/go-playground/validator/v10 v10.20.0 // indirect 28 | github.com/go-sql-driver/mysql v1.7.0 // indirect 29 | github.com/goccy/go-json v0.10.2 // indirect 30 | github.com/jinzhu/inflection v1.0.0 // indirect 31 | github.com/jinzhu/now v1.1.5 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 34 | github.com/kr/text v0.2.0 // indirect 35 | github.com/leodido/go-urn v1.4.0 // indirect 36 | github.com/mattn/go-isatty v0.0.20 // indirect 37 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 38 | github.com/modern-go/reflect2 v1.0.2 // indirect 39 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 40 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 41 | github.com/ugorji/go/codec v1.2.12 // indirect 42 | golang.org/x/arch v0.8.0 // indirect 43 | golang.org/x/net v0.25.0 // indirect 44 | golang.org/x/sys v0.20.0 // indirect 45 | golang.org/x/text v0.18.0 // indirect 46 | google.golang.org/protobuf v1.34.1 // indirect 47 | gopkg.in/yaml.v3 v3.0.1 // indirect 48 | ) 49 | -------------------------------------------------------------------------------- /initdb/01-init.sql: -------------------------------------------------------------------------------- 1 | CREATE DATABASE IF NOT EXISTS znav; 2 | CREATE DATABASE IF NOT EXISTS lsky; 3 | -------------------------------------------------------------------------------- /mysql-deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: znav-mysql 5 | namespace: production 6 | spec: 7 | replicas: 1 8 | selector: 9 | matchLabels: 10 | app: znav-mysql 11 | template: 12 | metadata: 13 | labels: 14 | app: znav-mysql 15 | spec: 16 | containers: 17 | - name: znav-mysql 18 | image: dockerpull.com/mysql:8.4.0-oraclelinux8 19 | env: 20 | - name: MYSQL_ROOT_PASSWORD 21 | value: "123456" 22 | - name: MYSQL_DATABASE 23 | value: "znav" 24 | - name: TZ 25 | value: "Asia/Shanghai" 26 | ports: 27 | - containerPort: 3306 28 | volumeMounts: 29 | - mountPath: /docker-entrypoint-initdb.d 30 | name: mysql-initdb 31 | volumes: 32 | - name: mysql-initdb 33 | persistentVolumeClaim: 34 | claimName: mysql-pvc 35 | --- 36 | apiVersion: v1 37 | kind: Service 38 | metadata: 39 | name: znav-mysql 40 | namespace: production 41 | spec: 42 | ports: 43 | - port: 3306 44 | targetPort: 3306 45 | selector: 46 | app: znav-mysql 47 | -------------------------------------------------------------------------------- /mysql-pv.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolume 3 | metadata: 4 | name: mysql-pv 5 | spec: 6 | capacity: 7 | storage: 1Gi 8 | accessModes: 9 | - ReadWriteOnce 10 | hostPath: 11 | path: /data/znav/initdb 12 | --- 13 | apiVersion: v1 14 | kind: PersistentVolumeClaim 15 | metadata: 16 | name: mysql-pvc 17 | namespace: production 18 | spec: 19 | accessModes: 20 | - ReadWriteOnce 21 | resources: 22 | requests: 23 | storage: 1Gi 24 | -------------------------------------------------------------------------------- /znav-ingress.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: networking.k8s.io/v1 2 | kind: Ingress 3 | metadata: 4 | name: znav-ingress 5 | namespace: production 6 | annotations: 7 | kubernetes.io/ingress.class: nginx 8 | spec: 9 | ingressClassName: nginx 10 | rules: 11 | - host: znav.bar.com 12 | http: 13 | paths: 14 | - path: / 15 | pathType: Prefix 16 | backend: 17 | service: 18 | name: znav-frontend 19 | port: 20 | number: 80 21 | --------------------------------------------------------------------------------