diff --git a/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md b/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md index b8a7f00ac..d837d45b2 100644 --- a/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md +++ b/docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md @@ -181,16 +181,6 @@ pub struct ProfileMembershipPlan { **回退为「保留列 + 写死默认值 + 运行时不读」**,并在生成绑定与表目录里明确标注为废弃残留。 两种路径都必须先让 schema 门禁通过,不得使用 `--delete-data=always` 绕过。 -**实测结论(2026-10-03,SpacetimeDB 2.8.3 本地 standalone)**:持久表删列会被发布检查直接拒绝, -报 `Removing a column ... requires a manual migration`,仅传 `--delete-data=never --yes=migrate,break-clients` -不足以放行;`--delete-data=on-conflict` 则会清空整库。保留列的回退未采用,改用仓库既有的显式迁移路径: -`scripts/deploy-rust-remote.sh` 的 `run_conflict_migration_publish`(默认 `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=true`) -会 导出旧库 → 清库发布新 wasm → 回灌迁移 JSON,因此不需要新增独立迁移脚本。 -旧迁移包缺少新增列的导入兼容已在 `server-rs/crates/spacetime-module/src/migration.rs` 的 -`normalize_migration_row` 补齐(`profile_membership` 回填 `plan` / 期数默认值;`profile_recharge_order` -回填 14 个 `membership_*` 快照默认值),并由同名单元测试守护。删列本身仍受本分支的 -`SPACETIME_SCHEMA_GUARD_ALLOW_BREAKING=1` schema 门禁确认。 - ### 2.8 会员侧遗留清理清单 实施时**删除**(不是废弃),并同步修掉引用它们的测试: diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 93f08ce41..eeb525c85 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -727,7 +727,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - Rust 结构体:`ProfileMembership` - 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs` -- 作用:会员有效期和当前周期限时泥点事实源。`started_at/expires_at` 表示会员有效期,`cycle_started_at/cycle_resets_at` 表示当前周期,`cycle_granted_points/cycle_remaining_points` 表示当前周期已发放和剩余限时泥点;`plan` / `cycle_index` / `cycle_count` / `cycle_kind` 表示会员档位(`RuntimeProfileMembershipPlan` 枚举)与自然月期数。旧 `tier` 与固定 30 天的 `cycle_period_days` 已随会员档位枚举化删除(会员未上线,属经用户授权的破坏性变更;迁移导入按 `migration.rs` 的 `profile_membership` 兼容分支回填默认值)。 +- 作用:会员有效期和当前周期限时泥点事实源。`started_at/expires_at` 表示会员有效期,`cycle_started_at/cycle_resets_at/cycle_period_days` 表示当前周期,`cycle_granted_points/cycle_remaining_points` 表示当前周期已发放和剩余限时泥点。 ### `profile_membership_plan` @@ -742,8 +742,8 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - Rust 结构体:`ProfileRechargeProductConfig` - 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs` -- 作用:泥点充值商品配置真相源,供充值中心展示、下单校验、支付确认和后台“充值商品”页维护;当前公开充值中心展示六档泥点商品,会员档位与价格改由 `profile_membership_plan` 目录驱动。 -- 字段补充:原先给会员商品用的 `tier` 与 `membership_period_points` / `membership_period_days` / `membership_queue_limit` / `membership_discount_bps` 已随会员档位枚举化删除;旧迁移包里的这些列在导入时按未知字段丢弃。 +- 作用:泥点和会员充值商品配置真相源,供充值中心展示、下单校验、支付确认和后台“充值商品”页维护;当前公开充值中心只展示四档泥点商品,会员商品配置留存但不公开购买或升级入口。 +- 字段补充:会员商品追加 `membership_period_points`、`membership_period_days`、`membership_queue_limit`、`membership_discount_bps`;泥点商品这些字段必须为 `0`。 ### `profile_played_world` @@ -754,7 +754,7 @@ Responses 的终态载荷既是工具调用的恢复源,也是正文的恢复 - Rust 结构体:`ProfileRechargeOrder` - 源码:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs` -- 作用:账户充值订单事实源。`status` 包含 `pending`、`paid`、`failed`、`closed`、`refunded`、`expired`;过期补偿字段 `expired_at`、`expiration_checked_at`、`expiration_provider_state`、`expiration_last_error` 用于记录本地过期和微信查单结果。末位追加的 `membership_*` 字段保存会员购买 / 升级前后的档位、周期、每期额度、到期与刷新时间快照、补点差额和报价明细,供人工复核退款;旧迁移包缺少这些列时由 `migration.rs` 的 `profile_recharge_order` 兼容分支回填为空 / `0`。 +- 作用:账户充值订单事实源。`status` 包含 `pending`、`paid`、`failed`、`closed`、`refunded`、`expired`;过期补偿字段 `expired_at`、`expiration_checked_at`、`expiration_provider_state`、`expiration_last_error` 用于记录本地过期和微信查单结果。 ### `profile_recharge_refund` diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index eeb4dcf00..5018f86d8 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -1284,67 +1284,6 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde object .entry("expiration_last_error".to_string()) .or_insert(serde_json::Value::Null); - // 中文注释:会员购买 / 升级变更快照晚于订单表加入,旧迁移包按普通充点订单兼容。 - object - .entry("membership_change_kind".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_plan_before".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_plan_after".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_cycle_kind_before".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_cycle_kind_after".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_period_points_before".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_period_points_after".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_granted_points_delta".to_string()) - .or_insert_with(|| serde_json::Value::from(0i64)); - object - .entry("membership_cycle_index".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_expires_at_before".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_expires_at_after".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_cycle_resets_at_before".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_cycle_resets_at_after".to_string()) - .or_insert(serde_json::Value::Null); - object - .entry("membership_price_breakdown_json".to_string()) - .or_insert(serde_json::Value::Null); - } - } - if table_name == "profile_membership" { - if let Some(object) = next_value.as_object_mut() { - // 中文注释:会员档位改枚举、账期改自然月后新增 plan / cycle_index / cycle_count / cycle_kind。 - // 会员侧未上线、无存量行;旧迁移包若带旧 `tier` / `cycle_period_days`,反序列化时会按未知字段丢弃。 - object - .entry("plan".to_string()) - .or_insert_with(|| serde_json::json!({ "Normal": [] })); - object - .entry("cycle_index".to_string()) - .or_insert_with(|| serde_json::Value::from(0u32)); - object - .entry("cycle_count".to_string()) - .or_insert_with(|| serde_json::Value::from(1u32)); - object - .entry("cycle_kind".to_string()) - .or_insert_with(|| serde_json::json!({ "Monthly": [] })); } } if table_name == "external_generation_job_summary" { @@ -1612,84 +1551,6 @@ mod migration_bootstrap_secret_tests { assert_eq!(normalized["asset_kind_override"], serde_json::Value::Null); } - #[test] - fn old_profile_membership_rows_default_to_normal_plan_and_first_month() { - let normalized = normalize_migration_row( - "profile_membership", - &serde_json::json!({ "user_id": "user-1", "tier": { "Month": [] } }), - ); - - assert_eq!(normalized["plan"], serde_json::json!({ "Normal": [] })); - assert_eq!(normalized["cycle_index"], serde_json::json!(0)); - assert_eq!(normalized["cycle_count"], serde_json::json!(1)); - assert_eq!( - normalized["cycle_kind"], - serde_json::json!({ "Monthly": [] }) - ); - } - - #[test] - fn old_profile_recharge_order_rows_default_to_plain_point_order_snapshot() { - let normalized = normalize_migration_row( - "profile_recharge_order", - &serde_json::json!({ "order_id": "order-1" }), - ); - - assert_eq!( - normalized["membership_change_kind"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_plan_before"], - serde_json::Value::Null - ); - assert_eq!(normalized["membership_plan_after"], serde_json::Value::Null); - assert_eq!( - normalized["membership_cycle_kind_before"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_cycle_kind_after"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_period_points_before"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_period_points_after"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_granted_points_delta"], - serde_json::json!(0) - ); - assert_eq!( - normalized["membership_cycle_index"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_expires_at_before"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_expires_at_after"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_cycle_resets_at_before"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_cycle_resets_at_after"], - serde_json::Value::Null - ); - assert_eq!( - normalized["membership_price_breakdown_json"], - serde_json::Value::Null - ); - } - #[test] fn bootstrap_secret_sha256_is_stable_and_does_not_contain_plaintext() { let secret = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";