diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index fd9ac80..bfbe6cc 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -2,6 +2,7 @@ package bootstrap import ( "context" + "embed" "fmt" "log" "os" @@ -22,7 +23,7 @@ import ( ) // Init 初始化应用各项服务 -func Init() *gin.Engine { +func Init(webFS embed.FS) *gin.Engine { // 0. 确保数据目录存在 _ = os.MkdirAll("data/picture", 0755) @@ -80,7 +81,7 @@ func Init() *gin.Engine { }() // 7. 设置路由 - return http.SetupRouter() + return http.SetupRouter(webFS) } // LogWelcomeInfo 输出欢迎信息和快速跳转地址 diff --git a/internal/http/router.go b/internal/http/router.go index ddec995..7c74505 100644 --- a/internal/http/router.go +++ b/internal/http/router.go @@ -1,22 +1,25 @@ package http import ( + "embed" + "io" + "io/fs" "net/http" "os" "path/filepath" + "strings" _ "BingPaper/docs" "BingPaper/internal/config" "BingPaper/internal/http/handlers" "BingPaper/internal/http/middleware" - "BingPaper/web" "github.com/gin-gonic/gin" swaggerFiles "github.com/swaggo/files" ginSwagger "github.com/swaggo/gin-swagger" ) -func SetupRouter() *gin.Engine { +func SetupRouter(webFS embed.FS) *gin.Engine { r := gin.Default() // Swagger @@ -25,28 +28,6 @@ func SetupRouter() *gin.Engine { // 静态文件 r.Static("/static", "./static") - webPath := config.GetConfig().Web.Path - indexPath := filepath.Join(webPath, "index.html") - - serveIndex := func(c *gin.Context) { - // 1. 优先尝试从配置的路径读取 - if _, err := os.Stat(indexPath); err == nil { - c.File(indexPath) - return - } - // 2. 如果外部文件不存在,则使用内置嵌入的文件 - data, err := web.FS.ReadFile("index.html") - if err != nil { - c.JSON(http.StatusNotFound, gin.H{"error": "web files not found"}) - return - } - c.Data(http.StatusOK, "text/html; charset=utf-8", data) - } - - r.GET("/", serveIndex) - r.GET("/admin", serveIndex) - r.GET("/login", serveIndex) - api := r.Group("/api/v1") { // 公共接口 @@ -86,5 +67,70 @@ func SetupRouter() *gin.Engine { } } + // 静态资源服务与 SPA 路由 (放在最后,确保 API 路由优先) + webSub, _ := fs.Sub(webFS, "web") + + r.NoRoute(func(c *gin.Context) { + path := c.Request.URL.Path + + // 如果请求的是 API 或 Swagger,则不处理静态资源 (让其返回 404) + if strings.HasPrefix(path, "/api") || strings.HasPrefix(path, "/swagger") { + return + } + + // 辅助函数:尝试从外部或嵌入服务文件 + serveFile := func(relPath string, allowExternal bool) bool { + // 1. 优先尝试外部路径 + webPath := config.GetConfig().Web.Path + if allowExternal && webPath != "" { + fullPath := filepath.Join(webPath, relPath) + if info, err := os.Stat(fullPath); err == nil && !info.IsDir() { + c.File(fullPath) + return true + } + } + + // 2. 尝试嵌入式文件 + f, err := webSub.Open(relPath) + if err == nil { + defer f.Close() + stat, err := f.Stat() + if err == nil && !stat.IsDir() { + if rs, ok := f.(io.ReadSeeker); ok { + http.ServeContent(c.Writer, c.Request, stat.Name(), stat.ModTime(), rs) + return true + } + // 兜底:直接读取并输出 + data, err := io.ReadAll(f) + if err == nil { + c.Data(http.StatusOK, "", data) + return true + } + } + } + return false + } + + // 1. 尝试直接请求的文件 (如果是 / 则尝试 index.html) + requestedPath := strings.TrimPrefix(path, "/") + if requestedPath == "" { + requestedPath = "index.html" + } + + if serveFile(requestedPath, true) { + return + } + + // 2. SPA 支持:对于非文件请求(没有后缀或不包含点),尝试返回 index.html + isAsset := strings.Contains(requestedPath, ".") + if !isAsset || requestedPath == "index.html" { + if serveFile("index.html", true) { + return + } + } + + c.Status(http.StatusNotFound) + }) + return r } diff --git a/main.go b/main.go index f610f55..aa506b1 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,9 @@ package main import ( + "embed" "fmt" + "mime" "BingPaper/internal/bootstrap" "BingPaper/internal/config" @@ -10,6 +12,9 @@ import ( "go.uber.org/zap" ) +//go:embed all:web +var webFS embed.FS + // @title BingPaper API // @version 1.0 // @description 必应每日一图抓取、存储、管理与公共 API 服务。 @@ -21,8 +26,13 @@ import ( // @name Authorization func main() { + // 注册常用 MIME 类型,确保嵌入式资源能被正确识别 + mime.AddExtensionType(".js", "application/javascript") + mime.AddExtensionType(".css", "text/css") + mime.AddExtensionType(".svg", "image/svg+xml") + // 1. 初始化 - r := bootstrap.Init() + r := bootstrap.Init(webFS) // 2. 输出欢迎信息 bootstrap.LogWelcomeInfo() diff --git a/web/web.go b/web/web.go deleted file mode 100644 index 38ca34f..0000000 --- a/web/web.go +++ /dev/null @@ -1,6 +0,0 @@ -package web - -import "embed" - -//go:embed index.html -var FS embed.FS