Files
Genarrative/server-rs/crates/shared-contracts/src/runtime.rs
T
k88936 abc92d230b chore(契约): 三池余额 DTO 交给 ts-rs 生成,同步 SpacetimeDB 绑定
- 给 ProfileMudPointBalanceResponse 加 ts-bindings 特性下的 ts_rs::TS 导出,u64 标 ts(type = "number") 避免前端分叉成 bigint
- 新增 packages/shared/src/contracts/generated/ProfileMudPointBalanceResponse.ts,runtime.ts 改为 re-export 不再手写第二份字段
- 新增 packages/shared/src/utils/mudPoints.ts:派生展示用合计与北京时间月度刷新文案
- 同步 spacetime-client 生成绑定:升级报价改名为 snapshot,与 module 侧改动对齐

Co-authored-by: Junie <junie@jetbrains.com>
2026-10-02 19:00:08 +08:00

2023 lines
72 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>,
}
#[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: String,
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: String,
/// 并发上限;`128` 表示不设上限。
pub concurrent_job_limit: u32,
pub sort_order: i32,
pub enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipResponse {
pub status: String,
/// 当前档位 token;非会员与已到期都是 `normal`。
pub plan: String,
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: String,
}
#[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: String,
}
/// 补差升级报价:付款前展示应付金额、补点与变更后的月度余额;下单时后端会用同一函数重算。
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileMembershipUpgradeQuoteResponse {
pub current_plan: String,
pub target_plan: String,
/// 周期 token:`monthly` / `yearly`;有效期内不支持月年互换,所以变更前后一致。
pub cycle_kind: String,
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>,
}
/// 后台按档位改会员价格与权益;`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,
#[serde(default)]
pub sort_order: Option<i32>,
}
#[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: "plus".to_string(),
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: "monthly".to_string(),
},
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: "plus".to_string(),
title: "Plus".to_string(),
rank: 2,
month_price_cents: 9_900,
year_price_cents: 99_000,
period_points: 1_150,
model_access: "full".to_string(),
concurrent_job_limit: 3,
sort_order: 2,
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: "normal".to_string(),
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: "monthly".to_string(),
},
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: "normal".to_string(),
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: "monthly".to_string(),
},
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")
);
}
}