修复隐藏拼图作品进入通关推荐
收口拼图公开消费路径的 Published + visible 判断 拦截隐藏拼图的公开详情、互动和正式运行态入口 补充隐藏拼图推荐候选回归测试 更新后端契约文档和团队踩坑记录
This commit is contained in:
@@ -103,6 +103,14 @@
|
||||
- 验证:`cargo test -p spacetime-module custom_world_public_interactions_accept_legacy_missing_published_at --manifest-path server-rs/Cargo.toml`。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/custom_world.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`。
|
||||
|
||||
## 拼图公开推荐不要只按 Published 判断
|
||||
|
||||
- 现象:后台把拼图作品隐藏后,作品不在公开列表里显示,但玩家通关其它拼图后的推荐下一作品仍可能出现这条隐藏作品。
|
||||
- 原因:拼图隐藏只把 `puzzle_work_profile.visible` 置为 `false`,不会把 `publication_status` 从 `Published` 改走;通关推荐候选曾只通过 `by_puzzle_work_publication_status().filter(Published)` 取数,漏掉可见性判断。
|
||||
- 处理:拼图公开消费路径统一使用 `Published + visible=true`,范围包括 `puzzle_gallery_view`、`puzzle_gallery_card_view`、兼容 gallery/detail procedure、公开点赞 / Remix、正式公开 runtime 启动和通关后的 `recommended_next_works` 候选。
|
||||
- 验证:`cargo test -p spacetime-module hidden_published_puzzle_work_is_not_public_visible_candidate --manifest-path server-rs/Cargo.toml`,并在需要时用后台隐藏一个已发布拼图后重试通关推荐。
|
||||
- 关联:`server-rs/crates/spacetime-module/src/puzzle.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。
|
||||
|
||||
## 推荐页 WF 点赞不要落到 RPG / custom-world
|
||||
|
||||
- 现象:推荐页里给 `WF-*` 敲木鱼作品点赞时,平台错误弹窗显示 `custom_world 已发布作品不存在,无法点赞`。
|
||||
|
||||
@@ -675,6 +675,8 @@ npm run check:server-rs-ddd
|
||||
|
||||
- Rust 结构体:`PuzzleWorkProfileRow`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
|
||||
- 说明:拼图作品 profile 表,保存草稿 / 已发布作品的标题、作者、关卡、封面、发布状态、可见性、基础游玩数、点赞数、改造数和积分激励领取状态。
|
||||
- 字段变更:`visible` 控制是否进入公开列表 / 详情、通关后的推荐下一作品候选、公开点赞 / Remix 和正式公开 runtime;默认 `true`。后台隐藏后作品可保留 `publication_status = Published`,但公开消费路径必须按 `Published + visible=true` 判断。
|
||||
|
||||
### `puzzle_clear_agent_session`
|
||||
|
||||
@@ -720,14 +722,14 @@ npm run check:server-rs-ddd
|
||||
- Rust view:`puzzle_gallery_view`
|
||||
- 返回类型:`Vec<PuzzleWorkProfile>`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
|
||||
- 说明:拼图广场公开详情 source / 兼容投影,只暴露 `publication_status = Published` 的作品,但返回完整 `PuzzleWorkProfile`,包含 levels / anchor_pack 等详情级字段;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。
|
||||
- 说明:拼图广场公开详情 source / 兼容投影,只暴露 `publication_status = Published` 且 `visible = true` 的作品,但返回完整 `PuzzleWorkProfile`,包含 levels / anchor_pack 等详情级字段;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。
|
||||
|
||||
### SpacetimeDB view:`puzzle_gallery_card_view`
|
||||
|
||||
- Rust view:`puzzle_gallery_card_view`
|
||||
- 返回类型:`Vec<PuzzleGalleryCardViewRow>`
|
||||
- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs`
|
||||
- 说明:拼图公开列表 source 投影,只暴露前端列表卡片需要的公开字段,不携带 levels / anchor_pack 等详情级载荷;统一公开列表主路径通过 `public_work_gallery_entry` 消费该 view,`/api/runtime/puzzle/gallery` 保留旧 HTTP shape,并从统一 public cache 映射回 `PuzzleGalleryResponse`。
|
||||
- 说明:拼图公开列表 source 投影,只暴露 `publication_status = Published` 且 `visible = true` 的公开字段,不携带 levels / anchor_pack 等详情级载荷;统一公开列表主路径通过 `public_work_gallery_entry` 消费该 view,`/api/runtime/puzzle/gallery` 保留旧 HTTP shape,并从统一 public cache 映射回 `PuzzleGalleryResponse`。
|
||||
|
||||
### 拼图公开列表 HTTP 窗口缓存
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec<PuzzleWorkProfile>
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.filter(|row| row.visible)
|
||||
.filter(is_public_visible_puzzle_work)
|
||||
.filter_map(
|
||||
|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) {
|
||||
Ok(profile) => Some(profile),
|
||||
@@ -183,7 +183,7 @@ pub fn puzzle_gallery_card_view(ctx: &AnonymousViewContext) -> Vec<PuzzleGallery
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.filter(|row| row.visible)
|
||||
.filter(is_public_visible_puzzle_work)
|
||||
.filter_map(|row| match build_puzzle_gallery_card_view_row(&row) {
|
||||
Ok(item) => Some(item),
|
||||
Err(error) => {
|
||||
@@ -2069,6 +2069,7 @@ fn list_puzzle_gallery_tx(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfile>, Str
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.filter(is_public_visible_puzzle_work)
|
||||
.collect::<Vec<_>>();
|
||||
let profile_ids = rows
|
||||
.iter()
|
||||
@@ -2094,8 +2095,8 @@ fn get_puzzle_gallery_detail_tx(
|
||||
.profile_id()
|
||||
.find(&input.profile_id)
|
||||
.ok_or_else(|| "拼图作品不存在".to_string())?;
|
||||
if row.publication_status != PuzzlePublicationStatus::Published {
|
||||
return Err("拼图作品尚未发布".to_string());
|
||||
if !is_public_visible_puzzle_work(&row) {
|
||||
return Err("拼图作品不可公开访问".to_string());
|
||||
}
|
||||
build_puzzle_work_profile_from_row_with_recent_count(
|
||||
ctx,
|
||||
@@ -2118,7 +2119,7 @@ fn record_puzzle_work_like_tx(
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.filter(is_public_visible_puzzle_work)
|
||||
.ok_or_else(|| "拼图已发布作品不存在,无法点赞".to_string())?;
|
||||
let inserted_like = record_public_work_like(
|
||||
ctx,
|
||||
@@ -2214,7 +2215,7 @@ fn remix_puzzle_work_tx(
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&source_profile_id.to_string())
|
||||
.filter(|row| row.publication_status == PuzzlePublicationStatus::Published)
|
||||
.filter(is_public_visible_puzzle_work)
|
||||
.ok_or_else(|| "拼图已发布源作品不存在".to_string())?;
|
||||
let source_profile = build_puzzle_work_profile_from_row(&source)?;
|
||||
let remixed_at = Timestamp::from_micros_since_unix_epoch(input.remixed_at_micros);
|
||||
@@ -2355,6 +2356,11 @@ fn start_puzzle_run_tx(
|
||||
{
|
||||
return Err("入口拼图作品未发布".to_string());
|
||||
}
|
||||
if entry_profile_row.publication_status == PuzzlePublicationStatus::Published
|
||||
&& !entry_profile_row.visible
|
||||
{
|
||||
return Err("入口拼图作品不可公开访问".to_string());
|
||||
}
|
||||
let mut entry_profile = build_puzzle_work_profile_from_row(&entry_profile_row)?;
|
||||
if entry_profile.cover_image_src.is_none() {
|
||||
return Err("入口拼图作品缺少正式图片".to_string());
|
||||
@@ -2387,7 +2393,7 @@ fn start_puzzle_run_tx(
|
||||
);
|
||||
refresh_next_level_handoff(ctx, &mut run)?;
|
||||
|
||||
if entry_profile_row.publication_status == PuzzlePublicationStatus::Published {
|
||||
if is_public_visible_puzzle_work(&entry_profile_row) {
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
PublicWorkPlayRecordInput {
|
||||
@@ -2595,6 +2601,7 @@ fn advance_puzzle_next_level_tx(
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&next_profile.profile_id)
|
||||
.filter(is_public_visible_puzzle_work)
|
||||
{
|
||||
record_public_work_play(
|
||||
ctx,
|
||||
@@ -2822,7 +2829,7 @@ fn submit_puzzle_leaderboard_entry_tx(
|
||||
if !matches_service_level && !is_frontend_puzzle_level_candidate(&run, &input.profile_id) {
|
||||
return Err("提交成绩的拼图作品与当前关卡不匹配".to_string());
|
||||
}
|
||||
if current_profile_row.publication_status != PuzzlePublicationStatus::Published {
|
||||
if !is_public_visible_puzzle_work(¤t_profile_row) {
|
||||
hydrate_puzzle_leaderboard_entries(
|
||||
ctx,
|
||||
&mut run,
|
||||
@@ -3832,10 +3839,15 @@ fn list_published_puzzle_profiles(ctx: &TxContext) -> Result<Vec<PuzzleWorkProfi
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.filter(is_public_visible_puzzle_work)
|
||||
.map(|row| build_puzzle_work_profile_from_row(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn is_public_visible_puzzle_work(row: &PuzzleWorkProfileRow) -> bool {
|
||||
row.publication_status == PuzzlePublicationStatus::Published && row.visible
|
||||
}
|
||||
|
||||
fn reset_next_level_handoff(run: &mut PuzzleRunSnapshot) {
|
||||
run.recommended_next_profile_id = None;
|
||||
run.next_level_mode = PUZZLE_NEXT_LEVEL_MODE_NONE.to_string();
|
||||
@@ -4250,6 +4262,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hidden_published_puzzle_work_is_not_public_visible_candidate() {
|
||||
let visible_published = puzzle_work_profile_row_for_visibility(
|
||||
"visible-published",
|
||||
PuzzlePublicationStatus::Published,
|
||||
true,
|
||||
);
|
||||
let hidden_published = puzzle_work_profile_row_for_visibility(
|
||||
"hidden-published",
|
||||
PuzzlePublicationStatus::Published,
|
||||
false,
|
||||
);
|
||||
let visible_draft = puzzle_work_profile_row_for_visibility(
|
||||
"visible-draft",
|
||||
PuzzlePublicationStatus::Draft,
|
||||
true,
|
||||
);
|
||||
|
||||
assert!(is_public_visible_puzzle_work(&visible_published));
|
||||
assert!(!is_public_visible_puzzle_work(&hidden_published));
|
||||
assert!(!is_public_visible_puzzle_work(&visible_draft));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn level_generation_failure_only_marks_target_level_failed() {
|
||||
let anchor_pack = infer_anchor_pack("画面描述:一只猫在雨夜灯牌下回头。", None);
|
||||
@@ -4371,4 +4406,39 @@ mod tests {
|
||||
assert_eq!(entries[1].nickname, "玩家 B");
|
||||
assert_eq!(entries[1].rank, 2);
|
||||
}
|
||||
|
||||
fn puzzle_work_profile_row_for_visibility(
|
||||
profile_id: &str,
|
||||
publication_status: PuzzlePublicationStatus,
|
||||
visible: bool,
|
||||
) -> PuzzleWorkProfileRow {
|
||||
let timestamp = Timestamp::from_micros_since_unix_epoch(1);
|
||||
PuzzleWorkProfileRow {
|
||||
profile_id: profile_id.to_string(),
|
||||
work_id: format!("work-{profile_id}"),
|
||||
owner_user_id: "owner".to_string(),
|
||||
source_session_id: None,
|
||||
author_display_name: "作者".to_string(),
|
||||
work_title: "作品".to_string(),
|
||||
work_description: String::new(),
|
||||
level_name: "第一关".to_string(),
|
||||
summary: "摘要".to_string(),
|
||||
theme_tags_json: "[]".to_string(),
|
||||
cover_image_src: Some("/cover.png".to_string()),
|
||||
cover_asset_id: Some("asset-cover".to_string()),
|
||||
levels_json: "[]".to_string(),
|
||||
publication_status,
|
||||
play_count: 0,
|
||||
anchor_pack_json: serialize_json(&empty_anchor_pack()),
|
||||
publish_ready: true,
|
||||
created_at: timestamp,
|
||||
updated_at: timestamp,
|
||||
published_at: Some(timestamp),
|
||||
remix_count: 0,
|
||||
like_count: 0,
|
||||
point_incentive_total_half_points: 0,
|
||||
point_incentive_claimed_points: 0,
|
||||
visible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user