fix: 防止定时任务引用失效工作流
- 保存任务时校验工作流权限与必填参数 - 删除工作流前检查并重新确认定时任务引用
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.math.BigInteger;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统任务表 服务层。
|
||||
@@ -26,4 +27,12 @@ public interface SysJobService extends IService<SysJob> {
|
||||
void startJob(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;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.quartz.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
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.EnumJobStatus;
|
||||
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.mapper.SysJobMapper;
|
||||
import tech.easyflow.job.service.SysJobService;
|
||||
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||
import tech.easyflow.job.util.JobUtil;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
@@ -22,6 +25,7 @@ import java.math.BigInteger;
|
||||
import java.time.Duration;
|
||||
import java.util.Collection;
|
||||
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.job.entity.SysJob;
|
||||
import tech.easyflow.job.job.JobConstant;
|
||||
import tech.easyflow.job.support.SysJobWorkflowReferenceSupport;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
|
||||
@@ -69,7 +71,7 @@ public class JobUtil {
|
||||
public static Object execWorkFlow(SysJob job) {
|
||||
Map<String, Object> jobParams = job.getJobParams();
|
||||
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);
|
||||
|
||||
ChainExecutor executor = SpringContextUtil.getBean(ChainExecutor.class);
|
||||
@@ -79,21 +81,21 @@ public class JobUtil {
|
||||
try {
|
||||
TenantManager.ignoreTenantCondition();
|
||||
|
||||
ChainDefinition chain = executor.getDefinitionRepository().getChainDefinitionById(workflowId);
|
||||
if (chain != null) {
|
||||
if (accountId != null) {
|
||||
// 设置的归属者
|
||||
SysAccount account = accountService.getById(accountId.toString());
|
||||
if (account != null) {
|
||||
params.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
}
|
||||
return executor.execute(workflowId, params);
|
||||
ChainDefinition chain = executor.getDefinitionRepository().getChainDefinitionById(workflowId.toString());
|
||||
if (chain == null) {
|
||||
throw new IllegalStateException("定时任务关联的工作流不存在或已删除,id=" + workflowId);
|
||||
}
|
||||
if (accountId != null) {
|
||||
// 设置的归属者
|
||||
SysAccount account = accountService.getById(accountId.toString());
|
||||
if (account != null) {
|
||||
params.put(Constants.LOGIN_USER_KEY, SaTokenUtil.getLoginAccount());
|
||||
}
|
||||
}
|
||||
return executor.execute(workflowId.toString(), params);
|
||||
} finally {
|
||||
TenantManager.restoreTenantCondition();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Object execute(SysJob job) {
|
||||
|
||||
Reference in New Issue
Block a user