refactor(http, diy): 抽象通用网络错误处理逻辑

1. 抽离通用http错误分类器到单独模块,实现错误处理复用
2. 统一多语言文案,将diy专属错误合并到通用http命名空间
3. 简化diy页面的错误处理代码,降低维护成本
This commit is contained in:
2026-07-18 16:18:35 +08:00
parent 71ea945743
commit 9d6c3218fb
4 changed files with 133 additions and 48 deletions

View File

@@ -1,6 +1,7 @@
import WxMap from 'common/js/map-wx-jssdk.js';
import Config from '@/common/js/config.js';
import util from '@/common/js/util.js';
import { classifyHttpError, httpErrorKey } from '@/common/js/http-error.js';
let systemInfo = util.getDeviceInfo();
@@ -326,30 +327,19 @@ export default {
// 根据错误信息组织更详细的提示文案
// 说明:remote-only 模式下 $lang 可能返回空串,故统一用 `|| 中文兜底` 保证非空
_buildDiyErrorMsg(err) {
// 无错误信息 -> 通用网络异常
if (!err) return this.$lang('diy.networkError');
// 服务端明确返回的业务消息(字符串)
if (typeof err === 'string') return err;
const info = classifyHttpError(err);
const statusCode = err.statusCode;
const errMsg = err.errMsg || err.message || '';
let reason;
// 服务端明确返回的业务消息(字符串):直接透传
if (info.type === 'business') return info.raw;
if (statusCode) {
// HTTP 状态码类错误
if (statusCode === 404) reason = this.$lang('diy.errNotFound');
else if (statusCode >= 500) reason = this.$lang('diy.errServer');
else if (statusCode === 401 || statusCode === 403) reason = this.$lang('diy.errAuth');
else reason = this.$lang('diy.errRequest');
reason += '(' + statusCode + ')';
} else if (/timeout/i.test(errMsg)) {
reason = this.$lang('diy.errTimeout');
} else if (/(connect|network|fail)/i.test(errMsg)) {
reason = this.$lang('diy.errNetwork');
} else {
// 未知错误 -> 通用网络异常
return this.$lang('diy.networkError');
}
const key = diyErrorKey(info.type);
let reason = this.$lang(key);
// HTTP 状态码类错误:把 statusCode 拼到 reason 后面
if (info.statusCode) reason += '(' + info.statusCode + ')';
// empty / unknown 不走模板,直接返回通用网络异常
if (info.type === 'empty' || info.type === 'unknown') return reason;
const tpl = this.$lang('diy.loadFailTpl');
return tpl.replace('{reason}', reason);

101
common/js/http-error.js Normal file
View File

@@ -0,0 +1,101 @@
/**
* 通用网络/接口错误分类器
*
* 适用场景:
* - uniapp 各端统一错误分类(diy.js / 商品 / 订单 / 自定义页面等)
* - 任意传入 {statusCode, errMsg, message} 形式错误对象的位置
*
* 设计要点:
* 1) 与 Vue / $lang 解耦:不调用任何 i18n 函数,只返回分类结果 + 原始错误字段
* 调用方拿到 type 后,自行决定使用 $lang('http.timeout') / $lang('http.serverError')
* 或其它命名空间的文案,完全保持各模块原有的 key 命名习惯
* 2) 通用网络/接口错误统一映射到 common.http.* 命名空间:
* - http.networkError: 通用网络异常(兜底)
* - http.timeout: 请求超时
* - http.serverError: 服务器错误(5xx)
* - http.notFound: 接口不存在(404)
* - http.auth: 无访问权限(401/403)
* - http.unknown: 其他非 2xx
* 3) 兼容多种错误对象形态:
* - uniapp request: { statusCode, errMsg, data }
* - axios: { response: { status }, message }
* - fetch / 原生: { message, name }
*
* @param {Object|string} err 错误对象或后端业务消息字符串
* @returns {{ type: string, statusCode?: number, errMsg?: string, raw?: any }}
* - type 取值:
* 'business' - 后端返回的业务消息字符串(已透传到 raw)
* 'empty' - 错误对象为空
* 'notFound' - HTTP 404
* 'server' - HTTP 5xx
* 'auth' - HTTP 401 / 403
* 'request' - 其它非 2xx 状态码
* 'timeout' - errMsg 含 timeout
* 'network' - errMsg 含 connect / network / fail
* 'unknown' - 无法分类
* - statusCode: 若有则原样返回
* - errMsg: 归一后的原始消息
* - raw: 原始输入,方便调用方进一步处理
*/
export function classifyHttpError(err) {
// 1) 无错误对象
if (!err) {
return { type: 'empty', raw: err };
}
// 2) 后端直接返回的业务消息(字符串)
if (typeof err === 'string') {
return { type: 'business', raw: err };
}
// 3) 字段归一(兼容 uni / axios / 原生 Error)
const statusCode = err.statusCode
?? err.response?.status
?? err.status;
const errMsg = err.errMsg || err.message || '';
if (statusCode) {
if (statusCode === 404) return { type: 'notFound', statusCode, errMsg, raw: err };
if (statusCode >= 500) return { type: 'server', statusCode, errMsg, raw: err };
if (statusCode === 401 || statusCode === 403) return { type: 'auth', statusCode, errMsg, raw: err };
return { type: 'request', statusCode, errMsg, raw: err };
}
if (/timeout/i.test(errMsg)) {
return { type: 'timeout', errMsg, raw: err };
}
if (/(connect|network|fail)/i.test(errMsg)) {
return { type: 'network', errMsg, raw: err };
}
return { type: 'unknown', errMsg, raw: err };
}
/**
* 根据 classifyHttpError 的结果,映射到 common.http.* 命名空间的 i18n key
*
* 通用网络/接口错误统一走 http.*,调用方通过 $lang(key) 解析文案。
* 原 diy.errTimeout 这类 key 已合并到 http.*,保持项目国际化更专业。
*
* @param {string} type classifyHttpError 返回的 type
* @returns {string|null} 对应的 http.* key;type='business' 时返回 null(透传 raw)
*/
export function httpErrorKey(type) {
switch (type) {
case 'empty': return 'http.networkError';
case 'business': return null; // 透传,不用 key
case 'notFound': return 'http.notFound';
case 'server': return 'http.serverError';
case 'auth': return 'http.auth';
case 'request': return 'http.unknown';
case 'timeout': return 'http.timeout';
case 'network': return 'http.networkError';
case 'unknown':
default: return 'http.networkError';
}
}
export default {
classifyHttpError,
httpErrorKey,
};

View File

@@ -54,21 +54,14 @@ export const lang = {
serviceUnavailable: 'Navigation service temporarily unavailable',
},
// diy custom page
// diy custom page (diy-specific copy, generic HTTP errors reuse http.*)
diy: {
networkError: 'Network error, please try again later',
networkErrorTitle: 'Notice',
retry: 'Retry',
noDiyData: 'Custom page data not configured',
locationNotAvailable: 'Location not available',
// detailed data-load failure messages
// detailed data-load failure template (diy-specific, {reason} is filled by generic http.*)
loadFailTpl: 'Failed to load page data: {reason}, please try again later',
errNotFound: 'API not found',
errServer: 'Server error',
errAuth: 'No access permission',
errRequest: 'Request failed',
errTimeout: 'Request timeout',
errNetwork: 'Network connection failed',
},
//customer service
@@ -144,13 +137,17 @@ export const lang = {
requestFailed: '{url} request failed. {err}',
// network timeout
timeout: 'Network request timeout, please try again later',
// network connection failed
networkError: 'Network connection failed, please check your network',
// network connection failed / generic network error (unified copy, reused by diy and other generic scenarios)
networkError: 'Network error, please try again later',
// server error
serverError: 'Service temporarily unavailable, please try again later',
serverError: 'Server error, please try again later',
// API not found (HTTP 404)
notFound: 'API not found',
// no access permission (HTTP 401 / 403)
auth: 'No access permission',
// request aborted
aborted: 'Request cancelled',
// unknown error (errorType fallback, e.g. non-5xx HTTP status like 404)
// unknown error (errorType fallback, e.g. non-5xx/404/401/403 HTTP status)
unknown: 'Request failed, please try again later',
// unknown error
unknownError: 'Request failed, please try again later',

View File

@@ -61,21 +61,14 @@ export const lang = {
weChatWorkKefu: '企业微信客服',
},
// diy 自定义页面
// diy 自定义页面(diy 专属文案,通用网络/接口错误复用 http.*)
diy: {
networkError: '网络异常,请稍后重试',
networkErrorTitle: '提示',
retry: '重试',
noDiyData: '未配置自定义页面数据',
locationNotAvailable: '未获取到定位',
// 数据加载失败详细文案
// 数据加载失败详细文案(diy 专属模板,内部 {reason} 由通用 http.* 填充)
loadFailTpl: '页面数据加载失败:{reason},请稍后重试',
errNotFound: '接口不存在',
errServer: '服务器异常',
errAuth: '无访问权限',
errRequest: '请求失败',
errTimeout: '请求超时',
errNetwork: '网络连接失败',
},
// 客服服务
@@ -144,13 +137,17 @@ export const lang = {
requestFailed: '{url}请求失败. {err}',
// 网络超时
timeout: '网络请求超时,请稍后重试',
// 网络连接失败
networkError: '网络连接失败,请检查网络设置',
// 网络连接失败 / 通用网络异常(统一文案,供 diy 等通用场景复用)
networkError: '网络异常,请稍后重试',
// 服务器错误
serverError: '服务暂时不可用,请稍后重试',
serverError: '服务器异常,请稍后重试',
// 接口不存在(HTTP 404)
notFound: '接口不存在',
// 无访问权限(HTTP 401 / 403)
auth: '无访问权限',
// 请求被中断
aborted: '请求已取消',
// 未知错误(errorType 分类兜底,如非 5xx 的 HTTP 状态码 404 等)
// 未知错误(errorType 分类兜底,如非 5xx/404/401/403 的 HTTP 状态码)
unknown: '请求失败,请稍后重试',
// 未知错误
unknownError: '请求失败,请稍后重试',