Integrate unfinished server-rs refactor worklists
This commit is contained in:
@@ -590,7 +590,7 @@ pub(crate) fn sync_profile_projections_from_snapshot(
|
||||
let game_state_object = game_state.as_object();
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(snapshot.saved_at_micros);
|
||||
|
||||
if is_non_persistent_runtime_snapshot(&game_state) {
|
||||
if module_runtime::is_non_persistent_runtime_snapshot(&game_state) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -614,7 +614,7 @@ pub(crate) fn upsert_profile_played_work(
|
||||
}
|
||||
|
||||
let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||
let played_world_id = format!("{user_id}:{world_key}");
|
||||
let played_world_id = build_runtime_profile_played_world_id(user_id, world_key);
|
||||
let existing = ctx
|
||||
.db
|
||||
.profile_played_world()
|
||||
@@ -673,7 +673,7 @@ pub(crate) fn add_profile_observed_play_time(
|
||||
}
|
||||
|
||||
let observed_at = Timestamp::from_micros_since_unix_epoch(observed_at_micros);
|
||||
let played_world_id = format!("{user_id}:{world_key}");
|
||||
let played_world_id = build_runtime_profile_played_world_id(user_id, world_key);
|
||||
if let Some(existing) = ctx
|
||||
.db
|
||||
.profile_played_world()
|
||||
@@ -785,15 +785,17 @@ 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 next_wallet_balance = module_runtime::read_runtime_json_non_negative_u64(
|
||||
game_state.and_then(|state| state.get("playerCurrency")),
|
||||
);
|
||||
let mut next_total_play_time_ms = previous_total_play_time_ms;
|
||||
|
||||
if next_wallet_balance != previous_wallet_balance {
|
||||
ctx.db.profile_wallet_ledger().insert(ProfileWalletLedger {
|
||||
wallet_ledger_id: format!(
|
||||
"{}:{}:{}",
|
||||
snapshot.user_id, snapshot.saved_at_micros, next_wallet_balance
|
||||
wallet_ledger_id: build_runtime_profile_snapshot_wallet_ledger_id(
|
||||
&snapshot.user_id,
|
||||
snapshot.saved_at_micros,
|
||||
next_wallet_balance,
|
||||
),
|
||||
user_id: snapshot.user_id.clone(),
|
||||
amount_delta: next_wallet_balance as i64 - previous_wallet_balance as i64,
|
||||
@@ -803,14 +805,17 @@ fn sync_profile_dashboard_from_snapshot(
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(world_meta) = resolve_profile_world_snapshot_meta(game_state) {
|
||||
let current_play_time_ms = read_non_negative_u64(
|
||||
if let Some(world_meta) =
|
||||
module_runtime::resolve_runtime_profile_world_snapshot_meta(game_state)
|
||||
{
|
||||
let current_play_time_ms = module_runtime::read_runtime_json_non_negative_u64(
|
||||
game_state
|
||||
.and_then(|state| state.get("runtimeStats"))
|
||||
.and_then(JsonValue::as_object)
|
||||
.and_then(|stats| stats.get("playTimeMs")),
|
||||
);
|
||||
let played_world_id = format!("{}:{}", snapshot.user_id, world_meta.world_key);
|
||||
let played_world_id =
|
||||
build_runtime_profile_played_world_id(&snapshot.user_id, &world_meta.world_key);
|
||||
let existing = ctx
|
||||
.db
|
||||
.profile_played_world()
|
||||
@@ -893,13 +898,15 @@ fn sync_profile_save_archive_from_snapshot(
|
||||
game_state: &JsonValue,
|
||||
saved_at: Timestamp,
|
||||
) -> Result<(), String> {
|
||||
let Some(archive_meta) =
|
||||
resolve_profile_save_archive_meta(game_state, snapshot.current_story_json.as_deref())
|
||||
else {
|
||||
let Some(archive_meta) = module_runtime::resolve_runtime_profile_save_archive_meta(
|
||||
game_state,
|
||||
snapshot.current_story_json.as_deref(),
|
||||
) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let archive_id = format!("{}:{}", snapshot.user_id, archive_meta.world_key);
|
||||
let archive_id =
|
||||
build_runtime_profile_save_archive_id(&snapshot.user_id, &archive_meta.world_key);
|
||||
let existing = ctx.db.profile_save_archive().archive_id().find(&archive_id);
|
||||
let created_at = existing
|
||||
.as_ref()
|
||||
@@ -935,28 +942,6 @@ fn sync_profile_save_archive_from_snapshot(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ProfileWorldSnapshotMeta {
|
||||
world_key: String,
|
||||
owner_user_id: Option<String>,
|
||||
profile_id: Option<String>,
|
||||
world_type: Option<String>,
|
||||
world_title: String,
|
||||
world_subtitle: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct ProfileSaveArchiveMeta {
|
||||
world_key: String,
|
||||
owner_user_id: Option<String>,
|
||||
profile_id: Option<String>,
|
||||
world_type: Option<String>,
|
||||
world_name: String,
|
||||
subtitle: String,
|
||||
summary_text: String,
|
||||
cover_image_src: Option<String>,
|
||||
}
|
||||
|
||||
pub(crate) fn build_profile_save_archive_snapshot_from_row(
|
||||
row: &ProfileSaveArchive,
|
||||
) -> RuntimeProfileSaveArchiveSnapshot {
|
||||
@@ -980,196 +965,6 @@ pub(crate) fn build_profile_save_archive_snapshot_from_row(
|
||||
}
|
||||
}
|
||||
|
||||
fn read_non_negative_u64(value: Option<&JsonValue>) -> u64 {
|
||||
match value {
|
||||
Some(JsonValue::Number(number)) => {
|
||||
if let Some(raw) = number.as_u64() {
|
||||
raw
|
||||
} else if let Some(raw) = number.as_i64() {
|
||||
raw.max(0) as u64
|
||||
} else if let Some(raw) = number.as_f64() {
|
||||
if raw.is_finite() && raw > 0.0 {
|
||||
raw.floor() as u64
|
||||
} else {
|
||||
0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
Some(JsonValue::String(raw)) => raw.trim().parse::<u64>().ok().unwrap_or(0),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn read_string_from_json(value: Option<&JsonValue>) -> Option<String> {
|
||||
value
|
||||
.and_then(JsonValue::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(ToString::to_string)
|
||||
}
|
||||
|
||||
fn resolve_profile_world_snapshot_meta(
|
||||
game_state: Option<&serde_json::Map<String, JsonValue>>,
|
||||
) -> Option<ProfileWorldSnapshotMeta> {
|
||||
let game_state = game_state?;
|
||||
let custom_world_profile = game_state
|
||||
.get("customWorldProfile")
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
if let Some(custom_world_profile) = custom_world_profile {
|
||||
let profile_id = read_string_from_json(custom_world_profile.get("id"));
|
||||
let world_title = read_string_from_json(custom_world_profile.get("name"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("title")));
|
||||
if profile_id.is_some() || world_title.is_some() {
|
||||
let world_title = world_title.unwrap_or_else(|| "自定义世界".to_string());
|
||||
return Some(ProfileWorldSnapshotMeta {
|
||||
world_key: profile_id
|
||||
.as_ref()
|
||||
.map(|profile_id| format!("custom:{profile_id}"))
|
||||
.unwrap_or_else(|| format!("custom:{world_title}")),
|
||||
owner_user_id: None,
|
||||
profile_id,
|
||||
world_type: Some("CUSTOM".to_string()),
|
||||
world_title,
|
||||
world_subtitle: read_string_from_json(custom_world_profile.get("summary"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("settingText")))
|
||||
.unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let world_type = read_string_from_json(game_state.get("worldType"))?;
|
||||
let current_scene_preset = game_state
|
||||
.get("currentScenePreset")
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
Some(ProfileWorldSnapshotMeta {
|
||||
world_key: format!("builtin:{world_type}"),
|
||||
owner_user_id: None,
|
||||
profile_id: None,
|
||||
world_type: Some(world_type.clone()),
|
||||
world_title: current_scene_preset
|
||||
.and_then(|preset| read_string_from_json(preset.get("name")))
|
||||
.unwrap_or_else(|| build_builtin_world_title(&world_type)),
|
||||
world_subtitle: current_scene_preset
|
||||
.and_then(|preset| {
|
||||
read_string_from_json(preset.get("summary"))
|
||||
.or_else(|| read_string_from_json(preset.get("description")))
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_profile_save_archive_meta(
|
||||
game_state: &JsonValue,
|
||||
current_story_json: Option<&str>,
|
||||
) -> Option<ProfileSaveArchiveMeta> {
|
||||
if is_non_persistent_runtime_snapshot(game_state) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let game_state_object = game_state.as_object();
|
||||
let world_meta = resolve_profile_world_snapshot_meta(game_state_object)?;
|
||||
let story_engine_memory = game_state_object
|
||||
.and_then(|state| state.get("storyEngineMemory"))
|
||||
.and_then(JsonValue::as_object);
|
||||
let continue_game_digest = story_engine_memory
|
||||
.and_then(|memory| read_string_from_json(memory.get("continueGameDigest")));
|
||||
let current_story_text = parse_optional_json_str(current_story_json)
|
||||
.ok()
|
||||
.flatten()
|
||||
.and_then(|story| story.as_object().cloned())
|
||||
.and_then(|story| read_string_from_json(story.get("text")));
|
||||
let custom_world_profile = game_state_object
|
||||
.and_then(|state| state.get("customWorldProfile"))
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
if let Some(custom_world_profile) = custom_world_profile {
|
||||
let world_name = read_string_from_json(custom_world_profile.get("name"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("title")))
|
||||
.unwrap_or_else(|| world_meta.world_title.clone());
|
||||
let subtitle = read_string_from_json(custom_world_profile.get("summary"))
|
||||
.or_else(|| read_string_from_json(custom_world_profile.get("settingText")))
|
||||
.unwrap_or_else(|| world_meta.world_subtitle.clone());
|
||||
let summary_text = continue_game_digest
|
||||
.or(current_story_text)
|
||||
.or_else(|| {
|
||||
if subtitle.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(subtitle.clone())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT.to_string());
|
||||
|
||||
return Some(ProfileSaveArchiveMeta {
|
||||
world_key: world_meta.world_key,
|
||||
owner_user_id: world_meta.owner_user_id,
|
||||
profile_id: world_meta.profile_id,
|
||||
world_type: world_meta.world_type,
|
||||
world_name,
|
||||
subtitle,
|
||||
summary_text,
|
||||
cover_image_src: read_string_from_json(custom_world_profile.get("coverImageSrc")),
|
||||
});
|
||||
}
|
||||
|
||||
let summary_text = continue_game_digest
|
||||
.or(current_story_text)
|
||||
.or_else(|| {
|
||||
if world_meta.world_subtitle.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(world_meta.world_subtitle.clone())
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| DEFAULT_SAVE_ARCHIVE_SUMMARY_TEXT.to_string());
|
||||
let current_scene_preset = game_state_object
|
||||
.and_then(|state| state.get("currentScenePreset"))
|
||||
.and_then(JsonValue::as_object);
|
||||
|
||||
Some(ProfileSaveArchiveMeta {
|
||||
world_key: world_meta.world_key,
|
||||
owner_user_id: world_meta.owner_user_id,
|
||||
profile_id: world_meta.profile_id,
|
||||
world_type: world_meta.world_type,
|
||||
world_name: world_meta.world_title,
|
||||
subtitle: world_meta.world_subtitle.clone(),
|
||||
summary_text,
|
||||
cover_image_src: current_scene_preset
|
||||
.and_then(|preset| read_string_from_json(preset.get("imageSrc"))),
|
||||
})
|
||||
}
|
||||
|
||||
fn is_non_persistent_runtime_snapshot(game_state: &JsonValue) -> bool {
|
||||
let Some(game_state) = game_state.as_object() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if game_state
|
||||
.get("runtimePersistenceDisabled")
|
||||
.and_then(JsonValue::as_bool)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
matches!(
|
||||
read_string_from_json(game_state.get("runtimeMode")).as_deref(),
|
||||
Some("preview") | Some("test")
|
||||
)
|
||||
}
|
||||
|
||||
fn build_builtin_world_title(world_type: &str) -> String {
|
||||
match world_type {
|
||||
"WUXIA" => "武侠世界".to_string(),
|
||||
"XIANXIA" => "仙侠世界".to_string(),
|
||||
_ => "叙事世界".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_profile_dashboard_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
input: RuntimeProfileDashboardGetInput,
|
||||
@@ -1307,20 +1102,17 @@ fn create_profile_recharge_order_record(
|
||||
let (points_delta, membership_expires_at) = match product.kind {
|
||||
RuntimeProfileRechargeProductKind::Points => {
|
||||
let has_recharged = has_profile_points_recharged(ctx, &validated_input.user_id);
|
||||
let bonus_points = if has_recharged {
|
||||
0
|
||||
} else {
|
||||
product.bonus_points
|
||||
};
|
||||
let points_delta = product.points_amount.saturating_add(bonus_points);
|
||||
let points_delta =
|
||||
resolve_runtime_profile_points_recharge_delta(&product, has_recharged);
|
||||
apply_profile_wallet_delta(
|
||||
ctx,
|
||||
&validated_input.user_id,
|
||||
points_delta,
|
||||
RuntimeProfileWalletLedgerSourceType::PointsRecharge,
|
||||
&format!(
|
||||
"{}:{}:{}",
|
||||
validated_input.user_id, validated_input.created_at_micros, product.product_id
|
||||
&build_runtime_profile_recharge_wallet_ledger_id(
|
||||
&validated_input.user_id,
|
||||
validated_input.created_at_micros,
|
||||
&product.product_id,
|
||||
),
|
||||
created_at,
|
||||
)?;
|
||||
@@ -1339,9 +1131,10 @@ fn create_profile_recharge_order_record(
|
||||
};
|
||||
|
||||
let order = ProfileRechargeOrder {
|
||||
order_id: format!(
|
||||
"recharge:{}:{}:{}",
|
||||
validated_input.user_id, validated_input.created_at_micros, product.product_id
|
||||
order_id: build_runtime_profile_recharge_order_id(
|
||||
&validated_input.user_id,
|
||||
validated_input.created_at_micros,
|
||||
&product.product_id,
|
||||
),
|
||||
user_id: validated_input.user_id.clone(),
|
||||
product_id: product.product_id.clone(),
|
||||
@@ -1416,25 +1209,25 @@ fn redeem_profile_referral_invite_code_record(
|
||||
&invitee_user_id,
|
||||
PROFILE_REFERRAL_REWARD_POINTS,
|
||||
RuntimeProfileWalletLedgerSourceType::InviteInviteeReward,
|
||||
&format!(
|
||||
"invitee:{}:{}",
|
||||
invitee_user_id, validated_input.updated_at_micros
|
||||
&build_runtime_profile_referral_invitee_ledger_id(
|
||||
&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;
|
||||
should_grant_runtime_profile_inviter_reward(today_inviter_reward_count);
|
||||
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
|
||||
&build_runtime_profile_referral_inviter_ledger_id(
|
||||
&inviter_code.user_id,
|
||||
validated_input.updated_at_micros,
|
||||
),
|
||||
bound_at,
|
||||
)?
|
||||
@@ -1482,45 +1275,21 @@ fn redeem_profile_reward_code_record(
|
||||
.find(&code)
|
||||
.ok_or_else(|| "兑换码不存在".to_string())?;
|
||||
|
||||
if !redeem_code.enabled {
|
||||
return Err("兑换码已停用".to_string());
|
||||
}
|
||||
if redeem_code.reward_points == 0 {
|
||||
return Err("兑换码奖励无效".to_string());
|
||||
}
|
||||
|
||||
let user_used_count = count_profile_redeem_code_user_usage(ctx, &code, &user_id);
|
||||
match redeem_code.mode {
|
||||
RuntimeProfileRedeemCodeMode::Public if user_used_count >= redeem_code.max_uses => {
|
||||
return Err("兑换次数已用完".to_string());
|
||||
}
|
||||
RuntimeProfileRedeemCodeMode::Unique
|
||||
if redeem_code.global_used_count >= redeem_code.max_uses =>
|
||||
{
|
||||
return Err("兑换次数已用完".to_string());
|
||||
}
|
||||
RuntimeProfileRedeemCodeMode::Private => {
|
||||
if !redeem_code
|
||||
.allowed_user_ids
|
||||
.iter()
|
||||
.any(|item| item == &user_id)
|
||||
{
|
||||
return Err("该兑换码不适用于当前账号".to_string());
|
||||
}
|
||||
if redeem_code.global_used_count >= redeem_code.max_uses {
|
||||
return Err("兑换次数已用完".to_string());
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
validate_runtime_profile_redeem_code_usage(
|
||||
&build_profile_redeem_code_snapshot_from_row(&redeem_code),
|
||||
&user_id,
|
||||
user_used_count,
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
|
||||
let usage_id = build_profile_redeem_code_usage_id(
|
||||
ctx,
|
||||
let usage_id = build_runtime_profile_redeem_code_usage_id(
|
||||
&code,
|
||||
&user_id,
|
||||
validated_input.redeemed_at_micros,
|
||||
user_used_count,
|
||||
);
|
||||
let wallet_ledger_id = format!("{}:ledger", usage_id);
|
||||
let wallet_ledger_id = build_runtime_profile_redeem_code_ledger_id(&usage_id);
|
||||
let wallet_balance = apply_profile_wallet_delta(
|
||||
ctx,
|
||||
&user_id,
|
||||
@@ -1669,7 +1438,7 @@ fn build_profile_referral_invite_center_snapshot(
|
||||
RuntimeReferralInviteCenterSnapshot {
|
||||
user_id: user_id.to_string(),
|
||||
invite_code: code.invite_code.clone(),
|
||||
invite_link_path: format!("/?inviteCode={}", code.invite_code),
|
||||
invite_link_path: build_runtime_profile_invite_link_path(&code.invite_code),
|
||||
invited_count,
|
||||
rewarded_invite_count,
|
||||
today_inviter_reward_count,
|
||||
@@ -1697,7 +1466,7 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
|
||||
return row;
|
||||
}
|
||||
|
||||
let mut invite_code = build_profile_invite_code(user_id, 0);
|
||||
let mut invite_code = build_runtime_profile_invite_code(user_id, 0);
|
||||
let mut salt = 1;
|
||||
while ctx
|
||||
.db
|
||||
@@ -1706,7 +1475,7 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
|
||||
.find(&invite_code)
|
||||
.is_some()
|
||||
{
|
||||
invite_code = build_profile_invite_code(user_id, salt);
|
||||
invite_code = build_runtime_profile_invite_code(user_id, salt);
|
||||
salt += 1;
|
||||
}
|
||||
|
||||
@@ -1718,21 +1487,12 @@ fn ensure_profile_invite_code(ctx: &ReducerContext, user_id: &str) -> ProfileInv
|
||||
})
|
||||
}
|
||||
|
||||
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;
|
||||
let day_start_micros = runtime_profile_day_start_micros(now.to_micros_since_unix_epoch());
|
||||
ctx.db
|
||||
.profile_wallet_ledger()
|
||||
.iter()
|
||||
@@ -1831,24 +1591,25 @@ fn apply_profile_membership_purchase(
|
||||
.user_id()
|
||||
.find(&user_id.to_string());
|
||||
let purchased_at_micros = purchased_at.to_micros_since_unix_epoch();
|
||||
let start_at_micros = current
|
||||
.as_ref()
|
||||
.map(|row| row.expires_at.to_micros_since_unix_epoch())
|
||||
.filter(|expires_at_micros| *expires_at_micros > purchased_at_micros)
|
||||
.unwrap_or(purchased_at_micros);
|
||||
let expires_at = Timestamp::from_micros_since_unix_epoch(
|
||||
start_at_micros.saturating_add(duration_days as i64 * 86_400_000_000),
|
||||
let purchase_update = resolve_runtime_profile_membership_purchase_update(
|
||||
current
|
||||
.as_ref()
|
||||
.map(|row| row.started_at.to_micros_since_unix_epoch()),
|
||||
current
|
||||
.as_ref()
|
||||
.map(|row| row.expires_at.to_micros_since_unix_epoch()),
|
||||
purchased_at_micros,
|
||||
duration_days,
|
||||
);
|
||||
let created_at = current
|
||||
.as_ref()
|
||||
.map(|row| row.started_at)
|
||||
.unwrap_or(purchased_at);
|
||||
let expires_at = Timestamp::from_micros_since_unix_epoch(purchase_update.expires_at_micros);
|
||||
let created_at = Timestamp::from_micros_since_unix_epoch(purchase_update.started_at_micros);
|
||||
let current = current.map(|row| row.user_id);
|
||||
|
||||
if let Some(existing) = current {
|
||||
if let Some(existing_user_id) = current {
|
||||
ctx.db
|
||||
.profile_membership()
|
||||
.user_id()
|
||||
.delete(&existing.user_id);
|
||||
.delete(&existing_user_id);
|
||||
}
|
||||
|
||||
ctx.db.profile_membership().insert(ProfileMembership {
|
||||
@@ -1871,8 +1632,8 @@ fn apply_profile_wallet_delta(
|
||||
ledger_id: &str,
|
||||
created_at: Timestamp,
|
||||
) -> Result<u64, String> {
|
||||
let amount_delta =
|
||||
i64::try_from(amount_delta).map_err(|_| "profile.wallet_amount 超出上限".to_string())?;
|
||||
let amount_delta = convert_runtime_profile_wallet_unsigned_delta(amount_delta)
|
||||
.map_err(|error| error.to_string())?;
|
||||
apply_profile_wallet_signed_delta(
|
||||
ctx,
|
||||
user_id,
|
||||
@@ -1898,10 +1659,12 @@ fn apply_profile_wallet_adjustment(
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let created_at = Timestamp::from_micros_since_unix_epoch(validated_input.created_at_micros);
|
||||
let unsigned_delta = convert_runtime_profile_wallet_unsigned_delta(validated_input.amount)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let amount_delta = if consume {
|
||||
-(validated_input.amount as i64)
|
||||
-unsigned_delta
|
||||
} else {
|
||||
validated_input.amount as i64
|
||||
unsigned_delta
|
||||
};
|
||||
|
||||
apply_profile_wallet_signed_delta(
|
||||
@@ -1947,15 +1710,8 @@ fn apply_profile_wallet_signed_delta(
|
||||
.user_id()
|
||||
.find(&user_id.to_string());
|
||||
let previous_balance = current.as_ref().map(|row| row.wallet_balance).unwrap_or(0);
|
||||
let next_balance = if amount_delta >= 0 {
|
||||
previous_balance
|
||||
.checked_add(amount_delta as u64)
|
||||
.ok_or_else(|| "profile.wallet_balance 超出上限".to_string())?
|
||||
} else {
|
||||
previous_balance
|
||||
.checked_sub(amount_delta.unsigned_abs())
|
||||
.ok_or_else(|| "叙世币余额不足".to_string())?
|
||||
};
|
||||
let next_balance = calculate_runtime_profile_wallet_balance(previous_balance, amount_delta)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let created_state_at = current
|
||||
.as_ref()
|
||||
.map(|row| row.created_at)
|
||||
@@ -2034,19 +1790,6 @@ fn count_profile_redeem_code_user_usage(ctx: &ReducerContext, code: &str, user_i
|
||||
.count() as u32
|
||||
}
|
||||
|
||||
fn build_profile_redeem_code_usage_id(
|
||||
ctx: &ReducerContext,
|
||||
code: &str,
|
||||
user_id: &str,
|
||||
redeemed_at_micros: i64,
|
||||
) -> String {
|
||||
let sequence = count_profile_redeem_code_user_usage(ctx, code, user_id);
|
||||
format!(
|
||||
"redeem:{}:{}:{}:{}",
|
||||
code, user_id, redeemed_at_micros, sequence
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_profile_redeem_code_allowed_user_ids(
|
||||
ctx: &ReducerContext,
|
||||
input: &RuntimeProfileRedeemCodeAdminUpsertInput,
|
||||
|
||||
Reference in New Issue
Block a user