1
0

Compare commits

...

4 Commits

Author SHA1 Message Date
c3ca027f72 新增自动节点 2025-01-02 17:43:40 +08:00
7d88674c66 更改部份变量名 2025-01-02 16:09:18 +08:00
422e024b7b 完成spring jpa的存储适配 2025-01-02 16:05:15 +08:00
13b9366346 从头再来 2024-12-31 08:51:54 +08:00
46 changed files with 1392 additions and 933 deletions

4
.idea/encodings.xml generated
View File

@@ -1,6 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/adapter/flowable-spring-boot-jpa-starter/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/adapter/flowable-spring-boot-jpa-starter/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flowable-core/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/flowable-core/src/main/resources" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
<file url="file://$PROJECT_DIR$/src/main/resources" charset="UTF-8" />
</component>

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>flowable</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-spring-boot-jpa-starter</artifactId>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>flowable-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,42 @@
package com.lanyuanxiaoyao.flowable.jpa;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
import java.util.UUID;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Service;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
@Slf4j
@Service
public class SpringFlowableManager extends FlowableManager {
private final ApplicationContext applicationContext;
public SpringFlowableManager(FlowableRepository flowableRepository, ApplicationContext applicationContext) {
super(flowableRepository, () -> UUID.randomUUID().toString());
this.applicationContext = applicationContext;
}
@SneakyThrows
@Override
protected <T> T createBean(String classpath) {
Class<?> clazz = Class.forName(classpath);
T targetObject = null;
try {
targetObject = (T) applicationContext.getBean(clazz);
} catch (Exception springException) {
log.warn("{} not found in spring context", springException);
try {
targetObject = (T) clazz.newInstance();
} catch (Exception javaException) {
throw new IllegalArgumentException(javaException);
}
}
return targetObject;
}
}

View File

@@ -0,0 +1,109 @@
package com.lanyuanxiaoyao.flowable.jpa;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
import com.lanyuanxiaoyao.flowable.jpa.entity.JpaFlowableHistory;
import com.lanyuanxiaoyao.flowable.jpa.entity.JpaFlowableInstance;
import com.lanyuanxiaoyao.flowable.jpa.entity.JpaFlowableNode;
import com.lanyuanxiaoyao.flowable.jpa.repository.JpaFlowableHistoryRepository;
import com.lanyuanxiaoyao.flowable.jpa.repository.JpaFlowableInstanceRepository;
import com.lanyuanxiaoyao.flowable.jpa.repository.JpaFlowableNodeRepository;
import java.util.List;
import java.util.stream.Collectors;
import javax.transaction.Transactional;
import org.springframework.stereotype.Service;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
@Service
public class SpringJpaFlowableRepository implements FlowableRepository {
private final JpaFlowableNodeRepository jpaFlowableNodeRepository;
private final JpaFlowableInstanceRepository jpaFlowableInstanceRepository;
private final JpaFlowableHistoryRepository jpaFlowableHistoryRepository;
public SpringJpaFlowableRepository(JpaFlowableNodeRepository jpaFlowableNodeRepository, JpaFlowableInstanceRepository jpaFlowableInstanceRepository, JpaFlowableHistoryRepository jpaFlowableHistoryRepository) {
this.jpaFlowableNodeRepository = jpaFlowableNodeRepository;
this.jpaFlowableInstanceRepository = jpaFlowableInstanceRepository;
this.jpaFlowableHistoryRepository = jpaFlowableHistoryRepository;
}
@Transactional(rollbackOn = Exception.class)
@Override
public void saveNode(FlowableNode node) {
jpaFlowableNodeRepository.save(new JpaFlowableNode(node));
}
@Transactional(rollbackOn = Exception.class)
@Override
public void saveNode(List<FlowableNode> nodes) {
jpaFlowableNodeRepository.saveAll(nodes.stream().map(JpaFlowableNode::new).collect(Collectors.toList()));
}
@Override
public FlowableNode getNode(String nodeId) {
return jpaFlowableNodeRepository.findById(nodeId)
.map(JpaFlowableNode::toFlowableNode)
.orElse(null);
}
@Override
public List<FlowableNode> listNodes() {
return jpaFlowableNodeRepository.findAll()
.stream()
.map(JpaFlowableNode::toFlowableNode)
.collect(Collectors.toList());
}
@Transactional(rollbackOn = Exception.class)
@Override
public void saveInstance(FlowableInstance instance) {
jpaFlowableInstanceRepository.save(new JpaFlowableInstance(instance));
}
@Override
public FlowableInstance getInstance(String instantId) {
return jpaFlowableInstanceRepository.findById(instantId)
.map(JpaFlowableInstance::toFlowableInstance)
.orElse(null);
}
@Override
public List<FlowableInstance> listInstances() {
return jpaFlowableInstanceRepository.findAll()
.stream()
.map(JpaFlowableInstance::toFlowableInstance)
.collect(Collectors.toList());
}
@Transactional(rollbackOn = Exception.class)
@Override
public void saveHistory(FlowableHistory history) {
jpaFlowableHistoryRepository.save(new JpaFlowableHistory(history));
}
@Override
public FlowableHistory getHistory(String historyId) {
return jpaFlowableHistoryRepository.findById(historyId)
.map(JpaFlowableHistory::toFlowableHistory)
.orElse(null);
}
@Override
public List<FlowableHistory> listHistories(String instanceId) {
return jpaFlowableHistoryRepository.findAllByInstanceId(instanceId)
.stream()
.map(JpaFlowableHistory::toFlowableHistory)
.collect(Collectors.toList());
}
@Transactional(rollbackOn = Exception.class)
@Override
public void saveInstanceAndHistory(FlowableInstance instance, FlowableHistory history) {
saveInstance(instance);
saveHistory(history);
}
}

View File

@@ -0,0 +1,54 @@
package com.lanyuanxiaoyao.flowable.jpa.entity;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import java.time.LocalDateTime;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@NoArgsConstructor
@ToString
@Getter
@Setter
@Entity
@Table(name = "flowable_history")
@DynamicUpdate
@EntityListeners(AuditingEntityListener.class)
public class JpaFlowableHistory {
@Id
private String historyId;
@Column(nullable = false)
private String instanceId;
@Enumerated(EnumType.STRING)
private FlowableAction action;
private String comment;
@CreatedDate
private LocalDateTime createdTime;
public JpaFlowableHistory(FlowableHistory history) {
this.historyId = history.getHistoryId();
this.instanceId = history.getInstanceId();
this.action = history.getAction();
this.comment = history.getComment();
}
public FlowableHistory toFlowableHistory() {
return new FlowableHistory(historyId, instanceId, action, comment, createdTime);
}
}

View File

@@ -0,0 +1,94 @@
package com.lanyuanxiaoyao.flowable.jpa.entity;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.time.LocalDateTime;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;
import lombok.Cleanup;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.SneakyThrows;
import lombok.ToString;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@NoArgsConstructor
@ToString
@Getter
@Setter
@Entity
@Table(name = "flowable_instance")
@DynamicUpdate
@EntityListeners(AuditingEntityListener.class)
public class JpaFlowableInstance {
@Id
private String instanceId;
@Lob
private byte[] metadata;
@Column(nullable = false)
private String currentNodeId;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private FlowableInstance.Status status = FlowableInstance.Status.RUNNING;
@CreatedDate
private LocalDateTime createdTime;
@LastModifiedDate
private LocalDateTime updatedTime;
@SneakyThrows
public JpaFlowableInstance(FlowableInstance instance) {
this.instanceId = instance.getInstanceId();
this.metadata = objectToBytes(instance.getMetadata());
this.currentNodeId = instance.getCurrentNodeId();
this.status = instance.getStatus();
}
private static byte[] objectToBytes(Object object) throws IOException {
@Cleanup
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
@Cleanup
ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(object);
return byteArrayOutputStream.toByteArray();
}
private static Object bytesToObject(byte[] bytes) throws IOException, ClassNotFoundException {
@Cleanup
java.io.ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
@Cleanup
java.io.ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
return objectInputStream.readObject();
}
@SneakyThrows
public FlowableInstance toFlowableInstance() {
return new FlowableInstance(
instanceId,
currentNodeId,
(Map<String, Object>) bytesToObject(this.metadata),
status,
createdTime,
updatedTime
);
}
}

View File

@@ -0,0 +1,76 @@
package com.lanyuanxiaoyao.flowable.jpa.entity;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import java.time.LocalDateTime;
import java.util.Map;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ConstraintMode;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.EntityListeners;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.ForeignKey;
import javax.persistence.Id;
import javax.persistence.MapKeyColumn;
import javax.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;
import org.hibernate.annotations.DynamicUpdate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
@NoArgsConstructor
@ToString
@Getter
@Setter
@Entity
@Table(name = "flowable_node")
@DynamicUpdate
@EntityListeners(AuditingEntityListener.class)
public class JpaFlowableNode {
@Id
private String nodeId;
@Column(nullable = false)
private String name;
private String description;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private FlowableNode.Type type;
private String automaticAction;
@ElementCollection(fetch = javax.persistence.FetchType.EAGER)
@MapKeyColumn(name = "action")
@Column(name = "nodeId")
@CollectionTable(name = "flowable_node_manual_actions", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Map<FlowableAction, String> manualActions;
@CreatedDate
private LocalDateTime createdTime;
@LastModifiedDate
private LocalDateTime updateTime;
public JpaFlowableNode(FlowableNode node) {
this.nodeId = node.getNodeId();
this.name = node.getName();
this.description = node.getDescription();
this.type = node.getType();
this.automaticAction = node.getAutomaticAction();
this.manualActions = node.getManualActions();
}
public FlowableNode toFlowableNode() {
FlowableNode node = new FlowableNode(nodeId, name, description, type, automaticAction, manualActions, null, createdTime);
node.setUpdatedTime(updateTime);
return node;
}
}

View File

@@ -0,0 +1,15 @@
package com.lanyuanxiaoyao.flowable.jpa.repository;
import com.lanyuanxiaoyao.flowable.jpa.entity.JpaFlowableHistory;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@Repository
public interface JpaFlowableHistoryRepository extends JpaRepository<JpaFlowableHistory, String> {
List<JpaFlowableHistory> findAllByInstanceId(String instanceId);
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.flowable.jpa.repository;
import com.lanyuanxiaoyao.flowable.jpa.entity.JpaFlowableInstance;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@Repository
public interface JpaFlowableInstanceRepository extends JpaRepository<JpaFlowableInstance, String> {
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.flowable.jpa.repository;
import com.lanyuanxiaoyao.flowable.jpa.entity.JpaFlowableNode;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@Repository
public interface JpaFlowableNodeRepository extends JpaRepository<JpaFlowableNode, String> {
}

View File

@@ -0,0 +1,25 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import java.util.Map;
import javax.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@Slf4j
public class SimpleAutoAction implements FlowableNode.AutoAction {
@Resource
private FlowableManager flowableManager;
@Override
public FlowableAction action(FlowableInstance instance, FlowableNode node, Map<String, Object> metadata) {
log.info("Initial with spring: {}", flowableManager.listNodes());
return FlowableAction.APPROVE;
}
}

View File

@@ -0,0 +1,24 @@
package com.lanyuanxiaoyao.flowable.jpa;
import com.lanyuanxiaoyao.flowable.core.SimpleAutoAction;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@SpringBootApplication
@EnableJpaAuditing
public class SpringFlowableApplication {
public static void main(String[] args) {
SpringApplication.run(SpringFlowableApplication.class, args);
}
@Bean
public SimpleAutoAction simpleAutoAction() {
return new SimpleAutoAction();
}
}

View File

@@ -0,0 +1,21 @@
package com.lanyuanxiaoyao.flowable.jpa;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
import com.lanyuanxiaoyao.flowable.core.test.TestFlowableManager;
import javax.annotation.Resource;
import org.springframework.boot.test.context.SpringBootTest;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
@SpringBootTest
public class TestSpringFlowableManager extends TestFlowableManager {
@Resource
private SpringFlowableManager flowableManager;
@Override
protected FlowableManager flowableManager() {
return flowableManager;
}
}

View File

@@ -0,0 +1,18 @@
spring:
datasource:
url: jdbc:h2:mem:testdb
username: flowable
password: flowable
driver-class-name: org.h2.Driver
jpa:
show-sql: true
hibernate:
ddl-auto: create
logging:
level:
root: info
org:
hibernate:
orm:
jdbc:
bind: trace

46
flowable-core/pom.xml Normal file
View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>flowable</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>flowable-core</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.16</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.11.4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.16</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,24 @@
package com.lanyuanxiaoyao.flowable.core.helper;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.NoArgsConstructor;
/**
* List工具
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class ListHelper {
@SafeVarargs
public static <T> List<T> of(T... entries) {
return new ArrayList<>(Arrays.asList(entries));
}
public static <T> List<T> empty() {
return new ArrayList<>();
}
}

View File

@@ -0,0 +1,30 @@
package com.lanyuanxiaoyao.flowable.core.helper;
import java.util.HashMap;
import java.util.Map;
import lombok.NoArgsConstructor;
/**
* Map工具
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class MapHelper {
@SuppressWarnings("unchecked")
public static <K, V> Map<K, V> of(Object... entries) {
if (entries.length % 2 != 0) {
throw new IllegalArgumentException("参数必须为偶数个");
}
Map<K, V> map = new HashMap<>(entries.length / 2 + 1);
for (int index = 0; index < entries.length; index += 2) {
map.put((K) entries[index], (V) entries[index + 1]);
}
return map;
}
public static Map<String, Object> empty() {
return new HashMap<>(0);
}
}

View File

@@ -0,0 +1,20 @@
package com.lanyuanxiaoyao.flowable.core.helper;
import lombok.NoArgsConstructor;
/**
* String工具
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE)
public class StringHelper {
public static Boolean isBlank(String text) {
return text == null || text.trim().isEmpty();
}
public static Boolean equals(String text1, String text2) {
return text1 != null && text1.equals(text2);
}
}

View File

@@ -0,0 +1,167 @@
package com.lanyuanxiaoyao.flowable.core.manager;
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
import com.lanyuanxiaoyao.flowable.core.helper.MapHelper;
import com.lanyuanxiaoyao.flowable.core.helper.StringHelper;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableListener;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
import lombok.extern.slf4j.Slf4j;
/**
* 流程服务
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@Slf4j
public abstract class FlowableManager {
private final FlowableRepository flowableRepository;
private final IdGenerator idGenerator;
public FlowableManager(FlowableRepository flowableRepository, IdGenerator idGenerator) {
this.flowableRepository = flowableRepository;
this.idGenerator = idGenerator;
}
public List<FlowableNode> listNodes() {
return flowableRepository.listNodes();
}
public FlowableNode getNode(String nodeId) {
return flowableRepository.getNode(nodeId);
}
public List<FlowableInstance> listInstances() {
return flowableRepository.listInstances();
}
public FlowableInstance getInstance(String instantId) {
return flowableRepository.getInstance(instantId);
}
public List<FlowableHistory> listHistories(String instanceId) {
return flowableRepository.listHistories(instanceId);
}
public FlowableHistory getHistory(String historyId) {
return flowableRepository.getHistory(historyId);
}
public void create(FlowableNode... node) {
flowableRepository.saveNode(ListHelper.of(node));
}
public String start(String nodeId) {
return start(nodeId, MapHelper.empty());
}
public String start(String nodeId, Map<String, Object> metadata) {
FlowableNode node = flowableRepository.getNode(nodeId);
FlowableInstance instance = new FlowableInstance(idGenerator.createId(), node.getNodeId(), metadata);
flowableRepository.saveInstance(instance);
if (FlowableNode.Type.AUTOMATIC.equals(node.getType())) {
autoAction(instance, node, MapHelper.empty());
}
return instance.getInstanceId();
}
public void approve(String instanceId) {
approve(instanceId, "审批通过");
}
public void approve(String instanceId, String comment) {
action(instanceId, FlowableAction.APPROVE, comment);
}
public void reject(String instanceId) {
reject(instanceId, "审批不通过");
}
public void reject(String instanceId, String comment) {
action(instanceId, FlowableAction.REJECT, comment);
}
public void terminal(String instanceId) {
terminal(instanceId, "流程被终止");
}
public void terminal(String instanceId, String comment) {
action(instanceId, FlowableAction.TERMINAL, comment);
}
public void action(String instanceId, FlowableAction action, String comment) {
action(instanceId, action, comment, MapHelper.empty());
}
private void autoAction(FlowableInstance instance, FlowableNode node, Map<String, Object> metadata) {
String actionClass = node.getAutomaticAction();
if (StringHelper.isBlank(actionClass)) {
throw new IllegalArgumentException("自动节点执行器为空");
}
FlowableNode.AutoAction autoAction = createBean(actionClass);
action(instance, node, autoAction.action(instance, node, metadata), "系统自动执行", metadata);
}
private void action(String instanceId, FlowableAction action, String comment, Map<String, Object> metadata) {
FlowableInstance instance = flowableRepository.getInstance(instanceId);
FlowableNode node = flowableRepository.getNode(instance.getCurrentNodeId());
action(instance, node, action, comment, metadata);
}
private void action(FlowableInstance instance, FlowableNode node, FlowableAction action, String comment, Map<String, Object> metadata) {
if (FlowableInstance.Status.COMPLETED.equals(instance.getStatus()) || FlowableInstance.Status.ERROR.equals(instance.getStatus())) {
throw new IllegalArgumentException("ID为" + instance.getInstanceId() + "的流程已结束,无法操作");
}
if (FlowableAction.TERMINAL.equals(action)) {
saveInstance(instance, FlowableInstance.Status.ERROR, metadata, action, comment);
return;
}
if (Objects.isNull(node.getManualActions())
|| !node.getManualActions().containsKey(action)
|| StringHelper.isBlank(node.getManualActions().get(action))) {
saveInstance(instance, FlowableInstance.Status.COMPLETED, metadata, action, comment);
return;
}
String nextNodeId = node.getManualActions().get(action);
FlowableNode nextNode = flowableRepository.getNode(nextNodeId);
instance.setCurrentNodeId(nextNode.getNodeId());
saveInstance(instance, FlowableInstance.Status.RUNNING, metadata, action, comment);
if (FlowableNode.Type.AUTOMATIC.equals(nextNode.getType())) {
autoAction(instance, node, metadata);
}
}
private void saveInstance(FlowableInstance instance, FlowableInstance.Status status, Map<String, Object> metadata, FlowableAction action, String comment) {
instance.setStatus(status);
if (Objects.nonNull(metadata)) {
instance.addMetadata(metadata);
}
instance.setUpdatedTime(LocalDateTime.now());
FlowableHistory history = new FlowableHistory(idGenerator.createId(), instance.getInstanceId(), action, comment);
flowableRepository.saveInstanceAndHistory(instance, history);
}
private void callListeners(List<FlowableListener> listeners, Consumer<FlowableListener> handler) {
for (FlowableListener listener : listeners) {
handler.accept(listener);
}
}
protected abstract <T> T createBean(String classpath);
public interface IdGenerator {
String createId();
}
}

View File

@@ -0,0 +1,11 @@
package com.lanyuanxiaoyao.flowable.core.model;
/**
* 权限校验
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public interface FlowableAccessor {
void access(String accessor);
}

View File

@@ -0,0 +1,13 @@
package com.lanyuanxiaoyao.flowable.core.model;
/**
* 节点操作
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public enum FlowableAction {
APPROVE,
REJECT,
TERMINAL,
}

View File

@@ -0,0 +1,29 @@
package com.lanyuanxiaoyao.flowable.core.model;
import java.time.LocalDateTime;
import lombok.Data;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
@Data
public class FlowableHistory {
private final String historyId;
private final String instanceId;
private final FlowableAction action;
private final String comment;
private final LocalDateTime createdTime;
public FlowableHistory(String historyId, String instanceId, FlowableAction action, String comment) {
this(historyId, instanceId, action, comment, LocalDateTime.now());
}
public FlowableHistory(String historyId, String instanceId, FlowableAction action, String comment, LocalDateTime createdTime) {
this.historyId = historyId;
this.instanceId = instanceId;
this.action = action;
this.comment = comment;
this.createdTime = createdTime;
}
}

View File

@@ -0,0 +1,48 @@
package com.lanyuanxiaoyao.flowable.core.model;
import com.lanyuanxiaoyao.flowable.core.helper.MapHelper;
import java.time.LocalDateTime;
import java.util.Map;
import lombok.Data;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
@Data
public class FlowableInstance {
private final String instanceId;
private final Map<String, Object> metadata;
private final LocalDateTime createdTime;
private String currentNodeId;
private Status status;
private LocalDateTime updatedTime = LocalDateTime.now();
public FlowableInstance(String instanceId, String currentNodeId) {
this(instanceId, currentNodeId, MapHelper.empty(), Status.RUNNING, LocalDateTime.now(), LocalDateTime.now());
}
public FlowableInstance(String instanceId, String currentNodeId, Map<String, Object> metadata) {
this(instanceId, currentNodeId, metadata, Status.RUNNING, LocalDateTime.now(), LocalDateTime.now());
}
public FlowableInstance(String instanceId, String currentNodeId, Map<String, Object> metadata, Status status, LocalDateTime createdTime, LocalDateTime updatedTime) {
this.instanceId = instanceId;
this.metadata = metadata;
this.createdTime = createdTime;
this.currentNodeId = currentNodeId;
this.status = status;
this.updatedTime = updatedTime;
}
public void addMetadata(Map<String, Object> metadata) {
this.metadata.putAll(metadata);
}
public enum Status {
RUNNING,
COMPLETED,
ERROR,
}
}

View File

@@ -0,0 +1,19 @@
package com.lanyuanxiaoyao.flowable.core.model;
/**
* 节点监听
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public interface FlowableListener {
void onStart(FlowableInstance instance);
void onError(FlowableInstance instance, Throwable throwable);
void onComplete(FlowableInstance instance);
void onApprove(FlowableInstance instance);
void onReject(FlowableInstance instance);
}

View File

@@ -0,0 +1,53 @@
package com.lanyuanxiaoyao.flowable.core.model;
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* 流程节点
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@Data
public class FlowableNode {
private final String nodeId;
private final String name;
private final String description;
private final Type type;
private final String automaticAction;
private final Map<FlowableAction, String> manualActions;
private final List<String> listeners;
private final LocalDateTime createdTime;
private LocalDateTime updatedTime = LocalDateTime.now();
public FlowableNode(String nodeId, String name, String description, Type type, String automaticAction, Map<FlowableAction, String> manualActions) {
this(nodeId, name, description, type, automaticAction, manualActions, ListHelper.empty(), LocalDateTime.now());
}
public FlowableNode(String nodeId, String name, String description, Type type, String automaticAction, Map<FlowableAction, String> manualActions, List<String> listeners, LocalDateTime createdTime) {
this.nodeId = nodeId;
this.name = name;
this.description = description;
this.type = type;
this.automaticAction = automaticAction;
this.manualActions = manualActions;
this.listeners = listeners;
this.createdTime = createdTime;
}
public enum Type {
AUTOMATIC,
MANUAL,
}
public interface AutoAction {
FlowableAction action(FlowableInstance instance, FlowableNode node, Map<String, Object> metadata);
}
}

View File

@@ -0,0 +1,39 @@
package com.lanyuanxiaoyao.flowable.core.repository;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import java.util.List;
/**
* 存储统一管理
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public interface FlowableRepository {
void saveNode(FlowableNode node);
void saveNode(List<FlowableNode> nodes);
FlowableNode getNode(String nodeId);
List<FlowableNode> listNodes();
void saveInstance(FlowableInstance instance);
FlowableInstance getInstance(String instantId);
List<FlowableInstance> listInstances();
void saveHistory(FlowableHistory history);
FlowableHistory getHistory(String historyId);
List<FlowableHistory> listHistories(String instanceId);
/**
* 同时保存实例和历史记录,单独设立这里一个方法是方便数据库使用事务重写
*/
void saveInstanceAndHistory(FlowableInstance instance, FlowableHistory history);
}

View File

@@ -0,0 +1,140 @@
package com.lanyuanxiaoyao.flowable.core.test;
import com.lanyuanxiaoyao.flowable.core.helper.MapHelper;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import java.util.Map;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
/**
* 集成测试
*
* @author lanyuanxiaoyao
* @version 20241231
*/
@Slf4j
public abstract class TestFlowableManager {
protected abstract FlowableManager flowableManager();
private FlowableNode createManualNode() {
return createManualNode(UUID.randomUUID().toString(), null);
}
private FlowableNode createManualNode(String nodeId) {
return createManualNode(nodeId, null);
}
private FlowableNode createManualNode(String nodeId, Map<FlowableAction, String> nextNodes) {
return new FlowableNode(
nodeId,
UUID.randomUUID().toString(),
UUID.randomUUID().toString(),
FlowableNode.Type.MANUAL,
null,
nextNodes
);
}
/**
* 单节点审批
*/
@Test
public void testSingleNode() {
FlowableManager manager = flowableManager();
FlowableNode node1 = createManualNode();
manager.create(node1);
Assertions.assertNotNull(manager.getNode(node1.getNodeId()));
String instanceId = manager.start(node1.getNodeId());
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
Assertions.assertTrue(manager.listHistories(instanceId).isEmpty());
manager.approve(instanceId, "3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f");
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(1, manager.listHistories(instanceId).size());
Assertions.assertEquals(FlowableAction.APPROVE, manager.listHistories(instanceId).get(0).getAction());
Assertions.assertEquals("3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f", manager.listHistories(instanceId).get(0).getComment());
instanceId = manager.start(node1.getNodeId());
manager.reject(instanceId, "3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f");
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(FlowableAction.REJECT, manager.listHistories(instanceId).get(0).getAction());
Assertions.assertEquals("3ca20a11-dfb6-435b-8dcc-d00c6c0abd7f", manager.listHistories(instanceId).get(0).getComment());
}
@Test
public void testMultiNode() {
FlowableManager manager = flowableManager();
FlowableNode node1 = createManualNode(
"02779cbe-0d82-4e09-9bf8-60885400d100",
MapHelper.of(
FlowableAction.APPROVE, "1e126640-34ae-40f9-b55f-9cb8099d638f",
FlowableAction.REJECT, "02779cbe-0d82-4e09-9bf8-60885400d100"
)
);
FlowableNode node2 = createManualNode(
"1e126640-34ae-40f9-b55f-9cb8099d638f",
MapHelper.of(FlowableAction.REJECT, "02779cbe-0d82-4e09-9bf8-60885400d100")
);
manager.create(node1, node2);
String instanceId = manager.start(node1.getNodeId());
manager.reject(instanceId);
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(node1.getNodeId(), manager.getNode(manager.getInstance(instanceId).getCurrentNodeId()).getNodeId());
manager.approve(instanceId);
manager.reject(instanceId, "我觉得不行");
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(node1.getNodeId(), manager.getNode(manager.getInstance(instanceId).getCurrentNodeId()).getNodeId());
manager.approve(instanceId);
manager.approve(instanceId);
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(node2.getNodeId(), manager.getNode(manager.getInstance(instanceId).getCurrentNodeId()).getNodeId());
Assertions.assertEquals(FlowableAction.APPROVE, manager.listHistories(instanceId).get(4).getAction());
Assertions.assertEquals(5, manager.listHistories(instanceId).size());
}
@Test
public void testTerminal() {
FlowableManager manager = flowableManager();
FlowableNode node1 = createManualNode();
manager.create(node1);
String instanceId = manager.start(node1.getNodeId());
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
manager.terminal(instanceId, "d896b642-a1d8-499c-92e7-bed63581f2f8");
Assertions.assertEquals(FlowableInstance.Status.ERROR, manager.getInstance(instanceId).getStatus());
Assertions.assertEquals(1, manager.listHistories(instanceId).size());
Assertions.assertEquals(FlowableAction.TERMINAL, manager.listHistories(instanceId).get(0).getAction());
Assertions.assertEquals("d896b642-a1d8-499c-92e7-bed63581f2f8", manager.listHistories(instanceId).get(0).getComment());
Assertions.assertThrows(IllegalArgumentException.class, () -> manager.approve(instanceId));
}
@Test
public void testAutomaticNode() {
FlowableManager manager = flowableManager();
FlowableNode node = new FlowableNode(
"2733d930-7a4b-491e-b1ca-4d5811435e9f",
"自动节点",
"自动节点",
FlowableNode.Type.AUTOMATIC,
"com.lanyuanxiaoyao.flowable.core.SimpleAutoAction",
null
);
manager.create(node);
String instanceId = manager.start(node.getNodeId());
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
}
}

View File

@@ -0,0 +1,90 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
import com.lanyuanxiaoyao.flowable.core.helper.StringHelper;
import com.lanyuanxiaoyao.flowable.core.model.FlowableHistory;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 内存存储
*
* @author lanyuanxiaoyao
* @version 20241231
*/
public class InMemoryFlowableRepository implements FlowableRepository {
private static final Map<String, FlowableNode> nodes = new HashMap<>();
private static final Map<String, FlowableInstance> instances = new HashMap<>();
private static final Map<String, List<FlowableHistory>> histories = new HashMap<>();
@Override
public void saveNode(FlowableNode node) {
nodes.put(node.getNodeId(), node);
}
@Override
public void saveNode(List<FlowableNode> nodes) {
for (FlowableNode node : nodes) {
saveNode(node);
}
}
@Override
public FlowableNode getNode(String nodeId) {
return nodes.get(nodeId);
}
@Override
public List<FlowableNode> listNodes() {
return new ArrayList<>(nodes.values());
}
@Override
public void saveInstance(FlowableInstance instance) {
instances.put(instance.getInstanceId(), instance);
}
@Override
public FlowableInstance getInstance(String instantId) {
return instances.getOrDefault(instantId, null);
}
@Override
public List<FlowableInstance> listInstances() {
return new ArrayList<>(instances.values());
}
@Override
public void saveHistory(FlowableHistory history) {
String instanceId = history.getInstanceId();
if (!histories.containsKey(instanceId)) {
histories.put(instanceId, new ArrayList<>());
}
histories.get(instanceId).add(history);
}
@Override
public FlowableHistory getHistory(String historyId) {
return histories.values()
.stream()
.flatMap(List::stream)
.filter(history -> StringHelper.equals(history.getHistoryId(), historyId))
.findFirst()
.orElse(null);
}
@Override
public List<FlowableHistory> listHistories(String instanceId) {
return histories.getOrDefault(instanceId, ListHelper.empty());
}
@Override
public void saveInstanceAndHistory(FlowableInstance instance, FlowableHistory history) {
saveInstance(instance);
saveHistory(history);
}
}

View File

@@ -0,0 +1,17 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
import java.util.Map;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
public class SimpleAutoAction implements FlowableNode.AutoAction {
@Override
public FlowableAction action(FlowableInstance instance, FlowableNode node, Map<String, Object> metadata) {
return FlowableAction.APPROVE;
}
}

View File

@@ -0,0 +1,21 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
import java.util.UUID;
import lombok.SneakyThrows;
/**
* @author lanyuanxiaoyao
* @version 20241231
*/
public class SimpleFlowableManager extends FlowableManager {
public SimpleFlowableManager() {
super(new InMemoryFlowableRepository(), () -> UUID.randomUUID().toString());
}
@SneakyThrows
@Override
protected <T> T createBean(String classpath) {
return (T) Class.forName(classpath).newInstance();
}
}

View File

@@ -0,0 +1,15 @@
package com.lanyuanxiaoyao.flowable.core;
import com.lanyuanxiaoyao.flowable.core.test.TestFlowableManager;
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
/**
* @author lanyuanxiaoyao
* @version 20250102
*/
public class TestSimpleFlowableManager extends TestFlowableManager {
@Override
protected FlowableManager flowableManager() {
return new SimpleFlowableManager();
}
}

56
pom.xml
View File

@@ -6,26 +6,54 @@
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>flowable</artifactId>
<version>1.0-SNAPSHOT</version>
<version>1.0.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>flowable-core</module>
<module>adapter/flowable-spring-boot-jpa-starter</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>2.6.15</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.34</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.lanyuanxiaoyao</groupId>
<artifactId>flowable-core</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
<version>5.8.32</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring-boot.version}</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

View File

@@ -1,149 +0,0 @@
package com.lanyuanxiaoyao.flowable.model;
import com.lanyuanxiaoyao.flowable.node.FlowNode;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import lombok.Data;
/**
* 流程实例类
* 包含流程定义和执行状态
*/
@Data
public class Flow {
/**
* 流程实例ID
*/
private String id;
/**
* 流程名称
*/
private String name;
/**
* 流程描述
*/
private String description;
/**
* 流程节点列表
* 按照列表顺序依次执行,每个节点都是一个可执行的审批操作
*/
private List<FlowNode> nodes;
/**
* 当前执行到的节点ID
*/
private String currentNode;
/**
* 当前流程状态
*/
private FlowStatus status;
/**
* 流程上下文变量
* 用于存储流程执行过程中的数据,实现节点间的数据传递
* 这些数据会随着流程实例一起持久化
*/
private Map<String, Object> contextVariables;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 最后更新时间
*/
private LocalDateTime updateTime;
public Flow() {
this.nodes = new ArrayList<>();
this.contextVariables = new HashMap<>();
this.status = FlowStatus.PENDING;
this.currentNode = null;
}
/**
* 添加一个新的流程节点
*
* @param node 要添加的流程节点
*/
public void addNode(FlowNode node) {
nodes.add(node);
}
/**
* 获取当前节点
*
* @return 当前节点对象
*/
public FlowNode getCurrentNodeObject() {
return nodes.stream()
.filter(node -> node.getNodeId().equals(currentNode))
.findFirst()
.orElseThrow(() -> new IllegalStateException("找不到当前节点"));
}
/**
* 获取下一个节点
*
* @return 下一个节点对象如果已经是最后一个节点则返回null
*/
public FlowNode getNextNode() {
for (int i = 0; i < nodes.size() - 1; i++) {
if (nodes.get(i).getNodeId().equals(currentNode)) {
return nodes.get(i + 1);
}
}
return null;
}
/**
* 判断当前是否是最后一个节点
*
* @return true表示是最后一个节点
*/
public boolean isLastNode() {
return nodes.get(nodes.size() - 1).getNodeId().equals(currentNode);
}
/**
* 移动到下一个节点
*
* @throws IllegalStateException 如果已经是最后一个节点
*/
public void moveToNextNode() {
FlowNode nextNode = getNextNode();
if (nextNode == null) {
throw new IllegalStateException("已经是最后一个节点");
}
currentNode = nextNode.getNodeId();
}
/**
* 创建流程上下文
* 从持久化的上下<E4B88A><E4B88B><EFBFBD>变量中恢复数据
*/
public FlowContext createContext() {
FlowContext context = new FlowContext();
context.setFlowId(id);
// 使用新的 Map 避免直接修改存储的数据
context.setVariables(new HashMap<>(contextVariables));
return context;
}
/**
* 保存上下文变量
* 将变量持久化到流程实例中
*/
public void saveContext(FlowContext context) {
// 使用新的 Map 保存数据的副本
this.contextVariables = new HashMap<>(context.getVariables());
}
}

View File

@@ -1,44 +0,0 @@
package com.lanyuanxiaoyao.flowable.model;
import java.util.HashMap;
import java.util.Map;
import lombok.Data;
/**
* 流程上下文
* 用于在节点间传递数据
*/
@Data
public class FlowContext {
/**
* 流程ID
*/
private String flowId;
/**
* 上下文变量
*/
private Map<String, Object> variables;
public FlowContext() {
this.variables = new HashMap<>();
}
/**
* 获取指定类型的变量值
*/
public <T> T getVariable(String key, Class<T> type) {
Object value = variables.get(key);
if (value == null) {
return null;
}
return type.cast(value);
}
/**
* 设置变量值
*/
public void setVariable(String key, Object value) {
variables.put(key, value);
}
}

View File

@@ -1,35 +0,0 @@
package com.lanyuanxiaoyao.flowable.model;
/**
* 流程状态枚举
* 用于表示流程实例的当前状态
*/
public enum FlowStatus {
/**
* 进行中:流程正在执行中
*/
PENDING("进行中"),
/**
* 已通过:流程已经完成并通过
*/
APPROVED("已通过"),
/**
* 已拒绝:流程被拒绝
*/
REJECTED("已拒绝");
/**
* 状态的中文描述
*/
private final String description;
FlowStatus(String description) {
this.description = description;
}
public String getDescription() {
return description;
}
}

View File

@@ -1,21 +0,0 @@
package com.lanyuanxiaoyao.flowable.node;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public abstract class AbstractFlowNode implements FlowNode {
private final String nodeId;
@Override
public void onApprove(FlowContext context) {
// 默认实现为空
}
@Override
public void onReject(FlowContext context) {
// 默认实现为空
}
}

View File

@@ -1,24 +0,0 @@
package com.lanyuanxiaoyao.flowable.node;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
/**
* 流程节点接口
* 定义节点的基本操作
*/
public interface FlowNode {
/**
* 获取节点ID
*/
String getNodeId();
/**
* 处理通过操作
*/
void onApprove(FlowContext context);
/**
* 处理拒绝操作
*/
void onReject(FlowContext context);
}

View File

@@ -1,13 +0,0 @@
package com.lanyuanxiaoyao.flowable.node;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
/**
* 简单流程节点
* 仅用于演示基本的审批流程
*/
public class SimpleFlowNode extends AbstractFlowNode {
public SimpleFlowNode(String nodeId) {
super(nodeId);
}
}

View File

@@ -1,36 +0,0 @@
package com.lanyuanxiaoyao.flowable.node;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
/**
* 系统节点基类
* 提供自动审批功能
*/
public abstract class SystemFlowNode extends AbstractFlowNode {
protected SystemFlowNode(String nodeId) {
super(nodeId);
}
/**
* 判断是否自动通过
*
* @param context 流程上下文
* @return true表示自动通过false表示自动拒绝
*/
public abstract boolean autoApprove(FlowContext context);
/**
* 获取拒绝原因
*
* @param context 流程上下文
* @return 拒绝原因
*/
protected abstract String getRejectionReason(FlowContext context);
@Override
public void onReject(FlowContext context) {
// 设置拒绝原因到上下文
String rejectionReason = getRejectionReason(context);
context.setVariable("systemComment", rejectionReason);
}
}

View File

@@ -1,11 +0,0 @@
package com.lanyuanxiaoyao.flowable.repository;
import com.lanyuanxiaoyao.flowable.model.Flow;
public interface FlowRepository {
Flow save(Flow flow);
Flow update(Flow flow);
Flow findById(String id);
}

View File

@@ -1,33 +0,0 @@
package com.lanyuanxiaoyao.flowable.repository;
import com.lanyuanxiaoyao.flowable.model.Flow;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MemoryFlowRepository implements FlowRepository {
private final Map<String, Flow> flows = new ConcurrentHashMap<>();
@Override
public Flow save(Flow flow) {
if (flow.getId() == null) {
flow.setId(UUID.randomUUID().toString());
}
flows.put(flow.getId(), flow);
return flow;
}
@Override
public Flow update(Flow flow) {
if (flow.getId() == null || !flows.containsKey(flow.getId())) {
throw new IllegalArgumentException("找不到对应的流程实例");
}
flows.put(flow.getId(), flow);
return flow;
}
@Override
public Flow findById(String id) {
return flows.get(id);
}
}

View File

@@ -1,164 +0,0 @@
package com.lanyuanxiaoyao.flowable.service;
import com.lanyuanxiaoyao.flowable.model.Flow;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
import com.lanyuanxiaoyao.flowable.model.FlowStatus;
import com.lanyuanxiaoyao.flowable.node.FlowNode;
import com.lanyuanxiaoyao.flowable.node.SystemFlowNode;
import com.lanyuanxiaoyao.flowable.repository.FlowRepository;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import lombok.RequiredArgsConstructor;
/**
* 流程服务类
* 提供流程的创建、启动、审批等核心功能
*/
@RequiredArgsConstructor
public class FlowService {
private final FlowRepository flowRepository;
/**
* 启动一个新的流程实例
*
* @throws IllegalStateException 如果流程已经启动或已经结束
*/
public Flow startFlow(String flowId) {
Flow flow = findFlowById(flowId);
// 检查流程状态
if (!FlowStatus.PENDING.equals(flow.getStatus()) ||
flow.getCurrentNode() != null) { // 使用当前节点是否为空来判断是否已启动
throw new IllegalStateException("流程已经启动或已经结束");
}
// 设置初始节点
if (!flow.getNodes().isEmpty()) {
flow.setCurrentNode(flow.getNodes().get(0).getNodeId());
}
flow.setUpdateTime(LocalDateTime.now());
// 保存流程实例
flow = flowRepository.update(flow);
// 如果第一个节点是系统节点,自动执行审批
if (flow.getCurrentNodeObject() instanceof SystemFlowNode) {
return executeNode(flow);
}
return flow;
}
/**
* 审批通过当前节点
*/
public Flow approve(String flowId) {
Flow flow = findFlowById(flowId);
validateFlowNotCompleted(flow);
FlowContext context = flow.createContext();
FlowNode currentNode = flow.getCurrentNodeObject();
currentNode.onApprove(context);
if (flow.isLastNode()) {
flow.setStatus(FlowStatus.APPROVED);
return updateFlow(flow, context);
} else {
flow = updateFlow(flow, context); // 先保存当前节点的<E782B9><E79A84><EFBFBD>
flow.moveToNextNode();
return executeNode(flow); // 执行下一个节点(可能是系统节点)
}
}
/**
* 拒绝当前节点,结束流程
*/
public Flow reject(String flowId) {
Flow flow = findFlowById(flowId);
validateFlowNotCompleted(flow);
FlowContext context = flow.createContext();
flow.getCurrentNodeObject().onReject(context);
flow.saveContext(context); // 保存拒绝操作的上下文变量
flow.setStatus(FlowStatus.REJECTED);
return updateFlow(flow, context);
}
/**
* 获取流程实例
*/
public Flow getFlow(String flowId) {
return findFlowById(flowId);
}
// 私有辅助方法
private void validateFlowNodes(Flow flow) {
if (flow.getNodes() == null || flow.getNodes().isEmpty()) {
throw new IllegalArgumentException("流程节点不能为空");
}
}
private void initializeFlow(Flow flow) {
flow.setCreateTime(LocalDateTime.now());
flow.setUpdateTime(LocalDateTime.now());
}
private Flow findFlowById(String flowId) {
Flow flow = flowRepository.findById(flowId);
if (flow == null) {
throw new IllegalArgumentException("找不到对应的流程");
}
return flow;
}
private void validateFlowNotCompleted(Flow flow) {
if (FlowStatus.APPROVED.equals(flow.getStatus()) ||
FlowStatus.REJECTED.equals(flow.getStatus())) {
throw new IllegalStateException("当前流程已经结束");
}
}
private Flow executeNode(Flow flow) {
FlowContext context = flow.createContext();
FlowNode currentNode = flow.getCurrentNodeObject();
// 如果是系统节点,自动执行审批
if (currentNode instanceof SystemFlowNode) {
return handleSystemNode(flow, currentNode, context);
}
return updateFlow(flow, context);
}
private Flow handleSystemNode(Flow flow, FlowNode node, FlowContext context) {
SystemFlowNode systemNode = (SystemFlowNode) node;
if (systemNode.autoApprove(context)) {
// 自动通过
systemNode.onApprove(context);
flow = updateFlow(flow, context); // 先保存当前状态
if (!flow.isLastNode()) {
flow.moveToNextNode();
return executeNode(flow); // 执行下一个节点
} else {
flow.setStatus(FlowStatus.APPROVED);
return updateFlow(flow, context);
}
} else {
// 自动拒绝
systemNode.onReject(context);
flow.setStatus(FlowStatus.REJECTED);
return updateFlow(flow, context);
}
}
private Flow updateFlow(Flow flow, FlowContext context) {
flow.saveContext(context);
flow.setUpdateTime(LocalDateTime.now());
return flowRepository.update(flow);
}
}

View File

@@ -1,299 +0,0 @@
package com.lanyuanxiaoyao.flowable;
import com.lanyuanxiaoyao.flowable.model.Flow;
import com.lanyuanxiaoyao.flowable.model.FlowStatus;
import com.lanyuanxiaoyao.flowable.node.LeaveRequestNode;
import com.lanyuanxiaoyao.flowable.node.LeaveSystemCheckNode;
import com.lanyuanxiaoyao.flowable.node.ManagerApprovalNode;
import com.lanyuanxiaoyao.flowable.repository.FlowRepository;
import com.lanyuanxiaoyao.flowable.repository.MemoryFlowRepository;
import com.lanyuanxiaoyao.flowable.service.FlowService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("流程服务测试")
class FlowServiceTest {
private FlowService flowService;
private Flow leaveFlow;
private Flow longLeaveFlow; // 新增长期请假流程
@BeforeEach
void setUp() {
// 初始化仓储<E4BB93><E582A8><EFBFBD>
FlowRepository flowRepository = new MemoryFlowRepository();
flowService = new FlowService(flowRepository);
// 创建标准请假流程3天
leaveFlow = new Flow();
leaveFlow.setName("标准请假流程");
leaveFlow.setDescription("3天以内的请假流程");
leaveFlow.addNode(new LeaveRequestNode(3, "年假"));
leaveFlow.addNode(new LeaveSystemCheckNode(5));
leaveFlow.addNode(new ManagerApprovalNode());
leaveFlow = flowRepository.save(leaveFlow);
// 创建长期请假流程7天
longLeaveFlow = new Flow();
longLeaveFlow.setName("长期请假流程");
longLeaveFlow.setDescription("7天的请假流程");
longLeaveFlow.addNode(new LeaveRequestNode(7, "年假"));
longLeaveFlow.addNode(new LeaveSystemCheckNode(5));
longLeaveFlow.addNode(new ManagerApprovalNode());
longLeaveFlow = flowRepository.save(longLeaveFlow);
}
@Nested
@DisplayName("流程启动测试")
class FlowStartTest {
@Test
@DisplayName("找不到流程时启动应抛出异常")
void shouldThrowExceptionWhenFlowNotFound() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> flowService.startFlow("non-existent-id")
);
assertEquals("找不到对应的流程", exception.getMessage());
}
@Test
@DisplayName("启动流程时应正确初始化状态")
void shouldInitializeStateWhenStartFlow() {
// 获取流程实例验证初始状态
Flow flow = flowService.getFlow(leaveFlow.getId());
// 验证初始状态
assertNotNull(flow.getId(), "流程ID不应为空");
assertEquals(FlowStatus.PENDING, flow.getStatus(), "初始状态应为PENDING");
assertTrue(flow.getContextVariables().isEmpty(), "上下文变量应为空");
// 启动流程
flow = flowService.startFlow(leaveFlow.getId());
// 验证启动后状态
assertEquals(FlowStatus.PENDING, flow.getStatus(), "启动后状态应为PENDING");
assertEquals("请假申请", flow.getCurrentNode(), "应从第一个节点开始");
assertNotNull(flow.getUpdateTime(), "更新时间不应为空");
// 验证第一个节点执行后的变量
assertEquals(3, flow.getContextVariables().get("days"), "应包含请假天数");
assertEquals("年假", flow.getContextVariables().get("reason"), "应包含请假理由");
}
@Test
@DisplayName("不能重复启动已经结束的流程")
void shouldNotStartCompletedFlow() {
// 先启动并完成一个流程
Flow completedFlow = flowService.startFlow(leaveFlow.getId());
completedFlow = flowService.approve(completedFlow.getId());
final Flow finalFlow = flowService.approve(completedFlow.getId());
// 尝试重新启动
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> flowService.startFlow(finalFlow.getId())
);
assertEquals("流程已经启动或已经结束", exception.getMessage());
}
@Test
@DisplayName("不能重复启动已经拒绝的流程")
void shouldNotStartRejectedFlow() {
// 先启动并拒绝一个流程
Flow rejectedFlow = flowService.startFlow(leaveFlow.getId());
final Flow finalFlow = flowService.reject(rejectedFlow.getId());
// 尝试重新启动
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> flowService.startFlow(finalFlow.getId())
);
assertEquals("流程已经启动或已经结束", exception.getMessage());
}
@Test
@DisplayName("不能重复启动正在进行的流程")
void shouldNotStartRunningFlow() {
// 先启动流程
final Flow runningFlow = flowService.startFlow(leaveFlow.getId());
// 尝试重新启动
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> flowService.startFlow(runningFlow.getId())
);
assertEquals("流程已经启动或已经结束", exception.getMessage());
}
}
@Nested
@DisplayName("系统节点审批流程测试")
class SystemNodeFlowTest {
@Test
@DisplayName("请假天数在限制内时应自动通过系统审核")
void shouldAutoApproveWhenLeaveDaysWithinLimit() {
Flow flow = flowService.startFlow(leaveFlow.getId());
// 验证初始状态
assertEquals(FlowStatus.PENDING, flow.getStatus());
assertEquals("请假申请", flow.getCurrentNode());
assertEquals(3, flow.getContextVariables().get("days"));
assertEquals("年假", flow.getContextVariables().get("reason"));
// 提交请假申请
flow = flowService.approve(flow.getId());
// 验证系统审核通过
assertEquals(FlowStatus.PENDING, flow.getStatus());
assertEquals("经理审批", flow.getCurrentNode());
assertEquals("系统自动通过", flow.getContextVariables().get("systemComment"));
// 经理审批
flow = flowService.approve(flow.getId());
// 验证流程完成
assertEquals(FlowStatus.APPROVED, flow.getStatus());
assertEquals("同意", flow.getContextVariables().get("managerComment"));
}
@Test
@DisplayName("请假天数超出限制时应被系统自动拒绝")
void shouldAutoRejectWhenLeaveDaysExceedLimit() {
Flow flow = flowService.startFlow(longLeaveFlow.getId());
// 验证初始状态
assertEquals(FlowStatus.PENDING, flow.getStatus());
assertEquals("请假申请", flow.getCurrentNode());
assertEquals(7, flow.getContextVariables().get("days"));
// 提交请假申请
flow = flowService.approve(flow.getId());
// 验证系统拒绝
assertEquals(FlowStatus.REJECTED, flow.getStatus());
assertEquals("请假天数(7)超过系统限制(5),需要额外审批",
flow.getContextVariables().get("systemComment"));
}
}
@Nested
@DisplayName("人工审批流程测试")
class ManualApprovalTest {
@Test
@DisplayName("经理应能直接拒绝请假申请")
void shouldAllowManagerToReject() {
Flow flow = flowService.startFlow(leaveFlow.getId());
// 通过请假申请和系统审核
flow = flowService.approve(flow.getId());
// 经理拒绝
flow = flowService.reject(flow.getId());
assertEquals(FlowStatus.REJECTED, flow.getStatus());
assertEquals("经理审批", flow.getCurrentNode());
}
@Test
@DisplayName("已完成的流程不能再次审批")
void shouldNotAllowApproveCompletedFlow() {
Flow completedFlow = flowService.startFlow(leaveFlow.getId());
completedFlow = flowService.approve(completedFlow.getId());
final Flow finalFlow = flowService.approve(completedFlow.getId());
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> flowService.approve(finalFlow.getId())
);
assertEquals("当前流程已经结束", exception.getMessage());
}
@Test
@DisplayName("已拒绝的流程不能再次审批")
void shouldNotAllowApproveRejectedFlow() {
Flow rejectedFlow = flowService.startFlow(leaveFlow.getId());
final Flow finalFlow = flowService.reject(rejectedFlow.getId());
IllegalStateException exception = assertThrows(
IllegalStateException.class,
() -> flowService.approve(finalFlow.getId())
);
assertEquals("当前流程已经结束", exception.getMessage());
}
}
@Nested
@DisplayName("流程变量测试")
class FlowVariableTest {
@Test
@DisplayName("流程变量应在节点间正确传递")
void shouldPassVariablesBetweenNodes() {
Flow flow = flowService.startFlow(leaveFlow.getId());
// 验证请假申请节点设置的变量
assertEquals(3, flow.getContextVariables().get("days"));
assertEquals("年假", flow.getContextVariables().get("reason"));
// 提交请假申请,验证系统审核节点的变量
flow = flowService.approve(flow.getId());
assertEquals("系统自动通过", flow.getContextVariables().get("systemComment"));
// 经理审批,验证所有变量都被保留
flow = flowService.approve(flow.getId());
assertEquals(3, flow.getContextVariables().get("days"));
assertEquals("年假", flow.getContextVariables().get("reason"));
assertEquals("系统自动通过", flow.getContextVariables().get("systemComment"));
assertEquals("同意", flow.getContextVariables().get("managerComment"));
}
@Test
@DisplayName("流程变量应该在重新获取流程时保持不变")
void shouldPersistVariablesWhenReloadFlow() {
Flow flow = flowService.startFlow(leaveFlow.getId());
// 提交请假申请
flow = flowService.approve(flow.getId());
String flowId = flow.getId();
// 重新获取流程实例
Flow reloadedFlow = flowService.getFlow(flowId);
// 验证上下文变量被正确保存
assertEquals(3, reloadedFlow.getContextVariables().get("days"), "请假天数应被保存");
assertEquals("年假", reloadedFlow.getContextVariables().get("reason"), "请假理由应被保存");
assertEquals("系统自动通过", reloadedFlow.getContextVariables().get("systemComment"), "系统审核结果应被保存");
}
@Test
@DisplayName("流程变量应该在流程结束后仍然保持")
void shouldKeepVariablesAfterFlowCompleted() {
Flow flow = flowService.startFlow(leaveFlow.getId());
// 完成整个流程
flow = flowService.approve(flow.getId());
flow = flowService.approve(flow.getId());
String flowId = flow.getId();
// 重新获取已完成的流程
Flow completedFlow = flowService.getFlow(flowId);
// 验证所有变量都被保存
assertEquals(FlowStatus.APPROVED, completedFlow.getStatus(), "流程状态应为已通过");
assertEquals(3, completedFlow.getContextVariables().get("days"), "请假天数应被保存");
assertEquals("年假", completedFlow.getContextVariables().get("reason"), "请假理由应被保存");
assertEquals("系统自动通过", completedFlow.getContextVariables().get("systemComment"), "系统审核结果应被保存");
assertEquals("同意", completedFlow.getContextVariables().get("managerComment"), "经理审批结果应被保存");
}
}
}

View File

@@ -1,31 +0,0 @@
package com.lanyuanxiaoyao.flowable.node;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
/**
* 请假申请节点
* 测试用例中的示例节点,用于演示流程节点的实现
*/
public class LeaveRequestNode extends AbstractFlowNode {
private final int days;
private final String reason;
public LeaveRequestNode(int days, String reason) {
super("请假申请");
this.days = days;
this.reason = reason;
}
@Override
public void onApprove(FlowContext context) {
// 设置请假信息到上下文
context.setVariable("days", days);
context.setVariable("reason", reason);
context.setVariable("submitTime", System.currentTimeMillis());
}
@Override
public void onReject(FlowContext context) {
context.setVariable("rejectReason", "申请已撤销");
}
}

View File

@@ -1,34 +0,0 @@
package com.lanyuanxiaoyao.flowable.node;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
/**
* 系统自动审核节点
* 测试用例中的示例节点,用于演示系统节点的实现
*/
public class LeaveSystemCheckNode extends SystemFlowNode {
private final int maxDays;
public LeaveSystemCheckNode(int maxDays) {
super("系统审核");
this.maxDays = maxDays;
}
@Override
public boolean autoApprove(FlowContext context) {
int days = (int) context.getVariables().get("days");
return days <= maxDays;
}
@Override
public void onApprove(FlowContext context) {
context.setVariable("systemComment", "系统自动通过");
context.setVariable("systemCheckTime", System.currentTimeMillis());
}
@Override
protected String getRejectionReason(FlowContext context) {
int days = (int) context.getVariables().get("days");
return String.format("请假天数(%d)超过系统限制(%d),需要额外审批", days, maxDays);
}
}

View File

@@ -1,25 +0,0 @@
package com.lanyuanxiaoyao.flowable.node;
import com.lanyuanxiaoyao.flowable.model.FlowContext;
/**
* 经理审批节点
* 测试用例中的示例节点,用于演示人工审批节点的实现
*/
public class ManagerApprovalNode extends AbstractFlowNode {
public ManagerApprovalNode() {
super("经理审批");
}
@Override
public void onApprove(FlowContext context) {
context.setVariable("managerComment", "同意");
context.setVariable("approveTime", System.currentTimeMillis());
}
@Override
public void onReject(FlowContext context) {
context.setVariable("managerComment", "不同意");
context.setVariable("rejectTime", System.currentTimeMillis());
}
}