文档(会员目录):明确懒播种策略与只读入口首次写库的副作用
- ensure_default_profile_membership_plan 注释说明 init 与读取 helper 共用、目录空时首次读取会写库 - init_profile_membership_plan_catalog 注释改为不覆盖「表已存在但为空」的存放量库 - 报价与建单注释去掉「不写库」的绝对说法,注明目录空时的懒播种例外 - 同步设计文档 7 / 10.2 与决策日志(播种策略定为保留懒播种 + 明示副作用)
This commit is contained in:
@@ -9594,3 +9594,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策(行为变化):`会员目录缺少档位 ...` 与「已迁移的会员商品」原先落 502,现在明确 400;兑换码链路本轮未改造,仍保留一个文案匹配函数。procedure result 新增字段属 wire 契约变更,要求 module 与 api-server 同版本部署。
|
||||
- 影响范围:`server-rs/crates/module-runtime/src/domain.rs`、`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`、`server-rs/crates/spacetime-client/src/{active.rs,active/mapper/runtime_profile.rs}`、生成绑定、`server-rs/crates/api-server/src/{runtime_profile.rs,asset_billing.rs,editor_project.rs}`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
|
||||
- 验证:`npm run spacetime:generate`;`cargo check --workspace`;`api-server runtime_profile::tests` 41 passed;`module-runtime --lib membership::` 39 passed;`npm run check:encoding`、`npm run check:spacetime-schema`、`cargo fmt -- --check`、`git diff --check` 通过。
|
||||
|
||||
## 2026-10-03 会员目录播种策略定为「保留懒播种 + 明示副作用」
|
||||
|
||||
- 背景:评审第 20 项指出「只读」procedure(报价 / 充值中心 / 后台查询)首次读取时会经 `ensure_default_profile_membership_plan` 写库,与代码注释「不写库」矛盾。三条候选:①保留懒播种并承认副作用;②只靠 init + 发布期 migration 播种;③只读投影不落表、内存回退代码内目录。
|
||||
- 决策:选 ①。`init_profile_membership_plan_catalog` 只在数据库首次创建时执行,增量发布不重跑;「表已存在但为空」的存量库必须靠读取 helper 懒播种兜底,否则目录为空会让下单 / 报价失败。保留懒播种,并把「只读入口在目录为空的首次调用会写库、表非空后幂等只读」明确写进代码注释与设计文档 §7,消除注释与实现的矛盾。
|
||||
- 未选 ②/③ 的原因:②依赖发布流程保证存量库被种上,当前没有这条保证;③改动读路径回退逻辑,风险与收益不匹配。将来若做发布期 migration,再删除懒播种。
|
||||
- 影响范围:`server-rs/crates/spacetime-module/src/runtime/active/profile.rs`(`ensure_default_profile_membership_plan` / `init_profile_membership_plan_catalog` / `membership_plan_row` / `membership_plan_records` / 报价与建单函数注释)、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §7/§10.2、本文件。
|
||||
|
||||
@@ -471,6 +471,7 @@ flowchart TD
|
||||
- 旧充点行不删除(复用 id 并去掉赠送);首充资格与历史订单继续可读。
|
||||
- 旧会员档位枚举与会员商品行**删除**(会员侧未上线,无需兼容)。
|
||||
- **不做** `tier → plan` 回填;若实施时发现意外存量会员行,先停下评估再补。
|
||||
- 会员目录播种(第 20 项结论 = 保留懒播种):`init` 只在首次创建库时跑,增量发布不重跑;「表存在但为空」的存量库靠读取 helper 懒播种,代价是只读入口首次调用会写库。若要彻底移除副作用,需先把播种改到发布期 migration,再删懒播种——当前不做。
|
||||
- 钱包侧(总额、每日免费、永久余数、账本)口径不允许漂移;充点订单不允许破坏性变更。
|
||||
|
||||
---
|
||||
@@ -528,7 +529,11 @@ flowchart TD
|
||||
- 会员领域模块 `module-runtime/src/membership/{catalog,cycle,limits,upgrade}.rs`,公历基元抽为 `civil_calendar`。
|
||||
- 账本来源枚举末尾追加 `MembershipUpgradeGrant`,并同步契约常量、后台索引映射、前端来源标签与生成绑定。
|
||||
- 新增表 `profile_membership_plan`(主键为档位枚举 `RuntimeProfileMembershipPlan`),同步 `migration.rs`、表目录与生成绑定;
|
||||
播种 `ensure_default_profile_membership_plan`:表为空时按代码内目录整表播种,表非空时保留后台改价。
|
||||
播种策略:表为空时按代码内目录整表播种,表非空时保留后台改价。
|
||||
`#[reducer(init)] init_profile_membership_plan_catalog` 只在数据库首次创建时执行一次(增量发布不重跑),
|
||||
因此「表已存在但为空」的存量库由读取 helper `membership_plan_row` / `membership_plan_records` 里的
|
||||
`ensure_default_profile_membership_plan` 懒播种兜底。**副作用**:报价 / 充值中心 / 后台查询等只读入口在
|
||||
目录为空的首次调用会写库,表非空后保持只读;这是有意保留的策略,见 §7。
|
||||
- 删列与清退:`profile_membership` 去掉 `tier` / `cycle_period_days`、追加
|
||||
`plan` / `cycle_index` / `cycle_count` / `cycle_kind`;`profile_recharge_product_config` 去掉
|
||||
`tier` 与四个 `membership_*` 字段;`RuntimeProfileMembershipTier`、会员权益矩阵、
|
||||
|
||||
@@ -8404,9 +8404,12 @@ fn list_profile_membership_plan_records(
|
||||
|
||||
/// 补差升级报价。非会员、降档、月年互换、当前套餐重复购买都返回可读错误,不做静默兜底。
|
||||
///
|
||||
/// 全程只读:不刷新账期 / 钱包(不写库、不发账本),改用 [`project_profile_membership_cycle_at`]
|
||||
/// 全程不刷新账期 / 钱包(不写会员行、不发账本),改用 [`project_profile_membership_cycle_at`]
|
||||
/// 在内存里把会员行投射到当前时刻再报价,因此与建单、结算读到的账期口径一致,
|
||||
/// 也不会让「预览报价」提前消耗用户的懒刷新。
|
||||
///
|
||||
/// 例外:读取档位目录走 [`membership_plan_snapshot`] → [`membership_plan_row`],若目录表尚为空
|
||||
/// (存量库增量发布后首次读取)会触发懒播种写库;表非空后保持只读。
|
||||
fn build_profile_membership_upgrade_quote_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileMembershipUpgradeQuoteGetInput,
|
||||
@@ -9049,6 +9052,11 @@ fn ensure_default_profile_task_config(ctx: &ReducerContext) -> ProfileTaskConfig
|
||||
}
|
||||
|
||||
/// 中文注释:会员档位目录以代码内目录为种子真相源,表为空时整表播种一次;后台改价后不再覆盖。
|
||||
///
|
||||
/// 播种策略(已定):`init` reducer 与读取 helper(`membership_plan_row` / `membership_plan_records`)
|
||||
/// 共用本函数。`init` 只在数据库首次创建时执行,增量发布不会重跑,因此「表已存在但为空」的存放量库
|
||||
/// 仍要靠读取 helper 懒播种。**代价**:报价 / 充值中心 / 后台查询等只读入口在目录为空的首次调用会写库;
|
||||
/// 表非空后本函数直接返回,后续调用保持只读。若将来改为发布期统一播种,再删除这里的懒播种与注释。
|
||||
fn ensure_default_profile_membership_plan(ctx: &ReducerContext) {
|
||||
if ctx.db.profile_membership_plan().count() > 0 {
|
||||
return;
|
||||
@@ -9078,8 +9086,10 @@ fn ensure_default_profile_membership_plan(ctx: &ReducerContext) {
|
||||
|
||||
/// 模块初始化生命周期:只在数据库首次创建时把会员档位目录整表播种。
|
||||
///
|
||||
/// 迁移目的:报价 / 充值中心 / 后台查询等只读入口不再调用 `ensure_default_profile_membership_plan`
|
||||
/// 写库;存量数据库此前已由懒播种写入了目录行,新建数据库则由本 reducer 播种。
|
||||
/// 本 reducer **不覆盖**「目录表已存在但为空」的存放量库:增量发布不会重跑 `init`。这类库由
|
||||
/// 读取 helper 里的 [`ensure_default_profile_membership_plan`] 懒播种兜底,因此报价 / 充值中心 /
|
||||
/// 后台查询等「只读」入口首次调用仍可能写库(表非空后幂等只读)。这是当前有意保留的播种策略,
|
||||
/// 不是漏改;若改为发布期统一播种,需同步删除懒播种调用并更新本注释。
|
||||
#[spacetimedb::reducer(init)]
|
||||
pub fn init_profile_membership_plan_catalog(ctx: &ReducerContext) {
|
||||
ensure_default_profile_membership_plan(ctx);
|
||||
@@ -9464,7 +9474,8 @@ fn membership_plan_row(
|
||||
ctx: &ReducerContext,
|
||||
plan: RuntimeProfileMembershipPlan,
|
||||
) -> Option<ProfileMembershipPlan> {
|
||||
// 存量库增量发布不会触发 init reducer;首次读取时懒播种,避免目录为空导致下单 / 报价失败。
|
||||
// 存量库增量发布不会触发 init reducer;首次读取时懒播种(会写库),避免目录为空导致下单 / 报价失败;
|
||||
// 表非空后本调用保持只读。
|
||||
ensure_default_profile_membership_plan(ctx);
|
||||
ctx.db.profile_membership_plan().plan().find(&plan)
|
||||
}
|
||||
@@ -9507,7 +9518,8 @@ fn membership_plan_snapshot(
|
||||
|
||||
/// 充值中心与后台共用的目录行读取,按 `rank` 升序;表为空时先补种代码内目录。
|
||||
fn membership_plan_records(ctx: &ReducerContext) -> Vec<RuntimeProfileMembershipPlanRecord> {
|
||||
// 存量库增量发布不会触发 init reducer;首次读取时懒播种,避免充值中心目录为空。
|
||||
// 存量库增量发布不会触发 init reducer;首次读取时懒播种(会写库),避免充值中心目录为空;
|
||||
// 表非空后本调用保持只读。
|
||||
ensure_default_profile_membership_plan(ctx);
|
||||
let mut rows = ctx.db.profile_membership_plan().iter().collect::<Vec<_>>();
|
||||
rows.sort_by(|left, right| {
|
||||
@@ -9683,7 +9695,10 @@ fn resolve_enabled_profile_recharge_order_product(
|
||||
///
|
||||
/// 会员订单应付金额:首次购买按目录价;有效期内升级按补差报价重算。
|
||||
/// 计算补差前先用 [`project_profile_membership_cycle_at`] 只读地把账期投射到建单时刻,
|
||||
/// 不用过期账期、也不写库,因此建单金额与「预览报价」「支付结算」三处口径一致。
|
||||
/// 不用过期账期,因此建单金额与「预览报价」「支付结算」三处口径一致。
|
||||
///
|
||||
/// 例外:读取档位目录同样走懒播种,目录表为空的首次调用会写库(见
|
||||
/// [`ensure_default_profile_membership_plan`] 的播种策略说明)。
|
||||
fn build_profile_membership_order_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user