案例:创建考试功能

一、需求分析与领域建模

1. 需求收集
  • 功能需求
    • 支持创建考试,设置考试名称、时间、时长等基本信息
    • 支持选择试卷,关联考试和试卷
    • 支持设置考试状态(草稿、发布、进行中、已结束)
    • 支持设置参考人员范围
  • 非功能需求
    • 响应时间:创建考试操作响应时间不超过1秒
    • 可靠性:考试数据不丢失
    • 安全性:只有有权限的用户才能创建考试
2. 领域分析
  • 核心领域:考试管理
  • 支撑领域:试卷管理、用户管理
  • 通用领域:认证授权、日志记录
3. 概念建模
  • 实体
    • Exam:考试实体,包含考试基本信息
    • Paper:试卷实体,包含试卷基本信息和题目
  • 值对象
    • ExamTime:考试时间,包含开始时间和结束时间
    • ExamStatus:考试状态,枚举类型
  • 聚合根
    • Exam:考试聚合根,作为外部访问的入口
    • Paper:试卷聚合根
  • 领域服务
    • ExamDomainService:考试领域服务,处理考试相关的业务规则
  • 领域事件
    • ExamCreatedEvent:考试创建事件,用于通知其他系统
4. 边界划分
  • 限界上下文
    • 考试管理上下文:负责考试的创建、管理和监控
    • 试卷管理上下文:负责试卷的创建、管理和组卷
    • 用户管理上下文:负责用户的认证、授权和管理

二、架构设计

1. 技术选型
  • 后端:Spring Boot 3.x、MyBatis Plus、Redis
  • 前端:Vue 3、Element Plus、Vite
  • 数据库:MySQL
  • 认证:JWT
2. 架构风格
  • 分层架构
    • 接口层(API):处理HTTP请求和响应
    • 应用层(Application):协调领域对象完成业务用例
    • 领域层(Domain):包含核心业务逻辑和规则
    • 基础设施层(Infrastructure):提供技术支持服务
3. 模块划分
  • exam-system-api:接口层,包含控制器和DTO
  • exam-system-application:应用层,包含应用服务
  • exam-system-domain:领域层,包含实体、值对象、聚合根、领域服务和领域事件
  • exam-system-infrastructure:基础设施层,包含仓储实现和工具服务
4. 集成设计
  • 服务间调用:使用RESTful API
  • 数据传输:使用DTO对象
  • 事件通知:使用Spring Event或消息队列

三、详细设计与实现

1. 领域层实现
实体设计
// Exam.java - 考试实体
public class Exam implements AggregateRoot<Long> {
    private Long id;
    private String name;
    private String description;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Integer duration; // 考试时长(分钟)
    private Long paperId;
    private ExamStatus status;
    private Long creatorId;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    
    // 构造方法、getter和setter
    // 业务方法
    public void publish() {
        if (status != ExamStatus.DRAFT) {
            throw new BusinessException("只有草稿状态的考试才能发布");
        }
        status = ExamStatus.PUBLISHED;
    }
    
    public void start() {
        if (status != ExamStatus.PUBLISHED) {
            throw new BusinessException("只有已发布的考试才能开始");
        }
        status = ExamStatus.ONGOING;
    }
    
    public void end() {
        if (status != ExamStatus.ONGOING) {
            throw new BusinessException("只有进行中的考试才能结束");
        }
        status = ExamStatus.FINISHED;
    }
}
值对象设计
// ExamStatus.java - 考试状态枚举
public enum ExamStatus {
    DRAFT("草稿"),
    PUBLISHED("已发布"),
    ONGOING("进行中"),
    FINISHED("已结束");
    
    private String description;
    
    ExamStatus(String description) {
        this.description = description;
    }
    
    public String getDescription() {
        return description;
    }
}
领域服务设计
// ExamDomainService.java - 考试领域服务
public class ExamDomainService {
    private final ExamRepository examRepository;
    private final PaperRepository paperRepository;
    
    public ExamDomainService(ExamRepository examRepository, PaperRepository paperRepository) {
        this.examRepository = examRepository;
        this.paperRepository = paperRepository;
    }
    
    public void validateExam(Exam exam) {
        // 验证考试名称不能为空
        if (StringUtils.isBlank(exam.getName())) {
            throw new BusinessException("考试名称不能为空");
        }
        
        // 验证考试时间有效性
        if (exam.getEndTime().isBefore(exam.getStartTime())) {
            throw new BusinessException("考试结束时间不能早于开始时间");
        }
        
        // 验证试卷是否存在
        if (exam.getPaperId() != null) {
            Paper paper = paperRepository.findById(exam.getPaperId());
            if (paper == null) {
                throw new BusinessException("试卷不存在");
            }
        }
    }
}
领域事件设计
// ExamCreatedEvent.java - 考试创建事件
public class ExamCreatedEvent {
    private final Long examId;
    private final String examName;
    private final Long creatorId;
    private final LocalDateTime createTime;
    
    public ExamCreatedEvent(Long examId, String examName, Long creatorId, LocalDateTime createTime) {
        this.examId = examId;
        this.examName = examName;
        this.creatorId = creatorId;
        this.createTime = createTime;
    }
    
    // getter方法
}
2. 应用层实现
命令设计
// ExamCreateReq.java - 创建考试请求DTO
public class ExamCreateReq {
    private String name;
    private String description;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Integer duration;
    private Long paperId;
    private List<Long> participantIds; // 参考人员ID列表
    
    // getter和setter方法
}
应用服务设计
// ExamAppService.java - 考试应用服务接口
public interface ExamAppService {
    Long createExam(ExamCreateReq req);
    ExamResp getExamById(Long id);
    PageResult<ExamListResp> listExams(ExamListReq req);
    boolean updateExam(Long id, ExamUpdateReq req);
    boolean deleteExam(Long id);
}
应用服务实现
// ExamAppServiceImpl.java - 考试应用服务实现
@Service
public class ExamAppServiceImpl implements ExamAppService {
    private final ExamRepository examRepository;
    private final PaperRepository paperRepository;
    private final ExamDomainService examDomainService;
    private final ApplicationEventPublisher eventPublisher;
    private final RedisUtils redisUtils;
    private final AsyncService asyncService;
    
    @Autowired
    public ExamAppServiceImpl(ExamRepository examRepository, PaperRepository paperRepository,
                             ExamDomainService examDomainService, ApplicationEventPublisher eventPublisher,
                             RedisUtils redisUtils, AsyncService asyncService) {
        this.examRepository = examRepository;
        this.paperRepository = paperRepository;
        this.examDomainService = examDomainService;
        this.eventPublisher = eventPublisher;
        this.redisUtils = redisUtils;
        this.asyncService = asyncService;
    }
    
    @Override
    public Long createExam(ExamCreateReq req) {
        // 1. 构建考试实体
        Exam exam = new Exam();
        exam.setName(req.getName());
        exam.setDescription(req.getDescription());
        exam.setStartTime(req.getStartTime());
        exam.setEndTime(req.getEndTime());
        exam.setDuration(req.getDuration());
        exam.setPaperId(req.getPaperId());
        exam.setStatus(ExamStatus.DRAFT);
        exam.setCreatorId(UserContextUtils.getUserId());
        exam.setCreateTime(LocalDateTime.now());
        exam.setUpdateTime(LocalDateTime.now());
        
        // 2. 验证考试数据
        examDomainService.validateExam(exam);
        
        // 3. 保存考试
        Long examId = examRepository.save(exam);
        
        // 4. 处理参考人员(这里简化处理,实际可能需要关联表)
        if (req.getParticipantIds() != null && !req.getParticipantIds().isEmpty()) {
            // 保存参考人员信息
        }
        
        // 5. 发布领域事件
        eventPublisher.publishEvent(new ExamCreatedEvent(examId, exam.getName(), 
                                                       exam.getCreatorId(), exam.getCreateTime()));
        
        // 6. 异步记录操作日志
        asyncService.recordOperationLog("创建考试", "创建了考试:" + exam.getName(), 
                                       UserContextUtils.getUserId());
        
        return examId;
    }
    
    // 其他方法实现...
}
3. 基础设施层实现
仓储接口设计
// ExamRepository.java - 考试仓储接口
public interface ExamRepository {
    Long save(Exam exam);
    Exam findById(Long id);
    boolean update(Exam exam);
    boolean delete(Long id);
    List<Exam> findByCondition(ExamQuery query);
    int countByCondition(ExamQuery query);
}
仓储实现
// ExamRepositoryImpl.java - 考试仓储实现
@Repository
public class ExamRepositoryImpl implements ExamRepository {
    private final ExamMapper examMapper;
    
    @Autowired
    public ExamRepositoryImpl(ExamMapper examMapper) {
        this.examMapper = examMapper;
    }
    
    @Override
    public Long save(Exam exam) {
        examMapper.insert(exam);
        return exam.getId();
    }
    
    @Override
    public Exam findById(Long id) {
        return examMapper.selectById(id);
    }
    
    // 其他方法实现...
}
工具服务
// RedisUtils.java - Redis工具服务
@Component
public class RedisUtils {
    private final StringRedisTemplate stringRedisTemplate;
    
    @Autowired
    public RedisUtils(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    
    public void set(String key, Object value, long expireTime) {
        stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(value), expireTime, TimeUnit.SECONDS);
    }
    
    public <T> T get(String key, Class<T> clazz) {
        String value = stringRedisTemplate.opsForValue().get(key);
        return JSON.parseObject(value, clazz);
    }
    
    public void delete(String key) {
        stringRedisTemplate.delete(key);
    }
}
4. 接口层实现
控制器设计
// ExamController.java - 考试控制器
@RestController
@RequestMapping("/api/exam")
public class ExamController {
    private final ExamAppService examAppService;
    
    @Autowired
    public ExamController(ExamAppService examAppService) {
        this.examAppService = examAppService;
    }
    
    @PostMapping("/create")
    @RequiresPermissions("exam:create")
    public Result<Long> createExam(@Valid @RequestBody ExamCreateReq req) {
        Long examId = examAppService.createExam(req);
        return Result.success(examId);
    }
    
    @GetMapping("/{id}")
    @RequiresPermissions("exam:view")
    public Result<ExamResp> getExamById(@PathVariable Long id) {
        ExamResp exam = examAppService.getExamById(id);
        return Result.success(exam);
    }
    
    @GetMapping("/list")
    @RequiresPermissions("exam:list")
    public Result<PageResult<ExamListResp>> listExams(ExamListReq req) {
        PageResult<ExamListResp> result = examAppService.listExams(req);
        return Result.success(result);
    }
    
    @PutMapping("/{id}")
    @RequiresPermissions("exam:update")
    public Result<Boolean> updateExam(@PathVariable Long id, @Valid @RequestBody ExamUpdateReq req) {
        boolean success = examAppService.updateExam(id, req);
        return Result.success(success);
    }
    
    @DeleteMapping("/{id}")
    @RequiresPermissions("exam:delete")
    public Result<Boolean> deleteExam(@PathVariable Long id) {
        boolean success = examAppService.deleteExam(id);
        return Result.success(success);
    }
}
响应DTO设计
// ExamResp.java - 考试响应DTO
public class ExamResp {
    private Long id;
    private String name;
    private String description;
    private LocalDateTime startTime;
    private LocalDateTime endTime;
    private Integer duration;
    private Long paperId;
    private String paperName;
    private String status;
    private String statusText;
    private Long creatorId;
    private String creatorName;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    
    // getter和setter方法
}

四、测试与验证

1. 单元测试
// ExamAppServiceImplTest.java - 考试应用服务单元测试
@SpringBootTest
public class ExamAppServiceImplTest {
    @Autowired
    private ExamAppService examAppService;
    
    @Test
    public void testCreateExam() {
        // 构建测试数据
        ExamCreateReq req = new ExamCreateReq();
        req.setName("测试考试");
        req.setDescription("这是一个测试考试");
        req.setStartTime(LocalDateTime.now().plusDays(1));
        req.setEndTime(LocalDateTime.now().plusDays(1).plusHours(2));
        req.setDuration(120);
        req.setPaperId(1L);
        
        // 执行测试
        Long examId = examAppService.createExam(req);
        
        // 验证结果
        assertNotNull(examId);
        assertTrue(examId > 0);
        
        // 验证考试是否创建成功
        ExamResp exam = examAppService.getExamById(examId);
        assertNotNull(exam);
        assertEquals("测试考试", exam.getName());
        assertEquals("草稿", exam.getStatusText());
    }
}
2. 集成测试
// ExamControllerTest.java - 考试控制器集成测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class ExamControllerTest {
    @Autowired
    private TestRestTemplate restTemplate;
    
    @Test
    public void testCreateExam() {
        // 构建测试数据
        ExamCreateReq req = new ExamCreateReq();
        req.setName("集成测试考试");
        req.setDescription("这是一个集成测试考试");
        req.setStartTime(LocalDateTime.now().plusDays(2));
        req.setEndTime(LocalDateTime.now().plusDays(2).plusHours(2));
        req.setDuration(120);
        req.setPaperId(1L);
        
        // 执行测试
        ResponseEntity<Result> response = restTemplate.postForEntity(
                "/api/exam/create", req, Result.class);
        
        // 验证结果
        assertEquals(HttpStatus.OK, response.getStatusCode());
        assertTrue(response.getBody().isSuccess());
        assertNotNull(response.getBody().getData());
    }
}
3. 系统测试
  • 功能测试:验证创建考试功能是否符合需求
  • 性能测试:验证创建考试操作响应时间是否不超过1秒
  • 安全测试:验证只有有权限的用户才能创建考试

五、部署与运维

1. 环境准备
  • 开发环境:本地开发机器,安装JDK、Maven、MySQL、Redis
  • 测试环境:测试服务器,配置与生产环境相似
  • 生产环境:生产服务器集群,高可用配置
2. 部署上线
  • 构建mvn clean package
  • 部署:将jar包上传到服务器,使用java -jar命令启动
  • 配置:通过环境变量或配置文件设置数据库连接、Redis连接等
3. 监控运维
  • 监控:使用Spring Boot Actuator和Prometheus监控系统运行状态
  • 日志:使用ELK Stack收集和分析日志
  • 告警:设置系统异常告警,及时处理问题
4. 持续迭代
  • 版本管理:使用Git进行版本控制
  • CI/CD:配置Jenkins流水线,实现自动构建、测试、部署
  • 反馈收集:收集用户反馈,持续优化系统

六、流程图

创建考试流程

异步服务 事件总线 仓储层 领域服务层 应用服务层 控制器层 前端 异步服务 事件总线 仓储层 领域服务层 应用服务层 控制器层 前端 POST /api/exam/create (ExamCreateReq) createExam(req) 构建Exam实体 validateExam(exam) 验证考试数据 save(exam) 保存考试数据到数据库 处理参考人员信息 发布ExamCreatedEvent 异步记录操作日志 返回examId 200 OK { "success": true, "data": 1 } 处理领域事件 异步执行日志记录

数据流程图

前端请求

控制器层接收请求

应用服务层处理业务逻辑

领域服务层验证业务规则

仓储层保存数据

数据库

事件总线发布领域事件

异步服务记录操作日志

事件处理器

日志系统

应用服务层返回结果

控制器层返回响应

前端展示结果

七、技术要点总结

1. DDD核心概念应用

  • 聚合根Exam作为聚合根,维护考试相关数据的一致性
  • 领域服务ExamDomainService封装考试相关的业务规则
  • 领域事件ExamCreatedEvent用于通知其他系统考试创建事件
  • 值对象ExamTimeExamStatus作为值对象,提供值语义

2. 架构设计要点

  • 分层架构:清晰的分层设计,职责明确
  • 依赖倒置:上层依赖下层的接口,不依赖实现
  • 接口隔离:每个接口只提供必要的方法
  • 单一职责:每个类只负责一个功能

3. 性能优化要点

  • 异步处理:使用@Async注解异步处理非核心业务逻辑
  • 缓存:使用Redis缓存热点数据,提高查询性能
  • 批量操作:减少数据库操作次数,使用批量操作

4. 安全与可靠性要点

  • 权限控制:使用@RequiresPermissions注解控制访问权限
  • 参数验证:使用@Valid注解验证请求参数
  • 事务管理:确保数据操作的原子性
  • 异常处理:统一处理异常,返回友好的错误信息

5. 可维护性要点

  • 代码规范:遵循编码规范,使用有意义的命名
  • 文档:完善的代码注释和API文档
  • 测试:完善的单元测试和集成测试
  • 日志:详细的操作日志,便于问题排查
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐