# 闭环复盘 - 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 的限制、实现复杂度、资源消耗等因素,选择最合适的方案。 ### 坑3:Zip 文件处理方案选择不当 **问题描述**: 对于 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> 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:增加状态轮询示例 **问题描述**: 当前模板没有提供详细的状态轮询示例,可能导致 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 pollingJobs = new ConcurrentHashMap<>(); /** * 轮询检索状态 * * @param orgConfigId 组织配置ID * @param jobId 任务ID * @param future CompletableFuture */ public void pollRetrieveStatus(Long orgConfigId, String jobId, CompletableFuture 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 future) { PollingJob pollingJob = new PollingJob(orgConfigId, jobId, future, System.currentTimeMillis()); pollingJobs.put(jobId, pollingJob); } /** * 定时轮询任务状态 * 每 5 秒轮询一次 */ @Scheduled(fixedRate = 5000) public void pollJobStatus() { for (Map.Entry 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 |