feat(gateway): 内嵌 admin 控制台(go:embed),去掉单独 nginx 容器
复刻 sundynix-site 的 webfs 模式:gateway 一个容器同时 serve 控制台 UI + API + 供 桌面端直连。admin 用 HashRouter + API base 走相对 /api/v1(构建传 VITE_GATEWAY=''), 源码零改动即可同源 serve。 - internal/webui:go:embed all:admin_dist + Dist();占位 index.html 让不构建前端也能编译。 目录命名 admin_dist(避开 .gitignore dist/ 与 .dockerignore **/dist 通配)。 - router.go NoRoute:非 /api/ 路径走内嵌静态,命中 /assets/* 直吐、否则回退 index.html; embed 失败仅 log 降级不影响 API。/metrics /healthz /readyz /api/v1 已注册,永不进 NoRoute。 - Dockerfile 加 admin node 构建 stage,go build 前 COPY dist 覆盖 embed 占位。 - live 验证:/ 返回 admin(200)、/assets/*.js(200)、/api/v1/admin/overview(401 走 API)、/healthz(200)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,9 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -16,6 +18,7 @@ import (
|
||||
"github.com/sundynix/sundynix-gateway/internal/middleware"
|
||||
"github.com/sundynix/sundynix-gateway/internal/nats"
|
||||
"github.com/sundynix/sundynix-gateway/internal/store"
|
||||
"github.com/sundynix/sundynix-gateway/internal/webui"
|
||||
)
|
||||
|
||||
// New 构建带有 Guardrail / 限流中间件的 Gin 引擎。
|
||||
@@ -163,6 +166,31 @@ func New(db *store.Postgres, cache *store.Redis, bus *nats.Bus, blobStore *blob.
|
||||
admin.GET("/guardrail-events", h.GuardrailEvents) // 护栏命中安全事件流(倒序,翻页)
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内嵌 admin 运维控制台:非 API 路径走 SPA ──
|
||||
// admin 用 HashRouter,深链走 /#/...,服务端只会收到对 / 和 /assets/* 的请求:
|
||||
// 命中真实文件直吐,其余回退根 index.html 交前端接管。/api/、/metrics、/healthz、
|
||||
// /readyz 都是已注册路由,永不进 NoRoute。embed 加载失败仅降级(控制台不可用),不影响 API。
|
||||
if adminDist, err := webui.Dist(); err != nil {
|
||||
log.Printf("[gateway] admin 控制台静态资源加载失败(UI 不可用,API 不受影响): %v", err)
|
||||
} else {
|
||||
adminServer := http.FileServer(http.FS(adminDist))
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
p := c.Request.URL.Path
|
||||
if strings.HasPrefix(p, "/api/") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "接口不存在"})
|
||||
return
|
||||
}
|
||||
if rel := strings.TrimPrefix(p, "/"); rel != "" {
|
||||
if _, statErr := fs.Stat(adminDist, rel); statErr == nil {
|
||||
adminServer.ServeHTTP(c.Writer, c.Request) // /assets/* 等真实文件
|
||||
return
|
||||
}
|
||||
}
|
||||
c.Request.URL.Path = "/" // 其余回退 index.html
|
||||
adminServer.ServeHTTP(c.Writer, c.Request)
|
||||
})
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user