Implement registration invite code flow and admin invite codes

This commit is contained in:
2026-04-30 20:49:38 +08:00
parent 2aef81e51d
commit 42aab671ed
32 changed files with 1241 additions and 179 deletions

View File

@@ -2856,7 +2856,9 @@ fn list_custom_world_profile_snapshots(
Ok(entries)
}
fn build_custom_world_profile_list_snapshot(row: &CustomWorldProfile) -> CustomWorldProfileSnapshot {
fn build_custom_world_profile_list_snapshot(
row: &CustomWorldProfile,
) -> CustomWorldProfileSnapshot {
let mut snapshot = build_custom_world_profile_snapshot(row);
snapshot.profile_payload_json = build_custom_world_profile_list_payload_json(row);
snapshot

View File

@@ -1094,6 +1094,14 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
.or_insert(serde_json::Value::Null);
}
}
if table_name == "profile_invite_code" {
if let Some(object) = next_value.as_object_mut() {
// 中文注释:邀请码 metadata 晚于邀请表加入,旧迁移包按空对象兼容。
object
.entry("metadata_json".to_string())
.or_insert_with(|| serde_json::Value::String("{}".to_string()));
}
}
if table_name == "big_fish_creation_session" {
if let Some(object) = next_value.as_object_mut() {
// 中文注释:旧迁移包没有公开游玩次数字段,导入时按新建作品默认 0 兼容。

View File

@@ -1261,8 +1261,9 @@ fn start_puzzle_run_tx(
}
let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
let started_at_ms = micros_to_millis(input.started_at_micros);
let mut run = module_puzzle::start_run_at(input.run_id.clone(), &entry_profile, 0, started_at_ms)
.map_err(|error| error.to_string())?;
let mut run =
module_puzzle::start_run_at(input.run_id.clone(), &entry_profile, 0, started_at_ms)
.map_err(|error| error.to_string())?;
let current_grid_size = run.current_grid_size;
let current_profile_id = entry_profile.profile_id.clone();
hydrate_puzzle_leaderboard_entries(
@@ -1502,13 +1503,11 @@ fn use_puzzle_runtime_prop_tx(
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
let current_run = deserialize_run(&row.snapshot_json)?;
let next_run = match input.prop_kind.as_str() {
"freezeTime" | "freeze_time" => {
module_puzzle::apply_puzzle_freeze_time_at(
&current_run,
micros_to_millis(input.used_at_micros),
)
.map_err(|error| error.to_string())?
}
"freezeTime" | "freeze_time" => module_puzzle::apply_puzzle_freeze_time_at(
&current_run,
micros_to_millis(input.used_at_micros),
)
.map_err(|error| error.to_string())?,
"hint" => module_puzzle::set_puzzle_run_paused_at(
&current_run,
false,

View File

@@ -70,6 +70,7 @@ pub struct ProfileInviteCode {
pub(crate) user_id: String,
#[unique]
pub(crate) invite_code: String,
pub(crate) metadata_json: String,
pub(crate) created_at: Timestamp,
pub(crate) updated_at: Timestamp,
}
@@ -528,6 +529,25 @@ pub fn admin_disable_profile_redeem_code(
}
}
#[spacetimedb::procedure]
pub fn admin_upsert_profile_invite_code(
ctx: &mut ProcedureContext,
input: RuntimeProfileInviteCodeAdminUpsertInput,
) -> RuntimeProfileInviteCodeAdminProcedureResult {
match ctx.try_with_tx(|tx| admin_upsert_profile_invite_code_record(tx, input.clone())) {
Ok(record) => RuntimeProfileInviteCodeAdminProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => RuntimeProfileInviteCodeAdminProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
pub(crate) fn list_profile_save_archive_rows(
ctx: &ReducerContext,
input: RuntimeProfileSaveArchiveListInput,
@@ -1534,10 +1554,14 @@ fn redeem_profile_referral_invite_code_record(
),
bound_at,
)?;
let today_inviter_reward_count =
count_today_profile_referral_inviter_rewards(ctx, &inviter_code.user_id, bound_at);
let inviter_reward_granted =
today_inviter_reward_count < PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT;
let is_admin_invite_code = is_admin_profile_invite_code_user_id(&inviter_code.user_id);
let today_inviter_reward_count = if is_admin_invite_code {
0
} else {
count_today_profile_referral_inviter_rewards(ctx, &inviter_code.user_id, bound_at)
};
let inviter_reward_granted = !is_admin_invite_code
&& today_inviter_reward_count < PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT;
let inviter_balance_after = if inviter_reward_granted {
apply_profile_wallet_delta(
ctx,
@@ -1753,6 +1777,56 @@ fn admin_disable_profile_redeem_code_record(
Ok(build_profile_redeem_code_snapshot_from_row(&inserted))
}
fn admin_upsert_profile_invite_code_record(
ctx: &ReducerContext,
input: RuntimeProfileInviteCodeAdminUpsertInput,
) -> Result<RuntimeProfileInviteCodeSnapshot, String> {
let validated_input = build_runtime_profile_invite_code_admin_upsert_input(
input.admin_user_id,
input.invite_code,
input.metadata_json,
input.updated_at_micros,
)
.map_err(|error| error.to_string())?;
let updated_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
let user_id = build_admin_profile_invite_code_user_id(
&validated_input.admin_user_id,
&validated_input.invite_code,
);
if let Some(existing) = ctx
.db
.profile_invite_code()
.invite_code()
.find(&validated_input.invite_code)
{
if existing.user_id != user_id {
return Err("邀请码已被其他用户占用".to_string());
}
ctx.db
.profile_invite_code()
.user_id()
.delete(&existing.user_id);
let inserted = ctx.db.profile_invite_code().insert(ProfileInviteCode {
user_id,
invite_code: validated_input.invite_code,
metadata_json: validated_input.metadata_json,
created_at: existing.created_at,
updated_at,
});
return Ok(build_profile_invite_code_snapshot_from_row(&inserted));
}
let inserted = ctx.db.profile_invite_code().insert(ProfileInviteCode {
user_id,
invite_code: validated_input.invite_code,
metadata_json: validated_input.metadata_json,
created_at: updated_at,
updated_at,
});
Ok(build_profile_invite_code_snapshot_from_row(&inserted))
}
fn build_profile_referral_invite_center_snapshot(
ctx: &ReducerContext,
user_id: &str,
@@ -1825,6 +1899,7 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
ctx.db.profile_invite_code().insert(ProfileInviteCode {
user_id: user_id.to_string(),
invite_code,
metadata_json: PROFILE_INVITE_CODE_METADATA_DEFAULT_JSON.to_string(),
created_at: ctx.timestamp,
updated_at: ctx.timestamp,
})
@@ -1856,6 +1931,14 @@ fn count_today_profile_referral_inviter_rewards(
.count() as u32
}
fn is_admin_profile_invite_code_user_id(user_id: &str) -> bool {
user_id.starts_with("admin:")
}
fn build_admin_profile_invite_code_user_id(admin_user_id: &str, invite_code: &str) -> String {
format!("admin:{}:{}", admin_user_id, invite_code)
}
fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 {
ctx.db
.profile_dashboard_state()
@@ -2206,6 +2289,18 @@ fn build_profile_redeem_code_snapshot_from_row(
}
}
fn build_profile_invite_code_snapshot_from_row(
row: &ProfileInviteCode,
) -> RuntimeProfileInviteCodeSnapshot {
RuntimeProfileInviteCodeSnapshot {
user_id: row.user_id.clone(),
invite_code: row.invite_code.clone(),
metadata_json: row.metadata_json.clone(),
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}
}
fn build_profile_wallet_ledger_snapshot_from_row(
row: &ProfileWalletLedger,
) -> RuntimeProfileWalletLedgerEntrySnapshot {