fix(flow): 修正拒绝结束状态

(cherry picked from commit 5158476c3ff652f6a97256497074ba9cbd448ff5)
This commit is contained in:
PIGCLOUD committed 2026-09-22 21:43:38 +08:00
1 parent 269d7fe5e4
commit 4023b142d1
8 files changed
+237 -10

No files matched your search

@@ -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";
/**
* 流程结束原因:拒绝
*/
@@ -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}
* <p>
* 枚举值:0(未开始)、1(进行中)、2(已结束)、9(已终止)。
* </p>
*/
private Integer status;
/** 完成原因 */
/**
* 完成原因,取值定义见 {@link ProcessInstanceConstant}
* <p>
* 取值:0(拒绝)、1(通过)、3(撤回)、9(终止)。
* </p>
*/
private String finishReason;
}
@@ -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 ServiceImpl<BpmOaLeaveMapper, BpmOaLe
@Override
public void handleStatusEvent(ProcessInstanceParamDto paramDto) {
this.update(Wrappers.<BpmOaLeaveEntity>lambdaUpdate()
.set(BpmOaLeaveEntity::getLeaveStatus, paramDto.getStatus())
.set(BpmOaLeaveEntity::getLeaveStatus, Convert.toInt(paramDto.getFinishReason(), null))
.eq(BpmOaLeaveEntity::getProcessInstanceId, paramDto.getProcessInstanceId()));
}
@@ -336,14 +336,16 @@ public class ProcessInstanceServiceImpl implements IProcessInstanceService {
@Override
public R end(ProcessInstanceParamDto processInstanceParamDto) {
// 从参数中提取审批条件,用于判断流程结束原因(通过/拒绝)
Optional<Boolean> objectOptional = processInstanceParamDto.getParamMap().keySet().stream().filter(key -> key.contains("approve_condition")).reduce((first, second) -> second).map(key -> MapUtil.getBool(processInstanceParamDto.getParamMap(), key, true));
// 拒绝结束时会写入明确的结束原因;正常流转到结束节点时默认为通过
Map<String, Object> 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<ProcessInstanceRecord> lambdaUpdateChainWrapper = processInstanceRecordService.lambdaUpdate().set(ProcessInstanceRecord::getEndTime, new Date()).set(ProcessInstanceRecord::getStatus, NodeStatusEnum.YJS.getCode());
// 如果有审批结果,设置结束原因
objectOptional.ifPresent(aBoolean -> lambdaUpdateChainWrapper.set(ProcessInstanceRecord::getFinishReason, aBoolean));
LambdaUpdateChainWrapper<ProcessInstanceRecord> 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());
@@ -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,工作流将沿其正常路径继续
}
}
}
@@ -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<Wrapper<BpmOaLeaveEntity>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
verify(service).update(wrapperCaptor.capture());
LambdaUpdateWrapper<BpmOaLeaveEntity> updateWrapper = (LambdaUpdateWrapper<BpmOaLeaveEntity>) wrapperCaptor
.getValue();
updateWrapper.getSqlSet();
return (Integer) updateWrapper.getParamNameValuePairs().values().iterator().next();
}
}
@@ -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<String, Object> 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<String, Object> variables = new LinkedHashMap<>();
variables.put("approve_approve_condition", true);
assertThat(captureFinishReason(variables)).isEqualTo(ProcessInstanceConstant.FINISH_REASON_PASS);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private Object captureFinishReason(Map<String, Object> variables) {
IProcessInstanceRecordService recordService = mock(IProcessInstanceRecordService.class);
LambdaUpdateChainWrapper<ProcessInstanceRecord> 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<Object> values = ArgumentCaptor.forClass(Object.class);
verify(update, times(3)).set(any(), values.capture());
return values.getAllValues().get(2);
}
}
@@ -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();
}
}