From ef90d0550bed595d225e12a5816da2a9e01035fa Mon Sep 17 00:00:00 2001 From: xidongdong-153 Date: Wed, 22 Nov 2023 18:50:56 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AE=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E5=92=8C=E6=95=B0=E6=8D=AE=E5=BA=8F=E5=88=97=E5=8C=96=E5=A4=84?= =?UTF-8?q?=E7=90=86=20=20-=20=E5=A2=9E=E5=8A=A0=E4=BA=86DTO=E5=92=8C?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=20=20-=20=E6=9C=8D=E5=8A=A1=E7=B1=BB?= =?UTF-8?q?=E5=92=8C=E6=8E=A7=E5=88=B6=E5=99=A8=E5=8A=A0=E5=85=A5=E4=BA=86?= =?UTF-8?q?dto=20=20-=20=E5=A2=9E=E5=8A=A0=E5=BA=8F=E5=88=97=E5=8C=96?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=99=A8=EF=BC=8C=E5=AF=B9=E6=95=B0=E7=BB=84?= =?UTF-8?q?=E5=92=8C=E5=88=86=E9=A1=B5=E7=89=B9=E6=AE=8A=E5=A4=84=E7=90=86?= =?UTF-8?q?=20=20-=20=E5=AE=9E=E4=BD=93=E4=B8=AD=E5=88=86=E9=85=8D?= =?UTF-8?q?=E4=BA=86=E5=BA=8F=E5=88=97=E5=8C=96=E8=A3=85=E9=A5=B0=E5=99=A8?= =?UTF-8?q?=20=20-=20=E6=8E=A7=E5=88=B6=E5=99=A8=E5=BC=95=E5=85=A5?= =?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- back/database4.db | Bin 20480 -> 20480 bytes .../content/controllers/post.controller.ts | 24 +++- src/modules/content/dtos/create-post.dto.ts | 24 ---- src/modules/content/dtos/index.ts | 3 +- src/modules/content/dtos/post.dto.ts | 105 ++++++++++++++++++ src/modules/content/dtos/update-post.dto.ts | 14 --- src/modules/content/entities/post.entity.ts | 12 ++ src/modules/content/services/post.service.ts | 5 +- src/modules/core/providers/app.interceptor.ts | 0 src/modules/core/providers/index.ts | 40 +++++++ 10 files changed, 182 insertions(+), 45 deletions(-) delete mode 100644 src/modules/content/dtos/create-post.dto.ts create mode 100644 src/modules/content/dtos/post.dto.ts delete mode 100644 src/modules/content/dtos/update-post.dto.ts create mode 100644 src/modules/core/providers/app.interceptor.ts create mode 100644 src/modules/core/providers/index.ts diff --git a/back/database4.db b/back/database4.db index c25f1b87e49aa99bbadf1b634263df7cc46e6399..6f8106d1c2e47479f731ba9f586db14457b80ff1 100644 GIT binary patch delta 267 zcmZozz}T>Wae_3X&_o$$Mxl)f3;CtFpD=K7yg5XnHw9JrRka`S(xaWn3^W*CK;!h=$fUZnkOY1C8i`9 zm`$E8Z6Ir;U|?xwY;0v}VPs%rtZQhfYlIM*oF-i-z`(%3(#Xug9qKCyGMsbr2WdC9 zr!)2g(PRf1MP4Kw#%7!IWTr7oaPg%x@a^Pp<6qCu&3}VGmhTbY1io~Blg)|>-h7iM Q=&`60<36s<6Z8rc0nmv@v;Y7A delta 94 zcmV-k0HObYpaFoO0gxL350M;00S~cYp${1V000RKW&jUp4(SeR4eJeT4DAeV3*-xC zv4KnrlV%%!v*bG*1d<>G76}fJ01mkih7Yd~3lG>2T@K<7kPkGoAwYHxvyd)uAq3JJ A?f?J) diff --git a/src/modules/content/controllers/post.controller.ts b/src/modules/content/controllers/post.controller.ts index 579a54c..465616a 100644 --- a/src/modules/content/controllers/post.controller.ts +++ b/src/modules/content/controllers/post.controller.ts @@ -8,34 +8,50 @@ import { Patch, Post, Query, + SerializeOptions, + UseInterceptors, ValidationPipe, } from '@nestjs/common'; +import { CreatePostDto, UpdatePostDto } from '@/modules/content/dtos'; import { PostService } from '@/modules/content/services'; +import { AppIntercepter } from '@/modules/core/providers'; import { PaginateOptions } from '@/modules/database/types'; /** * 文章控制器 * 负责处理与文章相关的请求,如获取文章列表、创建新文章等。 */ +@UseInterceptors(AppIntercepter) @Controller('posts') export class PostController { constructor(private postService: PostService) {} @Get() + @SerializeOptions({ groups: ['post-list'] }) async list( - @Query() + @Query( + new ValidationPipe({ + transform: true, + whitelist: true, + forbidNonWhitelisted: true, + forbidUnknownValues: true, + validationError: { target: false }, + }), + ) options: PaginateOptions, ) { return this.postService.paginate(options); } @Get(':id') + @SerializeOptions({ groups: ['post-detail'] }) async detail(@Param('id', new ParseUUIDPipe()) id: string) { return this.postService.detail(id); } @Post() + @SerializeOptions({ groups: ['post-detail'] }) async store( @Body( new ValidationPipe({ @@ -46,12 +62,13 @@ export class PostController { groups: ['create'], }), ) - data: RecordAny, + data: CreatePostDto, ) { return this.postService.create(data); } @Patch() + @SerializeOptions({ groups: ['post-detail'] }) async update( @Body( new ValidationPipe({ @@ -62,12 +79,13 @@ export class PostController { groups: ['update'], }), ) - data: RecordAny, + data: UpdatePostDto, ) { return this.postService.update(data); } @Delete(':id') + @SerializeOptions({ groups: ['post-detail'] }) async delete(@Param('id', new ParseUUIDPipe()) id: string) { return this.postService.delete(id); } diff --git a/src/modules/content/dtos/create-post.dto.ts b/src/modules/content/dtos/create-post.dto.ts deleted file mode 100644 index 17aab68..0000000 --- a/src/modules/content/dtos/create-post.dto.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { IsNotEmpty, IsOptional, MaxLength } from 'class-validator'; - -@Injectable() -export class CreatePostDto { - @MaxLength(255, { - always: true, - message: '帖子标题长度最大为$constraint1', - }) - @IsNotEmpty({ groups: ['create'], message: '帖子标题必需填写' }) - @IsOptional({ groups: ['update'] }) - title: string; - - @IsNotEmpty({ groups: ['create'], message: '帖子内容必需填写' }) - @IsOptional({ groups: ['updatae'] }) - body: string; - - @MaxLength(500, { - always: true, - message: '帖子描述长度最大为$constraint1', - }) - @IsOptional({ always: true }) - summary: string; -} diff --git a/src/modules/content/dtos/index.ts b/src/modules/content/dtos/index.ts index e67ec36..f9efaaa 100644 --- a/src/modules/content/dtos/index.ts +++ b/src/modules/content/dtos/index.ts @@ -1,2 +1 @@ -export * from './create-post.dto'; -export * from './update-post.dto'; +export * from './post.dto'; diff --git a/src/modules/content/dtos/post.dto.ts b/src/modules/content/dtos/post.dto.ts new file mode 100644 index 0000000..372c38b --- /dev/null +++ b/src/modules/content/dtos/post.dto.ts @@ -0,0 +1,105 @@ +import { PartialType } from '@nestjs/swagger'; +import { Transform } from 'class-transformer'; + +import { + IsBoolean, + IsDateString, + IsDefined, + IsEnum, + IsNotEmpty, + IsNumber, + IsOptional, + IsUUID, + MaxLength, + Min, + ValidateIf, +} from 'class-validator'; + +import { isNil, toNumber } from 'lodash'; + +import { PostOrderType } from '@/modules/content/constants'; +import { toBoolean } from '@/modules/core/helpers'; +import { PaginateOptions } from '@/modules/database/types'; + +/** + * 文章分页查询验证 + */ +export class QueryPostDto implements PaginateOptions { + @Transform(({ value }) => toBoolean(value)) + @IsBoolean() + @IsOptional() + isPublished?: boolean; + + @IsEnum(PostOrderType, { + message: `排序规则必需是${Object.values(PostOrderType).join(',')}中的一项`, + }) + orderBy?: PostOrderType; + + @Transform(({ value }) => toNumber(value)) + @Min(1, { message: '当前页必需大于1' }) + @IsNumber() + @IsOptional() + page = 1; + + @Transform(({ value }) => toNumber(value)) + @Min(1, { message: '每页显示数据必须大于1' }) + @IsNumber() + @IsOptional() + limit: 10; +} + +/** + * 文章创建验证 + */ +export class CreatePostDto { + @MaxLength(255, { + always: true, + message: '文章标题长度最大为$constraint1', + }) + @IsNotEmpty({ + groups: ['create'], + message: '文章标题为必填', + }) + @IsOptional({ groups: ['update'] }) + title: string; + + @IsNotEmpty({ groups: ['create'], message: '文章内容为必填' }) + @IsOptional({ groups: ['update'] }) + body: string; + + @MaxLength(500, { + always: true, + message: '文章摘要长度最大为$constraint1', + }) + @IsOptional({ always: true }) + summary?: string; + + @IsDateString({ strict: true }, { always: true }) + @IsOptional({ always: true }) + @ValidateIf((value) => !isNil(value.publishedAt)) + @Transform(({ value }) => (value === 'null' ? null : value)) + publishedAt?: Date; + + @MaxLength(20, { + each: true, + always: true, + message: '每个关键字长度最大为$constraint1', + }) + @IsOptional({ always: true }) + keywords?: string[]; + + @Transform(({ value }) => toNumber(value)) + @Min(0, { always: true, message: '排序值必须大于0' }) + @IsNumber(undefined, { always: true }) + @IsOptional({ always: true }) + customOrder = 0; +} + +/** + * 文章更新验证 + */ +export class UpdatePostDto extends PartialType(CreatePostDto) { + @IsUUID(undefined, { groups: ['update'], message: '文章ID格式错误' }) + @IsDefined({ groups: ['update'], message: '文章ID必须指定' }) + id: string; +} diff --git a/src/modules/content/dtos/update-post.dto.ts b/src/modules/content/dtos/update-post.dto.ts deleted file mode 100644 index 5fc4d77..0000000 --- a/src/modules/content/dtos/update-post.dto.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from '@nestjs/common'; - -import { PartialType } from '@nestjs/swagger'; - -import { IsDefined, IsNumber } from 'class-validator'; - -import { CreatePostDto } from './create-post.dto'; - -@Injectable() -export class UpdatePostDto extends PartialType(CreatePostDto) { - @IsNumber(undefined, { groups: ['update'], message: '帖子ID格式错误' }) - @IsDefined({ groups: ['update'], message: '帖子ID必需指定' }) - id: number; -} diff --git a/src/modules/content/entities/post.entity.ts b/src/modules/content/entities/post.entity.ts index c93c0ac..550f265 100644 --- a/src/modules/content/entities/post.entity.ts +++ b/src/modules/content/entities/post.entity.ts @@ -1,3 +1,4 @@ +import { Exclude, Expose } from 'class-transformer'; import { BaseEntity, Column, @@ -9,35 +10,46 @@ import { import { PostBodyType } from '@/modules/content/constants'; +@Exclude() @Entity('content_posts') export class PostEntity extends BaseEntity { + @Expose() @PrimaryColumn({ type: 'varchar', generated: 'uuid', length: '36' }) id: string; + @Expose() @Column({ comment: '文章标题' }) title: string; + @Expose({ groups: ['post-detail'] }) @Column({ comment: '文章内容', type: 'text' }) body: string; + @Expose() @Column({ comment: '文章摘要', nullable: true }) summary: string; + @Expose() @Column({ comment: '关键字', type: 'simple-array', nullable: true }) keywords?: string[]; + @Expose() @Column({ comment: '文章类型', type: 'varchar', default: PostBodyType.MD }) type: PostBodyType; + @Expose() @Column({ comment: '发布时间', type: 'varchar', nullable: true }) publishedAt?: Date | null; + @Expose() @CreateDateColumn({ comment: '创建时间' }) createdAt: Date; + @Expose() @UpdateDateColumn({ comment: '更新时间' }) updatedAt: Date; + @Expose() @Column({ comment: '文章自定义排序', default: 0 }) customOrder: number; } diff --git a/src/modules/content/services/post.service.ts b/src/modules/content/services/post.service.ts index 3205bd5..30f8851 100644 --- a/src/modules/content/services/post.service.ts +++ b/src/modules/content/services/post.service.ts @@ -5,6 +5,7 @@ import { isFunction, isNil, omit } from 'lodash'; import { EntityNotFoundError, IsNull, Not, SelectQueryBuilder } from 'typeorm'; import { PostOrderType } from '@/modules/content/constants'; +import { CreatePostDto, UpdatePostDto } from '@/modules/content/dtos'; import { PostEntity } from '@/modules/content/entities'; import { PostRepository } from '@/modules/content/repositories'; @@ -43,7 +44,7 @@ export class PostService { * 创建文章 * @param data */ - async create(data: Record) { + async create(data: CreatePostDto) { const item = await this.repository.save(data); return this.detail(item.id); @@ -53,7 +54,7 @@ export class PostService { * 更新文章 * @param data */ - async update(data: Record) { + async update(data: UpdatePostDto) { await this.repository.update(data.id, omit(data, ['id'])); return this.detail(data.id); } diff --git a/src/modules/core/providers/app.interceptor.ts b/src/modules/core/providers/app.interceptor.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/modules/core/providers/index.ts b/src/modules/core/providers/index.ts new file mode 100644 index 0000000..201f443 --- /dev/null +++ b/src/modules/core/providers/index.ts @@ -0,0 +1,40 @@ +import { + ClassSerializerContextOptions, + ClassSerializerInterceptor, + PlainLiteralObject, + StreamableFile, +} from '@nestjs/common'; +import { PinoLoggerOptions } from 'fastify/types/logger'; +import { isArray, isNil, isObject } from 'lodash'; + +export class AppIntercepter extends ClassSerializerInterceptor { + serialize( + response: PlainLiteralObject | PlainLiteralObject[], + options: ClassSerializerContextOptions, + ): PlainLiteralObject | PlainLiteralObject[] { + if ((!isObject(response) && !isArray(response)) || response instanceof StreamableFile) { + return response; + } + + // 数组处理 - 如果是数组则对数组每一项元素序列化 + if (isArray(response)) { + return (response as PlainLiteralObject[]).map((item) => + !isObject(item) ? item : this.transformToPlain(item, options), + ); + } + + // 分页处理 - 对items中的每一项进行序列化 + if ('meta' in response && 'items' in response) { + const items = !isNil(response.items) && isArray(response.items) ? response.items : []; + + return { + ...response, + items: (items as PinoLoggerOptions[]).map((item) => + !isObject(item) ? item : this.transformToPlain(item, options), + ), + }; + } + + return this.transformToPlain(response, options); + } +}