# 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 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 retrieveAsync(RetrieveRequest request); /** * 异步执行 deploy 操作 */ CompletableFuture 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 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 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 文件处理、部署结果解析、错误处理机制 | 待反馈 | 待改进 |