You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

NestJs开发应选角色分离还是服务分离架构方案?

方案结论

最优实现不是二选一,而是按业务领域聚合单一考试模块+内部做角色权限隔离+DTO继承复用,完全符合NestJS官方推荐的模块划分原则,既不会因为拆分过细产生冗余,也不会把权限和业务逻辑混写导致后续维护困难。

为什么不推荐拆成两个独立Module
  • 两个接口核心都围绕「考试」这同一个业务领域,硬拆成教师考试、学生考试两个独立模块,会直接割裂同领域代码的内聚性:后续考试相关的数据库实体定义、通用校验逻辑(比如考试存在性校验、考试时间合法性校验)都要在两个模块重复维护,改一处逻辑要动两个地方,长期维护成本极高。
  • 两类接口DTO高度相似,拆模块会导致类型定义、校验规则重复编写,哪怕额外抽公共文件维护,也会增加不必要的层级复杂度。
  • NestJS模块划分的核心维度是业务领域边界,不是用户角色边界,按角色拆模块属于典型的划分维度错误,就像做电商系统硬拆成买家模块、卖家模块,最后订单、商品逻辑被拆得七零八落,找代码都要翻半天。
为什么不推荐只靠RBAC混写逻辑
  • 只靠路由级RBAC守卫做隔离,很容易出现权限漏配的风险,比如误把创建考试接口开放给学生角色。如果只写一套DTO做校验,要么会允许学生传入answer字段留下数据篡改隐患,要么要在DTO里写一堆条件判断区分角色,代码可读性极差。
  • 教师创建考试、学生参加考试是完全独立的两个业务动作,后续迭代方向大概率分叉:比如创建考试后续要加题目难度标记、题库关联、分值配置,参加考试要加答题计时、切屏检测、答案暂存,把两个动作的逻辑混在同一个Service方法里,代码只会越写越臃肿。
推荐的标准实现结构

直接创建单一的ExamModule,把考试领域的所有逻辑收敛在这个模块内,内部按职责分层即可:

1. DTO层用继承做复用,严格区分场景校验规则

先抽公共基础字段,再针对不同角色扩展专属字段,完全避免重复定义:

// 公共基础题目DTO
class BaseQuestionDto {
  @IsString()
  question: string;
}

// 公共基础考试DTO
class BaseExamDto {
  @IsString()
  name: string;
  @IsString()
  time: string;
  @ValidateNested({ each: true })
  @Type(() => BaseQuestionDto)
  qalist: BaseQuestionDto[];
}

// 教师创建考试用DTO,扩展答案字段
class CreateExamDto extends BaseExamDto {
  @ValidateNested({ each: true })
  @Type(() => QuestionWithAnswerDto)
  qalist: QuestionWithAnswerDto[];
}

class QuestionWithAnswerDto extends BaseQuestionDto {
  @IsString()
  answer: string;
}

// 学生参加考试用DTO,直接复用基础字段,天然不接收answer参数
class TakeExamDto extends BaseExamDto {}

2. Controller层拆分独立路由,挂载RBAC守卫和校验管道

不要把两个动作塞到同一个路由处理方法里,分开写路由,绑定对应的角色权限、DTO校验规则:

@Controller('exam')
export class ExamController {
  constructor(private readonly examService: ExamService) {}

  // 教师创建考试接口
  @Post('create')
  @Roles('teacher') // 自定义角色装饰器+对应守卫,仅教师角色可访问
  @UsePipes(new ValidationPipe({ whitelist: true })) // 自动过滤DTO未定义的字段,就算学生恶意传answer也会被自动剔除
  createExam(@Body() createExamDto: CreateExamDto) {
    return this.examService.create(createExamDto);
  }

  // 学生参加考试接口
  @Post('take')
  @Roles('student') // 仅学生角色可访问
  @UsePipes(new ValidationPipe({ whitelist: true }))
  takeExam(@Body() takeExamDto: TakeExamDto, @Req() req) {
    return this.examService.take(takeExamDto, req.user.id);
  }
}

3. Service层拆分独立方法处理业务逻辑

把创建考试、参加考试的逻辑拆成两个独立的Service方法,不要混写,后续各自迭代互不影响;通用逻辑(比如校验考试是否存在、时间是否在有效窗口)抽成Service内部私有方法复用即可。

如果后续考试模块逻辑膨胀到很大体量(比如加上阅卷、成绩统计、题库管理等子功能),可以再把ExamModule升级为领域模块,内部再拆分教师侧、学生侧的子模块,项目初期完全没必要做过度拆分,单一领域模块的结构已经足够清晰。

内容的提问来源于stack exchange,提问作者jdwins11

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 22:21:29