package api import ( "io/fs" "net/http" "path" "strings" "github.com/gin-gonic/gin" "fileshare/internal/response" web "fileshare/web" ) // registerWeb 注册前端静态资源与 SPA 回退(必须最后注册): // - 静态资源命中 web/dist 内文件则直接服务(带 Immutable 缓存,html 不缓存); // - 未命中且为 GET/HEAD 且非 /api 前缀:回退 index.html(前端 history 路由 // /s/:code、/admin/*、/docs、/openapi 由 SPA 接管); // - /api/* 未命中路由:JSON 404(避免调试时拿到 HTML 掩盖真实错误)。 func registerWeb(r *gin.Engine, d *Deps) { dist, err := web.Dist() if err != nil { return // 嵌入异常时跳过(API 仍可用) } fileServer := http.StripPrefix("/", http.FileServer(http.FS(dist))) indexHTML := readIndexHTML(dist) r.NoRoute(func(c *gin.Context) { p := c.Request.URL.Path // 1. API 未命中:JSON 404 if strings.HasPrefix(p, "/api/") || p == "/api" { response.Fail(c, http.StatusNotFound, "接口不存在") return } // 2. 静态资源命中:直接服务 if p != "/" { clean := strings.TrimPrefix(path.Clean(p), "/") if clean != "" { if f, err := dist.Open(clean); err == nil { _ = f.Close() fileServer.ServeHTTP(c.Writer, c.Request) return } } } // 3. SPA 回退:仅 GET/HEAD 且接受 HTML if c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead { accept := c.GetHeader("Accept") if accept == "" || strings.Contains(accept, "text/html") || strings.Contains(accept, "*/*") { if indexHTML != nil { c.Data(http.StatusOK, "text/html; charset=utf-8", indexHTML) return } } // 非 HTML 请求未命中:普通 404 c.Status(http.StatusNotFound) return } c.Status(http.StatusNotFound) }) } // readIndexHTML 读取嵌入的 index.html(SPA 回退用)。 func readIndexHTML(dist fs.FS) []byte { f, err := dist.Open("index.html") if err != nil { return nil } defer func() { _ = f.Close() }() data, err := fs.ReadFile(dist, "index.html") if err != nil { return nil } return data }