feat:统一前端请求层错误回显与长整型解析
This commit is contained in:
30
frontend/src/api/__tests__/json.spec.ts
Normal file
30
frontend/src/api/__tests__/json.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { parseJsonPreservingLong } from '../json';
|
||||
|
||||
describe('parseJsonPreservingLong', () => {
|
||||
it('should preserve unsafe long ids as strings', () => {
|
||||
const result = parseJsonPreservingLong<{ id: string; storeCode: string }>(
|
||||
'{"id":2057302206052372481,"storeCode":"TEXT-1"}',
|
||||
'application/json',
|
||||
) as { id: string; storeCode: string };
|
||||
|
||||
expect(result.id).toBe('2057302206052372481');
|
||||
expect(result.storeCode).toBe('TEXT-1');
|
||||
});
|
||||
|
||||
it('should keep safe integers as numbers', () => {
|
||||
const result = parseJsonPreservingLong<{ total: number }>(
|
||||
'{"total":12}',
|
||||
'application/json',
|
||||
) as { total: number };
|
||||
|
||||
expect(result.total).toBe(12);
|
||||
expect(typeof result.total).toBe('number');
|
||||
});
|
||||
|
||||
it('should skip non json payloads', () => {
|
||||
const csv = 'id,name\n1,test';
|
||||
|
||||
expect(parseJsonPreservingLong(csv, 'text/csv')).toBe(csv);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { request } from '../request';
|
||||
import { buildErrorDetail, request } from '../request';
|
||||
|
||||
describe('request client', () => {
|
||||
it('uses the backend api prefix', () => {
|
||||
expect(request.defaults.baseURL).toBe('/api');
|
||||
});
|
||||
|
||||
it('parses oversized numeric ids as strings to avoid precision loss', () => {
|
||||
const transformResponse = request.defaults.transformResponse;
|
||||
const transform = Array.isArray(transformResponse) ? transformResponse[0] : transformResponse;
|
||||
const payload = '{"resultcode":"0","message":null,"data":{"id":2057302206052372481,"storeCode":"TEXT-1"}}';
|
||||
|
||||
const parsed = transform
|
||||
? transform.call({} as never, payload, { 'content-type': 'application/json' } as never)
|
||||
: JSON.parse(payload);
|
||||
|
||||
expect(parsed.data.id).toBe('2057302206052372481');
|
||||
});
|
||||
|
||||
it('formats structured backend errors for dialog display', () => {
|
||||
const detail = buildErrorDetail({
|
||||
resultcode: '400',
|
||||
message: '知识库编码已存在: TEST-1',
|
||||
data: null,
|
||||
});
|
||||
|
||||
expect(detail).toContain('失败原因:知识库编码已存在: TEST-1');
|
||||
expect(detail).toContain('错误编码:400');
|
||||
});
|
||||
});
|
||||
|
||||
21
frontend/src/api/json.ts
Normal file
21
frontend/src/api/json.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import JSONBig from 'json-bigint';
|
||||
|
||||
const jsonParser = JSONBig({
|
||||
storeAsString: true,
|
||||
useNativeBigInt: false,
|
||||
});
|
||||
|
||||
function looksLikeJsonPayload(data: string) {
|
||||
const trimmed = data.trim();
|
||||
return trimmed.startsWith('{') || trimmed.startsWith('[');
|
||||
}
|
||||
|
||||
export function parseJsonPreservingLong<T>(data: unknown, contentType?: string): T | unknown {
|
||||
if (typeof data !== 'string' || !looksLikeJsonPayload(data)) {
|
||||
return data;
|
||||
}
|
||||
if (contentType && !contentType.toLowerCase().includes('application/json')) {
|
||||
return data;
|
||||
}
|
||||
return jsonParser.parse(data) as T;
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import axios from 'axios';
|
||||
import type { AxiosRequestConfig } from 'axios';
|
||||
import { ElMessageBox } from 'element-plus';
|
||||
import { parseJsonPreservingLong } from './json';
|
||||
|
||||
export interface RequestResult<T> {
|
||||
resultcode: string;
|
||||
@@ -10,8 +12,37 @@ export interface RequestResult<T> {
|
||||
export const request = axios.create({
|
||||
baseURL: '/api',
|
||||
timeout: 15000,
|
||||
transformResponse: [
|
||||
(data, headers) =>
|
||||
parseJsonPreservingLong(
|
||||
data,
|
||||
typeof headers?.['content-type'] === 'string' ? headers['content-type'] : undefined,
|
||||
),
|
||||
],
|
||||
});
|
||||
|
||||
export function buildErrorDetail(errorData: RequestResult<unknown>) {
|
||||
const lines = [
|
||||
`失败原因:${errorData.message || '未知错误'}`,
|
||||
`错误编码:${errorData.resultcode || '-'}`,
|
||||
];
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
request.interceptors.response.use(
|
||||
(response) => response,
|
||||
async (error) => {
|
||||
const errorData = error?.response?.data as RequestResult<unknown> | undefined;
|
||||
if (errorData?.resultcode && errorData?.message) {
|
||||
await ElMessageBox.alert(buildErrorDetail(errorData), '请求失败', {
|
||||
type: 'error',
|
||||
confirmButtonText: '知道了',
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
export function get<T>(url: string, config?: AxiosRequestConfig) {
|
||||
return request.get<RequestResult<T>>(url, config).then((response) => response.data);
|
||||
}
|
||||
|
||||
14
frontend/src/types/json-bigint.d.ts
vendored
Normal file
14
frontend/src/types/json-bigint.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
declare module 'json-bigint' {
|
||||
interface JsonBigOptions {
|
||||
storeAsString?: boolean;
|
||||
useNativeBigInt?: boolean;
|
||||
alwaysParseAsBig?: boolean;
|
||||
}
|
||||
|
||||
interface JsonBigInstance {
|
||||
parse<T = unknown>(text: string): T;
|
||||
stringify(value: unknown): string;
|
||||
}
|
||||
|
||||
export default function JSONBig(options?: JsonBigOptions): JsonBigInstance;
|
||||
}
|
||||
Reference in New Issue
Block a user