Files
goauto/server/app/admin/router/spa.go
T
QiuSWandClaude Opus 5 c86e98d0cb feat(#48): make one config work for both development and deployment
The repo already layered file defaults under environment overrides, on
both the backend (settings.yml < GOAUTO_*) and the frontend
(.env.production < process.env). What was missing was a translator for
production: config.yaml only ever existed for the PowerShell launchers,
so a packaged binary read none of it and had no database credentials
either — SYB was inheriting an existing gap, not creating one.

The server now reads config.yaml itself, between settings.yml and the
environment. Lookup is GOAUTO_CONFIG, then ./config.yaml, then beside the
executable, so a packaged binary works wherever it is started. An absent
file is not an error: containers supply everything through the
environment. Scalars are read by YAML type and coerced, so an unquoted
all-digit password cannot take startup down over a quoting detail.

This removed the need for a Read-SybConfig in PowerShell: the launcher
just hands over the path it already knows, rather than reimplementing a
YAML parser.

The server also serves the built frontend when dist is present, which is
what .env.production's empty VUE_APP_BASE_API already assumes. The
history fallback is restricted to non-API GETs, and is not installed at
all without dist, so development 404s stay 404s.

Precedence is mutation-tested: applying the local file after the
environment instead of before makes the layering test fail.

Not verified: the PowerShell change and any Windows deployment — both
need a run on the Windows side.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-20 09:28:45 +08:00

73 lines
2.2 KiB
Go

package router
import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
)
// SPADirEnv overrides where the built frontend lives. Without it the server
// looks for ./dist next to the working directory.
const SPADirEnv = "GOAUTO_WEB_DIST"
// InitSPARouter serves the built Vue app from the same origin as the API.
//
// Same-origin is what web/.env.production already assumes: VUE_APP_BASE_API is
// empty there, so the bundle issues relative requests. Serving dist from this
// binary makes that assumption true without a second web server and without
// CORS.
//
// `[必须]` Registering this is conditional on dist actually existing. In
// development the frontend runs under vite on its own port and there is no
// 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 {
return
}
engine.Static("/assets", filepath.Join(dist, "assets"))
for _, name := range []string{"favicon.ico", "logo.png"} {
if path := filepath.Join(dist, name); fileExists(path) {
engine.StaticFile("/"+name, path)
}
}
// History-mode routing means any unmatched non-API GET is a client route
// and must return index.html so the app can boot and route it itself.
//
// `[必须]` API paths are excluded. Answering a mistyped API call with 200
// and an HTML body would surface in the client as a JSON parse error
// instead of the 404 it actually is.
engine.NoRoute(func(c *gin.Context) {
if c.Request.Method != http.MethodGet || isAPIPath(c.Request.URL.Path) {
c.Status(http.StatusNotFound)
return
}
c.File(index)
})
}
// 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"} {
if strings.HasPrefix(path, prefix) {
return true
}
}
return false
}
func fileExists(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}