合并 master 并修复架构分支回归

合入 master 最新的认证、玩法契约与推荐页改动。

修复拼图草稿生成、推荐页下一关和公开详情访客试玩回归。

修复抓大鹅草稿试玩鉴权与首屏推荐详情测试入口。

补齐相关测试夹具、文档与团队记忆更新。
This commit is contained in:
2026-06-07 21:35:47 +08:00
80 changed files with 2627 additions and 511 deletions

View File

@@ -2640,6 +2640,7 @@ mod tests {
login_payload["bindingStatus"],
Value::String("pending_bind_phone".to_string())
);
assert_eq!(login_payload["created"], Value::Bool(true));
assert_eq!(
login_payload["user"]["loginMethod"],
Value::String("wechat".to_string())
@@ -2746,6 +2747,7 @@ mod tests {
login_payload["bindingStatus"],
Value::String("pending_bind_phone".to_string())
);
assert_eq!(login_payload["created"], Value::Bool(true));
let bind_response = app
.oneshot(
@@ -4423,4 +4425,4 @@ mod tests {
assert_eq!(response.status(), StatusCode::NOT_FOUND, "{path}");
}
}
}
}

View File

@@ -13,7 +13,8 @@ use serde_json::{Value, json};
use shared_contracts::jump_hop::{
JumpHopActionRequest, JumpHopActionType, JumpHopCharacterAsset, JumpHopDraftResponse,
JumpHopGalleryDetailResponse, JumpHopGenerationStatus, JumpHopJumpRequest, JumpHopJumpResponse,
JumpHopLeaderboardResponse, JumpHopRestartRunRequest, JumpHopRunResponse,
JumpHopLeaderboardEntry, JumpHopLeaderboardResponse, JumpHopRestartRunRequest,
JumpHopRunResponse,
JumpHopSessionResponse, JumpHopSessionSnapshotResponse, JumpHopStartRunRequest,
JumpHopTileAsset, JumpHopTileType, JumpHopWorkDetailResponse, JumpHopWorkMutationResponse,
JumpHopWorksResponse, JumpHopWorkspaceCreateRequest,
@@ -222,6 +223,34 @@ pub async fn list_jump_hop_works(
))
}
pub async fn get_jump_hop_work_detail(
State(state): State<AppState>,
Path(profile_id): Path<String>,
Extension(request_context): Extension<RequestContext>,
Extension(authenticated): Extension<AuthenticatedAccessToken>,
) -> Result<Json<Value>, Response> {
ensure_non_empty(&request_context, &profile_id, "profileId")?;
let work = state
.spacetime_client()
.get_jump_hop_work_profile(
profile_id,
authenticated.claims().user_id().to_string(),
)
.await
.map_err(|error| {
jump_hop_error_response(
&request_context,
JUMP_HOP_CREATION_PROVIDER,
map_jump_hop_client_error(error),
)
})?;
Ok(json_success_body(
Some(&request_context),
JumpHopWorkDetailResponse { item: work },
))
}
pub async fn delete_jump_hop_work(
State(state): State<AppState>,
Path(profile_id): Path<String>,
@@ -231,7 +260,10 @@ pub async fn delete_jump_hop_work(
ensure_non_empty(&request_context, &profile_id, "profileId")?;
let works = state
.spacetime_client()
.delete_jump_hop_work(profile_id, authenticated.claims().user_id().to_string())
.delete_jump_hop_work(
profile_id,
authenticated.claims().user_id().to_string(),
)
.await
.map_err(|error| {
jump_hop_error_response(
@@ -296,8 +328,14 @@ pub async fn get_jump_hop_leaderboard(
Some(&request_context),
JumpHopLeaderboardResponse {
profile_id: leaderboard.profile_id,
items: leaderboard.items,
viewer_best: leaderboard.viewer_best,
items: leaderboard
.items
.into_iter()
.map(|entry| resolve_jump_hop_leaderboard_entry_display_name(&state, entry))
.collect(),
viewer_best: leaderboard
.viewer_best
.map(|entry| resolve_jump_hop_leaderboard_entry_display_name(&state, entry)),
},
))
}
@@ -1270,6 +1308,51 @@ fn build_jump_hop_work_play_tracking_draft(
WorkPlayTrackingDraft::runtime_principal("jump-hop", work_id, principal, source_route)
}
fn resolve_jump_hop_leaderboard_entry_display_name(
state: &AppState,
mut entry: JumpHopLeaderboardEntry,
) -> JumpHopLeaderboardEntry {
entry.display_name = resolve_jump_hop_leaderboard_display_name(state, &entry.player_id);
entry
}
fn resolve_jump_hop_leaderboard_display_name(state: &AppState, player_id: &str) -> String {
resolve_jump_hop_leaderboard_display_name_with_lookup(player_id, |user_id| {
state
.auth_user_service()
.get_user_by_id(user_id)
.ok()
.flatten()
.and_then(|user| normalize_non_empty_text(user.display_name.as_str()))
})
}
fn resolve_jump_hop_leaderboard_display_name_with_lookup(
player_id: &str,
lookup_display_name: impl FnOnce(&str) -> Option<String>,
) -> String {
let player_id = player_id.trim();
if player_id.is_empty() {
return "玩家".to_string();
}
if player_id.starts_with("guest-runtime-") {
return "游客玩家".to_string();
}
lookup_display_name(player_id)
.and_then(|display_name| normalize_non_empty_text(display_name.as_str()))
.unwrap_or_else(|| "失效玩家".to_string())
}
fn normalize_non_empty_text(value: &str) -> Option<String> {
let value = value.trim();
if value.is_empty() {
None
} else {
Some(value.to_string())
}
}
fn is_jump_hop_draft_runtime_mode(runtime_mode: &str) -> bool {
runtime_mode.trim().eq_ignore_ascii_case("draft")
}
@@ -1464,6 +1547,33 @@ mod tests {
assert!(!is_jump_hop_draft_runtime_mode(""));
}
#[test]
fn jump_hop_leaderboard_display_name_never_falls_back_to_player_id() {
assert_eq!(
resolve_jump_hop_leaderboard_display_name_with_lookup(" user-secret-1 ", |user_id| {
assert_eq!(user_id, "user-secret-1");
Some(" 陶泥儿玩家 ".to_string())
}),
"陶泥儿玩家"
);
assert_eq!(
resolve_jump_hop_leaderboard_display_name_with_lookup("guest-runtime-1", |_| {
panic!("guest player should not query account display name")
}),
"游客玩家"
);
assert_eq!(
resolve_jump_hop_leaderboard_display_name_with_lookup("user-missing", |_| None),
"失效玩家"
);
assert_eq!(
resolve_jump_hop_leaderboard_display_name_with_lookup("", |_| {
panic!("empty player id should not query account display name")
}),
"玩家"
);
}
#[test]
fn jump_hop_tile_atlas_prompt_uses_dedicated_five_by_five_floor_layout() {
let prompt = build_jump_hop_tile_atlas_prompt("森林冒险", "森林主题清爽游戏化立体感平台");

View File

@@ -1,6 +1,6 @@
use axum::{
middleware,
routing::{delete, get, post},
routing::{get, post},
Router,
};
@@ -9,8 +9,9 @@ use crate::{
jump_hop::{
create_jump_hop_session, delete_jump_hop_work, execute_jump_hop_action,
get_jump_hop_gallery_detail, get_jump_hop_leaderboard, get_jump_hop_runtime_work,
get_jump_hop_session, jump_hop_run_jump, list_jump_hop_gallery, list_jump_hop_works,
publish_jump_hop_work, restart_jump_hop_run, start_jump_hop_run,
get_jump_hop_session, get_jump_hop_work_detail, jump_hop_run_jump,
list_jump_hop_gallery, list_jump_hop_works, publish_jump_hop_work, restart_jump_hop_run,
start_jump_hop_run,
},
state::AppState,
};
@@ -47,10 +48,12 @@ pub fn router(state: AppState) -> Router<AppState> {
)
.route(
"/api/creation/jump-hop/works/{profile_id}",
delete(delete_jump_hop_work).route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
get(get_jump_hop_work_detail)
.delete(delete_jump_hop_work)
.route_layer(middleware::from_fn_with_state(
state.clone(),
require_bearer_auth,
)),
)
.route(
"/api/creation/jump-hop/works/{profile_id}/publish",

View File

@@ -349,6 +349,7 @@ pub async fn login_wechat_mini_program(
token: signed_session.access_token,
binding_status: result.user.binding_status.as_str().to_string(),
user: map_auth_user_payload(result.user),
created: result.created,
},
),
))