Files
frontend-miniapp/docs/PRODUCTION_MOBILE_DEPLOYMENT_RUNBOOK.md
cxk 9037f4d3e5
Some checks failed
CI / verify (push) Has been cancelled
docs: 同步移动端生产部署手册
2026-07-24 23:56:22 +08:00

682 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# frontend-miniapp 正式环境部署手册
最后更新2026-07-24同步五节点 `17083` 缓存策略修复及逐节点验收)
## 适用范围
本文档适用于将本地仓库 `F:\深圳自然博物馆\frontend-miniapp` 的 H5 产物部署到深圳自然博物馆正式环境。
- 正式域名:`https://guide.sznhmuseum.org.cn`
- 主节点:`172.20.14.21`
- 部署目录:`/data/nginx/html/mobile`
- Nginx 容器:`sgs-nginx``nginx/1.30.3`Docker host 网络)
- Nginx 生效配置目录:`/data/nginx/conf.d`(容器内 `/etc/nginx/conf.d`,只读挂载)
- 同步脚本:`/data/scripts/sync-frontend-nodes.sh`
- Nginx 配置变更记录2026-07-24五台 `17083` 已统一 assets/字体缓存及静态资源 404 防 SPA 回退规则
- 同步节点:`172.20.14.23``172.20.14.26``172.20.14.27``172.20.14.33`
- 远程仓库:`http://192.168.0.93:3333/lyf/frontend-miniapp.git`
不要把服务器密码、证书私钥、token、真实后台账号、腾讯地图正式 key 写入仓库文档或提交记录。
## 部署前检查
进入项目目录:
```powershell
cd 'F:\深圳自然博物馆\frontend-miniapp'
```
检查分支、远程仓库和本地改动:
```powershell
git status --short --branch --untracked-files=all
git log -1 --pretty=format:'%h %s'
git remote -v
```
正式环境当前依赖一组本地生产补丁,拉取远程代码前必须保护。常见补丁文件包括:
```text
.env.development
.env.production
src/config/dataSource.ts
src/data/adapters/guideDataAdapter.ts
src/data/providers/staticMuseumContentProvider.ts
src/env.d.ts
src/repositories/ExplainRepository.ts
src/usecases/explainUseCase.ts
src/utils/publicUrl.ts
static/guide-data/exhibits.json
static/guide-data/guide-contents.json
static/guide-data/guide-stops.json
static/guide-data/manifest.json
```
这些文件中的改动属于正式环境本地补丁,拉取远程代码时不要直接丢弃。当前正式环境相关配置要点:
- `VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=172.20.14.21`,避免正式包混入测试服务器 `1.92.206.90`
- `VITE_EXPLAIN_ALLOW_STATIC_FALLBACK=true`,讲解数据接口异常时允许使用静态兜底数据。
- `VITE_SGS_SDK_ORIGIN=https://guide.sznhmuseum.org.cn`,正式域名不再使用 `:4430`
- `static/guide-data/manifest.json``sourceHost` 应为 `172.20.14.21`
- 腾讯地图正式 key 只通过环境变量 `VITE_TENCENT_MAP_KEY` 注入,不能写入文档或提交记录。
## 拉取远程最新代码
先临时保存本地生产补丁:
```powershell
$stamp = Get-Date -Format 'yyyyMMddHHmmss'
$msg = "pre-deploy-prod-local-fixes-$stamp"
if (git status --porcelain) {
git stash push -u -m $msg
Write-Output "STASHED=$msg"
}
```
执行成功时会看到类似输出,表示本地补丁已 stash
```text
Saved working directory and index state On master: pre-deploy-prod-local-fixes-YYYYMMDDHHmmss
STASHED=pre-deploy-prod-local-fixes-YYYYMMDDHHmmss
```
拉取远程最新代码:
```powershell
git fetch origin
git pull --ff-only origin master
git log -1 --pretty=format:'%h %s'
```
恢复生产补丁:
```powershell
$stash = git stash list | Select-String $msg | Select-Object -First 1
if ($stash) {
$stashRef = ($stash.Line -split ':')[0]
git stash pop $stashRef
}
git status --short --branch --untracked-files=all
```
如出现冲突,先解决冲突并重新确认生产配置,再继续构建。
恢复后重点检查 `.env.production``static/guide-data/manifest.json``src/utils/publicUrl.ts`,确认正式环境仍指向 `172.20.14.21``https://guide.sznhmuseum.org.cn`
## Nginx 当前生产配置2026-07-24
本节以 `172.20.14.21``docker exec sgs-nginx nginx -T` 的实际生效结果为准。主 Nginx 使用 Docker host 网络,公共域名不直接读取宿主静态目录,而是通过 upstream `sgs_mobile_pool` 转发到五台 `17083` 工作节点:
```text
guide.sznhmuseum.org.cn:80 -> sgs_mobile_pool -> 172.20.14.21/.23/.26/.27/.33:17083
guide.sznhmuseum.org.cn:443 -> sgs_mobile_pool -> 172.20.14.21/.23/.26/.27/.33:17083
```
当前移动端工作节点 `17083` 的关键规则如下:
```nginx
server {
listen 17083;
root /usr/share/nginx/html/mobile;
location = /healthz {
access_log off;
return 200 "ok\n";
}
# 构建后的带 hash 入口和静态依赖:一年不可变缓存
location ^~ /assets/ {
add_header Cache-Control "public, max-age=31536000, immutable";
try_files $uri =404;
}
# 模型、manifest、GLB/GLTF 等:明确 MIME缺失时返回 404
location ^~ /static/nav-assets/ {
types {
application/json json;
model/gltf-binary glb;
model/gltf+json gltf;
text/csv csv;
text/markdown md;
}
default_type application/octet-stream;
expires 30d;
add_header Cache-Control "public";
try_files $uri =404;
}
# 字体30 天缓存;其他 static 资源不得回退 index.html
location ^~ /static/Fonts/ {
add_header Cache-Control "public, max-age=2592000";
try_files $uri =404;
}
location ^~ /static/ {
try_files $uri =404;
}
# 页面路由才允许 SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}
```
注意:
- `/assets/``/static/nav-assets/``/static/Fonts/` 和其他 `/static/` 路径必须优先 `try_files $uri =404`,不能把缺失的 JSON、GLB、字体返回为 `index.html`
- `index.html` 仍应保持不缓存或短缓存;带 hash 的 `/assets/` 才使用长期不可变缓存。
- `guide-public.conf` 的 80/443 入口包含 ACME challenge 的直接静态规则,其余请求代理到 `sgs_mobile_pool`;不要把公共域名改回单节点静态 root。
- 当前 upstream 使用 `least_conn`,单节点由 `max_fails=3 fail_timeout=10s` 被动摘除。
2026-07-24 逐节点直连 `17083` 复核时发现配置漂移:`.21` 缓存策略正确,`.23/.26/.27/.33` 缺少 `/assets/` 长期 immutable 缓存头和 `/static/Fonts/` 专用规则。四台异常节点已分别备份配置、补齐对应 `17083` location并在 `nginx -t` 通过后平滑 reload。修复后五台均满足
- 字体返回 `Cache-Control: public, max-age=2592000`
- 带 hash 的 assets 返回 `Cache-Control: public, max-age=31536000, immutable`
- 字体 ETag 条件请求返回 HTTP `304`,且 `304` 响应仍保留 30 天缓存头;
- 20 个 `17080-17083 /healthz` 端点全部正常,五台 `sgs-nginx` 容器均未重启。
因此,不能只通过公网域名抽样或只检查 `/healthz` 判断五节点配置一致;`least_conn` 可能掩盖单节点漂移。发布验收必须执行本文后面的五节点直连缓存检查。
### Nginx 配置变更步骤
Nginx 配置变更不是普通静态文件发布的一部分,必须单独备份、检查和 reload
```powershell
$script = @'
set -euo pipefail
stamp=$(date +%Y%m%d%H%M%S)
backup=/data/nginx/_backups/nginx-conf-before-mobile-$stamp.tar.gz
mkdir -p /data/nginx/_backups
tar -czf "$backup" -C /data/nginx conf.d
docker exec sgs-nginx nginx -t
docker exec sgs-nginx nginx -s reload
printf 'NGINX_CONFIG_BACKUP=%s\\n' "$backup"
'@
$script | ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash -s'
```
确认配置变更后再执行静态文件发布;如果修改的是工作节点 `17083` 配置,必须在每个受影响节点分别备份、执行 `nginx -t`、平滑 reload 和直连响应头验证,不能假定 `.21` reload 会同步其他节点。静态文件发布完成后仍需执行五节点 `/healthz`、缓存头、ETag/304、资源 MIME 和 404 验证。
## 本地构建
安装依赖并做类型检查:
```powershell
corepack pnpm install --frozen-lockfile
$env:NODE_OPTIONS='--max-old-space-size=16384'
corepack pnpm type-check
```
设置正式构建变量。腾讯地图 key 只通过环境变量注入,不写入仓库:
```powershell
$env:NODE_OPTIONS='--max-old-space-size=16384'
$env:VITE_TENCENT_MAP_KEY='<正式腾讯地图 Web Key>'
$env:VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST='172.20.14.21'
$env:VITE_EXPLAIN_ALLOW_STATIC_FALLBACK='true'
```
执行正式 H5 构建:
```powershell
corepack pnpm build:h5
```
`build:h5` 会执行:
1. `uni build -p h5 --mode production`
2. `node scripts/finalize-h5-build.cjs --require-tencent-map-key`
`finalize-h5-build.cjs` 会自动完成:
- 拷贝 `static/nav-assets``static/guide-data``static/guide``static/icons``static/explain``static/sgs-map-sdk``static/three` 和字体资源。
- 使用 Node 按 UTF-8 安全替换构建产物里的 `__REPLACE_WITH_TENCENT_MAP_WEB_KEY__`
- 对全部构建 JS 执行 `node --check`,防止语法错误产物上线。
- 扫描并阻断 `1.92.206.90``guide.whaoyue.com`、腾讯地图 key 占位符等不应进入正式环境的字符串。
- 校验核心静态 JSON 和关键图标文件存在。
如 terser 压缩阶段出现内存不足,可使用不压缩构建:
```powershell
corepack pnpm run build:h5:no-minify
```
严禁再用 PowerShell `Get-Content` / `Set-Content` 手工替换构建后的 JS。PowerShell 默认编码容易破坏 UTF-8 中文注释或字符串,曾导致线上入口 JS 语法错误、页面空白。
## 构建产物检查
构建脚本会自动输出类似:
```text
H5_ENTRY=assets/index-xxxx.js
H5_ENTRY_SIZE=246232
H5_JS_SYNTAX_CHECKED=...
H5_TENCENT_PLACEHOLDER_REPLACED_FILES=2
H5_BAD_STRING_FILE_COUNT=0
H5_MANIFEST_SOURCE_HOST=172.20.14.21
H5_GUIDE_CONTENTS_ROWS=720
H5_EXHIBITS_ROWS=4700
H5_GUIDE_STOPS_ROWS=398
```
如果需要手工复查入口 JS
```powershell
$dist = Join-Path (Get-Location) 'dist\build\h5'
$html = Get-Content -LiteralPath (Join-Path $dist 'index.html') -Raw
$entry = [regex]::Match($html, 'src="/?([^"]*assets/index-[^"]+\.js)"').Groups[1].Value
node --check (Join-Path $dist $entry)
```
预期无错误输出。
## 打包与上传
```powershell
$ts = Get-Date -Format 'yyyyMMddHHmmss'
New-Item -ItemType Directory -Force -Path '.tmp' | Out-Null
$pkg = ".tmp\frontend-miniapp-h5-$ts.tar.gz"
if (Test-Path $pkg) { Remove-Item -LiteralPath $pkg -Force }
tar -czf $pkg -C 'dist\build\h5' .
scp -o StrictHostKeyChecking=no $pkg root@172.20.14.21:/tmp/frontend-miniapp-h5.tar.gz
```
## 主节点部署
部署时必须保留:
- `/data/nginx/html/mobile/gpL0svkeao.txt`
- `/data/nginx/html/mobile/.well-known`
执行:
```powershell
$script = @'
set -euo pipefail
pkg=/tmp/frontend-miniapp-h5.tar.gz
target=/data/nginx/html/mobile
backup_dir=/data/nginx/html/_backups
ts=$(date +%Y%m%d%H%M%S)
backup="$backup_dir/mobile-before-deploy-$ts.tar.gz"
tmp="/tmp/frontend-miniapp-h5-$ts"
test -s "$pkg"
mkdir -p "$target" "$backup_dir"
tar -czf "$backup" -C "$target" .
rm -rf "$tmp"
mkdir -p "$tmp"
tar -xzf "$pkg" -C "$tmp"
test -f "$tmp/index.html"
find "$target" -mindepth 1 -maxdepth 1 ! -name 'gpL0svkeao.txt' ! -name '.well-known' -exec rm -rf {} +
cp -a "$tmp"/. "$target"/
chmod -R a+rX "$target"
entry=$(grep -o "assets/index-[^\"]*\.js" "$target/index.html" | head -n 1 || true)
test -n "$entry"
test -f "$target/$entry"
node --check "$target/$entry"
if grep -R -I -E '1\.92\.206\.90|guide\.whaoyue\.com|__REPLACE_WITH_TENCENT_MAP_WEB_KEY__|http://1\.92\.206\.90:9000' "$target" >/tmp/mobile_bad_matches.txt; then
cat /tmp/mobile_bad_matches.txt >&2
exit 1
fi
# 仅替换静态文件,不 reload Nginx公共域名通过 sgs_mobile_pool 命中五台 17083 工作节点。
# 如本次同时修改了 /data/nginx/conf.d必须另行执行 nginx -t 后 reload。
printf 'DEPLOY_BACKUP=%s\n' "$backup"
printf 'DEPLOY_ENTRY=%s\n' "$entry"
printf 'DEPLOY_TARGET=%s\n' "$target"
printf 'PRESERVED_GP=%s\n' "$(test -f "$target/gpL0svkeao.txt" && echo yes || echo no)"
printf 'PRESERVED_WELL_KNOWN=%s\n' "$(test -d "$target/.well-known" && echo yes || echo no)"
'@
$script | ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash -s'
```
## 全量同步脚本(仅在 Nginx 配置已复核时使用)
```powershell
ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash /data/scripts/sync-frontend-nodes.sh'
```
执行前必须注意:该脚本不仅同步 `/data/nginx/html`,还会通过 `write_worker_conf()` 重写五台工作节点的 `/data/nginx/conf.d/sgs-worker.conf`。2026-07-24 已修复 `.23/.26/.27/.33``17083` 缓存配置漂移,当前五台生效配置均包含 `/assets/` 一年 immutable 缓存、`/static/Fonts/` 30 天缓存以及静态资源 404 规则;但同步脚本文件本身仍需要单独复核,不能用脚本内容推断当前生产配置。**普通移动端静态发布不得在未复核脚本内嵌配置前直接运行该脚本。**
继续使用同步脚本前先检查它是否已经包含本次规则:
```powershell
ssh -o StrictHostKeyChecking=no root@172.20.14.21 'grep -n -E "static/Fonts|31536000|try_files \\$uri =404" /data/scripts/sync-frontend-nodes.sh'
```
如果检查没有同时看到 `static/Fonts``31536000`,应先在维护窗口审核并更新 `write_worker_conf()`,再执行 `bash -n /data/scripts/sync-frontend-nodes.sh`、逐节点 `nginx -t``/healthz` 验证。不要用 `.21` 的整份 `sgs-worker.conf` 覆盖其他节点,因为 `.21``17080` 还包含本节点专用 CSP 配置;只同步审核后的 `17083` 移动端配置块。
如果本次仅发布 H5 静态文件、没有修改 Nginx 配置,推荐采用“仅同步 `/data/nginx/html/mobile`”的流程,不要触发同步脚本中的 `write_worker_conf()``/data/apps/sgs-map` 同步和 PM2 重启。静态文件替换完成后不需要 Nginx reload只有配置变更才执行 `nginx -t` 后 reload。
若必须按现有脚本执行,必须确认最终输出:
```text
All frontend worker nodes synced.
```
同步脚本会同步整个 `/data/nginx/html`,同时也会处理 `/data/apps/sgs-map` 并通过 PM2 重启 `sgs-map`。这是当前脚本行为,部署 mobile 时也会看到相关输出;除非已复核脚本内嵌 Nginx 配置,否则不要把它作为普通 mobile 发布命令。
### 仅同步 mobile 静态目录(推荐)
普通 H5 发布不需要重新加载 Nginx也不需要同步地图应用。主节点部署通过后将同一个已校验的 `$pkg` 分发到四个工作节点;每台节点独立备份并保留 `gpL0svkeao.txt``.well-known`
```powershell
$workers = @('172.20.14.23','172.20.14.26','172.20.14.27','172.20.14.33')
$workerScript = @'
set -euo pipefail
pkg=/tmp/frontend-miniapp-h5.tar.gz
target=/data/nginx/html/mobile
backup_dir=/data/nginx/html/_backups
ts=$(date +%Y%m%d%H%M%S)
backup="$backup_dir/mobile-before-deploy-$ts.tar.gz"
tmp="/tmp/frontend-miniapp-h5-$ts"
test -s "$pkg"
mkdir -p "$target" "$backup_dir" "$tmp"
tar -czf "$backup" -C "$target" .
tar -xzf "$pkg" -C "$tmp"
test -f "$tmp/index.html"
find "$target" -mindepth 1 -maxdepth 1 ! -name 'gpL0svkeao.txt' ! -name '.well-known' -exec rm -rf {} +
cp -a "$tmp"/. "$target"/
chmod -R a+rX "$target"
entry=$(grep -o 'assets/index-[^"]*\.js' "$target/index.html" | head -n 1)
test -n "$entry"
test -f "$target/$entry"
node --check "$target/$entry"
curl -fsS http://127.0.0.1:17083/healthz
printf 'WORKER_BACKUP=%s\nWORKER_ENTRY=%s\n' "$backup" "$entry"
rm -rf "$tmp" "$pkg"
'@
foreach ($node in $workers) {
scp -o StrictHostKeyChecking=no $pkg "root@${node}:/tmp/frontend-miniapp-h5.tar.gz"
$workerScript | ssh -o StrictHostKeyChecking=no root@$node 'bash -s'
}
```
同步完成后逐节点检查:
```powershell
$nodes = @('172.20.14.21','172.20.14.23','172.20.14.26','172.20.14.27','172.20.14.33')
foreach ($node in $nodes) {
ssh root@$node "curl -fsS http://127.0.0.1:17083/healthz && test -f /data/nginx/html/mobile/index.html"
}
```
此流程不修改 `/data/nginx/conf.d`,不执行 `nginx -s reload`,也不操作 `/data/apps/sgs-map` 或 PM2。只有 Nginx 配置变更才需要先 `nginx -t` 再 reload。
## 线上验证
公网验证:
```powershell
$base = 'https://guide.sznhmuseum.org.cn'
curl.exe -L -s -o NUL -w "home=%{http_code} %{content_type} %{size_download}`n" "$base/"
curl.exe -L -s -o NUL -w "guide_contents=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide-data/guide-contents.json"
curl.exe -L -s -o NUL -w "exhibits=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide-data/exhibits.json"
curl.exe -L -s -o NUL -w "manifest=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide-data/manifest.json"
curl.exe -L -s -o NUL -w "brand_icon=%{http_code} %{content_type} %{size_download}`n" "$base/static/guide/museum-brand-icons.webp"
curl.exe -L -s -o NUL -w "shortcut_icon=%{http_code} %{content_type} %{size_download}`n" "$base/static/icons/poi/shortcut-icons.svg"
curl.exe -L -s -o NUL -w "verify_txt=%{http_code} %{content_type} %{size_download}`n" "$base/gpL0svkeao.txt"
# 缓存/MIME/404assets 应 immutableJSON 应为 application/json缺失 static 不能返回 SPA HTML
curl.exe -sI "$base/assets/<本次入口文件名>.js" | Select-String 'HTTP/|Content-Type|Cache-Control'
curl.exe -sI "$base/static/guide-data/manifest.json" | Select-String 'HTTP/|Content-Type|Cache-Control'
curl.exe -sI "$base/static/nav-assets/<真实资源路径>.glb" | Select-String 'HTTP/|Content-Type|Cache-Control'
curl.exe -sI "$base/static/__not_exists__.json" | Select-String 'HTTP/|Content-Type'
```
预期:
- `/assets/<hash>.js`HTTP `200``Cache-Control``max-age=31536000, immutable`
- manifestHTTP `200``Content-Type: application/json`
- 真实 GLBHTTP `200``Content-Type: model/gltf-binary`
- `/static/__not_exists__.json`HTTP `404`,不得返回 `200 text/html`
入口 JS 验证:
```powershell
$tmp = Join-Path (Get-Location) '.tmp\public-mobile-verify'
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
$htmlPath = Join-Path $tmp 'index.html'
$entryPath = Join-Path $tmp 'entry.js'
curl.exe -L -s "$base/" -o $htmlPath
$html = Get-Content -LiteralPath $htmlPath -Raw
$entry = [regex]::Match($html, 'src="/?([^"]*assets/index-[^"]+\.js)"').Groups[1].Value
curl.exe -L -s "$base/$entry" -o $entryPath
node --check $entryPath
@('1.92.206.90','guide.whaoyue.com','__REPLACE_WITH_TENCENT_MAP_WEB_KEY__') |
Where-Object { (Get-Content -LiteralPath $entryPath -Raw).Contains($_) }
```
预期 `node --check` 通过,旧测试地址和占位符检查无输出。
五台节点一致性验证:
```powershell
$nodes = @('172.20.14.21','172.20.14.23','172.20.14.26','172.20.14.27','172.20.14.33')
$entryPath = "/data/nginx/html/mobile/$entry"
foreach ($node in $nodes) {
ssh -o StrictHostKeyChecking=no root@$node "node --check '$entryPath' >/dev/null && sha256sum '$entryPath' && stat -c %s '$entryPath'"
}
```
五台节点的 size 和 SHA256 应一致。另需逐节点验证 `17083` Nginx 配置与资源行为。以下命令从各节点当前部署目录动态选择真实字体和 hash asset不依赖某次构建的固定文件名
```powershell
$verify17083 = @'
set -euo pipefail
base=/data/nginx/html/mobile
font=$(find "$base/static/Fonts" -type f -print -quit)
asset=$(find "$base/assets" -maxdepth 1 -type f -print -quit)
test -n "$font"
test -n "$asset"
font_uri=${font#"$base"}
asset_uri=${asset#"$base"}
docker exec sgs-nginx nginx -t >/dev/null
curl -fsS http://127.0.0.1:17083/healthz >/dev/null
font_headers=$(curl -sSI "http://127.0.0.1:17083$font_uri")
printf '%s\n' "$font_headers" | grep -Eq '^HTTP/[^ ]+ 200'
printf '%s\n' "$font_headers" | grep -Eiq '^Cache-Control:[[:space:]]*public, max-age=2592000([[:space:]]|$)'
etag=$(printf '%s\n' "$font_headers" | sed -n 's/^[Ee][Tt][Aa][Gg]:[[:space:]]*//p' | tr -d '\r' | head -n 1)
test -n "$etag"
not_modified_headers=$(curl -sSI -H "If-None-Match: $etag" "http://127.0.0.1:17083$font_uri")
printf '%s\n' "$not_modified_headers" | grep -Eq '^HTTP/[^ ]+ 304'
printf '%s\n' "$not_modified_headers" | grep -Eiq '^Cache-Control:[[:space:]]*public, max-age=2592000([[:space:]]|$)'
asset_headers=$(curl -sSI "http://127.0.0.1:17083$asset_uri")
printf '%s\n' "$asset_headers" | grep -Eq '^HTTP/[^ ]+ 200'
printf '%s\n' "$asset_headers" | grep -Eiq '^Cache-Control:.*max-age=31536000.*immutable'
missing_code=$(curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:17083/static/__not_exists__.json)
test "$missing_code" = 404
printf 'PASS font=%s asset=%s etag=%s\n' "$font_uri" "$asset_uri" "$etag"
'@
foreach ($node in $nodes) {
$verify17083 | ssh -o StrictHostKeyChecking=no root@$node 'bash -s'
}
```
预期五台均输出 `PASS`。任一节点缺少字体或 asset 缓存头、字体条件请求不是 `304``304` 未保留缓存头,或缺失静态资源不是 `404`,均应中止发布验收并先处理配置漂移。
注意:`.21` 的整份 `sgs-worker.conf` 哈希可以与其余节点不同,因为它还承载额外的 `17080` CSP 配置;验收目标是五台 `17083` 移动端配置块和响应行为一致。不要用 `.21` 的整份配置覆盖其他节点。
## 回滚
主节点部署脚本会在 `/data/nginx/html/_backups` 下生成备份,例如:
```text
/data/nginx/html/_backups/mobile-before-deploy-YYYYMMDDHHmmss.tar.gz
```
回滚主节点:
```powershell
$backup = '/data/nginx/html/_backups/mobile-before-deploy-YYYYMMDDHHmmss.tar.gz'
$script = @"
set -euo pipefail
target=/data/nginx/html/mobile
backup='$backup'
test -f "`$backup"
find "`$target" -mindepth 1 -maxdepth 1 ! -name 'gpL0svkeao.txt' ! -name '.well-known' -exec rm -rf {} +
tar -xzf "`$backup" -C "`$target"
chmod -R a+rX "`$target"
# reload Nginx conf.d nginx -t reload
"@
$script | ssh -o StrictHostKeyChecking=no root@172.20.14.21 'bash -s'
```
回滚后应使用前述“仅同步 mobile 静态目录”流程把回滚版本同步到四个工作节点,并重新验证五节点入口 JS 的 SHA256。只有已确认 `/data/scripts/sync-frontend-nodes.sh` 内嵌 Nginx 配置与生产一致时,才允许用全量脚本同步。
## 已遇到的问题与处理
### 1. terser 压缩阶段内存不足
现象:
```text
[vite:terser] Worker terminated due to reaching memory limit: JS heap out of memory
```
处理:
- 设置 `NODE_OPTIONS=--max-old-space-size=16384`
- 优先使用 `corepack pnpm build:h5`
- 如仍 OOM使用 `corepack pnpm run build:h5:no-minify`,该命令仍会执行安全后处理和 `node --check`
### 2. 测试服务器 IP 混入正式环境
现象:
```text
Mixed Content: requested insecure image 'http://1.92.206.90:9000/...'
```
处理:
- 正式环境使用 `VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=172.20.14.21`
- 构建后必须扫描 `1.92.206.90``guide.whaoyue.com`
- 静态资源通过同源路径 `/museum-assets/...` 访问。
### 3. 腾讯地图接口 CORS
现象:
```text
Access to XMLHttpRequest at 'https://apis.map.qq.com/...' has been blocked by CORS policy
```
处理:
- 避免前端直接跨域请求不可 CORS 的接口。
- 需要由 Nginx 或后端代理转发第三方地图 API前端访问同源路径。
- 构建产物中的腾讯地图 key 占位符必须替换为正式 key。
### 4. 3D 模型或 JSON 返回 HTML
现象:
```text
SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSON
```
常见原因:
- 静态资源未复制到 `dist/build/h5/static/...`
- Nginx 对静态资源路径错误回退到 `index.html`
- 部署目录缺文件。
处理:
- 构建后必须执行 `scripts/finalize-h5-build.cjs`,它会调用 `copy-h5-nav-assets.cjs`
- 验证 `/static/guide-data/*.json``/static/nav-assets/...``/static/three/...` 返回正确资源。
- Nginx 静态资源路径应优先 `try_files $uri =404`,不要对资源文件回退 SPA。
### 5. 域名验证文件被覆盖
风险:
- 全量替换 `/data/nginx/html/mobile` 时误删 `gpL0svkeao.txt``.well-known`
处理:
- 清理目录时排除 `gpL0svkeao.txt``.well-known`
- 部署后验证 `https://guide.sznhmuseum.org.cn/gpL0svkeao.txt` 返回 200。
### 6. 同步脚本输出 Node engine warning
现象:
```text
npm warn EBADENGINE Unsupported engine
package: 'camera-controls@3.1.2'
required: { node: '>=22.0.0', npm: '>=10.5.1' }
current: { node: 'v20.19.5', npm: '10.8.2' }
```
处理:
- 当前为已知非阻断 warning。
- 只要脚本最终输出 `All frontend worker nodes synced.`,且 PM2 中 `sgs-map``online`,本次 mobile 静态部署可继续验收。
### 7. 同步脚本会重启 sgs-map
现象:
- 部署 mobile 时,脚本仍会同步 `/data/apps/sgs-map` 并重启 PM2 进程 `sgs-map`
处理:
- 当前按脚本现状执行。
- 验收时关注最终 PM2 状态是否 `online`
- 如未来只需同步 mobile可拆分或新增专用同步脚本避免不必要地重启 map 服务。
### 8. 页面返回 200 但浏览器空白
现象:
```text
https://guide.sznhmuseum.org.cn/ HTTP 200
浏览器控制台SyntaxError: missing ) after argument list
```
本次根因:
- 入口 JS 本身语法损坏,不是 Nginx、证书或后端问题。
- 故障版本中,构建后手工用 PowerShell `Get-Content` / `Set-Content` 替换腾讯地图 key错误编码重写了 JS破坏了中文注释或字符串。
- 典型错误位置表现为 uni 运行时代码附近的中文注释被改写,导致 `node --check``missing ) after argument list``Unexpected identifier`
修复:
- 重新生成干净 H5 产物。
- 使用 `scripts/finalize-h5-build.cjs` 按 UTF-8 替换腾讯地图 key。
- 部署前、主节点替换后、线上验证时都执行 `node --check`
预防:
- 不再手工替换构建产物 JS。
- 标准构建必须使用 `corepack pnpm build:h5``corepack pnpm run build:h5:no-minify`
- 部署脚本中保留 `node --check "$target/$entry"`
## 最近正式部署记录
- 2026-07-13多次部署 `frontend-miniapp``172.20.14.21:/data/nginx/html/mobile`,同步到 `172.20.14.23/26/27/33`。确认公网域名 `https://guide.sznhmuseum.org.cn` 正常,静态讲解数据 `guide-contents=720``exhibits=4700``manifest.sourceHost=172.20.14.21`
- 2026-07-13确认 `pnpm build:h5` 在 terser 阶段可能 OOM正式部署可使用 `build:h5:no-minify` 兜底。
- 2026-07-13新增并验证 `static/icons/poi/shortcut-icons.svg` 线上可访问。
- 2026-07-15排查并修复页面 200 但空白问题。根因为 PowerShell 手工替换构建 JS 时破坏 UTF-8已新增 `scripts/finalize-h5-build.cjs` 统一处理资源复制、腾讯地图 key 替换、JS 语法校验和正式环境字符串扫描。
- 2026-07-20拉取远程最新代码前已执行 `git stash push -u -m pre-deploy-prod-local-fixes-20260720232941`,确认“本地补丁已 stash”随后快进到 `735c5cd`,执行 `git stash pop` 恢复正式环境补丁,无冲突。构建入口为 `assets/index-CsFOrYfU.js`,主节点备份为 `/data/nginx/html/_backups/mobile-before-deploy-20260720233343.tar.gz`,同步脚本最终输出 `All frontend worker nodes synced.`
- 2026-07-23移动端 `17083` 目标配置新增 `/assets/` 一年 immutable 缓存、`/static/Fonts/` 30 天缓存、`/static/nav-assets/` MIME/30 天缓存及全部 `/static/` 缺失资源返回 404。普通 H5 发布改为优先仅同步 mobile 静态目录,不再无条件重写 `sgs-worker.conf`、同步地图应用或重启 PM2。
- 2026-07-24逐节点直连复核发现 `.23/.26/.27/.33` 存在 `17083` 缓存配置漂移,仅 `.21` 正确;四台异常节点已分别备份并补齐字体和 hash assets 缓存规则,`nginx -t` 后平滑 reload。修复后字体缓存头、hash asset immutable 缓存头及字体 ETag/304 均为 5/5 一致20 个工作端点和六类对外入口正常,五台 Nginx 容器均未重启。部署验收新增五节点直连缓存头与 ETag/304 检查,避免公网 `least_conn` 抽样漏检单节点漂移。