hookehuyr

refactor(auth): 重构鉴权架构,分离微信授权和用户登录

## 核心变更

### 架构调整
- 移除 sessionid 前端管理逻辑,改由后端自动处理(cookie)
- 移除接口白名单机制,所有接口直接发送
- 简化 401 处理,统一跳转登录页
- 分离微信授权(openid)和用户登录两个独立概念

### 文件变更

**新增文件:**
- src/utils/openid.js - 微信授权管理(wx.login、miniProgramAuthAPI)
- src/stores/user.js - 用户状态管理(Pinia)
- docs/specs/2026-02-02-auth-refactoring.md - 鉴权重构规划文档

**修改文件:**
- src/app.js - 启动时检查登录状态,移除旧授权逻辑
- src/utils/request.js - 简化拦截器,移除白名单和 sessionid
- src/pages/login/index.vue - 使用新的登录 API(uuid、password)
- src/app.config.js - 移除 pages/auth/index 引用
- src/pages/mine/index.vue - 适配新的鉴权逻辑
- src/utils/config.js - 配置调整
- src/api/user.js - API 文档更新
- .eslintrc.cjs - ESLint 配置调整
- .claude/settings.local.json - Claude 设置更新

**删除文件:**
- src/utils/authRedirect.js - 移除旧的授权重定向逻辑
- src/pages/auth/* - 移除旧的授权页面

## 新的鉴权流程

1. 小程序启动 → 确保 openid 已授权(wx.login)
2. 如果 miniProgramAuthAPI 返回 user → 自动登录
3. 如果未登录 → 不跳转,允许用户浏览小程序
4. 用户操作触发接口返回 401 → 跳转登录页

## 优势

- ✅ 简化前端逻辑,不需要维护白名单
- ✅ sessionid 由后端统一管理,更安全
- ✅ 用户体验更好,启动时不强制登录
- ✅ 代码更清晰,职责分离

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
......@@ -60,5 +60,9 @@
"Bash(do if [ -d \"/Users/huyirui/program/itomix/git/manulife-weapp/docs/$dir\" ])",
"Bash(then echo \"=== $dir/ ===\" ls -1 /Users/huyirui/program/itomix/git/manulife-weapp/docs/$dir/*.md)"
]
}
},
"enabledMcpjsonServers": [
"chrome-devtools"
],
"enableAllProjectMcpServers": true
}
......
......@@ -7,7 +7,6 @@ module.exports = {
globals: {
definePageConfig: 'readonly',
getCurrentPages: 'readonly',
ENABLE_AUTH_MODE: 'readonly',
wx: 'readonly'
},
extends: ['taro'],
......
This diff is collapsed. Click to expand it.
......@@ -3,6 +3,7 @@ import { fn, fetch } from '@/api/fn';
const Api = {
GetProfile: '/srv/?a=user&t=get_profile',
Login: '/srv/?a=user&t=login',
LoginStatus: '/srv/?a=user&t=login_status',
Logout: '/srv/?a=user&t=logout',
UpdateProfile: '/srv/?a=user&t=update_profile',
}
......@@ -40,6 +41,21 @@ export const getProfileAPI = (params) => fn(fetch.get(Api.GetProfile, params));
export const loginAPI = (params) => fn(fetch.post(Api.Login, params));
/**
* @description 查询登录状态
* @remark
* @param {Object} params 请求参数
* @returns {Promise<{
* code: number; // 状态码
* msg: string; // 消息
* data: {
* is_login: boolean; // true=登录,false=未登录
* is_openid: boolean; // true=已授权,false=未授权
* };
* }>}
*/
export const loginStatusAPI = (params) => fn(fetch.get(Api.LoginStatus, params));
/**
* @description 退出登录并解绑openid
* @remark
* @param {Object} params 请求参数
......
......@@ -11,7 +11,6 @@ const pages = [
'pages/webview/index',
'pages/document-preview/index',
'pages/document-demo/index',
'pages/auth/index',
'pages/onboarding/index',
'pages/family-office/index',
'pages/knowledge-base/index',
......
/*
* @Date: 2025-06-28 10:33:00
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-31 19:52:31
* @LastEditTime: 2026-02-02 18:00:00
* @FilePath: /manulife-weapp/src/app.js
* @Description: 应用入口文件
*/
......@@ -9,45 +9,32 @@ import { createApp } from 'vue'
import { createPinia } from 'pinia'
import './utils/polyfill'
import './app.less'
import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect'
import { useUserStore } from '@/stores/user'
const App = createApp({
// 对应 onLaunch
async onLaunch(options) {
const path = options?.path || ''
const query = options?.query || {}
console.log('小程序启动', options)
const query_string = Object.keys(query)
.map((key) => `${key}=${encodeURIComponent(query[key])}`)
.join('&')
const full_path = query_string ? `${path}?${query_string}` : path
// 保存当前页面路径,用于授权后跳转回原页面
if (full_path) {
saveCurrentPagePath(full_path)
}
// 如果用户已授权,则不需要额外操作
if (hasAuth()) {
return
}
if (path === 'pages/auth/index') return
// 获取用户 store
const userStore = useUserStore()
// 检查登录状态
// - 如果 is_openid=false,会自动调用 wx.login 授权
// - 如果授权后返回 user,说明已自动登录
// - 如果 is_login=false,会跳转到登录页
try {
// 尝试静默授权
await silentAuth()
await userStore.checkLoginStatus()
} catch (error) {
console.error('静默授权失败:', error)
// 授权失败则跳转至授权页面
navigateToAuth(full_path || undefined)
console.error('启动时检查登录状态失败:', error)
// 即使失败也继续,让用户可以正常使用小程序
}
return
},
onShow() {
// 页面显示时的逻辑
},
});
})
App.use(createPinia())
......
export default {
navigationBarTitleText: '授权页',
usingComponents: {
},
}
<!--
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-13 00:18:41
* @FilePath: /xyxBooking-weapp/src/pages/auth/index.vue
* @Description: 授权页
-->
<template>
<view class="auth-page">
<view class="loading">
<view>正在授权登录...</view>
</view>
</view>
</template>
<script setup>
import Taro, { useDidShow } from '@tarojs/taro'
import { silentAuth, returnToOriginalPage } from '@/utils/authRedirect'
let last_try_at = 0
let has_shown_fail_modal = false
let has_failed = false
useDidShow(() => {
if (has_failed) return
const now = Date.now()
if (now - last_try_at < 1200) return
last_try_at = now
/**
* 尝试静默授权
* - 授权成功后回跳到来源页
* - 授权失败则跳转至授权页面
*/
silentAuth()
.then(() => returnToOriginalPage())
.catch(async (error) => {
has_failed = true
if (has_shown_fail_modal) return
has_shown_fail_modal = true
await Taro.showModal({
title: '提示',
content: error?.message || '授权失败,请稍后再尝试',
showCancel: false,
confirmText: '我知道了',
})
})
})
</script>
<style lang="less">
.auth-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.loading {
text-align: center;
color: #999;
}
}
</style>
......@@ -22,13 +22,13 @@
<!-- Form -->
<view class="space-y-[48rpx]">
<!-- Email -->
<!-- Account -->
<view class="border-b border-gray-200 pb-[16rpx]">
<view class="text-[28rpx] text-gray-900 font-medium mb-[16rpx]">邮箱</view>
<view class="text-[28rpx] text-gray-900 font-medium mb-[16rpx]">账号</view>
<input
v-model="form.email"
v-model="form.uuid"
type="text"
placeholder="请输入工作邮箱"
placeholder="请输入账号"
placeholder-class="text-gray-300"
class="w-full text-[32rpx] text-gray-900 h-[80rpx]"
/>
......@@ -68,38 +68,23 @@
<script setup>
import { reactive } from 'vue'
import Taro from '@tarojs/taro'
import { useGo } from '@/hooks/useGo'
import { useUserStore } from '@/stores/user'
import NavHeader from '@/components/NavHeader.vue'
const go = useGo()
const userStore = useUserStore()
const form = reactive({
email: '',
uuid: '',
password: ''
})
/**
* 验证邮箱格式
* @param {string} email - 邮箱地址
* @returns {boolean} 是否有效
*/
const isValidEmail = (email) => {
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
return emailRegex.test(email)
}
/**
* Handle login action
*/
const handleLogin = () => {
// 验证邮箱
if (!form.email) {
Taro.showToast({ title: '请输入邮箱', icon: 'none' })
return
}
if (!isValidEmail(form.email)) {
Taro.showToast({ title: '请输入有效的邮箱地址', icon: 'none' })
const handleLogin = async () => {
// 验证账号
if (!form.uuid) {
Taro.showToast({ title: '请输入账号', icon: 'none' })
return
}
......@@ -114,16 +99,37 @@ const handleLogin = () => {
return
}
// Mock login success
// 调用登录接口
Taro.showLoading({ title: '登录中...', mask: true })
setTimeout(() => {
try {
const result = await userStore.login({
uuid: form.uuid,
password: form.password
})
if (result.success) {
Taro.hideLoading()
Taro.showToast({ title: '登录成功', icon: 'success' })
// 延迟后跳转到首页
setTimeout(() => {
// Redirect to home or previous page
Taro.reLaunch({ url: '/pages/index/index' })
}, 1500)
}, 1000)
} else {
Taro.hideLoading()
Taro.showToast({
title: result.message || '登录失败',
icon: 'none'
})
}
} catch (error) {
Taro.hideLoading()
Taro.showToast({
title: error.message || '登录失败,请重试',
icon: 'none'
})
}
}
</script>
......
......@@ -12,13 +12,13 @@
>
<!-- Avatar -->
<view class="w-[160rpx] h-[160rpx] rounded-full overflow-hidden border-2 border-white shadow-sm shrink-0">
<img class="w-full h-full object-cover" :src="defaultAvatar" />
<img class="w-full h-full object-cover" :src="userInfo?.avatar_url || defaultAvatar" />
</view>
<!-- Info -->
<view class="ml-[32rpx] flex-1 flex flex-col justify-center">
<text class="text-[36rpx] font-bold text-gray-800 mb-[8rpx]">张三</text>
<text class="text-[28rpx] text-gray-500 mb-[4rpx]">工号: EMP2026001</text>
<text class="text-[36rpx] font-bold text-gray-800 mb-[8rpx]">{{ userInfo?.name || '加载中...' }}</text>
<text class="text-[28rpx] text-gray-500 mb-[4rpx]">ID: {{ userInfo?.id || '--' }}</text>
<text class="text-[24rpx] text-gray-400">点击修改头像</text>
</view>
......@@ -68,14 +68,54 @@
</template>
<script setup>
import { ref } from 'vue'
import { useGo } from '@/hooks/useGo'
import { mainStore } from '@/stores/main'
import IconFont from '@/components/IconFont.vue'
import TabBar from '@/components/TabBar.vue'
import NavHeader from '@/components/NavHeader.vue'
import Taro from '@tarojs/taro'
import { useLoad } from '@tarojs/taro'
import { getProfileAPI } from '@/api/user'
import defaultAvatar from '@/assets/images/icon/avatar.svg'
const go = useGo()
const store = mainStore()
/**
* @description 用户信息(响应式)
* @type {import('vue').Ref<{id?: number, name?: string, avatar_url?: string}|null>}
*/
const userInfo = ref(null)
/**
* @description 获取用户个人信息
* @description 进入页面时调用,401 自动跳转登录页(由 request.js 拦截器处理)
* @returns {Promise<void>}
*/
const fetchUserProfile = async () => {
try {
const res = await getProfileAPI()
if (res.code === 1 && res.data?.user) {
// 更新响应式数据
userInfo.value = res.data.user
// 更新全局状态
store.changeUserInfo(res.data.user)
} else {
// 接口返回失败(非 401,因为 401 已被 request.js 拦截器处理)
console.warn('获取用户信息失败:', res.msg)
}
} catch (err) {
console.error('获取用户信息异常:', err)
}
}
/**
* @description 页面加载时获取用户信息
*/
useLoad(() => {
fetchUserProfile()
})
const menuItems = [
{ title: '我的计划书', icon: 'order', path: '/pages/plan/index' },
......
/**
* 用户状态管理
*
* @description 管理用户登录状态、用户信息等
* @module stores/user
*/
import { defineStore } from 'pinia'
import { ref } from 'vue'
import Taro from '@tarojs/taro'
import { loginStatusAPI, loginAPI, getProfileAPI, logoutAPI } from '@/api/user'
import { ensureOpenidAuthorized } from '@/utils/openid'
export const useUserStore = defineStore('user', () => {
// ========== 状态 ==========
/** 用户信息 */
const userInfo = ref(null)
/** 是否已授权(openid) */
const isOpenid = ref(false)
/** 是否已登录 */
const isLoggedIn = ref(false)
/** 加载状态 */
const loading = ref(false)
// ========== 方法 ==========
/**
* 检查登录状态
* @description 小程序启动时检查 openid 和登录状态
* - 只触发微信授权,不跳转登录页
* - 401 由接口拦截器统一处理
* @throws {Error} 检查失败时抛出错误
*
* @example
* await userStore.checkLoginStatus()
*/
async function checkLoginStatus() {
loading.value = true
try {
// 1. 确保 openid 已授权并尝试自动登录
const user = await ensureOpenidAuthorized()
if (user) {
// miniProgramAuthAPI 返回了用户信息,说明已自动登录
userInfo.value = user
isOpenid.value = true
isLoggedIn.value = true
return
}
// 2. 查询登录状态
const res = await loginStatusAPI()
if (res.code === 1) {
isOpenid.value = res.data.is_openid
isLoggedIn.value = res.data.is_login
// 3. 如果已登录,获取用户信息
if (isLoggedIn.value) {
await fetchUserInfo()
}
// 注意:这里不跳转登录页,让用户可以浏览小程序
// 当用户操作触发接口返回 401 时,会自动跳转登录页
} else {
throw new Error(res.msg || '查询登录状态失败')
}
} catch (err) {
console.error('检查登录状态失败:', err)
throw err
} finally {
loading.value = false
}
}
/**
* 获取用户信息
* @description 调用 getProfileAPI 获取用户信息
* @throws {Error} 获取失败时抛出错误
*
* @example
* await userStore.fetchUserInfo()
*/
async function fetchUserInfo() {
try {
const res = await getProfileAPI()
if (res.code === 1) {
userInfo.value = res.data.user
} else {
throw new Error(res.msg || '获取用户信息失败')
}
} catch (err) {
console.error('获取用户信息失败:', err)
throw err
}
}
/**
* 用户登录
* @description 调用 loginAPI 进行账号密码登录
* @param {Object} loginData 登录数据
* @param {string} loginData.uuid 账号
* @param {string} loginData.password 密码
* @returns {{success: boolean, message?: string}} 登录结果
*
* @example
* const result = await userStore.login({
* uuid: '13800138000',
* password: '123456'
* })
* if (result.success) {
* console.log('登录成功')
* }
*/
async function login(loginData) {
loading.value = true
try {
const res = await loginAPI(loginData)
if (res.code === 1) {
// 登录成功,获取用户信息
await fetchUserInfo()
isLoggedIn.value = true
return { success: true }
} else {
throw new Error(res.msg || '登录失败')
}
} catch (err) {
console.error('登录失败:', err)
return { success: false, message: err.message }
} finally {
loading.value = false
}
}
/**
* 用户登出
* @description 调用 logoutAPI 并清除本地状态
*
* @example
* await userStore.logout()
*/
async function logout() {
try {
// 调用登出接口
await logoutAPI()
// 清除本地状态
userInfo.value = null
isOpenid.value = false
isLoggedIn.value = false
} catch (err) {
console.error('登出失败:', err)
}
}
// ========== 返回 ==========
return {
// 状态
userInfo,
isOpenid,
isLoggedIn,
loading,
// 方法
checkLoginStatus,
fetchUserInfo,
login,
logout
}
})
This diff is collapsed. Click to expand it.
......@@ -30,11 +30,4 @@ export const REQUEST_DEFAULT_PARAMS = {
f: 'manulife', // 业务模块标识
}
/**
* @description 是否启用授权模式
* - true: 启用授权检查、自动跳转登录、401自动续期
* - false: 禁用所有授权相关功能(所有授权检查直接通过,不跳转登录页)
*/
export const ENABLE_AUTH_MODE = true // 启用授权模式
export default BASE_URL
......
/**
* 微信授权(openid)管理
*
* @description 处理小程序授权逻辑,包括 wx.login 和 miniProgramAuthAPI 调用
* @module utils/openid
*/
import Taro from '@tarojs/taro'
import { miniProgramAuthAPI } from '@/api/wechat'
import { loginStatusAPI } from '@/api/user'
/**
* 小程序授权
* @description 调用 wx.login 获取 code,由后端授权获取 openid
* @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
*
* @example
* const user = await miniProgramAuth()
* if (user) {
* console.log('已自动登录', user)
* } else {
* console.log('需要手动登录')
* }
*/
export async function miniProgramAuth() {
try {
// 1. 调用 wx.login 获取 code
const { code } = await Taro.login()
if (!code) {
throw new Error('获取微信 code 失败')
}
// 2. 调用后端授权接口
const res = await miniProgramAuthAPI({ code })
if (res.code === 1) {
return res.data.user || null
} else {
throw new Error(res.msg || '小程序授权失败')
}
} catch (err) {
console.error('小程序授权失败:', err)
throw err
}
}
/**
* 检查 openid 状态
* @description 调用 loginStatusAPI 检查 is_openid
* @returns {Promise<boolean>} 是否已授权
*
* @example
* const isOpenid = await checkOpenidStatus()
* if (!isOpenid) {
* await miniProgramAuth()
* }
*/
export async function checkOpenidStatus() {
try {
const res = await loginStatusAPI()
if (res.code === 1) {
return res.data.is_openid
} else {
return false
}
} catch (err) {
console.error('检查 openid 状态失败:', err)
return false
}
}
/**
* 确保 openid 已授权并尝试自动登录
* @description 如果未授权,则调用 wx.login 授权
* @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
*
* @example
* const user = await ensureOpenidAuthorized()
* if (user) {
* console.log('已自动登录', user)
* } else {
* console.log('已授权但未登录,需要检查登录状态')
* }
*/
export async function ensureOpenidAuthorized() {
const isOpenid = await checkOpenidStatus()
if (!isOpenid) {
// 未授权,调用 wx.login 授权
return await miniProgramAuth()
}
// 已授权,返回 null(需要检查登录状态)
return null
}
/*
* @Date: 2022-09-19 14:11:06
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2026-01-29 18:35:55
* @LastEditTime: 2026-02-02 18:00:00
* @FilePath: /manulife-weapp/src/utils/request.js
* @Description: 简单axios封装,后续按实际处理
* @Description: HTTP 请求封装(简化版)
*/
// import axios from 'axios'
import axios from 'axios-miniprogram';
import axios from 'axios-miniprogram'
import Taro from '@tarojs/taro'
// import qs from 'qs'
// import { strExist } from './tools'
import { refreshSession, saveCurrentPagePath, navigateToAuth } from './authRedirect'
import { parseQueryString } from './tools'
// import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress';
// import store from '@/store'
// import { getToken } from '@/utils/auth'
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config';
/**
* @description 获取 sessionid 的工具函数
* - sessionid 由 authRedirect.refreshSession 写入
* - 每次请求前动态读取,避免旧会话导致的 401
* @returns {string|null} sessionid或null
*/
export const getSessionId = () => {
try {
return Taro.getStorageSync("sessionid") || null;
} catch (error) {
console.error('获取sessionid失败:', error);
return null;
}
};
/**
* @description 设置 sessionid(一般不需要手动调用)
* - 正常情况下由 authRedirect.refreshSession 写入
* - 保留该方法用于极端场景的手动修复/兼容旧逻辑
* @param {string} sessionid cookie 字符串
* @returns {void} 无返回值
*/
export const setSessionId = (sessionid) => {
try {
if (!sessionid) return
Taro.setStorageSync('sessionid', sessionid)
} catch (error) {
console.error('设置sessionid失败:', error)
}
}
import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
/**
* @description 清空 sessionid(一般不需要手动调用)
* @returns {void} 无返回值
*/
export const clearSessionId = () => {
try {
Taro.removeStorageSync('sessionid')
} catch (error) {
console.error('清空sessionid失败:', error)
}
}
// const isPlainObject = (value) => {
// if (value === null || typeof value !== 'object') return false
// return Object.prototype.toString.call(value) === '[object Object]'
// }
/**
* @description axios 实例(axios-miniprogram)
* @description axios 实例
* - 统一 baseURL / timeout
* - 通过拦截器处理:默认参数、cookie 注入、401 自动续期、弱网降级
* - 通过拦截器处理:默认参数、401 跳转登录页、弱网降级
*/
const service = axios.create({
baseURL: BASE_URL, // url = base url + request url
// withCredentials: true, // send cookies when cross-domain requests
timeout: 5000, // request timeout
baseURL: BASE_URL,
timeout: 5000,
})
// service.defaults.params = {
// ...REQUEST_DEFAULT_PARAMS,
// };
let has_shown_timeout_modal = false
/**
......@@ -88,7 +27,6 @@ let has_shown_timeout_modal = false
* @param {Error} error 请求错误对象
* @returns {boolean} true=超时,false=非超时
*/
const is_timeout_error = (error) => {
const msg = String(error?.message || error?.errMsg || '')
if (error?.code === 'ECONNABORTED') return true
......@@ -134,7 +72,7 @@ const should_handle_bad_network = async (error) => {
/**
* @description 处理请求超时/弱网错误
* - 弹出弱网提示(统一文案由 uiText 管理)
* - 弹出弱网提示
* @returns {Promise<void>} 无返回值
*/
const handle_request_timeout = async () => {
......@@ -153,12 +91,9 @@ const handle_request_timeout = async () => {
}
}
// 请求拦截器:合并默认参数 / 注入 cookie
// 请求拦截器:合并默认参数
service.interceptors.request.use(
config => {
// console.warn(config)
// console.warn(store)
// 解析 URL 参数并合并
const url = config.url || ''
let url_params = {}
......@@ -174,38 +109,11 @@ service.interceptors.request.use(
...(config.params || {})
}
/**
* 动态获取 sessionid 并设置到请求头
* - 确保每个请求都带上最新的 sessionid
* - 注意:axios-miniprogram 的 headers 可能不存在,需要先兜底
*/
const sessionid = getSessionId();
if (sessionid) {
config.headers = config.headers || {}
config.headers.cookie = sessionid;
}
// 增加时间戳
if (config.method === 'get') {
config.params = { ...config.params, timestamp: (new Date()).valueOf() }
}
// if ((config.method || '').toLowerCase() === 'post') {
// const url = config.url || ''
// const headers = config.headers || {}
// const contentType = headers['content-type'] || headers['Content-Type']
// const shouldUrlEncode =
// !contentType || String(contentType).includes('application/x-www-form-urlencoded')
// if (shouldUrlEncode && !strExist(['upload.qiniup.com'], url) && isPlainObject(config.data)) {
// config.headers = {
// ...headers,
// 'content-type': 'application/x-www-form-urlencoded'
// }
// config.data = qs.stringify(config.data)
// }
// }
return config
},
error => {
......@@ -214,68 +122,44 @@ service.interceptors.request.use(
}
)
// 响应拦截器:401 自动续期 / 弱网降级
// 响应拦截器:401 跳转登录页 / 弱网降级
service.interceptors.response.use(
/**
* 响应拦截器说明
* - 这里统一处理后端自定义 code(例如 401 未授权)
* - 如需拿到 headers/status 等原始信息,直接返回 response 即可
* @description 响应成功拦截器
* - 处理 401 未授权,跳转到登录页
* - 处理其他自定义错误消息
*/
async response => {
const res = response.data
// 401 未授权处理
if (res.code === 401 && ENABLE_AUTH_MODE) {
const config = response?.config || {}
/**
* 避免死循环/重复重试:
* - __is_retry:本次请求是 401 后的重试请求,如果仍 401,不再继续重试
*/
if (config.__is_retry) {
return response
}
/**
* 记录来源页:用于授权成功后回跳
* - 避免死循环:如果已经在 auth 页则不重复记录/跳转
*/
const pages = Taro.getCurrentPages();
const currentPage = pages[pages.length - 1];
if (currentPage && currentPage.route !== '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
}
if (res.code === 401) {
// 跳转到登录页
Taro.navigateTo({
url: '/pages/login/index'
}).catch(() => {
// 如果跳转失败(如已经在登录页),则忽略
console.warn('跳转登录页失败,可能已在登录页')
})
}
// 处理特殊消息(不需要显示的错误)
if (['预约ID不存在'].includes(res.msg)) {
res.show = false;
res.show = false
}
return response
},
/**
* @description 响应失败拦截器
* - 处理网络错误、超时等
*/
async error => {
// Taro.showToast({
// title: error.message,
// icon: 'none',
// duration: 2000
// })
// 处理弱网/断网
if (await should_handle_bad_network(error)) {
handle_request_timeout()
}
return Promise.reject(error)
}
)
......