完善联调标准化与诊断链路
This commit is contained in:
220
b2f.md
220
b2f.md
@@ -1,6 +1,6 @@
|
|||||||
# b2f
|
# b2f
|
||||||
> 文档版本:v1.10
|
> 文档版本:v1.19
|
||||||
> 最后更新:2026-04-03 20:10:25
|
> 最后更新:2026-04-03 16:43:25
|
||||||
|
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
@@ -12,6 +12,172 @@
|
|||||||
|
|
||||||
## 待确认
|
## 待确认
|
||||||
|
|
||||||
|
### B2F-032
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:43:25
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已收到 frontend 最新一轮结构化调试日志,并确认积分赛主链已打通
|
||||||
|
- 当前日志事实一致指向:
|
||||||
|
- `entry-home.cardEventIds` 已包含 `evt_demo_score_o_001`
|
||||||
|
- `event-play.pageEventId = evt_demo_score_o_001`
|
||||||
|
- `event-prepare.pageEventId = evt_demo_score_o_001`
|
||||||
|
- `launch.response.releaseId = rel_74bb47a0d0d3d252`
|
||||||
|
- `runtime-compiler.details.game.mode = score-o`
|
||||||
|
- `runtime-compiler.details.playfield.kind = control-set`
|
||||||
|
- 当前 backend 没再看到“明明选积分赛却实际跑成顺序赛”的问题
|
||||||
|
- 当前日志链还有 3 个口径优化项:
|
||||||
|
- 非多赛道玩法时,`assignmentMode` 现在是空字符串 `\"\"`
|
||||||
|
- `variantId` 为空时,`runtimeCourseVariantId` 仍可能有值,前端展示层不要把两者混为“用户选了赛道”
|
||||||
|
- `occurredAt` 和 `receivedAt` 会有轻微时钟漂移,排查顺序时建议增加前端本地递增序号
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 后续日志上报请优化为:
|
||||||
|
- 非多赛道玩法时:
|
||||||
|
- `assignmentMode = null` 或不传
|
||||||
|
- `variantId = null` 或不传
|
||||||
|
- 新增:
|
||||||
|
- `details.seq`
|
||||||
|
- 如需展示赛道来源,请区分:
|
||||||
|
- `launchVariantId`
|
||||||
|
- `runtimeCourseVariantId`
|
||||||
|
- 是否已解决:否
|
||||||
|
|
||||||
|
### B2F-031
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:37:41
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已收到 frontend 新增的结构化调试日志
|
||||||
|
- 这批日志已经明确说明:本轮前端实际进入的是经典顺序赛,不是积分赛
|
||||||
|
- 关键事实如下:
|
||||||
|
- `entry-home.cardEventIds = [evt_demo_001, evt_demo_variant_manual_001]`
|
||||||
|
- `event-play.pageEventId = evt_demo_001`
|
||||||
|
- `event-prepare.pageEventId = evt_demo_001`
|
||||||
|
- `launch.response.releaseId = rel_demo_001`
|
||||||
|
- `runtime-compiler.details.game.mode = classic-sequential`
|
||||||
|
- backend 已确认根因不是积分赛 release 缺失,而是 demo 首页卡片入口配置错误:
|
||||||
|
- 当前首页卡片查询只取 `home_primary`
|
||||||
|
- score-o demo 卡此前被种到了 `home_secondary`
|
||||||
|
- 所以前端首页根本拿不到积分赛入口
|
||||||
|
- backend 已修复:
|
||||||
|
- `card_demo_score_o_001` 改为 `home_primary`
|
||||||
|
- 同时把优先级调高到 `98`
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 请在 backend 重启后重新执行:
|
||||||
|
- `Bootstrap Demo`
|
||||||
|
- 进入首页再看 `cardEventIds`
|
||||||
|
- 并确认首页卡片中是否已经出现:
|
||||||
|
- `evt_demo_score_o_001`
|
||||||
|
- 是否已解决:否
|
||||||
|
|
||||||
|
### B2F-030
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:16:38
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已新增 dev 调试接口:
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
- `GET /dev/client-logs`
|
||||||
|
- `DELETE /dev/client-logs`
|
||||||
|
- workbench 已新增:
|
||||||
|
- `前端调试日志`
|
||||||
|
- `拉取前端日志`
|
||||||
|
- `清空前端日志`
|
||||||
|
- 这套能力只用于联调,不参与正式生产日志链路
|
||||||
|
- backend 当前建议 frontend 在关键阶段主动上报日志,至少覆盖:
|
||||||
|
- launch 返回后
|
||||||
|
- 地图页拿到最终 manifest 后
|
||||||
|
- 运行时编译完成后
|
||||||
|
- 发现缓存命中 / 恢复 session / 模式不符时
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 请按最小字段约定接入并回传一轮:
|
||||||
|
- `source`
|
||||||
|
- `level`
|
||||||
|
- `category`
|
||||||
|
- `message`
|
||||||
|
- `eventId`
|
||||||
|
- `releaseId`
|
||||||
|
- `sessionId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `route`
|
||||||
|
- `details.schemaVersion`
|
||||||
|
- `details.playfield.kind`
|
||||||
|
- `details.game.mode`
|
||||||
|
- `details.phase`
|
||||||
|
- 是否已解决:否
|
||||||
|
|
||||||
|
### B2F-029
|
||||||
|
|
||||||
|
- 时间:2026-04-03 15:44:32
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已确认一个具体问题:
|
||||||
|
- 所有“带 `Bootstrap Demo` 的一键按钮”,此前都会先把默认 demo 数据重新写回表单
|
||||||
|
- 这会把前面已经选好的积分赛入口再次冲回顺序赛默认链
|
||||||
|
- 受影响的不只是:
|
||||||
|
- `整条链一键验收`
|
||||||
|
- 还包括:
|
||||||
|
- `看首页是否正常`
|
||||||
|
- `快速进一局`
|
||||||
|
- `发布活动配置(自动补 Runtime)`
|
||||||
|
- backend 已修复为:
|
||||||
|
- 这些一键流在调用 `Bootstrap Demo` 后,会按当前已选中的 `event` 回填对应的 `source / build / release / runtime`
|
||||||
|
- 不再无条件回退到 `evt_demo_001`
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 请重启 backend 后复验:
|
||||||
|
- `Use Score-O Demo`
|
||||||
|
- `整条链一键验收`
|
||||||
|
- 并确认地图信息面板里是否已切到:
|
||||||
|
- `eventPublicID = evt_demo_score_o_001`
|
||||||
|
- `game.mode = score-o`
|
||||||
|
- `playfield.kind = control-set`
|
||||||
|
- 是否已解决:否
|
||||||
|
|
||||||
|
### B2F-028
|
||||||
|
|
||||||
|
- 时间:2026-04-03 15:29:07
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已核对积分赛 demo 当前发布链,以下 3 层结果一致:
|
||||||
|
- 本地源配置 [score-o.json](D:/dev/cmr-mini/event/score-o.json)
|
||||||
|
- OSS 配置 [score-o.json](https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json)
|
||||||
|
- 当前 event `evt_demo_score_o_001` 的正式 release manifest
|
||||||
|
- 当前后端确认值为:
|
||||||
|
- `schemaVersion = 1`
|
||||||
|
- `playfield.kind = control-set`
|
||||||
|
- `game.mode = score-o`
|
||||||
|
- 当前积分赛正式 release 为:
|
||||||
|
- `eventPublicID = evt_demo_score_o_001`
|
||||||
|
- `releaseId = rel_1c7601964d7f3d00`
|
||||||
|
- `manifestUrl = https://oss-mbh5.colormaprun.com/gotomars/event/releases/evt_demo_score_o_001/rel_1c7601964d7f3d00/manifest.json`
|
||||||
|
- 如果 frontend 当前跑出来仍是顺序赛,优先怀疑“实际消费的不是当前 launch 返回的 manifest”,或“运行时仍走旧缓存/旧默认逻辑”
|
||||||
|
- backend 已在 workbench 新增:
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
|
- 该摘要会由 backend 代读当前 launch 对应 manifest,并直接显示:
|
||||||
|
- `configUrl`
|
||||||
|
- `releaseId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `schemaVersion`
|
||||||
|
- `playfield.kind`
|
||||||
|
- `game.mode`
|
||||||
|
- 这块摘要只用于联调排查,不参与正式客户端运行链路
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 请先排查并回传这 4 项事实:
|
||||||
|
- `POST /events/{eventPublicID}/launch` 响应里的:
|
||||||
|
- `launch.config.configUrl`
|
||||||
|
- `launch.resolvedRelease.manifestUrl`
|
||||||
|
- `launch.config.releaseId`
|
||||||
|
- 地图页/运行时真正读取的最终 manifest URL
|
||||||
|
- 最终加载后的 manifest 摘要:
|
||||||
|
- `schemaVersion`
|
||||||
|
- `playfield.kind`
|
||||||
|
- `game.mode`
|
||||||
|
- 如仍表现为顺序赛,请同时给出:
|
||||||
|
- 控制台日志
|
||||||
|
- 网络请求日志
|
||||||
|
- 是否存在本地缓存/上次 session 恢复痕迹
|
||||||
|
- 是否已解决:否
|
||||||
|
|
||||||
### B2F-001
|
### B2F-001
|
||||||
|
|
||||||
- 时间:2026-04-01
|
- 时间:2026-04-01
|
||||||
@@ -103,6 +269,56 @@
|
|||||||
|
|
||||||
## 已确认
|
## 已确认
|
||||||
|
|
||||||
|
### B2F-027
|
||||||
|
|
||||||
|
- 时间:2026-04-03 14:37:00
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- workbench 已提供 3 个显式玩法测试入口:
|
||||||
|
- `Use Classic Demo`
|
||||||
|
- `Use Score-O Demo`
|
||||||
|
- `Use Manual Variant Demo`
|
||||||
|
- 对应联调 event 为:
|
||||||
|
- `evt_demo_001`
|
||||||
|
- `evt_demo_score_o_001`
|
||||||
|
- `evt_demo_variant_manual_001`
|
||||||
|
- 积分赛入口已固定到:
|
||||||
|
- `rel_demo_score_o_001`
|
||||||
|
- `score-o.json`
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 后续若要测顺序赛或积分赛,优先使用上述显式入口,而不是自行猜 event/release
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
|
### B2F-026
|
||||||
|
|
||||||
|
- 时间:2026-04-03 14:29:42
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已把 manual 多赛道 demo 的赛道输入切到真实 KML
|
||||||
|
- 当前 `Bootstrap Demo` 会准备两条真实赛道输入:
|
||||||
|
- `variant_a -> c01.kml`
|
||||||
|
- `variant_b -> c02.kml`
|
||||||
|
- 当前地图仍继续共用同一组真实 tiles / mapmeta,这符合当前多赛道联调阶段的实际需要
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 如需回归 manual 多赛道,请先重新执行一次 `Bootstrap Demo`
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
|
### B2F-025
|
||||||
|
|
||||||
|
- 时间:2026-04-03 14:21:24
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已开始“真实输入替换第一刀”
|
||||||
|
- 当前 `Bootstrap Demo` 不再给生产骨架使用 `example.com` 占位赛道/地图地址
|
||||||
|
- 当前已改成真实可访问输入:
|
||||||
|
- `CourseSource.fileUrl = https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml`
|
||||||
|
- `TileRelease.tileBaseUrl = https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/`
|
||||||
|
- `TileRelease.metaUrl = https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json`
|
||||||
|
- 这批真实输入仍走同一条一键回归链,不要求 frontend 改联调方式
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- frontend 如需复验真实输入,请重新执行一次 `Bootstrap Demo`
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
### B2F-024
|
### B2F-024
|
||||||
|
|
||||||
- 时间:2026-04-03 20:10:25
|
- 时间:2026-04-03 20:10:25
|
||||||
|
|||||||
93
b2t.md
93
b2t.md
@@ -1,6 +1,6 @@
|
|||||||
# B2T 协作清单
|
# B2T 协作清单
|
||||||
> 文档版本:v1.13
|
> 文档版本:v1.18
|
||||||
> 最后更新:2026-04-03 13:24:38
|
> 最后更新:2026-04-03 16:16:38
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
@@ -36,6 +36,60 @@
|
|||||||
|
|
||||||
## 已确认
|
## 已确认
|
||||||
|
|
||||||
|
### B2T-028
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:16:38
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已新增 dev 调试日志入口:
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
- `GET /dev/client-logs`
|
||||||
|
- `DELETE /dev/client-logs`
|
||||||
|
- workbench 已新增 `前端调试日志` 面板,用于直接查看 frontend 主动上报的关键事实
|
||||||
|
- 这套能力只服务联调排查,不替代正式生产日志体系
|
||||||
|
- 当前目标是把“截图 + 口头描述”进一步收口成结构化事实,便于 backend 直接定位前端消费链问题
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
|
### B2T-027
|
||||||
|
|
||||||
|
- 时间:2026-04-03 15:29:07
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已在 workbench 新增:
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
|
- 该摘要由 backend 代读当前 launch 对应 manifest,用于统一显示:
|
||||||
|
- `configUrl`
|
||||||
|
- `releaseId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `schemaVersion`
|
||||||
|
- `playfield.kind`
|
||||||
|
- `game.mode`
|
||||||
|
- 该能力只用于联调排查,避免浏览器直接读取 OSS 时受跨域影响
|
||||||
|
- 正式客户端链路不变,仍应直接消费:
|
||||||
|
- `launch.config.configUrl`
|
||||||
|
- `launch.resolvedRelease.manifestUrl`
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
|
### B2T-026
|
||||||
|
|
||||||
|
- 时间:2026-04-03 14:37:00
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已把当前两种核心玩法和 manual 多赛道都挂成显式 demo 入口:
|
||||||
|
- 顺序赛:`evt_demo_001`
|
||||||
|
- 积分赛:`evt_demo_score_o_001`
|
||||||
|
- 多赛道:`evt_demo_variant_manual_001`
|
||||||
|
- 其中积分赛入口已固定到远端发布配置:
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json`
|
||||||
|
- workbench 当前可直接切换三种玩法做标准联调
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
### B2T-001
|
### B2T-001
|
||||||
|
|
||||||
- 时间:2026-04-03 08:52:11
|
- 时间:2026-04-03 08:52:11
|
||||||
@@ -130,6 +184,41 @@
|
|||||||
|
|
||||||
## 已完成
|
## 已完成
|
||||||
|
|
||||||
|
### B2T-024
|
||||||
|
|
||||||
|
- 时间:2026-04-03 14:21:24
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已按 [t2b.md](D:/dev/cmr-mini/t2b.md) v1.11 开始“真实输入替换第一刀”
|
||||||
|
- 当前已优先替换:
|
||||||
|
- 真实 KML / 赛道文件
|
||||||
|
- 真实地图资源 URL
|
||||||
|
- `Bootstrap Demo` 当前最小生产骨架已改用:
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml`
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/`
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json`
|
||||||
|
- 当前仍保持同一条标准联调入口,不新增新流程
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
|
### B2T-025
|
||||||
|
|
||||||
|
- 时间:2026-04-03 14:29:42
|
||||||
|
- 谁提的:backend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已继续推进“真实输入替换第一刀”的 manual 多赛道部分
|
||||||
|
- 当前 `Bootstrap Demo` 已为 demo course set 准备两条真实 KML 输入:
|
||||||
|
- `c01.kml`
|
||||||
|
- `c02.kml`
|
||||||
|
- 当前 manual 多赛道 demo 的说明已同步为:
|
||||||
|
- `variant_a -> c01.kml`
|
||||||
|
- `variant_b -> c02.kml`
|
||||||
|
- 当前仍沿用同一条标准联调入口,不新增新流程
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
### B2T-023
|
### B2T-023
|
||||||
|
|
||||||
- 时间:2026-04-03 13:24:38
|
- 时间:2026-04-03 13:24:38
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Backend
|
# Backend
|
||||||
> 文档版本:v1.12
|
> 文档版本:v1.17
|
||||||
> 最后更新:2026-04-03 13:24:38
|
> 最后更新:2026-04-03 16:16:38
|
||||||
|
|
||||||
|
|
||||||
这套后端现在已经能支撑一条完整主链:
|
这套后端现在已经能支撑一条完整主链:
|
||||||
@@ -14,6 +14,27 @@
|
|||||||
- 真正进入游戏时客户端消费的是 `manifest_url`
|
- 真正进入游戏时客户端消费的是 `manifest_url`
|
||||||
- `session` 会固化当时实际绑定的 `release`
|
- `session` 会固化当时实际绑定的 `release`
|
||||||
|
|
||||||
|
当前 workbench 里新增的“当前 Launch 实际配置摘要”仅用于调试:
|
||||||
|
|
||||||
|
- 它会由 backend 代读当前 launch 对应的 manifest
|
||||||
|
- 用来显示:
|
||||||
|
- `configUrl`
|
||||||
|
- `releaseId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `schemaVersion`
|
||||||
|
- `playfield.kind`
|
||||||
|
- `game.mode`
|
||||||
|
- 这块只服务联调排查,不参与正式客户端运行链路
|
||||||
|
- 正式客户端仍应直接消费 `launch` 返回的:
|
||||||
|
- `launch.config.configUrl`
|
||||||
|
- `launch.resolvedRelease.manifestUrl`
|
||||||
|
|
||||||
|
当前 workbench 里新增的“前端调试日志”也仅用于联调:
|
||||||
|
|
||||||
|
- frontend 可将页面侧调试日志 `POST` 到 `/dev/client-logs`
|
||||||
|
- backend 会临时保留最近 200 条日志,供 workbench 查看与清空
|
||||||
|
- 这块只用于联调排查,不替代正式生产日志体系
|
||||||
|
|
||||||
## 文档导航
|
## 文档导航
|
||||||
|
|
||||||
- [文档索引](D:/dev/cmr-mini/backend/docs/README.md)
|
- [文档索引](D:/dev/cmr-mini/backend/docs/README.md)
|
||||||
@@ -64,5 +85,15 @@ cd D:\dev\cmr-mini\backend
|
|||||||
- Bootstrap Demo 自动回填最小生产骨架 ID
|
- Bootstrap Demo 自动回填最小生产骨架 ID
|
||||||
- 一键测试环境:可从空白状态自动准备 demo event、source/build/release、presentation、content bundle、place、map asset、tile release、course source、course set、course variant、runtime binding,并输出逐步日志与预期判定
|
- 一键测试环境:可从空白状态自动准备 demo event、source/build/release、presentation、content bundle、place、map asset、tile release、course source、course set、course variant、runtime binding,并输出逐步日志与预期判定
|
||||||
- 一键标准回归:在标准发布链跑通后,继续自动验证 `play / launch / result / history`
|
- 一键标准回归:在标准发布链跑通后,继续自动验证 `play / launch / result / history`
|
||||||
|
- 真实输入替换第一刀:`Bootstrap Demo` 已改用真实可访问的 KML 与地图资源 URL
|
||||||
|
- manual 多赛道 demo:已切到真实 `c01.kml / c02.kml` 输入
|
||||||
|
- 前端调试日志:
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
- `GET /dev/client-logs`
|
||||||
|
- `DELETE /dev/client-logs`
|
||||||
|
- 显式玩法入口:
|
||||||
|
- 顺序赛:`evt_demo_001`
|
||||||
|
- 积分赛:`evt_demo_score_o_001`
|
||||||
|
- 多赛道:`evt_demo_variant_manual_001`
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 开发说明
|
# 开发说明
|
||||||
> 文档版本:v1.14
|
> 文档版本:v1.20
|
||||||
> 最后更新:2026-04-03 20:10:25
|
> 最后更新:2026-04-03 16:16:38
|
||||||
|
|
||||||
|
|
||||||
## 1. 环境变量
|
## 1. 环境变量
|
||||||
@@ -39,6 +39,35 @@ cd D:\dev\cmr-mini\backend
|
|||||||
.\scripts\start-dev.ps1
|
.\scripts\start-dev.ps1
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 3. Workbench 当前重点
|
||||||
|
|
||||||
|
- 推荐联调入口:
|
||||||
|
- `Bootstrap Demo`
|
||||||
|
- `Use Classic Demo / Use Score-O Demo / Use Manual Variant Demo`
|
||||||
|
- `整条链一键验收`
|
||||||
|
- 如果 frontend 需要把页面侧调试日志直接打到 backend,优先使用:
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
- 然后在 workbench 的 `前端调试日志` 面板里查看
|
||||||
|
- 如果需要判断前端到底拿到了哪份配置,优先看 workbench 的:
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
|
- 这块会直接显示:
|
||||||
|
- `configUrl`
|
||||||
|
- `releaseId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `schemaVersion`
|
||||||
|
- `playfield.kind`
|
||||||
|
- `game.mode`
|
||||||
|
- 这组信息用于和前端地图页实际消费结果对口排查,避免只靠口头描述“像顺序赛/像积分赛”。
|
||||||
|
- 注意:
|
||||||
|
- 这块摘要由 backend 代读 manifest,只用于 workbench 调试
|
||||||
|
- 这样做是为了避免浏览器直接读取 OSS 时受跨域影响
|
||||||
|
- 它不替代正式客户端加载逻辑
|
||||||
|
- 正式客户端仍必须直接消费 `launch.config.configUrl` 或 `launch.resolvedRelease.manifestUrl`
|
||||||
|
- `前端调试日志` 也是调试专用能力:
|
||||||
|
- backend 当前只在内存里保留最近 200 条
|
||||||
|
- 适合前端把关键事实直接打进来,避免只靠截图和口头描述
|
||||||
|
- 不替代正式生产日志体系
|
||||||
|
|
||||||
默认会设置:
|
默认会设置:
|
||||||
|
|
||||||
- `APP_ENV=development`
|
- `APP_ENV=development`
|
||||||
@@ -72,8 +101,12 @@ cd D:\dev\cmr-mini\backend
|
|||||||
当前推荐顺序:
|
当前推荐顺序:
|
||||||
|
|
||||||
1. `Bootstrap Demo`
|
1. `Bootstrap Demo`
|
||||||
2. `一键补齐 Runtime 并发布`
|
2. 选择一种玩法入口:
|
||||||
3. `一键标准回归`
|
- `Use Classic Demo`
|
||||||
|
- `Use Score-O Demo`
|
||||||
|
- `Use Manual Variant Demo`
|
||||||
|
3. `一键补齐 Runtime 并发布`
|
||||||
|
4. `一键标准回归`
|
||||||
|
|
||||||
当前这条一键链会自动完成:
|
当前这条一键链会自动完成:
|
||||||
|
|
||||||
@@ -94,6 +127,20 @@ cd D:\dev\cmr-mini\backend
|
|||||||
- `play / launch / result / history` 回归汇总
|
- `play / launch / result / history` 回归汇总
|
||||||
- demo 活动残留 ongoing session 清理:
|
- demo 活动残留 ongoing session 清理:
|
||||||
- 会把 demo event 下历史遗留的 `launched / running` session 自动改成 `cancelled`
|
- 会把 demo event 下历史遗留的 `launched / running` session 自动改成 `cancelled`
|
||||||
|
- 真实输入替换第一刀:
|
||||||
|
- `CourseSource.fileUrl` 当前已切到真实 KML:
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml`
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c02.kml`
|
||||||
|
- `TileRelease.tileBaseUrl / metaUrl` 当前已切到真实地图资源:
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/`
|
||||||
|
- `https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json`
|
||||||
|
- manual 多赛道 demo 当前已使用两条真实赛道输入:
|
||||||
|
- `variant_a -> c01.kml`
|
||||||
|
- `variant_b -> c02.kml`
|
||||||
|
- 显式玩法测试入口:
|
||||||
|
- 顺序赛:`evt_demo_001 -> rel_demo_001 -> classic-sequential.json`
|
||||||
|
- 积分赛:`evt_demo_score_o_001 -> rel_demo_score_o_001 -> score-o.json`
|
||||||
|
- 多赛道:`evt_demo_variant_manual_001 -> rel_demo_variant_manual_001`
|
||||||
|
|
||||||
当前日志能力:
|
当前日志能力:
|
||||||
|
|
||||||
@@ -116,6 +163,60 @@ cd D:\dev\cmr-mini\backend
|
|||||||
- `History`
|
- `History`
|
||||||
- `Session ID`
|
- `Session ID`
|
||||||
- `总判定`
|
- `总判定`
|
||||||
|
- workbench 现在还支持查看 frontend 主动上报的调试日志:
|
||||||
|
- `拉取前端日志`
|
||||||
|
- `清空前端日志`
|
||||||
|
- 前端建议最少带:
|
||||||
|
- `eventId`
|
||||||
|
- `releaseId`
|
||||||
|
- `sessionId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `route`
|
||||||
|
- `game.mode`
|
||||||
|
- `playfield.kind`
|
||||||
|
- 当前页面阶段或动作名
|
||||||
|
|
||||||
|
### 2.2 前端调试日志最小约定
|
||||||
|
|
||||||
|
dev 环境下,frontend 可直接把关键调试事实发到 backend:
|
||||||
|
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
|
||||||
|
建议请求体最少包含:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"source": "miniprogram",
|
||||||
|
"level": "info",
|
||||||
|
"category": "runtime",
|
||||||
|
"message": "map page loaded manifest",
|
||||||
|
"eventId": "evt_demo_score_o_001",
|
||||||
|
"releaseId": "rel_xxx",
|
||||||
|
"sessionId": "sess_xxx",
|
||||||
|
"manifestUrl": "https://oss-mbh5.colormaprun.com/...",
|
||||||
|
"route": "pages/map/map",
|
||||||
|
"occurredAt": "2026-04-03T16:16:38+08:00",
|
||||||
|
"details": {
|
||||||
|
"schemaVersion": "1",
|
||||||
|
"playfield.kind": "control-set",
|
||||||
|
"game.mode": "score-o",
|
||||||
|
"phase": "map-init"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前说明:
|
||||||
|
|
||||||
|
- `source`:建议填终端来源,例如 `miniprogram`
|
||||||
|
- `level`:建议填 `info / warn / error`
|
||||||
|
- `category`:建议填 `launch / runtime / cache / network`
|
||||||
|
- `message`:一句话说明当前发生了什么
|
||||||
|
- `details`:放结构化调试细节,backend 原样收下
|
||||||
|
|
||||||
|
辅助接口:
|
||||||
|
|
||||||
|
- `GET /dev/client-logs?limit=50`
|
||||||
|
- `DELETE /dev/client-logs`
|
||||||
|
|
||||||
## 3. 当前开发约定
|
## 3. 当前开发约定
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# API 清单
|
# API 清单
|
||||||
> 文档版本:v1.8
|
> 文档版本:v1.9
|
||||||
> 最后更新:2026-04-03 12:36:15
|
> 最后更新:2026-04-03 16:16:38
|
||||||
|
|
||||||
|
|
||||||
本文档只记录当前 backend 已实现接口,不写未来规划接口。
|
本文档只记录当前 backend 已实现接口,不写未来规划接口。
|
||||||
@@ -436,6 +436,61 @@
|
|||||||
|
|
||||||
- 自动准备 demo tenant / channel / event / release / card
|
- 自动准备 demo tenant / channel / event / release / card
|
||||||
|
|
||||||
|
### `POST /dev/client-logs`
|
||||||
|
|
||||||
|
环境:
|
||||||
|
|
||||||
|
- 仅 non-production
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 接收 frontend 主动上报的调试日志
|
||||||
|
- 供 backend 通过 workbench 统一查看和排查
|
||||||
|
|
||||||
|
请求体重点:
|
||||||
|
|
||||||
|
- `source`
|
||||||
|
- `level`
|
||||||
|
- `category`
|
||||||
|
- `message`
|
||||||
|
- `eventId`
|
||||||
|
- `releaseId`
|
||||||
|
- `sessionId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `route`
|
||||||
|
- `occurredAt`
|
||||||
|
- `details`
|
||||||
|
|
||||||
|
补充说明:
|
||||||
|
|
||||||
|
- 当前只保存在内存中
|
||||||
|
- 默认最多保留最近 200 条
|
||||||
|
- 仅用于联调调试
|
||||||
|
|
||||||
|
### `GET /dev/client-logs`
|
||||||
|
|
||||||
|
环境:
|
||||||
|
|
||||||
|
- 仅 non-production
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 获取 frontend 最近上报的调试日志
|
||||||
|
|
||||||
|
查询参数:
|
||||||
|
|
||||||
|
- `limit`
|
||||||
|
|
||||||
|
### `DELETE /dev/client-logs`
|
||||||
|
|
||||||
|
环境:
|
||||||
|
|
||||||
|
- 仅 non-production
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 清空当前内存中的 frontend 调试日志
|
||||||
|
|
||||||
### `GET /dev/workbench`
|
### `GET /dev/workbench`
|
||||||
|
|
||||||
环境:
|
环境:
|
||||||
@@ -459,6 +514,26 @@
|
|||||||
- scenarios
|
- scenarios
|
||||||
- request history
|
- request history
|
||||||
- curl 导出
|
- curl 导出
|
||||||
|
- frontend 调试日志查看/清空
|
||||||
|
|
||||||
|
### `GET /dev/manifest-summary`
|
||||||
|
|
||||||
|
环境:
|
||||||
|
|
||||||
|
- 仅 non-production
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 由 backend 代读指定 manifest
|
||||||
|
- 返回最小调试摘要:
|
||||||
|
- `schemaVersion`
|
||||||
|
- `playfield.kind`
|
||||||
|
- `game.mode`
|
||||||
|
|
||||||
|
补充说明:
|
||||||
|
|
||||||
|
- 只用于 workbench 联调排查
|
||||||
|
- 不参与正式客户端运行链路
|
||||||
|
|
||||||
### `GET /dev/config/local-files`
|
### `GET /dev/config/local-files`
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
neturl "net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
"cmr-backend/internal/httpx"
|
"cmr-backend/internal/httpx"
|
||||||
"cmr-backend/internal/service"
|
"cmr-backend/internal/service"
|
||||||
@@ -24,6 +28,59 @@ func (h *DevHandler) BootstrapDemo(w http.ResponseWriter, r *http.Request) {
|
|||||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": result})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *DevHandler) CreateClientLog(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.devService.Enabled() {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var input service.CreateClientDebugLogInput
|
||||||
|
if err := httpx.DecodeJSON(r, &input); err != nil {
|
||||||
|
httpx.WriteError(w, fmt.Errorf("decode client log: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entry, err := h.devService.AddClientDebugLog(r.Context(), input)
|
||||||
|
if err != nil {
|
||||||
|
httpx.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": entry})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DevHandler) ListClientLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.devService.Enabled() {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
limit := 50
|
||||||
|
if raw := r.URL.Query().Get("limit"); raw != "" {
|
||||||
|
var parsed int
|
||||||
|
if _, err := fmt.Sscanf(raw, "%d", &parsed); err == nil {
|
||||||
|
limit = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
items, err := h.devService.ListClientDebugLogs(r.Context(), limit)
|
||||||
|
if err != nil {
|
||||||
|
httpx.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": items})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *DevHandler) ClearClientLogs(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.devService.Enabled() {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.devService.ClearClientDebugLogs(r.Context()); err != nil {
|
||||||
|
httpx.WriteError(w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": map[string]any{"cleared": true}})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *DevHandler) Workbench(w http.ResponseWriter, r *http.Request) {
|
func (h *DevHandler) Workbench(w http.ResponseWriter, r *http.Request) {
|
||||||
if !h.devService.Enabled() {
|
if !h.devService.Enabled() {
|
||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
@@ -34,6 +91,75 @@ func (h *DevHandler) Workbench(w http.ResponseWriter, r *http.Request) {
|
|||||||
_, _ = w.Write([]byte(devWorkbenchHTML))
|
_, _ = w.Write([]byte(devWorkbenchHTML))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *DevHandler) ManifestSummary(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !h.devService.Enabled() {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
rawURL := r.URL.Query().Get("url")
|
||||||
|
if rawURL == "" {
|
||||||
|
httpx.WriteError(w, fmt.Errorf("manifest summary url is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := neturl.Parse(rawURL)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
|
httpx.WriteError(w, fmt.Errorf("invalid manifest url"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
resp, err := client.Get(parsed.String())
|
||||||
|
if err != nil {
|
||||||
|
httpx.WriteError(w, fmt.Errorf("fetch manifest: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
httpx.WriteError(w, fmt.Errorf("fetch manifest: http %d", resp.StatusCode))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var manifest map[string]any
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&manifest); err != nil {
|
||||||
|
httpx.WriteError(w, fmt.Errorf("decode manifest: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summary := map[string]any{
|
||||||
|
"url": parsed.String(),
|
||||||
|
"schemaVersion": pickString(manifest["schemaVersion"]),
|
||||||
|
"playfieldKind": pickNestedString(manifest, "playfield", "kind"),
|
||||||
|
"gameMode": pickNestedString(manifest, "game", "mode"),
|
||||||
|
}
|
||||||
|
httpx.WriteJSON(w, http.StatusOK, map[string]any{"data": summary})
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickString(v any) string {
|
||||||
|
switch t := v.(type) {
|
||||||
|
case string:
|
||||||
|
return t
|
||||||
|
case float64:
|
||||||
|
return fmt.Sprintf("%.0f", t)
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pickNestedString(m map[string]any, parent, child string) string {
|
||||||
|
value, ok := m[parent]
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
nested, ok := value.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return pickString(nested[child])
|
||||||
|
}
|
||||||
|
|
||||||
const devWorkbenchHTML = `<!doctype html>
|
const devWorkbenchHTML = `<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
@@ -191,6 +317,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
align-items: start;
|
||||||
}
|
}
|
||||||
.stack {
|
.stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -203,6 +330,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
|
align-content: start;
|
||||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
|
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
|
||||||
}
|
}
|
||||||
.panel h2 {
|
.panel h2 {
|
||||||
@@ -261,6 +389,46 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
border: 1px solid var(--line);
|
border: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
|
.btn-stack {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 38px;
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
background: rgba(255,255,255,0.16);
|
||||||
|
color: #08231a;
|
||||||
|
}
|
||||||
|
.btn-badge.home {
|
||||||
|
background: rgba(79, 209, 165, 0.22);
|
||||||
|
color: #083226;
|
||||||
|
}
|
||||||
|
.btn-badge.game {
|
||||||
|
background: rgba(255, 209, 102, 0.3);
|
||||||
|
color: #3c2a00;
|
||||||
|
}
|
||||||
|
.btn-badge.publish {
|
||||||
|
background: rgba(125, 211, 252, 0.28);
|
||||||
|
color: #082a43;
|
||||||
|
}
|
||||||
|
.btn-badge.verify {
|
||||||
|
background: rgba(251, 146, 60, 0.3);
|
||||||
|
color: #482100;
|
||||||
|
}
|
||||||
|
.btn-badge.recommend {
|
||||||
|
background: rgba(248, 113, 113, 0.28);
|
||||||
|
color: #4a1111;
|
||||||
|
}
|
||||||
.actions {
|
.actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -469,19 +637,22 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
|
|
||||||
<div class="category-head" id="nav-main" data-modes="frontend config">
|
<div class="category-head" id="nav-main" data-modes="frontend config">
|
||||||
<div class="category-kicker">Main Flow</div>
|
<div class="category-kicker">Main Flow</div>
|
||||||
<h2>联调主区</h2>
|
<h2>第一步:选玩法与准备数据</h2>
|
||||||
<p>前台联调和配置发布最常用的入口都在这里。先跑通用户主链,再处理配置与发布。</p>
|
<p>先在这里选当前要测的玩法,workbench 后面的发布链、launch、result、history 都会复用这里的 event。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid">
|
<div class="grid">
|
||||||
<section class="panel" data-modes="frontend config admin">
|
<section class="panel" data-modes="frontend config admin">
|
||||||
<h2>准备 Demo 数据</h2>
|
<h2>第一步:选玩法</h2>
|
||||||
<p>初始化 demo tenant / channel / event / card。</p>
|
<p>先在这里选玩法入口。顺序赛、积分赛、多赛道各有一套独立 demo 数据,后面一键流程都会复用这里选中的 event。</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="btn-bootstrap">Bootstrap Demo</button>
|
<button id="btn-bootstrap"><span class="btn-stack"><span class="btn-badge recommend">先点</span><span>Bootstrap Demo</span></span></button>
|
||||||
<button class="secondary" id="btn-use-variant-manual-demo">Use Manual Variant Demo</button>
|
<button class="secondary" id="btn-use-classic-demo"><span class="btn-stack"><span class="btn-badge home">顺序赛</span><span>Use Classic Demo</span></span></button>
|
||||||
|
<button class="secondary" id="btn-use-score-o-demo"><span class="btn-stack"><span class="btn-badge home">积分赛</span><span>Use Score-O Demo</span></span></button>
|
||||||
|
<button class="secondary" id="btn-use-variant-manual-demo"><span class="btn-stack"><span class="btn-badge home">多赛道</span><span>Use Manual Variant Demo</span></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="kv">
|
<div class="kv">
|
||||||
<div>默认入口 <code id="bootstrap-entry">tenant_demo / mini-demo / evt_demo_001</code></div>
|
<div>默认入口 <code id="bootstrap-entry">tenant_demo / mini-demo / evt_demo_001</code></div>
|
||||||
|
<div>积分赛入口 <code id="bootstrap-score-o-entry">tenant_demo / mini-demo / evt_demo_score_o_001</code></div>
|
||||||
<div>多赛道入口 <code id="bootstrap-variant-entry">tenant_demo / mini-demo / evt_demo_variant_manual_001</code></div>
|
<div>多赛道入口 <code id="bootstrap-variant-entry">tenant_demo / mini-demo / evt_demo_variant_manual_001</code></div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -720,23 +891,30 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
|
|
||||||
<div class="category-head" id="nav-fast" data-modes="frontend config admin">
|
<div class="category-head" id="nav-fast" data-modes="frontend config admin">
|
||||||
<div class="category-kicker">Fast Path</div>
|
<div class="category-kicker">Fast Path</div>
|
||||||
<h2>快捷操作</h2>
|
<h2>第二步:选测试目标</h2>
|
||||||
<p>当你只是想验证“能不能跑通”,优先使用这一组。</p>
|
<p>玩法选好以后,再决定你现在要测首页、发布链、局内流程,还是整条链一次验收。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="grid" style="margin-top:16px;" data-modes="frontend config admin">
|
<div class="grid" style="margin-top:16px; grid-template-columns:minmax(380px,1.2fr) minmax(320px,0.8fr);" data-modes="frontend config admin">
|
||||||
<section class="panel" data-modes="frontend config admin">
|
<section class="panel" data-modes="frontend config admin">
|
||||||
<h2>一键流程</h2>
|
<h2>第二步:点测试目标</h2>
|
||||||
<p>把常用接口串成一键工作流,减少重复点击。</p>
|
<p>先选玩法入口,再按“你现在想测什么”点对应按钮。大多数情况下,你只需要点最后一个“一键标准回归”。</p>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button id="btn-flow-home">Bootstrap + WeChat + Entry Home</button>
|
<button id="btn-flow-home"><span class="btn-stack"><span class="btn-badge home">首页</span><span>看首页是否正常</span></span></button>
|
||||||
<button class="secondary" id="btn-flow-launch">Login + Launch + Start</button>
|
<button class="secondary" id="btn-flow-launch"><span class="btn-stack"><span class="btn-badge game">局内</span><span>快速进一局</span></span></button>
|
||||||
<button class="ghost" id="btn-flow-finish">Finish Current Session</button>
|
<button class="ghost" id="btn-flow-finish"><span class="btn-stack"><span class="btn-badge game">局内</span><span>结束当前这一局</span></span></button>
|
||||||
<button class="ghost" id="btn-flow-result">Finish + Result</button>
|
<button class="ghost" id="btn-flow-result"><span class="btn-stack"><span class="btn-badge game">结果</span><span>结束并看结果</span></span></button>
|
||||||
<button class="secondary" id="btn-flow-admin-default-publish">一键默认绑定发布</button>
|
<button class="secondary" id="btn-flow-admin-default-publish"><span class="btn-stack"><span class="btn-badge publish">发布</span><span>发布活动配置(默认绑定)</span></span></button>
|
||||||
<button class="secondary" id="btn-flow-admin-runtime-publish">一键补齐 Runtime 并发布</button>
|
<button class="secondary" id="btn-flow-admin-runtime-publish"><span class="btn-stack"><span class="btn-badge publish">发布</span><span>发布活动配置(自动补 Runtime)</span></span></button>
|
||||||
<button class="secondary" id="btn-flow-standard-regression">一键标准回归</button>
|
<button class="secondary" id="btn-flow-standard-regression"><span class="btn-stack"><span class="btn-badge verify">推荐</span><span>整条链一键验收</span></span></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="muted-note">这些流程会复用当前表单里的手机号、设备、event、channel 等输入。“一键默认绑定发布” 会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。“一键补齐 Runtime 并发布” 会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。“一键标准回归” 会继续执行:play -> launch -> start -> finish -> result -> history。</div>
|
<div class="muted-note">
|
||||||
|
推荐顺序:
|
||||||
|
<br>1. 先点上面的玩法入口:Use Classic Demo / Use Score-O Demo / Use Manual Variant Demo
|
||||||
|
<br>2. 想直接验收,就点 整条链一键验收
|
||||||
|
<br>3. 想只测发布链,就点 发布活动配置(自动补 Runtime)
|
||||||
|
<br>4. 想只测局内流程,就点 快速进一局、结束并看结果
|
||||||
|
</div>
|
||||||
|
<div class="muted-note">这些流程会复用当前表单里的手机号、设备、event、channel 等输入。发布活动配置(默认绑定)会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。发布活动配置(自动补 Runtime)会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。整条链一键验收会继续执行:play -> launch -> start -> finish -> result -> history。</div>
|
||||||
<div class="subpanel">
|
<div class="subpanel">
|
||||||
<div class="muted-note">预期结果</div>
|
<div class="muted-note">预期结果</div>
|
||||||
<div class="kv">
|
<div class="kv">
|
||||||
@@ -759,20 +937,45 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
<div>总判定 <code id="flow-regression-overall">待执行</code></div>
|
<div>总判定 <code id="flow-regression-overall">待执行</code></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="subpanel">
|
||||||
|
<div class="muted-note">当前 Launch 实际配置摘要</div>
|
||||||
|
<div class="kv">
|
||||||
|
<div>Config URL <code id="launch-config-url">-</code></div>
|
||||||
|
<div>Release ID <code id="launch-config-release-id">-</code></div>
|
||||||
|
<div>Manifest URL <code id="launch-config-manifest-url">-</code></div>
|
||||||
|
<div>Schema Version <code id="launch-config-schema-version">-</code></div>
|
||||||
|
<div>Playfield Kind <code id="launch-config-playfield-kind">-</code></div>
|
||||||
|
<div>Game Mode <code id="launch-config-game-mode">-</code></div>
|
||||||
|
<div>判定 <code id="launch-config-verdict">待执行</code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel" data-modes="common">
|
<div class="stack" data-modes="common frontend">
|
||||||
<h2>请求导出</h2>
|
<section class="panel" data-modes="common">
|
||||||
<p>最后一次请求会生成一条可复制的 curl,后面做问题复现会方便很多。</p>
|
<h2>请求导出</h2>
|
||||||
<div class="actions">
|
<p>最后一次请求会生成一条可复制的 curl,后面做问题复现会方便很多。</p>
|
||||||
<button id="btn-copy-curl">Copy Last Curl</button>
|
<div class="actions">
|
||||||
<button class="ghost" id="btn-clear-history">Clear History</button>
|
<button id="btn-copy-curl">Copy Last Curl</button>
|
||||||
</div>
|
<button class="ghost" id="btn-clear-history">Clear History</button>
|
||||||
<div class="subpanel">
|
</div>
|
||||||
<div class="muted-note">Last Curl</div>
|
<div class="subpanel">
|
||||||
<div id="curl" class="log" style="min-height:120px; max-height:200px;"></div>
|
<div class="muted-note">Last Curl</div>
|
||||||
</div>
|
<div id="curl" class="log" style="min-height:120px; max-height:200px;"></div>
|
||||||
</section>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel" data-modes="common frontend">
|
||||||
|
<h2>前端调试日志</h2>
|
||||||
|
<p>前端可把 launch、manifest、地图页、结果页等调试信息直接打到 backend。这里显示最近日志,便于和 workbench 当前配置对口排查。</p>
|
||||||
|
<div class="actions">
|
||||||
|
<button id="btn-client-logs-refresh">拉取前端日志</button>
|
||||||
|
<button class="ghost" id="btn-client-logs-clear">清空前端日志</button>
|
||||||
|
</div>
|
||||||
|
<div class="muted-note">建议前端至少上报:eventId / releaseId / manifestUrl / game.mode / playfield.kind / 页面阶段。</div>
|
||||||
|
<div id="client-logs" class="log" style="min-height:180px; max-height:420px;"></div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="category-head" id="nav-admin" data-modes="config admin">
|
<div class="category-head" id="nav-admin" data-modes="config admin">
|
||||||
@@ -1633,6 +1836,45 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
<div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
|
<div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="api-item" data-api="dev workbench 工作台 面板 调试">
|
||||||
|
<div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/workbench</span></div>
|
||||||
|
<div class="api-desc">开发态工作台页面,集中提供一键流、日志、配置摘要、API 目录和后台运营联调入口。</div>
|
||||||
|
<div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-item" data-api="dev client logs 前端 调试 日志 上报">
|
||||||
|
<div class="api-head"><span class="api-method">POST</span><span class="api-path">/dev/client-logs</span></div>
|
||||||
|
<div class="api-desc">接收 frontend 主动上报的调试日志,供 backend 在 workbench 中统一查看。</div>
|
||||||
|
<div class="api-meta">
|
||||||
|
<div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
|
||||||
|
<div><strong>关键参数:</strong><code>source</code>、<code>level</code>、<code>category</code>、<code>message</code>、<code>eventId</code>、<code>releaseId</code>、<code>sessionId</code>、<code>manifestUrl</code>、<code>route</code>、<code>details</code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-item" data-api="dev client logs 前端 调试 日志 列表">
|
||||||
|
<div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/client-logs</span></div>
|
||||||
|
<div class="api-desc">获取 frontend 最近上报的调试日志,便于 backend 直接对照排查。</div>
|
||||||
|
<div class="api-meta">
|
||||||
|
<div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
|
||||||
|
<div><strong>查询参数:</strong><code>limit</code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-item" data-api="dev client logs 前端 调试 日志 清空">
|
||||||
|
<div class="api-head"><span class="api-method">DELETE</span><span class="api-path">/dev/client-logs</span></div>
|
||||||
|
<div class="api-desc">清空当前内存中的 frontend 调试日志,方便开始新一轮联调。</div>
|
||||||
|
<div class="api-meta"><div><strong>鉴权:</strong>仅 non-production,无需鉴权</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="api-item" data-api="dev manifest summary manifest 摘要 代读 调试">
|
||||||
|
<div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/manifest-summary</span></div>
|
||||||
|
<div class="api-desc">由 backend 代读指定 manifest,并返回 <code>schemaVersion</code>、<code>playfield.kind</code>、<code>game.mode</code> 调试摘要。</div>
|
||||||
|
<div class="api-meta">
|
||||||
|
<div><strong>鉴权:</strong>仅 non-production,无需鉴权</div>
|
||||||
|
<div><strong>查询参数:</strong><code>url</code></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="api-item" data-api="dev config local files 本地 配置 文件 列表">
|
<div class="api-item" data-api="dev config local files 本地 配置 文件 列表">
|
||||||
<div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/config/local-files</span></div>
|
<div class="api-head"><span class="api-method">GET</span><span class="api-path">/dev/config/local-files</span></div>
|
||||||
<div class="api-desc">列出本地配置目录中的 JSON 文件,作为 source config 导入入口。</div>
|
<div class="api-desc">列出本地配置目录中的 JSON 文件,作为 source config 导入入口。</div>
|
||||||
@@ -2257,6 +2499,159 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
$('flow-regression-overall').textContent = overall;
|
$('flow-regression-overall').textContent = overall;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetLaunchConfigSummary() {
|
||||||
|
$('launch-config-url').textContent = '-';
|
||||||
|
$('launch-config-release-id').textContent = '-';
|
||||||
|
$('launch-config-manifest-url').textContent = '-';
|
||||||
|
$('launch-config-schema-version').textContent = '-';
|
||||||
|
$('launch-config-playfield-kind').textContent = '-';
|
||||||
|
$('launch-config-game-mode').textContent = '-';
|
||||||
|
$('launch-config-verdict').textContent = '待执行';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveLaunchConfigSummary(launchPayload) {
|
||||||
|
const launchData = launchPayload && launchPayload.launch ? launchPayload.launch : {};
|
||||||
|
const config = launchData.config || {};
|
||||||
|
const resolvedRelease = launchData.resolvedRelease || {};
|
||||||
|
const configUrl = config.configUrl || '-';
|
||||||
|
const releaseId = config.releaseId || resolvedRelease.releaseId || '-';
|
||||||
|
const manifestUrl = resolvedRelease.manifestUrl || config.configUrl || '-';
|
||||||
|
const summary = {
|
||||||
|
configUrl: configUrl,
|
||||||
|
releaseId: releaseId,
|
||||||
|
manifestUrl: manifestUrl,
|
||||||
|
schemaVersion: '-',
|
||||||
|
playfieldKind: '-',
|
||||||
|
gameMode: '-',
|
||||||
|
verdict: '未读取 manifest'
|
||||||
|
};
|
||||||
|
const targetUrl = config.configUrl || resolvedRelease.manifestUrl;
|
||||||
|
if (!targetUrl) {
|
||||||
|
summary.verdict = '未通过:launch 未返回 configUrl / manifestUrl';
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const proxy = await request('GET', '/dev/manifest-summary?url=' + encodeURIComponent(targetUrl));
|
||||||
|
const data = proxy && proxy.data ? proxy.data : {};
|
||||||
|
summary.schemaVersion = data.schemaVersion || '-';
|
||||||
|
summary.playfieldKind = data.playfieldKind || '-';
|
||||||
|
summary.gameMode = data.gameMode || '-';
|
||||||
|
if (summary.schemaVersion !== '-' && summary.playfieldKind !== '-' && summary.gameMode !== '-') {
|
||||||
|
summary.verdict = '通过:已读取 launch 实际 manifest 摘要';
|
||||||
|
} else {
|
||||||
|
summary.verdict = '未通过:manifest 已读取,但关键信息不完整';
|
||||||
|
}
|
||||||
|
return summary;
|
||||||
|
} catch (error) {
|
||||||
|
summary.verdict = '未通过:manifest 读取异常';
|
||||||
|
summary.error = error && error.message ? error.message : String(error);
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setLaunchConfigSummary(summary) {
|
||||||
|
const data = summary || {};
|
||||||
|
$('launch-config-url').textContent = data.configUrl || '-';
|
||||||
|
$('launch-config-release-id').textContent = data.releaseId || '-';
|
||||||
|
$('launch-config-manifest-url').textContent = data.manifestUrl || '-';
|
||||||
|
$('launch-config-schema-version').textContent = data.schemaVersion || '-';
|
||||||
|
$('launch-config-playfield-kind').textContent = data.playfieldKind || '-';
|
||||||
|
$('launch-config-game-mode').textContent = data.gameMode || '-';
|
||||||
|
$('launch-config-verdict').textContent = data.verdict || '待执行';
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderClientLogs(items) {
|
||||||
|
const logs = Array.isArray(items) ? items : [];
|
||||||
|
if (!logs.length) {
|
||||||
|
$('client-logs').textContent = '暂无前端调试日志';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$('client-logs').textContent = logs.map(function(item) {
|
||||||
|
const lines = [];
|
||||||
|
lines.push('[' + (item.receivedAt || '-') + '] #' + (item.id || '-') + ' ' + (item.level || 'info').toUpperCase() + ' ' + (item.source || 'unknown'));
|
||||||
|
if (item.category) {
|
||||||
|
lines.push('category: ' + item.category);
|
||||||
|
}
|
||||||
|
lines.push('message: ' + (item.message || '-'));
|
||||||
|
if (item.eventId || item.releaseId || item.sessionId) {
|
||||||
|
lines.push('event/release/session: ' + (item.eventId || '-') + ' / ' + (item.releaseId || '-') + ' / ' + (item.sessionId || '-'));
|
||||||
|
}
|
||||||
|
if (item.manifestUrl) {
|
||||||
|
lines.push('manifest: ' + item.manifestUrl);
|
||||||
|
}
|
||||||
|
if (item.route) {
|
||||||
|
lines.push('route: ' + item.route);
|
||||||
|
}
|
||||||
|
if (item.details && Object.keys(item.details).length) {
|
||||||
|
lines.push('details: ' + JSON.stringify(item.details, null, 2));
|
||||||
|
}
|
||||||
|
return lines.join('\n');
|
||||||
|
}).join('\n\n---\n\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function refreshClientLogs() {
|
||||||
|
const result = await request('GET', '/dev/client-logs?limit=50');
|
||||||
|
renderClientLogs(result.data);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectBootstrapContextForEvent(bootstrap, eventId) {
|
||||||
|
const data = bootstrap || {};
|
||||||
|
if (eventId && data.scoreOEventId && eventId === data.scoreOEventId) {
|
||||||
|
return {
|
||||||
|
eventId: data.scoreOEventId,
|
||||||
|
releaseId: data.scoreOReleaseId || '',
|
||||||
|
sourceId: data.scoreOSourceId || '',
|
||||||
|
buildId: data.scoreOBuildId || '',
|
||||||
|
courseSetId: data.scoreOCourseSetId || '',
|
||||||
|
courseVariantId: data.scoreOCourseVariantId || '',
|
||||||
|
runtimeBindingId: data.scoreORuntimeBindingId || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (eventId && data.variantManualEventId && eventId === data.variantManualEventId) {
|
||||||
|
return {
|
||||||
|
eventId: data.variantManualEventId,
|
||||||
|
releaseId: data.variantManualReleaseId || '',
|
||||||
|
sourceId: data.sourceId || '',
|
||||||
|
buildId: data.buildId || '',
|
||||||
|
courseSetId: data.courseSetId || '',
|
||||||
|
courseVariantId: data.courseVariantId || '',
|
||||||
|
runtimeBindingId: data.runtimeBindingId || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
eventId: data.eventId || '',
|
||||||
|
releaseId: data.releaseId || '',
|
||||||
|
sourceId: data.sourceId || '',
|
||||||
|
buildId: data.buildId || '',
|
||||||
|
courseSetId: data.courseSetId || '',
|
||||||
|
courseVariantId: data.courseVariantId || '',
|
||||||
|
runtimeBindingId: data.runtimeBindingId || ''
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyBootstrapContext(bootstrap, explicitEventId) {
|
||||||
|
const eventId = explicitEventId || $('event-id').value || $('admin-event-ref-id').value || '';
|
||||||
|
const selected = selectBootstrapContextForEvent(bootstrap, eventId);
|
||||||
|
state.sourceId = selected.sourceId || state.sourceId;
|
||||||
|
state.buildId = selected.buildId || state.buildId;
|
||||||
|
state.releaseId = selected.releaseId || state.releaseId;
|
||||||
|
$('admin-pipeline-source-id').value = selected.sourceId || $('admin-pipeline-source-id').value;
|
||||||
|
$('admin-pipeline-build-id').value = selected.buildId || $('admin-pipeline-build-id').value;
|
||||||
|
$('admin-pipeline-release-id').value = selected.releaseId || $('admin-pipeline-release-id').value;
|
||||||
|
$('event-release-id').value = selected.releaseId || $('event-release-id').value;
|
||||||
|
$('prod-runtime-event-id').value = selected.eventId || $('prod-runtime-event-id').value;
|
||||||
|
$('prod-place-id').value = bootstrap.placeId || $('prod-place-id').value;
|
||||||
|
$('prod-map-asset-id').value = bootstrap.mapAssetId || $('prod-map-asset-id').value;
|
||||||
|
$('prod-tile-release-id').value = bootstrap.tileReleaseId || $('prod-tile-release-id').value;
|
||||||
|
$('prod-course-source-id').value = bootstrap.courseSourceId || $('prod-course-source-id').value;
|
||||||
|
$('prod-course-set-id').value = selected.courseSetId || $('prod-course-set-id').value;
|
||||||
|
$('prod-course-variant-id').value = selected.courseVariantId || $('prod-course-variant-id').value;
|
||||||
|
$('prod-runtime-binding-id').value = selected.runtimeBindingId || $('prod-runtime-binding-id').value;
|
||||||
|
syncState();
|
||||||
|
return selected;
|
||||||
|
}
|
||||||
|
|
||||||
function extractList(payload) {
|
function extractList(payload) {
|
||||||
if (Array.isArray(payload)) {
|
if (Array.isArray(payload)) {
|
||||||
return payload;
|
return payload;
|
||||||
@@ -2306,20 +2701,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
writeLog(flowTitle + '.step', { step: 'bootstrap-demo' });
|
writeLog(flowTitle + '.step', { step: 'bootstrap-demo' });
|
||||||
const bootstrap = await request('POST', '/dev/bootstrap-demo');
|
const bootstrap = await request('POST', '/dev/bootstrap-demo');
|
||||||
if (bootstrap.data) {
|
if (bootstrap.data) {
|
||||||
state.sourceId = bootstrap.data.sourceId || state.sourceId;
|
applyBootstrapContext(bootstrap.data, eventId);
|
||||||
state.buildId = bootstrap.data.buildId || state.buildId;
|
|
||||||
state.releaseId = bootstrap.data.releaseId || state.releaseId;
|
|
||||||
$('admin-pipeline-source-id').value = bootstrap.data.sourceId || $('admin-pipeline-source-id').value;
|
|
||||||
$('admin-pipeline-build-id').value = bootstrap.data.buildId || $('admin-pipeline-build-id').value;
|
|
||||||
$('admin-pipeline-release-id').value = bootstrap.data.releaseId || $('admin-pipeline-release-id').value;
|
|
||||||
$('prod-runtime-event-id').value = bootstrap.data.eventId || $('prod-runtime-event-id').value;
|
|
||||||
$('prod-place-id').value = bootstrap.data.placeId || $('prod-place-id').value;
|
|
||||||
$('prod-map-asset-id').value = bootstrap.data.mapAssetId || $('prod-map-asset-id').value;
|
|
||||||
$('prod-tile-release-id').value = bootstrap.data.tileReleaseId || $('prod-tile-release-id').value;
|
|
||||||
$('prod-course-source-id').value = bootstrap.data.courseSourceId || $('prod-course-source-id').value;
|
|
||||||
$('prod-course-set-id').value = bootstrap.data.courseSetId || $('prod-course-set-id').value;
|
|
||||||
$('prod-course-variant-id').value = bootstrap.data.courseVariantId || $('prod-course-variant-id').value;
|
|
||||||
$('prod-runtime-binding-id').value = bootstrap.data.runtimeBindingId || $('prod-runtime-binding-id').value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2336,6 +2718,9 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
if (eventDetail.data.currentRuntime && eventDetail.data.currentRuntime.runtimeBindingId) {
|
if (eventDetail.data.currentRuntime && eventDetail.data.currentRuntime.runtimeBindingId) {
|
||||||
$('admin-release-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
|
$('admin-release-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
|
||||||
$('prod-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
|
$('prod-runtime-binding-id').value = eventDetail.data.currentRuntime.runtimeBindingId;
|
||||||
|
} else {
|
||||||
|
$('admin-release-runtime-binding-id').value = '';
|
||||||
|
$('prod-runtime-binding-id').value = '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2539,6 +2924,9 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
launch.data.launch.resolvedRelease &&
|
launch.data.launch.resolvedRelease &&
|
||||||
launch.data.launch.resolvedRelease.manifestUrl
|
launch.data.launch.resolvedRelease.manifestUrl
|
||||||
);
|
);
|
||||||
|
const launchConfigSummary = await resolveLaunchConfigSummary(launch.data);
|
||||||
|
setLaunchConfigSummary(launchConfigSummary);
|
||||||
|
writeLog(flowTitle + '.launch-summary', launchConfigSummary);
|
||||||
|
|
||||||
writeLog(flowTitle + '.step', {
|
writeLog(flowTitle + '.step', {
|
||||||
step: 'session-start',
|
step: 'session-start',
|
||||||
@@ -2618,6 +3006,41 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function applyFrontendDemoSelection(options) {
|
||||||
|
resetLaunchConfigSummary();
|
||||||
|
$('entry-channel-code').value = 'mini-demo';
|
||||||
|
$('entry-channel-type').value = 'wechat_mini';
|
||||||
|
$('event-id').value = options.eventId;
|
||||||
|
$('event-release-id').value = options.releaseId;
|
||||||
|
$('event-variant-id').value = options.variantId || '';
|
||||||
|
$('config-event-id').value = options.eventId;
|
||||||
|
$('admin-event-ref-id').value = options.eventId;
|
||||||
|
$('local-config-file').value = options.localConfigFile || $('local-config-file').value;
|
||||||
|
if (options.gameModeCode) {
|
||||||
|
$('admin-game-mode-code').value = options.gameModeCode;
|
||||||
|
$('prod-course-mode').value = options.gameModeCode;
|
||||||
|
}
|
||||||
|
$('prod-runtime-event-id').value = options.eventId;
|
||||||
|
$('prod-course-set-id').value = options.courseSetId || $('prod-course-set-id').value;
|
||||||
|
$('prod-course-variant-id').value = options.courseVariantId || $('prod-course-variant-id').value;
|
||||||
|
$('prod-runtime-binding-id').value = options.runtimeBindingId || '';
|
||||||
|
$('admin-release-runtime-binding-id').value = options.runtimeBindingId || '';
|
||||||
|
$('admin-pipeline-source-id').value = options.sourceId || '';
|
||||||
|
$('admin-pipeline-build-id').value = options.buildId || '';
|
||||||
|
state.sourceId = options.sourceId || '';
|
||||||
|
state.buildId = options.buildId || '';
|
||||||
|
state.releaseId = options.releaseId || state.releaseId;
|
||||||
|
localStorage.setItem(MODE_KEY, 'frontend');
|
||||||
|
syncWorkbenchMode();
|
||||||
|
writeLog(options.logTitle, {
|
||||||
|
eventId: $('event-id').value,
|
||||||
|
releaseId: $('event-release-id').value,
|
||||||
|
variantId: $('event-variant-id').value || null,
|
||||||
|
localConfigFile: $('local-config-file').value
|
||||||
|
});
|
||||||
|
setStatus(options.statusText);
|
||||||
|
}
|
||||||
|
|
||||||
function setStatus(text, isError = false) {
|
function setStatus(text, isError = false) {
|
||||||
statusEl.textContent = text;
|
statusEl.textContent = text;
|
||||||
statusEl.className = isError ? 'status error' : 'status';
|
statusEl.className = isError ? 'status error' : 'status';
|
||||||
@@ -3485,6 +3908,10 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
}, true);
|
}, true);
|
||||||
state.sessionId = result.data.launch.business.sessionId;
|
state.sessionId = result.data.launch.business.sessionId;
|
||||||
state.sessionToken = result.data.launch.business.sessionToken;
|
state.sessionToken = result.data.launch.business.sessionToken;
|
||||||
|
syncState();
|
||||||
|
const configSummary = await resolveLaunchConfigSummary(result.data);
|
||||||
|
setLaunchConfigSummary(configSummary);
|
||||||
|
writeLog('event-launch.summary', configSummary);
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4221,6 +4648,16 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
setStatus('ok: history cleared');
|
setStatus('ok: history cleared');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$('btn-client-logs-refresh').onclick = () => run('dev/client-logs', async () => {
|
||||||
|
return await refreshClientLogs();
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btn-client-logs-clear').onclick = () => run('dev/client-logs/clear', async () => {
|
||||||
|
const result = await request('DELETE', '/dev/client-logs');
|
||||||
|
renderClientLogs([]);
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
$('btn-scenario-save').onclick = saveCurrentScenario;
|
$('btn-scenario-save').onclick = saveCurrentScenario;
|
||||||
$('btn-scenario-load').onclick = loadSelectedScenario;
|
$('btn-scenario-load').onclick = loadSelectedScenario;
|
||||||
$('btn-scenario-delete').onclick = deleteSelectedScenario;
|
$('btn-scenario-delete').onclick = deleteSelectedScenario;
|
||||||
@@ -4228,7 +4665,10 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
$('btn-scenario-import').onclick = importScenarioFromJSON;
|
$('btn-scenario-import').onclick = importScenarioFromJSON;
|
||||||
|
|
||||||
$('btn-flow-home').onclick = () => run('flow-home', async () => {
|
$('btn-flow-home').onclick = () => run('flow-home', async () => {
|
||||||
await request('POST', '/dev/bootstrap-demo');
|
const bootstrap = await request('POST', '/dev/bootstrap-demo');
|
||||||
|
if (bootstrap.data) {
|
||||||
|
applyBootstrapContext(bootstrap.data);
|
||||||
|
}
|
||||||
const login = await request('POST', '/auth/login/wechat-mini', {
|
const login = await request('POST', '/auth/login/wechat-mini', {
|
||||||
code: $('wechat-code').value,
|
code: $('wechat-code').value,
|
||||||
clientType: 'wechat',
|
clientType: 'wechat',
|
||||||
@@ -4241,43 +4681,70 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
|
|
||||||
$('btn-bootstrap').onclick = () => run('bootstrap-demo', async () => {
|
$('btn-bootstrap').onclick = () => run('bootstrap-demo', async () => {
|
||||||
const result = await request('POST', '/dev/bootstrap-demo');
|
const result = await request('POST', '/dev/bootstrap-demo');
|
||||||
state.sourceId = result.data.sourceId || '';
|
applyBootstrapContext(result.data);
|
||||||
state.buildId = result.data.buildId || '';
|
return result;
|
||||||
state.releaseId = result.data.releaseId || state.releaseId || '';
|
});
|
||||||
if (result.data.releaseId) {
|
|
||||||
$('event-release-id').value = result.data.releaseId;
|
$('btn-use-classic-demo').onclick = () => run('use-classic-demo', async () => {
|
||||||
}
|
const result = await request('POST', '/dev/bootstrap-demo');
|
||||||
$('prod-runtime-event-id').value = result.data.eventId || $('prod-runtime-event-id').value;
|
applyFrontendDemoSelection({
|
||||||
$('prod-place-id').value = result.data.placeId || $('prod-place-id').value;
|
eventId: result.data.eventId || 'evt_demo_001',
|
||||||
$('prod-map-asset-id').value = result.data.mapAssetId || $('prod-map-asset-id').value;
|
releaseId: result.data.releaseId || 'rel_demo_001',
|
||||||
$('prod-tile-release-id').value = result.data.tileReleaseId || $('prod-tile-release-id').value;
|
localConfigFile: 'classic-sequential.json',
|
||||||
$('prod-course-source-id').value = result.data.courseSourceId || $('prod-course-source-id').value;
|
gameModeCode: 'classic-sequential',
|
||||||
$('prod-course-set-id').value = result.data.courseSetId || $('prod-course-set-id').value;
|
sourceId: result.data.sourceId || '',
|
||||||
$('prod-course-variant-id').value = result.data.courseVariantId || $('prod-course-variant-id').value;
|
buildId: result.data.buildId || '',
|
||||||
$('prod-runtime-binding-id').value = result.data.runtimeBindingId || $('prod-runtime-binding-id').value;
|
courseSetId: result.data.courseSetId || '',
|
||||||
|
courseVariantId: result.data.courseVariantId || '',
|
||||||
|
runtimeBindingId: result.data.runtimeBindingId || '',
|
||||||
|
logTitle: 'classic-demo-ready',
|
||||||
|
statusText: 'ok: classic demo loaded'
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
|
||||||
|
$('btn-use-score-o-demo').onclick = () => run('use-score-o-demo', async () => {
|
||||||
|
const result = await request('POST', '/dev/bootstrap-demo');
|
||||||
|
applyFrontendDemoSelection({
|
||||||
|
eventId: result.data.scoreOEventId || 'evt_demo_score_o_001',
|
||||||
|
releaseId: result.data.scoreOReleaseId || 'rel_demo_score_o_001',
|
||||||
|
localConfigFile: 'score-o.json',
|
||||||
|
gameModeCode: 'score-o',
|
||||||
|
sourceId: result.data.scoreOSourceId || '',
|
||||||
|
buildId: result.data.scoreOBuildId || '',
|
||||||
|
courseSetId: result.data.scoreOCourseSetId || '',
|
||||||
|
courseVariantId: result.data.scoreOCourseVariantId || '',
|
||||||
|
runtimeBindingId: result.data.scoreORuntimeBindingId || '',
|
||||||
|
logTitle: 'score-o-demo-ready',
|
||||||
|
statusText: 'ok: score-o demo loaded'
|
||||||
|
});
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
$('btn-use-variant-manual-demo').onclick = () => run('use-variant-manual-demo', async () => {
|
$('btn-use-variant-manual-demo').onclick = () => run('use-variant-manual-demo', async () => {
|
||||||
const result = await request('POST', '/dev/bootstrap-demo');
|
const result = await request('POST', '/dev/bootstrap-demo');
|
||||||
$('entry-channel-code').value = 'mini-demo';
|
applyFrontendDemoSelection({
|
||||||
$('entry-channel-type').value = 'wechat_mini';
|
eventId: result.data.variantManualEventId || 'evt_demo_variant_manual_001',
|
||||||
$('event-id').value = result.data.variantManualEventId || 'evt_demo_variant_manual_001';
|
releaseId: result.data.variantManualReleaseId || 'rel_demo_variant_manual_001',
|
||||||
$('event-release-id').value = result.data.variantManualReleaseId || 'rel_demo_variant_manual_001';
|
variantId: 'variant_b',
|
||||||
$('event-variant-id').value = 'variant_b';
|
localConfigFile: 'classic-sequential.json',
|
||||||
localStorage.setItem(MODE_KEY, 'frontend');
|
gameModeCode: 'classic-sequential',
|
||||||
syncWorkbenchMode();
|
sourceId: result.data.sourceId || '',
|
||||||
writeLog('variant-manual-demo-ready', {
|
buildId: result.data.buildId || '',
|
||||||
eventId: $('event-id').value,
|
courseSetId: result.data.courseSetId || '',
|
||||||
releaseId: $('event-release-id').value,
|
courseVariantId: result.data.courseVariantId || '',
|
||||||
variantId: $('event-variant-id').value
|
runtimeBindingId: '',
|
||||||
|
logTitle: 'variant-manual-demo-ready',
|
||||||
|
statusText: 'ok: manual variant demo loaded'
|
||||||
});
|
});
|
||||||
setStatus('ok: manual variant demo loaded');
|
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
$('btn-flow-launch').onclick = () => run('flow-launch', async () => {
|
$('btn-flow-launch').onclick = () => run('flow-launch', async () => {
|
||||||
await request('POST', '/dev/bootstrap-demo');
|
const bootstrap = await request('POST', '/dev/bootstrap-demo');
|
||||||
|
if (bootstrap.data) {
|
||||||
|
applyBootstrapContext(bootstrap.data);
|
||||||
|
}
|
||||||
const smsSend = await request('POST', '/auth/sms/send', {
|
const smsSend = await request('POST', '/auth/sms/send', {
|
||||||
countryCode: $('sms-country').value,
|
countryCode: $('sms-country').value,
|
||||||
mobile: $('sms-mobile').value,
|
mobile: $('sms-mobile').value,
|
||||||
@@ -4388,6 +4855,7 @@ const devWorkbenchHTML = `<!doctype html>
|
|||||||
renderScenarioOptions();
|
renderScenarioOptions();
|
||||||
applyAPIFilter();
|
applyAPIFilter();
|
||||||
syncAPICounts();
|
syncAPICounts();
|
||||||
|
renderClientLogs([]);
|
||||||
writeLog('workbench-ready', { ok: true, hint: 'Use Bootstrap Demo first on a fresh database.' });
|
writeLog('workbench-ready', { ok: true, hint: 'Use Bootstrap Demo first on a fresh database.' });
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -105,6 +105,10 @@ func NewRouter(
|
|||||||
if appEnv != "production" {
|
if appEnv != "production" {
|
||||||
mux.HandleFunc("GET /dev/workbench", devHandler.Workbench)
|
mux.HandleFunc("GET /dev/workbench", devHandler.Workbench)
|
||||||
mux.HandleFunc("POST /dev/bootstrap-demo", devHandler.BootstrapDemo)
|
mux.HandleFunc("POST /dev/bootstrap-demo", devHandler.BootstrapDemo)
|
||||||
|
mux.HandleFunc("POST /dev/client-logs", devHandler.CreateClientLog)
|
||||||
|
mux.HandleFunc("GET /dev/client-logs", devHandler.ListClientLogs)
|
||||||
|
mux.HandleFunc("DELETE /dev/client-logs", devHandler.ClearClientLogs)
|
||||||
|
mux.HandleFunc("GET /dev/manifest-summary", devHandler.ManifestSummary)
|
||||||
mux.HandleFunc("GET /dev/config/local-files", configHandler.ListLocalFiles)
|
mux.HandleFunc("GET /dev/config/local-files", configHandler.ListLocalFiles)
|
||||||
mux.HandleFunc("POST /dev/events/{eventPublicID}/config-sources/import-local", configHandler.ImportLocal)
|
mux.HandleFunc("POST /dev/events/{eventPublicID}/config-sources/import-local", configHandler.ImportLocal)
|
||||||
mux.HandleFunc("POST /dev/config-builds/preview", configHandler.BuildPreview)
|
mux.HandleFunc("POST /dev/config-builds/preview", configHandler.BuildPreview)
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ package service
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"cmr-backend/internal/apperr"
|
"cmr-backend/internal/apperr"
|
||||||
"cmr-backend/internal/store/postgres"
|
"cmr-backend/internal/store/postgres"
|
||||||
@@ -11,6 +14,39 @@ import (
|
|||||||
type DevService struct {
|
type DevService struct {
|
||||||
appEnv string
|
appEnv string
|
||||||
store *postgres.Store
|
store *postgres.Store
|
||||||
|
mu sync.Mutex
|
||||||
|
logSeq int64
|
||||||
|
logs []ClientDebugLogEntry
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientDebugLogEntry struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
Source string `json:"source"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Category string `json:"category,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
EventID string `json:"eventId,omitempty"`
|
||||||
|
ReleaseID string `json:"releaseId,omitempty"`
|
||||||
|
SessionID string `json:"sessionId,omitempty"`
|
||||||
|
ManifestURL string `json:"manifestUrl,omitempty"`
|
||||||
|
Route string `json:"route,omitempty"`
|
||||||
|
OccurredAt time.Time `json:"occurredAt"`
|
||||||
|
ReceivedAt time.Time `json:"receivedAt"`
|
||||||
|
Details map[string]any `json:"details,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateClientDebugLogInput struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Level string `json:"level"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
EventID string `json:"eventId"`
|
||||||
|
ReleaseID string `json:"releaseId"`
|
||||||
|
SessionID string `json:"sessionId"`
|
||||||
|
ManifestURL string `json:"manifestUrl"`
|
||||||
|
Route string `json:"route"`
|
||||||
|
OccurredAt string `json:"occurredAt"`
|
||||||
|
Details map[string]any `json:"details"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDevService(appEnv string, store *postgres.Store) *DevService {
|
func NewDevService(appEnv string, store *postgres.Store) *DevService {
|
||||||
@@ -30,3 +66,83 @@ func (s *DevService) BootstrapDemo(ctx context.Context) (*postgres.DemoBootstrap
|
|||||||
}
|
}
|
||||||
return s.store.EnsureDemoData(ctx)
|
return s.store.EnsureDemoData(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *DevService) AddClientDebugLog(_ context.Context, input CreateClientDebugLogInput) (*ClientDebugLogEntry, error) {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
|
||||||
|
}
|
||||||
|
if input.Message == "" {
|
||||||
|
return nil, apperr.New(http.StatusBadRequest, "invalid_request", "message is required")
|
||||||
|
}
|
||||||
|
if input.Source == "" {
|
||||||
|
input.Source = "unknown"
|
||||||
|
}
|
||||||
|
if input.Level == "" {
|
||||||
|
input.Level = "info"
|
||||||
|
}
|
||||||
|
|
||||||
|
occurredAt := time.Now().UTC()
|
||||||
|
if input.OccurredAt != "" {
|
||||||
|
parsed, err := time.Parse(time.RFC3339, input.OccurredAt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, apperr.New(http.StatusBadRequest, "invalid_request", "occurredAt must be RFC3339")
|
||||||
|
}
|
||||||
|
occurredAt = parsed.UTC()
|
||||||
|
}
|
||||||
|
|
||||||
|
entry := ClientDebugLogEntry{
|
||||||
|
Source: input.Source,
|
||||||
|
Level: input.Level,
|
||||||
|
Category: input.Category,
|
||||||
|
Message: input.Message,
|
||||||
|
EventID: input.EventID,
|
||||||
|
ReleaseID: input.ReleaseID,
|
||||||
|
SessionID: input.SessionID,
|
||||||
|
ManifestURL: input.ManifestURL,
|
||||||
|
Route: input.Route,
|
||||||
|
OccurredAt: occurredAt,
|
||||||
|
ReceivedAt: time.Now().UTC(),
|
||||||
|
Details: input.Details,
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.logSeq++
|
||||||
|
entry.ID = s.logSeq
|
||||||
|
s.logs = append(s.logs, entry)
|
||||||
|
if len(s.logs) > 200 {
|
||||||
|
s.logs = append([]ClientDebugLogEntry(nil), s.logs[len(s.logs)-200:]...)
|
||||||
|
}
|
||||||
|
copyEntry := entry
|
||||||
|
return ©Entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DevService) ListClientDebugLogs(_ context.Context, limit int) ([]ClientDebugLogEntry, error) {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return nil, apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
|
||||||
|
}
|
||||||
|
if limit <= 0 || limit > 200 {
|
||||||
|
limit = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
items := append([]ClientDebugLogEntry(nil), s.logs...)
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].ID > items[j].ID
|
||||||
|
})
|
||||||
|
if len(items) > limit {
|
||||||
|
items = items[:limit]
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DevService) ClearClientDebugLogs(_ context.Context) error {
|
||||||
|
if !s.Enabled() {
|
||||||
|
return apperr.New(http.StatusNotFound, "not_found", "dev client logs are disabled")
|
||||||
|
}
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
s.logs = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,24 +6,32 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DemoBootstrapSummary struct {
|
type DemoBootstrapSummary struct {
|
||||||
TenantCode string `json:"tenantCode"`
|
TenantCode string `json:"tenantCode"`
|
||||||
ChannelCode string `json:"channelCode"`
|
ChannelCode string `json:"channelCode"`
|
||||||
EventID string `json:"eventId"`
|
EventID string `json:"eventId"`
|
||||||
ReleaseID string `json:"releaseId"`
|
ReleaseID string `json:"releaseId"`
|
||||||
SourceID string `json:"sourceId"`
|
SourceID string `json:"sourceId"`
|
||||||
BuildID string `json:"buildId"`
|
BuildID string `json:"buildId"`
|
||||||
CardID string `json:"cardId"`
|
CardID string `json:"cardId"`
|
||||||
PlaceID string `json:"placeId"`
|
PlaceID string `json:"placeId"`
|
||||||
MapAssetID string `json:"mapAssetId"`
|
MapAssetID string `json:"mapAssetId"`
|
||||||
TileReleaseID string `json:"tileReleaseId"`
|
TileReleaseID string `json:"tileReleaseId"`
|
||||||
CourseSourceID string `json:"courseSourceId"`
|
CourseSourceID string `json:"courseSourceId"`
|
||||||
CourseSetID string `json:"courseSetId"`
|
CourseSetID string `json:"courseSetId"`
|
||||||
CourseVariantID string `json:"courseVariantId"`
|
CourseVariantID string `json:"courseVariantId"`
|
||||||
RuntimeBindingID string `json:"runtimeBindingId"`
|
RuntimeBindingID string `json:"runtimeBindingId"`
|
||||||
VariantManualEventID string `json:"variantManualEventId"`
|
ScoreOEventID string `json:"scoreOEventId"`
|
||||||
VariantManualRelease string `json:"variantManualReleaseId"`
|
ScoreOReleaseID string `json:"scoreOReleaseId"`
|
||||||
VariantManualCardID string `json:"variantManualCardId"`
|
ScoreOCardID string `json:"scoreOCardId"`
|
||||||
CleanedSessionCount int64 `json:"cleanedSessionCount"`
|
ScoreOSourceID string `json:"scoreOSourceId"`
|
||||||
|
ScoreOBuildID string `json:"scoreOBuildId"`
|
||||||
|
ScoreOCourseSetID string `json:"scoreOCourseSetId"`
|
||||||
|
ScoreOCourseVariantID string `json:"scoreOCourseVariantId"`
|
||||||
|
ScoreORuntimeBindingID string `json:"scoreORuntimeBindingId"`
|
||||||
|
VariantManualEventID string `json:"variantManualEventId"`
|
||||||
|
VariantManualRelease string `json:"variantManualReleaseId"`
|
||||||
|
VariantManualCardID string `json:"variantManualCardId"`
|
||||||
|
CleanedSessionCount int64 `json:"cleanedSessionCount"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, error) {
|
func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, error) {
|
||||||
@@ -361,7 +369,7 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
'tile_demo_001', $1, 'v2026-04-03', 'published',
|
'tile_demo_001', $1, 'v2026-04-03', 'published',
|
||||||
'https://example.com/tiles/demo/', 'https://example.com/tiles/demo/meta.json', NOW()
|
'https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/', 'https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json', NOW()
|
||||||
)
|
)
|
||||||
ON CONFLICT (map_asset_id, version_code) DO UPDATE SET
|
ON CONFLICT (map_asset_id, version_code) DO UPDATE SET
|
||||||
status = EXCLUDED.status,
|
status = EXCLUDED.status,
|
||||||
@@ -387,7 +395,7 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
course_source_public_id, source_type, file_url, import_status
|
course_source_public_id, source_type, file_url, import_status
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
'csource_demo_001', 'kml', 'https://example.com/course/demo.kml', 'imported'
|
'csource_demo_001', 'kml', 'https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml', 'imported'
|
||||||
)
|
)
|
||||||
ON CONFLICT (course_source_public_id) DO UPDATE SET
|
ON CONFLICT (course_source_public_id) DO UPDATE SET
|
||||||
source_type = EXCLUDED.source_type,
|
source_type = EXCLUDED.source_type,
|
||||||
@@ -398,6 +406,23 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
return nil, fmt.Errorf("ensure demo course source: %w", err)
|
return nil, fmt.Errorf("ensure demo course source: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var courseSourceVariantBID string
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO course_sources (
|
||||||
|
course_source_public_id, source_type, file_url, import_status
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'csource_demo_002', 'kml', 'https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c02.kml', 'imported'
|
||||||
|
)
|
||||||
|
ON CONFLICT (course_source_public_id) DO UPDATE SET
|
||||||
|
source_type = EXCLUDED.source_type,
|
||||||
|
file_url = EXCLUDED.file_url,
|
||||||
|
import_status = EXCLUDED.import_status
|
||||||
|
RETURNING id, course_source_public_id
|
||||||
|
`).Scan(&courseSourceVariantBID, new(string)); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure demo course source variant b: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var courseSetID, courseSetPublicID string
|
var courseSetID, courseSetPublicID string
|
||||||
if err := tx.QueryRow(ctx, `
|
if err := tx.QueryRow(ctx, `
|
||||||
INSERT INTO course_sets (
|
INSERT INTO course_sets (
|
||||||
@@ -439,6 +464,28 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
return nil, fmt.Errorf("ensure demo course variant: %w", err)
|
return nil, fmt.Errorf("ensure demo course variant: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var courseVariantBID string
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO course_variants (
|
||||||
|
course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count, status, is_default
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'cvariant_demo_002', $1, $2, 'Demo Variant B', 'route-demo-b', 'classic-sequential', 10, 'active', false
|
||||||
|
)
|
||||||
|
ON CONFLICT (course_variant_public_id) DO UPDATE SET
|
||||||
|
course_set_id = EXCLUDED.course_set_id,
|
||||||
|
source_id = EXCLUDED.source_id,
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
route_code = EXCLUDED.route_code,
|
||||||
|
mode = EXCLUDED.mode,
|
||||||
|
control_count = EXCLUDED.control_count,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
is_default = EXCLUDED.is_default
|
||||||
|
RETURNING id, course_variant_public_id
|
||||||
|
`, courseSetID, courseSourceVariantBID).Scan(&courseVariantBID, new(string)); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure demo course variant b: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := tx.Exec(ctx, `
|
if _, err := tx.Exec(ctx, `
|
||||||
UPDATE course_sets
|
UPDATE course_sets
|
||||||
SET current_variant_id = $2
|
SET current_variant_id = $2
|
||||||
@@ -529,14 +576,14 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
{
|
{
|
||||||
"id": "variant_a",
|
"id": "variant_a",
|
||||||
"name": "A 线",
|
"name": "A 线",
|
||||||
"description": "短线体验版",
|
"description": "短线体验版(c01.kml)",
|
||||||
"routeCode": "route-variant-a",
|
"routeCode": "route-variant-a",
|
||||||
"selectable": true
|
"selectable": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "variant_b",
|
"id": "variant_b",
|
||||||
"name": "B 线",
|
"name": "B 线",
|
||||||
"description": "长线挑战版",
|
"description": "长线挑战版(c02.kml)",
|
||||||
"routeCode": "route-variant-b",
|
"routeCode": "route-variant-b",
|
||||||
"selectable": true
|
"selectable": true
|
||||||
}
|
}
|
||||||
@@ -598,6 +645,273 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
return nil, fmt.Errorf("ensure variant manual demo card: %w", err)
|
return nil, fmt.Errorf("ensure variant manual demo card: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var scoreOEventID string
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO events (
|
||||||
|
tenant_id, event_public_id, slug, display_name, summary, status
|
||||||
|
)
|
||||||
|
VALUES ($1, 'evt_demo_score_o_001', 'demo-score-o-run', 'Demo Score-O Run', '积分赛联调活动', 'active')
|
||||||
|
ON CONFLICT (event_public_id) DO UPDATE SET
|
||||||
|
tenant_id = EXCLUDED.tenant_id,
|
||||||
|
slug = EXCLUDED.slug,
|
||||||
|
display_name = EXCLUDED.display_name,
|
||||||
|
summary = EXCLUDED.summary,
|
||||||
|
status = EXCLUDED.status
|
||||||
|
RETURNING id
|
||||||
|
`, tenantID).Scan(&scoreOEventID); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scoreOReleaseRow struct {
|
||||||
|
ID string
|
||||||
|
PublicID string
|
||||||
|
}
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO event_releases (
|
||||||
|
release_public_id,
|
||||||
|
event_id,
|
||||||
|
release_no,
|
||||||
|
config_label,
|
||||||
|
manifest_url,
|
||||||
|
manifest_checksum_sha256,
|
||||||
|
route_code,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'rel_demo_score_o_001',
|
||||||
|
$1,
|
||||||
|
1,
|
||||||
|
'Demo Score-O Config v1',
|
||||||
|
'https://oss-mbh5.colormaprun.com/gotomars/event/score-o.json',
|
||||||
|
'demo-score-o-checksum-001',
|
||||||
|
'route-score-o-001',
|
||||||
|
'published'
|
||||||
|
)
|
||||||
|
ON CONFLICT (release_public_id) DO UPDATE SET
|
||||||
|
event_id = EXCLUDED.event_id,
|
||||||
|
config_label = EXCLUDED.config_label,
|
||||||
|
manifest_url = EXCLUDED.manifest_url,
|
||||||
|
manifest_checksum_sha256 = EXCLUDED.manifest_checksum_sha256,
|
||||||
|
route_code = EXCLUDED.route_code,
|
||||||
|
status = EXCLUDED.status
|
||||||
|
RETURNING id, release_public_id
|
||||||
|
`, scoreOEventID).Scan(&scoreOReleaseRow.ID, &scoreOReleaseRow.PublicID); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo release: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
UPDATE events
|
||||||
|
SET current_release_id = $2
|
||||||
|
WHERE id = $1
|
||||||
|
`, scoreOEventID, scoreOReleaseRow.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("attach score-o demo release: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreOSourceNotes := "demo source config imported from local event sample score-o"
|
||||||
|
scoreOSource, err := s.UpsertEventConfigSource(ctx, tx, UpsertEventConfigSourceParams{
|
||||||
|
EventID: scoreOEventID,
|
||||||
|
SourceVersionNo: 1,
|
||||||
|
SourceKind: "event_bundle",
|
||||||
|
SchemaID: "event-source",
|
||||||
|
SchemaVersion: "1",
|
||||||
|
Status: "active",
|
||||||
|
Notes: &scoreOSourceNotes,
|
||||||
|
Source: map[string]any{
|
||||||
|
"schemaVersion": "1",
|
||||||
|
"app": map[string]any{
|
||||||
|
"id": "sample-score-o-001",
|
||||||
|
"title": "积分赛示例",
|
||||||
|
},
|
||||||
|
"branding": map[string]any{
|
||||||
|
"tenantCode": "tenant_demo",
|
||||||
|
"entryChannel": "mini-demo",
|
||||||
|
},
|
||||||
|
"map": map[string]any{
|
||||||
|
"tiles": "../map/lxcb-001/tiles/",
|
||||||
|
"mapmeta": "../map/lxcb-001/tiles/meta.json",
|
||||||
|
},
|
||||||
|
"playfield": map[string]any{
|
||||||
|
"kind": "control-set",
|
||||||
|
"source": map[string]any{
|
||||||
|
"type": "kml",
|
||||||
|
"url": "../kml/lxcb-001/10/c01.kml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"game": map[string]any{
|
||||||
|
"mode": "score-o",
|
||||||
|
},
|
||||||
|
"content": map[string]any{
|
||||||
|
"h5Template": "content-h5-test-template.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo event config source: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
scoreOBuildLog := "demo build generated from sample score-o.json"
|
||||||
|
scoreOBuild, err := s.UpsertEventConfigBuild(ctx, tx, UpsertEventConfigBuildParams{
|
||||||
|
EventID: scoreOEventID,
|
||||||
|
SourceID: scoreOSource.ID,
|
||||||
|
BuildNo: 1,
|
||||||
|
BuildStatus: "success",
|
||||||
|
BuildLog: &scoreOBuildLog,
|
||||||
|
Manifest: map[string]any{
|
||||||
|
"schemaVersion": "1",
|
||||||
|
"releaseId": "rel_demo_score_o_001",
|
||||||
|
"version": "2026.04.01",
|
||||||
|
"app": map[string]any{
|
||||||
|
"id": "sample-score-o-001",
|
||||||
|
"title": "积分赛示例",
|
||||||
|
},
|
||||||
|
"map": map[string]any{
|
||||||
|
"tiles": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/",
|
||||||
|
"mapmeta": "https://oss-mbh5.colormaprun.com/gotomars/map/lxcb-001/tiles/meta.json",
|
||||||
|
},
|
||||||
|
"playfield": map[string]any{
|
||||||
|
"kind": "control-set",
|
||||||
|
"source": map[string]any{
|
||||||
|
"type": "kml",
|
||||||
|
"url": "https://oss-mbh5.colormaprun.com/gotomars/kml/lxcb-001/10/c01.kml",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"game": map[string]any{
|
||||||
|
"mode": "score-o",
|
||||||
|
},
|
||||||
|
"assets": map[string]any{
|
||||||
|
"contentHtml": "https://oss-mbh5.colormaprun.com/gotomars/event/content-h5-test-template.html",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AssetIndex: []map[string]any{
|
||||||
|
{"assetType": "manifest", "assetKey": "manifest"},
|
||||||
|
{"assetType": "mapmeta", "assetKey": "mapmeta"},
|
||||||
|
{"assetType": "playfield", "assetKey": "playfield-kml"},
|
||||||
|
{"assetType": "content_html", "assetKey": "content-html"},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo event config build: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.AttachBuildToRelease(ctx, tx, scoreOReleaseRow.ID, scoreOBuild.ID); err != nil {
|
||||||
|
return nil, fmt.Errorf("attach score-o demo build to release: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scoreOCardPublicID string
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO cards (
|
||||||
|
card_public_id,
|
||||||
|
tenant_id,
|
||||||
|
entry_channel_id,
|
||||||
|
card_type,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
cover_url,
|
||||||
|
event_id,
|
||||||
|
display_slot,
|
||||||
|
display_priority,
|
||||||
|
status
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'card_demo_score_o_001',
|
||||||
|
$1,
|
||||||
|
$2,
|
||||||
|
'event',
|
||||||
|
'Demo Score-O Run',
|
||||||
|
'积分赛联调入口',
|
||||||
|
'https://oss-mbh5.colormaprun.com/gotomars/assets/demo-cover.jpg',
|
||||||
|
$3,
|
||||||
|
'home_primary',
|
||||||
|
98,
|
||||||
|
'active'
|
||||||
|
)
|
||||||
|
ON CONFLICT (card_public_id) DO UPDATE SET
|
||||||
|
tenant_id = EXCLUDED.tenant_id,
|
||||||
|
entry_channel_id = EXCLUDED.entry_channel_id,
|
||||||
|
card_type = EXCLUDED.card_type,
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
subtitle = EXCLUDED.subtitle,
|
||||||
|
cover_url = EXCLUDED.cover_url,
|
||||||
|
event_id = EXCLUDED.event_id,
|
||||||
|
display_slot = EXCLUDED.display_slot,
|
||||||
|
display_priority = EXCLUDED.display_priority,
|
||||||
|
status = EXCLUDED.status
|
||||||
|
RETURNING card_public_id
|
||||||
|
`, tenantID, channelID, scoreOEventID).Scan(&scoreOCardPublicID); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo card: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scoreOCourseSetID, scoreOCourseSetPublicID string
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO course_sets (
|
||||||
|
course_set_public_id, place_id, map_asset_id, code, mode, name, status
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'cset_demo_score_o_001', $1, $2, 'cset-demo-score-o-001', 'score-o', 'Demo Score-O Course Set', 'active'
|
||||||
|
)
|
||||||
|
ON CONFLICT (code) DO UPDATE SET
|
||||||
|
place_id = EXCLUDED.place_id,
|
||||||
|
map_asset_id = EXCLUDED.map_asset_id,
|
||||||
|
mode = EXCLUDED.mode,
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
status = EXCLUDED.status
|
||||||
|
RETURNING id, course_set_public_id
|
||||||
|
`, placeID, mapAssetID).Scan(&scoreOCourseSetID, &scoreOCourseSetPublicID); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo course set: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scoreOCourseVariantID, scoreOCourseVariantPublicID string
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO course_variants (
|
||||||
|
course_variant_public_id, course_set_id, source_id, name, route_code, mode, control_count, status, is_default
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'cvariant_demo_score_o_001', $1, $2, 'Demo Score-O Variant', 'route-score-o-001', 'score-o', 10, 'active', true
|
||||||
|
)
|
||||||
|
ON CONFLICT (course_variant_public_id) DO UPDATE SET
|
||||||
|
course_set_id = EXCLUDED.course_set_id,
|
||||||
|
source_id = EXCLUDED.source_id,
|
||||||
|
name = EXCLUDED.name,
|
||||||
|
route_code = EXCLUDED.route_code,
|
||||||
|
mode = EXCLUDED.mode,
|
||||||
|
control_count = EXCLUDED.control_count,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
is_default = EXCLUDED.is_default
|
||||||
|
RETURNING id, course_variant_public_id
|
||||||
|
`, scoreOCourseSetID, courseSourceID).Scan(&scoreOCourseVariantID, &scoreOCourseVariantPublicID); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo course variant: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tx.Exec(ctx, `
|
||||||
|
UPDATE course_sets
|
||||||
|
SET current_variant_id = $2
|
||||||
|
WHERE id = $1
|
||||||
|
`, scoreOCourseSetID, scoreOCourseVariantID); err != nil {
|
||||||
|
return nil, fmt.Errorf("attach score-o demo course variant: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var scoreORuntimeBindingID, scoreORuntimeBindingPublicID string
|
||||||
|
if err := tx.QueryRow(ctx, `
|
||||||
|
INSERT INTO map_runtime_bindings (
|
||||||
|
runtime_binding_public_id, event_id, place_id, map_asset_id, tile_release_id, course_set_id, course_variant_id, status, notes
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
'runtime_demo_score_o_001', $1, $2, $3, $4, $5, $6, 'active', 'demo score-o runtime binding'
|
||||||
|
)
|
||||||
|
ON CONFLICT (runtime_binding_public_id) DO UPDATE SET
|
||||||
|
event_id = EXCLUDED.event_id,
|
||||||
|
place_id = EXCLUDED.place_id,
|
||||||
|
map_asset_id = EXCLUDED.map_asset_id,
|
||||||
|
tile_release_id = EXCLUDED.tile_release_id,
|
||||||
|
course_set_id = EXCLUDED.course_set_id,
|
||||||
|
course_variant_id = EXCLUDED.course_variant_id,
|
||||||
|
status = EXCLUDED.status,
|
||||||
|
notes = EXCLUDED.notes
|
||||||
|
RETURNING id, runtime_binding_public_id
|
||||||
|
`, scoreOEventID, placeID, mapAssetID, tileReleaseID, scoreOCourseSetID, scoreOCourseVariantID).Scan(&scoreORuntimeBindingID, &scoreORuntimeBindingPublicID); err != nil {
|
||||||
|
return nil, fmt.Errorf("ensure score-o demo runtime binding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
var cleanedSessionCount int64
|
var cleanedSessionCount int64
|
||||||
if err := tx.QueryRow(ctx, `
|
if err := tx.QueryRow(ctx, `
|
||||||
WITH cleaned AS (
|
WITH cleaned AS (
|
||||||
@@ -611,7 +925,7 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
RETURNING 1
|
RETURNING 1
|
||||||
)
|
)
|
||||||
SELECT COUNT(*) FROM cleaned
|
SELECT COUNT(*) FROM cleaned
|
||||||
`, []string{eventID, manualEventID}).Scan(&cleanedSessionCount); err != nil {
|
`, []string{eventID, scoreOEventID, manualEventID}).Scan(&cleanedSessionCount); err != nil {
|
||||||
return nil, fmt.Errorf("cleanup demo ongoing sessions: %w", err)
|
return nil, fmt.Errorf("cleanup demo ongoing sessions: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,23 +934,31 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &DemoBootstrapSummary{
|
return &DemoBootstrapSummary{
|
||||||
TenantCode: "tenant_demo",
|
TenantCode: "tenant_demo",
|
||||||
ChannelCode: "mini-demo",
|
ChannelCode: "mini-demo",
|
||||||
EventID: "evt_demo_001",
|
EventID: "evt_demo_001",
|
||||||
ReleaseID: releaseRow.PublicID,
|
ReleaseID: releaseRow.PublicID,
|
||||||
SourceID: source.ID,
|
SourceID: source.ID,
|
||||||
BuildID: build.ID,
|
BuildID: build.ID,
|
||||||
CardID: cardPublicID,
|
CardID: cardPublicID,
|
||||||
PlaceID: placePublicID,
|
PlaceID: placePublicID,
|
||||||
MapAssetID: mapAssetPublicID,
|
MapAssetID: mapAssetPublicID,
|
||||||
TileReleaseID: tileReleasePublicID,
|
TileReleaseID: tileReleasePublicID,
|
||||||
CourseSourceID: courseSourcePublicID,
|
CourseSourceID: courseSourcePublicID,
|
||||||
CourseSetID: courseSetPublicID,
|
CourseSetID: courseSetPublicID,
|
||||||
CourseVariantID: courseVariantPublicID,
|
CourseVariantID: courseVariantPublicID,
|
||||||
RuntimeBindingID: runtimeBindingPublicID,
|
RuntimeBindingID: runtimeBindingPublicID,
|
||||||
VariantManualEventID: "evt_demo_variant_manual_001",
|
ScoreOEventID: "evt_demo_score_o_001",
|
||||||
VariantManualRelease: manualReleaseRow.PublicID,
|
ScoreOReleaseID: scoreOReleaseRow.PublicID,
|
||||||
VariantManualCardID: manualCardPublicID,
|
ScoreOCardID: scoreOCardPublicID,
|
||||||
CleanedSessionCount: cleanedSessionCount,
|
ScoreOSourceID: scoreOSource.ID,
|
||||||
|
ScoreOBuildID: scoreOBuild.ID,
|
||||||
|
ScoreOCourseSetID: scoreOCourseSetPublicID,
|
||||||
|
ScoreOCourseVariantID: scoreOCourseVariantPublicID,
|
||||||
|
ScoreORuntimeBindingID: scoreORuntimeBindingPublicID,
|
||||||
|
VariantManualEventID: "evt_demo_variant_manual_001",
|
||||||
|
VariantManualRelease: manualReleaseRow.PublicID,
|
||||||
|
VariantManualCardID: manualCardPublicID,
|
||||||
|
CleanedSessionCount: cleanedSessionCount,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 程序默认规则基线
|
# 程序默认规则基线
|
||||||
> 文档版本:v1.0
|
> 文档版本:v1.1
|
||||||
> 最后更新:2026-04-02 08:28:05
|
> 最后更新:2026-04-03 20:40:00
|
||||||
|
|
||||||
|
|
||||||
本文档用于定义当前客户端在**不依赖活动配置细项**时,程序层应该内建的默认规则。
|
本文档用于定义当前客户端在**不依赖活动配置细项**时,程序层应该内建的默认规则。
|
||||||
@@ -129,6 +129,7 @@
|
|||||||
- 成功打开始点后开始计时
|
- 成功打开始点后开始计时
|
||||||
- 起点完成后只给短反馈,并更新引导和 HUD
|
- 起点完成后只给短反馈,并更新引导和 HUD
|
||||||
- 默认不弹白色开始卡
|
- 默认不弹白色开始卡
|
||||||
|
- 从准备页进入地图即视为进入对局,不再额外要求点击开始按钮
|
||||||
- 默认不弹答题卡
|
- 默认不弹答题卡
|
||||||
|
|
||||||
### 3.2 普通点
|
### 3.2 普通点
|
||||||
|
|||||||
182
doc/gameplay/联调架构阶段总结.md
Normal file
182
doc/gameplay/联调架构阶段总结.md
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
# 联调架构阶段总结
|
||||||
|
> 文档版本:v1.0
|
||||||
|
> 最后更新:2026-04-03 16:59:19
|
||||||
|
|
||||||
|
## 1. 当前结论
|
||||||
|
|
||||||
|
当前联调架构已经从“能联”升级为“可诊断、可回归、可收口”。
|
||||||
|
|
||||||
|
这次阶段性进步的核心不是多了几个接口,而是三条链一起立住了:
|
||||||
|
|
||||||
|
1. 标准测试链
|
||||||
|
2. 结构化诊断链
|
||||||
|
3. 多线程协作链
|
||||||
|
|
||||||
|
也就是说,当前联调已经不再主要依赖:
|
||||||
|
|
||||||
|
- 截图
|
||||||
|
- 口头描述
|
||||||
|
- 各自猜测
|
||||||
|
|
||||||
|
而是可以依赖统一入口、统一日志和统一回写口径来定位问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 标准测试链
|
||||||
|
|
||||||
|
backend 当前已经把联调入口收敛成标准路径:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Bootstrap Demo
|
||||||
|
-> 一键补齐 Runtime 并发布
|
||||||
|
-> 一键标准回归
|
||||||
|
-> play / launch / result / history 验证
|
||||||
|
```
|
||||||
|
|
||||||
|
当前这条链的价值是:
|
||||||
|
|
||||||
|
- 从空白环境直接起链
|
||||||
|
- 不再手工预铺多份 demo 对象
|
||||||
|
- 同一条测试链可以反复执行
|
||||||
|
- 回归结果有统一出口
|
||||||
|
|
||||||
|
当前 workbench 已具备:
|
||||||
|
|
||||||
|
- `Bootstrap Demo`
|
||||||
|
- `一键补齐 Runtime 并发布`
|
||||||
|
- `一键标准回归`
|
||||||
|
- `回归结果汇总`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 稳定测试数据链
|
||||||
|
|
||||||
|
当前联调环境已经不再只靠临时假数据,而是开始切入更接近生产的真实输入。
|
||||||
|
|
||||||
|
当前已接入:
|
||||||
|
|
||||||
|
- 真实 KML / 赛道文件
|
||||||
|
- 真实地图资源 URL
|
||||||
|
- manual 多赛道双 KML 输入
|
||||||
|
- 三类显式 demo 入口:
|
||||||
|
- `evt_demo_001`
|
||||||
|
- `evt_demo_score_o_001`
|
||||||
|
- `evt_demo_variant_manual_001`
|
||||||
|
|
||||||
|
当前阶段这条链的意义是:
|
||||||
|
|
||||||
|
- 前后端终于在测同一套对象
|
||||||
|
- demo 数据不再漂
|
||||||
|
- 联调结果更接近生产环境
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 结构化诊断链
|
||||||
|
|
||||||
|
这次联调真正发生质变的关键,是结构化诊断口径已经建立。
|
||||||
|
|
||||||
|
backend 当前已提供:
|
||||||
|
|
||||||
|
- 分步执行日志
|
||||||
|
- 真实错误
|
||||||
|
- stack
|
||||||
|
- 最后一次 curl
|
||||||
|
- 预期判定
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
|
|
||||||
|
frontend 当前已配合提供:
|
||||||
|
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
- 首页、活动页、准备页、地图关键链路的主动上报
|
||||||
|
- 更明确的本地诊断字段:
|
||||||
|
- `details.seq`
|
||||||
|
- `launchVariantId`
|
||||||
|
- `runtimeCourseVariantId`
|
||||||
|
|
||||||
|
当前这条结构化诊断链意味着:
|
||||||
|
|
||||||
|
- 不再只知道“失败了”
|
||||||
|
- 可以知道:
|
||||||
|
- 卡在哪一步
|
||||||
|
- 当前 launch 实际拿到了什么
|
||||||
|
- 前端消费到了什么
|
||||||
|
- 是后端发布问题、前端消费问题,还是规则理解问题
|
||||||
|
|
||||||
|
这也是为什么最近某些问题反复修改多轮仍未命中,而补上结构化日志后能一次定位成功。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 多线程协作链
|
||||||
|
|
||||||
|
当前多线程联调已经形成稳定协作方式:
|
||||||
|
|
||||||
|
- 总控 -> 后端:
|
||||||
|
- [t2b.md](D:/dev/cmr-mini/t2b.md)
|
||||||
|
- 后端 -> 总控:
|
||||||
|
- [b2t.md](D:/dev/cmr-mini/b2t.md)
|
||||||
|
- 总控 -> 前端:
|
||||||
|
- [t2f.md](D:/dev/cmr-mini/t2f.md)
|
||||||
|
- 前端 -> 总控:
|
||||||
|
- [f2t.md](D:/dev/cmr-mini/f2t.md)
|
||||||
|
|
||||||
|
这条协作链的作用是:
|
||||||
|
|
||||||
|
- 前后端不再互相口头转述
|
||||||
|
- 总控能统一收口
|
||||||
|
- 阶段性结论能及时沉淀回:
|
||||||
|
- [文档索引](D:/dev/cmr-mini/doc/文档索引.md)
|
||||||
|
- [readme-develop.md](D:/dev/cmr-mini/readme-develop.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 当前阶段结论
|
||||||
|
|
||||||
|
可以把当前状态明确成:
|
||||||
|
|
||||||
|
### 6.1 已完成
|
||||||
|
|
||||||
|
- 基础骨架
|
||||||
|
- 活动运营域摘要第一刀
|
||||||
|
- 联调标准化第一版
|
||||||
|
|
||||||
|
### 6.2 正在推进
|
||||||
|
|
||||||
|
- 真实输入替换
|
||||||
|
- 更接近生产的联调环境
|
||||||
|
|
||||||
|
### 6.3 暂不启动
|
||||||
|
|
||||||
|
- 活动卡片(列表)产品化
|
||||||
|
- 新玩家侧页面扩张
|
||||||
|
- 更复杂后台运营功能
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 下一步建议
|
||||||
|
|
||||||
|
当前下一步不再是继续搭骨架,而是继续把真实输入往活动层推进。
|
||||||
|
|
||||||
|
优先顺序建议:
|
||||||
|
|
||||||
|
1. `content manifest`
|
||||||
|
2. `presentation schema`
|
||||||
|
3. 活动文案样例
|
||||||
|
|
||||||
|
同时继续保持:
|
||||||
|
|
||||||
|
- 前端只做联调回归和小修
|
||||||
|
- 后端继续保证一键回归链稳定
|
||||||
|
- 排障优先看:
|
||||||
|
- `回归结果汇总`
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
|
- `前端调试日志`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 一句话总结
|
||||||
|
|
||||||
|
当前联调架构已经从“人肉协作”升级成:
|
||||||
|
|
||||||
|
**标准测试链 + 结构化诊断链 + 多线程协作链**
|
||||||
|
|
||||||
|
这代表系统已经从“能跑”进入“可持续联调、可持续收口、可逐步逼近生产”的阶段。
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# 积分赛规则说明文档
|
# 积分赛规则说明文档
|
||||||
> 文档版本:v1.0
|
> 文档版本:v1.1
|
||||||
> 最后更新:2026-04-02 08:28:05
|
> 最后更新:2026-04-03 20:40:00
|
||||||
|
|
||||||
|
|
||||||
本文档用于定义 `score-o` 在**最小模板**下的系统默认规则,作为后续实现、联调和配置扩展的共同基线。
|
本文档用于定义 `score-o` 在**最小模板**下的系统默认规则,作为后续实现、联调和配置扩展的共同基线。
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
- 基础 HUD
|
- 基础 HUD
|
||||||
- 所有积分点和结束点默认不显示
|
- 所有积分点和结束点默认不显示
|
||||||
- 页面提示玩家:需要先打开始点,比赛才会正式开始并开始计时
|
- 页面提示玩家:需要先打开始点,比赛才会正式开始并开始计时
|
||||||
|
- 从准备页进入地图即视为进入本局,不再额外要求点击开始按钮
|
||||||
|
|
||||||
### 3.2 打开始点
|
### 3.2 打开始点
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 顺序打点规则说明文档
|
# 顺序打点规则说明文档
|
||||||
> 文档版本:v1.0
|
> 文档版本:v1.1
|
||||||
> 最后更新:2026-04-02 08:28:05
|
> 最后更新:2026-04-03 20:40:00
|
||||||
|
|
||||||
|
|
||||||
本文档用于定义 `classic-sequential` 在**最小模板**下的系统默认规则,作为后续实现、联调和配置扩展的共同基线。
|
本文档用于定义 `classic-sequential` 在**最小模板**下的系统默认规则,作为后续实现、联调和配置扩展的共同基线。
|
||||||
@@ -46,6 +46,7 @@
|
|||||||
- 基础 HUD
|
- 基础 HUD
|
||||||
- 普通控制点、终点、路线和腿线默认不显示
|
- 普通控制点、终点、路线和腿线默认不显示
|
||||||
- 页面提示玩家:需要先打开始点,比赛才会正式开始并开始计时
|
- 页面提示玩家:需要先打开始点,比赛才会正式开始并开始计时
|
||||||
|
- 从准备页进入地图即视为进入本局,不再额外要求点击开始按钮
|
||||||
- 最小模板下,点击检查点默认不弹详情卡
|
- 最小模板下,点击检查点默认不弹详情卡
|
||||||
|
|
||||||
### 3.2 打开始点
|
### 3.2 打开始点
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 文档索引
|
# 文档索引
|
||||||
> 文档版本:v1.3
|
> 文档版本:v1.4
|
||||||
> 最后更新:2026-04-03 19:38:00
|
> 最后更新:2026-04-03 16:59:19
|
||||||
|
|
||||||
维护约定:
|
维护约定:
|
||||||
|
|
||||||
@@ -44,6 +44,7 @@
|
|||||||
- [多赛道 Variant 五层设计草案](/D:/dev/cmr-mini/doc/gameplay/多赛道Variant五层设计草案.md)
|
- [多赛道 Variant 五层设计草案](/D:/dev/cmr-mini/doc/gameplay/多赛道Variant五层设计草案.md)
|
||||||
- [多赛道 Variant 前后端最小契约](/D:/dev/cmr-mini/doc/gameplay/多赛道Variant前后端最小契约.md)
|
- [多赛道 Variant 前后端最小契约](/D:/dev/cmr-mini/doc/gameplay/多赛道Variant前后端最小契约.md)
|
||||||
- [多线程联调协作方式](/D:/dev/cmr-mini/doc/gameplay/多线程联调协作方式.md)
|
- [多线程联调协作方式](/D:/dev/cmr-mini/doc/gameplay/多线程联调协作方式.md)
|
||||||
|
- [联调架构阶段总结](/D:/dev/cmr-mini/doc/gameplay/联调架构阶段总结.md)
|
||||||
- [APP全局产品架构草案](/D:/dev/cmr-mini/doc/gameplay/APP全局产品架构草案.md)
|
- [APP全局产品架构草案](/D:/dev/cmr-mini/doc/gameplay/APP全局产品架构草案.md)
|
||||||
- [故障恢复机制](/D:/dev/cmr-mini/doc/gameplay/故障恢复机制.md)
|
- [故障恢复机制](/D:/dev/cmr-mini/doc/gameplay/故障恢复机制.md)
|
||||||
- [活动运营域摘要第一刀联调回归清单](/D:/dev/cmr-mini/doc/gameplay/活动运营域摘要第一刀联调回归清单.md)
|
- [活动运营域摘要第一刀联调回归清单](/D:/dev/cmr-mini/doc/gameplay/活动运营域摘要第一刀联调回归清单.md)
|
||||||
|
|||||||
96
f2b.md
96
f2b.md
@@ -1,6 +1,6 @@
|
|||||||
# F2B 协作清单
|
# F2B 协作清单
|
||||||
> 文档版本:v1.5
|
> 文档版本:v1.9
|
||||||
> 最后更新:2026-04-03 20:02:00
|
> 最后更新:2026-04-03 16:45:26
|
||||||
|
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
@@ -264,6 +264,87 @@
|
|||||||
- 无
|
- 无
|
||||||
- 状态:已完成
|
- 状态:已完成
|
||||||
|
|
||||||
|
### F2B-D006
|
||||||
|
|
||||||
|
- 时间:2026-04-03
|
||||||
|
- 提出方:前端
|
||||||
|
- 当前事实:
|
||||||
|
- 已按 backend `B2F-028` 的排查口径补充前端诊断链,当前地图信息面板/赛后结果里可直接查看:
|
||||||
|
- `launch.config.configUrl`
|
||||||
|
- `launch.resolvedRelease.manifestUrl`
|
||||||
|
- `launch.config.releaseId`
|
||||||
|
- `launch.resolvedRelease.releaseId`
|
||||||
|
- 最终加载后的:
|
||||||
|
- `Schema版本`
|
||||||
|
- `场地类型(playfield.kind)`
|
||||||
|
- `模式编码(game.mode)`
|
||||||
|
- 当前只补了诊断与观测,没有改动正式 launch 主链
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
### F2B-D007
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:26:37
|
||||||
|
- 提出方:前端
|
||||||
|
- 当前事实:
|
||||||
|
- 已按 `B2F-030` 接入 backend `POST /dev/client-logs`
|
||||||
|
- 当前关键阶段会主动上报最小调试日志:
|
||||||
|
- `entry-home`
|
||||||
|
- `event-play`
|
||||||
|
- `event-prepare`
|
||||||
|
- `launch-diagnostic`
|
||||||
|
- `runtime-compiler`
|
||||||
|
- `session-recovery`
|
||||||
|
- 当前主日志字段已按 backend 建议最小口径回传:
|
||||||
|
- `source`
|
||||||
|
- `level`
|
||||||
|
- `category`
|
||||||
|
- `message`
|
||||||
|
- `eventId`
|
||||||
|
- `releaseId`
|
||||||
|
- `sessionId`
|
||||||
|
- `manifestUrl`
|
||||||
|
- `route`
|
||||||
|
- `details.phase`
|
||||||
|
- `details.schemaVersion`
|
||||||
|
- `details.playfield.kind`
|
||||||
|
- `details.game.mode`
|
||||||
|
- 模拟器日志不再作为当前联调主诊断口,保留地图内调试面板作为本地辅助能力
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
### F2B-D008
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:45:26
|
||||||
|
- 提出方:前端
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已通过 `B2F-031` 明确确认:积分赛误进顺序赛的根因不是前端解析,而是首页卡片入口配置错误
|
||||||
|
- 具体根因为:
|
||||||
|
- 首页卡片查询此前只取 `home_primary`
|
||||||
|
- 积分赛 demo 卡此前被种到 `home_secondary`
|
||||||
|
- 前端首页因此根本拿不到 `evt_demo_score_o_001`
|
||||||
|
- backend 已修复积分赛卡片入口配置
|
||||||
|
- 前端当前无需再为该问题修改玩法解析或 manifest 消费逻辑
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
|
### F2B-D009
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:45:26
|
||||||
|
- 提出方:前端
|
||||||
|
- 当前事实:
|
||||||
|
- 已按 `B2F-032` 优化前端结构化调试日志口径:
|
||||||
|
- 非多赛道玩法时,不再上报空字符串形式的 `assignmentMode`
|
||||||
|
- 非手选赛道时,不再把空 `variantId` 伪装成已选赛道
|
||||||
|
- 所有 client log 现在都会附带前端本地递增 `details.seq`
|
||||||
|
- `launchVariantId` 与 `runtimeCourseVariantId` 已明确区分
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 状态:已完成
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
@@ -311,4 +392,15 @@
|
|||||||
- 无
|
- 无
|
||||||
- 状态:前端执行中
|
- 状态:前端执行中
|
||||||
|
|
||||||
|
### F2B-N005
|
||||||
|
|
||||||
|
- 时间:2026-04-03
|
||||||
|
- 提出方:前端
|
||||||
|
- 当前事实:
|
||||||
|
- 当前已具备积分赛 demo 发布链诊断信息,下一步将按 backend 一键测试环境回归 `evt_demo_score_o_001`
|
||||||
|
- 如仍表现为顺序赛,前端将回传 launch/config/runtime 三段事实,不再只报“现象”
|
||||||
|
- 需要对方确认什么:
|
||||||
|
- 无
|
||||||
|
- 状态:前端执行中
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
39
f2t.md
39
f2t.md
@@ -1,6 +1,6 @@
|
|||||||
# F2T 协作清单
|
# F2T 协作清单
|
||||||
> 文档版本:v1.7
|
> 文档版本:v1.9
|
||||||
> 最后更新:2026-04-03 19:48:00
|
> 最后更新:2026-04-03 16:45:26
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
@@ -172,6 +172,41 @@
|
|||||||
- 无
|
- 无
|
||||||
- 是否已解决:是
|
- 是否已解决:是
|
||||||
|
|
||||||
|
### F2T-D006
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:26:37
|
||||||
|
- 谁提的:frontend
|
||||||
|
- 当前事实:
|
||||||
|
- 已按 backend 新增 dev 调试接口切换当前联调诊断主出口:
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
- 当前首页、活动页、准备页、地图关键链路会主动上报:
|
||||||
|
- `entry-home`
|
||||||
|
- `event-play`
|
||||||
|
- `event-prepare`
|
||||||
|
- `launch-diagnostic`
|
||||||
|
- `runtime-compiler`
|
||||||
|
- `session-recovery`
|
||||||
|
- 登录后自动连接模拟器日志的链路已撤掉
|
||||||
|
- 地图内调试面板继续保留,仅作为本地开发辅助,不再作为当前联调主诊断口
|
||||||
|
- 需要确认什么:
|
||||||
|
- 无
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
|
### F2T-D007
|
||||||
|
|
||||||
|
- 时间:2026-04-03 16:45:26
|
||||||
|
- 谁提的:frontend
|
||||||
|
- 当前事实:
|
||||||
|
- backend 已确认积分赛误进顺序赛的根因在 backend demo 首页卡片入口配置,不在前端玩法解析
|
||||||
|
- 前端本轮未再修改 runtime / manifest 消费主链
|
||||||
|
- 前端仅补了联调日志口径优化:
|
||||||
|
- 非多赛道玩法不再上报空字符串 `assignmentMode`
|
||||||
|
- 日志新增前端本地递增 `details.seq`
|
||||||
|
- `launchVariantId` 与 `runtimeCourseVariantId` 明确区分
|
||||||
|
- 需要确认什么:
|
||||||
|
- 无
|
||||||
|
- 是否已解决:是
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|||||||
@@ -1108,6 +1108,7 @@ export class MapEngine {
|
|||||||
configAppId: string
|
configAppId: string
|
||||||
configSchemaVersion: string
|
configSchemaVersion: string
|
||||||
configVersion: string
|
configVersion: string
|
||||||
|
playfieldKind: string
|
||||||
controlScoreOverrides: Record<string, number>
|
controlScoreOverrides: Record<string, number>
|
||||||
controlContentOverrides: Record<string, GameControlDisplayContentOverride>
|
controlContentOverrides: Record<string, GameControlDisplayContentOverride>
|
||||||
defaultControlContentOverride: GameControlDisplayContentOverride | null
|
defaultControlContentOverride: GameControlDisplayContentOverride | null
|
||||||
@@ -1417,6 +1418,7 @@ export class MapEngine {
|
|||||||
this.configAppId = ''
|
this.configAppId = ''
|
||||||
this.configSchemaVersion = '1'
|
this.configSchemaVersion = '1'
|
||||||
this.configVersion = ''
|
this.configVersion = ''
|
||||||
|
this.playfieldKind = ''
|
||||||
this.controlScoreOverrides = {}
|
this.controlScoreOverrides = {}
|
||||||
this.controlContentOverrides = {}
|
this.controlContentOverrides = {}
|
||||||
this.defaultControlContentOverride = null
|
this.defaultControlContentOverride = null
|
||||||
@@ -1721,6 +1723,8 @@ export class MapEngine {
|
|||||||
{ label: '比赛名称', value: title || '--' },
|
{ label: '比赛名称', value: title || '--' },
|
||||||
{ label: '配置版本', value: this.configVersion || '--' },
|
{ label: '配置版本', value: this.configVersion || '--' },
|
||||||
{ label: 'Schema版本', value: this.configSchemaVersion || '--' },
|
{ label: 'Schema版本', value: this.configSchemaVersion || '--' },
|
||||||
|
{ label: '场地类型', value: this.playfieldKind || '--' },
|
||||||
|
{ label: '模式编码', value: this.gameMode || '--' },
|
||||||
{ label: '活动ID', value: this.configAppId || '--' },
|
{ label: '活动ID', value: this.configAppId || '--' },
|
||||||
{ label: '动画等级', value: formatAnimationLevelText(this.state.animationLevel) },
|
{ label: '动画等级', value: formatAnimationLevelText(this.state.animationLevel) },
|
||||||
{ label: '地图', value: this.state.mapName || '--' },
|
{ label: '地图', value: this.state.mapName || '--' },
|
||||||
@@ -3423,8 +3427,8 @@ export class MapEngine {
|
|||||||
this.courseOverlayVisible = true
|
this.courseOverlayVisible = true
|
||||||
const gameModeText = this.gameMode === 'score-o' ? '积分赛' : '顺序打点'
|
const gameModeText = this.gameMode === 'score-o' ? '积分赛' : '顺序打点'
|
||||||
const defaultStatusText = this.currentGpsPoint
|
const defaultStatusText = this.currentGpsPoint
|
||||||
? `${gameModeText}已开始 (${this.buildVersion})`
|
? `已进入${gameModeText},请先打开始点 (${this.buildVersion})`
|
||||||
: `${gameModeText}已开始,GPS定位启动中 (${this.buildVersion})`
|
: `已进入${gameModeText},GPS定位启动中,请先打开始点 (${this.buildVersion})`
|
||||||
this.commitGameResult(gameResult, defaultStatusText)
|
this.commitGameResult(gameResult, defaultStatusText)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3683,6 +3687,15 @@ export class MapEngine {
|
|||||||
this.mockSimulatorDebugLogger.disconnect()
|
this.mockSimulatorDebugLogger.disconnect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleEmitMockDebugLog(
|
||||||
|
scope: string,
|
||||||
|
level: 'info' | 'warn' | 'error',
|
||||||
|
message: string,
|
||||||
|
payload?: Record<string, unknown>,
|
||||||
|
): void {
|
||||||
|
this.mockSimulatorDebugLogger.log(scope, level, message, payload)
|
||||||
|
}
|
||||||
|
|
||||||
handleSetGameMode(nextMode: 'classic-sequential' | 'score-o'): void {
|
handleSetGameMode(nextMode: 'classic-sequential' | 'score-o'): void {
|
||||||
if (this.gameMode === nextMode) {
|
if (this.gameMode === nextMode) {
|
||||||
return
|
return
|
||||||
@@ -3882,6 +3895,7 @@ export class MapEngine {
|
|||||||
this.configAppId = config.configAppId
|
this.configAppId = config.configAppId
|
||||||
this.configSchemaVersion = config.configSchemaVersion
|
this.configSchemaVersion = config.configSchemaVersion
|
||||||
this.configVersion = config.configVersion
|
this.configVersion = config.configVersion
|
||||||
|
this.playfieldKind = config.playfieldKind
|
||||||
this.controlScoreOverrides = config.controlScoreOverrides
|
this.controlScoreOverrides = config.controlScoreOverrides
|
||||||
this.controlContentOverrides = config.controlContentOverrides
|
this.controlContentOverrides = config.controlContentOverrides
|
||||||
this.defaultControlContentOverride = config.defaultControlContentOverride
|
this.defaultControlContentOverride = config.defaultControlContentOverride
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ function getGuidanceEffects(
|
|||||||
|
|
||||||
function buildPunchHintText(definition: GameDefinition, state: GameSessionState, currentTarget: GameControl | null): string {
|
function buildPunchHintText(definition: GameDefinition, state: GameSessionState, currentTarget: GameControl | null): string {
|
||||||
if (state.status === 'idle') {
|
if (state.status === 'idle') {
|
||||||
return '点击开始后先打开始点'
|
return '先打开始点即可正式开始比赛'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status === 'finished') {
|
if (state.status === 'finished') {
|
||||||
|
|||||||
@@ -271,7 +271,7 @@ function buildPunchHintText(
|
|||||||
focusedTarget: GameControl | null,
|
focusedTarget: GameControl | null,
|
||||||
): string {
|
): string {
|
||||||
if (state.status === 'idle') {
|
if (state.status === 'idle') {
|
||||||
return '点击开始后先打开始点'
|
return '先打开始点即可正式开始比赛'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.status === 'finished') {
|
if (state.status === 'finished') {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { getEventPlay, launchEvent, type BackendCourseVariantSummary, type Backe
|
|||||||
import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
|
import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter'
|
||||||
import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
|
import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
|
||||||
import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
|
import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch'
|
||||||
|
import { reportBackendClientLog } from '../../utils/backendClientLogs'
|
||||||
import { HeartRateController } from '../../engine/sensor/heartRateController'
|
import { HeartRateController } from '../../engine/sensor/heartRateController'
|
||||||
|
|
||||||
const PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY = 'cmr.preferredHeartRateDevice'
|
const PREFERRED_HEART_RATE_DEVICE_STORAGE_KEY = 'cmr.preferredHeartRateDevice'
|
||||||
@@ -290,12 +291,32 @@ Page({
|
|||||||
result.play.assignmentMode,
|
result.play.assignmentMode,
|
||||||
result.play.courseVariants,
|
result.play.courseVariants,
|
||||||
)
|
)
|
||||||
|
const assignmentMode = result.play.assignmentMode ? result.play.assignmentMode : null
|
||||||
|
const logVariantId = assignmentMode === 'manual' && selectedVariantId ? selectedVariantId : null
|
||||||
const selectableVariants = buildSelectableVariants(
|
const selectableVariants = buildSelectableVariants(
|
||||||
selectedVariantId,
|
selectedVariantId,
|
||||||
result.play.assignmentMode,
|
result.play.assignmentMode,
|
||||||
result.play.courseVariants,
|
result.play.courseVariants,
|
||||||
)
|
)
|
||||||
const selectedVariant = selectableVariants.find((item) => item.id === selectedVariantId) || null
|
const selectedVariant = selectableVariants.find((item) => item.id === selectedVariantId) || null
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'event-prepare',
|
||||||
|
message: 'prepare play loaded',
|
||||||
|
eventId: result.event.id || this.data.eventId || '',
|
||||||
|
releaseId: result.resolvedRelease && result.resolvedRelease.releaseId
|
||||||
|
? result.resolvedRelease.releaseId
|
||||||
|
: '',
|
||||||
|
manifestUrl: result.resolvedRelease && result.resolvedRelease.manifestUrl
|
||||||
|
? result.resolvedRelease.manifestUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
pageEventId: this.data.eventId || '',
|
||||||
|
resultEventId: result.event.id || '',
|
||||||
|
selectedVariantId: logVariantId,
|
||||||
|
assignmentMode,
|
||||||
|
},
|
||||||
|
})
|
||||||
this.setData({
|
this.setData({
|
||||||
loading: false,
|
loading: false,
|
||||||
titleText: `${result.event.displayName} / 开始前准备`,
|
titleText: `${result.event.displayName} / 开始前准备`,
|
||||||
@@ -586,6 +607,22 @@ Page({
|
|||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const assignmentMode = this.data.assignmentMode ? this.data.assignmentMode : null
|
||||||
|
const selectedVariantId = assignmentMode === 'manual' && this.data.selectedVariantId
|
||||||
|
? this.data.selectedVariantId
|
||||||
|
: null
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'event-prepare',
|
||||||
|
message: 'launch requested',
|
||||||
|
eventId: this.data.eventId || '',
|
||||||
|
details: {
|
||||||
|
pageEventId: this.data.eventId || '',
|
||||||
|
selectedVariantId,
|
||||||
|
assignmentMode,
|
||||||
|
phase: 'launch-requested',
|
||||||
|
},
|
||||||
|
})
|
||||||
const app = getApp<IAppOption>()
|
const app = getApp<IAppOption>()
|
||||||
if (app.globalData) {
|
if (app.globalData) {
|
||||||
const pendingDeviceName = prepareHeartRateController && prepareHeartRateController.currentDeviceName
|
const pendingDeviceName = prepareHeartRateController && prepareHeartRateController.currentDeviceName
|
||||||
@@ -608,6 +645,32 @@ Page({
|
|||||||
clientType: 'wechat',
|
clientType: 'wechat',
|
||||||
deviceKey: 'mini-dev-device-001',
|
deviceKey: 'mini-dev-device-001',
|
||||||
})
|
})
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'event-prepare',
|
||||||
|
message: 'launch response received',
|
||||||
|
eventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : this.data.eventId || '',
|
||||||
|
releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
|
||||||
|
sessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
|
||||||
|
manifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
|
||||||
|
? result.launch.resolvedRelease.manifestUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
pageEventId: this.data.eventId || '',
|
||||||
|
launchEventId: result.launch.business && result.launch.business.eventId ? result.launch.business.eventId : '',
|
||||||
|
launchSessionId: result.launch.business && result.launch.business.sessionId ? result.launch.business.sessionId : '',
|
||||||
|
configUrl: result.launch.config && result.launch.config.configUrl ? result.launch.config.configUrl : '',
|
||||||
|
releaseId: result.launch.config && result.launch.config.releaseId ? result.launch.config.releaseId : '',
|
||||||
|
resolvedReleaseId: result.launch.resolvedRelease && result.launch.resolvedRelease.releaseId
|
||||||
|
? result.launch.resolvedRelease.releaseId
|
||||||
|
: '',
|
||||||
|
resolvedManifestUrl: result.launch.resolvedRelease && result.launch.resolvedRelease.manifestUrl
|
||||||
|
? result.launch.resolvedRelease.manifestUrl
|
||||||
|
: '',
|
||||||
|
launchVariantId: result.launch.variant && result.launch.variant.id ? result.launch.variant.id : null,
|
||||||
|
phase: 'launch-response',
|
||||||
|
},
|
||||||
|
})
|
||||||
const envelope = adaptBackendLaunchResultToEnvelope(result)
|
const envelope = adaptBackendLaunchResultToEnvelope(result)
|
||||||
wx.navigateTo({
|
wx.navigateTo({
|
||||||
url: prepareMapPageUrlForLaunch(envelope),
|
url: prepareMapPageUrlForLaunch(envelope),
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
||||||
import { getEventPlay, type BackendEventPlayResult } from '../../utils/backendApi'
|
import { getEventPlay, type BackendEventPlayResult } from '../../utils/backendApi'
|
||||||
import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
|
import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy'
|
||||||
|
import { reportBackendClientLog } from '../../utils/backendClientLogs'
|
||||||
|
|
||||||
type EventPageData = {
|
type EventPageData = {
|
||||||
eventId: string
|
eventId: string
|
||||||
@@ -130,6 +131,26 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
applyEventPlay(result: BackendEventPlayResult) {
|
applyEventPlay(result: BackendEventPlayResult) {
|
||||||
|
const assignmentMode = result.play.assignmentMode ? result.play.assignmentMode : null
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'event-play',
|
||||||
|
message: 'event play loaded',
|
||||||
|
eventId: result.event.id || this.data.eventId || '',
|
||||||
|
releaseId: result.resolvedRelease && result.resolvedRelease.releaseId
|
||||||
|
? result.resolvedRelease.releaseId
|
||||||
|
: '',
|
||||||
|
manifestUrl: result.resolvedRelease && result.resolvedRelease.manifestUrl
|
||||||
|
? result.resolvedRelease.manifestUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
pageEventId: this.data.eventId || '',
|
||||||
|
resultEventId: result.event.id || '',
|
||||||
|
primaryAction: result.play.primaryAction || '',
|
||||||
|
assignmentMode,
|
||||||
|
variantCount: result.play.courseVariants ? result.play.courseVariants.length : 0,
|
||||||
|
},
|
||||||
|
})
|
||||||
this.setData({
|
this.setData({
|
||||||
loading: false,
|
loading: false,
|
||||||
titleText: result.event.displayName,
|
titleText: result.event.displayName,
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { clearBackendAuthTokens, loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
import { clearBackendAuthTokens, loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth'
|
||||||
import { getEntryHome, type BackendCardResult, type BackendEntryHomeResult } from '../../utils/backendApi'
|
import { getEntryHome, type BackendCardResult, type BackendEntryHomeResult } from '../../utils/backendApi'
|
||||||
|
import { reportBackendClientLog } from '../../utils/backendClientLogs'
|
||||||
|
import { setGlobalMockDebugBridgeEnabled } from '../../utils/globalMockDebugBridge'
|
||||||
|
|
||||||
const DEFAULT_CHANNEL_CODE = 'mini-demo'
|
const DEFAULT_CHANNEL_CODE = 'mini-demo'
|
||||||
const DEFAULT_CHANNEL_TYPE = 'wechat_mini'
|
const DEFAULT_CHANNEL_TYPE = 'wechat_mini'
|
||||||
@@ -100,6 +102,18 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
applyEntryHomeResult(result: BackendEntryHomeResult) {
|
applyEntryHomeResult(result: BackendEntryHomeResult) {
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'entry-home',
|
||||||
|
message: 'entry home loaded',
|
||||||
|
details: {
|
||||||
|
ongoingSessionId: result.ongoingSession && result.ongoingSession.id ? result.ongoingSession.id : '',
|
||||||
|
ongoingEventId: result.ongoingSession && result.ongoingSession.eventId ? result.ongoingSession.eventId : '',
|
||||||
|
recentSessionId: result.recentSession && result.recentSession.id ? result.recentSession.id : '',
|
||||||
|
recentEventId: result.recentSession && result.recentSession.eventId ? result.recentSession.eventId : '',
|
||||||
|
cardEventIds: (result.cards || []).map((item) => (item.event && item.event.id ? item.event.id : '')),
|
||||||
|
},
|
||||||
|
})
|
||||||
this.setData({
|
this.setData({
|
||||||
loading: false,
|
loading: false,
|
||||||
statusText: '首页加载完成',
|
statusText: '首页加载完成',
|
||||||
@@ -141,6 +155,7 @@ Page({
|
|||||||
|
|
||||||
handleLogout() {
|
handleLogout() {
|
||||||
clearBackendAuthTokens()
|
clearBackendAuthTokens()
|
||||||
|
setGlobalMockDebugBridgeEnabled(false)
|
||||||
const app = getApp<IAppOption>()
|
const app = getApp<IAppOption>()
|
||||||
if (app.globalData) {
|
if (app.globalData) {
|
||||||
app.globalData.backendAuthTokens = null
|
app.globalData.backendAuthTokens = null
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { clearBackendAuthTokens, saveBackendAuthTokens, saveBackendBaseUrl } from '../../utils/backendAuth'
|
import { clearBackendAuthTokens, saveBackendAuthTokens, saveBackendBaseUrl } from '../../utils/backendAuth'
|
||||||
import { loginWechatMini } from '../../utils/backendApi'
|
import { loginWechatMini } from '../../utils/backendApi'
|
||||||
|
import { setGlobalMockDebugBridgeEnabled } from '../../utils/globalMockDebugBridge'
|
||||||
|
|
||||||
const DEFAULT_BACKEND_BASE_URL = 'https://api.gotomars.xyz'
|
const DEFAULT_BACKEND_BASE_URL = 'https://api.gotomars.xyz'
|
||||||
const DEFAULT_DEVICE_KEY = 'mini-dev-device-001'
|
const DEFAULT_DEVICE_KEY = 'mini-dev-device-001'
|
||||||
@@ -116,6 +117,7 @@ Page({
|
|||||||
|
|
||||||
handleClearLoginState() {
|
handleClearLoginState() {
|
||||||
clearBackendAuthTokens()
|
clearBackendAuthTokens()
|
||||||
|
setGlobalMockDebugBridgeEnabled(false)
|
||||||
const app = getApp<IAppOption>()
|
const app = getApp<IAppOption>()
|
||||||
if (app.globalData) {
|
if (app.globalData) {
|
||||||
app.globalData.backendAuthTokens = null
|
app.globalData.backendAuthTokens = null
|
||||||
|
|||||||
@@ -16,6 +16,13 @@ import {
|
|||||||
import { finishSession, startSession, type BackendSessionFinishSummaryPayload } from '../../utils/backendApi'
|
import { finishSession, startSession, type BackendSessionFinishSummaryPayload } from '../../utils/backendApi'
|
||||||
import { loadBackendBaseUrl } from '../../utils/backendAuth'
|
import { loadBackendBaseUrl } from '../../utils/backendAuth'
|
||||||
import { loadRemoteMapConfig, type RemoteMapConfig } from '../../utils/remoteMapConfig'
|
import { loadRemoteMapConfig, type RemoteMapConfig } from '../../utils/remoteMapConfig'
|
||||||
|
import {
|
||||||
|
persistStoredMockDebugLogBridgeUrl,
|
||||||
|
setGlobalMockDebugBridgeChannelId,
|
||||||
|
setGlobalMockDebugBridgeEnabled,
|
||||||
|
setGlobalMockDebugBridgeUrl,
|
||||||
|
} from '../../utils/globalMockDebugBridge'
|
||||||
|
import { reportBackendClientLog } from '../../utils/backendClientLogs'
|
||||||
import { type H5ExperienceFallbackPayload, type H5ExperienceRequest } from '../../game/experience/h5Experience'
|
import { type H5ExperienceFallbackPayload, type H5ExperienceRequest } from '../../game/experience/h5Experience'
|
||||||
import { type TrackColorPreset } from '../../game/presentation/trackStyleConfig'
|
import { type TrackColorPreset } from '../../game/presentation/trackStyleConfig'
|
||||||
import { type GpsMarkerColorPreset } from '../../game/presentation/gpsMarkerStyleConfig'
|
import { type GpsMarkerColorPreset } from '../../game/presentation/gpsMarkerStyleConfig'
|
||||||
@@ -146,6 +153,7 @@ type MapPageData = MapEngineViewState & {
|
|||||||
showLeftButtonGroup: boolean
|
showLeftButtonGroup: boolean
|
||||||
showRightButtonGroups: boolean
|
showRightButtonGroups: boolean
|
||||||
showBottomDebugButton: boolean
|
showBottomDebugButton: boolean
|
||||||
|
showStartEntryButton: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function getGlobalTelemetryProfile(): PlayerTelemetryProfile | null {
|
function getGlobalTelemetryProfile(): PlayerTelemetryProfile | null {
|
||||||
@@ -184,6 +192,7 @@ let systemSettingsLockLifetimeActive = false
|
|||||||
let syncedBackendSessionStartId = ''
|
let syncedBackendSessionStartId = ''
|
||||||
let syncedBackendSessionFinishId = ''
|
let syncedBackendSessionFinishId = ''
|
||||||
let shouldAutoRestoreRecoverySnapshot = false
|
let shouldAutoRestoreRecoverySnapshot = false
|
||||||
|
let shouldAutoStartSessionOnEnter = false
|
||||||
let redirectedToResultPage = false
|
let redirectedToResultPage = false
|
||||||
let pendingHeartRateSwitchDeviceName: string | null = null
|
let pendingHeartRateSwitchDeviceName: string | null = null
|
||||||
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
|
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
|
||||||
@@ -828,6 +837,52 @@ function buildRuntimeSummaryRows(envelope: GameLaunchEnvelope): MapEngineGameInf
|
|||||||
return rows
|
return rows
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildLaunchConfigSummaryRows(envelope: GameLaunchEnvelope): MapEngineGameInfoRow[] {
|
||||||
|
const rows: MapEngineGameInfoRow[] = []
|
||||||
|
rows.push({ label: '配置标签', value: envelope.config.configLabel || '--' })
|
||||||
|
rows.push({ label: '配置URL', value: envelope.config.configUrl || '--' })
|
||||||
|
rows.push({ label: '配置Release', value: envelope.config.releaseId || '--' })
|
||||||
|
rows.push({
|
||||||
|
label: 'Launch Event',
|
||||||
|
value: envelope.business && envelope.business.eventId
|
||||||
|
? envelope.business.eventId
|
||||||
|
: '--',
|
||||||
|
})
|
||||||
|
rows.push({
|
||||||
|
label: 'Resolved Manifest',
|
||||||
|
value: envelope.resolvedRelease && envelope.resolvedRelease.manifestUrl
|
||||||
|
? envelope.resolvedRelease.manifestUrl
|
||||||
|
: '--',
|
||||||
|
})
|
||||||
|
rows.push({
|
||||||
|
label: 'Resolved Release',
|
||||||
|
value: envelope.resolvedRelease && envelope.resolvedRelease.releaseId
|
||||||
|
? envelope.resolvedRelease.releaseId
|
||||||
|
: '--',
|
||||||
|
})
|
||||||
|
return rows
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitSimulatorLaunchDiagnostic(
|
||||||
|
stage: string,
|
||||||
|
payload: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'launch-diagnostic',
|
||||||
|
message: stage,
|
||||||
|
eventId: typeof payload.launchEventId === 'string' ? payload.launchEventId : '',
|
||||||
|
releaseId: typeof payload.configReleaseId === 'string'
|
||||||
|
? payload.configReleaseId
|
||||||
|
: (typeof payload.resolvedReleaseId === 'string' ? payload.resolvedReleaseId : ''),
|
||||||
|
sessionId: typeof payload.launchSessionId === 'string' ? payload.launchSessionId : '',
|
||||||
|
manifestUrl: typeof payload.resolvedManifestUrl === 'string'
|
||||||
|
? payload.resolvedManifestUrl
|
||||||
|
: (typeof payload.configUrl === 'string' ? payload.configUrl : ''),
|
||||||
|
details: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
Page({
|
Page({
|
||||||
data: {
|
data: {
|
||||||
showDebugPanel: false,
|
showDebugPanel: false,
|
||||||
@@ -967,6 +1022,7 @@ Page({
|
|||||||
centerScaleRulerMajorMarks: [],
|
centerScaleRulerMajorMarks: [],
|
||||||
compassTicks: buildCompassTicks(),
|
compassTicks: buildCompassTicks(),
|
||||||
compassLabels: buildCompassLabels(),
|
compassLabels: buildCompassLabels(),
|
||||||
|
showStartEntryButton: true,
|
||||||
...buildSideButtonVisibility('shown'),
|
...buildSideButtonVisibility('shown'),
|
||||||
...buildSideButtonState({
|
...buildSideButtonState({
|
||||||
sideButtonMode: 'shown',
|
sideButtonMode: 'shown',
|
||||||
@@ -989,10 +1045,15 @@ Page({
|
|||||||
syncedBackendSessionFinishId = ''
|
syncedBackendSessionFinishId = ''
|
||||||
redirectedToResultPage = false
|
redirectedToResultPage = false
|
||||||
shouldAutoRestoreRecoverySnapshot = options && options.recoverSession === '1'
|
shouldAutoRestoreRecoverySnapshot = options && options.recoverSession === '1'
|
||||||
currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
|
shouldAutoStartSessionOnEnter = !!(options && options.autoStartOnEnter === '1')
|
||||||
if (!hasExplicitLaunchOptions(options)) {
|
const recoverySnapshot = loadSessionRecoverySnapshot()
|
||||||
const recoverySnapshot = loadSessionRecoverySnapshot()
|
if (shouldAutoRestoreRecoverySnapshot && recoverySnapshot) {
|
||||||
if (recoverySnapshot) {
|
// Recovery should trust the persisted session envelope first so it can
|
||||||
|
// survive launchId stash misses and still reconstruct the original round.
|
||||||
|
currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
|
||||||
|
} else {
|
||||||
|
currentGameLaunchEnvelope = resolveGameLaunchEnvelope(options)
|
||||||
|
if (!hasExplicitLaunchOptions(options) && recoverySnapshot) {
|
||||||
currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
|
currentGameLaunchEnvelope = recoverySnapshot.launchEnvelope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1005,6 +1066,9 @@ Page({
|
|||||||
const statusBarHeight = systemInfo.statusBarHeight || 0
|
const statusBarHeight = systemInfo.statusBarHeight || 0
|
||||||
const menuButtonRect = wx.getMenuButtonBoundingClientRect()
|
const menuButtonRect = wx.getMenuButtonBoundingClientRect()
|
||||||
const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
|
const menuButtonBottom = menuButtonRect && typeof menuButtonRect.bottom === 'number' ? menuButtonRect.bottom : statusBarHeight
|
||||||
|
this.setData({
|
||||||
|
showStartEntryButton: !shouldAutoStartSessionOnEnter,
|
||||||
|
})
|
||||||
|
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
mapEngine.destroy()
|
mapEngine.destroy()
|
||||||
@@ -1514,11 +1578,27 @@ Page({
|
|||||||
systemSettingsLockLifetimeActive = false
|
systemSettingsLockLifetimeActive = false
|
||||||
currentGameLaunchEnvelope = getDemoGameLaunchEnvelope()
|
currentGameLaunchEnvelope = getDemoGameLaunchEnvelope()
|
||||||
shouldAutoRestoreRecoverySnapshot = false
|
shouldAutoRestoreRecoverySnapshot = false
|
||||||
|
shouldAutoStartSessionOnEnter = false
|
||||||
redirectedToResultPage = false
|
redirectedToResultPage = false
|
||||||
stageCanvasAttached = false
|
stageCanvasAttached = false
|
||||||
},
|
},
|
||||||
|
|
||||||
loadGameLaunchEnvelope(envelope: GameLaunchEnvelope) {
|
loadGameLaunchEnvelope(envelope: GameLaunchEnvelope) {
|
||||||
|
emitSimulatorLaunchDiagnostic('loadGameLaunchEnvelope', {
|
||||||
|
launchEventId: envelope.business && envelope.business.eventId ? envelope.business.eventId : '',
|
||||||
|
launchSessionId: envelope.business && envelope.business.sessionId ? envelope.business.sessionId : '',
|
||||||
|
configUrl: envelope.config.configUrl || '',
|
||||||
|
configReleaseId: envelope.config.releaseId || '',
|
||||||
|
resolvedManifestUrl: envelope.resolvedRelease && envelope.resolvedRelease.manifestUrl
|
||||||
|
? envelope.resolvedRelease.manifestUrl
|
||||||
|
: '',
|
||||||
|
resolvedReleaseId: envelope.resolvedRelease && envelope.resolvedRelease.releaseId
|
||||||
|
? envelope.resolvedRelease.releaseId
|
||||||
|
: '',
|
||||||
|
launchVariantId: envelope.variant && envelope.variant.variantId ? envelope.variant.variantId : null,
|
||||||
|
launchVariantRouteCode: envelope.variant && envelope.variant.routeCode ? envelope.variant.routeCode : null,
|
||||||
|
runtimeCourseVariantId: envelope.runtime && envelope.runtime.courseVariantId ? envelope.runtime.courseVariantId : null,
|
||||||
|
})
|
||||||
this.loadMapConfigFromRemote(
|
this.loadMapConfigFromRemote(
|
||||||
envelope.config.configUrl,
|
envelope.config.configUrl,
|
||||||
envelope.config.configLabel,
|
envelope.config.configLabel,
|
||||||
@@ -1621,10 +1701,49 @@ Page({
|
|||||||
reportAbandonedRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
|
reportAbandonedRecoverySnapshot(snapshot: SessionRecoverySnapshot) {
|
||||||
const sessionContext = getBackendSessionContextFromLaunchEnvelope(snapshot.launchEnvelope)
|
const sessionContext = getBackendSessionContextFromLaunchEnvelope(snapshot.launchEnvelope)
|
||||||
if (!sessionContext) {
|
if (!sessionContext) {
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'warn',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'abandon recovery without valid session context',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'abandon-no-session',
|
||||||
|
},
|
||||||
|
})
|
||||||
clearSessionRecoverySnapshot()
|
clearSessionRecoverySnapshot()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'abandon recovery requested',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: sessionContext.sessionId,
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'abandon-requested',
|
||||||
|
},
|
||||||
|
})
|
||||||
finishSession({
|
finishSession({
|
||||||
baseUrl: getCurrentBackendBaseUrl(),
|
baseUrl: getCurrentBackendBaseUrl(),
|
||||||
sessionId: sessionContext.sessionId,
|
sessionId: sessionContext.sessionId,
|
||||||
@@ -1634,6 +1753,26 @@ Page({
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
syncedBackendSessionFinishId = sessionContext.sessionId
|
syncedBackendSessionFinishId = sessionContext.sessionId
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'abandon recovery synced as cancelled',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: sessionContext.sessionId,
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'abandon-finished',
|
||||||
|
},
|
||||||
|
})
|
||||||
clearSessionRecoverySnapshot()
|
clearSessionRecoverySnapshot()
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: '已放弃上次对局',
|
title: '已放弃上次对局',
|
||||||
@@ -1642,6 +1781,27 @@ Page({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'warn',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'abandon recovery finish(cancelled) failed',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: sessionContext.sessionId,
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'abandon-failed',
|
||||||
|
message: error && error.message ? error.message : '未知错误',
|
||||||
|
},
|
||||||
|
})
|
||||||
clearSessionRecoverySnapshot()
|
clearSessionRecoverySnapshot()
|
||||||
const message = error && error.message ? error.message : '未知错误'
|
const message = error && error.message ? error.message : '未知错误'
|
||||||
this.setData({
|
this.setData({
|
||||||
@@ -1712,6 +1872,28 @@ Page({
|
|||||||
this.applyRuntimeSystemSettings(true)
|
this.applyRuntimeSystemSettings(true)
|
||||||
const restored = mapEngine ? mapEngine.restoreSessionRecoveryRuntimeSnapshot(snapshot.runtime) : false
|
const restored = mapEngine ? mapEngine.restoreSessionRecoveryRuntimeSnapshot(snapshot.runtime) : false
|
||||||
if (!restored) {
|
if (!restored) {
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'warn',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'recovery restore failed',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
|
||||||
|
? snapshot.launchEnvelope.business.sessionId
|
||||||
|
: '',
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'restore-failed',
|
||||||
|
},
|
||||||
|
})
|
||||||
clearSessionRecoverySnapshot()
|
clearSessionRecoverySnapshot()
|
||||||
wx.showToast({
|
wx.showToast({
|
||||||
title: '恢复失败,已回到初始状态',
|
title: '恢复失败,已回到初始状态',
|
||||||
@@ -1726,11 +1908,34 @@ Page({
|
|||||||
showDebugPanel: false,
|
showDebugPanel: false,
|
||||||
showGameInfoPanel: false,
|
showGameInfoPanel: false,
|
||||||
showSystemSettingsPanel: false,
|
showSystemSettingsPanel: false,
|
||||||
|
showStartEntryButton: false,
|
||||||
})
|
})
|
||||||
const sessionContext = getCurrentBackendSessionContext()
|
const sessionContext = getCurrentBackendSessionContext()
|
||||||
if (sessionContext) {
|
if (sessionContext) {
|
||||||
syncedBackendSessionStartId = sessionContext.sessionId
|
syncedBackendSessionStartId = sessionContext.sessionId
|
||||||
}
|
}
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'recovery restored',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
|
||||||
|
? snapshot.launchEnvelope.business.sessionId
|
||||||
|
: '',
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'restored',
|
||||||
|
},
|
||||||
|
})
|
||||||
this.syncSessionRecoveryLifecycle('running')
|
this.syncSessionRecoveryLifecycle('running')
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
@@ -1752,24 +1957,77 @@ Page({
|
|||||||
maybePromptSessionRecoveryRestore(config: RemoteMapConfig) {
|
maybePromptSessionRecoveryRestore(config: RemoteMapConfig) {
|
||||||
const snapshot = loadSessionRecoverySnapshot()
|
const snapshot = loadSessionRecoverySnapshot()
|
||||||
if (!snapshot || !mapEngine) {
|
if (!snapshot || !mapEngine) {
|
||||||
return
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
snapshot.launchEnvelope.config.configUrl !== currentGameLaunchEnvelope.config.configUrl
|
snapshot.launchEnvelope.config.configUrl !== currentGameLaunchEnvelope.config.configUrl
|
||||||
|| snapshot.configAppId !== config.configAppId
|
|| snapshot.configAppId !== config.configAppId
|
||||||
|| snapshot.configVersion !== config.configVersion
|
|
||||||
) {
|
) {
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'warn',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'recovery snapshot dropped due to config mismatch',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
|
||||||
|
? snapshot.launchEnvelope.business.sessionId
|
||||||
|
: '',
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'config-mismatch',
|
||||||
|
currentConfigUrl: currentGameLaunchEnvelope.config.configUrl,
|
||||||
|
snapshotConfigUrl: snapshot.launchEnvelope.config.configUrl,
|
||||||
|
currentConfigAppId: config.configAppId,
|
||||||
|
snapshotConfigAppId: snapshot.configAppId,
|
||||||
|
},
|
||||||
|
})
|
||||||
clearSessionRecoverySnapshot()
|
clearSessionRecoverySnapshot()
|
||||||
return
|
this.setData({
|
||||||
|
statusText: '检测到旧局恢复记录,但当前配置源已变化,已回到初始状态',
|
||||||
|
})
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldAutoRestoreRecoverySnapshot) {
|
if (shouldAutoRestoreRecoverySnapshot) {
|
||||||
shouldAutoRestoreRecoverySnapshot = false
|
shouldAutoRestoreRecoverySnapshot = false
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'session-recovery',
|
||||||
|
message: 'auto recovery requested',
|
||||||
|
eventId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.eventId
|
||||||
|
? snapshot.launchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.releaseId
|
||||||
|
? snapshot.launchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: snapshot.launchEnvelope.business && snapshot.launchEnvelope.business.sessionId
|
||||||
|
? snapshot.launchEnvelope.business.sessionId
|
||||||
|
: '',
|
||||||
|
manifestUrl: snapshot.launchEnvelope.resolvedRelease && snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? snapshot.launchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: snapshot.launchEnvelope.config && snapshot.launchEnvelope.config.configUrl
|
||||||
|
? snapshot.launchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'auto-restore',
|
||||||
|
},
|
||||||
|
})
|
||||||
this.restoreRecoverySnapshot(snapshot)
|
this.restoreRecoverySnapshot(snapshot)
|
||||||
return
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.setData({
|
||||||
|
showStartEntryButton: true,
|
||||||
|
})
|
||||||
wx.showModal({
|
wx.showModal({
|
||||||
title: '恢复对局',
|
title: '恢复对局',
|
||||||
content: '检测到上次有未正常结束的对局,是否继续恢复?',
|
content: '检测到上次有未正常结束的对局,是否继续恢复?',
|
||||||
@@ -1784,6 +2042,21 @@ Page({
|
|||||||
this.restoreRecoverySnapshot(snapshot)
|
this.restoreRecoverySnapshot(snapshot)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
|
||||||
|
maybeAutoStartSessionOnEnter() {
|
||||||
|
if (!shouldAutoStartSessionOnEnter || !mapEngine) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAutoStartSessionOnEnter = false
|
||||||
|
systemSettingsLockLifetimeActive = true
|
||||||
|
this.applyRuntimeSystemSettings(true)
|
||||||
|
this.setData({
|
||||||
|
showStartEntryButton: false,
|
||||||
|
})
|
||||||
|
mapEngine.handleStartGame()
|
||||||
},
|
},
|
||||||
|
|
||||||
compileCurrentRuntimeProfile(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
|
compileCurrentRuntimeProfile(lockLifetimeActive = isSystemSettingsLockLifetimeActive()) {
|
||||||
@@ -1913,20 +2186,76 @@ Page({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitSimulatorLaunchDiagnostic('loadRemoteMapConfig:resolved', {
|
||||||
|
launchEventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
|
||||||
|
? currentGameLaunchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
configUrl,
|
||||||
|
configVersion: config.configVersion || '',
|
||||||
|
schemaVersion: config.configSchemaVersion || '',
|
||||||
|
playfieldKind: config.playfieldKind || '',
|
||||||
|
gameMode: config.gameMode || '',
|
||||||
|
configTitle: config.configTitle || '',
|
||||||
|
})
|
||||||
|
|
||||||
currentEngine.applyRemoteMapConfig(config)
|
currentEngine.applyRemoteMapConfig(config)
|
||||||
this.applyConfiguredSystemSettings(config)
|
this.applyConfiguredSystemSettings(config)
|
||||||
this.applyCompiledRuntimeProfiles(true, {
|
const compiledProfile = this.applyCompiledRuntimeProfiles(true, {
|
||||||
includeMap: true,
|
includeMap: true,
|
||||||
includeGame: true,
|
includeGame: true,
|
||||||
includePresentation: true,
|
includePresentation: true,
|
||||||
})
|
})
|
||||||
this.maybePromptSessionRecoveryRestore(config)
|
if (compiledProfile) {
|
||||||
|
reportBackendClientLog({
|
||||||
|
level: 'info',
|
||||||
|
category: 'runtime-compiler',
|
||||||
|
message: 'compiled runtime profile applied',
|
||||||
|
eventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
|
||||||
|
? currentGameLaunchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
releaseId: currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.releaseId
|
||||||
|
? currentGameLaunchEnvelope.config.releaseId
|
||||||
|
: '',
|
||||||
|
sessionId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.sessionId
|
||||||
|
? currentGameLaunchEnvelope.business.sessionId
|
||||||
|
: '',
|
||||||
|
manifestUrl: currentGameLaunchEnvelope.resolvedRelease && currentGameLaunchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
? currentGameLaunchEnvelope.resolvedRelease.manifestUrl
|
||||||
|
: currentGameLaunchEnvelope.config && currentGameLaunchEnvelope.config.configUrl
|
||||||
|
? currentGameLaunchEnvelope.config.configUrl
|
||||||
|
: '',
|
||||||
|
details: {
|
||||||
|
phase: 'compiled-runtime-applied',
|
||||||
|
schemaVersion: config.configSchemaVersion || '',
|
||||||
|
playfield: {
|
||||||
|
kind: config.playfieldKind || '',
|
||||||
|
},
|
||||||
|
game: {
|
||||||
|
mode: config.gameMode || '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const recoveryHandled = this.maybePromptSessionRecoveryRestore(config)
|
||||||
|
if (!recoveryHandled) {
|
||||||
|
this.maybeAutoStartSessionOnEnter()
|
||||||
|
} else {
|
||||||
|
shouldAutoStartSessionOnEnter = false
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
if (mapEngine !== currentEngine) {
|
if (mapEngine !== currentEngine) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
emitSimulatorLaunchDiagnostic('loadRemoteMapConfig:error', {
|
||||||
|
launchEventId: currentGameLaunchEnvelope.business && currentGameLaunchEnvelope.business.eventId
|
||||||
|
? currentGameLaunchEnvelope.business.eventId
|
||||||
|
: '',
|
||||||
|
configUrl,
|
||||||
|
message: error && error.message ? error.message : '未知错误',
|
||||||
|
})
|
||||||
|
|
||||||
const rawErrorMessage = error && error.message ? error.message : '未知错误'
|
const rawErrorMessage = error && error.message ? error.message : '未知错误'
|
||||||
const errorMessage = rawErrorMessage.indexOf('404') >= 0
|
const errorMessage = rawErrorMessage.indexOf('404') >= 0
|
||||||
? `release manifest 不存在或未发布 (${configLabel})`
|
? `release manifest 不存在或未发布 (${configLabel})`
|
||||||
@@ -2115,6 +2444,10 @@ Page({
|
|||||||
})
|
})
|
||||||
persistMockChannelId(channelId)
|
persistMockChannelId(channelId)
|
||||||
persistMockAutoConnectEnabled(true)
|
persistMockAutoConnectEnabled(true)
|
||||||
|
setGlobalMockDebugBridgeChannelId(channelId)
|
||||||
|
setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
|
||||||
|
persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
|
||||||
|
setGlobalMockDebugBridgeEnabled(true)
|
||||||
mapEngine.handleSetMockChannelId(channelId)
|
mapEngine.handleSetMockChannelId(channelId)
|
||||||
mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
|
mapEngine.handleSetMockLocationBridgeUrl(this.data.mockBridgeUrlDraft)
|
||||||
mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
|
mapEngine.handleSetMockHeartRateBridgeUrl(this.data.mockHeartRateBridgeUrlDraft)
|
||||||
@@ -2144,6 +2477,7 @@ Page({
|
|||||||
mockChannelIdDraft: channelId,
|
mockChannelIdDraft: channelId,
|
||||||
})
|
})
|
||||||
persistMockChannelId(channelId)
|
persistMockChannelId(channelId)
|
||||||
|
setGlobalMockDebugBridgeChannelId(channelId)
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
mapEngine.handleSetMockChannelId(channelId)
|
mapEngine.handleSetMockChannelId(channelId)
|
||||||
}
|
}
|
||||||
@@ -2199,12 +2533,17 @@ Page({
|
|||||||
},
|
},
|
||||||
|
|
||||||
handleSaveMockDebugLogBridgeUrl() {
|
handleSaveMockDebugLogBridgeUrl() {
|
||||||
|
persistStoredMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
|
||||||
|
setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
|
mapEngine.handleSetMockDebugLogBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleConnectMockDebugLogBridge() {
|
handleConnectMockDebugLogBridge() {
|
||||||
|
setGlobalMockDebugBridgeChannelId((this.data.mockChannelIdDraft || '').trim() || 'default')
|
||||||
|
setGlobalMockDebugBridgeUrl(this.data.mockDebugLogBridgeUrlDraft)
|
||||||
|
setGlobalMockDebugBridgeEnabled(true)
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
mapEngine.handleConnectMockDebugLogBridge()
|
mapEngine.handleConnectMockDebugLogBridge()
|
||||||
}
|
}
|
||||||
@@ -2212,6 +2551,7 @@ Page({
|
|||||||
|
|
||||||
handleDisconnectMockDebugLogBridge() {
|
handleDisconnectMockDebugLogBridge() {
|
||||||
persistMockAutoConnectEnabled(false)
|
persistMockAutoConnectEnabled(false)
|
||||||
|
setGlobalMockDebugBridgeEnabled(false)
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
mapEngine.handleDisconnectMockDebugLogBridge()
|
mapEngine.handleDisconnectMockDebugLogBridge()
|
||||||
}
|
}
|
||||||
@@ -2358,8 +2698,12 @@ Page({
|
|||||||
|
|
||||||
handleStartGame() {
|
handleStartGame() {
|
||||||
if (mapEngine) {
|
if (mapEngine) {
|
||||||
|
shouldAutoStartSessionOnEnter = false
|
||||||
systemSettingsLockLifetimeActive = true
|
systemSettingsLockLifetimeActive = true
|
||||||
this.applyRuntimeSystemSettings(true)
|
this.applyRuntimeSystemSettings(true)
|
||||||
|
this.setData({
|
||||||
|
showStartEntryButton: false,
|
||||||
|
})
|
||||||
mapEngine.handleStartGame()
|
mapEngine.handleStartGame()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2443,6 +2787,7 @@ Page({
|
|||||||
const snapshot = mapEngine.getGameInfoSnapshot()
|
const snapshot = mapEngine.getGameInfoSnapshot()
|
||||||
const localRows = snapshot.localRows.concat([
|
const localRows = snapshot.localRows.concat([
|
||||||
...buildRuntimeSummaryRows(currentGameLaunchEnvelope),
|
...buildRuntimeSummaryRows(currentGameLaunchEnvelope),
|
||||||
|
...buildLaunchConfigSummaryRows(currentGameLaunchEnvelope),
|
||||||
{ label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
|
{ label: '比例尺开关', value: this.data.showCenterScaleRuler ? '开启' : '关闭' },
|
||||||
{ label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
|
{ label: '比例尺锚点', value: this.data.centerScaleRulerAnchorMode === 'compass-center' ? '指北针圆心' : '屏幕中心' },
|
||||||
{ label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
|
{ label: '按钮习惯', value: this.data.sideButtonPlacement === 'right' ? '右手' : '左手' },
|
||||||
@@ -2471,7 +2816,9 @@ Page({
|
|||||||
resultSceneSubtitle: snapshot.subtitle,
|
resultSceneSubtitle: snapshot.subtitle,
|
||||||
resultSceneHeroLabel: snapshot.heroLabel,
|
resultSceneHeroLabel: snapshot.heroLabel,
|
||||||
resultSceneHeroValue: snapshot.heroValue,
|
resultSceneHeroValue: snapshot.heroValue,
|
||||||
resultSceneRows: snapshot.rows.concat(buildRuntimeSummaryRows(currentGameLaunchEnvelope)),
|
resultSceneRows: snapshot.rows
|
||||||
|
.concat(buildRuntimeSummaryRows(currentGameLaunchEnvelope))
|
||||||
|
.concat(buildLaunchConfigSummaryRows(currentGameLaunchEnvelope)),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@
|
|||||||
<cover-view class="map-content-entry__text">{{pendingContentEntryText}}</cover-view>
|
<cover-view class="map-content-entry__text">{{pendingContentEntryText}}</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
<cover-view class="screen-button-layer screen-button-layer--start-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showResultScene && !showSystemSettingsPanel && showBottomDebugButton && gameSessionStatus !== 'running'}}" bindtap="handleStartGame">
|
<cover-view class="screen-button-layer screen-button-layer--start-left" wx:if="{{!showDebugPanel && !showGameInfoPanel && !showResultScene && !showSystemSettingsPanel && showBottomDebugButton && showStartEntryButton && gameSessionStatus !== 'running'}}" bindtap="handleStartGame">
|
||||||
<cover-view class="screen-button-layer__text screen-button-layer__text--start">开始</cover-view>
|
<cover-view class="screen-button-layer__text screen-button-layer__text--start">开始</cover-view>
|
||||||
</cover-view>
|
</cover-view>
|
||||||
|
|
||||||
|
|||||||
@@ -237,6 +237,20 @@ export interface BackendSessionResultView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface BackendClientLogInput {
|
||||||
|
source: string
|
||||||
|
level: 'debug' | 'info' | 'warn' | 'error'
|
||||||
|
category: string
|
||||||
|
message: string
|
||||||
|
eventId?: string
|
||||||
|
releaseId?: string
|
||||||
|
sessionId?: string
|
||||||
|
manifestUrl?: string
|
||||||
|
route?: string
|
||||||
|
occurredAt?: string
|
||||||
|
details?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
type BackendEnvelope<T> = {
|
type BackendEnvelope<T> = {
|
||||||
data: T
|
data: T
|
||||||
}
|
}
|
||||||
@@ -428,3 +442,15 @@ export function getMyResults(input: {
|
|||||||
authToken: input.accessToken,
|
authToken: input.accessToken,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function postClientLog(input: {
|
||||||
|
baseUrl: string
|
||||||
|
payload: BackendClientLogInput
|
||||||
|
}): Promise<void> {
|
||||||
|
return requestBackend<void>({
|
||||||
|
method: 'POST',
|
||||||
|
baseUrl: input.baseUrl,
|
||||||
|
path: '/dev/client-logs',
|
||||||
|
body: input.payload as unknown as Record<string, unknown>,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
90
miniprogram/utils/backendClientLogs.ts
Normal file
90
miniprogram/utils/backendClientLogs.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import { loadBackendBaseUrl } from './backendAuth'
|
||||||
|
import { postClientLog, type BackendClientLogInput } from './backendApi'
|
||||||
|
|
||||||
|
type ClientLogLevel = BackendClientLogInput['level']
|
||||||
|
|
||||||
|
type ClientLogEntry = {
|
||||||
|
level: ClientLogLevel
|
||||||
|
category: string
|
||||||
|
message: string
|
||||||
|
eventId?: string
|
||||||
|
releaseId?: string
|
||||||
|
sessionId?: string
|
||||||
|
manifestUrl?: string
|
||||||
|
route?: string
|
||||||
|
details?: Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
const CLIENT_LOG_SOURCE = 'wechat-mini'
|
||||||
|
const MAX_PENDING_CLIENT_LOGS = 100
|
||||||
|
|
||||||
|
const pendingClientLogs: BackendClientLogInput[] = []
|
||||||
|
let clientLogFlushInProgress = false
|
||||||
|
let clientLogSequence = 0
|
||||||
|
|
||||||
|
function getCurrentRoute(): string {
|
||||||
|
const pages = getCurrentPages()
|
||||||
|
if (!pages.length) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
const current = pages[pages.length - 1]
|
||||||
|
return current && current.route ? current.route : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueClientLog(payload: BackendClientLogInput) {
|
||||||
|
pendingClientLogs.push(payload)
|
||||||
|
if (pendingClientLogs.length > MAX_PENDING_CLIENT_LOGS) {
|
||||||
|
pendingClientLogs.shift()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushNextClientLog() {
|
||||||
|
if (clientLogFlushInProgress || !pendingClientLogs.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseUrl = loadBackendBaseUrl()
|
||||||
|
if (!baseUrl) {
|
||||||
|
pendingClientLogs.length = 0
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = pendingClientLogs.shift()
|
||||||
|
if (!payload) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientLogFlushInProgress = true
|
||||||
|
postClientLog({
|
||||||
|
baseUrl,
|
||||||
|
payload,
|
||||||
|
}).catch(() => {
|
||||||
|
// 联调日志不打断主流程,失败时静默丢弃。
|
||||||
|
}).finally(() => {
|
||||||
|
clientLogFlushInProgress = false
|
||||||
|
if (pendingClientLogs.length) {
|
||||||
|
flushNextClientLog()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function reportBackendClientLog(entry: ClientLogEntry) {
|
||||||
|
clientLogSequence += 1
|
||||||
|
const details = entry.details ? { ...entry.details } : {}
|
||||||
|
details.seq = clientLogSequence
|
||||||
|
const payload: BackendClientLogInput = {
|
||||||
|
source: CLIENT_LOG_SOURCE,
|
||||||
|
level: entry.level,
|
||||||
|
category: entry.category,
|
||||||
|
message: entry.message,
|
||||||
|
eventId: entry.eventId || '',
|
||||||
|
releaseId: entry.releaseId || '',
|
||||||
|
sessionId: entry.sessionId || '',
|
||||||
|
manifestUrl: entry.manifestUrl || '',
|
||||||
|
route: entry.route || getCurrentRoute(),
|
||||||
|
occurredAt: new Date().toISOString(),
|
||||||
|
details,
|
||||||
|
}
|
||||||
|
enqueueClientLog(payload)
|
||||||
|
flushNextClientLog()
|
||||||
|
}
|
||||||
@@ -21,6 +21,18 @@ export function adaptBackendLaunchResultToEnvelope(result: BackendLaunchResult):
|
|||||||
sessionToken: result.launch.business.sessionToken,
|
sessionToken: result.launch.business.sessionToken,
|
||||||
sessionTokenExpiresAt: result.launch.business.sessionTokenExpiresAt,
|
sessionTokenExpiresAt: result.launch.business.sessionTokenExpiresAt,
|
||||||
},
|
},
|
||||||
|
resolvedRelease: result.launch.resolvedRelease
|
||||||
|
? {
|
||||||
|
launchMode: result.launch.resolvedRelease.launchMode || null,
|
||||||
|
source: result.launch.resolvedRelease.source || null,
|
||||||
|
eventId: result.launch.resolvedRelease.eventId || null,
|
||||||
|
releaseId: result.launch.resolvedRelease.releaseId || null,
|
||||||
|
configLabel: result.launch.resolvedRelease.configLabel || null,
|
||||||
|
manifestUrl: result.launch.resolvedRelease.manifestUrl || null,
|
||||||
|
manifestChecksumSha256: result.launch.resolvedRelease.manifestChecksumSha256 || null,
|
||||||
|
routeCode: result.launch.resolvedRelease.routeCode || null,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
variant: result.launch.variant
|
variant: result.launch.variant
|
||||||
? {
|
? {
|
||||||
variantId: result.launch.variant.id,
|
variantId: result.launch.variant.id,
|
||||||
|
|||||||
@@ -9,6 +9,17 @@ export interface GameConfigLaunchRequest {
|
|||||||
routeCode?: string | null
|
routeCode?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GameResolvedReleaseLaunchContext {
|
||||||
|
launchMode?: string | null
|
||||||
|
source?: string | null
|
||||||
|
eventId?: string | null
|
||||||
|
releaseId?: string | null
|
||||||
|
configLabel?: string | null
|
||||||
|
manifestUrl?: string | null
|
||||||
|
manifestChecksumSha256?: string | null
|
||||||
|
routeCode?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
export interface BusinessLaunchContext {
|
export interface BusinessLaunchContext {
|
||||||
source: BusinessLaunchSource
|
source: BusinessLaunchSource
|
||||||
competitionId?: string | null
|
competitionId?: string | null
|
||||||
@@ -56,6 +67,7 @@ export interface GameContentBundleLaunchContext {
|
|||||||
export interface GameLaunchEnvelope {
|
export interface GameLaunchEnvelope {
|
||||||
config: GameConfigLaunchRequest
|
config: GameConfigLaunchRequest
|
||||||
business: BusinessLaunchContext | null
|
business: BusinessLaunchContext | null
|
||||||
|
resolvedRelease?: GameResolvedReleaseLaunchContext | null
|
||||||
variant?: GameVariantLaunchContext | null
|
variant?: GameVariantLaunchContext | null
|
||||||
runtime?: GameRuntimeLaunchContext | null
|
runtime?: GameRuntimeLaunchContext | null
|
||||||
presentation?: GamePresentationLaunchContext | null
|
presentation?: GamePresentationLaunchContext | null
|
||||||
@@ -65,6 +77,7 @@ export interface GameLaunchEnvelope {
|
|||||||
export interface MapPageLaunchOptions {
|
export interface MapPageLaunchOptions {
|
||||||
launchId?: string
|
launchId?: string
|
||||||
recoverSession?: string
|
recoverSession?: string
|
||||||
|
autoStartOnEnter?: string
|
||||||
preset?: string
|
preset?: string
|
||||||
configUrl?: string
|
configUrl?: string
|
||||||
configLabel?: string
|
configLabel?: string
|
||||||
@@ -292,6 +305,7 @@ export function getDemoGameLaunchEnvelope(preset: DemoGamePreset = 'classic'): G
|
|||||||
business: {
|
business: {
|
||||||
source: 'demo',
|
source: 'demo',
|
||||||
},
|
},
|
||||||
|
resolvedRelease: null,
|
||||||
variant: null,
|
variant: null,
|
||||||
runtime: null,
|
runtime: null,
|
||||||
presentation: null,
|
presentation: null,
|
||||||
@@ -324,12 +338,24 @@ export function consumePendingGameLaunchEnvelope(launchId: string): GameLaunchEn
|
|||||||
return envelope
|
return envelope
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildMapPageUrlWithLaunchId(launchId: string): string {
|
export function buildMapPageUrlWithLaunchId(launchId: string, extraQuery?: Record<string, string>): string {
|
||||||
return `/pages/map/map?launchId=${encodeURIComponent(launchId)}`
|
const queryParts = [`launchId=${encodeURIComponent(launchId)}`]
|
||||||
|
if (extraQuery) {
|
||||||
|
Object.keys(extraQuery).forEach((key) => {
|
||||||
|
const value = extraQuery[key]
|
||||||
|
if (typeof value === 'string' && value) {
|
||||||
|
queryParts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return `/pages/map/map?${queryParts.join('&')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareMapPageUrlForLaunch(envelope: GameLaunchEnvelope): string {
|
export function prepareMapPageUrlForLaunch(envelope: GameLaunchEnvelope): string {
|
||||||
return buildMapPageUrlWithLaunchId(stashPendingGameLaunchEnvelope(envelope))
|
return buildMapPageUrlWithLaunchId(
|
||||||
|
stashPendingGameLaunchEnvelope(envelope),
|
||||||
|
{ autoStartOnEnter: '1' },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prepareMapPageUrlForRecovery(envelope: GameLaunchEnvelope): string {
|
export function prepareMapPageUrlForRecovery(envelope: GameLaunchEnvelope): string {
|
||||||
@@ -367,6 +393,7 @@ export function resolveGameLaunchEnvelope(options?: MapPageLaunchOptions | null)
|
|||||||
routeCode: normalizeOptionalString(options ? options.routeCode : undefined),
|
routeCode: normalizeOptionalString(options ? options.routeCode : undefined),
|
||||||
},
|
},
|
||||||
business: buildBusinessLaunchContext(options),
|
business: buildBusinessLaunchContext(options),
|
||||||
|
resolvedRelease: null,
|
||||||
variant: buildVariantLaunchContext(options),
|
variant: buildVariantLaunchContext(options),
|
||||||
runtime: buildRuntimeLaunchContext(options),
|
runtime: buildRuntimeLaunchContext(options),
|
||||||
presentation: buildPresentationLaunchContext(options),
|
presentation: buildPresentationLaunchContext(options),
|
||||||
|
|||||||
88
miniprogram/utils/globalMockDebugBridge.ts
Normal file
88
miniprogram/utils/globalMockDebugBridge.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import { MockSimulatorDebugLogger, type MockSimulatorDebugLogLevel } from '../engine/debug/mockSimulatorDebugLogger'
|
||||||
|
|
||||||
|
const DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY = 'cmr.debug.mockChannelId.v1'
|
||||||
|
const DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY = 'cmr.debug.autoConnectMockSources.v1'
|
||||||
|
const DEBUG_MOCK_LOG_URL_STORAGE_KEY = 'cmr.debug.logBridgeUrl.v1'
|
||||||
|
const DEFAULT_DEBUG_LOG_URL = 'wss://gs.gotomars.xyz/debug-log'
|
||||||
|
|
||||||
|
let globalMockDebugLogger: MockSimulatorDebugLogger | null = null
|
||||||
|
|
||||||
|
function ensureLogger(): MockSimulatorDebugLogger {
|
||||||
|
if (!globalMockDebugLogger) {
|
||||||
|
globalMockDebugLogger = new MockSimulatorDebugLogger()
|
||||||
|
}
|
||||||
|
return globalMockDebugLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadStoredMockChannelIdForGlobalDebug(): string {
|
||||||
|
try {
|
||||||
|
const value = wx.getStorageSync(DEBUG_MOCK_CHANNEL_ID_STORAGE_KEY)
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
return value.trim()
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore storage read failures and fall back to default.
|
||||||
|
}
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadMockAutoConnectEnabledForGlobalDebug(): boolean {
|
||||||
|
try {
|
||||||
|
return wx.getStorageSync(DEBUG_MOCK_AUTO_CONNECT_STORAGE_KEY) === true
|
||||||
|
} catch (_error) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadStoredMockDebugLogBridgeUrl(): string {
|
||||||
|
try {
|
||||||
|
const value = wx.getStorageSync(DEBUG_MOCK_LOG_URL_STORAGE_KEY)
|
||||||
|
if (typeof value === 'string' && value.trim().length > 0) {
|
||||||
|
return value.trim()
|
||||||
|
}
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore storage read failures and fall back to default.
|
||||||
|
}
|
||||||
|
return DEFAULT_DEBUG_LOG_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export function persistStoredMockDebugLogBridgeUrl(url: string) {
|
||||||
|
try {
|
||||||
|
wx.setStorageSync(DEBUG_MOCK_LOG_URL_STORAGE_KEY, url)
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore storage write failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function syncGlobalMockDebugBridgeFromStorage(): void {
|
||||||
|
const logger = ensureLogger()
|
||||||
|
logger.setChannelId(loadStoredMockChannelIdForGlobalDebug())
|
||||||
|
logger.setUrl(loadStoredMockDebugLogBridgeUrl())
|
||||||
|
logger.setEnabled(loadMockAutoConnectEnabledForGlobalDebug())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGlobalMockDebugBridgeChannelId(channelId: string): void {
|
||||||
|
const logger = ensureLogger()
|
||||||
|
logger.setChannelId(channelId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGlobalMockDebugBridgeEnabled(enabled: boolean): void {
|
||||||
|
const logger = ensureLogger()
|
||||||
|
logger.setEnabled(enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setGlobalMockDebugBridgeUrl(url: string): void {
|
||||||
|
const logger = ensureLogger()
|
||||||
|
logger.setUrl(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitGlobalMockDebugLog(
|
||||||
|
scope: string,
|
||||||
|
level: MockSimulatorDebugLogLevel,
|
||||||
|
message: string,
|
||||||
|
payload?: Record<string, unknown>,
|
||||||
|
): void {
|
||||||
|
const logger = ensureLogger()
|
||||||
|
logger.log(scope, level, message, payload)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -68,6 +68,7 @@ export interface RemoteMapConfig {
|
|||||||
configAppId: string
|
configAppId: string
|
||||||
configSchemaVersion: string
|
configSchemaVersion: string
|
||||||
configVersion: string
|
configVersion: string
|
||||||
|
playfieldKind: string
|
||||||
tileSource: string
|
tileSource: string
|
||||||
minZoom: number
|
minZoom: number
|
||||||
maxZoom: number
|
maxZoom: number
|
||||||
@@ -122,6 +123,7 @@ interface ParsedGameConfig {
|
|||||||
appId: string
|
appId: string
|
||||||
schemaVersion: string
|
schemaVersion: string
|
||||||
version: string
|
version: string
|
||||||
|
playfieldKind: string
|
||||||
mapRoot: string
|
mapRoot: string
|
||||||
mapMeta: string
|
mapMeta: string
|
||||||
course: string | null
|
course: string | null
|
||||||
@@ -1754,6 +1756,7 @@ function parseGameConfigFromJson(text: string, gameConfigUrl: string): ParsedGam
|
|||||||
appId: rawApp && typeof rawApp.id === 'string' ? rawApp.id : '',
|
appId: rawApp && typeof rawApp.id === 'string' ? rawApp.id : '',
|
||||||
schemaVersion: typeof parsed.schemaVersion === 'string' ? parsed.schemaVersion : '1',
|
schemaVersion: typeof parsed.schemaVersion === 'string' ? parsed.schemaVersion : '1',
|
||||||
version: typeof parsed.version === 'string' ? parsed.version : '',
|
version: typeof parsed.version === 'string' ? parsed.version : '',
|
||||||
|
playfieldKind: rawPlayfield && typeof rawPlayfield.kind === 'string' ? rawPlayfield.kind : '',
|
||||||
mapRoot,
|
mapRoot,
|
||||||
mapMeta,
|
mapMeta,
|
||||||
course: rawPlayfieldSource && typeof rawPlayfieldSource.url === 'string'
|
course: rawPlayfieldSource && typeof rawPlayfieldSource.url === 'string'
|
||||||
@@ -1855,6 +1858,7 @@ function parseGameConfigFromYaml(text: string, gameConfigUrl: string): ParsedGam
|
|||||||
appId: '',
|
appId: '',
|
||||||
schemaVersion: '1',
|
schemaVersion: '1',
|
||||||
version: '',
|
version: '',
|
||||||
|
playfieldKind: typeof config.playfieldkind === 'string' ? config.playfieldkind : '',
|
||||||
mapRoot,
|
mapRoot,
|
||||||
mapMeta,
|
mapMeta,
|
||||||
course: typeof config.course === 'string' ? config.course : null,
|
course: typeof config.course === 'string' ? config.course : null,
|
||||||
@@ -2157,6 +2161,7 @@ export async function loadRemoteMapConfig(gameConfigUrl: string): Promise<Remote
|
|||||||
configAppId: gameConfig.appId || '',
|
configAppId: gameConfig.appId || '',
|
||||||
configSchemaVersion: gameConfig.schemaVersion || '1',
|
configSchemaVersion: gameConfig.schemaVersion || '1',
|
||||||
configVersion: gameConfig.version || '',
|
configVersion: gameConfig.version || '',
|
||||||
|
playfieldKind: gameConfig.playfieldKind || '',
|
||||||
tileSource: resolveUrl(mapRootUrl, mapMeta.tilePathTemplate),
|
tileSource: resolveUrl(mapRootUrl, mapMeta.tilePathTemplate),
|
||||||
minZoom: mapMeta.minZoom,
|
minZoom: mapMeta.minZoom,
|
||||||
maxZoom: mapMeta.maxZoom,
|
maxZoom: mapMeta.maxZoom,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CMR Mini 开发架构阶段总结
|
# CMR Mini 开发架构阶段总结
|
||||||
> 文档版本:v1.14
|
> 文档版本:v1.16
|
||||||
> 最后更新:2026-04-03 14:16:17
|
> 最后更新:2026-04-03 16:59:19
|
||||||
|
|
||||||
文档维护约定:
|
文档维护约定:
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
当前补充约定:
|
当前补充约定:
|
||||||
|
|
||||||
- 多线程联调场景下,正式架构与长期结论优先沉淀到 `doc/`。
|
- 多线程联调场景下,正式架构与长期结论优先沉淀到 `doc/`。
|
||||||
|
- 当前联调架构的阶段总结见:
|
||||||
|
- [联调架构阶段总结](D:/dev/cmr-mini/doc/gameplay/联调架构阶段总结.md)
|
||||||
- 面向后端线程的阶段性实施说明,优先写入根目录 [t2b.md](D:/dev/cmr-mini/t2b.md)。
|
- 面向后端线程的阶段性实施说明,优先写入根目录 [t2b.md](D:/dev/cmr-mini/t2b.md)。
|
||||||
- backend 新增写给总控线程的回写板:
|
- backend 新增写给总控线程的回写板:
|
||||||
- [b2t.md](D:/dev/cmr-mini/b2t.md)
|
- [b2t.md](D:/dev/cmr-mini/b2t.md)
|
||||||
@@ -46,6 +48,12 @@
|
|||||||
- `place / map asset / tile release / course source / course set / course variant / runtime binding`
|
- `place / map asset / tile release / course source / course set / course variant / runtime binding`
|
||||||
- `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链
|
- `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链
|
||||||
- `一键标准回归` 与 `回归结果汇总` 已接入 workbench
|
- `一键标准回归` 与 `回归结果汇总` 已接入 workbench
|
||||||
|
- `当前 Launch 实际配置摘要` 已接入 workbench
|
||||||
|
- `前端调试日志` 已接入 workbench
|
||||||
|
- 三类标准 demo 入口已显式挂出:
|
||||||
|
- `evt_demo_001`
|
||||||
|
- `evt_demo_score_o_001`
|
||||||
|
- `evt_demo_variant_manual_001`
|
||||||
- workbench 日志已具备:
|
- workbench 日志已具备:
|
||||||
- 分步日志
|
- 分步日志
|
||||||
- 真实错误
|
- 真实错误
|
||||||
@@ -54,13 +62,12 @@
|
|||||||
- 预期判定
|
- 预期判定
|
||||||
- 下一步建议:
|
- 下一步建议:
|
||||||
- 联调标准化第一版视为已完成
|
- 联调标准化第一版视为已完成
|
||||||
- 下一步进入“真实输入替换第一刀”
|
- 当前主线进入“真实输入替换第二刀”
|
||||||
- 逐步把 demo 输入替换成更接近生产的真实输入:
|
- 当前优先替换:
|
||||||
- KML / 赛道文件
|
- `content manifest`
|
||||||
- 地图资源 URL
|
- `presentation schema`
|
||||||
- 内容 manifest
|
- `活动文案样例`
|
||||||
- presentation schema
|
- `KML / 赛道文件` 与 `地图资源 URL` 已接入,不再作为本轮重点
|
||||||
- 活动文案样例
|
|
||||||
- backend 在联调标准化阶段应优先保证:
|
- backend 在联调标准化阶段应优先保证:
|
||||||
- 从空白环境直接可跑
|
- 从空白环境直接可跑
|
||||||
- workbench 日志能明确定位失败步骤
|
- workbench 日志能明确定位失败步骤
|
||||||
@@ -83,8 +90,13 @@
|
|||||||
- frontend 进入联调标准化配合与小范围修复阶段
|
- frontend 进入联调标准化配合与小范围修复阶段
|
||||||
- 只做字段修正、摘要打磨、一致性修复
|
- 只做字段修正、摘要打磨、一致性修复
|
||||||
- 优先复用 backend 一键测试环境做回归
|
- 优先复用 backend 一键测试环境做回归
|
||||||
|
- 优先复用:
|
||||||
|
- `回归结果汇总`
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
|
- `前端调试日志`
|
||||||
- 不继续扩新页面链
|
- 不继续扩新页面链
|
||||||
- 不做复杂运营样式
|
- 不做复杂运营样式
|
||||||
|
- 不启动活动卡片(列表)产品化开发
|
||||||
|
|
||||||
当前阶段的核心目标已经从“把地图画出来”升级为“建立一套可长期扩展的运动地图游戏底座”。
|
当前阶段的核心目标已经从“把地图画出来”升级为“建立一套可长期扩展的运动地图游戏底座”。
|
||||||
这套底座已经具备以下关键能力:
|
这套底座已经具备以下关键能力:
|
||||||
|
|||||||
32
t2b.md
32
t2b.md
@@ -1,6 +1,6 @@
|
|||||||
# T2B 协作清单
|
# T2B 协作清单
|
||||||
> 文档版本:v1.11
|
> 文档版本:v1.12
|
||||||
> 最后更新:2026-04-03 14:16:17
|
> 最后更新:2026-04-03 16:55:07
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
@@ -25,6 +25,11 @@ backend 当前已完成:
|
|||||||
- `publish` 默认继承当前 active 三元组
|
- `publish` 默认继承当前 active 三元组
|
||||||
- `Bootstrap Demo` 与 `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链
|
- `Bootstrap Demo` 与 `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链
|
||||||
- `一键标准回归` 与 `回归结果汇总` 已接入标准联调入口
|
- `一键标准回归` 与 `回归结果汇总` 已接入标准联调入口
|
||||||
|
- `前端调试日志` 与 `当前 Launch 实际配置摘要` 已接入 workbench
|
||||||
|
- 三类标准 demo 入口已显式挂出:
|
||||||
|
- `evt_demo_001`
|
||||||
|
- `evt_demo_score_o_001`
|
||||||
|
- `evt_demo_variant_manual_001`
|
||||||
- workbench 日志已补齐:
|
- workbench 日志已补齐:
|
||||||
- 分步日志
|
- 分步日志
|
||||||
- 真实错误
|
- 真实错误
|
||||||
@@ -42,16 +47,22 @@ backend 当前已完成:
|
|||||||
2. 固化详细日志口径,失败时明确定位在哪一步
|
2. 固化详细日志口径,失败时明确定位在哪一步
|
||||||
3. 固化稳定测试数据,并逐步支持更接近生产的真实输入
|
3. 固化稳定测试数据,并逐步支持更接近生产的真实输入
|
||||||
|
|
||||||
当前认为“联调标准化第一版”已经基本到位,backend 下一步应进入:
|
当前认为“联调标准化第一版”已经完成,backend 下一步应进入:
|
||||||
|
|
||||||
**真实输入替换第一刀**
|
**真实输入替换第一刀**
|
||||||
|
|
||||||
优先顺序建议:
|
优先顺序建议:
|
||||||
|
|
||||||
1. 先替换真实 KML / 赛道文件
|
1. 继续推进真实 `content manifest`
|
||||||
2. 再替换真实地图资源 URL
|
2. 再推进真实 `presentation schema`
|
||||||
3. 再替换真实内容 manifest / presentation schema
|
3. 最后补真实 `活动文案样例`
|
||||||
4. 最后再补真实活动文案样例
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 真实 `KML / 赛道文件`
|
||||||
|
- 真实 `地图资源 URL`
|
||||||
|
|
||||||
|
这两类输入已接入,当前不再作为本轮重点。
|
||||||
|
|
||||||
原则:
|
原则:
|
||||||
|
|
||||||
@@ -59,6 +70,13 @@ backend 当前已完成:
|
|||||||
- 不重新设计联调流程
|
- 不重新设计联调流程
|
||||||
- 只是把 demo 输入逐步换成更接近生产的真实输入
|
- 只是把 demo 输入逐步换成更接近生产的真实输入
|
||||||
|
|
||||||
|
当前 backend 不建议切去做:
|
||||||
|
|
||||||
|
- 活动卡片列表产品化
|
||||||
|
- 新的玩家侧页面入口
|
||||||
|
- 更多管理对象
|
||||||
|
- 更复杂后台 UI
|
||||||
|
|
||||||
当前进一步明确 backend 的执行口径如下:
|
当前进一步明确 backend 的执行口径如下:
|
||||||
|
|
||||||
### 0.1 一键测试链路
|
### 0.1 一键测试链路
|
||||||
|
|||||||
18
t2f.md
18
t2f.md
@@ -1,6 +1,6 @@
|
|||||||
# T2F 协作清单
|
# T2F 协作清单
|
||||||
> 文档版本:v1.6
|
> 文档版本:v1.7
|
||||||
> 最后更新:2026-04-03 13:08:15
|
> 最后更新:2026-04-03 16:55:07
|
||||||
|
|
||||||
说明:
|
说明:
|
||||||
|
|
||||||
@@ -19,6 +19,10 @@
|
|||||||
- 验证活动运营域摘要接线是否稳定
|
- 验证活动运营域摘要接线是否稳定
|
||||||
- 修正联调中发现的小范围字段、展示、一致性问题
|
- 修正联调中发现的小范围字段、展示、一致性问题
|
||||||
- 使用 backend 当前统一的“一键测试环境”和稳定 demo 数据做回归
|
- 使用 backend 当前统一的“一键测试环境”和稳定 demo 数据做回归
|
||||||
|
- 使用 backend 当前统一的结构化诊断入口做回归:
|
||||||
|
- `回归结果汇总`
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
|
- `前端调试日志`
|
||||||
- 继续保持 runtime 主链稳定,不扩新页面链
|
- 继续保持 runtime 主链稳定,不扩新页面链
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -48,7 +52,11 @@
|
|||||||
- backend 当前测试能力已升级:
|
- backend 当前测试能力已升级:
|
||||||
- `Bootstrap Demo`
|
- `Bootstrap Demo`
|
||||||
- `一键补齐 Runtime 并发布`
|
- `一键补齐 Runtime 并发布`
|
||||||
|
- `一键标准回归`
|
||||||
|
- `回归结果汇总`
|
||||||
|
- `当前 Launch 实际配置摘要`
|
||||||
- 分步日志 / 真实错误 / stack / 最后一次 curl / 预期判定
|
- 分步日志 / 真实错误 / stack / 最后一次 curl / 预期判定
|
||||||
|
- `POST /dev/client-logs`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -93,6 +101,7 @@
|
|||||||
- 不消费完整 `EventPresentation` 结构
|
- 不消费完整 `EventPresentation` 结构
|
||||||
- 不把 `ContentBundle` 展开成资源明细
|
- 不把 `ContentBundle` 展开成资源明细
|
||||||
- 不重构首页、结果页、历史页已有结构
|
- 不重构首页、结果页、历史页已有结构
|
||||||
|
- 不启动活动卡片(列表)产品化开发
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -105,6 +114,7 @@
|
|||||||
- 先做“看得见活动运营对象”,不先做复杂运营化样式
|
- 先做“看得见活动运营对象”,不先做复杂运营化样式
|
||||||
- 当前进入联调回归阶段,优先修问题,不主动扩新页面入口
|
- 当前进入联调回归阶段,优先修问题,不主动扩新页面入口
|
||||||
- 当前联调应优先复用 backend 一键测试环境,不再各自手工铺多份 demo 对象
|
- 当前联调应优先复用 backend 一键测试环境,不再各自手工铺多份 demo 对象
|
||||||
|
- 当前联调应优先复用 backend 提供的结构化诊断链,不再依赖截图 + 口头描述排查
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -127,6 +137,10 @@
|
|||||||
4. 不继续扩新页面链,不做复杂运营样式
|
4. 不继续扩新页面链,不做复杂运营样式
|
||||||
5. 如果前端发现缺字段,再由总控统一回写给 backend
|
5. 如果前端发现缺字段,再由总控统一回写给 backend
|
||||||
6. 当前前端下一步重点是配合 backend 的一键测试环境做稳定回归,不再新增玩家侧功能入口
|
6. 当前前端下一步重点是配合 backend 的一键测试环境做稳定回归,不再新增玩家侧功能入口
|
||||||
|
7. 当前前端继续只做:
|
||||||
|
- 联调回归
|
||||||
|
- 小范围修复
|
||||||
|
- 结构化日志补充
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -307,6 +307,16 @@ function testLaunchRuntimeAdapter(): void {
|
|||||||
},
|
},
|
||||||
launch: {
|
launch: {
|
||||||
source: 'event',
|
source: 'event',
|
||||||
|
resolvedRelease: {
|
||||||
|
launchMode: 'formal-release',
|
||||||
|
source: 'current-release',
|
||||||
|
eventId: 'evt_demo_variant_manual_001',
|
||||||
|
releaseId: 'rel_runtime_001',
|
||||||
|
configLabel: 'runtime demo',
|
||||||
|
manifestUrl: 'https://example.com/releases/rel_runtime_001/manifest.json',
|
||||||
|
manifestChecksumSha256: 'manifest-sha-001',
|
||||||
|
routeCode: 'route-variant-b',
|
||||||
|
},
|
||||||
config: {
|
config: {
|
||||||
configUrl: 'https://example.com/runtime.json',
|
configUrl: 'https://example.com/runtime.json',
|
||||||
configLabel: 'runtime demo',
|
configLabel: 'runtime demo',
|
||||||
@@ -352,6 +362,9 @@ function testLaunchRuntimeAdapter(): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const envelope = adaptBackendLaunchResultToEnvelope(launchResult)
|
const envelope = adaptBackendLaunchResultToEnvelope(launchResult)
|
||||||
|
assert(!!envelope.resolvedRelease, 'resolvedRelease 应映射到 GameLaunchEnvelope.resolvedRelease')
|
||||||
|
assert(envelope.resolvedRelease!.manifestUrl === 'https://example.com/releases/rel_runtime_001/manifest.json', 'resolvedRelease.manifestUrl 应正确适配')
|
||||||
|
assert(envelope.resolvedRelease!.releaseId === 'rel_runtime_001', 'resolvedRelease.releaseId 应正确适配')
|
||||||
assert(!!envelope.runtime, 'launch.runtime 应映射到 GameLaunchEnvelope.runtime')
|
assert(!!envelope.runtime, 'launch.runtime 应映射到 GameLaunchEnvelope.runtime')
|
||||||
assert(envelope.runtime!.runtimeBindingId === 'rtb_001', 'runtimeBindingId 应正确适配')
|
assert(envelope.runtime!.runtimeBindingId === 'rtb_001', 'runtimeBindingId 应正确适配')
|
||||||
assert(envelope.runtime!.placeName === '示范校园', 'placeName 应正确适配')
|
assert(envelope.runtime!.placeName === '示范校园', 'placeName 应正确适配')
|
||||||
|
|||||||
Reference in New Issue
Block a user