feat: add admin work visibility controls
This commit is contained in:
@@ -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,
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ pub struct BigFishCreationSession {
|
||||
pub(crate) like_count: u32,
|
||||
#[default(None::<Timestamp>)]
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
// ???????????????????????????????
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
@@ -14,39 +14,39 @@ const WORK_VISIBLE_DEFAULT: bool = true;
|
||||
)]
|
||||
pub struct CustomWorldProfile {
|
||||
#[primary_key]
|
||||
profile_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
// 当前 profile 承接 library / publish / enter-world 的正式世界工件真相。
|
||||
owner_user_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
// 作品公开编号是稳定分享键,第一次发布时分配,后续重复发布沿用。
|
||||
public_work_code: Option<String>,
|
||||
pub(crate) public_work_code: Option<String>,
|
||||
// 作者公开陶泥号在发布时固化到作品真相,供广场读模型与搜索结果直接展示。
|
||||
author_public_user_code: Option<String>,
|
||||
source_agent_session_id: Option<String>,
|
||||
publication_status: CustomWorldPublicationStatus,
|
||||
world_name: String,
|
||||
subtitle: String,
|
||||
summary_text: String,
|
||||
theme_mode: CustomWorldThemeMode,
|
||||
cover_image_src: Option<String>,
|
||||
profile_payload_json: String,
|
||||
playable_npc_count: u32,
|
||||
landmark_count: u32,
|
||||
pub(crate) author_public_user_code: Option<String>,
|
||||
pub(crate) source_agent_session_id: Option<String>,
|
||||
pub(crate) publication_status: CustomWorldPublicationStatus,
|
||||
pub(crate) world_name: String,
|
||||
pub(crate) subtitle: String,
|
||||
pub(crate) summary_text: String,
|
||||
pub(crate) theme_mode: CustomWorldThemeMode,
|
||||
pub(crate) cover_image_src: Option<String>,
|
||||
pub(crate) profile_payload_json: String,
|
||||
pub(crate) playable_npc_count: u32,
|
||||
pub(crate) landmark_count: u32,
|
||||
// 公开消费计数随 profile 真相持久化,发布、编辑和取消发布都不能重置。
|
||||
#[default(0)]
|
||||
play_count: u32,
|
||||
pub(crate) play_count: u32,
|
||||
#[default(0)]
|
||||
remix_count: u32,
|
||||
pub(crate) remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
author_display_name: String,
|
||||
published_at: Option<Timestamp>,
|
||||
pub(crate) like_count: u32,
|
||||
pub(crate) author_display_name: String,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
// 软删除后保留 profile 真相,供审计与幂等删除使用。
|
||||
deleted_at: Option<Timestamp>,
|
||||
created_at: Timestamp,
|
||||
updated_at: Timestamp,
|
||||
// ??????????????????????????
|
||||
pub(crate) deleted_at: Option<Timestamp>,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
visible: bool,
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
#[spacetimedb::table(
|
||||
@@ -175,31 +175,31 @@ pub struct CustomWorldDraftCard {
|
||||
)]
|
||||
pub struct CustomWorldGalleryEntry {
|
||||
#[primary_key]
|
||||
profile_id: String,
|
||||
pub(crate) profile_id: String,
|
||||
// 画廊是公开订阅读模型,不再运行时从 profile 即席拼装。
|
||||
owner_user_id: String,
|
||||
public_work_code: String,
|
||||
author_public_user_code: String,
|
||||
author_display_name: String,
|
||||
world_name: String,
|
||||
subtitle: String,
|
||||
summary_text: String,
|
||||
cover_image_src: Option<String>,
|
||||
theme_mode: CustomWorldThemeMode,
|
||||
playable_npc_count: u32,
|
||||
landmark_count: u32,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) public_work_code: String,
|
||||
pub(crate) author_public_user_code: String,
|
||||
pub(crate) author_display_name: String,
|
||||
pub(crate) world_name: String,
|
||||
pub(crate) subtitle: String,
|
||||
pub(crate) summary_text: String,
|
||||
pub(crate) cover_image_src: Option<String>,
|
||||
pub(crate) theme_mode: CustomWorldThemeMode,
|
||||
pub(crate) playable_npc_count: u32,
|
||||
pub(crate) landmark_count: u32,
|
||||
// 画廊读模型直接同步互动计数,避免前端临时把评分或游玩数改名成点赞。
|
||||
#[default(0)]
|
||||
play_count: u32,
|
||||
pub(crate) play_count: u32,
|
||||
#[default(0)]
|
||||
remix_count: u32,
|
||||
pub(crate) remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
published_at: Timestamp,
|
||||
updated_at: Timestamp,
|
||||
// ??????????????????????????
|
||||
pub(crate) like_count: u32,
|
||||
pub(crate) published_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
visible: bool,
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
// Agent 会话首版只负责把可持久化创作状态落进 SpacetimeDB,LLM 采集与卡片生成后续再接入。
|
||||
#[spacetimedb::procedure]
|
||||
|
||||
@@ -53,7 +53,7 @@ pub struct JumpHopWorkProfileRow {
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
// ???????????????????????????????
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ pub struct Match3DWorkProfileRow {
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) generated_item_assets_json: Option<String>,
|
||||
// ???????????????????????????????
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
@@ -1243,6 +1243,10 @@ 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));
|
||||
// 中文注释:自定义世界公开互动计数字段晚于基础作品表加入,旧迁移包按 0 兼容。
|
||||
object
|
||||
.entry("play_count".to_string())
|
||||
@@ -1257,7 +1261,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));
|
||||
@@ -1298,6 +1302,14 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
.or_insert(fallback_description);
|
||||
}
|
||||
}
|
||||
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));
|
||||
}
|
||||
}
|
||||
if matches!(
|
||||
table_name,
|
||||
"jump_hop_work_profile"
|
||||
@@ -1306,7 +1318,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));
|
||||
@@ -1314,7 +1326,7 @@ fn normalize_migration_row(table_name: &str, value: &serde_json::Value) -> serde
|
||||
}
|
||||
if 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));
|
||||
@@ -1326,7 +1338,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));
|
||||
|
||||
@@ -85,37 +85,37 @@ pub struct PuzzleAgentMessageRow {
|
||||
)]
|
||||
pub struct PuzzleWorkProfileRow {
|
||||
#[primary_key]
|
||||
profile_id: String,
|
||||
work_id: String,
|
||||
owner_user_id: String,
|
||||
source_session_id: Option<String>,
|
||||
author_display_name: String,
|
||||
work_title: String,
|
||||
work_description: String,
|
||||
level_name: String,
|
||||
summary: String,
|
||||
theme_tags_json: String,
|
||||
cover_image_src: Option<String>,
|
||||
cover_asset_id: Option<String>,
|
||||
levels_json: String,
|
||||
publication_status: PuzzlePublicationStatus,
|
||||
play_count: u32,
|
||||
anchor_pack_json: String,
|
||||
publish_ready: bool,
|
||||
created_at: Timestamp,
|
||||
updated_at: Timestamp,
|
||||
published_at: Option<Timestamp>,
|
||||
pub(crate) profile_id: String,
|
||||
pub(crate) work_id: String,
|
||||
pub(crate) owner_user_id: String,
|
||||
pub(crate) source_session_id: Option<String>,
|
||||
pub(crate) author_display_name: String,
|
||||
pub(crate) work_title: String,
|
||||
pub(crate) work_description: String,
|
||||
pub(crate) level_name: String,
|
||||
pub(crate) summary: String,
|
||||
pub(crate) theme_tags_json: String,
|
||||
pub(crate) cover_image_src: Option<String>,
|
||||
pub(crate) cover_asset_id: Option<String>,
|
||||
pub(crate) levels_json: String,
|
||||
pub(crate) publication_status: PuzzlePublicationStatus,
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) anchor_pack_json: String,
|
||||
pub(crate) publish_ready: bool,
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
#[default(0)]
|
||||
remix_count: u32,
|
||||
pub(crate) remix_count: u32,
|
||||
#[default(0)]
|
||||
like_count: u32,
|
||||
pub(crate) like_count: u32,
|
||||
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
||||
point_incentive_total_half_points: u64,
|
||||
pub(crate) point_incentive_total_half_points: u64,
|
||||
#[default(PUZZLE_POINT_INCENTIVE_DEFAULT_U64)]
|
||||
point_incentive_claimed_points: u64,
|
||||
// ???????????????????????????????
|
||||
pub(crate) point_incentive_claimed_points: u64,
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
visible: bool,
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
/// 拼图广场公开详情兼容投影。
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod analytics_date_dimension;
|
||||
mod admin_work_visibility;
|
||||
mod browse_history;
|
||||
pub mod creation_entry_config;
|
||||
mod profile;
|
||||
@@ -6,6 +7,7 @@ mod settings;
|
||||
mod snapshots;
|
||||
|
||||
pub use analytics_date_dimension::*;
|
||||
pub use admin_work_visibility::*;
|
||||
pub use browse_history::*;
|
||||
pub use creation_entry_config::*;
|
||||
pub use profile::*;
|
||||
|
||||
@@ -0,0 +1,708 @@
|
||||
use crate::*;
|
||||
use crate::puzzle::{PuzzleWorkProfileRow, puzzle_work_profile};
|
||||
use module_custom_world::CustomWorldPublicationStatus;
|
||||
use module_puzzle::PuzzlePublicationStatus;
|
||||
|
||||
const SOURCE_TYPE_PUZZLE: &str = "puzzle";
|
||||
const SOURCE_TYPE_CUSTOM_WORLD: &str = "custom-world";
|
||||
const SOURCE_TYPE_JUMP_HOP: &str = "jump-hop";
|
||||
const SOURCE_TYPE_WOODEN_FISH: &str = "wooden-fish";
|
||||
const SOURCE_TYPE_MATCH3D: &str = "match3d";
|
||||
const SOURCE_TYPE_SQUARE_HOLE: &str = "square-hole";
|
||||
const SOURCE_TYPE_VISUAL_NOVEL: &str = "visual-novel";
|
||||
const SOURCE_TYPE_BIG_FISH: &str = "big-fish";
|
||||
const SOURCE_TYPE_BARK_BATTLE: &str = "bark-battle";
|
||||
|
||||
/// 后台作品可见性列表。
|
||||
///
|
||||
/// 中文注释:后台必须能看到 hidden 作品,不能复用 public_work_* view,否则隐藏后无法恢复。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn admin_list_work_visibility(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: AdminWorkVisibilityListInput,
|
||||
) -> AdminWorkVisibilityListProcedureResult {
|
||||
match ctx.try_with_tx(|tx| list_work_visibility_tx(tx, input.clone())) {
|
||||
Ok(entries) => AdminWorkVisibilityListProcedureResult {
|
||||
ok: true,
|
||||
entries,
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => AdminWorkVisibilityListProcedureResult {
|
||||
ok: false,
|
||||
entries: Vec::new(),
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 后台修改单个作品可见性。
|
||||
#[spacetimedb::procedure]
|
||||
pub fn admin_update_work_visibility(
|
||||
ctx: &mut ProcedureContext,
|
||||
input: AdminWorkVisibilityUpdateInput,
|
||||
) -> AdminWorkVisibilityProcedureResult {
|
||||
match ctx.try_with_tx(|tx| update_work_visibility_tx(tx, input.clone())) {
|
||||
Ok(record) => AdminWorkVisibilityProcedureResult {
|
||||
ok: true,
|
||||
record: Some(record),
|
||||
error_message: None,
|
||||
},
|
||||
Err(message) => AdminWorkVisibilityProcedureResult {
|
||||
ok: false,
|
||||
record: None,
|
||||
error_message: Some(message),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn list_work_visibility_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: AdminWorkVisibilityListInput,
|
||||
) -> Result<Vec<AdminWorkVisibilitySnapshot>, String> {
|
||||
require_admin_user_id(&input.admin_user_id)?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
entries.extend(list_puzzle_work_visibility(ctx));
|
||||
entries.extend(list_custom_world_work_visibility(ctx));
|
||||
entries.extend(list_jump_hop_work_visibility(ctx));
|
||||
entries.extend(list_wooden_fish_work_visibility(ctx));
|
||||
entries.extend(list_match3d_work_visibility(ctx));
|
||||
entries.extend(list_square_hole_work_visibility(ctx));
|
||||
entries.extend(list_visual_novel_work_visibility(ctx));
|
||||
entries.extend(list_big_fish_work_visibility(ctx));
|
||||
entries.extend(list_bark_battle_work_visibility(ctx));
|
||||
sort_work_visibility_entries(&mut entries);
|
||||
Ok(entries)
|
||||
}
|
||||
|
||||
fn update_work_visibility_tx(
|
||||
ctx: &ReducerContext,
|
||||
input: AdminWorkVisibilityUpdateInput,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
require_admin_user_id(&input.admin_user_id)?;
|
||||
let source_type = normalize_source_type(&input.source_type)?;
|
||||
let profile_id = normalize_required_text(&input.profile_id, "profileId")?;
|
||||
|
||||
match source_type.as_str() {
|
||||
SOURCE_TYPE_PUZZLE => update_puzzle_work_visibility(ctx, &profile_id, input.visible),
|
||||
SOURCE_TYPE_CUSTOM_WORLD => {
|
||||
update_custom_world_work_visibility(ctx, &profile_id, input.visible)
|
||||
}
|
||||
SOURCE_TYPE_JUMP_HOP => update_jump_hop_work_visibility(ctx, &profile_id, input.visible),
|
||||
SOURCE_TYPE_WOODEN_FISH => {
|
||||
update_wooden_fish_work_visibility(ctx, &profile_id, input.visible)
|
||||
}
|
||||
SOURCE_TYPE_MATCH3D => update_match3d_work_visibility(ctx, &profile_id, input.visible),
|
||||
SOURCE_TYPE_SQUARE_HOLE => update_square_hole_work_visibility(ctx, &profile_id, input.visible),
|
||||
SOURCE_TYPE_VISUAL_NOVEL => {
|
||||
update_visual_novel_work_visibility(ctx, &profile_id, input.visible)
|
||||
}
|
||||
SOURCE_TYPE_BIG_FISH => update_big_fish_work_visibility(ctx, &profile_id, input.visible),
|
||||
SOURCE_TYPE_BARK_BATTLE => {
|
||||
update_bark_battle_work_visibility(ctx, &profile_id, input.visible)
|
||||
}
|
||||
_ => Err(format!("不支持的作品类型:{source_type}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn list_puzzle_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.puzzle_work_profile()
|
||||
.by_puzzle_work_publication_status()
|
||||
.filter(PuzzlePublicationStatus::Published)
|
||||
.map(|row| puzzle_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_puzzle_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "拼图作品不存在".to_string())?;
|
||||
if row.publication_status != PuzzlePublicationStatus::Published {
|
||||
return Err("只能修改已发布拼图作品可见性".to_string());
|
||||
}
|
||||
let next = PuzzleWorkProfileRow { visible, ..row };
|
||||
ctx.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.delete(&next.profile_id);
|
||||
ctx.db.puzzle_work_profile().insert(next);
|
||||
let updated = ctx
|
||||
.db
|
||||
.puzzle_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "拼图作品可见性更新失败".to_string())?;
|
||||
Ok(puzzle_work_visibility_snapshot(&updated))
|
||||
}
|
||||
|
||||
fn puzzle_work_visibility_snapshot(row: &PuzzleWorkProfileRow) -> AdminWorkVisibilitySnapshot {
|
||||
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_PUZZLE.to_string(),
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
source_session_id: row.source_session_id.clone(),
|
||||
public_work_code: build_prefixed_public_work_code("PZ", &row.profile_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
title: choose_non_empty(&[row.work_title.as_str(), row.level_name.as_str()]),
|
||||
subtitle: "拼图关卡".to_string(),
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
visible: row.visible,
|
||||
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: sort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_custom_world_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.custom_world_profile()
|
||||
.by_custom_world_profile_publication_status()
|
||||
.filter(CustomWorldPublicationStatus::Published)
|
||||
.filter(|row| row.deleted_at.is_none())
|
||||
.map(|row| custom_world_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_custom_world_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "自定义世界作品不存在".to_string())?;
|
||||
if row.publication_status != CustomWorldPublicationStatus::Published || row.deleted_at.is_some()
|
||||
{
|
||||
return Err("只能修改已发布自定义世界作品可见性".to_string());
|
||||
}
|
||||
let next = CustomWorldProfile { visible, ..row };
|
||||
let snapshot = custom_world_work_visibility_snapshot(&next);
|
||||
let profile_id = next.profile_id.clone();
|
||||
ctx.db
|
||||
.custom_world_profile()
|
||||
.profile_id()
|
||||
.delete(&profile_id);
|
||||
ctx.db.custom_world_profile().insert(next);
|
||||
|
||||
if let Some(gallery) = ctx
|
||||
.db
|
||||
.custom_world_gallery_entry()
|
||||
.profile_id()
|
||||
.find(&profile_id)
|
||||
{
|
||||
let next_gallery = CustomWorldGalleryEntry { visible, ..gallery };
|
||||
let gallery_profile_id = next_gallery.profile_id.clone();
|
||||
ctx.db
|
||||
.custom_world_gallery_entry()
|
||||
.profile_id()
|
||||
.delete(&gallery_profile_id);
|
||||
ctx.db.custom_world_gallery_entry().insert(next_gallery);
|
||||
}
|
||||
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn custom_world_work_visibility_snapshot(row: &CustomWorldProfile) -> AdminWorkVisibilitySnapshot {
|
||||
let public_work_code = row
|
||||
.public_work_code
|
||||
.clone()
|
||||
.unwrap_or_else(|| build_custom_world_public_work_code(&row.profile_id));
|
||||
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_CUSTOM_WORLD.to_string(),
|
||||
work_id: format!("custom-world:{}", row.profile_id),
|
||||
profile_id: row.profile_id.clone(),
|
||||
source_session_id: row.source_agent_session_id.clone(),
|
||||
public_work_code,
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
title: row.world_name.clone(),
|
||||
subtitle: row.subtitle.clone(),
|
||||
cover_image_src: row.cover_image_src.clone(),
|
||||
visible: row.visible,
|
||||
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: sort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_jump_hop_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.jump_hop_work_profile()
|
||||
.by_jump_hop_work_publication_status()
|
||||
.filter(JUMP_HOP_PUBLICATION_PUBLISHED)
|
||||
.map(|row| jump_hop_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_jump_hop_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.jump_hop_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "跳一跳作品不存在".to_string())?;
|
||||
if row.publication_status != JUMP_HOP_PUBLICATION_PUBLISHED {
|
||||
return Err("只能修改已发布跳一跳作品可见性".to_string());
|
||||
}
|
||||
let next = JumpHopWorkProfileRow { visible, ..row };
|
||||
let snapshot = jump_hop_work_visibility_snapshot(&next);
|
||||
let profile_id = next.profile_id.clone();
|
||||
ctx.db
|
||||
.jump_hop_work_profile()
|
||||
.profile_id()
|
||||
.delete(&profile_id);
|
||||
ctx.db.jump_hop_work_profile().insert(next);
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn jump_hop_work_visibility_snapshot(row: &JumpHopWorkProfileRow) -> AdminWorkVisibilitySnapshot {
|
||||
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_JUMP_HOP.to_string(),
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||
public_work_code: build_prefixed_public_work_code("JH", &row.profile_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
title: row.work_title.clone(),
|
||||
subtitle: "跳一跳".to_string(),
|
||||
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||
visible: row.visible,
|
||||
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: sort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_wooden_fish_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.wooden_fish_work_profile()
|
||||
.by_wooden_fish_work_publication_status()
|
||||
.filter(WOODEN_FISH_PUBLICATION_PUBLISHED)
|
||||
.map(|row| wooden_fish_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_wooden_fish_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.wooden_fish_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "敲木鱼作品不存在".to_string())?;
|
||||
if row.publication_status != WOODEN_FISH_PUBLICATION_PUBLISHED {
|
||||
return Err("只能修改已发布敲木鱼作品可见性".to_string());
|
||||
}
|
||||
let next = WoodenFishWorkProfileRow { visible, ..row };
|
||||
let snapshot = wooden_fish_work_visibility_snapshot(&next);
|
||||
let profile_id = next.profile_id.clone();
|
||||
ctx.db
|
||||
.wooden_fish_work_profile()
|
||||
.profile_id()
|
||||
.delete(&profile_id);
|
||||
ctx.db.wooden_fish_work_profile().insert(next);
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn wooden_fish_work_visibility_snapshot(
|
||||
row: &WoodenFishWorkProfileRow,
|
||||
) -> AdminWorkVisibilitySnapshot {
|
||||
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_WOODEN_FISH.to_string(),
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||
public_work_code: build_prefixed_public_work_code("WF", &row.profile_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
title: row.work_title.clone(),
|
||||
subtitle: "敲木鱼".to_string(),
|
||||
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||
visible: row.visible,
|
||||
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: sort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_match3d_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.match3d_work_profile()
|
||||
.by_match3d_work_publication_status()
|
||||
.filter(MATCH3D_PUBLICATION_PUBLISHED)
|
||||
.map(|row| match3d_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_match3d_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.match3d_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "抓大鹅作品不存在".to_string())?;
|
||||
if row.publication_status != MATCH3D_PUBLICATION_PUBLISHED {
|
||||
return Err("只能修改已发布抓大鹅作品可见性".to_string());
|
||||
}
|
||||
let next = Match3DWorkProfileRow { visible, ..row };
|
||||
let snapshot = match3d_work_visibility_snapshot(&next);
|
||||
let profile_id = next.profile_id.clone();
|
||||
ctx.db
|
||||
.match3d_work_profile()
|
||||
.profile_id()
|
||||
.delete(&profile_id);
|
||||
ctx.db.match3d_work_profile().insert(next);
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn match3d_work_visibility_snapshot(row: &Match3DWorkProfileRow) -> AdminWorkVisibilitySnapshot {
|
||||
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_MATCH3D.to_string(),
|
||||
work_id: row.profile_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||
public_work_code: build_prefixed_public_work_code("M3", &row.profile_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
title: row.game_name.clone(),
|
||||
subtitle: "抓大鹅".to_string(),
|
||||
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||
visible: row.visible,
|
||||
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: sort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_square_hole_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.square_hole_work_profile()
|
||||
.by_square_hole_work_publication_status()
|
||||
.filter(SQUARE_HOLE_PUBLICATION_PUBLISHED)
|
||||
.map(|row| square_hole_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_square_hole_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.square_hole_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "方洞挑战作品不存在".to_string())?;
|
||||
if row.publication_status != SQUARE_HOLE_PUBLICATION_PUBLISHED {
|
||||
return Err("只能修改已发布方洞挑战作品可见性".to_string());
|
||||
}
|
||||
let next = SquareHoleWorkProfileRow { visible, ..row };
|
||||
let snapshot = square_hole_work_visibility_snapshot(&next);
|
||||
let profile_id = next.profile_id.clone();
|
||||
ctx.db
|
||||
.square_hole_work_profile()
|
||||
.profile_id()
|
||||
.delete(&profile_id);
|
||||
ctx.db.square_hole_work_profile().insert(next);
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn square_hole_work_visibility_snapshot(
|
||||
row: &SquareHoleWorkProfileRow,
|
||||
) -> AdminWorkVisibilitySnapshot {
|
||||
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_SQUARE_HOLE.to_string(),
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||
public_work_code: build_prefixed_public_work_code("SH", &row.profile_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
title: row.game_name.clone(),
|
||||
subtitle: "方洞挑战".to_string(),
|
||||
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||
visible: row.visible,
|
||||
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: sort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_visual_novel_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.visual_novel_work_profile()
|
||||
.by_visual_novel_work_publication_status()
|
||||
.filter(VISUAL_NOVEL_PUBLICATION_PUBLISHED)
|
||||
.map(|row| visual_novel_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_visual_novel_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.visual_novel_work_profile()
|
||||
.profile_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "视觉小说作品不存在".to_string())?;
|
||||
if row.publication_status != VISUAL_NOVEL_PUBLICATION_PUBLISHED {
|
||||
return Err("只能修改已发布视觉小说作品可见性".to_string());
|
||||
}
|
||||
let next = VisualNovelWorkProfileRow { visible, ..row };
|
||||
let snapshot = visual_novel_work_visibility_snapshot(&next);
|
||||
let profile_id = next.profile_id.clone();
|
||||
ctx.db
|
||||
.visual_novel_work_profile()
|
||||
.profile_id()
|
||||
.delete(&profile_id);
|
||||
ctx.db.visual_novel_work_profile().insert(next);
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn visual_novel_work_visibility_snapshot(
|
||||
row: &VisualNovelWorkProfileRow,
|
||||
) -> AdminWorkVisibilitySnapshot {
|
||||
let sort_time = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_VISUAL_NOVEL.to_string(),
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.profile_id.clone(),
|
||||
source_session_id: Some(row.source_session_id.clone()).filter(|value| !value.is_empty()),
|
||||
public_work_code: build_prefixed_public_work_code("VN", &row.profile_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: row.author_display_name.clone(),
|
||||
title: row.work_title.clone(),
|
||||
subtitle: "视觉小说".to_string(),
|
||||
cover_image_src: Some(row.cover_image_src.clone()).filter(|value| !value.is_empty()),
|
||||
visible: row.visible,
|
||||
published_at_micros: row.published_at.map(|value| value.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: sort_time,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_big_fish_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.big_fish_creation_session()
|
||||
.by_big_fish_session_stage()
|
||||
.filter(BigFishCreationStage::Published)
|
||||
.map(|row| big_fish_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_big_fish_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "大鱼吃小鱼作品不存在".to_string())?;
|
||||
if row.stage != BigFishCreationStage::Published {
|
||||
return Err("只能修改已发布大鱼吃小鱼作品可见性".to_string());
|
||||
}
|
||||
let next = BigFishCreationSession { visible, ..row };
|
||||
let snapshot = big_fish_work_visibility_snapshot(&next);
|
||||
let session_id = next.session_id.clone();
|
||||
ctx.db
|
||||
.big_fish_creation_session()
|
||||
.session_id()
|
||||
.delete(&session_id);
|
||||
ctx.db.big_fish_creation_session().insert(next);
|
||||
Ok(snapshot)
|
||||
}
|
||||
|
||||
fn big_fish_work_visibility_snapshot(
|
||||
row: &BigFishCreationSession,
|
||||
) -> AdminWorkVisibilitySnapshot {
|
||||
let published_at = row.published_at.map(|value| value.to_micros_since_unix_epoch());
|
||||
let updated_at = timestamp_sort_micros(row.published_at, row.updated_at);
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_BIG_FISH.to_string(),
|
||||
work_id: row.session_id.clone(),
|
||||
profile_id: row.session_id.clone(),
|
||||
source_session_id: Some(row.session_id.clone()),
|
||||
public_work_code: build_prefixed_public_work_code("BF", &row.session_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: "玩家".to_string(),
|
||||
title: "大鱼吃小鱼".to_string(),
|
||||
subtitle: "成长挑战".to_string(),
|
||||
cover_image_src: None,
|
||||
visible: row.visible,
|
||||
published_at_micros: published_at,
|
||||
updated_at_micros: updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn list_bark_battle_work_visibility(ctx: &ReducerContext) -> Vec<AdminWorkVisibilitySnapshot> {
|
||||
ctx.db
|
||||
.bark_battle_published_config()
|
||||
.iter()
|
||||
.map(|row| bark_battle_work_visibility_snapshot(&row))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn update_bark_battle_work_visibility(
|
||||
ctx: &ReducerContext,
|
||||
profile_id: &str,
|
||||
visible: bool,
|
||||
) -> Result<AdminWorkVisibilitySnapshot, String> {
|
||||
let row = ctx
|
||||
.db
|
||||
.bark_battle_published_config()
|
||||
.work_id()
|
||||
.find(&profile_id.to_string())
|
||||
.ok_or_else(|| "汪汪声浪作品不存在".to_string())?;
|
||||
let next = BarkBattlePublishedConfigRow { visible, ..row };
|
||||
ctx.db.bark_battle_published_config().delete(next.clone());
|
||||
ctx.db.bark_battle_published_config().insert(next.clone());
|
||||
Ok(bark_battle_work_visibility_snapshot(&next))
|
||||
}
|
||||
|
||||
fn bark_battle_work_visibility_snapshot(
|
||||
row: &BarkBattlePublishedConfigRow,
|
||||
) -> AdminWorkVisibilitySnapshot {
|
||||
AdminWorkVisibilitySnapshot {
|
||||
source_type: SOURCE_TYPE_BARK_BATTLE.to_string(),
|
||||
work_id: row.work_id.clone(),
|
||||
profile_id: row.work_id.clone(),
|
||||
source_session_id: row.source_draft_id.clone(),
|
||||
public_work_code: build_bark_battle_public_work_code(&row.work_id),
|
||||
owner_user_id: row.owner_user_id.clone(),
|
||||
author_display_name: "玩家".to_string(),
|
||||
title: "汪汪声浪".to_string(),
|
||||
subtitle: row.difficulty_preset.clone(),
|
||||
cover_image_src: None,
|
||||
visible: row.visible,
|
||||
published_at_micros: Some(row.published_at.to_micros_since_unix_epoch()),
|
||||
updated_at_micros: timestamp_sort_micros(Some(row.published_at), row.updated_at),
|
||||
}
|
||||
}
|
||||
|
||||
fn require_admin_user_id(value: &str) -> Result<(), String> {
|
||||
normalize_required_text(value, "adminUserId").map(|_| ())
|
||||
}
|
||||
|
||||
fn normalize_source_type(value: &str) -> Result<String, String> {
|
||||
let normalized = normalize_required_text(value, "sourceType")?
|
||||
.to_ascii_lowercase()
|
||||
.replace('_', "-");
|
||||
let source_type = match normalized.as_str() {
|
||||
"match-3-d" | "match-3d" | "match3-d" => SOURCE_TYPE_MATCH3D,
|
||||
other => other,
|
||||
};
|
||||
Ok(source_type.to_string())
|
||||
}
|
||||
|
||||
fn normalize_required_text(value: &str, field_name: &str) -> Result<String, String> {
|
||||
let normalized = value.trim();
|
||||
if normalized.is_empty() {
|
||||
return Err(format!("{field_name} 不能为空"));
|
||||
}
|
||||
Ok(normalized.to_string())
|
||||
}
|
||||
|
||||
fn sort_work_visibility_entries(entries: &mut [AdminWorkVisibilitySnapshot]) {
|
||||
entries.sort_by(|left, right| {
|
||||
right
|
||||
.updated_at_micros
|
||||
.cmp(&left.updated_at_micros)
|
||||
.then_with(|| left.source_type.cmp(&right.source_type))
|
||||
.then_with(|| left.profile_id.cmp(&right.profile_id))
|
||||
});
|
||||
}
|
||||
|
||||
fn timestamp_sort_micros(published_at: Option<Timestamp>, updated_at: Timestamp) -> i64 {
|
||||
published_at
|
||||
.unwrap_or(updated_at)
|
||||
.to_micros_since_unix_epoch()
|
||||
}
|
||||
|
||||
fn build_prefixed_public_work_code(prefix: &str, value: &str) -> String {
|
||||
let normalized = normalize_public_code_text(value);
|
||||
let fallback = if normalized.is_empty() {
|
||||
"00000000".to_string()
|
||||
} else {
|
||||
normalized
|
||||
};
|
||||
let suffix = last_eight_padded(&fallback);
|
||||
format!("{prefix}-{suffix}")
|
||||
}
|
||||
|
||||
fn build_bark_battle_public_work_code(work_id: &str) -> String {
|
||||
let normalized = normalize_public_code_text(work_id);
|
||||
let without_prefix = normalized
|
||||
.strip_prefix("BB")
|
||||
.map(ToString::to_string)
|
||||
.unwrap_or_else(|| normalized.clone());
|
||||
let fallback = if without_prefix.is_empty() {
|
||||
if normalized.is_empty() {
|
||||
"00000000".to_string()
|
||||
} else {
|
||||
normalized
|
||||
}
|
||||
} else {
|
||||
without_prefix
|
||||
};
|
||||
format!("BB-{}", last_eight_padded(&fallback))
|
||||
}
|
||||
|
||||
fn normalize_public_code_text(value: &str) -> String {
|
||||
value
|
||||
.trim()
|
||||
.chars()
|
||||
.filter(|character| character.is_ascii_alphanumeric())
|
||||
.flat_map(char::to_uppercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn last_eight_padded(value: &str) -> String {
|
||||
let suffix = value
|
||||
.chars()
|
||||
.rev()
|
||||
.take(8)
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.rev()
|
||||
.collect::<String>();
|
||||
format!("{suffix:0>8}")
|
||||
}
|
||||
|
||||
fn choose_non_empty(values: &[&str]) -> String {
|
||||
values
|
||||
.iter()
|
||||
.map(|value| value.trim())
|
||||
.find(|value| !value.is_empty())
|
||||
.unwrap_or_default()
|
||||
.to_string()
|
||||
}
|
||||
@@ -61,7 +61,7 @@ pub struct SquareHoleWorkProfileRow {
|
||||
pub(crate) play_count: u32,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
// ???????????????????????????????
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ pub struct VisualNovelWorkProfileRow {
|
||||
pub(crate) created_at: Timestamp,
|
||||
pub(crate) updated_at: Timestamp,
|
||||
pub(crate) published_at: Option<Timestamp>,
|
||||
// ???????????????????????????????
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ pub struct WoodenFishWorkProfileRow {
|
||||
pub(crate) background_asset_json: Option<String>,
|
||||
#[default(None::<String>)]
|
||||
pub(crate) back_button_asset_json: Option<String>,
|
||||
// ???????????????????????????????
|
||||
// 后台可见性开关;默认显示,隐藏后不进入公开列表。
|
||||
#[default(WORK_VISIBLE_DEFAULT)]
|
||||
pub(crate) visible: bool,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user