1
This commit is contained in:
@@ -56,6 +56,7 @@ pub struct UserAccount {
|
||||
pub(crate) public_user_code: String,
|
||||
pub(crate) username: String,
|
||||
pub(crate) display_name: String,
|
||||
pub(crate) avatar_url: Option<String>,
|
||||
pub(crate) phone_number_masked: Option<String>,
|
||||
pub(crate) phone_number_e164: Option<String>,
|
||||
pub(crate) login_method: String,
|
||||
@@ -256,6 +257,7 @@ fn import_auth_store_snapshot_tx(
|
||||
public_user_code: user.public_user_code,
|
||||
username: user.username,
|
||||
display_name: user.display_name,
|
||||
avatar_url: user.avatar_url,
|
||||
phone_number_masked: user.phone_number_masked,
|
||||
phone_number_e164: stored_user.phone_number.clone(),
|
||||
login_method: user.login_method,
|
||||
@@ -387,6 +389,7 @@ fn export_auth_store_snapshot_from_tables_tx(
|
||||
public_user_code: user.public_user_code,
|
||||
username: user.username.clone(),
|
||||
display_name: user.display_name,
|
||||
avatar_url: user.avatar_url,
|
||||
phone_number_masked: user.phone_number_masked,
|
||||
login_method: user.login_method,
|
||||
binding_status: user.binding_status,
|
||||
@@ -519,6 +522,8 @@ struct AuthUserSnapshot {
|
||||
public_user_code: String,
|
||||
username: String,
|
||||
display_name: String,
|
||||
#[serde(default)]
|
||||
avatar_url: Option<String>,
|
||||
phone_number_masked: Option<String>,
|
||||
login_method: String,
|
||||
binding_status: String,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::big_fish::tables::{big_fish_agent_message, big_fish_creation_session};
|
||||
use crate::runtime::{
|
||||
ProfilePlayedWorkUpsertInput, add_profile_observed_play_time, upsert_profile_played_work,
|
||||
ProfilePlayedWorkUpsertInput, PublicWorkPlayRecordInput, add_profile_observed_play_time,
|
||||
count_recent_public_work_plays, record_public_work_play, upsert_profile_played_work,
|
||||
};
|
||||
use crate::*;
|
||||
|
||||
@@ -288,6 +289,7 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
input: BigFishWorksListInput,
|
||||
) -> Result<Vec<BigFishWorkSummarySnapshot>, String> {
|
||||
validate_works_list_input(&input).map_err(|error| error.to_string())?;
|
||||
let now_micros = ctx.timestamp.to_micros_since_unix_epoch();
|
||||
|
||||
let mut items = ctx
|
||||
.db
|
||||
@@ -300,7 +302,7 @@ pub(crate) fn list_big_fish_works_tx(
|
||||
|
||||
row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row)
|
||||
})
|
||||
.map(|row| build_big_fish_work_summary(ctx, &row))
|
||||
.map(|row| build_big_fish_work_summary(ctx, &row, now_micros))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
items.sort_by(|left, right| {
|
||||
@@ -676,6 +678,15 @@ pub(crate) fn record_big_fish_play_tx(
|
||||
input.elapsed_ms,
|
||||
input.played_at_micros,
|
||||
)?;
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
PublicWorkPlayRecordInput {
|
||||
source_type: "big-fish".to_string(),
|
||||
owner_user_id: session.owner_user_id.clone(),
|
||||
profile_id: session.session_id.clone(),
|
||||
played_at_micros: input.played_at_micros,
|
||||
},
|
||||
)?;
|
||||
let next_session = BigFishCreationSession {
|
||||
session_id: session.session_id.clone(),
|
||||
owner_user_id: session.owner_user_id.clone(),
|
||||
@@ -698,13 +709,7 @@ pub(crate) fn record_big_fish_play_tx(
|
||||
};
|
||||
replace_big_fish_session(ctx, &session, next_session);
|
||||
|
||||
list_big_fish_works_tx(
|
||||
ctx,
|
||||
BigFishWorksListInput {
|
||||
owner_user_id: String::new(),
|
||||
published_only: true,
|
||||
},
|
||||
)
|
||||
list_big_fish_works_tx(ctx, build_public_big_fish_gallery_list_input())
|
||||
}
|
||||
|
||||
fn remix_big_fish_work_tx(
|
||||
@@ -876,6 +881,7 @@ pub(crate) fn build_big_fish_session_snapshot(
|
||||
pub(crate) fn build_big_fish_work_summary(
|
||||
ctx: &ReducerContext,
|
||||
row: &BigFishCreationSession,
|
||||
now_micros: i64,
|
||||
) -> Result<BigFishWorkSummarySnapshot, String> {
|
||||
let draft = row
|
||||
.draft_json
|
||||
@@ -940,6 +946,12 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: count_recent_public_work_plays(
|
||||
ctx,
|
||||
"big-fish",
|
||||
&row.session_id,
|
||||
now_micros,
|
||||
),
|
||||
published_at_micros: row
|
||||
.published_at
|
||||
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
|
||||
@@ -947,6 +959,14 @@ pub(crate) fn build_big_fish_work_summary(
|
||||
})
|
||||
}
|
||||
|
||||
fn build_public_big_fish_gallery_list_input() -> BigFishWorksListInput {
|
||||
BigFishWorksListInput {
|
||||
// 中文注释:published_only 分支不会按 owner 过滤;非空占位用于兼容旧部署模块的前置校验。
|
||||
owner_user_id: PUBLIC_BIG_FISH_GALLERY_OWNER_USER_ID.to_string(),
|
||||
published_only: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn replace_big_fish_session(
|
||||
ctx: &ReducerContext,
|
||||
current: &BigFishCreationSession,
|
||||
|
||||
@@ -17,10 +17,40 @@ pub struct BigFishCreationSession {
|
||||
pub(crate) asset_coverage_json: String,
|
||||
pub(crate) last_assistant_reply: Option<String>,
|
||||
pub(crate) publish_ready: bool,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
#[default(0)]
|
||||
pub(crate) play_count: u32,
|
||||
#[default(0)]
|
||||
pub(crate) remix_count: u32,
|
||||
#[default(0)]
|
||||
pub(crate) like_count: u32,
|
||||
#[default(None::<Timestamp>)]
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, SpacetimeType)]
|
||||
pub enum BigFishRuntimeSnapshot {
|
||||
Running,
|
||||
Won,
|
||||
Failed,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = big_fish_runtime_run,
|
||||
index(accessor = by_big_fish_run_session_id, btree(columns = [session_id])),
|
||||
index(accessor = by_big_fish_run_owner_user_id, btree(columns = [owner_user_id]))
|
||||
)]
|
||||
pub struct BigFishRuntimeRun {
|
||||
#[primary_key]
|
||||
pub(crate) run_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) status: BigFishRuntimeSnapshot,
|
||||
pub(crate) snapshot_json: String,
|
||||
pub(crate) last_input_x: f32,
|
||||
pub(crate) last_input_y: f32,
|
||||
pub(crate) tick: u64,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
)?;
|
||||
|
||||
@@ -325,7 +325,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,
|
||||
@@ -337,16 +337,19 @@ pub struct CustomWorldProfile {
|
||||
profile_payload_json: String,
|
||||
playable_npc_count: u32,
|
||||
landmark_count: u32,
|
||||
// 公开消费计数随 profile 真相持久化,发布、编辑和取消发布都不能重置。
|
||||
play_count: u32,
|
||||
remix_count: u32,
|
||||
like_count: u32,
|
||||
author_display_name: String,
|
||||
published_at: Option<Timestamp>,
|
||||
// 软删除后保留 profile 真相,供审计与幂等删除使用。
|
||||
deleted_at: Option<Timestamp>,
|
||||
created_at: Timestamp,
|
||||
updated_at: Timestamp,
|
||||
// 公开消费计数随 profile 真相持久化,发布、编辑和取消发布都不能重置。
|
||||
#[default(0)]
|
||||
play_count: u32,
|
||||
#[default(0)]
|
||||
remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
@@ -488,12 +491,15 @@ pub struct CustomWorldGalleryEntry {
|
||||
theme_mode: CustomWorldThemeMode,
|
||||
playable_npc_count: u32,
|
||||
landmark_count: u32,
|
||||
// 画廊读模型直接同步互动计数,避免前端临时把评分或游玩数改名成点赞。
|
||||
play_count: u32,
|
||||
remix_count: u32,
|
||||
like_count: u32,
|
||||
published_at: Timestamp,
|
||||
updated_at: Timestamp,
|
||||
// 画廊读模型直接同步互动计数,避免前端临时把评分或游玩数改名成点赞。
|
||||
#[default(0)]
|
||||
play_count: u32,
|
||||
#[default(0)]
|
||||
remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
}
|
||||
|
||||
// 成长状态默认按 user_id 单行持久化;若尚未存在记录则返回 Lv.1 / 0 XP 的兼容初始值。
|
||||
@@ -2839,9 +2845,10 @@ fn list_custom_world_profile_snapshots(
|
||||
let mut entries = ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.iter()
|
||||
.filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none())
|
||||
.map(|row| build_custom_world_profile_snapshot(&row))
|
||||
.by_custom_world_profile_owner_user_id()
|
||||
.filter(&input.owner_user_id)
|
||||
.filter(|row| row.deleted_at.is_none())
|
||||
.map(|row| build_custom_world_profile_list_snapshot(&row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
entries.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||
@@ -2849,6 +2856,86 @@ fn list_custom_world_profile_snapshots(
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn build_custom_world_profile_list_snapshot(row: &CustomWorldProfile) -> CustomWorldProfileSnapshot {
|
||||
let mut snapshot = build_custom_world_profile_snapshot(row);
|
||||
snapshot.profile_payload_json = build_custom_world_profile_list_payload_json(row);
|
||||
snapshot
|
||||
}
|
||||
|
||||
fn build_custom_world_profile_list_payload_json(row: &CustomWorldProfile) -> String {
|
||||
let source_profile = serde_json::from_str::<JsonValue>(&row.profile_payload_json).ok();
|
||||
let source_object = source_profile.as_ref().and_then(JsonValue::as_object);
|
||||
let empty_roles = JsonValue::Array(Vec::new());
|
||||
let empty_landmarks = JsonValue::Array(Vec::new());
|
||||
|
||||
// 中文注释:首屏作品列表只需要卡片摘要,不能继续把完整 profile 大 JSON 随列表搬回 Axum。
|
||||
let payload = json!({
|
||||
"id": row.profile_id,
|
||||
"name": row.world_name,
|
||||
"subtitle": row.subtitle,
|
||||
"summary": row.summary_text,
|
||||
"tone": source_object
|
||||
.and_then(|object| object.get("tone"))
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or_default(),
|
||||
"playerGoal": source_object
|
||||
.and_then(|object| object.get("playerGoal"))
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or_default(),
|
||||
"settingText": source_object
|
||||
.and_then(|object| object.get("settingText"))
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or_default(),
|
||||
"themeMode": row.theme_mode.as_str(),
|
||||
"templateWorldType": source_object
|
||||
.and_then(|object| object.get("templateWorldType"))
|
||||
.and_then(JsonValue::as_str)
|
||||
.unwrap_or("WUXIA"),
|
||||
"compatibilityTemplateWorldType": source_object
|
||||
.and_then(|object| object.get("compatibilityTemplateWorldType"))
|
||||
.cloned()
|
||||
.unwrap_or(JsonValue::Null),
|
||||
"cover": row.cover_image_src.as_ref().map(|image_src| json!({
|
||||
"sourceType": "generated",
|
||||
"imageSrc": image_src,
|
||||
})),
|
||||
"majorFactions": source_object
|
||||
.and_then(|object| object.get("majorFactions"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| JsonValue::Array(Vec::new())),
|
||||
"coreConflicts": source_object
|
||||
.and_then(|object| object.get("coreConflicts"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| JsonValue::Array(Vec::new())),
|
||||
"playableNpcs": source_object
|
||||
.and_then(|object| object.get("playableNpcs"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| empty_roles.clone()),
|
||||
"storyNpcs": source_object
|
||||
.and_then(|object| object.get("storyNpcs"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| JsonValue::Array(Vec::new())),
|
||||
"items": source_object
|
||||
.and_then(|object| object.get("items"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| JsonValue::Array(Vec::new())),
|
||||
"camp": source_object
|
||||
.and_then(|object| object.get("camp"))
|
||||
.cloned()
|
||||
.unwrap_or(JsonValue::Null),
|
||||
"landmarks": source_object
|
||||
.and_then(|object| object.get("landmarks"))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| empty_landmarks.clone()),
|
||||
"ownedSettingLayers": source_object
|
||||
.and_then(|object| object.get("ownedSettingLayers"))
|
||||
.cloned()
|
||||
.unwrap_or(JsonValue::Null),
|
||||
});
|
||||
|
||||
serde_json::to_string(&payload).unwrap_or_else(|_| "{}".to_string())
|
||||
}
|
||||
|
||||
fn list_custom_world_gallery_snapshots(
|
||||
ctx: &ReducerContext,
|
||||
) -> Result<Vec<CustomWorldGalleryEntrySnapshot>, String> {
|
||||
@@ -2858,7 +2945,7 @@ fn list_custom_world_gallery_snapshots(
|
||||
.db
|
||||
.custom_world_gallery_entry()
|
||||
.iter()
|
||||
.map(|row| build_custom_world_gallery_entry_snapshot(&row))
|
||||
.map(|row| build_custom_world_gallery_entry_snapshot(ctx, &row))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
entries.sort_by(|left, right| {
|
||||
@@ -2905,7 +2992,7 @@ fn get_custom_world_library_detail_record(
|
||||
profile.as_ref().map(build_custom_world_profile_snapshot),
|
||||
gallery_entry
|
||||
.as_ref()
|
||||
.map(build_custom_world_gallery_entry_snapshot),
|
||||
.map(|row| build_custom_world_gallery_entry_snapshot(ctx, row)),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2943,7 +3030,7 @@ fn get_custom_world_gallery_detail_record(
|
||||
profile.as_ref().map(build_custom_world_profile_snapshot),
|
||||
gallery_entry
|
||||
.as_ref()
|
||||
.map(build_custom_world_gallery_entry_snapshot),
|
||||
.map(|row| build_custom_world_gallery_entry_snapshot(ctx, row)),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -2985,7 +3072,7 @@ fn get_custom_world_gallery_detail_record_by_code(
|
||||
profile.as_ref().map(build_custom_world_profile_snapshot),
|
||||
gallery_entry
|
||||
.as_ref()
|
||||
.map(build_custom_world_gallery_entry_snapshot),
|
||||
.map(|row| build_custom_world_gallery_entry_snapshot(ctx, row)),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -3123,6 +3210,15 @@ fn record_custom_world_profile_play_record(
|
||||
})
|
||||
.ok_or_else(|| "custom_world 已发布作品不存在,无法记录游玩".to_string())?;
|
||||
let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
crate::runtime::PublicWorkPlayRecordInput {
|
||||
source_type: "custom-world".to_string(),
|
||||
owner_user_id: existing.owner_user_id.clone(),
|
||||
profile_id: existing.profile_id.clone(),
|
||||
played_at_micros: input.played_at_micros,
|
||||
},
|
||||
)?;
|
||||
// 游玩计数是公开广场消费数据,只增加计数并保持作品内容不变。
|
||||
let next_row = CustomWorldProfile {
|
||||
profile_id: existing.profile_id.clone(),
|
||||
@@ -3790,7 +3886,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 {
|
||||
@@ -5299,7 +5395,7 @@ fn sync_custom_world_gallery_entry_from_profile(
|
||||
|
||||
let inserted = ctx.db.custom_world_gallery_entry().insert(row);
|
||||
|
||||
Ok(build_custom_world_gallery_entry_snapshot(&inserted))
|
||||
Ok(build_custom_world_gallery_entry_snapshot(ctx, &inserted))
|
||||
}
|
||||
|
||||
fn sync_missing_custom_world_gallery_entries(ctx: &ReducerContext) -> Result<(), String> {
|
||||
@@ -5570,6 +5666,7 @@ fn build_custom_world_draft_card_snapshot(
|
||||
}
|
||||
|
||||
fn build_custom_world_gallery_entry_snapshot(
|
||||
ctx: &ReducerContext,
|
||||
row: &CustomWorldGalleryEntry,
|
||||
) -> CustomWorldGalleryEntrySnapshot {
|
||||
CustomWorldGalleryEntrySnapshot {
|
||||
@@ -5588,6 +5685,12 @@ fn build_custom_world_gallery_entry_snapshot(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: count_recent_public_work_plays(
|
||||
ctx,
|
||||
"custom-world",
|
||||
&row.profile_id,
|
||||
ctx.timestamp.to_micros_since_unix_epoch(),
|
||||
),
|
||||
published_at_micros: row.published_at.to_micros_since_unix_epoch(),
|
||||
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ macro_rules! migration_tables {
|
||||
profile_invite_code,
|
||||
profile_referral_relation,
|
||||
profile_played_world,
|
||||
public_work_play_daily_stat,
|
||||
profile_membership,
|
||||
profile_recharge_order,
|
||||
profile_save_archive,
|
||||
@@ -742,6 +743,14 @@ where
|
||||
|
||||
fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde_json::Value {
|
||||
let mut next_value = value.clone();
|
||||
if table_name == "user_account" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:头像字段晚于认证拆表加入,旧迁移包按未设置头像兼容。
|
||||
object
|
||||
.entry("avatar_url".to_string())
|
||||
.or_insert(serde_json::Value::Null);
|
||||
}
|
||||
}
|
||||
if table_name == "big_fish_creation_session" {
|
||||
if let Some(object) = next_value.as_object_mut() {
|
||||
// 中文注释:旧迁移包没有公开游玩次数字段,导入时按新建作品默认 0 兼容。
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::runtime::{
|
||||
ProfilePlayedWorkUpsertInput, add_profile_observed_play_time, upsert_profile_played_work,
|
||||
ProfilePlayedWorkUpsertInput, PublicWorkPlayRecordInput, add_profile_observed_play_time,
|
||||
count_recent_public_work_plays, record_public_work_play, upsert_profile_played_work,
|
||||
};
|
||||
use module_puzzle::{
|
||||
PUZZLE_MAX_TAG_COUNT, PuzzleAgentMessageFinalizeInput, PuzzleAgentMessageKind,
|
||||
@@ -8,13 +9,14 @@ use module_puzzle::{
|
||||
PuzzleAgentStage, PuzzleAnchorPack, PuzzleDraftCompileInput, PuzzleGeneratedImageCandidate,
|
||||
PuzzleGeneratedImagesSaveInput, PuzzleLeaderboardEntry, PuzzleLeaderboardSubmitInput,
|
||||
PuzzlePublicationStatus, PuzzlePublishInput, PuzzleResultDraft, PuzzleRunDragInput,
|
||||
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunProcedureResult, PuzzleRunSnapshot,
|
||||
PuzzleRunStartInput, PuzzleRunSwapInput, PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput,
|
||||
PuzzleWorkDeleteInput, PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile,
|
||||
PuzzleWorkRemixInput, PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
||||
PuzzleRunGetInput, PuzzleRunNextLevelInput, PuzzleRunPauseInput, PuzzleRunProcedureResult,
|
||||
PuzzleRunPropInput, PuzzleRunSnapshot, PuzzleRunStartInput, PuzzleRunSwapInput,
|
||||
PuzzleRuntimeLevelStatus, PuzzleSelectCoverImageInput, PuzzleWorkDeleteInput,
|
||||
PuzzleWorkGetInput, PuzzleWorkProcedureResult, PuzzleWorkProfile, PuzzleWorkRemixInput,
|
||||
PuzzleWorkUpsertInput, PuzzleWorksListInput, PuzzleWorksProcedureResult,
|
||||
apply_publish_overrides_to_draft, apply_selected_candidate, build_result_preview,
|
||||
compile_result_draft, create_work_profile, infer_anchor_pack, normalize_theme_tags,
|
||||
publish_work_profile, resolve_puzzle_grid_size, select_next_profile, start_run, swap_pieces,
|
||||
publish_work_profile, resolve_puzzle_grid_size, select_next_profile,
|
||||
};
|
||||
use serde_json::from_str as json_from_str;
|
||||
use serde_json::to_string as json_to_string;
|
||||
@@ -77,13 +79,15 @@ pub struct PuzzleWorkProfileRow {
|
||||
cover_asset_id: Option<String>,
|
||||
publication_status: PuzzlePublicationStatus,
|
||||
play_count: u32,
|
||||
remix_count: u32,
|
||||
like_count: u32,
|
||||
anchor_pack_json: String,
|
||||
publish_ready: bool,
|
||||
created_at: Timestamp,
|
||||
updated_at: Timestamp,
|
||||
published_at: Option<Timestamp>,
|
||||
#[default(0)]
|
||||
remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
}
|
||||
|
||||
/// 运行态 run 快照表。
|
||||
@@ -503,6 +507,44 @@ pub fn advance_puzzle_next_level(
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn update_puzzle_run_pause(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: PuzzleRunPauseInput,
|
||||
) -> PuzzleRunProcedureResult {
|
||||
match ctx.try_with_tx(|tx| update_puzzle_run_pause_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn use_puzzle_runtime_prop(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: PuzzleRunPropInput,
|
||||
) -> PuzzleRunProcedureResult {
|
||||
match ctx.try_with_tx(|tx| use_puzzle_runtime_prop_tx(tx, input.clone())) {
|
||||
Ok(run) => PuzzleRunProcedureResult {
|
||||
ok: true,
|
||||
run_json: Some(serialize_json(&run)),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => PuzzleRunProcedureResult {
|
||||
ok: false,
|
||||
run_json: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[spacetimedb::procedure]
|
||||
pub fn submit_puzzle_leaderboard_entry(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -755,7 +797,7 @@ fn save_puzzle_generated_images_tx(
|
||||
}
|
||||
|
||||
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
|
||||
@@ -804,7 +846,7 @@ fn select_puzzle_cover_image_tx(
|
||||
let draft =
|
||||
apply_selected_candidate(draft, &input.candidate_id).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
|
||||
@@ -1029,12 +1071,13 @@ fn delete_puzzle_work_tx(
|
||||
}
|
||||
|
||||
fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, String> {
|
||||
let now_micros = ctx.timestamp.to_micros_since_unix_epoch();
|
||||
let mut items = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.iter()
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.map(|row| build_puzzle_work_profile_from_row(&row))
|
||||
.map(|row| build_puzzle_work_profile_from_row_with_recent_count(ctx, &row, now_micros))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros));
|
||||
Ok(items)
|
||||
@@ -1053,7 +1096,11 @@ fn get_puzzle_gallery_detail_tx(
|
||||
if row.publication_status != PuzzlePublicationStatus::Published {
|
||||
return Err("拼图作品尚未发布".to_string());
|
||||
}
|
||||
build_puzzle_work_profile_from_row(&row)
|
||||
build_puzzle_work_profile_from_row_with_recent_count(
|
||||
ctx,
|
||||
&row,
|
||||
ctx.timestamp.to_micros_since_unix_epoch(),
|
||||
)
|
||||
}
|
||||
|
||||
fn remix_puzzle_work_tx(
|
||||
@@ -1213,8 +1260,9 @@ fn start_puzzle_run_tx(
|
||||
return Err("入口拼图作品未发布".to_string());
|
||||
}
|
||||
let entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
|
||||
let mut run =
|
||||
start_run(input.run_id.clone(), &entry_profile, 0).map_err(|error| error.to_string())?;
|
||||
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 current_grid_size = run.current_grid_size;
|
||||
let current_profile_id = entry_profile.profile_id.clone();
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
@@ -1231,6 +1279,15 @@ fn start_puzzle_run_tx(
|
||||
)
|
||||
.map(|value| value.profile_id.clone());
|
||||
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
PublicWorkPlayRecordInput {
|
||||
source_type: "puzzle".to_string(),
|
||||
owner_user_id: entry_profile_row.owner_user_id.clone(),
|
||||
profile_id: entry_profile_row.profile_id.clone(),
|
||||
played_at_micros: input.started_at_micros,
|
||||
},
|
||||
)?;
|
||||
increment_puzzle_profile_play_count(ctx, &entry_profile_row, input.started_at_micros);
|
||||
upsert_puzzle_profile_played_work(
|
||||
ctx,
|
||||
@@ -1247,7 +1304,14 @@ fn get_puzzle_run_tx(
|
||||
input: PuzzleRunGetInput,
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let mut run = deserialize_run(&row.snapshot_json)?;
|
||||
let now_micros = ctx.timestamp.to_micros_since_unix_epoch();
|
||||
let mut run = module_puzzle::resolve_puzzle_run_timer_at(
|
||||
deserialize_run(&row.snapshot_json)?,
|
||||
micros_to_millis(now_micros),
|
||||
);
|
||||
if serialize_json(&run) != row.snapshot_json {
|
||||
replace_puzzle_runtime_run(ctx, &row, &run, now_micros);
|
||||
}
|
||||
if let Some((profile_id, grid_size)) = run
|
||||
.current_level
|
||||
.as_ref()
|
||||
@@ -1270,9 +1334,27 @@ fn swap_puzzle_pieces_tx(
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let mut next_run = swap_pieces(¤t_run, &input.first_piece_id, &input.second_piece_id)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut next_run = module_puzzle::swap_pieces_at(
|
||||
¤t_run,
|
||||
&input.first_piece_id,
|
||||
&input.second_piece_id,
|
||||
micros_to_millis(input.swapped_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
refresh_next_profile_recommendation(ctx, &mut next_run)?;
|
||||
if let Some((profile_id, grid_size)) = next_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut next_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.swapped_at_micros);
|
||||
Ok(next_run)
|
||||
}
|
||||
@@ -1283,14 +1365,28 @@ fn drag_puzzle_piece_or_group_tx(
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let mut next_run = module_puzzle::drag_piece_or_group(
|
||||
let mut next_run = module_puzzle::drag_piece_or_group_at(
|
||||
¤t_run,
|
||||
&input.piece_id,
|
||||
input.target_row,
|
||||
input.target_col,
|
||||
micros_to_millis(input.dragged_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
refresh_next_profile_recommendation(ctx, &mut next_run)?;
|
||||
if let Some((profile_id, grid_size)) = next_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut next_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.dragged_at_micros);
|
||||
Ok(next_run)
|
||||
}
|
||||
@@ -1323,8 +1419,12 @@ fn advance_puzzle_next_level_tx(
|
||||
)
|
||||
.ok_or_else(|| "没有可用的下一关候选".to_string())?
|
||||
.clone();
|
||||
let mut next_run = module_puzzle::advance_next_level(¤t_run, &next_profile)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let mut next_run = module_puzzle::advance_next_level_at(
|
||||
¤t_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();
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
@@ -1344,6 +1444,15 @@ fn advance_puzzle_next_level_tx(
|
||||
.profile_id()
|
||||
.find(&next_profile.profile_id)
|
||||
{
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
PublicWorkPlayRecordInput {
|
||||
source_type: "puzzle".to_string(),
|
||||
owner_user_id: next_profile_row.owner_user_id.clone(),
|
||||
profile_id: next_profile_row.profile_id.clone(),
|
||||
played_at_micros: input.advanced_at_micros,
|
||||
},
|
||||
)?;
|
||||
increment_puzzle_profile_play_count(ctx, &next_profile_row, input.advanced_at_micros);
|
||||
upsert_puzzle_profile_played_work(
|
||||
ctx,
|
||||
@@ -1356,6 +1465,82 @@ fn advance_puzzle_next_level_tx(
|
||||
Ok(next_run)
|
||||
}
|
||||
|
||||
fn update_puzzle_run_pause_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleRunPauseInput,
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let next_run = module_puzzle::set_puzzle_run_paused_at(
|
||||
¤t_run,
|
||||
input.paused,
|
||||
micros_to_millis(input.updated_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?;
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.updated_at_micros);
|
||||
let mut hydrated_run = next_run;
|
||||
if let Some((profile_id, grid_size)) = hydrated_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut hydrated_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
Ok(hydrated_run)
|
||||
}
|
||||
|
||||
fn use_puzzle_runtime_prop_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleRunPropInput,
|
||||
) -> Result<PuzzleRunSnapshot, String> {
|
||||
let row = get_owned_run_row(ctx, &input.run_id, &input.owner_user_id)?;
|
||||
let current_run = deserialize_run(&row.snapshot_json)?;
|
||||
let next_run = match input.prop_kind.as_str() {
|
||||
"freezeTime" | "freeze_time" => {
|
||||
module_puzzle::apply_puzzle_freeze_time_at(
|
||||
¤t_run,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?
|
||||
}
|
||||
"hint" => module_puzzle::set_puzzle_run_paused_at(
|
||||
¤t_run,
|
||||
false,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
"reference" => module_puzzle::set_puzzle_run_paused_at(
|
||||
¤t_run,
|
||||
true,
|
||||
micros_to_millis(input.used_at_micros),
|
||||
)
|
||||
.map_err(|error| error.to_string())?,
|
||||
_ => return Err("未知拼图道具".to_string()),
|
||||
};
|
||||
replace_puzzle_runtime_run(ctx, &row, &next_run, input.used_at_micros);
|
||||
let mut hydrated_run = next_run;
|
||||
if let Some((profile_id, grid_size)) = hydrated_run
|
||||
.current_level
|
||||
.as_ref()
|
||||
.map(|level| (level.profile_id.clone(), level.grid_size))
|
||||
{
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut hydrated_run,
|
||||
&input.owner_user_id,
|
||||
&profile_id,
|
||||
grid_size,
|
||||
);
|
||||
}
|
||||
Ok(hydrated_run)
|
||||
}
|
||||
|
||||
fn submit_puzzle_leaderboard_entry_tx(
|
||||
ctx: &TxContext,
|
||||
input: PuzzleLeaderboardSubmitInput,
|
||||
@@ -1424,7 +1609,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(),
|
||||
@@ -1447,6 +1632,23 @@ fn build_puzzle_agent_session_snapshot(
|
||||
|
||||
fn build_puzzle_work_profile_from_row(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
build_puzzle_work_profile_from_row_without_recent_count(row)
|
||||
}
|
||||
|
||||
fn build_puzzle_work_profile_from_row_with_recent_count(
|
||||
ctx: &TxContext,
|
||||
row: &PuzzleWorkProfileRow,
|
||||
now_micros: i64,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
let mut profile = build_puzzle_work_profile_from_row_without_recent_count(row)?;
|
||||
profile.recent_play_count_7d =
|
||||
count_recent_public_work_plays(ctx, "puzzle", &row.profile_id, now_micros);
|
||||
Ok(profile)
|
||||
}
|
||||
|
||||
fn build_puzzle_work_profile_from_row_without_recent_count(
|
||||
row: &PuzzleWorkProfileRow,
|
||||
) -> Result<PuzzleWorkProfile, String> {
|
||||
Ok(PuzzleWorkProfile {
|
||||
work_id: row.work_id.clone(),
|
||||
@@ -1467,6 +1669,7 @@ fn build_puzzle_work_profile_from_row(
|
||||
play_count: row.play_count,
|
||||
remix_count: row.remix_count,
|
||||
like_count: row.like_count,
|
||||
recent_play_count_7d: 0,
|
||||
publish_ready: row.publish_ready,
|
||||
anchor_pack: deserialize_anchor_pack(&row.anchor_pack_json)?,
|
||||
})
|
||||
@@ -1482,6 +1685,13 @@ fn build_puzzle_work_ids_from_session_id(session_id: &str) -> (String, String) {
|
||||
)
|
||||
}
|
||||
|
||||
fn micros_to_millis(value: i64) -> u64 {
|
||||
if value <= 0 {
|
||||
return 0;
|
||||
}
|
||||
(value as u64).saturating_div(1_000)
|
||||
}
|
||||
|
||||
fn upsert_puzzle_draft_work_profile(
|
||||
ctx: &TxContext,
|
||||
session_id: &str,
|
||||
@@ -1500,7 +1710,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,
|
||||
)
|
||||
@@ -2095,6 +2305,9 @@ mod tests {
|
||||
updated_at_micros: 1,
|
||||
published_at_micros: Some(1),
|
||||
play_count: 0,
|
||||
recent_play_count_7d: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: empty_anchor_pack(),
|
||||
};
|
||||
@@ -2110,6 +2323,9 @@ mod tests {
|
||||
updated_at_micros: 2,
|
||||
published_at_micros: Some(2),
|
||||
play_count: 0,
|
||||
recent_play_count_7d: 0,
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
publish_ready: true,
|
||||
anchor_pack: empty_anchor_pack(),
|
||||
source_session_id: None,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::*;
|
||||
|
||||
const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000;
|
||||
const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7;
|
||||
|
||||
#[spacetimedb::table(accessor = profile_dashboard_state)]
|
||||
pub struct ProfileDashboardState {
|
||||
#[primary_key]
|
||||
@@ -116,6 +119,26 @@ pub struct ProfilePlayedWorld {
|
||||
pub(crate) last_observed_play_time_ms: u64,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
accessor = public_work_play_daily_stat,
|
||||
index(
|
||||
accessor = by_public_work_play_daily_stat_work_day,
|
||||
btree(columns = [source_type, profile_id, played_day])
|
||||
)
|
||||
)]
|
||||
pub struct PublicWorkPlayDailyStat {
|
||||
#[primary_key]
|
||||
pub(crate) stat_id: String,
|
||||
// 中文注释:source_type 区分 custom-world / puzzle / big-fish,避免不同玩法 profile_id 撞桶。
|
||||
pub(crate) source_type: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
// 中文注释:UTC 自 Unix 纪元起的自然日桶,用于快速聚合近 7 日新增游玩次数。
|
||||
pub(crate) played_day: i64,
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
}
|
||||
|
||||
pub(crate) struct ProfilePlayedWorkUpsertInput {
|
||||
pub(crate) user_id: String,
|
||||
pub(crate) world_key: String,
|
||||
@@ -127,6 +150,13 @@ pub(crate) struct ProfilePlayedWorkUpsertInput {
|
||||
pub(crate) played_at_micros: i64,
|
||||
}
|
||||
|
||||
pub(crate) struct PublicWorkPlayRecordInput {
|
||||
pub(crate) source_type: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) played_at_micros: i64,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(accessor = profile_membership)]
|
||||
pub struct ProfileMembership {
|
||||
#[primary_key]
|
||||
@@ -420,7 +450,7 @@ pub fn get_profile_referral_invite_center(
|
||||
}
|
||||
}
|
||||
|
||||
// 填码绑定、每日邀请者奖励上限和双方叙世币发放都在同一事务内完成。
|
||||
// 填码绑定、每日邀请者奖励上限和双方陶泥币发放都在同一事务内完成。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn redeem_profile_referral_invite_code(
|
||||
ctx: &mut ProcedureContext,
|
||||
@@ -705,6 +735,88 @@ pub(crate) fn add_profile_observed_play_time(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn record_public_work_play(
|
||||
ctx: &ReducerContext,
|
||||
input: PublicWorkPlayRecordInput,
|
||||
) -> Result<(), String> {
|
||||
let source_type = input.source_type.trim();
|
||||
let owner_user_id = input.owner_user_id.trim();
|
||||
let profile_id = input.profile_id.trim();
|
||||
if source_type.is_empty() || owner_user_id.is_empty() || profile_id.is_empty() {
|
||||
return Err("public_work_play_daily_stat 参数不能为空".to_string());
|
||||
}
|
||||
|
||||
let played_day = public_work_play_day_from_micros(input.played_at_micros);
|
||||
let stat_id = build_public_work_play_daily_stat_id(source_type, profile_id, played_day);
|
||||
let updated_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros);
|
||||
let next_count = ctx
|
||||
.db
|
||||
.public_work_play_daily_stat()
|
||||
.stat_id()
|
||||
.find(&stat_id)
|
||||
.map(|existing| {
|
||||
ctx.db
|
||||
.public_work_play_daily_stat()
|
||||
.stat_id()
|
||||
.delete(&existing.stat_id);
|
||||
existing.play_count.saturating_add(1)
|
||||
})
|
||||
.unwrap_or(1);
|
||||
|
||||
ctx.db
|
||||
.public_work_play_daily_stat()
|
||||
.insert(PublicWorkPlayDailyStat {
|
||||
stat_id,
|
||||
source_type: source_type.to_string(),
|
||||
owner_user_id: owner_user_id.to_string(),
|
||||
profile_id: profile_id.to_string(),
|
||||
played_day,
|
||||
play_count: next_count,
|
||||
updated_at,
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn count_recent_public_work_plays(
|
||||
ctx: &ReducerContext,
|
||||
source_type: &str,
|
||||
profile_id: &str,
|
||||
now_micros: i64,
|
||||
) -> u32 {
|
||||
let source_type = source_type.trim();
|
||||
let profile_id = profile_id.trim();
|
||||
if source_type.is_empty() || profile_id.is_empty() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let current_day = public_work_play_day_from_micros(now_micros);
|
||||
let first_day = current_day.saturating_sub(PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1);
|
||||
|
||||
ctx.db
|
||||
.public_work_play_daily_stat()
|
||||
.iter()
|
||||
.filter(|row| {
|
||||
row.source_type == source_type
|
||||
&& row.profile_id == profile_id
|
||||
&& row.played_day >= first_day
|
||||
&& row.played_day <= current_day
|
||||
})
|
||||
.fold(0u32, |total, row| total.saturating_add(row.play_count))
|
||||
}
|
||||
|
||||
fn public_work_play_day_from_micros(value: i64) -> i64 {
|
||||
value.div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS)
|
||||
}
|
||||
|
||||
fn build_public_work_play_daily_stat_id(
|
||||
source_type: &str,
|
||||
profile_id: &str,
|
||||
played_day: i64,
|
||||
) -> String {
|
||||
format!("{source_type}:{profile_id}:{played_day}")
|
||||
}
|
||||
|
||||
fn ensure_profile_dashboard_state(ctx: &ReducerContext, user_id: &str, updated_at: Timestamp) {
|
||||
if ctx
|
||||
.db
|
||||
@@ -1954,7 +2066,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()
|
||||
|
||||
Reference in New Issue
Block a user