基于Spring AI的分布式在线考试系统-事件处理架构实现方案
工程结构:采用多模块Maven架构,公共模块抽离通用代码,各服务职责单一,符合微服务设计原则;核心流程:考试提交→MQ事件→AI评分→MQ事件→成绩通知,全程解耦、异步、幂等;关键技术Spring Event解耦服务内逻辑,RocketMQ解耦跨服务通信;Spring AI一键调用大模型,无需手写HTTP请求;Redis实现幂等校验,避免重复消费;可扩展性:支持切换MQ(RabbitMQ)、大模型
·
一、工程整体结构说明
基于上述分布式在线考试系统设计,演示可直接运行的最小化工程示例(聚焦核心的「考试提交→AI评分→通知」流程),工程采用多模块Maven结构,涵盖考试服务、评分服务、通知服务三大核心服务,以及公共模块(存放通用事件、DTO、配置)。
工程目录结构
exam-system/
├── exam-common/ // 公共模块:通用事件、DTO、工具类
│ ├── src/main/java/com/exam/common/
│ │ ├── event/ // 分布式事件父类+子类
│ │ ├── dto/ // 通用数据传输对象
│ │ ├── config/ // 通用配置(如MQ序列化)
│ │ └── util/ // 工具类(幂等校验、JSON处理)
│ └── pom.xml
├── exam-service/ // 考试服务(生产者)
│ ├── src/main/java/com/exam/service/
│ │ ├── ExamApplication.java
│ │ ├── controller/ // 接口层
│ │ ├── service/ // 业务层
│ │ ├── event/ // 本地事件
│ │ └── config/ // 服务配置(MQ、Nacos)
│ └── pom.xml
├── score-service/ // 评分服务(消费者+Spring AI)
│ ├── src/main/java/com/exam/score/
│ │ ├── ScoreApplication.java
│ │ ├── listener/ // MQ消费者+本地事件监听器
│ │ ├── service/ // 评分业务层
│ │ └── config/ // Spring AI、MQ配置
│ └── pom.xml
├── notify-service/ // 通知服务(消费者)
│ ├── src/main/java/com/exam/notify/
│ │ ├── NotifyApplication.java
│ │ ├── listener/ // MQ消费者
│ │ ├── service/ // 通知业务层
│ │ └── config/ // MQ配置
│ └── pom.xml
└── pom.xml // 父工程pom
二、核心依赖配置(父工程+各模块)
1. 父工程pom.xml(统一版本管理)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.2</version>
<relativePath/>
</parent>
<groupId>com.exam</groupId>
<artifactId>exam-system</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<modules>
<module>exam-common</module>
<module>exam-service</module>
<module>score-service</module>
<module>notify-service</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<spring-cloud-alibaba.version>2025.0.0.0</spring-cloud-alibaba.version>
<spring-ai.version>1.0.0</spring-ai.version>
<rocketmq-spring-boot.version>2.2.3</rocketmq-spring-boot.version>
<fastjson2.version>2.0.45</fastjson2.version>
</properties>
<dependencyManagement>
<dependencies>
<!-- Spring Cloud Alibaba 依赖管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Spring AI 依赖管理 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- RocketMQ Spring Boot -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>${rocketmq-spring-boot.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
2. exam-common模块pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.exam</groupId>
<artifactId>exam-system</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>exam-common</artifactId>
<dependencies>
<!-- Spring Boot 基础 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- RocketMQ 核心 -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
3. exam-service模块pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.exam</groupId>
<artifactId>exam-system</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>exam-service</artifactId>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.exam</groupId>
<artifactId>exam-common</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4. score-service模块pom.xml(含Spring AI)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.exam</groupId>
<artifactId>exam-system</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>score-service</artifactId>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.exam</groupId>
<artifactId>exam-common</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring AI 智谱AI对接 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-zhipu-spring-boot-starter</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 异步支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-async</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
5. notify-service模块pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.exam</groupId>
<artifactId>exam-system</artifactId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>notify-service</artifactId>
<dependencies>
<!-- 公共模块 -->
<dependency>
<groupId>com.exam</groupId>
<artifactId>exam-common</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Nacos 注册中心 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
三、核心代码实现
1. exam-common模块核心代码
(1)统一分布式事件父类
package com.exam.common.event;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.UUID;
@Data
@NoArgsConstructor
public class BaseDistributedEvent {
// 事件唯一ID(幂等标识)
private String eventId = UUID.randomUUID().toString().replace("-", "");
// 事件类型(如exam:submit、score:complete)
private String eventType;
// 事件触发时间
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date triggerTime = new Date();
// 事件来源服务
private String sourceService;
// 业务数据
private Object data;
public BaseDistributedEvent(String eventType, String sourceService, Object data) {
this.eventType = eventType;
this.sourceService = sourceService;
this.data = data;
}
}
(2)考生提交试卷事件
package com.exam.common.event;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ExamSubmitEvent extends BaseDistributedEvent {
private Long examId; // 试卷ID
private Long userId; // 考生ID
private Long useTime; // 答题耗时(秒)
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date submitTime; // 提交时间
public ExamSubmitEvent(String sourceService, Long examId, Long userId, Long useTime, Date submitTime) {
super("exam:submit", sourceService, null);
this.examId = examId;
this.userId = userId;
this.useTime = useTime;
this.submitTime = submitTime;
}
}
(3)评分完成事件
package com.exam.common.event;
import com.alibaba.fastjson2.annotation.JSONField;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import java.util.Date;
@Data
@NoArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class ScoreCompleteEvent extends BaseDistributedEvent {
private Long examId; // 试卷ID
private Long userId; // 考生ID
private Integer score; // 评分结果
private String scoreDesc; // 评分说明
@JSONField(format = "yyyy-MM-dd HH:mm:ss")
private Date scoreTime; // 评分时间
public ScoreCompleteEvent(String sourceService, Long examId, Long userId, Integer score, String scoreDesc, Date scoreTime) {
super("score:complete", sourceService, null);
this.examId = examId;
this.userId = userId;
this.score = score;
this.scoreDesc = scoreDesc;
this.scoreTime = scoreTime;
}
}
(4)通用DTO
package com.exam.common.dto;
import lombok.Data;
// 提交试卷DTO
@Data
public class ExamSubmitDTO {
private Long examId;
private Long userId;
private Long useTime;
// 考生答题内容(简化版,实际可存储JSON字符串)
private String answerContent;
}
// AI评分结果DTO
@Data
public class ScoreResultDTO {
private Integer score;
private String desc;
}
// 考生答题数据DTO
@Data
public class ExamAnswerDTO {
private Long examId;
private Long userId;
private String questions; // 试题内容(JSON)
private String userAnswers; // 考生答案(JSON)
}
(5)MQ序列化配置(通用)
package com.exam.common.config;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.spring.support.RocketMQMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.messaging.converter.StringMessageConverter;
@Configuration
public class RocketMQConfig {
/**
* 配置RocketMQ序列化方式为FastJSON2
*/
@Bean
public RocketMQMessageConverter rocketMQMessageConverter() {
RocketMQMessageConverter converter = new RocketMQMessageConverter();
// 设置消息体转换器
StringMessageConverter stringMessageConverter = new StringMessageConverter();
// 自定义序列化/反序列化
stringMessageConverter.setPayloadConverter(new Converter<Object, String>() {
@Override
public String convert(Object source) {
return JSON.toJSONString(source, JSONWriter.Feature.WriteDateUseDateFormat);
}
});
stringMessageConverter.setPayloadConverter(new Converter<byte[], Object>() {
@Override
public Object convert(byte[] source) {
return JSON.parseObject(source, Object.class, JSONReader.Feature.SupportDateFormats);
}
});
converter.setPayloadConverter(stringMessageConverter);
return converter;
}
}
2. exam-service模块核心代码
(1)配置文件(application.yml)
server:
port: 8081
spring:
application:
name: exam-service
# Nacos配置
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# Redis配置
data:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
# RocketMQ配置
rocketmq:
name-server: 127.0.0.1:9876
producer:
group: exam-service-producer-group
# 同步发送+重试
retry-times-when-send-failed: 3
(2)启动类
package com.exam.service;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient // 开启Nacos注册
public class ExamApplication {
public static void main(String[] args) {
SpringApplication.run(ExamApplication.class, args);
}
}
(3)本地事件:考生进入考试事件
package com.exam.service.event;
import org.springframework.context.ApplicationEvent;
public class ExamEnterLocalEvent extends ApplicationEvent {
private Long userId;
private Long examId;
public ExamEnterLocalEvent(Object source, Long userId, Long examId) {
super(source);
this.userId = userId;
this.examId = examId;
}
// getter
public Long getUserId() {
return userId;
}
public Long getExamId() {
return examId;
}
}
(4)本地事件监听器:初始化考试状态
package com.exam.service.listener;
import com.exam.service.event.ExamEnterLocalEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
@Component
public class ExamStatusInitListener {
@EventListener(ExamEnterLocalEvent.class)
public void initExamStatus(ExamEnterLocalEvent event) {
// 模拟:更新考生考试状态为「正在考试」
System.out.println("【考试服务-本地事件】考生" + event.getUserId() + "进入考试" + event.getExamId() + ",初始化状态成功");
}
}
(5)业务层:考试核心逻辑
package com.exam.service.service;
import com.exam.common.dto.ExamSubmitDTO;
import com.exam.common.event.ExamSubmitEvent;
import com.exam.service.event.ExamEnterLocalEvent;
import jakarta.annotation.Resource;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Service
public class ExamService {
@Resource
private ApplicationEventPublisher eventPublisher;
@Resource
private RocketMQTemplate rocketMQTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
// 考生进入考试(发布本地事件)
public void enterExam(Long userId, Long examId) {
// 模拟:校验考生考试资格
checkExamAuth(userId, examId);
// 发布本地事件
eventPublisher.publishEvent(new ExamEnterLocalEvent(this, userId, examId));
}
// 考生提交试卷(发送MQ分布式事件)
public String submitExam(ExamSubmitDTO dto) {
try {
// 1. 模拟:保存考生答题数据、更新考试状态
saveExamAnswer(dto);
// 2. 幂等标记:Redis存储eventId,24小时过期
ExamSubmitEvent event = new ExamSubmitEvent(
"exam-service",
dto.getExamId(),
dto.getUserId(),
dto.getUseTime(),
new Date()
);
stringRedisTemplate.opsForValue().set(
"exam:event:id:" + event.getEventId(),
"1",
24,
TimeUnit.HOURS
);
// 3. 发送MQ消息(Topic:EXAM_EVENT, Tag:exam:submit)
rocketMQTemplate.send(
"EXAM_EVENT:exam:submit",
MessageBuilder.withPayload(event).build()
);
System.out.println("【考试服务-分布式事件】考生" + dto.getUserId() + "提交试卷" + dto.getExamId() + ",MQ消息发送成功,eventId=" + event.getEventId());
return "提交成功";
} catch (Exception e) {
System.err.println("【考试服务】提交试卷失败:" + e.getMessage());
return "提交失败";
}
}
// 模拟:校验考生考试资格
private void checkExamAuth(Long userId, Long examId) {
// 实际业务:校验考生是否有考试资格、考试是否未过期等
System.out.println("【考试服务】校验考生" + userId + "考试" + examId + "资格:通过");
}
// 模拟:保存考生答题数据
private void saveExamAnswer(ExamSubmitDTO dto) {
// 实际业务:保存到MySQL
System.out.println("【考试服务】保存考生" + dto.getUserId() + "试卷" + dto.getExamId() + "答题数据:成功");
}
}
(6)控制器:对外接口
package com.exam.service.controller;
import com.exam.common.dto.ExamSubmitDTO;
import com.exam.service.service.ExamService;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/exam")
public class ExamController {
@Resource
private ExamService examService;
// 考生进入考试
@PostMapping("/enter/{userId}/{examId}")
public String enterExam(@PathVariable Long userId, @PathVariable Long examId) {
examService.enterExam(userId, examId);
return "进入考试成功";
}
// 考生提交试卷
@PostMapping("/submit")
public String submitExam(@RequestBody ExamSubmitDTO dto) {
return examService.submitExam(dto);
}
}
3. score-service模块核心代码
(1)配置文件(application.yml)
server:
port: 8082
spring:
application:
name: score-service
# Nacos配置
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# Redis配置
data:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
# RocketMQ配置
rocketmq:
name-server: 127.0.0.1:9876
consumer:
group: score-service-exam-submit-group
# 手动ACK
enable-msg-trace: true
# Spring AI 智谱AI配置(需替换为自己的API Key)
ai:
zhipu:
api-key: your-zhipu-api-key
base-url: https://open.bigmodel.cn/api/paas/v4/
(2)启动类(开启异步)
package com.exam.score;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableDiscoveryClient
@EnableAsync // 开启异步
public class ScoreApplication {
public static void main(String[] args) {
SpringApplication.run(ScoreApplication.class, args);
}
}
(3)本地事件:AI评分开始事件
package com.exam.score.event;
import com.exam.common.event.ExamSubmitEvent;
import org.springframework.context.ApplicationEvent;
public class AIScoreStartLocalEvent extends ApplicationEvent {
private ExamSubmitEvent examSubmitEvent;
public AIScoreStartLocalEvent(Object source, ExamSubmitEvent examSubmitEvent) {
super(source);
this.examSubmitEvent = examSubmitEvent;
}
// getter
public ExamSubmitEvent getExamSubmitEvent() {
return examSubmitEvent;
}
}
(4)MQ消费者:接收提交试卷事件
package com.exam.score.listener;
import com.exam.common.event.ExamSubmitEvent;
import com.exam.score.event.AIScoreStartLocalEvent;
import jakarta.annotation.Resource;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
topic = "EXAM_EVENT",
selectorExpression = "exam:submit",
consumerGroup = "score-service-exam-submit-group"
)
public class ExamSubmitMQConsumer implements RocketMQListener<ExamSubmitEvent> {
@Resource
private ApplicationEventPublisher eventPublisher;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void onMessage(ExamSubmitEvent event) {
// 1. 幂等校验:检查eventId是否已消费
String key = "exam:event:id:" + event.getEventId();
if (stringRedisTemplate.hasKey(key + ":consumed")) {
System.out.println("【评分服务-MQ消费】重复事件,eventId=" + event.getEventId());
return;
}
try {
// 2. 转换为本地事件并发布
eventPublisher.publishEvent(new AIScoreStartLocalEvent(this, event));
// 3. 标记为已消费,24小时过期
stringRedisTemplate.opsForValue().set(
key + ":consumed",
"1",
24,
java.util.concurrent.TimeUnit.HOURS
);
System.out.println("【评分服务-MQ消费】接收考生提交试卷事件,已转换为本地AI评分事件,eventId=" + event.getEventId());
} catch (Exception e) {
System.err.println("【评分服务-MQ消费】处理失败:" + e.getMessage());
// 实际业务:可发送死信队列,人工处理
}
}
}
(5)本地监听器:AI评分逻辑(集成Spring AI)
package com.exam.score.listener;
import com.alibaba.fastjson2.JSON;
import com.exam.common.dto.ExamAnswerDTO;
import com.exam.common.dto.ScoreResultDTO;
import com.exam.common.event.ScoreCompleteEvent;
import com.exam.score.event.AIScoreStartLocalEvent;
import com.exam.score.service.ScoreService;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.ChatClient;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class AIScoreListener {
@Resource
private ChatClient chatClient;
@Resource
private ScoreService scoreService;
@EventListener(AIScoreStartLocalEvent.class)
@Async // 异步执行,不阻塞MQ消费
public void doAIScore(AIScoreStartLocalEvent event) {
ExamSubmitEvent submitEvent = event.getExamSubmitEvent();
Long userId = submitEvent.getUserId();
Long examId = submitEvent.getExamId();
try {
// 1. 模拟:获取考生答题数据(实际为OpenFeign调用考试服务)
ExamAnswerDTO answerDTO = scoreService.getExamAnswer(userId, examId);
// 2. 构造AI评分提示词
String prompt = String.format(
"作为在线考试系统的AI评分老师,对考生%s的试卷%s进行评分。试题内容:%s,考生答案:%s。要求:1. 给出0-100的整数分数;2. 评分说明不超过50字;3. 仅返回JSON格式,无需其他内容,JSON结构:{\"score\":0,\"desc\":\"\"}",
userId, examId, answerDTO.getQuestions(), answerDTO.getUserAnswers()
);
// 3. Spring AI调用智谱AI
String aiResult = chatClient.call(prompt);
System.out.println("【评分服务-AI评分】AI返回结果:" + aiResult);
// 4. 解析评分结果
ScoreResultDTO scoreResult = JSON.parseObject(aiResult, ScoreResultDTO.class);
// 5. 保存评分结果
scoreService.saveScoreResult(userId, examId, scoreResult);
// 6. 发送评分完成MQ事件
scoreService.publishScoreCompleteEvent(userId, examId, scoreResult);
System.out.println("【评分服务-AI评分】考生" + userId + "试卷" + examId + "评分完成,分数:" + scoreResult.getScore());
} catch (Exception e) {
System.err.println("【评分服务-AI评分】失败:考生" + userId + "试卷" + examId + ",原因:" + e.getMessage());
// 实际业务:发送评分异常事件,触发人工评分
}
}
}
(6)评分业务层
package com.exam.score.service;
import com.exam.common.dto.ExamAnswerDTO;
import com.exam.common.dto.ScoreResultDTO;
import com.exam.common.event.ScoreCompleteEvent;
import jakarta.annotation.Resource;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
public class ScoreService {
@Resource
private RocketMQTemplate rocketMQTemplate;
// 模拟:获取考生答题数据
public ExamAnswerDTO getExamAnswer(Long userId, Long examId) {
ExamAnswerDTO dto = new ExamAnswerDTO();
dto.setExamId(examId);
dto.setUserId(userId);
// 模拟试题:Java基础题
dto.setQuestions("{\"1\":\"Java中String是否可变?\",\"2\":\"Spring Boot的核心注解是什么?\"}");
// 模拟考生答案
dto.setUserAnswers("{\"1\":\"不可变\",\"2\":\"@SpringBootApplication\"}");
return dto;
}
// 模拟:保存评分结果
public void saveScoreResult(Long userId, Long examId, ScoreResultDTO scoreResult) {
// 实际业务:保存到MySQL
System.out.println("【评分服务】保存考生" + userId + "试卷" + examId + "评分结果:" + scoreResult.getScore() + "分,说明:" + scoreResult.getDesc());
}
// 发布评分完成事件
public void publishScoreCompleteEvent(Long userId, Long examId, ScoreResultDTO scoreResult) {
ScoreCompleteEvent event = new ScoreCompleteEvent(
"score-service",
examId,
userId,
scoreResult.getScore(),
scoreResult.getDesc(),
new Date()
);
// 发送MQ消息(Topic:SCORE_EVENT, Tag:score:complete)
rocketMQTemplate.send(
"SCORE_EVENT:score:complete",
MessageBuilder.withPayload(event).build()
);
System.out.println("【评分服务】发送评分完成MQ事件,考生" + userId + "试卷" + examId + ",eventId=" + event.getEventId());
}
}
4. notify-service模块核心代码
(1)配置文件(application.yml)
server:
port: 8083
spring:
application:
name: notify-service
# Nacos配置
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# Redis配置
data:
redis:
host: 127.0.0.1
port: 6379
password:
database: 0
# RocketMQ配置
rocketmq:
name-server: 127.0.0.1:9876
consumer:
group: notify-service-score-complete-group
(2)启动类
package com.exam.notify;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class NotifyApplication {
public static void main(String[] args) {
SpringApplication.run(NotifyApplication.class, args);
}
}
(3)MQ消费者:接收评分完成事件
package com.exam.notify.listener;
import com.exam.common.event.ScoreCompleteEvent;
import com.exam.notify.service.NotifyService;
import jakarta.annotation.Resource;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
@Component
@RocketMQMessageListener(
topic = "SCORE_EVENT",
selectorExpression = "score:complete",
consumerGroup = "notify-service-score-complete-group"
)
public class ScoreCompleteMQConsumer implements RocketMQListener<ScoreCompleteEvent> {
@Resource
private NotifyService notifyService;
@Resource
private StringRedisTemplate stringRedisTemplate;
@Override
public void onMessage(ScoreCompleteEvent event) {
// 1. 幂等校验
String key = "score:event:id:" + event.getEventId();
if (stringRedisTemplate.hasKey(key + ":notified")) {
System.out.println("【通知服务-MQ消费】重复事件,eventId=" + event.getEventId());
return;
}
try {
// 2. 发送通知
notifyService.sendScoreNotify(event);
// 3. 标记为已通知
stringRedisTemplate.opsForValue().set(
key + ":notified",
"1",
24,
java.util.concurrent.TimeUnit.HOURS
);
System.out.println("【通知服务-MQ消费】考生" + event.getUserId() + "成绩通知发送成功,eventId=" + event.getEventId());
} catch (Exception e) {
System.err.println("【通知服务-MQ消费】处理失败:" + e.getMessage());
}
}
}
(4)通知业务层
package com.exam.notify.service;
import com.exam.common.event.ScoreCompleteEvent;
import org.springframework.stereotype.Service;
@Service
public class NotifyService {
// 模拟:发送成绩通知(短信/站内信)
public void sendScoreNotify(ScoreCompleteEvent event) {
// 实际业务:调用短信API/站内信推送
String notifyContent = String.format(
"【在线考试系统】您的试卷%s已评分完成,成绩:%d分,评分说明:%s",
event.getExamId(),
event.getScore(),
event.getScoreDesc()
);
System.out.println("【通知服务】发送通知给考生" + event.getUserId() + ":" + notifyContent);
}
}
四、环境准备与运行说明
1. 前置环境
- JDK 17+(Spring Boot 3.2要求)
- Maven 3.8+
- Nacos 2.3.0(启动命令:
startup.cmd -m standalone) - RocketMQ 5.1.0(启动NameServer:
mqnamesrv.cmd;启动Broker:mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true) - Redis 7.0+(默认配置,无密码)
- 智谱AI API Key(替换score-service配置文件中的
your-zhipu-api-key,可在智谱开放平台申请)
2. 运行步骤
- 编译父工程:
mvn clean install - 依次启动:
- exam-service(8081端口)
- score-service(8082端口)
- notify-service(8083端口)
- 接口测试(使用Postman/Curl):
- 考生进入考试:
POST http://localhost:8081/exam/enter/1001/2001 - 考生提交试卷:
curl -X POST http://localhost:8081/exam/submit \ -H "Content-Type: application/json" \ -d '{ "examId": 2001, "userId": 1001, "useTime": 1800, "answerContent": "{\"1\":\"不可变\",\"2\":\"@SpringBootApplication\"}" }'
- 考生进入考试:
3. 预期输出
# exam-service日志
【考试服务】校验考生1001考试2001资格:通过
【考试服务-本地事件】考生1001进入考试2001,初始化状态成功
【考试服务】保存考生1001试卷2001答题数据:成功
【考试服务-分布式事件】考生1001提交试卷2001,MQ消息发送成功,eventId=xxx
# score-service日志
【评分服务-MQ消费】接收考生提交试卷事件,已转换为本地AI评分事件,eventId=xxx
【评分服务-AI评分】AI返回结果:{"score":95,"desc":"答案全部正确,基础扎实"}
【评分服务】保存考生1001试卷2001评分结果:95分,说明:答案全部正确,基础扎实
【评分服务】发送评分完成MQ事件,考生1001试卷2001,eventId=yyy
# notify-service日志
【通知服务-MQ消费】考生1001成绩通知发送成功,eventId=yyy
【通知服务】发送通知给考生1001:【在线考试系统】您的试卷2001已评分完成,成绩:95分,评分说明:答案全部正确,基础扎实
五、核心扩展说明
- 数据库集成:可在各服务中添加MyBatis-Plus依赖,实现答题数据、评分结果的持久化;
- 限流熔断:添加Sentinel依赖,在考试服务接口层配置限流规则(如每秒1000次请求);
- 多大模型适配:Spring AI支持切换OpenAI/文心一言,仅需修改配置文件和依赖;
- 消息可靠性:开启RocketMQ事务消息,保证考试提交事件的最终一致性;
- 监控链路:添加SkyWalking依赖,实现全链路追踪,定位跨服务问题。
总结
- 工程结构:采用多模块Maven架构,公共模块抽离通用代码,各服务职责单一,符合微服务设计原则;
- 核心流程:考试提交→MQ事件→AI评分→MQ事件→成绩通知,全程解耦、异步、幂等;
- 关键技术:
- Spring Event解耦服务内逻辑,RocketMQ解耦跨服务通信;
- Spring AI一键调用大模型,无需手写HTTP请求;
- Redis实现幂等校验,避免重复消费;
- 可扩展性:支持切换MQ(RabbitMQ)、大模型(文心一言)、中间件(如MinIO存储附件),适配不同规模的考试系统。
该工程是可直接运行的最小化实现,覆盖了分布式考试系统的核心流程,可基于此扩展更多功能(如AI出卷、考试监控、数据统计等)。
更多推荐



所有评论(0)