从"考生参加考试"看DDD四层架构:把业务需求拆成可落地的代码逻辑
在企业级在线考试系统中,"考生参加考试并提交答卷"是核心场景之一。我们用DDD四层架构,把用户的自然语言需求,转化为"业务规则→流程→接口→技术实现"的完整链路。
一、先明确:用户的"自然语言业务需求"是什么?
这是领域层的输入源,必须先把用户的口语化需求,提炼成清晰的业务规则:
- 考生只能参加"已发布且未结束"的考试;
- 考试开始后,系统自动加载对应试卷,且试卷内容不可修改;
- 考生答题过程中,系统每5分钟自动保存一次答案;
- 考生主动交卷/考试时间到,系统自动计算客观题分数,主观题进入阅卷队列;
- 考试结束后,考生能立即看到客观题得分和错题提示。
二、领域层:把"自然语言规则"转化为业务代码
领域层是业务规则的载体,我们要把上面的需求,封装成实体、聚合根、领域服务:
1. 核心领域实体/聚合根设计
以Exam(考试)为聚合根(因为考试是该场景的核心载体),包含关联的实体/值对象:
java
// 聚合根:Exam(考试)
@AggregateRoot
@Data
public class Exam {
@Identifier
private Long id;
private String title; // 考试名称
private Long paperId; // 关联试卷ID
private LocalDateTime startTime; // 开始时间
private LocalDateTime endTime; // 结束时间
private ExamStatus status; // 状态:DRAFT(草稿)/PUBLISHED(已发布)/FINISHED(已结束)
// 值对象:封装"考试配置"的业务规则
@Embedded
private ExamConfig config;
// 业务规则1:判断考生是否能参加考试
public boolean isJoinable() {
LocalDateTime now = LocalDateTime.now();
return this.status == ExamStatus.PUBLISHED
&& now.isAfter(this.startTime)
&& now.isBefore(this.endTime);
}
// 业务规则2:考试开始后不能修改试卷
public boolean canModifyPaper() {
return LocalDateTime.now().isBefore(this.startTime);
}
}
// 值对象:ExamConfig(考试配置)
@ValueObject
@Data
public class ExamConfig {
private Integer autoSaveInterval; // 自动保存间隔(分钟)
// 业务规则3:获取自动保存的时间间隔
public Duration getAutoSaveDuration() {
return Duration.ofMinutes(this.autoSaveInterval);
}
}
// 实体:AnswerSheet(考生答卷)
@Entity
@Data
public class AnswerSheet {
@Id
private Long id;
private Long examId; // 关联考试ID
private Long userId; // 关联考生ID
private Map<Long, String> answers; // 题目ID→考生答案
private LocalDateTime lastSaveTime; // 最后自动保存时间
private BigDecimal objectiveScore; // 客观题得分
// 业务规则4:判断是否需要自动保存
public boolean needAutoSave(ExamConfig examConfig) {
LocalDateTime nextSaveTime = this.lastSaveTime.plus(examConfig.getAutoSaveDuration());
return LocalDateTime.now().isAfter(nextSaveTime);
}
}
2. 领域服务:跨聚合的业务规则封装
如果业务需要多个聚合协同(比如"计算客观题分数"需要AnswerSheet+Question聚合),用领域服务封装:
java
// 领域服务:AnswerSheetDomainService
@Service
public class AnswerSheetDomainService {
// 依赖Question聚合的仓储(接口,由基础设施层实现)
private final QuestionRepository questionRepository;
// 业务规则4:计算客观题分数
public BigDecimal calculateObjectiveScore(AnswerSheet answerSheet) {
List<Question> questions = questionRepository.findByExamId(answerSheet.getExamId());
BigDecimal score = BigDecimal.ZERO;
for (Question question : questions) {
if (question.getType() == QuestionType.OBJECTIVE) { // 只算客观题
String correctAnswer = question.getCorrectAnswer();
String userAnswer = answerSheet.getAnswers().get(question.getId());
if (correctAnswer.equals(userAnswer)) {
score = score.add(question.getScore());
}
}
}
answerSheet.setObjectiveScore(score);
return score;
}
}
三、应用层:把"业务规则"编排成"用户流程"
应用层不写业务规则,只负责把领域层的能力串成用户需要的操作流程(对应"考生参加考试"的完整步骤):
java
// 应用服务:ExamApplicationService
@Service
public class ExamApplicationService {
// 依赖领域层的仓储和服务
private final ExamRepository examRepository;
private final AnswerSheetRepository answerSheetRepository;
private final AnswerSheetDomainService answerSheetDomainService;
private final MessageProducer messageProducer; // 消息发送(基础设施层提供)
// 编排"考生参加考试并提交答卷"的流程
public ExamResultDTO participateAndSubmit(Long examId, Long userId, Map<Long, String> answers) {
// 步骤1:校验考试是否可参加(调用领域层规则)
Exam exam = examRepository.findById(examId)
.orElseThrow(() -> new BusinessException("考试不存在"));
if (!exam.isJoinable()) {
throw new BusinessException("当前考试不可参加");
}
// 步骤2:创建/更新答卷(调用领域层实体)
AnswerSheet answerSheet = answerSheetRepository.findByExamIdAndUserId(examId, userId)
.orElse(new AnswerSheet(examId, userId));
answerSheet.setAnswers(answers);
answerSheet.setLastSaveTime(LocalDateTime.now());
// 步骤3:计算客观题分数(调用领域服务)
BigDecimal objectiveScore = answerSheetDomainService.calculateObjectiveScore(answerSheet);
answerSheetRepository.save(answerSheet);
// 步骤4:发送"主观题阅卷"消息(调用基础设施层能力)
messageProducer.sendSubjectiveMarkingMessage(answerSheet.getId());
// 步骤5:组装结果返回
return ExamResultDTO.builder()
.examId(examId)
.userId(userId)
.objectiveScore(objectiveScore)
.build();
}
}
四、接口层:把"流程"包装成"对外服务"
接口层是系统的对外入口,负责把应用层的流程,暴露为前端/其他系统可调用的形式(比如RESTful API):
java
// 接口层:ExamController
@RestController
@RequestMapping("/api/exam")
public class ExamController {
private final ExamApplicationService examApplicationService;
// 对外提供"参加考试并提交答卷"的API
@PostMapping("/{examId}/submit")
public Result<ExamResultDTO> submitExam(
@PathVariable Long examId,
@RequestParam Long userId,
@RequestBody Map<Long, String> answers) {
ExamResultDTO result = examApplicationService.participateAndSubmit(examId, userId, answers);
return Result.success(result);
}
}
五、基础设施层:把"业务逻辑"落地为"技术实现"
基础设施层是纯技术细节,负责实现领域层/应用层需要的技术能力(数据库、消息队列等):
1. 仓储的技术实现(比如MySQL)
java
// 基础设施层:ExamRepository的MySQL实现
@Repository
public class ExamRepositoryImpl implements ExamRepository {
private final JdbcTemplate jdbcTemplate;
@Override
public Optional<Exam> findById(Long id) {
String sql = "SELECT * FROM exam WHERE id = ?";
return jdbcTemplate.query(sql, new Object[]{id}, rs -> {
if (rs.next()) {
Exam exam = new Exam();
exam.setId(rs.getLong("id"));
exam.setTitle(rs.getString("title"));
// 其他字段赋值...
return Optional.of(exam);
}
return Optional.empty();
});
}
@Override
public void save(Exam exam) {
// 实现MySQL的插入/更新逻辑
}
}
2. 消息队列的技术实现(比如RabbitMQ)
java
// 基础设施层:MessageProducer的RabbitMQ实现
@Service
public class RabbitMQMessageProducer implements MessageProducer {
private final RabbitTemplate rabbitTemplate;
@Override
public void sendSubjectiveMarkingMessage(Long answerSheetId) {
rabbitTemplate.convertAndSend(
"subjective-marking-exchange",
"subjective.marking",
answerSheetId
);
}
}
六、总结:四层架构的"职责边界"
通过"考生参加考试"场景,我们能清晰看到DDD四层的分工:
| 层级 | 角色 | 核心产出物 |
|---|---|---|
| 领域层 | 业务规则的"翻译器" | 实体、聚合根、领域服务 |
| 应用层 | 业务流程的"串线工" | 应用服务(流程编排) |
| 接口层 | 对外服务的"窗口" | RESTful API/消息处理器 |
| 基础设施层 | 技术实现的"工具箱" | 仓储实现、消息队列集成、外部接口适配 |
这样的分层设计,既能保证业务规则不被技术细节污染 (领域层独立),又能让流程和技术实现灵活替换(比如把MySQL换成MongoDB,只改基础设施层),是企业级系统"易维护、易扩展"的关键。