302 lines
11 KiB
Rust
302 lines
11 KiB
Rust
use axum::{
|
||
Json,
|
||
body::Body,
|
||
extract::{Extension, State},
|
||
http::{Request, StatusCode},
|
||
middleware::Next,
|
||
response::Response,
|
||
};
|
||
use serde_json::{Value, json};
|
||
|
||
use module_runtime::build_creation_entry_config_response;
|
||
use shared_contracts::creation_entry_config::CreationEntryConfigResponse;
|
||
|
||
use crate::{
|
||
api_response::json_success_body, http_error::AppError, request_context::RequestContext,
|
||
state::AppState,
|
||
};
|
||
|
||
/// 中文注释:入口配置由 SpacetimeDB 表提供;api-server 只负责读取同一份配置并熔断运行态路由。
|
||
pub async fn get_creation_entry_config_handler(
|
||
State(state): State<AppState>,
|
||
Extension(request_context): Extension<RequestContext>,
|
||
) -> Result<Json<Value>, Response> {
|
||
let config = state.get_creation_entry_config().await.map_err(|error| {
|
||
creation_entry_error_response(
|
||
&request_context,
|
||
AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({
|
||
"provider": "spacetimedb",
|
||
"message": error.to_string(),
|
||
})),
|
||
)
|
||
})?;
|
||
|
||
Ok(json_success_body(Some(&request_context), config))
|
||
}
|
||
|
||
/// 中文注释:api-server 路由熔断只拦新建创作入口,不限制已有作品读取、发布作品游玩或公开广场浏览。
|
||
pub async fn require_creation_entry_route_enabled(
|
||
State(state): State<AppState>,
|
||
request: Request<Body>,
|
||
next: Next,
|
||
) -> Response {
|
||
let path = request.uri().path();
|
||
let route_id = resolve_creation_entry_route_id(path);
|
||
if route_id.is_some() {
|
||
let route_id = route_id.expect("route id should exist");
|
||
match state.is_creation_entry_route_enabled(route_id).await {
|
||
Ok(true) => {}
|
||
Ok(false) => {
|
||
return AppError::from_status(StatusCode::SERVICE_UNAVAILABLE)
|
||
.with_message("该玩法入口暂不可用")
|
||
.with_details(json!({
|
||
"reason": "creation_entry_disabled",
|
||
"creationTypeId": route_id,
|
||
}))
|
||
.into();
|
||
}
|
||
Err(error) => {
|
||
return AppError::from_status(StatusCode::BAD_GATEWAY)
|
||
.with_message("读取玩法入口配置失败")
|
||
.with_details(json!({
|
||
"provider": "spacetimedb",
|
||
"message": error.to_string(),
|
||
}))
|
||
.into();
|
||
}
|
||
}
|
||
}
|
||
|
||
next.run(request).await
|
||
}
|
||
|
||
pub fn resolve_creation_entry_route_id(path: &str) -> Option<&'static str> {
|
||
let normalized = path.trim_end_matches('/');
|
||
if normalized == "/api/runtime/puzzle/agent/sessions"
|
||
|| normalized == "/api/runtime/puzzle/onboarding/generate"
|
||
{
|
||
return Some("puzzle");
|
||
}
|
||
if normalized.starts_with("/api/runtime/puzzle/gallery/") && normalized.ends_with("/remix") {
|
||
return Some("puzzle");
|
||
}
|
||
if normalized == "/api/runtime/big-fish/agent/sessions" {
|
||
return Some("big-fish");
|
||
}
|
||
if normalized.starts_with("/api/runtime/big-fish/gallery/") && normalized.ends_with("/remix") {
|
||
return Some("big-fish");
|
||
}
|
||
if normalized == "/api/runtime/custom-world/agent/sessions"
|
||
|| normalized == "/api/runtime/custom-world/profile"
|
||
{
|
||
return Some("rpg");
|
||
}
|
||
if normalized.starts_with("/api/runtime/custom-world-gallery/")
|
||
&& normalized.ends_with("/remix")
|
||
{
|
||
return Some("rpg");
|
||
}
|
||
if normalized == "/api/creation/match3d/sessions" {
|
||
return Some("match3d");
|
||
}
|
||
if normalized == "/api/creation/square-hole/sessions" {
|
||
return Some("square-hole");
|
||
}
|
||
if normalized == "/api/creation/bark-battle/drafts" {
|
||
return Some("bark-battle");
|
||
}
|
||
if normalized == "/api/creation/wooden-fish/sessions" {
|
||
return Some("wooden-fish");
|
||
}
|
||
if normalized == "/api/creation/jump-hop/sessions" {
|
||
return Some("jump-hop");
|
||
}
|
||
if normalized == "/api/creation/puzzle-clear/sessions" {
|
||
return Some("puzzle-clear");
|
||
}
|
||
if normalized == "/api/creation/visual-novel/sessions" {
|
||
return Some("visual-novel");
|
||
}
|
||
if normalized == "/api/creation/edutainment/baby-object-match/assets" {
|
||
return Some("baby-object-match");
|
||
}
|
||
if normalized == "/api/creation/edutainment/baby-love-drawing/magic" {
|
||
return Some("baby-love-drawing");
|
||
}
|
||
None
|
||
}
|
||
|
||
fn creation_entry_error_response(request_context: &RequestContext, error: AppError) -> Response {
|
||
error.into_response_with_context(Some(request_context))
|
||
}
|
||
|
||
/// 中文注释:本地 debug 兜底也来自后端领域默认种子,避免前端恢复硬编码入口配置。
|
||
pub(crate) fn default_creation_entry_config_response() -> CreationEntryConfigResponse {
|
||
build_creation_entry_config_response(module_runtime::CreationEntryConfigSnapshot {
|
||
config_id: module_runtime::CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string(),
|
||
start_card: module_runtime::CreationEntryStartCardSnapshot {
|
||
title: module_runtime::DEFAULT_CREATION_ENTRY_START_TITLE.to_string(),
|
||
description: module_runtime::DEFAULT_CREATION_ENTRY_START_DESCRIPTION.to_string(),
|
||
idle_badge: module_runtime::DEFAULT_CREATION_ENTRY_START_IDLE_BADGE.to_string(),
|
||
busy_badge: module_runtime::DEFAULT_CREATION_ENTRY_START_BUSY_BADGE.to_string(),
|
||
},
|
||
type_modal: module_runtime::CreationEntryTypeModalSnapshot {
|
||
title: module_runtime::DEFAULT_CREATION_ENTRY_MODAL_TITLE.to_string(),
|
||
description: module_runtime::DEFAULT_CREATION_ENTRY_MODAL_DESCRIPTION.to_string(),
|
||
},
|
||
event_banner: module_runtime::CreationEntryEventBannerSnapshot {
|
||
title: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_TITLE.to_string(),
|
||
description: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_DESCRIPTION.to_string(),
|
||
cover_image_src: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_COVER_IMAGE_SRC
|
||
.to_string(),
|
||
prize_pool_mud_points:
|
||
module_runtime::DEFAULT_CREATION_ENTRY_EVENT_PRIZE_POOL_MUD_POINTS,
|
||
starts_at_text: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_STARTS_AT_TEXT.to_string(),
|
||
ends_at_text: module_runtime::DEFAULT_CREATION_ENTRY_EVENT_ENDS_AT_TEXT.to_string(),
|
||
render_mode: "structured".to_string(),
|
||
html_code: None,
|
||
},
|
||
event_banners_json: Some(module_runtime::default_creation_entry_event_banners_json()),
|
||
creation_types: module_runtime::default_creation_entry_type_snapshots(0),
|
||
updated_at_micros: 0,
|
||
})
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub(crate) fn test_creation_entry_config_response() -> CreationEntryConfigResponse {
|
||
default_creation_entry_config_response()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn resolves_new_creation_paths_to_creation_type_ids() {
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/puzzle/agent/sessions"),
|
||
Some("puzzle"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/puzzle-clear/sessions"),
|
||
Some("puzzle-clear"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/puzzle/gallery/profile-1/remix"),
|
||
Some("puzzle"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/match3d/sessions"),
|
||
Some("match3d"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/square-hole/sessions"),
|
||
Some("square-hole"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/visual-novel/sessions"),
|
||
Some("visual-novel"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/big-fish/agent/sessions"),
|
||
Some("big-fish"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/custom-world/agent/sessions"),
|
||
Some("rpg"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id(
|
||
"/api/runtime/custom-world-gallery/user-1/profile-1/remix"
|
||
),
|
||
Some("rpg"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/custom-world-library/profile-1"),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/custom-world-gallery/user-1/profile-1"),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/story/sessions/runtime"),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/chat/npc/turn/stream"),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/bark-battle/drafts"),
|
||
Some("bark-battle"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/wooden-fish/runs/run-1"),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/runtime/puzzle-clear/runs/run-1"),
|
||
None,
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/wooden-fish/sessions"),
|
||
Some("wooden-fish"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/edutainment/baby-object-match/assets"),
|
||
Some("baby-object-match"),
|
||
);
|
||
assert_eq!(
|
||
resolve_creation_entry_route_id("/api/creation/edutainment/baby-love-drawing/magic"),
|
||
Some("baby-love-drawing"),
|
||
);
|
||
assert_eq!(resolve_creation_entry_route_id("/healthz"), None);
|
||
}
|
||
|
||
#[test]
|
||
fn test_creation_entry_config_response_opens_bark_battle() {
|
||
let config = test_creation_entry_config_response();
|
||
let bark_battle = config
|
||
.creation_types
|
||
.iter()
|
||
.find(|item| item.id == "bark-battle")
|
||
.expect("test creation entry config should include bark-battle");
|
||
|
||
assert_eq!(bark_battle.title, "\u{6c6a}\u{6c6a}\u{58f0}\u{6d6a}");
|
||
assert!(bark_battle.visible);
|
||
assert!(bark_battle.open);
|
||
assert_eq!(bark_battle.badge, "\u{53ef}\u{521b}\u{5efa}");
|
||
assert_eq!(
|
||
bark_battle.image_src,
|
||
"/creation-type-references/bark-battle.webp"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn test_creation_entry_config_response_keeps_baby_object_match_visible() {
|
||
let config = test_creation_entry_config_response();
|
||
let baby_object_match = config
|
||
.creation_types
|
||
.iter()
|
||
.find(|item| item.id == "baby-object-match")
|
||
.expect("test creation entry config should include baby-object-match");
|
||
|
||
assert_eq!(baby_object_match.title, "\u{5b9d}\u{8d1d}\u{8bc6}\u{7269}");
|
||
assert!(baby_object_match.visible);
|
||
assert!(baby_object_match.open);
|
||
assert_eq!(baby_object_match.badge, "\u{53ef}\u{521b}\u{5efa}");
|
||
assert_eq!(baby_object_match.sort_order, 90);
|
||
assert_eq!(baby_object_match.category_id, "character");
|
||
assert_eq!(
|
||
baby_object_match.category_label,
|
||
"\u{89d2}\u{8272}\u{521b}\u{4f5c}"
|
||
);
|
||
assert_eq!(baby_object_match.category_sort_order, 40);
|
||
}
|
||
}
|