feat(auth): 重构授权逻辑并添加工具函数
- 新增 buildApiUrl 工具函数统一构建 API 请求 URL - 重构 refreshSession 使用新工具函数并提取 cookie 处理逻辑 - 改进 navigateToAuth 使用常量定义延迟时间并添加错误处理 - 优化错误处理传递完整错误对象 - 添加代码注释和类型标注提升可维护性
Showing
2 changed files
with
77 additions
and
46 deletions
| 1 | import Taro from '@tarojs/taro' | 1 | import Taro from '@tarojs/taro' |
| 2 | import { routerStore } from '@/stores/router' | 2 | import { routerStore } from '@/stores/router' |
| 3 | -import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config' | 3 | +import { buildApiUrl } from './tools' |
| 4 | 4 | ||
| 5 | +// 改进:添加全局状态变量注释 | ||
| 6 | +// 上一次跳转到授权页的时间戳,用于防抖(避免短时间内重复跳转) | ||
| 5 | let last_navigate_auth_at = 0 | 7 | let last_navigate_auth_at = 0 |
| 8 | +// 是否正在跳转到授权页,用于防重复(避免并发跳转) | ||
| 6 | let navigating_to_auth = false | 9 | let navigating_to_auth = false |
| 7 | 10 | ||
| 8 | /** | 11 | /** |
| ... | @@ -24,8 +27,9 @@ export const getCurrentPageFullPath = () => { | ... | @@ -24,8 +27,9 @@ export const getCurrentPageFullPath = () => { |
| 24 | const route = current_page.route | 27 | const route = current_page.route |
| 25 | const options = current_page.options || {} | 28 | const options = current_page.options || {} |
| 26 | 29 | ||
| 30 | + // 改进:key 也需要编码,避免特殊字符导致 URL 解析错误 | ||
| 27 | const query_params = Object.keys(options) | 31 | const query_params = Object.keys(options) |
| 28 | - .map((key) => `${key}=${encodeURIComponent(options[key])}`) | 32 | + .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(options[key])}`) |
| 29 | .join('&') | 33 | .join('&') |
| 30 | 34 | ||
| 31 | return query_params ? `${route}?${query_params}` : route | 35 | return query_params ? `${route}?${query_params}` : route |
| ... | @@ -59,6 +63,21 @@ export const hasAuth = () => { | ... | @@ -59,6 +63,21 @@ export const hasAuth = () => { |
| 59 | let auth_promise = null | 63 | let auth_promise = null |
| 60 | 64 | ||
| 61 | /** | 65 | /** |
| 66 | + * 从响应中提取 cookie | ||
| 67 | + * 兼容小程序端和 H5 端的不同返回格式 | ||
| 68 | + * @param {object} response Taro.request 响应对象 | ||
| 69 | + * @returns {string|null} cookie 字符串或 null | ||
| 70 | + */ | ||
| 71 | +const extractCookie = (response) => { | ||
| 72 | + // 小程序端优先从 response.cookies 取 | ||
| 73 | + if (response.cookies?.[0]) return response.cookies[0] | ||
| 74 | + // H5 端从 header 取(兼容不同大小写) | ||
| 75 | + const cookie = response.header?.['Set-Cookie'] || response.header?.['set-cookie'] | ||
| 76 | + if (Array.isArray(cookie)) return cookie[0] | ||
| 77 | + return cookie || null | ||
| 78 | +} | ||
| 79 | + | ||
| 80 | +/** | ||
| 62 | * 刷新会话:通过 Taro.login 获取 code,换取后端会话 cookie 并写入缓存 | 81 | * 刷新会话:通过 Taro.login 获取 code,换取后端会话 cookie 并写入缓存 |
| 63 | * - 被 request.js 的 401 拦截器调用,用于自动“静默续期 + 原请求重放” | 82 | * - 被 request.js 的 401 拦截器调用,用于自动“静默续期 + 原请求重放” |
| 64 | * - 复用 auth_promise,防止多个接口同时 401 时并发触发多次登录 | 83 | * - 复用 auth_promise,防止多个接口同时 401 时并发触发多次登录 |
| ... | @@ -97,25 +116,9 @@ export const refreshSession = async (options) => { | ... | @@ -97,25 +116,9 @@ export const refreshSession = async (options) => { |
| 97 | code: login_result.code, | 116 | code: login_result.code, |
| 98 | } | 117 | } |
| 99 | 118 | ||
| 100 | - // 开发环境可按需手动传 openid(仅用于本地联调) | ||
| 101 | - if (process.env.NODE_ENV === 'development') { | ||
| 102 | - // request_data.openid = 'waj'; | ||
| 103 | - // request_data.openid = 'h-009'; | ||
| 104 | - // request_data.openid = 'h-010'; | ||
| 105 | - // request_data.openid = 'h-011'; | ||
| 106 | - // request_data.openid = 'h-012'; | ||
| 107 | - // request_data.openid = 'h-013'; | ||
| 108 | - // request_data.openid = 'oWbdFvkD5VtloC50wSNR9IWiU2q8'; | ||
| 109 | - // request_data.openid = 'oex8h5QZnZJto3ttvO6swSvylAQo'; | ||
| 110 | - } | ||
| 111 | - | ||
| 112 | - if (process.env.NODE_ENV === 'production') { | ||
| 113 | - // request_data.openid = 'h-013'; | ||
| 114 | - } | ||
| 115 | - | ||
| 116 | // 换取后端会话(服务端通过 Set-Cookie 返回会话信息) | 119 | // 换取后端会话(服务端通过 Set-Cookie 返回会话信息) |
| 117 | const response = await Taro.request({ | 120 | const response = await Taro.request({ |
| 118 | - url: `${BASE_URL}/srv/?a=openid_wxapp&f=${encodeURIComponent(REQUEST_DEFAULT_PARAMS.f)}&client_name=${encodeURIComponent(REQUEST_DEFAULT_PARAMS.client_name)}`, | 121 | + url: buildApiUrl('openid_wxapp'), |
| 119 | method: 'POST', | 122 | method: 'POST', |
| 120 | data: request_data, | 123 | data: request_data, |
| 121 | }) | 124 | }) |
| ... | @@ -124,14 +127,8 @@ export const refreshSession = async (options) => { | ... | @@ -124,14 +127,8 @@ export const refreshSession = async (options) => { |
| 124 | throw new Error(response?.data?.msg || '授权失败') | 127 | throw new Error(response?.data?.msg || '授权失败') |
| 125 | } | 128 | } |
| 126 | 129 | ||
| 127 | - // 兼容小程序环境下 cookie 的不同字段位置 | 130 | + // 改进:使用 extractCookie 函数统一处理 cookie 提取逻辑 |
| 128 | - let cookie = | 131 | + const cookie = extractCookie(response) |
| 129 | - (response.cookies && response.cookies[0]) || | ||
| 130 | - response.header?.['Set-Cookie'] || | ||
| 131 | - response.header?.['set-cookie'] | ||
| 132 | - | ||
| 133 | - if (Array.isArray(cookie)) cookie = cookie[0] | ||
| 134 | - | ||
| 135 | if (!cookie) { | 132 | if (!cookie) { |
| 136 | throw new Error('授权失败:没有获取到有效的会话信息') | 133 | throw new Error('授权失败:没有获取到有效的会话信息') |
| 137 | } | 134 | } |
| ... | @@ -164,8 +161,10 @@ export const refreshSession = async (options) => { | ... | @@ -164,8 +161,10 @@ export const refreshSession = async (options) => { |
| 164 | * 执行静默授权:检查是否已授权,若否则调用 refreshSession 刷新会话 | 161 | * 执行静默授权:检查是否已授权,若否则调用 refreshSession 刷新会话 |
| 165 | * @param {boolean} show_loading 是否展示 loading,默认 true | 162 | * @param {boolean} show_loading 是否展示 loading,默认 true |
| 166 | * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果 | 163 | * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果 |
| 164 | + * | ||
| 165 | + * 改进:使用下划线前缀表示私有函数,仅供 silentAuth 内部使用 | ||
| 167 | */ | 166 | */ |
| 168 | -const do_silent_auth = async (show_loading) => { | 167 | +const _do_silent_auth = async (show_loading) => { |
| 169 | // 已有 sessionid 时直接视为已授权 | 168 | // 已有 sessionid 时直接视为已授权 |
| 170 | if (hasAuth()) { | 169 | if (hasAuth()) { |
| 171 | return { code: 1, msg: '已授权' } | 170 | return { code: 1, msg: '已授权' } |
| ... | @@ -179,7 +178,7 @@ const do_silent_auth = async (show_loading) => { | ... | @@ -179,7 +178,7 @@ const do_silent_auth = async (show_loading) => { |
| 179 | * 静默授权:用于启动阶段/分享页/授权页发起授权 | 178 | * 静默授权:用于启动阶段/分享页/授权页发起授权 |
| 180 | * - 与 refreshSession 共用 auth_promise,避免并发重复调用 | 179 | * - 与 refreshSession 共用 auth_promise,避免并发重复调用 |
| 181 | * @param {(result: any) => void} on_success 成功回调(可选) | 180 | * @param {(result: any) => void} on_success 成功回调(可选) |
| 182 | - * @param {(error_msg: string) => void} on_error 失败回调(可选,入参为错误文案) | 181 | + * @param {(error: {message:string, original:Error}) => void} on_error 失败回调(可选,入参为错误对象) |
| 183 | * @param {object} options 可选项 | 182 | * @param {object} options 可选项 |
| 184 | * @param {boolean} options.show_loading 是否展示 loading,默认 true | 183 | * @param {boolean} options.show_loading 是否展示 loading,默认 true |
| 185 | * @returns {Promise<any>} 授权结果(成功 resolve,失败 reject) | 184 | * @returns {Promise<any>} 授权结果(成功 resolve,失败 reject) |
| ... | @@ -191,16 +190,16 @@ export const silentAuth = async (on_success, on_error, options) => { | ... | @@ -191,16 +190,16 @@ export const silentAuth = async (on_success, on_error, options) => { |
| 191 | // 未有授权进行中时才发起一次授权,并复用 Promise | 190 | // 未有授权进行中时才发起一次授权,并复用 Promise |
| 192 | if (!auth_promise) { | 191 | if (!auth_promise) { |
| 193 | /** | 192 | /** |
| 194 | - * 用 auth_promise 做“单例锁”,把同一时刻并发触发的多次授权合并成一次。 | 193 | + * 用 auth_promise 做"单例锁",把同一时刻并发触发的多次授权合并成一次。 |
| 195 | * 把正在执行的授权 Promise 存起来;后面如果又有人调用 silentAuth() , | 194 | * 把正在执行的授权 Promise 存起来;后面如果又有人调用 silentAuth() , |
| 196 | * 看到 auth_promise 不为空,就直接 await 同一个 Promise,避免同时发起多次 Taro.login / 换会话请求 | 195 | * 看到 auth_promise 不为空,就直接 await 同一个 Promise,避免同时发起多次 Taro.login / 换会话请求 |
| 197 | * --------------------------------------------------------------------------------------- | 196 | * --------------------------------------------------------------------------------------- |
| 198 | - * .finally(() => { auth_promise = null }) 不管授权成功还是失败(resolve/reject),都把“锁”释放掉。 | 197 | + * .finally(() => { auth_promise = null }) 不管授权成功还是失败(resolve/reject),都把"锁"释放掉。 |
| 199 | * 不用 finally 的问题:如果授权失败抛错了,而你只在 .then 里清空,那么 auth_promise 会一直卡着旧的 rejected Promise; | 198 | * 不用 finally 的问题:如果授权失败抛错了,而你只在 .then 里清空,那么 auth_promise 会一直卡着旧的 rejected Promise; |
| 200 | * 后续再调用 silentAuth() 会复用这个失败的 Promise,导致永远失败、且永远不会重新发起授权。 | 199 | * 后续再调用 silentAuth() 会复用这个失败的 Promise,导致永远失败、且永远不会重新发起授权。 |
| 201 | * 用 finally :保证成功/失败都会清空,下一次调用才有机会重新走授权流程。 | 200 | * 用 finally :保证成功/失败都会清空,下一次调用才有机会重新走授权流程。 |
| 202 | */ | 201 | */ |
| 203 | - auth_promise = do_silent_auth(show_loading) | 202 | + auth_promise = _do_silent_auth(show_loading) |
| 204 | .finally(() => { | 203 | .finally(() => { |
| 205 | auth_promise = null | 204 | auth_promise = null |
| 206 | }) | 205 | }) |
| ... | @@ -214,24 +213,31 @@ export const silentAuth = async (on_success, on_error, options) => { | ... | @@ -214,24 +213,31 @@ export const silentAuth = async (on_success, on_error, options) => { |
| 214 | * - 授权页: silentAuth().then(() => returnToOriginalPage()) then 里也没接 res , auth/index.vue | 213 | * - 授权页: silentAuth().then(() => returnToOriginalPage()) then 里也没接 res , auth/index.vue |
| 215 | * - 分享场景: await silentAuth(successCb, errorCb) 只看成功/失败分支,不用返回值, handleSharePageAuth | 214 | * - 分享场景: await silentAuth(successCb, errorCb) 只看成功/失败分支,不用返回值, handleSharePageAuth |
| 216 | * 所以这行 return result 的作用目前是 语义完整 + 未来扩展位 : | 215 | * 所以这行 return result 的作用目前是 语义完整 + 未来扩展位 : |
| 217 | - * 如果以后要在调用处根据 code/msg/cookie 做分支或埋点,返回值就能直接用;现在等价于“只用 resolve/reject 表达成功失败”。 | 216 | + * 如果以后要在调用处根据 code/msg/cookie 做分支或埋点,返回值就能直接用;现在等价于"只用 resolve/reject 表达成功失败"。 |
| 218 | */ | 217 | */ |
| 219 | 218 | ||
| 220 | return result | 219 | return result |
| 221 | } catch (error) { | 220 | } catch (error) { |
| 222 | - const error_msg = error?.message || '授权失败,请稍后重试' | 221 | + // 改进:统一传递完整错误对象,包含 message 和 original error |
| 223 | - if (on_error) on_error(error_msg) | 222 | + const error_obj = { |
| 223 | + message: error?.message || '授权失败,请稍后重试', | ||
| 224 | + original: error | ||
| 225 | + } | ||
| 226 | + if (on_error) on_error(error_obj) | ||
| 224 | throw error | 227 | throw error |
| 225 | } | 228 | } |
| 226 | } | 229 | } |
| 227 | 230 | ||
| 231 | +const NAVIGATE_AUTH_COOLDOWN_MS = 1200 // 防重复跳转冷却时间 | ||
| 232 | +const NAVIGATING_RESET_DELAY_MS = 300 // 导航状态重置延迟 | ||
| 233 | + | ||
| 228 | /** | 234 | /** |
| 229 | * 跳转到授权页(降级方案) | 235 | * 跳转到授权页(降级方案) |
| 230 | * - 会先保存回跳路径(默认当前页),授权成功后在 auth 页回跳 | 236 | * - 会先保存回跳路径(默认当前页),授权成功后在 auth 页回跳 |
| 231 | * @param {string} return_path 指定回跳路径(可选) | 237 | * @param {string} return_path 指定回跳路径(可选) |
| 232 | - * @returns {void} 无返回值 | 238 | + * @returns {Promise<void>} 无返回值 |
| 233 | */ | 239 | */ |
| 234 | -export const navigateToAuth = (return_path) => { | 240 | +export const navigateToAuth = async (return_path) => { |
| 235 | const pages = Taro.getCurrentPages() | 241 | const pages = Taro.getCurrentPages() |
| 236 | const current_page = pages[pages.length - 1] | 242 | const current_page = pages[pages.length - 1] |
| 237 | const current_route = current_page?.route | 243 | const current_route = current_page?.route |
| ... | @@ -241,7 +247,7 @@ export const navigateToAuth = (return_path) => { | ... | @@ -241,7 +247,7 @@ export const navigateToAuth = (return_path) => { |
| 241 | 247 | ||
| 242 | const now = Date.now() | 248 | const now = Date.now() |
| 243 | if (navigating_to_auth) return | 249 | if (navigating_to_auth) return |
| 244 | - if (now - last_navigate_auth_at < 1200) return | 250 | + if (now - last_navigate_auth_at < NAVIGATE_AUTH_COOLDOWN_MS) return |
| 245 | 251 | ||
| 246 | last_navigate_auth_at = now | 252 | last_navigate_auth_at = now |
| 247 | navigating_to_auth = true | 253 | navigating_to_auth = true |
| ... | @@ -252,15 +258,18 @@ export const navigateToAuth = (return_path) => { | ... | @@ -252,15 +258,18 @@ export const navigateToAuth = (return_path) => { |
| 252 | saveCurrentPagePath() | 258 | saveCurrentPagePath() |
| 253 | } | 259 | } |
| 254 | 260 | ||
| 255 | - const done = () => { | 261 | + // 改进:使用 try-finally 明确状态恢复逻辑,确保无论成功失败都会重置状态 |
| 262 | + try { | ||
| 263 | + await Taro.navigateTo({ url: '/pages/auth/index' }) | ||
| 264 | + } catch (error) { | ||
| 265 | + // 改进:添加错误日志,方便追踪降级场景 | ||
| 266 | + console.warn('navigateTo 失败,降级使用 redirectTo:', error) | ||
| 267 | + await Taro.redirectTo({ url: '/pages/auth/index' }) | ||
| 268 | + } finally { | ||
| 256 | setTimeout(() => { | 269 | setTimeout(() => { |
| 257 | navigating_to_auth = false | 270 | navigating_to_auth = false |
| 258 | - }, 300) | 271 | + }, NAVIGATING_RESET_DELAY_MS) |
| 259 | } | 272 | } |
| 260 | - | ||
| 261 | - Taro.navigateTo({ url: '/pages/auth/index' }) | ||
| 262 | - .catch(() => Taro.redirectTo({ url: '/pages/auth/index' })) | ||
| 263 | - .finally(done) | ||
| 264 | } | 273 | } |
| 265 | 274 | ||
| 266 | /** | 275 | /** |
| ... | @@ -295,6 +304,8 @@ export const returnToOriginalPage = async (default_path = '/pages/index/index') | ... | @@ -295,6 +304,8 @@ export const returnToOriginalPage = async (default_path = '/pages/index/index') |
| 295 | try { | 304 | try { |
| 296 | await Taro.redirectTo({ url: target_path }) | 305 | await Taro.redirectTo({ url: target_path }) |
| 297 | } catch (error) { | 306 | } catch (error) { |
| 307 | + // 改进:添加错误日志,方便追踪降级场景 | ||
| 308 | + console.warn('redirectTo 失败,降级使用 reLaunch:', error) | ||
| 298 | await Taro.reLaunch({ url: target_path }) | 309 | await Taro.reLaunch({ url: target_path }) |
| 299 | } | 310 | } |
| 300 | } catch (error) { | 311 | } catch (error) { | ... | ... |
| 1 | /* | 1 | /* |
| 2 | * @Date: 2022-04-18 15:59:42 | 2 | * @Date: 2022-04-18 15:59:42 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-13 11:39:03 | 4 | + * @LastEditTime: 2026-01-17 23:51:44 |
| 5 | * @FilePath: /xyxBooking-weapp/src/utils/tools.js | 5 | * @FilePath: /xyxBooking-weapp/src/utils/tools.js |
| 6 | * @Description: 文件描述 | 6 | * @Description: 文件描述 |
| 7 | */ | 7 | */ |
| 8 | import dayjs from 'dayjs'; | 8 | import dayjs from 'dayjs'; |
| 9 | import Taro from '@tarojs/taro'; | 9 | import Taro from '@tarojs/taro'; |
| 10 | +import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config' | ||
| 10 | 11 | ||
| 11 | // 格式化时间 | 12 | // 格式化时间 |
| 12 | const formatDate = (date) => { | 13 | const formatDate = (date) => { |
| ... | @@ -110,4 +111,23 @@ const formatDatetime = (data) => { | ... | @@ -110,4 +111,23 @@ const formatDatetime = (data) => { |
| 110 | return `${start.format('YYYY-MM-DD')} ${start.format('HH:mm')}-${end.format('HH:mm')}`; | 111 | return `${start.format('YYYY-MM-DD')} ${start.format('HH:mm')}-${end.format('HH:mm')}`; |
| 111 | }; | 112 | }; |
| 112 | 113 | ||
| 113 | -export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime }; | 114 | +/** |
| 115 | + * @description 构建 API 请求 URL | ||
| 116 | + * @param {string} action - API 动作名称(如 'get_user_info') | ||
| 117 | + * @param {Object} [params={}] - 请求参数对象,默认空对象 | ||
| 118 | + * @returns {string} 完整的 API 请求 URL,包含基础路径、动作参数、默认参数和查询字符串 | ||
| 119 | + * @example | ||
| 120 | + * buildApiUrl('get_user_info', { user_id: 123 }); | ||
| 121 | + * 返回 "/srv/?a=get_user_info&f=json&client_name=xyxBooking&user_id=123" | ||
| 122 | + */ | ||
| 123 | +const buildApiUrl = (action, params = {}) => { | ||
| 124 | + const queryParams = new URLSearchParams({ | ||
| 125 | + a: action, | ||
| 126 | + f: REQUEST_DEFAULT_PARAMS.f, | ||
| 127 | + client_name: REQUEST_DEFAULT_PARAMS.client_name, | ||
| 128 | + ...params | ||
| 129 | + }) | ||
| 130 | + return `${BASE_URL}/srv/?${queryParams.toString()}` | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +export { formatDate, wxInfo, parseQueryString, strExist, formatDatetime, buildApiUrl }; | ... | ... |
-
Please register or login to post a comment