feat: 切换定时任务至分布式调度底座

- 以执行账本和有界 Worker 承载重负载任务与故障接管

- 接入统一调度 Starter 并增加 MySQL 迁移、指标和回归测试
This commit is contained in:
2026-08-31 14:57:08 +08:00
parent 17ef189862
commit 8c174e5c02
66 changed files with 5348 additions and 545 deletions

View File

@@ -2,7 +2,7 @@ package tech.easyflow.ai.service;
import org.springframework.stereotype.Service;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
@@ -15,7 +15,7 @@ import java.util.Objects;
/**
* 工作流使用权限校验服务。
*
* <p>统一封装工作流存在性、租户、启用状态和资源使用权限校验,供页面能力和后台任务复用。</p>
* <p>统一封装工作流存在性、租户、发布快照和资源使用权限校验,供页面能力和后台任务复用。</p>
*/
@Service
public class WorkflowUsageAuthorizationService {
@@ -40,20 +40,20 @@ public class WorkflowUsageAuthorizationService {
}
/**
* 获取当前账号可使用的启用工作流。
* 获取当前账号可使用的已发布工作流视图
*
* @param workflowId 工作流 ID
* @param account 使用工作流的账号
* @param denyMessage 校验失败提示
* @return 可使用的工作流
* @throws BusinessException 工作流不存在、未启用、跨租户或无使用权限时抛出
* @return 可使用的工作流发布视图
* @throws BusinessException 工作流不存在、未发布、缺少发布快照、跨租户或无使用权限时抛出
*/
public Workflow requireUsableWorkflow(
BigInteger workflowId,
LoginAccount account,
String denyMessage) {
String message = denyMessage == null || denyMessage.isBlank()
? "工作流不存在、已禁用或无权使用"
? "工作流不存在、未发布或无权使用"
: denyMessage;
if (workflowId == null || account == null || account.getId() == null
|| account.getTenantId() == null) {
@@ -62,7 +62,9 @@ public class WorkflowUsageAuthorizationService {
Workflow workflow = workflowService.getById(workflowId);
boolean usable = workflow != null
&& Objects.equals(workflow.getTenantId(), account.getTenantId())
&& EnumDataStatus.AVAILABLE.getCode().equals(workflow.getStatus())
&& PublishStatus.PUBLISHED.getCode().equals(workflow.getPublishStatus())
&& workflow.getPublishedSnapshotJson() != null
&& !workflow.getPublishedSnapshotJson().isEmpty()
&& resourceAccessService.canAccess(
account,
CategoryResourceType.WORKFLOW,
@@ -71,6 +73,10 @@ public class WorkflowUsageAuthorizationService {
if (!usable) {
throw new BusinessException(403, 403, message);
}
return workflow;
Workflow published = workflowService.toPublishedView(workflow);
if (published == null) {
throw new BusinessException(403, 403, message);
}
return published;
}
}

View File

@@ -3,7 +3,7 @@ package tech.easyflow.ai.service;
import org.junit.Assert;
import org.junit.Test;
import tech.easyflow.ai.entity.Workflow;
import tech.easyflow.common.constant.enums.EnumDataStatus;
import tech.easyflow.ai.enums.PublishStatus;
import tech.easyflow.common.entity.LoginAccount;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.system.enums.CategoryResourceType;
@@ -11,6 +11,7 @@ import tech.easyflow.system.enums.ResourceAction;
import tech.easyflow.system.service.ResourceAccessService;
import java.math.BigInteger;
import java.util.Map;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -21,14 +22,14 @@ import static org.mockito.Mockito.when;
public class WorkflowUsageAuthorizationServiceTest {
/**
* 验证禁用工作流即使资源权限允许也不能被使用。
* 验证未发布工作流即使资源权限允许也不能被使用。
*/
@Test
public void shouldRejectDisabledWorkflow() {
public void shouldRejectUnpublishedWorkflow() {
BigInteger workflowId = BigInteger.valueOf(101);
WorkflowService workflowService = mock(WorkflowService.class);
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
Workflow workflow = workflow(workflowId, BigInteger.TEN, EnumDataStatus.UNAVAILABLE.getCode());
Workflow workflow = workflow(workflowId, BigInteger.TEN, PublishStatus.DRAFT, Map.of());
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
when(workflowService.getById(workflowId)).thenReturn(workflow);
when(resourceAccessService.canAccess(
@@ -58,7 +59,8 @@ public class WorkflowUsageAuthorizationServiceTest {
Workflow workflow = workflow(
workflowId,
BigInteger.valueOf(20),
EnumDataStatus.AVAILABLE.getCode());
PublishStatus.PUBLISHED,
Map.of("title", "published"));
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
when(workflowService.getById(workflowId)).thenReturn(workflow);
WorkflowUsageAuthorizationService service =
@@ -71,17 +73,50 @@ public class WorkflowUsageAuthorizationServiceTest {
}
/**
* 验证启用、同租户且具有使用权限的工作流可以返回。
* 验证已发布、同租户且具有使用权限的工作流返回发布视图
*/
@Test
public void shouldReturnUsableWorkflow() {
public void shouldReturnPublishedWorkflowView() {
BigInteger workflowId = BigInteger.valueOf(103);
WorkflowService workflowService = mock(WorkflowService.class);
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
Workflow workflow = workflow(
workflowId,
BigInteger.TEN,
EnumDataStatus.AVAILABLE.getCode());
PublishStatus.PUBLISHED,
Map.of("title", "published"));
Workflow published = new Workflow();
published.setId(workflowId);
published.setTitle("发布版");
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
when(workflowService.getById(workflowId)).thenReturn(workflow);
when(resourceAccessService.canAccess(
account,
CategoryResourceType.WORKFLOW,
workflow,
ResourceAction.USE)).thenReturn(true);
when(workflowService.toPublishedView(workflow)).thenReturn(published);
WorkflowUsageAuthorizationService service =
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
Workflow result = service.requireUsableWorkflow(workflowId, account, "工作流不可用");
Assert.assertSame(result, published);
}
/**
* 验证发布状态异常但缺少快照的工作流不可被后台任务使用。
*/
@Test
public void shouldRejectPublishedWorkflowWithoutSnapshot() {
BigInteger workflowId = BigInteger.valueOf(104);
WorkflowService workflowService = mock(WorkflowService.class);
ResourceAccessService resourceAccessService = mock(ResourceAccessService.class);
Workflow workflow = workflow(
workflowId,
BigInteger.TEN,
PublishStatus.PUBLISHED,
Map.of());
LoginAccount account = account(BigInteger.ONE, BigInteger.TEN);
when(workflowService.getById(workflowId)).thenReturn(workflow);
when(resourceAccessService.canAccess(
@@ -92,9 +127,10 @@ public class WorkflowUsageAuthorizationServiceTest {
WorkflowUsageAuthorizationService service =
new WorkflowUsageAuthorizationService(workflowService, resourceAccessService);
Workflow result = service.requireUsableWorkflow(workflowId, account, "工作流不可用");
Assert.assertSame(result, workflow);
Assert.assertThrows(
BusinessException.class,
() -> service.requireUsableWorkflow(workflowId, account, "工作流不可用")
);
}
/**
@@ -102,14 +138,18 @@ public class WorkflowUsageAuthorizationServiceTest {
*
* @param id 工作流 ID
* @param tenantId 租户 ID
* @param status 工作流状态
* @param publishStatus 工作流发布状态
* @param snapshot 工作流发布快照
* @return 工作流
*/
private Workflow workflow(BigInteger id, BigInteger tenantId, Integer status) {
private Workflow workflow(BigInteger id, BigInteger tenantId,
PublishStatus publishStatus,
Map<String, Object> snapshot) {
Workflow workflow = new Workflow();
workflow.setId(id);
workflow.setTenantId(tenantId);
workflow.setStatus(status);
workflow.setPublishStatus(publishStatus.getCode());
workflow.setPublishedSnapshotJson(snapshot);
return workflow;
}