# Prompt - Destructive Changes功能实现 ## 输入引用 引用相关的 docs 文档链接: - [REQ-010-10.md](../requirements/REQ-010-10.md) - Destructive Changes功能实现需求文档 - [0019-destructive-changes.md](../decisions/adr/0019-destructive-changes.md) - Destructive Changes功能实现架构决策 - [REQ-010-8.md](../requirements/REQ-010-8.md) - 元数据部署核心功能需求文档 - [0017-metadata-deploy-core.md](../decisions/adr/0017-metadata-deploy-core.md) - 元数据部署核心功能架构决策 - [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的各种连接方式 - **相关节点**: [IMetadataApiService](node_metadata_api_service) - 元数据API服务接口 - **相关节点**: [MetadataApiClient](node_metadata_api_client) - Metadata API客户端 ## 目标 实现 Destructive Changes 功能,包括 destructiveChanges.xml 配置、Destructive Changes 部署、Destructive Changes 验证、Destructive Changes 历史记录、Destructive Changes 回滚。支持部署时删除指定的元数据组件,使用 destructiveChanges.xml 配置删除规则。 ## 输出格式 ### 代码结构 ``` datai-salesforce-metadata/ ├── destructive-changes/ │ ├── controller/ │ │ └── DestructiveChangesController.java │ ├── service/ │ │ ├── IDestructiveChangesService.java │ │ ├── DestructiveChangesServiceImpl.java │ │ ├── IDestructiveChangesConfigService.java │ │ └── DestructiveChangesConfigServiceImpl.java │ ├── dto/ │ │ ├── DestructiveChangesConfigRequest.java │ │ ├── DestructiveChangesConfigResponse.java │ │ ├── DestructiveChangesDeployRequest.java │ │ ├── DestructiveChangesDeployResponse.java │ │ ├── DestructiveChangesValidateRequest.java │ │ ├── DestructiveChangesValidateResponse.java │ │ ├── DestructiveChangesHistoryResponse.java │ │ ├── DestructiveChangesRollbackRequest.java │ │ └── DestructiveChangesRollbackResponse.java │ ├── entity/ │ │ └── DestructiveChangeRule.java │ ├── mapper/ │ │ └── DestructiveChangeRuleMapper.java │ ├── enums/ │ │ └── MetadataType.java │ ├── util/ │ │ ├── DestructiveChangesXmlParser.java │ │ └── DestructiveChangesXmlBuilder.java │ └── exception/ │ └── DestructiveChangesException.java ``` ### 代码示例(语言:Java) #### 1. DestructiveChangesController 控制器 ```java package com.datai.salesforce.metadata.destructive.controller; import com.datai.salesforce.metadata.destructive.dto.*; import com.datai.salesforce.metadata.destructive.service.IDestructiveChangesService; import com.datai.salesforce.metadata.destructive.service.IDestructiveChangesConfigService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/metadata/destructive-changes") public class DestructiveChangesController { @Autowired private IDestructiveChangesService destructiveChangesService; @Autowired private IDestructiveChangesConfigService destructiveChangesConfigService; @PostMapping("/config") public DestructiveChangesConfigResponse saveConfig(@RequestBody DestructiveChangesConfigRequest request) { return destructiveChangesConfigService.saveConfig(request); } @GetMapping("/config/{taskId}") public DestructiveChangesConfigResponse getConfig(@PathVariable Long taskId) { return destructiveChangesConfigService.getConfig(taskId); } @PostMapping("/validate") public DestructiveChangesValidateResponse validateConfig(@RequestBody DestructiveChangesValidateRequest request) { return destructiveChangesConfigService.validateConfig(request); } @PostMapping("/deploy") public DestructiveChangesDeployResponse deploy(@RequestBody DestructiveChangesDeployRequest request) { return destructiveChangesService.deploy(request); } @GetMapping("/history") public DestructiveChangesHistoryResponse getHistory( @RequestParam(required = false) Long orgConfigId, @RequestParam(required = false) String status, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return destructiveChangesService.getHistory(orgConfigId, status, page, size); } @PostMapping("/rollback/{jobId}") public DestructiveChangesRollbackResponse rollback(@PathVariable Long jobId) { return destructiveChangesService.rollback(jobId); } } ``` #### 2. IDestructiveChangesService 服务接口 ```java package com.datai.salesforce.metadata.destructive.service; import com.datai.salesforce.metadata.destructive.dto.*; public interface IDestructiveChangesService { DestructiveChangesDeployResponse deploy(DestructiveChangesDeployRequest request); DestructiveChangesHistoryResponse getHistory(Long orgConfigId, String status, int page, int size); DestructiveChangesRollbackResponse rollback(Long jobId); } ``` #### 3. DestructiveChangesServiceImpl 服务实现 ```java package com.datai.salesforce.metadata.destructive.service; import com.datai.salesforce.metadata.destructive.dto.*; import com.datai.salesforce.metadata.destructive.entity.DestructiveChangeRule; import com.datai.salesforce.metadata.destructive.mapper.DestructiveChangeRuleMapper; import com.datai.salesforce.metadata.destructive.util.DestructiveChangesXmlBuilder; import com.datai.salesforce.metadata.entity.DataiMetaJobExecution; import com.datai.salesforce.metadata.mapper.DataiMetaJobExecutionMapper; import com.datai.salesforce.metadata.service.IMetadataApiService; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; 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.Date; import java.util.List; import java.util.concurrent.Future; @Service public class DestructiveChangesServiceImpl implements IDestructiveChangesService { @Autowired private IMetadataApiService metadataApiService; @Autowired private DataiMetaJobExecutionMapper jobExecutionMapper; @Autowired private DestructiveChangeRuleMapper destructiveChangeRuleMapper; @Autowired private DestructiveChangesXmlBuilder xmlBuilder; @Override @Transactional public DestructiveChangesDeployResponse deploy(DestructiveChangesDeployRequest request) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("task_id", request.getTaskId()); List rules = destructiveChangeRuleMapper.selectList(queryWrapper); if (rules.isEmpty()) { throw new RuntimeException("No destructive change rules found"); } String destructiveChangesXml = xmlBuilder.buildXml(rules); File destructiveChangesFile = xmlBuilder.saveXmlToFile(request.getTaskId(), destructiveChangesXml); DataiMetaJobExecution jobExecution = new DataiMetaJobExecution(); jobExecution.setTaskId(request.getTaskId()); jobExecution.setOrgConfigId(request.getOrgConfigId()); jobExecution.setJobType("DESTRUCTIVE_CHANGES"); jobExecution.setStatus("PENDING"); jobExecution.setCreatedBy(request.getCreatedBy()); jobExecutionMapper.insert(jobExecution); deployAsync(jobExecution.getId(), request.getTaskId(), request.getOrgConfigId(), destructiveChangesFile); DestructiveChangesDeployResponse response = new DestructiveChangesDeployResponse(); response.setJobId(jobExecution.getId().toString()); response.setStatus("PENDING"); response.setMessage("Destructive Changes deploy task created"); return response; } @Override @Async("deployExecutor") public Future deployAsync(Long jobExecutionId, Long taskId, Long orgConfigId, File destructiveChangesFile) { String jobId = jobExecutionId.toString(); try { updateJobStatus(jobExecutionId, "PROCESSING"); String deployResult = metadataApiService.deploy(orgConfigId, null, destructiveChangesFile); updateJobStatus(jobExecutionId, "SUCCESS"); parseDeployResult(jobExecutionId, deployResult); } catch (Exception e) { updateJobStatus(jobExecutionId, "FAILED"); throw new RuntimeException("Destructive Changes deploy failed", e); } return null; } @Override public DestructiveChangesHistoryResponse getHistory(Long orgConfigId, String status, int page, int size) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("job_type", "DESTRUCTIVE_CHANGES"); if (orgConfigId != null) { queryWrapper.eq("org_config_id", orgConfigId); } if (status != null && !status.isEmpty()) { queryWrapper.eq("status", status); } queryWrapper.orderByDesc("create_time"); Page pageResult = jobExecutionMapper.selectPage( new Page<>(page, size), queryWrapper); DestructiveChangesHistoryResponse response = new DestructiveChangesHistoryResponse(); response.setTotal(pageResult.getTotal()); response.setPage(page); response.setSize(size); response.setRecords(pageResult.getRecords()); return response; } @Override @Transactional public DestructiveChangesRollbackResponse rollback(Long jobId) { DataiMetaJobExecution jobExecution = jobExecutionMapper.selectById(jobId); if (jobExecution == null) { throw new RuntimeException("Job not found"); } if (!"SUCCESS".equals(jobExecution.getStatus())) { throw new RuntimeException("Only successful deployments can be rolled back"); } String backupData = jobExecution.getBackupData(); if (backupData == null || backupData.isEmpty()) { throw new RuntimeException("No backup data found"); } rollbackAsync(jobId, jobExecution.getOrgConfigId(), backupData); DestructiveChangesRollbackResponse response = new DestructiveChangesRollbackResponse(); response.setJobId(jobId.toString()); response.setStatus("ROLLBACK_IN_PROGRESS"); response.setMessage("Rollback task created"); return response; } @Async("deployExecutor") public Future rollbackAsync(Long jobId, Long orgConfigId, String backupData) { try { updateJobStatus(jobId, "ROLLBACK_PROCESSING"); String rollbackResult = metadataApiService.rollback(orgConfigId, backupData); updateJobStatus(jobId, "ROLLBACK_SUCCESS"); parseRollbackResult(jobId, rollbackResult); } catch (Exception e) { updateJobStatus(jobId, "ROLLBACK_FAILED"); throw new RuntimeException("Rollback failed", e); } return null; } private void updateJobStatus(Long jobExecutionId, String status) { DataiMetaJobExecution jobExecution = new DataiMetaJobExecution(); jobExecution.setId(jobExecutionId); jobExecution.setStatus(status); jobExecution.setUpdateTime(new Date()); jobExecutionMapper.updateById(jobExecution); } private void parseDeployResult(Long jobExecutionId, String deployResult) { DataiMetaJobExecution jobExecution = jobExecutionMapper.selectById(jobExecutionId); if (jobExecution != null) { jobExecution.setRemark(deployResult); jobExecutionMapper.updateById(jobExecution); } } private void parseRollbackResult(Long jobId, String rollbackResult) { DataiMetaJobExecution jobExecution = jobExecutionMapper.selectById(jobId); if (jobExecution != null) { jobExecution.setRemark(rollbackResult); jobExecutionMapper.updateById(jobExecution); } } } ``` #### 4. IDestructiveChangesConfigService 服务接口 ```java package com.datai.salesforce.metadata.destructive.service; import com.datai.salesforce.metadata.destructive.dto.*; public interface IDestructiveChangesConfigService { DestructiveChangesConfigResponse saveConfig(DestructiveChangesConfigRequest request); DestructiveChangesConfigResponse getConfig(Long taskId); DestructiveChangesValidateResponse validateConfig(DestructiveChangesValidateRequest request); } ``` #### 5. DestructiveChangesConfigServiceImpl 服务实现 ```java package com.datai.salesforce.metadata.destructive.service; import com.datai.salesforce.metadata.destructive.dto.*; import com.datai.salesforce.metadata.destructive.entity.DestructiveChangeRule; import com.datai.salesforce.metadata.destructive.mapper.DestructiveChangeRuleMapper; import com.datai.salesforce.metadata.destructive.util.DestructiveChangesXmlParser; import com.datai.salesforce.metadata.destructive.util.DestructiveChangesXmlBuilder; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service public class DestructiveChangesConfigServiceImpl implements IDestructiveChangesConfigService { @Autowired private DestructiveChangeRuleMapper destructiveChangeRuleMapper; @Autowired private DestructiveChangesXmlParser xmlParser; @Autowired private DestructiveChangesXmlBuilder xmlBuilder; @Override @Transactional public DestructiveChangesConfigResponse saveConfig(DestructiveChangesConfigRequest request) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("task_id", request.getTaskId()); destructiveChangeRuleMapper.delete(queryWrapper); for (DestructiveChangesConfigRequest.Rule rule : request.getRules()) { DestructiveChangeRule entity = new DestructiveChangeRule(); entity.setTaskId(request.getTaskId()); entity.setMetadataType(rule.getMetadataType()); entity.setMemberName(rule.getMemberName()); destructiveChangeRuleMapper.insert(entity); } String xml = xmlBuilder.buildXml(request.getTaskId()); DestructiveChangesConfigResponse response = new DestructiveChangesConfigResponse(); response.setTaskId(request.getTaskId()); response.setXml(xml); response.setMessage("Configuration saved successfully"); return response; } @Override public DestructiveChangesConfigResponse getConfig(Long taskId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("task_id", taskId); List rules = destructiveChangeRuleMapper.selectList(queryWrapper); String xml = xmlBuilder.buildXml(taskId); DestructiveChangesConfigResponse response = new DestructiveChangesConfigResponse(); response.setTaskId(taskId); response.setXml(xml); response.setRules(rules); return response; } @Override public DestructiveChangesValidateResponse validateConfig(DestructiveChangesValidateRequest request) { try { xmlParser.parseXml(request.getXml()); DestructiveChangesValidateResponse response = new DestructiveChangesValidateResponse(); response.setValid(true); response.setMessage("Configuration is valid"); return response; } catch (Exception e) { DestructiveChangesValidateResponse response = new DestructiveChangesValidateResponse(); response.setValid(false); response.setMessage(e.getMessage()); return response; } } } ``` #### 6. DestructiveChangesXmlParser 工具类 ```java package com.datai.salesforce.metadata.destructive.util; import org.springframework.stereotype.Component; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import java.io.ByteArrayInputStream; import java.io.IOException; @Component public class DestructiveChangesXmlParser { public void parseXml(String xml) throws Exception { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new ByteArrayInputStream(xml.getBytes())); Element root = document.getDocumentElement(); if (!"Package".equals(root.getNodeName())) { throw new RuntimeException("Root element must be 'Package'"); } NodeList types = root.getElementsByTagName("types"); for (int i = 0; i < types.getLength(); i++) { Element typeElement = (Element) types.item(i); NodeList members = typeElement.getElementsByTagName("members"); NodeList name = typeElement.getElementsByTagName("name"); if (members.getLength() == 0) { throw new RuntimeException("Type element must have at least one 'members' element"); } if (name.getLength() != 1) { throw new RuntimeException("Type element must have exactly one 'name' element"); } } } } ``` #### 7. DestructiveChangesXmlBuilder 工具类 ```java package com.datai.salesforce.metadata.destructive.util; import com.datai.salesforce.metadata.destructive.entity.DestructiveChangeRule; import com.datai.salesforce.metadata.destructive.mapper.DestructiveChangeRuleMapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Component public class DestructiveChangesXmlBuilder { @Autowired private DestructiveChangeRuleMapper destructiveChangeRuleMapper; public String buildXml(Long taskId) { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("task_id", taskId); List rules = destructiveChangeRuleMapper.selectList(queryWrapper); return buildXml(rules); } public String buildXml(List rules) { StringBuilder xml = new StringBuilder(); xml.append("\n"); xml.append("\n"); Map> groupedRules = rules.stream() .collect(Collectors.groupingBy(DestructiveChangeRule::getMetadataType)); for (Map.Entry> entry : groupedRules.entrySet()) { xml.append(" \n"); for (DestructiveChangeRule rule : entry.getValue()) { xml.append(" ").append(rule.getMemberName()).append("\n"); } xml.append(" ").append(entry.getKey()).append("\n"); xml.append(" \n"); } xml.append(" 56.0\n"); xml.append(""); return xml.toString(); } public File saveXmlToFile(Long taskId, String xml) throws IOException { String fileName = "destructiveChanges-" + taskId + ".xml"; File file = new File(System.getProperty("java.io.tmpdir"), fileName); try (FileWriter writer = new FileWriter(file)) { writer.write(xml); } return file; } } ``` #### 8. DestructiveChangeRule 实体类 ```java package com.datai.salesforce.metadata.destructive.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("datai_destructive_change_rule") public class DestructiveChangeRule { @TableId(type = IdType.AUTO) private Long id; private Long taskId; private String metadataType; private String memberName; private LocalDateTime createTime; private LocalDateTime updateTime; } ``` #### 9. DestructiveChangeRuleMapper 接口 ```java package com.datai.salesforce.metadata.destructive.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.datai.salesforce.metadata.destructive.entity.DestructiveChangeRule; import org.apache.ibatis.annotations.Mapper; @Mapper public interface DestructiveChangeRuleMapper extends BaseMapper { } ``` #### 10. DTO 类 ```java package com.datai.salesforce.metadata.destructive.dto; import lombok.Data; import java.util.List; @Data public class DestructiveChangesConfigRequest { private Long taskId; private List rules; @Data public static class Rule { private String metadataType; private String memberName; } } @Data public class DestructiveChangesConfigResponse { private Long taskId; private String xml; private List rules; private String message; } @Data public class DestructiveChangesValidateRequest { private String xml; } @Data public class DestructiveChangesValidateResponse { private boolean valid; private String message; } @Data public class DestructiveChangesDeployRequest { private Long taskId; private Long orgConfigId; private String createdBy; } @Data public class DestructiveChangesDeployResponse { private String jobId; private String status; private String message; } @Data public class DestructiveChangesHistoryResponse { private long total; private int page; private int size; private List records; } @Data public class DestructiveChangesRollbackRequest { private Long jobId; } @Data public class DestructiveChangesRollbackResponse { private String jobId; private String status; private String message; } ``` #### 11. DestructiveChangesException 异常类 ```java package com.datai.salesforce.metadata.destructive.exception; public class DestructiveChangesException extends RuntimeException { public DestructiveChangesException(String message) { super(message); } public DestructiveChangesException(String message, Throwable cause) { super(message, cause); } } ``` ## 约束 ### 技术栈限制 - 必须基于 Spring Boot 3 + MyBatis Plus + MySQL 技术栈 - 必须使用现有的 IMetadataApiService 接口进行 Salesforce API 调用 - 必须使用现有的 DataiMetaJobExecution 实体和 Mapper - 必须使用 Java XML 解析库(如 JAXB 或 DOM4J)处理 destructiveChanges.xml ### 架构约束 - 必须遵循 Authentication.canvas 中定义的架构和调用关系 - 必须在 datai-salesforce-metadata 模块下实现 - 必须使用 RESTful API 设计规范 ### 性能要求 - destructiveChanges.xml 配置响应时间 < 1s - Destructive Changes 部署触发响应时间 < 1s - Destructive Changes 验证响应时间 < 500ms - Destructive Changes 历史记录查询响应时间 < 1s - Destructive Changes 回滚响应时间 < 2s ### 安全性要求 - Destructive Changes 操作需要认证 - 删除操作需要备份,支持回滚 - 部署数据传输加密 - 敏感信息不记录到日志 ## Rule Set "请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。" **具体规则**: - 必须使用 Canvas 中定义的类名和方法名 - 必须遵循 Canvas 中定义的调用关系 - 必须参考 Canvas 中的流程图逻辑 - 必须复用现有的 DeployStatus 枚举和状态管理逻辑 - 必须复用现有的 AsyncConfig 配置类和线程池 ## 验收标准 ### 功能完整性 - destructiveChanges.xml 配置成功,支持可视化编辑 - destructiveChanges.xml 格式符合 Salesforce Metadata API 规范 - 支持多种元数据类型配置 - 支持通配符配置 - destructiveChanges.xml 验证功能正常工作 - Destructive Changes 部署成功,支持删除指定的元数据组件 - 支持批量删除 - 删除操作安全可靠 - API 接口符合 RESTful 规范 - Destructive Changes 验证功能正常工作 - destructiveChanges.xml 格式验证正确 - 删除规则验证正确 - 验证失败返回详细错误信息 - Destructive Changes 历史记录成功 - 支持分页查询 - 支持条件查询 - 历史记录完整 - Destructive Changes 回滚功能正常工作 - 支持恢复被删除的元数据组件 - 回滚操作安全可靠 - 回滚日志记录完整 ### 代码正确性 - 代码符合项目编码规范,有清晰的注释 - 单元测试覆盖率 > 80% - 集成测试通过率 100% - 无严重的代码质量问题 ### 文档准确性 - API 文档完整,包含接口说明、参数说明、返回值说明 - 代码注释清晰,包含方法说明、参数说明、返回值说明 - 使用文档完整,包含使用示例和注意事项 ### 性能指标 - destructiveChanges.xml 配置响应时间 < 1s - Destructive Changes 部署触发响应时间 < 1s - Destructive Changes 验证响应时间 < 500ms - Destructive Changes 历史记录查询响应时间 < 1s - Destructive Changes 回滚响应时间 < 2s ## 风险 ### 输出质量风险 - destructiveChanges.xml 格式验证不完善可能导致误删除 - 删除操作不可逆可能导致数据丢失 - 回滚功能不完善可能导致无法恢复 ### 技术实现风险 - XML 解析库选择不当可能导致解析错误 - 备份和回滚逻辑复杂可能导致实现困难 - 异步任务状态跟踪困难 ### 时间成本风险 - Destructive Changes 功能实现可能需要较长时间 - 测试和验证可能需要较长时间 - 文档编写可能需要较长时间 ## 使用记录 | 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 | |------|---------|---------|---------|------|----------| | 2026-01-19 | Destructive Changes功能实现 | REQ-010-10, ADR-0019 | Destructive Changes功能代码 | 待反馈 | 待改进 |