# Prompt - Metadata API客户端封装实现 ## 输入引用 引用相关的 docs 文档链接: - [REQ-010-5.md](../requirements/REQ-010-5.md) - Metadata API客户端封装需求 - [ADR-0014.md](../decisions/adr/0014-metadata-api-client.md) - Metadata API客户端封装架构决策 - [metadata-module.md](../reference-code/com/docs/metadata-module.md) - Salesforce Metadata API 模块说明(唯一真源) - [index.md](../reference-code/com/docs/index.md) - Salesforce SOAP API Java 客户端参考文档(唯一真源) - [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) - 元数据任务定义管理 ## Context Maps 强制列出本次 Prompt 依赖的 Canvas 文件: - [Authentication.canvas](../Authentication.canvas) - 项目架构视觉化展示 - **相关节点**: [集成核心](node_integration_core) - 提供与Salesforce的各种连接方式 - **相关节点**: [SessionManager](node_session_manager_detail) - 会话管理,提供登录服务 ## 目标 实现 Salesforce Metadata API 客户端封装,提供 retrieve() 和 deploy() 方法的调用接口,支持异步执行和状态轮询。 ## 输出格式 ### 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 工具类 └── src/main/resources/ └── mapper/ └── metadata/ └── MetadataApiMapper.xml - Metadata API Mapper XML ``` ### 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; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.zip.ZipInputStream; /** * 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); } } /** * 部署元数据 * * @param orgConfigId 组织配置ID * @param zipFile Zip 文件字节数组 * @param options 部署选项 * @return 部署结果 * @throws MetadataApiException Metadata API 异常 */ public DeployResult deploy(Long orgConfigId, byte[] zipFile, DeployOptions options) throws MetadataApiException { try { // 获取 Metadata 连接 MetadataConnection connection = getMetadataConnection(orgConfigId); // 执行部署 AsyncResult asyncResult = connection.deploy(zipFile, options); // 返回部署结果 return new DeployResult(asyncResult.getId(), asyncResult.getState().name()); } catch (ConnectionException e) { log.error("部署元数据失败", e); throw new MetadataApiException("部署元数据失败: " + e.getMessage(), e); } } /** * 检查检索状态 * * @param orgConfigId 组织配置ID * @param jobId 任务ID * @return 检索状态 * @throws MetadataApiException Metadata API 异常 */ public RetrieveResult checkRetrieveStatus(Long orgConfigId, String jobId) throws MetadataApiException { try { // 获取 Metadata 连接 MetadataConnection connection = getMetadataConnection(orgConfigId); // 检查状态 com.sforce.soap.metadata.RetrieveResult result = connection.checkRetrieveStatus(jobId); // 返回检索结果 RetrieveResult retrieveResult = new RetrieveResult(jobId, result.getStatus().name()); retrieveResult.setZipFile(result.getZipFile()); retrieveResult.setMessages(result.getMessages()); return retrieveResult; } catch (ConnectionException e) { log.error("检查检索状态失败", e); throw new MetadataApiException("检查检索状态失败: " + e.getMessage(), e); } } /** * 检查部署状态 * * @param orgConfigId 组织配置ID * @param jobId 任务ID * @return 部署状态 * @throws MetadataApiException Metadata API 异常 */ public DeployResult checkDeployStatus(Long orgConfigId, String jobId) throws MetadataApiException { try { // 获取 Metadata 连接 MetadataConnection connection = getMetadataConnection(orgConfigId); // 检查状态 com.sforce.soap.metadata.DeployResult result = connection.checkDeployStatus(jobId, false); // 返回部署结果 DeployResult deployResult = new DeployResult(jobId, result.getStatus().name()); deployResult.setSuccess(result.isSuccess()); deployResult.setNumberComponentErrors(result.getNumberComponentErrors()); deployResult.setNumberComponentsDeployed(result.getNumberComponentsDeployed()); deployResult.setNumberComponentsTotal(result.getNumberComponentsTotal()); deployResult.setNumberTestErrors(result.getNumberTestErrors()); deployResult.setNumberTestsCompleted(result.getNumberTestsCompleted()); deployResult.setNumberTestsTotal(result.getNumberTestsTotal()); deployResult.setDetails(result.getDetails()); deployResult.setMessages(result.getMessages()); return deployResult; } catch (ConnectionException e) { log.error("检查部署状态失败", e); throw new MetadataApiException("检查部署状态失败: " + e.getMessage(), e); } } /** * 读取 package.xml * * @param packageXml package.xml 内容 * @return Package 对象 */ private Package readPackageXml(String packageXml) { // 使用 XML 解析器解析 package.xml // 返回 Package 对象 return new Package(); } } ``` #### 2.2 MetadataApiService 接口 ```java package com.datai.metadata.service; import com.datai.metadata.client.DeployResult; import com.datai.metadata.client.MetadataApiException; import com.datai.metadata.client.RetrieveResult; import com.sforce.soap.metadata.DeployOptions; import java.util.concurrent.CompletableFuture; /** * Metadata API 服务接口 */ public interface IMetadataApiService { /** * 异步检索元数据 * * @param orgConfigId 组织配置ID * @param packageXml package.xml 内容 * @param apiVersion API 版本 * @return 检索结果 */ CompletableFuture retrieveAsync(Long orgConfigId, String packageXml, String apiVersion); /** * 异步部署元数据 * * @param orgConfigId 组织配置ID * @param zipFile Zip 文件字节数组 * @param options 部署选项 * @return 部署结果 */ CompletableFuture deployAsync(Long orgConfigId, byte[] zipFile, DeployOptions options); /** * 取消任务 * * @param jobId 任务ID */ void cancelJob(String jobId); /** * 获取任务状态 * * @param jobId 任务ID * @return 任务状态 */ JobStatus getJobStatus(String jobId); } ``` #### 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> jobMap = new ConcurrentHashMap<>(); @Override @Async("metadataTaskExecutor") public CompletableFuture retrieveAsync(Long orgConfigId, String packageXml, String apiVersion) { String jobId = generateJobId(); CompletableFuture 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 deployAsync(Long orgConfigId, byte[] zipFile, DeployOptions options) { String jobId = generateJobId(); CompletableFuture 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 future) { // 使用定时任务轮询状态 // 实现略 } /** * 轮询部署状态 * * @param orgConfigId 组织配置ID * @param jobId 任务ID * @param future CompletableFuture */ private void pollDeployStatus(Long orgConfigId, String jobId, CompletableFuture future) { // 使用定时任务轮询状态 // 实现略 } /** * 生成任务ID * * @return 任务ID */ private String generateJobId() { return System.currentTimeMillis() + "-" + (int)(Math.random() * 1000); } } ``` #### 2.4 AsyncConfig 配置类 ```java package com.datai.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; import java.util.concurrent.ThreadPoolExecutor; /** * 异步配置类 */ @Configuration @EnableAsync public class AsyncConfig { /** * Metadata 任务线程池 * * @return 线程池执行器 */ @Bean("metadataTaskExecutor") public Executor metadataTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("metadata-task-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ``` #### 2.5 ZipUtil 工具类 ```java package com.datai.metadata.util; import lombok.extern.slf4j.Slf4j; import java.io.*; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; /** * Zip 文件处理工具类 */ @Slf4j public class ZipUtil { /** * 解压 Zip 文件 * * @param zipFile Zip 文件字节数组 * @param destDir 目标目录 * @throws IOException IO 异常 */ public static void unzip(byte[] zipFile, String destDir) throws IOException { File destDirectory = new File(destDir); if (!destDirectory.exists()) { destDirectory.mkdirs(); } try (ByteArrayInputStream bais = new ByteArrayInputStream(zipFile); ZipInputStream zis = new ZipInputStream(bais)) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { File file = new File(destDir, entry.getName()); if (entry.isDirectory()) { file.mkdirs(); } else { try (FileOutputStream fos = new FileOutputStream(file)) { byte[] buffer = new byte[1024]; int len; while ((len = zis.read(buffer)) > 0) { fos.write(buffer, 0, len); } } } zis.closeEntry(); } } } /** * 读取 Zip 文件内容 * * @param zipFile Zip 文件字节数组 * @param fileName 文件名 * @return 文件内容 * @throws IOException IO 异常 */ public static String readZipFile(byte[] zipFile, String fileName) throws IOException { try (ByteArrayInputStream bais = new ByteArrayInputStream(zipFile); ZipInputStream zis = new ZipInputStream(bais)) { ZipEntry entry; while ((entry = zis.getNextEntry()) != null) { if (entry.getName().equals(fileName)) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int len; while ((len = zis.read(buffer)) > 0) { baos.write(buffer, 0, len); } return baos.toString(); } zis.closeEntry(); } } return null; } } ``` ### 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} - 取消任务 ## 约束 ### 技术栈限制 - **后端**: Spring Boot 3, MyBatis Plus, MySQL - **Salesforce API**: Metadata API, Partner API - **Salesforce WSC**: 必须使用 Salesforce WSC (Web Service Connector) 库 - **异步执行**: 必须使用 Spring 的 @Async 注解和线程池 - **状态轮询**: 必须使用定时任务实现状态轮询 ### 架构约束 - **模块约束**: 必须在 datai-salesforce-metadata 模块下实现 - **认证约束**: 必须使用 SessionManager 进行会话管理和自动重新登录 - **API 约束**: 必须遵循 Salesforce Metadata API 调用规范 ### 性能要求 - **异步执行**: 异步执行不影响系统响应 - **轮询频率**: 轮询频率合理(如每 5 秒轮询一次) - **内存使用**: Zip 文件处理时内存使用合理,避免内存溢出 ### 安全性要求 - **敏感信息**: 不记录敏感信息(如密码、令牌) - **错误信息**: 不暴露系统内部信息 ### 兼容性要求 - **API 版本**: 支持 API 版本 58.0、57.0、56.0 - **Java 版本**: 支持 Java 17 ## Rule Set "请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。" **具体规则**: - 必须使用 Canvas 中定义的类名和方法名 - 必须遵循 Canvas 中定义的调用关系 - 必须使用 SessionManager 进行会话管理和自动重新登录 - 必须使用现有的认证模块进行 OAuth 认证 - 必须使用现有的集成核心功能进行 API 调用 - 必须遵循现有的异常处理机制 - 必须遵循现有的日志记录规范 ## 验收标准 ### 功能完整性 - MetadataApiClient 创建成功,使用 Salesforce WSC 库 - retrieve() 方法实现成功,支持异步执行、Job ID 获取、状态轮询、Zip 文件下载 - deploy() 方法实现成功,支持异步执行、Job ID 获取、状态轮询、部署结果解析 - 状态轮询机制正常工作,轮询频率合理,支持超时处理和取消轮询 - Zip 文件处理正常工作,下载成功,解压成功,支持大文件处理,内存使用合理 - 部署结果解析正常工作,错误信息提取正确,代码覆盖率提取正确,支持多种部署状态 - 错误处理机制正常工作,支持多种异常类型,错误信息详细,支持错误重试 ### 代码正确性 - 代码符合项目编码规范,有清晰的注释 - 代码结构清晰,易于扩展和维护 - 代码易于单元测试和集成测试 ### 性能指标 - 异步执行不影响系统响应,系统响应时间在可接受范围内 - 轮询频率合理,不会导致 API 限流 - Zip 文件处理时内存使用合理,不会导致内存溢出 ## 风险 ### 输出质量风险 - **代码质量**: AI 生成的代码可能存在 bug 或性能问题 - **缓解措施**: 编写详细的单元测试和集成测试,进行代码审查 ### 技术实现风险 - **WSC 库版本**: WSC 库版本兼容性问题,可能不支持最新的 Metadata API 功能 - **缓解措施**: 选择稳定的 WSC 库版本,定期更新 - **异步执行**: 异步执行机制复杂可能导致状态管理困难 - **缓解措施**: 使用 ConcurrentHashMap 存储异步任务状态,提供任务取消和超时处理 - **状态轮询**: 状态轮询频率不当可能导致 API 限流 - **缓解措施**: 设置合理的轮询频率(如每 5 秒轮询一次),使用超时机制防止无限轮询 - **Zip 文件处理**: Zip 文件处理不当可能导致内存溢出 - **缓解措施**: 使用流式处理,避免一次性加载整个 Zip 文件,支持大文件处理 - **错误处理**: 错误处理不完善可能导致任务失败无法恢复 - **缓解措施**: 使用统一的异常处理机制,使用重试机制处理网络异常 ### 时间成本风险 - **开发周期**: 开发复杂度高,可能延长开发周期 - **缓解措施**: 分阶段实施,先实现核心功能,再实现辅助功能 ## 使用记录 | 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 | |------|---------|---------|---------|------|----------| | 2026-01-18 | Metadata API 客户端封装实现 | REQ-010-5, ADR-0014 | MetadataApiClient, MetadataApiService, AsyncConfig, ZipUtil | 代码结构清晰,符合项目规范 | 无 |