1
This commit is contained in:
@@ -28,6 +28,34 @@ pub struct ProfileWalletLedger {
|
||||
pub(crate) created_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = profile_invite_code)]
|
||||
pub struct ProfileInviteCode {
|
||||
#[primary_key]
|
||||
pub(crate) user_id: String,
|
||||
#[unique]
|
||||
pub(crate) invite_code: String,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = profile_referral_relation,
|
||||
index(accessor = by_profile_referral_inviter_user_id, btree(columns = [inviter_user_id])),
|
||||
index(
|
||||
accessor = by_profile_referral_inviter_bound_at,
|
||||
btree(columns = [inviter_user_id, bound_at])
|
||||
)
|
||||
)]
|
||||
pub struct ProfileReferralRelation {
|
||||
#[primary_key]
|
||||
pub(crate) invitee_user_id: String,
|
||||
pub(crate) inviter_user_id: String,
|
||||
pub(crate) invite_code: String,
|
||||
pub(crate) inviter_reward_granted: bool,
|
||||
pub(crate) invitee_reward_granted: bool,
|
||||
pub(crate) bound_at: Timestamp,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = profile_played_world,
|
||||
index(accessor = by_profile_played_world_user_id, btree(columns = [user_id])),
|
||||
@@ -274,6 +302,46 @@ pub fn create_profile_recharge_order_and_return(
|
||||
}
|
||||
}
|
||||
|
||||
// 邀请中心会在首次打开时为账号创建稳定邀请码,前端只展示这里返回的后端状态。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn get_profile_referral_invite_center(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeReferralInviteCenterGetInput,
|
||||
) -> RuntimeReferralInviteCenterProcedureResult {
|
||||
match ctx.try_with_tx(|tx| get_profile_referral_invite_center_snapshot(tx, input.clone())) {
|
||||
Ok(record) => RuntimeReferralInviteCenterProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeReferralInviteCenterProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 填码绑定、每日邀请者奖励上限和双方积分发放都在同一事务内完成。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn redeem_profile_referral_invite_code(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: RuntimeReferralRedeemInput,
|
||||
) -> RuntimeReferralRedeemProcedureResult {
|
||||
match ctx.try_with_tx(|tx| redeem_profile_referral_invite_code_record(tx, input.clone())) {
|
||||
Ok(record) => RuntimeReferralRedeemProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => RuntimeReferralRedeemProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn list_profile_save_archive_rows(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileSaveArchiveListInput,
|
||||
@@ -948,6 +1016,215 @@ fn create_profile_recharge_order_record(
|
||||
))
|
||||
}
|
||||
|
||||
fn get_profile_referral_invite_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeReferralInviteCenterGetInput,
|
||||
) -> Result<RuntimeReferralInviteCenterSnapshot, String> {
|
||||
let validated_input = build_runtime_referral_invite_center_get_input(input.user_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
Ok(build_profile_referral_invite_center_snapshot(
|
||||
ctx,
|
||||
&validated_input.user_id,
|
||||
))
|
||||
}
|
||||
|
||||
fn redeem_profile_referral_invite_code_record(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeReferralRedeemInput,
|
||||
) -> Result<RuntimeReferralRedeemSnapshot, String> {
|
||||
let validated_input = build_runtime_referral_redeem_input(
|
||||
input.user_id,
|
||||
input.invite_code,
|
||||
input.updated_at_micros,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let bound_at = Timestamp::from_micros_since_unix_epoch(validated_input.updated_at_micros);
|
||||
let invitee_user_id = validated_input.user_id;
|
||||
let invite_code = validated_input.invite_code;
|
||||
|
||||
if ctx
|
||||
.db
|
||||
.profile_referral_relation()
|
||||
.invitee_user_id()
|
||||
.find(&invitee_user_id)
|
||||
.is_some()
|
||||
{
|
||||
return Err("每个用户最多只能填写一个邀请码".to_string());
|
||||
}
|
||||
|
||||
let inviter_code = ctx
|
||||
.db
|
||||
.profile_invite_code()
|
||||
.invite_code()
|
||||
.find(&invite_code)
|
||||
.ok_or_else(|| "邀请码不存在".to_string())?;
|
||||
if inviter_code.user_id == invitee_user_id {
|
||||
return Err("不能填写自己的邀请码".to_string());
|
||||
}
|
||||
|
||||
let invitee_balance_after = apply_profile_wallet_delta(
|
||||
ctx,
|
||||
&invitee_user_id,
|
||||
PROFILE_REFERRAL_REWARD_POINTS,
|
||||
RuntimeProfileWalletLedgerSourceType::InviteInviteeReward,
|
||||
&format!(
|
||||
"invitee:{}:{}",
|
||||
invitee_user_id, validated_input.updated_at_micros
|
||||
),
|
||||
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 inviter_balance_after = if inviter_reward_granted {
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
&inviter_code.user_id,
|
||||
PROFILE_REFERRAL_REWARD_POINTS,
|
||||
RuntimeProfileWalletLedgerSourceType::InviteInviterReward,
|
||||
&format!(
|
||||
"inviter:{}:{}",
|
||||
inviter_code.user_id, validated_input.updated_at_micros
|
||||
),
|
||||
bound_at,
|
||||
)?
|
||||
} else {
|
||||
profile_wallet_balance(ctx, &inviter_code.user_id)
|
||||
};
|
||||
|
||||
ctx.db
|
||||
.profile_referral_relation()
|
||||
.insert(ProfileReferralRelation {
|
||||
invitee_user_id: invitee_user_id.clone(),
|
||||
inviter_user_id: inviter_code.user_id,
|
||||
invite_code,
|
||||
inviter_reward_granted,
|
||||
invitee_reward_granted: true,
|
||||
bound_at,
|
||||
});
|
||||
|
||||
Ok(RuntimeReferralRedeemSnapshot {
|
||||
center: build_profile_referral_invite_center_snapshot(ctx, &invitee_user_id),
|
||||
invitee_reward_granted: true,
|
||||
inviter_reward_granted,
|
||||
invitee_balance_after,
|
||||
inviter_balance_after,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_profile_referral_invite_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
) -> RuntimeReferralInviteCenterSnapshot {
|
||||
let code = ensure_profile_invite_code(ctx, user_id);
|
||||
let today_inviter_reward_count =
|
||||
count_today_profile_referral_inviter_rewards(ctx, user_id, ctx.timestamp);
|
||||
let invited_count = ctx
|
||||
.db
|
||||
.profile_referral_relation()
|
||||
.iter()
|
||||
.filter(|row| row.inviter_user_id == user_id)
|
||||
.count() as u32;
|
||||
let rewarded_invite_count = ctx
|
||||
.db
|
||||
.profile_referral_relation()
|
||||
.iter()
|
||||
.filter(|row| row.inviter_user_id == user_id && row.inviter_reward_granted)
|
||||
.count() as u32;
|
||||
let bound_relation = ctx
|
||||
.db
|
||||
.profile_referral_relation()
|
||||
.invitee_user_id()
|
||||
.find(&user_id.to_string());
|
||||
|
||||
RuntimeReferralInviteCenterSnapshot {
|
||||
user_id: user_id.to_string(),
|
||||
invite_code: code.invite_code.clone(),
|
||||
invite_link_path: format!("/?inviteCode={}", code.invite_code),
|
||||
invited_count,
|
||||
rewarded_invite_count,
|
||||
today_inviter_reward_count,
|
||||
today_inviter_reward_remaining: PROFILE_REFERRAL_DAILY_INVITER_REWARD_LIMIT
|
||||
.saturating_sub(today_inviter_reward_count),
|
||||
reward_points: PROFILE_REFERRAL_REWARD_POINTS,
|
||||
has_redeemed_code: bound_relation.is_some(),
|
||||
bound_inviter_user_id: bound_relation
|
||||
.as_ref()
|
||||
.map(|relation| relation.inviter_user_id.clone()),
|
||||
bound_at_micros: bound_relation
|
||||
.as_ref()
|
||||
.map(|relation| relation.bound_at.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: code.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInviteCode {
|
||||
if let Some(row) = ctx
|
||||
.db
|
||||
.profile_invite_code()
|
||||
.user_id()
|
||||
.find(&user_id.to_string())
|
||||
{
|
||||
return row;
|
||||
}
|
||||
|
||||
let mut invite_code = build_profile_invite_code(user_id, 0);
|
||||
let mut salt = 1;
|
||||
while ctx
|
||||
.db
|
||||
.profile_invite_code()
|
||||
.invite_code()
|
||||
.find(&invite_code)
|
||||
.is_some()
|
||||
{
|
||||
invite_code = build_profile_invite_code(user_id, salt);
|
||||
salt += 1;
|
||||
}
|
||||
|
||||
ctx.db.profile_invite_code().insert(ProfileInviteCode {
|
||||
user_id: user_id.to_string(),
|
||||
invite_code,
|
||||
created_at: ctx.timestamp,
|
||||
updated_at: ctx.timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
fn build_profile_invite_code(user_id: &str, salt: u32) -> String {
|
||||
let mut hash = 14_695_981_039_346_656_037u64;
|
||||
for byte in user_id.as_bytes().iter().copied().chain(salt.to_le_bytes()) {
|
||||
hash ^= byte as u64;
|
||||
hash = hash.wrapping_mul(1_099_511_628_211);
|
||||
}
|
||||
format!("SY{:08X}", hash as u32)
|
||||
}
|
||||
|
||||
fn count_today_profile_referral_inviter_rewards(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
now: Timestamp,
|
||||
) -> u32 {
|
||||
let day_start_micros = (now.to_micros_since_unix_epoch() / 86_400_000_000) * 86_400_000_000;
|
||||
ctx.db
|
||||
.profile_wallet_ledger()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.user_id == user_id
|
||||
&& row.source_type == RuntimeProfileWalletLedgerSourceType::InviteInviterReward
|
||||
&& row.created_at.to_micros_since_unix_epoch() >= day_start_micros
|
||||
})
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
fn profile_wallet_balance(ctx: &ReducerContext, user_id: &str) -> u64 {
|
||||
ctx.db
|
||||
.profile_dashboard_state()
|
||||
.user_id()
|
||||
.find(&user_id.to_string())
|
||||
.map(|row| row.wallet_balance)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn build_profile_recharge_center_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
user_id: &str,
|
||||
|
||||
Reference in New Issue
Block a user