hookehuyr

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

- 新增 buildApiUrl 工具函数统一构建 API 请求 URL
- 重构 refreshSession 使用新工具函数并提取 cookie 处理逻辑
- 改进 navigateToAuth 使用常量定义延迟时间并添加错误处理
- 优化错误处理传递完整错误对象
- 添加代码注释和类型标注提升可维护性
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 };
......