124 lines
5.0 KiB
Markdown
124 lines
5.0 KiB
Markdown
# 拼图运行时真实排行榜落地说明
|
||
|
||
更新时间:`2026-04-27`
|
||
|
||
## 1. 背景
|
||
|
||
当前拼图关卡结束弹窗里的排行榜数据并不是真实用户成绩。
|
||
|
||
问题根因有两层:
|
||
|
||
1. 前端本地运行态 `src/services/puzzle-runtime/puzzleLocalRuntime.ts` 在通关后会直接拼出几条演示昵称数据。
|
||
2. `server-rs` 拼图运行时虽然已经预留了 `leaderboardEntries` 字段,但 `module-puzzle`、`spacetime-client`、`api-server` 还没有真实成绩表与聚合过程,因此接口层长期返回空数组。
|
||
|
||
这导致用户在结算弹窗里看到的是“看起来像真实排行榜,但实际上是本地假数据”的结果,和平台“真实用户数据”要求冲突。
|
||
|
||
## 2. 本次目标
|
||
|
||
本次改动只解决一个明确问题:
|
||
|
||
1. 拼图关卡结束后的排行榜必须使用真实用户成绩。
|
||
2. 删除现有前端演示昵称、演示耗时等假数据。
|
||
3. 不在 UI 中默认塞入任何说明型占位文案。
|
||
|
||
## 3. 本次落地边界
|
||
|
||
为了控制改动范围,本次不把整套拼图运行态全部迁回后端,而是在当前“本地棋盘运行态”基础上补一条真实成绩回写链路:
|
||
|
||
1. 拼图拖拽、交换、合并、拆分、通关判定,仍然沿用当前本地运行态。
|
||
2. 玩家一旦通关,前端立即把当前关卡成绩提交到 `server-rs`。
|
||
3. `server-rs` 将成绩写入 `SpacetimeDB` 成绩表,并返回该关卡的真实排行榜。
|
||
4. 结算弹窗只展示后端返回的真实成绩榜单,不再混入本地演示数据。
|
||
|
||
这意味着:
|
||
|
||
1. 这次不是“完整后端裁决化”。
|
||
2. 这次是“先把排行榜真相源收回后端”,满足真实成绩展示要求。
|
||
|
||
## 4. 成绩真相源设计
|
||
|
||
新增拼图成绩表,按“关卡作品 + 网格规格 + 用户”维护最佳成绩。
|
||
|
||
正式开始拼图关卡时还必须同步用户玩过作品明细:
|
||
|
||
1. 作品自身统计继续更新 `puzzle_work_profile.play_count`。
|
||
2. 已登录用户写入 `profile_played_world`,`world_key = puzzle:{profile_id}`。
|
||
3. `profile_id` 保存拼图作品号,`world_type = PUZZLE`。
|
||
4. `world_title` 使用关卡名,`world_subtitle` 使用作品摘要,`owner_user_id` 使用拼图作者用户 ID。
|
||
5. 下一关切换到新 `profile_id` 时按同一规则再次写入。
|
||
6. 排行榜提交携带的 `elapsedMs` 是本关可观测时长,后端按增量累计到 `profile_dashboard_state.total_play_time_ms`。
|
||
|
||
建议字段:
|
||
|
||
1. `entry_id`
|
||
唯一主键。
|
||
2. `profile_id`
|
||
当前关卡作品 `profile_id`。
|
||
3. `grid_size`
|
||
当前成绩对应的拼图网格规格,至少区分 `3x3` 与 `4x4`。
|
||
4. `user_id`
|
||
成绩所属真实用户 ID。
|
||
5. `nickname`
|
||
成绩展示昵称。当前优先使用提交时的用户显示名快照。
|
||
6. `best_elapsed_ms`
|
||
用户在该关卡该规格下的最佳通关耗时。
|
||
7. `last_run_id`
|
||
最近一次刷新该最佳成绩的运行态 `run_id`。
|
||
8. `updated_at`
|
||
最后一次刷新时间。
|
||
|
||
## 5. 排行榜口径
|
||
|
||
排行榜必须遵守下面规则:
|
||
|
||
1. 只读真实成绩表。
|
||
2. 同一用户在同一 `profile_id + grid_size` 下只保留 1 条最佳成绩。
|
||
3. 排序按 `best_elapsed_ms` 从小到大。
|
||
4. 同耗时按 `updated_at` 更早者优先,再按 `user_id` 稳定排序。
|
||
5. 返回前 `N` 条,当前阶段固定 `10` 条即可。
|
||
6. 当前用户如果在榜单内,需要标记 `isCurrentPlayer = true`。
|
||
|
||
## 6. 接口落地
|
||
|
||
新增拼图排行榜提交接口:
|
||
|
||
`POST /api/runtime/puzzle/runs/:runId/leaderboard`
|
||
|
||
请求体至少包含:
|
||
|
||
1. `profileId`
|
||
2. `gridSize`
|
||
3. `elapsedMs`
|
||
4. `nickname`
|
||
|
||
返回体采用现有 `PuzzleRunResponse`,但要求:
|
||
|
||
1. `run.currentLevel.leaderboardEntries` 返回真实榜单。
|
||
2. `run.leaderboardEntries` 同步返回当前关卡真实榜单,方便现有结算弹窗兼容读取。
|
||
|
||
## 7. 前端改动规则
|
||
|
||
1. 删除 `puzzleLocalRuntime.ts` 中本地演示榜单构造逻辑。
|
||
2. 本地通关后,运行态只保留真实通关耗时,不再生成假昵称榜单。
|
||
3. 结算弹窗显示时,如果真实榜单尚未回写完成,可以显示加载态;但不能回退到假数据。
|
||
4. 下一关开始后,当前关卡榜单状态清空。
|
||
|
||
## 7.1 2026-04-29 与前端拖动裁决的对齐
|
||
|
||
当前拼图拖动、合并、拆分与通关判定完全由前端运行态负责,后端排行榜接口只负责真实成绩表与榜单聚合:
|
||
|
||
1. 排行榜提交不得依赖 SpacetimeDB 里的旧棋盘快照已经通过后端拖动接口进入 `cleared`。
|
||
2. 后端仍校验 `profileId`、`gridSize`、昵称和成绩,并把当前提交写入真实成绩表。
|
||
3. 后端响应里的 `leaderboardEntries` 是唯一需要合并回前端当前 run 的数据。
|
||
4. 前端不能用排行榜响应里的旧棋盘快照覆盖本地拖动后的棋盘,否则会把刚刚通关的前端状态回滚。
|
||
|
||
## 8. 测试要求
|
||
|
||
至少覆盖:
|
||
|
||
1. 通关后不会再生成本地假榜单。
|
||
2. 同一用户重复通关同一关卡时,只保留更优成绩。
|
||
3. 不同用户成绩会按耗时正确排序。
|
||
4. `3x3` 与 `4x4` 不混榜。
|
||
5. 下一关开启后上一关榜单不会污染新关卡。
|