修复(后台邀请码): 拒绝补机器可读错误码走 400

- module-runtime 新增 RuntimeProfileInviteCodeAdminErrorCode(invalid_field / in_use),两个后台邀请码 procedure 结果末尾追加 error_code
- module 侧用 ProfileInviteCodeAdminProcedureError 在拒绝点赋码,字段校验统一 invalid_field、被他人占用归 in_use
- spacetime-client mapper 有码走 ProcedureRejected,并把 api-server 映射注释收窄为「带 typed code 的四条链路」
- 重新生成 SpacetimeDB 绑定;补 client 与 api-server 单测;同步设计文档 §5.8 与决策日志
This commit is contained in:
2026-10-05 14:22:39 +08:00
parent e87837381a
commit c3866b4267
11 changed files with 259 additions and 41 deletions
@@ -9728,3 +9728,11 @@ 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,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` 通过。
## 2026-10-03 后台邀请码管理补机器可读错误码
- 背景:后台邀请码的 `admin_upsert_profile_invite_code` / `admin_list_profile_invite_codes` 与上一轮的后台兑换码同病——`map_runtime_profile_invite_code_admin_procedure_result` 把 `ok = false` 一律拍成 `Procedure(String)`,`邀请码已被其他用户占用` 这类明确业务拒绝在 `api-server` 落 502,且 `map_runtime_profile_client_error` 的注释夸大成 «profile procedure 的业务拒绝都已 typed»。
- 决策:`module-runtime` 新增 `RuntimeProfileInviteCodeAdminErrorCode`(`InvalidField` / `InUse`);两个后台邀请码 procedure 结果末尾追加 `error_code: Option<...>`;`spacetime-module` 用 `ProfileInviteCodeAdminProcedureError::{Classified, Plain}` 赋码,字段校验归 `invalid_field`、被他人占用归 `in_use`;`RuntimeProfileProcedureRejectionCode` 增加 `InviteCodeAdmin` 聚合变体,`spacetime-client` mapper 有码走 `ProcedureRejected`、缺失时保持 `Procedure`;`api-server` 复用既有映射,并把注释收窄为 «带 typed error_code 的四条链路»。
- 行为变化:后台邀请码的入参校验失败与 `邀请码已被其他用户占用` 由 502 变为 400 且带机器码;procedure result 新增字段仍要求 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` 32 passed;`cargo test -p api-server runtime_profile::tests` 43 passed;`cargo test -p module-runtime --lib` 114 passed;`cargo test -p spacetime-module` 271 passed。
@@ -460,15 +460,20 @@ flowchart TD
`RuntimeProfileRedeemCodeAdminProcedureResult` / `RuntimeProfileRedeemCodeAdminListProcedureResult`
末尾追加 `error_code`;字段校验错误统一归 `invalid_field`,私有码缺可兑换用户归
`allowed_users_required`,停用不存在的码归 `not_found`。
- 后台邀请码管理链路:`module-runtime` 的 `RuntimeProfileInviteCodeAdminErrorCode`(`InvalidField` /
`InUse`)在拒绝点显式产出,`RuntimeProfileInviteCodeAdminProcedureResult` /
`RuntimeProfileInviteCodeAdminListProcedureResult` 末尾追加 `error_code`;字段校验错误统一归
`invalid_field`,邀请码被他人占用归 `in_use`。
- `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。
`code.as_str()`,基础设施错误保持 502。尚未接 typed code 的 procedure 字段校验类错误(如
user_id 缺失)不产出 `error_code`,仍按字符串错误落到 502。
- 行为变化:`会员目录缺少档位 ...`、「已迁移的会员商品」、兑换码核销的 `兑换码不存在` /
`已停用` / `未生效` / `已过期` / `次数已用完` / `不适用于当前账号` / `奖励无效`,以及后台兑换码
管理的 `兑换码不存在` / 入参校验失败 / 私有码缺可兑换用户原先落到 502,现在有明确错误码,统一
400。`api-server` 已删除 `is_runtime_profile_redeem_code_domain_error` 文案匹配函数。
`已停用` / `未生效` / `已过期` / `次数已用完` / `不适用于当前账号` / `奖励无效`,后台兑换码
管理的 `兑换码不存在` / 入参校验失败 / 私有码缺可兑换用户,以及后台邀请码管理的入参校验失败 /
`邀请码已被其他用户占用` 原先落到 502,现在有明确错误码,统一 400。`api-server` 已删除
`is_runtime_profile_redeem_code_domain_error` 文案匹配函数。
- 兼容边界:procedure result 是 wire 结构,新增字段要求 module 与 api-server 同版本部署;
旧 module 不返回 `error_code` 时,新 client 绑定反序列化会失败。
@@ -1462,8 +1462,9 @@ pub async fn get_profile_play_stats(
fn map_runtime_profile_client_error(error: SpacetimeClientError) -> AppError {
match &error {
// profile procedure 的业务拒绝:机器码与人读文案都取自 typed 变体,
// HTTP 状态、错误码和 message 全部结构化透传,不做任何字符串匹配或解析。
// 带 typed error_code 的 profile procedure(会员、兑换码核销、后台兑换码管理、后台邀请码
// 管理)业务拒绝:机器码与人读文案都取自 typed 变体,HTTP 状态、错误码和 message 全部
// 结构化透传,不做任何字符串匹配或解析;其余 procedure 仍按裸字符串落下面的 502。
SpacetimeClientError::ProcedureRejected { code, message } => {
AppError::from_status(StatusCode::BAD_REQUEST)
.with_code(code.as_str())
@@ -2484,14 +2485,15 @@ mod tests {
use module_auth::{ResolveWechatLoginInput, WechatIdentityProfile};
use module_runtime::{
PROFILE_RECHARGE_PAYMENT_CHANNEL_WECHAT_MINI_PROGRAM_VIRTUAL,
RuntimeProfileDailyFreePointsRecord, RuntimeProfileMembershipCycleKind,
RuntimeProfileMembershipErrorCode, RuntimeProfileMembershipModelAccess,
RuntimeProfileMembershipPlan, RuntimeProfileMembershipPlanRecord,
RuntimeProfileMembershipRecord, RuntimeProfileMembershipStatus,
RuntimeProfileRechargeCenterRecord, RuntimeProfileRechargeOrderRecord,
RuntimeProfileRechargeOrderStatus, RuntimeProfileRechargeProductKind,
RuntimeProfileRechargeProductRecord, RuntimeProfileRedeemCodeAdminErrorCode,
RuntimeProfileRewardCodeRedeemErrorCode, RuntimeProfileWalletLedgerSourceType,
RuntimeProfileDailyFreePointsRecord, RuntimeProfileInviteCodeAdminErrorCode,
RuntimeProfileMembershipCycleKind, RuntimeProfileMembershipErrorCode,
RuntimeProfileMembershipModelAccess, RuntimeProfileMembershipPlan,
RuntimeProfileMembershipPlanRecord, RuntimeProfileMembershipRecord,
RuntimeProfileMembershipStatus, RuntimeProfileRechargeCenterRecord,
RuntimeProfileRechargeOrderRecord, RuntimeProfileRechargeOrderStatus,
RuntimeProfileRechargeProductKind, RuntimeProfileRechargeProductRecord,
RuntimeProfileRedeemCodeAdminErrorCode, RuntimeProfileRewardCodeRedeemErrorCode,
RuntimeProfileWalletLedgerSourceType,
};
use super::{
@@ -2606,6 +2608,33 @@ mod tests {
}
}
#[test]
fn invite_code_admin_rejections_map_to_bad_request() {
// 后台邀请码管理与后台兑换码管理同构:「邀请码已被占用」/入参非法必须落 400 并带机器码,
// 不能因为缺 error_code 被当成 SpacetimeDB 基础设施故障落 502。
for (code, expected_code, message) in [
(
RuntimeProfileInviteCodeAdminErrorCode::InvalidField,
"invalid_field",
"邀请码不能为空",
),
(
RuntimeProfileInviteCodeAdminErrorCode::InUse,
"in_use",
"邀请码已被其他用户占用",
),
] {
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 返回;
+37 -2
View File
@@ -1593,8 +1593,31 @@ impl RuntimeProfileRedeemCodeAdminErrorCode {
}
}
/// profile procedure 业务拒绝的统一错误码:把会员、兑换码核销与后台兑换码管理的 typed code
/// 收在一处。
/// 后台邀请码管理的机器可读拒绝码。
///
/// 与后台兑换码管理同一约定:邀请码增改的拒绝点显式产出本枚举并随 procedure 结果透传,
/// `api-server` 据此返回 400 而不是把业务拒绝当成 502。
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RuntimeProfileInviteCodeAdminErrorCode {
/// 邀请码管理入参不合法(缺码、元数据不是对象、有效期倒挂等)。
InvalidField,
/// 邀请码已被其他用户占用,不能改绑给当前用户。
InUse,
}
impl RuntimeProfileInviteCodeAdminErrorCode {
pub fn as_str(&self) -> &'static str {
match self {
Self::InvalidField => "invalid_field",
Self::InUse => "in_use",
}
}
}
/// profile procedure 业务拒绝的统一错误码:把会员、兑换码核销、后台兑换码管理与后台邀请码管理
/// 的 typed code 收在一处。
///
/// 只在 `spacetime-client` 进程内传递,不落表、不过 wire,因此不派生 `SpacetimeType`。
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -1602,6 +1625,7 @@ pub enum RuntimeProfileProcedureRejectionCode {
Membership(RuntimeProfileMembershipErrorCode),
RewardCodeRedeem(RuntimeProfileRewardCodeRedeemErrorCode),
RedeemCodeAdmin(RuntimeProfileRedeemCodeAdminErrorCode),
InviteCodeAdmin(RuntimeProfileInviteCodeAdminErrorCode),
}
impl RuntimeProfileProcedureRejectionCode {
@@ -1610,6 +1634,7 @@ impl RuntimeProfileProcedureRejectionCode {
Self::Membership(code) => code.as_str(),
Self::RewardCodeRedeem(code) => code.as_str(),
Self::RedeemCodeAdmin(code) => code.as_str(),
Self::InviteCodeAdmin(code) => code.as_str(),
}
}
}
@@ -1632,6 +1657,12 @@ impl From<RuntimeProfileRedeemCodeAdminErrorCode> for RuntimeProfileProcedureRej
}
}
impl From<RuntimeProfileInviteCodeAdminErrorCode> for RuntimeProfileProcedureRejectionCode {
fn from(code: RuntimeProfileInviteCodeAdminErrorCode) -> Self {
Self::InviteCodeAdmin(code)
}
}
/// 会员领域错误:机器可读 `code` + 人读 `message`。
///
/// 只在模块内部与 procedure 结果装载之间传递,不落 SpacetimeDB 表,因此不派生 `SpacetimeType`。
@@ -2793,6 +2824,8 @@ pub struct RuntimeProfileInviteCodeAdminProcedureResult {
pub ok: bool,
pub record: Option<RuntimeProfileInviteCodeSnapshot>,
pub error_message: Option<String>,
/// 业务拒绝的机器可读码;入参 / 系统错误为 `None`,由 `api-server` 落 502。
pub error_code: Option<RuntimeProfileInviteCodeAdminErrorCode>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -2802,6 +2835,8 @@ pub struct RuntimeProfileInviteCodeAdminListProcedureResult {
pub entries: Vec<RuntimeProfileInviteCodeSnapshot>,
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
pub error_message: Option<String>,
/// 业务拒绝的机器可读码;入参 / 系统错误为 `None`,由 `api-server` 落 502。
pub error_code: Option<RuntimeProfileInviteCodeAdminErrorCode>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -235,7 +235,8 @@ pub enum SpacetimeClientError {
Build(String),
ConnectDropped,
Procedure(String),
/// profile procedure 返回了带机器可读错误码的业务拒绝(会员 / 兑换码核销 / 后台兑换码管理)。
/// profile procedure 返回了带机器可读错误码的业务拒绝(会员 / 兑换码核销 / 后台兑换码管理 /
/// 后台邀请码管理)。
ProcedureRejected {
code: module_runtime::RuntimeProfileProcedureRejectionCode,
message: String,
@@ -1325,11 +1325,13 @@ pub(crate) fn map_runtime_profile_invite_code_admin_procedure_result(
result: RuntimeProfileInviteCodeAdminProcedureResult,
) -> Result<RuntimeProfileInviteCodeRecord, SpacetimeClientError> {
if !result.ok {
return Err(SpacetimeClientError::Procedure(
result
.error_message
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
));
return Err(match result.error_code {
Some(code) => SpacetimeClientError::procedure_rejected(
map_runtime_profile_invite_code_admin_error_code_back(code),
result.error_message,
),
None => SpacetimeClientError::procedure_failed(result.error_message),
});
}
let snapshot = result.record.ok_or_else(|| {
@@ -1345,7 +1347,13 @@ pub(crate) fn map_runtime_profile_invite_code_admin_list_procedure_result(
result: RuntimeProfileInviteCodeAdminListProcedureResult,
) -> Result<RuntimeProfileInviteCodeAdminListRecord, 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_invite_code_admin_error_code_back(code),
result.error_message,
),
None => SpacetimeClientError::procedure_failed(result.error_message),
});
}
Ok(RuntimeProfileInviteCodeAdminListRecord {
@@ -2169,6 +2177,19 @@ pub(crate) fn map_runtime_profile_redeem_code_admin_error_code_back(
}
}
pub(crate) fn map_runtime_profile_invite_code_admin_error_code_back(
value: crate::module_bindings::RuntimeProfileInviteCodeAdminErrorCode,
) -> module_runtime::RuntimeProfileInviteCodeAdminErrorCode {
match value {
crate::module_bindings::RuntimeProfileInviteCodeAdminErrorCode::InvalidField => {
module_runtime::RuntimeProfileInviteCodeAdminErrorCode::InvalidField
}
crate::module_bindings::RuntimeProfileInviteCodeAdminErrorCode::InUse => {
module_runtime::RuntimeProfileInviteCodeAdminErrorCode::InUse
}
}
}
pub(crate) fn map_runtime_profile_membership_model_access(
value: module_runtime::RuntimeProfileMembershipModelAccess,
) -> crate::module_bindings::RuntimeProfileMembershipModelAccess {
@@ -2553,4 +2574,39 @@ mod tests {
other => panic!("expected Procedure, got {other:?}"),
}
}
#[test]
fn admin_invite_code_rejection_uses_typed_code() {
let result = RuntimeProfileInviteCodeAdminProcedureResult {
ok: false,
record: None,
error_message: Some("邀请码已被其他用户占用".to_string()),
error_code: Some(crate::module_bindings::RuntimeProfileInviteCodeAdminErrorCode::InUse),
};
match map_runtime_profile_invite_code_admin_procedure_result(result) {
Err(SpacetimeClientError::ProcedureRejected { code, message }) => {
assert_eq!(code.as_str(), "in_use");
assert_eq!(message, "邀请码已被其他用户占用");
}
other => panic!("expected ProcedureRejected, got {other:?}"),
}
}
#[test]
fn admin_invite_code_without_error_code_stays_plain_procedure_error() {
let result = RuntimeProfileInviteCodeAdminProcedureResult {
ok: false,
record: None,
error_message: Some("SpacetimeDB 事务失败".to_string()),
error_code: None,
};
match map_runtime_profile_invite_code_admin_procedure_result(result) {
Err(SpacetimeClientError::Procedure(message)) => {
assert_eq!(message, "SpacetimeDB 事务失败");
}
other => panic!("expected Procedure, got {other:?}"),
}
}
}
@@ -707,6 +707,7 @@ pub mod runtime_profile_feedback_status_type;
pub mod runtime_profile_feedback_submission_input_type;
pub mod runtime_profile_feedback_submission_procedure_result_type;
pub mod runtime_profile_feedback_submission_snapshot_type;
pub mod runtime_profile_invite_code_admin_error_code_type;
pub mod runtime_profile_invite_code_admin_list_input_type;
pub mod runtime_profile_invite_code_admin_list_procedure_result_type;
pub mod runtime_profile_invite_code_admin_procedure_result_type;
@@ -1594,6 +1595,7 @@ pub use runtime_profile_feedback_status_type::RuntimeProfileFeedbackStatus;
pub use runtime_profile_feedback_submission_input_type::RuntimeProfileFeedbackSubmissionInput;
pub use runtime_profile_feedback_submission_procedure_result_type::RuntimeProfileFeedbackSubmissionProcedureResult;
pub use runtime_profile_feedback_submission_snapshot_type::RuntimeProfileFeedbackSubmissionSnapshot;
pub use runtime_profile_invite_code_admin_error_code_type::RuntimeProfileInviteCodeAdminErrorCode;
pub use runtime_profile_invite_code_admin_list_input_type::RuntimeProfileInviteCodeAdminListInput;
pub use runtime_profile_invite_code_admin_list_procedure_result_type::RuntimeProfileInviteCodeAdminListProcedureResult;
pub use runtime_profile_invite_code_admin_procedure_result_type::RuntimeProfileInviteCodeAdminProcedureResult;
@@ -0,0 +1,18 @@
// 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 RuntimeProfileInviteCodeAdminErrorCode {
InvalidField,
InUse,
}
impl __sdk::InModule for RuntimeProfileInviteCodeAdminErrorCode {
type Module = super::RemoteModule;
}
@@ -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_invite_code_admin_error_code_type::RuntimeProfileInviteCodeAdminErrorCode;
use super::runtime_profile_invite_code_snapshot_type::RuntimeProfileInviteCodeSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
@@ -14,6 +15,7 @@ pub struct RuntimeProfileInviteCodeAdminListProcedureResult {
pub entries: Vec<RuntimeProfileInviteCodeSnapshot>,
pub operations: Vec<RuntimeProfileCodeOperationSnapshot>,
pub error_message: Option<String>,
pub error_code: Option<RuntimeProfileInviteCodeAdminErrorCode>,
}
impl __sdk::InModule for RuntimeProfileInviteCodeAdminListProcedureResult {
@@ -4,6 +4,7 @@
#![allow(unused, clippy::all)]
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::runtime_profile_invite_code_admin_error_code_type::RuntimeProfileInviteCodeAdminErrorCode;
use super::runtime_profile_invite_code_snapshot_type::RuntimeProfileInviteCodeSnapshot;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
@@ -12,6 +13,7 @@ pub struct RuntimeProfileInviteCodeAdminProcedureResult {
pub ok: bool,
pub record: Option<RuntimeProfileInviteCodeSnapshot>,
pub error_message: Option<String>,
pub error_code: Option<RuntimeProfileInviteCodeAdminErrorCode>,
}
impl __sdk::InModule for RuntimeProfileInviteCodeAdminProcedureResult {
@@ -173,6 +173,53 @@ fn redeem_code_admin_error_from_field_error(
)
}
/// 后台邀请码管理 procedure 的错误载体。
///
/// 与 [`ProfileRedeemCodeAdminProcedureError`] 同一约定:业务拒绝带机器可读
/// [`RuntimeProfileInviteCodeAdminErrorCode`],其它错误保持普通字符串。
enum ProfileInviteCodeAdminProcedureError {
Classified(RuntimeProfileInviteCodeAdminErrorCode, String),
Plain(String),
}
impl From<String> for ProfileInviteCodeAdminProcedureError {
fn from(message: String) -> Self {
Self::Plain(message)
}
}
impl From<&str> for ProfileInviteCodeAdminProcedureError {
fn from(message: &str) -> Self {
Self::Plain(message.to_string())
}
}
impl ProfileInviteCodeAdminProcedureError {
fn classified(
code: RuntimeProfileInviteCodeAdminErrorCode,
message: impl Into<String>,
) -> Self {
Self::Classified(code, message.into())
}
fn into_parts(self) -> (String, Option<RuntimeProfileInviteCodeAdminErrorCode>) {
match self {
Self::Classified(code, message) => (message, Some(code)),
Self::Plain(message) => (message, None),
}
}
}
/// 后台邀请码的字段校验错误统一归入 `invalid_field`,保证入参拒绝也落 400 而不是 502。
fn invite_code_admin_error_from_field_error(
error: RuntimeProfileFieldError,
) -> ProfileInviteCodeAdminProcedureError {
ProfileInviteCodeAdminProcedureError::classified(
RuntimeProfileInviteCodeAdminErrorCode::InvalidField,
error.to_string(),
)
}
#[spacetimedb::table(accessor = profile_dashboard_state)]
pub struct ProfileDashboardState {
#[primary_key]
@@ -2818,12 +2865,17 @@ pub fn admin_upsert_profile_invite_code(
ok: true,
record: Some(record),
error_message: None,
error_code: None,
},
Err(message) => RuntimeProfileInviteCodeAdminProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
Err(error) => {
let (message, error_code) = error.into_parts();
RuntimeProfileInviteCodeAdminProcedureResult {
ok: false,
record: None,
error_message: Some(message),
error_code,
}
}
}
}
@@ -2838,13 +2890,18 @@ pub fn admin_list_profile_invite_codes(
entries,
operations,
error_message: None,
error_code: None,
},
Err(message) => RuntimeProfileInviteCodeAdminListProcedureResult {
ok: false,
entries: Vec::new(),
operations: Vec::new(),
error_message: Some(message),
},
Err(error) => {
let (message, error_code) = error.into_parts();
RuntimeProfileInviteCodeAdminListProcedureResult {
ok: false,
entries: Vec::new(),
operations: Vec::new(),
error_message: Some(message),
error_code,
}
}
}
}
@@ -7919,7 +7976,7 @@ fn admin_disable_profile_redeem_code_record(
fn admin_upsert_profile_invite_code_record(
ctx: &ReducerContext,
input: RuntimeProfileInviteCodeAdminUpsertInput,
) -> Result<RuntimeProfileInviteCodeSnapshot, String> {
) -> Result<RuntimeProfileInviteCodeSnapshot, ProfileInviteCodeAdminProcedureError> {
let validated_input = build_runtime_profile_invite_code_admin_upsert_input(
input.admin_user_id,
input.invite_code,
@@ -7928,7 +7985,7 @@ fn admin_upsert_profile_invite_code_record(
input.expires_at_micros,
input.updated_at_micros,
)
.map_err(|error| error.to_string())?;
.map_err(invite_code_admin_error_from_field_error)?;
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
let user_id = build_admin_profile_invite_code_user_id(
&validated_input.admin_user_id,
@@ -7944,7 +8001,10 @@ fn admin_upsert_profile_invite_code_record(
.find(&validated_input.invite_code)
{
if existing.user_id != user_id {
return Err("邀请码已被其他用户占用".to_string());
return Err(ProfileInviteCodeAdminProcedureError::classified(
RuntimeProfileInviteCodeAdminErrorCode::InUse,
"邀请码已被其他用户占用",
));
}
ctx.db
.profile_invite_code()
@@ -8545,10 +8605,10 @@ fn admin_list_profile_invite_code_records(
Vec<RuntimeProfileInviteCodeSnapshot>,
Vec<RuntimeProfileCodeOperationSnapshot>,
),
String,
ProfileInviteCodeAdminProcedureError,
> {
let _validated_input = build_runtime_profile_invite_code_admin_list_input(input.admin_user_id)
.map_err(|error| error.to_string())?;
.map_err(invite_code_admin_error_from_field_error)?;
let mut entries = ctx
.db