fix: unify all aihr collations, stop silent logout on biz 403, enable knowledge apps

线上现象:「问」页语音提问报 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 <noreply@anthropic.com>
This commit is contained in:
2026-07-18 04:24:45 +08:00
co-authored by Claude Fable 5
parent a4bbfc2222
commit dfd82e1886
3 changed files with 161 additions and 5 deletions
@@ -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;
+17 -5
View File
@@ -1,5 +1,5 @@
import type { ApiPayload } from '@/types/api'; 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'; const apiBase = import.meta.env.VITE_API_BASE || '/dev-api';
@@ -48,8 +48,11 @@ export const readPayload = <T>(statusCode: number, data: unknown, clearAuthOnUna
const payload = data as ApiPayload<T>; const payload = data as ApiPayload<T>;
const code = typeof payload?.code === 'number' ? payload.code : undefined; const code = typeof payload?.code === 'number' ? payload.code : undefined;
if (statusCode < 200 || statusCode >= 300 || (code !== undefined && code !== 200)) { 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(); clearAuth();
redirectToLoginForAuthLoss();
} }
throw new ApiError(payload?.msg || `HTTP ${statusCode}`, statusCode, code); throw new ApiError(payload?.msg || `HTTP ${statusCode}`, statusCode, code);
} }
@@ -61,7 +64,10 @@ export const parsePayloadText = (statusCode: number, text: string, clearAuthOnUn
try { try {
return JSON.parse(text || '{}'); return JSON.parse(text || '{}');
} catch (error) { } catch (error) {
if (clearAuthOnUnauthorized && (statusCode === 401 || statusCode === 403)) clearAuth(); if (clearAuthOnUnauthorized && statusCode === 401) {
clearAuth();
redirectToLoginForAuthLoss();
}
throw error instanceof SyntaxError throw error instanceof SyntaxError
? new ApiError(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败', statusCode) ? new ApiError(statusCode >= 400 ? `HTTP ${statusCode}` : '响应解析失败', statusCode)
: error; : error;
@@ -138,7 +144,10 @@ export const authenticatedFileUrl = async (url: string): Promise<string> => {
resolve(URL.createObjectURL(xhr.response)); resolve(URL.createObjectURL(xhr.response));
return; return;
} }
if (xhr.status === 401 || xhr.status === 403) clearAuth(); if (xhr.status === 401) {
clearAuth();
redirectToLoginForAuthLoss();
}
reject(new Error(`HTTP ${xhr.status}`)); reject(new Error(`HTTP ${xhr.status}`));
}; };
xhr.onerror = () => reject(new Error('网络请求失败')); xhr.onerror = () => reject(new Error('网络请求失败'));
@@ -155,7 +164,10 @@ export const authenticatedFileUrl = async (url: string): Promise<string> => {
resolve(response.tempFilePath); resolve(response.tempFilePath);
return; return;
} }
if (response.statusCode === 401 || response.statusCode === 403) clearAuth(); if (response.statusCode === 401) {
clearAuth();
redirectToLoginForAuthLoss();
}
reject(new Error(`HTTP ${response.statusCode}`)); reject(new Error(`HTTP ${response.statusCode}`));
}, },
fail: () => reject(new Error('网络请求失败')) fail: () => reject(new Error('网络请求失败'))
+23
View File
@@ -160,6 +160,29 @@ export const rememberLoginRedirect = (url: string) => {
uni.setStorageSync(loginRedirectKey, url); 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 = () => { export const consumeLoginRedirect = () => {
const url = uni.getStorageSync(loginRedirectKey) || ''; const url = uni.getStorageSync(loginRedirectKey) || '';
uni.removeStorageSync(loginRedirectKey); uni.removeStorageSync(loginRedirectKey);