hookehuyr

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

- 将授权相关功能抽离到独立模块 authRedirect.js
- 实现静默续期和请求重放机制
- 优化授权失败后的降级处理
- 统一管理页面跳转和路径保存逻辑
- 移除未使用的代码和组件
......@@ -10,7 +10,6 @@ declare module 'vue' {
NutButton: typeof import('@nutui/nutui-taro')['Button']
NutCheckbox: typeof import('@nutui/nutui-taro')['Checkbox']
NutCheckboxGroup: typeof import('@nutui/nutui-taro')['CheckboxGroup']
NutConfigProvider: typeof import('@nutui/nutui-taro')['ConfigProvider']
NutDatePicker: typeof import('@nutui/nutui-taro')['DatePicker']
NutInput: typeof import('@nutui/nutui-taro')['Input']
NutPicker: typeof import('@nutui/nutui-taro')['Picker']
......
......@@ -45,29 +45,19 @@
</template>
<script setup>
import { ref } from 'vue'
import Taro, { useDidShow, useShareAppMessage } from '@tarojs/taro'
import Taro, { useShareAppMessage } from '@tarojs/taro'
// import { showSuccessToast, showFailToast } from 'vant'; // NutUI 或 Taro API
import { billListAPI } from '@/api/index';
import { useGo } from '@/hooks/useGo'
import icon_1 from '@/assets/images/立即预约@2x.png'
import icon_2 from '@/assets/images/预约记录@2x.png'
import icon_3 from '@/assets/images/首页02@2x.png'
import icon_4 from '@/assets/images/二维码icon.png'
import icon_5 from '@/assets/images/我的01@2x.png'
import icon_6 from '@/assets/images/luru@2x.png'
const go = useGo();
const toBooking = () => { // 跳转到预约须知
go('/notice');
}
const toRecord = () => { // 跳转到预约记录
go('/bookingList');
}
const toSearch = () => { // 跳转到寺院录入
go('/search');
}
const toCode = () => { // 跳转到预约码
Taro.redirectTo({
url: '/pages/bookingCode/index'
......@@ -79,12 +69,6 @@ const toMy = () => { // 跳转到我的
})
}
useDidShow(async () => {
// TAG: 触发授权页面 (检查 session 或调用接口触发 401)
// 小程序中,request.js 拦截器会处理 401 跳转
await billListAPI({ page: 1, row_num: 1 });
});
useShareAppMessage(() => {
return {
title: '西园寺预约',
......
import Taro from '@tarojs/taro'
import { routerStore } from '@/stores/router'
import request from '@/utils/request'
import BASE_URL from './config'
/**
* 授权与回跳相关工具
* - 统一管理:保存来源页、静默授权、跳转授权页、授权后回跳
* - 约定:sessionid 存在于本地缓存 key 为 sessionid
* - 说明:refreshSession/silentAuth 使用单例 Promise,避免并发重复授权
*/
/**
* 获取当前页完整路径(含 query)
* @returns {string} 当前页路径,示例:pages/index/index?a=1
*/
export const getCurrentPageFullPath = () => {
const pages = getCurrentPages()
const pages = Taro.getCurrentPages()
if (!pages || pages.length === 0) return ''
const current_page = pages[pages.length - 1]
......@@ -17,12 +28,21 @@ export const getCurrentPageFullPath = () => {
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=需要授权,false=已存在 sessionid
*/
export const needAuth = () => {
try {
const sessionid = Taro.getStorageSync('sessionid')
......@@ -33,71 +53,150 @@ export const needAuth = () => {
}
}
export const silentAuth = async (on_success, on_error) => {
try {
if (!needAuth()) {
const result = { code: 1, msg: '已授权' }
if (on_success) on_success(result)
return result
}
let auth_promise = null
Taro.showLoading({
title: '加载中...',
mask: true,
})
/**
* 刷新会话:通过 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
const login_result = await new Promise((resolve, reject) => {
Taro.login({
success: resolve,
fail: reject,
// 已有授权进行中时,直接复用同一个 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失败')
}
if (!login_result || !login_result.code) {
throw new Error('获取微信登录code失败')
}
const response = await request.post('/srv/?a=openid', {
code: login_result.code,
})
const request_data = {
code: login_result.code,
}
Taro.hideLoading()
// 开发环境可按需手动传 openid(仅用于本地联调)
if (process.env.NODE_ENV === 'development') {
// request_data.openid = 'h-008';
// 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 (!response?.data || response.data.code !== 1) {
const error_msg = response?.data?.msg || '授权失败'
if (on_error) on_error(error_msg)
throw new Error(error_msg)
}
if (process.env.NODE_ENV === 'production') {
// request_data.openid = 'h-013';
}
let cookie =
(response.cookies && response.cookies[0]) ||
response.header?.['Set-Cookie'] ||
response.header?.['set-cookie']
// 换取后端会话(服务端通过 Set-Cookie 返回会话信息)
const response = await Taro.request({
url: `${BASE_URL}/srv/?a=openid&f=reserve&client_name=${encodeURIComponent('智慧西园寺')}`,
method: 'POST',
data: request_data,
})
if (Array.isArray(cookie)) cookie = cookie[0]
if (!response?.data || response.data.code !== 1) {
throw new Error(response?.data?.msg || '授权失败')
}
if (!cookie) {
const error_msg = '授权失败:没有获取到有效的会话信息'
if (on_error) on_error(error_msg)
throw new Error(error_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('授权失败:没有获取到有效的会话信息')
}
Taro.setStorageSync('sessionid', cookie)
// 写入本地缓存:后续请求会从缓存取 sessionid 并带到请求头
Taro.setStorageSync('sessionid', cookie)
if (request?.defaults?.headers) {
request.defaults.headers.cookie = 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 (!needAuth()) {
return { code: 1, msg: '已授权' }
}
if (on_success) on_success(response.data)
return response.data
// 需要授权时,走刷新会话逻辑
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) {
Taro.hideLoading()
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)
......@@ -105,11 +204,26 @@ export const navigateToAuth = (return_path) => {
saveCurrentPagePath()
}
Taro.navigateTo({
url: '/pages/auth/index',
const pages = Taro.getCurrentPages()
const current_page = pages[pages.length - 1]
const current_route = current_page?.route
if (current_route === 'pages/auth/index') {
return
}
// 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
......@@ -147,10 +261,23 @@ export const returnToOriginalPage = async (default_path = '/pages/index/index')
}
}
/**
* 判断是否来自分享场景
* @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 (!needAuth()) {
if (typeof callback === 'function') callback()
......@@ -167,16 +294,21 @@ export const handleSharePageAuth = async (options, callback) => {
if (typeof callback === 'function') callback()
},
() => {
Taro.navigateTo({ url: '/pages/auth/index' })
// Taro.navigateTo({ url: '/pages/auth/index' })
}
)
return true
} catch (error) {
Taro.navigateTo({ url: '/pages/auth/index' })
// 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`
......
/*
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-07 21:44:54
* @LastEditTime: 2026-01-08 16:27:50
* @FilePath: /xyxBooking-weapp/src/utils/request.js
* @Description: 简单axios封装,后续按实际处理
*/
......@@ -10,7 +10,7 @@ import axios from 'axios-miniprogram';
import Taro from '@tarojs/taro'
// import qs from 'qs'
// import { strExist } from './tools'
import { routerStore } from '@/stores/router'
import { refreshSession, saveCurrentPagePath, navigateToAuth } from './authRedirect'
// import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress';
// import store from '@/store'
......@@ -30,25 +30,6 @@ const getSessionId = () => {
}
};
const getCurrentPageFullPath = () => {
try {
const pages = Taro.getCurrentPages()
if (!pages || pages.length === 0) return ''
const currentPage = pages[pages.length - 1]
const route = currentPage.route
const options = currentPage.options || {}
const queryParams = Object.keys(options)
.map(key => `${key}=${encodeURIComponent(options[key])}`)
.join('&')
return queryParams ? `${route}?${queryParams}` : route
} catch (error) {
return ''
}
}
// const isPlainObject = (value) => {
// if (value === null || typeof value !== 'object') return false
// return Object.prototype.toString.call(value) === '[object Object]'
......@@ -114,33 +95,49 @@ service.interceptors.request.use(
// response interceptor
service.interceptors.response.use(
/**
* If you want to get http information such as headers or status
* Please return response => response
* 响应拦截器说明
* - 这里统一处理后端自定义 code(例如 401 未授权)
* - 如需拿到 headers/status 等原始信息,直接返回 response 即可
*/
/**
* Determine the request status by custom code
* Here is just an example
* You can also judge the status by HTTP Status Code
*/
response => {
async response => {
const res = response.data
// 401 未授权处理
if (res.code === 401) {
// 跳转到授权页
// 避免死循环,如果已经在 auth 页则不跳
const config = response?.config || {}
/**
* 避免死循环/重复授权:
* - __is_auth_request:本次请求就是“换取会话”的授权请求,不应再次触发刷新
* - __is_retry:本次请求是 401 后的重试请求,如果仍 401,不再继续重试
*/
if (config.__is_auth_request || config.__is_retry) {
return response
}
/**
* 记录来源页:用于授权成功后回跳
* - 避免死循环:如果已经在 auth 页则不重复记录/跳转
*/
const pages = Taro.getCurrentPages();
const currentPage = pages[pages.length - 1];
if (currentPage && currentPage.route !== 'pages/auth/index') {
const router = routerStore()
const currentPath = getCurrentPageFullPath()
if (currentPath) {
router.add(currentPath)
}
// Taro.navigateTo({ url: '/pages/auth/index' });
saveCurrentPagePath()
}
try {
// 优先走静默续期:成功后重放原请求
await refreshSession()
const retry_config = { ...config, __is_retry: true }
return await service(retry_config)
} catch (error) {
// 静默续期失败:降级跳转到授权页(由授权页完成授权并回跳)
const pages_retry = Taro.getCurrentPages();
const current_page_retry = pages_retry[pages_retry.length - 1];
if (current_page_retry && current_page_retry.route !== 'pages/auth/index') {
navigateToAuth()
}
return response
}
return response; // 返回 response 以便业务代码处理(或者这里 reject)
}
if (['预约ID不存在'].includes(res.msg)) {
......