diff --git a/reports/guide-formal-20260910/.gitignore b/reports/guide-formal-20260910/.gitignore new file mode 100644 index 0000000..9351210 --- /dev/null +++ b/reports/guide-formal-20260910/.gitignore @@ -0,0 +1,3 @@ +# 本地实测导出包及已修复失败尝试,仅保留在当前设备供追溯。 +formal-offline.zip +*failure*.png diff --git a/reports/guide-formal-20260910/01-formal-created.png b/reports/guide-formal-20260910/01-formal-created.png new file mode 100644 index 0000000..9619236 Binary files /dev/null and b/reports/guide-formal-20260910/01-formal-created.png differ diff --git a/reports/guide-formal-20260910/02-formal-assets.png b/reports/guide-formal-20260910/02-formal-assets.png new file mode 100644 index 0000000..10ef3be Binary files /dev/null and b/reports/guide-formal-20260910/02-formal-assets.png differ diff --git a/reports/guide-formal-20260910/03-formal-fox.png b/reports/guide-formal-20260910/03-formal-fox.png new file mode 100644 index 0000000..9245e62 Binary files /dev/null and b/reports/guide-formal-20260910/03-formal-fox.png differ diff --git a/reports/guide-formal-20260910/04-formal-published.png b/reports/guide-formal-20260910/04-formal-published.png new file mode 100644 index 0000000..ad9c916 Binary files /dev/null and b/reports/guide-formal-20260910/04-formal-published.png differ diff --git a/reports/guide-formal-20260910/05-reader-notes.png b/reports/guide-formal-20260910/05-reader-notes.png new file mode 100644 index 0000000..30f63fa Binary files /dev/null and b/reports/guide-formal-20260910/05-reader-notes.png differ diff --git a/reports/guide-formal-20260910/06-reader-layout.png b/reports/guide-formal-20260910/06-reader-layout.png new file mode 100644 index 0000000..18cfc8c Binary files /dev/null and b/reports/guide-formal-20260910/06-reader-layout.png differ diff --git a/reports/guide-formal-20260910/07-reader-interactive.png b/reports/guide-formal-20260910/07-reader-interactive.png new file mode 100644 index 0000000..9426a0e Binary files /dev/null and b/reports/guide-formal-20260910/07-reader-interactive.png differ diff --git a/reports/guide-formal-20260910/08-http-integration.png b/reports/guide-formal-20260910/08-http-integration.png new file mode 100644 index 0000000..7283aca Binary files /dev/null and b/reports/guide-formal-20260910/08-http-integration.png differ diff --git a/reports/guide-formal-20260910/09-reader-model.png b/reports/guide-formal-20260910/09-reader-model.png new file mode 100644 index 0000000..9426a0e Binary files /dev/null and b/reports/guide-formal-20260910/09-reader-model.png differ diff --git a/reports/guide-formal-20260910/10-two-scene-resources.png b/reports/guide-formal-20260910/10-two-scene-resources.png new file mode 100644 index 0000000..aa9f171 Binary files /dev/null and b/reports/guide-formal-20260910/10-two-scene-resources.png differ diff --git a/reports/guide-formal-20260910/11-reader-truck.png b/reports/guide-formal-20260910/11-reader-truck.png new file mode 100644 index 0000000..3b5b8fc Binary files /dev/null and b/reports/guide-formal-20260910/11-reader-truck.png differ diff --git a/reports/guide-formal-20260910/12-reading-completed.png b/reports/guide-formal-20260910/12-reading-completed.png new file mode 100644 index 0000000..687cdb2 Binary files /dev/null and b/reports/guide-formal-20260910/12-reading-completed.png differ diff --git a/reports/guide-formal-20260910/13-http-conflict.png b/reports/guide-formal-20260910/13-http-conflict.png new file mode 100644 index 0000000..cd07b19 Binary files /dev/null and b/reports/guide-formal-20260910/13-http-conflict.png differ diff --git a/reports/guide-formal-20260910/14-formal-templates.png b/reports/guide-formal-20260910/14-formal-templates.png new file mode 100644 index 0000000..891eaee Binary files /dev/null and b/reports/guide-formal-20260910/14-formal-templates.png differ diff --git a/reports/guide-formal-20260910/15-offline-fox-scene.png b/reports/guide-formal-20260910/15-offline-fox-scene.png new file mode 100644 index 0000000..d7969a3 Binary files /dev/null and b/reports/guide-formal-20260910/15-offline-fox-scene.png differ diff --git a/reports/guide-formal-20260910/16-offline-truck-scene.png b/reports/guide-formal-20260910/16-offline-truck-scene.png new file mode 100644 index 0000000..6246b92 Binary files /dev/null and b/reports/guide-formal-20260910/16-offline-truck-scene.png differ diff --git a/reports/guide-formal-20260910/17-template-editor.png b/reports/guide-formal-20260910/17-template-editor.png new file mode 100644 index 0000000..7998baf Binary files /dev/null and b/reports/guide-formal-20260910/17-template-editor.png differ diff --git a/reports/guide-formal-20260910/18-formal-video.png b/reports/guide-formal-20260910/18-formal-video.png new file mode 100644 index 0000000..d2fd077 Binary files /dev/null and b/reports/guide-formal-20260910/18-formal-video.png differ diff --git a/reports/guide-formal-20260910/create.json b/reports/guide-formal-20260910/create.json new file mode 100644 index 0000000..2f41cdb --- /dev/null +++ b/reports/guide-formal-20260910/create.json @@ -0,0 +1,15 @@ +{ + "checks": [ + { + "name": "新建请求实际进入内容服务", + "pass": true, + "detail": "真实 HTTP 请求成功;响应内容已脱敏。" + }, + { + "name": "正式后台可查询新工程", + "pass": true, + "detail": "" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/final.json b/reports/guide-formal-20260910/final.json new file mode 100644 index 0000000..cf07818 --- /dev/null +++ b/reports/guide-formal-20260910/final.json @@ -0,0 +1,70 @@ +{ + "checks": [ + { + "name": "重新打开恢复上次第三页阅读位置", + "pass": true, + "detail": "" + }, + { + "name": "笔记编辑保存成功", + "pass": true, + "detail": "" + }, + { + "name": "笔记删除成功", + "pass": true, + "detail": "" + }, + { + "name": "取消收藏写回服务器", + "pass": true, + "detail": "" + }, + { + "name": "动态阅读同步第四页和百分百页进度", + "pass": true, + "detail": "" + }, + { + "name": "教学只读模式允许目录定位和选择动画", + "pass": true, + "detail": "" + }, + { + "name": "教学动态版式切换卡车步骤", + "pass": true, + "detail": "" + }, + { + "name": "浏览器完成阅读并更新状态", + "pass": true, + "detail": "" + }, + { + "name": "HTTP 面板实际 PUT 阅读记录", + "pass": true, + "detail": "" + }, + { + "name": "HTTP 面板旧版本返回 409", + "pass": true, + "detail": "" + }, + { + "name": "错误 HTTP 方法明确返回 405", + "pass": true, + "detail": "" + }, + { + "name": "正式模板库可用", + "pass": true, + "detail": "" + }, + { + "name": "无浏览器脚本异常", + "pass": true, + "detail": "" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/formal.ofd b/reports/guide-formal-20260910/formal.ofd new file mode 100644 index 0000000..1e94a61 Binary files /dev/null and b/reports/guide-formal-20260910/formal.ofd differ diff --git a/reports/guide-formal-20260910/index.html b/reports/guide-formal-20260910/index.html new file mode 100644 index 0000000..9c909c8 --- /dev/null +++ b/reports/guide-formal-20260910/index.html @@ -0,0 +1,9 @@ +作业指导正式接口与教学闭环 · 实测报告 + +

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

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

已完成编辑器接入、教学阅读记录、真实 HTTP 联调三项实施。正式发布工程 412,版本 V3.0.0(版本 ID 472)。

58/58E2E 检查点通过
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 警告。

正式工程新建 JSON 证据

检查点结果
新建请求实际进入内容服务
真实 HTTP 请求成功;响应内容已脱敏。
通过
正式后台可查询新工程通过

模型资源上传与恢复 JSON 证据

检查点结果
上传 09-Fox.glb
真实 HTTP 请求成功;响应内容已脱敏。
通过
上传 08-CesiumMilkTruck.glb
真实 HTTP 请求成功;响应内容已脱敏。
通过
刷新后受控模型资源恢复显示通过

固定场景版本与修订 JSON 证据

检查点结果
新修订保持原发布版本可阅读通过
狐狸与卡车场景固定版本已保存
{"old":"469","published":"469","assets":7,"bindings":["160@463","159@209"],"version":12}
通过

正式发布与交付物 JSON 证据

检查点结果
正式发布事务完成
真实 HTTP 请求成功;响应内容已脱敏。
通过
教学指导书列表出现正式发布工程通过
发布版本提供 OFD 与离线 ZIP通过
下载正式交付物 OFD通过
下载正式交付物 OFFLINE_HTML通过

教学阅读及真实 HTTP 查询 JSON 证据

检查点结果
发布版本提供两份正式交付物通过
阅读位置、收藏、笔记真实写入通过
旧乐观锁版本被拒绝通过
旧发布版本不能覆盖当前阅读记录通过
收藏和笔记进入真实统计通过
浏览器笔记保存请求成功通过
浏览器翻页位置已同步通过
教学动态阅读显示三维画布通过
对外接口面板返回真实工程数据通过
浏览器无脚本异常通过

权限、私人记录与反馈 JSON 证据

检查点结果
学员不能进入内容编辑接口通过
学员可以阅读正式发布版本通过
学员投影没有文件存储 URI通过
学员看不到其他用户的笔记和收藏通过
学员能保存本人的阅读记录通过
学员写入不覆盖教员记录通过
学员不能查看教员聚合统计通过
开始阅读产生正式记录
success
通过
阅读完成记录成功
success
通过
学员反馈写入正式服务通过
教员回复并处理反馈
success
通过
学员通过教学接口下载 GLB通过
临时测试学员已清理
success
通过

阅读、笔记、进度及 HTTP 写入 JSON 证据

检查点结果
重新打开恢复上次第三页阅读位置通过
笔记编辑保存成功通过
笔记删除成功通过
取消收藏写回服务器通过
动态阅读同步第四页和百分百页进度通过
教学只读模式允许目录定位和选择动画通过
教学动态版式切换卡车步骤通过
浏览器完成阅读并更新状态通过
HTTP 面板实际 PUT 阅读记录通过
HTTP 面板旧版本返回 409通过
错误 HTTP 方法明确返回 405通过
正式模板库可用通过
无浏览器脚本异常通过

正式模板新建 JSON 证据

检查点结果
模板新建实际进入正式内容服务通过
模板初稿进入编辑器并保存通过
模板章节和步骤不丢失通过

图片、视频及截图引用 JSON 证据

检查点结果
图片与视频上传、截图引用保存后重开保持通过
OFD 预览读取受控图片和截图通过
正式视频通过受控字节恢复并实际播放通过

正式离线包运行 JSON 证据

检查点结果
15-offline-fox-scene
实测场景-狐狸野外训练区-20260905.glb · 186 个网格 · 拖拽旋转 / 滚轮缩放
通过
16-offline-truck-scene
实测场景-牛奶卡车检验区-20260905.glb · 37 个网格 · 拖拽旋转 / 滚轮缩放
通过
离线资源不依赖业务服务器或公网通过
无脚本异常通过

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

+

关键截图

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

+

交付与迁移

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

离线 ZIP:71,380,256 字节;4 个 GLB 文件通过头部与长度检查。从正式 V3 下载后解压并运行,禁止访问业务服务及公网仍能加载两个场景。镜头修复代码已经包含在正式交付包中。

SHA-256:7227683a98f98e48aa31c3839fcb63782572fe6a9d9f7f48949fbae07e64f19b

开发环境迁移说明
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。

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

\ No newline at end of file diff --git a/reports/guide-formal-20260910/media.json b/reports/guide-formal-20260910/media.json new file mode 100644 index 0000000..9da38a4 --- /dev/null +++ b/reports/guide-formal-20260910/media.json @@ -0,0 +1,17 @@ +{ + "projectId": "414", + "checks": [ + { + "name": "图片与视频上传、截图引用保存后重开保持", + "pass": true + }, + { + "name": "OFD 预览读取受控图片和截图", + "pass": true + }, + { + "name": "正式视频通过受控字节恢复并实际播放", + "pass": true + } + ] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/offline-browser.json b/reports/guide-formal-20260910/offline-browser.json new file mode 100644 index 0000000..d6e0031 --- /dev/null +++ b/reports/guide-formal-20260910/offline-browser.json @@ -0,0 +1,25 @@ +{ + "checks": [ + { + "name": "15-offline-fox-scene", + "pass": true, + "detail": "实测场景-狐狸野外训练区-20260905.glb · 186 个网格 · 拖拽旋转 / 滚轮缩放" + }, + { + "name": "16-offline-truck-scene", + "pass": true, + "detail": "实测场景-牛奶卡车检验区-20260905.glb · 37 个网格 · 拖拽旋转 / 滚轮缩放" + }, + { + "name": "离线资源不依赖业务服务器或公网", + "pass": true, + "detail": "" + }, + { + "name": "无脚本异常", + "pass": true, + "detail": "" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/offline.json b/reports/guide-formal-20260910/offline.json new file mode 100644 index 0000000..92c4cf0 --- /dev/null +++ b/reports/guide-formal-20260910/offline.json @@ -0,0 +1,36 @@ +{ + "integrity": "passed", + "fixedCameraRuntime": true, + "zipBytes": 71380256, + "sha256": "7227683a98f98e48aa31c3839fcb63782572fe6a9d9f7f48949fbae07e64f19b", + "models": [ + { + "file": "assets/models/model-001.glb", + "size": 162852, + "validGlb": true, + "hasFox": true, + "hasMilkTruck": false + }, + { + "file": "assets/models/model-002.glb", + "size": 68091456, + "validGlb": true, + "hasFox": false, + "hasMilkTruck": false + }, + { + "file": "assets/models/model-003.glb", + "size": 318276, + "validGlb": true, + "hasFox": false, + "hasMilkTruck": true + }, + { + "file": "assets/models/model-004.glb", + "size": 1034616, + "validGlb": true, + "hasFox": false, + "hasMilkTruck": true + } + ] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/permissions.json b/reports/guide-formal-20260910/permissions.json new file mode 100644 index 0000000..4c365e8 --- /dev/null +++ b/reports/guide-formal-20260910/permissions.json @@ -0,0 +1,69 @@ +{ + "checks": [ + { + "name": "学员不能进入内容编辑接口", + "pass": true, + "detail": "" + }, + { + "name": "学员可以阅读正式发布版本", + "pass": true, + "detail": "" + }, + { + "name": "学员投影没有文件存储 URI", + "pass": true, + "detail": "" + }, + { + "name": "学员看不到其他用户的笔记和收藏", + "pass": true, + "detail": "" + }, + { + "name": "学员能保存本人的阅读记录", + "pass": true, + "detail": "" + }, + { + "name": "学员写入不覆盖教员记录", + "pass": true, + "detail": "" + }, + { + "name": "学员不能查看教员聚合统计", + "pass": true, + "detail": "" + }, + { + "name": "开始阅读产生正式记录", + "pass": true, + "detail": "success" + }, + { + "name": "阅读完成记录成功", + "pass": true, + "detail": "success" + }, + { + "name": "学员反馈写入正式服务", + "pass": true, + "detail": "" + }, + { + "name": "教员回复并处理反馈", + "pass": true, + "detail": "success" + }, + { + "name": "学员通过教学接口下载 GLB", + "pass": true, + "detail": "" + }, + { + "name": "临时测试学员已清理", + "pass": true, + "detail": "success" + } + ] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/publish.json b/reports/guide-formal-20260910/publish.json new file mode 100644 index 0000000..5619bf5 --- /dev/null +++ b/reports/guide-formal-20260910/publish.json @@ -0,0 +1,30 @@ +{ + "checks": [ + { + "name": "正式发布事务完成", + "pass": true, + "detail": "真实 HTTP 请求成功;响应内容已脱敏。" + }, + { + "name": "教学指导书列表出现正式发布工程", + "pass": true, + "detail": "" + }, + { + "name": "发布版本提供 OFD 与离线 ZIP", + "pass": true, + "detail": "" + }, + { + "name": "下载正式交付物 OFD", + "pass": true, + "detail": "" + }, + { + "name": "下载正式交付物 OFFLINE_HTML", + "pass": true, + "detail": "" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/release.json b/reports/guide-formal-20260910/release.json new file mode 100644 index 0000000..c535a68 --- /dev/null +++ b/reports/guide-formal-20260910/release.json @@ -0,0 +1,53 @@ +{ + "projectId": "412", + "versionId": "472", + "versionCode": "V3.0.0", + "status": "PUBLISHED", + "dependencies": [ + { + "projectId": "160", + "versionId": "463" + }, + { + "projectId": "159", + "versionId": "209" + } + ], + "assets": [ + { + "name": "09-Fox.glb", + "sizeBytes": 162852, + "type": "MODEL_FILE" + }, + { + "name": "08-CesiumMilkTruck.glb", + "sizeBytes": 369980, + "type": "MODEL_FILE" + }, + { + "name": "capture.jpeg", + "sizeBytes": 25174, + "type": "IMAGE" + }, + { + "name": "实测场景-狐狸野外训练区-20260905.glb", + "sizeBytes": 81483944, + "type": "MODEL_FILE" + }, + { + "name": "实测场景-牛奶卡车检验区-20260905.glb", + "sizeBytes": 1330380, + "type": "MODEL_FILE" + }, + { + "name": "正式闭环-狐狸与卡车指导书-20260910_V3.0.0.ofd", + "sizeBytes": 53551, + "type": "DOCUMENT" + }, + { + "name": "正式闭环-狐狸与卡车指导书-20260910_V3.0.0_动态版式离线包.zip", + "sizeBytes": 71380256, + "type": "DOCUMENT" + } + ] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/report-preview.png b/reports/guide-formal-20260910/report-preview.png new file mode 100644 index 0000000..acba5fd Binary files /dev/null and b/reports/guide-formal-20260910/report-preview.png differ diff --git a/reports/guide-formal-20260910/report-validation.json b/reports/guide-formal-20260910/report-validation.json new file mode 100644 index 0000000..088700d --- /dev/null +++ b/reports/guide-formal-20260910/report-validation.json @@ -0,0 +1,6 @@ +{ + "images": 17, + "brokenImages": [], + "lanPageLoaded": true, + "viewport": "1440x1000" +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/review.json b/reports/guide-formal-20260910/review.json new file mode 100644 index 0000000..c3c171f --- /dev/null +++ b/reports/guide-formal-20260910/review.json @@ -0,0 +1,55 @@ +{ + "checks": [ + { + "name": "发布版本提供两份正式交付物", + "pass": true, + "detail": "" + }, + { + "name": "阅读位置、收藏、笔记真实写入", + "pass": true, + "detail": "" + }, + { + "name": "旧乐观锁版本被拒绝", + "pass": true, + "detail": "" + }, + { + "name": "旧发布版本不能覆盖当前阅读记录", + "pass": true, + "detail": "" + }, + { + "name": "收藏和笔记进入真实统计", + "pass": true, + "detail": "" + }, + { + "name": "浏览器笔记保存请求成功", + "pass": true, + "detail": "" + }, + { + "name": "浏览器翻页位置已同步", + "pass": true, + "detail": "" + }, + { + "name": "教学动态阅读显示三维画布", + "pass": true, + "detail": "" + }, + { + "name": "对外接口面板返回真实工程数据", + "pass": true, + "detail": "" + }, + { + "name": "浏览器无脚本异常", + "pass": true, + "detail": "" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/scenes.json b/reports/guide-formal-20260910/scenes.json new file mode 100644 index 0000000..d070215 --- /dev/null +++ b/reports/guide-formal-20260910/scenes.json @@ -0,0 +1,14 @@ +{ + "checks": [ + { + "name": "新修订保持原发布版本可阅读", + "pass": true, + "detail": "" + }, + { + "name": "狐狸与卡车场景固定版本已保存", + "pass": true, + "detail": "{\"old\":\"469\",\"published\":\"469\",\"assets\":7,\"bindings\":[\"160@463\",\"159@209\"],\"version\":12}" + } + ] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/template.json b/reports/guide-formal-20260910/template.json new file mode 100644 index 0000000..9960b1a --- /dev/null +++ b/reports/guide-formal-20260910/template.json @@ -0,0 +1,17 @@ +{ + "projectId": "413", + "checks": [ + { + "name": "模板新建实际进入正式内容服务", + "pass": true + }, + { + "name": "模板初稿进入编辑器并保存", + "pass": true + }, + { + "name": "模板章节和步骤不丢失", + "pass": true + } + ] +} \ No newline at end of file diff --git a/reports/guide-formal-20260910/upload.json b/reports/guide-formal-20260910/upload.json new file mode 100644 index 0000000..943475f --- /dev/null +++ b/reports/guide-formal-20260910/upload.json @@ -0,0 +1,20 @@ +{ + "checks": [ + { + "name": "上传 09-Fox.glb", + "pass": true, + "detail": "真实 HTTP 请求成功;响应内容已脱敏。" + }, + { + "name": "上传 08-CesiumMilkTruck.glb", + "pass": true, + "detail": "真实 HTTP 请求成功;响应内容已脱敏。" + }, + { + "name": "刷新后受控模型资源恢复显示", + "pass": true, + "detail": "" + } + ], + "errors": [] +} \ No newline at end of file diff --git a/tools/guide-formal-final.mjs b/tools/guide-formal-final.mjs new file mode 100644 index 0000000..9b7994b --- /dev/null +++ b/tools/guide-formal-final.mjs @@ -0,0 +1,70 @@ +import fs from 'node:fs' +import path from 'node:path' +import {chromium,expect} from '@playwright/test' +import {login,call} from './role-permissions-live.mjs' +const report=path.resolve('reports/guide-formal-20260910'), fixture=JSON.parse(fs.readFileSync('../../.codex-tmp/guide-formal/fixture.json')) +const session=await login('root'), token=session.accessToken, checks=[],errors=[] +const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}) +const context=await browser.newContext({viewport:{width:1920,height:1080}}) +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session) +const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message)) +const check=(name,pass,detail='')=>{checks.push({name,pass,detail});console.log(pass?'PASS':'FAIL',name);if(!pass)process.exitCode=1} +const shot=name=>page.screenshot({path:path.join(report,name+'.png')}) +const openReader=async()=>{ + await page.goto('http://127.0.0.1:6180/teaching/guides') + await page.getByPlaceholder('搜索指导书名称或编码').fill('正式闭环-狐狸与卡车指导书-20260910') + await page.getByPlaceholder('搜索指导书名称或编码').press('Enter') + await expect(page.locator('.guide-card-v2')).toHaveCount(1) + await page.locator('.guide-card-v2 button').first().click() + await expect(page.locator('.legacy-published-reader .ofd-preview-pager')).toBeVisible({timeout:60000}) +} +try{ + const state=(await call(`tran/v1/teaching/guides/${fixture.id}/reader-state`,token)).data + await call(`tran/v1/teaching/guides/${fixture.id}/reader-state`,token,{...state,pageId:'page:2',pageIndex:2,pageCount:4,favorite:true,notes:[{id:'final-1',pageId:'page:1',text:'定位核对记录'},{id:'final-2',pageId:'page:2',text:'编辑与删除测试记录'}]},'PUT') + await openReader() + await expect(page.locator('.legacy-published-reader .ofd-preview-pager')).toContainText('3 / 4') + check('重新打开恢复上次第三页阅读位置',true) + await page.getByRole('button',{name:/笔记 2/}).click() + const note=page.locator('.reader-note').last() + await note.getByRole('button',{name:'编辑',exact:true}).click() + await page.getByPlaceholder('记录当前页的阅读笔记').fill('已修改:复查牛奶卡车后确认记录。') + const edited=page.waitForResponse(r=>r.url().endsWith('/reader-state')&&r.request().method()==='PUT') + await page.getByRole('button',{name:'保存修改',exact:true}).click();check('笔记编辑保存成功',(await edited).ok()) + await expect(note).toContainText('已修改') + const removed=page.waitForResponse(r=>r.url().endsWith('/reader-state')&&r.request().method()==='PUT') + await note.getByRole('button',{name:'删除',exact:true}).click();check('笔记删除成功',(await removed).ok()) + await expect(page.locator('.reader-note')).toHaveCount(1) + await page.locator('.el-drawer__close-btn:visible').click() + const unfavorite=page.waitForResponse(r=>r.url().endsWith('/reader-state')&&r.request().method()==='PUT') + await page.getByRole('button',{name:'取消收藏',exact:true}).click();check('取消收藏写回服务器',(await unfavorite).ok()) + await page.locator('.legacy-published-reader [data-action="preview-tab"][data-tab="interactive"]').click() + await page.locator('.legacy-published-reader [data-action="interactive-next"]').click() + await page.locator('.legacy-published-reader canvas').first().scrollIntoViewIfNeeded() + await page.waitForTimeout(3000);await shot('11-reader-truck') + const position=await call(`tran/v1/teaching/guides/${fixture.id}/reader-state`,token) + check('动态阅读同步第四页和百分百页进度',position.data?.pageIndex===3&&position.data?.progress===100) + await expect(page.locator('.legacy-published-reader [data-action="interactive-step"]').first()).toBeEnabled() + await expect(page.locator('.legacy-published-reader [data-model-clip-select]')).toBeEnabled() + check('教学只读模式允许目录定位和选择动画',true) + check('教学动态版式切换卡车步骤',await page.locator('.legacy-published-reader').textContent().then(t=>t.includes('牛奶卡车检查'))) + if(await page.getByRole('button',{name:'标记阅读完成',exact:true}).count())await page.getByRole('button',{name:'标记阅读完成',exact:true}).click() + await expect(page.getByRole('button',{name:'已完成阅读',exact:true})).toBeDisabled() + await shot('12-reading-completed');check('浏览器完成阅读并更新状态',true) + await page.goto('http://127.0.0.1:6180/content/ofd/api') + await page.locator('.guide-http-page nav button').filter({hasText:'保存笔记、收藏与进度'}).click() + await page.locator('.guide-http-page .el-select').first().click() + await page.getByRole('option').filter({hasText:'正式闭环-狐狸与卡车指导书-20260910'}).click() + await page.getByRole('button',{name:'载入当前工程版本参数',exact:true}).click() + await expect(page.getByRole('textbox',{name:'请求 JSON'})).toHaveValue(/versionId/) + const saved=page.waitForResponse(r=>r.url().endsWith('/reader-state')&&r.request().method()==='PUT') + await page.getByRole('button',{name:'执行调用',exact:true}).click();check('HTTP 面板实际 PUT 阅读记录',(await saved).ok()) + const conflict=page.waitForResponse(r=>r.url().endsWith('/reader-state')&&r.request().method()==='PUT') + await page.getByRole('button',{name:'执行调用',exact:true}).click();check('HTTP 面板旧版本返回 409',(await conflict).status()===409) + await shot('13-http-conflict') + const method=await call(`tran/v1/teaching/guides/${fixture.id}/reading/start`,token,{},'POST') + check('错误 HTTP 方法明确返回 405',method.status===405) + await page.goto('http://127.0.0.1:6180/content/ofd/templates') + await expect(page.locator('.template-card').first()).toBeVisible({timeout:30000}) + await shot('14-formal-templates');check('正式模板库可用',true) +}catch(e){check('最终浏览器回归',false,e.message);await shot('final-failure')} +finally{check('无浏览器脚本异常',errors.length===0,errors.join('\n'));fs.writeFileSync(path.join(report,'final.json'),JSON.stringify({checks,errors},null,2));await browser.close()} diff --git a/tools/guide-formal-live.mjs b/tools/guide-formal-live.mjs new file mode 100644 index 0000000..a5f21f1 --- /dev/null +++ b/tools/guide-formal-live.mjs @@ -0,0 +1,77 @@ +import fs from 'node:fs' +import path from 'node:path' +import { chromium, expect } from '@playwright/test' +import { login, call } from './role-permissions-live.mjs' +const report=path.resolve('reports/guide-formal-20260910'),work=path.resolve('../../.codex-tmp/guide-formal') +fs.mkdirSync(report,{recursive:true});fs.mkdirSync(work,{recursive:true}) +const phase=process.argv[2]||'create', fixturePath=path.join(work,'fixture.json') +const fixture=fs.existsSync(fixturePath)?JSON.parse(fs.readFileSync(fixturePath)):{} +const session=await login('root'),browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}) +const context=await browser.newContext({viewport:{width:1920,height:1080}}) +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session) +const page=await context.newPage(),checks=[],errors=[] +page.on('pageerror',e=>errors.push(e.message)) +page.on('response',r=>{if(r.status()>=400) console.log('HTTP',r.status(),new URL(r.url()).pathname)}) +await page.addInitScript(()=>{new MutationObserver(()=>{for(const el of document.querySelectorAll('.ofd-toast,.el-message'))if(!el.dataset.reported){el.dataset.reported='true';console.log('NOTICE',el.textContent)}}).observe(document,{childList:true,subtree:true})}) +page.on('console',m=>{if(m.text().startsWith('NOTICE'))console.log(m.text())}) +const shot=async name=>page.screenshot({path:path.join(report,name+'.png')}) +const check=(name,pass,detail='')=>{checks.push({name,pass,detail});console.log(pass?'PASS':'FAIL',name);if(!pass)process.exitCode=1} +try{ + if(phase==='create'){ + await page.goto('http://127.0.0.1:6180/content/ofd') + await page.getByRole('button',{name:'新建指导书',exact:true}).click() + await page.locator('[data-wizard="name"]').fill('正式闭环-狐狸与卡车指导书-20260910') + await page.locator('[data-wizard="equipment"]').fill('狐狸与牛奶卡车功能测试素材') + await page.locator('[data-wizard="owner"]').fill('功能闭环测试') + await page.locator('[data-action="wizard-next"]').click() + await page.locator('[data-action="wizard-template"][data-template="__blank__"]').click() + await page.locator('[data-action="wizard-next"]').click() + const response=page.waitForResponse(r=>r.url().endsWith('/content/projects')&&r.request().method()==='POST') + await page.locator('[data-action="wizard-next"]').click() + const created=await response;check('新建请求实际进入内容服务',created.status()===200,await created.text()) + await page.waitForURL(/\/ofd\/\d+\/studio/) + fixture.id=page.url().match(/\/ofd\/(\d+)\/studio/)[1];fs.writeFileSync(fixturePath,JSON.stringify(fixture)) + const server=await call(`tran/v1/content/projects/${fixture.id}`,session.accessToken) + check('正式后台可查询新工程',server.status===200) + await shot('01-formal-created') + } + if(phase==='publish'){ + await page.goto(`http://127.0.0.1:6180/content/ofd/${fixture.id}/studio?view=editor`) + await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) + await page.locator('[data-action="save"]').click();await page.waitForTimeout(700) + await page.locator('[data-action="open-publish"]').click() + const response=page.waitForResponse(r=>r.url().endsWith('/publish')&&r.request().method()==='POST',{timeout:600000}) + await page.locator('[data-action="lifecycle-action"][data-lifecycle-action="publish"]').click() + const result=await response;check('正式发布事务完成',result.ok(),(await result.text()).slice(0,400)) + await page.waitForTimeout(700);await shot('04-formal-published') + const guides=await call('tran/v1/teaching/guides?keyword='+encodeURIComponent('正式闭环-狐狸与卡车指导书-20260910'),session.accessToken) + check('教学指导书列表出现正式发布工程',guides.data?.records?.some(r=>r.projectId===fixture.id)) + const assets=await call(`tran/v1/teaching/guides/${fixture.id}/deliveries`,session.accessToken) + check('发布版本提供 OFD 与离线 ZIP',assets.data?.length===2) + for(const format of ['OFD','OFFLINE_HTML']){ + const result=await context.request.get(`http://127.0.0.1:6180/api/tran/v1/teaching/guides/${fixture.id}/delivery?format=${format}`,{headers:{Authorization:`Bearer ${session.accessToken}`},timeout:180000}) + check('下载正式交付物 '+format,result.ok());if(result.ok())fs.writeFileSync(path.join(report,format==='OFD'?'formal.ofd':'formal-offline.zip'),await result.body()) + } + } + if(phase==='upload'){ + await page.goto(`http://127.0.0.1:6180/content/ofd/${fixture.id}/studio?view=editor`) + await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) + for(const name of ['09-Fox.glb','08-CesiumMilkTruck.glb']){ + const wait=page.waitForResponse(r=>r.url().includes('/assets/upload')&&r.request().method()==='POST') + await page.locator('#ofd-resource-file').setInputFiles(path.resolve('../../文档/素材',name)) + const response=await wait;check('上传 '+name,response.ok(),(await response.text()).slice(0,300)) + await page.waitForTimeout(1800) + } + await page.locator('[data-action="save"]').click();await page.waitForTimeout(1200) + await shot('02-formal-assets') + await page.reload();await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) + await page.locator('[data-action="select-step"]').first().click() + await page.locator('[data-action="right-dock-tab"]').filter({hasText:'资源'}).click() + await page.locator('[data-action="library-insert"][data-kind="model"]').filter({hasText:'Fox'}).first().click() + await page.locator('[data-action="media-open"]').first().click() + await expect(page.locator('.ofd-media-panel canvas')).toBeVisible({timeout:30000}) + await page.waitForTimeout(1200);await shot('03-formal-fox') + check('刷新后受控模型资源恢复显示',true) + } +}catch(e){check(phase,false,e.message);console.log(await page.locator('.legacy-guide-page').evaluate(el=>el.__vueParentComponent?.setupState?.mountError).catch(()=>''));await shot(phase+'-failure').catch(()=>{})} +finally{fs.writeFileSync(path.join(report,phase+'.json'),JSON.stringify({checks,errors},null,2));await browser.close()} diff --git a/tools/guide-formal-media.mjs b/tools/guide-formal-media.mjs new file mode 100644 index 0000000..9ed7c60 --- /dev/null +++ b/tools/guide-formal-media.mjs @@ -0,0 +1,48 @@ +import fs from 'node:fs' +import {chromium,expect} from '@playwright/test' +import {login} from './role-permissions-live.mjs' +const session=await login('root'),checks=[] +const browser=await chromium.launch({channel:'msedge',headless:true}) +const page=await browser.newPage({viewport:{width:1920,height:1080}}) +await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session) +let id +try{ + await page.goto('http://127.0.0.1:6180/content/ofd') + await expect(page.getByRole('button',{name:'新建指导书',exact:true})).toBeVisible() + const result=process.env.GUIDE_MEDIA_ID ? await page.evaluate(async id=>{ + const {FormalGuideStore}=await import('/src/features/guide-legacy/formalStore.ts') + const store=new FormalGuideStore();await store.load(id) + const step=store.record.state.chapters[0].steps[0] + const valid=step.mediaBlocks.every(b=>b.kind==='video'?!!b.poster:!!b.imageSource)&&!JSON.stringify(step).includes('blob:') + store.dispose();return {id,valid} + },process.env.GUIDE_MEDIA_ID) : await page.evaluate(async()=>{ + const {FormalGuideStore}=await import('/src/features/guide-legacy/formalStore.ts') + const store=new FormalGuideStore();await store.createOFDProject({title:'正式图片视频资源回归-20260911'}) + const canvas=document.createElement('canvas');canvas.width=400;canvas.height=240 + const ctx=canvas.getContext('2d');ctx.fillStyle='#168a72';ctx.fillRect(0,0,400,240);ctx.fillStyle='white';ctx.font='24px sans-serif';ctx.fillText('Formal image resource',45,130) + const png=await new Promise(resolve=>canvas.toBlob(resolve,'image/png')) + const image=await store.assetStore.put(png,{name:'guide-image.png'}) + const video=await store.assetStore.put(await(await fetch('/videos/physical-training-real-main-720p.mp4')).blob(),{name:'guide-video.mp4'}) + const state=structuredClone(store.record.state) + state.resources=[{...image,id:image.assetKey,name:'guide-image.png',type:'图片',category:'image',resourceUrl:`asset://${image.assetKey}`,blob:undefined},{...video,id:video.assetKey,name:'guide-video.mp4',type:'视频',category:'video',resourceUrl:`asset://${video.assetKey}`,blob:undefined}] + const step=state.chapters[0].steps[0];step.image=`asset://${image.assetKey}` + step.mediaBlocks=[{id:'image-block',kind:'image',imageSource:`asset://${image.assetKey}`,x:120,y:62,width:72,height:58},{id:'video-block',kind:'video',assetId:video.assetKey,assetName:'guide-video.mp4',poster:canvas.toDataURL('image/png'),x:120,y:130,width:72,height:58}] + await store.saveOFDProjectState(store.project.id,state) + const id=store.project.id;store.dispose();const loaded=new FormalGuideStore();await loaded.load(id) + const saved=loaded.record.state.chapters[0].steps[0] + const valid=saved.mediaBlocks.every(b=>b.kind==='video'?!!b.poster:!!b.imageSource)&&!JSON.stringify(saved).includes('blob:') + loaded.dispose();return {id,valid} + }) + id=result.id;checks.push({name:'图片与视频上传、截图引用保存后重开保持',pass:result.valid}) + await page.goto(`http://127.0.0.1:6180/content/ofd/${id}/studio?view=preview`) + await expect(page.locator('.ofd-preview-pager')).toBeVisible({timeout:60000}) + checks.push({name:'OFD 预览读取受控图片和截图',pass:!(await page.locator('.ofd-analysis-empty').allTextContents()).some(t=>t.includes('失败'))}) + await page.locator('[data-action="preview-tab"][data-tab="interactive"]').click() + const video=page.locator('.ofd-ia-media-item.video video') + await expect(video).toHaveAttribute('src',/^blob:/,{timeout:30000}) + await expect.poll(()=>video.evaluate(v=>v.readyState)).toBeGreaterThanOrEqual(2) + await video.evaluate(v=>v.play());await expect.poll(()=>video.evaluate(v=>v.currentTime)).toBeGreaterThan(0) + await video.scrollIntoViewIfNeeded();await page.screenshot({path:'reports/guide-formal-20260910/18-formal-video.png'}) + checks.push({name:'正式视频通过受控字节恢复并实际播放',pass:true}) +}catch(e){checks.push({name:'图片视频资源闭环',pass:false,detail:e.message});process.exitCode=1;await page.screenshot({path:'reports/guide-formal-20260910/media-failure.png'})} +finally{console.log(checks);fs.writeFileSync('reports/guide-formal-20260910/media.json',JSON.stringify({projectId:id,checks},null,2));await browser.close()} diff --git a/tools/guide-formal-offline.mjs b/tools/guide-formal-offline.mjs new file mode 100644 index 0000000..bb0e215 --- /dev/null +++ b/tools/guide-formal-offline.mjs @@ -0,0 +1,20 @@ +import fs from 'node:fs' +import {chromium,expect} from '@playwright/test' +const report='reports/guide-formal-20260910',checks=[],errors=[],external=[] +const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}) +const page=await browser.newPage({viewport:{width:1440,height:1000}}) +page.on('pageerror',e=>errors.push(e.message)) +await page.route('**/*',route=>{const url=new URL(route.request().url());if(url.hostname!=='127.0.0.1'&&!['blob:','data:'].includes(url.protocol)){external.push(url.origin);return route.abort()}return route.continue()}) +try{ + await page.goto('http://127.0.0.1:18769/') + await page.locator('[data-action="open-model-library"]').click() + await page.locator('[data-model-select]').scrollIntoViewIfNeeded() + for(const [index,name] of [['1','15-offline-fox-scene'],['3','16-offline-truck-scene']]){ + await page.locator('[data-model-select]').selectOption(index) + await expect(page.locator('[data-model-status]')).not.toContainText(/正在|尚未|失败/,{timeout:90000}) + await page.waitForTimeout(2000) + await page.screenshot({path:`${report}/${name}.png`}) + checks.push({name,pass:true,detail:await page.locator('[data-model-status]').textContent()}) + } +}catch(e){checks.push({name:'离线场景浏览器读取',pass:false,detail:e.message});process.exitCode=1} +finally{checks.push({name:'离线资源不依赖业务服务器或公网',pass:external.length===0,detail:external.join(',')},{name:'无脚本异常',pass:errors.length===0,detail:errors.join('\n')});console.log(checks);fs.writeFileSync(`${report}/offline-browser.json`,JSON.stringify({checks,errors},null,2));await browser.close()} diff --git a/tools/guide-formal-permissions.mjs b/tools/guide-formal-permissions.mjs new file mode 100644 index 0000000..12e93bd --- /dev/null +++ b/tools/guide-formal-permissions.mjs @@ -0,0 +1,36 @@ +import fs from 'node:fs' +import crypto from 'node:crypto' +import {login,call} from './role-permissions-live.mjs' +const fixture=JSON.parse(fs.readFileSync('../../.codex-tmp/guide-formal/fixture.json')),session=await login('root'),token=session.accessToken,checks=[] +const check=(name,pass,detail='')=>{checks.push({name,pass,detail});console.log(pass?'PASS':'FAIL',name);if(!pass)process.exitCode=1} +let user +try{ + const roles=(await call('auth/v1/roles/all',token)).data,role=(roles.records||roles).find(r=>r.code==='student') + const me=(await call('auth/v1/auth/me',token)).data,password='G!'+crypto.randomBytes(7).toString('hex'),username='guide_'+crypto.randomBytes(5).toString('hex') + const created=await call('auth/v1/users',token,{username,password,displayName:'作业指导闭环测试学员',departmentId:me.departmentId,roleIds:[role.id],defaultRoleId:role.id,enabled:true,remark:'UTE2E-GUIDE-FORMAL'}) + if(created.status!==200)throw new Error('测试学员创建失败 '+created.message) + user=created.data + const logged=await call('auth/v1/auth/login',null,{userId:user.id,password,roleCode:'student',rememberMe:false}) + const actor=logged.data?.accessToken + if(!actor)throw new Error('测试学员登录失败 '+logged.status+' '+logged.message) + const base=`tran/v1/teaching/guides/${fixture.id}` + const content=await call(`tran/v1/content/projects/${fixture.id}`,actor);check('学员不能进入内容编辑接口',content.status===403) + const detail=await call(base,actor);check('学员可以阅读正式发布版本',detail.status===200) + check('学员投影没有文件存储 URI',!JSON.stringify(detail.data).includes('fsvc://')) + const state=(await call(`${base}/reader-state`,actor)).data;check('学员看不到其他用户的笔记和收藏',state?.notes.length===0&&state.favorite===false) + const saved=await call(`${base}/reader-state`,actor,{...state,pageId:'page:0',pageIndex:0,pageCount:3,favorite:true,notes:[{id:'student-note',pageId:'page:0',text:'学员本人私人笔记'}]},'PUT');check('学员能保存本人的阅读记录',saved.status===200) + const owner=(await call(`${base}/reader-state`,token)).data;check('学员写入不覆盖教员记录',!owner.notes.some(n=>n.id==='student-note')) + const stats=await call(`${base}/reader-statistics`,actor);check('学员不能查看教员聚合统计',stats.status===403) + const start=await call(`${base}/reading/start`,actor,{},'PUT');check('开始阅读产生正式记录',start.status===200,start.message) + const complete=await call(`${base}/reading/complete`,actor,{},'PUT');check('阅读完成记录成功',complete.status===200,complete.message) + const feedback=await call(`${base}/feedback`,actor,{content:'实测反馈:狐狸和卡车指导书资源已加载,请复核说明。'});check('学员反馈写入正式服务',feedback.status===200) + if(feedback.data?.id){const managed=await call(`${base}/feedback/${feedback.data.id}`,token,{version:feedback.data.version,status:'HANDLED',reply:'已通过正式接口复核。'},'PUT');check('教员回复并处理反馈',managed.status===200,managed.message)} + const asset=detail.data?.assets.find(a=>a.type==='MODEL_FILE') + const response=await fetch(`http://127.0.0.1:6180/api/${base}/assets/${asset.assetCode}/content`,{headers:{Authorization:`Bearer ${actor}`}}) + const bytes=await response.arrayBuffer();check('学员通过教学接口下载 GLB',response.ok&&new TextDecoder().decode(bytes.slice(0,4))==='glTF') + await call('auth/v1/auth/logout',actor,{}) +}catch(e){check('权限与阅读反馈流程',false,e.message)} +finally{ + if(user?.id){const current=await call(`auth/v1/users/${user.id}`,token);const removed=await call(`auth/v1/users/${user.id}`,token,{version:current.data.version},'DELETE');check('临时测试学员已清理',removed.status===200,removed.message)} + fs.writeFileSync('reports/guide-formal-20260910/permissions.json',JSON.stringify({checks},null,2)) +} diff --git a/tools/guide-formal-report.mjs b/tools/guide-formal-report.mjs new file mode 100644 index 0000000..be487e9 --- /dev/null +++ b/tools/guide-formal-report.mjs @@ -0,0 +1,46 @@ +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 diff --git a/tools/guide-formal-review.mjs b/tools/guide-formal-review.mjs new file mode 100644 index 0000000..a319d95 --- /dev/null +++ b/tools/guide-formal-review.mjs @@ -0,0 +1,56 @@ +import fs from 'node:fs' +import path from 'node:path' +import {chromium,expect} from '@playwright/test' +import {login,call} from './role-permissions-live.mjs' +const report=path.resolve('reports/guide-formal-20260910'),work=path.resolve('../../.codex-tmp/guide-formal') +const fixture=JSON.parse(fs.readFileSync(path.join(work,'fixture.json'))),checks=[],errors=[] +const session=await login('root'),token=session.accessToken +const check=(name,pass,detail='')=>{checks.push({name,pass,detail});console.log(pass?'PASS':'FAIL',name);if(!pass)process.exitCode=1} +const base=`tran/v1/teaching/guides/${fixture.id}` +const browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}) +const context=await browser.newContext({viewport:{width:1920,height:1080}}) +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session) +const page=await context.newPage();page.on('pageerror',e=>errors.push(e.message)) +await page.addInitScript(()=>{new MutationObserver(()=>{for(const el of document.querySelectorAll('.ofd-toast,.el-message'))if(!el.dataset.reported){el.dataset.reported='true';console.log('NOTICE',el.textContent)}}).observe(document,{childList:true,subtree:true})}) +page.on('console',m=>{if(m.text().startsWith('NOTICE'))console.log(m.text())}) +page.on('response',r=>{if(r.status()>=400)console.log('HTTP',r.status(),new URL(r.url()).pathname)}) +const shot=async name=>page.screenshot({path:path.join(report,name+'.png'),fullPage:false}) +try{ + const assets=await call(`${base}/deliveries`,token);check('发布版本提供两份正式交付物',assets.data?.length===2) + for(const format of (process.argv.includes('--browser-only')?[]:['OFD','OFFLINE_HTML'])){const r=await context.request.get(`http://127.0.0.1:6180/api/${base}/delivery?format=${format}`,{headers:{Authorization:`Bearer ${token}`},timeout:180000});check('统一文件服务交付物下载 '+format,r.ok());if(r.ok())fs.writeFileSync(path.join(report,format==='OFD'?'formal.ofd':'formal-offline.zip'),await r.body())} + const before=(await call(`${base}/reader-state`,token)).data + const command={...before,pageId:'page:1',pageIndex:1,pageCount:3,favorite:true,notes:[{id:'formal-note',pageId:'page:1',text:'正式版本笔记:核对狐狸模型与卡车资源,注意安全检查。'}]} + const saved=await call(`${base}/reader-state`,token,command,'PUT');check('阅读位置、收藏、笔记真实写入',saved.status===200&&saved.data.favorite&&saved.data.notes.length===1) + const stale=await call(`${base}/reader-state`,token,command,'PUT');check('旧乐观锁版本被拒绝',stale.status===409) + const release=await call(`${base}/reader-state`,token,{...command,versionId:'1'},'PUT');check('旧发布版本不能覆盖当前阅读记录',release.status===409) + const stats=await call(`${base}/reader-statistics`,token);check('收藏和笔记进入真实统计',stats.data?.favorites>=1&&stats.data?.notes>=1) + await page.goto('http://127.0.0.1:6180/teaching/guides') + await page.getByPlaceholder('搜索指导书名称或编码').fill('正式闭环-狐狸与卡车指导书-20260910') + await page.getByPlaceholder('搜索指导书名称或编码').press('Enter') + await expect(page.locator('.guide-card-v2')).toHaveCount(1) + await page.locator('.guide-card-v2 button').first().click() + await expect(page.locator('.legacy-published-reader .ofd-app')).toBeVisible({timeout:60000}) + await expect(page.getByRole('button',{name:'取消收藏',exact:true})).toBeVisible() + await page.getByRole('button',{name:/笔记 1/}).click() + await expect(page.locator('.reader-note')).toContainText('正式版本笔记') + await shot('05-reader-notes') + await page.getByPlaceholder('记录当前页的阅读笔记').fill('浏览器实测新增笔记:页码和内容由正式服务保存。') + const noteResponse=page.waitForResponse(r=>r.url().endsWith('/reader-state')&&r.request().method()==='PUT') + await page.getByRole('button',{name:'添加笔记',exact:true}).click() + check('浏览器笔记保存请求成功',(await noteResponse).ok()) + await page.locator('.el-drawer__close-btn:visible').click() + await page.locator('.legacy-published-reader [data-action="preview-next"]').click() + await page.waitForTimeout(1300);await shot('06-reader-layout') + const persisted=await call(`${base}/reader-state`,token);check('浏览器翻页位置已同步',persisted.data?.pageIndex===2) + await page.locator('.legacy-published-reader [data-action="preview-tab"][data-tab="interactive"]').click() + await page.waitForTimeout(3500);await shot('07-reader-interactive') + await page.locator('.legacy-published-reader canvas').first().scrollIntoViewIfNeeded() + await page.waitForTimeout(1200);await shot('09-reader-model') + check('教学动态阅读显示三维画布',await page.locator('.legacy-published-reader canvas').count()>0) + await page.goto('http://127.0.0.1:6180/content/ofd/api') + await page.getByRole('button',{name:'执行调用',exact:true}).click() + await expect(page.locator('.guide-http-page pre')).toContainText('records') + await shot('08-http-integration') + check('对外接口面板返回真实工程数据',await page.locator('.guide-http-page pre').textContent().then(t=>t.includes('GUIDE-'))) +}catch(e){check('浏览器闭环流程',false,e.message);await shot('failure-review')} +finally{check('浏览器无脚本异常',errors.length===0,errors.join('\n'));fs.writeFileSync(path.join(report,'review.json'),JSON.stringify({checks,errors},null,2));await browser.close()} diff --git a/tools/guide-formal-scenes.mjs b/tools/guide-formal-scenes.mjs new file mode 100644 index 0000000..dea9181 --- /dev/null +++ b/tools/guide-formal-scenes.mjs @@ -0,0 +1,36 @@ +import fs from 'node:fs' +import path from 'node:path' +import {chromium,expect} from '@playwright/test' +import {login,call} from './role-permissions-live.mjs' +const report=path.resolve('reports/guide-formal-20260910'),fixture=JSON.parse(fs.readFileSync('../../.codex-tmp/guide-formal/fixture.json')) +const session=await login('root'),browser=await chromium.launch({channel:'msedge',headless:true,args:['--enable-unsafe-swiftshader']}) +const context=await browser.newContext({viewport:{width:1920,height:1080}}) +await context.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session) +const page=await context.newPage(),checks=[] +const check=(name,pass,detail='')=>{checks.push({name,pass,detail});console.log(pass?'PASS':'FAIL',name);if(!pass)process.exitCode=1} +try{ + await page.goto('http://127.0.0.1:6180/content/ofd') + await expect(page.getByRole('button',{name:'新建指导书',exact:true})).toBeVisible() + const result=await page.evaluate(async id=>{ + const {FormalGuideStore}=await import('/src/features/guide-legacy/formalStore.ts') + const store=new FormalGuideStore() + try{ + await store.load(id);await store.loadCatalog();const old=store.project.publishedVersionId + if(store.project.status==='PUBLISHED')await store.transitionOFDProject(id,'edit',null) + const state=JSON.parse(JSON.stringify(store.record.state)),first=state.chapters[0].steps[0] + first.image='';first.title='狐狸模型巡检' + const truck=state.resources.find(r=>r.name?.includes('MilkTruck')) + const second={...JSON.parse(JSON.stringify(first)),id:'formal-truck-step',title:'牛奶卡车检查',image:'',modelRef:{...first.modelRef,assetId:truck.id,nodeName:''},mediaBlocks:[]} + state.chapters[0].steps=[first,second] + for(const [step,key] of [[first,'160@463'],[second,'159@209']])step.mediaBlocks=[{id:'scene-'+step.id,kind:'scene',assetId:key,title:step.title+'场景',x:20,y:180,width:170,height:80}] + await store.saveOFDProjectState(id,state) + return {old,published:store.project.publishedVersionId,assets:store.project.assets.length,bindings:Object.keys(store.record.state.sceneBindings||{}),version:store.project.version} + }finally{store.dispose()} + },fixture.id) + check('新修订保持原发布版本可阅读',result.old===result.published) + check('狐狸与卡车场景固定版本已保存',result.bindings.length===2,JSON.stringify(result)) + await page.goto(`http://127.0.0.1:6180/content/ofd/${fixture.id}/studio`) + await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) + await page.screenshot({path:path.join(report,'10-two-scene-resources.png')}) +}catch(e){check('正式场景资源闭环',false,e.message);await page.screenshot({path:path.join(report,'failure-scenes.png')})} +finally{fs.writeFileSync(path.join(report,'scenes.json'),JSON.stringify({checks},null,2));await browser.close()} diff --git a/tools/guide-formal-template.mjs b/tools/guide-formal-template.mjs new file mode 100644 index 0000000..109b4eb --- /dev/null +++ b/tools/guide-formal-template.mjs @@ -0,0 +1,28 @@ +import fs from 'node:fs' +import {chromium,expect} from '@playwright/test' +import {login,call} from './role-permissions-live.mjs' +const session=await login('root'),checks=[] +const browser=await chromium.launch({channel:'msedge',headless:true}) +const page=await browser.newPage({viewport:{width:1920,height:1080}}) +await page.addInitScript(s=>{sessionStorage.setItem('unreal-tran:web:access-token:v1',s.accessToken);sessionStorage.setItem('unreal-tran:web:refresh-token:v1',s.refreshToken)},session) +let id +try{ + await page.goto('http://127.0.0.1:6180/content/ofd/templates') + await page.locator('.library-heading button').click() + await page.getByLabel('指导书名称',{exact:true}).fill('正式模板新建回归-20260911') + const created=page.waitForResponse(r=>r.url().includes('/guide-templates/')&&r.url().endsWith('/projects')&&r.request().method()==='POST') + await page.getByRole('button',{name:'创建并进入规划',exact:true}).click() + const response=await created;const body=await response.json();id=String(body.data.project.id) + checks.push({name:'模板新建实际进入正式内容服务',pass:response.ok()}) + await page.waitForURL(/\/ofd\/\d+\/studio/) + await page.goto(`http://127.0.0.1:6180/content/ofd/${id}/studio?view=editor`) + await expect(page.locator('.ofd-view-editor')).toBeVisible({timeout:60000}) + const before=await call(`tran/v1/content/projects/${id}`,session.accessToken) + const save=page.waitForResponse(r=>r.url().endsWith(`/projects/${id}`)&&r.request().method()==='PUT') + await page.locator('[data-action="save"]').click();const saved=await save + checks.push({name:'模板初稿进入编辑器并保存',pass:saved.ok()}) + const after=await call(`tran/v1/content/projects/${id}`,session.accessToken) + checks.push({name:'模板章节和步骤不丢失',pass:JSON.stringify(before.data.currentVersion.content.chapters.map(c=>[c.id,c.title,c.steps.map(s=>[s.id,s.title])]))===JSON.stringify(after.data.currentVersion.content.chapters.map(c=>[c.id,c.title,c.steps.map(s=>[s.id,s.title])]))}) + await page.screenshot({path:'reports/guide-formal-20260910/17-template-editor.png'}) +}catch(e){checks.push({name:'模板编制闭环',pass:false,detail:e.message});process.exitCode=1;await page.screenshot({path:'reports/guide-formal-20260910/template-failure.png'})} +finally{console.log(checks);fs.writeFileSync('reports/guide-formal-20260910/template.json',JSON.stringify({projectId:id,checks},null,2));await browser.close()}