diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 9ac65580a..389432a1d 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -16,6 +16,14 @@ --- +## 2026-07-03 作品公开默认关闭 + +- 背景:作品发布完成不应默认进入公开广场 / 公开详情 / 公开互动消费路径,需要先由后台可见性开关明确开启。 +- 决策:各玩法源表的 `visible` 新作品默认值改为 `false`;从草稿首次发布时仍保持 `false`,只有已发布作品再次发布 / 更新时才保留既有 `visible`。公开列表、详情、点赞、Remix 和正式公开 runtime 继续按 `Published + visible=true` 判断。旧迁移数据缺少 `visible` 时仍补 `true`,避免历史已公开作品被批量隐藏。 +- 影响范围:`spacetime-module` 各玩法作品表、发布 / 编译 / Remix 写入路径、统一公开作品 read model、后台作品可见性管理。 +- 验证方式:运行 `cargo fmt --manifest-path server-rs/Cargo.toml --all`、`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`、`npm run check:encoding` 和 `git diff --check`。 +- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`。 + ## 2026-07-03 外部编辑器 API 生成默认写入画布与素材库 - 背景:外部 API 面向美术 Agent 使用时,需要从自然语言自动选路,并保证生成结果不会只停留在接口回包里;同时后续素材生成需要复用已抽象出的美术规范,避免每次重新追问风格要求。 diff --git a/docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md b/docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md index ac1c723d8..ff8f6f236 100644 --- a/docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md +++ b/docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md @@ -39,7 +39,7 @@ - `sort_time_micros` - `detail_payload_json` -作品源表新增 `visible` 可见性字段,默认 `true`。`visible` 属于源表 / source view 过滤条件,不作为统一公开契约默认返回字段;当 `visible=false` 时,对应作品不得进入 `public_work_gallery_entry` 和 `public_work_detail_entry`。 +作品源表新增 `visible` 可见性字段,新作品默认 `false`。`visible` 属于源表 / source view 过滤条件,不作为统一公开契约默认返回字段;当 `visible=false` 时,对应作品不得进入 `public_work_gallery_entry` 和 `public_work_detail_entry`。 其中 `detail_payload_json` 只承载平台详情页展示扩展,不承载正式 runtime 配置、玩法规则或草稿真相。 @@ -76,7 +76,7 @@ - 旧 view 保留,不删除。 - 旧 view 退到底层 source / 兼容职责。 - 新 `public_work_*` view 是 `api-server` 公开列表 / 详情的统一主读模型。 -- 各玩法 source view 只暴露 `visible=true` 的已发布作品;旧数据迁移默认补 `visible=true`,避免历史作品被误隐藏。 +- 各玩法 source view 只暴露 `visible=true` 的已发布作品;旧数据迁移仍补 `visible=true`,避免历史作品被误隐藏;新发布作品默认不公开,需后台开启后才进入公开消费路径。 - RPG / 自定义世界旧数据可能缺少 `published_at`。统一公开详情可以用 `updated_at` 作为展示和排序兜底;点赞、游玩、Remix 等写入路径也必须按 `publication_status=Published + visible=true + 未删除` 判断作品存在,不能额外要求 `published_at` 非空。 - 临时运行约束:SpacetimeDB 2.2 下抓大鹅 `match_3_d_gallery_view` 的 `publication_status` 索引过滤在源表更新触发统一 view 刷新时可能初始化 panic;为避免后台隐藏作品打爆 module instance,统一 `public_work_*` view 暂不级联抓大鹅 source view,抓大鹅公开入口先保留玩法专用路径。后续应以 source projection 表替代索引 view 后再重新并入统一 read model。 - 旧 `/api/runtime//gallery` 响应 shape 保持兼容,由 BFF mapper 把统一 cache 再映射回当前 DTO。 diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index dbf566438..e999cd218 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -313,7 +313,7 @@ npm run check:server-rs-ddd - Rust 结构体:`BarkBattlePublishedConfigRow` - 源码:`server-rs/crates/spacetime-module/src/bark_battle/tables.rs` -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `bark_battle_runtime_run` @@ -350,7 +350,7 @@ npm run check:server-rs-ddd - Rust 结构体:`BigFishCreationSession` - 源码:`server-rs/crates/spacetime-module/src/big_fish/tables.rs` - 索引:`by_big_fish_session_owner_user_id`、`by_big_fish_session_stage`。公开广场 view 使用 `by_big_fish_session_stage` 读取已发布会话,避免扫整表。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `big_fish_event` @@ -414,13 +414,13 @@ npm run check:server-rs-ddd - Rust 结构体:`CustomWorldGalleryEntry` - 源码:`server-rs/crates/spacetime-module/src/custom_world.rs` - 作用:自定义世界公开 source 读模型。统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该投影并映射成跨玩法契约;`/api/runtime/custom-world-gallery` 保留旧 HTTP shape,并从统一 public cache 映射回旧 DTO。旧 procedure 只用于兼容旧库缺少 gallery 读模型行时的一次性同步兜底。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `custom_world_profile` - Rust 结构体:`CustomWorldProfile` - 源码:`server-rs/crates/spacetime-module/src/custom_world.rs` -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 - 兼容约束:历史公开 RPG / 自定义世界 profile 可能存在 `publication_status=Published` 但 `published_at=None`。公开详情、点赞、游玩、Remix 和 `custom_world_gallery_entry` 同步都以 `Published + deleted_at=None + visible=true` 判断作品可公开互动;展示和 gallery 同步时间在 `published_at` 缺失时回退 `updated_at`,不得仅因 `published_at` 为空返回“已发布作品不存在”。 ### `custom_world_session` @@ -527,7 +527,7 @@ npm run check:server-rs-ddd - 返回类型:`Vec` - 源码:`server-rs/crates/spacetime-module/src/jump_hop.rs` - 说明:跳一跳公开详情兼容投影,包含作品、路径和素材字段;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `wooden_fish_agent_session` @@ -563,7 +563,7 @@ npm run check:server-rs-ddd - 返回类型:`Vec` - 源码:`server-rs/crates/spacetime-module/src/wooden_fish.rs` - 说明:敲木鱼公开详情兼容投影,包含敲击物图案、背景环境图、主题返回按钮图、敲击音效和飘字配置;统一公开详情主路径通过 `public_work_detail_entry` 消费该 view,只保留平台详情页展示摘要。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `match3d_agent_message` @@ -593,7 +593,7 @@ npm run check:server-rs-ddd - 返回类型:`Vec` - 源码:`server-rs/crates/spacetime-module/src/match3d.rs` - 说明:抓大鹅公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `npc_state` @@ -740,7 +740,7 @@ 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` 判断。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情、通关后的推荐下一作品候选、公开点赞 / Remix 和正式公开 runtime;新作品默认 `false`,旧迁移数据按历史公开默认补 `true`。后台隐藏后作品可保留 `publication_status = Published`,但公开消费路径必须按 `Published + visible=true` 判断。 ### `puzzle_clear_agent_session` @@ -753,7 +753,7 @@ npm run check:server-rs-ddd - Rust 结构体:`PuzzleClearWorkProfileRow` - 源码:`server-rs/crates/spacetime-module/src/puzzle_clear/tables.rs` - 说明:拼消消作品 profile 表,保存中央底图资产、4 张素材工作表切片后合成的最终 atlas、35 个复合图案组、95 个 1x1 卡牌切片、卡背占位图、发布状态、可见性和基础 play count;公开列表 / 详情只通过 read model 消费,不让前端直接订阅源表。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `puzzle_clear_runtime_run` @@ -842,7 +842,7 @@ RPG 创作入口的配置 ID 是 `rpg`,当前 `visible=true`、`open=true`; 结构化创作和 RPG 的 LLM JSON 链路默认不启用 Responses `web_search`;只有在明确需要联网增强时,才通过 `GENARRATIVE_RPG_LLM_WEB_SEARCH_ENABLED` 或 `GENARRATIVE_CREATION_AGENT_LLM_WEB_SEARCH_ENABLED` 显式打开。否则未开通工具的上游会先吐自然语言再返回 `ToolNotOpen`,这类失败要按上游工具不可用处理,不要误判成模型返回结果解析失败。 统一公开作品 BFF 路由是 `GET /api/public-works` 与 `GET /api/public-works/{publicWorkCode}`,响应契约由 `shared-contracts::public_work` 和 `packages/shared/src/contracts/publicWork.ts` 共同维护。前端首期仍走 BFF HTTP,不直接订阅 SpacetimeDB;后续若允许浏览器直连订阅,也只能订阅 `public_work_gallery_entry` / `public_work_detail_entry` 这类稳定公开 read model,不能订阅 `puzzle_work_profile`、`custom_world_profile` 等源表后自行拼装列表。设计细节见 `docs/technical/【后端架构】统一公开作品ReadModel设计-2026-05-26.md`。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `quest_log` @@ -895,7 +895,7 @@ RPG 创作入口的配置 ID 是 `rpg`,当前 `visible=true`、`open=true`; - 返回类型:`Vec` - 源码:`server-rs/crates/spacetime-module/src/square_hole.rs` - 说明:方洞挑战公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 ### `story_event` @@ -972,4 +972,4 @@ RPG 创作入口的配置 ID 是 `rpg`,当前 `visible=true`、`open=true`; - 返回类型:`Vec` - 源码:`server-rs/crates/spacetime-module/src/visual_novel.rs` - 说明:视觉小说公开 source 投影,只暴露 `publication_status = published` 的作品卡片字段,不把完整 `draft` 暴露给公开列表订阅;统一公开列表 / 详情主路径通过 `public_work_gallery_entry` / `public_work_detail_entry` 消费该 view 并映射成跨玩法契约。个人历史、详情、运行态和发布仍按原有 procedure / reducer 路径处理。 -- 字段变更:`visible` 控制是否进入公开列表 / 详情,默认 `true`;旧迁移数据由 `migration.rs` 补默认值。 +- 字段变更:`visible` 控制是否进入公开列表 / 详情,新作品默认 `false`;旧迁移数据由 `migration.rs` 按历史公开默认补 `true`。 diff --git a/server-rs/crates/spacetime-module/src/bark_battle.rs b/server-rs/crates/spacetime-module/src/bark_battle.rs index 0bf4c65a1..a56905d26 100644 --- a/server-rs/crates/spacetime-module/src/bark_battle.rs +++ b/server-rs/crates/spacetime-module/src/bark_battle.rs @@ -272,7 +272,7 @@ fn publish_bark_battle_work_tx( created_at: published_at, updated_at: published_at, published_at, - visible: true, + visible: WORK_VISIBLE_DEFAULT, }; let mut published = published; match ctx @@ -283,6 +283,7 @@ fn publish_bark_battle_work_tx( { Some(existing) => { published.created_at = existing.created_at; + published.visible = existing.visible; ctx.db .bark_battle_published_config() .work_id() @@ -413,11 +414,20 @@ fn get_bark_battle_runtime_config_tx( .work_id() .find(&input.work_id) .ok_or_else(|| "bark_battle published config 不存在".to_string())?; + let requester_is_owner = input + .owner_user_id + .as_deref() + .map(str::trim) + .filter(|owner_user_id| !owner_user_id.is_empty()) + .is_some_and(|owner_user_id| owner_user_id == row.owner_user_id); if let Some(owner_user_id) = input.owner_user_id.as_deref() { - if !owner_user_id.trim().is_empty() && row.owner_user_id != owner_user_id.trim() { + if !owner_user_id.trim().is_empty() && !requester_is_owner { return Err("bark_battle runtime config owner 不匹配".to_string()); } } + if !requester_is_owner && !row.visible { + return Err("bark_battle runtime config 只开放已公开作品".to_string()); + } Ok(runtime_config_snapshot(&row)) } @@ -435,6 +445,9 @@ fn start_bark_battle_run_tx( .work_id() .find(&input.work_id) .ok_or_else(|| "bark_battle published config 不存在".to_string())?; + if published.owner_user_id != input.owner_user_id && !published.visible { + return Err("bark_battle run 只能启动已公开作品".to_string()); + } if published.config_version != input.config_version || published.ruleset_version != input.ruleset_version || published.difficulty_preset != input.difficulty_preset diff --git a/server-rs/crates/spacetime-module/src/bark_battle/tables.rs b/server-rs/crates/spacetime-module/src/bark_battle/tables.rs index e3130fff1..bb9398eb5 100644 --- a/server-rs/crates/spacetime-module/src/bark_battle/tables.rs +++ b/server-rs/crates/spacetime-module/src/bark_battle/tables.rs @@ -1,6 +1,6 @@ use crate::*; -const WORK_VISIBLE_DEFAULT: bool = true; +pub(super) const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = bark_battle_draft_config, @@ -42,7 +42,7 @@ pub struct BarkBattlePublishedConfigRow { pub(crate) created_at: Timestamp, pub(crate) updated_at: Timestamp, pub(crate) published_at: Timestamp, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } diff --git a/server-rs/crates/spacetime-module/src/big_fish/assets.rs b/server-rs/crates/spacetime-module/src/big_fish/assets.rs index 9c7fda1fd..8bdd994e6 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/assets.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/assets.rs @@ -1,4 +1,6 @@ -use crate::big_fish::tables::{big_fish_asset_slot, big_fish_creation_session}; +use crate::big_fish::tables::{ + WORK_VISIBLE_DEFAULT, big_fish_asset_slot, big_fish_creation_session, +}; use crate::*; use module_big_fish::{EvaluateBigFishPublishReadinessCommand, evaluate_publish_readiness}; @@ -129,7 +131,13 @@ pub(crate) fn generate_big_fish_asset_tx( published_at: session.published_at, created_at: session.created_at, updated_at, - visible: session.visible, + visible: if session.stage == BigFishCreationStage::Published + || session.published_at.is_some() + { + session.visible + } else { + WORK_VISIBLE_DEFAULT + }, }; replace_big_fish_session(ctx, &session, next_session); for event in readiness.events { @@ -201,7 +209,13 @@ pub(crate) fn publish_big_fish_game_tx( published_at: Some(published_at), created_at: session.created_at, updated_at: published_at, - visible: session.visible, + visible: if session.stage == BigFishCreationStage::Published + || session.published_at.is_some() + { + session.visible + } else { + WORK_VISIBLE_DEFAULT + }, }; replace_big_fish_session(ctx, &session, next_session); for event in readiness.events { diff --git a/server-rs/crates/spacetime-module/src/big_fish/runtime.rs b/server-rs/crates/spacetime-module/src/big_fish/runtime.rs index 6e0f56f8d..f27726577 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/runtime.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/runtime.rs @@ -154,7 +154,7 @@ fn ensure_big_fish_session_playable( if session.owner_user_id == player_user_id { return Ok(()); } - if session.stage == BigFishCreationStage::Published { + if session.stage == BigFishCreationStage::Published && session.visible { return Ok(()); } Err("未发布的大鱼吃小鱼作品不允许非作者启动运行态".to_string()) diff --git a/server-rs/crates/spacetime-module/src/big_fish/session.rs b/server-rs/crates/spacetime-module/src/big_fish/session.rs index 359afe4fc..ea1239f8e 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/session.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/session.rs @@ -1,5 +1,5 @@ use crate::big_fish::tables::{ - big_fish_agent_message, big_fish_creation_session, big_fish_runtime_run, + WORK_VISIBLE_DEFAULT, big_fish_agent_message, big_fish_creation_session, big_fish_runtime_run, }; use crate::runtime::{ ProfilePlayedWorkUpsertInput, PublicWorkLikeRecordInput, PublicWorkPlayRecordInput, @@ -285,7 +285,7 @@ pub(crate) fn create_big_fish_session_tx( published_at: None, created_at, updated_at: created_at, - visible: true, + visible: WORK_VISIBLE_DEFAULT, }); ctx.db.big_fish_agent_message().insert(BigFishAgentMessage { message_id: input.welcome_message_id, @@ -338,7 +338,7 @@ pub(crate) fn list_big_fish_works_tx( .iter() .filter(|row| { if input.published_only { - return row.stage == BigFishCreationStage::Published; + return row.stage == BigFishCreationStage::Published && row.visible; } should_include_big_fish_work(ctx, row) @@ -698,7 +698,7 @@ pub(crate) fn record_big_fish_play_tx( .big_fish_creation_session() .session_id() .find(&input.session_id) - .filter(|row| row.stage == BigFishCreationStage::Published) + .filter(|row| row.stage == BigFishCreationStage::Published && row.visible) .ok_or_else(|| "big_fish 已发布作品不存在".to_string())?; let played_at = Timestamp::from_micros_since_unix_epoch(input.played_at_micros); let draft = session @@ -795,7 +795,7 @@ pub(crate) fn record_big_fish_like_tx( .big_fish_creation_session() .session_id() .find(&session_id.to_string()) - .filter(|row| row.stage == BigFishCreationStage::Published) + .filter(|row| row.stage == BigFishCreationStage::Published && row.visible) .ok_or_else(|| "big_fish 已发布作品不存在,无法点赞".to_string())?; let inserted_like = record_public_work_like( ctx, @@ -875,7 +875,7 @@ fn remix_big_fish_work_tx( .big_fish_creation_session() .session_id() .find(&source_session_id.to_string()) - .filter(|row| row.stage == BigFishCreationStage::Published) + .filter(|row| row.stage == BigFishCreationStage::Published && row.visible) .ok_or_else(|| "big_fish 已发布源作品不存在".to_string())?; let remixed_at = Timestamp::from_micros_since_unix_epoch(input.remixed_at_micros); let next_source = BigFishCreationSession { @@ -918,7 +918,7 @@ fn remix_big_fish_work_tx( published_at: None, created_at: remixed_at, updated_at: remixed_at, - visible: true, + visible: WORK_VISIBLE_DEFAULT, }; ctx.db.big_fish_creation_session().insert(target_session); ctx.db.big_fish_agent_message().insert(BigFishAgentMessage { diff --git a/server-rs/crates/spacetime-module/src/big_fish/tables.rs b/server-rs/crates/spacetime-module/src/big_fish/tables.rs index cf6ac376e..fe0acb0ca 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/tables.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/tables.rs @@ -1,6 +1,6 @@ use crate::*; -const WORK_VISIBLE_DEFAULT: bool = true; +pub(super) const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = big_fish_creation_session, @@ -30,7 +30,7 @@ pub struct BigFishCreationSession { pub(crate) like_count: u32, #[default(None::)] pub(crate) published_at: Option, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } diff --git a/server-rs/crates/spacetime-module/src/custom_world.rs b/server-rs/crates/spacetime-module/src/custom_world.rs index 646c12ac1..0df1376e2 100644 --- a/server-rs/crates/spacetime-module/src/custom_world.rs +++ b/server-rs/crates/spacetime-module/src/custom_world.rs @@ -2,7 +2,7 @@ use crate::*; use spacetimedb::AnonymousViewContext; use std::collections::{HashMap, HashSet}; -const WORK_VISIBLE_DEFAULT: bool = true; +const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = custom_world_profile, @@ -44,7 +44,7 @@ pub struct CustomWorldProfile { pub(crate) deleted_at: Option, pub(crate) created_at: Timestamp, pub(crate) updated_at: Timestamp, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } @@ -197,7 +197,7 @@ pub struct CustomWorldGalleryEntry { pub(crate) like_count: u32, pub(crate) published_at: Timestamp, pub(crate) updated_at: Timestamp, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } @@ -1210,6 +1210,8 @@ fn upsert_custom_world_profile_record( let next_row = match current { Some(existing) => { + let existing_was_published = + existing.publication_status == CustomWorldPublicationStatus::Published; ctx.db .custom_world_profile() .profile_id() @@ -1237,7 +1239,11 @@ fn upsert_custom_world_profile_record( deleted_at: None, created_at: existing.created_at, updated_at, - visible: existing.visible, + visible: if existing_was_published { + existing.visible + } else { + WORK_VISIBLE_DEFAULT + }, } } None => CustomWorldProfile { @@ -1263,7 +1269,7 @@ fn upsert_custom_world_profile_record( deleted_at: None, created_at: updated_at, updated_at, - visible: true, + visible: WORK_VISIBLE_DEFAULT, }, }; @@ -1379,6 +1385,8 @@ fn publish_custom_world_profile_record( .ok_or_else(|| "custom_world_profile 不存在,无法发布".to_string())?; let published_at = Timestamp::from_micros_since_unix_epoch(input.published_at_micros); + let existing_was_published = + existing.publication_status == CustomWorldPublicationStatus::Published; ctx.db .custom_world_profile() @@ -1411,7 +1419,11 @@ fn publish_custom_world_profile_record( deleted_at: None, created_at: existing.created_at, updated_at: published_at, - visible: existing.visible, + visible: if existing_was_published { + existing.visible + } else { + WORK_VISIBLE_DEFAULT + }, }; let inserted = ctx.db.custom_world_profile().insert(next_row); @@ -1813,7 +1825,7 @@ fn remix_custom_world_profile_record( deleted_at: None, created_at: remixed_at, updated_at: remixed_at, - visible: true, + visible: WORK_VISIBLE_DEFAULT, }; if let Some(existing_target) = ctx @@ -2597,7 +2609,6 @@ fn is_same_agent_draft_profile_candidate( ) -> bool { row.owner_user_id == owner_user_id && row.deleted_at.is_none() - && row.visible && row.publication_status == CustomWorldPublicationStatus::Draft && row.source_agent_session_id.as_deref() == Some(source_agent_session_id) } diff --git a/server-rs/crates/spacetime-module/src/jump_hop.rs b/server-rs/crates/spacetime-module/src/jump_hop.rs index ee865db2e..b6dbf5c48 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop.rs @@ -422,6 +422,18 @@ fn compile_jump_hop_draft_tx( .unwrap_or_else(|| JUMP_HOP_GENERATION_READY.to_string()), }; let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros); + let previous_published_work = ctx + .db + .jump_hop_work_profile() + .profile_id() + .find(&input.profile_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .filter(|row| row.publication_status == JUMP_HOP_PUBLICATION_PUBLISHED); + let previous_visible = previous_published_work + .as_ref() + .map(|row| row.visible) + .unwrap_or(WORK_VISIBLE_DEFAULT); + let previous_published_at = previous_published_work.and_then(|row| row.published_at); let row = JumpHopWorkProfileRow { profile_id: input.profile_id.clone(), work_id: input.profile_id.clone(), @@ -455,8 +467,8 @@ fn compile_jump_hop_draft_tx( publication_status: JUMP_HOP_PUBLICATION_DRAFT.to_string(), play_count: 0, updated_at: compiled_at, - published_at: None, - visible: true, + published_at: previous_published_at, + visible: previous_visible, theme_text: Some(draft.theme_text.clone()), }; upsert_work(ctx, row); @@ -542,6 +554,13 @@ fn publish_jump_hop_work_tx( publication_status: JUMP_HOP_PUBLICATION_PUBLISHED.to_string(), updated_at: published_at, published_at: Some(published_at), + visible: if row.publication_status == JUMP_HOP_PUBLICATION_PUBLISHED + || row.published_at.is_some() + { + row.visible + } else { + WORK_VISIBLE_DEFAULT + }, ..clone_work(&row) }, ); @@ -652,10 +671,12 @@ fn start_jump_hop_run_tx( if runtime_mode == JUMP_HOP_RUNTIME_MODE_DRAFT && work.owner_user_id != input.owner_user_id { return Err("jump_hop draft runtime 只能由作品所有者启动".to_string()); } - if runtime_mode == JUMP_HOP_RUNTIME_MODE_PUBLISHED - && work.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED - { - return Err("jump_hop published runtime 只能启动已发布作品".to_string()); + if runtime_mode == JUMP_HOP_RUNTIME_MODE_PUBLISHED { + let visible_to_player = work.publication_status == JUMP_HOP_PUBLICATION_PUBLISHED + && (work.visible || work.owner_user_id == input.owner_user_id); + if !visible_to_player { + return Err("jump_hop published runtime 只能启动已公开作品".to_string()); + } } let path = parse_json::(&work.path_json)?; let domain_run = start_run( @@ -743,8 +764,8 @@ fn get_jump_hop_leaderboard_tx( > { require_non_empty(&input.profile_id, "jump_hop profile_id")?; let work = find_work(ctx, &input.profile_id)?; - if work.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED { - return Err("jump_hop leaderboard 只开放已发布作品".to_string()); + if work.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED || !work.visible { + return Err("jump_hop leaderboard 只开放已公开作品".to_string()); } let limit = input.limit.clamp(1, 50) as usize; let mut rows = ctx diff --git a/server-rs/crates/spacetime-module/src/jump_hop/tables.rs b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs index c5b667717..38611c169 100644 --- a/server-rs/crates/spacetime-module/src/jump_hop/tables.rs +++ b/server-rs/crates/spacetime-module/src/jump_hop/tables.rs @@ -1,6 +1,6 @@ use crate::*; -const WORK_VISIBLE_DEFAULT: bool = true; +pub(super) const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = jump_hop_agent_session, @@ -53,7 +53,7 @@ pub struct JumpHopWorkProfileRow { pub(crate) play_count: u32, pub(crate) updated_at: Timestamp, pub(crate) published_at: Option, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, // 跳一跳生成主题独立于作品标题;旧行按 work_title 兜底。 diff --git a/server-rs/crates/spacetime-module/src/match3d.rs b/server-rs/crates/spacetime-module/src/match3d.rs index 649c6f8d5..4984520c5 100644 --- a/server-rs/crates/spacetime-module/src/match3d.rs +++ b/server-rs/crates/spacetime-module/src/match3d.rs @@ -541,6 +541,11 @@ fn compile_match3d_draft_tx( .map(|work| work.play_count) .unwrap_or(0); let previous_published_at = existing_work.as_ref().and_then(|work| work.published_at); + let previous_visible = existing_work + .as_ref() + .filter(|work| work.publication_status == MATCH3D_PUBLICATION_PUBLISHED) + .map(|work| work.visible) + .unwrap_or(WORK_VISIBLE_DEFAULT); let cover_image_src = resolve_compile_optional_text( &input.cover_image_src, existing_work @@ -572,7 +577,7 @@ fn compile_match3d_draft_tx( updated_at: compiled_at, published_at: previous_published_at, generated_item_assets_json, - visible: true, + visible: previous_visible, }; upsert_work(ctx, work); replace_session( @@ -661,6 +666,13 @@ fn publish_match3d_work_tx( publication_status: MATCH3D_PUBLICATION_PUBLISHED.to_string(), updated_at: published_at, published_at: Some(published_at), + visible: if current.publication_status == MATCH3D_PUBLICATION_PUBLISHED + || current.published_at.is_some() + { + current.visible + } else { + WORK_VISIBLE_DEFAULT + }, ..clone_work(¤t) }; let snapshot = build_work_snapshot(&next)?; @@ -731,7 +743,7 @@ fn get_match3d_work_detail_tx( .find(&input.profile_id) .filter(|row| { row.owner_user_id == input.owner_user_id - || row.publication_status == MATCH3D_PUBLICATION_PUBLISHED + || (row.publication_status == MATCH3D_PUBLICATION_PUBLISHED && row.visible) }) .ok_or_else(|| "match_3_d_work_profile 不存在".to_string())?; build_work_snapshot(&row) @@ -786,7 +798,7 @@ fn start_match3d_run_tx( .find(&input.profile_id) .filter(|row| { row.owner_user_id == input.owner_user_id - || row.publication_status == MATCH3D_PUBLICATION_PUBLISHED + || (row.publication_status == MATCH3D_PUBLICATION_PUBLISHED && row.visible) }) .ok_or_else(|| "match_3_d_work_profile 不存在".to_string())?; let started_at_ms = if input.started_at_ms > 0 { diff --git a/server-rs/crates/spacetime-module/src/match3d/tables.rs b/server-rs/crates/spacetime-module/src/match3d/tables.rs index bc7d131a0..00f89afe3 100644 --- a/server-rs/crates/spacetime-module/src/match3d/tables.rs +++ b/server-rs/crates/spacetime-module/src/match3d/tables.rs @@ -1,6 +1,6 @@ use crate::*; -const WORK_VISIBLE_DEFAULT: bool = true; +pub(super) const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = match3d_agent_session, @@ -62,7 +62,7 @@ pub struct Match3DWorkProfileRow { pub(crate) published_at: Option, #[default(None::)] pub(crate) generated_item_assets_json: Option, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } diff --git a/server-rs/crates/spacetime-module/src/migration.rs b/server-rs/crates/spacetime-module/src/migration.rs index ff23fe2db..a05a54030 100644 --- a/server-rs/crates/spacetime-module/src/migration.rs +++ b/server-rs/crates/spacetime-module/src/migration.rs @@ -1272,7 +1272,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde } if table_name == "custom_world_profile" || table_name == "custom_world_gallery_entry" { if let Some(object) = next_value.as_object_mut() { - // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。 + // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包保留历史公开默认。 object .entry("visible".to_string()) .or_insert_with(|| serde_json::Value::Bool(true)); @@ -1290,7 +1290,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde } if table_name == "puzzle_work_profile" { if let Some(object) = next_value.as_object_mut() { - // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。 + // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包保留历史公开默认。 object .entry("visible".to_string()) .or_insert_with(|| serde_json::Value::Bool(true)); @@ -1333,7 +1333,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde } if table_name == "big_fish_creation_session" { if let Some(object) = next_value.as_object_mut() { - // 中文注释:作品可见性字段晚于大鱼吃小鱼创作会话表加入,旧迁移包按默认显示兼容。 + // 中文注释:作品可见性字段晚于大鱼吃小鱼创作会话表加入,旧迁移包保留历史公开默认。 object .entry("visible".to_string()) .or_insert_with(|| serde_json::Value::Bool(true)); @@ -1348,7 +1348,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde | "bark_battle_published_config" ) { if let Some(object) = next_value.as_object_mut() { - // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。 + // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包保留历史公开默认。 object .entry("visible".to_string()) .or_insert_with(|| serde_json::Value::Bool(true)); @@ -1368,7 +1368,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde } if table_name == "match_3_d_work_profile" || table_name == "match3d_work_profile" { if let Some(object) = next_value.as_object_mut() { - // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。 + // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包保留历史公开默认。 object .entry("visible".to_string()) .or_insert_with(|| serde_json::Value::Bool(true)); @@ -1380,7 +1380,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde } if table_name == "wooden_fish_work_profile" { if let Some(object) = next_value.as_object_mut() { - // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包按默认显示兼容。 + // 中文注释:作品可见性字段晚于首版作品表加入,旧迁移包保留历史公开默认。 object .entry("visible".to_string()) .or_insert_with(|| serde_json::Value::Bool(true)); diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index 2a568294e..9ac0dc5c8 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -42,7 +42,7 @@ use crate::validate_external_generation_job_lease_for_tx; const PUZZLE_POINT_INCENTIVE_DEFAULT_U64: u64 = 0; const PUZZLE_BACKGROUND_COMPILE_TASK_LEASE_MICROS: i64 = 30 * 60 * 1_000_000; -const WORK_VISIBLE_DEFAULT: bool = true; +const WORK_VISIBLE_DEFAULT: bool = false; const PUZZLE_EXTERNAL_GENERATION_SOURCE_MODULE: &str = "puzzle"; const PUZZLE_COMPILE_DRAFT_JOB_KIND: &str = "puzzle_compile_draft"; const PUZZLE_GENERATE_IMAGES_JOB_KIND: &str = "puzzle_generate_images"; @@ -137,7 +137,7 @@ pub struct PuzzleWorkProfileRow { pub(crate) point_incentive_total_half_points: u64, #[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)] pub(crate) point_incentive_claimed_points: u64, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } @@ -2402,7 +2402,7 @@ fn remix_puzzle_work_tx( created_at: remixed_at, updated_at: remixed_at, published_at: None, - visible: true, + visible: WORK_VISIBLE_DEFAULT, }); get_puzzle_agent_session_tx( @@ -3445,6 +3445,9 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re .profile_id() .find(&profile.profile_id) { + let existing_was_published = + existing.publication_status == PuzzlePublicationStatus::Published; + let next_is_published = profile.publication_status == PuzzlePublicationStatus::Published; replace_puzzle_work_profile( ctx, &existing, @@ -3481,7 +3484,11 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re published_at: profile .published_at_micros .map(Timestamp::from_micros_since_unix_epoch), - visible: existing.visible, + visible: if existing_was_published && next_is_published { + existing.visible + } else { + WORK_VISIBLE_DEFAULT + }, }, ); return Ok(()); @@ -3514,7 +3521,7 @@ fn upsert_puzzle_work_profile(ctx: &TxContext, profile: PuzzleWorkProfile) -> Re published_at: profile .published_at_micros .map(Timestamp::from_micros_since_unix_epoch), - visible: true, + visible: WORK_VISIBLE_DEFAULT, }); Ok(()) } diff --git a/server-rs/crates/spacetime-module/src/puzzle_clear.rs b/server-rs/crates/spacetime-module/src/puzzle_clear.rs index 62b7a42a3..7000a086c 100644 --- a/server-rs/crates/spacetime-module/src/puzzle_clear.rs +++ b/server-rs/crates/spacetime-module/src/puzzle_clear.rs @@ -411,6 +411,18 @@ fn compile_puzzle_clear_draft_tx( card_assets: card_assets.clone(), generation_status: generation_status.clone(), }; + let previous_published_work = ctx + .db + .puzzle_clear_work_profile() + .profile_id() + .find(&input.profile_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .filter(|row| row.publication_status == PUZZLE_CLEAR_PUBLICATION_PUBLISHED); + let previous_visible = previous_published_work + .as_ref() + .map(|row| row.visible) + .unwrap_or(WORK_VISIBLE_DEFAULT); + let previous_published_at = previous_published_work.and_then(|row| row.published_at); let row = PuzzleClearWorkProfileRow { profile_id: input.profile_id.clone(), work_id: input.profile_id.clone(), @@ -435,8 +447,8 @@ fn compile_puzzle_clear_draft_tx( publication_status: PUZZLE_CLEAR_PUBLICATION_DRAFT.to_string(), play_count: 0, updated_at: compiled_at, - published_at: None, - visible: true, + published_at: previous_published_at, + visible: previous_visible, }; upsert_work(ctx, row); replace_session( @@ -651,6 +663,13 @@ fn publish_puzzle_clear_work_tx( publication_status: PUZZLE_CLEAR_PUBLICATION_PUBLISHED.to_string(), updated_at: published_at, published_at: Some(published_at), + visible: if row.publication_status == PUZZLE_CLEAR_PUBLICATION_PUBLISHED + || row.published_at.is_some() + { + row.visible + } else { + WORK_VISIBLE_DEFAULT + }, ..clone_work(&row) }, ); @@ -713,6 +732,9 @@ fn start_puzzle_clear_runtime_run_tx( if work.publication_status != PUZZLE_CLEAR_PUBLICATION_PUBLISHED { return Err("拼消消 runtime 只能启动已发布作品".to_string()); } + if work.owner_user_id != input.owner_user_id && !work.visible { + return Err("拼消消 runtime 只能启动已公开作品".to_string()); + } let cards = domain_cards_from_work(&work)?; let (board, deck) = build_level_board_and_deck(1, &work.profile_id, &cards)?; let domain_run = start_puzzle_clear_run( diff --git a/server-rs/crates/spacetime-module/src/puzzle_clear/tables.rs b/server-rs/crates/spacetime-module/src/puzzle_clear/tables.rs index bdae388ac..391ac787e 100644 --- a/server-rs/crates/spacetime-module/src/puzzle_clear/tables.rs +++ b/server-rs/crates/spacetime-module/src/puzzle_clear/tables.rs @@ -1,6 +1,6 @@ use crate::*; -const WORK_VISIBLE_DEFAULT: bool = true; +pub(super) const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = puzzle_clear_agent_session, @@ -46,7 +46,7 @@ pub struct PuzzleClearWorkProfileRow { pub(crate) play_count: u32, pub(crate) updated_at: Timestamp, pub(crate) published_at: Option, - // 中文注释:后台可见性开关,隐藏后不进入公开列表。 + // 中文注释:后台可见性开关,新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } diff --git a/server-rs/crates/spacetime-module/src/square_hole.rs b/server-rs/crates/spacetime-module/src/square_hole.rs index 27f16684c..210803df4 100644 --- a/server-rs/crates/spacetime-module/src/square_hole.rs +++ b/server-rs/crates/spacetime-module/src/square_hole.rs @@ -500,6 +500,18 @@ fn compile_square_hole_draft_tx( let cover_image_src = clean_optional(&input.cover_image_src) .or_else(|| clean_optional(&Some(config.cover_image_src.clone()))) .unwrap_or_default(); + let previous_published_work = ctx + .db + .square_hole_work_profile() + .profile_id() + .find(&input.profile_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .filter(|row| row.publication_status == SQUARE_HOLE_PUBLICATION_PUBLISHED); + let previous_visible = previous_published_work + .as_ref() + .map(|row| row.visible) + .unwrap_or(WORK_VISIBLE_DEFAULT); + let previous_published_at = previous_published_work.and_then(|row| row.published_at); let draft = SquareHoleDraftSnapshot { profile_id: input.profile_id.clone(), game_name: domain_draft.game_name.clone(), @@ -537,8 +549,8 @@ fn compile_square_hole_draft_tx( publication_status: SQUARE_HOLE_PUBLICATION_DRAFT.to_string(), play_count: 0, updated_at: compiled_at, - published_at: None, - visible: true, + published_at: previous_published_at, + visible: previous_visible, }; upsert_work(ctx, work); replace_session( @@ -634,6 +646,13 @@ fn publish_square_hole_work_tx( publication_status: SQUARE_HOLE_PUBLICATION_PUBLISHED.to_string(), updated_at: published_at, published_at: Some(published_at), + visible: if current.publication_status == SQUARE_HOLE_PUBLICATION_PUBLISHED + || current.published_at.is_some() + { + current.visible + } else { + WORK_VISIBLE_DEFAULT + }, ..clone_work(¤t) }; let snapshot = build_work_snapshot(&next)?; @@ -698,7 +717,7 @@ fn get_square_hole_work_detail_tx( .find(&input.profile_id) .ok_or_else(|| "方洞挑战作品不存在".to_string())?; if row.owner_user_id != input.owner_user_id - && row.publication_status != SQUARE_HOLE_PUBLICATION_PUBLISHED + && (row.publication_status != SQUARE_HOLE_PUBLICATION_PUBLISHED || !row.visible) { return Err("无权访问该方洞挑战作品".to_string()); } @@ -1033,7 +1052,7 @@ fn find_playable_work( .find(&profile_id.to_string()) .ok_or_else(|| "方洞挑战作品不存在".to_string())?; if row.owner_user_id != owner_user_id - && row.publication_status != SQUARE_HOLE_PUBLICATION_PUBLISHED + && (row.publication_status != SQUARE_HOLE_PUBLICATION_PUBLISHED || !row.visible) { return Err("无权试玩该方洞挑战作品".to_string()); } diff --git a/server-rs/crates/spacetime-module/src/square_hole/tables.rs b/server-rs/crates/spacetime-module/src/square_hole/tables.rs index 6be841ac6..f86e870b9 100644 --- a/server-rs/crates/spacetime-module/src/square_hole/tables.rs +++ b/server-rs/crates/spacetime-module/src/square_hole/tables.rs @@ -1,6 +1,6 @@ use crate::*; -const WORK_VISIBLE_DEFAULT: bool = true; +pub(super) const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = square_hole_agent_session, @@ -61,7 +61,7 @@ pub struct SquareHoleWorkProfileRow { pub(crate) play_count: u32, pub(crate) updated_at: Timestamp, pub(crate) published_at: Option, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } diff --git a/server-rs/crates/spacetime-module/src/visual_novel.rs b/server-rs/crates/spacetime-module/src/visual_novel.rs index 8ff05399b..d1929e02a 100644 --- a/server-rs/crates/spacetime-module/src/visual_novel.rs +++ b/server-rs/crates/spacetime-module/src/visual_novel.rs @@ -2,7 +2,7 @@ use crate::*; use serde::de::DeserializeOwned; use spacetimedb::AnonymousViewContext; -const WORK_VISIBLE_DEFAULT: bool = true; +const WORK_VISIBLE_DEFAULT: bool = false; pub const VISUAL_NOVEL_SOURCE_IDEA: &str = "idea"; pub const VISUAL_NOVEL_SOURCE_DOCUMENT: &str = "document"; @@ -95,7 +95,7 @@ pub struct VisualNovelWorkProfileRow { pub(crate) created_at: Timestamp, pub(crate) updated_at: Timestamp, pub(crate) published_at: Option, - // 管理端可见性开关;默认显示,隐藏后不进入广场列表。 + // 管理端可见性开关;新作品默认不公开,开启后才进入广场列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, } @@ -1015,6 +1015,18 @@ fn compile_visual_novel_work_profile_tx( .or_else(|| draft_string_field(&draft, "coverImageSrc")) .unwrap_or_default(); let publish_ready = draft_bool_field(&draft, "publishReady"); + let previous_published_work = ctx + .db + .visual_novel_work_profile() + .profile_id() + .find(&input.profile_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .filter(|row| row.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED); + let previous_visible = previous_published_work + .as_ref() + .map(|row| row.visible) + .unwrap_or(WORK_VISIBLE_DEFAULT); + let previous_published_at = previous_published_work.and_then(|row| row.published_at); let work = VisualNovelWorkProfileRow { profile_id: input.profile_id.clone(), @@ -1033,8 +1045,8 @@ fn compile_visual_novel_work_profile_tx( play_count: 0, created_at: compiled_at, updated_at: compiled_at, - published_at: None, - visible: true, + published_at: previous_published_at, + visible: previous_visible, }; upsert_work(ctx, work); replace_session( @@ -1095,6 +1107,13 @@ fn publish_visual_novel_work_tx( publication_status: VISUAL_NOVEL_PUBLICATION_PUBLISHED.to_string(), updated_at: published_at, published_at: Some(published_at), + visible: if current.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED + || current.published_at.is_some() + { + current.visible + } else { + WORK_VISIBLE_DEFAULT + }, ..clone_work(¤t) }; if !next.source_session_id.is_empty() { @@ -1165,7 +1184,7 @@ fn get_visual_novel_work_detail_tx( .find(&input.profile_id) .filter(|row| { row.owner_user_id == input.owner_user_id - || row.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED + || (row.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED && row.visible) }) .ok_or_else(|| "visual_novel_work_profile 不存在".to_string())?; build_work_snapshot(&row) @@ -1437,7 +1456,7 @@ fn find_readable_work( .find(&profile_id.to_string()) .filter(|row| { row.owner_user_id == owner_user_id - || row.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED + || (row.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED && row.visible) }) .ok_or_else(|| "visual_novel_work_profile 不存在".to_string()) } diff --git a/server-rs/crates/spacetime-module/src/wooden_fish.rs b/server-rs/crates/spacetime-module/src/wooden_fish.rs index bf0e04546..3049abd1f 100644 --- a/server-rs/crates/spacetime-module/src/wooden_fish.rs +++ b/server-rs/crates/spacetime-module/src/wooden_fish.rs @@ -401,6 +401,18 @@ fn compile_wooden_fish_draft_tx( .unwrap_or_else(|| WOODEN_FISH_GENERATION_READY.to_string()), }; let compiled_at = Timestamp::from_micros_since_unix_epoch(input.compiled_at_micros); + let previous_published_work = ctx + .db + .wooden_fish_work_profile() + .profile_id() + .find(&input.profile_id) + .filter(|row| row.owner_user_id == input.owner_user_id) + .filter(|row| row.publication_status == WOODEN_FISH_PUBLICATION_PUBLISHED); + let previous_visible = previous_published_work + .as_ref() + .map(|row| row.visible) + .unwrap_or(WORK_VISIBLE_DEFAULT); + let previous_published_at = previous_published_work.and_then(|row| row.published_at); let row = WoodenFishWorkProfileRow { profile_id: input.profile_id.clone(), work_id: input.profile_id.clone(), @@ -430,10 +442,10 @@ fn compile_wooden_fish_draft_tx( publication_status: WOODEN_FISH_PUBLICATION_DRAFT.to_string(), play_count: 0, updated_at: compiled_at, - published_at: None, + published_at: previous_published_at, background_asset_json: background_asset.as_ref().map(to_json_string), back_button_asset_json: back_button_asset.as_ref().map(to_json_string), - visible: true, + visible: previous_visible, }; upsert_work(ctx, row); let config = config_from_draft(&draft); @@ -567,6 +579,13 @@ fn publish_wooden_fish_work_tx( publication_status: WOODEN_FISH_PUBLICATION_PUBLISHED.to_string(), updated_at: published_at, published_at: Some(published_at), + visible: if row.publication_status == WOODEN_FISH_PUBLICATION_PUBLISHED + || row.published_at.is_some() + { + row.visible + } else { + WORK_VISIBLE_DEFAULT + }, ..clone_work(&row) }, ); @@ -680,6 +699,11 @@ fn start_wooden_fish_run_tx( require_non_empty(&input.run_id, "wooden_fish run_id")?; let stored_work = find_work(ctx, &input.profile_id)?; let work = backfill_historical_runtime_content(&stored_work); + if work.owner_user_id != input.owner_user_id + && (work.publication_status != WOODEN_FISH_PUBLICATION_PUBLISHED || !work.visible) + { + return Err("敲木鱼 runtime 只能启动已公开作品".to_string()); + } if !is_publish_ready(&work) { return Err("敲木鱼运行态需要完整作品配置".to_string()); } diff --git a/server-rs/crates/spacetime-module/src/wooden_fish/tables.rs b/server-rs/crates/spacetime-module/src/wooden_fish/tables.rs index 306f444d4..bb911b4ce 100644 --- a/server-rs/crates/spacetime-module/src/wooden_fish/tables.rs +++ b/server-rs/crates/spacetime-module/src/wooden_fish/tables.rs @@ -1,6 +1,6 @@ use crate::*; -const WORK_VISIBLE_DEFAULT: bool = true; +pub(super) const WORK_VISIBLE_DEFAULT: bool = false; #[spacetimedb::table( accessor = wooden_fish_agent_session, @@ -51,7 +51,7 @@ pub struct WoodenFishWorkProfileRow { pub(crate) background_asset_json: Option, #[default(None::)] pub(crate) back_button_asset_json: Option, - // 后台可见性开关;默认显示,隐藏后不进入公开列表。 + // 后台可见性开关;新作品默认不公开,开启后才进入公开列表。 #[default(WORK_VISIBLE_DEFAULT)] pub(crate) visible: bool, }