From dfd82e18860b6e4bd32097b31c4c86a3a95bd0af Mon Sep 17 00:00:00 2001 From: let5sne Date: Sat, 18 Jul 2026 04:24:45 +0800 Subject: [PATCH] fix: unify all aihr collations, stop silent logout on biz 403, enable knowledge apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 线上现象:「问」页语音提问报 401(请求不带 token),用户观察为 「点了几个页面后登录状态失效」。 根因链(逐环生产验证): 1. 问师傅 /api/knowledge/query 对真实登录返回业务 403 「当前登录端未启用知识问答应用」——生产 aihr_knowledge_app 为空表, 多租户知识平台的应用注册/空间绑定/角色授权从未在生产配置(P1 遗留)。 2. 前端 readPayload 把 403 也视为认证失败并 clearAuth() —— 业务 403 触发静默登出,localStorage token 被清。 3. 之后语音 ASR 请求裸奔 -> 401「未能读取到有效 token」。 同期回归:凌晨对 aihr_practice_assignment.ext_party_id 的单列 collation 热修复,使其与 aihr_practice_session(仍 0900_ai_ci)的 JOIN 反向失配, /practice/mistakes 500。 修复: - backend/script/sql/update/aihr_20260718_unify_all_collations_mysql8.sql: 47 张 aihr 表全部 CONVERT 到运行时基准 collation(生产=general_ci), 终结 collation 打地鼠;本地验证后已在生产执行 (schema 备份 /opt/wygj/backups/aihr-schema-before-unify-20260718041407.sql), 13 项移动端接口回归全 200(含 mistakes)。 - api.ts/auth.ts:本地登录态仅在真正 401(HTTP 或业务码)时清除,并经 redirectToLoginForAuthLoss 记住当前页自动回登录(2s 防抖,登录页内不跳); 业务 403(权限拒绝,如录音归属/知识空间)只报错不登出; authenticatedFileUrl 同步收紧(403 不再清登录态)。 - 生产数据配置(幂等 INSERT,业务范围保守): · aihr_knowledge_app 注册 yc_admin(pc)/yc_mobile(app) SESSION 应用 · yc_mobile 绑定 SOP 域 5 库(1001/1002/1003/1201/1202), employee/supervisor ROLE READ;yc_admin 绑定全部 13 库, hr_operator/superadmin MANAGE · 财务/销售/行政等大库(1004-1010)暂仅管理端,待业务确认再放开 生产验证: - 问师傅 query 200,真实回答+引用「投诉处理 SOP v1.0」 - E2E:登录后全 tab 浏览+语音+文字提问,token 全程稳定(storage 打桩零触发) - 无效 token 点语音提问 -> 自动回登录页(原为死胡同 toast) - H5 已发布(备份 h5-before-20260718042238) Co-Authored-By: Claude Fable 5 --- ...r_20260718_unify_all_collations_mysql8.sql | 121 ++++++++++++++++++ mobile-uni/src/services/api.ts | 22 +++- mobile-uni/src/services/auth.ts | 23 ++++ 3 files changed, 161 insertions(+), 5 deletions(-) create mode 100644 backend/script/sql/update/aihr_20260718_unify_all_collations_mysql8.sql diff --git a/backend/script/sql/update/aihr_20260718_unify_all_collations_mysql8.sql b/backend/script/sql/update/aihr_20260718_unify_all_collations_mysql8.sql new file mode 100644 index 00000000..7ae345c9 --- /dev/null +++ b/backend/script/sql/update/aihr_20260718_unify_all_collations_mysql8.sql @@ -0,0 +1,121 @@ +-- Unify the collation of EVERY aihr table (tables and all string columns). +-- +-- History: the 2026-07-18 release aligned the 18 newly added tables to the +-- collation of aihr_knowledge_info.tenant_id, and a same-day hotfix aligned +-- aihr_practice_assignment.ext_party_id. Both were partial: older tables kept +-- utf8mb4_0900_ai_ci columns (e.g. aihr_practice_session.ext_party_id), so any +-- join between the two families still raises error 1267 +-- (Illegal mix of collations). Seen in production on +-- /api/aihr/mobile/practice/assignments and /api/aihr/mobile/practice/mistakes. +-- +-- This migration converts ALL aihr tables to the canonical collation derived +-- at run time, ending the per-join whack-a-mole. CONVERT TO rewrites the table +-- default plus every character column, and is idempotent. + +SET @aihr_uni_collation := ( + SELECT collation_name + FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'aihr_knowledge_info' + AND column_name = 'tenant_id' + LIMIT 1 +); +SET @aihr_uni_collation := COALESCE(@aihr_uni_collation, 'utf8mb4_0900_ai_ci'); + +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_candidate_employee_link` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_candidate_material` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_case_record` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_interview_result` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_answer_feedback` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_attach` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_fragment` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_gap` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_import_task` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_info` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_upload_item` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_model_config` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_model_provider` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_onboard_task` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_org_snapshot` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_position_responsibility` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_annotation` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_assignment` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_audio` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_audio_upload` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_calibration` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_rubric` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_rubric_dimension` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_scenario` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_session` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_prompt_template` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_qualification_gate` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_sop_answer_review` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_sop_applicability` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_community_answer` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_community_question` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_incentive_rule` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_admin_audit` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_app` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_app_space` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_query_log` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_knowledge_space_grant` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_learning_question` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_answer` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_attempt` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_question` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_onboard_exam_target` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_points_ledger` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_practice_question_feedback` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_web_ai_query_audit` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; +SET @aihr_uni_ddl := CONCAT('ALTER TABLE `aihr_web_search_provider` CONVERT TO CHARACTER SET utf8mb4 COLLATE ', @aihr_uni_collation); +PREPARE aihr_uni_stmt FROM @aihr_uni_ddl; EXECUTE aihr_uni_stmt; DEALLOCATE PREPARE aihr_uni_stmt; + +SET @aihr_uni_ddl := NULL; +SET @aihr_uni_collation := NULL; diff --git a/mobile-uni/src/services/api.ts b/mobile-uni/src/services/api.ts index c2fe5ee9..01255a11 100644 --- a/mobile-uni/src/services/api.ts +++ b/mobile-uni/src/services/api.ts @@ -1,5 +1,5 @@ import type { ApiPayload } from '@/types/api'; -import { clearAuth, getAuth } from './auth'; +import { clearAuth, getAuth, redirectToLoginForAuthLoss } from './auth'; const apiBase = import.meta.env.VITE_API_BASE || '/dev-api'; @@ -48,8 +48,11 @@ export const readPayload = (statusCode: number, data: unknown, clearAuthOnUna const payload = data as ApiPayload; const code = typeof payload?.code === 'number' ? payload.code : undefined; if (statusCode < 200 || statusCode >= 300 || (code !== undefined && code !== 200)) { - if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403 || code === 401 || code === 403)) { + // Only a real login failure (401) drops the local session; business 403 + // (permission denied) must surface as a message, never a silent logout. + if (clearAuthOnUnauthorized && (statusCode === 401 || code === 401)) { clearAuth(); + redirectToLoginForAuthLoss(); } throw new ApiError(payload?.msg || `HTTP ${statusCode}`, statusCode, code); } @@ -61,7 +64,10 @@ export const parsePayloadText = (statusCode: number, text: string, clearAuthOnUn try { return JSON.parse(text || '{}'); } catch (error) { - if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403)) clearAuth(); + if (clearAuthOnUnauthorized && statusCode === 401) { + clearAuth(); + redirectToLoginForAuthLoss(); + } throw error instanceof SyntaxError ? new ApiError(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败', statusCode) : error; @@ -138,7 +144,10 @@ export const authenticatedFileUrl = async (url: string): Promise => { resolve(URL.createObjectURL(xhr.response)); return; } - if (xhr.status === 401 || xhr.status === 403) clearAuth(); + if (xhr.status === 401) { + clearAuth(); + redirectToLoginForAuthLoss(); + } reject(new Error(`HTTP ${xhr.status}`)); }; xhr.onerror = () => reject(new Error('网络请求失败')); @@ -155,7 +164,10 @@ export const authenticatedFileUrl = async (url: string): Promise => { resolve(response.tempFilePath); return; } - if (response.statusCode === 401 || response.statusCode === 403) clearAuth(); + if (response.statusCode === 401) { + clearAuth(); + redirectToLoginForAuthLoss(); + } reject(new Error(`HTTP ${response.statusCode}`)); }, fail: () => reject(new Error('网络请求失败')) diff --git a/mobile-uni/src/services/auth.ts b/mobile-uni/src/services/auth.ts index a9ac1f58..c4d0c8e1 100644 --- a/mobile-uni/src/services/auth.ts +++ b/mobile-uni/src/services/auth.ts @@ -160,6 +160,29 @@ export const rememberLoginRedirect = (url: string) => { uni.setStorageSync(loginRedirectKey, url); }; +let lastAuthLossRedirectAt = 0; + +/** + * Central recovery for a lost login (HTTP/biz 401): remember the current page, + * then relaunch to login. Debounced because parallel requests can 401 together. + * Business 403 must NOT end up here — permission denials are not logouts. + */ +export const redirectToLoginForAuthLoss = () => { + const now = Date.now(); + if (now - lastAuthLossRedirectAt < 2000) return; + lastAuthLossRedirectAt = now; + try { + const pages = getCurrentPages(); + const current = pages[pages.length - 1] as { route?: string; $page?: { fullPath?: string } } | undefined; + const path = current?.$page?.fullPath || (current?.route ? `/${current.route}` : ''); + if (path.startsWith('/pages/auth/')) return; + if (path.startsWith('/pages/')) uni.setStorageSync(loginRedirectKey, path); + } catch { + // Best effort only; still return to login below. + } + uni.reLaunch({ url: '/pages/auth/login/index' }); +}; + export const consumeLoginRedirect = () => { const url = uni.getStorageSync(loginRedirectKey) || ''; uni.removeStorageSync(loginRedirectKey);