# Prompt - 元数据部署核心功能实现 ## 输入引用 引用相关的 docs 文档链接: - [REQ-010-8.md](../requirements/REQ-010-8.md) - 元数据部署核心功能需求文档 - [0017-metadata-deploy-core.md](../decisions/adr/0017-metadata-deploy-core.md) - 元数据部署核心功能架构决策 - [REQ-010-6.md](../requirements/REQ-010-6.md) - 元数据拉取核心功能需求文档 - [REQ-010-7.md](../requirements/REQ-010-7.md) - 文件存储和解压处理需求文档 - [004-元数据部署网页资料链接地址](../reference-code/metadata/004-元数据部署网页资料链接地址) - 官方文档和开源项目参考 - [metadata-module.md](../reference-code/com/docs/metadata-module.md) - Salesforce Metadata API 模块说明(唯一真源) ## Context Maps 强制列出本次 Prompt 依赖的 Canvas 文件: - [Authentication.canvas](../../Authentication.canvas) - 项目架构视觉化展示 - **相关节点**: [集成核心](node_integration_core) - 提供与Salesforce的各种连接方式 - **相关节点**: [SessionManager](node_session_manager_detail) - 会话管理,提供登录服务 ## 目标 实现元数据部署核心功能,包括: 1. 实现手动触发部署功能,支持选择任务ID、组织配置和上传Zip文件 2. 实现异步部署执行,使用Spring的@Async注解和自定义线程池 3. 实现状态监控,使用状态机管理部署状态(Pending/Processing/Success/Failed/Partial_Success/Cancelled) 4. 实现部署历史记录,支持分页查询和条件查询 5. 实现部署进度查询,支持实时进度查询和进度百分比显示 6. 实现部署取消功能,使用Future.cancel()取消异步任务 7. 实现部署结果解析,提取错误信息和代码覆盖率 ## 输出格式 ### 代码结构 ``` datai-salesforce-metadata/ ├── src/main/java/com/datai/salesforce/metadata/deploy/ │ ├── controller/ │ │ └── MetadataDeployController.java # 元数据部署控制器 │ ├── service/ │ │ ├── IMetadataDeployService.java # 元数据部署服务接口 │ │ └── MetadataDeployServiceImpl.java # 元数据部署服务实现 │ ├── enums/ │ │ └── DeployStatus.java # 部署状态枚举 │ ├── dto/ │ │ ├── DeployTriggerRequest.java # 部署触发请求 │ │ ├── DeployTriggerResponse.java # 部署触发响应 │ │ ├── DeployProgressResponse.java # 部署进度响应 │ │ ├── DeployHistoryResponse.java # 部署历史响应 │ │ └── DeployResult.java # 部署结果 │ ├── config/ │ │ └── AsyncConfig.java # 异步配置 │ └── exception/ │ └── DeployException.java # 部署异常 ├── src/main/resources/ │ ├── application.yml # 应用配置 │ └── logback-spring.xml # 日志配置 └── src/test/java/com/datai/salesforce/metadata/deploy/ ├── service/ │ └── MetadataDeployServiceTest.java # 部署服务测试 └── controller/ └── MetadataDeployControllerTest.java # 部署控制器测试 ``` ### 代码示例 #### 1. DeployStatus.java ```java package com.datai.salesforce.metadata.deploy.enums; import lombok.Getter; @Getter public enum DeployStatus { PENDING(0, "待处理"), PROCESSING(1, "处理中"), SUCCESS(2, "成功"), FAILED(3, "失败"), PARTIAL_SUCCESS(4, "部分成功"), CANCELLED(5, "已取消"); private final Integer code; private final String desc; DeployStatus(Integer code, String desc) { this.code = code; this.desc = desc; } public static DeployStatus fromCode(Integer code) { for (DeployStatus status : values()) { if (status.getCode().equals(code)) { return status; } } throw new IllegalArgumentException("Invalid status code: " + code); } } ``` #### 2. DeployTriggerRequest.java ```java package com.datai.salesforce.metadata.deploy.dto; import lombok.Data; @Data public class DeployTriggerRequest { private Long taskId; private Long orgConfigId; private String zipFilePath; private String createdBy; } ``` #### 3. DeployTriggerResponse.java ```java package com.datai.salesforce.metadata.deploy.dto; import lombok.Data; @Data public class DeployTriggerResponse { private String jobId; private Integer status; private String message; } ``` #### 4. DeployProgressResponse.java ```java package com.datai.salesforce.metadata.deploy.dto; import lombok.Data; @Data public class DeployProgressResponse { private String jobId; private Integer status; private Integer progress; private Integer processedComponents; private Integer totalComponents; private String message; } ``` #### 5. DeployHistoryResponse.java ```java package com.datai.salesforce.metadata.deploy.dto; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Data public class DeployHistoryResponse { private Long id; private Long taskId; private Long orgConfigId; private Integer status; private LocalDateTime startTime; private LocalDateTime endTime; private String createdBy; private List results; } ``` #### 6. DeployResult.java ```java package com.datai.salesforce.metadata.deploy.dto; import lombok.Data; import java.util.List; @Data public class DeployResult { private String componentType; private String fileName; private Boolean success; private String errorMessage; private Integer lineNumber; private Integer columnNumber; } ``` #### 7. IMetadataDeployService.java ```java package com.datai.salesforce.metadata.deploy.service; import com.datai.salesforce.metadata.deploy.dto.*; import java.util.List; import java.util.concurrent.Future; public interface IMetadataDeployService { DeployTriggerResponse triggerDeploy(DeployTriggerRequest request); Future deployAsync(Long jobExecutionId, Long taskId, Long orgConfigId, String zipFilePath); DeployProgressResponse getProgress(String jobId); List getHistory(Long taskId, Long orgConfigId, Integer pageNum, Integer pageSize); boolean cancelDeploy(String jobId); void parseDeployResult(Long jobExecutionId, String deployResult); } ``` #### 8. MetadataDeployServiceImpl.java ```java package com.datai.salesforce.metadata.deploy.service; import com.datai.salesforce.metadata.deploy.dto.*; import com.datai.salesforce.metadata.deploy.enums.DeployStatus; import com.datai.salesforce.metadata.entity.DataiMetaJobExecution; import com.datai.salesforce.metadata.entity.DataiMetaTask; import com.datai.salesforce.metadata.mapper.DataiMetaJobExecutionMapper; import com.datai.salesforce.metadata.mapper.DataiMetaTaskMapper; import com.datai.salesforce.metadata.service.IMetadataApiService; import lombok.extern.slf4j.Slf4j; 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.io.File; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Future; @Slf4j @Service public class MetadataDeployServiceImpl implements IMetadataDeployService { @Autowired private IMetadataApiService metadataApiService; @Autowired private DataiMetaJobExecutionMapper jobExecutionMapper; @Autowired private DataiMetaTaskMapper taskMapper; private final Map> deployFutureMap = new ConcurrentHashMap<>(); private final Map progressMap = new ConcurrentHashMap<>(); @Override @Transactional public DeployTriggerResponse triggerDeploy(DeployTriggerRequest request) { DataiMetaTask task = taskMapper.selectById(request.getTaskId()); if (task == null) { throw new RuntimeException("Task not found"); } DataiMetaJobExecution jobExecution = new DataiMetaJobExecution(); jobExecution.setTaskId(request.getTaskId()); jobExecution.setOrgConfigId(request.getOrgConfigId()); jobExecution.setJobType(task.getTaskType()); jobExecution.setStatus(DeployStatus.PENDING.getCode()); jobExecution.setCreatedBy(request.getCreatedBy()); jobExecutionMapper.insert(jobExecution); deployAsync(jobExecution.getId(), request.getTaskId(), request.getOrgConfigId(), request.getZipFilePath()); DeployTriggerResponse response = new DeployTriggerResponse(); response.setJobId(jobExecution.getId().toString()); response.setStatus(DeployStatus.PENDING.getCode()); response.setMessage("Deploy task created"); return response; } @Override @Async("deployExecutor") public Future deployAsync(Long jobExecutionId, Long taskId, Long orgConfigId, String zipFilePath) { String jobId = jobExecutionId.toString(); try { updateJobStatus(jobExecutionId, DeployStatus.PROCESSING); File zipFile = new File(zipFilePath); String deployResult = metadataApiService.deploy(orgConfigId, zipFile); updateJobStatus(jobExecutionId, DeployStatus.SUCCESS); parseDeployResult(jobExecutionId, deployResult); log.info("Deploy completed successfully: {}", jobId); } catch (Exception e) { log.error("Deploy failed: {}", jobId, e); updateJobStatus(jobExecutionId, DeployStatus.FAILED); throw new RuntimeException("Deploy failed", e); } finally { deployFutureMap.remove(jobId); } return null; } @Override public DeployProgressResponse getProgress(String jobId) { DeployProgressResponse progress = progressMap.get(jobId); if (progress == null) { DataiMetaJobExecution jobExecution = jobExecutionMapper.selectById(Long.parseLong(jobId)); progress = new DeployProgressResponse(); progress.setJobId(jobId); progress.setStatus(jobExecution.getStatus()); progress.setProgress(0); progress.setMessage("Deploy task not found in progress map"); } return progress; } @Override public List getHistory(Long taskId, Long orgConfigId, Integer pageNum, Integer pageSize) { return jobExecutionMapper.selectDeployHistory(taskId, orgConfigId, pageNum, pageSize); } @Override public boolean cancelDeploy(String jobId) { Future future = deployFutureMap.get(jobId); if (future != null) { boolean cancelled = future.cancel(true); if (cancelled) { updateJobStatus(Long.parseLong(jobId), DeployStatus.CANCELLED); progressMap.remove(jobId); log.info("Deploy cancelled: {}", jobId); } return cancelled; } return false; } @Override @Transactional public void parseDeployResult(Long jobExecutionId, String deployResult) { log.info("Parsing deploy result for job: {}", jobExecutionId); DataiMetaJobExecution jobExecution = jobExecutionMapper.selectById(jobExecutionId); jobExecution.setDeployResult(deployResult); jobExecutionMapper.updateById(jobExecution); } private void updateJobStatus(Long jobExecutionId, DeployStatus status) { DataiMetaJobExecution jobExecution = new DataiMetaJobExecution(); jobExecution.setId(jobExecutionId); jobExecution.setStatus(status.getCode()); jobExecutionMapper.updateById(jobExecution); } } ``` #### 9. MetadataDeployController.java ```java package com.datai.salesforce.metadata.deploy.controller; import com.datai.salesforce.metadata.deploy.dto.*; import com.datai.salesforce.metadata.deploy.service.IMetadataDeployService; import com.datai.common.core.domain.R; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @Slf4j @RestController @RequestMapping("/metadata/deploy") public class MetadataDeployController { @Autowired private IMetadataDeployService metadataDeployService; @PostMapping("/trigger") public R triggerDeploy(@RequestBody DeployTriggerRequest request) { log.info("Trigger deploy: taskId={}, orgConfigId={}", request.getTaskId(), request.getOrgConfigId()); DeployTriggerResponse response = metadataDeployService.triggerDeploy(request); return R.ok(response); } @GetMapping("/progress/{jobId}") public R getProgress(@PathVariable String jobId) { log.info("Get deploy progress: jobId={}", jobId); DeployProgressResponse response = metadataDeployService.getProgress(jobId); return R.ok(response); } @GetMapping("/history") public R> getHistory( @RequestParam(required = false) Long taskId, @RequestParam(required = false) Long orgConfigId, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { log.info("Get deploy history: taskId={}, orgConfigId={}, pageNum={}, pageSize={}", taskId, orgConfigId, pageNum, pageSize); List history = metadataDeployService.getHistory(taskId, orgConfigId, pageNum, pageSize); return R.ok(history); } @PostMapping("/cancel/{jobId}") public R cancelDeploy(@PathVariable String jobId) { log.info("Cancel deploy: jobId={}", jobId); boolean cancelled = metadataDeployService.cancelDeploy(jobId); return R.ok(cancelled); } } ``` #### 10. AsyncConfig.java ```java package com.datai.salesforce.metadata.deploy.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 { @Bean("deployExecutor") public Executor deployExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setCorePoolSize(5); executor.setMaxPoolSize(10); executor.setQueueCapacity(100); executor.setThreadNamePrefix("deploy-async-"); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); executor.initialize(); return executor; } } ``` #### 11. application.yml ```yaml spring: task: execution: pool: core-size: 5 max-size: 10 queue-capacity: 100 thread-name-prefix: async- keep-alive: 60s metadata: deploy: poll-interval: 5000 # 状态轮询间隔(毫秒) max-retry: 3 # 最大重试次数 ``` ## 约束 ### 技术栈约束 - 必须基于 Spring Boot 3 - 必须使用 Java 17+ - 必须使用 MyBatis Plus - 必须使用 MySQL ### 性能约束 - 部署触发响应时间 < 1s - 状态轮询任务执行时间 < 1s - 进度查询响应时间 < 500ms - 历史记录查询响应时间 < 1s - 并发触发 10 个部署任务无性能下降 ### 安全性约束 - 部署操作需要认证 - 必须使用 SessionManager 进行会话管理 - 自动重新登录功能正常 - 部署数据传输加密 - 敏感信息不记录到日志 - 部署历史记录权限控制 ### 兼容性约束 - 必须使用现有的集成核心功能进行 Salesforce API 调用 - 必须使用现有的认证模块进行 OAuth 认证 - 必须遵循现有的异常处理机制 - 必须遵循现有的日志记录规范 ## Rule Set "请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。" **具体规则**: - 必须使用 Canvas 中定义的类名和方法名 - 必须遵循 Canvas 中定义的调用关系 - 必须参考 Canvas 中的流程图逻辑 - 必须使用 SessionManager 进行会话管理和自动重新登录 - 必须使用现有的认证模块进行 OAuth 认证 - 必须使用现有的集成核心功能进行 API 调用 - 必须遵循现有的异常处理机制 - 必须遵循现有的日志记录规范 - 必须使用状态机模式管理部署状态 - 必须使用 Spring 的 @Async 注解实现异步执行 - 必须使用自定义线程池管理异步任务 - 必须使用轮询机制检查部署状态 ## 验收标准 ### 功能验收标准 - [ ] 能够成功手动触发部署 - [ ] 支持选择任务ID - [ ] 支持选择组织配置 - [ ] 支持上传Zip文件 - [ ] 触发成功返回Job ID - [ ] API接口符合RESTful规范 - [ ] 异步部署执行正常工作 - [ ] 使用线程池管理异步任务 - [ ] 支持并发部署 - [ ] 部署任务不阻塞系统响应 - [ ] 状态监控正常工作 - [ ] 使用状态机管理部署状态 - [ ] 支持状态查询 - [ ] 状态更新及时 - [ ] 部署历史记录成功 - [ ] 支持分页查询 - [ ] 支持条件查询 - [ ] 历史记录完整 - [ ] 部署进度查询成功 - [ ] 支持实时进度查询 - [ ] 进度信息准确 - [ ] 支持进度百分比显示 - [ ] 部署取消功能正常工作 - [ ] 支持取消正在进行的部署任务 - [ ] 取消后资源正确释放 - [ ] 取消状态更新及时 - [ ] 部署结果解析成功 - [ ] 错误信息提取正确 - [ ] 代码覆盖率提取正确 - [ ] 支持多种部署状态 - [ ] 错误信息详细 ### 性能验收标准 - [ ] 部署触发响应时间 < 1s - [ ] 并发触发 10 个部署任务无性能下降 - [ ] 状态轮询任务执行时间 < 1s - [ ] 不影响系统正常运行 - [ ] 进度查询响应时间 < 500ms - [ ] 并发查询 100 次无性能下降 - [ ] 历史记录查询响应时间 < 1s - [ ] 分页查询 100 条记录无性能下降 ### 安全性验收标准 - [ ] 部署操作需要认证 - [ ] 使用 SessionManager 进行会话管理 - [ ] 自动重新登录功能正常 - [ ] 部署数据传输加密 - [ ] 敏感信息不记录到日志 - [ ] 部署历史记录权限控制 ### 代码质量验收标准 - [ ] 代码符合项目编码规范 - [ ] 有清晰的注释 - [ ] 单元测试覆盖率 > 80% - [ ] 集成测试覆盖主要场景 - [ ] 通过代码审查 ## 风险 ### 异步执行风险 - **风险描述**: 异步执行机制复杂可能导致状态管理困难 - **缓解措施**: 使用状态机模式管理状态,使用 Future 对象跟踪异步任务 - **监控指标**: 监控异步任务执行时间和状态转换 ### 状态轮询风险 - **风险描述**: 状态轮询频率不当可能导致 API 限流 - **缓解措施**: 设置合理的轮询间隔(如 5 秒),使用指数退避策略 - **监控指标**: 监控 API 调用次数和响应时间 ### 部署取消风险 - **风险描述**: 部署取消功能复杂可能导致资源泄漏 - **缓解措施**: 使用 Future.cancel() 安全取消,确保资源正确释放 - **监控指标**: 监控取消操作的成功率和资源释放情况 ### 历史记录风险 - **风险描述**: 部署历史记录过多可能影响查询性能 - **缓解措施**: 使用分页查询,定期清理过期历史记录 - **监控指标**: 监控历史记录查询性能 ### 进度查询风险 - **风险描述**: 进度查询不准确可能导致用户体验差 - **缓解措施**: 使用缓存提高查询性能,优化轮询策略 - **监控指标**: 监控进度查询的准确性和响应时间 ### 结果解析风险 - **风险描述**: 部署结果解析不完善可能导致错误信息不准确 - **缓解措施**: 使用 JSON 解析库和正则表达式双重验证,充分测试 - **监控指标**: 监控结果解析的成功率和准确性 ## 使用记录 | 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 | |------|---------|---------|---------|------|----------| | 2026-01-19 | 实现元数据部署核心功能 | REQ-010-8 需求文档、ADR-0017 决策文档 | 完整的代码实现和测试用例 | 待使用 | 待改进 |