feat: 将项目改为Starter的结构, 为后续开发做扩展准备

This commit is contained in:
2025-04-21 21:59:31 +08:00
parent bd4c374952
commit 2b2864b8df
111 changed files with 162 additions and 177 deletions

60
metis-starter/pom.xml Normal file
View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<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>

View File

@@ -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 {
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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);
}
}

View 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;
}

View 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);
}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,6 @@
package com.metis.flow.constant;
public interface BaseConstant {
Integer DEFAULT_VERSION = 0;
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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));
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,4 @@
package com.metis.flow.domain.entity.base;
public abstract class NodeConfig {
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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 {
}

View File

@@ -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;
}

View File

@@ -0,0 +1 @@
package com.metis.flow.domain.entity.config;

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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> {
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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();
}

View File

@@ -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;
}

View File

@@ -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);
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
});
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}

View File

@@ -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);
}
}

View File

@@ -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("无法获取泛型类型");
}
}

View File

@@ -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 = true2019-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);
}
}

View File

@@ -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());
}
}

View 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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}
});
}
}

View File

@@ -0,0 +1 @@
com.metis.config.MetisStarterAutoConfiguration