本文属于 NestJS BBS 实战教程,共 11 节,建议按顺序学习。
1. 这节课要实现的功能是什么
- 实现论坛核心三域:
boards、threads、replies。 - 支持发帖、回帖、按板块分页看主题。
- 用领域模块拆分,保持结构可扩展。
2. 这节课涉及的 NestJS 知识点有哪些(先讲概念)
2.1 为什么要按领域拆模块
BBS 的核心复杂度不在“单个接口”,而在“多个业务对象关系”。按领域拆模块可以避免所有逻辑堆进一个大 Service。
2.2 模块间依赖如何设计
ThreadsModule依赖BoardsModule(发帖前要校验板块存在);RepliesModule依赖ThreadsModule(回帖前要校验主题存在)。
2.3 分页与排序为什么必须做 DTO 约束
分页参数是外部输入,必须做范围和枚举约束,否则容易引发性能问题或异常排序行为。
2.4 领域服务要做什么
领域服务负责:
- 业务规则(例如回复后增加
replyCount); - 查询策略(最新/最热);
- 领域一致性(先验证依赖实体存在)。
3. 实际开发中这些 NestJS 知识点被如何使用(详细代码)
3.1 src/boards/boards.module.ts / boards.service.ts - 板块域
@Module({
controllers: [BoardsController],
providers: [BoardsService],
exports: [BoardsService],
})
export class BoardsModule {}
@Injectable()
export class BoardsService {
private readonly boards = [
{ id: 1, name: 'General', description: 'General discussion', createdAt: new Date() },
{ id: 2, name: 'Tech', description: 'Technology sharing', createdAt: new Date() },
];
findById(id: number) {
const board = this.boards.find((item) => item.id === id);
if (!board) throw new NotFoundException('Board not found');
return board;
}
}
3.2 src/threads/dto/list-threads-query.dto.ts - 分页与排序约束
import { Type } from 'class-transformer';
import { IsIn, IsInt, IsOptional, Max, Min } from 'class-validator';
export class ListThreadsQueryDto {
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
page = 1;
@IsOptional()
@Type(() => Number)
@IsInt()
@Min(1)
@Max(50)
pageSize = 10;
@IsOptional()
@IsIn(['latest', 'hot'])
sort: 'latest' | 'hot' = 'latest';
}
3.3 src/threads/threads.controller.ts - 发帖与列表
@Controller('boards/:boardId/threads')
export class ThreadsController {
@Post()
@UseGuards(JwtAuthGuard)
create(
@Param('boardId', ParseIntPipe) boardId: number,
@CurrentUser() author: AuthUser,
@Body() dto: CreateThreadDto,
) {
return this.threadsService.create(boardId, author, dto);
}
@Get()
list(
@Param('boardId', ParseIntPipe) boardId: number,
@Query() query: ListThreadsQueryDto,
) {
return this.threadsService.list(boardId, query);
}
}
3.4 src/threads/threads.service.ts - 领域规则与分页
create(boardId: number, author: AuthUser, dto: CreateThreadDto): ThreadEntity {
this.boardsService.findById(boardId);
const thread: ThreadEntity = {
id: this.nextId++,
boardId,
authorId: author.userId,
authorName: author.username,
title: dto.title.trim(),
content: dto.content.trim(),
replyCount: 0,
createdAt: new Date(),
};
this.threads.push(thread);
return thread;
}
list(boardId: number, query: ListThreadsQueryDto) {
this.boardsService.findById(boardId);
const source = this.threads.filter((item) => item.boardId === boardId);
const sorted = [...source].sort((left, right) =>
query.sort === 'hot'
? right.replyCount - left.replyCount
: right.createdAt.getTime() - left.createdAt.getTime(),
);
const start = (query.page - 1) * query.pageSize;
return {
items: sorted.slice(start, start + query.pageSize),
page: query.page,
pageSize: query.pageSize,
total: source.length,
};
}
3.5 src/replies/replies.service.ts - 回帖后维护主题统计
create(threadId: number, author: AuthUser, dto: CreateReplyDto): ReplyEntity {
this.threadsService.findById(threadId);
const reply: ReplyEntity = {
id: this.nextId++,
threadId,
authorId: author.userId,
authorName: author.username,
content: dto.content.trim(),
createdAt: new Date(),
};
this.replies.push(reply);
this.threadsService.increaseReplyCount(threadId);
return reply;
}
4. 总结这节课知识点
- 你建立了论坛核心域模型和模块协作关系。
- 你掌握了分页/排序 DTO 与领域规则的组合方式。
- 你现在的项目已经具备“可继续扩展业务”的骨架。
5. 每节课分支
lesson-07-forum-domain- 远程仓库:
https://github.com/Shiuwn/nestjs-tutorial.git