基于SpringAI的在线考试系统-考试系统开发流程案例
·
案例:创建考试功能
一、需求分析与领域建模
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流水线,实现自动构建、测试、部署
- 反馈收集:收集用户反馈,持续优化系统
六、流程图
创建考试流程
数据流程图
七、技术要点总结
1. DDD核心概念应用
- 聚合根:
Exam作为聚合根,维护考试相关数据的一致性 - 领域服务:
ExamDomainService封装考试相关的业务规则 - 领域事件:
ExamCreatedEvent用于通知其他系统考试创建事件 - 值对象:
ExamTime和ExamStatus作为值对象,提供值语义
2. 架构设计要点
- 分层架构:清晰的分层设计,职责明确
- 依赖倒置:上层依赖下层的接口,不依赖实现
- 接口隔离:每个接口只提供必要的方法
- 单一职责:每个类只负责一个功能
3. 性能优化要点
- 异步处理:使用
@Async注解异步处理非核心业务逻辑 - 缓存:使用Redis缓存热点数据,提高查询性能
- 批量操作:减少数据库操作次数,使用批量操作
4. 安全与可靠性要点
- 权限控制:使用
@RequiresPermissions注解控制访问权限 - 参数验证:使用
@Valid注解验证请求参数 - 事务管理:确保数据操作的原子性
- 异常处理:统一处理异常,返回友好的错误信息
5. 可维护性要点
- 代码规范:遵循编码规范,使用有意义的命名
- 文档:完善的代码注释和API文档
- 测试:完善的单元测试和集成测试
- 日志:详细的操作日志,便于问题排查
更多推荐


所有评论(0)