Refine play type integration flow and docs

This commit is contained in:
2026-06-03 00:57:24 +08:00
parent dbe4c902b4
commit 67ba40c678
35 changed files with 2226 additions and 619 deletions

View File

@@ -11,7 +11,7 @@ pub use errors::*;
pub use events::*;
use std::{
collections::HashMap,
collections::{HashMap, HashSet},
sync::{Arc, Mutex},
};
@@ -918,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,
}
}
@@ -1159,10 +1190,17 @@ impl InMemoryAuthStore {
.inner
.lock()
.map_err(|_| PhoneAuthError::Store("用户仓储锁已中毒".to_string()))?;
if state.phone_to_user_id.contains_key(&phone_number.e164) {
return Err(PhoneAuthError::Store(
"手机号已存在,无法重复创建账号".to_string(),
));
if let Some(existing_user_id) = state.phone_to_user_id.get(&phone_number.e164).cloned() {
let existing_user_exists = state
.users_by_username
.values()
.any(|stored_user| stored_user.user.id == existing_user_id);
if existing_user_exists {
return Err(PhoneAuthError::Store(
"手机号已存在,无法重复创建账号".to_string(),
));
}
state.phone_to_user_id.remove(&phone_number.e164);
}
let created_at = format_rfc3339(OffsetDateTime::now_utc()).map_err(|message| {
@@ -1213,8 +1251,15 @@ impl InMemoryAuthStore {
.inner
.lock()
.map_err(|_| PasswordEntryError::Store("用户仓储锁已中毒".to_string()))?;
if state.phone_to_user_id.contains_key(&phone_number.e164) {
return Err(PasswordEntryError::InvalidCredentials);
if let Some(existing_user_id) = state.phone_to_user_id.get(&phone_number.e164).cloned() {
let existing_user_exists = state
.users_by_username
.values()
.any(|stored_user| stored_user.user.id == existing_user_id);
if existing_user_exists {
return Err(PasswordEntryError::InvalidCredentials);
}
state.phone_to_user_id.remove(&phone_number.e164);
}
let created_at = format_rfc3339(OffsetDateTime::now_utc()).map_err(|message| {
@@ -2629,6 +2674,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());