package routers import ( "net/http" "os" "strings" beego "github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/server/web/context" ) // RegisterPlatformSPA 让 Go 自己吐指定 hosts 的平台后台 SPA 静态资源, // 替代 nginx 在对应 server 块里的 // // location ~ ^/(platform|backend|auth|api|app)/ { proxy_pass http://127.0.0.1:8081; ... } // // 那段反代规则(曾因宝塔重生成 vhost 被冲掉而出现 "页面 404")。 // // 规则: // - 仅对 hosts 列表里的 host 生效; // - 这些前缀直接放行交给 beego Router / Static 处理,不接管: // /platform/、/backend/、/auth/、/api/、/app/ → 业务 API // /uploads/、/themes/ → main.go SetStaticPath 注册的静态目录 // /.well-known/ → ACME 证书验证目录 // - 其余请求从 spaDir 直接 ServeFile; // - 文件不存在时回 index.html(SPA hash 路由 fallback)。 // // 调用方在 router.go init() 里读 app.conf 的 platform_spa_dir / platform_spa_hosts。 // spaDir 为空或 hosts 为空则视为关闭功能,行为不变。 func RegisterPlatformSPA(spaDir string, hosts ...string) { spaDir = strings.TrimSpace(spaDir) if spaDir == "" || len(hosts) == 0 { return } hostSet := make(map[string]struct{}, len(hosts)) for _, h := range hosts { h = strings.ToLower(strings.TrimSpace(h)) if h != "" { hostSet[h] = struct{}{} } } passThrough := []string{ "/platform/", "/backend/", "/auth/", "/api/", "/app/", "/uploads/", "/themes/", "/.well-known/", } beego.InsertFilter("*", beego.BeforeStatic, func(ctx *context.Context) { host := strings.ToLower(ctx.Request.Host) if i := strings.IndexByte(host, ':'); i > 0 { host = host[:i] } if _, ok := hostSet[host]; !ok { return } path := ctx.Request.URL.Path for _, p := range passThrough { if strings.HasPrefix(path, p) { return } } // 静态资源或入口 rel := path if rel == "" || rel == "/" { rel = "/index.html" } target := spaDir + rel if f, err := os.Stat(target); err == nil && !f.IsDir() { http.ServeFile(ctx.ResponseWriter, ctx.Request, target) return } // SPA fallback:未知路径(hash 路由直接访问 /xxx/yyy)回 index.html。 // 标记一下,避免安全探针过滤器把「兜底返回首页」误判成敏感文件泄露。 ctx.Input.SetData("spa_fallback", true) http.ServeFile(ctx.ResponseWriter, ctx.Request, spaDir+"/index.html") }) }