user.js 7.13 KB
/**
 * 用户状态管理
 *
 * @description 管理用户登录状态、用户信息等
 * @module stores/user
 */

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import Taro from '@tarojs/taro'
import { loginStatusAPI, loginAPI, getProfileAPI, logoutAPI } from '@/api/user'
import { ensureOpenidAuthorized } from '@/utils/openid'
import { isFeatureEnabled, getFeatureConfig } from '@/config/features'

export const useUserStore = defineStore('user', () => {
  // ========== 状态 ==========
  /** 用户信息 */
  const userInfo = ref(null)

  /** 未读消息数 */
  const unreadMsgCount = ref(0)

  /** 是否已授权(openid) */
  const isOpenid = ref(false)

  /** 是否已登录 */
  const isLoggedIn = ref(false)

  /** 加载状态 */
  const loading = ref(false)

  /** 登录态是否已经完成过首次检查 */
  const loginStatusChecked = ref(false)

  /** 上次获取用户信息的时间戳(用于防抖) */
  let lastFetchTime = 0

  /** 进行中的登录态检查 Promise,用于并发复用 */
  let loginStatusPromise = null

  /** 防抖时间间隔(毫秒) */
  const FETCH_DEBOUNCE_TIME = 5000

  // ========== 方法 ==========

  /**
   * 检查登录状态
   * @description 小程序启动时检查 openid 和登录状态
   * - 只触发微信授权,不跳转登录页
   * - 401 由接口拦截器统一处理
   * @throws {Error} 检查失败时抛出错误
   *
   * @example
   * await userStore.checkLoginStatus()
   */
  async function checkLoginStatus() {
    if (loginStatusPromise) {
      return loginStatusPromise
    }

    loginStatusPromise = (async () => {
      loading.value = true

      try {
        // 1. 确保 openid 已授权并尝试自动登录
        const user = await ensureOpenidAuthorized()

        if (user) {
          // miniProgramAuthAPI 返回了用户信息,说明已自动登录
          userInfo.value = user
          isOpenid.value = true
          isLoggedIn.value = true
          return
        }

        // 2. 查询登录状态
        const res = await loginStatusAPI()

        if (res.code === 1) {
          isOpenid.value = res.data.is_openid
          isLoggedIn.value = res.data.is_login

          // 3. 如果已登录,获取用户信息
          if (isLoggedIn.value) {
            await fetchUserInfo()
          }
          // 注意:这里不跳转登录页,让用户可以浏览小程序
          // 当用户操作触发接口返回 401 时,会自动跳转登录页
        } else {
          throw new Error(res.msg || '查询登录状态失败')
        }
      } catch (err) {
        console.error('检查登录状态失败:', err)
        throw err
      } finally {
        loading.value = false
        loginStatusChecked.value = true
        loginStatusPromise = null
      }
    })()

    return loginStatusPromise
  }

  /**
   * 等待登录态初始化完成
   * @description 冷启动时复用 App.onLaunch 的登录态检查,避免页面抢跑请求
   * @returns {Promise<void>}
   */
  async function waitForLoginStatusReady() {
    if (loginStatusPromise) {
      await loginStatusPromise
      return
    }

    if (!loginStatusChecked.value) {
      await checkLoginStatus()
    }
  }

  /**
   * 获取用户信息
   * @description 调用 getProfileAPI 获取用户信息,带防抖机制(5秒内不重复请求)
   * @param {boolean} force - 是否强制刷新(忽略防抖)
   * @throws {Error} 获取失败时抛出错误
   *
   * @example
   * await userStore.fetchUserInfo()
   * await userStore.fetchUserInfo(true) // 强制刷新
   */
  async function fetchUserInfo(force = false) {
    // 防抖检查:如果不是强制刷新,且距离上次请求不足 5 秒,则跳过
    if (!force) {
      const now = Date.now()
      if (now - lastFetchTime < FETCH_DEBOUNCE_TIME) {
        console.log('[UserStore] 跳过频繁的用户信息请求')
        return
      }
    }

    try {
      loading.value = true
      const res = await getProfileAPI()

      if (res.code === 1) {
        userInfo.value = res.data.user

        // 从 res.data 中读取 unread_msg_count(不在 user 对象里)
        unreadMsgCount.value = res.data?.unread_msg_count ?? 0

        // 更新最后请求时间
        lastFetchTime = Date.now()
      } else {
        throw new Error(res.msg || '获取用户信息失败')
      }
    } catch (err) {
      console.error('获取用户信息失败:', err)
      throw err
    } finally {
      loading.value = false
    }
  }

  /**
   * 用户登录
   * @description 调用 loginAPI 进行账号密码登录
   * @param {Object} loginData 登录数据
   * @param {string} loginData.uuid 账号
   * @param {string} loginData.password 密码
   * @returns {{success: boolean, message?: string}} 登录结果
   *
   * @example
   * const result = await userStore.login({
   *   uuid: '13800138000',
   *   password: '123456'
   * })
   * if (result.success) {
   *   console.log('登录成功')
   * }
   */
  async function login(loginData) {
    loading.value = true

    try {
      const res = await loginAPI(loginData)

      if (res.code === 1) {
        // 登录成功,获取用户信息
        await fetchUserInfo()

        isLoggedIn.value = true

        return { success: true }
      } else {
        throw new Error(res.msg || '登录失败')
      }
    } catch (err) {
      console.error('登录失败:', err)
      return { success: false, message: err.message }
    } finally {
      loading.value = false
    }
  }

  /**
   * 用户登出
   * @description 调用 logoutAPI 并清除本地状态
   *
   * @example
   * await userStore.logout()
   */
  async function logout() {
    try {
      // 调用登出接口
      await logoutAPI()

      // 清除本地状态
      userInfo.value = null
      isOpenid.value = false
      isLoggedIn.value = false
      unreadMsgCount.value = 0  // 清除未读消息数,隐藏红点
    } catch (err) {
      console.error('登出失败:', err)
    }
  }

  /**
   * TabBar 红点状态
   *
   * @description 根据 userInfo 中的字段计算是否显示红点
   * - 只在功能开关启用时生效
   * - 只处理 'me' 按钮的红点
   * - 根据 unread_count 字段判断(可配置)
   *
   * @returns {string[]} 需要显示红点的 tab key 数组
   *
   * @example
   * // 返回 ['me'] 表示在 '我的' 按钮显示红点
   * // 返回 [] 表示不显示红点
   */
  const tabBarBadges = computed(() => {
    // 1. 检查功能开关
    if (!isFeatureEnabled('tabbarBadge')) {
      return []
    }

    // 2. 获取阈值
    const threshold = getFeatureConfig('tabbarBadgeThreshold') // 1

    // 3. 从 unreadMsgCount 读取值(而不是从 userInfo 中)
    const count = unreadMsgCount.value

    // 4. 判断是否显示红点
    const badges = []
    if (count >= threshold) {
      badges.push('me')
    }

    return badges
  })

  // ========== 返回 ==========
  return {
    // 状态
    userInfo,
    unreadMsgCount,
    isOpenid,
    isLoggedIn,
    loading,
    loginStatusChecked,

    // 计算属性
    tabBarBadges,

    // 方法
    checkLoginStatus,
    waitForLoginStatusReady,
    fetchUserInfo,
    login,
    logout
  }
})