build / build-push (push) Successful in 54s
- deptKey=本地自增 id;source_group_id 幂等(一键拉取=初始化/刷新语义,不重复建组);不做存量迁移 - 四处读源全部切新表:AssigneeResolver(宽容口径:deptKey 不存在/停用返回空,图示/轨迹不炸)、 enabledDeptKeys 发布校验白名单、DeptLabelSupport 唯一出口(图示科室视图+轨迹富化)、编辑器 deptList 聚合 - /admin/dept/* 全套端点(CRUD/成员/拉取预览导入);旧 /map/* 映射 CRUD 端点删除 (保留 /map/lookupUser 选人检索与 /user/teacherTree);flw_assignee_map 实体/表保留不删 - IT 种子切源(FlowTestSupport tag→deptKey 注册表,用例主体零改动);全量 191 绿;E2E 22/22 (local-debug/four-topics-e-e2e.py,含切源后果验证与生产盘点 SQL)
786 lines
44 KiB
Java
786 lines
44 KiB
Java
package com.yunhai.flowcenter.engine;
|
||
|
||
import com.yunhai.flowcenter.entity.FlwBranch;
|
||
import com.yunhai.flowcenter.entity.FlwDef;
|
||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||
import com.yunhai.flowcenter.entity.FlwTask;
|
||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||
import com.yunhai.flowcenter.service.FlowInstanceService;
|
||
import com.yunhai.flowcenter.service.FlowTaskService;
|
||
import com.yunhai.flowcenter.support.FlowTestSupport;
|
||
import com.yunhai.platform.apistd.BizException;
|
||
import org.junit.jupiter.api.BeforeEach;
|
||
import org.junit.jupiter.api.Test;
|
||
import org.springframework.beans.factory.annotation.Autowired;
|
||
import org.springframework.boot.test.context.SpringBootTest;
|
||
import org.springframework.boot.test.context.TestConfiguration;
|
||
import org.springframework.context.annotation.Bean;
|
||
import org.springframework.context.annotation.Primary;
|
||
import org.springframework.test.context.ActiveProfiles;
|
||
|
||
import java.util.List;
|
||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||
|
||
/**
|
||
* 引擎闭环 IT(真实 MySQL bsch_flowcenter_test;不开测试事务——AFTER_COMMIT 推进
|
||
* 监听器依赖真实提交):激活/ALL 会签合并/并行分支/拒绝/撤回/催办/R4 重启重提/乐观锁。
|
||
* 注意:本类必须单类独跑(平台 IT 判据,与他类合跑共享库必假红)。
|
||
*/
|
||
@SpringBootTest
|
||
@ActiveProfiles("test")
|
||
class FlowEngineIT extends FlowTestSupport {
|
||
|
||
@Autowired
|
||
private FlowInstanceService instanceService;
|
||
@Autowired
|
||
private FlowTaskService taskService;
|
||
@Autowired
|
||
private com.yunhai.flowcenter.service.FlowQueryService queryService;
|
||
@Autowired
|
||
private com.yunhai.flowcenter.service.FlowRouterAdminService routerAdminService;
|
||
|
||
@TestConfiguration
|
||
static class StubConfig {
|
||
@Bean
|
||
@Primary
|
||
public com.yunhai.flowcenter.stub.StubUserClient stubUserClient() {
|
||
return new com.yunhai.flowcenter.stub.StubUserClient();
|
||
}
|
||
|
||
/**
|
||
* 路由判断桩(2026-09-24 CONDITION):替换真实 RouteJudgeClient(不出网),
|
||
* 每次调用取 StubJudge 当前编程值——出口名或抛错。真实客户端的重试/寻址逻辑
|
||
* 由 RouteJudgeClientTest 覆盖;本类聚焦引擎行为(选路/挂起/重跑/重判)。
|
||
*/
|
||
@Bean
|
||
@Primary
|
||
public RouteJudgeClient stubRouteJudgeClient() {
|
||
return new RouteJudgeClient(null, new com.fasterxml.jackson.databind.ObjectMapper()) {
|
||
@Override
|
||
public RouteJudgeClient.JudgeResult call(com.yunhai.flowcenter.entity.FlwRouter router,
|
||
RouteJudgeClient.JudgeBody body) {
|
||
return StubJudge.next();
|
||
}
|
||
};
|
||
}
|
||
}
|
||
|
||
/** 判断桩的编程值:outlet=命中出口;failure=非空时抛出(模拟服务不可用) */
|
||
static final class StubJudge {
|
||
static volatile String outlet = "甲线";
|
||
static volatile RuntimeException failure;
|
||
|
||
static RouteJudgeClient.JudgeResult next() {
|
||
if (failure != null) {
|
||
throw failure;
|
||
}
|
||
return new RouteJudgeClient.JudgeResult(outlet, "{\"outlet\":\"" + outlet + "\"}");
|
||
}
|
||
|
||
static void reset(String defaultOutlet) {
|
||
outlet = defaultOutlet;
|
||
failure = null;
|
||
}
|
||
}
|
||
|
||
@BeforeEach
|
||
void setUp() {
|
||
cleanDb();
|
||
StubJudge.reset("甲线");
|
||
}
|
||
|
||
private FlowInstanceService.ApplyOutcome apply(String flowGuid, String bizId,
|
||
Integer submitterId, String submitterName) {
|
||
FlwDef def = defRepository.findFirstBySchIdAndFlowGuidAndStatusOrderByVersionDesc(
|
||
SCH, flowGuid, 1).orElseThrow(IllegalStateException::new);
|
||
FlowInstanceService.ApplyCommand cmd = new FlowInstanceService.ApplyCommand(
|
||
SCH, def, "svc_item", bizId, "张三的测试办件", "http://biz/1", "{\"k\":\"v\"}",
|
||
submitterId, submitterName, null);
|
||
return instanceService.apply(cmd);
|
||
}
|
||
|
||
@Test
|
||
void apply_createsInstanceAndCountersignTasks_allApprove_completes() {
|
||
mapDept("ybb", 101, "甲");
|
||
mapDept("ybb", 102, "乙");
|
||
FlwDef def = saveAndPublishDef("t1", "会签流程", linearDef("ybb", "联合审核"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-1", 201, "张三");
|
||
assertFalse(outcome.isDuplicated());
|
||
|
||
FlwInstance instance = instanceRepository.findById(outcome.getInstanceId()).orElse(null);
|
||
assertNotNull(instance);
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(), instance.getStatus());
|
||
// ALL 会签:每办理人一条任务
|
||
List<FlwTask> tasks = taskRepository.findByInstanceIdAndStatus(instance.getId(), 0);
|
||
assertEquals(2, tasks.size());
|
||
assertEquals("n1", instance.getCurrentNodes());
|
||
|
||
// 第一人同意:节点未清零,仍在途
|
||
taskService.handle(SCH, tasks.get(0).getId(), true, "同意", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instance.getId()).get().getStatus());
|
||
// 第二人同意:全部清零 → 推进 END → 办件完成
|
||
taskService.handle(SCH, tasks.get(1).getId(), true, null, 102);
|
||
FlwInstance after = instanceRepository.findById(instance.getId()).get();
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(), after.getStatus());
|
||
assertNotNull(after.getFinishTime());
|
||
assertTrue(after.getVersion() >= 2, "完成时乐观锁版本应递增");
|
||
}
|
||
|
||
@Test
|
||
void anyReject_endsRejected_andInvalidatesOthers() {
|
||
mapDept("ybb", 101, "甲");
|
||
mapDept("ybb", 102, "乙");
|
||
FlwDef def = saveAndPublishDef("t2", "会签拒绝", linearDef("ybb", "联合审核"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-2", 201, "张三");
|
||
List<FlwTask> tasks = taskRepository.findByInstanceIdAndStatus(outcome.getInstanceId(), 0);
|
||
|
||
taskService.handle(SCH, tasks.get(0).getId(), false, "材料不全", 101);
|
||
FlwInstance instance = instanceRepository.findById(outcome.getInstanceId()).get();
|
||
assertEquals(FlwEnums.InstanceStatus.REJECTED.getCode(), instance.getStatus());
|
||
assertNotNull(instance.getFinishTime());
|
||
// 同批另一待办已失效
|
||
List<FlwTask> all = allTasks(instance.getId());
|
||
assertEquals(FlwEnums.TaskStatus.REJECTED.getCode(), all.get(0).getStatus());
|
||
assertEquals(FlwEnums.TaskStatus.INVALIDATED.getCode(), all.get(1).getStatus());
|
||
}
|
||
|
||
@Test
|
||
void reject_requiresOpinion() {
|
||
mapDept("ybb", 101, "甲");
|
||
FlwDef def = saveAndPublishDef("t3", "意见必填", linearDef("ybb", "审核"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-3", 201, "张三");
|
||
FlwTask task = taskRepository.findByInstanceIdAndStatus(outcome.getInstanceId(), 0).get(0);
|
||
assertThrows(BizException.class, () -> taskService.handle(SCH, task.getId(), false, " ", 101));
|
||
}
|
||
|
||
@Test
|
||
void withdraw_beforeAnyHandle_ok_afterHandle_forbidden() {
|
||
mapDept("ybb", 101, "甲");
|
||
FlwDef def = saveAndPublishDef("t4", "撤回", linearDef("ybb", "审核"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-4", 201, "张三");
|
||
instanceService.withdraw(SCH, outcome.getInstanceId());
|
||
assertEquals(FlwEnums.InstanceStatus.WITHDRAWN.getCode(),
|
||
instanceRepository.findById(outcome.getInstanceId()).get().getStatus());
|
||
// 撤回留痕(2026-09-18):轨迹里必须有"撤回申请"行,否则撤回在时间线上是空白
|
||
List<FlwTask> afterWithdraw = traceRows(outcome.getInstanceId());
|
||
FlwTask withdrawRow = afterWithdraw.stream()
|
||
.filter(t -> FlwEnums.TaskStatus.NODE_WITHDRAW.equals(t.getNodeId())).findFirst().orElse(null);
|
||
assertNotNull(withdrawRow, "撤回必须留痕");
|
||
assertEquals(FlwEnums.TaskStatus.WITHDRAWN.getCode(), withdrawRow.getStatus());
|
||
assertEquals(201, withdrawRow.getAssigneeId()); // 操作人=提交人
|
||
// 留痕行不算"已被办理":重提前的撤回守卫口径不能被自己写的事件行挡住
|
||
assertFalse(FlwEnums.TaskStatus.handledCodes().contains(withdrawRow.getStatus()));
|
||
|
||
// 重提重启后再办理,随后撤回必须被拒(已有任务被办理)
|
||
FlowInstanceService.ApplyOutcome reapplied = apply(def.getFlowGuid(), "biz-4", 201, "张三");
|
||
FlwTask task = taskRepository.findByInstanceIdAndStatus(reapplied.getInstanceId(), 0).get(0);
|
||
taskService.handle(SCH, task.getId(), true, "ok", 101);
|
||
assertThrows(BizException.class, () -> instanceService.withdraw(SCH, reapplied.getInstanceId()));
|
||
}
|
||
|
||
/**
|
||
* 轨迹时间流 + 已办去重(2026-09-18):
|
||
* ① 发起/重新提交留痕成事件行,detail.trace 按发生时间升序且带 kind/round/科室富化;
|
||
* ② 同一办件被本人办理两次(驳回 + 重提后再同意)时,已办列表只出一行、doneCount=2、
|
||
* 结果取最后一次(同意),并保留"共办理 N 次"的提示依据。
|
||
*/
|
||
@Test
|
||
void trace_timeline_kindsRounds_andDoneDedup() {
|
||
mapDept("ybb", 101, "甲");
|
||
FlwDef def = saveAndPublishDef("t20", "轨迹时间流", linearDef("ybb", "审核"));
|
||
FlowInstanceService.ApplyOutcome first = apply(def.getFlowGuid(), "biz-20", 201, "张三");
|
||
Long instanceId = first.getInstanceId();
|
||
|
||
// 一轮:办理人驳回;提交人重提;二轮:同一个人同意 → 同一办件上本人两条已办
|
||
taskService.handle(SCH, taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId(),
|
||
false, "材料不全", 101);
|
||
apply(def.getFlowGuid(), "biz-20", 201, "张三");
|
||
taskService.handle(SCH, taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0).getId(),
|
||
true, "补齐了", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
|
||
// ① 轨迹时间流:发起 → 驳回 → 重新提交 → 同意
|
||
java.util.List<java.util.Map<String, Object>> trace =
|
||
queryService.detail(SCH, instanceId, null).getTrace();
|
||
java.util.List<String> kinds = new java.util.ArrayList<>();
|
||
for (java.util.Map<String, Object> row : trace) {
|
||
kinds.add((String) row.get("kind"));
|
||
}
|
||
assertEquals(java.util.Arrays.asList("SUBMIT", "REJECT", "RESUBMIT", "APPROVE"), kinds);
|
||
// 时间为升序(前端直接按后端顺序渲染)
|
||
for (int i = 1; i < trace.size(); i++) {
|
||
assertTrue(String.valueOf(trace.get(i).get("time"))
|
||
.compareTo(String.valueOf(trace.get(i - 1).get("time"))) >= 0,
|
||
"轨迹必须按发生时间升序:" + trace);
|
||
}
|
||
// 轮次:驳回属于第 1 轮,重新提交与二轮同意属于第 2 轮
|
||
assertEquals(1, trace.get(1).get("round"));
|
||
assertEquals(2, trace.get(2).get("round"));
|
||
assertEquals(2, trace.get(3).get("round"));
|
||
// 科室口径富化(DEPT_KEY 节点):deptName 取映射里的科室名,前端"按科室"据此换掉人名
|
||
assertEquals("DEPT_KEY", trace.get(3).get("assigneeType"));
|
||
assertEquals(dk("ybb"), trace.get(3).get("deptKey"));
|
||
assertEquals("ybb科", trace.get(3).get("deptName"));
|
||
|
||
// ② 已办去重:同一办件一行,结果是最后一次(同意),次数=2
|
||
com.yunhai.platform.utils.model.BasePageResult<com.yunhai.flowcenter.model.FlowModels.TaskItem>
|
||
done = queryService.pageDone(SCH, 101, 1, 10);
|
||
assertEquals(1, done.getContent().size(), "一个办件只能有一条已办记录");
|
||
assertEquals(2, done.getContent().get(0).getDoneCount());
|
||
assertEquals(FlwEnums.TaskStatus.APPROVED.getCode(), done.getContent().get(0).getStatus());
|
||
assertEquals("审核", done.getContent().get(0).getNodeName());
|
||
}
|
||
|
||
@Test
|
||
void reapply_inFlight_duplicated_noNewInstance() {
|
||
mapDept("ybb", 101, "甲");
|
||
FlwDef def = saveAndPublishDef("t5", "在途幂等", linearDef("ybb", "审核"));
|
||
FlowInstanceService.ApplyOutcome first = apply(def.getFlowGuid(), "biz-5", 201, "张三");
|
||
FlowInstanceService.ApplyOutcome second = apply(def.getFlowGuid(), "biz-5", 201, "张三");
|
||
assertTrue(second.isDuplicated());
|
||
assertEquals(first.getInstanceId(), second.getInstanceId());
|
||
// 在途命中只返回不重建,任务不翻倍
|
||
assertEquals(1, taskRepository.findByInstanceIdAndStatus(first.getInstanceId(), 0).size());
|
||
}
|
||
|
||
@Test
|
||
void reapply_afterReject_restartsSameInstance_withHistoryKept() {
|
||
mapDept("ybb", 101, "甲");
|
||
FlwDef def = saveAndPublishDef("t6", "R4 重启", linearDef("ybb", "审核"));
|
||
FlowInstanceService.ApplyOutcome first = apply(def.getFlowGuid(), "biz-6", 201, "张三");
|
||
FlwInstance before = instanceRepository.findById(first.getInstanceId()).get();
|
||
int versionBefore = before.getVersion();
|
||
|
||
// 拒绝 → 终态
|
||
FlwTask task = taskRepository.findByInstanceIdAndStatus(first.getInstanceId(), 0).get(0);
|
||
taskService.handle(SCH, task.getId(), false, "不合格", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.REJECTED.getCode(),
|
||
instanceRepository.findById(first.getInstanceId()).get().getStatus());
|
||
|
||
// R4:重提复用同一办件行重启推进
|
||
FlowInstanceService.ApplyOutcome second = apply(def.getFlowGuid(), "biz-6", 201, "张三");
|
||
assertTrue(second.isDuplicated());
|
||
assertEquals(first.getInstanceId(), second.getInstanceId());
|
||
FlwInstance after = instanceRepository.findById(first.getInstanceId()).get();
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(), after.getStatus());
|
||
assertTrue(after.getVersion() > versionBefore, "重启后 instanceVersion 必须单调递增");
|
||
// 轨迹连续:历史拒绝任务保留 + 新一轮任务待办
|
||
List<FlwTask> all = allTasks(first.getInstanceId());
|
||
assertEquals(FlwEnums.TaskStatus.REJECTED.getCode(), all.get(0).getStatus());
|
||
assertEquals(1, taskRepository.findByInstanceIdAndStatus(first.getInstanceId(), 0).size());
|
||
assertNull(after.getFinishTime());
|
||
// 二轮通过 → 完成
|
||
FlwTask newTask = taskRepository.findByInstanceIdAndStatus(first.getInstanceId(), 0).get(0);
|
||
taskService.handle(SCH, newTask.getId(), true, "通过", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(first.getInstanceId()).get().getStatus());
|
||
}
|
||
|
||
@Test
|
||
void parallelSplitJoin_bothBranchesArrive_thenComplete() {
|
||
mapDept("ka", 101, "甲");
|
||
mapDept("kb", 102, "乙");
|
||
FlwDef def = saveAndPublishDef("t7", "并行", parallelDef("ka", "kb"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-7", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
// split 激活两条分支
|
||
List<FlwBranch> branches = instanceRepository.findById(instanceId)
|
||
.map(i -> i.getId()).map(id -> branchRows(id)).orElse(null);
|
||
assertNotNull(branches);
|
||
assertEquals(2, branches.size());
|
||
// 甲科先过:分支一到达汇聚点,办件仍在途
|
||
FlwTask taskA = tasksOf(instanceId, 101).get(0);
|
||
taskService.handle(SCH, taskA.getId(), true, "同意", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
// 乙科再过:全部到达 → JOIN 放行 → END → 完成
|
||
FlwTask taskB = tasksOf(instanceId, 102).get(0);
|
||
taskService.handle(SCH, taskB.getId(), true, "同意", 102);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
}
|
||
|
||
@Test
|
||
void reject_inParallelBranch_failsWholeInstance() {
|
||
mapDept("ka", 101, "甲");
|
||
mapDept("kb", 102, "乙");
|
||
FlwDef def = saveAndPublishDef("t8", "并行拒绝", parallelDef("ka", "kb"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-8", 201, "张三");
|
||
FlwTask taskB = tasksOf(outcome.getInstanceId(), 102).get(0);
|
||
taskService.handle(SCH, taskB.getId(), false, "乙科不同意", 102);
|
||
FlwInstance instance = instanceRepository.findById(outcome.getInstanceId()).get();
|
||
assertEquals(FlwEnums.InstanceStatus.REJECTED.getCode(), instance.getStatus());
|
||
// 另一分支的待办被失效(失效后不在待办集合,按全量任务查找)
|
||
FlwTask taskA = allTasks(outcome.getInstanceId()).stream()
|
||
.filter(t -> t.getAssigneeId().equals(101)).findFirst().get();
|
||
assertEquals(FlwEnums.TaskStatus.INVALIDATED.getCode(),
|
||
taskRepository.findById(taskA.getId()).get().getStatus());
|
||
}
|
||
|
||
@Test
|
||
void concurrentApprove_sameNode_onlyOnePathAdvances() throws Exception {
|
||
mapDept("ybb", 101, "甲");
|
||
mapDept("ybb", 102, "乙");
|
||
FlwDef def = saveAndPublishDef("t9", "并发会签", linearDef("ybb", "会签审核"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-9", 201, "张三");
|
||
List<FlwTask> tasks = taskRepository.findByInstanceIdAndStatus(outcome.getInstanceId(), 0);
|
||
// 两线程同时办理各自任务(推进互斥由乐观锁保证)
|
||
Thread t1 = new Thread(() -> taskService.handle(SCH, tasks.get(0).getId(), true, "ok", 101));
|
||
Thread t2 = new Thread(() -> taskService.handle(SCH, tasks.get(1).getId(), true, "ok", 102));
|
||
t1.start();
|
||
t2.start();
|
||
t1.join(10_000);
|
||
t2.join(10_000);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(outcome.getInstanceId()).get().getStatus());
|
||
// 只应有一轮 END 完成(无重复推进导致的重复任务创建:总任务数=2)
|
||
assertEquals(2, allTasks(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 = allTasks(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());
|
||
}
|
||
|
||
/**
|
||
* NOTIFY 抄送·主线(2026-09-23):激活即逐人落 flw_cc,不建任务、不进 currentNodes,
|
||
* 流程无停顿直达下一审批节点;办件轨迹出现 kind=CC 行且"抄送我的"可查到。
|
||
*/
|
||
@Test
|
||
void notify_onMainline_writesCcRows_andKeepsFlowMoving() {
|
||
mapDept("km", 101, "甲");
|
||
FlwDef def = saveAndPublishDef("t16", "抄送主线", notifyMainlineDef("km"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-16", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
|
||
// 抄送瞬时放行:两个接收人各一行,无 cc 节点任务,直接建审核待办
|
||
List<com.yunhai.flowcenter.entity.FlwCc> ccs = ccRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||
assertEquals(2, ccs.size());
|
||
assertEquals("cc1", ccs.get(0).getNodeId());
|
||
assertEquals("抄送校领导", ccs.get(0).getNodeName());
|
||
assertEquals(301, ccs.get(0).getUserId());
|
||
assertEquals(302, ccs.get(1).getUserId());
|
||
assertEquals(1, taskRepository.findByInstanceIdAndStatus(instanceId, 0).size());
|
||
FlwInstance instance = instanceRepository.findById(instanceId).get();
|
||
assertEquals("n1", instance.getCurrentNodes());
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(), instance.getStatus());
|
||
// 审核通过 → 完成(抄送节点从未阻塞过推进)
|
||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
|
||
// 轨迹:发起 → 抄送×2 → 同意。时间列毫秒精度(DATETIME(3))后亚秒机器链因果序可复现;
|
||
// 展示字符串截断到秒(str),排序用原值。
|
||
java.util.List<java.util.Map<String, Object>> trace =
|
||
queryService.detail(SCH, instanceId, null).getTrace();
|
||
java.util.List<String> kinds = new java.util.ArrayList<>();
|
||
for (java.util.Map<String, Object> row : trace) {
|
||
kinds.add((String) row.get("kind"));
|
||
}
|
||
assertEquals(java.util.Arrays.asList("SUBMIT", "CC", "CC", "APPROVE"), kinds);
|
||
// 抄送我的:301 视角一行,回填办件信息
|
||
com.yunhai.platform.utils.model.BasePageResult<com.yunhai.flowcenter.model.FlowModels.CcItem>
|
||
ccPage = queryService.pageCc(SCH, 301, 1, 10);
|
||
assertEquals(1, ccPage.getContent().size());
|
||
assertEquals(instanceId, ccPage.getContent().get(0).getInstanceId());
|
||
assertEquals("张三的测试办件", ccPage.getContent().get(0).getBizTitle());
|
||
}
|
||
|
||
/** NOTIFY 在并行分支内:瞬时到达 JOIN 不占等待(审批支到齐即放行),落库与轨迹同主线口径 */
|
||
@Test
|
||
void notify_inParallelBranch_reachesJoin_withoutBlocking() {
|
||
mapDept("km", 101, "甲");
|
||
FlwDef def = saveAndPublishDef("t17", "抄送并行", notifyParallelDef("km"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-17", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
// 抄送支激活即写行并到达 JOIN:分支一 RUNNING(审批支)、一 ARRIVED(抄送支)
|
||
List<FlwBranch> branches = branchRows(instanceId);
|
||
assertEquals(2, branches.size());
|
||
assertEquals(1, branches.stream()
|
||
.filter(b -> FlwEnums.BranchStatus.RUNNING.getCode() == b.getStatus()).count());
|
||
assertEquals(1, ccRepository.findByInstanceIdOrderByIdAsc(instanceId).size());
|
||
// 办件仍在途等审批支:乙支瞬时完成没有让 JOIN 提前放行
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
// 审批支到齐 → JOIN 放行 → 完成
|
||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
}
|
||
|
||
/** NOTIFY 接收人解析不出人(映射停用/名单空):不阻塞推进,只是没有抄送行 */
|
||
@Test
|
||
void notify_unresolvableReceivers_skipsCc_butFlowContinues() {
|
||
mapDept("km", 101, "甲");
|
||
// deptKey 未注册任何映射(mapDept 只注册 km,抄送挂 kx)
|
||
String json = notifyMainlineDef("km").replace(
|
||
"\"receivers\":{\"type\":\"USERS\",\"userIds\":[301,302]}}",
|
||
"\"receivers\":{\"type\":\"DEPT_KEY\",\"deptKey\":\"kx\"}}");
|
||
FlwDef def = saveAndPublishDef("t18", "抄送空对象", json);
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-18", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
assertTrue(ccRepository.findByInstanceIdOrderByIdAsc(instanceId).isEmpty(),
|
||
"解析不出人不应落行");
|
||
assertEquals("n1", instanceRepository.findById(instanceId).get().getCurrentNodes());
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
}
|
||
|
||
/**
|
||
* 嵌套并行 scope 修复(2026-09-24 §4.6):外层分支 tb 内再套并行段 s2{tc,td}——
|
||
* 修复前 JOIN 计数是实例级(tb 行仍在进行),内层 join2 到不齐、外层 join1 等 tb,互锁死。
|
||
* 修复后:内层两支到齐 → join2 放行并以外层上下文 tb 续走 → join1 两轮到齐才完成。
|
||
*/
|
||
@Test
|
||
void nestedParallel_innerJoinReleasesFirst_outerCompletesAfterBoth() {
|
||
mapDept("km", 103, "主");
|
||
mapDept("ka", 101, "甲");
|
||
mapDept("kb", 102, "乙");
|
||
mapDept("kc", 104, "丙");
|
||
mapDept("kd", 105, "丁");
|
||
FlwDef def = saveAndPublishDef("t19", "嵌套并行", nestedParallelDef("km", "ka", "kb", "kc", "kd"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-19", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
taskService.handle(SCH, tasksOf(instanceId, 103).get(0).getId(), true, "ok", 103);
|
||
// 外层 split:顶层分支键无点号(存量口径)
|
||
assertEquals(new java.util.TreeSet<>(java.util.Arrays.asList("ta", "tb")),
|
||
branchRows(instanceId).stream().map(FlwBranch::getBranchKey)
|
||
.collect(java.util.stream.Collectors.toCollection(java.util.TreeSet::new)));
|
||
// 甲支先过:join1 等外层 tb(其内层段未走),办件仍在途
|
||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
// 乙支过 → 进入内层并行段:分支键=路径前缀 tb.tc / tb.td
|
||
taskService.handle(SCH, tasksOf(instanceId, 102).get(0).getId(), true, "ok", 102);
|
||
java.util.Map<String, Integer> byKey = branchRows(instanceId).stream()
|
||
.collect(java.util.stream.Collectors.toMap(FlwBranch::getBranchKey, FlwBranch::getStatus));
|
||
assertEquals(4, byKey.size());
|
||
assertEquals(FlwEnums.BranchStatus.ARRIVED.getCode(), (int) byKey.get("ta"));
|
||
assertEquals(FlwEnums.BranchStatus.RUNNING.getCode(), (int) byKey.get("tb"));
|
||
assertEquals(FlwEnums.BranchStatus.RUNNING.getCode(), (int) byKey.get("tb.tc"));
|
||
assertEquals(FlwEnums.BranchStatus.RUNNING.getCode(), (int) byKey.get("tb.td"));
|
||
// 内层丙支先过:join2 等丁支(修复前这里会因实例级计数连带等 tb 而死锁)
|
||
taskService.handle(SCH, tasksOf(instanceId, 104).get(0).getId(), true, "ok", 104);
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
// 丁支到齐 → join2 放行(tb 标记到达)→ join1 两轮到齐 → END 完成
|
||
taskService.handle(SCH, tasksOf(instanceId, 105).get(0).getId(), true, "ok", 105);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
}
|
||
|
||
/** 出口分支内并行段(CONDITION 分支内并行的前置形态):选"乙线"出口 → 并行段两支到齐 → 汇聚完成 */
|
||
@Test
|
||
void outletBranch_withParallelSegment_joinsAndCompletes() {
|
||
mapDept("km", 103, "主");
|
||
mapDept("ka", 101, "甲");
|
||
mapDept("kb", 102, "乙");
|
||
FlwDef def = saveAndPublishDef("t21", "出口并行", outletParallelDef("km", "ka", "kb"));
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-21", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
// 选"乙线"出口进入并行段(scope=主线,出口校验应放行)
|
||
taskService.handle(SCH, tasksOf(instanceId, 103).get(0).getId(), true, "走乙线", 103, "乙线");
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
assertEquals(2, taskRepository.findByInstanceIdAndStatus(instanceId, 0).size());
|
||
// 两支到齐 → join2 放行 → END 完成
|
||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
taskService.handle(SCH, tasksOf(instanceId, 102).get(0).getId(), true, "ok", 102);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
}
|
||
|
||
/**
|
||
* CONDITION 命中选路(2026-09-24 §4):判断返回"甲线"→ 只激活甲线待办,乙线不建任务;
|
||
* 办理后完成;轨迹出现 kind=ROUTE 行("系统判断 → 甲线"),留痕 outlet=甲线。
|
||
*/
|
||
@Test
|
||
void condition_routesByOutlet_andTraceShowsRouteRow() {
|
||
mapDept("km", 101, "甲");
|
||
mapDept("kb", 102, "乙");
|
||
registerRouter("test.judge");
|
||
FlwDef def = saveAndPublishDef("t22", "条件分支", conditionDef("test.judge", "km", "kb"));
|
||
StubJudge.reset("甲线");
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-22", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
|
||
// 甲线命中:仅甲科一条待办,CONDITION 已放行(不在 currentNodes)
|
||
List<FlwTask> pending = taskRepository.findByInstanceIdAndStatus(instanceId, 0);
|
||
assertEquals(1, pending.size());
|
||
assertEquals("n1", pending.get(0).getNodeId());
|
||
assertEquals("n1", instanceRepository.findById(instanceId).get().getCurrentNodes());
|
||
|
||
taskService.handle(SCH, pending.get(0).getId(), true, "ok", 101);
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
|
||
// 留痕 + 轨迹:ROUTE 行在发起之后、办理之前
|
||
List<com.yunhai.flowcenter.entity.FlwRouteLog> logs =
|
||
routeLogRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||
assertEquals(1, logs.size());
|
||
assertEquals("甲线", logs.get(0).getOutlet());
|
||
assertEquals(1, logs.get(0).getAttempt());
|
||
java.util.List<java.util.Map<String, Object>> trace =
|
||
queryService.detail(SCH, instanceId, null).getTrace();
|
||
java.util.List<String> kinds = new java.util.ArrayList<>();
|
||
for (java.util.Map<String, Object> row : trace) {
|
||
kinds.add((String) row.get("kind"));
|
||
}
|
||
assertEquals(java.util.Arrays.asList("SUBMIT", "ROUTE", "APPROVE"), kinds);
|
||
assertEquals("甲线", trace.get(1).get("outlet"));
|
||
}
|
||
|
||
/**
|
||
* CONDITION 判断失败挂起 + 管理端重跑(§4.4 已定策略):失败=节点保持在途、办件仍 10、
|
||
* 落 outlet=NULL 留痕行(无默认出口,宁挂起);重跑成功后按新出口续走到完成。
|
||
*/
|
||
@Test
|
||
void condition_judgeFails_suspends_thenAdminReroute_resumes() {
|
||
mapDept("km", 101, "甲");
|
||
mapDept("kb", 102, "乙");
|
||
registerRouter("test.judge");
|
||
FlwDef def = saveAndPublishDef("t23", "条件挂起", conditionDef("test.judge", "km", "kb"));
|
||
StubJudge.failure = new RouteJudgeClient.JudgeUnavailableException("服务不可用", null);
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-23", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
|
||
// 挂起:办件在途、CONDITION 占位在 currentNodes、无待办任务
|
||
FlwInstance suspended = instanceRepository.findById(instanceId).get();
|
||
assertEquals(FlwEnums.InstanceStatus.APPROVING.getCode(), suspended.getStatus());
|
||
assertEquals("cond1", suspended.getCurrentNodes());
|
||
assertTrue(taskRepository.findByInstanceIdAndStatus(instanceId, 0).isEmpty());
|
||
List<com.yunhai.flowcenter.entity.FlwRouteLog> failLogs =
|
||
routeLogRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||
assertEquals(1, failLogs.size());
|
||
assertNull(failLogs.get(0).getOutlet(), "失败行 outlet=NULL");
|
||
assertEquals(1, failLogs.get(0).getAttempt());
|
||
|
||
// 重跑仍失败:留痕 attempt=2,办件继续挂起(业务错抛出)
|
||
assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||
() -> routerAdminService.reroute(SCH, instanceId));
|
||
assertEquals(2, routeLogRepository.findByInstanceIdOrderByIdAsc(instanceId).size());
|
||
assertEquals("cond1", instanceRepository.findById(instanceId).get().getCurrentNodes());
|
||
|
||
// 判断恢复 → 重跑成功:按乙线续走建待办,办理完成;attempt=3 留痕 outlet=乙线
|
||
StubJudge.reset("乙线");
|
||
routerAdminService.reroute(SCH, instanceId);
|
||
FlwInstance resumed = instanceRepository.findById(instanceId).get();
|
||
assertEquals("n2", resumed.getCurrentNodes());
|
||
List<FlwTask> pending = taskRepository.findByInstanceIdAndStatus(instanceId, 0);
|
||
assertEquals(1, pending.size());
|
||
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());
|
||
List<com.yunhai.flowcenter.entity.FlwRouteLog> logs =
|
||
routeLogRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||
assertEquals(3, logs.size());
|
||
assertEquals("乙线", logs.get(2).getOutlet());
|
||
assertEquals(3, logs.get(2).getAttempt());
|
||
// 完成后的办件不再可重跑
|
||
assertThrows(com.yunhai.platform.apistd.BizException.class,
|
||
() -> routerAdminService.reroute(SCH, instanceId));
|
||
}
|
||
|
||
/**
|
||
* 退回重走 CONDITION 必须重新调判断(§4.4:单据可能已改,留痕只增不改多轮可见):
|
||
* 一轮判"甲线"到 n1,退回 n0 重走判"乙线"直达 END;route_log 两行 attempt 递增。
|
||
*/
|
||
@Test
|
||
void condition_returnBack_rewalk_rejudges() {
|
||
mapDept("km", 101, "甲");
|
||
mapDept("ka", 102, "乙");
|
||
registerRouter("test.judge");
|
||
FlwDef def = saveAndPublishDef("t24", "条件重判", mainlineConditionDef("test.judge", "km", "ka"));
|
||
StubJudge.reset("甲线");
|
||
FlowInstanceService.ApplyOutcome outcome = apply(def.getFlowGuid(), "biz-24", 201, "张三");
|
||
Long instanceId = outcome.getInstanceId();
|
||
|
||
// 一轮:n0 过 → 判甲线 → n1 待办
|
||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "ok", 101);
|
||
assertEquals("n1", instanceRepository.findById(instanceId).get().getCurrentNodes());
|
||
// 退回入口 n0 重走(改判乙线)
|
||
StubJudge.reset("乙线");
|
||
FlwTask n1Task = taskRepository.findByInstanceIdAndStatus(instanceId, 0).get(0);
|
||
taskService.returnBack(SCH, n1Task.getId(), "n0", "改判乙线", 102);
|
||
taskService.handle(SCH, tasksOf(instanceId, 101).get(0).getId(), true, "再走一遍", 101);
|
||
// 乙线直达 END:办件完成(n1 未再激活)
|
||
assertEquals(FlwEnums.InstanceStatus.COMPLETED.getCode(),
|
||
instanceRepository.findById(instanceId).get().getStatus());
|
||
// 留痕两轮:attempt 1=甲线、2=乙线(只增不改)
|
||
List<com.yunhai.flowcenter.entity.FlwRouteLog> logs =
|
||
routeLogRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||
assertEquals(2, logs.size());
|
||
assertEquals("甲线", logs.get(0).getOutlet());
|
||
assertEquals(1, logs.get(0).getAttempt());
|
||
assertEquals("乙线", logs.get(1).getOutlet());
|
||
assertEquals(2, logs.get(1).getAttempt());
|
||
}
|
||
|
||
private List<FlwBranch> branchRows(Long instanceId) {
|
||
return branchRepository.findByInstanceId(instanceId);
|
||
}
|
||
|
||
@Autowired
|
||
private com.yunhai.flowcenter.repository.FlwBranchRepository branchRepository;
|
||
|
||
/**
|
||
* 审批任务行(剔除 6/7 留痕事件行,2026-09-18 起"发起/重新提交/撤回"也落 flw_task 行):
|
||
* 只关心审批动作的断言(按序号取值、按数量计数)必须走这个口,否则会取到事件行。
|
||
*/
|
||
private List<FlwTask> allTasks(Long instanceId) {
|
||
return taskRepository.findByInstanceIdOrderByIdAsc(instanceId).stream()
|
||
.filter(t -> !FlwEnums.TaskStatus.eventCodes().contains(t.getStatus()))
|
||
.collect(java.util.stream.Collectors.toList());
|
||
}
|
||
|
||
/** 全量轨迹行(含 6/7 留痕事件行):留痕类断言用 */
|
||
private List<FlwTask> traceRows(Long instanceId) {
|
||
return taskRepository.findByInstanceIdOrderByIdAsc(instanceId);
|
||
}
|
||
|
||
private List<FlwTask> tasksOf(Long instanceId, Integer assigneeId) {
|
||
List<FlwTask> out = new java.util.ArrayList<>();
|
||
for (FlwTask t : taskRepository.findByInstanceIdAndStatus(instanceId, 0)) {
|
||
if (assigneeId.equals(t.getAssigneeId())) {
|
||
out.add(t);
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
}
|