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 @@ ...@@ -60,5 +60,9 @@
60 "Bash(do if [ -d \"/Users/huyirui/program/itomix/git/manulife-weapp/docs/$dir\" ])", 60 "Bash(do if [ -d \"/Users/huyirui/program/itomix/git/manulife-weapp/docs/$dir\" ])",
61 "Bash(then echo \"=== $dir/ ===\" ls -1 /Users/huyirui/program/itomix/git/manulife-weapp/docs/$dir/*.md)" 61 "Bash(then echo \"=== $dir/ ===\" ls -1 /Users/huyirui/program/itomix/git/manulife-weapp/docs/$dir/*.md)"
62 ] 62 ]
63 - } 63 + },
64 + "enabledMcpjsonServers": [
65 + "chrome-devtools"
66 + ],
67 + "enableAllProjectMcpServers": true
64 } 68 }
......
...@@ -7,7 +7,6 @@ module.exports = { ...@@ -7,7 +7,6 @@ module.exports = {
7 globals: { 7 globals: {
8 definePageConfig: 'readonly', 8 definePageConfig: 'readonly',
9 getCurrentPages: 'readonly', 9 getCurrentPages: 'readonly',
10 - ENABLE_AUTH_MODE: 'readonly',
11 wx: 'readonly' 10 wx: 'readonly'
12 }, 11 },
13 extends: ['taro'], 12 extends: ['taro'],
......
This diff is collapsed. Click to expand it.
...@@ -3,6 +3,7 @@ import { fn, fetch } from '@/api/fn'; ...@@ -3,6 +3,7 @@ import { fn, fetch } from '@/api/fn';
3 const Api = { 3 const Api = {
4 GetProfile: '/srv/?a=user&t=get_profile', 4 GetProfile: '/srv/?a=user&t=get_profile',
5 Login: '/srv/?a=user&t=login', 5 Login: '/srv/?a=user&t=login',
6 + LoginStatus: '/srv/?a=user&t=login_status',
6 Logout: '/srv/?a=user&t=logout', 7 Logout: '/srv/?a=user&t=logout',
7 UpdateProfile: '/srv/?a=user&t=update_profile', 8 UpdateProfile: '/srv/?a=user&t=update_profile',
8 } 9 }
...@@ -40,6 +41,21 @@ export const getProfileAPI = (params) => fn(fetch.get(Api.GetProfile, params)); ...@@ -40,6 +41,21 @@ export const getProfileAPI = (params) => fn(fetch.get(Api.GetProfile, params));
40 export const loginAPI = (params) => fn(fetch.post(Api.Login, params)); 41 export const loginAPI = (params) => fn(fetch.post(Api.Login, params));
41 42
42 /** 43 /**
44 + * @description 查询登录状态
45 + * @remark
46 + * @param {Object} params 请求参数
47 + * @returns {Promise<{
48 + * code: number; // 状态码
49 + * msg: string; // 消息
50 + * data: {
51 + * is_login: boolean; // true=登录,false=未登录
52 + * is_openid: boolean; // true=已授权,false=未授权
53 + * };
54 + * }>}
55 + */
56 +export const loginStatusAPI = (params) => fn(fetch.get(Api.LoginStatus, params));
57 +
58 +/**
43 * @description 退出登录并解绑openid 59 * @description 退出登录并解绑openid
44 * @remark 60 * @remark
45 * @param {Object} params 请求参数 61 * @param {Object} params 请求参数
......
...@@ -11,7 +11,6 @@ const pages = [ ...@@ -11,7 +11,6 @@ const pages = [
11 'pages/webview/index', 11 'pages/webview/index',
12 'pages/document-preview/index', 12 'pages/document-preview/index',
13 'pages/document-demo/index', 13 'pages/document-demo/index',
14 - 'pages/auth/index',
15 'pages/onboarding/index', 14 'pages/onboarding/index',
16 'pages/family-office/index', 15 'pages/family-office/index',
17 'pages/knowledge-base/index', 16 'pages/knowledge-base/index',
......
1 /* 1 /*
2 * @Date: 2025-06-28 10:33:00 2 * @Date: 2025-06-28 10:33:00
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2026-01-31 19:52:31 4 + * @LastEditTime: 2026-02-02 18:00:00
5 * @FilePath: /manulife-weapp/src/app.js 5 * @FilePath: /manulife-weapp/src/app.js
6 * @Description: 应用入口文件 6 * @Description: 应用入口文件
7 */ 7 */
...@@ -9,45 +9,32 @@ import { createApp } from 'vue' ...@@ -9,45 +9,32 @@ import { createApp } from 'vue'
9 import { createPinia } from 'pinia' 9 import { createPinia } from 'pinia'
10 import './utils/polyfill' 10 import './utils/polyfill'
11 import './app.less' 11 import './app.less'
12 -import { saveCurrentPagePath, hasAuth, silentAuth, navigateToAuth } from '@/utils/authRedirect' 12 +import { useUserStore } from '@/stores/user'
13 13
14 const App = createApp({ 14 const App = createApp({
15 - // 对应 onLaunch 15 + // 对应 onLaunch
16 - async onLaunch(options) { 16 + async onLaunch(options) {
17 - const path = options?.path || '' 17 + console.log('小程序启动', options)
18 - const query = options?.query || {} 18 +
19 - 19 + // 获取用户 store
20 - const query_string = Object.keys(query) 20 + const userStore = useUserStore()
21 - .map((key) => `${key}=${encodeURIComponent(query[key])}`) 21 +
22 - .join('&') 22 + // 检查登录状态
23 - const full_path = query_string ? `${path}?${query_string}` : path 23 + // - 如果 is_openid=false,会自动调用 wx.login 授权
24 - 24 + // - 如果授权后返回 user,说明已自动登录
25 - // 保存当前页面路径,用于授权后跳转回原页面 25 + // - 如果 is_login=false,会跳转到登录页
26 - if (full_path) { 26 + try {
27 - saveCurrentPagePath(full_path) 27 + await userStore.checkLoginStatus()
28 - } 28 + } catch (error) {
29 - 29 + console.error('启动时检查登录状态失败:', error)
30 - // 如果用户已授权,则不需要额外操作 30 + // 即使失败也继续,让用户可以正常使用小程序
31 - if (hasAuth()) { 31 + }
32 - return 32 + },
33 - } 33 +
34 - 34 + onShow() {
35 - if (path === 'pages/auth/index') return 35 + // 页面显示时的逻辑
36 - 36 + },
37 - try { 37 +})
38 - // 尝试静默授权
39 - await silentAuth()
40 - } catch (error) {
41 - console.error('静默授权失败:', error)
42 - // 授权失败则跳转至授权页面
43 - navigateToAuth(full_path || undefined)
44 - }
45 -
46 - return
47 - },
48 - onShow() {
49 - },
50 -});
51 38
52 App.use(createPinia()) 39 App.use(createPinia())
53 40
......
1 -export default {
2 - navigationBarTitleText: '授权页',
3 - usingComponents: {
4 - },
5 -}
1 -.red {
2 - color: red;
3 -}
1 -<!--
2 - * @Date: 2022-09-19 14:11:06
3 - * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2026-01-13 00:18:41
5 - * @FilePath: /xyxBooking-weapp/src/pages/auth/index.vue
6 - * @Description: 授权页
7 --->
8 -<template>
9 - <view class="auth-page">
10 - <view class="loading">
11 - <view>正在授权登录...</view>
12 - </view>
13 - </view>
14 -</template>
15 -
16 -<script setup>
17 -import Taro, { useDidShow } from '@tarojs/taro'
18 -import { silentAuth, returnToOriginalPage } from '@/utils/authRedirect'
19 -
20 -let last_try_at = 0
21 -let has_shown_fail_modal = false
22 -let has_failed = false
23 -
24 -useDidShow(() => {
25 - if (has_failed) return
26 - const now = Date.now()
27 - if (now - last_try_at < 1200) return
28 - last_try_at = now
29 -
30 - /**
31 - * 尝试静默授权
32 - * - 授权成功后回跳到来源页
33 - * - 授权失败则跳转至授权页面
34 - */
35 - silentAuth()
36 - .then(() => returnToOriginalPage())
37 - .catch(async (error) => {
38 - has_failed = true
39 - if (has_shown_fail_modal) return
40 - has_shown_fail_modal = true
41 - await Taro.showModal({
42 - title: '提示',
43 - content: error?.message || '授权失败,请稍后再尝试',
44 - showCancel: false,
45 - confirmText: '我知道了',
46 - })
47 - })
48 -})
49 -</script>
50 -
51 -<style lang="less">
52 -.auth-page {
53 - min-height: 100vh;
54 - display: flex;
55 - align-items: center;
56 - justify-content: center;
57 - .loading {
58 - text-align: center;
59 - color: #999;
60 - }
61 -}
62 -</style>
...@@ -22,13 +22,13 @@ ...@@ -22,13 +22,13 @@
22 22
23 <!-- Form --> 23 <!-- Form -->
24 <view class="space-y-[48rpx]"> 24 <view class="space-y-[48rpx]">
25 - <!-- Email --> 25 + <!-- Account -->
26 <view class="border-b border-gray-200 pb-[16rpx]"> 26 <view class="border-b border-gray-200 pb-[16rpx]">
27 - <view class="text-[28rpx] text-gray-900 font-medium mb-[16rpx]">邮箱</view> 27 + <view class="text-[28rpx] text-gray-900 font-medium mb-[16rpx]">账号</view>
28 <input 28 <input
29 - v-model="form.email" 29 + v-model="form.uuid"
30 type="text" 30 type="text"
31 - placeholder="请输入工作邮箱" 31 + placeholder="请输入账号"
32 placeholder-class="text-gray-300" 32 placeholder-class="text-gray-300"
33 class="w-full text-[32rpx] text-gray-900 h-[80rpx]" 33 class="w-full text-[32rpx] text-gray-900 h-[80rpx]"
34 /> 34 />
...@@ -68,38 +68,23 @@ ...@@ -68,38 +68,23 @@
68 <script setup> 68 <script setup>
69 import { reactive } from 'vue' 69 import { reactive } from 'vue'
70 import Taro from '@tarojs/taro' 70 import Taro from '@tarojs/taro'
71 -import { useGo } from '@/hooks/useGo' 71 +import { useUserStore } from '@/stores/user'
72 import NavHeader from '@/components/NavHeader.vue' 72 import NavHeader from '@/components/NavHeader.vue'
73 73
74 -const go = useGo() 74 +const userStore = useUserStore()
75 75
76 const form = reactive({ 76 const form = reactive({
77 - email: '', 77 + uuid: '',
78 password: '' 78 password: ''
79 }) 79 })
80 80
81 /** 81 /**
82 - * 验证邮箱格式
83 - * @param {string} email - 邮箱地址
84 - * @returns {boolean} 是否有效
85 - */
86 -const isValidEmail = (email) => {
87 - const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
88 - return emailRegex.test(email)
89 -}
90 -
91 -/**
92 * Handle login action 82 * Handle login action
93 */ 83 */
94 -const handleLogin = () => { 84 +const handleLogin = async () => {
95 - // 验证邮箱 85 + // 验证账号
96 - if (!form.email) { 86 + if (!form.uuid) {
97 - Taro.showToast({ title: '请输入邮箱', icon: 'none' }) 87 + Taro.showToast({ title: '请输入账号', icon: 'none' })
98 - return
99 - }
100 -
101 - if (!isValidEmail(form.email)) {
102 - Taro.showToast({ title: '请输入有效的邮箱地址', icon: 'none' })
103 return 88 return
104 } 89 }
105 90
...@@ -114,16 +99,37 @@ const handleLogin = () => { ...@@ -114,16 +99,37 @@ const handleLogin = () => {
114 return 99 return
115 } 100 }
116 101
117 - // Mock login success 102 + // 调用登录接口
118 Taro.showLoading({ title: '登录中...', mask: true }) 103 Taro.showLoading({ title: '登录中...', mask: true })
119 - setTimeout(() => { 104 +
105 + try {
106 + const result = await userStore.login({
107 + uuid: form.uuid,
108 + password: form.password
109 + })
110 +
111 + if (result.success) {
112 + Taro.hideLoading()
113 + Taro.showToast({ title: '登录成功', icon: 'success' })
114 +
115 + // 延迟后跳转到首页
116 + setTimeout(() => {
117 + Taro.reLaunch({ url: '/pages/index/index' })
118 + }, 1500)
119 + } else {
120 + Taro.hideLoading()
121 + Taro.showToast({
122 + title: result.message || '登录失败',
123 + icon: 'none'
124 + })
125 + }
126 + } catch (error) {
120 Taro.hideLoading() 127 Taro.hideLoading()
121 - Taro.showToast({ title: '登录成功', icon: 'success' }) 128 + Taro.showToast({
122 - setTimeout(() => { 129 + title: error.message || '登录失败,请重试',
123 - // Redirect to home or previous page 130 + icon: 'none'
124 - Taro.reLaunch({ url: '/pages/index/index' }) 131 + })
125 - }, 1500) 132 + }
126 - }, 1000)
127 } 133 }
128 </script> 134 </script>
129 135
......
...@@ -12,13 +12,13 @@ ...@@ -12,13 +12,13 @@
12 > 12 >
13 <!-- Avatar --> 13 <!-- Avatar -->
14 <view class="w-[160rpx] h-[160rpx] rounded-full overflow-hidden border-2 border-white shadow-sm shrink-0"> 14 <view class="w-[160rpx] h-[160rpx] rounded-full overflow-hidden border-2 border-white shadow-sm shrink-0">
15 - <img class="w-full h-full object-cover" :src="defaultAvatar" /> 15 + <img class="w-full h-full object-cover" :src="userInfo?.avatar_url || defaultAvatar" />
16 </view> 16 </view>
17 17
18 <!-- Info --> 18 <!-- Info -->
19 <view class="ml-[32rpx] flex-1 flex flex-col justify-center"> 19 <view class="ml-[32rpx] flex-1 flex flex-col justify-center">
20 - <text class="text-[36rpx] font-bold text-gray-800 mb-[8rpx]">张三</text> 20 + <text class="text-[36rpx] font-bold text-gray-800 mb-[8rpx]">{{ userInfo?.name || '加载中...' }}</text>
21 - <text class="text-[28rpx] text-gray-500 mb-[4rpx]">工号: EMP2026001</text> 21 + <text class="text-[28rpx] text-gray-500 mb-[4rpx]">ID: {{ userInfo?.id || '--' }}</text>
22 <text class="text-[24rpx] text-gray-400">点击修改头像</text> 22 <text class="text-[24rpx] text-gray-400">点击修改头像</text>
23 </view> 23 </view>
24 24
...@@ -68,14 +68,54 @@ ...@@ -68,14 +68,54 @@
68 </template> 68 </template>
69 69
70 <script setup> 70 <script setup>
71 +import { ref } from 'vue'
71 import { useGo } from '@/hooks/useGo' 72 import { useGo } from '@/hooks/useGo'
73 +import { mainStore } from '@/stores/main'
72 import IconFont from '@/components/IconFont.vue' 74 import IconFont from '@/components/IconFont.vue'
73 import TabBar from '@/components/TabBar.vue' 75 import TabBar from '@/components/TabBar.vue'
74 import NavHeader from '@/components/NavHeader.vue' 76 import NavHeader from '@/components/NavHeader.vue'
75 import Taro from '@tarojs/taro' 77 import Taro from '@tarojs/taro'
78 +import { useLoad } from '@tarojs/taro'
79 +import { getProfileAPI } from '@/api/user'
76 import defaultAvatar from '@/assets/images/icon/avatar.svg' 80 import defaultAvatar from '@/assets/images/icon/avatar.svg'
77 81
78 const go = useGo() 82 const go = useGo()
83 +const store = mainStore()
84 +
85 +/**
86 + * @description 用户信息(响应式)
87 + * @type {import('vue').Ref<{id?: number, name?: string, avatar_url?: string}|null>}
88 + */
89 +const userInfo = ref(null)
90 +
91 +/**
92 + * @description 获取用户个人信息
93 + * @description 进入页面时调用,401 自动跳转登录页(由 request.js 拦截器处理)
94 + * @returns {Promise<void>}
95 + */
96 +const fetchUserProfile = async () => {
97 + try {
98 + const res = await getProfileAPI()
99 + if (res.code === 1 && res.data?.user) {
100 + // 更新响应式数据
101 + userInfo.value = res.data.user
102 + // 更新全局状态
103 + store.changeUserInfo(res.data.user)
104 + } else {
105 + // 接口返回失败(非 401,因为 401 已被 request.js 拦截器处理)
106 + console.warn('获取用户信息失败:', res.msg)
107 + }
108 + } catch (err) {
109 + console.error('获取用户信息异常:', err)
110 + }
111 +}
112 +
113 +/**
114 + * @description 页面加载时获取用户信息
115 + */
116 +useLoad(() => {
117 + fetchUserProfile()
118 +})
79 119
80 const menuItems = [ 120 const menuItems = [
81 { title: '我的计划书', icon: 'order', path: '/pages/plan/index' }, 121 { title: '我的计划书', icon: 'order', path: '/pages/plan/index' },
......
1 +/**
2 + * 用户状态管理
3 + *
4 + * @description 管理用户登录状态、用户信息等
5 + * @module stores/user
6 + */
7 +
8 +import { defineStore } from 'pinia'
9 +import { ref } from 'vue'
10 +import Taro from '@tarojs/taro'
11 +import { loginStatusAPI, loginAPI, getProfileAPI, logoutAPI } from '@/api/user'
12 +import { ensureOpenidAuthorized } from '@/utils/openid'
13 +
14 +export const useUserStore = defineStore('user', () => {
15 + // ========== 状态 ==========
16 + /** 用户信息 */
17 + const userInfo = ref(null)
18 +
19 + /** 是否已授权(openid) */
20 + const isOpenid = ref(false)
21 +
22 + /** 是否已登录 */
23 + const isLoggedIn = ref(false)
24 +
25 + /** 加载状态 */
26 + const loading = ref(false)
27 +
28 + // ========== 方法 ==========
29 +
30 + /**
31 + * 检查登录状态
32 + * @description 小程序启动时检查 openid 和登录状态
33 + * - 只触发微信授权,不跳转登录页
34 + * - 401 由接口拦截器统一处理
35 + * @throws {Error} 检查失败时抛出错误
36 + *
37 + * @example
38 + * await userStore.checkLoginStatus()
39 + */
40 + async function checkLoginStatus() {
41 + loading.value = true
42 +
43 + try {
44 + // 1. 确保 openid 已授权并尝试自动登录
45 + const user = await ensureOpenidAuthorized()
46 +
47 + if (user) {
48 + // miniProgramAuthAPI 返回了用户信息,说明已自动登录
49 + userInfo.value = user
50 + isOpenid.value = true
51 + isLoggedIn.value = true
52 + return
53 + }
54 +
55 + // 2. 查询登录状态
56 + const res = await loginStatusAPI()
57 +
58 + if (res.code === 1) {
59 + isOpenid.value = res.data.is_openid
60 + isLoggedIn.value = res.data.is_login
61 +
62 + // 3. 如果已登录,获取用户信息
63 + if (isLoggedIn.value) {
64 + await fetchUserInfo()
65 + }
66 + // 注意:这里不跳转登录页,让用户可以浏览小程序
67 + // 当用户操作触发接口返回 401 时,会自动跳转登录页
68 + } else {
69 + throw new Error(res.msg || '查询登录状态失败')
70 + }
71 + } catch (err) {
72 + console.error('检查登录状态失败:', err)
73 + throw err
74 + } finally {
75 + loading.value = false
76 + }
77 + }
78 +
79 + /**
80 + * 获取用户信息
81 + * @description 调用 getProfileAPI 获取用户信息
82 + * @throws {Error} 获取失败时抛出错误
83 + *
84 + * @example
85 + * await userStore.fetchUserInfo()
86 + */
87 + async function fetchUserInfo() {
88 + try {
89 + const res = await getProfileAPI()
90 +
91 + if (res.code === 1) {
92 + userInfo.value = res.data.user
93 + } else {
94 + throw new Error(res.msg || '获取用户信息失败')
95 + }
96 + } catch (err) {
97 + console.error('获取用户信息失败:', err)
98 + throw err
99 + }
100 + }
101 +
102 + /**
103 + * 用户登录
104 + * @description 调用 loginAPI 进行账号密码登录
105 + * @param {Object} loginData 登录数据
106 + * @param {string} loginData.uuid 账号
107 + * @param {string} loginData.password 密码
108 + * @returns {{success: boolean, message?: string}} 登录结果
109 + *
110 + * @example
111 + * const result = await userStore.login({
112 + * uuid: '13800138000',
113 + * password: '123456'
114 + * })
115 + * if (result.success) {
116 + * console.log('登录成功')
117 + * }
118 + */
119 + async function login(loginData) {
120 + loading.value = true
121 +
122 + try {
123 + const res = await loginAPI(loginData)
124 +
125 + if (res.code === 1) {
126 + // 登录成功,获取用户信息
127 + await fetchUserInfo()
128 +
129 + isLoggedIn.value = true
130 +
131 + return { success: true }
132 + } else {
133 + throw new Error(res.msg || '登录失败')
134 + }
135 + } catch (err) {
136 + console.error('登录失败:', err)
137 + return { success: false, message: err.message }
138 + } finally {
139 + loading.value = false
140 + }
141 + }
142 +
143 + /**
144 + * 用户登出
145 + * @description 调用 logoutAPI 并清除本地状态
146 + *
147 + * @example
148 + * await userStore.logout()
149 + */
150 + async function logout() {
151 + try {
152 + // 调用登出接口
153 + await logoutAPI()
154 +
155 + // 清除本地状态
156 + userInfo.value = null
157 + isOpenid.value = false
158 + isLoggedIn.value = false
159 + } catch (err) {
160 + console.error('登出失败:', err)
161 + }
162 + }
163 +
164 + // ========== 返回 ==========
165 + return {
166 + // 状态
167 + userInfo,
168 + isOpenid,
169 + isLoggedIn,
170 + loading,
171 +
172 + // 方法
173 + checkLoginStatus,
174 + fetchUserInfo,
175 + login,
176 + logout
177 + }
178 +})
This diff is collapsed. Click to expand it.
...@@ -30,11 +30,4 @@ export const REQUEST_DEFAULT_PARAMS = { ...@@ -30,11 +30,4 @@ export const REQUEST_DEFAULT_PARAMS = {
30 f: 'manulife', // 业务模块标识 30 f: 'manulife', // 业务模块标识
31 } 31 }
32 32
33 -/**
34 - * @description 是否启用授权模式
35 - * - true: 启用授权检查、自动跳转登录、401自动续期
36 - * - false: 禁用所有授权相关功能(所有授权检查直接通过,不跳转登录页)
37 - */
38 -export const ENABLE_AUTH_MODE = true // 启用授权模式
39 -
40 export default BASE_URL 33 export default BASE_URL
......
1 +/**
2 + * 微信授权(openid)管理
3 + *
4 + * @description 处理小程序授权逻辑,包括 wx.login 和 miniProgramAuthAPI 调用
5 + * @module utils/openid
6 + */
7 +
8 +import Taro from '@tarojs/taro'
9 +import { miniProgramAuthAPI } from '@/api/wechat'
10 +import { loginStatusAPI } from '@/api/user'
11 +
12 +/**
13 + * 小程序授权
14 + * @description 调用 wx.login 获取 code,由后端授权获取 openid
15 + * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
16 + *
17 + * @example
18 + * const user = await miniProgramAuth()
19 + * if (user) {
20 + * console.log('已自动登录', user)
21 + * } else {
22 + * console.log('需要手动登录')
23 + * }
24 + */
25 +export async function miniProgramAuth() {
26 + try {
27 + // 1. 调用 wx.login 获取 code
28 + const { code } = await Taro.login()
29 +
30 + if (!code) {
31 + throw new Error('获取微信 code 失败')
32 + }
33 +
34 + // 2. 调用后端授权接口
35 + const res = await miniProgramAuthAPI({ code })
36 +
37 + if (res.code === 1) {
38 + return res.data.user || null
39 + } else {
40 + throw new Error(res.msg || '小程序授权失败')
41 + }
42 + } catch (err) {
43 + console.error('小程序授权失败:', err)
44 + throw err
45 + }
46 +}
47 +
48 +/**
49 + * 检查 openid 状态
50 + * @description 调用 loginStatusAPI 检查 is_openid
51 + * @returns {Promise<boolean>} 是否已授权
52 + *
53 + * @example
54 + * const isOpenid = await checkOpenidStatus()
55 + * if (!isOpenid) {
56 + * await miniProgramAuth()
57 + * }
58 + */
59 +export async function checkOpenidStatus() {
60 + try {
61 + const res = await loginStatusAPI()
62 +
63 + if (res.code === 1) {
64 + return res.data.is_openid
65 + } else {
66 + return false
67 + }
68 + } catch (err) {
69 + console.error('检查 openid 状态失败:', err)
70 + return false
71 + }
72 +}
73 +
74 +/**
75 + * 确保 openid 已授权并尝试自动登录
76 + * @description 如果未授权,则调用 wx.login 授权
77 + * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
78 + *
79 + * @example
80 + * const user = await ensureOpenidAuthorized()
81 + * if (user) {
82 + * console.log('已自动登录', user)
83 + * } else {
84 + * console.log('已授权但未登录,需要检查登录状态')
85 + * }
86 + */
87 +export async function ensureOpenidAuthorized() {
88 + const isOpenid = await checkOpenidStatus()
89 +
90 + if (!isOpenid) {
91 + // 未授权,调用 wx.login 授权
92 + return await miniProgramAuth()
93 + }
94 +
95 + // 已授权,返回 null(需要检查登录状态)
96 + return null
97 +}
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-29 18:35:55 4 + * @LastEditTime: 2026-02-02 18:00:00
5 * @FilePath: /manulife-weapp/src/utils/request.js 5 * @FilePath: /manulife-weapp/src/utils/request.js
6 - * @Description: 简单axios封装,后续按实际处理 6 + * @Description: HTTP 请求封装(简化版)
7 */ 7 */
8 -// import axios from 'axios' 8 +import axios from 'axios-miniprogram'
9 -import axios from 'axios-miniprogram';
10 import Taro from '@tarojs/taro' 9 import Taro from '@tarojs/taro'
11 -// import qs from 'qs'
12 -// import { strExist } from './tools'
13 -import { refreshSession, saveCurrentPagePath, navigateToAuth } from './authRedirect'
14 import { parseQueryString } from './tools' 10 import { parseQueryString } from './tools'
15 - 11 +import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config'
16 -// import { ProgressStart, ProgressEnd } from '@/components/axios-progress/progress';
17 -// import store from '@/store'
18 -// import { getToken } from '@/utils/auth'
19 -import BASE_URL, { REQUEST_DEFAULT_PARAMS } from './config';
20 -
21 -/**
22 - * @description 获取 sessionid 的工具函数
23 - * - sessionid 由 authRedirect.refreshSession 写入
24 - * - 每次请求前动态读取,避免旧会话导致的 401
25 - * @returns {string|null} sessionid或null
26 - */
27 -export const getSessionId = () => {
28 - try {
29 - return Taro.getStorageSync("sessionid") || null;
30 - } catch (error) {
31 - console.error('获取sessionid失败:', error);
32 - return null;
33 - }
34 -};
35 -
36 -/**
37 - * @description 设置 sessionid(一般不需要手动调用)
38 - * - 正常情况下由 authRedirect.refreshSession 写入
39 - * - 保留该方法用于极端场景的手动修复/兼容旧逻辑
40 - * @param {string} sessionid cookie 字符串
41 - * @returns {void} 无返回值
42 - */
43 -export const setSessionId = (sessionid) => {
44 - try {
45 - if (!sessionid) return
46 - Taro.setStorageSync('sessionid', sessionid)
47 - } catch (error) {
48 - console.error('设置sessionid失败:', error)
49 - }
50 -}
51 12
52 /** 13 /**
53 - * @description 清空 sessionid(一般不需要手动调用) 14 + * @description axios 实例
54 - * @returns {void} 无返回值
55 - */
56 -export const clearSessionId = () => {
57 - try {
58 - Taro.removeStorageSync('sessionid')
59 - } catch (error) {
60 - console.error('清空sessionid失败:', error)
61 - }
62 -}
63 -
64 -// const isPlainObject = (value) => {
65 -// if (value === null || typeof value !== 'object') return false
66 -// return Object.prototype.toString.call(value) === '[object Object]'
67 -// }
68 -
69 -/**
70 - * @description axios 实例(axios-miniprogram)
71 * - 统一 baseURL / timeout 15 * - 统一 baseURL / timeout
72 - * - 通过拦截器处理:默认参数、cookie 注入、401 自动续期、弱网降级 16 + * - 通过拦截器处理:默认参数、401 跳转登录页、弱网降级
73 */ 17 */
74 const service = axios.create({ 18 const service = axios.create({
75 - baseURL: BASE_URL, // url = base url + request url 19 + baseURL: BASE_URL,
76 - // withCredentials: true, // send cookies when cross-domain requests 20 + timeout: 5000,
77 - timeout: 5000, // request timeout
78 }) 21 })
79 22
80 -// service.defaults.params = {
81 -// ...REQUEST_DEFAULT_PARAMS,
82 -// };
83 -
84 let has_shown_timeout_modal = false 23 let has_shown_timeout_modal = false
85 24
86 /** 25 /**
...@@ -88,7 +27,6 @@ let has_shown_timeout_modal = false ...@@ -88,7 +27,6 @@ let has_shown_timeout_modal = false
88 * @param {Error} error 请求错误对象 27 * @param {Error} error 请求错误对象
89 * @returns {boolean} true=超时,false=非超时 28 * @returns {boolean} true=超时,false=非超时
90 */ 29 */
91 -
92 const is_timeout_error = (error) => { 30 const is_timeout_error = (error) => {
93 const msg = String(error?.message || error?.errMsg || '') 31 const msg = String(error?.message || error?.errMsg || '')
94 if (error?.code === 'ECONNABORTED') return true 32 if (error?.code === 'ECONNABORTED') return true
...@@ -134,7 +72,7 @@ const should_handle_bad_network = async (error) => { ...@@ -134,7 +72,7 @@ const should_handle_bad_network = async (error) => {
134 72
135 /** 73 /**
136 * @description 处理请求超时/弱网错误 74 * @description 处理请求超时/弱网错误
137 - * - 弹出弱网提示(统一文案由 uiText 管理) 75 + * - 弹出弱网提示
138 * @returns {Promise<void>} 无返回值 76 * @returns {Promise<void>} 无返回值
139 */ 77 */
140 const handle_request_timeout = async () => { 78 const handle_request_timeout = async () => {
...@@ -153,59 +91,29 @@ const handle_request_timeout = async () => { ...@@ -153,59 +91,29 @@ const handle_request_timeout = async () => {
153 } 91 }
154 } 92 }
155 93
156 -// 请求拦截器:合并默认参数 / 注入 cookie 94 +// 请求拦截器:合并默认参数
157 service.interceptors.request.use( 95 service.interceptors.request.use(
158 config => { 96 config => {
159 - // console.warn(config)
160 - // console.warn(store)
161 -
162 // 解析 URL 参数并合并 97 // 解析 URL 参数并合并
163 const url = config.url || '' 98 const url = config.url || ''
164 let url_params = {} 99 let url_params = {}
165 if (url.includes('?')) { 100 if (url.includes('?')) {
166 - url_params = parseQueryString(url) 101 + url_params = parseQueryString(url)
167 - config.url = url.split('?')[0] 102 + config.url = url.split('?')[0]
168 } 103 }
169 104
170 // 优先级:调用传参 > URL参数 > 默认参数 105 // 优先级:调用传参 > URL参数 > 默认参数
171 config.params = { 106 config.params = {
172 - ...REQUEST_DEFAULT_PARAMS, 107 + ...REQUEST_DEFAULT_PARAMS,
173 - ...url_params, 108 + ...url_params,
174 - ...(config.params || {}) 109 + ...(config.params || {})
175 - }
176 -
177 - /**
178 - * 动态获取 sessionid 并设置到请求头
179 - * - 确保每个请求都带上最新的 sessionid
180 - * - 注意:axios-miniprogram 的 headers 可能不存在,需要先兜底
181 - */
182 - const sessionid = getSessionId();
183 - if (sessionid) {
184 - config.headers = config.headers || {}
185 - config.headers.cookie = sessionid;
186 } 110 }
187 111
188 // 增加时间戳 112 // 增加时间戳
189 if (config.method === 'get') { 113 if (config.method === 'get') {
190 - config.params = { ...config.params, timestamp: (new Date()).valueOf() } 114 + config.params = { ...config.params, timestamp: (new Date()).valueOf() }
191 } 115 }
192 116
193 - // if ((config.method || '').toLowerCase() === 'post') {
194 - // const url = config.url || ''
195 - // const headers = config.headers || {}
196 - // const contentType = headers['content-type'] || headers['Content-Type']
197 - // const shouldUrlEncode =
198 - // !contentType || String(contentType).includes('application/x-www-form-urlencoded')
199 -
200 - // if (shouldUrlEncode && !strExist(['upload.qiniup.com'], url) && isPlainObject(config.data)) {
201 - // config.headers = {
202 - // ...headers,
203 - // 'content-type': 'application/x-www-form-urlencoded'
204 - // }
205 - // config.data = qs.stringify(config.data)
206 - // }
207 - // }
208 -
209 return config 117 return config
210 }, 118 },
211 error => { 119 error => {
...@@ -214,68 +122,44 @@ service.interceptors.request.use( ...@@ -214,68 +122,44 @@ service.interceptors.request.use(
214 } 122 }
215 ) 123 )
216 124
217 -// 响应拦截器:401 自动续期 / 弱网降级 125 +// 响应拦截器:401 跳转登录页 / 弱网降级
218 service.interceptors.response.use( 126 service.interceptors.response.use(
219 /** 127 /**
220 - * 响应拦截器说明 128 + * @description 响应成功拦截器
221 - * - 这里统一处理后端自定义 code(例如 401 未授权) 129 + * - 处理 401 未授权,跳转到登录页
222 - * - 如需拿到 headers/status 等原始信息,直接返回 response 即可 130 + * - 处理其他自定义错误消息
223 */ 131 */
224 async response => { 132 async response => {
225 const res = response.data 133 const res = response.data
226 134
227 // 401 未授权处理 135 // 401 未授权处理
228 - if (res.code === 401 && ENABLE_AUTH_MODE) { 136 + if (res.code === 401) {
229 - const config = response?.config || {} 137 + // 跳转到登录页
230 - /** 138 + Taro.navigateTo({
231 - * 避免死循环/重复重试: 139 + url: '/pages/login/index'
232 - * - __is_retry:本次请求是 401 后的重试请求,如果仍 401,不再继续重试 140 + }).catch(() => {
233 - */ 141 + // 如果跳转失败(如已经在登录页),则忽略
234 - if (config.__is_retry) { 142 + console.warn('跳转登录页失败,可能已在登录页')
235 - return response 143 + })
236 - }
237 -
238 - /**
239 - * 记录来源页:用于授权成功后回跳
240 - * - 避免死循环:如果已经在 auth 页则不重复记录/跳转
241 - */
242 - const pages = Taro.getCurrentPages();
243 - const currentPage = pages[pages.length - 1];
244 - if (currentPage && currentPage.route !== 'pages/auth/index') {
245 - saveCurrentPagePath()
246 - }
247 -
248 - try {
249 - // 优先走静默续期:成功后重放原请求
250 - await refreshSession()
251 - const retry_config = { ...config, __is_retry: true }
252 - return await service(retry_config)
253 - } catch (error) {
254 - // 静默续期失败:降级跳转到授权页(由授权页完成授权并回跳)
255 - const pages_retry = Taro.getCurrentPages();
256 - const current_page_retry = pages_retry[pages_retry.length - 1];
257 - if (current_page_retry && current_page_retry.route !== 'pages/auth/index') {
258 - navigateToAuth()
259 - }
260 - return response
261 - }
262 } 144 }
263 145
146 + // 处理特殊消息(不需要显示的错误)
264 if (['预约ID不存在'].includes(res.msg)) { 147 if (['预约ID不存在'].includes(res.msg)) {
265 - res.show = false; 148 + res.show = false
266 } 149 }
267 150
268 return response 151 return response
269 }, 152 },
153 + /**
154 + * @description 响应失败拦截器
155 + * - 处理网络错误、超时等
156 + */
270 async error => { 157 async error => {
271 - // Taro.showToast({ 158 + // 处理弱网/断网
272 - // title: error.message,
273 - // icon: 'none',
274 - // duration: 2000
275 - // })
276 if (await should_handle_bad_network(error)) { 159 if (await should_handle_bad_network(error)) {
277 handle_request_timeout() 160 handle_request_timeout()
278 } 161 }
162 +
279 return Promise.reject(error) 163 return Promise.reject(error)
280 } 164 }
281 ) 165 )
......