Merge branch 'master' into codex/puzzle-clear-template-runtime-fixes

# Conflicts:
#	.hermes/shared-memory/decision-log.md
#	.hermes/shared-memory/project-overview.md
#	docs/【开发运维】本地开发验证与生产运维-2026-05-15.md
#	scripts/dev.test.ts
#	server-rs/crates/api-server/src/creation_entry_config.rs
#	server-rs/crates/api-server/src/wooden_fish.rs
#	server-rs/crates/module-auth/src/lib.rs
#	server-rs/crates/spacetime-client/src/wooden_fish.rs
#	server-rs/crates/spacetime-module/src/auth/procedures.rs
#	src/components/custom-world-home/creationWorkShelf.ts
#	src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
#	src/components/rpg-entry/rpgEntryWorldPresentation.ts
#	src/services/miniGameDraftGenerationProgress.test.ts
#	src/services/miniGameDraftGenerationProgress.ts
This commit is contained in:
2026-06-04 11:24:14 +08:00
451 changed files with 18452 additions and 5266 deletions

View File

@@ -116,6 +116,7 @@ pub struct WechatIdentityProfile {
pub provider_union_id: Option<String>,
pub display_name: Option<String>,
pub avatar_url: Option<String>,
pub session_key: Option<String>,
}
/// 已绑定微信身份快照。
@@ -124,6 +125,7 @@ pub struct WechatIdentityRecord {
pub user_id: String,
pub provider_uid: String,
pub provider_union_id: Option<String>,
pub session_key: Option<String>,
}
/// 微信授权 state 快照。

View File

@@ -11,7 +11,7 @@ pub use errors::*;
pub use events::*;
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};
@@ -94,6 +94,7 @@ struct StoredWechatIdentity {
provider_union_id: Option<String>,
display_name: Option<String>,
avatar_url: Option<String>,
session_key: Option<String>,
}
#[derive(Clone, Debug)]
@@ -917,16 +918,47 @@ impl Default for InMemoryAuthStoreState {
impl InMemoryAuthStoreState {
fn from_persistent_snapshot(snapshot: PersistentAuthStoreSnapshot) -> Self {
let existing_user_ids = snapshot
.users_by_username
.values()
.map(|stored| stored.user.id.clone())
.collect::<HashSet<_>>();
let phone_to_user_id = snapshot
.phone_to_user_id
.into_iter()
.filter(|(_, user_id)| existing_user_ids.contains(user_id))
.collect();
let sessions_by_id = snapshot
.sessions_by_id
.into_iter()
.filter(|(_, stored)| existing_user_ids.contains(&stored.session.user_id))
.collect::<HashMap<_, _>>();
let session_id_by_refresh_token_hash = snapshot
.session_id_by_refresh_token_hash
.into_iter()
.filter(|(_, session_id)| sessions_by_id.contains_key(session_id))
.collect();
let wechat_identity_by_provider_uid = snapshot
.wechat_identity_by_provider_uid
.into_iter()
.filter(|(_, identity)| existing_user_ids.contains(&identity.user_id))
.collect();
let user_id_by_provider_union_id = snapshot
.user_id_by_provider_union_id
.into_iter()
.filter(|(_, user_id)| existing_user_ids.contains(user_id))
.collect();
Self {
next_user_id: snapshot.next_user_id,
users_by_username: snapshot.users_by_username,
phone_to_user_id: snapshot.phone_to_user_id,
sessions_by_id: snapshot.sessions_by_id,
session_id_by_refresh_token_hash: snapshot.session_id_by_refresh_token_hash,
phone_to_user_id,
sessions_by_id,
session_id_by_refresh_token_hash,
phone_codes_by_key: HashMap::new(),
wechat_states_by_token: HashMap::new(),
wechat_identity_by_provider_uid: snapshot.wechat_identity_by_provider_uid,
user_id_by_provider_union_id: snapshot.user_id_by_provider_union_id,
wechat_identity_by_provider_uid,
user_id_by_provider_union_id,
}
}
@@ -1324,6 +1356,7 @@ impl InMemoryAuthStore {
provider_union_id: normalize_optional_string(profile.provider_union_id),
display_name: normalize_optional_string(profile.display_name),
avatar_url,
session_key: normalize_optional_string(profile.session_key),
};
if let Some(provider_union_id) = identity.provider_union_id.clone() {
state
@@ -1393,6 +1426,7 @@ impl InMemoryAuthStore {
user_id: identity.user_id.clone(),
provider_uid: identity.provider_uid.clone(),
provider_union_id: identity.provider_union_id.clone(),
session_key: identity.session_key.clone(),
}))
}
@@ -1409,6 +1443,7 @@ impl InMemoryAuthStore {
let next_display_name = normalize_optional_string(profile.display_name);
let next_avatar_url = normalize_optional_string(profile.avatar_url);
let next_provider_union_id = normalize_optional_string(profile.provider_union_id);
let next_session_key = normalize_optional_string(profile.session_key);
let next_provider_uid =
normalize_required_string(&profile.provider_uid).unwrap_or_default();
{
@@ -1430,6 +1465,9 @@ impl InMemoryAuthStore {
identity.display_name = next_display_name.clone();
identity.avatar_url = next_avatar_url;
identity.provider_union_id = next_provider_union_id.clone();
if next_session_key.is_some() {
identity.session_key = next_session_key.clone();
}
state
.wechat_identity_by_provider_uid
.insert(next_provider_uid.clone(), identity);
@@ -2722,6 +2760,54 @@ mod tests {
assert_eq!(rotated.user.id, user.id);
}
#[tokio::test]
async fn snapshot_json_drops_orphan_phone_index_before_phone_login() {
let snapshot = PersistentAuthStoreSnapshot {
next_user_id: 9,
users_by_username: HashMap::new(),
phone_to_user_id: HashMap::from([(
"+8613800138032".to_string(),
"user_missing_phone_owner".to_string(),
)]),
sessions_by_id: HashMap::new(),
session_id_by_refresh_token_hash: HashMap::new(),
wechat_identity_by_provider_uid: HashMap::new(),
user_id_by_provider_union_id: HashMap::new(),
};
let snapshot_json = serde_json::to_string(&snapshot).expect("snapshot should serialize");
let restored_store = InMemoryAuthStore::from_snapshot_json(&snapshot_json)
.expect("snapshot json should restore");
let phone_service = build_phone_service(restored_store);
let now = OffsetDateTime::now_utc();
phone_service
.send_code(
SendPhoneCodeInput {
phone_number: "13800138032".to_string(),
scene: PhoneAuthScene::Login,
},
now,
)
.await
.expect("phone code should send");
let result = phone_service
.login(
PhoneLoginInput {
phone_number: "13800138032".to_string(),
verify_code: DEFAULT_SMS_MOCK_VERIFY_CODE.to_string(),
},
now + Duration::seconds(1),
)
.await
.expect("orphan phone index should not block phone login");
assert!(result.created);
assert_eq!(
result.user.phone_number_masked.as_deref(),
Some("138****8032")
);
}
#[tokio::test]
async fn password_entry_rejects_email_or_username_identifier() {
let service = build_password_service(build_store());
@@ -3314,6 +3400,7 @@ mod tests {
provider_union_id: Some("wx-union-shared".to_string()),
display_name: Some("微信旅人甲".to_string()),
avatar_url: None,
session_key: None,
},
})
.await
@@ -3335,6 +3422,7 @@ mod tests {
provider_union_id: Some("wx-union-shared".to_string()),
display_name: Some("微信旅人乙".to_string()),
avatar_url: None,
session_key: None,
},
})
.await
@@ -3383,6 +3471,7 @@ mod tests {
provider_union_id: Some("wx-union-bind".to_string()),
display_name: Some("待绑定微信用户".to_string()),
avatar_url: None,
session_key: None,
},
})
.await
@@ -3428,6 +3517,7 @@ mod tests {
provider_union_id: Some("wx-union-bind".to_string()),
display_name: Some("已归并微信用户".to_string()),
avatar_url: None,
session_key: None,
},
})
.await