NestJS 搭建博客系统(九)— 标签模块
前言
经过前面几章基建配置后,终于回到了业务开发上,继续开发博客系统功能。
有时开发过程就是这样,开发到一半,需要跳出来开发一些基础的东西,基础的东西搞好了,后面的开发才会越来越快,否则就是cvcvcv,最后开发出来的东西能跑,但是自己知道不优雅,而老板可是不管你优不优雅,老板只看你这个东西现在能不能跑,而不会特意给你任何一秒钟的时间去优化,另外在团队开发中,虽然有时也会组织重构,但凤毛麟角,而且测试或者其他人为你的个人重构花时间去测试或者新bug买单。
开发标签模块
标签模块是文章标签模块,标签和模块是一个聚合关系,通过标签可以搜索到相关的一些文章,而查看文章也可以知道这篇文章属于那几个标签。如果需要文章分类,我们可以单独开发一个树状分类系统,通过节点关联标签也就等同于给文章分类了。
标签模块的增删改查
一开始开发的标签模块在未关联文章之前和文章模块极像,我们可以参考着来开发
创建模块
nest g mo modules/tag
nest g co modules/tag
nest g s modules/tag
同时在 tag 目录增加 entity dto vo 三个目录
创建 tagEntity
// src/modules/tag/entity
import { IsNotEmpty } from "class-validator";
import { Common } from "src/common/entity/common.entity";
import { Column, Entity } from "typeorm";
@Entity()
export class Tag extends Common{
@Column()
@IsNotEmpty()
label: string
}
复制代码
到 tagModule 引用 Entity
Controller DTO VO Service
先在Controller定义好方法和路由,再去 DTO 和 VO 定义传参和返回,填充好 Controller, 再到 service 实现具体方法,最终结果如下
Controller
// src/modules/tag/tag.controller.ts
import { Body, Controller, Get, Post, Query, UseGuards } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { IdDTO } from 'src/common/dto/id.dto';
import { TagCreateDTO } from './dto/tag-create.dto';
import { TagUpdateDTO } from './dto/tag-edit.dto';
import { TagService } from './tag.service';
import { TagInfoSuccessVO, TagInfoVO } from './vo/tag-info.vo';
import { TagListSuccessVO, TagListVO } from './vo/tag-list.vo';
@ApiTags('标签模块')
@Controller('tag')
export class TagController {
constructor(
private tagService: TagService
) {}
@ApiOkResponse({ description: '标签列表', type: TagListSuccessVO })
@Get('list')
getMore(): Promise<TagListVO> {
return this.tagService.getMore()
}
@ApiBearerAuth()
@ApiCreatedResponse({ description: '创建标签', type: TagInfoSuccessVO })
@UseGuards(AuthGuard('jwt'))
@Post('create')
create(
@Body() tagCreateDto: TagCreateDTO
): Promise<TagInfoVO> {
return this.tagService.create(tagCreateDto)
}
@ApiBearerAuth()
@ApiCreatedResponse({ description: '编辑标签', type: TagInfoSuccessVO })
@UseGuards(AuthGuard('jwt'))
@Post('update')
update(
@Body() tagUpdateDto: TagUpdateDTO
): Promise<TagInfoVO> {
return this.tagService.update(tagUpdateDto)
}
@ApiBearerAuth()
@ApiCreatedResponse({ description: '删除标签', type: TagInfoSuccessVO })
@UseGuards(AuthGuard('jwt'))
@Post('remove')
remove(
@Body() idDto: IdDTO
): Promise<TagInfoVO> {
return this.tagService.remove(idDto)
}
}
复制代码
DTO
// src/modules/tag/dto/tag.dto.ts
import { IsNotEmpty } from "class-validator";
export class TagDTO {
/**
* 标签名称
* @example 标签1
*/
@IsNotEmpty()
label: string
}
复制代码
// src/modules/tag/dto/tag-create.dto.ts
import { TagDTO } from "./tag.dto";
export class TagCreateDTO extends TagDTO {}
复制代码
// src/modules/tag/dto/tag-update.dto.ts
import { IntersectionType, PartialType } from "@nestjs/swagger";
import { IdDTO } from "src/common/dto/id.dto";
import { TagDTO } from "./tag.dto";
export class TagUpdateDTO extends IntersectionType(
IdDTO,
PartialType(TagDTO)
){}
复制代码
VO
// src/modules/tag/vo/tag-info.dto.ts
import { TagDTO } from "../dto/tag.dto";
export class TagInfoVO {
info: TagDTO
}
export class TagInfoSuccessVO {
data: {
info: TagDTO
}
}
复制代码
// src/modules/tag/vo/tag-list.dto.ts
import { TagDTO } from "../dto/tag.dto";
export class TagListVO {
list: TagDTO[]
}
export class TagListSuccessVO {
data: TagListVO
}
复制代码
Service
// src/modules/tag/tag.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { IdDTO } from 'src/common/dto/id.dto';
import { Repository } from 'typeorm';
import { TagCreateDTO } from './dto/tag-create.dto';
import { TagUpdateDTO } from './dto/tag-edit.dto';
import { Tag } from './entity/tag.entity';
import { TagInfoVO } from './vo/tag-info.vo';
import { TagListVO } from './vo/tag-list.vo';
@Injectable()
export class TagService {
constructor(
@InjectRepository(Tag)
private readonly tagRepository: Repository<Tag>,
) {}
async getMore(): Promise<TagListVO> {
const getList = this.tagRepository
.createQueryBuilder('tag')
.where({ isDelete: false })
.select([
'tag.id',
'tag.label',
])
.getMany()
const result = await getList
return {
list: result
}
}
async create(
tagCreateDTO: TagCreateDTO
): Promise<TagInfoVO> {
const { label } = tagCreateDTO
const hasTag = await this.tagRepository.findOne({ label })
if (hasTag) {
throw new NotFoundException(`${label}标签已存在`)
}
const tag = new Tag();
tag.label = tagCreateDTO.label
const result = await this.tagRepository.save(tag);
return {
info: result
}
}
async update(
tagUpdateDto: TagUpdateDTO
): Promise<TagInfoVO> {
const { id, label } = tagUpdateDto
const tag = await this.tagRepository.findOne({ id })
tag.label = label
const result = await this.tagRepository.save(tag)
return {
info: result
}
}
async remove(
idDto: IdDTO
) {
const { id } = idDto
const tag = await this.tagRepository.findOne({ id })
tag.isDelete = true
const result = await this.tagRepository.save(tag)
return {
info: result
}
}
}
复制代码
关联文章模块
本章的目标是把文章和标签关联起来,可以通过标签搜索到相关文章,同时文章列表也应该返回其对应的标签列表
修改 ArticleEntity 和 TagEntity
// src/modules/article/entity/articel.entity.ts
import { Common } from 'src/common/entity/common.entity';
import { Tag } from 'src/modules/tag/entity/tag.entity';
import { Entity, Column, ManyToMany, JoinTable } from 'typeorm';
@Entity()
export class Article extends Common{
// 文章标题
@Column('text')
title: string;
// 文章描述
@Column('text')
description: string;
// 文章内容
@Column('text')
content: string;
// 标签
@ManyToMany(type => Tag, tag => tag.articles)
@JoinTable()
tags: Tag[];
}
复制代码
// src/modules/tag/entity/tag.entity.ts
import { Common } from "src/common/entity/common.entity";
import { Article } from "src/modules/article/entity/article.entity";
import { Column, Entity, JoinTable, ManyToMany } from "typeorm";
@Entity()
export class Tag extends Common{
// 标签名称
@Column()
label: string
// 文章
@ManyToMany(() => Article, article => article.tags)
articles: Article[];
}
复制代码
修改创建文章的 DTO
// src/modules/article/dto/article.dto.ts
import { IsNotEmpty } from "class-validator";
import { Tag } from "src/modules/tag/entity/tag.entity";
export class ArticleDTO {
/**
* 文章标题
* @example 啊!美丽的大海
*/
@IsNotEmpty({ message: '请输入文章标题' })
readonly title: string;
/**
* 文章简述
* @example 给你讲述美丽的大海
*/
@IsNotEmpty({ message: '请输入文章描述' })
readonly description: string;
/**
* 文章内容
* @example 啊!美丽的大海,你是如此美丽
*/
@IsNotEmpty({ message: '请输入文章内容' })
readonly content: string;
/**
* 标签 格式 [{id: 1}, {id: 2}]
* @example [{id: 1}]
*/
readonly tags?: Tag[]
}
复制代码
修改 ArticleService
// src/modules/article/article.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { ArticleCreateDTO } from './dto/article-create.dto';
import { ArticleEditDTO } from './dto/article-edit.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Article } from './entity/article.entity';
import { getPagination } from 'src/utils/index.util';
import { PageDTO } from 'src/common/dto/Page.dto';
import { IdDTO } from 'src/common/dto/id.dto';
import { ArticleListDTO } from './dto/article-list.dto';
@Injectable()
export class ArticleService {
constructor(
@InjectRepository(Article)
private readonly articleRepository: Repository<Article>,
) {}
async getMore(
pageDTO: PageDTO,
) {
const { page = 1, pageSize = 10 } = pageDTO
const getList = this.articleRepository
.createQueryBuilder('article')
.where({ isDelete: false })
.leftJoin("article.tags","tag")
.select([
'article.id',
'article.title',
'article.description',
'article.createTime',
'article.updateTime',
])
.addSelect([
'tag.id',
'tag.label'
])
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount()
const [list, total] = await getList
const pagination = getPagination(total, pageSize, page)
return {
list,
pagination,
}
}
async getMoreByTagId(
articleListDto: ArticleListDTO
) {
const { page = 1, pageSize = 10, tagId } = articleListDto
const getList = this.articleRepository
.createQueryBuilder('article')
.where({ isDelete: 0 })
.andWhere('tag.id = :id', { id: tagId })
.andWhere('tag.isDelete = :isDelete', { isDelete: false })
.leftJoin("article.tags","tag")
.select([
'article.id',
'article.title',
'article.description',
'article.createTime',
'article.updateTime',
])
.addSelect([
'tag.id',
'tag.label'
])
.skip((page - 1) * pageSize)
.take(pageSize)
.getManyAndCount()
const [list, total] = await getList
const pagination = getPagination(total, pageSize, page)
return {
list,
pagination,
}
}
async getOne(
idDto: IdDTO
) {
const { id } = idDto
const articleDetial = await this.articleRepository
.createQueryBuilder('article')
.where('article.id = :id', { id })
.leftJoin("article.tags","tag")
.select([
'article.id',
'article.title',
'article.description',
'article.createTime',
'article.updateTime',
])
.addSelect([
'tag.id',
'tag.label'
])
.getOne()
if (!articleDetial) {
throw new NotFoundException('找不到文章')
}
return {
info: articleDetial
}
}
/**
*
* @param articleCreateDTO
* @returns
*/
async create(
articleCreateDTO: ArticleCreateDTO
){
const article = new Article()
for (let key in articleCreateDTO) {
article[key] = articleCreateDTO[key]
}
const result = await this.articleRepository.save(article);
return {
info: result
}
}
/**
*
* @param articleEditDTO
* @returns
*/
async update(
articleEditDTO: ArticleEditDTO
) {
const { id } = articleEditDTO
let articleToUpdate = await this.articleRepository.findOne({ id })
for (let key in articleEditDTO) {
if (key !== 'id') {
articleToUpdate[key] = articleEditDTO[key]
}
}
const result = await this.articleRepository.save(articleToUpdate)
return {
info: result,
}
}
/**
*
* @param idDTO
* @returns
*/
async remove (
idDTO: IdDTO,
) {
const { id } = idDTO
let articleToUpdate = await this.articleRepository.findOne({ id })
articleToUpdate.isDelete = true
const result = await this.articleRepository.save(articleToUpdate)
return {
info: result
}
}
}
复制代码
可以看到我们还增加了一个 getMoreByTagId 方法,修改下 article/list 的入参
// src/modules/article/article.controller.ts
@ApiOkResponse({ description: '文章列表', type: ArticleListSuccessVO })
@Get('list')
async getMore(
@Query() articleListDto: ArticleListDTO,
): Promise<ArticleListVO> {
const { tagId } = articleListDto
if (tagId) {
return await this.articleService.getMoreByTagId(articleListDto)
}
return await this.articleService.getMore(articleListDto)
}
复制代码
其中 ArticleListDTO 如下
// src/modules/article/dto/article-list.dto.ts
import { PageDTO } from "src/common/dto/Page.dto";
export class ArticleListDTO extends PageDTO {
/**
* tagId
* @example 1
*/
tagId?: number
}
复制代码
至此,本章实现了 tag 模块的 curd,并且关联到 article 模块,/article/list 可以根据 tagId 查询到相关文章。