- 多出口:FlwTask 增 edge_name 列(sql/20260916),FlwTaskRepository.claimAndFinish
同步落列;FlowTaskService.handle 出度>1 时 edgeName 必填;FlowEngine.continueAdvance
按 edge name 选路(单出口原路径逐字节兼容,未命中不推进);FlowAdvanceService/
FlowNodeAdvanceListener 透传;BranchScope 从 FlowEngine 提取
- DefValidator:多出口出口命名必填且同节点唯一;跨分支连线发布期明确拒绝
- 退回 POST /task/{id}/returnBack:默认退主管线最近已批节点,精确失效当前节点待办,
split 幂等 upsert 使退回重走并行段不撞 flw_branch uk
- 移交 POST /task/{id}/transfer:TRANSFERRED(5)+新建待办,AssigneeResolver.requireUserName
- 或签 ANY 补齐:首个同意失效同节点兄弟待办
- 已办口径/撤回守卫:status in (1,2,4,5);新事件 flow.returned/flow.transferred
- FlowDiagramService:EdgeVo.name/NodeVo.state/DiagramVo.layout,办件图状态计算
- 新 IT:多出口×3、ANY 或签、退回重走并行段、退回/移交/撤回
mvn test 84/84 绿
This commit is contained in:
@@ -243,6 +243,131 @@ class FlowEngineIT extends FlowTestSupport {
|
||||
assertEquals(2, taskRepository.findByInstanceIdOrderByIdAsc(outcome.getInstanceId()).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_approveWithChosenEdge_routesToTarget() {
|
||||
mapDept("km", 101, "甲");
|
||||
mapDept("kf", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t10", "多出口", multiOutletDef("km", "kf"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-10", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
// 选"通过"出口 → 直达 END 完成
|
||||
taskService.handle(SCH, task.getId(), true, "ok", 101, "通过");
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
assertEquals("通过", taskRepository.findById(task.getId()).get().getEdgeName());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_chooseOtherEdge_routesToSecondNode() throws Exception {
|
||||
mapDept("km", 101, "甲");
|
||||
mapDept("kf", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t11", "多出口转向", multiOutletDef("km", "kf"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-11", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
// 选"转乙科"出口 → 到 n2 建新待办
|
||||
taskService.handle(SCH, task.getId(), true, "转给乙科", 101, "转乙科");
|
||||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
List<FlwTask> pending = taskRepository.findByInstanceIdAndStatus(instanceId, 0);
|
||||
assertEquals(1, pending.size());
|
||||
assertEquals("n2", pending.get(0).getNodeId());
|
||||
assertEquals(102, pending.get(0).getAssigneeId());
|
||||
// 乙科通过 → 完成
|
||||
taskService.handle(SCH, pending.get(0).getId(), true, "ok", 102);
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void multiOutlet_multiOutletNode_requiresEdgeName() {
|
||||
mapDept("km", 101, "甲");
|
||||
mapDept("kf", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t12", "多出口必选", multiOutletDef("km", "kf"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-12", 201, "张三");
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(outcome.getInstanceId(), 0).get(0);
|
||||
assertThrows(BizException.class,
|
||||
() -> taskService.handle(SCH, task.getId(), true, "ok", 101, null));
|
||||
assertThrows(BizException.class,
|
||||
() -> taskService.handle(SCH, task.getId(), true, "ok", 101, "不存在的出口"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void anyCountersign_firstApprove_invalidatesSiblings_thenAdvances() {
|
||||
mapDept("ky", 101, "甲");
|
||||
mapDept("ky", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t13", "或签", anyCountersignDef("ky"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-13", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
List<FlwTask> pending = taskRepository.findByInstanceIdAndStatus(instanceId, 0);
|
||||
assertEquals(2, pending.size());
|
||||
// 或签:任一人同意即放行,兄弟待办失效(修复前行为=卡到全员办完)
|
||||
taskService.handle(SCH, pending.get(0).getId(), true, "我先批", 101);
|
||||
FlwInstance instance = instanceRepository.findById(instanceId).get();
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(), instance.getStatus());
|
||||
List<FlwTask> all = taskRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||||
assertEquals(FlwEnums.TaskStatus.APPROVED.getCode(), all.get(0).getStatus());
|
||||
assertEquals(FlwEnums.TaskStatus.INVALIDATED.getCode(), all.get(1).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnBack_fromTailNode_backToHead_thenReTraverseParallel_noUkViolation() {
|
||||
mapDept("km", 103, "主");
|
||||
mapDept("ka", 101, "甲");
|
||||
mapDept("kb", 102, "乙");
|
||||
FlwDef def = saveAndPublishDef("t14", "退回重入", mainlineParallelTailDef("km", "km"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-14", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
// 顺序推进:n1 → 并行双分支 → join → n3(在出口复核节点)
|
||||
taskService.handle(SCH, tasksOf(instanceId, 103).get(0).getId(), true, "ok", 103);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 102).get(0).getId(), true, "ok", 102);
|
||||
FlwTask tail = tasksOf(instanceId, 103).get(0);
|
||||
assertEquals("n3", tail.getNodeId());
|
||||
// 退回至入口(缺省主线最近已办=n1;并行内节点被过滤,不会命中 ta/tb)
|
||||
taskService.returnBack(SCH, tail.getId(), null, "重新审一遍", 103);
|
||||
FlwInstance returned = instanceRepository.findById(instanceId).get();
|
||||
assertEquals("n1", returned.getCurrentNodes());
|
||||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(), returned.getStatus());
|
||||
// 重入并行段:split 幂等 upsert,不撞 uk(instance,branchKey)
|
||||
taskService.handle(SCH, tasksOf(instanceId, 103).get(0).getId(), true, "再入口", 103);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||||
taskService.handle(SCH, tasksOf(instanceId, 102).get(0).getId(), true, "ok", 102);
|
||||
FlwTask n3Again = tasksOf(instanceId, 103).get(0);
|
||||
assertEquals("n3", n3Again.getNodeId());
|
||||
taskService.handle(SCH, n3Again.getId(), true, "复核通过", 103);
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
// 轨迹含 RETURNED 行
|
||||
assertEquals(1, allTasks(instanceId).stream()
|
||||
.filter(t -> FlwEnums.TaskStatus.RETURNED.getCode() == t.getStatus()).count());
|
||||
}
|
||||
|
||||
@Test
|
||||
void returnBack_noMainlineTarget_throws_andTransfer_reassigns() {
|
||||
mapDept("km", 101, "甲");
|
||||
FlwDef def = saveAndPublishDef("t15", "退回目标", linearDef("km", "首审"));
|
||||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-15", 201, "张三");
|
||||
Long instanceId = outcome.getInstanceId();
|
||||
FlwTask task = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
// 首节点无上游已办:缺省退回应明确报错
|
||||
assertThrows(BizException.class, () ->
|
||||
taskService.returnBack(SCH, task.getId(), null, "试试", 101));
|
||||
// 移交:当前任务置 TRANSFERRED,同节点给新办理人建待办
|
||||
taskService.transfer(SCH, task.getId(), 202, "人不在岗", 101);
|
||||
List<FlwTask> handled = allTasks(instanceId);
|
||||
assertEquals(2, handled.size(), "移交后应产生新待办行");
|
||||
assertEquals(FlwEnums.TaskStatus.TRANSFERRED.getCode(), handled.get(0).getStatus());
|
||||
FlwTask reassigned = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||||
assertEquals(202, reassigned.getAssigneeId());
|
||||
assertEquals(101, handled.get(0).getAssigneeId()); // 原办理人留在轨迹
|
||||
// 新办理人通过 → 完成;已退回/移交后提交人不能撤回
|
||||
taskService.handle(SCH, reassigned.getId(), true, "接手批准", 202);
|
||||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||||
instanceRepository.findById(instanceId).get().getStatus());
|
||||
}
|
||||
|
||||
private List<FlwBranch> branchRows(Long instanceId) {
|
||||
return branchRepository.findByInstanceId(instanceId);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user