合并 origin/master(52c83cc2a,248 个提交)到 feat/game-purchase
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m28s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m26s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 4m41s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m56s
Project CI / Backend tests (pull_request) Successful in 9m4s
Project CI / Repository checks (pull_request) Successful in 7m13s
Project CI / Native shell tests (pull_request) Successful in 10m16s
Project CI / Frontend tests (pull_request) Successful in 2m26s

- 同步 master 侧 248 个提交:会员与泥点计费(PR #588 / #603)、创作者主页与关注粉丝(PR #638)、运行页刷新真重载(PR #640)、AGC 模板 templateVersion 递增(PR #643)等;确认 master 未触碰发行网关与 `api-server/src/modules/game_distribution.rs`。
- 冲突 1(server-rs/crates/module-runtime/src/domain.rs:1157-1162)`RuntimeProfileWalletLedgerSourceType` 两侧各自在末尾追加变体:保留 master 的 `MembershipUpgradeGrant` 为索引 16(线上已落库口径),本分支 `GamePurchase` 顺延为索引 17,并保留其「每账号每游戏一次扣费」注释。
- 冲突 2(server-rs/crates/module-runtime/src/domain.rs:1184-1188)`as_str()` 同时保留 `membership_upgrade_grant` 与 `game_purchase` 两个分支。
- 冲突 3(server-rs/crates/spacetime-client/src/active/mapper/runtime_profile.rs:95-101)bindings→领域枚举映射同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,按新索引顺序排列。
- 冲突 4(server-rs/crates/spacetime-client/src/module_bindings/runtime_profile_wallet_ledger_source_type_type.rs:43-47)生成枚举按模块源码顺序合并为 …`MembershipUpgradeGrant`、`GamePurchase`;随后 `npm run spacetime:generate` 复核该文件与生成结果逐字节一致。
- 冲突 5(server-rs/crates/api-server/src/admin.rs:4849-4853)sats 索引映射改为 `16 => membership_upgrade_grant` 与 `17 => game_purchase` 两条并存。
- 冲突 6(server-rs/crates/api-server/src/admin.rs:6368-6461)索引断言并入 master 的 `[16] => membership_upgrade_grant`,并把 master 的「17 尚未映射」占位断言替换为 `[17] => game_purchase`;本分支的后台消耗统计两个用例原样保留。
- 冲突 7(server-rs/crates/api-server/src/runtime_profile.rs:219-226)来源文案格式化同时保留 `MembershipUpgradeGrant` 与 `GamePurchase` 两支,注释随 `GamePurchase` 保留。
- 冲突 8(docs/project-memory/shared-memory/decision-log.md:3-24)两条决策记录并列保留:本分支 2026-10-05「播放会话前缀在三处入口清空 Cookie」在前,master 2026-10-03「每日免费发放额为 0 时前端隐藏该池」在后。
- 保住 master 侧:发行响应 ETag/304/gzip(`release_asset_response_with_cache` 五参形态与 `release_asset_etag` / `if_none_match_matches` / `accepts_gzip_encoding` / `gzip_release_asset` / `release_asset_not_modified_response`)、共享加载面 `PlatformGameLoadingSurface` 及其使用方、三池钱包与会员档位目录改造。
- 保住本分支侧:付费作品 404 守卫(`price_mud_points > 0` 且位于 `load_package` 之前)、购买与播放会话网关路由、nginx×3 / Pingora / Vite 的播放会话 Cookie 清空与 `nginx-route-parity.matrix.json` 登记、viewer 感知公开详情、后台审核价格、`PlatformGamePricingField` 共享定价组件与两端接入、AGC 项目版本只读 + 平台派发下一版本号、schema 与迁移白名单、钱包来源 `game_purchase`。
- 验证:`git merge-base --is-ancestor origin/master HEAD` 返回 0;`check:encoding`、`check:doc-index`、`check:spacetime-schema`、`check:server-rs-ddd`、`check:rustfmt`、`check:npm-workspaces`、`check:game-distribution-dto-parity`、`check:game-distribution-price-limit-parity`、`check:pingora-route-parity`、`check:nginx-spa-routes`、根/admin-web/AGC typecheck、`git diff --check` 全绿。
- 测试:`cargo test -p module-game-distribution` 30 passed,`cargo test -p api-server game_distribution` 61 passed,`cargo test -p spacetime-module game_distribution` 7 passed;定向 vitest 28 个文件 252 个用例全通过。
- `npm run spacetime:generate` 除 11 个 procedure 文件的纯空白格式差异外无任何语义改动(`git diff -w` 为空),已保留 master 的生成产物形态,提交内无 schema/绑定语义变化。
This commit is contained in:
2026-10-06 13:12:32 +08:00
201 changed files with 15890 additions and 13199 deletions
@@ -0,0 +1,207 @@
# 【实施计划】双余额泥点与会员补差升级
> 适用范围:`server-rs/crates`(SpacetimeDB 模块、`module-runtime`、`api-server`、`shared-contracts`)
> 与前端余额展示 / 充值弹层(`packages/shared`、`apps/ai-game-creator-shell`、`apps/admin-web`)。
> 状态:方案定稿(2026-10-02),未实施。本文是开发依据;术语见 `CONTEXT.md`,决策见四份 ADR。
> 字段级后端设计(DB schema / 账本 / 领域结构 / HTTP 契约 / 开放决策)见
> [`【技术设计】泥点三池与会员计费后端设计-2026-10-02`](./【技术设计】泥点三池与会员计费后端设计-2026-10-02.md)。
## 0. 一句话交付与验收判据
一句话:把「一个总额 + 三视图」的钱包改造成对外**并列三池**(每日免费 / 月度 / 永久)的记账,
并在此之上建成月付 / 年付会员——按开通日自然月计期、年付 12 期逐月发放、支持补差升级与到期重选。
验收判据(全部满足才算完成):
1. 余额接口并列返回每日免费泥点、月度泥点(含到期日、刷新日、期数)、永久泥点,
不再出现「永久泥点 = 总额 − 其他池」的对外口径。
2. 扣减顺序恒为 每日免费 → 月度 → 永久;可用总额 = 月度 + 永久;每日免费单独计量与展示。
3. 月付 / 年付按**开通日自然月**计期;年付共 12 期,每期刷新先清上期余额再发新额度;
到期未续清月度余额、停发,永久泥点保留可用。
4. 补差升级按 §3.3 公式计算;金额向上取到分、补点向下取整;刷新日与到期日不变;
防重复回调重复发点。
5. 在线部分零回归:每日免费池、永久泥点口径、账本历史、充点订单与首充资格不受影响;
`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过。
## 1. 现状事实基线
| 关注点 | 现状 |
|---|---|
| 权威余额 | `profile_dashboard_state.wallet_balance`,**包含**每日免费与月度 |
| 每日免费池 | `profile_daily_free_points`(北京时间 `day_key`、`granted_points` / `remaining_points`),在线 |
| 月度池 | `profile_membership.cycle_*`(`cycle_started_at` / `cycle_resets_at` / `cycle_granted_points` / `cycle_remaining_points` / `cycle_period_days`),**固定 30 天、无年付、无期数**,会员侧未上线 |
| 永久池 | **推导余数** `wallet_balance − 每日免费剩余 − 月度剩余`,无独立表,在线 |
| 扣减顺序 | 每日免费 → 会员周期 → 永久(`apply_profile_wallet_signed_delta`) |
| 退款回填 | 账本感知,跨天 / 跨周期回填失败的金额落永久池 |
| 账本 | `profile_wallet_ledger` 只追加,幂等靠稳定 id;16 种来源类型,**无升级补点** |
| 充值商品 | `profile_recharge_product_config`:四档充点 `points_60/180/300/680`(基础 ¥1 = 10,后三档首充赠送 50%),会员商品 `duration_days` 全 30 |
| 对外投影 | `ProfileMudPointBalanceResponse`:`totalPoints = wallet_balance`、`limitedPoints = cycle_remaining_points`、`permanentPoints = 总额 − 每日 − 月度` |
| 后台权益 | `ProfileMembershipBenefitResponse` 含 `month/season/year/starter/basic/pro/ultimate` 七个旧档位字段 |
| 年付 / 补差 | 全仓无实现 |
冲突点(本次必须一次性收口):
- `totalPoints` 带池语义(含每日与月度),与「可用总额 = 月度 + 永久」直接冲突。
- `limitedPoints` 是全仓唯一出现「限时」措辞的地方,与「月度泥点」规范词冲突。
- 原细则表只写「月度优先扣减」,与权威契约的「每日免费优先」冲突——本方案维持每日免费优先。
- 权威契约把充值商品写死为四档带首充赠送,与本方案的 6 档无赠送冲突。
## 2. 术语
规范词见 `CONTEXT.md` 的「泥点与会员计费」:每日免费泥点、月度泥点、**永久泥点**、永久余数、会员账期、
补差升级、可用总额。实现侧只保留一个规范词:**永久泥点**(`_Avoid_: 不限时泥点`、充值余额、无限泥点)。
## 3. 规则细则(合并去重,开发以此节为准)
### 3.1 双余额记账
| 规则 | 月度泥点 | 永久泥点 |
|---|---|---|
| 来源 | 会员每期额度 | 直接充值 + 非会员奖励(历史赠点保留) |
| 有效期 | 当期有效,到期清零 | 永不过期 |
| 扣减顺序 | 每日免费耗尽后优先 | 月度不足时补扣 |
| 刷新 | 下期发新额度,旧余额不结转 | 不刷新,保留余额 |
| 升级时 | 剩余保留 + 按 §3.3 补发差额 | 完全不变 |
| 展示 | 独立卡片 + 到期 / 刷新日期 | 独立卡片 + 永久标记 |
**每日免费泥点保留**,并位于扣减顺序最前(本方案对原细则的补充):
每日免费当天 24 点清零,先扣它对用户最有利,且不用改动现有的退款回填语义。
可用总额 = 月度 + 永久,每日免费单独一行展示,不并入可用总额。
示例:月度剩 100 + 永久 500,消费 150 → 先扣 100 月度再扣 50 永久。
「永久」指不过期,创作照常扣点。
### 3.2 账期与刷新
- 按**开通日**计期(9/20 开通 → 10/20 到期),不按自然月;锚点为北京时间的开通日时刻。
- 每一期从**原始开通日**重算并夹取月末(1/31 开通 → 2/28 → 3/31),不从上一期递推。
- 年付:开通发首期,后续按月再发 11 期,共 12 期;`expires_at` = 开通时刻 + 12 个月(同样夹取)。
- 每期刷新:先清上期余额,再发新额度。
- 到期未续:清月度余额、停发;永久泥点保留可用。
- 本期为单次购买,不自动续费;到期后由用户重新选择套餐(月转年、年转月、降档一律到期后再选)。
### 3.3 补差升级
```
月付应付 = 新月价 − 当前月价
月付补点 = 新套餐每期额度 − 当前套餐每期额度
年付应付 = (新年价 − 当前年价) / 12 × (后续完整月数 + 本期剩余时间比例)
年付本期补点 = (新每期额度 − 当前每期额度) × 本期剩余时间比例
后续各期额度 = 新档每期额度
```
- 本期剩余时间比例 = 本期剩余毫秒 / 本期实际毫秒(`cycle_started_at` → `cycle_resets_at`);
后续完整月数 = 从本期 `cycle_resets_at` 到 `expires_at` 之间剩余的完整自然月期数。
- 费用向上取到分,补点向下取整。
- 升级后月度余额 = 原剩余月度余额 + 补点(已用点不返还,永久点不动);
`cycle_granted_points` 同步 +补点,否则退款回填的上限判据会算错。
- 有效期 = 原刷新日与原到期日不变。
- 连续升级以**当前套餐**为比较基准(Starter→Plus 再 →Pro 时按 Plus vs Pro 算,不回到最初套餐)。
- 支付成功才变更;失败 / 关闭不动原状态;防重复回调重复发点。
参考示例(口径来自产品细则,实施时用领域测试固化):
| 变更 | 补付 | 补点 | 变更后月度余额 | 有效期 |
|---|---|---|---|---|
| Plus 月 → Pro 月 | ¥200 | 2,500 | 3,115(原剩 615) | 原到期日不变 |
| Plus 年 → Pro 年 | ¥1,083.34 | 1,250 | 1,865(原剩 615) | 原到期日不变 |
### 3.4 允许操作矩阵
| 当前状态 | 可操作 | 不可操作 |
|---|---|---|
| 无会员 / 已到期 | 开任意月付或年付 | — |
| 月付会员 | 升更高月付 | 即时月转年、降档(到期后再选) |
| 年付会员 | 升更高年付 | 年转月、降档(到期后再改) |
| 同套餐同周期 | — | 重复购买(显示「当前套餐」) |
### 3.5 与现状的差异清单
1. `limitedPoints` → `monthlyPoints`(全仓改名,含 `shared-contracts`、`packages/shared` 契约、AGC、后台)。
2. `totalPoints` 的池语义退役:对外只给三个池;不提供「可用总额」后端字段,前端按需相加。
3. 会员周期从固定 30 天改为按开通日自然月;新增 `cycle_index` / `cycle_count`。
4. 新增年付(12 期)与补差升级;新增后端只读报价接口。
5. 账本来源枚举新增 `MembershipUpgradeGrant`。
6. 会员订单复用 `profile_recharge_order` + 末位变更快照字段。
7. 充点商品复用旧 4 档 id 并把赠送置 0(取消首充赠送),新增 ¥128 / ¥328 两档,共 6 档;不删除任何行(历史订单与首充资格引用 `product_id`)。
8. 会员档位改为枚举 `RuntimeProfileMembershipPlan { Normal, Starter, Plus, Pro, Max }`;
会员目录迁到 `profile_membership_plan` 并以**该枚举为主键**(后台按档位改价);
删除 `RuntimeProfileMembershipTier`、`profile_membership.tier` 与 `profile_recharge_product_config.tier`。
## 4. 数据模型与账本
- **不动**:`profile_dashboard_state.wallet_balance`(在线唯一权威总额)、`profile_daily_free_points`、
`profile_wallet_ledger` 既有形状与 id 方案、永久余数推导口径。
- `profile_membership`:**删除** `tier` / `cycle_period_days`;新增 `plan`(`RuntimeProfileMembershipPlan` 枚举,字段名 `plan`、类型必须是枚举)/ `cycle_index` / `cycle_count` / `cycle_kind`。
- 新增 `profile_membership_plan`:**以档位枚举为主键**,含标题、rank、月价、年价(独立可配置字段)、每期额度、模型权限、并发上限(`128` = 不设上限)、排序与上下架;一行一档,月 / 年共享每期额度。
- `profile_recharge_product_config`:**删除** `tier` 与 `membership_period_points` / `membership_period_days` / `membership_queue_limit` / `membership_discount_bps`;删 4 行 `member_*`;充点复用旧 4 档 id 并置 `bonus_points = 0`,新增两档(`points_1280` / `points_3280`)。
- 新增账本来源类型 `MembershipUpgradeGrant`;幂等锚 `membership-upgrade-grant:{user}:{order_id}`。
- `profile_recharge_order` 末尾追加会员变更快照字段(变更前 / 后档位、周期、补点数、有效期、kind);快照字段全部用枚举,不用字符串。
- schema 变更必须同步 `migration.rs`、表目录、生成绑定,并运行 `npm run check:spacetime-schema`;
已有表新增字段一律放在结构体最后并带明确默认值。
- 会员侧删列 / 删枚举属**破坏性变更**(会员未上线,已授权):迁移计划与回退见技术设计 §2.7;
账本来源枚举仍只允许末尾追加 `MembershipUpgradeGrant`。
## 5. 接口契约
| 接口 | 变更 |
|---|---|
| `GET /api/profile/recharge-center` 等余额投影 | 并列三池;`monthlyPoints` 带到期日 / 刷新日 / 期数;不再暴露池语义的 `totalPoints` |
| `POST /api/profile/membership/upgrade-quote`(新增) | 只读报价:入参目标档位 + 目标周期,返回应付金额、补点、变更后月度余额、有效期;目标档位 token 严格解析进枚举 |
| 会员购买 / 升级下单 | **复用** `POST /api/profile/recharge/orders`;`product_id` = 目标计划 id,下单时重算并落订单快照 |
| `GET|POST /admin/api/profile/membership-plans`(新增) | 后台套餐配置,按档位枚举改月价 / 年价 / 每期额度 / 模型权限 / 并发上限 |
| `GET|POST /admin/api/profile/recharge-products` | 6 档充点商品的上下架与新档位 |
`/api/external/v1` 不涉及本方案;若实施中触及其路由、DTO 或语义,必须在同一次变更中同步修订
`docs/openapi/genarrative-external-v1.openapi.json` 与契约测试。
## 6. 里程碑
### M1 双余额与月度会员(不含年付与补差)
- 交付物:三池并列投影(含 `monthlyPoints` 改名与 `totalPoints` 退役)、自然月账期、
`profile_membership_plan`(档位枚举主键)与月度会员购买、充点 6 档(复用旧 id + 新增 2 档)、
会员档位枚举化与旧 `tier` / 旧权益字段退役。
- 验收判据:§0 的判据 1、2、5;月付会员可购买、可到期清零、可在新期收到新额度。
- 必测点:三池恒等式(总额 = 每日 + 月度 + 永久)、扣减顺序与可用总额、月末夹取(1/31 → 2/28 → 3/31)、
每日免费与月度同时存在时的扣减拆分元数据、退款跨期落永久池、旧充点商品历史订单仍可展示。
### M2 年付 12 期与补差升级
- 交付物:年付(12 期)、后端只读报价接口、`MembershipUpgradeGrant` 与订单维度幂等、
订单变更快照、后台套餐配置页。
- 验收判据:§0 的判据 3、4;年付 12 期逐月发放;升级补付 / 补点与 §3.3 示例一致;
重复回调不重复发点;退款进 `manual_review` 且快照可读。
- 必测点:金额向上取到分 / 补点向下取整的边界(含 0.5 期、闰年、12/31 跨年)、
连续升级基准、报价与下单重算一致、支付失败不动原状态、年付到期未续清月度保留永久。
## 7. 风险与边界
- **永久余数是临时实现**:池层面的 bug 会表现为永久泥点虚高,排查必须回到账本 `permanentPointsDelta`(见 ADR 的 TODO)。
- **在线契约变更**:`totalPoints` / `limitedPoints` 的消费方分布在后端投影、`packages/shared` 契约与 store、
`PlatformMudPointWalletEntry`、`PlatformProfileRechargeModal`、AGC `AccountWallet` / `useAccountWallet`、
admin-web 多处;漏改会静默显示错数字。必须一次性改完并全量跑相关测试。
- **升级补点不可自动回收**:它只有「发」没有「扣」的对称半边,退款只能人工复核。
- **惰性刷新没有 cron**:到期与时区问题只在用户被触碰时暴露,测试必须直接构造时间。
- 跨天 / 跨周期退款仍然有损(落永久池),这是刻意行为,不要「顺手修」。
## 8. 待确认输入与遗留 TODO
- **套餐目录数值已定**:`Starter` ¥39 / 400 点 / ¥390 · `Plus` ¥99 / 1150 / ¥990 · `Pro` ¥299 / 3650 / ¥2990 · `Max` ¥699 / 8650 / ¥6990(`Normal` 全 0);模型权限 Starter 仅基础、其余含高性能;并发 1 / 3 / 5 / 128(不设上限)。
- **充点 6 档金额已定**:复用旧 4 档 id 并置赠送 0,新增 `points_1280`(¥128 / 1280 点)与 `points_3280`(¥328 / 3280 点);数值按原型 ¥6 / 18 / 30 / 68 / 128 / 328、¥1 = 10 泥点固化。
- **TODO(永久泥点独立存储)**:见 `docs/adr/【ADR】泥点三池以单一总额为权威-2026-10-02.md` 第 5 条。
- **TODO(文档同步)**:实施时同批修订权威契约
`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` 的「账户充值数据契约」与
「用户钱包与编辑器生成扣费契约」两节,以及 `docs/【编辑器】模型定价配置管理方案-2026-06-22.md` 中
与扣费顺序相关的表述。
## 9. 需同步修订的权威文档
| 文档 | 修订点 |
|---|---|
| `docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md` | 三池投影与扣减顺序、永久泥点来源、会员账期与年付、升级补点与幂等、充值商品档位与首充赠送、账本来源全集 |
| `docs/【项目基线】当前产品与工程约束-2026-05-15.md` | 「用户钱包与编辑器生成扣费契约」的余额字段 |
| `docs/【编辑器】模型定价配置管理方案-2026-06-22.md` | 与扣费顺序 / 月度池相关的表述 |
| `docs/project-memory/shared-memory/decision-log.md` | 本次四条决策(已追加 2026-10-02 条目) |
| `CONTEXT.md` | 新增「泥点与会员计费」术语 |
@@ -42,6 +42,7 @@
- 上游变化不自动跟随:目录只在未初始化时重建;上游新增或移除模型由 owner 在后台增删条目或调整启用、默认项。
- `GET/PUT /admin/api/agc-models` 仅 owner 可用,返回完整配置;PUT 携带上次读取的 revision,冲突拒绝覆盖。
- `GET /api/llm/models` 返回启用项的 `id/displayName`、`defaultModelId` 和目录 `revision`,不返回实际模型名、Router 目录、凭据或能力原始数据。
- 2026-10-03 修订(模型权限):`GET /api/llm/models` 改为两个平行数组——`models` 只放本档可用;`unavailableModels` 放目录里其余(含后台停用与本档档位不够),元素带 `reason` 枚举(`plan_required` / `disabled` / `unknown`,同时停用且档位不够取 `disabled`)。`unavailableModels` 是加法字段,旧客户端忽略后行为不变,且不放松服务端按档强制;`defaultModelId` 仍必须落在 `models` 内。客户端把 `unavailableModels` 渲染为不可点项并 hover / focus 出原因(`plan_required`→「订阅计划不支持」、`disabled`→「该模型已下线」、`unknown`→「暂不可用」);已选模型落进不可用桶时自动回退默认并给出对应原因文案,后台直接删除(两桶都没有)用泛化文案。
- 客户端缓存最近 `revision`,在项目切换 / 对话表面挂载 / 下拉展开 / 窗口聚焦时条件刷新:`revision` 未变化不更新界面,同一时刻只保留一个在途请求,刷新失败保留上一次有效目录与本地选择。发起对话前用同一份快照校验所选模型仍启用,已停用或删除则回退默认模型并提示。
- 手动刷新立即显示进行中状态;真实刷新成功后显示完成反馈,即使 `revision` 未变化也有反馈。失败沿用有效缓存时仍显示失败,不能报告刷新成功;HTTP 状态和超时使用可辨认的提示。
- 模型目录与其它客户端 JSON API 的成功、失败响应体读取均复用 `readClientHttpResponseText` 的 15 秒上限;响应头已返回但响应体卡住时必须结束本次等待、释放目录在途请求并允许重试,迟到的响应不得覆盖新目录。
@@ -0,0 +1,342 @@
# 【技术设计】会员与泥点前端改造
> 定位:承接 [`【技术设计】泥点三池与会员计费后端设计-2026-10-02`](./【技术设计】泥点三池与会员计费后端设计-2026-10-02.md)(后端契约与接口已落地)
> 与原型 [`充值与会员.html`](../../充值与会员.html),给出前端(`packages/shared` + `src` + `apps/ai-game-creator-shell`)的改造设计。
> 范围:共享充值弹层、钱包入口、三处消费方的 controller 与客户端接口;不含后端实现。
> 状态:**已实施(2026-10-03)**。评审已通过,按 §11 清单落地;实现与验证结果见 §11、§13。
---
## 0. 一句话
把共享充值弹层从「一列泥点商品」升级为与原型一致的「会员与泥点」双 Tab 弹层
(套餐卡 + 月/年切换 + 会员状态 + 补差升级 + 六档充值 + 套餐对比 + 确认页),
金额与权益只读后端目录与报价接口,三处消费方共用同一套组件。
---
## 1. 已确认决策(拷问结论)
| # | 决策 | 结论 |
|---|---|---|
| 1 | 改造层级 | 扩展**共享** `PlatformProfileRechargeModal` 及其 controller,三处消费方共用 |
| 2 | 与原型对齐 | **完全像素级对齐**:两个 Tab、四张套餐卡、对比表、数量估算、确认页都做 |
| 3 | 上线方式 | **直接全量开放**,不加功能开关;会员 UI 对所有用户可见 |
| 4 | 钱包入口 | 改用**原型入口形态**(可用总额 + 行内月度/永久 + 「会员与泥点」按钮) |
| 5 | 入口次级功能 | 保留「使用详情」「兑换码」为次级入口,不因贴原型而砍功能 |
| 6 | 余额池 | 弹层内显示**三池**(每日免费 / 月度 / 永久)并按真实扣减顺序说明 |
| 7 | 金额真源 | **服务端报价**:购买用目录价、升级调 `POST /api/profile/membership/upgrade-quote`,前端不计费 |
| 8 | 确认页 | **做**确认页;**不做**前端倒计时,也**不做**价格 compare / 不一致提示 |
| 9 | 支付方式 | **仅微信**,渠道按运行环境自动解析,不出现支付宝 |
| 10 | 套餐文案 | 价格/每期泥点/模型/并发/标题读后端 catalog;「数量估算」「推荐之选」等营销文案放前端常量 |
| 11 | 消费方范围 | Web 平台入口、图片编辑器、AGC 桌面壳**三处都接** |
| 12 | 文档流程 | 先出本设计文档评审,再动代码 |
| 13 | `review.txt` | 后端评审问题**不作为本次门禁**,本次只做前端 |
---
## 2. 现状(已核实)
- 共享弹层 `packages/shared/src/components/PlatformProfileRechargeModal/index.tsx`(538 行)
只渲染 `center.pointProducts` 一列商品,标题「购买更多泥点」;props 只有
`center / isLoading / error / submittingProductId / nativePayment / onClose / onRetry / onBuy / onConfirmNativePayment / onCloseNativePayment`。
- 同目录 `index.test.tsx` 目前**断言不暴露会员**:`queryByRole('tablist')` 为 `null`、`queryByText('Starter')` 为 `null`,
用例名即 `shows the point-product empty state without exposing membership purchase`。本次必须改这些断言。
- Web 平台入口 `src/components/platform-entry/PlatformEntryActiveFlowShell.tsx` 与
图片编辑器 `src/components/image-editor/ImageCanvasEditorView.tsx` **共用**同一个 controller
`src/components/platform-entry/usePlatformProfileCenterController.ts`(1923 行)→ Web 端只需改一处。
- AGC 桌面壳走独立链路:`apps/ai-game-creator-shell/src/features/app-shell/useAccountWallet.ts`
→ `apps/ai-game-creator-shell/src/services/accountHost.ts`(Tauri `invoke`)
→ `apps/ai-game-creator-shell/src-tauri/src/account_api.rs`(命令注册在 `desktop.rs`)。
- 三端都**还没有**升级报价的客户端方法;Web 端现有客户端为
`getPlatformProfileRechargeCenter` / `createPlatformProfileRechargeOrder(productId, paymentChannel)` /
`confirmWechatPlatformProfileRechargeOrder` / `watchWechatPlatformProfileRechargeOrder`。
- 契约侧 `packages/shared/src/contracts/runtime.ts` 已有
`ProfileMembershipPlan` / `ProfileMembershipCycleKind` / `ProfileMembershipPlanRecord` /
`ProfileMembership` / `ProfileMembershipUpgradeQuote` / `ProfileRechargeCenterResponse`。
- 真实支付渠道只有微信(`wechat_mp` / `wechat_mp_virtual` / `wechat_jsapi` / `wechat_h5` / `wechat_native` / `mock`),
由 `resolveProfileRechargePaymentChannel` 按运行环境解析,**没有支付宝**。
- 会员下单复用 `POST /api/profile/recharge/orders`,`product_id = membership-{plan}-{cycleKind}`
(如 `membership-pro-yearly`),会员商品没有商品配置行,标题与价格来自目录表。
---
## 3. 契约与需要新增的前端接口
### 3.1 复用(不改)
- `GET /api/profile/recharge-center` → `ProfileRechargeCenterResponse`
(`mudPointBalance` 三池 / `membership` / `membershipPlans` / `pointProducts`)。
- `POST /api/profile/recharge/orders { productId, paymentChannel }` → 泥点与会员下单共用。
- `POST /api/profile/recharge/orders/{orderId}/wechat/confirm` → 微信支付确认。
### 3.2 新增(前端缺、后端已有)
Web 端 `src/services/platform-entry/platformProfileClient.ts` 新增:
```ts
export function getPlatformProfileMembershipUpgradeQuote(
targetPlan: ProfileMembershipPlan,
options: PlatformProfileRequestOptions = {},
) {
return requestPlatformProfileJson<ProfileMembershipUpgradeQuote>(
'/membership/upgrade-quote',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ targetPlan }),
},
'读取升级报价失败',
options,
);
}
```
AGC 桌面壳需要新增同名能力:`accountHost.ts` 增加 `getClientProfileMembershipUpgradeQuote(targetPlan)`,
并在 `src-tauri/src/account_api.rs` 新增 `#[tauri::command] read_profile_membership_upgrade_quote(target_plan: String)`,
在 `src-tauri/src/desktop.rs` 的命令清单里注册。这是桌面壳唯一的非纯前端改动。
### 3.3 会员下单 productId
前端按 `membership-${plan}-${cycleKind}`(`yearly` / `monthly`)拼串后走既有下单接口,
不再向后端请求商品行;`plan === 'normal'` 不可下单。
---
## 4. 信息架构与组件拆分
弹层保持「一个 `index.tsx` + 内部子组件」的现有约定,另抽一个纯常量模块。
```
PlatformProfileRechargeModal/
├── index.tsx // Shell / Tab / 余额概览 / 套餐卡 / 周期切换 / 会员状态 / 对比表 / 充值网格 / 确认页 / 成功态 / 微信二维码
├── membershipCopy.ts // 「数量估算」文案、推荐档位、省费标签等纯前端常量
├── index.css // 复用现有 class,按需补样式
└── index.test.tsx
```
信息架构(对齐原型):
```
顶部入口(PlatformMudPointWalletEntry)
└─ 弹层「会员与泥点」
├─ Tab:会员订阅 | 泥点充值
├─ 余额概览(三池:每日免费 / 月度 / 永久 + 各自失效时间)
├─ 会员订阅面板
│ ├─ 当前会员状态(非会员时隐藏)
│ ├─ 月付 / 年付切换(有效期内锁定为当前周期)
│ ├─ 套餐卡 × 4(Starter / Plus / Pro / Max,来自 catalog,enabled 过滤,normal 不出现)
│ ├─ 套餐对比表(4 行 × 4 档)
│ └─ 底栏主 CTA
├─ 泥点充值面板
│ ├─ 六档金额网格(¥6/18/30/68/128/328 ↔ 60/180/300/680/1280/3280 泥点,¥1 = 10 泥点)
│ └─ 底栏「充值 ¥X」
└─ 确认页(弹窗)
├─ 价格与权益明细(购买:目录价 + 首期额度;升级:补差 + 补点 + 有效期不变)
├─ 支付方式(仅微信,自动解析渠道)
└─ 支付成功态
└─ 微信扫码 / 跳转支付(复用现有 native 支付弹窗,不改)
```
### 4.1 新增 props(`PlatformProfileRechargeModalProps`)
```ts
export type PlatformProfileMembershipConfirmState = {
/** 目标档位与周期;normal 不会出现在这里。 */
plan: ProfileMembershipPlan;
cycleKind: ProfileMembershipCycleKind;
/** 首次购买时来自目录价;升级时来自 upgrade-quote。 */
amountCents: number;
/** 服务端返回的补点;首次购买为 0。 */
grantedPointsDelta: number;
/** 升级后的月度余额;首次购买为首期额度。 */
monthlyBalanceAfter: number | null;
mode: 'purchase' | 'upgrade';
};
// 追加到现有 props:
membershipQuote: ProfileMembershipUpgradeQuote | null;
isLoadingMembershipQuote: boolean;
membershipQuoteError: string | null;
membershipConfirm: PlatformProfileMembershipConfirmState | null;
onRequestMembershipQuote: (plan: ProfileMembershipPlan) => void;
onRequestMembershipPurchase: (plan: ProfileMembershipPlan, cycleKind: ProfileMembershipCycleKind) => void;
onCancelMembershipConfirm: () => void;
onConfirmMembershipPayment: () => void;
```
`onBuy(product)` 只服务泥点商品;会员走 `onRequestMembershipQuote` / `onRequestMembershipPurchase`。
---
## 5. 数据流与状态
- 套餐卡的价格、每期泥点、模型权限、并发上限、标题全部读 `center.membershipPlans`
(按 `sortOrder` 升序、`enabled` 过滤、排除 `normal`)。前端**不**存第二份价格。
- 点套餐判定:
- 当前有效会员且 `plan !== 当前档位` 且 `cycleKind === 当前周期` → 调 `onRequestMembershipQuote(plan)`;
- 否则(非会员 / 已到期)→ 直接 `onRequestMembershipPurchase(plan, cycleKind)` 用目录价开确认页。
- 周期切换:有效期内锁定为 `membership.cycleKind`,另一档禁用并提示「到期后可重新选择」。
- 可升级判定用 `rank`(`center.membershipPlans`),高于当前才能升;同档与降档按钮禁用。
- 确认页金额:升级取 `membershipQuote.amountCents`,购买取目录价;**不做**任何本地计费与价格比对。
- 支付:确认页「去支付」→ 控制器用既有 `createPlatformProfileRechargeOrder(productId, channel)`,
渠道走 `resolveProfileRechargePaymentChannel`;桌面/浏览器差异沿用现有 native 二维码弹窗。
- 支付成功后刷新 recharge center;成功态展示本期泥点与三池余额。
---
## 6. 交互细则
- 与原型一致,但**去掉**原型里的本地 2 分钟报价倒计时与「报价已失效」逻辑
(金额真源在后端,下单与支付确认由后端重算)。
- **不做**价格 compare:不在前端比较「页面展示价」与「服务端返回金额」,也不做不一致提示。
- 移动端优先:弹层宽度、套餐卡网格、对比表横向滚动,遵循现有 `PlatformProfileRechargeModalShell` 尺寸约定。
- 可访问性:Tab 用 `role="tablist"` / `role="tab"` / `aria-selected` + 左右方向键;
套餐卡与充值卡用 `role="radio"` + `aria-checked`;确认页与支付弹窗沿用 `role="dialog"`。
---
## 7. 文案与常量(`membershipCopy.ts`)
只放后端没有、且属于营销表达的内容,按 `plan` token 匹配:
- 每档「每月预计可完成」数量估算与说明;
- 「推荐之选」标签(原型为 `plus`);
- 年付「省 ¥X」文案(按目录 `yearPriceCents` 与 `monthPriceCents × 12` 计算,只用于展示);
- 并发文案:`concurrentJobLimit >= 128` 显示「不设套餐上限」,否则「同时运行 N 个独立任务」;
- 模型权限文案:`basic` →「基础模型」,`full` →「基础及高性能模型」。
未知 `plan` token 时给通用兜底文案,不抛错。
---
## 8. 三处消费方接入清单
| 消费方 | 文件 | 改动 |
|---|---|---|
| Web 平台入口 | `src/components/platform-entry/PlatformEntryActiveFlowShell.tsx` | 传新增 props;入口按钮文案改「会员与泥点」 |
| 图片编辑器 | `src/components/image-editor/ImageCanvasEditorView.tsx` | 同上(共用 controller) |
| Web/编辑器 controller | `src/components/platform-entry/usePlatformProfileCenterController.ts` | 新增报价状态、确认状态、会员下单 handler |
| Web 客户端 | `src/services/platform-entry/platformProfileClient.ts` | 新增 `getPlatformProfileMembershipUpgradeQuote` |
| AGC 桌面壳 | `apps/ai-game-creator-shell/src/features/app-shell/useAccountWallet.ts`、`AccountWallet.tsx` | 新增报价状态与会员下单 handler |
| AGC 客户端 | `apps/ai-game-creator-shell/src/services/accountHost.ts` | 新增 `getClientProfileMembershipUpgradeQuote` |
| AGC Tauri | `apps/ai-game-creator-shell/src-tauri/src/account_api.rs`、`desktop.rs` | 新增并注册 `read_profile_membership_upgrade_quote` |
| 共享入口 | `packages/shared/src/components/PlatformMudPointWalletEntry/index.tsx` | 入口形态按原型调整,保留次级入口 |
---
## 9. 测试计划
- 共享弹层(`index.test.tsx`):
- 改写现有两条「不暴露会员」断言为「暴露会员 Tab 与套餐卡」;
- 新增:四档套餐按 `sortOrder` 渲染、`normal` 与 `enabled=false` 不出现;
- 新增:周期切换在有效期内锁定;
- 新增:点套餐触发 `onRequestMembershipQuote`;
- 新增:确认页展示金额与补点,且**不出现**倒计时与价格 compare 文案;
- 新增:六档充值网格与底栏充值按钮触发 `onBuy`/下单;
- 新增:三池余额展示(含每日免费重置值)。
- Web controller:报价成功/失败、会员下单 productId 拼串、支付成功刷新。
- AGC 壳:`accountHost` 新命令的 invoke 参数与 `useAccountWallet` 报价状态。
- 回归:`PlatformEntryActiveFlowShell.test.tsx`、`ImageCanvasEditorView.test.tsx` 中与弹层标题/入口文案相关的断言同步更新。
- 命令:定向 vitest + `npm run check:encoding` + `git diff --check`。
---
## 10. 验收判据
1. 打开「会员与泥点」弹层可见两个 Tab、四张套餐卡、周期切换、套餐对比表与六档充值网格。
2. 套餐价格、每期泥点、模型权限、并发上限与后端 `membershipPlans` 完全一致,前端无第二份价格。
3. 点套餐进确认页:购买显示目录价,升级显示服务端报价金额与补点;确认页无倒计时、无价格不一致提示。
4. 支付方式只有微信,渠道按运行环境解析,不出现支付宝。
5. 会员与泥点下单都走既有 `POST /api/profile/recharge/orders`,会员 productId 为 `membership-{plan}-{cycleKind}`。
6. Web 平台入口、图片编辑器、AGC 桌面壳三处行为一致,入口保留「使用详情」「兑换码」。
7. 三池余额(每日免费 / 月度 / 永久)与失效时间展示正确,弹层与入口不出现 `NaN`。
8. `npm run check:encoding`、定向 vitest、`git diff --check` 通过。
---
## 11. 实施清单(按顺序)
1. `packages/shared/src/contracts/runtime.ts`:如有缺字段补齐(本轮预计只读,不改形状)。
2. `src/services/platform-entry/platformProfileClient.ts`:新增升级报价客户端方法。
3. `packages/shared/.../PlatformProfileRechargeModal/membershipCopy.ts`:新增前端常量。
4. `packages/shared/.../PlatformProfileRechargeModal/index.tsx`:双 Tab + 会员面板 + 充值网格 + 确认页。
5. `packages/shared/.../PlatformMudPointWalletEntry/index.tsx`:入口形态按原型调整。
6. `src/components/platform-entry/usePlatformProfileCenterController.ts`:报价/确认/下单状态与 handler。
7. `PlatformEntryActiveFlowShell.tsx`、`ImageCanvasEditorView.tsx`:传新 props。
8. AGC:`accountHost.ts` + `account_api.rs` + `desktop.rs` + `useAccountWallet.ts` + `AccountWallet.tsx`。
9. 更新与新增测试,跑定向 vitest 与编码检查。
---
## 12. 风险与后续
- **后端已知缺陷**(见 `review.txt`):结算不比对 `order.amount_cents`、报价接口写库、
旧 `tier` 无迁移等。按用户决定**不作为前端门禁**;作为后端跟进项单独处理。
- 确认页只展示服务端金额,不做事前拦截;若后端金额与展示不同,以支付确认结果为准。
- AGC 桌面壳新增 Tauri 命令属于壳内 Rust 改动,需要与桌面端一起回归。
- 「完全像素级对齐」意味着弹层代码量与测试量显著上升,后续可按效果评估是否精简对比表。
---
## 13. 实施与验证结果(2026-10-03)
### 13.1 落地清单
- 共享弹层 `PlatformProfileRechargeModal`:双 Tab、三池余额概览、套餐卡、周期切换(有效期内锁定)、会员状态、套餐对比表、六档充值网格与底栏充值按钮、确认页(仅微信支付,无倒计时、无价格对比);新增 `membershipCopy.ts` 承载营销文案与并发/模型展示口径。
- 钱包入口 `PlatformMudPointWalletEntry`:改为「可用总额 + 行内月度/永久 + 会员与泥点」,`使用详情` / `兑换码` 作为次级入口保留。
- Web:`platformProfileClient.ts` 新增 `getPlatformProfileMembershipUpgradeQuote`;`usePlatformProfileCenterController` 新增报价状态与 `submitMembershipCheckout`;平台入口与图片编辑器已传参。
- AGC 桌面壳:`accountHost.ts` 新增 `getClientProfileMembershipUpgradeQuote`;`account_api.rs` 新增并注册 `read_profile_membership_upgrade_quote`;`useAccountWallet` 新增报价状态与会员结算;`AccountWallet.tsx` 已传参。
### 13.2 验证证据
- `npm run typecheck` 通过(`tsconfig.typecheck-guardrails.json`)。
- `npx tsc -p apps/ai-game-creator-shell/tsconfig.json --noEmit` 通过。
- 定向 vitest:共享弹层 6/6;平台入口 22/22;图片编辑器顶栏 5/5;`usePlatformProfileCenterController.recharge` 8/8;AGC `accountHost` + `walletStore` 17/17。
- `npm run check:encoding` 通过(5249 个文件);`git diff --check` 通过。
- `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` 通过。
- ⚠ **未执行 `cargo check`**:仓库无 Rust 构建缓存,冷编译成本过高;新增 Tauri 命令只复用同文件既有 `require_session` / `build_client` / `bounded_business_id` / `request_json`,四个签名已逐一核对,风险集中在类型名拼写。
### 13.3 既有问题(非本次引入)
- `ImageCanvasEditorView.test.tsx > shows the breakdown read failure without inventing rows` 在改动前的基线同样失败(已用 `git stash` 对照验证):弹层打开时仍处于 loading 分支,断言未等待错误态。属既有时序问题,本次未修改该行为。
---
## 14. 追加变更:每日免费发放额为 0 时隐藏(2026-10-03)
### 14.1 语义
- **不引入** `retired` / 状态位 / 新字段;每日免费只是一个普通数值,发放额可以是 0。
- **唯一信号**:`ProfileMudPointBalance.dailyFreeResetPoints <= 0`(后端每日发放额,来自后台配置 `daily_free_points_per_day`)。
- **可逆**:后台把配置改回 > 0,前端下次读取自动恢复展示,无需额外开关。
- **契约不变**:`ProfileMudPointBalanceResponse` 字段仍是必填 `u64`,`0` 本就合法;不改 `shared-contracts`、ts-rs 生成物与 OpenAPI。
- **扣减与账本逻辑不变**:每日免费为 0 时不再产生发放账本(`profile.rs` 在 `amount_delta == 0` 时直接返回),创作扣点顺序仍为每日免费 → 月度 → 永久。
### 14.2 显示规则
```
showDailyFreePool = dailyFreePoints > 0 || dailyFreeResetPoints > 0
```
| 场景 | dailyFreePoints | dailyFreeResetPoints | 表现 |
|---|---|---|---|
| 当天正常用完 | 0 | 20 | 显示("每天重置为 20") |
| 每日免费发放额为 0 | 0 | 0 | 隐藏 |
| 配置中途改为 0,当天仍有存量 | > 0 | 0 | 显示(存量仍会被优先扣减,不能隐身) |
判定收敛到 `packages/shared/src/utils/mudPoints.ts` 的纯函数,三端共享组件共用,消费方 controller 不改。
### 14.3 改动点
- 后端 `server-rs/crates/module-runtime/src/commands.rs`:`daily_free_points_per_day` 由"必须 > 0"放宽为 `0..=i64::MAX`;`errors.rs` 对应文案同步。
- Admin Web `apps/admin-web/src/pages/AdminProfileWalletConfigPage.tsx`:每日免费泥点允许填 0。
- 共享层 `packages/shared/src/utils/mudPoints.ts`:新增 `shouldShowProfileDailyFreePool`。
- 共享层 `PlatformMudPointWalletEntry`:为 0 时隐藏"每日免费泥点"行。
- 共享层 `PlatformProfileRechargeModal`:池概览按可见池动态渲染(三列变两列)、隐藏扣点顺序提示中与每日免费相关的口径。
- 账本来源文案 `daily_free_grant` / `daily_free_reset` 保留,历史流水不改写。
### 14.4 测试
- 删除"每日免费必然存在"的断言(`PlatformMudPointWalletEntry`、`ImageCanvasTopbarView`、`ImageCanvasEditorView`),改为在共享层新增"发放额为 0 隐藏 / 有存量仍显示"的行为用例。
- 后端与 Admin 追加"允许 0"的用例。