# Prompt - 元数据拉取核心功能实现 ## 输入引用 引用相关的 docs 文档链接: - [REQ-010-6.md](../requirements/REQ-010-6.md) - 元数据拉取核心功能需求 - [ADR-0015.md](../decisions/adr/0015-metadata-retrieve-core.md) - 元数据拉取核心功能架构决策 - [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) - 元数据任务定义管理 - [REQ-010-5.md](../requirements/REQ-010-5.md) - Metadata API客户端封装 ## Context Maps 强制列出本次 Prompt 依赖的 Canvas 文件: - [Authentication.canvas](../Authentication.canvas) - 项目架构视觉化展示 - **相关节点**: [集成核心](node_integration_core) - 提供与Salesforce的各种连接方式 - **相关节点**: [SessionManager](node_session_manager_detail) - 会话管理,提供登录服务 ## 目标 实现 Salesforce 元数据拉取的核心功能,包括手动触发拉取、异步拉取执行、状态监控、拉取历史记录、拉取进度查询、拉取取消功能。 ## 输出格式 ### 1. 代码结构 ``` datai-salesforce-metadata/ ├── src/main/java/com/datai/metadata/ │ ├── controller/ │ │ └── MetadataRetrieveController.java - 元数据拉取控制器 │ ├── service/ │ │ ├── IMetadataRetrieveService.java - 元数据拉取服务接口 │ │ └── impl/ │ │ └── MetadataRetrieveServiceImpl.java - 元数据拉取服务实现 │ ├── enums/ │ │ └── RetrieveStatus.java - 拉取状态枚举 │ └── dto/ │ ├── RetrieveTriggerRequest.java - 拉取触发请求DTO │ ├── RetrieveTriggerResponse.java - 拉取触发响应DTO │ ├── RetrieveProgressResponse.java - 拉取进度响应DTO │ └── RetrieveHistoryResponse.java - 拉取历史响应DTO └── src/main/resources/ └── mapper/ └── metadata/ └── DataiMetaJobExecutionMapper.xml - 作业执行Mapper XML ``` ### 2. 代码示例 #### 2.1 RetrieveStatus 枚举 ```java package com.datai.metadata.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonValue; /** * 拉取状态枚举 */ public enum RetrieveStatus { PENDING("Pending", "待处理"), PROCESSING("Processing", "处理中"), SUCCESS("Success", "成功"), FAILED("Failed", "失败"), PARTIAL_SUCCESS("Partial_Success", "部分成功"), CANCELLED("Cancelled", "已取消"); @EnumValue @JsonValue private final String code; private final String description; RetrieveStatus(String code, String description) { this.code = code; this.description = description; } public String getCode() { return code; } public String getDescription() { return description; } } ``` #### 2.2 IMetadataRetrieveService 接口 ```java package com.datai.metadata.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.datai.metadata.dto.*; import com.datai.metadata.entity.DataiMetaJobExecution; /** * 元数据拉取服务接口 */ public interface IMetadataRetrieveService { /** * 触发拉取 * * @param request 拉取触发请求 * @return 拉取触发响应 */ RetrieveTriggerResponse triggerRetrieve(RetrieveTriggerRequest request); /** * 查询拉取进度 * * @param jobId 作业ID * @return 拉取进度响应 */ RetrieveProgressResponse getRetrieveProgress(String jobId); /** * 取消拉取 * * @param jobId 作业ID */ void cancelRetrieve(String jobId); /** * 查询拉取历史 * * @param pageNum 页码 * @param pageSize 页大小 * @param taskId 任务ID * @param orgConfigId 组织配置ID * @param status 状态 * @return 拉取历史响应 */ IPage getRetrieveHistory(int pageNum, int pageSize, Long taskId, Long orgConfigId, String status); } ``` #### 2.3 MetadataRetrieveServiceImpl 实现类 ```java package com.datai.metadata.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.datai.common.core.domain.AjaxResult; import com.datai.metadata.client.*; import com.datai.metadata.dto.*; import com.datai.metadata.entity.DataiMetaJobExecution; import com.datai.metadata.entity.DataiMetaTask; import com.datai.metadata.enums.RetrieveStatus; import com.datai.metadata.mapper.DataiMetaJobExecutionMapper; import com.datai.metadata.mapper.DataiMetaTaskMapper; import com.datai.metadata.service.IMetadataApiService; import com.datai.metadata.service.IMetadataRetrieveService; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.concurrent.ConcurrentHashMap; /** * 元数据拉取服务实现类 */ @Slf4j @Service public class MetadataRetrieveServiceImpl implements IMetadataRetrieveService { @Autowired private IMetadataApiService metadataApiService; @Autowired private DataiMetaJobExecutionMapper jobExecutionMapper; @Autowired private DataiMetaTaskMapper taskMapper; private final ConcurrentHashMap progressMap = new ConcurrentHashMap<>(); @Override @Transactional public RetrieveTriggerResponse triggerRetrieve(RetrieveTriggerRequest request) { // 查询任务 DataiMetaTask task = taskMapper.selectById(request.getTaskId()); if (task == null) { throw new BusinessException("任务不存在"); } // 创建作业执行记录 DataiMetaJobExecution jobExecution = new DataiMetaJobExecution(); jobExecution.setTaskId(request.getTaskId()); jobExecution.setOrgConfigId(request.getOrgConfigId()); jobExecution.setJobType(task.getTaskType()); jobExecution.setStatus(RetrieveStatus.PENDING.getCode()); jobExecution.setCreatedBy(request.getCreatedBy()); jobExecutionMapper.insert(jobExecution); // 异步执行拉取 executeRetrieveAsync(jobExecution.getId(), task); // 返回响应 RetrieveTriggerResponse response = new RetrieveTriggerResponse(); response.setJobId(jobExecution.getId().toString()); response.setStatus(RetrieveStatus.PENDING.getCode()); response.setMessage("拉取任务已创建"); return response; } @Override public RetrieveProgressResponse getRetrieveProgress(String jobId) { // 查询作业执行记录 DataiMetaJobExecution jobExecution = jobExecutionMapper.selectById(Long.parseLong(jobId)); if (jobExecution == null) { throw new BusinessException("作业不存在"); } // 查询进度 RetrieveProgress progress = progressMap.get(jobId); if (progress == null) { progress = new RetrieveProgress(); progress.setStatus(jobExecution.getStatus()); progress.setProgress(jobExecution.getStatus().equals(RetrieveStatus.SUCCESS.getCode()) ? 100 : 0); } // 返回响应 RetrieveProgressResponse response = new RetrieveProgressResponse(); response.setJobId(jobId); response.setStatus(progress.getStatus()); response.setProgress(progress.getProgress()); response.setMessage(progress.getMessage()); return response; } @Override @Transactional public void cancelRetrieve(String jobId) { // 查询作业执行记录 DataiMetaJobExecution jobExecution = jobExecutionMapper.selectById(Long.parseLong(jobId)); if (jobExecution == null) { throw new BusinessException("作业不存在"); } // 检查状态 if (!RetrieveStatus.PROCESSING.getCode().equals(jobExecution.getStatus())) { throw new BusinessException("只能取消处理中的作业"); } // 取消作业 metadataApiService.cancelJob(jobId); // 更新状态 jobExecution.setStatus(RetrieveStatus.CANCELLED.getCode()); jobExecutionMapper.updateById(jobExecution); // 清除进度 progressMap.remove(jobId); log.info("拉取任务已取消: {}", jobId); } @Override public IPage getRetrieveHistory(int pageNum, int pageSize, Long taskId, Long orgConfigId, String status) { // 构建查询条件 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("job_type", "retrieve"); if (taskId != null) { queryWrapper.eq("task_id", taskId); } if (orgConfigId != null) { queryWrapper.eq("org_config_id", orgConfigId); } if (status != null && !status.isEmpty()) { queryWrapper.eq("status", status); } queryWrapper.orderByDesc("created_time"); // 分页查询 Page page = new Page<>(pageNum, pageSize); IPage jobExecutionPage = jobExecutionMapper.selectPage(page, queryWrapper); // 转换为响应 IPage responsePage = new Page<>(pageNum, pageSize, jobExecutionPage.getTotal()); for (DataiMetaJobExecution jobExecution : jobExecutionPage.getRecords()) { RetrieveHistoryResponse response = new RetrieveHistoryResponse(); BeanUtils.copyProperties(jobExecution, response); responsePage.getRecords().add(response); } return responsePage; } /** * 异步执行拉取 * * @param jobExecutionId 作业执行ID * @param task 任务 */ @Async("metadataTaskExecutor") public void executeRetrieveAsync(Long jobExecutionId, DataiMetaTask task) { String jobId = jobExecutionId.toString(); try { // 更新状态为处理中 updateJobStatus(jobExecutionId, RetrieveStatus.PROCESSING); // 更新进度 updateProgress(jobId, RetrieveStatus.PROCESSING.getCode(), 0, "正在拉取元数据"); // 调用 Metadata API 拉取 CompletableFuture future = metadataApiService.retrieveAsync( task.getOrgConfigId(), task.getPackageXml(), task.getApiVersion() ); // 等待拉取完成 RetrieveResult result = future.get(); // 更新进度 updateProgress(jobId, RetrieveStatus.PROCESSING.getCode(), 80, "正在下载 Zip 文件"); // 保存 Zip 文件 saveZipFile(jobExecutionId, result.getZipFile()); // 更新状态为成功 updateJobStatus(jobExecutionId, RetrieveStatus.SUCCESS); // 更新进度 updateProgress(jobId, RetrieveStatus.SUCCESS.getCode(), 100, "拉取成功"); // 清除进度 progressMap.remove(jobId); } catch (Exception e) { log.error("拉取元数据失败: {}", jobId, e); // 更新状态为失败 updateJobStatus(jobExecutionId, RetrieveStatus.FAILED); // 更新进度 updateProgress(jobId, RetrieveStatus.FAILED.getCode(), 0, "拉取失败: " + e.getMessage()); // 清除进度 progressMap.remove(jobId); } } /** * 更新作业状态 * * @param jobExecutionId 作业执行ID * @param status 状态 */ private void updateJobStatus(Long jobExecutionId, RetrieveStatus status) { DataiMetaJobExecution jobExecution = new DataiMetaJobExecution(); jobExecution.setId(jobExecutionId); jobExecution.setStatus(status.getCode()); jobExecutionMapper.updateById(jobExecution); } /** * 更新进度 * * @param jobId 作业ID * @param status 状态 * @param progress 进度 * @param message 消息 */ private void updateProgress(String jobId, String status, int progress, String message) { RetrieveProgress retrieveProgress = new RetrieveProgress(); retrieveProgress.setStatus(status); retrieveProgress.setProgress(progress); retrieveProgress.setMessage(message); progressMap.put(jobId, retrieveProgress); } /** * 保存 Zip 文件 * * @param jobExecutionId 作业执行ID * @param zipFile Zip 文件字节数组 */ private void saveZipFile(Long jobExecutionId, byte[] zipFile) { // 实现略 } /** * 拉取进度 */ private static class RetrieveProgress { private String status; private int progress; private String message; public String getStatus() { return status; } public void setStatus(String status) { this.status = status; } public int getProgress() { return progress; } public void setProgress(int progress) { this.progress = progress; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } } } ``` #### 2.4 MetadataRetrieveController 控制器 ```java package com.datai.metadata.controller; import com.baomidou.mybatisplus.core.metadata.IPage; import com.datai.common.core.controller.BaseController; import com.datai.common.core.domain.AjaxResult; import com.datai.metadata.dto.*; import com.datai.metadata.service.IMetadataRetrieveService; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * 元数据拉取控制器 */ @Slf4j @Api(tags = "元数据拉取管理") @RestController @RequestMapping("/metadata/retrieve") public class MetadataRetrieveController extends BaseController { @Autowired private IMetadataRetrieveService metadataRetrieveService; /** * 触发拉取 * * @param request 拉取触发请求 * @return 拉取触发响应 */ @ApiOperation("触发拉取") @PostMapping("/trigger") public AjaxResult triggerRetrieve(@Valid @RequestBody RetrieveTriggerRequest request) { RetrieveTriggerResponse response = metadataRetrieveService.triggerRetrieve(request); return AjaxResult.success(response); } /** * 查询拉取进度 * * @param jobId 作业ID * @return 拉取进度响应 */ @ApiOperation("查询拉取进度") @GetMapping("/progress/{jobId}") public AjaxResult getRetrieveProgress(@PathVariable String jobId) { RetrieveProgressResponse response = metadataRetrieveService.getRetrieveProgress(jobId); return AjaxResult.success(response); } /** * 取消拉取 * * @param jobId 作业ID * @return 操作结果 */ @ApiOperation("取消拉取") @DeleteMapping("/{jobId}") public AjaxResult cancelRetrieve(@PathVariable String jobId) { metadataRetrieveService.cancelRetrieve(jobId); return AjaxResult.success(); } /** * 查询拉取历史 * * @param pageNum 页码 * @param pageSize 页大小 * @param taskId 任务ID * @param orgConfigId 组织配置ID * @param status 状态 * @return 拉取历史响应 */ @ApiOperation("查询拉取历史") @GetMapping("/history") public AjaxResult getRetrieveHistory( @RequestParam(defaultValue = "1") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(required = false) Long taskId, @RequestParam(required = false) Long orgConfigId, @RequestParam(required = false) String status) { IPage response = metadataRetrieveService.getRetrieveHistory( pageNum, pageSize, taskId, orgConfigId, status); return AjaxResult.success(response); } } ``` ### 3. 单元测试 必须为以下类编写单元测试: - MetadataRetrieveServiceImpl - MetadataRetrieveController ### 4. 集成测试 必须编写以下集成测试: - 手动触发拉取测试 - 异步拉取执行测试 - 状态监控测试 - 拉取历史记录测试 - 拉取进度查询测试 - 拉取取消测试 ### 5. API 文档 为以下接口编写 API 文档: - POST /metadata/retrieve/trigger - 触发拉取 - GET /metadata/retrieve/progress/{jobId} - 查询拉取进度 - DELETE /metadata/retrieve/{jobId} - 取消拉取 - GET /metadata/retrieve/history - 查询拉取历史 ## 约束 ### 技术栈限制 - **后端**: Spring Boot 3, MyBatis Plus, MySQL - **前端**: Vue 3, Element Plus - **Salesforce API**: Metadata API, Partner API - **异步执行**: 必须使用 Spring 的 @Async 注解和线程池 - **状态轮询**: 必须使用定时任务实现状态轮询 ### 架构约束 - **模块约束**: 必须在 datai-salesforce-metadata 模块下实现 - **认证约束**: 必须使用 SessionManager 进行会话管理和自动重新登录 - **API 约束**: 必须遵循 Salesforce Metadata API 调用规范 ### 性能要求 - **异步执行**: 异步执行不影响系统响应 - **状态轮询**: 状态轮询频率合理(如每 5 秒轮询一次) - **历史记录查询**: 历史记录查询性能良好,支持分页和条件查询 ### 安全性要求 - **敏感信息**: 不记录敏感信息(如密码、令牌) - **错误信息**: 不暴露系统内部信息 ### 兼容性要求 - **API 版本**: 支持 API 版本 58.0、57.0、56.0 - **Java 版本**: 支持 Java 17 ## Rule Set "请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。" **具体规则**: - 必须使用 Canvas 中定义的类名和方法名 - 必须遵循 Canvas 中定义的调用关系 - 必须使用 SessionManager 进行会话管理和自动重新登录 - 必须使用现有的认证模块进行 OAuth 认证 - 必须使用现有的集成核心功能进行 API 调用 - 必须遵循现有的异常处理机制 - 必须遵循现有的日志记录规范 ## 验收标准 ### 功能完整性 - 手动触发拉取功能正常工作:能够成功手动触发拉取,支持选择任务ID和组织配置,触发成功返回 Job ID - 异步拉取执行正常工作:异步拉取执行正常工作,使用线程池管理异步任务,支持并发拉取,拉取任务不阻塞系统响应 - 状态监控正常工作:状态监控正常工作,使用状态机管理拉取状态,支持状态查询,状态更新及时 - 拉取历史记录正常工作:拉取历史记录成功,支持分页查询,支持条件查询,历史记录完整 - 拉取进度查询正常工作:拉取进度查询成功,支持实时进度查询,进度信息准确,支持进度百分比显示 - 拉取取消功能正常工作:拉取取消功能正常工作,支持取消正在进行的拉取任务,取消后资源正确释放,取消状态更新及时 ### 代码正确性 - 代码符合项目编码规范,有清晰的注释 - 代码结构清晰,易于扩展和维护 - 代码易于单元测试和集成测试 ### 性能指标 - 拉取操作不影响系统响应,系统响应时间在可接受范围内 - 状态轮询频率合理,不会导致 API 限流 - 历史记录查询性能良好,支持分页和条件查询 ## 风险 ### 输出质量风险 - **代码质量**: AI 生成的代码可能存在 bug 或性能问题 - **缓解措施**: 编写详细的单元测试和集成测试,进行代码审查 ### 技术实现风险 - **异步执行**: 异步执行机制复杂可能导致状态管理困难 - **缓解措施**: 使用状态机管理拉取状态,使用 ConcurrentHashMap 存储异步任务状态 - **状态轮询**: 状态轮询频率不当可能导致 API 限流 - **缓解措施**: 设置合理的轮询频率(如每 5 秒轮询一次),使用超时机制防止无限轮询 - **拉取取消**: 拉取取消功能复杂可能导致资源泄漏 - **缓解措施**: 使用 Future.cancel() 取消异步任务,使用状态机管理取消状态 - **历史记录**: 拉取历史记录过多可能影响查询性能 - **缓解措施**: 使用分页查询,定期清理历史记录 - **进度查询**: 进度查询不准确可能导致用户体验差 - **缓解措施**: 使用轮询机制获取进度,使用缓存提高查询性能 ### 时间成本风险 - **开发周期**: 开发复杂度高,可能延长开发周期 - **缓解措施**: 分阶段实施,先实现核心功能,再实现辅助功能 ## 使用记录 | 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 | |------|---------|---------|---------|------|----------| | 2026-01-18 | 元数据拉取核心功能实现 | REQ-010-6, ADR-0015 | MetadataRetrieveController, MetadataRetrieveService, RetrieveStatus | 代码结构清晰,符合项目规范 | 无 |