diff --git a/docs/technical/WECHAT_LOGIN_REAL_INTEGRATION_RUNBOOK_2026-04-21.md b/docs/technical/WECHAT_LOGIN_REAL_INTEGRATION_RUNBOOK_2026-04-21.md index 6dd75c74..451bcf92 100644 --- a/docs/technical/WECHAT_LOGIN_REAL_INTEGRATION_RUNBOOK_2026-04-21.md +++ b/docs/technical/WECHAT_LOGIN_REAL_INTEGRATION_RUNBOOK_2026-04-21.md @@ -290,6 +290,8 @@ POST /api/auth/wechat/miniprogram-login 4. 若 `auth_binding_status=pending_bind_phone`,页面必须进入绑定手机号界面 5. 绑定成功后,应切回正常已登录状态 +小程序原生手机号授权链路中,请求体应携带 `wechatPhoneCode`。后端调用微信 `getuserphonenumber` 后,需要按微信原始响应字段 `phoneNumber` / `purePhoneNumber` / `countryCode` 解析手机号;如果误按 Rust 字段名 `phone_number` / `pure_phone_number` / `country_code` 解析,会出现已传 `wechatPhoneCode` 但返回“微信手机号授权失败:缺少手机号”的假失败。 + ## 10. 后端验收点 当前后端至少应满足以下检查: diff --git a/docs/technical/WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md b/docs/technical/WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md index 3584ec12..d68a146d 100644 --- a/docs/technical/WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md +++ b/docs/technical/WECHAT_MINIPROGRAM_WEB_VIEW_SHELL_2026-05-03.md @@ -132,7 +132,7 @@ Content-Type: application/json } ``` -9. `api-server` 通过微信 `stable_token` 获取小程序 `access_token`,再调用 `getuserphonenumber` 换取平台验证后的手机号,并复用现有微信待绑定账号合并逻辑。成功后重新签发 `active` 系统 token。 +9. `api-server` 通过微信 `stable_token` 获取小程序 `access_token`,再调用 `getuserphonenumber` 换取平台验证后的手机号,并复用现有微信待绑定账号合并逻辑。微信返回的手机号字段使用 `phoneNumber` / `purePhoneNumber` / `countryCode`,后端解析时必须兼容这些原始 camelCase 字段;否则会在已收到 `wechatPhoneCode` 的情况下误报“微信手机号授权失败:缺少手机号”。成功后重新签发 `active` 系统 token。 10. H5 复用 `consumeAuthCallbackResult()` 消费 `auth_token` 并进入现有登录态恢复流程。 补充:H5 里的旧短信验证码绑定页继续保留为非小程序环境兜底;小程序原生手机号授权只替代“手动输入手机号 + 短信验证码”这一步,不代表后台静默读取本机号码。 diff --git a/server-rs/crates/platform-auth/src/lib.rs b/server-rs/crates/platform-auth/src/lib.rs index 1d7be11b..ffa21519 100644 --- a/server-rs/crates/platform-auth/src/lib.rs +++ b/server-rs/crates/platform-auth/src/lib.rs @@ -358,10 +358,13 @@ struct WechatPhoneNumberResponse { #[derive(Debug, Deserialize)] struct WechatPhoneNumberInfo { #[serde(default)] + #[serde(alias = "phoneNumber")] phone_number: Option, #[serde(default)] + #[serde(alias = "purePhoneNumber")] pure_phone_number: Option, #[serde(default)] + #[serde(alias = "countryCode")] country_code: Option, } @@ -2109,6 +2112,30 @@ mod tests { ); } + #[test] + fn wechat_phone_number_response_accepts_wechat_camel_case_fields() { + let payload = serde_json::from_str::( + r#"{ + "errcode": 0, + "errmsg": "ok", + "phone_info": { + "phoneNumber": "+8613800138000", + "purePhoneNumber": "13800138000", + "countryCode": "86" + } + }"#, + ) + .expect("wechat phone number response should parse"); + let phone_info = payload.phone_info.expect("phone info should exist"); + + assert_eq!(phone_info.phone_number.as_deref(), Some("+8613800138000")); + assert_eq!( + phone_info.pure_phone_number.as_deref(), + Some("13800138000") + ); + assert_eq!(phone_info.country_code.as_deref(), Some("86")); + } + #[test] fn mock_wechat_provider_builds_callback_authorization_url() { let provider = WechatProvider::new(WechatAuthConfig::new(