diff --git a/server/app/admin/router/spa.go b/server/app/admin/router/spa.go index 9903f77..3685bc1 100644 --- a/server/app/admin/router/spa.go +++ b/server/app/admin/router/spa.go @@ -25,12 +25,8 @@ const SPADirEnv = "GOAUTO_WEB_DIST" // dist; a NoRoute handler installed anyway would turn every genuine 404 into // an HTML page, which is far more confusing than a plain 404. func InitSPARouter(engine *gin.Engine) { - dist := strings.TrimSpace(os.Getenv(SPADirEnv)) - if dist == "" { - dist = "dist" - } - index := filepath.Join(dist, "index.html") - if _, err := os.Stat(index); err != nil { + dist, index, ok := spaIndex() + if !ok { return } @@ -56,6 +52,38 @@ func InitSPARouter(engine *gin.Engine) { }) } +// spaIndex resolves the built frontend directory and reports whether its +// index.html exists. +func spaIndex() (dist, index string, ok bool) { + dist = strings.TrimSpace(os.Getenv(SPADirEnv)) + if dist == "" { + dist = "dist" + } + index = filepath.Join(dist, "index.html") + if _, err := os.Stat(index); err != nil { + return dist, index, false + } + return dist, index, true +} + +// registerRootRoute decides what `GET /` returns (#346). +// +// `[必须]` When the built frontend exists, `/` must be the Admin SPA. go-admin's +// welcome page used to own `/` in every non-prod mode, so any reverse proxy +// that forwarded `/` to this server (instead of serving dist itself) showed +// "GO-ADMIN欢迎您" instead of the Admin — which is exactly what happened after +// the 2026-09-28 server migration. The welcome page is kept only for +// development without a dist, where the frontend runs under vite. +func registerRootRoute(r gin.IRoutes, mode string, welcome gin.HandlerFunc) { + if _, index, ok := spaIndex(); ok { + r.GET("/", func(c *gin.Context) { c.File(index) }) + return + } + if mode != "prod" { + r.GET("/", welcome) + } +} + // isAPIPath reports whether a path belongs to the server rather than the SPA. func isAPIPath(path string) bool { for _, prefix := range []string{"/api/", "/swagger/", "/static/", "/form-generator/", "/ws/", "/wslogout/", "/info"} { diff --git a/server/app/admin/router/spa_test.go b/server/app/admin/router/spa_test.go index 6d9a112..553779e 100644 --- a/server/app/admin/router/spa_test.go +++ b/server/app/admin/router/spa_test.go @@ -91,3 +91,46 @@ func TestWithoutDistNoFallbackIsInstalled(t *testing.T) { t.Fatalf("没有 dist 时接口仍应正常: %d", response.Code) } } + +// #346: with a built frontend, `/` must be the Admin SPA — never go-admin's +// welcome page, even in non-prod modes where the welcome page used to own `/`. +func TestRootServesSPAWhenDistExists(t *testing.T) { + gin.SetMode(gin.TestMode) + dist := filepath.Join(t.TempDir(), "dist") + if err := os.MkdirAll(dist, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(dist, "index.html"), []byte("SPA"), 0o644); err != nil { + t.Fatal(err) + } + t.Setenv(SPADirEnv, dist) + for _, mode := range []string{"dev", "test", "prod"} { + engine := gin.New() + registerRootRoute(engine, mode, func(c *gin.Context) { c.String(http.StatusOK, "GO-ADMIN欢迎您") }) + InitSPARouter(engine) + response := do(engine, http.MethodGet, "/") + if response.Code != http.StatusOK || response.Body.String() != "SPA" { + t.Fatalf("mode=%s: / should serve index.html, got %d %q", mode, response.Code, response.Body.String()) + } + } +} + +// Without a dist (development under vite) the previous behaviour is kept: +// welcome page outside prod, nothing registered in prod. +func TestRootWithoutDistKeepsPreviousBehaviour(t *testing.T) { + gin.SetMode(gin.TestMode) + t.Setenv(SPADirEnv, filepath.Join(t.TempDir(), "missing-dist")) + welcome := func(c *gin.Context) { c.String(http.StatusOK, "GO-ADMIN欢迎您") } + + dev := gin.New() + registerRootRoute(dev, "dev", welcome) + if response := do(dev, http.MethodGet, "/"); response.Code != http.StatusOK || response.Body.String() != "GO-ADMIN欢迎您" { + t.Fatalf("dev without dist should keep the welcome page, got %d %q", response.Code, response.Body.String()) + } + + prod := gin.New() + registerRootRoute(prod, "prod", welcome) + if response := do(prod, http.MethodGet, "/"); response.Code != http.StatusNotFound { + t.Fatalf("prod without dist should not register /, got %d", response.Code) + } +} diff --git a/server/app/admin/router/sys_router.go b/server/app/admin/router/sys_router.go index 3176a17..885d218 100644 --- a/server/app/admin/router/sys_router.go +++ b/server/app/admin/router/sys_router.go @@ -40,9 +40,7 @@ func sysBaseRouter(r *gin.RouterGroup) { go ws.WebsocketManager.SendService() go ws.WebsocketManager.SendAllService() - if config.ApplicationConfig.Mode != "prod" { - r.GET("/", apis.GoAdmin) - } + registerRootRoute(r, config.ApplicationConfig.Mode, apis.GoAdmin) r.GET("/info", handler.Ping) }