feat: 支持创作入口公告配置
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::creation_entry_config::UnifiedCreationSpecResponse;
|
||||
use crate::creation_entry_config::{CreationEntryEventBannerResponse, UnifiedCreationSpecResponse};
|
||||
|
||||
// 管理后台协议统一收口在 shared-contracts,避免页面脚本和 Rust handler 各自手拼字段。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -18,6 +18,8 @@ pub struct AdminLoginRequest {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminCreationEntryConfigResponse {
|
||||
pub entries: Vec<AdminCreationEntryTypeConfigPayload>,
|
||||
/// 底部加号创作入口页的后台公告列表。
|
||||
pub event_banners: Vec<CreationEntryEventBannerResponse>,
|
||||
}
|
||||
|
||||
/// 后台单个创作入口开关配置。
|
||||
@@ -59,6 +61,14 @@ pub struct AdminUpsertCreationEntryTypeConfigRequest {
|
||||
pub unified_creation_spec: Option<UnifiedCreationSpecResponse>,
|
||||
}
|
||||
|
||||
/// 后台保存创作入口公告表单序列化结果请求。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AdminUpsertCreationEntryEventBannersRequest {
|
||||
/// 传输字段沿用既有契约,内容由后台标题 / HTML 表单生成。
|
||||
pub event_banners_json: String,
|
||||
}
|
||||
|
||||
/// 后台作品可见性列表项。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
/// 前台创作入口配置响应。
|
||||
///
|
||||
/// `event_banner` 保留单条旧契约兼容;新创作入口公告位应优先读取
|
||||
/// `event_banners`,由后台表单配置多条公告并支持轮播。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryConfigResponse {
|
||||
pub start_card: CreationEntryStartCardResponse,
|
||||
pub type_modal: CreationEntryTypeModalResponse,
|
||||
pub event_banner: CreationEntryEventBannerResponse,
|
||||
pub event_banners: Vec<CreationEntryEventBannerResponse>,
|
||||
pub creation_types: Vec<CreationEntryTypeResponse>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
/// 创作入口起始卡片文案契约,保留给旧入口卡片兼容使用。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryStartCardResponse {
|
||||
pub title: String,
|
||||
@@ -19,14 +25,19 @@ pub struct CreationEntryStartCardResponse {
|
||||
pub busy_badge: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
/// 创作类型选择弹层的基础文案契约。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryTypeModalResponse {
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
/// 创作入口单条公告。
|
||||
///
|
||||
/// `html_code` 是后台公告代码的主格式,只允许以前端沙箱 iframe 展示;
|
||||
/// 结构化字段仅保留旧数据兼容,不能作为可执行 JSX 或非受控 DOM 注入。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryEventBannerResponse {
|
||||
pub title: String,
|
||||
@@ -35,9 +46,19 @@ pub struct CreationEntryEventBannerResponse {
|
||||
pub prize_pool_mud_points: u64,
|
||||
pub starts_at_text: String,
|
||||
pub ends_at_text: String,
|
||||
#[serde(default = "default_creation_entry_event_banner_render_mode")]
|
||||
pub render_mode: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub html_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
/// 默认渲染模式使用受控结构化 UI,用于旧数据兼容。
|
||||
pub fn default_creation_entry_event_banner_render_mode() -> String {
|
||||
"structured".to_string()
|
||||
}
|
||||
|
||||
/// 单个创作模板入口配置,决定底部加号入口中的分类、排序和开放状态。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreationEntryTypeResponse {
|
||||
pub id: String,
|
||||
@@ -56,6 +77,7 @@ pub struct CreationEntryTypeResponse {
|
||||
pub unified_creation_spec: Option<UnifiedCreationSpecResponse>,
|
||||
}
|
||||
|
||||
/// 统一创作工作台契约,把玩法入口连接到工作台、生成页和结果页阶段。
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UnifiedCreationSpecResponse {
|
||||
@@ -262,8 +284,18 @@ mod tests {
|
||||
);
|
||||
|
||||
let jump_hop = build_phase1_unified_creation_spec("jump-hop").expect("jump-hop spec");
|
||||
assert!(jump_hop.fields.iter().any(|field| field.id == "stylePreset"));
|
||||
assert!(jump_hop.fields.iter().any(|field| field.id == "endMoodPrompt"));
|
||||
assert!(
|
||||
jump_hop
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| field.id == "stylePreset")
|
||||
);
|
||||
assert!(
|
||||
jump_hop
|
||||
.fields
|
||||
.iter()
|
||||
.any(|field| field.id == "endMoodPrompt")
|
||||
);
|
||||
|
||||
let wooden_fish =
|
||||
build_phase1_unified_creation_spec("wooden-fish").expect("wooden-fish spec");
|
||||
@@ -271,4 +303,68 @@ mod tests {
|
||||
assert!(build_phase1_unified_creation_spec("visual-novel").is_none());
|
||||
assert!(build_phase1_unified_creation_spec("bark-battle").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creation_entry_event_banner_defaults_to_structured_render_mode() {
|
||||
let banner = serde_json::from_str::<CreationEntryEventBannerResponse>(
|
||||
r#"{
|
||||
"title": "旧版横幅",
|
||||
"description": "兼容旧字段",
|
||||
"coverImageSrc": "/creation-type-references/puzzle.webp",
|
||||
"prizePoolMudPoints": 1000,
|
||||
"startsAtText": "2026-06-01",
|
||||
"endsAtText": "2026-06-30"
|
||||
}"#,
|
||||
)
|
||||
.expect("legacy banner json should decode");
|
||||
|
||||
assert_eq!(banner.render_mode, "structured");
|
||||
assert!(banner.html_code.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn creation_entry_config_serializes_event_banners_contract() {
|
||||
let response = CreationEntryConfigResponse {
|
||||
start_card: CreationEntryStartCardResponse {
|
||||
title: "新建作品".to_string(),
|
||||
description: "选择模板".to_string(),
|
||||
idle_badge: "模板".to_string(),
|
||||
busy_badge: "开启中".to_string(),
|
||||
},
|
||||
type_modal: CreationEntryTypeModalResponse {
|
||||
title: "选择创作类型".to_string(),
|
||||
description: "先选玩法".to_string(),
|
||||
},
|
||||
event_banner: CreationEntryEventBannerResponse {
|
||||
title: "第一条".to_string(),
|
||||
description: "兼容单条".to_string(),
|
||||
cover_image_src: "/creation-type-references/puzzle.webp".to_string(),
|
||||
prize_pool_mud_points: 1000,
|
||||
starts_at_text: "2026-06-01".to_string(),
|
||||
ends_at_text: "2026-06-30".to_string(),
|
||||
render_mode: "structured".to_string(),
|
||||
html_code: None,
|
||||
},
|
||||
event_banners: vec![CreationEntryEventBannerResponse {
|
||||
title: "HTML 条".to_string(),
|
||||
description: "沙箱".to_string(),
|
||||
cover_image_src: "/creation-type-references/match3d.webp".to_string(),
|
||||
prize_pool_mud_points: 800,
|
||||
starts_at_text: "2026-07-01".to_string(),
|
||||
ends_at_text: "2026-07-31".to_string(),
|
||||
render_mode: "html".to_string(),
|
||||
html_code: Some("<section>ok</section>".to_string()),
|
||||
}],
|
||||
creation_types: Vec::new(),
|
||||
};
|
||||
let value = serde_json::to_value(response).expect("response should serialize");
|
||||
|
||||
assert_eq!(value["eventBanners"][0]["renderMode"], "html");
|
||||
assert_eq!(
|
||||
value["eventBanners"][0]["htmlCode"],
|
||||
"<section>ok</section>"
|
||||
);
|
||||
assert!(value.get("event_banner").is_none());
|
||||
assert!(value.get("eventBanner").is_some());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user