start sprint 4 error code mapping
Some checks failed
CI / pytest (Python 3.10) (push) Has been cancelled
CI / pytest (Python 3.11) (push) Has been cancelled
CI / pytest (Python 3.12) (push) Has been cancelled

This commit is contained in:
Frontend Developer
2026-07-03 21:55:30 +08:00
parent e554215335
commit 86296bdb0e
5 changed files with 247 additions and 3 deletions

View File

@@ -9,6 +9,9 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
@@ -158,6 +161,12 @@ class TestRegistry:
"""
assert len(MESSAGES_ZH_CN) == 17
def test_frontend_i18n_catalog_matches_registry(self):
catalog_path = Path(__file__).resolve().parents[2] / "packages" / "i18n" / "zh-CN" / "errors.json"
catalog = json.loads(catalog_path.read_text(encoding="utf-8"))
expected = {code: message.to_dict() for code, message in MESSAGES_ZH_CN.items()}
assert catalog == expected
# ---------------- exceptions.py ----------------

View File

@@ -0,0 +1,76 @@
import { z } from 'zod';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { apiClient, HttpError } from './api-client';
const errorCases = [
['E01101', '用户名或密码不正确'],
['E01102', '账号已被停用'],
['E01201', '登录状态已过期'],
['E01202', '登录凭证无效'],
['E01301', '当前账号无访问权限'],
['E02001', '未找到该订单'],
['E02002', '订单仍在数据保留期内'],
['E02301', '订单当前状态不支持该操作'],
['E02501', '请求过于频繁'],
['E03001', '请求数据未通过校验'],
['E03002', '未找到对应的数据'],
['E03003', '数据保存失败'],
['E04001', '外部服务暂时不可用'],
['E04002', '外部服务响应超时'],
['E05001', '系统内部异常'],
['E05002', '系统配置缺失'],
['E05003', '系统资源不足'],
] as const;
describe('apiClient error handling', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
it.each(errorCases)('maps backend error code %s to localized user copy', async (code, message) => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
code,
message: `backend raw message for ${code}`,
details: { requestId: 'req-1' },
}),
{
status: 422,
headers: { 'Content-Type': 'application/json' },
},
),
),
);
await expect(apiClient.get('/boom', z.object({ ok: z.boolean() }))).rejects.toMatchObject({
name: 'HttpError',
status: 422,
code,
message,
details: { requestId: 'req-1' },
});
});
it('falls back to the backend message for unknown error codes', async () => {
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(JSON.stringify({ code: 'E09999', message: 'backend fallback', details: null }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
}),
),
);
await expect(apiClient.get('/unknown', z.object({ ok: z.boolean() }))).rejects.toMatchObject({
name: 'HttpError',
status: 500,
code: 'E09999',
message: 'backend fallback',
details: null,
} satisfies Partial<HttpError>);
});
});

View File

@@ -4,27 +4,43 @@
*
* G1 闸门:
* - 0 any (所有 response 强类型, 通过 zod 校验)
* - 错误码 → i18n 文案 (Sprint 4 完善映射, 现在先返回原始错误)
* - 错误码 → i18n 文案
*/
import { ZodError, type ZodType, type ZodTypeDef } from 'zod';
import { getLocalizedApiErrorMessage, type ApiErrorSeverity } from './error-messages';
export interface ApiError extends Error {
status: number;
code?: string;
details?: unknown;
suggestion?: string;
severity?: ApiErrorSeverity;
retryable?: boolean;
}
export class HttpError extends Error implements ApiError {
readonly status: number;
readonly code: string | undefined;
readonly details: unknown;
readonly suggestion: string | undefined;
readonly severity: ApiErrorSeverity | undefined;
readonly retryable: boolean | undefined;
constructor(message: string, status: number, code?: string, details?: unknown) {
constructor(
message: string,
status: number,
code?: string,
details?: unknown,
options: Pick<ApiError, 'suggestion' | 'severity' | 'retryable'> = {},
) {
super(message);
this.name = 'HttpError';
this.status = status;
this.code = code;
this.details = details;
this.suggestion = options.suggestion;
this.severity = options.severity;
this.retryable = options.retryable;
}
}
@@ -67,11 +83,13 @@ async function request<TResponse, TBody = unknown>(
} catch {
// response body 不是 JSON
}
const localizedError = getLocalizedApiErrorMessage(errorBody.code);
throw new HttpError(
errorBody.message ?? `HTTP ${res.status}`,
localizedError?.message ?? errorBody.message ?? `HTTP ${res.status}`,
res.status,
errorBody.code,
errorBody.details,
localizedError,
);
}

View File

@@ -0,0 +1,20 @@
import zhCNErrorMessages from '../../../../packages/i18n/zh-CN/errors.json';
export type ApiErrorSeverity = 'info' | 'warn' | 'error';
export interface LocalizedApiErrorMessage {
code: string;
message: string;
suggestion: string;
severity: ApiErrorSeverity;
retryable: boolean;
}
const errorMessages = zhCNErrorMessages as Record<string, LocalizedApiErrorMessage>;
export function getLocalizedApiErrorMessage(code: string | undefined): LocalizedApiErrorMessage | undefined {
if (code === undefined) {
return undefined;
}
return errorMessages[code];
}

View File

@@ -0,0 +1,121 @@
{
"E01101": {
"code": "E01101",
"message": "用户名或密码不正确",
"suggestion": "请检查大小写和输入法,确认无误后重新登录。忘记密码请联系管理员重置。",
"severity": "warn",
"retryable": false
},
"E01102": {
"code": "E01102",
"message": "账号已被停用",
"suggestion": "请联系管理员确认账号状态,启用后再登录。",
"severity": "warn",
"retryable": false
},
"E01201": {
"code": "E01201",
"message": "登录状态已过期",
"suggestion": "请重新登录后再继续操作。系统默认会话有效期 5 分钟。",
"severity": "warn",
"retryable": false
},
"E01202": {
"code": "E01202",
"message": "登录凭证无效",
"suggestion": "请重新登录。如反复出现,请清除浏览器 Cookie 后重试。",
"severity": "warn",
"retryable": false
},
"E01301": {
"code": "E01301",
"message": "当前账号无访问权限",
"suggestion": "请使用具备相应权限的账号登录,或联系管理员开通权限。",
"severity": "warn",
"retryable": false
},
"E02001": {
"code": "E02001",
"message": "未找到该订单",
"suggestion": "请检查订单号是否正确,或在订单列表中通过筛选条件重新查询。",
"severity": "warn",
"retryable": false
},
"E02002": {
"code": "E02002",
"message": "订单仍在数据保留期内",
"suggestion": "请等待保留期(服务完成后 180 天)结束,或走支付争议/法务豁免审批流程后再处理。",
"severity": "warn",
"retryable": false
},
"E02301": {
"code": "E02301",
"message": "订单当前状态不支持该操作",
"suggestion": "请刷新订单详情查看最新状态,或前往订单列表查看状态流转。",
"severity": "warn",
"retryable": false
},
"E02501": {
"code": "E02501",
"message": "请求过于频繁",
"suggestion": "请稍候 30 秒后再试。如持续触发,可联系管理员调整配额。",
"severity": "warn",
"retryable": true
},
"E03001": {
"code": "E03001",
"message": "请求数据未通过校验",
"suggestion": "请检查必填字段和格式。鼠标悬停字段标签可查看填写要求。",
"severity": "warn",
"retryable": false
},
"E03002": {
"code": "E03002",
"message": "未找到对应的数据",
"suggestion": "该记录可能已被删除或尚未创建,请刷新页面或返回列表确认。",
"severity": "warn",
"retryable": false
},
"E03003": {
"code": "E03003",
"message": "数据保存失败",
"suggestion": "请稍后重试。如反复失败,请联系技术支持并保留错误码。",
"severity": "error",
"retryable": true
},
"E04001": {
"code": "E04001",
"message": "外部服务暂时不可用",
"suggestion": "我们正在同步上游状态,请稍后重试;如紧急可联系客服。",
"severity": "error",
"retryable": true
},
"E04002": {
"code": "E04002",
"message": "外部服务响应超时",
"suggestion": "请稍后重试,或在网络稳定的环境下再次尝试。",
"severity": "error",
"retryable": true
},
"E05001": {
"code": "E05001",
"message": "系统内部异常",
"suggestion": "请稍后重试。如反复出现,请联系技术支持并提供错误码。",
"severity": "error",
"retryable": true
},
"E05002": {
"code": "E05002",
"message": "系统配置缺失",
"suggestion": "请联系运维人员检查服务配置,确认环境变量已正确注入。",
"severity": "error",
"retryable": false
},
"E05003": {
"code": "E05003",
"message": "系统资源不足",
"suggestion": "当前访问量过高,请稍后再试;如紧急可联系运维扩容。",
"severity": "error",
"retryable": true
}
}