# 前端接口契约:设备信任免绑手机号(uni-app 用户端) **Date**: 2026-09-20 | **Spec**: [spec.md](../spec.md) | **后端分支**: `029-device-trust-phone-bind` > 本文档是后端对前端的唯一契约。前端按此实施,无需口头追问。所有接口均为现有接口的**增量**,不传新字段时行为与现网完全一致(老版本 App 无感)。 ## 0. 设备唯一标识 deviceId(前端生成) - **定义**:App 首次启动时生成的 UUID 字符串,**≤64 字符**,持久化保存,之后每次登录请求都带上。 - **持久化建议**: - **iOS**:存 Keychain(卸载重装不丢,有现成 uni-app 插件);退化用 storage 也可,只是重装后要重新绑一次。 - **Android**:优先取 `ANDROID_ID`(同设备+同签名跨重装稳定、免权限);退化用 storage 存自生成 UUID。 - **传参规则**:字符串、可选。空串/超长后端按未传处理。**不要**在未登录的其它业务接口上报。 ## 1. 入参变更(三个现有接口加可选字段 `deviceId`) | 接口 | 位置 | 变更 | |---|---|---| | `POST /infouser/user/lodeing` | UserDTO | + `deviceId`(登录成功后端记录设备信任) | | `POST /infouser/user/oauthLogin` | OAuthLoginDto | + `deviceId`(未绑定时影响返回分支,见 §2) | | `POST /infouser/user/oauthBindPhone` | OAuthBindDto | + `deviceId`(绑定成功后端记录设备信任) | ## 2. `oauthLogin` 返回新增分支 `deviceConfirm` 现有返回不变,新增一种: | 返回(AjaxResult data) | 条件 | App 行为 | |---|---|---| | `token` + 用户信息 | 三方身份已绑定 | 直接进 App(现状不变) | | `{status:"needPhone", tempKey}` | 未绑定,且设备无信任记录 | 现有输手机号+短信流程(不变) | | `{status:"deviceConfirm", tempKey, maskedPhone}` **新增** | 未绑定,且 deviceId 有信任记录 | 弹一键确认框,见 §3 | - `maskedPhone`:脱敏手机号,格式 `前3位****后4位`(如 `098****4321`),**只读展示,不可编辑、不提供换号输入**。 - tempKey 有效期 **5 分钟**,过期后确认接口返回 `no.oauth.tempkey.expired`,需重新点三方登录拿新的。 ## 3. 确认弹窗规范 - 文案示意:`将绑定 {maskedPhone} 并登录`;按钮:`确认` / `取消`。 - **确认** → 调 §4 新接口;**取消** → 关闭弹窗回登录页,不产生任何绑定(用户仍可用手机号+短信登录,或换其它三方登录)。 - 弹窗内不出现验证码输入框、不出现可编辑手机号。 ## 4. 新接口:`POST /infouser/user/oauthDeviceConfirm` **用途**:设备确认分支的一键绑定登录(免短信)。 ```json // 请求体(OAuthDeviceConfirmDto) { "tempKey": "abc123...", "deviceId": "uuid-..." } ``` **成功**:响应结构与 `oauthBindPhone` 成功完全一致(msg=登录成功、`user` 用户信息、`token`),直接进 App。 **失败**(AjaxResult error,msg 为国际化文案): | 场景 | 错误 key(参考) | App 处理建议 | |---|---|---| | tempKey 缺失 | `no.oauth.tempkey.missing` | 提示后回登录页 | | tempKey 过期/不存在 | `no.oauth.tempkey.expired` | 提示"请重新登录",回登录页 | | deviceId 缺失 / tempKey 不是设备确认凭证 / deviceId 不匹配 / 设备无信任记录 | `no.oauth.device.mismatch` | 提示后回登录页(可回落手动绑定流程) | | 信任手机号账号已停用 | `no.user.stop` | 提示账号停用,回登录页 | | 期间该三方身份已被其它端绑定 | 无错误——直接返回 token 登录成功(容错直登,与 oauthBindPhone 一致) | 直接进 App | ## 5. LINE 登录(网页回调链路) **authorize URL 变更(唯一的必改点)**:`state` 参数从随机串改为传 `deviceId`: ``` https://access.line.me/oauth2/v2.1/authorize?response_type=code&client_id=...&redirect_uri=...&scope=profile%20openid&state={deviceId} ``` **302 回跳 App(`com.twanmsdyh.app://oauthLogin`)分支变化**: | 回跳参数 | 条件 | App 行为 | |---|---|---| | `?token=xxx` | 已绑定 | 进 App(现状不变) | | `?needPhone=1&tempKey=xxx` | 未绑定、无设备信任 | 现有手机号+短信流程(不变) | | `?deviceConfirm=1&tempKey=xxx&maskedPhone=098****4321` **新增** | 未绑定、state 携带的 deviceId 有信任记录 | 弹 §3 同款确认框,确认 → §4 接口 | | `?error=xxx` | 异常 | 现状不变 | - `maskedPhone` 已做 URL 编码,App 解码后展示。 - 走 `POST /oauthLogin`(前端自拿 code 的场景,如 H5)时同样受益:body 里带 `deviceId` 即可,返回分支同 §2。 ## 6. 联调自测清单 1. 手机号+短信登录(带 deviceId)→ 卸载重装前,用 Apple 新身份登录 → 应弹 deviceConfirm(maskedPhone = 刚登录的号)。 2. deviceConfirm 确认 → 返回 token,进 App 后"我的"页手机号 = maskedPhone 对应账号。 3. deviceConfirm 取消 → 再用 Google 新身份登录 → 仍弹 deviceConfirm(未产生绑定)。 4. 不带 deviceId(模拟老版本)→ 三方新身份登录 → 走 needPhone 原流程。 5. 等 tempKey 过期(>5 分钟)后点确认 → 收到 `no.oauth.tempkey.expired`。 6. LINE:authorize state=deviceId → 已信任设备走 deviceConfirm 回跳分支。