# 前端接口契约:设备信任免绑手机号(uni-app 用户端) > 2026-10-30 增量见文末:同设备换手机号解绑。五字段数据库方案已确认;后端已实现,用户已确认测试库 SQL 执行完成;数据库并发及 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 回跳分支。 ## 2026-10-30 增量契约:同设备换号解绑 本节优先于上文的旧换号语义;基础请求字段沿用现有接口,不新增接口。后端代码已实现,用户已确认测试库 SQL 已执行,部署及联调状态待核对。 - 新版本用户 App 的 lodeing、oauthLogin、oauthBindPhone、oauthDeviceConfirm 均提交同一个稳定 deviceId,放在已有 POST body 内;设备 ID 不因退出或换手机号而重建。deviceToken/cid 是推送字段,不能代替 deviceId。LINE 网页回调沿用既有设备传递契约。 - lodeing:验证成功且手机号从 A 变 B,解除本设备关联的旧用户绑定后,以 B 登录;同号不解绑。 - oauthBindPhone:验证 B 并绑定登录与手机号登录 B 具有同等效力,允许 B 与设备之前的 A 不同。清理旧绑定后保留本次新绑定。不要求先独立调用 lodeing。 - oauthLogin:未绑定身份仍返回 needPhone 或 deviceConfirm;已有绑定与本设备当前用户冲突时不直接返回旧用户 token,提示先完成对应手机号验证码登录后重试。不得把旧绑定静默转给其他手机号。 - oauthDeviceConfirm:确认当前设备手机号,不接受新手机号。用户选择其他手机号时走完整验证码绑定;凭证签发后设备用户已改变则拒绝旧凭证,重新发起 OAuth。 - 新版本 App 应在换号登录成功后替换本机会话,清理旧账号的本地第三方登录缓存;后端负责解绑服务器记录,不能清除 App 本地状态。 - 无有效 deviceId 的老版本沿用既有兼容登录,无法保证按设备解绑;不要在前端宣称解绑成功。 - 成功响应结构保持现有 token/用户信息;新增校验失败沿用项目国际化业务错误包装,错误含义见下表(响应 msg 为国际化文本,key 不作为新增响应字段)。 - 同账号在设备2登录成功,按现有策略使设备1旧 token 失效;接口401仍按现有方式返回登录页。该行为不等于删除第三方绑定。 | 场景 | 后端消息 key / LINE 回跳 | App 处理 | | --- | --- | --- | | 第三方已有绑定与设备当前手机号冲突 | `no.oauth.device.phone.changed`;LINE 回跳 `error=phone_login_required` | 引导对应手机号验证码登录,再重新发起 OAuth | | 绑定过程中身份已归属于其他用户,或绑定已被删除 | `no.oauth.binding.conflict`;LINE 回跳 `error=fail` | 不接收旧账号会话,提示重新发起登录 | | 确认凭证设备或签发时用户不匹配 | `no.oauth.device.mismatch` | 关闭旧确认页,重新发起 OAuth | | tempKey 已消费/过期 | `no.oauth.tempkey.expired` | 重新发起 OAuth 获取新 tempKey | 确认缓存新增内部键 `oauth:bind:user:{tempKey}`,值为签发时 userId,TTL 同现有键为5分钟;无此快照的旧确认凭证需重新发起 OAuth。前端不传该 userId。新表未创建前不得部署本次代码。