openid.js 5.84 KB
/**
 * 微信授权(openid)管理
 *
 * @description 处理小程序授权逻辑,包括 wx.login 和 miniProgramAuthAPI 调用
 * @module utils/openid
 */

import Taro from '@tarojs/taro'
import axios from '@/utils/request'
import { setSessionId } from '@/utils/request'
import { loginStatusAPI } from '@/api/user'

/**
 * 小程序授权
 * @description 调用 wx.login 获取 code,由后端授权获取 openid
 * @description 授权成功后会自动将 sessionid 写入本地存储
 * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
 *
 * @example
 * const user = await miniProgramAuth()
 * if (user) {
 *   console.log('已自动登录', user)
 * } else {
 *   console.log('需要手动登录')
 * }
 */
export async function miniProgramAuth() {
  try {
    // 1. 调用 wx.login 获取 code
    const { code } = await Taro.login()

    if (!code) {
      throw new Error('获取微信 code 失败')
    }

    // 2. 调用后端授权接口(直接使用 axios 以访问响应头)
    const response = await axios.post('/srv/?a=openid', { code })

    // 检查响应状态
    if (!response?.data || response.data.code !== 1) {
      throw new Error(response?.data?.msg || '小程序授权失败')
    }

    // 3. 从响应中提取 cookie 并写入本地存储
    const cookie = extractCookieFromResponse(response)
    if (cookie) {
      setSessionId(cookie)
      console.log('授权成功,sessionid 已写入本地存储')
    }

    // 4. 返回用户信息(如果已自动登录)
    return response.data.data?.user || null
  } catch (err) {
    console.error('小程序授权失败:', err)
    throw err
  }
}

/**
 * 从 axios 响应中提取 cookie
 * @description 尝试从多个可能的位置提取 cookie,并去重
 * @param {Object} response axios 响应对象
 * @returns {string|null} cookie 字符串或 null
 *
 * @example
 * const cookie = extractCookieFromResponse(response)
 * if (cookie) {
 *   setSessionId(cookie)
 * }
 */
function extractCookieFromResponse(response) {
  // 尝试从 response.headers 中提取
  if (response?.headers) {
    // 标准的 set-cookie 头
    if (response.headers['set-cookie']) {
      const cookies = response.headers['set-cookie']
      return normalizeCookies(cookies)
    }

    // 小写版本(某些环境)
    if (response.headers['Set-Cookie']) {
      const cookies = response.headers['Set-Cookie']
      return normalizeCookies(cookies)
    }

    // 小程序环境中可能在 cookies 字段
    if (response.headers.cookies) {
      const cookies = response.headers.cookies
      return normalizeCookies(cookies)
    }
  }

  // 尝试从 response.cookies 中提取(axios-miniprogram)
  if (response?.cookies) {
    const cookies = response.cookies
    // 如果是对象数组格式 [{name, value}, ...]
    if (Array.isArray(cookies) && cookies.length > 0 && cookies[0].name) {
      return cookies.map(c => `${c.name}=${c.value}`).join('; ')
    }
    // 如果是字符串数组
    return normalizeCookies(cookies)
  }

  console.warn('未能从响应中提取 cookie')
  return null
}

/**
 * 标准化 cookie 数组并提取会话 cookie
 * @description 将 cookie 数组转换为字符串,优先提取包含会话标识的 cookie
 * @param {string|string[]} cookies cookie 字符串或数组
 * @returns {string|null} 标准化后的 cookie 字符串
 *
 * @example
 * // 多个 Set-Cookie,优先返回包含 sessionid/PHPSESSID 的
 * normalizeCookies(['sessionid=xxx; path=/', 'csrftoken=yyy; path=/'])
 * // 返回: 'sessionid=xxx; path=/'
 *
 * @example
 * // 单个 cookie 字符串
 * normalizeCookies('PHPSESSID=xxx; path=/')
 * // 返回: 'PHPSESSID=xxx; path=/'
 */
function normalizeCookies(cookies) {
  if (!cookies) return null

  // 如果是单个 cookie 字符串,直接返回
  if (typeof cookies === 'string') {
    return cookies
  }

  // 如果是数组,智能提取会话 cookie
  if (Array.isArray(cookies)) {
    // 去重
    const uniqueCookies = [...new Set(cookies)]

    // 常见的会话 cookie 名称
    const sessionCookieNames = ['sessionid', 'PHPSESSID', 'jsessionid', 'JSESSIONID', 'sid', 'SID']

    // 优先查找包含会话标识的 cookie
    for (const cookie of uniqueCookies) {
      const cookieLower = cookie.toLowerCase()
      for (const name of sessionCookieNames) {
        if (cookieLower.includes(name.toLowerCase() + '=')) {
          console.log(`[openid] 找到会话 cookie: ${name}`)
          return cookie
        }
      }
    }

    // 如果没有找到会话 cookie,返回第一个(兼容旧逻辑)
    console.warn('[openid] 未找到会话 cookie,使用第一个 cookie')
    return uniqueCookies[0] || null
  }

  return null
}

/**
 * 检查 openid 状态
 * @description 调用 loginStatusAPI 检查 is_openid
 * @returns {Promise<boolean>} 是否已授权
 *
 * @example
 * const isOpenid = await checkOpenidStatus()
 * if (!isOpenid) {
 *   await miniProgramAuth()
 * }
 */
export async function checkOpenidStatus() {
  try {
    const res = await loginStatusAPI()

    if (res.code === 1) {
      return res.data.is_openid
    } else {
      return false
    }
  } catch (err) {
    console.error('检查 openid 状态失败:', err)
    return false
  }
}

/**
 * 确保 openid 已授权并尝试自动登录
 * @description 如果未授权,则调用 wx.login 授权
 * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
 *
 * @example
 * const user = await ensureOpenidAuthorized()
 * if (user) {
 *   console.log('已自动登录', user)
 * } else {
 *   console.log('已授权但未登录,需要检查登录状态')
 * }
 */
export async function ensureOpenidAuthorized() {
  const isOpenid = await checkOpenidStatus()

  if (!isOpenid) {
    // 未授权,调用 wx.login 授权
    return await miniProgramAuth()
  }

  // 已授权,返回 null(需要检查登录状态)
  return null
}