1
This commit is contained in:
@@ -2344,7 +2344,7 @@ fn execute_publish_world_action(
|
||||
draft_profile_json: serialize_json_value(&JsonValue::Object(draft_profile.clone()))?,
|
||||
legacy_result_profile_json,
|
||||
setting_text,
|
||||
author_display_name: "陶泥主".to_string(),
|
||||
author_display_name: "百梦主".to_string(),
|
||||
published_at_micros: input.submitted_at_micros,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -327,7 +327,7 @@ pub struct CustomWorldProfile {
|
||||
owner_user_id: String,
|
||||
// 作品公开编号是稳定分享键,第一次发布时分配,后续重复发布沿用。
|
||||
public_work_code: Option<String>,
|
||||
// 作者公开陶泥号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
|
||||
// 作者公开百梦号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
|
||||
author_public_user_code: Option<String>,
|
||||
source_agent_session_id: Option<String>,
|
||||
publication_status: CustomWorldPublicationStatus,
|
||||
@@ -3997,7 +3997,7 @@ fn execute_publish_world_action(
|
||||
let author_public_user_code = read_optional_text_field(payload, &["authorPublicUserCode"])
|
||||
.unwrap_or_else(|| build_public_user_code_from_owner_user_id(&session.owner_user_id));
|
||||
let author_display_name = read_optional_text_field(payload, &["authorDisplayName"])
|
||||
.unwrap_or_else(|| "陶泥主".to_string());
|
||||
.unwrap_or_else(|| "百梦主".to_string());
|
||||
let publish_result = publish_custom_world_world_record(
|
||||
ctx,
|
||||
CustomWorldPublishWorldInput {
|
||||
|
||||
@@ -466,7 +466,7 @@ fn compile_match3d_draft_tx(
|
||||
profile_id: input.profile_id.clone(),
|
||||
owner_user_id: input.owner_user_id.clone(),
|
||||
source_session_id: input.session_id.clone(),
|
||||
author_display_name: clean_string(&input.author_display_name, "陶泥主"),
|
||||
author_display_name: clean_string(&input.author_display_name, "百梦主"),
|
||||
game_name,
|
||||
theme_text: config.theme_text.clone(),
|
||||
summary_text,
|
||||
|
||||
@@ -947,7 +947,7 @@ fn save_puzzle_generated_images_tx(
|
||||
draft = replace_puzzle_level(&draft, next_level).map_err(|error| error.to_string())?;
|
||||
|
||||
let saved_at = Timestamp::from_micros_since_unix_epoch(input.saved_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥主")).publish_ready {
|
||||
let next_stage = if build_result_preview(&draft, Some("百梦主")).publish_ready {
|
||||
PuzzleAgentStage::ReadyToPublish
|
||||
} else {
|
||||
PuzzleAgentStage::ImageRefining
|
||||
@@ -1026,7 +1026,7 @@ fn select_puzzle_cover_image_tx(
|
||||
};
|
||||
let draft = replace_puzzle_level(&draft, next_level).map_err(|error| error.to_string())?;
|
||||
let selected_at = Timestamp::from_micros_since_unix_epoch(input.selected_at_micros);
|
||||
let next_stage = if build_result_preview(&draft, Some("陶泥主")).publish_ready {
|
||||
let next_stage = if build_result_preview(&draft, Some("百梦主")).publish_ready {
|
||||
PuzzleAgentStage::ReadyToPublish
|
||||
} else {
|
||||
PuzzleAgentStage::ImageRefining
|
||||
@@ -2080,7 +2080,7 @@ fn build_puzzle_agent_session_snapshot(
|
||||
let messages = list_session_messages(ctx, &row.session_id);
|
||||
let result_preview = draft
|
||||
.as_ref()
|
||||
.map(|value| build_result_preview(value, Some("陶泥主")));
|
||||
.map(|value| build_result_preview(value, Some("百梦主")));
|
||||
|
||||
Ok(PuzzleAgentSessionSnapshot {
|
||||
session_id: row.session_id.clone(),
|
||||
@@ -2268,7 +2268,7 @@ fn upsert_puzzle_draft_work_profile(
|
||||
profile_id,
|
||||
owner_user_id.to_string(),
|
||||
Some(session_id.to_string()),
|
||||
"陶泥主".to_string(),
|
||||
"百梦主".to_string(),
|
||||
draft,
|
||||
updated_at_micros,
|
||||
)
|
||||
@@ -2565,6 +2565,8 @@ fn upsert_puzzle_profile_save_archive(
|
||||
};
|
||||
let world_key = format!("puzzle:{}", run.entry_profile_id);
|
||||
let target = resolve_puzzle_archive_target(ctx, run, current_level)?;
|
||||
let work_title = resolve_puzzle_archive_work_title(ctx, &target.profile_id, &target.level_name);
|
||||
let subtitle = build_puzzle_archive_subtitle(target.level_index, &target.level_name);
|
||||
|
||||
// 中文注释:拼图存档只保存恢复入口所需的最小运行态索引,棋盘真相继续放在 puzzle_runtime_run。
|
||||
let game_state_json = json_to_string(&json!({
|
||||
@@ -2586,8 +2588,8 @@ fn upsert_puzzle_profile_save_archive(
|
||||
owner_user_id: target.owner_user_id,
|
||||
profile_id: Some(run.entry_profile_id.clone()),
|
||||
world_type: Some("PUZZLE".to_string()),
|
||||
world_name: target.level_name,
|
||||
subtitle: format!("第 {} 关", target.level_index),
|
||||
world_name: work_title,
|
||||
subtitle,
|
||||
summary_text: puzzle_archive_summary_text(target.status),
|
||||
cover_image_src: target.cover_image_src,
|
||||
bottom_tab: "puzzle".to_string(),
|
||||
@@ -2682,6 +2684,37 @@ fn resolve_puzzle_archive_target(
|
||||
})
|
||||
}
|
||||
|
||||
fn resolve_puzzle_archive_work_title(
|
||||
ctx: &TxContext,
|
||||
profile_id: &str,
|
||||
fallback_level_name: &str,
|
||||
) -> String {
|
||||
// 中文注释:存档主标题必须是作品名;历史数据或异常行缺失作品名时才回退到关卡名。
|
||||
ctx.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.map(|row| {
|
||||
let title = row.work_title.trim();
|
||||
if title.is_empty() {
|
||||
fallback_level_name.to_string()
|
||||
} else {
|
||||
title.to_string()
|
||||
}
|
||||
})
|
||||
.unwrap_or_else(|| fallback_level_name.to_string())
|
||||
}
|
||||
|
||||
fn build_puzzle_archive_subtitle(level_index: u32, level_name: &str) -> String {
|
||||
let level_label = format!("第 {level_index} 关");
|
||||
let level_name = level_name.trim();
|
||||
if level_name.is_empty() {
|
||||
level_label
|
||||
} else {
|
||||
format!("{level_label} · {level_name}")
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_puzzle_current_owner_user_id(ctx: &TxContext, profile_id: &str) -> Option<String> {
|
||||
ctx.db
|
||||
.puzzle_work_profile()
|
||||
@@ -2745,10 +2778,11 @@ fn accrue_puzzle_point_incentive(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
point_incentive_total_half_points: module_puzzle::puzzle_point_incentive_total_after_spend(
|
||||
row.point_incentive_total_half_points,
|
||||
spent_points,
|
||||
),
|
||||
point_incentive_total_half_points:
|
||||
module_puzzle::puzzle_point_incentive_total_after_spend(
|
||||
row.point_incentive_total_half_points,
|
||||
spent_points,
|
||||
),
|
||||
point_incentive_claimed_points: row.point_incentive_claimed_points,
|
||||
anchor_pack_json: row.anchor_pack_json.clone(),
|
||||
publish_ready: row.publish_ready,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user