Feat/陶泥儿会员订阅方案 #588
@@ -9687,7 +9687,7 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
## 2026-10-03 会员链路错误分类改为机器可读错误码(typed error)
|
||||
|
||||
- 背景:`api-server/src/runtime_profile.rs` 的 `is_runtime_profile_membership_domain_error` 用中文 `error_message` 前缀 / 精确匹配决定返回 400 还是 502;文案一改(或新增拒绝点)就静默退化,且分类逻辑与 module 的错误文案跨层重复。
|
||||
- 决策(错误码随 procedure 结果透传):`module-runtime` 新增 `RuntimeProfileMembershipErrorCode`(`UpgradeRejected` / `NotMember` / `AlreadyActive` / `CycleKindLocked` / `MembershipExpired` / `StateChanged` / `PlanNotPurchasable` / `PlanCatalogMissing` / `ProductIdUnparsable` / `MembershipProductRetired` / `InvalidPlanConfig`)与内部 `RuntimeProfileMembershipDomainError { code, message }`;三个会员 procedure 结果(升级报价、充值中心 / 下单、后台档位 upsert)末尾追加 `error_code: Option<...>`,拒绝点显式赋码,不再做字符串分类。
|
||||
- 决策(错误码随 procedure 结果透传):`module-runtime` 新增 `RuntimeProfileMembershipErrorCode`(`UpgradeRejected` / `NotMember` / `AlreadyActive` / `CycleKindLocked` / `MembershipExpired` / `StateChanged` / `PlanNotPurchasable` / `PlanCatalogMissing` / `ProductIdUnparseable` / `MembershipProductRetired` / `InvalidPlanConfig`)与内部 `RuntimeProfileMembershipDomainError { code, message }`;三个会员 procedure 结果(升级报价、充值中心 / 下单、后台档位 upsert)末尾追加 `error_code: Option<...>`,拒绝点显式赋码,不再做字符串分类。
|
||||
- 决策(客户端错误类型):`spacetime-client` 新增 `SpacetimeClientError::ProcedureRejected { code, message }`;`api-server` 据此返回 400(provider `runtime-profile`),基础设施错误保持 502。字段校验类错误不产码,仍落 502。
|
||||
- 决策(行为变化):`会员目录缺少档位 ...` 与「已迁移的会员商品」原先落 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、本文件。
|
||||
@@ -9750,3 +9750,10 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策:只给**响应** DTO 的 token 字段挂 `#[serde(deserialize_with = "...")]` 做前向兼容,未知取值 fail-open 兜底到已知最高档(plan→`max`、status→`active`、cycle→`yearly`、model_access→`full`;真实权益 / 下单金额以后端重算为准,避免把付费会员显示成非会员后引导重复购买)。**入参** DTO 不改,非法 token 仍在反序列化即拒;不改枚举本身的 `Deserialize`,避免 `Unknown` 变体污染入参与穷尽 match。
|
||||
- 影响范围:`server-rs/crates/shared-contracts/src/runtime.rs`(4 个 helper + `ProfileMembershipPlanResponse` / `ProfileMembershipResponse` / `ProfileMembershipUpgradeQuoteResponse` 的 8 个字段)、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §2.5。后台 DTO(`AdminProfileMembershipPayload` / `AdminMembershipOrderChangePayload`)只有 TS 消费方,不做 Rust 侧处理。
|
||||
- 验证:`cargo test -p shared-contracts --lib` 106 passed(含 `membership_response_tokens_tolerate_unknown_values_from_newer_backend`、`membership_request_tokens_stay_strict_for_unknown_values`)。
|
||||
|
||||
## 2026-10-03 会员错误码 `ProductIdUnparsable` 拼写修正(未上线,直接破坏性改名)
|
||||
|
||||
- 背景:`RuntimeProfileMembershipErrorCode::ProductIdUnparsable` 拼写有误,生成绑定与对外 `error.code = "product_id_unparsable"` 同错。
|
||||
- 决策:该 PR 尚未上线,不留兼容,直接改名为 `ProductIdUnparseable`,wire 串同步改为 `"product_id_unparseable"`;重新生成 SpacetimeDB 绑定。
|
||||
- 影响范围:`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/mapper/runtime_profile.rs,module_bindings/runtime_profile_membership_error_code_type.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、设计文档 §5.8、本文件(含上文旧条目中的同名标识符同步更正)。
|
||||
- 验证:`cargo test -p module-runtime --lib` 114 passed;`cargo test -p spacetime-client` 32 passed;`cargo test -p api-server runtime_profile::tests` 43 passed;`cargo test -p spacetime-module` 271 passed。
|
||||
|
||||
@@ -450,7 +450,7 @@ flowchart TD
|
||||
`error_code` 透传。
|
||||
- 会员链路:`module-runtime` 的 `RuntimeProfileMembershipErrorCode`(`UpgradeRejected` /
|
||||
`NotMember` / `AlreadyActive` / `CycleKindLocked` / `MembershipExpired` / `StateChanged` /
|
||||
`PlanNotPurchasable` / `PlanCatalogMissing` / `ProductIdUnparsable` /
|
||||
`PlanNotPurchasable` / `PlanCatalogMissing` / `ProductIdUnparseable` /
|
||||
`MembershipProductRetired` / `InvalidPlanConfig`)在拒绝点显式产出,
|
||||
`RuntimeProfileMembershipUpgradeQuoteProcedureResult` /
|
||||
`RuntimeProfileRechargeCenterProcedureResult` / `RuntimeProfileMembershipPlanAdminProcedureResult`
|
||||
|
||||
@@ -2645,7 +2645,7 @@ mod tests {
|
||||
"会员升级被拒绝: cycle_kind_change_unsupported",
|
||||
),
|
||||
(
|
||||
RuntimeProfileMembershipErrorCode::ProductIdUnparsable,
|
||||
RuntimeProfileMembershipErrorCode::ProductIdUnparseable,
|
||||
"会员商品 product_id 无法解析: membership-pro-yearly",
|
||||
),
|
||||
(
|
||||
|
||||
@@ -1503,7 +1503,7 @@ pub enum RuntimeProfileMembershipErrorCode {
|
||||
/// 代码内档位目录缺少该档位。
|
||||
PlanCatalogMissing,
|
||||
/// 会员商品 product_id 无法解析。
|
||||
ProductIdUnparsable,
|
||||
ProductIdUnparseable,
|
||||
/// 会员商品已迁移到档位目录,旧商品不再可用。
|
||||
MembershipProductRetired,
|
||||
/// 后台会员档位改价 / 改权益配置校验失败。
|
||||
@@ -1521,7 +1521,7 @@ impl RuntimeProfileMembershipErrorCode {
|
||||
Self::StateChanged => "state_changed",
|
||||
Self::PlanNotPurchasable => "plan_not_purchasable",
|
||||
Self::PlanCatalogMissing => "plan_catalog_missing",
|
||||
Self::ProductIdUnparsable => "product_id_unparsable",
|
||||
Self::ProductIdUnparseable => "product_id_unparseable",
|
||||
Self::MembershipProductRetired => "membership_product_retired",
|
||||
Self::InvalidPlanConfig => "invalid_plan_config",
|
||||
}
|
||||
|
||||
@@ -2121,8 +2121,8 @@ pub(crate) fn map_runtime_profile_membership_error_code_back(
|
||||
crate::module_bindings::RuntimeProfileMembershipErrorCode::PlanCatalogMissing => {
|
||||
module_runtime::RuntimeProfileMembershipErrorCode::PlanCatalogMissing
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileMembershipErrorCode::ProductIdUnparsable => {
|
||||
module_runtime::RuntimeProfileMembershipErrorCode::ProductIdUnparsable
|
||||
crate::module_bindings::RuntimeProfileMembershipErrorCode::ProductIdUnparseable => {
|
||||
module_runtime::RuntimeProfileMembershipErrorCode::ProductIdUnparseable
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileMembershipErrorCode::MembershipProductRetired => {
|
||||
module_runtime::RuntimeProfileMembershipErrorCode::MembershipProductRetired
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ pub enum RuntimeProfileMembershipErrorCode {
|
||||
|
||||
PlanCatalogMissing,
|
||||
|
||||
ProductIdUnparsable,
|
||||
ProductIdUnparseable,
|
||||
|
||||
MembershipProductRetired,
|
||||
|
||||
|
||||
@@ -10040,7 +10040,7 @@ fn build_profile_membership_order_snapshot(
|
||||
let (plan, cycle_kind) =
|
||||
parse_runtime_profile_membership_product_id(product_id).ok_or_else(|| {
|
||||
RuntimeProfileMembershipDomainError::new(
|
||||
RuntimeProfileMembershipErrorCode::ProductIdUnparsable,
|
||||
RuntimeProfileMembershipErrorCode::ProductIdUnparseable,
|
||||
format!("会员商品 product_id 无法解析: {product_id}"),
|
||||
)
|
||||
})?;
|
||||
@@ -10678,7 +10678,7 @@ fn build_recomputed_profile_membership_settlement(
|
||||
let (plan, cycle_kind) =
|
||||
parse_runtime_profile_membership_product_id(product_id).ok_or_else(|| {
|
||||
RuntimeProfileMembershipDomainError::new(
|
||||
RuntimeProfileMembershipErrorCode::ProductIdUnparsable,
|
||||
RuntimeProfileMembershipErrorCode::ProductIdUnparseable,
|
||||
format!("会员商品 product_id 无法解析: {product_id}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
Reference in New Issue
Block a user