一、工程整体结构说明

基于上述分布式在线考试系统设计,演示可直接运行的最小化工程示例(聚焦核心的「考试提交→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. 运行步骤
  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
    • 考生提交试卷:
      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出卷、考试监控、数据统计等)。

Logo

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

更多推荐