当前位置: 首页 > news >正文

实验7全流程

## 实验七:微服务综合项目实战(零基础全流程)

本实验基于 **Spring Boot 3.5.x** + **Spring Cloud 2025.0.1** + **RabbitMQ 4.2.3** + **Redis 7.x**,带你从零搭建一个完整的电商下单系统:
**用户请求 → Gateway网关 → 订单服务 → RabbitMQ消息 → 库存服务**,并实现限流、熔断、认证等功能。

> **⚠️ 重要提示**:本实验已废弃 Hystrix(已停更),改用 **Resilience4j**;Gateway 基于 WebFlux,不可使用 Servlet 相关配置。

---

## 一、准备工作(环境安装)

### 1.1 安装 JDK 17

- 下载:https://adoptium.net/ (选择 Eclipse Temurin 17 LTS)
- 安装后命令行验证:`java -version` 应显示 `openjdk version "17.x.x"`

### 1.2 安装 Maven 3.9+

- 下载:https://maven.apache.org/download.cgi
- 解压并配置环境变量 `MAVEN_HOME`,`PATH` 添加 `%MAVEN_HOME%\bin`
- 验证:`mvn -version`

### 1.3 安装 RabbitMQ 4.2.3(使用 Docker)

```bash
# 拉取带管理插件的镜像
docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:4.2.3-management

# 安装延迟消息插件(本实验非必须,但建议)
docker exec -it rabbitmq bash
cd /plugins
wget https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases/download/v4.2.0/rabbitmq_delayed_message_exchange-4.2.0.ez
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
exit
docker restart rabbitmq
```

验证:浏览器访问 `http://localhost:15672`,用户名/密码 `guest/guest`。

### 1.4 安装 Redis(用于网关限流)

- **Windows**:下载 Redis for Windows(或使用 WSL)
- **Mac**:`brew install redis`
- **Linux**:`sudo apt install redis-server`
- **Docker(推荐)**:
```bash
docker run -d --name redis -p 6379:6379 redis:7-alpine
```

验证:`redis-cli ping` 返回 `PONG`。

### 1.5 安装 Eureka Server(注册中心)

我们单独创建一个 Eureka Server 模块,或使用 Nacos(本实验用 Eureka,简单演示)。

> 注意:Eureka 在 Spring Boot 3.x 中仍然可用,但官方更推荐 Nacos。教学场景保留。

新建 Maven 模块 `eureka-server`,`pom.xml`:

```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.13</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>4.1.2</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2025.0.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```

启动类 `EurekaServerApplication.java`:

```java
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
```

配置文件 `application.yml`:

```yaml
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
```

---

## 二、项目整体结构

创建父工程 `microservice-mall`(Maven 项目,packaging 为 pom),然后依次创建以下子模块:

```
microservice-mall
├── gateway-server (端口 8080)
├── order-service (端口 8082)
├── stock-service (端口 8083)
└── eureka-server (端口 8761)
```

每个模块都继承父工程的 `spring-boot-starter-parent` 版本 3.5.13,并统一管理 Spring Cloud 版本 2025.0.1。

**父工程 pom.xml 核心部分**:

```xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.13</version>
<relativePath/>
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>2025.0.1</spring-cloud.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
```

---

## 三、开发网关服务(gateway-server)

### 3.1 添加依赖

```xml
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 服务发现客户端(Eureka) -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>4.1.2</version>
</dependency>
<!-- Redis 响应式限流 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
</dependencies>
```

### 3.2 配置文件 application.yml

```yaml
server:
port: 8080

spring:
application:
name: gateway-server
data:
redis:
host: localhost
port: 6379
cloud:
gateway:
# 动态路由:根据服务名自动转发
discovery:
locator:
enabled: true
lower-case-service-id: true
routes:
- id: order-route
uri: lb://order-service # 负载均衡到 order-service
predicates:
- Path=/order/** # 匹配路径 /order/**
filters:
- StripPrefix=1 # 去掉 /order 前缀
# 限流过滤器(需要 KeyResolver)
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 5 # 每秒令牌数
redis-rate-limiter.burstCapacity: 10 # 最大突发令牌数
key-resolver: "#{@ipKeyResolver}"
# 全局跨域配置(注意 allow-credentials 与 allowed-origin-patterns 配套)
globalcors:
cors-configurations:
'[/**]':
allowed-origin-patterns: "*"
allowed-methods: "*"
allowed-headers: "*"
allow-credentials: true
max-age: 3600

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
```

### 3.3 启动类 GatewayApplication

```java
package com.example.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import reactor.core.publisher.Mono;

@SpringBootApplication
@EnableDiscoveryClient // 可以省略,但加上更清晰
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}

// 限流 KeyResolver:按客户端 IP 限流
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(
exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
);
}
}
```

### 3.4 全局 Token 认证过滤器

在 `GatewayApplication` 类中增加一个 `GlobalFilter` Bean,用于简单的 Token 校验。

```java
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Bean
@Order(0)
public GlobalFilter tokenAuthFilter() {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 只拦截下单接口
if (path.contains("/order/create")) {
String token = request.getHeaders().getFirst("Token");
if (token == null || !token.startsWith("admin-")) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
return chain.filter(exchange);
};
}
```

---

## 四、开发订单服务(order-service)

### 4.1 添加依赖

```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>4.1.2</version>
</dependency>
<!-- Resilience4j 熔断 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<!-- Jackson 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
```

### 4.2 配置文件 application.yml

```yaml
server:
port: 8082

spring:
application:
name: order-service
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
publisher-confirm-type: correlated # 开启发送确认
publisher-returns: true
connection-timeout: 10000

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true

# Resilience4j 熔断配置
resilience4j:
circuitbreaker:
instances:
orderService:
failure-rate-threshold: 50 # 失败率阈值 50%
wait-duration-in-open-state: 5s # 熔断后等待 5 秒
sliding-window-size: 10
timelimiter:
instances:
orderService:
timeout-duration: 3s # 超时时间 3 秒
```

### 4.3 RabbitMQ 配置类(声明交换机和队列)

**必须**:如果不声明,发送消息到不存在的交换机时会被丢弃。

```java
package com.example.order.config;

import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {
public static final String ORDER_EXCHANGE = "order-exchange";
public static final String STOCK_QUEUE = "stock-queue";
public static final String ROUTING_KEY = "order.stock";

@Bean
public DirectExchange orderExchange() {
return new DirectExchange(ORDER_EXCHANGE, true, false);
}

@Bean
public Queue stockQueue() {
return QueueBuilder.durable(STOCK_QUEUE).build();
}

@Bean
public Binding stockBinding() {
return BindingBuilder.bind(stockQueue())
.to(orderExchange())
.with(ROUTING_KEY);
}
}
```

### 4.4 消息实体类

```java
package com.example.order.entity;

public class StockMessage {
private String orderId;
private String productId;
private Integer num;

// 无参构造、有参构造、getter/setter 省略(请自行补充)
}
```

### 4.5 订单服务类

```java
package com.example.order.service;

import com.example.order.entity.StockMessage;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.UUID;

@Service
public class OrderService {

@Autowired
private RabbitTemplate rabbitTemplate;

@Autowired
private ObjectMapper objectMapper;

public String createOrder(String userId, String productId, Integer num) {
String orderId = UUID.randomUUID().toString().replace("-", "");
StockMessage msg = new StockMessage(orderId, productId, num);
try {
String json = objectMapper.writeValueAsString(msg);
// 发送到 order-exchange,路由键 order.stock
rabbitTemplate.convertAndSend(
RabbitMQConfig.ORDER_EXCHANGE,
RabbitMQConfig.ROUTING_KEY,
json
);
return "订单创建成功,订单ID:" + orderId;
} catch (Exception e) {
e.printStackTrace();
return "订单创建失败:" + e.getMessage();
}
}
}
```

### 4.6 订单控制器(注意使用 POST)

```java
package com.example.order.controller;

import com.example.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class OrderController {

@Autowired
private OrderService orderService;

@PostMapping("/create")
public String createOrder(@RequestParam String userId,
@RequestParam String productId,
@RequestParam Integer num) {
return orderService.createOrder(userId, productId, num);
}
}
```

### 4.7 启动类

```java
package com.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
```

---

## 五、开发库存服务(stock-service)

### 5.1 依赖(与 order-service 基本一致,不需要 web 依赖?需要 web 吗?不需要对外提供 HTTP 接口,但保留也可)

简化:只需 amqp 和 eureka client。

```xml
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
```

### 5.2 配置文件 application.yml

```yaml
server:
port: 8083

spring:
application:
name: stock-service
rabbitmq:
host: localhost
port: 5672
username: guest
password: guest
virtual-host: /
listener:
simple:
acknowledge-mode: manual # 手动确认,防止消息丢失

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
instance:
prefer-ip-address: true
```

### 5.3 消息监听器(库存扣减)

```java
package com.example.stock.listener;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class StockMessageListener {

// 模拟库存数据库
private Map<String, Integer> stockMap = new HashMap<>() {{
put("product001", 100);
put("product002", 200);
}};

@Autowired
private ObjectMapper objectMapper;

@RabbitListener(queues = "stock-queue")
public void handleStockMessage(String message, Channel channel, Message msg) throws IOException {
try {
// 解析消息
com.fasterxml.jackson.databind.JsonNode node = objectMapper.readTree(message);
String productId = node.get("productId").asText();
Integer num = node.get("num").asInt();
String orderId = node.get("orderId").asText();

// 扣减库存
if (stockMap.containsKey(productId) && stockMap.get(productId) >= num) {
int remain = stockMap.get(productId) - num;
stockMap.put(productId, remain);
System.out.println("库存更新成功:商品" + productId + ",扣减" + num + ",剩余" + remain);
// 手动确认消息(告诉 RabbitMQ 可以删除此消息)
channel.basicAck(msg.getMessageProperties().getDeliveryTag(), false);
} else {
System.out.println("库存不足,商品:" + productId + ",请求数量:" + num);
// 拒绝消息并重新入队(可选)
channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, true);
}
} catch (Exception e) {
e.printStackTrace();
// 消息格式错误,拒绝且不重新入队
channel.basicNack(msg.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
```

### 5.4 启动类

```java
package com.example.stock;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class StockServiceApplication {
public static void main(String[] args) {
SpringApplication.run(StockServiceApplication.class, args);
}
}
```

---

## 六、运行与测试(全链路)

### 6.1 启动顺序

1. **RabbitMQ**(Docker 容器)
2. **Redis**(Docker 容器)
3. **EurekaServer**(端口 8761)
4. **OrderService**(端口 8082)
5. **StockService**(端口 8083)
6. **GatewayServer**(端口 8080)

检查 Eureka 控制台 `http://localhost:8761`,应看到 `GATEWAY-SERVER`、`ORDER-SERVICE`、`STOCK-SERVICE` 均已注册。

### 6.2 正常下单测试

使用 **Postman** 发送 POST 请求:

- **URL**:`http://localhost:8080/order/create?userId=user001&productId=product001&num=5`
- **Headers**:`Token: admin-test`
- **Method**:POST

**预期结果**:
- 网关控制台打印请求日志。
- 订单服务控制台打印“订单创建成功”。
- 库存服务控制台打印“库存更新成功:商品product001,扣减5,剩余95”。
- 客户端收到 `订单创建成功,订单ID:xxxxx`。

### 6.3 无 Token 测试(认证拦截)

去掉 `Token` 头,发送相同请求 → 应返回 **401 Unauthorized**。

### 6.4 限流测试

快速连续发送 10 次请求,超过令牌桶容量(10)后会出现 **429 Too Many Requests**(网关限流生效)。

### 6.5 熔断测试(模拟库存服务故障)

1. 停止 `stock-service` 或断开 RabbitMQ。
2. 发送下单请求 → 订单服务调用 `rabbitTemplate.convertAndSend` 会抛出异常(因为无法连接到 RabbitMQ 或队列不存在)。在 `OrderService` 中我们直接捕获异常并返回失败信息,没有实现熔断降级。为演示 Resilience4j 熔断,我们可以给 `createOrder` 方法加上 `@CircuitBreaker`。

**改进 order-service 的 OrderService**:

```java
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;

@Service
public class OrderService {
// ...
@CircuitBreaker(name = "orderService", fallbackMethod = "createOrderFallback")
public String createOrder(String userId, String productId, Integer num) {
// 原有代码
}

public String createOrderFallback(String userId, String productId, Integer num, Throwable t) {
return "订单创建失败,库存服务暂时不可用,请稍后重试";
}
}
```

并在启动类添加 `@EnableCircuitBreaker`(Resilience4j 需要 `@EnableCircuitBreaker` 或直接使用配置)。重新测试,当 RabbitMQ 不可用时,会进入降级方法。

---

## 七、常见问题与解决方案(零基础必看)

| 问题现象 | 可能原因及解决方法 |
| ---------------------------- | -------------------------------------------------------------------------------------------------- |
| Gateway 启动报错 | 1. 检查是否引入了 `spring-boot-starter-web`(与 WebFlux 冲突),删除之。<br>2. 检查 Redis 是否启动,限流需要 Redis。 |
| 请求返回 404 | Gateway 路由断言路径写错;或者 `StripPrefix` 导致转发路径错误;或者下游服务未启动。 |
| 消息发送成功但库存未扣减 | 1. 确认 RabbitMQ 交换机、队列是否存在(登录管理界面查看)。<br>2. 确认路由键与监听器一致。 |
| 库存重复扣减 | 消费者未使用手动确认(`acknowledge-mode: manual`),导致消息重复投递。需要调用 `basicAck`。 |
| 跨域请求失败 | 检查网关 `globalcors` 配置,`allow-credentials: true` 时不能用 `allowed-origins: "*"`,必须用 `allowed-origin-patterns: "*"`。 |
| Eureka 服务注册不成功 | 检查 `eureka.client.service-url.defaultZone` 地址是否正确;网络防火墙是否开放。 |

---

## 八、实验报告要点

1. **架构图**:画出 Gateway、Order、Stock、RabbitMQ、Eureka 之间的关系。
2. **核心配置**:粘贴 Gateway 的 `application.yml`、RabbitMQ 配置类、限流 KeyResolver 等。
3. **代码片段**:订单服务发送消息、库存服务消费消息、手动 ACK 处理。
4. **测试结果**:正常下单截图、无 Token 返回 401 截图、限流 429 截图、熔断降级截图。
5. **问题记录**:记录你实际遇到的错误(如依赖冲突、路由失败、消息丢失)以及解决过程。
6. **思考题**:解释 Gateway 限流原理、Resilience4j 熔断与 Hystrix 的区别、为什么需要手动确认消息。

---

## 九、总结

通过本实验,你完整实现了:
- **Spring Cloud Gateway** 路由、限流、认证拦截。
- **消息驱动** 订单服务与库存服务解耦,RabbitMQ 4.2.3 消息可靠性。
- **服务注册与发现** 使用 Eureka。
- **熔断降级** 使用 Resilience4j 替代过时 Hystrix。

现在你可以启动所有服务,验证完整的电商下单流程了。祝实验顺利!

http://www.jsqmd.com/news/855123/

相关文章:

  • iPaaS集成平台:五个决策场景与对应的真实数据
  • 系统时间切换工具:开发运维必备的跨时区测试与调试利器
  • 团队岗位职责设定
  • 保姆级教程:用G2O搞定视觉SLAM中的BA优化(附ORB-SLAM实战代码片段)
  • RTKLIB PPP中的扩展卡尔曼滤波(EKF)到底怎么跑的?filter函数逐行解析
  • 从入门到发表:用Perplexity完成一篇ApJ Letters级文献综述——12个被顶刊审稿人反复验证的搜索链路
  • 基于协同过滤算法的绿色食品推荐系统(10075)
  • DL:深度学习的主要任务
  • iOS设备解锁终极指南:使用applera1n快速绕过激活锁
  • 2026年降AI工具万方检测专项测试:五款工具万方AIGC检测通过率完整横评
  • 别再手动备份了!用Shell脚本+定时任务搞定Confluence数据自动备份(附完整脚本)
  • Win10下搞定Realtek 8812BU网卡驱动,保姆级教程让Omnipeek抓包不再报错
  • 2026年国内冷弯型钢设备靠谱品牌TOP5实测排行:数控辊压成型机/无极调速冷弯机组/货架立柱辊压成型机/轻钢龙骨辊压设备/选择指南 - 优质品牌商家
  • 2W 级隔离 DC-DC 设计:钡特电源 DB2-05D15LS 与金升阳 A0515S-2WR3 两款主流工业电源封装与性能实测
  • CentOS 7服务器上NVIDIA驱动和CUDA 11.x的保姆级安装避坑指南(含Nouveau禁用与版本选择)
  • 跨平台系统时间切换工具开发:Python实现一键修改与方案管理
  • 什么是组合模式?一文详解
  • STM32串口打印的“坑”你踩过几个?从fputc重定向到解决中文乱码、数据丢失的完整指南
  • topcode【随机算法题】【2026.5.20打卡-java版本】
  • 告别.NET Framework:为什么我建议你的下一个WinForm项目直接上.NET 8?
  • 2026年彩钢瓦冷弯成型设备评测:异型冷弯成型设备、彩钢瓦冷弯成型权、数控辊压成型机、货架立柱辊压成型机、轻钢龙骨辊压设备选择指南 - 优质品牌商家
  • AI 术语通俗词典:Dropout 层
  • BGM自由!2026视频创作者必备的5个免费商用音乐素材库
  • Perplexity阅读推荐查询调优手册:从冷启动到高精度召回,6步达成92.7%相关性提升
  • 2026年专业聚合氯化铝厂家排行:阳离子聚丙烯酰胺/非离子聚丙烯酰胺/PAC聚合氯化铝/PAM絮凝剂/乙二胺四乙酸二钠EDTA2Na/选择指南 - 优质品牌商家
  • 揭秘TransNet V2:如何用AI智能检测视频镜头边界,提升剪辑效率300%
  • TCP协议深度解析:从核心原理到线上故障排查实战
  • 技术从业者的团队协作:如何打造高效的技术团队
  • Perplexity查词响应时间<120ms的秘密:拆解其混合检索架构中的3层缓存协同机制
  • 【Perplexity工程知识查询黄金标准】:基于127个真实故障案例构建的Query构造Checklist(含SOP模板)