- 完成REQ-010-17(性能优化和限流处理)的所有6个阶段 - 创建ADR文档:0026-performance-optimization.md - 创建Prompt文档:027-performance-optimization.md - 创建会话记录:20260119-performance-optimization.md - 创建变更记录:20260119-performance-optimization.md - 创建复盘报告:20260119-performance-optimization-retro.md - 更新index.md和CHANGELOG.md - 完成REQ-010-2(基础实体类和Mapper创建)的前3个阶段 - 更新ADR文档:0011-entity-mapper-create.md - 创建Prompt文档:002-entity-mapper-create.md - 更新index.md 所有文档均按照SSOT方法论创建,包括需求定义、架构决策、提示词资产化、执行会话、变更记录和闭环复盘。
23 KiB
23 KiB
Prompt - Metadata API客户端封装实现
输入引用
引用相关的 docs 文档链接:
- REQ-010-5.md - Metadata API客户端封装需求
- ADR-0014.md - Metadata API客户端封装架构决策
- metadata-module.md - Salesforce Metadata API 模块说明(唯一真源)
- index.md - Salesforce SOAP API Java 客户端参考文档(唯一真源)
- REQ-010-1.md - 数据库表结构设计和创建
- REQ-010-2.md - 基础实体类和Mapper创建
- REQ-010-3.md - Salesforce组织配置管理
- REQ-010-4.md - 元数据任务定义管理
Context Maps
强制列出本次 Prompt 依赖的 Canvas 文件:
- Authentication.canvas - 项目架构视觉化展示
- 相关节点: 集成核心 - 提供与Salesforce的各种连接方式
- 相关节点: SessionManager - 会话管理,提供登录服务
目标
实现 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 类
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 接口
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<RetrieveResult> retrieveAsync(Long orgConfigId, String packageXml, String apiVersion);
/**
* 异步部署元数据
*
* @param orgConfigId 组织配置ID
* @param zipFile Zip 文件字节数组
* @param options 部署选项
* @return 部署结果
*/
CompletableFuture<DeployResult> 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 实现类
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.4 AsyncConfig 配置类
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 工具类
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 | 代码结构清晰,符合项目规范 | 无 |