This commit is contained in:
2026-04-22 23:44:57 +08:00
parent 76ac9d22a5
commit 84dc92646a
484 changed files with 9598 additions and 9135 deletions

View File

@@ -23,6 +23,8 @@ pub struct CustomWorldProfile {
landmark_count: u32,
author_display_name: String,
published_at: Option<Timestamp>,
// 软删除后保留 profile 真相,供审计与幂等删除使用。
deleted_at: Option<Timestamp>,
created_at: Timestamp,
updated_at: Timestamp,
}
@@ -701,6 +703,34 @@ pub fn unpublish_custom_world_profile_and_return(
}
}
// 删除入口继续走 owner-only 软删除,不直接物理删除 profile 真相。
#[spacetimedb::procedure]
pub fn delete_custom_world_profile_and_return(
ctx: &mut ProcedureContext,
input: CustomWorldProfileDeleteInput,
) -> CustomWorldProfileListResult {
match ctx.try_with_tx(|tx| {
delete_custom_world_profile_record(tx, input.clone())?;
list_custom_world_profile_snapshots(
tx,
CustomWorldProfileListInput {
owner_user_id: input.owner_user_id.clone(),
},
)
}) {
Ok(entries) => CustomWorldProfileListResult {
ok: true,
entries,
error_message: None,
},
Err(message) => CustomWorldProfileListResult {
ok: false,
entries: Vec::new(),
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn list_custom_world_profiles(
ctx: &mut ProcedureContext,
@@ -986,6 +1016,7 @@ fn upsert_custom_world_profile_record(
landmark_count: input.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: existing.published_at,
deleted_at: None,
created_at: existing.created_at,
updated_at,
}
@@ -1005,6 +1036,7 @@ fn upsert_custom_world_profile_record(
landmark_count: input.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: None,
deleted_at: None,
created_at: updated_at,
updated_at,
},
@@ -1139,6 +1171,7 @@ fn publish_custom_world_profile_record(
landmark_count: existing.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: Some(published_at),
deleted_at: None,
created_at: existing.created_at,
updated_at: published_at,
};
@@ -1199,6 +1232,7 @@ fn unpublish_custom_world_profile_record(
landmark_count: existing.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: None,
deleted_at: None,
created_at: existing.created_at,
updated_at,
};
@@ -1208,6 +1242,62 @@ fn unpublish_custom_world_profile_record(
Ok((build_custom_world_profile_snapshot(&inserted), None))
}
fn delete_custom_world_profile_record(
ctx: &ReducerContext,
input: CustomWorldProfileDeleteInput,
) -> Result<(), String> {
validate_custom_world_profile_delete_input(&input).map_err(|error| error.to_string())?;
let Some(existing) = ctx
.db
.custom_world_profile()
.profile_id()
.find(&input.profile_id)
.filter(|row| row.owner_user_id == input.owner_user_id)
else {
return Ok(());
};
if existing.deleted_at.is_some() {
return Ok(());
}
let deleted_at = Timestamp::from_micros_since_unix_epoch(input.deleted_at_micros);
ctx.db
.custom_world_profile()
.profile_id()
.delete(&existing.profile_id);
ctx.db
.custom_world_gallery_entry()
.profile_id()
.delete(&existing.profile_id);
let next_row = CustomWorldProfile {
profile_id: existing.profile_id.clone(),
owner_user_id: existing.owner_user_id.clone(),
source_agent_session_id: existing.source_agent_session_id.clone(),
publication_status: CustomWorldPublicationStatus::Draft,
world_name: existing.world_name.clone(),
subtitle: existing.subtitle.clone(),
summary_text: existing.summary_text.clone(),
theme_mode: existing.theme_mode,
cover_image_src: existing.cover_image_src.clone(),
profile_payload_json: existing.profile_payload_json.clone(),
playable_npc_count: existing.playable_npc_count,
landmark_count: existing.landmark_count,
author_display_name: existing.author_display_name.clone(),
published_at: None,
deleted_at: Some(deleted_at),
created_at: existing.created_at,
updated_at: deleted_at,
};
let _ = ctx.db.custom_world_profile().insert(next_row);
Ok(())
}
fn list_custom_world_profile_snapshots(
ctx: &ReducerContext,
input: CustomWorldProfileListInput,
@@ -1218,7 +1308,7 @@ fn list_custom_world_profile_snapshots(
.db
.custom_world_profile()
.iter()
.filter(|row| row.owner_user_id == input.owner_user_id)
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
.map(|row| build_custom_world_profile_snapshot(&row))
.collect::<Vec<_>>();
@@ -1264,7 +1354,7 @@ fn get_custom_world_library_detail_record(
.custom_world_profile()
.profile_id()
.find(&input.profile_id)
.filter(|row| row.owner_user_id == input.owner_user_id);
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none());
let gallery_entry = profile
.as_ref()
@@ -1305,6 +1395,7 @@ fn get_custom_world_gallery_detail_record(
.filter(|row| {
row.owner_user_id == input.owner_user_id
&& row.publication_status == CustomWorldPublicationStatus::Published
&& row.deleted_at.is_none()
});
let gallery_entry = ctx
@@ -1377,7 +1468,7 @@ fn list_custom_world_work_snapshots(
.db
.custom_world_profile()
.iter()
.filter(|row| row.owner_user_id == input.owner_user_id)
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
{
items.push(CustomWorldWorkSummarySnapshot {
work_id: format!("published:{}", profile.profile_id),
@@ -3107,6 +3198,9 @@ fn build_custom_world_profile_snapshot(row: &CustomWorldProfile) -> CustomWorldP
published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
deleted_at_micros: row
.deleted_at
.map(|value| value.to_micros_since_unix_epoch()),
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}

View File

@@ -65,10 +65,10 @@ use module_custom_world::{
validate_custom_world_agent_operation_get_input,
validate_custom_world_agent_session_create_input,
validate_custom_world_agent_session_get_input, validate_custom_world_gallery_detail_input,
validate_custom_world_library_detail_input, validate_custom_world_profile_list_input,
validate_custom_world_profile_publish_input, validate_custom_world_profile_unpublish_input,
validate_custom_world_profile_upsert_input, validate_custom_world_publish_world_input,
validate_custom_world_works_list_input,
validate_custom_world_library_detail_input, validate_custom_world_profile_delete_input,
validate_custom_world_profile_list_input, validate_custom_world_profile_publish_input,
validate_custom_world_profile_unpublish_input, validate_custom_world_profile_upsert_input,
validate_custom_world_publish_world_input, validate_custom_world_works_list_input,
};
use module_inventory::{
GrantInventoryItemInput, INVENTORY_MUTATION_ID_PREFIX, INVENTORY_SLOT_ID_PREFIX,
@@ -799,6 +799,8 @@ pub struct CustomWorldProfile {
landmark_count: u32,
author_display_name: String,
published_at: Option<Timestamp>,
// 软删除后保留 profile 真相,供审计与幂等删除使用。
deleted_at: Option<Timestamp>,
created_at: Timestamp,
updated_at: Timestamp,
}
@@ -3353,6 +3355,34 @@ pub fn unpublish_custom_world_profile_and_return(
}
}
// 删除入口继续走 owner-only 软删除,不直接物理删除 profile 真相。
#[spacetimedb::procedure]
pub fn delete_custom_world_profile_and_return(
ctx: &mut ProcedureContext,
input: module_custom_world::CustomWorldProfileDeleteInput,
) -> CustomWorldProfileListResult {
match ctx.try_with_tx(|tx| {
delete_custom_world_profile_record(tx, input.clone())?;
list_custom_world_profile_snapshots(
tx,
CustomWorldProfileListInput {
owner_user_id: input.owner_user_id.clone(),
},
)
}) {
Ok(entries) => CustomWorldProfileListResult {
ok: true,
entries,
error_message: None,
},
Err(message) => CustomWorldProfileListResult {
ok: false,
entries: Vec::new(),
error_message: Some(message),
},
}
}
#[spacetimedb::procedure]
pub fn list_custom_world_profiles(
ctx: &mut ProcedureContext,
@@ -3894,6 +3924,7 @@ fn upsert_custom_world_profile_record(
landmark_count: input.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: existing.published_at,
deleted_at: None,
created_at: existing.created_at,
updated_at,
}
@@ -3913,6 +3944,7 @@ fn upsert_custom_world_profile_record(
landmark_count: input.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: None,
deleted_at: None,
created_at: updated_at,
updated_at,
},
@@ -4047,6 +4079,7 @@ fn publish_custom_world_profile_record(
landmark_count: existing.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: Some(published_at),
deleted_at: None,
created_at: existing.created_at,
updated_at: published_at,
};
@@ -4107,6 +4140,7 @@ fn unpublish_custom_world_profile_record(
landmark_count: existing.landmark_count,
author_display_name: input.author_display_name.clone(),
published_at: None,
deleted_at: None,
created_at: existing.created_at,
updated_at,
};
@@ -4116,6 +4150,62 @@ fn unpublish_custom_world_profile_record(
Ok((build_custom_world_profile_snapshot(&inserted), None))
}
fn delete_custom_world_profile_record(
ctx: &ReducerContext,
input: module_custom_world::CustomWorldProfileDeleteInput,
) -> Result<(), String> {
validate_custom_world_profile_delete_input(&input).map_err(|error| error.to_string())?;
let Some(existing) = ctx
.db
.custom_world_profile()
.profile_id()
.find(&input.profile_id)
.filter(|row| row.owner_user_id == input.owner_user_id)
else {
return Ok(());
};
if existing.deleted_at.is_some() {
return Ok(());
}
let deleted_at = Timestamp::from_micros_since_unix_epoch(input.deleted_at_micros);
ctx.db
.custom_world_profile()
.profile_id()
.delete(&existing.profile_id);
ctx.db
.custom_world_gallery_entry()
.profile_id()
.delete(&existing.profile_id);
let next_row = CustomWorldProfile {
profile_id: existing.profile_id.clone(),
owner_user_id: existing.owner_user_id.clone(),
source_agent_session_id: existing.source_agent_session_id.clone(),
publication_status: CustomWorldPublicationStatus::Draft,
world_name: existing.world_name.clone(),
subtitle: existing.subtitle.clone(),
summary_text: existing.summary_text.clone(),
theme_mode: existing.theme_mode,
cover_image_src: existing.cover_image_src.clone(),
profile_payload_json: existing.profile_payload_json.clone(),
playable_npc_count: existing.playable_npc_count,
landmark_count: existing.landmark_count,
author_display_name: existing.author_display_name.clone(),
published_at: None,
deleted_at: Some(deleted_at),
created_at: existing.created_at,
updated_at: deleted_at,
};
let _ = ctx.db.custom_world_profile().insert(next_row);
Ok(())
}
fn list_custom_world_profile_snapshots(
ctx: &ReducerContext,
input: CustomWorldProfileListInput,
@@ -4126,7 +4216,7 @@ fn list_custom_world_profile_snapshots(
.db
.custom_world_profile()
.iter()
.filter(|row| row.owner_user_id == input.owner_user_id)
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
.map(|row| build_custom_world_profile_snapshot(&row))
.collect::<Vec<_>>();
@@ -4172,7 +4262,7 @@ fn get_custom_world_library_detail_record(
.custom_world_profile()
.profile_id()
.find(&input.profile_id)
.filter(|row| row.owner_user_id == input.owner_user_id);
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none());
let gallery_entry = profile
.as_ref()
@@ -4213,6 +4303,7 @@ fn get_custom_world_gallery_detail_record(
.filter(|row| {
row.owner_user_id == input.owner_user_id
&& row.publication_status == CustomWorldPublicationStatus::Published
&& row.deleted_at.is_none()
});
let gallery_entry = ctx
@@ -4283,7 +4374,7 @@ fn list_custom_world_work_snapshots(
.db
.custom_world_profile()
.iter()
.filter(|row| row.owner_user_id == input.owner_user_id)
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
{
items.push(CustomWorldWorkSummarySnapshot {
work_id: format!("published:{}", profile.profile_id),
@@ -6104,6 +6195,9 @@ fn build_custom_world_profile_snapshot(row: &CustomWorldProfile) -> CustomWorldP
published_at_micros: row
.published_at
.map(|value| value.to_micros_since_unix_epoch()),
deleted_at_micros: row
.deleted_at
.map(|value| value.to_micros_since_unix_epoch()),
created_at_micros: row.created_at.to_micros_since_unix_epoch(),
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
}