hookehuyr

refactor(auth): 重构授权逻辑,优化会话管理和错误处理

- 将授权相关功能抽离到独立模块 authRedirect.js
- 实现静默续期和请求重放机制
- 优化授权失败后的降级处理
- 统一管理页面跳转和路径保存逻辑
- 移除未使用的代码和组件
...@@ -10,7 +10,6 @@ declare module 'vue' { ...@@ -10,7 +10,6 @@ declare module 'vue' {
10 NutButton: typeof import('@nutui/nutui-taro')['Button'] 10 NutButton: typeof import('@nutui/nutui-taro')['Button']
11 NutCheckbox: typeof import('@nutui/nutui-taro')['Checkbox'] 11 NutCheckbox: typeof import('@nutui/nutui-taro')['Checkbox']
12 NutCheckboxGroup: typeof import('@nutui/nutui-taro')['CheckboxGroup'] 12 NutCheckboxGroup: typeof import('@nutui/nutui-taro')['CheckboxGroup']
13 - NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
14 NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker'] 13 NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
15 NutInput: typeof import('@nutui/nutui-taro')['Input'] 14 NutInput: typeof import('@nutui/nutui-taro')['Input']
16 NutPicker: typeof import('@nutui/nutui-taro')['Picker'] 15 NutPicker: typeof import('@nutui/nutui-taro')['Picker']
......
...@@ -45,29 +45,19 @@ ...@@ -45,29 +45,19 @@
45 </template> 45 </template>
46 46
47 <script setup> 47 <script setup>
48 -import { ref } from 'vue' 48 +import Taro, { useShareAppMessage } from '@tarojs/taro'
49 -import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
50 // import { showSuccessToast, showFailToast } from 'vant'; // NutUI 或 Taro API 49 // import { showSuccessToast, showFailToast } from 'vant'; // NutUI 或 Taro API
51 -import { billListAPI } from '@/api/index';
52 import { useGo } from '@/hooks/useGo' 50 import { useGo } from '@/hooks/useGo'
53 import icon_1 from '@/assets/images/立即预约@2x.png' 51 import icon_1 from '@/assets/images/立即预约@2x.png'
54 -import icon_2 from '@/assets/images/预约记录@2x.png'
55 import icon_3 from '@/assets/images/首页02@2x.png' 52 import icon_3 from '@/assets/images/首页02@2x.png'
56 import icon_4 from '@/assets/images/二维码icon.png' 53 import icon_4 from '@/assets/images/二维码icon.png'
57 import icon_5 from '@/assets/images/我的01@2x.png' 54 import icon_5 from '@/assets/images/我的01@2x.png'
58 -import icon_6 from '@/assets/images/luru@2x.png'
59 55
60 const go = useGo(); 56 const go = useGo();
61 57
62 const toBooking = () => { // 跳转到预约须知 58 const toBooking = () => { // 跳转到预约须知
63 go('/notice'); 59 go('/notice');
64 } 60 }
65 -const toRecord = () => { // 跳转到预约记录
66 - go('/bookingList');
67 -}
68 -const toSearch = () => { // 跳转到寺院录入
69 - go('/search');
70 -}
71 const toCode = () => { // 跳转到预约码 61 const toCode = () => { // 跳转到预约码
72 Taro.redirectTo({ 62 Taro.redirectTo({
73 url: '/pages/bookingCode/index' 63 url: '/pages/bookingCode/index'
...@@ -79,12 +69,6 @@ const toMy = () => { // 跳转到我的 ...@@ -79,12 +69,6 @@ const toMy = () => { // 跳转到我的
79 }) 69 })
80 } 70 }
81 71
82 -useDidShow(async () => {
83 - // TAG: 触发授权页面 (检查 session 或调用接口触发 401)
84 - // 小程序中,request.js 拦截器会处理 401 跳转
85 - await billListAPI({ page: 1, row_num: 1 });
86 -});
87 -
88 useShareAppMessage(() => { 72 useShareAppMessage(() => {
89 return { 73 return {
90 title: '西园寺预约', 74 title: '西园寺预约',
......
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 request from '@/utils/request' 3 +import BASE_URL from './config'
4 - 4 +
5 +/**
6 + * 授权与回跳相关工具
7 + * - 统一管理:保存来源页、静默授权、跳转授权页、授权后回跳
8 + * - 约定:sessionid 存在于本地缓存 key 为 sessionid
9 + * - 说明:refreshSession/silentAuth 使用单例 Promise,避免并发重复授权
10 + */
11 +
12 +/**
13 + * 获取当前页完整路径(含 query)
14 + * @returns {string} 当前页路径,示例:pages/index/index?a=1
15 + */
5 export const getCurrentPageFullPath = () => { 16 export const getCurrentPageFullPath = () => {
6 - const pages = getCurrentPages() 17 + const pages = Taro.getCurrentPages()
7 if (!pages || pages.length === 0) return '' 18 if (!pages || pages.length === 0) return ''
8 19
9 const current_page = pages[pages.length - 1] 20 const current_page = pages[pages.length - 1]
...@@ -17,12 +28,21 @@ export const getCurrentPageFullPath = () => { ...@@ -17,12 +28,21 @@ export const getCurrentPageFullPath = () => {
17 return query_params ? `${route}?${query_params}` : route 28 return query_params ? `${route}?${query_params}` : route
18 } 29 }
19 30
31 +/**
32 + * 保存当前页路径(用于授权成功后回跳)
33 + * @param {string} custom_path 自定义路径,不传则取当前页
34 + * @returns {void}
35 + */
20 export const saveCurrentPagePath = (custom_path) => { 36 export const saveCurrentPagePath = (custom_path) => {
21 const router = routerStore() 37 const router = routerStore()
22 const path = custom_path || getCurrentPageFullPath() 38 const path = custom_path || getCurrentPageFullPath()
23 router.add(path) 39 router.add(path)
24 } 40 }
25 41
42 +/**
43 + * 判断是否需要授权
44 + * @returns {boolean} true=需要授权,false=已存在 sessionid
45 + */
26 export const needAuth = () => { 46 export const needAuth = () => {
27 try { 47 try {
28 const sessionid = Taro.getStorageSync('sessionid') 48 const sessionid = Taro.getStorageSync('sessionid')
...@@ -33,71 +53,150 @@ export const needAuth = () => { ...@@ -33,71 +53,150 @@ export const needAuth = () => {
33 } 53 }
34 } 54 }
35 55
36 -export const silentAuth = async (on_success, on_error) => { 56 +let auth_promise = null
37 - try {
38 - if (!needAuth()) {
39 - const result = { code: 1, msg: '已授权' }
40 - if (on_success) on_success(result)
41 - return result
42 - }
43 57
44 - Taro.showLoading({ 58 +/**
45 - title: '加载中...', 59 + * 刷新会话:通过 Taro.login 获取 code,换取后端会话 cookie 并写入缓存
46 - mask: true, 60 + * - 被 request.js 的 401 拦截器调用,用于自动“静默续期 + 原请求重放”
47 - }) 61 + * - 复用 auth_promise,防止多个接口同时 401 时并发触发多次登录
62 + * @param {object} options 可选项
63 + * @param {boolean} options.show_loading 是否展示 loading,默认 true
64 + * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 后端返回 + cookie
65 + */
66 +export const refreshSession = async (options) => {
67 + const show_loading = options?.show_loading !== false
48 68
49 - const login_result = await new Promise((resolve, reject) => { 69 + // 已有授权进行中时,直接复用同一个 Promise
50 - Taro.login({ 70 + if (auth_promise) return auth_promise
51 - success: resolve, 71 +
52 - fail: reject, 72 + auth_promise = (async () => {
73 + try {
74 + if (show_loading) {
75 + Taro.showLoading({
76 + title: '加载中...',
77 + mask: true,
78 + })
79 + }
80 +
81 + // 调用微信登录获取临时 code
82 + const login_result = await new Promise((resolve, reject) => {
83 + Taro.login({
84 + success: resolve,
85 + fail: reject,
86 + })
53 }) 87 })
54 - })
55 88
56 - if (!login_result || !login_result.code) { 89 + if (!login_result || !login_result.code) {
57 - throw new Error('获取微信登录code失败') 90 + throw new Error('获取微信登录code失败')
58 - } 91 + }
59 92
60 - const response = await request.post('/srv/?a=openid', { 93 + const request_data = {
61 - code: login_result.code, 94 + code: login_result.code,
62 - }) 95 + }
63 96
64 - Taro.hideLoading() 97 + // 开发环境可按需手动传 openid(仅用于本地联调)
98 + if (process.env.NODE_ENV === 'development') {
99 + // request_data.openid = 'h-008';
100 + // request_data.openid = 'h-009';
101 + // request_data.openid = 'h-010';
102 + // request_data.openid = 'h-011';
103 + // request_data.openid = 'h-012';
104 + // request_data.openid = 'h-013';
105 + // request_data.openid = 'oWbdFvkD5VtloC50wSNR9IWiU2q8';
106 + // request_data.openid = 'oex8h5QZnZJto3ttvO6swSvylAQo';
107 + }
65 108
66 - if (!response?.data || response.data.code !== 1) { 109 + if (process.env.NODE_ENV === 'production') {
67 - const error_msg = response?.data?.msg || '授权失败' 110 + // request_data.openid = 'h-013';
68 - if (on_error) on_error(error_msg) 111 + }
69 - throw new Error(error_msg)
70 - }
71 112
72 - let cookie = 113 + // 换取后端会话(服务端通过 Set-Cookie 返回会话信息)
73 - (response.cookies && response.cookies[0]) || 114 + const response = await Taro.request({
74 - response.header?.['Set-Cookie'] || 115 + url: `${BASE_URL}/srv/?a=openid&f=reserve&client_name=${encodeURIComponent('智慧西园寺')}`,
75 - response.header?.['set-cookie'] 116 + method: 'POST',
117 + data: request_data,
118 + })
76 119
77 - if (Array.isArray(cookie)) cookie = cookie[0] 120 + if (!response?.data || response.data.code !== 1) {
121 + throw new Error(response?.data?.msg || '授权失败')
122 + }
78 123
79 - if (!cookie) { 124 + // 兼容小程序环境下 cookie 的不同字段位置
80 - const error_msg = '授权失败:没有获取到有效的会话信息' 125 + let cookie =
81 - if (on_error) on_error(error_msg) 126 + (response.cookies && response.cookies[0]) ||
82 - throw new Error(error_msg) 127 + response.header?.['Set-Cookie'] ||
83 - } 128 + response.header?.['set-cookie']
129 +
130 + if (Array.isArray(cookie)) cookie = cookie[0]
131 +
132 + if (!cookie) {
133 + throw new Error('授权失败:没有获取到有效的会话信息')
134 + }
84 135
85 - Taro.setStorageSync('sessionid', cookie) 136 + // 写入本地缓存:后续请求会从缓存取 sessionid 并带到请求头
137 + Taro.setStorageSync('sessionid', cookie)
86 138
87 - if (request?.defaults?.headers) { 139 + return {
88 - request.defaults.headers.cookie = cookie 140 + ...response.data,
141 + cookie,
142 + }
143 + } finally {
144 + if (show_loading) {
145 + Taro.hideLoading()
146 + }
89 } 147 }
148 + })().finally(() => {
149 + auth_promise = null
150 + })
151 +
152 + return auth_promise
153 +}
154 +
155 +const do_silent_auth = async (show_loading) => {
156 + // 已有 sessionid 时直接视为已授权
157 + if (!needAuth()) {
158 + return { code: 1, msg: '已授权' }
159 + }
90 160
91 - if (on_success) on_success(response.data) 161 + // 需要授权时,走刷新会话逻辑
92 - return response.data 162 + return await refreshSession({ show_loading })
163 +}
164 +
165 +/**
166 + * 静默授权:用于启动阶段/分享页/授权页发起授权
167 + * - 与 refreshSession 共用 auth_promise,避免并发重复调用
168 + * @param {Function} on_success 成功回调
169 + * @param {Function} on_error 失败回调(入参为错误文案)
170 + * @param {object} options 可选项
171 + * @param {boolean} options.show_loading 是否展示 loading,默认 true
172 + * @returns {Promise<any>} 授权结果
173 + */
174 +export const silentAuth = async (on_success, on_error, options) => {
175 + const show_loading = options?.show_loading !== false
176 +
177 + try {
178 + // 未有授权进行中时才发起一次授权,并复用 Promise
179 + if (!auth_promise) {
180 + auth_promise = do_silent_auth(show_loading).finally(() => {
181 + auth_promise = null
182 + })
183 + }
184 + const result = await auth_promise
185 + if (on_success) on_success(result)
186 + return result
93 } catch (error) { 187 } catch (error) {
94 - Taro.hideLoading()
95 const error_msg = error?.message || '授权失败,请稍后重试' 188 const error_msg = error?.message || '授权失败,请稍后重试'
96 if (on_error) on_error(error_msg) 189 if (on_error) on_error(error_msg)
97 throw error 190 throw error
98 } 191 }
99 } 192 }
100 193
194 +/**
195 + * 跳转到授权页(降级方案)
196 + * - 会先保存回跳路径(默认当前页),授权成功后在 auth 页回跳
197 + * @param {string} return_path 指定回跳路径
198 + * @returns {void}
199 + */
101 export const navigateToAuth = (return_path) => { 200 export const navigateToAuth = (return_path) => {
102 if (return_path) { 201 if (return_path) {
103 saveCurrentPagePath(return_path) 202 saveCurrentPagePath(return_path)
...@@ -105,11 +204,26 @@ export const navigateToAuth = (return_path) => { ...@@ -105,11 +204,26 @@ export const navigateToAuth = (return_path) => {
105 saveCurrentPagePath() 204 saveCurrentPagePath()
106 } 205 }
107 206
108 - Taro.navigateTo({ 207 + const pages = Taro.getCurrentPages()
109 - url: '/pages/auth/index', 208 + const current_page = pages[pages.length - 1]
209 + const current_route = current_page?.route
210 + if (current_route === 'pages/auth/index') {
211 + return
212 + }
213 +
214 + // navigateTo 失败时(例如页面栈满),降级为 redirectTo
215 + Taro.navigateTo({ url: '/pages/auth/index' }).catch(() => {
216 + return Taro.redirectTo({ url: '/pages/auth/index' })
110 }) 217 })
111 } 218 }
112 219
220 +/**
221 + * 授权成功后回跳到来源页
222 + * - 优先使用 routerStore 里保存的路径
223 + * - 失败降级:redirectTo -> reLaunch
224 + * @param {string} default_path 未保存来源页时的默认回跳路径
225 + * @returns {Promise<void>}
226 + */
113 export const returnToOriginalPage = async (default_path = '/pages/index/index') => { 227 export const returnToOriginalPage = async (default_path = '/pages/index/index') => {
114 const router = routerStore() 228 const router = routerStore()
115 const saved_path = router.url 229 const saved_path = router.url
...@@ -147,10 +261,23 @@ export const returnToOriginalPage = async (default_path = '/pages/index/index') ...@@ -147,10 +261,23 @@ export const returnToOriginalPage = async (default_path = '/pages/index/index')
147 } 261 }
148 } 262 }
149 263
264 +/**
265 + * 判断是否来自分享场景
266 + * @param {object} options 页面 options
267 + * @returns {boolean}
268 + */
150 export const isFromShare = (options) => { 269 export const isFromShare = (options) => {
151 return options && (options.from_share === '1' || options.scene) 270 return options && (options.from_share === '1' || options.scene)
152 } 271 }
153 272
273 +/**
274 + * 分享页进入时的授权处理
275 + * - 来自分享且未授权:保存当前页路径,授权成功后回跳
276 + * - 授权失败:返回 false,由调用方决定是否继续降级处理
277 + * @param {object} options 页面 options
278 + * @param {Function} callback 授权成功后的继续逻辑
279 + * @returns {Promise<boolean>} true=已处理且可继续,false=授权失败
280 + */
154 export const handleSharePageAuth = async (options, callback) => { 281 export const handleSharePageAuth = async (options, callback) => {
155 if (!needAuth()) { 282 if (!needAuth()) {
156 if (typeof callback === 'function') callback() 283 if (typeof callback === 'function') callback()
...@@ -167,16 +294,21 @@ export const handleSharePageAuth = async (options, callback) => { ...@@ -167,16 +294,21 @@ export const handleSharePageAuth = async (options, callback) => {
167 if (typeof callback === 'function') callback() 294 if (typeof callback === 'function') callback()
168 }, 295 },
169 () => { 296 () => {
170 - Taro.navigateTo({ url: '/pages/auth/index' }) 297 + // Taro.navigateTo({ url: '/pages/auth/index' })
171 } 298 }
172 ) 299 )
173 return true 300 return true
174 } catch (error) { 301 } catch (error) {
175 - Taro.navigateTo({ url: '/pages/auth/index' }) 302 + // Taro.navigateTo({ url: '/pages/auth/index' })
176 return false 303 return false
177 } 304 }
178 } 305 }
179 306
307 +/**
308 + * 为路径追加分享标记
309 + * @param {string} path 原路径
310 + * @returns {string} 追加后的路径
311 + */
180 export const addShareFlag = (path) => { 312 export const addShareFlag = (path) => {
181 const separator = path.includes('?') ? '&' : '?' 313 const separator = path.includes('?') ? '&' : '?'
182 return `${path}${separator}from_share=1` 314 return `${path}${separator}from_share=1`
......
1 /* 1 /*
2 * @Date: 2022-09-19 14:11:06 2 * @Date: 2022-09-19 14:11:06
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2026-01-07 21:44:54 4 + * @LastEditTime: 2026-01-08 16:27:50
5 * @FilePath: /xyxBooking-weapp/src/utils/request.js 5 * @FilePath: /xyxBooking-weapp/src/utils/request.js
6 * @Description: 简单axios封装,后续按实际处理 6 * @Description: 简单axios封装,后续按实际处理
7 */ 7 */
...@@ -10,7 +10,7 @@ import axios from 'axios-miniprogram'; ...@@ -10,7 +10,7 @@ import axios from 'axios-miniprogram';
10 import Taro from '@tarojs/taro' 10 import Taro from '@tarojs/taro'
11 // import qs from 'qs' 11 // import qs from 'qs'
12 // import { strExist } from './tools' 12 // import { strExist } from './tools'
13 -import { routerStore } from '@/stores/router' 13 +import { refreshSession, saveCurrentPagePath, navigateToAuth } from './authRedirect'
14 14
15 // import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress'; 15 // import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress';
16 // import store from '@/store' 16 // import store from '@/store'
...@@ -30,25 +30,6 @@ const getSessionId = () => { ...@@ -30,25 +30,6 @@ const getSessionId = () => {
30 } 30 }
31 }; 31 };
32 32
33 -const getCurrentPageFullPath = () => {
34 - try {
35 - const pages = Taro.getCurrentPages()
36 - if (!pages || pages.length === 0) return ''
37 -
38 - const currentPage = pages[pages.length - 1]
39 - const route = currentPage.route
40 - const options = currentPage.options || {}
41 -
42 - const queryParams = Object.keys(options)
43 - .map(key => `${key}=${encodeURIComponent(options[key])}`)
44 - .join('&')
45 -
46 - return queryParams ? `${route}?${queryParams}` : route
47 - } catch (error) {
48 - return ''
49 - }
50 -}
51 -
52 // const isPlainObject = (value) => { 33 // const isPlainObject = (value) => {
53 // if (value === null || typeof value !== 'object') return false 34 // if (value === null || typeof value !== 'object') return false
54 // return Object.prototype.toString.call(value) === '[object Object]' 35 // return Object.prototype.toString.call(value) === '[object Object]'
...@@ -114,33 +95,49 @@ service.interceptors.request.use( ...@@ -114,33 +95,49 @@ service.interceptors.request.use(
114 // response interceptor 95 // response interceptor
115 service.interceptors.response.use( 96 service.interceptors.response.use(
116 /** 97 /**
117 - * If you want to get http information such as headers or status 98 + * 响应拦截器说明
118 - * Please return response => response 99 + * - 这里统一处理后端自定义 code(例如 401 未授权)
100 + * - 如需拿到 headers/status 等原始信息,直接返回 response 即可
119 */ 101 */
120 - 102 + async response => {
121 - /**
122 - * Determine the request status by custom code
123 - * Here is just an example
124 - * You can also judge the status by HTTP Status Code
125 - */
126 - response => {
127 const res = response.data 103 const res = response.data
128 104
129 // 401 未授权处理 105 // 401 未授权处理
130 if (res.code === 401) { 106 if (res.code === 401) {
131 - // 跳转到授权页 107 + const config = response?.config || {}
132 - // 避免死循环,如果已经在 auth 页则不跳 108 + /**
109 + * 避免死循环/重复授权:
110 + * - __is_auth_request:本次请求就是“换取会话”的授权请求,不应再次触发刷新
111 + * - __is_retry:本次请求是 401 后的重试请求,如果仍 401,不再继续重试
112 + */
113 + if (config.__is_auth_request || config.__is_retry) {
114 + return response
115 + }
116 +
117 + /**
118 + * 记录来源页:用于授权成功后回跳
119 + * - 避免死循环:如果已经在 auth 页则不重复记录/跳转
120 + */
133 const pages = Taro.getCurrentPages(); 121 const pages = Taro.getCurrentPages();
134 const currentPage = pages[pages.length - 1]; 122 const currentPage = pages[pages.length - 1];
135 if (currentPage && currentPage.route !== 'pages/auth/index') { 123 if (currentPage && currentPage.route !== 'pages/auth/index') {
136 - const router = routerStore() 124 + saveCurrentPagePath()
137 - const currentPath = getCurrentPageFullPath() 125 + }
138 - if (currentPath) { 126 +
139 - router.add(currentPath) 127 + try {
140 - } 128 + // 优先走静默续期:成功后重放原请求
141 - // Taro.navigateTo({ url: '/pages/auth/index' }); 129 + await refreshSession()
130 + const retry_config = { ...config, __is_retry: true }
131 + return await service(retry_config)
132 + } catch (error) {
133 + // 静默续期失败:降级跳转到授权页(由授权页完成授权并回跳)
134 + const pages_retry = Taro.getCurrentPages();
135 + const current_page_retry = pages_retry[pages_retry.length - 1];
136 + if (current_page_retry && current_page_retry.route !== 'pages/auth/index') {
137 + navigateToAuth()
138 + }
139 + return response
142 } 140 }
143 - return response; // 返回 response 以便业务代码处理(或者这里 reject)
144 } 141 }
145 142
146 if (['预约ID不存在'].includes(res.msg)) { 143 if (['预约ID不存在'].includes(res.msg)) {
......