Skip to content
Merged

Dev #227

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package com.codingapi.flow.node;

import com.codingapi.flow.action.ActionType;
import com.codingapi.flow.context.LoopTriggerTraceContext;
import com.codingapi.flow.error.ErrorThrow;
import com.codingapi.flow.event.FlowRecordDoneEvent;
import com.codingapi.flow.exception.FlowExecutionException;
import com.codingapi.flow.exception.FlowValidationException;
import com.codingapi.flow.form.FlowForm;
Expand All @@ -11,6 +14,7 @@
import com.codingapi.flow.record.FlowRecord;
import com.codingapi.flow.session.FlowSession;
import com.codingapi.flow.strategy.node.MultiOperatorAuditStrategy;
import com.codingapi.springboot.framework.event.EventPusher;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
Expand Down Expand Up @@ -180,18 +184,22 @@ private List<FlowRecord> generateCurrentRecords(FlowSession session, Set<String>
List<IFlowOperator> operators = operatorManager.getOperators();
// 提交人与审批人一致时自动审批(issue #224):提交人流转到该节点且审批人含提交人本人时,
// 过滤掉与提交人一致的操作员,避免本人审批本人提交的节点(AUTO_PASS 自动通过 / MANUAL_PASS 不跳过)。
// 守卫条件(currentOperator == submitOperator)用于区分正常流转与加签等"为他人新增记录"的调用路径:
// 加签(AddAuditAction)通过 updateSession(加签目标) 构造会话,currentOperator 为被加签人而非提交人。
// 守卫条件(currentOperator == submitOperator)用于区分正常流转与加签等"为他人新增记录"的调用路径;
// 动作类型限定为 PASS(issue #226 评审):仅正向通过触发本机制,退回(ReturnAction)、拒绝
// (RejectAction)、加签(AddAuditAction)等路径保持普通记录生成契约——它们的消费方按"返回的
// 即当前节点待办记录"处理(resetAddAudit、先清后存等),自动通过的留痕记录落库/事件副作用会破坏其语义。
if (nodeStrategyManager.isSameOperatorAutoPass()
&& session.getCurrentOperator() != null
&& session.getCurrentOperator().getUserId() == session.getSubmitOperatorId()) {
&& session.getCurrentOperator().getUserId() == session.getSubmitOperatorId()
&& session.getCurrentAction() != null
&& ActionType.PASS.name().equalsIgnoreCase(session.getCurrentAction().type())) {
long submitOperatorId = session.getSubmitOperatorId();
operators = operators.stream()
.filter(operator -> operator.getUserId() != submitOperatorId)
.toList();
// 全部审批人均与提交人一致,当前节点自动通过,继续向后续节点生成记录
// 全部审批人均与提交人一致,当前节点自动通过:生成留痕记录并继续向后续节点生成记录(issue #226)
if (operators.isEmpty()) {
return this.generateNextNodeRecords(session);
return this.autoPassAndGenerateNextNodeRecords(session, records);
}
}
for (int order = 0; order < operators.size(); order++) {
Expand Down Expand Up @@ -229,6 +237,52 @@ private List<FlowRecord> generateCurrentRecords(FlowSession session, Set<String>
}


/**
* 当前节点自动通过(审批人均为提交人本人且配置相同人员自动审批,issue #226)。
*
* <p>自动通过不等于静默跳过:为当前节点生成一条无审批动作的已办记录
* ({@link FlowRecord#autoDone()},与或签/并签遗留待办的自动办结同一语义,
* 展示层依据 {@link FlowRecord#isAutoDone()} 标记 autoSkip),并立即持久化、
* 推送已办事件。该记录随后作为记录链上的前驱,继续向后续节点生成记录,
* 保证流程记录与节点展示中保留当前节点的审批痕迹。
*
* <p>记录不入调用方返回列表:调用方(如 {@code PassAction})在后续节点触发完成后
* 才统一保存返回列表,若本记录走同一保存路径,会以运行中状态覆盖结束节点
* {@code fillNewRecord} 已写入的流程结束状态。
*
* <p>已办事件在本节点下游记录流转成功之后推送:下游生成(取审批人、异常跳转等)是
* 最易抛出异常的环节,先流转后推事件保证抛错时事件尚未派发;推送时刻该记录已被
* 结束节点 {@code over()} 定型,订阅方拿到的载荷即终态。事件相对调用方
* {@code FlowRecordDoneEvent(前驱)} 的先后次序不受控,与
* {@code EndNode#fillNewRecord} 直推 {@code FlowRecordFinishEvent} 属同一引擎既有模式。
*
* @param session 当前会话(currentNode 为自动通过的节点)
* @param records 当前调用已累计的记录集合
* @return 本记录不入列,返回后续节点生成的流程记录,可能为空
*/
private List<FlowRecord> autoPassAndGenerateNextNodeRecords(FlowSession session, List<FlowRecord> records) {
FlowRecord currentRecord = session.getCurrentRecord();
if (currentRecord != null) {
// 被动式环检测:与抄送节点(NotifyNode)同一模式,时间窗口内同一流程实例的
// 同一节点再次自动通过即判定为自动流转环,在任何留痕落库/事件派发之前终止
String traceKey = currentRecord.getProcessId() + ":AUTO_PASS:" + this.getId();
if (LoopTriggerTraceContext.getInstance().trace(traceKey)) {
throw FlowExecutionException.nodeLoopDepthExceeded(session.getWorkflow().getMaxNestDepth());
}
}
FlowRecord autoPassRecord = new FlowRecord(session.updateSession(session.getCurrentOperator()), 0);
autoPassRecord.cleanAction();
autoPassRecord.autoDone();
// 先行持久化:级联直达结束节点时,EndNode.fillNewRecord 按 processId 加载历史并标记完成,
// 必须包含本记录,否则本记录以运行中状态滞留、流程终态不一致
session.getRepositoryHolder().saveRecord(autoPassRecord);
// 本记录作为后续节点记录的前驱,fromId 链经过当前节点
session.setCurrentRecord(autoPassRecord);
records.addAll(this.generateNextNodeRecords(session));
EventPusher.push(new FlowRecordDoneEvent(autoPassRecord, session.isMock()));
return records;
}

/**
* 当前节点自动通过(如审批人均为提交人本人且配置相同人员自动审批),
* 继续向后续节点生成流程记录。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,9 @@ public void setOperationAction(Workflow workflow, FlowRecord flowRecord) {
this.urge = false;
}

if (flowRecord.isDone() && !flowRecord.isFinish()) {
// 自动办结记录(相同人员自动通过 / 或签并签遗留待办的自动已办)未发生真实审批,
// 不暴露撤销/催办入口,与服务端 FlowRevokeService 的 isAutoDone 拦截保持一致(issue #226)
if (flowRecord.isDone() && !flowRecord.isFinish() && !flowRecord.isAutoDone()) {
IFlowNode node = workflow.getFlowNode(flowRecord.getNodeId());
this.revoke = node.strategyManager().isEnableRevoke();
this.urge = workflow.strategyManager().isEnableUrge();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.codingapi.flow.cache.FlowRuntimeScriptLocalCache;

import com.codingapi.flow.action.IFlowAction;
import com.codingapi.flow.action.actions.PassAction;
import com.codingapi.flow.cache.FlowOperatorLocalThreadCache;
import com.codingapi.flow.domain.DelayTask;
import com.codingapi.flow.exception.FlowNotFoundException;
Expand Down Expand Up @@ -63,6 +64,11 @@ public void trigger() {
IFlowOperator submitOperator = flowOperatorGateway.get(flowRecord.getSubmitOperatorId());
IFlowOperator currentOperator = flowOperatorGateway.get(flowRecord.getCurrentOperatorId());
IFlowAction flowAction = currentNode.actionManager().getActionById(flowRecord.getActionId());
// 延迟任务固化的前驱记录可能为相同人员自动通过的留痕记录(无审批动作,actionId=null,issue #226):
// 以该记录所在审批节点的通过动作兜底恢复流转,避免 NPE 使流程永久停滞在延迟节点
if (flowAction == null && flowRecord.isAutoDone()) {
flowAction = currentNode.actionManager().getAction(PassAction.class);
}
FormData formData = new FormData(workflow.getForm());
formData.reset(flowRecord.getFormData());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,11 @@ private void loadWaitingSubProcessNextData(IFlowOperator currentOperator) {
}
IFlowNode sourceNode = this.workflow.getFlowNode(parentRecord.getNodeId());
IFlowAction sourceAction = sourceNode.actionManager().getActionById(parentRecord.getActionId());
// 前驱记录可能为相同人员自动通过的留痕记录(actionId=null,issue #226):
// 预览会话与恢复语义一致,以来源节点通过动作兜底
if (sourceAction == null && parentRecord.isAutoDone()) {
sourceAction = sourceNode.actionManager().getAction(PassAction.class);
}
IFlowNode subProcessNode = this.workflow.getFlowNode(subProcessRecord.getNodeId());
IFlowOperator createOperator = this.loadRecordOperator(parentRecord.getCreateOperatorId());
IFlowOperator submitOperator = this.loadRecordOperator(parentRecord.getSubmitOperatorId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
import com.codingapi.springboot.framework.event.EventPusher;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
* 撤销流程服务
Expand Down Expand Up @@ -54,6 +56,11 @@ public void revoke() {
if (currentRecord.isFinish()) {
throw FlowStateException.recordNotSupportRevoke();
}
// 自动办结记录(无审批动作的已办:相同人员自动通过 / 或签并签遗留待办)从未产生过待办,
// 不存在"撤回自己办理的审批"的语义,不支持撤销(issue #226)
if (currentRecord.isAutoDone()) {
throw FlowStateException.recordNotSupportRevoke();
}
boolean waitingSubProcess = repositoryHolder.getSubProcessRepository()
.findByParentRecordId(currentRecord.getId()).stream()
.anyMatch(SubProcessRecord::isWaiting);
Expand All @@ -78,11 +85,37 @@ public void revoke() {
}

List<FlowRecord> afterRecords = flowRecordService.findFlowRecordAfterRecords(currentRecord.getProcessId(), currentRecord.getId());
// 退回下级记录, 如果下级记录都完成则不允许退回
// 退回下级记录, 如果下级记录都完成则不允许退回。
// 计算有效直接后继时,自动办结记录(isAutoDone:相同人员自动通过的留痕记录)视为透明节点,
// 沿 fromId 链向下穿透到真实后继——否则自动通过留痕会让"下级已办"误判成立,
// 在更下游仍有真实待办时永久阻断撤回(issue #226)。
// 仅当透明记录确有下游后继时才穿透,或签/并签遗留的 autoDone 记录无后继,保持原判。
if (revokeStrategy.isRemoveNext()) {
List<FlowRecord> nextRecords = afterRecords.stream()
.filter(flowRecord -> flowRecord.getFromId() == currentRecord.getId())
.toList();
List<FlowRecord> nextRecords = new ArrayList<>();
Set<Long> frontier = new HashSet<>();
frontier.add(currentRecord.getId());
while (!frontier.isEmpty()) {
Set<Long> currentFrontier = frontier;
List<FlowRecord> directRecords = afterRecords.stream()
.filter(flowRecord -> currentFrontier.contains(flowRecord.getFromId()))
.toList();
boolean hasTodo = false;
Set<Long> nextFrontier = new HashSet<>();
for (FlowRecord directRecord : directRecords) {
if (directRecord.isAutoDone()) {
nextFrontier.add(directRecord.getId());
} else {
nextRecords.add(directRecord);
if (directRecord.isTodo()) {
hasTodo = true;
}
}
}
if (hasTodo) {
break;
}
frontier = nextFrontier;
}
boolean nextRecordDone = true;
for (FlowRecord nextRecord : nextRecords) {
if (nextRecord.isTodo()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.codingapi.flow.action.BaseAction;
import com.codingapi.flow.action.IFlowAction;
import com.codingapi.flow.action.actions.PassAction;
import com.codingapi.flow.cache.FlowRuntimeScriptLocalCache;
import com.codingapi.flow.domain.SubProcessContext;
import com.codingapi.flow.domain.SubProcessRecord;
Expand Down Expand Up @@ -101,6 +102,11 @@ private FlowSession createParentSession(SubProcessRecord subProcessRecord) {
Workflow workflow = workflowRuntime.toWorkflow();
IFlowNode sourceNode = workflow.getFlowNode(parentRecord.getNodeId());
IFlowAction action = sourceNode.actionManager().getActionById(parentRecord.getActionId());
// 前驱记录可能为相同人员自动通过的留痕记录(无审批动作,actionId=null,issue #226):
// 恢复下游以来源节点的通过动作兜底,避免空动作会话导致 NPE 使主流程永久停滞
if (action == null && parentRecord.isAutoDone()) {
action = sourceNode.actionManager().getAction(PassAction.class);
}
IFlowOperator currentOperator = repositoryHolder.getOperatorById(parentRecord.getCurrentOperatorId());
IFlowOperator createdOperator = repositoryHolder.getOperatorById(parentRecord.getCreateOperatorId());
IFlowOperator submitOperator = repositoryHolder.getOperatorById(parentRecord.getSubmitOperatorId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@
* <li>MANUAL_PASS —— 即使提交人与审批人相同,仍需手动审批。</li>
* </ul>
*
* <p>场景一:B 节点审批人仅为 a(与提交人一致)+ 相同人员自动审批,a 提交后应自动跳过
* B 节点,直达 C 节点由 c 审批。
* <p>场景一:B 节点审批人仅为 a(与提交人一致)+ 相同人员自动审批,a 提交后 B 节点自动通过
* (a 无待办,但保留一条自动通过的已办记录,issue #226),直达 C 节点由 c 审批。
*
* <p>场景二:B 节点审批人为 a、b(提交人 a 位列首位)+ 相同人员自动审批 + 依次审批,
* a 提交后应自动跳过 a 本人,B 节点直接转交 b 审批。
Expand Down Expand Up @@ -163,15 +163,15 @@ private List<FlowRecord> processRecords(FlowRecord anyRecord) {
return factory.flowRecordRepository.findProcessRecords(anyRecord.getProcessId());
}

// ==================== 场景一:单审批人 = 提交人,自动通过,跳过 B 节点 ====================
// ==================== 场景一:单审批人 = 提交人,自动通过(保留已办记录,issue #226) ====================

/**
* 场景一:B 节点审批人仅为 a(与发起人一致)+ 相同人员自动审批(AUTO_PASS)。
* <p>a 提交后应自动跳过 B 节点(B 节点不产生记录、a 无待办),
* <p>a 提交后 B 节点自动通过:a 无待办,但 B 节点保留一条无审批动作的自动通过已办记录(issue #226),
* 直接流转到 C 节点由 c 审批。
*/
@Test
void autoPassShouldSkipBNodeWhenOnlyApproverIsInitiator() {
void autoPassShouldKeepDoneRecordAndSkipToDoWhenOnlyApproverIsInitiator() {
User a = new User(A, "a");
User c = new User(C, "c");
registerUsers(a, c);
Expand All @@ -186,23 +186,27 @@ void autoPassShouldSkipBNodeWhenOnlyApproverIsInitiator() {
Workflow workflow = saveWorkflow(a, startNode, bNode, cNode, endNode);
submitStart(workflow, startNode, a, data());

// a 本人不应收到 B 节点待办(相同人员自动通过,跳过 B 节点
// a 本人不应收到 B 节点待办(相同人员自动通过)
assertNoTodo(a);

// 直接流转到 C 节点:c 收到待办,且待办节点为 C 审批节点
FlowRecord cTodo = todoOf(c);
assertEquals(cNode.getId(), cTodo.getNodeId(), "a 提交后应直达 C 节点,待办应落在 C 审批节点");

// 流程记录:开始 + C 共 2 条,B 节点未产生任何记录(自动通过
// 流程记录:开始 + B 自动通过 + C 共 3 条(issue #226:自动通过保留已办记录留痕
List<FlowRecord> records = processRecords(cTodo);
assertEquals(2, records.size(), "B 节点自动通过,流程记录应仅含 开始、C 两条");
assertEquals(0, records.stream().filter(r -> bNode.getId().equals(r.getNodeId())).count(),
"B 节点不应产生流程记录");
assertEquals(3, records.size(), "B 节点自动通过,流程记录应含 开始、B、C 三条");
List<FlowRecord> bRecords = records.stream()
.filter(r -> bNode.getId().equals(r.getNodeId()))
.toList();
assertEquals(1, bRecords.size(), "B 节点应保留一条自动通过的流程记录");
assertTrue(bRecords.get(0).isDone(), "B 节点自动通过记录应为已办状态");
assertTrue(bRecords.get(0).isAutoDone(), "B 节点记录应为无审批动作的自动办结(autoSkip)");

// c 审批后流程正常结束
pass(cTodo, cNode, c, data());
records = processRecords(cTodo);
assertEquals(2, records.size());
assertEquals(3, records.size());
assertTrue(records.stream().allMatch(FlowRecord::isFinish), "流程结束后全部记录应为完成状态");
assertNoTodo(c);
}
Expand Down
Loading
Loading