Files
Genarrative/server-rs/crates/spacetime-module/src/runtime/admin_work_visibility.rs
2026-05-28 15:42:46 +08:00

727 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use crate::puzzle::{PuzzleWorkProfileRow, puzzle_work_profile};
use crate::*;
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()
.iter()
// 中文注释:后台页签是低频管理入口,列表优先保证稳定性,避免二级索引 filter 初始化异常打爆 wasm 实例。
.filter(|row| row.publication_status == 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()
.iter()
// 中文注释:后台必须能读到所有已发布源表记录,包括已隐藏作品,因此不复用公开 view。
.filter(|row| row.publication_status == 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()
.iter()
.filter(|row| row.publication_status == 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()
.iter()
.filter(|row| row.publication_status == 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
.match_3_d_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
.match_3_d_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
.match_3_d_work_profile()
.profile_id()
.delete(&profile_id);
ctx.db.match_3_d_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()
.iter()
.filter(|row| row.publication_status == 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()
.iter()
.filter(|row| row.publication_status == 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()
.iter()
.filter(|row| row.stage == 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()
}