@devflow-tools/plugin-nest 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1129 -21
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -68,16 +68,451 @@ export const nestPlugin = {
|
|
|
68
68
|
{
|
|
69
69
|
name: "new-module",
|
|
70
70
|
description: "新建 NestJS 模块(Controller + Service + Module + DTO + 测试)",
|
|
71
|
-
version: "
|
|
71
|
+
version: "2.0.0",
|
|
72
72
|
steps: [
|
|
73
|
-
{
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
73
|
+
{
|
|
74
|
+
id: "analyze-domain",
|
|
75
|
+
title: "分析业务领域确定模块边界",
|
|
76
|
+
instruction: [
|
|
77
|
+
"## 分析业务领域确定模块边界",
|
|
78
|
+
"",
|
|
79
|
+
"根据需求描述,识别独立的业务领域,确定新 Module 的职责边界和与现有模块的关系。",
|
|
80
|
+
"",
|
|
81
|
+
"**执行动作**:",
|
|
82
|
+
"1. 查看现有模块结构:",
|
|
83
|
+
" ```bash",
|
|
84
|
+
" find src/modules -maxdepth 2 -type d",
|
|
85
|
+
" find src/ -name '*.module.ts'",
|
|
86
|
+
" ```",
|
|
87
|
+
"2. 查看现有 Module 的 exports 清单:",
|
|
88
|
+
" ```bash",
|
|
89
|
+
" grep -rn 'exports:' src/ --include='*.module.ts'",
|
|
90
|
+
" ```",
|
|
91
|
+
"3. 查看项目根模块 AppModule 的 imports:",
|
|
92
|
+
" ```bash",
|
|
93
|
+
" grep -rn 'imports:' src/app.module.ts",
|
|
94
|
+
" ```",
|
|
95
|
+
"4. 根据业务领域划分新 Module 的边界:",
|
|
96
|
+
" - 是否应该新增独立 Module,还是扩展现有 Module",
|
|
97
|
+
" - 新 Module 需要提供哪些 Controller 端点",
|
|
98
|
+
" - 新 Module 依赖哪些现有 Module(通过 imports)",
|
|
99
|
+
" - 新 Module 要 exports 哪些 Provider 供其他模块使用",
|
|
100
|
+
"5. 检查是否存在命名约定(如 `feature/` vs `modules/`)",
|
|
101
|
+
"",
|
|
102
|
+
"**产出格式**:",
|
|
103
|
+
"```",
|
|
104
|
+
"module_name: <PascalCase>",
|
|
105
|
+
"module_path: src/modules/<module-name>/",
|
|
106
|
+
"responsibilities: [<一句话描述>, ...]",
|
|
107
|
+
"depends_on: [<existing module name>, ...]",
|
|
108
|
+
"exports: [<provider name>, ...]",
|
|
109
|
+
"endpoints: [{ method, path, description }, ...]",
|
|
110
|
+
"```",
|
|
111
|
+
].join("\n"),
|
|
112
|
+
suggestedTools: ["Bash", "Read"],
|
|
113
|
+
output: "模块边界定义、依赖关系、端点规划",
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
id: "define-dtos",
|
|
117
|
+
title: "定义 DTO 和验证规则",
|
|
118
|
+
instruction: [
|
|
119
|
+
"## 定义 DTO 和验证规则(class-validator + Swagger 装饰器)",
|
|
120
|
+
"",
|
|
121
|
+
"为 Module 的所有请求和响应定义类型安全的 DTO,并加上 class-validator 验证规则和 Swagger 文档装饰器。",
|
|
122
|
+
"",
|
|
123
|
+
"**执行动作**:",
|
|
124
|
+
"1. 在 `src/modules/<name>/dto/` 目录下创建 DTO 文件:",
|
|
125
|
+
" - `create-<name>.dto.ts` — 创建请求体",
|
|
126
|
+
" - `update-<name>.dto.ts` — 更新请求体",
|
|
127
|
+
" - `query-<name>.dto.ts` — 查询参数(分页、过滤、排序)",
|
|
128
|
+
" - `<name>-response.dto.ts` — 响应体(可选)",
|
|
129
|
+
"2. 每个字段使用 class-validator 装饰器:",
|
|
130
|
+
" ```typescript",
|
|
131
|
+
" import { IsString, IsInt, MinLength, IsOptional } from 'class-validator';",
|
|
132
|
+
" import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';",
|
|
133
|
+
"",
|
|
134
|
+
" export class CreateUserDto {",
|
|
135
|
+
" @ApiProperty({ description: '用户名', example: 'alice' })",
|
|
136
|
+
" @IsString()",
|
|
137
|
+
" @MinLength(3)",
|
|
138
|
+
" username: string;",
|
|
139
|
+
"",
|
|
140
|
+
" @ApiPropertyOptional({ description: '年龄' })",
|
|
141
|
+
" @IsOptional()",
|
|
142
|
+
" @IsInt()",
|
|
143
|
+
" age?: number;",
|
|
144
|
+
" }",
|
|
145
|
+
" ```",
|
|
146
|
+
"3. 每个字段同时使用 `@ApiProperty` 和 `@IsXxx` 装饰器,避免遗漏",
|
|
147
|
+
"4. 使用 `@ApiProperty({ example: ... })` 提供示例值",
|
|
148
|
+
"5. 查询类 DTO 使用 `@ApiPropertyOptional` 标记可选参数",
|
|
149
|
+
"6. 对于分页 DTO,使用 `@Type(() => Number)` 配合 class-transformer:",
|
|
150
|
+
" ```typescript",
|
|
151
|
+
" import { Type } from 'class-transformer';",
|
|
152
|
+
" @IsOptional()",
|
|
153
|
+
" @Type(() => Number)",
|
|
154
|
+
" @IsInt()",
|
|
155
|
+
" page?: number;",
|
|
156
|
+
" ```",
|
|
157
|
+
"7. 复用已有 DTO 时使用 `PartialType`、`PickType`、`OmitType`:",
|
|
158
|
+
" ```typescript",
|
|
159
|
+
" export class UpdateUserDto extends PartialType(CreateUserDto) {}",
|
|
160
|
+
" ```",
|
|
161
|
+
"",
|
|
162
|
+
"**产出格式**:",
|
|
163
|
+
"```",
|
|
164
|
+
"dto_files_created: [{ path, class_name, fields: [{ name, type, validators }] }]",
|
|
165
|
+
"```",
|
|
166
|
+
].join("\n"),
|
|
167
|
+
suggestedTools: ["Read", "Write", "Edit"],
|
|
168
|
+
output: "DTO 文件清单、字段验证规则、Swagger 装饰器",
|
|
169
|
+
depends: ["analyze-domain"],
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
id: "create-service",
|
|
173
|
+
title: "创建 Service",
|
|
174
|
+
instruction: [
|
|
175
|
+
"## 创建 Service(业务逻辑 + Prisma 查询)",
|
|
176
|
+
"",
|
|
177
|
+
"封装业务逻辑的 Service,使用 Prisma 进行数据库操作。",
|
|
178
|
+
"",
|
|
179
|
+
"**执行动作**:",
|
|
180
|
+
"1. 创建 Service 文件 `src/modules/<name>/<name>.service.ts`:",
|
|
181
|
+
" ```typescript",
|
|
182
|
+
" import { Injectable, NotFoundException, ConflictException } from '@nestjs/common';",
|
|
183
|
+
" import { PrismaService } from '../prisma/prisma.service';",
|
|
184
|
+
" import { CreateXxxDto } from './dto/create-xxx.dto';",
|
|
185
|
+
"",
|
|
186
|
+
" @Injectable()",
|
|
187
|
+
" export class XxxService {",
|
|
188
|
+
" constructor(private readonly prisma: PrismaService) {}",
|
|
189
|
+
"",
|
|
190
|
+
" async create(dto: CreateXxxDto) {",
|
|
191
|
+
" try {",
|
|
192
|
+
" return await this.prisma.xxx.create({ data: dto });",
|
|
193
|
+
" } catch (error) {",
|
|
194
|
+
" if (error.code === 'P2002') {",
|
|
195
|
+
" throw new ConflictException('记录已存在');",
|
|
196
|
+
" }",
|
|
197
|
+
" throw error;",
|
|
198
|
+
" }",
|
|
199
|
+
" }",
|
|
200
|
+
"",
|
|
201
|
+
" async findAll(query: QueryXxxDto) {",
|
|
202
|
+
" const { page = 1, pageSize = 20 } = query;",
|
|
203
|
+
" return this.prisma.xxx.findMany({",
|
|
204
|
+
" skip: (page - 1) * pageSize,",
|
|
205
|
+
" take: pageSize,",
|
|
206
|
+
" orderBy: { createdAt: 'desc' },",
|
|
207
|
+
" });",
|
|
208
|
+
" }",
|
|
209
|
+
"",
|
|
210
|
+
" async findOne(id: string) {",
|
|
211
|
+
" const record = await this.prisma.xxx.findUnique({ where: { id } });",
|
|
212
|
+
" if (!record) throw new NotFoundException(`Xxx #${id} not found`);",
|
|
213
|
+
" return record;",
|
|
214
|
+
" }",
|
|
215
|
+
"",
|
|
216
|
+
" async update(id: string, dto: UpdateXxxDto) {",
|
|
217
|
+
" await this.findOne(id);",
|
|
218
|
+
" return this.prisma.xxx.update({ where: { id }, data: dto });",
|
|
219
|
+
" }",
|
|
220
|
+
"",
|
|
221
|
+
" async remove(id: string) {",
|
|
222
|
+
" await this.findOne(id);",
|
|
223
|
+
" return this.prisma.xxx.delete({ where: { id } });",
|
|
224
|
+
" }",
|
|
225
|
+
" }",
|
|
226
|
+
" ```",
|
|
227
|
+
"2. 使用 `@Injectable()` 装饰器标记所有 Service",
|
|
228
|
+
"3. 通过构造函数注入 PrismaService(或其他依赖的 Service)",
|
|
229
|
+
"4. 使用 NestJS 内置 HttpException 子类(NotFoundException, ConflictException 等)",
|
|
230
|
+
"5. Prisma 已知异常(P2002 唯一约束、P2025 记录不存在)要显式捕获并转换为 HttpException",
|
|
231
|
+
"6. 事务场景使用 `prisma.$transaction([ ... ])` 或交互式事务",
|
|
232
|
+
"7. 避免在 Service 中处理 HTTP 请求/响应细节(那是 Controller 的职责)",
|
|
233
|
+
"",
|
|
234
|
+
"**产出格式**:",
|
|
235
|
+
"```",
|
|
236
|
+
"service_file: <path>",
|
|
237
|
+
"class_name: <Name>Service",
|
|
238
|
+
"methods: [{ name, signature, throws }]",
|
|
239
|
+
"dependencies: [PrismaService, ...]",
|
|
240
|
+
"```",
|
|
241
|
+
].join("\n"),
|
|
242
|
+
suggestedTools: ["Read", "Write", "Edit"],
|
|
243
|
+
output: "Service 文件、方法签名、依赖注入清单",
|
|
244
|
+
depends: ["define-dtos"],
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "create-controller",
|
|
248
|
+
title: "创建 Controller",
|
|
249
|
+
instruction: [
|
|
250
|
+
"## 创建 Controller(路由 + 守卫 + 拦截器)",
|
|
251
|
+
"",
|
|
252
|
+
"定义 HTTP 路由和处理请求参数,将业务逻辑委托给 Service。",
|
|
253
|
+
"",
|
|
254
|
+
"**执行动作**:",
|
|
255
|
+
"1. 创建 Controller 文件 `src/modules/<name>/<name>.controller.ts`:",
|
|
256
|
+
" ```typescript",
|
|
257
|
+
" import {",
|
|
258
|
+
" Controller, Get, Post, Put, Delete, Patch,",
|
|
259
|
+
" Body, Param, Query, ParseIntPipe,",
|
|
260
|
+
" UseGuards, UseInterceptors,",
|
|
261
|
+
" HttpCode, HttpStatus,",
|
|
262
|
+
" } from '@nestjs/common';",
|
|
263
|
+
" import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';",
|
|
264
|
+
" import { XxxService } from './xxx.service';",
|
|
265
|
+
" import { CreateXxxDto } from './dto/create-xxx.dto';",
|
|
266
|
+
" import { UpdateXxxDto } from './dto/update-xxx.dto';",
|
|
267
|
+
" import { QueryXxxDto } from './dto/query-xxx.dto';",
|
|
268
|
+
" import { JwtAuthGuard } from '../auth/jwt-auth.guard';",
|
|
269
|
+
"",
|
|
270
|
+
" @ApiTags('xxx')",
|
|
271
|
+
" @ApiBearerAuth()",
|
|
272
|
+
" @UseGuards(JwtAuthGuard)",
|
|
273
|
+
" @Controller('xxx')",
|
|
274
|
+
" export class XxxController {",
|
|
275
|
+
" constructor(private readonly xxxService: XxxService) {}",
|
|
276
|
+
"",
|
|
277
|
+
" @Post()",
|
|
278
|
+
" @ApiOperation({ summary: '创建 xxx' })",
|
|
279
|
+
" create(@Body() dto: CreateXxxDto) {",
|
|
280
|
+
" return this.xxxService.create(dto);",
|
|
281
|
+
" }",
|
|
282
|
+
"",
|
|
283
|
+
" @Get()",
|
|
284
|
+
" @ApiOperation({ summary: '查询 xxx 列表' })",
|
|
285
|
+
" findAll(@Query() query: QueryXxxDto) {",
|
|
286
|
+
" return this.xxxService.findAll(query);",
|
|
287
|
+
" }",
|
|
288
|
+
"",
|
|
289
|
+
" @Get(':id')",
|
|
290
|
+
" @ApiOperation({ summary: '查询单个 xxx' })",
|
|
291
|
+
" findOne(@Param('id') id: string) {",
|
|
292
|
+
" return this.xxxService.findOne(id);",
|
|
293
|
+
" }",
|
|
294
|
+
"",
|
|
295
|
+
" @Patch(':id')",
|
|
296
|
+
" @ApiOperation({ summary: '更新 xxx' })",
|
|
297
|
+
" update(@Param('id') id: string, @Body() dto: UpdateXxxDto) {",
|
|
298
|
+
" return this.xxxService.update(id, dto);",
|
|
299
|
+
" }",
|
|
300
|
+
"",
|
|
301
|
+
" @Delete(':id')",
|
|
302
|
+
" @HttpCode(HttpStatus.NO_CONTENT)",
|
|
303
|
+
" @ApiOperation({ summary: '删除 xxx' })",
|
|
304
|
+
" remove(@Param('id') id: string) {",
|
|
305
|
+
" return this.xxxService.remove(id);",
|
|
306
|
+
" }",
|
|
307
|
+
" }",
|
|
308
|
+
" ```",
|
|
309
|
+
"2. 路由前缀使用 `@Controller('<resource-name>')`(kebab-case 复数,如 `users`)",
|
|
310
|
+
"3. Controller 只做两件事:接收参数 + 委托给 Service",
|
|
311
|
+
"4. 使用 `@ApiTags()`、`@ApiBearerAuth()`、`@ApiOperation()` 生成 Swagger 文档",
|
|
312
|
+
"5. 使用 `@UseGuards(JwtAuthGuard)` 应用认证守卫(按需)",
|
|
313
|
+
"6. 使用 `@UseInterceptors()` 应用拦截器(如 TransformInterceptor)",
|
|
314
|
+
"7. 参数转换使用 NestJS 内置 Pipe:`ParseIntPipe`、`ParseUUIDPipe` 等",
|
|
315
|
+
"8. 不要在 Controller 中写业务逻辑,所有逻辑在 Service 中",
|
|
316
|
+
"",
|
|
317
|
+
"**产出格式**:",
|
|
318
|
+
"```",
|
|
319
|
+
"controller_file: <path>",
|
|
320
|
+
"class_name: <Name>Controller",
|
|
321
|
+
"route_prefix: <resource-name>",
|
|
322
|
+
"endpoints: [{ method, path, handler, guards, pipes }]",
|
|
323
|
+
"```",
|
|
324
|
+
].join("\n"),
|
|
325
|
+
suggestedTools: ["Read", "Write", "Edit"],
|
|
326
|
+
output: "Controller 文件、路由端点清单、使用的 Guard/Interceptor",
|
|
327
|
+
depends: ["define-dtos"],
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
id: "create-module",
|
|
331
|
+
title: "创建 Module 并注册依赖",
|
|
332
|
+
instruction: [
|
|
333
|
+
"## 创建 Module 并注册依赖",
|
|
334
|
+
"",
|
|
335
|
+
"将所有 Controller、Service、以及依赖的其他 Module 组合成一个内聚的 Module。",
|
|
336
|
+
"",
|
|
337
|
+
"**执行动作**:",
|
|
338
|
+
"1. 创建 Module 文件 `src/modules/<name>/<name>.module.ts`:",
|
|
339
|
+
" ```typescript",
|
|
340
|
+
" import { Module } from '@nestjs/common';",
|
|
341
|
+
" import { XxxController } from './xxx.controller';",
|
|
342
|
+
" import { XxxService } from './xxx.service';",
|
|
343
|
+
" import { PrismaModule } from '../prisma/prisma.module';",
|
|
344
|
+
" import { AuthModule } from '../auth/auth.module';",
|
|
345
|
+
"",
|
|
346
|
+
" @Module({",
|
|
347
|
+
" imports: [PrismaModule, AuthModule],",
|
|
348
|
+
" controllers: [XxxController],",
|
|
349
|
+
" providers: [XxxService],",
|
|
350
|
+
" exports: [XxxService],",
|
|
351
|
+
" })",
|
|
352
|
+
" export class XxxModule {}",
|
|
353
|
+
" ```",
|
|
354
|
+
"2. `imports`:列出所有依赖的其他 Module(提供 PrismaService、AuthService 等)",
|
|
355
|
+
"3. `controllers`:列出本 Module 的所有 Controller",
|
|
356
|
+
"4. `providers`:列出本 Module 的所有 Service / Provider",
|
|
357
|
+
"5. `exports`:如果其他 Module 需要使用本 Module 的 Service,必须显式 export",
|
|
358
|
+
"6. 遵循单一职责:一个 Module 只负责一个业务领域",
|
|
359
|
+
"7. 如果 PrismaService 是全局注册的(`@Global()`),则不需要在 imports 中重复添加",
|
|
360
|
+
"8. 检查是否遗漏了任何依赖的 Provider 未在 Module 中注册",
|
|
361
|
+
"",
|
|
362
|
+
"**产出格式**:",
|
|
363
|
+
"```",
|
|
364
|
+
"module_file: <path>",
|
|
365
|
+
"class_name: <Name>Module",
|
|
366
|
+
"imports: [<module names>]",
|
|
367
|
+
"controllers: [<controller names>]",
|
|
368
|
+
"providers: [<service names>]",
|
|
369
|
+
"exports: [<exported service names>]",
|
|
370
|
+
"```",
|
|
371
|
+
].join("\n"),
|
|
372
|
+
suggestedTools: ["Read", "Write", "Edit"],
|
|
373
|
+
output: "Module 文件、依赖注册清单",
|
|
374
|
+
depends: ["create-service", "create-controller"],
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
id: "register-parent",
|
|
378
|
+
title: "在父模块中注册新模块",
|
|
379
|
+
instruction: [
|
|
380
|
+
"## 在父模块中注册新模块",
|
|
381
|
+
"",
|
|
382
|
+
"将新创建的 Module 注册到根模块(AppModule)或特性父模块中,让 NestJS 能发现并加载它。",
|
|
383
|
+
"",
|
|
384
|
+
"**执行动作**:",
|
|
385
|
+
"1. 找到根模块或父模块文件:",
|
|
386
|
+
" ```bash",
|
|
387
|
+
" grep -rn 'imports:' src/app.module.ts",
|
|
388
|
+
" # 或找到特性父模块",
|
|
389
|
+
" grep -rn 'XxxModule' src/ --include='*.module.ts'",
|
|
390
|
+
" ```",
|
|
391
|
+
"2. 在父模块的 `imports` 数组中添加新 Module:",
|
|
392
|
+
" ```typescript",
|
|
393
|
+
" import { XxxModule } from './xxx/xxx.module';",
|
|
394
|
+
"",
|
|
395
|
+
" @Module({",
|
|
396
|
+
" imports: [",
|
|
397
|
+
" // ... existing imports",
|
|
398
|
+
" XxxModule,",
|
|
399
|
+
" ],",
|
|
400
|
+
" })",
|
|
401
|
+
" export class AppModule {}",
|
|
402
|
+
" ```",
|
|
403
|
+
"3. 如果是特性模块,注册到对应的父特性模块而不是 AppModule",
|
|
404
|
+
"4. 检查 import 路径是否正确(使用相对路径)",
|
|
405
|
+
"5. 检查是否应该使用动态 Module(`XxxModule.forRoot()` / `XxxModule.register()`)",
|
|
406
|
+
"6. 验证 Module 注册顺序(被依赖的 Module 应该先注册)",
|
|
407
|
+
"",
|
|
408
|
+
"**产出格式**:",
|
|
409
|
+
"```",
|
|
410
|
+
"parent_module_file: <path>",
|
|
411
|
+
"parent_class_name: AppModule | <Feature>Module",
|
|
412
|
+
"import_statement: <added import line>",
|
|
413
|
+
"registration_order_ok: true | false",
|
|
414
|
+
"```",
|
|
415
|
+
].join("\n"),
|
|
416
|
+
suggestedTools: ["Read", "Edit", "Bash"],
|
|
417
|
+
output: "父模块修改 diff、注册确认",
|
|
418
|
+
depends: ["create-module"],
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
id: "create-tests",
|
|
422
|
+
title: "生成单元测试和 E2E 测试",
|
|
423
|
+
instruction: [
|
|
424
|
+
"## 生成单元测试和 E2E 测试",
|
|
425
|
+
"",
|
|
426
|
+
"为新 Module 的 Service 和 Controller 编写完整的测试用例。",
|
|
427
|
+
"",
|
|
428
|
+
"**执行动作**:",
|
|
429
|
+
"1. 创建 Service 单元测试文件 `src/modules/<name>/<name>.service.spec.ts`:",
|
|
430
|
+
" ```typescript",
|
|
431
|
+
" import { Test, TestingModule } from '@nestjs/testing';",
|
|
432
|
+
" import { NotFoundException, ConflictException } from '@nestjs/common';",
|
|
433
|
+
" import { XxxService } from './xxx.service';",
|
|
434
|
+
" import { PrismaService } from '../prisma/prisma.service';",
|
|
435
|
+
"",
|
|
436
|
+
" const mockPrisma = {",
|
|
437
|
+
" xxx: {",
|
|
438
|
+
" create: jest.fn(),",
|
|
439
|
+
" findMany: jest.fn(),",
|
|
440
|
+
" findUnique: jest.fn(),",
|
|
441
|
+
" update: jest.fn(),",
|
|
442
|
+
" delete: jest.fn(),",
|
|
443
|
+
" },",
|
|
444
|
+
" };",
|
|
445
|
+
"",
|
|
446
|
+
" describe('XxxService', () => {",
|
|
447
|
+
" let service: XxxService;",
|
|
448
|
+
"",
|
|
449
|
+
" beforeEach(async () => {",
|
|
450
|
+
" const module: TestingModule = await Test.createTestingModule({",
|
|
451
|
+
" providers: [",
|
|
452
|
+
" XxxService,",
|
|
453
|
+
" { provide: PrismaService, useValue: mockPrisma },",
|
|
454
|
+
" ],",
|
|
455
|
+
" }).compile();",
|
|
456
|
+
" service = module.get<XxxService>(XxxService);",
|
|
457
|
+
" });",
|
|
458
|
+
"",
|
|
459
|
+
" afterEach(() => jest.clearAllMocks());",
|
|
460
|
+
"",
|
|
461
|
+
" it('should create a record', async () => {",
|
|
462
|
+
" mockPrisma.xxx.create.mockResolvedValue({ id: '1', ... });",
|
|
463
|
+
" const result = await service.create({ ... });",
|
|
464
|
+
" expect(result).toEqual({ id: '1', ... });",
|
|
465
|
+
" });",
|
|
466
|
+
"",
|
|
467
|
+
" it('should throw NotFoundException when not found', async () => {",
|
|
468
|
+
" mockPrisma.xxx.findUnique.mockResolvedValue(null);",
|
|
469
|
+
" await expect(service.findOne('999')).rejects.toThrow(NotFoundException);",
|
|
470
|
+
" });",
|
|
471
|
+
" });",
|
|
472
|
+
" ```",
|
|
473
|
+
"2. 创建 Controller 单元测试文件 `src/modules/<name>/<name>.controller.spec.ts`:",
|
|
474
|
+
" ```typescript",
|
|
475
|
+
" describe('XxxController', () => {",
|
|
476
|
+
" // 使用 TestingModule 创建 controller",
|
|
477
|
+
" // mock XxxService 的所有方法",
|
|
478
|
+
" // 测试每个端点的返回和异常处理",
|
|
479
|
+
" });",
|
|
480
|
+
" ```",
|
|
481
|
+
"3. (可选)创建 E2E 测试 `test/<name>.e2e-spec.ts`:",
|
|
482
|
+
" ```typescript",
|
|
483
|
+
" import { INestApplication } from '@nestjs/common';",
|
|
484
|
+
" import * as request from 'supertest';",
|
|
485
|
+
" describe('Xxx (e2e)', () => {",
|
|
486
|
+
" let app: INestApplication;",
|
|
487
|
+
" beforeAll(async () => {",
|
|
488
|
+
" const moduleFixture = await Test.createTestingModule({ imports: [AppModule] }).compile();",
|
|
489
|
+
" app = moduleFixture.createNestApplication();",
|
|
490
|
+
" await app.init();",
|
|
491
|
+
" });",
|
|
492
|
+
" it('GET /xxx', () => request(app.getHttpServer()).get('/xxx').expect(200));",
|
|
493
|
+
" });",
|
|
494
|
+
" ```",
|
|
495
|
+
"4. Service 测试使用 `Test.createTestingModule` + 手动 mock Provider",
|
|
496
|
+
"5. Controller 测试 mock Service 的方法返回值",
|
|
497
|
+
"6. 测试覆盖所有分支:成功路径 + 所有异常分支(NotFoundException、ConflictException 等)",
|
|
498
|
+
"7. 使用 `jest.clearAllMocks()` 避免测试间污染",
|
|
499
|
+
"",
|
|
500
|
+
"**产出格式**:",
|
|
501
|
+
"```",
|
|
502
|
+
"test_files_created: [{ path, test_count: N }]",
|
|
503
|
+
"test_cases: [{ describe, it, status: 'written' }]",
|
|
504
|
+
"coverage_target: { statements: '>80%', branches: '>75%' }",
|
|
505
|
+
"```",
|
|
506
|
+
].join("\n"),
|
|
507
|
+
suggestedTools: ["Bash", "Read", "Write"],
|
|
508
|
+
output: "单元测试和 E2E 测试文件、测试用例清单",
|
|
509
|
+
depends: ["create-controller", "create-service"],
|
|
510
|
+
},
|
|
511
|
+
{
|
|
512
|
+
id: "run-tests",
|
|
513
|
+
stepTemplate: "run-tests",
|
|
514
|
+
depends: ["create-tests"],
|
|
515
|
+
},
|
|
81
516
|
],
|
|
82
517
|
triggers: [{ type: "cli", command: "new:module" }, { type: "mcp", tool: "new_module" }],
|
|
83
518
|
},
|
|
@@ -462,26 +897,699 @@ export const nestPlugin = {
|
|
|
462
897
|
{
|
|
463
898
|
name: "swagger-generate",
|
|
464
899
|
description: "Swagger 文档生成:扫描 API → 补充装饰器 → 验证",
|
|
465
|
-
version: "
|
|
900
|
+
version: "2.0.0",
|
|
466
901
|
steps: [
|
|
467
|
-
{
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
902
|
+
{
|
|
903
|
+
id: "scan-controllers",
|
|
904
|
+
title: "扫描所有 Controller 和 DTO",
|
|
905
|
+
instruction: [
|
|
906
|
+
"## 扫描所有 Controller 和 DTO",
|
|
907
|
+
"",
|
|
908
|
+
"全面盘点项目中所有 NestJS Controller 和 DTO 文件,建立 API 清单。",
|
|
909
|
+
"",
|
|
910
|
+
"**执行动作**:",
|
|
911
|
+
"1. 找出所有 Controller 文件:",
|
|
912
|
+
" ```bash",
|
|
913
|
+
" find src/ -name '*.controller.ts' -type f",
|
|
914
|
+
" ```",
|
|
915
|
+
"2. 扫描每个 Controller 的路由定义:",
|
|
916
|
+
" ```bash",
|
|
917
|
+
" grep -rn '@Controller\\|@Get\\|@Post\\|@Put\\|@Delete\\|@Patch' src/ --include='*.controller.ts'",
|
|
918
|
+
" ```",
|
|
919
|
+
"3. 找出所有 DTO 文件:",
|
|
920
|
+
" ```bash",
|
|
921
|
+
" find src/ -path '*/dto/*' -name '*.ts' -type f",
|
|
922
|
+
" ```",
|
|
923
|
+
"4. 提取每个 Controller 的 `@Controller(<prefix>)` 前缀",
|
|
924
|
+
"5. 统计每个 Controller 包含的端点数量和 HTTP 方法分布",
|
|
925
|
+
"6. 记录每个端点使用的 DTO 类型(`@Body()`、`@Query()`、`@Param()` 参数类型)",
|
|
926
|
+
"7. 检查是否已经配置了 `SwaggerModule`:",
|
|
927
|
+
" ```bash",
|
|
928
|
+
" grep -rn 'SwaggerModule\\|DocumentBuilder' src/ --include='*.ts'",
|
|
929
|
+
" ```",
|
|
930
|
+
"",
|
|
931
|
+
"**产出格式**:",
|
|
932
|
+
"```",
|
|
933
|
+
"controllers: [",
|
|
934
|
+
" { file: '...', class_name: '...', prefix: '...', endpoints: N }",
|
|
935
|
+
"]",
|
|
936
|
+
"total_endpoints: <N>",
|
|
937
|
+
"dto_files: [{ path, class_name }]",
|
|
938
|
+
"swagger_module_exists: true | false",
|
|
939
|
+
"swagger_module_path: <path> | null",
|
|
940
|
+
"```",
|
|
941
|
+
].join("\n"),
|
|
942
|
+
suggestedTools: ["Bash", "Read"],
|
|
943
|
+
output: "Controller 和 DTO 完整清单、现有 Swagger 配置状态",
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
id: "audit-decorators",
|
|
947
|
+
title: "审计缺失的 Swagger 装饰器",
|
|
948
|
+
instruction: [
|
|
949
|
+
"## 审计缺失的 @ApiTags / @ApiOperation / @ApiResponse 装饰器",
|
|
950
|
+
"",
|
|
951
|
+
"检查每个 Controller 和 DTO 是否具备完整的 Swagger 文档装饰器。",
|
|
952
|
+
"",
|
|
953
|
+
"**Controller 必备装饰器清单**:",
|
|
954
|
+
"- `@ApiTags('<tag-name>')` — 在 Controller 类级别",
|
|
955
|
+
"- `@ApiBearerAuth()` — 如果 Controller 受 JwtAuthGuard 保护",
|
|
956
|
+
"- `@ApiOperation({ summary: '...' })` — 每个 handler 方法",
|
|
957
|
+
"- `@ApiResponse({ status: 200, description: '...', type: XxxDto })` — 每个 handler",
|
|
958
|
+
"- `@ApiParam({ name: 'id', description: '...' })` — 有 URL 参数的 handler",
|
|
959
|
+
"- `@ApiQuery({ name: 'page', description: '...' })` — 有查询参数的 handler(如 DTO 未标注则需手动补)",
|
|
960
|
+
"",
|
|
961
|
+
"**DTO 必备装饰器清单**:",
|
|
962
|
+
"- 每个字段必须有 `@ApiProperty()` 或 `@ApiPropertyOptional()`",
|
|
963
|
+
"- `@ApiProperty({ example: '...', description: '...' })` 提供示例",
|
|
964
|
+
"- 枚举类型使用 `@ApiProperty({ enum: XxxEnum })`",
|
|
965
|
+
"- 嵌套 DTO 使用 `@ApiProperty({ type: () => NestedDto, isArray: true })`",
|
|
966
|
+
"",
|
|
967
|
+
"**执行动作**:",
|
|
968
|
+
"1. 对每个 Controller 文件,逐方法检查:",
|
|
969
|
+
" ```bash",
|
|
970
|
+
" grep -n '@ApiOperation\\|@Get\\|@Post' src/modules/users/users.controller.ts",
|
|
971
|
+
" ```",
|
|
972
|
+
"2. 对比 `@Get/@Post/...` 数量和 `@ApiOperation` 数量,差集即为缺失项",
|
|
973
|
+
"3. 对每个 DTO 文件,检查每个 class 字段是否有 `@ApiProperty`:",
|
|
974
|
+
" ```bash",
|
|
975
|
+
" grep -c '@ApiProperty\\|@ApiPropertyOptional' src/modules/users/dto/create-user.dto.ts",
|
|
976
|
+
" grep -c '^\\s*[a-zA-Z]' src/modules/users/dto/create-user.dto.ts",
|
|
977
|
+
" ```",
|
|
978
|
+
"4. 生成缺失项清单,按优先级排序:",
|
|
979
|
+
" - P0:无 `@ApiTags` 的 Controller",
|
|
980
|
+
" - P1:无 `@ApiOperation` 的 handler",
|
|
981
|
+
" - P2:无 `@ApiProperty` 的 DTO 字段",
|
|
982
|
+
" - P3:缺少 `@ApiResponse` 的 handler",
|
|
983
|
+
"",
|
|
984
|
+
"**产出格式**:",
|
|
985
|
+
"```",
|
|
986
|
+
"audit_summary: {",
|
|
987
|
+
" controllers_without_tags: [<file>],",
|
|
988
|
+
" handlers_without_operation: [{ controller, method, route }],",
|
|
989
|
+
" dto_fields_without_api_property: [{ file, field }],",
|
|
990
|
+
" handlers_without_response: [{ controller, method }],",
|
|
991
|
+
"}",
|
|
992
|
+
"priority_order: [P0, P1, P2, P3]",
|
|
993
|
+
"```",
|
|
994
|
+
].join("\n"),
|
|
995
|
+
suggestedTools: ["Bash", "Read"],
|
|
996
|
+
output: "缺失 Swagger 装饰器的详细清单、优先级排序",
|
|
997
|
+
depends: ["scan-controllers"],
|
|
998
|
+
},
|
|
999
|
+
{
|
|
1000
|
+
id: "supplement-docs",
|
|
1001
|
+
title: "补充缺失的 Swagger 装饰器",
|
|
1002
|
+
instruction: [
|
|
1003
|
+
"## 补充缺失的 Swagger 装饰器",
|
|
1004
|
+
"",
|
|
1005
|
+
"按优先级顺序,为 Controller 和 DTO 补充缺失的 Swagger 装饰器。",
|
|
1006
|
+
"",
|
|
1007
|
+
"**执行动作**:",
|
|
1008
|
+
"1. 优先补充 `@ApiTags`(Controller 类级别):",
|
|
1009
|
+
" ```typescript",
|
|
1010
|
+
" import { ApiTags, ApiBearerAuth, ApiOperation, ApiResponse, ApiProperty } from '@nestjs/swagger';",
|
|
1011
|
+
"",
|
|
1012
|
+
" @ApiTags('users')",
|
|
1013
|
+
" @ApiBearerAuth()",
|
|
1014
|
+
" @Controller('users')",
|
|
1015
|
+
" export class UsersController {}",
|
|
1016
|
+
" ```",
|
|
1017
|
+
"2. 为每个 handler 补充 `@ApiOperation` + `@ApiResponse`:",
|
|
1018
|
+
" ```typescript",
|
|
1019
|
+
" @Get(':id')",
|
|
1020
|
+
" @ApiOperation({ summary: '根据 ID 查询用户' })",
|
|
1021
|
+
" @ApiResponse({ status: 200, description: '查询成功', type: UserResponseDto })",
|
|
1022
|
+
" @ApiResponse({ status: 404, description: '用户不存在' })",
|
|
1023
|
+
" findOne(@Param('id') id: string) {",
|
|
1024
|
+
" return this.usersService.findOne(id);",
|
|
1025
|
+
" }",
|
|
1026
|
+
" ```",
|
|
1027
|
+
"3. 为 DTO 每个字段补充 `@ApiProperty`:",
|
|
1028
|
+
" ```typescript",
|
|
1029
|
+
" export class CreateUserDto {",
|
|
1030
|
+
" @ApiProperty({ description: '用户邮箱', example: 'alice@example.com' })",
|
|
1031
|
+
" @IsEmail()",
|
|
1032
|
+
" email: string;",
|
|
1033
|
+
"",
|
|
1034
|
+
" @ApiPropertyOptional({ description: '用户头像 URL' })",
|
|
1035
|
+
" @IsOptional()",
|
|
1036
|
+
" @IsUrl()",
|
|
1037
|
+
" avatar?: string;",
|
|
1038
|
+
" }",
|
|
1039
|
+
" ```",
|
|
1040
|
+
"4. 枚举类型字段:",
|
|
1041
|
+
" ```typescript",
|
|
1042
|
+
" @ApiProperty({ enum: UserRole, example: UserRole.ADMIN })",
|
|
1043
|
+
" role: UserRole;",
|
|
1044
|
+
" ```",
|
|
1045
|
+
"5. 嵌套 DTO 字段:",
|
|
1046
|
+
" ```typescript",
|
|
1047
|
+
" @ApiProperty({ type: () => AddressDto })",
|
|
1048
|
+
" address: AddressDto;",
|
|
1049
|
+
"",
|
|
1050
|
+
" @ApiProperty({ type: () => TagDto, isArray: true })",
|
|
1051
|
+
" tags: TagDto[];",
|
|
1052
|
+
" ```",
|
|
1053
|
+
"6. 为全局 ExceptionFilter 注册的响应添加 `@ApiResponse` 文档",
|
|
1054
|
+
"7. 使用 `@ApiBody({ type: CreateXxxDto })` 显式声明请求体(如需要)",
|
|
1055
|
+
"",
|
|
1056
|
+
"**产出格式**:",
|
|
1057
|
+
"```",
|
|
1058
|
+
"files_modified: [{ path, decorators_added: N }]",
|
|
1059
|
+
"total_decorators_added: <N>",
|
|
1060
|
+
"coverage_before: <percent>",
|
|
1061
|
+
"coverage_after: <percent>",
|
|
1062
|
+
"```",
|
|
1063
|
+
].join("\n"),
|
|
1064
|
+
suggestedTools: ["Read", "Edit", "Bash"],
|
|
1065
|
+
output: "修改后的文件清单、新增装饰器数量、覆盖率提升",
|
|
1066
|
+
depends: ["audit-decorators"],
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
id: "generate-spec",
|
|
1070
|
+
title: "生成 OpenAPI 规范文件",
|
|
1071
|
+
instruction: [
|
|
1072
|
+
"## 生成 OpenAPI 规范文件",
|
|
1073
|
+
"",
|
|
1074
|
+
"使用 `@nestjs/swagger` 的 `SwaggerModule` 生成 OpenAPI 规范 JSON/YAML 文件。",
|
|
1075
|
+
"",
|
|
1076
|
+
"**执行动作**:",
|
|
1077
|
+
"1. 在 `src/main.ts` 中配置 SwaggerModule(如果尚未配置):",
|
|
1078
|
+
" ```typescript",
|
|
1079
|
+
" import { NestFactory } from '@nestjs/core';",
|
|
1080
|
+
" import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';",
|
|
1081
|
+
" import { AppModule } from './app.module';",
|
|
1082
|
+
"",
|
|
1083
|
+
" async function bootstrap() {",
|
|
1084
|
+
" const app = await NestFactory.create(AppModule);",
|
|
1085
|
+
"",
|
|
1086
|
+
" const config = new DocumentBuilder()",
|
|
1087
|
+
" .setTitle('API Documentation')",
|
|
1088
|
+
" .setDescription('The API description')",
|
|
1089
|
+
" .setVersion('1.0')",
|
|
1090
|
+
" .addBearerAuth()",
|
|
1091
|
+
" .addTag('users', '用户管理')",
|
|
1092
|
+
" .addTag('posts', '文章管理')",
|
|
1093
|
+
" .build();",
|
|
1094
|
+
" const document = SwaggerModule.createDocument(app, config);",
|
|
1095
|
+
" SwaggerModule.setup('api', app, document);",
|
|
1096
|
+
"",
|
|
1097
|
+
" await app.listen(3000);",
|
|
1098
|
+
" }",
|
|
1099
|
+
" bootstrap();",
|
|
1100
|
+
" ```",
|
|
1101
|
+
"2. 生成静态 OpenAPI 规范文件(用于离线查看或外部工具集成):",
|
|
1102
|
+
" ```bash",
|
|
1103
|
+
" # 启动服务后通过 /api-json 获取 JSON 规范文件",
|
|
1104
|
+
" curl http://localhost:3000/api-json > openapi.json",
|
|
1105
|
+
" # 或使用 @nestjs/swagger 的脚本直接生成",
|
|
1106
|
+
" ```",
|
|
1107
|
+
"3. 在 `DocumentBuilder` 中配置:",
|
|
1108
|
+
" - `setTitle()` — 项目名称",
|
|
1109
|
+
" - `setDescription()` — 项目描述",
|
|
1110
|
+
" - `setVersion()` — API 版本",
|
|
1111
|
+
" - `addBearerAuth()` — JWT 认证(如有)",
|
|
1112
|
+
" - `addTag()` — 分组标签描述",
|
|
1113
|
+
" - `addServer()` — 环境地址(dev/staging/prod)",
|
|
1114
|
+
"4. 如果项目使用多个 Module,使用 `SwaggerModule.createDocument(app, config, { include: [XxxModule] })` 分模块生成",
|
|
1115
|
+
"5. 保存生成的 `openapi.json` 到项目根目录或 `docs/` 目录",
|
|
1116
|
+
"",
|
|
1117
|
+
"**产出格式**:",
|
|
1118
|
+
"```",
|
|
1119
|
+
"spec_file: <path>/openapi.json",
|
|
1120
|
+
"spec_version: '3.0.0'",
|
|
1121
|
+
"paths_count: <N>",
|
|
1122
|
+
"tags: [<tag names>]",
|
|
1123
|
+
"schemas_count: <N>",
|
|
1124
|
+
"security_schemes: ['bearer'] | []",
|
|
1125
|
+
"```",
|
|
1126
|
+
].join("\n"),
|
|
1127
|
+
suggestedTools: ["Bash", "Read", "Write"],
|
|
1128
|
+
output: "生成的 OpenAPI 规范文件、路径/标签/Schema 统计",
|
|
1129
|
+
depends: ["supplement-docs"],
|
|
1130
|
+
},
|
|
1131
|
+
{
|
|
1132
|
+
id: "validate-spec",
|
|
1133
|
+
title: "验证生成的 Swagger 文档完整性",
|
|
1134
|
+
instruction: [
|
|
1135
|
+
"## 验证生成的 Swagger 文档完整性",
|
|
1136
|
+
"",
|
|
1137
|
+
"校验生成的 OpenAPI 规范文件是否符合 OpenAPI 3.0 规范,并与实际端点一一对应。",
|
|
1138
|
+
"",
|
|
1139
|
+
"**执行动作**:",
|
|
1140
|
+
"1. 使用 OpenAPI 校验工具验证文件格式:",
|
|
1141
|
+
" ```bash",
|
|
1142
|
+
" # 使用 swagger-cli 校验",
|
|
1143
|
+
" npx @apidevtools/swagger-cli validate openapi.json",
|
|
1144
|
+
" # 或使用 swagger-parser",
|
|
1145
|
+
" npx swagger-cli bundle openapi.json --validate",
|
|
1146
|
+
" ```",
|
|
1147
|
+
"2. 核对所有 Controller 路由都已出现在 `paths` 中:",
|
|
1148
|
+
" ```bash",
|
|
1149
|
+
" # 从源码提取所有路由",
|
|
1150
|
+
" grep -rn '@Get\\|@Post\\|@Put\\|@Delete\\|@Patch' src/ --include='*.controller.ts'",
|
|
1151
|
+
" # 与 openapi.json 的 paths 字段对比",
|
|
1152
|
+
" jq '.paths | keys' openapi.json",
|
|
1153
|
+
" ```",
|
|
1154
|
+
"3. 验证所有 DTO 都已出现在 `components.schemas` 中:",
|
|
1155
|
+
" ```bash",
|
|
1156
|
+
" jq '.components.schemas | keys' openapi.json",
|
|
1157
|
+
" ```",
|
|
1158
|
+
"4. 验证 `@ApiResponse` 都生成了正确的状态码描述",
|
|
1159
|
+
"5. 验证认证配置:如果有 `@ApiBearerAuth()`,`components.securitySchemes` 必须包含对应定义",
|
|
1160
|
+
"6. 验证请求体 Schema:每个 `@Body()` 参数都映射到正确的 `$ref`",
|
|
1161
|
+
"7. 验证 URL 参数:每个 `:param` 都在 `parameters` 中声明",
|
|
1162
|
+
"8. 使用 Swagger UI 本地预览验证渲染效果:",
|
|
1163
|
+
" ```bash",
|
|
1164
|
+
" npx @redocly/cli preview-docs openapi.json",
|
|
1165
|
+
" ```",
|
|
1166
|
+
"",
|
|
1167
|
+
"**产出格式**:",
|
|
1168
|
+
"```",
|
|
1169
|
+
"validation_result: passed | failed",
|
|
1170
|
+
"spec_valid_openapi: true | false",
|
|
1171
|
+
"paths_match_source: true | false",
|
|
1172
|
+
"missing_paths: [<path>],",
|
|
1173
|
+
"missing_schemas: [<schema name>],",
|
|
1174
|
+
"security_schemes_ok: true | false",
|
|
1175
|
+
"issues: [{ location, description }]",
|
|
1176
|
+
"```",
|
|
1177
|
+
].join("\n"),
|
|
1178
|
+
suggestedTools: ["Bash", "Read"],
|
|
1179
|
+
output: "规范校验结果、缺失项、问题清单",
|
|
1180
|
+
depends: ["generate-spec"],
|
|
1181
|
+
},
|
|
472
1182
|
],
|
|
473
1183
|
triggers: [{ type: "cli", command: "generate:swagger" }],
|
|
474
1184
|
},
|
|
475
1185
|
{
|
|
476
1186
|
name: "microservice-setup",
|
|
477
1187
|
description: "微服务端点配置:消息模式 → 传输层 → 客户端代理",
|
|
478
|
-
version: "
|
|
1188
|
+
version: "2.0.0",
|
|
479
1189
|
steps: [
|
|
480
|
-
{
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
1190
|
+
{
|
|
1191
|
+
id: "define-patterns",
|
|
1192
|
+
title: "定义 MessagePattern 和 EventPattern",
|
|
1193
|
+
instruction: [
|
|
1194
|
+
"## 定义 MessagePattern 和 EventPattern",
|
|
1195
|
+
"",
|
|
1196
|
+
"根据业务需求规划微服务之间的消息模式:请求-响应 vs 事件广播。",
|
|
1197
|
+
"",
|
|
1198
|
+
"**NestJS 消息模式**:",
|
|
1199
|
+
"- `@MessagePattern(<cmd>)` — 请求-响应,客户端等待结果",
|
|
1200
|
+
"- `@EventPattern(<event>)` — 事件广播,无响应",
|
|
1201
|
+
"",
|
|
1202
|
+
"**执行动作**:",
|
|
1203
|
+
"1. 梳理业务场景,识别哪些操作需要跨服务调用:",
|
|
1204
|
+
" - 用户下单后需要扣减库存 → `@MessagePattern({ cmd: 'deductInventory' })`",
|
|
1205
|
+
" - 订单创建后通知多个服务 → `@EventPattern('order.created')`",
|
|
1206
|
+
"2. 命名规范:",
|
|
1207
|
+
" - MessagePattern: `{ cmd: '<verb><Noun>' }` 或字符串 `'<service>.<action>'`",
|
|
1208
|
+
" - EventPattern: `'<domain>.<event>'`(如 `user.created`、`payment.completed`)",
|
|
1209
|
+
"3. 创建微服务 Controller 文件 `src/modules/<name>/<name>.controller.ts`:",
|
|
1210
|
+
" ```typescript",
|
|
1211
|
+
" import { Controller } from '@nestjs/common';",
|
|
1212
|
+
" import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';",
|
|
1213
|
+
"",
|
|
1214
|
+
" @Controller()",
|
|
1215
|
+
" export class InventoryController {",
|
|
1216
|
+
" @MessagePattern({ cmd: 'deductInventory' })",
|
|
1217
|
+
" async deductInventory(@Payload() data: { productId: string; quantity: number }) {",
|
|
1218
|
+
" // 业务逻辑:扣减库存",
|
|
1219
|
+
" return { success: true, remaining: 100 };",
|
|
1220
|
+
" }",
|
|
1221
|
+
"",
|
|
1222
|
+
" @EventPattern('order.created')",
|
|
1223
|
+
" async handleOrderCreated(@Payload() data: { orderId: string; items: any[] }) {",
|
|
1224
|
+
" // 事件处理:无需返回值",
|
|
1225
|
+
" }",
|
|
1226
|
+
" }",
|
|
1227
|
+
" ```",
|
|
1228
|
+
"4. 定义每个 Pattern 的 Payload 类型(DTO):",
|
|
1229
|
+
" ```typescript",
|
|
1230
|
+
" export interface DeductInventoryPayload {",
|
|
1231
|
+
" productId: string;",
|
|
1232
|
+
" quantity: number;",
|
|
1233
|
+
" }",
|
|
1234
|
+
" ```",
|
|
1235
|
+
"5. 检查现有微服务是否有类似的 Pattern,避免命名冲突",
|
|
1236
|
+
"",
|
|
1237
|
+
"**产出格式**:",
|
|
1238
|
+
"```",
|
|
1239
|
+
"message_patterns: [{ cmd: '...', payload_type: '...', description: '...' }]",
|
|
1240
|
+
"event_patterns: [{ event: '...', payload_type: '...', consumers: [...] }]",
|
|
1241
|
+
"naming_convention: { cmd: '{ cmd: ... }' | 'service.action', event: 'domain.event' }",
|
|
1242
|
+
"```",
|
|
1243
|
+
].join("\n"),
|
|
1244
|
+
suggestedTools: ["Read", "Write", "Bash"],
|
|
1245
|
+
output: "MessagePattern 和 EventPattern 清单、Payload DTO",
|
|
1246
|
+
},
|
|
1247
|
+
{
|
|
1248
|
+
id: "setup-transport",
|
|
1249
|
+
title: "配置传输层",
|
|
1250
|
+
instruction: [
|
|
1251
|
+
"## 配置传输层(TCP / Redis / Kafka 选型建议)",
|
|
1252
|
+
"",
|
|
1253
|
+
"根据业务场景选择合适的微服务传输层,并配置 `main.ts` 和 Module。",
|
|
1254
|
+
"",
|
|
1255
|
+
"**传输层对比**:",
|
|
1256
|
+
"| 传输层 | 适用场景 | 特点 |",
|
|
1257
|
+
"|--------|----------|------|",
|
|
1258
|
+
"| TCP | 简单内部通信、开发测试 | 默认、无外部依赖 |",
|
|
1259
|
+
"| Redis | 中等规模、消息队列 | 需要 Redis 服务 |",
|
|
1260
|
+
"| Kafka | 高吞吐事件流、日志、事件溯源 | 需要 Kafka 集群 |",
|
|
1261
|
+
"| RabbitMQ | 复杂消息路由、优先级队列 | 需要 RabbitMQ 服务 |",
|
|
1262
|
+
"| NATS | 轻量级、云原生 | 资源占用小 |",
|
|
1263
|
+
"| gRPC | 强类型、跨语言、高性能 | 需要 .proto 文件 |",
|
|
1264
|
+
"",
|
|
1265
|
+
"**执行动作**:",
|
|
1266
|
+
"1. 根据业务场景选定传输层(通常一个微服务只选一种)",
|
|
1267
|
+
"2. 安装对应的传输层包:",
|
|
1268
|
+
" ```bash",
|
|
1269
|
+
" # TCP 无需额外安装(NestJS 内置)",
|
|
1270
|
+
" npm install --save @nestjs/microservices",
|
|
1271
|
+
"",
|
|
1272
|
+
" # Redis",
|
|
1273
|
+
" npm install --save ioredis",
|
|
1274
|
+
"",
|
|
1275
|
+
" # Kafka",
|
|
1276
|
+
" npm install --save kafkajs",
|
|
1277
|
+
"",
|
|
1278
|
+
" # RabbitMQ",
|
|
1279
|
+
" npm install --save amqplib",
|
|
1280
|
+
"",
|
|
1281
|
+
" # NATS",
|
|
1282
|
+
" npm install --save nats",
|
|
1283
|
+
"",
|
|
1284
|
+
" # gRPC",
|
|
1285
|
+
" npm install --save @grpc/grpc-js @grpc/proto-loader",
|
|
1286
|
+
" ```",
|
|
1287
|
+
"3. 在 `main.ts` 中配置微服务启动:",
|
|
1288
|
+
" ```typescript",
|
|
1289
|
+
" import { NestFactory } from '@nestjs/core';",
|
|
1290
|
+
" import { MicroserviceOptions, Transport } from '@nestjs/microservices';",
|
|
1291
|
+
" import { AppModule } from './app.module';",
|
|
1292
|
+
"",
|
|
1293
|
+
" async function bootstrap() {",
|
|
1294
|
+
" const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {",
|
|
1295
|
+
" transport: Transport.TCP, // 或 Transport.REDIS / Transport.KAFKA 等",
|
|
1296
|
+
" options: {",
|
|
1297
|
+
" host: '127.0.0.1',",
|
|
1298
|
+
" port: 3001,",
|
|
1299
|
+
" // Redis: { host: 'localhost', port: 6379 }",
|
|
1300
|
+
" // Kafka: { client: { brokers: ['localhost:9092'] }, consumer: { groupId: 'my-group' } }",
|
|
1301
|
+
" },",
|
|
1302
|
+
" });",
|
|
1303
|
+
" await app.listen();",
|
|
1304
|
+
" }",
|
|
1305
|
+
" bootstrap();",
|
|
1306
|
+
" ```",
|
|
1307
|
+
"4. 如果微服务同时需要提供 HTTP 接口,使用混合应用:",
|
|
1308
|
+
" ```typescript",
|
|
1309
|
+
" const app = await NestFactory.create(AppModule);",
|
|
1310
|
+
" const microservice = app.connectMicroservice<MicroserviceOptions>({",
|
|
1311
|
+
" transport: Transport.TCP,",
|
|
1312
|
+
" options: { port: 3001 },",
|
|
1313
|
+
" });",
|
|
1314
|
+
" await app.startAllMicroservices();",
|
|
1315
|
+
" await app.listen(3000);",
|
|
1316
|
+
" ```",
|
|
1317
|
+
"5. 在 `.env` 文件中配置传输层参数(便于不同环境切换)",
|
|
1318
|
+
"",
|
|
1319
|
+
"**产出格式**:",
|
|
1320
|
+
"```",
|
|
1321
|
+
"transport_selected: TCP | REDIS | KAFKA | RABBITMQ | NATS | GRPC",
|
|
1322
|
+
"transport_options: { host, port, ... }",
|
|
1323
|
+
"packages_installed: ['@nestjs/microservices', ...]",
|
|
1324
|
+
"hybrid_mode: true | false (是否同时提供 HTTP)",
|
|
1325
|
+
"env_vars: { MICROSERVICE_PORT, KAFKA_BROKERS, ... }",
|
|
1326
|
+
"```",
|
|
1327
|
+
].join("\n"),
|
|
1328
|
+
suggestedTools: ["Bash", "Read", "Write"],
|
|
1329
|
+
output: "选定的传输层、配置代码、安装依赖清单",
|
|
1330
|
+
depends: ["define-patterns"],
|
|
1331
|
+
},
|
|
1332
|
+
{
|
|
1333
|
+
id: "create-client",
|
|
1334
|
+
title: "创建 ClientProxy 代理服务",
|
|
1335
|
+
instruction: [
|
|
1336
|
+
"## 创建 ClientProxy 代理服务",
|
|
1337
|
+
"",
|
|
1338
|
+
"在调用方服务中创建代理 Service 用于跨服务调用。",
|
|
1339
|
+
"",
|
|
1340
|
+
"**执行动作**:",
|
|
1341
|
+
"1. 在调用方 Module 中注册 ClientProxy:",
|
|
1342
|
+
" ```typescript",
|
|
1343
|
+
" import { Module } from '@nestjs/common';",
|
|
1344
|
+
" import { ClientsModule, Transport } from '@nestjs/microservices';",
|
|
1345
|
+
" import { OrderModule } from './order.module';",
|
|
1346
|
+
"",
|
|
1347
|
+
" @Module({",
|
|
1348
|
+
" imports: [",
|
|
1349
|
+
" ClientsModule.register([",
|
|
1350
|
+
" {",
|
|
1351
|
+
" name: 'INVENTORY_SERVICE',",
|
|
1352
|
+
" transport: Transport.TCP,",
|
|
1353
|
+
" options: { host: 'localhost', port: 3001 },",
|
|
1354
|
+
" },",
|
|
1355
|
+
" ]),",
|
|
1356
|
+
" OrderModule,",
|
|
1357
|
+
" ],",
|
|
1358
|
+
" })",
|
|
1359
|
+
" export class AppModule {}",
|
|
1360
|
+
" ```",
|
|
1361
|
+
"2. 在 Service 中注入 `@Inject('INVENTORY_SERVICE')` 的 ClientProxy:",
|
|
1362
|
+
" ```typescript",
|
|
1363
|
+
" import { Injectable, Inject, OnModuleInit } from '@nestjs/common';",
|
|
1364
|
+
" import { ClientProxy } from '@nestjs/microservices';",
|
|
1365
|
+
"",
|
|
1366
|
+
" @Injectable()",
|
|
1367
|
+
" export class OrderService implements OnModuleInit {",
|
|
1368
|
+
" constructor(",
|
|
1369
|
+
" @Inject('INVENTORY_SERVICE') private readonly inventoryClient: ClientProxy,",
|
|
1370
|
+
" ) {}",
|
|
1371
|
+
"",
|
|
1372
|
+
" async onModuleInit() {",
|
|
1373
|
+
" await this.inventoryClient.connect();",
|
|
1374
|
+
" }",
|
|
1375
|
+
"",
|
|
1376
|
+
" async createOrder(dto: CreateOrderDto) {",
|
|
1377
|
+
" // 请求-响应",
|
|
1378
|
+
" const result = await this.inventoryClient",
|
|
1379
|
+
" .send<{ success: boolean; remaining: number }>(",
|
|
1380
|
+
" { cmd: 'deductInventory' },",
|
|
1381
|
+
" { productId: dto.productId, quantity: dto.quantity },",
|
|
1382
|
+
" )",
|
|
1383
|
+
" .toPromise();",
|
|
1384
|
+
"",
|
|
1385
|
+
" if (!result.success) {",
|
|
1386
|
+
" throw new BadRequestException('库存不足');",
|
|
1387
|
+
" }",
|
|
1388
|
+
"",
|
|
1389
|
+
" // 创建订单后广播事件",
|
|
1390
|
+
" this.inventoryClient.emit('order.created', {",
|
|
1391
|
+
" orderId: order.id,",
|
|
1392
|
+
" items: dto.items,",
|
|
1393
|
+
" });",
|
|
1394
|
+
"",
|
|
1395
|
+
" return order;",
|
|
1396
|
+
" }",
|
|
1397
|
+
" }",
|
|
1398
|
+
" ```",
|
|
1399
|
+
"3. 使用 `client.send<T>(pattern, data).toPromise()` 做请求-响应调用",
|
|
1400
|
+
"4. 使用 `client.emit(event, data)` 做事件广播调用",
|
|
1401
|
+
"5. 封装成独立的方法,避免业务代码里直接调用 `client.send`",
|
|
1402
|
+
"6. 为 ClientProxy 注入添加完善的 TypeScript 类型(避免 `any`)",
|
|
1403
|
+
"7. 考虑将每个远程调用封装成独立的方法便于测试:",
|
|
1404
|
+
" ```typescript",
|
|
1405
|
+
" async deductInventory(productId: string, quantity: number) {",
|
|
1406
|
+
" return this.inventoryClient.send<{ success: boolean }>(",
|
|
1407
|
+
" { cmd: 'deductInventory' },",
|
|
1408
|
+
" { productId, quantity },",
|
|
1409
|
+
" ).toPromise();",
|
|
1410
|
+
" }",
|
|
1411
|
+
" ```",
|
|
1412
|
+
"",
|
|
1413
|
+
"**产出格式**:",
|
|
1414
|
+
"```",
|
|
1415
|
+
"client_module_setup: { module_file, client_name, transport, options }",
|
|
1416
|
+
"proxy_services: [{ service_name, client_token, methods: [{ name, pattern, return_type }] }]",
|
|
1417
|
+
"lifecycle_hook: OnModuleInit (for connect())",
|
|
1418
|
+
"```",
|
|
1419
|
+
].join("\n"),
|
|
1420
|
+
suggestedTools: ["Read", "Write", "Edit"],
|
|
1421
|
+
output: "ClientProxy 注册代码、代理服务方法清单",
|
|
1422
|
+
depends: ["setup-transport"],
|
|
1423
|
+
},
|
|
1424
|
+
{
|
|
1425
|
+
id: "add-error-handling",
|
|
1426
|
+
title: "添加异常过滤器和重试逻辑",
|
|
1427
|
+
instruction: [
|
|
1428
|
+
"## 添加异常过滤器和重试逻辑",
|
|
1429
|
+
"",
|
|
1430
|
+
"跨服务调用必须考虑网络抖动、服务不可用、超时等异常场景。",
|
|
1431
|
+
"",
|
|
1432
|
+
"**执行动作**:",
|
|
1433
|
+
"1. 为 `ClientProxy` 调用添加超时控制:",
|
|
1434
|
+
" ```typescript",
|
|
1435
|
+
" import { timeout, catchError, throwError, retry } from 'rxjs';",
|
|
1436
|
+
"",
|
|
1437
|
+
" async deductInventory(productId: string, quantity: number) {",
|
|
1438
|
+
" return this.inventoryClient",
|
|
1439
|
+
" .send<{ success: boolean }>({ cmd: 'deductInventory' }, { productId, quantity })",
|
|
1440
|
+
" .pipe(",
|
|
1441
|
+
" timeout(5000), // 5 秒超时",
|
|
1442
|
+
" retry({",
|
|
1443
|
+
" count: 3,",
|
|
1444
|
+
" delay: 1000,",
|
|
1445
|
+
" resetOnSuccess: true,",
|
|
1446
|
+
" }),",
|
|
1447
|
+
" catchError((err) => {",
|
|
1448
|
+
" if (err.name === 'TimeoutError') {",
|
|
1449
|
+
" return throwError(() => new ServiceUnavailableException('库存服务超时'));",
|
|
1450
|
+
" }",
|
|
1451
|
+
" return throwError(() => err);",
|
|
1452
|
+
" }),",
|
|
1453
|
+
" )",
|
|
1454
|
+
" .toPromise();",
|
|
1455
|
+
" }",
|
|
1456
|
+
" ```",
|
|
1457
|
+
"2. 创建 RPC 异常过滤器处理跨服务异常:",
|
|
1458
|
+
" ```typescript",
|
|
1459
|
+
" import { Catch, RpcExceptionFilter, ArgumentsHost } from '@nestjs/common';",
|
|
1460
|
+
" import { RpcException } from '@nestjs/microservices';",
|
|
1461
|
+
" import { Observable, throwError } from 'rxjs';",
|
|
1462
|
+
"",
|
|
1463
|
+
" @Catch(RpcException)",
|
|
1464
|
+
" export class RpcExceptionFilter implements RpcExceptionFilter {",
|
|
1465
|
+
" catch(exception: RpcException, host: ArgumentsHost): Observable<any> {",
|
|
1466
|
+
" const error = exception.getError();",
|
|
1467
|
+
" // 记录日志、转换异常格式",
|
|
1468
|
+
" return throwError(() => ({",
|
|
1469
|
+
" status: 'error',",
|
|
1470
|
+
" message: typeof error === 'string' ? error : error.message,",
|
|
1471
|
+
" });",
|
|
1472
|
+
" }",
|
|
1473
|
+
" }",
|
|
1474
|
+
" ```",
|
|
1475
|
+
"3. 使用 `@UseFilters(new RpcExceptionFilter())` 应用到 Controller",
|
|
1476
|
+
"4. 关键操作添加幂等性保障(使用唯一请求 ID):",
|
|
1477
|
+
" ```typescript",
|
|
1478
|
+
" async createOrder(dto: CreateOrderDto, requestId: string) {",
|
|
1479
|
+
" // 先查询是否已处理",
|
|
1480
|
+
" const existing = await this.prisma.order.findUnique({ where: { requestId } });",
|
|
1481
|
+
" if (existing) return existing;",
|
|
1482
|
+
" // 否则创建新订单",
|
|
1483
|
+
" return this.prisma.order.create({ data: { ...dto, requestId } });",
|
|
1484
|
+
" }",
|
|
1485
|
+
" ```",
|
|
1486
|
+
"5. 配置 Circuit Breaker(断路器)防止雪崩效应(可选,使用 `opossum` 等库)",
|
|
1487
|
+
"6. 对于事件广播,考虑使用 Dead Letter Queue 处理失败事件",
|
|
1488
|
+
"7. 日志记录每次跨服务调用:调用方、被调用方、耗时、成功/失败",
|
|
1489
|
+
"",
|
|
1490
|
+
"**产出格式**:",
|
|
1491
|
+
"```",
|
|
1492
|
+
"retry_config: { count: N, delay_ms: N }",
|
|
1493
|
+
"timeout_ms: N",
|
|
1494
|
+
"rpc_exception_filter: <file path>",
|
|
1495
|
+
"idempotency_strategy: 'request-id' | 'none'",
|
|
1496
|
+
"error_handling_patterns: [{ pattern, file }]```",
|
|
1497
|
+
].join("\n"),
|
|
1498
|
+
suggestedTools: ["Read", "Write", "Edit"],
|
|
1499
|
+
output: "重试/超时/幂等性配置、RPC 异常过滤器文件",
|
|
1500
|
+
depends: ["create-client"],
|
|
1501
|
+
},
|
|
1502
|
+
{
|
|
1503
|
+
id: "integration-test",
|
|
1504
|
+
title: "集成测试验证消息收发",
|
|
1505
|
+
instruction: [
|
|
1506
|
+
"## 集成测试验证消息收发",
|
|
1507
|
+
"",
|
|
1508
|
+
"编写集成测试验证微服务的消息发送和接收正确工作。",
|
|
1509
|
+
"",
|
|
1510
|
+
"**执行动作**:",
|
|
1511
|
+
"1. 创建集成测试文件 `test/<service-name>.e2e-spec.ts`:",
|
|
1512
|
+
" ```typescript",
|
|
1513
|
+
" import { Test, TestingModule } from '@nestjs/testing';",
|
|
1514
|
+
" import { INestApplication, HttpStatus } from '@nestjs/common';",
|
|
1515
|
+
" import { ClientProxy, ClientsModule, Transport } from '@nestjs/microservices';",
|
|
1516
|
+
" import { AppModule } from '../src/app.module';",
|
|
1517
|
+
"",
|
|
1518
|
+
" describe('InventoryMicroservice (e2e)', () => {",
|
|
1519
|
+
" let app: INestApplication;",
|
|
1520
|
+
" let client: ClientProxy;",
|
|
1521
|
+
"",
|
|
1522
|
+
" beforeAll(async () => {",
|
|
1523
|
+
" const moduleFixture: TestingModule = await Test.createTestingModule({",
|
|
1524
|
+
" imports: [AppModule],",
|
|
1525
|
+
" }).compile();",
|
|
1526
|
+
"",
|
|
1527
|
+
" app = moduleFixture.createNestApplication();",
|
|
1528
|
+
" app.connectMicroservice({",
|
|
1529
|
+
" transport: Transport.TCP,",
|
|
1530
|
+
" options: { host: 'localhost', port: 3001 },",
|
|
1531
|
+
" });",
|
|
1532
|
+
" await app.startAllMicroservices();",
|
|
1533
|
+
" await app.init();",
|
|
1534
|
+
"",
|
|
1535
|
+
" client = moduleFixture.get<ClientProxy>('INVENTORY_SERVICE');",
|
|
1536
|
+
" await client.connect();",
|
|
1537
|
+
" });",
|
|
1538
|
+
"",
|
|
1539
|
+
" afterAll(async () => {",
|
|
1540
|
+
" await client.close();",
|
|
1541
|
+
" await app.close();",
|
|
1542
|
+
" });",
|
|
1543
|
+
"",
|
|
1544
|
+
" it('should respond to deductInventory message', async () => {",
|
|
1545
|
+
" const result = await client",
|
|
1546
|
+
" .send({ cmd: 'deductInventory' }, { productId: 'p1', quantity: 1 })",
|
|
1547
|
+
" .toPromise();",
|
|
1548
|
+
" expect(result).toEqual({ success: true, remaining: expect.any(Number) });",
|
|
1549
|
+
" });",
|
|
1550
|
+
"",
|
|
1551
|
+
" it('should handle order.created event', async () => {",
|
|
1552
|
+
" client.emit('order.created', { orderId: 'o1', items: [] });",
|
|
1553
|
+
" // 等待事件被处理",
|
|
1554
|
+
" await new Promise((resolve) => setTimeout(resolve, 100));",
|
|
1555
|
+
" // 验证数据库或状态变化",
|
|
1556
|
+
" });",
|
|
1557
|
+
" });",
|
|
1558
|
+
" ```",
|
|
1559
|
+
"2. 测试 `@MessagePattern` 的请求-响应调用",
|
|
1560
|
+
"3. 测试 `@EventPattern` 的事件广播",
|
|
1561
|
+
"4. 测试异常场景:",
|
|
1562
|
+
" - 服务不可用时的超时处理",
|
|
1563
|
+
" - 重试逻辑是否生效",
|
|
1564
|
+
" - RpcExceptionFilter 是否正确转换异常",
|
|
1565
|
+
"5. 测试边界条件:",
|
|
1566
|
+
" - 并发消息处理",
|
|
1567
|
+
" - 大消息体",
|
|
1568
|
+
" - 网络断开恢复后的行为",
|
|
1569
|
+
"6. 使用 Jest 的 `--runInBand` 串行运行避免端口冲突:",
|
|
1570
|
+
" ```bash",
|
|
1571
|
+
" npx jest --config ./test/jest-e2e.json --runInBand",
|
|
1572
|
+
" ```",
|
|
1573
|
+
"7. 如果项目使用 Docker Compose,测试前启动依赖服务:",
|
|
1574
|
+
" ```bash",
|
|
1575
|
+
" docker-compose -f docker-compose.test.yml up -d",
|
|
1576
|
+
" npm run test:e2e",
|
|
1577
|
+
" docker-compose -f docker-compose.test.yml down",
|
|
1578
|
+
" ```",
|
|
1579
|
+
"",
|
|
1580
|
+
"**产出格式**:",
|
|
1581
|
+
"```",
|
|
1582
|
+
"test_file: <path>",
|
|
1583
|
+
"test_cases: [{ describe, it, status: 'written' }]",
|
|
1584
|
+
"patterns_tested: [{ cmd | event, result }]",
|
|
1585
|
+
"test_result: passed | failed",
|
|
1586
|
+
"services_required: [TCP, Redis, ...]",
|
|
1587
|
+
"```",
|
|
1588
|
+
].join("\n"),
|
|
1589
|
+
suggestedTools: ["Bash", "Read", "Write"],
|
|
1590
|
+
output: "集成测试文件、测试用例清单、测试结果",
|
|
1591
|
+
depends: ["add-error-handling"],
|
|
1592
|
+
},
|
|
485
1593
|
],
|
|
486
1594
|
triggers: [{ type: "cli", command: "setup:microservice" }],
|
|
487
1595
|
},
|