From e8a8ad389c36e687e907764fe9b9f7fe6d3c541f Mon Sep 17 00:00:00 2001 From: ZHENG XIAOYI Date: Sat, 7 Feb 2026 03:43:59 +0800 Subject: [PATCH] chore(deps): add zod and nestjs-zod for schema validation --- package.json | 8 ++-- pnpm-lock.yaml | 54 ++++++++++++++++------- src/auth/auth.controller.ts | 51 +++++++++++++++------ src/auth/dto/index.ts | 41 ++++++----------- src/main.ts | 38 ++++++++-------- src/serials/dto/index.ts | 56 +++++++----------------- src/serials/serials.controller.ts | 73 ++++++++++++++++++++----------- 7 files changed, 175 insertions(+), 146 deletions(-) diff --git a/package.json b/package.json index 899d568..ffe2237 100644 --- a/package.json +++ b/package.json @@ -32,16 +32,15 @@ "@prisma/client": "^7.3.0", "bcryptjs": "^3.0.3", "better-sqlite3": "^12.6.2", - "class-transformer": "^0.5.1", - "class-validator": "^0.14.3", "dotenv": "^17.2.3", - "jsonwebtoken": "^9.0.2", + "nestjs-zod": "^5.1.1", "passport": "^0.7.0", "passport-jwt": "^4.0.1", "prisma": "^7.3.0", "qrcode": "^1.5.4", "reflect-metadata": "^0.2.2", - "rxjs": "^7.8.2" + "rxjs": "^7.8.2", + "zod": "^4.3.6" }, "devDependencies": { "@nestjs/cli": "^11.0.16", @@ -49,7 +48,6 @@ "@nestjs/testing": "^11.1.13", "@types/express": "^5.0.6", "@types/jest": "^30.0.0", - "@types/jsonwebtoken": "^9.0.10", "@types/node": "^25.2.1", "@types/passport-jwt": "^4.0.1", "@types/qrcode": "^1.5.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 94d2c29..034890f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -41,18 +41,12 @@ importers: better-sqlite3: specifier: ^12.6.2 version: 12.6.2 - class-transformer: - specifier: ^0.5.1 - version: 0.5.1 - class-validator: - specifier: ^0.14.3 - version: 0.14.3 dotenv: specifier: ^17.2.3 version: 17.2.4 - jsonwebtoken: - specifier: ^9.0.2 - version: 9.0.3 + nestjs-zod: + specifier: ^5.1.1 + version: 5.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6) passport: specifier: ^0.7.0 version: 0.7.0 @@ -71,6 +65,9 @@ importers: rxjs: specifier: ^7.8.2 version: 7.8.2 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@nestjs/cli': specifier: ^11.0.16 @@ -87,9 +84,6 @@ importers: '@types/jest': specifier: ^30.0.0 version: 30.0.0 - '@types/jsonwebtoken': - specifier: ^9.0.10 - version: 9.0.10 '@types/node': specifier: ^25.2.1 version: 25.2.1 @@ -2601,6 +2595,17 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + nestjs-zod@5.1.1: + resolution: {integrity: sha512-pXa9Jrdip7iedKvGxJTvvCFVRCoIcNENPCsHjpCefPH3PcFejRgkZkUcr3TYITRyxnUk7Zy5OsLpirZGLYBfBQ==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 + rxjs: ^7.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@nestjs/swagger': + optional: true + node-abi@3.87.0: resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==} engines: {node: '>=10'} @@ -3439,6 +3444,9 @@ packages: zeptomatch@2.1.0: resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@angular-devkit/core@19.2.17(chokidar@4.0.3)': @@ -4587,7 +4595,8 @@ snapshots: '@types/methods': 1.1.4 '@types/superagent': 8.1.9 - '@types/validator@13.15.10': {} + '@types/validator@13.15.10': + optional: true '@types/yargs-parser@21.0.3': {} @@ -5027,13 +5036,15 @@ snapshots: cjs-module-lexer@2.2.0: {} - class-transformer@0.5.1: {} + class-transformer@0.5.1: + optional: true class-validator@0.14.3: dependencies: '@types/validator': 13.15.10 libphonenumber-js: 1.12.36 validator: 13.15.26 + optional: true cli-cursor@3.1.0: dependencies: @@ -6055,7 +6066,8 @@ snapshots: leven@3.1.0: {} - libphonenumber-js@1.12.36: {} + libphonenumber-js@1.12.36: + optional: true lilconfig@2.1.0: {} @@ -6221,6 +6233,13 @@ snapshots: neo-async@2.6.2: {} + nestjs-zod@5.1.1(@nestjs/common@11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(rxjs@7.8.2)(zod@4.3.6): + dependencies: + '@nestjs/common': 11.1.13(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + deepmerge: 4.3.1 + rxjs: 7.8.2 + zod: 4.3.6 + node-abi@3.87.0: dependencies: semver: 7.7.4 @@ -6964,7 +6983,8 @@ snapshots: optionalDependencies: typescript: 5.9.3 - validator@13.15.26: {} + validator@13.15.26: + optional: true vary@1.1.2: {} @@ -7099,3 +7119,5 @@ snapshots: dependencies: grammex: 3.1.12 graphmatch: 1.1.0 + + zod@4.3.6: {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index a4fc867..544898c 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -1,29 +1,45 @@ -import { Controller, Post, Get, Put, Body, UseGuards, Request, HttpCode, HttpStatus } from '@nestjs/common'; -import { AuthService } from './auth.service'; -import { AuthGuard } from './auth.guard'; -import { LoginDto, ChangePasswordDto, UpdateProfileDto } from './dto'; +import { + Controller, + Post, + Get, + Put, + Body, + UseGuards, + Request, + HttpCode, + HttpStatus, +} from "@nestjs/common"; +import { AuthService } from "./auth.service"; +import { AuthGuard } from "./auth.guard"; +import { LoginDto, ChangePasswordDto, UpdateProfileDto } from "./dto"; -@Controller('auth') +@Controller("auth") export class AuthController { constructor(private authService: AuthService) {} - @Post('login') + @Post("login") @HttpCode(HttpStatus.OK) - async login(@Body() loginDto: LoginDto) { - const user = await this.authService.validateUser(loginDto.username, loginDto.password); + async login(@Body(LoginDto) loginDto: any) { + const user = await this.authService.validateUser( + loginDto.username, + loginDto.password, + ); return this.authService.login(user); } - @Get('profile') + @Get("profile") @UseGuards(AuthGuard) async getProfile(@Request() req) { return this.authService.getProfile(req.user.id); } - @Post('change-password') + @Post("change-password") @UseGuards(AuthGuard) @HttpCode(HttpStatus.OK) - async changePassword(@Request() req, @Body() changePasswordDto: ChangePasswordDto) { + async changePassword( + @Request() req, + @Body(ChangePasswordDto) changePasswordDto: any, + ) { return this.authService.changePassword( req.user.id, changePasswordDto.currentPassword, @@ -31,10 +47,17 @@ export class AuthController { ); } - @Put('profile') + @Put("profile") @UseGuards(AuthGuard) @HttpCode(HttpStatus.OK) - async updateProfile(@Request() req, @Body() updateProfileDto: UpdateProfileDto) { - return this.authService.updateProfile(req.user.id, updateProfileDto.name, updateProfileDto.email); + async updateProfile( + @Request() req, + @Body(UpdateProfileDto) updateProfileDto: any, + ) { + return this.authService.updateProfile( + req.user.id, + updateProfileDto.name, + updateProfileDto.email, + ); } } diff --git a/src/auth/dto/index.ts b/src/auth/dto/index.ts index 6681dc3..d0db76a 100644 --- a/src/auth/dto/index.ts +++ b/src/auth/dto/index.ts @@ -1,31 +1,16 @@ -import { IsString, IsNotEmpty, MinLength, IsEmail, IsOptional } from 'class-validator'; +import { z } from "zod"; -export class LoginDto { - @IsString() - @IsNotEmpty({ message: '用户名不能为空' }) - username: string; +export const LoginDto = z.object({ + username: z.string().min(1, "用户名不能为空"), + password: z.string().min(1, "密码不能为空"), +}); - @IsString() - @IsNotEmpty({ message: '密码不能为空' }) - password: string; -} +export const ChangePasswordDto = z.object({ + currentPassword: z.string().min(1, "当前密码不能为空"), + newPassword: z.string().min(6, "新密码长度至少为6位"), +}); -export class ChangePasswordDto { - @IsString() - @IsNotEmpty({ message: '当前密码不能为空' }) - currentPassword: string; - - @IsString() - @MinLength(6, { message: '新密码长度至少为6位' }) - newPassword: string; -} - -export class UpdateProfileDto { - @IsString() - @IsNotEmpty({ message: '姓名不能为空' }) - name: string; - - @IsOptional() - @IsEmail({}, { message: '邮箱格式不正确' }) - email?: string; -} +export const UpdateProfileDto = z.object({ + name: z.string().min(1, "姓名不能为空"), + email: z.string().email("邮箱格式不正确").optional(), +}); diff --git a/src/main.ts b/src/main.ts index 4d1c06f..ef6bea3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,34 +1,34 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; -import { ValidationPipe } from '@nestjs/common'; -import { ConfigService } from '@nestjs/config'; -import { join } from 'path'; -import { NestExpressApplication } from '@nestjs/platform-express'; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; +import { ConfigService } from "@nestjs/config"; +import { join } from "path"; +import { NestExpressApplication } from "@nestjs/platform-express"; +import { ZodValidationPipe } from "nestjs-zod"; async function bootstrap() { const app = await NestFactory.create(AppModule); const configService = app.get(ConfigService); - + app.enableCors(); - app.useGlobalPipes(new ValidationPipe({ transform: true })); - app.setGlobalPrefix('api'); - + app.useGlobalPipes(new ZodValidationPipe()); + app.setGlobalPrefix("api"); + // 静态文件服务 - const frontendPath = join(__dirname, '..', 'frontend'); - const distPath = join(frontendPath, 'dist'); - const publicPath = join(frontendPath, 'public'); - - if (process.env.NODE_ENV === 'production') { + const frontendPath = join(__dirname, "..", "frontend"); + const distPath = join(frontendPath, "dist"); + const publicPath = join(frontendPath, "public"); + + if (process.env.NODE_ENV === "production") { app.useStaticAssets(distPath); } else { app.useStaticAssets(publicPath); } - - const port = configService.get('PORT', 3000); + + const port = configService.get("PORT", 3000); await app.listen(port); - + console.log(`服务器运行在 http://localhost:${port}`); console.log(`API文档: http://localhost:${port}/api/health`); - console.log(`环境: ${process.env.NODE_ENV || 'development'}`); + console.log(`环境: ${process.env.NODE_ENV || "development"}`); } bootstrap(); diff --git a/src/serials/dto/index.ts b/src/serials/dto/index.ts index e56ab81..a76c2f5 100644 --- a/src/serials/dto/index.ts +++ b/src/serials/dto/index.ts @@ -1,46 +1,24 @@ -import { IsString, IsNotEmpty, IsNumber, IsOptional, IsBoolean, Min, Max } from 'class-validator'; +import { z } from "zod"; -export class GenerateSerialDto { - @IsString() - @IsNotEmpty({ message: '企业名称不能为空' }) - companyName: string; +export const GenerateSerialDto = z.object({ + companyName: z.string().min(1, "企业名称不能为空"), + quantity: z.number().min(1).max(100).optional(), + validDays: z.number().optional(), +}); - @IsOptional() - @IsNumber() - @Min(1) - @Max(100) - quantity?: number; +export const GenerateWithPrefixDto = GenerateSerialDto.extend({ + serialPrefix: z.string().min(1, "自定义前缀不能为空"), +}); - @IsOptional() - @IsNumber() - validDays?: number; -} +export const QRCodeDto = z.object({ + baseUrl: z.string().optional(), +}); -export class GenerateWithPrefixDto extends GenerateSerialDto { - @IsString() - @IsNotEmpty({ message: '自定义前缀不能为空' }) - serialPrefix: string; -} - -export class QRCodeDto { - @IsOptional() - @IsString() - baseUrl?: string; -} - -export class UpdateSerialDto { - @IsOptional() - @IsString() - companyName?: string; - - @IsOptional() - @IsString() - validUntil?: string; - - @IsOptional() - @IsBoolean() - isActive?: boolean; -} +export const UpdateSerialDto = z.object({ + companyName: z.string().optional(), + validUntil: z.string().optional(), + isActive: z.boolean().optional(), +}); export interface Serial { id: number; diff --git a/src/serials/serials.controller.ts b/src/serials/serials.controller.ts index fcf8441..a0292c5 100644 --- a/src/serials/serials.controller.ts +++ b/src/serials/serials.controller.ts @@ -1,18 +1,38 @@ -import { Controller, Get, Post, Patch, Body, Param, Query, UseGuards, Req, HttpCode, HttpStatus } from '@nestjs/common'; -import { Request } from 'express'; -import { SerialsService } from './serials.service'; -import { AuthGuard } from '../auth/auth.guard'; -import { AdminGuard } from '../auth/admin.guard'; -import { GenerateSerialDto, GenerateWithPrefixDto, QRCodeDto, UpdateSerialDto } from './dto'; +import { + Controller, + Get, + Post, + Patch, + Body, + Param, + Query, + UseGuards, + Req, + HttpCode, + HttpStatus, +} from "@nestjs/common"; +import { Request } from "express"; +import { SerialsService } from "./serials.service"; +import { AuthGuard } from "../auth/auth.guard"; +import { AdminGuard } from "../auth/admin.guard"; +import { + GenerateSerialDto, + GenerateWithPrefixDto, + QRCodeDto, + UpdateSerialDto, +} from "./dto"; -@Controller('serials') +@Controller("serials") export class SerialsController { constructor(private readonly serialsService: SerialsService) {} - @Post('generate') + @Post("generate") @UseGuards(AuthGuard, AdminGuard) @HttpCode(HttpStatus.OK) - async generate(@Body() generateDto: GenerateSerialDto, @Req() req: Request) { + async generate( + @Body(GenerateSerialDto) generateDto: any, + @Req() req: Request, + ) { const serials = await this.serialsService.generate( generateDto.companyName, generateDto.quantity || 1, @@ -25,10 +45,13 @@ export class SerialsController { }; } - @Post('generate-with-prefix') + @Post("generate-with-prefix") @UseGuards(AuthGuard, AdminGuard) @HttpCode(HttpStatus.OK) - async generateWithPrefix(@Body() generateDto: GenerateWithPrefixDto, @Req() req: Request) { + async generateWithPrefix( + @Body(GenerateWithPrefixDto) generateDto: any, + @Req() req: Request, + ) { const serials = await this.serialsService.generate( generateDto.companyName, generateDto.quantity || 1, @@ -42,44 +65,44 @@ export class SerialsController { }; } - @Post(':serialNumber/qrcode') + @Post(":serialNumber/qrcode") @UseGuards(AuthGuard) @HttpCode(HttpStatus.OK) async generateQRCode( - @Param('serialNumber') serialNumber: string, - @Body() qrCodeDto: QRCodeDto, + @Param("serialNumber") serialNumber: string, + @Body(QRCodeDto) qrCodeDto: any, @Req() req: Request, ) { return this.serialsService.generateQRCode( serialNumber, qrCodeDto.baseUrl, - req.get('host'), + req.get("host"), req.protocol, ); } - @Get(':serialNumber/query') + @Get(":serialNumber/query") @HttpCode(HttpStatus.OK) - async query(@Param('serialNumber') serialNumber: string) { + async query(@Param("serialNumber") serialNumber: string) { return this.serialsService.query(serialNumber); } @Get() @UseGuards(AuthGuard) async findAll( - @Query('page') page: string = '1', - @Query('limit') limit: string = '20', - @Query('search') search: string = '', + @Query("page") page: string = "1", + @Query("limit") limit: string = "20", + @Query("search") search: string = "", ) { return this.serialsService.findAll(parseInt(page), parseInt(limit), search); } - @Patch(':serialNumber') + @Patch(":serialNumber") @UseGuards(AuthGuard, AdminGuard) @HttpCode(HttpStatus.OK) async update( - @Param('serialNumber') serialNumber: string, - @Body() updateDto: UpdateSerialDto, + @Param("serialNumber") serialNumber: string, + @Body(UpdateSerialDto) updateDto: any, ) { return this.serialsService.update(serialNumber, { companyName: updateDto.companyName, @@ -88,10 +111,10 @@ export class SerialsController { }); } - @Post(':serialNumber/revoke') + @Post(":serialNumber/revoke") @UseGuards(AuthGuard, AdminGuard) @HttpCode(HttpStatus.OK) - async revoke(@Param('serialNumber') serialNumber: string) { + async revoke(@Param("serialNumber") serialNumber: string) { return this.serialsService.revoke(serialNumber); } }