feat: 将项目改为Starter的结构, 为后续开发做扩展准备
This commit is contained in:
60
metis-starter/pom.xml
Normal file
60
metis-starter/pom.xml
Normal file
@@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>com.metis</groupId>
|
||||
<artifactId>metis</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>metis-starter</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
|
||||
|
||||
<build>
|
||||
<!-- 默认生效的插件 -->
|
||||
<plugins>
|
||||
<!-- 编译插件 -->
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
<compilerArgs>
|
||||
<arg>-parameters</arg>
|
||||
</compilerArgs>
|
||||
<!-- 注解静态编译功能 注:仅支持 maven-compiler-plugin 的 version 在3.6.0 以上才生效 -->
|
||||
<annotationProcessorPaths>
|
||||
<!-- 必须配置 lombok 的注解编译,否则会因为配置了(mapstruct-processor)启动了导致 lombok 对内部类的静态编译失效 -->
|
||||
<path>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.34</version>
|
||||
</path>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok-mapstruct-binding</artifactId>
|
||||
<version>0.2.0</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.metis.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.ComponentScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@MapperScan(basePackages = {"com.metis.flow.mapper"})
|
||||
@ComponentScan("com.metis.*")
|
||||
public class MetisStarterAutoConfiguration {
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.metis.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import lombok.Data;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/30
|
||||
*/
|
||||
@Data
|
||||
public class SimpleBaseEntity implements Serializable {
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime createTime;
|
||||
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
|
||||
/**
|
||||
* 逻辑删除字段
|
||||
*/
|
||||
@TableLogic(value = "0", delval = "1")
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Integer isDeleted;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.metis.domain.bo;
|
||||
|
||||
import com.metis.enums.YesOrNoEnum;
|
||||
import com.metis.flow.domain.bo.GraphBO;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ProcessBo {
|
||||
|
||||
private Long appId;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
private GraphBO graph;
|
||||
|
||||
private YesOrNoEnum defaultUse;
|
||||
|
||||
}
|
||||
59
metis-starter/src/main/java/com/metis/enums/BaseEnum.java
Normal file
59
metis-starter/src/main/java/com/metis/enums/BaseEnum.java
Normal file
@@ -0,0 +1,59 @@
|
||||
package com.metis.enums;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IEnum;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public interface BaseEnum<T extends Enum<T>> extends IEnum<Integer> {
|
||||
|
||||
/**
|
||||
* 获取编码
|
||||
*
|
||||
* @return {@link Integer}
|
||||
*/
|
||||
Integer getCode();
|
||||
|
||||
/**
|
||||
* 获取名称
|
||||
*
|
||||
* @return {@link String}
|
||||
*/
|
||||
String getName();
|
||||
|
||||
/**
|
||||
* 获取value值
|
||||
*
|
||||
* @return {@link Integer}
|
||||
*/
|
||||
@Override
|
||||
default Integer getValue() {
|
||||
return getCode();
|
||||
}
|
||||
/**
|
||||
* 解析通过编码
|
||||
*
|
||||
* @param enumClass 枚举班
|
||||
* @param code 编码
|
||||
* @return {@link E}
|
||||
*/
|
||||
static <E extends Enum<E> & BaseEnum<E>> E parseByCode(Class<E> enumClass, Integer code) {
|
||||
return Arrays.stream(enumClass.getEnumConstants())
|
||||
.filter(e -> e.getCode().equals(code))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按名称解析
|
||||
*
|
||||
* @param enumClass 枚举班
|
||||
* @param name 名称
|
||||
* @return {@link E}
|
||||
*/
|
||||
static <E extends Enum<E> & BaseEnum<E>> E parseByName(Class<E> enumClass, String name) {
|
||||
return Arrays.stream(enumClass.getEnumConstants())
|
||||
.filter(e -> e.getName().equals(name))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
}
|
||||
47
metis-starter/src/main/java/com/metis/enums/ResultEnum.java
Normal file
47
metis-starter/src/main/java/com/metis/enums/ResultEnum.java
Normal file
@@ -0,0 +1,47 @@
|
||||
package com.metis.enums;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2023-05-10
|
||||
*/
|
||||
public enum ResultEnum {
|
||||
/**
|
||||
* 返回状态枚举
|
||||
*/
|
||||
SUCCESS(1000, "操作成功", HttpStatus.OK),
|
||||
|
||||
NO_DATA(1001, "查询结果为空", HttpStatus.OK),
|
||||
|
||||
RESUBMIT_LOCK(2002, "重复提交", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
|
||||
ERROR(2000, "操作失败", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
|
||||
SYS_ERROR(2001, "系统异常", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
|
||||
SENTINEL_FLOW(3000, "限流了", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
|
||||
SENTINEL_PARAM_FLOW(3000, "热点参数限流", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
|
||||
SENTINEL_SYSTEM(3000, "系统规则负载等不满足要求", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
|
||||
SENTINEL_AUTHORITY(3000, "授权规则不通过", HttpStatus.UNAUTHORIZED),
|
||||
|
||||
SENTINEL_DEGRADE(3000, "降级了", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||
;
|
||||
|
||||
ResultEnum(int code, String msg, HttpStatus status) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public final int code;
|
||||
|
||||
public final String msg;
|
||||
|
||||
public final transient HttpStatus status;
|
||||
|
||||
|
||||
}
|
||||
52
metis-starter/src/main/java/com/metis/enums/YesOrNoEnum.java
Normal file
52
metis-starter/src/main/java/com/metis/enums/YesOrNoEnum.java
Normal file
@@ -0,0 +1,52 @@
|
||||
package com.metis.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 基础状态枚举
|
||||
*
|
||||
* @author ZhangQiang
|
||||
* @date 2024/10/22
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum YesOrNoEnum implements BaseEnum<YesOrNoEnum> {
|
||||
|
||||
/**
|
||||
* 基础状态枚举
|
||||
*/
|
||||
YES(1, "是"),
|
||||
NO(0, "否"),
|
||||
;
|
||||
|
||||
@JsonValue
|
||||
private final Integer code;
|
||||
private final String name;
|
||||
|
||||
/**
|
||||
* 根据 code 转换枚举
|
||||
*
|
||||
* @param code 编码
|
||||
* @return 枚举
|
||||
*/
|
||||
public static Optional<YesOrNoEnum> of(Integer code) {
|
||||
return Optional.ofNullable(BaseEnum.parseByCode(YesOrNoEnum.class, code));
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举序列化器(前端传code时自动转换为对应枚举)
|
||||
*
|
||||
* @param code 编码
|
||||
* @return 枚举
|
||||
*/
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static YesOrNoEnum get(Integer code) {
|
||||
return BaseEnum.parseByCode(YesOrNoEnum.class, code);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.metis.facade;
|
||||
|
||||
import com.metis.domain.bo.ProcessBo;
|
||||
import com.metis.flow.convert.GraphConvert;
|
||||
import com.metis.flow.domain.bo.CreateApp;
|
||||
import com.metis.flow.domain.bo.UpdateApp;
|
||||
import com.metis.flow.domain.entity.App;
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import com.metis.flow.engine.AppEngineService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ProcessDefinitionFacade {
|
||||
|
||||
private final AppEngineService appEngineService;
|
||||
|
||||
/**
|
||||
* 创建
|
||||
*
|
||||
* @param processBo 过程业务对象
|
||||
*/
|
||||
public Long create(ProcessBo processBo) {
|
||||
Graph graph = GraphConvert.INSTANCE.toEntity(processBo.getGraph());
|
||||
CreateApp createApp = CreateApp.builder()
|
||||
.name(processBo.getName())
|
||||
.graph(graph)
|
||||
.build();
|
||||
App app = appEngineService.create(createApp);
|
||||
return app.getWorkflowId();
|
||||
}
|
||||
|
||||
public App getByDeploymentId(Long deploymentId) {
|
||||
return appEngineService.getByWorkflowId(deploymentId);
|
||||
}
|
||||
|
||||
public void update(ProcessBo processBo) {
|
||||
Graph graph = GraphConvert.INSTANCE.toEntity(processBo.getGraph());
|
||||
appEngineService.update(UpdateApp.builder()
|
||||
.defaultUse(processBo.getDefaultUse())
|
||||
.appId(processBo.getAppId())
|
||||
.name(processBo.getName())
|
||||
.graph(graph)
|
||||
.build());
|
||||
|
||||
}
|
||||
|
||||
public void delete(Long appId) {
|
||||
appEngineService.delete(appId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.metis.flow.constant;
|
||||
|
||||
public interface BaseConstant {
|
||||
|
||||
Integer DEFAULT_VERSION = 0;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.metis.flow.convert;
|
||||
|
||||
|
||||
import com.metis.flow.domain.bo.GraphBO;
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import org.mapstruct.Mapper;
|
||||
import org.mapstruct.factory.Mappers;
|
||||
|
||||
@Mapper
|
||||
public interface GraphConvert {
|
||||
|
||||
GraphConvert INSTANCE = Mappers.getMapper(GraphConvert.class);
|
||||
|
||||
|
||||
Graph toEntity(GraphBO graph);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public final class BuildApp {
|
||||
|
||||
private Long appId;
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
@NotBlank(message = "流程名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "流程模型不能为空")
|
||||
@Valid
|
||||
private Graph graph;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class CreateApp {
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
@NotBlank(message = "流程名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "流程模型不能为空")
|
||||
@Valid
|
||||
private Graph graph;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import com.metis.flow.enums.EdgeType;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class EdgeBO {
|
||||
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
@NotNull(message = "唯一标识符不能为空")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 节点类型
|
||||
*/
|
||||
@NotNull(message = "线类型不能为空")
|
||||
private EdgeType type;
|
||||
|
||||
/**
|
||||
* 源节点ID,对应节点id
|
||||
*/
|
||||
@NotNull(message = "源节点ID不能为空")
|
||||
private Long source;
|
||||
|
||||
/**
|
||||
* 目标节点ID,对应节点id
|
||||
*/
|
||||
@NotNull(message = "目标节点ID不能为空")
|
||||
private Long target;
|
||||
/**
|
||||
* 源句柄id
|
||||
*/
|
||||
@NotNull(message = "源句柄ID不能为空")
|
||||
private Long sourceHandle;
|
||||
|
||||
/**
|
||||
* 目标句柄id
|
||||
*/
|
||||
@NotNull(message = "目标句柄ID不能为空")
|
||||
private Long targetHandle;
|
||||
|
||||
/**
|
||||
* 边是否动画true/false
|
||||
*/
|
||||
private Boolean animated;
|
||||
|
||||
/**
|
||||
* 开始标志
|
||||
*/
|
||||
private String markerStart;
|
||||
|
||||
/**
|
||||
* 结束标记
|
||||
*/
|
||||
private String markerEnd;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class GraphBO {
|
||||
|
||||
/**
|
||||
* 边缘
|
||||
*/
|
||||
@Valid
|
||||
@NotEmpty(message = "连线不能为空")
|
||||
private List<EdgeBO> edges;
|
||||
|
||||
/**
|
||||
* 节点
|
||||
*/
|
||||
@Valid
|
||||
@NotEmpty(message = "节点不能为空")
|
||||
private List<NodeBO> nodes;
|
||||
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
private List<Double> position;
|
||||
|
||||
/**
|
||||
* 变焦
|
||||
*/
|
||||
private Double zoom;
|
||||
|
||||
/**
|
||||
* 视窗
|
||||
*/
|
||||
private ViewportBo viewport;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import com.metis.flow.enums.HandleType;
|
||||
import com.metis.flow.enums.PositionType;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 句柄对象
|
||||
*/
|
||||
@Data
|
||||
public class HandleBO {
|
||||
/**
|
||||
* 句柄id
|
||||
*/
|
||||
@NotNull(message = "句柄id不能为空")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 句柄类型
|
||||
*/
|
||||
@NotNull(message = "句柄类型不能为空")
|
||||
private HandleType type;
|
||||
|
||||
/**
|
||||
* 句柄位置
|
||||
*/
|
||||
@NotNull(message = "句柄位置不能为空")
|
||||
private PositionType position;
|
||||
|
||||
/**
|
||||
* 是否可以连接
|
||||
*/
|
||||
@NotNull(message = "是否可以连接不能为空")
|
||||
private Boolean connectable;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class NodeBO {
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@NotNull(message = "节点id不能为空")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@NotNull(message = "节点类型不能为空")
|
||||
private NodeType type;
|
||||
|
||||
/**
|
||||
* 自定义类型
|
||||
*/
|
||||
private String customType;
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "节点位置不能为空")
|
||||
private PositionBO position;
|
||||
|
||||
/**
|
||||
* 业务数据
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "节点业务数据不能为空")
|
||||
private NodeDataBO data;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
// @NotNull(message = "节点宽度不能为空")
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
// @NotNull(message = "节点高度不能为空")
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 节点是否选中
|
||||
*/
|
||||
private Boolean selected;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.metis.flow.enums.PositionType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class NodeDataBO {
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@NotBlank(message = "标签不能为空")
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 工具栏位置
|
||||
*/
|
||||
private PositionType toolbarPosition;
|
||||
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
private JSONObject config;
|
||||
|
||||
/**
|
||||
* 句柄列表
|
||||
*/
|
||||
@Valid
|
||||
@NotEmpty(message = "句柄列表不能为空")
|
||||
private List<HandleBO> handles;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class PositionBO {
|
||||
/**
|
||||
* x坐标
|
||||
*/
|
||||
@NotNull(message = "x坐标不能为空")
|
||||
private Double x;
|
||||
|
||||
/**
|
||||
* y坐标
|
||||
*/
|
||||
@NotNull(message = "y坐标不能为空")
|
||||
private Double y;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import com.metis.enums.YesOrNoEnum;
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class UpdateApp {
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 名字
|
||||
*/
|
||||
@NotBlank(message = "流程名称不能为空")
|
||||
private String name;
|
||||
|
||||
@NotNull(message = "流程模型不能为空")
|
||||
@Valid
|
||||
private Graph graph;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
private YesOrNoEnum defaultUse;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.metis.flow.domain.bo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ViewportBo {
|
||||
private Double x;
|
||||
private Double y;
|
||||
private Double zoom;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.metis.flow.domain.context;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.metis.flow.runner.FlowRunningContext;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
@Getter
|
||||
@Builder
|
||||
public class RunningContext {
|
||||
|
||||
/**
|
||||
* 系统数据
|
||||
*/
|
||||
private SysContext sys;
|
||||
|
||||
/**
|
||||
* 自定义数据
|
||||
*/
|
||||
private JSONObject custom;
|
||||
|
||||
/**
|
||||
* 节点运行上下文, 需要数据进行传递
|
||||
*/
|
||||
private Map<Long, JSONObject> nodeRunningContext;
|
||||
|
||||
/**
|
||||
* 下一个运行节点id集合, 可能是多个, 执行器每一次清空该节点
|
||||
*/
|
||||
private Set<Long> nextRunNodeId;
|
||||
|
||||
|
||||
/**
|
||||
* 添加节点运行环境
|
||||
*
|
||||
* @param nodeId 节点id
|
||||
* @param nodeRunningContext 节点运行背景信息
|
||||
*/
|
||||
public void addNodeRunningContext(Long nodeId, JSONObject nodeRunningContext) {
|
||||
this.nodeRunningContext.put(nodeId, nodeRunningContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建上下文
|
||||
*
|
||||
* @param context 上下文
|
||||
* @return {@link RunningContext }
|
||||
*/
|
||||
public static RunningContext buildContext(SysContext sysContext, FlowRunningContext context) {
|
||||
return RunningContext.builder()
|
||||
.sys(sysContext)
|
||||
.custom(context.getCustom())
|
||||
.nodeRunningContext(new HashMap<>())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.metis.flow.domain.context;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class RunningResult {
|
||||
|
||||
/**
|
||||
* 节点上下文
|
||||
*/
|
||||
private JSONObject nodeContext;
|
||||
|
||||
/**
|
||||
* 下一个运行节点id, 一些特殊节点需要, 必须条件节点满足后, 才会运行下一个节点
|
||||
*/
|
||||
private Set<Long> nextRunNodeId;
|
||||
|
||||
|
||||
/**
|
||||
* 构建结果
|
||||
*
|
||||
* @param nodeContext 节点上下文
|
||||
* @param nextRunNodeId 下一个运行节点id
|
||||
* @return {@link RunningResult }
|
||||
*/
|
||||
public static RunningResult buildResult(JSONObject nodeContext, Set<Long> nextRunNodeId) {
|
||||
return RunningResult.builder()
|
||||
.nodeContext(nodeContext)
|
||||
.nextRunNodeId(nextRunNodeId)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建结果
|
||||
*
|
||||
* @param nodeContext 节点上下文
|
||||
* @return {@link RunningResult }
|
||||
*/
|
||||
public static RunningResult buildResult(JSONObject nodeContext) {
|
||||
return RunningResult.builder()
|
||||
.nodeContext(nodeContext)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static RunningResult buildResult() {
|
||||
return RunningResult.builder()
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.metis.flow.domain.context;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class SysContext {
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private List<String> files;
|
||||
|
||||
/**
|
||||
* 沟通的内容
|
||||
*/
|
||||
private String query;
|
||||
|
||||
/**
|
||||
* 对话数
|
||||
*/
|
||||
private Integer dialogueCount;
|
||||
|
||||
/**
|
||||
* 沟通的id
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 应用程序id
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 工作流id
|
||||
*/
|
||||
private Long workflowId;
|
||||
|
||||
/**
|
||||
* 实例id
|
||||
*/
|
||||
private Long instanceId;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package com.metis.flow.domain.entity;
|
||||
|
||||
|
||||
import com.metis.enums.YesOrNoEnum;
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class App {
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 部署id
|
||||
*/
|
||||
private Long workflowId;
|
||||
|
||||
/**
|
||||
* 名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 图
|
||||
*/
|
||||
private Graph graph;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 版本
|
||||
*/
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* 默认使用
|
||||
*/
|
||||
private YesOrNoEnum defaultUse;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.metis.flow.domain.entity;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.metis.domain.SimpleBaseEntity;
|
||||
import com.metis.enums.YesOrNoEnum;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 基础应用
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/05
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName(value = "base_app")
|
||||
public class BaseApp extends SimpleBaseEntity {
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 应用程序id
|
||||
*/
|
||||
@TableField(value = "app_id")
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* app名称
|
||||
*/
|
||||
@TableField(value = "name")
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@TableField(value = "description")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 图json数据
|
||||
*/
|
||||
@TableField(value = "graph_json")
|
||||
private String graphJson;
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
@TableField(value = "version")
|
||||
private Integer version;
|
||||
|
||||
/**
|
||||
* 创建人id
|
||||
*/
|
||||
@TableField(value = "create_user_id")
|
||||
private Long createUserId;
|
||||
|
||||
/**
|
||||
* 默认使用
|
||||
*/
|
||||
private YesOrNoEnum defaultUse;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.metis.flow.domain.entity;
|
||||
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class GraphDemo {
|
||||
private Map<Long, Node> nodes = new HashMap<>();
|
||||
private Map<Long, List<Long>> adjacencyList = new HashMap<>();
|
||||
|
||||
public void addNode(Node node) {
|
||||
nodes.put(node.getId(), node);
|
||||
adjacencyList.put(node.getId(), new ArrayList<>());
|
||||
}
|
||||
|
||||
public void addEdge(Edge edge) {
|
||||
adjacencyList.get(edge.getSource())
|
||||
.add(edge.getTarget());
|
||||
}
|
||||
|
||||
public List<Node> topologicalSort() {
|
||||
List<Node> sortedNodes = new ArrayList<>();
|
||||
Set<Long> visited = new HashSet<>();
|
||||
Set<Long> visiting = new HashSet<>();
|
||||
|
||||
for (Long nodeId : nodes.keySet()) {
|
||||
if (!visited.contains(nodeId)) {
|
||||
dfs(nodeId, visited, visiting, sortedNodes);
|
||||
}
|
||||
}
|
||||
|
||||
Collections.reverse(sortedNodes);
|
||||
return sortedNodes;
|
||||
}
|
||||
|
||||
private void dfs(Long nodeId, Set<Long> visited, Set<Long> visiting, List<Node> sortedNodes) {
|
||||
if (visiting.contains(nodeId)) {
|
||||
throw new IllegalStateException("Cycle detected in the graph");
|
||||
}
|
||||
|
||||
if (!visited.contains(nodeId)) {
|
||||
visiting.add(nodeId);
|
||||
for (Long neighbor : adjacencyList.get(nodeId)) {
|
||||
dfs(neighbor, visited, visiting, sortedNodes);
|
||||
}
|
||||
visiting.remove(nodeId);
|
||||
visited.add(nodeId);
|
||||
sortedNodes.add(nodes.get(nodeId));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import com.metis.flow.enums.EdgeType;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Edge {
|
||||
|
||||
/**
|
||||
* 唯一标识符
|
||||
*/
|
||||
@NotNull(message = "唯一标识符不能为空")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 节点类型
|
||||
*/
|
||||
@NotNull(message = "线类型不能为空")
|
||||
private EdgeType type;
|
||||
|
||||
/**
|
||||
* 源节点ID,对应节点id
|
||||
*/
|
||||
@NotNull(message = "源节点ID不能为空")
|
||||
private Long source;
|
||||
|
||||
/**
|
||||
* 目标节点ID,对应节点id
|
||||
*/
|
||||
@NotNull(message = "目标节点ID不能为空")
|
||||
private Long target;
|
||||
/**
|
||||
* 源句柄id
|
||||
*/
|
||||
@NotNull(message = "源句柄ID不能为空")
|
||||
private Long sourceHandle;
|
||||
|
||||
/**
|
||||
* 目标句柄id
|
||||
*/
|
||||
@NotNull(message = "目标句柄ID不能为空")
|
||||
private Long targetHandle;
|
||||
|
||||
/**
|
||||
* 边是否动画true/false
|
||||
*/
|
||||
private Boolean animated;
|
||||
|
||||
/**
|
||||
* 开始标志
|
||||
*/
|
||||
private String markerStart;
|
||||
|
||||
/**
|
||||
* 结束标记
|
||||
*/
|
||||
private String markerEnd;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class Graph {
|
||||
|
||||
/**
|
||||
* 边缘
|
||||
*/
|
||||
@Valid
|
||||
@NotEmpty(message = "连线不能为空")
|
||||
private List<Edge> edges;
|
||||
|
||||
/**
|
||||
* 节点
|
||||
*/
|
||||
@Valid
|
||||
@NotEmpty(message = "节点不能为空")
|
||||
private List<Node> nodes;
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
private List<Double> position;
|
||||
|
||||
/**
|
||||
* 变焦
|
||||
*/
|
||||
private Double zoom;
|
||||
|
||||
/**
|
||||
* 视窗
|
||||
*/
|
||||
private Viewport viewport;
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import com.metis.flow.enums.HandleType;
|
||||
import com.metis.flow.enums.PositionType;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 句柄对象
|
||||
*/
|
||||
@Data
|
||||
public class Handle {
|
||||
/**
|
||||
* 句柄id
|
||||
*/
|
||||
@NotNull(message = "句柄id不能为空")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 句柄类型
|
||||
*/
|
||||
@NotNull(message = "句柄类型不能为空")
|
||||
private HandleType type;
|
||||
|
||||
/**
|
||||
* 句柄位置
|
||||
*/
|
||||
@NotNull(message = "句柄位置不能为空")
|
||||
private PositionType position;
|
||||
|
||||
/**
|
||||
* 是否可以连接
|
||||
*/
|
||||
@NotNull(message = "是否可以连接不能为空")
|
||||
private Boolean connectable;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Node {
|
||||
|
||||
/**
|
||||
* id
|
||||
*/
|
||||
@NotNull(message = "节点id不能为空")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@NotNull(message = "节点类型不能为空")
|
||||
private NodeType type;
|
||||
|
||||
private String customType;
|
||||
|
||||
/**
|
||||
* 位置
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "节点位置不能为空")
|
||||
private Position position;
|
||||
|
||||
/**
|
||||
* 业务数据
|
||||
*/
|
||||
@Valid
|
||||
@NotNull(message = "节点业务数据不能为空")
|
||||
private NodeData data;
|
||||
|
||||
/**
|
||||
* 宽度
|
||||
*/
|
||||
// @NotNull(message = "节点宽度不能为空")
|
||||
private Integer width;
|
||||
|
||||
/**
|
||||
* 高度
|
||||
*/
|
||||
// @NotNull(message = "节点高度不能为空")
|
||||
private Integer height;
|
||||
|
||||
/**
|
||||
* 节点是否选中
|
||||
*/
|
||||
private Boolean selected;
|
||||
|
||||
|
||||
private Class<?> configClass;
|
||||
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*
|
||||
* @return {@link T }
|
||||
*/
|
||||
public <T> T getConfig() {
|
||||
if (ObjectUtil.isNull(data.getConfig())) {
|
||||
return null;
|
||||
}
|
||||
return (T) data.getConfig().to(configClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置配置类
|
||||
*
|
||||
* @param configClass 配置类
|
||||
*/
|
||||
public <T> void setConfigClass(Class<T> configClass) {
|
||||
if (ObjectUtil.isNotNull(this.configClass)) {
|
||||
return;
|
||||
}
|
||||
this.configClass = configClass;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
public abstract class NodeConfig {
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.metis.flow.enums.PositionType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class NodeData {
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
@NotBlank(message = "标签不能为空")
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 图标
|
||||
*/
|
||||
private String icon;
|
||||
|
||||
/**
|
||||
* 工具栏位置
|
||||
*/
|
||||
private PositionType toolbarPosition;
|
||||
|
||||
|
||||
/**
|
||||
* 配置
|
||||
*/
|
||||
private JSONObject config;
|
||||
|
||||
/**
|
||||
* 句柄列表
|
||||
*/
|
||||
@Valid
|
||||
@NotEmpty(message = "句柄列表不能为空")
|
||||
private List<Handle> handles;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.metis.flow.enums.FileUploadType;
|
||||
import com.metis.flow.enums.NodeVariableType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class NodeVariable {
|
||||
|
||||
/**
|
||||
* 参数字段
|
||||
*/
|
||||
@NotBlank(message = "参数字段不能为空")
|
||||
private String variable;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 最大长度
|
||||
*/
|
||||
private Integer maxLength;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
@NotNull(message = "类型不能为空")
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 是否必填
|
||||
*/
|
||||
@NotNull(message = "是否必填不能为空")
|
||||
private Boolean required;
|
||||
|
||||
/**
|
||||
* 选项
|
||||
*/
|
||||
@Valid
|
||||
private List<VariableOption> options;
|
||||
|
||||
/**
|
||||
* 允许上传方式
|
||||
*/
|
||||
private List<FileUploadType> allowedFileUploadMethods;
|
||||
|
||||
/**
|
||||
* 允许文件类型
|
||||
*/
|
||||
private List<String> allowedFileTypes;
|
||||
|
||||
/**
|
||||
* 允许文件扩展名
|
||||
*/
|
||||
private List<String> allowedFileExtensions;
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public Object getValue(JSONObject custom) {
|
||||
Object serializable = getSerializable(custom);
|
||||
Assert.isTrue(!(ObjectUtil.isNull(serializable) && ObjectUtil.isNotNull(required) && required), "参数字段 {} 的值不能为空", variable);
|
||||
return serializable;
|
||||
}
|
||||
|
||||
private Object getSerializable(JSONObject custom) {
|
||||
switch (getVariableType()) {
|
||||
case TEXT_INPUT, PARAGRAPH, SELECT, FILE -> {
|
||||
return custom.getString(variable);
|
||||
}
|
||||
case NUMBER -> {
|
||||
return custom.getInteger(variable);
|
||||
}
|
||||
case FILE_LIST -> {
|
||||
return custom.getList(variable, String.class);
|
||||
}
|
||||
default -> throw new RuntimeException("不支持的类型");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@JsonIgnore
|
||||
public NodeVariableType getVariableType() {
|
||||
return NodeVariableType.get(type);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Position {
|
||||
/**
|
||||
* x坐标
|
||||
*/
|
||||
@NotNull(message = "x坐标不能为空")
|
||||
private Double x;
|
||||
|
||||
/**
|
||||
* y坐标
|
||||
*/
|
||||
@NotNull(message = "y坐标不能为空")
|
||||
private Double y;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class VariableOption {
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private String label;
|
||||
|
||||
/**
|
||||
* 值
|
||||
*/
|
||||
@NotNull(message = "值不能为空")
|
||||
private String value;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.metis.flow.domain.entity.base;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class Viewport {
|
||||
private Double x;
|
||||
private Double y;
|
||||
private Double zoom;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.metis.flow.domain.entity.config.node;
|
||||
|
||||
import com.metis.flow.domain.entity.base.NodeConfig;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DocumentExtractorNodeConfig extends NodeConfig {
|
||||
|
||||
@NotBlank(message = "文件类型不能为空")
|
||||
private String fileType;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.metis.flow.domain.entity.config.node;
|
||||
|
||||
import com.metis.flow.domain.entity.base.NodeConfig;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class EndNodeConfig extends NodeConfig {
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.metis.flow.domain.entity.config.node;
|
||||
|
||||
import com.metis.flow.domain.entity.base.NodeConfig;
|
||||
import com.metis.flow.domain.entity.base.NodeVariable;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class StartNodeConfig extends NodeConfig {
|
||||
|
||||
@Valid
|
||||
private List<NodeVariable> variables;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package com.metis.flow.domain.entity.config;
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.metis.flow.domain.query;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
public class AppQuery {
|
||||
|
||||
private Long appId;
|
||||
|
||||
private String name;
|
||||
|
||||
private String description;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.metis.flow.engine;
|
||||
|
||||
import com.metis.flow.domain.query.AppQuery;
|
||||
import com.metis.flow.domain.entity.App;
|
||||
import com.metis.flow.domain.bo.CreateApp;
|
||||
import com.metis.flow.domain.bo.UpdateApp;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface AppEngineService {
|
||||
|
||||
/**
|
||||
* 列表页面
|
||||
*
|
||||
* @param query 查询
|
||||
* @return {@link List }<{@link App }>
|
||||
*/
|
||||
List<App> listPage(AppQuery query);
|
||||
|
||||
/**
|
||||
* 列表
|
||||
*
|
||||
* @param query 查询
|
||||
* @return {@link List }<{@link App }>
|
||||
*/
|
||||
List<App> list(AppQuery query);
|
||||
|
||||
|
||||
/**
|
||||
* 通过应用id获取
|
||||
*
|
||||
* @param appId 应用程序id
|
||||
* @return {@link App }
|
||||
*/
|
||||
App getByAppId(Long appId);
|
||||
|
||||
/**
|
||||
* 按身份证领取
|
||||
*
|
||||
* @param appId id
|
||||
* @return {@link App }
|
||||
*/
|
||||
App getByAppId(Long appId, Integer version);
|
||||
|
||||
/**
|
||||
* 通过部署id获取
|
||||
*
|
||||
* @param workflowId 部署id
|
||||
* @return {@link App }
|
||||
*/
|
||||
App getByWorkflowId(Long workflowId);
|
||||
|
||||
/**
|
||||
* 按id列表
|
||||
*
|
||||
* @param appIds id
|
||||
* @return {@link List }<{@link App }>
|
||||
*/
|
||||
List<App> listByAppIds(List<Long> appIds);
|
||||
|
||||
|
||||
/**
|
||||
* 创建
|
||||
*
|
||||
* @param createApp 应用程序
|
||||
* @return {@link App }
|
||||
*/
|
||||
App create(CreateApp createApp);
|
||||
|
||||
|
||||
/**
|
||||
* 更新
|
||||
*
|
||||
* @param updateApp 应用程序
|
||||
* @return {@link App }
|
||||
*/
|
||||
App update(UpdateApp updateApp);
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*
|
||||
* @param appId id
|
||||
*/
|
||||
void delete(Long appId);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.metis.flow.engine;
|
||||
|
||||
import com.metis.flow.runner.FlowRunningContext;
|
||||
import com.metis.flow.runner.RunnerResult;
|
||||
|
||||
/**
|
||||
* 应用引擎运行器服务
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/07
|
||||
*/
|
||||
public interface AppFlowEngineRunnerService {
|
||||
|
||||
|
||||
/**
|
||||
* 运行
|
||||
*
|
||||
* @param context 上下文
|
||||
* @return {@link RunnerResult }
|
||||
*/
|
||||
RunnerResult running(FlowRunningContext context);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package com.metis.flow.engine.impl;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.metis.enums.YesOrNoEnum;
|
||||
import com.metis.flow.constant.BaseConstant;
|
||||
import com.metis.flow.convert.BaseAppConvert;
|
||||
import com.metis.flow.domain.bo.BuildApp;
|
||||
import com.metis.flow.domain.bo.CreateApp;
|
||||
import com.metis.flow.domain.bo.UpdateApp;
|
||||
import com.metis.flow.domain.query.AppQuery;
|
||||
import com.metis.flow.domain.entity.*;
|
||||
import com.metis.flow.engine.AppEngineService;
|
||||
import com.metis.flow.service.BaseAppService;
|
||||
import com.metis.flow.validator.ValidatorService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppEngineServiceImpl implements AppEngineService {
|
||||
|
||||
private final BaseAppService baseAppService;
|
||||
private final ValidatorService validatorService;
|
||||
|
||||
|
||||
@Override
|
||||
public List<App> listPage(AppQuery query) {
|
||||
List<BaseApp> list = baseAppService.listPage(query);
|
||||
return BaseAppConvert.INSTANCE.toApps(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<App> list(AppQuery query) {
|
||||
List<BaseApp> list = baseAppService.listQuery(query);
|
||||
return BaseAppConvert.INSTANCE.toApps(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public App getByAppId(Long appId) {
|
||||
BaseApp baseApp = baseAppService.getByAppId(appId);
|
||||
return BaseAppConvert.INSTANCE.toApp(baseApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public App getByAppId(Long appId, Integer version) {
|
||||
BaseApp baseApp = baseAppService.getByAppIdAndVersion(appId, version);
|
||||
return BaseAppConvert.INSTANCE.toApp(baseApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public App getByWorkflowId(Long workflowId) {
|
||||
BaseApp baseApp = baseAppService.getById(workflowId);
|
||||
return BaseAppConvert.INSTANCE.toApp(baseApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<App> listByAppIds(List<Long> appIds) {
|
||||
List<BaseApp> baseApps = baseAppService.listByAppIds(appIds);
|
||||
return BaseAppConvert.INSTANCE.toApps(baseApps);
|
||||
}
|
||||
|
||||
@Override
|
||||
public App create(CreateApp createApp) {
|
||||
BuildApp buildApp = BaseAppConvert.INSTANCE.toBuildApp(createApp);
|
||||
// 校验
|
||||
validatorService.validate(buildApp);
|
||||
// 构建保存对象
|
||||
BaseApp baseApp = creatBaseApp(buildApp);
|
||||
// 保存
|
||||
boolean save = baseAppService.save(baseApp);
|
||||
Assert.isTrue(save, "保存失败");
|
||||
return creatApp(baseApp, buildApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public synchronized App update(UpdateApp updateApp) {
|
||||
BaseApp entity = baseAppService.getByAppId(updateApp.getAppId());
|
||||
Assert.isTrue(ObjectUtil.isNotNull(entity), "app不存在");
|
||||
BuildApp buildApp = BaseAppConvert.INSTANCE.toBuildApp(updateApp);
|
||||
// 校验
|
||||
validatorService.validate(buildApp);
|
||||
// 构建保存对象
|
||||
BaseApp baseApp = updateBaseApp(buildApp);
|
||||
if (ObjectUtil.isNotNull(updateApp.getDefaultUse()) && YesOrNoEnum.YES.equals(updateApp.getDefaultUse())) {
|
||||
baseApp.setDefaultUse(YesOrNoEnum.YES);
|
||||
baseAppService.updateDefaultUseByAppId(updateApp.getAppId(), YesOrNoEnum.NO);
|
||||
} else {
|
||||
baseApp.setDefaultUse(YesOrNoEnum.NO);
|
||||
}
|
||||
baseApp.setVersion(entity.getVersion() + 1);
|
||||
// 保存
|
||||
boolean save = baseAppService.save(baseApp);
|
||||
Assert.isTrue(save, "更新失败");
|
||||
return creatApp(baseApp, buildApp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(Long appId) {
|
||||
baseAppService.removeByAppId(appId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建应用程序
|
||||
*
|
||||
* @param baseApp 基础应用
|
||||
* @param buildApp 构建应用程序
|
||||
* @return {@link App }
|
||||
*/
|
||||
private App creatApp(BaseApp baseApp, BuildApp buildApp) {
|
||||
App app = BaseAppConvert.INSTANCE.toApp(buildApp);
|
||||
app.setId(baseApp.getId());
|
||||
app.setWorkflowId(baseApp.getId());
|
||||
app.setDefaultUse(baseApp.getDefaultUse());
|
||||
return app;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建应用程序
|
||||
*
|
||||
* @param buildApp 应用程序
|
||||
* @return {@link BaseApp }
|
||||
*/
|
||||
private BaseApp creatBaseApp(BuildApp buildApp) {
|
||||
BaseApp baseApp = BaseAppConvert.INSTANCE.toBaseApp(buildApp);
|
||||
long id = IdUtil.getSnowflakeNextId();
|
||||
baseApp.setAppId(id);
|
||||
baseApp.setId(id);
|
||||
baseApp.setVersion(BaseConstant.DEFAULT_VERSION);
|
||||
baseApp.setDefaultUse(YesOrNoEnum.YES);
|
||||
return baseApp;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新基础应用程序
|
||||
*
|
||||
* @param buildApp 构建应用程序
|
||||
* @return {@link BaseApp }
|
||||
*/
|
||||
private BaseApp updateBaseApp(BuildApp buildApp) {
|
||||
BaseApp baseApp = BaseAppConvert.INSTANCE.toBaseApp(buildApp);
|
||||
long id = IdUtil.getSnowflakeNextId();
|
||||
baseApp.setId(id);
|
||||
return baseApp;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package com.metis.flow.engine.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.metis.flow.domain.context.RunningContext;
|
||||
import com.metis.flow.domain.context.RunningResult;
|
||||
import com.metis.flow.domain.context.SysContext;
|
||||
import com.metis.flow.domain.entity.App;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.engine.AppEngineService;
|
||||
import com.metis.flow.engine.AppFlowEngineRunnerService;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.runner.FlowRunningContext;
|
||||
import com.metis.flow.runner.NodeRunner;
|
||||
import com.metis.flow.runner.RunnerResult;
|
||||
import com.metis.flow.runner.factory.NodeRunnerFactory;
|
||||
import com.metis.utils.GenericInterfacesUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class AppFlowEngineRunnerServiceImpl implements AppFlowEngineRunnerService {
|
||||
|
||||
private final AppEngineService appEngineService;
|
||||
|
||||
|
||||
@Override
|
||||
public RunnerResult running(FlowRunningContext context) {
|
||||
App app = getApp(context);
|
||||
// todo 构建运行实例, 并将运行实例放入上下文
|
||||
Long instanceId = IdUtil.getSnowflakeNextId();
|
||||
// 构建系统上下文信息
|
||||
SysContext sysContext = SysContext.builder()
|
||||
.files(context.getFiles())
|
||||
.appId(app.getId())
|
||||
.workflowId(app.getWorkflowId())
|
||||
.instanceId(instanceId)
|
||||
.build();
|
||||
// 构建运行中上下文
|
||||
RunningContext runningContext = RunningContext.buildContext(sysContext, context);
|
||||
// 构建节点映射对象
|
||||
Graph graph = app.getGraph();
|
||||
Map<Long, Node> nodeMap = graph.getNodes().stream()
|
||||
.collect(Collectors.toMap(Node::getId, Function.identity()));
|
||||
Map<Long, List<Edge>> edgeMap = graph.getEdges().stream()
|
||||
.collect(Collectors.groupingBy(Edge::getSource));
|
||||
Set<Node> readyRunningNode = new HashSet<>();
|
||||
|
||||
// 获取到开始节点
|
||||
// 开始节点为空,则表示数据存在异常
|
||||
Assert.isTrue(ObjectUtil.isNotNull(readyRunningNode), "流程图不存在开始节点");
|
||||
while (CollUtil.isNotEmpty(readyRunningNode)) {
|
||||
// todo 出现多个节点同时运行, 需要找到他们最终运行的聚合节点, 前期默认只有一条线路运行, 不支持并行流程
|
||||
doRunning(readyRunningNode, edgeMap, runningContext);
|
||||
|
||||
|
||||
readyRunningNode = null;
|
||||
}
|
||||
|
||||
return RunnerResult.builder()
|
||||
.content("你他妈的!")
|
||||
.context(sysContext)
|
||||
.build();
|
||||
|
||||
}
|
||||
|
||||
private void doRunning(Set<Node> readyRunningNode, Map<Long, List<Edge>> edgeMap, RunningContext runningContext) {
|
||||
Set<Long> nextRunningNodeId = new HashSet<>();
|
||||
for (Node runningNode : readyRunningNode) {
|
||||
// 当前节点接下来的连接线信息
|
||||
List<Edge> edges = edgeMap.getOrDefault(runningNode.getId(), new ArrayList<>());
|
||||
// 执行
|
||||
NodeRunner nodeRunner = getNodeRunner(runningNode);
|
||||
runningNode.setConfigClass(GenericInterfacesUtils.getClass(nodeRunner));
|
||||
// 获取到返回结果
|
||||
RunningResult result = nodeRunner.run(runningContext, runningNode, edges);
|
||||
// 节点执行结果参数放入上下文中
|
||||
if (ObjectUtil.isNotNull(result.getNodeContext())) {
|
||||
runningContext.addNodeRunningContext(runningNode.getId(), result.getNodeContext());
|
||||
}
|
||||
if (CollUtil.isNotEmpty(result.getNextRunNodeId())) {
|
||||
nextRunningNodeId.addAll(result.getNextRunNodeId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private RunningResult doRunning(RunningContext runningContext, Node node, List<Edge> edges) {
|
||||
NodeRunner nodeRunner = getNodeRunner(node);
|
||||
node.setConfigClass(GenericInterfacesUtils.getClass(nodeRunner));
|
||||
// 获取到返回结果
|
||||
return nodeRunner.run(runningContext, node, edges);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取节点运行程序
|
||||
*
|
||||
* @param node 节点
|
||||
* @return {@link NodeRunner }
|
||||
*/
|
||||
private NodeRunner getNodeRunner(Node node) {
|
||||
if (NodeType.CUSTOM_NODE.equals(node.getType())) {
|
||||
Assert.isTrue(StrUtil.isNotBlank(node.getCustomType()), "自定义节点类型不能为空");
|
||||
return NodeRunnerFactory.getCustom(node.getCustomType());
|
||||
}
|
||||
return NodeRunnerFactory.get(node.getType());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取到应用程序信息
|
||||
*
|
||||
* @param context 上下文
|
||||
* @return {@link App }
|
||||
*/
|
||||
private App getApp(FlowRunningContext context) {
|
||||
if (ObjectUtil.isNotNull(context.getWorkflowId())) {
|
||||
return appEngineService.getByWorkflowId(context.getWorkflowId());
|
||||
}
|
||||
return appEngineService.getByAppId(context.getAppId());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.metis.flow.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum EdgeType {
|
||||
|
||||
DEFAULT(1, "default", "默认线"),
|
||||
|
||||
;
|
||||
|
||||
|
||||
private final Integer code;
|
||||
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举序列化器(前端传code时自动转换为对应枚举)
|
||||
*
|
||||
* @param value 值
|
||||
* @return 枚举
|
||||
*/
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static EdgeType get(String value) {
|
||||
return Arrays.stream(EdgeType.class.getEnumConstants())
|
||||
.filter(e -> e.getValue().equals(value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.metis.flow.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 文件上传类型
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/08
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum FileUploadType {
|
||||
LOCAL_FILE(1, "localFile", "本地文件"),
|
||||
REMOTE_URL(2, "remoteUrl", "远程URL");
|
||||
|
||||
|
||||
private final Integer code;
|
||||
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举序列化器(前端传code时自动转换为对应枚举)
|
||||
*
|
||||
* @param value 值
|
||||
* @return 枚举
|
||||
*/
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static FileUploadType get(String value) {
|
||||
return Arrays.stream(FileUploadType.class.getEnumConstants())
|
||||
.filter(e -> e.getValue().equals(value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.metis.flow.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum HandleType {
|
||||
SOURCE(1, "source", "源节点"),
|
||||
TARGET(2, "target", "目标节点"),
|
||||
;
|
||||
|
||||
|
||||
private final Integer code;
|
||||
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举序列化器(前端传code时自动转换为对应枚举)
|
||||
*
|
||||
* @param value 值
|
||||
* @return 枚举
|
||||
*/
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static HandleType get(String value) {
|
||||
return Arrays.stream(HandleType.class.getEnumConstants())
|
||||
.filter(e -> e.getValue().equals(value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.metis.flow.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum NodeType {
|
||||
|
||||
START(1, "start", "开始"),
|
||||
END(2, "end", "结束"),
|
||||
DOCUMENT_EXTRACTOR(3, "document-extractor", "文档提取器"),
|
||||
CUSTOM_NODE(4, "Custom-Node", "自定义节点");
|
||||
|
||||
|
||||
private final Integer code;
|
||||
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String name;
|
||||
|
||||
// private final Class<?> configClass;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举序列化器(前端传code时自动转换为对应枚举)
|
||||
*
|
||||
* @param value 值
|
||||
* @return 枚举
|
||||
*/
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static NodeType get(String value) {
|
||||
return Arrays.stream(NodeType.class.getEnumConstants())
|
||||
.filter(e -> e.getValue().equals(value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.metis.flow.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 节点变量类型
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/08
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum NodeVariableType {
|
||||
TEXT_INPUT(1, "text-input", "文本"),
|
||||
PARAGRAPH(2, "paragraph", "段落"),
|
||||
SELECT(3, "select", "下拉框"),
|
||||
NUMBER(4, "number", "数字"),
|
||||
FILE(5, "file", "文件"),
|
||||
FILE_LIST(6, "file-list", "文件列表")
|
||||
;
|
||||
|
||||
|
||||
private final Integer code;
|
||||
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举序列化器(前端传code时自动转换为对应枚举)
|
||||
*
|
||||
* @param value 值
|
||||
* @return 枚举
|
||||
*/
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static NodeVariableType get(String value) {
|
||||
return Arrays.stream(NodeVariableType.class.getEnumConstants())
|
||||
.filter(e -> e.getValue().equals(value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.metis.flow.enums;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonCreator;
|
||||
import com.fasterxml.jackson.annotation.JsonValue;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 句柄位置类型
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/05
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum PositionType {
|
||||
|
||||
LEFT(1, "left", "左"),
|
||||
RIGHT(2, "right", "右"),
|
||||
TOP(3, "top", "上"),
|
||||
BOTTOM(4, "bottom", "下");
|
||||
|
||||
|
||||
private final Integer code;
|
||||
|
||||
@JsonValue
|
||||
private final String value;
|
||||
|
||||
private final String name;
|
||||
|
||||
|
||||
/**
|
||||
* 枚举序列化器(前端传code时自动转换为对应枚举)
|
||||
*
|
||||
* @param value 值
|
||||
* @return 枚举
|
||||
*/
|
||||
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
|
||||
public static PositionType get(String value) {
|
||||
return Arrays.stream(PositionType.class.getEnumConstants())
|
||||
.filter(e -> e.getValue().equals(value))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.metis.flow.mapper;
|
||||
|
||||
import com.metis.flow.domain.entity.BaseApp;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
/**
|
||||
* 基础应用映射器
|
||||
*
|
||||
* @author 18209
|
||||
* @date 2025/04/05
|
||||
*/
|
||||
@Mapper
|
||||
public interface BaseAppMapper extends BaseMapper<BaseApp> {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.metis.flow.runner;
|
||||
|
||||
import com.metis.flow.domain.entity.base.NodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
|
||||
/**
|
||||
* 自定义节点运行器
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/20
|
||||
*/
|
||||
public interface CustomNodeRunner<T extends NodeConfig> extends NodeRunner<T> {
|
||||
|
||||
|
||||
/**
|
||||
* 获取自定义节点的节点类型
|
||||
*
|
||||
* @return {@link String }
|
||||
*/
|
||||
String getCustomNodeType();
|
||||
|
||||
|
||||
/**
|
||||
* 得到类型
|
||||
*
|
||||
* @return {@link NodeType }
|
||||
*/
|
||||
default NodeType getType() {
|
||||
return NodeType.CUSTOM_NODE;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.metis.flow.runner;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 运行上下文
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/07
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class FlowRunningContext {
|
||||
|
||||
/**
|
||||
* 文件列表
|
||||
*/
|
||||
private List<String> files;
|
||||
|
||||
/**
|
||||
* 应用程序id
|
||||
*/
|
||||
private Long appId;
|
||||
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 工作流id
|
||||
*/
|
||||
private Long workflowId;
|
||||
|
||||
/**
|
||||
* 自定义
|
||||
*/
|
||||
private JSONObject custom;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.metis.flow.runner;
|
||||
|
||||
import com.metis.flow.domain.context.RunningContext;
|
||||
import com.metis.flow.domain.context.RunningResult;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.domain.entity.base.NodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 内置节点运行器
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/20
|
||||
*/
|
||||
public interface NodeRunner<T extends NodeConfig> {
|
||||
|
||||
|
||||
/**
|
||||
* 运行
|
||||
*
|
||||
* @param context 上下文
|
||||
* @param node 节点配置信息
|
||||
* @param edges 当前接下来的连线信息, 一些特殊节点需要节点内部判断下一个运行节点
|
||||
* @return {@link RunningContext }
|
||||
*/
|
||||
RunningResult run(RunningContext context, Node node, List<Edge> edges);
|
||||
|
||||
|
||||
/**
|
||||
* 获取节点类型
|
||||
*
|
||||
* @return {@link NodeType }
|
||||
*/
|
||||
NodeType getType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.metis.flow.runner;
|
||||
|
||||
|
||||
import com.metis.flow.domain.context.SysContext;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 运行结果
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/07
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
public class RunnerResult {
|
||||
|
||||
/**
|
||||
* 运行内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 上下文
|
||||
*/
|
||||
private SysContext context;
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.metis.flow.runner.factory;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.runner.CustomNodeRunner;
|
||||
import com.metis.flow.runner.NodeRunner;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public final class NodeRunnerFactory {
|
||||
|
||||
/**
|
||||
* 内置节点运行器
|
||||
*/
|
||||
private static final Map<NodeType, NodeRunner> NODE_MAP = new ConcurrentHashMap<>(8);
|
||||
/**
|
||||
* 自定义节点映射
|
||||
*/
|
||||
private static final Map<String, NodeRunner> CUSTOM_NODE_MAP = new ConcurrentHashMap<>(8);
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*
|
||||
* @param runner 跑步者
|
||||
*/
|
||||
static void register(NodeRunner runner) {
|
||||
NODE_MAP.put(runner.getType(), runner);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 得到
|
||||
*
|
||||
* @param type 类型
|
||||
* @return {@link NodeRunner }
|
||||
*/
|
||||
public static NodeRunner get(NodeType type) {
|
||||
return NODE_MAP.get(type);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 注册自定义节点
|
||||
*
|
||||
* @param runner 跑步者
|
||||
*/
|
||||
static void registerCustom(CustomNodeRunner runner) {
|
||||
Assert.isTrue(!CUSTOM_NODE_MAP.containsKey(runner.getCustomNodeType()), "已存在类型:{}, class:{}的运行器", runner.getCustomNodeType(), runner.getClass());
|
||||
CUSTOM_NODE_MAP.put(runner.getCustomNodeType(), runner);
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到自定义节点运行器
|
||||
*
|
||||
* @param type 类型
|
||||
* @return {@link NodeRunner }
|
||||
*/
|
||||
public static NodeRunner getCustom(String type) {
|
||||
return CUSTOM_NODE_MAP.get(type);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.metis.flow.runner.factory;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.runner.CustomNodeRunner;
|
||||
import com.metis.flow.runner.NodeRunner;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* runner初始化
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/07
|
||||
*/
|
||||
@Service
|
||||
public class RunnerInitialize implements ApplicationContextAware {
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||
|
||||
Map<String, NodeRunner> runnerMap = applicationContext.getBeansOfType(NodeRunner.class);
|
||||
|
||||
runnerMap.forEach((runnerBeanName, runner) -> {
|
||||
if (NodeType.CUSTOM_NODE.equals(runner.getType())) {
|
||||
Assert.isTrue(runner instanceof CustomNodeRunner, "自定义节点必须实现CustomNodeRunner接口");
|
||||
NodeRunnerFactory.registerCustom((CustomNodeRunner) runner);
|
||||
} else {
|
||||
NodeRunnerFactory.register(runner);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.metis.flow.runner.impl;
|
||||
|
||||
|
||||
import com.metis.flow.domain.context.RunningContext;
|
||||
import com.metis.flow.domain.context.RunningResult;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.domain.entity.config.node.EndNodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.runner.NodeRunner;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class EndNodeRunner implements NodeRunner<EndNodeConfig> {
|
||||
|
||||
@Override
|
||||
public RunningResult run(RunningContext context, Node node, List<Edge> edges) {
|
||||
return RunningResult.buildResult();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
return NodeType.END;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.metis.flow.runner.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.metis.flow.domain.context.RunningContext;
|
||||
import com.metis.flow.domain.context.RunningResult;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.domain.entity.base.NodeVariable;
|
||||
import com.metis.flow.domain.entity.config.node.StartNodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.runner.NodeRunner;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 启动节点运行器, 开始节点功能主要为将用户传入的参数进行校验, 并放入到上下文中
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/08
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class StartNodeRunner implements NodeRunner<StartNodeConfig> {
|
||||
|
||||
@Override
|
||||
public RunningResult run(RunningContext context, Node node, List<Edge> edges) {
|
||||
log.info("开始节点{}, 节点id: {} 运行", node.getData().getLabel(), node.getId());
|
||||
StartNodeConfig config = node.getConfig();
|
||||
// 获取到节点的自定义参数
|
||||
List<NodeVariable> variables = config.getVariables();
|
||||
// 如果没有自定义参数, 则直接返回
|
||||
if (CollUtil.isEmpty(variables)) {
|
||||
return RunningResult.buildResult();
|
||||
}
|
||||
// 获取用户自定义参数
|
||||
JSONObject custom = context.getCustom();
|
||||
JSONObject contextNodeValue = new JSONObject();
|
||||
for (NodeVariable variable : variables) {
|
||||
Object value = variable.getValue(custom);
|
||||
contextNodeValue.put(variable.getVariable(), value);
|
||||
}
|
||||
return RunningResult.buildResult(contextNodeValue);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
return NodeType.START;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.metis.flow.service;
|
||||
|
||||
import com.metis.enums.YesOrNoEnum;
|
||||
import com.metis.flow.domain.query.AppQuery;
|
||||
import com.metis.flow.domain.entity.BaseApp;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基础应用服务
|
||||
*
|
||||
* @author 18209
|
||||
* @date 2025/04/05
|
||||
*/
|
||||
public interface BaseAppService extends IService<BaseApp> {
|
||||
|
||||
|
||||
/**
|
||||
* 通过应用id获取
|
||||
*
|
||||
* @param appId 应用程序id
|
||||
* @return {@link BaseApp }
|
||||
*/
|
||||
BaseApp getByAppId(Long appId);
|
||||
|
||||
|
||||
/**
|
||||
* 通过id和版本获取
|
||||
*
|
||||
* @param appId id
|
||||
* @param version 版本
|
||||
* @return {@link BaseApp }
|
||||
*/
|
||||
BaseApp getByAppIdAndVersion(Long appId, Integer version);
|
||||
|
||||
/**
|
||||
* 列表页面
|
||||
*
|
||||
* @param query 查询
|
||||
* @return {@link List }<{@link BaseApp }>
|
||||
*/
|
||||
List<BaseApp> listPage(AppQuery query);
|
||||
|
||||
/**
|
||||
* 列表查询
|
||||
*
|
||||
* @param query 查询
|
||||
* @return {@link List }<{@link BaseApp }>
|
||||
*/
|
||||
List<BaseApp> listQuery(AppQuery query);
|
||||
|
||||
/**
|
||||
* 应用id列表
|
||||
*
|
||||
* @param appIds 应用程序id
|
||||
* @return {@link List }<{@link BaseApp }>
|
||||
*/
|
||||
List<BaseApp> listByAppIds(List<Long> appIds);
|
||||
|
||||
/**
|
||||
* 按应用id删除
|
||||
*
|
||||
* @param appId 应用程序id
|
||||
* @return int
|
||||
*/
|
||||
int removeByAppId(Long appId);
|
||||
|
||||
/**
|
||||
* 更新默认使用
|
||||
*
|
||||
* @param appId 应用程序id
|
||||
* @param yesOrNoEnum 是或否enum
|
||||
* @return int
|
||||
*/
|
||||
boolean updateDefaultUseByAppId(Long appId, YesOrNoEnum yesOrNoEnum);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package com.metis.flow.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.metis.enums.YesOrNoEnum;
|
||||
import com.metis.flow.domain.query.AppQuery;
|
||||
import com.metis.flow.domain.entity.BaseApp;
|
||||
import com.metis.flow.mapper.BaseAppMapper;
|
||||
import com.metis.flow.service.BaseAppService;
|
||||
import com.metis.utils.PageConditionUtil;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基础应用服务impl
|
||||
*
|
||||
* @author 18209
|
||||
* @date 2025/04/05
|
||||
*/
|
||||
@Service
|
||||
public class BaseAppServiceImpl extends ServiceImpl<BaseAppMapper, BaseApp>
|
||||
implements BaseAppService {
|
||||
|
||||
@Override
|
||||
public BaseApp getByAppId(Long appId) {
|
||||
return this.lambdaQuery()
|
||||
.eq(BaseApp::getAppId, appId)
|
||||
.orderByDesc(BaseApp::getVersion)
|
||||
.last("limit 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public BaseApp getByAppIdAndVersion(Long appId, Integer version) {
|
||||
return this.lambdaQuery()
|
||||
.eq(BaseApp::getId, appId)
|
||||
.eq(BaseApp::getVersion, version)
|
||||
.last("limit 1")
|
||||
.one();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BaseApp> listPage(AppQuery query) {
|
||||
IPage<BaseApp> page = PageConditionUtil.getPage();
|
||||
LambdaQueryWrapper<BaseApp> wrapper = buildQuery(query);
|
||||
return baseMapper.selectList(page, wrapper);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<BaseApp> listQuery(AppQuery query) {
|
||||
LambdaQueryWrapper<BaseApp> wrapper = buildQuery(query);
|
||||
return baseMapper.selectList(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BaseApp> listByAppIds(List<Long> appIds) {
|
||||
return this.lambdaQuery()
|
||||
.in(BaseApp::getAppId, appIds)
|
||||
.eq(BaseApp::getDefaultUse, YesOrNoEnum.YES)
|
||||
.list();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int removeByAppId(Long appId) {
|
||||
LambdaQueryWrapper<BaseApp> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.eq(BaseApp::getAppId, appId);
|
||||
return baseMapper.delete(wrapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean updateDefaultUseByAppId(Long appId, YesOrNoEnum yesOrNoEnum) {
|
||||
return this.lambdaUpdate()
|
||||
.eq(BaseApp::getAppId, appId)
|
||||
.set(BaseApp::getDefaultUse, yesOrNoEnum)
|
||||
.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建查询
|
||||
*
|
||||
* @param query 查询
|
||||
* @return {@link LambdaQueryWrapper }<{@link BaseApp }>
|
||||
*/
|
||||
public LambdaQueryWrapper<BaseApp> buildQuery(AppQuery query) {
|
||||
LambdaQueryWrapper<BaseApp> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.select(BaseApp::getId, BaseApp::getAppId, BaseApp::getName,
|
||||
BaseApp::getDescription, BaseApp::getVersion, BaseApp::getIsDeleted,
|
||||
BaseApp::getCreateTime, BaseApp::getUpdateTime)
|
||||
.like(StrUtil.isNotBlank(query.getName()), BaseApp::getName, query.getName())
|
||||
.like(StrUtil.isNotBlank(query.getDescription()), BaseApp::getDescription, query.getDescription());
|
||||
if (ObjectUtil.isNotNull(query.getAppId())) {
|
||||
wrapper.eq(BaseApp::getAppId, query.getAppId());
|
||||
} else {
|
||||
wrapper.eq(BaseApp::getDefaultUse, YesOrNoEnum.YES);
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.metis.flow.validator;
|
||||
|
||||
import com.metis.flow.domain.entity.base.NodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
|
||||
/**
|
||||
* 自定义节点验证器
|
||||
*
|
||||
* @author clay
|
||||
* @date 2025/04/20
|
||||
*/
|
||||
public interface CustomNodeValidator<T extends NodeConfig> extends NodeValidator<T> {
|
||||
|
||||
|
||||
/**
|
||||
* 获取自定义节点的节点类型
|
||||
*
|
||||
* @return {@link String }
|
||||
*/
|
||||
String getCustomNodeType();
|
||||
|
||||
|
||||
/**
|
||||
* 得到类型
|
||||
*
|
||||
* @return {@link NodeType }
|
||||
*/
|
||||
default NodeType getType() {
|
||||
return NodeType.CUSTOM_NODE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.metis.flow.validator;
|
||||
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.enums.EdgeType;
|
||||
|
||||
public interface EdgeValidator {
|
||||
|
||||
|
||||
/**
|
||||
* 线验证
|
||||
*
|
||||
* @param edge 线
|
||||
* @return {@link ValidatorResult }
|
||||
*/
|
||||
ValidatorResult validate(Edge edge);
|
||||
|
||||
|
||||
/**
|
||||
* 得到类型
|
||||
*
|
||||
* @return {@link EdgeType }
|
||||
*/
|
||||
EdgeType getType();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.metis.flow.validator;
|
||||
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.domain.entity.base.NodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface NodeValidator<T extends NodeConfig> {
|
||||
|
||||
|
||||
/**
|
||||
* 节点验证
|
||||
*
|
||||
* @param node 节点
|
||||
* @return {@link ValidatorResult }
|
||||
*/
|
||||
ValidatorResult validateValue(Node node);
|
||||
|
||||
|
||||
/**
|
||||
* 验证关系
|
||||
*
|
||||
* @param node 节点
|
||||
* @param sources 来源
|
||||
* @param targets 目标
|
||||
* @return {@link ValidatorResult }
|
||||
*/
|
||||
ValidatorResult validateRelation(Node node, List<Edge> sources, List<Edge> targets);
|
||||
|
||||
|
||||
/**
|
||||
* 得到类型
|
||||
*
|
||||
* @return {@link NodeType }
|
||||
*/
|
||||
NodeType getType();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.metis.flow.validator;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.Validator;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ValidatorCodeService {
|
||||
|
||||
private final Validator globalValidator;
|
||||
|
||||
/**
|
||||
* 验证, 出现校验未通过则抛出异常
|
||||
*
|
||||
* @param validObject 有效对象
|
||||
*/
|
||||
public <T> void validateThrow(T validObject) {
|
||||
// validation 编程式校验
|
||||
Set<ConstraintViolation<T>> validates = globalValidator.validate(validObject);
|
||||
if (CollUtil.isNotEmpty(validates)) {
|
||||
List<String> errorMessage = validates.stream()
|
||||
.map(ConstraintViolation::getMessage).toList();
|
||||
throw new RuntimeException(String.join(",", errorMessage));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证错误信息
|
||||
*
|
||||
* @param validObject 有效对象
|
||||
* @return {@link String }
|
||||
*/
|
||||
public <T> String validateErrorMsg(T validObject) {
|
||||
// validation 编程式校验
|
||||
Set<ConstraintViolation<T>> validates = globalValidator.validate(validObject);
|
||||
if (CollUtil.isNotEmpty(validates)) {
|
||||
List<String> errorMessage = validates.stream()
|
||||
.map(ConstraintViolation::getMessage).toList();
|
||||
return String.join(",", errorMessage);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证
|
||||
*
|
||||
* @param validObject 有效对象
|
||||
* @return boolean
|
||||
*/
|
||||
public <T> boolean validate(T validObject) {
|
||||
// validation 编程式校验
|
||||
Set<ConstraintViolation<T>> validates = globalValidator.validate(validObject);
|
||||
return CollUtil.isEmpty(validates);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.metis.flow.validator;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public final class ValidatorResult {
|
||||
|
||||
private final Boolean valid;
|
||||
|
||||
private final String message;
|
||||
|
||||
private ValidatorResult(Boolean valid, String message) {
|
||||
this.valid = valid;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public static ValidatorResult valid() {
|
||||
return new ValidatorResult(true, "");
|
||||
}
|
||||
|
||||
public static ValidatorResult invalid(String message) {
|
||||
return new ValidatorResult(false, message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.metis.flow.validator;
|
||||
|
||||
import com.metis.flow.domain.bo.BuildApp;
|
||||
|
||||
public interface ValidatorService {
|
||||
|
||||
|
||||
/**
|
||||
* 验证
|
||||
*
|
||||
* @param graph 流程信息
|
||||
*/
|
||||
void validate(BuildApp graph);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.metis.flow.validator.factory;
|
||||
|
||||
import com.metis.flow.enums.EdgeType;
|
||||
import com.metis.flow.validator.EdgeValidator;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
public class EdgeValidatorFactory {
|
||||
|
||||
private static final Map<EdgeType, EdgeValidator> MAP = new ConcurrentHashMap<>(8);
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*
|
||||
* @param validator 验证器
|
||||
*/
|
||||
public static void register(EdgeValidator validator) {
|
||||
MAP.put(validator.getType(), validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到
|
||||
*
|
||||
* @param type 类型
|
||||
* @return {@link EdgeValidator }
|
||||
*/
|
||||
public static EdgeValidator get(EdgeType type) {
|
||||
return MAP.get(type);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.metis.flow.validator.factory;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.validator.CustomNodeValidator;
|
||||
import com.metis.flow.validator.NodeValidator;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
|
||||
public final class NodeValidatorFactory {
|
||||
|
||||
/**
|
||||
* 内置节点验证器
|
||||
*/
|
||||
private static final Map<NodeType, NodeValidator> NODE_MAP = new ConcurrentHashMap<>(8);
|
||||
/**
|
||||
* 自定义节点验证器
|
||||
*/
|
||||
private static final Map<String, NodeValidator> CUSTOM_NODE_MAP = new ConcurrentHashMap<>(8);
|
||||
|
||||
|
||||
/**
|
||||
* 注册
|
||||
*
|
||||
* @param validator 验证器
|
||||
*/
|
||||
static void register(NodeValidator validator) {
|
||||
NODE_MAP.put(validator.getType(), validator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 得到
|
||||
*
|
||||
* @param type 类型
|
||||
* @return {@link NodeValidator }
|
||||
*/
|
||||
public static NodeValidator get(NodeType type) {
|
||||
return NODE_MAP.get(type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param validator 验证器
|
||||
*/
|
||||
static void registerCustom(CustomNodeValidator validator) {
|
||||
Assert.isTrue(!CUSTOM_NODE_MAP.containsKey(validator.getCustomNodeType()), "已存在类型:{}, class:{}的验证器", validator.getCustomNodeType(), validator.getClass());
|
||||
CUSTOM_NODE_MAP.put(validator.getCustomNodeType(), validator);
|
||||
}
|
||||
|
||||
public static NodeValidator getCustom(String type) {
|
||||
return CUSTOM_NODE_MAP.get(type);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.metis.flow.validator.factory;
|
||||
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.validator.CustomNodeValidator;
|
||||
import com.metis.flow.validator.EdgeValidator;
|
||||
import com.metis.flow.validator.NodeValidator;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class ValidatorInitialize implements ApplicationContextAware {
|
||||
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(@NotNull ApplicationContext applicationContext) throws BeansException {
|
||||
|
||||
Map<String, EdgeValidator> edgeMap = applicationContext.getBeansOfType(EdgeValidator.class);
|
||||
|
||||
edgeMap.forEach((edgeValidatorBeanName, edgeValidator) -> {
|
||||
EdgeValidatorFactory.register(edgeValidator);
|
||||
});
|
||||
|
||||
Map<String, NodeValidator> nodeMap = applicationContext.getBeansOfType(NodeValidator.class);
|
||||
|
||||
nodeMap.forEach((nodeValidatorBeanName, nodeValidator) -> {
|
||||
if (NodeType.CUSTOM_NODE.equals(nodeValidator.getType())) {
|
||||
Assert.isTrue(nodeValidator instanceof CustomNodeValidator, "自定义节点必须实现CustomNodeValidator接口");
|
||||
NodeValidatorFactory.registerCustom((CustomNodeValidator) nodeValidator);
|
||||
} else {
|
||||
NodeValidatorFactory.register(nodeValidator);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package com.metis.flow.validator.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.metis.flow.domain.bo.BuildApp;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Graph;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.enums.EdgeType;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.validator.*;
|
||||
import com.metis.flow.validator.factory.EdgeValidatorFactory;
|
||||
import com.metis.flow.validator.factory.NodeValidatorFactory;
|
||||
import com.metis.utils.GenericInterfacesUtils;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ValidatorServiceImpl implements ValidatorService {
|
||||
|
||||
private final ValidatorCodeService validatorCodeService;
|
||||
|
||||
@Override
|
||||
public void validate(BuildApp buildApp) {
|
||||
// validation 编程式校验
|
||||
validatorCodeService.validateThrow(buildApp);
|
||||
Graph graph = buildApp.getGraph();
|
||||
// 节点参数校验
|
||||
validateNode(graph.getNodes());
|
||||
// 线参数校验
|
||||
validateEdge(graph.getEdges());
|
||||
// 关系验证
|
||||
validateRelation(graph.getNodes(), graph.getEdges());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证节点
|
||||
*
|
||||
* @param nodes 节点
|
||||
*/
|
||||
private void validateNode(List<Node> nodes) {
|
||||
List<String> errorMessage = new ArrayList<>();
|
||||
for (Node node : nodes) {
|
||||
NodeType type = node.getType();
|
||||
NodeValidator validator = getNodeValidator(node);
|
||||
node.setConfigClass(GenericInterfacesUtils.getClass(validator));
|
||||
// 节点校验器
|
||||
Assert.isTrue(ObjectUtil.isNotNull(validator), "无:{}类型的节点校验器", type.getName());
|
||||
ValidatorResult result = validator.validateValue(node);
|
||||
// 返回值检查
|
||||
Assert.isTrue(ObjectUtil.isNotNull(result), "类型:{} 的校验器无返回值", validator.getType().getName());
|
||||
if (!result.getValid()) {
|
||||
errorMessage.add(result.getMessage());
|
||||
}
|
||||
}
|
||||
Assert.isTrue(CollUtil.isEmpty(errorMessage), String.join(",", errorMessage));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证边缘
|
||||
*
|
||||
* @param edges 线
|
||||
*/
|
||||
private void validateEdge(List<Edge> edges) {
|
||||
List<String> errorMessage = new ArrayList<>();
|
||||
for (Edge edge : edges) {
|
||||
EdgeType type = edge.getType();
|
||||
EdgeValidator validator = EdgeValidatorFactory.get(type);
|
||||
// 节点校验器
|
||||
Assert.isTrue(ObjectUtil.isNotNull(validator), "无:{}类型的边校验器", type.getName());
|
||||
ValidatorResult result = validator.validate(edge);
|
||||
// 返回值检查
|
||||
Assert.isTrue(ObjectUtil.isNotNull(result), "类型:{} 的校验器无返回值", validator.getType().getName());
|
||||
if (!result.getValid()) {
|
||||
errorMessage.add(result.getMessage());
|
||||
}
|
||||
}
|
||||
Assert.isTrue(CollUtil.isEmpty(errorMessage), String.join(",", errorMessage));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 验证关系
|
||||
*
|
||||
* @param nodes 节点
|
||||
* @param edges 边缘
|
||||
*/
|
||||
private void validateRelation(List<Node> nodes, List<Edge> edges) {
|
||||
// 0. 检查是否只有一个起始节点
|
||||
validateSingleStartNode(nodes);
|
||||
|
||||
// 1. 检查线是否连接有效节点
|
||||
validateEdgeConnections(nodes, edges);
|
||||
|
||||
// 2. 检查是否存在环结构
|
||||
validateCycle(nodes, edges);
|
||||
|
||||
// 3. 检查是否存在孤立节点
|
||||
validateIsolatedNodes(nodes, edges);
|
||||
|
||||
// 4. 检查两个节点间是否有多根连线
|
||||
validateMultipleEdgesBetweenNodes(edges);
|
||||
|
||||
// 5. 检查节点自己的关系信息
|
||||
validateNodeRelations(nodes, edges);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否只有一个起始节点
|
||||
*
|
||||
* @param nodes 节点
|
||||
*/
|
||||
private void validateSingleStartNode(List<Node> nodes) {
|
||||
Long startNodeCount = nodes.stream()
|
||||
.filter(node -> NodeType.START.equals(node.getType()))
|
||||
.count();
|
||||
Assert.isTrue(startNodeCount.equals(1L), "图中必须且只能有一个起始节点,当前起始节点数量: {}", startNodeCount);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查线是否连接有效节点
|
||||
*
|
||||
* @param nodes 节点
|
||||
* @param edges 边缘
|
||||
*/
|
||||
private void validateEdgeConnections(List<Node> nodes, List<Edge> edges) {
|
||||
Map<Long, Node> nodeMap = nodes.stream().collect(Collectors.toMap(Node::getId, Function.identity()));
|
||||
for (Edge edge : edges) {
|
||||
Long source = edge.getSource();
|
||||
Long target = edge.getTarget();
|
||||
Assert.isTrue(nodeMap.containsKey(source), "边 {} 的源节点 {} 不存在", edge.getLabel(), source);
|
||||
Assert.isTrue(nodeMap.containsKey(target), "边 {} 的目标节点 {} 不存在", edge.getLabel(), target);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在环结构
|
||||
*
|
||||
* @param nodes 节点
|
||||
* @param edges 边缘
|
||||
*/
|
||||
private void validateCycle(List<Node> nodes, List<Edge> edges) {
|
||||
Map<Long, List<Long>> adjacencyList = buildAdjacencyList(edges);
|
||||
for (Node node : nodes) {
|
||||
if (hasCycle(node.getId(), adjacencyList, new HashSet<>())) {
|
||||
throw new IllegalArgumentException("图中存在环结构,起始节点: " + node.getData().getLabel());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在孤立节点
|
||||
*
|
||||
* @param nodes 节点
|
||||
* @param edges 边缘
|
||||
*/
|
||||
private void validateIsolatedNodes(List<Node> nodes, List<Edge> edges) {
|
||||
Set<Long> connectedNodes = new HashSet<>();
|
||||
for (Edge edge : edges) {
|
||||
connectedNodes.add(edge.getSource());
|
||||
connectedNodes.add(edge.getTarget());
|
||||
}
|
||||
for (Node node : nodes) {
|
||||
Assert.isTrue(connectedNodes.contains(node.getId()), "节点 {} 是孤立节点,未与任何边连接", node.getId());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建邻接表
|
||||
*
|
||||
* @param edges 边缘
|
||||
* @return {@link Map }<{@link Long }, {@link List }<{@link Long }>>
|
||||
*/
|
||||
private Map<Long, List<Long>> buildAdjacencyList(List<Edge> edges) {
|
||||
Map<Long, List<Long>> adjacencyList = new HashMap<>();
|
||||
for (Edge edge : edges) {
|
||||
adjacencyList.computeIfAbsent(edge.getSource(), k -> new ArrayList<>()).add(edge.getTarget());
|
||||
}
|
||||
return adjacencyList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 深度优先搜索(DFS)检查环
|
||||
*
|
||||
* @param nodeId 节点id
|
||||
* @param adjacencyList 邻接表
|
||||
* @param visited 是否已经访问过了
|
||||
* @return boolean
|
||||
*/
|
||||
private boolean hasCycle(Long nodeId, Map<Long, List<Long>> adjacencyList, Set<Long> visited) {
|
||||
if (visited.contains(nodeId)) {
|
||||
return true; // 发现环
|
||||
}
|
||||
visited.add(nodeId);
|
||||
for (Long neighbor : adjacencyList.getOrDefault(nodeId, new ArrayList<>())) {
|
||||
if (hasCycle(neighbor, adjacencyList, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
visited.remove(nodeId);
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查两个节点间是否有多根连线
|
||||
*
|
||||
* @param edges 边缘
|
||||
*/
|
||||
private void validateMultipleEdgesBetweenNodes(List<Edge> edges) {
|
||||
Map<String, Integer> edgeCountMap = new HashMap<>();
|
||||
for (Edge edge : edges) {
|
||||
String key = edge.getSource() + "-" + edge.getTarget(); // 使用 source 和 target 作为唯一标识
|
||||
edgeCountMap.put(key, edgeCountMap.getOrDefault(key, 0) + 1);
|
||||
if (edgeCountMap.get(key) > 1) {
|
||||
throw new IllegalArgumentException("节点 " + edge.getSource() + " 和节点 " + edge.getTarget() + " 之间存在多根连线");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 检查节点的关系信息
|
||||
*/
|
||||
private void validateNodeRelations(List<Node> nodes, List<Edge> edges) {
|
||||
// 构建 source 和 target 的映射
|
||||
Map<Long, List<Edge>> sourceMap = edges.stream().collect(Collectors.groupingBy(Edge::getSource));
|
||||
Map<Long, List<Edge>> targetMap = edges.stream().collect(Collectors.groupingBy(Edge::getTarget));
|
||||
|
||||
for (Node node : nodes) {
|
||||
NodeValidator validator = getNodeValidator(node);
|
||||
// 获取当前节点的 source 和 target
|
||||
List<Edge> sources = targetMap.getOrDefault(node.getId(), new ArrayList<>());
|
||||
List<Edge> targets = sourceMap.getOrDefault(node.getId(), new ArrayList<>());
|
||||
// 检查节点关系
|
||||
ValidatorResult result = validator.validateRelation(node, sources, targets);
|
||||
Assert.isTrue(result.getValid(), result.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取节点验证器
|
||||
*
|
||||
* @param node 节点
|
||||
* @return {@link NodeValidator }
|
||||
*/
|
||||
private NodeValidator getNodeValidator(Node node) {
|
||||
if (NodeType.CUSTOM_NODE.equals(node.getType())) {
|
||||
Assert.isTrue(StrUtil.isNotBlank(node.getCustomType()), "自定义节点类型不能为空");
|
||||
return NodeValidatorFactory.getCustom(node.getCustomType());
|
||||
}
|
||||
return NodeValidatorFactory.get(node.getType());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.metis.flow.validator.impl.edge;
|
||||
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.enums.EdgeType;
|
||||
import com.metis.flow.validator.EdgeValidator;
|
||||
import com.metis.flow.validator.ValidatorResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class DefaultEdgeValidator implements EdgeValidator {
|
||||
@Override
|
||||
public ValidatorResult validate(Edge edge) {
|
||||
return ValidatorResult.valid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public EdgeType getType() {
|
||||
return EdgeType.DEFAULT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.metis.flow.validator.impl.node;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.domain.entity.config.node.DocumentExtractorNodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.validator.NodeValidator;
|
||||
import com.metis.flow.validator.ValidatorCodeService;
|
||||
import com.metis.flow.validator.ValidatorResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class DocumentExtractorNodeValidator implements NodeValidator<DocumentExtractorNodeConfig> {
|
||||
private final ValidatorCodeService validatorCodeService;
|
||||
|
||||
@Override
|
||||
public ValidatorResult validateValue(Node node) {
|
||||
DocumentExtractorNodeConfig config = node.getConfig();
|
||||
validatorCodeService.validateThrow(config);
|
||||
// 业务检查未通过
|
||||
return ValidatorResult.invalid("业务报错");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidatorResult validateRelation(Node node, List<Edge> sources, List<Edge> targets) {
|
||||
// 1. 检查 targets 数量是否等于 1(只允许一个出)
|
||||
Assert.isTrue(targets.size() == 1, "节点 {} 的出连接数必须为 1,当前数量: {}", node.getId(), targets.size());
|
||||
|
||||
return ValidatorResult.valid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
return NodeType.DOCUMENT_EXTRACTOR;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.metis.flow.validator.impl.node;
|
||||
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.domain.entity.config.node.EndNodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.validator.NodeValidator;
|
||||
import com.metis.flow.validator.ValidatorResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class EndNodeValidator implements NodeValidator<EndNodeConfig> {
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public ValidatorResult validateValue(Node node) {
|
||||
return ValidatorResult.valid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ValidatorResult validateRelation(Node node, List<Edge> sources, List<Edge> targets) {
|
||||
// 1. 结束节点不允许有目标连接
|
||||
Assert.isTrue(targets.isEmpty(), "结束节点 {} 不允许有目标连接", node.getId());
|
||||
|
||||
// 2. 检查 sources 数量是否小于 handles 数量
|
||||
int handleCount = node.getData().getHandles().size();
|
||||
Assert.isTrue(sources.size() <= handleCount, "结束节点 {} 的源连接数超过 handles 数量", node.getId());
|
||||
|
||||
return ValidatorResult.valid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
return NodeType.END;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.metis.flow.validator.impl.node;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.metis.flow.domain.entity.base.Edge;
|
||||
import com.metis.flow.domain.entity.base.Node;
|
||||
import com.metis.flow.domain.entity.base.NodeVariable;
|
||||
import com.metis.flow.domain.entity.config.node.StartNodeConfig;
|
||||
import com.metis.flow.enums.NodeType;
|
||||
import com.metis.flow.validator.NodeValidator;
|
||||
import com.metis.flow.validator.ValidatorCodeService;
|
||||
import com.metis.flow.validator.ValidatorResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class StartNodeValidator implements NodeValidator<StartNodeConfig> {
|
||||
|
||||
private final ValidatorCodeService validatorCodeService;
|
||||
|
||||
|
||||
@Override
|
||||
public ValidatorResult validateValue(Node node) {
|
||||
StartNodeConfig config = node.getConfig();
|
||||
validatorCodeService.validateThrow(config);
|
||||
|
||||
List<NodeVariable> variables = config.getVariables();
|
||||
if (CollUtil.isEmpty(variables)) {
|
||||
return ValidatorResult.valid();
|
||||
}
|
||||
|
||||
for (NodeVariable variable : variables) {
|
||||
switch (variable.getVariableType()) {
|
||||
// 文本类型校验
|
||||
case TEXT_INPUT, PARAGRAPH -> textVariableValidator(variable);
|
||||
// 下拉框变量校验
|
||||
case SELECT -> selectVariableValidator(variable);
|
||||
// 文件变量校验
|
||||
case FILE, FILE_LIST -> fileVariableValidator(variable);
|
||||
}
|
||||
}
|
||||
return ValidatorResult.valid();
|
||||
}
|
||||
|
||||
/**
|
||||
* 文本变量验证器
|
||||
*
|
||||
* @param variable 变量
|
||||
*/
|
||||
private void textVariableValidator(NodeVariable variable) {
|
||||
Assert.isTrue(ObjectUtil.isNotNull(variable.getMaxLength()), "文本变量 {} {} 的最大长度不能为空", variable.getLabel(), variable.getLabel(), variable.getVariable());
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择变量验证器
|
||||
*
|
||||
* @param variable 变量
|
||||
*/
|
||||
private void selectVariableValidator(NodeVariable variable) {
|
||||
Assert.isTrue(CollUtil.isNotEmpty(variable.getOptions()), "选择变量 {} {} 的选项不能为空", variable.getLabel(), variable.getLabel(), variable.getVariable());
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件变量验证器
|
||||
*
|
||||
* @param variable 变量
|
||||
*/
|
||||
private void fileVariableValidator(NodeVariable variable) {
|
||||
Assert.isTrue(ObjectUtil.isNotNull(variable.getMaxLength()), "文本变量 {} {} 的最大长度不能为空", variable.getLabel(), variable.getVariable());
|
||||
Assert.isTrue(CollUtil.isNotEmpty(variable.getAllowedFileUploadMethods()), "文本变量 {} {} 的允许上传方式不能为空", variable.getLabel(), variable.getVariable());
|
||||
Assert.isTrue(CollUtil.isNotEmpty(variable.getAllowedFileTypes()) || CollUtil.isNotEmpty(variable.getAllowedFileExtensions()), "文本变量 {} 的允许上传文件类型不能为空", variable.getLabel(), variable.getVariable());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public ValidatorResult validateRelation(Node node, List<Edge> sources, List<Edge> targets) {
|
||||
// 1. 开始节点不允许有源连接
|
||||
Assert.isTrue(sources.isEmpty(), "开始节点 {} 不允许有源连接", node.getId());
|
||||
|
||||
// 2. 检查 targets 数量是否小于 handles 数量
|
||||
int handleCount = node.getData().getHandles().size();
|
||||
Assert.isTrue(targets.size() <= handleCount, "开始节点 {} 的目标连接数超过 handles 数量", node.getId());
|
||||
|
||||
return ValidatorResult.valid();
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeType getType() {
|
||||
return NodeType.START;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.metis.handle;
|
||||
|
||||
|
||||
import com.metis.result.Result;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
import javax.security.auth.login.AccountExpiredException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
|
||||
|
||||
/**
|
||||
* 全局异常处理器
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/30
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
public GlobalExceptionHandler() {
|
||||
log.info("开始初始化全局异常处理器");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 路径不存在
|
||||
*
|
||||
* @param e
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler(NoHandlerFoundException.class)
|
||||
public Result<Object> handlerNoFoundException(Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.notFound( "路径不存在,请检查路径是否正确");
|
||||
}
|
||||
|
||||
/**
|
||||
* 授权失败
|
||||
*
|
||||
* @param e
|
||||
* @return
|
||||
*/
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public Result<String> handleAuthorizationException(AccessDeniedException e) {
|
||||
log.error(e.getMessage());
|
||||
return Result.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权");
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccountExpiredException.class)
|
||||
public Result<String> handleAccountExpiredException(AccountExpiredException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(BindException.class)
|
||||
public Result<String> validatedBindException(BindException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
String message = e.getAllErrors().get(0).getDefaultMessage();
|
||||
return Result.error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义验证异常
|
||||
*/
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public Result<String> validExceptionHandler(MethodArgumentNotValidException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
String message = e.getBindingResult().getFieldError().getDefaultMessage();
|
||||
return Result.error(message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public Result<String> runtimeExceptionHandler(RuntimeException e) {
|
||||
log.error(e.getMessage(), e);
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package com.metis.mybatis;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import com.baomidou.mybatisplus.core.injector.ISqlInjector;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.metis.mybatis.handler.BaseEntityMetaObjectHandler;
|
||||
import com.metis.mybatis.support.CustomSqlInjector;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus配置
|
||||
*
|
||||
* @author ZhangQiang
|
||||
* @date 2024/09/24
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfiguration {
|
||||
|
||||
|
||||
/**
|
||||
* mybatis-plus 拦截器
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 配置分页拦截器
|
||||
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
|
||||
paginationInnerInterceptor.setOptimizeJoin(false);
|
||||
paginationInnerInterceptor.setMaxLimit(500L);
|
||||
paginationInnerInterceptor.setOverflow(false);
|
||||
interceptor.addInnerInterceptor(paginationInnerInterceptor);
|
||||
// 乐观锁插件
|
||||
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
|
||||
// 添加防止全表更新与删除插件
|
||||
interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
|
||||
return interceptor;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动填充字段信息
|
||||
*/
|
||||
@Bean
|
||||
public MetaObjectHandler metaObjectHandler() {
|
||||
return new BaseEntityMetaObjectHandler();
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义SQL注入器
|
||||
*
|
||||
* @return 自定义SQL注入器
|
||||
*/
|
||||
@Bean
|
||||
public ISqlInjector iSqlInjector() {
|
||||
return new CustomSqlInjector();
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.metis.mybatis.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 基础实体-自动填充处理器
|
||||
*
|
||||
* @author ZhangQiang
|
||||
* @date 2024/09/29
|
||||
*/
|
||||
@Slf4j
|
||||
public class BaseEntityMetaObjectHandler implements MetaObjectHandler {
|
||||
|
||||
|
||||
/**
|
||||
* 插入元对象字段填充(用于插入时对公共字段的填充)
|
||||
*
|
||||
* @param metaObject 元对象
|
||||
*/
|
||||
@Override
|
||||
public void insertFill(MetaObject metaObject) {
|
||||
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
|
||||
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
this.strictInsertFill(metaObject, "isDeleted", Integer.class, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新元对象字段填充(用于更新时对公共字段的填充)
|
||||
*
|
||||
* @param metaObject 元对象
|
||||
*/
|
||||
@Override
|
||||
public void updateFill(MetaObject metaObject) {
|
||||
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.metis.mybatis.logic;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.enums.SqlMethod;
|
||||
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
|
||||
import org.apache.ibatis.mapping.MappedStatement;
|
||||
import org.apache.ibatis.mapping.SqlSource;
|
||||
|
||||
/**
|
||||
* 选择忽略逻辑删除
|
||||
*
|
||||
* @author clay
|
||||
* @date 2024/12/02
|
||||
*/
|
||||
public class SelectIgnoreLogicDelete extends AbstractMethod {
|
||||
public SelectIgnoreLogicDelete() {
|
||||
super("SelectIgnoreLogicDelete");
|
||||
}
|
||||
|
||||
@Override
|
||||
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
|
||||
SqlMethod sqlMethod = SqlMethod.SELECT_LIST;
|
||||
String sql = String.format(sqlMethod.getSql(),
|
||||
StrUtil.EMPTY,
|
||||
sqlFirst(),
|
||||
sqlSelectColumns(tableInfo, true),
|
||||
tableInfo.getTableName(),
|
||||
sqlWhereEntityWrapper(true, tableInfo),
|
||||
sqlComment());
|
||||
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
|
||||
return this.addSelectMappedStatementForTable(mapperClass, "selectIgnoreLogicDelete", sqlSource, tableInfo);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String sqlWhereEntityWrapper(boolean newLine, TableInfo table) {
|
||||
String sqlScript = table.getAllSqlWhere(false, true, true, WRAPPER_ENTITY_DOT);
|
||||
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER_ENTITY), true);
|
||||
sqlScript += NEWLINE;
|
||||
sqlScript += SqlScriptUtils.convertIf(String.format(SqlScriptUtils.convertIf(" AND", String.format("%s and %s", WRAPPER_NONEMPTYOFENTITY, WRAPPER_NONEMPTYOFNORMAL), false) + " ${%s}", WRAPPER_SQLSEGMENT),
|
||||
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
|
||||
WRAPPER_NONEMPTYOFWHERE), true);
|
||||
sqlScript = SqlScriptUtils.convertWhere(sqlScript) + NEWLINE;
|
||||
sqlScript += SqlScriptUtils.convertIf(String.format(" ${%s}", WRAPPER_SQLSEGMENT),
|
||||
String.format("%s != null and %s != '' and %s", WRAPPER_SQLSEGMENT, WRAPPER_SQLSEGMENT,
|
||||
WRAPPER_EMPTYOFWHERE), true);
|
||||
sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", WRAPPER), true);
|
||||
return newLine ? NEWLINE + sqlScript : sqlScript;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.metis.mybatis.support;
|
||||
|
||||
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
|
||||
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
|
||||
import com.baomidou.mybatisplus.core.metadata.TableInfo;
|
||||
import com.metis.mybatis.logic.SelectIgnoreLogicDelete;
|
||||
import org.apache.ibatis.session.Configuration;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 自定义SQL注入器
|
||||
*
|
||||
* @author clay
|
||||
* @date 2024/12/02
|
||||
*/
|
||||
public class CustomSqlInjector extends DefaultSqlInjector {
|
||||
@Override
|
||||
public List<AbstractMethod> getMethodList(Configuration configuration, Class<?> mapperClass, TableInfo tableInfo) {
|
||||
List<AbstractMethod> methodList = super.getMethodList(configuration, mapperClass, tableInfo);
|
||||
// 查询所有数据(包括已经逻辑删除数据)
|
||||
methodList.add(new SelectIgnoreLogicDelete());
|
||||
return methodList;
|
||||
}
|
||||
}
|
||||
149
metis-starter/src/main/java/com/metis/result/Result.java
Normal file
149
metis-starter/src/main/java/com/metis/result/Result.java
Normal file
@@ -0,0 +1,149 @@
|
||||
package com.metis.result;
|
||||
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||
import com.metis.enums.ResultEnum;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 返回结果集
|
||||
*
|
||||
* @author: Clay
|
||||
* @date: 2022/5/31 10:00
|
||||
*/
|
||||
public class Result<T> implements Serializable {
|
||||
private Integer code;
|
||||
private String msg;
|
||||
private T data;
|
||||
private transient HttpStatus status;
|
||||
|
||||
|
||||
public Result() {
|
||||
}
|
||||
|
||||
public Result(Integer code, String msg, T data, HttpStatus status) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
this.data = data;
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(Integer code, String msg, T data) {
|
||||
return new Result<>(code, msg, data, HttpStatus.OK);
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(Integer code, String msg, T data, HttpStatus status) {
|
||||
return new Result<>(code, msg, data, status);
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(String msg, T data) {
|
||||
return Result.ok(ResultEnum.SUCCESS.code, msg, data, ResultEnum.SUCCESS.status);
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(Integer code, T data) {
|
||||
return Result.ok(code, ResultEnum.SUCCESS.msg, data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(String msg) {
|
||||
return ok(ResultEnum.SUCCESS.code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok(T data) {
|
||||
return Result.ok(ResultEnum.SUCCESS.msg, data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> ok() {
|
||||
return Result.ok(ResultEnum.SUCCESS.msg, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String msg, T data) {
|
||||
return Result.error(ResultEnum.ERROR.code, msg, data);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String msg) {
|
||||
return Result.error(code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> notFound(String msg) {
|
||||
return Result.error(HttpStatus.NOT_FOUND.value(), msg, null, HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String msg, T data) {
|
||||
return new Result<>(code, msg, data, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(Integer code, String msg, T data, HttpStatus status) {
|
||||
return new Result<>(code, msg, data, status);
|
||||
}
|
||||
|
||||
public static <T> Result<T> unauthorized(String msg) {
|
||||
return new Result<>(HttpStatus.UNAUTHORIZED.value(), msg, null, HttpStatus.UNAUTHORIZED);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(HttpStatus status, String msg) {
|
||||
return new Result<>(status.value(), msg, null, status);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(ResultEnum resultEnum) {
|
||||
return Result.error(resultEnum.code, resultEnum.msg, null, resultEnum.status);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error(String msg) {
|
||||
return Result.error(ResultEnum.ERROR.code, msg, null, ResultEnum.ERROR.status);
|
||||
}
|
||||
|
||||
public static <T> Result<T> error() {
|
||||
return Result.error(ResultEnum.ERROR.code, ResultEnum.ERROR.msg, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> info(String msg) {
|
||||
return Result.ok(ResultEnum.NO_DATA.code, msg, null);
|
||||
}
|
||||
|
||||
public static <T> Result<T> info(ResultEnum resultEnum) {
|
||||
return Result.error(resultEnum.code, resultEnum.msg, null, resultEnum.status);
|
||||
}
|
||||
|
||||
public static <T> Result<T> noData() {
|
||||
return Result.ok(ResultEnum.NO_DATA.code, ResultEnum.NO_DATA.msg, null);
|
||||
}
|
||||
|
||||
|
||||
public Integer getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(Integer code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getMsg() {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public void setMsg(String msg) {
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||
public T getData() {
|
||||
return data;
|
||||
}
|
||||
|
||||
public void setData(T data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public HttpStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
public void setStatus(HttpStatus status) {
|
||||
this.status = status;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.metis.result.page;
|
||||
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 表格分页数据对象
|
||||
*
|
||||
* @author binlin
|
||||
*/
|
||||
@Data
|
||||
public class TableDataInfo<T> implements Serializable {
|
||||
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private long total;
|
||||
|
||||
/**
|
||||
* 列表数据
|
||||
*/
|
||||
private List<T> rows;
|
||||
|
||||
/**
|
||||
* 表格数据对象
|
||||
*/
|
||||
public TableDataInfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页
|
||||
*
|
||||
* @param list 列表数据
|
||||
* @param total 总记录数
|
||||
*/
|
||||
public TableDataInfo(List<T> list, int total) {
|
||||
this.rows = list;
|
||||
this.total = total;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package com.metis.sseclient;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.model.chat.request.json.JsonArraySchema;
|
||||
import dev.langchain4j.model.chat.request.json.JsonBooleanSchema;
|
||||
import dev.langchain4j.model.chat.request.json.JsonEnumSchema;
|
||||
import dev.langchain4j.model.chat.request.json.JsonIntegerSchema;
|
||||
import dev.langchain4j.model.chat.request.json.JsonNumberSchema;
|
||||
import dev.langchain4j.model.chat.request.json.JsonObjectSchema;
|
||||
import dev.langchain4j.model.chat.request.json.JsonSchemaElement;
|
||||
import dev.langchain4j.model.chat.request.json.JsonStringSchema;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class ToolSpecificationHelper {
|
||||
|
||||
/**
|
||||
* Converts the 'tools' element from a ListToolsResult MCP message
|
||||
* to a list of ToolSpecification objects.
|
||||
*/
|
||||
public static List<ToolSpecification> toolSpecificationListFromMcpResponse(ArrayNode array) {
|
||||
List<ToolSpecification> result = new ArrayList<>();
|
||||
for (JsonNode tool : array) {
|
||||
final ToolSpecification.Builder builder = ToolSpecification.builder();
|
||||
builder.name(tool.get("name").asText());
|
||||
if (tool.has("description")) {
|
||||
builder.description(tool.get("description").asText());
|
||||
}
|
||||
builder.parameters((JsonObjectSchema) jsonNodeToJsonSchemaElement(tool.get("inputSchema")));
|
||||
result.add(builder.build());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts the 'inputSchema' element (inside the 'Tool' type in the MCP schema)
|
||||
* to a JsonSchemaElement object that describes the tool's arguments.
|
||||
*/
|
||||
public static JsonSchemaElement jsonNodeToJsonSchemaElement(JsonNode node) {
|
||||
String nodeType = node.get("type").asText();
|
||||
if (nodeType.equals("object")) {
|
||||
JsonObjectSchema.Builder builder = JsonObjectSchema.builder();
|
||||
JsonNode required = node.get("required");
|
||||
if (required != null) {
|
||||
builder.required(toStringArray((ArrayNode) required));
|
||||
}
|
||||
if (node.has("additionalProperties")) {
|
||||
builder.additionalProperties(node.get("additionalProperties").asBoolean(false));
|
||||
}
|
||||
JsonNode description = node.get("description");
|
||||
if (description != null) {
|
||||
builder.description(description.asText());
|
||||
}
|
||||
JsonNode properties = node.get("properties");
|
||||
if (properties != null) {
|
||||
ObjectNode propertiesObject = (ObjectNode) properties;
|
||||
for (Map.Entry<String, JsonNode> property : propertiesObject.properties()) {
|
||||
builder.addProperty(property.getKey(), jsonNodeToJsonSchemaElement(property.getValue()));
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
} else if (nodeType.equals("string")) {
|
||||
if (node.has("enum")) {
|
||||
JsonEnumSchema.Builder builder = JsonEnumSchema.builder();
|
||||
if (node.has("description")) {
|
||||
builder.description(node.get("description").asText());
|
||||
}
|
||||
builder.enumValues(toStringArray((ArrayNode) node.get("enum")));
|
||||
return builder.build();
|
||||
} else {
|
||||
JsonStringSchema.Builder builder = JsonStringSchema.builder();
|
||||
if (node.has("description")) {
|
||||
builder.description(node.get("description").asText());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
} else if (nodeType.equals("number")) {
|
||||
JsonNumberSchema.Builder builder = JsonNumberSchema.builder();
|
||||
if (node.has("description")) {
|
||||
builder.description(node.get("description").asText());
|
||||
}
|
||||
return builder.build();
|
||||
} else if (nodeType.equals("integer")) {
|
||||
JsonIntegerSchema.Builder builder = JsonIntegerSchema.builder();
|
||||
if (node.has("description")) {
|
||||
builder.description(node.get("description").asText());
|
||||
}
|
||||
return builder.build();
|
||||
} else if (nodeType.equals("boolean")) {
|
||||
JsonBooleanSchema.Builder builder = JsonBooleanSchema.builder();
|
||||
if (node.has("description")) {
|
||||
builder.description(node.get("description").asText());
|
||||
}
|
||||
return builder.build();
|
||||
} else if (nodeType.equals("array")) {
|
||||
JsonArraySchema.Builder builder = JsonArraySchema.builder();
|
||||
if (node.has("description")) {
|
||||
builder.description(node.get("description").asText());
|
||||
}
|
||||
builder.items(jsonNodeToJsonSchemaElement(node.get("items")));
|
||||
return builder.build();
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unknown element type: " + nodeType);
|
||||
}
|
||||
}
|
||||
|
||||
private static String[] toStringArray(ArrayNode jsonArray) {
|
||||
String[] result = new String[jsonArray.size()];
|
||||
for (int i = 0; i < jsonArray.size(); i++) {
|
||||
result[i] = jsonArray.get(i).asText();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.metis.sseclient.check;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.metis.sseclient.ToolSpecificationHelper;
|
||||
import com.metis.sseclient.event.SseEventListener;
|
||||
import com.metis.sseclient.handler.McpOperationHandler;
|
||||
import dev.langchain4j.agent.tool.ToolSpecification;
|
||||
import dev.langchain4j.mcp.client.protocol.*;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.*;
|
||||
import okhttp3.sse.EventSource;
|
||||
import okhttp3.sse.EventSources;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
|
||||
@Slf4j
|
||||
public class SseCheck {
|
||||
private final String sseUrl;
|
||||
private volatile String postUrl;
|
||||
private final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private final AtomicLong idGenerator = new AtomicLong(0);
|
||||
private final OkHttpClient client;
|
||||
private EventSource eventSource;
|
||||
private final String clientVersion = "1.0";
|
||||
private final String clientName = "langchain4j";
|
||||
private final String protocolVersion = "2024-11-05";
|
||||
|
||||
|
||||
public SseCheck(String sseUrl) {
|
||||
this.sseUrl = sseUrl;
|
||||
OkHttpClient.Builder httpClientBuilder = getHttpClientBuilder();
|
||||
this.client = httpClientBuilder.build();
|
||||
initialize();
|
||||
}
|
||||
|
||||
public void start() {
|
||||
Map<Long, CompletableFuture<JsonNode>> pendingOperations = new ConcurrentHashMap<>();
|
||||
Request request = new Request.Builder().url(sseUrl).build();
|
||||
CompletableFuture<String> initializationFinished = new CompletableFuture<>();
|
||||
McpOperationHandler operationHandler = new McpOperationHandler(pendingOperations);
|
||||
SseEventListener listener = new SseEventListener(operationHandler, true, initializationFinished);
|
||||
this.eventSource = EventSources.createFactory(client).newEventSource(request, listener);
|
||||
// wait for the SSE channel to be created, receive the POST url from the server, throw an exception if that
|
||||
// failed
|
||||
try {
|
||||
int timeout = client.callTimeoutMillis() > 0 ? client.callTimeoutMillis() : Integer.MAX_VALUE;
|
||||
String relativePostUrl = initializationFinished.get(timeout, TimeUnit.MILLISECONDS);
|
||||
postUrl = buildAbsolutePostUrl(relativePostUrl);
|
||||
log.debug("Received the server's POST URL: {}", postUrl);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private OkHttpClient.Builder getHttpClientBuilder() {
|
||||
OkHttpClient.Builder httpClientBuilder = new OkHttpClient.Builder();
|
||||
Duration timeout = Duration.ofSeconds(60);
|
||||
httpClientBuilder.callTimeout(timeout);
|
||||
httpClientBuilder.connectTimeout(timeout);
|
||||
httpClientBuilder.readTimeout(timeout);
|
||||
httpClientBuilder.writeTimeout(timeout);
|
||||
return httpClientBuilder;
|
||||
}
|
||||
|
||||
private String buildAbsolutePostUrl(String relativePostUrl) {
|
||||
try {
|
||||
return URI.create(sseUrl).resolve(relativePostUrl).toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void initialize() {
|
||||
start();
|
||||
long operationId = idGenerator.getAndIncrement();
|
||||
McpInitializeRequest request = new McpInitializeRequest(operationId);
|
||||
InitializeParams params = createInitializeParams();
|
||||
request.setParams(params);
|
||||
try {
|
||||
Request httpRequest = null;
|
||||
Request initializationNotification = null;
|
||||
try {
|
||||
httpRequest = createRequest(request);
|
||||
initializationNotification = createRequest(new InitializationNotification());
|
||||
} catch (JsonProcessingException e) {
|
||||
return;
|
||||
}
|
||||
final Request finalInitializationNotification = initializationNotification;
|
||||
CompletableFuture<JsonNode> future = execute(httpRequest, operationId)
|
||||
.thenCompose(originalResponse -> execute(finalInitializationNotification, null)
|
||||
.thenCompose(nullNode -> CompletableFuture.completedFuture(originalResponse)));
|
||||
JsonNode jsonNode = future.get();
|
||||
// log.debug("MCP server capabilities: {}", capabilities.get("result"));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private InitializeParams createInitializeParams() {
|
||||
InitializeParams params = new InitializeParams();
|
||||
params.setProtocolVersion(protocolVersion);
|
||||
|
||||
InitializeParams.ClientInfo clientInfo = new InitializeParams.ClientInfo();
|
||||
clientInfo.setName(clientName);
|
||||
clientInfo.setVersion(clientVersion);
|
||||
params.setClientInfo(clientInfo);
|
||||
|
||||
InitializeParams.Capabilities capabilities = new InitializeParams.Capabilities();
|
||||
InitializeParams.Capabilities.Roots roots = new InitializeParams.Capabilities.Roots();
|
||||
roots.setListChanged(false); // TODO: listChanged is not supported yet
|
||||
capabilities.setRoots(roots);
|
||||
params.setCapabilities(capabilities);
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
public List<ToolSpecification> listTools() throws JsonProcessingException {
|
||||
McpListToolsRequest operation = new McpListToolsRequest(idGenerator.getAndIncrement());
|
||||
Request request = createRequest(operation);
|
||||
CompletableFuture<JsonNode> resultFuture = execute(request, idGenerator.getAndIncrement());
|
||||
JsonNode result = null;
|
||||
try {
|
||||
result = resultFuture.get();
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return ToolSpecificationHelper.toolSpecificationListFromMcpResponse(
|
||||
(ArrayNode) result.get("result").get("tools"));
|
||||
}
|
||||
|
||||
private Request createRequest(McpClientMessage message) throws JsonProcessingException {
|
||||
return new Request.Builder()
|
||||
.url(postUrl)
|
||||
.header("Content-Type", "application/json")
|
||||
.post(RequestBody.create(OBJECT_MAPPER.writeValueAsBytes(message)))
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
private CompletableFuture<JsonNode> execute(Request request, Long id) {
|
||||
CompletableFuture<JsonNode> future = new CompletableFuture<>();
|
||||
try {
|
||||
Response execute = client.newCall(request).execute();
|
||||
|
||||
System.out.println(execute);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
client.newCall(request).enqueue(new Callback() {
|
||||
@Override
|
||||
public void onFailure(Call call, IOException e) {
|
||||
future.completeExceptionally(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(Call call, Response response) throws IOException {
|
||||
int statusCode = response.code();
|
||||
if (!isExpectedStatusCode(statusCode)) {
|
||||
future.completeExceptionally(new RuntimeException("Unexpected status code: " + statusCode));
|
||||
}
|
||||
// For messages with null ID, we don't wait for a response in the SSE channel
|
||||
if (id == null) {
|
||||
future.complete(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
|
||||
private boolean isExpectedStatusCode(int statusCode) {
|
||||
return statusCode >= 200 && statusCode < 300;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.metis.sseclient.event;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.metis.sseclient.handler.McpOperationHandler;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.sse.EventSource;
|
||||
import okhttp3.sse.EventSourceListener;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
public class SseEventListener extends EventSourceListener {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
private final boolean logEvents;
|
||||
// this will contain the POST url for sending commands to the server
|
||||
private final CompletableFuture<String> initializationFinished;
|
||||
private final McpOperationHandler messageHandler;
|
||||
|
||||
public SseEventListener(
|
||||
McpOperationHandler messageHandler, boolean logEvents, CompletableFuture initializationFinished) {
|
||||
this.messageHandler = messageHandler;
|
||||
this.logEvents = logEvents;
|
||||
this.initializationFinished = initializationFinished;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClosed(EventSource eventSource) {
|
||||
log.debug("SSE channel closed");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(EventSource eventSource, String id, String type, String data) {
|
||||
if (type.equals("message")) {
|
||||
if (logEvents) {
|
||||
log.info("< {}", data);
|
||||
}
|
||||
try {
|
||||
JsonNode jsonNode = OBJECT_MAPPER.readTree(data);
|
||||
messageHandler.handle(jsonNode);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.warn("Failed to parse JSON message: {}", data, e);
|
||||
}
|
||||
} else if (type.equals("endpoint")) {
|
||||
if (initializationFinished.isDone()) {
|
||||
log.warn("Received endpoint event after initialization");
|
||||
return;
|
||||
}
|
||||
initializationFinished.complete(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(EventSource eventSource, Throwable t, Response response) {
|
||||
if (!initializationFinished.isDone()) {
|
||||
if (t != null) {
|
||||
initializationFinished.completeExceptionally(t);
|
||||
} else if (response != null) {
|
||||
initializationFinished.completeExceptionally(
|
||||
new RuntimeException("The server returned: " + response.message()));
|
||||
}
|
||||
}
|
||||
if (t != null && (t.getMessage() == null || !t.getMessage().contains("Socket closed"))) {
|
||||
log.warn("SSE channel failure", t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen(EventSource eventSource, Response response) {
|
||||
log.debug("Connected to SSE channel at {}", response.request().url());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.metis.sseclient.handler;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Handles incoming messages from the MCP server. Transport implementations
|
||||
* should call the "handle" method on each received message. A transport also has
|
||||
* to call "startOperation" when before starting an operation that requires a response
|
||||
* to register its ID in the map of pending operations.
|
||||
*/
|
||||
public class McpOperationHandler {
|
||||
|
||||
private final Map<Long, CompletableFuture<JsonNode>> pendingOperations;
|
||||
private static final Logger log = LoggerFactory.getLogger(McpOperationHandler.class);
|
||||
|
||||
public McpOperationHandler(
|
||||
Map<Long, CompletableFuture<JsonNode>> pendingOperations) {
|
||||
this.pendingOperations = pendingOperations;
|
||||
}
|
||||
|
||||
public void handle(JsonNode message) {
|
||||
if (message.has("id")) {
|
||||
long messageId = message.get("id").asLong();
|
||||
CompletableFuture<JsonNode> op = pendingOperations.remove(messageId);
|
||||
if (op != null) {
|
||||
op.complete(message);
|
||||
} else {
|
||||
if (message.has("method")) {
|
||||
String method = message.get("method").asText();
|
||||
if (method.equals("ping")) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
log.warn("Received response for unknown message id: {}", messageId);
|
||||
}
|
||||
} else if (message.has("method") && message.get("method").asText().equals("notifications/message")) {
|
||||
// this is a log message
|
||||
if (message.has("params")) {
|
||||
log.info("Received log message: {}", message);
|
||||
} else {
|
||||
log.warn("Received log message without params: {}", message);
|
||||
}
|
||||
} else {
|
||||
log.warn("Received unknown message: {}", message);
|
||||
}
|
||||
}
|
||||
|
||||
public void startOperation(Long id, CompletableFuture<JsonNode> future) {
|
||||
pendingOperations.put(id, future);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.metis.utils;
|
||||
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Type;
|
||||
|
||||
public class GenericInterfacesUtils {
|
||||
|
||||
|
||||
public static <T> Class<T> getClass(Object object) {
|
||||
|
||||
Type[] genericInterfaces = object.getClass().getGenericInterfaces();
|
||||
for (Type genericInterface : genericInterfaces) {
|
||||
if (genericInterface instanceof ParameterizedType) {
|
||||
ParameterizedType parameterizedType = (ParameterizedType) genericInterface;
|
||||
Type[] typeArguments = parameterizedType.getActualTypeArguments();
|
||||
if (typeArguments.length > 0) {
|
||||
return (Class<T>) typeArguments[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new IllegalStateException("无法获取泛型类型");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,399 @@
|
||||
package com.metis.utils;
|
||||
|
||||
import java.time.*;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.time.temporal.Temporal;
|
||||
import java.time.temporal.TemporalAdjusters;
|
||||
import java.time.temporal.TemporalUnit;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 地方日期时间工具类
|
||||
*
|
||||
* @author ZhangQiang
|
||||
* @date 2024/10/28
|
||||
*/
|
||||
public class LocalDateTimeUtils extends cn.hutool.core.date.LocalDateTimeUtil {
|
||||
|
||||
/**
|
||||
* 在范围内
|
||||
*
|
||||
* @param target 目标
|
||||
* @param start 开始
|
||||
* @param end 结束
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isWithinRange(LocalDateTime target, LocalDateTime start, LocalDateTime end) {
|
||||
return !target.isBefore(start) && !target.isAfter(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 现在时间在范围内
|
||||
*
|
||||
* @param start 开始
|
||||
* @param end 结束
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean nowIsWithinRange(LocalDateTime start, LocalDateTime end) {
|
||||
LocalDateTime target = LocalDateTime.now();
|
||||
return !target.isBefore(start) && !target.isAfter(end);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间是周几
|
||||
*
|
||||
* @param time 时间
|
||||
* @return int
|
||||
*/
|
||||
public static int week(LocalDateTime time) {
|
||||
return time.getDayOfWeek().getValue();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取加或减N月的第一天
|
||||
*
|
||||
* @param num 数字
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime monthFirst(int num) {
|
||||
LocalDateTime newTime = plus(LocalDateTime.now(), num, ChronoUnit.MONTHS);
|
||||
newTime = newTime.with(TemporalAdjusters.firstDayOfMonth());
|
||||
return getDayStart(newTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加或减N月的最后天
|
||||
*
|
||||
* @param num 数字
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime monthLast(int num) {
|
||||
LocalDateTime newTime = plus(LocalDateTime.now(), num, ChronoUnit.MONTHS);
|
||||
newTime = newTime.with(TemporalAdjusters.lastDayOfMonth());
|
||||
return getDayEnd(newTime);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取加或减N周的第一天
|
||||
*
|
||||
* @param num 数字
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime weekFirst(int num) {
|
||||
int week = week(LocalDateTime.now());
|
||||
LocalDateTime newTime = subtract(LocalDateTime.now(), week - 1, ChronoUnit.DAYS);
|
||||
newTime = plus(newTime, num * 7L, ChronoUnit.DAYS);
|
||||
return getDayStart(newTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取加或减N周的最后一天
|
||||
*
|
||||
* @param num 数字
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime weekLast(int num) {
|
||||
int week = week(LocalDateTime.now());
|
||||
LocalDateTime newTime = plus(LocalDateTime.now(), 7 - week, ChronoUnit.DAYS);
|
||||
newTime = plus(newTime, num * 7L, ChronoUnit.DAYS);
|
||||
return getDayEnd(newTime);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断时间 ==> t1 < t2 = true (2019-10-13 11:11:00 < 2020-11-13 13:13:00 = true)
|
||||
*
|
||||
* @param t1 t1
|
||||
* @param t2 t2
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isBefore(LocalDateTime t1, LocalDateTime t2) {
|
||||
return t1.isBefore(t2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断时间 ==> t1 > t2 = true(2019-10-13 11:11:00 > 2020-11-13 13:13:00 = false)
|
||||
*
|
||||
* @param t1 t1
|
||||
* @param t2 t2
|
||||
* @return boolean
|
||||
*/
|
||||
public static boolean isAfter(LocalDateTime t1, LocalDateTime t2) {
|
||||
return t1.isAfter(t2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Date 转 LocalDateTime
|
||||
*
|
||||
* @param date 日期
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime convertToLocalDateTime(Date date) {
|
||||
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
/**
|
||||
* LocalDateTime 转 Date
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link Date}
|
||||
*/
|
||||
public static Date convertToDate(LocalDateTime time) {
|
||||
return Date.from(time.atZone(ZoneId.systemDefault()).toInstant());
|
||||
}
|
||||
|
||||
/**
|
||||
* Date转LocalDate
|
||||
*
|
||||
* @param date 日期
|
||||
* @return {@link LocalDate}
|
||||
*/
|
||||
public static LocalDate convertLocalDate(Date date) {
|
||||
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Date转LocalDate
|
||||
*
|
||||
* @param date 日期
|
||||
* @return {@link LocalTime}
|
||||
*/
|
||||
public static LocalTime convertLocalTime(Date date) {
|
||||
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalTime();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取指定日期的毫秒
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link Long}
|
||||
*/
|
||||
public static Long getMilliByTime(LocalDateTime time) {
|
||||
return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取秒数通过时间
|
||||
* 获取指定日期的秒
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link Long}
|
||||
*/
|
||||
public static Long getSecondsByTime(LocalDateTime time) {
|
||||
return time.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定时间的指定格式 ==> yyyy-MM-dd HH:mm:ss:SSS (HH是24小时制,而hh是12小时制, ss是秒,SSS是毫秒)
|
||||
*
|
||||
* @param time 时间
|
||||
* @param pattern 图案
|
||||
* @return {@link String}
|
||||
*/
|
||||
public static String formatTime(LocalDateTime time, String pattern) {
|
||||
return time.format(DateTimeFormatter.ofPattern(pattern));
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期加上一个数,根据field不同加不同值,field为ChronoUnit.*
|
||||
*
|
||||
* @param time 时间
|
||||
* @param number 数
|
||||
* @param field 领域
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime plus(LocalDateTime time, long number, TemporalUnit field) {
|
||||
return time.plus(number, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 减去
|
||||
* 日期减去一个数,根据field不同减不同值,field参数为ChronoUnit.*
|
||||
*
|
||||
* @param time 时间
|
||||
* @param number 数
|
||||
* @param field 领域
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime subtract(LocalDateTime time, long number, TemporalUnit field) {
|
||||
return time.minus(number, field);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取白天开始
|
||||
* 获取指定某一天的开始时间 00:00:00
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime getDayStart(LocalDateTime time) {
|
||||
return time.withHour(0)
|
||||
.withMinute(0)
|
||||
.withSecond(0)
|
||||
.withNano(0);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取白天结束
|
||||
* 获取指定某一天的结束时间 23:59:59.999
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime getDayEnd(LocalDateTime time) {
|
||||
return time.withHour(23)
|
||||
.withMinute(59)
|
||||
.withSecond(59)
|
||||
.withNano(999999999);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本周周一
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime getWeekOfFirst(LocalDateTime time) {
|
||||
return time.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)).
|
||||
plusDays(1).withHour(0).withMinute(0).withSecond(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本周周日
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime getWeekOfLast(LocalDateTime time) {
|
||||
return time.with(TemporalAdjusters.next(DayOfWeek.MONDAY)).
|
||||
minusDays(1).withHour(23).withMinute(59).withSecond(59);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本月第一天
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime getMonthOfFirst(LocalDateTime time) {
|
||||
LocalDateTime firsthand = time.with(TemporalAdjusters.firstDayOfMonth());
|
||||
return LocalDateTime.of(firsthand.toLocalDate(), LocalTime.MIN);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取本月最后一天
|
||||
*
|
||||
* @param time 时间
|
||||
* @return {@link LocalDateTime}
|
||||
*/
|
||||
public static LocalDateTime getMonthOfLast(LocalDateTime time) {
|
||||
LocalDateTime lastDay = time.with(TemporalAdjusters.lastDayOfMonth());
|
||||
return LocalDateTime.of(lastDay.toLocalDate(), LocalTime.MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期相隔天数
|
||||
*
|
||||
* @param startDateInclusive 开始日期(含)
|
||||
* @param endDateExclusive 结束日期除外
|
||||
* @return int
|
||||
*/
|
||||
public static int periodDays(LocalDate startDateInclusive, LocalDate endDateExclusive) {
|
||||
return Period.between(startDateInclusive, endDateExclusive).getDays();
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期相隔小时
|
||||
*
|
||||
* @param startInclusive 开始包容性
|
||||
* @param endExclusive 结束独家
|
||||
* @return long
|
||||
*/
|
||||
public static long durationHours(Temporal startInclusive, Temporal endExclusive) {
|
||||
return Duration.between(startInclusive, endExclusive).toHours();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 日期相隔分钟
|
||||
*
|
||||
* @param startInclusive 开始包容性
|
||||
* @param endExclusive 结束独家
|
||||
* @return long
|
||||
*/
|
||||
public static long durationMinutes(Temporal startInclusive, Temporal endExclusive) {
|
||||
return Duration.between(startInclusive, endExclusive).toMinutes();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 持续时间秒
|
||||
*
|
||||
* @param startInclusive 开始包容
|
||||
* @param endExclusive 结束独家
|
||||
* @return long
|
||||
*/
|
||||
public static long durationSeconds(Temporal startInclusive, Temporal endExclusive) {
|
||||
return Duration.between(startInclusive, endExclusive).toSeconds();
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期相隔毫秒数
|
||||
*
|
||||
* @param startInclusive 开始包容性
|
||||
* @param endExclusive 结束独家
|
||||
* @return long
|
||||
*/
|
||||
public static long durationMillis(Temporal startInclusive, Temporal endExclusive) {
|
||||
return Duration.between(startInclusive, endExclusive).toMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否当天
|
||||
*/
|
||||
public static boolean isToday(LocalDate date) {
|
||||
return LocalDate.now().equals(date);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断两个时间是否相差指定时间(精确到毫秒)
|
||||
*
|
||||
* @param time1 第一个时间点
|
||||
* @param time2 第二个时间点
|
||||
* @param milliseconds 指定时间差(毫秒)
|
||||
* @return true 如果两个时间相差超过指定毫秒数,否则 false
|
||||
*/
|
||||
public static boolean isTimeDifferenceExceeds(LocalDateTime time1, LocalDateTime time2, long milliseconds) {
|
||||
if (time1 == null || time2 == null) {
|
||||
throw new IllegalArgumentException("Time arguments cannot be null");
|
||||
}
|
||||
|
||||
Duration duration = Duration.between(time1, time2);
|
||||
return Math.abs(duration.toMillis()) > milliseconds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定时间是否与当前时间相差超过 24 小时(精确到毫秒)
|
||||
*
|
||||
* @param targetTime 指定的 LocalDateTime 时间
|
||||
* @return true 如果相差 24 小时以上,否则 false
|
||||
*/
|
||||
public static boolean isMoreThan24HoursApart(LocalDateTime targetTime) {
|
||||
if (targetTime == null) {
|
||||
throw new IllegalArgumentException("Target time cannot be null");
|
||||
}
|
||||
// 比较时间差的绝对值是否大于 24 小时
|
||||
return isTimeDifferenceExceeds(LocalDateTime.now(), targetTime, 86400000L);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.metis.utils;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.metis.result.page.TableDataInfo;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2023-05-25
|
||||
*/
|
||||
public class PageConditionUtil {
|
||||
|
||||
|
||||
/**
|
||||
* 转换为TableDataInfo对象
|
||||
*
|
||||
* @param page 源对象
|
||||
* @param map 转换方法
|
||||
* @param <T> 转换后的对象类型
|
||||
* @param <R> 需要转换的对象类型
|
||||
* @return 转换后的对象
|
||||
*/
|
||||
public static <T, R> TableDataInfo<T> convertDataTable(IPage<R> page, Function<R, T> map) {
|
||||
List<T> convertList = page.getRecords().stream().map(map).collect(Collectors.toList());
|
||||
return convertDataTable(convertList, page.getTotal());
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为TableDataInfo对象
|
||||
*
|
||||
* @param page 源对象
|
||||
* @return 转换后的对象
|
||||
*/
|
||||
public static <T> TableDataInfo<T> convertDataTable(IPage<T> page) {
|
||||
return convertDataTable(page.getRecords(), page.getTotal());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 转换为TableDataInfo对象
|
||||
*
|
||||
* @param list
|
||||
* @param count
|
||||
* @param <T>
|
||||
* @return
|
||||
*/
|
||||
public static <T> TableDataInfo<T> convertDataTable(List<T> list, Long count) {
|
||||
if (null == list) {
|
||||
return new TableDataInfo<>(new ArrayList<>(), 0);
|
||||
}
|
||||
TableDataInfo<T> tableDataInfo = new TableDataInfo<>();
|
||||
tableDataInfo.setRows(list);
|
||||
tableDataInfo.setTotal(count);
|
||||
return tableDataInfo;
|
||||
}
|
||||
|
||||
public static <T> IPage<T> getPage() {
|
||||
PageInfo pageInfo = TableSupport.getPageInfo();
|
||||
return new Page<>(pageInfo.getPageNum(), pageInfo.getPageSize());
|
||||
}
|
||||
}
|
||||
44
metis-starter/src/main/java/com/metis/utils/PageInfo.java
Normal file
44
metis-starter/src/main/java/com/metis/utils/PageInfo.java
Normal file
@@ -0,0 +1,44 @@
|
||||
package com.metis.utils;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* @author Clay
|
||||
* @date 2022/10/30
|
||||
*/
|
||||
@Data
|
||||
public class PageInfo {
|
||||
|
||||
/**
|
||||
* 当前记录起始索引
|
||||
*/
|
||||
private Integer pageNum;
|
||||
|
||||
/**
|
||||
* 每页显示记录数
|
||||
*/
|
||||
private Integer pageSize;
|
||||
|
||||
/**
|
||||
* 排序列
|
||||
*/
|
||||
private String orderByColumn;
|
||||
|
||||
/**
|
||||
* 排序的方向desc或者asc
|
||||
*/
|
||||
private String isAsc = "asc";
|
||||
|
||||
/**
|
||||
* 分页参数合理化
|
||||
*/
|
||||
private Boolean reasonable = true;
|
||||
|
||||
public String getOrderBy() {
|
||||
if (StrUtil.isEmpty(orderByColumn)) {
|
||||
return "";
|
||||
}
|
||||
return StrUtil.toUnderlineCase(orderByColumn) + " " + isAsc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package com.metis.utils;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
|
||||
/**
|
||||
* 表格数据处理
|
||||
*
|
||||
* @author Clay
|
||||
* @date 2022/10/30
|
||||
*/
|
||||
public class TableSupport {
|
||||
|
||||
/**
|
||||
* 当前记录起始索引
|
||||
*/
|
||||
public static final String PAGE_NUM = "pageNum";
|
||||
|
||||
/**
|
||||
* 每页显示记录数
|
||||
*/
|
||||
public static final String PAGE_SIZE = "pageSize";
|
||||
|
||||
/**
|
||||
* 排序列
|
||||
*/
|
||||
public static final String ORDER_BY_COLUMN = "orderByColumn";
|
||||
|
||||
/**
|
||||
* 排序的方向 "desc" 或者 "asc".
|
||||
*/
|
||||
public static final String IS_ASC = "isAsc";
|
||||
|
||||
/**
|
||||
* 分页参数合理化
|
||||
*/
|
||||
public static final String REASONABLE = "reasonable";
|
||||
|
||||
/**
|
||||
* 封装分页对象
|
||||
*/
|
||||
public static PageInfo getPageInfo() {
|
||||
PageInfo pageInfo = new PageInfo();
|
||||
pageInfo.setPageNum(Convert.toInt(getParameter(PAGE_NUM), 1));
|
||||
pageInfo.setPageSize(Convert.toInt(getParameter(PAGE_SIZE), 10));
|
||||
pageInfo.setOrderByColumn(getParameter(ORDER_BY_COLUMN));
|
||||
pageInfo.setIsAsc(getParameter(IS_ASC));
|
||||
pageInfo.setReasonable(getParameterToBool(REASONABLE));
|
||||
return pageInfo;
|
||||
}
|
||||
|
||||
public static PageInfo buildPageRequest() {
|
||||
return getPageInfo();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Boolean参数
|
||||
*/
|
||||
public static Boolean getParameterToBool(String name) {
|
||||
return Convert.toBool(getRequest().getParameter(name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取String参数
|
||||
*/
|
||||
public static String getParameter(String name) {
|
||||
return Convert.toStr(getRequest().getParameter(name));
|
||||
}
|
||||
|
||||
|
||||
public static HttpServletRequest getRequest() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
return attributes.getRequest();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.metis.utils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.transaction.support.TransactionSynchronization;
|
||||
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
||||
|
||||
@Slf4j
|
||||
public class TransactionalUtils {
|
||||
|
||||
/**
|
||||
* 提交后执行
|
||||
*
|
||||
* @param runnable 可运行
|
||||
*/
|
||||
public static void afterCommitExecute(Runnable runnable) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCommit() {
|
||||
log.info("事务提交后执行业务");
|
||||
runnable.run();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务回滚时执行
|
||||
*
|
||||
* @param runnable 可运行
|
||||
*/
|
||||
public static void afterRollbackExecute(Runnable runnable) {
|
||||
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
|
||||
@Override
|
||||
public void afterCompletion(int status) {
|
||||
// 只在事务状态为 STATUS_ROLLED_BACK 时执行
|
||||
if (status == STATUS_ROLLED_BACK) {
|
||||
log.info("事务回滚后执行业务");
|
||||
runnable.run();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
com.metis.config.MetisStarterAutoConfiguration
|
||||
Reference in New Issue
Block a user