This commit is contained in:
2026-05-11 16:15:48 +08:00
parent 0c9254502c
commit e30b733b17
87 changed files with 3527 additions and 1261 deletions

View File

@@ -33,6 +33,12 @@ pub struct AuthStoreSnapshotProcedureResult {
pub error_message: Option<String>,
}
fn normalize_user_account_tags(
tags: Option<Vec<String>>,
) -> Result<Vec<String>, module_runtime::RuntimeProfileFieldError> {
module_runtime::normalize_profile_user_tags(tags.unwrap_or_default())
}
#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)]
pub struct AuthStoreSnapshotImportRecord {
pub imported_user_count: u32,
@@ -210,8 +216,10 @@ fn import_auth_store_snapshot_tx(
password_hash: stored_user.password_hash,
password_login_enabled: stored_user.password_login_enabled,
token_version: user.token_version,
user_tags: module_runtime::normalize_profile_user_tags(user.user_tags)
.map_err(|error| error.to_string())?,
user_tags: Some(
module_runtime::normalize_profile_user_tags(user.user_tags)
.map_err(|error| error.to_string())?,
),
});
imported_user_count += 1;
@@ -341,7 +349,8 @@ fn export_auth_store_snapshot_from_tables_tx(
binding_status: user.binding_status,
wechat_bound: user.wechat_bound,
token_version: user.token_version,
user_tags: user.user_tags,
user_tags: normalize_user_account_tags(user.user_tags)
.map_err(|error| error.to_string())?,
};
users_by_username.insert(
user.username,

View File

@@ -28,7 +28,8 @@ pub struct UserAccount {
pub(crate) password_hash: String,
pub(crate) password_login_enabled: bool,
pub(crate) token_version: u64,
pub(crate) user_tags: Vec<String>,
#[default(None::<Vec<String>>)]
pub(crate) user_tags: Option<Vec<String>>,
}
#[spacetimedb::table(

View File

@@ -452,8 +452,12 @@ fn compile_match3d_draft_tx(
.unwrap_or_else(|| default_tags(&config.theme_text));
let game_name =
clean_optional(&input.game_name).unwrap_or_else(|| format!("{}抓大鹅", config.theme_text));
let summary_text = clean_optional(&input.summary_text)
.unwrap_or_else(|| format!("{}主题的经典消除玩法。", config.theme_text));
let summary_text = input
.summary_text
.as_deref()
.map(str::trim)
.unwrap_or_default()
.to_string();
let draft = Match3DDraftSnapshot {
profile_id: input.profile_id.clone(),
game_name: game_name.clone(),

View File

@@ -1140,7 +1140,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
// 中文注释:账号标签字段晚于认证表加入,旧迁移包默认无标签。
object
.entry("user_tags".to_string())
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
.or_insert(serde_json::Value::Null);
}
}
if table_name == "profile_invite_code" {
@@ -1149,10 +1149,6 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
object
.entry("metadata_json".to_string())
.or_insert_with(|| serde_json::Value::String("{}".to_string()));
// 中文注释:邀请码授予标签字段晚于邀请表加入,旧迁移包默认不授予标签。
object
.entry("granted_user_tags".to_string())
.or_insert_with(|| serde_json::Value::Array(Vec::new()));
}
}
if table_name == "big_fish_creation_session" {

View File

@@ -3226,7 +3226,11 @@ fn list_puzzle_leaderboard_entries(
.user_account()
.user_id()
.find(&row.user_id)
.map(|account| visible_runtime_profile_user_tags(&account.user_tags))
.map(|account| {
visible_runtime_profile_user_tags(
account.user_tags.as_deref().unwrap_or_default(),
)
})
.unwrap_or_default(),
rank: index as u32 + 1,
nickname: row.nickname,

View File

@@ -188,7 +188,7 @@ fn default_creation_entry_type_configs(now: Timestamp) -> Vec<CreationEntryTypeC
build_creation_entry_type_seed("big-fish", "摸鱼", "轻量闯关玩法", "可创建", "/creation-type-references/big-fish.webp", false, true, 20, now),
build_creation_entry_type_seed("puzzle", "拼图", "拼图关卡创作", "可创建", "/creation-type-references/puzzle.webp", true, true, 30, now),
build_creation_entry_type_seed("match3d", "抓大鹅", "3D 消除关卡", "可创建", "/creation-type-references/match3d.webp", true, true, 40, now),
build_creation_entry_type_seed("square-hole", "方洞", "形状投放挑战", "可创建", "/creation-type-references/square-hole.webp", true, true, 50, now),
build_creation_entry_type_seed("square-hole", "方洞", "形状投放挑战", "可创建", "/creation-type-references/square-hole.webp", false, true, 50, now),
build_creation_entry_type_seed("visual-novel", "视觉小说", "分支叙事体验", "可创建", "/creation-type-references/visual-novel.webp", true, true, 60, now),
build_creation_entry_type_seed("airp", "AI RPG", "原生角色扮演", "即将开放", "/creation-type-references/airp.webp", true, false, 70, now),
build_creation_entry_type_seed("creative-agent", "智能体创作", "对话式创作实验", "内测", "/creation-type-references/creative-agent.webp", false, true, 80, now),

View File

@@ -191,7 +191,6 @@ pub struct ProfileInviteCode {
pub(crate) starts_at: Option<Timestamp>,
#[default(None::<Timestamp>)]
pub(crate) expires_at: Option<Timestamp>,
pub(crate) granted_user_tags: Vec<String>,
}
#[spacetimedb::table(
@@ -2062,7 +2061,8 @@ fn redeem_profile_referral_invite_code_record(
if inviter_code.user_id == invitee_user_id {
return Err("不能填写自己的邀请码".to_string());
}
let granted_user_tags = inviter_code.granted_user_tags.clone();
let invite_metadata_user_tags =
profile_invite_code_metadata_user_tags(&inviter_code.metadata_json)?;
let invitee_balance_after = apply_profile_wallet_delta(
ctx,
@@ -2109,7 +2109,7 @@ fn redeem_profile_referral_invite_code_record(
invitee_reward_granted: true,
bound_at,
});
merge_user_account_tags(ctx, &invitee_user_id, granted_user_tags)?;
merge_user_account_tags(ctx, &invitee_user_id, invite_metadata_user_tags)?;
Ok(RuntimeReferralRedeemSnapshot {
center: build_profile_referral_invite_center_snapshot(ctx, &invitee_user_id),
@@ -2283,7 +2283,6 @@ fn admin_upsert_profile_invite_code_record(
input.admin_user_id,
input.invite_code,
input.metadata_json,
input.granted_user_tags,
input.starts_at_micros,
input.expires_at_micros,
input.updated_at_micros,
@@ -2320,7 +2319,6 @@ fn admin_upsert_profile_invite_code_record(
expires_at: validated_input
.expires_at_micros
.map(Timestamp::from_micros_since_unix_epoch),
granted_user_tags: validated_input.granted_user_tags,
});
return Ok(build_profile_invite_code_snapshot_from_row(&inserted));
}
@@ -2337,7 +2335,6 @@ fn admin_upsert_profile_invite_code_record(
expires_at: validated_input
.expires_at_micros
.map(Timestamp::from_micros_since_unix_epoch),
granted_user_tags: validated_input.granted_user_tags,
});
Ok(build_profile_invite_code_snapshot_from_row(&inserted))
}
@@ -2464,7 +2461,6 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
updated_at: ctx.timestamp,
starts_at: None,
expires_at: None,
granted_user_tags: Vec::new(),
})
}
@@ -2483,14 +2479,33 @@ fn merge_user_account_tags(
return Err("用户不存在".to_string());
};
account.user_tags.extend(granted_tags);
let mut next_tags = account.user_tags.take().unwrap_or_default();
next_tags.extend(granted_tags);
account.user_tags =
normalize_profile_user_tags(account.user_tags).map_err(|error| error.to_string())?;
Some(normalize_profile_user_tags(next_tags).map_err(|error| error.to_string())?);
ctx.db.user_account().user_id().delete(&account.user_id);
ctx.db.user_account().insert(account);
Ok(())
}
fn profile_invite_code_metadata_user_tags(metadata_json: &str) -> Result<Vec<String>, String> {
let metadata = serde_json::from_str::<JsonValue>(metadata_json)
.map_err(|_| RuntimeProfileFieldError::InvalidInviteCodeMetadata.to_string())?;
let tags = metadata
.get("userTags")
.or_else(|| metadata.get("user_tags"))
.and_then(JsonValue::as_array)
.map(|items| {
items
.iter()
.filter_map(JsonValue::as_str)
.map(str::to_string)
.collect::<Vec<_>>()
})
.unwrap_or_default();
normalize_profile_user_tags(tags).map_err(|error| error.to_string())
}
fn validate_profile_invite_code_redeem_time(
invite_code: &ProfileInviteCode,
now_micros: i64,
@@ -3515,7 +3530,6 @@ fn build_profile_invite_code_snapshot_from_row(
user_id: row.user_id.clone(),
invite_code: row.invite_code.clone(),
metadata_json: row.metadata_json.clone(),
granted_user_tags: row.granted_user_tags.clone(),
starts_at_micros: row
.starts_at
.map(|value| value.to_micros_since_unix_epoch()),