完善一键回归与真实输入准备
This commit is contained in:
71
b2f.md
71
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
|
||||
|
||||
69
b2t.md
69
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
|
||||
|
||||
@@ -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`
|
||||
|
||||
|
||||
|
||||
@@ -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. 当前开发约定
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
19
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 判定或测试环境回收逻辑
|
||||
- 状态:待确认
|
||||
|
||||
---
|
||||
|
||||
|
||||
20
f2t.md
20
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 一键测试环境下的联调回归与小范围修复阶段
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
65
miniprogram/utils/backendPlayCopy.ts
Normal file
65
miniprogram/utils/backendPlayCopy.ts
Normal 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 '可启动'
|
||||
}
|
||||
@@ -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
80
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 继续发散去做:
|
||||
|
||||
- 更多新对象
|
||||
|
||||
Reference in New Issue
Block a user