基于Spring AI的分布式在线考试系统-事件处理架构实现方案

一、工程整体结构说明

基于上述分布式在线考试系统设计,演示可直接运行的最小化工程示例 (聚焦核心的「考试提交→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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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 复制代码
<?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)统一分布式事件父类
java 复制代码
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)考生提交试卷事件
java 复制代码
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)评分完成事件
java 复制代码
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
java 复制代码
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序列化配置(通用)
java 复制代码
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)
yaml 复制代码
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)启动类
java 复制代码
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)本地事件:考生进入考试事件
java 复制代码
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)本地事件监听器:初始化考试状态
java 复制代码
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)业务层:考试核心逻辑
java 复制代码
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)控制器:对外接口
java 复制代码
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)
yaml 复制代码
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)启动类(开启异步)
java 复制代码
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评分开始事件
java 复制代码
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消费者:接收提交试卷事件
java 复制代码
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)
java 复制代码
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)评分业务层
java 复制代码
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)
yaml 复制代码
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)启动类
java 复制代码
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消费者:接收评分完成事件
java 复制代码
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)通知业务层
java 复制代码
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. 运行步骤
  1. 编译父工程:mvn clean install
  2. 依次启动:
    • exam-service(8081端口)
    • score-service(8082端口)
    • notify-service(8083端口)
  3. 接口测试(使用Postman/Curl):
    • 考生进入考试:POST http://localhost:8081/exam/enter/1001/2001

    • 考生提交试卷:

      bash 复制代码
      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分,评分说明:答案全部正确,基础扎实

五、核心扩展说明

  1. 数据库集成:可在各服务中添加MyBatis-Plus依赖,实现答题数据、评分结果的持久化;
  2. 限流熔断:添加Sentinel依赖,在考试服务接口层配置限流规则(如每秒1000次请求);
  3. 多大模型适配:Spring AI支持切换OpenAI/文心一言,仅需修改配置文件和依赖;
  4. 消息可靠性:开启RocketMQ事务消息,保证考试提交事件的最终一致性;
  5. 监控链路:添加SkyWalking依赖,实现全链路追踪,定位跨服务问题。

总结

  1. 工程结构:采用多模块Maven架构,公共模块抽离通用代码,各服务职责单一,符合微服务设计原则;
  2. 核心流程:考试提交→MQ事件→AI评分→MQ事件→成绩通知,全程解耦、异步、幂等;
  3. 关键技术
    • Spring Event解耦服务内逻辑,RocketMQ解耦跨服务通信;
    • Spring AI一键调用大模型,无需手写HTTP请求;
    • Redis实现幂等校验,避免重复消费;
  4. 可扩展性:支持切换MQ(RabbitMQ)、大模型(文心一言)、中间件(如MinIO存储附件),适配不同规模的考试系统。

该工程是可直接运行的最小化实现,覆盖了分布式考试系统的核心流程,可基于此扩展更多功能(如AI出卷、考试监控、数据统计等)。

相关推荐
NAGNIP44 分钟前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab2 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab2 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP6 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年6 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼6 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS6 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区7 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈7 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang8 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx