diff --git a/src/App.vue b/src/App.vue index 7b9e9f3..cbcb5a7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -46,8 +46,12 @@ const code = urlParams.get('code'); if (code) { - // 有 code → 进入授权 Loading 页处理,保留 code 参数 + // 有 code → 进入授权 Loading 页处理,保留 code/action/device_id 参数 + const action = urlParams.get('action') || ''; let loadingUrl = '/pages/auth/loading?code=' + encodeURIComponent(code); + if (action) { + loadingUrl += '&action=' + action; + } if (deviceId) { loadingUrl += '&device_id=' + deviceId; } diff --git a/src/pages/auth/loading.vue b/src/pages/auth/loading.vue index 7faf22d..5a53ef4 100644 --- a/src/pages/auth/loading.vue +++ b/src/pages/auth/loading.vue @@ -15,14 +15,16 @@ export default { return { statusText: '正在登录...', deviceId: '', + action: '', }; }, onLoad(options) { this.deviceId = options.device_id || ''; + this.action = options.action || ''; // #ifdef H5 // Mock 模式:跳过微信 OAuth,直接登录 - if (config.mockWechatLogin) { + if (config.mockWechatLogin && this.action !== 'userinfo') { this.mockLogin(); return; } @@ -33,14 +35,30 @@ export default { if (!code) { const urlParams = new URLSearchParams(window.location.search); code = urlParams.get('code') || ''; + // 同步获取 action(可能在 URL 参数中) + if (!this.action) { + this.action = urlParams.get('action') || ''; + } + // 同步获取 device_id + if (!this.deviceId) { + this.deviceId = urlParams.get('device_id') || ''; + } } - if (code) { - // 有 code → 调用后端登录 - this.handleCallback(code); + if (this.action === 'userinfo') { + // 获取用户信息流程 + if (code) { + this.handleUserInfoCallback(code); + } else { + this.startUserInfoAuth(); + } } else { - // 无 code → 发起 OAuth 授权 - this.startAuth(); + // 登录流程 + if (code) { + this.handleCallback(code); + } else { + this.startAuth(); + } } // #endif @@ -94,7 +112,7 @@ export default { }, /** - * 处理授权回调(从 URL 中获取 code 后换取 openid,再用 openid 登录) + * 处理授权回调(用 code 换 openid,再用 openid 登录) */ async handleCallback(code) { const appNo = uni.getStorageSync('app_no') || getAppNo(detectPlatform()); @@ -108,35 +126,56 @@ export default { try { this.statusText = '正在验证身份...'; - // 用 code 换取 openid(后端处理,前端只传 code) - const res = await gatewayPost('/api/v1/user/login', { + // 1. 用 code 换取 openid(snsapi_base 静默授权) + const callbackRes = await gatewayPost('/api/v1/user/auth/exchange', { app_no: appNo, code: code, }); - const data = res.data || {}; + const callbackData = callbackRes.data || {}; + console.log('[loading] auth callback response:', JSON.stringify(callbackData, null, 2)); + + if (callbackData.code !== 0 || !callbackData.data) { + uni.showToast({ title: callbackData.message || '授权失败', icon: 'none' }); + setTimeout(() => this.goToScan(), 1500); + return; + } + + const openid = callbackData.data.openid; + if (!openid) { + uni.showToast({ title: '获取用户标识失败', icon: 'none' }); + setTimeout(() => this.goToScan(), 1500); + return; + } + + // 清除 URL 中的 code 参数,避免刷新重复处理 + const url = new URL(window.location.href); + url.searchParams.delete('code'); + url.searchParams.delete('state'); + window.history.replaceState({}, '', url.toString()); + + // 存储 openid + uni.setStorageSync('openid', openid); + + // 2. 用 openid 调用登录接口 + this.statusText = '正在登录...'; + const loginRes = await gatewayPost('/api/v1/user/login', { + app_no: appNo, + openid: openid, + }); + + const data = loginRes.data || {}; console.log('[loading] login response:', JSON.stringify(data, null, 2)); if (data.code === 0 && data.data) { const result = data.data; - console.log('[loading] login result:', JSON.stringify(result, null, 2)); - - // 清除 URL 中的 code 参数,避免刷新重复处理 - const url = new URL(window.location.href); - url.searchParams.delete('code'); - url.searchParams.delete('state'); - window.history.replaceState({}, '', url.toString()); - - // 存储 openid - uni.setStorageSync('openid', result.openid); if (result.register) { - // 已注册 → 缓存 JWT → 跳转首页 + // 已注册 → 存 token → 进入开门页 uni.setStorageSync('token', result.token); - // 不再存储 user_info,点击"我的"时再获取 this.goToHome(); } else { - // 未注册 → 缓存临时数据 → 跳转注册页 + // 未注册 → 存 temp_token → 跳注册页 uni.setStorageSync('temp_token', result.temp_token); this.goToRegister(); } @@ -151,6 +190,114 @@ export default { } }, + /** + * 发起非静默授权获取用户信息(snsapi_userinfo,弹窗确认) + */ + async startUserInfoAuth() { + const platform = detectPlatform(); + const appNo = getAppNo(platform); + + if (!appNo) { + // 非微信环境,直接跳回首页 + this.goToHomeWithAction(); + return; + } + + try { + this.statusText = '正在获取用户信息...'; + + // 回调地址:当前 loading 页,带 action=userinfo 和 device_id + const baseUri = config.authRedirectUri || ( + window.location.origin.replace(/^http:/, 'https:') + window.location.pathname + ); + const redirectUriObj = new URL(baseUri); + redirectUriObj.searchParams.set('action', 'userinfo'); + if (this.deviceId) { + redirectUriObj.searchParams.set('device_id', this.deviceId); + } + const redirectUri = redirectUriObj.toString(); + + const res = await gatewayGet('/api/v1/user/auth/url', { + app_no: appNo, + scopes: 'snsapi_userinfo', + redirect_uri: redirectUri, + }); + + const data = res.data || {}; + if (data.code === 0 && data.data && data.data.auth_url) { + // 跳转微信授权页(弹窗确认) + window.location.href = data.data.auth_url; + } else { + uni.showToast({ title: data.message || '获取授权失败', icon: 'none' }); + setTimeout(() => this.goToHomeWithAction(), 1500); + } + } catch (e) { + console.error('[Auth] startUserInfoAuth error:', e); + uni.showToast({ title: '网络请求失败', icon: 'none' }); + setTimeout(() => this.goToHomeWithAction(), 1500); + } + }, + + /** + * 处理用户信息授权回调(用 code 调 /api/v1/user/info 获取头像昵称) + */ + async handleUserInfoCallback(code) { + const appNo = uni.getStorageSync('app_no') || getAppNo(detectPlatform()); + + if (!appNo) { + this.goToHomeWithAction(); + return; + } + + try { + this.statusText = '正在获取用户信息...'; + + // 清除 URL 中的 code/action 参数,避免刷新重复处理 + const url = new URL(window.location.href); + url.searchParams.delete('code'); + url.searchParams.delete('state'); + url.searchParams.delete('action'); + window.history.replaceState({}, '', url.toString()); + + // 用 code 调用 /api/v1/user/info 获取用户信息 + const res = await gatewayGet('/api/v1/user/info', { + app_no: appNo, + code: code, + }); + + const data = res.data || {}; + console.log('[loading] user info response:', JSON.stringify(data, null, 2)); + + if (data.code === 0 && data.data) { + const userInfo = data.data; + // 存入缓存 + uni.setStorageSync('user_info', { + nickname: userInfo.nickname || '', + avatar: userInfo.avatar || '', + phone: userInfo.phone || '', + }); + } else { + console.warn('[loading] 获取用户信息失败:', data.message); + } + + // 跳回首页(我的 tab) + this.goToHomeWithAction(); + } catch (e) { + console.error('[Auth] handleUserInfoCallback error:', e); + this.goToHomeWithAction(); + } + }, + + /** + * 跳回首页并标记为"我的"tab + */ + goToHomeWithAction() { + const url = this.deviceId + ? `/pages/index/index?device_id=${this.deviceId}&action=mine` + : '/pages/index/index?action=mine'; + uni.redirectTo({ url }); + }, + goToHome() { const url = this.deviceId ? `/pages/index/index?device_id=${this.deviceId}` diff --git a/src/pages/index/index.vue b/src/pages/index/index.vue index 3b98eb0..6e3c3da 100644 --- a/src/pages/index/index.vue +++ b/src/pages/index/index.vue @@ -164,7 +164,7 @@ import PageNavBar from '@/components/page-nav-bar/page-nav-bar.vue'; import config from '@/config/env.js'; import { get, gatewayGet } from '@/utils/request.js'; -import { checkLoginWithPrompt, getCachedUserInfo } from '@/utils/auth-guard.js'; +import { checkLoginWithPrompt, getCachedUserInfo, isLoggedIn } from '@/utils/auth-guard.js'; import { isVConsoleEnabled, showVConsole, hideVConsole } from '@/utils/vconsole'; export default { @@ -226,22 +226,24 @@ export default { this.isWechat = /MicroMessenger/i.test(navigator.userAgent); // #endif + // 处理"我的"用户信息授权回调(从 loading 页跳回) + if (options.action === 'mine') { + this.currentTab = 'mine'; + this.fromPage = 'navigate'; + } + // 支持通过 tab 参数切换到"我的"页面 if (options.tab === 'mine') { this.currentTab = 'mine'; - // 记录来源:从其他页面跳转过来的 this.fromPage = 'navigate'; - // 获取用户信息 - this.fetchUserInfo(); } if (options.device_id) { this.deviceId = options.device_id; - // 动态设置导航栏标题 uni.setNavigationBarTitle({ title: `智能货柜-${this.deviceId}` }); this.fetchProducts(); - } else if (!options.tab) { - // 无设备编号且非 tab 跳转,提示并跳至扫码页 + } else if (!options.tab && options.action !== 'mine') { + // 无设备编号且非 tab/action 跳转,提示并跳至扫码页 uni.showToast({ title: '请扫描设备二维码', icon: 'none' }); setTimeout(() => { uni.redirectTo({ url: '/pages/index/scan' }); @@ -265,14 +267,20 @@ export default { }, methods: { switchTab(tab) { - // 切换到"我的"Tab 时检查登录 - if (tab === 'mine' && !checkLoginWithPrompt('mine')) return; - this.currentTab = tab; - // 记录来源:从 tab 切换的 if (tab === 'mine') { + // 切换到"我的"Tab 时检查登录 + if (!checkLoginWithPrompt('mine')) return; + this.currentTab = tab; this.fromPage = 'tab'; - // 获取用户信息 - this.fetchUserInfo(); + // 已有缓存的用户信息则直接显示,否则发起非静默授权获取 + const cached = getCachedUserInfo(); + if (cached && cached.nickname) { + this.userInfo = { ...this.userInfo, ...cached }; + } else { + this.startUserInfoAuth(); + } + } else { + this.currentTab = tab; } }, switchCategory(cat) { this.activeCategory = cat; }, @@ -292,24 +300,14 @@ export default { } console.log('[index] this.userInfo:', JSON.stringify(this.userInfo, null, 2)); }, - // 从接口获取用户信息(点击"我的"时调用) - async fetchUserInfo() { - const appNo = uni.getStorageSync('app_no') || config.mockAppNo; - - if (!appNo) { - console.warn('[index] 无法获取应用信息'); - return; - } - - // Mock 模式下,从缓存读取用户信息 + // 发起非静默授权获取用户信息(snsapi_userinfo) + async startUserInfoAuth() { + // Mock 模式下,直接使用 mock 数据 if (config.mockWechatLogin) { - console.log('[index] Mock 模式,从缓存读取用户信息'); const cached = getCachedUserInfo(); - if (cached) { - // 有缓存(注册后存储的),直接使用 + if (cached && cached.nickname) { this.userInfo = { ...this.userInfo, ...cached }; } else { - // 无缓存,使用 mock 数据(已注册用户) this.userInfo = { nickname: config.mockUser.user_info.nickname || '', phone: config.mockUser.user_info.phone || '', @@ -320,41 +318,38 @@ export default { return; } + const appNo = uni.getStorageSync('app_no'); + if (!appNo) { + console.warn('[index] 无法获取应用信息'); + return; + } + try { - // 调用接口获取用户信息 - const res = await gatewayGet('/api/v1/user/info', { + // 回调地址:loading 页处理后跳回首页(带 action 和 device_id) + const baseUri = config.authRedirectUri || window.location.origin + '/pages/auth/loading'; + const redirectUriObj = new URL(baseUri); + redirectUriObj.searchParams.set('action', 'userinfo'); + if (this.deviceId) { + redirectUriObj.searchParams.set('device_id', this.deviceId); + } + const redirectUri = redirectUriObj.toString(); + + // 获取非静默授权链接(会弹窗确认) + const res = await gatewayGet('/api/v1/user/auth/url', { app_no: appNo, + scopes: 'snsapi_userinfo', + redirect_uri: redirectUri, }); const data = res.data || {}; - console.log('[index] fetchUserInfo response:', JSON.stringify(data, null, 2)); - - if (data.code === 0 && data.data) { - const userInfo = data.data; - this.userInfo = { - nickname: userInfo.nickname || '', - phone: userInfo.phone || '', - avatar: userInfo.avatar || '', - }; - // 存入缓存 - uni.setStorageSync('user_info', this.userInfo); - console.log('[index] fetchUserInfo success:', JSON.stringify(this.userInfo, null, 2)); - } else if (data.code === 1010) { - // 用户未注册,需要重新走授权流程获取 temp_token - console.warn('[index] 用户未注册,重新授权'); - uni.removeStorageSync('token'); - uni.removeStorageSync('user_info'); - // 跳转到 loading 页重新授权(会获取 temp_token 后跳注册页) - const deviceId = this.deviceId || ''; - const loadingUrl = deviceId - ? `/pages/auth/loading?device_id=${deviceId}` - : '/pages/auth/loading'; - uni.redirectTo({ url: loadingUrl }); + if (data.code === 0 && data.data && data.data.auth_url) { + // 跳转微信授权页(弹窗确认) + window.location.href = data.data.auth_url; } else { - console.warn('[index] fetchUserInfo failed:', data.message); + console.warn('[index] 获取授权链接失败:', data.message); } } catch (e) { - console.error('[index] fetchUserInfo error:', e); + console.error('[index] startUserInfoAuth error:', e); } }, async fetchProducts() { diff --git a/src/utils/request.js b/src/utils/request.js index 0f342ae..9c1255b 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -40,10 +40,11 @@ export function request(options = {}) { // 公开接口(授权/登录/注册相关)不带 Authorization,避免 CORS 预检和 JWT 拦截 const publicPaths = [ '/api/v1/user/auth/url', - '/api/v1/user/auth/callback', + '/api/v1/user/auth/exchange', '/api/v1/user/login', '/api/v1/user/register', '/api/v1/user/sms/send', + '/api/v1/user/info', ]; const isPublic = publicPaths.some((p) => url.includes(p));