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

View File

@@ -99,6 +99,111 @@ export default {
return uid;
},
/* ---------- 复用宿主 App.vue 那次「静默登录」,不重复换 code(2026-09-20 新增) ----------
App.vue onLaunch 本来就会走一次 wx.login → v2/wechat/silence_auth,
并把 promise 挂在 globalData._silenceAuthPromise 上(见 App.vue silenceAuth())。
宿主页在缓存未命中时**先 await 它**,而不是自己再发一次:
· 微信的 code 是一次性的,两家各换一次不但多花时间(实测 code2session 约 200–300ms),
而且两边拿到的 openid 必须是同一个来源才可靠;
· 它已经结束/不存在(比如从商城宫格进来)时立即返回,不引入任何等待。
返回 Promise<{}>(只为「等它结束」,openid 由调用方重新读缓存拿)—— 这里**不缓存 openid**,
因为 App.vue 那条路自己会落 store + OPENID 缓存(两处口径见 App.vue 的注释)。 */
_awaitHostLogin(timeout) {
return new Promise((resolve) => {
let p = null;
try { const app = getApp(); p = app && app.globalData && app.globalData._silenceAuthPromise; } catch (e) {}
if (!p || typeof p.then !== 'function') return resolve({});
const timer = setTimeout(() => {
console.warn('[auth-flow] ⚠️ 等 App.vue 静默登录超时 ' + (timeout || 1500) + 'ms,改用自己取');
resolve({});
}, timeout || 1500);
p.then(() => { clearTimeout(timer); resolve({}); })
.catch(() => { clearTimeout(timer); resolve({}); });
});
},
/* ---------- 兜底:三处都读不到 openid 时,主动取一次(2026-09-20 新增) ----------
背景:宿主页的身份只来自 store.openid / 缓存 OPENID / JWT(见 _hostOpenid)。
三处全空时(新用户未走绑定手机号、存储被清、冷启动竞态),首页壳会**匿名加载**
mama-box —— H5 进访客态(孕周 `--`、档案空),此后 jumpApp 跳应用也不带身份。
容器页 (pages/annex/web_view) 早有同名兜底(原 _fetchOpenidAndLoad,现统一到本方法),首页壳没有;这里把兜底
收进 mixin,两页共用一份实现(web_view 那份暂未迁移,口径保持一致,改一处看另一处):
wx.login → CRMEB v2/wechat/silence_auth →
{auth_login:1, openid, unionid}(新用户,无 token)或 {token, ...}(老用户)
两端都算成功,只要拿到 openid 就落到 store + 缓存。
返回 Promise<{openid, unionid}>;拿不到就 resolve 空串,由调用方决定是否匿名加载。 */
_silenceAuthOpenid() {
const empty = { openid: '', unionid: '' };
return new Promise((resolve) => {
/* ⚠️ 保险丝:wx.login / 接口有可能长时间不回调(弱网、域名没配好)。
首页壳现在「拿不到身份就不加载」,没有这个超时会把用户永久停在
「正在获取微信身份」那一屏;超时后走失败分支(重试/给重试入口)。 */
let settled = false;
const settle = (v) => { if (!settled) { settled = true; resolve(v); } };
const timer = setTimeout(() => {
console.warn('[auth-flow] ⚠️ 兜底取 openid 超时 8s,按失败处理');
settle(empty);
}, 8000);
const done = (openid, unionid) => {
clearTimeout(timer);
if (!openid) return settle(empty);
try { store.commit('SET_OPENID', openid); uni.setStorageSync('OPENID', openid); } catch (e) {}
if (unionid) {
try { store.commit('SET_UNIONID', unionid); uni.setStorageSync('UNIONID', unionid); } catch (e) {}
}
settle({ openid, unionid: unionid || '' });
};
// #ifdef MP
uni.login({
provider: 'weixin',
success: (loginRes) => {
const code = (loginRes && loginRes.code) || '';
if (!code) { console.warn('[auth-flow] ⚠️ 兜底 wx.login 返回空 code'); return settle(empty); }
import('@/api/public.js').then(api => {
api.silenceAuth({ code }).then(res => {
const data = (res && res.data) || {};
let openid = data.openid || '';
let unionid = data.unionid || '';
if (!openid) {
const ui = data.userInfo || {};
openid = ui.openid || '';
unionid = unionid || ui.unionid || '';
}
if (!openid && data.token) {
try {
const parts = String(data.token).split('.');
if (parts.length === 3) {
let payload = parts[1].replace(/-/g, '+').replace(/_/g, '/');
while (payload.length % 4 !== 0) payload += '=';
const p = JSON.parse(base64Decode(payload));
openid = p.openid || '';
unionid = unionid || p.unionid || '';
}
} catch (e) { console.warn('[auth-flow] 兜底 JWT 解码 openid 失败:', e.message); }
}
console.log('[auth-flow] 兜底 silenceAuth: has openid=' + !!openid + ', auth_login=' + data.auth_login);
done(openid, unionid);
}).catch(err => {
console.warn('[auth-flow] ⚠️ 兜底 silenceAuth 网络失败:', err);
settle(empty);
});
}).catch(err => {
console.warn('[auth-flow] ⚠️ 兜底 import api 失败:', err);
settle(empty);
});
},
fail: (err) => {
console.warn('[auth-flow] ⚠️ 兜底 wx.login 失败:', err);
settle(empty);
}
});
// #endif
// #ifndef MP
settle(empty);
// #endif
});
},
/* ---------- H5 postMessage 统一处理(4 个 action) ---------- */
handleHostMessage(e) {
const msgs = (e.detail && e.detail.data) || [];