fix(security): 修复 Codex 审查发现的多个安全和质量问题
## 修复内容
### P1 级别(高优先级)
- 修复 Cookie 解析潜在破坏风险(删除 cleanupDuplicateCookies 的 split(',') 逻辑)
- 修复授权响应只保留第一个 cookie 的问题(智能识别 sessionid)
- 增强搜索响应的防御性访问(添加可选链 ?. 防止字段缺失抛错)
- 修复启动日志泄露用户隐私(生产环境手机号和用户名脱敏)
### P2 级别(中优先级)
- 修复弱网提示只会出现一次的问题(时间窗口防抖 30 秒)
- 修复搜索测试用例与实现不一致(file → article)
- 修复 usePlanView 集成测试失败(添加 useFileOperation mock)
## 测试结果
- ✅ 所有测试通过 (178/178)
- ✅ ESLint 检查通过(0 errors, 37 warnings 已存在)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Showing
7 changed files
with
197 additions
and
95 deletions
| ... | @@ -7535,3 +7535,75 @@ | ... | @@ -7535,3 +7535,75 @@ |
| 7535 | 7535 | ||
| 7536 | --- | 7536 | --- |
| 7537 | 7537 | ||
| 7538 | + | ||
| 7539 | +### 10:51:02 - 完成任务 | ||
| 7540 | + | ||
| 7541 | +**影响文件**: | ||
| 7542 | +- `.husky/pre-commit` | ||
| 7543 | +- `.husky/prepare-commit-msg` | ||
| 7544 | + | ||
| 7545 | +**变更摘要**: | ||
| 7546 | +- 无详细描述 | ||
| 7547 | + | ||
| 7548 | +### 10:51:49 - 完成任务 | ||
| 7549 | + | ||
| 7550 | +**影响文件**: | ||
| 7551 | +- `.husky/pre-commit` | ||
| 7552 | +- `.husky/prepare-commit-msg` | ||
| 7553 | + | ||
| 7554 | +**变更摘要**: | ||
| 7555 | +- 无详细描述 | ||
| 7556 | + | ||
| 7557 | +### 10:52:54 - 完成任务 | ||
| 7558 | + | ||
| 7559 | +**影响文件**: | ||
| 7560 | +- `.husky/pre-commit` | ||
| 7561 | +- `.husky/prepare-commit-msg` | ||
| 7562 | + | ||
| 7563 | +**变更摘要**: | ||
| 7564 | +- 无详细描述 | ||
| 7565 | + | ||
| 7566 | +### 10:53:57 - 完成任务 | ||
| 7567 | + | ||
| 7568 | +**影响文件**: | ||
| 7569 | +- `.husky/pre-commit` | ||
| 7570 | +- `.husky/prepare-commit-msg` | ||
| 7571 | + | ||
| 7572 | +**变更摘要**: | ||
| 7573 | +- 无详细描述 | ||
| 7574 | + | ||
| 7575 | +### 10:57:47 - 完成任务 | ||
| 7576 | + | ||
| 7577 | +**影响文件**: | ||
| 7578 | +- `.husky/pre-commit` | ||
| 7579 | +- `.husky/prepare-commit-msg` | ||
| 7580 | + | ||
| 7581 | +**变更摘要**: | ||
| 7582 | +- 无详细描述 | ||
| 7583 | + | ||
| 7584 | +### 10:58:24 - 完成任务 | ||
| 7585 | + | ||
| 7586 | +**影响文件**: | ||
| 7587 | +- `.husky/pre-commit` | ||
| 7588 | +- `.husky/prepare-commit-msg` | ||
| 7589 | + | ||
| 7590 | +**变更摘要**: | ||
| 7591 | +- 无详细描述 | ||
| 7592 | + | ||
| 7593 | +### 10:59:19 - 完成任务 | ||
| 7594 | + | ||
| 7595 | +**影响文件**: | ||
| 7596 | +- `.husky/pre-commit` | ||
| 7597 | +- `.husky/prepare-commit-msg` | ||
| 7598 | + | ||
| 7599 | +**变更摘要**: | ||
| 7600 | +- 无详细描述 | ||
| 7601 | + | ||
| 7602 | +### 10:59:46 - 完成任务 | ||
| 7603 | + | ||
| 7604 | +**影响文件**: | ||
| 7605 | +- `.husky/pre-commit` | ||
| 7606 | +- `.husky/prepare-commit-msg` | ||
| 7607 | + | ||
| 7608 | +**变更摘要**: | ||
| 7609 | +- 无详细描述 | ... | ... |
| ... | @@ -34,6 +34,7 @@ const App = createApp({ | ... | @@ -34,6 +34,7 @@ const App = createApp({ |
| 34 | setTimeout(() => { | 34 | setTimeout(() => { |
| 35 | const env = process.env.NODE_ENV || 'unknown' | 35 | const env = process.env.NODE_ENV || 'unknown' |
| 36 | const platform = process.env.TARO_ENV || 'unknown' | 36 | const platform = process.env.TARO_ENV || 'unknown' |
| 37 | + const isDev = env === 'development' | ||
| 37 | 38 | ||
| 38 | console.log('\n==================== 环境信息 ====================') | 39 | console.log('\n==================== 环境信息 ====================') |
| 39 | console.log(`📱 当前环境: ${env === 'development' ? '开发环境' : env === 'production' ? '生产环境' : env}`) | 40 | console.log(`📱 当前环境: ${env === 'development' ? '开发环境' : env === 'production' ? '生产环境' : env}`) |
| ... | @@ -59,14 +60,38 @@ const App = createApp({ | ... | @@ -59,14 +60,38 @@ const App = createApp({ |
| 59 | console.log('🔐 会话状态: 无法获取') | 60 | console.log('🔐 会话状态: 无法获取') |
| 60 | } | 61 | } |
| 61 | 62 | ||
| 62 | - // 打印用户登录状态 | 63 | + // 打印用户登录状态(生产环境不显示敏感信息) |
| 63 | console.log('\n==================== 用户信息 ====================') | 64 | console.log('\n==================== 用户信息 ====================') |
| 64 | if (userStore.isLoggedIn && userStore.userInfo) { | 65 | if (userStore.isLoggedIn && userStore.userInfo) { |
| 65 | console.log(`✅ 登录状态: 已登录`) | 66 | console.log(`✅ 登录状态: 已登录`) |
| 67 | + | ||
| 68 | + // 仅开发环境显示详细用户信息 | ||
| 69 | + if (isDev) { | ||
| 66 | console.log(`👤 用户ID: ${userStore.userInfo.id || '未知'}`) | 70 | console.log(`👤 用户ID: ${userStore.userInfo.id || '未知'}`) |
| 67 | console.log(`👤 用户名: ${userStore.userInfo.name || userStore.userInfo.nickname || '未知'}`) | 71 | console.log(`👤 用户名: ${userStore.userInfo.name || userStore.userInfo.nickname || '未知'}`) |
| 68 | console.log(`📱 手机号: ${userStore.userInfo.phone || userStore.userInfo.mobile || '未绑定'}`) | 72 | console.log(`📱 手机号: ${userStore.userInfo.phone || userStore.userInfo.mobile || '未绑定'}`) |
| 69 | } else { | 73 | } else { |
| 74 | + // 生产环境:只显示脱敏信息 | ||
| 75 | + const phone = userStore.userInfo.phone || userStore.userInfo.mobile | ||
| 76 | + const maskedPhone = phone ? `${phone.slice(0, 3)}****${phone.slice(-4)}` : '未绑定' | ||
| 77 | + | ||
| 78 | + // 用户名脱敏:保留首字和末字,中间用 * 代替 | ||
| 79 | + const userName = userStore.userInfo.name || userStore.userInfo.nickname || '' | ||
| 80 | + let maskedName = '***' | ||
| 81 | + if (userName && userName.length > 0) { | ||
| 82 | + if (userName.length === 1) { | ||
| 83 | + maskedName = '*' | ||
| 84 | + } else if (userName.length === 2) { | ||
| 85 | + maskedName = userName[0] + '*' | ||
| 86 | + } else { | ||
| 87 | + maskedName = userName[0] + '*'.repeat(userName.length - 2) + userName[userName.length - 1] | ||
| 88 | + } | ||
| 89 | + } | ||
| 90 | + | ||
| 91 | + console.log(`👤 用户名: ${maskedName}`) | ||
| 92 | + console.log(`📱 手机号: ${maskedPhone}`) | ||
| 93 | + } | ||
| 94 | + } else { | ||
| 70 | console.log(`❌ 登录状态: 未登录`) | 95 | console.log(`❌ 登录状态: 未登录`) |
| 71 | } | 96 | } |
| 72 | 97 | ... | ... |
| ... | @@ -17,23 +17,48 @@ import { PLAN_FIELD_DEFINITIONS, FIELD_GROUPS, getFieldsByGroup } from '@/config | ... | @@ -17,23 +17,48 @@ import { PLAN_FIELD_DEFINITIONS, FIELD_GROUPS, getFieldsByGroup } from '@/config |
| 17 | import { viewAPI } from '@/api/plan' | 17 | import { viewAPI } from '@/api/plan' |
| 18 | 18 | ||
| 19 | // Mock Taro API | 19 | // Mock Taro API |
| 20 | -vi.mock('@tarojs/taro', () => ({ | 20 | +vi.mock('@tarojs/taro', () => { |
| 21 | + const showToast = vi.fn() | ||
| 22 | + const showModal = vi.fn() | ||
| 23 | + const showLoading = vi.fn() | ||
| 24 | + const hideLoading = vi.fn() | ||
| 25 | + const showActionSheet = vi.fn() | ||
| 26 | + const navigateTo = vi.fn() | ||
| 27 | + const redirectTo = vi.fn() | ||
| 28 | + | ||
| 29 | + return { | ||
| 21 | default: { | 30 | default: { |
| 22 | - showToast: vi.fn(), | 31 | + showToast, |
| 23 | - showModal: vi.fn(), | 32 | + showModal, |
| 24 | - showLoading: vi.fn(), | 33 | + showLoading, |
| 25 | - hideLoading: vi.fn(), | 34 | + hideLoading, |
| 26 | - showActionSheet: vi.fn(), | 35 | + showActionSheet, |
| 27 | - navigateTo: vi.fn(), | 36 | + navigateTo, |
| 28 | - redirectTo: vi.fn() | 37 | + redirectTo |
| 38 | + }, | ||
| 39 | + // 导出命名导出以匹配 useFileOperation 中的用法 | ||
| 40 | + showToast, | ||
| 41 | + showModal, | ||
| 42 | + showLoading, | ||
| 43 | + hideLoading, | ||
| 44 | + showActionSheet, | ||
| 45 | + navigateTo, | ||
| 46 | + redirectTo | ||
| 29 | } | 47 | } |
| 30 | -})) | 48 | +}) |
| 31 | 49 | ||
| 32 | // Mock viewAPI | 50 | // Mock viewAPI |
| 33 | vi.mock('@/api/plan', () => ({ | 51 | vi.mock('@/api/plan', () => ({ |
| 34 | viewAPI: vi.fn() | 52 | viewAPI: vi.fn() |
| 35 | })) | 53 | })) |
| 36 | 54 | ||
| 55 | +// Mock useFileOperation | ||
| 56 | +vi.mock('@/composables/useFileOperation', () => ({ | ||
| 57 | + useFileOperation: () => ({ | ||
| 58 | + viewFile: vi.fn().mockResolvedValue(true) // 模拟文件预览成功 | ||
| 59 | + }) | ||
| 60 | +})) | ||
| 61 | + | ||
| 37 | describe('计划书模块集成测试', () => { | 62 | describe('计划书模块集成测试', () => { |
| 38 | beforeEach(() => { | 63 | beforeEach(() => { |
| 39 | vi.clearAllMocks() | 64 | vi.clearAllMocks() | ... | ... |
| ... | @@ -64,7 +64,7 @@ describe('搜索页面测试', () => { | ... | @@ -64,7 +64,7 @@ describe('搜索页面测试', () => { |
| 64 | expect(wrapper.vm.shouldEnableScrollLoad).toBe(false) | 64 | expect(wrapper.vm.shouldEnableScrollLoad).toBe(false) |
| 65 | expect(wrapper.vm.tabsData.length).toBe(2) | 65 | expect(wrapper.vm.tabsData.length).toBe(2) |
| 66 | expect(wrapper.vm.tabsData[0].id).toBe('product') | 66 | expect(wrapper.vm.tabsData[0].id).toBe('product') |
| 67 | - expect(wrapper.vm.tabsData[1].id).toBe('file') | 67 | + expect(wrapper.vm.tabsData[1].id).toBe('article') |
| 68 | }) | 68 | }) |
| 69 | 69 | ||
| 70 | it('空关键词时提示并保持未搜索状态', async () => { | 70 | it('空关键词时提示并保持未搜索状态', async () => { |
| ... | @@ -87,8 +87,8 @@ describe('搜索页面测试', () => { | ... | @@ -87,8 +87,8 @@ describe('搜索页面测试', () => { |
| 87 | list: [{ id: 1, product_name: '保险A', tags: [] }], | 87 | list: [{ id: 1, product_name: '保险A', tags: [] }], |
| 88 | total: 1 | 88 | total: 1 |
| 89 | }, | 89 | }, |
| 90 | - files: { | 90 | + article: { |
| 91 | - list: [{ id: 10, name: '培训资料', value: 'url', size: '1MB', extension: 'pdf', is_favorite: 1 }], | 91 | + list: [{ id: 10, post_title: '培训指南', post_excerpt: '这是培训资料简介', post_date: '2026-01-01', is_favorite: 1 }], |
| 92 | total: 1 | 92 | total: 1 |
| 93 | } | 93 | } |
| 94 | } | 94 | } |
| ... | @@ -114,9 +114,9 @@ describe('搜索页面测试', () => { | ... | @@ -114,9 +114,9 @@ describe('搜索页面测试', () => { |
| 114 | list: [{ id: 1, product_name: '保险A', tags: [] }], | 114 | list: [{ id: 1, product_name: '保险A', tags: [] }], |
| 115 | total: 1 | 115 | total: 1 |
| 116 | }, | 116 | }, |
| 117 | - files: { | 117 | + article: { |
| 118 | - list: [], | 118 | + list: [{ id: 20, post_title: '相关文章', post_excerpt: '摘要', post_date: '2026-01-01', is_favorite: 0 }], |
| 119 | - total: 0 | 119 | + total: 1 |
| 120 | } | 120 | } |
| 121 | } | 121 | } |
| 122 | }) | 122 | }) |
| ... | @@ -132,18 +132,18 @@ describe('搜索页面测试', () => { | ... | @@ -132,18 +132,18 @@ describe('搜索页面测试', () => { |
| 132 | list: [], | 132 | list: [], |
| 133 | total: 0 | 133 | total: 0 |
| 134 | }, | 134 | }, |
| 135 | - files: { | 135 | + article: { |
| 136 | - list: [{ id: 10, name: '培训资料', value: 'url', size: '1MB', extension: 'pdf', is_favorite: 0 }], | 136 | + list: [{ id: 10, post_title: '培训资料', post_excerpt: '摘要', post_date: '2026-01-01', is_favorite: 0 }], |
| 137 | total: 1 | 137 | total: 1 |
| 138 | } | 138 | } |
| 139 | } | 139 | } |
| 140 | }) | 140 | }) |
| 141 | 141 | ||
| 142 | - await wrapper.vm.onTabClick('file') | 142 | + await wrapper.vm.onTabClick('article') |
| 143 | 143 | ||
| 144 | - expect(wrapper.vm.activeTab).toBe('file') | 144 | + expect(wrapper.vm.activeTab).toBe('article') |
| 145 | expect(wrapper.vm.currentList.length).toBe(1) | 145 | expect(wrapper.vm.currentList.length).toBe(1) |
| 146 | - expect(searchAPI).toHaveBeenCalledWith({ keyword: '保险', page: 0, limit: 20, type: 'file' }) | 146 | + expect(searchAPI).toHaveBeenCalledWith({ keyword: '保险', page: 0, limit: 20, type: 'article' }) |
| 147 | }) | 147 | }) |
| 148 | 148 | ||
| 149 | it('清空搜索后重置状态', async () => { | 149 | it('清空搜索后重置状态', async () => { |
| ... | @@ -156,7 +156,7 @@ describe('搜索页面测试', () => { | ... | @@ -156,7 +156,7 @@ describe('搜索页面测试', () => { |
| 156 | list: [{ id: 1, product_name: '保险A', tags: [] }], | 156 | list: [{ id: 1, product_name: '保险A', tags: [] }], |
| 157 | total: 1 | 157 | total: 1 |
| 158 | }, | 158 | }, |
| 159 | - files: { | 159 | + article: { |
| 160 | list: [], | 160 | list: [], |
| 161 | total: 0 | 161 | total: 0 |
| 162 | } | 162 | } | ... | ... |
| ... | @@ -281,11 +281,11 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo | ... | @@ -281,11 +281,11 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo |
| 281 | : await searchAPI(params) | 281 | : await searchAPI(params) |
| 282 | 282 | ||
| 283 | if (res.code === 1) { | 283 | if (res.code === 1) { |
| 284 | - // 映射产品列表 | 284 | + // 映射产品列表(使用可选链防止后端字段缺失时抛错) |
| 285 | - const newProducts = res.data.products.list || [] | 285 | + const newProducts = res.data.products?.list || [] |
| 286 | 286 | ||
| 287 | // 映射文章列表(参考本周热门文章的映射方式) | 287 | // 映射文章列表(参考本周热门文章的映射方式) |
| 288 | - const newArticles = (res.data.article.list || []).map(item => ({ | 288 | + const newArticles = (res.data.article?.list || []).map(item => ({ |
| 289 | id: item.id, | 289 | id: item.id, |
| 290 | title: item.post_title || '未命名文章', | 290 | title: item.post_title || '未命名文章', |
| 291 | excerpt: item.post_excerpt || '', | 291 | excerpt: item.post_excerpt || '', |
| ... | @@ -307,8 +307,8 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo | ... | @@ -307,8 +307,8 @@ const performSearch = async (keyword, type, page = 0, limit = pageSize, isLoadMo |
| 307 | articles.value = newArticles | 307 | articles.value = newArticles |
| 308 | } | 308 | } |
| 309 | 309 | ||
| 310 | - productsTotal.value = res.data.products.total || 0 | 310 | + productsTotal.value = res.data.products?.total || 0 |
| 311 | - articlesTotal.value = res.data.article.total || 0 | 311 | + articlesTotal.value = res.data.article?.total || 0 |
| 312 | 312 | ||
| 313 | // ⚠️ 重要:必须先自动选择 tab,然后再计算 hasMore | 313 | // ⚠️ 重要:必须先自动选择 tab,然后再计算 hasMore |
| 314 | // 如果不传 type,自动选择有数据的 tab(仅首次搜索时) | 314 | // 如果不传 type,自动选择有数据的 tab(仅首次搜索时) | ... | ... |
| ... | @@ -106,15 +106,18 @@ function extractCookieFromResponse(response) { | ... | @@ -106,15 +106,18 @@ function extractCookieFromResponse(response) { |
| 106 | } | 106 | } |
| 107 | 107 | ||
| 108 | /** | 108 | /** |
| 109 | - * 标准化 cookie 数组并去重 | 109 | + * 标准化 cookie 数组并提取会话 cookie |
| 110 | - * @description 将 cookie 数组转换为字符串,只提取唯一的 cookie(忽略重复项) | 110 | + * @description 将 cookie 数组转换为字符串,优先提取包含会话标识的 cookie |
| 111 | * @param {string|string[]} cookies cookie 字符串或数组 | 111 | * @param {string|string[]} cookies cookie 字符串或数组 |
| 112 | * @returns {string|null} 标准化后的 cookie 字符串 | 112 | * @returns {string|null} 标准化后的 cookie 字符串 |
| 113 | * | 113 | * |
| 114 | * @example | 114 | * @example |
| 115 | - * normalizeCookies(['PHPSESSID=xxx; path=/', 'PHPSESSID=xxx; path=/']) | 115 | + * // 多个 Set-Cookie,优先返回包含 sessionid/PHPSESSID 的 |
| 116 | - * // 返回: 'PHPSESSID=xxx; path=/' | 116 | + * normalizeCookies(['sessionid=xxx; path=/', 'csrftoken=yyy; path=/']) |
| 117 | + * // 返回: 'sessionid=xxx; path=/' | ||
| 117 | * | 118 | * |
| 119 | + * @example | ||
| 120 | + * // 单个 cookie 字符串 | ||
| 118 | * normalizeCookies('PHPSESSID=xxx; path=/') | 121 | * normalizeCookies('PHPSESSID=xxx; path=/') |
| 119 | * // 返回: 'PHPSESSID=xxx; path=/' | 122 | * // 返回: 'PHPSESSID=xxx; path=/' |
| 120 | */ | 123 | */ |
| ... | @@ -126,12 +129,27 @@ function normalizeCookies(cookies) { | ... | @@ -126,12 +129,27 @@ function normalizeCookies(cookies) { |
| 126 | return cookies | 129 | return cookies |
| 127 | } | 130 | } |
| 128 | 131 | ||
| 129 | - // 如果是数组,只取第一个 cookie(忽略重复项) | 132 | + // 如果是数组,智能提取会话 cookie |
| 130 | if (Array.isArray(cookies)) { | 133 | if (Array.isArray(cookies)) { |
| 131 | - // 使用 Set 去重 | 134 | + // 去重 |
| 132 | const uniqueCookies = [...new Set(cookies)] | 135 | const uniqueCookies = [...new Set(cookies)] |
| 133 | 136 | ||
| 134 | - // 只返回第一个唯一的 cookie | 137 | + // 常见的会话 cookie 名称 |
| 138 | + const sessionCookieNames = ['sessionid', 'PHPSESSID', 'jsessionid', 'JSESSIONID', 'sid', 'SID'] | ||
| 139 | + | ||
| 140 | + // 优先查找包含会话标识的 cookie | ||
| 141 | + for (const cookie of uniqueCookies) { | ||
| 142 | + const cookieLower = cookie.toLowerCase() | ||
| 143 | + for (const name of sessionCookieNames) { | ||
| 144 | + if (cookieLower.includes(name.toLowerCase() + '=')) { | ||
| 145 | + console.log(`[openid] 找到会话 cookie: ${name}`) | ||
| 146 | + return cookie | ||
| 147 | + } | ||
| 148 | + } | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + // 如果没有找到会话 cookie,返回第一个(兼容旧逻辑) | ||
| 152 | + console.warn('[openid] 未找到会话 cookie,使用第一个 cookie') | ||
| 135 | return uniqueCookies[0] || null | 153 | return uniqueCookies[0] || null |
| 136 | } | 154 | } |
| 137 | 155 | ... | ... |
| ... | @@ -28,11 +28,13 @@ export const getSessionId = () => { | ... | @@ -28,11 +28,13 @@ export const getSessionId = () => { |
| 28 | 28 | ||
| 29 | /** | 29 | /** |
| 30 | * @description 设置 sessionid(一般不需要手动调用) | 30 | * @description 设置 sessionid(一般不需要手动调用) |
| 31 | - * - 正常情况下由 authRedirect.refreshSession 写入 | 31 | + * - 正常情况下由 authRedirect.refreshSession 或 miniProgramAuth 写入 |
| 32 | * - 保留该方法用于极端场景的手动修复/兼容旧逻辑 | 32 | * - 保留该方法用于极端场景的手动修复/兼容旧逻辑 |
| 33 | - * - 自动清理重复的 cookie(防止后端返回重复的 set-cookie) | ||
| 34 | * @param {string} sessionid cookie 字符串 | 33 | * @param {string} sessionid cookie 字符串 |
| 35 | * @returns {void} 无返回值 | 34 | * @returns {void} 无返回值 |
| 35 | + * | ||
| 36 | + * @note 输入的 sessionid 已经是标准化后的单个 cookie 字符串 | ||
| 37 | + * (由 openid.js 的 normalizeCookies 处理过),无需额外清理 | ||
| 36 | */ | 38 | */ |
| 37 | export const setSessionId = sessionid => { | 39 | export const setSessionId = sessionid => { |
| 38 | try { | 40 | try { |
| ... | @@ -40,67 +42,16 @@ export const setSessionId = sessionid => { | ... | @@ -40,67 +42,16 @@ export const setSessionId = sessionid => { |
| 40 | return | 42 | return |
| 41 | } | 43 | } |
| 42 | 44 | ||
| 43 | - // 自动清理重复的 cookie | 45 | + // 直接存储 sessionid |
| 44 | - // 例如:"PHPSESSID=xxx; ...,PHPSESSID=xxx; ..." -> "PHPSESSID=xxx; ..." | 46 | + // 注意:输入已经是标准化后的 cookie 字符串,不需要 split(',') 处理 |
| 45 | - const cleaned = cleanupDuplicateCookies(sessionid) | 47 | + // split(',') 会破坏 Expires 属性中的日期逗号(如 "Wed, 21 Oct 2025") |
| 46 | - | 48 | + Taro.setStorageSync('sessionid', sessionid) |
| 47 | - Taro.setStorageSync('sessionid', cleaned) | ||
| 48 | } catch (error) { | 49 | } catch (error) { |
| 49 | console.error('设置sessionid失败:', error) | 50 | console.error('设置sessionid失败:', error) |
| 50 | } | 51 | } |
| 51 | } | 52 | } |
| 52 | 53 | ||
| 53 | /** | 54 | /** |
| 54 | - * @description 清理重复的 cookie | ||
| 55 | - * @description 如果 cookie 字符串中有重复项,只保留第一个 | ||
| 56 | - * @param {string} cookies cookie 字符串 | ||
| 57 | - * @returns {string} 清理后的 cookie 字符串 | ||
| 58 | - * | ||
| 59 | - * @example | ||
| 60 | - * cleanupDuplicateCookies('PHPSESSID=xxx; ...,PHPSESSID=xxx; ...') | ||
| 61 | - * // 返回: 'PHPSESSID=xxx; ...' | ||
| 62 | - */ | ||
| 63 | -function cleanupDuplicateCookies(cookies) { | ||
| 64 | - if (!cookies || typeof cookies !== 'string') { | ||
| 65 | - return cookies | ||
| 66 | - } | ||
| 67 | - | ||
| 68 | - // 按逗号分割(重复的 cookie 用逗号连接) | ||
| 69 | - const parts = cookies.split(',') | ||
| 70 | - | ||
| 71 | - // 如果只有一个部分,直接返回 | ||
| 72 | - if (parts.length === 1) { | ||
| 73 | - return cookies | ||
| 74 | - } | ||
| 75 | - | ||
| 76 | - // 提取 cookie 名称(用于去重) | ||
| 77 | - const extractCookieName = (cookieStr) => { | ||
| 78 | - const match = cookieStr.match(/^([^=]+)=/) | ||
| 79 | - return match ? match[1].trim() : null | ||
| 80 | - } | ||
| 81 | - | ||
| 82 | - // 去重:只保留第一次出现的 cookie | ||
| 83 | - const seen = new Set() | ||
| 84 | - const uniqueParts = [] | ||
| 85 | - | ||
| 86 | - for (const part of parts) { | ||
| 87 | - const name = extractCookieName(part) | ||
| 88 | - if (name && !seen.has(name)) { | ||
| 89 | - seen.add(name) | ||
| 90 | - uniqueParts.push(part) | ||
| 91 | - } | ||
| 92 | - } | ||
| 93 | - | ||
| 94 | - // 如果去重后只剩一个,直接返回 | ||
| 95 | - if (uniqueParts.length === 1) { | ||
| 96 | - return uniqueParts[0] | ||
| 97 | - } | ||
| 98 | - | ||
| 99 | - // 否则用逗号连接 | ||
| 100 | - return uniqueParts.join(',') | ||
| 101 | -} | ||
| 102 | - | ||
| 103 | -/** | ||
| 104 | * @description 清空 sessionid(一般不需要手动调用) | 55 | * @description 清空 sessionid(一般不需要手动调用) |
| 105 | * @returns {void} 无返回值 | 56 | * @returns {void} 无返回值 |
| 106 | */ | 57 | */ |
| ... | @@ -153,7 +104,9 @@ const service = axios.create({ | ... | @@ -153,7 +104,9 @@ const service = axios.create({ |
| 153 | timeout: 5000, | 104 | timeout: 5000, |
| 154 | }) | 105 | }) |
| 155 | 106 | ||
| 156 | -let has_shown_timeout_modal = false | 107 | +// 网络提示状态管理 |
| 108 | +let last_timeout_prompt_time = 0 | ||
| 109 | +const TIMEOUT_PROMPT_COOLDOWN = 30000 // 30 秒内不重复提示 | ||
| 157 | 110 | ||
| 158 | /** | 111 | /** |
| 159 | * @description 判断是否为超时错误 | 112 | * @description 判断是否为超时错误 |
| ... | @@ -205,12 +158,21 @@ const should_handle_bad_network = async (error) => { | ... | @@ -205,12 +158,21 @@ const should_handle_bad_network = async (error) => { |
| 205 | 158 | ||
| 206 | /** | 159 | /** |
| 207 | * @description 处理请求超时/弱网错误 | 160 | * @description 处理请求超时/弱网错误 |
| 208 | - * - 弹出弱网提示 | 161 | + * - 使用时间窗口防抖,30 秒内不重复提示 |
| 162 | + * - 提示用户检查网络连接 | ||
| 209 | * @returns {Promise<void>} 无返回值 | 163 | * @returns {Promise<void>} 无返回值 |
| 210 | */ | 164 | */ |
| 211 | const handle_request_timeout = async () => { | 165 | const handle_request_timeout = async () => { |
| 212 | - if (has_shown_timeout_modal) return | 166 | + const now = Date.now() |
| 213 | - has_shown_timeout_modal = true | 167 | + |
| 168 | + // 检查是否在冷却时间内 | ||
| 169 | + if (now - last_timeout_prompt_time < TIMEOUT_PROMPT_COOLDOWN) { | ||
| 170 | + console.log('[request] 网络提示在冷却期内,跳过') | ||
| 171 | + return | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + // 更新上次提示时间 | ||
| 175 | + last_timeout_prompt_time = now | ||
| 214 | 176 | ||
| 215 | // 提示用户检查网络连接 | 177 | // 提示用户检查网络连接 |
| 216 | try { | 178 | try { | ... | ... |
-
Please register or login to post a comment