diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 858606b5..ec77c557 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -416,11 +416,13 @@ npm run check:server-rs-ddd - Rust 结构体:`JumpHopRuntimeRunRow` - 源码:`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` +- 说明:运行记录持久化 `runtime_mode`,取值为 `draft` / `published`;草稿试玩只允许作品所有者启动,不累计公开游玩次数,也不写入公开排行榜。 ### `jump_hop_work_profile` - Rust 结构体:`JumpHopWorkProfileRow` - 源码:`server-rs/crates/spacetime-module/src/jump_hop/tables.rs` +- 说明:作品投影持久化独立 `theme_text`,用于生成主题和公开卡片主题展示;历史行为空时按 `work_title` 兜底。 ### SpacetimeDB view:`jump_hop_gallery_card_view` diff --git a/server-rs/crates/spacetime-client/src/jump_hop.rs b/server-rs/crates/spacetime-client/src/jump_hop.rs index d4b84451..4f3ed703 100644 --- a/server-rs/crates/spacetime-client/src/jump_hop.rs +++ b/server-rs/crates/spacetime-client/src/jump_hop.rs @@ -238,11 +238,16 @@ impl SpacetimeClient { payload: JumpHopStartRunRequest, owner_user_id: String, ) -> Result { - let profile_id = payload.profile_id; - let work = self - .get_jump_hop_work_profile(profile_id.clone(), String::new()) - .await?; let runtime_mode = normalize_jump_hop_runtime_mode(payload.runtime_mode.as_deref()); + let profile_id = payload.profile_id; + let work_owner_user_id = if runtime_mode == "draft" { + owner_user_id.clone() + } else { + String::new() + }; + let work = self + .get_jump_hop_work_profile(profile_id.clone(), work_owner_user_id) + .await?; validate_jump_hop_runtime_ready(&work, runtime_mode)?; let run_id = build_prefixed_uuid_id("jump-hop-run-"); let procedure_input = JumpHopRunStartInput { diff --git a/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs b/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs index e59a722d..6836d6e2 100644 --- a/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs +++ b/server-rs/crates/spacetime-client/src/mapper/jump_hop.rs @@ -84,13 +84,18 @@ pub(crate) fn map_jump_hop_leaderboard_procedure_result( pub(crate) fn map_jump_hop_gallery_card_view_row( row: JumpHopGalleryCardViewRow, ) -> JumpHopGalleryCardResponse { + let theme_text = if row.theme_text.trim().is_empty() { + row.work_title.clone() + } else { + row.theme_text.clone() + }; JumpHopGalleryCardResponse { public_work_code: row.public_work_code, work_id: row.work_id, profile_id: row.profile_id, owner_user_id: row.owner_user_id, author_display_name: row.author_display_name, - theme_text: row.work_title.clone(), + theme_text, work_title: row.work_title, work_description: row.work_description, cover_image_src: empty_string_to_none(row.cover_image_src), @@ -125,11 +130,16 @@ fn map_jump_hop_session_snapshot( fn map_jump_hop_work_snapshot( snapshot: JumpHopWorkSnapshot, ) -> Result { + let theme_text = if snapshot.theme_text.trim().is_empty() { + snapshot.work_title.clone() + } else { + snapshot.theme_text.clone() + }; let draft = JumpHopDraftResponse { template_id: "jump-hop".to_string(), template_name: "跳一跳".to_string(), profile_id: Some(snapshot.profile_id.clone()), - theme_text: snapshot.work_title.clone(), + theme_text: theme_text.clone(), work_title: snapshot.work_title.clone(), work_description: snapshot.work_description.clone(), theme_tags: snapshot.theme_tags.clone(), @@ -166,7 +176,7 @@ fn map_jump_hop_work_snapshot( profile_id: snapshot.profile_id, owner_user_id: snapshot.owner_user_id, source_session_id: empty_string_to_none(snapshot.source_session_id), - theme_text: snapshot.work_title.clone(), + theme_text, work_title: snapshot.work_title, work_description: snapshot.work_description, theme_tags: snapshot.theme_tags, @@ -195,7 +205,11 @@ fn map_jump_hop_work_snapshot( } fn map_jump_hop_draft_snapshot(snapshot: JumpHopDraftSnapshot) -> JumpHopDraftResponse { - let theme_text = snapshot.work_title.clone(); + let theme_text = if snapshot.theme_text.trim().is_empty() { + snapshot.work_title.clone() + } else { + snapshot.theme_text.clone() + }; JumpHopDraftResponse { template_id: snapshot.template_id, template_name: snapshot.template_name, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_snapshot_type.rs index 09e12197..cc2f6d8d 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_draft_snapshot_type.rs @@ -14,6 +14,7 @@ pub struct JumpHopDraftSnapshot { pub template_id: String, pub template_name: String, pub profile_id: Option, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_row_type.rs index 25622a80..011228e1 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_row_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_card_view_row_type.rs @@ -12,6 +12,7 @@ pub struct JumpHopGalleryCardViewRow { pub profile_id: String, pub owner_user_id: String, pub author_display_name: String, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, @@ -38,6 +39,7 @@ pub struct JumpHopGalleryCardViewRowCols { pub profile_id: __sdk::__query_builder::Col, pub owner_user_id: __sdk::__query_builder::Col, pub author_display_name: __sdk::__query_builder::Col, + pub theme_text: __sdk::__query_builder::Col, pub work_title: __sdk::__query_builder::Col, pub work_description: __sdk::__query_builder::Col, pub theme_tags: __sdk::__query_builder::Col>, @@ -63,6 +65,7 @@ impl __sdk::__query_builder::HasCols for JumpHopGalleryCardViewRow { table_name, "author_display_name", ), + theme_text: __sdk::__query_builder::Col::new(table_name, "theme_text"), work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_row_type.rs index cdf7e954..cadb7726 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_row_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_gallery_view_row_type.rs @@ -16,6 +16,7 @@ pub struct JumpHopGalleryViewRow { pub owner_user_id: String, pub source_session_id: String, pub author_display_name: String, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, @@ -51,6 +52,7 @@ pub struct JumpHopGalleryViewRowCols { pub owner_user_id: __sdk::__query_builder::Col, pub source_session_id: __sdk::__query_builder::Col, pub author_display_name: __sdk::__query_builder::Col, + pub theme_text: __sdk::__query_builder::Col, pub work_title: __sdk::__query_builder::Col, pub work_description: __sdk::__query_builder::Col, pub theme_tags: __sdk::__query_builder::Col>, @@ -88,6 +90,7 @@ impl __sdk::__query_builder::HasCols for JumpHopGalleryViewRow { table_name, "author_display_name", ), + theme_text: __sdk::__query_builder::Col::new(table_name, "theme_text"), work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_row_type.rs index 64c5205f..1a78cae3 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_row_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_runtime_run_row_type.rs @@ -19,6 +19,7 @@ pub struct JumpHopRuntimeRunRow { pub snapshot_json: String, pub created_at: __sdk::Timestamp, pub updated_at: __sdk::Timestamp, + pub runtime_mode: Option, } impl __sdk::InModule for JumpHopRuntimeRunRow { @@ -41,6 +42,7 @@ pub struct JumpHopRuntimeRunRowCols { pub snapshot_json: __sdk::__query_builder::Col, pub created_at: __sdk::__query_builder::Col, pub updated_at: __sdk::__query_builder::Col, + pub runtime_mode: __sdk::__query_builder::Col>, } impl __sdk::__query_builder::HasCols for JumpHopRuntimeRunRow { @@ -62,6 +64,7 @@ impl __sdk::__query_builder::HasCols for JumpHopRuntimeRunRow { snapshot_json: __sdk::__query_builder::Col::new(table_name, "snapshot_json"), created_at: __sdk::__query_builder::Col::new(table_name, "created_at"), updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), + runtime_mode: __sdk::__query_builder::Col::new(table_name, "runtime_mode"), } } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_row_type.rs index 660ea530..c95dc8c9 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_row_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_profile_row_type.rs @@ -32,6 +32,7 @@ pub struct JumpHopWorkProfileRow { pub updated_at: __sdk::Timestamp, pub published_at: Option<__sdk::Timestamp>, pub visible: bool, + pub theme_text: Option, } impl __sdk::InModule for JumpHopWorkProfileRow { @@ -67,6 +68,7 @@ pub struct JumpHopWorkProfileRowCols { pub updated_at: __sdk::__query_builder::Col, pub published_at: __sdk::__query_builder::Col>, pub visible: __sdk::__query_builder::Col, + pub theme_text: __sdk::__query_builder::Col>, } impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow { @@ -107,6 +109,7 @@ impl __sdk::__query_builder::HasCols for JumpHopWorkProfileRow { updated_at: __sdk::__query_builder::Col::new(table_name, "updated_at"), published_at: __sdk::__query_builder::Col::new(table_name, "published_at"), visible: __sdk::__query_builder::Col::new(table_name, "visible"), + theme_text: __sdk::__query_builder::Col::new(table_name, "theme_text"), } } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_snapshot_type.rs index bda718d0..d72083f8 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_snapshot_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/jump_hop_work_snapshot_type.rs @@ -16,6 +16,7 @@ pub struct JumpHopWorkSnapshot { pub owner_user_id: String, pub source_session_id: String, pub author_display_name: String, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, diff --git a/server-rs/crates/spacetime-module/src/jump_hop.rs b/server-rs/crates/spacetime-module/src/jump_hop.rs index 4f53d9df..7eb34301 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop.rs @@ -52,6 +52,7 @@ pub fn jump_hop_gallery_card_view(ctx: &AnonymousViewContext) -> Vec, @@ -103,6 +105,7 @@ pub struct JumpHopGalleryCardViewRow { pub profile_id: String, pub owner_user_id: String, pub author_display_name: String, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, @@ -295,6 +298,7 @@ fn create_jump_hop_agent_session_tx( template_id: JUMP_HOP_TEMPLATE_ID.to_string(), template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), profile_id: None, + theme_text: config.theme_text.clone(), work_title: input.work_title.clone(), work_description: input.work_description.clone(), theme_tags: parse_tags(input.theme_tags_json.as_deref().unwrap_or("[]"))?, @@ -360,6 +364,7 @@ fn compile_jump_hop_draft_tx( template_id: JUMP_HOP_TEMPLATE_ID.to_string(), template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), profile_id: Some(input.profile_id.clone()), + theme_text: clean_string(&config.theme_text, &input.work_title), work_title: clean_string(&input.work_title, "跳一跳作品"), work_description: input.work_description.trim().to_string(), theme_tags: tags.clone(), @@ -426,6 +431,7 @@ fn compile_jump_hop_draft_tx( updated_at: compiled_at, published_at: None, visible: true, + theme_text: Some(draft.theme_text.clone()), }; upsert_work(ctx, row); replace_session( @@ -567,6 +573,9 @@ fn start_jump_hop_run_tx( require_non_empty(&input.run_id, "jump_hop run_id")?; let work = find_work(ctx, &input.profile_id)?; let runtime_mode = normalize_runtime_mode(&input.runtime_mode); + if runtime_mode == JUMP_HOP_RUNTIME_MODE_DRAFT && work.owner_user_id != input.owner_user_id { + return Err("jump_hop draft runtime 只能由作品所有者启动".to_string()); + } if runtime_mode == JUMP_HOP_RUNTIME_MODE_PUBLISHED && work.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED { @@ -582,7 +591,7 @@ fn start_jump_hop_run_tx( ) .map_err(|error| error.to_string())?; let snapshot = domain_run; - upsert_run(ctx, &snapshot, input.started_at_ms); + upsert_run(ctx, &snapshot, input.started_at_ms, runtime_mode); if runtime_mode == JUMP_HOP_RUNTIME_MODE_PUBLISHED { increment_work_play_count(ctx, &work, input.started_at_ms); } @@ -623,7 +632,10 @@ fn jump_hop_jump_tx( .map_err(|error| error.to_string())?; let next = domain_next; replace_run(ctx, &row, &next, input.jumped_at_ms); - if next.status == module_jump_hop::JumpHopRunStatus::Failed { + if next.status == module_jump_hop::JumpHopRunStatus::Failed + && normalize_runtime_mode(row.runtime_mode.as_deref().unwrap_or_default()) + == JUMP_HOP_RUNTIME_MODE_PUBLISHED + { upsert_jump_hop_leaderboard_entry(ctx, &next, input.jumped_at_ms); } insert_event( @@ -654,7 +666,10 @@ fn get_jump_hop_leaderboard_tx( String, > { require_non_empty(&input.profile_id, "jump_hop profile_id")?; - let _ = find_work(ctx, &input.profile_id)?; + let work = find_work(ctx, &input.profile_id)?; + if work.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED { + return Err("jump_hop leaderboard 只开放已发布作品".to_string()); + } let limit = input.limit.clamp(1, 50) as usize; let mut rows = ctx .db @@ -696,7 +711,8 @@ fn restart_jump_hop_run_tx( ) .map_err(|error| error.to_string())?; let next = domain_next; - upsert_run(ctx, &next, input.restarted_at_ms); + let runtime_mode = normalize_runtime_mode(source.runtime_mode.as_deref().unwrap_or_default()); + upsert_run(ctx, &next, input.restarted_at_ms, runtime_mode); insert_event( ctx, input.client_action_id, @@ -718,6 +734,7 @@ fn build_gallery_view_row(row: &JumpHopWorkProfileRow) -> Result Result { let path = parse_json(&row.path_json)?; + let theme_text = row + .theme_text + .as_deref() + .and_then(clean_optional) + .unwrap_or_else(|| row.work_title.trim().to_string()); Ok(JumpHopWorkSnapshot { work_id: row.work_id.clone(), profile_id: row.profile_id.clone(), owner_user_id: row.owner_user_id.clone(), source_session_id: row.source_session_id.clone(), author_display_name: row.author_display_name.clone(), + theme_text, work_title: row.work_title.clone(), work_description: row.work_description.clone(), theme_tags: parse_tags(&row.theme_tags_json)?, @@ -833,7 +856,11 @@ fn sync_session_from_work_update( }; let mut config = parse_config(&session.config_json)?; - config.theme_text = work.work_title.clone(); + config.theme_text = work + .theme_text + .as_deref() + .and_then(clean_optional) + .unwrap_or_else(|| work.work_title.trim().to_string()); config.difficulty = work.difficulty.clone(); config.style_preset = work.style_preset.clone(); config.character_prompt = work.character_prompt.clone(); @@ -844,6 +871,7 @@ fn sync_session_from_work_update( template_id: JUMP_HOP_TEMPLATE_ID.to_string(), template_name: JUMP_HOP_TEMPLATE_NAME.to_string(), profile_id: Some(work.profile_id.clone()), + theme_text: config.theme_text.clone(), work_title: work.work_title.clone(), work_description: work.work_description.clone(), theme_tags: parse_tags(&work.theme_tags_json)?, @@ -957,7 +985,12 @@ fn replace_session( ctx.db.jump_hop_agent_session().insert(next); } -fn upsert_run(ctx: &ReducerContext, snapshot: &JumpHopRunSnapshot, updated_at_ms: i64) { +fn upsert_run( + ctx: &ReducerContext, + snapshot: &JumpHopRunSnapshot, + updated_at_ms: i64, + runtime_mode: &str, +) { if let Some(old) = ctx .db .jump_hop_runtime_run() @@ -967,9 +1000,12 @@ fn upsert_run(ctx: &ReducerContext, snapshot: &JumpHopRunSnapshot, updated_at_ms ctx.db.jump_hop_runtime_run().delete(old); } let created_at = Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)); - ctx.db - .jump_hop_runtime_run() - .insert(run_row_from_snapshot(snapshot, created_at, created_at)); + ctx.db.jump_hop_runtime_run().insert(run_row_from_snapshot( + snapshot, + created_at, + created_at, + runtime_mode, + )); } fn replace_run( @@ -983,6 +1019,7 @@ fn replace_run( snapshot, old.created_at, Timestamp::from_micros_since_unix_epoch(updated_at_ms.saturating_mul(1000)), + normalize_runtime_mode(old.runtime_mode.as_deref().unwrap_or_default()), )); } @@ -990,6 +1027,7 @@ fn run_row_from_snapshot( snapshot: &JumpHopRunSnapshot, created_at: Timestamp, updated_at: Timestamp, + runtime_mode: &str, ) -> JumpHopRuntimeRunRow { JumpHopRuntimeRunRow { run_id: snapshot.run_id.clone(), @@ -1007,6 +1045,7 @@ fn run_row_from_snapshot( snapshot_json: to_json_string(snapshot), created_at, updated_at, + runtime_mode: Some(normalize_runtime_mode(runtime_mode).to_string()), } } @@ -1359,6 +1398,7 @@ fn clone_work(row: &JumpHopWorkProfileRow) -> JumpHopWorkProfileRow { updated_at: row.updated_at, published_at: row.published_at, visible: row.visible, + theme_text: row.theme_text.clone(), } } @@ -1376,6 +1416,7 @@ fn clone_run(row: &JumpHopRuntimeRunRow) -> JumpHopRuntimeRunRow { snapshot_json: row.snapshot_json.clone(), created_at: row.created_at, updated_at: row.updated_at, + runtime_mode: row.runtime_mode.clone(), } } diff --git a/server-rs/crates/spacetime-module/src/jump_hop/tables.rs b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs index 1524f75c..4806fbac 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop/tables.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs @@ -56,6 +56,9 @@ pub struct JumpHopWorkProfileRow { // 后台可见性开关;默认显示,隐藏后不进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, + // 跳一跳生成主题独立于作品标题;旧行按 work_title 兜底。 + #[default(None::)] + pub(crate) theme_text: Option, } #[spacetimedb::table( @@ -77,6 +80,9 @@ pub struct JumpHopRuntimeRunRow { pub(crate) snapshot_json: String, pub(crate) created_at: Timestamp, pub(crate) updated_at: Timestamp, + // draft / published,用于隔离试玩统计和公开排行榜;旧行按 published 兜底。 + #[default(None::)] + pub(crate) runtime_mode: Option, } #[spacetimedb::table( diff --git a/server-rs/crates/spacetime-module/src/jump_hop/types.rs b/server-rs/crates/spacetime-module/src/jump_hop/types.rs index 05f6092f..6edb7312 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop/types.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop/types.rs @@ -233,6 +233,8 @@ pub struct JumpHopDraftSnapshot { pub template_id: String, pub template_name: String, pub profile_id: Option, + #[serde(default)] + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, @@ -274,6 +276,7 @@ pub struct JumpHopWorkSnapshot { pub owner_user_id: String, pub source_session_id: String, pub author_display_name: String, + pub theme_text: String, pub work_title: String, pub work_description: String, pub theme_tags: Vec, diff --git a/server-rs/crates/spacetime-module/src/public_work.rs b/server-rs/crates/spacetime-module/src/public_work.rs index 98aaa6ce..e4a2edb5 100644 --- a/server-rs/crates/spacetime-module/src/public_work.rs +++ b/server-rs/crates/spacetime-module/src/public_work.rs @@ -338,6 +338,7 @@ fn map_custom_world_detail_entry(row: CustomWorldProfileSnapshot) -> PublicWorkD fn map_jump_hop_gallery_entry(row: JumpHopGalleryCardViewRow) -> PublicWorkGalleryEntry { let subtitle = jump_hop_difficulty_label(&row.difficulty).to_string(); let sort_time_micros = row.published_at_micros.unwrap_or(row.updated_at_micros); + let theme_text = row.theme_text.clone(); PublicWorkGalleryEntry { source_type: "jump-hop".to_string(), @@ -352,7 +353,7 @@ fn map_jump_hop_gallery_entry(row: JumpHopGalleryCardViewRow) -> PublicWorkGalle summary_text: row.work_description, cover_image_src: empty_string_to_option(row.cover_image_src), cover_asset_id: None, - theme_tags: fallback_tags(row.theme_tags, &["跳一跳"]), + theme_tags: fallback_tags(row.theme_tags, &[theme_text.as_str(), "跳一跳"]), play_count: row.play_count, remix_count: 0, like_count: 0, @@ -363,6 +364,7 @@ fn map_jump_hop_gallery_entry(row: JumpHopGalleryCardViewRow) -> PublicWorkGalle } fn map_jump_hop_detail_entry(row: JumpHopGalleryViewRow) -> PublicWorkDetailEntry { + let theme_text = row.theme_text.clone(); let entry = PublicWorkGalleryEntry { source_type: "jump-hop".to_string(), work_id: row.work_id, @@ -376,7 +378,7 @@ fn map_jump_hop_detail_entry(row: JumpHopGalleryViewRow) -> PublicWorkDetailEntr summary_text: row.work_description, cover_image_src: empty_string_to_option(row.cover_image_src), cover_asset_id: None, - theme_tags: fallback_tags(row.theme_tags, &["跳一跳"]), + theme_tags: fallback_tags(row.theme_tags, &[theme_text.as_str(), "跳一跳"]), play_count: row.play_count, remix_count: 0, like_count: 0, @@ -388,6 +390,7 @@ fn map_jump_hop_detail_entry(row: JumpHopGalleryViewRow) -> PublicWorkDetailEntr "sourceType": "jump-hop", "difficulty": row.difficulty, "stylePreset": row.style_preset, + "themeText": theme_text, "tileAssetCount": row.tile_assets.len(), "platformCount": row.path.platforms.len(), "generationStatus": row.generation_status, diff --git a/src/components/jump-hop-result/JumpHopResultView.test.tsx b/src/components/jump-hop-result/JumpHopResultView.test.tsx index 3faeb959..07287372 100644 --- a/src/components/jump-hop-result/JumpHopResultView.test.tsx +++ b/src/components/jump-hop-result/JumpHopResultView.test.tsx @@ -1,7 +1,7 @@ /* @vitest-environment jsdom */ import { render, screen } from '@testing-library/react'; -import { expect, test, vi } from 'vitest'; +import { beforeEach, expect, test, vi } from 'vitest'; import type { JumpHopWorkProfileResponse } from '../../../packages/shared/src/contracts/jumpHop'; import { useJumpHopLeaderboard } from '../../services/jump-hop/useJumpHopLeaderboard'; @@ -11,6 +11,16 @@ vi.mock('../../services/jump-hop/useJumpHopLeaderboard', () => ({ useJumpHopLeaderboard: vi.fn(), })); +beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(useJumpHopLeaderboard).mockReturnValue({ + leaderboard: null, + isLoading: false, + error: null, + refresh: vi.fn(), + }); +}); + test('跳一跳结果页展示排行榜列表', () => { vi.mocked(useJumpHopLeaderboard).mockReturnValue({ leaderboard: { @@ -40,7 +50,7 @@ test('跳一跳结果页展示排行榜列表', () => { render( {}} onEdit={() => {}} onStartTestRun={() => {}} @@ -57,13 +67,6 @@ test('跳一跳结果页展示排行榜列表', () => { }); test('跳一跳结果页默认角色预览使用陶泥儿透明 logo', () => { - vi.mocked(useJumpHopLeaderboard).mockReturnValue({ - leaderboard: null, - isLoading: false, - error: null, - refresh: vi.fn(), - }); - render( { ); }); -function buildProfile(): JumpHopWorkProfileResponse { +test('跳一跳草稿结果页不请求公开排行榜', () => { + render( + {}} + onEdit={() => {}} + onStartTestRun={() => {}} + onPublish={() => {}} + onRegenerateTiles={() => {}} + />, + ); + + expect(useJumpHopLeaderboard).not.toHaveBeenCalled(); + expect(screen.queryByText('排行榜')).toBeNull(); +}); + +function buildProfile( + options: { + publicationStatus?: JumpHopWorkProfileResponse['summary']['publicationStatus']; + } = {}, +): JumpHopWorkProfileResponse { return { summary: { runtimeKind: 'jump-hop', @@ -95,7 +118,7 @@ function buildProfile(): JumpHopWorkProfileResponse { difficulty: 'standard', stylePreset: 'minimal-blocks', coverImageSrc: null, - publicationStatus: 'draft', + publicationStatus: options.publicationStatus ?? 'draft', playCount: 0, updatedAt: '2026-05-27T00:00:00Z', publishedAt: null, diff --git a/src/components/jump-hop-result/JumpHopResultView.tsx b/src/components/jump-hop-result/JumpHopResultView.tsx index cbfaaaed..bc084de5 100644 --- a/src/components/jump-hop-result/JumpHopResultView.tsx +++ b/src/components/jump-hop-result/JumpHopResultView.tsx @@ -272,6 +272,8 @@ export function JumpHopResultView({ const profileId = isWorkProfile ? profile.summary.profileId : safeDraft.profileId; + const canShowLeaderboard = + isWorkProfile && profile.summary.publicationStatus === 'published'; const titleSource = isWorkProfile ? profile.summary.workTitle : profile.workTitle; @@ -365,7 +367,9 @@ export function JumpHopResultView({
结果操作
- + {canShowLeaderboard ? ( + + ) : null} {error ? (
{error} diff --git a/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx b/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx index 1a574ed4..e882b62e 100644 --- a/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx +++ b/src/components/jump-hop-runtime/JumpHopRuntimeShell.test.tsx @@ -278,7 +278,7 @@ test('跳一跳运行态失败后在弹窗中展示排行榜', () => { render( { expect(within(leaderboard).getByText('00:08')).toBeTruthy(); }); +test('跳一跳草稿运行失败后不请求公开排行榜', () => { + render( + {}} + />, + ); + + expect(useJumpHopLeaderboard).not.toHaveBeenCalled(); + expect(screen.getByRole('dialog', { name: '失败' })).toBeTruthy(); + expect(screen.queryByTestId('jump-hop-runtime-leaderboard')).toBeNull(); +}); + test('跳一跳角色层永远压在地块层之上', () => { render(