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>
Showing
16 changed files
with
1254 additions
and
707 deletions
| ... | @@ -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'], | ... | ... |
docs/specs/2026-02-02-auth-refactoring.md
0 → 100644
| 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 || {} | 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 | ... | ... |
src/pages/auth/index.config.js
deleted
100755 → 0
src/pages/auth/index.less
deleted
100644 → 0
src/pages/auth/index.vue
deleted
100644 → 0
| 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' }, | ... | ... |
src/stores/user.js
0 → 100644
| 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 | +}) |
src/utils/authRedirect.js
deleted
100644 → 0
| 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 | ... | ... |
src/utils/openid.js
0 → 100644
| 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 | ) | ... | ... |
-
Please register or login to post a comment