import fs from 'node:fs' import path from 'node:path' const dir='reports/guide-formal-20260910' const esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])) // 报告只保留结论,不携带完整接口载荷、内部存储地址或上传凭据。 for(const file of ['create.json','upload.json','publish.json']){ const data=JSON.parse(fs.readFileSync(path.join(dir,file),'utf8')) for(const check of data.checks||[])if(check.detail?.startsWith('{'))check.detail='真实 HTTP 请求成功;响应内容已脱敏。' fs.writeFileSync(path.join(dir,file),JSON.stringify(data,null,2)) } const suites=[['create.json','正式工程新建'],['upload.json','模型资源上传与恢复'],['scenes.json','固定场景版本与修订'],['publish.json','正式发布与交付物'],['review.json','教学阅读及真实 HTTP 查询'],['permissions.json','权限、私人记录与反馈'],['final.json','阅读、笔记、进度及 HTTP 写入'],['template.json','正式模板新建'],['media.json','图片、视频及截图引用'],['offline-browser.json','正式离线包运行']] const results=suites.map(([file,title])=>({file,title,...JSON.parse(fs.readFileSync(path.join(dir,file),'utf8'))})) const checks=results.flatMap(r=>r.checks),passed=checks.filter(c=>c.pass).length const release=JSON.parse(fs.readFileSync(path.join(dir,'release.json'),'utf8')) const offline=JSON.parse(fs.readFileSync(path.join(dir,'offline.json'),'utf8')) const shots=[ ['01-formal-created.png','正式指导书新建,写入内容服务'], ['02-formal-assets.png','狐狸、牛奶卡车 GLB 上传至正式资源存储'], ['03-formal-fox.png','刷新后狐狸模型恢复显示'], ['04-formal-published.png','V3.0.0 正式发布及交付物'], ['05-reader-notes.png','教学阅读:笔记与收藏'], ['06-reader-layout.png','教学阅读:OFD 版式'], ['09-reader-model.png','教学动态阅读:狐狸模型'], ['10-two-scene-resources.png','两个已发布场景固定版本引用'], ['11-reader-truck.png','教学动态阅读:牛奶卡车'], ['12-reading-completed.png','完成阅读,进度与统计更新'], ['08-http-integration.png','对外接口:真实 HTTP 查询'], ['13-http-conflict.png','对外接口:旧版本写入返回 409'], ['14-formal-templates.png','正式模板库'], ['17-template-editor.png','模板生成章节与步骤并保存'], ['15-offline-fox-scene.png','离线包:场景 160 当前发布内容(维修车间与装备)'], ['16-offline-truck-scene.png','离线包:牛奶卡车场景'], ['18-formal-video.png','图片与截图引用保持,视频实际播放'], ] const html=`作业指导正式接口与教学闭环 · 实测报告

开发环境 · 2026-09-10 — 2026-09-11 · 顺序实施与回归

作业指导正式接口与教学闭环

已完成编辑器接入、教学阅读记录、真实 HTTP 联调三项实施。正式发布工程 ${esc(release.projectId)},版本 ${esc(release.versionCode)}(版本 ID ${esc(release.versionId)})。

${passed}/${checks.length}E2E 检查点通过
391后端测试通过
17关键截图
3保留复核工程

实施内容

顺序落地结果验证方式
1. 内容、资源、版本完整编辑文档写入正式内容服务;上传资源进入受控存储;模板、模型和场景从正式接口加载;保存、发布、历史版本及恢复使用服务端版本。场景资源固定到发布版本,修改草稿不改变已发布教学内容。新建、模板生成、保存重开、GLB/图片/视频上传、固定场景引用、修订与发布、OFD 和离线 ZIP 实际下载。
2. 教学阅读闭环教学端消费正式发布文档与受控资源;阅读位置、笔记、收藏按用户和发布版本独立保存;记录开始/完成、反馈与处理,提供授权聚合统计。翻页恢复、笔记增改删、收藏取消、动态步骤同步进度、学员权限及数据隔离、反馈回复、完成记录。
3. 真实 HTTP 联调接口页面通过统一 HTTP 客户端调用 Gateway/业务服务,展示真实响应。涵盖内容、资源、版本、教学、个人阅读记录、统计与交付物。浏览器实际 GET/PUT;旧版本返回 409;错误方法返回 405;未经授权内容和统计接口返回 403。

接口沿用平台统一登录会话与权限体系。第三方系统需取得平台会话;数字人开放应用 APPID 不用于这组作业指导接口。

本轮修复

测试结果

浏览器为 Edge / Playwright,主要视口 1920×1080。管理操作使用内部维护账号;教学权限使用临时学员账号,测试后已清理。HTTP 通过 Web 6180 → Gateway 6100 → Tran 6102。

mvn -pl ut_tran -am test package:391 项通过,0 失败、0 错误、0 跳过。
pnpm run build:类型检查、Vite 构建与模型编辑器发布资源校验通过;保留既有 chunk 大小及运行时资源 URL 警告。

${results.map(s=>`

${esc(s.title)} JSON 证据

${s.checks.map(c=>``).join('')}
检查点结果
${esc(c.name)}${c.detail?`
${esc(c.detail)}
`:''}
${c.pass?'通过':'失败'}
`).join('')}

检查点来自分阶段脚本,覆盖关键链路,不表示所有边缘输入均已穷举。create/scenes 等早期证据对应创建及 V1→V2 阶段;最新发布、阅读和离线回归对应 V3。

关键截图

点击图片查看原图。文件名中的“fox-scene”沿用测试场景名称,不代表该场景当前实际包含狐狸。

交付与迁移

下载实测正式 OFD · 下载实测正式离线 ZIP(本地文件) · 发布摘要 · ZIP 完整性证据

离线 ZIP:${Number(offline.zipBytes).toLocaleString('zh-CN')} 字节;4 个 GLB 文件通过头部与长度检查。从正式 V3 下载后解压并运行,禁止访问业务服务及公网仍能加载两个场景。镜头修复代码已经包含在正式交付包中。

SHA-256:${esc(offline.sha256)}

开发环境迁移说明
20260826_017_teaching_guide_reading_closed_loop.sql补执行既有阅读与反馈表迁移,先回滚试执行再正式执行。
20260826_018_content_guide_template_library.sql补执行既有正式模板库迁移,保留 4 个模板。
20260911_001_guide_reader_state.sql新增按用户/项目/发布版本隔离的阅读位置、笔记、收藏及进度表;完整 DDL、数据库设计、迁移记录和 Wiki 已同步。

其他环境部署前先检查既有表,避免重复执行。新增流下载配置 UT_CONTENT_STREAM_TIMEOUT 默认 30m。本轮不代表其他环境已完成数据库迁移。

复核说明与保留数据

需要核对的数据差异:场景 160@463 名为“实测场景-狐狸野外训练区-20260905”,当前发布内容实际是维修车间与装备,不包含狐狸。已按其固定发布版本验证,未修改源场景;狐狸模型在指导书独立步骤中验证通过。牛奶卡车场景使用 159@209。

三项实施均在本次授权范围内,没有新增待批准的大幅功能调整。旧浏览器本地工程如需迁移,使用显式导入新工程;无法从服务端恢复的本地文件需重新上传。

` fs.writeFileSync(path.join(dir,'index.html'),html) console.log(`Report generated: ${passed}/${checks.length} checks, ${shots.length} screenshots`) if(passed!==checks.length)process.exitCode=1