use std::{collections::HashMap, sync::Arc};
use crate::rediscache::RedisPool;
use crate::response::error::{AppError, AppResult};
use crate::sms::aliyun::Aliyun;
use crate::sms::tencent::{Region, Tencent};
#[derive(Debug, Clone)]
pub struct CaptchaTemplate {
pub code: String,
}
impl CaptchaTemplate {
pub fn to_aliyun_template_param_json(&self) -> String {
format!(r#"{{"code":"{}"}}"#, self.code)
}
pub fn to_tencent_template_param_vec(&self) -> Vec<String> {
vec![self.code.clone()]
}
}
#[derive(Debug, Clone)]
pub enum SmsProviderConfig {
Aliyun(AliyunSmsConfig),
Tencent(TencentSmsConfig),
}
#[derive(Debug, Clone)]
pub struct AliyunSmsConfig {
pub access_key_id: String,
pub access_key_secret: String,
pub sign_name: String,
pub template_code: String,
}
#[derive(Debug, Clone)]
pub struct TencentSmsConfig {
pub secret_id: String,
pub secret_key: String,
pub sms_app_id: String,
pub region: Region,
pub sign_name: String,
pub template_id: String,
}
#[derive(Debug, Clone)]
pub struct SmsConfig {
pub debug: bool,
pub provider: SmsProviderConfig,
}
#[derive(Debug, Clone)]
pub struct SmsSendResult {
pub provider: &'static str,
pub request_id: Option<String>,
pub raw_code: Option<String>,
pub raw_message: Option<String>,
}
pub struct SmsService;
impl SmsService {
pub async fn send_captcha(
config: &Arc<SmsConfig>,
redis_pool: &Arc<RedisPool>,
mobile: &str,
redis_key_prefix: &str,
mobile_regex: ®ex::Regex,
) -> AppResult<()> {
Self::send_captcha_with_options(
config,
redis_pool,
mobile,
redis_key_prefix,
mobile_regex,
60 * 5,
true,
)
.await
.map(|_| ())
}
pub async fn send_captcha_with_options(
config: &Arc<SmsConfig>,
redis_pool: &Arc<RedisPool>,
mobile: &str,
redis_key_prefix: &str,
mobile_regex: ®ex::Regex,
expire_seconds: u64,
_delete_on_mismatch: bool,
) -> AppResult<SmsSendResult> {
if !mobile_regex.is_match(mobile) {
return Err(AppError::ClientError("手机号码格式不正确".to_string()));
}
let code_num: u32 = rand::random::<u32>() % 900000 + 100000;
let template = CaptchaTemplate {
code: code_num.to_string(),
};
tracing::info!(
"「send_captcha」 mobile: {}, code: {}",
mobile,
template.code
);
if config.debug {
Self::store_captcha_code_with_options(
redis_pool,
mobile,
code_num,
expire_seconds,
redis_key_prefix,
)
.await?;
tracing::warn!("「send_captcha」 Debug mode: SMS not sent, code stored in Redis");
return Ok(SmsSendResult {
provider: "debug",
request_id: None,
raw_code: Some("OK".to_string()),
raw_message: Some("debug mode".to_string()),
});
}
let send_result = Self::send_via_provider(config, mobile, &template).await?;
Self::store_captcha_code_with_options(
redis_pool,
mobile,
code_num,
expire_seconds,
redis_key_prefix,
)
.await?;
tracing::info!("「send_captcha」 SMS sent and code stored successfully");
Ok(send_result)
}
async fn send_via_provider(
config: &Arc<SmsConfig>,
mobile: &str,
template: &CaptchaTemplate,
) -> AppResult<SmsSendResult> {
match &config.provider {
SmsProviderConfig::Aliyun(aliyun_cfg) => {
let aliyun = Aliyun::new(&aliyun_cfg.access_key_id, &aliyun_cfg.access_key_secret);
let resp: HashMap<String, String> = aliyun
.send_sms(
mobile,
&aliyun_cfg.sign_name,
&aliyun_cfg.template_code,
&template.to_aliyun_template_param_json(),
)
.await
.map_err(|e| AppError::ClientError(format!("短信发送失败(Aliyun): {}", e)))?;
match resp.get("Code").map(|s| s.as_str()) {
Some("OK") => Ok(SmsSendResult {
provider: "aliyun",
request_id: resp.get("RequestId").cloned(),
raw_code: resp.get("Code").cloned(),
raw_message: resp.get("Message").cloned(),
}),
_ => Err(AppError::ClientError(format!(
"发送短信失败(Aliyun): {}",
resp.get("Message")
.cloned()
.unwrap_or_else(|| "Unknown error".to_string())
))),
}
}
SmsProviderConfig::Tencent(tencent_cfg) => {
let tencent = Tencent::new(
tencent_cfg.secret_id.clone(),
tencent_cfg.secret_key.clone(),
tencent_cfg.sms_app_id.clone(),
);
let phone = if mobile.starts_with('+') {
mobile.to_string()
} else {
format!("+86{}", mobile)
};
let params = template
.to_tencent_template_param_vec()
.into_iter()
.map(|s| s.to_string())
.collect::<Vec<String>>();
let params_ref = params.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
let resp = tencent
.send_sms(
tencent_cfg.region.clone(),
&tencent_cfg.sign_name,
vec![phone.as_str()],
tencent_cfg.template_id.clone(),
params_ref,
)
.await
.map_err(|e| AppError::ClientError(format!("短信发送失败(Tencent): {}", e)))?;
let status = resp
.response
.send_status_set
.get(0)
.cloned()
.ok_or_else(|| {
AppError::ClientError("发送短信失败(Tencent): empty response".to_string())
})?;
if status.code.eq_ignore_ascii_case("Ok") {
Ok(SmsSendResult {
provider: "tencent",
request_id: Some(resp.response.request_id),
raw_code: Some(status.code),
raw_message: Some(status.message),
})
} else {
Err(AppError::ClientError(format!(
"发送短信失败(Tencent): {}",
status.message
)))
}
}
}
}
pub async fn valid_auth_captcha(
redis_pool: &Arc<RedisPool>,
mobile: &str,
captcha: &str,
redis_key_prefix: &str,
delete: bool,
) -> AppResult<()> {
let code = Self::get_captcha_code(redis_pool, mobile, redis_key_prefix).await?;
match code {
Some(code) => {
if code != captcha {
Self::delete_captcha_code(redis_pool, mobile, redis_key_prefix).await?;
tracing::warn!(
"「valid_auth_captcha」 failed mobile:{}, captcha:{}",
mobile,
captcha
);
Err(AppError::ClientError("验证码错误".to_string()))
} else {
if delete {
Self::delete_captcha_code(redis_pool, mobile, redis_key_prefix).await?;
}
tracing::info!(
"「valid_auth_captcha」 success mobile:{} captcha:{}",
mobile,
captcha
);
Ok(())
}
}
None => Err(AppError::ClientError("验证码已过期".to_string())),
}
}
pub async fn store_captcha_code(
redis_pool: &Arc<RedisPool>,
mobile: &str,
code: u32,
redis_key_prefix: &str,
) -> AppResult<()> {
Self::store_captcha_code_with_options(redis_pool, mobile, code, 60 * 5, redis_key_prefix)
.await
}
pub async fn store_captcha_code_with_options(
redis_pool: &Arc<RedisPool>,
mobile: &str,
code: u32,
expire_seconds: u64,
key_prefix: &str,
) -> AppResult<()> {
let key = format!("{}{}", key_prefix, mobile);
let value = code.to_string();
redis_pool
.setex(&key, &value, expire_seconds)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
tracing::info!(
"「store_captcha_code」 验证码已存储: key={}, expire_seconds={}",
key,
expire_seconds
);
Ok(())
}
pub async fn get_captcha_code(
redis_pool: &Arc<RedisPool>,
mobile: &str,
redis_key_prefix: &str,
) -> AppResult<Option<String>> {
let key = format!("{}{}", redis_key_prefix, mobile);
match redis_pool.get(&key).await {
Ok(Some(value)) => Ok(Some(value)),
Ok(None) => Ok(None),
Err(e) => Err(AppError::RedisError(e.to_string())),
}
}
pub async fn delete_captcha_code(
redis_pool: &Arc<RedisPool>,
mobile: &str,
redis_key_prefix: &str,
) -> AppResult<()> {
let key = format!("{}{}", redis_key_prefix, mobile);
redis_pool
.del(&key)
.await
.map_err(|e| AppError::RedisError(e.to_string()))?;
tracing::info!("「delete_captcha_code」 验证码已删除: mobile={}", mobile);
Ok(())
}
}