Merge remote-tracking branch 'origin/master' into codex/ddd

# Conflicts:
#	docs/technical/README.md
#	docs/technical/RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md
#	docs/technical/SPACETIMEDB_TABLE_CATALOG.md
#	scripts/generate-spacetime-bindings.mjs
#	server-rs/crates/api-server/src/app.rs
#	server-rs/crates/api-server/src/assets.rs
#	server-rs/crates/api-server/src/big_fish.rs
#	server-rs/crates/api-server/src/custom_world_ai.rs
#	server-rs/crates/api-server/src/llm.rs
#	server-rs/crates/api-server/src/main.rs
#	server-rs/crates/api-server/src/puzzle.rs
#	server-rs/crates/api-server/src/runtime_profile.rs
#	server-rs/crates/api-server/src/runtime_story/compat/ai.rs
#	server-rs/crates/api-server/src/runtime_story/compat/npc_actions.rs
#	server-rs/crates/api-server/src/runtime_story/compat/presentation.rs
#	server-rs/crates/api-server/src/runtime_story/compat/tests.rs
#	server-rs/crates/api-server/src/state.rs
#	server-rs/crates/module-auth/src/lib.rs
#	server-rs/crates/module-big-fish/src/lib.rs
#	server-rs/crates/module-custom-world/src/lib.rs
#	server-rs/crates/module-puzzle/src/lib.rs
#	server-rs/crates/module-runtime/src/lib.rs
#	server-rs/crates/spacetime-client/src/big_fish.rs
#	server-rs/crates/spacetime-client/src/lib.rs
#	server-rs/crates/spacetime-client/src/mapper.rs
#	server-rs/crates/spacetime-client/src/module_bindings/admin_disable_profile_redeem_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/admin_upsert_profile_redeem_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/advance_puzzle_next_level_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/append_ai_text_chunk_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/apply_chapter_progression_ledger_entry_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/attach_ai_result_reference_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/authorize_database_migration_operator_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/begin_story_session_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_run_type.rs
#	server-rs/crates/spacetime-client/src/module_bindings/bind_asset_object_to_entity_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/cancel_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/clear_platform_browse_history_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/compile_big_fish_draft_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/compile_custom_world_published_profile_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/compile_puzzle_agent_draft_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/complete_ai_stage_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/complete_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/confirm_asset_object_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/consume_profile_wallet_points_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/continue_story_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_battle_state_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_big_fish_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_custom_world_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_profile_recharge_order_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/create_puzzle_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_big_fish_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_puzzle_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/delete_runtime_snapshot_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/drag_puzzle_piece_or_group_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/execute_custom_world_agent_action_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/export_auth_store_snapshot_from_tables_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/export_database_migration_to_file_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/fail_ai_task_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/finalize_big_fish_agent_message_turn_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/finalize_custom_world_agent_message_turn_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/finalize_puzzle_agent_message_turn_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/generate_big_fish_asset_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_auth_store_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_battle_state_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_big_fish_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_chapter_progression_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_card_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_operation_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_by_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_gallery_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_custom_world_library_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_player_progression_or_default_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_dashboard_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_play_stats_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_recharge_center_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_profile_referral_invite_center_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_agent_session_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_gallery_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_run_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_puzzle_work_detail_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_runtime_inventory_state_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_runtime_setting_or_default_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_runtime_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/get_story_session_state_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/grant_player_progression_experience_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/import_auth_store_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_from_file_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/import_database_migration_incremental_from_file_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_asset_history_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_big_fish_works_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_gallery_entries_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_profiles_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_custom_world_works_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_platform_browse_history_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_profile_save_archives_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_profile_wallet_ledger_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_gallery_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/list_puzzle_works_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/mod.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_big_fish_game_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_custom_world_world_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/publish_puzzle_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/record_big_fish_play_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_referral_invite_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/redeem_profile_reward_code_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/refund_profile_wallet_points_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_combat_action_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_battle_interaction_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_interaction_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_npc_social_action_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resolve_treasure_interaction_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/resume_profile_save_archive_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/revoke_database_migration_operator_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/save_puzzle_generated_images_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/select_puzzle_cover_image_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/start_puzzle_run_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_big_fish_message_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_custom_world_agent_message_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_agent_message_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/submit_puzzle_leaderboard_entry_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/swap_puzzle_pieces_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/unpublish_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/update_puzzle_work_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_auth_store_snapshot_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_chapter_progression_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_agent_operation_progress_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_custom_world_profile_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_npc_state_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_platform_browse_history_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_setting_and_return_procedure.rs
#	server-rs/crates/spacetime-client/src/module_bindings/upsert_runtime_snapshot_and_return_procedure.rs
#	server-rs/crates/spacetime-module/src/auth/procedures.rs
#	server-rs/crates/spacetime-module/src/custom_world/mod.rs
#	server-rs/crates/spacetime-module/src/lib.rs
#	server-rs/crates/spacetime-module/src/migration.rs
#	server-rs/crates/spacetime-module/src/puzzle.rs
#	server-rs/crates/spacetime-module/src/runtime/profile.rs
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx
#	src/services/aiService.ts
#	src/services/puzzle-runtime/puzzleRuntimeClient.ts
This commit is contained in:
kdletters
2026-05-02 03:35:59 +08:00
513 changed files with 52813 additions and 6013 deletions

View File

@@ -19,10 +19,12 @@ pub struct AuthUserPayload {
pub public_user_code: String,
pub username: String,
pub display_name: String,
pub avatar_url: Option<String>,
pub phone_number_masked: Option<String>,
pub login_method: String,
pub binding_status: String,
pub wechat_bound: bool,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -31,6 +33,7 @@ pub struct PublicUserSummaryPayload {
pub id: String,
pub public_user_code: String,
pub display_name: String,
pub avatar_url: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -66,6 +69,19 @@ pub struct PasswordChangeResponse {
pub user: AuthUserPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileUpdateRequest {
pub display_name: Option<String>,
pub avatar_data_url: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ProfileUpdateResponse {
pub user: AuthUserPayload,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PasswordResetRequest {
@@ -149,6 +165,8 @@ pub struct PhoneSendCodeResponse {
pub struct PhoneLoginRequest {
pub phone: String,
pub code: String,
#[serde(default)]
pub invite_code: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -156,6 +174,19 @@ pub struct PhoneLoginRequest {
pub struct PhoneLoginResponse {
pub token: String,
pub user: AuthUserPayload,
pub created: bool,
pub referral: Option<PhoneLoginReferralResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PhoneLoginReferralResponse {
pub ok: bool,
pub message: Option<String>,
pub invitee_reward_granted: bool,
pub inviter_reward_granted: bool,
pub invitee_balance_after: Option<u64>,
pub inviter_balance_after: Option<u64>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -252,6 +283,23 @@ mod tests {
);
}
#[test]
fn profile_update_request_uses_camel_case_fields() {
let payload = serde_json::to_value(ProfileUpdateRequest {
display_name: Some("旅人甲".to_string()),
avatar_data_url: Some("data:image/png;base64,AAAA".to_string()),
})
.expect("payload should serialize");
assert_eq!(
payload,
json!({
"displayName": "旅人甲",
"avatarDataUrl": "data:image/png;base64,AAAA"
})
);
}
#[test]
fn wechat_callback_query_keeps_provider_compatible_field_names() {
let payload = serde_json::to_value(WechatCallbackQuery {

View File

@@ -6,6 +6,7 @@ pub struct BigFishWorkSummaryResponse {
pub work_id: String,
pub source_session_id: String,
pub owner_user_id: String,
pub author_display_name: String,
pub title: String,
pub subtitle: String,
pub summary: String,
@@ -13,6 +14,8 @@ pub struct BigFishWorkSummaryResponse {
pub cover_image_src: Option<String>,
pub status: String,
pub updated_at: String,
#[serde(default)]
pub published_at: Option<String>,
pub publish_ready: bool,
pub level_count: u32,
pub level_main_image_ready_count: u32,
@@ -20,6 +23,12 @@ pub struct BigFishWorkSummaryResponse {
pub background_ready: bool,
#[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)]

View File

@@ -7,6 +7,9 @@ pub mod big_fish;
pub mod big_fish_works;
pub mod creation_agent_document_input;
pub mod llm;
pub mod match3d_agent;
pub mod match3d_runtime;
pub mod match3d_works;
pub mod puzzle_agent;
pub mod puzzle_gallery;
pub mod puzzle_runtime;

View File

@@ -0,0 +1,161 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct CreateMatch3DAgentSessionRequest {
#[serde(default)]
pub seed_text: Option<String>,
#[serde(default)]
pub theme_text: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
#[serde(default)]
pub clear_count: Option<u32>,
#[serde(default)]
pub difficulty: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SendMatch3DAgentMessageRequest {
pub client_message_id: String,
pub text: String,
#[serde(default)]
pub quick_fill_requested: Option<bool>,
#[serde(default)]
pub reference_image_src: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ExecuteMatch3DAgentActionRequest {
pub action: String,
#[serde(default)]
pub game_name: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub tags: Option<Vec<String>>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub clear_count: Option<u32>,
#[serde(default)]
pub difficulty: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DCreatorConfigResponse {
pub theme_text: String,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DResultDraftResponse {
pub profile_id: String,
pub game_name: String,
pub theme_text: String,
#[serde(default)]
pub summary_text: Option<String>,
pub summary: String,
pub tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
pub total_item_count: u32,
pub publish_ready: bool,
pub blockers: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAnchorItemResponse {
pub key: String,
pub label: String,
pub value: String,
pub status: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAnchorPackResponse {
pub theme: Match3DAnchorItemResponse,
pub clear_count: Match3DAnchorItemResponse,
pub difficulty: Match3DAnchorItemResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentMessageResponse {
pub id: String,
pub role: String,
pub kind: String,
pub text: String,
pub created_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentSessionSnapshotResponse {
pub session_id: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,
pub anchor_pack: Match3DAnchorPackResponse,
#[serde(default)]
pub config: Option<Match3DCreatorConfigResponse>,
#[serde(default)]
pub draft: Option<Match3DResultDraftResponse>,
pub messages: Vec<Match3DAgentMessageResponse>,
#[serde(default)]
pub last_assistant_reply: Option<String>,
#[serde(default)]
pub published_profile_id: Option<String>,
pub updated_at: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentSessionResponse {
pub session: Match3DAgentSessionSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DAgentActionResponse {
pub session: Match3DAgentSessionSnapshotResponse,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn create_match3d_session_request_uses_camel_case() {
let payload = serde_json::to_value(CreateMatch3DAgentSessionRequest {
seed_text: Some("水果消除".to_string()),
theme_text: Some("水果".to_string()),
reference_image_src: Some("data:image/png;base64,abc".to_string()),
clear_count: Some(4),
difficulty: Some(3),
})
.expect("payload should serialize");
assert_eq!(payload["seedText"], json!("水果消除"));
assert_eq!(payload["themeText"], json!("水果"));
assert_eq!(
payload["referenceImageSrc"],
json!("data:image/png;base64,abc")
);
assert_eq!(payload["clearCount"], json!(4));
}
}

View File

@@ -0,0 +1,125 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct StartMatch3DRunRequest {
pub profile_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ClickMatch3DItemRequest {
#[serde(default)]
pub run_id: Option<String>,
pub item_instance_id: String,
pub client_snapshot_version: u64,
pub client_event_id: String,
pub clicked_at_ms: u64,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct StopMatch3DRunRequest {
pub client_action_id: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DItemSnapshotResponse {
pub item_instance_id: String,
pub item_type_id: String,
pub visual_key: String,
pub x: f32,
pub y: f32,
pub radius: f32,
pub layer: u32,
pub state: String,
pub clickable: bool,
#[serde(default)]
pub tray_slot_index: Option<u32>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DTraySlotResponse {
pub slot_index: u32,
#[serde(default)]
pub item_instance_id: Option<String>,
#[serde(default)]
pub item_type_id: Option<String>,
#[serde(default)]
pub visual_key: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DRunSnapshotResponse {
pub run_id: String,
pub profile_id: String,
pub owner_user_id: String,
pub status: String,
/// 对外 HTTP 快照版本。领域层内部字段名为 board_versionfacade 需要在这里完成映射。
pub snapshot_version: u64,
pub started_at_ms: u64,
pub duration_limit_ms: u64,
#[serde(default)]
pub server_now_ms: Option<u64>,
pub remaining_ms: u64,
pub clear_count: u32,
pub total_item_count: u32,
pub cleared_item_count: u32,
pub items: Vec<Match3DItemSnapshotResponse>,
pub tray_slots: Vec<Match3DTraySlotResponse>,
#[serde(default)]
pub failure_reason: Option<String>,
#[serde(default)]
pub last_confirmed_action_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DClickConfirmationResponse {
pub accepted: bool,
#[serde(default)]
pub reject_reason: Option<String>,
#[serde(default)]
pub entered_slot_index: Option<u32>,
pub cleared_item_instance_ids: Vec<String>,
pub run: Match3DRunSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DRunResponse {
pub run: Match3DRunSnapshotResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DClickResponse {
pub confirmation: Match3DClickConfirmationResponse,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn click_match3d_item_request_uses_camel_case() {
let payload = serde_json::to_value(ClickMatch3DItemRequest {
run_id: Some("run-1".to_string()),
item_instance_id: "item-1".to_string(),
client_snapshot_version: 7,
client_event_id: "event-1".to_string(),
clicked_at_ms: 12_345,
})
.expect("payload should serialize");
assert_eq!(payload["runId"], json!("run-1"));
assert_eq!(payload["itemInstanceId"], json!("item-1"));
assert_eq!(payload["clientSnapshotVersion"], json!(7));
assert_eq!(payload["clientEventId"], json!("event-1"));
assert_eq!(payload["clickedAtMs"], json!(12_345));
}
}

View File

@@ -0,0 +1,92 @@
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PutMatch3DWorkRequest {
pub game_name: String,
#[serde(default)]
pub theme_text: Option<String>,
pub summary: String,
pub tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkSummaryResponse {
pub work_id: String,
pub profile_id: String,
pub owner_user_id: String,
#[serde(default)]
pub source_session_id: Option<String>,
pub game_name: String,
pub theme_text: String,
pub summary: String,
pub tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
pub clear_count: u32,
pub difficulty: u32,
pub publication_status: String,
pub play_count: u32,
pub updated_at: String,
#[serde(default)]
pub published_at: Option<String>,
pub publish_ready: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkProfileResponse {
#[serde(flatten)]
pub summary: Match3DWorkSummaryResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorksResponse {
pub items: Vec<Match3DWorkSummaryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkDetailResponse {
pub item: Match3DWorkProfileResponse,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Match3DWorkMutationResponse {
pub item: Match3DWorkProfileResponse,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn match3d_work_request_uses_camel_case() {
let payload = serde_json::to_value(PutMatch3DWorkRequest {
game_name: "水果抓大鹅".to_string(),
theme_text: Some("水果".to_string()),
summary: "水果主题".to_string(),
tags: vec!["水果".to_string()],
cover_image_src: None,
reference_image_src: None,
clear_count: 4,
difficulty: 5,
})
.expect("payload should serialize");
assert_eq!(payload["gameName"], json!("水果抓大鹅"));
assert_eq!(payload["clearCount"], json!(4));
}
}

View File

@@ -5,6 +5,14 @@ use serde::{Deserialize, Serialize};
pub struct CreatePuzzleAgentSessionRequest {
#[serde(default)]
pub seed_text: Option<String>,
#[serde(default)]
pub work_title: Option<String>,
#[serde(default)]
pub work_description: Option<String>,
#[serde(default)]
pub picture_description: Option<String>,
#[serde(default)]
pub reference_image_src: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -29,11 +37,32 @@ pub struct ExecutePuzzleAgentActionRequest {
#[serde(default)]
pub candidate_id: Option<String>,
#[serde(default)]
pub level_id: Option<String>,
#[serde(default)]
pub work_title: Option<String>,
#[serde(default)]
pub work_description: Option<String>,
#[serde(default)]
pub picture_description: Option<String>,
#[serde(default)]
pub level_name: Option<String>,
#[serde(default)]
pub summary: Option<String>,
#[serde(default)]
pub theme_tags: Option<Vec<String>>,
#[serde(default)]
pub levels_json: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleFormDraftResponse {
#[serde(default)]
pub work_title: Option<String>,
#[serde(default)]
pub work_description: Option<String>,
#[serde(default)]
pub picture_description: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -84,6 +113,8 @@ pub struct PuzzleCreatorIntentResponse {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleResultDraftResponse {
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -99,6 +130,25 @@ pub struct PuzzleResultDraftResponse {
#[serde(default)]
pub cover_asset_id: Option<String>,
pub generation_status: String,
pub levels: Vec<PuzzleDraftLevelResponse>,
#[serde(default)]
pub form_draft: Option<PuzzleFormDraftResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleDraftLevelResponse {
pub level_id: String,
pub level_name: String,
pub picture_description: String,
pub candidates: Vec<PuzzleGeneratedImageCandidateResponse>,
#[serde(default)]
pub selected_candidate_id: Option<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
#[serde(default)]
pub cover_asset_id: Option<String>,
pub generation_status: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
@@ -149,6 +199,7 @@ pub struct PuzzleResultPreviewEnvelopeResponse {
#[serde(rename_all = "camelCase")]
pub struct PuzzleAgentSessionSnapshotResponse {
pub session_id: String,
pub seed_text: String,
pub current_turn: u32,
pub progress_percent: u32,
pub stage: String,

View File

@@ -1,6 +1,6 @@
use serde::{Deserialize, Serialize};
use crate::puzzle_works::PuzzleWorkSummaryResponse;
use crate::puzzle_works::{PuzzleWorkProfileResponse, PuzzleWorkSummaryResponse};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
@@ -11,5 +11,5 @@ pub struct PuzzleGalleryResponse {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleGalleryDetailResponse {
pub item: PuzzleWorkSummaryResponse,
pub item: PuzzleWorkProfileResponse,
}

View File

@@ -4,6 +4,8 @@ use serde::{Deserialize, Serialize};
#[serde(rename_all = "camelCase")]
pub struct StartPuzzleRunRequest {
pub profile_id: String,
#[serde(default)]
pub level_id: Option<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -21,6 +23,18 @@ pub struct DragPuzzlePieceRequest {
pub target_col: u32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UsePuzzleRuntimePropRequest {
pub prop_kind: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct UpdatePuzzleRuntimePauseRequest {
pub paused: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct SubmitPuzzleLeaderboardRequest {
@@ -84,6 +98,8 @@ pub struct PuzzleBoardSnapshotResponse {
pub struct PuzzleRuntimeLevelSnapshotResponse {
pub run_id: String,
pub level_index: u32,
#[serde(default)]
pub level_id: Option<String>,
pub grid_size: u32,
pub profile_id: String,
pub level_name: String,
@@ -100,9 +116,35 @@ pub struct PuzzleRuntimeLevelSnapshotResponse {
#[serde(default)]
pub elapsed_ms: Option<u64>,
#[serde(default)]
pub time_limit_ms: u64,
#[serde(default)]
pub remaining_ms: u64,
#[serde(default)]
pub paused_accumulated_ms: u64,
#[serde(default)]
pub pause_started_at_ms: Option<u64>,
#[serde(default)]
pub freeze_accumulated_ms: u64,
#[serde(default)]
pub freeze_started_at_ms: Option<u64>,
#[serde(default)]
pub freeze_until_ms: Option<u64>,
#[serde(default)]
pub leaderboard_entries: Vec<PuzzleLeaderboardEntryResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleRecommendedNextWorkResponse {
pub profile_id: String,
pub level_name: String,
pub author_display_name: String,
pub theme_tags: Vec<String>,
#[serde(default)]
pub cover_image_src: Option<String>,
pub similarity_score: f32,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PuzzleRunSnapshotResponse {
@@ -118,6 +160,14 @@ pub struct PuzzleRunSnapshotResponse {
#[serde(default)]
pub recommended_next_profile_id: Option<String>,
#[serde(default)]
pub next_level_mode: String,
#[serde(default)]
pub next_level_profile_id: Option<String>,
#[serde(default)]
pub next_level_id: Option<String>,
#[serde(default)]
pub recommended_next_works: Vec<PuzzleRecommendedNextWorkResponse>,
#[serde(default)]
pub leaderboard_entries: Vec<PuzzleLeaderboardEntryResponse>,
}

View File

@@ -1,10 +1,12 @@
use serde::{Deserialize, Serialize};
use crate::puzzle_agent::PuzzleAnchorPackResponse;
use crate::puzzle_agent::{PuzzleAnchorPackResponse, PuzzleDraftLevelResponse};
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PutPuzzleWorkRequest {
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -12,6 +14,8 @@ pub struct PutPuzzleWorkRequest {
pub cover_image_src: Option<String>,
#[serde(default)]
pub cover_asset_id: Option<String>,
#[serde(default)]
pub levels: Vec<PuzzleDraftLevelResponse>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -23,6 +27,8 @@ pub struct PuzzleWorkSummaryResponse {
#[serde(default)]
pub source_session_id: Option<String>,
pub author_display_name: String,
pub work_title: String,
pub work_description: String,
pub level_name: String,
pub summary: String,
pub theme_tags: Vec<String>,
@@ -35,7 +41,65 @@ pub struct PuzzleWorkSummaryResponse {
#[serde(default)]
pub published_at: Option<String>,
pub play_count: u32,
#[serde(default)]
pub remix_count: u32,
#[serde(default)]
pub like_count: u32,
#[serde(default)]
pub recent_play_count_7d: u32,
#[serde(default)]
pub point_incentive_total_half_points: u64,
#[serde(default)]
pub point_incentive_claimed_points: u64,
#[serde(default)]
pub point_incentive_total_points: f64,
#[serde(default)]
pub point_incentive_claimable_points: u64,
pub publish_ready: bool,
#[serde(default)]
pub levels: Vec<PuzzleDraftLevelResponse>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn puzzle_work_summary_response_uses_point_incentive_fields() {
let payload = serde_json::to_value(PuzzleWorkSummaryResponse {
work_id: "work-1".to_string(),
profile_id: "profile-1".to_string(),
owner_user_id: "user-1".to_string(),
source_session_id: None,
author_display_name: "作者".to_string(),
work_title: "作品".to_string(),
work_description: "描述".to_string(),
level_name: "第一关".to_string(),
summary: "画面".to_string(),
theme_tags: vec!["拼图".to_string(), "夜色".to_string(), "灯光".to_string()],
cover_image_src: None,
cover_asset_id: None,
publication_status: "published".to_string(),
updated_at: "2026-05-01T00:00:00Z".to_string(),
published_at: Some("2026-05-01T00:00:00Z".to_string()),
play_count: 1,
remix_count: 0,
like_count: 0,
recent_play_count_7d: 1,
point_incentive_total_half_points: 3,
point_incentive_claimed_points: 1,
point_incentive_total_points: 1.5,
point_incentive_claimable_points: 0,
publish_ready: true,
levels: Vec::new(),
})
.expect("payload should serialize");
assert_eq!(payload["pointIncentiveTotalHalfPoints"], 3);
assert_eq!(payload["pointIncentiveClaimedPoints"], 1);
assert_eq!(payload["pointIncentiveTotalPoints"], 1.5);
assert_eq!(payload["pointIncentiveClaimablePoints"], 0);
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]

View File

@@ -4,6 +4,8 @@ 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_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_INVITE_INVITER_REWARD: &str = "invite_inviter_reward";
pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD: &str = "invite_invitee_reward";
@@ -11,6 +13,8 @@ pub const PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME: &str =
"asset_operation_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 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";
@@ -235,6 +239,15 @@ pub struct CreateProfileRechargeOrderResponse {
pub center: ProfileRechargeCenterResponse,
}
#[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 {
@@ -245,6 +258,7 @@ pub struct ProfileReferralInviteCenterResponse {
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>,
@@ -296,6 +310,14 @@ pub struct AdminUpsertProfileRedeemCodeRequest {
pub allowed_public_user_codes: Vec<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>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdminDisableProfileRedeemCodeRequest {
@@ -317,6 +339,16 @@ pub struct ProfileRedeemCodeAdminResponse {
pub updated_at: String,
}
#[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 created_at: String,
pub updated_at: String,
}
fn default_true() -> bool {
true
}
@@ -437,6 +469,14 @@ pub struct CustomWorldLibraryEntryResponse {
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)]
@@ -457,6 +497,14 @@ pub struct CustomWorldGalleryCardResponse {
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)]
@@ -850,13 +898,21 @@ mod tests {
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(),
},
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(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-2".to_string(),
id: "ledger-3".to_string(),
amount_delta: 30,
balance_after: 110,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD
@@ -864,7 +920,7 @@ mod tests {
created_at: "2026-04-22T10:01:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-3".to_string(),
id: "ledger-4".to_string(),
amount_delta: 30,
balance_after: 140,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD
@@ -872,14 +928,14 @@ mod tests {
created_at: "2026-04-22T10:02:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-4".to_string(),
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(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-5".to_string(),
id: "ledger-6".to_string(),
amount_delta: -1,
balance_after: 199,
source_type: PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME
@@ -887,46 +943,62 @@ mod tests {
created_at: "2026-04-22T10:04:00Z".to_string(),
},
ProfileWalletLedgerEntryResponse {
id: "ledger-6".to_string(),
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(),
},
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(),
},
],
})
.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]["amountDelta"], json!(10));
assert_eq!(payload["entries"][0]["balanceAfter"], json!(10));
assert_eq!(
payload["entries"][0]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC)
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_NEW_USER_REGISTRATION_REWARD)
);
assert_eq!(
payload["entries"][1]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD)
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_SNAPSHOT_SYNC)
);
assert_eq!(
payload["entries"][2]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD)
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITER_REWARD)
);
assert_eq!(
payload["entries"][3]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_POINTS_RECHARGE)
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_INVITE_INVITEE_REWARD)
);
assert_eq!(
payload["entries"][4]["sourceType"],
json!(PROFILE_WALLET_LEDGER_SOURCE_TYPE_ASSET_OPERATION_CONSUME)
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"][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"][0]["createdAt"],
json!("2026-04-22T10:00:00Z")
json!("2026-04-22T09:59:00Z")
);
}
@@ -943,14 +1015,14 @@ mod tests {
},
point_products: vec![ProfileRechargeProductResponse {
product_id: "points_60".to_string(),
title: "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(),
description: "首充送60光点".to_string(),
tier: "normal".to_string(),
}],
membership_products: vec![],
@@ -966,11 +1038,11 @@ mod tests {
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]["title"], json!("60光点"));
assert_eq!(payload["pointProducts"][0]["priceCents"], json!(600));
assert_eq!(
payload["pointProducts"][0]["description"],
json!("首充送60叙世币")
json!("首充送60光点")
);
assert_eq!(payload["hasPointsRecharged"], json!(false));
}