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

23 KiB
Raw Blame History

Prompt - Metadata API客户端封装实现

输入引用

引用相关的 docs 文档链接:

Context Maps

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

目标

实现 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 代码结构清晰,符合项目规范