修正跳一跳排行榜展示名

新增排行榜 displayName 契约并在 api-server 出口补齐展示名

调整跳一跳结果页和运行态排行榜只显示 displayName

补充禁止展示 user_id 的前后端回归测试

更新跳一跳 PRD、后端契约文档和 Hermes 决策记录
This commit is contained in:
2026-06-07 16:25:58 +08:00
parent 8dca8a6443
commit 78791af424
12 changed files with 116 additions and 12 deletions

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,
@@ -327,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)),
},
))
}
@@ -1301,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")
}
@@ -1495,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("森林冒险", "森林主题清爽游戏化立体感平台");