feat: 为OPENAPI添加统一返回数据结构
parent
a110e3a59d
commit
c89a732209
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -3,6 +3,10 @@ import { LoggerService } from './logger.service';
|
|||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { LoggerInterceptor } from './logger.interceptor';
|
||||
|
||||
/**
|
||||
* 日志模块
|
||||
* @description 包含全局拦截器
|
||||
*/
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
|
||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||
|
||||
/**
|
||||
* 序列化模块
|
||||
* @description 包含全局序列化拦截器
|
||||
*/
|
||||
@Module({
|
||||
providers: [
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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 [
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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: [
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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方法
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
|||
|
||||
/**
|
||||
* 数据库模块
|
||||
* @description 基于 `typeorm` 封装
|
||||
*/
|
||||
export const DatabaseModule = TypeOrmModule.forRootAsync({
|
||||
useFactory: (config: ConfigService) => {
|
||||
|
|
|
|||
|
|
@ -1 +1,6 @@
|
|||
export class CreateUploadDto {}
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class CreateUploadDto {
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
file: any;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue