Browse Source

docs: 029 前端说明精简为纯接口调整与传参指引

qmj 1 day ago
parent
commit
dddbcef347
1 changed files with 54 additions and 149 deletions
  1. 54 149
      specs/029-device-trust-phone-bind/frontend-guide.md

+ 54 - 149
specs/029-device-trust-phone-bind/frontend-guide.md

@@ -1,198 +1,103 @@
-# 用户端(uni-app)前端改造指南:同设备三方登录免绑手机号
+# 前端接口调整说明:同设备三方登录免绑手机号(029)
 
-**Date**: 2026-09-20 | **后端已合并**: `test` 分支 `cb8721b` | **接口契约**: [contracts/api-contract.md](contracts/api-contract.md)(字段/错误码以此为准)
+**后端分支**: `test`(`cb8721b` 起) | **日期**: 2026-09-20
 
-> **一句话效果**:这台手机只要验证过一次手机号(短信登录或三方绑号),之后用任何三方登录(Apple/Google/LINE,包括全新的三方账号)都不再收短信,只弹一个**只读**确认框"将绑定 098\*\*\*\*4321 并登录",点确认即登录
+> 一句话:登录请求多传一个可选字段 `deviceId`;三方登录多一种返回分支 `deviceConfirm`,弹只读确认框后调一个新接口即可,全程免短信
 
----
-
-## 改动总览(6 项)
+## deviceId 字段说明
 
-| # | 改动 | 性质 |
-|---|---|---|
-| 1 | 新增设备标识模块:生成/持久化/读取 `deviceId` | 新增 |
-| 2 | 3 个登录请求体加 `deviceId` 字段 | 小改 |
-| 3 | `oauthLogin` 返回处理新增 `deviceConfirm` 分支 | 小改 |
-| 4 | 新增一键确认弹窗 + 调新接口 `oauthDeviceConfirm` | 新增 |
-| 5 | LINE 授权 URL 的 `state` 改传 `deviceId`;回跳参数新增 `deviceConfirm` 分支 | 小改 |
-| 6 | 弹窗 i18n 文案(zh/tw/en/vi) | 新增 |
-
-**老版本兼容**:所有改动都是增量的——不传 `deviceId` 时后端行为与现在完全一致。可以分版本上线,不需要强同步。
+- 字符串,**可选**,长度 ≤64,所有接口通用同一个值;
+- 前端自行生成并持久化(建议:首启生成 UUID 长期保存,尽量卸载重装后不变,如 iOS 存 Keychain / Android 用 ANDROID_ID——具体实现前端自定);
+- **不传 = 与现在完全一致**,老版本 App 无影响。
 
 ---
 
-## 1. 设备标识 deviceId(核心新增)
-
-### 生成与持久化规则
+## 一、调整的 3 个现有接口(各加 1 个可选参数)
 
-- 首次启动生成一次 UUID(如 32 位小写十六进制),**之后永远复用同一个值**;
-- 长度 ≤64;三端(请求体、LINE state)用**同一个值**;
-- 取不到稳定标识时降级自生成 UUID,功能仍可用(只是卸载重装后要重新绑一次)。
+### 1. `POST /infouser/user/lodeing`(手机号+验证码登录)
 
-### 各平台建议
-
-| 平台 | 首选 | 说明 |
-|---|---|---|
-| iOS | **Keychain 原生插件**存 UUID | Keychain 数据卸载重装不丢 → 重装后依然免绑。uni-app 插件市场搜 "Keychain"(uts/原生插件) |
-| iOS(降级) | `plus.storage` / `uni.setStorageSync` | 重装后 UUID 会重新生成 → 重装后需重新绑一次(可接受) |
-| Android | **ANDROID_ID** | 同设备 + 同签名跨重装稳定、免权限、恢复出厂才变(见下方代码) |
-| Android(降级) | 自生成 UUID 存 storage | 同上 |
-
-### 示例代码(按项目实际封装调整)
-
-```js
-// device-id.js —— App 启动时调用一次 getDeviceId(),之后全局复用返回值
-const KEY = 'app_device_id';
-
-export function getDeviceId() {
-  let id = uni.getStorageSync(KEY);
-  if (id) return id;
-
-  // Android:优先系统 ANDROID_ID(跨重装稳定)
-  // #ifdef APP-PLUS
-  if (uni.getSystemInfoSync().platform === 'android') {
-    try {
-      const Secure = plus.android.importClass('android.provider.Settings$Secure');
-      id = Secure.getString(plus.android.runtimeMainActivity().getContentResolver(), 'android_id');
-    } catch (e) { /* 忽略,走降级 */ }
-  }
-  // iOS:有 Keychain 插件则读写插件,见插件文档(本示例省略)
-  // #endif
-
-  if (!id || id.length > 64) {
-    id = genUuid(); // 32 位小写十六进制
-  }
-  uni.setStorageSync(KEY, id);
-  return id;
-}
-
-function genUuid() {
-  return 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'.replace(/x/g, () =>
-    Math.floor(Math.random() * 16).toString(16));
-}
+```json
+{ "phone": "0987654321", "code": "123456", "deviceId": "a3f8c2d4..." }
 ```
 
----
+返回不变。传了 `deviceId` 后,后端会记住"这台设备验证过这个手机号",之后三方登录才能免绑。
 
-## 2. 登录请求体加 deviceId(3 个接口)
-
-在**现有请求对象**上加一个可选字段即可:
-
-| 接口 | 改动 |
-|---|---|
-| `POST /infouser/user/lodeing` | body + `deviceId` |
-| `POST /infouser/user/oauthLogin` | body + `deviceId` |
-| `POST /infouser/user/oauthBindPhone` | body + `deviceId` |
+### 2. `POST /infouser/user/oauthLogin`(三方登录)
 
 ```json
-{ "phone": "0987654321", "code": "123456", "deviceId": "a3f8c2..." }
+{ "provider": "apple", "credential": "<idToken>", "deviceId": "a3f8c2d4..." }
 ```
 
----
-
-## 3. oauthLogin 返回三分支处理
+**返回由 2 种变 3 种**(`data` 内容判断):
 
-| `data` 内容 | 条件 | 处理 |
+| `data.status` | 含义 | 前端处理 |
 |---|---|---|
-| `token` + 用户信息 | 三方身份已绑定 | 现状不变:直接登录 |
-| `{status:"needPhone", tempKey}` | 未绑定、设备无信任 | 现状不变:弹手机号+短信绑定 UI |
-| `{status:"deviceConfirm", tempKey, maskedPhone}` **新增** | 未绑定、设备已信任 | 弹一键确认框(第 4 节) |
+| 无(直接返回 `token`+用户) | 三方身份已绑定 | 现状不变,直接登录 |
+| `"needPhone"` + `tempKey` | 未绑定,设备无信任 | 现状不变:弹手机号+短信绑定 UI → `oauthBindPhone` |
+| `"deviceConfirm"` + `tempKey` + `maskedPhone` **新增** | 未绑定,但本设备验证过手机号 | 弹确认框(见下)→ 确认后调新接口 `oauthDeviceConfirm` |
 
-`maskedPhone` 格式:前 3 + `****` + 后 4(如 `098****4321`),**仅用于展示**,不要当手机号提交
+`maskedPhone` 为脱敏手机号(如 `098****4321`),**只读展示用,不要提交**。
 
----
+### 3. `POST /infouser/user/oauthBindPhone`(三方绑手机号)
 
-## 4. 一键确认弹窗 + 新接口(新增)
+```json
+{ "tempKey": "xxx", "phone": "0987654321", "code": "123456", "deviceId": "a3f8c2d4..." }
+```
 
-### 弹窗规范
+返回不变,传 `deviceId` 只是为了让后端记录设备信任。
 
-```
-标题:绑定手机号
-正文:将绑定 {maskedPhone} 并登录
-按钮:[确认]  [取消]
-```
+---
 
-- 手机号**只读展示,不可编辑**,弹窗内不出现手机号输入框和验证码输入框(后端也不支持换号,换号走"取消后手动绑定");
-- 取消:直接关闭弹窗,无任何副作用(后端什么都没发生),回到登录页。
+## 二、新增 1 个接口:`POST /infouser/user/oauthDeviceConfirm`
 
-### 确认后调用
+用于 `deviceConfirm` 分支的一键确认(免短信)。
 
-```
-POST /infouser/user/oauthDeviceConfirm
-Content-Type: application/json
+**入参**:
 
+```json
 { "tempKey": "oauthLogin 返回的 tempKey", "deviceId": "本机 deviceId" }
 ```
 
-**成功**:响应结构与 `oauthBindPhone` 成功完全一致(`msg` + `user` + `token`),按现有登录成功逻辑处理(存 token、跳首页)
+**成功**:返回结构与 `oauthBindPhone` 成功完全一致(`msg` + `user` + `token`),按现有登录成功逻辑处理。特殊:若确认期间该三方身份已被其它端绑定,**不报错**,直接返回 token 登录成功
 
-**失败**处理(`code=500`,`msg` 为后端 i18n 文案,可直接 toast `msg`):
+**失败**(`code=500`,`msg` 可直接 toast):
 
-| 错误(按 msg 判断或直接透传) | 建议 UI |
+| msg 含义 | 处理 |
 |---|---|
-| 登录凭证已过期(`no.oauth.tempkey.expired`) | 提示后回登录页,用户重新点三方登录拿新 tempKey |
-| 设备确认无效(`no.oauth.device.mismatch`) | 提示后回登录页(可回落到手动绑定流程) |
-| 账号已停用(`no.user.stop`) | 提示账号停用,回登录页 |
+| 登录凭证已过期(tempKey 5 分钟有效) | 提示后回登录页,重新点三方登录 |
+| 设备确认无效(tempKey 不是 deviceConfirm 凭证 / deviceId 不匹配 / 设备无信任) | 提示后回登录页 |
+| 账号已停用 | 提示账号停用,回登录页 |
 
-**注意**:确认期间该三方身份若已被其它端绑定,接口**不会报错**而是直接返回 token 登录成功——按成功处理即可
+**确认弹窗规范**:正文"将绑定 {maskedPhone} 并登录",手机号**只读不可编辑**,弹窗内无手机号/验证码输入框;点"取消"直接关闭,无任何副作用
 
 ---
 
-## 5. LINE 登录改造(网页回调链路)
-
-### 唯一必改点:authorize URL 的 `state`
+## 三、LINE 登录(网页回调链路):`state` 传参调整
 
-把原来的随机串换成 `deviceId`(其余参数不动)
+**唯一改动**:打开 LINE 授权 URL 时,`state` 参数从随机串改为传 `deviceId`:
 
 ```
-https://access.line.me/oauth2/v2.1/authorize
-  ?response_type=code
-  &client_id=...
-  &redirect_uri=https%3A%2F%2Fapi.awayqtw.com%2Fauth%2Fline%2Fcallback
-  &scope=profile%20openid
-  &state={deviceId}          ← 原来是随机串,现在传 deviceId
+https://access.line.me/oauth2/v2.1/authorize?...&state={deviceId}
 ```
 
-### 302 回跳 scheme 参数分支(`com.twanmsdyh.app://oauthLogin?...`)
-
-| 参数 | 条件 | 处理 |
-|---|---|---|
-| `token=xxx` | 已绑定 | 现状不变 |
-| `needPhone=1&tempKey=xxx` | 未绑定、无信任 | 现状不变(手动绑定 UI) |
-| `deviceConfirm=1&tempKey=xxx&maskedPhone=098****4321` **新增** | 未绑定、设备已信任 | 弹第 4 节同款确认框(`maskedPhone` 记得 URL 解码),确认 → 调 `oauthDeviceConfirm` |
-| `error=xxx` | 异常 | 现状不变 |
-
-> 前端自拿 code 走 `POST /oauthLogin` 的场景(H5 等):body 带 `deviceId` 即可自动生效,返回分支同第 3 节。
-
----
-
-## 6. 弹窗 i18n 文案建议(key 用英文驼峰,与现有语言文件层级规范一致)
+**302 回跳参数**(`com.twanmsdyh.app://oauthLogin?...`)由 3 种变 4 种:
 
-| key | zh | tw | en | vi |
-|---|---|---|---|---|
-| `deviceConfirmTitle` | 绑定手机号 | 綁定手機號 | Bind Phone | Liên kết số điện thoại |
-| `deviceConfirmMsg` | 将绑定 {phone} 并登录 | 將綁定 {phone} 並登入 | Will bind {phone} and log in | Sẽ liên kết {phone} và đăng nhập |
-| `deviceConfirmBtn` | 确认绑定 | 確認綁定 | Confirm | Xác nhận |
+| 参数 | 处理 |
+|---|---|
+| `token=xxx` | 现状不变 |
+| `needPhone=1&tempKey=xxx` | 现状不变 |
+| `deviceConfirm=1&tempKey=xxx&maskedPhone=098****4321` **新增** | 弹同款确认框(`maskedPhone` 记得 URL 解码)→ 调 `oauthDeviceConfirm` |
+| `error=xxx` | 现状不变 |
 
-(取消按钮、错误 toast 建议复用现有通用 key;`{phone}` 用后端返回的 `maskedPhone` 填充。)
+前端自拿 code 走 `POST /oauthLogin` 的场景:body 带 `deviceId` 即自动生效,分支同上表。
 
 ---
 
-## 7. 联调自测清单
-
-1. 手机号+短信登录(带 deviceId)→ 再用**全新**的 Apple/Google 身份登录 → 应弹确认框,`maskedPhone` = 刚登录的号。
-2. 确认 → 直接进 App,"我的"页手机号 = 该账号。
-3. 取消 → 再换 Google 新身份登录 → 仍弹确认框(未产生绑定)。
-4. 不带 deviceId(模拟老版本)→ 三方登录走原 needPhone 流程,行为与现在一致。
-5. 等待 5 分钟后点确认 → toast "登录凭证已过期",回登录页重试成功。
-6. LINE:state 传 deviceId → 已信任设备走 `deviceConfirm=1` 回跳分支,弹窗与确认流程同 1-2。
-7. 卸载重装(iOS Keychain / Android ANDROID_ID 生效时)→ 三方登录仍免绑。
-
----
+## 四、改动清单(速查)
 
-## 8. 注意事项
+1. `lodeing` / `oauthLogin` / `oauthBindPhone` 请求体 + `deviceId`(可选)
+2. `oauthLogin` 返回新增 `deviceConfirm` 分支 → 新确认弹窗
+3. 新接口 `oauthDeviceConfirm {tempKey, deviceId}`
+4. LINE authorize 的 `state` = `deviceId`,回跳新增 `deviceConfirm=1` 分支
 
-- `tempKey` 5 分钟有效,**不要缓存复用**,每次弹窗都用当次返回的;
-- `deviceId` 异常值(空/超长)后端按未传处理,不会报错;
-- LINE 的 `state` 从随机串改为 deviceId 不影响安全性(后端原本就不校验 state 的 CSRF 随机性);
-- 后端接口详细字段、错误码以 [contracts/api-contract.md](contracts/api-contract.md) 为准,两边文档冲突时以契约为准并反馈后端修正。
+字段与错误码细节见 [contracts/api-contract.md](contracts/api-contract.md)。