authRedirect.js 9.99 KB
import Taro from '@tarojs/taro'
import { routerStore } from '@/stores/router'
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'

/**
 * 授权与回跳相关工具
 * - 统一管理:保存来源页、静默授权、跳转授权页、授权后回跳
 * - 约定:sessionid 存在于本地缓存 key 为 sessionid
 * - 说明:refreshSession/silentAuth 使用单例 Promise,避免并发重复授权
 */

/**
 * 获取当前页完整路径(含 query)
 * @returns {string} 当前页路径,示例:pages/index/index?a=1
 */
export const getCurrentPageFullPath = () => {
    const pages = Taro.getCurrentPages()
    if (!pages || pages.length === 0) return ''

    const current_page = pages[pages.length - 1]
    const route = current_page.route
    const options = current_page.options || {}

    const query_params = Object.keys(options)
        .map((key) => `${key}=${encodeURIComponent(options[key])}`)
        .join('&')

    return query_params ? `${route}?${query_params}` : route
}

/**
 * 保存当前页路径(用于授权成功后回跳)
 * @param {string} custom_path 自定义路径,不传则取当前页
 * @returns {void}
 */
export const saveCurrentPagePath = (custom_path) => {
    const router = routerStore()
    const path = custom_path || getCurrentPageFullPath()
    router.add(path)
}

/**
 * 判断是否已授权
 * @returns {boolean} true=已存在 sessionid,false=需要授权
 */
export const hasAuth = () => {
    try {
        const sessionid = Taro.getStorageSync('sessionid')
        return !!sessionid && sessionid !== ''
    } catch (error) {
        console.error('检查授权状态失败:', error)
        return false
    }
}

let auth_promise = null

/**
 * 刷新会话:通过 Taro.login 获取 code,换取后端会话 cookie 并写入缓存
 * - 被 request.js 的 401 拦截器调用,用于自动“静默续期 + 原请求重放”
 * - 复用 auth_promise,防止多个接口同时 401 时并发触发多次登录
 * @param {object} options 可选项
 * @param {boolean} options.show_loading 是否展示 loading,默认 true
 * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 后端返回 + cookie
 */
export const refreshSession = async (options) => {
    const show_loading = options?.show_loading !== false

    // 已有授权进行中时,直接复用同一个 Promise
    if (auth_promise) return auth_promise

    auth_promise = (async () => {
        try {
            if (show_loading) {
                Taro.showLoading({
                    title: '加载中...',
                    mask: true,
                })
            }

            // 调用微信登录获取临时 code
            const login_result = await new Promise((resolve, reject) => {
                Taro.login({
                    success: resolve,
                    fail: reject,
                })
            })

            if (!login_result || !login_result.code) {
                throw new Error('获取微信登录code失败')
            }

            const request_data = {
                code: login_result.code,
            }

            // 开发环境可按需手动传 openid(仅用于本地联调)
            if (process.env.NODE_ENV === 'development') {
                // request_data.openid = 'waj';
                // request_data.openid = 'h-009';
                // request_data.openid = 'h-010';
                // request_data.openid = 'h-011';
                // request_data.openid = 'h-012';
                // request_data.openid = 'h-013';
                // request_data.openid = 'oWbdFvkD5VtloC50wSNR9IWiU2q8';
                // request_data.openid = 'oex8h5QZnZJto3ttvO6swSvylAQo';
            }

            if (process.env.NODE_ENV === 'production') {
                // request_data.openid = 'h-013';
            }

            // 换取后端会话(服务端通过 Set-Cookie 返回会话信息)
            const response = await Taro.request({
                url: `${BASE_URL}/srv/?a=openid_wxapp&f=${encodeURIComponent(REQUEST_DEFAULT_PARAMS.f)}&client_name=${encodeURIComponent(REQUEST_DEFAULT_PARAMS.client_name)}`,
                method: 'POST',
                data: request_data,
            })

            if (!response?.data || response.data.code !== 1) {
                throw new Error(response?.data?.msg || '授权失败')
            }

            // 兼容小程序环境下 cookie 的不同字段位置
            let cookie =
                (response.cookies && response.cookies[0]) ||
                response.header?.['Set-Cookie'] ||
                response.header?.['set-cookie']

            if (Array.isArray(cookie)) cookie = cookie[0]

            if (!cookie) {
                throw new Error('授权失败:没有获取到有效的会话信息')
            }

            // 写入本地缓存:后续请求会从缓存取 sessionid 并带到请求头
            Taro.setStorageSync('sessionid', cookie)

            return {
                ...response.data,
                cookie,
            }
        } finally {
            if (show_loading) {
                Taro.hideLoading()
            }
        }
    })().finally(() => {
        auth_promise = null
    })

    return auth_promise
}

const do_silent_auth = async (show_loading) => {
    // 已有 sessionid 时直接视为已授权
    if (hasAuth()) {
        return { code: 1, msg: '已授权' }
    }

    // 需要授权时,走刷新会话逻辑
    return await refreshSession({ show_loading })
}

/**
 * 静默授权:用于启动阶段/分享页/授权页发起授权
 * - 与 refreshSession 共用 auth_promise,避免并发重复调用
 * @param {Function} on_success 成功回调
 * @param {Function} on_error 失败回调(入参为错误文案)
 * @param {object} options 可选项
 * @param {boolean} options.show_loading 是否展示 loading,默认 true
 * @returns {Promise<any>} 授权结果
 */
export const silentAuth = async (on_success, on_error, options) => {
    const show_loading = options?.show_loading !== false

    try {
        // 未有授权进行中时才发起一次授权,并复用 Promise
        if (!auth_promise) {
            auth_promise = do_silent_auth(show_loading).finally(() => {
                auth_promise = null
            })
        }
        const result = await auth_promise
        if (on_success) on_success(result)
        return result
    } catch (error) {
        const error_msg = error?.message || '授权失败,请稍后重试'
        if (on_error) on_error(error_msg)
        throw error
    }
}

/**
 * 跳转到授权页(降级方案)
 * - 会先保存回跳路径(默认当前页),授权成功后在 auth 页回跳
 * @param {string} return_path 指定回跳路径
 * @returns {void}
 */
export const navigateToAuth = (return_path) => {
    if (return_path) {
        saveCurrentPagePath(return_path)
    } else {
        saveCurrentPagePath()
    }

    const pages = Taro.getCurrentPages()
    const current_page = pages[pages.length - 1]
    const current_route = current_page?.route
    if (current_route === 'pages/auth/index') {
        return
    }

    // TAG: navigateTo 失败时(例如页面栈满),降级为 redirectTo
    // Taro.navigateTo({ url: '/pages/auth/index' }).catch(() => {
    //     return Taro.redirectTo({ url: '/pages/auth/index' })
    // })
}

/**
 * 授权成功后回跳到来源页
 * - 优先使用 routerStore 里保存的路径
 * - 失败降级:redirectTo -> reLaunch
 * @param {string} default_path 未保存来源页时的默认回跳路径
 * @returns {Promise<void>}
 */
export const returnToOriginalPage = async (default_path = '/pages/index/index') => {
    const router = routerStore()
    const saved_path = router.url

    try {
        router.remove()

        const pages = Taro.getCurrentPages()
        const current_page = pages[pages.length - 1]
        const current_route = current_page?.route

        let target_path = default_path
        if (saved_path && saved_path !== '') {
            target_path = saved_path.startsWith('/') ? saved_path : `/${saved_path}`
        }

        const target_route = target_path.split('?')[0].replace(/^\//, '')

        if (current_route === target_route) {
            return
        }

        try {
            await Taro.redirectTo({ url: target_path })
        } catch (error) {
            await Taro.reLaunch({ url: target_path })
        }
    } catch (error) {
        console.error('returnToOriginalPage 执行出错:', error)
        try {
            await Taro.reLaunch({ url: default_path })
        } catch (final_error) {
            console.error('最终降级方案也失败了:', final_error)
        }
    }
}

/**
 * 判断是否来自分享场景
 * @param {object} options 页面 options
 * @returns {boolean}
 */
export const isFromShare = (options) => {
    return options && (options.from_share === '1' || options.scene)
}

/**
 * 分享页进入时的授权处理
 * - 来自分享且未授权:保存当前页路径,授权成功后回跳
 * - 授权失败:返回 false,由调用方决定是否继续降级处理
 * @param {object} options 页面 options
 * @param {Function} callback 授权成功后的继续逻辑
 * @returns {Promise<boolean>} true=已处理且可继续,false=授权失败
 */
export const handleSharePageAuth = async (options, callback) => {
    if (hasAuth()) {
        if (typeof callback === 'function') callback()
        return true
    }

    if (isFromShare(options)) {
        saveCurrentPagePath()
    }

    try {
        await silentAuth(
            () => {
                if (typeof callback === 'function') callback()
            },
            () => {
                Taro.navigateTo({ url: '/pages/auth/index' })
            }
        )
        return true
    } catch (error) {
        Taro.navigateTo({ url: '/pages/auth/index' })
        return false
    }
}

/**
 * 为路径追加分享标记
 * @param {string} path 原路径
 * @returns {string} 追加后的路径
 */
export const addShareFlag = (path) => {
    const separator = path.includes('?') ? '&' : '?'
    return `${path}${separator}from_share=1`
}