Merge remote-tracking branch 'origin/master' into feat/recommend-runtime-guest

# Conflicts:
#	docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
This commit is contained in:
kdletters
2026-05-25 14:12:39 +08:00
470 changed files with 8570 additions and 3058 deletions

View File

@@ -1159,6 +1159,43 @@ where
fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde_json::Value {
let mut next_value = value.clone();
if table_name == "creation_entry_config" {
if let Some(object) = next_value.as_object_mut() {
// 中文注释:入口活动横幅字段晚于创作入口配置表加入,旧迁移包按运行态默认横幅兼容。
object
.entry("event_title".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("event_description".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("event_cover_image_src".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("event_prize_pool_mud_points".to_string())
.or_insert_with(|| serde_json::Value::from(58_000));
object
.entry("event_starts_at_text".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("event_ends_at_text".to_string())
.or_insert(serde_json::Value::Null);
}
}
if table_name == "creation_entry_type_config" {
if let Some(object) = next_value.as_object_mut() {
// 中文注释:入口分类字段晚于入口类型配置表加入,旧迁移包按未分类兼容。
object
.entry("category_id".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("category_label".to_string())
.or_insert(serde_json::Value::Null);
object
.entry("category_sort_order".to_string())
.or_insert_with(|| serde_json::Value::from(0));
}
}
if table_name == "user_account" {
if let Some(object) = next_value.as_object_mut() {
// 中文注释:头像字段晚于认证拆表加入,旧迁移包按未设置头像兼容。
@@ -1271,6 +1308,10 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
object
.entry("background_asset_json".to_string())
.or_insert(serde_json::Value::Null);
// 中文注释:敲木鱼返回按钮图晚于首版作品表加入,旧迁移包按未生成返回按钮兼容。
object
.entry("back_button_asset_json".to_string())
.or_insert(serde_json::Value::Null);
}
}
next_value

View File

@@ -11,6 +11,18 @@ pub struct CreationEntryConfig {
pub(crate) modal_title: String,
pub(crate) modal_description: String,
pub(crate) updated_at: Timestamp,
#[default(None::<String>)]
pub(crate) event_title: Option<String>,
#[default(None::<String>)]
pub(crate) event_description: Option<String>,
#[default(None::<String>)]
pub(crate) event_cover_image_src: Option<String>,
#[default(DEFAULT_CREATION_ENTRY_EVENT_PRIZE_POOL_MUD_POINTS)]
pub(crate) event_prize_pool_mud_points: u64,
#[default(None::<String>)]
pub(crate) event_starts_at_text: Option<String>,
#[default(None::<String>)]
pub(crate) event_ends_at_text: Option<String>,
}
#[spacetimedb::table(
@@ -28,6 +40,12 @@ pub struct CreationEntryTypeConfig {
pub(crate) open: bool,
pub(crate) sort_order: i32,
pub(crate) updated_at: Timestamp,
#[default(None::<String>)]
pub(crate) category_id: Option<String>,
#[default(None::<String>)]
pub(crate) category_label: Option<String>,
#[default(0)]
pub(crate) category_sort_order: i32,
}
#[spacetimedb::procedure]
@@ -88,6 +106,9 @@ fn upsert_creation_entry_type_config_in_tx(
open: input.open,
sort_order: input.sort_order,
updated_at: now,
category_id: Some(normalize_category_id(&input.category_id)),
category_label: Some(normalize_category_label(&input.category_label)),
category_sort_order: input.category_sort_order,
};
if ctx.db.creation_entry_type_config().id().find(&id).is_some() {
ctx.db.creation_entry_type_config().id().update(row);
@@ -120,6 +141,9 @@ fn get_or_seed_creation_entry_config_snapshot(
visible: row.visible,
open: row.open,
sort_order: row.sort_order,
category_id: normalize_optional_category_id(row.category_id.as_deref()),
category_label: normalize_optional_category_label(row.category_label.as_deref()),
category_sort_order: row.category_sort_order,
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
})
.collect::<Vec<_>>();
@@ -141,6 +165,29 @@ fn get_or_seed_creation_entry_config_snapshot(
title: header.modal_title,
description: header.modal_description,
},
event_banner: CreationEntryEventBannerSnapshot {
title: normalize_optional_text(
header.event_title.as_deref(),
DEFAULT_CREATION_ENTRY_EVENT_TITLE,
),
description: normalize_optional_text(
header.event_description.as_deref(),
DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION,
),
cover_image_src: normalize_optional_text(
header.event_cover_image_src.as_deref(),
DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC,
),
prize_pool_mud_points: header.event_prize_pool_mud_points,
starts_at_text: normalize_optional_text(
header.event_starts_at_text.as_deref(),
DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT,
),
ends_at_text: normalize_optional_text(
header.event_ends_at_text.as_deref(),
DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT,
),
},
creation_types,
updated_at_micros: header.updated_at.to_micros_since_unix_epoch(),
})
@@ -164,6 +211,12 @@ fn seed_creation_entry_config_if_missing(ctx: &ReducerContext) {
modal_title: DEFAULT_CREATION_ENTRY_MODAL_TITLE.to_string(),
modal_description: DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION.to_string(),
updated_at: now,
event_title: Some(DEFAULT_CREATION_ENTRY_EVENT_TITLE.to_string()),
event_description: Some(DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION.to_string()),
event_cover_image_src: Some(DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC.to_string()),
event_prize_pool_mud_points: DEFAULT_CREATION_ENTRY_EVENT_PRIZE_POOL_MUD_POINTS,
event_starts_at_text: Some(DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT.to_string()),
event_ends_at_text: Some(DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT.to_string()),
});
}
@@ -348,6 +401,43 @@ fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeC
open: snapshot.open,
sort_order: snapshot.sort_order,
updated_at: now,
category_id: Some(snapshot.category_id),
category_label: Some(snapshot.category_label),
category_sort_order: snapshot.category_sort_order,
})
.collect()
}
fn normalize_category_id(value: &str) -> String {
let normalized = value.trim();
if normalized.is_empty() {
DEFAULT_CREATION_ENTRY_CATEGORY_ID.to_string()
} else {
normalized.to_string()
}
}
fn normalize_category_label(value: &str) -> String {
let normalized = value.trim();
if normalized.is_empty() {
DEFAULT_CREATION_ENTRY_CATEGORY_LABEL.to_string()
} else {
normalized.to_string()
}
}
fn normalize_optional_category_id(value: Option<&str>) -> String {
normalize_optional_text(value, DEFAULT_CREATION_ENTRY_CATEGORY_ID)
}
fn normalize_optional_category_label(value: Option<&str>) -> String {
normalize_optional_text(value, DEFAULT_CREATION_ENTRY_CATEGORY_LABEL)
}
fn normalize_optional_text(value: Option<&str>, fallback: &str) -> String {
value
.map(str::trim)
.filter(|normalized| !normalized.is_empty())
.unwrap_or(fallback)
.to_string()
}

View File

@@ -82,6 +82,7 @@ pub struct WoodenFishGalleryViewRow {
pub hit_sound_prompt: Option<String>,
pub hit_object_asset: Option<WoodenFishImageAssetSnapshot>,
pub background_asset: Option<WoodenFishImageAssetSnapshot>,
pub back_button_asset: Option<WoodenFishImageAssetSnapshot>,
pub hit_sound_asset: Option<WoodenFishAudioAssetSnapshot>,
pub floating_words: Vec<String>,
pub cover_image_src: String,
@@ -333,6 +334,11 @@ fn compile_wooden_fish_draft_tx(
.as_deref()
.map(parse_json)
.transpose()?;
let back_button_asset = input
.back_button_asset_json
.as_deref()
.map(parse_json)
.transpose()?;
let cover_image_src = input
.cover_image_src
.as_deref()
@@ -361,6 +367,7 @@ fn compile_wooden_fish_draft_tx(
floating_words: floating_words.clone(),
hit_object_asset: hit_object_asset.clone(),
background_asset: background_asset.clone(),
back_button_asset: back_button_asset.clone(),
hit_sound_asset: hit_sound_asset.clone(),
cover_image_src: cover_image_src.clone(),
generation_status: input
@@ -400,6 +407,7 @@ fn compile_wooden_fish_draft_tx(
updated_at: compiled_at,
published_at: None,
background_asset_json: background_asset.as_ref().map(to_json_string),
back_button_asset_json: back_button_asset.as_ref().map(to_json_string),
};
upsert_work(ctx, row);
let config = config_from_draft(&draft);
@@ -485,6 +493,14 @@ fn update_wooden_fish_work_tx(
let asset = parse_json::<WoodenFishImageAssetSnapshot>(&value)?;
next.background_asset_json = Some(to_json_string(&asset));
}
if let Some(value) = input
.back_button_asset_json
.as_deref()
.and_then(clean_optional)
{
let asset = parse_json::<WoodenFishImageAssetSnapshot>(&value)?;
next.back_button_asset_json = Some(to_json_string(&asset));
}
if let Some(value) = input
.floating_words_json
.as_deref()
@@ -512,7 +528,7 @@ fn publish_wooden_fish_work_tx(
) -> Result<WoodenFishWorkSnapshot, String> {
let row = find_owned_work(ctx, &input.profile_id, &input.owner_user_id)?;
if !is_publish_ready(&row) {
return Err("发布需要完整的敲击物图案、敲击音效和飘字配置".to_string());
return Err("发布需要完整的敲击物图案、背景、返回按钮、敲击音效和飘字配置".to_string());
}
let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros);
replace_work(
@@ -691,6 +707,7 @@ fn build_gallery_view_row(
hit_sound_prompt: work.hit_sound_prompt,
hit_object_asset: work.hit_object_asset,
background_asset: work.background_asset,
back_button_asset: work.back_button_asset,
hit_sound_asset: work.hit_sound_asset,
floating_words: work.floating_words,
cover_image_src: work.cover_image_src,
@@ -744,6 +761,12 @@ fn build_work_snapshot(row: &WoodenFishWorkProfileRow) -> Result<WoodenFishWorkS
.and_then(clean_optional)
.map(|value| parse_json(&value))
.transpose()?,
back_button_asset: row
.back_button_asset_json
.as_deref()
.and_then(clean_optional)
.map(|value| parse_json(&value))
.transpose()?,
hit_sound_asset: clean_optional(&row.hit_sound_asset_json)
.map(|value| parse_json(&value))
.transpose()?,
@@ -993,6 +1016,11 @@ fn is_publish_ready(row: &WoodenFishWorkProfileRow) -> bool {
.as_deref()
.and_then(clean_optional)
.is_some()
&& row
.back_button_asset_json
.as_deref()
.and_then(clean_optional)
.is_some()
&& !row.hit_sound_asset_json.trim().is_empty()
&& !row.floating_words_json.trim().is_empty()
&& row.generation_status == WOODEN_FISH_GENERATION_READY
@@ -1031,6 +1059,7 @@ fn draft_from_config(
floating_words: normalize_floating_words(&config.floating_words),
hit_object_asset: None,
background_asset: None,
back_button_asset: None,
hit_sound_asset: None,
cover_image_src: None,
generation_status: generation_status.to_string(),
@@ -1051,6 +1080,7 @@ fn draft_from_work_snapshot(work: &WoodenFishWorkSnapshot) -> WoodenFishDraftSna
floating_words: work.floating_words.clone(),
hit_object_asset: work.hit_object_asset.clone(),
background_asset: work.background_asset.clone(),
back_button_asset: work.back_button_asset.clone(),
hit_sound_asset: work.hit_sound_asset.clone(),
cover_image_src: clean_optional(&work.cover_image_src),
generation_status: work.generation_status.clone(),
@@ -1231,6 +1261,7 @@ fn clone_work(row: &WoodenFishWorkProfileRow) -> WoodenFishWorkProfileRow {
hit_object_asset_json: row.hit_object_asset_json.clone(),
background_asset_json: row.background_asset_json.clone(),
hit_sound_asset_json: row.hit_sound_asset_json.clone(),
back_button_asset_json: row.back_button_asset_json.clone(),
floating_words_json: row.floating_words_json.clone(),
cover_image_src: row.cover_image_src.clone(),
generation_status: row.generation_status.clone(),

View File

@@ -47,6 +47,8 @@ pub struct WoodenFishWorkProfileRow {
pub(crate) published_at: Option<Timestamp>,
#[default(None::<String>)]
pub(crate) background_asset_json: Option<String>,
#[default(None::<String>)]
pub(crate) back_button_asset_json: Option<String>,
}
#[spacetimedb::table(

View File

@@ -47,6 +47,7 @@ pub struct WoodenFishDraftCompileInput {
pub hit_object_asset_json: Option<String>,
pub background_asset_json: Option<String>,
pub hit_sound_asset_json: Option<String>,
pub back_button_asset_json: Option<String>,
pub floating_words_json: Option<String>,
pub cover_image_src: Option<String>,
pub generation_status: Option<String>,
@@ -66,6 +67,7 @@ pub struct WoodenFishWorkUpdateInput {
pub hit_object_asset_json: Option<String>,
pub background_asset_json: Option<String>,
pub hit_sound_asset_json: Option<String>,
pub back_button_asset_json: Option<String>,
pub floating_words_json: Option<String>,
pub cover_image_src: Option<String>,
pub generation_status: Option<String>,
@@ -210,6 +212,7 @@ pub struct WoodenFishDraftSnapshot {
pub floating_words: Vec<String>,
pub hit_object_asset: Option<WoodenFishImageAssetSnapshot>,
pub background_asset: Option<WoodenFishImageAssetSnapshot>,
pub back_button_asset: Option<WoodenFishImageAssetSnapshot>,
pub hit_sound_asset: Option<WoodenFishAudioAssetSnapshot>,
pub cover_image_src: Option<String>,
pub generation_status: String,
@@ -246,6 +249,7 @@ pub struct WoodenFishWorkSnapshot {
pub hit_sound_prompt: Option<String>,
pub hit_object_asset: Option<WoodenFishImageAssetSnapshot>,
pub background_asset: Option<WoodenFishImageAssetSnapshot>,
pub back_button_asset: Option<WoodenFishImageAssetSnapshot>,
pub hit_sound_asset: Option<WoodenFishAudioAssetSnapshot>,
pub floating_words: Vec<String>,
pub cover_image_src: String,