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:
@@ -132,7 +132,78 @@ export default {
|
||||
{auth_login:1, openid, unionid}(新用户,无 token)或 {token, ...}(老用户)
|
||||
两端都算成功,只要拿到 openid 就落到 store + 缓存。
|
||||
返回 Promise<{openid, unionid}>;拿不到就 resolve 空串,由调用方决定是否匿名加载。 */
|
||||
/* ============================================================
|
||||
App(Android 一期)身份链路 —— 2026-10-26
|
||||
------------------------------------------------------------
|
||||
App 没有 wx.login / openid;本地身份来源是「短信登录后的 uid」:
|
||||
① 有 token → GET /api/wechat/app_identity(routine openid 优先)
|
||||
② 没有 openid(全新用户)→ uni.login({provider:'weixin'}) 拿 code
|
||||
→ POST /api/wechat/app_bind 绑定到当前 uid → 再取 app_identity
|
||||
③ 没装微信/拒绝授权 → 返回空,页面给「登录后继续 / 重试」
|
||||
⚠️ 不在启动时弹微信:只有走到这里(且确实没有 openid)才授权。
|
||||
============================================================ */
|
||||
_appIdentityFlow() {
|
||||
const needLogin = { openid: '', unionid: '', needLogin: true };
|
||||
const empty = { openid: '', unionid: '', needLogin: false };
|
||||
let hasToken = false;
|
||||
try { hasToken = !!(store.state.app.token || uni.getStorageSync(LOGIN_STATUS)); } catch (e) { hasToken = !!store.state.app.token; }
|
||||
if (!hasToken) return Promise.resolve(needLogin);
|
||||
|
||||
const save = (openid, unionid) => {
|
||||
if (!openid) return 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) {}
|
||||
}
|
||||
return { openid: openid, unionid: unionid || '', needLogin: false };
|
||||
};
|
||||
const read = (res) => {
|
||||
const d = (res && res.data) || {};
|
||||
return { openid: d.openid || '', unionid: d.unionid || '', source: d.source || '' };
|
||||
};
|
||||
return import('@/api/user.js').then((api) => {
|
||||
return api.appIdentity().then((res) => {
|
||||
const id = read(res);
|
||||
console.log('[auth-flow] App app_identity: openid=' + (id.openid ? '有' : '无') + ' source=' + (id.source || '?'));
|
||||
if (id.openid) return save(id.openid, id.unionid);
|
||||
return this._weixinLoginCode().then((code) => {
|
||||
if (!code) {
|
||||
console.warn('[auth-flow] App:未拿到微信 code(未安装微信/拒绝授权)');
|
||||
return empty;
|
||||
}
|
||||
return api.appBind({ code: code }).then(() => {
|
||||
return api.appIdentity().then((r2) => {
|
||||
const id2 = read(r2);
|
||||
console.log('[auth-flow] App app_bind 后身份: openid=' + (id2.openid ? '有' : '无'));
|
||||
return save(id2.openid, id2.unionid);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}).catch((err) => {
|
||||
console.warn('[auth-flow] App 身份解析失败:', err && (err.message || err));
|
||||
return empty;
|
||||
});
|
||||
},
|
||||
_weixinLoginCode() {
|
||||
return new Promise((resolve) => {
|
||||
// #ifdef APP-PLUS
|
||||
uni.login({
|
||||
provider: 'weixin',
|
||||
success: (r) => resolve((r && r.code) || ''),
|
||||
fail: (e) => { console.warn('[auth-flow] uni.login(weixin) 失败:', e); resolve(''); }
|
||||
});
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
resolve('');
|
||||
// #endif
|
||||
});
|
||||
},
|
||||
_silenceAuthOpenid() {
|
||||
// #ifdef APP-PLUS
|
||||
/* App:走短信登录 + app_identity / app_bind(不碰 wx.login) */
|
||||
return this._appIdentityFlow();
|
||||
// #endif
|
||||
const empty = { openid: '', unionid: '' };
|
||||
return new Promise((resolve) => {
|
||||
/* ⚠️ 保险丝:wx.login / 接口有可能长时间不回调(弱网、域名没配好)。
|
||||
@@ -253,11 +324,18 @@ export default {
|
||||
break;
|
||||
}
|
||||
case 'requestProfile':
|
||||
/* App:没有小程序授权页,也不该弹页内面板(web-view 是原生组件、会盖住)。
|
||||
直接用当前 openid 查共享资料,把 hash 写回 web-view 即可。 */
|
||||
// #ifdef APP-PLUS
|
||||
this._pushAppProfile(m.href);
|
||||
// #endif
|
||||
// #ifndef APP-PLUS
|
||||
this.showAuth = true;
|
||||
/* 面板回传基址:H5 在消息里带 href(发起面板时的当前地址)→ 提交后
|
||||
按它回传。消息送达=面板弹出,href 同一条消息到达,确定性强于
|
||||
赌 shareHref 的 postMessage 派发时机(宫格换应用后可能还是旧地址)。 */
|
||||
if (m.href) this._panelBaseUrl = m.href;
|
||||
// #endif
|
||||
break;
|
||||
case 'setTitle':
|
||||
if (m.title) {
|
||||
@@ -312,6 +390,42 @@ export default {
|
||||
'&avatar=' + encodeURIComponent(r.avatar || '');
|
||||
},
|
||||
|
||||
/* App:H5 发 requestProfile 时,直接用当前 openid 查共享资料并回传 hash。
|
||||
—— 不弹面板、不调 getPhoneNumber(短信登录已验证手机号);
|
||||
—— 也绕开「App 的 web-view 是原生组件、页内面板会被盖住」的问题。 */
|
||||
_pushAppProfile(href) {
|
||||
const openid = this._hostOpenid();
|
||||
if (!openid) return uni.showToast({ title: '未获得 openid,请先登录', icon: 'none' });
|
||||
const current = (typeof this._hostCurrentUrl === 'function') ? this._hostCurrentUrl() : '';
|
||||
const base = String(href || this.shareHref || current || '').split('#')[0];
|
||||
if (!base) return;
|
||||
uni.showLoading({ title: '获取资料中', mask: true });
|
||||
uni.request({
|
||||
url: SHARED_AUTH_API + '?openid=' + encodeURIComponent(openid),
|
||||
method: 'GET',
|
||||
timeout: 10000,
|
||||
success: (r) => {
|
||||
uni.hideLoading();
|
||||
const d = r.data || {};
|
||||
if (d.code === 200) {
|
||||
const res = d.result || {};
|
||||
this._hostSetWebviewUrl(this.hostAuthUrl(base, {
|
||||
openid: openid,
|
||||
phone: res.phone || '',
|
||||
nickname: res.userName || '',
|
||||
avatar: res.avatar || ''
|
||||
}));
|
||||
} else {
|
||||
uni.showToast({ title: d.message || '资料获取失败', icon: 'none' });
|
||||
}
|
||||
},
|
||||
fail: () => {
|
||||
uni.hideLoading();
|
||||
uni.showToast({ title: '资料请求失败,请检查网络', icon: 'none' });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取独立授权页(pages/annex/auth/index)写下的 __AUTH_RESULT__。
|
||||
* 该页成功后 navigateBack 回到宿主页 —— 两个宿主页都必须在 onShow 调用本方法,
|
||||
|
||||
Reference in New Issue
Block a user