feat: support mini program phone authorization binding
This commit is contained in:
@@ -3801,6 +3801,113 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wechat_miniprogram_bind_phone_code_activates_pending_user() {
|
||||
let config = AppConfig {
|
||||
wechat_auth_enabled: true,
|
||||
..AppConfig::default()
|
||||
};
|
||||
let app = build_router(AppState::new(config).expect("state should build"));
|
||||
|
||||
let login_response = app
|
||||
.clone()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/wechat/miniprogram-login")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-client-type", "mini_program")
|
||||
.header("x-client-runtime", "wechat_mini_program")
|
||||
.header("x-client-platform", "ios")
|
||||
.header("x-client-instance-id", "mini-bind-instance-001")
|
||||
.header("x-mini-program-app-id", "wx-mini-test")
|
||||
.header("x-mini-program-env", "develop")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"code": "wx-mini-code-bind-001"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("mini program login request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("mini program login request should succeed");
|
||||
|
||||
assert_eq!(login_response.status(), StatusCode::OK);
|
||||
let login_body = login_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("mini program login body should collect")
|
||||
.to_bytes();
|
||||
let login_payload: Value =
|
||||
serde_json::from_slice(&login_body).expect("mini program login payload should be json");
|
||||
let token = login_payload["token"]
|
||||
.as_str()
|
||||
.expect("system token should exist")
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
login_payload["bindingStatus"],
|
||||
Value::String("pending_bind_phone".to_string())
|
||||
);
|
||||
|
||||
let bind_response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth/wechat/bind-phone")
|
||||
.header("authorization", format!("Bearer {token}"))
|
||||
.header("content-type", "application/json")
|
||||
.header("x-client-type", "mini_program")
|
||||
.header("x-client-runtime", "wechat_mini_program")
|
||||
.header("x-client-platform", "ios")
|
||||
.header("x-client-instance-id", "mini-bind-instance-001")
|
||||
.header("x-mini-program-app-id", "wx-mini-test")
|
||||
.header("x-mini-program-env", "develop")
|
||||
.body(Body::from(
|
||||
serde_json::json!({
|
||||
"wechatPhoneCode": "13800138000"
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("bind request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("bind request should succeed");
|
||||
|
||||
assert_eq!(bind_response.status(), StatusCode::OK);
|
||||
assert!(
|
||||
bind_response
|
||||
.headers()
|
||||
.get("set-cookie")
|
||||
.and_then(|value| value.to_str().ok())
|
||||
.is_some_and(|value| value.contains("genarrative_refresh_session="))
|
||||
);
|
||||
let bind_body = bind_response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("bind body should collect")
|
||||
.to_bytes();
|
||||
let bind_payload: Value =
|
||||
serde_json::from_slice(&bind_body).expect("bind payload should be json");
|
||||
|
||||
assert_eq!(
|
||||
bind_payload["user"]["bindingStatus"],
|
||||
Value::String("active".to_string())
|
||||
);
|
||||
assert_eq!(bind_payload["user"]["wechatBound"], Value::Bool(true));
|
||||
assert_eq!(
|
||||
bind_payload["user"]["phoneNumberMasked"],
|
||||
Value::String("138****8000".to_string())
|
||||
);
|
||||
assert!(
|
||||
bind_payload["token"]
|
||||
.as_str()
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn wechat_bind_phone_merges_into_existing_phone_user() {
|
||||
let config = AppConfig {
|
||||
|
||||
@@ -64,6 +64,8 @@ pub struct AppConfig {
|
||||
pub wechat_access_token_endpoint: String,
|
||||
pub wechat_user_info_endpoint: String,
|
||||
pub wechat_js_code_session_endpoint: String,
|
||||
pub wechat_stable_access_token_endpoint: String,
|
||||
pub wechat_phone_number_endpoint: String,
|
||||
pub wechat_state_ttl_minutes: u32,
|
||||
pub wechat_mock_user_id: String,
|
||||
pub wechat_mock_union_id: Option<String>,
|
||||
@@ -178,6 +180,10 @@ impl Default for AppConfig {
|
||||
wechat_user_info_endpoint: "https://api.weixin.qq.com/sns/userinfo".to_string(),
|
||||
wechat_js_code_session_endpoint: "https://api.weixin.qq.com/sns/jscode2session"
|
||||
.to_string(),
|
||||
wechat_stable_access_token_endpoint: "https://api.weixin.qq.com/cgi-bin/stable_token"
|
||||
.to_string(),
|
||||
wechat_phone_number_endpoint:
|
||||
"https://api.weixin.qq.com/wxa/business/getuserphonenumber".to_string(),
|
||||
wechat_state_ttl_minutes: 15,
|
||||
wechat_mock_user_id: "wx-mock-user".to_string(),
|
||||
wechat_mock_union_id: Some("wx-mock-union".to_string()),
|
||||
@@ -426,6 +432,16 @@ impl AppConfig {
|
||||
{
|
||||
config.wechat_js_code_session_endpoint = wechat_js_code_session_endpoint;
|
||||
}
|
||||
if let Some(wechat_stable_access_token_endpoint) =
|
||||
read_first_non_empty_env(&["WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT"])
|
||||
{
|
||||
config.wechat_stable_access_token_endpoint = wechat_stable_access_token_endpoint;
|
||||
}
|
||||
if let Some(wechat_phone_number_endpoint) =
|
||||
read_first_non_empty_env(&["WECHAT_PHONE_NUMBER_ENDPOINT"])
|
||||
{
|
||||
config.wechat_phone_number_endpoint = wechat_phone_number_endpoint;
|
||||
}
|
||||
if let Some(wechat_state_ttl_minutes) =
|
||||
read_first_positive_u32_env(&["WECHAT_STATE_TTL_MINUTES"])
|
||||
{
|
||||
|
||||
@@ -5,7 +5,8 @@ use axum::{
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use module_auth::{
|
||||
AuthLoginMethod, BindWechatPhoneInput, CreateWechatAuthStateInput, WechatAuthError,
|
||||
AuthLoginMethod, BindWechatPhoneInput, BindWechatVerifiedPhoneInput,
|
||||
CreateWechatAuthStateInput, WechatAuthError,
|
||||
};
|
||||
use platform_auth::WechatAuthScene;
|
||||
use shared_contracts::auth::{
|
||||
@@ -191,18 +192,55 @@ pub async fn bind_wechat_phone(
|
||||
if !state.config.wechat_auth_enabled {
|
||||
return Err(AppError::from_status(StatusCode::BAD_REQUEST).with_message("微信登录暂未启用"));
|
||||
}
|
||||
let result = state
|
||||
.phone_auth_service()
|
||||
.bind_wechat_phone(
|
||||
BindWechatPhoneInput {
|
||||
let result = if let Some(wechat_phone_code) = payload
|
||||
.wechat_phone_code
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
let phone_profile = state
|
||||
.wechat_provider()
|
||||
.resolve_mini_program_phone_number(Some(wechat_phone_code))
|
||||
.await
|
||||
.map_err(map_wechat_provider_error)?;
|
||||
state
|
||||
.phone_auth_service()
|
||||
.bind_wechat_verified_phone(BindWechatVerifiedPhoneInput {
|
||||
user_id: authenticated.claims().user_id().to_string(),
|
||||
phone_number: payload.phone,
|
||||
verify_code: payload.code,
|
||||
},
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_wechat_bind_phone_error)?;
|
||||
phone_number: phone_profile.phone_number,
|
||||
})
|
||||
.await
|
||||
.map_err(map_wechat_bind_phone_error)?
|
||||
} else {
|
||||
let phone = payload
|
||||
.phone
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("缺少需要绑定的手机号")
|
||||
})?;
|
||||
let code = payload
|
||||
.code
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_message("缺少短信验证码")
|
||||
})?;
|
||||
state
|
||||
.phone_auth_service()
|
||||
.bind_wechat_phone(
|
||||
BindWechatPhoneInput {
|
||||
user_id: authenticated.claims().user_id().to_string(),
|
||||
phone_number: phone.to_string(),
|
||||
verify_code: code.to_string(),
|
||||
},
|
||||
OffsetDateTime::now_utc(),
|
||||
)
|
||||
.await
|
||||
.map_err(map_wechat_bind_phone_error)?
|
||||
};
|
||||
if result.activated_new_user {
|
||||
crate::registration_reward::grant_new_user_registration_wallet_reward(
|
||||
&state,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use platform_auth::{
|
||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_AUTHORIZE_ENDPOINT,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_USER_INFO_ENDPOINT, WechatAuthConfig,
|
||||
WechatProvider,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT, DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT,
|
||||
DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT, DEFAULT_WECHAT_USER_INFO_ENDPOINT,
|
||||
WechatAuthConfig, WechatProvider,
|
||||
};
|
||||
|
||||
use crate::config::AppConfig;
|
||||
@@ -30,6 +31,14 @@ pub fn build_wechat_provider(config: &AppConfig) -> WechatProvider {
|
||||
&config.wechat_js_code_session_endpoint,
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT,
|
||||
),
|
||||
normalize_wechat_endpoint(
|
||||
&config.wechat_stable_access_token_endpoint,
|
||||
DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT,
|
||||
),
|
||||
normalize_wechat_endpoint(
|
||||
&config.wechat_phone_number_endpoint,
|
||||
DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT,
|
||||
),
|
||||
config.wechat_mock_user_id.clone(),
|
||||
config.wechat_mock_union_id.clone(),
|
||||
config.wechat_mock_display_name.clone(),
|
||||
|
||||
@@ -67,6 +67,12 @@ pub struct BindWechatPhoneInput {
|
||||
pub verify_code: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BindWechatVerifiedPhoneInput {
|
||||
pub user_id: String,
|
||||
pub phone_number: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CreateRefreshSessionInput {
|
||||
pub user_id: String,
|
||||
|
||||
@@ -627,6 +627,33 @@ impl PhoneAuthService {
|
||||
activated_new_user,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn bind_wechat_verified_phone(
|
||||
&self,
|
||||
input: BindWechatVerifiedPhoneInput,
|
||||
) -> Result<BindWechatPhoneResult, PhoneAuthError> {
|
||||
let normalized_phone = normalize_mainland_china_phone_number(&input.phone_number)?;
|
||||
let current_user = self
|
||||
.store
|
||||
.find_by_user_id(&input.user_id)
|
||||
.map_err(map_password_error_to_phone_error)?
|
||||
.ok_or(PhoneAuthError::UserNotFound)?;
|
||||
if current_user.user.binding_status != AuthBindingStatus::PendingBindPhone {
|
||||
return Err(PhoneAuthError::UserStateMismatch);
|
||||
}
|
||||
if !current_user.user.wechat_bound {
|
||||
return Err(PhoneAuthError::UserStateMismatch);
|
||||
}
|
||||
|
||||
let (merged_user, activated_new_user) = self
|
||||
.store
|
||||
.bind_wechat_phone_to_user(&input.user_id, normalized_phone)?;
|
||||
|
||||
Ok(BindWechatPhoneResult {
|
||||
user: merged_user,
|
||||
activated_new_user,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl WechatAuthStateService {
|
||||
|
||||
@@ -42,6 +42,10 @@ pub const DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT: &str =
|
||||
pub const DEFAULT_WECHAT_USER_INFO_ENDPOINT: &str = "https://api.weixin.qq.com/sns/userinfo";
|
||||
pub const DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT: &str =
|
||||
"https://api.weixin.qq.com/sns/jscode2session";
|
||||
pub const DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT: &str =
|
||||
"https://api.weixin.qq.com/cgi-bin/stable_token";
|
||||
pub const DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT: &str =
|
||||
"https://api.weixin.qq.com/wxa/business/getuserphonenumber";
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
@@ -184,6 +188,8 @@ pub struct WechatAuthConfig {
|
||||
pub access_token_endpoint: String,
|
||||
pub user_info_endpoint: String,
|
||||
pub js_code_session_endpoint: String,
|
||||
pub stable_access_token_endpoint: String,
|
||||
pub phone_number_endpoint: String,
|
||||
pub mock_user_id: String,
|
||||
pub mock_union_id: Option<String>,
|
||||
pub mock_display_name: String,
|
||||
@@ -224,6 +230,15 @@ pub struct RealWechatProvider {
|
||||
access_token_endpoint: String,
|
||||
user_info_endpoint: String,
|
||||
js_code_session_endpoint: String,
|
||||
stable_access_token_endpoint: String,
|
||||
phone_number_endpoint: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct WechatPhoneNumberProfile {
|
||||
pub phone_number: String,
|
||||
pub pure_phone_number: Option<String>,
|
||||
pub country_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -325,6 +340,31 @@ struct WechatJsCodeSessionResponse {
|
||||
errmsg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WechatStableAccessTokenResponse {
|
||||
access_token: Option<String>,
|
||||
errcode: Option<i64>,
|
||||
errmsg: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WechatPhoneNumberResponse {
|
||||
errcode: Option<i64>,
|
||||
errmsg: Option<String>,
|
||||
#[serde(default)]
|
||||
phone_info: Option<WechatPhoneNumberInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct WechatPhoneNumberInfo {
|
||||
#[serde(default)]
|
||||
phone_number: Option<String>,
|
||||
#[serde(default)]
|
||||
pure_phone_number: Option<String>,
|
||||
#[serde(default)]
|
||||
country_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct AliyunSendSmsVerifyCodeResponse {
|
||||
// 阿里云 RPC 原始 JSON 使用首字母大写字段名,这里必须显式映射,避免把成功响应误判成空值。
|
||||
@@ -648,6 +688,8 @@ impl WechatAuthConfig {
|
||||
access_token_endpoint: String,
|
||||
user_info_endpoint: String,
|
||||
js_code_session_endpoint: String,
|
||||
stable_access_token_endpoint: String,
|
||||
phone_number_endpoint: String,
|
||||
mock_user_id: String,
|
||||
mock_union_id: Option<String>,
|
||||
mock_display_name: String,
|
||||
@@ -664,6 +706,8 @@ impl WechatAuthConfig {
|
||||
access_token_endpoint,
|
||||
user_info_endpoint,
|
||||
js_code_session_endpoint,
|
||||
stable_access_token_endpoint,
|
||||
phone_number_endpoint,
|
||||
mock_user_id,
|
||||
mock_union_id,
|
||||
mock_display_name,
|
||||
@@ -717,6 +761,8 @@ impl WechatProvider {
|
||||
access_token_endpoint: config.access_token_endpoint,
|
||||
user_info_endpoint: config.user_info_endpoint,
|
||||
js_code_session_endpoint: config.js_code_session_endpoint,
|
||||
stable_access_token_endpoint: config.stable_access_token_endpoint,
|
||||
phone_number_endpoint: config.phone_number_endpoint,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -755,6 +801,28 @@ impl WechatProvider {
|
||||
Self::Real(provider) => provider.resolve_mini_program_login_profile(code).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn resolve_mini_program_phone_number(
|
||||
&self,
|
||||
code: Option<&str>,
|
||||
) -> Result<WechatPhoneNumberProfile, WechatProviderError> {
|
||||
match self {
|
||||
Self::Disabled => Err(WechatProviderError::Disabled),
|
||||
Self::Mock(_) => {
|
||||
let phone_number = code
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("13800138000")
|
||||
.to_string();
|
||||
Ok(WechatPhoneNumberProfile {
|
||||
phone_number: phone_number.clone(),
|
||||
pure_phone_number: Some(phone_number),
|
||||
country_code: Some("86".to_string()),
|
||||
})
|
||||
}
|
||||
Self::Real(provider) => provider.resolve_mini_program_phone_number(code).await,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MockWechatProvider {
|
||||
@@ -990,6 +1058,141 @@ impl RealWechatProvider {
|
||||
avatar_url: None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn resolve_mini_program_phone_number(
|
||||
&self,
|
||||
code: Option<&str>,
|
||||
) -> Result<WechatPhoneNumberProfile, WechatProviderError> {
|
||||
let code = code
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or(WechatProviderError::MissingCode)?;
|
||||
let app_id = self
|
||||
.mini_program_app_id
|
||||
.as_ref()
|
||||
.or(self.app_id.as_ref())
|
||||
.ok_or_else(|| {
|
||||
WechatProviderError::InvalidConfig("微信小程序 AppID 未配置".to_string())
|
||||
})?;
|
||||
let app_secret = self
|
||||
.mini_program_app_secret
|
||||
.as_ref()
|
||||
.or(self.app_secret.as_ref())
|
||||
.ok_or_else(|| {
|
||||
WechatProviderError::InvalidConfig("微信小程序 AppSecret 未配置".to_string())
|
||||
})?;
|
||||
|
||||
let access_token = self
|
||||
.request_mini_program_access_token(app_id, app_secret)
|
||||
.await?;
|
||||
let mut phone_number_url = Url::parse(&self.phone_number_endpoint).map_err(|error| {
|
||||
WechatProviderError::InvalidConfig(format!("微信手机号接口地址非法:{error}"))
|
||||
})?;
|
||||
phone_number_url
|
||||
.query_pairs_mut()
|
||||
.append_pair("access_token", &access_token);
|
||||
|
||||
let payload = self
|
||||
.client
|
||||
.post(phone_number_url.as_str())
|
||||
.json(&serde_json::json!({ "code": code }))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
warn!(error = %error, "微信小程序手机号请求失败");
|
||||
WechatProviderError::RequestFailed("微信手机号授权失败:手机号请求失败".to_string())
|
||||
})?
|
||||
.json::<WechatPhoneNumberResponse>()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
warn!(error = %error, "微信小程序手机号响应解析失败");
|
||||
WechatProviderError::DeserializeFailed(
|
||||
"微信手机号授权失败:手机号响应非法".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(errcode) = payload.errcode.filter(|value| *value != 0) {
|
||||
return Err(WechatProviderError::Upstream(format!(
|
||||
"微信手机号授权失败:{}",
|
||||
payload
|
||||
.errmsg
|
||||
.unwrap_or_else(|| format!("getuserphonenumber 返回错误 {errcode}"))
|
||||
)));
|
||||
}
|
||||
|
||||
let phone_info = payload.phone_info.ok_or_else(|| {
|
||||
WechatProviderError::MissingProfile("微信手机号授权失败:缺少手机号信息".to_string())
|
||||
})?;
|
||||
let phone_number = phone_info
|
||||
.pure_phone_number
|
||||
.clone()
|
||||
.or(phone_info.phone_number.clone())
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
WechatProviderError::MissingProfile("微信手机号授权失败:缺少手机号".to_string())
|
||||
})?;
|
||||
|
||||
Ok(WechatPhoneNumberProfile {
|
||||
phone_number,
|
||||
pure_phone_number: phone_info.pure_phone_number,
|
||||
country_code: phone_info.country_code,
|
||||
})
|
||||
}
|
||||
|
||||
async fn request_mini_program_access_token(
|
||||
&self,
|
||||
app_id: &str,
|
||||
app_secret: &str,
|
||||
) -> Result<String, WechatProviderError> {
|
||||
let url = Url::parse(&self.stable_access_token_endpoint).map_err(|error| {
|
||||
WechatProviderError::InvalidConfig(format!("微信 stable_token 地址非法:{error}"))
|
||||
})?;
|
||||
let payload = self
|
||||
.client
|
||||
.post(url.as_str())
|
||||
.json(&serde_json::json!({
|
||||
"grant_type": "client_credential",
|
||||
"appid": app_id,
|
||||
"secret": app_secret,
|
||||
"force_refresh": false
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
warn!(error = %error, "微信小程序 stable_token 请求失败");
|
||||
WechatProviderError::RequestFailed(
|
||||
"微信手机号授权失败:access_token 请求失败".to_string(),
|
||||
)
|
||||
})?
|
||||
.json::<WechatStableAccessTokenResponse>()
|
||||
.await
|
||||
.map_err(|error| {
|
||||
warn!(error = %error, "微信小程序 stable_token 响应解析失败");
|
||||
WechatProviderError::DeserializeFailed(
|
||||
"微信手机号授权失败:access_token 响应非法".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(errcode) = payload.errcode.filter(|value| *value != 0) {
|
||||
return Err(WechatProviderError::Upstream(format!(
|
||||
"微信手机号授权失败:{}",
|
||||
payload
|
||||
.errmsg
|
||||
.unwrap_or_else(|| format!("stable_token 返回错误 {errcode}"))
|
||||
)));
|
||||
}
|
||||
|
||||
payload
|
||||
.access_token
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| {
|
||||
WechatProviderError::Upstream(
|
||||
payload
|
||||
.errmsg
|
||||
.unwrap_or_else(|| "微信手机号授权失败:缺少 access_token".to_string()),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn build_mock_wechat_authorization_url(
|
||||
@@ -1919,6 +2122,8 @@ mod tests {
|
||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_USER_INFO_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT.to_string(),
|
||||
"wx-user-001".to_string(),
|
||||
Some("wx-union-001".to_string()),
|
||||
"微信测试用户".to_string(),
|
||||
@@ -1950,6 +2155,8 @@ mod tests {
|
||||
DEFAULT_WECHAT_ACCESS_TOKEN_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_USER_INFO_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_JS_CODE_SESSION_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_STABLE_ACCESS_TOKEN_ENDPOINT.to_string(),
|
||||
DEFAULT_WECHAT_PHONE_NUMBER_ENDPOINT.to_string(),
|
||||
"wx-user-001".to_string(),
|
||||
Some("wx-union-001".to_string()),
|
||||
"微信测试用户".to_string(),
|
||||
|
||||
@@ -211,8 +211,12 @@ pub struct WechatCallbackQuery {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WechatBindPhoneRequest {
|
||||
pub phone: String,
|
||||
pub code: String,
|
||||
#[serde(default)]
|
||||
pub phone: Option<String>,
|
||||
#[serde(default)]
|
||||
pub code: Option<String>,
|
||||
#[serde(default)]
|
||||
pub wechat_phone_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
@@ -332,4 +336,23 @@ mod tests {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wechat_bind_phone_request_accepts_mini_program_phone_code() {
|
||||
let payload = serde_json::to_value(WechatBindPhoneRequest {
|
||||
phone: None,
|
||||
code: None,
|
||||
wechat_phone_code: Some("wx-phone-code-001".to_string()),
|
||||
})
|
||||
.expect("payload should serialize");
|
||||
|
||||
assert_eq!(
|
||||
payload,
|
||||
json!({
|
||||
"phone": null,
|
||||
"code": null,
|
||||
"wechatPhoneCode": "wx-phone-code-001"
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user