/** * 登录认证工具类 * 实现30天免登录功能(实际由后端Token有效期控制,当前为7天) * * 功能: * - 本地登录状态检查 * - 服务端Token验证 * - 微信手机号快速登录 * - 登录状态持久化 */ const config = require('../config/index') const api = require('./api') // 存储键名 const TOKEN_KEY = config.STORAGE_KEYS.TOKEN const USER_KEY = config.STORAGE_KEYS.USER_INFO const USER_ID_KEY = config.STORAGE_KEYS.USER_ID const TOKEN_EXPIRY_KEY = config.STORAGE_KEYS.TOKEN_EXPIRY /** * 检查是否已登录(本地验证) * 仅检查本地存储,不发起网络请求 * @returns {boolean} 是否已登录 */ function isLoggedIn() { const token = wx.getStorageSync(TOKEN_KEY) const expiry = wx.getStorageSync(TOKEN_EXPIRY_KEY) if (!token) return false // 如果有过期时间,检查是否过期 if (expiry) { return new Date(expiry) > new Date() } // 没有过期时间但有token,认为已登录(兼容旧数据) return true } /** * 获取本地保存的用户信息 * @returns {object|null} 用户信息 */ function getLocalUserInfo() { return wx.getStorageSync(USER_KEY) || null } /** * 获取登录Token * @returns {string|null} Token */ function getToken() { return wx.getStorageSync(TOKEN_KEY) || null } /** * 获取用户ID * @returns {string|null} 用户ID */ function getUserId() { return wx.getStorageSync(USER_ID_KEY) || null } /** * 验证登录状态(服务端验证) * 调用后端API验证Token是否有效 * @returns {Promise<{valid: boolean, user?: object, expired?: boolean, error?: any}>} */ async function verifyLogin() { const token = getToken() if (!token) { return { valid: false } } try { const res = await api.auth.getCurrentUser() if (res.success && res.data) { // 更新本地用户信息 saveUserInfo(res.data, token) return { valid: true, user: res.data } } else { // Token无效,清除本地数据 clearLoginInfo() return { valid: false, expired: true } } } catch (error) { console.error('验证登录失败:', error) // 401错误表示Token过期 if (error.code === 401) { clearLoginInfo() return { valid: false, expired: true } } // 网络错误等情况,保持本地状态 return { valid: false, error } } } /** * 微信手机号快速登录 * @param {string} code - 微信getPhoneNumber返回的code * @returns {Promise<{success: boolean, user?: object, error?: string}>} */ async function wxPhoneLogin(code, loginCode) { try { const res = await api.auth.wxPhoneLogin(code, loginCode) if (res.success && res.data) { const { token, user } = res.data // 计算过期时间(7天后,与后端保持一致) const expiresAt = new Date() expiresAt.setDate(expiresAt.getDate() + 7) // 保存登录信息 saveUserInfo(user, token, expiresAt.toISOString()) return { success: true, user } } return { success: false, error: res.error || '登录失败' } } catch (error) { console.error('微信手机号登录失败:', error) return { success: false, error: error.message || '网络错误' } } } /** * 保存用户信息到本地 * @param {object} user - 用户信息 * @param {string} token - 登录Token * @param {string} expiresAt - 过期时间(可选) */ function saveUserInfo(user, token, expiresAt = null) { if (token) { wx.setStorageSync(TOKEN_KEY, token) } if (user) { wx.setStorageSync(USER_KEY, user) if (user.id) { wx.setStorageSync(USER_ID_KEY, user.id) } } if (expiresAt) { wx.setStorageSync(TOKEN_EXPIRY_KEY, expiresAt) } // 登录成功后,检查并绑定推荐码 if (user && user.id && token) { checkAndBindReferralCode(user.id, token) } } /** * 检查并绑定推荐码(佣金系统) * 在用户登录成功后自动调用 * 包含重试机制,确保网络波动时也能成功绑定 * @param {string} userId - 用户ID * @param {string} token - 登录token * @param {number} retryCount - 重试次数(内部使用) */ async function checkAndBindReferralCode(userId, token, retryCount = 0) { const MAX_RETRY = 3 const REFERRAL_CODE_KEY = 'pendingReferralCode' try { const referralCode = wx.getStorageSync(REFERRAL_CODE_KEY) if (!referralCode) { console.log('[推荐码绑定] 没有待绑定的推荐码') return } console.log(`[推荐码绑定] 检测到待绑定推荐码: ${referralCode}, 重试次数: ${retryCount}`) const res = await api.commission.bindReferral(referralCode) if (res.success) { wx.removeStorageSync(REFERRAL_CODE_KEY) const app = getApp() if (app && app.globalData) { app.globalData.pendingReferralCode = null } console.log('[推荐码绑定] 推荐关系绑定成功') } else { console.log('[推荐码绑定] 推荐关系绑定失败:', res.error) if (res.error && ( res.error.includes('已绑定') || res.error.includes('自己') || res.error.includes('无效') )) { wx.removeStorageSync(REFERRAL_CODE_KEY) const app = getApp() if (app && app.globalData) { app.globalData.pendingReferralCode = null } console.log('[推荐码绑定] 已清除无效的推荐码') } else if (retryCount < MAX_RETRY) { console.log(`[推荐码绑定] 将在5秒后进行第${retryCount + 1}次重试...`) await new Promise(resolve => setTimeout(resolve, 5000)) await checkAndBindReferralCode(userId, token, retryCount + 1) } else { console.log('[推荐码绑定] 重试次数已用尽,绑定失败') } } } catch (error) { console.error('[推荐码绑定] 绑定推荐码失败:', error) if (retryCount < MAX_RETRY) { console.log(`[推荐码绑定] 网络错误,5秒后进行第${retryCount + 1}次重试...`) await new Promise(resolve => setTimeout(resolve, 5000)) await checkAndBindReferralCode(userId, token, retryCount + 1) } else { console.log('[推荐码绑定] 重试次数已用尽,网络错误') } } } /** * 清除登录信息 */ function clearLoginInfo() { wx.removeStorageSync(TOKEN_KEY) wx.removeStorageSync(USER_KEY) wx.removeStorageSync(USER_ID_KEY) wx.removeStorageSync(TOKEN_EXPIRY_KEY) wx.removeStorageSync(config.STORAGE_KEYS.REFRESH_TOKEN) } /** * 退出登录 * @returns {Promise} */ async function logout() { const token = getToken() if (token) { try { await api.auth.logout() } catch (error) { console.error('退出登录API调用失败:', error) } } clearLoginInfo() } /** * 检查并恢复登录状态 * 用于小程序启动时调用 * @returns {Promise<{isLoggedIn: boolean, user?: object}>} */ async function checkAndRestoreLogin() { // 1. 先检查本地状态 if (!isLoggedIn()) { return { isLoggedIn: false } } // 2. 验证服务端状态 const result = await verifyLogin() if (result.valid) { return { isLoggedIn: true, user: result.user } } // 3. 验证失败,清除本地状态 if (result.expired) { console.log('Token已过期,需要重新登录') } return { isLoggedIn: false } } /** * 需要登录时的处理 * 跳转到登录页面 * @param {string} redirectUrl - 登录成功后跳转的URL(可选) */ function requireLogin(redirectUrl = '') { const url = redirectUrl ? `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` : '/pages/login/login' wx.navigateTo({ url, fail: () => { // 如果navigateTo失败,尝试redirectTo wx.redirectTo({ url }) } }) } /** * 显示登录提示弹窗 * @param {string} redirectUrl - 登录成功后跳转的URL(可选) * @returns {Promise} 用户是否选择去登录 */ function showLoginTip(redirectUrl = '') { return new Promise((resolve) => { wx.showModal({ title: '提示', content: '请先登录后再操作', confirmText: '去登录', confirmColor: '#b06ab3', success: (res) => { if (res.confirm) { requireLogin(redirectUrl) resolve(true) } else { resolve(false) } } }) }) } /** * 页面级登录验证(统一验证机制) * 用于页面onLoad时调用,确保用户已登录且token有效 * * @param {object} options - 配置选项 * @param {string} options.pageName - 页面名称(用于日志) * @param {string} options.redirectUrl - 未登录时的跳转URL * @param {boolean} options.silent - 是否静默失败(不显示提示) * @returns {Promise} 是否已登录且token有效 * * @example * // 在页面onLoad中使用 * async onLoad() { * const isValid = await auth.ensureLogin({ * pageName: 'gift-shop', * redirectUrl: '/pages/gift-shop/gift-shop' * }) * * if (!isValid) return * * // 继续加载页面数据 * this.loadData() * } */ async function ensureLogin(options = {}) { const { pageName = 'unknown', redirectUrl = '', silent = false } = options const config = require('../config/index') const app = getApp() console.log(`[auth.ensureLogin] ${pageName} - 开始验证登录状态`) // 1. 等待app初始化完成 if (app && app.waitForLoginCheck) { await app.waitForLoginCheck() console.log(`[auth.ensureLogin] ${pageName} - app登录检查完成`) } // 2. 检查全局登录状态 if (!app || !app.globalData || !app.globalData.isLoggedIn) { console.warn(`[auth.ensureLogin] ${pageName} - 全局状态显示未登录`) if (!silent) { wx.redirectTo({ url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` }) } return false } // 3. 检查token是否存在(最多重试3次) let token = null let retryCount = 0 const maxRetries = 3 while (!token && retryCount < maxRetries) { token = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) if (!token) { retryCount++ console.warn(`[auth.ensureLogin] ${pageName} - Token不存在,第${retryCount}次重试`) if (retryCount < maxRetries) { // 等待递增的时间:100ms, 200ms, 300ms await new Promise(resolve => setTimeout(resolve, retryCount * 100)) } } } if (!token) { console.error(`[auth.ensureLogin] ${pageName} - Token在${maxRetries}次重试后仍然不存在`) if (!silent) { wx.redirectTo({ url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` }) } return false } // 4. 检查token是否过期 const expiry = wx.getStorageSync(config.STORAGE_KEYS.TOKEN_EXPIRY) if (expiry && new Date(expiry) <= new Date()) { console.warn(`[auth.ensureLogin] ${pageName} - Token已过期`) clearLoginInfo() if (app && app.globalData) { app.globalData.isLoggedIn = false } if (!silent) { wx.redirectTo({ url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` }) } return false } // 5. 最终验证:再次确认token可以被获取(防止storage异步问题) await new Promise(resolve => setTimeout(resolve, 50)) const finalToken = wx.getStorageSync(config.STORAGE_KEYS.TOKEN) if (!finalToken) { console.error(`[auth.ensureLogin] ${pageName} - 最终验证失败,token无法获取`) if (!silent) { wx.redirectTo({ url: `/pages/login/login?redirect=${encodeURIComponent(redirectUrl)}` }) } return false } console.log(`[auth.ensureLogin] ${pageName} - 验证通过,token有效 (长度: ${finalToken.length})`) return true } module.exports = { // 状态检查 isLoggedIn, getLocalUserInfo, getToken, getUserId, // 登录验证 verifyLogin, checkAndRestoreLogin, ensureLogin, // 新增:统一的页面级登录验证 // 登录操作 wxPhoneLogin, logout, // 数据管理 saveUserInfo, clearLoginInfo, // 推荐码绑定 checkAndBindReferralCode, // 辅助方法 requireLogin, showLoginTip }