590 lines
19 KiB
Markdown
590 lines
19 KiB
Markdown
# Prompt - Metadata API客户端封装
|
||
|
||
## 输入引用
|
||
|
||
引用相关的 docs 文档链接:
|
||
|
||
- [REQ-010-5.md](../requirements/REQ-010-5.md) - Metadata API客户端封装需求文档
|
||
- [REQ-010-1.md](../requirements/REQ-010-1.md) - 数据库表结构设计和创建
|
||
- [REQ-010-2.md](../requirements/REQ-010-2.md) - 基础实体类和Mapper创建
|
||
- [REQ-010-3.md](../requirements/REQ-010-3.md) - Salesforce组织配置管理
|
||
- [REQ-010-4.md](../requirements/REQ-010-4.md) - 元数据任务定义管理
|
||
- [0014-metadata-api-client.md](../decisions/adr/0014-metadata-api-client.md) - Metadata API客户端封装架构决策
|
||
|
||
## Context Maps
|
||
|
||
强制列出本次 Prompt 依赖的 Canvas 文件:
|
||
|
||
- [Authentication.canvas](../Authentication.canvas) - 项目架构视觉化展示
|
||
- **相关节点**: [集成核心](node_integration_core) - 提供与Salesforce的各种连接方式
|
||
- **相关节点**: [SessionManager](node_session_manager_detail) - 会话管理,提供登录服务
|
||
|
||
## 目标
|
||
|
||
实现 Metadata API 客户端封装,提供 retrieve() 和 deploy() 方法的调用接口,支持异步执行、状态轮询、Job ID 获取、Zip 文件下载、部署结果解析、错误处理机制等。
|
||
|
||
## 输出格式
|
||
|
||
### 代码示例(语言:Java)
|
||
|
||
#### 1. MetadataApiClient 客户端类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.client;
|
||
|
||
import com.datai.salesforce.auth.service.ISessionManager;
|
||
import com.datai.salesforce.metadata.exception.MetadataApiException;
|
||
import com.datai.salesforce.metadata.model.RetrieveRequest;
|
||
import com.datai.salesforce.metadata.model.RetrieveResult;
|
||
import com.datai.salesforce.metadata.model.DeployRequest;
|
||
import com.datai.salesforce.metadata.model.DeployResult;
|
||
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;
|
||
|
||
import java.io.ByteArrayInputStream;
|
||
import java.io.IOException;
|
||
import java.util.zip.ZipInputStream;
|
||
|
||
/**
|
||
* Metadata API 客户端
|
||
*/
|
||
@Slf4j
|
||
@Component
|
||
public class MetadataApiClient {
|
||
|
||
@Autowired
|
||
private ISessionManager sessionManager;
|
||
|
||
/**
|
||
* 创建 Metadata 连接
|
||
*/
|
||
private MetadataConnection createMetadataConnection(Long orgConfigId) throws ConnectionException {
|
||
String accessToken = sessionManager.getAccessToken(orgConfigId);
|
||
String instanceUrl = sessionManager.getInstanceUrl(orgConfigId);
|
||
|
||
ConnectorConfig config = new ConnectorConfig();
|
||
config.setSessionId(accessToken);
|
||
config.setRestEndpoint(instanceUrl + "/services/Soap/m/" + getApiVersion());
|
||
config.setCompression(true);
|
||
config.setTraceMessage(false);
|
||
|
||
return new MetadataConnection(config);
|
||
}
|
||
|
||
/**
|
||
* 获取 API 版本
|
||
*/
|
||
private String getApiVersion() {
|
||
return "60.0";
|
||
}
|
||
|
||
/**
|
||
* 执行 retrieve 操作
|
||
*/
|
||
public RetrieveResult retrieve(RetrieveRequest request) throws MetadataApiException {
|
||
try {
|
||
MetadataConnection connection = createMetadataConnection(request.getOrgConfigId());
|
||
com.sforce.soap.metadata.RetrieveRequest sfRequest = new com.sforce.soap.metadata.RetrieveRequest();
|
||
sfRequest.setApiVersion(getApiVersion());
|
||
sfRequest.setPackageNames(request.getPackageNames());
|
||
sfRequest.setSinglePackage(request.isSinglePackage());
|
||
sfRequest.setSpecificFiles(request.getSpecificFiles());
|
||
sfRequest.setUnpackaged(request.getUnpackaged());
|
||
|
||
AsyncResult asyncResult = connection.retrieve(sfRequest);
|
||
return pollRetrieveStatus(connection, asyncResult.getId(), request.getTimeout());
|
||
} catch (ConnectionException e) {
|
||
log.error("Retrieve operation failed", e);
|
||
throw new MetadataApiException("Retrieve operation failed: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 轮询 retrieve 状态
|
||
*/
|
||
private RetrieveResult pollRetrieveStatus(MetadataConnection connection, String jobId, long timeout) throws ConnectionException, MetadataApiException {
|
||
long startTime = System.currentTimeMillis();
|
||
while (true) {
|
||
AsyncResult result = connection.checkStatus(new String[]{jobId})[0];
|
||
if (result.isDone()) {
|
||
RetrieveResult retrieveResult = connection.checkRetrieveStatus(jobId);
|
||
return retrieveResult;
|
||
}
|
||
|
||
if (System.currentTimeMillis() - startTime > timeout) {
|
||
throw new MetadataApiException("Retrieve operation timed out");
|
||
}
|
||
|
||
try {
|
||
Thread.sleep(2000);
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
throw new MetadataApiException("Retrieve operation interrupted", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 执行 deploy 操作
|
||
*/
|
||
public DeployResult deploy(DeployRequest request) throws MetadataApiException {
|
||
try {
|
||
MetadataConnection connection = createMetadataConnection(request.getOrgConfigId());
|
||
com.sforce.soap.metadata.DeployOptions options = new com.sforce.soap.metadata.DeployOptions();
|
||
options.setAllowMissingFiles(request.isAllowMissingFiles());
|
||
options.setAutoUpdatePackage(request.isAutoUpdatePackage());
|
||
options.setCheckOnly(request.isCheckOnly());
|
||
options.setIgnoreWarnings(request.isIgnoreWarnings());
|
||
options.setPerformRetrieve(request.isPerformRetrieve());
|
||
options.setPurgeOnDelete(request.isPurgeOnDelete());
|
||
options.setRollbackOnError(request.isRollbackOnError());
|
||
options.setTestLevel(request.getTestLevel());
|
||
options.setRunTests(request.getRunTests());
|
||
|
||
AsyncResult asyncResult = connection.deploy(request.getZipFile(), options);
|
||
return pollDeployStatus(connection, asyncResult.getId(), request.getTimeout());
|
||
} catch (ConnectionException e) {
|
||
log.error("Deploy operation failed", e);
|
||
throw new MetadataApiException("Deploy operation failed: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 轮询 deploy 状态
|
||
*/
|
||
private DeployResult pollDeployStatus(MetadataConnection connection, String jobId, long timeout) throws ConnectionException, MetadataApiException {
|
||
long startTime = System.currentTimeMillis();
|
||
while (true) {
|
||
AsyncResult result = connection.checkStatus(new String[]{jobId})[0];
|
||
if (result.isDone()) {
|
||
DeployResult deployResult = connection.checkDeployStatus(jobId, true);
|
||
return deployResult;
|
||
}
|
||
|
||
if (System.currentTimeMillis() - startTime > timeout) {
|
||
throw new MetadataApiException("Deploy operation timed out");
|
||
}
|
||
|
||
try {
|
||
Thread.sleep(2000);
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
throw new MetadataApiException("Deploy operation interrupted", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 下载 Zip 文件
|
||
*/
|
||
public byte[] downloadZipFile(RetrieveResult result) throws MetadataApiException {
|
||
try {
|
||
return result.getZipFile();
|
||
} catch (Exception e) {
|
||
log.error("Download zip file failed", e);
|
||
throw new MetadataApiException("Download zip file failed: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 解压 Zip 文件
|
||
*/
|
||
public void unzipFile(byte[] zipFile, String targetDir) throws MetadataApiException {
|
||
try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(zipFile))) {
|
||
java.util.zip.ZipEntry entry;
|
||
while ((entry = zis.getNextEntry()) != null) {
|
||
java.io.File file = new java.io.File(targetDir, entry.getName());
|
||
if (entry.isDirectory()) {
|
||
file.mkdirs();
|
||
} else {
|
||
file.getParentFile().mkdirs();
|
||
try (java.io.FileOutputStream fos = new java.io.FileOutputStream(file)) {
|
||
byte[] buffer = new byte[1024];
|
||
int len;
|
||
while ((len = zis.read(buffer)) > 0) {
|
||
fos.write(buffer, 0, len);
|
||
}
|
||
}
|
||
}
|
||
zis.closeEntry();
|
||
}
|
||
} catch (IOException e) {
|
||
log.error("Unzip file failed", e);
|
||
throw new MetadataApiException("Unzip file failed: " + e.getMessage(), e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 2. RetrieveRequest 类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.model;
|
||
|
||
import lombok.Data;
|
||
|
||
/**
|
||
* Retrieve 请求
|
||
*/
|
||
@Data
|
||
public class RetrieveRequest {
|
||
private Long orgConfigId;
|
||
private String[] packageNames;
|
||
private boolean singlePackage;
|
||
private String[] specificFiles;
|
||
private com.sforce.soap.metadata.Package unpackaged;
|
||
private long timeout = 300000; // 5 minutes
|
||
}
|
||
```
|
||
|
||
#### 3. RetrieveResult 类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.model;
|
||
|
||
import lombok.Data;
|
||
|
||
/**
|
||
* Retrieve 结果
|
||
*/
|
||
@Data
|
||
public class RetrieveResult {
|
||
private String id;
|
||
private boolean success;
|
||
private String status;
|
||
private String message;
|
||
private byte[] zipFile;
|
||
private String[] fileProperties;
|
||
}
|
||
```
|
||
|
||
#### 4. DeployRequest 类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.model;
|
||
|
||
import lombok.Data;
|
||
|
||
/**
|
||
* Deploy 请求
|
||
*/
|
||
@Data
|
||
public class DeployRequest {
|
||
private Long orgConfigId;
|
||
private byte[] zipFile;
|
||
private boolean allowMissingFiles;
|
||
private boolean autoUpdatePackage;
|
||
private boolean checkOnly;
|
||
private boolean ignoreWarnings;
|
||
private boolean performRetrieve;
|
||
private boolean purgeOnDelete;
|
||
private boolean rollbackOnError;
|
||
private String testLevel;
|
||
private String[] runTests;
|
||
private long timeout = 300000; // 5 minutes
|
||
}
|
||
```
|
||
|
||
#### 5. DeployResult 类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.model;
|
||
|
||
import lombok.Data;
|
||
|
||
import java.util.List;
|
||
|
||
/**
|
||
* Deploy 结果
|
||
*/
|
||
@Data
|
||
public class DeployResult {
|
||
private String id;
|
||
private boolean success;
|
||
private String status;
|
||
private String message;
|
||
private String startDate;
|
||
private String finishDate;
|
||
private int numberComponentErrors;
|
||
private int numberComponentsDeployed;
|
||
private int numberComponentsTotal;
|
||
private int numberTestErrors;
|
||
private int numberTestsCompleted;
|
||
private int numberTestsTotal;
|
||
private String testCoverage;
|
||
private List<DeployMessage> messages;
|
||
}
|
||
```
|
||
|
||
#### 6. DeployMessage 类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.model;
|
||
|
||
import lombok.Data;
|
||
|
||
/**
|
||
* Deploy 消息
|
||
*/
|
||
@Data
|
||
public class DeployMessage {
|
||
private String fileName;
|
||
private String componentName;
|
||
private String problem;
|
||
private String line;
|
||
private String column;
|
||
private boolean success;
|
||
}
|
||
```
|
||
|
||
#### 7. MetadataApiException 类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.exception;
|
||
|
||
/**
|
||
* Metadata API 异常
|
||
*/
|
||
public class MetadataApiException extends RuntimeException {
|
||
public MetadataApiException(String message) {
|
||
super(message);
|
||
}
|
||
|
||
public MetadataApiException(String message, Throwable cause) {
|
||
super(message, cause);
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 8. 异步配置类
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.config;
|
||
|
||
import org.springframework.context.annotation.Bean;
|
||
import org.springframework.context.annotation.Configuration;
|
||
import org.springframework.scheduling.annotation.EnableAsync;
|
||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||
|
||
import java.util.concurrent.Executor;
|
||
|
||
/**
|
||
* 异步配置
|
||
*/
|
||
@Configuration
|
||
@EnableAsync
|
||
public class AsyncConfig {
|
||
|
||
@Bean(name = "metadataTaskExecutor")
|
||
public Executor metadataTaskExecutor() {
|
||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||
executor.setCorePoolSize(5);
|
||
executor.setMaxPoolSize(10);
|
||
executor.setQueueCapacity(100);
|
||
executor.setThreadNamePrefix("metadata-async-");
|
||
executor.initialize();
|
||
return executor;
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 9. 异步服务接口
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.service;
|
||
|
||
import com.datai.salesforce.metadata.model.DeployRequest;
|
||
import com.datai.salesforce.metadata.model.DeployResult;
|
||
import com.datai.salesforce.metadata.model.RetrieveRequest;
|
||
import com.datai.salesforce.metadata.model.RetrieveResult;
|
||
|
||
import java.util.concurrent.CompletableFuture;
|
||
|
||
/**
|
||
* Metadata API 异步服务
|
||
*/
|
||
public interface IMetadataApiAsyncService {
|
||
|
||
/**
|
||
* 异步执行 retrieve 操作
|
||
*/
|
||
CompletableFuture<RetrieveResult> retrieveAsync(RetrieveRequest request);
|
||
|
||
/**
|
||
* 异步执行 deploy 操作
|
||
*/
|
||
CompletableFuture<DeployResult> deployAsync(DeployRequest request);
|
||
}
|
||
```
|
||
|
||
#### 10. 异步服务实现
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.service.impl;
|
||
|
||
import com.datai.salesforce.metadata.client.MetadataApiClient;
|
||
import com.datai.salesforce.metadata.exception.MetadataApiException;
|
||
import com.datai.salesforce.metadata.model.DeployRequest;
|
||
import com.datai.salesforce.metadata.model.DeployResult;
|
||
import com.datai.salesforce.metadata.model.RetrieveRequest;
|
||
import com.datai.salesforce.metadata.model.RetrieveResult;
|
||
import com.datai.salesforce.metadata.service.IMetadataApiAsyncService;
|
||
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;
|
||
|
||
/**
|
||
* Metadata API 异步服务实现
|
||
*/
|
||
@Slf4j
|
||
@Service
|
||
public class MetadataApiAsyncServiceImpl implements IMetadataApiAsyncService {
|
||
|
||
@Autowired
|
||
private MetadataApiClient metadataApiClient;
|
||
|
||
@Async("metadataTaskExecutor")
|
||
@Override
|
||
public CompletableFuture<RetrieveResult> retrieveAsync(RetrieveRequest request) {
|
||
try {
|
||
RetrieveResult result = metadataApiClient.retrieve(request);
|
||
return CompletableFuture.completedFuture(result);
|
||
} catch (MetadataApiException e) {
|
||
log.error("Async retrieve failed", e);
|
||
return CompletableFuture.failedFuture(e);
|
||
}
|
||
}
|
||
|
||
@Async("metadataTaskExecutor")
|
||
@Override
|
||
public CompletableFuture<DeployResult> deployAsync(DeployRequest request) {
|
||
try {
|
||
DeployResult result = metadataApiClient.deploy(request);
|
||
return CompletableFuture.completedFuture(result);
|
||
} catch (MetadataApiException e) {
|
||
log.error("Async deploy failed", e);
|
||
return CompletableFuture.failedFuture(e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
#### 11. 单元测试
|
||
|
||
```java
|
||
package com.datai.salesforce.metadata.client;
|
||
|
||
import com.datai.salesforce.metadata.exception.MetadataApiException;
|
||
import com.datai.salesforce.metadata.model.DeployRequest;
|
||
import com.datai.salesforce.metadata.model.DeployResult;
|
||
import com.datai.salesforce.metadata.model.RetrieveRequest;
|
||
import com.datai.salesforce.metadata.model.RetrieveResult;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.junit.jupiter.api.extension.ExtendWith;
|
||
import org.mockito.InjectMocks;
|
||
import org.mockito.Mock;
|
||
import org.mockito.junit.jupiter.MockitoExtension;
|
||
|
||
import static org.junit.jupiter.api.Assertions.*;
|
||
import static org.mockito.ArgumentMatchers.any;
|
||
import static org.mockito.Mockito.*;
|
||
|
||
@ExtendWith(MockitoExtension.class)
|
||
class MetadataApiClientTest {
|
||
|
||
@Mock
|
||
private ISessionManager sessionManager;
|
||
|
||
@InjectMocks
|
||
private MetadataApiClient metadataApiClient;
|
||
|
||
@Test
|
||
void testRetrieve() throws MetadataApiException {
|
||
RetrieveRequest request = new RetrieveRequest();
|
||
request.setOrgConfigId(1L);
|
||
request.setTimeout(30000);
|
||
|
||
when(sessionManager.getAccessToken(any())).thenReturn("test-token");
|
||
when(sessionManager.getInstanceUrl(any())).thenReturn("https://test.salesforce.com");
|
||
|
||
RetrieveResult result = metadataApiClient.retrieve(request);
|
||
|
||
assertNotNull(result);
|
||
}
|
||
|
||
@Test
|
||
void testDeploy() throws MetadataApiException {
|
||
DeployRequest request = new DeployRequest();
|
||
request.setOrgConfigId(1L);
|
||
request.setZipFile(new byte[0]);
|
||
request.setTimeout(30000);
|
||
|
||
when(sessionManager.getAccessToken(any())).thenReturn("test-token");
|
||
when(sessionManager.getInstanceUrl(any())).thenReturn("https://test.salesforce.com");
|
||
|
||
DeployResult result = metadataApiClient.deploy(request);
|
||
|
||
assertNotNull(result);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 约束
|
||
|
||
列出使用此提示词时的约束条件,例如:
|
||
|
||
- **技术栈限制**: 必须使用 Salesforce WSC (Web Service Connector) 库
|
||
- **架构约束**: 必须遵循 Authentication.canvas 中定义的架构和调用关系
|
||
- **模块约束**: 必须在 datai-salesforce-metadata 模块下实现
|
||
- **认证约束**: 必须使用 SessionManager 进行会话管理和自动重新登录
|
||
- **API 约束**: 必须遵循 Salesforce Metadata API 调用规范
|
||
- **异步约束**: 必须使用异步线程池执行长时间任务
|
||
- **依赖约束**: 必须依赖于 REQ-010-1, REQ-010-2, REQ-010-3, REQ-010-4
|
||
|
||
## Rule Set
|
||
|
||
"请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。"
|
||
|
||
**具体规则**:
|
||
- 必须使用 Canvas 中定义的类名和方法名
|
||
- 必须遵循 Canvas 中定义的调用关系
|
||
- 必须参考 Canvas 中的流程图逻辑
|
||
- 必须使用 SessionManager 进行会话管理和自动重新登录
|
||
- 必须使用现有的认证模块进行 OAuth 认证
|
||
- 必须使用现有的集成核心功能进行 API 调用
|
||
- 必须遵循现有的异常处理机制
|
||
- 必须遵循现有的日志记录规范
|
||
|
||
## 验收标准
|
||
|
||
定义验证输出质量的具体标准,例如:
|
||
|
||
- **功能完整性**: 所有 API 方法能够正常工作,异步执行机制正常
|
||
- **代码规范性**: 代码符合项目编码规范,有清晰的注释
|
||
- **性能要求**: 异步执行不影响系统响应,轮询频率合理
|
||
- **可维护性**: 代码结构清晰,易于扩展和维护
|
||
- **可测试性**: 代码易于单元测试和集成测试
|
||
|
||
## 风险
|
||
|
||
识别使用此提示词可能带来的风险,例如:
|
||
|
||
- **WSC 库版本兼容性风险**: WSC 库版本可能与项目其他依赖不兼容
|
||
- **异步执行风险**: 异步执行机制复杂可能导致状态管理困难
|
||
- **状态轮询风险**: 状态轮询频率不当可能导致 API 限流
|
||
- **Zip 文件处理风险**: Zip 文件处理不当可能导致内存溢出
|
||
- **错误处理风险**: 错误处理不完善可能导致任务失败无法恢复
|
||
|
||
## 使用记录
|
||
|
||
| 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 |
|
||
|------|---------|---------|---------|------|----------|
|
||
| 2026-01-19 | Metadata API客户端封装实现 | REQ-010-5 需求文档、ADR 文档 | MetadataApiClient 客户端类、retrieve() 方法、deploy() 方法、状态轮询机制、Zip 文件处理、部署结果解析、错误处理机制 | 待反馈 | 待改进 |
|