|
|
@@ -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)。
|