发布 v1.10 #5
@@ -2,18 +2,29 @@ package tech.easyflow.admin.controller.job;
|
|||||||
|
|
||||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||||
import cn.hutool.core.date.DateUtil;
|
import cn.hutool.core.date.DateUtil;
|
||||||
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
import org.quartz.CronExpression;
|
import org.quartz.CronExpression;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||||
import tech.easyflow.common.domain.Result;
|
import tech.easyflow.common.domain.Result;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
import tech.easyflow.common.web.controller.BaseCurdController;
|
import tech.easyflow.common.web.controller.BaseCurdController;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
import tech.easyflow.job.entity.SysJob;
|
import tech.easyflow.job.entity.SysJob;
|
||||||
|
import tech.easyflow.job.job.JobConstant;
|
||||||
import tech.easyflow.job.service.SysJobService;
|
import tech.easyflow.job.service.SysJobService;
|
||||||
|
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||||
import tech.easyflow.log.annotation.LogRecord;
|
import tech.easyflow.log.annotation.LogRecord;
|
||||||
|
import tech.easyflow.system.enums.CategoryResourceType;
|
||||||
import tech.easyflow.common.entity.LoginAccount;
|
import tech.easyflow.system.enums.ResourceAction;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
@@ -21,6 +32,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统任务表 控制层。
|
* 系统任务表 控制层。
|
||||||
@@ -31,8 +43,32 @@ import java.util.List;
|
|||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/sysJob")
|
@RequestMapping("/api/v1/sysJob")
|
||||||
public class SysJobController extends BaseCurdController<SysJobService, SysJob> {
|
public class SysJobController extends BaseCurdController<SysJobService, SysJob> {
|
||||||
public SysJobController(SysJobService service) {
|
|
||||||
|
/** 工作流服务。 */
|
||||||
|
private final WorkflowService workflowService;
|
||||||
|
|
||||||
|
/** 资源访问控制服务。 */
|
||||||
|
private final ResourceAccessService resourceAccessService;
|
||||||
|
|
||||||
|
/** 工作流运行参数解析器。 */
|
||||||
|
private final WorkflowRunningParameterResolver workflowRunningParameterResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建定时任务控制器。
|
||||||
|
*
|
||||||
|
* @param service 定时任务服务
|
||||||
|
* @param workflowService 工作流服务
|
||||||
|
* @param resourceAccessService 资源访问控制服务
|
||||||
|
* @param workflowRunningParameterResolver 工作流运行参数解析器
|
||||||
|
*/
|
||||||
|
public SysJobController(SysJobService service,
|
||||||
|
WorkflowService workflowService,
|
||||||
|
ResourceAccessService resourceAccessService,
|
||||||
|
WorkflowRunningParameterResolver workflowRunningParameterResolver) {
|
||||||
super(service);
|
super(service);
|
||||||
|
this.workflowService = workflowService;
|
||||||
|
this.resourceAccessService = resourceAccessService;
|
||||||
|
this.workflowRunningParameterResolver = workflowRunningParameterResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/start")
|
@GetMapping("/start")
|
||||||
@@ -73,9 +109,94 @@ public class SysJobController extends BaseCurdController<SysJobService, SysJob>
|
|||||||
entity.setModified(new Date());
|
entity.setModified(new Date());
|
||||||
entity.setModifiedBy(loginUser.getId());
|
entity.setModifiedBy(loginUser.getId());
|
||||||
}
|
}
|
||||||
|
validateWorkflowReference(entity);
|
||||||
return super.onSaveOrUpdateBefore(entity, isSave);
|
return super.onSaveOrUpdateBefore(entity, isSave);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验工作流类型任务引用的工作流可被当前用户运行。
|
||||||
|
*
|
||||||
|
* @param entity 待保存的定时任务
|
||||||
|
* @throws BusinessException 工作流不存在、参数非法或无运行权限时抛出
|
||||||
|
*/
|
||||||
|
private void validateWorkflowReference(SysJob entity) {
|
||||||
|
if (entity == null
|
||||||
|
|| !Integer.valueOf(EnumJobType.TINY_FLOW.getCode()).equals(entity.getJobType())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(entity);
|
||||||
|
Workflow workflow = workflowService.getById(workflowId);
|
||||||
|
if (workflow == null) {
|
||||||
|
throw new BusinessException("工作流不存在,请重新选择");
|
||||||
|
}
|
||||||
|
resourceAccessService.assertAccess(
|
||||||
|
CategoryResourceType.WORKFLOW,
|
||||||
|
workflow,
|
||||||
|
ResourceAction.USE,
|
||||||
|
"无权限运行所选工作流"
|
||||||
|
);
|
||||||
|
validateRequiredWorkflowParams(entity, workflow);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 校验定时任务已填写工作流的全部必填运行参数。
|
||||||
|
*
|
||||||
|
* @param entity 待保存定时任务
|
||||||
|
* @param workflow 关联工作流
|
||||||
|
* @throws BusinessException 工作流参数配置无效或必填值缺失时抛出
|
||||||
|
*/
|
||||||
|
private void validateRequiredWorkflowParams(SysJob entity, Workflow workflow) {
|
||||||
|
List<Parameter> parameters =
|
||||||
|
workflowRunningParameterResolver.resolveStartParameters(workflow.getContent());
|
||||||
|
if (parameters == null) {
|
||||||
|
throw new BusinessException("工作流参数配置无效,请检查工作流后重试");
|
||||||
|
}
|
||||||
|
Map<String, Object> jobParams = entity.getJobParams();
|
||||||
|
Object rawWorkflowParams = jobParams == null
|
||||||
|
? null
|
||||||
|
: jobParams.get(JobConstant.WORKFLOW_PARAMS_KEY);
|
||||||
|
Map<?, ?> workflowParams = rawWorkflowParams instanceof Map<?, ?> map
|
||||||
|
? map
|
||||||
|
: Map.of();
|
||||||
|
for (Parameter parameter : parameters) {
|
||||||
|
if (parameter == null || !parameter.isRequired()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String name = parameter.getName();
|
||||||
|
if (!StringUtils.hasText(name)) {
|
||||||
|
throw new BusinessException("工作流存在无效必填参数配置,请检查工作流后重试");
|
||||||
|
}
|
||||||
|
if (!hasRequiredValue(workflowParams.get(name))) {
|
||||||
|
String label = StringUtils.hasText(parameter.getFormLabel())
|
||||||
|
? parameter.getFormLabel()
|
||||||
|
: name;
|
||||||
|
throw new BusinessException("工作流必填参数“" + label + "”不能为空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断必填参数值是否有效。
|
||||||
|
*
|
||||||
|
* @param value 参数值
|
||||||
|
* @return 非空时为 true
|
||||||
|
*/
|
||||||
|
private boolean hasRequiredValue(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value instanceof CharSequence sequence) {
|
||||||
|
return StringUtils.hasText(sequence);
|
||||||
|
}
|
||||||
|
if (value instanceof Collection<?> collection) {
|
||||||
|
return !collection.isEmpty();
|
||||||
|
}
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
return !map.isEmpty();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
protected Result onRemoveBefore(Collection<Serializable> ids) {
|
||||||
service.deleteJob(ids);
|
service.deleteJob(ids);
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
package tech.easyflow.admin.controller.job;
|
||||||
|
|
||||||
|
import com.easyagents.flow.core.chain.Parameter;
|
||||||
|
import org.mockito.MockedStatic;
|
||||||
|
import org.testng.Assert;
|
||||||
|
import org.testng.annotations.Test;
|
||||||
|
import tech.easyflow.ai.easyagentsflow.service.WorkflowRunningParameterResolver;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||||
|
import tech.easyflow.common.entity.LoginAccount;
|
||||||
|
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.job.entity.SysJob;
|
||||||
|
import tech.easyflow.job.job.JobConstant;
|
||||||
|
import tech.easyflow.job.service.SysJobService;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.mockStatic;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link SysJobController} 工作流任务参数校验测试。
|
||||||
|
*/
|
||||||
|
public class SysJobControllerTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证缺少工作流必填参数时拒绝保存定时任务。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldRejectWorkflowJobWhenRequiredParameterIsMissing() {
|
||||||
|
BigInteger workflowId = BigInteger.valueOf(101);
|
||||||
|
SysJobService jobService = mock(SysJobService.class);
|
||||||
|
WorkflowService workflowService = mock(WorkflowService.class);
|
||||||
|
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
|
||||||
|
WorkflowRunningParameterResolver parameterResolver =
|
||||||
|
mock(WorkflowRunningParameterResolver.class);
|
||||||
|
Workflow workflow = new Workflow();
|
||||||
|
workflow.setId(workflowId);
|
||||||
|
workflow.setContent("{}");
|
||||||
|
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||||
|
Parameter requiredParameter = mock(Parameter.class);
|
||||||
|
when(requiredParameter.isRequired()).thenReturn(true);
|
||||||
|
when(requiredParameter.getName()).thenReturn("user_input");
|
||||||
|
when(requiredParameter.getFormLabel()).thenReturn("用户问题");
|
||||||
|
when(parameterResolver.resolveStartParameters(workflow.getContent()))
|
||||||
|
.thenReturn(List.of(requiredParameter));
|
||||||
|
SysJobController controller = new SysJobController(
|
||||||
|
jobService,
|
||||||
|
workflowService,
|
||||||
|
resourceAccessService,
|
||||||
|
parameterResolver
|
||||||
|
);
|
||||||
|
SysJob job = new SysJob();
|
||||||
|
job.setJobType(EnumJobType.TINY_FLOW.getCode());
|
||||||
|
job.setJobParams(Map.of(
|
||||||
|
JobConstant.WORKFLOW_KEY, workflowId.toString(),
|
||||||
|
JobConstant.WORKFLOW_PARAMS_KEY, Map.of()
|
||||||
|
));
|
||||||
|
LoginAccount account = new LoginAccount();
|
||||||
|
account.setId(BigInteger.ONE);
|
||||||
|
|
||||||
|
try (MockedStatic<SaTokenUtil> saToken = mockStatic(SaTokenUtil.class)) {
|
||||||
|
saToken.when(SaTokenUtil::getLoginAccount).thenReturn(account);
|
||||||
|
|
||||||
|
BusinessException exception = Assert.expectThrows(
|
||||||
|
BusinessException.class,
|
||||||
|
() -> controller.onSaveOrUpdateBefore(job, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertTrue(exception.getMessage().contains("用户问题"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,9 @@ import tech.easyflow.ai.plugin.workflow.binding.WorkflowPluginBindingService;
|
|||||||
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||||
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
import tech.easyflow.ai.service.WorkflowService;
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowScheduleReferenceProvider;
|
||||||
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||||
import tech.easyflow.approval.service.ApprovalInstanceService;
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
import tech.easyflow.approval.enums.ApprovalResourceType;
|
import tech.easyflow.approval.enums.ApprovalResourceType;
|
||||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
@@ -18,6 +20,7 @@ import tech.easyflow.system.service.ResourceAccessService;
|
|||||||
|
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,6 +34,7 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
private final ResourceOfflineImpactService resourceOfflineImpactService;
|
||||||
private final WorkflowPluginBindingService workflowPluginBindingService;
|
private final WorkflowPluginBindingService workflowPluginBindingService;
|
||||||
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
|
||||||
|
private final List<WorkflowScheduleReferenceProvider> workflowScheduleReferenceProviders;
|
||||||
|
|
||||||
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
|
||||||
ResourceAccessService resourceAccessService,
|
ResourceAccessService resourceAccessService,
|
||||||
@@ -38,13 +42,17 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
ResourceOfflineImpactService resourceOfflineImpactService,
|
ResourceOfflineImpactService resourceOfflineImpactService,
|
||||||
WorkflowPluginBindingService workflowPluginBindingService,
|
WorkflowPluginBindingService workflowPluginBindingService,
|
||||||
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
|
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
|
||||||
ObjectMapper objectMapper) {
|
ObjectMapper objectMapper,
|
||||||
|
List<WorkflowScheduleReferenceProvider> workflowScheduleReferenceProviders) {
|
||||||
super(approvalInstanceService, objectMapper);
|
super(approvalInstanceService, objectMapper);
|
||||||
this.workflowService = workflowService;
|
this.workflowService = workflowService;
|
||||||
this.resourceAccessService = resourceAccessService;
|
this.resourceAccessService = resourceAccessService;
|
||||||
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
this.resourceOfflineImpactService = resourceOfflineImpactService;
|
||||||
this.workflowPluginBindingService = workflowPluginBindingService;
|
this.workflowPluginBindingService = workflowPluginBindingService;
|
||||||
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
|
||||||
|
this.workflowScheduleReferenceProviders = workflowScheduleReferenceProviders == null
|
||||||
|
? List.of()
|
||||||
|
: List.copyOf(workflowScheduleReferenceProviders);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -186,6 +194,44 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
|
|||||||
if (impact.isHasAgentBindings()) {
|
if (impact.isHasAgentBindings()) {
|
||||||
throw new BusinessException("此工作流仍被智能体使用,请先取消绑定后再删除");
|
throw new BusinessException("此工作流仍被智能体使用,请先取消绑定后再删除");
|
||||||
}
|
}
|
||||||
|
OfflineImpactBindingVo scheduledJob = findFirstScheduledJobReference(resource.getId());
|
||||||
|
if (scheduledJob != null) {
|
||||||
|
String jobName = scheduledJob.getTitle() == null ? "未命名任务" : scheduledJob.getTitle();
|
||||||
|
throw new BusinessException("此工作流仍被定时任务“" + jobName + "”引用,请先删除或重新选择定时任务中的工作流后再删除");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 审批通过后执行真实删除前,重新校验工作流引用。
|
||||||
|
*
|
||||||
|
* @param resourceId 工作流 ID
|
||||||
|
* @throws BusinessException 工作流不存在或仍被引用时抛出
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected void beforeRemove(BigInteger resourceId) {
|
||||||
|
Workflow workflow = requireResource(resourceId);
|
||||||
|
validateDelete(workflow, getCurrentStatus(workflow));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询第一个引用指定工作流的定时任务。
|
||||||
|
*
|
||||||
|
* @param workflowId 工作流 ID
|
||||||
|
* @return 定时任务摘要;未被引用时为 null
|
||||||
|
*/
|
||||||
|
private OfflineImpactBindingVo findFirstScheduledJobReference(BigInteger workflowId) {
|
||||||
|
for (WorkflowScheduleReferenceProvider provider : workflowScheduleReferenceProviders) {
|
||||||
|
List<OfflineImpactBindingVo> jobs = provider.listScheduledJobsByWorkflowId(workflowId);
|
||||||
|
if (jobs == null || jobs.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (OfflineImpactBindingVo job : jobs) {
|
||||||
|
if (job != null) {
|
||||||
|
return job;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package tech.easyflow.ai.service;
|
||||||
|
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流定时任务引用查询契约。
|
||||||
|
*
|
||||||
|
* <p>契约定义在 AI 模块中,由定时任务模块实现,避免工作流生命周期反向依赖定时任务实体。</p>
|
||||||
|
*/
|
||||||
|
public interface WorkflowScheduleReferenceProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询引用指定工作流的定时任务。
|
||||||
|
*
|
||||||
|
* @param workflowId 工作流 ID
|
||||||
|
* @return 定时任务摘要列表
|
||||||
|
*/
|
||||||
|
List<OfflineImpactBindingVo> listScheduledJobsByWorkflowId(BigInteger workflowId);
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
package tech.easyflow.ai.publish;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Test;
|
||||||
|
import tech.easyflow.ai.entity.Workflow;
|
||||||
|
import tech.easyflow.ai.enums.PublishStatus;
|
||||||
|
import tech.easyflow.ai.plugin.workflow.binding.WorkflowPluginBindingService;
|
||||||
|
import tech.easyflow.ai.plugin.workflow.snapshot.WorkflowPluginSnapshotResolver;
|
||||||
|
import tech.easyflow.ai.service.ResourceOfflineImpactService;
|
||||||
|
import tech.easyflow.ai.service.WorkflowScheduleReferenceProvider;
|
||||||
|
import tech.easyflow.ai.service.WorkflowService;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
|
||||||
|
import tech.easyflow.approval.enums.ApprovalActionType;
|
||||||
|
import tech.easyflow.approval.service.ApprovalInstanceService;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.system.service.ResourceAccessService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
|
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流删除依赖校验测试。
|
||||||
|
*/
|
||||||
|
public class WorkflowApprovalSubjectHandlerTest {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证工作流被定时任务引用时不能删除。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldRejectDeleteWhenScheduledJobReferencesWorkflow() {
|
||||||
|
BigInteger workflowId = BigInteger.valueOf(101);
|
||||||
|
ResourceOfflineImpactService offlineImpactService = mock(ResourceOfflineImpactService.class);
|
||||||
|
OfflineImpactCheckVo impact = new OfflineImpactCheckVo();
|
||||||
|
impact.setHasAgentBindings(false);
|
||||||
|
when(offlineImpactService.checkWorkflowImpact(workflowId)).thenReturn(impact);
|
||||||
|
WorkflowScheduleReferenceProvider scheduleReferenceProvider = ignored -> List.of(binding(201, "每日同步"));
|
||||||
|
WorkflowApprovalSubjectHandler handler = new WorkflowApprovalSubjectHandler(
|
||||||
|
mock(WorkflowService.class),
|
||||||
|
mock(ResourceAccessService.class),
|
||||||
|
mock(ApprovalInstanceService.class),
|
||||||
|
offlineImpactService,
|
||||||
|
mock(WorkflowPluginBindingService.class),
|
||||||
|
mock(WorkflowPluginSnapshotResolver.class),
|
||||||
|
new ObjectMapper(),
|
||||||
|
List.of(scheduleReferenceProvider)
|
||||||
|
);
|
||||||
|
Workflow workflow = new Workflow();
|
||||||
|
workflow.setId(workflowId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
handler.buildDeleteSnapshot(workflow, PublishStatus.OFFLINE);
|
||||||
|
Assert.fail("工作流被定时任务引用时应阻止删除");
|
||||||
|
} catch (BusinessException exception) {
|
||||||
|
Assert.assertTrue(exception.getMessage().contains("每日同步"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证审批等待期间新增定时任务引用后,真实删除动作会重新校验并阻止删除。
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
public void shouldRecheckScheduledJobReferenceBeforeApprovedDelete() {
|
||||||
|
BigInteger workflowId = BigInteger.valueOf(102);
|
||||||
|
WorkflowService workflowService = mock(WorkflowService.class);
|
||||||
|
ResourceOfflineImpactService offlineImpactService = mock(ResourceOfflineImpactService.class);
|
||||||
|
OfflineImpactCheckVo impact = new OfflineImpactCheckVo();
|
||||||
|
impact.setHasAgentBindings(false);
|
||||||
|
when(offlineImpactService.checkWorkflowImpact(workflowId)).thenReturn(impact);
|
||||||
|
AtomicInteger referenceChecks = new AtomicInteger();
|
||||||
|
WorkflowScheduleReferenceProvider scheduleReferenceProvider = ignored ->
|
||||||
|
referenceChecks.incrementAndGet() == 1
|
||||||
|
? List.of()
|
||||||
|
: List.of(binding(202, "审批期间新增任务"));
|
||||||
|
WorkflowApprovalSubjectHandler handler = new WorkflowApprovalSubjectHandler(
|
||||||
|
workflowService,
|
||||||
|
mock(ResourceAccessService.class),
|
||||||
|
mock(ApprovalInstanceService.class),
|
||||||
|
offlineImpactService,
|
||||||
|
mock(WorkflowPluginBindingService.class),
|
||||||
|
mock(WorkflowPluginSnapshotResolver.class),
|
||||||
|
new ObjectMapper(),
|
||||||
|
List.of(scheduleReferenceProvider)
|
||||||
|
);
|
||||||
|
Workflow workflow = new Workflow();
|
||||||
|
workflow.setId(workflowId);
|
||||||
|
workflow.setPublishStatus(PublishStatus.OFFLINE.getCode());
|
||||||
|
when(workflowService.getById(workflowId)).thenReturn(workflow);
|
||||||
|
|
||||||
|
handler.buildDeleteSnapshot(workflow, PublishStatus.OFFLINE);
|
||||||
|
|
||||||
|
try {
|
||||||
|
handler.applyApprovedAction(
|
||||||
|
ApprovalActionType.DELETE.getCode(),
|
||||||
|
workflowId,
|
||||||
|
Map.of(),
|
||||||
|
BigInteger.ONE
|
||||||
|
);
|
||||||
|
Assert.fail("审批期间新增定时任务引用后应阻止删除");
|
||||||
|
} catch (BusinessException exception) {
|
||||||
|
Assert.assertTrue(exception.getMessage().contains("审批期间新增任务"));
|
||||||
|
}
|
||||||
|
|
||||||
|
verify(workflowService, never()).removeById(workflowId);
|
||||||
|
Assert.assertEquals(2, referenceChecks.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建定时任务引用摘要。
|
||||||
|
*
|
||||||
|
* @param id 定时任务 ID
|
||||||
|
* @param title 定时任务名称
|
||||||
|
* @return 引用摘要
|
||||||
|
*/
|
||||||
|
private OfflineImpactBindingVo binding(long id, String title) {
|
||||||
|
OfflineImpactBindingVo binding = new OfflineImpactBindingVo();
|
||||||
|
binding.setId(BigInteger.valueOf(id));
|
||||||
|
binding.setTitle(title);
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import tech.easyflow.job.entity.SysJob;
|
|||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.math.BigInteger;
|
import java.math.BigInteger;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统任务表 服务层。
|
* 系统任务表 服务层。
|
||||||
@@ -26,4 +27,12 @@ public interface SysJobService extends IService<SysJob> {
|
|||||||
void startJob(BigInteger id);
|
void startJob(BigInteger id);
|
||||||
|
|
||||||
void stopJob(BigInteger id);
|
void stopJob(BigInteger id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询引用指定工作流的定时任务。
|
||||||
|
*
|
||||||
|
* @param workflowId 工作流 ID
|
||||||
|
* @return 引用该工作流的定时任务
|
||||||
|
*/
|
||||||
|
List<SysJob> listWorkflowJobsByWorkflowId(BigInteger workflowId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package tech.easyflow.job.service.impl;
|
package tech.easyflow.job.service.impl;
|
||||||
|
|
||||||
|
import com.mybatisflex.core.query.QueryWrapper;
|
||||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||||
import org.quartz.*;
|
import org.quartz.*;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||||
import tech.easyflow.common.constant.enums.EnumMisfirePolicy;
|
import tech.easyflow.common.constant.enums.EnumMisfirePolicy;
|
||||||
import tech.easyflow.common.constant.enums.EnumJobStatus;
|
import tech.easyflow.common.constant.enums.EnumJobStatus;
|
||||||
import tech.easyflow.common.cache.RedisLockExecutor;
|
import tech.easyflow.common.cache.RedisLockExecutor;
|
||||||
@@ -14,6 +16,7 @@ import tech.easyflow.job.job.QuartzJob;
|
|||||||
import tech.easyflow.job.job.QuartzJobNoConcurrent;
|
import tech.easyflow.job.job.QuartzJobNoConcurrent;
|
||||||
import tech.easyflow.job.mapper.SysJobMapper;
|
import tech.easyflow.job.mapper.SysJobMapper;
|
||||||
import tech.easyflow.job.service.SysJobService;
|
import tech.easyflow.job.service.SysJobService;
|
||||||
|
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||||
import tech.easyflow.job.util.JobUtil;
|
import tech.easyflow.job.util.JobUtil;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
@@ -22,6 +25,7 @@ import java.math.BigInteger;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 系统任务表 服务层实现。
|
* 系统任务表 服务层实现。
|
||||||
@@ -153,4 +157,19 @@ public class SysJobServiceImpl extends ServiceImpl<SysJobMapper, SysJob> implem
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<SysJob> listWorkflowJobsByWorkflowId(BigInteger workflowId) {
|
||||||
|
if (workflowId == null) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||||
|
.eq(SysJob::getJobType, EnumJobType.TINY_FLOW.getCode());
|
||||||
|
return list(queryWrapper).stream()
|
||||||
|
.filter(job -> workflowId.equals(SysJobWorkflowReferenceSupport.resolveWorkflowId(job)))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package tech.easyflow.job.service.impl;
|
||||||
|
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import tech.easyflow.ai.service.WorkflowScheduleReferenceProvider;
|
||||||
|
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
|
||||||
|
import tech.easyflow.job.entity.SysJob;
|
||||||
|
import tech.easyflow.job.service.SysJobService;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 定时任务对工作流的引用查询实现。
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
public class SysJobWorkflowReferenceProvider implements WorkflowScheduleReferenceProvider {
|
||||||
|
|
||||||
|
private final SysJobService sysJobService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建定时任务工作流引用查询提供者。
|
||||||
|
*
|
||||||
|
* @param sysJobService 定时任务服务
|
||||||
|
*/
|
||||||
|
public SysJobWorkflowReferenceProvider(SysJobService sysJobService) {
|
||||||
|
this.sysJobService = sysJobService;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritDoc}
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<OfflineImpactBindingVo> listScheduledJobsByWorkflowId(BigInteger workflowId) {
|
||||||
|
return sysJobService.listWorkflowJobsByWorkflowId(workflowId).stream()
|
||||||
|
.map(this::toBinding)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将定时任务转换为删除影响摘要。
|
||||||
|
*
|
||||||
|
* @param job 定时任务
|
||||||
|
* @return 影响摘要
|
||||||
|
*/
|
||||||
|
private OfflineImpactBindingVo toBinding(SysJob job) {
|
||||||
|
OfflineImpactBindingVo binding = new OfflineImpactBindingVo();
|
||||||
|
binding.setId(job.getId());
|
||||||
|
binding.setTitle(job.getJobName());
|
||||||
|
return binding;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package tech.easyflow.job.support;
|
||||||
|
|
||||||
|
import tech.easyflow.common.constant.enums.EnumJobType;
|
||||||
|
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||||
|
import tech.easyflow.job.entity.SysJob;
|
||||||
|
import tech.easyflow.job.job.JobConstant;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工作流类型定时任务的引用解析工具。
|
||||||
|
*/
|
||||||
|
public final class SysJobWorkflowReferenceSupport {
|
||||||
|
|
||||||
|
private SysJobWorkflowReferenceSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断任务是否为工作流类型。
|
||||||
|
*
|
||||||
|
* @param job 定时任务
|
||||||
|
* @return 工作流类型时为 true
|
||||||
|
*/
|
||||||
|
public static boolean isWorkflowJob(SysJob job) {
|
||||||
|
return job != null
|
||||||
|
&& Integer.valueOf(EnumJobType.TINY_FLOW.getCode()).equals(job.getJobType());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析工作流 ID;参数缺失或格式非法时返回 null。
|
||||||
|
*
|
||||||
|
* @param job 定时任务
|
||||||
|
* @return 工作流 ID,无法解析时为 null
|
||||||
|
*/
|
||||||
|
public static BigInteger resolveWorkflowId(SysJob job) {
|
||||||
|
if (!isWorkflowJob(job)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Map<String, Object> jobParams = job.getJobParams();
|
||||||
|
Object workflowId = jobParams == null ? null : jobParams.get(JobConstant.WORKFLOW_KEY);
|
||||||
|
if (workflowId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String value = String.valueOf(workflowId).trim();
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new BigInteger(value);
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取有效的工作流 ID。
|
||||||
|
*
|
||||||
|
* @param job 定时任务
|
||||||
|
* @return 工作流 ID
|
||||||
|
* @throws BusinessException 工作流参数缺失或格式非法时抛出
|
||||||
|
*/
|
||||||
|
public static BigInteger requireWorkflowId(SysJob job) {
|
||||||
|
BigInteger workflowId = resolveWorkflowId(job);
|
||||||
|
if (workflowId == null) {
|
||||||
|
throw new BusinessException("定时任务未配置有效工作流,请重新选择");
|
||||||
|
}
|
||||||
|
return workflowId;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,10 +14,12 @@ import tech.easyflow.common.satoken.util.SaTokenUtil;
|
|||||||
import tech.easyflow.common.util.SpringContextUtil;
|
import tech.easyflow.common.util.SpringContextUtil;
|
||||||
import tech.easyflow.job.entity.SysJob;
|
import tech.easyflow.job.entity.SysJob;
|
||||||
import tech.easyflow.job.job.JobConstant;
|
import tech.easyflow.job.job.JobConstant;
|
||||||
|
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||||
import tech.easyflow.system.entity.SysAccount;
|
import tech.easyflow.system.entity.SysAccount;
|
||||||
import tech.easyflow.system.service.SysAccountService;
|
import tech.easyflow.system.service.SysAccountService;
|
||||||
|
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
|
import java.math.BigInteger;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
@@ -69,7 +71,7 @@ public class JobUtil {
|
|||||||
public static Object execWorkFlow(SysJob job) {
|
public static Object execWorkFlow(SysJob job) {
|
||||||
Map<String, Object> jobParams = job.getJobParams();
|
Map<String, Object> jobParams = job.getJobParams();
|
||||||
JSONObject obj = new JSONObject(jobParams);
|
JSONObject obj = new JSONObject(jobParams);
|
||||||
String workflowId = obj.getString(JobConstant.WORKFLOW_KEY);
|
BigInteger workflowId = SysJobWorkflowReferenceSupport.requireWorkflowId(job);
|
||||||
JSONObject params = obj.getJSONObject(JobConstant.WORKFLOW_PARAMS_KEY);
|
JSONObject params = obj.getJSONObject(JobConstant.WORKFLOW_PARAMS_KEY);
|
||||||
|
|
||||||
ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class);
|
ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class);
|
||||||
@@ -79,8 +81,10 @@ public class JobUtil {
|
|||||||
try {
|
try {
|
||||||
TenantManager.ignoreTenantCondition();
|
TenantManager.ignoreTenantCondition();
|
||||||
|
|
||||||
ChainDefinition chain = executor.getDefinitionRepository().getChainDefinitionById(workflowId);
|
ChainDefinition chain = executor.getDefinitionRepository().getChainDefinitionById(workflowId.toString());
|
||||||
if (chain != null) {
|
if (chain == null) {
|
||||||
|
throw new IllegalStateException("定时任务关联的工作流不存在或已删除,id=" + workflowId);
|
||||||
|
}
|
||||||
if (accountId != null) {
|
if (accountId != null) {
|
||||||
// 设置的归属者
|
// 设置的归属者
|
||||||
SysAccount account = accountService.getById(accountId.toString());
|
SysAccount account = accountService.getById(accountId.toString());
|
||||||
@@ -88,12 +92,10 @@ public class JobUtil {
|
|||||||
params.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
params.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return executor.execute(workflowId, params);
|
return executor.execute(workflowId.toString(), params);
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
TenantManager.restoreTenantCondition();
|
TenantManager.restoreTenantCondition();
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Object execute(SysJob job) {
|
public static Object execute(SysJob job) {
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
/* eslint-disable vue/one-component-per-file */
|
||||||
|
|
||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import { defineComponent, h, nextTick } from 'vue';
|
||||||
|
|
||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import SysJobModal from './SysJobModal.vue';
|
||||||
|
|
||||||
|
const apiMocks = vi.hoisted(() => ({
|
||||||
|
get: vi.fn(),
|
||||||
|
post: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('#/api/request', () => ({ api: apiMocks }));
|
||||||
|
vi.mock('#/locales', () => ({ $t: (key: string) => key }));
|
||||||
|
vi.mock('#/components/cron/CronPicker.vue', () => ({
|
||||||
|
default: defineComponent({ name: 'CronPicker', setup: () => () => h('div') }),
|
||||||
|
}));
|
||||||
|
vi.mock('#/components/dict/DictSelect.vue', () => ({
|
||||||
|
default: defineComponent({ name: 'DictSelect', setup: () => () => h('div') }),
|
||||||
|
}));
|
||||||
|
vi.mock('#/views/ai/workflow/components/WorkflowFormItem.vue', () => ({
|
||||||
|
default: defineComponent({
|
||||||
|
name: 'WorkflowFormItem',
|
||||||
|
setup: () => () => h('div'),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
vi.mock('@easyflow/common-ui', () => ({
|
||||||
|
EasyFlowFormModal: defineComponent({
|
||||||
|
name: 'EasyFlowFormModal',
|
||||||
|
props: {
|
||||||
|
confirmDisabled: Boolean,
|
||||||
|
confirmLoading: Boolean,
|
||||||
|
open: Boolean,
|
||||||
|
},
|
||||||
|
emits: ['confirm'],
|
||||||
|
setup(props, { emit, slots }) {
|
||||||
|
return () =>
|
||||||
|
props.open
|
||||||
|
? h('section', [
|
||||||
|
...(slots.default?.() || []),
|
||||||
|
h(
|
||||||
|
'button',
|
||||||
|
{
|
||||||
|
'data-test': 'confirm',
|
||||||
|
onClick: () => emit('confirm'),
|
||||||
|
},
|
||||||
|
'confirm',
|
||||||
|
),
|
||||||
|
])
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
vi.mock('element-plus', () => ({
|
||||||
|
ElAlert: defineComponent({
|
||||||
|
name: 'ElAlert',
|
||||||
|
props: { title: { default: '', type: String } },
|
||||||
|
setup(props) {
|
||||||
|
return () => h('p', props.title);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ElForm: defineComponent({
|
||||||
|
name: 'ElForm',
|
||||||
|
setup(_, { expose, slots }) {
|
||||||
|
expose({
|
||||||
|
resetFields: vi.fn(),
|
||||||
|
validate: (callback: (valid: boolean) => void) => callback(true),
|
||||||
|
});
|
||||||
|
return () => h('form', slots.default?.());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ElFormItem: defineComponent({
|
||||||
|
name: 'ElFormItem',
|
||||||
|
setup(_, { slots }) {
|
||||||
|
return () => h('div', slots.default?.());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
ElInput: defineComponent({ name: 'ElInput', setup: () => () => h('input') }),
|
||||||
|
ElMessage: { success: vi.fn() },
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('sys job modal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
apiMocks.get.mockRejectedValue(new Error('工作流不存在'));
|
||||||
|
apiMocks.post.mockResolvedValue({ errorCode: 0, message: '保存成功' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('releases loading and keeps new task creation available after a deleted workflow fails to load', async () => {
|
||||||
|
const wrapper = mount(SysJobModal, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
loading: {
|
||||||
|
mounted(element, binding) {
|
||||||
|
element.dataset.loading = String(binding.value);
|
||||||
|
},
|
||||||
|
updated(element, binding) {
|
||||||
|
element.dataset.loading = String(binding.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(wrapper.vm as { openDialog: (row: unknown) => void }).openDialog({
|
||||||
|
id: 1,
|
||||||
|
jobParams: { workflowId: '101', workflowParams: {} },
|
||||||
|
jobType: 1,
|
||||||
|
});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(apiMocks.get).toHaveBeenCalledWith(
|
||||||
|
'/api/v1/workflow/getRunningParameters?id=101',
|
||||||
|
);
|
||||||
|
expect(wrapper.get('form').attributes('data-loading')).toBe('false');
|
||||||
|
expect(wrapper.text()).toContain('所选工作流已不可用,请重新选择');
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.findComponent({ name: 'EasyFlowFormModal' })
|
||||||
|
.props('confirmDisabled'),
|
||||||
|
).toBe(true);
|
||||||
|
await wrapper.get('[data-test="confirm"]').trigger('click');
|
||||||
|
expect(apiMocks.post).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
(wrapper.vm as { openDialog: (row: unknown) => void }).openDialog({});
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(wrapper.get('form').attributes('data-loading')).toBe('false');
|
||||||
|
expect(
|
||||||
|
wrapper
|
||||||
|
.findComponent({ name: 'EasyFlowFormModal' })
|
||||||
|
.props('confirmDisabled'),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('blocks save while workflow parameters are loading', async () => {
|
||||||
|
let resolveParameters: ((value: unknown) => void) | undefined;
|
||||||
|
apiMocks.get.mockReturnValue(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
resolveParameters = resolve;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const wrapper = mount(SysJobModal, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
loading: {
|
||||||
|
mounted(element, binding) {
|
||||||
|
element.dataset.loading = String(binding.value);
|
||||||
|
},
|
||||||
|
updated(element, binding) {
|
||||||
|
element.dataset.loading = String(binding.value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(wrapper.vm as { openDialog: (row: unknown) => void }).openDialog({
|
||||||
|
id: 1,
|
||||||
|
jobParams: { workflowId: '101', workflowParams: {} },
|
||||||
|
jobType: 1,
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
const modal = wrapper.findComponent({ name: 'EasyFlowFormModal' });
|
||||||
|
expect(modal.props('confirmDisabled')).toBe(true);
|
||||||
|
expect(modal.props('confirmLoading')).toBe(true);
|
||||||
|
|
||||||
|
await wrapper.get('[data-test="confirm"]').trigger('click');
|
||||||
|
expect(apiMocks.post).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
resolveParameters?.({ data: { parameters: [] } });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(modal.props('confirmDisabled')).toBe(false);
|
||||||
|
expect(modal.props('confirmLoading')).toBe(false);
|
||||||
|
|
||||||
|
await wrapper.get('[data-test="confirm"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
expect(apiMocks.post).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { FormInstance } from 'element-plus';
|
import type { FormInstance } from 'element-plus';
|
||||||
|
|
||||||
import { onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||||
|
|
||||||
@@ -60,10 +60,18 @@ const baseRules = ref({
|
|||||||
|
|
||||||
const paramsLoading = ref(false);
|
const paramsLoading = ref(false);
|
||||||
const workflowParams = ref<any[]>([]);
|
const workflowParams = ref<any[]>([]);
|
||||||
|
const workflowParamsLoadError = ref('');
|
||||||
const rules = ref({ ...baseRules.value });
|
const rules = ref({ ...baseRules.value });
|
||||||
|
let workflowParamsRequestId = 0;
|
||||||
|
const workflowParamsSubmissionBlocked = computed(
|
||||||
|
() =>
|
||||||
|
entity.value.jobType === 1 &&
|
||||||
|
(paramsLoading.value || Boolean(workflowParamsLoadError.value)),
|
||||||
|
);
|
||||||
|
|
||||||
// functions
|
// functions
|
||||||
function openDialog(row: any) {
|
function openDialog(row: any) {
|
||||||
|
resetWorkflowParamsState();
|
||||||
if (row.id) {
|
if (row.id) {
|
||||||
entity.value = { ...row };
|
entity.value = { ...row };
|
||||||
// 确保 jobParams 存在
|
// 确保 jobParams 存在
|
||||||
@@ -80,8 +88,13 @@ function openDialog(row: any) {
|
|||||||
dialogVisible.value = true;
|
dialogVisible.value = true;
|
||||||
}
|
}
|
||||||
function save() {
|
function save() {
|
||||||
|
if (btnLoading.value || workflowParamsSubmissionBlocked.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
saveForm.value?.validate((valid) => {
|
saveForm.value?.validate((valid) => {
|
||||||
if (valid) {
|
if (!valid || btnLoading.value || workflowParamsSubmissionBlocked.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
btnLoading.value = true;
|
btnLoading.value = true;
|
||||||
api
|
api
|
||||||
.post(
|
.post(
|
||||||
@@ -99,18 +112,17 @@ function save() {
|
|||||||
.catch(() => {
|
.catch(() => {
|
||||||
btnLoading.value = false;
|
btnLoading.value = false;
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function closeDialog() {
|
function closeDialog() {
|
||||||
saveForm.value?.resetFields();
|
saveForm.value?.resetFields();
|
||||||
isAdd.value = true;
|
isAdd.value = true;
|
||||||
entity.value = { ...initEntity };
|
entity.value = { ...initEntity };
|
||||||
workflowParams.value = [];
|
resetWorkflowParamsState();
|
||||||
dialogVisible.value = false;
|
dialogVisible.value = false;
|
||||||
}
|
}
|
||||||
function jobTypeChange(v: any) {
|
function jobTypeChange(v: any) {
|
||||||
workflowParams.value = [];
|
resetWorkflowParamsState();
|
||||||
entity.value.jobParams = {
|
entity.value.jobParams = {
|
||||||
workflowParams: {},
|
workflowParams: {},
|
||||||
};
|
};
|
||||||
@@ -122,12 +134,34 @@ function workflowChange(v: any) {
|
|||||||
entity.value.jobParams.workflowParams = {};
|
entity.value.jobParams.workflowParams = {};
|
||||||
getWorkflowParams(v);
|
getWorkflowParams(v);
|
||||||
}
|
}
|
||||||
function getWorkflowParams(v: any) {
|
function resetWorkflowParamsState() {
|
||||||
paramsLoading.value = true;
|
workflowParamsRequestId += 1;
|
||||||
api.get(`/api/v1/workflow/getRunningParameters?id=${v}`).then((res) => {
|
|
||||||
paramsLoading.value = false;
|
paramsLoading.value = false;
|
||||||
workflowParams.value = res.data.parameters;
|
workflowParams.value = [];
|
||||||
});
|
workflowParamsLoadError.value = '';
|
||||||
|
}
|
||||||
|
async function getWorkflowParams(v: any) {
|
||||||
|
const requestId = ++workflowParamsRequestId;
|
||||||
|
paramsLoading.value = true;
|
||||||
|
workflowParams.value = [];
|
||||||
|
workflowParamsLoadError.value = '';
|
||||||
|
try {
|
||||||
|
const res = await api.get(`/api/v1/workflow/getRunningParameters?id=${v}`);
|
||||||
|
if (requestId !== workflowParamsRequestId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workflowParams.value = Array.isArray(res.data?.parameters)
|
||||||
|
? res.data.parameters
|
||||||
|
: [];
|
||||||
|
} catch {
|
||||||
|
if (requestId === workflowParamsRequestId) {
|
||||||
|
workflowParamsLoadError.value = '所选工作流已不可用,请重新选择';
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (requestId === workflowParamsRequestId) {
|
||||||
|
paramsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const str = '"param"';
|
const str = '"param"';
|
||||||
</script>
|
</script>
|
||||||
@@ -138,7 +172,8 @@ const str = '"param"';
|
|||||||
:closable="!btnLoading"
|
:closable="!btnLoading"
|
||||||
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
:title="isAdd ? $t('button.add') : $t('button.edit')"
|
||||||
:before-close="closeDialog"
|
:before-close="closeDialog"
|
||||||
:confirm-loading="btnLoading"
|
:confirm-disabled="workflowParamsSubmissionBlocked"
|
||||||
|
:confirm-loading="btnLoading || paramsLoading"
|
||||||
:confirm-text="$t('button.save')"
|
:confirm-text="$t('button.save')"
|
||||||
:submitting="btnLoading"
|
:submitting="btnLoading"
|
||||||
@confirm="save"
|
@confirm="save"
|
||||||
@@ -180,6 +215,13 @@ const str = '"param"';
|
|||||||
@change="workflowChange"
|
@change="workflowChange"
|
||||||
/>
|
/>
|
||||||
</ElFormItem>
|
</ElFormItem>
|
||||||
|
<ElAlert
|
||||||
|
v-if="workflowParamsLoadError"
|
||||||
|
:title="workflowParamsLoadError"
|
||||||
|
type="error"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
/>
|
||||||
<WorkflowFormItem
|
<WorkflowFormItem
|
||||||
v-model:run-params="entity.jobParams.workflowParams"
|
v-model:run-params="entity.jobParams.workflowParams"
|
||||||
:parameters="workflowParams"
|
:parameters="workflowParams"
|
||||||
|
|||||||
Reference in New Issue
Block a user