Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c13dcd3ebc | ||
|
|
0b01b92c98 |
@@ -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
|
||||
<!-- gitea-wiki-mirror:end -->
|
||||
|
||||
<!-- gitea-wiki-mirror:start -->
|
||||
@@ -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 误配为全部转发也不再出现欢迎页。
|
||||
|
||||
@@ -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"} {
|
||||
|
||||
@@ -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("<!doctype html>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() != "<!doctype html>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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user