This commit is contained in:
2026-05-01 01:30:02 +08:00
parent aabad6407f
commit 2e9d0f4640
92 changed files with 4548 additions and 248 deletions

View File

@@ -1145,6 +1145,12 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
object
.entry("like_count".to_string())
.or_insert_with(|| serde_json::Value::from(0));
object
.entry("point_incentive_total_half_points".to_string())
.or_insert_with(|| serde_json::Value::from(0));
object
.entry("point_incentive_claimed_points".to_string())
.or_insert_with(|| serde_json::Value::from(0));
// 中文注释:拼图多关卡字段晚于旧作品表加入,旧迁移包留空并由读取层补出首关。
object
.entry("levels_json".to_string())

View File

@@ -1,8 +1,8 @@
use crate::runtime::{
ProfilePlayedWorkUpsertInput, PublicWorkLikeRecordInput, PublicWorkPlayRecordInput,
ProfileSaveArchiveUpsertInput,
add_profile_observed_play_time, count_recent_public_work_plays, record_public_work_like,
record_public_work_play, upsert_profile_played_work, upsert_profile_save_archive,
ProfilePlayedWorkUpsertInput, ProfileSaveArchiveUpsertInput, PublicWorkLikeRecordInput,
PublicWorkPlayRecordInput, add_profile_observed_play_time, count_recent_public_work_plays,
grant_profile_wallet_points, record_public_work_like, record_public_work_play,
upsert_profile_played_work, upsert_profile_save_archive,
};
use module_puzzle::{
PUZZLE_MAX_TAG_COUNT, PUZZLE_NEXT_LEVEL_MODE_NONE, PUZZLE_NEXT_LEVEL_MODE_SAME_WORK,
@@ -16,19 +16,23 @@ use module_puzzle::{
PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult, PuzzleRunPropInput,
PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus,
PuzzleSelectCoverImageInput, PuzzleWorkDeleteInput, PuzzleWorkGetInput,
PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
apply_publish_overrides_to_draft, apply_selected_candidate, build_form_draft_from_seed,
build_result_preview, compile_result_draft_from_seed, create_work_profile, infer_anchor_pack,
normalize_puzzle_draft, normalize_puzzle_levels, normalize_theme_tags, publish_work_profile,
replace_puzzle_level, resolve_puzzle_grid_size, select_next_profiles,
selected_profile_level_after_runtime_level, selected_puzzle_level, tag_similarity_score,
PuzzleWorkLikeRecordInput as PuzzleWorkLikeInput, PuzzleWorkPointIncentiveClaimInput,
PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkRemixInput, PuzzleWorkUpsertInput,
PuzzleWorksListInput, PuzzleWorksProcedureResult, apply_publish_overrides_to_draft,
apply_selected_candidate, build_form_draft_from_seed, build_result_preview,
compile_result_draft_from_seed, create_work_profile, infer_anchor_pack, normalize_puzzle_draft,
normalize_puzzle_levels, normalize_theme_tags, publish_work_profile, replace_puzzle_level,
select_next_profiles, selected_profile_level_after_runtime_level, selected_puzzle_level,
tag_similarity_score,
};
use module_runtime::RuntimeProfileWalletLedgerSourceType;
use serde_json::from_str as json_from_str;
use serde_json::json;
use serde_json::to_string as json_to_string;
use spacetimedb::{ProcedureContext, Table, Timestamp, TxContext};
const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0;
/// 拼图 Agent session 真相表。
/// 当前只保存结构化字段与 JSON 草稿,不提前拆出更多编辑态子表。
#[spacetimedb::table(
@@ -98,6 +102,10 @@ pub struct PuzzleWorkProfileRow {
remix_count: u32,
#[default(0)]
like_count: u32,
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
point_incentive_total_half_points: u64,
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
point_incentive_claimed_points: u64,
}
/// 运行态 run 快照表。
@@ -595,6 +603,25 @@ pub fn use_puzzle_runtime_prop(
}
}
#[spacetimedb::procedure]
pub fn claim_puzzle_work_point_incentive(
ctx: &mut ProcedureContext,
input: PuzzleWorkPointIncentiveClaimInput,
) -> PuzzleWorkProcedureResult {
match ctx.try_with_tx(|tx| claim_puzzle_work_point_incentive_tx(tx, input.clone())) {
Ok(item) => PuzzleWorkProcedureResult {
ok: true,
item_json: Some(serialize_json(&item)),
error_message: None,
},
Err(message) => PuzzleWorkProcedureResult {
ok: false,
item_json: None,
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn submit_puzzle_leaderboard_entry(
ctx: &mut ProcedureContext,
@@ -1186,6 +1213,8 @@ fn update_puzzle_work_tx(
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
point_incentive_total_half_points: row.point_incentive_total_half_points,
point_incentive_claimed_points: row.point_incentive_claimed_points,
anchor_pack_json: row.anchor_pack_json.clone(),
publish_ready: build_result_preview(&preview_draft, Some(&row.author_display_name))
.publish_ready,
@@ -1341,6 +1370,8 @@ fn record_puzzle_work_like_tx(
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count.saturating_add(1),
point_incentive_total_half_points: row.point_incentive_total_half_points,
point_incentive_claimed_points: row.point_incentive_claimed_points,
anchor_pack_json: row.anchor_pack_json.clone(),
publish_ready: row.publish_ready,
created_at: row.created_at,
@@ -1427,6 +1458,8 @@ fn remix_puzzle_work_tx(
play_count: source.play_count,
remix_count: source.remix_count.saturating_add(1),
like_count: source.like_count,
point_incentive_total_half_points: source.point_incentive_total_half_points,
point_incentive_claimed_points: source.point_incentive_claimed_points,
anchor_pack_json: source.anchor_pack_json.clone(),
publish_ready: source.publish_ready,
created_at: source.created_at,
@@ -1492,6 +1525,8 @@ fn remix_puzzle_work_tx(
play_count: 0,
remix_count: 0,
like_count: 0,
point_incentive_total_half_points: 0,
point_incentive_claimed_points: 0,
anchor_pack_json: serialize_json(&source_profile.anchor_pack),
publish_ready: true,
created_at: remixed_at,
@@ -1531,13 +1566,20 @@ fn start_puzzle_run_tx(
return Err("入口拼图作品未发布".to_string());
}
let mut entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
let mut cleared_level_count = 0;
if let Some(level) = selected_profile_level(&entry_profile, input.level_id.as_deref())? {
cleared_level_count =
module_puzzle::resolve_restart_cleared_level_count(&entry_profile, &level.level_id);
entry_profile = profile_for_single_level(&entry_profile, &level);
}
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,
cleared_level_count,
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(
@@ -1682,26 +1724,40 @@ fn advance_puzzle_next_level_tx(
.find(&current_level.profile_id)
.ok_or_else(|| "当前拼图作品不存在".to_string())?;
let current_profile = build_puzzle_work_profile_from_row(&current_profile_row)?;
let next_profile = selected_profile_level_after_runtime_level(&current_profile, current_level)
.map(|level| profile_for_single_level(&current_profile, &level))
.or_else(|| {
let candidates = list_published_puzzle_profiles(ctx).ok()?;
select_next_profiles(
&current_profile,
&current_run.played_profile_ids,
&candidates,
1,
)
.into_iter()
.next()
.cloned()
})
let same_work_next_profile =
selected_profile_level_after_runtime_level(&current_profile, current_level)
.map(|level| profile_for_single_level(&current_profile, &level));
let similar_work_next_profile = if same_work_next_profile.is_none() {
let candidates = list_published_puzzle_profiles(ctx)?;
select_next_profiles(
&current_profile,
&current_run.played_profile_ids,
&candidates,
1,
)
.into_iter()
.next()
.cloned()
} else {
None
};
let next_profile = same_work_next_profile
.as_ref()
.or(similar_work_next_profile.as_ref())
.ok_or_else(|| "没有可用的下一关候选".to_string())?;
let mut next_run = module_puzzle::advance_next_level_at(
&current_run,
&next_profile,
micros_to_millis(input.advanced_at_micros),
)
let mut next_run = if same_work_next_profile.is_some() {
module_puzzle::advance_next_level_at(
&current_run,
next_profile,
micros_to_millis(input.advanced_at_micros),
)
} else {
module_puzzle::advance_to_new_work_first_level_at(
&current_run,
next_profile,
micros_to_millis(input.advanced_at_micros),
)
}
.map_err(|error| error.to_string())?;
let next_grid_size = next_run.current_grid_size;
let next_profile_id = next_profile.profile_id.clone();
@@ -1805,6 +1861,19 @@ fn use_puzzle_runtime_prop_tx(
};
let mut hydrated_run = next_run;
refresh_next_level_handoff(ctx, &mut hydrated_run)?;
if let Some(profile_id) = hydrated_run
.current_level
.as_ref()
.map(|level| level.profile_id.clone())
{
accrue_puzzle_point_incentive(
ctx,
&profile_id,
&input.owner_user_id,
input.spent_points,
input.used_at_micros,
)?;
}
replace_puzzle_runtime_run(ctx, &row, &hydrated_run, input.used_at_micros);
if let Some((profile_id, grid_size)) = hydrated_run
.current_level
@@ -1822,6 +1891,86 @@ fn use_puzzle_runtime_prop_tx(
Ok(hydrated_run)
}
fn claim_puzzle_work_point_incentive_tx(
ctx: &TxContext,
input: PuzzleWorkPointIncentiveClaimInput,
) -> Result<PuzzleWorkProfile, String> {
let profile_id = input.profile_id.trim();
let owner_user_id = input.owner_user_id.trim();
if profile_id.is_empty() || owner_user_id.is_empty() {
return Err("拼图积分激励参数不能为空".to_string());
}
let row = ctx
.db
.puzzle_work_profile()
.profile_id()
.find(&profile_id.to_string())
.ok_or_else(|| "拼图作品不存在".to_string())?;
if row.owner_user_id != owner_user_id {
return Err("无权领取该作品的积分激励".to_string());
}
let claimable_points = puzzle_point_incentive_claimable_points(
row.point_incentive_total_half_points,
row.point_incentive_claimed_points,
);
if claimable_points == 0 {
return Err("暂无可领取积分激励".to_string());
}
let claimed_at = Timestamp::from_micros_since_unix_epoch(input.claimed_at_micros);
let next_row = PuzzleWorkProfileRow {
profile_id: row.profile_id.clone(),
work_id: row.work_id.clone(),
owner_user_id: row.owner_user_id.clone(),
source_session_id: row.source_session_id.clone(),
author_display_name: row.author_display_name.clone(),
work_title: row.work_title.clone(),
work_description: row.work_description.clone(),
level_name: row.level_name.clone(),
summary: row.summary.clone(),
theme_tags_json: row.theme_tags_json.clone(),
cover_image_src: row.cover_image_src.clone(),
cover_asset_id: row.cover_asset_id.clone(),
levels_json: row.levels_json.clone(),
publication_status: row.publication_status,
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
point_incentive_total_half_points: row.point_incentive_total_half_points,
point_incentive_claimed_points: row
.point_incentive_claimed_points
.saturating_add(claimable_points),
anchor_pack_json: row.anchor_pack_json.clone(),
publish_ready: row.publish_ready,
created_at: row.created_at,
updated_at: claimed_at,
published_at: row.published_at,
};
replace_puzzle_work_profile(ctx, &row, next_row);
grant_profile_wallet_points(
ctx,
owner_user_id,
claimable_points,
RuntimeProfileWalletLedgerSourceType::PuzzleAuthorIncentiveClaim,
&format!(
"puzzle_author_incentive_claim:{}:{}:{}",
profile_id, owner_user_id, input.claimed_at_micros
),
claimed_at,
)?;
let updated = ctx
.db
.puzzle_work_profile()
.profile_id()
.find(&profile_id.to_string())
.ok_or_else(|| "拼图积分激励领取更新失败".to_string())?;
build_puzzle_work_profile_from_row(&updated)
}
fn submit_puzzle_leaderboard_entry_tx(
ctx: &TxContext,
input: PuzzleLeaderboardSubmitInput,
@@ -1835,7 +1984,7 @@ fn submit_puzzle_leaderboard_entry_tx(
if input.profile_id.trim().is_empty() {
return Err("提交成绩的拼图作品不能为空".to_string());
}
if input.grid_size != 3 && input.grid_size != 4 {
if !module_puzzle::is_supported_puzzle_grid_size(input.grid_size) {
return Err("提交成绩的网格规格无效".to_string());
}
let matches_service_level =
@@ -2002,6 +2151,8 @@ fn build_puzzle_work_profile_from_row_without_recent_count(
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
point_incentive_total_half_points: row.point_incentive_total_half_points,
point_incentive_claimed_points: row.point_incentive_claimed_points,
recent_play_count_7d: 0,
publish_ready: row.publish_ready,
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
@@ -2108,6 +2259,8 @@ fn upsert_puzzle_draft_work_profile(
profile.play_count = existing.play_count;
profile.remix_count = existing.remix_count;
profile.like_count = existing.like_count;
profile.point_incentive_total_half_points = existing.point_incentive_total_half_points;
profile.point_incentive_claimed_points = existing.point_incentive_claimed_points;
return upsert_puzzle_work_profile(ctx, profile);
}
let profile = create_work_profile(
@@ -2286,6 +2439,12 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
play_count: existing.play_count.max(profile.play_count),
remix_count: existing.remix_count.max(profile.remix_count),
like_count: existing.like_count.max(profile.like_count),
point_incentive_total_half_points: existing
.point_incentive_total_half_points
.max(profile.point_incentive_total_half_points),
point_incentive_claimed_points: existing
.point_incentive_claimed_points
.max(profile.point_incentive_claimed_points),
anchor_pack_json: serialize_json(&profile.anchor_pack),
publish_ready: profile.publish_ready,
created_at: existing.created_at,
@@ -2316,6 +2475,8 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re
play_count: profile.play_count,
remix_count: profile.remix_count,
like_count: profile.like_count,
point_incentive_total_half_points: profile.point_incentive_total_half_points,
point_incentive_claimed_points: profile.point_incentive_claimed_points,
anchor_pack_json: serialize_json(&profile.anchor_pack),
publish_ready: profile.publish_ready,
created_at: Timestamp::from_micros_since_unix_epoch(profile.updated_at_micros),
@@ -2375,7 +2536,7 @@ fn replace_puzzle_runtime_run(
.unwrap_or_else(|| current.current_profile_id.clone()),
cleared_level_count: run.cleared_level_count,
current_level_index: run.current_level_index,
current_grid_size: resolve_puzzle_grid_size(run.cleared_level_count),
current_grid_size: run.current_grid_size,
played_profile_ids_json: serialize_json(&run.played_profile_ids),
previous_level_tags_json: serialize_json(&run.previous_level_tags),
snapshot_json: serialize_json(run),
@@ -2403,16 +2564,17 @@ fn upsert_puzzle_profile_save_archive(
return Ok(());
};
let world_key = format!("puzzle:{}", run.entry_profile_id);
let target = resolve_puzzle_archive_target(ctx, run, current_level)?;
// 中文注释:拼图存档只保存恢复入口所需的最小运行态索引,棋盘真相继续放在 puzzle_runtime_run。
let game_state_json = json_to_string(&json!({
"runtimeKind": "puzzle",
"runId": run.run_id,
"entryProfileId": run.entry_profile_id,
"currentProfileId": current_level.profile_id,
"currentLevelIndex": current_level.level_index,
"currentLevelId": current_level.level_id,
"status": current_level.status.as_str(),
"currentProfileId": target.profile_id,
"currentLevelIndex": target.level_index,
"currentLevelId": target.level_id,
"status": target.status.as_str(),
}))
.unwrap_or_else(|_| "{}".to_string());
@@ -2421,13 +2583,13 @@ fn upsert_puzzle_profile_save_archive(
ProfileSaveArchiveUpsertInput {
user_id: user_id.to_string(),
world_key,
owner_user_id: resolve_puzzle_current_owner_user_id(ctx, &current_level.profile_id),
owner_user_id: target.owner_user_id,
profile_id: Some(run.entry_profile_id.clone()),
world_type: Some("PUZZLE".to_string()),
world_name: current_level.level_name.clone(),
subtitle: format!("第 {} 关", current_level.level_index),
summary_text: puzzle_archive_summary_text(current_level.status),
cover_image_src: current_level.cover_image_src.clone(),
world_name: target.level_name,
subtitle: format!("第 {} 关", target.level_index),
summary_text: puzzle_archive_summary_text(target.status),
cover_image_src: target.cover_image_src,
bottom_tab: "puzzle".to_string(),
game_state_json,
current_story_json: None,
@@ -2436,6 +2598,88 @@ fn upsert_puzzle_profile_save_archive(
)
}
struct PuzzleArchiveTarget {
profile_id: String,
level_index: u32,
level_id: Option<String>,
level_name: String,
status: PuzzleRuntimeLevelStatus,
cover_image_src: Option<String>,
owner_user_id: Option<String>,
}
fn resolve_puzzle_archive_target(
ctx: &TxContext,
run: &PuzzleRunSnapshot,
current_level: &module_puzzle::PuzzleRuntimeLevelSnapshot,
) -> Result<PuzzleArchiveTarget, String> {
let owner_user_id = resolve_puzzle_current_owner_user_id(ctx, &current_level.profile_id);
if current_level.status != PuzzleRuntimeLevelStatus::Cleared {
return Ok(PuzzleArchiveTarget {
profile_id: current_level.profile_id.clone(),
level_index: current_level.level_index,
level_id: current_level.level_id.clone(),
level_name: current_level.level_name.clone(),
status: current_level.status,
cover_image_src: current_level.cover_image_src.clone(),
owner_user_id,
});
}
let Some(next_level_id) = run
.next_level_id
.as_deref()
.filter(|value| !value.trim().is_empty())
else {
return Ok(PuzzleArchiveTarget {
profile_id: current_level.profile_id.clone(),
level_index: current_level.level_index,
level_id: current_level.level_id.clone(),
level_name: current_level.level_name.clone(),
status: current_level.status,
cover_image_src: current_level.cover_image_src.clone(),
owner_user_id,
});
};
if run.next_level_profile_id.as_deref() != Some(current_level.profile_id.as_str())
|| run.next_level_mode != PUZZLE_NEXT_LEVEL_MODE_SAME_WORK
{
return Ok(PuzzleArchiveTarget {
profile_id: current_level.profile_id.clone(),
level_index: current_level.level_index,
level_id: current_level.level_id.clone(),
level_name: current_level.level_name.clone(),
status: current_level.status,
cover_image_src: current_level.cover_image_src.clone(),
owner_user_id,
});
}
let current_profile = build_puzzle_work_profile_from_row(
&ctx.db
.puzzle_work_profile()
.profile_id()
.find(&current_level.profile_id)
.ok_or_else(|| "当前拼图作品不存在".to_string())?,
)?;
let next_level = current_profile
.levels
.iter()
.find(|level| level.level_id == next_level_id)
.cloned()
.ok_or_else(|| "下一关拼图关卡不存在".to_string())?;
Ok(PuzzleArchiveTarget {
profile_id: current_profile.profile_id,
level_index: current_level.level_index.saturating_add(1),
level_id: Some(next_level.level_id),
level_name: next_level.level_name,
status: PuzzleRuntimeLevelStatus::Playing,
cover_image_src: next_level.cover_image_src,
owner_user_id,
})
}
fn resolve_puzzle_current_owner_user_id(ctx: &TxContext, profile_id: &str) -> Option<String> {
ctx.db
.puzzle_work_profile()
@@ -2453,6 +2697,72 @@ fn puzzle_archive_summary_text(status: PuzzleRuntimeLevelStatus) -> String {
.to_string()
}
fn puzzle_point_incentive_claimable_points(total_half_points: u64, claimed_points: u64) -> u64 {
total_half_points
.saturating_div(2)
.saturating_sub(claimed_points)
}
fn accrue_puzzle_point_incentive(
ctx: &TxContext,
profile_id: &str,
player_user_id: &str,
spent_points: u64,
updated_at_micros: i64,
) -> Result<(), String> {
if spent_points == 0 {
return Ok(());
}
let Some(row) = ctx
.db
.puzzle_work_profile()
.profile_id()
.find(&profile_id.to_string())
else {
return Ok(());
};
if row.publication_status != PuzzlePublicationStatus::Published
|| row.owner_user_id == player_user_id
{
return Ok(());
}
replace_puzzle_work_profile(
ctx,
&row,
PuzzleWorkProfileRow {
profile_id: row.profile_id.clone(),
work_id: row.work_id.clone(),
owner_user_id: row.owner_user_id.clone(),
source_session_id: row.source_session_id.clone(),
author_display_name: row.author_display_name.clone(),
work_title: row.work_title.clone(),
work_description: row.work_description.clone(),
level_name: row.level_name.clone(),
summary: row.summary.clone(),
theme_tags_json: row.theme_tags_json.clone(),
cover_image_src: row.cover_image_src.clone(),
cover_asset_id: row.cover_asset_id.clone(),
levels_json: row.levels_json.clone(),
publication_status: row.publication_status,
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
point_incentive_total_half_points: row
.point_incentive_total_half_points
.saturating_add(spent_points),
point_incentive_claimed_points: row.point_incentive_claimed_points,
anchor_pack_json: row.anchor_pack_json.clone(),
publish_ready: row.publish_ready,
created_at: row.created_at,
updated_at: Timestamp::from_micros_since_unix_epoch(updated_at_micros),
published_at: row.published_at,
},
);
Ok(())
}
fn increment_puzzle_profile_play_count(
ctx: &TxContext,
row: &PuzzleWorkProfileRow,
@@ -2479,6 +2789,8 @@ fn increment_puzzle_profile_play_count(
play_count: row.play_count.saturating_add(1),
remix_count: row.remix_count,
like_count: row.like_count,
point_incentive_total_half_points: row.point_incentive_total_half_points,
point_incentive_claimed_points: row.point_incentive_claimed_points,
anchor_pack_json: row.anchor_pack_json.clone(),
publish_ready: row.publish_ready,
created_at: row.created_at,
@@ -2841,7 +3153,7 @@ mod tests {
}];
replace_generated_candidate(
&mut draft,
&mut draft.candidates,
vec![PuzzleGeneratedImageCandidate {
candidate_id: "session-1-candidate-2".to_string(),
image_src: "/generated-puzzle-assets/session-1/new/cover.png".to_string(),
@@ -2866,11 +3178,14 @@ mod tests {
owner_user_id: "owner-a".to_string(),
source_session_id: None,
author_display_name: "作者".to_string(),
work_title: "A".to_string(),
work_description: String::new(),
level_name: "A".to_string(),
summary: String::new(),
theme_tags: vec!["雨夜".to_string(), "猫咪".to_string()],
cover_image_src: Some("/a.png".to_string()),
cover_asset_id: Some("asset-a".to_string()),
levels: Vec::new(),
publication_status: PuzzlePublicationStatus::Published,
updated_at_micros: 1,
published_at_micros: Some(1),
@@ -2878,6 +3193,8 @@ mod tests {
recent_play_count_7d: 0,
remix_count: 0,
like_count: 0,
point_incentive_total_half_points: 0,
point_incentive_claimed_points: 0,
publish_ready: true,
anchor_pack: empty_anchor_pack(),
};
@@ -2885,10 +3202,13 @@ mod tests {
owner_user_id: "owner-a".to_string(),
profile_id: "profile-b".to_string(),
work_id: "work-b".to_string(),
work_title: "B".to_string(),
work_description: String::new(),
level_name: "B".to_string(),
theme_tags: vec!["雨夜".to_string(), "蒸汽城市".to_string()],
cover_image_src: Some("/b.png".to_string()),
cover_asset_id: Some("asset-b".to_string()),
levels: Vec::new(),
publication_status: PuzzlePublicationStatus::Published,
updated_at_micros: 2,
published_at_micros: Some(2),
@@ -2896,6 +3216,8 @@ mod tests {
recent_play_count_7d: 0,
remix_count: 0,
like_count: 0,
point_incentive_total_half_points: 0,
point_incentive_claimed_points: 0,
publish_ready: true,
anchor_pack: empty_anchor_pack(),
source_session_id: None,

View File

@@ -2120,6 +2120,24 @@ fn apply_profile_wallet_delta(
)
}
pub(crate) fn grant_profile_wallet_points(
ctx: &ReducerContext,
user_id: &str,
amount_delta: u64,
source_type: RuntimeProfileWalletLedgerSourceType,
ledger_id: &str,
created_at: Timestamp,
) -> Result<u64, String> {
apply_profile_wallet_delta(
ctx,
user_id,
amount_delta,
source_type,
ledger_id,
created_at,
)
}
fn apply_profile_wallet_adjustment(
ctx: &ReducerContext,
input: RuntimeProfileWalletAdjustmentInput,