692 lines
23 KiB
Markdown
692 lines
23 KiB
Markdown
# 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 | 代码结构清晰,符合项目规范 | 无 |
|