fix: 防止定时任务引用失效工作流

- 保存任务时校验工作流权限与必填参数

- 删除工作流前检查并重新确认定时任务引用
This commit is contained in:
2026-08-03 14:50:28 +08:00
parent 93db17b384
commit 8c334be65d
12 changed files with 817 additions and 43 deletions

View File

@@ -8,7 +8,9 @@ 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.WorkflowService;
import tech.easyflow.ai.service.WorkflowScheduleReferenceProvider;
import tech.easyflow.ai.vo.OfflineImpactCheckVo;
import tech.easyflow.ai.vo.OfflineImpactBindingVo;
import tech.easyflow.approval.service.ApprovalInstanceService;
import tech.easyflow.approval.enums.ApprovalResourceType;
import tech.easyflow.common.web.exceptions.BusinessException;
@@ -18,6 +20,7 @@ import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
@@ -31,6 +34,7 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
private final ResourceOfflineImpactService resourceOfflineImpactService;
private final WorkflowPluginBindingService workflowPluginBindingService;
private final WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver;
private final List<WorkflowScheduleReferenceProvider> workflowScheduleReferenceProviders;
public WorkflowApprovalSubjectHandler(WorkflowService workflowService,
ResourceAccessService resourceAccessService,
@@ -38,13 +42,17 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
ResourceOfflineImpactService resourceOfflineImpactService,
WorkflowPluginBindingService workflowPluginBindingService,
WorkflowPluginSnapshotResolver workflowPluginSnapshotResolver,
ObjectMapper objectMapper) {
ObjectMapper objectMapper,
List<WorkflowScheduleReferenceProvider> workflowScheduleReferenceProviders) {
super(approvalInstanceService, objectMapper);
this.workflowService = workflowService;
this.resourceAccessService = resourceAccessService;
this.resourceOfflineImpactService = resourceOfflineImpactService;
this.workflowPluginBindingService = workflowPluginBindingService;
this.workflowPluginSnapshotResolver = workflowPluginSnapshotResolver;
this.workflowScheduleReferenceProviders = workflowScheduleReferenceProviders == null
? List.of()
: List.copyOf(workflowScheduleReferenceProviders);
}
@Override
@@ -186,6 +194,44 @@ public class WorkflowApprovalSubjectHandler extends AbstractAiResourceLifecycleH
if (impact.isHasAgentBindings()) {
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

View File

@@ -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);
}

View File

@@ -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;
}
}