465 lines
12 KiB
JavaScript
465 lines
12 KiB
JavaScript
/**
|
||
* 登录认证工具类
|
||
* 实现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<void>}
|
||
*/
|
||
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<boolean>} 用户是否选择去登录
|
||
*/
|
||
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<boolean>} 是否已登录且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
|
||
}
|