This commit is contained in:
2026-05-01 20:29:09 +08:00
parent 8718472dbd
commit 87fbf41fab
137 changed files with 2922 additions and 989 deletions

View File

@@ -2,6 +2,8 @@ use crate::*;
const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000;
const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
const PROFILE_REFERRAL_INVITED_USERS_LIMIT: usize = 20;
const PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX: &str = "new-user-registration";
#[spacetimedb::table(accessor = profile_dashboard_state)]
pub struct ProfileDashboardState {
@@ -353,6 +355,26 @@ pub fn list_profile_wallet_ledger(
}
}
// 新用户注册赠送由后端注册链路调用;流水 ID 固定,保证重试不重复发放。
#[spacetimedb::procedure]
pub fn grant_new_user_registration_wallet_reward(
ctx: &mut ProcedureContext,
input: RuntimeProfileDashboardGetInput,
) -> RuntimeProfileWalletAdjustmentProcedureResult {
match ctx.try_with_tx(|tx| grant_new_user_registration_wallet_reward_tx(tx, input.clone())) {
Ok(record) => RuntimeProfileWalletAdjustmentProcedureResult {
ok: true,
record: Some(record),
error_message: None,
},
Err(message) => RuntimeProfileWalletAdjustmentProcedureResult {
ok: false,
record: None,
error_message: Some(message),
},
}
}
// 资产生成由 Axum 调用外部模型,钱包扣费必须先在 SpacetimeDB 内原子落账。
#[spacetimedb::procedure]
pub fn consume_profile_wallet_points_and_return(
@@ -491,7 +513,7 @@ pub fn get_profile_referral_invite_center(
}
}
// 填码绑定、每日邀请者奖励上限和双方陶泥币发放都在同一事务内完成。
// 填码绑定、每日邀请者奖励上限和双方光点发放都在同一事务内完成。
#[spacetimedb::procedure]
pub fn redeem_profile_referral_invite_code(
ctx: &mut ProcedureContext,
@@ -1041,11 +1063,18 @@ fn sync_profile_dashboard_from_snapshot(
.as_ref()
.map(|row| row.total_play_time_ms)
.unwrap_or(0);
let next_wallet_balance =
read_non_negative_u64(game_state.and_then(|state| state.get("playerCurrency")));
let has_business_wallet_ledger = has_profile_business_wallet_ledger(ctx, &snapshot.user_id);
let synced_wallet_balance = if has_business_wallet_ledger {
None
} else {
read_optional_non_negative_u64(game_state.and_then(|state| state.get("playerCurrency")))
};
let next_wallet_balance = synced_wallet_balance.unwrap_or(previous_wallet_balance);
let mut next_total_play_time_ms = previous_total_play_time_ms;
if next_wallet_balance != previous_wallet_balance {
if let Some(next_wallet_balance) = synced_wallet_balance
&& next_wallet_balance != previous_wallet_balance
{
ctx.db.profile_wallet_ledger().insert(ProfileWalletLedger {
wallet_ledger_id: format!(
"{}:{}:{}",
@@ -1258,6 +1287,10 @@ fn read_non_negative_u64(value: Option<&JsonValue>) -> u64 {
}
}
fn read_optional_non_negative_u64(value: Option<&JsonValue>) -> Option<u64> {
value.map(|raw| read_non_negative_u64(Some(raw)))
}
fn read_string_from_json(value: Option<&JsonValue>) -> Option<String> {
value
.and_then(JsonValue::as_str)
@@ -1986,6 +2019,7 @@ fn build_profile_referral_invite_center_snapshot(
today_inviter_reward_remaining: PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT
.saturating_sub(today_inviter_reward_count),
reward_points: PROFILE_REFERRAL_REWARD_POINTS,
invited_users: list_profile_referral_invited_users(ctx, user_id),
has_redeemed_code: bound_relation.is_some(),
bound_inviter_user_id: bound_relation
.as_ref()
@@ -1997,6 +2031,50 @@ fn build_profile_referral_invite_center_snapshot(
}
}
fn list_profile_referral_invited_users(
ctx: &ReducerContext,
inviter_user_id: &str,
) -> Vec<RuntimeReferralInvitedUserSnapshot> {
// 中文注释:邀请面板只展示最近成功邀请用户,完整统计仍由计数字段承担。
let inviter_user_id = inviter_user_id.to_string();
let mut relations = ctx
.db
.profile_referral_relation()
.by_profile_referral_inviter_user_id()
.filter(&inviter_user_id)
.collect::<Vec<_>>();
relations.sort_by(|left, right| {
right
.bound_at
.to_micros_since_unix_epoch()
.cmp(&left.bound_at.to_micros_since_unix_epoch())
});
relations
.into_iter()
.take(PROFILE_REFERRAL_INVITED_USERS_LIMIT)
.map(|relation| {
let account = ctx
.db
.user_account()
.user_id()
.find(&relation.invitee_user_id);
RuntimeReferralInvitedUserSnapshot {
user_id: relation.invitee_user_id,
display_name: account
.as_ref()
.map(|user| user.display_name.trim())
.filter(|name| !name.is_empty())
.unwrap_or("玩家")
.to_string(),
avatar_url: account.and_then(|user| user.avatar_url),
bound_at_micros: relation.bound_at.to_micros_since_unix_epoch(),
}
})
.collect()
}
fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInviteCode {
if let Some(row) = ctx
.db
@@ -2072,6 +2150,42 @@ fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 {
.unwrap_or(0)
}
fn build_new_user_registration_wallet_ledger_id(user_id: &str) -> String {
format!("{PROFILE_NEW_USER_REGISTRATION_LEDGER_PREFIX}:{user_id}")
}
fn grant_new_user_registration_wallet_reward_tx(
ctx: &ReducerContext,
input: RuntimeProfileDashboardGetInput,
) -> Result<RuntimeProfileDashboardSnapshot, String> {
let validated_input = build_runtime_profile_dashboard_get_input(input.user_id)
.map_err(|error| error.to_string())?;
let ledger_id = build_new_user_registration_wallet_ledger_id(&validated_input.user_id);
if ctx
.db
.profile_wallet_ledger()
.wallet_ledger_id()
.find(&ledger_id)
.is_none()
{
apply_profile_wallet_delta(
ctx,
&validated_input.user_id,
PROFILE_NEW_USER_INITIAL_WALLET_POINTS,
RuntimeProfileWalletLedgerSourceType::NewUserRegistrationReward,
&ledger_id,
ctx.timestamp,
)?;
}
get_profile_dashboard_snapshot(
ctx,
RuntimeProfileDashboardGetInput {
user_id: validated_input.user_id,
},
)
}
fn build_profile_recharge_center_snapshot(
ctx: &ReducerContext,
user_id: &str,
@@ -2291,7 +2405,7 @@ fn apply_profile_wallet_signed_delta(
} else {
previous_balance
.checked_sub(amount_delta.unsigned_abs())
.ok_or_else(|| "陶泥币余额不足".to_string())?
.ok_or_else(|| "光点余额不足".to_string())?
};
let created_state_at = current
.as_ref()
@@ -2343,6 +2457,13 @@ fn has_profile_points_recharged(ctx: &ReducerContext, user_id: &str) -> bool {
})
}
fn has_profile_business_wallet_ledger(ctx: &ReducerContext, user_id: &str) -> bool {
ctx.db.profile_wallet_ledger().iter().any(|row| {
row.user_id == user_id
&& row.source_type != RuntimeProfileWalletLedgerSourceType::SnapshotSync
})
}
fn latest_profile_recharge_order(
ctx: &ReducerContext,
user_id: &str,