feat: 提交流程中心后端代码
build / build-push (push) Successful in 28s

Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、
回调分发、oapi 接口及单元测试。
保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile,
新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
chenxing
2026-09-15 10:42:56 +08:00
parent 0296b2b5dd
commit c1103aeb29
65 changed files with 7119 additions and 0 deletions
@@ -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;
}
}