白留白WHITE SPACE
返回所有文章
NestJS / 后端开发

NestJS 实战 07:BBS 核心域模型(板块、主题、回复)

实现论坛核心三域:boards、threads、replies。支持发帖、回帖、按板块分页看主题。用领域模块拆分,保持结构可扩展。

本文属于 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

返回课程目录

上一课:Guard、Decorator 与 RBAC 权限控制

下一课:Interceptor 与 Exception Filter 统一响应风格

— 感谢阅读 —