Files
frontend-miniapp/docs/PRODUCTION_MOBILE_DEPLOYMENT_RUNBOOK.md
cxk 0b3a31f556
Some checks failed
CI / verify (push) Has been cancelled
docs: update production mobile deployment runbook
Sync the CDN origin, five-node cache policy, audio compatibility routing, validation, rollback, and current branch baseline.

Made-with: Proma
2026-07-28 01:00:40 +08:00

802 lines
36 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-28同步 CDN 源站域名、五节点缓存策略、讲解音频兼容路由及 CDN 验收/回滚流程)
## 适用范围
本文档适用于将本地仓库 `E:\深圳自然博物馆\workplace\frontend-miniapp` 的 H5 产物部署到深圳自然博物馆正式环境。
- 正式域名:`https://guide.sznhmuseum.org.cn`
- CDN 源站域名:`https://guide-real.sznhmuseum.org.cn`(只供 CDN 回源和源站排障,不作为游客入口)
- 源站公网 IP`218.17.240.244`
- 主节点:`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-28`.21` 公网入口及五台 `17083` 已完成 CDN 源站、分层缓存、静态资源 404 防 SPA 回退和音频兼容路由适配
- 同步节点:`172.20.14.23``172.20.14.26``172.20.14.27``172.20.14.33`
- 远程仓库:`https://git.whaoyue.com/lyf/frontend-miniapp.git`
- 本次文档同步基线:分支 `codex/import-sgs-frontend-mobile-20260727`,提交 `fc93b8d7d32a208a037be576ce3b9da2db3c1fe3`(仅表示仓库同步基线,不表示该提交已部署到生产)
不要把服务器密码、证书私钥、token、真实后台账号、腾讯地图正式 key 写入仓库文档或提交记录。
## 部署前检查
进入项目目录:
```powershell
cd 'E:\深圳自然博物馆\workplace\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
```
拉取目标发布分支。发布前必须由变更单或版本记录明确分支名,不得默认把 `master` 当作本次目标:
```powershell
$branch = 'codex/import-sgs-frontend-mobile-20260727'
git fetch origin
git switch $branch
git pull --ff-only origin $branch
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 与 CDN 当前生产配置2026-07-28
本节以 `172.20.14.21``docker exec sgs-nginx nginx -T` 的实际生效结果为准。主 Nginx 使用 Docker host 网络,公共域名不直接读取宿主静态目录,而是通过 upstream `sgs_mobile_pool` 转发到五台 `17083` 工作节点:
```text
游客 HTTPS -> guide.sznhmuseum.org.cn CDN -> guide-real.sznhmuseum.org.cn:443
-> 218.17.240.244 / 172.20.14.21 sgs-nginx
-> sgs_mobile_pool -> 172.20.14.21/.23/.26/.27/.33:17083
```
`.21:/data/nginx/conf.d/guide-public.conf` 的 HTTP/HTTPS `server_name` 均包含 `guide.sznhmuseum.org.cn``guide-real.sznhmuseum.org.cn`。通配符证书 `*.sznhmuseum.org.cn` 覆盖两者,有效期至 2027-01-14。80 端口只保留 ACME challenge 直出和 `/_analytics/` 统一 404其余请求 301 到正式 HTTPS 域名。游客 HTTP 到 HTTPS 的跳转仍应在 CDN 边缘启用CDN 使用 HTTPS 回源时,不能依赖源站 80 端口完成游客侧跳转。
CDN 控制台应使用以下回源基线:
- 回源地址、Host 和 SNI`guide-real.sznhmuseum.org.cn`
- 回源协议/端口HTTPS `443`
- 启用源站证书校验;
- 游客域名始终为 `guide.sznhmuseum.org.cn`
- 不把 `guide-real` 写入前端构建变量或对外链接。
当前移动端工作节点 `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;
}
# 实际配置还按可变性拆分以下资源;变更时以 nginx -T 为准:
# - index.html、SPA fallback、/engine/index.html、guide-data、固定名 SDK 入口no-store
# - engine chunks、字体、模型及普通静态文件30 天
# - 缺失静态资源404且不附加长期缓存头
# - 页面路由:仅页面请求允许回退 /index.html回退响应为 no-store
}
```
注意:
- `/assets/``/static/nav-assets/``/static/Fonts/` 和其他 `/static/` 路径必须优先 `try_files $uri =404`,不能把缺失的 JSON、GLB、字体返回为 `index.html`
- `index.html`、SPA fallback、`/engine/index.html``/static/guide-data/` 和固定文件名 SDK 入口必须为 `Cache-Control: no-store`;带 hash 的 `/assets/` 才使用一年不可变缓存。
- engine chunk、字体、模型和普通静态文件使用 30 天缓存;缺失资源返回 404不能带长期缓存头。
- `guide-public.conf` 的 80/443 入口包含 ACME challenge 和 `/_analytics/` 404 规则;不要把公共域名改回单节点静态 root。
- 当前 upstream 使用 `least_conn`,单节点由 `max_fails=3 fail_timeout=10s` 被动摘除。
- 所有 `/_analytics/` 路径必须保持 404不得恢复 Tracker、Matomo 注入、匿名访问通知、`visitor.js``privacy.html`
2026-07-24 逐节点直连 `17083` 复核时发现配置漂移:`.21` 缓存策略正确,`.23/.26/.27/.33` 缺少 `/assets/` 长期 immutable 缓存头和 `/static/Fonts/` 专用规则。四台异常节点已分别备份配置、补齐对应 `17083` location并在 `nginx -t` 通过后平滑 reload。2026-07-27 至 28 日又完成 CDN 分层缓存适配,并同步更新 `.21:/data/scripts/sync-frontend-nodes.sh``write_worker_conf()` 模板。当前五台均满足:
- 字体返回 `Cache-Control: public, max-age=2592000`
- 带 hash 的 assets 返回 `Cache-Control: public, max-age=31536000, immutable`
- HTML、SPA fallback、guide-data 和固定名 SDK 入口返回 `Cache-Control: no-store`
- engine chunk、字体、模型及普通静态文件返回 30 天缓存;
- 缺失静态资源返回 HTTP `404` 且不附加长期缓存头;
- 字体 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-28 同步当前 `17083` CDN 分层缓存策略,但每次使用前仍必须复核脚本与现网配置。**普通移动端静态发布不得在未复核脚本内嵌配置前直接运行该脚本。**
继续使用同步脚本前先检查它是否已经包含本次规则:
```powershell
ssh -o StrictHostKeyChecking=no root@172.20.14.21 'grep -n -E "no-store|static/guide-data|engine/index.html|31536000|2592000|try_files \\$uri =404" /data/scripts/sync-frontend-nodes.sh'
```
如果检查未同时覆盖 no-store、30 天缓存、一年 immutable 和静态 404 规则,应先在维护窗口审核并更新 `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。
## 讲解音频与 CDN 兼容
### 当前链路
讲解音频的正确生产链路为:
```text
/app-api/gis/guide/stop/info 或 /app-api/gis/guide/audio/play-info
-> 返回 https://guide.sznhmuseum.org.cn/minio/museum-assets/...mp3
-> CDN
-> .21 /minio/ 代理
-> sgs_minio / museum-assets 对象
```
`.21:/data/nginx/conf.d/guide-public.conf` 当前还保留以下兼容措施:
- `/app-api/gis/sdk/minio/museum-assets/` 直接代理到 `sgs_minio/museum-assets/`,绕过会返回拒绝访问 HTML 的 Java SDK 媒体代理;
-`/app-api/gis/guide/stop/info``/app-api/gis/guide/audio/play-info` 精确启用 no-store 响应改写,将相对 `/minio/museum-assets/...` 地址改为正式域名绝对 URL
- `/minio/museum-assets/tts-audio/audio/` 保留 Range 请求能力,正常响应应为 `audio/mpeg` 和 HTTP `206`
上述 Nginx 兼容层属于生产止血措施。长期方案应在以下两项中择一实施并完成回归测试:
1. 前端修正 `src/utils/publicUrl.ts`,不再把 `/minio/` 强制转换为 `/app-api/gis/sdk/minio/`
2. Java SDK 媒体代理修复权限、状态码、Content-Type 和 Range 转发。
在长期方案上线、CDN 污染对象完成清理且原 SDK 媒体 URL 验证正常前,不得删除当前兼容路由或 API URL 改写。
### 故障识别
已确认的错误特征为:请求 MP3 时收到 HTTP `206`,但 `Content-Type``text/html;charset=utf-8`、响应约 726 字节,正文提示页面被管理员禁止。该响应来自 Java SDK 媒体代理的拒绝访问页CDN 曾缓存该错误对象。只看 HTTP 状态码会误判成功,音频验收必须同时检查 MIME、Range、长度和文件头。
## 线上验证
公网验证:
```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`
- `/``/index.html``/engine/index.html``/static/guide-data/*.json`HTTP `200``Cache-Control` 为 no-store
- 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` 的整份配置覆盖其他节点。
### 源站、音频与安全负向验证
使用 `--resolve` 绕过 CDN 直连源站,确认 Host/SNI、入口缓存和音频 Range 行为:
```powershell
$origin = '218.17.240.244'
$originHost = 'guide-real.sznhmuseum.org.cn'
$public = 'https://guide.sznhmuseum.org.cn'
$audio = '/minio/museum-assets/tts-audio/audio/2026/07/17/cdb263222e7749009db1768b49477283.mp3'
curl.exe --resolve "${originHost}:443:${origin}" -sI "https://${originHost}/"
curl.exe --resolve "${originHost}:443:${origin}" -sI "https://${originHost}/engine/index.html"
curl.exe --resolve "${originHost}:443:${origin}" -sI "https://${originHost}/static/guide-data/manifest.json"
curl.exe --resolve "${originHost}:443:${origin}" -sS -D - -o NUL -H 'Range: bytes=0-31' "https://${originHost}${audio}"
curl.exe -sS -D - -o NUL -H 'Range: bytes=0-31' "${public}${audio}"
```
音频源站和 CDN 响应均应满足HTTP `206``Content-Type: audio/mpeg`、有效 `Content-Range`,且不得出现约 726 字节的 `text/html`。随后在 390 x 844 移动浏览器中打开英文展项 `865546638960193536`,确认播放器时间从 `0:00` 推进、可完整播放并正常结束。
安全负向检查:
```powershell
@('/_analytics/visitor.js','/_analytics/privacy.html','/_analytics/matomo.js','/_analytics/matomo.php') |
ForEach-Object { curl.exe -s -o NUL -w "%{http_code} $_`n" "$public$_" }
```
四项必须全部返回 HTTP `404`
### CDN 控制台发布后检查
CDN 控制台操作不能由源站部署脚本代替。每次部署涉及 HTML、guide-data、engine 固定入口或音频路由时,至少刷新:
```text
https://guide.sznhmuseum.org.cn/
https://guide.sznhmuseum.org.cn/index.html
https://guide.sznhmuseum.org.cn/engine/index.html
https://guide.sznhmuseum.org.cn/static/guide-data/
```
若处理过讲解音频代理,还需刷新:
```text
https://guide.sznhmuseum.org.cn/app-api/gis/sdk/minio/museum-assets/
```
CDN 规则应保持:
- `/assets/*`:遵循源站一年 immutable
- `/minio/museum-assets/tts-audio/audio/*`30 天;仅在确认文件名内容不可变时才可提升为一年 immutable
- `/app-api/gis/guide/audio/*``/app-api/gis/guide/stop/*`TTL 0
- `/app-api/gis/sdk/minio/museum-assets/*`:污染对象清理和直源验证完成前 TTL 0
- HTML、SPA fallback、`/engine/index.html`、guide-data 和固定名 SDK 入口TTL 0 或严格遵循源站 no-store
- 媒体缓存必须校验成功状态和正确 Content-Type禁止把 `text/html` 当作音频缓存。
同时在 CDN 边缘启用游客 HTTP 到 HTTPS 的 301。控制台变更后重新执行公网与 `--resolve` 对照检查,以区分 CDN 和源站响应。
## 回滚
主节点部署脚本会在 `/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 配置与生产一致时,才允许用全量脚本同步。
如需回滚 CDN/Nginx 适配,使用对应变更备份,不能用旧整目录覆盖现网:
- CDN 源站集群适配:`/data/nginx/_backups/cdn-origin-cluster-20260727223921`
- SDK MinIO 音频绕过:`/data/nginx/_backups/audio-sdk-minio-bypass-20260727232518`
- 音频 API URL 改写:`/data/nginx/_backups/audio-api-url-rewrite-20260727233006`
回滚顺序固定为:确认影响范围,备份当前配置,只恢复对应文件或 location逐节点执行 `docker exec sgs-nginx nginx -t`,平滑 reload再验证五节点 `/healthz`、正式域名、源站域名、音频 Range 和 `/_analytics/` 404。若 CDN 已缓存回滚前对象,还需在控制台刷新对应 URL仅回滚源站不会自动清除 CDN 对象。
## 已遇到的问题与处理
### 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. CDN 后讲解音频返回 HTML
现象:
```text
HTTP 206
Content-Type: text/html;charset=utf-8
Content-Length: 726
```
根因:
- 前端把 `/minio/...mp3` 转换成 `/app-api/gis/sdk/minio/...mp3`
- Java SDK 媒体代理返回拒绝访问 HTML但状态仍为 200
- CDN 缓存后以 206 响应 Range 请求,造成播放器无法解码。
处理:
- `.21` 兼容路由将 `/app-api/gis/sdk/minio/museum-assets/` 直接代理到 MinIO
- 两个讲解 API 的响应 URL 改写为正式域名 `/minio/museum-assets/...`
- CDN 清除污染前SDK media 路径 TTL 设为 0
- 验收同时检查状态、`audio/mpeg``Content-Range`、文件头和浏览器播放进度。
### 9. 页面返回 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` 抽样漏检单节点漂移。
- 2026-07-27从分支 `codex/import-sgs-frontend-mobile-20260727` 部署提交 `f1047414bbf00a254e1c0cf99755556e45a18e6f`,生产入口 `assets/index-DvpqYP-p.js`SHA256 `b9f099e2d30d71ca691cc64f20ca6808af834b12dce1c9af8838a6aef1eb893d`;五节点一致,`/engine/` 运行时和移动端导览/讲解流程验证通过。
- 2026-07-27 至 28 日:新增 `guide-real.sznhmuseum.org.cn` CDN 源站域名并完成五节点 `17083` 分层缓存适配;同步脚本模板已更新。随后修复 CDN 后英文讲解音频失败:绕过错误 Java SDK 媒体代理,并把讲解 API 的 `/minio/` URL 改写为正式域名绝对路径。五组语言/声线音频均验证为 HTTP 206、`audio/mpeg` 和正确 MPEG 文件头,展项 `865546638960193536` 英文音频可完整播放至 48.96 秒。CDN 控制台刷新、边缘 HTTP 到 HTTPS 和 Host/SNI 规则仍需按本文清单人工维护。
- 2026-07-28目标分支已快进同步到 `fc93b8d7d32a208a037be576ce3b9da2db3c1fe3`,包含讲解列表、宿主环境判断、展厅卡片图片及相关测试更新。本条只记录文档编制时的代码基线;生产仍以最近一次明确部署记录 `f1047414bbf00a254e1c0cf99755556e45a18e6f` 为准,后续上线 `fc93b8d` 时必须另行补充构建产物、五节点哈希和验收记录。