【付费游玩】游戏支持买断制泥点付费,购买后才能游玩,后台审核可见价格并无限制试玩 #634

Closed
opened 2026-10-05 16:25:03 +08:00 by suzmii · 0 comments
Member

背景与问题原因

当前游戏分发只有「已公开即可免费游玩」一条链路,没有任何付费/购买/价格概念:

  • 公开目录、详情、游玩页、发行网关的唯一授权依据是「游戏 visibility=published 且存在 published 活动版本」,不校验登录、所有权或付费(server-rs/crates/api-server/src/modules/game_distribution.rs、SpacetimeDB get_public_game_distribution_game_and_return / list_public_game_distribution_games_and_return)。
  • 游戏表 game_distribution_game、版本表 game_distribution_version 都没有价格字段;server-rs/crates/shared-contracts/src/game_distribution.rs 与 packages/shared/src/contracts/gameDistribution.ts 的游戏 DTO 里也没有 price / purchase。
  • 泥点钱包在 runtime profile 域:余额 profile_dashboard_state.wallet_balance,流水 profile_wallet_ledger(source_type: RuntimeProfileWalletLedgerSourceType),唯一暴露的消费入口是 consume_profile_wallet_points_and_return(server-rs/crates/spacetime-module/src/runtime/active/profile.rs)。游戏分发与钱包没有任何关联。
  • 后台审核详情页 apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx 展示资料、包摘要和待审版本预览试玩,但没有价格。
  • 主规范 docs/【玩法创作】平台入口与玩法链路-2026-05-15.md 明确把「交易与创作者收入」列为首版不做,本次需求要先解除该排除并补行为合同。

需求:作者上传游戏时可选买断制付费;其他用户需花泥点购买后才能游玩;后台审核时可见该泥点数量,并且审核员可以无限制游玩。

现状事实(已核对)

  • 游玩链路:/games/detail → GameDetailPage「立即玩」→ /games/play?id= → GamePlayPage.startGame 挂载 sandbox="allow-scripts" 的 iframe,src = currentVersion.entryUrl(服务端派生的同源 /games/<gameId>/),发行网关(serve_release_entry / serve_release_asset)公开、拒绝任意 Cookie、扩展名白名单、Cache-Control: public,max-age=60。
  • 审核链路:/admin/api/game-distribution/reviews 待审列表 → 版本详情 → POST /versions/{id}/review(approve/reject,带 expectedPublicationRevision CAS + Idempotency-Key);审核员试玩走 POST /admin/api/game-distribution/versions/{id}/preview-session,签发 10 分钟、绑定 versionId、仅 pending_review/rejected 可用的 /api/game-distribution/admin-previews/{token}/,资源按 token 前缀解析,不限次数、不扣泥点——这条已满足「审核员无限制游玩」,本次只需保证不破坏它并在详情显示价格。
  • 冻结资料先例:版本 metadata_json 冻结作者资料(封面/截图等),审核通过时整体生效到 game 行(cover_object_key / screenshots_json)。价格应沿用同一冻结-生效模式。
  • 钱包先例:LLM 结算(commit 736a1b6ac)曾给 RuntimeProfileWalletLedgerSourceType 追加 LlmRouterConsume 变体,同步 TS 联合类型、前端文案与生成绑定,未改 profile_wallet_ledger 表结构——game_purchase 可沿用同一做法。

方案(已与用户确认)

1. 定价与数据模型

  • game_distribution_game 末尾追加 price_mud_points: u64(#[default(0)],0 = 免费)。价格与封面/截图一样来自版本冻结资料,审核通过时在 approve 事务内写入 game 行。
  • 版本冻结资料 metadata_json 增加 priceMudPoints;作者创建/更新版本时可提交,api-server 校验范围(例如 0..=1_000_000 整数)。
  • 新增持久表 game_distribution_purchase:purchase_id(PK)、game_id、user_id、price_mud_points(成交价快照)、wallet_ledger_id(泥点流水 ID,便于对账)、created_at;(user_id, game_id) 唯一(买断制一人一条),加入 migration.rs 白名单、表目录与生成绑定。
  • 不新增 entitlement 表:买断制下 purchase 行即所有权事实源;「永久可玩」由 purchase 行存在决定。

2. 泥点流水

  • RuntimeProfileWalletLedgerSourceType 追加 GamePurchase => "game_purchase";同步 packages/shared/src/contracts/runtime.ts、shared-contracts、前端 platformProfileFundsModel.ts 文案(如「购买游戏」)、后台相关映射与生成绑定。
  • 扣费复用钱包原子写路径(余额 + 流水 + consumption total 同事务),metadata_json 记 {kind:"game_purchase", gameId, priceMudPoints}。
  • 余额不足返回明确错误码(如 INSUFFICIENT_MUD_POINTS),不落 purchase、不改余额。

3. 购买接口与并发

  • POST /api/game-distribution/games/{game_id}/purchase(require_bearer_auth,Idempotency-Key):
    • 校验游戏已公开、price > 0、当前版本可服务;免费游戏或已购买时幂等返回既有 purchase(replayed=true),不重复扣费。
    • 扣泥点 → 写 purchase 行,必须做成原子(SpacetimeDB procedure 内跨表事务),避免「扣了没记录」或「记录了没扣」。
  • 并发双击 / 响应丢失重试:唯一约束 + 幂等键双重兜底,只扣一次。

4. 播放鉴权(真付费保护,已确认选 token 会话)

  • 公开详情 DTO 仍返回到价格与购买态,但付费且未购买时不下发可播放入口;GameDetailPage 显示「N 泥点购买」,不显示可点的「立即玩」。
  • 新增 POST /api/game-distribution/games/{game_id}/play-session(付费游戏 require_bearer_auth):校验已购买 / 作者本人 / 管理员,签发绑定 gameId + userId + 过期时间 的短时 token,返回 /api/game-distribution/play-sessions/{token}/。
  • 新增网关 GET /api/game-distribution/play-sessions/{token}[/{*asset}]:沿用现有 admin preview 的实现套路(token 前缀承载相对资源、无登录中间件、拒可解析 Cookie、no-store、token 过期/游戏下架 → 404)。
  • 现有公开 /games/<game_id>/ 网关:当前活动版本 price_mud_points > 0 时返回 404,杜绝「猜 gameId 直连绕过」;免费游戏完全不变。
  • 作者本人与管理员免购买即可游玩自己的 / 任意付费作品,且不计费(审核需要)。
  • 游玩计数只在真正开始游玩后上报(保持现有 record_game_play 口径,付费游戏经由播放会话后上报)。

5. 后台审核

  • 审核详情页新增价格展示:priceMudPoints(0 → 「免费」/「按当前版本免费」,否则「N 泥点」);待审列表可加价格列。
  • 保留并回归「审核员试玩当前待审版本」:不限次数、不校验购买、不扣泥点。
  • 管理员查看已公开付费游戏时也可无限制试玩(走播放会话的管理员分支)。

6. 前端

  • 网页发布页 GamePublishPage.tsx 新增「付费方式」:免费 / 买断制(正整数泥点,上限与后端一致);更新模式同样可改价(走新版本冻结资料)。
  • AGC 发布面板同步加同一选项(可在第二个切片做,避免一次改动过大)。
  • 详情页展示价格与购买态;购买前用 PlatformMudPointConfirmDialog(「确认消耗泥点」)二次确认并展示余额,余额不足引导充值;购买成功后刷新 usePlatformWalletStore 余额。
  • 游玩页:付费游戏先请求 play-session 拿 token URL 再挂 iframe;免费游戏维持现状。
  • 广场卡片可加「泥点」角标(可选)。

契约、文档与迁移

  • SpacetimeDB:game_distribution_game 追加 price_mud_points(表末尾 + 默认值);新增 game_distribution_purchase;同步 migration.rs、后端架构文档表目录、生成绑定,运行 npm run check:spacetime-schema。
  • Rust/TS DTO:priceMudPoints、purchased / ownedByViewer、购买请求/响应、播放会话响应。
  • 主规范:修订 docs/【玩法创作】平台入口与玩法链路-2026-05-15.md,解除「不做交易」排除并新增「买断制付费与播放鉴权」合同章;先按 docs/【协作规范】规范驱动开发工作流-2026-09-12.md 建主规范/里程碑/实施计划再编码。
  • 不涉及 /api/external/v1,无需更新 docs/openapi/genarrative-external-v1.openapi.json。

验收判据

  • 作者网页发布可选「免费」或「买断制 N 泥点」;非法值(负数/超上限/非整数)被前后端同时拦截。
  • 后台审核详情显示该版本价格;审核员可无限次试玩待审版本,不扣泥点。
  • 未购买用户详情页显示「N 泥点购买」,不显示可播放入口;直连 /games/<gameId>/ 返回 404。
  • 余额不足购买失败且余额/账单不变;余额充足购买成功后余额减少 N、账单出现 game_purchase 流水、purchase 行唯一。
  • 重复点击 / 并发购买 / 响应丢失重试只扣一次(幂等 + 唯一约束)。
  • 购买后可反复游玩、不再扣费;换设备/重新登录仍可玩(买断制永久)。
  • 作者本人与管理员对付费作品免购买、无限制游玩。
  • 免费游戏全链路(发布/详情/游玩/网关/计数)无行为变化。
  • npm run check:spacetime-schema、npm run check:encoding、npm run typecheck、定向 Rust/Vitest、git diff --check 通过;真实本地栈 smoke 覆盖发布→审核→购买→游玩闭环。

不在本轮范围

退款/撤销购买、作者分成与结算、促销/限时免费、价格运营审核规则、AGC 发布面板(可作第二切片)、评分/评论与付费的耦合、外部 API。

工作区

分支 feat/game-purchase,worktree .worktrees/feat/game-purchase(基于 origin/master)。按规范驱动开发工作流先落主规范/里程碑/实施计划,再进入实现。

## 背景与问题原因 当前游戏分发只有「已公开即可免费游玩」一条链路,没有任何付费/购买/价格概念: - 公开目录、详情、游玩页、发行网关的唯一授权依据是「游戏 `visibility=published` 且存在 `published` 活动版本」,不校验登录、所有权或付费(`server-rs/crates/api-server/src/modules/game_distribution.rs`、SpacetimeDB `get_public_game_distribution_game_and_return` / `list_public_game_distribution_games_and_return`)。 - 游戏表 `game_distribution_game`、版本表 `game_distribution_version` 都没有价格字段;`server-rs/crates/shared-contracts/src/game_distribution.rs` 与 `packages/shared/src/contracts/gameDistribution.ts` 的游戏 DTO 里也没有 price / purchase。 - 泥点钱包在 runtime profile 域:余额 `profile_dashboard_state.wallet_balance`,流水 `profile_wallet_ledger`(`source_type: RuntimeProfileWalletLedgerSourceType`),唯一暴露的消费入口是 `consume_profile_wallet_points_and_return`(`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`)。游戏分发与钱包没有任何关联。 - 后台审核详情页 `apps/admin-web/src/pages/AdminGameDistributionReviewPage.tsx` 展示资料、包摘要和待审版本预览试玩,但没有价格。 - 主规范 `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md` 明确把「交易与创作者收入」列为首版不做,本次需求要先解除该排除并补行为合同。 需求:作者上传游戏时可选**买断制付费**;其他用户需花**泥点**购买后才能游玩;后台审核时**可见该泥点数量**,并且审核员**可以无限制游玩**。 ## 现状事实(已核对) - 游玩链路:`/games/detail` → `GameDetailPage`「立即玩」→ `/games/play?id=` → `GamePlayPage.startGame` 挂载 `sandbox="allow-scripts"` 的 iframe,src = `currentVersion.entryUrl`(服务端派生的同源 `/games/<gameId>/`),发行网关(`serve_release_entry` / `serve_release_asset`)公开、拒绝任意 Cookie、扩展名白名单、`Cache-Control: public,max-age=60`。 - 审核链路:`/admin/api/game-distribution/reviews` 待审列表 → 版本详情 → `POST /versions/{id}/review`(approve/reject,带 `expectedPublicationRevision` CAS + `Idempotency-Key`);审核员试玩走 `POST /admin/api/game-distribution/versions/{id}/preview-session`,签发 10 分钟、绑定 `versionId`、仅 `pending_review/rejected` 可用的 `/api/game-distribution/admin-previews/{token}/`,资源按 token 前缀解析,**不限次数、不扣泥点**——这条已满足「审核员无限制游玩」,本次只需保证不破坏它并在详情显示价格。 - 冻结资料先例:版本 `metadata_json` 冻结作者资料(封面/截图等),审核通过时整体生效到 game 行(`cover_object_key` / `screenshots_json`)。价格应沿用同一冻结-生效模式。 - 钱包先例:LLM 结算(commit `736a1b6ac`)曾给 `RuntimeProfileWalletLedgerSourceType` 追加 `LlmRouterConsume` 变体,同步 TS 联合类型、前端文案与生成绑定,未改 `profile_wallet_ledger` 表结构——`game_purchase` 可沿用同一做法。 ## 方案(已与用户确认) ### 1. 定价与数据模型 - `game_distribution_game` 末尾追加 `price_mud_points: u64`(`#[default(0)]`,`0` = 免费)。价格与封面/截图一样来自版本冻结资料,审核通过时在 approve 事务内写入 game 行。 - 版本冻结资料 `metadata_json` 增加 `priceMudPoints`;作者创建/更新版本时可提交,api-server 校验范围(例如 `0..=1_000_000` 整数)。 - 新增持久表 `game_distribution_purchase`:`purchase_id`(PK)、`game_id`、`user_id`、`price_mud_points`(成交价快照)、`wallet_ledger_id`(泥点流水 ID,便于对账)、`created_at`;`(user_id, game_id)` 唯一(买断制一人一条),加入 `migration.rs` 白名单、表目录与生成绑定。 - **不新增 entitlement 表**:买断制下 purchase 行即所有权事实源;「永久可玩」由 purchase 行存在决定。 ### 2. 泥点流水 - `RuntimeProfileWalletLedgerSourceType` 追加 `GamePurchase => "game_purchase"`;同步 `packages/shared/src/contracts/runtime.ts`、`shared-contracts`、前端 `platformProfileFundsModel.ts` 文案(如「购买游戏」)、后台相关映射与生成绑定。 - 扣费复用钱包原子写路径(余额 + 流水 + consumption total 同事务),`metadata_json` 记 `{kind:"game_purchase", gameId, priceMudPoints}`。 - 余额不足返回明确错误码(如 `INSUFFICIENT_MUD_POINTS`),不落 purchase、不改余额。 ### 3. 购买接口与并发 - `POST /api/game-distribution/games/{game_id}/purchase`(require_bearer_auth,`Idempotency-Key`): - 校验游戏已公开、`price > 0`、当前版本可服务;免费游戏或已购买时幂等返回既有 purchase(`replayed=true`),不重复扣费。 - 扣泥点 → 写 purchase 行,必须做成原子(SpacetimeDB procedure 内跨表事务),避免「扣了没记录」或「记录了没扣」。 - 并发双击 / 响应丢失重试:唯一约束 + 幂等键双重兜底,只扣一次。 ### 4. 播放鉴权(真付费保护,已确认选 token 会话) - 公开详情 DTO 仍返回到价格与购买态,但**付费且未购买时不下发可播放入口**;`GameDetailPage` 显示「N 泥点购买」,不显示可点的「立即玩」。 - 新增 `POST /api/game-distribution/games/{game_id}/play-session`(付费游戏 require_bearer_auth):校验已购买 / 作者本人 / 管理员,签发绑定 `gameId + userId + 过期时间` 的短时 token,返回 `/api/game-distribution/play-sessions/{token}/`。 - 新增网关 `GET /api/game-distribution/play-sessions/{token}[/{*asset}]`:沿用现有 admin preview 的实现套路(token 前缀承载相对资源、无登录中间件、拒可解析 Cookie、`no-store`、token 过期/游戏下架 → 404)。 - 现有公开 `/games/<game_id>/` 网关:当前活动版本 `price_mud_points > 0` 时返回 404,杜绝「猜 gameId 直连绕过」;免费游戏完全不变。 - 作者本人与管理员免购买即可游玩自己的 / 任意付费作品,且不计费(审核需要)。 - 游玩计数只在真正开始游玩后上报(保持现有 `record_game_play` 口径,付费游戏经由播放会话后上报)。 ### 5. 后台审核 - 审核详情页新增价格展示:`priceMudPoints`(`0` → 「免费」/「按当前版本免费」,否则「N 泥点」);待审列表可加价格列。 - 保留并回归「审核员试玩当前待审版本」:不限次数、不校验购买、不扣泥点。 - 管理员查看已公开付费游戏时也可无限制试玩(走播放会话的管理员分支)。 ### 6. 前端 - 网页发布页 `GamePublishPage.tsx` 新增「付费方式」:免费 / 买断制(正整数泥点,上限与后端一致);更新模式同样可改价(走新版本冻结资料)。 - AGC 发布面板同步加同一选项(可在第二个切片做,避免一次改动过大)。 - 详情页展示价格与购买态;购买前用 `PlatformMudPointConfirmDialog`(「确认消耗泥点」)二次确认并展示余额,余额不足引导充值;购买成功后刷新 `usePlatformWalletStore` 余额。 - 游玩页:付费游戏先请求 play-session 拿 token URL 再挂 iframe;免费游戏维持现状。 - 广场卡片可加「泥点」角标(可选)。 ## 契约、文档与迁移 - SpacetimeDB:`game_distribution_game` 追加 `price_mud_points`(表末尾 + 默认值);新增 `game_distribution_purchase`;同步 `migration.rs`、后端架构文档表目录、生成绑定,运行 `npm run check:spacetime-schema`。 - Rust/TS DTO:`priceMudPoints`、`purchased` / `ownedByViewer`、购买请求/响应、播放会话响应。 - 主规范:修订 `docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`,解除「不做交易」排除并新增「买断制付费与播放鉴权」合同章;先按 `docs/【协作规范】规范驱动开发工作流-2026-09-12.md` 建主规范/里程碑/实施计划再编码。 - 不涉及 `/api/external/v1`,无需更新 `docs/openapi/genarrative-external-v1.openapi.json`。 ## 验收判据 - [ ] 作者网页发布可选「免费」或「买断制 N 泥点」;非法值(负数/超上限/非整数)被前后端同时拦截。 - [ ] 后台审核详情显示该版本价格;审核员可无限次试玩待审版本,不扣泥点。 - [ ] 未购买用户详情页显示「N 泥点购买」,不显示可播放入口;直连 `/games/<gameId>/` 返回 404。 - [ ] 余额不足购买失败且余额/账单不变;余额充足购买成功后余额减少 N、账单出现 `game_purchase` 流水、purchase 行唯一。 - [ ] 重复点击 / 并发购买 / 响应丢失重试只扣一次(幂等 + 唯一约束)。 - [ ] 购买后可反复游玩、不再扣费;换设备/重新登录仍可玩(买断制永久)。 - [ ] 作者本人与管理员对付费作品免购买、无限制游玩。 - [ ] 免费游戏全链路(发布/详情/游玩/网关/计数)无行为变化。 - [ ] `npm run check:spacetime-schema`、`npm run check:encoding`、`npm run typecheck`、定向 Rust/Vitest、`git diff --check` 通过;真实本地栈 smoke 覆盖发布→审核→购买→游玩闭环。 ## 不在本轮范围 退款/撤销购买、作者分成与结算、促销/限时免费、价格运营审核规则、AGC 发布面板(可作第二切片)、评分/评论与付费的耦合、外部 API。 ## 工作区 分支 `feat/game-purchase`,worktree `.worktrees/feat/game-purchase`(基于 `origin/master`)。按规范驱动开发工作流先落主规范/里程碑/实施计划,再进入实现。
suzmii added the
Priority
High
2
Kind/Feature
labels 2026-10-05 16:25:03 +08:00
suzmii self-assigned this 2026-10-05 16:25:03 +08:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: GenarrativeAI/Genarrative#634