From 4023b142d1c58d8fa5f29aabb62828a67805f540 Mon Sep 17 00:00:00 2001 From: PIGCLOUD Date: Tue, 11 Aug 2026 15:58:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(flow):=20=E4=BF=AE=E6=AD=A3=E6=8B=92?= =?UTF-8?q?=E7=BB=9D=E7=BB=93=E6=9D=9F=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 5158476c3ff652f6a97256497074ba9cbd448ff5) --- .../constant/ProcessInstanceConstant.java | 5 ++ .../flow/dto/ProcessInstanceParamDto.java | 16 +++- .../service/impl/BpmOaLeaveServiceImpl.java | 3 +- .../impl/ProcessInstanceServiceImpl.java | 14 ++-- .../support/tasks/ApproveServiceTask.java | 5 +- .../impl/BpmOaLeaveServiceImplTests.java | 61 +++++++++++++++ .../impl/ProcessInstanceServiceImplTests.java | 75 +++++++++++++++++++ .../tasks/ApproveServiceTaskTests.java | 68 +++++++++++++++++ 8 files changed, 237 insertions(+), 10 deletions(-) create mode 100644 pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImplTests.java create mode 100644 pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java create mode 100644 pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java index 980f97f24..240bac4b7 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java @@ -60,6 +60,11 @@ public class ProcessInstanceConstant { */ public static final String REJECT_TO_STARTER_VAR = "rejectToStarter"; + /** + * 流程变量:明确的流程结束原因 + */ + public static final String FINISH_REASON_VAR = "_pigx_finish_reason"; + /** * 流程结束原因:拒绝 */ diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessInstanceParamDto.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessInstanceParamDto.java index 6f4797751..e0609612d 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessInstanceParamDto.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/dto/ProcessInstanceParamDto.java @@ -1,5 +1,7 @@ package com.pig4cloud.pigx.flow.dto; +import com.pig4cloud.pigx.flow.constant.NodeStatusEnum; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; import lombok.Data; import java.util.HashMap; @@ -49,10 +51,20 @@ public class ProcessInstanceParamDto { */ private String processInstanceId; - /** 状态 */ + /** + * 状态,对应 {@link NodeStatusEnum} + *

+ * 枚举值:0(未开始)、1(进行中)、2(已结束)、9(已终止)。 + *

+ */ private Integer status; - /** 完成原因 */ + /** + * 完成原因,取值定义见 {@link ProcessInstanceConstant} + *

+ * 取值:0(拒绝)、1(通过)、3(撤回)、9(终止)。 + *

+ */ private String finishReason; } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImpl.java index d3713caaa..df94c2624 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImpl.java @@ -1,6 +1,7 @@ package com.pig4cloud.pigx.flow.service.impl; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Dict; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; @@ -48,7 +49,7 @@ public class BpmOaLeaveServiceImpl extends ServiceImpllambdaUpdate() - .set(BpmOaLeaveEntity::getLeaveStatus, paramDto.getStatus()) + .set(BpmOaLeaveEntity::getLeaveStatus, Convert.toInt(paramDto.getFinishReason(), null)) .eq(BpmOaLeaveEntity::getProcessInstanceId, paramDto.getProcessInstanceId())); } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java index d9a3c917d..f95a124a9 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java @@ -336,14 +336,16 @@ public class ProcessInstanceServiceImpl implements IProcessInstanceService { @Override public R end(ProcessInstanceParamDto processInstanceParamDto) { - // 从参数中提取审批条件,用于判断流程结束原因(通过/拒绝) - Optional objectOptional = processInstanceParamDto.getParamMap().keySet().stream().filter(key -> key.contains("approve_condition")).reduce((first, second) -> second).map(key -> MapUtil.getBool(processInstanceParamDto.getParamMap(), key, true)); + // 拒绝结束时会写入明确的结束原因;正常流转到结束节点时默认为通过 + Map paramMap = Optional.ofNullable(processInstanceParamDto.getParamMap()) + .orElseGet(Collections::emptyMap); + String finishReason = MapUtil.getStr(paramMap, ProcessInstanceConstant.FINISH_REASON_VAR); + if (StrUtil.isBlank(finishReason)) { + finishReason = ProcessInstanceConstant.FINISH_REASON_PASS; + } // 构建更新条件 - LambdaUpdateChainWrapper lambdaUpdateChainWrapper = processInstanceRecordService.lambdaUpdate().set(ProcessInstanceRecord::getEndTime, new Date()).set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()); - - // 如果有审批结果,设置结束原因 - objectOptional.ifPresent(aBoolean -> lambdaUpdateChainWrapper.set(ProcessInstanceRecord::getFinishReason, aBoolean)); + LambdaUpdateChainWrapper lambdaUpdateChainWrapper = processInstanceRecordService.lambdaUpdate().set(ProcessInstanceRecord::getEndTime, new Date()).set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode()).set(ProcessInstanceRecord::getFinishReason, finishReason); // 执行更新 lambdaUpdateChainWrapper.eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceParamDto.getProcessInstanceId()).update(new ProcessInstanceRecord()); diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java index 7a0a22db1..2eada9c80 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java @@ -2,6 +2,7 @@ package com.pig4cloud.pigx.flow.support.tasks; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; import com.pig4cloud.pigx.flow.dto.Node; import com.pig4cloud.pigx.flow.dto.Refuse; import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; @@ -112,10 +113,12 @@ public class ApproveServiceTask implements JavaDelegate { } else { // 默认行为:重定向到结束节点 + runtimeService.setVariable(processInstanceId, ProcessInstanceConstant.FINISH_REASON_VAR, + ProcessInstanceConstant.FINISH_REASON_REFUSE); stateChangeBuilder.moveActivityIdTo(currentActivityId, "end").changeState(); } } // 如果未设置refuseConfig,工作流将沿其正常路径继续 } -} \ No newline at end of file +} diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImplTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImplTests.java new file mode 100644 index 000000000..e8c902d60 --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/BpmOaLeaveServiceImplTests.java @@ -0,0 +1,61 @@ +package com.pig4cloud.pigx.flow.service.impl; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.pig4cloud.pigx.flow.api.feign.RemoteFlowApiFlowService; +import com.pig4cloud.pigx.flow.dto.ProcessInstanceParamDto; +import com.pig4cloud.pigx.flow.entity.BpmOaLeaveEntity; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.mockito.ArgumentCaptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +class BpmOaLeaveServiceImplTests { + + @BeforeAll + static void initializeTableInfo() { + TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"), + BpmOaLeaveEntity.class); + } + + @ParameterizedTest + @CsvSource({ "0, 0", "1, 1", "3, 3", "9, 9" }) + void statusEventStoresFinishReasonAsLeaveStatus(String finishReason, int expectedStatus) { + assertThat(captureLeaveStatus(finishReason)).isEqualTo(expectedStatus); + } + + @Test + void statusEventClearsLeaveStatusBeforeProcessFinishes() { + assertThat(captureLeaveStatus(null)).isNull(); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Integer captureLeaveStatus(String finishReason) { + BpmOaLeaveServiceImpl service = spy(new BpmOaLeaveServiceImpl(mock(RemoteFlowApiFlowService.class))); + doReturn(true).when(service).update(any(Wrapper.class)); + ProcessInstanceParamDto paramDto = new ProcessInstanceParamDto(); + paramDto.setProcessInstanceId("process-7598"); + paramDto.setFinishReason(finishReason); + + service.handleStatusEvent(paramDto); + + ArgumentCaptor> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class); + verify(service).update(wrapperCaptor.capture()); + LambdaUpdateWrapper updateWrapper = (LambdaUpdateWrapper) wrapperCaptor + .getValue(); + updateWrapper.getSqlSet(); + return (Integer) updateWrapper.getParamNameValuePairs().values().iterator().next(); + } + +} diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java new file mode 100644 index 000000000..aab4f9457 --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImplTests.java @@ -0,0 +1,75 @@ +package com.pig4cloud.pigx.flow.service.impl; + +import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.admin.api.feign.RemoteUserService; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; +import com.pig4cloud.pigx.flow.dto.ProcessInstanceParamDto; +import com.pig4cloud.pigx.flow.entity.ProcessInstanceRecord; +import com.pig4cloud.pigx.flow.service.IProcessCopyService; +import com.pig4cloud.pigx.flow.service.IProcessInstanceRecordService; +import com.pig4cloud.pigx.flow.service.IProcessNodeRecordAssignUserService; +import com.pig4cloud.pigx.flow.service.IProcessNodeRecordService; +import com.pig4cloud.pigx.flow.service.IProcessService; +import org.flowable.engine.HistoryService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.RETURNS_SELF; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ProcessInstanceServiceImplTests { + + @Test + void endPersistsExplicitRefusalWhenHistoricalApprovalVariableAppearsLast() { + Map variables = new LinkedHashMap<>(); + variables.put("current_approve_condition", false); + variables.put("previous_approve_condition", true); + variables.put(ProcessInstanceConstant.FINISH_REASON_VAR, ProcessInstanceConstant.FINISH_REASON_REFUSE); + + assertThat(captureFinishReason(variables)).isEqualTo(ProcessInstanceConstant.FINISH_REASON_REFUSE); + } + + @Test + void endDefaultsToPassWhenNoExplicitFinishReasonExists() { + Map variables = new LinkedHashMap<>(); + variables.put("approve_approve_condition", true); + + assertThat(captureFinishReason(variables)).isEqualTo(ProcessInstanceConstant.FINISH_REASON_PASS); + } + + @SuppressWarnings({ "rawtypes", "unchecked" }) + private Object captureFinishReason(Map variables) { + IProcessInstanceRecordService recordService = mock(IProcessInstanceRecordService.class); + LambdaUpdateChainWrapper update = mock(LambdaUpdateChainWrapper.class, RETURNS_SELF); + when(recordService.lambdaUpdate()).thenReturn(update); + doReturn(update).when(update).set(any(), any()); + doReturn(update).when(update).eq(any(), any()); + + ProcessInstanceServiceImpl service = new ProcessInstanceServiceImpl(mock(RemoteUserService.class), + mock(TaskService.class), mock(RuntimeService.class), mock(HistoryService.class), recordService, + mock(IProcessCopyService.class), mock(IProcessService.class), mock(IProcessNodeRecordService.class), + mock(IProcessNodeRecordAssignUserService.class), new ObjectMapper()); + ProcessInstanceParamDto request = new ProcessInstanceParamDto(); + request.setProcessInstanceId("process-7598"); + request.setParamMap(variables); + + service.end(request); + + ArgumentCaptor values = ArgumentCaptor.forClass(Object.class); + verify(update, times(3)).set(any(), values.capture()); + return values.getAllValues().get(2); + } + +} diff --git a/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java new file mode 100644 index 000000000..ea79746b0 --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/test/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTaskTests.java @@ -0,0 +1,68 @@ +package com.pig4cloud.pigx.flow.support.tasks; + +import cn.hutool.extra.spring.SpringUtil; +import com.pig4cloud.pigx.common.core.util.R; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; +import com.pig4cloud.pigx.flow.dto.Node; +import com.pig4cloud.pigx.flow.dto.Refuse; +import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl; +import org.flowable.engine.runtime.ChangeActivityStateBuilder; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericApplicationContext; + +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class ApproveServiceTaskTests { + + @Test + void directRejectionMarksFinishReasonBeforeMovingToEnd() { + ExecutionEntityImpl execution = mock(ExecutionEntityImpl.class); + when(execution.getActivityId()).thenReturn("approve_service_task_approve-1"); + when(execution.getProcessDefinitionKey()).thenReturn("leave"); + when(execution.getProcessInstanceId()).thenReturn("process-7598"); + when(execution.getVariable("approve-1_approve_condition", Boolean.class)).thenReturn(false); + + RuntimeService runtimeService = mock(RuntimeService.class); + ChangeActivityStateBuilder stateChangeBuilder = mock(ChangeActivityStateBuilder.class); + when(runtimeService.createChangeActivityStateBuilder()).thenReturn(stateChangeBuilder); + when(stateChangeBuilder.processInstanceId("process-7598")).thenReturn(stateChangeBuilder); + when(stateChangeBuilder.moveActivityIdTo("approve_service_task_approve-1", "end")) + .thenReturn(stateChangeBuilder); + + Refuse refuse = new Refuse(); + refuse.setHandler("TO_END"); + Node node = new Node(); + node.setRefuse(refuse); + IProcessNodeDataService nodeDataService = mock(IProcessNodeDataService.class); + when(nodeDataService.getNodeData("leave", "approve-1")).thenReturn(R.ok(node)); + + ApplicationContext previousContext = SpringUtil.getApplicationContext(); + GenericApplicationContext context = new GenericApplicationContext(); + context.getBeanFactory().registerSingleton("runtimeService", runtimeService); + context.getBeanFactory().registerSingleton("processNodeDataService", nodeDataService); + context.refresh(); + new SpringUtil().setApplicationContext(context); + try { + new ApproveServiceTask().execute(execution); + } + finally { + new SpringUtil().setApplicationContext(previousContext); + context.close(); + } + + InOrder inOrder = inOrder(runtimeService, stateChangeBuilder); + inOrder.verify(runtimeService).setVariable("process-7598", ProcessInstanceConstant.FINISH_REASON_VAR, + ProcessInstanceConstant.FINISH_REASON_REFUSE); + inOrder.verify(stateChangeBuilder).moveActivityIdTo("approve_service_task_approve-1", "end"); + inOrder.verify(stateChangeBuilder).changeState(); + verify(runtimeService).createChangeActivityStateBuilder(); + } + +}