From c89a7322090f0194b0194826963d00d807cac0ed Mon Sep 17 00:00:00 2001 From: luoer Date: Wed, 2 Aug 2023 17:27:38 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=BAOPENAPI=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E8=BF=94=E5=9B=9E=E6=95=B0=E6=8D=AE=E7=BB=93?= =?UTF-8?q?=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app.module.ts | 4 +- src/common/logger/logger.module.ts | 4 ++ src/common/response/pagination.dto.ts | 7 +- src/common/response/response.module.ts | 6 +- .../serialization/serialization.module.ts | 4 ++ src/common/static/static.module.ts | 4 ++ src/common/swagger/index.ts | 14 +++- src/common/swagger/util.ts | 70 +++++++++++++++++++ src/common/validation/validation.module.ts | 4 ++ src/config/config.module.ts | 4 ++ src/config/config.service.ts | 7 +- src/database/database.module.ts | 1 + src/modules/upload/dto/create-upload.dto.ts | 7 +- src/modules/upload/upload.controller.ts | 5 +- src/modules/user/dto/find-user.dto.ts | 4 ++ 15 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 src/common/swagger/util.ts diff --git a/src/app.module.ts b/src/app.module.ts index 7383009..745ef7c 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -10,8 +10,8 @@ import { DatabaseModule } from '@/database'; import { ValidationModule } from '@/common/validation'; import { AuthModule } from '@/modules/auth'; import { UserModule } from '@/modules/user'; -import { ResponseModule } from './common/response'; -import { SerializationModule } from './common/serialization'; +import { ResponseModule } from '@/common/response'; +import { SerializationModule } from '@/common/serialization'; @Module({ imports: [ diff --git a/src/common/logger/logger.module.ts b/src/common/logger/logger.module.ts index f2d9cb1..439ba11 100644 --- a/src/common/logger/logger.module.ts +++ b/src/common/logger/logger.module.ts @@ -3,6 +3,10 @@ import { LoggerService } from './logger.service'; import { APP_INTERCEPTOR } from '@nestjs/core'; import { LoggerInterceptor } from './logger.interceptor'; +/** + * 日志模块 + * @description 包含全局拦截器 + */ @Global() @Module({ providers: [ diff --git a/src/common/response/pagination.dto.ts b/src/common/response/pagination.dto.ts index ceae88a..40ab775 100644 --- a/src/common/response/pagination.dto.ts +++ b/src/common/response/pagination.dto.ts @@ -1,11 +1,15 @@ import { Transform } from 'class-transformer'; import { IsNumber, IsOptional, Min } from 'class-validator'; +/** + * 分页 DTO + * @example { page: 1, size: 10 } + */ export class PaginationDto { /** * 页码 + * @example 1 */ - // @IsNumber() @IsOptional() @IsNumber() @Min(1) @@ -14,6 +18,7 @@ export class PaginationDto { /** * 每页条数 + * @example 10 */ @IsOptional() @IsNumber() diff --git a/src/common/response/response.module.ts b/src/common/response/response.module.ts index c4794b4..0a20768 100644 --- a/src/common/response/response.module.ts +++ b/src/common/response/response.module.ts @@ -4,6 +4,10 @@ import { AllExecptionFilter } from './notcaptured.filter'; import { HttpExecptionFilter } from './http.filter'; import { ResponseInterceptor } from './response.interceptor'; +/** + * 响应模块 + * @description 包含全局异常/HTTP异常/响应结果拦截器 + */ @Module({ providers: [ /** @@ -33,5 +37,3 @@ import { ResponseInterceptor } from './response.interceptor'; ], }) export class ResponseModule {} - -export const a = 1; diff --git a/src/common/serialization/serialization.module.ts b/src/common/serialization/serialization.module.ts index d4da1e5..feea81d 100644 --- a/src/common/serialization/serialization.module.ts +++ b/src/common/serialization/serialization.module.ts @@ -1,6 +1,10 @@ import { ClassSerializerInterceptor, Module } from '@nestjs/common'; import { APP_INTERCEPTOR } from '@nestjs/core'; +/** + * 序列化模块 + * @description 包含全局序列化拦截器 + */ @Module({ providers: [ /** diff --git a/src/common/static/static.module.ts b/src/common/static/static.module.ts index 3cbf05f..5db8123 100644 --- a/src/common/static/static.module.ts +++ b/src/common/static/static.module.ts @@ -1,6 +1,10 @@ import { ConfigService } from '@/config'; import { ServeStaticModule as _ServeStaticModule } from '@nestjs/serve-static'; +/** + * 静态资源模块 + * @see https://docs.nestjs.com/techniques/mvc#serve-static + */ export const ServeStaticModule = _ServeStaticModule.forRootAsync({ useFactory: (config: ConfigService) => { return [ diff --git a/src/common/swagger/index.ts b/src/common/swagger/index.ts index 576f632..0d9e11d 100644 --- a/src/common/swagger/index.ts +++ b/src/common/swagger/index.ts @@ -1,7 +1,12 @@ import { INestApplication } from '@nestjs/common'; import { ConfigService } from '@/config'; -import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; +import { DocumentBuilder, SwaggerDocumentOptions, SwaggerModule } from '@nestjs/swagger'; +import { addResponseWrapper } from './util'; +/** + * 初始化Swagger + * @param app 应用实例 + */ export const initSwagger = (app: INestApplication) => { const config = app.get(ConfigService); const docConfig = new DocumentBuilder() @@ -16,7 +21,12 @@ export const initSwagger = (app: INestApplication) => { .addTag('post', '文章管理') .addTag('upload', '文件上传') .build(); - const document = SwaggerModule.createDocument(app, docConfig); + const options: SwaggerDocumentOptions = { + operationIdFactory(controllerKey, methodKey) { + return `${controllerKey}_${methodKey}`; + }, + }; + const document = addResponseWrapper(SwaggerModule.createDocument(app, docConfig, options)); SwaggerModule.setup(config.apiDocPrefix, app, document, { jsonDocumentUrl: `${config.apiDocPrefix}.json`, yamlDocumentUrl: `${config.apiDocPrefix}.yaml`, diff --git a/src/common/swagger/util.ts b/src/common/swagger/util.ts new file mode 100644 index 0000000..6199ccd --- /dev/null +++ b/src/common/swagger/util.ts @@ -0,0 +1,70 @@ +import { OpenAPIObject } from '@nestjs/swagger'; + +/** + * 为所有接口添加统一的返回数据结构 + * @param doc OPENAPI文档对象 + * @example + * ```json + * { + * "code": 2000, + * "message": "请求成功", + * "data": [] + * } + * ``` + * @returns + */ +export function addResponseWrapper(doc: OpenAPIObject) { + for (const path of Object.keys(doc.paths)) { + const pathItem = doc.paths[path]; + if (!pathItem) { + continue; + } + for (const method of Object.keys(pathItem)) { + const responses = doc.paths[path][method].responses; + if (!responses) { + continue; + } + for (const status of Object.keys(responses)) { + const json = responses[status].content?.['application/json']; + if (!json) { + continue; + } + const schema = json.schema; + json.schema = { + allOf: [ + { + $ref: '#/components/schemas/Response', + }, + { + type: 'object', + description: '返回数据', + properties: { + data: schema, + }, + }, + ], + }; + } + } + } + + doc.components.schemas.Response = { + type: 'object', + properties: { + code: { + type: 'integer', + description: '状态码', + example: 2000, + format: 'int32', + }, + message: { + type: 'string', + description: '提示信息', + example: '请求成功', + }, + }, + required: ['code', 'message'], + }; + + return doc; +} diff --git a/src/common/validation/validation.module.ts b/src/common/validation/validation.module.ts index b068943..b68e548 100644 --- a/src/common/validation/validation.module.ts +++ b/src/common/validation/validation.module.ts @@ -3,6 +3,10 @@ import { APP_FILTER, APP_PIPE } from '@nestjs/core'; import { validationPipeFactory } from './validation.pipe'; import { ValidationExecptionFilter } from './validation.filter'; +/** + * 校验模块 + * @description 包含全局验证管道和全局验证异常过滤器 + */ @Module({ providers: [ /** diff --git a/src/config/config.module.ts b/src/config/config.module.ts index b28d336..1592a0d 100644 --- a/src/config/config.module.ts +++ b/src/config/config.module.ts @@ -2,6 +2,10 @@ import { Global, Module } from '@nestjs/common'; import { ConfigModule as _ConfigModule } from '@nestjs/config'; import { ConfigService } from './config.service'; +/** + * 配置模块 + * @description 基于 `@nestjs/config` 封装,提供更便捷且类型安全的配置读取方式 + */ @Global() @Module({ imports: [ diff --git a/src/config/config.service.ts b/src/config/config.service.ts index 0237ac7..09702a8 100644 --- a/src/config/config.service.ts +++ b/src/config/config.service.ts @@ -3,7 +3,12 @@ import { ConfigService as _ConfigService } from '@nestjs/config'; @Injectable() export class ConfigService { - constructor(public config: _ConfigService) {} + constructor( + /** + * `@nestjs/config` 的 ConfigService实例 + */ + public config: _ConfigService, + ) {} /** * 保留原有的get方法 diff --git a/src/database/database.module.ts b/src/database/database.module.ts index 07acd89..4a4f65b 100644 --- a/src/database/database.module.ts +++ b/src/database/database.module.ts @@ -4,6 +4,7 @@ import { SnakeNamingStrategy } from 'typeorm-naming-strategies'; /** * 数据库模块 + * @description 基于 `typeorm` 封装 */ export const DatabaseModule = TypeOrmModule.forRootAsync({ useFactory: (config: ConfigService) => { diff --git a/src/modules/upload/dto/create-upload.dto.ts b/src/modules/upload/dto/create-upload.dto.ts index b9ec6cd..cf80dc1 100644 --- a/src/modules/upload/dto/create-upload.dto.ts +++ b/src/modules/upload/dto/create-upload.dto.ts @@ -1 +1,6 @@ -export class CreateUploadDto {} +import { ApiProperty } from '@nestjs/swagger'; + +export class CreateUploadDto { + @ApiProperty({ type: 'string', format: 'binary' }) + file: any; +} diff --git a/src/modules/upload/upload.controller.ts b/src/modules/upload/upload.controller.ts index 7c010f0..58968ed 100644 --- a/src/modules/upload/upload.controller.ts +++ b/src/modules/upload/upload.controller.ts @@ -1,8 +1,9 @@ import { Controller, Delete, Get, Param, Patch, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; -import { ApiOperation, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; import { UploadService } from './upload.service'; import { FileInterceptor } from '@nestjs/platform-express'; import { Respond } from '@/common/response'; +import { CreateUploadDto } from './dto/create-upload.dto'; @ApiTags('upload') @Controller('upload') @@ -12,6 +13,8 @@ export class UploadController { @Post() @UseInterceptors(FileInterceptor('file')) @ApiOperation({ summary: '上传文件', operationId: 'upload' }) + @ApiConsumes('multipart/form-data') + @ApiBody({ description: '文件', type: CreateUploadDto }) create(@UploadedFile() file: Express.Multer.File) { return this.uploadService.create(file); } diff --git a/src/modules/user/dto/find-user.dto.ts b/src/modules/user/dto/find-user.dto.ts index 1be951a..a353382 100644 --- a/src/modules/user/dto/find-user.dto.ts +++ b/src/modules/user/dto/find-user.dto.ts @@ -3,6 +3,10 @@ import { IsOptional, IsString } from 'class-validator'; import { PaginationDto } from '@/common/response'; export class FindUserDto extends IntersectionType(PaginationDto) { + /** + * 用户昵称 + * @example '绝弹' + */ @IsOptional() @IsString() nickname: string;