perf: cache public gallery views

This commit is contained in:
kdletters
2026-05-17 01:19:12 +08:00
parent d9c8473504
commit 81fe3dcf28
39 changed files with 2124 additions and 298 deletions

View File

@@ -8,9 +8,42 @@ use crate::runtime::{
};
use crate::*;
use module_big_fish::{EvaluateBigFishPublishReadinessCommand, evaluate_publish_readiness};
use spacetimedb::AnonymousViewContext;
const INITIAL_BIG_FISH_CREATION_PROGRESS_PERCENT: u32 = 0;
/// 大鱼吃小鱼公开广场列表投影。
///
/// 公开列表从已发布 creation session 生成卡片字段7 日播放数由
/// `api-server` 订阅 `public_work_play_daily_stat` 后在本地聚合。
#[spacetimedb::view(accessor = big_fish_gallery_view, public)]
pub fn big_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec<BigFishWorkSummarySnapshot> {
let mut items = ctx
.db
.big_fish_creation_session()
.by_big_fish_session_stage()
.filter(BigFishCreationStage::Published)
.filter_map(|row| match build_big_fish_gallery_view_row(ctx, &row) {
Ok(snapshot) => Some(snapshot),
Err(error) => {
log::warn!(
"大鱼吃小鱼公开广场 view 跳过损坏的作品投影 session_id={}: {}",
row.session_id,
error
);
None
}
})
.collect::<Vec<_>>();
items.sort_by(|left, right| {
right
.updated_at_micros
.cmp(&left.updated_at_micros)
.then_with(|| left.source_session_id.cmp(&right.source_session_id))
});
items
}
#[spacetimedb::procedure]
pub fn create_big_fish_session(
ctx: &mut ProcedureContext,
@@ -988,6 +1021,16 @@ pub(crate) fn build_big_fish_work_summary(
ctx: &ReducerContext,
row: &BigFishCreationSession,
now_micros: i64,
) -> Result<BigFishWorkSummarySnapshot, String> {
let mut summary = build_big_fish_work_summary_without_recent_count(ctx, row)?;
summary.recent_play_count_7d =
count_recent_public_work_plays(ctx, "big-fish", &row.session_id, now_micros);
Ok(summary)
}
fn build_big_fish_work_summary_without_recent_count(
ctx: &ReducerContext,
row: &BigFishCreationSession,
) -> Result<BigFishWorkSummarySnapshot, String> {
let draft = row
.draft_json
@@ -1052,12 +1095,7 @@ 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,
),
recent_play_count_7d: 0,
published_at_micros: row
.published_at
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
@@ -1065,6 +1103,113 @@ pub(crate) fn build_big_fish_work_summary(
})
}
fn build_big_fish_gallery_view_row(
ctx: &AnonymousViewContext,
row: &BigFishCreationSession,
) -> Result<BigFishWorkSummarySnapshot, String> {
let draft = row
.draft_json
.as_deref()
.map(deserialize_draft)
.transpose()
.map_err(|error| format!("big_fish.draft_json 非法: {error}"))?;
let asset_slots = list_big_fish_asset_slots_for_view(ctx, &row.session_id);
let coverage = build_asset_coverage(draft.as_ref(), &asset_slots);
let cover_image_src = asset_slots
.iter()
.find(|slot| slot.asset_kind == BigFishAssetKind::StageBackground)
.and_then(|slot| slot.asset_url.clone())
.or_else(|| {
asset_slots
.iter()
.find(|slot| slot.asset_kind == BigFishAssetKind::LevelMainImage)
.and_then(|slot| slot.asset_url.clone())
});
let title = draft
.as_ref()
.map(|value| value.title.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "未命名大鱼草稿".to_string());
let subtitle = draft
.as_ref()
.map(|value| value.subtitle.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "等待整理玩法草稿".to_string());
let summary = draft
.as_ref()
.map(|value| value.core_fun.clone())
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| {
row.last_assistant_reply
.clone()
.unwrap_or_else(|| "继续补齐锚点后即可生成玩法草稿。".to_string())
});
Ok(BigFishWorkSummarySnapshot {
work_id: format!("big-fish-work-{}", row.session_id),
source_session_id: row.session_id.clone(),
owner_user_id: row.owner_user_id.clone(),
title,
subtitle,
summary,
cover_image_src,
status: if row.stage == BigFishCreationStage::Published {
"published".to_string()
} else {
"draft".to_string()
},
updated_at_micros: row.updated_at.to_micros_since_unix_epoch(),
publish_ready: coverage.publish_ready,
level_count: draft
.as_ref()
.map(|value| value.runtime_params.level_count)
.unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT),
level_main_image_ready_count: coverage.level_main_image_ready_count,
level_motion_ready_count: coverage.level_motion_ready_count,
background_ready: coverage.background_ready,
play_count: row.play_count,
remix_count: row.remix_count,
like_count: row.like_count,
recent_play_count_7d: 0,
published_at_micros: row
.published_at
.or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at))
.map(|value| value.to_micros_since_unix_epoch()),
})
}
fn list_big_fish_asset_slots_for_view(
ctx: &AnonymousViewContext,
session_id: &str,
) -> Vec<BigFishAssetSlotSnapshot> {
let mut slots = ctx
.db
.big_fish_asset_slot()
.by_big_fish_asset_session_id()
.filter(session_id)
.map(|slot| BigFishAssetSlotSnapshot {
slot_id: slot.slot_id,
session_id: slot.session_id,
asset_kind: slot.asset_kind,
level: slot.level,
motion_key: slot.motion_key,
status: slot.status,
asset_url: slot.asset_url,
prompt_snapshot: slot.prompt_snapshot,
updated_at_micros: slot.updated_at.to_micros_since_unix_epoch(),
})
.collect::<Vec<_>>();
slots.sort_by_key(|slot| {
(
slot.level.unwrap_or(0),
slot.asset_kind.as_str().to_string(),
slot.motion_key.clone().unwrap_or_default(),
slot.slot_id.clone(),
)
});
slots
}
fn build_public_big_fish_gallery_list_input() -> BigFishWorksListInput {
BigFishWorksListInput {
// 中文注释published_only 分支不会按 owner 过滤;非空占位用于兼容旧部署模块的前置校验。

View File

@@ -2,7 +2,8 @@ use crate::*;
#[spacetimedb::table(
accessor = big_fish_creation_session,
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id]))
index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])),
index(accessor = by_big_fish_session_stage, btree(columns = [stage]))
)]
pub struct BigFishCreationSession {
#[primary_key]