Merge branch 'codex/feature-1'
# Conflicts: # docs/【玩法创作】平台入口与玩法链路-2026-05-15.md # src/components/platform-entry/PlatformEntryFlowShellImpl.tsx # src/components/rpg-entry/RpgEntryFlowShell.agent.interaction.test.tsx # src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
@@ -25,8 +25,8 @@ use shared_contracts::admin::{
|
||||
AdminLoginResponse, AdminMeResponse, AdminOverviewResponse, AdminServiceOverviewPayload,
|
||||
AdminSessionPayload, AdminTrackingEventEntryPayload, AdminTrackingEventListQuery,
|
||||
AdminTrackingEventListResponse, AdminUpdateWorkVisibilityRequest,
|
||||
AdminUpdateWorkVisibilityResponse, AdminUpsertCreationEntryTypeConfigRequest,
|
||||
AdminWorkVisibilityListResponse,
|
||||
AdminUpdateWorkVisibilityResponse, AdminUpsertCreationEntryEventBannersRequest,
|
||||
AdminUpsertCreationEntryTypeConfigRequest, AdminWorkVisibilityListResponse,
|
||||
};
|
||||
use shared_contracts::creation_entry_config::{
|
||||
encode_unified_creation_spec_response, validate_unified_creation_spec_for_play,
|
||||
@@ -200,6 +200,7 @@ pub async fn admin_list_database_table_rows(
|
||||
Ok(json_success_body(Some(&request_context), response))
|
||||
}
|
||||
|
||||
/// 读取后台创作入口配置,包含模板入口和底部加号入口页公告。
|
||||
pub async fn admin_get_creation_entry_config(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -212,6 +213,7 @@ pub async fn admin_get_creation_entry_config(
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminCreationEntryConfigResponse {
|
||||
event_banners: config.event_banners,
|
||||
entries: config
|
||||
.creation_types
|
||||
.into_iter()
|
||||
@@ -221,6 +223,7 @@ pub async fn admin_get_creation_entry_config(
|
||||
))
|
||||
}
|
||||
|
||||
/// 保存单个创作模板入口配置,并返回最新公告与入口快照。
|
||||
pub async fn admin_upsert_creation_entry_config(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
@@ -235,6 +238,38 @@ pub async fn admin_upsert_creation_entry_config(
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminCreationEntryConfigResponse {
|
||||
event_banners: config.event_banners,
|
||||
entries: config
|
||||
.creation_types
|
||||
.into_iter()
|
||||
.map(map_admin_creation_entry_type_config)
|
||||
.collect(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
/// 保存底部加号创作入口页的多公告表单序列化配置。
|
||||
pub async fn admin_upsert_creation_entry_event_banners_config(
|
||||
State(state): State<AppState>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(_admin): Extension<AuthenticatedAdmin>,
|
||||
Json(payload): Json<AdminUpsertCreationEntryEventBannersRequest>,
|
||||
) -> Result<Json<Value>, AppError> {
|
||||
let normalized_json =
|
||||
module_runtime::normalize_creation_entry_event_banners_json(&payload.event_banners_json)
|
||||
.map_err(|error| AppError::from_status(StatusCode::BAD_REQUEST).with_message(error))?;
|
||||
let config = state
|
||||
.upsert_creation_entry_event_banners_config(
|
||||
module_runtime::CreationEntryEventBannersAdminUpsertInput {
|
||||
event_banners_json: normalized_json,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(map_admin_spacetime_error)?;
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
AdminCreationEntryConfigResponse {
|
||||
event_banners: config.event_banners,
|
||||
entries: config
|
||||
.creation_types
|
||||
.into_iter()
|
||||
@@ -1502,11 +1537,7 @@ mod tests {
|
||||
};
|
||||
use axum::{http::StatusCode, response::IntoResponse};
|
||||
use serde_json::json;
|
||||
use shared_contracts::admin::{
|
||||
AdminCreationEntryConfigResponse, AdminCreationEntryTypeConfigPayload,
|
||||
AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery,
|
||||
AdminUpsertCreationEntryTypeConfigRequest,
|
||||
};
|
||||
use shared_contracts::admin::{AdminDatabaseTableRowsQuery, AdminTrackingEventListQuery};
|
||||
|
||||
#[test]
|
||||
fn normalize_debug_path_rejects_absolute_url() {
|
||||
|
||||
@@ -518,6 +518,40 @@ mod tests {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// 中文注释:后台路由测试通过真实登录流程取 token,避免绕过鉴权中间件。
|
||||
async fn read_admin_access_token(app: Router) -> String {
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/admin/api/login")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"username": "root",
|
||||
"password": "secret123"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("admin login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("admin login request should succeed");
|
||||
let body = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("admin login body should collect")
|
||||
.to_bytes();
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("admin login payload should be json");
|
||||
|
||||
payload["token"]
|
||||
.as_str()
|
||||
.expect("admin token should exist")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
async fn password_login_request(
|
||||
app: Router,
|
||||
phone_number: &str,
|
||||
@@ -3980,6 +4014,91 @@ mod tests {
|
||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
}
|
||||
|
||||
/// 中文注释:验证入口公告表单提交的 HTML 会保存进独立公告配置。
|
||||
#[tokio::test]
|
||||
async fn admin_creation_entry_banners_route_saves_html_form_payload() {
|
||||
let mut config = AppConfig::default();
|
||||
config.admin_username = Some("root".to_string());
|
||||
config.admin_password = Some("secret123".to_string());
|
||||
let app = build_router(AppState::new(config).expect("state should build"));
|
||||
let admin_token = read_admin_access_token(app.clone()).await;
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/admin/api/creation-entry/config/banners")
|
||||
.header("authorization", format!("Bearer {admin_token}"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"eventBannersJson": serde_json::json!([
|
||||
{
|
||||
"title": "后台表单公告",
|
||||
"htmlCode": "<section>入口公告 HTML</section>"
|
||||
}
|
||||
]).to_string()
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("banners request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("banners request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("banners body should collect")
|
||||
.to_bytes();
|
||||
let payload: Value =
|
||||
serde_json::from_slice(&body).expect("banners payload should be json");
|
||||
|
||||
assert_eq!(payload["eventBanners"][0]["title"], "后台表单公告");
|
||||
assert_eq!(payload["eventBanners"][0]["renderMode"], "html");
|
||||
assert_eq!(
|
||||
payload["eventBanners"][0]["htmlCode"],
|
||||
"<section>入口公告 HTML</section>"
|
||||
);
|
||||
}
|
||||
|
||||
/// 中文注释:验证入口公告拒绝可执行脚本,避免后台配置变成不受控注入。
|
||||
#[tokio::test]
|
||||
async fn admin_creation_entry_banners_route_rejects_script_html() {
|
||||
let mut config = AppConfig::default();
|
||||
config.admin_username = Some("root".to_string());
|
||||
config.admin_password = Some("secret123".to_string());
|
||||
let app = build_router(AppState::new(config).expect("state should build"));
|
||||
let admin_token = read_admin_access_token(app.clone()).await;
|
||||
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/admin/api/creation-entry/config/banners")
|
||||
.header("authorization", format!("Bearer {admin_token}"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"eventBannersJson": serde_json::json!([
|
||||
{
|
||||
"title": "危险公告",
|
||||
"htmlCode": "<script>alert(1)</script>"
|
||||
}
|
||||
]).to_string()
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("banners request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("banners request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn admin_debug_http_can_probe_healthz_when_authenticated() {
|
||||
let mut config = AppConfig::default();
|
||||
|
||||
@@ -152,7 +152,10 @@ pub(crate) fn default_creation_entry_config_response() -> CreationEntryConfigRes
|
||||
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,
|
||||
})
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
use axum::{Router, middleware, routing::get};
|
||||
use axum::{
|
||||
Router, middleware,
|
||||
routing::{get, post},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
admin::{
|
||||
admin_debug_http, admin_get_creation_entry_config, admin_list_database_table_rows,
|
||||
admin_list_database_tables, admin_list_tracking_events, admin_list_work_visibility,
|
||||
admin_login, admin_me, admin_overview, admin_update_work_visibility,
|
||||
admin_upsert_creation_entry_config, require_admin_auth,
|
||||
admin_upsert_creation_entry_config, admin_upsert_creation_entry_event_banners_config,
|
||||
require_admin_auth,
|
||||
},
|
||||
runtime_profile::{
|
||||
admin_disable_profile_redeem_code, admin_disable_profile_task_config,
|
||||
@@ -71,6 +75,12 @@ pub fn router(state: AppState) -> Router<AppState> {
|
||||
require_admin_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/creation-entry/config/banners",
|
||||
post(admin_upsert_creation_entry_event_banners_config).route_layer(
|
||||
middleware::from_fn_with_state(state.clone(), require_admin_auth),
|
||||
),
|
||||
)
|
||||
.route(
|
||||
"/admin/api/works/visibility",
|
||||
get(admin_list_work_visibility)
|
||||
|
||||
@@ -468,6 +468,45 @@ impl AppState {
|
||||
}
|
||||
}
|
||||
|
||||
/// 通过 SpacetimeDB 保存创作入口页多公告配置,并同步测试缓存。
|
||||
pub async fn upsert_creation_entry_event_banners_config(
|
||||
&self,
|
||||
input: module_runtime::CreationEntryEventBannersAdminUpsertInput,
|
||||
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
|
||||
#[cfg(test)]
|
||||
let test_event_banners_json = input.event_banners_json.clone();
|
||||
match self
|
||||
.spacetime_client
|
||||
.upsert_creation_entry_event_banners_config(input)
|
||||
.await
|
||||
{
|
||||
Ok(config) => {
|
||||
#[cfg(test)]
|
||||
self.cache_test_creation_entry_config(config.clone());
|
||||
Ok(config)
|
||||
}
|
||||
#[cfg(test)]
|
||||
Err(_) => {
|
||||
let mut config = self.read_test_creation_entry_config();
|
||||
if let Ok(banners) = module_runtime::decode_creation_entry_event_banner_snapshots(
|
||||
test_event_banners_json.as_str(),
|
||||
) {
|
||||
config.event_banners = banners
|
||||
.into_iter()
|
||||
.map(module_runtime::build_creation_entry_event_banner_response)
|
||||
.collect();
|
||||
if let Some(first_banner) = config.event_banners.first().cloned() {
|
||||
config.event_banner = first_banner;
|
||||
}
|
||||
self.cache_test_creation_entry_config(config.clone());
|
||||
}
|
||||
Ok(config)
|
||||
}
|
||||
#[cfg(not(test))]
|
||||
Err(error) => Err(error),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_creation_entry_config(
|
||||
&self,
|
||||
) -> Result<CreationEntryConfigResponse, SpacetimeClientError> {
|
||||
|
||||
Reference in New Issue
Block a user