fix: localize customer-facing errors
Deploy olons-server production / deploy (push) Successful in 1m48s

This commit is contained in:
richjohnrambo
2026-10-24 19:21:58 +08:00
parent 7e1b4a721e
commit 357ac35434
4 changed files with 182 additions and 7 deletions
+11 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { dateTime, dateTimeSeconds } from './api';
import { dateTime, dateTimeSeconds, errorText } from './api';
describe('Beijing business time display', () => {
it.each(['UTC', 'America/New_York', 'Asia/Shanghai'])(
@@ -28,3 +28,13 @@ describe('Beijing business time display', () => {
expect(dateTimeSeconds('invalid-time')).toBe('--');
});
});
describe('Chinese error display', () => {
it('keeps Chinese business messages', () => {
expect(errorText(new Error('登录密码不正确'))).toBe('登录密码不正确');
});
it('does not expose raw English errors to users', () => {
expect(errorText(new Error('login password is invalid'))).toBe('操作失败,请稍后重试');
});
});
+56 -5
View File
@@ -5,33 +5,75 @@ const DEVICE_KEY = 'olons_web_device';
const LOGIN_NOTICE_PENDING_KEY = 'olons_login_notice_pending';
const reasonMessages: Record<string, string> = {
MALFORMED_REQUEST: '请求参数格式错误',
METHOD_NOT_ALLOWED: '当前请求方式不支持',
AUTH_REQUIRED: '登录已失效,请重新登录',
FORBIDDEN: '当前账户无权执行此操作',
USERNAME_INVALID_FORMAT: '用户名格式不正确',
PHONE_INVALID_FORMAT: '手机号码格式不正确',
PASSWORD_INVALID_FORMAT: '密码格式不正确',
INVALID_CREDENTIALS: '手机号或密码错误',
ACCOUNT_ALREADY_REGISTERED: '该账户或手机号已经注册',
REFERRAL_CODE_INVALID: '邀请码无效,请检查后重新输入',
PRIVACY_POLICY_NOT_ACCEPTED: '请先阅读并同意用户协议和隐私政策',
RATE_LIMITED: '操作过于频繁,请稍后再试',
AVATAR_ATTACHMENT_INVALID: '头像图片无效,请重新上传',
CURRENT_PASSWORD_INVALID: '当前登录密码不正确',
LOGIN_PASSWORD_INVALID: '登录密码不正确',
NEW_PASSWORD_INVALID: '新密码格式不正确',
OLD_TRADE_PASSWORD_REQUIRED: '请输入原支付密码',
OLD_TRADE_PASSWORD_INVALID: '原支付密码不正确',
ACCOUNT_NOT_FOUND: '账户不存在',
ACCOUNT_CHANGED: '账户信息已发生变化,请重试',
CATEGORY_ID_INVALID: '商品分类参数不正确',
PRODUCT_ID_INVALID: '商品参数不正确',
PRODUCT_NOT_FOUND: '商品不存在或已下架',
PAGE_INVALID: '页码参数不正确',
PAGE_SIZE_INVALID: '每页数量参数不正确',
HALL_ID_INVALID: '任务大厅参数不正确',
ORDER_ID_INVALID: '订单参数不正确',
ORDER_STATUS_INVALID: '订单状态参数不正确',
EVIDENCE_INVALID: '任务凭证不符合要求',
TASK_DISABLED: '领取功能暂未开放',
OUTSIDE_BUSINESS_HOURS: '当前不在营业时间内',
INSUFFICIENT_BALANCE: '可用余额不足299元,暂时无法领取',
DAILY_LIMIT_REACHED: '今日领取次数已用完',
LEVEL_NOT_ELIGIBLE: '当前会员等级不满足领取条件',
ACCOUNT_RESTRICTED: '账户状态受限,请联系客服',
PENDING_ORDER_EXISTS: '存在未完成的任务,请先处理',
NO_TASK_AVAILABLE: '当前暂无可领取任务',
ORDER_NOT_FOUND: '任务订单不存在',
ORDER_EXPIRED: '该任务已超时',
ORDER_STATE_INVALID: '当前任务状态不允许此操作',
TRADE_PASSWORD_NOT_SET: '请先设置支付密码',
TRADE_PASSWORD_INVALID: '支付密码不正确',
ACCOUNT_TYPE_INVALID: '收款账户类型不正确',
PAYMENT_ACCOUNT_FIELDS_INVALID: '绑卡信息填写不完整或格式不正确',
CURRENCY_INVALID: '币种参数不正确',
QR_ATTACHMENT_INVALID: '二维码图片无效,请重新上传',
IMAGE_INVALID: '图片格式或大小不符合要求',
RECHARGE_REMARK_INVALID: '储入备注格式不正确',
PAYMENT_ACCOUNT_INVALID: '收款账户未核验或不可用',
PAYMENT_ACCOUNT_EXISTS: '该收款账户已经存在',
WITHDRAWAL_DISABLED: '取出功能暂未开放',
WITHDRAWAL_AMOUNT_INVALID: '取出金额不符合当前规则',
WITHDRAWAL_DAILY_LIMIT_REACHED: '今日取出次数已用完',
WITHDRAWAL_CHANNEL_UNAVAILABLE: '当前没有可用的取出通道',
WITHDRAWAL_STATUS_INVALID: '取出状态参数不正确',
WITHDRAWAL_ORDER_ID_INVALID: '取出订单参数不正确',
WITHDRAWAL_ORDER_NOT_FOUND: '取出订单不存在',
RECHARGE_AMOUNT_INVALID: '储入金额不符合当前通道规则',
RECHARGE_CHANNEL_UNAVAILABLE: '当前没有可用的储入通道',
RECHARGE_VOUCHER_INVALID: '储入凭证无效',
RECHARGE_STATUS_INVALID: '储入状态参数不正确',
RECHARGE_ORDER_ID_INVALID: '储入订单参数不正确',
RECHARGE_ORDER_NOT_FOUND: '储入订单不存在',
IDEMPOTENCY_KEY_INVALID: '请求标识无效,请刷新后重试',
IDEMPOTENCY_CONFLICT: '请勿重复提交不同的请求',
LEDGER_FILTER_INVALID: '账变记录筛选条件不正确',
RESOURCE_NOT_FOUND: '请求的内容不存在',
BANNER_POSITION_INVALID: '轮播图位置参数不正确',
CONTENT_KEY_INVALID: '内容页面参数不正确',
SERVICE_UNAVAILABLE: '服务暂不可用,请稍后重试',
CUSTOMER_SERVICE_NOT_FOUND: '客服已下线或不存在',
CUSTOMER_SERVICE_NOT_BOUND: '客服账号尚未配置',
@@ -90,9 +132,7 @@ client.interceptors.response.use(
const reason = payload?.reason || '';
return Promise.reject(
new MobileApiError(
reasonMessages[reason] ||
payload?.message ||
(status ? '请求失败,请稍后重试' : '网络连接失败'),
localizedErrorMessage(reason, payload?.message, status),
reason,
status
)
@@ -110,7 +150,7 @@ export async function request<T>(config: AxiosRequestConfig): Promise<T> {
if (response.data.code !== 0) {
const reason = response.data.reason || '';
throw new MobileApiError(
reasonMessages[reason] || response.data.message || '请求失败',
localizedErrorMessage(reason, response.data.message, response.status),
reason,
response.status
);
@@ -218,5 +258,16 @@ export function safeImage(url?: string) {
}
export function errorText(error: unknown) {
return error instanceof Error ? error.message : '操作失败,请稍后重试';
if (!(error instanceof Error)) return '操作失败,请稍后重试';
return containsChinese(error.message) ? error.message : '操作失败,请稍后重试';
}
function localizedErrorMessage(reason: string, message: string | undefined, status: number) {
if (reasonMessages[reason]) return reasonMessages[reason];
if (message && containsChinese(message)) return message;
return status ? '请求失败,请稍后重试' : '网络连接失败,请检查网络后重试';
}
function containsChinese(value: string) {
return /[\u3400-\u9fff]/.test(value);
}