修复(后台兑换码): 管理拒绝补机器可读错误码走 400
Project CI / Backend tests (pull_request) Failing after 53s
Project CI / Native shell tests (pull_request) Failing after 3m51s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m25s
Project CI / Frontend tests (pull_request) Failing after 48s
Project CI / AI game creator shell web tests (pull_request) Failing after 43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m17s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m6s
Project CI / Repository checks (pull_request) Successful in 4m37s
Project CI / Backend tests (pull_request) Failing after 53s
Project CI / Native shell tests (pull_request) Failing after 3m51s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 5m25s
Project CI / Frontend tests (pull_request) Failing after 48s
Project CI / AI game creator shell web tests (pull_request) Failing after 43s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 6m17s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 7m6s
Project CI / Repository checks (pull_request) Successful in 4m37s
- module-runtime 新增 RuntimeProfileRedeemCodeAdminErrorCode(not_found / invalid_field / allowed_users_required),后台兑换码增改与列表 procedure 结果末尾追加 error_code - module 侧用 ProfileRedeemCodeAdminProcedureError 在拒绝点赋码,字段校验统一 invalid_field、私有码缺用户 allowed_users_required - spacetime-client 后台 mapper 有码走 ProcedureRejected,api-server 由此返回 400 而不是 502 - 重新生成 SpacetimeDB 绑定;补 client 与 api-server 单测;同步设计文档 §5.8 与决策日志
This commit is contained in:
@@ -9719,3 +9719,12 @@ CI 上 `background_agent_runtime_recovers_stale_running_before_pending_task` 在
|
||||
- 决策(结算归一,20):`frozen_membership_cycle_matches` 比较双方都取 `max(1)`,修复 `cycle_index == 0` 迁移行被永久判成「会员状态已变化」的问题;不新增写库副作用。
|
||||
- 影响范围:`shared-contracts/{runtime,admin}.rs`、`module-runtime/{domain,commands,errors}.rs` 与 `module-runtime/src/{membership/upgrade,membership/cycle,agc_model_access}.rs`、`spacetime-client/{active,active/mapper/runtime_profile,module_bindings}`、`spacetime-module/src/runtime/active/profile.rs`、`api-server/src/{runtime_profile,llm/mod,agc_models}.rs`、`packages/shared/src/contracts/runtime.ts`、`apps/admin-web/src/api/adminApiTypes.ts`、`apps/admin-web/src/pages/AdminRechargeProductPage.tsx`、`scripts/admin-web-fake-api.mjs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、本文件。
|
||||
- 验证:`cargo test -p api-server` 1206 passed;`-p module-runtime` / `-p spacetime-client` / `-p spacetime-module` 全通过;`npm run spacetime:generate`、`npm run check:spacetime-schema`(92 表)、`npm run check:generated-bindings`(117 文件)、`npm run admin-web:typecheck`、`npm run typecheck`、`npm run check:encoding`、`git diff --check` 通过。
|
||||
|
||||
## 2026-10-03 后台兑换码管理补机器可读错误码(typed error 收口)
|
||||
|
||||
- 背景:`bd06e4269` 删除中文文案匹配后,`admin_disable_profile_redeem_code` / `admin_upsert_profile_redeem_code` / `admin_list_profile_redeem_codes` 仍只回传裸 `error_message`。这些 procedure 的 `ok = false` 是明确的业务拒绝(停用不存在的码、入参非法、私有码缺可兑换用户),却被 `spacetime-client` 拍平成 `Procedure(String)`,在 `api-server` 落 502,与会员 / 核销两条链路已落地的 typed error 不一致。
|
||||
- 决策(module 侧赋码):`module-runtime` 新增 `RuntimeProfileRedeemCodeAdminErrorCode`(`NotFound` / `InvalidField` / `AllowedUsersRequired`);`RuntimeProfileRedeemCodeAdminProcedureResult` 与 `RuntimeProfileRedeemCodeAdminListProcedureResult` 末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileRedeemCodeAdminProcedureError::{Classified, Plain}` 在拒绝点显式赋码,`RuntimeProfileFieldError` 统一归 `invalid_field`,私有码缺可兑换用户归 `allowed_users_required`,停用不存在的码归 `not_found`。
|
||||
- 决策(客户端 / BFF):`RuntimeProfileProcedureRejectionCode` 增加 `RedeemCodeAdmin` 聚合变体,`spacetime-client` 两个后台 mapper 有 `error_code` 时走 `ProcedureRejected`、缺失时保持 `Procedure`;`api-server` 复用既有 `map_runtime_profile_client_error`,无需再引入字符串分类。
|
||||
- 决策(行为变化):后台兑换码的 `兑换码不存在`、入参校验失败、私有码缺可兑换用户由 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,module_bindings,module_bindings.rs}`、`server-rs/crates/api-server/src/runtime_profile.rs`、`docs/technical/【技术设计】泥点三池与会员计费后端设计-2026-10-02.md` §5.8、本文件。
|
||||
- 验证:`npm run spacetime:generate`;`cargo check`(module-runtime / spacetime-module / spacetime-client / api-server);`cargo test -p spacetime-client` 30 passed;`cargo test -p api-server runtime_profile::tests` 42 passed;`cargo test -p module-runtime --lib` 113 passed;`npm run check:spacetime-schema`、`npm run check:encoding`、`git diff --check` 通过。
|
||||
|
||||
@@ -451,18 +451,24 @@ flowchart TD
|
||||
`RuntimeProfileMembershipUpgradeQuoteProcedureResult` /
|
||||
`RuntimeProfileRechargeCenterProcedureResult` / `RuntimeProfileMembershipPlanAdminProcedureResult`
|
||||
末尾追加 `error_code: Option<RuntimeProfileMembershipErrorCode>`(`error_message` 保留人读文案)。
|
||||
- 兑换码链路:`module-runtime` 的 `RuntimeProfileRewardCodeRedeemErrorCode`(`NotFound` /
|
||||
- 兑换码核销链路:`module-runtime` 的 `RuntimeProfileRewardCodeRedeemErrorCode`(`NotFound` /
|
||||
`Disabled` / `NotStarted` / `Expired` / `UsesExhausted` / `NotAllowedForUser` /
|
||||
`InvalidReward`)在拒绝点显式产出,`RuntimeProfileRewardCodeRedeemProcedureResult` 末尾同样追加
|
||||
`error_code`。
|
||||
- 后台兑换码管理链路:`module-runtime` 的 `RuntimeProfileRedeemCodeAdminErrorCode`(`NotFound` /
|
||||
`InvalidField` / `AllowedUsersRequired`)在拒绝点显式产出,
|
||||
`RuntimeProfileRedeemCodeAdminProcedureResult` / `RuntimeProfileRedeemCodeAdminListProcedureResult`
|
||||
末尾追加 `error_code`;字段校验错误统一归 `invalid_field`,私有码缺可兑换用户归
|
||||
`allowed_users_required`,停用不存在的码归 `not_found`。
|
||||
- `spacetime-client` 的 `SpacetimeClientError::ProcedureRejected { code, message }` 用
|
||||
`RuntimeProfileProcedureRejectionCode` 聚合两条链路的 typed code;`api-server` 的 `runtime_profile`
|
||||
`RuntimeProfileProcedureRejectionCode` 聚合三条链路的 typed code;`api-server` 的 `runtime_profile`
|
||||
映射直接读变体字段返回 400(`provider = runtime-profile`),`AppError.code` 取
|
||||
`code.as_str()`,基础设施错误保持 502。字段校验类错误(如 user_id 缺失)不产出 `error_code`,
|
||||
仍按字符串错误落到 502。
|
||||
- 行为变化:`会员目录缺少档位 ...`、「已迁移的会员商品」以及兑换码的 `兑换码不存在` / `已停用` /
|
||||
`未生效` / `已过期` / `次数已用完` / `不适用于当前账号` / `奖励无效` 原先落到 502,现在有明确
|
||||
错误码,统一 400。`api-server` 已删除 `is_runtime_profile_redeem_code_domain_error` 文案匹配函数。
|
||||
`code.as_str()`,基础设施错误保持 502。会员 / 核销链路里未列出的字段校验类错误(如 user_id
|
||||
缺失)不产出 `error_code`,仍按字符串错误落到 502。
|
||||
- 行为变化:`会员目录缺少档位 ...`、「已迁移的会员商品」、兑换码核销的 `兑换码不存在` /
|
||||
`已停用` / `未生效` / `已过期` / `次数已用完` / `不适用于当前账号` / `奖励无效`,以及后台兑换码
|
||||
管理的 `兑换码不存在` / 入参校验失败 / 私有码缺可兑换用户原先落到 502,现在有明确错误码,统一
|
||||
400。`api-server` 已删除 `is_runtime_profile_redeem_code_domain_error` 文案匹配函数。
|
||||
- 兼容边界:procedure result 是 wire 结构,新增字段要求 module 与 api-server 同版本部署;
|
||||
旧 module 不返回 `error_code` 时,新 client 绑定反序列化会失败。
|
||||
|
||||
|
||||
@@ -2490,8 +2490,8 @@ mod tests {
|
||||
RuntimeProfileMembershipRecord, RuntimeProfileMembershipStatus,
|
||||
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
|
||||
RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductKind,
|
||||
RuntimeProfileRechargeProductRecord, RuntimeProfileRewardCodeRedeemErrorCode,
|
||||
RuntimeProfileWalletLedgerSourceType,
|
||||
RuntimeProfileRechargeProductRecord, RuntimeProfileRedeemCodeAdminErrorCode,
|
||||
RuntimeProfileRewardCodeRedeemErrorCode, RuntimeProfileWalletLedgerSourceType,
|
||||
};
|
||||
|
||||
use super::{
|
||||
@@ -2574,6 +2574,38 @@ mod tests {
|
||||
assert_eq!(legacy_string_error.status_code(), StatusCode::BAD_GATEWAY);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn redeem_code_admin_rejections_map_to_bad_request() {
|
||||
// 后台兑换码管理的业务拒绝同样随 procedure 透传 error_code;「停用不存在的码」这类
|
||||
// 拒绝必须落 400,不能因为缺少机器码被当成 SpacetimeDB 基础设施故障落 502。
|
||||
for (code, expected_code, message) in [
|
||||
(
|
||||
RuntimeProfileRedeemCodeAdminErrorCode::NotFound,
|
||||
"not_found",
|
||||
"兑换码不存在",
|
||||
),
|
||||
(
|
||||
RuntimeProfileRedeemCodeAdminErrorCode::InvalidField,
|
||||
"invalid_field",
|
||||
"兑换码奖励点数必须大于 0",
|
||||
),
|
||||
(
|
||||
RuntimeProfileRedeemCodeAdminErrorCode::AllowedUsersRequired,
|
||||
"allowed_users_required",
|
||||
"私有兑换码必须指定可兑换用户",
|
||||
),
|
||||
] {
|
||||
let error = map_runtime_profile_client_error(SpacetimeClientError::ProcedureRejected {
|
||||
code: code.into(),
|
||||
message: message.to_string(),
|
||||
});
|
||||
|
||||
assert_eq!(error.status_code(), StatusCode::BAD_REQUEST, "{message}");
|
||||
assert_eq!(error.code(), expected_code, "{message}");
|
||||
assert_eq!(error.message(), message);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn membership_business_rejections_map_to_bad_request() {
|
||||
// 中文注释:会员业务拒绝由 procedure 透传机器可读 error_code,统一按 400 返回;
|
||||
|
||||
@@ -1567,13 +1567,41 @@ impl RuntimeProfileRewardCodeRedeemErrorCode {
|
||||
}
|
||||
}
|
||||
|
||||
/// profile procedure 业务拒绝的统一错误码:把会员与兑换码两条链路的 typed code 收在一处。
|
||||
/// 后台兑换码管理的机器可读拒绝码。
|
||||
///
|
||||
/// 与 [`RuntimeProfileRewardCodeRedeemErrorCode`] 同样的目的:后台兑换码的增改 / 停用拒绝
|
||||
/// 不能靠中文文案分类 HTTP 状态,每个拒绝点显式产出本枚举并随 procedure 结果透传。
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum RuntimeProfileRedeemCodeAdminErrorCode {
|
||||
/// 兑换码在目录里不存在。
|
||||
NotFound,
|
||||
/// 兑换码管理入参不合法(缺码、奖励为 0、次数为 0、有效期倒挂等)。
|
||||
InvalidField,
|
||||
/// 私有兑换码没有可用于兑换的用户。
|
||||
AllowedUsersRequired,
|
||||
}
|
||||
|
||||
impl RuntimeProfileRedeemCodeAdminErrorCode {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::NotFound => "not_found",
|
||||
Self::InvalidField => "invalid_field",
|
||||
Self::AllowedUsersRequired => "allowed_users_required",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// profile procedure 业务拒绝的统一错误码:把会员、兑换码核销与后台兑换码管理的 typed code
|
||||
/// 收在一处。
|
||||
///
|
||||
/// 只在 `spacetime-client` 进程内传递,不落表、不过 wire,因此不派生 `SpacetimeType`。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum RuntimeProfileProcedureRejectionCode {
|
||||
Membership(RuntimeProfileMembershipErrorCode),
|
||||
RewardCodeRedeem(RuntimeProfileRewardCodeRedeemErrorCode),
|
||||
RedeemCodeAdmin(RuntimeProfileRedeemCodeAdminErrorCode),
|
||||
}
|
||||
|
||||
impl RuntimeProfileProcedureRejectionCode {
|
||||
@@ -1581,6 +1609,7 @@ impl RuntimeProfileProcedureRejectionCode {
|
||||
match self {
|
||||
Self::Membership(code) => code.as_str(),
|
||||
Self::RewardCodeRedeem(code) => code.as_str(),
|
||||
Self::RedeemCodeAdmin(code) => code.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1597,6 +1626,12 @@ impl From<RuntimeProfileRewardCodeRedeemErrorCode> for RuntimeProfileProcedureRe
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RuntimeProfileRedeemCodeAdminErrorCode> for RuntimeProfileProcedureRejectionCode {
|
||||
fn from(code: RuntimeProfileRedeemCodeAdminErrorCode) -> Self {
|
||||
Self::RedeemCodeAdmin(code)
|
||||
}
|
||||
}
|
||||
|
||||
/// 会员领域错误:机器可读 `code` + 人读 `message`。
|
||||
///
|
||||
/// 只在模块内部与 procedure 结果装载之间传递,不落 SpacetimeDB 表,因此不派生 `SpacetimeType`。
|
||||
@@ -2708,6 +2743,8 @@ pub struct RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileRedeemCodeSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
/// 后台管理业务拒绝的机器可读错误码;非业务错误(入参、系统等)为 `None`。
|
||||
pub error_code: Option<RuntimeProfileRedeemCodeAdminErrorCode>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
@@ -2717,6 +2754,8 @@ pub struct RuntimeProfileRedeemCodeAdminListProcedureResult {
|
||||
pub entries: Vec<RuntimeProfileRedeemCodeSnapshot>,
|
||||
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
/// 后台管理业务拒绝的机器可读错误码;非业务错误(入参、系统等)为 `None`。
|
||||
pub error_code: Option<RuntimeProfileRedeemCodeAdminErrorCode>,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
|
||||
|
||||
@@ -235,7 +235,7 @@ pub enum SpacetimeClientError {
|
||||
Build(String),
|
||||
ConnectDropped,
|
||||
Procedure(String),
|
||||
/// profile procedure 返回了带机器可读错误码的业务拒绝(会员 / 兑换码核销)。
|
||||
/// profile procedure 返回了带机器可读错误码的业务拒绝(会员 / 兑换码核销 / 后台兑换码管理)。
|
||||
ProcedureRejected {
|
||||
code: module_runtime::RuntimeProfileProcedureRejectionCode,
|
||||
message: String,
|
||||
|
||||
@@ -1268,7 +1268,13 @@ pub(crate) fn map_runtime_profile_redeem_code_admin_procedure_result(
|
||||
result: RuntimeProfileRedeemCodeAdminProcedureResult,
|
||||
) -> Result<RuntimeProfileRedeemCodeRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
return Err(match result.error_code {
|
||||
Some(code) => SpacetimeClientError::procedure_rejected(
|
||||
map_runtime_profile_redeem_code_admin_error_code_back(code),
|
||||
result.error_message,
|
||||
),
|
||||
None => SpacetimeClientError::procedure_failed(result.error_message),
|
||||
});
|
||||
}
|
||||
|
||||
let snapshot = result
|
||||
@@ -1284,7 +1290,13 @@ pub(crate) fn map_runtime_profile_redeem_code_admin_list_procedure_result(
|
||||
result: RuntimeProfileRedeemCodeAdminListProcedureResult,
|
||||
) -> Result<RuntimeProfileRedeemCodeAdminListRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::procedure_failed(result.error_message));
|
||||
return Err(match result.error_code {
|
||||
Some(code) => SpacetimeClientError::procedure_rejected(
|
||||
map_runtime_profile_redeem_code_admin_error_code_back(code),
|
||||
result.error_message,
|
||||
),
|
||||
None => SpacetimeClientError::procedure_failed(result.error_message),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(RuntimeProfileRedeemCodeAdminListRecord {
|
||||
@@ -2141,6 +2153,22 @@ pub(crate) fn map_runtime_profile_reward_code_redeem_error_code_back(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_redeem_code_admin_error_code_back(
|
||||
value: crate::module_bindings::RuntimeProfileRedeemCodeAdminErrorCode,
|
||||
) -> module_runtime::RuntimeProfileRedeemCodeAdminErrorCode {
|
||||
match value {
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeAdminErrorCode::NotFound => {
|
||||
module_runtime::RuntimeProfileRedeemCodeAdminErrorCode::NotFound
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeAdminErrorCode::InvalidField => {
|
||||
module_runtime::RuntimeProfileRedeemCodeAdminErrorCode::InvalidField
|
||||
}
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeAdminErrorCode::AllowedUsersRequired => {
|
||||
module_runtime::RuntimeProfileRedeemCodeAdminErrorCode::AllowedUsersRequired
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_runtime_profile_membership_model_access(
|
||||
value: module_runtime::RuntimeProfileMembershipModelAccess,
|
||||
) -> crate::module_bindings::RuntimeProfileMembershipModelAccess {
|
||||
@@ -2484,3 +2512,45 @@ pub(crate) fn map_runtime_profile_feedback_status_back(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn admin_redeem_code_rejection_uses_typed_code() {
|
||||
let result = RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some("兑换码不存在".to_string()),
|
||||
error_code: Some(
|
||||
crate::module_bindings::RuntimeProfileRedeemCodeAdminErrorCode::NotFound,
|
||||
),
|
||||
};
|
||||
|
||||
match map_runtime_profile_redeem_code_admin_procedure_result(result) {
|
||||
Err(SpacetimeClientError::ProcedureRejected { code, message }) => {
|
||||
assert_eq!(code.as_str(), "not_found");
|
||||
assert_eq!(message, "兑换码不存在");
|
||||
}
|
||||
other => panic!("expected ProcedureRejected, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn admin_redeem_code_without_error_code_stays_plain_procedure_error() {
|
||||
let result = RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some("SpacetimeDB 事务失败".to_string()),
|
||||
error_code: None,
|
||||
};
|
||||
|
||||
match map_runtime_profile_redeem_code_admin_procedure_result(result) {
|
||||
Err(SpacetimeClientError::Procedure(message)) => {
|
||||
assert_eq!(message, "SpacetimeDB 事务失败");
|
||||
}
|
||||
other => panic!("expected Procedure, got {other:?}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,6 +783,7 @@ pub mod runtime_profile_recharge_refund_recovery_status_type;
|
||||
pub mod runtime_profile_recharge_refund_snapshot_type;
|
||||
pub mod runtime_profile_recharge_refund_status_type;
|
||||
pub mod runtime_profile_redeem_code_admin_disable_input_type;
|
||||
pub mod runtime_profile_redeem_code_admin_error_code_type;
|
||||
pub mod runtime_profile_redeem_code_admin_list_input_type;
|
||||
pub mod runtime_profile_redeem_code_admin_list_procedure_result_type;
|
||||
pub mod runtime_profile_redeem_code_admin_procedure_result_type;
|
||||
@@ -1669,6 +1670,7 @@ pub use runtime_profile_recharge_refund_recovery_status_type::RuntimeProfileRech
|
||||
pub use runtime_profile_recharge_refund_snapshot_type::RuntimeProfileRechargeRefundSnapshot;
|
||||
pub use runtime_profile_recharge_refund_status_type::RuntimeProfileRechargeRefundStatus;
|
||||
pub use runtime_profile_redeem_code_admin_disable_input_type::RuntimeProfileRedeemCodeAdminDisableInput;
|
||||
pub use runtime_profile_redeem_code_admin_error_code_type::RuntimeProfileRedeemCodeAdminErrorCode;
|
||||
pub use runtime_profile_redeem_code_admin_list_input_type::RuntimeProfileRedeemCodeAdminListInput;
|
||||
pub use runtime_profile_redeem_code_admin_list_procedure_result_type::RuntimeProfileRedeemCodeAdminListProcedureResult;
|
||||
pub use runtime_profile_redeem_code_admin_procedure_result_type::RuntimeProfileRedeemCodeAdminProcedureResult;
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
#[derive(Copy, Eq, Hash)]
|
||||
pub enum RuntimeProfileRedeemCodeAdminErrorCode {
|
||||
NotFound,
|
||||
|
||||
InvalidField,
|
||||
|
||||
AllowedUsersRequired,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeAdminErrorCode {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
+2
@@ -5,6 +5,7 @@
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_code_operation_snapshot_type::RuntimeProfileCodeOperationSnapshot;
|
||||
use super::runtime_profile_redeem_code_admin_error_code_type::RuntimeProfileRedeemCodeAdminErrorCode;
|
||||
use super::runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
@@ -14,6 +15,7 @@ pub struct RuntimeProfileRedeemCodeAdminListProcedureResult {
|
||||
pub entries: Vec<RuntimeProfileRedeemCodeSnapshot>,
|
||||
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
pub error_code: Option<RuntimeProfileRedeemCodeAdminErrorCode>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeAdminListProcedureResult {
|
||||
|
||||
+2
@@ -4,6 +4,7 @@
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::runtime_profile_redeem_code_admin_error_code_type::RuntimeProfileRedeemCodeAdminErrorCode;
|
||||
use super::runtime_profile_redeem_code_snapshot_type::RuntimeProfileRedeemCodeSnapshot;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
@@ -12,6 +13,7 @@ pub struct RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
pub ok: bool,
|
||||
pub record: Option<RuntimeProfileRedeemCodeSnapshot>,
|
||||
pub error_message: Option<String>,
|
||||
pub error_code: Option<RuntimeProfileRedeemCodeAdminErrorCode>,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
|
||||
@@ -126,6 +126,53 @@ fn reward_code_redeem_error_from_field_error(
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台兑换码管理 procedure 的错误载体。
|
||||
///
|
||||
/// 业务拒绝携带机器可读 [`RuntimeProfileRedeemCodeAdminErrorCode`];入参、系统等其它错误保持
|
||||
/// 普通字符串,透传时 `error_code` 为 `null`(与会员 / 核销链路同一约定)。
|
||||
enum ProfileRedeemCodeAdminProcedureError {
|
||||
Classified(RuntimeProfileRedeemCodeAdminErrorCode, String),
|
||||
Plain(String),
|
||||
}
|
||||
|
||||
impl From<String> for ProfileRedeemCodeAdminProcedureError {
|
||||
fn from(message: String) -> Self {
|
||||
Self::Plain(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ProfileRedeemCodeAdminProcedureError {
|
||||
fn from(message: &str) -> Self {
|
||||
Self::Plain(message.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl ProfileRedeemCodeAdminProcedureError {
|
||||
fn classified(
|
||||
code: RuntimeProfileRedeemCodeAdminErrorCode,
|
||||
message: impl Into<String>,
|
||||
) -> Self {
|
||||
Self::Classified(code, message.into())
|
||||
}
|
||||
|
||||
fn into_parts(self) -> (String, Option<RuntimeProfileRedeemCodeAdminErrorCode>) {
|
||||
match self {
|
||||
Self::Classified(code, message) => (message, Some(code)),
|
||||
Self::Plain(message) => (message, None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台兑换码的字段校验错误统一归入 `invalid_field`,保证入参拒绝也落 400 而不是 502。
|
||||
fn redeem_code_admin_error_from_field_error(
|
||||
error: RuntimeProfileFieldError,
|
||||
) -> ProfileRedeemCodeAdminProcedureError {
|
||||
ProfileRedeemCodeAdminProcedureError::classified(
|
||||
RuntimeProfileRedeemCodeAdminErrorCode::InvalidField,
|
||||
error.to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = profile_dashboard_state)]
|
||||
pub struct ProfileDashboardState {
|
||||
#[primary_key]
|
||||
@@ -2697,12 +2744,17 @@ pub fn admin_upsert_profile_redeem_code(
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
error_code: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
Err(error) => {
|
||||
let (message, error_code) = error.into_parts();
|
||||
RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
error_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2716,12 +2768,17 @@ pub fn admin_disable_profile_redeem_code(
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
error_code: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
Err(error) => {
|
||||
let (message, error_code) = error.into_parts();
|
||||
RuntimeProfileRedeemCodeAdminProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
error_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2736,13 +2793,18 @@ pub fn admin_list_profile_redeem_codes(
|
||||
entries,
|
||||
operations,
|
||||
error_message: None,
|
||||
error_code: None,
|
||||
},
|
||||
Err(message) => RuntimeProfileRedeemCodeAdminListProcedureResult {
|
||||
ok: false,
|
||||
entries: Vec::new(),
|
||||
operations: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
Err(error) => {
|
||||
let (message, error_code) = error.into_parts();
|
||||
RuntimeProfileRedeemCodeAdminListProcedureResult {
|
||||
ok: false,
|
||||
entries: Vec::new(),
|
||||
operations: Vec::new(),
|
||||
error_message: Some(message),
|
||||
error_code,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7742,7 +7804,7 @@ fn redeem_profile_reward_code_record(
|
||||
fn admin_upsert_profile_redeem_code_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
) -> Result<RuntimeProfileRedeemCodeSnapshot, String> {
|
||||
) -> Result<RuntimeProfileRedeemCodeSnapshot, ProfileRedeemCodeAdminProcedureError> {
|
||||
let validated_input = build_runtime_profile_redeem_code_admin_upsert_input(
|
||||
input.admin_user_id,
|
||||
input.code,
|
||||
@@ -7756,7 +7818,7 @@ fn admin_upsert_profile_redeem_code_record(
|
||||
input.expires_at_micros,
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
.map_err(redeem_code_admin_error_from_field_error)?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
|
||||
let allowed_user_ids = resolve_profile_redeem_code_allowed_user_ids(ctx, &validated_input)?;
|
||||
let existing = ctx
|
||||
@@ -7817,20 +7879,25 @@ fn admin_upsert_profile_redeem_code_record(
|
||||
fn admin_disable_profile_redeem_code_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileRedeemCodeAdminDisableInput,
|
||||
) -> Result<RuntimeProfileRedeemCodeSnapshot, String> {
|
||||
) -> Result<RuntimeProfileRedeemCodeSnapshot, ProfileRedeemCodeAdminProcedureError> {
|
||||
let validated_input = build_runtime_profile_redeem_code_admin_disable_input(
|
||||
input.admin_user_id,
|
||||
input.code,
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
.map_err(redeem_code_admin_error_from_field_error)?;
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
|
||||
let existing = ctx
|
||||
.db
|
||||
.profile_redeem_code()
|
||||
.code()
|
||||
.find(&validated_input.code)
|
||||
.ok_or_else(|| "兑换码不存在".to_string())?;
|
||||
.ok_or_else(|| {
|
||||
ProfileRedeemCodeAdminProcedureError::classified(
|
||||
RuntimeProfileRedeemCodeAdminErrorCode::NotFound,
|
||||
"兑换码不存在",
|
||||
)
|
||||
})?;
|
||||
|
||||
ctx.db.profile_redeem_code().code().delete(&existing.code);
|
||||
let inserted = ctx.db.profile_redeem_code().insert(ProfileRedeemCode {
|
||||
@@ -8450,10 +8517,10 @@ fn admin_list_profile_redeem_code_records(
|
||||
Vec<RuntimeProfileRedeemCodeSnapshot>,
|
||||
Vec<RuntimeProfileCodeOperationSnapshot>,
|
||||
),
|
||||
String,
|
||||
ProfileRedeemCodeAdminProcedureError,
|
||||
> {
|
||||
let _validated_input = build_runtime_profile_redeem_code_admin_list_input(input.admin_user_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
.map_err(redeem_code_admin_error_from_field_error)?;
|
||||
|
||||
let mut entries = ctx
|
||||
.db
|
||||
@@ -12151,7 +12218,7 @@ fn count_profile_redeem_code_user_usage(ctx: &ReducerContext, code: &str, user_i
|
||||
fn resolve_profile_redeem_code_allowed_user_ids(
|
||||
ctx: &ReducerContext,
|
||||
input: &RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
) -> Result<Vec<String>, String> {
|
||||
) -> Result<Vec<String>, ProfileRedeemCodeAdminProcedureError> {
|
||||
if input.mode != RuntimeProfileRedeemCodeMode::Private {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
@@ -12172,7 +12239,10 @@ fn resolve_profile_redeem_code_allowed_user_ids(
|
||||
allowed_user_ids.dedup();
|
||||
|
||||
if allowed_user_ids.is_empty() {
|
||||
return Err("私有兑换码必须指定可兑换用户".to_string());
|
||||
return Err(ProfileRedeemCodeAdminProcedureError::classified(
|
||||
RuntimeProfileRedeemCodeAdminErrorCode::AllowedUsersRequired,
|
||||
"私有兑换码必须指定可兑换用户",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(allowed_user_ids)
|
||||
|
||||
Reference in New Issue
Block a user