datai/docs/archive/retros/20260118-metadata-api-client-retro.md

755 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 闭环复盘 - Metadata API客户端封装
## 复盘信息
- **复盘编号**: Retro-20260118-002
- **复盘日期**: 2026-01-18
- **相关需求**: [REQ-010-5](../requirements/REQ-010-5.md) - Metadata API客户端封装
- **相关决策**: [ADR-0014](../decisions/adr/0014-metadata-api-client.md) - Metadata API客户端封装架构决策
- **相关提示词**: [Prompt-015](../prompts/015-metadata-api-client.md) - Metadata API客户端封装实现提示词
- **相关会话**: [Session-20260118-002](../sessions/20260118-metadata-api-client.md) - Metadata API客户端封装执行会话
- **相关变更**: [Change-015](../changelog/20260118-metadata-api-client.md) - Metadata API客户端封装变更记录
## 目标与实际产出对比
### 目标
1. 封装 Salesforce Metadata API 客户端,提供 retrieve() 和 deploy() 方法的调用接口
2. 支持异步执行
3. 支持状态轮询
4. 支持 Job ID 获取
5. 支持 Zip 文件下载和解压处理
6. 支持部署结果解析
7. 实现错误处理机制
### 实际产出
1. ✅ 完成了 MetadataApiClient 客户端类的创建,包括:
- getMetadataConnection: 获取 Metadata 连接
- retrieve: 检索元数据
- deploy: 部署元数据
- checkRetrieveStatus: 检查检索状态
- checkDeployStatus: 检查部署状态
2. ✅ 完成了 MetadataApiService 服务层的创建,包括:
- retrieveAsync: 异步检索元数据
- deployAsync: 异步部署元数据
- cancelJob: 取消任务
- getJobStatus: 获取任务状态
3. ✅ 完成了 AsyncConfig 配置类的创建,包括:
- 配置线程池(核心线程数 5最大线程数 10队列容量 100
- 配置线程名称前缀
- 配置拒绝策略
4. ✅ 完成了状态轮询机制的创建,包括:
- 定时轮询任务状态
- 支持超时处理30 分钟)
- 支持取消轮询
- 更新任务状态
5. ✅ 完成了 Zip 文件处理工具类的创建,包括:
- unzip: 解压 Zip 文件
- readZipFile: 读取 Zip 文件内容
- 使用流式处理避免内存溢出
6. ✅ 完成了 MetadataApiUtil 工具类的创建,包括:
- parsePackageXml: 解析 package.xml
- parseDeployResult: 解析部署结果
- extractErrorMessages: 提取错误信息
- extractCodeCoverage: 提取代码覆盖率
7. ✅ 完成了自定义异常类的创建,包括:
- MetadataApiException: 统一的异常处理机制
8. ✅ 完成了结果类的创建,包括:
- RetrieveResult: 检索结果类
- DeployResult: 部署结果类
- JobStatus: 任务状态类
9. ✅ 完成了 MetadataApiController 控制器的创建,包括:
- POST /metadata/retrieve - 检索元数据
- POST /metadata/deploy - 部署元数据
- GET /metadata/job/{jobId}/status - 获取任务状态
- DELETE /metadata/job/{jobId} - 取消任务
10. ✅ 完成了 MetadataManager 前端组件的创建,包括:
- 元数据检索界面
- 元数据部署界面
- 任务状态查询界面
- 任务取消界面
### 对比分析
| 目标项 | 完成度 | 说明 |
|--------|--------|------|
| MetadataApiClient 客户端类 | 100% | 完全实现,使用 Salesforce WSC 库 |
| MetadataApiService 服务层 | 100% | 完全实现,支持异步执行 |
| AsyncConfig 配置类 | 100% | 完全实现,配置线程池 |
| 状态轮询机制 | 100% | 完全实现,支持超时处理和取消轮询 |
| Zip 文件处理工具类 | 100% | 完全实现,使用流式处理 |
| MetadataApiUtil 工具类 | 100% | 完全实现,提供多种工具方法 |
| 自定义异常类 | 100% | 完全实现,统一的异常处理机制 |
| 结果类 | 100% | 完全实现,封装任务执行结果 |
| MetadataApiController 控制器 | 100% | 完全实现,提供 4 个 RESTful API 接口 |
| MetadataManager 前端组件 | 100% | 完全实现,基于 Vue 3 和 Element Plus |
**总体完成度**: 100%
**结论**: 所有目标均已实现,功能完整,符合需求。
## 有效的 Prompt 技巧3条
### 技巧1明确角色设定和技术栈
**描述**: 在提示词开头明确设定角色和技术栈,让 AI 清楚自己的定位和要使用的技术。
**示例**:
```
### 角色设定
你是一个经验丰富的 Spring Boot 全栈开发工程师,专注于 Salesforce 元数据管理系统的开发。你熟悉以下技术栈:
- **后端**: Spring Boot 3, MyBatis Plus, MySQL
- **前端**: Vue 3, Element Plus
- **Salesforce API**: Metadata API, Partner API
- **Salesforce WSC**: Web Service Connector 库
- **工具**: Maven, Git, Postman
### 技术栈限制
- **后端**: Spring Boot 3, MyBatis Plus, MySQL
- **Salesforce API**: Metadata API, Partner API
- **Salesforce WSC**: 必须使用 Salesforce WSC (Web Service Connector) 库
- **异步执行**: 必须使用 Spring 的 @Async 注解和线程池
- **状态轮询**: 必须使用定时任务实现状态轮询
```
**效果**: AI 能够快速理解角色定位和技术栈,生成符合要求的代码。
### 技巧2提供详细的代码示例和实现方案
**描述**: 在提示词中提供详细的代码示例和实现方案,包括类结构、方法签名、参数类型等。
**示例**:
```
#### 2.1 MetadataApiClient 类
```java
package com.datai.metadata.client;
import com.datai.auth.service.ISessionManager;
import com.sforce.soap.metadata.*;
import com.sforce.ws.ConnectionException;
import com.sforce.ws.ConnectorConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* Metadata API 客户端类
* 封装 Salesforce Metadata API 调用
*/
@Slf4j
@Component
public class MetadataApiClient {
@Autowired
private ISessionManager sessionManager;
/**
* 获取 Metadata 连接
*
* @param orgConfigId 组织配置ID
* @return Metadata 连接
* @throws ConnectionException 连接异常
*/
private MetadataConnection getMetadataConnection(Long orgConfigId) throws ConnectionException {
// 使用 SessionManager 获取会话信息
SessionInfo sessionInfo = sessionManager.getSession(orgConfigId);
// 创建连接配置
ConnectorConfig config = new ConnectorConfig();
config.setSessionId(sessionInfo.getSessionId());
config.setServiceEndpoint(sessionInfo.getMetadataEndpoint());
// 创建 Metadata 连接
return new MetadataConnection(config);
}
/**
* 检索元数据
*
* @param orgConfigId 组织配置ID
* @param packageXml package.xml 内容
* @param apiVersion API 版本
* @return 检索结果
* @throws MetadataApiException Metadata API 异常
*/
public RetrieveResult retrieve(Long orgConfigId, String packageXml, String apiVersion)
throws MetadataApiException {
try {
// 获取 Metadata 连接
MetadataConnection connection = getMetadataConnection(orgConfigId);
// 创建 Retrieve 请求
com.sforce.soap.metadata.RetrieveRequest request = new com.sforce.soap.metadata.RetrieveRequest();
request.setApiVersion(Double.parseDouble(apiVersion));
request.setUnpackaged(readPackageXml(packageXml));
// 执行检索
AsyncResult asyncResult = connection.retrieve(request);
// 返回检索结果
return new RetrieveResult(asyncResult.getId(), asyncResult.getState().name());
} catch (ConnectionException e) {
log.error("检索元数据失败", e);
throw new MetadataApiException("检索元数据失败: " + e.getMessage(), e);
}
}
}
```
```
**效果**: AI 能够根据提供的代码示例生成符合项目规范的代码,减少修改和调整的工作量。
### 技巧3明确输出格式和验收标准
**描述**: 在提示词中明确输出格式和验收标准包括代码结构、单元测试、集成测试、API 文档等。
**示例**:
```
### 输出格式
#### 1. 代码结构
```
datai-salesforce-metadata/
├── src/main/java/com/datai/metadata/
│ ├── client/
│ │ ├── MetadataApiClient.java - Metadata API 客户端类
│ │ ├── MetadataApiException.java - Metadata API 异常类
│ │ ├── RetrieveResult.java - 检索结果类
│ │ ├── DeployResult.java - 部署结果类
│ │ └── JobStatus.java - 任务状态类
│ ├── config/
│ │ └── AsyncConfig.java - 异步配置类
│ ├── service/
│ │ ├── IMetadataApiService.java - Metadata API 服务接口
│ │ └── impl/
│ │ └── MetadataApiServiceImpl.java - Metadata API 服务实现
│ ├── task/
│ │ └── MetadataPollingTask.java - Metadata 轮询任务
│ └── util/
│ ├── ZipUtil.java - Zip 文件处理工具类
│ └── MetadataApiUtil.java - Metadata API 工具类
```
#### 3. 单元测试
必须为以下类编写单元测试:
- MetadataApiClient
- MetadataApiServiceImpl
- ZipUtil
- MetadataApiUtil
#### 4. 集成测试
必须编写以下集成测试:
- Metadata API 客户端调用测试
- 异步执行测试
- 状态轮询测试
- Zip 文件处理测试
- 错误处理测试
#### 5. API 文档
为以下接口编写 API 文档:
- POST /metadata/retrieve - 检索元数据
- POST /metadata/deploy - 部署元数据
- GET /metadata/job/{jobId}/status - 获取任务状态
- DELETE /metadata/job/{jobId} - 取消任务
```
**效果**: AI 能够按照要求的格式输出代码,并确保代码质量和完整性。
## 避免的坑3个
### 坑1异步执行方案选择不当
**问题描述**: 在架构决策阶段对于异步执行方案最初考虑了多种方案Spring @Async、消息队列、CompletableFuture 等),如果选择不当可能导致实现复杂或功能受限。
**避免方法**: 在 ADR-0014 中,我们详细分析了三种方案的优缺点,最终选择了使用 Spring 的 @Async 注解和线程池的方案,理由是:
1. Spring 的 @Async 注解简单易用,无需额外配置
2. Spring 提供了线程池管理,可以控制并发数和资源使用
3. 异步执行不影响系统响应,提高用户体验
4. 支持异步任务的状态跟踪和结果获取
5. 对于简单的异步任务,使用消息队列是过度设计
**经验教训**: 在做技术选型时,要充分考虑业务需求、技术约束、实现复杂度等因素,选择最合适的方案。
### 坑2状态轮询方案选择不当
**问题描述**: 对于状态轮询方案最初考虑了多种方案定时任务、回调机制、WebSocket 等),如果选择不当可能导致实现复杂或功能受限。
**避免方法**: 我们选择了使用定时任务实现状态轮询,理由是:
1. Salesforce Metadata API 不支持回调机制
2. 定时任务简单易用,无需额外引入其他库
3. 可以控制轮询频率,避免 API 限流
4. 支持超时处理,防止无限轮询
5. 支持取消轮询,提高灵活性
**经验教训**: 在选择状态轮询方案时,要考虑 API 的限制、实现复杂度、资源消耗等因素,选择最合适的方案。
### 坑3Zip 文件处理方案选择不当
**问题描述**: 对于 Zip 文件处理方案最初考虑了多种方案ZipInputStream、Apache Commons Compress、其他 Zip 库等),如果选择不当可能导致内存溢出或性能问题。
**避免方法**: 我们选择了使用 Java 的 ZipInputStream 处理 Zip 文件,理由是:
1. Java 的 ZipInputStream 是标准库,无需额外引入其他库
2. 流式处理可以避免内存溢出,支持大文件处理
3. 可以逐条读取 Zip 文件中的条目,提高处理效率
4. 支持解压到指定目录,便于后续处理
**经验教训**: 在选择 Zip 文件处理方案时,要考虑内存使用、性能、依赖复杂度等因素,选择最合适的方案。
## 模板迭代
### 模板适用性评估
本次使用的 Prompt-015 模板Metadata API客户端封装实现提示词整体适用性良好能够指导 AI 完成开发任务。但仍有以下改进空间:
### 改进建议1增加异步执行示例
**问题描述**: 当前模板没有提供详细的异步执行示例,可能导致 AI 生成的代码异步执行不够完善。
**改进建议**: 在模板中增加异步执行示例,包括:
- @Async 注解的使用
- CompletableFuture 的使用
- 异步任务的状态跟踪
- 异步任务的取消和超时处理
**示例**:
```
#### 2.3 MetadataApiServiceImpl 实现类
```java
package com.datai.metadata.service.impl;
import com.datai.metadata.client.*;
import com.datai.metadata.service.IMetadataApiService;
import com.sforce.soap.metadata.DeployOptions;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
/**
* Metadata API 服务实现类
*/
@Slf4j
@Service
public class MetadataApiServiceImpl implements IMetadataApiService {
@Autowired
private MetadataApiClient metadataApiClient;
private final ConcurrentHashMap<String, CompletableFuture<?>> jobMap = new ConcurrentHashMap<>();
@Override
@Async("metadataTaskExecutor")
public CompletableFuture<RetrieveResult> retrieveAsync(Long orgConfigId, String packageXml, String apiVersion) {
String jobId = generateJobId();
CompletableFuture<RetrieveResult> future = new CompletableFuture<>();
jobMap.put(jobId, future);
try {
// 执行检索
RetrieveResult result = metadataApiClient.retrieve(orgConfigId, packageXml, apiVersion);
// 轮询状态
pollRetrieveStatus(orgConfigId, result.getJobId(), future);
return future;
} catch (Exception e) {
log.error("异步检索元数据失败", e);
future.completeExceptionally(e);
jobMap.remove(jobId);
return future;
}
}
@Override
@Async("metadataTaskExecutor")
public CompletableFuture<DeployResult> deployAsync(Long orgConfigId, byte[] zipFile, DeployOptions options) {
String jobId = generateJobId();
CompletableFuture<DeployResult> future = new CompletableFuture<>();
jobMap.put(jobId, future);
try {
// 执行部署
DeployResult result = metadataApiClient.deploy(orgConfigId, zipFile, options);
// 轮询状态
pollDeployStatus(orgConfigId, result.getJobId(), future);
return future;
} catch (Exception e) {
log.error("异步部署元数据失败", e);
future.completeExceptionally(e);
jobMap.remove(jobId);
return future;
}
}
@Override
public void cancelJob(String jobId) {
CompletableFuture<?> future = jobMap.get(jobId);
if (future != null && !future.isDone()) {
future.cancel(true);
jobMap.remove(jobId);
log.info("任务已取消: {}", jobId);
}
}
@Override
public JobStatus getJobStatus(String jobId) {
CompletableFuture<?> future = jobMap.get(jobId);
if (future == null) {
return JobStatus.NOT_FOUND;
}
if (future.isDone()) {
if (future.isCompletedExceptionally()) {
return JobStatus.FAILED;
} else {
return JobStatus.COMPLETED;
}
} else {
return JobStatus.IN_PROGRESS;
}
}
/**
* 轮询检索状态
*
* @param orgConfigId 组织配置ID
* @param jobId 任务ID
* @param future CompletableFuture
*/
private void pollRetrieveStatus(Long orgConfigId, String jobId, CompletableFuture<RetrieveResult> future) {
// 使用定时任务轮询状态
// 实现略
}
/**
* 轮询部署状态
*
* @param orgConfigId 组织配置ID
* @param jobId 任务ID
* @param future CompletableFuture
*/
private void pollDeployStatus(Long orgConfigId, String jobId, CompletableFuture<DeployResult> future) {
// 使用定时任务轮询状态
// 实现略
}
/**
* 生成任务ID
*
* @return 任务ID
*/
private String generateJobId() {
return System.currentTimeMillis() + "-" + (int)(Math.random() * 1000);
}
}
```
```
### 改进建议2增加状态轮询示例
**问题描述**: 当前模板没有提供详细的状态轮询示例,可能导致 AI 生成的代码状态轮询不够完善。
**改进建议**: 在模板中增加状态轮询示例,包括:
- @Scheduled 注解的使用
- 轮询频率的配置
- 超时处理的实现
- 取消轮询的实现
**示例**:
```
#### 2.4 MetadataPollingTask 轮询任务
```java
package com.datai.metadata.task;
import com.datai.metadata.client.*;
import com.datai.metadata.service.IMetadataApiService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* Metadata 轮询任务
*/
@Slf4j
@Component
public class MetadataPollingTask {
@Autowired
private IMetadataApiService metadataApiService;
private final ConcurrentHashMap<String, PollingJob> pollingJobs = new ConcurrentHashMap<>();
/**
* 轮询检索状态
*
* @param orgConfigId 组织配置ID
* @param jobId 任务ID
* @param future CompletableFuture
*/
public void pollRetrieveStatus(Long orgConfigId, String jobId, CompletableFuture<RetrieveResult> future) {
PollingJob pollingJob = new PollingJob(orgConfigId, jobId, future, System.currentTimeMillis());
pollingJobs.put(jobId, pollingJob);
}
/**
* 轮询部署状态
*
* @param orgConfigId 组织配置ID
* @param jobId 任务ID
* @param future CompletableFuture
*/
public void pollDeployStatus(Long orgConfigId, String jobId, CompletableFuture<DeployResult> future) {
PollingJob pollingJob = new PollingJob(orgConfigId, jobId, future, System.currentTimeMillis());
pollingJobs.put(jobId, pollingJob);
}
/**
* 定时轮询任务状态
* 每 5 秒轮询一次
*/
@Scheduled(fixedRate = 5000)
public void pollJobStatus() {
for (Map.Entry<String, PollingJob> entry : pollingJobs.entrySet()) {
String jobId = entry.getKey();
PollingJob pollingJob = entry.getValue();
// 检查是否超时
long elapsedTime = System.currentTimeMillis() - pollingJob.getStartTime();
if (elapsedTime > TimeUnit.MINUTES.toMillis(30)) {
log.warn("任务超时: {}", jobId);
pollingJob.getFuture().completeExceptionally(new TimeoutException("任务超时"));
pollingJobs.remove(jobId);
continue;
}
// 检查是否已取消
if (pollingJob.getFuture().isCancelled()) {
log.info("任务已取消: {}", jobId);
pollingJobs.remove(jobId);
continue;
}
// 轮询状态
try {
if (pollingJob.getFuture() instanceof CompletableFuture<?>) {
// 根据任务类型轮询状态
// 实现略
}
} catch (Exception e) {
log.error("轮询任务状态失败: {}", jobId, e);
pollingJob.getFuture().completeExceptionally(e);
pollingJobs.remove(jobId);
}
}
}
/**
* 轮询任务
*/
private static class PollingJob {
private final Long orgConfigId;
private final String jobId;
private final CompletableFuture<?> future;
private final long startTime;
public PollingJob(Long orgConfigId, String jobId, CompletableFuture<?> future, long startTime) {
this.orgConfigId = orgConfigId;
this.jobId = jobId;
this.future = future;
this.startTime = startTime;
}
public Long getOrgConfigId() {
return orgConfigId;
}
public String getJobId() {
return jobId;
}
public CompletableFuture<?> getFuture() {
return future;
}
public long getStartTime() {
return startTime;
}
}
}
```
```
### 改进建议3增加错误处理示例
**问题描述**: 当前模板没有提供详细的错误处理示例,可能导致 AI 生成的代码错误处理不够完善。
**改进建议**: 在模板中增加错误处理示例,包括:
- 自定义异常类的使用
- 统一的异常处理机制
- 错误信息的返回
- 错误重试机制
**示例**:
```
#### 2.5 MetadataApiException 异常类
```java
package com.datai.metadata.client;
import lombok.Getter;
/**
* Metadata API 异常类
*/
@Getter
public class MetadataApiException extends RuntimeException {
private final String errorCode;
private final String errorMessage;
public MetadataApiException(String errorMessage) {
super(errorMessage);
this.errorCode = "METADATA_API_ERROR";
this.errorMessage = errorMessage;
}
public MetadataApiException(String errorMessage, Throwable cause) {
super(errorMessage, cause);
this.errorCode = "METADATA_API_ERROR";
this.errorMessage = errorMessage;
}
public MetadataApiException(String errorCode, String errorMessage) {
super(errorMessage);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public MetadataApiException(String errorCode, String errorMessage, Throwable cause) {
super(errorMessage, cause);
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
}
```
#### 2.6 统一异常处理
```java
package com.datai.metadata.exception;
import com.datai.common.core.domain.AjaxResult;
import com.datai.metadata.client.MetadataApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 统一异常处理
*/
@Slf4j
@RestControllerAdvice
public class MetadataExceptionHandler {
/**
* 处理 Metadata API 异常
*
* @param e Metadata API 异常
* @return 错误结果
*/
@ExceptionHandler(MetadataApiException.class)
public AjaxResult handleMetadataApiException(MetadataApiException e) {
log.error("Metadata API 异常: {}", e.getErrorMessage(), e);
return AjaxResult.error(e.getErrorCode(), e.getErrorMessage());
}
/**
* 处理其他异常
*
* @param e 异常
* @return 错误结果
*/
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e) {
log.error("系统异常", e);
return AjaxResult.error("系统异常: " + e.getMessage());
}
}
```
```
## 总结
### 成功经验
1. **遵循项目规则**: 严格按照项目规则的 6 个阶段执行,确保文档的完整性和可追溯性。
2. **架构决策合理**: 在架构决策阶段详细分析了多种方案,选择了最合适的方案。
3. **提示词设计完善**: 提示词设计清晰、详细,能够指导 AI 完成开发任务。
4. **文档更新及时**: 及时更新 CHANGELOG.md 和 index.md保持文档的同步。
### 改进方向
1. **增加异步执行示例**: 在提示词模板中增加详细的异步执行示例。
2. **增加状态轮询示例**: 在提示词模板中增加详细的状态轮询示例。
3. **增加错误处理示例**: 在提示词模板中增加详细的错误处理示例。
4. **增加性能优化建议**: 在提示词模板中增加性能优化建议。
### 下一步行动
1. **更新提示词模板**: 根据复盘结果,更新 Prompt-015 模板,增加异步执行、状态轮询、错误处理示例。
2. **应用到其他需求**: 将改进后的模板应用到其他 REQ-010 子需求的实现中。
3. **持续优化**: 在后续的开发中,持续优化提示词模板,提高 AI 生成代码的质量和效率。
## 相关文档
- [REQ-010-5.md](../requirements/REQ-010-5.md) - Metadata API客户端封装需求
- [ADR-0014.md](../decisions/adr/0014-metadata-api-client.md) - Metadata API客户端封装架构决策
- [Prompt-015.md](../prompts/015-metadata-api-client.md) - Metadata API客户端封装实现提示词
- [Session-20260118-002.md](../sessions/20260118-metadata-api-client.md) - Metadata API客户端封装执行会话
- [Change-015.md](../changelog/20260118-metadata-api-client.md) - Metadata API客户端封装变更记录
## 审核记录
| 日期 | 审核人 | 审核结果 | 审核意见 |
|------|--------|----------|----------|
| 2026-01-18 | Datai Team | 已通过 | 复盘完整,改进建议合理 |
## 变更历史
| 日期 | 版本 | 变更内容 | 变更人 |
|------|------|---------|--------|
| 2026-01-18 | v1.0.0 | 初始版本 | Datai Team |