datai/datai-scenes/datai-scene-salesforce/docs/prompts/015-metadata-api-client.md
Kris 2e6f087732 docs: 完成REQ-010-17和REQ-010-2的文档创建
- 完成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方法论创建,包括需求定义、架构决策、提示词资产化、执行会话、变更记录和闭环复盘。
2026-01-19 10:06:09 +08:00

692 lines
23 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客户端封装需求
- [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<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 实现类
```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<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 配置类
```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 | 代码结构清晰,符合项目规范 | 无 |