diff --git a/docs/13-deployment-and-operations.md b/docs/13-deployment-and-operations.md index aaf0f90..1d0f490 100644 --- a/docs/13-deployment-and-operations.md +++ b/docs/13-deployment-and-operations.md @@ -2,8 +2,8 @@ generated: true (请先修改 Gitea Wiki,禁止直接编辑本文件) wiki_page: Deployment-and-Operations wiki_url: https://git.ilapage.cn/OPC/goauto/wiki/Deployment-and-Operations.- -wiki_revision: 9c10d971fe1dde057b13972566444a2234dd7f0e -synchronized_at: 2026-09-27T03:42:42Z +wiki_revision: 3a76e16d43c940425f68cc4748944213e980ad60 +synchronized_at: 2026-09-28T08:10:00Z @@ -26,13 +26,47 @@ synchronized_at: 2026-09-22T02:56:36Z ## 当前线上拓扑 -- 外部入口:`http://185.216.248.75:9527`,Nginx 同时承载 Admin 静态资源并反向代理 GoAuto API。 +- 外部入口:`http://122.228.200.167:9527`(2026-09-28 起;此前为 `185.216.248.75:9527`,旧机 `goauto.service` 已停止)。Nginx 在 9527 **直接提供 Admin 静态资源**,未命中静态文件的请求反向代理到 GoAuto API,配置见下节「Nginx 入口(9527)」。 - GoAuto 服务监听:`127.0.0.1:8010`。 -- 常驻服务:systemd `goauto.service`。 +- 常驻服务:systemd `goauto.service`(新机依赖 `mysql84-cmhub.service`)。 - 工作目录:`/home/goauto/current`,指向 `/home/goauto/releases/<发布标识>`。 - 服务配置:`/home/goauto/current/config/settings.yml`;敏感环境变量由 `/etc/goauto/goauto.env` 提供,不写入 Git、Wiki、工单或日志。 - Agent APK 私有目录:相对工作目录的 `var/goauto-agent-releases`;下载必须通过已认证接口。 +## Nginx 入口(9527) + +线上 vhost:`/www/server/panel/vhost/nginx/goauto-9527.conf`(宝塔面板目录)。标准配置: + +```nginx +server { + listen 9527 default_server; + server_name 122.228.200.167 _; + root /home/goauto/current/dist; + index index.html; + client_max_body_size 100m; + allow all; + location = / { + try_files /index.html =404; + } + location / { + try_files $uri @goauto_backend; + } + location @goauto_backend { + proxy_pass http://127.0.0.1:8010; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_read_timeout 300s; + } +} +``` + +`[必须]` 不得把 9527 写成「全部 `proxy_pass` 到 8010」:GoAuto 服务端(go-admin)在 `GET /` 注册了「GO-ADMIN欢迎您」欢迎页,前端 SPA 只通过未匹配路由兜底提供,全部转发时首页就会显示欢迎页而不是 Admin 后台(2026-09-28 迁移时发生过,见 #346)。`location = /` 必须由 Nginx 返回 `dist/index.html`;其余路径先找静态文件,找不到再交给后端(后端对非 API 的 GET 返回 index.html 以支持前端路由,对 `/api/` 等返回真实结果)。修改后先 `nginx -t` 再 `nginx -s reload`,改前备份原文件。 + ## Agent HTTP 例外 服务端生产模式默认要求 Agent 使用 HTTPS。当前线上入口只有 HTTP,因此经 #181 用户明确确认,在 `/etc/goauto/goauto.env` 设置: @@ -49,7 +83,13 @@ GOAUTO_ALLOW_INSECURE_AGENT_HTTP=true 1. 在本地完成服务端测试/构建和 Android 单测/APK 构建,记录提交、versionCode、SHA-256 与大小。 2. 创建新的 `/home/goauto/releases/<发布标识>`,复制服务端二进制、Web 静态资源和非敏感配置;保留旧发布目录用于回滚。 -3. 原子切换 `/home/goauto/current` 后重启 `goauto.service`,确认 `systemctl is-active goauto.service` 为 `active`,并从外部入口验证 API。 +3. 原子切换 `/home/goauto/current` 后重启 `goauto.service`,确认 `systemctl is-active goauto.service` 为 `active`,再从外部入口**按内容**验收(只看 HTTP 200 不算通过——欢迎页、错误页也可能是 200): + - `GET /`:返回 HTML,包含 `id="app"`,且**不包含**「GO-ADMIN欢迎您」; + - 前端路由(如 `GET /login`):同样返回 Admin 的 index.html; + - `index.html` 引用的 `/js/…`、`/css/…` 资源:HTTP 200; + - `GET /api/v1/captcha`:`application/json` 且 `code=200`; + - 任一业务接口未登录访问(如 `GET /api/admin/v1/yeeke-returns`):JSON 业务码 401; + - 结构日志无 panic/fatal/1146/1054。 4. 上传 APK 到 Admin Agent 版本并按需设为当前;服务端解析 Manifest,校验 versionCode 唯一性并保存 SHA-256。 5. 真机安装前确认设备没有运行中的任务。Android 系统安装确认仍由人工完成,Agent 不静默安装。 @@ -61,6 +101,19 @@ Admin 蝦皮规格 AI 匹配会同步等待外部 Provider:Provider 配置允 Provider 故障日志只允许记录调用关联 ID、操作类型、耗时、上游 HTTP 状态码或网络错误分类;不得记录 API Key、Authorization、Provider URL、模型输入、候选规格、商品原始内容或响应正文。Provider 失败时接口返回 HTTP 503 与 `AI_MATCHING_UNAVAILABLE`,供 Admin 显示安全中文提示。 +## 服务器迁移清单 + +更换线上服务器时逐项核对,每项都要在新机上回读确认: + +1. **Nginx**:9527 vhost 按上节标准配置写入(root 指向 `/home/goauto/current/dist`,`location = /` 返回 index.html),`nginx -t` 通过后 reload; +2. **发布目录**:`/home/goauto/releases/<发布标识>` 含二进制、`dist/index.html`、`config/settings.yml`(含 `extend.syb`、`extend.yeeke` 段);`current` 软链接指向它;静态目录与 `var`(APK 私有目录)随迁;属主 `goauto`; +3. **环境变量**:`/etc/goauto/goauto.env` 与旧机字段一致(`GOAUTO_DB_*`、`GOAUTO_SERVER_PORT=8010`、`GOAUTO_WEB_DIST`、`GOAUTO_CONFIG`、SYB/yeeke 账号、Agent HTTP 例外等),权限 600 `root:goauto`;含中文等非 ASCII 值时按字节核对,不能只比长度; +4. **systemd**:`goauto.service` 的 `WorkingDirectory`、`EnvironmentFile`、`ExecStart` 与依赖的数据库服务; +5. **数据库**:数据完整迁移后,`sys_migration` 最新版本与旧机一致; +6. **定时任务**:`sys_job` 启用状态与旧机一致,避免新旧两机同时执行同一定时任务(旧机须停服); +7. **外部依赖**:SYB、yeeke、OCR 服务可达,先用手动同步验证登录; +8. 按「发布与验证」第 3 步做按内容验收;更新本页「当前线上拓扑」。 + ## 回滚 服务异常时把 `/home/goauto/current` 切回上一已验证发布目录并重启 `goauto.service`,随后复核服务状态和 Agent 接口。不要删除当前或历史 APK/发布目录来代替回滚;数据库变化如需回退必须单独评估。 @@ -180,3 +233,9 @@ Provider 故障日志只允许记录调用关联 ID、操作类型、耗时、 ## #338 退货匹配发布 发布时执行退货匹配及售后权限迁移,切换 Server/Web release,重启 `goauto.service` 并 reload Nginx;发布后验证健康接口、Web 首页、售后登录及退货匹配只读接口,不用真实商品提交作为健康检查。 + +## 2026-09-28 迁移到 122.228.200.167 与首页修复(#346) + +- 线上服务由 185.216.248.75 迁移到 122.228.200.167(发布目录 `20260928-344-9dace6a`)。 +- 迁移后 `http://122.228.200.167:9527/` 显示 go-admin 欢迎页:新机 9527 vhost 写成了全部 `proxy_pass`,缺少 `root /home/goauto/current/dist` 与 `location = /`。已按「Nginx 入口(9527)」标准配置修复(原文件备份为 `goauto-9527.conf.bak-20260928150822`),`nginx -t` 通过后 reload;按内容验收首页、前端路由、静态资源、验证码与未登录接口均通过。 +- 代码侧根治见 #346:存在 dist 时服务端 `GET /` 也返回 SPA index.html,即使 Nginx 误配为全部转发也不再出现欢迎页。 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) }