[{"data":1,"prerenderedAt":3445},["ShallowReactive",2],{"page-/post/nest/nest-project-quick-start":3,"surrounding-page":3436},{"id":4,"title":5,"author":6,"body":7,"date":3420,"description":3421,"extension":3422,"group":3423,"lastmod":3424,"meta":3425,"navigation":626,"path":3428,"rawbody":3429,"seo":3430,"showTitle":5,"stem":3431,"tags":3432,"versions":3423,"__hash__":3435},"content/post/Nest/nest-project-quick-start.md","一个产品要有一个“好底子”：Nest项目搭建","枣把儿",{"type":8,"value":9,"toc":3406},"minimark",[10,14,17,25,30,41,44,47,50,65,68,71,74,77,80,83,86,89,93,96,126,129,145,148,151,154,159,162,178,184,187,200,203,206,231,234,239,242,262,265,375,378,424,427,535,538,735,738,758,761,764,799,802,805,808,811,814,817,820,823,839,842,872,875,878,881,887,892,895,900,903,917,920,1064,1067,1070,1073,1098,1101,1138,1142,1145,1153,1156,1460,1463,1508,1511,1560,1564,1567,1589,1592,1907,1910,1973,1976,2022,2025,2085,2088,2091,2094,2169,2172,2231,2234,2239,2242,2245,2248,2268,2271,2488,2491,2573,2579,2582,2585,2588,2609,2612,3159,3162,3182,3185,3347,3350,3353,3356,3359,3362,3365,3368,3379,3396,3399,3402],[11,12,13],"p",{},"大家好，我是枣把儿。",[11,15,16],{},"上周搞了一个前端小项目：Pixeled Pic Pro， 是一个用来制作像素风格LOGO的Canvas编辑器。",[11,18,19,20,24],{},"同样，它的后端也是",[21,22,23],"strong",{},"本着能用就行，先把功能搞出来","的原则。我来分享一下后端实现过程以及发生的故事~",[26,27,29],"h2",{"id":28},"底子","“底子”",[11,31,32,33,40],{},"我一开始要做的项目起名叫：",[34,35,39],"a",{"href":36,"rel":37},"https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A",[38],"nofollow","早早集市","。这是一个从不知道什么时候到23年8月份左右完成构思，在方向上开始清晰起来（当时是这么认为的）的项目。",[11,42,43],{},"原因就是，平时脑子里想法太多，多到必须下来，越写越多之后，我就在想怎么把他们搞出来，并且搞的有点联系。",[11,45,46],{},"然后再去了几次夜市吃喝之后，我就有了点灵感：我要不整个电子集市吧！",[11,48,49],{},"和大家去赶大集一样，每个产品相当于一个摊位，可以在一个入口里，看到所有在营业的\"摊位\"，并且像大集里的摊主一样，每个产品也是在向互联网用户提供服务或商品。",[11,51,52,53,56,57,60,61,64],{},"我仔细想想之后啊，感觉真不错。摊位五花八门，没有限制，我的想法也是天马行空，指不定想做什么，可以取悦自己；有的摊位提供“",[21,54,55],{},"商品","”；有的摊位提供“",[21,58,59],{},"服务","”；还有的摊位给别的摊位提供商品，个体也可以直接去他那“",[21,62,63],{},"进货","”。",[11,66,67],{},"打通了自己的想法之后，就越想越顺，也越想越复杂。生怕架构层面无法满足自己的设想。",[11,69,70],{},"搜索了很久微服务架构相关文章，问了下前同事（Java、运维）相关的思路（你问我为什么不问现同事？也问了，大部分表示就是用Spring全家桶，也知道微服务，也知道消息队列，也知道k8s。问怎么实现的、怎么设计的。不知道），最后也是给自己泼了泼冷水。",[11,72,73],{},"算了，不整那么复杂了，本来搞个项目，也是为了万一35岁以后真不干前端了，给自己留点\"互联网遗产\"，证明自己来过。别还没开始就给自己折腾\"死了\"。",[11,75,76],{},"冷却下来之后，我还发现，这玩意搞好了是个集市，搞不好不就是个工具站吗，网上一搜一大堆！",[11,78,79],{},"你看，果然还是打退堂鼓的时候思路更清晰一些。",[11,81,82],{},"但好在这次的构思过程足够深入，冷静下来之后还是让我感觉值得做下去，所以还是继续开始了这个故事。",[11,84,85],{},"所以，Pixeled Pic Pro 也是其中一个“摊位”，摊主(名字待定)提供的正是“服务”。",[11,87,88],{},"听完了故事，那就一起开始摆摊吧。",[26,90,92],{"id":91},"新建nest项目","新建Nest项目",[11,94,95],{},"nest提供了 @nestjs/cli 这个包，先来安装一下",[97,98,103],"pre",{"className":99,"code":100,"language":101,"meta":102,"style":102},"language-shell shiki shiki-themes github-light","npm i -g @nestjs/cli\n","shell","",[104,105,106],"code",{"__ignoreMap":102},[107,108,111,115,119,123],"span",{"class":109,"line":110},"line",1,[107,112,114],{"class":113},"s7eDp","npm",[107,116,118],{"class":117},"sYBdl"," i",[107,120,122],{"class":121},"sYu0t"," -g",[107,124,125],{"class":117}," @nestjs/cli\n",[11,127,128],{},"已经安装完了的话，可以升级一下",[97,130,132],{"className":99,"code":131,"language":101,"meta":102,"style":102},"npm update -g @nestjs/cli\n",[104,133,134],{"__ignoreMap":102},[107,135,136,138,141,143],{"class":109,"line":110},[107,137,114],{"class":113},[107,139,140],{"class":117}," update",[107,142,122],{"class":121},[107,144,125],{"class":117},[11,146,147],{},"安装完成之后可以使用 ==nest -h== 查看有哪些命令，后续会经常用到",[11,149,150],{},"其中 ==--no-spec== 可以指定不生成测试文件，后面会用到",[11,152,153],{},"这里我把服务分为两个，一个gateway服务，用来对外实现api接口及鉴权。 一个主服务，实现所有业务。除非不能满足业务，否则不再拆分。",[11,155,156],{},[21,157,158],{},"PS: 拆出gateway只是为了在实际业务中感受它的好处和坏处，大家自行甄别、自由选择",[11,160,161],{},"开始创建项目！",[97,163,165],{"className":99,"code":164,"language":101,"meta":102,"style":102},"nest new 项目名\n",[104,166,167],{"__ignoreMap":102},[107,168,169,172,175],{"class":109,"line":110},[107,170,171],{"class":113},"nest",[107,173,174],{"class":117}," new",[107,176,177],{"class":117}," 项目名\n",[11,179,180],{},[181,182],"img",{"alt":102,"src":183},"https://img.zzao.club/article/202411191446171.png",[11,185,186],{},"选择pnpm后等待安装完成，完成后已经可以运行， 进入项目根目录",[97,188,190],{"className":99,"code":189,"language":101,"meta":102,"style":102},"pnpm start:dev\n",[104,191,192],{"__ignoreMap":102},[107,193,194,197],{"class":109,"line":110},[107,195,196],{"class":113},"pnpm",[107,198,199],{"class":117}," start:dev\n",[11,201,202],{},"打开网站localhost:3000，可以看到Hello World！字样",[11,204,205],{},"因为我这里需要有另一个网关服务，所以我再新建一个app，通过monorepo的方式管理",[97,207,209],{"className":99,"code":208,"language":101,"meta":102,"style":102},"# generator 可以缩写为 g\nnest g app gateway\n",[104,210,211,217],{"__ignoreMap":102},[107,212,213],{"class":109,"line":110},[107,214,216],{"class":215},"sAwPA","# generator 可以缩写为 g\n",[107,218,220,222,225,228],{"class":109,"line":219},2,[107,221,171],{"class":113},[107,223,224],{"class":117}," g",[107,226,227],{"class":117}," app",[107,229,230],{"class":117}," gateway\n",[11,232,233],{},"此时可以看到app gateway 已经被创建， 同时自动创建了一个apps文件夹，里面包含两个app",[11,235,236],{},[181,237],{"alt":102,"src":238},"https://img.zzao.club/article/202411191446172.png",[11,240,241],{},"安装微服务需要的包：",[97,243,245],{"className":99,"code":244,"language":101,"meta":102,"style":102},"# add会安装在dependencies  加参数 -D 会安装在 devDependencies\npnpm add @nestjs/microservices\n",[104,246,247,252],{"__ignoreMap":102},[107,248,249],{"class":109,"line":110},[107,250,251],{"class":215},"# add会安装在dependencies  加参数 -D 会安装在 devDependencies\n",[107,253,254,256,259],{"class":109,"line":219},[107,255,196],{"class":113},[107,257,258],{"class":117}," add",[107,260,261],{"class":117}," @nestjs/microservices\n",[11,263,264],{},"然后改造一下server部分，因为gateway在前，server在后，所以server需要改成微服务，通过TCP和gateway通信",[97,266,270],{"className":267,"code":268,"language":269,"meta":102,"style":102},"language-typescript shiki shiki-themes github-light","const app = await NestFactory.createMicroservice\u003CMicroserviceOptions>(\n    AppModule,\n    {\n      transport: Transport.TCP,\n      options: {\n        port: 7577,\n      },\n    },\n  );\n  await app.listen();\n","typescript",[104,271,272,302,307,313,325,331,342,348,354,360],{"__ignoreMap":102},[107,273,274,278,280,283,286,290,293,296,299],{"class":109,"line":110},[107,275,277],{"class":276},"sD7c4","const",[107,279,227],{"class":121},[107,281,282],{"class":276}," =",[107,284,285],{"class":276}," await",[107,287,289],{"class":288},"sgsFI"," NestFactory.",[107,291,292],{"class":113},"createMicroservice",[107,294,295],{"class":288},"\u003C",[107,297,298],{"class":113},"MicroserviceOptions",[107,300,301],{"class":288},">(\n",[107,303,304],{"class":109,"line":219},[107,305,306],{"class":288},"    AppModule,\n",[107,308,310],{"class":109,"line":309},3,[107,311,312],{"class":288},"    {\n",[107,314,316,319,322],{"class":109,"line":315},4,[107,317,318],{"class":288},"      transport: Transport.",[107,320,321],{"class":121},"TCP",[107,323,324],{"class":288},",\n",[107,326,328],{"class":109,"line":327},5,[107,329,330],{"class":288},"      options: {\n",[107,332,334,337,340],{"class":109,"line":333},6,[107,335,336],{"class":288},"        port: ",[107,338,339],{"class":121},"7577",[107,341,324],{"class":288},[107,343,345],{"class":109,"line":344},7,[107,346,347],{"class":288},"      },\n",[107,349,351],{"class":109,"line":350},8,[107,352,353],{"class":288},"    },\n",[107,355,357],{"class":109,"line":356},9,[107,358,359],{"class":288},"  );\n",[107,361,363,366,369,372],{"class":109,"line":362},10,[107,364,365],{"class":276},"  await",[107,367,368],{"class":288}," app.",[107,370,371],{"class":113},"listen",[107,373,374],{"class":288},"();\n",[11,376,377],{},"然后在app.controller.ts里改一下接口，一会用来测试一下",[97,379,381],{"className":267,"code":380,"language":269,"meta":102,"style":102},"@MessagePattern('hello')\n  getHello(): string {\n    return 'hello by zzstudio-server';\n  }\n",[104,382,383,400,408,419],{"__ignoreMap":102},[107,384,385,388,391,394,397],{"class":109,"line":110},[107,386,387],{"class":288},"@",[107,389,390],{"class":113},"MessagePattern",[107,392,393],{"class":288},"(",[107,395,396],{"class":117},"'hello'",[107,398,399],{"class":288},")\n",[107,401,402,405],{"class":109,"line":219},[107,403,404],{"class":113},"  getHello",[107,406,407],{"class":288},"(): string {\n",[107,409,410,413,416],{"class":109,"line":309},[107,411,412],{"class":276},"    return",[107,414,415],{"class":117}," 'hello by zzstudio-server'",[107,417,418],{"class":288},";\n",[107,420,421],{"class":109,"line":315},[107,422,423],{"class":288},"  }\n",[11,425,426],{},"来到gateway这边， gateway.module.ts里同样也要注册一下微服务，端口号和上面对应起来",[97,428,430],{"className":267,"code":429,"language":269,"meta":102,"style":102},"@Module({\n  imports: [\n    ClientsModule.register([\n      {\n        name: 'ZZSTUDIO_SERVER',\n        transport: Transport.TCP,\n        options: {\n          port: 7577,\n        },\n      },\n    ]),\n  ],\n  controllers: [GatewayController],\n  providers: [GatewayService],\n})\n",[104,431,432,442,447,458,463,473,482,487,496,501,505,511,517,523,529],{"__ignoreMap":102},[107,433,434,436,439],{"class":109,"line":110},[107,435,387],{"class":288},[107,437,438],{"class":113},"Module",[107,440,441],{"class":288},"({\n",[107,443,444],{"class":109,"line":219},[107,445,446],{"class":288},"  imports: [\n",[107,448,449,452,455],{"class":109,"line":309},[107,450,451],{"class":288},"    ClientsModule.",[107,453,454],{"class":113},"register",[107,456,457],{"class":288},"([\n",[107,459,460],{"class":109,"line":315},[107,461,462],{"class":288},"      {\n",[107,464,465,468,471],{"class":109,"line":327},[107,466,467],{"class":288},"        name: ",[107,469,470],{"class":117},"'ZZSTUDIO_SERVER'",[107,472,324],{"class":288},[107,474,475,478,480],{"class":109,"line":333},[107,476,477],{"class":288},"        transport: Transport.",[107,479,321],{"class":121},[107,481,324],{"class":288},[107,483,484],{"class":109,"line":344},[107,485,486],{"class":288},"        options: {\n",[107,488,489,492,494],{"class":109,"line":350},[107,490,491],{"class":288},"          port: ",[107,493,339],{"class":121},[107,495,324],{"class":288},[107,497,498],{"class":109,"line":356},[107,499,500],{"class":288},"        },\n",[107,502,503],{"class":109,"line":362},[107,504,347],{"class":288},[107,506,508],{"class":109,"line":507},11,[107,509,510],{"class":288},"    ]),\n",[107,512,514],{"class":109,"line":513},12,[107,515,516],{"class":288},"  ],\n",[107,518,520],{"class":109,"line":519},13,[107,521,522],{"class":288},"  controllers: [GatewayController],\n",[107,524,526],{"class":109,"line":525},14,[107,527,528],{"class":288},"  providers: [GatewayService],\n",[107,530,532],{"class":109,"line":531},15,[107,533,534],{"class":288},"})\n",[11,536,537],{},"然后在gateway.controller.ts里也写个方法测试一下\n对了，先用@Inject注入刚才注册的。可以看到有个ts报错，我们回到server那边",[97,539,541],{"className":267,"code":540,"language":269,"meta":102,"style":102},"@Controller()\nexport class GatewayController {\n  @Inject('ZZSTUDIO_SERVER')\n  private serverClient: ClientProxy;\n  constructor(private readonly gatewayService: GatewayService) {}\n\n  @Get()\n  getHello(): string {\n    return this.gatewayService.getHello();\n  }\n\n  @Get('app')\n  getServerHello(): unknown {\n    return this.serverClient.send('hello', 'hello');\n  }\n}\n",[104,542,543,553,567,581,598,622,628,637,651,666,670,674,687,701,725,729],{"__ignoreMap":102},[107,544,545,547,550],{"class":109,"line":110},[107,546,387],{"class":288},[107,548,549],{"class":113},"Controller",[107,551,552],{"class":288},"()\n",[107,554,555,558,561,564],{"class":109,"line":219},[107,556,557],{"class":276},"export",[107,559,560],{"class":276}," class",[107,562,563],{"class":113}," GatewayController",[107,565,566],{"class":288}," {\n",[107,568,569,572,575,577,579],{"class":109,"line":309},[107,570,571],{"class":288},"  @",[107,573,574],{"class":113},"Inject",[107,576,393],{"class":288},[107,578,470],{"class":117},[107,580,399],{"class":288},[107,582,583,586,590,593,596],{"class":109,"line":315},[107,584,585],{"class":276},"  private",[107,587,589],{"class":588},"sqxcx"," serverClient",[107,591,592],{"class":276},":",[107,594,595],{"class":113}," ClientProxy",[107,597,418],{"class":288},[107,599,600,603,605,608,611,614,616,619],{"class":109,"line":327},[107,601,602],{"class":276},"  constructor",[107,604,393],{"class":288},[107,606,607],{"class":276},"private",[107,609,610],{"class":276}," readonly",[107,612,613],{"class":588}," gatewayService",[107,615,592],{"class":276},[107,617,618],{"class":113}," GatewayService",[107,620,621],{"class":288},") {}\n",[107,623,624],{"class":109,"line":333},[107,625,627],{"emptyLinePlaceholder":626},true,"\n",[107,629,630,632,635],{"class":109,"line":344},[107,631,571],{"class":288},[107,633,634],{"class":113},"Get",[107,636,552],{"class":288},[107,638,639,641,644,646,649],{"class":109,"line":350},[107,640,404],{"class":113},[107,642,643],{"class":288},"()",[107,645,592],{"class":276},[107,647,648],{"class":121}," string",[107,650,566],{"class":288},[107,652,653,655,658,661,664],{"class":109,"line":356},[107,654,412],{"class":276},[107,656,657],{"class":121}," this",[107,659,660],{"class":288},".gatewayService.",[107,662,663],{"class":113},"getHello",[107,665,374],{"class":288},[107,667,668],{"class":109,"line":362},[107,669,423],{"class":288},[107,671,672],{"class":109,"line":507},[107,673,627],{"emptyLinePlaceholder":626},[107,675,676,678,680,682,685],{"class":109,"line":513},[107,677,571],{"class":288},[107,679,634],{"class":113},[107,681,393],{"class":288},[107,683,684],{"class":117},"'app'",[107,686,399],{"class":288},[107,688,689,692,694,696,699],{"class":109,"line":519},[107,690,691],{"class":113},"  getServerHello",[107,693,643],{"class":288},[107,695,592],{"class":276},[107,697,698],{"class":121}," unknown",[107,700,566],{"class":288},[107,702,703,705,707,710,713,715,717,720,722],{"class":109,"line":525},[107,704,412],{"class":276},[107,706,657],{"class":121},[107,708,709],{"class":288},".serverClient.",[107,711,712],{"class":113},"send",[107,714,393],{"class":288},[107,716,396],{"class":117},[107,718,719],{"class":288},", ",[107,721,396],{"class":117},[107,723,724],{"class":288},");\n",[107,726,727],{"class":109,"line":531},[107,728,423],{"class":288},[107,730,732],{"class":109,"line":731},16,[107,733,734],{"class":288},"}\n",[11,736,737],{},"把两个项目跑起来测试一下， 因为我们用的zzstudio-server新建的项目，所以跑dev默认启动的是zzstudio-server。",[97,739,741],{"className":99,"code":740,"language":101,"meta":102,"style":102},"pnpm start:dev\npnpm start:dev gateway\n",[104,742,743,749],{"__ignoreMap":102},[107,744,745,747],{"class":109,"line":110},[107,746,196],{"class":113},[107,748,199],{"class":117},[107,750,751,753,756],{"class":109,"line":219},[107,752,196],{"class":113},[107,754,755],{"class":117}," start:dev",[107,757,230],{"class":117},[11,759,760],{},"然后浏览器输入localhost:3000/app，可以看到hello by zzstudio-server，通了。",[11,762,763],{},"然后再试试打包",[97,765,767],{"className":99,"code":766,"language":101,"meta":102,"style":102},"# 这会打包zzstuido-server服务\npnpm build\n\n# 这会打包gateway服务\npnpm build gateway\n",[104,768,769,774,781,785,790],{"__ignoreMap":102},[107,770,771],{"class":109,"line":110},[107,772,773],{"class":215},"# 这会打包zzstuido-server服务\n",[107,775,776,778],{"class":109,"line":219},[107,777,196],{"class":113},[107,779,780],{"class":117}," build\n",[107,782,783],{"class":109,"line":309},[107,784,627],{"emptyLinePlaceholder":626},[107,786,787],{"class":109,"line":315},[107,788,789],{"class":215},"# 这会打包gateway服务\n",[107,791,792,794,797],{"class":109,"line":327},[107,793,196],{"class":113},[107,795,796],{"class":117}," build",[107,798,230],{"class":117},[11,800,801],{},"打包完成后，可以看到dist里分别产生了各自服务的文件夹",[26,803,804],{"id":804},"功能梳理",[11,806,807],{},"还是按照以前的习惯，做事之前先梳理和拆解，只要不影响核心功能，就放在下一个版本迭代。",[11,809,810],{},"也许会有一些同学奇怪，明明是自己的产品，为什么要和公司打工一样，还要搞版本，还要搞大纲，我在公司都没这么搞！",[11,812,813],{},"我是这样理解的：首先做产品的核心是==要把一个产品实现==，==过程有序，结果不遗漏==，就和你记账一样，如果你不记的很细致，你就无法总结到底哪些地方不该花钱。其次，你不能等有了用户，再重新完善文档，==因为你说不好是你的产品、还是你的故事、还是你的过程吸引了别人==。最后，这是自己的产品，是自己内心的==乌托邦==，你会本能的对它倾注更多的心血。",[11,815,816],{},"这样也能明白，为什么在公司打工为什么提不起劲儿来，因为它不是你的，也不是你感兴趣的，只是一个赚取收入的渠道。 同时也可以知道，如果真的能把公司的产品，代入到自己的产品中，同时被公司领导们注意到，且不被自己的小领导窃取成果，且愿意推举给大领导，且老板也有正确的认知，公司也会因你而精彩（狗头保命）",[11,818,819],{},"说完了废话，开始正题。",[11,821,822],{},"首先gateway部分。",[824,825,826,830,833,836],"ol",{},[827,828,829],"li",{},"对外提供接口，可以起一个公共的前缀，比如/api/v1。",[827,831,832],{},"如果前端发生了改动，则去修改gateway里的请求逻辑，主服务不需要变",[827,834,835],{},"实现鉴权。jwt 双token，前端无感刷新，过滤掉没权限的请求。",[827,837,838],{},"如果主服务发生了改动，则去修改gateway里的请求逻辑，前端不需要变",[11,840,841],{},"接口目前很简单：",[824,843,844,855,866,869],{},[827,845,846,847],{},"登录注册\n",[824,848,849,852],{},[827,850,851],{},"先用用户名密码+邮箱验证码注册",[827,853,854],{},"后续再添加关注公众号注册之类的操作",[827,856,857,858],{},"导出功能\n",[824,859,860,863],{},[827,861,862],{},"次数统计 看看有多少人使用了导出。 相当于埋点了",[827,864,865],{},"导出并压缩 （这是一个不着急实现的公共功能，可以预见其他的产品也会有这个功能）",[827,867,868],{},"保存预设，json",[827,870,871],{},"保存图片，以一种字符串或者json的形式",[11,873,874],{},"其中3，4都不是必须的，先放一放。只要实现了框架结构和基本功能，后续按功能再加就很快了",[26,876,877],{"id":877},"功能实现",[11,879,880],{},"实现之前先用图来串一串思路。",[882,883,884],"blockquote",{},[11,885,886],{},"用的Obsidian的Excalidraw画的",[11,888,889],{},[181,890],{"alt":102,"src":891},"https://img.zzao.club/article/202411191446173.png",[11,893,894],{},"然后开始按照这个思路去实现功能，这里我只演示几个关键点。同样代码贴在文末，免费、开源",[896,897,899],"h3",{"id":898},"jwt模块注册","JWT模块注册",[11,901,902],{},"安装 @nestjs/jwt",[97,904,906],{"className":99,"code":905,"language":101,"meta":102,"style":102},"pnpm add @nestjs/jwt\n",[104,907,908],{"__ignoreMap":102},[107,909,910,912,914],{"class":109,"line":110},[107,911,196],{"class":113},[107,913,258],{"class":117},[107,915,916],{"class":117}," @nestjs/jwt\n",[11,918,919],{},"gateway.module.ts里注册",[97,921,923],{"className":267,"code":922,"language":269,"meta":102,"style":102},"@Module({\n  imports: [\n    ClientsModule.register([\n      {\n        name: 'ZZSTUDIO_SERVER',\n        transport: Transport.TCP,\n        options: {\n          port: 7577,\n        },\n      },\n    ]),\n    JwtModule.register({\n        global: true,\n        secret: 'zzdaddy',\n        signOptions: {\n          expiresIn: '1d',\n        },\n      }),\n  ],\n  controllers: [GatewayController],\n  providers: [GatewayService],\n})\n",[104,924,925,933,937,945,949,957,965,969,977,981,985,989,998,1008,1018,1023,1033,1038,1044,1049,1054,1059],{"__ignoreMap":102},[107,926,927,929,931],{"class":109,"line":110},[107,928,387],{"class":288},[107,930,438],{"class":113},[107,932,441],{"class":288},[107,934,935],{"class":109,"line":219},[107,936,446],{"class":288},[107,938,939,941,943],{"class":109,"line":309},[107,940,451],{"class":288},[107,942,454],{"class":113},[107,944,457],{"class":288},[107,946,947],{"class":109,"line":315},[107,948,462],{"class":288},[107,950,951,953,955],{"class":109,"line":327},[107,952,467],{"class":288},[107,954,470],{"class":117},[107,956,324],{"class":288},[107,958,959,961,963],{"class":109,"line":333},[107,960,477],{"class":288},[107,962,321],{"class":121},[107,964,324],{"class":288},[107,966,967],{"class":109,"line":344},[107,968,486],{"class":288},[107,970,971,973,975],{"class":109,"line":350},[107,972,491],{"class":288},[107,974,339],{"class":121},[107,976,324],{"class":288},[107,978,979],{"class":109,"line":356},[107,980,500],{"class":288},[107,982,983],{"class":109,"line":362},[107,984,347],{"class":288},[107,986,987],{"class":109,"line":507},[107,988,510],{"class":288},[107,990,991,994,996],{"class":109,"line":513},[107,992,993],{"class":288},"    JwtModule.",[107,995,454],{"class":113},[107,997,441],{"class":288},[107,999,1000,1003,1006],{"class":109,"line":519},[107,1001,1002],{"class":288},"        global: ",[107,1004,1005],{"class":121},"true",[107,1007,324],{"class":288},[107,1009,1010,1013,1016],{"class":109,"line":525},[107,1011,1012],{"class":288},"        secret: ",[107,1014,1015],{"class":117},"'zzdaddy'",[107,1017,324],{"class":288},[107,1019,1020],{"class":109,"line":531},[107,1021,1022],{"class":288},"        signOptions: {\n",[107,1024,1025,1028,1031],{"class":109,"line":731},[107,1026,1027],{"class":288},"          expiresIn: ",[107,1029,1030],{"class":117},"'1d'",[107,1032,324],{"class":288},[107,1034,1036],{"class":109,"line":1035},17,[107,1037,500],{"class":288},[107,1039,1041],{"class":109,"line":1040},18,[107,1042,1043],{"class":288},"      }),\n",[107,1045,1047],{"class":109,"line":1046},19,[107,1048,516],{"class":288},[107,1050,1052],{"class":109,"line":1051},20,[107,1053,522],{"class":288},[107,1055,1057],{"class":109,"line":1056},21,[107,1058,528],{"class":288},[107,1060,1062],{"class":109,"line":1061},22,[107,1063,534],{"class":288},[896,1065,1066],{"id":1066},"自定义decorator",[11,1068,1069],{},"我想设置一个开关，标识哪个接口可以不需要登录就访问，没有这个标识的就都需要鉴权",[11,1071,1072],{},"先自定义一个装饰器，用于设置接口是否是公开的（true），没设置就是false",[97,1074,1076],{"className":99,"code":1075,"language":101,"meta":102,"style":102},"# 在gateway服务下，新建了一个custom文件夹，里面有一个custom.decorator.ts\nnest g decorator custom --project=gateway\n",[104,1077,1078,1083],{"__ignoreMap":102},[107,1079,1080],{"class":109,"line":110},[107,1081,1082],{"class":215},"# 在gateway服务下，新建了一个custom文件夹，里面有一个custom.decorator.ts\n",[107,1084,1085,1087,1089,1092,1095],{"class":109,"line":219},[107,1086,171],{"class":113},[107,1088,224],{"class":117},[107,1090,1091],{"class":117}," decorator",[107,1093,1094],{"class":117}," custom",[107,1096,1097],{"class":121}," --project=gateway\n",[11,1099,1100],{},"实现装饰器 custom.decorator.ts",[97,1102,1104],{"className":267,"code":1103,"language":269,"meta":102,"style":102},"export const setPublicRoute = () => SetMetadata('isPublicRoute', true);\n",[104,1105,1106],{"__ignoreMap":102},[107,1107,1108,1110,1113,1116,1118,1121,1124,1127,1129,1132,1134,1136],{"class":109,"line":110},[107,1109,557],{"class":276},[107,1111,1112],{"class":276}," const",[107,1114,1115],{"class":113}," setPublicRoute",[107,1117,282],{"class":276},[107,1119,1120],{"class":288}," () ",[107,1122,1123],{"class":276},"=>",[107,1125,1126],{"class":113}," SetMetadata",[107,1128,393],{"class":288},[107,1130,1131],{"class":117},"'isPublicRoute'",[107,1133,719],{"class":288},[107,1135,1005],{"class":121},[107,1137,724],{"class":288},[896,1139,1141],{"id":1140},"自定义guard","自定义Guard",[11,1143,1144],{},"按照上图的思路，现在应该写一个Guard，用来控制权限",[97,1146,1151],{"className":1147,"code":1149,"language":1150},[1148],"language-text","# 生成后自己改个名， 我改成了LoginGuard\nnest g guard globalGuard --project=gateway --no-spec\n","text",[104,1152,1149],{"__ignoreMap":102},[11,1154,1155],{},"然后在生成的guard里实现",[97,1157,1159],{"className":267,"code":1158,"language":269,"meta":102,"style":102},"canActivate(\n    context: ExecutionContext,\n  ): boolean | Promise\u003Cboolean> | Observable\u003Cboolean> {\n    const request: Request = context.switchToHttp().getRequest();\n\n    const isPublicRoute = this.reflector.getAllAndOverride('isPublicRoute', [\n      context.getClass(),\n      context.getHandler(),\n    ]);\n    if (isPublicRoute) {\n      return true;\n    }\n\n    const authorization = request.headers.authorization;\n\n    if (!authorization) {\n      throw new UnauthorizedException('用户未登录');\n    }\n\n    try {\n      const token = authorization.split(' ')[1];\n      const data = this.jwtService.verify(token);\n      // 这里会报没有user, 可以用declare module 给上边的 Request 在类型空间定义一下user\n      request.user = data.user;\n      return true;\n    } catch (e) {\n      throw new UnauthorizedException('token 失效，请重新登录');\n    }\n  }\n",[104,1160,1161,1169,1174,1207,1229,1233,1255,1266,1275,1280,1288,1293,1298,1302,1312,1316,1329,1344,1348,1352,1357,1384,1402,1408,1419,1424,1436,1450,1455],{"__ignoreMap":102},[107,1162,1163,1166],{"class":109,"line":110},[107,1164,1165],{"class":113},"canActivate",[107,1167,1168],{"class":288},"(\n",[107,1170,1171],{"class":109,"line":219},[107,1172,1173],{"class":288},"    context: ExecutionContext,\n",[107,1175,1176,1179,1182,1185,1187,1190,1193,1196,1199,1201,1203,1205],{"class":109,"line":309},[107,1177,1178],{"class":288},"  ): boolean ",[107,1180,1181],{"class":276},"|",[107,1183,1184],{"class":121}," Promise",[107,1186,295],{"class":276},[107,1188,1189],{"class":288},"boolean",[107,1191,1192],{"class":276},">",[107,1194,1195],{"class":276}," |",[107,1197,1198],{"class":288}," Observable",[107,1200,295],{"class":276},[107,1202,1189],{"class":288},[107,1204,1192],{"class":276},[107,1206,566],{"class":288},[107,1208,1209,1212,1215,1218,1221,1224,1227],{"class":109,"line":315},[107,1210,1211],{"class":288},"    const request: Request ",[107,1213,1214],{"class":276},"=",[107,1216,1217],{"class":288}," context.",[107,1219,1220],{"class":113},"switchToHttp",[107,1222,1223],{"class":288},"().",[107,1225,1226],{"class":113},"getRequest",[107,1228,374],{"class":288},[107,1230,1231],{"class":109,"line":327},[107,1232,627],{"emptyLinePlaceholder":626},[107,1234,1235,1238,1240,1242,1245,1248,1250,1252],{"class":109,"line":333},[107,1236,1237],{"class":288},"    const isPublicRoute ",[107,1239,1214],{"class":276},[107,1241,657],{"class":121},[107,1243,1244],{"class":288},".reflector.",[107,1246,1247],{"class":113},"getAllAndOverride",[107,1249,393],{"class":288},[107,1251,1131],{"class":117},[107,1253,1254],{"class":288},", [\n",[107,1256,1257,1260,1263],{"class":109,"line":344},[107,1258,1259],{"class":288},"      context.",[107,1261,1262],{"class":113},"getClass",[107,1264,1265],{"class":288},"(),\n",[107,1267,1268,1270,1273],{"class":109,"line":350},[107,1269,1259],{"class":288},[107,1271,1272],{"class":113},"getHandler",[107,1274,1265],{"class":288},[107,1276,1277],{"class":109,"line":356},[107,1278,1279],{"class":288},"    ]);\n",[107,1281,1282,1285],{"class":109,"line":362},[107,1283,1284],{"class":113},"    if",[107,1286,1287],{"class":288}," (isPublicRoute) {\n",[107,1289,1290],{"class":109,"line":507},[107,1291,1292],{"class":288},"      return true;\n",[107,1294,1295],{"class":109,"line":513},[107,1296,1297],{"class":288},"    }\n",[107,1299,1300],{"class":109,"line":519},[107,1301,627],{"emptyLinePlaceholder":626},[107,1303,1304,1307,1309],{"class":109,"line":525},[107,1305,1306],{"class":288},"    const authorization ",[107,1308,1214],{"class":276},[107,1310,1311],{"class":288}," request.headers.authorization;\n",[107,1313,1314],{"class":109,"line":531},[107,1315,627],{"emptyLinePlaceholder":626},[107,1317,1318,1320,1323,1326],{"class":109,"line":731},[107,1319,1284],{"class":113},[107,1321,1322],{"class":288}," (",[107,1324,1325],{"class":276},"!",[107,1327,1328],{"class":288},"authorization) {\n",[107,1330,1331,1334,1337,1339,1342],{"class":109,"line":1035},[107,1332,1333],{"class":288},"      throw new ",[107,1335,1336],{"class":113},"UnauthorizedException",[107,1338,393],{"class":288},[107,1340,1341],{"class":117},"'用户未登录'",[107,1343,724],{"class":288},[107,1345,1346],{"class":109,"line":1040},[107,1347,1297],{"class":288},[107,1349,1350],{"class":109,"line":1046},[107,1351,627],{"emptyLinePlaceholder":626},[107,1353,1354],{"class":109,"line":1051},[107,1355,1356],{"class":288},"    try {\n",[107,1358,1359,1362,1364,1367,1370,1372,1375,1378,1381],{"class":109,"line":1056},[107,1360,1361],{"class":288},"      const token ",[107,1363,1214],{"class":276},[107,1365,1366],{"class":288}," authorization.",[107,1368,1369],{"class":113},"split",[107,1371,393],{"class":288},[107,1373,1374],{"class":117},"' '",[107,1376,1377],{"class":288},")[",[107,1379,1380],{"class":121},"1",[107,1382,1383],{"class":288},"];\n",[107,1385,1386,1389,1391,1393,1396,1399],{"class":109,"line":1061},[107,1387,1388],{"class":288},"      const data ",[107,1390,1214],{"class":276},[107,1392,657],{"class":121},[107,1394,1395],{"class":288},".jwtService.",[107,1397,1398],{"class":113},"verify",[107,1400,1401],{"class":288},"(token);\n",[107,1403,1405],{"class":109,"line":1404},23,[107,1406,1407],{"class":215},"      // 这里会报没有user, 可以用declare module 给上边的 Request 在类型空间定义一下user\n",[107,1409,1411,1414,1416],{"class":109,"line":1410},24,[107,1412,1413],{"class":288},"      request.user ",[107,1415,1214],{"class":276},[107,1417,1418],{"class":288}," data.user;\n",[107,1420,1422],{"class":109,"line":1421},25,[107,1423,1292],{"class":288},[107,1425,1427,1430,1433],{"class":109,"line":1426},26,[107,1428,1429],{"class":288},"    } ",[107,1431,1432],{"class":113},"catch",[107,1434,1435],{"class":288}," (e) {\n",[107,1437,1439,1441,1443,1445,1448],{"class":109,"line":1438},27,[107,1440,1333],{"class":288},[107,1442,1336],{"class":113},[107,1444,393],{"class":288},[107,1446,1447],{"class":117},"'token 失效，请重新登录'",[107,1449,724],{"class":288},[107,1451,1453],{"class":109,"line":1452},28,[107,1454,1297],{"class":288},[107,1456,1458],{"class":109,"line":1457},29,[107,1459,423],{"class":288},[11,1461,1462],{},"guard要想生效，还要在gateway.module.ts里注册一下",[97,1464,1466],{"className":267,"code":1465,"language":269,"meta":102,"style":102}," providers: [\n    {\n      provide: APP_GUARD,\n      useClass: LoginGuard,\n    },\n    GatewayService,\n  ],\n",[104,1467,1468,1476,1480,1490,1495,1499,1504],{"__ignoreMap":102},[107,1469,1470,1473],{"class":109,"line":110},[107,1471,1472],{"class":113}," providers",[107,1474,1475],{"class":288},": [\n",[107,1477,1478],{"class":109,"line":219},[107,1479,312],{"class":288},[107,1481,1482,1485,1488],{"class":109,"line":309},[107,1483,1484],{"class":288},"      provide: ",[107,1486,1487],{"class":121},"APP_GUARD",[107,1489,324],{"class":288},[107,1491,1492],{"class":109,"line":315},[107,1493,1494],{"class":288},"      useClass: LoginGuard,\n",[107,1496,1497],{"class":109,"line":327},[107,1498,353],{"class":288},[107,1500,1501],{"class":109,"line":333},[107,1502,1503],{"class":288},"    GatewayService,\n",[107,1505,1506],{"class":109,"line":344},[107,1507,516],{"class":288},[11,1509,1510],{},"此时注册完成后，再去浏览器请求一下 /app 接口，可以发现已经被拦截住了",[97,1512,1514],{"className":267,"code":1513,"language":269,"meta":102,"style":102},"{\n  message: \"用户未登录\",\n  error: \"Unauthorized\",\n  statusCode: 401\n}\n",[104,1515,1516,1521,1534,1546,1556],{"__ignoreMap":102},[107,1517,1518],{"class":109,"line":110},[107,1519,1520],{"class":288},"{\n",[107,1522,1523,1526,1529,1532],{"class":109,"line":219},[107,1524,1525],{"class":113},"  message",[107,1527,1528],{"class":288},": ",[107,1530,1531],{"class":117},"\"用户未登录\"",[107,1533,324],{"class":288},[107,1535,1536,1539,1541,1544],{"class":109,"line":309},[107,1537,1538],{"class":113},"  error",[107,1540,1528],{"class":288},[107,1542,1543],{"class":117},"\"Unauthorized\"",[107,1545,324],{"class":288},[107,1547,1548,1551,1553],{"class":109,"line":315},[107,1549,1550],{"class":113},"  statusCode",[107,1552,1528],{"class":288},[107,1554,1555],{"class":121},"401\n",[107,1557,1558],{"class":109,"line":327},[107,1559,734],{"class":288},[896,1561,1563],{"id":1562},"自定义filter","自定义Filter",[11,1565,1566],{},"拦截住之后，问题就来了，貌似公司里Java接口，返回的都是内种的格式，我怎么自定义自己的返回格式\n再回顾上图，实现一个过滤器，因为401是抛出了一个错误，会被filter捕捉到\n再从custom里建一个filter吧，建完了把名字改改",[97,1568,1570],{"className":99,"code":1569,"language":101,"meta":102,"style":102},"nest g filter custom --project=gateway --no-spec\n",[104,1571,1572],{"__ignoreMap":102},[107,1573,1574,1576,1578,1581,1583,1586],{"class":109,"line":110},[107,1575,171],{"class":113},[107,1577,224],{"class":117},[107,1579,1580],{"class":117}," filter",[107,1582,1094],{"class":117},[107,1584,1585],{"class":121}," --project=gateway",[107,1587,1588],{"class":121}," --no-spec\n",[11,1590,1591],{},"实现一下功能，因为他会捕捉所有错误，也就是你其他地方抛出来的不是HttpException的错误也会在这里捕捉到，所以要判断一下。",[97,1593,1595],{"className":267,"code":1594,"language":269,"meta":102,"style":102},"@Catch()\nexport class HttpCatchFilter implements ExceptionFilter {\n  catch(exception: HttpException, host: ArgumentsHost) {\n    const http = host.switchToHttp();\n    const response = http.getResponse\u003CResponse>();\n\n    // 我把自己代码写错导致的错误都返回500\n    const statusCode =\n      exception instanceof HttpException\n        ? exception.getStatus()\n        : HttpStatus.INTERNAL_SERVER_ERROR;\n    // 使用exception的message 也可能是 exception.message.message 或 exception.message.error\n    let message = exception.message;\n    // 使用了参数校验之后，多个参数校验不通过，会返回一个数组，所以这里合并了一下，优化展示\n    if (exception instanceof HttpException) {\n      let res = exception.getResponse() as { message: string[] };\n      message = res?.message?.join\n        ? res?.message?.join(',')\n        : exception.message;\n    }\n    // 这里json的格式、字段、内容，自己随便写\n    response.status(statusCode).json({\n      code: statusCode,\n      message,\n      error: 'Bad Request',\n    });\n  }\n}\n\n",[104,1596,1597,1606,1623,1651,1668,1691,1695,1700,1710,1721,1734,1747,1752,1765,1770,1783,1816,1826,1843,1849,1853,1858,1874,1879,1884,1894,1899,1903],{"__ignoreMap":102},[107,1598,1599,1601,1604],{"class":109,"line":110},[107,1600,387],{"class":288},[107,1602,1603],{"class":113},"Catch",[107,1605,552],{"class":288},[107,1607,1608,1610,1612,1615,1618,1621],{"class":109,"line":219},[107,1609,557],{"class":276},[107,1611,560],{"class":276},[107,1613,1614],{"class":113}," HttpCatchFilter",[107,1616,1617],{"class":276}," implements",[107,1619,1620],{"class":113}," ExceptionFilter",[107,1622,566],{"class":288},[107,1624,1625,1628,1630,1633,1635,1638,1640,1643,1645,1648],{"class":109,"line":309},[107,1626,1627],{"class":113},"  catch",[107,1629,393],{"class":288},[107,1631,1632],{"class":588},"exception",[107,1634,592],{"class":276},[107,1636,1637],{"class":113}," HttpException",[107,1639,719],{"class":288},[107,1641,1642],{"class":588},"host",[107,1644,592],{"class":276},[107,1646,1647],{"class":113}," ArgumentsHost",[107,1649,1650],{"class":288},") {\n",[107,1652,1653,1656,1659,1661,1664,1666],{"class":109,"line":315},[107,1654,1655],{"class":276},"    const",[107,1657,1658],{"class":121}," http",[107,1660,282],{"class":276},[107,1662,1663],{"class":288}," host.",[107,1665,1220],{"class":113},[107,1667,374],{"class":288},[107,1669,1670,1672,1675,1677,1680,1683,1685,1688],{"class":109,"line":327},[107,1671,1655],{"class":276},[107,1673,1674],{"class":121}," response",[107,1676,282],{"class":276},[107,1678,1679],{"class":288}," http.",[107,1681,1682],{"class":113},"getResponse",[107,1684,295],{"class":288},[107,1686,1687],{"class":113},"Response",[107,1689,1690],{"class":288},">();\n",[107,1692,1693],{"class":109,"line":333},[107,1694,627],{"emptyLinePlaceholder":626},[107,1696,1697],{"class":109,"line":344},[107,1698,1699],{"class":215},"    // 我把自己代码写错导致的错误都返回500\n",[107,1701,1702,1704,1707],{"class":109,"line":350},[107,1703,1655],{"class":276},[107,1705,1706],{"class":121}," statusCode",[107,1708,1709],{"class":276}," =\n",[107,1711,1712,1715,1718],{"class":109,"line":356},[107,1713,1714],{"class":288},"      exception ",[107,1716,1717],{"class":276},"instanceof",[107,1719,1720],{"class":113}," HttpException\n",[107,1722,1723,1726,1729,1732],{"class":109,"line":362},[107,1724,1725],{"class":276},"        ?",[107,1727,1728],{"class":288}," exception.",[107,1730,1731],{"class":113},"getStatus",[107,1733,552],{"class":288},[107,1735,1736,1739,1742,1745],{"class":109,"line":507},[107,1737,1738],{"class":276},"        :",[107,1740,1741],{"class":288}," HttpStatus.",[107,1743,1744],{"class":121},"INTERNAL_SERVER_ERROR",[107,1746,418],{"class":288},[107,1748,1749],{"class":109,"line":513},[107,1750,1751],{"class":215},"    // 使用exception的message 也可能是 exception.message.message 或 exception.message.error\n",[107,1753,1754,1757,1760,1762],{"class":109,"line":519},[107,1755,1756],{"class":276},"    let",[107,1758,1759],{"class":288}," message ",[107,1761,1214],{"class":276},[107,1763,1764],{"class":288}," exception.message;\n",[107,1766,1767],{"class":109,"line":525},[107,1768,1769],{"class":215},"    // 使用了参数校验之后，多个参数校验不通过，会返回一个数组，所以这里合并了一下，优化展示\n",[107,1771,1772,1774,1777,1779,1781],{"class":109,"line":531},[107,1773,1284],{"class":276},[107,1775,1776],{"class":288}," (exception ",[107,1778,1717],{"class":276},[107,1780,1637],{"class":113},[107,1782,1650],{"class":288},[107,1784,1785,1788,1791,1793,1795,1797,1800,1803,1806,1809,1811,1813],{"class":109,"line":731},[107,1786,1787],{"class":276},"      let",[107,1789,1790],{"class":288}," res ",[107,1792,1214],{"class":276},[107,1794,1728],{"class":288},[107,1796,1682],{"class":113},[107,1798,1799],{"class":288},"() ",[107,1801,1802],{"class":276},"as",[107,1804,1805],{"class":288}," { ",[107,1807,1808],{"class":588},"message",[107,1810,592],{"class":276},[107,1812,648],{"class":121},[107,1814,1815],{"class":288},"[] };\n",[107,1817,1818,1821,1823],{"class":109,"line":1035},[107,1819,1820],{"class":288},"      message ",[107,1822,1214],{"class":276},[107,1824,1825],{"class":288}," res?.message?.join\n",[107,1827,1828,1830,1833,1836,1838,1841],{"class":109,"line":1040},[107,1829,1725],{"class":276},[107,1831,1832],{"class":288}," res?.message?.",[107,1834,1835],{"class":113},"join",[107,1837,393],{"class":288},[107,1839,1840],{"class":117},"','",[107,1842,399],{"class":288},[107,1844,1845,1847],{"class":109,"line":1046},[107,1846,1738],{"class":276},[107,1848,1764],{"class":288},[107,1850,1851],{"class":109,"line":1051},[107,1852,1297],{"class":288},[107,1854,1855],{"class":109,"line":1056},[107,1856,1857],{"class":215},"    // 这里json的格式、字段、内容，自己随便写\n",[107,1859,1860,1863,1866,1869,1872],{"class":109,"line":1061},[107,1861,1862],{"class":288},"    response.",[107,1864,1865],{"class":113},"status",[107,1867,1868],{"class":288},"(statusCode).",[107,1870,1871],{"class":113},"json",[107,1873,441],{"class":288},[107,1875,1876],{"class":109,"line":1404},[107,1877,1878],{"class":288},"      code: statusCode,\n",[107,1880,1881],{"class":109,"line":1410},[107,1882,1883],{"class":288},"      message,\n",[107,1885,1886,1889,1892],{"class":109,"line":1421},[107,1887,1888],{"class":288},"      error: ",[107,1890,1891],{"class":117},"'Bad Request'",[107,1893,324],{"class":288},[107,1895,1896],{"class":109,"line":1426},[107,1897,1898],{"class":288},"    });\n",[107,1900,1901],{"class":109,"line":1438},[107,1902,423],{"class":288},[107,1904,1905],{"class":109,"line":1452},[107,1906,734],{"class":288},[11,1908,1909],{},"写完同样需要在gateway.module.ts里的providers下注册（其他全局注册方式建议自行查阅）",[97,1911,1913],{"className":267,"code":1912,"language":269,"meta":102,"style":102},"providers: [\n    {\n      provide: APP_GUARD,\n      useClass: LoginGuard,\n    },\n    {\n        provide: APP_FILTER,\n        useClass: CommonErrorCatchFilter,\n      },\n    GatewayService,\n  ],\n",[104,1914,1915,1922,1926,1934,1938,1942,1946,1956,1961,1965,1969],{"__ignoreMap":102},[107,1916,1917,1920],{"class":109,"line":110},[107,1918,1919],{"class":113},"providers",[107,1921,1475],{"class":288},[107,1923,1924],{"class":109,"line":219},[107,1925,312],{"class":288},[107,1927,1928,1930,1932],{"class":109,"line":309},[107,1929,1484],{"class":288},[107,1931,1487],{"class":121},[107,1933,324],{"class":288},[107,1935,1936],{"class":109,"line":315},[107,1937,1494],{"class":288},[107,1939,1940],{"class":109,"line":327},[107,1941,353],{"class":288},[107,1943,1944],{"class":109,"line":333},[107,1945,312],{"class":288},[107,1947,1948,1951,1954],{"class":109,"line":344},[107,1949,1950],{"class":288},"        provide: ",[107,1952,1953],{"class":121},"APP_FILTER",[107,1955,324],{"class":288},[107,1957,1958],{"class":109,"line":350},[107,1959,1960],{"class":288},"        useClass: CommonErrorCatchFilter,\n",[107,1962,1963],{"class":109,"line":356},[107,1964,347],{"class":288},[107,1966,1967],{"class":109,"line":362},[107,1968,1503],{"class":288},[107,1970,1971],{"class":109,"line":507},[107,1972,516],{"class":288},[11,1974,1975],{},"再回到浏览器看一下/app接口, 在校验失败的情况下，返回结果已经变成了我们想要的结构",[97,1977,1980],{"className":1978,"code":1979,"language":1871,"meta":102,"style":102},"language-json shiki shiki-themes github-light","{\n  code: 401,\n  message: \"用户未登录\",\n  error: \"Bad Request\"\n}\n",[104,1981,1982,1986,1999,2009,2018],{"__ignoreMap":102},[107,1983,1984],{"class":109,"line":110},[107,1985,1520],{"class":288},[107,1987,1988,1992,1994,1997],{"class":109,"line":219},[107,1989,1991],{"class":1990},"sB1qb","  code",[107,1993,1528],{"class":288},[107,1995,1996],{"class":121},"401",[107,1998,324],{"class":288},[107,2000,2001,2003,2005,2007],{"class":109,"line":309},[107,2002,1525],{"class":1990},[107,2004,1528],{"class":288},[107,2006,1531],{"class":117},[107,2008,324],{"class":288},[107,2010,2011,2013,2015],{"class":109,"line":315},[107,2012,1538],{"class":1990},[107,2014,1528],{"class":288},[107,2016,2017],{"class":117},"\"Bad Request\"\n",[107,2019,2020],{"class":109,"line":327},[107,2021,734],{"class":288},[11,2023,2024],{},"然后再把刚才写的setPublicRoute给/app这个接口用一下",[97,2026,2028],{"className":267,"code":2027,"language":269,"meta":102,"style":102},"@Get('app')\n@setPublicRoute()\ngetServerHello(): Observable\u003Cany> {\n  return this.serverClient.send('hello', 'hello');\n}\n",[104,2029,2030,2042,2051,2068,2081],{"__ignoreMap":102},[107,2031,2032,2034,2036,2038,2040],{"class":109,"line":110},[107,2033,387],{"class":288},[107,2035,634],{"class":113},[107,2037,393],{"class":288},[107,2039,684],{"class":117},[107,2041,399],{"class":288},[107,2043,2044,2046,2049],{"class":109,"line":219},[107,2045,387],{"class":288},[107,2047,2048],{"class":113},"setPublicRoute",[107,2050,552],{"class":288},[107,2052,2053,2056,2059,2061,2064,2066],{"class":109,"line":309},[107,2054,2055],{"class":113},"getServerHello",[107,2057,2058],{"class":288},"(): Observable",[107,2060,295],{"class":276},[107,2062,2063],{"class":288},"any",[107,2065,1192],{"class":276},[107,2067,566],{"class":288},[107,2069,2070,2073,2075,2077,2079],{"class":109,"line":315},[107,2071,2072],{"class":288},"  return this.serverClient.send(",[107,2074,396],{"class":117},[107,2076,719],{"class":288},[107,2078,396],{"class":117},[107,2080,724],{"class":288},[107,2082,2083],{"class":109,"line":327},[107,2084,734],{"class":288},[11,2086,2087],{},"再去浏览器看一下，hello by zzstudio-server，这个内容又出来了。",[11,2089,2090],{},"写完了别忘了保存啊。我都忘了，还以为哪里写错了呢。",[11,2092,2093],{},"然后再实现一个post接口试试看",[97,2095,2097],{"className":267,"code":2096,"language":269,"meta":102,"style":102},"@Post('login')\n@setPublicRoute()\nlogin(): Observable\u003Cany> {\n  return this.serverClient.send('login', { username: 1, password: 2 });\n}\n",[104,2098,2099,2113,2121,2136,2165],{"__ignoreMap":102},[107,2100,2101,2103,2106,2108,2111],{"class":109,"line":110},[107,2102,387],{"class":288},[107,2104,2105],{"class":113},"Post",[107,2107,393],{"class":288},[107,2109,2110],{"class":117},"'login'",[107,2112,399],{"class":288},[107,2114,2115,2117,2119],{"class":109,"line":219},[107,2116,387],{"class":288},[107,2118,2048],{"class":113},[107,2120,552],{"class":288},[107,2122,2123,2126,2128,2130,2132,2134],{"class":109,"line":309},[107,2124,2125],{"class":113},"login",[107,2127,2058],{"class":288},[107,2129,295],{"class":276},[107,2131,2063],{"class":288},[107,2133,1192],{"class":276},[107,2135,566],{"class":288},[107,2137,2138,2140,2142,2145,2148,2150,2152,2154,2157,2159,2162],{"class":109,"line":315},[107,2139,2072],{"class":288},[107,2141,2110],{"class":117},[107,2143,2144],{"class":288},", { ",[107,2146,2147],{"class":113},"username",[107,2149,1528],{"class":288},[107,2151,1380],{"class":121},[107,2153,719],{"class":288},[107,2155,2156],{"class":113},"password",[107,2158,1528],{"class":288},[107,2160,2161],{"class":121},"2",[107,2163,2164],{"class":288}," });\n",[107,2166,2167],{"class":109,"line":327},[107,2168,734],{"class":288},[11,2170,2171],{},"然后在我的主服务里，接受这个请求，然后返回俩token",[97,2173,2175],{"className":267,"code":2174,"language":269,"meta":102,"style":102},"@MessagePattern('login')\n  login(): object {\n    return {\n      access_token: '123456',\n      refresh_token: '123456',\n    };\n  }\n",[104,2176,2177,2189,2197,2203,2213,2222,2227],{"__ignoreMap":102},[107,2178,2179,2181,2183,2185,2187],{"class":109,"line":110},[107,2180,387],{"class":288},[107,2182,390],{"class":113},[107,2184,393],{"class":288},[107,2186,2110],{"class":117},[107,2188,399],{"class":288},[107,2190,2191,2194],{"class":109,"line":219},[107,2192,2193],{"class":113},"  login",[107,2195,2196],{"class":288},"(): object {\n",[107,2198,2199,2201],{"class":109,"line":309},[107,2200,412],{"class":276},[107,2202,566],{"class":288},[107,2204,2205,2208,2211],{"class":109,"line":315},[107,2206,2207],{"class":288},"      access_token: ",[107,2209,2210],{"class":117},"'123456'",[107,2212,324],{"class":288},[107,2214,2215,2218,2220],{"class":109,"line":327},[107,2216,2217],{"class":288},"      refresh_token: ",[107,2219,2210],{"class":117},[107,2221,324],{"class":288},[107,2223,2224],{"class":109,"line":333},[107,2225,2226],{"class":288},"    };\n",[107,2228,2229],{"class":109,"line":344},[107,2230,423],{"class":288},[11,2232,2233],{},"然后再拿postman、postwoman、apifox去请求试一试，我用的apifox",[11,2235,2236],{},[181,2237],{"alt":102,"src":2238},"https://img.zzao.club/article/202411191446174.png",[11,2240,2241],{},"可以看到，拿到了数据，但明显还不是我们想要的结构。",[896,2243,2244],{"id":2244},"自定义interceptor",[11,2246,2247],{},"所以我们再回顾一下上图，可以在interceptor里去处理一下next.handle() 之后的数据\n再回顾上图，实现一个拦截器\n再从custom里建一个吧，建完了把名字改改",[97,2249,2251],{"className":99,"code":2250,"language":101,"meta":102,"style":102},"nest g interceptor custom --project=gateway --no-spec\n",[104,2252,2253],{"__ignoreMap":102},[107,2254,2255,2257,2259,2262,2264,2266],{"class":109,"line":110},[107,2256,171],{"class":113},[107,2258,224],{"class":117},[107,2260,2261],{"class":117}," interceptor",[107,2263,1094],{"class":117},[107,2265,1585],{"class":121},[107,2267,1588],{"class":121},[11,2269,2270],{},"实现一下功能",[97,2272,2274],{"className":267,"code":2273,"language":269,"meta":102,"style":102},"@Injectable()\nexport class HttpCommonInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    const response = context.switchToHttp().getResponse\u003CResponse>();\n    // 201时返回200\n    if (response.statusCode === HttpStatus.CREATED)\n      response.status(HttpStatus.OK);\n    return next.handle().pipe(\n      map((data) => {\n        return {\n          code: 200,\n          data,\n          message: 'ok',\n        };\n      }),\n    );\n  }\n}\n",[104,2275,2276,2285,2301,2340,2362,2367,2384,2399,2416,2434,2441,2451,2456,2466,2471,2475,2480,2484],{"__ignoreMap":102},[107,2277,2278,2280,2283],{"class":109,"line":110},[107,2279,387],{"class":288},[107,2281,2282],{"class":113},"Injectable",[107,2284,552],{"class":288},[107,2286,2287,2289,2291,2294,2296,2299],{"class":109,"line":219},[107,2288,557],{"class":276},[107,2290,560],{"class":276},[107,2292,2293],{"class":113}," HttpCommonInterceptor",[107,2295,1617],{"class":276},[107,2297,2298],{"class":113}," NestInterceptor",[107,2300,566],{"class":288},[107,2302,2303,2306,2308,2311,2313,2316,2318,2321,2323,2326,2329,2331,2333,2335,2337],{"class":109,"line":309},[107,2304,2305],{"class":113},"  intercept",[107,2307,393],{"class":288},[107,2309,2310],{"class":588},"context",[107,2312,592],{"class":276},[107,2314,2315],{"class":113}," ExecutionContext",[107,2317,719],{"class":288},[107,2319,2320],{"class":588},"next",[107,2322,592],{"class":276},[107,2324,2325],{"class":113}," CallHandler",[107,2327,2328],{"class":288},")",[107,2330,592],{"class":276},[107,2332,1198],{"class":113},[107,2334,295],{"class":288},[107,2336,2063],{"class":121},[107,2338,2339],{"class":288},"> {\n",[107,2341,2342,2344,2346,2348,2350,2352,2354,2356,2358,2360],{"class":109,"line":315},[107,2343,1655],{"class":276},[107,2345,1674],{"class":121},[107,2347,282],{"class":276},[107,2349,1217],{"class":288},[107,2351,1220],{"class":113},[107,2353,1223],{"class":288},[107,2355,1682],{"class":113},[107,2357,295],{"class":288},[107,2359,1687],{"class":113},[107,2361,1690],{"class":288},[107,2363,2364],{"class":109,"line":327},[107,2365,2366],{"class":215},"    // 201时返回200\n",[107,2368,2369,2371,2374,2377,2379,2382],{"class":109,"line":333},[107,2370,1284],{"class":276},[107,2372,2373],{"class":288}," (response.statusCode ",[107,2375,2376],{"class":276},"===",[107,2378,1741],{"class":288},[107,2380,2381],{"class":121},"CREATED",[107,2383,399],{"class":288},[107,2385,2386,2389,2391,2394,2397],{"class":109,"line":344},[107,2387,2388],{"class":288},"      response.",[107,2390,1865],{"class":113},[107,2392,2393],{"class":288},"(HttpStatus.",[107,2395,2396],{"class":121},"OK",[107,2398,724],{"class":288},[107,2400,2401,2403,2406,2409,2411,2414],{"class":109,"line":350},[107,2402,412],{"class":276},[107,2404,2405],{"class":288}," next.",[107,2407,2408],{"class":113},"handle",[107,2410,1223],{"class":288},[107,2412,2413],{"class":113},"pipe",[107,2415,1168],{"class":288},[107,2417,2418,2421,2424,2427,2430,2432],{"class":109,"line":356},[107,2419,2420],{"class":113},"      map",[107,2422,2423],{"class":288},"((",[107,2425,2426],{"class":588},"data",[107,2428,2429],{"class":288},") ",[107,2431,1123],{"class":276},[107,2433,566],{"class":288},[107,2435,2436,2439],{"class":109,"line":362},[107,2437,2438],{"class":276},"        return",[107,2440,566],{"class":288},[107,2442,2443,2446,2449],{"class":109,"line":507},[107,2444,2445],{"class":288},"          code: ",[107,2447,2448],{"class":121},"200",[107,2450,324],{"class":288},[107,2452,2453],{"class":109,"line":513},[107,2454,2455],{"class":288},"          data,\n",[107,2457,2458,2461,2464],{"class":109,"line":519},[107,2459,2460],{"class":288},"          message: ",[107,2462,2463],{"class":117},"'ok'",[107,2465,324],{"class":288},[107,2467,2468],{"class":109,"line":525},[107,2469,2470],{"class":288},"        };\n",[107,2472,2473],{"class":109,"line":531},[107,2474,1043],{"class":288},[107,2476,2477],{"class":109,"line":731},[107,2478,2479],{"class":288},"    );\n",[107,2481,2482],{"class":109,"line":1035},[107,2483,423],{"class":288},[107,2485,2486],{"class":109,"line":1040},[107,2487,734],{"class":288},[11,2489,2490],{},"也需要在gateway.module.ts里注册一下",[97,2492,2494],{"className":267,"code":2493,"language":269,"meta":102,"style":102},"providers: [\n    {\n      provide: APP_GUARD,\n      useClass: LoginGuard,\n    },\n    {\n      provide: APP_INTERCEPTOR,\n      useClass: HttpCommonInterceptor,\n    },\n    {\n      provide: APP_FILTER,\n      useClass: CommonErrorCatchFilter,\n    },\n    GatewayService,\n  ],\n",[104,2495,2496,2502,2506,2514,2518,2522,2526,2535,2540,2544,2548,2556,2561,2565,2569],{"__ignoreMap":102},[107,2497,2498,2500],{"class":109,"line":110},[107,2499,1919],{"class":113},[107,2501,1475],{"class":288},[107,2503,2504],{"class":109,"line":219},[107,2505,312],{"class":288},[107,2507,2508,2510,2512],{"class":109,"line":309},[107,2509,1484],{"class":288},[107,2511,1487],{"class":121},[107,2513,324],{"class":288},[107,2515,2516],{"class":109,"line":315},[107,2517,1494],{"class":288},[107,2519,2520],{"class":109,"line":327},[107,2521,353],{"class":288},[107,2523,2524],{"class":109,"line":333},[107,2525,312],{"class":288},[107,2527,2528,2530,2533],{"class":109,"line":344},[107,2529,1484],{"class":288},[107,2531,2532],{"class":121},"APP_INTERCEPTOR",[107,2534,324],{"class":288},[107,2536,2537],{"class":109,"line":350},[107,2538,2539],{"class":288},"      useClass: HttpCommonInterceptor,\n",[107,2541,2542],{"class":109,"line":356},[107,2543,353],{"class":288},[107,2545,2546],{"class":109,"line":362},[107,2547,312],{"class":288},[107,2549,2550,2552,2554],{"class":109,"line":507},[107,2551,1484],{"class":288},[107,2553,1953],{"class":121},[107,2555,324],{"class":288},[107,2557,2558],{"class":109,"line":513},[107,2559,2560],{"class":288},"      useClass: CommonErrorCatchFilter,\n",[107,2562,2563],{"class":109,"line":519},[107,2564,353],{"class":288},[107,2566,2567],{"class":109,"line":525},[107,2568,1503],{"class":288},[107,2570,2571],{"class":109,"line":531},[107,2572,516],{"class":288},[11,2574,2575,2576],{},"然后再去apifox请求看一下\n",[181,2577],{"alt":102,"src":2578},"https://img.zzao.club/article/202411191446175.png",[11,2580,2581],{},"是我们想要的格式了。",[896,2583,2584],{"id":2584},"权限校验部分",[11,2586,2587],{},"我新建一个auth模块， 在里面实现login、refreshToken接口",[97,2589,2591],{"className":99,"code":2590,"language":101,"meta":102,"style":102},"nest g resource auth --project=gateway --no-spec\n",[104,2592,2593],{"__ignoreMap":102},[107,2594,2595,2597,2599,2602,2605,2607],{"class":109,"line":110},[107,2596,171],{"class":113},[107,2598,224],{"class":117},[107,2600,2601],{"class":117}," resource",[107,2603,2604],{"class":117}," auth",[107,2606,1585],{"class":121},[107,2608,1588],{"class":121},[11,2610,2611],{},"登录接口，返回两个token，给到前端之后，前端请求时需要在headers里携带access_token，当提示前端已过期时，前端再用refresh_token去请求refresh接口。refresh接口则会再返回两个新的token。以此达到无限续签。",[97,2613,2615],{"className":267,"code":2614,"language":269,"meta":102,"style":102},"@Post('login')\n  async login(\n    @Body() user: LoginDto,\n    @Res({ passthrough: true }) res: Response,\n  ): Promise\u003Cany> {\n    let userInfo = await this.authService.login(user);\n    if (userInfo) {\n      const access_token = this.jwtService.sign(\n        {\n          user: {\n           ...\n          },\n        },\n        {\n          expiresIn: '60m',\n        },\n      );\n      const refresh_token = this.jwtService.sign(\n        {\n          userId: userInfo.id,\n        },\n        {\n          expiresIn: '7d',\n        },\n      );\n\n      res.setHeader('token', access_token);\n      return {\n        access_token,\n        refresh_token,\n      };\n    }\n  }\n\n   @Get('refresh')\n  async refresh(@Query() refreshParams: refreshDto) {\n    try {\n      const data = this.jwtService.verify(refreshParams.refreshToken);\n\n      const user = await this.authService.findUserById(data.userId);\n\n      const access_token = this.jwtService.sign(\n        {\n          ...\n        },\n        {\n          expiresIn: '60m',\n        },\n      );\n\n      const refresh_token = this.jwtService.sign(\n        {\n          userId: user.id,\n        },\n        {\n          expiresIn: '7d',\n        },\n      );\n\n      return {\n        access_token,\n        refresh_token,\n      };\n    } catch (e) {\n      throw new UnauthorizedException('token 已失效，请重新登录');\n    }\n  }\n",[104,2616,2617,2629,2638,2649,2664,2680,2699,2710,2729,2734,2739,2744,2749,2753,2757,2766,2770,2775,2792,2796,2801,2805,2809,2818,2822,2826,2830,2846,2853,2858,2864,2870,2875,2880,2885,2900,2917,2925,2944,2949,2971,2976,2993,2998,3004,3009,3014,3023,3028,3033,3038,3055,3060,3066,3071,3076,3085,3090,3095,3100,3107,3112,3117,3122,3131,3149,3154],{"__ignoreMap":102},[107,2618,2619,2621,2623,2625,2627],{"class":109,"line":110},[107,2620,387],{"class":288},[107,2622,2105],{"class":113},[107,2624,393],{"class":288},[107,2626,2110],{"class":117},[107,2628,399],{"class":288},[107,2630,2631,2634,2636],{"class":109,"line":219},[107,2632,2633],{"class":288},"  async ",[107,2635,2125],{"class":113},[107,2637,1168],{"class":288},[107,2639,2640,2643,2646],{"class":109,"line":309},[107,2641,2642],{"class":288},"    @",[107,2644,2645],{"class":113},"Body",[107,2647,2648],{"class":288},"() user: LoginDto,\n",[107,2650,2651,2653,2656,2659,2661],{"class":109,"line":315},[107,2652,2642],{"class":288},[107,2654,2655],{"class":113},"Res",[107,2657,2658],{"class":288},"({ passthrough: ",[107,2660,1005],{"class":121},[107,2662,2663],{"class":288}," }) res: Response,\n",[107,2665,2666,2669,2672,2674,2676,2678],{"class":109,"line":327},[107,2667,2668],{"class":288},"  ): ",[107,2670,2671],{"class":121},"Promise",[107,2673,295],{"class":276},[107,2675,2063],{"class":288},[107,2677,1192],{"class":276},[107,2679,566],{"class":288},[107,2681,2682,2685,2687,2689,2691,2694,2696],{"class":109,"line":333},[107,2683,2684],{"class":288},"    let userInfo ",[107,2686,1214],{"class":276},[107,2688,285],{"class":276},[107,2690,657],{"class":121},[107,2692,2693],{"class":288},".authService.",[107,2695,2125],{"class":113},[107,2697,2698],{"class":288},"(user);\n",[107,2700,2701,2703,2705,2708],{"class":109,"line":344},[107,2702,1284],{"class":113},[107,2704,1322],{"class":288},[107,2706,2707],{"class":588},"userInfo",[107,2709,1650],{"class":288},[107,2711,2712,2715,2718,2720,2722,2724,2727],{"class":109,"line":350},[107,2713,2714],{"class":276},"      const",[107,2716,2717],{"class":121}," access_token",[107,2719,282],{"class":276},[107,2721,657],{"class":121},[107,2723,1395],{"class":288},[107,2725,2726],{"class":113},"sign",[107,2728,1168],{"class":288},[107,2730,2731],{"class":109,"line":356},[107,2732,2733],{"class":288},"        {\n",[107,2735,2736],{"class":109,"line":362},[107,2737,2738],{"class":288},"          user: {\n",[107,2740,2741],{"class":109,"line":507},[107,2742,2743],{"class":276},"           ...\n",[107,2745,2746],{"class":109,"line":513},[107,2747,2748],{"class":288},"          },\n",[107,2750,2751],{"class":109,"line":519},[107,2752,500],{"class":288},[107,2754,2755],{"class":109,"line":525},[107,2756,2733],{"class":288},[107,2758,2759,2761,2764],{"class":109,"line":531},[107,2760,1027],{"class":288},[107,2762,2763],{"class":117},"'60m'",[107,2765,324],{"class":288},[107,2767,2768],{"class":109,"line":731},[107,2769,500],{"class":288},[107,2771,2772],{"class":109,"line":1035},[107,2773,2774],{"class":288},"      );\n",[107,2776,2777,2779,2782,2784,2786,2788,2790],{"class":109,"line":1040},[107,2778,2714],{"class":276},[107,2780,2781],{"class":121}," refresh_token",[107,2783,282],{"class":276},[107,2785,657],{"class":121},[107,2787,1395],{"class":288},[107,2789,2726],{"class":113},[107,2791,1168],{"class":288},[107,2793,2794],{"class":109,"line":1046},[107,2795,2733],{"class":288},[107,2797,2798],{"class":109,"line":1051},[107,2799,2800],{"class":288},"          userId: userInfo.id,\n",[107,2802,2803],{"class":109,"line":1056},[107,2804,500],{"class":288},[107,2806,2807],{"class":109,"line":1061},[107,2808,2733],{"class":288},[107,2810,2811,2813,2816],{"class":109,"line":1404},[107,2812,1027],{"class":288},[107,2814,2815],{"class":117},"'7d'",[107,2817,324],{"class":288},[107,2819,2820],{"class":109,"line":1410},[107,2821,500],{"class":288},[107,2823,2824],{"class":109,"line":1421},[107,2825,2774],{"class":288},[107,2827,2828],{"class":109,"line":1426},[107,2829,627],{"emptyLinePlaceholder":626},[107,2831,2832,2835,2838,2840,2843],{"class":109,"line":1438},[107,2833,2834],{"class":288},"      res.",[107,2836,2837],{"class":113},"setHeader",[107,2839,393],{"class":288},[107,2841,2842],{"class":117},"'token'",[107,2844,2845],{"class":288},", access_token);\n",[107,2847,2848,2851],{"class":109,"line":1452},[107,2849,2850],{"class":276},"      return",[107,2852,566],{"class":288},[107,2854,2855],{"class":109,"line":1457},[107,2856,2857],{"class":288},"        access_token,\n",[107,2859,2861],{"class":109,"line":2860},30,[107,2862,2863],{"class":288},"        refresh_token,\n",[107,2865,2867],{"class":109,"line":2866},31,[107,2868,2869],{"class":288},"      };\n",[107,2871,2873],{"class":109,"line":2872},32,[107,2874,1297],{"class":288},[107,2876,2878],{"class":109,"line":2877},33,[107,2879,423],{"class":288},[107,2881,2883],{"class":109,"line":2882},34,[107,2884,627],{"emptyLinePlaceholder":626},[107,2886,2888,2891,2893,2895,2898],{"class":109,"line":2887},35,[107,2889,2890],{"class":288},"   @",[107,2892,634],{"class":113},[107,2894,393],{"class":288},[107,2896,2897],{"class":117},"'refresh'",[107,2899,399],{"class":288},[107,2901,2903,2905,2908,2911,2914],{"class":109,"line":2902},36,[107,2904,2633],{"class":288},[107,2906,2907],{"class":113},"refresh",[107,2909,2910],{"class":288},"(@",[107,2912,2913],{"class":113},"Query",[107,2915,2916],{"class":288},"() refreshParams: refreshDto) {\n",[107,2918,2920,2923],{"class":109,"line":2919},37,[107,2921,2922],{"class":276},"    try",[107,2924,566],{"class":288},[107,2926,2928,2930,2933,2935,2937,2939,2941],{"class":109,"line":2927},38,[107,2929,2714],{"class":276},[107,2931,2932],{"class":121}," data",[107,2934,282],{"class":276},[107,2936,657],{"class":121},[107,2938,1395],{"class":288},[107,2940,1398],{"class":113},[107,2942,2943],{"class":288},"(refreshParams.refreshToken);\n",[107,2945,2947],{"class":109,"line":2946},39,[107,2948,627],{"emptyLinePlaceholder":626},[107,2950,2952,2954,2957,2959,2961,2963,2965,2968],{"class":109,"line":2951},40,[107,2953,2714],{"class":276},[107,2955,2956],{"class":121}," user",[107,2958,282],{"class":276},[107,2960,285],{"class":276},[107,2962,657],{"class":121},[107,2964,2693],{"class":288},[107,2966,2967],{"class":113},"findUserById",[107,2969,2970],{"class":288},"(data.userId);\n",[107,2972,2974],{"class":109,"line":2973},41,[107,2975,627],{"emptyLinePlaceholder":626},[107,2977,2979,2981,2983,2985,2987,2989,2991],{"class":109,"line":2978},42,[107,2980,2714],{"class":276},[107,2982,2717],{"class":121},[107,2984,282],{"class":276},[107,2986,657],{"class":121},[107,2988,1395],{"class":288},[107,2990,2726],{"class":113},[107,2992,1168],{"class":288},[107,2994,2996],{"class":109,"line":2995},43,[107,2997,2733],{"class":288},[107,2999,3001],{"class":109,"line":3000},44,[107,3002,3003],{"class":276},"          ...\n",[107,3005,3007],{"class":109,"line":3006},45,[107,3008,500],{"class":288},[107,3010,3012],{"class":109,"line":3011},46,[107,3013,2733],{"class":288},[107,3015,3017,3019,3021],{"class":109,"line":3016},47,[107,3018,1027],{"class":288},[107,3020,2763],{"class":117},[107,3022,324],{"class":288},[107,3024,3026],{"class":109,"line":3025},48,[107,3027,500],{"class":288},[107,3029,3031],{"class":109,"line":3030},49,[107,3032,2774],{"class":288},[107,3034,3036],{"class":109,"line":3035},50,[107,3037,627],{"emptyLinePlaceholder":626},[107,3039,3041,3043,3045,3047,3049,3051,3053],{"class":109,"line":3040},51,[107,3042,2714],{"class":276},[107,3044,2781],{"class":121},[107,3046,282],{"class":276},[107,3048,657],{"class":121},[107,3050,1395],{"class":288},[107,3052,2726],{"class":113},[107,3054,1168],{"class":288},[107,3056,3058],{"class":109,"line":3057},52,[107,3059,2733],{"class":288},[107,3061,3063],{"class":109,"line":3062},53,[107,3064,3065],{"class":288},"          userId: user.id,\n",[107,3067,3069],{"class":109,"line":3068},54,[107,3070,500],{"class":288},[107,3072,3074],{"class":109,"line":3073},55,[107,3075,2733],{"class":288},[107,3077,3079,3081,3083],{"class":109,"line":3078},56,[107,3080,1027],{"class":288},[107,3082,2815],{"class":117},[107,3084,324],{"class":288},[107,3086,3088],{"class":109,"line":3087},57,[107,3089,500],{"class":288},[107,3091,3093],{"class":109,"line":3092},58,[107,3094,2774],{"class":288},[107,3096,3098],{"class":109,"line":3097},59,[107,3099,627],{"emptyLinePlaceholder":626},[107,3101,3103,3105],{"class":109,"line":3102},60,[107,3104,2850],{"class":276},[107,3106,566],{"class":288},[107,3108,3110],{"class":109,"line":3109},61,[107,3111,2857],{"class":288},[107,3113,3115],{"class":109,"line":3114},62,[107,3116,2863],{"class":288},[107,3118,3120],{"class":109,"line":3119},63,[107,3121,2869],{"class":288},[107,3123,3125,3127,3129],{"class":109,"line":3124},64,[107,3126,1429],{"class":288},[107,3128,1432],{"class":276},[107,3130,1435],{"class":288},[107,3132,3134,3137,3139,3142,3144,3147],{"class":109,"line":3133},65,[107,3135,3136],{"class":276},"      throw",[107,3138,174],{"class":276},[107,3140,3141],{"class":113}," UnauthorizedException",[107,3143,393],{"class":288},[107,3145,3146],{"class":117},"'token 已失效，请重新登录'",[107,3148,724],{"class":288},[107,3150,3152],{"class":109,"line":3151},66,[107,3153,1297],{"class":288},[107,3155,3157],{"class":109,"line":3156},67,[107,3158,423],{"class":288},[11,3160,3161],{},"使用typeorm链接mysql",[97,3163,3165],{"className":99,"code":3164,"language":101,"meta":102,"style":102},"pnpm add typeorm @nestjs/typeorm mysql2\n",[104,3166,3167],{"__ignoreMap":102},[107,3168,3169,3171,3173,3176,3179],{"class":109,"line":110},[107,3170,196],{"class":113},[107,3172,258],{"class":117},[107,3174,3175],{"class":117}," typeorm",[107,3177,3178],{"class":117}," @nestjs/typeorm",[107,3180,3181],{"class":117}," mysql2\n",[11,3183,3184],{},"在gateway.module.ts里注册。synchronize: true时 user表会自动创建。不过这里只是为了演示，实际上我是在主服务里维护的user模块",[97,3186,3188],{"className":267,"code":3187,"language":269,"meta":102,"style":102},"TypeOrmModule.forRootAsync({\n      useFactory() {\n        return {\n          type: 'mysql',\n          host: 'localhost',\n          port: 3306,\n          username: 'root',\n          password: '123456',\n          database: 'zzstudio',\n          synchronize: true,\n          logging: true,\n          entities: [User],\n          poolSize: 10,\n          connectorPackage: 'mysql2',\n          extra: {\n            authPlugin: 'sha256_password',\n          },\n        };\n      },\n    }),\n",[104,3189,3190,3200,3208,3214,3224,3234,3243,3253,3262,3272,3281,3290,3295,3305,3315,3320,3330,3334,3338,3342],{"__ignoreMap":102},[107,3191,3192,3195,3198],{"class":109,"line":110},[107,3193,3194],{"class":288},"TypeOrmModule.",[107,3196,3197],{"class":113},"forRootAsync",[107,3199,441],{"class":288},[107,3201,3202,3205],{"class":109,"line":219},[107,3203,3204],{"class":113},"      useFactory",[107,3206,3207],{"class":288},"() {\n",[107,3209,3210,3212],{"class":109,"line":309},[107,3211,2438],{"class":276},[107,3213,566],{"class":288},[107,3215,3216,3219,3222],{"class":109,"line":315},[107,3217,3218],{"class":288},"          type: ",[107,3220,3221],{"class":117},"'mysql'",[107,3223,324],{"class":288},[107,3225,3226,3229,3232],{"class":109,"line":327},[107,3227,3228],{"class":288},"          host: ",[107,3230,3231],{"class":117},"'localhost'",[107,3233,324],{"class":288},[107,3235,3236,3238,3241],{"class":109,"line":333},[107,3237,491],{"class":288},[107,3239,3240],{"class":121},"3306",[107,3242,324],{"class":288},[107,3244,3245,3248,3251],{"class":109,"line":344},[107,3246,3247],{"class":288},"          username: ",[107,3249,3250],{"class":117},"'root'",[107,3252,324],{"class":288},[107,3254,3255,3258,3260],{"class":109,"line":350},[107,3256,3257],{"class":288},"          password: ",[107,3259,2210],{"class":117},[107,3261,324],{"class":288},[107,3263,3264,3267,3270],{"class":109,"line":356},[107,3265,3266],{"class":288},"          database: ",[107,3268,3269],{"class":117},"'zzstudio'",[107,3271,324],{"class":288},[107,3273,3274,3277,3279],{"class":109,"line":362},[107,3275,3276],{"class":288},"          synchronize: ",[107,3278,1005],{"class":121},[107,3280,324],{"class":288},[107,3282,3283,3286,3288],{"class":109,"line":507},[107,3284,3285],{"class":288},"          logging: ",[107,3287,1005],{"class":121},[107,3289,324],{"class":288},[107,3291,3292],{"class":109,"line":513},[107,3293,3294],{"class":288},"          entities: [User],\n",[107,3296,3297,3300,3303],{"class":109,"line":519},[107,3298,3299],{"class":288},"          poolSize: ",[107,3301,3302],{"class":121},"10",[107,3304,324],{"class":288},[107,3306,3307,3310,3313],{"class":109,"line":525},[107,3308,3309],{"class":288},"          connectorPackage: ",[107,3311,3312],{"class":117},"'mysql2'",[107,3314,324],{"class":288},[107,3316,3317],{"class":109,"line":531},[107,3318,3319],{"class":288},"          extra: {\n",[107,3321,3322,3325,3328],{"class":109,"line":731},[107,3323,3324],{"class":288},"            authPlugin: ",[107,3326,3327],{"class":117},"'sha256_password'",[107,3329,324],{"class":288},[107,3331,3332],{"class":109,"line":1035},[107,3333,2748],{"class":288},[107,3335,3336],{"class":109,"line":1040},[107,3337,2470],{"class":288},[107,3339,3340],{"class":109,"line":1046},[107,3341,347],{"class":288},[107,3343,3344],{"class":109,"line":1051},[107,3345,3346],{"class":288},"    }),\n",[11,3348,3349],{},"这样，实现了基本的接口之后，再配合上边写的自定义Guard，就可以实现对权限的校验了。细节部分，我就不展开了，对大家意义也不大。",[11,3351,3352],{},"本地开发的话，我是用的docker desktop，先跑一个mysql，这样感觉比较省事。后续上线的话，我用的是docker-compose，把服务+mysql+redis 一起编排上线",[11,3354,3355],{},"ok。结束",[11,3357,3358],{},"PS：后续更新的方向，将会按照故事的发展进行，但每一个系列我也会尽快收尾。",[26,3360,3361],{"id":3361},"小结",[11,3363,3364],{},"这次分享了一下我最初构思的那个项目的大概背景和设定。以及从零搭建一个Nest项目，按照上边流程，搭建出来是没问题的。",[11,3366,3367],{},"因为我自己的服务早就写完了，这次我专门从头建了一个项目，又码了一遍，后面可能鉴权，token方面写的不太细致。因为感觉这种教程应该是一搜一大堆了，我再重新来一遍意义不大。",[11,3369,3370,3371,3378],{},"代码我还是会尽快放在",[34,3372,3375],{"href":3373,"rel":3374},"https://github.com/zzdaddy/nestjs-template-zz",[38],[21,3376,3377],{},"Github","中，作为v0.1.0版本，后续会把鉴权、日志、文件、邮箱、支付等等一些公共的模块会更新在这个仓库里，需要的同学可以pull下来，再自己改改，作为项目的启动模版。",[11,3380,3381,3382,3387,3388,3391,3392,3395],{},"当然，有任何问题也可以在公众号：",[34,3383,3385],{"href":36,"rel":3384},[38],[21,3386,39],{}," 找到我。后面我会持续分享早早集市里的每一个“摊位”的诞生和迭代过程，但",[21,3389,3390],{},"不构成","对大家技术栈、代码规范、命名方式、架构层面合理性等等见仁见智的角度的",[21,3393,3394],{},"任何建议","。",[11,3397,3398],{},"大家就当听个故事，新手的话顺便还能入个门，半路进厂想做全栈的也可以参考一下我的思路，也欢迎来找我一起交流 ~",[11,3400,3401],{},"感谢阅读，我是枣把儿 ~",[3403,3404,3405],"style",{},"html pre.shiki code .s7eDp, html code.shiki .s7eDp{--shiki-default:#6F42C1}html pre.shiki code .sYBdl, html code.shiki .sYBdl{--shiki-default:#032F62}html pre.shiki code .sYu0t, html code.shiki .sYu0t{--shiki-default:#005CC5}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html pre.shiki code .sD7c4, html code.shiki .sD7c4{--shiki-default:#D73A49}html pre.shiki code .sgsFI, html code.shiki .sgsFI{--shiki-default:#24292E}html pre.shiki code .sqxcx, html code.shiki .sqxcx{--shiki-default:#E36209}html pre.shiki code .sB1qb, html code.shiki .sB1qb{--shiki-default:#B31D28;--shiki-default-font-style:italic}",{"title":102,"searchDepth":219,"depth":219,"links":3407},[3408,3409,3410,3411,3419],{"id":28,"depth":219,"text":29},{"id":91,"depth":219,"text":92},{"id":804,"depth":219,"text":804},{"id":877,"depth":219,"text":877,"children":3412},[3413,3414,3415,3416,3417,3418],{"id":898,"depth":309,"text":899},{"id":1066,"depth":309,"text":1066},{"id":1140,"depth":309,"text":1141},{"id":1562,"depth":309,"text":1563},{"id":2244,"depth":309,"text":2244},{"id":2584,"depth":309,"text":2584},{"id":3361,"depth":219,"text":3361},"2023-12-13T00:00:00.000Z","Nest 项目搭建","md",null,"2025-08-19T00:00:00.000Z",{"published":3426,"category":3427,"date created":3426},"2023-12-13","技术","/post/nest/nest-project-quick-start","---\npublished: 2023-12-13\nauthor: 枣把儿\ntitle: 一个产品要有一个“好底子”：Nest项目搭建\ndescription: Nest 项目搭建\ntags: [\"Node\", \"Nest\"]\ncategory: 技术\ndate created: 2023-12-13\nlastmod: 2025-08-19\ndate: 2023-12-13\nshowTitle: 一个产品要有一个“好底子”：Nest项目搭建\n---\n大家好，我是枣把儿。\n\n上周搞了一个前端小项目：Pixeled Pic Pro， 是一个用来制作像素风格LOGO的Canvas编辑器。\n\n同样，它的后端也是**本着能用就行，先把功能搞出来**的原则。我来分享一下后端实现过程以及发生的故事~\n\n## “底子”\n\n我一开始要做的项目起名叫：[早早集市](https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A)。这是一个从不知道什么时候到23年8月份左右完成构思，在方向上开始清晰起来（当时是这么认为的）的项目。\n\n原因就是，平时脑子里想法太多，多到必须下来，越写越多之后，我就在想怎么把他们搞出来，并且搞的有点联系。\n\n然后再去了几次夜市吃喝之后，我就有了点灵感：我要不整个电子集市吧！\n\n和大家去赶大集一样，每个产品相当于一个摊位，可以在一个入口里，看到所有在营业的\"摊位\"，并且像大集里的摊主一样，每个产品也是在向互联网用户提供服务或商品。\n\n我仔细想想之后啊，感觉真不错。摊位五花八门，没有限制，我的想法也是天马行空，指不定想做什么，可以取悦自己；有的摊位提供“**商品**”；有的摊位提供“**服务**”；还有的摊位给别的摊位提供商品，个体也可以直接去他那“**进货**”。\n\n打通了自己的想法之后，就越想越顺，也越想越复杂。生怕架构层面无法满足自己的设想。\n\n搜索了很久微服务架构相关文章，问了下前同事（Java、运维）相关的思路（你问我为什么不问现同事？也问了，大部分表示就是用Spring全家桶，也知道微服务，也知道消息队列，也知道k8s。问怎么实现的、怎么设计的。不知道），最后也是给自己泼了泼冷水。\n\n算了，不整那么复杂了，本来搞个项目，也是为了万一35岁以后真不干前端了，给自己留点\"互联网遗产\"，证明自己来过。别还没开始就给自己折腾\"死了\"。\n\n冷却下来之后，我还发现，这玩意搞好了是个集市，搞不好不就是个工具站吗，网上一搜一大堆！\n\n你看，果然还是打退堂鼓的时候思路更清晰一些。\n\n但好在这次的构思过程足够深入，冷静下来之后还是让我感觉值得做下去，所以还是继续开始了这个故事。\n\n所以，Pixeled Pic Pro 也是其中一个“摊位”，摊主(名字待定)提供的正是“服务”。\n\n听完了故事，那就一起开始摆摊吧。\n\n## 新建Nest项目\n\nnest提供了 @nestjs/cli 这个包，先来安装一下\n\n```shell\nnpm i -g @nestjs/cli\n```\n\n已经安装完了的话，可以升级一下\n\n```shell\nnpm update -g @nestjs/cli\n```\n\n安装完成之后可以使用 ==nest -h== 查看有哪些命令，后续会经常用到\n\n其中 ==--no-spec== 可以指定不生成测试文件，后面会用到\n\n这里我把服务分为两个，一个gateway服务，用来对外实现api接口及鉴权。 一个主服务，实现所有业务。除非不能满足业务，否则不再拆分。\n\n**PS: 拆出gateway只是为了在实际业务中感受它的好处和坏处，大家自行甄别、自由选择**\n\n开始创建项目！\n\n```shell\nnest new 项目名\n```\n\n![](https://img.zzao.club/article/202411191446171.png)\n\n选择pnpm后等待安装完成，完成后已经可以运行， 进入项目根目录\n\n```shell\npnpm start:dev\n```\n\n打开网站localhost:3000，可以看到Hello World！字样\n\n因为我这里需要有另一个网关服务，所以我再新建一个app，通过monorepo的方式管理\n\n```shell\n# generator 可以缩写为 g\nnest g app gateway\n```\n\n此时可以看到app gateway 已经被创建， 同时自动创建了一个apps文件夹，里面包含两个app\n\n![](https://img.zzao.club/article/202411191446172.png)\n\n安装微服务需要的包：\n\n```shell\n# add会安装在dependencies  加参数 -D 会安装在 devDependencies\npnpm add @nestjs/microservices\n```\n\n然后改造一下server部分，因为gateway在前，server在后，所以server需要改成微服务，通过TCP和gateway通信\n\n```typescript\nconst app = await NestFactory.createMicroservice\u003CMicroserviceOptions>(\n    AppModule,\n    {\n      transport: Transport.TCP,\n      options: {\n        port: 7577,\n      },\n    },\n  );\n  await app.listen();\n```\n\n然后在app.controller.ts里改一下接口，一会用来测试一下\n\n```typescript\n@MessagePattern('hello')\n  getHello(): string {\n    return 'hello by zzstudio-server';\n  }\n```\n\n来到gateway这边， gateway.module.ts里同样也要注册一下微服务，端口号和上面对应起来\n\n```typescript\n@Module({\n  imports: [\n    ClientsModule.register([\n      {\n        name: 'ZZSTUDIO_SERVER',\n        transport: Transport.TCP,\n        options: {\n          port: 7577,\n        },\n      },\n    ]),\n  ],\n  controllers: [GatewayController],\n  providers: [GatewayService],\n})\n```\n\n然后在gateway.controller.ts里也写个方法测试一下\n对了，先用@Inject注入刚才注册的。可以看到有个ts报错，我们回到server那边\n```typescript\n@Controller()\nexport class GatewayController {\n  @Inject('ZZSTUDIO_SERVER')\n  private serverClient: ClientProxy;\n  constructor(private readonly gatewayService: GatewayService) {}\n\n  @Get()\n  getHello(): string {\n    return this.gatewayService.getHello();\n  }\n\n  @Get('app')\n  getServerHello(): unknown {\n    return this.serverClient.send('hello', 'hello');\n  }\n}\n```\n\n把两个项目跑起来测试一下， 因为我们用的zzstudio-server新建的项目，所以跑dev默认启动的是zzstudio-server。\n\n```shell\npnpm start:dev\npnpm start:dev gateway\n```\n\n然后浏览器输入localhost:3000/app，可以看到hello by zzstudio-server，通了。\n\n然后再试试打包\n\n```shell\n# 这会打包zzstuido-server服务\npnpm build\n\n# 这会打包gateway服务\npnpm build gateway\n```\n\n打包完成后，可以看到dist里分别产生了各自服务的文件夹\n\n## 功能梳理\n\n还是按照以前的习惯，做事之前先梳理和拆解，只要不影响核心功能，就放在下一个版本迭代。\n\n也许会有一些同学奇怪，明明是自己的产品，为什么要和公司打工一样，还要搞版本，还要搞大纲，我在公司都没这么搞！\n\n我是这样理解的：首先做产品的核心是==要把一个产品实现==，==过程有序，结果不遗漏==，就和你记账一样，如果你不记的很细致，你就无法总结到底哪些地方不该花钱。其次，你不能等有了用户，再重新完善文档，==因为你说不好是你的产品、还是你的故事、还是你的过程吸引了别人==。最后，这是自己的产品，是自己内心的==乌托邦==，你会本能的对它倾注更多的心血。\n\n这样也能明白，为什么在公司打工为什么提不起劲儿来，因为它不是你的，也不是你感兴趣的，只是一个赚取收入的渠道。 同时也可以知道，如果真的能把公司的产品，代入到自己的产品中，同时被公司领导们注意到，且不被自己的小领导窃取成果，且愿意推举给大领导，且老板也有正确的认知，公司也会因你而精彩（狗头保命）\n\n说完了废话，开始正题。\n\n首先gateway部分。\n1. 对外提供接口，可以起一个公共的前缀，比如/api/v1。\n2. 如果前端发生了改动，则去修改gateway里的请求逻辑，主服务不需要变\n3. 实现鉴权。jwt 双token，前端无感刷新，过滤掉没权限的请求。\n4. 如果主服务发生了改动，则去修改gateway里的请求逻辑，前端不需要变\n\n接口目前很简单：\n1. 登录注册\n\t1. 先用用户名密码+邮箱验证码注册\n\t2. 后续再添加关注公众号注册之类的操作\n2. 导出功能 \n\t1. 次数统计 看看有多少人使用了导出。 相当于埋点了\n\t2. 导出并压缩 （这是一个不着急实现的公共功能，可以预见其他的产品也会有这个功能）\n3. 保存预设，json\n4. 保存图片，以一种字符串或者json的形式\n\n其中3，4都不是必须的，先放一放。只要实现了框架结构和基本功能，后续按功能再加就很快了\n\n## 功能实现\n\n实现之前先用图来串一串思路。\n\n> 用的Obsidian的Excalidraw画的\n\n\n![](https://img.zzao.club/article/202411191446173.png)\n\n然后开始按照这个思路去实现功能，这里我只演示几个关键点。同样代码贴在文末，免费、开源\n\n### JWT模块注册\n\n安装 @nestjs/jwt\n```shell\npnpm add @nestjs/jwt\n```\n\ngateway.module.ts里注册\n```typescript\n@Module({\n  imports: [\n    ClientsModule.register([\n      {\n        name: 'ZZSTUDIO_SERVER',\n        transport: Transport.TCP,\n        options: {\n          port: 7577,\n        },\n      },\n    ]),\n    JwtModule.register({\n        global: true,\n        secret: 'zzdaddy',\n        signOptions: {\n          expiresIn: '1d',\n        },\n      }),\n  ],\n  controllers: [GatewayController],\n  providers: [GatewayService],\n})\n```\n\n### 自定义decorator\n我想设置一个开关，标识哪个接口可以不需要登录就访问，没有这个标识的就都需要鉴权\n\n先自定义一个装饰器，用于设置接口是否是公开的（true），没设置就是false\n\n```shell\n# 在gateway服务下，新建了一个custom文件夹，里面有一个custom.decorator.ts\nnest g decorator custom --project=gateway\n```\n\n实现装饰器 custom.decorator.ts\n\n```typescript\nexport const setPublicRoute = () => SetMetadata('isPublicRoute', true);\n```\n\n### 自定义Guard\n按照上图的思路，现在应该写一个Guard，用来控制权限\n```\n# 生成后自己改个名， 我改成了LoginGuard\nnest g guard globalGuard --project=gateway --no-spec\n```\n\n然后在生成的guard里实现\n\n```typescript\ncanActivate(\n    context: ExecutionContext,\n  ): boolean | Promise\u003Cboolean> | Observable\u003Cboolean> {\n    const request: Request = context.switchToHttp().getRequest();\n\n    const isPublicRoute = this.reflector.getAllAndOverride('isPublicRoute', [\n      context.getClass(),\n      context.getHandler(),\n    ]);\n    if (isPublicRoute) {\n      return true;\n    }\n\n    const authorization = request.headers.authorization;\n\n    if (!authorization) {\n      throw new UnauthorizedException('用户未登录');\n    }\n\n    try {\n      const token = authorization.split(' ')[1];\n      const data = this.jwtService.verify(token);\n      // 这里会报没有user, 可以用declare module 给上边的 Request 在类型空间定义一下user\n      request.user = data.user;\n      return true;\n    } catch (e) {\n      throw new UnauthorizedException('token 失效，请重新登录');\n    }\n  }\n```\n\nguard要想生效，还要在gateway.module.ts里注册一下\n\n```typescript\n providers: [\n    {\n      provide: APP_GUARD,\n      useClass: LoginGuard,\n    },\n    GatewayService,\n  ],\n```\n\n此时注册完成后，再去浏览器请求一下 /app 接口，可以发现已经被拦截住了\n\n```typescript\n{\n  message: \"用户未登录\",\n  error: \"Unauthorized\",\n  statusCode: 401\n}\n```\n\n### 自定义Filter\n拦截住之后，问题就来了，貌似公司里Java接口，返回的都是内种的格式，我怎么自定义自己的返回格式\n再回顾上图，实现一个过滤器，因为401是抛出了一个错误，会被filter捕捉到\n再从custom里建一个filter吧，建完了把名字改改\n\n```shell\nnest g filter custom --project=gateway --no-spec\n```\n\n实现一下功能，因为他会捕捉所有错误，也就是你其他地方抛出来的不是HttpException的错误也会在这里捕捉到，所以要判断一下。\n\n```typescript\n@Catch()\nexport class HttpCatchFilter implements ExceptionFilter {\n  catch(exception: HttpException, host: ArgumentsHost) {\n    const http = host.switchToHttp();\n    const response = http.getResponse\u003CResponse>();\n\n\t// 我把自己代码写错导致的错误都返回500\n    const statusCode =\n      exception instanceof HttpException\n        ? exception.getStatus()\n        : HttpStatus.INTERNAL_SERVER_ERROR;\n    // 使用exception的message 也可能是 exception.message.message 或 exception.message.error\n    let message = exception.message;\n    // 使用了参数校验之后，多个参数校验不通过，会返回一个数组，所以这里合并了一下，优化展示\n    if (exception instanceof HttpException) {\n      let res = exception.getResponse() as { message: string[] };\n      message = res?.message?.join\n        ? res?.message?.join(',')\n        : exception.message;\n    }\n    // 这里json的格式、字段、内容，自己随便写\n    response.status(statusCode).json({\n      code: statusCode,\n      message,\n      error: 'Bad Request',\n    });\n  }\n}\n\n```\n\n写完同样需要在gateway.module.ts里的providers下注册（其他全局注册方式建议自行查阅）\n\n```typescript\nproviders: [\n    {\n      provide: APP_GUARD,\n      useClass: LoginGuard,\n    },\n    {\n        provide: APP_FILTER,\n        useClass: CommonErrorCatchFilter,\n      },\n    GatewayService,\n  ],\n```\n\n再回到浏览器看一下/app接口, 在校验失败的情况下，返回结果已经变成了我们想要的结构\n\n```json\n{\n  code: 401,\n  message: \"用户未登录\",\n  error: \"Bad Request\"\n}\n```\n\n然后再把刚才写的setPublicRoute给/app这个接口用一下\n\n```typescript\n@Get('app')\n@setPublicRoute()\ngetServerHello(): Observable\u003Cany> {\n  return this.serverClient.send('hello', 'hello');\n}\n```\n\n再去浏览器看一下，hello by zzstudio-server，这个内容又出来了。\n\n写完了别忘了保存啊。我都忘了，还以为哪里写错了呢。\n\n然后再实现一个post接口试试看\n\n```typescript\n@Post('login')\n@setPublicRoute()\nlogin(): Observable\u003Cany> {\n  return this.serverClient.send('login', { username: 1, password: 2 });\n}\n```\n\n\n然后在我的主服务里，接受这个请求，然后返回俩token\n\n```typescript\n@MessagePattern('login')\n  login(): object {\n    return {\n      access_token: '123456',\n      refresh_token: '123456',\n    };\n  }\n```\n\n然后再拿postman、postwoman、apifox去请求试一试，我用的apifox\n\n![](https://img.zzao.club/article/202411191446174.png)\n\n可以看到，拿到了数据，但明显还不是我们想要的结构。\n###  自定义interceptor\n所以我们再回顾一下上图，可以在interceptor里去处理一下next.handle() 之后的数据\n再回顾上图，实现一个拦截器\n再从custom里建一个吧，建完了把名字改改\n\n```shell\nnest g interceptor custom --project=gateway --no-spec\n```\n\n实现一下功能\n\n```typescript\n@Injectable()\nexport class HttpCommonInterceptor implements NestInterceptor {\n  intercept(context: ExecutionContext, next: CallHandler): Observable\u003Cany> {\n    const response = context.switchToHttp().getResponse\u003CResponse>();\n    // 201时返回200\n    if (response.statusCode === HttpStatus.CREATED)\n      response.status(HttpStatus.OK);\n    return next.handle().pipe(\n      map((data) => {\n        return {\n          code: 200,\n          data,\n          message: 'ok',\n        };\n      }),\n    );\n  }\n}\n```\n\n也需要在gateway.module.ts里注册一下\n```typescript\nproviders: [\n    {\n      provide: APP_GUARD,\n      useClass: LoginGuard,\n    },\n    {\n      provide: APP_INTERCEPTOR,\n      useClass: HttpCommonInterceptor,\n    },\n    {\n      provide: APP_FILTER,\n      useClass: CommonErrorCatchFilter,\n    },\n    GatewayService,\n  ],\n```\n\n然后再去apifox请求看一下\n![](https://img.zzao.club/article/202411191446175.png)\n\n是我们想要的格式了。\n\n### 权限校验部分\n\n我新建一个auth模块， 在里面实现login、refreshToken接口\n \n```shell\nnest g resource auth --project=gateway --no-spec\n```\n\n登录接口，返回两个token，给到前端之后，前端请求时需要在headers里携带access_token，当提示前端已过期时，前端再用refresh_token去请求refresh接口。refresh接口则会再返回两个新的token。以此达到无限续签。\n```typescript\n@Post('login')\n  async login(\n    @Body() user: LoginDto,\n    @Res({ passthrough: true }) res: Response,\n  ): Promise\u003Cany> {\n    let userInfo = await this.authService.login(user);\n    if (userInfo) {\n      const access_token = this.jwtService.sign(\n        {\n          user: {\n           ...\n          },\n        },\n        {\n          expiresIn: '60m',\n        },\n      );\n      const refresh_token = this.jwtService.sign(\n        {\n          userId: userInfo.id,\n        },\n        {\n          expiresIn: '7d',\n        },\n      );\n\n      res.setHeader('token', access_token);\n      return {\n        access_token,\n        refresh_token,\n      };\n    }\n  }\n\n   @Get('refresh')\n  async refresh(@Query() refreshParams: refreshDto) {\n    try {\n      const data = this.jwtService.verify(refreshParams.refreshToken);\n\n      const user = await this.authService.findUserById(data.userId);\n\n      const access_token = this.jwtService.sign(\n        {\n          ...\n        },\n        {\n          expiresIn: '60m',\n        },\n      );\n\n      const refresh_token = this.jwtService.sign(\n        {\n          userId: user.id,\n        },\n        {\n          expiresIn: '7d',\n        },\n      );\n\n      return {\n        access_token,\n        refresh_token,\n      };\n    } catch (e) {\n      throw new UnauthorizedException('token 已失效，请重新登录');\n    }\n  }\n```\n\n使用typeorm链接mysql\n\n```shell\npnpm add typeorm @nestjs/typeorm mysql2\n```\n\n在gateway.module.ts里注册。synchronize: true时 user表会自动创建。不过这里只是为了演示，实际上我是在主服务里维护的user模块\n```typescript\nTypeOrmModule.forRootAsync({\n      useFactory() {\n        return {\n          type: 'mysql',\n          host: 'localhost',\n          port: 3306,\n          username: 'root',\n          password: '123456',\n          database: 'zzstudio',\n          synchronize: true,\n          logging: true,\n          entities: [User],\n          poolSize: 10,\n          connectorPackage: 'mysql2',\n          extra: {\n            authPlugin: 'sha256_password',\n          },\n        };\n      },\n    }),\n```\n\n这样，实现了基本的接口之后，再配合上边写的自定义Guard，就可以实现对权限的校验了。细节部分，我就不展开了，对大家意义也不大。\n\n本地开发的话，我是用的docker desktop，先跑一个mysql，这样感觉比较省事。后续上线的话，我用的是docker-compose，把服务+mysql+redis 一起编排上线\n\nok。结束\n\nPS：后续更新的方向，将会按照故事的发展进行，但每一个系列我也会尽快收尾。\n\n## 小结\n\n这次分享了一下我最初构思的那个项目的大概背景和设定。以及从零搭建一个Nest项目，按照上边流程，搭建出来是没问题的。\n\n因为我自己的服务早就写完了，这次我专门从头建了一个项目，又码了一遍，后面可能鉴权，token方面写的不太细致。因为感觉这种教程应该是一搜一大堆了，我再重新来一遍意义不大。\n\n代码我还是会尽快放在[**Github**](https://github.com/zzdaddy/nestjs-template-zz)中，作为v0.1.0版本，后续会把鉴权、日志、文件、邮箱、支付等等一些公共的模块会更新在这个仓库里，需要的同学可以pull下来，再自己改改，作为项目的启动模版。\n\n当然，有任何问题也可以在公众号：[**早早集市**](https://mp.weixin.qq.com/s/A8wHxE5Q2jl6Su_7QA6f-A) 找到我。后面我会持续分享早早集市里的每一个“摊位”的诞生和迭代过程，但**不构成**对大家技术栈、代码规范、命名方式、架构层面合理性等等见仁见智的角度的**任何建议**。\n\n大家就当听个故事，新手的话顺便还能入个门，半路进厂想做全栈的也可以参考一下我的思路，也欢迎来找我一起交流 ~\n\n感谢阅读，我是枣把儿 ~\n\n\n\n\n",{"title":5,"description":3421},"post/Nest/nest-project-quick-start",[3433,3434],"Node","Nest","xc1C6qG-4yjkCkHuCVO78dKbH3j5SIdq9o8XGLQ7UQ8",[3437,3441],{"title":3438,"path":3439,"stem":3440},"OpenClaw 安装入门（Windows）","/post/zzao/openclaw/openclaw-install-windows","post/zzao/openclaw/openclaw-install-windows",{"title":3442,"path":3443,"stem":3444},"假设你是AI，你的Skill应该是什么样的","/post/zzao/ai-skill-structure","post/zzao/ai-skill-structure",1779005087128]