NestJS实战-后端开发-文章专栏功能模块
本文介绍 NestJS 实战的文章专栏功能模块:文章和专栏表接口构建、中间关联表及多表联查、关联表软删除、文章专栏异步遍历处理
供自己以后查漏补缺,也欢迎同道朋友交流学习。
引言
项目的全局配置及用户权限,上一章已经介绍过了,本章主要介绍几个功能模块的开发:文章管理、专栏管理
文章管理
文章表 article
我们先进行文章表结构设计,因为想做个简单的,就不建文章详情表了
- id (主键)
- title (文章名称)
- description (文章描述)
- content (文章内容)
- createdBy (创建人id 外键 关联user表)
- createdByAccount (创建人id 外键 关联user表)
- createdTime (创建时间)
- updatedBy (更新人Id 关联user表)
- updatedByAccount (更新人账号 关联user表)
- updatedTime (更新时间)
- isDeleted (是否删除 0未删除 1已删除)
文章专栏关联表 article_column
多对多,一篇文章可以收录至多个专栏,一个专栏也可以收录多个文章
- id (主键)
- articleId (文章id 外键 关联article表)
- columnId (专栏id 外键 关联column表)
- isDeleted (是否删除 0未删除 1已删除)
文章模块开发
使用快捷键新建 article
模块:
nest g res article
# 默认选择 REST API
根据文章表配置实体 article.entity.ts
如下:
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class Article {
// 主键 唯一且自增长
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 50, unique: true })
title: string;
@Column({ type: 'varchar', length: 200 })
description: string;
@Column({ type: 'text' })
content: string;
@Column({ default: 1 })
createdBy: number; // 创建人id
@Column()
createdByAccount: string;
@CreateDateColumn()
createdTime: Date;
@Column({ default: 1 })
updatedBy: number; // 更新人Id
@Column()
updatedByAccount: string;
@UpdateDateColumn()
updatedTime: Date;
@Column({ default: 0 })
isDeleted: number; // 是否删除,0表示未删除,1表示已删除
}
根据文章关联表配置实体 article-column-related.entity.ts
如下:
import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
import { Article } from './article.entity';
import { SpecialColumn } from 'src/special-column/entities/special-column.entity';
@Entity()
export class ArticleColumnRelated {
// 主键 唯一且自增长
@PrimaryGeneratedColumn()
id: number;
@Column()
articleId: number;
@Column()
articleTitle: string;
@Column()
columnId: number;
@Column()
columnTitle: string;
@Column({ default: 0 })
isDeleted: number; // 是否删除,0表示未删除,1表示已删除
@ManyToOne(() => Article, (article) => article.id, { eager: false })
article: Article;
@ManyToOne(() => SpecialColumn, (column) => column.id, { eager: false })
column: SpecialColumn;
}
定义文章数据传输对象
创建文章 CreateArticleDto
create-article.dto.ts
代码如下:
import { ApiProperty } from '@nestjs/swagger';
import {
IsNotEmpty,
IsString,
IsNumber,
IsOptional,
IsArray,
} from 'class-validator';
export class CreateArticleDto {
@IsString({ message: '文章标题必须是字符串' })
@IsNotEmpty({ message: '文章标题不能为空' })
@ApiProperty({
description: '文章标题',
example: '文章标题文章标题',
})
title: string;
@IsString({ message: '文章描述必须是字符串' })
@IsNotEmpty({ message: '文章描述不能为空' })
@ApiProperty({
description: '文章描述',
example: '文章描述文章描述文章描述文章描述',
})
description: string;
@IsString({ message: '文章内容必须是字符串' })
@IsNotEmpty({ message: '文章内容不能为空' })
@ApiProperty({
description: '文章内容',
example: '文章内容文章内容文章内容',
})
content: string;
@IsArray()
@IsNumber({}, { each: true }) // 验证数组中的每个元素是否为数字
@IsOptional()
@ApiProperty({
description: '专栏id',
example: [1, 2, 3],
})
columnIds: number[];
}
文章列表查询 ListArticleDto
list-article.dto.ts
代码如下:
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsNotEmpty } from 'class-validator';
export class ListArticleDto {
@IsOptional()
@ApiProperty({
description: '文章ID',
example: 1,
})
id: number;
@IsOptional()
@ApiProperty({
description: '文章标题',
example: '文章标题',
})
title: string;
@IsOptional()
@ApiProperty({
description: '专栏id',
example: 1,
})
columnId: number;
@ApiProperty({ description: '页码', example: 1 })
@IsNotEmpty({ message: 'pageNum不能为空' })
pageNum: number = 1;
@ApiProperty({ description: '每页查询数量', example: 10 })
@IsNotEmpty({ message: 'pageSize不能为空' })
pageSize: number = 10;
}
修改文章 UpdateArticleDto
update-article.dto.ts
代码如下:
import { PartialType } from '@nestjs/swagger';
import { CreateArticleDto } from './create-article.dto';
export class UpdateArticleDto extends PartialType(CreateArticleDto) {}
文章CRUD相关操作 ArticleController
在控制器中编写文章增删改查相关操作,这个也是给前端的接口定义,我们主要实现如下几个接口:
接口 | 请求 | 定义 | 描述 | 备注 |
---|---|---|---|---|
/article | Post | 创建文章 | 创建文章 | title是唯一值,不可重复,可以修改 |
/article | Get | 分页查询文章列表 | 分页查询文章列表 | 根据query、pageSize、PageNum分页查询用户列表 |
/article/{id} | Get | 根据ID查询文章详情 | 根据ID查询文章详情 | - |
/article/{id} | Patch | 根据ID修改文章 | 根据ID修改文章 | - |
/article/{id} | Delete | 删除文章 | 根据id删除文章 | - |
article.controller.ts
代码如下:
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Req,
UseGuards,
Query,
} from '@nestjs/common';
import { ArticleService } from './article.service';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import {
ApiBody,
ApiOperation,
ApiResponse,
ApiTags,
ApiParam,
ApiQuery,
} from '@nestjs/swagger';
import { ListArticleDto } from './dto/list-article.dto';
import { Roles } from 'src/user/roles.decorator';
import { RolesGuard } from 'src/user/roles.guard';
@UseGuards(RolesGuard)
@Controller('article')
@ApiTags('文章管理')
export class ArticleController {
constructor(private readonly articleService: ArticleService) {}
@Post()
@Roles('systemAdmin', 'admin', 'user')
@ApiOperation({
summary: '创建文章',
description: '创建文章',
})
@ApiBody({ type: CreateArticleDto })
@ApiResponse({ status: 200, description: '创建成功' })
create(@Body() createArticleDto: CreateArticleDto, @Req() req) {
return this.articleService.create(createArticleDto, req);
}
@Get()
@ApiOperation({
summary: '分页查询文章列表',
description: '分页查询文章列表',
})
@ApiQuery({ name: 'query', type: ListArticleDto })
@ApiResponse({ status: 200, description: '查询用户成功' })
findAll(@Query() query: ListArticleDto) {
return this.articleService.findAllByPage(query);
}
@Get(':id')
@ApiOperation({
summary: '根据ID查询文章详情',
description: '根据ID查询文章详情',
})
@ApiParam({ name: 'id', description: '文章id' })
@ApiResponse({ status: 200, description: '查询成功' })
findOne(@Param('id') id: string) {
return this.articleService.findOne(+id);
}
@Patch(':id')
@Roles('systemAdmin', 'admin', 'user')
@ApiOperation({
summary: '根据ID修改文章',
description: '根据ID修改文章',
})
@ApiParam({ name: 'id', description: '文章id' })
@ApiBody({ type: UpdateArticleDto })
@ApiResponse({ status: 200, description: '修改文章成功' })
update(
@Param('id') id: string,
@Body() updateArticleDto: UpdateArticleDto,
@Req() req,
) {
return this.articleService.update(+id, updateArticleDto, req);
}
@Delete(':id')
@Roles('systemAdmin', 'admin', 'user')
@ApiOperation({
summary: '删除文章',
description: '根据id删除文章',
})
@ApiParam({ name: 'id', description: '文章id' })
@ApiResponse({ status: 200, description: '删除文章成功' })
remove(@Param('id') id: string, @Req() req) {
return this.articleService.softDeleteArticle(+id, req);
}
}
文章服务 ArticleService
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
Req,
} from '@nestjs/common';
import { CreateArticleDto } from './dto/create-article.dto';
import { UpdateArticleDto } from './dto/update-article.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Like, Repository } from 'typeorm';
import { Article } from './entities/article.entity';
import { ArticleColumnRelated } from './entities/article-column-related.entity';
import { SpecialColumn } from 'src/special-column/entities/special-column.entity';
import {
removeUserData,
setCreatedUser,
removeUnnecessaryData,
setUpdatedUser,
setDeletedUser,
} from 'src/utils';
import { ListArticleDto } from './dto/list-article.dto';
@Injectable()
export class ArticleService {
private logger = new Logger('ArticleService');
constructor(
@InjectRepository(Article)
private readonly articleRepository: Repository<Article>,
@InjectRepository(ArticleColumnRelated)
private readonly articleColumnRelatedRepository: Repository<ArticleColumnRelated>,
@InjectRepository(SpecialColumn)
private readonly specialColumnRepository: Repository<SpecialColumn>,
) {}
// 异步获取文章专栏关联数据
async getArticleColumnData(columnIds, savedArticle) {
return await Promise.all(
columnIds.map(async (columnId) => {
const articleColumn = new ArticleColumnRelated();
// 文章信息
articleColumn.articleId = savedArticle.id;
articleColumn.articleTitle = savedArticle.title;
// 专栏信息
const column = await this.specialColumnRepository.findOne({
where: { id: columnId, isDeleted: 0 },
});
articleColumn.columnId = columnId;
articleColumn.columnTitle = column?.title;
return articleColumn;
}),
);
}
async createArticleWithColumns(
createArticleDto: CreateArticleDto,
req,
): Promise<any> {
return await this.articleRepository.manager.transaction(async (manager) => {
// 创建文章
const article = new Article();
article.title = createArticleDto.title;
article.description = createArticleDto.description;
article.content = createArticleDto.content;
// 设置创建用户信息
const createdArticle = setCreatedUser(req, article);
const savedArticle = await manager.save(createdArticle);
// 创建文章专栏关联记录
if (createArticleDto?.columnIds?.length) {
const articleColumns = await this.getArticleColumnData(
createArticleDto?.columnIds,
savedArticle,
);
await manager.save(articleColumns);
}
return savedArticle;
});
}
async validateHasColumnIds(createArticleDto) {
if (createArticleDto?.columnIds?.length) {
const columnIds = await this.specialColumnRepository.find({
select: ['id'],
where: {
id: In(createArticleDto?.columnIds),
isDeleted: 0,
},
});
const columnIdArr = columnIds.map((item) => item.id);
for (const id of createArticleDto?.columnIds) {
if (!columnIdArr.includes(id)) {
throw new BadRequestException(`专栏ID ${id} 不存在`);
}
}
}
}
// 新建文章
async create(createArticleDto: CreateArticleDto, @Req() req) {
// 先查询文章的专栏id是否存在
if (createArticleDto?.columnIds?.length) {
await this.validateHasColumnIds(createArticleDto);
}
// 新建文章
try {
const article = await this.createArticleWithColumns(
createArticleDto,
req,
);
return {
id: article.id,
title: article.title,
description: article.description,
};
} catch (error) {
this.logger.error('文章新建失败:', error);
throw new InternalServerErrorException('文章新建失败');
}
}
// 分页查询文章
async findAllByPage(query: ListArticleDto) {
try {
// 分页查询
const [data, total] = await this.articleRepository.findAndCount({
where: {
id: query.id,
title: query.title ? Like(`%${query.title}%`) : null,
isDeleted: 0,
},
order: {
id: 'DESC',
},
skip: (query.pageNum - 1) * query.pageSize,
take: query.pageSize,
});
this.logger.log('@@@ 分页查询文章:', data);
return {
list: removeUnnecessaryData(data),
pageNum: Number(query.pageNum),
pageSize: Number(query.pageSize),
total,
};
} catch (error) {
this.logger.error('@@@@ 账号列表查询失败:', error);
throw new InternalServerErrorException('账号列表查询失败');
}
}
async queryColumnIdsByArticleId(articleId) {
const columns = await this.articleColumnRelatedRepository.find({
select: ['columnId'],
where: { articleId },
});
return columns.map((item) => item.columnId);
}
// 查询文章详情
async findOne(id: number) {
if (!id) {
throw new BadRequestException('id必填');
}
try {
const data = await this.articleRepository.findOne({
where: { id, isDeleted: 0 },
});
const filterData = removeUserData(data);
this.logger.log('@@@ 查询文章详情', filterData);
// 关联查询返回专栏id
const columnIds = await this.queryColumnIdsByArticleId(id);
return {
...filterData,
columnIds,
};
} catch (error) {
this.logger.error('@@@@ 查询文章详情失败:', error);
throw new InternalServerErrorException('查询文章详情失败');
}
}
async updateArticleWithColumns(
id,
updateArticleDto: UpdateArticleDto,
req,
): Promise<any> {
return await this.articleRepository.manager.transaction(async (manager) => {
// 获取原始文章
const article = await manager.findOne(Article, {
where: { id, isDeleted: 0 },
});
if (!article) {
throw new BadRequestException(`文章ID ${id} 没有找到`);
}
// 更新文章基本信息
article.title = updateArticleDto.title;
article.description = updateArticleDto.description;
article.content = updateArticleDto.content;
// 设置更新用户信息
const updatedArticle = setUpdatedUser(req, article);
const savedArticle = await manager.save(updatedArticle);
// 处理文章专栏关联记录
const existingRelations = await manager.find(ArticleColumnRelated, {
where: { articleId: id },
});
this.logger.log('@@@@ existingRelations', existingRelations);
// 需要添加的新关联
const newRelationsToAdd = updateArticleDto.columnIds.filter(
(columnId) =>
!existingRelations.some((item) => item.columnId === columnId),
);
this.logger.log('@@@@ newRelationsToAdd', newRelationsToAdd);
// 需要标记为已删除的旧关联
const oldRelationsToMarkDeleted = existingRelations.filter(
(rel) => !updateArticleDto.columnIds.includes(rel.columnId),
);
this.logger.log(
'@@@@ oldRelationsToMarkDeleted',
oldRelationsToMarkDeleted,
);
// 添加新的关联记录
for (const columnId of newRelationsToAdd) {
const column = await this.specialColumnRepository.findOneBy({
id: columnId,
isDeleted: 0,
});
const newRelation = new ArticleColumnRelated();
newRelation.articleId = id;
newRelation.articleTitle = savedArticle.title;
newRelation.columnId = column.id;
newRelation.columnTitle = column.title;
await manager.save(newRelation);
}
// 标记旧的关联记录为已删除
for (const relation of oldRelationsToMarkDeleted) {
relation.isDeleted = 1; // 标记为已删除
await manager.save(relation);
}
// 给所有记录修改标题
for (const relation of existingRelations) {
relation.articleTitle = savedArticle.title; // 文章标题修改
await manager.save(relation);
}
return savedArticle;
});
}
// 修改文章
async update(id: number, updateArticleDto: UpdateArticleDto, @Req() req) {
if (!id) {
throw new BadRequestException('id必填');
}
this.logger.log('@@@@ 修改文章 updateArticleDto', updateArticleDto);
// 先查询文章的专栏id是否存在
if (updateArticleDto?.columnIds?.length) {
await this.validateHasColumnIds(updateArticleDto);
}
// 更新文章
try {
const article = await this.updateArticleWithColumns(
id,
updateArticleDto,
req,
);
return {
id: article.id,
title: article.title,
description: article.description,
};
} catch (error) {
this.logger.error('文章更新失败:', error);
throw new InternalServerErrorException('文章更新失败');
}
}
// 软删除文章专栏关联表
async softDeleteArticleColumnRelations(articleId: number) {
const relations = await this.articleColumnRelatedRepository.find({
where: { articleId, isDeleted: 0 },
});
for (const relation of relations) {
relation.isDeleted = 1; // 标记为已删除
await this.articleColumnRelatedRepository.save(relation);
}
}
// 软删除单个文章
async softDeleteArticle(id: number, @Req() req) {
if (!id) {
throw new BadRequestException('id必填');
}
try {
const article = new Article();
const removedArticle = setDeletedUser(req, article);
this.logger.log('@@@ 软删除单个文章 removedArticle', removedArticle);
await this.articleRepository.update(id, removedArticle);
// 软删除文章专栏关联表
await this.softDeleteArticleColumnRelations(id);
return { message: '删除成功' };
} catch (error) {
this.logger.error('@@@@ 删除文章失败:', error);
throw new InternalServerErrorException('删除文章失败');
}
}
}
文章模块 ArticleModule
import { Module } from '@nestjs/common';
import { ArticleService } from './article.service';
import { ArticleController } from './article.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from './entities/article.entity';
import { ArticleColumnRelated } from './entities/article-column-related.entity';
import { SpecialColumn } from 'src/special-column/entities/special-column.entity';
import { Role } from 'src/user/entities/role.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Article,
ArticleColumnRelated,
SpecialColumn,
Role,
]),
],
controllers: [ArticleController],
providers: [ArticleService],
exports: [ArticleService],
})
export class ArticleModule {}
文章管理功能接口完成
目前完成了5个文章相关的接口
专栏管理
专栏表 special-column
- id (主键)
- title (专栏标题)
- description (专栏描述)
- createdBy (创建人id 外键 关联user表)
- createdByAccount (创建人id 外键 关联user表)
- createdTime (创建时间)
- updatedBy (更新人Id 关联user表)
- updatedByAccount (更新人账号 关联user表)
- updatedTime (更新时间)
- isDeleted (是否删除 0未删除 1已删除)
专栏模块开发
使用快捷键新建 special-column
模块:
nest g res special-column
# 默认选择 REST API
根据专栏表结构配置 special-column.entity.ts
:
import {
Column,
CreateDateColumn,
Entity,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
@Entity()
export class SpecialColumn {
// 主键 唯一且自增长
@PrimaryGeneratedColumn()
id: number;
@Column({ type: 'varchar', length: 50, unique: true })
title: string;
@Column({ type: 'varchar', length: 200 })
description: string;
@Column({ default: 1 })
createdBy: number; // 创建人id
@Column()
createdByAccount: string;
@CreateDateColumn()
createdTime: Date;
@Column({ default: 1 })
updatedBy: number; // 更新人Id
@Column()
updatedByAccount: string;
@UpdateDateColumn()
updatedTime: Date;
@Column({ default: 0 })
isDeleted: number; // 是否删除,0表示未删除,1表示已删除
}
定义专栏数据传输对象
创建专栏 CreateSpecialColumnDto
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CreateSpecialColumnDto {
@IsString({ message: '专栏标题必须是字符串' })
@IsNotEmpty({ message: '专栏标题不能为空' })
@ApiProperty({
description: '专栏标题',
example: '专栏标题专栏标题',
})
title: string;
@IsString({ message: '专栏描述必须是字符串' })
@IsNotEmpty({ message: '专栏描述不能为空' })
@ApiProperty({
description: '专栏描述',
example: '专栏描述专栏描述专栏描述专栏描述',
})
description: string;
}
列表查询 ListArticleColumnDto
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional, IsNotEmpty } from 'class-validator';
export class ListArticleColumnDto {
@IsOptional()
@ApiProperty({
description: '文章标题',
example: '文章标题',
})
articleTitle: string;
@ApiProperty({ description: '页码', example: 1 })
@IsNotEmpty({ message: 'pageNum不能为空' })
pageNum: number = 1;
@ApiProperty({ description: '每页查询数量', example: 10 })
@IsNotEmpty({ message: 'pageSize不能为空' })
pageSize: number = 10;
}
更新专栏 UpdateSpecialColumnDto
import { PartialType } from '@nestjs/swagger';
import { CreateSpecialColumnDto } from './create-special-column.dto';
export class UpdateSpecialColumnDto extends PartialType(
CreateSpecialColumnDto,
) {}
文章专栏联表查询
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
export class ArticleColumnInclusionDto {
@IsNotEmpty({ message: '专栏ID不能为空' })
@ApiProperty({
description: '专栏ID',
example: 1,
})
columnId: number;
@IsNotEmpty({ message: '文章ids不能为空' })
@ApiProperty({
description: '文章ids',
example: [1, 2],
})
articleIds: number[];
}
专栏CRUD相关操作 SpecialColumnController
在控制器中编写用户增删改查相关操作,这个也是给前端的接口定义,我们主要实现如下几个接口:
接口 | 请求 | 定义 | 描述 | 备注 |
---|---|---|---|---|
/specialColumn | Post | 创建专栏 | 创建专栏 | title是唯一值,不能重复、但可修改 |
/specialColumn | Get | 专栏列表查询 | 专栏列表查询 | - |
/specialColumn/columnListForSelect | Get | 专栏列表下拉项 | 专栏列表下拉项 | - |
/specialColumn/{id} | Get | 根据ID查询专栏 | 根据ID查询专栏 | - |
/specialColumn/{id} | Patch | 根据ID修改专栏 | 根据ID修改专栏 | - |
/specialColumn/{id} | Delete | 删除专栏 | 根据id删除用户 | - |
/specialColumn/queryArticleListByColumnId/{id} | Get | 根据专栏id查询文章列表 | 根据专栏id查询文章列表 | - |
/specialColumn/queryOtherArticleListByColumnId/{id} | Post | 查询非该专栏的文章列表 | 根据专栏id查询非该专栏的文章列表 | - |
/specialColumn/cancelArticleInclusion | Post | 批量取消收录 | 批量取消收录至该专栏 | - |
/specialColumn/addArticleInclusion | Post | 文章批量收录 | 文章批量收录至该专栏 | - |
special-column.controller.ts
如下:
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
Req,
UseGuards,
Query,
} from '@nestjs/common';
import { SpecialColumnService } from './special-column.service';
import { CreateSpecialColumnDto } from './dto/create-special-column.dto';
import { UpdateSpecialColumnDto } from './dto/update-special-column.dto';
import {
ApiOperation,
ApiTags,
ApiBody,
ApiResponse,
ApiParam,
} from '@nestjs/swagger';
import { ListArticleColumnDto } from './dto/list-article-column.dto';
import { ArticleColumnInclusionDto } from './dto/article-column-inclusion.dto';
import { Roles } from 'src/user/roles.decorator';
import { RolesGuard } from 'src/user/roles.guard';
@Controller('specialColumn')
@UseGuards(RolesGuard)
@ApiTags('专栏管理')
export class SpecialColumnController {
constructor(private readonly specialColumnService: SpecialColumnService) {}
@Post()
@Roles('systemAdmin', 'admin', 'user')
@ApiOperation({
summary: '创建专栏',
description: '创建专栏',
})
@ApiBody({ type: CreateSpecialColumnDto })
@ApiResponse({ status: 200, description: '创建成功' })
create(@Body() createSpecialColumnDto: CreateSpecialColumnDto, @Req() req) {
return this.specialColumnService.create(createSpecialColumnDto, req);
}
@Get()
@ApiOperation({
summary: '专栏列表查询',
description: '专栏列表查询',
})
@ApiResponse({ status: 200, description: '查询成功' })
findAll() {
return this.specialColumnService.findAll();
}
@Get('columnListForSelect')
@ApiOperation({
summary: '专栏列表下拉项',
description: '专栏列表下拉项',
})
@ApiResponse({ status: 200, description: '查询成功' })
findAllForSelect() {
return this.specialColumnService.findAllForSelect();
}
@Get(':id')
@ApiOperation({
summary: '根据ID查询专栏',
description: '根据ID查询专栏',
})
@ApiParam({ name: 'id', description: '专栏id' })
@ApiResponse({ status: 200, description: '查询成功' })
findOne(@Param('id') id: string) {
return this.specialColumnService.findOne(+id);
}
@Patch(':id')
@ApiOperation({
summary: '根据ID修改专栏',
description: '根据ID修改专栏',
})
@ApiParam({ name: 'id', description: '专栏id' })
@ApiBody({ type: UpdateSpecialColumnDto })
@ApiResponse({ status: 200, description: '修改专栏成功' })
update(
@Param('id') id: string,
@Body() updateSpecialColumnDto: UpdateSpecialColumnDto,
@Req() req,
) {
return this.specialColumnService.update(+id, updateSpecialColumnDto, req);
}
@Delete(':id')
@Roles('systemAdmin', 'admin', 'user')
@ApiOperation({
summary: '删除专栏',
description: '根据id删除专栏',
})
@ApiParam({ name: 'id', description: '专栏id' })
@ApiResponse({ status: 200, description: '删除专栏成功' })
remove(@Param('id') id: string, @Req() req) {
return this.specialColumnService.softDeleteColumn(+id, req);
}
@Get('queryArticleListByColumnId/:id')
@ApiOperation({
summary: '根据专栏id查询文章列表',
description: '根据专栏id查询文章列表',
})
@ApiParam({ name: 'id', description: '专栏id' })
@ApiBody({ type: ListArticleColumnDto })
@ApiResponse({ status: 200, description: '查询成功' })
queryArticleListByColumnId(
@Param('id') id: string,
@Query() query: ListArticleColumnDto,
) {
return this.specialColumnService.queryArticleListByColumnId(+id, query);
}
@Get('queryOtherArticleListByColumnId/:id')
@ApiOperation({
summary: '查询非该专栏的文章列表',
description: '根据专栏id查询非该专栏的文章列表',
})
@ApiParam({ name: 'id', description: '专栏id' })
@ApiBody({ type: ListArticleColumnDto })
@ApiResponse({ status: 200, description: '查询成功' })
queryOtherArticleListByColumnId(
@Param('id') id: string,
@Query() query: ListArticleColumnDto,
) {
return this.specialColumnService.queryOtherArticleListByColumnId(
+id,
query,
);
}
@Post('cancelArticleInclusion')
@Roles('systemAdmin', 'admin', 'user')
@ApiOperation({
summary: '批量取消收录',
description: '批量取消收录至该专栏',
})
@ApiBody({ type: ArticleColumnInclusionDto })
@ApiResponse({ status: 200, description: '查询成功' })
cancelArticleInclusion(@Body() body: ArticleColumnInclusionDto) {
return this.specialColumnService.cancelArticleInclusion(body);
}
@Post('addArticleInclusion')
@Roles('systemAdmin', 'admin', 'user')
@ApiOperation({
summary: '文章批量收录',
description: '文章批量收录至该专栏',
})
@ApiBody({ type: ArticleColumnInclusionDto })
@ApiResponse({ status: 200, description: '查询成功' })
addArticleInclusion(@Body() body: ArticleColumnInclusionDto) {
return this.specialColumnService.addArticleInclusion(body);
}
}
专栏服务 SpecialColumnService
import {
BadRequestException,
Injectable,
InternalServerErrorException,
Logger,
Req,
} from '@nestjs/common';
import { CreateSpecialColumnDto } from './dto/create-special-column.dto';
import { UpdateSpecialColumnDto } from './dto/update-special-column.dto';
import { InjectRepository } from '@nestjs/typeorm';
import { In, Like, Not, Repository } from 'typeorm';
import { Article } from 'src/article/entities/article.entity';
import { ArticleColumnRelated } from 'src/article/entities/article-column-related.entity';
import { SpecialColumn } from './entities/special-column.entity';
import {
removeUnnecessaryData,
setCreatedUser,
setUpdatedUser,
setDeletedUser,
} from 'src/utils';
import { ListArticleColumnDto } from './dto/list-article-column.dto';
import { ArticleColumnInclusionDto } from './dto/article-column-inclusion.dto';
@Injectable()
export class SpecialColumnService {
private logger = new Logger('SpecialColumnService');
constructor(
@InjectRepository(Article)
private readonly articleRepository: Repository<Article>,
@InjectRepository(ArticleColumnRelated)
private readonly articleColumnRelatedRepository: Repository<ArticleColumnRelated>,
@InjectRepository(SpecialColumn)
private readonly specialColumnRepository: Repository<SpecialColumn>,
) {}
// 新建专栏
async create(createSpecialColumnDto: CreateSpecialColumnDto, @Req() req) {
try {
// 创建专栏实体
const column = this.specialColumnRepository.create(
createSpecialColumnDto,
);
const createdColumn = setCreatedUser(req, column);
this.logger.log('@@@@ 新建专栏 createdColumn', createdColumn);
return await this.specialColumnRepository.save(createdColumn);
} catch (error) {
this.logger.error('@@@@ 新建专栏失败:', error);
throw new InternalServerErrorException('新建专栏失败');
}
}
// 查询所有专栏
async findAll() {
try {
const data = await this.specialColumnRepository.find({
where: { isDeleted: 0 },
});
// 分别查询每个专栏的文章数量
const specialColumnsWithCount = await Promise.all(
data.map(async (column) => {
const count = await this.articleColumnRelatedRepository.count({
where: { columnId: column.id, isDeleted: 0 },
});
return { ...column, count };
}),
);
const filterData = removeUnnecessaryData(specialColumnsWithCount);
this.logger.log('@@@ 查询专栏列表', filterData);
return filterData;
} catch (error) {
this.logger.error('@@@@ 查询专栏列表失败:', error);
throw new InternalServerErrorException('查询专栏列表失败');
}
}
// 查询专栏列表下拉选择
async findAllForSelect() {
try {
const data = await this.specialColumnRepository.find({
select: ['id', 'title'],
where: { isDeleted: 0 },
});
this.logger.log('@@@ 查询专栏列表下拉选择', data);
return data;
} catch (error) {
this.logger.error('@@@@ 查询专栏列表下拉选择失败:', error);
throw new InternalServerErrorException('查询专栏列表下拉选择失败');
}
}
// 查询专栏详情
async findOne(id: number) {
if (!id) {
throw new BadRequestException('id必填');
}
try {
const data = await this.specialColumnRepository.findOne({
where: { id, isDeleted: 0 },
});
delete data.isDeleted;
this.logger.log('@@@ 查询专栏详情', data);
return data;
} catch (error) {
this.logger.error('@@@@ 查询专栏详情失败:', error);
throw new InternalServerErrorException('查询专栏详情失败');
}
}
// 修改专栏
async update(
id: number,
updateSpecialColumnDto: UpdateSpecialColumnDto,
@Req() req,
) {
if (!id) {
throw new BadRequestException('id必填');
}
try {
// 专栏实体
const column = Object.assign(
new UpdateSpecialColumnDto(),
updateSpecialColumnDto,
);
const updatedColumn = setUpdatedUser(req, column);
this.logger.log('@@@ 修改专栏 updatedColumn', updatedColumn);
// 更新专栏表
await this.specialColumnRepository.update(id, updatedColumn);
// 更新文章专栏关联表
const relations = await this.articleColumnRelatedRepository.find({
where: { columnId: id },
});
for (const relation of relations) {
relation.columnTitle = updateSpecialColumnDto.title;
await this.articleColumnRelatedRepository.save(relation);
}
return { message: '修改成功' };
} catch (error) {
this.logger.error('@@@@ 修改专栏失败:', error);
throw new InternalServerErrorException('修改专栏失败');
}
}
// 软删除文章专栏关联表
async softDeleteArticleColumnRelations(columnId: number) {
const relations = await this.articleColumnRelatedRepository.find({
where: { columnId, isDeleted: 0 },
});
for (const relation of relations) {
relation.isDeleted = 1; // 标记为已删除
await this.articleColumnRelatedRepository.save(relation);
}
}
// 软删除单个专栏
async softDeleteColumn(id: number, @Req() req) {
if (!id) {
throw new BadRequestException('id必填');
}
try {
const column = new SpecialColumn();
const removedColumn = setDeletedUser(req, column);
this.logger.log('@@@ 软删除单个专栏 removedColumn', removedColumn);
await this.specialColumnRepository.update(id, removedColumn);
// 软删除文章专栏关联表
await this.softDeleteArticleColumnRelations(id);
return { message: '删除成功' };
} catch (error) {
this.logger.error('@@@@ 删除专栏失败:', error);
throw new InternalServerErrorException('删除专栏失败');
}
}
// 根据专栏id查询文章列表
async queryArticleListByColumnId(id: number, query: ListArticleColumnDto) {
if (!id) {
throw new BadRequestException('专栏ID不能为空');
}
try {
// 查询关联中间表对应的articleIds
const articleIdsMap = await this.articleColumnRelatedRepository.find({
select: ['articleId'],
where: {
columnId: id,
isDeleted: 0,
},
});
const articleIds = articleIdsMap.map((item) => item.articleId);
// 分页查询文章表
const [data, total] = await this.articleRepository.findAndCount({
where: {
id: In(articleIds),
title: query.articleTitle ? Like(`%${query.articleTitle}%`) : null,
isDeleted: 0,
},
order: {
id: 'DESC',
},
skip: (query.pageNum - 1) * query.pageSize,
take: query.pageSize,
});
return {
list: removeUnnecessaryData(data),
pageNum: Number(query.pageNum),
pageSize: Number(query.pageSize),
total,
};
} catch (error) {
this.logger.error('@@@@ 根据专栏id查询文章列表:', error);
throw new InternalServerErrorException('列表查询失败');
}
}
// 查询非该专栏的文章列表
async queryOtherArticleListByColumnId(
id: number,
query: ListArticleColumnDto,
) {
if (!id) {
throw new BadRequestException('专栏ID不能为空');
}
try {
// 查询关联中间表对应的articleIds 取反逻辑
const articleIdsMap = await this.articleColumnRelatedRepository.find({
select: ['articleId'],
where: {
columnId: id,
isDeleted: 0,
},
});
const articleIds = articleIdsMap.map((item) => item.articleId);
// 分页查询文章表
const [data, total] = await this.articleRepository.findAndCount({
where: {
id: Not(In(articleIds)),
title: query.articleTitle ? Like(`%${query.articleTitle}%`) : null,
isDeleted: 0,
},
order: {
id: 'DESC',
},
skip: (query.pageNum - 1) * query.pageSize,
take: query.pageSize,
});
return {
list: removeUnnecessaryData(data),
pageNum: Number(query.pageNum),
pageSize: Number(query.pageSize),
total,
};
} catch (error) {
this.logger.error('@@@@ 查询非该专栏的文章列表:', error);
throw new InternalServerErrorException('列表查询失败');
}
}
// 批量取消收录至该专栏
async cancelArticleInclusion(body: ArticleColumnInclusionDto) {
try {
await this.articleColumnRelatedRepository.update(
{
columnId: body.columnId,
articleId: In(body.articleIds),
},
{
isDeleted: 1,
},
);
return {
message: '取消收录成功',
};
} catch (error) {
this.logger.error('取消收录失败:', error);
throw new InternalServerErrorException('取消收录失败');
}
}
// 批量添加新的文章至专栏
async batchAddNewInclusion(articleIds, columnId) {
return await Promise.all(
articleIds.map(async (articleId) => {
const column = await this.specialColumnRepository.findOneBy({
id: columnId,
});
const article = await this.articleRepository.findOneBy({
id: articleId,
});
const newRelation = new ArticleColumnRelated();
newRelation.articleId = articleId;
newRelation.articleTitle = article.title;
newRelation.columnId = columnId;
newRelation.columnTitle = column.title;
return await this.articleColumnRelatedRepository.save(newRelation);
}),
);
}
// 文章批量收录至该专栏
async addArticleInclusion(body: ArticleColumnInclusionDto) {
try {
// 判断是否已经收录过了但被软删除过的
const hasInclusionArticles =
await this.articleColumnRelatedRepository.find({
select: ['articleId'],
where: {
columnId: body.columnId,
},
});
// 已经收录的文章ID
const hasInclusionArticleIds = hasInclusionArticles.map(
(item) => item.articleId,
);
// 判断收录的文章id,哪些是新的(执行添加),哪些是已经收录的(修改isDeleted)
// 新添加的
const newArticleIds = body.articleIds.filter(
(id) => !hasInclusionArticleIds.includes(id),
);
const oldArticleIds = body.articleIds.filter((id) =>
hasInclusionArticleIds.includes(id),
);
// 修改isDeleted为0
await this.articleColumnRelatedRepository.update(
{
columnId: body.columnId,
articleId: In(oldArticleIds),
},
{
isDeleted: 0,
},
);
// 添加新
await this.batchAddNewInclusion(newArticleIds, body.columnId);
return {
message: '文章批量收录成功',
};
} catch (error) {
this.logger.error('文章批量收录至该专栏失败:', error);
throw new InternalServerErrorException('文章批量收录至该专栏失败');
}
}
}
专栏模块 SpecialColumnModule
import { Module } from '@nestjs/common';
import { SpecialColumnService } from './special-column.service';
import { SpecialColumnController } from './special-column.controller';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Article } from 'src/article/entities/article.entity';
import { ArticleColumnRelated } from 'src/article/entities/article-column-related.entity';
import { SpecialColumn } from './entities/special-column.entity';
import { Role } from 'src/user/entities/role.entity';
@Module({
imports: [
TypeOrmModule.forFeature([
Article,
ArticleColumnRelated,
SpecialColumn,
Role,
]),
],
controllers: [SpecialColumnController],
providers: [SpecialColumnService],
exports: [SpecialColumnService],
})
export class SpecialColumnModule {}
专栏管理功能接口完成
目前完成了10个专栏相关的接口
实战合集地址
- NestJS实战-产品需求规划
- NestJS实战-前端搭建
- NestJS实战-后端开发-全局配置
- NestJS实战-后端开发-用户及权限模块
- NestJS实战-后端开发-文章专栏功能模块
- NestJS实战-前后端联调
- NestJS实战-系统总结