27 KiB
27 KiB
Prompt - Destructive Changes功能实现
输入引用
引用相关的 docs 文档链接:
- REQ-010-10.md - Destructive Changes功能实现需求文档
- 0019-destructive-changes.md - Destructive Changes功能实现架构决策
- REQ-010-8.md - 元数据部署核心功能需求文档
- 0017-metadata-deploy-core.md - 元数据部署核心功能架构决策
- metadata-module.md - Salesforce Metadata API 模块说明
Context Maps
强制列出本次 Prompt 依赖的 Canvas 文件:
- Authentication.canvas - 项目架构视觉化展示
- 相关节点: 集成核心 - 提供与Salesforce的各种连接方式
- 相关节点: IMetadataApiService - 元数据API服务接口
- 相关节点: MetadataApiClient - 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 控制器
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 服务接口
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 服务实现
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<DestructiveChangeRule> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_id", request.getTaskId());
List<DestructiveChangeRule> 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<Void> 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<DataiMetaJobExecution> 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<DataiMetaJobExecution> 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<Void> 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 服务接口
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 服务实现
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<DestructiveChangeRule> 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<DestructiveChangeRule> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_id", taskId);
List<DestructiveChangeRule> 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 工具类
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 工具类
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<DestructiveChangeRule> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("task_id", taskId);
List<DestructiveChangeRule> rules = destructiveChangeRuleMapper.selectList(queryWrapper);
return buildXml(rules);
}
public String buildXml(List<DestructiveChangeRule> rules) {
StringBuilder xml = new StringBuilder();
xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.append("<Package xmlns=\"http://soap.sforce.com/2006/04/metadata\">\n");
Map<String, List<DestructiveChangeRule>> groupedRules = rules.stream()
.collect(Collectors.groupingBy(DestructiveChangeRule::getMetadataType));
for (Map.Entry<String, List<DestructiveChangeRule>> entry : groupedRules.entrySet()) {
xml.append(" <types>\n");
for (DestructiveChangeRule rule : entry.getValue()) {
xml.append(" <members>").append(rule.getMemberName()).append("</members>\n");
}
xml.append(" <name>").append(entry.getKey()).append("</name>\n");
xml.append(" </types>\n");
}
xml.append(" <version>56.0</version>\n");
xml.append("</Package>");
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 实体类
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 接口
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<DestructiveChangeRule> {
}
10. DTO 类
package com.datai.salesforce.metadata.destructive.dto;
import lombok.Data;
import java.util.List;
@Data
public class DestructiveChangesConfigRequest {
private Long taskId;
private List<Rule> rules;
@Data
public static class Rule {
private String metadataType;
private String memberName;
}
}
@Data
public class DestructiveChangesConfigResponse {
private Long taskId;
private String xml;
private List<Object> 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<Object> records;
}
@Data
public class DestructiveChangesRollbackRequest {
private Long jobId;
}
@Data
public class DestructiveChangesRollbackResponse {
private String jobId;
private String status;
private String message;
}
11. DestructiveChangesException 异常类
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功能代码 | 待反馈 | 待改进 |