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

19 KiB
Raw Blame History

Prompt - Metadata API客户端封装

输入引用

引用相关的 docs 文档链接:

Context Maps

强制列出本次 Prompt 依赖的 Canvas 文件:

目标

实现 Metadata API 客户端封装,提供 retrieve() 和 deploy() 方法的调用接口支持异步执行、状态轮询、Job ID 获取、Zip 文件下载、部署结果解析、错误处理机制等。

输出格式

代码示例语言Java

1. MetadataApiClient 客户端类

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 类

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 类

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 类

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 类

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 类

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 类

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. 异步配置类

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. 异步服务接口

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. 异步服务实现

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. 单元测试

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