From 716571197fbad148be3dc1fb666e92d42f688154 Mon Sep 17 00:00:00 2001 From: clay Date: Mon, 31 Mar 2025 23:54:04 +0800 Subject: [PATCH] feat: init --- .gitignore | 2 + pom.xml | 126 ++++++ src/main/java/com/metis/MetisApplication.java | 15 + .../com/metis/controller/TestController.java | 18 + .../java/com/metis/domain/BaseEntity.java | 45 ++ .../java/com/metis/domain/package-info.java | 1 + src/main/java/com/metis/enums/BaseEnum.java | 59 +++ src/main/java/com/metis/enums/ResultEnum.java | 47 +++ .../metis/handle/GlobalExceptionHandler.java | 92 ++++ .../mybatis/MybatisPlusConfiguration.java | 82 ++++ .../handler/BaseEntityMetaObjectHandler.java | 40 ++ .../logic/SelectIgnoreLogicDelete.java | 51 +++ .../mybatis/support/CustomSqlInjector.java | 25 ++ src/main/java/com/metis/result/Result.java | 149 +++++++ .../com/metis/result/page/TableDataInfo.java | 45 ++ .../sseclient/ToolSpecificationHelper.java | 119 ++++++ .../com/metis/sseclient/check/SseCheck.java | 180 ++++++++ .../sseclient/event/SseEventListener.java | 75 ++++ .../handler/McpOperationHandler.java | 56 +++ .../com/metis/utils/LocalDateTimeUtils.java | 399 ++++++++++++++++++ .../com/metis/utils/PageConditionUtil.java | 66 +++ src/main/java/com/metis/utils/PageInfo.java | 44 ++ .../java/com/metis/utils/TableSupport.java | 79 ++++ .../com/metis/utils/TransactionalUtils.java | 43 ++ src/main/resources/application-dev.yml | 20 + src/main/resources/application.yml | 7 + src/main/resources/script/sse.html | 45 ++ src/test/java/AnsMsgHandler.java | 8 + src/test/java/SSeTest.java | 16 + src/test/java/SseClient.java | 77 ++++ 30 files changed, 2031 insertions(+) create mode 100644 pom.xml create mode 100644 src/main/java/com/metis/MetisApplication.java create mode 100644 src/main/java/com/metis/controller/TestController.java create mode 100644 src/main/java/com/metis/domain/BaseEntity.java create mode 100644 src/main/java/com/metis/domain/package-info.java create mode 100644 src/main/java/com/metis/enums/BaseEnum.java create mode 100644 src/main/java/com/metis/enums/ResultEnum.java create mode 100644 src/main/java/com/metis/handle/GlobalExceptionHandler.java create mode 100644 src/main/java/com/metis/mybatis/MybatisPlusConfiguration.java create mode 100644 src/main/java/com/metis/mybatis/handler/BaseEntityMetaObjectHandler.java create mode 100644 src/main/java/com/metis/mybatis/logic/SelectIgnoreLogicDelete.java create mode 100644 src/main/java/com/metis/mybatis/support/CustomSqlInjector.java create mode 100644 src/main/java/com/metis/result/Result.java create mode 100644 src/main/java/com/metis/result/page/TableDataInfo.java create mode 100644 src/main/java/com/metis/sseclient/ToolSpecificationHelper.java create mode 100644 src/main/java/com/metis/sseclient/check/SseCheck.java create mode 100644 src/main/java/com/metis/sseclient/event/SseEventListener.java create mode 100644 src/main/java/com/metis/sseclient/handler/McpOperationHandler.java create mode 100644 src/main/java/com/metis/utils/LocalDateTimeUtils.java create mode 100644 src/main/java/com/metis/utils/PageConditionUtil.java create mode 100644 src/main/java/com/metis/utils/PageInfo.java create mode 100644 src/main/java/com/metis/utils/TableSupport.java create mode 100644 src/main/java/com/metis/utils/TransactionalUtils.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/script/sse.html create mode 100644 src/test/java/AnsMsgHandler.java create mode 100644 src/test/java/SSeTest.java create mode 100644 src/test/java/SseClient.java diff --git a/.gitignore b/.gitignore index 9154f4c..4b9bac1 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,8 @@ # Mobile Tools for Java (J2ME) .mtj.tmp/ +.idea +target # Package Files # *.jar diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..d71d0e9 --- /dev/null +++ b/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + + com.metis + metis + 1.0.0-SNAPSHOT + + + 17 + 17 + UTF-8 + 3.3.4 + 2.0.45 + 1.18.34 + 1.2.3 + 1.0.0-beta2 + 3.5.8 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.vserion} + pom + import + + + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-validation + + + dev.langchain4j + langchain4j-open-ai + ${langchain4j.version} + + + dev.langchain4j + langchain4j-mcp + 1.0.0-beta2 + + + com.alibaba.fastjson2 + fastjson2 + ${fastjson.version} + + + org.projectlombok + lombok + + + com.mikesamuel + json-sanitizer + ${sanitizer.version} + + + com.baomidou + mybatis-plus-spring-boot3-starter + ${mybatis-plus.version} + + + mysql + mysql-connector-java + 8.0.33 + + + cn.hutool + hutool-all + 5.8.22 + + + org.mapstruct + mapstruct + 1.6.2 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + 17 + 17 + + + org.mapstruct + mapstruct-processor + 1.6.2 + + + org.projectlombok + lombok + 1.18.34 + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/metis/MetisApplication.java b/src/main/java/com/metis/MetisApplication.java new file mode 100644 index 0000000..b083632 --- /dev/null +++ b/src/main/java/com/metis/MetisApplication.java @@ -0,0 +1,15 @@ +package com.metis; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; + +@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) +public class MetisApplication { + + public static void main(String[] args) { + SpringApplication.run(MetisApplication.class, args); + } + + +} diff --git a/src/main/java/com/metis/controller/TestController.java b/src/main/java/com/metis/controller/TestController.java new file mode 100644 index 0000000..53dbd80 --- /dev/null +++ b/src/main/java/com/metis/controller/TestController.java @@ -0,0 +1,18 @@ +package com.metis.controller; + +import com.metis.result.Result; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/test") +public class TestController { + + + @GetMapping + public Result test() { + return Result.ok("测试成功"); + } + +} diff --git a/src/main/java/com/metis/domain/BaseEntity.java b/src/main/java/com/metis/domain/BaseEntity.java new file mode 100644 index 0000000..de02d39 --- /dev/null +++ b/src/main/java/com/metis/domain/BaseEntity.java @@ -0,0 +1,45 @@ +package com.metis.domain; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * @author Clay + * @date 2022/10/30 + */ +@Data +public class BaseEntity implements Serializable { + + /** + * 创建者 + */ +// private Object createBy; + + /** + * 创建时间 + */ + @JsonFormat(locale = "zh",timezone = "GMT+8",pattern = "yyyy-MM-dd") + private LocalDateTime createTime; + + /** + * 更新者 + */ +// private Object updateBy; + + /** + * 更新时间 + */ + @JsonFormat(locale = "zh",timezone = "GMT+8",pattern = "yyyy-MM-dd") + private LocalDateTime updateTime; + + + /** + * 逻辑删除字段 + */ + private Boolean isDeleted; + + +} diff --git a/src/main/java/com/metis/domain/package-info.java b/src/main/java/com/metis/domain/package-info.java new file mode 100644 index 0000000..8890ae6 --- /dev/null +++ b/src/main/java/com/metis/domain/package-info.java @@ -0,0 +1 @@ +package com.metis.domain; \ No newline at end of file diff --git a/src/main/java/com/metis/enums/BaseEnum.java b/src/main/java/com/metis/enums/BaseEnum.java new file mode 100644 index 0000000..71c42b3 --- /dev/null +++ b/src/main/java/com/metis/enums/BaseEnum.java @@ -0,0 +1,59 @@ +package com.metis.enums; + +import com.baomidou.mybatisplus.annotation.IEnum; + +import java.util.Arrays; + +public interface BaseEnum> extends IEnum { + + /** + * 获取编码 + * + * @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 & BaseEnum> E parseByCode(Class 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 & BaseEnum> E parseByName(Class enumClass, String name) { + return Arrays.stream(enumClass.getEnumConstants()) + .filter(e -> e.getName().equals(name)) + .findFirst() + .orElse(null); + } +} diff --git a/src/main/java/com/metis/enums/ResultEnum.java b/src/main/java/com/metis/enums/ResultEnum.java new file mode 100644 index 0000000..ce7a754 --- /dev/null +++ b/src/main/java/com/metis/enums/ResultEnum.java @@ -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; + + +} \ No newline at end of file diff --git a/src/main/java/com/metis/handle/GlobalExceptionHandler.java b/src/main/java/com/metis/handle/GlobalExceptionHandler.java new file mode 100644 index 0000000..86133ac --- /dev/null +++ b/src/main/java/com/metis/handle/GlobalExceptionHandler.java @@ -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 handlerNoFoundException(Exception e) { + log.error(e.getMessage(), e); + return Result.notFound( "路径不存在,请检查路径是否正确"); + } + + /** + * 授权失败 + * + * @param e + * @return + */ + @ExceptionHandler(AccessDeniedException.class) + public Result handleAuthorizationException(AccessDeniedException e) { + log.error(e.getMessage()); + return Result.error(HttpStatus.FORBIDDEN, "没有权限,请联系管理员授权"); + } + + @ExceptionHandler(AccountExpiredException.class) + public Result handleAccountExpiredException(AccountExpiredException e) { + log.error(e.getMessage(), e); + return Result.error(e.getMessage()); + } + + + /** + * 自定义验证异常 + */ + @ExceptionHandler(BindException.class) + public Result validatedBindException(BindException e) { + log.error(e.getMessage(), e); + String message = e.getAllErrors().get(0).getDefaultMessage(); + return Result.error(message); + } + + /** + * 自定义验证异常 + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + public Result validExceptionHandler(MethodArgumentNotValidException e) { + log.error(e.getMessage(), e); + String message = e.getBindingResult().getFieldError().getDefaultMessage(); + return Result.error(message); + } + + @ExceptionHandler(RuntimeException.class) + public Result runtimeExceptionHandler(RuntimeException e) { + log.error(e.getMessage(), e); + return Result.error(e.getMessage()); + } + + + +} \ No newline at end of file diff --git a/src/main/java/com/metis/mybatis/MybatisPlusConfiguration.java b/src/main/java/com/metis/mybatis/MybatisPlusConfiguration.java new file mode 100644 index 0000000..7530f83 --- /dev/null +++ b/src/main/java/com/metis/mybatis/MybatisPlusConfiguration.java @@ -0,0 +1,82 @@ +package com.metis.mybatis; + +import cn.hutool.core.net.NetUtil; +import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; +import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator; +import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator; +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(optimisticLockerInnerInterceptor()); + // 添加防止全表更新与删除插件 + interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); + return interceptor; + } + + /** + * 自动填充字段信息 + */ + @Bean + public MetaObjectHandler metaObjectHandler() { + return new BaseEntityMetaObjectHandler(); + } + + /** + * 乐观锁插件 + */ + public OptimisticLockerInnerInterceptor optimisticLockerInnerInterceptor() { + return new OptimisticLockerInnerInterceptor(); + } + + /** + * 自定义SQL注入器 + * + * @return 自定义SQL注入器 + */ + @Bean + public ISqlInjector iSqlInjector() { + return new CustomSqlInjector(); + } + + + /** + * 使用网卡信息绑定雪花生成器,实现集群生成id不重复 + */ + @Bean + public IdentifierGenerator idGenerator() { + return new DefaultIdentifierGenerator(NetUtil.getLocalhost()); + } + + +} + diff --git a/src/main/java/com/metis/mybatis/handler/BaseEntityMetaObjectHandler.java b/src/main/java/com/metis/mybatis/handler/BaseEntityMetaObjectHandler.java new file mode 100644 index 0000000..c9465c9 --- /dev/null +++ b/src/main/java/com/metis/mybatis/handler/BaseEntityMetaObjectHandler.java @@ -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()); + } +} diff --git a/src/main/java/com/metis/mybatis/logic/SelectIgnoreLogicDelete.java b/src/main/java/com/metis/mybatis/logic/SelectIgnoreLogicDelete.java new file mode 100644 index 0000000..8eebb74 --- /dev/null +++ b/src/main/java/com/metis/mybatis/logic/SelectIgnoreLogicDelete.java @@ -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; + } +} diff --git a/src/main/java/com/metis/mybatis/support/CustomSqlInjector.java b/src/main/java/com/metis/mybatis/support/CustomSqlInjector.java new file mode 100644 index 0000000..d303dda --- /dev/null +++ b/src/main/java/com/metis/mybatis/support/CustomSqlInjector.java @@ -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 getMethodList(Configuration configuration, Class mapperClass, TableInfo tableInfo) { + List methodList = super.getMethodList(configuration, mapperClass, tableInfo); + // 查询所有数据(包括已经逻辑删除数据) + methodList.add(new SelectIgnoreLogicDelete()); + return methodList; + } +} \ No newline at end of file diff --git a/src/main/java/com/metis/result/Result.java b/src/main/java/com/metis/result/Result.java new file mode 100644 index 0000000..ed9c01f --- /dev/null +++ b/src/main/java/com/metis/result/Result.java @@ -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 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 Result ok(Integer code, String msg, T data) { + return new Result<>(code, msg, data, HttpStatus.OK); + } + + public static Result ok(Integer code, String msg, T data, HttpStatus status) { + return new Result<>(code, msg, data, status); + } + + public static Result ok(String msg, T data) { + return Result.ok(ResultEnum.SUCCESS.code, msg, data, ResultEnum.SUCCESS.status); + } + + public static Result ok(Integer code, T data) { + return Result.ok(code, ResultEnum.SUCCESS.msg, data); + } + + public static Result ok(String msg) { + return ok(ResultEnum.SUCCESS.code, msg, null); + } + + public static Result ok(T data) { + return Result.ok(ResultEnum.SUCCESS.msg, data); + } + + public static Result ok() { + return Result.ok(ResultEnum.SUCCESS.msg, null); + } + + public static Result error(String msg, T data) { + return Result.error(ResultEnum.ERROR.code, msg, data); + } + + public static Result error(Integer code, String msg) { + return Result.error(code, msg, null); + } + + public static Result notFound(String msg) { + return Result.error(HttpStatus.NOT_FOUND.value(), msg, null, HttpStatus.NOT_FOUND); + } + + public static Result error(Integer code, String msg, T data) { + return new Result<>(code, msg, data, HttpStatus.INTERNAL_SERVER_ERROR); + } + + public static Result error(Integer code, String msg, T data, HttpStatus status) { + return new Result<>(code, msg, data, status); + } + + public static Result unauthorized(String msg) { + return new Result<>(HttpStatus.UNAUTHORIZED.value(), msg, null, HttpStatus.UNAUTHORIZED); + } + + public static Result error(HttpStatus status, String msg) { + return new Result<>(status.value(), msg, null, status); + } + + public static Result error(ResultEnum resultEnum) { + return Result.error(resultEnum.code, resultEnum.msg, null, resultEnum.status); + } + + public static Result error(String msg) { + return Result.error(ResultEnum.ERROR.code, msg, null, ResultEnum.ERROR.status); + } + + public static Result error() { + return Result.error(ResultEnum.ERROR.code, ResultEnum.ERROR.msg, null); + } + + public static Result info(String msg) { + return Result.ok(ResultEnum.NO_DATA.code, msg, null); + } + + public static Result info(ResultEnum resultEnum) { + return Result.error(resultEnum.code, resultEnum.msg, null, resultEnum.status); + } + + public static Result 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; + } +} diff --git a/src/main/java/com/metis/result/page/TableDataInfo.java b/src/main/java/com/metis/result/page/TableDataInfo.java new file mode 100644 index 0000000..a715c8f --- /dev/null +++ b/src/main/java/com/metis/result/page/TableDataInfo.java @@ -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 implements Serializable { + + + /** + * 总记录数 + */ + private long total; + + /** + * 列表数据 + */ + private List rows; + + /** + * 表格数据对象 + */ + public TableDataInfo() { + } + + /** + * 分页 + * + * @param list 列表数据 + * @param total 总记录数 + */ + public TableDataInfo(List list, int total) { + this.rows = list; + this.total = total; + } + +} diff --git a/src/main/java/com/metis/sseclient/ToolSpecificationHelper.java b/src/main/java/com/metis/sseclient/ToolSpecificationHelper.java new file mode 100644 index 0000000..1f50a07 --- /dev/null +++ b/src/main/java/com/metis/sseclient/ToolSpecificationHelper.java @@ -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 toolSpecificationListFromMcpResponse(ArrayNode array) { + List 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 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; + } +} diff --git a/src/main/java/com/metis/sseclient/check/SseCheck.java b/src/main/java/com/metis/sseclient/check/SseCheck.java new file mode 100644 index 0000000..4d45e89 --- /dev/null +++ b/src/main/java/com/metis/sseclient/check/SseCheck.java @@ -0,0 +1,180 @@ +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 = "metis"; + 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> pendingOperations = new ConcurrentHashMap<>(); + Request request = new Request.Builder().url(sseUrl).build(); + CompletableFuture 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; + execute(httpRequest, operationId) + .thenCompose(originalResponse -> execute(finalInitializationNotification, null) + .thenCompose(nullNode -> CompletableFuture.completedFuture(originalResponse))); +// 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 listTools() throws JsonProcessingException { + McpListToolsRequest operation = new McpListToolsRequest(idGenerator.getAndIncrement()); + Request request = createRequest(operation); + CompletableFuture 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 execute(Request request, Long id) { + CompletableFuture future = new CompletableFuture<>(); + 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; + } +} diff --git a/src/main/java/com/metis/sseclient/event/SseEventListener.java b/src/main/java/com/metis/sseclient/event/SseEventListener.java new file mode 100644 index 0000000..feffeab --- /dev/null +++ b/src/main/java/com/metis/sseclient/event/SseEventListener.java @@ -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 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()); + } +} diff --git a/src/main/java/com/metis/sseclient/handler/McpOperationHandler.java b/src/main/java/com/metis/sseclient/handler/McpOperationHandler.java new file mode 100644 index 0000000..614a5e9 --- /dev/null +++ b/src/main/java/com/metis/sseclient/handler/McpOperationHandler.java @@ -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> pendingOperations; + private static final Logger log = LoggerFactory.getLogger(McpOperationHandler.class); + + public McpOperationHandler( + Map> pendingOperations) { + this.pendingOperations = pendingOperations; + } + + public void handle(JsonNode message) { + if (message.has("id")) { + long messageId = message.get("id").asLong(); + CompletableFuture 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 future) { + pendingOperations.put(id, future); + } +} diff --git a/src/main/java/com/metis/utils/LocalDateTimeUtils.java b/src/main/java/com/metis/utils/LocalDateTimeUtils.java new file mode 100644 index 0000000..64bf1df --- /dev/null +++ b/src/main/java/com/metis/utils/LocalDateTimeUtils.java @@ -0,0 +1,399 @@ +package com.metis.utils; + +import java.time.*; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.time.temporal.Temporal; +import java.time.temporal.TemporalAdjusters; +import java.time.temporal.TemporalUnit; +import java.util.Date; + +/** + * 地方日期时间工具类 + * + * @author ZhangQiang + * @date 2024/10/28 + */ +public class LocalDateTimeUtils extends cn.hutool.core.date.LocalDateTimeUtil { + + /** + * 在范围内 + * + * @param target 目标 + * @param start 开始 + * @param end 结束 + * @return boolean + */ + public static boolean isWithinRange(LocalDateTime target, LocalDateTime start, LocalDateTime end) { + return !target.isBefore(start) && !target.isAfter(end); + } + + /** + * 现在时间在范围内 + * + * @param start 开始 + * @param end 结束 + * @return boolean + */ + public static boolean nowIsWithinRange(LocalDateTime start, LocalDateTime end) { + LocalDateTime target = LocalDateTime.now(); + return !target.isBefore(start) && !target.isAfter(end); + } + + /** + * 获取指定时间是周几 + * + * @param time 时间 + * @return int + */ + public static int week(LocalDateTime time) { + return time.getDayOfWeek().getValue(); + } + + + /** + * 获取加或减N月的第一天 + * + * @param num 数字 + * @return {@link LocalDateTime} + */ + public static LocalDateTime monthFirst(int num) { + LocalDateTime newTime = plus(LocalDateTime.now(), num, ChronoUnit.MONTHS); + newTime = newTime.with(TemporalAdjusters.firstDayOfMonth()); + return getDayStart(newTime); + } + + /** + * 获取加或减N月的最后天 + * + * @param num 数字 + * @return {@link LocalDateTime} + */ + public static LocalDateTime monthLast(int num) { + LocalDateTime newTime = plus(LocalDateTime.now(), num, ChronoUnit.MONTHS); + newTime = newTime.with(TemporalAdjusters.lastDayOfMonth()); + return getDayEnd(newTime); + } + + + /** + * 获取加或减N周的第一天 + * + * @param num 数字 + * @return {@link LocalDateTime} + */ + public static LocalDateTime weekFirst(int num) { + int week = week(LocalDateTime.now()); + LocalDateTime newTime = subtract(LocalDateTime.now(), week - 1, ChronoUnit.DAYS); + newTime = plus(newTime, num * 7L, ChronoUnit.DAYS); + return getDayStart(newTime); + } + + /** + * 获取加或减N周的最后一天 + * + * @param num 数字 + * @return {@link LocalDateTime} + */ + public static LocalDateTime weekLast(int num) { + int week = week(LocalDateTime.now()); + LocalDateTime newTime = plus(LocalDateTime.now(), 7 - week, ChronoUnit.DAYS); + newTime = plus(newTime, num * 7L, ChronoUnit.DAYS); + return getDayEnd(newTime); + } + + + /** + * 判断时间 ==> t1 < t2 = true (2019-10-13 11:11:00 < 2020-11-13 13:13:00 = true) + * + * @param t1 t1 + * @param t2 t2 + * @return boolean + */ + public static boolean isBefore(LocalDateTime t1, LocalDateTime t2) { + return t1.isBefore(t2); + } + + /** + * 判断时间 ==> t1 > t2 = true(2019-10-13 11:11:00 > 2020-11-13 13:13:00 = false) + * + * @param t1 t1 + * @param t2 t2 + * @return boolean + */ + public static boolean isAfter(LocalDateTime t1, LocalDateTime t2) { + return t1.isAfter(t2); + } + + /** + * Date 转 LocalDateTime + * + * @param date 日期 + * @return {@link LocalDateTime} + */ + public static LocalDateTime convertToLocalDateTime(Date date) { + return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); + } + + /** + * LocalDateTime 转 Date + * + * @param time 时间 + * @return {@link Date} + */ + public static Date convertToDate(LocalDateTime time) { + return Date.from(time.atZone(ZoneId.systemDefault()).toInstant()); + } + + /** + * Date转LocalDate + * + * @param date 日期 + * @return {@link LocalDate} + */ + public static LocalDate convertLocalDate(Date date) { + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + } + + /** + * Date转LocalDate + * + * @param date 日期 + * @return {@link LocalTime} + */ + public static LocalTime convertLocalTime(Date date) { + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalTime(); + } + + + /** + * 获取指定日期的毫秒 + * + * @param time 时间 + * @return {@link Long} + */ + public static Long getMilliByTime(LocalDateTime time) { + return time.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + /** + * 获取秒数通过时间 + * 获取指定日期的秒 + * + * @param time 时间 + * @return {@link Long} + */ + public static Long getSecondsByTime(LocalDateTime time) { + return time.atZone(ZoneId.systemDefault()).toInstant().getEpochSecond(); + } + + /** + * 获取指定时间的指定格式 ==> yyyy-MM-dd HH:mm:ss:SSS (HH是24小时制,而hh是12小时制, ss是秒,SSS是毫秒) + * + * @param time 时间 + * @param pattern 图案 + * @return {@link String} + */ + public static String formatTime(LocalDateTime time, String pattern) { + return time.format(DateTimeFormatter.ofPattern(pattern)); + } + + /** + * 日期加上一个数,根据field不同加不同值,field为ChronoUnit.* + * + * @param time 时间 + * @param number 数 + * @param field 领域 + * @return {@link LocalDateTime} + */ + public static LocalDateTime plus(LocalDateTime time, long number, TemporalUnit field) { + return time.plus(number, field); + } + + /** + * 减去 + * 日期减去一个数,根据field不同减不同值,field参数为ChronoUnit.* + * + * @param time 时间 + * @param number 数 + * @param field 领域 + * @return {@link LocalDateTime} + */ + public static LocalDateTime subtract(LocalDateTime time, long number, TemporalUnit field) { + return time.minus(number, field); + } + + /** + * 获取白天开始 + * 获取指定某一天的开始时间 00:00:00 + * + * @param time 时间 + * @return {@link LocalDateTime} + */ + public static LocalDateTime getDayStart(LocalDateTime time) { + return time.withHour(0) + .withMinute(0) + .withSecond(0) + .withNano(0); + } + + + /** + * 获取白天结束 + * 获取指定某一天的结束时间 23:59:59.999 + * + * @param time 时间 + * @return {@link LocalDateTime} + */ + public static LocalDateTime getDayEnd(LocalDateTime time) { + return time.withHour(23) + .withMinute(59) + .withSecond(59) + .withNano(999999999); + } + + /** + * 获取本周周一 + * + * @param time 时间 + * @return {@link LocalDateTime} + */ + public static LocalDateTime getWeekOfFirst(LocalDateTime time) { + return time.with(TemporalAdjusters.previous(DayOfWeek.SUNDAY)). + plusDays(1).withHour(0).withMinute(0).withSecond(0); + } + + /** + * 获取本周周日 + * + * @param time 时间 + * @return {@link LocalDateTime} + */ + public static LocalDateTime getWeekOfLast(LocalDateTime time) { + return time.with(TemporalAdjusters.next(DayOfWeek.MONDAY)). + minusDays(1).withHour(23).withMinute(59).withSecond(59); + } + + /** + * 获取本月第一天 + * + * @param time 时间 + * @return {@link LocalDateTime} + */ + public static LocalDateTime getMonthOfFirst(LocalDateTime time) { + LocalDateTime firsthand = time.with(TemporalAdjusters.firstDayOfMonth()); + return LocalDateTime.of(firsthand.toLocalDate(), LocalTime.MIN); + } + + /** + * 获取本月最后一天 + * + * @param time 时间 + * @return {@link LocalDateTime} + */ + public static LocalDateTime getMonthOfLast(LocalDateTime time) { + LocalDateTime lastDay = time.with(TemporalAdjusters.lastDayOfMonth()); + return LocalDateTime.of(lastDay.toLocalDate(), LocalTime.MAX); + } + + /** + * 日期相隔天数 + * + * @param startDateInclusive 开始日期(含) + * @param endDateExclusive 结束日期除外 + * @return int + */ + public static int periodDays(LocalDate startDateInclusive, LocalDate endDateExclusive) { + return Period.between(startDateInclusive, endDateExclusive).getDays(); + } + + /** + * 日期相隔小时 + * + * @param startInclusive 开始包容性 + * @param endExclusive 结束独家 + * @return long + */ + public static long durationHours(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive).toHours(); + } + + + /** + * 日期相隔分钟 + * + * @param startInclusive 开始包容性 + * @param endExclusive 结束独家 + * @return long + */ + public static long durationMinutes(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive).toMinutes(); + } + + + /** + * 持续时间秒 + * + * @param startInclusive 开始包容 + * @param endExclusive 结束独家 + * @return long + */ + public static long durationSeconds(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive).toSeconds(); + } + + /** + * 日期相隔毫秒数 + * + * @param startInclusive 开始包容性 + * @param endExclusive 结束独家 + * @return long + */ + public static long durationMillis(Temporal startInclusive, Temporal endExclusive) { + return Duration.between(startInclusive, endExclusive).toMillis(); + } + + /** + * 是否当天 + */ + public static boolean isToday(LocalDate date) { + return LocalDate.now().equals(date); + } + + + /** + * 判断两个时间是否相差指定时间(精确到毫秒) + * + * @param time1 第一个时间点 + * @param time2 第二个时间点 + * @param milliseconds 指定时间差(毫秒) + * @return true 如果两个时间相差超过指定毫秒数,否则 false + */ + public static boolean isTimeDifferenceExceeds(LocalDateTime time1, LocalDateTime time2, long milliseconds) { + if (time1 == null || time2 == null) { + throw new IllegalArgumentException("Time arguments cannot be null"); + } + + Duration duration = Duration.between(time1, time2); + return Math.abs(duration.toMillis()) > milliseconds; + } + + /** + * 判断指定时间是否与当前时间相差超过 24 小时(精确到毫秒) + * + * @param targetTime 指定的 LocalDateTime 时间 + * @return true 如果相差 24 小时以上,否则 false + */ + public static boolean isMoreThan24HoursApart(LocalDateTime targetTime) { + if (targetTime == null) { + throw new IllegalArgumentException("Target time cannot be null"); + } + // 比较时间差的绝对值是否大于 24 小时 + return isTimeDifferenceExceeds(LocalDateTime.now(), targetTime, 86400000L); + } + + +} + + + diff --git a/src/main/java/com/metis/utils/PageConditionUtil.java b/src/main/java/com/metis/utils/PageConditionUtil.java new file mode 100644 index 0000000..9eb60d9 --- /dev/null +++ b/src/main/java/com/metis/utils/PageConditionUtil.java @@ -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 转换后的对象类型 + * @param 需要转换的对象类型 + * @return 转换后的对象 + */ + public static TableDataInfo convertDataTable(IPage page, Function map) { + List convertList = page.getRecords().stream().map(map).collect(Collectors.toList()); + return convertDataTable(convertList, page.getTotal()); + } + + /** + * 转换为TableDataInfo对象 + * + * @param page 源对象 + * @return 转换后的对象 + */ + public static TableDataInfo convertDataTable(IPage page) { + return convertDataTable(page.getRecords(), page.getTotal()); + } + + + /** + * 转换为TableDataInfo对象 + * + * @param list + * @param count + * @param + * @return + */ + public static TableDataInfo convertDataTable(List list, Long count) { + if (null == list) { + return new TableDataInfo<>(new ArrayList<>(), 0); + } + TableDataInfo tableDataInfo = new TableDataInfo<>(); + tableDataInfo.setRows(list); + tableDataInfo.setTotal(count); + return tableDataInfo; + } + + public static IPage getPage() { + PageInfo pageInfo = TableSupport.getPageInfo(); + return new Page<>(pageInfo.getPageNum(), pageInfo.getPageSize()); + } +} diff --git a/src/main/java/com/metis/utils/PageInfo.java b/src/main/java/com/metis/utils/PageInfo.java new file mode 100644 index 0000000..4dbb23d --- /dev/null +++ b/src/main/java/com/metis/utils/PageInfo.java @@ -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; + } +} diff --git a/src/main/java/com/metis/utils/TableSupport.java b/src/main/java/com/metis/utils/TableSupport.java new file mode 100644 index 0000000..76f7317 --- /dev/null +++ b/src/main/java/com/metis/utils/TableSupport.java @@ -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(); + } + +} diff --git a/src/main/java/com/metis/utils/TransactionalUtils.java b/src/main/java/com/metis/utils/TransactionalUtils.java new file mode 100644 index 0000000..4efb6d6 --- /dev/null +++ b/src/main/java/com/metis/utils/TransactionalUtils.java @@ -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(); + } + } + }); + } + +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..7b52eda --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,20 @@ +# Spring配置 +spring: + datasource: + url: jdbc:mysql://frp.feashow.cn:39306/metis?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 + username: root + password: yyz@2024 + driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + host: frp.feashow.cn + port: 39379 + password: yyz@2024 + database: 13 + timeout: 10s + lettuce: + pool: + min-idle: 0 + max-idle: 8 + max-active: 8 + max-wait: -1ms \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..a469978 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,7 @@ +server: + port: 8080 + + +spring: + application: + name: metis \ No newline at end of file diff --git a/src/main/resources/script/sse.html b/src/main/resources/script/sse.html new file mode 100644 index 0000000..2dfd71c --- /dev/null +++ b/src/main/resources/script/sse.html @@ -0,0 +1,45 @@ + + + + + + SSE Demo + + +

SSE 连接示例

+
+ + + + \ No newline at end of file diff --git a/src/test/java/AnsMsgHandler.java b/src/test/java/AnsMsgHandler.java new file mode 100644 index 0000000..8d79f62 --- /dev/null +++ b/src/test/java/AnsMsgHandler.java @@ -0,0 +1,8 @@ + +import java.io.InputStream; + +public interface AnsMsgHandler { + + void actMsg(InputStream is, String line); + +} diff --git a/src/test/java/SSeTest.java b/src/test/java/SSeTest.java new file mode 100644 index 0000000..1a34d62 --- /dev/null +++ b/src/test/java/SSeTest.java @@ -0,0 +1,16 @@ +import com.fasterxml.jackson.core.JsonProcessingException; +import com.metis.sseclient.check.SseCheck; +import dev.langchain4j.agent.tool.ToolSpecification; +import lombok.extern.slf4j.Slf4j; + +import java.util.List; + + +@Slf4j +public class SSeTest { + public static void main(String[] args) throws JsonProcessingException { + SseCheck sseCheck = new SseCheck("http://localhost:8081/sse"); + List listTools = sseCheck.listTools(); + System.out.println(listTools); + } +} diff --git a/src/test/java/SseClient.java b/src/test/java/SseClient.java new file mode 100644 index 0000000..b3615f6 --- /dev/null +++ b/src/test/java/SseClient.java @@ -0,0 +1,77 @@ + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; + +/** + * + * @author hyd + * + */ +public class SseClient { + + /** + * 获取SSE输入流。 + * + * @param urlPath + * @return + * @throws IOException + */ + public static InputStream getSseInputStream(String urlPath) throws IOException { + URL url = new URL(urlPath); + HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); + // 这儿根据自己的情况选择get或post + urlConnection.setRequestMethod("GET"); + urlConnection.setDoOutput(true); + urlConnection.setDoInput(true); + urlConnection.setUseCaches(false); + urlConnection.setRequestProperty("Connection", "Keep-Alive"); + urlConnection.setRequestProperty("Charset", "UTF-8"); + //读取过期时间(很重要,建议加上) + urlConnection.setReadTimeout(60 * 1000); + // text/plain模式 + urlConnection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8"); + InputStream inputStream = urlConnection.getInputStream(); + InputStream is = new BufferedInputStream(inputStream); + return is; + } + + /** + * 读取数据。 + * + * @param is + * @param ansMsgHandler + * @throws IOException + */ + public static void readStream(InputStream is, AnsMsgHandler ansMsgHandler) throws IOException { + try { + BufferedReader reader = new BufferedReader(new InputStreamReader(is)); + String line = ""; + while ((line = reader.readLine()) != null) { + // 处理数据接口 + ansMsgHandler.actMsg(is, line); + } + // 当服务器端主动关闭的时候,客户端无法获取到信号。现在还不清楚原因。所以无法执行的此处。 + reader.close(); + } catch (IOException e) { + e.printStackTrace(); + throw new IOException("关闭数据流!"); + } + } + + public static void main(String[] args) throws IOException { + String urlPath = "http://localhost:8081/sse"; + InputStream inputStream = getSseInputStream(urlPath); + readStream(inputStream, new AnsMsgHandler() { + + public void actMsg(InputStream is, String line) { + System.out.println(line); + } + }); + } + +}