轻松聘
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

10 KiB

运营商一键登录接入方案(P2Q)

状态:方案文档 · 不涉及代码改写 · 真实取号 SDK 就绪后按路线图替换 mock 即可 适用范围:轻松聘 App 运营商一键登录(本机号码认证) 关联文件:src/types/carrier-auth.ts、src/hooks/useCarrierPhone.ts、src/hooks/useLogin.ts、src/api/api.ts、components/onboarding/screens/P2QuickLogin.tsx、src/__tests__/carrier-login.test.ts 本文回答的问题:「一键登录时应当获取到手机号,为什么获取不到」—— 见第 1 节。


0. 一句话结论

在本机(Expo Go / 模拟器)跑,取号必然是假的:本机号码只能由运营商的号码认证 SDK 在端上取回,它是原生模块,Expo Go 内不存在,模拟器也没有 SIM 卡。当前 fetchCarrierPhone() 返回的是写死的号码 15000009367(掩码 150****9367),用于把页面链路跑通。 接入真实取号 = 端上换 1 处 SDK 调用(fetchCarrierPhone 函数体) + 服务器换 1 个接口(/api/auth/login-by-carrier) + 用 EAS Build 生成自定义客户端。 页面、hook、类型零改动。


1. 原先「取不到手机号」的两个真实原因(已修)

排查结论:不是取号 SDK 的问题,是页面根本没读取号结果。修复前 P2QuickLogin.tsx 有两个独立缺陷,叠加起来表现为「号码永远显示占位」:

# 缺陷 表现 现状态
1 页面读的是 data.phone —— 该字段只有短信登录通道(P2)才会写入 走一键登录进来时 data.phone 恒为空 → 号码位永远渲染占位文案「本机号码」,用户以为没取到号 ✅ 已改为读 useCarrierPhone() 的 maskedPhone
2 loginByCarrier() 返回的 phone 被直接丢弃 即使后端换了真号码,页面与后续步骤(P2P/P2R 预填、B3 联系人电话)也拿不到 ✅ 成功登录后 setData({ phone: result.phone }) 写回引导资料

顺带补上的失败分支(此前不存在,取号失败也照样显示号码、按钮照样可点):

  • carrierToken 为空 = 没取到号 → 「一键登录」置灰(空令牌调登录接口必然失败,不如不让点);
  • 号码位改为「正在获取本机号码…」/「未能获取本机号码」,并给出重新获取与改用验证码登录两个出口;
  • 取号是进页面即预取(useCarrierPhone → useAsyncData,带自动重试与请求去重),不是等用户点按钮才去取。

没修的(有意保留):P2QuickLogin 的《xx认证服务条款》不冒充能打开——该文档由运营商在认证页内提供,本仓库没有,故只弹一段如实说明;页脚服务热线为空时整块不渲染(SUPPORT_HOTLINE 为空)。


2. 现状盘点(已实现 vs 待接入)

层 文件 现状 接入动作
类型 src/types/carrier-auth.ts ✅ CarrierPhoneResult / CarrierName 已定义 无需改动
取号 hook src/hooks/useCarrierPhone.ts ✅ loading / error / refresh 已就绪 无需改动
页面流转 P2QuickLogin.tsx ✅ 取号 → 换登录态 → 写登录态 → 写回号码 → P3 无需改动
登录态 src/context/AuthContext.tsx ✅ login(token, expiresAt) 已就绪 无需改动
端上取号 src/api/api.ts · fetchCarrierPhone() ⚠️ mock:写死 15000009367 换运营商 SDK 调用(端上)
换登录态 src/api/api.ts · loginByCarrier() ⚠️ mock:直接下发 mock_token_carrier_* 换真实接口
运营商/聚合服务 —— ❌ 未开通 开通号码认证服务
构建产物 EAS ❌ 无自定义客户端 eas build --profile development

已验证的关键事实(直接取自源码,非推测):

  • loginByCarrier('') 直接 reject(本机号码未取到,请重新获取或改用验证码登录),页面因此不能靠空令牌换到登录态。
  • 登录成功后 writeAuth(result.token) 确实写入登录态(此前该页只 navigate('P3') 不写 token,是本项目曾存在的严重缺口,已于 2026-09-15 一并修复)。
  • 号码的唯一真相源是后端换取结果,不是端上预取的号码:loginByCarrier 返回的 phone 由接口层给出,页面不自己拼预取到的号码写资料。
  • 取号令牌一次性且 5 分钟内有效(CARRIER_TOKEN_TTL_MS),过期需重新取号。

3. 两个硬约束(设计已遵守,接后端不可破坏)

  1. 端上取号 ≠ 可信号码:运营商 SDK 在端上取回的号码可以被改机工具伪造,因此可信号码必须由后端拿令牌向运营商换取(/api/auth/login-by-carrier 的返回为准)。前端只把取到的号码拿去展示。
  2. 令牌一次性 + 短 TTL:carrierToken 用后即废、且 5 分钟过期。所以「取号」与「换登录态」必须紧挨着(页面级),不能缓存起来留到下个页面用;过期后唯一的正确动作是重新取号。

4. 接入路线图(按执行顺序)

Step 1 · 开通运营商号码认证服务(前置,阻塞项)

  • 任选其一:中国移动 / 联通 / 电信官方号码认证能力,或聚合方案(阿里云号码认证服务、极光、闪验等,一次接多家运营商)。
  • 需要准备:企业资质、android.package(com.qingsongpin.app,见 app.config.js)与签名 SHA256、iOS bundleIdentifier。
  • 拿到的 AppKey / AppSecret 需同时配置在**端上(SDK 初始化)与服务器(换取号码)**两侧。

Step 2 · 安装取号 SDK(Expo 插件优先)

npx expo install <所选方案的 RN 插件>
  • 原生模块需在 ios/ android/ 原生壳中注册;有 config plugin 的方案由预构建自动注入,无需手改原生代码。
  • Expo Go 一定跑不了取号(requireNativeModule 在 import 期执行,缺模块会直接崩),所以本地开发期保留 mock 分支,用 __DEV__ 或环境变量区分。

Step 3 · 生成 EAS 自定义开发客户端

eas build --profile development --platform android
eas build --profile development --platform ios
  • 与微信登录同一前提(见 微信登录接入方案.md 的 Step 3):后续所有真机验证都在此客户端中进行。
  • iOS 需要在 app.config.js 补运营商要求的隐私描述文案(expo-build-properties / infoPlist),Android 需确认运营商要求的权限未被裁剪。

Step 4 · 替换端上取号(唯一改动点 · 前端)

文件:src/api/api.ts 的 fetchCarrierPhone(),把 mock 返回换成 SDK 调用(注释里已写好变形写法):

const r = await NumberAuthSDK.getMobileNumber();   // 5 分钟内有效,用后即废
if (!r?.token) return { phone: '', maskedPhone: '', carrierToken: '', carrier: 'unknown', expiresAt: 0 };
return {
  phone: r.phone,
  maskedPhone: maskPhone(r.phone),
  carrierToken: r.token,
  carrier: carrierOf(r.operator),
  expiresAt: Date.now() + CARRIER_TOKEN_TTL_MS,
};
  • 失败一律返回空串而不是假号码 —— 页面正是靠 carrierToken === '' 判定失败并置灰按钮,返回假号码会让失败分支永远进不去。
  • maskPhone / carrierOf 已在接口层就位,页面不做脱敏。
  • 此函数签名不变,页面与 hook 不动。

Step 5 · 实现后端换号接口

接口 方法 职责 关键返回
/api/auth/carrier/number POST 端上取号的服务端旁路(可选:部分方案把 token 的校验放在这里) CarrierPhoneResult
/api/auth/login-by-carrier POST 用一次性 carrierToken 向运营商换取真实号码 → 查库 → 建号/登录 LoginResult:token + 已核验的 phone + expiresAt
  • 后端须保证:carrierToken 一次性、来源可校验、过期拒绝;号码以后端换取结果为准(前端传来的号码一律不信任)。
  • 该接口同时也是「登录即注册」入口:号码在库中不存在时应自动建档(与验证码通道一致)。

Step 6 · 清除 mock 残留

  • 删除 MOCK_CARRIER_PHONE / MOCK_CARRIER_NAME 与 fetchCarrierPhone 的 mock 分支。
  • src/__tests__/carrier-login.test.ts 中断言「mock 号码」的用例改为断言失败分支(空令牌 reject、空号码不置 ready),这部分在真实取号后依然是有效契约。

5. 手动验收路径(可复现)

  1. 在自定义客户端(非 Expo Go)上安装,插入有效 SIM 卡并开启数据流量(取号需要蜂窝网络鉴权,纯 Wi-Fi 下多数方案会失败)。
  2. 进入 P2Q → 期望不输入任何号码即展示掩码本机号码(与 SIM 卡一致),页脚显示「认证服务由 xx 提供」。
  3. 点「一键登录」→ 期望直接进 P3,且后续注册流程(P2P/P2R 预填手机号、B3 联系人电话)都用这个真号码。
  4. 关流量 / 开飞行模式 / 拔卡 → 期望号码位显示「未能获取本机号码」、按钮置灰、两个出口(重新获取 / 改用验证码登录)可用。
  5. 令牌过期后(>5 分钟再点)→ 期望提示重新取号,不出现「点了没反应」。
  6. 回归 npx tsc --noEmit 与 npx eslint . 均 0 错误。

6. 风险与待确认项

项 风险/阻塞 处置
运营商服务开通 需企业资质与签名指纹,非代码可解 Step 1 前置,未开通前无法真机验证
蜂窝网络依赖 无 SIM / 无流量 / 双卡选错卡都会取号失败 失败分支已就位;验收时明确用流量
端上号码不可信 改机工具可伪造取号结果 号码一律以后端换取结果为准(Step 5)
Expo Go 无法验证 原生模块不存在,import 期即崩 开发期保留 mock 分支 + dev build 出包
SDK 选型 各家插件维护状态不一 接入时以官方 docs 为准(AGENTS.md 要求先读 docs.expo.dev/versions/v57.0.0/)

7. 一句话给执行者

别动页面、hook、类型。 只动两处:fetchCarrierPhone()(换端上 SDK 调用,失败返回空串)和 loginByCarrier()(换真接口,号码以后端为准),配齐运营商号码认证服务 + EAS 自定义客户端,一键登录即可真机走通。