# Prompt - 元数据任务定义管理 ## 输入引用 引用相关的 docs 文档链接: - [REQ-010-4.md](../requirements/REQ-010-4.md) - 元数据任务定义管理需求文档 - [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组织配置管理 - [0013-metadata-task-management.md](../decisions/adr/0013-metadata-task-management.md) - 元数据任务定义管理架构决策 ## Context Maps 强制列出本次 Prompt 依赖的 Canvas 文件: - [Authentication.canvas](../../Authentication.canvas) - 项目架构视觉化展示 - **相关节点**: [集成任务](node_integration_task) - 处理Salesforce的定时同步任务 ## 目标 实现元数据任务定义的完整管理功能,包括: 1. 任务定义 CRUD 功能 - 支持分页查询、条件查询 2. package.xml 内容配置 - 支持可视化编辑,符合 Salesforce Metadata API 规范 3. API 版本选择 - 支持多种 API 版本 4. 调度类型选择 - 支持 Manual/Cron 调度类型 5. Cron 表达式配置 - 支持 Cron 表达式配置和验证 6. 任务验证功能 - 验证 package.xml 和调度配置 ## 输出格式 ### 代码输出格式 #### 1. 枚举类 ```java package com.datai.salesforce.metadata.enums; import com.baomidou.mybatisplus.annotation.EnumValue; import com.fasterxml.jackson.annotation.JsonValue; /** * 调度类型枚举 */ public enum ScheduleType { MANUAL("manual", "手动执行"), CRON("cron", "定时执行"); @EnumValue private final String code; @JsonValue private final String displayName; ScheduleType(String code, String displayName) { this.code = code; this.displayName = displayName; } public String getCode() { return code; } public String getDisplayName() { return displayName; } public static ScheduleType fromCode(String code) { for (ScheduleType type : ScheduleType.values()) { if (type.getCode().equals(code)) { return type; } } throw new IllegalArgumentException("Invalid schedule type code: " + code); } } ``` #### 2. package.xml 编辑器 ```java package com.datai.salesforce.metadata.util; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.NodeList; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.transform.OutputKeys; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import java.io.StringReader; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; /** * package.xml 编辑器 */ public class PackageXmlEditor { private static final String PACKAGE_XML_HEADER = "\n\n"; /** * 创建 package.xml */ public String createPackageXml() { return PACKAGE_XML_HEADER; } /** * 添加元数据类型 */ public String addMetadataType(String packageXml, String metadataType) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new InputSource(new StringReader(packageXml))); Element root = document.getDocumentElement(); Element typesElement = document.createElement("types"); root.appendChild(typesElement); Element nameElement = document.createElement("name"); nameElement.setTextContent(metadataType); typesElement.appendChild(nameElement); return documentToString(document); } catch (Exception e) { throw new RuntimeException("Failed to add metadata type", e); } } /** * 添加元数据成员 */ public String addMetadataMember(String packageXml, String metadataType, String member) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new InputSource(new StringReader(packageXml))); Element root = document.getDocumentElement(); NodeList typesList = root.getElementsByTagName("types"); for (int i = 0; i < typesList.getLength(); i++) { Element typesElement = (Element) typesList.item(i); NodeList nameList = typesElement.getElementsByTagName("name"); if (nameList.getLength() > 0) { Element nameElement = (Element) nameList.item(0); if (nameElement.getTextContent().equals(metadataType)) { Element membersElement = document.createElement("members"); membersElement.setTextContent(member); typesElement.appendChild(membersElement); break; } } } return documentToString(document); } catch (Exception e) { throw new RuntimeException("Failed to add metadata member", e); } } /** * 删除元数据类型 */ public String removeMetadataType(String packageXml, String metadataType) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new InputSource(new StringReader(packageXml))); Element root = document.getDocumentElement(); NodeList typesList = root.getElementsByTagName("types"); for (int i = 0; i < typesList.getLength(); i++) { Element typesElement = (Element) typesList.item(i); NodeList nameList = typesElement.getElementsByTagName("name"); if (nameList.getLength() > 0) { Element nameElement = (Element) nameList.item(0); if (nameElement.getTextContent().equals(metadataType)) { root.removeChild(typesElement); break; } } } return documentToString(document); } catch (Exception e) { throw new RuntimeException("Failed to remove metadata type", e); } } /** * 获取元数据类型列表 */ public List getMetadataTypes(String packageXml) { List metadataTypes = new ArrayList<>(); try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); Document document = builder.parse(new InputSource(new StringReader(packageXml))); Element root = document.getDocumentElement(); NodeList typesList = root.getElementsByTagName("types"); for (int i = 0; i < typesList.getLength(); i++) { Element typesElement = (Element) typesList.item(i); NodeList nameList = typesElement.getElementsByTagName("name"); if (nameList.getLength() > 0) { Element nameElement = (Element) nameList.item(0); metadataTypes.add(nameElement.getTextContent()); } } } catch (Exception e) { throw new RuntimeException("Failed to get metadata types", e); } return metadataTypes; } /** * 验证 package.xml */ public boolean validatePackageXml(String packageXml) { try { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); DocumentBuilder builder = factory.newDocumentBuilder(); builder.parse(new InputSource(new StringReader(packageXml))); return true; } catch (Exception e) { return false; } } private String documentToString(Document document) throws Exception { TransformerFactory transformerFactory = TransformerFactory.newInstance(); Transformer transformer = transformerFactory.newTransformer(); transformer.setOutputProperty(OutputKeys.INDENT, "yes"); transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2"); StringWriter writer = new StringWriter(); transformer.transform(new DOMSource(document), new StreamResult(writer)); return writer.toString(); } } ``` #### 3. Cron 表达式验证器 ```java package com.datai.salesforce.metadata.util; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.List; /** * Cron 表达式验证器 */ @Component public class CronExpressionValidator { /** * 验证 Cron 表达式 */ public ValidationResult validate(String cronExpression) { ValidationResult result = new ValidationResult(); result.setValid(true); if (cronExpression == null || cronExpression.trim().isEmpty()) { result.setValid(false); result.addError("Cron expression cannot be empty"); return result; } String[] parts = cronExpression.split("\\s+"); if (parts.length != 6) { result.setValid(false); result.addError("Cron expression must have 6 parts (second minute hour day month weekday)"); return result; } if (!validatePart(parts[0], 0, 59, "second")) { result.setValid(false); result.addError("Invalid second value"); } if (!validatePart(parts[1], 0, 59, "minute")) { result.setValid(false); result.addError("Invalid minute value"); } if (!validatePart(parts[2], 0, 23, "hour")) { result.setValid(false); result.addError("Invalid hour value"); } if (!validatePart(parts[3], 1, 31, "day")) { result.setValid(false); result.addError("Invalid day value"); } if (!validatePart(parts[4], 1, 12, "month")) { result.setValid(false); result.addError("Invalid month value"); } if (!validatePart(parts[5], 0, 6, "weekday")) { result.setValid(false); result.addError("Invalid weekday value"); } return result; } private boolean validatePart(String part, int min, int max, String partName) { if (part.equals("*")) { return true; } if (part.contains("/")) { String[] rangeParts = part.split("/"); if (rangeParts.length != 2) { return false; } try { int step = Integer.parseInt(rangeParts[1]); if (step <= 0) { return false; } return validatePart(rangeParts[0], min, max, partName); } catch (NumberFormatException e) { return false; } } if (part.contains("-")) { String[] rangeParts = part.split("-"); if (rangeParts.length != 2) { return false; } try { int start = Integer.parseInt(rangeParts[0]); int end = Integer.parseInt(rangeParts[1]); return start >= min && end <= max && start <= end; } catch (NumberFormatException e) { return false; } } if (part.contains(",")) { String[] values = part.split(","); for (String value : values) { try { int num = Integer.parseInt(value); if (num < min || num > max) { return false; } } catch (NumberFormatException e) { return false; } } return true; } try { int num = Integer.parseInt(part); return num >= min && num <= max; } catch (NumberFormatException e) { return false; } } /** * 验证结果 */ public static class ValidationResult { private boolean valid = true; private List errors = new ArrayList<>(); public boolean isValid() { return valid; } public void setValid(boolean valid) { this.valid = valid; } public List getErrors() { return errors; } public void addError(String error) { this.valid = false; this.errors.add(error); } } } ``` #### 4. 控制器 ```java package com.datai.salesforce.metadata.controller; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.datai.salesforce.metadata.entity.DataiMetaTask; import com.datai.salesforce.metadata.service.IMetadataTaskService; import com.datai.salesforce.metadata.service.ITaskValidationService; import com.datai.salesforce.metadata.validation.TaskValidationResult; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * 元数据任务控制器 */ @RestController @RequestMapping("/api/metadata-task") @RequiredArgsConstructor @Validated public class MetadataTaskController { private final IMetadataTaskService metadataTaskService; private final ITaskValidationService taskValidationService; /** * 创建元数据任务 */ @PostMapping public DataiMetaTask createMetadataTask(@Valid @RequestBody DataiMetaTask task) { return metadataTaskService.createMetadataTask(task); } /** * 更新元数据任务 */ @PutMapping("/{id}") public DataiMetaTask updateMetadataTask(@PathVariable Long id, @Valid @RequestBody DataiMetaTask task) { task.setId(id); return metadataTaskService.updateMetadataTask(task); } /** * 删除元数据任务 */ @DeleteMapping("/{id}") public void deleteMetadataTask(@PathVariable Long id) { metadataTaskService.deleteMetadataTask(id); } /** * 查询元数据任务详情 */ @GetMapping("/{id}") public DataiMetaTask getMetadataTask(@PathVariable Long id) { return metadataTaskService.getMetadataTask(id); } /** * 查询元数据任务列表 */ @GetMapping public Page getMetadataTaskList( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String taskName, @RequestParam(required = false) String scheduleType) { return metadataTaskService.getMetadataTaskList(page, size, taskName, scheduleType); } /** * 验证元数据任务 */ @PostMapping("/{id}/validate") public TaskValidationResult validateMetadataTask(@PathVariable Long id) { return taskValidationService.validateMetadataTask(id); } } ``` #### 5. 服务接口 ```java package com.datai.salesforce.metadata.service; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.datai.salesforce.metadata.entity.DataiMetaTask; /** * 元数据任务服务接口 */ public interface IMetadataTaskService { /** * 创建元数据任务 */ DataiMetaTask createMetadataTask(DataiMetaTask task); /** * 更新元数据任务 */ DataiMetaTask updateMetadataTask(DataiMetaTask task); /** * 删除元数据任务 */ void deleteMetadataTask(Long id); /** * 查询元数据任务详情 */ DataiMetaTask getMetadataTask(Long id); /** * 查询元数据任务列表 */ Page getMetadataTaskList(int page, int size, String taskName, String scheduleType); } ``` #### 6. 服务实现 ```java package com.datai.salesforce.metadata.service.impl; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.datai.salesforce.metadata.entity.DataiMetaTask; import com.datai.salesforce.metadata.enums.TaskStatus; import com.datai.salesforce.metadata.mapper.DataiMetaTaskMapper; import com.datai.salesforce.metadata.service.IMetadataTaskService; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; /** * 元数据任务服务实现 */ @Service @RequiredArgsConstructor public class MetadataTaskServiceImpl implements IMetadataTaskService { private final DataiMetaTaskMapper taskMapper; @Override @Transactional public DataiMetaTask createMetadataTask(DataiMetaTask task) { task.setTaskStatus(TaskStatus.PENDING); taskMapper.insert(task); return task; } @Override @Transactional public DataiMetaTask updateMetadataTask(DataiMetaTask task) { DataiMetaTask existing = taskMapper.selectById(task.getId()); if (existing == null) { throw new RuntimeException("Task not found"); } taskMapper.updateById(task); return task; } @Override @Transactional public void deleteMetadataTask(Long id) { taskMapper.deleteById(id); } @Override public DataiMetaTask getMetadataTask(Long id) { return taskMapper.selectById(id); } @Override public Page getMetadataTaskList(int page, int size, String taskName, String scheduleType) { QueryWrapper queryWrapper = new QueryWrapper<>(); if (taskName != null && !taskName.isEmpty()) { queryWrapper.like("task_name", taskName); } if (scheduleType != null && !scheduleType.isEmpty()) { queryWrapper.eq("schedule_type", scheduleType); } return taskMapper.selectPage(new Page<>(page, size), queryWrapper); } } ``` #### 7. 任务验证服务接口 ```java package com.datai.salesforce.metadata.service; import com.datai.salesforce.metadata.validation.TaskValidationResult; /** * 任务验证服务接口 */ public interface ITaskValidationService { /** * 验证元数据任务 */ TaskValidationResult validateMetadataTask(Long id); } ``` #### 8. 任务验证服务实现 ```java package com.datai.salesforce.metadata.service.impl; import com.datai.salesforce.metadata.entity.DataiMetaTask; import com.datai.salesforce.metadata.mapper.DataiMetaTaskMapper; import com.datai.salesforce.metadata.service.ITaskValidationService; import com.datai.salesforce.metadata.util.CronExpressionValidator; import com.datai.salesforce.metadata.util.PackageXmlEditor; import com.datai.salesforce.metadata.validation.TaskValidationResult; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; /** * 任务验证服务实现 */ @Service @RequiredArgsConstructor public class TaskValidationServiceImpl implements ITaskValidationService { private final DataiMetaTaskMapper taskMapper; private final PackageXmlEditor packageXmlEditor; private final CronExpressionValidator cronExpressionValidator; @Override public TaskValidationResult validateMetadataTask(Long id) { DataiMetaTask task = taskMapper.selectById(id); if (task == null) { return TaskValidationResult.failure("Task not found"); } TaskValidationResult result = new TaskValidationResult(); if (!packageXmlEditor.validatePackageXml(task.getPackageXml())) { result.addError("Invalid package.xml format"); } if (task.getScheduleType().equals("cron")) { CronExpressionValidator.ValidationResult cronResult = cronExpressionValidator.validate(task.getCronExpression()); if (!cronResult.isValid()) { result.addErrors(cronResult.getErrors()); } } return result; } } ``` #### 9. 验证结果类 ```java package com.datai.salesforce.metadata.validation; import lombok.Data; import java.util.ArrayList; import java.util.List; /** * 任务验证结果 */ @Data public class TaskValidationResult { private boolean valid = true; private List errors = new ArrayList<>(); public static TaskValidationResult success() { return new TaskValidationResult(); } public static TaskValidationResult failure(String error) { TaskValidationResult result = new TaskValidationResult(); result.setValid(false); result.addError(error); return result; } public void addError(String error) { this.valid = false; this.errors.add(error); } public void addErrors(List errors) { this.valid = false; this.errors.addAll(errors); } } ``` #### 10. 单元测试 ```java package com.datai.salesforce.metadata.service.impl; import com.datai.salesforce.metadata.entity.DataiMetaTask; import com.datai.salesforce.metadata.enums.TaskStatus; import com.datai.salesforce.metadata.mapper.DataiMetaTaskMapper; import com.datai.salesforce.metadata.service.IMetadataTaskService; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) class MetadataTaskServiceImplTest { @Mock private DataiMetaTaskMapper taskMapper; @InjectMocks private MetadataTaskServiceImpl metadataTaskService; @Test void testCreateMetadataTask() { DataiMetaTask task = new DataiMetaTask(); task.setTaskName("Test Task"); task.setScheduleType("manual"); when(taskMapper.insert(any(DataiMetaTask.class))).thenReturn(1); DataiMetaTask result = metadataTaskService.createMetadataTask(task); assertNotNull(result); assertEquals(TaskStatus.PENDING, result.getTaskStatus()); verify(taskMapper, times(1)).insert(any(DataiMetaTask.class)); } @Test void testGetMetadataTaskList() { when(taskMapper.selectPage(any(), any())).thenReturn(new Page<>()); Page result = metadataTaskService.getMetadataTaskList(1, 10, null, null); assertNotNull(result); verify(taskMapper, times(1)).selectPage(any(), any()); } } ``` ## 约束 ### 技术栈限制 - 必须基于现有的 Spring Boot 3 + Vue 3 技术栈 - 必须使用 MyBatis Plus 作为持久层框架 ### 架构约束 - 必须遵循 Authentication.canvas 中定义的架构和调用关系 - 必须在 datai-salesforce-metadata 模块下实现 ### API 约束 - 必须使用 RESTful API 设计接口 - 必须使用 @Valid 注解进行参数验证 - 必须支持分页查询和条件查询 ### package.xml 约束 - package.xml 格式必须符合 Salesforce Metadata API 规范 - package.xml 必须支持多种元数据类型配置 - package.xml 必须支持通配符配置 ### Cron 表达式约束 - Cron 表达式必须符合标准格式 - Cron 表达式必须支持秒、分、时、日、月、周 ### 性能约束 - 元数据任务 CRUD 操作响应时间不超过 500ms - package.xml 配置操作响应时间不超过 1s - Cron 表达式验证操作响应时间不超过 500ms - 任务验证操作响应时间不超过 1s ## Rule Set "请严格参考 @Authentication.canvas 中的状态机转移逻辑,不要自行发挥。" **具体规则**: - 必须使用 Canvas 中定义的类名和方法名 - 必须遵循 Canvas 中定义的调用关系 - 必须参考 Canvas 中的流程图逻辑 - 必须使用现有的集成核心功能进行 API 调用 - 必须遵循现有的异常处理机制 - 必须遵循现有的日志记录规范 ## 验收标准 ### 功能完整性 - 任务定义 CRUD 功能正常工作,能够成功创建、编辑、删除、查询元数据任务 - package.xml 内容配置正常工作,能够正确配置和验证 package.xml - API 版本选择正常工作,能够正确选择和显示 API 版本 - 调度类型选择正常工作,能够正确选择和显示调度类型 - Cron 表达式配置正常工作,能够正确配置和验证 Cron 表达式 - 任务验证功能正常工作,能够正确验证 package.xml 和调度配置 ### 代码规范性 - 代码符合项目编码规范,有清晰的注释 - 单元测试覆盖率不低于 80% - 集成测试覆盖率不低于 60% - 代码审查通过,没有严重的问题 ### 性能指标 - 元数据任务 CRUD 操作响应时间不超过 500ms - package.xml 配置操作响应时间不超过 1s - Cron 表达式验证操作响应时间不超过 500ms - 任务验证操作响应时间不超过 1s ## 风险 ### 输出质量风险 - package.xml 验证不准确可能导致验证错误 - Cron 表达式解析不准确可能导致验证错误 - 任务验证逻辑复杂可能导致验证不准确 ### 技术实现风险 - DOM 解析器内存占用高可能导致性能问题 - Cron 表达式解析库不支持某些格式可能导致验证错误 - 多任务管理复杂可能导致任务冲突 ### 时间成本风险 - 功能复杂可能导致开发周期延长 - 测试覆盖面广可能导致测试时间增加 - 文档编写详细可能导致文档时间增加 ## 使用记录 | 日期 | 使用场景 | 输入参数 | 输出结果 | 反馈 | 改进措施 | |------|---------|---------|---------|------|----------| | 2026-01-19 | 实现任务定义 CRUD 功能 | REQ-010-4 需求文档 | MetadataTaskController、IMetadataTaskService、MetadataTaskServiceImpl | 功能正常,符合需求 | 无 | | 2026-01-19 | 实现 package.xml 内容配置 | REQ-010-4 需求文档 | PackageXmlEditor | 功能正常,符合需求 | 无 | | 2026-01-19 | 实现 Cron 表达式配置 | REQ-010-4 需求文档 | CronExpressionValidator | 功能正常,符合需求 | 无 | | 2026-01-19 | 实现任务验证功能 | REQ-010-4 需求文档 | ITaskValidationService、TaskValidationServiceImpl | 功能正常,符合需求 | 无 |