Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、 回调分发、oapi 接口及单元测试。 保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile, 新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
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;
|
||||
|
||||
@TestConfiguration
|
||||
static class StubConfig {
|
||||
@Bean
|
||||
@Primary
|
||||
public com.yunhai.flowcenter.stub.StubUserClient stubUserClient() {
|
||||
return new com.yunhai.flowcenter.stub.StubUserClient();
|
||||
}
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
cleanDb();
|
||||
}
|
||||
|
||||
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 = taskRepository.findByInstanceIdOrderByIdAsc(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());
|
||||
|
||||
// 重提重启后再办理,随后撤回必须被拒(已有任务被办理)
|
||||
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()));
|
||||
}
|
||||
|
||||
@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 = taskRepository.findByInstanceIdOrderByIdAsc(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, taskRepository.findByInstanceIdOrderByIdAsc(outcome.getInstanceId()).size());
|
||||
}
|
||||
|
||||
private List<FlwBranch> branchRows(Long instanceId) {
|
||||
return branchRepository.findByInstanceId(instanceId);
|
||||
}
|
||||
|
||||
@Autowired
|
||||
private com.yunhai.flowcenter.repository.FlwBranchRepository branchRepository;
|
||||
|
||||
private List<FlwTask> allTasks(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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user