Quellcode durchsuchen

feat: 完成咨询相关接口

IlhamTahir vor 1 Jahr
Ursprung
Commit
7eb95c85b9

+ 6 - 3
src/article/article.module.ts

@@ -3,10 +3,13 @@ import { CategoryController } from './controller/category.controller';
 import { CategoryService } from './service/category.service';
 import { TypeOrmModule } from '@nestjs/typeorm';
 import { Category } from './entity/category.entity';
+import { ArticleService } from './service/article.service';
+import { Article } from './entity/article.entity';
+import { ArticleController } from './controller/article.controller';
 
 @Module({
-  controllers: [CategoryController],
-  imports: [TypeOrmModule.forFeature([Category])],
-  providers: [CategoryService],
+  controllers: [CategoryController, ArticleController],
+  imports: [TypeOrmModule.forFeature([Category, Article])],
+  providers: [CategoryService, ArticleService],
 })
 export class ArticleModule {}

+ 109 - 2
src/article/controller/article.controller.ts

@@ -1,4 +1,111 @@
-import { Controller } from '@nestjs/common';
+import {
+  Body,
+  Controller,
+  Delete,
+  Get,
+  Param,
+  Post,
+  Put,
+  Query,
+} from '@nestjs/common';
+import { CreateArticleRequest } from '../dto/create-article.request';
+import { ArticleService } from '../service/article.service';
+import { ApiBearerAuth, ApiOkResponse, getSchemaPath } from '@nestjs/swagger';
+import { ArticleVo } from '../vo/article.vo';
+import { ArticleMapper } from '../mapper/article.mapper';
+import { SearchArticleFilter } from '../dto/search-article.filter';
+import { PageResult } from '../../core/vo/page-result';
+import { PageResultMapper } from '../../core/mapper/page-result.mapper';
+import { Article } from '../entity/article.entity';
 
 @Controller('articles')
-export class ArticleController {}
+export class ArticleController {
+  constructor(private readonly articleService: ArticleService) {}
+  @Post()
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    type: ArticleVo,
+  })
+  async create(
+    @Body() createArticle: CreateArticleRequest,
+  ): Promise<ArticleVo> {
+    return ArticleMapper.toVo(await this.articleService.create(createArticle));
+  }
+
+  @Get()
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    description: '咨询分页列表',
+    schema: {
+      allOf: [
+        { $ref: getSchemaPath(PageResult) }, // 引用 PageResult 模型
+        {
+          properties: {
+            data: {
+              type: 'array',
+              items: { $ref: getSchemaPath(ArticleVo) }, // 泛型内容具体化
+            },
+          },
+        },
+      ],
+    },
+  })
+  async search(
+    @Query() searchArticleFilter: SearchArticleFilter,
+  ): Promise<PageResult<ArticleVo>> {
+    const [data, total] = await this.articleService.search(searchArticleFilter);
+    return PageResultMapper.toPageResult<ArticleVo>(
+      ArticleMapper.toVos(data),
+      searchArticleFilter.getPage(),
+      searchArticleFilter.getSize(),
+      total,
+    );
+  }
+
+  @Get(':id')
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    type: ArticleVo,
+  })
+  async get(@Param('id') id: string) {
+    return ArticleMapper.toVo(await this.articleService.get(id));
+  }
+
+  @Put(':id')
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    type: ArticleVo,
+  })
+  async update(
+    @Param('id') id: string,
+    @Body() updateArticle: CreateArticleRequest,
+  ) {
+    return ArticleMapper.toVo(
+      await this.articleService.update(id, updateArticle),
+    );
+  }
+
+  @Put(':id/publish')
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    type: ArticleVo,
+  })
+  async publish(@Param('id') id: string) {
+    return ArticleMapper.toVo(await this.articleService.publish(id));
+  }
+
+  @Put(':id/close')
+  @ApiBearerAuth()
+  @ApiOkResponse({
+    type: ArticleVo,
+  })
+  async close(@Param('id') id: string) {
+    return ArticleMapper.toVo(await this.articleService.close(id));
+  }
+
+  @Delete(':id')
+  @ApiBearerAuth()
+  async delete(@Param('id') id: string) {
+    await this.articleService.delete(id);
+  }
+}

+ 21 - 0
src/article/dto/create-article.request.ts

@@ -0,0 +1,21 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { IsNotEmpty, Length, MaxLength } from 'class-validator';
+
+export class CreateArticleRequest {
+  @ApiProperty({
+    example: '测试文章标题',
+  })
+  @IsNotEmpty({ message: '文章标题不能为空' })
+  @Length(4, 120, { message: '文章标题长度必须在4到120字符之间' })
+  title: string;
+
+  @ApiProperty()
+  @IsNotEmpty({ message: '文章分类不能为空' })
+  categoryId: string;
+
+  @ApiProperty({
+    example: '测试文章内容',
+  })
+  @MaxLength(10000, { message: '文章内容长度不能超过10000字符' })
+  content: string;
+}

+ 3 - 0
src/article/dto/search-article.filter.ts

@@ -0,0 +1,3 @@
+import { BaseFilter } from '../../core/dto/base.filter';
+
+export class SearchArticleFilter extends BaseFilter {}

+ 3 - 0
src/article/dto/update-article.request.ts

@@ -0,0 +1,3 @@
+import { CreateArticleRequest } from './create-article.request';
+
+export class UpdateArticleRequest extends CreateArticleRequest {}

+ 29 - 0
src/article/entity/article.entity.ts

@@ -0,0 +1,29 @@
+import { TraceableEntity } from '../../core/entity/traceable.entity';
+import { Column, Entity, ManyToOne, JoinColumn } from 'typeorm';
+import { ArticleStatusEnum } from '../enum/article-status.enum';
+import { Category } from './category.entity';
+
+@Entity()
+export class Article extends TraceableEntity {
+  @Column()
+  title: string;
+
+  @Column({
+    type: 'text',
+    nullable: true,
+  })
+  content: string;
+
+  @ManyToOne(() => Category)
+  @JoinColumn({
+    name: 'category_id',
+  })
+  category: Category;
+
+  @Column({
+    type: 'enum',
+    enum: ArticleStatusEnum,
+    default: ArticleStatusEnum.DRAFT,
+  })
+  status: ArticleStatusEnum = ArticleStatusEnum.DRAFT;
+}

+ 5 - 0
src/article/enum/article-status.enum.ts

@@ -0,0 +1,5 @@
+export enum ArticleStatusEnum {
+  DRAFT = 'draft',
+  PUBLISHED = 'published',
+  CLOSED = 'closed',
+}

+ 8 - 0
src/article/error/article.error.ts

@@ -0,0 +1,8 @@
+import { ErrorResponse } from '../../core/error/error.response';
+
+export const ArticleError: Record<string, ErrorResponse> = {
+  NOT_FOUND: {
+    code: 4001,
+    message: 'Article not found',
+  },
+};

+ 1 - 1
src/article/error/category.error.ts

@@ -1,4 +1,4 @@
-import { ErrorResponse } from '../../core/errors/error.response';
+import { ErrorResponse } from '../../core/error/error.response';
 
 export const CategoryError: Record<string, ErrorResponse> = {
   NOT_FOUND: {

+ 24 - 0
src/article/mapper/article.mapper.ts

@@ -0,0 +1,24 @@
+import { Article } from '../entity/article.entity';
+import { ArticleVo } from '../vo/article.vo';
+import { CategoryMapper } from './category.mapper';
+import { DateUtil } from '../../core/util/date.util';
+import { UserMapper } from '../../core/mapper/user.mapper';
+
+export class ArticleMapper {
+  static toVo(entity: Article): ArticleVo {
+    return {
+      id: entity.id,
+      title: entity.title,
+      content: entity.content,
+      category: CategoryMapper.toVo(entity.category),
+      status: entity.status,
+      createdTime: DateUtil.format(entity.createdTime),
+      updatedTime: DateUtil.format(entity.updatedTime),
+      createBy: UserMapper.toVo(entity.createBy),
+      updateBy: UserMapper.toVo(entity.updateBy),
+    };
+  }
+  static toVos(entities: Article[]): ArticleVo[] {
+    return entities.map((entity) => this.toVo(entity));
+  }
+}

+ 79 - 0
src/article/service/article.service.ts

@@ -0,0 +1,79 @@
+import { Injectable } from '@nestjs/common';
+import { CreateArticleRequest } from '../dto/create-article.request';
+import { Repository } from 'typeorm';
+import { Article } from '../entity/article.entity';
+import { InjectRepository } from '@nestjs/typeorm';
+import { CategoryService } from './category.service';
+import { SearchArticleFilter } from '../dto/search-article.filter';
+import { BizException } from '../../core/exception/biz.exception';
+import { ArticleError } from '../error/article.error';
+import { ArticleStatusEnum } from '../enum/article-status.enum';
+
+@Injectable()
+export class ArticleService {
+  constructor(
+    @InjectRepository(Article)
+    private readonly articleRepository: Repository<Article>,
+    private readonly categoryService: CategoryService,
+  ) {}
+
+  async create(createArticleRequest: CreateArticleRequest) {
+    const category = await this.categoryService.get(
+      createArticleRequest.categoryId,
+    );
+    const article = new Article();
+    article.title = createArticleRequest.title;
+    article.content = createArticleRequest.content;
+    article.category = category;
+    return this.articleRepository.save(article);
+  }
+
+  async search(searchArticleFilter: SearchArticleFilter) {
+    return this.articleRepository.findAndCount({
+      where: searchArticleFilter.getConditions(),
+      skip: searchArticleFilter.getSkip(),
+      take: searchArticleFilter.getSize(),
+      order: searchArticleFilter.getOrderBy(),
+    });
+  }
+
+  async get(id: string) {
+    const article = await this.articleRepository.findOneBy({ id });
+    if (!article) {
+      throw new BizException(ArticleError.NOT_FOUND);
+    }
+    return article;
+  }
+
+  async update(id: string, updateArticle: CreateArticleRequest) {
+    const article = await this.articleRepository.findOneBy({ id });
+    if (!article) {
+      throw new BizException(ArticleError.NOT_FOUND);
+    }
+    if (article.category.id !== updateArticle.categoryId) {
+      article.category = await this.categoryService.get(
+        updateArticle.categoryId,
+      );
+    }
+    article.title = updateArticle.title;
+    article.content = updateArticle.content;
+    return this.articleRepository.save(article);
+  }
+
+  async publish(id: string) {
+    const article = await this.get(id);
+    article.status = ArticleStatusEnum.PUBLISHED;
+    return this.articleRepository.save(article);
+  }
+
+  async close(id: string) {
+    const article = await this.get(id);
+    article.status = ArticleStatusEnum.CLOSED;
+    return this.articleRepository.save(article);
+  }
+
+  async delete(id: string) {
+    const article = await this.get(id);
+    await this.articleRepository.remove(article);
+  }
+}

+ 1 - 1
src/article/service/category.service.ts

@@ -4,7 +4,7 @@ import { Category } from '../entity/category.entity';
 import { Repository } from 'typeorm';
 import { CreateCategoryRequest } from '../dto/create-category.request';
 import { SearchCategoryFilter } from '../dto/search-category.filter';
-import { BizException } from '../../core/exceptions/biz.exception';
+import { BizException } from '../../core/exception/biz.exception';
 import { CategoryError } from '../error/category.error';
 import { UpdateCategoryRequest } from '../dto/update-category.request';
 import { UserService } from '../../core/service/user.service';

+ 21 - 0
src/article/vo/article.vo.ts

@@ -0,0 +1,21 @@
+import { TraceableVo } from '../../core/vo/traceable.vo';
+import { ArticleStatusEnum } from '../enum/article-status.enum';
+import { ApiProperty, ApiSchema } from '@nestjs/swagger';
+import { CategoryVo } from './category.vo';
+
+@ApiSchema({
+  name: 'Article',
+})
+export class ArticleVo extends TraceableVo {
+  @ApiProperty()
+  title: string;
+
+  @ApiProperty()
+  content: string;
+
+  @ApiProperty()
+  category: CategoryVo;
+
+  @ApiProperty()
+  status: ArticleStatusEnum;
+}

+ 0 - 0
src/core/errors/error.response.ts → src/core/error/error.response.ts


+ 0 - 0
src/core/errors/user.error.ts → src/core/error/user.error.ts


+ 1 - 1
src/core/exceptions/biz.exception.ts → src/core/exception/biz.exception.ts

@@ -1,5 +1,5 @@
 import { HttpException, HttpStatus } from '@nestjs/common';
-import { ErrorResponse } from '../errors/error.response';
+import { ErrorResponse } from '../error/error.response';
 
 export class BizException extends HttpException {
   constructor(error: ErrorResponse) {

+ 1 - 1
src/core/exceptions/global-exception.filter.ts → src/core/exception/global-exception.filter.ts

@@ -5,7 +5,7 @@ import {
   HttpException,
   HttpStatus,
 } from '@nestjs/common';
-import { ErrorResponse } from '../errors/error.response';
+import { ErrorResponse } from '../error/error.response';
 
 @Catch()
 export class GlobalExceptionFilter implements ExceptionFilter {

+ 1 - 1
src/core/pipe/global-validation.pipe.ts

@@ -1,5 +1,5 @@
 import { HttpStatus, ValidationError, ValidationPipe } from '@nestjs/common';
-import { ErrorResponse } from '../errors/error.response';
+import { ErrorResponse } from '../error/error.response';
 
 export class GlobalValidationPipe extends ValidationPipe {
   constructor() {

+ 1 - 1
src/main.ts

@@ -2,7 +2,7 @@ import { NestFactory } from '@nestjs/core';
 import { AppModule } from './app.module';
 import { SnowflakeUtil } from './core/util/snowflake.util';
 import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
-import { GlobalExceptionFilter } from './core/exceptions/global-exception.filter';
+import { GlobalExceptionFilter } from './core/exception/global-exception.filter';
 import { GlobalValidationPipe } from './core/pipe/global-validation.pipe';
 
 async function bootstrap() {