From 114c52404410da7e735975d0f8dd834d47334072 Mon Sep 17 00:00:00 2001 From: zhangyan Date: Fri, 3 Apr 2026 14:18:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E4=B8=80=E9=94=AE=E5=9B=9E?= =?UTF-8?q?=E5=BD=92=E4=B8=8E=E7=9C=9F=E5=AE=9E=E8=BE=93=E5=85=A5=E5=87=86?= =?UTF-8?q?=E5=A4=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- b2f.md | 71 +++++- b2t.md | 69 +++++- backend/README.md | 5 +- backend/docs/开发说明.md | 16 +- .../internal/httpapi/handlers/dev_handler.go | 217 +++++++++++++++++- backend/internal/store/postgres/dev_store.go | 19 ++ .../活动运营域摘要第一刀联调回归清单.md | 20 +- f2b.md | 19 +- f2t.md | 20 +- .../pages/event-prepare/event-prepare.ts | 5 +- miniprogram/pages/event/event.ts | 5 +- miniprogram/utils/backendPlayCopy.ts | 65 ++++++ readme-develop.md | 17 +- t2b.md | 80 ++++++- 14 files changed, 599 insertions(+), 29 deletions(-) create mode 100644 miniprogram/utils/backendPlayCopy.ts diff --git a/b2f.md b/b2f.md index 7dc55f2..5181580 100644 --- a/b2f.md +++ b/b2f.md @@ -1,6 +1,6 @@ # b2f -> 文档版本:v1.7 -> 最后更新:2026-04-03 12:36:15 +> 文档版本:v1.10 +> 最后更新:2026-04-03 20:10:25 说明: @@ -103,6 +103,73 @@ ## 已确认 +### B2F-024 + +- 时间:2026-04-03 20:10:25 +- 谁提的:backend +- 当前事实: + - backend 已确认 `evt_demo_variant_manual_001` 曾存在历史残留的 `launched` session,导致 `play.primaryAction=continue` + - backend 已把清理逻辑并入 `POST /dev/bootstrap-demo` + - 现在每次准备 demo 数据时,都会自动把 demo event 下残留的: + - `launched` + - `running` + session 改成 `cancelled` + - 这意味着前端后续再用标准测试链回归时,不需要手工清理旧 demo ongoing +- 需要对方确认什么: + - frontend 遇到这类“明明本地没有恢复快照,但后端仍返回 continue”的情况,优先先重新执行一次 `Bootstrap Demo` +- 是否已解决:是 + +### B2F-023 + +- 时间:2026-04-03 13:24:38 +- 谁提的:backend +- 当前事实: + - backend 已把标准联调回归收成一键流 + - workbench 当前新增: + - `一键标准回归` + - `回归结果汇总` + - 这条链会在标准发布链之后继续自动验证: + - `GET /events/{eventPublicID}/play` + - `POST /events/{eventPublicID}/launch` + - `GET /sessions/{sessionPublicID}/result` + - `GET /me/sessions` + - `GET /me/results` + - 回归结果会直接显示分项通过/未通过,不再要求 frontend 自己口头判断 +- 需要对方确认什么: + - frontend 当前回归优先使用这条一键标准回归链 +- 是否已解决:是 + +### B2F-022 + +- 时间:2026-04-03 13:18:42 +- 谁提的:backend +- 当前事实: + - backend 当前已进入“联调标准化阶段” + - 当前推荐 frontend 优先使用 workbench 的: + - `Bootstrap Demo` + - `一键补齐 Runtime 并发布` + 作为联调回归入口 + - backend 现在提供的不是零散 demo 文本,而是一套可重复创建的真实测试对象: + - `place` + - `map asset` + - `tile release` + - `course source` + - `course set` + - `course variant` + - `runtime binding` + - `presentation` + - `content bundle` + - `release` + - 如果联调失败,workbench 当前会直接给出: + - 分步日志 + - 真实错误消息 + - stack + - 最后一次 curl + - 预期判定 +- 需要对方确认什么: + - frontend 回归时优先基于这条一键测试链,不再先手工拼测试数据 +- 是否已解决:是 + ### B2F-019 - 时间:2026-04-03 12:36:15 diff --git a/b2t.md b/b2t.md index d0b104b..98823ce 100644 --- a/b2t.md +++ b/b2t.md @@ -1,6 +1,6 @@ # B2T 协作清单 -> 文档版本:v1.11 -> 最后更新:2026-04-03 13:04:32 +> 文档版本:v1.13 +> 最后更新:2026-04-03 13:24:38 说明: @@ -130,6 +130,55 @@ ## 已完成 +### B2T-023 + +- 时间:2026-04-03 13:24:38 +- 谁提的:backend +- 当前事实: + - backend 已把标准联调入口继续固化为一键回归流 + - workbench 当前新增: + - `一键标准回归` + - `回归结果汇总` + - 这条链当前会在: + - `Bootstrap Demo` + - `一键补齐 Runtime 并发布` + 之后,继续自动验证: + - `play` + - `launch` + - `result` + - `history` + - 回归汇总当前会直接显示: + - 分项通过/未通过 + - `Session ID` + - 总判定 +- 需要对方确认什么: + - 无 +- 是否已解决:是 + +### B2T-021 + +- 时间:2026-04-03 13:18:42 +- 谁提的:backend +- 当前事实: + - backend 已根据 [t2b.md](D:/dev/cmr-mini/t2b.md) v1.10 的最新口径,切入“联调标准化阶段” + - 当前 backend 主线不再是继续扩对象或扩 workbench 管理能力 + - 当前统一收口为 3 个目标: + - 固化“一键测试”链路 + - 固化详细日志口径 + - 固化稳定测试数据 + - 当前最推荐联调方式为: + - `Bootstrap Demo` + - `一键补齐 Runtime 并发布` + - 这条链当前已可从空白环境直接跑通,并可输出: + - 分步日志 + - 真实错误 + - stack + - 最后一次 curl + - 预期判定 +- 需要对方确认什么: + - 无 +- 是否已解决:是 + ### B2T-019 - 时间:2026-04-03 13:04:32 @@ -302,6 +351,22 @@ ## 下一步 +### B2T-022 + +- 时间:2026-04-03 13:18:42 +- 谁提的:backend +- 当前事实: + - backend 当前已具备“一键测试环境”与最小生产骨架测试数据 + - 后续联调阶段如要进一步贴近生产,只需要逐步替换以下 demo 输入: + - 地图资源 URL + - KML / 赛道文件 + - ContentBundle manifest + - Presentation schema + - 当前不需要继续新增对象层级即可支撑联调 +- 需要对方确认什么: + - 总控后续如需要更接近生产的真实测试输入,请直接指定优先级最高的一类资源 +- 是否已解决:否 + ### B2T-010 - 时间:2026-04-03 08:52:11 diff --git a/backend/README.md b/backend/README.md index 7ca7a09..42d4a49 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,6 +1,6 @@ # Backend -> 文档版本:v1.11 -> 最后更新:2026-04-03 13:04:32 +> 文档版本:v1.12 +> 最后更新:2026-04-03 13:24:38 这套后端现在已经能支撑一条完整主链: @@ -63,5 +63,6 @@ cd D:\dev\cmr-mini\backend - Runtime 自动补齐 + 默认绑定发布一键验证 - Bootstrap Demo 自动回填最小生产骨架 ID - 一键测试环境:可从空白状态自动准备 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` diff --git a/backend/docs/开发说明.md b/backend/docs/开发说明.md index 8d566bb..dfe5f3f 100644 --- a/backend/docs/开发说明.md +++ b/backend/docs/开发说明.md @@ -1,6 +1,6 @@ # 开发说明 -> 文档版本:v1.12 -> 最后更新:2026-04-03 13:04:32 +> 文档版本:v1.14 +> 最后更新:2026-04-03 20:10:25 ## 1. 环境变量 @@ -73,6 +73,7 @@ cd D:\dev\cmr-mini\backend 1. `Bootstrap Demo` 2. `一键补齐 Runtime 并发布` +3. `一键标准回归` 当前这条一键链会自动完成: @@ -90,6 +91,9 @@ cd D:\dev\cmr-mini\backend - `runtime binding` - publish - release 回读校验 +- `play / launch / result / history` 回归汇总 +- demo 活动残留 ongoing session 清理: + - 会把 demo event 下历史遗留的 `launched / running` session 自动改成 `cancelled` 当前日志能力: @@ -104,6 +108,14 @@ cd D:\dev\cmr-mini\backend - `Content Bundle` - `Runtime Binding` - `判定` +- 成功跑完标准回归后,“回归结果汇总”会直接给出: + - `发布链` + - `Play` + - `Launch` + - `Result` + - `History` + - `Session ID` + - `总判定` ## 3. 当前开发约定 diff --git a/backend/internal/httpapi/handlers/dev_handler.go b/backend/internal/httpapi/handlers/dev_handler.go index 528e554..f6b1f04 100644 --- a/backend/internal/httpapi/handlers/dev_handler.go +++ b/backend/internal/httpapi/handlers/dev_handler.go @@ -734,8 +734,9 @@ const devWorkbenchHTML = ` + -
这些流程会复用当前表单里的手机号、设备、event、channel 等输入。“一键默认绑定发布” 会自动执行:Get Event -> Import Presentation -> Import Bundle -> Save Event Defaults -> Build Source -> Publish Build -> Get Release。“一键补齐 Runtime 并发布” 会在缺少默认 runtime 时自动创建 Runtime Binding,再继续发布链。
+
这些流程会复用当前表单里的手机号、设备、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。
预期结果
@@ -746,6 +747,18 @@ const devWorkbenchHTML = `
判定 待执行
+
+
回归结果汇总
+
+
发布链 待执行
+
Play 待执行
+
Launch 待执行
+
Result 待执行
+
History 待执行
+
Session ID -
+
总判定 待执行
+
+
@@ -2217,6 +2230,70 @@ const devWorkbenchHTML = ` $('flow-admin-verdict').textContent = verdict; } + function resetStandardRegressionExpectation() { + $('flow-regression-publish-result').textContent = '待执行'; + $('flow-regression-play-result').textContent = '待执行'; + $('flow-regression-launch-result').textContent = '待执行'; + $('flow-regression-result-result').textContent = '待执行'; + $('flow-regression-history-result').textContent = '待执行'; + $('flow-regression-session-id').textContent = '-'; + $('flow-regression-overall').textContent = '待执行'; + } + + function setStandardRegressionExpectation(summary) { + const publishText = summary && summary.publish ? summary.publish : '待执行'; + const playText = summary && summary.play ? summary.play : '待执行'; + const launchText = summary && summary.launch ? summary.launch : '待执行'; + const resultText = summary && summary.result ? summary.result : '待执行'; + const historyText = summary && summary.history ? summary.history : '待执行'; + const sessionId = summary && summary.sessionId ? summary.sessionId : '-'; + const overall = summary && summary.overall ? summary.overall : '待执行'; + $('flow-regression-publish-result').textContent = publishText; + $('flow-regression-play-result').textContent = playText; + $('flow-regression-launch-result').textContent = launchText; + $('flow-regression-result-result').textContent = resultText; + $('flow-regression-history-result').textContent = historyText; + $('flow-regression-session-id').textContent = sessionId; + $('flow-regression-overall').textContent = overall; + } + + function extractList(payload) { + if (Array.isArray(payload)) { + return payload; + } + if (!payload || typeof payload !== 'object') { + return []; + } + if (Array.isArray(payload.items)) { + return payload.items; + } + if (Array.isArray(payload.results)) { + return payload.results; + } + if (Array.isArray(payload.sessions)) { + return payload.sessions; + } + return []; + } + + function listContainsSession(list, sessionId) { + if (!sessionId) { + return false; + } + return list.some(function(item) { + if (!item || typeof item !== 'object') { + return false; + } + if (item.id && item.id === sessionId) { + return true; + } + if (item.session && item.session.id && item.session.id === sessionId) { + return true; + } + return false; + }); + } + async function runAdminDefaultPublishFlow(options) { const ensureRuntime = options && options.ensureRuntime === true; const flowTitle = ensureRuntime ? 'flow-admin-runtime-publish' : 'flow-admin-default-publish'; @@ -2407,6 +2484,140 @@ const devWorkbenchHTML = ` return releaseDetail; } + async function runStandardRegressionFlow() { + const flowTitle = 'flow-standard-regression'; + const eventId = $('event-id').value || $('admin-event-ref-id').value; + if (!trimmedOrUndefined(eventId)) { + throw new Error('event id is required'); + } + resetStandardRegressionExpectation(); + + writeLog(flowTitle + '.step', { step: 'prepare-release', eventId: eventId }); + const releaseDetail = await runAdminDefaultPublishFlow({ ensureRuntime: true }); + const publishPass = $('flow-admin-verdict').textContent.indexOf('通过') === 0; + + writeLog(flowTitle + '.step', { + step: 'login-wechat', + code: $('wechat-code').value, + deviceKey: $('wechat-device').value + }); + const login = await request('POST', '/auth/login/wechat-mini', { + code: $('wechat-code').value, + clientType: 'wechat', + deviceKey: $('wechat-device').value + }); + state.accessToken = login.data.tokens.accessToken; + state.refreshToken = login.data.tokens.refreshToken; + + writeLog(flowTitle + '.step', { + step: 'event-play', + eventId: eventId + }); + const play = await request('GET', '/events/' + encodeURIComponent(eventId) + '/play', undefined, true); + const playPass = !!(play.data && play.data.play && play.data.resolvedRelease && play.data.resolvedRelease.manifestUrl); + + writeLog(flowTitle + '.step', { + step: 'event-launch', + eventId: eventId, + releaseId: $('event-release-id').value || state.releaseId, + variantId: trimmedOrUndefined($('event-variant-id').value) + }); + const launch = await request('POST', '/events/' + encodeURIComponent(eventId) + '/launch', { + releaseId: $('event-release-id').value, + variantId: trimmedOrUndefined($('event-variant-id').value), + clientType: $('sms-client-type').value, + deviceKey: $('event-device').value + }, true); + state.sessionId = launch.data.launch.business.sessionId; + state.sessionToken = launch.data.launch.business.sessionToken; + syncState(); + const launchPass = !!( + launch.data && + launch.data.launch && + launch.data.launch.business && + launch.data.launch.business.sessionId && + launch.data.launch.resolvedRelease && + launch.data.launch.resolvedRelease.manifestUrl + ); + + writeLog(flowTitle + '.step', { + step: 'session-start', + sessionId: state.sessionId + }); + await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/start', { + sessionToken: state.sessionToken + }); + + writeLog(flowTitle + '.step', { + step: 'session-finish', + sessionId: state.sessionId, + status: $('finish-status').value + }); + await request('POST', '/sessions/' + encodeURIComponent(state.sessionId) + '/finish', { + sessionToken: state.sessionToken, + status: $('finish-status').value, + summary: buildFinishSummary() + }); + + writeLog(flowTitle + '.step', { + step: 'session-result', + sessionId: state.sessionId + }); + const sessionResult = await request('GET', '/sessions/' + encodeURIComponent(state.sessionId) + '/result', undefined, true); + const resultPass = !!( + sessionResult.data && + sessionResult.data.session && + sessionResult.data.session.id === state.sessionId && + sessionResult.data.result + ); + + writeLog(flowTitle + '.step', { + step: 'history-check', + sessionId: state.sessionId + }); + const mySessions = await request('GET', '/me/sessions?limit=10', undefined, true); + const myResults = await request('GET', '/me/results?limit=10', undefined, true); + const sessionsList = extractList(mySessions.data); + const resultsList = extractList(myResults.data); + const historyPass = listContainsSession(sessionsList, state.sessionId) && listContainsSession(resultsList, state.sessionId); + + const summary = { + publish: publishPass ? '通过:发布链可重复跑通' : '未通过:发布链未返回通过判定', + play: playPass ? '通过:play 返回 resolvedRelease / play 摘要' : '未通过:play 缺少关键摘要', + launch: launchPass ? '通过:launch 返回 manifest + session' : '未通过:launch 缺少 manifest 或 session', + result: resultPass ? '通过:单局 result 可直接回查' : '未通过:单局 result 未回查成功', + history: historyPass ? '通过:me/sessions + me/results 均收录本局' : '未通过:history 未同时收录本局', + sessionId: state.sessionId || '-', + overall: publishPass && playPass && launchPass && resultPass && historyPass ? '通过:launch / play / result / history 回归已跑通' : '未通过:请看上面分项和日志' + }; + setStandardRegressionExpectation(summary); + writeLog(flowTitle + '.expected', { + eventId: eventId, + releaseId: releaseDetail && releaseDetail.data ? releaseDetail.data.id : state.releaseId, + sessionId: state.sessionId, + publish: summary.publish, + play: summary.play, + launch: summary.launch, + result: summary.result, + history: summary.history, + overall: summary.overall + }); + persistState(); + return { + data: { + eventId: eventId, + releaseId: releaseDetail && releaseDetail.data ? releaseDetail.data.id : state.releaseId, + sessionId: state.sessionId, + publish: publishPass, + play: playPass, + launch: launchPass, + result: resultPass, + history: historyPass, + overall: publishPass && playPass && launchPass && resultPass && historyPass + } + }; + } + function setStatus(text, isError = false) { statusEl.textContent = text; statusEl.className = isError ? 'status error' : 'status'; @@ -4124,6 +4335,10 @@ const devWorkbenchHTML = ` return await runAdminDefaultPublishFlow({ ensureRuntime: true }); }); + $('btn-flow-standard-regression').onclick = () => run('flow-standard-regression', async () => { + return await runStandardRegressionFlow(); + }); + [ 'sms-client-type', 'sms-scene', 'sms-mobile', 'sms-device', 'sms-country', 'sms-code', 'wechat-code', 'wechat-device', 'local-config-file', 'config-event-id', 'config-runtime-binding-id', 'config-presentation-id', 'config-content-bundle-id', 'entry-channel-code', 'entry-channel-type', diff --git a/backend/internal/store/postgres/dev_store.go b/backend/internal/store/postgres/dev_store.go index 44022ca..3ed2fd5 100644 --- a/backend/internal/store/postgres/dev_store.go +++ b/backend/internal/store/postgres/dev_store.go @@ -23,6 +23,7 @@ type DemoBootstrapSummary struct { 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) { @@ -597,6 +598,23 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro return nil, fmt.Errorf("ensure variant manual demo card: %w", err) } + var cleanedSessionCount int64 + if err := tx.QueryRow(ctx, ` + WITH cleaned AS ( + UPDATE game_sessions + SET + status = 'cancelled', + ended_at = NOW(), + updated_at = NOW() + WHERE event_id = ANY($1::uuid[]) + AND status IN ('launched', 'running') + RETURNING 1 + ) + SELECT COUNT(*) FROM cleaned + `, []string{eventID, manualEventID}).Scan(&cleanedSessionCount); err != nil { + return nil, fmt.Errorf("cleanup demo ongoing sessions: %w", err) + } + if err := tx.Commit(ctx); err != nil { return nil, err } @@ -619,5 +637,6 @@ func (s *Store) EnsureDemoData(ctx context.Context) (*DemoBootstrapSummary, erro VariantManualEventID: "evt_demo_variant_manual_001", VariantManualRelease: manualReleaseRow.PublicID, VariantManualCardID: manualCardPublicID, + CleanedSessionCount: cleanedSessionCount, }, nil } diff --git a/doc/gameplay/活动运营域摘要第一刀联调回归清单.md b/doc/gameplay/活动运营域摘要第一刀联调回归清单.md index c1b1be3..9d8e09e 100644 --- a/doc/gameplay/活动运营域摘要第一刀联调回归清单.md +++ b/doc/gameplay/活动运营域摘要第一刀联调回归清单.md @@ -1,6 +1,6 @@ # 活动运营域摘要第一刀联调回归清单 -> 文档版本:v1.0 -> 最后更新:2026-04-03 19:38:00 +> 文档版本:v1.1 +> 最后更新:2026-04-03 19:48:00 ## 目标 @@ -13,6 +13,22 @@ - `launch.presentation / launch.contentBundle` 会话快照 - 与 runtime 主链的相互不干扰 +## 联调环境 + +当前统一使用 backend 提供的一键测试环境做回归,不再各自手工准备多份 demo 对象。 + +推荐路径: + +- 先执行 `Bootstrap Demo` +- 再执行“一键补齐 Runtime 并发布” +- 再用稳定 demo 数据进入: + - 活动详情页 + - 准备页 + - 地图页 + - 结果页 + +当前重点不是验证 workbench 本身,而是利用这套统一环境回归前端摘要链是否稳定。 + ## 回归项 ### 1. 活动详情页摘要 diff --git a/f2b.md b/f2b.md index 81922c9..32941f5 100644 --- a/f2b.md +++ b/f2b.md @@ -1,6 +1,6 @@ # F2B 协作清单 -> 文档版本:v1.4 -> 最后更新:2026-04-03 19:20:00 +> 文档版本:v1.5 +> 最后更新:2026-04-03 20:02:00 说明: @@ -14,7 +14,20 @@ ## 待确认 -- 当前无 +### F2B-011 + +- 时间:2026-04-03 +- 提出方:前端 +- 当前事实: + - 使用 backend 一键测试环境联调 `evt_demo_variant_manual_001` 时,活动页 / 准备页返回: + - `primaryAction = continue` + - `reason = user has an ongoing session for this event` + - 但前端本地当前没有可恢复快照,且本轮联调主观确认“已经没有需要恢复的游戏” + - 当前看起来像是 backend 仍认定该用户在该活动下存在 ongoing session +- 需要对方确认什么: + - 请 backend 核对该用户在 `evt_demo_variant_manual_001` 下是否仍有 `launched / running` session 未清掉 + - 如这是预期行为,请说明推荐的标准清理路径;如不是预期,请修正 ongoing 判定或测试环境回收逻辑 +- 状态:待确认 --- diff --git a/f2t.md b/f2t.md index 742cbf4..1e69306 100644 --- a/f2t.md +++ b/f2t.md @@ -1,6 +1,6 @@ # F2T 协作清单 -> 文档版本:v1.6 -> 最后更新:2026-04-03 19:38:00 +> 文档版本:v1.7 +> 最后更新:2026-04-03 19:48:00 说明: @@ -158,8 +158,22 @@ - 无 - 是否已解决:是 +### F2T-D005 + +- 时间:2026-04-03 19:48:00 +- 谁提的:frontend +- 当前事实: + - 已按总控最新口径把联调方式标准化 + - 当前活动运营域摘要第一刀回归默认统一使用 backend 的一键测试环境: + - `Bootstrap Demo` + - `一键补齐 Runtime 并发布` + - 不再建议前后端各自手工铺多份 demo 对象 +- 需要确认什么: + - 无 +- 是否已解决:是 + --- ## 下一步 -- 当前进入活动运营域摘要第一刀的联调回归与小范围修复阶段 +- 当前进入活动运营域摘要第一刀在 backend 一键测试环境下的联调回归与小范围修复阶段 diff --git a/miniprogram/pages/event-prepare/event-prepare.ts b/miniprogram/pages/event-prepare/event-prepare.ts index 84cd07e..cd13710 100644 --- a/miniprogram/pages/event-prepare/event-prepare.ts +++ b/miniprogram/pages/event-prepare/event-prepare.ts @@ -1,6 +1,7 @@ import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth' import { getEventPlay, launchEvent, type BackendCourseVariantSummary, type BackendEventPlayResult } from '../../utils/backendApi' import { adaptBackendLaunchResultToEnvelope } from '../../utils/backendLaunchAdapter' +import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy' import { prepareMapPageUrlForLaunch } from '../../utils/gameLaunch' import { HeartRateController } from '../../engine/sensor/heartRateController' @@ -302,8 +303,8 @@ Page({ releaseText: result.resolvedRelease ? `${result.resolvedRelease.configLabel} / ${result.resolvedRelease.releaseId}` : '当前无可用 release', - actionText: `${result.play.primaryAction} / ${result.play.reason}`, - statusText: result.play.canLaunch ? '准备完成,可进入地图' : '当前不可启动', + actionText: formatBackendPlayActionText(result.play.primaryAction, result.play.reason), + statusText: formatBackendPlayStatusText(result.play.canLaunch, result.play.primaryAction, result.play.reason), assignmentMode: result.play.assignmentMode || '', variantModeText: formatAssignmentMode(result.play.assignmentMode), variantSummaryText: formatVariantSummary(result), diff --git a/miniprogram/pages/event/event.ts b/miniprogram/pages/event/event.ts index a220604..0b1ec8a 100644 --- a/miniprogram/pages/event/event.ts +++ b/miniprogram/pages/event/event.ts @@ -1,5 +1,6 @@ import { loadBackendAuthTokens, loadBackendBaseUrl } from '../../utils/backendAuth' import { getEventPlay, type BackendEventPlayResult } from '../../utils/backendApi' +import { formatBackendPlayActionText, formatBackendPlayStatusText } from '../../utils/backendPlayCopy' type EventPageData = { eventId: string @@ -136,8 +137,8 @@ Page({ releaseText: result.resolvedRelease ? `${result.resolvedRelease.configLabel} / ${result.resolvedRelease.releaseId}` : '当前无可用 release', - actionText: `${result.play.primaryAction} / ${result.play.reason}`, - statusText: result.play.canLaunch ? '可启动' : '当前不可启动', + actionText: formatBackendPlayActionText(result.play.primaryAction, result.play.reason), + statusText: formatBackendPlayStatusText(result.play.canLaunch, result.play.primaryAction, result.play.reason), variantModeText: formatAssignmentMode(result.play.assignmentMode), variantSummaryText: formatVariantSummary(result), presentationText: formatPresentationSummary(result), diff --git a/miniprogram/utils/backendPlayCopy.ts b/miniprogram/utils/backendPlayCopy.ts new file mode 100644 index 0000000..0c5103d --- /dev/null +++ b/miniprogram/utils/backendPlayCopy.ts @@ -0,0 +1,65 @@ +function normalizeReason(reason?: string | null): string { + if (!reason) { + return '' + } + + if (reason === 'user has an ongoing session for this event') { + return '当前活动存在未结束对局' + } + if (reason === 'no ongoing session for this event') { + return '当前活动没有进行中的对局' + } + if (reason === 'ready to launch') { + return '当前可直接开始' + } + if (reason === 'launch blocked') { + return '当前启动受限' + } + + return reason +} + +function normalizeAction(action?: string | null): string { + if (!action) { + return '--' + } + + if (action === 'continue') { + return '继续上一局' + } + if (action === 'launch' || action === 'start') { + return '开始比赛' + } + if (action === 'preview') { + return '查看活动' + } + + return action +} + +export function formatBackendPlayActionText(action?: string | null, reason?: string | null): string { + const actionText = normalizeAction(action) + const reasonText = normalizeReason(reason) + if (!reasonText) { + return actionText + } + + return `${actionText}(${reasonText})` +} + +export function formatBackendPlayStatusText(canLaunch: boolean, action?: string | null, reason?: string | null): string { + if (!canLaunch) { + return '当前不可启动' + } + + if (action === 'continue') { + return '检测到未结束对局,可继续进入地图' + } + + const reasonText = normalizeReason(reason) + if (reasonText) { + return `${reasonText},可进入地图` + } + + return '可启动' +} diff --git a/readme-develop.md b/readme-develop.md index ea320db..417df4b 100644 --- a/readme-develop.md +++ b/readme-develop.md @@ -1,6 +1,6 @@ # CMR Mini 开发架构阶段总结 -> 文档版本:v1.13 -> 最后更新:2026-04-03 13:08:15 +> 文档版本:v1.14 +> 最后更新:2026-04-03 14:16:17 文档维护约定: @@ -45,6 +45,7 @@ - `Bootstrap Demo` 已可补齐: - `place / map asset / tile release / course source / course set / course variant / runtime binding` - `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链 + - `一键标准回归` 与 `回归结果汇总` 已接入 workbench - workbench 日志已具备: - 分步日志 - 真实错误 @@ -52,14 +53,18 @@ - 最后一次 curl - 预期判定 - 下一步建议: - - 固化“一键测试”链路为联调标准路径 - - 固化稳定测试数据,不再依赖手工铺对象 - - 逐步准备更接近生产的真实输入: - - 地图资源 URL + - 联调标准化第一版视为已完成 + - 下一步进入“真实输入替换第一刀” + - 逐步把 demo 输入替换成更接近生产的真实输入: - KML / 赛道文件 + - 地图资源 URL - 内容 manifest - presentation schema - 活动文案样例 + - backend 在联调标准化阶段应优先保证: + - 从空白环境直接可跑 + - workbench 日志能明确定位失败步骤 + - 同一条测试链可重复执行 - 前端线程建议正式上场时机: - 现在已完成活动运营域摘要接线第一刀 - 当前已完成: diff --git a/t2b.md b/t2b.md index c3934fe..cedae95 100644 --- a/t2b.md +++ b/t2b.md @@ -1,6 +1,6 @@ # T2B 协作清单 -> 文档版本:v1.10 -> 最后更新:2026-04-03 13:08:15 +> 文档版本:v1.11 +> 最后更新:2026-04-03 14:16:17 说明: @@ -24,6 +24,7 @@ backend 当前已完成: - `currentRuntimeBindingId` - `publish` 默认继承当前 active 三元组 - `Bootstrap Demo` 与 `一键补齐 Runtime 并发布` 已可从空白状态跑完整测试链 +- `一键标准回归` 与 `回归结果汇总` 已接入标准联调入口 - workbench 日志已补齐: - 分步日志 - 真实错误 @@ -41,6 +42,81 @@ backend 当前已完成: 2. 固化详细日志口径,失败时明确定位在哪一步 3. 固化稳定测试数据,并逐步支持更接近生产的真实输入 +当前认为“联调标准化第一版”已经基本到位,backend 下一步应进入: + +**真实输入替换第一刀** + +优先顺序建议: + +1. 先替换真实 KML / 赛道文件 +2. 再替换真实地图资源 URL +3. 再替换真实内容 manifest / presentation schema +4. 最后再补真实活动文案样例 + +原则: + +- 仍走同一条一键回归链 +- 不重新设计联调流程 +- 只是把 demo 输入逐步换成更接近生产的真实输入 + +当前进一步明确 backend 的执行口径如下: + +### 0.1 一键测试链路 + +请继续以这条链作为唯一标准联调入口维护: + +```text +Bootstrap Demo +-> 一键补齐 Runtime 并发布 +-> launch / play / result / history 回归 +``` + +要求: + +- 从空白环境直接可跑 +- 不依赖手工预铺 6~8 个对象 +- 同一条链可反复执行 +- 失败时能明确知道卡在哪一跳 + +### 0.2 详细日志口径 + +workbench 和相关 backend 调试输出,当前应至少统一包含: + +- 当前步骤名 +- 核心输入参数 +- 真实错误信息 +- stack +- 最后一次 curl +- 预期判定 + +不要只输出“失败了”,要能回答: + +- 是哪一步失败 +- 为什么失败 +- 用什么请求复现 + +### 0.3 稳定测试数据 + +当前 demo 数据不要继续散落手工维护,统一以 backend 准备的一键测试数据为准。 + +后续逐步支持以下更接近生产的真实输入: + +- 地图资源 URL +- KML / 赛道文件 +- 内容 manifest +- presentation schema +- 活动文案样例 + +### 0.4 当前不建议做 + +联调标准化阶段不要继续发散去做: + +- 新对象扩张 +- 新管理面板 +- 更复杂 workbench UI +- 复杂后台运营功能 +- 与当前联调闭环无关的页面能力 + 当前不建议 backend 继续发散去做: - 更多新对象