ai-c/utils/auth.js
2026-02-02 18:21:32 +08:00

465 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 登录认证工具类
* 实现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
}