Files
Genarrative/server-rs/crates/api-server/src/creation_entry_config.rs
kdletters 502811a103 Merge remote-tracking branch 'origin/master' into hermes/hermes-1e775b03
# Conflicts:
#	server-rs/crates/api-server/src/app.rs
#	server-rs/crates/api-server/src/creation_entry_config.rs
#	server-rs/crates/api-server/src/puzzle.rs
#	server-rs/crates/spacetime-client/src/lib.rs
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
2026-05-14 19:17:17 +08:00

185 lines
6.7 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 axum::{
Json,
body::Body,
extract::{Extension, State},
http::{Request, StatusCode},
middleware::Next,
response::Response,
};
use serde_json::{Value, json};
#[cfg(test)]
use module_runtime::build_creation_entry_config_response;
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 路由熔断只拦创作/运行态 API 请求,不改变前端入口展示规则。
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.starts_with("/api/runtime/puzzle") {
return Some("puzzle");
}
if normalized.starts_with("/api/runtime/match3d") {
return Some("match3d");
}
if normalized.starts_with("/api/runtime/bark-battle") {
return Some("bark-battle");
}
if normalized.starts_with("/api/runtime/square-hole") {
return Some("square-hole");
}
if normalized.starts_with("/api/runtime/big-fish") {
return Some("big-fish");
}
if normalized.starts_with("/api/runtime/visual-novel") {
return Some("visual-novel");
}
if normalized.starts_with("/api/creation/visual-novel") {
return Some("visual-novel");
}
if normalized.starts_with("/api/creation/edutainment/baby-object-match") {
return Some("baby-object-match");
}
if normalized.starts_with("/api/creation/edutainment/baby-love-drawing") {
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))
}
#[cfg(test)]
pub(crate) fn test_creation_entry_config_response()
-> shared_contracts::creation_entry_config::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(),
},
creation_types: module_runtime::default_creation_entry_type_snapshots(0),
updated_at_micros: 0,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resolves_runtime_paths_to_creation_type_ids() {
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/puzzle/works"),
Some("puzzle"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/match3d/runs/run-1"),
Some("match3d"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/square-hole/runs/run-1"),
Some("square-hole"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/visual-novel/works"),
Some("visual-novel"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/creation/visual-novel/sessions"),
Some("visual-novel"),
);
assert_eq!(
resolve_creation_entry_route_id("/api/runtime/bark-battle/works/work-1/config"),
Some("bark-battle"),
);
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_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, "宝贝识物");
assert!(baby_object_match.visible);
assert!(baby_object_match.open);
assert_eq!(baby_object_match.sort_order, 90);
}
}