[{"data":1,"prerenderedAt":142},["ShallowReactive",2],{"content:\u002Fposts\u002F2025\u002Fsnippets-nestjs-custom-decorators":3,"surround:\u002Fposts\u002F2025\u002Fsnippets-nestjs-custom-decorators":130},{"id":4,"title":5,"body":6,"canonical":106,"categories":107,"date":109,"description":110,"draft":111,"extension":112,"image":106,"meta":113,"navigation":114,"path":115,"permalink":106,"readingTime":116,"recommend":106,"references":106,"seo":121,"sitemap":122,"stem":123,"tags":124,"type":106,"updated":109,"__hash__":129},"content\u002Fposts\u002F2025\u002Fsnippets-nestjs-custom-decorators.md","一些 Nestjs 的自定义装饰器",{"type":7,"value":8,"toc":99},"minimark",[9,14,23,49,52,55,74,77,80],[10,11,13],"h2",{"id":12},"auth-decorator","Auth Decorator",[15,16,17,18,22],"p",{},"在使用了 ",[19,20,21],"code",{"code":21},"@nestjs\u002Fpassport"," 进行用户登录验证后，我们时长需要一个装饰器来限制用户需要登录。",[24,25,27,41],"tab",{":tabs":26},"[\"代码片段\", \"使用示例\"]",[28,29,31],"template",{"v-slot:tab1":30},"",[32,33,39],"pre",{"className":34,"code":36,"filename":37,"language":38,"meta":30},[35],"language-ts","import { applyDecorators, UseGuards } from '@nestjs\u002Fcommon';\nimport { AuthGuard } from '@nestjs\u002Fpassport';\n\nexport function Auth() {\n    return applyDecorators(UseGuards(AuthGuard('jwt')));\n}\n","auth.decorator.ts","ts",[19,40,36],{"__ignoreMap":30},[28,42,43],{"v-slot:tab2":30},[32,44,47],{"className":45,"code":46,"language":38,"meta":30},[35],"@Auth() \u002F\u002F 此接口需要用户登录后才能使用\ngetUsers() {\n\n}\n",[19,48,46],{"__ignoreMap":30},[10,50,51],{"id":51},"当前登录用户",[15,53,54],{},"在登录后，我们时常需要获取当前登录用户的信息。这时候就可以配合之前的 AuthDecorator 来创建一个参数装饰器获取当前用户了",[24,56,57,66],{":tabs":26},[28,58,59],{"v-slot:tab1":30},[32,60,64],{"className":61,"code":62,"filename":63,"language":38,"meta":30},[35],"import { createParamDecorator } from '@nestjs\u002Fcommon';\nimport { ExecutionContextHost } from '@nestjs\u002Fcore\u002Fhelpers\u002Fexecution-context-host';\n\nexport const CurrentUser = createParamDecorator((data: unknown, ctx: ExecutionContextHost) => {\n    const request = ctx.switchToHttp().getRequest();\n    \u002F\u002F eslint-disable-next-line @typescript-eslint\u002Fno-unsafe-return\n    return request.user;\n});\n","current-user.decorator.ts",[19,65,62],{"__ignoreMap":30},[28,67,68],{"v-slot:tab2":30},[32,69,72],{"className":70,"code":71,"language":38,"meta":30},[35],"@Auth()\ngetUsers(@CurrentUser() user) {\n  console.log('当前登录用户', user)\n}\n",[19,73,71],{"__ignoreMap":30},[10,75,76],{"id":76},"合并路由参数",[15,78,79],{},"我们可能需要将 ID 等参数信息合并到 Body 中，并且自动转换为 number 类型",[24,81,82,91],{":tabs":26},[28,83,84],{"v-slot:tab1":30},[32,85,89],{"className":86,"code":87,"filename":88,"language":38,"meta":30},[35],"import { BadRequestException, createParamDecorator, ExecutionContext } from '@nestjs\u002Fcommon';\nimport { plainToInstance } from 'class-transformer';\nimport { validate, ValidationError } from 'class-validator';\n\n\u002F**\n * 将路由参数合并到 Body 中，然后进行验证\n * 用于在编辑场景中自动将 id 等参数合并到 DTO，使验证器能够正确工作\n *\n * @example\n * @Patch(':id')\n * update(\n *   @Param('id', ParseIntPipe) id: number,\n *   @BodyWithParams(UpdateUserDto) updateDto: UpdateUserDto,\n * ) {\n *   \u002F\u002F updateDto.id 已经包含了路由参数中的 id (已转换为 number)\n *   return this.userService.update(id, updateDto);\n * }\n *\u002F\nexport const BodyWithParams = createParamDecorator(async (dtoClass: any, ctx: ExecutionContext) => {\n    const request = ctx.switchToHttp().getRequest();\n    const body = request.body;\n    const params = request.params;\n\n    \u002F\u002F 合并 params 和 body (params 中的值优先，避免被篡改)\n    const merged = { ...body, ...params };\n\n    \u002F\u002F 转换参数类型（id 从 string 转为 number）\n    if (merged.id && typeof merged.id === 'string') {\n        merged.id = Number.parseInt(merged.id, 10);\n    }\n\n    \u002F\u002F 转换为 DTO 实例\n    const dtoInstance = plainToInstance(dtoClass, merged);\n\n    \u002F\u002F 执行验证\n    const errors = await validate(dtoInstance);\n    if (errors.length > 0) {\n    \u002F\u002F 格式化错误信息，保持与 FieldValidationPipe 一致的格式\n        const formatErrors = (errs: ValidationError[]): any[] => {\n            return errs.map(err => ({\n                field: err.property,\n                message: Object.values(err.constraints || {}),\n            }));\n        };\n\n        throw new BadRequestException(formatErrors(errors));\n    }\n\n    return dtoInstance;\n});\n","body-with-params.decorator.ts",[19,90,87],{"__ignoreMap":30},[28,92,93],{"v-slot:tab2":30},[32,94,97],{"className":95,"code":96,"language":38,"meta":30},[35],"@Patch(':id')\nupdate(\n  @Param('id') id: string,\n  @BodyWithParams(UpdateDictItemDto) updateDictItemDto: UpdateDictItemDto,\n) {\n  return this.dictItemService.update(+id, updateDictItemDto);\n}\n\n\u002F\u002F UpdateDictItemDto\nimport { IsNotEmpty } from 'class-validator';\n\nimport { IsUnique } from '@\u002Fcommon\u002Fvalidation';\n\nexport class UpdateDictItemDto {\n  id?: number;\n\n  @IsNotEmpty()\n  typeId: number;\n\n  @IsNotEmpty()\n  label: string;\n\n  @IsNotEmpty()\n  @IsUnique('dictItem', { message: '字典项值已存在', excludeField: 'id' })\n  value: string;\n\n  order?: number;\n\n  status?: boolean;\n\n  remark?: string;\n}\n",[19,98,96],{"__ignoreMap":30},{"title":30,"searchDepth":100,"depth":100,"links":101},4,[102,104,105],{"id":12,"depth":103,"text":13},2,{"id":51,"depth":103,"text":51},{"id":76,"depth":103,"text":76},null,[108],"代码","2025-12-31 15:05:56","可以直接复制使用的 Nestjs 自定义装饰器",false,"md",{},true,"\u002Fposts\u002F2025\u002Fsnippets-nestjs-custom-decorators",{"text":117,"minutes":118,"time":119,"words":120},"3 min read",2.635,158100,527,{"title":5,"description":110},{"loc":115},"posts\u002F2025\u002Fsnippets-nestjs-custom-decorators",[125,126,127,128],"Nest","Decorator","装饰器","代码片段","JDjI2S4YFXoEMkCyn3u0HFM7dFzk5k0CszCqviC_yfc",[131,136],{"title":132,"path":133,"stem":134,"date":135,"type":106,"children":-1},"Nestjs + Drizzle 项目中一些常用的自定义校验","\u002Fposts\u002F2025\u002Fsnippets-nestjs-drizzle-custom-validation","posts\u002F2025\u002Fsnippets-nestjs-drizzle-custom-validation","2025-12-30 17:14:59",{"title":137,"path":138,"stem":139,"date":140,"type":141,"children":-1},"2025年终总结","\u002Fposts\u002F2025\u002F2025-year-end-summary","posts\u002F2025\u002F2025-year-end-summary","2025-12-31 15:35:20","story",1789642587860]