Compare commits
37 Commits
07a997246b
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| e1d50571d1 | |||
| 6ef29b05c5 | |||
| 96dfcea7b8 | |||
| b48903c8af | |||
| dcf91a7e1e | |||
| 77567d997d | |||
| 1cc8bb5618 | |||
| d8f58ea2b5 | |||
| bf0e7e47be | |||
| b56555923a | |||
| 53c4be6e3e | |||
| 935793d5b6 | |||
| 2054f5802b | |||
| 28ef67d630 | |||
| 2c75cad680 | |||
| dddc9d7171 | |||
| 465cc8cd3f | |||
| 82af04a4c6 | |||
| f4e8896763 | |||
| 6d869ef915 | |||
| bca3933790 | |||
| 743e2db17d | |||
| 45ce5a2615 | |||
| 57bc3fb2a8 | |||
| b82c8fe4ea | |||
| 86f702db3e | |||
| b69da9a4e4 | |||
| 040b980a8b | |||
| eff6d6be02 | |||
| 53ab0d731c | |||
| 7885f8e87a | |||
| 07ee530b79 | |||
| 071beee794 | |||
| c3ca027f72 | |||
| 7d88674c66 | |||
| 422e024b7b | |||
| 13b9366346 |
6
.idea/encodings.xml
generated
6
.idea/encodings.xml
generated
@@ -1,6 +1,12 @@
|
||||
<?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$/flowable-example/src/main/java" charset="UTF-8" />
|
||||
<file url="file://$PROJECT_DIR$/flowable-example/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>
|
||||
|
||||
7
.idea/jpa.xml
generated
Normal file
7
.idea/jpa.xml
generated
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JpaBuddyIdeaProjectConfig">
|
||||
<option name="defaultUnitInitialized" value="true" />
|
||||
<option name="renamerInitialized" value="true" />
|
||||
</component>
|
||||
</project>
|
||||
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -11,4 +11,7 @@
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" default="true" project-jdk-name="temurin-1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="jpab" />
|
||||
</component>
|
||||
</project>
|
||||
52
adapter/flowable-spring-boot-jpa-starter/pom.xml
Normal file
52
adapter/flowable-spring-boot-jpa-starter/pom.xml
Normal file
@@ -0,0 +1,52 @@
|
||||
<?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-configuration-processor</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.lanyuanxiaoyao</groupId>
|
||||
<artifactId>flowable-example</artifactId>
|
||||
<scope>test</scope>
|
||||
</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>
|
||||
</project>
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
|
||||
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.repository.FlowableHistoryRepository;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.repository.FlowableInstanceRepository;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.repository.FlowableNodeRepository;
|
||||
import java.util.Map;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* 自动配置
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250103
|
||||
*/
|
||||
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
|
||||
@Configuration
|
||||
@EnableJpaRepositories("com.lanyuanxiaoyao.flowable.jpa.repository")
|
||||
@EnableConfigurationProperties(SpringFlowableConfiguration.class)
|
||||
public class SpringFlowableAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FlowableRepository.class)
|
||||
public SpringFlowableRepository flowableRepository(
|
||||
FlowableNodeRepository flowableNodeRepository,
|
||||
FlowableInstanceRepository flowableInstanceRepository,
|
||||
FlowableHistoryRepository flowableHistoryRepository
|
||||
) {
|
||||
return new SpringFlowableRepository(flowableNodeRepository, flowableInstanceRepository, flowableHistoryRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(FlowableRepository.class)
|
||||
@ConditionalOnMissingBean(FlowableManager.class)
|
||||
public SpringFlowableManager flowableManager(
|
||||
SpringFlowableConfiguration configuration,
|
||||
SpringFlowableRepository repository,
|
||||
ApplicationContext applicationContext
|
||||
) {
|
||||
SpringFlowableManager manager = new SpringFlowableManager(configuration, repository, applicationContext);
|
||||
|
||||
Map<String, SpringFlowableManagerInitializer> initializerMap = applicationContext.getBeansOfType(SpringFlowableManagerInitializer.class);
|
||||
for (SpringFlowableManagerInitializer initializer : initializerMap.values()) {
|
||||
manager = initializer.initial(manager);
|
||||
}
|
||||
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableConfiguration;
|
||||
import lombok.ToString;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 配置类
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250103
|
||||
*/
|
||||
@ToString(callSuper = true)
|
||||
@ConfigurationProperties("flowable")
|
||||
public class SpringFlowableConfiguration extends FlowableConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableHistory;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableInstance;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableNode;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
@Slf4j
|
||||
public class SpringFlowableManager extends FlowableManager {
|
||||
private final SpringFlowableRepository repository;
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
public SpringFlowableManager(SpringFlowableConfiguration configuration, SpringFlowableRepository repository, ApplicationContext applicationContext) {
|
||||
super(configuration, repository);
|
||||
log.info("Configuration: {}", configuration);
|
||||
this.repository = repository;
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> listNodes(Specification<FlowableNode> specification) {
|
||||
return repository.listNodes(specification);
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> listNodes(Specification<FlowableNode> specification, Sort sort) {
|
||||
return repository.listNodes(specification, sort);
|
||||
}
|
||||
|
||||
public Page<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> listNodes(Specification<FlowableNode> specification, Pageable pageable) {
|
||||
return repository.listNodes(specification, pageable);
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> listInstances(Specification<FlowableInstance> specification) {
|
||||
return repository.listInstances(specification);
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> listInstances(Specification<FlowableInstance> specification, Sort sort) {
|
||||
return repository.listInstances(specification, sort);
|
||||
}
|
||||
|
||||
public Page<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> listInstances(Specification<FlowableInstance> specification, Pageable pageable) {
|
||||
return repository.listInstances(specification, pageable);
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> listHistories(String instanceId, Specification<FlowableHistory> specification) {
|
||||
return repository.listHistories(instanceId, specification);
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> listHistories(String instanceId, Specification<FlowableHistory> specification, Sort sort) {
|
||||
return repository.listHistories(instanceId, specification, sort);
|
||||
}
|
||||
|
||||
public Page<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> listHistories(String instanceId, Specification<FlowableHistory> specification, Pageable pageable) {
|
||||
return repository.listHistories(instanceId, specification, pageable);
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
protected <T> T createBean(String classpath) {
|
||||
Class<?> clazz = Class.forName(classpath);
|
||||
T targetObject;
|
||||
try {
|
||||
targetObject = (T) applicationContext.getBean(clazz);
|
||||
} catch (Exception springException) {
|
||||
try {
|
||||
targetObject = (T) clazz.newInstance();
|
||||
} catch (Exception javaException) {
|
||||
throw new IllegalArgumentException(javaException.initCause(springException));
|
||||
}
|
||||
}
|
||||
return targetObject;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String createId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250106
|
||||
*/
|
||||
public interface SpringFlowableManagerInitializer {
|
||||
SpringFlowableManager initial(SpringFlowableManager manager);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.repository.FlowableRepository;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableHistory;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableInstance;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableNode;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.repository.FlowableHistoryRepository;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.repository.FlowableInstanceRepository;
|
||||
import com.lanyuanxiaoyao.flowable.jpa.repository.FlowableNodeRepository;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.StreamSupport;
|
||||
import javax.transaction.Transactional;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.data.jpa.domain.Specification;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
public class SpringFlowableRepository implements FlowableRepository {
|
||||
private final FlowableNodeRepository flowableNodeRepository;
|
||||
private final FlowableInstanceRepository flowableInstanceRepository;
|
||||
private final FlowableHistoryRepository flowableHistoryRepository;
|
||||
|
||||
public SpringFlowableRepository(FlowableNodeRepository flowableNodeRepository, FlowableInstanceRepository flowableInstanceRepository, FlowableHistoryRepository flowableHistoryRepository) {
|
||||
this.flowableNodeRepository = flowableNodeRepository;
|
||||
this.flowableInstanceRepository = flowableInstanceRepository;
|
||||
this.flowableHistoryRepository = flowableHistoryRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsNode(String nodeId) {
|
||||
return flowableNodeRepository.existsById(nodeId);
|
||||
}
|
||||
|
||||
@Transactional(rollbackOn = Exception.class)
|
||||
@Override
|
||||
public void saveNode(com.lanyuanxiaoyao.flowable.core.model.FlowableNode node) {
|
||||
flowableNodeRepository.save(new FlowableNode(node));
|
||||
}
|
||||
|
||||
@Transactional(rollbackOn = Exception.class)
|
||||
@Override
|
||||
public void saveNode(List<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> nodes) {
|
||||
flowableNodeRepository.saveAll(nodes.stream().map(FlowableNode::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.lanyuanxiaoyao.flowable.core.model.FlowableNode getNode(String nodeId) {
|
||||
return flowableNodeRepository.findById(nodeId)
|
||||
.map(FlowableNode::toFlowableNode)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private List<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> toNodes(Iterable<FlowableNode> nodes) {
|
||||
return StreamSupport.stream(nodes.spliterator(), false)
|
||||
.map(FlowableNode::toFlowableNode)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> listNodes() {
|
||||
return toNodes(flowableNodeRepository.findAll());
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> listNodes(Specification<FlowableNode> specification) {
|
||||
return toNodes(flowableNodeRepository.findAll(specification));
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> listNodes(Specification<FlowableNode> specification, Sort sort) {
|
||||
return toNodes(flowableNodeRepository.findAll(specification, sort));
|
||||
}
|
||||
|
||||
public Page<com.lanyuanxiaoyao.flowable.core.model.FlowableNode> listNodes(Specification<FlowableNode> specification, Pageable pageable) {
|
||||
Page<FlowableNode> page = flowableNodeRepository.findAll(specification, pageable);
|
||||
return new PageImpl<>(toNodes(page.getContent()), pageable, page.getTotalElements());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsInstance(String instanceId) {
|
||||
return flowableInstanceRepository.existsById(instanceId);
|
||||
}
|
||||
|
||||
@Transactional(rollbackOn = Exception.class)
|
||||
@Override
|
||||
public void saveInstance(com.lanyuanxiaoyao.flowable.core.model.FlowableInstance instance) {
|
||||
flowableInstanceRepository.save(new FlowableInstance(instance));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.lanyuanxiaoyao.flowable.core.model.FlowableInstance getInstance(String instantId) {
|
||||
return flowableInstanceRepository.findById(instantId)
|
||||
.map(FlowableInstance::toFlowableInstance)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private List<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> toInstances(Iterable<FlowableInstance> instances) {
|
||||
return StreamSupport.stream(instances.spliterator(), false)
|
||||
.map(FlowableInstance::toFlowableInstance)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> listInstances() {
|
||||
return toInstances(flowableInstanceRepository.findAll());
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> listInstances(Specification<FlowableInstance> specification) {
|
||||
return toInstances(flowableInstanceRepository.findAll(specification));
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> listInstances(Specification<FlowableInstance> specification, Sort sort) {
|
||||
return toInstances(flowableInstanceRepository.findAll(specification, sort));
|
||||
}
|
||||
|
||||
public Page<com.lanyuanxiaoyao.flowable.core.model.FlowableInstance> listInstances(Specification<FlowableInstance> specification, Pageable pageable) {
|
||||
Page<FlowableInstance> page = flowableInstanceRepository.findAll(specification, pageable);
|
||||
return new PageImpl<>(toInstances(page.getContent()), pageable, page.getTotalElements());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean existsHistory(String historyId) {
|
||||
return flowableHistoryRepository.existsById(historyId);
|
||||
}
|
||||
|
||||
@Transactional(rollbackOn = Exception.class)
|
||||
@Override
|
||||
public void saveHistory(com.lanyuanxiaoyao.flowable.core.model.FlowableHistory history) {
|
||||
flowableHistoryRepository.save(new FlowableHistory(history));
|
||||
}
|
||||
|
||||
@Override
|
||||
public com.lanyuanxiaoyao.flowable.core.model.FlowableHistory getHistory(String historyId) {
|
||||
return flowableHistoryRepository.findById(historyId)
|
||||
.map(FlowableHistory::toFlowableHistory)
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private List<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> toHistories(Iterable<FlowableHistory> histories) {
|
||||
return StreamSupport.stream(histories.spliterator(), false)
|
||||
.map(FlowableHistory::toFlowableHistory)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> listHistories(String instanceId) {
|
||||
return toHistories(flowableHistoryRepository.findAllByInstanceId(instanceId));
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> listHistories(String instanceId, Specification<FlowableHistory> specification) {
|
||||
return toHistories(
|
||||
flowableHistoryRepository.findAll(
|
||||
(root, query, builder) -> builder.and(
|
||||
builder.equal(root.get("instanceId"), instanceId),
|
||||
specification.toPredicate(root, query, builder)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public List<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> listHistories(String instanceId, Specification<FlowableHistory> specification, Sort sort) {
|
||||
return toHistories(
|
||||
flowableHistoryRepository.findAll(
|
||||
(root, query, builder) -> builder.and(
|
||||
builder.equal(root.get("instanceId"), instanceId),
|
||||
specification.toPredicate(root, query, builder)
|
||||
),
|
||||
sort
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public Page<com.lanyuanxiaoyao.flowable.core.model.FlowableHistory> listHistories(String instanceId, Specification<FlowableHistory> specification, Pageable pageable) {
|
||||
Page<FlowableHistory> page = flowableHistoryRepository.findAll(
|
||||
(root, query, builder) -> builder.and(
|
||||
builder.equal(root.get("instanceId"), instanceId),
|
||||
specification.toPredicate(root, query, builder)
|
||||
),
|
||||
pageable
|
||||
);
|
||||
return new PageImpl<>(toHistories(page.getContent()), pageable, page.getTotalElements());
|
||||
}
|
||||
|
||||
@Transactional(rollbackOn = Exception.class)
|
||||
@Override
|
||||
public void saveInstanceAndHistory(com.lanyuanxiaoyao.flowable.core.model.FlowableInstance instance, com.lanyuanxiaoyao.flowable.core.model.FlowableHistory history) {
|
||||
saveInstance(instance);
|
||||
saveHistory(history);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa.entity;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
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 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(name = "flowable_history")
|
||||
@DynamicUpdate
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class FlowableHistory {
|
||||
@Id
|
||||
private String historyId;
|
||||
@Column(nullable = false)
|
||||
private String instanceId;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private FlowableAction action;
|
||||
private String comment;
|
||||
@CreatedDate
|
||||
private LocalDateTime createdTime;
|
||||
|
||||
public FlowableHistory(com.lanyuanxiaoyao.flowable.core.model.FlowableHistory history) {
|
||||
this.historyId = history.getHistoryId();
|
||||
this.instanceId = history.getInstanceId();
|
||||
this.action = history.getAction();
|
||||
this.comment = history.getComment();
|
||||
this.createdTime = history.getCreatedTime();
|
||||
}
|
||||
|
||||
public com.lanyuanxiaoyao.flowable.core.model.FlowableHistory toFlowableHistory() {
|
||||
return new com.lanyuanxiaoyao.flowable.core.model.FlowableHistory(historyId, instanceId, action, comment, createdTime);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa.entity;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableMetadata;
|
||||
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 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 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(name = "flowable_instance")
|
||||
@DynamicUpdate
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class FlowableInstance {
|
||||
@Id
|
||||
private String instanceId;
|
||||
@Lob
|
||||
private byte[] metadata;
|
||||
@Column(nullable = false)
|
||||
private String currentNodeId;
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private com.lanyuanxiaoyao.flowable.core.model.FlowableInstance.Status status = com.lanyuanxiaoyao.flowable.core.model.FlowableInstance.Status.RUNNING;
|
||||
|
||||
@CreatedDate
|
||||
private LocalDateTime createdTime;
|
||||
@LastModifiedDate
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
private String extra;
|
||||
|
||||
@SneakyThrows
|
||||
public FlowableInstance(com.lanyuanxiaoyao.flowable.core.model.FlowableInstance instance) {
|
||||
this.instanceId = instance.getInstanceId();
|
||||
this.metadata = objectToBytes(instance.getMetadata());
|
||||
this.currentNodeId = instance.getCurrentNodeId();
|
||||
this.status = instance.getStatus();
|
||||
this.extra = instance.getExtra();
|
||||
this.createdTime = instance.getCreatedTime();
|
||||
this.updatedTime = instance.getUpdatedTime();
|
||||
}
|
||||
|
||||
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 com.lanyuanxiaoyao.flowable.core.model.FlowableInstance toFlowableInstance() {
|
||||
return com.lanyuanxiaoyao.flowable.core.model.FlowableInstance.builder()
|
||||
.instanceId(instanceId)
|
||||
.currentNodeId(currentNodeId)
|
||||
.metadata((FlowableMetadata) bytesToObject(this.metadata))
|
||||
.status(status)
|
||||
.createdTime(createdTime)
|
||||
.updatedTime(updatedTime)
|
||||
.extra(extra)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa.entity;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
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 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(name = "flowable_node")
|
||||
@DynamicUpdate
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class FlowableNode {
|
||||
@Id
|
||||
private String nodeId;
|
||||
@Column(nullable = false)
|
||||
private String name;
|
||||
private String description;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
private com.lanyuanxiaoyao.flowable.core.model.FlowableNode.Type type;
|
||||
@Column(nullable = false)
|
||||
private String handler;
|
||||
@ElementCollection(fetch = javax.persistence.FetchType.EAGER)
|
||||
@CollectionTable(name = "flowable_node_targets", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
|
||||
private Map<FlowableAction, String> targets;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String accessor;
|
||||
@ElementCollection(fetch = javax.persistence.FetchType.EAGER)
|
||||
@CollectionTable(name = "flowable_node_listeners", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
|
||||
private List<String> listeners;
|
||||
|
||||
@CreatedDate
|
||||
private LocalDateTime createdTime;
|
||||
@LastModifiedDate
|
||||
private LocalDateTime updatedTime;
|
||||
|
||||
public FlowableNode(com.lanyuanxiaoyao.flowable.core.model.FlowableNode node) {
|
||||
this.nodeId = node.getNodeId();
|
||||
this.name = node.getName();
|
||||
this.description = node.getDescription();
|
||||
this.type = node.getType();
|
||||
this.handler = node.getHandler();
|
||||
this.targets = node.getTargets();
|
||||
this.accessor = node.getAccessor();
|
||||
this.listeners = node.getListeners();
|
||||
this.createdTime = node.getCreatedTime();
|
||||
this.updatedTime = node.getUpdatedTime();
|
||||
}
|
||||
|
||||
public com.lanyuanxiaoyao.flowable.core.model.FlowableNode toFlowableNode() {
|
||||
return com.lanyuanxiaoyao.flowable.core.model.FlowableNode.builder()
|
||||
.nodeId(nodeId)
|
||||
.name(name)
|
||||
.description(description)
|
||||
.type(type)
|
||||
.handler(handler)
|
||||
.targets(targets)
|
||||
.accessor(accessor)
|
||||
.listeners(listeners)
|
||||
.createdTime(createdTime)
|
||||
.updatedTime(updatedTime)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa.repository;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableHistory;
|
||||
import java.util.List;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250102
|
||||
*/
|
||||
@Repository
|
||||
public interface FlowableHistoryRepository extends JpaRepository<FlowableHistory, String>, JpaSpecificationExecutor<FlowableHistory>, QueryByExampleExecutor<FlowableHistory> {
|
||||
List<FlowableHistory> findAllByInstanceId(String instanceId);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa.repository;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableInstance;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250102
|
||||
*/
|
||||
@Repository
|
||||
public interface FlowableInstanceRepository extends JpaRepository<FlowableInstance, String>, JpaSpecificationExecutor<FlowableInstance>, QueryByExampleExecutor<FlowableInstance> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa.repository;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.jpa.entity.FlowableNode;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
|
||||
import org.springframework.data.repository.query.QueryByExampleExecutor;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250102
|
||||
*/
|
||||
@Repository
|
||||
public interface FlowableNodeRepository extends JpaRepository<FlowableNode, String>, JpaSpecificationExecutor<FlowableNode>, QueryByExampleExecutor<FlowableNode> {
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.lanyuanxiaoyao.flowable.jpa.SpringFlowableAutoConfiguration
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableConfiguration;
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableHandler;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
|
||||
import javax.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250102
|
||||
*/
|
||||
@Slf4j
|
||||
public class SimpleAutoAction implements FlowableHandler {
|
||||
@Resource
|
||||
private FlowableManager flowableManager;
|
||||
|
||||
@Override
|
||||
public FlowableAction handle(FlowableConfiguration configuration, FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("Initial with spring: {}", flowableManager.listNodes());
|
||||
return FlowableAction.APPROVE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
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.FlowableListener;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
|
||||
import javax.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250106
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class SimpleListener implements FlowableListener {
|
||||
@Resource
|
||||
private FlowableManager flowableManager;
|
||||
|
||||
@Override
|
||||
public void onActionStart(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("onActionStart with spring: {}", flowableManager.listNodes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAction(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("onAction with spring: {}", flowableManager.listNodes());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionComplete(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("onActionComplete with spring: {}", flowableManager.listNodes());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.lanyuanxiaoyao.flowable.jpa;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableHandler;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableListener;
|
||||
import com.lanyuanxiaoyao.flowable.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;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends FlowableHandler> getHandler() {
|
||||
return SimpleAutoAction.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends FlowableListener> getListenerClass() {
|
||||
return SimpleListener.class;
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
21
flowable-core/pom.xml
Normal file
21
flowable-core/pom.xml
Normal file
@@ -0,0 +1,21 @@
|
||||
<?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.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -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<>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.helper;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
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 <K, V> Map<K, V> empty() {
|
||||
return new HashMap<>(0);
|
||||
}
|
||||
|
||||
public static boolean isEmpty(Map<?, ?> map) {
|
||||
return Objects.isNull(map) || map.isEmpty();
|
||||
}
|
||||
|
||||
public static boolean isNotEmpty(Map<?, ?> map) {
|
||||
return !isEmpty(map);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
public static <K, V> Map<K, V> concat(Map<K, V>... maps) {
|
||||
Map<K, V> result = new HashMap<>();
|
||||
for (Map<K, V> map : maps) {
|
||||
if (Objects.nonNull(map)) {
|
||||
result.putAll(map);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.manager;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 配置项
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250103
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class FlowableConfiguration {
|
||||
/**
|
||||
* 在上下文中查询权限的key
|
||||
*/
|
||||
@Builder.Default
|
||||
private String accessorKey = Constants.DEFAULT_ACCESSOR_KEY;
|
||||
|
||||
public interface Constants {
|
||||
String DEFAULT_ACCESSOR_KEY = "accessor";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
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.FlowableAccessor;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableHandler;
|
||||
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.FlowableMetadata;
|
||||
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 static final String DEFAULT_APPROVE_COMMENT = "审批通过";
|
||||
private final FlowableConfiguration configuration;
|
||||
private final FlowableRepository repository;
|
||||
|
||||
public FlowableManager(FlowableConfiguration configuration, FlowableRepository repository) {
|
||||
this.configuration = configuration;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
public boolean existsNode(String nodeId) {
|
||||
return repository.existsNode(nodeId);
|
||||
}
|
||||
|
||||
public List<FlowableNode> listNodes() {
|
||||
return repository.listNodes();
|
||||
}
|
||||
|
||||
public FlowableNode getNode(String nodeId) {
|
||||
return repository.getNode(nodeId);
|
||||
}
|
||||
|
||||
public boolean existsInstance(String instanceId) {
|
||||
return repository.existsInstance(instanceId);
|
||||
}
|
||||
|
||||
public List<FlowableInstance> listInstances() {
|
||||
return repository.listInstances();
|
||||
}
|
||||
|
||||
public FlowableInstance getInstance(String instantId) {
|
||||
return repository.getInstance(instantId);
|
||||
}
|
||||
|
||||
public boolean existsHistory(String historyId) {
|
||||
return repository.existsHistory(historyId);
|
||||
}
|
||||
|
||||
public List<FlowableHistory> listHistories(String instanceId) {
|
||||
return repository.listHistories(instanceId);
|
||||
}
|
||||
|
||||
public FlowableHistory getHistory(String historyId) {
|
||||
return repository.getHistory(historyId);
|
||||
}
|
||||
|
||||
public void create(FlowableNode... node) {
|
||||
repository.saveNode(ListHelper.of(node));
|
||||
}
|
||||
|
||||
public String start(String nodeId) {
|
||||
return start(nodeId, MapHelper.empty());
|
||||
}
|
||||
|
||||
public String start(String nodeId, Map<String, Object> metadata) {
|
||||
return start(nodeId, metadata, null);
|
||||
}
|
||||
|
||||
public String start(String nodeId, Map<String, Object> metadata, String extra) {
|
||||
FlowableNode node = repository.getNode(nodeId);
|
||||
FlowableInstance instance = FlowableInstance.builder()
|
||||
.instanceId(createId())
|
||||
.currentNodeId(node.getNodeId())
|
||||
.metadata(new FlowableMetadata(metadata))
|
||||
.extra(extra)
|
||||
.build();
|
||||
repository.saveInstance(instance);
|
||||
if (FlowableNode.Type.AUTOMATIC.equals(node.getType())) {
|
||||
automaticAction(instance);
|
||||
}
|
||||
return instance.getInstanceId();
|
||||
}
|
||||
|
||||
public void approve(String instanceId) {
|
||||
approve(instanceId, DEFAULT_APPROVE_COMMENT);
|
||||
}
|
||||
|
||||
public void approve(String instanceId, String comment) {
|
||||
manualAction(instanceId, FlowableAction.APPROVE, comment);
|
||||
}
|
||||
|
||||
public void approve(String instanceId, Map<String, Object> metadata) {
|
||||
manualAction(instanceId, FlowableAction.APPROVE, DEFAULT_APPROVE_COMMENT, metadata);
|
||||
}
|
||||
|
||||
public void approve(String instanceId, String comment, Map<String, Object> metadata) {
|
||||
manualAction(instanceId, FlowableAction.APPROVE, comment, metadata);
|
||||
}
|
||||
|
||||
public void reject(String instanceId) {
|
||||
reject(instanceId, "审批不通过");
|
||||
}
|
||||
|
||||
public void reject(String instanceId, String comment) {
|
||||
manualAction(instanceId, FlowableAction.REJECT, comment);
|
||||
}
|
||||
|
||||
public void terminal(String instanceId) {
|
||||
terminal(instanceId, "流程被终止");
|
||||
}
|
||||
|
||||
public void terminal(String instanceId, String comment) {
|
||||
manualAction(instanceId, FlowableAction.TERMINAL, comment);
|
||||
}
|
||||
|
||||
public void manualAction(String instanceId, FlowableAction action, String comment) {
|
||||
manualAction(instanceId, action, comment, MapHelper.empty());
|
||||
}
|
||||
|
||||
private void manualAction(String instanceId, FlowableAction action, String comment, Map<String, Object> metadata) {
|
||||
FlowableInstance instance = repository.getInstance(instanceId);
|
||||
if (MapHelper.isNotEmpty(metadata)) {
|
||||
instance.getMetadata().putAll(metadata);
|
||||
}
|
||||
action(instance, action, comment);
|
||||
}
|
||||
|
||||
private void automaticAction(FlowableInstance instance) {
|
||||
action(instance, null, "系统自动执行");
|
||||
}
|
||||
|
||||
private void action(FlowableInstance instance, final FlowableAction inputAction, String comment) {
|
||||
FlowableNode node = repository.getNode(instance.getCurrentNodeId());
|
||||
String handlerClass = node.getHandler();
|
||||
if (StringHelper.isBlank(handlerClass)) {
|
||||
throw new IllegalArgumentException("节点执行器为空");
|
||||
}
|
||||
FlowableHandler handler = createBean(handlerClass);
|
||||
final FlowableAction action = handler.handle(configuration, instance, node, inputAction);
|
||||
if (Objects.isNull(action)) {
|
||||
throw new IllegalArgumentException("节点执行结果不能为空");
|
||||
}
|
||||
|
||||
callListeners(node.getListeners(), listener -> listener.onActionStart(instance, node, action));
|
||||
|
||||
if (FlowableInstance.Status.COMPLETED.equals(instance.getStatus()) || FlowableInstance.Status.TERMINAL.equals(instance.getStatus())) {
|
||||
throw new IllegalArgumentException("ID为" + instance.getInstanceId() + "的流程已结束,无法操作");
|
||||
}
|
||||
|
||||
String accessorClass = node.getAccessor();
|
||||
if (StringHelper.isBlank(accessorClass)) {
|
||||
throw new IllegalArgumentException("权限检查器为空");
|
||||
}
|
||||
FlowableAccessor accessor = createBean(accessorClass);
|
||||
if (!accessor.access(instance, node, action)) {
|
||||
throw new IllegalArgumentException("权限校验不通过");
|
||||
}
|
||||
|
||||
callListeners(node.getListeners(), listener -> listener.onAction(instance, node, action));
|
||||
|
||||
// 如果是挂起操作,就直接返回,不做操作
|
||||
if (FlowableAction.SUSPEND.equals(action)) {
|
||||
saveInstance(instance, instance.getStatus(), action, comment);
|
||||
return;
|
||||
}
|
||||
|
||||
if (FlowableAction.TERMINAL.equals(action)) {
|
||||
saveInstance(instance, FlowableInstance.Status.TERMINAL, action, comment);
|
||||
callListeners(node.getListeners(), listener -> listener.onActionComplete(instance, node, action));
|
||||
return;
|
||||
}
|
||||
if (Objects.isNull(node.getTargets())
|
||||
|| !node.getTargets().containsKey(action)
|
||||
|| StringHelper.isBlank(node.getTargets().get(action))) {
|
||||
saveInstance(instance, FlowableInstance.Status.COMPLETED, action, comment);
|
||||
callListeners(node.getListeners(), listener -> listener.onActionComplete(instance, node, action));
|
||||
return;
|
||||
}
|
||||
String nextNodeId = node.getTargets().get(action);
|
||||
FlowableNode nextNode = repository.getNode(nextNodeId);
|
||||
instance.setCurrentNodeId(nextNode.getNodeId());
|
||||
saveInstance(instance, FlowableInstance.Status.RUNNING, action, comment);
|
||||
|
||||
callListeners(node.getListeners(), listener -> listener.onActionComplete(instance, node, action));
|
||||
if (FlowableNode.Type.AUTOMATIC.equals(nextNode.getType())) {
|
||||
automaticAction(instance);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveInstance(FlowableInstance instance, FlowableInstance.Status status, FlowableAction action, String comment) {
|
||||
instance.setStatus(status);
|
||||
instance.setUpdatedTime(LocalDateTime.now());
|
||||
|
||||
FlowableHistory history = new FlowableHistory(createId(), instance.getInstanceId(), action, comment);
|
||||
repository.saveInstanceAndHistory(instance, history);
|
||||
}
|
||||
|
||||
private void callListeners(List<String> listeners, Consumer<FlowableListener> handler) {
|
||||
for (String listenerClass : listeners) {
|
||||
FlowableListener listener = createBean(listenerClass);
|
||||
handler.accept(listener);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract <T> T createBean(String classpath);
|
||||
|
||||
protected abstract String createId();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
/**
|
||||
* 权限校验
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
public interface FlowableAccessor {
|
||||
boolean access(FlowableInstance instance, FlowableNode node, FlowableAction action);
|
||||
|
||||
class DefaultFlowableAccessor implements FlowableAccessor {
|
||||
@Override
|
||||
public boolean access(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
/**
|
||||
* 节点操作
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
public enum FlowableAction {
|
||||
APPROVE,
|
||||
REJECT,
|
||||
TERMINAL,
|
||||
SUSPEND,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableConfiguration;
|
||||
|
||||
/**
|
||||
* 处理器
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250103
|
||||
*/
|
||||
public interface FlowableHandler {
|
||||
FlowableAction handle(FlowableConfiguration configuration, FlowableInstance instance, FlowableNode node, FlowableAction action);
|
||||
|
||||
class DefaultFlowableHandler implements FlowableHandler {
|
||||
@Override
|
||||
public FlowableAction handle(FlowableConfiguration configuration, FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class FlowableInstance {
|
||||
private final String instanceId;
|
||||
@Builder.Default
|
||||
private final FlowableMetadata metadata = new FlowableMetadata();
|
||||
@Builder.Default
|
||||
private final LocalDateTime createdTime = LocalDateTime.now();
|
||||
|
||||
private String currentNodeId;
|
||||
@Builder.Default
|
||||
private Status status = Status.RUNNING;
|
||||
@Builder.Default
|
||||
private LocalDateTime updatedTime = LocalDateTime.now();
|
||||
|
||||
private String extra;
|
||||
|
||||
public void addMetadata(Map<String, Object> metadata) {
|
||||
this.metadata.putAll(metadata);
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
RUNNING,
|
||||
COMPLETED,
|
||||
TERMINAL,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
/**
|
||||
* 节点监听
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
public interface FlowableListener {
|
||||
void onActionStart(FlowableInstance instance, FlowableNode node, FlowableAction action);
|
||||
|
||||
void onAction(FlowableInstance instance, FlowableNode node, FlowableAction action);
|
||||
|
||||
void onActionComplete(FlowableInstance instance, FlowableNode node, FlowableAction action);
|
||||
|
||||
abstract class AbstractFlowableListener implements FlowableListener {
|
||||
@Override
|
||||
public void onActionStart(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAction(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionComplete(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* 节点流转上下文
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250103
|
||||
*/
|
||||
@ToString
|
||||
@Getter
|
||||
public class FlowableMetadata implements Serializable {
|
||||
private final Map<String, Object> metadata;
|
||||
|
||||
public FlowableMetadata() {
|
||||
this(new HashMap<>());
|
||||
}
|
||||
|
||||
public FlowableMetadata(Map<String, Object> metadata) {
|
||||
if (metadata == null) {
|
||||
metadata = new HashMap<>();
|
||||
}
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
public Object put(String key, Object value) {
|
||||
return metadata.put(key, value);
|
||||
}
|
||||
|
||||
public Object get(String key) {
|
||||
return metadata.get(key);
|
||||
}
|
||||
|
||||
public <T> T get(String key, Class<T> clazz) {
|
||||
return clazz.cast(metadata.get(key));
|
||||
}
|
||||
|
||||
public String getString(String key) {
|
||||
return get(key, String.class);
|
||||
}
|
||||
|
||||
public Integer getInt(String key) {
|
||||
return get(key, Integer.class);
|
||||
}
|
||||
|
||||
public Long getLong(String key) {
|
||||
return get(key, Long.class);
|
||||
}
|
||||
|
||||
public Boolean getBoolean(String key) {
|
||||
return get(key, Boolean.class);
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return metadata.isEmpty();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return metadata.size();
|
||||
}
|
||||
|
||||
public boolean containsKey(String key) {
|
||||
return metadata.containsKey(key);
|
||||
}
|
||||
|
||||
public boolean containsValue(String value) {
|
||||
return metadata.containsValue(value);
|
||||
}
|
||||
|
||||
public Object remove(String key) {
|
||||
return metadata.remove(key);
|
||||
}
|
||||
|
||||
public void putAll(Map<String, Object> m) {
|
||||
metadata.putAll(m);
|
||||
}
|
||||
|
||||
public Object getOrDefault(String key, Object defaultValue) {
|
||||
return metadata.getOrDefault(key, defaultValue);
|
||||
}
|
||||
|
||||
public <T> T getOrDefault(String key, Class<T> clazz, T defaultValue) {
|
||||
return clazz.cast(metadata.getOrDefault(key, defaultValue));
|
||||
}
|
||||
|
||||
public String getStringOrDefault(String key, String defaultValue) {
|
||||
return getOrDefault(key, String.class, defaultValue);
|
||||
}
|
||||
|
||||
public Integer getIntOrDefault(String key, Integer defaultValue) {
|
||||
return getOrDefault(key, Integer.class, defaultValue);
|
||||
}
|
||||
|
||||
public Long getLongOrDefault(String key, Long defaultValue) {
|
||||
return getOrDefault(key, Long.class, defaultValue);
|
||||
}
|
||||
|
||||
public Boolean getBooleanOrDefault(String key, Boolean defaultValue) {
|
||||
return getOrDefault(key, Boolean.class, defaultValue);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.lanyuanxiaoyao.flowable.core.model;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
|
||||
import com.lanyuanxiaoyao.flowable.core.helper.MapHelper;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 流程节点
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class FlowableNode {
|
||||
private final String nodeId;
|
||||
private final String name;
|
||||
private final String description;
|
||||
|
||||
@Builder.Default
|
||||
private final Type type = Type.MANUAL;
|
||||
@Builder.Default
|
||||
private final String handler = FlowableHandler.DefaultFlowableHandler.class.getName();
|
||||
@Builder.Default
|
||||
private final Map<FlowableAction, String> targets = MapHelper.empty();
|
||||
|
||||
@Builder.Default
|
||||
private final String accessor = FlowableAccessor.DefaultFlowableAccessor.class.getName();
|
||||
@Builder.Default
|
||||
private final List<String> listeners = ListHelper.empty();
|
||||
|
||||
@Builder.Default
|
||||
private final LocalDateTime createdTime = LocalDateTime.now();
|
||||
@Builder.Default
|
||||
private LocalDateTime updatedTime = LocalDateTime.now();
|
||||
|
||||
public enum Type {
|
||||
AUTOMATIC,
|
||||
MANUAL,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
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 {
|
||||
boolean existsNode(String nodeId);
|
||||
|
||||
void saveNode(FlowableNode node);
|
||||
|
||||
void saveNode(List<FlowableNode> nodes);
|
||||
|
||||
FlowableNode getNode(String nodeId);
|
||||
|
||||
List<FlowableNode> listNodes();
|
||||
|
||||
boolean existsInstance(String instanceId);
|
||||
|
||||
void saveInstance(FlowableInstance instance);
|
||||
|
||||
FlowableInstance getInstance(String instantId);
|
||||
|
||||
List<FlowableInstance> listInstances();
|
||||
|
||||
boolean existsHistory(String historyId);
|
||||
|
||||
void saveHistory(FlowableHistory history);
|
||||
|
||||
FlowableHistory getHistory(String historyId);
|
||||
|
||||
List<FlowableHistory> listHistories(String instanceId);
|
||||
|
||||
/**
|
||||
* 同时保存实例和历史记录,单独设立这里一个方法是方便数据库使用事务重写
|
||||
*/
|
||||
void saveInstanceAndHistory(FlowableInstance instance, FlowableHistory history);
|
||||
}
|
||||
36
flowable-example/pom.xml
Normal file
36
flowable-example/pom.xml
Normal file
@@ -0,0 +1,36 @@
|
||||
<?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-example</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.lanyuanxiaoyao</groupId>
|
||||
<artifactId>flowable-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,248 @@
|
||||
package com.lanyuanxiaoyao.flowable.test;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.helper.ListHelper;
|
||||
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.FlowableHandler;
|
||||
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.test.accessor.RoleCheckAccessor;
|
||||
import com.lanyuanxiaoyao.flowable.test.handler.TwoApproveHandler;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.MethodOrderer;
|
||||
import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
/**
|
||||
* 集成测试
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20241231
|
||||
*/
|
||||
@Slf4j
|
||||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
|
||||
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 FlowableNode.builder()
|
||||
.nodeId(nodeId)
|
||||
.name(UUID.randomUUID().toString())
|
||||
.description(UUID.randomUUID().toString())
|
||||
.targets(nextNodes)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(1)
|
||||
public void testRepository() {
|
||||
FlowableManager manager = flowableManager();
|
||||
FlowableNode node1 = createManualNode();
|
||||
Assertions.assertFalse(manager.existsNode(node1.getNodeId()));
|
||||
manager.create(node1);
|
||||
Assertions.assertTrue(manager.existsNode(node1.getNodeId()));
|
||||
Assertions.assertNotNull(manager.getNode(node1.getNodeId()));
|
||||
Assertions.assertEquals(1, manager.listNodes().size());
|
||||
}
|
||||
|
||||
/**
|
||||
* 单节点审批
|
||||
*/
|
||||
@Test
|
||||
@Order(2)
|
||||
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
|
||||
@Order(3)
|
||||
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
|
||||
@Order(4)
|
||||
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.TERMINAL, 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
|
||||
@Order(5)
|
||||
public void testSuspend() {
|
||||
FlowableManager manager = flowableManager();
|
||||
FlowableNode node = FlowableNode.builder()
|
||||
.nodeId("50342cb7-2029-4159-bd4c-cbfa4aebe474")
|
||||
.name("50342cb7-2029-4159-bd4c-cbfa4aebe474")
|
||||
.description("50342cb7-2029-4159-bd4c-cbfa4aebe474")
|
||||
.handler(TwoApproveHandler.class.getName())
|
||||
.build();
|
||||
manager.create(node);
|
||||
|
||||
String instanceId = manager.start(node.getNodeId());
|
||||
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
|
||||
manager.approve(instanceId);
|
||||
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
|
||||
manager.approve(instanceId);
|
||||
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
|
||||
}
|
||||
|
||||
protected abstract Class<? extends FlowableHandler> getHandler();
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
public void testAutomaticNode() {
|
||||
FlowableManager manager = flowableManager();
|
||||
FlowableNode node = FlowableNode.builder()
|
||||
.nodeId("5d60dab0-7691-4543-b753-af7ac02cb7ec")
|
||||
.name("5d60dab0-7691-4543-b753-af7ac02cb7ec")
|
||||
.description("5d60dab0-7691-4543-b753-af7ac02cb7ec")
|
||||
.type(FlowableNode.Type.AUTOMATIC)
|
||||
.handler(getHandler().getName())
|
||||
.build();
|
||||
manager.create(node);
|
||||
String instanceId = manager.start(node.getNodeId());
|
||||
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
public void testNodeContext() {
|
||||
FlowableManager manager = flowableManager();
|
||||
FlowableNode node1 = createManualNode(
|
||||
"a3f3f055-aa0e-49ed-9bc5-d0c04f11017e",
|
||||
MapHelper.of(
|
||||
FlowableAction.APPROVE, "88a4ef5b-9cca-4e89-8232-24b6e9e94f4a"
|
||||
)
|
||||
);
|
||||
FlowableNode node2 = createManualNode("88a4ef5b-9cca-4e89-8232-24b6e9e94f4a");
|
||||
manager.create(node1, node2);
|
||||
|
||||
String instanceId = manager.start(node1.getNodeId(), MapHelper.of("name", "lanyuanxiaoyao"));
|
||||
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
|
||||
Assertions.assertEquals("lanyuanxiaoyao", manager.getInstance(instanceId).getMetadata().get("name"));
|
||||
|
||||
manager.approve(instanceId);
|
||||
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
|
||||
Assertions.assertEquals("88a4ef5b-9cca-4e89-8232-24b6e9e94f4a", manager.getInstance(instanceId).getCurrentNodeId());
|
||||
Assertions.assertEquals("lanyuanxiaoyao", manager.getInstance(instanceId).getMetadata().get("name"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(8)
|
||||
public void testAccessor() {
|
||||
FlowableManager manager = flowableManager();
|
||||
FlowableNode node = FlowableNode.builder()
|
||||
.nodeId("a3f3f055-aa0e-49ed-9bc5-d0c04f11017e")
|
||||
.name("a3f3f055-aa0e-49ed-9bc5-d0c04f11017e")
|
||||
.description("a3f3f055-aa0e-49ed-9bc5-d0c04f11017e")
|
||||
.type(FlowableNode.Type.MANUAL)
|
||||
.accessor(RoleCheckAccessor.class.getName())
|
||||
.build();
|
||||
manager.create(node);
|
||||
String instanceId = manager.start(node.getNodeId());
|
||||
Assertions.assertEquals(FlowableInstance.Status.RUNNING, manager.getInstance(instanceId).getStatus());
|
||||
Assertions.assertThrows(IllegalArgumentException.class, () -> manager.approve(instanceId));
|
||||
|
||||
manager.approve(instanceId, MapHelper.of("role", "admin"));
|
||||
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
|
||||
}
|
||||
|
||||
protected abstract Class<? extends FlowableListener> getListenerClass();
|
||||
|
||||
@Test
|
||||
@Order(9)
|
||||
public void testListener() {
|
||||
FlowableManager manager = flowableManager();
|
||||
FlowableNode node = FlowableNode.builder()
|
||||
.nodeId("1c81366f-4102-4a9e-abb2-1dbcb09062e9")
|
||||
.name("1c81366f-4102-4a9e-abb2-1dbcb09062e9")
|
||||
.description("1c81366f-4102-4a9e-abb2-1dbcb09062e9")
|
||||
.type(FlowableNode.Type.MANUAL)
|
||||
.listeners(ListHelper.of(
|
||||
getListenerClass().getName()
|
||||
))
|
||||
.build();
|
||||
manager.create(node);
|
||||
String instanceId = manager.start(node.getNodeId());
|
||||
manager.approve(instanceId);
|
||||
Assertions.assertEquals(FlowableInstance.Status.COMPLETED, manager.getInstance(instanceId).getStatus());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.lanyuanxiaoyao.flowable.test.accessor;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAccessor;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableMetadata;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250103
|
||||
*/
|
||||
@Slf4j
|
||||
public class RoleCheckAccessor implements FlowableAccessor {
|
||||
@Override
|
||||
public boolean access(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
FlowableMetadata metadata = instance.getMetadata();
|
||||
return metadata.containsKey("role");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.lanyuanxiaoyao.flowable.test.handler;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableConfiguration;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableHandler;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableMetadata;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* 同意两次才能通过的自动节点
|
||||
*
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250103
|
||||
*/
|
||||
@Slf4j
|
||||
public class TwoApproveHandler implements FlowableHandler {
|
||||
private static final String KEY = "approve-times";
|
||||
|
||||
@Override
|
||||
public FlowableAction handle(FlowableConfiguration configuration, FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("{}", instance.getMetadata());
|
||||
FlowableMetadata metadata = instance.getMetadata();
|
||||
int approveCount = metadata.getIntOrDefault(KEY, 0);
|
||||
if (approveCount + 1 > 1) {
|
||||
return FlowableAction.APPROVE;
|
||||
}
|
||||
metadata.put(KEY, approveCount + 1);
|
||||
return FlowableAction.SUSPEND;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package com.lanyuanxiaoyao.flowable.test;
|
||||
|
||||
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 boolean existsNode(String nodeId) {
|
||||
return nodes.containsKey(nodeId);
|
||||
}
|
||||
|
||||
@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 boolean existsInstance(String instanceId) {
|
||||
return instances.containsKey(instanceId);
|
||||
}
|
||||
|
||||
@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 boolean existsHistory(String historyId) {
|
||||
return histories.containsKey(historyId);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.lanyuanxiaoyao.flowable.test;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableConfiguration;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableHandler;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250102
|
||||
*/
|
||||
@Slf4j
|
||||
public class SimpleAutoHandler implements FlowableHandler {
|
||||
@Override
|
||||
public FlowableAction handle(FlowableConfiguration configuration, FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("Simple handler initial");
|
||||
return FlowableAction.APPROVE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.lanyuanxiaoyao.flowable.test;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableConfiguration;
|
||||
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(FlowableConfiguration.builder().build(), new InMemoryFlowableRepository());
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
protected <T> T createBean(String classpath) {
|
||||
return (T) Class.forName(classpath).newInstance();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String createId() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.lanyuanxiaoyao.flowable.test;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableAction;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableInstance;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableListener;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableNode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250106
|
||||
*/
|
||||
@Slf4j
|
||||
public class SimpleListener implements FlowableListener {
|
||||
@Override
|
||||
public void onActionStart(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("onActionStart");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAction(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("onAction");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionComplete(FlowableInstance instance, FlowableNode node, FlowableAction action) {
|
||||
log.info("onActionComplete");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.lanyuanxiaoyao.flowable.test;
|
||||
|
||||
import com.lanyuanxiaoyao.flowable.core.manager.FlowableManager;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableHandler;
|
||||
import com.lanyuanxiaoyao.flowable.core.model.FlowableListener;
|
||||
|
||||
/**
|
||||
* @author lanyuanxiaoyao
|
||||
* @version 20250102
|
||||
*/
|
||||
public class TestSimpleFlowableManager extends TestFlowableManager {
|
||||
@Override
|
||||
protected FlowableManager flowableManager() {
|
||||
return new SimpleFlowableManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends FlowableHandler> getHandler() {
|
||||
return SimpleAutoHandler.class;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<? extends FlowableListener> getListenerClass() {
|
||||
return SimpleListener.class;
|
||||
}
|
||||
}
|
||||
80
pom.xml
80
pom.xml
@@ -6,12 +6,24 @@
|
||||
|
||||
<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>flowable-example</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>
|
||||
<hutool.version>5.8.32</hutool.version>
|
||||
<junit.version>5.8.2</junit.version>
|
||||
<slf4j.version>1.7.36</slf4j.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
@@ -19,13 +31,67 @@
|
||||
<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>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.lanyuanxiaoyao</groupId>
|
||||
<artifactId>flowable-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.lanyuanxiaoyao</groupId>
|
||||
<artifactId>flowable-example</artifactId>
|
||||
<version>${project.version}</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>${hutool.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter-api</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>${junit.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
<version>${slf4j.version}</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>
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
// 默认实现为空
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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"), "经理审批结果应被保存");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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", "申请已撤销");
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user