文章模块开发
使用快捷键新建 article 模块:
css
nest g res article
# 默认选择 REST API
根据文章表配置实体 article.entity.ts 如下:
less
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 如下:
typescript
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 代码如下:
less
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 代码如下:
less
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 代码如下:
scala
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 代码如下:
less
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
kotlin
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
python
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 {}