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
相关产品推荐
相关产品推荐

