feat(App): 宿主接入短信登录 + app_identity/app_bind

- mixin 新增 _appIdentityFlow:有 token → GET wechat/app_identity;
  无 openid → uni.login(weixin) → POST wechat/app_bind → 再取身份
- _silenceAuthOpenid 在 APP-PLUS 下改走该流程(不碰 wx.login)
- 首页壳/容器页:未登录时停止自动重试,兜底屏给「登录后继续」;
  登录页成功后 reLaunch 回原页(首页 / 容器页带原 url)
- App 的 requestProfile 改为直接查共享资料并回传 hash(不弹面板、
  不调 getPhoneNumber,规避 web-view 覆盖页内浮层)
- api/user.js 新增 appIdentity / appBind

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-26 20:04:03 +08:00
parent f3648d5cef
commit bd7f07d7d6
4 changed files with 194 additions and 10 deletions

View File

@@ -8,10 +8,11 @@
以前 url 为空时这里是白屏(没有任何占位),所以必须给一块可见的屏,
否则「不加载」会变成「白屏 + 用户不知道该干嘛」。 -->
<view v-else class="wv-fallback">
<view class="wv-emoji">{{ identityFail ? '🥲' : '🍼' }}</view>
<view class="wv-title">{{ identityFail ? '未获取到微信身份' : '正在获取微信身份' }}</view>
<view class="wv-desc">{{ identityFail ? '请检查网络后点下方重试,或退出小程序重新进入' : '马上就好,请稍候(不会加载空数据的页面)' }}</view>
<button v-if="identityFail" class="wv-retry" hover-class="wv-retry-hover" @tap="retryIdentity">重新获取身份</button>
<view class="wv-emoji">{{ appNeedLogin ? '🔐' : (identityFail ? '🥲' : '🍼') }}</view>
<view class="wv-title">{{ appNeedLogin ? '登录后继续' : (identityFail ? '未获取到微信身份' : '正在获取微信身份') }}</view>
<view class="wv-desc">{{ appNeedLogin ? '使用手机号验证码登录;登录后会获取微信身份并自动打开该应用' : (identityFail ? '请检查网络后点下方重试,或退出小程序重新进入' : '马上就好,请稍候(不会加载空数据的页面)') }}</view>
<button v-if="appNeedLogin" class="wv-retry" hover-class="wv-retry-hover" @tap="goAppLogin">登录后继续</button>
<button v-else-if="identityFail" class="wv-retry" hover-class="wv-retry-hover" @tap="retryIdentity">重新获取身份</button>
</view>
<!-- 共享原生授权面板(任意 H5 应用 postMessage({action:'requestProfile'}) 后显示) -->
@@ -156,6 +157,7 @@ export default {
savedUrl: '',
identityPending: false, // 正在获取 openid(此期间**不加载 H5**)
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
appNeedLogin: false, // App:未登录 → 兜底屏给「登录后继续」
_idAttempts: 0, // 自动重试计数
_pendingTarget: '' // 重试时要加载的目标 URL(重试按钮用)
}
@@ -378,9 +380,23 @@ export default {
console.log('[auth-flow] 🔑 容器页取 openid(第 ' + this._idAttempts + '/' + MAX + ' 次),此期间不加载 H5');
/* 2) 第 1 次先等 App.vue onLaunch 那次静默登录(同一次 exchange,别重复换 code);
之后的重试才自己 wx.login + 换 openid。理由与首页壳一致,见 mixins 的 _awaitHostLogin。 */
const task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
task.then(() => {
let task;
// #ifdef APP-PLUS
task = this._silenceAuthOpenid();
// #endif
// #ifndef APP-PLUS
task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
// #endif
task.then((r) => {
if (this._loadWithCachedIdentity(t)) return;
/* App 未登录:停止自动重试,兜底屏给「登录后继续」 */
if (r && r.needLogin) {
this.identityPending = false;
this.identityFail = false;
this.appNeedLogin = true;
console.log('[auth-flow] App 未登录(容器页)→ 等待用户登录');
return;
}
if (this._idAttempts < MAX) {
setTimeout(() => this._ensureIdentityThenLoad(t), 500); // 退避固定 500ms(原 600×n)
return;
@@ -396,6 +412,7 @@ export default {
if (!cached.openid) return false;
this.identityPending = false;
this.identityFail = false;
this.appNeedLogin = false;
this._idAttempts = 0;
this._applyIdentity(t, cached.openid, cached.unionid, getUrlParam(t, 'openid'));
return true;
@@ -404,9 +421,20 @@ export default {
retryIdentity() {
this._idAttempts = 0;
this.identityFail = false;
this.appNeedLogin = false;
this._ensureIdentityThenLoad(this._pendingTarget || this.savedUrl || this.url);
},
/* App:去登录;登录成功 reLaunch 回容器页(带原目标 URL) */
goAppLogin() {
const back = this._pendingTarget || this.savedUrl || this.url || '';
try {
uni.setStorageSync('login_back_url',
back ? '/pages/annex/web_view/index?url=' + encodeURIComponent(back) : '/pages/index/index');
} catch (e) {}
uni.navigateTo({ url: '/pages/users/login/index' });
},
_applyIdentity(targetUrl, openid, unionid, urlOpenid) {
console.log('[auth-flow] _applyIdentity: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)') + ', urlOpenid=' + (urlOpenid || '(无)'));
if (urlOpenid && urlOpenid !== openid) {

View File

@@ -32,7 +32,8 @@
<view class="mamabox-title">{{ fallbackTitle }}</view>
<view class="mamabox-desc">{{ fallbackDesc }}</view>
</view>
<button v-if="loadError || identityFail" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">{{ loadError ? '重新加载' : '重新获取身份' }}</button>
<button v-if="appNeedLogin" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="goAppLogin">登录后继续</button>
<button v-else-if="loadError || identityFail" class="mamabox-retry" hover-class="mamabox-retry-hover" @tap="retryLoad">{{ loadError ? '重新加载' : '重新获取身份' }}</button>
</view>
<!-- 小程序原生授权面板(H5 postMessage 请求后显示):手机号/昵称/头像 -->
@@ -122,19 +123,21 @@
别让用户对着「正在加载」干等却不知道在等什么。 */
fallbackTitle() {
if (this.loadError) return '页面加载失败';
if (this.appNeedLogin) return '登录后继续';
if (this.identityFail) return '未获取到微信身份';
if (this.identityPending) return '正在获取微信身份';
return '正在加载';
},
fallbackDesc() {
if (this.loadError) return '请检查网络后重试';
if (this.appNeedLogin) return '使用手机号验证码登录;登录后会获取微信身份并自动进入首页';
if (this.identityFail) return '请检查网络后点下方重试,或退出小程序重新进入';
if (this.identityPending) return '马上就好,请稍候(不会加载空数据的页面)';
if (this.restoring) return '正在回到你上次使用的应用';
return '正在为你打开「兜爸心选」';
},
fallbackEmoji() {
return (this.loadError || this.identityFail) ? '🥲' : '🍼';
return this.appNeedLogin ? '🔐' : ((this.loadError || this.identityFail) ? '🥲' : '🍼');
},
},
data() {
@@ -150,6 +153,7 @@
_noRestore: false, // 本次加载 mama-box 是否要带 #norestore=1(禁止 H5 兜底再把用户送回应用)
identityPending: false, // 正在获取 openid(此期间**不加载 H5**,见 fetchIdentityThenLoad)
identityFail: false, // 多次仍未拿到 openid → 停在兜底屏,给「重新获取身份」入口
appNeedLogin: false, // App:没登录(或登录后仍无身份)→ 兜底屏给「登录后继续」
shareInfo: {}, // CRMEB 后台「分享设置」(标题/图,无 App 匹配时兜底)
// 授权面板字段由 mixins/webviewHost.js 提供:
// showAuth / authNickname / authAvatar / _authCode
@@ -620,9 +624,25 @@
它先落地就直接加载 —— 避免两边各换一次 code 白花 200–300ms
(微信 code 一次性;实测 code2session 约 200–300ms)。
之后的重试才自己 wx.login + 换 openid(_silenceAuthOpenid)。 */
const task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
task.then(() => {
let task;
// #ifdef APP-PLUS
/* App:没有并行的 wx.login,直接走「短信登录态 + app_identity/app_bind」 */
task = this._silenceAuthOpenid();
// #endif
// #ifndef APP-PLUS
task = (seq === 0) ? this._awaitHostLogin(1500) : this._silenceAuthOpenid();
// #endif
task.then((r) => {
if (this._loadWithCachedIdentity(base)) return;
/* App 未登录:**停止自动重试**,兜底屏给「登录后继续」按钮 ——
没有 token 时重试多少次都拿不到身份,继续循环只是让用户干等。 */
if (r && r.needLogin) {
this.identityPending = false;
this.identityFail = false;
this.appNeedLogin = true;
console.log('[auth-flow] App 未登录 → 等待用户登录(不自动重试)');
return;
}
if (this._idAttempts < MAX) {
/* 退避固定 500ms(原来 600×n,最坏 3 次 + 1.8s 退避 ≈ 3.5s 才出失败屏;
现在 2 次 + 0.5s ≈ 1.5s 量级) */
@@ -639,6 +659,7 @@
if (!this._hostOpenid()) return false;
this.identityPending = false;
this.identityFail = false;
this.appNeedLogin = false;
this._idAttempts = 0;
this._shownOnce = true;
// 沿用本次的 _noRestore(见 loadMamaBox 注释):本轮加载是否需要禁止 mama-box 兜底恢复
@@ -697,6 +718,7 @@
this.requesting = false;
this._shownOnce = true;
this.identityFail = false;
this.appNeedLogin = false;
/* 有身份直接重载;没有就再走一遍「取到才加载」(重置尝试次数,自动重试 3 次)
⚠️ 沿用 this._noRestore:重试首页时同样不许首页 H5 兜底恢复(见 loadMamaBox)。 */
const homeUrl = this._homeBase();
@@ -709,6 +731,13 @@
this.fetchIdentityThenLoad(homeUrl);
},
/* App:去登录。登录页成功后会 reLaunch 回 login_back_url(这里显式设成本页),
回到首页再走一遍「短信登录态 → app_identity → app_bind」。 */
goAppLogin() {
try { uni.setStorageSync('login_back_url', '/pages/index/index'); } catch (e) {}
uni.navigateTo({ url: '/pages/users/login/index' });
},
/* 授权完善资料(postMessage → 原生面板 → 共享授权接口 → hash 回传)与
pages/annex/web_view 共用 mixins/webviewHost.js:
handleHostMessage / onChooseHostAvatar / onHostGetPhone /