点赞和改造开关加入后台配置

This commit is contained in:
2026-06-10 14:36:56 +08:00
parent 9db467d23f
commit e29992cf01
33 changed files with 1644 additions and 380 deletions

View File

@@ -11,7 +11,7 @@ use crate::errors::RuntimeProfileFieldError;
use crate::format_utc_micros;
use shared_contracts::creation_entry_config::{
CreationEntryConfigResponse, CreationEntryEventBannerResponse, CreationEntryStartCardResponse,
CreationEntryTypeModalResponse, CreationEntryTypeResponse,
CreationEntryTypeModalResponse, CreationEntryTypeResponse, PublicWorkInteractionConfigResponse,
encode_unified_creation_spec_response, resolve_unified_creation_spec_response,
};
@@ -27,6 +27,9 @@ pub fn build_creation_entry_config_response(
.first()
.cloned()
.unwrap_or_else(|| build_creation_entry_event_banner_response(snapshot.event_banner));
let public_work_interactions = resolve_public_work_interaction_config_responses(
snapshot.public_work_interactions_json.as_deref(),
);
CreationEntryConfigResponse {
start_card: CreationEntryStartCardResponse {
@@ -41,6 +44,7 @@ pub fn build_creation_entry_config_response(
},
event_banner,
event_banners,
public_work_interactions,
creation_types: snapshot
.creation_types
.into_iter()
@@ -69,6 +73,223 @@ pub fn build_creation_entry_config_response(
}
}
/// 返回公开作品点赞 / 改造默认矩阵,保持历史前端硬编码能力不变。
pub fn default_public_work_interaction_config_snapshots() -> Vec<PublicWorkInteractionConfigSnapshot>
{
vec![
public_work_interaction_config(
"custom-world",
true,
true,
"RPG 作品暂不支持点赞。",
"RPG 作品暂不支持改造。",
),
public_work_interaction_config(
"big-fish",
true,
true,
"摸鱼点赞暂不可用。",
"摸鱼作品改造暂不可用。",
),
public_work_interaction_config(
"puzzle",
true,
true,
"拼图点赞暂不可用。",
"拼图作品改造暂不可用。",
),
public_work_interaction_config(
"puzzle-clear",
false,
false,
"拼消消点赞将在后续版本开放。",
"拼消消作品改造将在后续版本开放。",
),
public_work_interaction_config(
"jump-hop",
false,
false,
"作品类型 jump-hop 暂不支持点赞。",
"跳一跳作品改造将在后续版本开放。",
),
public_work_interaction_config(
"wooden-fish",
false,
false,
"作品类型 wooden-fish 暂不支持点赞。",
"敲木鱼作品改造将在后续版本开放。",
),
public_work_interaction_config(
"match3d",
false,
false,
"作品类型 match3d 暂不支持点赞。",
"抓大鹅作品改造将在后续版本开放。",
),
public_work_interaction_config(
"square-hole",
false,
false,
"方洞挑战点赞将在后续版本开放。",
"方洞挑战作品改造将在后续版本开放。",
),
public_work_interaction_config(
"visual-novel",
false,
false,
"视觉小说点赞将在后续版本开放。",
"视觉小说作品改造将在后续版本开放。",
),
public_work_interaction_config(
"bark-battle",
false,
false,
"汪汪声浪点赞将在后续版本开放。",
"汪汪声浪作品改造将在后续版本开放。",
),
public_work_interaction_config(
"edutainment",
false,
false,
"宝贝识物点赞将在后续版本开放。",
"宝贝识物作品改造将在创作链路接入后开放。",
),
]
}
fn public_work_interaction_config(
source_type: &str,
like_enabled: bool,
remix_enabled: bool,
like_disabled_message: &str,
remix_disabled_message: &str,
) -> PublicWorkInteractionConfigSnapshot {
PublicWorkInteractionConfigSnapshot {
source_type: source_type.to_string(),
like_enabled,
remix_enabled,
like_disabled_message: like_disabled_message.to_string(),
remix_disabled_message: remix_disabled_message.to_string(),
}
}
/// 生成默认公开作品互动配置 JSON供 SpacetimeDB 表字段持久化。
pub fn default_public_work_interaction_config_json() -> String {
encode_public_work_interaction_config_snapshots(
&default_public_work_interaction_config_snapshots(),
)
.unwrap_or_else(|_| "[]".to_string())
}
/// 校验并归一后台公开作品互动配置 JSON。
pub fn normalize_public_work_interaction_config_json(input: &str) -> Result<String, String> {
let trimmed = input.trim();
if trimmed.is_empty() {
return Ok(default_public_work_interaction_config_json());
}
let configs = decode_public_work_interaction_config_snapshots(trimmed)?;
encode_public_work_interaction_config_snapshots(&configs)
}
/// 解析公开作品互动配置 JSON并补齐缺失 sourceType 的默认项。
pub fn decode_public_work_interaction_config_snapshots(
input: &str,
) -> Result<Vec<PublicWorkInteractionConfigSnapshot>, String> {
let raw_entries = serde_json::from_str::<Vec<PublicWorkInteractionConfigResponse>>(input)
.map_err(|error| format!("作品互动配置 JSON 非法:{error}"))?;
if raw_entries.len() > PUBLIC_WORK_INTERACTION_CONFIG_MAX_COUNT {
return Err(format!(
"作品互动配置最多允许 {}",
PUBLIC_WORK_INTERACTION_CONFIG_MAX_COUNT
));
}
let defaults = default_public_work_interaction_config_snapshots();
let default_by_source = defaults
.iter()
.map(|item| (item.source_type.clone(), item.clone()))
.collect::<BTreeMap<_, _>>();
let mut overrides = BTreeMap::<String, PublicWorkInteractionConfigSnapshot>::new();
for (index, entry) in raw_entries.into_iter().enumerate() {
let source_type = entry.source_type.trim().to_string();
let Some(default_entry) = default_by_source.get(&source_type) else {
return Err(format!("{} 条作品类型非法:{}", index + 1, source_type));
};
if overrides.contains_key(&source_type) {
return Err(format!("作品互动配置 sourceType 重复:{source_type}"));
}
overrides.insert(
source_type.clone(),
PublicWorkInteractionConfigSnapshot {
source_type,
like_enabled: entry.like_enabled,
remix_enabled: entry.remix_enabled,
like_disabled_message: normalize_interaction_message(
entry.like_disabled_message,
&default_entry.like_disabled_message,
),
remix_disabled_message: normalize_interaction_message(
entry.remix_disabled_message,
&default_entry.remix_disabled_message,
),
},
);
}
Ok(defaults
.into_iter()
.map(|item| overrides.remove(&item.source_type).unwrap_or(item))
.collect())
}
fn normalize_interaction_message(value: String, fallback: &str) -> String {
let trimmed = value.trim();
if trimmed.is_empty() {
fallback.to_string()
} else {
trimmed.to_string()
}
}
/// 把公开作品互动领域快照编码为稳定 JSON。
pub fn encode_public_work_interaction_config_snapshots(
configs: &[PublicWorkInteractionConfigSnapshot],
) -> Result<String, String> {
let responses = configs
.iter()
.cloned()
.map(build_public_work_interaction_config_response)
.collect::<Vec<_>>();
serde_json::to_string_pretty(&responses)
.map_err(|error| format!("作品互动配置 JSON 序列化失败:{error}"))
}
/// 根据持久化 JSON 得到前台可消费的公开作品互动矩阵。
pub fn resolve_public_work_interaction_config_responses(
public_work_interactions_json: Option<&str>,
) -> Vec<PublicWorkInteractionConfigResponse> {
public_work_interactions_json
.and_then(|raw| decode_public_work_interaction_config_snapshots(raw).ok())
.unwrap_or_else(default_public_work_interaction_config_snapshots)
.into_iter()
.map(build_public_work_interaction_config_response)
.collect()
}
pub fn build_public_work_interaction_config_response(
config: PublicWorkInteractionConfigSnapshot,
) -> PublicWorkInteractionConfigResponse {
PublicWorkInteractionConfigResponse {
source_type: config.source_type,
like_enabled: config.like_enabled,
remix_enabled: config.remix_enabled,
like_disabled_message: config.like_disabled_message,
remix_disabled_message: config.remix_disabled_message,
}
}
/// 返回平台默认公告配置,用于空库种子和旧库兜底。
pub fn default_creation_entry_event_banner_snapshots() -> Vec<CreationEntryEventBannerSnapshot> {
vec![CreationEntryEventBannerSnapshot {

View File

@@ -65,6 +65,8 @@ pub const DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT: &str = "2024.11.20 23:59";
pub const CREATION_ENTRY_EVENT_BANNERS_MAX_COUNT: usize = 8;
/// 单条 HTML 公告的代码大小上限,避免后台误贴超大片段拖慢入口页。
pub const CREATION_ENTRY_EVENT_BANNER_HTML_CODE_MAX_BYTES: usize = 12_000;
/// 公开作品互动配置最多允许覆盖的 sourceType 数量。
pub const PUBLIC_WORK_INTERACTION_CONFIG_MAX_COUNT: usize = 32;
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -96,6 +98,17 @@ pub struct CreationEntryEventBannerSnapshot {
pub html_code: Option<String>,
}
/// 单类公开作品互动配置,控制作品详情页点赞 / 改造入口与后端动作熔断。
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicWorkInteractionConfigSnapshot {
pub source_type: String,
pub like_enabled: bool,
pub remix_enabled: bool,
pub like_disabled_message: String,
pub remix_disabled_message: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryTypeSnapshot {
@@ -126,6 +139,8 @@ pub struct CreationEntryConfigSnapshot {
pub event_banners_json: Option<String>,
pub creation_types: Vec<CreationEntryTypeSnapshot>,
pub updated_at_micros: i64,
/// 公开作品点赞 / 改造能力 JSON 配置;旧库为空时由应用层兜底。
pub public_work_interactions_json: Option<String>,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
@@ -153,6 +168,14 @@ pub struct CreationEntryEventBannersAdminUpsertInput {
pub event_banners_json: String,
}
/// 后台保存公开作品互动能力表单序列化结果的领域输入。
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PublicWorkInteractionConfigAdminUpsertInput {
/// 持久化字段沿用 JSON 字符串,内容由后台表单生成。
pub public_work_interactions_json: String,
}
#[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))]
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct CreationEntryConfigProcedureResult {

View File

@@ -531,6 +531,7 @@ mod tests {
),
}],
updated_at_micros: 1,
public_work_interactions_json: Some(default_public_work_interaction_config_json()),
});
let puzzle = response
.creation_types
@@ -547,6 +548,37 @@ mod tests {
);
}
#[test]
fn public_work_interaction_config_defaults_and_overrides() {
let defaults = resolve_public_work_interaction_config_responses(None);
let puzzle = defaults
.iter()
.find(|item| item.source_type == "puzzle")
.expect("puzzle interaction should exist");
assert!(puzzle.like_enabled);
assert!(puzzle.remix_enabled);
let normalized = normalize_public_work_interaction_config_json(
r#"[{
"sourceType": "puzzle",
"likeEnabled": false,
"remixEnabled": true,
"likeDisabledMessage": "拼图点赞维护中。",
"remixDisabledMessage": ""
}]"#,
)
.expect("interaction config should normalize");
let resolved = resolve_public_work_interaction_config_responses(Some(&normalized));
let puzzle = resolved
.iter()
.find(|item| item.source_type == "puzzle")
.expect("puzzle interaction should exist");
assert!(!puzzle.like_enabled);
assert_eq!(puzzle.like_disabled_message, "拼图点赞维护中。");
assert_eq!(puzzle.remix_disabled_message, "拼图作品改造暂不可用。");
}
#[test]
fn normalized_clamps_music_volume_into_valid_range() {
let low = RuntimeSettings::normalized(-1.0, RuntimePlatformTheme::Light);