因为项目添加了 NestJS Response Interceptor . 所以接口的返回结果都会经过一层包装处理,为返回数据添加上 codesuccessdata 字段:

/**
 * 对返回数据格式进行统一
 */
import {
  Injectable,
  NestInterceptor,
  ExecutionContext,
  CallHandler,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { ResponseType } from 'types/interface';
 
@Injectable()
export class ResponseInterceptor implements NestInterceptor {
  intercept(
    context: ExecutionContext,
    next: CallHandler<any>,
  ): Observable<ResponseType> | Promise<Observable<ResponseType>> {
    return next.handle().pipe(
      map((data) => {
        return {
          code: context.switchToHttp().getResponse().statusCode,
          success: true,
          data: data,
        };
      }),
    );
  }
}

但是在 Controller 中使用 Swagger 时,定义 ApiOkResponse 总不能每次都定义返回结果包含 code 、success这些字段,

  @Get('/info')
  @ApiOperation({ summary: '获取当前用户信息' })
  @ApiOkResponse({
    description: '用户的基本信息',
    type: UserEntity,
  })
  async info(@User('uuid') uuid: string): Promise<UserEntity | null> {
    this.logger.log('获取当前用户信息:', uuid);
    return this.userService.findUserByUUID(uuid);
  }

这样写的话在 API 文档中只会有 UserEntity 类型,而不会有 codesuccess 字段,期望返回如下数据结构

{
	code: 200,
	success: true,
	data: UserEntity
}

可以通过自定义一个 Swagger 装饰器来实现

// result.decorator.ts
/**
 * 参考: https://blog.csdn.net/kuizuo12/article/details/131778404
 * 参考: https://github.com/wenqiyun/nest-admin/blob/dev/servers/src/common/decorators/api-result.decorator.ts
 */
import { Type, applyDecorators } from '@nestjs/common';
import { ApiExtraModels, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
import { ResultData } from '../model/result.model';
 
const baseTypeNames = ['String', 'Number', 'Boolean'];
/**
 * 封装 swagger 返回统一结构
 * 支持复杂类型 {  code, success, data }
 * @param model 返回的 data 的数据类型
 * @param isArray data 是否是数组
 * @param isPager 设置为 true, 则 data 类型为 {
 * list,
 * meta:{
 *  currentPage, isFirstPage, isLastPage, previoudPage, nextPage, pageCount,}
 * }
 * false data 类型是纯数组
 */
export const ApiResult = <TModel extends Type<any>>(
  model?: TModel,
  isArray?: boolean,
  isPager?: boolean,
) => {
  let items: any = null;
  const modelIsBaseType = model && baseTypeNames.includes(model.name);
  if (modelIsBaseType) {
    items = { type: model.name.toLocaleLowerCase() };
  } else {
    if (model) {
      items = { $ref: getSchemaPath(model) };
    }
  }
 
  let prop: any = null;
  if (isArray && isPager) {
    prop = {
      type: 'object',
      properties: {
        list: {
          type: 'array',
          items,
        },
        // 分页类型符合prisma-extension-pagination 扩展,
        meta: {
          type: 'object',
          properties: {
            currentPage: {
              type: 'number',
            },
            isFirstPage: {
              type: 'boolean',
            },
            isLastPage: {
              type: 'boolean',
            },
            previoudPage: {
              type: 'number',
            },
            nextPage: {
              type: 'number',
            },
            pageCount: {
              type: 'number',
            },
            totalCount: {
              type: 'number',
            },
          },
        },
      },
    };
  } else if (isArray) {
    prop = {
      type: 'array',
      items,
    };
  } else if (model) {
    prop = items;
  } else {
    prop = { type: 'null', default: null };
  }
  return applyDecorators(
    ApiExtraModels(
      ...(model && !modelIsBaseType ? [ResultData, model] : [ResultData]),
    ),
    ApiOkResponse({
      schema: {
        allOf: [
          { $ref: getSchemaPath(ResultData) },
          {
            properties: {
              data: prop,
            },
          },
        ],
      },
    }),
  );
};

ApiResult 支持返回普通的 Object 结构,也支持返回分页数据结构,因为分页使用的是 NestJS Prisma 中使用分页扩展, 所以这里需要定义好 meta 类型,使用方式

  @Get('/list')
  @ApiOperation({ summary: '用户列表(分页)' })
  @ApiResult(UserEntity, true, true)
  async list(){
    return this.userService.list();
  }

API文档就变成了下面这样

{
  "code": 200,
  "success": true,
  "data": {
    "list": [
      {
        "id": 0,
        "uuid": "string",
        "username": "string",
        "mobile": "string",
        "role": "FREE",
        "ruleId": 0,
        "expirationDate": {},
        "maxCollections": 0,
        "createdAt": "2024-07-04T08:11:15.837Z"
      }
    ],
    "meta": {
      "currentPage": 0,
      "isFirstPage": true,
      "isLastPage": true,
      "previoudPage": 0,
      "nextPage": 0,
      "pageCount": 0,
      "totalCount": 0
    }
  }
}