This commit is contained in:
2026-05-01 20:29:09 +08:00
parent 8718472dbd
commit 87fbf41fab
137 changed files with 2922 additions and 989 deletions

View File

@@ -53,6 +53,8 @@ pub struct AuthUser {
pub binding_status: AuthBindingStatus,
pub wechat_bound: bool,
pub token_version: u64,
#[serde(default = "default_auth_user_created_at")]
pub created_at: String,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -227,6 +229,7 @@ pub struct BindWechatPhoneInput {
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BindWechatPhoneResult {
pub user: AuthUser,
pub activated_new_user: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -966,11 +969,14 @@ impl PhoneAuthService {
return Err(PhoneAuthError::UserStateMismatch);
}
let merged_user = self
let (merged_user, activated_new_user) = self
.store
.bind_wechat_phone_to_user(&input.user_id, normalized_phone)?;
Ok(BindWechatPhoneResult { user: merged_user })
Ok(BindWechatPhoneResult {
user: merged_user,
activated_new_user,
})
}
}
@@ -1378,6 +1384,7 @@ impl InMemoryAuthStore {
let public_user_code = build_public_user_code(sequence);
state.next_user_id += 1;
let username = build_system_username("phone", state.next_user_id);
let created_at = current_auth_user_created_at();
let user = AuthUser {
id: user_id.clone(),
public_user_code,
@@ -1389,6 +1396,7 @@ impl InMemoryAuthStore {
binding_status: AuthBindingStatus::Active,
wechat_bound: false,
token_version: 1,
created_at,
};
state
.phone_to_user_id
@@ -1426,6 +1434,7 @@ impl InMemoryAuthStore {
let public_user_code = build_public_user_code(sequence);
state.next_user_id += 1;
let username = build_system_username("phone", state.next_user_id);
let created_at = current_auth_user_created_at();
let user = AuthUser {
id: user_id.clone(),
public_user_code,
@@ -1437,6 +1446,7 @@ impl InMemoryAuthStore {
binding_status: AuthBindingStatus::Active,
wechat_bound: false,
token_version: 1,
created_at,
};
state
.phone_to_user_id
@@ -1470,6 +1480,7 @@ impl InMemoryAuthStore {
let public_user_code = build_public_user_code(sequence);
state.next_user_id += 1;
let username = build_system_username("wechat", state.next_user_id);
let created_at = current_auth_user_created_at();
let display_name = profile
.display_name
.as_deref()
@@ -1488,6 +1499,7 @@ impl InMemoryAuthStore {
binding_status: AuthBindingStatus::PendingBindPhone,
wechat_bound: true,
token_version: 1,
created_at,
};
state.users_by_username.insert(
username,
@@ -1863,7 +1875,7 @@ impl InMemoryAuthStore {
&self,
pending_user_id: &str,
phone_number: PhoneNumberSnapshot,
) -> Result<AuthUser, PhoneAuthError> {
) -> Result<(AuthUser, bool), PhoneAuthError> {
let mut state = self
.inner
.lock()
@@ -1910,7 +1922,7 @@ impl InMemoryAuthStore {
let next_user = target_user.user.clone();
self.persist_phone_state(&state)?;
return Ok(next_user);
return Ok((next_user, false));
}
state
@@ -1929,7 +1941,7 @@ impl InMemoryAuthStore {
let next_user = stored_user.user.clone();
self.persist_phone_state(&state)?;
Ok(next_user)
Ok((next_user, true))
}
fn find_session_by_refresh_token_hash(
@@ -2219,7 +2231,7 @@ impl fmt::Display for PasswordEntryError {
match self {
Self::InvalidPhoneNumber => f.write_str("手机号格式不正确"),
Self::InvalidPasswordLength => f.write_str("密码长度需要在 6 到 128 位之间"),
Self::InvalidPublicUserCode => f.write_str("陶泥号格式不正确"),
Self::InvalidPublicUserCode => f.write_str("百梦号格式不正确"),
Self::InvalidDisplayName => {
f.write_str("昵称需要为 2 到 20 位中文、英文、数字或下划线")
}
@@ -2499,7 +2511,7 @@ fn build_system_username(prefix: &str, sequence: u64) -> String {
format!("{prefix}_{sequence:08}")
}
// 公开陶泥号是稳定的公开检索键,不替代内部 user_id仅用于展示、分享与搜索。
// 公开百梦号是稳定的公开检索键,不替代内部 user_id仅用于展示、分享与搜索。
fn build_public_user_code(sequence: u64) -> String {
format!("SY-{sequence:08}")
}
@@ -2527,6 +2539,15 @@ fn format_rfc3339(value: OffsetDateTime) -> Result<String, String> {
format_shared_rfc3339(value)
}
fn current_auth_user_created_at() -> String {
format_rfc3339(OffsetDateTime::now_utc())
.unwrap_or_else(|_| default_auth_user_created_at())
}
fn default_auth_user_created_at() -> String {
"1970-01-01T00:00:00Z".to_string()
}
fn parse_phone_code_time(value: &str, field_label: &str) -> Result<OffsetDateTime, PhoneAuthError> {
parse_rfc3339(value)
.map_err(|error| PhoneAuthError::Store(format!("短信验证码{field_label}解析失败:{error}")))
@@ -3467,6 +3488,7 @@ mod tests {
assert_eq!(merged.user.id, phone_user.id);
assert_eq!(merged.user.binding_status, AuthBindingStatus::Active);
assert!(merged.user.wechat_bound);
assert!(!merged.activated_new_user);
let reused_wechat_user = wechat_service
.resolve_login(ResolveWechatLoginInput {
@@ -3484,4 +3506,51 @@ mod tests {
assert_eq!(reused_wechat_user.user.id, phone_user.id);
assert!(reused_wechat_user.user.wechat_bound);
}
#[tokio::test]
async fn bind_wechat_phone_activates_pending_wechat_user_for_new_phone() {
let store = build_store();
let wechat_service = WechatAuthService::new(store.clone());
let phone_service = build_phone_service(store);
let now = OffsetDateTime::from_unix_timestamp(1_713_680_000).expect("valid timestamp");
let wechat_user = wechat_service
.resolve_login(ResolveWechatLoginInput {
profile: WechatIdentityProfile {
provider_uid: "wx-openid-new-phone".to_string(),
provider_union_id: Some("wx-union-new-phone".to_string()),
display_name: Some("新微信用户".to_string()),
avatar_url: None,
},
})
.await
.expect("wechat login should create pending user")
.user;
phone_service
.send_code(
SendPhoneCodeInput {
phone_number: "13800138099".to_string(),
scene: PhoneAuthScene::BindPhone,
},
now + Duration::seconds(1),
)
.await
.expect("bind phone code should send");
let bound = phone_service
.bind_wechat_phone(
BindWechatPhoneInput {
user_id: wechat_user.id.clone(),
phone_number: "13800138099".to_string(),
verify_code: "123456".to_string(),
},
now + Duration::seconds(2),
)
.await
.expect("bind phone should activate pending user");
assert_eq!(bound.user.id, wechat_user.id);
assert_eq!(bound.user.binding_status, AuthBindingStatus::Active);
assert!(bound.activated_new_user);
}
}