轻松聘
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.
 
 
 

13 KiB

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

状态:方案文档 · 真实取号 SDK 就绪后按路线图替换 mock 即可 适用范围:轻松聘 App 运营商一键登录(本机号码认证) 关联文件:src/types/carrier-auth.ts、src/lib/carrier-sdk.ts(取号适配层)、src/hooks/useCarrierPhone.ts、src/hooks/useLogin.ts、src/api/api.ts、components/onboarding/screens/P2QuickLogin.tsx、src/__tests__/carrier-login.test.ts、src/__tests__/carrier-sdk.test.ts 参考:https://www.cnblogs.com/Frankada/p/15689036.html (本机号码一键登录原理:三大运营商能力边界 + 取号/校验方法) 本文回答的问题:「一键登录时应当获取到手机号,为什么获取不到」—— 见第 1 节。


0. 一句话结论

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


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 / CarrierFailureReason / CarrierNetworkInfo 已定义 无需改动
取号 hook src/hooks/useCarrierPhone.ts ✅ loading / error / refresh / failureReason 已就绪 无需改动
页面流转 P2QuickLogin.tsx ✅ 取号 → 换登录态 → 写登录态 → 写回号码 → P3 无需改动
登录态 src/context/AuthContext.tsx ✅ login(token, expiresAt) 已就绪 无需改动
端上取号适配层 src/lib/carrier-sdk.ts ✅ 厂商无关适配层已就位(对齐 wechat-sdk.ts),现为 mock 换 SDK 调用(端上)
取号接口 src/api/api.ts · fetchCarrierPhone() ✅ 已改为调适配层,只做超时/归一化/脱敏 无需改动(改适配层即可)
换登录态 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/lib/carrier-sdk.ts(不是 api.ts —— 取号能力已隔离到适配层,与微信登录的 src/lib/wechat-sdk.ts 对称)。把下面 5 个 mock 函数体换成 SDK 调用,对外签名不变:

适配层函数 对应原生 API(Android / iOS)
isCarrierAuthAvailable() SDK 初始化结果
getCarrierNetworkInfo() getNetworkType / networkInfo
prefetchNumber(timeoutMs) getPhoneInfo / getPhoneNumberCompletion(只建缓存,不返号码)
getCarrierNumber(timeoutMs) 同上取号 → { phone, token, operator }
clearNumberCache() delScrip / delectScrip

api.ts 的 fetchCarrierPhone() 只负责三件业务规则:套超时(CARRIER_SDK_TIMEOUT_MS = 8000ms)、 用 carrierOf() 归一化运营商、调 maskPhone() 脱敏,它不碰 SDK,接真实 SDK 时不用改它。

// src/lib/carrier-sdk.ts · getCarrierNumber 真实实现示例
const r = await NumberAuthSDK.getMobileNumber();   // 5 分钟内有效,用后即废
if (!r?.token) throw new CarrierAuthError('unknown');
return { phone: r.phone, token: r.token, operator: r.operator };
  • 失败一律抛 CarrierAuthError(reason) 或返回空串,绝不返回假号码 —— 页面靠 carrierToken === '' 判定失败并置灰按钮,返回假号码会让失败分支永远进不去。
  • 运营商标识各家形状不同(中文名 / 大写缩写 / 数字码),carrierOf() 已做归一化,认不出即 unknown。

Step 4.5 · 四个不能漏的工程细节(来自运营商文档,非我们的设计选择)

细节 说明 落点
超时 SDK 默认 8s;不包装则弱信号下取号无限挂起,页面停在「正在获取本机号码…」没有出口 CARRIER_SDK_TIMEOUT_MS + guard() 超时包装
蜂窝网络预检 取号走数据网关,纯 Wi-Fi 取不到(SDK 会瞬切蜂窝再切回,但用户关流量时必然失败) getCarrierNetworkInfo(),失败原因 no-cellular
取号缓存 scrip 缓存提高成功率、降时延,且允许未开蜂窝时取号;换卡时 SDK 自动失效 prefetchNumber() 建缓存 / clearNumberCache() 主动清
敏感动作清缓存 注销、换绑手机号、退出登录后应主动 delScrip,避免下次取号命中旧缓存 clearNumberCache()(尚未接到注销/换绑流程,待补)

缓存清理目前只有函数、没有调用方 —— 等账号注销 / 换绑手机号功能落地时再接,不提前铺。

Step 5 · 实现后端换号接口

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

Step 6 · 清除 mock 残留

  • 删除 src/lib/carrier-sdk.ts 的 mock 块:MOCK_CARRIER_PHONE、mockState、setMockCarrierFailure、setMockCarrierHang(guard() 的超时包装要留,真实取号同样需要)。
  • api.ts 中 loginByCarrier() 的 mock 返回与 MOCK_CARRIER_PHONE 引用一并替换为真接口。
  • src/__tests__/carrier-sdk.test.ts 中断言 mock 行为的用例改为断言适配层契约(失败不返假号、超时必返 timeout、归一化认不出即 unknown),这部分在真实取号后依然是有效契约。

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、类型、api.ts。 只动两处:src/lib/carrier-sdk.ts 的 5 个函数体(换端上 SDK 调用,失败抛 CarrierAuthError 或返回空串)和 loginByCarrier()(换真接口,号码以后端为准),配齐运营商号码认证服务 + EAS 自定义客户端,一键登录即可真机走通。