From 33ec71e9a76e2fbd3fca7a69190d0b11d2a679a7 Mon Sep 17 00:00:00 2001 From: sunlei Date: Tue, 19 May 2026 10:07:44 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=BB=9F=E4=B8=80=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=93=8D=E5=BA=94=E7=BB=93=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API.md | 10 +-- README.md | 10 +++ src/admin/example/admin-example.controller.ts | 21 +++-- src/app.module.ts | 8 +- src/common/admin-response.ts | 29 ++++--- src/common/filters/api-exception.filter.ts | 81 +++++++++++++++++++ src/common/index.ts | 1 + src/common/services/tool.service.ts | 13 ++- src/types/res.d.ts | 16 ++-- test/app.e2e-spec.ts | 65 +++++++++------ 10 files changed, 193 insertions(+), 61 deletions(-) create mode 100644 src/common/filters/api-exception.filter.ts diff --git a/API.md b/API.md index 2bf5f12..a5a4260 100644 --- a/API.md +++ b/API.md @@ -12,13 +12,13 @@ } ``` -失败时仍使用相同结构,常见为: +失败时使用 `err` 承载错误信息,不返回成功结构里的 `data`: ```json { "code": 400, "msg": "操作失败", - "data": null + "err": "错误原因" } ``` @@ -315,12 +315,12 @@ Query: ## Vben Admin 真实接口 -这些接口用于 `Vue/kt-template-admin`,响应格式与 Vben 请求拦截器对齐: +这些接口用于 `Vue/kt-template-admin`,响应格式与项目统一响应结构对齐: ```json { - "code": 0, - "message": "ok", + "code": 200, + "msg": "操作成功", "data": {} } ``` diff --git a/README.md b/README.md index 9cd9de8..2f79010 100644 --- a/README.md +++ b/README.md @@ -106,6 +106,16 @@ pnpm test:e2e # e2e 测试 } ``` +失败时统一返回 `err` 字段,成功响应不包含 `err`: + +```json +{ + "code": 400, + "msg": "操作失败", + "err": "错误原因" +} +``` + ## 核心规则 - `admin_component` 表保存组件/图表模板,`admin_dict` 表是统一字典翻译数据源,`Component.typeMsg` 和 `Component.componentTypeMsg` 查询后自动映射;旧 `/dict/*` 接口路径保持兼容。 diff --git a/src/admin/example/admin-example.controller.ts b/src/admin/example/admin-example.controller.ts index 9774369..568b0bc 100644 --- a/src/admin/example/admin-example.controller.ts +++ b/src/admin/example/admin-example.controller.ts @@ -98,11 +98,16 @@ export class AdminExampleController { @Public() status(@Query('status') status: string, @Res() res: Response) { const code = Number(status) || 200; + + if (code === 200) { + res.status(code).send(vbenSuccess(`${code}`)); + return; + } + res.status(code).send({ - code: -1, - data: null, - error: `${code}`, - message: `${code}`, + code, + msg: `${code}`, + err: `${code}`, }); } @@ -110,8 +115,8 @@ export class AdminExampleController { async bigint(@Res() res: Response) { res.setHeader('Content-Type', 'application/json'); res.send(`{ - "code": 0, - "message": "success", + "code": 200, + "msg": "操作成功", "data": [ { "id": 123456789012345678901234567890123456789012345678901234567890, @@ -132,13 +137,13 @@ export class AdminExampleController { @Get('test') @Public() testGet() { - return 'Test get handler'; + return vbenSuccess('Test get handler'); } @Post('test') @Public() testPost() { - return 'Test post handler'; + return vbenSuccess('Test post handler'); } private sortRows(rows: DemoTableRow[], sortBy?: string, sortOrder?: string) { diff --git a/src/app.module.ts b/src/app.module.ts index 0108e40..dc5ae2b 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { ConfigService } from '@nestjs/config'; -import { APP_INTERCEPTOR } from '@nestjs/core'; +import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; import { AppService } from './app.service'; import { TypeOrmModule } from '@nestjs/typeorm'; import { MinioModule } from 'nestjs-minio-client'; import { MinioClientModule } from './minio/minio.module'; -import { SaveBodyInterceptor } from './common'; +import { ApiExceptionFilter, SaveBodyInterceptor } from './common'; import { AdminModule } from './admin/admin.module'; import { WordpressModule } from './wordpress/wordpress.module'; @@ -58,6 +58,10 @@ import { WordpressModule } from './wordpress/wordpress.module'; provide: APP_INTERCEPTOR, useClass: SaveBodyInterceptor, }, + { + provide: APP_FILTER, + useClass: ApiExceptionFilter, + }, ], }) export class AppModule {} diff --git a/src/common/admin-response.ts b/src/common/admin-response.ts index 930b66e..cd47a41 100644 --- a/src/common/admin-response.ts +++ b/src/common/admin-response.ts @@ -1,17 +1,24 @@ import { HttpException, HttpStatus } from '@nestjs/common'; export type VbenResponse = { - code: number; + code: 200; data: T; - error: any; - message: string; + msg: string; }; -export const vbenSuccess = (data: T): VbenResponse => ({ - code: 0, +export type ApiErrorResponse = { + code: number; + msg: string; + err: any; +}; + +export const vbenSuccess = ( + data: T, + msg = '操作成功', +): VbenResponse => ({ + code: 200, data, - error: null, - message: 'ok', + msg, }); export const vbenPage = (items: T[], total: number) => @@ -23,14 +30,12 @@ export const vbenPage = (items: T[], total: number) => export const throwVbenError = ( message: string, status = HttpStatus.BAD_REQUEST, - error: any = message, + err: any = message, ): never => { throw new HttpException( { - code: -1, - data: null, - error, - message, + msg: message, + err, }, status, ); diff --git a/src/common/filters/api-exception.filter.ts b/src/common/filters/api-exception.filter.ts new file mode 100644 index 0000000..0b1d165 --- /dev/null +++ b/src/common/filters/api-exception.filter.ts @@ -0,0 +1,81 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, +} from '@nestjs/common'; +import { Response } from 'express'; +import { ApiErrorResponse } from '../admin-response'; + +type ExceptionBody = { + err?: unknown; + error?: unknown; + message?: unknown; + msg?: unknown; +}; + +@Catch() +export class ApiExceptionFilter implements ExceptionFilter { + catch(exception: unknown, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const status = this.getStatus(exception); + const body = this.getBody(exception); + + response.status(status).json({ + code: status, + msg: this.getMessage(status, body, exception), + err: this.getErr(status, body, exception), + } satisfies ApiErrorResponse); + } + + private getStatus(exception: unknown) { + if (exception instanceof HttpException) { + return exception.getStatus(); + } + + return HttpStatus.INTERNAL_SERVER_ERROR; + } + + private getBody(exception: unknown): ExceptionBody | string | null { + if (!(exception instanceof HttpException)) { + return null; + } + + const body = exception.getResponse(); + + return typeof body === 'string' ? body : (body as ExceptionBody); + } + + private getMessage( + status: number, + body: ExceptionBody | string | null, + exception: unknown, + ) { + if (typeof body === 'string') return body; + if (body?.msg) return this.stringifyMessage(body.msg); + if (body?.message) return this.stringifyMessage(body.message); + if (exception instanceof Error && status < 500) return exception.message; + + return status >= 500 ? 'Internal server error' : '操作失败'; + } + + private getErr( + status: number, + body: ExceptionBody | string | null, + exception: unknown, + ) { + if (typeof body === 'string') return body; + if (body?.err !== undefined) return body.err; + if (body?.error !== undefined) return body.error; + if (body?.message !== undefined) return body.message; + if (exception instanceof Error) return exception.message; + + return status >= 500 ? 'Internal server error' : '操作失败'; + } + + private stringifyMessage(message: unknown) { + return Array.isArray(message) ? message.join('; ') : String(message); + } +} diff --git a/src/common/index.ts b/src/common/index.ts index e13ff85..5d4d8b4 100644 --- a/src/common/index.ts +++ b/src/common/index.ts @@ -3,6 +3,7 @@ export * from './admin-tree'; export * from './decorators/current-admin-user.decorator'; export * from './decorators/decode-dict.decorator'; export * from './decorators/public.decorator'; +export * from './filters/api-exception.filter'; export * from './interceptors/save-body.interceptor'; export * from './snowflake-id'; export * from './services/tool.service'; diff --git a/src/common/services/tool.service.ts b/src/common/services/tool.service.ts index f568032..e50a8a3 100644 --- a/src/common/services/tool.service.ts +++ b/src/common/services/tool.service.ts @@ -15,12 +15,19 @@ export class ToolsService { } res(code: number, msg: string, data: any): Res { - const retn: Res = { + if (code === 200) { + return { + code, + msg, + data, + }; + } + + return { code, msg, - data, + err: data, }; - return retn; } page(list: T[], total: number): Page { diff --git a/src/types/res.d.ts b/src/types/res.d.ts index 75cda74..483fa06 100644 --- a/src/types/res.d.ts +++ b/src/types/res.d.ts @@ -1,8 +1,14 @@ -type Res = { - code: number; - msg: string; - data: any; -}; +type Res = + | { + code: 200; + msg: string; + data: any; + } + | { + code: number; + msg: string; + err: any; + }; type Page = { list: T[]; diff --git a/test/app.e2e-spec.ts b/test/app.e2e-spec.ts index baebf62..c5b07e9 100644 --- a/test/app.e2e-spec.ts +++ b/test/app.e2e-spec.ts @@ -1,5 +1,5 @@ import { HttpException, HttpStatus, INestApplication } from '@nestjs/common'; -import { APP_INTERCEPTOR, Reflector } from '@nestjs/core'; +import { APP_FILTER, APP_INTERCEPTOR, Reflector } from '@nestjs/core'; import { Test, TestingModule } from '@nestjs/testing'; import request = require('supertest'); import { Readable } from 'stream'; @@ -11,7 +11,11 @@ import { ComponentController } from '../src/admin/component/component.controller import { ComponentService } from '../src/admin/component/component.service'; import { DictController } from '../src/admin/dict/dict.controller'; import { DictService } from '../src/admin/dict/dict.service'; -import { SaveBodyInterceptor, ToolsService } from '../src/common'; +import { + ApiExceptionFilter, + SaveBodyInterceptor, + ToolsService, +} from '../src/common'; import { MinioClientController } from '../src/minio/minio.controller'; import { MinioClientService } from '../src/minio/minio.service'; import { WordpressArticleController } from '../src/wordpress/wordpress-article.controller'; @@ -117,10 +121,8 @@ const authServiceMock = { const unauthorizedException = () => new HttpException( { - code: -1, - data: null, - error: 'Unauthorized Exception', - message: 'Unauthorized Exception', + msg: 'Unauthorized Exception', + err: 'Unauthorized Exception', }, HttpStatus.UNAUTHORIZED, ); @@ -564,7 +566,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressUser, }); }, @@ -584,7 +586,7 @@ const routeTestCases: Record = { wordpressLoginResult.cookie, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: { auth: wordpressLoginResult.auth, user: wordpressUser, @@ -601,7 +603,7 @@ const routeTestCases: Record = { expect.anything(), ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: true, }); }, @@ -630,7 +632,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: { list: [wordpressArticle], total: 1, @@ -651,7 +653,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressArticle, }); }, @@ -676,7 +678,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressArticle, }); }, @@ -700,7 +702,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressArticle, }); }, @@ -722,7 +724,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: true, }); }, @@ -751,7 +753,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: { list: [wordpressTerm], total: 1, @@ -772,7 +774,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressTerm, }); }, @@ -795,7 +797,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressTerm, }); }, @@ -819,7 +821,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressTerm, }); }, @@ -838,7 +840,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: true, }); }, @@ -867,7 +869,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: { list: [wordpressTerm], total: 1, @@ -888,7 +890,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressTerm, }); }, @@ -913,7 +915,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressTerm, }); }, @@ -939,7 +941,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: wordpressTerm, }); }, @@ -958,7 +960,7 @@ const routeTestCases: Record = { wordpressAuthContext, ); expect(response.body).toMatchObject({ - code: 0, + code: 200, data: true, }); }, @@ -999,6 +1001,10 @@ describe('KT Template Online API (e2e)', () => { provide: APP_INTERCEPTOR, useClass: SaveBodyInterceptor, }, + { + provide: APP_FILTER, + useClass: ApiExceptionFilter, + }, ], }).compile(); @@ -1052,7 +1058,7 @@ describe('KT Template Online API (e2e)', () => { expect(response.body).toEqual({ code: 400, msg: '操作失败', - data: false, + err: false, }); }); @@ -1062,7 +1068,14 @@ describe('KT Template Online API (e2e)', () => { await request(app.getHttpServer()) .get('/dict/getDictByKey') .query({ dictKey: 'COMPONENT_TYPE' }) - .expect(401); + .expect(401) + .expect(({ body }) => { + expect(body).toEqual({ + code: 401, + msg: 'Unauthorized Exception', + err: 'Unauthorized Exception', + }); + }); expect(dictServiceMock.getDictByKey).not.toHaveBeenCalled();