Skip to content

OAuth 前后联调实录 —— 实测差异与调试循环 ​

读者:前端开发者(qm-frontend / uniapp 基座真机调试) 性质:代码看着对、真机必踩的实测差异清单 + 后台↔前端调试循环规程 基线:后端 V2.0.42(生产 api.qingmulife.cn HTTPS)· 2026-09-17 首版 · 2026-09-20 补 D9-D11(登录 API 审查)铁律:契约以本文 + actions.md 实测版为准,不以下方任何单方代码为准

⚠️ D9-D11 需要后端先上迁移 0021/0022 —— 前端联调前请确认生产已部署对应版本。


一、实测差异清单(按踩坑顺序) ​

D1 · captcha 渲染:三层坑,唯一正解「原生 image + 真 PNG + 后端 ≥V2.0.43」 ​

  • 层 1(svg-icon 组件):uni_modules/svg-icon APP-PLUS 分支走 renderjs 手绘(svgPayload/change: 监听),对网络动态返回的 svg 源码与 dataURL 均渲染失败(空白、无报错)→ 弃用 svg-icon,换原生 <image>
  • 层 2(svg dataURL):后端 V2.0.42 及之前 png 字段名不副实(实为 data:image/svg+xml;base64)。Android 原生 image 不渲染 SVG dataURL(位图解码器不含 SVG)→ 依然空白
  • 层 3(修复,V2.0.43):后端 Pillow 画真 PNG(data:image/png;base64),逐字符 ±15° 旋转 + 干扰线,抗指纹扰动保留;svg 回退字段不变
  • 前端正解(三个条件缺一不可):
    html
    <image :src="res.data.png" mode="aspectFit" @error="..." @load="..." />
    1. 用原生 image(不是 svg-icon)
    2. 用 png 字段
    3. 确认后端 ≥ V2.0.43(png 前缀必须是 data:image/png;base64,;data:image/svg+xml = 后端旧版,先升后端)
  • 前端自查:绑定 @load/@error 观测加载态;catch 分支 toast 错误(勿静默 — 静默会让空白页无从定位)

D2 · mySessions 响应结构 ​

  • data.sessions[] —— 包一层 sessions 数组,不是裸数组
  • 当前设备标记字段是 current,不是 isCurrent(前端第一版写错,模板与类型两处都要对)

D3 · revokeSession 入参键 ​

  • 权威键 target(clientId 为兼容旧键);缺 key → 40001001
  • ⚠️ 后端 V2.0.41 及之前只读 clientId,传 target 会炸成 40201007 兜底 — 后端 < V2.0.42 时前端传 clientId 键

D4 · HTTP 401 三档语义(前端拦截器必读) ​

业务码含义前端动作
40101011access 无效/过期rotate 单飞 → 重放一次 → 仍 401 才登出
40101012refresh 重放连坐直接登出(rotate 无意义)
40101014设备信任 90 天窗口过直接登出(引导重新登录)
40101015revoke 目标会话不存在toast 即止(业务性 401,操作者会话不受影响)
  • ⚠️ 宽限窗:rotate 后 60s 内同 refresh 重放 → 返回同一对新 token(code 0,Auth0 overlap 设计)— 不是漏洞;60s 后重放 → 40101012

D5 · rotate 请求形态(uni-app 版本坑) ​

  • 本工程(Vue3 现代 uni-app)await uni.request() 直接 resolve 响应对象,不是 [err, res] 元组(旧版约定)
  • 前端 request 层统一用 callback 形式(与 utils/request.ts 惯例一致),零版本风险
  • rotate 的 Authorization 带 refresh token;logout/unbind/revokeSession 带 access token

D6 · 限流组合拳(联调脚本/手工测试必读) ​

  • 同手机号 sms-send 60s 冷却(40101005「发送间隔过短」)
  • 图形验证码一次性消费(每次发码都要 fresh captcha,40101003)
  • 连续登录测试的编排:换号 或 sleep 62s 或 直接库造数据

D7 · clientId 复合格式(设备级隔离核心) ​

  • 格式:android:{设备UUID}(UUID 首装随机生成并持久化;后端校验 ^[A-Za-z0-9_-]{1,20}:[A-Za-z0-9-]{1,40}$)
  • 卸载重装 = 新设备(符合安全语义)
  • 同账号 >10 台设备 → LRU 自动淘汰最旧(当前设备豁免)

D8 · 双 token 存储统一 ​

  • access/refresh/expiresAt 统一走 utils/storage.ts(禁止散落 uni.setStorageSync('key_xxx'))
  • 设备身份(deviceId/clientId/deviceName)独立于会话生命周期 — 登出不清设备身份

D9 · ⭐ user.phone 可能是假的:占位号陷阱(前端必读) ​

微信登录用户从来没有真实手机号,后端为其 phone 填入派生占位号的脱敏值。 它长这样:

json
"user": { "phone": "138****6623", "phoneIsPlaceholder": true }

外观与真实绑定号完全一致 —— 光看 phone 无法分辨。若前端据此渲染:

  • ❌ 会给从没绑过手机的微信用户显示「已绑定 138****6623」
  • ❌ 无法决定要不要展示「绑定手机号」入口

正解:读 phoneIsPlaceholder(本次新增字段,两处登录响应都有):

字段含义前端动作
phoneIsPlaceholder: truephone 是占位号,未真实绑定展示「绑定手机号」入口
phoneIsPlaceholder: falsephone 是真实的脱敏号展示「已绑定 138****XXXX」

⚠️ phone 本身语义不变(仍是脱敏串,非空),不要改成判断 phone == null。

D10 · bind-phone / unbind 契约(含一处行为变更) ​

bind-phone(请求)

json
{ "action": "bind-phone",
  "payload": { "userId": "...", "phone": "13900009301", "smsCode": "****", "clientId": "ios" } }
  • Authorization: Bearer <access>(必需)
  • 目标号需先发码(sms-send),否则 40101001
  • 响应只有 { "ok": true, "token_expiring": false } —— ⚠️ 不回显绑定后的号码, 前端要显示新号须自行刷新用户信息(已知缺口,见台账 D10 备注)

🔴 行为变更(本次修复):此前该接口对任何已登录用户都返回 40101007 「已绑定手机号」—— 微信用户永远绑不上。修复后占位号不再算「已绑定」。

unbind(请求)

json
{ "action": "unbind", "payload": { "userId": "...", "target": "mobile" } }
  • ⚠️ target 只接受 "" / "mobile" / "mini" —— 它们是微信渠道, 不是手机号。传 "phone" 会得到 40001001(参数非法)。
  • 约束:解绑后必须仍保留至少一种登录方式,否则 40101007 (例:只绑了微信、没真手机号的用户不能解绑微信)

D11 · 微信登录:同 openid 必须复用同一账号 ​

同一微信身份(unionid / openid)再次登录 → 返回同一个 user.id,不是新建账号。

⚠️ 生产此前不是这样(后端缺陷:反查恒为空 → 每次登录建新号)。 本次修复后行为才正确 —— 前端若观察过「同一微信登录出多个 user.id」,那正是该缺陷, 升级后端后即消失。


二、调试循环规程(后台 ↔ 前端) ​

┌─ 后台(AI/服务端侧)
│   1. 服务端日志/watch 发现问题 → 定位根因
│   2. 修复代码(后端 fix / 前端 fix 建议)
│   3. 【本文档】差异与结论沉淀到 D 清单 + actions.md
│   4. 告知前端:改了什么、要重新编译基座吗
│        ↓
├─ 前端(HBuilderX 侧)
│   5. 编译/重新运行到手机(HBuilder 基座)
│        ↓
└─ USB 调试循环(后台 adb 驱动)
    6. adb logcat 监听 + screencap 截屏 + input tap 模拟操作
    7. 服务端 docker logs 双端对照
    8. 发现新差异 → 回到 1(如此反复)

双端观测通道:

通道命令/位置
手机 consoleadb logcat --pid=<pid> -v time(uniapp console 不一定进 logcat,以 HBuilderX 控制台为准)
手机屏幕adb exec-out screencap -p > x.png
模拟操作adb shell input tap <x> <y>(截图坐标 ×1.2 = 物理像素)
服务端请求ssh prod "docker logs qm-serverv2-app --since 1m"
服务端业务码同上 grep POST /api/v1/auth/oauth

循环纪律:

  1. 每轮只改一件事(一个 fix = 一次基座重编译),改完必记录
  2. 「数据到了没」看服务端日志,「渲染对不对」看截屏 — 两头对照定位,不猜
  3. 新差异按 D 编号追加,已解决的标注修复版本

三、差异台账 ​

#状态修复版本一句话
D1✅ 已收口(真机验证)后端 V2.0.43(真 PNG)+ 前端 2026-09-17(image 兜底)captcha 三层坑:svg-icon→svg dataURL→真 PNG;2026-09-17 11:28 Redmi 真机渲染成功 + 点击刷新正常
D2✅ 已修(前端对齐)前端 2026-09-17sessions[] + current
D3✅ 已修(后端兼容双键)V2.0.42revokeSession target 权威
D4✅ 已修(前端分档)前端 2026-09-17401 三档:rotate/强登/toast
D5✅ 已修(前端 callback 形式)前端 2026-09-17uni.request 形态坑
D6认知项(无需修)—限流组合拳
D7✅ 已接入(前端 device.ts)前端 2026-09-17复合 clientId
D8✅ 已修(前端 storage 统一)前端 2026-09-17双 token 统一存取
D9⬆️ 待前端适配(后端已就绪)后端 2026-09-20(新增 phoneIsPlaceholder)user.phone 可能是占位号假号 — 必须读 phoneIsPlaceholder 区分
D10⬆️ 待前端适配(含行为变更)后端 2026-09-20(bind-phone 修复)bind-phone 此前恒失败,现已可用;unbind 的 target 只收 ""/mobile/mini
D11✅ 后端已修后端 2026-09-20(cz_find_by_openid 实现)同 openid 复用同一账号(此前生产每次登录新建号)

四、当前联调状态(2026-09-17 11:35 — D1 收口) ​

项状态
后端V2.0.43 已部署生产(api.qingmulife.cn HTTPS,真 PNG captcha;curl 实测 png 前缀 data:image/png;base64,iVBORw0K)
前端 qm-frontend512be67(诊断版)已编译推送真机(uniapp CLI 3.0.0-5020420260813001 编译 + 基座 www 资源替换重签)
手机Redmi Note 12 Turbo(adb c9e5cc0f),muhehealth 诊断包运行中
D1 验证结果✅ 三条件全通:① logcat [captcha] res.code=0、keys=[captchaId,png,svg]、前缀 data:image/png;base64, ② 验证码图渲染成功 ③ 点击刷新正常(0090→3783)
收尾项前端 512be67 的 [captcha] 诊断打点确认无用后可删;uniapp CLI 编译骨架(package.json/vite.config.ts)可沉淀回 CNB 仓库建 build/cli 分支,脱离 HBuilderX 编译