Files
frontend-miniapp/docs/Data/explain-static-data-refresh-runbook.md
lyf c146beba9e 对齐讲解业务单元静态数据口径
- 刷新讲解静态数据包并保留完整 outline 树
- 按后台顶层业务单元口径归并讲解点统计
- 更新讲解静态数据刷新操作文档

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 02:05:02 +08:00

15 KiB
Raw Permalink Blame History

讲解业务静态数据包更新操作文档

本文固化 frontend-miniapp 项目中“讲解”业务静态数据包的检查与更新流程。操作范围覆盖讲解展厅、后台讲解树业务单元、讲解点、讲解内容、音频内容元数据以及讲解到位置预览的关联数据不扩展到馆内导览模型、路线图、Tencent 地图或小程序专项构建。

适用范围

  • 项目目录:E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp
  • 静态包目录:static/guide-data
  • 导出脚本:scripts/export-guide-static-data.py
  • 达梦数据库服务器:1.92.206.90
  • 后端数据库配置来源:E:\MyWork\深圳国际艺术馆\智慧导览\smart-navigation-system\yudao-server\src\main\resources\application-local.yaml
  • 当前 nav-assets 包:static/nav-assets/codex_nav_20260621_175310_museum_default_centerline_finalxy

不要在文档、提交信息或终端输出中记录数据库密码。脚本从后端本地配置读取连接信息,操作文档只记录配置文件路径。

讲解业务数据源结论

讲解内容数据源由 VITE_GUIDE_CONTENT_SOURCE_MODE 控制,共 3 个模式:

  • static:默认模式,读取 static/guide-data 静态包。
  • remote:后端接口模式,BackendExplainContentProvider 加载后端搜索、详情、展厅和讲解点接口,并保留静态兜底。
  • mock:开发环境显式 mock仅在 import.meta.env.DEV 下启用。

代码入口:

  • src/config/dataSource.ts
  • src/data/providers/staticMuseumContentProvider.ts
  • src/data/providers/staticGuideDataProvider.ts
  • src/data/providers/backendExplainContentProvider.ts
  • src/data/adapters/guideDataAdapter.ts
  • src/repositories/ExplainRepository.ts
  • src/repositories/MediaRepository.ts

音频播放解析还有独立后端 API

  • src/repositories/AudioPlayInfoRepository.ts
  • src/repositories/PublishedExhibitAudioRepository.ts

该音频 API 用于播放地址、讲解词正文和目标解析,不等同于讲解列表/内容静态包数据源。

业务单元口径

后台管理端 http://1.92.206.90:3001/guide/exhibits 的讲解树使用后端接口:

GET /guide/exhibits/tree
GET /guide/exhibits/tree/{nodeId}/children

后端实现位于:

E:\MyWork\深圳国际艺术馆\智慧导览\smart-navigation-system\yudao-module-gis\src\main\java\cn\iocoder\yudao\module\gis\controller\admin\guide\V2ExhibitTreeController.java

该接口的业务单元来源是 NATURE_SGS.SGS_EXHIBIT_OUTLINE,不是 SGS_EXHIBITION_UNIT。后台树的关键口径:

  • 展厅节点来自 SGS_EXHIBITION_HALL
  • 业务单元来自展厅下的顶层 SGS_EXHIBIT_OUTLINE,即 outline.parentId == hall.id
  • 统计讲解点时,讲解点可归并到所属顶层业务单元;如果顶层业务单元下没有有效讲解点,也仍然显示为业务单元。
  • 因此 outlines.json 必须保留完整 SGS_EXHIBIT_OUTLINE 树,不能再过滤为“有讲解点引用的 outline 子集”。

前端静态 adapter 的对应口径:

  • businessUnitCount:展厅下顶层、未删除的 outline 数量。
  • guideStopCount:这些顶层 outline 及其后代下未删除讲解点数量。
  • 空业务单元保留展示,guideStopCount = 0

当前用于回归的人类厅示例:

人类厅5 个业务单元 · 38 个讲解点

其中:

  • 第一单元什么是灵长类27 个讲解点
  • 第二单元猿与猴5 个讲解点
  • 第三单元丛林远足6 个讲解点
  • 第四单元与巨兽同行0 个讲解点
  • 第五单元从燧石到硅基0 个讲解点

静态包文件边界

讲解首页、讲解展厅、业务单元、讲解点、详情、位置预览和媒体可用状态由以下文件共同生成:

  • manifest.json
  • halls.json
  • outlines.json
  • guide-stops.json
  • guide-contents.json
  • poi-bridge.json

同一目录下的 exhibits.jsonpois.jsonindexes.json 也应随导出一并保持一致。虽然它们不都是讲解主列表的直接输入但会参与详情兜底、内容关联、POI 桥接和索引一致性。不要只覆盖单个 JSON避免 manifest、索引和桥接统计不一致。

注意:

  • outlines.json 是完整后台讲解树,不是用户端“可播放讲解点”的子集。
  • indexes.outlinesByHallId 包含展厅下所有后代 outline用于索引和详情关联用户端业务单元数量不要直接用该数组长度应由 adapter 取顶层 outline 计算。

操作前检查

确认依赖可用:

python -c "import importlib.util; print('pyodbc', bool(importlib.util.find_spec('pyodbc'))); print('yaml', bool(importlib.util.find_spec('yaml')))"
Get-OdbcDriver | Where-Object { $_.Name -like '*DM*' -or $_.Name -like '*Dameng*' } | Select-Object Name,Platform | Format-List

预期:

  • pyodbc True
  • yaml True
  • 存在 DM8 ODBC DRIVER

确认 nav-assets 包存在:

Test-Path 'static\nav-assets\codex_nav_20260621_175310_museum_default_centerline_finalxy\app_nav_manifest.json'
Test-Path 'static\nav-assets\codex_nav_20260621_175310_museum_default_centerline_finalxy\data\poi_all.json'

预期均为 True

导出静态包

优先直接使用后端原配置导出。脚本会读取配置中的达梦连接信息,不需要把含密码配置复制到项目目录:

$repo = 'E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp'
$sourceConfig = 'E:\MyWork\深圳国际艺术馆\智慧导览\smart-navigation-system\yudao-server\src\main\resources\application-local.yaml'
$navAssets = Join-Path $repo 'static\nav-assets\codex_nav_20260621_175310_museum_default_centerline_finalxy'

Set-Location $repo
python 'scripts\export-guide-static-data.py' `
  --config $sourceConfig `
  --output 'static\guide-data' `
  --nav-assets $navAssets

成功输出示例:

Exported sgs-guide-static/v1 to ...\static\guide-data
Counts: {"exhibits": 4700, "guide-contents": 717, "guide-stops": 391, "halls": 8, "outlines": 250, "pois": 2244}
Bridge: {"hallBridgeCount": 8, "navPoiCount": 298, "sgsPoiBridgeCount": 876, "sgsPoiCandidateCount": 13}

异常时不要手动编辑单个 JSON先排查数据库连接、ODBC 驱动、schema 或 nav-assets 路径。

SSH 隧道兜底方案

如果当前网络不能直连 1.92.206.90:5237,再使用 SSH 隧道。SSH 密钥路径:

E:\MyWork\深圳自然馆\服务器信息\KeyPair-c006.pem

确认 SSH 能登录服务器:

ssh -i 'E:\MyWork\深圳自然馆\服务器信息\KeyPair-c006.pem' -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 root@1.92.206.90 hostname

确认本地转发端口空闲:

Get-NetTCPConnection -LocalPort 15237 -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,State,OwningProcess

启动本地端口转发:

$sshArgs = @(
  '-i', 'E:\MyWork\深圳自然馆\服务器信息\KeyPair-c006.pem',
  '-o', 'BatchMode=yes',
  '-o', 'ExitOnForwardFailure=yes',
  '-o', 'StrictHostKeyChecking=accept-new',
  '-N',
  '-L', '127.0.0.1:15237:127.0.0.1:5237',
  'root@1.92.206.90'
)
$tunnel = Start-Process -FilePath 'ssh' -ArgumentList $sshArgs -WindowStyle Hidden -PassThru
Start-Sleep -Seconds 3
$tunnel.Id

确认隧道可用:

Test-NetConnection 127.0.0.1 -Port 15237 | Select-Object ComputerName,RemotePort,TcpTestSucceeded | Format-List

创建临时配置。临时配置只把 JDBC 地址从服务器地址替换为本地隧道地址,不修改原后端配置文件,也不要写入项目目录:

$repo = 'E:\MyWork\深圳国际艺术馆\museum-guide\museum-guide-v4.0\frontend-miniapp'
$sourceConfig = 'E:\MyWork\深圳国际艺术馆\智慧导览\smart-navigation-system\yudao-server\src\main\resources\application-local.yaml'
$tmpRoot = Join-Path $env:TEMP ('sgs-explain-export-' + (Get-Date -Format 'yyyyMMddHHmmss'))
New-Item -ItemType Directory -Path $tmpRoot | Out-Null
$tmpConfig = Join-Path $tmpRoot 'application-local-tunnel.yaml'
(Get-Content -Raw -LiteralPath $sourceConfig).Replace('jdbc:dm://1.92.206.90:5237','jdbc:dm://127.0.0.1:15237') | Set-Content -LiteralPath $tmpConfig -Encoding UTF8

导出到项目静态包:

Set-Location $repo
python 'scripts\export-guide-static-data.py' `
  --config $tmpConfig `
  --output 'static\guide-data' `
  --nav-assets 'static\nav-assets\codex_nav_20260621_175310_museum_default_centerline_finalxy'

导出通过本地隧道完成时,脚本会在 manifest.json 记录 127.0.0.1:15237 和临时配置路径。提交前应改回真实来源:

"source": {
  "type": "dameng",
  "host": "1.92.206.90",
  "port": "5237",
  "schema": "NATURE_SGS",
  "configPath": "E:\\MyWork\\深圳国际艺术馆\\智慧导览\\smart-navigation-system\\yudao-server\\src\\main\\resources\\application-local.yaml"
}

校验

JSON 与 rowCount 校验:

node -e "const fs=require('fs'); const dir='static/guide-data'; const m=JSON.parse(fs.readFileSync(dir+'/manifest.json','utf8')); for (const f of Object.values(m.files)) { const j=JSON.parse(fs.readFileSync(dir+'/'+f,'utf8')); if (j.rows && typeof j.rowCount==='number' && j.rows.length!==j.rowCount) throw new Error(f+' rowCount mismatch'); } console.log(JSON.stringify({generatedAt:m.generatedAt,counts:m.counts,bridgeStats:m.bridgeStats,source:m.source},null,2));"

讲解内容和音频 URL 数量检查:

node -e "const fs=require('fs'); const m=JSON.parse(fs.readFileSync('static/guide-data/manifest.json','utf8')); const g=JSON.parse(fs.readFileSync('static/guide-data/guide-contents.json','utf8')); const playable=g.rows.filter(r=>r.standardAudioUrl||r.extendedAudioUrl||r.audioUrl||r.standardAudioUrlEn||r.extendedAudioUrlEn).length; console.log(JSON.stringify({generatedAt:m.generatedAt,counts:m.counts,bridgeStats:m.bridgeStats,guideContentsRowCount:g.rowCount,guideContentsWithAudioUrl:playable},null,2));"

业务单元口径回归校验。人类厅应为 5 个业务单元、38 个有效讲解点:

$script = @'
const fs = require('fs')
const dir = 'static/guide-data'
const halls = JSON.parse(fs.readFileSync(dir + '/halls.json', 'utf8')).rows
const outlines = JSON.parse(fs.readFileSync(dir + '/outlines.json', 'utf8')).rows
const stops = JSON.parse(fs.readFileSync(dir + '/guide-stops.json', 'utf8')).rows
const human = halls.find((h) => h.name === '人类厅')
const hallIds = new Set(halls.filter((h) => !(h.deleted === 1 || h.deleted === true || h.deleted === '1')).map((h) => String(h.id)))
const outlineById = new Map(outlines.filter((o) => !(o.deleted === 1 || o.deleted === true || o.deleted === '1')).map((o) => [String(o.id), o]))
const topOutlines = outlines.filter((o) => String(o.parentId) === String(human.id) && !(o.deleted === 1 || o.deleted === true || o.deleted === '1'))
const topForStop = (stop) => {
  let current = outlineById.get(String(stop.outlineId))
  for (let guard = 0; current && guard < 16; guard++) {
    const parentId = String(current.parentId)
    if (hallIds.has(parentId)) return current
    current = outlineById.get(parentId)
  }
}
const visibleStops = stops.filter((s) => !(s.deleted === 1 || s.deleted === true || s.deleted === '1'))
const units = topOutlines.map((u) => ({ id: String(u.id), name: u.name, guideStopCount: visibleStops.filter((s) => String(topForStop(s)?.id) === String(u.id)).length }))
if (units.length !== 5) throw new Error('人类厅业务单元数量不为 5: ' + units.length)
const guideStopCount = units.reduce((sum, u) => sum + u.guideStopCount, 0)
if (guideStopCount !== 38) throw new Error('人类厅讲解点数量不为 38: ' + guideStopCount)
console.log(JSON.stringify({ hall: '人类厅', businessUnitCount: units.length, guideStopCount, units }, null, 2))
'@
$script | node

最小代码边界校验:

pnpm type-check

如果本次仅更新静态 JSON一般不需要运行 pnpm build:h5,因为它会写入 dist。若同时改了读取逻辑、adapter、repository 或 UI再补充

pnpm lint
pnpm build:h5

关闭隧道

仅在使用 SSH 隧道兜底时需要执行。用启动隧道时返回的进程 ID 关闭:

Stop-Process -Id <tunnel-process-id> -ErrorAction SilentlyContinue
Start-Sleep -Seconds 1
Get-NetTCPConnection -LocalPort 15237 -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,State,OwningProcess

预期:没有残留监听。

2026-07-02 最新更新记录

本次使用后端原始配置连接 1.92.206.90 达梦数据库,导出并更新 static/guide-data

更新后关键统计:

  • halls: 8
  • outlines: 250
  • guide-stops: 391
  • exhibits: 4700
  • guide-contents: 717
  • pois: 2244
  • sgsPoiBridgeCount: 876
  • sgsPoiCandidateCount: 13
  • 有音频 URL 的讲解内容:382

业务单元口径修正:

  • outlines.json 恢复完整 SGS_EXHIBIT_OUTLINE 树,不再裁剪到 28 条。
  • 前端 adapter 改为按展厅下顶层 outline 生成业务单元,空业务单元也保留。
  • 人类厅回归结果:5 个业务单元 · 38 个讲解点

更新文件:

  • scripts/export-guide-static-data.py
  • src/data/adapters/guideDataAdapter.ts
  • static/guide-data/README.md
  • static/guide-data/exhibits.json
  • static/guide-data/guide-contents.json
  • static/guide-data/guide-stops.json
  • static/guide-data/halls.json
  • static/guide-data/indexes.json
  • static/guide-data/manifest.json
  • static/guide-data/outlines.json
  • static/guide-data/poi-bridge.json
  • static/guide-data/pois.json

已完成校验:

  • JSON 全量解析通过。
  • rowCountrows.length 一致。
  • 人类厅业务单元回归校验通过。
  • pnpm type-check 通过。
  • pnpm lint 通过。
  • pnpm build:h5 通过。

常见风险

  • 不要直接读取或改写 src/assets/data,该目录是历史 demo 数据。
  • 不要把 remotemock 模式误判为当前默认生产数据源;默认是 static
  • 不要只更新 guide-contents.json,否则 manifestindexespoi-bridge 可能与内容数据不一致。
  • 不要把本地隧道地址提交到 manifest.json
  • 不要把 outlines.json 过滤成只有讲解点引用的子集;后台业务单元需要完整 outline 树和顶层 outline 口径。
  • 不要用 indexes.outlinesByHallId.length 当作用户端业务单元数量,该索引包含所有后代 outline。
  • 不要因为 poi-bridge.json 增加了位置关联就宣称已支持正式路线规划;当前用户侧仍应按位置预览能力表述。
  • 不要在文档、日志或提交中复制数据库密码、MinIO 密钥、微信密钥等敏感配置。