use serde::{Deserialize, Serialize}; pub const RUNTIME_PLATFORM_THEME_LIGHT: &str = "light"; pub const RUNTIME_PLATFORM_THEME_DARK: &str = "dark"; 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_POINTS_RECHARGE: &str = "points_recharge"; 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 BROWSE_HISTORY_THEME_MODE_MARTIAL: &str = "martial"; pub const BROWSE_HISTORY_THEME_MODE_ARCANE: &str = "arcane"; pub const BROWSE_HISTORY_THEME_MODE_MACHINA: &str = "machina"; pub const BROWSE_HISTORY_THEME_MODE_TIDE: &str = "tide"; pub const BROWSE_HISTORY_THEME_MODE_RIFT: &str = "rift"; pub const BROWSE_HISTORY_THEME_MODE_MYTHIC: &str = "mythic"; pub const CUSTOM_WORLD_VISIBILITY_DRAFT: &str = "draft"; 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, } #[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, } #[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(default)] pub saved_at: Option, } #[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, 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, #[serde(default)] pub summary_text: Option, #[serde(default)] pub cover_image_src: Option, #[serde(default)] pub theme_mode: Option, #[serde(default)] pub author_display_name: Option, #[serde(default)] pub visited_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct PlatformBrowseHistoryBatchSyncRequest { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(untagged)] pub enum PlatformBrowseHistoryUpsertRequest { Single(PlatformBrowseHistoryWriteEntryRequest), Batch(PlatformBrowseHistoryBatchSyncRequest), } impl PlatformBrowseHistoryUpsertRequest { pub fn into_entries(self) -> Vec { 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, } #[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, } #[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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileWalletLedgerResponse { pub entries: Vec, } #[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, pub tier: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileMembershipBenefitResponse { pub benefit_name: String, pub normal_value: String, pub month_value: String, pub season_value: String, pub year_value: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileMembershipResponse { pub status: String, pub tier: String, pub started_at: Option, pub expires_at: Option, pub updated_at: Option, } #[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: String, pub created_at: String, pub points_delta: i64, pub membership_expires_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileRechargeCenterResponse { pub wallet_balance: u64, pub membership: ProfileMembershipResponse, pub point_products: Vec, pub membership_products: Vec, pub benefits: Vec, pub latest_order: Option, pub has_points_recharged: bool, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreateProfileRechargeOrderRequest { pub product_id: String, #[serde(default)] pub payment_channel: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreateProfileRechargeOrderResponse { pub order: ProfileRechargeOrderResponse, pub center: ProfileRechargeCenterResponse, } #[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 has_redeemed_code: bool, pub bound_inviter_user_id: Option, pub bound_at: Option, 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 ProfilePlayedWorkSummaryResponse { pub world_key: String, pub owner_user_id: Option, pub profile_id: Option, pub world_type: Option, 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")] pub struct ProfilePlayStatsResponse { pub total_play_time_ms: u64, pub played_works: Vec, pub updated_at: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileSaveArchiveSummaryResponse { pub world_key: String, pub owner_user_id: Option, pub profile_id: Option, pub world_type: Option, pub world_name: String, pub subtitle: String, pub summary_text: String, pub cover_image_src: Option, pub last_played_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileSaveArchiveListResponse { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct ProfileSaveArchiveResumeResponse { pub entry: ProfileSaveArchiveSummaryResponse, pub snapshot: SavedGameSnapshotResponse, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] 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, pub quantity: u32, pub rarity: String, pub tags: Vec, pub stackable: bool, pub stack_key: String, pub equipment_slot_id: Option, pub source_kind: String, pub source_reference_id: Option, pub created_at: String, pub updated_at: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct RuntimeInventoryStateResponse { pub runtime_session_id: String, pub actor_user_id: String, pub backpack_items: Vec, pub equipment_items: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldProfileUpsertRequest { pub profile: serde_json::Value, pub source_agent_session_id: Option, } #[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, pub author_public_user_code: Option, pub profile: serde_json::Value, pub visibility: String, pub published_at: Option, 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, pub theme_mode: String, pub playable_npc_count: u32, pub landmark_count: 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, 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, pub theme_mode: String, pub playable_npc_count: u32, pub landmark_count: u32, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldLibraryResponse { pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldLibraryMutationResponse { pub entry: CustomWorldLibraryEntryResponse, pub entries: Vec, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CustomWorldGalleryResponse { pub entries: Vec, } #[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, #[serde(default)] pub cover_render_mode: Option, #[serde(default)] pub cover_character_image_srcs: Vec, pub updated_at: String, pub published_at: Option, pub stage: Option, pub stage_label: Option, pub playable_npc_count: u32, pub landmark_count: u32, pub role_visual_ready_count: Option, pub role_animation_ready_count: Option, pub role_asset_summary_label: Option, pub session_id: Option, pub profile_id: Option, 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, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] pub struct CreateCustomWorldAgentSessionRequest { #[serde(default)] pub seed_text: Option, } #[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, #[serde(default)] pub focus_card_id: Option, #[serde(default)] pub selected_card_ids: Option>, } #[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, } #[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, pub started_at: Option, pub updated_at: Option, } #[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, pub warning_count: u32, pub asset_status: Option, pub asset_status_label: Option, } #[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, pub linked_ids: Vec, pub locked: bool, pub editable: bool, pub editable_section_ids: Vec, pub warning_messages: Vec, pub asset_status: Option, pub asset_status_label: Option, } #[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, } #[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, 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 CustomWorldAgentSessionSnapshotResponse { pub session_id: String, pub current_turn: u32, pub anchor_content: serde_json::Value, pub progress_percent: u32, pub last_assistant_reply: Option, pub stage: String, pub focus_card_id: Option, 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, pub draft_cards: Vec, pub pending_clarifications: Vec, pub suggested_actions: Vec, pub recommended_replies: Vec, pub quality_findings: Vec, pub asset_coverage: serde_json::Value, pub checkpoints: Vec, pub supported_actions: Vec, pub publish_gate: Option, pub result_preview: Option, 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, #[serde(default)] pub draft_profile: Option, #[serde(default)] pub legacy_result_profile: Option, #[serde(default)] pub setting_text: Option, #[serde(default)] pub card_id: Option, #[serde(default)] pub sections: Option>, #[serde(default)] pub profile: Option, #[serde(default)] pub count: Option, #[serde(default)] pub role_type: Option, #[serde(default)] pub prompt_text: Option, #[serde(default)] pub anchor_card_ids: Option>, #[serde(default)] pub role_ids: Option>, #[serde(default)] pub role_id: Option, #[serde(default)] pub portrait_path: Option, #[serde(default)] pub generated_visual_asset_id: Option, #[serde(default)] pub generated_animation_set_id: Option, #[serde(default)] pub animation_map: Option, #[serde(default)] pub scene_ids: Option>, #[serde(default)] pub scene_id: Option, #[serde(default)] pub scene_kind: Option, #[serde(default)] pub image_src: Option, #[serde(default)] pub generated_scene_asset_id: Option, #[serde(default)] pub generated_scene_prompt: Option, #[serde(default)] pub generated_scene_model: Option, #[serde(default)] pub checkpoint_id: Option, } #[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 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); } #[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") ); } #[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()), }) .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")); } #[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: 12, balance_after: 80, source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC.to_string(), created_at: "2026-04-22T10:00:00Z".to_string(), }], }) .expect("payload should serialize"); assert_eq!(payload["entries"][0]["amountDelta"], json!(12)); assert_eq!(payload["entries"][0]["balanceAfter"], json!(80)); assert_eq!(payload["entries"][0]["sourceType"], json!("snapshot_sync")); assert_eq!( payload["entries"][0]["createdAt"], json!("2026-04-22T10:00:00Z") ); } #[test] fn profile_recharge_center_response_uses_camel_case_fields() { let payload = serde_json::to_value(ProfileRechargeCenterResponse { wallet_balance: 29, membership: ProfileMembershipResponse { status: "active".to_string(), tier: "month".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()), }, 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: "首充双倍".to_string(), description: "首充送60叙世币".to_string(), tier: "normal".to_string(), }], membership_products: vec![], benefits: vec![], latest_order: None, has_points_recharged: false, }) .expect("payload should serialize"); assert_eq!(payload["walletBalance"], json!(29)); assert_eq!( payload["membership"]["expiresAt"], json!("2026-05-25T10:00:00Z") ); 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["hasPointsRecharged"], json!(false)); } #[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 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")); } #[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") ); } }