datai/datai-scenes/datai-scene-salesforce/docs/prompts/005-metadata-api-client.md
Kris 1c9f73421f docs: 完成REQ-010-5(Metadata API客户端封装)的所有6个阶段
- 阶段1:需求定义与入库 - 更新 index.md,标记 REQ-010-5 为阶段1-需求定义与入库
- 阶段2:方案决策 - 创建 ADR 文档:0014-metadata-api-client.md
- 阶段3:提示词资产化 - 创建 Prompt 文档:005-metadata-api-client.md
- 阶段4:执行会话与代码生成 - 创建会话记录:20260119-metadata-api-client.md
- 阶段5:变更记录与归档 - 创建变更记录:20260119-metadata-api-client.md
- 阶段6:闭环复盘 - 创建复盘报告:20260119-metadata-api-client-retro.md
- 更新 index.md,标记 REQ-010-5 为已完成
- 更新 CHANGELOG.md,添加 Metadata API客户端封装的变更记录

所有文档均按照SSOT方法论创建,包括需求定义、架构决策、提示词资产化、执行会话、变更记录 和闭环复盘。
2026-01-19 11:00:41 +08:00

590 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 文件处理、部署结果解析、错误处理机制 | 待反馈 | 待改进 |