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'],
......
1 +# 鉴权功能重构规划
2 +
3 +## 📋 文档信息
4 +
5 +- **创建日期**: 2026-02-02
6 +- **最后更新**: 2026-02-02
7 +- **作者**: Claude Code
8 +- **状态**: 规划阶段
9 +
10 +---
11 +
12 +## 🎯 需求概述
13 +
14 +重构项目的鉴权功能,采用**简化设计**
15 +
16 +### 核心原则
17 +
18 +1. **sessionid 只存储和传递**:前端存储 sessionid 并在请求中发送给后端,但前端不依赖它来判断是否登录
19 +2. **401 统一跳转登录页**:所有接口返回 401 时,跳转到登录页
20 +3. **不需要白名单**:后端自己决定哪些接口需要鉴权,返回 401 即可
21 +4. **启动时检查状态**:通过 `loginStatusAPI` 检查 openid 和登录状态
22 +
23 +### 核心概念
24 +
25 +| 概念 | 说明 | 状态字段 |
26 +|------|------|----------|
27 +| **微信授权(openid)** | 后端通过 `miniProgramAuthAPI` 授权获取用户的 openid | `loginStatusAPI.data.is_openid` |
28 +| **用户登录** | 用户通过 `loginAPI` 输入账号密码后绑定 openid | `loginStatusAPI.data.is_login` |
29 +| **401 响应** | 表示**用户未登录**,跳转到登录页 | 后端接口响应 |
30 +
31 +**关键理解**
32 +- sessionid 只是传递给后端的凭证,前端不依赖它做判断
33 +- 所有请求直接发送,401 统一处理
34 +- 不需要白名单机制
35 +
36 +---
37 +
38 +## 📊 API 数据结构
39 +
40 +### 1. `miniProgramAuthAPI` - 小程序授权
41 +
42 +**接口地址**`/srv/?a=openid`
43 +
44 +**功能**:小程序授权,获取 openid 并尝试自动登录
45 +
46 +**请求方式**`POST`
47 +
48 +**请求参数**
49 +```javascript
50 +{
51 + code: string // wx.login 获取的 code
52 +}
53 +```
54 +
55 +**响应结构**
56 +```javascript
57 +{
58 + code: 1,
59 + msg: "success",
60 + data: {
61 + user: {
62 + id: integer, // 用户ID(可能为空)
63 + avatar_url: string, // 头像
64 + name: string // 姓名
65 + } || null // 如果为空,表示需要调用登录接口
66 + }
67 +}
68 +```
69 +
70 +**登录流程设计**(来自注释):
71 +- 先小程序授权
72 +- 如果返回 **用户为空**,则需要调用登录接口(`loginAPI`
73 +- 如果返回 **用户非空**,则不需要调用登录接口,授权接口内部按照 OpenID 绑定的账号,自动登录
74 +
75 +### 2. `loginAPI` - 账号密码登录
76 +
77 +**接口地址**`/srv/?a=user&t=login`
78 +
79 +**功能**:登录并绑定 openid
80 +
81 +**请求方式**`POST`
82 +
83 +**请求参数**
84 +```javascript
85 +{
86 + uuid: string, // 账号(手机号或其他)
87 + password: string // 密码
88 +}
89 +```
90 +
91 +**响应结构**
92 +```javascript
93 +{
94 + code: 1,
95 + msg: "success",
96 + data: any // 没有返回值,只有 code 和 msg
97 +}
98 +```
99 +
100 +### 3. `loginStatusAPI` - 查询登录状态
101 +
102 +**接口地址**`/srv/?a=user&t=login_status`
103 +
104 +**功能**:查询用户的微信授权状态和登录状态
105 +
106 +**请求方式**`GET`
107 +
108 +**响应结构**
109 +```javascript
110 +{
111 + code: 1,
112 + msg: "success",
113 + data: {
114 + is_login: boolean, // true=已登录,false=未登录
115 + is_openid: boolean // true=已授权(有 openid),false=未授权
116 + }
117 +}
118 +```
119 +
120 +**示例响应**
121 +```javascript
122 +// 场景 1:已授权 + 已登录
123 +{
124 + code: 1,
125 + msg: "success",
126 + data: {
127 + is_login: true,
128 + is_openid: true
129 + }
130 +}
131 +
132 +// 场景 2:已授权 + 未登录
133 +{
134 + code: 1,
135 + msg: "success",
136 + data: {
137 + is_login: false,
138 + is_openid: true
139 + }
140 +}
141 +
142 +// 场景 3:未授权
143 +{
144 + code: 1,
145 + msg: "success",
146 + data: {
147 + is_login: false,
148 + is_openid: false
149 + }
150 +}
151 +```
152 +
153 +### 4. `getProfileAPI` - 获取个人信息
154 +
155 +**接口地址**`/srv/?a=user&t=get_profile`
156 +
157 +**功能**:获取当前登录用户的个人信息
158 +
159 +**请求方式**`GET`
160 +
161 +**响应结构**
162 +```javascript
163 +{
164 + code: 1,
165 + msg: "success",
166 + data: {
167 + user: {
168 + id: integer,
169 + avatar_url: string,
170 + name: string
171 + }
172 + }
173 +}
174 +```
175 +
176 +### 5. `logoutAPI` - 退出登录
177 +
178 +**接口地址**`/srv/?a=user&t=logout`
179 +
180 +**功能**:退出登录并解绑 openid
181 +
182 +**请求方式**`POST`
183 +
184 +**响应结构**
185 +```javascript
186 +{
187 + code: 1,
188 + msg: "success",
189 + data: any
190 +}
191 +```
192 +
193 +---
194 +
195 +## 🏗️ 鉴权架构设计
196 +
197 +### 简化的鉴权模型
198 +
199 +```
200 +┌─────────────────────────────────────────────────────────────┐
201 +│ 小程序启动 │
202 +└─────────────────────────────────────────────────────────────┘
203 +
204 +
205 +┌─────────────────────────────────────────────────────────────┐
206 +│ 调用 loginStatusAPI │
207 +│ 检查 is_openid + is_login │
208 +└─────────────────────────────────────────────────────────────┘
209 +
210 + ┌───────────────┴───────────────┐
211 + │ │
212 + ▼ ▼
213 + ┌─────────────────┐ ┌─────────────────┐
214 + │ is_openid │ │ is_login │
215 + │ = false │ │ = false │
216 + └─────────────────┘ └─────────────────┘
217 + │ │
218 + ▼ ▼
219 + ┌─────────────────┐ ┌─────────────────┐
220 + │ 调用 wx.login │ │ 跳转到登录页 │
221 + │ 获取 code │ │ (/pages/login) │
222 + │ 调用 │ │ │
223 + │ miniProgramAuthAPI│ │ │
224 + └─────────────────┘ └─────────────────┘
225 +
226 +
227 + ┌─────────────────┐
228 + │ 返回 user? │
229 + └─────────────────┘
230 + │ │
231 + 有值 │ │ 为空
232 + │ └──────────→ 跳转到登录页
233 +
234 + 正常进入小程序
235 +```
236 +
237 +### 请求处理流程
238 +
239 +```
240 +所有接口请求
241 +
242 + ├─→ 添加 sessionid 到请求头(如果有)
243 +
244 + ├─→ 发送请求
245 +
246 + └─→ 响应处理
247 + ├─→ 200 → 正常处理
248 + ├─→ 401 → 清除 sessionid → 跳转登录页
249 + └─→ 其他错误 → 显示错误提示
250 +```
251 +
252 +---
253 +
254 +## 🔄 鉴权流程设计
255 +
256 +### 场景 1:小程序启动
257 +
258 +```mermaid
259 +flowchart TD
260 + A[小程序启动] --> B[调用 loginStatusAPI]
261 + B --> C{检查响应}
262 + C -->|网络错误| D[显示错误提示]
263 + C -->|成功| E{is_openid?}
264 + E -->|false| F[调用 wx.login 获取 code]
265 + F --> G[调用 miniProgramAuthAPI]
266 + G --> H{返回 user?}
267 + H -->|为空| I[跳转到登录页]
268 + H -->|有值| J[正常进入小程序]
269 + E -->|true| K{is_login?}
270 + K -->|false| I
271 + K -->|true| L[正常进入小程序]
272 +```
273 +
274 +**代码流程**
275 +```javascript
276 +// 1. 检查状态
277 +const { is_openid, is_login } = await loginStatusAPI()
278 +
279 +// 2. 处理未授权
280 +if (!is_openid) {
281 + const { code } = await Taro.login()
282 + const res = await miniProgramAuthAPI({ code })
283 +
284 + if (res.data.user) {
285 + // 已自动登录
286 + return res.data.user
287 + } else {
288 + // 需要登录
289 + Taro.navigateTo({ url: '/pages/login/index' })
290 + return
291 + }
292 +}
293 +
294 +// 3. 处理未登录
295 +if (!is_login) {
296 + Taro.navigateTo({ url: '/pages/login/index' })
297 +}
298 +```
299 +
300 +### 场景 2:用户登录
301 +
302 +```mermaid
303 +flowchart TD
304 + A[用户在登录页] --> B[输入账号密码]
305 + B --> C[调用 loginAPI]
306 + C --> D{登录结果}
307 + D -->|成功| E[保存 sessionid]
308 + E --> F[保存用户信息]
309 + F --> G[返回上一页或首页]
310 + D -->|失败| H[显示错误提示]
311 +```
312 +
313 +### 场景 3:接口请求 401
314 +
315 +```mermaid
316 +flowchart TD
317 + A[发起接口请求] --> B[添加 sessionid 到请求头]
318 + B --> C[发送请求]
319 + C --> D{响应状态}
320 + D -->|200| E[正常处理]
321 + D -->|401| F[清除 sessionid]
322 + F --> G[跳转到登录页]
323 + D -->|其他错误| H[显示错误提示]
324 +```
325 +
326 +**关键点**
327 +- 401 只表示**用户未登录**,不表示 openid 未授权
328 +- 401 时清除 sessionid,跳转到登录页
329 +- **不在 401 时重新调用 wx.login 或 miniProgramAuthAPI**
330 +
331 +---
332 +
333 +## 📁 文件结构规划
334 +
335 +### 核心文件
336 +
337 +```
338 +src/
339 +├── utils/
340 +│ ├── openid.js # 新增:微信授权(openid)管理
341 +│ ├── request.js # 修改:HTTP 请求拦截器(移除白名单和 sessionid)
342 +│ └── authRedirect.js # 删除:完全替换为新逻辑
343 +
344 +├── api/
345 +│ ├── user.js # 已存在:用户相关 API
346 +│ └── wechat.js # 已存在:微信授权 API
347 +
348 +├── pages/
349 +│ ├── auth/
350 +│ │ └── index.vue # 删除:不再需要单独的授权页
351 +│ └── login/
352 +│ └── index.vue # 保留:用户登录页(账号密码登录)
353 +
354 +├── stores/
355 +│ └── user.js # 新增:用户状态管理(Pinia)
356 +
357 +└── app.js # 修改:启动时检查登录状态
358 +```
359 +
360 +### 文件职责
361 +
362 +| 文件 | 职责 | 状态 |
363 +|------|------|------|
364 +| `utils/openid.js` | 微信授权逻辑(wx.login、miniProgramAuthAPI) | 新增 |
365 +| `utils/request.js` | HTTP 拦截器(401 处理,移除白名单和 sessionid) | 修改 |
366 +| `utils/authRedirect.js` | 旧授权逻辑 | 删除 |
367 +| `api/user.js` | 用户相关 API | 已存在 |
368 +| `api/wechat.js` | 微信授权 API | 已存在 |
369 +| `stores/user.js` | 用户信息状态管理 | 新增 |
370 +| `app.js` | 启动时检查登录状态 | 修改 |
371 +
372 +---
373 +
374 +## 🔧 实现细节
375 +
376 +### 1. 微信授权管理 (`utils/openid.js`)
377 +
378 +```javascript
379 +import Taro from '@tarojs/taro'
380 +import { miniProgramAuthAPI } from '@/api/wechat'
381 +import { loginStatusAPI } from '@/api/user'
382 +
383 +/**
384 + * 小程序授权
385 + * @description 调用 wx.login 获取 code,由后端授权获取 openid
386 + * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
387 + */
388 +export async function miniProgramAuth() {
389 + try {
390 + // 1. 调用 wx.login 获取 code
391 + const { code } = await Taro.login()
392 +
393 + if (!code) {
394 + throw new Error('获取微信 code 失败')
395 + }
396 +
397 + // 2. 调用后端授权接口
398 + const res = await miniProgramAuthAPI({ code })
399 +
400 + if (res.code === 1) {
401 + return res.data.user || null
402 + } else {
403 + throw new Error(res.msg || '小程序授权失败')
404 + }
405 + } catch (err) {
406 + console.error('小程序授权失败:', err)
407 + throw err
408 + }
409 +}
410 +
411 +/**
412 + * 检查 openid 状态
413 + * @description 调用 loginStatusAPI 检查 is_openid
414 + * @returns {Promise<boolean>} 是否已授权
415 + */
416 +export async function checkOpenidStatus() {
417 + try {
418 + const res = await loginStatusAPI()
419 +
420 + if (res.code === 1) {
421 + return res.data.is_openid
422 + } else {
423 + return false
424 + }
425 + } catch (err) {
426 + console.error('检查 openid 状态失败:', err)
427 + return false
428 + }
429 +}
430 +
431 +/**
432 + * 确保 openid 已授权并尝试自动登录
433 + * @description 如果未授权,则调用 wx.login 授权
434 + * @returns {Promise<{user: Object|null}>} 返回用户信息(如果已自动登录)
435 + */
436 +export async function ensureOpenidAuthorized() {
437 + const isOpenid = await checkOpenidStatus()
438 +
439 + if (!isOpenid) {
440 + // 未授权,调用 wx.login 授权
441 + return await miniProgramAuth()
442 + }
443 +
444 + // 已授权,返回 null(需要检查登录状态)
445 + return null
446 +}
447 +```
448 +
449 +### 2. sessionid 管理 (`utils/session.js`)
450 +
451 +```javascript
452 +const SESSION_KEY = 'sessionid'
453 +
454 +/**
455 + * 保存 sessionid
456 + * @param {string} sessionid
457 + */
458 +export function setSessionId(sessionid) {
459 + if (!sessionid) {
460 + console.warn('sessionid 为空,无法保存')
461 + return
462 + }
463 + localStorage.setItem(SESSION_KEY, sessionid)
464 +}
465 +
466 +/**
467 + * 获取 sessionid
468 + * @returns {string|null}
469 + */
470 +export function getSessionId() {
471 + return localStorage.getItem(SESSION_KEY)
472 +}
473 +
474 +/**
475 + * 清除 sessionid
476 + */
477 +export function clearSessionId() {
478 + localStorage.removeItem(SESSION_KEY)
479 +}
480 +
481 +/**
482 + * 检查是否有 sessionid
483 + * @description 注意:这只是检查本地是否有 sessionid,不代表用户已登录
484 + * @returns {boolean}
485 + */
486 +export function hasSessionId() {
487 + return !!getSessionId()
488 +}
489 +```
490 +
491 +### 3. 请求拦截器 (`utils/request.js`)
492 +
493 +```javascript
494 +// 请求拦截器
495 +instance.interceptors.request.use((config) => {
496 + // 后端通过 cookie 自动处理 sessionid
497 + // 前端不需要添加任何 sessionid 相关的逻辑
498 + return config
499 +})
500 +
501 +// 响应拦截器
502 +instance.interceptors.response.use(
503 + (response) => {
504 + // 正常响应
505 + return response
506 + },
507 + (error) => {
508 + // 错误响应
509 + if (error.response?.status === 401) {
510 + // 401 表示用户未登录
511 + // 跳转到登录页
512 + Taro.navigateTo({
513 + url: '/pages/login/index'
514 + })
515 + }
516 +
517 + return Promise.reject(error)
518 + }
519 +)
520 +```
521 +
522 +**关键点**
523 +- **不需要白名单**:所有接口直接发送
524 +- **不处理 sessionid**:后端通过 cookie 自动处理
525 +- **401 统一处理**:跳转登录页
526 +
527 +### 4. 用户状态管理 (`stores/user.js`)
528 +
529 +```javascript
530 +import { defineStore } from 'pinia'
531 +import { ref } from 'vue'
532 +import { loginStatusAPI, loginAPI, getProfileAPI, logoutAPI } from '@/api/user'
533 +import { ensureOpenidAuthorized } from '@/utils/openid'
534 +
535 +export const useUserStore = defineStore('user', () => {
536 + // 状态
537 + const userInfo = ref(null) // 用户信息
538 + const isOpenid = ref(false) // 是否已授权(openid)
539 + const isLoggedIn = ref(false) // 是否已登录
540 + const loading = ref(false) // 加载状态
541 +
542 + /**
543 + * 检查登录状态
544 + * @description 检查 openid 和登录状态,处理相应的逻辑
545 + */
546 + async function checkLoginStatus() {
547 + loading.value = true
548 +
549 + try {
550 + // 1. 确保 openid 已授权并尝试自动登录
551 + const user = await ensureOpenidAuthorized()
552 +
553 + if (user) {
554 + // miniProgramAuthAPI 返回了用户信息,说明已自动登录
555 + userInfo.value = user
556 + isOpenid.value = true
557 + isLoggedIn.value = true
558 + return
559 + }
560 +
561 + // 2. 查询登录状态
562 + const res = await loginStatusAPI()
563 +
564 + if (res.code === 1) {
565 + isOpenid.value = res.data.is_openid
566 + isLoggedIn.value = res.data.is_login
567 +
568 + // 3. 如果已登录,获取用户信息
569 + if (isLoggedIn.value) {
570 + await fetchUserInfo()
571 + } else {
572 + // 未登录,跳转到登录页
573 + Taro.navigateTo({
574 + url: '/pages/login/index'
575 + })
576 + }
577 + } else {
578 + throw new Error(res.msg || '查询登录状态失败')
579 + }
580 + } catch (err) {
581 + console.error('检查登录状态失败:', err)
582 + throw err
583 + } finally {
584 + loading.value = false
585 + }
586 + }
587 +
588 + /**
589 + * 获取用户信息
590 + */
591 + async function fetchUserInfo() {
592 + try {
593 + const res = await getProfileAPI()
594 +
595 + if (res.code === 1) {
596 + userInfo.value = res.data.user
597 + } else {
598 + throw new Error(res.msg || '获取用户信息失败')
599 + }
600 + } catch (err) {
601 + console.error('获取用户信息失败:', err)
602 + throw err
603 + }
604 + }
605 +
606 + /**
607 + * 用户登录
608 + * @param {Object} loginData 登录数据
609 + * @param {string} loginData.uuid 账号
610 + * @param {string} loginData.password 密码
611 + */
612 + async function login(loginData) {
613 + loading.value = true
614 +
615 + try {
616 + const res = await loginAPI(loginData)
617 +
618 + if (res.code === 1) {
619 + // 登录成功,获取用户信息
620 + await fetchUserInfo()
621 +
622 + isLoggedIn.value = true
623 +
624 + return { success: true }
625 + } else {
626 + throw new Error(res.msg || '登录失败')
627 + }
628 + } catch (err) {
629 + console.error('登录失败:', err)
630 + return { success: false, message: err.message }
631 + } finally {
632 + loading.value = false
633 + }
634 + }
635 +
636 + /**
637 + * 用户登出
638 + */
639 + async function logout() {
640 + try {
641 + // 调用登出接口
642 + await logoutAPI()
643 +
644 + // 清除本地状态
645 + userInfo.value = null
646 + isOpenid.value = false
647 + isLoggedIn.value = false
648 + } catch (err) {
649 + console.error('登出失败:', err)
650 + }
651 + }
652 +
653 + return {
654 + // 状态
655 + userInfo,
656 + isOpenid,
657 + isLoggedIn,
658 + loading,
659 +
660 + // 方法
661 + checkLoginStatus,
662 + fetchUserInfo,
663 + login,
664 + logout
665 + }
666 +})
667 +```
668 +
669 +### 5. 应用启动逻辑 (`app.js`)
670 +
671 +```javascript
672 +import { useUserStore } from '@/stores/user'
673 +
674 +function App(props) {
675 + const userStore = useUserStore()
676 +
677 + useLaunch(() => {
678 + console.log('小程序启动')
679 +
680 + // 检查登录状态
681 + userStore.checkLoginStatus().catch(err => {
682 + console.error('启动时检查登录状态失败:', err)
683 + })
684 + })
685 +
686 + return props.children
687 +}
688 +```
689 +
690 +---
691 +
692 +## 🚀 实施步骤
693 +
694 +### 第 1 步:创建新文件
695 +
696 +- [ ] 创建 `src/utils/openid.js` - 微信授权(openid)管理
697 +- [ ] 创建 `src/stores/user.js` - 用户状态管理
698 +
699 +### 第 2 步:修改现有文件
700 +
701 +- [ ] 修改 `src/utils/request.js` - 更新请求拦截器
702 + - 移除白名单配置
703 + - 移除 sessionid 相关逻辑
704 + - 更新 401 响应处理
705 +- [ ] 修改 `src/app.js` - 启动时检查登录状态
706 +- [ ] 删除 `src/utils/authRedirect.js` - 移除旧的授权逻辑
707 +- [ ] 删除 `src/pages/auth/index.vue` - 不再需要单独的授权页
708 +
709 +### 第 3 步:更新登录页
710 +
711 +- [ ] 修改 `src/pages/login/index.vue` - 使用新的登录逻辑
712 + - 调用 `userStore.login()`
713 + - 处理登录成功/失败
714 +
715 +### 第 4 步:测试验证
716 +
717 +- [ ] 测试首次启动流程(无 openid)
718 +- [ ] 测试 openid 授权流程
719 +- [ ] 测试自动登录(openid 已绑定账号)
720 +- [ ] 测试手动登录流程
721 +- [ ] 测试 401 处理
722 +- [ ] 测试已登录用户启动
723 +
724 +### 第 5 步:文档更新
725 +
726 +- [ ] 更新 `CLAUDE.md` 鉴权部分
727 +- [ ] 更新 `docs/lessons-learned.md`
728 +- [ ] 添加鉴权流程图
729 +
730 +---
731 +
732 +## ⚠️ 注意事项
733 +
734 +### 1. sessionid 的作用
735 +
736 +- **miniProgramAuthAPI 后端自动处理 sessionid**(如通过 cookie),前端不需要保存
737 +- **loginAPI 不返回 sessionid**,后端通过 cookie 或其他方式自动处理
738 +- 前端**不依赖** sessionid 判断用户是否登录
739 +- 是否登录由后端通过 401 判断
740 +
741 +### 2. 不需要白名单
742 +
743 +- 所有接口直接发送
744 +- 后端自己决定哪些接口需要鉴权
745 +- 返回 401 的接口统一跳转登录页
746 +
747 +### 3. miniProgramAuthAPI 的特殊逻辑
748 +
749 +根据注释:
750 +> 如果返回 **用户为空**,则需要调用登录接口(`loginAPI`)
751 +> 如果返回 **用户非空**,则不需要调用登录接口,授权接口内部按照 OpenID 绑定的账号,**自动登录**
752 +
753 +这意味着:
754 +- **后端自动处理 sessionid**(如通过 cookie),前端不需要保存
755 +- 用户第一次使用:`is_openid=false` → 调用 `miniProgramAuthAPI` → 返回 `user=null` → 跳转登录页
756 +- 用户已绑定账号:`is_openid=true` → 调用 `miniProgramAuthAPI` → 返回 `user` → 自动登录
757 +- **loginAPI 不返回 sessionid 和 user**,登录成功后需要单独调用 `getProfileAPI` 获取用户信息
758 +
759 +### 4. 401 错误处理
760 +
761 +- 401 只表示**用户未登录**,不表示 openid 未授权
762 +- 401 时清除 sessionid,跳转到登录页
763 +- **不要在 401 时重新调用 wx.login 或 miniProgramAuthAPI**
764 +
765 +### 5. 登录流程
766 +
767 +根据现有 API,登录流程应该是:
768 +1. 小程序启动 → 检查 `is_openid`
769 +2. 如果 `is_openid=false` → 调用 `wx.login` → 调用 `miniProgramAuthAPI`
770 +3. 如果返回 `user=null` → 跳转登录页
771 +4. 用户输入账号密码 → 调用 `loginAPI` 绑定
772 +
773 +---
774 +
775 +## 📊 状态机
776 +
777 +```javascript
778 +// 用户状态枚举
779 +const UserState = {
780 + // 未授权(未绑定 openid)
781 + UNAUTHORIZED: 'unauthorized',
782 +
783 + // 已授权,未登录(已绑定 openid,但未绑定业务账号)
784 + AUTH_NOT_LOGIN: 'auth_not_login',
785 +
786 + // 已登录(已绑定业务账号)
787 + LOGGED_IN: 'logged_in'
788 +}
789 +
790 +// 状态转换
791 +UNAUTHORIZED [miniProgramAuthAPI] AUTH_NOT_LOGIN [loginAPI] LOGGED_IN
792 +
793 + [跳转登录页]
794 +```
795 +
796 +**实际字段映射**
797 +- `UNAUTHORIZED``is_openid = false`
798 +- `AUTH_NOT_LOGIN``is_openid = true && is_login = false`
799 +- `LOGGED_IN``is_openid = true && is_login = true`
800 +
801 +---
802 +
803 +## 🔗 相关文档
804 +
805 +- [Taro 登录流程](https://docs.taro.zone/docs/weapp/next/login)
806 +- [微信小程序登录](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html)
807 +- [项目经验教训](../lessons-learned.md)
808 +- [API 文档](../../src/api/user.js)
809 +- [微信 API 文档](../../src/api/wechat.js)
810 +
811 +---
812 +
813 +**最后更新**: 2026-02-02
814 +**维护者**: Claude Code
...@@ -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 || {}
19 18
20 - const query_string = Object.keys(query) 19 + // 获取用户 store
21 - .map((key) => `${key}=${encodeURIComponent(query[key])}`) 20 + const userStore = useUserStore()
22 - .join('&')
23 - const full_path = query_string ? `${path}?${query_string}` : path
24 -
25 - // 保存当前页面路径,用于授权后跳转回原页面
26 - if (full_path) {
27 - saveCurrentPagePath(full_path)
28 - }
29 -
30 - // 如果用户已授权,则不需要额外操作
31 - if (hasAuth()) {
32 - return
33 - }
34 -
35 - if (path === 'pages/auth/index') return
36 21
22 + // 检查登录状态
23 + // - 如果 is_openid=false,会自动调用 wx.login 授权
24 + // - 如果授权后返回 user,说明已自动登录
25 + // - 如果 is_login=false,会跳转到登录页
37 try { 26 try {
38 - // 尝试静默授权 27 + await userStore.checkLoginStatus()
39 - await silentAuth()
40 } catch (error) { 28 } catch (error) {
41 - console.error('静默授权失败:', error) 29 + console.error('启动时检查登录状态失败:', error)
42 - // 授权失败则跳转至授权页面 30 + // 即使失败也继续,让用户可以正常使用小程序
43 - navigateToAuth(full_path || undefined)
44 } 31 }
45 -
46 - return
47 }, 32 },
33 +
48 onShow() { 34 onShow() {
35 + // 页面显示时的逻辑
49 }, 36 },
50 -}); 37 +})
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) {
120 Taro.hideLoading() 112 Taro.hideLoading()
121 Taro.showToast({ title: '登录成功', icon: 'success' }) 113 Taro.showToast({ title: '登录成功', icon: 'success' })
114 +
115 + // 延迟后跳转到首页
122 setTimeout(() => { 116 setTimeout(() => {
123 - // Redirect to home or previous page
124 Taro.reLaunch({ url: '/pages/index/index' }) 117 Taro.reLaunch({ url: '/pages/index/index' })
125 }, 1500) 118 }, 1500)
126 - }, 1000) 119 + } else {
120 + Taro.hideLoading()
121 + Taro.showToast({
122 + title: result.message || '登录失败',
123 + icon: 'none'
124 + })
125 + }
126 + } catch (error) {
127 + Taro.hideLoading()
128 + Taro.showToast({
129 + title: error.message || '登录失败,请重试',
130 + icon: 'none'
131 + })
132 + }
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 +})
1 -import Taro from '@tarojs/taro'
2 -import { routerStore } from '@/stores/router'
3 -import { buildApiUrl } from './tools'
4 -import { ENABLE_AUTH_MODE } from './config'
5 -
6 -// 改进:添加全局状态变量注释
7 -/**
8 - * 上一次跳转到授权页的时间戳,用于防抖(避免短时间内重复跳转)
9 - * @type {number}
10 - */
11 -let last_navigate_auth_at = 0
12 -
13 -/**
14 - * 是否正在跳转到授权页,用于防重复(避免并发跳转)
15 - * @type {boolean}
16 - */
17 -let navigating_to_auth = false
18 -
19 -/**
20 - * 授权与回跳相关工具
21 - * - 统一管理:保存来源页、静默授权、跳转授权页、授权后回跳
22 - * - 约定:sessionid 存在于本地缓存 key 为 sessionid
23 - * - 说明:refreshSession/silentAuth 使用单例 Promise,避免并发重复授权
24 - */
25 -
26 -/**
27 - * 获取当前页完整路径(含 query)
28 - * @returns {string} 当前页路径,示例:pages/index/index?a=1;获取失败返回空字符串
29 - */
30 -export const getCurrentPageFullPath = () => {
31 - const pages = Taro.getCurrentPages()
32 - if (!pages || pages.length === 0) return ''
33 -
34 - const current_page = pages[pages.length - 1]
35 - const route = current_page.route
36 - const options = current_page.options || {}
37 -
38 - // 改进:key 也需要编码,避免特殊字符导致 URL 解析错误
39 - const query_params = Object.keys(options)
40 - .map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(options[key])}`)
41 - .join('&')
42 -
43 - return query_params ? `${route}?${query_params}` : route
44 -}
45 -
46 -/**
47 - * 保存当前页路径(用于授权成功后回跳)
48 - * @param {string} custom_path 自定义路径,不传则取当前页完整路径
49 - * @returns {void} 无返回值
50 - */
51 -export const saveCurrentPagePath = (custom_path) => {
52 - const router = routerStore()
53 - const path = custom_path || getCurrentPageFullPath()
54 - router.add(path)
55 -}
56 -
57 -/**
58 - * 判断是否已授权
59 - * @returns {boolean} true=已存在 sessionid,false=需要授权
60 - */
61 -export const hasAuth = () => {
62 - // 如果禁用了授权模式,直接视为已授权
63 - if (!ENABLE_AUTH_MODE) return true
64 -
65 - try {
66 - const sessionid = Taro.getStorageSync('sessionid')
67 - return !!sessionid && sessionid !== ''
68 - } catch (error) {
69 - console.error('检查授权状态失败:', error)
70 - return false
71 - }
72 -}
73 -
74 -let auth_promise = null
75 -
76 -/**
77 - * 从响应中提取 cookie
78 - * 兼容小程序端和 H5 端的不同返回格式
79 - * @param {object} response Taro.request 响应对象
80 - * @returns {string|null} cookie 字符串或 null
81 - */
82 -const extractCookie = (response) => {
83 - // 小程序端优先从 response.cookies 取
84 - if (response.cookies?.[0]) return response.cookies[0]
85 - // H5 端从 header 取(兼容不同大小写)
86 - const cookie = response.header?.['Set-Cookie'] || response.header?.['set-cookie']
87 - if (Array.isArray(cookie)) return cookie[0]
88 - return cookie || null
89 -}
90 -
91 -/**
92 - * 刷新会话:通过 Taro.login 获取 code,换取后端会话 cookie 并写入缓存
93 - * - 被 request.js 的 401 拦截器调用,用于自动“静默续期 + 原请求重放”
94 - * - 复用 auth_promise,防止多个接口同时 401 时并发触发多次登录
95 - * @param {object} options 可选项
96 - * @param {boolean} options.show_loading 是否展示 loading,默认 true
97 - * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果(会把 cookie 写入 storage 的 sessionid)
98 - */
99 -export const refreshSession = async (options) => {
100 - // 如果禁用了授权模式,直接返回模拟成功
101 - if (!ENABLE_AUTH_MODE) {
102 - return { code: 1, msg: '授权模式已禁用', cookie: 'mock_session_id' }
103 - }
104 -
105 - const show_loading = options?.show_loading !== false
106 -
107 - // 已有授权进行中时,直接复用同一个 Promise
108 - if (auth_promise) return auth_promise
109 -
110 - auth_promise = (async () => {
111 - try {
112 - if (show_loading) {
113 - Taro.showLoading({
114 - title: '加载中...',
115 - mask: true,
116 - })
117 - }
118 -
119 - // 调用微信登录获取临时 code
120 - const login_result = await new Promise((resolve, reject) => {
121 - Taro.login({
122 - success: resolve,
123 - fail: reject,
124 - })
125 - })
126 -
127 - if (!login_result || !login_result.code) {
128 - throw new Error('获取微信登录code失败')
129 - }
130 -
131 - const request_data = {
132 - code: login_result.code,
133 - }
134 -
135 - // 换取后端会话(服务端通过 Set-Cookie 返回会话信息)
136 - const response = await Taro.request({
137 - url: buildApiUrl('openid'),
138 - method: 'POST',
139 - data: request_data,
140 - })
141 -
142 - if (!response?.data || response.data.code !== 1) {
143 - throw new Error(response?.data?.msg || '授权失败')
144 - }
145 -
146 - // 改进:使用 extractCookie 函数统一处理 cookie 提取逻辑
147 - const cookie = extractCookie(response)
148 - if (!cookie) {
149 - throw new Error('授权失败:没有获取到有效的会话信息')
150 - }
151 -
152 - // NOTE: 写入本地缓存:后续请求会从缓存取 sessionid 并带到请求头
153 - Taro.setStorageSync('sessionid', cookie)
154 -
155 - /**
156 - * refreshSession() 的返回值当前没有任何业务消费点:在 request.js 里只是 await refreshSession() ,不解构、不使用;其他地方也没直接调用它
157 - * 所以 return { ...response.data, cookie } 目前属于“严谨保留”:方便未来需要拿 cookie / code / msg 做埋点、提示、分支处理时直接用(例如授权页显示更细错误、统计刷新成功率等)。
158 - */
159 -
160 - return {
161 - ...response.data,
162 - cookie,
163 - }
164 - } finally {
165 - if (show_loading) {
166 - Taro.hideLoading()
167 - }
168 - }
169 - })().finally(() => {
170 - auth_promise = null
171 - })
172 -
173 - return auth_promise
174 -}
175 -
176 -/**
177 - * 执行静默授权:检查是否已授权,若否则调用 refreshSession 刷新会话
178 - * @param {boolean} show_loading 是否展示 loading,默认 true
179 - * @returns {Promise<{code:number,msg?:string,data?:any,cookie?:string}>} 授权结果
180 - *
181 - * 改进:使用下划线前缀表示私有函数,仅供 silentAuth 内部使用
182 - */
183 -const _do_silent_auth = async (show_loading) => {
184 - // 已有 sessionid 时直接视为已授权
185 - if (hasAuth()) {
186 - return { code: 1, msg: '已授权' }
187 - }
188 -
189 - // 需要授权时,走刷新会话逻辑
190 - return await refreshSession({ show_loading })
191 -}
192 -
193 -/**
194 - * 静默授权:用于启动阶段/分享页/授权页发起授权
195 - * - 与 refreshSession 共用 auth_promise,避免并发重复调用
196 - * @param {(result: any) => void} on_success 成功回调(可选)
197 - * @param {(error: {message:string, original:Error}) => void} on_error 失败回调(可选,入参为错误对象)
198 - * @param {object} options 可选项
199 - * @param {boolean} options.show_loading 是否展示 loading,默认 true
200 - * @returns {Promise<any>} 授权结果(成功 resolve,失败 reject)
201 - */
202 -export const silentAuth = async (on_success, on_error, options) => {
203 - const show_loading = options?.show_loading !== false
204 -
205 - try {
206 - // 未有授权进行中时才发起一次授权,并复用 Promise
207 - if (!auth_promise) {
208 - /**
209 - * 用 auth_promise 做"单例锁",把同一时刻并发触发的多次授权合并成一次。
210 - * 把正在执行的授权 Promise 存起来;后面如果又有人调用 silentAuth() ,
211 - * 看到 auth_promise 不为空,就直接 await 同一个 Promise,避免同时发起多次 Taro.login / 换会话请求
212 - * ---------------------------------------------------------------------------------------
213 - * .finally(() => { auth_promise = null }) 不管授权成功还是失败(resolve/reject),都把"锁"释放掉。
214 - * 不用 finally 的问题:如果授权失败抛错了,而你只在 .then 里清空,那么 auth_promise 会一直卡着旧的 rejected Promise;
215 - * 后续再调用 silentAuth() 会复用这个失败的 Promise,导致永远失败、且永远不会重新发起授权。
216 - * 用 finally :保证成功/失败都会清空,下一次调用才有机会重新走授权流程。
217 - */
218 - auth_promise = _do_silent_auth(show_loading)
219 - .finally(() => {
220 - auth_promise = null
221 - })
222 - }
223 - const result = await auth_promise
224 - if (on_success) on_success(result)
225 -
226 - /**
227 - * 当前返回值 没有实际消费点 :全项目只在 3 处调用,全部都 不使用返回值 。
228 - * - 启动预加载: await silentAuth() 仅等待,不用结果, app.js
229 - * - 授权页: silentAuth().then(() => returnToOriginalPage()) then 里也没接 res , auth/index.vue
230 - * - 分享场景: await silentAuth(successCb, errorCb) 只看成功/失败分支,不用返回值, handleSharePageAuth
231 - * 所以这行 return result 的作用目前是 语义完整 + 未来扩展位 :
232 - * 如果以后要在调用处根据 code/msg/cookie 做分支或埋点,返回值就能直接用;现在等价于"只用 resolve/reject 表达成功失败"。
233 - */
234 -
235 - return result
236 - } catch (error) {
237 - // 改进:统一传递完整错误对象,包含 message 和 original error
238 - const error_obj = {
239 - message: error?.message || '授权失败,请稍后重试',
240 - original: error
241 - }
242 - if (on_error) on_error(error_obj)
243 - throw error
244 - }
245 -}
246 -
247 -/**
248 - * 防重复跳转冷却时间 (毫秒)
249 - * @type {number}
250 - */
251 -const NAVIGATE_AUTH_COOLDOWN_MS = 1200
252 -
253 -/**
254 - * 导航状态重置延迟时间 (毫秒)
255 - * @type {number}
256 - */
257 -const NAVIGATING_RESET_DELAY_MS = 300
258 -
259 -/**
260 - * 跳转到授权页(降级方案)
261 - * - 会先保存回跳路径(默认当前页),授权成功后在 auth 页回跳
262 - * @param {string} return_path 指定回跳路径(可选)
263 - * @returns {Promise<void>} 无返回值
264 - */
265 -export const navigateToAuth = async (return_path) => {
266 - // 如果禁用了授权模式,直接返回不跳转
267 - if (!ENABLE_AUTH_MODE) return
268 -
269 - const pages = Taro.getCurrentPages()
270 - const current_page = pages[pages.length - 1]
271 - const current_route = current_page?.route
272 - if (current_route === 'pages/auth/index') {
273 - return
274 - }
275 -
276 - const now = Date.now()
277 - if (navigating_to_auth) return
278 - if (now - last_navigate_auth_at < NAVIGATE_AUTH_COOLDOWN_MS) return
279 -
280 - last_navigate_auth_at = now
281 - navigating_to_auth = true
282 -
283 - if (return_path) {
284 - saveCurrentPagePath(return_path)
285 - } else {
286 - saveCurrentPagePath()
287 - }
288 -
289 - // 改进:使用 try-finally 明确状态恢复逻辑,确保无论成功失败都会重置状态
290 - try {
291 - await Taro.navigateTo({ url: '/pages/auth/index' })
292 - } catch (error) {
293 - // 改进:添加错误日志,方便追踪降级场景
294 - console.warn('navigateTo 失败,降级使用 redirectTo:', error)
295 - await Taro.redirectTo({ url: '/pages/auth/index' })
296 - } finally {
297 - setTimeout(() => {
298 - navigating_to_auth = false
299 - }, NAVIGATING_RESET_DELAY_MS)
300 - }
301 -}
302 -
303 -/**
304 - * 授权成功后回跳到来源页
305 - * - 优先使用 routerStore 里保存的路径
306 - * - 失败降级:redirectTo -> reLaunch
307 - * @param {string} default_path 未保存来源页时的默认回跳路径
308 - * @returns {Promise<void>} 回跳完成
309 - */
310 -export const returnToOriginalPage = async (default_path = '/pages/index/index') => {
311 - const router = routerStore()
312 - const saved_path = router.url
313 -
314 - try {
315 - router.remove()
316 -
317 - const pages = Taro.getCurrentPages()
318 - const current_page = pages[pages.length - 1]
319 - const current_route = current_page?.route
320 -
321 - let target_path = default_path
322 - if (saved_path && saved_path !== '') {
323 - target_path = saved_path.startsWith('/') ? saved_path : `/${saved_path}`
324 - }
325 -
326 - const target_route = target_path.split('?')[0].replace(/^\//, '')
327 -
328 - if (current_route === target_route) {
329 - return
330 - }
331 -
332 - try {
333 - await Taro.redirectTo({ url: target_path })
334 - } catch (error) {
335 - // 改进:添加错误日志,方便追踪降级场景
336 - console.warn('redirectTo 失败,降级使用 reLaunch:', error)
337 - await Taro.reLaunch({ url: target_path })
338 - }
339 - } catch (error) {
340 - console.error('returnToOriginalPage 执行出错:', error)
341 - try {
342 - await Taro.reLaunch({ url: default_path })
343 - } catch (final_error) {
344 - console.error('最终降级方案也失败了:', final_error)
345 - }
346 - }
347 -}
348 -
349 -/**
350 - * 判断是否来自分享场景
351 - * @param {object} options 页面 options
352 - * @returns {boolean} true=来自分享场景,false=非分享场景
353 - */
354 -export const isFromShare = (options) => {
355 - return options && (options.from_share === '1' || options.scene)
356 -}
357 -
358 -/**
359 - * 分享页进入时的授权处理
360 - * - 来自分享且未授权:保存当前页路径,授权成功后回跳
361 - * - 授权失败:返回 false,由调用方决定是否继续降级处理
362 - * @param {object} options 页面 options
363 - * @param {Function} callback 授权成功后的继续逻辑(可选)
364 - * @returns {Promise<boolean>} true=授权已完成/无需授权,false=授权失败
365 - */
366 -export const handleSharePageAuth = async (options, callback) => {
367 - if (hasAuth()) {
368 - if (typeof callback === 'function') callback()
369 - return true
370 - }
371 -
372 - if (isFromShare(options)) {
373 - saveCurrentPagePath()
374 - }
375 -
376 - try {
377 - await silentAuth(
378 - () => {
379 - if (typeof callback === 'function') callback()
380 - },
381 - () => {
382 - navigateToAuth()
383 - }
384 - )
385 - return true
386 - } catch (error) {
387 - navigateToAuth()
388 - return false
389 - }
390 -}
391 -
392 -/**
393 - * 为路径追加分享标记
394 - * @param {string} path 原路径
395 - * @returns {string} 追加后的路径
396 - */
397 -export const addShareFlag = (path) => {
398 - const separator = path.includes('?') ? '&' : '?'
399 - return `${path}${separator}from_share=1`
400 -}
...@@ -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,12 +91,9 @@ const handle_request_timeout = async () => { ...@@ -153,12 +91,9 @@ 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 = {}
...@@ -174,38 +109,11 @@ service.interceptors.request.use( ...@@ -174,38 +109,11 @@ service.interceptors.request.use(
174 ...(config.params || {}) 109 ...(config.params || {})
175 } 110 }
176 111
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 - }
187 -
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 )
......