fix(身份): 宿主页「拿到 openid 才加载 H5」+ 复用 App.vue 并行登录(不重复换 code)

背景(2026-09-20 线上排查):删掉小程序缓存重进时 H5 以访客态加载(孕周 `--`、档案空、
跳应用不带 openid,实测跳授权示例是裸 URL)。服务端证据:该设备只打了 `/api/mp/apps`,
没有任何带 openid 的 profile/pregnancy 调用 —— 宿主那次没注入身份,不是数据丢了。

根因:
1) App.vue 静默授权只处理 token 分支:v2/wechat/silence_auth 对新用户 / 强制绑定手机号返回
   {auth_login:1, key, openid, unionid}(无 token),openid 被丢掉;且 res.data.userInfo 缺失时
   取 .uid 会抛异常把后续 commit 一起带走。
2) 两个宿主页只从 store.openid / 缓存 OPENID / JWT 读身份,读不到就匿名加载(首页壳不等不重试,
   容器页 _finalizeUrl / _fetchOpenidAndLoad 兜底)。

改法:
- App.vue:补「无 token 但有 openid」分支(SET_OPENID + 缓存,unionid 同理);userInfo 判空;
  **并把这次静默登录的 promise 挂到 globalData._silenceAuthPromise**(供宿主页复用,见下)。
- mixins/webviewHost.js:新增
  · _silenceAuthOpenid()(wx.login → v2/wechat/silence_auth,兼容 auth_login/userInfo/JWT 三种
    openid 来源,拿到即落 store + 缓存,带 8s 超时保险丝);
  · _awaitHostLogin(timeout)(等 App.vue 那次登录结束,**不重复换 code**)。
- 两个宿主页统一 fail-closed「拿到 openid 才加载」,并且**第 1 次先 await App.vue 的并行登录**、
  命中缓存就直接加载;之后才自己取。**网络尝试上限 3 → 2,退避 600×n → 固定 500ms**
  (实测:code2session 约 200–300ms,重复换一次就是白花这个钱;最坏等待从 ~3.5s 降到 ~1–2.5s)。
- 首页壳:读不到身份就不设 webUrl(停在小程序自己的加载屏,文案分「取身份中/失败/页面加载失败」三态);
  2 次仍失败 → 「未获取到微信身份」+「重新获取身份」。
- 容器页:三条失败分支(有 token 无 openid 的重试失败、无 token 无 openid、onShow 仍无 openid)
  全部改走 _ensureIdentityThenLoad();删除本页重复实现的 _fetchOpenidAndLoad;新增兜底屏
  (以前 url 为空是白屏)。非可信域(商城自身域名等)不注入身份、照常直接加载。
- utils/util.js:注释里的方法名同步(_fetchOpenidAndLoad → _ensureIdentityThenLoad)。

⚠️ 提交同时包含这两处**此前已存在的未提交 WIP**(同文件、改动交织):pages/annex/web_view/index.vue
   的 mixin 重构(handleMessage→handleHostMessage 等 4 处 handler 改名)与 pages/index/index.vue
   的两行 tabBar 调整。
⚠️ 刻意 fail-closed:CRMEB silence_auth 整体不可用时会停在失败屏而不是降级加载。
⚠️ 未动 manifest.json 版本号(见另一次改动:已统一为 0.3.6 / 36)。
This commit is contained in:
2026-09-20 23:10:05 +08:00
parent c216865852
commit e99b4a475c
5 changed files with 431 additions and 346 deletions

38
App.vue
View File

@@ -227,7 +227,7 @@
let that = this;
let spid = that.globalData.spid ? that.globalData.spid : '';
console.log('[auth-flow] silenceAuth 发起, code=' + (code ? code.substring(0, 10) + '...' : '(空)'));
silenceAuth({
const authPromise = silenceAuth({
code: code,
spread_spid: spid,
spread_code: that.globalData.code
@@ -241,11 +241,30 @@
token: res.data.token,
time: time
});
that.$store.commit('SETUID', res.data.userInfo.uid);
that.$store.commit('UPDATE_USERINFO', res.data.userInfo);
/* ⚠️ 2026-09-20:`userInfo` 不是每个响应都有(v2/wechat/silence_auth
只回 token+expires_time),直接取 `.uid` 会抛异常,把后面的
commit 一起带走。加一层判空,缺了也不影响身份(openid 由 LOGIN 从 JWT 解出)。 */
if (res.data.userInfo) {
that.$store.commit('SETUID', res.data.userInfo.uid);
that.$store.commit('UPDATE_USERINFO', res.data.userInfo);
}
console.log('[auth-flow] ✅ silenceAuth 成功, token 已存 store (含 openid)');
} else if (res.data.openid) {
/* ⚠️⚠️ 2026-09-20 修:新用户 / 后台开启了「强制绑定手机号」时,
该接口返回的是 {auth_login:1, key, openid, unionid} —— **有 openid、没有 token**。
以前这段落到下面的 else,只打一条警告就把 openid 丢了;于是
store.openid / 缓存 OPENID / JWT 三处全空,首页壳 _hostOpenid() 拿不到身份
→ **匿名加载 mama-box**(H5 进访客态:孕周 `--`、档案空、跳应用跳裸 URL)。
这里必须把它落 store + 缓存。 */
that.$store.commit('SET_OPENID', res.data.openid);
try { uni.setStorageSync('OPENID', res.data.openid); } catch (e) {}
if (res.data.unionid) {
that.$store.commit('SET_UNIONID', res.data.unionid);
try { uni.setStorageSync('UNIONID', res.data.unionid); } catch (e) {}
}
console.log('[auth-flow] ✅ 无 token 但拿到 openid,已缓存(首页壳据此注入身份): ' + String(res.data.openid).substring(0, 8) + '...');
} else {
console.warn('[auth-flow] ⚠️ silenceAuth 返回无 token:', JSON.stringify(res.data || res).substring(0, 200));
console.warn('[auth-flow] ⚠️ silenceAuth 返回无 token 也无 openid:', JSON.stringify(res.data || res).substring(0, 200));
}
}).catch(err => {
console.warn('[auth-flow] ❌ silenceAuth 失败:', JSON.stringify(err).substring(0, 300));
@@ -253,6 +272,17 @@
title: err
})
});
/* ⚠️ 2026-09-20:把这次静默登录的 promise 挂到 globalData,
让宿主页(首页壳 / 容器页)在缓存未命中时**先 await 它**、而不是自己再换一次 code
(见 mixins/webviewHost.js 的 _awaitHostLogin)。
动机:微信 code 一次性,两边各换一次要多花 200–300ms(实测 code2session 就这么贵),
而 openid 本来就该由这一条链路落缓存(本文件上面的 SET_OPENID 分支 / LOGIN 从 JWT 提取)。
settle 后清掉,避免下次误等一个已经结束的 promise。 */
try { that.globalData._silenceAuthPromise = authPromise; } catch (e) {}
authPromise.then(() => {
try { if (that.globalData._silenceAuthPromise === authPromise) that.globalData._silenceAuthPromise = null; } catch (e) {}
});
return authPromise;
},
/**
* 检测当前的小程序