- 刷新讲解静态数据包并保留完整 outline 树 - 按后台顶层业务单元口径归并讲解点统计 - 更新讲解静态数据刷新操作文档 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
15 KiB
讲解业务静态数据包更新操作文档
本文固化 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.tssrc/data/providers/staticMuseumContentProvider.tssrc/data/providers/staticGuideDataProvider.tssrc/data/providers/backendExplainContentProvider.tssrc/data/adapters/guideDataAdapter.tssrc/repositories/ExplainRepository.tssrc/repositories/MediaRepository.ts
音频播放解析还有独立后端 API:
src/repositories/AudioPlayInfoRepository.tssrc/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.jsonhalls.jsonoutlines.jsonguide-stops.jsonguide-contents.jsonpoi-bridge.json
同一目录下的 exhibits.json、pois.json、indexes.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 Trueyaml 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:8outlines:250guide-stops:391exhibits:4700guide-contents:717pois:2244sgsPoiBridgeCount:876sgsPoiCandidateCount:13- 有音频 URL 的讲解内容:
382
业务单元口径修正:
outlines.json恢复完整SGS_EXHIBIT_OUTLINE树,不再裁剪到 28 条。- 前端 adapter 改为按展厅下顶层 outline 生成业务单元,空业务单元也保留。
- 人类厅回归结果:
5 个业务单元 · 38 个讲解点。
更新文件:
scripts/export-guide-static-data.pysrc/data/adapters/guideDataAdapter.tsstatic/guide-data/README.mdstatic/guide-data/exhibits.jsonstatic/guide-data/guide-contents.jsonstatic/guide-data/guide-stops.jsonstatic/guide-data/halls.jsonstatic/guide-data/indexes.jsonstatic/guide-data/manifest.jsonstatic/guide-data/outlines.jsonstatic/guide-data/poi-bridge.jsonstatic/guide-data/pois.json
已完成校验:
- JSON 全量解析通过。
rowCount与rows.length一致。- 人类厅业务单元回归校验通过。
pnpm type-check通过。pnpm lint通过。pnpm build:h5通过。
常见风险
- 不要直接读取或改写
src/assets/data,该目录是历史 demo 数据。 - 不要把
remote或mock模式误判为当前默认生产数据源;默认是static。 - 不要只更新
guide-contents.json,否则manifest、indexes、poi-bridge可能与内容数据不一致。 - 不要把本地隧道地址提交到
manifest.json。 - 不要把
outlines.json过滤成只有讲解点引用的子集;后台业务单元需要完整 outline 树和顶层 outline 口径。 - 不要用
indexes.outlinesByHallId.length当作用户端业务单元数量,该索引包含所有后代 outline。 - 不要因为
poi-bridge.json增加了位置关联就宣称已支持正式路线规划;当前用户侧仍应按位置预览能力表述。 - 不要在文档、日志或提交中复制数据库密码、MinIO 密钥、微信密钥等敏感配置。