c660ca41fe
- shared-contracts 新增 ProfileMembershipPlan/CycleKind/ModelAccess/ChangeKind Token 枚举 - RuntimeProfileMembershipOrderChangeSnapshot 与 UpgradeQuoteRecord 档位/周期改用模块枚举 - AdminMembershipOrderChangePayload、ProfileMembership/Plan/UpgradeQuote Response 改用 token 枚举 - module-runtime 提供模块枚举与 wire token 的双向 From 映射 - api-server / spacetime-client / spacetime-module 同步去掉 as_str().to_string() 拼装 - 重生成 SpacetimeDB 绑定:订单变更快照字段改为枚举
2104 lines
75 KiB
Rust
2104 lines
75 KiB
Rust
use serde::{Deserialize, Serialize};
|
||
|
||
pub const RUNTIME_PLATFORM_THEME_LIGHT: &str = "light";
|
||
pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark";
|
||
#[cfg(any())]
|
||
pub const SAVE_SNAPSHOT_VERSION: u32 = 2;
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC: &str = "snapshot_sync";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD: &str =
|
||
"new_user_registration_reward";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE: &str = "points_recharge";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_MEMBERSHIP_PERIOD_GRANT: &str =
|
||
"membership_period_grant";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_MEMBERSHIP_PERIOD_RESET: &str =
|
||
"membership_period_reset";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_FREE_GRANT: &str = "daily_free_grant";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_FREE_RESET: &str = "daily_free_reset";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_RECHARGE_REFUND_RECOVERY: &str =
|
||
"recharge_refund_recovery";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD: &str = "invite_inviter_reward";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME: &str =
|
||
"asset_operation_consume";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_LLM_ROUTER_CONSUME: &str = "llm_router_consume";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND: &str = "asset_operation_refund";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_REDEEM_CODE_REWARD: &str = "redeem_code_reward";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM: &str =
|
||
"puzzle_author_incentive_claim";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD: &str = "daily_task_reward";
|
||
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_MEMBERSHIP_UPGRADE_GRANT: &str =
|
||
"membership_upgrade_grant";
|
||
pub const PROFILE_TASK_CYCLE_DAILY: &str = "daily";
|
||
pub const PROFILE_TASK_STATUS_INCOMPLETE: &str = "incomplete";
|
||
pub const PROFILE_TASK_STATUS_CLAIMABLE: &str = "claimable";
|
||
pub const PROFILE_TASK_STATUS_CLAIMED: &str = "claimed";
|
||
pub const PROFILE_TASK_STATUS_DISABLED: &str = "disabled";
|
||
pub const PROFILE_RECHARGE_PRODUCT_KIND_POINTS: &str = "points";
|
||
pub const PROFILE_RECHARGE_PRODUCT_KIND_MEMBERSHIP: &str = "membership";
|
||
pub const PROFILE_FEEDBACK_STATUS_OPEN: &str = "open";
|
||
pub const TRACKING_SCOPE_KIND_SITE: &str = "site";
|
||
pub const TRACKING_SCOPE_KIND_WORK: &str = "work";
|
||
pub const TRACKING_SCOPE_KIND_MODULE: &str = "module";
|
||
pub const TRACKING_SCOPE_KIND_USER: &str = "user";
|
||
pub const ANALYTICS_GRANULARITY_DAY: &str = "day";
|
||
pub const ANALYTICS_GRANULARITY_WEEK: &str = "week";
|
||
pub const ANALYTICS_GRANULARITY_MONTH: &str = "month";
|
||
pub const ANALYTICS_GRANULARITY_QUARTER: &str = "quarter";
|
||
pub const ANALYTICS_GRANULARITY_YEAR: &str = "year";
|
||
#[cfg(any())]
|
||
pub const BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial";
|
||
#[cfg(any())]
|
||
pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane";
|
||
#[cfg(any())]
|
||
pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina";
|
||
#[cfg(any())]
|
||
pub const BROWSE_HISTORY_THEME_MODE_TIDE: &str = "tide";
|
||
#[cfg(any())]
|
||
pub const BROWSE_HISTORY_THEME_MODE_RIFT: &str = "rift";
|
||
#[cfg(any())]
|
||
pub const BROWSE_HISTORY_THEME_MODE_MYTHIC: &str = "mythic";
|
||
#[cfg(any())]
|
||
pub const CUSTOM_WORLD_VISIBILITY_DRAFT: &str = "draft";
|
||
#[cfg(any())]
|
||
pub const CUSTOM_WORLD_VISIBILITY_PUBLISHED: &str = "published";
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RuntimeSettingsResponse {
|
||
pub music_volume: f32,
|
||
pub platform_theme: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PutRuntimeSettingsRequest {
|
||
pub music_volume: f32,
|
||
pub platform_theme: String,
|
||
}
|
||
|
||
#[cfg(any())]
|
||
mod retired_runtime_support_contracts {
|
||
use super::*;
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SavedGameSnapshotResponse {
|
||
pub version: u32,
|
||
pub saved_at: String,
|
||
pub game_state: serde_json::Value,
|
||
pub bottom_tab: String,
|
||
pub current_story: Option<serde_json::Value>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PutSavedGameSnapshotRequest {
|
||
pub game_state: serde_json::Value,
|
||
pub bottom_tab: String,
|
||
#[serde(default)]
|
||
pub current_story: Option<serde_json::Value>,
|
||
#[serde(default)]
|
||
pub saved_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||
pub struct PutRuntimeSaveCheckpointRequest {
|
||
pub session_id: String,
|
||
pub bottom_tab: String,
|
||
#[serde(default)]
|
||
pub saved_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct BasicOkResponse {
|
||
pub ok: bool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PlatformBrowseHistoryEntryResponse {
|
||
pub owner_user_id: String,
|
||
pub profile_id: String,
|
||
pub world_name: String,
|
||
pub subtitle: String,
|
||
pub summary_text: String,
|
||
pub cover_image_src: Option<String>,
|
||
pub theme_mode: String,
|
||
pub author_display_name: String,
|
||
pub visited_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PlatformBrowseHistoryWriteEntryRequest {
|
||
pub owner_user_id: String,
|
||
pub profile_id: String,
|
||
pub world_name: String,
|
||
#[serde(default)]
|
||
pub subtitle: Option<String>,
|
||
#[serde(default)]
|
||
pub summary_text: Option<String>,
|
||
#[serde(default)]
|
||
pub cover_image_src: Option<String>,
|
||
#[serde(default)]
|
||
pub theme_mode: Option<String>,
|
||
#[serde(default)]
|
||
pub author_display_name: Option<String>,
|
||
#[serde(default)]
|
||
pub visited_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PlatformBrowseHistoryBatchSyncRequest {
|
||
pub entries: Vec<PlatformBrowseHistoryWriteEntryRequest>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(untagged)]
|
||
pub enum PlatformBrowseHistoryUpsertRequest {
|
||
Single(PlatformBrowseHistoryWriteEntryRequest),
|
||
Batch(PlatformBrowseHistoryBatchSyncRequest),
|
||
}
|
||
|
||
impl PlatformBrowseHistoryUpsertRequest {
|
||
pub fn into_entries(self) -> Vec<PlatformBrowseHistoryWriteEntryRequest> {
|
||
match self {
|
||
Self::Single(entry) => vec![entry],
|
||
Self::Batch(batch) => batch.entries,
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct PlatformBrowseHistoryResponse {
|
||
pub entries: Vec<PlatformBrowseHistoryEntryResponse>,
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileDashboardSummaryResponse {
|
||
pub wallet_balance: u64,
|
||
pub total_play_time_ms: u64,
|
||
pub played_world_count: u32,
|
||
pub updated_at: Option<String>,
|
||
pub daily_free_points: ProfileDailyFreePointsResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileDailyFreePointsResponse {
|
||
pub day_key: i64,
|
||
pub granted_points: u64,
|
||
pub remaining_points: u64,
|
||
pub resets_at: String,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg_attr(feature = "ts-bindings", derive(ts_rs::TS))]
|
||
#[cfg_attr(
|
||
feature = "ts-bindings",
|
||
ts(
|
||
export,
|
||
export_to = concat!(
|
||
env!("CARGO_MANIFEST_DIR"),
|
||
"/../../../packages/shared/src/contracts/generated/"
|
||
)
|
||
)
|
||
)]
|
||
pub struct ProfileMudPointBalanceResponse {
|
||
/// 每日免费泥点:北京业务日有效,跨日清零并重置。
|
||
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
|
||
pub daily_free_points: u64,
|
||
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
|
||
pub daily_free_reset_points: u64,
|
||
pub daily_free_resets_at: String,
|
||
/// 月度泥点:会员每期额度,到刷新时刻清零再发新额度。
|
||
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
|
||
pub monthly_points: u64,
|
||
pub monthly_resets_at: Option<String>,
|
||
/// 永久泥点:不过期,创作照常扣点。
|
||
#[cfg_attr(feature = "ts-bindings", ts(type = "number"))]
|
||
pub permanent_points: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileWalletLedgerEntryResponse {
|
||
pub id: String,
|
||
pub amount_delta: i64,
|
||
pub balance_after: u64,
|
||
pub source_type: String,
|
||
pub created_at: String,
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
pub reason: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileWalletLedgerResponse {
|
||
pub entries: Vec<ProfileWalletLedgerEntryResponse>,
|
||
}
|
||
|
||
/// 会员档位 token;与前端 `ProfileMembershipPlan` 联合类型一一对应。
|
||
///
|
||
/// 用枚举而非 `String` 收口 wire 值:未知 token 在反序列化阶段即被拒绝,后台也不能再
|
||
/// 写入任意档位字符串。
|
||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum ProfileMembershipPlanToken {
|
||
Normal,
|
||
Starter,
|
||
Plus,
|
||
Pro,
|
||
Max,
|
||
}
|
||
|
||
impl ProfileMembershipPlanToken {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
Self::Normal => "normal",
|
||
Self::Starter => "starter",
|
||
Self::Plus => "plus",
|
||
Self::Pro => "pro",
|
||
Self::Max => "max",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 会员购买周期 token;与前端 `ProfileMembershipCycleKind` 一一对应。
|
||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum ProfileMembershipCycleKindToken {
|
||
Monthly,
|
||
Yearly,
|
||
}
|
||
|
||
impl ProfileMembershipCycleKindToken {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
Self::Monthly => "monthly",
|
||
Self::Yearly => "yearly",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 模型权限 token;与前端 `ProfileMembershipModelAccess` 一一对应。
|
||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum ProfileMembershipModelAccessToken {
|
||
Basic,
|
||
Full,
|
||
}
|
||
|
||
impl ProfileMembershipModelAccessToken {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
Self::Basic => "basic",
|
||
Self::Full => "full",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 会员订单权益变更类型 token;区分“新购”与“补差升级”。
|
||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum ProfileMembershipChangeKindToken {
|
||
Purchase,
|
||
Upgrade,
|
||
}
|
||
|
||
impl ProfileMembershipChangeKindToken {
|
||
pub fn as_str(&self) -> &'static str {
|
||
match self {
|
||
Self::Purchase => "purchase",
|
||
Self::Upgrade => "upgrade",
|
||
}
|
||
}
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileRechargeProductResponse {
|
||
pub product_id: String,
|
||
pub title: String,
|
||
pub price_cents: u64,
|
||
pub kind: String,
|
||
pub points_amount: u64,
|
||
pub bonus_points: u64,
|
||
pub duration_days: u32,
|
||
pub badge_label: String,
|
||
pub description: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileMembershipPlanResponse {
|
||
/// 档位 token:`normal` / `starter` / `plus` / `pro` / `max`。
|
||
pub plan: ProfileMembershipPlanToken,
|
||
pub title: String,
|
||
pub rank: u8,
|
||
pub month_price_cents: u64,
|
||
pub year_price_cents: u64,
|
||
pub period_points: u64,
|
||
/// 模型权限 token:`basic` / `full`。
|
||
pub model_access: ProfileMembershipModelAccessToken,
|
||
/// 并发上限;`128` 表示不设上限。
|
||
pub concurrent_job_limit: u32,
|
||
/// 是否为运营推荐档位;由后端目录决定,前端不再硬编码档位 token。
|
||
pub recommended: bool,
|
||
/// 是否不设并发上限;由后端按哨兵值派生,前端不再硬编码 `128`。
|
||
pub unlimited_concurrency: bool,
|
||
pub enabled: bool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileMembershipResponse {
|
||
pub status: String,
|
||
/// 当前档位 token;非会员与已到期都是 `normal`。
|
||
pub plan: ProfileMembershipPlanToken,
|
||
pub started_at: Option<String>,
|
||
pub expires_at: Option<String>,
|
||
pub updated_at: Option<String>,
|
||
pub cycle_started_at: Option<String>,
|
||
pub cycle_resets_at: Option<String>,
|
||
pub cycle_granted_points: u64,
|
||
pub cycle_remaining_points: u64,
|
||
/// 当前期序号(1-based)。
|
||
pub cycle_index: u32,
|
||
/// 本次购买总期数(月付 1 / 年付 12)。
|
||
pub cycle_count: u32,
|
||
/// 周期 token:`monthly` / `yearly`。
|
||
pub cycle_kind: ProfileMembershipCycleKindToken,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileRechargeOrderResponse {
|
||
pub order_id: String,
|
||
pub product_id: String,
|
||
pub product_title: String,
|
||
pub kind: String,
|
||
pub amount_cents: u64,
|
||
pub status: String,
|
||
pub payment_channel: String,
|
||
pub paid_at: Option<String>,
|
||
pub provider_transaction_id: Option<String>,
|
||
pub created_at: String,
|
||
pub points_delta: i64,
|
||
pub membership_expires_at: Option<String>,
|
||
pub expired_at: Option<String>,
|
||
pub expiration_checked_at: Option<String>,
|
||
pub expiration_provider_state: Option<String>,
|
||
pub expiration_last_error: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct WechatMiniProgramPayParamsResponse {
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub app_id: Option<String>,
|
||
pub time_stamp: String,
|
||
pub nonce_str: String,
|
||
pub package: String,
|
||
pub sign_type: String,
|
||
pub pay_sign: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct WechatMiniProgramVirtualPayParamsResponse {
|
||
pub mode: String,
|
||
pub sign_data: String,
|
||
pub pay_sig: String,
|
||
pub signature: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(untagged)]
|
||
pub enum WechatMiniProgramPaymentParamsResponse {
|
||
Ordinary(WechatMiniProgramPayParamsResponse),
|
||
Virtual(WechatMiniProgramVirtualPayParamsResponse),
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct WechatH5PaymentResponse {
|
||
pub h5_url: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct WechatNativePaymentResponse {
|
||
pub code_url: String,
|
||
pub expires_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileRechargeCenterResponse {
|
||
pub mud_point_balance: ProfileMudPointBalanceResponse,
|
||
pub membership: ProfileMembershipResponse,
|
||
pub point_products: Vec<ProfileRechargeProductResponse>,
|
||
/// 会员档位目录(后台可改价),会员购买 / 升级的价格与权益都以它为准。
|
||
pub membership_plans: Vec<ProfileMembershipPlanResponse>,
|
||
pub latest_order: Option<ProfileRechargeOrderResponse>,
|
||
pub has_points_recharged: bool,
|
||
pub daily_free_points: ProfileDailyFreePointsResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CreateProfileRechargeOrderRequest {
|
||
pub product_id: String,
|
||
#[serde(default)]
|
||
pub payment_channel: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CreateProfileRechargeOrderResponse {
|
||
pub order: ProfileRechargeOrderResponse,
|
||
pub center: ProfileRechargeCenterResponse,
|
||
#[serde(default)]
|
||
pub wechat_mini_program_pay_params: Option<WechatMiniProgramPaymentParamsResponse>,
|
||
#[serde(default)]
|
||
pub wechat_h5_payment: Option<WechatH5PaymentResponse>,
|
||
#[serde(default)]
|
||
pub wechat_native_payment: Option<WechatNativePaymentResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ConfirmWechatProfileRechargeOrderResponse {
|
||
pub order: ProfileRechargeOrderResponse,
|
||
pub center: ProfileRechargeCenterResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct WechatProfileRechargeOrderDoneEvent {
|
||
pub order_id: String,
|
||
pub status: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct WechatProfileRechargeOrderErrorEvent {
|
||
pub message: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileFeedbackEvidenceItemRequest {
|
||
pub file_name: String,
|
||
pub content_type: String,
|
||
pub size_bytes: u64,
|
||
pub data_url: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SubmitProfileFeedbackRequest {
|
||
pub description: String,
|
||
#[serde(default)]
|
||
pub contact_phone: Option<String>,
|
||
#[serde(default)]
|
||
pub evidence_items: Vec<ProfileFeedbackEvidenceItemRequest>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileFeedbackEvidenceItemResponse {
|
||
pub evidence_id: String,
|
||
pub file_name: String,
|
||
pub content_type: String,
|
||
pub size_bytes: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileFeedbackSubmissionResponse {
|
||
pub feedback_id: String,
|
||
pub status: String,
|
||
pub created_at: String,
|
||
pub evidence_items: Vec<ProfileFeedbackEvidenceItemResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SubmitProfileFeedbackResponse {
|
||
pub feedback: ProfileFeedbackSubmissionResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileReferralInvitedUserResponse {
|
||
pub user_id: String,
|
||
pub display_name: String,
|
||
pub avatar_url: Option<String>,
|
||
pub bound_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileReferralInviteCenterResponse {
|
||
pub invite_code: String,
|
||
pub invite_link_path: String,
|
||
pub invited_count: u32,
|
||
pub rewarded_invite_count: u32,
|
||
pub today_inviter_reward_count: u32,
|
||
pub today_inviter_reward_remaining: u32,
|
||
pub reward_points: u64,
|
||
pub invited_users: Vec<ProfileReferralInvitedUserResponse>,
|
||
pub has_redeemed_code: bool,
|
||
pub bound_inviter_user_id: Option<String>,
|
||
pub bound_at: Option<String>,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RedeemProfileReferralInviteCodeRequest {
|
||
pub invite_code: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RedeemProfileReferralInviteCodeResponse {
|
||
pub center: ProfileReferralInviteCenterResponse,
|
||
pub invitee_reward_granted: bool,
|
||
pub inviter_reward_granted: bool,
|
||
pub invitee_balance_after: u64,
|
||
pub inviter_balance_after: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RedeemProfileRewardCodeRequest {
|
||
pub code: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct RedeemProfileRewardCodeResponse {
|
||
pub wallet_balance: u64,
|
||
pub amount_granted: u64,
|
||
pub ledger_entry: ProfileWalletLedgerEntryResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileTaskItemResponse {
|
||
pub task_id: String,
|
||
pub title: String,
|
||
pub description: String,
|
||
pub event_key: String,
|
||
pub cycle: String,
|
||
pub threshold: u32,
|
||
pub progress_count: u32,
|
||
pub reward_points: u64,
|
||
pub status: String,
|
||
pub day_key: i64,
|
||
pub claimed_at: Option<String>,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileTaskCenterResponse {
|
||
pub day_key: i64,
|
||
pub wallet_balance: u64,
|
||
pub tasks: Vec<ProfileTaskItemResponse>,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ClaimProfileTaskRewardResponse {
|
||
pub task_id: String,
|
||
pub day_key: i64,
|
||
pub reward_points: u64,
|
||
pub wallet_balance: u64,
|
||
pub ledger_entry: ProfileWalletLedgerEntryResponse,
|
||
pub center: ProfileTaskCenterResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileTaskConfigAdminResponse {
|
||
pub task_id: String,
|
||
pub title: String,
|
||
pub description: String,
|
||
pub event_key: String,
|
||
pub cycle: String,
|
||
pub scope_kind: String,
|
||
pub threshold: u32,
|
||
pub reward_points: u64,
|
||
pub enabled: bool,
|
||
pub sort_order: i32,
|
||
pub created_by: String,
|
||
pub created_by_display_name: String,
|
||
pub created_at: String,
|
||
pub updated_by: String,
|
||
pub updated_by_display_name: String,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileTaskConfigAdminListResponse {
|
||
pub entries: Vec<ProfileTaskConfigAdminResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileRechargeProductConfigAdminResponse {
|
||
pub product_id: String,
|
||
pub title: String,
|
||
pub price_cents: u64,
|
||
pub kind: String,
|
||
pub points_amount: u64,
|
||
pub bonus_points: u64,
|
||
pub duration_days: u32,
|
||
pub badge_label: String,
|
||
pub description: String,
|
||
pub enabled: bool,
|
||
pub sort_order: i32,
|
||
pub created_by: String,
|
||
pub created_at: String,
|
||
pub updated_by: String,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileRechargeProductConfigAdminListResponse {
|
||
pub entries: Vec<ProfileRechargeProductConfigAdminResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileWalletConfigAdminResponse {
|
||
pub config_id: String,
|
||
pub initial_mud_points: u64,
|
||
pub created_by: String,
|
||
pub created_by_display_name: String,
|
||
pub created_at: String,
|
||
pub updated_by: String,
|
||
pub updated_by_display_name: String,
|
||
pub updated_at: String,
|
||
pub daily_free_points_per_day: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AnalyticsMetricQueryRequest {
|
||
pub event_key: String,
|
||
pub scope_kind: String,
|
||
pub scope_id: String,
|
||
pub granularity: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AnalyticsBucketMetricResponse {
|
||
pub bucket_key: String,
|
||
pub bucket_start_date_key: i64,
|
||
pub bucket_end_date_key: i64,
|
||
pub value: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AnalyticsMetricQueryResponse {
|
||
pub buckets: Vec<AnalyticsBucketMetricResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminUpsertProfileTaskConfigRequest {
|
||
pub task_id: String,
|
||
pub title: String,
|
||
#[serde(default)]
|
||
pub description: Option<String>,
|
||
pub event_key: String,
|
||
pub cycle: String,
|
||
pub scope_kind: String,
|
||
pub threshold: u32,
|
||
pub reward_points: u64,
|
||
#[serde(default = "default_true")]
|
||
pub enabled: bool,
|
||
#[serde(default)]
|
||
pub sort_order: Option<i32>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminUpsertProfileRechargeProductRequest {
|
||
pub product_id: String,
|
||
pub title: String,
|
||
pub price_cents: u64,
|
||
pub kind: String,
|
||
pub points_amount: u64,
|
||
pub bonus_points: u64,
|
||
pub duration_days: u32,
|
||
#[serde(default)]
|
||
pub badge_label: Option<String>,
|
||
#[serde(default)]
|
||
pub description: Option<String>,
|
||
#[serde(default = "default_true")]
|
||
pub enabled: bool,
|
||
#[serde(default)]
|
||
pub sort_order: Option<i32>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileMembershipUpgradeQuoteRequest {
|
||
/// 目标档位 token:`starter` / `plus` / `pro` / `max`。
|
||
pub target_plan: ProfileMembershipPlanToken,
|
||
}
|
||
|
||
/// 补差升级报价:付款前展示应付金额、补点与变更后的月度余额;下单时后端会用同一函数重算。
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileMembershipUpgradeQuoteResponse {
|
||
pub current_plan: ProfileMembershipPlanToken,
|
||
pub target_plan: ProfileMembershipPlanToken,
|
||
/// 周期 token:`monthly` / `yearly`;有效期内不支持月年互换,所以变更前后一致。
|
||
pub cycle_kind: ProfileMembershipCycleKindToken,
|
||
pub cycle_index: u32,
|
||
pub cycle_count: u32,
|
||
/// 应付金额(分);月付 = 月价差,年付 = 年价差 ÷ 12 × (后续完整月数 + 本期剩余比例),向上取整到分。
|
||
pub amount_cents: u64,
|
||
/// 本期补点;年付按本期剩余比例向下取整。
|
||
pub granted_points_delta: u64,
|
||
/// 变更后月度余额 = 原剩余月度余额 + 补点(已用点不返还,永久泥点不动)。
|
||
pub monthly_balance_after: u64,
|
||
pub expires_at: Option<String>,
|
||
pub cycle_resets_at: Option<String>,
|
||
pub remaining_full_months: u32,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileMembershipPlanAdminListResponse {
|
||
pub entries: Vec<ProfileMembershipPlanResponse>,
|
||
/// 并发哨兵阈值:`concurrent_job_limit >= 该值` 即不设上限;由后端下发,前端不硬编码。
|
||
pub unlimited_concurrency_threshold: u32,
|
||
}
|
||
|
||
/// 后台按档位改会员价格与权益;`rank` 不在请求里,升级比较基准由代码内目录决定。
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminUpsertProfileMembershipPlanRequest {
|
||
/// 档位 token,作为主键即档位身份。
|
||
pub plan: String,
|
||
pub title: String,
|
||
pub month_price_cents: u64,
|
||
pub year_price_cents: u64,
|
||
pub period_points: u64,
|
||
/// 模型权限 token:`basic` / `full`;本期只落字段、不做服务端拦截。
|
||
pub model_access: String,
|
||
/// 并发上限;`128` 表示不设上限。
|
||
pub concurrent_job_limit: u32,
|
||
#[serde(default = "default_true")]
|
||
pub enabled: bool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminUpsertProfileWalletConfigRequest {
|
||
pub initial_mud_points: u64,
|
||
pub daily_free_points_per_day: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminDisableProfileTaskConfigRequest {
|
||
pub task_id: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminUpsertProfileRedeemCodeRequest {
|
||
pub code: String,
|
||
pub mode: String,
|
||
pub reward_points: u64,
|
||
pub max_uses: u32,
|
||
#[serde(default = "default_true")]
|
||
pub enabled: bool,
|
||
#[serde(default)]
|
||
pub allowed_user_ids: Vec<String>,
|
||
#[serde(default)]
|
||
pub allowed_public_user_codes: Vec<String>,
|
||
#[serde(default)]
|
||
pub starts_at: Option<String>,
|
||
#[serde(default)]
|
||
pub expires_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminUpsertProfileInviteCodeRequest {
|
||
pub invite_code: String,
|
||
#[serde(default)]
|
||
pub metadata: Option<serde_json::Value>,
|
||
#[serde(default)]
|
||
pub starts_at: Option<String>,
|
||
#[serde(default)]
|
||
pub expires_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct AdminDisableProfileRedeemCodeRequest {
|
||
pub code: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileRedeemCodeAdminResponse {
|
||
pub code: String,
|
||
pub mode: String,
|
||
pub reward_points: u64,
|
||
pub max_uses: u32,
|
||
pub global_used_count: u32,
|
||
pub enabled: bool,
|
||
pub allowed_user_ids: Vec<String>,
|
||
pub created_by: String,
|
||
pub created_at: String,
|
||
pub updated_at: String,
|
||
pub starts_at: Option<String>,
|
||
pub expires_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileCodeOperationAdminResponse {
|
||
pub operation_id: String,
|
||
pub code_kind: String,
|
||
pub code: String,
|
||
pub action: String,
|
||
pub operator_user_id: String,
|
||
pub operator_display_name: String,
|
||
pub created_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileRedeemCodeAdminListResponse {
|
||
pub entries: Vec<ProfileRedeemCodeAdminResponse>,
|
||
pub operations: Vec<ProfileCodeOperationAdminResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileInviteCodeAdminResponse {
|
||
pub user_id: String,
|
||
pub invite_code: String,
|
||
pub metadata: serde_json::Value,
|
||
pub starts_at: Option<String>,
|
||
pub expires_at: Option<String>,
|
||
pub status: String,
|
||
pub created_at: String,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ProfileInviteCodeAdminListResponse {
|
||
pub entries: Vec<ProfileInviteCodeAdminResponse>,
|
||
pub operations: Vec<ProfileCodeOperationAdminResponse>,
|
||
}
|
||
|
||
fn default_true() -> bool {
|
||
true
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(any())]
|
||
pub struct ProfilePlayedWorkSummaryResponse {
|
||
pub world_key: String,
|
||
pub owner_user_id: Option<String>,
|
||
pub profile_id: Option<String>,
|
||
pub world_type: Option<String>,
|
||
pub world_title: String,
|
||
pub world_subtitle: String,
|
||
pub first_played_at: String,
|
||
pub last_played_at: String,
|
||
pub last_observed_play_time_ms: u64,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(any())]
|
||
pub struct ProfilePlayStatsResponse {
|
||
pub total_play_time_ms: u64,
|
||
pub played_works: Vec<ProfilePlayedWorkSummaryResponse>,
|
||
pub updated_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(any())]
|
||
pub struct ProfileSaveArchiveSummaryResponse {
|
||
pub world_key: String,
|
||
pub owner_user_id: Option<String>,
|
||
pub profile_id: Option<String>,
|
||
pub world_type: Option<String>,
|
||
pub world_name: String,
|
||
pub subtitle: String,
|
||
pub summary_text: String,
|
||
pub cover_image_src: Option<String>,
|
||
pub last_played_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(any())]
|
||
pub struct ProfileSaveArchiveListResponse {
|
||
pub entries: Vec<ProfileSaveArchiveSummaryResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(any())]
|
||
pub struct ProfileSaveArchiveResumeResponse {
|
||
pub entry: ProfileSaveArchiveSummaryResponse,
|
||
pub snapshot: SavedGameSnapshotResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(any())]
|
||
pub struct RuntimeInventorySlotResponse {
|
||
pub slot_id: String,
|
||
pub container_kind: String,
|
||
pub slot_key: String,
|
||
pub item_id: String,
|
||
pub category: String,
|
||
pub name: String,
|
||
pub description: Option<String>,
|
||
pub quantity: u32,
|
||
pub rarity: String,
|
||
pub tags: Vec<String>,
|
||
pub stackable: bool,
|
||
pub stack_key: String,
|
||
pub equipment_slot_id: Option<String>,
|
||
pub source_kind: String,
|
||
pub source_reference_id: Option<String>,
|
||
pub created_at: String,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
#[cfg(any())]
|
||
pub struct RuntimeInventoryStateResponse {
|
||
pub runtime_session_id: String,
|
||
pub actor_user_id: String,
|
||
pub backpack_items: Vec<RuntimeInventorySlotResponse>,
|
||
pub equipment_items: Vec<RuntimeInventorySlotResponse>,
|
||
}
|
||
|
||
#[cfg(any())]
|
||
mod retired_custom_world_contracts {
|
||
use super::*;
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldProfileUpsertRequest {
|
||
pub profile: serde_json::Value,
|
||
pub source_agent_session_id: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct GenerateCustomWorldProfileRequest {
|
||
pub setting_text: String,
|
||
#[serde(default)]
|
||
pub creator_intent: Option<serde_json::Value>,
|
||
#[serde(default)]
|
||
pub generation_mode: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldLibraryEntryResponse {
|
||
pub owner_user_id: String,
|
||
pub profile_id: String,
|
||
pub public_work_code: Option<String>,
|
||
pub author_public_user_code: Option<String>,
|
||
pub profile: serde_json::Value,
|
||
pub visibility: String,
|
||
pub published_at: Option<String>,
|
||
pub updated_at: String,
|
||
pub author_display_name: String,
|
||
pub world_name: String,
|
||
pub subtitle: String,
|
||
pub summary_text: String,
|
||
pub cover_image_src: Option<String>,
|
||
pub theme_mode: String,
|
||
pub playable_npc_count: u32,
|
||
pub landmark_count: u32,
|
||
#[serde(default)]
|
||
pub play_count: u32,
|
||
#[serde(default)]
|
||
pub remix_count: u32,
|
||
#[serde(default)]
|
||
pub like_count: u32,
|
||
#[serde(default)]
|
||
pub recent_play_count_7d: u32,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldGalleryCardResponse {
|
||
pub owner_user_id: String,
|
||
pub profile_id: String,
|
||
pub public_work_code: String,
|
||
pub author_public_user_code: String,
|
||
pub visibility: String,
|
||
pub published_at: Option<String>,
|
||
pub updated_at: String,
|
||
pub author_display_name: String,
|
||
pub world_name: String,
|
||
pub subtitle: String,
|
||
pub summary_text: String,
|
||
pub cover_image_src: Option<String>,
|
||
pub theme_mode: String,
|
||
pub playable_npc_count: u32,
|
||
pub landmark_count: u32,
|
||
#[serde(default)]
|
||
pub play_count: u32,
|
||
#[serde(default)]
|
||
pub remix_count: u32,
|
||
#[serde(default)]
|
||
pub like_count: u32,
|
||
#[serde(default)]
|
||
pub recent_play_count_7d: u32,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldLibraryResponse {
|
||
pub entries: Vec<CustomWorldLibraryEntryResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldLibraryMutationResponse {
|
||
pub entry: CustomWorldLibraryEntryResponse,
|
||
pub entries: Vec<CustomWorldLibraryEntryResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldGalleryResponse {
|
||
pub entries: Vec<CustomWorldGalleryCardResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldGalleryDetailResponse {
|
||
pub entry: CustomWorldLibraryEntryResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldWorkSummaryResponse {
|
||
pub work_id: String,
|
||
pub source_type: String,
|
||
pub status: String,
|
||
pub title: String,
|
||
pub subtitle: String,
|
||
pub summary: String,
|
||
pub cover_image_src: Option<String>,
|
||
#[serde(default)]
|
||
pub cover_render_mode: Option<String>,
|
||
#[serde(default)]
|
||
pub cover_character_image_srcs: Vec<String>,
|
||
pub updated_at: String,
|
||
pub published_at: Option<String>,
|
||
pub stage: Option<String>,
|
||
pub stage_label: Option<String>,
|
||
pub playable_npc_count: u32,
|
||
pub landmark_count: u32,
|
||
pub role_visual_ready_count: Option<u32>,
|
||
pub role_animation_ready_count: Option<u32>,
|
||
pub role_asset_summary_label: Option<String>,
|
||
pub session_id: Option<String>,
|
||
pub profile_id: Option<String>,
|
||
pub can_resume: bool,
|
||
pub can_enter_world: bool,
|
||
pub blocker_count: u32,
|
||
pub publish_ready: bool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldWorksResponse {
|
||
pub items: Vec<CustomWorldWorkSummaryResponse>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CreateCustomWorldAgentSessionRequest {
|
||
#[serde(default)]
|
||
pub seed_text: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct SendCustomWorldAgentMessageRequest {
|
||
pub client_message_id: String,
|
||
pub text: String,
|
||
#[serde(default)]
|
||
pub quick_fill_requested: Option<bool>,
|
||
#[serde(default)]
|
||
pub focus_card_id: Option<String>,
|
||
#[serde(default)]
|
||
pub selected_card_ids: Option<Vec<String>>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldAgentMessageResponse {
|
||
pub id: String,
|
||
pub role: String,
|
||
pub kind: String,
|
||
pub text: String,
|
||
pub created_at: String,
|
||
pub related_operation_id: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldAgentOperationResponse {
|
||
pub operation_id: String,
|
||
#[serde(rename = "type")]
|
||
pub operation_type: String,
|
||
pub status: String,
|
||
pub phase_label: String,
|
||
pub phase_detail: String,
|
||
pub progress: u32,
|
||
pub error: Option<String>,
|
||
pub started_at: Option<String>,
|
||
pub updated_at: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldDraftCardSummaryResponse {
|
||
pub id: String,
|
||
pub kind: String,
|
||
pub title: String,
|
||
pub subtitle: String,
|
||
pub summary: String,
|
||
pub status: String,
|
||
pub linked_ids: Vec<String>,
|
||
pub warning_count: u32,
|
||
pub asset_status: Option<String>,
|
||
pub asset_status_label: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldDraftCardDetailSectionResponse {
|
||
pub id: String,
|
||
pub label: String,
|
||
pub value: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldDraftCardDetailResponse {
|
||
pub id: String,
|
||
pub kind: String,
|
||
pub title: String,
|
||
pub sections: Vec<CustomWorldDraftCardDetailSectionResponse>,
|
||
pub linked_ids: Vec<String>,
|
||
pub locked: bool,
|
||
pub editable: bool,
|
||
pub editable_section_ids: Vec<String>,
|
||
pub warning_messages: Vec<String>,
|
||
pub asset_status: Option<String>,
|
||
pub asset_status_label: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldAgentCardDetailResponse {
|
||
pub card: CustomWorldDraftCardDetailResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldAgentCheckpointResponse {
|
||
pub checkpoint_id: String,
|
||
pub created_at: String,
|
||
pub label: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldSupportedActionResponse {
|
||
pub action: String,
|
||
pub enabled: bool,
|
||
pub reason: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldResultPreviewBlockerResponse {
|
||
pub id: String,
|
||
pub code: String,
|
||
pub message: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldPublishGateResponse {
|
||
pub profile_id: String,
|
||
pub blockers: Vec<CustomWorldResultPreviewBlockerResponse>,
|
||
pub blocker_count: u32,
|
||
pub publish_ready: bool,
|
||
pub can_enter_world: bool,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldCreationResultViewResponse {
|
||
pub session: CustomWorldAgentSessionSnapshotResponse,
|
||
pub profile: Option<serde_json::Value>,
|
||
pub profile_source: String,
|
||
pub target_stage: String,
|
||
pub generation_view_source: Option<String>,
|
||
pub result_view_source: Option<String>,
|
||
pub can_autosave_library: bool,
|
||
pub can_sync_result_profile: bool,
|
||
pub publish_ready: bool,
|
||
pub can_enter_world: bool,
|
||
pub blocker_count: u32,
|
||
pub recovery_action: String,
|
||
pub recovery_reason: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldAgentSessionSnapshotResponse {
|
||
pub session_id: String,
|
||
pub current_turn: u32,
|
||
pub anchor_content: serde_json::Value,
|
||
pub progress_percent: u32,
|
||
pub last_assistant_reply: Option<String>,
|
||
pub stage: String,
|
||
pub focus_card_id: Option<String>,
|
||
pub creator_intent: serde_json::Value,
|
||
pub creator_intent_readiness: serde_json::Value,
|
||
pub anchor_pack: serde_json::Value,
|
||
pub lock_state: serde_json::Value,
|
||
pub draft_profile: serde_json::Value,
|
||
pub messages: Vec<CustomWorldAgentMessageResponse>,
|
||
pub draft_cards: Vec<CustomWorldDraftCardSummaryResponse>,
|
||
pub pending_clarifications: Vec<serde_json::Value>,
|
||
pub suggested_actions: Vec<serde_json::Value>,
|
||
pub recommended_replies: Vec<String>,
|
||
pub quality_findings: Vec<serde_json::Value>,
|
||
pub asset_coverage: serde_json::Value,
|
||
pub checkpoints: Vec<CustomWorldAgentCheckpointResponse>,
|
||
pub supported_actions: Vec<CustomWorldSupportedActionResponse>,
|
||
pub publish_gate: Option<CustomWorldPublishGateResponse>,
|
||
pub result_preview: Option<serde_json::Value>,
|
||
pub updated_at: String,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct CustomWorldAgentSessionResponse {
|
||
pub session: CustomWorldAgentSessionSnapshotResponse,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ExecuteCustomWorldAgentActionRequest {
|
||
pub action: String,
|
||
#[serde(default)]
|
||
pub profile_id: Option<String>,
|
||
#[serde(default)]
|
||
pub draft_profile: Option<serde_json::Value>,
|
||
#[serde(default)]
|
||
pub legacy_result_profile: Option<serde_json::Value>,
|
||
#[serde(default)]
|
||
pub setting_text: Option<String>,
|
||
#[serde(default)]
|
||
pub card_id: Option<String>,
|
||
#[serde(default)]
|
||
pub sections: Option<Vec<ExecuteCustomWorldAgentDraftCardSectionPatch>>,
|
||
#[serde(default)]
|
||
pub profile: Option<serde_json::Value>,
|
||
#[serde(default)]
|
||
pub count: Option<u32>,
|
||
#[serde(default)]
|
||
pub role_type: Option<String>,
|
||
#[serde(default)]
|
||
pub prompt_text: Option<String>,
|
||
#[serde(default)]
|
||
pub anchor_card_ids: Option<Vec<String>>,
|
||
#[serde(default)]
|
||
pub role_ids: Option<Vec<String>>,
|
||
#[serde(default)]
|
||
pub role_id: Option<String>,
|
||
#[serde(default)]
|
||
pub portrait_path: Option<String>,
|
||
#[serde(default)]
|
||
pub generated_visual_asset_id: Option<String>,
|
||
#[serde(default)]
|
||
pub generated_animation_set_id: Option<String>,
|
||
#[serde(default)]
|
||
pub animation_map: Option<serde_json::Value>,
|
||
#[serde(default)]
|
||
pub scene_ids: Option<Vec<String>>,
|
||
#[serde(default)]
|
||
pub scene_id: Option<String>,
|
||
#[serde(default)]
|
||
pub scene_kind: Option<String>,
|
||
#[serde(default)]
|
||
pub image_src: Option<String>,
|
||
#[serde(default)]
|
||
pub generated_scene_asset_id: Option<String>,
|
||
#[serde(default)]
|
||
pub generated_scene_prompt: Option<String>,
|
||
#[serde(default)]
|
||
pub generated_scene_model: Option<String>,
|
||
#[serde(default)]
|
||
pub checkpoint_id: Option<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||
#[serde(rename_all = "camelCase")]
|
||
pub struct ExecuteCustomWorldAgentDraftCardSectionPatch {
|
||
pub section_id: String,
|
||
pub value: String,
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use serde_json::json;
|
||
|
||
#[test]
|
||
fn profile_wallet_config_uses_camel_case_daily_free_points() {
|
||
let request: AdminUpsertProfileWalletConfigRequest = serde_json::from_value(json!({
|
||
"initialMudPoints": 100,
|
||
"dailyFreePointsPerDay": 35
|
||
}))
|
||
.expect("wallet config request should deserialize");
|
||
assert_eq!(request.daily_free_points_per_day, 35);
|
||
|
||
let payload = serde_json::to_value(ProfileWalletConfigAdminResponse {
|
||
config_id: "profile_wallet".to_string(),
|
||
initial_mud_points: 100,
|
||
created_by: "owner-1".to_string(),
|
||
created_by_display_name: "管理员".to_string(),
|
||
created_at: "2026-07-31T00:00:00Z".to_string(),
|
||
updated_by: "owner-1".to_string(),
|
||
updated_by_display_name: "管理员".to_string(),
|
||
updated_at: "2026-07-31T00:00:00Z".to_string(),
|
||
daily_free_points_per_day: 35,
|
||
})
|
||
.expect("wallet config response should serialize");
|
||
|
||
assert_eq!(payload["dailyFreePointsPerDay"], json!(35));
|
||
}
|
||
|
||
#[test]
|
||
fn redeem_code_admin_contract_uses_optional_camel_case_validity_window() {
|
||
let request: AdminUpsertProfileRedeemCodeRequest = serde_json::from_value(json!({
|
||
"code": "GIFT",
|
||
"mode": "public",
|
||
"rewardPoints": 30,
|
||
"maxUses": 1,
|
||
"startsAt": "2026-07-13T00:00:00Z",
|
||
"expiresAt": null
|
||
}))
|
||
.expect("redeem code request should accept validity window");
|
||
assert_eq!(request.starts_at.as_deref(), Some("2026-07-13T00:00:00Z"));
|
||
assert_eq!(request.expires_at, None);
|
||
|
||
let response = ProfileRedeemCodeAdminResponse {
|
||
code: "GIFT".to_string(),
|
||
mode: "public".to_string(),
|
||
reward_points: 30,
|
||
max_uses: 1,
|
||
global_used_count: 0,
|
||
enabled: true,
|
||
allowed_user_ids: Vec::new(),
|
||
created_by: "admin".to_string(),
|
||
created_at: "2026-07-13T00:00:00Z".to_string(),
|
||
updated_at: "2026-07-13T00:00:00Z".to_string(),
|
||
starts_at: Some("2026-07-13T00:00:00Z".to_string()),
|
||
expires_at: None,
|
||
};
|
||
let payload = serde_json::to_value(response).expect("response should serialize");
|
||
assert_eq!(payload["startsAt"], json!("2026-07-13T00:00:00Z"));
|
||
assert_eq!(payload["expiresAt"], json!(null));
|
||
}
|
||
|
||
fn empty_mud_point_balance() -> ProfileMudPointBalanceResponse {
|
||
ProfileMudPointBalanceResponse {
|
||
daily_free_points: 0,
|
||
daily_free_reset_points: 20,
|
||
daily_free_resets_at: "2026-07-12T16:00:00Z".to_string(),
|
||
monthly_points: 0,
|
||
monthly_resets_at: None,
|
||
permanent_points: 0,
|
||
}
|
||
}
|
||
|
||
#[cfg(any())]
|
||
#[test]
|
||
fn runtime_settings_request_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(PutRuntimeSettingsRequest {
|
||
music_volume: 0.42,
|
||
platform_theme: RUNTIME_PLATFORM_THEME_LIGHT.to_string(),
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["platformTheme"], json!("light"));
|
||
let music_volume = payload["musicVolume"]
|
||
.as_f64()
|
||
.expect("musicVolume should serialize as number");
|
||
assert!((music_volume - 0.42).abs() < 0.0001);
|
||
}
|
||
|
||
#[cfg(any())]
|
||
#[test]
|
||
fn browse_history_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(PlatformBrowseHistoryResponse {
|
||
entries: vec![PlatformBrowseHistoryEntryResponse {
|
||
owner_user_id: "owner-1".to_string(),
|
||
profile_id: "profile-1".to_string(),
|
||
world_name: "世界".to_string(),
|
||
subtitle: "".to_string(),
|
||
summary_text: "".to_string(),
|
||
cover_image_src: None,
|
||
theme_mode: BROWSE_HISTORY_THEME_MODE_MYTHIC.to_string(),
|
||
author_display_name: "玩家".to_string(),
|
||
visited_at: "2026-04-21T00:00:00Z".to_string(),
|
||
}],
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["entries"][0]["ownerUserId"], json!("owner-1"));
|
||
assert_eq!(payload["entries"][0]["themeMode"], json!("mythic"));
|
||
assert_eq!(
|
||
payload["entries"][0]["visitedAt"],
|
||
json!("2026-04-21T00:00:00Z")
|
||
);
|
||
}
|
||
|
||
#[cfg(any())]
|
||
#[test]
|
||
fn browse_history_upsert_request_accepts_single_or_batch_shape() {
|
||
let single: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({
|
||
"ownerUserId": "owner-1",
|
||
"profileId": "profile-1",
|
||
"worldName": "世界"
|
||
}))
|
||
.expect("single shape should deserialize");
|
||
let batch: PlatformBrowseHistoryUpsertRequest = serde_json::from_value(json!({
|
||
"entries": [{
|
||
"ownerUserId": "owner-1",
|
||
"profileId": "profile-1",
|
||
"worldName": "世界"
|
||
}]
|
||
}))
|
||
.expect("batch shape should deserialize");
|
||
|
||
assert_eq!(single.into_entries().len(), 1);
|
||
assert_eq!(batch.into_entries().len(), 1);
|
||
}
|
||
|
||
#[test]
|
||
fn profile_dashboard_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(ProfileDashboardSummaryResponse {
|
||
wallet_balance: 8,
|
||
total_play_time_ms: 16,
|
||
played_world_count: 3,
|
||
updated_at: Some("2026-04-22T10:00:00Z".to_string()),
|
||
daily_free_points: ProfileDailyFreePointsResponse {
|
||
day_key: 20_646,
|
||
granted_points: 20,
|
||
remaining_points: 12,
|
||
resets_at: "2026-07-12T16:00:00Z".to_string(),
|
||
updated_at: "2026-07-12T08:00:00Z".to_string(),
|
||
},
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["walletBalance"], json!(8));
|
||
assert_eq!(payload["totalPlayTimeMs"], json!(16));
|
||
assert_eq!(payload["playedWorldCount"], json!(3));
|
||
assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z"));
|
||
assert_eq!(payload["dailyFreePoints"]["remainingPoints"], json!(12));
|
||
}
|
||
|
||
#[test]
|
||
fn profile_wallet_ledger_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(ProfileWalletLedgerResponse {
|
||
entries: vec![
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-1".to_string(),
|
||
amount_delta: 10,
|
||
balance_after: 10,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD
|
||
.to_string(),
|
||
created_at: "2026-04-22T09:59:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-2".to_string(),
|
||
amount_delta: 12,
|
||
balance_after: 80,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC.to_string(),
|
||
created_at: "2026-04-22T10:00:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-3".to_string(),
|
||
amount_delta: 30,
|
||
balance_after: 110,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD
|
||
.to_string(),
|
||
created_at: "2026-04-22T10:01:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-4".to_string(),
|
||
amount_delta: 30,
|
||
balance_after: 140,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD
|
||
.to_string(),
|
||
created_at: "2026-04-22T10:02:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-5".to_string(),
|
||
amount_delta: 60,
|
||
balance_after: 200,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE.to_string(),
|
||
created_at: "2026-04-22T10:03:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-6".to_string(),
|
||
amount_delta: -1,
|
||
balance_after: 199,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME
|
||
.to_string(),
|
||
created_at: "2026-04-22T10:04:00Z".to_string(),
|
||
reason: Some("生成美术素材".to_string()),
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-7".to_string(),
|
||
amount_delta: 1,
|
||
balance_after: 200,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND
|
||
.to_string(),
|
||
created_at: "2026-04-22T10:05:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-8".to_string(),
|
||
amount_delta: 2,
|
||
balance_after: 202,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM
|
||
.to_string(),
|
||
created_at: "2026-04-22T10:06:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
ProfileWalletLedgerEntryResponse {
|
||
id: "ledger-9".to_string(),
|
||
amount_delta: 10,
|
||
balance_after: 212,
|
||
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD.to_string(),
|
||
created_at: "2026-04-22T10:07:00Z".to_string(),
|
||
reason: None,
|
||
},
|
||
],
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["entries"][0]["amountDelta"], json!(10));
|
||
assert_eq!(payload["entries"][0]["balanceAfter"], json!(10));
|
||
assert_eq!(
|
||
payload["entries"][0]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][1]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][2]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][3]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][4]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][5]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME)
|
||
);
|
||
assert_eq!(payload["entries"][5]["reason"], json!("生成美术素材"));
|
||
assert!(payload["entries"][0].get("reason").is_none());
|
||
assert_eq!(
|
||
payload["entries"][6]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_REFUND)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][7]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_PUZZLE_AUTHOR_INCENTIVE_CLAIM)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][8]["sourceType"],
|
||
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_DAILY_TASK_REWARD)
|
||
);
|
||
assert_eq!(
|
||
payload["entries"][0]["createdAt"],
|
||
json!("2026-04-22T09:59:00Z")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn profile_task_center_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(ProfileTaskCenterResponse {
|
||
day_key: 20576,
|
||
wallet_balance: 18,
|
||
tasks: vec![ProfileTaskItemResponse {
|
||
task_id: "daily_login".to_string(),
|
||
title: "每日登录".to_string(),
|
||
description: "".to_string(),
|
||
event_key: "daily_login".to_string(),
|
||
cycle: PROFILE_TASK_CYCLE_DAILY.to_string(),
|
||
threshold: 1,
|
||
progress_count: 1,
|
||
reward_points: 10,
|
||
status: PROFILE_TASK_STATUS_CLAIMABLE.to_string(),
|
||
day_key: 20576,
|
||
claimed_at: None,
|
||
updated_at: "2026-05-03T00:00:00Z".to_string(),
|
||
}],
|
||
updated_at: "2026-05-03T00:00:00Z".to_string(),
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["walletBalance"], json!(18));
|
||
assert_eq!(payload["tasks"][0]["taskId"], json!("daily_login"));
|
||
assert_eq!(payload["tasks"][0]["rewardPoints"], json!(10));
|
||
assert_eq!(
|
||
payload["tasks"][0]["status"],
|
||
json!(PROFILE_TASK_STATUS_CLAIMABLE)
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn admin_task_config_request_accepts_defaults() {
|
||
let payload: AdminUpsertProfileTaskConfigRequest = serde_json::from_value(json!({
|
||
"taskId": "daily_login",
|
||
"title": "每日登录",
|
||
"eventKey": "daily_login",
|
||
"cycle": "daily",
|
||
"scopeKind": "user",
|
||
"threshold": 1,
|
||
"rewardPoints": 10
|
||
}))
|
||
.expect("request should deserialize");
|
||
|
||
assert_eq!(payload.description, None);
|
||
assert_eq!(payload.enabled, true);
|
||
assert_eq!(payload.sort_order, None);
|
||
}
|
||
|
||
#[test]
|
||
fn profile_recharge_center_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(ProfileRechargeCenterResponse {
|
||
mud_point_balance: ProfileMudPointBalanceResponse {
|
||
daily_free_points: 20,
|
||
daily_free_reset_points: 20,
|
||
daily_free_resets_at: "2026-07-12T16:00:00Z".to_string(),
|
||
monthly_points: 5,
|
||
monthly_resets_at: Some("2026-05-25T10:00:00Z".to_string()),
|
||
permanent_points: 4,
|
||
},
|
||
membership: ProfileMembershipResponse {
|
||
status: "active".to_string(),
|
||
plan: ProfileMembershipPlanToken::Plus,
|
||
started_at: Some("2026-04-25T10:00:00Z".to_string()),
|
||
expires_at: Some("2026-05-25T10:00:00Z".to_string()),
|
||
updated_at: Some("2026-04-25T10:00:00Z".to_string()),
|
||
cycle_started_at: Some("2026-04-25T10:00:00Z".to_string()),
|
||
cycle_resets_at: Some("2026-05-25T10:00:00Z".to_string()),
|
||
cycle_granted_points: 1_150,
|
||
cycle_remaining_points: 320,
|
||
cycle_index: 1,
|
||
cycle_count: 1,
|
||
cycle_kind: ProfileMembershipCycleKindToken::Monthly,
|
||
},
|
||
point_products: vec![ProfileRechargeProductResponse {
|
||
product_id: "points_60".to_string(),
|
||
title: "60泥点".to_string(),
|
||
price_cents: 600,
|
||
kind: "points".to_string(),
|
||
points_amount: 60,
|
||
bonus_points: 60,
|
||
duration_days: 0,
|
||
badge_label: String::new(),
|
||
description: "60泥点".to_string(),
|
||
}],
|
||
membership_plans: vec![ProfileMembershipPlanResponse {
|
||
plan: ProfileMembershipPlanToken::Plus,
|
||
title: "Plus".to_string(),
|
||
rank: 2,
|
||
month_price_cents: 9_900,
|
||
year_price_cents: 99_000,
|
||
period_points: 1_150,
|
||
model_access: ProfileMembershipModelAccessToken::Full,
|
||
concurrent_job_limit: 3,
|
||
recommended: true,
|
||
unlimited_concurrency: false,
|
||
enabled: true,
|
||
}],
|
||
latest_order: None,
|
||
has_points_recharged: false,
|
||
daily_free_points: ProfileDailyFreePointsResponse {
|
||
day_key: 20_646,
|
||
granted_points: 20,
|
||
remaining_points: 20,
|
||
resets_at: "2026-07-12T16:00:00Z".to_string(),
|
||
updated_at: "2026-07-12T08:00:00Z".to_string(),
|
||
},
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["mudPointBalance"]["permanentPoints"], json!(4));
|
||
assert_eq!(payload["mudPointBalance"]["monthlyPoints"], json!(5));
|
||
assert_eq!(payload["mudPointBalance"]["dailyFreePoints"], json!(20));
|
||
assert_eq!(
|
||
payload["mudPointBalance"]["monthlyResetsAt"],
|
||
json!("2026-05-25T10:00:00Z")
|
||
);
|
||
assert_eq!(payload["membership"]["plan"], json!("plus"));
|
||
assert_eq!(
|
||
payload["membership"]["expiresAt"],
|
||
json!("2026-05-25T10:00:00Z")
|
||
);
|
||
assert_eq!(payload["membership"]["cycleRemainingPoints"], json!(320));
|
||
assert_eq!(payload["membership"]["cycleKind"], json!("monthly"));
|
||
assert_eq!(payload["pointProducts"][0]["productId"], json!("points_60"));
|
||
assert_eq!(payload["pointProducts"][0]["title"], json!("60泥点"));
|
||
assert_eq!(payload["pointProducts"][0]["priceCents"], json!(600));
|
||
assert_eq!(payload["pointProducts"][0]["description"], json!("60泥点"));
|
||
assert_eq!(payload["membershipPlans"][0]["plan"], json!("plus"));
|
||
assert_eq!(
|
||
payload["membershipPlans"][0]["concurrentJobLimit"],
|
||
json!(3)
|
||
);
|
||
assert_eq!(payload["hasPointsRecharged"], json!(false));
|
||
assert_eq!(payload["dailyFreePoints"]["grantedPoints"], json!(20));
|
||
}
|
||
|
||
#[test]
|
||
fn create_profile_recharge_order_request_accepts_optional_channel() {
|
||
let payload: CreateProfileRechargeOrderRequest = serde_json::from_value(json!({
|
||
"productId": "member_month"
|
||
}))
|
||
.expect("request should deserialize");
|
||
|
||
assert_eq!(payload.product_id, "member_month");
|
||
assert_eq!(payload.payment_channel, None);
|
||
}
|
||
|
||
#[test]
|
||
fn create_profile_recharge_order_response_serializes_web_wechat_payloads() {
|
||
let order = ProfileRechargeOrderResponse {
|
||
order_id: "rcgtest001".to_string(),
|
||
product_id: "points_60".to_string(),
|
||
product_title: "60泥点".to_string(),
|
||
kind: "points".to_string(),
|
||
amount_cents: 600,
|
||
status: "pending".to_string(),
|
||
payment_channel: "wechat_native".to_string(),
|
||
paid_at: None,
|
||
provider_transaction_id: None,
|
||
created_at: "2026-05-15T10:00:00Z".to_string(),
|
||
points_delta: 0,
|
||
membership_expires_at: None,
|
||
expired_at: None,
|
||
expiration_checked_at: None,
|
||
expiration_provider_state: None,
|
||
expiration_last_error: None,
|
||
};
|
||
let center = ProfileRechargeCenterResponse {
|
||
mud_point_balance: empty_mud_point_balance(),
|
||
membership: ProfileMembershipResponse {
|
||
status: "normal".to_string(),
|
||
plan: ProfileMembershipPlanToken::Normal,
|
||
started_at: None,
|
||
expires_at: None,
|
||
updated_at: None,
|
||
cycle_started_at: None,
|
||
cycle_resets_at: None,
|
||
cycle_granted_points: 0,
|
||
cycle_remaining_points: 0,
|
||
cycle_index: 1,
|
||
cycle_count: 1,
|
||
cycle_kind: ProfileMembershipCycleKindToken::Monthly,
|
||
},
|
||
point_products: vec![],
|
||
membership_plans: vec![],
|
||
latest_order: None,
|
||
has_points_recharged: false,
|
||
daily_free_points: ProfileDailyFreePointsResponse {
|
||
day_key: 20_646,
|
||
granted_points: 20,
|
||
remaining_points: 20,
|
||
resets_at: "2026-07-12T16:00:00Z".to_string(),
|
||
updated_at: "2026-07-12T08:00:00Z".to_string(),
|
||
},
|
||
};
|
||
let payload = serde_json::to_value(CreateProfileRechargeOrderResponse {
|
||
order,
|
||
center,
|
||
wechat_mini_program_pay_params: None,
|
||
wechat_h5_payment: Some(WechatH5PaymentResponse {
|
||
h5_url: "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb".to_string(),
|
||
}),
|
||
wechat_native_payment: Some(WechatNativePaymentResponse {
|
||
code_url: "weixin://pay.weixin.qq.com/bizpayurl/up?pr=test".to_string(),
|
||
expires_at: "2026-05-15T10:05:00Z".to_string(),
|
||
}),
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(
|
||
payload["wechatH5Payment"]["h5Url"],
|
||
json!("https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb")
|
||
);
|
||
assert_eq!(
|
||
payload["wechatNativePayment"]["codeUrl"],
|
||
json!("weixin://pay.weixin.qq.com/bizpayurl/up?pr=test")
|
||
);
|
||
assert_eq!(
|
||
payload["wechatNativePayment"]["expiresAt"],
|
||
json!("2026-05-15T10:05:00Z")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn create_profile_recharge_order_response_serializes_virtual_wechat_payloads() {
|
||
let order = ProfileRechargeOrderResponse {
|
||
order_id: "rcgtest002".to_string(),
|
||
product_id: "member_month".to_string(),
|
||
product_title: "月卡".to_string(),
|
||
kind: "membership".to_string(),
|
||
amount_cents: 2800,
|
||
status: "pending".to_string(),
|
||
payment_channel: "wechat_mp_virtual".to_string(),
|
||
paid_at: None,
|
||
provider_transaction_id: None,
|
||
created_at: "2026-05-15T10:00:00Z".to_string(),
|
||
points_delta: 0,
|
||
membership_expires_at: Some("2026-06-15T10:00:00Z".to_string()),
|
||
expired_at: None,
|
||
expiration_checked_at: None,
|
||
expiration_provider_state: None,
|
||
expiration_last_error: None,
|
||
};
|
||
let center = ProfileRechargeCenterResponse {
|
||
mud_point_balance: empty_mud_point_balance(),
|
||
membership: ProfileMembershipResponse {
|
||
status: "normal".to_string(),
|
||
plan: ProfileMembershipPlanToken::Normal,
|
||
started_at: None,
|
||
expires_at: None,
|
||
updated_at: None,
|
||
cycle_started_at: None,
|
||
cycle_resets_at: None,
|
||
cycle_granted_points: 0,
|
||
cycle_remaining_points: 0,
|
||
cycle_index: 1,
|
||
cycle_count: 1,
|
||
cycle_kind: ProfileMembershipCycleKindToken::Monthly,
|
||
},
|
||
point_products: vec![],
|
||
membership_plans: vec![],
|
||
latest_order: None,
|
||
has_points_recharged: false,
|
||
daily_free_points: ProfileDailyFreePointsResponse {
|
||
day_key: 20_646,
|
||
granted_points: 20,
|
||
remaining_points: 20,
|
||
resets_at: "2026-07-12T16:00:00Z".to_string(),
|
||
updated_at: "2026-07-12T08:00:00Z".to_string(),
|
||
},
|
||
};
|
||
let payload = serde_json::to_value(CreateProfileRechargeOrderResponse {
|
||
order,
|
||
center,
|
||
wechat_mini_program_pay_params: Some(WechatMiniProgramPaymentParamsResponse::Virtual(
|
||
WechatMiniProgramVirtualPayParamsResponse {
|
||
mode: "short_series_goods".to_string(),
|
||
sign_data:
|
||
"{\"offerId\":\"offer-1\",\"productId\":\"member_month\",\"goodsPrice\":2800}"
|
||
.to_string(),
|
||
pay_sig: "pay-sig".to_string(),
|
||
signature: "user-sig".to_string(),
|
||
},
|
||
)),
|
||
wechat_h5_payment: None,
|
||
wechat_native_payment: None,
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(
|
||
payload["wechatMiniProgramPayParams"]["mode"],
|
||
json!("short_series_goods")
|
||
);
|
||
assert_eq!(
|
||
payload["wechatMiniProgramPayParams"]["signData"],
|
||
json!("{\"offerId\":\"offer-1\",\"productId\":\"member_month\",\"goodsPrice\":2800}")
|
||
);
|
||
assert_eq!(
|
||
payload["wechatMiniProgramPayParams"]["paySig"],
|
||
json!("pay-sig")
|
||
);
|
||
assert_eq!(
|
||
payload["wechatMiniProgramPayParams"]["signature"],
|
||
json!("user-sig")
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn profile_feedback_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(SubmitProfileFeedbackResponse {
|
||
feedback: ProfileFeedbackSubmissionResponse {
|
||
feedback_id: "feedback:user-1:1".to_string(),
|
||
status: PROFILE_FEEDBACK_STATUS_OPEN.to_string(),
|
||
created_at: "2026-05-08T10:00:00Z".to_string(),
|
||
evidence_items: vec![ProfileFeedbackEvidenceItemResponse {
|
||
evidence_id: "feedback:user-1:1:evidence:01".to_string(),
|
||
file_name: "问题截图.png".to_string(),
|
||
content_type: "image/png".to_string(),
|
||
size_bytes: 128,
|
||
}],
|
||
},
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(
|
||
payload["feedback"]["feedbackId"],
|
||
json!("feedback:user-1:1")
|
||
);
|
||
assert_eq!(
|
||
payload["feedback"]["status"],
|
||
json!(PROFILE_FEEDBACK_STATUS_OPEN)
|
||
);
|
||
assert_eq!(
|
||
payload["feedback"]["evidenceItems"][0]["contentType"],
|
||
json!("image/png")
|
||
);
|
||
assert_eq!(
|
||
payload["feedback"]["evidenceItems"][0]["sizeBytes"],
|
||
json!(128)
|
||
);
|
||
}
|
||
|
||
#[cfg(any())]
|
||
#[test]
|
||
fn profile_play_stats_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(ProfilePlayStatsResponse {
|
||
total_play_time_ms: 18,
|
||
played_works: vec![ProfilePlayedWorkSummaryResponse {
|
||
world_key: "builtin:WUXIA".to_string(),
|
||
owner_user_id: None,
|
||
profile_id: None,
|
||
world_type: Some("WUXIA".to_string()),
|
||
world_title: "武侠世界".to_string(),
|
||
world_subtitle: "".to_string(),
|
||
first_played_at: "2026-04-20T10:00:00Z".to_string(),
|
||
last_played_at: "2026-04-22T10:00:00Z".to_string(),
|
||
last_observed_play_time_ms: 1200,
|
||
}],
|
||
updated_at: Some("2026-04-22T10:00:00Z".to_string()),
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["totalPlayTimeMs"], json!(18));
|
||
assert_eq!(
|
||
payload["playedWorks"][0]["worldKey"],
|
||
json!("builtin:WUXIA")
|
||
);
|
||
assert_eq!(
|
||
payload["playedWorks"][0]["lastObservedPlayTimeMs"],
|
||
json!(1200)
|
||
);
|
||
assert_eq!(payload["updatedAt"], json!("2026-04-22T10:00:00Z"));
|
||
}
|
||
|
||
#[cfg(any())]
|
||
#[test]
|
||
fn runtime_inventory_state_response_uses_camel_case_fields() {
|
||
let payload = serde_json::to_value(RuntimeInventoryStateResponse {
|
||
runtime_session_id: "runtime_001".to_string(),
|
||
actor_user_id: "user_001".to_string(),
|
||
backpack_items: vec![RuntimeInventorySlotResponse {
|
||
slot_id: "invslot_001".to_string(),
|
||
container_kind: "backpack".to_string(),
|
||
slot_key: "invslot_001".to_string(),
|
||
item_id: "consumable_heal_potion".to_string(),
|
||
category: "消耗品".to_string(),
|
||
name: "疗伤药".to_string(),
|
||
description: Some("用于恢复少量气血。".to_string()),
|
||
quantity: 2,
|
||
rarity: "common".to_string(),
|
||
tags: vec!["healing".to_string()],
|
||
stackable: true,
|
||
stack_key: "heal_potion".to_string(),
|
||
equipment_slot_id: None,
|
||
source_kind: "treasure_reward".to_string(),
|
||
source_reference_id: Some("treasure_001".to_string()),
|
||
created_at: "2026-04-22T10:00:00Z".to_string(),
|
||
updated_at: "2026-04-22T10:01:00Z".to_string(),
|
||
}],
|
||
equipment_items: vec![],
|
||
})
|
||
.expect("payload should serialize");
|
||
|
||
assert_eq!(payload["runtimeSessionId"], json!("runtime_001"));
|
||
assert_eq!(payload["actorUserId"], json!("user_001"));
|
||
assert_eq!(payload["backpackItems"][0]["slotId"], json!("invslot_001"));
|
||
assert_eq!(
|
||
payload["backpackItems"][0]["sourceKind"],
|
||
json!("treasure_reward")
|
||
);
|
||
}
|
||
}
|