完善一键回归与真实输入准备

This commit is contained in:
2026-04-03 14:18:11 +08:00
parent 129ea935db
commit 114c524044
14 changed files with 599 additions and 29 deletions

71
b2f.md
View File

@@ -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

69
b2t.md
View File

@@ -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

View File

@@ -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`

View File

@@ -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. 当前开发约定

View File

@@ -734,8 +734,9 @@ const devWorkbenchHTML = `<!doctype html>
<button class="ghost" id="btn-flow-result">Finish + Result</button>
<button class="secondary" id="btn-flow-admin-default-publish">一键默认绑定发布</button>
<button class="secondary" id="btn-flow-admin-runtime-publish">一键补齐 Runtime 并发布</button>
<button class="secondary" id="btn-flow-standard-regression">一键标准回归</button>
</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再继续发布链。</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="muted-note">预期结果</div>
<div class="kv">
@@ -746,6 +747,18 @@ const devWorkbenchHTML = `<!doctype html>
<div>判定 <code id="flow-admin-verdict">待执行</code></div>
</div>
</div>
<div class="subpanel">
<div class="muted-note">回归结果汇总</div>
<div class="kv">
<div>发布链 <code id="flow-regression-publish-result">待执行</code></div>
<div>Play <code id="flow-regression-play-result">待执行</code></div>
<div>Launch <code id="flow-regression-launch-result">待执行</code></div>
<div>Result <code id="flow-regression-result-result">待执行</code></div>
<div>History <code id="flow-regression-history-result">待执行</code></div>
<div>Session ID <code id="flow-regression-session-id">-</code></div>
<div>总判定 <code id="flow-regression-overall">待执行</code></div>
</div>
</div>
</section>
<section class="panel" data-modes="common">
@@ -2217,6 +2230,70 @@ const devWorkbenchHTML = `<!doctype html>
$('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 = `<!doctype html>
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 = `<!doctype html>
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',

View File

@@ -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
}

View File

@@ -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. 活动详情页摘要

19
f2b.md
View File

@@ -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 判定或测试环境回收逻辑
- 状态:待确认
---

20
f2t.md
View File

@@ -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 一键测试环境下的联调回归与小范围修复阶段

View File

@@ -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),

View File

@@ -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),

View File

@@ -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 '可启动'
}

View File

@@ -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 日志能明确定位失败步骤
- 同一条测试链可重复执行
- 前端线程建议正式上场时机:
- 现在已完成活动运营域摘要接线第一刀
- 当前已完成:

80
t2b.md
View File

@@ -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 继续发散去做:
- 更多新对象