diff --git a/pom.xml b/pom.xml index 9185289ee..295a50e0a 100644 --- a/pom.xml +++ b/pom.xml @@ -62,7 +62,7 @@ 3.3.5 - 2.1.0 + 2.1.1 2.16.3.1 2.6.12 diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwEventWaitController.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwEventWaitController.java index 3ff758728..a7f8f704c 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwEventWaitController.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/FlwEventWaitController.java @@ -27,7 +27,7 @@ import java.util.Map; /** * 定时任务(事件等待记录)控制器。 *

数据来源为引擎事件等待表,默认只查定时类事件;消息类 / 错误类等待需显式放开事件类型范围。 - * 写操作只有一项:对定时启动任务手动补发一次实例——补发不读取、也不推进等待记录, + * 支持定时启动任务手动补发,以及已死事件等待记录复活;补发不推进等待记录, * 定时任务的下次触发时间不受影响。

* * @author may @@ -92,4 +92,19 @@ public class FlwEventWaitController extends BaseController { public R trigger(@RequestBody FlowTimerTriggerBo bo) { return R.ok(flwEventWaitService.manualTrigger(bo)); } + + /** + * 复活已死的事件等待记录,定时启动任务需通过重新发布或挂起后激活恢复。 + * + * @param id 事件等待记录主键 + * @return 操作结果 + */ + @SaCheckPermission("workflow:eventWait:trigger") + @Log(title = "事件等待记录复活", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PostMapping("/{id}/revive") + public R revive(@NotNull(message = "主键不能为空") @PathVariable Long id) { + flwEventWaitService.revive(id); + return R.ok(); + } } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoTimerInspectHandler.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoTimerInspectHandler.java index b3889fa43..2e952b8e1 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoTimerInspectHandler.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/demo/DemoTimerInspectHandler.java @@ -47,8 +47,7 @@ public class DemoTimerInspectHandler implements EventTaskHandler { if (isInspectTimerStart(wait)) { // 这里应该使用业务的id demo使用自动生成id 需要用户自行替换 String businessId = String.valueOf(IdWorker.getId()); - WorkflowContext context = new WorkflowContext(); - context.setHandler(FlowEngine.getFlowConfig().getSystemHandler()); + WorkflowContext context = WorkflowContext.create(); FlowEngine.insService().startByTimer(businessId, wait.getDefinitionId(), wait.getNodeCode(), null, context); log.info("定时启动示例已自动发起审批: businessId={}", businessId); diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwEventWaitService.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwEventWaitService.java index 31b709230..772d023a9 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwEventWaitService.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IFlwEventWaitService.java @@ -9,7 +9,7 @@ import java.util.Map; /** * 定时任务(事件等待记录)服务接口。 - *

数据来自引擎 flow_event_wait,页面提供查询、状态统计与定时启动任务的手动补发。 + *

数据来自引擎 flow_event_wait,提供查询、状态统计、已死记录复活与定时启动任务的手动补发。 * 手动补发只创建一次实例,不读取也不推进等待记录,定时任务的下次触发时间不受影响。

* * @author may @@ -50,4 +50,11 @@ public interface IFlwEventWaitService { * @return 新建流程实例主键 */ Long manualTrigger(FlowTimerTriggerBo bo); + + /** + * 复活已死的事件等待记录,状态与实例有效性由引擎校验。 + * + * @param id 事件等待记录主键 + */ + void revive(Long id); } diff --git a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImpl.java b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImpl.java index d686c77a4..b5ac228ee 100644 --- a/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImpl.java +++ b/ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImpl.java @@ -16,6 +16,7 @@ import org.dromara.warm.flow.core.entity.Instance; import org.dromara.warm.flow.core.enums.EventType; import org.dromara.warm.flow.core.exception.FlowException; import org.dromara.warm.flow.core.utils.page.Page; +import org.dromara.warm.flow.core.workflow.command.ReviveEventCommand; import org.dromara.warm.flow.core.workflow.context.WorkflowContext; import org.dromara.workflow.common.ConditionalOnEnable; import org.dromara.workflow.domain.bo.FlowEventWaitBo; @@ -156,8 +157,7 @@ public class FlwEventWaitServiceImpl implements IFlwEventWaitService { String finalBusinessId = StringUtils.isNotBlank(bo.getBusinessId()) ? bo.getBusinessId().trim() : String.valueOf(IdWorker.getId()); - WorkflowContext context = new WorkflowContext(); - context.setHandler(LoginHelper.getUserIdStr()); + WorkflowContext context = WorkflowContext.create(LoginHelper.getUserIdStr()); try { Instance instance = FlowEngine.insService().startByTimer(finalBusinessId, wait.getDefinitionId() , wait.getNodeCode(), bo.getVariables(), context); @@ -168,6 +168,23 @@ public class FlwEventWaitServiceImpl implements IFlwEventWaitService { } } + /** + * 复活已死的事件等待记录,复用引擎的状态校验、排期重算与乐观锁更新。 + * + * @param id 事件等待记录主键 + */ + @Override + public void revive(Long id) { + ReviveEventCommand command = new ReviveEventCommand(); + command.setEventWaitId(id); + try { + FlowEngine.workflow().reviveEvent(command); + } catch (FlowException e) { + log.warn("事件等待记录复活失败 waitId={} 原因={}", id, e.getMessage()); + throw new ServiceException(e.getMessage()); + } + } + /** * 统计某一状态下的等待记录数量。 * diff --git a/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImplTest.java b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImplTest.java new file mode 100644 index 000000000..939e6595b --- /dev/null +++ b/ruoyi-modules/ruoyi-workflow/src/test/java/org/dromara/workflow/service/impl/FlwEventWaitServiceImplTest.java @@ -0,0 +1,62 @@ +package org.dromara.workflow.service.impl; + +import org.dromara.common.core.exception.ServiceException; +import org.dromara.warm.flow.core.FlowEngine; +import org.dromara.warm.flow.core.constant.EventCons; +import org.dromara.warm.flow.core.entity.EventWait; +import org.dromara.warm.flow.core.enums.EventType; +import org.dromara.warm.flow.core.service.EventWaitService; +import org.dromara.warm.flow.core.service.impl.EventWaitServiceImpl; +import org.dromara.warm.flow.core.workflow.WorkflowServiceImpl; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mockito.MockedStatic; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +class FlwEventWaitServiceImplTest { + + @ParameterizedTest + @ValueSource(strings = {"WAITING", "RETRY", "RUNNING", "CANCELLED", "SUCCESS"}) + void recoveryRejectsRecordsThatAreNotDead(String status) { + ServiceException error = recover(status, EventType.TIMER_CATCH); + assertTrue(error.getMessage().contains("DEAD"), error.getMessage()); + } + + @Test + void deadTimerStartMustBeRebuiltFromDefinition() { + ServiceException error = recover(EventCons.STATUS_DEAD, EventType.TIMER_START); + assertTrue(error.getMessage().contains("定时启动"), error.getMessage()); + } + + @Test + void deadEventCannotResumeWithoutItsInstance() { + ServiceException error = recover(EventCons.STATUS_DEAD, EventType.MESSAGE_CATCH); + assertTrue(error.getMessage().contains("实例"), error.getMessage()); + } + + /** + * 使用实际 2.1.1 门面和事件服务验证恢复约束,避免宿主绕过引擎直接改状态。 + */ + private ServiceException recover(String status, EventType type) { + EventWait wait = mock(EventWait.class); + when(wait.getStatus()).thenReturn(status); + when(wait.getEventType()).thenReturn(type.getValue()); + when(wait.getAttachedTaskId()).thenReturn(null); + when(wait.getInstanceId()).thenReturn(null); + EventWaitService eventService = spy(new EventWaitServiceImpl()); + doReturn(wait).when(eventService).getById(12L); + try (MockedStatic engine = mockStatic(FlowEngine.class)) { + engine.when(FlowEngine::workflow).thenReturn(new WorkflowServiceImpl()); + engine.when(FlowEngine::eventWaitService).thenReturn(eventService); + return assertThrows(ServiceException.class, () -> new FlwEventWaitServiceImpl().revive(12L)); + } + } +} diff --git a/script/sql/ry_workflow.sql b/script/sql/ry_workflow.sql index 8ecb940e7..64b7c18f5 100644 --- a/script/sql/ry_workflow.sql +++ b/script/sql/ry_workflow.sql @@ -387,4 +387,5 @@ INSERT INTO sys_dict_data VALUES (1761600000000000064, 9, '转办', 'transfer', INSERT INTO sys_dict_data VALUES (1761600000000000065, 10, '委派', 'delegate', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '委派'); INSERT INTO sys_dict_data VALUES (1761600000000000066, 11, '加签', 'add_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '加签'); INSERT INTO sys_dict_data VALUES (1761600000000000067, 12, '减签', 'remove_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '减签'); +INSERT INTO sys_dict_data VALUES (1761600000000000068, 13, '已终止', 'termination', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '终止归档留痕'); diff --git a/script/sql/update/update_6.0.0-6.1.0.sql b/script/sql/update/update_6.0.0-6.1.0.sql index 550133f70..e50109a29 100644 --- a/script/sql/update/update_6.0.0-6.1.0.sql +++ b/script/sql/update/update_6.0.0-6.1.0.sql @@ -76,8 +76,8 @@ INSERT INTO sys_dict_data VALUES (1761600000000000066, 11, '加签', 'add_signer INSERT INTO sys_dict_data VALUES (1761600000000000067, 12, '减签', 'remove_signer', 'wf_task_status', '', 'warning', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '减签'); -- 原来的 waiting / termination / transfer / depute / sign / sign_off / timeout 已随上面整组删除: --- 前两个属实例维度(wf_business_status 有),其余属协作维度 cooperate_type(transfer / delegate / --- add_signer / remove_signer),timeout 是 flow_event_wait 的状态,都不是任务状态。 +-- waiting 属实例维度;termination 也是终止归档留痕使用的编码,在下方 2.1.1 适配段补回。 +-- 协作动作使用 transfer / delegate / add_signer / remove_signer,timeout 不属于任务状态。 -- take_back 目前没有写入方,是留给「拿回」功能的占位,别当冗余删掉。 -- 5. 实例状态字典补 restart(引擎 FlowStatus 有,字典缺) @@ -130,3 +130,16 @@ DELETE FROM sys_menu WHERE menu_id IN (1761400000000011901, 1761400000000011902, insert into sys_menu values (1761400000000011901, '定时任务', 1761400000000011630, 3, 'timer', 'workflow/timer/index', '', 'N', 'N', 'C', '0', '0', 'workflow:eventWait:list', 'time', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, '定时任务与事件等待监控菜单'); insert into sys_menu values (1761400000000011902, '定时任务查询', 1761400000000011901, 1, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:query', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, ''); insert into sys_menu values (1761400000000011903, '定时任务手动补发', 1761400000000011901, 2, '#', '', '', 'N', 'Y', 'F', '0', '0', 'workflow:eventWait:trigger', '#', '', '', 1761000000000000103, 1761100000000000001, sysdate(), null, null, ''); + +-- 8. Warm-Flow 2.1.1 适配(无引擎表结构变更) +-- 已执行过本脚本 2.1.0 迁移的数据库只需执行本段,无需重跑前面的迁移。 +-- 补充任务终止归档字典,执行后刷新字典缓存;恢复接口复用页面权限 workflow:eventWait:list。 +INSERT INTO sys_dict_data +SELECT 1761600000000000068, 13, '已终止', 'termination', 'wf_task_status', '', 'danger', 'N', 1761000000000000103, 1761100000000000001, sysdate(), NULL, NULL, '终止归档留痕' +WHERE NOT EXISTS (SELECT 1 FROM sys_dict_data WHERE dict_type = 'wf_task_status' AND dict_value = 'termination'); + +-- 存量数据检查(仅查询,不自动改写审批历史):2.1.0 可能把实例 waiting 写入待办状态。 +SELECT id, instance_id, node_code, flow_status FROM flow_task WHERE flow_status = 'waiting'; +-- 确认上述记录为待审批任务后,可在备份并暂停流程写入的维护窗口执行: +-- UPDATE flow_task SET flow_status = 'pending' WHERE flow_status = 'waiting'; +-- 旧撤回历史 cancel 保留原值及字典,新撤回历史由引擎写 back;终止归档仍写 termination。