package main import ( "embed" "io/fs" "net/http" "strings" "github.com/gin-gonic/gin" ) //go:embed all:static var frontendFS embed.FS // setupFrontend 配置前端静态文件服务(go:embed 模式) // Logan.Li // 前端构建产物放在 backend/static/ 目录,编译时嵌入二进制 func setupFrontend(r *gin.Engine) { // 获取 static 子目录的 fs.FS staticFS, err := fs.Sub(frontendFS, "static") if err != nil { // static 目录不存在(开发模式或未构建前端),跳过 return } httpFS := http.FS(staticFS) // 静态资源文件(js/css/图片等)直接返回 r.Use(func(c *gin.Context) { // API 和 WebSocket 路径不走静态文件 path := c.Request.URL.Path if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/ws/") || strings.HasPrefix(path, "/agent/") || strings.HasPrefix(path, "/health") || strings.HasPrefix(path, "/version") { c.Next() return } c.Next() }) // NoRoute: SPA fallback — 非 API 请求尝试静态文件,找不到则返回 index.html r.NoRoute(func(c *gin.Context) { path := c.Request.URL.Path // API/WS 路径返回 JSON 404 if strings.HasPrefix(path, "/api/") || strings.HasPrefix(path, "/ws/") || strings.HasPrefix(path, "/agent/") { c.JSON(http.StatusNotFound, gin.H{"code": 404, "message": "接口不存在"}) return } // 尝试打开静态文件 cleanPath := strings.TrimPrefix(path, "/") if cleanPath == "" { cleanPath = "index.html" } f, err := staticFS.Open(cleanPath) if err == nil { f.Close() if cleanPath == "index.html" || strings.HasSuffix(cleanPath, ".html") { // HTML 入口必须每次校验,避免浏览器长期执行旧版前端 bundle。// Logan.Li c.Header("Cache-Control", "no-store, no-cache, must-revalidate") } else { // 当前内嵌构建在本地验证阶段需要强制校验,避免旧 bundle 残留。// Logan.Li c.Header("Cache-Control", "no-cache") } c.FileFromFS(path, httpFS) return } if strings.HasPrefix(cleanPath, "assets/") { // 构建哈希资源缺失时返回真实 404,避免 SPA fallback 把 index.html 当 JS 模块返回。 // Logan.Li c.Header("Cache-Control", "no-store, no-cache, must-revalidate") c.String(http.StatusNotFound, "static asset not found") return } // SPA fallback → 直接返回 index.html 字节(避免 http.FileServer 的 301 redirect) indexBytes, err := fs.ReadFile(staticFS, "index.html") if err != nil { c.String(http.StatusInternalServerError, "前端文件缺失") return } c.Header("Cache-Control", "no-store, no-cache, must-revalidate") c.Data(http.StatusOK, "text/html; charset=utf-8", indexBytes) }) }