基于SpringAI的在线考试系统-企业级软件研发工程应用规范实现细节

从"考生参加考试"看DDD四层架构:把业务需求拆成可落地的代码逻辑

在企业级在线考试系统中,"考生参加考试并提交答卷"是核心场景之一。我们用DDD四层架构,把用户的自然语言需求,转化为"业务规则→流程→接口→技术实现"的完整链路。

一、先明确:用户的"自然语言业务需求"是什么?

这是领域层的输入源,必须先把用户的口语化需求,提炼成清晰的业务规则:

  1. 考生只能参加"已发布且未结束"的考试;
  2. 考试开始后,系统自动加载对应试卷,且试卷内容不可修改;
  3. 考生答题过程中,系统每5分钟自动保存一次答案;
  4. 考生主动交卷/考试时间到,系统自动计算客观题分数,主观题进入阅卷队列;
  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,只改基础设施层),是企业级系统"易维护、易扩展"的关键。

相关推荐
lly2024062 小时前
SQL SELECT 语句详解
开发语言
5***b972 小时前
Spring Boot--@PathVariable、@RequestParam、@RequestBody
java·spring boot·后端
qq_318121592 小时前
Java大厂面试故事:Spring Boot、微服务与AI场景深度解析
java·spring boot·redis·微服务·ai·kafka·spring security
superman超哥2 小时前
Rust 异步时间管理核心:Tokio 定时器实现机制深度剖析
开发语言·rust·编程语言·rust异步时间管理核心·tokio定时器实现机制·tokio定时器
朔北之忘 Clancy2 小时前
2025 年 9 月青少年软编等考 C 语言一级真题解析
c语言·开发语言·c++·学习·数学·青少年编程·题解
Hello.Reader2 小时前
Flink State Processor API 读写/修复 Savepoint,把“状态”当成可查询的数据
大数据·flink
玛丽莲茼蒿2 小时前
javaSE 集合框架(五)——java 8新品Stream类
java·开发语言
wjs20242 小时前
SQLite Glob 子句详解
开发语言
木风小助理3 小时前
Elasticsearch生产环境最佳实践指南
大数据·elasticsearch·搜索引擎