Merge branch 'master' of gitee.com:sz_30/supervison-java into wsm-init-commit

Signed-off-by: new_new_new <8454518+new-twice@user.noreply.gitee.com>
This commit is contained in:
new_new_new
2026-04-10 06:21:47 +00:00
committed by Gitee
583 changed files with 364528 additions and 5 deletions
@@ -149,11 +149,11 @@
<artifactId>commons-langs</artifactId>
<version>2.7.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-xj-jdbc</artifactId>
<version>2.11.2</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.apache.commons</groupId>-->
<!-- <artifactId>commons-xj-jdbc</artifactId>-->
<!-- <version>2.11.2</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.jsoup</groupId>
@@ -232,5 +232,12 @@
<artifactId>jeecg-boot-starter-chatgpt</artifactId>
</dependency>
<!-- end JOA作为微服务独立启动,需要放开以下依赖 -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-module-bpm-flowable</artifactId>
<version>${jeecgboot.version}</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,194 @@
<?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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-platform</artifactId>
<version>3.8.0</version>
</parent>
<artifactId>jeecg-boot-module-airag-flow</artifactId>
<description>此模块属于商业功能范畴,需要购买商业授权才可使用,禁止盗卖、泄露、传播商业源码和商业资料,双方签订合同有保密协议,违者收回商业授权,并追究法律责任</description>
<url>http://www.jeecg.com</url>
<licenses>
<license>
<name>商业授权说明:商业版本只针对企业用户授权,提供更专业更强大的商业版本</name>
<url>http://jeecg.com/vip</url>
<distribution>禁止盗版买卖,违者必究,举报邮箱: jeecgos@163.com</distribution>
<comments>Copyright:北京国炬信息技术有限公司</comments>
</license>
</licenses>
<developers>
<developer>
<name>北京国炬信息技术有限公司</name>
<email>jeecgos@163.com</email>
</developer>
<developer>
<name>授权对象(合作企业): 中国船舶集团有限公司第七〇四研究所</name>
<email>授权范围:贵司通过商业合作,获得商业产品的使用权,但商业版源码、资料禁止传播、盗卖,违者必究!!!</email>
</developer>
</developers>
<scm>
<connection>http://www.jeecg.com</connection>
<developerConnection>http://www.guojusoft.com</developerConnection>
<url>http://www.jeecg.com/vip</url>
</scm>
<properties>
<kotlin.version>1.6.21</kotlin.version>
<liteflow.version>2.12.4.1</liteflow.version>
</properties>
<dependencies>
<!-- system单体 api-->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- liteflow -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>${liteflow.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-lang</artifactId>
<groupId>commons-lang</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-rule-sql</artifactId>
<version>${liteflow.version}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-graaljs</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-groovy</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-kotlin</artifactId>
<version>${liteflow.version}</version>
<exclusions>
<exclusion>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-scripting-jsr223</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-script-aviator</artifactId>
<version>${liteflow.version}</version>
<scope>runtime</scope>
<exclusions>
<exclusion>
<artifactId>aviator</artifactId>
<groupId>com.googlecode.aviator</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<!-- ProGuard混淆插件-->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.0</version>
<executions>
<execution>
<!-- 打包的时候开始混淆-->
<phase>package</phase>
<goals><goal>proguard</goal></goals>
</execution>
</executions>
<configuration>
<putLibraryJarsInTempDir>true</putLibraryJarsInTempDir>
<!--指定混淆的jar,如springboot打出来的jar基于它混淆-->
<injar>${project.build.finalName}.jar</injar>
<!--输出的jar-->
<outjar>${project.build.finalName}-pg.jar</outjar>
<!-- 是否混淆-->
<obfuscate>true</obfuscate>
<options>
<option>-target ${java.version}</option> <!--指定java版本号-->
<option>-keepdirectories</option> <!-- 保持目录结构,否则会导致spring自动装配失败 -->
<option>-dontshrink</option> <!--默认开启,不做收缩(删除注释、未被引用代码)-->
<option>-dontoptimize</option><!--默认是开启的,这里关闭字节码级别的优化-->
<option>-adaptclassstrings</option><!--混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代-->
<option>-ignorewarnings</option><!-- 忽略warn消息,如果提示org.apache.http.* 这个包里的类有问题,那么就加入下述代码:-keep class org.apache.http.** { *; } -dontwarn org.apache.http.**-->
<option>-keep class org.apache.logging.log4j.util.* { *; }</option>
<option>-dontwarn org.apache.logging.log4j.util.**</option>
<option>-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod</option><!--对异常、注解信息在runtime予以保留,不然影响springboot启动-->
<!--不混淆所有interface接口-->
<option>-keepnames interface org.jeecg.modules.**.service.* { *; }</option>
<option>-keepnames interface org.jeecg.modules.**.mapper.* { *; }</option>
<option>-keepnames enum org.jeecg.modules.**.enums.* { *; }</option>
<option>-keep enum org.jeecg.modules.airag.flow.component.code.CodeNodeTypeEnum {*;}</option>
<option>-keepnames interface org.jeecg.modules.airag.common.handler.IAIChatHandler { *; }</option>
<option>-keepnames interface org.jeecg.modules.airag.common.handler.IEmbeddingHandler { *; }</option>
<option>-keepnames interface org.jeecg.modules.airag.common.handler.AIChatParams { *; }</option>
<option>-keepnames interface org.jeecg.modules.airag.flow.component.enhance.IAiRagEnhanceJava { *; }</option>
<option>-keep class org.jeecg.modules.airag.common.consts.AiragConsts {*;}</option>
<option>-keep class org.jeecg.modules.airag.common.vo.** {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.vo.** {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.entity.** {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.context {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.consts.FlowConsts {*;}</option>
<option>-keep class org.jeecg.modules.airag.common.utils.AiragLocalCache {*;}</option>
<option>-keep class org.jeecg.modules.airag.common.handler.AIChatParams {*;}</option>
<option>-keep class org.jeecg.modules.airag.flow.context.JeecgFlowContext {*;}</option>
<option>-keepparameternames</option>
<option>-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);}</option> <!--保留main方法的类及其方法名-->
<!--忽略note消息,如果提示javax.annotation有问题,那麽就加入以下代码-->
<option>-dontnote javax.annotation.**</option>
<option>-dontnote sun.applet.**</option>
<option>-dontnote sun.tools.jar.**</option>
<option>-dontnote org.apache.commons.logging.**</option>
<option>-dontnote javax.inject.**</option>
<option>-dontnote org.aopalliance.intercept.**</option>
<option>-dontnote org.aopalliance.aop.**</option>
<option>-dontnote org.apache.logging.log4j.**</option>
<option>-dontnote module-info</option>
<!-- 不混淆所有的set/get方法 -->
<option>-keepclassmembers public class * {void set*(***);***
get*();}</option>
<option>-keepclassmembers class * {
@org.springframework.beans.factory.annotation.Autowired *;
@org.springframework.beans.factory.annotation.Value *;
}
</option>
</options>
<libs>
<lib>${java.home}/</lib>
</libs>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,32 @@
package org.jeecg.modules.airag.common.consts;
/**
* @Description: airag 公用常量
* @Author: chenrui
* @Date: 2025/3/26 16:39
*/
public class AiragConsts {
/**
* sse缓存key
*/
public static final String CACHE_TYPE_SSE = "CHAT:TYPE:SSE";
/**
* sse send Time 缓存key
*/
public static final String CACHE_TYPE_SSE_SEND_TIME = "CHAT:TYPE:SSE_SEND_TIME";
/**
* 消息类型: 系统
*/
public static final String MESSAGE_ROLE_SYSTEM = "system";
/**
* 消息类型: 用户
*/
public static final String MESSAGE_ROLE_USER = "user";
/**
* 消息类型: AI
*/
public static final String MESSAGE_ROLE_AI = "ai";
}
@@ -0,0 +1,28 @@
package org.jeecg.modules.airag.common.handler;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import org.jeecg.ai.handler.AIParams;
import java.util.List;
/**
* @Description: 自定义ai聊天参数
* @Author: chenrui
* @Date: 2025/3/11 17:45
*/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
public class AIChatParams extends AIParams {
/**
* 知识库ids
*/
List<String> knowIds;
/**
* 无需思考
*/
Boolean noThinking;
}
@@ -0,0 +1,106 @@
package org.jeecg.modules.airag.common.handler;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ImageContent;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.service.TokenStream;
import java.util.List;
/**
* @Description: AI聊天处理器接口
* @Author: chenrui
* @Date: 2025/3/26 17:10
*/
public interface IAIChatHandler {
/**
* 使用默认模型问答
*
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/3/12 15:13
*/
String completionsByDefaultModel(List<ChatMessage> messages, AIChatParams params);
/**
* AI问答
*
* @param modelId 模型id
* @param messages
* @return
* @author chenrui
* @date 2025/3/26 17:18
*/
String completions(String modelId, List<ChatMessage> messages);
/**
* AI问答
*
* @param modelId 模型id
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/3/26 17:12
*/
String completions(String modelId, List<ChatMessage> messages, AIChatParams params);
/**
* 聊天(流式)
*
* @param modelId
* @param messages
* @return
* @author chenrui
* @date 2025/2/20 21:06
*/
TokenStream chat(String modelId, List<ChatMessage> messages);
/**
* 聊天(流式)
*
* @param modelId
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/2/18 21:03
*/
TokenStream chat(String modelId, List<ChatMessage> messages, AIChatParams params);
/**
* 使用默认模型聊天
*
* @param messages
* @param params
* @return
* @author chenrui
* @date 2025/3/12 15:13
*/
TokenStream chatByDefaultModel(List<ChatMessage> messages, AIChatParams params);
/**
* 构造用户消息
* @param content
* @param images
* @return
* @author chenrui
* @date 2025/3/26 18:27
*/
UserMessage buildUserMessage(String content, List<String> images);
/**
* 构造图片消息内容
*
* @param images
* @return
* @author chenrui
* @date 2025/3/18 17:56
*/
List<ImageContent> buildImageContents(List<String> images);
}
@@ -0,0 +1,38 @@
package org.jeecg.modules.airag.common.handler;
import dev.langchain4j.rag.query.router.QueryRouter;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import java.util.List;
/**
* @Description: 向量工具类
* @Author: chenrui
* @Date: 2025/3/26 17:03
*/
public interface IEmbeddingHandler {
/**
* 向量搜索
* @param knowIds
* @param queryText
* @param topNumber
* @param similarity
* @return
* @author chenrui
* @date 2025/4/16 19:10
*/
public KnowledgeSearchResult embeddingSearch(List<String> knowIds, String queryText, Integer topNumber, Double similarity);
/**
* 获取知识库查询路由
* @param knowIds
* @param topNumber
* @param similarity
* @return
* @author chenrui
* @date 2025/4/16 19:09
*/
public QueryRouter getQueryRouter(List<String> knowIds, Integer topNumber, Double similarity);
}
@@ -0,0 +1,89 @@
package org.jeecg.modules.airag.common.utils;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
/**
* @Description: 本地缓存
* @Author: chenrui
* @Date: 2025/2/24 18:10
*/
public class AiragLocalCache {
private static final Map<String, Cache<String, Object>> CACHE_MAP = new ConcurrentHashMap<>();
/**
* 获取缓存值
*
* @param cacheType 缓存类型
* @param key 缓存键
* @param <T> 返回值类型
* @return 缓存值
*/
public static <T> T get(String cacheType, String key) {
Cache<String, Object> cache = getCache(cacheType);
return (T) cache.getIfPresent(key);
}
/**
* 添加或更新缓存值
*
* @param cacheType 缓存类型
* @param key 缓存键
* @param value 缓存值
*/
public static void put(String cacheType, String key, Object value) {
Cache<String, Object> cache = getCache(cacheType);
cache.put(key, value);
}
/**
* 删除缓存值
*
* @param cacheType 缓存类型
* @param key 缓存键
*/
public static void remove(String cacheType, String key) {
if (CACHE_MAP.containsKey(cacheType)) {
Cache<String, Object> cache = CACHE_MAP.get(cacheType);
cache.invalidate(key);
}
}
/**
* 清空指定缓存类型的所有缓存值
*
* @param cacheType 缓存类型
*/
public static void clear(String cacheType) {
if (CACHE_MAP.containsKey(cacheType)) {
Cache<String, Object> cache = CACHE_MAP.get(cacheType);
cache.invalidateAll();
}
}
/**
* 获取缓存
*
* @param cacheType
* @author chenrui
* @date 2025/2/24 18:50
*/
private static Cache<String, Object> getCache(String cacheType) {
if (!CACHE_MAP.containsKey(cacheType)) {
Cache<String, Object> cache = CacheBuilder.newBuilder()
.maximumSize(300)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
CACHE_MAP.put(cacheType, cache);
}
return CACHE_MAP.get(cacheType);
}
}
@@ -0,0 +1,60 @@
package org.jeecg.modules.airag.common.utils;
import freemarker.core.TemplateClassResolver;
import freemarker.template.Configuration;
import freemarker.template.Template;
import lombok.extern.slf4j.Slf4j;
import java.io.StringWriter;
import java.util.Map;
/**
*
* @description:Freemarker引擎工具类
* @Author scott
* @Date 2019-03-22
* @Version V1.0
*/
@Slf4j
public class FreemarkerHelper {
private static Configuration _tplConfig = new Configuration(Configuration.VERSION_2_3_28);
static{
//模板对于数字超过1000,会自动格式为1,,000(禁止转换)
_tplConfig.setNumberFormat("0.#####################");
_tplConfig.setClassForTemplateLoading(FreemarkerHelper.class, "/");
//update-begin-author:scott date:2023-8-15 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
_tplConfig.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);
//update-end-author:scott date:2023-8-15 for: freemarker模板注入问题 禁止解析ObjectConstructorExecute和freemarker.template.utility.JythonRuntime。
}
/**
* 解析ftl
* @param tplName 模板名
* @param encoding 编码
* @param paras 参数
* @return
*/
private static String parseTemplate(String tplName, String encoding,
Map<String, Object> paras) {
try {
StringWriter swriter = new StringWriter();
Template mytpl = null;
mytpl = _tplConfig.getTemplate(tplName, encoding);
mytpl.process(paras, swriter);
return swriter.toString();
} catch (Exception e) {
log.error(e.getMessage(), e);
return e.toString();
}
}
public static String parseTemplate(String tplName, Map<String, Object> paras) {
return parseTemplate(tplName, "utf-8", paras);
}
// public static void main(String[] args) {
// String html = FreemarkerHelper.parseTemplate("org/jeecg/modules/online/config/engine/tableTemplate.ftl", null);
// System.out.println(html);
// }
}
@@ -0,0 +1,36 @@
package org.jeecg.modules.airag.common.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Condition;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* JDK 版本判断条件
* <p>
* 该类用于判断当前运行环境的 JDK 版本是否为 21。
* 如果是 JDK 21,则返回 false,表示不满足条件。
* 否则返回 true,表示满足条件。
* </p>
*/
public class JdkVersionCondition implements Condition {
private static final Logger logger = LoggerFactory.getLogger(JdkVersionCondition.class);
private static final String JAVA_VERSION = System.getProperty("java.version");
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 判断 JDK 版本是否为 21
Pattern pattern = Pattern.compile("^(1\\.)?21\\..*");
Matcher matcher = pattern.matcher(JAVA_VERSION);
if (matcher.matches()) {
logger.warn("【::AI环境要求::】当前运行环境是 JDK 21 无法使用AI流程编排,目前 AI流程 只支持 JDK 8 ~ JDK 17");
return false; // 如果是 JDK 21,返回 false
}
return true; // 如果不是 JDK 21,返回 true
}
}
@@ -0,0 +1,77 @@
package org.jeecg.modules.airag.common.vo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.net.URI;
import java.util.List;
/**
* 聊天记录
*
* @author chenrui
* @date 2025/2/25 14:49
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MessageHistory implements Serializable {
private static final long serialVersionUID = 3238429500037511283L;
/**
* 会话id
*/
String conversationId;
/**
* 话题id
*/
String topicId;
/**
* 角色
*
* @see org.jeecg.modules.airag.common.consts.AiragConsts
*/
String role;
/**
* 聊天记录内容
*/
String content;
/**
* 图片列表
*/
List<ImageHistory> images;
/**
* 发送时间
*/
String datetime;
@Data
public static class ImageHistory {
private URI url;
private String base64Data;
private String mimeType;
public ImageHistory() {
}
public ImageHistory(URI url, String base64Data, String mimeType) {
this.url = url;
this.base64Data = base64Data;
this.mimeType = mimeType;
}
public static ImageHistory from(URI url, String base64Data, String mimeType) {
return new ImageHistory(url, base64Data, mimeType);
}
}
}
@@ -0,0 +1,109 @@
package org.jeecg.modules.airag.common.vo.event;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 流程事件数据
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Data
@NoArgsConstructor
public class EventData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 事件类型:节点开始
*/
public static final String EVENT_NODE_STARTED = "NODE_STARTED";
/**
* 事件类型:节点结束
*/
public static final String EVENT_NODE_FINISHED = "NODE_FINISHED";
/**
* 事件类型:消息
*/
public static final String EVENT_MESSAGE = "MESSAGE";
/**
* 事件类型:消息完结
*/
public static final String EVENT_MESSAGE_END = "MESSAGE_END";
/**
* 事件类型:流程开始
*/
public static final String EVENT_FLOW_STARTED = "FLOW_STARTED";
/**
* 事件类型:流程结束
*/
public static final String EVENT_FLOW_FINISHED = "FLOW_FINISHED";
/**
* 事件类型:异常
*/
public static final String EVENT_FLOW_ERROR = "ERROR";
/**
* 事件类型:推送初始化requestId
*/
public static final String EVENT_INIT_REQUEST_ID = "INIT_REQUEST_ID";
public EventData(String requestId, String flowId, String event) {
this.requestId = requestId;
this.flowId = flowId;
this.event = event;
}
public EventData(String requestId, String flowId, String event, String conversationId, String topicId) {
this.event = event;
this.flowId = flowId;
this.requestId = requestId;
this.conversationId = conversationId;
this.topicId = topicId;
}
/**
* 事件
*/
private String event;
/**
* 流程id
*/
private String flowId;
/**
* 请求id
*/
private String requestId;
/**
* 会话id
*/
private String conversationId;
/**
* 话题id
*/
private String topicId;
/**
* 数据
*/
private Object data;
@Override
public String toString() {
return JSONObject.toJSONString(this);
}
}
@@ -0,0 +1,46 @@
package org.jeecg.modules.airag.common.vo.event;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 流程事件数据:流程开始结束
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Builder
@Data
public class EventFlowData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 是否成功
*/
private boolean success;
/**
* 消息
*/
private String message;
/**
* 执行的节点
*/
private String executeSteps;
/**
* 流程入参
*/
private Map<String, Object> inputs = new HashMap<>();
/**
* 流程结果
*/
private Object outputs;
}
@@ -0,0 +1,56 @@
package org.jeecg.modules.airag.common.vo.event;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 流程事件数据:节点
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Builder
@Data
public class EventFlowNodeData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节点id
*/
private String id;
/**
* 节点类型
*/
private String type;
/**
* 节点名称(描述)
*/
private String text;
/**
* 入参
*/
private Map<String, Object> inputs = new HashMap<>();
/**
* 出参
*/
private Object outputs;
/**
* 是否成功
*/
private boolean success;
/**
* 消息
*/
private String message;
}
@@ -0,0 +1,29 @@
package org.jeecg.modules.airag.common.vo.event;
import lombok.Builder;
import lombok.Data;
import java.io.Serializable;
/**
* 流程事件数据: 消息
*
* @author chenrui
* @date 2025/2/19 17:56
*/
@Builder
@Data
public class EventMessageData implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 节点id
*/
private String fromNodeId;
/**
* 消息(实时)
*/
private String message;
}
@@ -0,0 +1,33 @@
package org.jeecg.modules.airag.common.vo.knowledge;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
/**
* 知识库查询返回结果
*
* @Author: chenrui
* @Date: 2025/2/18 17:53
*/
@Data
@NoArgsConstructor
public class KnowledgeSearchResult {
/**
* 命中的文档内容
*/
String data;
/**
* 命中的文档列表
*/
List<Map<String, Object>> documents;
public KnowledgeSearchResult(String data, List<Map<String, Object>> documents) {
this.data = data;
this.documents = documents;
}
}
@@ -0,0 +1,90 @@
package org.jeecg.modules.airag.flow.aspect;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.lifecycle.PostProcessFlowExecuteLifeCycle;
import com.yomahub.liteflow.slot.Slot;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
/**
* 流程切面
*
* @author chenrui
* @date 2025/2/20 15:08
*/
@Slf4j
@Component("flowExecuteAspect")
public class FlowExecuteAspect implements PostProcessFlowExecuteLifeCycle {
@Override
public void postProcessBeforeFlowExecute(String chainId, Slot slot) {
JeecgFlowContext context = slot.getContextBean(JeecgFlowContext.class);
EventData eventData = new EventData(context.getRequestId(),
slot.getChainId(), EventData.EVENT_FLOW_STARTED, context.getConversationId(), context.getTopicId());
EventFlowData data = EventFlowData.builder()
.success(true)
.inputs(context.getRequestDatas())
.build();
eventData.setData(data);
String flowEventDataStr = JSONObject.toJSONString(eventData);
sendEmitter(context, flowEventDataStr, false);
log.info("[Flow-Process-Before]Event:{}", flowEventDataStr);
}
@Override
public void postProcessAfterFlowExecute(String chainId, Slot slot) {
LiteflowResponse response = LiteflowResponse.newMainResponse(slot);
JeecgFlowContext context = slot.getContextBean(JeecgFlowContext.class);
EventData eventData = new EventData(context.getRequestId(),
slot.getChainId(), EventData.EVENT_FLOW_FINISHED, context.getConversationId(), context.getTopicId());
EventFlowData data = EventFlowData.builder()
.success(response.isSuccess())
.executeSteps(response.getExecuteStepStr())
.message(response.getMessage())
.inputs(context.getRequestDatas())
.outputs(context.getResult())
.build();
eventData.setData(data);
if (oConvertUtils.isObjectNotEmpty(context.getEventCallback())) {
try {
context.getEventCallback().accept(eventData);
} catch (Throwable e) {
log.error("[flow]run event callback fail:{}", e.getMessage());
}
}
String flowEventDataStr = JSONObject.toJSONString(eventData);
sendEmitter(context, flowEventDataStr, true);
log.info("[Flow-Process-after]Event:{}", flowEventDataStr);
}
/**
* 发送sse消息
*
* @param context
* @param eventData
* @author chenrui
* @date 2025/2/20 16:55
*/
private static void sendEmitter(JeecgFlowContext context, String eventData, boolean isFinished) {
SseEmitter emitter = context.getEmitter();
if (null != emitter) {
try {
emitter.send(SseEmitter.event().data(eventData));
if (isFinished) {
emitter.complete();
}
} catch (IOException e) {
log.error("SSE send error", e);
emitter.completeWithError(e);
}
}
}
}
@@ -0,0 +1,99 @@
package org.jeecg.modules.airag.flow.aspect;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.aop.ICmpAroundAspect;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.modules.airag.common.vo.event.EventFlowData;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventFlowNodeData;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
/**
* 流程节点切面
*
* @author chenrui
* @date 2025/2/20 15:08
*/
@Slf4j
@Component("nodeProcessAspect")
public class NodeProcessAspect implements ICmpAroundAspect {
@Override
public void beforeProcess(NodeComponent cmp) {
EventData flowEventData = buildEventData(cmp, EventData.EVENT_NODE_STARTED);
String eventData = JSONObject.toJSONString(flowEventData);
sendEmitter(cmp, eventData);
log.info("[Node-Process-Before]Event:{}", eventData);
}
@Override
public void afterProcess(NodeComponent cmp) {
}
@Override
public void onSuccess(NodeComponent cmp) {
EventData flowEventData = buildEventData(cmp, EventData.EVENT_NODE_FINISHED);
EventFlowNodeData eventFlowNodeData = (EventFlowNodeData) flowEventData.getData();
eventFlowNodeData.setSuccess(true);
String eventData = JSONObject.toJSONString(flowEventData);
sendEmitter(cmp, eventData);
log.info("[Node-Process-After]Event:{}", JSONObject.toJSONString(flowEventData));
}
@Override
public void onError(NodeComponent cmp, Exception e) {
EventData flowEventData = buildEventData(cmp, EventData.EVENT_NODE_FINISHED);
EventFlowNodeData eventFlowNodeData = (EventFlowNodeData) flowEventData.getData();
eventFlowNodeData.setSuccess(false);
eventFlowNodeData.setMessage(e.getMessage());
String eventData = JSONObject.toJSONString(flowEventData);
sendEmitter(cmp, eventData);
log.info("[Node-Process-Error]Event:{}", JSONObject.toJSONString(flowEventData));
}
/**
* 构造事件数据
*
* @param cmp
* @param event
* @author chenrui
* @date 2025/2/19 19:25
*/
private static EventData buildEventData(NodeComponent cmp, String event) {
JeecgFlowContext context = cmp.getContextBean(JeecgFlowContext.class);
EventData eventData = new EventData(context.getRequestId(),
cmp.getChainId(), event, context.getConversationId(), context.getTopicId());
FlowNodeConfig nodeConfig = JeecgBaseFlowNode.getNodeConfig(cmp);
EventFlowNodeData eventFlowNodeData = EventFlowNodeData.builder()
.id(nodeConfig.getNodeId())
.type(nodeConfig.getNodeType())
.text(nodeConfig.getText())
.inputs(JeecgBaseFlowNode.getInputs(nodeConfig, context))
.outputs(JeecgBaseFlowNode.getOutputs(nodeConfig, context))
.build();
eventData.setData(eventFlowNodeData);
return eventData;
}
private static void sendEmitter(NodeComponent cmp, String eventData) {
JeecgFlowContext context = cmp.getContextBean(JeecgFlowContext.class);
SseEmitter emitter = context.getEmitter();
if(null != emitter){
try {
emitter.send(SseEmitter.event().data(eventData));
} catch (IOException e) {
log.error("SSE send error", e);
emitter.completeWithError(e);
}
}
}
}
@@ -0,0 +1,169 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 分类器节点
*
* @author chenrui
* @date 2025/02/17 11:20
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_CLASSIFIER)
public class ClassifierNode extends JeecgBaseFlowNode {
@Autowired
IAIChatHandler aiChatHandler;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_SWITCH, nodeType = NodeTypeEnum.SWITCH)
public String process(NodeComponent component) {
log.info("节点开始-classifier");
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
JSONObject modelOption = options.getJSONObject(FLOW_NODE_OPTION_MODEL);
if (!modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID) || oConvertUtils.isEmpty(modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID))) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择模型");
}
String modelId = modelOption.getString(FLOW_NODE_OPTION_MODEL_MODEID);
// 整理消息
JSONArray categories = options.getJSONArray(FLOW_NODE_OPTION_MODEL_CATEGORIES);
AssertUtils.assertNotEmpty("[classifier]节点至少配置一个分类", categories);
List<ChatMessage> chatMessages = collateMessage(component, categories, inputParams);
AIChatParams aiChatParams = buildChatParams(modelOption);
// 发送消息,让AI选择
String chooseNodeTag = null;
try {
chooseNodeTag = aiChatHandler.completions(modelId, chatMessages, aiChatParams);
} catch (Exception e) {
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
if (e.getMessage().contains("timeout")) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT);
}
throw e;
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
}
JeecgFlowContext contextBean = getContext(component);
for (int i = 0; i < categories.size(); i++) {
JSONObject categoryObj = categories.getJSONObject(i);
String next = oConvertUtils.getString(categoryObj.getString(FLOW_NODE_OPTION_NEXT), "");
if (chooseNodeTag.equalsIgnoreCase(next)) {
String categoryDescr = oConvertUtils.getString(categoryObj.getString(FLOW_NODE_OPTION_MODEL_CATEGORY), "");
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, i);
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_CONTENT, categoryDescr);
log.info("[classifier]已选择节点:[{}]", chooseNodeTag);
return getNextTag(flowNodeConfig, chooseNodeTag);
}
}
// else
Map<String, Object> elseOption = (Map<String, Object>) options.get(FLOW_NODE_OPTION_ELSE);
if (null != elseOption && !elseOption.isEmpty()) {
log.info("[classifier]选择else:{}", elseOption.get(FLOW_NODE_OPTION_NEXT));
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, -1);
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_CONTENT, "都不符合");
return getNextTag(flowNodeConfig, (String) elseOption.get(FLOW_NODE_OPTION_NEXT));
}
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "没有找到匹配的结果");
}
/**
* 构造AI问答参数
* @param modelOption
* @return
* @author chenrui
* @date 2025/3/27 16:09
*/
private static AIChatParams buildChatParams(JSONObject modelOption) {
AIChatParams aiChatParams = new AIChatParams();
JSONObject modelParams = modelOption.getJSONObject(FLOW_NODE_OPTION_MODEL_PARAMS);
if (null != modelParams) {
Double temperature = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TEMPERATURE);
if (null != temperature) {
aiChatParams.setTemperature(temperature);
}
Double topP = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TOP);
if (null != topP) {
aiChatParams.setTopP(topP);
}
Double presencePenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_PRESENCE_PENALTY);
if (null != presencePenalty) {
aiChatParams.setPresencePenalty(presencePenalty);
}
Double frequencyPenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_FREQUENCY_PENALTY);
if (null != frequencyPenalty) {
aiChatParams.setFrequencyPenalty(frequencyPenalty);
}
Integer maxTokens = modelParams.getInteger(FLOW_NODE_OPTION_MODEL_MAX_TOKENS);
if (null != maxTokens) {
aiChatParams.setMaxTokens(maxTokens);
}
}
return aiChatParams;
}
private List<ChatMessage> collateMessage(NodeComponent component, JSONArray categories, List<FlowNodeConfig.NodeParam> inputParams) {
JeecgFlowContext contextBean = getContext(component);
List<ChatMessage> chatMessages = new ArrayList<>();
AssertUtils.assertNotEmpty("分类器节点,请选择输入变量", inputParams);
AssertUtils.assertNotEmpty("分类器节点,至少需要配置一个分类", categories);
for (int i = 0; i < categories.size(); i++) {
JSONObject categoryObj = (JSONObject) categories.get(i);
String categoryDescr = oConvertUtils.getString(categoryObj.getString(FLOW_NODE_OPTION_MODEL_CATEGORY), "");
AssertUtils.assertNotEmpty("" + (i + 1) + "个分类未填写描述", categoryDescr);
AssertUtils.assertNotEmpty("分类" + categoryDescr + "未选择跳转节点", categoryObj.getString(FLOW_NODE_OPTION_NEXT));
}
// 系统消息
String systemMsgContent = "请根据用户的问题,从以下分类数组中选择最符合的分类对象。 \n" +
"回复该分类对象的 `next` 属性值。 \n" +
"如果 **找不到最符合的**,请返回 `-1`。 " +
"分类数据如下:\n" +
"---------------------------\n" +
categories.toJSONString() + "\n" +
"---------------------------\n";
chatMessages.add(new SystemMessage(systemMsgContent));
// 用户消息
FlowNodeConfig.NodeParam firstInputParams = inputParams.get(0);
Object inputData = contextBean.getContextData(firstInputParams.getNodeId(), firstInputParams.getField());
chatMessages.add(new UserMessage(inputData.toString()));
return chatMessages;
}
}
@@ -0,0 +1,73 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_END)
public class EndNode extends JeecgBaseFlowNode {
public static final Pattern VAR_PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{(.*?)}}");
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-end");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
// 整理输出参数
List<FlowNodeConfig.NodeParam> outputParams = flowNodeConfig.getOutputParams();
Map<String, Object> outputMap = new HashMap<>();
for (FlowNodeConfig.NodeParam outputParam : outputParams) {
Object data = contextBean.getContextData(outputParam.getNodeId(), outputParam.getField());
outputMap.put(outputParam.getName(), data);
}
// 判断是否输出文本
boolean outputText = false;
JSONObject options = null;
if (oConvertUtils.isObjectNotEmpty(flowNodeConfig.getOptions())) {
options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
outputText = options.getBooleanValue(FlowConsts.FLOW_NODE_OPTION_OUTPUT_TEXT);
}
if (outputText) {
// 输出文本: 构建文本内容
String outputContent = options.getString(FlowConsts.FLOW_NODE_OPTION_OUTPUT_CONTENT);
if (oConvertUtils.isNotEmpty(outputContent)) {
Matcher matcher = VAR_PLACEHOLDER_PATTERN.matcher(outputContent);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
Object val = (oConvertUtils.isObjectNotEmpty(outputMap)) ? outputMap.get(key) : null;
matcher.appendReplacement(sb, (val != null) ? Matcher.quoteReplacement(val.toString()) : "");
}
matcher.appendTail(sb);
contextBean.setResult(sb.toString());
}
} else {
contextBean.setResult(outputMap);
}
// 结束流程
component.setIsEnd(true);
}
}
@@ -0,0 +1,469 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.CommonUtils;
import org.jeecg.common.util.RestUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
/**
* http节点
*
* @author chenrui
* @date 2025/2/11 17:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_HTTP)
public class HttpNode extends JeecgBaseFlowNode {
/**
* 输出参数:body
*/
public static final String OUTPUT_PARAM_NAME_BODY = "body";
/**
* 输出参数:statusCode
*/
public static final String OUTPUT_PARAM_NAME_STATUS_CODE = "statusCode";
/**
* 请求体类型: none
*/
private static final String OPTIONS_BODY_TYPE_NONE = "none";
/**
* 请求体类型: binary
*/
private static final String OPTIONS_BODY_TYPE_BINARY = "binary";
/**
* 请求体类型: raw
*/
private static final String OPTIONS_BODY_TYPE_RAW = "raw";
/**
* 请求体类型: x-www-form-urlencoded
*/
private static final String OPTIONS_BODY_TYPE_X_WWW_FORM_URLENCODED = "x-www-form-urlencoded";
/**
* 请求体类型: form-data
*/
private static final String OPTIONS_BODY_TYPE_FORM_DATA = "form-data";
/**
* 请求体类型: json
*/
private static final String OPTIONS_BODY_TYPE_JSON = "json";
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-http");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
// 节点配置
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
HttpOptions httpOptions = options.getJSONObject(FlowConsts.FLOW_NODE_OPTION_HTTP).toJavaObject(HttpOptions.class);
// 检查必填参数
if (oConvertUtils.isEmpty(httpOptions.getMethod()) || oConvertUtils.isEmpty(httpOptions.getUrl())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "缺少必要参数");
}
// 防御SSRF: 检查domain是否在允许的范围内
// TODO author: chenrui for:防御SSRF: 检查domain是否在允许的范围内 date:2025/3/13
// 处理请求参数
ReqParam reqParam = preprocessReq(httpOptions, flowNodeConfig, contextBean, inputParams);
// 发送请求
ResponseEntity<String> responseEntity;
try {
// 发送请求
log.info("[FLOW_HTTP]发送http请求:url:[{}]", reqParam.getUrl());
responseEntity = RestUtil.request(reqParam.getUrl(), reqParam.getHttpMethod(), reqParam.getHttpHeaders(), reqParam.getRequestParams(), reqParam.getBody(), String.class);
// 这是返回参数
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_STATUS_CODE, responseEntity.getStatusCodeValue());
// 获取响应体
String respBody = responseEntity.getBody();
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_BODY, respBody);
if(oConvertUtils.isNotEmpty(respBody)) {
processOutputParams(flowNodeConfig,respBody,contextBean);
}
} catch (Exception e) {
String errMessage = e.getMessage();
String detailMessage = errMessage;
if (e instanceof HttpClientErrorException) {
HttpClientErrorException httpClientException = (HttpClientErrorException) e;
errMessage = httpClientException.getStatusCode().toString();
}
log.error("[FLOW_HTTP]发送http请求失败:" + detailMessage,e);
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "发送http请求失败:" + errMessage);
}
}
/**
* 预处理请求
*
* @param httpOptions
* @param flowNodeConfig
* @param contextBean
* @param inputParams
* @return
* @author chenrui
* @date 2025/3/25 11:07
*/
private ReqParam preprocessReq(HttpOptions httpOptions, FlowNodeConfig flowNodeConfig, JeecgFlowContext contextBean, List<FlowNodeConfig.NodeParam> inputParams) {
// method
HttpMethod httpMethod = HttpMethod.resolve(httpOptions.getMethod().toUpperCase());
if (httpMethod == null) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "无效的HTTP方法");
}
// 流程上下文参数以及系统上下文变量
HttpServletRequest httpRequest = contextBean.getHttpRequest();
SysUserCacheInfo user = contextBean.getCacheUser();
Map<String, Object> inputs = collateInputParams(inputParams, contextBean);
// 处理请求地址
String url = httpOptions.getUrl();
if (url.contains("{") && url.contains("}")) {
// 替换变量
if (url.contains("{{domainURL}}")) {
String baseUrl = CommonUtils.getBaseUrl(httpRequest);
url = url.replaceFirst("\\{\\{domainURL}}", baseUrl);
}
url = replaceVar(url, inputs, user);
}
// 获取请求体
JSONObject requestBody = httpOptions.getRequestBody();
String bodyType = requestBody.getString("type");
String bodyStr = requestBody.getString("body");
// 处理输入内容
bodyStr = sanitizeInput(bodyStr);
// 替换变量
bodyStr = replaceVar(bodyStr, inputs, user);
Object body = bodyStr;
if (!bodyType.equalsIgnoreCase(OPTIONS_BODY_TYPE_RAW) && !bodyType.equalsIgnoreCase(OPTIONS_BODY_TYPE_NONE)) {
body = JSONObject.parseObject(bodyStr);
}
// 处理params
JSONObject requestParams = httpOptions.getRequestParams();
if (null != requestParams) {
requestParams.keySet().forEach(key -> {
Object val = requestParams.get(key);
if (val instanceof String) {
// 替换变量
String strVal = (String) val;
strVal = sanitizeInput(strVal);
strVal = replaceVar(strVal, inputs, user);
requestParams.put(key, strVal);
}
});
}
// 组装请求头,格式化body
HttpHeaders httpHeaders = buildHeader(bodyType, contextBean, httpOptions, inputs, user);
return new ReqParam(httpMethod, url, body, requestParams, httpHeaders);
}
/**
* 替换变量
*
* @param bodyStr
* @param inputs
* @return
* @author chenrui
* @date 2025/3/13 17:37
*/
private static String replaceVar(String bodyStr, Map<String, Object> inputs, SysUserCacheInfo user) {
Matcher matcher = FlowConsts.VAR_PLACEHOLDER_PATTERN.matcher(bodyStr);
StringBuffer resMessage = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
Object val = (oConvertUtils.isObjectNotEmpty(inputs)) ? inputs.get(key) : null;
if (null == val) {
val = JwtUtil.getUserSystemData("#{" + key + "}", user);
}
matcher.appendReplacement(resMessage, (val != null) ? Matcher.quoteReplacement(val.toString()) : "");
}
matcher.appendTail(resMessage);
return resMessage.toString();
}
/**
* 整理输入变量
*
* @param inputParams
* @param contextBean
* @return
* @author chenrui
* @date 2025/3/13 15:59
*/
private Map<String, Object> collateInputParams(List<FlowNodeConfig.NodeParam> inputParams, JeecgFlowContext contextBean) {
Map<String, Object> inputs = new HashMap<>();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
inputs.put(inputParam.getName(), inputData);
}
return inputs;
}
/**
* 构造header
*
* @param bodyType
* @param contextBean
* @param httpOptions
* @param inputs
* @param user
* @return
* @author chenrui
* @date 2025/3/13 17:37
*/
private static HttpHeaders buildHeader(String bodyType, JeecgFlowContext contextBean, HttpOptions httpOptions, Map<String, Object> inputs, SysUserCacheInfo user) {
HttpHeaders httpHeaders;
switch (bodyType) {
case OPTIONS_BODY_TYPE_NONE:
httpHeaders = new HttpHeaders();
break;
case OPTIONS_BODY_TYPE_JSON:
httpHeaders = RestUtil.getHeader(MediaType.APPLICATION_JSON_UTF8_VALUE);
break;
case OPTIONS_BODY_TYPE_FORM_DATA:
httpHeaders = RestUtil.getHeader(MediaType.MULTIPART_FORM_DATA_VALUE);
break;
case OPTIONS_BODY_TYPE_X_WWW_FORM_URLENCODED:
httpHeaders = RestUtil.getHeader(MediaType.APPLICATION_FORM_URLENCODED_VALUE);
break;
case OPTIONS_BODY_TYPE_RAW:
httpHeaders = RestUtil.getHeader(MediaType.TEXT_PLAIN_VALUE);
break;
case OPTIONS_BODY_TYPE_BINARY:
httpHeaders = RestUtil.getHeader(MediaType.APPLICATION_OCTET_STREAM_VALUE);
break;
default:
httpHeaders = RestUtil.getHeaderApplicationJson();
break;
}
HttpServletRequest httpRequest = contextBean.getHttpRequest();
// token
String token = TokenUtils.getTokenByRequest(httpRequest);
if (oConvertUtils.isNotEmpty(token)) {
httpHeaders.set("X-Access-Token", token);
}
// tenantId
String tenantId = TokenUtils.getTenantIdByRequest(httpRequest);
if (oConvertUtils.isNotEmpty(tenantId)) {
httpHeaders.set("X-Tenant-Id", tenantId);
}
// 自定义请求头
JSONObject headers = httpOptions.getHeaders();
for (String key : headers.keySet()) {
String headerVal = headers.getString(key);
headerVal = replaceVar(headerVal, inputs, user);
httpHeaders.add(key, headerVal);
}
return httpHeaders;
}
/**
* 检查请求内容,防止XSS等攻击
*
* @param input 输入字符串
* @return 清理后的字符串
*/
private String sanitizeInput(String input) {
// 防御XSS: 对特殊字符进行HTML编码
input = input.replaceAll("&", "&amp;");
input = input.replaceAll("<", "&lt;");
input = input.replaceAll(">", "&gt;");
input = input.replaceAll("'", "&#x27;");
input = input.replaceAll("/", "&#x2F;");
// 替换掉空格,否则json解析失败
input = input.replace("\u00A0", " ");
return input;
}
/**
* 处理输出参数
*
* @param flowNodeConfig
* @param bodyString
* @param contextBean
*/
public void processOutputParams(FlowNodeConfig flowNodeConfig, String bodyString, JeecgFlowContext contextBean) {
List<FlowNodeConfig.NodeParam> outputParams = flowNodeConfig.getOutputParams();
if (oConvertUtils.isEmpty(bodyString)) {
return;
}
JSONObject body = JSONObject.parseObject(bodyString);
for (FlowNodeConfig.NodeParam outputParam : outputParams) {
String field = outputParam.getField();
if (oConvertUtils.isEmpty(field) || OUTPUT_PARAM_NAME_BODY.equalsIgnoreCase(field) || OUTPUT_PARAM_NAME_STATUS_CODE.equalsIgnoreCase(field)) {
continue;
}
Object value = getValueFromBody(field, body);
if (value != null) {
try {
value = convertToType(value, outputParam.getType());
}catch (Exception e) {
log.error("转换类型失败,field:{},value:{}", field, value);
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "转换类型失败");
}
}
contextBean.setContextData(flowNodeConfig.getNodeId(), field, value != null ? value : "");
}
}
/**
* 从响应体中获取值
*
* @param field 字段名
* @param body 响应体
* @return 值
*/
private Object getValueFromBody(String field, JSONObject body) {
if(field.startsWith(OUTPUT_PARAM_NAME_BODY+".")) {
field = field.replaceFirst(OUTPUT_PARAM_NAME_BODY+".", "");
}
String[] fieldParts = field.split("\\.");
Object value = body;
for (String part : fieldParts) {
if (value instanceof JSONObject) {
if (part.contains("[") && part.contains("]")) {
String arrayField = part.substring(0, part.indexOf("["));
int index = Integer.parseInt(part.substring(part.indexOf("[") + 1, part.indexOf("]")));
JSONArray arrayData = ((JSONObject) value).getJSONArray(arrayField);
if(arrayData.size()>index+1) {
value = arrayData.get(index);
}else{
value = null;
}
} else {
value = ((JSONObject) value).get(part);
}
} else {
return null;
}
}
return value;
}
private Object convertToType(Object value, String type) {
if (FlowConsts.FLOW_PARAM_TYPE_STRING.equals(type)) {
return value.toString();
} else if (FlowConsts.FLOW_PARAM_TYPE_NUMBER.equals(type)) {
return value instanceof Number ? value : (Object) Double.parseDouble(value.toString());
} else if (FlowConsts.FLOW_PARAM_TYPE_PICTURE.equals(type)) {
// Assuming picture is a base64 encoded string
return value.toString();
} else if (FlowConsts.FLOW_PARAM_TYPE_OBJECT.equals(type)) {
return value instanceof JSONObject ? value : JSONObject.parseObject(value.toString());
} else if (FlowConsts.FLOW_PARAM_TYPE_STRING_ARRAY.equals(type)) {
return value instanceof List ? ((List<?>) value).toArray(new Object[0]) : new String[]{value.toString()};
} else if (FlowConsts.FLOW_PARAM_TYPE_NUMBER_ARRAY.equals(type)) {
return value instanceof List ? ((List<?>) value).stream().mapToDouble(v -> Double.parseDouble(v.toString())).toArray() : new double[]{Double.parseDouble(value.toString())};
} else if (FlowConsts.FLOW_PARAM_TYPE_OBJECT_ARRAY.equals(type)) {
return value instanceof List ? ((List<?>) value).toArray(new Object[0]) : new JSONObject[]{JSONObject.parseObject(value.toString())};
} else {
return value;
}
}
/**
* 请求参数类
*
* @author chenrui
* @date 2025/3/25 11:09
*/
@Data
private static class ReqParam {
private final HttpMethod httpMethod;
private final String url;
private final Object body;
private final JSONObject requestParams;
private final HttpHeaders httpHeaders;
public ReqParam(HttpMethod httpMethod, String url, Object body, JSONObject requestParams, HttpHeaders httpHeaders) {
this.httpMethod = httpMethod;
this.url = url;
this.body = body;
this.requestParams = requestParams;
this.httpHeaders = httpHeaders;
}
}
/**
* HTTP选项类
*/
@Data
public static class HttpOptions {
/**
* HTTP方法(例如:GET, POST
*/
private String method;
/**
* 发送请求的URL
*/
private String url;
/**
* 请求参数,JSON对象
*/
private JSONObject requestParams;
/**
* 请求体
*/
private JSONObject requestBody;
/**
* HTTP头,JSON对象
*/
private JSONObject headers;
/**
* 请求超时时间,单位为毫秒
*/
private int timeout;
}
}
@@ -0,0 +1,91 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import dev.langchain4j.rag.query.router.QueryRouter;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.handler.IEmbeddingHandler;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.common.vo.knowledge.KnowledgeSearchResult;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 知识库节点
*
* @author chenrui
* @date 2025/2/18 20:49
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_KNOWLEDGE)
public class KnowledgeNode extends JeecgBaseFlowNode {
/**
* 输出参数:data (拼接的命中内容)
*/
public static final String OUTPUT_PARAM_NAME_DATA = "data";
/**
* 输出参数:documents (命中的文档片段)
*/
public static final String OUTPUT_PARAM_NAME_DOCUMENTS = "documents";
/**
* 输出参数:queryRouter (查询路由,供llm节点使用)
*/
public static final String OUTPUT_PARAM_NAME_QUERY_ROUTER = "queryRouter";
@Autowired
IEmbeddingHandler embeddingHandler;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-knowledge");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
// 获取参数
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
if (oConvertUtils.isObjectEmpty(inputParams)) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "必须选择输入变量");
}
FlowNodeConfig.NodeParam inputParam = inputParams.get(0);
Object question = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
Integer topNumber = options.getInteger(FLOW_NODE_OPTION_TOP_NUMBER);
Double similarity = options.getDouble(FLOW_NODE_OPTION_SIMILARITY);
JSONArray knowIdArray = options.getJSONArray(FLOW_NODE_OPTION_KNOW_IDS);
if (oConvertUtils.isObjectEmpty(knowIdArray)) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择知识库");
}
List<String> knowIds = knowIdArray.stream().map(Object::toString).collect(Collectors.toList());
// 向量查询
KnowledgeSearchResult resultData = embeddingHandler.embeddingSearch(knowIds, question.toString(), topNumber, similarity);
QueryRouter queryRouter = embeddingHandler.getQueryRouter(knowIds, topNumber, similarity);
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_DATA, resultData.getData());
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_DOCUMENTS, resultData.getDocuments());
contextBean.setContextData(flowNodeConfig.getNodeId(), OUTPUT_PARAM_NAME_QUERY_ROUTER, queryRouter);
}
}
@@ -0,0 +1,339 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import dev.langchain4j.data.message.*;
import dev.langchain4j.rag.query.router.QueryRouter;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.handler.AIChatParams;
import org.jeecg.modules.airag.common.handler.IAIChatHandler;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.common.vo.MessageHistory;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 大模型节点
*
* @author chenrui
* @date 2025/2/11 17:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_LLM)
public class LlmNode extends JeecgBaseFlowNode {
@Autowired
IAIChatHandler aiChatHandler;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-llm");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
// 整理入参
CollatedInputParams collatedParams = collateInputParams(component, inputParams);
// 整理消息
String messagesOpStr = options.getString(FLOW_NODE_OPTION_MODEL_MESSAGES);
List<MessageHistory> messages = JSONArray.parseArray(messagesOpStr, MessageHistory.class);
AssertUtils.assertNotEmpty("LLM节点至少配置一条用户消息", messages);
// 历史消息
Object historyMessageObj = contextBean.getRequestData(FLOW_INPUT_PARAM_HISTORY);
if (historyMessageObj instanceof List) {
List<MessageHistory> historyMessages = (List) historyMessageObj;
if (oConvertUtils.isObjectNotEmpty(historyMessages)) {
messages.addAll(0, historyMessages);
}
}
List<ChatMessage> chatMessages = convertToChatMessage(messages, collatedParams);
// AI发送消息
String response = null;
try {
response = sendMessage2AI(component, options.getJSONObject(FLOW_NODE_OPTION_MODEL),
collatedParams.queryRouter, contextBean, flowNodeConfig, chatMessages);
} catch (Exception e) {
//update-begin---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
if (e.getMessage().contains("timeout")) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), FlowConsts.FLOW_ERROR_MSG_LLM_TIMEOUT);
}
throw e;
//update-end---author:chenrui ---date:20250425 for[QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示------------
}
List<FlowNodeConfig.NodeParam> outputParams = flowNodeConfig.getOutputParams();
FlowNodeConfig.NodeParam outputParam = outputParams.get(0);
contextBean.setContextData(flowNodeConfig.getNodeId(), outputParam.getField(), response);
}
/**
* 给AI发消息
*
* @param component
* @param modelOption
* @param queryRouter
* @param contextBean
* @param flowNodeConfig
* @param chatMessages
* @return
* @author chenrui
* @date 2025/2/27 19:40
*/
private String sendMessage2AI(NodeComponent component, JSONObject modelOption, QueryRouter queryRouter,
JeecgFlowContext contextBean, FlowNodeConfig flowNodeConfig, List<ChatMessage> chatMessages) {
// 构造ai请求参数
if (!modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID) || oConvertUtils.isEmpty(modelOption.containsKey(FLOW_NODE_OPTION_MODEL_MODEID))) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择模型");
}
String modelId = modelOption.getString(FLOW_NODE_OPTION_MODEL_MODEID);
AIChatParams aiParams = buildAiParams(modelOption, queryRouter, flowNodeConfig);
return aiChatHandler.completions(modelId, chatMessages, aiParams);
}
/**
* 构造ai参数
*
* @param modelOption
* @param queryRouter
* @param flowNodeConfig
* @return
* @author chenrui
* @date 2025/3/3 20:10
*/
private static AIChatParams buildAiParams(JSONObject modelOption, QueryRouter queryRouter, FlowNodeConfig flowNodeConfig) {
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
// 历史消息数量
Integer historyNum = oConvertUtils.getInt(options.getInteger(FLOW_NODE_OPTION_HISTORY_NUM), 3);
AIChatParams aiChatParams = new AIChatParams();
aiChatParams.setQueryRouter(queryRouter);
aiChatParams.setMaxMsgNumber(historyNum);
JSONObject modelParams = modelOption.getJSONObject(FLOW_NODE_OPTION_MODEL_PARAMS);
if (null != modelParams) {
Double temperature = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TEMPERATURE);
if (null != temperature) {
aiChatParams.setTemperature(temperature);
}
Double topP = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_TOP);
if (null != topP) {
aiChatParams.setTopP(topP);
}
Double presencePenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_PRESENCE_PENALTY);
if (null != presencePenalty) {
aiChatParams.setPresencePenalty(presencePenalty);
}
Double frequencyPenalty = modelParams.getDouble(FLOW_NODE_OPTION_MODEL_FREQUENCY_PENALTY);
if (null != frequencyPenalty) {
aiChatParams.setFrequencyPenalty(frequencyPenalty);
}
Integer maxTokens = modelParams.getInteger(FLOW_NODE_OPTION_MODEL_MAX_TOKENS);
if (null != maxTokens) {
aiChatParams.setMaxTokens(maxTokens);
}
}
return aiChatParams;
}
/**
* 整理入参
*
* @param component
* @param inputParams
* @return
* @author chenrui
* @date 2025/2/18 21:13
*/
private CollatedInputParams collateInputParams(NodeComponent component, List<FlowNodeConfig.NodeParam> inputParams) {
JeecgFlowContext contextBean = getContext(component);
// 知识库查询路由
QueryRouter queryRouter = null;
// 消息替换map
Map<String, Object> messageReplaceMap = new HashMap<>();
List<String> images = new ArrayList<>();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
if (oConvertUtils.isEmpty(inputData)) {
continue;
}
// 判断入参是不是knowledge节点
FlowNode paramFlowNode = getFlowNodeByTag(component, inputParam.getNodeId());
if (null != paramFlowNode) {
if (FLOW_NODE_TYPE_KNOWLEDGE.equalsIgnoreCase(paramFlowNode.getType())) {
queryRouter = contextBean.getContextData(paramFlowNode.getId(), KnowledgeNode.OUTPUT_PARAM_NAME_QUERY_ROUTER);
continue;
}
}
boolean isImage = isParamImage(component, inputParam);
if (isImage) {
// 图片类型
if (inputData instanceof List) {
List<String> imageList = (List<String>) inputData;
if (oConvertUtils.isObjectNotEmpty(imageList)) {
images.addAll(imageList);
}
} else if (inputData instanceof String) {
if (((String) inputData).contains(",")) {
// 逗号分隔的图片
String[] imageArray = ((String) inputData).split(",");
if (oConvertUtils.isObjectNotEmpty(imageArray)) {
images.addAll(Arrays.asList(imageArray));
}
} else {
// 单个图片
images.add((String) inputData);
}
}
continue;
}
String placeholder = "{{" + inputParam.getName() + "}}";
messageReplaceMap.put(placeholder, inputData);
}
return new CollatedInputParams(queryRouter, messageReplaceMap, images);
}
protected static boolean isParamImage(NodeComponent component, FlowNodeConfig.NodeParam param) {
FlowNode paramFlowNode = getFlowNodeByTag(component, param.getNodeId());
if (null == paramFlowNode) {
return false;
}
FlowNodeConfig properties = paramFlowNode.getProperties();
if (FLOW_NODE_TYPE_START.equalsIgnoreCase(paramFlowNode.getType())) {
// 变量来源于开始节点
List<FlowNodeConfig.NodeParam> beginNodeInputParams = properties.getInputParams();
if (null != beginNodeInputParams) {
for (FlowNodeConfig.NodeParam inputParam : beginNodeInputParams) {
if (inputParam.getField().equalsIgnoreCase(param.getField())) {
if (FlowConsts.FLOW_PARAM_TYPE_PICTURE.equalsIgnoreCase(inputParam.getType())) {
// 参数是图片类型
return true;
}
}
}
}
} else {
// 不来源于开始
if (oConvertUtils.isNotEmpty(properties.getOutputParams())) {
for (FlowNodeConfig.NodeParam outputParam : properties.getOutputParams()) {
if (outputParam.getField().equalsIgnoreCase(param.getField())) {
if (FlowConsts.FLOW_PARAM_TYPE_PICTURE.equalsIgnoreCase(param.getType())) {
// 参数是图片类型
return true;
}
}
}
}
}
return false;
}
/**
* 整理后的入参
*
* @author chenrui
* @date 2025/2/18 21:12
*/
private static class CollatedInputParams {
public final QueryRouter queryRouter;
public final Map<String, Object> messageReplaceMap;
public final List<String> images;
public CollatedInputParams(QueryRouter queryRouter, Map<String, Object> messageReplaceMap, List<String> images) {
this.queryRouter = queryRouter;
this.messageReplaceMap = messageReplaceMap;
this.images = images;
}
}
/**
* 类型转换并替换占位符
*
* @param messages
* @param collatedInputParams
* @return
* @author chenrui
* @date 2025/2/20 20:28
*/
private List<ChatMessage> convertToChatMessage(List<MessageHistory> messages, CollatedInputParams collatedInputParams) {
Map<String, Object> messageReplaceMap = collatedInputParams.messageReplaceMap;
AtomicInteger userCount = new AtomicInteger(0);
List<ChatMessage> chatMessages = messages.stream()
.filter(Objects::nonNull)
.filter(message -> oConvertUtils.isNotEmpty(message.getContent()))
.map(message -> {
// 替换占位符
String content = message.getContent();
for (String placeholder : messageReplaceMap.keySet()) {
content = content.replace(placeholder, messageReplaceMap.get(placeholder).toString());
}
if (AiragConsts.MESSAGE_ROLE_SYSTEM.equalsIgnoreCase(message.getRole())) {
// 系统消息
return new SystemMessage(content);
} else if (AiragConsts.MESSAGE_ROLE_USER.equalsIgnoreCase(message.getRole())) {
// 用户消息,组装图片和文本
List<Content> contents = new ArrayList<>();
List<MessageHistory.ImageHistory> images = message.getImages();
if (oConvertUtils.isObjectNotEmpty(images)
&& !images.isEmpty()) {
contents.addAll(images.stream().map(imageHistory -> {
if (oConvertUtils.isNotEmpty(imageHistory.getUrl())) {
return ImageContent.from(imageHistory.getUrl());
} else {
return ImageContent.from(imageHistory.getBase64Data(), imageHistory.getMimeType());
}
}).collect(Collectors.toList()));
}
contents.add(TextContent.from(content));
userCount.getAndIncrement();
return UserMessage.from(contents);
} else {
// 助手消息
return new AiMessage(content);
}
}).collect(Collectors.toList());
AssertUtils.assertGt("LLM节点至少配置一条用户消息", userCount.get(), 0);
// 如果入参有图片,则将图片添加到最后一条用户消息中
if (oConvertUtils.isObjectNotEmpty(collatedInputParams.images)) {
for (int i = chatMessages.size() - 1; i >= 0; i--) {
ChatMessage message = chatMessages.get(i);
if (ChatMessageType.USER.equals(message.type())) {
UserMessage lastUserMessage = (UserMessage) message;
List<ImageContent> imageContents = aiChatHandler.buildImageContents(collatedInputParams.images);
List<Content> contents = new ArrayList<>(lastUserMessage.contents());
contents.addAll(imageContents);
chatMessages.set(i, UserMessage.from(contents));
break;
}
}
}
return chatMessages;
}
}
@@ -0,0 +1,90 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.common.vo.event.EventMessageData;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
/**
* @Description: 直接回复节点
* @Author: chenrui
* @Date: 2025/3/13 10:26
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_REPLY)
public class ReplyNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-http");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
Map<String, Object> inputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
inputs.put(inputParam.getName(), inputData);
}
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
// sse
SseEmitter emitter = contextBean.getEmitter();
if (null != emitter) {
// 回复内容
String content = options.getString(FlowConsts.FLOW_NODE_OPTION_CONTENT);
if (oConvertUtils.isNotEmpty(content)) {
Matcher matcher = FlowConsts.VAR_PLACEHOLDER_PATTERN.matcher(content);
StringBuffer resMessage = new StringBuffer();
while (matcher.find()) {
String key = matcher.group(1);
Object val = (oConvertUtils.isObjectNotEmpty(inputs)) ? inputs.get(key) : null;
matcher.appendReplacement(resMessage, (val != null) ? Matcher.quoteReplacement(val.toString()) : "");
}
matcher.appendTail(resMessage);
// 流式回复消息
EventData eventData = new EventData(contextBean.getRequestId(),
component.getChainId(), EventData.EVENT_MESSAGE, contextBean.getConversationId(), contextBean.getTopicId());
EventMessageData messageEventData = EventMessageData.builder()
.fromNodeId(flowNodeConfig.getNodeId())
.message(resMessage.toString())
.build();
eventData.setData(messageEventData);
try {
emitter.send(SseEmitter.event().data(JSONObject.toJSONString(eventData)));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
}
@@ -0,0 +1,53 @@
package org.jeecg.modules.airag.flow.component;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 开始节点
*
* @author chenrui
* @date 2025/2/11 17:13
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_START)
public class StartNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-begin");
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
JeecgFlowContext context = getContext(component);
// 初始化租户
if(oConvertUtils.isNotEmpty(context.getTenantId())){
TenantContext.setTenant(context.getTenantId());
}
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
Object requestData = context.getRequestData(inputParam.getField());
if(oConvertUtils.isEmpty(requestData)){
if (inputParam.getRequired() && !inputParam.getField().equals(FlowConsts.FLOW_INPUT_PARAM_HISTORY)) {
throw new AiRagFlowRuntimeException("缺少必要参数:" + inputParam.getName() + "(" + inputParam.getField() + ")");
}
}
context.setContextData(flowNodeConfig.getNodeId(), inputParam.getField(), requestData);
}
}
}
@@ -0,0 +1,96 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.FlowRunInnerParams;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.FLOW_NODE_OPTION_SUBFLOW_ID;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.FLOW_RESPONSE_MODE_BLOCKING;
/**
* 子流程节点
*
* @author chenrui
* @date 2025/2/26 17:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_SUBFLOW)
public class SubflowNode extends JeecgBaseFlowNode {
@Autowired
IAiragFlowService airagFlowService;
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-sub");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
String subflowId = options.getString(FLOW_NODE_OPTION_SUBFLOW_ID);
if (oConvertUtils.isEmpty(subflowId)) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "请选择子流程");
}
FlowRunInnerParams flowRunParams = new FlowRunInnerParams();
// 子流程id
flowRunParams.setFlowId(subflowId);
// 阻塞式返回
flowRunParams.setResponseMode(FLOW_RESPONSE_MODE_BLOCKING);
// 子流程入参
Map<String, Object> subInputs = new HashMap<>();
// 默认参数:历史记录
Object history = contextBean.getRequestData(FlowConsts.FLOW_INPUT_PARAM_HISTORY);
subInputs.put(FlowConsts.FLOW_INPUT_PARAM_HISTORY,history);
// 自定义入参
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
subInputs.put(inputParam.getName(), inputData);
}
flowRunParams.setInputParams(subInputs);
flowRunParams.setHttpRequest(contextBean.getHttpRequest());
flowRunParams.setCacheUser(contextBean.getCacheUser());
Result<?> subFlowResp = (Result<?>) airagFlowService.runFlow(flowRunParams);
if (!subFlowResp.isSuccess()) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "子流程执行失败:" + subFlowResp.getMessage());
}
Object respData = subFlowResp.getResult();
if(respData instanceof String){
// 输出文本
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT, respData);
}else {
Map<String,Object> respDataMap = (Map<String, Object>) respData;
respDataMap.forEach((key, val) -> {
contextBean.setContextData(flowNodeConfig.getNodeId(), key, val);
});
}
}
}
@@ -0,0 +1,163 @@
package org.jeecg.modules.airag.flow.component;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowCondition;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowSwitchRule;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import static org.jeecg.modules.airag.flow.consts.FlowConsts.*;
/**
* 流程节点:条件选择
*
* @author chenrui
* @date 2025/2/18 20:38
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_SWITCH)
public class SwitchNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_SWITCH, nodeType = NodeTypeEnum.SWITCH)
public String process(NodeComponent component) {
log.info("节点开始-switch");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
Map<String, Object> options = flowNodeConfig.getOptions();
List<Map<String, Object>> ifRules = (List<Map<String, Object>>) options.get(FLOW_NODE_OPTION_IF);
List<FlowSwitchRule> rules = new ArrayList<>();
if (null != ifRules && !ifRules.isEmpty()) {
rules = ifRules.stream()
.map(map -> JSONObject.parseObject(JSONObject.toJSONString(map), FlowSwitchRule.class))
.collect(Collectors.toList());
}
// if & elif
for (int i = 0; i < rules.size(); i++) {
FlowSwitchRule rule = rules.get(i);
List<FlowCondition> conditions = rule.getConditions();
String logic = rule.getLogic();
boolean ruleResult = false;
for (FlowCondition condition : conditions) {
Object conditionData = contextBean.getContextData(condition.getNodeId(), condition.getField());
ruleResult = evaluateCondition(conditionData, condition.getOperator(), condition.getValue());
if (ruleResult && FLOW_NODE_OPTION_LOGIC_OR.equals(logic) || !ruleResult && FLOW_NODE_OPTION_LOGIC_AND.equals(logic)) {
break;
}
}
if (ruleResult) {
log.info("[switch]跳转到{}", rule.getNext());
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, i);
return getNextTag(flowNodeConfig, rule.getNext());
}
}
// else
Map<String, Object> elseOption = (Map<String, Object>) options.get(FLOW_NODE_OPTION_ELSE);
if (null != elseOption && !elseOption.isEmpty()) {
log.info("[switch]跳转到{}", elseOption.get(FLOW_NODE_OPTION_NEXT));
contextBean.setContextData(flowNodeConfig.getNodeId(), FlowConsts.FLOW_OUTPUT_FIELD_INDEX, -1);
return getNextTag(flowNodeConfig, (String) elseOption.get("next"));
}
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "没有找到匹配的结果");
}
/**
* 执行表达式
*
* @param conditionData
* @param operator
* @param value
* @return
* @author chenrui
* @date 2025/2/13 14:11
*/
private boolean evaluateCondition(Object conditionData, String operator, String value) {
switch (operator) {
case FLOW_NODE_OPTION_OP_CONTAINS:
// 包含
return conditionData != null && conditionData.toString().contains(value);
case FLOW_NODE_OPTION_OP_NOT_CONTAINS:
// 不包含
return conditionData == null || !conditionData.toString().contains(value);
case FLOW_NODE_OPTION_OP_EQUALS:
// 相等
return conditionData != null && conditionData.toString().equals(value);
case FLOW_NODE_OPTION_OP_GTE:
// 大于等于
return conditionData != null && compareAsNumber(conditionData, value) >= 0;
case FLOW_NODE_OPTION_OP_GT:
// 大于
return conditionData != null && compareAsNumber(conditionData, value) > 0;
case FLOW_NODE_OPTION_OP_LTE:
// 小于等于
return conditionData != null && compareAsNumber(conditionData, value) <= 0;
case FLOW_NODE_OPTION_OP_LT:
// 小于
return conditionData != null && compareAsNumber(conditionData, value) < 0;
case FLOW_NODE_OPTION_OP_LEN_EQ:
// 长度比较:相等
return conditionData != null && conditionData.toString().length() == Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_GTE:
// 长度比较:大于等于
return conditionData != null && conditionData.toString().length() >= Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_GT:
// 长度比较:大于
return conditionData != null && conditionData.toString().length() > Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_LTE:
// 长度比较:小于等于
return conditionData != null && conditionData.toString().length() <= Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_LEN_LT:
// 长度比较:小于
return conditionData != null && conditionData.toString().length() < Integer.parseInt(value);
case FLOW_NODE_OPTION_OP_EMPTY:
// 是否为空
return oConvertUtils.isObjectEmpty(conditionData);
case FLOW_NODE_OPTION_OP_NOT_EMPTY:
// 是否不为空
return oConvertUtils.isObjectNotEmpty(conditionData);
default:
return false;
}
}
/**
* 数字比较
*
* @param conditionData
* @param value
* @return
* @author chenrui
* @date 2025/3/13 17:10
*/
private int compareAsNumber(Object conditionData, String value) {
try {
double conditionNumber = Double.parseDouble(conditionData.toString());
double valueNumber = Double.parseDouble(value);
return Double.compare(conditionNumber, valueNumber);
} catch (NumberFormatException e) {
throw new RuntimeException("Invalid number format for comparison", e);
}
}
}
@@ -0,0 +1,170 @@
package org.jeecg.modules.airag.flow.component.base;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 开始节点
*
* @author chenrui
* @date 2025/2/11 17:13
*/
@Slf4j
public abstract class JeecgBaseFlowNode {
public static FlowNode getFlowNode(NodeComponent component) {
String tag = component.getTag();
if (StringUtils.isEmpty(tag)) {
tag = component.getNodeId();
}
// 通过tag获取节点配置
return getFlowNodeByTag(component, tag);
}
/**
* 通过tag获取节点信息
*
* @param component
* @param tag
* @return
* @author chenrui
* @date 2025/2/18 20:33
*/
public static FlowNode getFlowNodeByTag(NodeComponent component, String tag) {
Map<String, FlowNode> flowNodes = getContext(component).getFlowNodes();
if (null != flowNodes && !flowNodes.isEmpty()) {
return flowNodes.get(tag);
}
return null;
}
/**
* 获取节点配置
*
* @param component
* @return
* @author chenrui
* @date 2025/2/19 16:52
*/
public static FlowNodeConfig getNodeConfig(NodeComponent component) {
FlowNode flowNode = getFlowNode(component);
if (null == flowNode) {
log.error("获取节点配置信息失败:nodeId:{},nodeTag::{},nodeTag:{}", component.getNodeId(), component.getTag());
throw new AiRagFlowRuntimeException(component.getNodeId(), "", "获取节点配置信息失败");
}
FlowNodeConfig properties = flowNode.getProperties();
if (null == properties) {
log.error("获取节点配置信息失败:nodeId:{},nodeTag:{}", component.getNodeId(), component.getTag());
throw new AiRagFlowRuntimeException(component.getNodeId(), "", "获取节点配置信息失败");
} else {
properties.setNodeId(flowNode.getId());
properties.setNodeType(flowNode.getType());
}
return properties;
}
/**
* 获取请求数据
*
* @param component
* @return
* @author chenrui
* @date 2025/2/19 16:52
*/
public static Map<String, Object> getRequestData(NodeComponent component) {
return getContext(component).getRequestDatas();
}
/**
* 获取上下文环境
*
* @param component
* @return
* @author chenrui
* @date 2025/2/19 16:53
*/
public static JeecgFlowContext getContext(NodeComponent component) {
return component.getContextBean(JeecgFlowContext.class);
}
/**
* @param nodeConfig
* @param context
* @return
* @author chenrui
* @date 2025/2/19 19:26
*/
public static Map<String, Object> getInputs(FlowNodeConfig nodeConfig, JeecgFlowContext context) {
Map<String, Object> inputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> inputParams = nodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
if (FlowConsts.FLOW_NODE_TYPE_START.equalsIgnoreCase(nodeConfig.getNodeType())) {
// 从requestData中获取
Object inputData = context.getRequestData(inputParam.getField());
inputs.put(inputParam.getField(), inputData);
} else {
// 从context中获取
Object inputData = context.getContextData(inputParam.getNodeId(), inputParam.getField());
inputs.put(inputParam.getNodeId() + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR + inputParam.getField(), inputData);
}
}
return inputs;
}
public static Object getOutputs(FlowNodeConfig nodeConfig, JeecgFlowContext context) {
if (FlowConsts.FLOW_NODE_TYPE_END.equalsIgnoreCase(nodeConfig.getNodeType())) {
// 从result中获取
return context.getResult();
} else {
Map<String, Object> outputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> outputParams = nodeConfig.getOutputParams();
for (FlowNodeConfig.NodeParam outputParam : outputParams) {
// 从context中获取
Map<String, Object> contextDatas = context.getContextDatas();
String prefix = nodeConfig.getNodeId() + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR;
contextDatas.forEach((key, value) -> {
if (key.startsWith(prefix)) {
outputs.put(key, value);
}
});
}
return outputs;
}
}
/**
* 获取next配置
*
* @param flowNodeConfig
* @param next
* @return
* @author chenrui
* @date 2025/2/21 19:53
*/
protected String getNextTag(FlowNodeConfig flowNodeConfig, String next) {
if (oConvertUtils.isEmpty(next)) {
log.error("{},{}未指定下一节点", flowNodeConfig.getNodeId(), flowNodeConfig.getText());
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "未指定下一节点");
}
if (next.startsWith("tag:")) {
return next;
} else {
return "tag:" + next;
}
}
}
@@ -0,0 +1,85 @@
package org.jeecg.modules.airag.flow.component.code;
import com.yomahub.liteflow.builder.LiteFlowNodeBuilder;
import com.yomahub.liteflow.enums.ScriptTypeEnum;
import com.yomahub.liteflow.flow.FlowBus;
import com.yomahub.liteflow.script.validator.ScriptValidator;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.FreemarkerHelper;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class CodeNode {
/**
* 动态构造并装载代码节点
*
* @param flowNodeObj
* @author chenrui
* @date 2025/2/14 16:44
*/
public static void buildCodeFLowNode(FlowNode flowNodeObj) {
FlowNodeConfig flowNodeConfig = flowNodeObj.getProperties();
if (!FlowConsts.FLOW_NODE_TYPE_CODE.equals(flowNodeObj.getType()) || oConvertUtils.isEmpty(flowNodeConfig)) {
return ;
}
Map<String, Object> options = flowNodeConfig.getOptions();
if (oConvertUtils.isEmpty(options) || !options.containsKey(FlowConsts.FLOW_NODE_OPTION_CODE)) {
return ;
}
String nodeId = flowNodeObj.getId();
if (!FlowBus.containNode(nodeId)) {
String codeType = (String) options.get(FlowConsts.FLOW_NODE_OPTION_CODE_TYPE);
AssertUtils.assertNotEmpty("请选择代码类型",codeType);
CodeNodeTypeEnum codeTypeEnum = CodeNodeTypeEnum.valueOf(codeType.toUpperCase());
if (codeTypeEnum == CodeNodeTypeEnum.PYTHON) {
log.warn("暂不支持python脚本节点");
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点[" + flowNodeConfig.getText() + "]:暂不支持python脚本,请使用其他脚本语言");
}
// 获取代码
String script = (String) options.get(FlowConsts.FLOW_NODE_OPTION_CODE);
AssertUtils.assertNotEmpty("请输入代码", script);
Map<String, Object> templateData = new HashMap<>();
templateData.put("inputParams", flowNodeConfig.getInputParams());
templateData.put("outputParams", flowNodeConfig.getOutputParams());
templateData.put("flowNodeObjId", nodeId);
templateData.put("scriptContent", script);
script = FreemarkerHelper.parseTemplate(codeTypeEnum.getTplPath(), templateData);
log.info("类型:{},脚本:{}", codeTypeEnum.getScriptType(), script);
if(ScriptValidator.validate(script, ScriptTypeEnum.getEnumByDisplayName(codeTypeEnum.getScriptType()))){
// 动态创建节点
LiteFlowNodeBuilder.createScriptNode().setId(nodeId)
.setScript(script)
.setLanguage(codeTypeEnum.getScriptType())
.build();
}else{
log.error("脚本校验失败,请检查脚本语法");
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点["+flowNodeConfig.getText()+"]:脚本校验失败,请检查脚本语法");
}
}
}
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("kotlin");
if (engine != null) {
System.out.println("Kotlin ScriptEngine loaded: " + engine);
} else {
System.err.println("Kotlin ScriptEngine not found!");
}
}
}
@@ -0,0 +1,48 @@
package org.jeecg.modules.airag.flow.component.code;
import lombok.Getter;
/**
* @Description: 代码节点类型枚举
* @Author: chenrui
* @Date: 2025/2/14 19:00
*/
@Getter
public enum CodeNodeTypeEnum {
/**
* js类型
*/
JAVASCRIPT("javascript", "js", "org/jeecg/modules/airag/flow/component/code/code_node_javascript.ftl"),
/**
* python类型
*/
PYTHON("python", "python", "org/jeecg/modules/airag/flow/component/code/code_node_python.ftl"),
/**
* groovy类型
*/
GROOVY("groovy", "groovy", "org/jeecg/modules/airag/flow/component/code/code_node_groovy.ftl"),
/**
* kotlin类型
*/
KOTLIN("kotlin", "kotlin", "org/jeecg/modules/airag/flow/component/code/code_node_kotlin.ftl"),
/**
* aviatorScript类型
*/
AVIATOR("aviator", "aviator", "org/jeecg/modules/airag/flow/component/code/code_node_aviator.ftl");
String name;
/**
* 对应liteFlow的脚本类型
*/
String scriptType;
String tplPath;
CodeNodeTypeEnum(String name,String type, String tplPath) {
this.name = name;
this.scriptType = type;
this.tplPath = tplPath;
}
}
@@ -0,0 +1,12 @@
let params = seq.map();
<#list inputParams as inputParam>
seq.put(params, "${inputParam.name}",getContextData(jeecgFlowContext, "${inputParam.nodeId}", "${inputParam.field}"));
</#list>
println(params);
${scriptContent}
println(resp);
<#list outputParams as outputParam>
setContextData(jeecgFlowContext, "${flowNodeObjId}", "${outputParam.field}", seq.get(resp,"${outputParam.field}"));
</#list>
@@ -0,0 +1,12 @@
${scriptContent}
def params = [:]
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}")
</#list>
def resp = main(params)
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}", "${outputParam.field}", resp["${outputParam.field}"])
</#list>
@@ -0,0 +1,12 @@
${scriptContent}
var params = {};
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}");
</#list>
var resp = main(params);
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}","${outputParam.field}", resp["${outputParam.field}"]);
</#list>
@@ -0,0 +1,16 @@
import org.jeecg.modules.airag.flow.context.JeecgFlowContext
val jeecgFlowContext = bindings["jeecgFlowContext"] as JeecgFlowContext
val params = mutableMapOf<String, Any?>()
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}")
</#list>
${scriptContent}
val resp = main(params)
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}", "${outputParam.field}", resp["${outputParam.field}"])
</#list>
@@ -0,0 +1,10 @@
params = {}
<#list inputParams as inputParam>
params["${inputParam.name}"] = jeecgFlowContext.getContextData("${inputParam.nodeId}", "${inputParam.field}")
</#list>
${scriptContent}
<#list outputParams as outputParam>
jeecgFlowContext.setContextData("${flowNodeObjId}", "${outputParam.field}", resp.get("${outputParam.field}"))
</#list>
@@ -0,0 +1,94 @@
package org.jeecg.modules.airag.flow.component.enhance;
import com.alibaba.fastjson.JSONObject;
import com.yomahub.liteflow.annotation.LiteflowMethod;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.enums.LiteFlowMethodEnum;
import com.yomahub.liteflow.enums.NodeTypeEnum;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.util.MyClassLoader;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.common.utils.JdkVersionCondition;
import org.jeecg.modules.airag.flow.component.base.JeecgBaseFlowNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.exception.AiRagFlowRuntimeException;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.context.annotation.Conditional;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* java增强节点
*
* @author chenrui
* @date 2025/2/26 21:25
*/
@Slf4j
@Conditional(JdkVersionCondition.class)
@Component(FlowConsts.FLOW_NODE_TYPE_ENHANCE_JAVA)
public class EnhanceJavaNode extends JeecgBaseFlowNode {
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeType = NodeTypeEnum.COMMON)
public void process(NodeComponent component) {
log.info("节点开始-enhanceJava");
JeecgFlowContext contextBean = getContext(component);
FlowNodeConfig flowNodeConfig = getNodeConfig(component);
if (oConvertUtils.isObjectEmpty(flowNodeConfig.getOptions())) {
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "节点配置错误");
}
JSONObject options = JSONObject.parseObject(JSONObject.toJSONString(flowNodeConfig.getOptions()));
JSONObject enhanceConf = options.getJSONObject(FlowConsts.FLOW_NODE_OPTION_ENHANCE);
IAiRagEnhanceJava enhanceObject = getEnhanceObject(flowNodeConfig, enhanceConf);
// 增强类入参
Map<String, Object> enhanceInputs = new HashMap<>();
List<FlowNodeConfig.NodeParam> inputParams = flowNodeConfig.getInputParams();
for (FlowNodeConfig.NodeParam inputParam : inputParams) {
// 从context中获取
Object inputData = contextBean.getContextData(inputParam.getNodeId(), inputParam.getField());
enhanceInputs.put(inputParam.getName(), inputData);
}
// 执行
Map<String, Object> enhanceResp;
try {
enhanceResp = enhanceObject.process(enhanceInputs);
}catch(Throwable t){
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "运行java增强失败:" + t.getMessage());
}
// 设置出参
enhanceResp.forEach((key, val) -> {
contextBean.setContextData(flowNodeConfig.getNodeId(), key, val);
});
}
private IAiRagEnhanceJava getEnhanceObject(FlowNodeConfig flowNodeConfig, JSONObject enhanceConf) {
Object obj = null;
if (enhanceConf != null) {
String type = enhanceConf.getString("type");
String javaPath = enhanceConf.getString("path");
if (oConvertUtils.isNotEmpty(javaPath)) {
if ("class".equals(type)) {
try {
obj = MyClassLoader.getClassByScn(javaPath).newInstance();
} catch (InstantiationException | IllegalAccessException e) {
log.error(e.getMessage(), e);
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "获取增强对象失败:" + e.getMessage());
}
} else if ("spring".equals(type)) {
obj = SpringContextUtils.getBean(javaPath);
}
}
}
if (obj instanceof IAiRagEnhanceJava) {
return (IAiRagEnhanceJava) obj;
}
throw new AiRagFlowRuntimeException(flowNodeConfig.getNodeId(), flowNodeConfig.getText(), "获取增强对象失败:类型不匹配");
}
}
@@ -0,0 +1,21 @@
package org.jeecg.modules.airag.flow.component.enhance;
import java.util.Map;
/**
* AiRag流程java增强节点接口
* @author chenrui
* @date 2025/2/26 17:34
*/
public interface IAiRagEnhanceJava {
/**
* 执行流程
* @param inputParams 入参,内容与流程编排中配置的一致
* @return Map<String, Object> 返回值,返回值的key可以在编排中获取到.
* @author chenrui
* @date 2025/2/26 17:33
*/
Map<String, Object> process(Map<String, Object> inputParams);
}
@@ -0,0 +1,392 @@
package org.jeecg.modules.airag.flow.consts;
import java.util.regex.Pattern;
/**
* @Description: airag流程常量类
* @Author: chenrui
* @Date: 2025/2/12 17:35
*/
public class FlowConsts {
/**
* 流程状态:启用
*/
public static final String FLOW_STATUS_ENABLE = "enable";
/**
* 流程状态:禁用
*/
public static final String FLOW_STATUS_DISABLE = "disable";
/**
* 内置的输入参数: 历史记录
*/
public static final String FLOW_INPUT_PARAM_HISTORY = "history";
/**
* 内置的输入参数: 用户提问
*/
public static final String FLOW_INPUT_PARAM_QUESTION = "content";
/**
* 内置的输入参数: 图片列表
*/
public static final String FLOW_INPUT_PARAM_IMAGES = "images";
/**
* 参数类型:String
*/
public static final String FLOW_PARAM_TYPE_STRING = "string";
/**
* 参数类型:Number
*/
public static final String FLOW_PARAM_TYPE_NUMBER = "number";
/**
* 参数类型:Picture
*/
public static final String FLOW_PARAM_TYPE_PICTURE = "picture";
/**
* 参数类型:Object
*/
public static final String FLOW_PARAM_TYPE_OBJECT = "object";
/**
* 参数类型:String Array
*/
public static final String FLOW_PARAM_TYPE_STRING_ARRAY = "string[]";
/**
* 参数类型:Number Array
*/
public static final String FLOW_PARAM_TYPE_NUMBER_ARRAY = "number[]";
/**
* 参数类型:Object Array
*/
public static final String FLOW_PARAM_TYPE_OBJECT_ARRAY = "object[]";
/**
* 流程返回模式:流式
*/
public static final String FLOW_RESPONSE_MODE_STREAMING = "streaming";
/**
* 流程返回模式:阻塞式
*/
public static final String FLOW_RESPONSE_MODE_BLOCKING = "blocking";
/**
* 流程设计:节点列表
*/
public static final String FLOW_DESIGN_NODES = "nodes";
/**
* 流程元数据:输入
*/
public static final String FLOW_METADATA_INPUTS = "inputs";
/**
* 流程元数据:输出
*/
public static final String FLOW_METADATA_OUTPUTS = "outputs";
/**
* 流程节点类型:start
*/
public static final String FLOW_NODE_TYPE_START = "start";
/**
* 流程节点类型:switch
*/
public static final String FLOW_NODE_TYPE_SWITCH = "switch";
/**
* 流程节点类型:llm
*/
public static final String FLOW_NODE_TYPE_LLM = "llm";
/**
* 流程节点类型:knowledge
*/
public static final String FLOW_NODE_TYPE_KNOWLEDGE = "knowledge";
/**
* 流程节点类型:classifier
*/
public static final String FLOW_NODE_TYPE_CLASSIFIER = "classifier";
/**
* 流程节点类型:end
*/
public static final String FLOW_NODE_TYPE_END = "end";
/**
* 流程节点类型:code
*/
public static final String FLOW_NODE_TYPE_CODE = "code";
/**
* 流程节点类型:subflow
*/
public static final String FLOW_NODE_TYPE_SUBFLOW = "subflow";
/**
* 流程节点类型:java增强节点
*/
public static final String FLOW_NODE_TYPE_ENHANCE_JAVA = "enhanceJava";
/**
* 流程节点类型:http 请求
*/
public static final String FLOW_NODE_TYPE_HTTP = "http";
/**
* 流程节点类型:reply 直接回复
*/
public static final String FLOW_NODE_TYPE_REPLY = "reply";
/**
* 流程节点Options:代码类型
*/
public static final String FLOW_NODE_OPTION_CODE_TYPE = "codeType";
/**
* 流程节点Options:代码
*/
public static final String FLOW_NODE_OPTION_CODE = "code";
/**
* 流程节点Options:if
*/
public static final String FLOW_NODE_OPTION_IF = "if";
/**
* 流程节点Options:else
*/
public static final String FLOW_NODE_OPTION_ELSE = "else";
/**
* 流程节点Options:下一个节点(next)
*/
public static final String FLOW_NODE_OPTION_NEXT = "next";
/**
* 流程节点Options:结果条数
*/
public static final String FLOW_NODE_OPTION_TOP_NUMBER = "topNumber";
/**
* 流程节点Options:相似度
*/
public static final String FLOW_NODE_OPTION_SIMILARITY = "similarity";
/**
* 流程节点Options:知识库id集合
*/
public static final String FLOW_NODE_OPTION_KNOW_IDS = "knowIds";
/**
* 流程节点Options:http ( http)
*/
public static final String FLOW_NODE_OPTION_HTTP = "http";
/**
* 流程节点Options:model ( 模型)
*/
public static final String FLOW_NODE_OPTION_MODEL = "model";
/**
* 流程节点Options:modeId ( 模型id)
*/
public static final String FLOW_NODE_OPTION_MODEL_MODEID = "modeId";
/**
* 流程节点Options:params ( 模型参数)
*/
public static final String FLOW_NODE_OPTION_MODEL_PARAMS= "params";
/**
* 流程节点Options:温度
*/
public static final String FLOW_NODE_OPTION_MODEL_TEMPERATURE = "temperature";
/**
* 流程节点Options:多样性
*/
public static final String FLOW_NODE_OPTION_MODEL_TOP = "topP";
/**
* 流程节点Options:存在惩罚
*/
public static final String FLOW_NODE_OPTION_MODEL_PRESENCE_PENALTY = "presencePenalty";
/**
* 流程节点Options:频率惩罚
*/
public static final String FLOW_NODE_OPTION_MODEL_FREQUENCY_PENALTY = "frequencyPenalty";
/**
* 流程节点Options:最大标记
*/
public static final String FLOW_NODE_OPTION_MODEL_MAX_TOKENS = "maxTokens";
/**
* 流程节点Options:messages ( 消息列表)
*/
public static final String FLOW_NODE_OPTION_MODEL_MESSAGES = "messages";
/**
* 流程节点Options:历史消息数量
*/
public static final String FLOW_NODE_OPTION_HISTORY_NUM = "history";
/**
* 流程节点Options:categories ( 分类列表)
*/
public static final String FLOW_NODE_OPTION_MODEL_CATEGORIES = "categories";
/**
* 流程节点Options:category ( 分类)
*/
public static final String FLOW_NODE_OPTION_MODEL_CATEGORY = "category";
/**
* 流程节点Options:子流程id
*/
public static final String FLOW_NODE_OPTION_SUBFLOW_ID = "subflowId";
/**
* 流程节点Options:增强
*/
public static final String FLOW_NODE_OPTION_ENHANCE = "enhance";
/**
* 流程节点Options:是否输出文本
*/
public static final String FLOW_NODE_OPTION_OUTPUT_TEXT = "outputText";
/**
* 流程节点Options:输出内容
*/
public static final String FLOW_NODE_OPTION_OUTPUT_CONTENT = "outputContent";
/**
* 条件逻辑:与
*/
public static final String FLOW_NODE_OPTION_LOGIC_AND = "AND";
/**
* 条件逻辑:或
*/
public static final String FLOW_NODE_OPTION_LOGIC_OR = "OR";
/**
* 包含
*/
public static final String FLOW_NODE_OPTION_OP_CONTAINS = "CONTAINS";
/**
* 不包含
*/
public static final String FLOW_NODE_OPTION_OP_NOT_CONTAINS = "NOT_CONTAINS";
/**
* 等于
*/
public static final String FLOW_NODE_OPTION_OP_EQUALS = "EQUALS";
/**
* 大于等于
*/
public static final String FLOW_NODE_OPTION_OP_GTE = "GTE";
/**
* 大于
*/
public static final String FLOW_NODE_OPTION_OP_GT = "GT";
/**
* 小于等于
*/
public static final String FLOW_NODE_OPTION_OP_LTE = "LTE";
/**
* 小于
*/
public static final String FLOW_NODE_OPTION_OP_LT = "LT";
/**
* 长度等于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_EQ = "LEN_EQ";
/**
* 长度大于等于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_GTE = "LEN_GTE";
/**
* 长度大于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_GT = "LEN_GT";
/**
* 长度小于等于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_LTE = "LEN_LTE";
/**
* 长度小于
*/
public static final String FLOW_NODE_OPTION_OP_LEN_LT = "LEN_LT";
/**
* 为空
*/
public static final String FLOW_NODE_OPTION_OP_EMPTY = "EMPTY";
/**
* 不为空
*/
public static final String FLOW_NODE_OPTION_OP_NOT_EMPTY = "NOT_EMPTY";
/**
* 回复内容
*/
public static final String FLOW_NODE_OPTION_CONTENT = "content";
/**
* 上下文变量分隔符
*/
public static final String FLOW_CONTEXT_PARAM_SEPARATOR = ".";
/**
* 流程节点输出字段名:outputText <br/>
* 只有子流程用了
*/
public static final String FLOW_OUTPUT_FIELD_OUTPUT_TEXT = "outputText";
/**
* 分支节点:序号(选择项的下标)
*/
public static final String FLOW_OUTPUT_FIELD_INDEX = "index";
/**
* 分支节点:回复内容(选择项的描述)
*/
public static final String FLOW_OUTPUT_FIELD_CONTENT = "content";
/**
* 变量占位符正则表达式
*/
public static final Pattern VAR_PLACEHOLDER_PATTERN = Pattern.compile("\\{\\{(.*?)}}");
/**
* 流程错误信息:大模型调用超时
* for [QQYUN-12203]AI 聊天,超时或者服务器报错,给个友好提示
*/
public static final String FLOW_ERROR_MSG_LLM_TIMEOUT = "llm request timeout";
}
@@ -0,0 +1,164 @@
package org.jeecg.modules.airag.flow.context;
import lombok.Getter;
import lombok.Setter;
import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.modules.airag.common.vo.event.EventData;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
/**
* @Description: jeecg流程上下文
* @Author: chenrui
* @Date: 2025/2/11 19:06
*/
public class JeecgFlowContext implements Serializable {
private static final long serialVersionUID = -3630250155343804583L;
/**
* 请求id
*/
@Getter
@Setter
private String requestId = "";
/**
* 会话id
* 不一定有,如果是从会话过来的就有值
*/
@Getter
@Setter
private String conversationId;
/**
* 话题id
* 不一定有,如果是从会话过来的就有值
*/
@Getter
@Setter
private String topicId;
/**
* 结果数据 <br/>
* 类型:String or Map
* 取决于end节点配置
*/
@Getter
@Setter
private Object result = null;
/**
* 上下文数据
*/
private final ConcurrentHashMap<String, Object> contextData = new ConcurrentHashMap<>();
/**
* 请求参数
*/
Map<String, Object> requestData = new ConcurrentHashMap<>();
/**
* 流程节点map id:flowNode
*/
Map<String, FlowNode> flowNodes = new ConcurrentHashMap<>();
/**
* SSE emitter
*/
@Getter
@Setter
private SseEmitter emitter;
/**
* 事件回调
*/
@Getter
@Setter
private Consumer<EventData> eventCallback;
/**
* 调用流程的http请求
*/
@Getter
@Setter
HttpServletRequest httpRequest;
/**
* 用户信息
*/
@Setter
@Getter
SysUserCacheInfo cacheUser;
/**
* 租户信息
*/
@Getter
@Setter
String tenantId;
public JeecgFlowContext() {
}
/*****************************************上下文数据*********************************************/
public Map<String, Object> getContextDatas() {
return contextData;
}
public <T> T getContextData(String key) {
return contextData.containsKey(key) ? (T) contextData.get(key) : null;
}
public <T> T getContextData(String prefix, String key) {
key = prefix + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR + key;
return getContextData(key);
}
public <T> void setContextData(String key, T t) {
if (null != t) {
contextData.put(key, t);
}
}
public <T> void setContextData(String prefix, String key, T t) {
key = prefix + FlowConsts.FLOW_CONTEXT_PARAM_SEPARATOR + key;
setContextData(key, t);
}
/*****************************************请求参数*********************************************/
public Map<String, Object> getRequestDatas() {
return requestData;
}
public void setRequestDatas(Map<String, Object> requestData) {
this.requestData = requestData;
}
public <T> T getRequestData(String key) {
return requestData.containsKey(key) ? (T) requestData.get(key) : null;
}
/*****************************************节点详情*********************************************/
public Map<String, FlowNode> getFlowNodes() {
return flowNodes;
}
public void setFlowNodes(Map<String, FlowNode> flowNodes) {
this.flowNodes = flowNodes;
}
}
@@ -0,0 +1,305 @@
package org.jeecg.modules.airag.flow.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.FlowBus;
import com.yomahub.liteflow.springboot.LiteflowProperty;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.base.controller.JeecgController;
import org.jeecg.common.system.query.QueryGenerator;
import org.jeecg.common.util.AssertUtils;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.config.mybatis.MybatisPlusSaasConfig;
import org.jeecg.config.shiro.IgnoreAuth;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.service.IAiragFlowAiGenService;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.AigcTestDataParams;
import org.jeecg.modules.airag.flow.vo.api.FlowDebugParams;
import org.jeecg.modules.airag.flow.vo.api.FlowDesignParams;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
@Tag(name = "AiRag流程")
@RestController("airagFlowController")
@RequestMapping("/airag/flow")
@Slf4j
public class AiragFlowController extends JeecgController<AiragFlow, IAiragFlowService> {
@Autowired
private IAiragFlowService airagFlowService;
@Autowired
private IAiragFlowAiGenService airagFlowAiGenService;
@Autowired
LiteflowProperty liteflowProperty;
@Autowired
FlowExecutor flowExecutor;
/**
* 分页列表查询
*
* @param airagFlow
* @param pageNo
* @param pageSize
* @param req
* @return
*/
@GetMapping(value = "/list")
public Result<IPage<AiragFlow>> queryPageList(AiragFlow airagFlow,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize,
HttpServletRequest req) {
QueryWrapper<AiragFlow> queryWrapper = QueryGenerator.initQueryWrapper(airagFlow, req.getParameterMap());
Page<AiragFlow> page = new Page<AiragFlow>(pageNo, pageSize);
IPage<AiragFlow> pageList = airagFlowService.page(page, queryWrapper);
return Result.OK(pageList);
}
/**
* 添加
*
* @param airagFlow
* @return
*/
@PostMapping(value = "/add")
@RequiresPermissions("airag:flow:add")
public Result<String> add(@RequestBody AiragFlow airagFlow) {
Map<String, String> ruleSourceExtDataMap = liteflowProperty.getRuleSourceExtDataMap();
String appName = "jeecg";
if (null != ruleSourceExtDataMap) {
appName = oConvertUtils.getString(ruleSourceExtDataMap.get("applicationName"), "jeecg");
}
airagFlow.setApplicationName(appName);
airagFlow.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
airagFlowService.save(airagFlow);
return Result.OK("添加成功!", airagFlow.getId());
}
/**
* 编辑
*
* @param airagFlow
* @return
*/
@RequestMapping(value = "/edit", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:flow:edit")
public Result<String> edit(@RequestBody AiragFlow airagFlow) {
AiragFlow existsFlow = airagFlowService.getById(airagFlow.getId());
AssertUtils.assertNotEmpty("流程不存在", existsFlow);
airagFlow.setChain(null);
airagFlow.setDesign(null);
airagFlow.setApplicationName(null);
airagFlowService.updateById(airagFlow);
return Result.OK("编辑成功!");
}
/**
* 编排设计保存
*
* @param flowDesignParams
* @return
* @author chenrui
* @date 2025/2/19 11:37
*/
@RequestMapping(value = "/design/save", method = {RequestMethod.PUT, RequestMethod.POST})
@RequiresPermissions("airag:flow:designSave")
public Result<String> saveDesign(@RequestBody FlowDesignParams flowDesignParams) {
return airagFlowService.saveDesign(flowDesignParams);
}
/**
* 通过id删除
*
* @param id
* @return
*/
@DeleteMapping(value = "/delete")
@RequiresPermissions("airag:flow:delete")
public Result<String> delete(HttpServletRequest request, @RequestParam(name = "id", required = true) String id) {
//如果是saas隔离的情况下,判断当前租户id是否是当前租户下的
if (MybatisPlusSaasConfig.OPEN_SYSTEM_TENANT_CONTROL) {
AiragFlow flow = airagFlowService.getById(id);
//获取当前租户
String currentTenantId = TokenUtils.getTenantIdByRequest(request);
if (null == flow || !flow.getTenantId().equals(currentTenantId)) {
return Result.error("删除AI流程失败,不能删除其他租户的AI流程!");
}
}
FlowBus.removeChain(id);
airagFlowService.removeById(id);
return Result.OK("删除成功!");
}
/**
* 通过id查询
*
* @param id
* @return
*/
@GetMapping(value = "/queryById")
public Result<AiragFlow> queryById(@RequestParam(name = "id", required = true) String id) {
AiragFlow airagFlow = airagFlowService.getById(id);
if (airagFlow == null) {
return Result.error("未找到对应数据");
}
return Result.OK(airagFlow);
}
/**
* 获取子流程以及子流程的开始节点的参数列表
*
* @param id 主流程ID(用于排除自身)
* @param keywords 查询关键字(默认仅返回10条,可通过查询显示其他流程)
* @return
*/
@GetMapping(value = "/subflowList")
public Result<IPage<SubFlowResult>> subflowList(
@RequestParam(name = "id", required = false) String id,
@RequestParam(name = "keywords", required = false) String keywords,
@RequestParam(name = "pageNo", defaultValue = "1") Integer pageNo,
@RequestParam(name = "pageSize", defaultValue = "10") Integer pageSize
) {
IPage<AiragFlow> page = new Page<>(pageNo, pageSize);
IPage<SubFlowResult> result = airagFlowService.subflowPage(page, id, keywords);
return Result.OK(result);
}
/**
* 根据ID获取子流程以及子流程的开始节点的参数列表
*
* @param subflowId 子流程ID
* @return
*/
@GetMapping(value = "/querySubflowById")
public Result<SubFlowResult> querySubflowById(@RequestParam(name = "subflowId") String subflowId) {
SubFlowResult result = airagFlowService.querySubflowById(subflowId);
return Result.OK(result);
}
/**
* 导出excel
*
* @param request
* @param airagFlow
*/
@RequestMapping(value = "/exportXls")
public ModelAndView exportXls(HttpServletRequest request, AiragFlow airagFlow) {
return super.exportXls(request, airagFlow, AiragFlow.class, "AiRag流程");
}
/**
* 通过excel导入数据
*
* @param request
* @param response
* @return
*/
@RequestMapping(value = "/importExcel", method = RequestMethod.POST)
public Result<?> importExcel(HttpServletRequest request, HttpServletResponse response) {
return super.importExcel(request, response, AiragFlow.class);
}
/**
* 调试流程 <br/>
* 实时返回,通过SSE返回数据 <br/>
* 前端使用EventSource接收数据
*
* @param flowDebugParams
* @return 取决于responseMode的值: <br/>
* streaming : 流式模式(推荐),基于SSE实现类似打字机输出方式的流式返回。<br/>
* blocking : 阻塞模式,等待执行完毕后返回结果。
* @author chenrui
* @date 2025/3/04 11:17
*/
@PostMapping(value = "/debug")
public Object debugFlow(@RequestBody FlowDebugParams flowDebugParams) {
return airagFlowService.debugFlow(flowDebugParams);
}
/**
* 运行流程 <br/>
* 实时返回,通过SSE返回数据 <br/>
* 前端使用EventSource接收数据
*
* @param flowRunParams
* @return 取决于responseMode的值: <br/>
* streaming : 流式模式(推荐),基于SSE实现类似打字机输出方式的流式返回。<br/>
* blocking : 阻塞模式,等待执行完毕后返回结果。
* @author chenrui
* @date 2025/2/13 15:19
*/
@IgnoreAuth
@PostMapping(value = "/run")
public Object runFlow(@RequestBody FlowRunParams flowRunParams) {
return airagFlowService.runFlow(flowRunParams);
}
/**
* 运行流程 - GET版本 <br/>
* 将id作为路径变量,responseMode固定为blockinginputParams通过请求参数获取
* for [QQYUN-11718]【AI】积木报表对接AI流程编排接口展示报表
* @param id 流程ID(作为路径变量)
* @return 以阻塞模式返回流程执行结果
* @author chenrui
* @date 2025/4/30 11:15
*/
@IgnoreAuth
@GetMapping(value = "/run/{flowId}")
public Object runFlowGetSimple(@PathVariable(name = "flowId") String flowId, HttpServletRequest request) {
FlowRunParams flowRunParams = new FlowRunParams();
flowRunParams.setFlowId(flowId);
// 固定为blocking模式
flowRunParams.setResponseMode(FlowConsts.FLOW_RESPONSE_MODE_BLOCKING);
// 从请求参数中获取所有参数作为inputParams
Map<String, Object> inputParams = new HashMap<>();
Map<String, String[]> parameterMap = request.getParameterMap();
for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
String key = entry.getKey();
String[] values = entry.getValue();
// 单值参数直接使用第一个值,多值参数使用数组
if (values.length == 1) {
inputParams.put(key, values[0]);
} else {
inputParams.put(key, values);
}
}
flowRunParams.setInputParams(inputParams);
return airagFlowService.runFlow(flowRunParams);
}
/**
* 根据提示生成调试运行的测试数据
*
* @author sunjianlei
* @date 2025-4-9
*/
@PostMapping(value = "/aigc/test-data")
public Result<?> aigcTestData(@RequestBody AigcTestDataParams params) {
return airagFlowAiGenService.genTestData(params);
}
}
@@ -0,0 +1,123 @@
package org.jeecg.modules.airag.flow.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.jeecg.common.aspect.annotation.Dict;
import org.jeecgframework.poi.excel.annotation.Excel;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.util.Date;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
@Data
@TableName("airag_flow")
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@Schema(description="AiRag流程")
public class AiragFlow implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(type = IdType.ASSIGN_ID)
@Schema(description = "主键")
private String id;
/**
* 创建人
*/
@Schema(description = "创建人")
@Dict(dictTable = "sys_user",dicCode = "username",dicText = "realname")
private String createBy;
/**
* 创建日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "创建日期")
private Date createTime;
/**
* 更新人
*/
@Schema(description = "更新人")
private String updateBy;
/**
* 更新日期
*/
@JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Schema(description = "更新日期")
private Date updateTime;
/**
* 所属部门
*/
@Schema(description = "所属部门")
private String sysOrgCode;
/**
* 租户id
*/
@Excel(name = "租户id", width = 15)
@Schema(description = "租户id")
private String tenantId;
/**
* 应用名称
*/
@Excel(name = "应用名称", width = 15)
@Schema(description = "应用名称")
private String applicationName;
/**
* 名称
*/
@Excel(name = "名称", width = 15)
@Schema(description = "名称")
private String name;
/**
* 描述
*/
@Excel(name = "描述", width = 15)
@Schema(description = "描述")
private String descr;
/**
* 应用图标
*/
@Excel(name = "应用图标", width = 15)
@Schema(description = "应用图标")
private String icon;
/**
* 编排规则
*/
@Excel(name = "编排规则", width = 15)
@Schema(description = "编排规则")
private String chain;
/**
* 编排设计
*/
@Excel(name = "编排设计", width = 15)
@Schema(description = "编排设计")
private String design;
/**
* 状态(enable=启用、disable=禁用、release=发布)
*/
@Excel(name = "状态", width = 15)
@Schema(description = "状态")
private String status;
/**
* 元数据
*/
@Excel(name = "元数据", width = 15)
@Schema(description = "元数据")
private String metadata;
}
@@ -0,0 +1,33 @@
package org.jeecg.modules.airag.flow.exception;
import org.jeecg.common.exception.JeecgBootException;
/**
* 流程运行时异常
* @Author: chenrui
* @Date: 2025/2/14 11:24
*/
public class AiRagFlowRuntimeException extends JeecgBootException {
private static final long serialVersionUID = -9008987328133246231L;
String nodeId;
String nodeName;
public AiRagFlowRuntimeException(String message) {
super(message);
}
public AiRagFlowRuntimeException(String nodeId, String nodeName, String message) {
super(message);
this.nodeName = nodeName;
}
public String getNodeId() {
return nodeId;
}
public String getNodeName() {
return nodeName;
}
}
@@ -0,0 +1,17 @@
package org.jeecg.modules.airag.flow.mapper;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
public interface AiragFlowMapper extends BaseMapper<AiragFlow> {
@InterceptorIgnore(tenantLine = "true")
public AiragFlow getFlowByIdIgnoreTenant(String id);
}
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.jeecg.modules.airag.flow.mapper.AiragFlowMapper">
<select id="getFlowByIdIgnoreTenant" resultType="org.jeecg.modules.airag.flow.entity.AiragFlow" parameterType="string">
SELECT
*
FROM
airag_flow
WHERE id = #{id}
</select>
</mapper>
@@ -0,0 +1,19 @@
package org.jeecg.modules.airag.flow.service;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.flow.vo.api.AigcTestDataParams;
/**
* Aiflow 生成数据
*/
public interface IAiragFlowAiGenService {
/**
* 根据提示生成调试运行的测试数据
*
* @param params 提示词
* @return
*/
Result<?> genTestData(AigcTestDataParams params);
}
@@ -0,0 +1,67 @@
package org.jeecg.modules.airag.flow.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.vo.api.FlowDebugParams;
import org.jeecg.modules.airag.flow.vo.api.FlowDesignParams;
import org.jeecg.modules.airag.flow.vo.api.FlowRunParams;
import org.jeecg.modules.airag.flow.vo.api.SubFlowResult;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
public interface IAiragFlowService extends IService<AiragFlow> {
/**
* 运行流程 <br/>
* 实时返回,通过SSE返回数据 <br/>
* 前端使用EventSource接收数据
*
* @param flowRunParams
* @return
* @author chenrui
* @date 2025/2/20 16:11
*/
Object runFlow(FlowRunParams flowRunParams);
/**
* 保存设计
* @param flowDesignParams
* @author chenrui
* @date 2025/2/21 14:56
*/
Result<String> saveDesign(FlowDesignParams flowDesignParams);
/**
* 调试流程
* @param flowDebugParams
* @return
* @author chenrui
* @date 2025/3/4 11:18
*/
Object debugFlow(FlowDebugParams flowDebugParams);
/**
* 获取子流程以及子流程的开始节点的参数列表
*
* @param page
* @param id 主流程ID(用于排除自身)
* @param keywords 查询关键字(默认仅返回10条,可通过查询显示其他流程)
* @return
*/
IPage<SubFlowResult> subflowPage(IPage<AiragFlow> page, String id, String keywords);
/**
* 根据ID获取子流程以及子流程的开始节点的参数列表
*
* @param subflowId 子流程ID
* @return
*/
SubFlowResult querySubflowById(String subflowId);
}
@@ -0,0 +1,113 @@
package org.jeecg.modules.airag.flow.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONException;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.jeecg.chatgpt.dto.chat.MultiChatMessage;
import org.jeecg.chatgpt.service.AiChatService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.service.IAiragFlowAiGenService;
import org.jeecg.modules.airag.flow.vo.api.AigcTestDataParams;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.LinkedList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Service("airagFlowAiGenServiceImpl")
public class AiragFlowAiGenServiceImpl implements IAiragFlowAiGenService {
@Autowired
AiChatService aiChatService;
@Override
public Result<?> genTestData(AigcTestDataParams params) {
if (params == null || CollectionUtils.isEmpty(params.getFields())) {
return Result.error("没有待生成的字段");
} else {
JeecgBootBizTipException aiException = new JeecgBootBizTipException("Ai开小差了,请稍后再试");
// 遍历字段列表,拼接提示词
List<AigcTestDataParams.FieldItem> fields = params.getFields();
StringBuilder promptSB = new StringBuilder();
for (AigcTestDataParams.FieldItem field : fields) {
if (oConvertUtils.isEmpty(field.getField())) {
continue;
}
promptSB.append(field.getField());
if (oConvertUtils.isNotEmpty(field.getName())) {
promptSB.append("").append(field.getName()).append("");
}
promptSB.append("").append(field.getType());
if (field.isRequired()) {
promptSB.append(":必填");
}
promptSB.append("\n");
}
String prompt = promptSB.toString();
if (oConvertUtils.isEmpty(prompt)) {
return Result.error("没有待生成的字段");
}
// 从AI获取表设计数据
String schemaStr = genTestDataFromAi(prompt);
if (oConvertUtils.isEmpty(schemaStr)) {
throw aiException;
}
// 解析AI返回数据
JSONObject schemaObj;
try {
schemaObj = JSONArray.parseObject(schemaStr);
} catch (JSONException e) {
throw aiException;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw aiException;
}
JSONObject res = new JSONObject();
res.put("data", schemaObj);
return Result.OK("生成成功", res);
}
}
/**
* 通过AI生成测试数据
*/
private String genTestDataFromAi(String prompt) {
// 示例JSON
String demoJson = "{\"name\":\"张三\",\"age\":23}";
String sysMsgContent = "" +
"根据字段列表帮我生成一个测试数据JSON," +
"测试数据参考JSON如下:" + demoJson + "\n" +
"在该JSON数组中,key为字段的field,value为你生成的数据," +
"要严格按照字段类型,如果是文本需要保持在10个字左右";
List<MultiChatMessage> messages = new LinkedList<>();
messages.add(MultiChatMessage.builder().role(MultiChatMessage.Role.SYSTEM).content("请严格按照参考json数组的格式输出,不要有其他任何描述,输出内容应以{开头,以}结尾").build());
messages.add(MultiChatMessage.builder().role(MultiChatMessage.Role.USER).content(sysMsgContent).build());
messages.add(MultiChatMessage.builder().role(MultiChatMessage.Role.USER).content("\n\n我的字段如下:\n> " + prompt + "\n").build());
String gptResp = aiChatService.multiCompletions(messages);
if (StringUtils.isEmpty(gptResp)) {
throw new JeecgBootBizTipException("如果您想使用AI助手,请先设置相应配置!");
}
// 兼容 think 思考过程
if (gptResp.contains("</think>")) {
String[] thinkSplit = gptResp.split("</think>");
gptResp = thinkSplit[thinkSplit.length - 1];
}
log.debug("Ai返回结果:" + gptResp);
Pattern pattern = Pattern.compile("\\{.*}", Pattern.DOTALL);
Matcher matcher = pattern.matcher(gptResp);
String returnData = "";
if (matcher.find()) {
returnData = matcher.group(0);
}
return returnData;
}
}
@@ -0,0 +1,384 @@
package org.jeecg.modules.airag.flow.service.impl;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.FlowBus;
import com.yomahub.liteflow.flow.LiteflowResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.config.TenantContext;
import org.jeecg.common.exception.JeecgBootBizTipException;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.SysUserCacheInfo;
import org.jeecg.common.util.*;
import org.jeecg.modules.airag.common.consts.AiragConsts;
import org.jeecg.modules.airag.common.utils.AiragLocalCache;
import org.jeecg.modules.airag.flow.component.code.CodeNode;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.context.JeecgFlowContext;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.mapper.AiragFlowMapper;
import org.jeecg.modules.airag.flow.service.IAiragFlowService;
import org.jeecg.modules.airag.flow.vo.api.*;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* @Description: AiRag流程
* @Author: jeecg-boot
* @Date: 2025-02-12
* @Version: V1.0
*/
@Slf4j
@Service("airagFlowService")
public class AiragFlowServiceImpl extends ServiceImpl<AiragFlowMapper, AiragFlow> implements IAiragFlowService {
@Autowired
FlowExecutor flowExecutor;
@Autowired
AiragFlowMapper airagFlowMapper;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Override
public Result<String> saveDesign(FlowDesignParams flowDesignParams) {
AiragFlow existsFlow = getById(flowDesignParams.getId());
AssertUtils.assertNotEmpty("流程不存在", existsFlow);
AssertUtils.assertNotEmpty("流程规则不能为空", flowDesignParams.getChain());
AssertUtils.assertNotEmpty("设计数据不能为空", flowDesignParams.getDesign());
String design = existsFlow.getDesign();
String metadataStr = existsFlow.getMetadata();
JSONObject metadata = new JSONObject();
if (oConvertUtils.isNotEmpty(metadataStr)) {
metadata = JSONObject.parseObject(metadataStr);
}
if (StringUtils.isNotEmpty(design)) {
JSONObject designJson = JSONObject.parseObject(design);
JSONArray nodes = designJson.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
// 移除旧的code节点
removeCodeNodeCache(nodes);
updateMetadata(nodes, metadata);
} else {
metadata.put(FlowConsts.FLOW_METADATA_INPUTS, null);
metadata.put(FlowConsts.FLOW_METADATA_OUTPUTS, null);
}
// 移除旧的流程
if (FlowBus.containChain(existsFlow.getId())) {
FlowBus.removeChain(existsFlow.getId());
}
// 更新数据
existsFlow.setName(flowDesignParams.getName());
existsFlow.setChain(flowDesignParams.getChain());
existsFlow.setDesign(flowDesignParams.getDesign());
existsFlow.setMetadata(metadata.toJSONString());
updateById(existsFlow);
return Result.OK("保存成功");
}
/**
* 更新源数据
*
* @param nodes
* @param metadata
* @author chenrui
* @date 2025/3/12 17:52
*/
private static void updateMetadata(JSONArray nodes, JSONObject metadata) {
// 更新metadata
Map<String, FlowNodeConfig.NodeParam> outputParamMap = new HashMap<>();
for (Object node : nodes) {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
FlowNodeConfig properties = flowNodeObj.getProperties();
if (FlowConsts.FLOW_NODE_TYPE_START.equals(flowNodeObj.getType())) {
// 开始节点,更新元数据:输入参数
List<FlowNodeConfig.NodeParam> inputParams = properties.getInputParams();
metadata.put(FlowConsts.FLOW_METADATA_INPUTS, inputParams);
} else if (FlowConsts.FLOW_NODE_TYPE_END.equals(flowNodeObj.getType())) {
// 结束节点,更新元数据:输出参数
boolean outputText = false;
if (oConvertUtils.isObjectNotEmpty(properties.getOptions())) {
outputText = (boolean) properties.getOptions().get(FlowConsts.FLOW_NODE_OPTION_OUTPUT_TEXT);
}
if (outputText) {
FlowNodeConfig.NodeParam nodeParam = new FlowNodeConfig.NodeParam();
nodeParam.setField(FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT);
nodeParam.setType("string");
outputParamMap.put(nodeParam.getField(), nodeParam);
} else {
properties.getOutputParams().forEach(nodeParam -> {
outputParamMap.put(nodeParam.getField(), nodeParam);
});
}
}
}
metadata.put(FlowConsts.FLOW_METADATA_OUTPUTS, outputParamMap.values());
}
@Override
public Object runFlow(FlowRunParams flowRunParams) {
AiragFlow flow = airagFlowMapper.getFlowByIdIgnoreTenant(flowRunParams.getFlowId());
JeecgFlowContext context = initFlowNodes(flow, flowRunParams);
return executeFlow(flowRunParams, context);
}
@Override
public Object debugFlow(FlowDebugParams flowDebugParams) {
AiragFlow flow = flowDebugParams.getFlow();
String tempFLowId = UUIDGenerator.generate();
flow.setId(tempFLowId);
String appName = "jeecg";
flow.setApplicationName(appName);
flow.setStatus(FlowConsts.FLOW_STATUS_ENABLE);
flowDebugParams.setFlowId(tempFLowId);
flowDebugParams.setEventCallback(eventData -> {
// 移除临时流程
String eventFlowId = eventData.getFlowId();
if (FlowBus.containChain(eventFlowId)) {
FlowBus.removeChain(eventFlowId);
}
// 移除code
if (oConvertUtils.isNotEmpty(flow.getDesign())) {
JSONObject designJson = JSONObject.parseObject(flow.getDesign());
JSONArray nodes = designJson.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
removeCodeNodeCache(nodes);
}
});
JeecgFlowContext context = initFlowNodes(flow, flowDebugParams);
return executeFlow(flowDebugParams, context);
}
/**
* 初始化流程节点
*
* @param flowRunParams
* @return PAIR K : 临时新建的节点id, V : context
* @author chenrui
* @date 2025/2/19 16:00
*/
private JeecgFlowContext initFlowNodes(AiragFlow flow, FlowRunParams flowRunParams) {
AssertUtils.assertNotEmpty("流程不存在", flow);
AssertUtils.assertNotEmpty("请先设计流程", flow.getChain());
String design = flow.getDesign();
Map<String, FlowNode> flowNode;
if (StringUtils.isNotEmpty(design)) {
JSONObject designJson = JSONObject.parseObject(design);
JSONArray nodes = designJson.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
flowNode = new HashMap<>();
nodes.forEach(node -> {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
if (oConvertUtils.isEmpty(flowNodeObj)) {
return;
}
// 动态构造代码节点
CodeNode.buildCodeFLowNode(flowNodeObj);
flowNode.put(flowNodeObj.getId(), flowNodeObj);
});
} else {
flowNode = null;
}
JeecgFlowContext context = new JeecgFlowContext();
context.setFlowNodes(flowNode);
context.setRequestDatas(flowRunParams.getInputParams());
// 请求id
if (oConvertUtils.isNotEmpty(flowRunParams.getRequestId())) {
context.setRequestId(flowRunParams.getRequestId());
} else {
context.setRequestId(UUIDGenerator.generate());
}
// 会话id
if (oConvertUtils.isNotEmpty(flowRunParams.getConversationId())) {
context.setConversationId(flowRunParams.getConversationId());
}
// 话题
if (oConvertUtils.isNotEmpty(flowRunParams.getTopicId())) {
context.setTopicId(flowRunParams.getTopicId());
}
// 回调函数
if (oConvertUtils.isObjectNotEmpty(flowRunParams.getEventCallback())) {
context.setEventCallback(flowRunParams.getEventCallback());
}
// 设置request
if (oConvertUtils.isObjectNotEmpty(flowRunParams.getHttpRequest())) {
context.setHttpRequest(flowRunParams.getHttpRequest());
} else {
try {
HttpServletRequest request = SpringContextUtils.getHttpServletRequest();
context.setHttpRequest(request);
} catch (Exception ignored) {
}
}
SysUserCacheInfo cacheUserInfo = null;
// 当前登录用户
if (flowRunParams instanceof FlowRunInnerParams) {
cacheUserInfo = ((FlowRunInnerParams) flowRunParams).getCacheUser();
}
if (oConvertUtils.isObjectNotEmpty(cacheUserInfo)) {
context.setCacheUser(cacheUserInfo);
} else {
try {
LoginUser sysUser = (LoginUser) SecurityUtils.getSubject().getPrincipal();
if (null != sysUser) {
context.setCacheUser(sysBaseAPI.getCacheUser(sysUser.getUsername()));
}
} catch (Exception e) {
log.warn("SecurityUtils.getSubject() 获取用户信息异常:" + e.getMessage());
}
}
// 当前租户
String tenantId = TenantContext.getTenant();
if(oConvertUtils.isNotEmpty(tenantId)){
context.setTenantId(tenantId);
}
// 加载流程
log.info("流程编排规则:{}", flow.getChain());
if (!FlowBus.containChain(flow.getId())) {
FlowBus.reloadChain(flow.getId(), flow.getChain());
}
return context;
}
/**
* 移除脚本节点的缓存
*
* @param nodes
* @author chenrui
* @date 2025/3/5 19:24
*/
private static void removeCodeNodeCache(JSONArray nodes) {
nodes.forEach(node -> {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
if (oConvertUtils.isEmpty(flowNodeObj) || !FlowConsts.FLOW_NODE_TYPE_CODE.equals(flowNodeObj.getType())) {
return;
}
// 移除节点
if (FlowBus.containNode(flowNodeObj.getId())) {
FlowBus.removeNode(flowNodeObj.getId());
}
});
}
/**
* 运行流程
*
* @param flowRunParams
* @param context
* @return
* @author chenrui
* @date 2025/3/4 11:34
*/
private Object executeFlow(FlowRunParams flowRunParams, JeecgFlowContext context) {
if (FlowConsts.FLOW_RESPONSE_MODE_BLOCKING.equalsIgnoreCase(flowRunParams.getResponseMode())) {
// 阻塞式运行
LiteflowResponse flowResponse = flowExecutor.execute2Resp(flowRunParams.getFlowId(), null, context);
if (flowResponse.isSuccess()) {
JeecgFlowContext resultContext = flowResponse.getContextBean(JeecgFlowContext.class);
return Result.OK(resultContext.getResult());
} else {
if (null != flowResponse.getCause()) {
log.error(flowResponse.getCause().getMessage(), flowResponse.getCause());
}
return Result.error(flowResponse.getMessage());
}
} else {
SseEmitter emitter;
if (oConvertUtils.isNotEmpty(flowRunParams.getRequestId())) {
emitter = AiragLocalCache.get(AiragConsts.CACHE_TYPE_SSE, flowRunParams.getRequestId());
} else {
emitter = new SseEmitter(-0L);
emitter.onError(throwable -> {
log.warn("SEE向客户端发送消息失败: {}", throwable.getMessage());
try {
emitter.complete();
} catch (Exception ignore) {}
});
}
context.setEmitter(emitter);
// 异步运行(流式)
flowExecutor.execute2Future(flowRunParams.getFlowId(), null, context);
return emitter;
}
}
@Override
public IPage<SubFlowResult> subflowPage(IPage<AiragFlow> page, String id, String keywords) {
LambdaQueryWrapper<AiragFlow> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.orderByDesc(AiragFlow::getCreateTime);
queryWrapper.select(Arrays.asList(
AiragFlow::getId,
AiragFlow::getName,
AiragFlow::getDescr,
AiragFlow::getStatus,
AiragFlow::getDesign
));
if (oConvertUtils.isNotEmpty(id)) {
queryWrapper.ne(AiragFlow::getId, id);
}
// 如果传递了 keywords 就根据 keywords 进行模糊查询
if (oConvertUtils.isNotEmpty(keywords)) {
queryWrapper.like(AiragFlow::getName, "%" + keywords + "%");
}
IPage<AiragFlow> pageList = baseMapper.selectPage(page, queryWrapper);
List<AiragFlow> flowList = pageList.getRecords();
// 拼接返回结果
IPage<SubFlowResult> results = new Page<>();
results.setPages(pageList.getPages());
results.setTotal(pageList.getTotal());
results.setCurrent(page.getCurrent());
results.setSize(page.getSize());
results.setRecords(new ArrayList<>());
if (CollectionUtils.isEmpty(flowList)) {
return results;
}
for (AiragFlow flow : flowList) {
SubFlowResult result = new SubFlowResult(flow);
if (result.getInputParams() == null) {
continue;
}
results.getRecords().add(result);
}
return results;
}
@Override
public SubFlowResult querySubflowById(String subflowId) {
if (oConvertUtils.isEmpty(subflowId)) {
throw new JeecgBootBizTipException("subflowId 不能为空");
}
AiragFlow flow = this.getById(subflowId);
if (flow == null) {
throw new JeecgBootBizTipException("流程不存在");
}
SubFlowResult result = new SubFlowResult(flow);
if (result.getInputParams() == null) {
throw new JeecgBootBizTipException("流程配置异常");
}
return result;
}
}
@@ -0,0 +1,23 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import java.util.List;
/**
* Ai生成测试数据所需参数
*/
@Data
public class AigcTestDataParams {
// 待生成的字段
private List<FieldItem> fields;
@Data
public static class FieldItem {
private String field;
private String name;
private String type;
private boolean required;
}
}
@@ -0,0 +1,21 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
/**
* 流程调试入参
*
* @author chenrui
* @date 2025/3/4 11:39
*/
@Data
public class FlowDebugParams extends FlowRunParams {
private static final long serialVersionUID = 1L;
/**
* 流程实体
*/
private AiragFlow flow;
}
@@ -0,0 +1,33 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import java.io.Serializable;
/**
* 流程设计
*
* @author chenrui
* @date 2025/2/19 11:39
*/
@Data
public class FlowDesignParams implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键
*/
private String id;
/**
* 编排规则
*/
private String chain;
/**
* 编排设计
*/
private String design;
/**
* 流程名称
*/
private String name;
}
@@ -0,0 +1,22 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.jeecg.common.system.vo.SysUserCacheInfo;
/**
* 流程运行入参(内部使用)
*
* @author sjlei
* @date 2025-5-23 20:22:29
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class FlowRunInnerParams extends FlowRunParams {
/**
* 用户信息
*/
private SysUserCacheInfo cacheUser;
}
@@ -0,0 +1,66 @@
package org.jeecg.modules.airag.flow.vo.api;
import lombok.Data;
import org.jeecg.modules.airag.common.vo.event.EventData;
import javax.servlet.http.HttpServletRequest;
import java.io.Serializable;
import java.util.Map;
import java.util.function.Consumer;
/**
* 流程运行入参
*
* @author chenrui
* @date 2025/2/19 11:39
*/
@Data
public class FlowRunParams implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 流程id
*/
private String flowId;
/**
* 输入参数
*/
private Map<String, Object> inputParams;
/**
* 返回模式: <br/>
* streaming : 流式模式(推荐)。基于SSE实现类似打字机输出方式的流式返回。<br/>
* blocking : 阻塞模式,等待执行完毕后返回结果。
*/
private String responseMode;
/**
* 请求id
*/
private String requestId;
/**
* 事件回调 <br/>
* 流程的执行节点会调用该回调.
*/
private Consumer<EventData> eventCallback;
/**
* 会话id <br/>
* app调用流程时有值
*/
private String conversationId;
/**
* 话题id <br/>
* app调用流程时有值
*/
private String topicId;
/**
* http请求 <br/>
*/
private HttpServletRequest httpRequest;
}
@@ -0,0 +1,92 @@
package org.jeecg.modules.airag.flow.vo.api;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import lombok.Data;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.airag.flow.consts.FlowConsts;
import org.jeecg.modules.airag.flow.entity.AiragFlow;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNode;
import org.jeecg.modules.airag.flow.vo.flow.config.FlowNodeConfig;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 查询子流程的返回对象
*/
@Data
public class SubFlowResult {
private String id;
private String name;
private String descr;
private String status;
/**
* 开始节点的输入参数列表
*/
private List<FlowNodeConfig.NodeParam> inputParams;
/**
* 流程的输出参数列表
*/
private List<FlowNodeConfig.NodeParam> outputParams;
public SubFlowResult(AiragFlow flow) {
this.id = flow.getId();
this.name = flow.getName();
this.descr = flow.getDescr();
this.status = flow.getStatus();
this.inputParams = new ArrayList<>();
this.outputParams = new ArrayList<>();
this.initParams(flow);
}
private void initParams(AiragFlow flow) {
JSONObject design;
try {
design = JSONObject.parseObject(flow.getDesign());
} catch (Exception e) {
return;
}
if (null == design || !design.containsKey(FlowConsts.FLOW_DESIGN_NODES)) {
return;
}
JSONArray nodes = design.getJSONArray(FlowConsts.FLOW_DESIGN_NODES);
if (CollectionUtils.isEmpty(nodes)) {
return;
}
// 开始和结束节点
Map<String, FlowNodeConfig.NodeParam> outputParamMap = new HashMap<>();
for (Object node : nodes) {
FlowNode flowNodeObj = ((JSONObject) node).toJavaObject(FlowNode.class);
FlowNodeConfig properties = flowNodeObj.getProperties();
if (FlowConsts.FLOW_NODE_TYPE_START.equals(flowNodeObj.getType())) {
this.inputParams = properties.getInputParams();
} else if (FlowConsts.FLOW_NODE_TYPE_END.equals(flowNodeObj.getType())) {
boolean outputText = false;
if (oConvertUtils.isObjectNotEmpty(properties.getOptions())) {
outputText = (boolean) properties.getOptions().get(FlowConsts.FLOW_NODE_OPTION_OUTPUT_TEXT);
}
if (outputText) {
FlowNodeConfig.NodeParam nodeParam = new FlowNodeConfig.NodeParam();
nodeParam.setName(FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT);
nodeParam.setField(FlowConsts.FLOW_OUTPUT_FIELD_OUTPUT_TEXT);
outputParamMap.put(nodeParam.getField(),nodeParam);
} else {
properties.getOutputParams().forEach(nodeParam -> {
outputParamMap.put(nodeParam.getName(), nodeParam);
});
}
}
}
this.outputParams = new ArrayList<>(outputParamMap.values());
}
}
@@ -0,0 +1,23 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import lombok.Data;
@Data
public class FlowCondition {
/**
* 节点id
*/
private String nodeId;
/**
* 字段名
*/
private String field;
/**
* 操作符
*/
private String operator;
/**
* 值
*/
private String value;
}
@@ -0,0 +1,37 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import lombok.AllArgsConstructor;
import lombok.Data;
/**
* 流程节点
*
* @Author: chenrui
* @Date: 2025/2/12 10:47
*/
@Data
@AllArgsConstructor
public class FlowNode {
public FlowNode() {
}
/**
* id 节点唯一标识
*/
private String id;
/**
* 节点类型
*/
private String type;
/**
* 节点配置
*/
private FlowNodeConfig properties;
}
@@ -0,0 +1,76 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import java.util.List;
import java.util.Map;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
/**
* 节点参数
* @author chenrui
* @date 2025/2/11 18:22
*/
@Data
@AllArgsConstructor
public class FlowNodeConfig {
public FlowNodeConfig() {
}
/**
* 节点id
*/
private String nodeId;
/**
* 节点类型
*/
private String nodeType;
/**
* 入参
*/
private List<NodeParam> inputParams;
/**
* 出参
*/
private List<NodeParam> outputParams;
/**
* 选项
*/
private Map<String, Object> options;
/**
* 节点名称
*/
private String text;
@Getter
@Setter
public static class NodeParam {
/**
* 节点id
*/
private String nodeId;
/**
* 参数显示名称
*/
private String name;
/**
* 参数名
*/
private String field;
/**
* 类型
*/
private String type;
/**
* 是否必填
*/
private Boolean required;
}
}
@@ -0,0 +1,36 @@
package org.jeecg.modules.airag.flow.vo.flow.config;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* 分支流程规则
* @author chenrui
* @date 2025/2/13 11:25
*/
@Getter
@Setter
@AllArgsConstructor
public class FlowSwitchRule {
/**
* 逻辑 AND , OR
*/
private String logic;
/**
* 条件表达式
*/
private List<FlowCondition> conditions;
/**
* 跳转节点 tag:tagName
*/
private String next;
// 构造函数
public FlowSwitchRule() {
}
}
@@ -0,0 +1,45 @@
liteflow:
print-banner: false
parse-mode: PARSE_ONE_ON_FIRST_EXEC
rule-source-ext-data-map:
# url: jdbc:mysql://localhost:3306/poseidon
# driverClassName: com.mysql.cj.jdbc.Driver
# username: root
# password: 123456
applicationName: jeecg
#是否开启SQL日志
sqlLogEnabled: true
#是否开启SQL数据轮询自动刷新机制 默认不开启
# pollingEnabled: false
#SQL数据轮询时间间隔(s) 默认为60s
# pollingIntervalSeconds: 60
#规则配置后首次轮询的起始时间(s) 默认为60s
# pollingStartSeconds: 60
#以下是chain表的配置,这个一定得有
#编排规则表的表名
chainTableName: airag_flow
#编排规则表中应用名称存储字段名
chainApplicationNameField: application_name
#规则名称存储的字段名
chainNameField: id
#EL表达式的字段(只存EL)
elDataField: chain
#以下是决策路由字段的配置,如果你没用到决策路由,可以不配置
# routeField: route
# namespaceField: namespace
# #是否启用这条规则
# chainEnableField: enable
# #规则表自定义过滤SQL
chainCustomSql: select id, application_name, chain from airag_flow where status = 'enable' and chain is not null
#以下是script表的配置,如果你没使用到脚本,下面可以不配置
# scriptTableName: script
# scriptApplicationNameField: application_name
# scriptIdField: script_id
# scriptNameField: script_name
# scriptDataField: script_data
# scriptTypeField: script_type
# scriptLanguageField: script_language
# #是否启用这条脚本
# scriptEnableField: enable
# #脚本表自定义过滤SQL
# scriptCustomSql: 这里设置自定义脚本表SQL
@@ -0,0 +1,435 @@
## 有哪些提醒
- 催办提醒
- 任务超时提醒
- 新任务提醒
## 自定义开发表单与流程对接
##### 流程对接步骤
#####(1)设计流程:【流程管理-流程设计-新建流程】创建流程
![输入图片说明](https://static.oschina.net/uploads/img/201904/29112127_MERJ.png "在这里输入图片标题")
#####(2)表单挂接:流程设计完成后,选择流程【流程配置-业务管理-新增】增加表单关联
![输入图片说明](https://static.oschina.net/uploads/img/201905/13112345_Ns8g.png "在这里输入图片标题")
说明:
- 表单类型:选择自定义开发
- 表名:填写需要的对接的表单对应业务的表名,存在多种表时填写主表名
- 唯一编码:流程业务对接唯一编码,流程发起时需要用到该值,该值定义整个系统中不能重复,会根据表名生成一个默认编码,可自己修改, 例如:`dev_ext_biz_leave_001`
- 流程状态列名:记录流程状态的字段,在关联的Online表名指定的表中存在的字段,该字段用于接收,流程处理过程中回填过来的处理状态
- 标题表达式:流程提交后在,任务列表中展示的业务标题,例如:测试新流程【${name}】,其中表达式${name},取流程变量中的值
#####(3)表单流程发起代码实现:
申请单,进行流程发起
- 1、申请单列表增加按钮 `<a @click="startProcess(record)">`提交流程</a> 控制待提交状态下展示 `v-if="record.bpmStatus === '1'"`
- 2、提交流程,功能实现
```
1、data()中定义
flowCode:"dev_ext_biz_leave_001",
url: {
startProcess: "/act/process/extActProcess/startMutilProcess",
},
2、方法实现
startProcess: function(record){
var that = this;
this.$confirm({
title:"提示",
content:"确认提交流程吗?",
onOk: function(){
var param = {
flowCode:that.flowCode,
id:record.id,
formUrl:"modules/extbpm/biz/modules/ExtBizLeaveForm",
formUrlMobile:"modules/extbpm/biz/modules/ExtBizLeaveForm"
}
postAction(that.url.startProcess,param).then((res)=>{
if(res.success){
that.$message.success(res.message);
that.loadData();
that.onClearSelected();
}else{
that.$message.warning(res.message);
}
});
}
});
},
```
说明:提交流程是需要传递四个参数
- flowCode:流程业务对接唯一编码
- id:表单数据id
- formUrl:pc端流程审批,默认展示的表单组件
- formUrlMobile:移动端流程审批,默认展示的表单组件
#####(4)流程对接完成,发布流程,然后申请单提交流程即可。
## Online表单与流程对接
##### 流程对接步骤
#####(1)设计流程:【流程管理-流程设计-新建流程】创建流程
![输入图片说明](https://static.oschina.net/uploads/img/201904/29112127_MERJ.png "在这里输入图片标题")
#####2Online表单增加字段`bpm_status`,string类型,设置默认值1,字典code设置`bpm_status`
![输入图片说明](https://static.oschina.net/uploads/img/201905/13105615_67iA.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201905/13105710_uTZH.png "在这里输入图片标题")
#####(3)表单挂接:流程设计完成后,选择流程【流程配置-业务管理-新增】增加表单关联
表单类型:选择Online表单,
表名:填写对接的Online表单表名
唯一编码:自动生成,不能修改
流程状态列表:默认值`bpm_status`,不能修改
标题表达式:通过表达式`${}`获取流程变量的值,组成一个标题
![输入图片说明](https://static.oschina.net/uploads/img/201905/13105841_QLTG.png "在这里输入图片标题")
#####4)表单流程发起:
流入申请单,点击提交流程按钮发起流程,在【个人办公-我的任务中进行流程审批办理】
## Online表单与流程对接(开发人员对接步骤)
##### 流程对接步骤
(1)设计流程:【流程管理-流程设计-新建流程】创建流程
<img src="https://static.oschina.net/uploads/img/201904/29112127_MERJ.png" width="80%">
(2)表单挂接:流程设计完成后,选择流程【流程配置-业务管理-新增】增加表单关联
<img src="https://static.oschina.net/uploads/img/201904/29121406_rcP5.png" width="50%">
说明:
- 唯一编码:流程业务对接唯一编码,流程发起时需要用到该值,该值定义整个系统中不能重复,对接Online表单时,该编码命名规则`ol_{Online表名}` 例如:`ol_ext_biz_leave`
- 业务名称:对接的业务名称
- 表单类型:选择 Online表单
- Online表名:对接的Online表名,例如:`ext_biz_leave`
- 标题表达式:流程提交后在,任务列表中展示的业务标题,例如:测试新流程【${name}】,其中表达式${name},取流程变量中的值
- 流程状态列名:记录流程状态的字段,默认`bpm_status`,在关联的Online表名指定的表中存在的字段,该字段用于接收,流程处理过程中回填过来的处理状态
3)表单流程发起代码实现:
申请单,进行流程发起
- 1、申请单列表增加按钮 `<a @click="startProcess(record)">提交流程</a>` 控制待提交状态下展示 `v-if="record.bpm_status === '1'"`
- 2、提交流程,功能实现
```
1、data()中定义
flowCode:"ol_ext_biz_leave",
url: {
startProcess: "/act/process/extActProcess/startMutilProcess",
},
2、方法实现
startProcess: function(record){
var that = this;
this.$confirm({
title:"提示",
content:"确认提交流程吗?",
onOk: function(){
var param = {
flowCode:that.flowCode,
id:record.id,
formUrl:"modules/bpm/task/form/OnlineFormDetail",
formUrlMobile:"modules/bpm/task/form/OnlineFormDetail"
}
postAction(that.url.startProcess,param).then((res)=>{
if(res.success){
that.$message.success(res.message);
that.loadData();
that.onClearSelected();
}else{
that.$message.warning(res.message);
}
});
}
});
},
```
说明:提交流程是需要传递四个参数
- flowCode:流程业务对接唯一编码
- id:表单数据id
- formUrl:pc端流程审批,默认展示的表单组件
- formUrlMobile:移动端流程审批,默认展示的表单组件
组件modules/bpm/task/form/OnlineFormDetail 为Online表单查看信息的表单
组件modules/bpm/task/form/OnlineFormOpt 为Online表单带可编辑的操作的表单
(4)流程对接完成,发布流程,然后申请单提交流程即可。
## 流程节点个性化表单设置
####一、表单设置说明
流程节点个性化表单设置,解决不同节点审批人查看或者处理不同表单的需求,具体设置【流程配置--流程节点】,选择需要个性化设置的节点编辑,配置“PC表单地址”
![输入图片说明](https://static.oschina.net/uploads/img/201904/29154859_O8oe.png "在这里输入图片标题")
- 配置规则:
- (1)PC表单地址可配置组件或者第三表单地址(http/https开头)
- (2)配置组件地址:可对接自己开发的表单页面组件,也可以对接Online表单页面组件
自己开发的表单页面组件:开发方法见【流程节点对接表单页面开发方法】
选择views目录下开发页面
例如页面src/views/modules/extbpm/biz/modules/ExtBizLeaveForm.vue 则组件名为modules/extbpm/biz/modules/ExtBizLeaveForm
Online表单页面组件:
不带操作按钮的页面:modules/bpm/task/form/OnlineFormDetail
带操作按钮的页面:modules/bpm/task/form/OnlineFormOpt
- (3)配置第三表单地址(http/https开头),例如:http://www.test.com/formtest?dataId=${BPM_DATA_ID},其中${BPM_DATA_ID}去流程变量中的值。
流程变量固定值说明:
BPM_DATA_ID:表单数据id
BPM_FORM_KEY:表名
applyUserId:流程发起人
JG_LOCAL_PROCESS_ID:当前流程实例id
其他属性对应表单字段数据:例如表字段id,name,流程变量中也会存在
####流程节点对接表单页面开发方法
(1)在普通的列表表单页面改造,渠道<a-modal> 模态框
2)页面引入
```
import { httpAction,getAction } from '@/api/manage'
```
3)定义props
```
props: ['formData'],
```
4)定义data
```
url: {
queryById: "",//填写表单的queryById请求地址
},
```
(5)created方法实现
```
created () {
console.log("form start");
console.log("formdata",this.formData);
var params = {id:this.formData.dataId};//查询条件
getAction(this.url.queryById,params).then((res)=>{
if(res.success){
console.log("获取流程节点信息",res);
this.edit (res.result);
}
})
},
```
6'formData'对象说明:
节点对接页面组件时通过定义的props: ['formData'],传入数据,该对象为流程节点和页面的数据传输对象,可根据其中的数据展示表单
formData对象属性如下:
dataId:表单数据id
taskId:当前环节任务id
taskDefKey:当前环节任务key
procInsId:流程实例id
tableName:表名
permissionList:节点权限配置
vars:流程变量
页面接收该对象数据后可根据数据信息通过接口获取相关的业务数据展示表单。
## 页面权限控制
### 显示隐藏控制
####一、用法
```
v-has="'name'"
```
代码示例:
```
<a-form-item v-has="'name'"
:labelCol="labelCol"
:wrapperCol="wrapperCol"
label="请假人">
<a-input placeholder="请输入请假人" v-decorator="['name', {}]" />
</a-form-item>
```
####二、权限配置:
![输入图片说明](https://static.oschina.net/uploads/img/201905/16150058_JC6X.png "在这里输入图片标题")
####三、使用说明
- `v-has="'name'"` 指令值“name”为授权标识,可对该授权标识进行“显示/访问”控制
- 权限编码在【系统管理--菜单管理】中配置,添加按钮类型的菜单数据,授权标识配置值“name”,策略选择显示/访问,状态选择有效
- 控制规则:
- 1)使用`v-has`指令后,菜单权限中若没有对应指令编码的配置,则不显示控件,
- (2)权限配置无效状态时,则不进行权限控制,有效状态时进行控制
- (3)策略:显示/访问,未授权时不显示,授权后显示
####四、流程节点权限
1)说明:
- 节点权限配置优先级高于菜单权限配置
- 节点权限应用于使用组件方式加载的附加表单页面,并对附加表单页面进行权限控制
- 显示控制用法见上面用法描述
- 节点权限是通过 props: ['formData'],来传递给节点表单页面的,因此页面一定要定义这个,否则,节点配置的权限不生效
- 权限配置无效状态时,则不进行权限控制,有效状态时进行控制
2)权限配置:
在【流程管理-流程设计】中找到需要配置的流程,进入【流程配置-流程节点】选择需要进行权限控制的节点,
点击【更多-权限设置】,新增/编辑 来配置权限。
![输入图片说明](https://static.oschina.net/uploads/img/201904/29111317_wzTj.png "在这里输入图片标题")
### 禁用控制用法一
####一、用法
1)页面引入混入代码
```
import {DisabledAuthFilterMixin} from '@/mixins/DisabledAuthFilterMixin'
```
```
mixins: [DisabledAuthFilterMixin],
```
![输入图片说明](https://static.oschina.net/uploads/img/201905/16154133_smhI.png "在这里输入图片标题")
2)权限控制代码示例:
```
<a-input-number :disabled="disabledAuth('name')" v-decorator="[ 'days', {}]" />
```
####二、权限配置:
![输入图片说明](https://static.oschina.net/uploads/img/201905/16150845_JvPu.png "在这里输入图片标题")
####三、使用说明
- `:disabled="disabledAuth('name')" ` 调用方法disabledAuth,方法参数“name”为授权标识,该方法根据授权规则返回true/false,控制是否禁用
- 权限编码在【系统管理--菜单管理】中配置,添加按钮类型的菜单数据,授权标识配置值“name”,策略选择可编辑,状态选择有效
- 控制规则:
- (1)菜单权限中若没有对应指令编码的配置,则不进行禁用控制,
- (2)权限配置无效状态时,则不进行权限控制,有效状态时进行控制
- (3)策略:可编辑,未授权时控件禁用,授权后可编辑
####四、流程节点权限
1)说明:
- 节点权限配置优先级高于菜单权限配置
- 节点权限应用于使用组件方式加载的附加表单页面,并对附加表单页面进行权限控制
- 显示控制用法见上面用法描述
- 节点权限是通过 props: ['formData'],来传递给节点表单页面的,因此页面一定要定义这个,否则,节点配置的权限不生效,节点表单开发方法见【流程节点对接表单页面开发方法】
- 权限配置无效状态时,则不进行权限控制,有效状态时进行控制
2)权限配置:
在【流程管理-流程设计】中找到需要配置的流程,进入【流程配置-流程节点】选择需要进行权限控制的节点,
点击【更多-权限设置】,新增/编辑 来配置权限。
![输入图片说明](https://static.oschina.net/uploads/img/201904/29111317_wzTj.png "在这里输入图片标题")
### 禁用控制用法二
####一、用法
1)页面引入工具js
```
import { disabledAuthFilter } from "@/utils/authFilter"
```
(2)methods方法中实现:
```
disabledAuth(code){
return disabledAuthFilter(code);
},
```
![输入图片说明](https://static.oschina.net/uploads/img/201905/16152021_GhSq.png "在这里输入图片标题")
![输入图片说明](https://static.oschina.net/uploads/img/201905/16152033_mJqR.png "在这里输入图片标题")
2)权限控制代码示例:
```
<a-input-number :disabled="disabledAuth('name')" v-decorator="[ 'days', {}]" />
```
####二、权限配置:
![输入图片说明](https://static.oschina.net/uploads/img/201905/16150845_JvPu.png "在这里输入图片标题")
####三、使用说明
- `:disabled="disabledAuth('name')" ` 调用方法disabledAuth,方法参数“name”为授权标识,该方法根据授权规则返回true/false,控制是否禁用
- 权限编码在【系统管理--菜单管理】中配置,添加按钮类型的菜单数据,授权标识配置值“name”,策略选择可编辑,状态选择有效
- 控制规则:
- (1)菜单权限中若没有对应指令编码的配置,则不进行禁用控制,
- (2)权限配置无效状态时,则不进行权限控制,有效状态时进行控制
- (3)策略:可编辑,未授权时控件禁用,授权后可编辑
####四、流程节点权限
1)说明:
- 节点权限配置优先级高于菜单权限配置
- 节点权限应用于使用组件方式加载的附加表单页面,并对附加表单页面进行权限控制
- 显示控制用法见上面用法描述
- 节点权限是通过 props: ['formData'],来传递给节点表单页面的,因此页面一定要定义这个,否则,节点配置的权限不生效,节点表单开发方法见【流程节点对接表单页面开发方法】
- 权限配置无效状态时,则不进行权限控制,有效状态时进行控制
-
(2)methods方法中实现:
```
disabledAuth(code){
return disabledAuthFilter(code,this.formData);
},
```
2)权限配置:
在【流程管理-流程设计】中找到需要配置的流程,进入【流程配置-流程节点】选择需要进行权限控制的节点,
点击【更多-权限设置】,新增/编辑 来配置权限。
![输入图片说明](https://static.oschina.net/uploads/img/201904/29111317_wzTj.png "在这里输入图片标题")
## FineReport报表集成
##### 集成步骤
-1)集成maven依赖
```
<dependency>
<groupId>com.ifrp</groupId>
<artifactId>fr-server-boot</artifactId>
<version>6.5</version>
</dependency>
<dependency>
<groupId>com.ifrp</groupId>
<artifactId>fr-third</artifactId>
<version>6.5</version>
</dependency>
```
-(2)创建报表模板目录 src/main/resources/fr/reportlets
-(3)创建报表配置文件 src/main/resources/fr/resources
```
下载配置
链接:https://pan.baidu.com/s/11DGREUOT-iti6RCVg49p5A
提取码:8yww
```
-4)创建config集成报表Servlet
```
@Bean
public ServletRegistrationBean eventProperties() {
return new ServletRegistrationBean(new com.fr.web.ReportServlet(), "/ReportServer");
}
```
@@ -0,0 +1,247 @@
<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">
<modelVersion>4.0.0</modelVersion>
<artifactId>jeecg-boot-module-bpm-flowable</artifactId>
<parent>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-boot-platform</artifactId>
<version>3.8.0</version>
</parent>
<description>此模块属于商业功能范畴,需要购买商业授权才可使用,禁止盗卖、泄露、传播商业源码和商业资料,双方签订合同有保密协议,违者收回商业授权,并追究法律责任</description>
<url>http://www.jeecg.com</url>
<licenses>
<license>
<name>商业授权说明:商业版本只针对企业用户授权,提供更专业更强大的商业版本</name>
<url>http://jeecg.com/vip</url>
<distribution>禁止盗版买卖,违者必究,举报邮箱: jeecgos@163.com</distribution>
<comments>Copyright:北京国炬信息技术有限公司</comments>
</license>
</licenses>
<developers>
<developer>
<name>北京国炬信息技术有限公司</name>
<email>jeecgos@163.com</email>
</developer>
<developer>
<name>授权对象(合作企业): 中国船舶集团有限公司第七〇四研究所</name>
<email>授权范围:贵司通过商业合作,获得商业产品的使用权,但商业版源码、资料禁止传播、盗卖,违者必究!!!</email>
</developer>
</developers>
<scm>
<connection>http://www.jeecg.com</connection>
<developerConnection>http://www.guojusoft.com</developerConnection>
<url>http://www.jeecg.com/vip</url>
</scm>
<properties>
<flowable.version>6.8.0</flowable.version>
</properties>
<dependencies>
<!-- jeecg system api -->
<dependency>
<groupId>org.jeecgframework.boot</groupId>
<artifactId>jeecg-system-local-api</artifactId>
</dependency>
<!--flowable 核心依赖-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter-process</artifactId>
<version>${flowable.version}</version>
<exclusions>
<!-- 排除mybatis依赖,跟mybatis-plus冲突 -->
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--flowable 用户分组自定义配置依赖 -->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-idm-spring</artifactId>
<version>${flowable.version}</version>
<exclusions>
<!-- 排除mybatis依赖,跟mybatis-plus冲突 -->
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Flowable报错:Can't find scripting engine for 'javascript', 从Java15开始,JDK移除了内置的Nashorn JavaScript引擎 -->
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.4</version>
</dependency>
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-idm-spring-configurator</artifactId>
<version>${flowable.version}</version>
<exclusions>
<!-- 排除mybatis依赖,跟mybatis-plus冲突 -->
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--flowable json转换依赖-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-json-converter</artifactId>
<version>${flowable.version}</version>
</dependency>
<!-- flowable6.4以上添加这个依赖,用于解决流程图预览,发布报错问题-->
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>2.12.2</version>
<optional>true</optional>
</dependency>
<!--flowable 排版布局-->
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-bpmn-layout</artifactId>
<version>${flowable.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-langs</artifactId>
<version>2.7.1</version>
</dependency>
<!-- jsoup -->
<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.11.3</version>
</dependency>
<!-- dom4j -->
<dependency>
<groupId>dom4j</groupId>
<artifactId>dom4j</artifactId>
<exclusions>
<exclusion>
<groupId>xml-apis</groupId>
<artifactId>xml-apis</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Jsp版流程设计器
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>conmons-des</artifactId>
<version>1.7.41</version>
</dependency>
&lt;!&ndash; Jsp引擎支持 &ndash;&gt;
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<scope>provided</scope>
</dependency>-->
<!-- Jsp版流程设计器 -->
</dependencies>
<build>
<plugins>
<!-- ProGuard混淆插件-->
<plugin>
<groupId>com.github.wvengen</groupId>
<artifactId>proguard-maven-plugin</artifactId>
<version>2.6.0</version>
<executions>
<execution>
<!-- 打包的时候开始混淆-->
<phase>package</phase>
<goals><goal>proguard</goal></goals>
</execution>
</executions>
<configuration>
<putLibraryJarsInTempDir>true</putLibraryJarsInTempDir>
<!--指定混淆的jar,如springboot打出来的jar基于它混淆-->
<injar>${project.build.finalName}.jar</injar>
<!--输出的jar-->
<outjar>${project.build.finalName}-pg.jar</outjar>
<!-- 是否混淆-->
<obfuscate>true</obfuscate>
<options>
<option>-target ${java.version}</option> <!--指定java版本号-->
<option>-keepdirectories</option> <!-- 保持目录结构,否则会导致spring自动装配失败 -->
<option>-dontshrink</option> <!--默认开启,不做收缩(删除注释、未被引用代码)-->
<option>-dontoptimize</option><!--默认是开启的,这里关闭字节码级别的优化-->
<option>-adaptclassstrings</option><!--混淆类名之后,对使用Class.forName('className')之类的地方进行相应替代-->
<option>-ignorewarnings</option><!-- 忽略warn消息,如果提示org.apache.http.* 这个包里的类有问题,那么就加入下述代码:-keep class org.apache.http.** { *; } -dontwarn org.apache.http.**-->
<option>-keep class org.apache.logging.log4j.util.* { *; }</option>
<option>-dontwarn org.apache.logging.log4j.util.**</option>
<option>-keepattributes Exceptions,InnerClasses,Signature,Deprecated,SourceFile,LineNumberTable,*Annotation*,EnclosingMethod</option><!--对异常、注解信息在runtime予以保留,不然影响springboot启动-->
<!--不混淆所有interface接口-->
<option>-keepnames interface org.jeecg.modules.**.service.* { *; }</option>
<option>-keepnames interface org.jeecg.modules.**.mapper.* { *; }</option>
<!--<option>-keepclassmembers enum * { *; }</option>保留枚举成员及方法-->
<option>-keepparameternames</option>
<option>-keepclasseswithmembers public class * {
public static void main(java.lang.String[]);}</option> <!--保留main方法的类及其方法名-->
<!--忽略note消息,如果提示javax.annotation有问题,那麽就加入以下代码-->
<option>-dontnote javax.annotation.**</option>
<option>-dontnote sun.applet.**</option>
<option>-dontnote sun.tools.jar.**</option>
<option>-dontnote org.apache.commons.logging.**</option>
<option>-dontnote javax.inject.**</option>
<option>-dontnote org.aopalliance.intercept.**</option>
<option>-dontnote org.aopalliance.aop.**</option>
<option>-dontnote org.apache.logging.log4j.**</option>
<option>-dontnote module-info</option>
<!-- <option>-keep class * implements java.io.Serializable </option>-->
<option>-keep class org.jeecg.modules.extbpm.process.service.impl.FlowUtilService {*;}</option>
<option>-keep class org.jeecg.modules.aspect.SysUserAspect {*;}</option>
<option>-keep class org.jeecg.modules.aspect.SynFlowUserPrincipalDataInterceptor {*;}</option>
<option>-keep class org.jeecg.modules.bpm.util.JoaUtil {*;}</option>
<option>-keep class org.jeecg.modules.extbpm.process.common.CodeUtil {*** getUUIDNum(...);}</option>
<option>-keep class org.jeecg.modules.bpm.config.ProcessEngineConfig {*** configure(...);}</option>
<option>-keep class org.jeecg.modules.extbpm.process.service.impl.n {*** pointCut(...);}</option>
<!-- 这个注意JOA项目把这个类拷贝到对应目录 <option>-keep class org.jeecg.modules.extbpm.process.common.WorkFlowGlobals {*;}</option> -->
<option>-keep class org.jeecg.modules.bpm.dto.** {*;}</option>
<option>-keep class org.jeecg.modules.bpm.cmd.dto.** {*;}</option>
<option>-keep class org.jeecg.modules.extbpm.process.pojo.** {*;}</option>
<option>-keep class org.jeecg.modules.extbpm.process.common.expression.FlowNodeExpression {*;}</option>
<option>-keep class org.jeecg.modules.extbpm.service.ProcessUtils {*;}</option>
<option>-keep class org.jeecg.modules.bpm.cmd.CustomTimeStartJobCmd {*;}</option>
<option>-keep class org.jeecg.modules.extbpm.process.service.impl.BpmBaseExtApiImpl</option>
<option>-keep class org.jeecg.modules.bpm.job.CustomTimeStartJobEventHandler {*;}</option>
<option>-keep class org.jeecg.modules.extbpm.process.entity.** {*;}</option>
<option>-keep class org.jeecgframework.designer.vo.** {*;}</option>
<option>-keep class org.jeecg.modules.extbpm.job.UserAgentJob</option>
<option>-keep class org.jeecg.modules.extbpm.job.TimeOutTask</option>
<!-- 不混淆所有的set/get方法 -->
<option>-keepclassmembers public class * {void set*(***);***
get*();}</option>
<option>-keep class org.jeecg.modules.extbpm.listener.** { *; }</option>
<option>-keep class org.jeecg.modules.extbpm.process.service.IExtActProcessService {*** startMutilProcessApi(...);}</option>
<option>-keepclassmembers class * {
@org.springframework.beans.factory.annotation.Autowired *;
@org.springframework.beans.factory.annotation.Value *;
}
</option>
</options>
<libs>
<lib>${java.home}/</lib>
</libs>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,64 @@
///* Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//package org.activiti.spring.boot;
//
//import java.io.IOException;
//
//import javax.sql.DataSource;
//
//import org.activiti.spring.SpringAsyncExecutor;
//import org.activiti.spring.SpringProcessEngineConfiguration;
//import org.springframework.boot.autoconfigure.AutoConfigureAfter;
//import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
//import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
//import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
//import org.springframework.boot.context.properties.EnableConfigurationProperties;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.jdbc.datasource.DataSourceTransactionManager;
//import org.springframework.transaction.PlatformTransactionManager;
//
///**
// * 重构此类强制走Mybatis: 原来的逻辑是Activiti判断,如果项目使用了JPA,那走JPA,如果没有,则走Mybatis
// * @author scott
// * @参考链接:https://blog.csdn.net/jiaoshaoping/article/details/NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
// * @源码jar: activiti-spring-boot-starter-basic-5.22.0.jar
// * @date: 2019-04-13
// */
//@Configuration
//@AutoConfigureAfter(DataSourceAutoConfiguration.class)
//public class DataSourceProcessEngineAutoConfiguration {
//
// @Configuration
// //@ConditionalOnMissingClass(name= "javax.persistence.EntityManagerFactory")
// @EnableConfigurationProperties(ActivitiProperties.class)
// public static class DataSourceProcessEngineConfiguration extends AbstractProcessEngineAutoConfiguration {
//
// @Bean
// @ConditionalOnMissingBean
// public PlatformTransactionManager transactionManager(DataSource dataSource) {
// return new DataSourceTransactionManager(dataSource);
// }
//
// @Bean
// @ConditionalOnMissingBean
// public SpringProcessEngineConfiguration springProcessEngineConfiguration(
// DataSource dataSource,
// PlatformTransactionManager transactionManager,
// SpringAsyncExecutor springAsyncExecutor) throws IOException {
//
// return this.baseSpringProcessEngineConfiguration(dataSource, transactionManager, springAsyncExecutor);
// }
// }
//
//}
@@ -0,0 +1,72 @@
///* Licensed under the Apache License, Version 2.0 (the "License");
// * you may not use this file except in compliance with the License.
// * You may obtain a copy of the License at
// *
// * http://www.apache.org/licenses/LICENSE-2.0
// *
// * Unless required by applicable law or agreed to in writing, software
// * distributed under the License is distributed on an "AS IS" BASIS,
// * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// * See the License for the specific language governing permissions and
// * limitations under the License.
// */
//package org.activiti.spring.boot;
//
//import java.io.IOException;
//
//import javax.persistence.EntityManagerFactory;
//import javax.sql.DataSource;
//
//import org.activiti.spring.SpringAsyncExecutor;
//import org.activiti.spring.SpringProcessEngineConfiguration;
//import org.springframework.boot.autoconfigure.AutoConfigureAfter;
//import org.springframework.boot.autoconfigure.AutoConfigureBefore;
//import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
//import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
//import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
//import org.springframework.boot.context.properties.EnableConfigurationProperties;
//import org.springframework.context.annotation.Bean;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.orm.jpa.JpaTransactionManager;
//import org.springframework.transaction.PlatformTransactionManager;
//
///**
// * 重构此类强制走Mybatis: 原来的逻辑是Activiti判断,如果项目使用了JPA,那走JPA,如果没有,则走Mybatis
// * @author scott
// * @参考链接:https://blog.csdn.net/jiaoshaoping/article/details/NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
// * @源码jar: activiti-spring-boot-starter-basic-5.22.0.jar
// * @date: 2019-04-13
// */
//@Configuration
//@AutoConfigureBefore(DataSourceProcessEngineAutoConfiguration.class)
//@AutoConfigureAfter(DataSourceAutoConfiguration.class)
//public class JpaProcessEngineAutoConfiguration {
//
//// @Configuration
//// @ConditionalOnClass(name= "javax.persistence.EntityManagerFactory")
//// @EnableConfigurationProperties(ActivitiProperties.class)
//// public static class JpaConfiguration extends AbstractProcessEngineAutoConfiguration {
////
//// @Bean
//// @ConditionalOnMissingBean
//// public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
//// return new JpaTransactionManager(emf);
//// }
////
//// @Bean
//// @ConditionalOnMissingBean
//// public SpringProcessEngineConfiguration springProcessEngineConfiguration(
//// DataSource dataSource, EntityManagerFactory entityManagerFactory,
//// PlatformTransactionManager transactionManager, SpringAsyncExecutor springAsyncExecutor) throws IOException {
////
//// SpringProcessEngineConfiguration config = this.baseSpringProcessEngineConfiguration(dataSource,
//// transactionManager, springAsyncExecutor);
//// config.setJpaEntityManagerFactory(entityManagerFactory);
//// config.setTransactionManager(transactionManager);
//// config.setJpaHandleTransaction(false);
//// config.setJpaCloseEntityManager(false);
//// return config;
//// }
//// }
//
//}
@@ -0,0 +1,20 @@
//package org.jeecg;
//
//import org.springframework.boot.SpringApplication;
//import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
//import org.springframework.boot.autoconfigure.SpringBootApplication;
//
///**
// *
// * @author scott
// *
// */
//@SpringBootApplication
//@EnableAutoConfiguration(exclude={org.flowable.spring.boot.FlowableSecurityAutoConfiguration.class})
//public class JeecgBpmApplication {
//
// public static void main(String[] args) {
// System.setProperty("spring.devtools.restart.enabled", "false");
// SpringApplication.run(JeecgBpmApplication.class, args);
// }
//}
@@ -0,0 +1,41 @@
package org.jeecg.modules.aspect;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 拦截器——同步流程用户、角色、部门、职务
*/
@Configuration("synFlowUserPrincipalDataConfiguration")
public class SynFlowUserPrincipalDataConfiguration implements WebMvcConfigurer {
private static String[] URL_PATTERNS = new String[]{
"/sys/role/add",
"/sys/role/edit",
"/sys/role/delete",
"/sys/sysDepart/add",
"/sys/sysDepart/delete",
"/sys/user/doUpdateDepartInfo",
"/sys/user/deleteUserRoleBatch",
"/sys/tenant/invitationUser",
"/sys/user/userQuitAgent",
"/sys/user/batchEditUsers",
"/sys/user/addSysUserRole",
"/sys/position/removePositionUser",
"/sys/position/savePositionUser",
"/sys/position/add",
"/sys/position/edit",
"/sys/user/editTenantUser",
};
@Bean("synFlowUserPrincipalDataInterceptor")
public SynFlowUserPrincipalDataInterceptor dynFlowUserPrincipalDataInterceptor() {
return new SynFlowUserPrincipalDataInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(dynFlowUserPrincipalDataInterceptor()).addPathPatterns(URL_PATTERNS);
}
}
@@ -0,0 +1,54 @@
package org.jeecg.modules.aspect;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.modules.extbpm.process.controller.SyncTenantUserThreadTask;
import org.springframework.lang.Nullable;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* 注意:这里同步是同步用户下的所有数据(不区分租户)
*
* 针对零代码应用的操作请求,自动同步流程用户的 角色、部门、职务到流程中(用户下的所有租户数据一次性同步)
* @author zhang
*/
@Slf4j
public class SynFlowUserPrincipalDataInterceptor implements HandlerInterceptor {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
//触发同步流程主数据
log.info("===== 同步流程主数据 ===== URL:"+ request.getRequestURI());
String syncUserFlag = SyncTenantUserThreadTask.syncUserFlag;
if (syncUserFlag != null && "1".equals(syncUserFlag)) {
log.info("===== 正在同步中===== ");
} else {
SyncTenantUserThreadTask.syncUserFlag = "1";
String username = JwtUtil.getUserNameByToken(request);
//获取登录租户ID
String tenantId = TokenUtils.getTenantIdByRequest(request);
FutureTask<Boolean> futureTask = new FutureTask<Boolean>(new SyncTenantUserThreadTask(username, tenantId));
Thread newThread = new Thread(futureTask);
newThread.start();
try {
Boolean threadResult = futureTask.get(200, TimeUnit.MILLISECONDS);;
}catch (TimeoutException e) {
log.error("同步流程租户数据,等待数据200毫秒以上,异常信息!");
}catch (Exception e) {
e.printStackTrace();
log.error("fail:", e);
}
log.info("===== 同步流程主数据 完成 =====");
}
}
}
@@ -0,0 +1,250 @@
package org.jeecg.modules.aspect;
import java.util.Arrays;
import java.util.List;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.system.vo.UserAccountInfo;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.extbpm.process.pojo.DepInfo;
import org.jeecg.modules.extbpm.process.pojo.PositionInfo;
import org.jeecg.modules.extbpm.process.pojo.RoleInfo;
import org.jeecg.modules.extbpm.process.pojo.UserInfo;
import org.jeecg.modules.extbpm.process.service.IExtActProcessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
/**
*
* 用户同步到工作流
* @author zhoujf
*
*/
@Aspect
@Slf4j
@Component("sysUserAspect")
public class SysUserAspect {
@Lazy
@Autowired
private ISysBaseAPI sysBaseAPI;
@Autowired
private IExtActProcessService extActProcessService;
@Pointcut("execution(public * org.jeecg.modules.*.controller.SysUserController.add(..))")
public void excudeServiceAdd() {
}
@Around("excudeServiceAdd()")
public Object doAroundAdd(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
try {
Object[] args = pjp.getArgs();
JSONObject param = (JSONObject)args[0];
String userName = param.getString("username");
log.info("------------SysUserAspect------add----"+userName);
LoginUser user = sysBaseAPI.getUserByName(userName);
if(user!=null) {
if(CommonConstant.ACT_SYNC_1.equals(user.getActivitiSync())) {
//同步则进行同步用户和角色信息
UserInfo userInfo = new UserInfo();
userInfo.setUuid(user.getId());
userInfo.setFirstName(user.getRealname());
userInfo.setEmail(user.getEmail());
userInfo.setId(user.getUsername());
//将用户的角色同步到流程
List<RoleInfo> roleList = extActProcessService.getSysRolesByUserId(userInfo.getUuid());
//将用户的部门同步到流程
List<DepInfo> depList = extActProcessService.getSysDepsByUserId(userInfo.getUuid());
//将用户的职务同步到流程
List<PositionInfo> positionList = extActProcessService.getSysPositionsByUserId(userInfo.getUuid());
extActProcessService.saveActIdMembership(userInfo, roleList,depList,positionList);
}
}
} catch (Exception e) {
}
return result;
}
@Pointcut("execution(public * org.jeecg.modules.*.controller.SysUserController.edit(..))")
public void excudeServiceEdit() {
}
@Around("excudeServiceEdit()")
public Object doAroundEdit(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
try {
Object[] args = pjp.getArgs();
JSONObject param = (JSONObject)args[0];
String userName = param.getString("username");
log.info("------------SysUserAspect------edit----"+userName);
LoginUser user = sysBaseAPI.getUserByName(userName);
if(user!=null) {
if(CommonConstant.ACT_SYNC_1.equals(user.getActivitiSync())) {
// NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
//同步则进行同步用户和角色信息
UserInfo userInfo = new UserInfo();
userInfo.setUuid(user.getId());
userInfo.setFirstName(user.getRealname());
userInfo.setEmail(user.getEmail());
userInfo.setId(user.getUsername());
//将用户的角色同步到流程
List<RoleInfo> roleList = extActProcessService.getSysRolesByUserId(userInfo.getUuid());
//将用户的部门同步到流程中
List<DepInfo> depList = extActProcessService.getSysDepsByUserId(userInfo.getUuid());
//将用户的职务同步到流程
List<PositionInfo> positionList = extActProcessService.getSysPositionsByUserId(userInfo.getUuid());
extActProcessService.saveActIdMembership(userInfo, roleList,depList,positionList);
}else {
//不同步删除用户信息
extActProcessService.deleteActIdMembership(userName);
}
}
} catch (Exception e) {
}
return result;
}
@Pointcut("execution(public * org.jeecg.modules.*.controller.SysUserController.putRecycleBin(..))")
public void excudeServicePutRecycleBin() {
}
@Around("excudeServicePutRecycleBin()")
public Object doAroundPutRecycleBin(ProceedingJoinPoint pjp) throws Throwable {
Object result = pjp.proceed();
try {
Object[] args = pjp.getArgs();
JSONObject param = (JSONObject) args[0];
String userIds = param.getString("userIds");
log.info("------------SysUserAspect------PutRecycleBin----" + userIds);
if (oConvertUtils.isNotEmpty(userIds)) {
List<UserAccountInfo> userList = sysBaseAPI.queryAllUserByIds((userIds.split(",")));
for (UserAccountInfo user : userList) {
if (CommonConstant.ACT_SYNC_1.equals(user.getActivitiSync())) {
//同步则进行同步用户和角色信息
UserInfo userInfo = new UserInfo();
userInfo.setUuid(user.getId());
userInfo.setFirstName(user.getRealname());
userInfo.setEmail(user.getEmail());
userInfo.setId(user.getUsername());
//将用户的角色同步到流程
List<RoleInfo> roleList = extActProcessService.getSysRolesByUserId(userInfo.getUuid());
//将用户的部门同步到流程
List<DepInfo> depList = extActProcessService.getSysDepsByUserId(userInfo.getUuid());
//将用户的职务同步到流程
List<PositionInfo> positionList = extActProcessService.getSysPositionsByUserId(userInfo.getUuid());
extActProcessService.saveActIdMembership(userInfo, roleList,depList,positionList);
}
}
}
} catch (Exception e) {
}
return result;
}
@Pointcut("execution(public * org.jeecg.modules.*.controller.SysUserController.delete(..))")
public void excudeServiceDelete() {
}
@Around("excudeServiceDelete()")
public Object doAroundDelete(ProceedingJoinPoint pjp) throws Throwable {
try {
Object[] args = pjp.getArgs();
String id = (String)args[0];
log.info("------------SysUserAspect------delete----"+id);
LoginUser user = sysBaseAPI.getUserById(id);
if(user!=null) {
//不同步删除用户信息
extActProcessService.deleteActIdMembership(user.getUsername());
}
} catch (Exception e) {
}
Object result = pjp.proceed();
return result;
}
@Pointcut("execution(public * org.jeecg.modules.*.controller.SysUserController.deleteBatch(..))")
public void excudeServiceDeleteBatch() {
}
@Around("excudeServiceDeleteBatch()")
public Object doAroundDeleteBatch(ProceedingJoinPoint pjp) throws Throwable {
try {
Object[] args = pjp.getArgs();
String ids = (String)args[0];
log.info("------------SysUserAspect------deleteBatch----"+ids);
String[] idArr = ids.split(",");
for(String id :idArr) {
LoginUser user = sysBaseAPI.getUserById(id);
if(user!=null) {
//不同步删除用户信息
extActProcessService.deleteActIdMembership(user.getUsername());
}
}
} catch (Exception e) {
}
Object result = pjp.proceed();
return result;
}
//----------------------------------------------------------------------------------------------------
/**
* 系统角色删除,同步删除流程中的角色
*/
@Pointcut("execution(public * org.jeecg.modules.*.controller.SysRoleController.delete(..))")
public void excudeServiceRoleDelete() {
}
@Pointcut("execution(public * org.jeecg.modules.*.controller.SysRoleController.deleteBatch(..))")
public void excudeServiceRoleDeleteBatch() {
}
@Around("excudeServiceRoleDelete()")
public Object doAroundRoleDelete(ProceedingJoinPoint pjp) throws Throwable {
try {
Object[] args = pjp.getArgs();
String id = (String) args[0];
log.info("------------SysUserAspect---Role---delete----" + id);
//同步删除流程中的角色
String roleCode = sysBaseAPI.getRoleCodeById(id);
extActProcessService.deleteFlowRoleSet(roleCode);
} catch (Exception e) {
}
Object result = pjp.proceed();
return result;
}
@Around("excudeServiceRoleDeleteBatch()")
public Object doAroundRoleDeleteBatch(ProceedingJoinPoint pjp) throws Throwable {
try {
Object[] args = pjp.getArgs();
String ids = (String) args[0];
log.info("------------SysUserAspect---Roles---deleteBatch----" + ids);
String[] idArr = ids.split(",");
for (String id : idArr) {
//同步删除流程中的角色
String roleCode = sysBaseAPI.getRoleCodeById(id);
extActProcessService.deleteFlowRoleSet(roleCode);
}
} catch (Exception e) {
}
Object result = pjp.proceed();
return result;
}
//----------------------------------------------------------------------------------------------------
}
@@ -0,0 +1,53 @@
package org.jeecg.modules.aspect.event;
import lombok.extern.slf4j.Slf4j;
import org.jeecg.common.base.BaseMap;
import org.jeecg.common.config.mqtoken.UserTokenContext;
import org.jeecg.common.constant.FlowEventTypeConst;
import org.jeecg.common.modules.redis.listener.JeecgRedisListener;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.modules.extbpm.process.service.IExtActProcessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
/**
* 说明:系统角色删除,同步删除BPM中的角色
* <p>
*
* @author: scott
* @date: 2024-05-27
*/
@Slf4j
@Component(FlowEventTypeConst.DELETE_ROLE_2_BPM_REDIS_HANDLER)
@ConditionalOnMissingClass("org.jeecg.modules.system.controller.SysUserController") //与SysUserAspect互斥关系
public class DeleteRole2BpmRedisHandler implements JeecgRedisListener {
@Lazy
@Autowired
private ISysBaseAPI sysBaseAPI;
@Autowired
private IExtActProcessService extActProcessService;
@Override
public void onMessage(BaseMap map) {
//1.设置线程会话Token
UserTokenContext.setToken(UpdateUser2BpmRedisHandler.getTemporaryToken());
log.info("【同步删除流程角色】redis Listener: {},参数:{}", FlowEventTypeConst.DELETE_ROLE_2_BPM_REDIS_HANDLER, map.toString());
String ids = map.get("roleIds");
log.info("------------DeleteRole2BpmRedisHandler---Roles---deleteBatch----" + ids);
String[] idArr = ids.split(",");
for (String id : idArr) {
//同步删除流程中的角色
String roleCode = sysBaseAPI.getRoleCodeById(id);
extActProcessService.deleteFlowRoleSet(roleCode);
}
//2.使用完删除Token,避免性能(这一步可以不做,但是为了性能建议执行)
UserTokenContext.remove();
}
}
@@ -0,0 +1,141 @@
package org.jeecg.modules.aspect.event;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.base.BaseMap;
import org.jeecg.common.config.mqtoken.UserTokenContext;
import org.jeecg.common.constant.CommonConstant;
import org.jeecg.common.constant.FlowEventTypeConst;
import org.jeecg.common.modules.redis.listener.JeecgRedisListener;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.util.JwtUtil;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.SpringContextUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.extbpm.process.pojo.DepInfo;
import org.jeecg.modules.extbpm.process.pojo.PositionInfo;
import org.jeecg.modules.extbpm.process.pojo.RoleInfo;
import org.jeecg.modules.extbpm.process.pojo.UserInfo;
import org.jeecg.modules.extbpm.process.service.IExtActProcessService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 说明:BPM作为独立微服务模块允许,编辑用户,不会自动同步用户到工作流中
* <p>
* 此类解决:: 微服务情况下,同步用户信息到流程中的问题
*
* @author: scott
* @date: 2024-05-27
*/
@Slf4j
@Component(FlowEventTypeConst.UPDATE_USER_2_BPM_REDIS_HANDLER)
@ConditionalOnMissingClass("org.jeecg.modules.system.controller.SysUserController") //与SysUserAspect互斥关系
public class UpdateUser2BpmRedisHandler implements JeecgRedisListener {
@Lazy
@Autowired
private ISysBaseAPI sysBaseAPI;
@Autowired
private IExtActProcessService extActProcessService;
@Override
public void onMessage(BaseMap map) {
//1.设置线程会话Token
UserTokenContext.setToken(getTemporaryToken());
log.info("【同步流程用户】redis Listener: {},参数:{}", FlowEventTypeConst.UPDATE_USER_2_BPM_REDIS_HANDLER, map.toString());
String userNames = map.get("userNames");
if (StringUtils.isEmpty(userNames)) {
return;
}
if (userNames.contains(",")) {
// 判断传入的是多个用户
String[] userNamesArray = userNames.split(",");
for (String userName : userNamesArray) {
if (StringUtils.isEmpty(userName)) {
continue;
}
// 进行处理
synchronizeUser(userName);
}
} else {
// 判断传入的是单个用户
// 进行处理
synchronizeUser(userNames);
}
//2.使用完删除Token,避免性能(这一步可以不做,但是为了性能建议执行)
UserTokenContext.remove();
}
/**
* 同步单个用户
*
* @param userName
*/
private void synchronizeUser(String userName) {
log.info("------------SysUserAspect------Synch----userName=" + userName);
LoginUser user = sysBaseAPI.getUserByName(userName);
if (user != null) {
if (CommonConstant.ACT_SYNC_1.equals(user.getActivitiSync())) {
// NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
//同步则进行同步用户和角色信息
UserInfo userInfo = new UserInfo();
userInfo.setUuid(user.getId());
userInfo.setFirstName(user.getRealname());
userInfo.setEmail(user.getEmail());
userInfo.setId(user.getUsername());
//将用户的角色同步到流程
List<RoleInfo> roleList = extActProcessService.getSysRolesByUserId(userInfo.getUuid());
//将用户的部门同步到流程中
List<DepInfo> depList = extActProcessService.getSysDepsByUserId(userInfo.getUuid());
//将用户的职务同步到流程
List<PositionInfo> positionList = extActProcessService.getSysPositionsByUserId(userInfo.getUuid());
extActProcessService.saveActIdMembership(userInfo, roleList, depList, positionList);
} else {
// 从流程中删除用户信息(逻辑删除用户 或 用户变更为不同步流程走此逻辑)
extActProcessService.deleteActIdMembership(userName);
}
} else {
// 从流程中删除用户信息(逻辑删除用户 或 用户变更为不同步流程走此逻辑)
extActProcessService.deleteActIdMembership(userName);
}
}
//*********************************************************************************************************
/**
* TODO 有风险,考虑更好方案
* 模拟登陆临时账号密码
* _reserve_user_external
* jeecg@123
*/
private static final String EXTERNAL_USERNAME = "X3Jlc2VydmVfdXNlcl9leHRlcm5hbA==";
private static final String EXTERNAL_PASSWORD = "amVlY2dAMTIz";
/**
* 获取临时登录令牌
* <p>
* 模拟登陆接口,获取模拟 Token
*
* @return
*/
public static String getTemporaryToken() {
RedisUtil redisUtil = SpringContextUtils.getBean(RedisUtil.class);
//模拟登录生成临时Token
//参数说明:第一个参数是用户名、第二个参数是密码的加密串
String token = JwtUtil.sign(oConvertUtils.decodeBase64Str(EXTERNAL_USERNAME), oConvertUtils.decodeBase64Str(EXTERNAL_PASSWORD));
// 设置Token缓存有效时间为 5 分钟
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, 5 * 60 * 1000);
return token;
}
//*********************************************************************************************************
}
@@ -0,0 +1,100 @@
package org.jeecg.modules.bpm.cmd;
import org.apache.commons.lang.StringUtils;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.common.engine.api.FlowableObjectNotFoundException;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.compatibility.Flowable5CompatibilityHandler;
import org.flowable.engine.impl.persistence.entity.CommentEntity;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.impl.util.Flowable5Util;
import org.flowable.engine.runtime.Execution;
import org.flowable.engine.task.Comment;
import org.flowable.engine.task.Event;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.jeecg.modules.bpm.constant.enums.CommentTypeEnum;
/**
* @Description:
* @Author: Bruce.liu
* @Since:19:01 2018/11/24
* 爱拼才会赢 2019 ~ 2030 版权所有
*/
public class AddCommentCmd implements Command<Comment> {
protected String taskId;
protected String userId;
protected String processInstanceId;
protected String type;
protected String message;
public AddCommentCmd(String userId, String processInstanceId, String type, String message) {
this.processInstanceId = processInstanceId;
this.message = message;
this.type = type;
this.userId = userId;
}
public AddCommentCmd(String taskId, String userId, String processInstanceId, String type, String message) {
this.taskId = taskId;
this.processInstanceId = processInstanceId;
this.type = type;
this.message = message;
this.userId = userId;
}
@Override
public Comment execute(CommandContext commandContext) {
TaskEntity task = null;
ExecutionEntity execution = null;
if (processInstanceId != null) {
execution = CommandContextUtil.getExecutionEntityManager(commandContext).findById(processInstanceId);
if (execution == null) {
throw new FlowableObjectNotFoundException("execution " + processInstanceId + " doesn't exist", Execution.class);
}
if (execution.isSuspended()) {
throw new FlowableException(getSuspendedExceptionMessage());
}
}
String processDefinitionId = null;
if (execution != null) {
processDefinitionId = execution.getProcessDefinitionId();
} else if (task != null) {
processDefinitionId = task.getProcessDefinitionId();
}
if (processDefinitionId != null && Flowable5Util.isFlowable5ProcessDefinitionId(commandContext, processDefinitionId)) {
Flowable5CompatibilityHandler compatibilityHandler = Flowable5Util.getFlowable5CompatibilityHandler();
if (org.apache.commons.lang.StringUtil.isBlank(message)){
message = "";
}
return compatibilityHandler.addComment(taskId, processInstanceId, type, message);
}
CommentEntity comment = CommandContextUtil.getCommentEntityManager(commandContext).create();
comment.setUserId(userId);
comment.setType((type == null) ? CommentTypeEnum.SP.toString() : type);
comment.setTime(CommandContextUtil.getProcessEngineConfiguration(commandContext).getClock().getCurrentTime());
comment.setTaskId(taskId);
comment.setProcessInstanceId(processInstanceId);
comment.setAction(Event.ACTION_ADD_COMMENT);
String eventMessage = org.apache.commons.lang.StringUtil.isNotBlank(message)?message.replaceAll("\\s+", " "):"";
if (eventMessage.length() > 3900) {
eventMessage = eventMessage.substring(0, 3900) + "...";
}
comment.setMessage(eventMessage);
comment.setFullMessage(message);
CommandContextUtil.getCommentEntityManager(commandContext).insert(comment);
return comment;
}
protected String getSuspendedTaskException() {
return "Cannot add a comment to a suspended task";
}
protected String getSuspendedExceptionMessage() {
return "Cannot add a comment to a suspended execution";
}
}
@@ -0,0 +1,71 @@
package org.jeecg.modules.bpm.cmd;
import org.apache.commons.lang.StringUtils;
import org.flowable.common.engine.api.FlowableObjectNotFoundException;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.persistence.entity.CommentEntity;
import org.flowable.engine.impl.persistence.entity.HistoricProcessInstanceEntity;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.task.Comment;
import org.flowable.engine.task.Event;
import org.jeecg.modules.bpm.constant.enums.CommentTypeEnum;
/**
* 网上的参考代码(目前未使用)
*
* @Description: 添加历史审批记录
* @Author: scoot
* @Since:19:01 2022-12-06
*/
public class AddHisCommentCmd implements Command<Comment> {
protected String taskId;
protected String userId;
protected String processInstanceId;
protected String type;
protected String message;
public AddHisCommentCmd(String taskId, String userId, String processInstanceId, String type, String message) {
this.taskId = taskId;
this.userId = userId;
this.processInstanceId = processInstanceId;
this.type = type;
this.message = message;
}
@Override
public Comment execute(CommandContext commandContext) {
HistoricProcessInstanceEntity hisProIns = null;
if (processInstanceId != null) {
hisProIns = CommandContextUtil.getHistoricProcessInstanceEntityManager(commandContext).findById(processInstanceId);
if (hisProIns == null) {
throw new FlowableObjectNotFoundException("HistoricProcessInstance " + processInstanceId + " doesn't exist", HistoricProcessInstanceEntity.class);
}
}
CommentEntity comment = CommandContextUtil.getCommentEntityManager(commandContext).create();
comment.setUserId(userId);
comment.setType((type == null) ? CommentTypeEnum.SP.toString() : type);
comment.setTime(CommandContextUtil.getProcessEngineConfiguration(commandContext).getClock().getCurrentTime());
comment.setTaskId(taskId);
comment.setProcessInstanceId(processInstanceId);
comment.setAction(Event.ACTION_ADD_COMMENT);
String eventMessage = org.apache.commons.lang.StringUtil.isNotBlank(message) ? message.replaceAll("\\s+", " ") : "";
if (eventMessage.length() > 3900) {
eventMessage = eventMessage.substring(0, 3900) + "...";
}
comment.setMessage(eventMessage);
comment.setFullMessage(message);
CommandContextUtil.getCommentEntityManager(commandContext).insert(comment);
return comment;
}
protected String getSuspendedTaskException() {
return "Cannot add a comment to a suspended task";
}
protected String getSuspendedExceptionMessage() {
return "Cannot add a comment to a suspended execution";
}
}
@@ -0,0 +1,301 @@
package org.jeecg.modules.bpm.cmd;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import org.flowable.bpmn.BpmnAutoLayout;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.cmd.AbstractDynamicInjectionCmd;
import org.flowable.engine.impl.context.Context;
import org.flowable.engine.impl.dynamic.BaseDynamicSubProcessInjectUtil;
import org.flowable.engine.impl.dynamic.DynamicUserTaskBuilder;
import org.flowable.engine.impl.persistence.entity.DeploymentEntity;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.impl.util.ProcessDefinitionUtil;
import org.flowable.task.service.TaskService;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.jeecg.modules.bpm.cmd.dto.TaskAddRequest;
import org.jeecg.modules.bpm.cmd.util.ApiAssert;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* @Description: 动态添加用户节点(向后加签)
* @Author: zyf
* @Date: 2022-05-06
*/
public class AfterSignUserTaskCmd extends AbstractDynamicInjectionCmd implements Command<Void> {
protected String processInstanceId;
protected String currentTaskDefKey;
protected TaskAddRequest taskAddRequest;
private boolean autoLayout = true;
public AfterSignUserTaskCmd(TaskAddRequest taskAddRequest) {
this.processInstanceId = taskAddRequest.getProcInsId();
this.currentTaskDefKey = taskAddRequest.getCurrentTaskDefKey();
this.taskAddRequest = taskAddRequest;
}
@Override
public Void execute(CommandContext commandContext) {
createDerivedProcessDefinitionForProcessInstance(commandContext, processInstanceId);
return null;
}
@Override
protected void updateBpmnProcess(CommandContext commandContext, Process process, BpmnModel bpmnModel, ProcessDefinitionEntity originalProcessDefinitionEntity, DeploymentEntity newDeploymentEntity) {
String dynamicTaskId = "Task_" + RandomUtil.randomString(7);
DynamicUserTaskBuilder dynamicUserTaskBuilder = new DynamicUserTaskBuilder();
dynamicUserTaskBuilder.setId(dynamicTaskId);
dynamicUserTaskBuilder.setName(taskAddRequest.getTaskName());
dynamicUserTaskBuilder.setAssignee(taskAddRequest.getAssignee());
FlowElement currentFlowElement = process.getFlowElement(currentTaskDefKey);
if (ObjectUtil.isNotEmpty(currentFlowElement)) {
if (!(currentFlowElement instanceof Task)) {
ApiAssert.failure("非任务节点");
}
UserTask currentUserTask = (UserTask) currentFlowElement;
//定义新的节点
UserTask newUserTask = new UserTask();
if (dynamicUserTaskBuilder.getId() != null) {
newUserTask.setId(dynamicUserTaskBuilder.getId());
} else {
newUserTask.setId(dynamicUserTaskBuilder.nextTaskId(process.getFlowElementMap()));
}
dynamicUserTaskBuilder.setDynamicTaskId(newUserTask.getId());
newUserTask.setName(dynamicUserTaskBuilder.getName());
newUserTask.setAssignee(dynamicUserTaskBuilder.getAssignee());
List<SequenceFlow> outgoingFlows = new ArrayList<>();
//获取当前流程节点流出的sequence
List<SequenceFlow> currentOutgoingFlows = currentUserTask.getOutgoingFlows();
for (SequenceFlow sequenceFlow : currentOutgoingFlows) {
SequenceFlow outgoingFlow = new SequenceFlow(newUserTask.getId(), sequenceFlow.getTargetRef());
outgoingFlow.setSkipExpression(sequenceFlow.getSkipExpression());
outgoingFlow.setConditionExpression(sequenceFlow.getConditionExpression());
outgoingFlow.setExtensionElements(sequenceFlow.getExtensionElements());
outgoingFlow.setExecutionListeners(sequenceFlow.getExecutionListeners());
outgoingFlow.setName(sequenceFlow.getName());
outgoingFlow.setId("Flow_" + RandomUtil.randomString(7));
outgoingFlows.add(outgoingFlow);
//删除原先节点的出线
process.removeFlowElement(sequenceFlow.getId());
process.addFlowElement(outgoingFlow);
}
List<SequenceFlow> incomingFlows = new ArrayList<>();
SequenceFlow incomingFlow = new SequenceFlow(currentFlowElement.getId(), newUserTask.getId());
//设置唯一编
incomingFlow.setId("Flow_" + RandomUtil.randomString(7));
incomingFlows.add(incomingFlow);
process.addFlowElement(incomingFlow);
newUserTask.setOutgoingFlows(outgoingFlows);
newUserTask.setIncomingFlows(incomingFlows);
process.addFlowElement(newUserTask);
//新增坐标 点
GraphicInfo elementGraphicInfo = bpmnModel.getGraphicInfo(currentFlowElement.getId());
if (elementGraphicInfo != null) {
//自动排版
if (autoLayout) {
new BpmnAutoLayout(bpmnModel).execute();
} else {
drawNode(bpmnModel, elementGraphicInfo, newUserTask, outgoingFlows, incomingFlows, process);
}
}
} else {
drawNode(process, bpmnModel, dynamicUserTaskBuilder);
}
BaseDynamicSubProcessInjectUtil.processFlowElements(commandContext, process, bpmnModel, originalProcessDefinitionEntity, newDeploymentEntity);
}
@Override
protected void updateExecutions(CommandContext commandContext, ProcessDefinitionEntity processDefinitionEntity, ExecutionEntity processInstance, List<ExecutionEntity> childExecutions) {
}
private void drawNode(BpmnModel bpmnModel, GraphicInfo elementGraphicInfo, UserTask userTask, List<SequenceFlow> outgoingFlows, List<SequenceFlow> incomingFlows, Process process) {
double yDiff = 0;
double xDiff = 80;
if (elementGraphicInfo.getY() < 173) {
yDiff = 173 - elementGraphicInfo.getY();
elementGraphicInfo.setY(173);
}
StartEvent initialStartEvent = getStartEvent(process);
Map<String, GraphicInfo> locationMap = bpmnModel.getLocationMap();
for (String locationId : locationMap.keySet()) {
if (initialStartEvent.getId().equals(locationId)) {
continue;
}
GraphicInfo locationGraphicInfo = locationMap.get(locationId);
locationGraphicInfo.setX(locationGraphicInfo.getX() + xDiff);
locationGraphicInfo.setY(locationGraphicInfo.getY() + yDiff);
}
Map<String, List<GraphicInfo>> flowLocationMap = bpmnModel.getFlowLocationMap();
for (String flowId : flowLocationMap.keySet()) {
//if (flowFromStart.getId().equals(flowId)) {
//continue;
//}
List<GraphicInfo> flowGraphicInfoList = flowLocationMap.get(flowId);
for (GraphicInfo flowGraphicInfo : flowGraphicInfoList) {
flowGraphicInfo.setX(flowGraphicInfo.getX() + xDiff);
flowGraphicInfo.setY(flowGraphicInfo.getY() + yDiff);
}
}
//绘制节点
GraphicInfo newTaskGraphicInfo = new GraphicInfo(elementGraphicInfo.getX() + 185, elementGraphicInfo.getY() - 163, 80, 100);
bpmnModel.addGraphicInfo(userTask.getId(), newTaskGraphicInfo);
bpmnModel.addFlowGraphicInfoList(userTask.getId(), createWayPoints(elementGraphicInfo.getX() + 95, elementGraphicInfo.getY() - 5, elementGraphicInfo.getX() + 95, elementGraphicInfo.getY() - 123, elementGraphicInfo.getX() + 185, elementGraphicInfo.getY() - 123));
List<SequenceFlow> addFlows = new ArrayList<>();
addFlows.addAll(outgoingFlows);
addFlows.addAll(incomingFlows);
/* 绘制连线 */
for (SequenceFlow sequenceFlow1 : addFlows) {
bpmnModel.addFlowGraphicInfoList(sequenceFlow1.getId(), createWayPoints(elementGraphicInfo.getX() + 30, elementGraphicInfo.getY() + 15, elementGraphicInfo.getX() + 75, elementGraphicInfo.getY() + 15));
}
}
private void drawNode(Process process, BpmnModel bpmnModel, DynamicUserTaskBuilder dynamicUserTaskBuilder) {
StartEvent initialStartEvent = getStartEvent(process);
ParallelGateway parallelGateway = new ParallelGateway();
parallelGateway.setId(dynamicUserTaskBuilder.nextForkGatewayId(process.getFlowElementMap()));
process.addFlowElement(parallelGateway);
UserTask newUserTask = new UserTask();
if (dynamicUserTaskBuilder.getId() != null) {
newUserTask.setId(dynamicUserTaskBuilder.getId());
} else {
newUserTask.setId(dynamicUserTaskBuilder.nextTaskId(process.getFlowElementMap()));
}
dynamicUserTaskBuilder.setDynamicTaskId(newUserTask.getId());
newUserTask.setName(dynamicUserTaskBuilder.getName());
newUserTask.setAssignee(dynamicUserTaskBuilder.getAssignee());
process.addFlowElement(newUserTask);
EndEvent endEvent = new EndEvent();
endEvent.setId(dynamicUserTaskBuilder.nextEndEventId(process.getFlowElementMap()));
process.addFlowElement(endEvent);
SequenceFlow flowToUserTask = new SequenceFlow(parallelGateway.getId(), newUserTask.getId());
flowToUserTask.setId(dynamicUserTaskBuilder.nextFlowId(process.getFlowElementMap()));
process.addFlowElement(flowToUserTask);
SequenceFlow flowFromUserTask = new SequenceFlow(newUserTask.getId(), endEvent.getId());
flowFromUserTask.setId(dynamicUserTaskBuilder.nextFlowId(process.getFlowElementMap()));
process.addFlowElement(flowFromUserTask);
SequenceFlow initialFlow = initialStartEvent.getOutgoingFlows().get(0);
initialFlow.setSourceRef(parallelGateway.getId());
SequenceFlow flowFromStart = new SequenceFlow(initialStartEvent.getId(), parallelGateway.getId());
flowFromStart.setId(dynamicUserTaskBuilder.nextFlowId(process.getFlowElementMap()));
process.addFlowElement(flowFromStart);
//跳整节点的布局
GraphicInfo elementGraphicInfo = bpmnModel.getGraphicInfo(initialStartEvent.getId());
if (elementGraphicInfo != null) {
double yDiff = 0;
double xDiff = 80;
if (elementGraphicInfo.getY() < 173) {
yDiff = 173 - elementGraphicInfo.getY();
elementGraphicInfo.setY(173);
}
Map<String, GraphicInfo> locationMap = bpmnModel.getLocationMap();
for (String locationId : locationMap.keySet()) {
if (initialStartEvent.getId().equals(locationId)) {
continue;
}
GraphicInfo locationGraphicInfo = locationMap.get(locationId);
locationGraphicInfo.setX(locationGraphicInfo.getX() + xDiff);
locationGraphicInfo.setY(locationGraphicInfo.getY() + yDiff);
}
Map<String, List<GraphicInfo>> flowLocationMap = bpmnModel.getFlowLocationMap();
for (String flowId : flowLocationMap.keySet()) {
if (flowFromStart.getId().equals(flowId)) {
continue;
}
List<GraphicInfo> flowGraphicInfoList = flowLocationMap.get(flowId);
for (GraphicInfo flowGraphicInfo : flowGraphicInfoList) {
flowGraphicInfo.setX(flowGraphicInfo.getX() + xDiff);
flowGraphicInfo.setY(flowGraphicInfo.getY() + yDiff);
}
}
GraphicInfo forkGraphicInfo = new GraphicInfo(elementGraphicInfo.getX() + 75, elementGraphicInfo.getY() - 5, 40, 40);
bpmnModel.addGraphicInfo(parallelGateway.getId(), forkGraphicInfo);
bpmnModel.addFlowGraphicInfoList(flowFromStart.getId(), createWayPoints(elementGraphicInfo.getX() + 30, elementGraphicInfo.getY() + 15, elementGraphicInfo.getX() + 75, elementGraphicInfo.getY() + 15));
GraphicInfo newTaskGraphicInfo = new GraphicInfo(elementGraphicInfo.getX() + 185, elementGraphicInfo.getY() - 163, 80, 100);
bpmnModel.addGraphicInfo(newUserTask.getId(), newTaskGraphicInfo);
bpmnModel.addFlowGraphicInfoList(flowToUserTask.getId(), createWayPoints(elementGraphicInfo.getX() + 95, elementGraphicInfo.getY() - 5, elementGraphicInfo.getX() + 95, elementGraphicInfo.getY() - 123, elementGraphicInfo.getX() + 185, elementGraphicInfo.getY() - 123));
GraphicInfo endGraphicInfo = new GraphicInfo(elementGraphicInfo.getX() + 335, elementGraphicInfo.getY() - 137, 28, 28);
bpmnModel.addGraphicInfo(endEvent.getId(), endGraphicInfo);
bpmnModel.addFlowGraphicInfoList(flowFromUserTask.getId(), createWayPoints(elementGraphicInfo.getX() + 285, elementGraphicInfo.getY() - 123, elementGraphicInfo.getX() + 335, elementGraphicInfo.getY() - 123));
}
}
private void updateExecutions(CommandContext commandContext, ExecutionEntity processInstance, ProcessDefinitionEntity processDefinitionEntity, DynamicUserTaskBuilder dynamicUserTaskBuilder) {
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager(commandContext);
List<ExecutionEntity> oldExecution = executionEntityManager.findChildExecutionsByProcessInstanceId(processInstance.getProcessInstanceId());
ExecutionEntity execution = executionEntityManager.createChildExecution(processInstance);
BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionEntity.getId());
TaskService taskService = CommandContextUtil.getTaskService(commandContext);
List<TaskEntity> taskEntities = taskService.findTasksByProcessInstanceId(processInstanceId);
// 删除当前活动任务
for (TaskEntity taskEntity : taskEntities) {
taskEntity.getIdentityLinks().stream().forEach(identityLinkEntity -> {
if (identityLinkEntity.isGroup()) {
taskEntity.deleteGroupIdentityLink(identityLinkEntity.getGroupId(), "candidate");
} else {
taskEntity.deleteUserIdentityLink(identityLinkEntity.getUserId(), "participant");
}
});
if (taskEntity.getTaskDefinitionKey().equals(currentTaskDefKey)) {
taskService.deleteTask(taskEntity, false);
}
}
//设置活动后的节点
UserTask userTask = (UserTask) bpmnModel.getProcessById(processDefinitionEntity.getKey()).getFlowElement(dynamicUserTaskBuilder.getId());
execution.setCurrentFlowElement(userTask);
Context.getAgenda().planContinueProcessOperation(execution);
}
private StartEvent getStartEvent(Process process) {
List<StartEvent> startEvents = process.findFlowElementsOfType(StartEvent.class);
StartEvent initialStartEvent = null;
for (StartEvent startEvent : startEvents) {
if (startEvent.getEventDefinitions().size() == 0) {
initialStartEvent = startEvent;
break;
} else if (initialStartEvent == null) {
initialStartEvent = startEvent;
}
}
return initialStartEvent;
}
}
@@ -0,0 +1,113 @@
package org.jeecg.modules.bpm.cmd;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.impl.calendar.CronExpression;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.common.engine.impl.util.DefaultClockImpl;
import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntity;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.job.api.Job;
import org.flowable.job.service.TimerJobService;
import org.flowable.job.service.impl.persistence.entity.TimerJobEntity;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bpm.job.CustomTimeStartJobEventHandler;
import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
import java.io.Serializable;
import java.text.ParseException;
import java.util.Date;
/**
* 使用flowable内部Api手动创建作业(Job
*
* 参考博客
* https://blog.csdn.net/II_bat/article/details/51329399
* https://blog.csdn.net/oouuyy/article/details/118890552
*/
@Slf4j
public class CustomTimeStartJobCmd implements Command<Void>, Serializable {
//流程定义key
protected String processKey;
//表单编码或者表名
protected String formKey;
//表单数据id
protected String dataId;
//流程发起人
protected String applyUserId;
//定时开始时间
protected Date dueDate;
//触发字段
protected String triggerField;
//定时频率表达式
protected String repeat;
public CustomTimeStartJobCmd(String processKey, String formKey, String dataId, Date dueDate, String applyUserId, String triggerField) {
this.formKey = formKey;
this.dataId = dataId;
this.processKey = processKey;
this.applyUserId = applyUserId;
this.dueDate = dueDate;
this.triggerField = triggerField;
}
public CustomTimeStartJobCmd(String processKey, String formKey, String dataId, Date dueDate, String applyUserId,String repeat, String triggerField) {
this.formKey = formKey;
this.dataId = dataId;
this.processKey = processKey;
this.applyUserId = applyUserId;
this.dueDate = dueDate;
this.repeat = repeat;
this.triggerField = triggerField;
}
@Override
public Void execute(CommandContext commandContext) {
JSONObject jsonObject = new JSONObject();
jsonObject.put("activityId", "start");
jsonObject.put(WorkFlowGlobals.BPM_DATA_ID, this.dataId);
jsonObject.put(WorkFlowGlobals.BPM_FORM_KEY, this.formKey);
jsonObject.put(WorkFlowGlobals.APPLY_USER_ID, this.applyUserId);
jsonObject.put(WorkFlowGlobals.TIME_TRIGGER_FIELD, this.triggerField);
TimerJobService timerJobService = CommandContextUtil.getTimerJobService(commandContext);
TimerJobEntity job = timerJobService.createTimerJob();
job.setJobType(Job.JOB_TYPE_TIMER);
job.setExclusive(true);
// 自定义作业处理器
//job.setJobHandlerType(TimerStartEventJobHandler.TYPE);
job.setJobHandlerType(CustomTimeStartJobEventHandler.TYPE);
// 处理时间
job.setDuedate(this.dueDate);
job.setExclusive(true);
if(oConvertUtils.isNotEmpty(this.repeat)){
job.setRepeat(this.repeat);
//--------------------------------------------------------------------------------
//测试发现flowable可以自动调整开始时间改为下一次有效日期,但是发现会发起一次流程,这个不应该发起,所以还是需要手工计算下次有效期
//判断开始时间 dueDate小于当前时间,则生成下次有效执行时间
try {
if (!this.dueDate.after(new Date())) {
CronExpression cronExpression = new CronExpression(this.repeat, new DefaultClockImpl());
Date nextDuteDate = cronExpression.getNextValidTimeAfter(new Date());
log.info("执行日期小于当前时间,重新计算下次执行时间!执行日期:{},通过计算获取下次执行有效期:{},循环表达式:{}",
DateUtils.datetimeFormat.get().format(this.dueDate), DateUtils.datetimeFormat.get().format(nextDuteDate), this.repeat);
job.setDuedate(nextDuteDate);
}
} catch (ParseException e) {
log.error("重新计算下次执行时间异常:" + e.getMessage());
}
//--------------------------------------------------------------------------------
}
//流程key
ProcessDefinitionEntity processDefinition = CommandContextUtil.getProcessDefinitionEntityManager().findLatestProcessDefinitionByKey(processKey);
String processDefinitionId = processDefinition.getId();
job.setProcessDefinitionId(processDefinitionId);
job.setJobHandlerConfiguration(jsonObject.toJSONString());
timerJobService.scheduleTimerJob(job);
return null;
}
}
@@ -0,0 +1,32 @@
package org.jeecg.modules.bpm.cmd;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
import org.flowable.engine.impl.util.CommandContextUtil;
import java.io.Serializable;
/**
* @Description: 删除执行数据命令
* @Author: zyf
* @Date: 2022-06-01
*/
public class DeleteExecutionCmd implements Command<String>, Serializable {
/**
* 执行id
*/
private String executionId;
public DeleteExecutionCmd(String executionId) {
this.executionId = executionId;
}
@Override
public String execute(CommandContext commandContext) {
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
executionEntityManager.delete(executionId);
return null;
}
}
@@ -0,0 +1,30 @@
package org.jeecg.modules.bpm.cmd;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.cmd.NeedsActiveTaskCmd;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.task.service.TaskService;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
/**
* @Description: 删除任务命令
* @Author: zyf
* @Date: 2022-06-01
*/
public class DeleteTaskCmd extends NeedsActiveTaskCmd<String> {
/**
* 当前任务对应的 act_ru_task 任务id
*/
public DeleteTaskCmd(String taskId) {
super(taskId);
}
@Override
protected String execute(CommandContext commandContext, TaskEntity task) {
TaskService taskService = CommandContextUtil.getTaskService(commandContext);
taskService.deleteTask(task,true);
return null;
}
}
@@ -0,0 +1,91 @@
package org.jeecg.modules.bpm.cmd;
import cn.hutool.core.text.StrFormatter;
import cn.hutool.core.util.ObjectUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.persistence.entity.ProcessDefinitionEntityManager;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.repository.ProcessDefinition;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bpm.cmd.util.ApiAssert;
import java.io.Serializable;
/**
* @Description: 获取流程定义
* @Author: zyf
* @Date: 2022-05-06
*/
@Slf4j
public class GetProcessDefinitionCmd implements Command<ProcessDefinition>, Serializable {
private static final long serialVersionUID = 1L;
/**
* 流程定义ID
*/
protected String processDefinitionId;
/**
* 流程KEY
*/
protected String processDefinitionKey;
/**
* 租户ID
*/
protected String tenantId;
public GetProcessDefinitionCmd(String processDefinitionId, String processDefinitionKey, String tenantId) {
this.processDefinitionId = processDefinitionId;
this.processDefinitionKey = processDefinitionKey;
this.tenantId = tenantId;
}
public GetProcessDefinitionCmd(String processDefinitionId) {
this.processDefinitionId = processDefinitionId;
}
public GetProcessDefinitionCmd(String processDefinitionId, String processDefinitionKey) {
this.processDefinitionId = processDefinitionId;
this.processDefinitionKey = processDefinitionKey;
}
@Override
public ProcessDefinition execute(CommandContext commandContext) {
ProcessEngineConfigurationImpl processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext);
//获取流程定义管理器
ProcessDefinitionEntityManager processDefinitionEntityManager = processEngineConfiguration.getProcessDefinitionEntityManager();
ProcessDefinition processDefinition = null;
if (processDefinitionId != null) {
processDefinition = processDefinitionEntityManager.findById(processDefinitionId);
log.error(StrFormatter.format("流程定义不存在,processDefinitionId:{}", processDefinitionId));
ApiAssert.isNotEmpty("流程定义不存在", processDefinition);
} else if (processDefinitionKey != null && ObjectUtil.isEmpty(tenantId)) {
processDefinition = processDefinitionEntityManager.findLatestProcessDefinitionByKey(processDefinitionKey);
log.error(StrFormatter.format("流程定义不存在,processDefinitionKey:{}", processDefinitionKey));
ApiAssert.isNotEmpty("流程定义不存在", processDefinition);
} else if (processDefinitionKey != null && oConvertUtils.isNotEmpty(tenantId)) {
processDefinition = processDefinitionEntityManager.findLatestProcessDefinitionByKeyAndTenantId(processDefinitionKey, tenantId);
if (processDefinition == null) {
if (processEngineConfiguration.isFallbackToDefaultTenant()) {
if (org.apache.commons.lang.StringUtil.isNotEmpty(processEngineConfiguration.getDefaultTenantValue())) {
processDefinition = processDefinitionEntityManager.findLatestProcessDefinitionByKeyAndTenantId(processDefinitionKey, processEngineConfiguration.getDefaultTenantValue());
} else {
processDefinition = processDefinitionEntityManager.findLatestProcessDefinitionByKey(processDefinitionKey);
}
log.error(StrFormatter.format("流程定义不存在,还应用了默认租户的回退,processDefinitionKey:{}", processDefinitionKey));
ApiAssert.isNotEmpty("流程定义不存在", processDefinition);
} else {
log.error(StrFormatter.format("流程定义不存在,processDefinitionKey:{},and 租户ID:{1}", processDefinitionKey, tenantId));
ApiAssert.failure("流程定义不存在");
}
}
} else {
ApiAssert.failure("参数异常");
}
return processDefinition;
}
}
@@ -0,0 +1,58 @@
package org.jeecg.modules.bpm.cmd;
import cn.hutool.core.util.ObjectUtil;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.jeecg.modules.bpm.cmd.util.ApiAssert;
import org.jeecg.modules.extbpm.process.entity.ExtActProcess;
import java.io.Serializable;
/**
* @Description: 流程级联删除命令
* @Author: zyf
* @Date: 2022-05-06
*/
public class ProcessDeleteCmd implements Command<Boolean>, Serializable {
private static final long serialVersionUID = 1L;
/**
* 管理控制部署与流程定义
*/
private RepositoryService repositoryService;
private ExtActProcess flowProcess;
public ProcessDeleteCmd(ExtActProcess flowProcess) {
this.flowProcess = flowProcess;
}
@Override
public Boolean execute(CommandContext commandContext) {
ProcessEngineConfiguration processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext);
this.repositoryService = processEngineConfiguration.getRepositoryService();
ProcessDefinitionQuery definitionQuery = repositoryService.createProcessDefinitionQuery();
String processKey = flowProcess.getProcessKey();
definitionQuery.processDefinitionKey(processKey);
long procDefCount = definitionQuery.count();
if (procDefCount > 0) {
ApiAssert.failure("流程已发布,禁止删除");
}
String procModelId = flowProcess.getProcessKey();
if (ObjectUtil.isNotEmpty(procModelId)) {
Model model = repositoryService.getModel(procModelId);
if (ObjectUtil.isNotEmpty(model)) {
repositoryService.deleteModel(procModelId);
}
}
return true;
}
}
@@ -0,0 +1,86 @@
package org.jeecg.modules.bpm.cmd;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.DeploymentBuilder;
import org.flowable.engine.repository.Model;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bpm.cmd.util.ApiAssert;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
/**
* @Description: 流程部署命令
* @Author: zyf
* @Date: 2022-05-06
*/
public class ProcessDeployCmd implements Command<Deployment>, Serializable {
private static final long serialVersionUID = 1L;
/**
* 流程模型ID
*/
private String modelId;
private Model model;
public ProcessDeployCmd(String modelId, Model model) {
this.modelId = modelId;
this.model = model;
}
@Override
public Deployment execute(CommandContext commandContext) {
Deployment deployment = null;
RepositoryService repositoryService = CommandContextUtil.getProcessEngineConfiguration(commandContext).getRepositoryService();
if (model == null) {
ApiAssert.failure("流程模型不存在:" + modelId);
}
if (!model.hasEditorSource()) {
ApiAssert.failure("资源不存在,流程模型:" + modelId);
}
byte[] bpmnBytes = CommandContextUtil.getProcessEngineConfiguration(commandContext).getModelEntityManager().findEditorSourceByModelId(modelId);
if (bpmnBytes == null) {
ApiAssert.failure("资源不存在,流程模型:" + modelId);
}
try {
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
String fileName = model.getId() + ".bpmn20.xml";
ByteArrayInputStream bis = new ByteArrayInputStream(bpmnBytes);
deploymentBuilder.addInputStream(fileName, bis);
deploymentBuilder.name(fileName);
// modelId设置为部署的分类字段作为后续关联的需要
deploymentBuilder.category(model.getCategory());
XMLInputFactory xif = XMLInputFactory.newInstance();
InputStreamReader xmlIn = new InputStreamReader(new ByteArrayInputStream(bpmnBytes), "UTF-8");
XMLStreamReader xtr = xif.createXMLStreamReader(xmlIn);
BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(xtr);
org.flowable.bpmn.model.Process process = bpmnModel.getMainProcess();
deploymentBuilder.key(process.getId());
//租户ID
String tenantId = model.getTenantId();
if (oConvertUtils.isNotEmpty(tenantId)) {
//设置租户ID
deploymentBuilder.tenantId(tenantId);
}
deployment = deploymentBuilder.deploy();
if (deployment != null) {
model.setDeploymentId(deployment.getId());
}
} catch (Exception e) {
ApiAssert.failure("发布失败");
}
return deployment;
}
}
@@ -0,0 +1,92 @@
package org.jeecg.modules.bpm.cmd;
import com.google.common.io.CharStreams;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.Process;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.common.engine.impl.util.io.InputStreamSource;
import org.flowable.common.engine.impl.util.io.StreamSource;
import org.flowable.engine.ProcessEngineConfiguration;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.repository.DeploymentBuilder;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bpm.cmd.util.ApiAssert;
import org.jeecg.modules.bpm.cmd.util.FlowableUtils;
import org.jeecg.modules.extbpm.process.entity.ExtActProcess;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
/**
* @Description: 根据导入文件构建FlowProcess
* @Author: zyf
* @Date: 2022-05-06
*/
@Slf4j
public class ProcessImportCmd implements Command<ExtActProcess>, Serializable {
private static final long serialVersionUID = 1L;
/**
* 文件名
*/
private String fileName;
/**
* 上传文件对象
*/
private MultipartFile file;
public ProcessImportCmd(HttpServletRequest request) {
if (!(request instanceof MultipartHttpServletRequest)) {
log.error("请求必须是MultipartHttpServletRequest的实例");
ApiAssert.failure("导入失败");
}
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
if (multipartRequest.getFileMap().size() == 0) {
log.error("请求文件为空");
ApiAssert.failure("导入失败");
}
this.file = multipartRequest.getFileMap().values().iterator().next();
this.fileName = this.file.getOriginalFilename();
//检测文件是否合法
FlowableUtils.checkImportFileName(this.fileName);
}
@Override
public ExtActProcess execute(CommandContext commandContext) {
ProcessEngineConfiguration processEngineConfiguration = CommandContextUtil.getProcessEngineConfiguration(commandContext);
RepositoryService repositoryService = processEngineConfiguration.getRepositoryService();
DeploymentBuilder deploymentBuilder = repositoryService.createDeployment();
boolean isBpmnFile = FlowableUtils.isBpmnFile(fileName);
ExtActProcess flowProcess = new ExtActProcess();
try {
if (isBpmnFile) {
deploymentBuilder.addInputStream(fileName, file.getInputStream());
StreamSource xmlSource = new InputStreamSource(file.getInputStream());
BpmnModel bpmnModel = new BpmnXMLConverter().convertToBpmnModel(xmlSource, false, false, "UTF-8");
Process process = bpmnModel.getMainProcess();
if (oConvertUtils.isNotEmpty(process)) {
//添加自定义流程定义
String processXml = CharStreams.toString(new InputStreamReader(file.getInputStream(), StandardCharsets.UTF_8));
flowProcess.setProcessXml(processXml.getBytes());
flowProcess.setProcessKey(process.getId());
flowProcess.setProcessName(process.getName());
flowProcess.setNote(process.getDocumentation());
}
} else {
}
} catch (Exception e) {
log.error("导入异常:", e.getMessage());
}
return flowProcess;
}
}
@@ -0,0 +1,151 @@
package org.jeecg.modules.bpm.cmd;
import cn.hutool.core.text.StrFormatter;
import com.google.common.collect.Sets;
import org.flowable.bpmn.model.FlowNode;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.impl.util.ProcessDefinitionUtil;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.jeecg.modules.bpm.cmd.util.ApiAssert;
import org.jeecg.modules.bpm.cmd.util.FlowableUtils;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @TODO 此方法暂时未用上,是从 yf 项目迁移过来
*
* @Description: 驳回任务(指定节点)
* @Author: zyf
* @Date: 2022-05-10
*/
public class RejectTaskCmd implements Command<String>, Serializable {
protected RuntimeService runtimeService;
/**
* 当前节点
*/
protected String taskId;
/**
* 跳转目标节点ID
*/
protected String targetActivityId;
public RejectTaskCmd(RuntimeService runtimeService, String taskId, String targetActivityId) {
this.runtimeService = runtimeService;
this.taskId = taskId;
this.targetActivityId = targetActivityId;
}
@Override
public String execute(CommandContext commandContext) {
// NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
ApiAssert.isNotEmpty("驳回节点ID不能为空", this.targetActivityId);
//当前任务节点对象
TaskEntity task = CommandContextUtil.getProcessEngineConfiguration().getTaskServiceConfiguration().getTaskService().getTask(taskId);
ApiAssert.isNotEmpty("任务不存在", task);
//流程定义ID
String processDefinitionId = task.getProcessDefinitionId();
//流程对象
Process process = ProcessDefinitionUtil.getProcess(processDefinitionId);
//当然任务节点key
String taskDefinitionKey = task.getTaskDefinitionKey();
//当前节点对象
FlowNode sourceFlowElement = (FlowNode) process.getFlowElement(taskDefinitionKey, true);
// 只支持从用户任务退回
if (!(sourceFlowElement instanceof UserTask)) {
ApiAssert.failure("当前节点不是UserTask对象");
}
//目标节点对象
FlowNode targetFlowElement = (FlowNode) process.getFlowElement(this.targetActivityId, true);
//检测目标节点是否可访问
if (!FlowableUtils.isReachable(process, targetFlowElement, sourceFlowElement)) {
ApiAssert.failure(StrFormatter.format("无法驳回到指定节点,targetActivityId:{}", this.targetActivityId));
}
//目标节点如果相对当前节点是在子流程内部,则无法直接退回,目前处理是只能退回到子流程开始节点
String[] sourceAndTargetRealActivityId = FlowableUtils.getSourceAndTargetRealActivityId(sourceFlowElement, targetFlowElement);
// 实际应操作的当前节点ID
String sourceRealActivityId = sourceAndTargetRealActivityId[0];
// 实际应操作的目标节点ID
String targetRealActivityId = sourceAndTargetRealActivityId[1];
//获取特殊网关节点
Map<String, Set<String>> specialGatewayNodes = FlowableUtils.getSpecialGatewayElements(process);
// 当前节点处在的并行网关list
List<String> sourceInSpecialGatewayList = new ArrayList<>();
// 目标节点处在的并行网关list
List<String> targetInSpecialGatewayList = new ArrayList<>();
FlowableUtils.setSpecialGatewayList(sourceRealActivityId, targetRealActivityId, specialGatewayNodes, sourceInSpecialGatewayList, targetInSpecialGatewayList);
// 实际应筛选的节点ID
Set<String> sourceRealAcitivtyIds = null;
// 若退回目标节点相对当前节点在并行网关中,则要找到相对当前节点最近的这个并行网关,后续做特殊处理
String targetRealSpecialGateway = null;
// 1.目标节点和当前节点都不在并行网关中
if (targetInSpecialGatewayList.isEmpty() && sourceInSpecialGatewayList.isEmpty()) {
sourceRealAcitivtyIds = Sets.newHashSet(sourceRealActivityId);
}
// 2.目标节点不在并行网关中、当前节点在并行网关中
else if (targetInSpecialGatewayList.isEmpty() && !sourceInSpecialGatewayList.isEmpty()) {
sourceRealAcitivtyIds = specialGatewayNodes.get(sourceInSpecialGatewayList.get(0));
}
// 3.目标节点在并行网关中、当前节点不在并行网关中
else if (!targetInSpecialGatewayList.isEmpty() && sourceInSpecialGatewayList.isEmpty()) {
sourceRealAcitivtyIds = Sets.newHashSet(sourceRealActivityId);
targetRealSpecialGateway = targetInSpecialGatewayList.get(0);
}
// 4.目标节点和当前节点都在并行网关中
else {
int diffSpecialGatewayLevel = FlowableUtils.getDiffLevel(sourceInSpecialGatewayList,
targetInSpecialGatewayList);
// 在并行网关同一层且在同一分支
if (diffSpecialGatewayLevel == -1) {
sourceRealAcitivtyIds = Sets.newHashSet(sourceRealActivityId);
} else {
// 当前节点最内层并行网关不被目标节点最内层并行网关包含
// 或理解为当前节点相对目标节点在并行网关外
// 只筛选当前节点的execution
if (sourceInSpecialGatewayList.size() == diffSpecialGatewayLevel) {
sourceRealAcitivtyIds = Sets.newHashSet(sourceRealActivityId);
}
// 当前节点相对目标节点在并行网关内,应筛选相对目标节点最近的并行网关的所有节点的execution
else {
sourceRealAcitivtyIds = specialGatewayNodes
.get(sourceInSpecialGatewayList.get(diffSpecialGatewayLevel));
}
// 目标节点最内层并行网关包含当前节点最内层并行网关
// 或理解为目标节点相对当前节点在并行网关外
// 不做处理
if (targetInSpecialGatewayList.size() == diffSpecialGatewayLevel) {
}
// 目标节点相对当前节点在并行网关内
else {
targetRealSpecialGateway = targetInSpecialGatewayList.get(diffSpecialGatewayLevel);
}
}
}
String processInstanceId = task.getProcessInstanceId();
// 筛选需要处理的execution
List<ExecutionEntity> realExecutions = FlowableUtils.getRealExecutions(commandContext, processInstanceId, task.getExecutionId(), sourceRealActivityId, sourceRealAcitivtyIds);
// 执行退回,直接跳转到实际的 targetRealActivityId
List<String> realExecutionIds = realExecutions.stream().map(ExecutionEntity::getId).collect(Collectors.toList());
runtimeService.createChangeActivityStateBuilder().processInstanceId(processInstanceId).moveExecutionsToSingleActivityId(realExecutionIds, targetRealActivityId).changeState();
// 目标节点相对当前节点处于并行网关内,需要特殊处理,需要手动生成并行网关汇聚节点(_end)的execution数据
if (targetRealSpecialGateway != null) {
FlowableUtils.createTargetInSpecialGatewayEndExecutions(commandContext, realExecutions, process, targetInSpecialGatewayList, targetRealSpecialGateway);
}
return targetRealActivityId;
}
}
@@ -0,0 +1,82 @@
package org.jeecg.modules.bpm.cmd;
import cn.hutool.core.util.ObjectUtil;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.jeecg.modules.bpm.constant.FlowableConstant;
import java.util.*;
/**
* @Description: 串行任务加签
* @Author: zyf NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
* @Date: 2022-05-24
*/
public class SequenceMultiInstanceAddCmd implements Command<Void> {
/**
* 会签人员集合KEY
*/
private String assigneeListKey;
/**
* 任务ID
*/
private String executionId;
/**
* 加签人员
*/
private List<String> assignees;
public SequenceMultiInstanceAddCmd(String executionId, String assigneeListKey, List<String> assignees) {
this.executionId = executionId;
this.assigneeListKey = assigneeListKey;
this.assignees = assignees;
}
@Override
public Void execute(CommandContext commandContext) {
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager(commandContext);
//会签参数是否是字符串类型(逗号分割)
boolean isStringVals = false;
// 查询执行实例
ExecutionEntity executionEntity = executionEntityManager.findById(executionId);
//多实例任务总数加一
Integer nrOfInstances = executionEntity.getVariable(FlowableConstant.NUMBER_OF_INSTANCES, Integer.class);
executionEntity.setVariable(FlowableConstant.NUMBER_OF_INSTANCES, nrOfInstances + assignees.size());
// 设置流程变量
List<String> userIds = null;
//兼容当前默认会签配置传递的是,逗号分割的用户账号字符串
Object userIdsStr = executionEntity.getVariable(assigneeListKey);
if(userIdsStr instanceof String){
isStringVals = true;
userIds = Arrays.asList(((String)userIdsStr).split(","));
}else{
userIds = executionEntity.getVariable(assigneeListKey, List.class);
isStringVals = false;
}
List<String> assigneeList = new ArrayList<>();
if (ObjectUtil.isNotEmpty(userIds)) {
assigneeList.addAll(userIds);
}
if (ObjectUtil.isNotEmpty(assignees)) {
assigneeList.addAll(assignees);
}
Map<String, Object> variables = new HashMap<>(5);
if(isStringVals){
variables.put(assigneeListKey, String.join(",", assigneeList));
}else{
variables.put(assigneeListKey, assigneeList);
}
executionEntity.setVariables(variables);
return null;
}
}
@@ -0,0 +1,97 @@
package org.jeecg.modules.bpm.cmd;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.jeecg.modules.bpm.constant.FlowableConstant;
import java.util.*;
/**
* @Description: 串行任务减签
* @Author: zyf NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
* @Date: 2022-05-24
*/
public class SequenceMultiInstanceDeleteCmd implements Command<Void> {
/**
* 当前节点审批人员id
*/
private String currentUserId;
/**
* 执行id
*/
private String executionId;
/**
* 会签人员集合KEY
*/
private String assigneeListKey;
/**
* 减签人员
*/
private List<String> assignees;
public SequenceMultiInstanceDeleteCmd(String currentUserId, String executionId, String assigneeListKey, List<String> assignees) {
this.currentUserId = currentUserId;
this.executionId = executionId;
this.assigneeListKey = assigneeListKey;
this.assignees = assignees;
}
@Override
public Void execute(CommandContext commandContext) {
//会签参数是否是字符串类型(逗号分割)
boolean isStringVals = false;
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
ExecutionEntity entity = executionEntityManager.findById(executionId);
//多实例任务总数加
Integer nrOfInstances = entity.getVariable(FlowableConstant.NUMBER_OF_INSTANCES, Integer.class);
entity.setVariable(FlowableConstant.NUMBER_OF_INSTANCES, nrOfInstances + assignees.size());
// 设置流程变量
List<String> userIds = null;
//兼容当前默认会签配置传递的是,逗号分割的用户账号字符串
Object userIdsStr = entity.getVariable(assigneeListKey);
if(userIdsStr instanceof String) {
isStringVals = true;
userIds = Arrays.asList(((String)userIdsStr).split(","));
}else{
isStringVals = false;
userIds = entity.getVariable(assigneeListKey,List.class);
}
List<String> userIdList = new ArrayList<>();
//遍历计算出剩余会签人
userIds.forEach(e -> {
String userId = assignees.stream().filter(id -> id.equals(e)).findFirst().orElse(null);
if (userId == null) {
userIdList.add(e);
}
});
//当前任务执行位置
int loopCounterIndex = -1;
for (int i = 0; i < userIdList.size(); i++) {
String userId = userIdList.get(i);
if (currentUserId.equals(userId)) {
loopCounterIndex = i;
}
}
Map<String, Object> variables = new HashMap<>(5);
variables.put(FlowableConstant.NUMBER_OF_INSTANCES, userIdList.size());
if(isStringVals){
variables.put(assigneeListKey, String.join(",", userIdList));
}else{
variables.put(assigneeListKey, userIdList);
}
variables.put(FlowableConstant.LOOP_COUNTER, loopCounterIndex);
entity.setVariables(variables);
return null;
}
}
@@ -0,0 +1,97 @@
package org.jeecg.modules.bpm.cmd;
import org.flowable.bpmn.model.Activity;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Process;
import org.flowable.common.engine.impl.interceptor.Command;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.FlowableEngineAgenda;
import org.flowable.engine.impl.bpmn.behavior.MultiInstanceActivityBehavior;
import org.flowable.engine.impl.history.HistoryManager;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.impl.util.ProcessDefinitionUtil;
import org.flowable.task.service.TaskService;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.jeecg.common.util.DateUtils;
import org.jeecg.modules.bpm.cmd.dto.TaskJumpRequest;
import java.io.Serializable;
import java.util.List;
/**
* @Description: 任务跳转命令
* @Author: zyf
* @Date: 2022-05-19
*/
public class TaskJumpCmd implements Command<Void>, Serializable {
private TaskJumpRequest taskJumpRequest;
public TaskJumpCmd(TaskJumpRequest taskJumpRequest) {
super();
this.taskJumpRequest = taskJumpRequest;
}
@Override
public Void execute(CommandContext commandContext) {
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager(commandContext);
// 查询当前任务节点
TaskService taskService = CommandContextUtil.getProcessEngineConfiguration().getTaskServiceConfiguration().getTaskService();
TaskEntity taskEntity = taskService.getTask(taskJumpRequest.getTaskId());
// 查询执行实例
ExecutionEntity executionEntity = executionEntityManager.findById(taskEntity.getExecutionId());
// 查询流程
Process process = ProcessDefinitionUtil.getProcess(taskEntity.getProcessDefinitionId());
// 当前任务的key
String currentTaskDefinitionKey = taskEntity.getTaskDefinitionKey();
// 当前任务
Activity currentActivity = (Activity) process.getFlowElement(currentTaskDefinitionKey);
Object currentBehavior = currentActivity.getBehavior();
// 父执行实例
ExecutionEntity parentExecutionEntity = executionEntity.getParent();
//是否是会签节点
if (currentBehavior instanceof MultiInstanceActivityBehavior) {
executionEntityManager.deleteChildExecutions(parentExecutionEntity, "", false);
// 修改is_mi_root_字段
parentExecutionEntity.setMultiInstanceRoot(false);
// 修改is_active_
parentExecutionEntity.setActive(true);
// 需要更新数据
executionEntityManager.update(parentExecutionEntity);
} else {
// 删除跳转前的任务节点
taskService.deleteTask(taskEntity, true);
HistoryManager historyManager = CommandContextUtil.getHistoryManager();
historyManager.recordTaskEnd(taskEntity, executionEntity, "", DateUtils.getDate());
historyManager.recordActivityEnd(parentExecutionEntity, "", DateUtils.getDate());
}
//----------------------------执行节点跳转-------------------------------
// 获取目标节点
FlowElement targetFlowElement = process.getFlowElement(taskJumpRequest.getTargetNode());
// 指定当前节点为目标节点
executionEntity.setCurrentFlowElement(targetFlowElement);
FlowableEngineAgenda flowableEngineAgenda = CommandContextUtil.getAgenda();
if (!(currentBehavior instanceof MultiInstanceActivityBehavior)) {
flowableEngineAgenda.planContinueProcessInCompensation(executionEntity);
} else {
// 触发一下,让执行实例往下运行 NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
ExecutionEntity rootExecutionEntity = parentExecutionEntity.getParent();
List<ExecutionEntity> executionEntities = executionEntityManager.findChildExecutionsByParentExecutionId(rootExecutionEntity.getId());
if (executionEntities.size() > 0) {
ExecutionEntity oneExecutionEntity = executionEntities.get(0);
oneExecutionEntity.setCurrentFlowElement(targetFlowElement);
// 让流程实例运转
flowableEngineAgenda.planContinueProcessInCompensation(oneExecutionEntity);
}
}
return null;
}
}
@@ -0,0 +1,29 @@
package org.jeecg.modules.bpm.cmd.dto;
import lombok.Data;
/**
* @Description: 动态添加节点
* @Author: zyf
* @Date: 2022-05-26
*/
@Data
public class TaskAddRequest {
/**
* 流程实例ID
*/
private String procInsId;
/**
* 当前任务节点
*/
private String currentTaskDefKey;
/**
* 添加节点的名字
*/
private String taskName;
/**
* 添加节点处理人
*/
private String assignee;
}
@@ -0,0 +1,29 @@
package org.jeecg.modules.bpm.cmd.dto;
import lombok.Data;
/**
* @Description: 任务办理请求参数对象
* @Author: zyf
* @Date: 2022-05-10
*/
@Data
public class TaskJumpRequest {
/**
* 流程实例ID
*/
private String procInsId;
/**
* 当前任务ID
*/
private String taskId;
/**
* 目标任务ID
*/
private String targetNode;
}
@@ -0,0 +1,64 @@
package org.jeecg.modules.bpm.cmd.util;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.jeecg.common.exception.JeecgBootException;
import java.util.Objects;
/**
* @Description: API断言工具类
* @Author: zyf
* @Date: 2022-05-07
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class ApiAssert {
/**
* obj1 eq obj2
*
* @param obj1
* @param obj2
* @param info
*/
public static void eq(String info, Object obj1, Object obj2) {
if (!Objects.equals(obj1, obj2)) {
failure(info);
}
}
public static void isTrue(String info, boolean condition) {
if (!condition) {
failure(info);
}
}
public static void isFalse(String info, boolean condition) {
if (condition) {
failure(info);
}
}
public static void isEmpty(String info, Object obj) {
if (ObjectUtils.isNotEmpty(obj)) {
failure(info);
}
}
public static void isNotEmpty(String info, Object obj) {
if (ObjectUtils.isEmpty(obj)) {
failure(info);
}
}
/**
* 失败结果
*
* @param info 错误信息
*/
public static void failure(String info) {
throw new JeecgBootException(info);
}
}
@@ -0,0 +1,461 @@
package org.jeecg.modules.bpm.cmd.util;
import cn.hutool.core.util.ObjectUtil;
import com.google.common.collect.Sets;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.common.engine.impl.interceptor.CommandContext;
import org.flowable.engine.impl.delegate.ActivityBehavior;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
import org.flowable.engine.impl.util.CommandContextUtil;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bpm.constant.FlowableConstant;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @Description: 流程工具类
* @Author: zyf
* @Date: 2022-05-07
*/
public class FlowableUtils {
/**
* 检测导入文件名是否符合flowable导入规范
*
* @param fileName
* @return
*/
public static boolean checkImportFileName(String fileName) {
boolean check = oConvertUtils.isNotEmpty(fileName) && (fileName.endsWith(".bpmn20.xml") || fileName.endsWith(".bpmn") || fileName.toLowerCase().endsWith(".bar") || fileName.toLowerCase().endsWith(".zip"));
if (!check) {
ApiAssert.failure("请求文件必须以.bpmn20.xml,.bpmn,.bar,.zip结尾");
}
return true;
}
/**
* 检测文件是否是BpmnFile
*
* @param fileName
* @return
*/
public static boolean isBpmnFile(String fileName) {
return fileName.endsWith(".bpmn20.xml") || fileName.endsWith(".bpmn");
}
/**
* 是否可访问
*
* @param process
* @param sourceElement
* @param targetElement
* @return
*/
public static boolean isReachable(Process process, FlowNode sourceElement, FlowNode targetElement) {
return isReachable(process, sourceElement, targetElement, Sets.newHashSet());
}
/**
* 是否可访问
*
* @param process
* @param sourceElement
* @param targetElement
* @param visitedElements
* @return
*/
public static boolean isReachable(Process process, FlowNode sourceElement, FlowNode targetElement,
Set<String> visitedElements) {
//特殊情况:事件子流程中的启动事件可能作为执行存在,并且最有可能达到目标
//当目标位于事件子流程中,但应忽略,因为它们不是“真正的”运行时执行,(而是在等待/触发器)
if (sourceElement instanceof StartEvent && isInEventSubprocess(sourceElement)) {
return false;
}
// 没有传出序列流:可能是eg的结束。进程或嵌入式子进程
if (sourceElement.getOutgoingFlows().size() == 0) {
visitedElements.add(sourceElement.getId());
FlowElementsContainer parentElement = process.findParent(sourceElement);
if (parentElement instanceof SubProcess) {
sourceElement = (SubProcess) parentElement;
// 子流程的结束节点,若目标节点在该子流程中,说明无法到达,返回false
if (((SubProcess) sourceElement).getFlowElement(targetElement.getId()) != null) {
return false;
}
} else {
return false;
}
}
if (sourceElement.getId().equals(targetElement.getId())) {
return true;
}
// 为了避免无限循环,我们必须捕获我们访问的每个节点
visitedElements.add(sourceElement.getId());
// 当前节点能够到达子流程,且目标节点在子流程中,说明可以到达,返回true
if (sourceElement instanceof SubProcess
&& ((SubProcess) sourceElement).getFlowElement(targetElement.getId()) != null) {
return true;
}
List<SequenceFlow> sequenceFlows = sourceElement.getOutgoingFlows();
if (sequenceFlows != null && sequenceFlows.size() > 0) {
for (SequenceFlow sequenceFlow : sequenceFlows) {
String targetRef = sequenceFlow.getTargetRef();
FlowNode sequenceFlowTarget = (FlowNode) process.getFlowElement(targetRef, true);
if (sequenceFlowTarget != null && !visitedElements.contains(sequenceFlowTarget.getId())) {
boolean reachable = isReachable(process, sequenceFlowTarget, targetElement, visitedElements);
if (reachable) {
return true;
}
}
}
}
return false;
}
/**
* 是否是在事件子进程中
*
* @param flowNode
* @return
*/
protected static boolean isInEventSubprocess(FlowNode flowNode) {
FlowElementsContainer flowElementsContainer = flowNode.getParentContainer();
while (flowElementsContainer != null) {
if (flowElementsContainer instanceof EventSubProcess) {
return true;
}
if (flowElementsContainer instanceof FlowElement) {
flowElementsContainer = ((FlowElement) flowElementsContainer).getParentContainer();
} else {
flowElementsContainer = null;
}
}
return false;
}
public static List<String> getParentProcessIds(FlowNode flowNode) {
List<String> result = new ArrayList<>();
FlowElementsContainer flowElementsContainer = flowNode.getParentContainer();
while (flowElementsContainer != null) {
if (flowElementsContainer instanceof SubProcess) {
SubProcess flowElement = (SubProcess) flowElementsContainer;
result.add(flowElement.getId());
flowElementsContainer = flowElement.getParentContainer();
} else if (flowElementsContainer instanceof Process) {
Process flowElement = (Process) flowElementsContainer;
result.add(flowElement.getId());
flowElementsContainer = null;
}
}
// 第一层Process为第0个
Collections.reverse(result);
return result;
}
/**
* 查询不同层级
*
* @param sourceList
* @param targetList
* @return 返回不同的层级,如果其中一个层级较深,则返回层级小的+1,从第0层开始,请注意判断是否会出现下标越界异常;返回 -1 表示在同一层
*/
public static Integer getDiffLevel(List<String> sourceList, List<String> targetList) {
if (sourceList == null || sourceList.isEmpty() || targetList == null || targetList.isEmpty()) {
throw new FlowableException("sourceList and targetList cannot be empty");
}
if (sourceList.size() == 1 && targetList.size() == 1) {
// 都在第0层且不相等
if (!sourceList.get(0).equals(targetList.get(0))) {
return 0;
} else {// 都在第0层且相等
return -1;
}
}
int minSize = sourceList.size() < targetList.size() ? sourceList.size() : targetList.size();
Integer targetLevel = null;
for (int i = 0; i < minSize; i++) {
if (!sourceList.get(i).equals(targetList.get(i))) {
targetLevel = i;
break;
}
}
if (targetLevel == null) {
if (sourceList.size() == targetList.size()) {
targetLevel = -1;
} else {
targetLevel = minSize;
}
}
return targetLevel;
}
public static Map<String, Set<String>> getSpecialGatewayElements(FlowElementsContainer container) {
return getSpecialGatewayElements(container, null);
}
public static Map<String, Set<String>> getSpecialGatewayElements(FlowElementsContainer container, Map<String, Set<String>> specialGatewayElements) {
if (specialGatewayElements == null) {
specialGatewayElements = new HashMap<>(16);
}
Collection<FlowElement> flowelements = container.getFlowElements();
for (FlowElement flowElement : flowelements) {
boolean isBeginSpecialGateway = flowElement.getId().endsWith(FlowableConstant.SPECIAL_GATEWAY_BEGIN_SUFFIX)
&& (flowElement instanceof ParallelGateway || flowElement instanceof InclusiveGateway
|| flowElement instanceof ComplexGateway);
if (isBeginSpecialGateway) {
String gatewayBeginRealId = flowElement.getId();
String gatewayId = gatewayBeginRealId.substring(0, gatewayBeginRealId.length() - 6);
Set<String> gatewayIdContainFlowelements = specialGatewayElements.computeIfAbsent(gatewayId,
k -> new HashSet<>());
findElementsBetweenSpecialGateway(flowElement, gatewayId + FlowableConstant.SPECIAL_GATEWAY_END_SUFFIX,
gatewayIdContainFlowelements);
} else if (flowElement instanceof SubProcess) {
getSpecialGatewayElements((SubProcess) flowElement, specialGatewayElements);
}
}
// 外层到里层排序
Map<String, Set<String>> specialGatewayNodesSort = new LinkedHashMap<>();
specialGatewayElements.entrySet().stream().sorted((o1, o2) -> o2.getValue().size() - o1.getValue().size())
.forEach(entry -> specialGatewayNodesSort.put(entry.getKey(), entry.getValue()));
return specialGatewayNodesSort;
}
public static void findElementsBetweenSpecialGateway(FlowElement specialGatewayBegin, String specialGatewayEndId,
Set<String> elements) {
elements.add(specialGatewayBegin.getId());
List<SequenceFlow> sequenceFlows = ((FlowNode) specialGatewayBegin).getOutgoingFlows();
if (sequenceFlows != null && sequenceFlows.size() > 0) {
for (SequenceFlow sequenceFlow : sequenceFlows) {
FlowElement targetFlowElement = sequenceFlow.getTargetFlowElement();
String targetFlowElementId = targetFlowElement.getId();
elements.add(specialGatewayEndId);
if (targetFlowElementId.equals(specialGatewayEndId)) {
continue;
} else {
findElementsBetweenSpecialGateway(targetFlowElement, specialGatewayEndId, elements);
}
}
}
}
public static String[] getSourceAndTargetRealActivityId(FlowNode sourceFlowElement, FlowNode targetFlowElement) {
// 实际应操作的当前节点ID
String sourceRealActivityId = sourceFlowElement.getId();
// 实际应操作的目标节点ID
String targetRealActivityId = targetFlowElement.getId();
List<String> sourceParentProcesss = FlowableUtils.getParentProcessIds(sourceFlowElement);
List<String> targetParentProcesss = getParentProcessIds(targetFlowElement);
int diffParentLevel = getDiffLevel(sourceParentProcesss, targetParentProcesss);
if (diffParentLevel != -1) {
sourceRealActivityId = sourceParentProcesss.size() == diffParentLevel ? sourceRealActivityId : sourceParentProcesss.get(diffParentLevel);
targetRealActivityId = targetParentProcesss.size() == diffParentLevel ? targetRealActivityId : targetParentProcesss.get(diffParentLevel);
}
return new String[]{sourceRealActivityId, targetRealActivityId};
}
public static void setSpecialGatewayList(String sourceActivityId, String targetActivityId, Map<String, Set<String>> specialGatewayNodes, List<String> sourceInSpecialGatewayList, List<String> targetInSpecialGatewayList) {
for (Map.Entry<String, Set<String>> entry : specialGatewayNodes.entrySet()) {
if (entry.getValue().contains(sourceActivityId)) {
sourceInSpecialGatewayList.add(entry.getKey());
}
if (entry.getValue().contains(targetActivityId)) {
targetInSpecialGatewayList.add(entry.getKey());
}
}
}
public static boolean isFlowElementMultiInstance(FlowElement flowElement) {
if (flowElement instanceof Activity) {
return ((Activity) flowElement).getLoopCharacteristics() != null;
}
return false;
}
public static Optional<String> getFlowElementMultiInstanceParentId(FlowElement flowElement) {
FlowElementsContainer parentContainer = flowElement.getParentContainer();
while (parentContainer instanceof Activity) {
if (isFlowElementMultiInstance((Activity) parentContainer)) {
return Optional.of(((Activity) parentContainer).getId());
}
parentContainer = ((Activity) parentContainer).getParentContainer();
}
return Optional.empty();
}
public static boolean isExecutionInsideMultiInstance(ExecutionEntity execution) {
return getFlowElementMultiInstanceParentId(execution.getCurrentFlowElement()).isPresent();
}
public static Set<String> getParentExecutionIdsByActivityId(List<ExecutionEntity> executions, String activityId) {
List<ExecutionEntity> activityIdExecutions = executions.stream()
.filter(e -> activityId.equals(e.getActivityId())).collect(Collectors.toList());
if (activityIdExecutions.isEmpty()) {
throw new FlowableException("Active execution could not be found with activity id " + activityId);
}
// check for a multi instance root execution
ExecutionEntity miExecution = null;
boolean isInsideMultiInstance = false;
for (ExecutionEntity possibleMiExecution : activityIdExecutions) {
if (possibleMiExecution.isMultiInstanceRoot()) {
miExecution = possibleMiExecution;
isInsideMultiInstance = true;
break;
}
if (isExecutionInsideMultiInstance(possibleMiExecution)) {
isInsideMultiInstance = true;
}
}
Set<String> parentExecutionIds = new HashSet<>();
if (isInsideMultiInstance) {
Stream<ExecutionEntity> executionEntitiesStream = activityIdExecutions.stream();
if (miExecution != null) {
executionEntitiesStream = executionEntitiesStream.filter(ExecutionEntity::isMultiInstanceRoot);
}
executionEntitiesStream.forEach(childExecution -> {
parentExecutionIds.add(childExecution.getParentId());
});
} else {
ExecutionEntity execution = activityIdExecutions.iterator().next();
parentExecutionIds.add(execution.getParentId());
}
return parentExecutionIds;
}
public static String getParentExecutionIdFromParentIds(ExecutionEntity execution, Set<String> parentExecutionIds) {
ExecutionEntity taskParentExecution = execution.getParent();
String realParentExecutionId = null;
while (taskParentExecution != null) {
if (parentExecutionIds.contains(taskParentExecution.getId())) {
realParentExecutionId = taskParentExecution.getId();
break;
}
taskParentExecution = taskParentExecution.getParent();
}
if (realParentExecutionId == null || realParentExecutionId.length() == 0) {
throw new FlowableException("Parent execution could not be found with executionId id " + execution.getId());
}
return realParentExecutionId;
}
public static List<ExecutionEntity> getRealExecutions(CommandContext commandContext, String processInstanceId,
String taskExecutionId, String sourceRealActivityId, Set<String> activityIds) {
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager(commandContext);
ExecutionEntity taskExecution = executionEntityManager.findById(taskExecutionId);
List<ExecutionEntity> executions = executionEntityManager.findChildExecutionsByProcessInstanceId(processInstanceId);
Set<String> parentExecutionIds = getParentExecutionIdsByActivityId(executions, sourceRealActivityId);
String realParentExecutionId = getParentExecutionIdFromParentIds(taskExecution,
parentExecutionIds);
List<ExecutionEntity> childExecutions = executionEntityManager.findExecutionsByParentExecutionAndActivityIds(realParentExecutionId, activityIds);
return childExecutions;
}
public static void createTargetInSpecialGatewayEndExecutions(CommandContext commandContext, List<ExecutionEntity> excutionEntitys, Process process, List<String> targetInSpecialGatewayList, String targetRealSpecialGateway) {
// 目标节点相对当前节点处于并行网关,需要手动生成并行网关汇聚节点(_end)的execution数据
String parentExecutionId = excutionEntitys.iterator().next().getParentId();
ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager(commandContext);
ExecutionEntity parentExecutionEntity = executionEntityManager.findById(parentExecutionId);
int index = targetInSpecialGatewayList.indexOf(targetRealSpecialGateway);
for (; index < targetInSpecialGatewayList.size(); index++) {
String targetInSpecialGateway = targetInSpecialGatewayList.get(index);
String targetInSpecialGatewayEndId = targetInSpecialGateway + FlowableConstant.SPECIAL_GATEWAY_END_SUFFIX;
FlowNode targetInSpecialGatewayEnd = (FlowNode) process.getFlowElement(targetInSpecialGatewayEndId, true);
int nbrOfExecutionsToJoin = targetInSpecialGatewayEnd.getIncomingFlows().size();
// 处理目标节点所处的分支以外的分支,即 总分枝数-1 = nbrOfExecutionsToJoin - 1
for (int i = 0; i < nbrOfExecutionsToJoin - 1; i++) {
ExecutionEntity childExecution = executionEntityManager.createChildExecution(parentExecutionEntity);
childExecution.setCurrentFlowElement(targetInSpecialGatewayEnd);
ActivityBehavior activityBehavior = (ActivityBehavior) targetInSpecialGatewayEnd.getBehavior();
activityBehavior.execute(childExecution);
}
}
}
/**
* long类型日期转换为x天x时x分格式
*
* @param startTime
* @param endTime
* @return
*/
public static String longTimeToStr(Date startTime, Date endTime) {
long start = startTime.getTime();
long end = DateUtils.getDate().getTime();
if (ObjectUtil.isNotEmpty(endTime)) {
end = endTime.getTime();
}
long mu=end-start;
return longTimeToStr(mu);
}
/**
* long类型日期转换为x天x时x分格式
*
* @param time
* @return
*/
public static String longTimeToStr(Long time) {
StringBuilder builder = new StringBuilder();
if (ObjectUtil.isNotEmpty(time)) {
try {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
format.setTimeZone(TimeZone.getTimeZone("GMT+00:00"));
String dateTime = format.format(time);
Date date = format.parse(dateTime);
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
Calendar orgin = Calendar.getInstance();
orgin.setTimeInMillis(0);
int year = calendar.get(Calendar.YEAR) - orgin.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) - orgin.get(Calendar.MONTH);
int day = calendar.get(Calendar.DATE) - orgin.get(Calendar.DATE);
int hour = calendar.get(Calendar.HOUR_OF_DAY) - orgin.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE) - orgin.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND) - orgin.get(Calendar.SECOND);
int millisecond = calendar.get(Calendar.MILLISECOND) - orgin.get(Calendar.MILLISECOND);
if (year != 0) {
builder.append(year).append("");
}
if (month != 0) {
builder.append(month).append("");
}
if (day != 0) {
builder.append(day).append("");
}
if (hour != 0) {
builder.append(Math.abs(hour)).append("小时");
}
if (minute != 0) {
builder.append(minute).append("");
}
if (second != 0 && day <= 0) {
builder.append(second).append("");
}
if (millisecond != 0 && second <= 0) {
builder.append(millisecond).append("毫秒");
}
} catch (Exception e) {
ApiAssert.failure("格式化日期出错");
}
}
return builder.toString();
}
}
@@ -0,0 +1,22 @@
package org.jeecg.modules.bpm.config;
/**
* 项目名称:jeecg
* 类名称:Globals
* 类描述: 全局变量定义
* 创建人:scott
* @version
*
*/
public final class BpmGlobals {
/**
* 我发起流程: 根据不同关键词查询不同状态流程
* isUnfinished/查询未完成流程
* isFinished/查询已完成流程
*/
public static final String IS_UN_FINISHED = "isUnfinished";
public static final String IS_FINISHED = "isFinished";
public static final String XX_SIGN_GG = "NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=";
}
@@ -0,0 +1,46 @@
//package org.jeecg.modules.bpm.config;
//
//import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
//import org.flowable.common.engine.api.delegate.event.FlowableEventDispatcher;
//import org.flowable.spring.SpringProcessEngineConfiguration;
//import org.jeecg.modules.bpm.config.flowable.GlobalProcessStartedListener;
//import org.jeecg.modules.bpm.config.flowable.GlobalProcistEndListener;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.context.ApplicationListener;
//import org.springframework.context.annotation.Configuration;
//import org.springframework.context.event.ContextRefreshedEvent;
//
///**
// * @TODO 待实现未用上1
// *
// * Flowable全局监听配置
// * 用途:在任务特殊节点或者流程的特殊节点做一些自定义操作
// *
// * @author gblfy
// * @Date 2022-06-21
// **/
//@Configuration("flowableGlobListenerConfig")
//public class FlowableGlobListenerConfig implements ApplicationListener<ContextRefreshedEvent> {
// @Autowired
// private SpringProcessEngineConfiguration configuration;
// @Autowired
// private GlobalProcessStartedListener globalProcessStartedListener;
// @Autowired
// private GlobalProcistEndListener globalProcistEndListener;
//
// @Override
// public void onApplicationEvent(ContextRefreshedEvent event) {
// FlowableEventDispatcher dispatcher = configuration.getEventDispatcher();
//
// //流程开始全局监听
// dispatcher.addEventListener(globalProcessStartedListener, FlowableEngineEventType.PROCESS_STARTED);
//
// //流程结束全局监听
// dispatcher.addEventListener(globalProcistEndListener, FlowableEngineEventType.PROCESS_COMPLETED,
// FlowableEngineEventType.PROCESS_CANCELLED,
// FlowableEngineEventType.PROCESS_COMPLETED_WITH_ERROR_END_EVENT,
// FlowableEngineEventType.PROCESS_COMPLETED_WITH_TERMINATE_END_EVENT);
// }
//
//}
//
@@ -0,0 +1,122 @@
package org.jeecg.modules.bpm.config;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import lombok.extern.slf4j.Slf4j;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.common.engine.impl.cfg.IdGenerator;
import org.flowable.image.ProcessDiagramGenerator;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.flowable.spring.boot.EngineConfigurationConfigurer;
import org.jeecg.modules.bpm.job.CustomTimeStartJobEventHandler;
import org.jeecg.modules.bpm.constant.FlowableConstant;
import org.jeecg.modules.bpm.service.impl.image.CustomProcessDiagramGenerator;
import org.jeecg.modules.extbpm.listener.global.GlobalEventListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 流程配置信息
* @date 2019/03/06
* @author scott
*/
@Slf4j
@Configuration("flowableEngineConfiguration")
public class ProcessEngineConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
@Autowired
public DataSource dataSource;
@Override
public void configure(SpringProcessEngineConfiguration springProcessEngineConfiguration) {
//------------------------------------------------------------------------
// 1.流程图渲染设置
springProcessEngineConfiguration.setProcessDiagramGenerator(processDiagramGenerator());
//设置流程图字体(防止乱码)
springProcessEngineConfiguration.setActivityFontName(FlowableConstant.FONT_NAME);
//设置label字体
springProcessEngineConfiguration.setLabelFontName(FlowableConstant.FONT_NAME);
//设置注释字体
springProcessEngineConfiguration.setAnnotationFontName(FlowableConstant.FONT_NAME);
//关闭定时任务,有需要再开启 NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
//springProcessEngineConfiguration.setAsyncExecutorActivate(false);
//------------------------------------------------------------------------
// 2.自定义id生成规则
springProcessEngineConfiguration.setIdGenerator(new IdGenerator() {
@Override
public String getNextId() {
try {
return IdWorker.getIdStr(); //id生成方法
} catch (Exception e) {
}
return null;
}
});
//------------------------------------------------------------------------
// 3.国产化数据库适配
this.setFlowableDataSource(springProcessEngineConfiguration);
//------------------------------------------------------------------------
// 4.全局监听器
Map<String, String> handlers = new HashMap<String, String>();
List<FlowableEventListener> eventListeners = new ArrayList<FlowableEventListener>();
GlobalEventListener global = new GlobalEventListener();
global.setHandlers(handlers);
eventListeners.add(global);
springProcessEngineConfiguration.setEventListeners(eventListeners);
//5.注册自定义作业处理器
springProcessEngineConfiguration.addCustomJobHandler(new CustomTimeStartJobEventHandler());
}
/**
* 国产化数据库适配兼容
* @param springProcessEngineConfiguration
*/
private void setFlowableDataSource(SpringProcessEngineConfiguration springProcessEngineConfiguration){
long time1=System.currentTimeMillis();
try {
Connection connection = dataSource.getConnection();
DatabaseMetaData md = connection.getMetaData();
String dbType = md.getDatabaseProductName();
//log.info("DataSource Customize DbType = " + dbType);
//log.info("ProcessEngine databaseType = " + springProcessEngineConfiguration.getDatabaseType());
//达梦数据库兼容,采用oracle类型
if (dbType.toLowerCase().indexOf("dm") >= 0) {
springProcessEngineConfiguration.setDataSource(dataSource);
springProcessEngineConfiguration.setDatabaseType("oracle");
long time2=System.currentTimeMillis();
log.info("ProcessEngine 兼容达梦逻辑耗时:"+(time2-time1)+"ms");
}
//人大金仓数据库兼容,采用PostgreSQL类型
if (dbType.toLowerCase().indexOf("kingbase") >= 0) {
springProcessEngineConfiguration.setDataSource(dataSource);
springProcessEngineConfiguration.setDatabaseType("postgres");
long time2=System.currentTimeMillis();
log.info("ProcessEngine 兼容人大金仓逻辑耗时:"+(time2-time1)+"ms");
}
} catch (SQLException e) {
log.error(e.getMessage(), e);
}
}
/**
* 自定义流程进度图
*
* @return
*/
@Bean("customProcessDiagramGenerator")
public ProcessDiagramGenerator processDiagramGenerator() {
return new CustomProcessDiagramGenerator();
}
}
@@ -0,0 +1,49 @@
package org.jeecg.modules.bpm.config.flowable;
import org.flowable.engine.delegate.event.impl.FlowableEntityEventImpl;
import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
import org.flowable.engine.delegate.event.FlowableProcessStartedEvent;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 全局的流程启动的监听器(有效,但未写逻辑)
*
* @author: gblfy
* @date 2022-06-21
*/
@Component("globalProcessStartedListener")
public class GlobalProcessStartedListener extends AbstractFlowableEngineEventListener {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
protected void processStarted(FlowableProcessStartedEvent event) {
// logger.info("进入流程开始监听器------------------------Start---------------------->");
String eventName = event.getType().name();
FlowableEntityEventImpl flowableEntityEvent = (FlowableEntityEventImpl) event;
ExecutionEntityImpl processInstance = (ExecutionEntityImpl) flowableEntityEvent.getEntity();
Date startTime = processInstance.getStartTime();
String processDefinitionKey = processInstance.getProcessDefinitionKey();
String processInstanceId = processInstance.getProcessInstanceId();
String processInstanceBusinessKey = processInstance.getProcessInstanceBusinessKey();
int suspensionState = processInstance.getSuspensionState();
// logger.info("流程事件类型->{}", eventName);
// logger.info("流程开始时间->{}", startTime);
// logger.info("流程定义Key->{}", processDefinitionKey);
// logger.info("流程实例ID->{}", processInstanceId);
// logger.info("流程业务key->{}", processInstanceBusinessKey);
// logger.info("流程是否挂起标志->{}", suspensionState);
// logger.info("流程开始监听器------------------------End---------------------->");
}
}
@@ -0,0 +1,49 @@
package org.jeecg.modules.bpm.config.flowable;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
import org.flowable.engine.delegate.event.impl.FlowableEntityEventImpl;
import org.flowable.engine.impl.persistence.entity.ExecutionEntityImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 流程结束全局监听器(有效,但未写逻辑)
*
* @author: gblfy
* @date 2022-06-21
*/
@Component("globalProcistEndListener")
public class GlobalProcistEndListener extends AbstractFlowableEngineEventListener {
protected Logger logger = LoggerFactory.getLogger(this.getClass());
@Override
protected void processCompleted(FlowableEngineEntityEvent event) {
// logger.info("进入流程完成监听器------------------------Start---------------------->");
String eventName = event.getType().name();
FlowableEntityEventImpl flowableEntityEvent = (FlowableEntityEventImpl) event;
ExecutionEntityImpl processInstance = (ExecutionEntityImpl) flowableEntityEvent.getEntity();
Date startTime = processInstance.getStartTime();
String processDefinitionKey = processInstance.getProcessDefinitionKey();
String processInstanceId = processInstance.getProcessInstanceId();
String processInstanceBusinessKey = processInstance.getProcessInstanceBusinessKey();
int suspensionState = processInstance.getSuspensionState();
// logger.info("流程事件类型->{}", eventName);
// logger.info("流程开始时间->{}", startTime);
// logger.info("流程定义Key->{}", processDefinitionKey);
// logger.info("流程实例ID->{}", processInstanceId);
// logger.info("流程业务key->{}", processInstanceBusinessKey);
// logger.info("流程是否挂起标志->{}", suspensionState);
// logger.info("流程完成监听器------------------------End---------------------->");
}
}
@@ -0,0 +1,52 @@
package org.jeecg.modules.bpm.constant;
/**
* @Description: 流程常量定义
* @Author: zyf
* @Date: 2022-05-05
*/
public final class FlowableConstant {
//TODO 待测试
public final static String SPECIAL_GATEWAY_BEGIN_SUFFIX = "_begin";
//TODO 待测试
public final static String SPECIAL_GATEWAY_END_SUFFIX = "_end";
/**
* 流程图字体
*/
public static final String FONT_NAME = "宋体";
/**
* 会签任务总数
*/
public static final String NUMBER_OF_INSTANCES = "nrOfInstances";
/**
* 【自定义变量:串行会签人员集合】
* 简流模式支持选固定会签人员,进行加减签
*/
public static final String LOOP_ASSIGNEE_COLLECTION = "loopAssigneeCollection";
/**
* 正在执行的会签总数
*/
public static final String NUMBER_OF_ACTIVE_INSTANCES = "nrOfActiveInstances";
/**
* 已完成的会签任务总数
*/
public static final String NUMBER_OF_COMPLETED_INSTANCES = "nrOfCompletedInstances";
/**
* 循环的索引值,可以使用elementIndexVariable属性修改loopCounter的变量名 NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
*/
public static final String LOOP_COUNTER = "loopCounter";
//后加签
public static final String AFTER_ADDSIGN = "after";
//前加签
public static final String BEFORE_ADDSIGN = "before";
}
@@ -0,0 +1,44 @@
package org.jeecg.modules.bpm.constant.enums;
/**
* @TODO 待实现未用上1
*
* @Description: 业务状态
* @Author: zyf
* @Date: 2022-06-31
*/
public enum BusinessStatusEnum {
/**
* 枚举字段
*/
NOTSUBMIT("1", "待提交"), PROCESSING("2", "处理中"), FINISH("3", "已完成"), DELETE("4", "已作废"), SUSPEND("5", "已挂起");
/**
* 名称
*/
private String status;
/**
* 名称
*/
private String statusName;
public static String getStatusName(String status) {
for (BusinessStatusEnum e : BusinessStatusEnum.values()) {
if (e.status.equals(status)) {
return e.statusName;
}
}
return "";
}
BusinessStatusEnum(String status, String statusName) {
this.status = status;
this.statusName = statusName;
}
public String getStatus() {
return status;
}
}
@@ -0,0 +1,60 @@
package org.jeecg.modules.bpm.constant.enums;
/**
* @TODO 待实现未用上1
*
* @author : scott
* @title: : CommentTypeEnum
* @projectName : flowable
* @description: 审批意见的类型
* @date : 2019/11/2412:53
*/
public enum CommentTypeEnum {
SP("审批"),
BH("驳回"),
CH("撤回"),
ZC("暂存"),
QS("签收"),
WP("委派"),
ZH("知会"),
ZY("转阅"),
YY("已阅"),
ZB("转办"),
QJQ("前加签"),
HJQ("后加签"),
XTZX("系统执行"),
TJ("提交"),
CXTJ("重新提交"),
SPJS("审批结束"),
LCZZ("流程终止"),
SQ("授权"),
CFTG("重复跳过"),
XT("协同"),
PS("评审");
private String name;//名称
/**
* 通过type获取Msg
*
* @param type
* @return
* @Description:
*/
public static String getEnumMsgByType(String type) {
for (CommentTypeEnum e : CommentTypeEnum.values()) {
if (e.toString().equals(type)) {
return e.name;
}
}
return "";
}
private CommentTypeEnum(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@@ -0,0 +1,120 @@
//package org.jeecg.modules.bpm.controller;
//
//import java.util.Arrays;
//import java.util.List;
//import java.util.Map;
//
//import javax.servlet.http.HttpServletRequest;
//import javax.servlet.http.HttpServletResponse;
//
//import lombok.extern.slf4j.Slf4j;
//import org.activiti.engine.repository.Model;
//import org.jeecg.common.api.vo.Result;
//import org.jeecg.common.util.oConvertUtils;
//import org.jeecg.modules.bpm.dto.ModelDTO;
//import org.jeecg.modules.bpm.service.impl.ActModelService;
//import org.jeecg.modules.extbpm.util.CommonRandomUtil;
//import org.springframework.beans.factory.annotation.Autowired;
//import org.springframework.web.bind.annotation.DeleteMapping;
//import org.springframework.web.bind.annotation.GetMapping;
//import org.springframework.web.bind.annotation.PathVariable;
//import org.springframework.web.bind.annotation.RequestBody;
//import org.springframework.web.bind.annotation.RequestMapping;
//import org.springframework.web.bind.annotation.RequestParam;
//import org.springframework.web.bind.annotation.RestController;
//
//import com.baomidou.mybatisplus.core.metadata.IPage;
//import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
//
//import springfox.documentation.annotations.ApiIgnore;
//
///**
// * 流程设计管理
// *
// * @author scott
// * @date 2019/03/06
// * @desc
// */
//@Slf4j
//@RestController("act/model")
//@RequestMapping("/act/model")
//public class ActModelController {
// @Autowired
// private ActModelService actModelService;
//
// /**
// * 流程设计模型列表
// * @param params
// * @return
// */
// @GetMapping("list")
// public Result<IPage<Model>> list(@Parameter(hidden = true) @RequestParam Map<String, Object> params) {
// Result<IPage<Model>> result = new Result<IPage<Model>>();
// IPage<Model> pageList = new Page<Model>();
// List<Model> records = actModelService.page(params);
// pageList.setRecords(records);
// result.setSuccess(true);
// result.setResult(pageList);
// return result;
// }
//
// /**
// * 创建模型
// */
// @RequestMapping("/create")
// public Result<String> create(@RequestBody ModelDTO dto, HttpServletRequest request, HttpServletResponse response) {
// Result<String> result = new Result<String>();
// try {
// String modelId = actModelService.save(dto.getName(), dto.getKey(), dto.getDescription());
// result.setResult(modelId);
// //response.sendRedirect(request.getContextPath() + "/modeler.html?modelId=" + modelId);
// } catch (Exception e) {
// e.printStackTrace();
// log.error("新建流程失败!");
// }
// return result;
// }
//
//
// /**
// * 流程发布
// * @param id
// * @return
// */
// @GetMapping("deploy/{id}")
// public Result deploy(@PathVariable("id") String id) {
// actModelService.deploy(id);
// return Result.ok("流程发布成功");
// }
//
// /**
// * 流程导出
// * @param id
// * @param response
// * @return
// */
// @GetMapping("export/{id}")
// public Result<Object> export(@PathVariable("id") String id, @Parameter(hidden = true) HttpServletResponse response) {
// try {
// actModelService.export(id, response);
// return Result.ok("导出成功!");
// } catch (Exception e) {
// return Result.error("导出失败:"+e.toString());
// }
// }
//
// /**
// * 删除流程设计模型
// * @param ids
// * @return
// */
// @DeleteMapping(value = "/delete")
// public Result delete(@RequestParam(name = "ids", required = true) String ids) {
// if(CommonRandomUtil.isNotEmpty(ids)) {
// for (String id : Arrays.asList(ids.split(","))) {
// actModelService.delete(id);
// }
// }
// return Result.ok("流程删除成功");
// }
//}
@@ -0,0 +1,279 @@
package org.jeecg.modules.bpm.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.flowable.common.engine.api.FlowableException;
import org.flowable.engine.RepositoryService;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.util.RedisUtil;
import org.jeecg.common.util.TokenUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bpm.service.impl.ActProcessService;
import org.jeecg.modules.extbpm.process.service.IExtActProcessService;
import org.jeecg.modules.extbpm.util.CommonRandomUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipInputStream;
/**
* 流程管理功能
*
* @author scott
*/
@Slf4j
@Tag(name="流程实例")
@RestController("actProcessController")
@RequestMapping("/act/process")
public class ActProcessController {
@Autowired
private RepositoryService repositoryService;
@Autowired
private ActProcessService actProcessService;
@Autowired
private IExtActProcessService extActProcessService;
@Lazy
@Autowired
private ISysBaseAPI sysBaseAPI;
@Autowired
@Lazy
private RedisUtil redisUtil;
/**
* 查询所有版本的流程
* @param params
* @return
*/
@GetMapping("list")
public Result<IPage<Map<String, Object>>> list(@RequestParam Map<String, Object> params) {
Result<IPage<Map<String, Object>>> result = new Result<IPage<Map<String, Object>>>();
IPage<Map<String, Object>> pageList = new Page<Map<String, Object>>();
List<Map<String, Object>> records = actProcessService.page(params);
pageList.setRecords(records);
result.setSuccess(true);
result.setResult(pageList);
return result;
}
@PostMapping("deploy")
@Operation(summary = "部署流程文件")
public Result deploy(@RequestParam("processFile") MultipartFile file) throws IOException {
if (file.isEmpty()) {
return Result.error("发布流程失败!");
}
try {
actProcessService.deploy(file);
return Result.ok("流程上传成功!");
} catch (Exception e) {
return Result.error("发布流程失败: "+ e.getMessage());
}
}
/**
* 激活流程
* @param id
* @return
*/
@GetMapping("active/{id}")
public Result active(@PathVariable("id") String id) {
actProcessService.active(id);
return new Result();
}
/**
* 挂起流程
* @param id
* @return
*/
@GetMapping("suspend/{id}")
@Operation(summary="挂起流程")
@Parameter(name = "id", description = "流程ID")
public Result suspend(@PathVariable("id") String id) {
actProcessService.suspend(id);
return new Result();
}
/**
* 关闭流程
* @param key
* @return
*/
@GetMapping("close/{key}")
@Operation(summary="关闭流程")
@Parameter(name = "key", description = "流程key")
public Result close(@PathVariable("key") String key) {
actProcessService.closeProcess(key);
return new Result();
}
/**
* 启动流程
* @param key
* @return
*/
@GetMapping("open/{key}")
@Operation(summary="启动流程")
@Parameter(name = "key", description = "流程key")
public Result open(@PathVariable("key") String key) {
actProcessService.openProcess(key);
return new Result();
}
/**
* 删除
* @param deploymentIds
* @return
*/
@DeleteMapping(value = "/delete")
public Result delete(@RequestParam(name = "ids", required = true) String ids) {
if(CommonRandomUtil.isNotEmpty(ids)) {
for (String id : Arrays.asList(ids.split(","))) {
actProcessService.deleteDeployment(id);
}
}
return new Result();
}
/**
* 显示静态资源(图片/XML)
* @param deploymentId
* @param resourceName
* @param response
* @throws Exception
*/
@GetMapping(value = "resource")
public void resource(@RequestParam(value="deploymentId")String deploymentId, @RequestParam(value="resourceName")String resourceName, @Parameter(hidden = true) HttpServletResponse response) throws Exception {
log.debug("-------------deploymentId-----",deploymentId);
log.debug("-------------resourceName-----",resourceName);
InputStream resourceAsStream = actProcessService.getResourceAsStream(deploymentId, resourceName);
IOUtils.copy(resourceAsStream, response.getOutputStream());
}
/**
* 获取流程进度图
* @param processInstanceId
* @param response
* @throws Exception
*/
@GetMapping(value = "processPic")
public void processPic(@RequestParam(value="processInstanceId")String processInstanceId, HttpServletResponse response) throws Exception {
actProcessService.genProcessDiagram(response, processInstanceId);
}
/**
* 读取资源,通过部署ID
*
* @param deploymentId 流程部署的ID
* @param resourceName 资源名称(foo.xml|foo.png)
* @param response
* @throws Exception
*/
@RequestMapping(value = "downProcessXml")
public void downProcessXmlByDeploy(HttpServletRequest request, HttpServletResponse response) throws Exception {
InputStream resourceAsStream = null;
try {
String deploymentId = oConvertUtils.getString(request.getParameter("deploymentId"));
String resourceName = oConvertUtils.getString(request.getParameter("resourceName"));
String token = oConvertUtils.getString(request.getParameter("token"));
//校验 token 登录有效性
TokenUtils.verifyToken(request, sysBaseAPI, redisUtil);
resourceAsStream = repositoryService.getResourceAsStream(deploymentId, resourceName);
// 文件下载
File file = new File(resourceName);
String filename = file.getName();
// 设置response的Header
response.setContentType("application/octet-stream");
response.setHeader("Content-disposition", "attachment;filename="+ new String(filename.getBytes("utf-8"), "ISO8859-1"));
IOUtils.copy(resourceAsStream, response.getOutputStream());
response.flushBuffer();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (resourceAsStream != null) {
resourceAsStream.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
/**
* @TODO 未实现
* 通过上传流程文件,发布流程
*
* @param deploymentId 流程部署的ID
* @param resourceName 资源名称(foo.xml|foo.png)
* @param response
* @throws Exception
*/
@RequestMapping(value = "deployProcess")
@ResponseBody
public void deployProcess(HttpServletRequest request) {
MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
Map<String, MultipartFile> files = multipartRequest.getFileMap();
List<String> urls = new ArrayList<>();
for (Map.Entry<String, MultipartFile> entry : files.entrySet()) {
MultipartFile file = entry.getValue();// 获取上传文件对象
String fileName = file.getOriginalFilename();
InputStream fileInputStream = null;
try {
fileInputStream = file.getInputStream();
String extension = FilenameUtils.getExtension(fileName);
if (extension.equals("zip") || extension.equals("bar")) {
ZipInputStream zip = new ZipInputStream(fileInputStream);
repositoryService.createDeployment().addZipInputStream(zip).deploy();
zip.close();
} else if (extension.equals("png")) {
repositoryService.createDeployment().addInputStream(fileName, fileInputStream).deploy();
} else if (extension.indexOf("bpmn20.xml") != -1) {
repositoryService.createDeployment().addInputStream(fileName, fileInputStream).deploy();
} else if (extension.equals("bpmn")) {
/*
* bpmn扩展名特殊处理,转换为bpmn20.xml
*/
String baseName = FilenameUtils.getBaseName(fileName);
repositoryService.createDeployment().addInputStream(baseName + ".bpmn20.xml", fileInputStream).deploy();
} else {
throw new FlowableException("错误信息:不支改文件类型" + extension);
}
} catch (Exception e) {
log.error("错误信息:在部署过程中,文件输入流异常" + e.toString());
} finally {
try {
if (fileInputStream != null) {
fileInputStream.close();
}
} catch (Exception e) {
}
}
}
}
}
@@ -0,0 +1,476 @@
package org.jeecg.modules.bpm.controller;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.history.HistoricProcessInstanceQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.shiro.SecurityUtils;
import org.flowable.task.api.DelegationState;
import org.flowable.task.api.Task;
import org.jeecg.common.api.vo.Result;
import org.jeecg.common.system.api.ISysBaseAPI;
import org.jeecg.common.system.vo.LoginUser;
import org.jeecg.common.util.DateUtils;
import org.jeecg.common.util.oConvertUtils;
import org.jeecg.modules.bpm.dto.BatchSuspendInfo;
import org.jeecg.modules.bpm.dto.ProcessInstHisDTO;
import org.jeecg.modules.bpm.dto.SuspendInfo;
import org.jeecg.modules.bpm.service.ActivitiService;
import org.jeecg.modules.bpm.service.impl.SuperTaskService;
import org.jeecg.modules.extbpm.process.common.WorkFlowGlobals;
import org.jeecg.modules.extbpm.process.entity.ExtActFlowData;
import org.jeecg.modules.extbpm.process.service.IExtActFlowDataService;
import org.jeecg.modules.extbpm.util.CommonRandomUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
/**
* 流程实例管理功能
*
* @author scott
*/
@Slf4j
@RestController("actProcessInstanceController")
@RequestMapping("/act/processInstance")
public class ActProcessInstanceController {
private final String START_USER_ID = CommonRandomUtil.trim("startUserId");
private final String PROCESS_INSTANCE_ID = CommonRandomUtil.trim("processInstanceId");
@Autowired
protected RepositoryService repositoryService;
@Autowired
private HistoryService historyService;
@Autowired
private RuntimeService runtimeService;
@Autowired
private TaskService taskService;
@Autowired
private ActivitiService activitiService;
@Autowired
private SuperTaskService superTaskService;
@Autowired
private IExtActFlowDataService extActFlowDataService;
@Lazy
@Autowired
private ISysBaseAPI sysBaseAPI;
/**
* 运行中流程实例数据
*
* @param request
* @return
*/
@GetMapping(value = "/list")
public Result<IPage<ProcessInstHisDTO>> listRunning(HttpServletRequest request) {
Result<IPage<ProcessInstHisDTO>> result = new Result<IPage<ProcessInstHisDTO>>();
IPage<ProcessInstHisDTO> pageList = new Page<ProcessInstHisDTO>();
HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
if (CommonRandomUtil.isNotEmpty(request.getParameter(START_USER_ID))) {
historicProcessInstanceQuery = historicProcessInstanceQuery.startedBy(request.getParameter(START_USER_ID));
}
if (CommonRandomUtil.isNotEmpty(request.getParameter(PROCESS_INSTANCE_ID))) {
historicProcessInstanceQuery = historicProcessInstanceQuery.processInstanceId(request.getParameter(PROCESS_INSTANCE_ID));
}
String startTimeBegin = request.getParameter("startTime_begin");// 时间开始
String startTimeEnd = request.getParameter("startTime_end");// 时间结束
if (CommonRandomUtil.isNotEmpty(startTimeBegin)) {
try {
historicProcessInstanceQuery.startedAfter(DateUtils.parseDate(startTimeBegin, "yyyy-MM-dd"));
} catch (ParseException e) {
e.printStackTrace();
}
}
if (CommonRandomUtil.isNotEmpty(startTimeEnd)) {
try {
historicProcessInstanceQuery.startedBefore(DateUtils.parseDate(startTimeEnd, "yyyy-MM-dd"));
} catch (ParseException e) {
e.printStackTrace();
}
}
String endtimeBegin = request.getParameter("endTime_begin");// 时间开始
String endtimeEnd = request.getParameter("endTime_end");// 时间结束
if (CommonRandomUtil.isNotEmpty(endtimeBegin)) {
try {
historicProcessInstanceQuery.finishedAfter(DateUtils.parseDate(endtimeBegin, "yyyy-MM-dd"));
} catch (ParseException e) {
e.printStackTrace();
}
}
if (CommonRandomUtil.isNotEmpty(endtimeEnd)) {
try {
historicProcessInstanceQuery.finishedBefore(DateUtils.parseDate(endtimeEnd, "yyyy-MM-dd"));
} catch (ParseException e) {
e.printStackTrace();
}
}
//改造treegrid
// 分页参数
Integer pageNo = oConvertUtils.getInt(request.getParameter("pageNo"),1);
Integer pageSize = oConvertUtils.getInt(request.getParameter("pageSize"),10);
Integer start = (pageNo - 1) * pageSize;
//Integer end = page * rows - 1;
List<HistoricProcessInstance> list = historicProcessInstanceQuery.unfinished().orderByProcessInstanceStartTime().desc().listPage(start, pageSize);
long count = historicProcessInstanceQuery.unfinished().count();
List<ProcessInstHisDTO> records = new ArrayList<ProcessInstHisDTO>();
ProcessInstHisDTO processInstHisDTO = null;
for (HistoricProcessInstance hi : list) {
String starttime = DateFormatUtils.format(hi.getStartTime(), "yyyy-MM-dd HH:mm:ss");
String endtime = hi.getEndTime() == null ? "" : DateFormatUtils.format(hi.getEndTime(), "yyyy-MM-dd HH:mm:ss");
long totalTimes = hi.getEndTime() == null ? (Calendar.getInstance().getTimeInMillis() - hi.getStartTime().getTime()) : (hi.getEndTime().getTime() - hi.getStartTime().getTime());
long dayCount = totalTimes / (1000 * 60 * 60 * 24);// 计算天
long restTimes = totalTimes % (1000 * 60 * 60 * 24);// 剩下的时间用于计于小时
long hourCount = restTimes / (1000 * 60 * 60);// 小时
restTimes = restTimes % (1000 * 60 * 60);
long minuteCount = restTimes / (1000 * 60);
String spendTimes = dayCount + "" + hourCount + "小时" + minuteCount + "";
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery().processDefinitionId(hi.getProcessDefinitionId()).singleResult();
log.debug("spendTimes" + spendTimes);
String isSuspended = "finished";
String activityName = "";// 当前任务名称
String activityUser = "";// 当前任务签收人
String taskId = "";// 任务ID
if (hi.getEndTime() == null) {
// endtime为空表示流程实例未结束
// 增加业务标题
String bpmBizTitle = this.activitiService.getHisVarinst(WorkFlowGlobals.BPM_BIZ_TITLE, hi.getId());
bpmBizTitle = oConvertUtils.isEmpty(bpmBizTitle)?processDefinition.getName():bpmBizTitle;
ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(hi.getId()).singleResult();
List<Task> tasks = taskService.createTaskQuery().processInstanceId(hi.getId()).list();
if (tasks.size() > 1) {
// 针对会签或者一个流程多个分支任务(一个流程实例,同时存在多个任务情况),流程实例报错处理--------------------
isSuspended = "" + pi.isSuspended();
activityName = CommonRandomUtil.trimToEmpty(tasks.get(0).getName());
activityUser = CommonRandomUtil.trimToEmpty(tasks.get(0).getAssignee());
taskId = oConvertUtils.getString(tasks.get(0).getId());
processInstHisDTO = new ProcessInstHisDTO();
processInstHisDTO.setId(hi.getId());
processInstHisDTO.setBpmBizTitle(bpmBizTitle);
processInstHisDTO.setParentId("");
processInstHisDTO.setTaskId(taskId);
processInstHisDTO.setName(activityName);
processInstHisDTO.setAssignee("");
processInstHisDTO.setPrcocessDefinitionName(processDefinition.getName());
processInstHisDTO.setStartUserId(hi.getStartUserId());
if(oConvertUtils.isNotEmpty(processInstHisDTO.getStartUserId())) {
LoginUser sysUser = sysBaseAPI.getUserByName(processInstHisDTO.getStartUserId());
processInstHisDTO.setStartUserName(sysUser.getRealname());
}
processInstHisDTO.setStartTime(starttime);
processInstHisDTO.setEndTime(endtime);
processInstHisDTO.setSpendTimes(spendTimes);
processInstHisDTO.setIsSuspended(isSuspended);
processInstHisDTO.setProcessDefinitionId(hi.getProcessDefinitionId());
processInstHisDTO.setProcessInstanceId(hi.getId());
int i = 1;
List<ProcessInstHisDTO> childrens = new ArrayList<ProcessInstHisDTO>();
ProcessInstHisDTO children = null;
for (Task task : tasks) {
isSuspended = "" + pi.isSuspended();
activityName = CommonRandomUtil.trimToEmpty(task.getName());
activityUser = CommonRandomUtil.trimToEmpty(task.getAssignee());
taskId = oConvertUtils.getString(task.getId());
children = new ProcessInstHisDTO();
children.setId(hi.getId() + ":" + (i++));
children.setBpmBizTitle(bpmBizTitle);
children.setParentId(hi.getId());
children.setTaskId(taskId);
children.setName(activityName);
children.setAssignee(activityUser);
if(oConvertUtils.isNotEmpty(children.getAssignee())) {
LoginUser sysUser = sysBaseAPI.getUserByName(children.getAssignee());
children.setAssigneeName(sysUser!=null?sysUser.getRealname():children.getAssignee());
}
children.setPrcocessDefinitionName(processDefinition.getName());
children.setStartUserId(hi.getStartUserId());
if(oConvertUtils.isNotEmpty(children.getStartUserId())) {
LoginUser sysUser = sysBaseAPI.getUserByName(children.getStartUserId());
children.setStartUserName(sysUser.getRealname());
}
children.setStartTime(starttime);
children.setEndTime(endtime);
children.setSpendTimes(spendTimes);
children.setIsSuspended(isSuspended);
children.setProcessDefinitionId(hi.getProcessDefinitionId());
children.setProcessInstanceId(hi.getId());
childrens.add(children);
}
processInstHisDTO.setChildren(childrens);
records.add(processInstHisDTO);
} else {
for (Task task : tasks) {
isSuspended = "" + pi.isSuspended();
activityName = CommonRandomUtil.trimToEmpty(task.getName());
activityUser = CommonRandomUtil.trimToEmpty(task.getAssignee());
taskId = oConvertUtils.getString(task.getId());
processInstHisDTO = new ProcessInstHisDTO();
processInstHisDTO.setId(hi.getId());
processInstHisDTO.setBpmBizTitle(bpmBizTitle);
processInstHisDTO.setParentId("");
processInstHisDTO.setTaskId(taskId);
processInstHisDTO.setName(activityName);
// NY5LzSY2VW1BSthYSnJArCFqbgwtZqSuyPQ/OD1n1twWJGU2RN/wkzf+kBVO5DztN85Ca9keeuaRAiwcatr8N0M15+Wv2SmRw82lMwawE2naX5tpJMpkxrUhUUcjnC+BSBL4+PV2JUXBFW8/oOG8HLqYvmPxoP7MMBhMi9D7lRY=
processInstHisDTO.setAssignee(activityUser);
if(oConvertUtils.isNotEmpty(processInstHisDTO.getAssignee())) {
LoginUser sysUser = sysBaseAPI.getUserByName(processInstHisDTO.getAssignee());
processInstHisDTO.setAssigneeName(sysUser!=null?sysUser.getRealname():processInstHisDTO.getAssignee());
}
processInstHisDTO.setPrcocessDefinitionName(processDefinition.getName());
processInstHisDTO.setStartUserId(hi.getStartUserId());
if(oConvertUtils.isNotEmpty(processInstHisDTO.getStartUserId())) {
LoginUser sysUser = sysBaseAPI.getUserByName(processInstHisDTO.getStartUserId());
processInstHisDTO.setStartUserName(sysUser!=null?sysUser.getRealname():processInstHisDTO.getStartUserId());
}
processInstHisDTO.setStartTime(starttime);
processInstHisDTO.setEndTime(endtime);
processInstHisDTO.setSpendTimes(spendTimes);
processInstHisDTO.setIsSuspended(isSuspended);
processInstHisDTO.setProcessDefinitionId(hi.getProcessDefinitionId());
processInstHisDTO.setProcessInstanceId(hi.getId());
records.add(processInstHisDTO);
}
}
}
}
pageList.setRecords(records);
log.debug(" count : " + count);
pageList.setTotal(count);
result.setSuccess(true);
result.setResult(pageList);
return result;
}
/**
* 进行委派操作
* @param processInstanceId 流程部署ID
*/
@GetMapping(value = "reassign")
public Result reassign(@RequestParam("taskId") String taskId, @RequestParam("userName") String assignUserId,HttpServletRequest request) {
Task task = activitiService.getTask(taskId);
//去掉最后面的逗号
if(oConvertUtils.isNotEmpty(assignUserId) && assignUserId.endsWith(",")) {
assignUserId = assignUserId.substring(0, assignUserId.length() -1);
}
LoginUser sysUser = (LoginUser)SecurityUtils.getSubject().getPrincipal();
taskService.setOwner(task.getId(), sysUser.getUsername());
//TODO 委派给多个人好使吗?
taskService.setAssignee(task.getId(), assignUserId);
return Result.ok("委派成功!");
}
/**
* 暂停正在运行的流程实例
* @param processInstanceId 流程实例ID
*/
@GetMapping(value = "suspend")
public Result suspend(@RequestParam("processInstanceId") String processInstanceId, HttpServletRequest request) {
activitiService.suspend(processInstanceId);
return Result.ok("挂起成功!");
}
/**
* 启动暂停的流程实例
* @param processInstanceId 流程实例ID
*/
@GetMapping(value = "restart")
public Result restart(@RequestParam("processInstanceId") String processInstanceId, HttpServletRequest request) {
activitiService.restart(processInstanceId);
return Result.ok("启动成功!");
}
/**
* 暂停正在运行的流程实例(批量处理)
*/
@PostMapping(value = "batchSuspend")
public Result<BatchSuspendInfo> batchSuspend(@RequestBody BatchSuspendInfo batchSuspendInfo, HttpServletRequest request) {
Result<BatchSuspendInfo> result = new Result<BatchSuspendInfo>();
List<SuspendInfo> data = batchSuspendInfo.getData();
if(data==null||data.size()<=0) {
result.error500("请选择数据");
return result;
}
int sucessCount = 0;
for(SuspendInfo info :data) {
try {
LambdaQueryWrapper<ExtActFlowData> queryWrapper = new LambdaQueryWrapper<ExtActFlowData>();
queryWrapper.eq(ExtActFlowData::getFormDataId, info.getDataId());
queryWrapper.eq(ExtActFlowData::getRelationCode, info.getFlowCode());
ExtActFlowData extActFlowData = extActFlowDataService.getOne(queryWrapper);
if(extActFlowData!=null) {
info.setProcInstId(extActFlowData.getProcessInstId());
activitiService.suspend(extActFlowData.getProcessInstId());
info.setStatus("1");//处理成功
sucessCount++;
continue;
}
} catch (Exception e) {
log.error(e.getMessage(),e);
}
info.setStatus("0");//处理失败
}
batchSuspendInfo.setData(data);
batchSuspendInfo.setSucessCount(sucessCount);
result.setResult(batchSuspendInfo);
result.success("处理完成!");
return result;
}
/**
* 启动暂停的流程实例(批量处理)
*/
@PostMapping(value = "batchRestart")
public Result<BatchSuspendInfo> batchRestart(@RequestBody BatchSuspendInfo batchSuspendInfo, HttpServletRequest request) {
Result<BatchSuspendInfo> result = new Result<BatchSuspendInfo>();
List<SuspendInfo> data = batchSuspendInfo.getData();
if(data==null||data.size()<=0) {
result.error500("请选择数据");
return result;
}
int sucessCount = 0;
for(SuspendInfo info :data) {
try {
LambdaQueryWrapper<ExtActFlowData> queryWrapper = new LambdaQueryWrapper<ExtActFlowData>();
queryWrapper.eq(ExtActFlowData::getFormDataId, info.getDataId());
queryWrapper.eq(ExtActFlowData::getRelationCode, info.getFlowCode());
ExtActFlowData extActFlowData = extActFlowDataService.getOne(queryWrapper);
if(extActFlowData!=null) {
info.setProcInstId(extActFlowData.getProcessInstId());
activitiService.restart(extActFlowData.getProcessInstId());
info.setStatus("1");//处理成功
sucessCount++;
continue;
}
} catch (Exception e) {
log.error(e.getMessage(),e);
}
info.setStatus("0");//处理失败
}
batchSuspendInfo.setData(data);
batchSuspendInfo.setSucessCount(sucessCount);
result.setResult(batchSuspendInfo);
result.success("处理完成!");
return result;
}
/**
* 作废运行中的流程实例
* @param processInstanceId 流程实例ID
*/
@GetMapping(value = "close")
public Result close(@RequestParam("processInstanceId") String processInstanceId, HttpServletRequest request) {
//JTC-853 流程实例中被关闭,提示作废成功,但是我的工单列表显示状态是 完成
runtimeService.setVariable(processInstanceId, WorkFlowGlobals.BPM_STATUS, WorkFlowGlobals.PROCESS_INVALIDPROCESS_STATUS);
runtimeService.deleteProcessInstance(processInstanceId, "作废流程");
return Result.ok("作废成功!");
}
/**
* 跳转到某个节点
* @param taskId 当前节点
* @param skipNode 跳转到那个节点
* @param request
* @return
* @throws Exception
*/
@GetMapping(value = "skipNode")
public Result skipNode(@RequestParam("taskId") String taskId, @RequestParam("skipTaskNode") String skipNode,HttpServletRequest request) throws Exception {
//TODO 流程变量参数
//Map<String, Object> map = var.getVariableMap(processHandle.getTpProcesspros());
//判断是否是委托任务,被委托人先解决委托
Task task=taskService.createTaskQuery().taskId(taskId).active().singleResult();
if (org.apache.commons.lang.StringUtil.isNotBlank(task.getOwner())) {
DelegationState delegationState = task.getDelegationState();
switch (delegationState) {
case PENDING:
taskService.resolveTask(taskId);
break;
case RESOLVED:
//委托任务已经完成
break;
default:
//不是委托任务
break;
}
}
if("end".equals(skipNode)){
superTaskService.endProcess(taskId);
}else{
superTaskService.goProcessTaskNode(taskId,skipNode);
}
return Result.ok("跳转成功!");
}
/**
* 通过当前taskId获取所有的流程任务节点
* @param taskId 当前节点
* @param request
* @return
* @throws Exception
*/
@GetMapping(value = "getAllTask")
public Result getAllTask(@RequestParam("taskId") String taskId,HttpServletRequest request) throws Exception {
List<Map> ls = activitiService.getAllTaskNode(taskId);
List<Map> list = new ArrayList<Map>();
for(Map map :ls) {
String taskKey = (String)map.get("taskKey");
String name = (String)map.get("name");
//author:scott date:20220118 for:JTC-1044【工作流】流程实例 跳转 有空节点
if(taskKey==null
||name == null
||taskKey.startsWith("exclusiveGateway")
||taskKey.startsWith("parallelGateway")
||taskKey.startsWith("callSubProcess")
||taskKey.startsWith("Gateway_")
||taskKey.startsWith("Start_")
||taskKey.startsWith("End_")
||taskKey.startsWith("Start")
||taskKey.startsWith("end")
||taskKey.startsWith("start1")) {
continue;
}
list.add(map);
}
return Result.ok(list);
}
}
@@ -0,0 +1 @@
package org.jeecg.modules.bpm.dto;import java.util.Date;import lombok.Data;@Datapublic class ActHiActinstDTO implements java.io.Serializable { private static final long serialVersionUID = 1L; /**␍ *␍ */ private String id; /**␍ *␍ */ private String procDefId; /**␍ *␍ */ private String procInstId; /**␍ *␍ */ private String executionId; /**␍ *␍ */ private String actId; /**␍ *␍ */ private String taskId; /**␍ *␍ */ private String callProcInstId; /**␍ *␍ */ private String actName; /**␍ *␍ */ private String actType; /**␍ *␍ */ private String assignee; /**␍ *␍ */ private Date startTime; /**␍ *␍ */ private Date endTime; /**␍ *␍ */ private Long duration; /**␍ *␍ */ private String tenantId; }
@@ -0,0 +1,22 @@
package org.jeecg.modules.bpm.dto;
import lombok.Data;
import java.util.Date;
/**
* @Description: 运行时超时任务
* @author: scott
* @date: 2020年04月21日 19:59
*/
@Data
public class ActRuTaskDTO {
private Date createTime;
private String id;
private String name;
private String assignee;
private String owner;
private String procInstId;
private String executionId;
private Integer nodeTimeout;
}

Some files were not shown because too many files have changed in this diff Show More