chore: checkpoint local workspace changes
This commit is contained in:
@@ -17,6 +17,7 @@ rand_core = { version = "0.6", features = ["getrandom"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
shared-kernel = { path = "../shared-kernel" }
|
||||
time = { version = "0.3", features = ["std"] }
|
||||
tracing = "0.1"
|
||||
urlencoding = "2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ use sha1::Sha1;
|
||||
use sha2::{Digest, Sha256};
|
||||
use shared_kernel::{new_uuid_simple_string, normalize_optional_string, normalize_required_string};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use tracing::{info, warn};
|
||||
|
||||
pub const ACCESS_TOKEN_ALGORITHM: Algorithm = Algorithm::HS256;
|
||||
pub const DEFAULT_ACCESS_TOKEN_TTL_SECONDS: u64 = 2 * 60 * 60;
|
||||
@@ -110,7 +111,7 @@ pub struct RefreshCookieConfig {
|
||||
refresh_session_ttl_days: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum SmsAuthProviderKind {
|
||||
Mock,
|
||||
Aliyun,
|
||||
@@ -203,15 +204,16 @@ pub enum SmsProviderError {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AliyunSendSmsVerifyCodeResponse {
|
||||
#[serde(default)]
|
||||
// 阿里云 RPC 原始 JSON 使用首字母大写字段名,这里必须显式映射,避免把成功响应误判成空值。
|
||||
#[serde(default, rename = "Code")]
|
||||
code: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "Message")]
|
||||
message: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "RequestId")]
|
||||
request_id: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "Success")]
|
||||
success: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "Model")]
|
||||
model: Option<AliyunSendSmsVerifyCodeModel>,
|
||||
}
|
||||
|
||||
@@ -227,13 +229,14 @@ struct AliyunSendSmsVerifyCodeModel {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AliyunCheckSmsVerifyCodeResponse {
|
||||
#[serde(default)]
|
||||
// 校验接口同样返回首字母大写字段名,保持和发送接口一致的显式映射。
|
||||
#[serde(default, rename = "Code")]
|
||||
code: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "Message")]
|
||||
message: Option<String>,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "Success")]
|
||||
success: Option<bool>,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "Model")]
|
||||
model: Option<AliyunCheckSmsVerifyCodeModel>,
|
||||
}
|
||||
|
||||
@@ -356,6 +359,13 @@ impl SmsAuthProviderKind {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Mock => "mock",
|
||||
Self::Aliyun => "aliyun",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SmsAuthConfig {
|
||||
@@ -477,6 +487,13 @@ impl SmsAuthProvider {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn kind(&self) -> SmsAuthProviderKind {
|
||||
match self {
|
||||
Self::Mock(_) => SmsAuthProviderKind::Mock,
|
||||
Self::Aliyun(_) => SmsAuthProviderKind::Aliyun,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_code(
|
||||
&self,
|
||||
request: SmsSendCodeRequest,
|
||||
@@ -530,11 +547,25 @@ impl AliyunSmsAuthProvider {
|
||||
request: SmsSendCodeRequest,
|
||||
) -> Result<SmsSendCodeResult, SmsProviderError> {
|
||||
let provider_out_id = build_sms_provider_out_id(&request.scene, &request.national_phone_number);
|
||||
let phone_masked = mask_phone_number(&request.national_phone_number);
|
||||
let template_param = serde_json::json!({
|
||||
self.config.template_param_key.clone(): "##code##",
|
||||
"min": self.config.valid_time_seconds,
|
||||
})
|
||||
.to_string();
|
||||
info!(
|
||||
provider = "aliyun",
|
||||
scene = request.scene.as_str(),
|
||||
phone_masked = phone_masked.as_str(),
|
||||
endpoint = self.config.endpoint.as_str(),
|
||||
sign_name = self.config.sign_name.as_str(),
|
||||
template_code = self.config.template_code.as_str(),
|
||||
code_length = self.config.code_length,
|
||||
valid_time_seconds = self.config.valid_time_seconds,
|
||||
interval_seconds = self.config.interval_seconds,
|
||||
provider_out_id = provider_out_id.as_str(),
|
||||
"准备调用阿里云短信发送接口"
|
||||
);
|
||||
|
||||
let mut query = BTreeMap::new();
|
||||
query.insert("Action".to_string(), "SendSmsVerifyCode".to_string());
|
||||
@@ -596,9 +627,49 @@ impl AliyunSmsAuthProvider {
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| SmsProviderError::Upstream(format!("短信验证码发送失败:{error}")))?;
|
||||
let http_status = payload.status();
|
||||
|
||||
let body = parse_aliyun_json_response(payload, "短信验证码发送失败").await?;
|
||||
info!(
|
||||
provider = "aliyun",
|
||||
scene = request.scene.as_str(),
|
||||
phone_masked = phone_masked.as_str(),
|
||||
http_status = http_status.as_u16(),
|
||||
provider_code = body.code.as_deref().unwrap_or("unknown"),
|
||||
provider_message = body.message.as_deref().unwrap_or("unknown"),
|
||||
provider_request_id = body
|
||||
.request_id
|
||||
.as_deref()
|
||||
.or_else(|| body.model.as_ref().and_then(|model| model.request_id.as_deref()))
|
||||
.unwrap_or("unknown"),
|
||||
provider_out_id = body
|
||||
.model
|
||||
.as_ref()
|
||||
.and_then(|model| model.out_id.as_deref())
|
||||
.unwrap_or("unknown"),
|
||||
success = body.success.unwrap_or(false),
|
||||
"阿里云短信发送接口返回响应"
|
||||
);
|
||||
if !body.success.unwrap_or(false) || body.code.as_deref() != Some("OK") {
|
||||
warn!(
|
||||
provider = "aliyun",
|
||||
scene = request.scene.as_str(),
|
||||
phone_masked = phone_masked.as_str(),
|
||||
http_status = http_status.as_u16(),
|
||||
provider_code = body.code.as_deref().unwrap_or("unknown"),
|
||||
provider_message = body.message.as_deref().unwrap_or("unknown"),
|
||||
provider_request_id = body
|
||||
.request_id
|
||||
.as_deref()
|
||||
.or_else(|| body.model.as_ref().and_then(|model| model.request_id.as_deref()))
|
||||
.unwrap_or("unknown"),
|
||||
provider_out_id = body
|
||||
.model
|
||||
.as_ref()
|
||||
.and_then(|model| model.out_id.as_deref())
|
||||
.unwrap_or("unknown"),
|
||||
"阿里云短信发送接口返回业务失败"
|
||||
);
|
||||
return Err(map_aliyun_provider_error(
|
||||
"短信验证码发送失败",
|
||||
body.message,
|
||||
@@ -1173,6 +1244,23 @@ fn build_provider_error_message(prefix: &str, provider_message: &str) -> String
|
||||
}
|
||||
}
|
||||
|
||||
fn mask_phone_number(phone_number: &str) -> String {
|
||||
let chars: Vec<char> = phone_number.chars().collect();
|
||||
if chars.len() <= 4 {
|
||||
return "*".repeat(chars.len().max(1));
|
||||
}
|
||||
let prefix_len = chars.len().min(3);
|
||||
let suffix_len = 4.min(chars.len().saturating_sub(prefix_len));
|
||||
let mask_len = chars.len().saturating_sub(prefix_len + suffix_len);
|
||||
let mut masked = String::new();
|
||||
masked.extend(chars.iter().take(prefix_len));
|
||||
masked.push_str(&"*".repeat(mask_len.max(1)));
|
||||
if suffix_len > 0 {
|
||||
masked.extend(chars.iter().skip(chars.len() - suffix_len));
|
||||
}
|
||||
masked
|
||||
}
|
||||
|
||||
impl fmt::Display for SmsProviderError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
@@ -1469,4 +1557,65 @@ mod tests {
|
||||
"Action=SendSmsVerifyCode&PhoneNumber=13800138000&TemplateParam=%7B%22code%22%3A%22%23%23code%23%23%22%7D"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliyun_send_response_deserializes_pascal_case_fields() {
|
||||
let payload = serde_json::from_str::<AliyunSendSmsVerifyCodeResponse>(
|
||||
r#"{
|
||||
"Code": "OK",
|
||||
"Message": "成功",
|
||||
"RequestId": "req_123",
|
||||
"Success": true,
|
||||
"Model": {
|
||||
"BizId": "biz_456",
|
||||
"OutId": "out_789",
|
||||
"RequestId": "req_model_001"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("aliyun send response should deserialize");
|
||||
|
||||
assert_eq!(payload.code.as_deref(), Some("OK"));
|
||||
assert_eq!(payload.message.as_deref(), Some("成功"));
|
||||
assert_eq!(payload.request_id.as_deref(), Some("req_123"));
|
||||
assert_eq!(payload.success, Some(true));
|
||||
assert_eq!(
|
||||
payload.model.as_ref().and_then(|model| model.out_id.as_deref()),
|
||||
Some("out_789")
|
||||
);
|
||||
assert_eq!(
|
||||
payload
|
||||
.model
|
||||
.as_ref()
|
||||
.and_then(|model| model.request_id.as_deref()),
|
||||
Some("req_model_001")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn aliyun_verify_response_deserializes_pascal_case_fields() {
|
||||
let payload = serde_json::from_str::<AliyunCheckSmsVerifyCodeResponse>(
|
||||
r#"{
|
||||
"Code": "OK",
|
||||
"Message": "成功",
|
||||
"Success": true,
|
||||
"Model": {
|
||||
"OutId": "out_789",
|
||||
"VerifyResult": "PASS"
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("aliyun verify response should deserialize");
|
||||
|
||||
assert_eq!(payload.code.as_deref(), Some("OK"));
|
||||
assert_eq!(payload.message.as_deref(), Some("成功"));
|
||||
assert_eq!(payload.success, Some(true));
|
||||
assert_eq!(
|
||||
payload
|
||||
.model
|
||||
.as_ref()
|
||||
.and_then(|model| model.verify_result.as_deref()),
|
||||
Some("PASS")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user