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

27 KiB
Raw Blame History

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-nginxnginx/1.30.3Docker 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.23172.20.14.26172.20.14.27172.20.14.33
  • 远程仓库:http://192.168.0.93:3333/lyf/frontend-miniapp.git

不要把服务器密码、证书私钥、token、真实后台账号、腾讯地图正式 key 写入仓库文档或提交记录。

部署前检查

进入项目目录:

cd 'F:\深圳自然博物馆\frontend-miniapp'

检查分支、远程仓库和本地改动:

git status --short --branch --untracked-files=all
git log -1 --pretty=format:'%h %s'
git remote -v

正式环境当前依赖一组本地生产补丁,拉取远程代码前必须保护。常见补丁文件包括:

.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.jsonsourceHost 应为 172.20.14.21
  • 腾讯地图正式 key 只通过环境变量 VITE_TENCENT_MAP_KEY 注入,不能写入文档或提交记录。

拉取远程最新代码

先临时保存本地生产补丁:

$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

Saved working directory and index state On master: pre-deploy-prod-local-fixes-YYYYMMDDHHmmss
STASHED=pre-deploy-prod-local-fixes-YYYYMMDDHHmmss

拉取远程最新代码:

git fetch origin
git pull --ff-only origin master
git log -1 --pretty=format:'%h %s'

恢复生产补丁:

$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.productionstatic/guide-data/manifest.jsonsrc/utils/publicUrl.ts,确认正式环境仍指向 172.20.14.21https://guide.sznhmuseum.org.cn

Nginx 当前生产配置2026-07-24

本节以 172.20.14.21docker exec sgs-nginx nginx -T 的实际生效结果为准。主 Nginx 使用 Docker host 网络,公共域名不直接读取宿主静态目录,而是通过 upstream sgs_mobile_pool 转发到五台 17083 工作节点:

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 的关键规则如下:

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

$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 验证。

本地构建

安装依赖并做类型检查:

corepack pnpm install --frozen-lockfile
$env:NODE_OPTIONS='--max-old-space-size=16384'
corepack pnpm type-check

设置正式构建变量。腾讯地图 key 只通过环境变量注入,不写入仓库:

$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 构建:

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-assetsstatic/guide-datastatic/guidestatic/iconsstatic/explainstatic/sgs-map-sdkstatic/three 和字体资源。
  • 使用 Node 按 UTF-8 安全替换构建产物里的 __REPLACE_WITH_TENCENT_MAP_WEB_KEY__
  • 对全部构建 JS 执行 node --check,防止语法错误产物上线。
  • 扫描并阻断 1.92.206.90guide.whaoyue.com、腾讯地图 key 占位符等不应进入正式环境的字符串。
  • 校验核心静态 JSON 和关键图标文件存在。

如 terser 压缩阶段出现内存不足,可使用不压缩构建:

corepack pnpm run build:h5:no-minify

严禁再用 PowerShell Get-Content / Set-Content 手工替换构建后的 JS。PowerShell 默认编码容易破坏 UTF-8 中文注释或字符串,曾导致线上入口 JS 语法错误、页面空白。

构建产物检查

构建脚本会自动输出类似:

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

$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)

预期无错误输出。

打包与上传

$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

执行:

$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 配置已复核时使用)

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/.3317083 缓存配置漂移,当前五台生效配置均包含 /assets/ 一年 immutable 缓存、/static/Fonts/ 30 天缓存以及静态资源 404 规则;但同步脚本文件本身仍需要单独复核,不能用脚本内容推断当前生产配置。普通移动端静态发布不得在未复核脚本内嵌配置前直接运行该脚本。

继续使用同步脚本前先检查它是否已经包含本次规则:

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/Fonts31536000,应先在维护窗口审核并更新 write_worker_conf(),再执行 bash -n /data/scripts/sync-frontend-nodes.sh、逐节点 nginx -t/healthz 验证。不要用 .21 的整份 sgs-worker.conf 覆盖其他节点,因为 .2117080 还包含本节点专用 CSP 配置;只同步审核后的 17083 移动端配置块。

如果本次仅发布 H5 静态文件、没有修改 Nginx 配置,推荐采用“仅同步 /data/nginx/html/mobile”的流程,不要触发同步脚本中的 write_worker_conf()/data/apps/sgs-map 同步和 PM2 重启。静态文件替换完成后不需要 Nginx reload只有配置变更才执行 nginx -t 后 reload。

若必须按现有脚本执行,必须确认最终输出:

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

$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'
}

同步完成后逐节点检查:

$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。

线上验证

公网验证:

$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>.jsHTTP 200Cache-Controlmax-age=31536000, immutable
  • manifestHTTP 200Content-Type: application/json
  • 真实 GLBHTTP 200Content-Type: model/gltf-binary
  • /static/__not_exists__.jsonHTTP 404,不得返回 200 text/html

入口 JS 验证:

$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 通过,旧测试地址和占位符检查无输出。

五台节点一致性验证:

$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不依赖某次构建的固定文件名

$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 缓存头、字体条件请求不是 304304 未保留缓存头,或缺失静态资源不是 404,均应中止发布验收并先处理配置漂移。

注意:.21 的整份 sgs-worker.conf 哈希可以与其余节点不同,因为它还承载额外的 17080 CSP 配置;验收目标是五台 17083 移动端配置块和响应行为一致。不要用 .21 的整份配置覆盖其他节点。

回滚

主节点部署脚本会在 /data/nginx/html/_backups 下生成备份,例如:

/data/nginx/html/_backups/mobile-before-deploy-YYYYMMDDHHmmss.tar.gz

回滚主节点:

$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 压缩阶段内存不足

现象:

[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 混入正式环境

现象:

Mixed Content: requested insecure image 'http://1.92.206.90:9000/...'

处理:

  • 正式环境使用 VITE_PUBLIC_SAME_ORIGIN_ASSET_HOST=172.20.14.21
  • 构建后必须扫描 1.92.206.90guide.whaoyue.com
  • 静态资源通过同源路径 /museum-assets/... 访问。

3. 腾讯地图接口 CORS

现象:

Access to XMLHttpRequest at 'https://apis.map.qq.com/...' has been blocked by CORS policy

处理:

  • 避免前端直接跨域请求不可 CORS 的接口。
  • 需要由 Nginx 或后端代理转发第三方地图 API前端访问同源路径。
  • 构建产物中的腾讯地图 key 占位符必须替换为正式 key。

4. 3D 模型或 JSON 返回 HTML

现象:

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

现象:

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-maponline,本次 mobile 静态部署可继续验收。

7. 同步脚本会重启 sgs-map

现象:

  • 部署 mobile 时,脚本仍会同步 /data/apps/sgs-map 并重启 PM2 进程 sgs-map

处理:

  • 当前按脚本现状执行。
  • 验收时关注最终 PM2 状态是否 online
  • 如未来只需同步 mobile可拆分或新增专用同步脚本避免不必要地重启 map 服务。

8. 页面返回 200 但浏览器空白

现象:

https://guide.sznhmuseum.org.cn/ HTTP 200
浏览器控制台SyntaxError: missing ) after argument list

本次根因:

  • 入口 JS 本身语法损坏,不是 Nginx、证书或后端问题。
  • 故障版本中,构建后手工用 PowerShell Get-Content / Set-Content 替换腾讯地图 key错误编码重写了 JS破坏了中文注释或字符串。
  • 典型错误位置表现为 uni 运行时代码附近的中文注释被改写,导致 node --checkmissing ) after argument listUnexpected identifier

修复:

  • 重新生成干净 H5 产物。
  • 使用 scripts/finalize-h5-build.cjs 按 UTF-8 替换腾讯地图 key。
  • 部署前、主节点替换后、线上验证时都执行 node --check

预防:

  • 不再手工替换构建产物 JS。
  • 标准构建必须使用 corepack pnpm build:h5corepack pnpm run build:h5:no-minify
  • 部署脚本中保留 node --check "$target/$entry"

最近正式部署记录

  • 2026-07-13多次部署 frontend-miniapp172.20.14.21:/data/nginx/html/mobile,同步到 172.20.14.23/26/27/33。确认公网域名 https://guide.sznhmuseum.org.cn 正常,静态讲解数据 guide-contents=720exhibits=4700manifest.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 抽样漏检单节点漂移。