hookehuyr

feat(auth): 重构授权逻辑并添加工具函数

- 新增 buildApiUrl 工具函数统一构建 API 请求 URL
- 重构 refreshSession 使用新工具函数并提取 cookie 处理逻辑
- 改进 navigateToAuth 使用常量定义延迟时间并添加错误处理
- 优化错误处理传递完整错误对象
- 添加代码注释和类型标注提升可维护性
import Taro from '@tarojs/taro'
import { routerStore } from '@/stores/router'
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
import { buildApiUrl } from './tools'
// 改进:添加全局状态变量注释
// 上一次跳转到授权页的时间戳,用于防抖(避免短时间内重复跳转)
let last_navigate_auth_at = 0
// 是否正在跳转到授权页,用于防重复(避免并发跳转)
let navigating_to_auth = false
/**
......@@ -24,8 +27,9 @@ export const getCurrentPageFullPath = () => {
const route = current_page.route
const options = current_page.options || {}
// 改进:key 也需要编码,避免特殊字符导致 URL 解析错误
const query_params = Object.keys(options)
.map((key) => `${key}=${encodeURIComponent(options[key])}`)
.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(options[key])}`)
.join('&')
return query_params ? `${route}?${query_params}` : route
......@@ -59,6 +63,21 @@ export const hasAuth = () => {
let auth_promise = null
/**
* 从响应中提取 cookie
* 兼容小程序端和 H5 端的不同返回格式
* @param {object} response Taro.request 响应对象
* @returns {string|null} cookie 字符串或 null
*/
const extractCookie = (response) => {
// 小程序端优先从 response.cookies 取
if (response.cookies?.[0]) return response.cookies[0]
// H5 端从 header 取(兼容不同大小写)
const cookie = response.header?.['Set-Cookie'] || response.header?.['set-cookie']
if (Array.isArray(cookie)) return cookie[0]
return cookie || null
}
/**
* 刷新会话:通过 Taro.login 获取 code,换取后端会话 cookie 并写入缓存
* - 被 request.js 的 401 拦截器调用,用于自动“静默续期 + 原请求重放”
* - 复用 auth_promise,防止多个接口同时 401 时并发触发多次登录
......@@ -97,25 +116,9 @@ export const refreshSession = async (options) => {
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)}`,
url: buildApiUrl('openid_wxapp'),
method: 'POST',
data: request_data,
})
......@@ -124,14 +127,8 @@ export const refreshSession = async (options) => {
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]
// 改进:使用 extractCookie 函数统一处理 cookie 提取逻辑
const cookie = extractCookie(response)
if (!cookie) {
throw new Error('授权失败:没有获取到有效的会话信息')
}
......@@ -164,8 +161,10 @@ export const refreshSession = async (options) => {
* 执行静默授权:检查是否已授权,若否则调用 refreshSession 刷新会话
* @param {boolean} show_loading 是否展示 loading,默认 true
* @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果
*
* 改进:使用下划线前缀表示私有函数,仅供 silentAuth 内部使用
*/
const do_silent_auth = async (show_loading) => {
const _do_silent_auth = async (show_loading) => {
// 已有 sessionid 时直接视为已授权
if (hasAuth()) {
return { code: 1, msg: '已授权' }
......@@ -179,7 +178,7 @@ const do_silent_auth = async (show_loading) => {
* 静默授权:用于启动阶段/分享页/授权页发起授权
* - 与 refreshSession 共用 auth_promise,避免并发重复调用
* @param {(result: any) => void} on_success 成功回调(可选)
* @param {(error_msg: string) => void} on_error 失败回调(可选,入参为错误文案
* @param {(error: {message:string, original:Error}) => void} on_error 失败回调(可选,入参为错误对象
* @param {object} options 可选项
* @param {boolean} options.show_loading 是否展示 loading,默认 true
* @returns {Promise<any>} 授权结果(成功 resolve,失败 reject)
......@@ -191,16 +190,16 @@ export const silentAuth = async (on_success, on_error, options) => {
// 未有授权进行中时才发起一次授权,并复用 Promise
if (!auth_promise) {
/**
* 用 auth_promise 做“单例锁”,把同一时刻并发触发的多次授权合并成一次。
* 用 auth_promise 做"单例锁",把同一时刻并发触发的多次授权合并成一次。
* 把正在执行的授权 Promise 存起来;后面如果又有人调用 silentAuth() ,
* 看到 auth_promise 不为空,就直接 await 同一个 Promise,避免同时发起多次 Taro.login / 换会话请求
* ---------------------------------------------------------------------------------------
* .finally(() => { auth_promise = null }) 不管授权成功还是失败(resolve/reject),都把“锁”释放掉。
* .finally(() => { auth_promise = null }) 不管授权成功还是失败(resolve/reject),都把"锁"释放掉。
* 不用 finally 的问题:如果授权失败抛错了,而你只在 .then 里清空,那么 auth_promise 会一直卡着旧的 rejected Promise;
* 后续再调用 silentAuth() 会复用这个失败的 Promise,导致永远失败、且永远不会重新发起授权。
* 用 finally :保证成功/失败都会清空,下一次调用才有机会重新走授权流程。
*/
auth_promise = do_silent_auth(show_loading)
auth_promise = _do_silent_auth(show_loading)
.finally(() => {
auth_promise = null
})
......@@ -214,24 +213,31 @@ export const silentAuth = async (on_success, on_error, options) => {
* - 授权页: silentAuth().then(() => returnToOriginalPage()) then 里也没接 res , auth/index.vue
* - 分享场景: await silentAuth(successCb, errorCb) 只看成功/失败分支,不用返回值, handleSharePageAuth
* 所以这行 return result 的作用目前是 语义完整 + 未来扩展位 :
* 如果以后要在调用处根据 code/msg/cookie 做分支或埋点,返回值就能直接用;现在等价于“只用 resolve/reject 表达成功失败”
* 如果以后要在调用处根据 code/msg/cookie 做分支或埋点,返回值就能直接用;现在等价于"只用 resolve/reject 表达成功失败"
*/
return result
} catch (error) {
const error_msg = error?.message || '授权失败,请稍后重试'
if (on_error) on_error(error_msg)
// 改进:统一传递完整错误对象,包含 message 和 original error
const error_obj = {
message: error?.message || '授权失败,请稍后重试',
original: error
}
if (on_error) on_error(error_obj)
throw error
}
}
const NAVIGATE_AUTH_COOLDOWN_MS = 1200 // 防重复跳转冷却时间
const NAVIGATING_RESET_DELAY_MS = 300 // 导航状态重置延迟
/**
* 跳转到授权页(降级方案)
* - 会先保存回跳路径(默认当前页),授权成功后在 auth 页回跳
* @param {string} return_path 指定回跳路径(可选)
* @returns {void} 无返回值
* @returns {Promise<void>} 无返回值
*/
export const navigateToAuth = (return_path) => {
export const navigateToAuth = async (return_path) => {
const pages = Taro.getCurrentPages()
const current_page = pages[pages.length - 1]
const current_route = current_page?.route
......@@ -241,7 +247,7 @@ export const navigateToAuth = (return_path) => {
const now = Date.now()
if (navigating_to_auth) return
if (now - last_navigate_auth_at < 1200) return
if (now - last_navigate_auth_at < NAVIGATE_AUTH_COOLDOWN_MS) return
last_navigate_auth_at = now
navigating_to_auth = true
......@@ -252,15 +258,18 @@ export const navigateToAuth = (return_path) => {
saveCurrentPagePath()
}
const done = () => {
// 改进:使用 try-finally 明确状态恢复逻辑,确保无论成功失败都会重置状态
try {
await Taro.navigateTo({ url: '/pages/auth/index' })
} catch (error) {
// 改进:添加错误日志,方便追踪降级场景
console.warn('navigateTo 失败,降级使用 redirectTo:', error)
await Taro.redirectTo({ url: '/pages/auth/index' })
} finally {
setTimeout(() => {
navigating_to_auth = false
}, 300)
}, NAVIGATING_RESET_DELAY_MS)
}
Taro.navigateTo({ url: '/pages/auth/index' })
.catch(() => Taro.redirectTo({ url: '/pages/auth/index' }))
.finally(done)
}
/**
......@@ -295,6 +304,8 @@ export const returnToOriginalPage = async (default_path = '/pages/index/index')
try {
await Taro.redirectTo({ url: target_path })
} catch (error) {
// 改进:添加错误日志,方便追踪降级场景
console.warn('redirectTo 失败,降级使用 reLaunch:', error)
await Taro.reLaunch({ url: target_path })
}
} catch (error) {
......
/*
* @Date: 2022-04-18 15:59:42
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-13 11:39:03
* @LastEditTime: 2026-01-17 23:51:44
* @FilePath: /xyxBooking-weapp/src/utils/tools.js
* @Description: 文件描述
*/
import dayjs from 'dayjs';
import Taro from '@tarojs/taro';
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
// 格式化时间
const formatDate = (date) => {
......@@ -110,4 +111,23 @@ const formatDatetime = (data) => {
return `${start.format('YYYY-MM-DD')} ${start.format('HH:mm')}-${end.format('HH:mm')}`;
};
export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime };
/**
* @description 构建 API 请求 URL
* @param {string} action - API 动作名称(如 'get_user_info')
* @param {Object} [params={}] - 请求参数对象,默认空对象
* @returns {string} 完整的 API 请求 URL,包含基础路径、动作参数、默认参数和查询字符串
* @example
* buildApiUrl('get_user_info', { user_id: 123 });
* 返回 "/srv/?a=get_user_info&f=json&client_name=xyxBooking&user_id=123"
*/
const buildApiUrl = (action, params = {}) => {
const queryParams = new URLSearchParams({
a: action,
f: REQUEST_DEFAULT_PARAMS.f,
client_name: REQUEST_DEFAULT_PARAMS.client_name,
...params
})
return `${BASE_URL}/srv/?${queryParams.toString()}`
}
export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, buildApiUrl };
......