🔀 合并 fix/import-error-message-blob

修复导入失败时前端误报「系统错误」,根因有两层:
- axios 的 validateStatus 允许 200-500,HTTP 400 走的是成功分支而不是错误分支;
- 导入请求用 responseType='blob',res.data 是 Blob,读不到 code/msg,
  于是状态码退回 res.status、文案落到「系统错误」兜底。

改法:按 Content-Type 判断,声明为 JSON 的响应体才把 Blob 读成文本解析回
对象;真·文件流(Excel 明细)保持 Blob 原样走下载分支。
另统一 notifiedError() 给 Error 打 _notified 标记,业务层据此避免同一条
错误被弹两次。
This commit is contained in:
2026-09-26 15:32:00 +08:00
2 changed files with 89 additions and 13 deletions
+56 -12
View File
@@ -70,11 +70,44 @@ const redirectToLogin = () => {
store.dispatch('FedLogOut').then(() => router.push({ path: '/login' }));
};
/**
* 统一弹出错误提示并返回带标记的 Error。
* `_notified` 标记用于让业务层(批量导入等)识别「拦截器已经提示过」,
* 避免同一条错误被弹两次。
*/
const notifiedError = message => {
ElMessage({
message: message,
type: 'error',
});
const error = new Error(message);
error._notified = true;
return error;
};
axios.defaults.timeout = 10000;
//返回其他状态码
axios.defaults.validateStatus = function (status) {
return status >= 200 && status <= 500; // 默认的
};
/**
* 判断响应是否为 JSON。
* 用于 blob 请求(导入/导出)的兜底:后端在业务校验失败时返回的是 JSON
* (Content-Type: application/json),而不是文件流,需要解析后才能取到 msg。
*/
const isJsonContentType = res => {
const headers = res.headers || {};
const contentType = String(headers['content-type'] || res.data?.type || '').toLowerCase();
return contentType.includes('application/json') || contentType.includes('text/json');
};
/**
* 判断响应体是否已是解析好的对象(无需再解析)。
* 注意 Blob 也是 object,必须排除,否则会把待解析的文件流误判为已完成。
*/
const isPlainObject = value =>
value !== null && typeof value === 'object' && !(value instanceof Blob);
//跨域请求,允许保存cookie
axios.defaults.withCredentials = true;
// NProgress Configuration
@@ -140,7 +173,7 @@ axios.interceptors.request.use(
//http response拦截
axios.interceptors.response.use(
res => {
async res => {
NProgress.done();
//获取配置信息
const config = res.config;
@@ -149,6 +182,22 @@ axios.interceptors.response.use(
if (cryptoData) {
res.data = JSON.parse(crypto.decryptAES(res.data, crypto.aesKey));
}
// blob 请求(文件下载、导入)的响应体是 Blob,直接读 res.data.code / res.data.msg
// 拿不到任何字段:状态码会退回 res.status、提示文案会落到「系统错误」兜底,
// 用户就会以为系统崩了——而后端其实返回了明确的业务提示(如「未读取到数据行…」)。
// 这里按 Content-Type 判断:JSON 才解析回对象;真·文件流(Excel 等)保持原样不动。
// 兼容 Blob(浏览器 responseType='blob')与 string(非浏览器环境)两种形态。
if (isJsonContentType(res) && !isPlainObject(res.data)) {
try {
const text = res.data instanceof Blob ? await res.data.text() : res.data;
const parsed = typeof text === 'string' && text ? JSON.parse(text) : null;
if (parsed && typeof parsed === 'object') {
res.data = parsed;
}
} catch (ignored) {
// 声明是 JSON 却解析失败:保持原样,交给下游按文件处理
}
}
//获取状态信息
const status = res.data.error_code || res.data.code || res.status;
const statusWhiteList = website.statusWhiteList || [];
@@ -204,26 +253,21 @@ axios.interceptors.response.use(
// OAuth2 业务错误码提示
if (status > 2000 && !validatenull(res.data.error_description)) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
return Promise.reject(notifiedError(message));
}
// 其余非 200 响应统一提示
if (status !== 200) {
ElMessage({
message: message,
type: 'error',
});
return Promise.reject(new Error(message));
return Promise.reject(notifiedError(message));
}
return res;
},
error => {
NProgress.done();
return Promise.reject(new Error(error));
// 保留原始 axios 错误对象:下游(如导入失败明细解析)需要 error.response
// 读取响应体。若包装成 new Error(error),response 会丢失、message 退化成
// "[object Object]",用户只能看到无意义的通用提示。
return Promise.reject(error);
}
);
+33 -1
View File
@@ -28,6 +28,34 @@ const parseBlobJson = async blob => {
return text ? JSON.parse(text) : {};
};
/**
* 解析导入失败时的错误提示文案。
*
* 背景:导入接口统一用 `responseType: 'blob'` 请求,因此**非 2xx 响应体也是 Blob**。
* axios 的错误分支不做解析,直接 reject,`error.message` 只有
* 「Request failed with status code 400」这类通用文案(或全局兜底的「系统错误」),
* 用户会误以为系统崩了——而后端其实已返回了明确的中文业务提示(如「导入数据不能为空」)。
*
* 这里补上这一步:从错误响应的 Blob 里取出后端 `msg`,取不到再退回通用文案。
*/
const resolveErrorMessage = async error => {
const blob = error?.response?.data ?? error?.data;
if (blob instanceof Blob) {
try {
const result = await parseBlobJson(blob);
if (result?.msg) return result.msg;
} catch (ignored) {
// 响应体不是 JSON(如网关返回的 HTML 错误页),退回通用文案
}
}
const fallback = error?.response?.data?.msg || error?.message;
// axios 的英文通用文案对用户没有意义,统一换成中文提示
if (!fallback || /Request failed with status code/i.test(fallback)) {
return '导入失败,请检查导入文件后重试';
}
return fallback;
};
/**
* 加工后端返回的导入失败明细 Excel。
* 若传入 failDetailDecorator(函数),则动态加载 exceljs 读取 blob 流,
@@ -125,7 +153,11 @@ export const handleImportExcel = async (vm, option, column, businessName, onSucc
option.onSuccess(res);
}
} catch (error) {
vm.$message.error(error.message || '导入失败');
// 拦截器已提示过的错误(如后端 400 返回的业务提示)不再重复弹一次,
// 只保留一次清晰的中文提示。
if (!error?._notified) {
vm.$message.error(await resolveErrorMessage(error));
}
if (typeof option.onError === 'function') {
option.onError(error);
}