chore: checkpoint local workspace changes
This commit is contained in:
@@ -8,6 +8,7 @@ license.workspace = true
|
||||
platform-auth = { path = "../platform-auth" }
|
||||
shared-kernel = { path = "../shared-kernel" }
|
||||
time = { version = "0.3", features = ["formatting", "parsing"] }
|
||||
tracing = "0.1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -14,6 +14,7 @@ use shared_kernel::{
|
||||
normalize_optional_string, normalize_required_string, parse_rfc3339,
|
||||
};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use tracing::{info, warn};
|
||||
|
||||
const USERNAME_MIN_LENGTH: usize = 3;
|
||||
const USERNAME_MAX_LENGTH: usize = 24;
|
||||
@@ -90,6 +91,10 @@ pub struct SendPhoneCodeResult {
|
||||
pub cooldown_seconds: u64,
|
||||
pub expires_in_seconds: u64,
|
||||
pub provider_request_id: Option<String>,
|
||||
pub provider_out_id: Option<String>,
|
||||
pub provider: String,
|
||||
pub scene: String,
|
||||
pub phone_number_masked: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -102,6 +107,9 @@ pub struct PhoneLoginInput {
|
||||
pub struct PhoneLoginResult {
|
||||
pub user: AuthUser,
|
||||
pub created: bool,
|
||||
pub provider: String,
|
||||
pub provider_out_id: Option<String>,
|
||||
pub phone_number_masked: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
@@ -577,7 +585,18 @@ impl PhoneAuthService {
|
||||
input: SendPhoneCodeInput,
|
||||
now: OffsetDateTime,
|
||||
) -> Result<SendPhoneCodeResult, PhoneAuthError> {
|
||||
let scene = input.scene.clone();
|
||||
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)?;
|
||||
let national_phone_number = build_national_phone_number(&normalized_phone.e164)?;
|
||||
info!(
|
||||
scene = scene.as_str(),
|
||||
provider = self.sms_provider.kind().as_str(),
|
||||
phone_e164_masked = mask_phone_number(&normalized_phone.e164).as_str(),
|
||||
phone_national_masked = normalized_phone.masked_national_number.as_str(),
|
||||
"手机号验证码发送准备调用 provider"
|
||||
);
|
||||
self.store
|
||||
.ensure_phone_code_not_cooling_down(&normalized_phone.e164, &scene, now)?;
|
||||
let expires_at = now
|
||||
.checked_add(Duration::minutes(SMS_CODE_TTL_MINUTES))
|
||||
.ok_or_else(|| PhoneAuthError::Store("短信验证码过期时间计算溢出".to_string()))?;
|
||||
@@ -588,16 +607,27 @@ impl PhoneAuthService {
|
||||
let provider_result = self
|
||||
.sms_provider
|
||||
.send_code(SmsSendCodeRequest {
|
||||
national_phone_number: build_national_phone_number(&normalized_phone.e164)?,
|
||||
national_phone_number,
|
||||
scene: input.scene.as_str().to_string(),
|
||||
})
|
||||
.await
|
||||
.map_err(map_sms_provider_error_to_phone_error)?;
|
||||
info!(
|
||||
scene = scene.as_str(),
|
||||
provider = self.sms_provider.kind().as_str(),
|
||||
phone_e164_masked = mask_phone_number(&normalized_phone.e164).as_str(),
|
||||
phone_national_masked = normalized_phone.masked_national_number.as_str(),
|
||||
cooldown_seconds = provider_result.cooldown_seconds,
|
||||
expires_in_seconds = provider_result.expires_in_seconds,
|
||||
provider_request_id = provider_result.provider_request_id.as_deref().unwrap_or("unknown"),
|
||||
provider_out_id = provider_result.provider_out_id.as_deref().unwrap_or("unknown"),
|
||||
"手机号验证码 provider 调用成功,准备写入本地快照"
|
||||
);
|
||||
|
||||
self.store.upsert_phone_code(
|
||||
StoredPhoneCode {
|
||||
phone_number: normalized_phone.e164.clone(),
|
||||
scene: input.scene,
|
||||
scene,
|
||||
expires_at,
|
||||
last_sent_at: format_rfc3339(now).map_err(|message| {
|
||||
PhoneAuthError::Store(format!("短信验证码发送时间格式化失败:{message}"))
|
||||
@@ -612,6 +642,10 @@ impl PhoneAuthService {
|
||||
cooldown_seconds: provider_result.cooldown_seconds,
|
||||
expires_in_seconds: provider_result.expires_in_seconds,
|
||||
provider_request_id: provider_result.provider_request_id,
|
||||
provider_out_id: provider_result.provider_out_id,
|
||||
provider: self.sms_provider.kind().as_str().to_string(),
|
||||
scene: input.scene.as_str().to_string(),
|
||||
phone_number_masked: normalized_phone.masked_national_number,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -656,6 +690,9 @@ impl PhoneAuthService {
|
||||
..user
|
||||
},
|
||||
created: false,
|
||||
provider: self.sms_provider.kind().as_str().to_string(),
|
||||
provider_out_id,
|
||||
phone_number_masked: normalized_phone.masked_national_number,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -671,6 +708,9 @@ impl PhoneAuthService {
|
||||
Ok(PhoneLoginResult {
|
||||
user: created_user,
|
||||
created: true,
|
||||
provider: self.sms_provider.kind().as_str().to_string(),
|
||||
provider_out_id,
|
||||
phone_number_masked: normalized_phone.masked_national_number,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1205,7 +1245,7 @@ impl InMemoryAuthStore {
|
||||
fn upsert_phone_code(
|
||||
&self,
|
||||
code: StoredPhoneCode,
|
||||
now: OffsetDateTime,
|
||||
_now: OffsetDateTime,
|
||||
) -> Result<(), PhoneAuthError> {
|
||||
let mut state = self
|
||||
.inner
|
||||
@@ -1213,26 +1253,49 @@ impl InMemoryAuthStore {
|
||||
.map_err(|_| PhoneAuthError::Store("短信验证码仓储锁已中毒".to_string()))?;
|
||||
// 手机号和业务场景共同决定同一份验证码快照,重复发送时直接覆盖旧值。
|
||||
let key = build_phone_code_key(&code.phone_number, &code.scene);
|
||||
if let Some(stored) = state.phone_codes_by_key.get(&key).cloned() {
|
||||
let expires_at = parse_phone_code_time(&stored.expires_at, "过期时间")?;
|
||||
if expires_at > now {
|
||||
let last_sent_at = parse_phone_code_time(&stored.last_sent_at, "发送时间")?;
|
||||
let cooling_until = last_sent_at
|
||||
.checked_add(Duration::seconds(SMS_CODE_COOLDOWN_SECONDS as i64))
|
||||
.ok_or_else(|| {
|
||||
PhoneAuthError::Store("短信验证码冷却时间计算溢出".to_string())
|
||||
})?;
|
||||
if cooling_until > now {
|
||||
return Err(PhoneAuthError::SendCoolingDown {
|
||||
retry_after_seconds: seconds_until(now, cooling_until),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
state.phone_codes_by_key.insert(key, code);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_phone_code_not_cooling_down(
|
||||
&self,
|
||||
phone_number: &str,
|
||||
scene: &PhoneAuthScene,
|
||||
now: OffsetDateTime,
|
||||
) -> Result<(), PhoneAuthError> {
|
||||
let state = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| PhoneAuthError::Store("短信验证码仓储锁已中毒".to_string()))?;
|
||||
let key = build_phone_code_key(phone_number, scene);
|
||||
let Some(stored) = state.phone_codes_by_key.get(&key).cloned() else {
|
||||
return Ok(());
|
||||
};
|
||||
drop(state);
|
||||
|
||||
let expires_at = parse_phone_code_time(&stored.expires_at, "过期时间")?;
|
||||
if expires_at <= now {
|
||||
return Ok(());
|
||||
}
|
||||
let last_sent_at = parse_phone_code_time(&stored.last_sent_at, "发送时间")?;
|
||||
let cooling_until = last_sent_at
|
||||
.checked_add(Duration::seconds(SMS_CODE_COOLDOWN_SECONDS as i64))
|
||||
.ok_or_else(|| PhoneAuthError::Store("短信验证码冷却时间计算溢出".to_string()))?;
|
||||
if cooling_until <= now {
|
||||
return Ok(());
|
||||
}
|
||||
let retry_after_seconds = seconds_until(now, cooling_until);
|
||||
warn!(
|
||||
scene = scene.as_str(),
|
||||
phone_masked = mask_phone_number(phone_number).as_str(),
|
||||
retry_after_seconds,
|
||||
"手机号验证码发送命中本地冷却限制"
|
||||
);
|
||||
Err(PhoneAuthError::SendCoolingDown {
|
||||
retry_after_seconds,
|
||||
})
|
||||
}
|
||||
|
||||
fn assert_phone_code_active(
|
||||
&self,
|
||||
phone_number: &str,
|
||||
|
||||
Reference in New Issue
Block a user