diff --git a/App.vue b/App.vue index 6c53ee3..e4677ab 100644 --- a/App.vue +++ b/App.vue @@ -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; }, /** * 检测当前的小程序 diff --git a/mixins/webviewHost.js b/mixins/webviewHost.js index cf37b78..7dfee90 100644 --- a/mixins/webviewHost.js +++ b/mixins/webviewHost.js @@ -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) || []; diff --git a/pages/annex/web_view/index.vue b/pages/annex/web_view/index.vue index c679bf5..2a32ea4 100644 --- a/pages/annex/web_view/index.vue +++ b/pages/annex/web_view/index.vue @@ -1,22 +1,33 @@