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。