# 讲解业务静态数据包更新操作文档 本文固化 `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` 的讲解树使用后端接口: ```text GET /guide/exhibits/tree GET /guide/exhibits/tree/{nodeId}/children ``` 后端实现位于: ```text 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`。 当前用于回归的人类厅示例: ```text 人类厅:5 个业务单元 · 38 个讲解点 ``` 其中: - 第一单元:什么是灵长类,27 个讲解点 - 第二单元:猿与猴,5 个讲解点 - 第三单元:丛林远足,6 个讲解点 - 第四单元:与巨兽同行,0 个讲解点 - 第五单元:从燧石到硅基,0 个讲解点 ## 静态包文件边界 讲解首页、讲解展厅、业务单元、讲解点、详情、位置预览和媒体可用状态由以下文件共同生成: - `manifest.json` - `halls.json` - `outlines.json` - `guide-stops.json` - `guide-contents.json` - `poi-bridge.json` 同一目录下的 `exhibits.json`、`pois.json`、`indexes.json` 也应随导出一并保持一致。虽然它们不都是讲解主列表的直接输入,但会参与详情兜底、内容关联、POI 桥接和索引一致性。不要只覆盖单个 JSON,避免 manifest、索引和桥接统计不一致。 注意: - `outlines.json` 是完整后台讲解树,不是用户端“可播放讲解点”的子集。 - `indexes.outlinesByHallId` 包含展厅下所有后代 outline,用于索引和详情关联;用户端业务单元数量不要直接用该数组长度,应由 adapter 取顶层 outline 计算。 ## 操作前检查 确认依赖可用: ```powershell 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 包存在: ```powershell 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`。 ## 导出静态包 优先直接使用后端原配置导出。脚本会读取配置中的达梦连接信息,不需要把含密码配置复制到项目目录: ```powershell $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 ``` 成功输出示例: ```text 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 密钥路径: ```text E:\MyWork\深圳自然馆\服务器信息\KeyPair-c006.pem ``` 确认 SSH 能登录服务器: ```powershell ssh -i 'E:\MyWork\深圳自然馆\服务器信息\KeyPair-c006.pem' -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 root@1.92.206.90 hostname ``` 确认本地转发端口空闲: ```powershell Get-NetTCPConnection -LocalPort 15237 -ErrorAction SilentlyContinue | Select-Object LocalAddress,LocalPort,State,OwningProcess ``` 启动本地端口转发: ```powershell $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 ``` 确认隧道可用: ```powershell Test-NetConnection 127.0.0.1 -Port 15237 | Select-Object ComputerName,RemotePort,TcpTestSucceeded | Format-List ``` 创建临时配置。临时配置只把 JDBC 地址从服务器地址替换为本地隧道地址,不修改原后端配置文件,也不要写入项目目录: ```powershell $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 ``` 导出到项目静态包: ```powershell 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` 和临时配置路径。提交前应改回真实来源: ```json "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` 校验: ```powershell 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 数量检查: ```powershell 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 个有效讲解点: ```powershell $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 ``` 最小代码边界校验: ```powershell pnpm type-check ``` 如果本次仅更新静态 JSON,一般不需要运行 `pnpm build:h5`,因为它会写入 `dist`。若同时改了读取逻辑、adapter、repository 或 UI,再补充: ```powershell pnpm lint pnpm build:h5 ``` ## 关闭隧道 仅在使用 SSH 隧道兜底时需要执行。用启动隧道时返回的进程 ID 关闭: ```powershell Stop-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 全量解析通过。 - `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 密钥、微信密钥等敏感配置。