Merge master into user play stats branch
Some checks failed
CI / verify (pull_request) Has been cancelled
Some checks failed
CI / verify (pull_request) Has been cancelled
This commit is contained in:
@@ -210,3 +210,23 @@ npm.cmd run build
|
||||
- 没有人手逐个点击整局游戏所有 function 的视觉回放,本轮重点是自动化测试、服务端测试、内容校验与 smoke 门禁。
|
||||
|
||||
因此,本审计可以说明“当前 function 系统的自动化测试层状况”,但不等于“所有视觉演出与在线模型联动都已人工验证完毕”。
|
||||
|
||||
## 8. 执行回填(2026-04-28,修复聊天任务领取入口)
|
||||
|
||||
- 问题现象:
|
||||
- NPC 聊天中的待领取任务,点击“查看任务”进入详情后,再点“领取任务”没有把面板切到正式已接任务状态,表现上像“无法领取”。
|
||||
- 根因:
|
||||
- `npcChatQuestOfferUi.acceptPendingOffer()` 会异步把 `npc_quest_accept` 发到服务端。
|
||||
- 但 [`src/components/rpg-runtime-panels/RpgAdventurePanel.tsx`](../../src/components/rpg-runtime-panels/RpgAdventurePanel.tsx) 里“待领取任务详情弹层”的 `onAcceptPendingNpcQuestOffer` 只返回了 `questId`,没有把它写入共享的 `pendingAcceptedQuestId`。
|
||||
- 结果是本来负责等待 quest 真正进入 `quests` 后再统一收口面板状态的 `useEffect` 根本不会触发。
|
||||
- 修复:
|
||||
- 在 `onAcceptPendingNpcQuestOffer` 中补写 `setPendingAcceptedQuestId(acceptedQuestId)`,让待领取任务详情弹层复用普通 `npc_quest_accept` 已有的异步收口链。
|
||||
- 新增 [`src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx`](../../src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx),覆盖“查看任务 -> 领取任务 -> 服务端异步写回 quest log -> 面板切到正式任务状态”的回归路径。
|
||||
- 本次回归验证:
|
||||
- `src/components/rpg-runtime-panels/RpgAdventurePanel.questOffer.test.tsx`
|
||||
- `src/components/rpg-runtime-panels/RpgAdventurePanel.test.tsx`
|
||||
- `src/hooks/rpg-runtime-story/npcEncounterActions.test.ts`
|
||||
- `src/hooks/rpg-runtime-story/choiceActions.test.ts`
|
||||
- `src/hooks/rpg-runtime-story/runtimeStoryCoordinator.test.ts`
|
||||
- `src/hooks/rpg-runtime-story/sessionActions.test.ts`
|
||||
- 共 `57` 条测试通过。
|
||||
|
||||
48
docs/audits/RPG_REVIVE_CONTINUE_FLOW_FIX_2026-04-28.md
Normal file
48
docs/audits/RPG_REVIVE_CONTINUE_FLOW_FIX_2026-04-28.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# RPG 复活后继续冒险链路修复记录 2026-04-28
|
||||
|
||||
## 问题现象
|
||||
|
||||
RPG 运行态里,角色在战斗死亡并复活后,面板会显示一个“继续前进”入口。
|
||||
|
||||
此前这一步只有 `story_continue_adventure` 控制项,没有同步挂出复活后首场景应该展示的 `deferredOptions`。因此玩家点击继续后,系统会把它当作一次普通剧情续推入口,而不是单纯展示“复活后的下一批可选动作”。
|
||||
|
||||
在带有场景章节主 NPC 的自定义世界里,这会让玩家看起来像是“刚复活就直接和对面主 NPC 聊天”,造成复活后第一拍体验被主 NPC 对话链抢走。
|
||||
|
||||
## 根因结论
|
||||
|
||||
根因不在 NPC 聊天函数本身,而在死亡复活链没有沿用 `story_continue_adventure -> deferredOptions` 的延迟展示协议。
|
||||
|
||||
- 战后胜利链已经使用 `story_continue_adventure + deferredOptions`
|
||||
- 死亡复活链此前只保留了 `story_continue_adventure`
|
||||
- 结果是“继续前进”点击后无法走纯展示分支,只能落回普通续推
|
||||
|
||||
## 本次修复
|
||||
|
||||
本次在 `src/hooks/rpg-runtime-story/postBattleFlow.ts` 与复活调用链中补齐:
|
||||
|
||||
- `buildDeathStory(...)` 现在支持在复活文案上同步挂出 `deferredOptions`
|
||||
- 这些 `deferredOptions` 复用 `buildFallbackStoryForState(...)` 产出的复活后可用入口
|
||||
- 点击“继续前进”时只揭示这些入口,不再额外触发一次普通剧情推演
|
||||
|
||||
## 当前行为规则
|
||||
|
||||
角色死亡复活后:
|
||||
|
||||
1. 先显示“你在战斗中倒下,随后重新醒来”
|
||||
2. 面板只展示一个 `story_continue_adventure`
|
||||
3. 点击后展示复活后首场景已有的后续动作
|
||||
4. 不应直接自动推进到主 NPC 聊天执行态
|
||||
|
||||
## 回归覆盖
|
||||
|
||||
已补两条测试:
|
||||
|
||||
- `src/hooks/rpg-runtime-story/choiceActions.test.ts`
|
||||
- 覆盖本地战斗失败后的复活链
|
||||
- `src/hooks/rpg-runtime-story/storyChoiceRuntime.test.ts`
|
||||
- 覆盖服务端战斗失败后的复活链
|
||||
|
||||
两条测试都要求复活文案返回:
|
||||
|
||||
- `story_continue_adventure`
|
||||
- 非空 `deferredOptions`
|
||||
65
docs/experience/BIG_FISH_WORKS_JSON_COMPAT_FIX_2026-04-28.md
Normal file
65
docs/experience/BIG_FISH_WORKS_JSON_COMPAT_FIX_2026-04-28.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# 大鱼作品列表 `items_json` 兼容修复 2026-04-28
|
||||
|
||||
## 背景
|
||||
|
||||
大鱼吃小鱼作品列表在 `server-rs` 链路里由 SpacetimeDB procedure 返回 `items_json`,再由 `spacetime-client` 反序列化成 `BigFishWorkSummaryRecord`。
|
||||
|
||||
本轮出现的线上报错为:
|
||||
|
||||
```text
|
||||
big fish works items_json 非法: missing field `owner_user_id`
|
||||
```
|
||||
|
||||
这说明:
|
||||
|
||||
1. 客户端 record 结构已经把 `owner_user_id` 当成必填字段。
|
||||
2. 某些历史 `items_json` 仍是旧字段集,没有带上 `owner_user_id`。
|
||||
3. 一旦直接按新结构强反序列化,整个 works 列表接口都会失败,而不是只丢失单字段。
|
||||
|
||||
## 根因判断
|
||||
|
||||
这不是前端展示问题,也不是 Axum 路由参数问题,而是:
|
||||
|
||||
1. SpacetimeDB procedure 输出 JSON 的结构发生过升级。
|
||||
2. `spacetime-client` 映射层没有为旧 JSON 做向后兼容。
|
||||
3. 作品列表是聚合读模型,一条旧记录就可能拖垮整批列表读取。
|
||||
|
||||
## 本次落地口径
|
||||
|
||||
本次只做最小风险修复,不改前端契约,不改现有表结构:
|
||||
|
||||
1. `server-rs/crates/spacetime-client/src/mapper.rs`
|
||||
- 大鱼 works 反序列化改为先读兼容结构。
|
||||
- `owner_user_id` 改为兼容层里的可缺省字段。
|
||||
|
||||
2. 私有 works 列表
|
||||
- 若旧 JSON 缺 `owner_user_id`,用当前查询的 `owner_user_id` 回填。
|
||||
- 这样不会破坏创作中心里依赖 `ownerUserId` 的恢复、归属和 key 逻辑。
|
||||
|
||||
3. 公开 gallery 列表
|
||||
- 若旧 JSON 缺 `owner_user_id`,先回填空串,保证列表接口不再整体失败。
|
||||
- 后续若公开画廊明确需要作者归属真相,再补模块端回填或数据修复。
|
||||
|
||||
4. 新增定向测试
|
||||
- 覆盖“私有 works 旧 JSON 缺字段仍可回填”
|
||||
- 覆盖“公开 works 旧 JSON 缺字段不再报错”
|
||||
|
||||
## 经验结论
|
||||
|
||||
以后只要是 `procedure -> items_json -> client record` 这类链路,都要默认遵守下面两条:
|
||||
|
||||
1. 聚合读模型的 JSON 字段升级不能假设全量历史数据同步完成。
|
||||
2. `spacetime-client` 的映射层必须承担兼容旧 JSON 的责任,不能把结构升级风险直接抛给上层接口。
|
||||
|
||||
尤其是 works / gallery / library 这种平台入口级接口:
|
||||
|
||||
1. 允许单字段降级
|
||||
2. 不允许整批列表因单字段缺失而 500 / 400
|
||||
|
||||
## 后续建议
|
||||
|
||||
如果后面继续演进大鱼 works 字段,推荐优先遵守:
|
||||
|
||||
1. 新增字段优先 `Option` 或兼容层解析。
|
||||
2. 聚合 JSON 升级时同步补回归测试。
|
||||
3. 如果字段已经进入前端关键逻辑,再决定是在模块端回填、客户端兜底,还是补历史数据迁移。
|
||||
@@ -31,3 +31,4 @@
|
||||
- [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。
|
||||
- [AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md](./AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md):记录 Agent 空会话不应进入作品草稿列表的后端判定规则。
|
||||
- [BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md](./BIG_FISH_PUBLISH_FEEDBACK_FIX_2026-04-26.md):记录大鱼吃小鱼发布成功后结果页反馈与作品列表刷新的修复口径。
|
||||
- [BIG_FISH_WORKS_JSON_COMPAT_FIX_2026-04-28.md](./BIG_FISH_WORKS_JSON_COMPAT_FIX_2026-04-28.md):记录大鱼作品列表 `items_json` 字段升级后的向后兼容修复口径,避免旧 JSON 直接打崩 works 接口。
|
||||
|
||||
@@ -16,6 +16,16 @@
|
||||
6. 启动测试运行态
|
||||
7. 后端推进摇杆输入、刷怪、吞噬收编、三合一、屏外清理和胜负裁决
|
||||
|
||||
### 1.1 2026-04-27 公开游玩次数补充
|
||||
|
||||
正式发布的大鱼吃小鱼作品需要记录公开游玩次数,落地口径如下:
|
||||
|
||||
1. `big_fish_creation_session.play_count` 保存该作品被正式启动的次数,默认值为 `0`。
|
||||
2. 只有平台作品详情、作品架等正式入口启动已发布作品时递增;创作结果页内的测试运行不计入。
|
||||
3. 前端作品摘要 contract 暴露 `playCount`,作品架展示与拼图一致使用该后端值。
|
||||
4. 本轮仅记录“进入玩法”次数,不记录大鱼吃小鱼总时长;个人 profile 的 RPG 时长统计仍由 runtime snapshot 负责。
|
||||
5. schema 变更需要同步 `migration.rs` 已纳入的 `big_fish_creation_session` 导入导出结构。
|
||||
|
||||
## 2. 本轮明确不做
|
||||
|
||||
1. 不在本文件内展开正式图片模型链、OSS 真相链和占位兼容层的细节;相关正式出图方案以 `BIG_FISH_FORMAL_IMAGE_GENERATION_2026-04-23.md` 为准。
|
||||
|
||||
@@ -395,6 +395,17 @@ Node 侧入口位于:
|
||||
|
||||
这些都等 `runtime_snapshot / save archive` 主链文档冻结后继续推进。
|
||||
|
||||
## 10.1 2026-04-27 统计写链修正
|
||||
|
||||
`runtime_snapshot / save archive` 主链已接入后,profile projection 的写入语义补充冻结如下:
|
||||
|
||||
1. 正式 RPG 游玩只通过 `PUT /api/runtime/save/snapshot` 刷新 `profile_dashboard_state` 与 `profile_played_world`。
|
||||
2. `runtimeMode = "preview"`、`runtimeMode = "test"` 或 `runtimePersistenceDisabled = true` 的快照不刷新 profile projection。
|
||||
3. 前端发起自动保存与手动保存前,必须先把 `runtimeStats.lastPlayTickAt` 到当前时间的 live 时长同步进 `runtimeStats.playTimeMs`,避免 15 秒内进入又退出时保存 0。
|
||||
4. `profile_played_world` 的一行表示“当前用户玩过这个世界”,不是全站作品热度计数;`playedWorldCount` 读取当前用户的去重世界数。
|
||||
5. `profile_dashboard_state.total_play_time_ms` 通过同一用户同一世界的 `runtimeStats.playTimeMs - last_observed_play_time_ms` 增量累积,后端使用 `saturating_sub` 防止旧快照回退导致负增量。
|
||||
6. 作品卡上的公开热度计数如果需要覆盖 RPG 作品,应另立公开作品统计方案;不能把个人 `profile_played_world` 误当成全站作品 `playCount`。
|
||||
|
||||
## 11. 测试策略
|
||||
|
||||
### 11.1 必跑
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# 密码登录入口历史落地设计
|
||||
|
||||
> 2026-04-25 更新:当前产品策略已调整为“不开放密码注册”。新用户必须通过手机号验证码注册/登录,密码登录只面向已经登录后设置过密码的手机号账号。`POST /api/auth/entry` 只接受 `phone + password`,不支持邮箱、用户名或叙世号登录,也不承担自动建号能力。本文原有“密码自动建号”内容仅作为历史背景保留,当前落地以本更新和 [PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md](./PASSWORD_LOGIN_CHANGE_RESET_DESIGN_2026-04-24.md) 为准。
|
||||
>
|
||||
> 2026-04-28 更新:为开发期本地/测试服联调新增服务端环境变量 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED`,默认 `false`。仅当该变量显式为 `true` 时,`POST /api/auth/entry` 可对未知手机号用本次密码直接创建账号并登录;默认关闭时仍严格保持未知手机号返回 `401` 的生产语义。该开关不得用于生产环境,也不新增任何前端规则说明文案。
|
||||
|
||||
日期:`2026-04-21`
|
||||
|
||||
@@ -166,6 +168,13 @@
|
||||
2. 不创建账号。
|
||||
3. 不写 `password_hash`。
|
||||
|
||||
开发期例外:
|
||||
|
||||
1. 当 `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED=true` 时,未知手机号会创建手机号账号。
|
||||
2. 新账号立即写入本次密码的 `password_hash`,并将 `password_login_enabled` 置为 `true`。
|
||||
3. 成功响应沿用密码登录响应体,`created` 只保留在领域结果中,不额外暴露到当前 HTTP contract。
|
||||
4. 手机号格式和密码长度校验仍完全沿用正式密码入口规则。
|
||||
|
||||
### 8.2 未设置密码
|
||||
|
||||
当账号存在但 `password_login_enabled = false` 时:
|
||||
@@ -233,6 +242,8 @@
|
||||
4. 邮箱、用户名或叙世号作为密码登录标识返回 `400`。
|
||||
5. 登录成功时返回 access token。
|
||||
6. 登录成功时写回 refresh cookie。
|
||||
7. `GENARRATIVE_DEV_PASSWORD_ENTRY_AUTO_REGISTER_ENABLED` 默认关闭时行为不变。
|
||||
8. 开关开启时,未知手机号可通过 `/api/auth/entry` 创建账号并登录;同手机号后续用相同密码登录复用同一用户,错误密码仍返回 `401`。
|
||||
|
||||
## 13. 完成定义
|
||||
|
||||
|
||||
131
docs/technical/PROFILE_REDEEM_CODE_IMPLEMENTATION_2026-04-28.md
Normal file
131
docs/technical/PROFILE_REDEEM_CODE_IMPLEMENTATION_2026-04-28.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 资料兑换码模块落地设计
|
||||
|
||||
## 1. 目标
|
||||
|
||||
本轮在现有“我的”资料与钱包 projection 上新增兑换码能力。用户兑换成功后直接增加叙世币余额,写入 `profile_wallet_ledger`,并同步刷新 `profile_dashboard_state.wallet_balance`。
|
||||
|
||||
管理侧本轮只提供后端 API,不新增管理后台页面。私有兑换码创建时支持内部 `userId` 与公开叙世号两类输入,后端创建阶段统一解析成内部 `userId` 存储。
|
||||
|
||||
## 2. 兑换码类型
|
||||
|
||||
`RuntimeProfileRedeemCodeMode` 固定为三种:
|
||||
|
||||
| 类型 | 规则 |
|
||||
| --- | --- |
|
||||
| `Public` | 任意用户可兑换,`max_uses` 按用户独立计算。 |
|
||||
| `Unique` | 任意用户可兑换,`max_uses` 全局共用。 |
|
||||
| `Private` | 仅 `allowed_user_ids` 中的用户可兑换,`max_uses` 全局共用。 |
|
||||
|
||||
兑换码入库前必须 `trim + uppercase`。空兑换码、奖励为 0、次数为 0 均拒绝。
|
||||
|
||||
## 3. 表结构
|
||||
|
||||
### 3.1 `profile_redeem_code`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `code` | `String` | 主键,标准化后的兑换码。 |
|
||||
| `mode` | `RuntimeProfileRedeemCodeMode` | 兑换码模式。 |
|
||||
| `reward_points` | `u64` | 单次到账叙世币。 |
|
||||
| `max_uses` | `u32` | 公共码为单用户上限,唯一码/私有码为全局上限。 |
|
||||
| `global_used_count` | `u32` | 全局已使用次数。公共码也记录总使用次数,但不参与公共码上限判断。 |
|
||||
| `enabled` | `bool` | 是否启用。 |
|
||||
| `allowed_user_ids` | `Vec<String>` | 私有码允许用户;公共/唯一码存空数组。 |
|
||||
| `created_by` | `String` | 管理员用户 ID。 |
|
||||
| `created_at` | `Timestamp` | 创建时间。 |
|
||||
| `updated_at` | `Timestamp` | 更新时间。 |
|
||||
|
||||
### 3.2 `profile_redeem_code_usage`
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `usage_id` | `String` | 主键,格式 `redeem:{code}:{user_id}:{micros}:{sequence}`。 |
|
||||
| `code` | `String` | 兑换码。 |
|
||||
| `user_id` | `String` | 兑换用户。 |
|
||||
| `amount_granted` | `u64` | 到账叙世币。 |
|
||||
| `created_at` | `Timestamp` | 兑换时间。 |
|
||||
|
||||
索引:`code`、`user_id`、`(code, user_id)`。
|
||||
|
||||
## 4. SpacetimeDB 过程
|
||||
|
||||
### 4.1 用户兑换
|
||||
|
||||
`redeem_profile_reward_code(input: RuntimeProfileRewardCodeRedeemInput) -> RuntimeProfileRewardCodeRedeemProcedureResult`
|
||||
|
||||
流程:
|
||||
|
||||
1. 标准化 code。
|
||||
2. 校验兑换码存在、启用、奖励大于 0。
|
||||
3. 按模式校验使用范围与次数。
|
||||
4. 同一事务内写入 `profile_redeem_code_usage`、增加钱包余额、写入 `profile_wallet_ledger`,最后更新 `profile_redeem_code.global_used_count`。
|
||||
5. 返回 `walletBalance`、`amountGranted` 与本次 `ledgerEntry`。
|
||||
|
||||
### 4.2 管理创建/更新
|
||||
|
||||
`admin_upsert_profile_redeem_code(input: RuntimeProfileRedeemCodeAdminUpsertInput) -> RuntimeProfileRedeemCodeAdminProcedureResult`
|
||||
|
||||
私有码必须至少解析出一个内部用户 ID。公共码与唯一码忽略 allowed 列表并存空数组。
|
||||
|
||||
### 4.3 管理停用
|
||||
|
||||
`admin_disable_profile_redeem_code(input: RuntimeProfileRedeemCodeAdminDisableInput) -> RuntimeProfileRedeemCodeAdminProcedureResult`
|
||||
|
||||
只更新 `enabled=false` 与 `updated_at`,不存在时返回“兑换码不存在”。
|
||||
|
||||
## 5. Axum API
|
||||
|
||||
用户接口:
|
||||
|
||||
- `POST /api/profile/redeem-codes/redeem`
|
||||
- `POST /api/runtime/profile/redeem-codes/redeem`
|
||||
|
||||
请求:`{ "code": "WELCOME2026" }`
|
||||
|
||||
成功返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"walletBalance": 130,
|
||||
"amountGranted": 100,
|
||||
"ledgerEntry": {
|
||||
"id": "redeem:WELCOME2026:user:1777392000000000:0",
|
||||
"amountDelta": 100,
|
||||
"balanceAfter": 130,
|
||||
"sourceType": "redeem_code_reward",
|
||||
"createdAt": "2026-04-28T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
管理员接口:
|
||||
|
||||
- `POST /admin/api/profile/redeem-codes`
|
||||
- `POST /admin/api/profile/redeem-codes/disable`
|
||||
|
||||
管理员接口复用现有 `require_admin_auth`。
|
||||
|
||||
## 6. 错误文案
|
||||
|
||||
| 场景 | message |
|
||||
| --- | --- |
|
||||
| 空 code | `兑换码不能为空` |
|
||||
| 不存在 | `兑换码不存在` |
|
||||
| 停用 | `兑换码已停用` |
|
||||
| 奖励为 0 | `兑换码奖励无效` |
|
||||
| 次数耗尽 | `兑换次数已用完` |
|
||||
| 私有码账号不匹配 | `该兑换码不适用于当前账号` |
|
||||
| 私有码无允许用户 | `私有兑换码必须指定可兑换用户` |
|
||||
|
||||
## 7. 前端交互
|
||||
|
||||
“我的”页头像右侧入口由 `会员充值` 改为 `兑换码`。点击打开独立模态窗口,窗口内只保留输入框、兑换按钮和后端返回提示,不展示兑换规则说明。
|
||||
|
||||
成功后展示 `已到账 X 叙世币`,并刷新 profile dashboard。失败后直接展示后端 `message`。
|
||||
|
||||
## 8. 测试矩阵
|
||||
|
||||
- Rust/module-runtime:覆盖公共码、唯一码、私有码、失败场景、流水来源和余额累加。
|
||||
- Axum:覆盖用户鉴权、管理员鉴权、runtime error 到 400 的映射和兼容路径。
|
||||
- 前端:覆盖入口替换、独立 modal、成功刷新余额和失败展示后端 message。
|
||||
- 验证命令:`cargo test`、目标前端测试、`npm run api-server:maincloud`、`npm run check:encoding`。
|
||||
@@ -0,0 +1,58 @@
|
||||
# 拼图结果页自动保存与标签发布门槛修复
|
||||
|
||||
## 背景
|
||||
|
||||
拼图结果页此前存在两个串联问题:
|
||||
|
||||
1. 创作者在结果页修改 `关卡名`、新增标签、删除标签,只会改前端本地 `editState`,不会立即写回拼图作品 profile。
|
||||
2. 发布弹窗同时混用了旧 session 内的 `publishReady` 与前端本地编辑态,导致标签已经在界面里补够,但发布校验仍然盯着旧草稿里的标签数量,用户无法通过发布检验。
|
||||
|
||||
这会直接破坏拼图创作主链的可用性:用户明明已经在结果页补齐正式标签,却因为没有自动保存、也没有按当前编辑态重算门槛而卡在发布前。
|
||||
|
||||
## 修复目标
|
||||
|
||||
1. 拼图结果页中的 `关卡名`、`添加标签`、`删除标签` 统一接入自动保存。
|
||||
2. 自动保存复用现有 `PUT /api/runtime/puzzle/works/:profileId`,不新增新系统。
|
||||
3. 前端发布门槛与后端 `module-puzzle` 规则显式对齐,统一采用 `3~6` 个正式标签。
|
||||
4. 发布弹窗要基于“当前可编辑态”判断是否通过,不再被旧 session 中可由本地编辑修复的 blocker 卡死。
|
||||
|
||||
## 实现口径
|
||||
|
||||
### 1. 结果页自动保存
|
||||
|
||||
- `src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`
|
||||
- 为拼图结果页显式透传稳定 `profileId`。
|
||||
- 草稿结果页默认按 `puzzle-session-* -> puzzle-profile-*` 规则推导 profileId;已发布作品优先复用 `publishedProfileId`。
|
||||
|
||||
- `src/components/puzzle-result/PuzzleResultView.tsx`
|
||||
- 当 `levelName / summary / themeTags` 相对当前 `draft` 发生变化时,触发防抖自动保存。
|
||||
- 自动保存复用 `updatePuzzleWork(...)`,同步写回:
|
||||
- `levelName`
|
||||
- `summary`
|
||||
- `themeTags`
|
||||
- 当前正式图 `coverImageSrc / coverAssetId`
|
||||
- 顶部只展示轻量保存状态角标:`保存中 / 已自动保存 / 保存失败`。
|
||||
|
||||
### 2. 发布门槛统一
|
||||
|
||||
- 前端结果页发布判定不再使用“至少 1 个标签”的旧口径。
|
||||
- 统一改为和后端 `module-puzzle` 一致的规则:正式标签数量必须在 `3 到 6` 之间。
|
||||
|
||||
### 3. 结果页 blocker 重算策略
|
||||
|
||||
`session.resultPreview.blockers` 中与可编辑字段直接相关的 blocker:
|
||||
|
||||
- `MISSING_LEVEL_NAME`
|
||||
- `INVALID_TAG_COUNT`
|
||||
- `MISSING_COVER_IMAGE`
|
||||
|
||||
这些项不再原样阻断前端发布按钮,而是改由结果页基于当前 `editState + formalImageSrc` 重新计算。
|
||||
其余后端 blocker 仍继续保留,避免前端绕过真正不可编辑的发布门禁。
|
||||
|
||||
## 验收点
|
||||
|
||||
1. 修改拼图关卡名后,不点发布也会自动写回作品 profile。
|
||||
2. 添加标签、删除标签后,不点发布也会自动写回作品 profile。
|
||||
3. 标签少于 `3` 个时,发布弹窗明确提示“正式标签数量必须在 3 到 6 之间”。
|
||||
4. 标签补到 `3~6` 个后,无需刷新页面即可通过前端发布校验。
|
||||
5. 结果页顶部能看到轻量自动保存状态,不额外堆叠说明文案。
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
- [PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md](./PUZZLE_RESULT_AUTOSAVE_AND_TAG_GATE_FIX_2026-04-28.md):记录拼图结果页名称与标签编辑自动保存、发布门槛统一到 `3~6` 标签,以及前端发布校验不再被旧 session blocker 卡死的修复口径。
|
||||
- [SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md](./SPACETIMEDB_START_SH_EARLY_EXIT_DIAGNOSTICS_2026-04-27.md):记录发布包 `start.sh` 只输出“SpacetimeDB 进程在就绪前退出”时的诊断补强,启动失败或超时时自动回显 `logs/spacetimedb.log`、`server ping`、端口监听和 root-dir 相关进程。
|
||||
- [RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md](./RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md):记录 RPG 运行时 NPC 聊天、RPG/自定义世界 Agent 与大鱼 Agent 从“拼完整 SSE 字符串后一次性返回”改为 `mpsc + Sse<Event>` 真流式输出的后端落地口径。
|
||||
- [SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md](./SPACETIMEDB_START_SH_ROOT_OWNER_FALSE_POSITIVE_FIX_2026-04-27.md):记录发布包 `start.sh` root-dir 占用检测把 `grep -F .../.spacetimedb` 误判为 SpacetimeDB 实例的根因、脚本修复和现场处理方式。
|
||||
|
||||
@@ -96,6 +96,9 @@
|
||||
4. 前端 `storyChoiceContinuation` / `useRpgRuntimeNpcInteraction`:
|
||||
- `fight_defeat` 不能再被当成“本地 NPC 战斗胜利”进入战后收束
|
||||
- 玩家死亡后必须直接走死亡复活链,且复活时重置到开局场景第一幕,不能先推进下一幕
|
||||
5. 前端 `postBattleFlow`:
|
||||
- 复活回到开局场景时,必须重新走首幕 encounter preview 恢复链
|
||||
- 第一幕主交互 NPC 与同幕陪衬 NPC 要继续沿用既有场景槽位,不能退化成全部站成一排
|
||||
|
||||
## 继续收口(2026-04-28)
|
||||
|
||||
@@ -118,6 +121,24 @@
|
||||
- 非 NPC 通用敌对战斗 `!inBattle`
|
||||
5. 这样可以保证作品测试、幕预览与正式游戏在“死亡后回开局第一幕”这一口径上继续对齐
|
||||
|
||||
## 继续收口:复活后首幕 NPC 与站位恢复(2026-04-28)
|
||||
|
||||
在继续复测后,又确认死亡复活链还有一层表现问题:
|
||||
|
||||
1. 角色虽然已经回到开局场景第一幕;
|
||||
2. 但复活态旧实现只是重置 `currentSceneActState`,没有重新恢复第一幕 encounter preview;
|
||||
3. 于是画布只能把第一幕 NPC 都按普通 ambient 角色绘制;
|
||||
4. 视觉上就会表现为:
|
||||
- 主交互 NPC 没有按首幕重新成为前景目标
|
||||
- 同幕 NPC 失去原本的前后排关系
|
||||
- 最终看起来像“所有人站成一排”
|
||||
|
||||
本轮补充修正如下:
|
||||
|
||||
1. `buildRevivedFirstSceneState(...)` 在重置到首幕之后,立即复用 `ensureSceneEncounterPreview(...)`
|
||||
2. 这样复活链与“开局进入世界 / 场景正常进场”继续共用同一套首幕恢复逻辑
|
||||
3. 第一幕主交互 NPC、同幕陪衬 NPC 与既有槽位会一起恢复,不再额外发明一套复活专用站位规则
|
||||
|
||||
## 结论
|
||||
|
||||
本次修复后,RPG 战斗 compat 主链的胜负判定口径变为:
|
||||
|
||||
@@ -200,3 +200,47 @@
|
||||
|
||||
1. 自定义世界角色型敌人在战斗态不会再重复叠加场景立绘下沉偏移;
|
||||
2. 相关战斗编队、runtime gateway 与 battle plan 既有回归继续通过。
|
||||
|
||||
## 9. 本轮继续修正:战斗结束后和平态站位被战斗坐标污染
|
||||
|
||||
在前面解决“开战瞬间跳位”后,用户继续反馈战斗结束时敌对角色站位仍会被改掉。继续顺着 NPC 战斗收尾链核对后,确认这次的问题不在画布层,而在“战后恢复使用了哪一份 encounter 真相”。
|
||||
|
||||
### 9.1 根因梳理
|
||||
|
||||
此前 `fight_victory` 收尾时,恢复 `currentEncounter` 的优先级仍可能落到:
|
||||
|
||||
1. 战斗中的 `currentEncounter`
|
||||
2. `activeBattleHostiles[0]?.encounter`
|
||||
|
||||
这两份 encounter 都已经是战斗态里被压到前排中心位后的数据,`xMeters` 往往已经变成 `3.2`。因此即使战前和平态 NPC 原本站在更靠后的场景位置,战斗结束后也会被错误恢复到战斗中心位,表现为“打完架后角色站位被改掉”。
|
||||
|
||||
### 9.2 本次收口
|
||||
|
||||
这次修正把“战前原始 encounter 保存”和“战后 encounter 恢复”两端一起收口:
|
||||
|
||||
1. `sceneEncounterPreviews.ts`
|
||||
- NPC 自动开战时立即保存战前原始 encounter
|
||||
- 复用现有 `sparReturnEncounter` 存槽,避免新增一套并行状态
|
||||
2. `rpgRuntimeStoryGateway.ts`
|
||||
- 若服务端战斗快照未带回 `sparReturnEncounter`
|
||||
- 网关自动沿用进入战斗前的原始 NPC encounter 回填
|
||||
3. `useRpgRuntimeNpcInteraction.ts`
|
||||
- `fight_victory` 恢复和平态时,优先使用保存下来的战前 encounter
|
||||
- 只在缺失时才退回到 battle encounter / fallback encounter
|
||||
|
||||
### 9.3 效果
|
||||
|
||||
这样处理后:
|
||||
|
||||
1. 战斗结束后恢复到场景中的 NPC,会回到战前那份 encounter 对应的位置;
|
||||
2. 不会再把战斗前排中心位误带回和平态;
|
||||
3. `fight` 与 `spar` 两条 NPC 战斗收尾链恢复口径保持一致;
|
||||
4. 作品测试、幕预览与正式运行的战后站位表现继续对齐。
|
||||
|
||||
### 9.4 验证
|
||||
|
||||
本轮新增并通过了以下回归验证:
|
||||
|
||||
1. 负好感 NPC 自动开战后会保存战前 encounter;
|
||||
2. `npc_fight` 服务端空战场快照桥接后会保留战前 encounter;
|
||||
3. `fight_victory` 收尾时会恢复战前 encounter,而不是战斗态 encounter。
|
||||
|
||||
@@ -23,7 +23,7 @@ spacetime sql <db> "SELECT * FROM custom_world_gallery_entry"
|
||||
| 领域 | 表 |
|
||||
| --- | --- |
|
||||
| 认证 | `auth_store_snapshot`, `user_account`, `auth_identity`, `refresh_session` |
|
||||
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `profile_played_world`, `profile_save_archive` |
|
||||
| 运行时档案 | `runtime_setting`, `runtime_snapshot`, `user_browse_history`, `profile_dashboard_state`, `profile_wallet_ledger`, `profile_redeem_code`, `profile_redeem_code_usage`, `profile_played_world`, `profile_save_archive` |
|
||||
| RPG 运行时 | `story_session`, `story_event`, `npc_state`, `inventory_slot`, `battle_state`, `treasure_record`, `quest_record`, `quest_log`, `player_progression`, `chapter_progression` |
|
||||
| 世界创作 | `custom_world_profile`, `custom_world_session`, `custom_world_agent_session`, `custom_world_agent_message`, `custom_world_agent_operation`, `custom_world_draft_card`, `custom_world_gallery_entry` |
|
||||
| 拼图 | `puzzle_agent_session`, `puzzle_agent_message`, `puzzle_work_profile`, `puzzle_runtime_run` |
|
||||
@@ -133,6 +133,27 @@ SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>';
|
||||
SELECT * FROM profile_wallet_ledger WHERE user_id = '<user_id>' ORDER BY created_at DESC;
|
||||
```
|
||||
|
||||
### `profile_redeem_code`
|
||||
|
||||
- 作用:运营发放的叙世币兑换码,支持公共码、唯一码和私有码。
|
||||
- 结构:`code PK: String`, `mode: RuntimeProfileRedeemCodeMode`, `reward_points: u64`, `max_uses: u32`, `global_used_count: u32`, `enabled: bool`, `allowed_user_ids: Vec<String>`, `created_by: String`, `created_at: Timestamp`, `updated_at: Timestamp`。
|
||||
- 索引:主键 `code`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_redeem_code WHERE code = '<CODE>';
|
||||
```
|
||||
|
||||
### `profile_redeem_code_usage`
|
||||
|
||||
- 作用:记录每一次兑换行为,为公共码用户维度计次、唯一/私有码全局计次提供依据。
|
||||
- 结构:`usage_id PK: String`, `code: String`, `user_id: String`, `amount_granted: u64`, `created_at: Timestamp`。
|
||||
- 索引:`code`, `user_id`, `(code, user_id)`。
|
||||
|
||||
```sql
|
||||
SELECT * FROM profile_redeem_code_usage WHERE code = '<CODE>';
|
||||
SELECT * FROM profile_redeem_code_usage WHERE user_id = '<user_id>';
|
||||
```
|
||||
|
||||
### `profile_played_world`
|
||||
|
||||
- 作用:记录用户玩过的世界及最后游玩时间,用于个人页历史和继续游戏入口。
|
||||
|
||||
Reference in New Issue
Block a user