feat: 为OPENAPI添加统一返回数据结构
parent
a110e3a59d
commit
c89a732209
|
|
@ -10,8 +10,8 @@ import { DatabaseModule } from '@/database';
|
||||||
import { ValidationModule } from '@/common/validation';
|
import { ValidationModule } from '@/common/validation';
|
||||||
import { AuthModule } from '@/modules/auth';
|
import { AuthModule } from '@/modules/auth';
|
||||||
import { UserModule } from '@/modules/user';
|
import { UserModule } from '@/modules/user';
|
||||||
import { ResponseModule } from './common/response';
|
import { ResponseModule } from '@/common/response';
|
||||||
import { SerializationModule } from './common/serialization';
|
import { SerializationModule } from '@/common/serialization';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { LoggerService } from './logger.service';
|
||||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
import { LoggerInterceptor } from './logger.interceptor';
|
import { LoggerInterceptor } from './logger.interceptor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志模块
|
||||||
|
* @description 包含全局拦截器
|
||||||
|
*/
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
import { Transform } from 'class-transformer';
|
import { Transform } from 'class-transformer';
|
||||||
import { IsNumber, IsOptional, Min } from 'class-validator';
|
import { IsNumber, IsOptional, Min } from 'class-validator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页 DTO
|
||||||
|
* @example { page: 1, size: 10 }
|
||||||
|
*/
|
||||||
export class PaginationDto {
|
export class PaginationDto {
|
||||||
/**
|
/**
|
||||||
* 页码
|
* 页码
|
||||||
|
* @example 1
|
||||||
*/
|
*/
|
||||||
// @IsNumber()
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(1)
|
@Min(1)
|
||||||
|
|
@ -14,6 +18,7 @@ export class PaginationDto {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 每页条数
|
* 每页条数
|
||||||
|
* @example 10
|
||||||
*/
|
*/
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,10 @@ import { AllExecptionFilter } from './notcaptured.filter';
|
||||||
import { HttpExecptionFilter } from './http.filter';
|
import { HttpExecptionFilter } from './http.filter';
|
||||||
import { ResponseInterceptor } from './response.interceptor';
|
import { ResponseInterceptor } from './response.interceptor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应模块
|
||||||
|
* @description 包含全局异常/HTTP异常/响应结果拦截器
|
||||||
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
/**
|
/**
|
||||||
|
|
@ -33,5 +37,3 @@ import { ResponseInterceptor } from './response.interceptor';
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ResponseModule {}
|
export class ResponseModule {}
|
||||||
|
|
||||||
export const a = 1;
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
|
import { ClassSerializerInterceptor, Module } from '@nestjs/common';
|
||||||
import { APP_INTERCEPTOR } from '@nestjs/core';
|
import { APP_INTERCEPTOR } from '@nestjs/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 序列化模块
|
||||||
|
* @description 包含全局序列化拦截器
|
||||||
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import { ConfigService } from '@/config';
|
import { ConfigService } from '@/config';
|
||||||
import { ServeStaticModule as _ServeStaticModule } from '@nestjs/serve-static';
|
import { ServeStaticModule as _ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态资源模块
|
||||||
|
* @see https://docs.nestjs.com/techniques/mvc#serve-static
|
||||||
|
*/
|
||||||
export const ServeStaticModule = _ServeStaticModule.forRootAsync({
|
export const ServeStaticModule = _ServeStaticModule.forRootAsync({
|
||||||
useFactory: (config: ConfigService) => {
|
useFactory: (config: ConfigService) => {
|
||||||
return [
|
return [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,12 @@
|
||||||
import { INestApplication } from '@nestjs/common';
|
import { INestApplication } from '@nestjs/common';
|
||||||
import { ConfigService } from '@/config';
|
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) => {
|
export const initSwagger = (app: INestApplication) => {
|
||||||
const config = app.get(ConfigService);
|
const config = app.get(ConfigService);
|
||||||
const docConfig = new DocumentBuilder()
|
const docConfig = new DocumentBuilder()
|
||||||
|
|
@ -16,7 +21,12 @@ export const initSwagger = (app: INestApplication) => {
|
||||||
.addTag('post', '文章管理')
|
.addTag('post', '文章管理')
|
||||||
.addTag('upload', '文件上传')
|
.addTag('upload', '文件上传')
|
||||||
.build();
|
.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, {
|
SwaggerModule.setup(config.apiDocPrefix, app, document, {
|
||||||
jsonDocumentUrl: `${config.apiDocPrefix}.json`,
|
jsonDocumentUrl: `${config.apiDocPrefix}.json`,
|
||||||
yamlDocumentUrl: `${config.apiDocPrefix}.yaml`,
|
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 { validationPipeFactory } from './validation.pipe';
|
||||||
import { ValidationExecptionFilter } from './validation.filter';
|
import { ValidationExecptionFilter } from './validation.filter';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验模块
|
||||||
|
* @description 包含全局验证管道和全局验证异常过滤器
|
||||||
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import { Global, Module } from '@nestjs/common';
|
||||||
import { ConfigModule as _ConfigModule } from '@nestjs/config';
|
import { ConfigModule as _ConfigModule } from '@nestjs/config';
|
||||||
import { ConfigService } from './config.service';
|
import { ConfigService } from './config.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 配置模块
|
||||||
|
* @description 基于 `@nestjs/config` 封装,提供更便捷且类型安全的配置读取方式
|
||||||
|
*/
|
||||||
@Global()
|
@Global()
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,12 @@ import { ConfigService as _ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ConfigService {
|
export class ConfigService {
|
||||||
constructor(public config: _ConfigService) {}
|
constructor(
|
||||||
|
/**
|
||||||
|
* `@nestjs/config` 的 ConfigService实例
|
||||||
|
*/
|
||||||
|
public config: _ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保留原有的get方法
|
* 保留原有的get方法
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { SnakeNamingStrategy } from 'typeorm-naming-strategies';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数据库模块
|
* 数据库模块
|
||||||
|
* @description 基于 `typeorm` 封装
|
||||||
*/
|
*/
|
||||||
export const DatabaseModule = TypeOrmModule.forRootAsync({
|
export const DatabaseModule = TypeOrmModule.forRootAsync({
|
||||||
useFactory: (config: ConfigService) => {
|
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 { 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 { UploadService } from './upload.service';
|
||||||
import { FileInterceptor } from '@nestjs/platform-express';
|
import { FileInterceptor } from '@nestjs/platform-express';
|
||||||
import { Respond } from '@/common/response';
|
import { Respond } from '@/common/response';
|
||||||
|
import { CreateUploadDto } from './dto/create-upload.dto';
|
||||||
|
|
||||||
@ApiTags('upload')
|
@ApiTags('upload')
|
||||||
@Controller('upload')
|
@Controller('upload')
|
||||||
|
|
@ -12,6 +13,8 @@ export class UploadController {
|
||||||
@Post()
|
@Post()
|
||||||
@UseInterceptors(FileInterceptor('file'))
|
@UseInterceptors(FileInterceptor('file'))
|
||||||
@ApiOperation({ summary: '上传文件', operationId: 'upload' })
|
@ApiOperation({ summary: '上传文件', operationId: 'upload' })
|
||||||
|
@ApiConsumes('multipart/form-data')
|
||||||
|
@ApiBody({ description: '文件', type: CreateUploadDto })
|
||||||
create(@UploadedFile() file: Express.Multer.File) {
|
create(@UploadedFile() file: Express.Multer.File) {
|
||||||
return this.uploadService.create(file);
|
return this.uploadService.create(file);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,10 @@ import { IsOptional, IsString } from 'class-validator';
|
||||||
import { PaginationDto } from '@/common/response';
|
import { PaginationDto } from '@/common/response';
|
||||||
|
|
||||||
export class FindUserDto extends IntersectionType(PaginationDto) {
|
export class FindUserDto extends IntersectionType(PaginationDto) {
|
||||||
|
/**
|
||||||
|
* 用户昵称
|
||||||
|
* @example '绝弹'
|
||||||
|
*/
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
nickname: string;
|
nickname: string;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue