mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2026-07-18 02:26:17 +00:00
docs 补充项目注释
This commit is contained in:
@@ -13,6 +13,11 @@ import org.springframework.boot.context.metrics.buffering.BufferingApplicationSt
|
||||
@SpringBootApplication
|
||||
public class DromaraApplication {
|
||||
|
||||
/**
|
||||
* 应用启动入口。
|
||||
*
|
||||
* @param args 启动参数
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
SpringApplication application = new SpringApplication(DromaraApplication.class);
|
||||
application.setApplicationStartup(new BufferingApplicationStartup(2048));
|
||||
|
||||
@@ -10,6 +10,12 @@ import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
|
||||
*/
|
||||
public class DromaraServletInitializer extends SpringBootServletInitializer {
|
||||
|
||||
/**
|
||||
* 配置外部 Web 容器启动源。
|
||||
*
|
||||
* @param application Spring 应用构建器
|
||||
* @return 配置后的 Spring 应用构建器
|
||||
*/
|
||||
@Override
|
||||
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
|
||||
return application.sources(DromaraApplication.class);
|
||||
|
||||
@@ -46,9 +46,15 @@ import java.util.function.Supplier;
|
||||
@Service
|
||||
public class SysLoginService {
|
||||
|
||||
/**
|
||||
* 最大重试次数。
|
||||
*/
|
||||
@Value("${user.password.maxRetryCount}")
|
||||
private Integer maxRetryCount;
|
||||
|
||||
/**
|
||||
* 锁定时间。
|
||||
*/
|
||||
@Value("${user.password.lockTime}")
|
||||
private Integer lockTime;
|
||||
|
||||
|
||||
@@ -34,6 +34,12 @@ public class TaskAssigneeDTO implements Serializable {
|
||||
*/
|
||||
private List<TaskHandler> list;
|
||||
|
||||
/**
|
||||
* 创建任务受让人分页结果。
|
||||
*
|
||||
* @param total 总大小
|
||||
* @param list 受让人列表
|
||||
*/
|
||||
public TaskAssigneeDTO(Long total, List<TaskHandler> list) {
|
||||
this.total = total;
|
||||
this.list = list;
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ import java.util.concurrent.*;
|
||||
* 线程池配置
|
||||
*
|
||||
* @author Lion Li
|
||||
**/
|
||||
*/
|
||||
@Slf4j
|
||||
@AutoConfiguration
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
+3
@@ -20,6 +20,9 @@ import java.util.concurrent.TimeUnit;
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
|
||||
/**
|
||||
* 日期解析格式集合。
|
||||
*/
|
||||
private static final String[] PARSE_PATTERNS = {
|
||||
"yyyy-MM-dd", "yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM",
|
||||
"yyyy/MM/dd", "yyyy/MM/dd HH:mm:ss", "yyyy/MM/dd HH:mm", "yyyy/MM",
|
||||
|
||||
+31
@@ -6,25 +6,56 @@ package org.dromara.common.core.utils.file;
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class MimeTypeUtils {
|
||||
|
||||
/**
|
||||
* PNG 图片 MIME 类型。
|
||||
*/
|
||||
public static final String IMAGE_PNG = "image/png";
|
||||
|
||||
/**
|
||||
* JPG 图片 MIME 类型。
|
||||
*/
|
||||
public static final String IMAGE_JPG = "image/jpg";
|
||||
|
||||
/**
|
||||
* JPEG 图片 MIME 类型。
|
||||
*/
|
||||
public static final String IMAGE_JPEG = "image/jpeg";
|
||||
|
||||
/**
|
||||
* BMP 图片 MIME 类型。
|
||||
*/
|
||||
public static final String IMAGE_BMP = "image/bmp";
|
||||
|
||||
/**
|
||||
* GIF 图片 MIME 类型。
|
||||
*/
|
||||
public static final String IMAGE_GIF = "image/gif";
|
||||
|
||||
/**
|
||||
* 图片扩展名集合。
|
||||
*/
|
||||
public static final String[] IMAGE_EXTENSION = {"bmp", "gif", "jpg", "jpeg", "png"};
|
||||
|
||||
/**
|
||||
* Flash 扩展名集合。
|
||||
*/
|
||||
public static final String[] FLASH_EXTENSION = {"swf", "flv"};
|
||||
|
||||
/**
|
||||
* 媒体扩展名集合。
|
||||
*/
|
||||
public static final String[] MEDIA_EXTENSION = {"swf", "flv", "mp3", "wav", "wma", "wmv", "mid", "avi", "mpg",
|
||||
"asf", "rm", "rmvb"};
|
||||
|
||||
/**
|
||||
* 视频扩展名集合。
|
||||
*/
|
||||
public static final String[] VIDEO_EXTENSION = {"mp4", "avi", "rmvb"};
|
||||
|
||||
/**
|
||||
* 默认允许上传扩展名集合。
|
||||
*/
|
||||
public static final String[] DEFAULT_ALLOWED_EXTENSION = {
|
||||
// 图片
|
||||
"bmp", "gif", "jpg", "jpeg", "png",
|
||||
|
||||
+15
@@ -16,8 +16,16 @@ import java.util.Set;
|
||||
*/
|
||||
public class EnumPatternValidator implements ConstraintValidator<EnumPattern, String> {
|
||||
|
||||
/**
|
||||
* 枚举允许值集合。
|
||||
*/
|
||||
private final Set<String> values = new HashSet<>();
|
||||
|
||||
/**
|
||||
* 初始化枚举允许值集合。
|
||||
*
|
||||
* @param annotation 枚举校验注解
|
||||
*/
|
||||
@Override
|
||||
public void initialize(EnumPattern annotation) {
|
||||
ConstraintValidator.super.initialize(annotation);
|
||||
@@ -33,6 +41,13 @@ public class EnumPatternValidator implements ConstraintValidator<EnumPattern, St
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验字符串是否在枚举允许值集合内。
|
||||
*
|
||||
* @param value 待校验值
|
||||
* @param constraintValidatorContext 校验上下文
|
||||
* @return true 校验通过 false 校验失败
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
|
||||
+7
@@ -13,6 +13,13 @@ import org.dromara.common.core.utils.StringUtils;
|
||||
*/
|
||||
public class XssValidator implements ConstraintValidator<Xss, String> {
|
||||
|
||||
/**
|
||||
* 校验字符串是否包含 HTML 标签。
|
||||
*
|
||||
* @param value 待校验值
|
||||
* @param constraintValidatorContext 校验上下文
|
||||
* @return true 校验通过 false 包含 HTML 标签
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
|
||||
+11
@@ -12,12 +12,23 @@ import org.springframework.core.env.ConfigurableEnvironment;
|
||||
*/
|
||||
public class ActuatorEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
|
||||
|
||||
/**
|
||||
* 根据 easy-es 开关同步设置 Elasticsearch 健康检查开关。
|
||||
*
|
||||
* @param environment Spring 环境配置
|
||||
* @param application Spring 应用实例
|
||||
*/
|
||||
@Override
|
||||
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
|
||||
String enable = environment.getProperty("easy-es.enable", "false");
|
||||
System.setProperty("management.health.elasticsearch.enabled", enable);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回环境后处理器执行顺序。
|
||||
*
|
||||
* @return 最高优先级
|
||||
*/
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return Ordered.HIGHEST_PRECEDENCE;
|
||||
|
||||
+6
-3
@@ -56,7 +56,7 @@ public class EncryptorAutoConfiguration {
|
||||
/**
|
||||
* 创建加密字段处理器。
|
||||
*
|
||||
* @param encryptorManager 加解密管理器
|
||||
* @param encryptorManager 加解密管理器
|
||||
* @param encryptContextFactory 加密上下文工厂
|
||||
* @return 加密字段处理器
|
||||
*/
|
||||
@@ -87,6 +87,11 @@ public class EncryptorAutoConfiguration {
|
||||
return new MybatisDecryptInterceptor(encryptedFieldProcessor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验字段加解密配置完整性。
|
||||
*
|
||||
* @param properties 字段加解密配置
|
||||
*/
|
||||
private void validateEncryptorProperties(EncryptorProperties properties) {
|
||||
AlgorithmType algorithm = properties.getAlgorithm();
|
||||
if (algorithm == AlgorithmType.AES || algorithm == AlgorithmType.SM4) {
|
||||
@@ -102,5 +107,3 @@ public class EncryptorAutoConfiguration {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
+10
-3
@@ -23,7 +23,7 @@ public class EncryptedFieldProcessor {
|
||||
* 构造加密字段处理器。
|
||||
*
|
||||
* @param encryptorManager 加解密管理器
|
||||
* @param contextFactory 加密上下文工厂
|
||||
* @param contextFactory 加密上下文工厂
|
||||
*/
|
||||
public EncryptedFieldProcessor(EncryptorManager encryptorManager, EncryptContextFactory contextFactory) {
|
||||
this.encryptorManager = encryptorManager;
|
||||
@@ -58,6 +58,13 @@ public class EncryptedFieldProcessor {
|
||||
field.set(target, encryptorManager.decrypt(value, contextFactory.create(field))));
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归处理对象、集合或 Map 中声明加密注解的字段。
|
||||
*
|
||||
* @param sourceObject 待处理对象
|
||||
* @param visited 已访问对象集合
|
||||
* @param fieldHandler 字段处理回调
|
||||
*/
|
||||
private void handle(Object sourceObject, Set<Object> visited, FieldHandler fieldHandler) {
|
||||
if (ObjectUtil.isNull(sourceObject) || sourceObject instanceof String || visited.contains(sourceObject)) {
|
||||
return;
|
||||
@@ -100,8 +107,8 @@ public class EncryptedFieldProcessor {
|
||||
* 处理单个加密字段。
|
||||
*
|
||||
* @param target 字段所属对象
|
||||
* @param field 加密字段
|
||||
* @param value 字段原始字符串值
|
||||
* @param field 加密字段
|
||||
* @param value 字段原始字符串值
|
||||
* @throws IllegalAccessException 字段访问失败时抛出
|
||||
*/
|
||||
void handle(Object target, Field field, String value) throws IllegalAccessException;
|
||||
|
||||
+5
@@ -11,6 +11,11 @@ import org.dromara.common.encrypt.core.IEncryptor;
|
||||
*/
|
||||
public abstract class AbstractEncryptor implements IEncryptor {
|
||||
|
||||
/**
|
||||
* 初始化加密执行者。
|
||||
*
|
||||
* @param context 加密上下文
|
||||
*/
|
||||
public AbstractEncryptor(EncryptContext context) {
|
||||
// 用户配置校验与配置注入
|
||||
}
|
||||
|
||||
+5
@@ -13,6 +13,11 @@ import org.dromara.common.encrypt.utils.EncryptUtils;
|
||||
*/
|
||||
public class Base64Encryptor extends AbstractEncryptor {
|
||||
|
||||
/**
|
||||
* 初始化 Base64 加密执行者。
|
||||
*
|
||||
* @param context 加密上下文
|
||||
*/
|
||||
public Base64Encryptor(EncryptContext context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
+6
-1
@@ -17,6 +17,11 @@ public class RsaEncryptor extends AbstractEncryptor {
|
||||
|
||||
private final EncryptContext context;
|
||||
|
||||
/**
|
||||
* 构造 RSA 加密器。
|
||||
*
|
||||
* @param context 加密上下文
|
||||
*/
|
||||
public RsaEncryptor(EncryptContext context) {
|
||||
super(context);
|
||||
String privateKey = context.getPrivateKey();
|
||||
@@ -53,7 +58,7 @@ public class RsaEncryptor extends AbstractEncryptor {
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param value 待加密字符串
|
||||
* @param value 待加密字符串
|
||||
*/
|
||||
@Override
|
||||
public String decrypt(String value) {
|
||||
|
||||
+6
-1
@@ -16,6 +16,11 @@ public class Sm2Encryptor extends AbstractEncryptor {
|
||||
|
||||
private final EncryptContext context;
|
||||
|
||||
/**
|
||||
* 构造 SM2 加密器。
|
||||
*
|
||||
* @param context 加密上下文
|
||||
*/
|
||||
public Sm2Encryptor(EncryptContext context) {
|
||||
super(context);
|
||||
String privateKey = context.getPrivateKey();
|
||||
@@ -52,7 +57,7 @@ public class Sm2Encryptor extends AbstractEncryptor {
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param value 待加密字符串
|
||||
* @param value 待加密字符串
|
||||
*/
|
||||
@Override
|
||||
public String decrypt(String value) {
|
||||
|
||||
+6
-1
@@ -15,6 +15,11 @@ public class Sm4Encryptor extends AbstractEncryptor {
|
||||
|
||||
private final EncryptContext context;
|
||||
|
||||
/**
|
||||
* 构造 SM4 加密器。
|
||||
*
|
||||
* @param context 加密上下文
|
||||
*/
|
||||
public Sm4Encryptor(EncryptContext context) {
|
||||
super(context);
|
||||
this.context = context;
|
||||
@@ -46,7 +51,7 @@ public class Sm4Encryptor extends AbstractEncryptor {
|
||||
/**
|
||||
* 解密
|
||||
*
|
||||
* @param value 待加密字符串
|
||||
* @param value 待加密字符串
|
||||
*/
|
||||
@Override
|
||||
public String decrypt(String value) {
|
||||
|
||||
+17
-2
@@ -32,9 +32,9 @@ public class CryptoFilter implements Filter {
|
||||
/**
|
||||
* 构造加解密过滤器。
|
||||
*
|
||||
* @param properties API 解密配置
|
||||
* @param properties API 解密配置
|
||||
* @param requestMappingHandlerMapping 请求映射处理器
|
||||
* @param handlerExceptionResolver 异常处理器
|
||||
* @param handlerExceptionResolver 异常处理器
|
||||
*/
|
||||
public CryptoFilter(ApiDecryptProperties properties,
|
||||
RequestMappingHandlerMapping requestMappingHandlerMapping,
|
||||
@@ -46,6 +46,15 @@ public class CryptoFilter implements Filter {
|
||||
EncryptUtils.validateRsaPrivateKey(properties.getPrivateKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据接口注解与请求头执行请求解密和响应加密。
|
||||
*
|
||||
* @param request 原始请求
|
||||
* @param response 原始响应
|
||||
* @param chain 过滤器链
|
||||
* @throws IOException IO 异常
|
||||
* @throws ServletException Servlet 异常
|
||||
*/
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
|
||||
HttpServletRequest servletRequest = (HttpServletRequest) request;
|
||||
@@ -96,6 +105,9 @@ public class CryptoFilter implements Filter {
|
||||
|
||||
/**
|
||||
* 获取 ApiEncrypt 注解
|
||||
*
|
||||
* @param servletRequest 当前请求
|
||||
* @return API 加密注解
|
||||
*/
|
||||
private ApiEncrypt getApiEncryptAnnotation(HttpServletRequest servletRequest) {
|
||||
// 获取注解
|
||||
@@ -116,6 +128,9 @@ public class CryptoFilter implements Filter {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁过滤器。
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
+53
@@ -23,6 +23,9 @@ import java.nio.charset.StandardCharsets;
|
||||
*/
|
||||
public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
/**
|
||||
* 请求体字节数据。
|
||||
*/
|
||||
private final byte[] body;
|
||||
|
||||
/**
|
||||
@@ -48,6 +51,11 @@ public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
body = decryptBody.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于解密后的请求体创建字符读取器。
|
||||
*
|
||||
* @return 字符读取器
|
||||
*/
|
||||
@Override
|
||||
public BufferedReader getReader() {
|
||||
Charset charset = Charset.forName(getCharacterEncoding());
|
||||
@@ -55,46 +63,91 @@ public class DecryptRequestBodyWrapper extends HttpServletRequestWrapper {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回解密后请求体长度。
|
||||
*
|
||||
* @return 请求体长度
|
||||
*/
|
||||
@Override
|
||||
public int getContentLength() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回解密后请求体长度。
|
||||
*
|
||||
* @return 请求体长度
|
||||
*/
|
||||
@Override
|
||||
public long getContentLengthLong() {
|
||||
return body.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回解密后的请求体类型。
|
||||
*
|
||||
* @return JSON 内容类型
|
||||
*/
|
||||
@Override
|
||||
public String getContentType() {
|
||||
return MediaType.APPLICATION_JSON_VALUE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 返回基于解密请求体的输入流。
|
||||
*
|
||||
* @return 解密请求体输入流
|
||||
*/
|
||||
@Override
|
||||
public ServletInputStream getInputStream() {
|
||||
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
/**
|
||||
* 读取解密请求体下一个字节。
|
||||
*
|
||||
* @return 下一个字节
|
||||
*/
|
||||
@Override
|
||||
public int read() {
|
||||
return bais.read();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回解密请求体剩余可读字节数。
|
||||
*
|
||||
* @return 剩余字节数
|
||||
*/
|
||||
@Override
|
||||
public int available() {
|
||||
return bais.available();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断解密请求体是否读取完毕。
|
||||
*
|
||||
* @return 是否读取完毕
|
||||
*/
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断解密请求体输入流是否可读。
|
||||
*
|
||||
* @return 固定为 true
|
||||
*/
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步读取监听器。
|
||||
*
|
||||
* @param readListener 读取监听器
|
||||
*/
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
|
||||
|
||||
+66
-1
@@ -6,7 +6,10 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpServletResponseWrapper;
|
||||
import org.dromara.common.encrypt.utils.EncryptUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.PrintWriter;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
@@ -39,6 +42,11 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
this.charset = resolveCharset(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回缓存响应内容的字符输出器。
|
||||
*
|
||||
* @return 字符输出器
|
||||
*/
|
||||
@Override
|
||||
public PrintWriter getWriter() {
|
||||
if (printWriter == null) {
|
||||
@@ -48,6 +56,11 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
return printWriter;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新缓存的响应输出流和字符输出器。
|
||||
*
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public void flushBuffer() throws IOException {
|
||||
if (servletOutputStream != null) {
|
||||
@@ -58,11 +71,17 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置已缓存的响应内容。
|
||||
*/
|
||||
@Override
|
||||
public void reset() {
|
||||
byteArrayOutputStream.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置已缓存的响应缓冲区。
|
||||
*/
|
||||
@Override
|
||||
public void resetBuffer() {
|
||||
byteArrayOutputStream.reset();
|
||||
@@ -122,29 +141,64 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
return encryptContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回缓存响应内容的二进制输出流。
|
||||
*
|
||||
* @return 响应输出流
|
||||
*/
|
||||
@Override
|
||||
public ServletOutputStream getOutputStream() throws IOException {
|
||||
return new ServletOutputStream() {
|
||||
/**
|
||||
* 判断响应输出流是否可写。
|
||||
*
|
||||
* @return 固定为 true
|
||||
*/
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步写入监听器。
|
||||
*
|
||||
* @param writeListener 写入监听器
|
||||
*/
|
||||
@Override
|
||||
public void setWriteListener(WriteListener writeListener) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入单个字节到响应缓存。
|
||||
*
|
||||
* @param b 待写入字节
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
byteArrayOutputStream.write(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入字节数组到响应缓存。
|
||||
*
|
||||
* @param b 待写入字节数组
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
byteArrayOutputStream.write(b);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入字节数组片段到响应缓存。
|
||||
*
|
||||
* @param b 待写入字节数组
|
||||
* @param off 起始偏移量
|
||||
* @param len 写入长度
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
byteArrayOutputStream.write(b, off, len);
|
||||
@@ -152,6 +206,12 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应字符集,未设置时默认使用 UTF-8。
|
||||
*
|
||||
* @param response 原始响应
|
||||
* @return 响应字符集
|
||||
*/
|
||||
private Charset resolveCharset(HttpServletResponse response) {
|
||||
String characterEncoding = response.getCharacterEncoding();
|
||||
if (characterEncoding == null) {
|
||||
@@ -160,6 +220,11 @@ public class EncryptResponseBodyWrapper extends HttpServletResponseWrapper {
|
||||
return Charset.forName(characterEncoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成响应内容 AES 加密密钥。
|
||||
*
|
||||
* @return AES 密钥
|
||||
*/
|
||||
private String generateAesPassword() {
|
||||
byte[] bytes = new byte[24];
|
||||
SECURE_RANDOM.nextBytes(bytes);
|
||||
|
||||
+18
@@ -23,6 +23,13 @@ public class MybatisDecryptInterceptor implements Interceptor {
|
||||
|
||||
private final EncryptedFieldProcessor encryptedFieldProcessor;
|
||||
|
||||
/**
|
||||
* 解密 MyBatis 查询结果中的加密字段。
|
||||
*
|
||||
* @param invocation 拦截调用信息
|
||||
* @return 查询结果
|
||||
* @throws Throwable 拦截处理异常
|
||||
*/
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
// 获取执行mysql执行结果
|
||||
@@ -34,11 +41,22 @@ public class MybatisDecryptInterceptor implements Interceptor {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装 MyBatis 目标对象。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @return 包装后的对象
|
||||
*/
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return Plugin.wrap(target, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置插件属性。
|
||||
*
|
||||
* @param properties 插件属性
|
||||
*/
|
||||
@Override
|
||||
public void setProperties(Properties properties) {
|
||||
|
||||
|
||||
+18
@@ -25,6 +25,13 @@ public class MybatisEncryptInterceptor implements Interceptor {
|
||||
|
||||
private final EncryptedFieldProcessor encryptedFieldProcessor;
|
||||
|
||||
/**
|
||||
* 加密 MyBatis 入参中的加密字段,并在执行后恢复原始值。
|
||||
*
|
||||
* @param invocation 拦截调用信息
|
||||
* @return MyBatis 执行结果
|
||||
* @throws Throwable 拦截处理异常
|
||||
*/
|
||||
@Override
|
||||
public Object intercept(Invocation invocation) throws Throwable {
|
||||
List<EncryptedFieldProcessor.FieldSnapshot> snapshots = List.of();
|
||||
@@ -44,11 +51,22 @@ public class MybatisEncryptInterceptor implements Interceptor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装 MyBatis 目标对象。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @return 包装后的对象
|
||||
*/
|
||||
@Override
|
||||
public Object plugin(Object target) {
|
||||
return Plugin.wrap(target, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置插件属性。
|
||||
*
|
||||
* @param properties 插件属性
|
||||
*/
|
||||
@Override
|
||||
public void setProperties(Properties properties) {
|
||||
}
|
||||
|
||||
+5
@@ -381,6 +381,11 @@ public class EncryptUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 RSA 密钥长度是否满足最低安全要求。
|
||||
*
|
||||
* @param rsaKey RSA 密钥
|
||||
*/
|
||||
private static void validateRsaKeySize(RSAKey rsaKey) {
|
||||
int keySize = rsaKey.getModulus().bitLength();
|
||||
if (keySize < MIN_RSA_KEY_SIZE) {
|
||||
|
||||
+26
@@ -21,21 +21,47 @@ import java.math.BigDecimal;
|
||||
@Slf4j
|
||||
public class ExcelBigNumberConvert implements Converter<Long> {
|
||||
|
||||
/**
|
||||
* 支持的 Java 类型。
|
||||
*
|
||||
* @return Long 类型
|
||||
*/
|
||||
@Override
|
||||
public Class<Long> supportJavaTypeKey() {
|
||||
return Long.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的 Excel 单元格类型。
|
||||
*
|
||||
* @return 默认支持全部类型
|
||||
*/
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Excel 单元格数据转换为 Long。
|
||||
*
|
||||
* @param cellData 单元格数据
|
||||
* @param contentProperty 内容属性
|
||||
* @param globalConfiguration 全局配置
|
||||
* @return Long 值
|
||||
*/
|
||||
@Override
|
||||
public Long convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
return Convert.toLong(cellData.getStringValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Long 转换为 Excel 单元格数据,超长数字按字符串写出。
|
||||
*
|
||||
* @param object Java 值
|
||||
* @param contentProperty 内容属性
|
||||
* @param globalConfiguration 全局配置
|
||||
* @return Excel 写入数据
|
||||
*/
|
||||
@Override
|
||||
public WriteCellData<Object> convertToExcelData(Long object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
if (ObjectUtil.isNull(object)) {
|
||||
|
||||
+62
-2
@@ -3,17 +3,17 @@ package org.dromara.common.excel.convert;
|
||||
import cn.hutool.core.annotation.AnnotationUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fesod.sheet.converters.Converter;
|
||||
import org.apache.fesod.sheet.enums.CellDataTypeEnum;
|
||||
import org.apache.fesod.sheet.metadata.GlobalConfiguration;
|
||||
import org.apache.fesod.sheet.metadata.data.ReadCellData;
|
||||
import org.apache.fesod.sheet.metadata.data.WriteCellData;
|
||||
import org.apache.fesod.sheet.metadata.property.ExcelContentProperty;
|
||||
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
||||
import org.dromara.common.core.service.DictService;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -30,16 +30,34 @@ public class ExcelDictConvert implements Converter<Object> {
|
||||
|
||||
private DictService dictService;
|
||||
|
||||
/**
|
||||
* 支持的 Java 类型。
|
||||
*
|
||||
* @return Object 类型
|
||||
*/
|
||||
@Override
|
||||
public Class<Object> supportJavaTypeKey() {
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的 Excel 单元格类型。
|
||||
*
|
||||
* @return 默认支持全部类型
|
||||
*/
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Excel 字典显示值转换为 Java 字段值。
|
||||
*
|
||||
* @param cellData 单元格数据
|
||||
* @param contentProperty 内容属性
|
||||
* @param globalConfiguration 全局配置
|
||||
* @return Java 字段值
|
||||
*/
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
ExcelDictFormat anno = getAnnotation(contentProperty.getField());
|
||||
@@ -54,6 +72,14 @@ public class ExcelDictConvert implements Converter<Object> {
|
||||
return Convert.convert(contentProperty.getField().getType(), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Java 字段值转换为 Excel 字典显示值。
|
||||
*
|
||||
* @param object Java 字段值
|
||||
* @param contentProperty 内容属性
|
||||
* @param globalConfiguration 全局配置
|
||||
* @return Excel 写入数据
|
||||
*/
|
||||
@Override
|
||||
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
if (ObjectUtil.isNull(object)) {
|
||||
@@ -71,10 +97,21 @@ public class ExcelDictConvert implements Converter<Object> {
|
||||
return new WriteCellData<>(label);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段上的字典格式注解。
|
||||
*
|
||||
* @param field 字段
|
||||
* @return 字典格式注解
|
||||
*/
|
||||
private ExcelDictFormat getAnnotation(Field field) {
|
||||
return AnnotationUtil.getAnnotation(field, ExcelDictFormat.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取字典服务。
|
||||
*
|
||||
* @return 字典服务
|
||||
*/
|
||||
private DictService getDictService() {
|
||||
if (dictService == null) {
|
||||
dictService = SpringUtils.getBean(DictService.class);
|
||||
@@ -84,6 +121,11 @@ public class ExcelDictConvert implements Converter<Object> {
|
||||
|
||||
/**
|
||||
* 解析导出值 0=男,1=女,2=未知。
|
||||
*
|
||||
* @param propertyValue 字段值
|
||||
* @param converterExp 转换表达式
|
||||
* @param separator 分隔符
|
||||
* @return 导出显示值
|
||||
*/
|
||||
private static String convertByExp(String propertyValue, String converterExp, String separator) {
|
||||
StringBuilder propertyString = new StringBuilder();
|
||||
@@ -107,6 +149,11 @@ public class ExcelDictConvert implements Converter<Object> {
|
||||
|
||||
/**
|
||||
* 反向解析值 男=0,女=1,未知=2。
|
||||
*
|
||||
* @param propertyValue 显示值
|
||||
* @param converterExp 转换表达式
|
||||
* @param separator 分隔符
|
||||
* @return 字段值
|
||||
*/
|
||||
private static String reverseByExp(String propertyValue, String converterExp, String separator) {
|
||||
StringBuilder propertyString = new StringBuilder();
|
||||
@@ -128,6 +175,12 @@ public class ExcelDictConvert implements Converter<Object> {
|
||||
return StringUtils.stripEnd(propertyString.toString(), separator);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析转换表达式。
|
||||
*
|
||||
* @param converterExp 转换表达式
|
||||
* @return 字段值与显示值映射
|
||||
*/
|
||||
private static Map<String, String> parseConverterExp(String converterExp) {
|
||||
Map<String, String> result = new LinkedHashMap<>();
|
||||
if (StringUtils.isBlank(converterExp)) {
|
||||
@@ -143,6 +196,13 @@ public class ExcelDictConvert implements Converter<Object> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按分隔符拆分多值字段。
|
||||
*
|
||||
* @param propertyValue 字段值
|
||||
* @param separator 分隔符
|
||||
* @return 拆分后的字段值数组
|
||||
*/
|
||||
private static String[] splitPropertyValue(String propertyValue, String separator) {
|
||||
return propertyValue.split(Pattern.quote(separator));
|
||||
}
|
||||
|
||||
+44
@@ -26,19 +26,43 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
@Slf4j
|
||||
public class ExcelEnumConvert implements Converter<Object> {
|
||||
|
||||
/**
|
||||
* 枚举字典缓存。
|
||||
*/
|
||||
private static final Map<Field, Map<Object, String>> ENUM_MAP_CACHE = new ConcurrentHashMap<>();
|
||||
/**
|
||||
* 枚举反向字典缓存。
|
||||
*/
|
||||
private static final Map<Field, Map<String, Object>> ENUM_REVERSE_MAP_CACHE = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 支持的 Java 类型。
|
||||
*
|
||||
* @return Object 类型
|
||||
*/
|
||||
@Override
|
||||
public Class<Object> supportJavaTypeKey() {
|
||||
return Object.class;
|
||||
}
|
||||
|
||||
/**
|
||||
* 支持的 Excel 单元格类型。
|
||||
*
|
||||
* @return 默认支持全部类型
|
||||
*/
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Excel 枚举文本转换为 Java 字段值。
|
||||
*
|
||||
* @param cellData 单元格数据
|
||||
* @param contentProperty 内容属性
|
||||
* @param globalConfiguration 全局配置
|
||||
* @return Java 字段值
|
||||
*/
|
||||
@Override
|
||||
public Object convertToJavaData(ReadCellData<?> cellData, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
cellData.checkEmpty();
|
||||
@@ -76,6 +100,14 @@ public class ExcelEnumConvert implements Converter<Object> {
|
||||
return Convert.convert(contentProperty.getField().getType(), codeValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 Java 枚举编码转换为 Excel 显示文本。
|
||||
*
|
||||
* @param object Java 字段值
|
||||
* @param contentProperty 内容属性
|
||||
* @param globalConfiguration 全局配置
|
||||
* @return Excel 写入数据
|
||||
*/
|
||||
@Override
|
||||
public WriteCellData<String> convertToExcelData(Object object, ExcelContentProperty contentProperty, GlobalConfiguration globalConfiguration) {
|
||||
if (ObjectUtil.isNull(object)) {
|
||||
@@ -86,6 +118,12 @@ public class ExcelEnumConvert implements Converter<Object> {
|
||||
return new WriteCellData<>(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备枚举编码与显示文本映射。
|
||||
*
|
||||
* @param contentProperty 内容属性
|
||||
* @return 枚举编码与显示文本映射
|
||||
*/
|
||||
private Map<Object, String> beforeConvert(ExcelContentProperty contentProperty) {
|
||||
return ENUM_MAP_CACHE.computeIfAbsent(contentProperty.getField(), field -> {
|
||||
ExcelEnumFormat anno = getAnnotation(field);
|
||||
@@ -103,6 +141,12 @@ public class ExcelEnumConvert implements Converter<Object> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字段上的枚举格式注解。
|
||||
*
|
||||
* @param field 字段
|
||||
* @return 枚举格式注解
|
||||
*/
|
||||
private ExcelEnumFormat getAnnotation(Field field) {
|
||||
return AnnotationUtil.getAnnotation(field, ExcelEnumFormat.class);
|
||||
}
|
||||
|
||||
+55
@@ -21,14 +21,32 @@ import java.util.*;
|
||||
*/
|
||||
public class CellMergeHandler {
|
||||
|
||||
/**
|
||||
* 是否包含标题行。
|
||||
*/
|
||||
private final boolean hasTitle;
|
||||
/**
|
||||
* 当前行索引。
|
||||
*/
|
||||
private int rowIndex;
|
||||
|
||||
/**
|
||||
* 创建单元格合并处理器。
|
||||
*
|
||||
* @param hasTitle 是否存在标题行
|
||||
*/
|
||||
private CellMergeHandler(final boolean hasTitle) {
|
||||
this.hasTitle = hasTitle;
|
||||
// 行合并开始下标
|
||||
this.rowIndex = hasTitle ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建单元格合并处理器。
|
||||
*
|
||||
* @param hasTitle 是否存在标题行
|
||||
* @param rowIndex 起始行索引
|
||||
*/
|
||||
private CellMergeHandler(final boolean hasTitle, final int rowIndex) {
|
||||
this.hasTitle = hasTitle;
|
||||
this.rowIndex = hasTitle ? rowIndex : 0;
|
||||
@@ -143,6 +161,14 @@ public class CellMergeHandler {
|
||||
return mergeFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断当前行是否满足依赖字段合并条件。
|
||||
*
|
||||
* @param currentRow 当前行数据
|
||||
* @param preRow 上一行数据
|
||||
* @param cellMerge 合并配置
|
||||
* @return 是否允许合并
|
||||
*/
|
||||
private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
|
||||
final String[] mergeBy = cellMerge.mergeBy();
|
||||
if (StrUtil.isAllNotBlank(mergeBy)) {
|
||||
@@ -159,10 +185,24 @@ public class CellMergeHandler {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断单元格值是否为空。
|
||||
*
|
||||
* @param value 单元格值
|
||||
* @return 是否为空
|
||||
*/
|
||||
private boolean isBlankCell(Object value) {
|
||||
return value == null || StrUtil.isBlankIfStr(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加有效的合并区域。
|
||||
*
|
||||
* @param result 合并区域结果集
|
||||
* @param repeatCell 连续重复单元格信息
|
||||
* @param endIndex 结束行索引
|
||||
* @param colNum 列索引
|
||||
*/
|
||||
private void appendMergeResult(List<CellRangeAddress> result, RepeatCell repeatCell, int endIndex, int colNum) {
|
||||
if (repeatCell == null || endIndex <= repeatCell.current()) {
|
||||
return;
|
||||
@@ -174,6 +214,13 @@ public class CellMergeHandler {
|
||||
* 单元格合并
|
||||
*/
|
||||
record RepeatCell(Object value, int current) {
|
||||
/**
|
||||
* 创建连续重复单元格信息。
|
||||
*
|
||||
* @param value 单元格值
|
||||
* @param current 当前行索引
|
||||
* @return 连续重复单元格信息
|
||||
*/
|
||||
static RepeatCell of(Object value, int current) {
|
||||
return new RepeatCell(value, current);
|
||||
}
|
||||
@@ -183,10 +230,18 @@ public class CellMergeHandler {
|
||||
* 字段列索引和合并注解信息
|
||||
*/
|
||||
record FieldColumnIndex(int colIndex, CellMerge cellMerge) {
|
||||
/**
|
||||
* 创建字段列索引和合并注解信息。
|
||||
*
|
||||
* @param colIndex 列索引
|
||||
* @param cellMerge 合并注解
|
||||
* @return 字段列索引和合并注解信息
|
||||
*/
|
||||
static FieldColumnIndex of(int colIndex, CellMerge cellMerge) {
|
||||
return new FieldColumnIndex(colIndex, cellMerge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个单元格合并处理器实例
|
||||
*
|
||||
|
||||
+35
@@ -21,20 +21,49 @@ import java.util.List;
|
||||
@Slf4j
|
||||
public class CellMergeStrategy extends AbstractMergeStrategy implements SheetWriteHandler {
|
||||
|
||||
/**
|
||||
* 合并单元格区域集合。
|
||||
*/
|
||||
private final List<CellRangeAddress> cellList;
|
||||
|
||||
/**
|
||||
* 创建单元格合并策略。
|
||||
*
|
||||
* @param cellList 合并区域列表
|
||||
*/
|
||||
public CellMergeStrategy(List<CellRangeAddress> cellList) {
|
||||
this.cellList = cellList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据列表创建单元格合并策略。
|
||||
*
|
||||
* @param list 数据列表
|
||||
* @param hasTitle 是否存在标题行
|
||||
*/
|
||||
public CellMergeStrategy(List<?> list, boolean hasTitle) {
|
||||
this.cellList = CellMergeHandler.of(hasTitle).handle(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据数据列表和起始行创建单元格合并策略。
|
||||
*
|
||||
* @param list 数据列表
|
||||
* @param hasTitle 是否存在标题行
|
||||
* @param rowIndex 起始行索引
|
||||
*/
|
||||
public CellMergeStrategy(List<?> list, boolean hasTitle, int rowIndex) {
|
||||
this.cellList = CellMergeHandler.of(hasTitle, rowIndex).handle(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入单元格时清理合并区域非首行内容。
|
||||
*
|
||||
* @param sheet 工作表
|
||||
* @param cell 当前单元格
|
||||
* @param head 表头
|
||||
* @param relativeRowIndex 相对行索引
|
||||
*/
|
||||
@Override
|
||||
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
|
||||
if (CollUtil.isEmpty(cellList)) {
|
||||
@@ -50,6 +79,12 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements SheetWri
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作表创建后写入合并区域。
|
||||
*
|
||||
* @param writeWorkbookHolder 工作簿上下文
|
||||
* @param writeSheetHolder 工作表上下文
|
||||
*/
|
||||
@Override
|
||||
public void afterSheetCreate(final WriteWorkbookHolder writeWorkbookHolder, final WriteSheetHolder writeSheetHolder) {
|
||||
if (CollUtil.isEmpty(cellList)) {
|
||||
|
||||
+22
@@ -103,12 +103,24 @@ public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 Excel 表头信息。
|
||||
*
|
||||
* @param headMap 表头映射
|
||||
* @param context 解析上下文
|
||||
*/
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
|
||||
this.headMap = headMap;
|
||||
log.debug("解析到一条表头数据: {}", JsonUtils.toJsonString(headMap));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理一行导入数据。
|
||||
*
|
||||
* @param data 行数据
|
||||
* @param context 解析上下文
|
||||
*/
|
||||
@Override
|
||||
public void invoke(T data, AnalysisContext context) {
|
||||
if (isValidate) {
|
||||
@@ -117,11 +129,21 @@ public class DefaultExcelListener<T> extends AnalysisEventListener<T> implements
|
||||
excelResult.getList().add(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 全部数据解析完成回调。
|
||||
*
|
||||
* @param context 解析上下文
|
||||
*/
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
log.debug("所有数据解析完成!");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Excel 导入解析结果。
|
||||
*
|
||||
* @return 导入解析结果
|
||||
*/
|
||||
@Override
|
||||
public ExcelResult<T> getExcelResult() {
|
||||
return excelResult;
|
||||
|
||||
+24
@@ -26,26 +26,50 @@ public class DefaultExcelResult<T> implements ExcelResult<T> {
|
||||
@Setter
|
||||
private List<String> errorList;
|
||||
|
||||
/**
|
||||
* 创建默认导入结果。
|
||||
*/
|
||||
public DefaultExcelResult() {
|
||||
this.list = new ArrayList<>();
|
||||
this.errorList = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建导入结果。
|
||||
*
|
||||
* @param list 成功数据列表
|
||||
* @param errorList 错误信息列表
|
||||
*/
|
||||
public DefaultExcelResult(List<T> list, List<String> errorList) {
|
||||
this.list = list;
|
||||
this.errorList = errorList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制导入结果。
|
||||
*
|
||||
* @param excelResult 原导入结果
|
||||
*/
|
||||
public DefaultExcelResult(ExcelResult<T> excelResult) {
|
||||
this.list = excelResult.getList();
|
||||
this.errorList = excelResult.getErrorList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取成功数据列表。
|
||||
*
|
||||
* @return 成功数据列表
|
||||
*/
|
||||
@Override
|
||||
public List<T> getList() {
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误信息列表。
|
||||
*
|
||||
* @return 错误信息列表
|
||||
*/
|
||||
@Override
|
||||
public List<String> getErrorList() {
|
||||
return errorList;
|
||||
|
||||
+12
-1
@@ -6,13 +6,13 @@ import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.EnumUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fesod.sheet.metadata.FieldCache;
|
||||
import org.apache.fesod.sheet.metadata.FieldWrapper;
|
||||
import org.apache.fesod.sheet.util.ClassUtils;
|
||||
import org.apache.fesod.sheet.write.handler.SheetWriteHandler;
|
||||
import org.apache.fesod.sheet.write.metadata.holder.WriteSheetHolder;
|
||||
import org.apache.fesod.sheet.write.metadata.holder.WriteWorkbookHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.*;
|
||||
import org.apache.poi.ss.util.CellRangeAddressList;
|
||||
import org.apache.poi.ss.util.CellReference;
|
||||
@@ -41,7 +41,13 @@ import java.util.*;
|
||||
@Slf4j
|
||||
public class ExcelDownHandler implements SheetWriteHandler {
|
||||
|
||||
/**
|
||||
* 首个数据行索引。
|
||||
*/
|
||||
private static final int FIRST_DATA_ROW_INDEX = 1;
|
||||
/**
|
||||
* 最后数据行索引。
|
||||
*/
|
||||
private static final int LAST_DATA_ROW_INDEX = 1000;
|
||||
/**
|
||||
* 单选数据Sheet名
|
||||
@@ -411,6 +417,11 @@ public class ExcelDownHandler implements SheetWriteHandler {
|
||||
return CellReference.convertNumToColString(columnIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 延迟获取字典服务。
|
||||
*
|
||||
* @return 字典服务
|
||||
*/
|
||||
private DictService getDictService() {
|
||||
if (dictService == null) {
|
||||
dictService = SpringUtils.getBean(DictService.class);
|
||||
|
||||
+5
@@ -9,6 +9,11 @@ import org.apache.fesod.sheet.read.listener.ReadListener;
|
||||
*/
|
||||
public interface ExcelListener<T> extends ReadListener<T> {
|
||||
|
||||
/**
|
||||
* 获取 Excel 导入结果。
|
||||
*
|
||||
* @return Excel 导入结果
|
||||
*/
|
||||
ExcelResult<T> getExcelResult();
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -50,6 +50,11 @@ public class DataWriteHandler implements SheetWriteHandler, CellWriteHandler {
|
||||
headColumnMap = getRequiredMap(clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在单元格写入后设置表头文本格式、必填样式与批注。
|
||||
*
|
||||
* @param context 单元格写入上下文
|
||||
*/
|
||||
@Override
|
||||
public void afterCellDispose(CellWriteHandlerContext context) {
|
||||
if (CollUtil.isEmpty(notationMap) && CollUtil.isEmpty(headColumnMap)) {
|
||||
|
||||
+210
@@ -55,52 +55,127 @@ import java.util.zip.ZipOutputStream;
|
||||
*/
|
||||
public final class ExcelBuilder<T> {
|
||||
|
||||
/**
|
||||
* 默认工作表名称。
|
||||
*/
|
||||
private static final String DEFAULT_SHEET_NAME = "sheet1";
|
||||
|
||||
/**
|
||||
* 默认 ZIP 分页大小。
|
||||
*/
|
||||
private static final int DEFAULT_ZIP_PAGE_SIZE = 999;
|
||||
|
||||
/**
|
||||
* 数据内容。
|
||||
*/
|
||||
private final List<T> data;
|
||||
|
||||
/**
|
||||
* 表头类型。
|
||||
*/
|
||||
private final Class<T> headType;
|
||||
|
||||
/**
|
||||
* 工作表名称。
|
||||
*/
|
||||
private String sheetName = DEFAULT_SHEET_NAME;
|
||||
|
||||
/**
|
||||
* 工作表编号。
|
||||
*/
|
||||
private Integer sheetNo;
|
||||
|
||||
/**
|
||||
* 是否合并单元格。
|
||||
*/
|
||||
private boolean merge;
|
||||
|
||||
/**
|
||||
* 下拉选项集合。
|
||||
*/
|
||||
private List<DropDownOptions> options;
|
||||
|
||||
/**
|
||||
* 是否 ZIP 打包。
|
||||
*/
|
||||
private boolean zip;
|
||||
|
||||
/**
|
||||
* 分页大小。
|
||||
*/
|
||||
private int pageSize = DEFAULT_ZIP_PAGE_SIZE;
|
||||
|
||||
/**
|
||||
* 认证密码。
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 是否写入表头。
|
||||
*/
|
||||
private Boolean needHead;
|
||||
|
||||
/**
|
||||
* 是否自动合并表头。
|
||||
*/
|
||||
private Boolean automaticMergeHead;
|
||||
|
||||
/**
|
||||
* 包含字段集合。
|
||||
*/
|
||||
private Collection<String> includeFields;
|
||||
|
||||
/**
|
||||
* 排除字段集合。
|
||||
*/
|
||||
private Collection<String> excludeFields;
|
||||
|
||||
/**
|
||||
* 包含列索引集合。
|
||||
*/
|
||||
private Collection<Integer> includeIndexes;
|
||||
|
||||
/**
|
||||
* 排除列索引集合。
|
||||
*/
|
||||
private Collection<Integer> excludeIndexes;
|
||||
|
||||
/**
|
||||
* 是否按包含列排序。
|
||||
*/
|
||||
private Boolean orderByIncludeColumn;
|
||||
|
||||
/**
|
||||
* 列宽。
|
||||
*/
|
||||
private Integer columnWidth;
|
||||
|
||||
/**
|
||||
* 表头行高。
|
||||
*/
|
||||
private Short headRowHeight;
|
||||
|
||||
/**
|
||||
* 内容行高。
|
||||
*/
|
||||
private Short contentRowHeight;
|
||||
|
||||
/**
|
||||
* 写入处理器集合。
|
||||
*/
|
||||
private List<WriteHandler> writeHandlers;
|
||||
|
||||
/**
|
||||
* 转换器集合。
|
||||
*/
|
||||
private List<Converter<?>> converters;
|
||||
|
||||
/**
|
||||
* 创建 Excel 导出构造器。
|
||||
*
|
||||
* @param data 导出数据
|
||||
* @param headType 表头类型
|
||||
*/
|
||||
private ExcelBuilder(List<T> data, Class<T> headType) {
|
||||
this.data = data;
|
||||
this.headType = headType;
|
||||
@@ -373,6 +448,11 @@ public final class ExcelBuilder<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入单个工作表。
|
||||
*
|
||||
* @param outputStream 输出流
|
||||
*/
|
||||
private void writeSheet(OutputStream outputStream) {
|
||||
ExcelWriterSheetBuilder builder = createSheetBuilder(createWriterBuilder(outputStream));
|
||||
if (merge) {
|
||||
@@ -382,10 +462,22 @@ public final class ExcelBuilder<T> {
|
||||
builder.doWrite(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Excel 写入器。
|
||||
*
|
||||
* @param outputStream 输出流
|
||||
* @return Excel 写入器
|
||||
*/
|
||||
private ExcelWriter createWriter(OutputStream outputStream) {
|
||||
return createWriterBuilder(outputStream).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并配置 Excel 写入构造器。
|
||||
*
|
||||
* @param outputStream 输出流
|
||||
* @return Excel 写入构造器
|
||||
*/
|
||||
private ExcelWriterBuilder createWriterBuilder(OutputStream outputStream) {
|
||||
ExcelWriterBuilder builder = FesodSheet.write(outputStream, headType)
|
||||
.autoCloseStream(false)
|
||||
@@ -437,6 +529,12 @@ public final class ExcelBuilder<T> {
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作表写入构造器。
|
||||
*
|
||||
* @param builder Excel 写入构造器
|
||||
* @return 工作表写入构造器
|
||||
*/
|
||||
private ExcelWriterSheetBuilder createSheetBuilder(ExcelWriterBuilder builder) {
|
||||
if (sheetNo != null) {
|
||||
return builder.sheet(sheetNo, sheetName);
|
||||
@@ -444,6 +542,11 @@ public final class ExcelBuilder<T> {
|
||||
return builder.sheet(sheetName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将大数据导出拆分为多个 Excel 文件并压缩写入响应。
|
||||
*
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
private void exportZipToResponse(HttpServletResponse response) {
|
||||
if (pageSize <= 0) {
|
||||
throw new IllegalArgumentException("pageSize 必须大于 0");
|
||||
@@ -474,6 +577,13 @@ public final class ExcelBuilder<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 ZIP 内单个 Excel 文件内容。
|
||||
*
|
||||
* @param pageData 当前分页数据
|
||||
* @param exportSheetName 导出工作表名称
|
||||
* @return Excel 文件字节
|
||||
*/
|
||||
private byte[] buildZipEntry(List<T> pageData, String exportSheetName) {
|
||||
try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
|
||||
ExcelBuilder<T> builder = ExcelBuilder.of(pageData, headType)
|
||||
@@ -486,6 +596,11 @@ public final class ExcelBuilder<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制导出配置到分页导出构造器。
|
||||
*
|
||||
* @param builder 目标构造器
|
||||
*/
|
||||
private void copyOptionsTo(ExcelBuilder<T> builder) {
|
||||
builder.merge = merge;
|
||||
builder.options = options;
|
||||
@@ -506,12 +621,22 @@ public final class ExcelBuilder<T> {
|
||||
|
||||
/**
|
||||
* 重置响应体。
|
||||
*
|
||||
* @param filename 文件名
|
||||
* @param response HTTP 响应
|
||||
* @throws UnsupportedEncodingException 文件名编码异常
|
||||
*/
|
||||
private static void resetResponse(String filename, HttpServletResponse response) throws UnsupportedEncodingException {
|
||||
FileUtils.setAttachmentResponseHeader(response, encodingFilename(filename));
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带随机前缀的导出文件名。
|
||||
*
|
||||
* @param filename 原始文件名
|
||||
* @return 编码前的导出文件名
|
||||
*/
|
||||
private static String encodingFilename(String filename) {
|
||||
return IdUtil.fastSimpleUUID() + "_" + filename + ".xlsx";
|
||||
}
|
||||
@@ -521,34 +646,82 @@ public final class ExcelBuilder<T> {
|
||||
*/
|
||||
public static final class ReadBuilder<T> {
|
||||
|
||||
/**
|
||||
* 导入输入流。
|
||||
*/
|
||||
private final InputStream inputStream;
|
||||
|
||||
/**
|
||||
* 表头类型。
|
||||
*/
|
||||
private final Class<T> headType;
|
||||
|
||||
/**
|
||||
* 是否执行数据校验。
|
||||
*/
|
||||
private boolean validate = true;
|
||||
|
||||
/**
|
||||
* 是否快速失败。
|
||||
*/
|
||||
private boolean failFast = true;
|
||||
|
||||
/**
|
||||
* Excel 监听器。
|
||||
*/
|
||||
private ExcelListener<T> listener;
|
||||
|
||||
/**
|
||||
* 工作表编号。
|
||||
*/
|
||||
private Integer sheetNo;
|
||||
|
||||
/**
|
||||
* 工作表名称。
|
||||
*/
|
||||
private String sheetName;
|
||||
|
||||
/**
|
||||
* 表头行数。
|
||||
*/
|
||||
private Integer headRowNumber;
|
||||
|
||||
/**
|
||||
* 是否忽略空行。
|
||||
*/
|
||||
private Boolean ignoreEmptyRow;
|
||||
|
||||
/**
|
||||
* 认证密码。
|
||||
*/
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 是否自动裁剪空白。
|
||||
*/
|
||||
private Boolean autoTrim;
|
||||
|
||||
/**
|
||||
* 是否自动清理不可见字符。
|
||||
*/
|
||||
private Boolean autoStrip;
|
||||
|
||||
/**
|
||||
* 读取行数。
|
||||
*/
|
||||
private Integer numRows;
|
||||
|
||||
/**
|
||||
* 转换器集合。
|
||||
*/
|
||||
private List<Converter<?>> converters;
|
||||
|
||||
/**
|
||||
* 创建 Excel 导入读取构造器。
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @param headType 表头类型
|
||||
*/
|
||||
private ReadBuilder(InputStream inputStream, Class<T> headType) {
|
||||
this.inputStream = inputStream;
|
||||
this.headType = headType;
|
||||
@@ -700,6 +873,12 @@ public final class ExcelBuilder<T> {
|
||||
return createReaderBuilder(null).doReadAllSync();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建并配置 Excel 读取构造器。
|
||||
*
|
||||
* @param readListener 读取监听器
|
||||
* @return Excel 读取构造器
|
||||
*/
|
||||
private ExcelReaderBuilder createReaderBuilder(ExcelListener<T> readListener) {
|
||||
ExcelReaderBuilder builder = FesodSheet.read(inputStream)
|
||||
.head(headType)
|
||||
@@ -731,6 +910,12 @@ public final class ExcelBuilder<T> {
|
||||
return builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建工作表读取构造器。
|
||||
*
|
||||
* @param builder Excel 读取构造器
|
||||
* @return 工作表读取构造器
|
||||
*/
|
||||
private ExcelReaderSheetBuilder createSheetBuilder(ExcelReaderBuilder builder) {
|
||||
ExcelReaderSheetBuilder sheetBuilder;
|
||||
if (sheetNo != null && StringUtils.isNotBlank(sheetName)) {
|
||||
@@ -754,14 +939,31 @@ public final class ExcelBuilder<T> {
|
||||
*/
|
||||
public static final class TemplateBuilder {
|
||||
|
||||
/**
|
||||
* 模板路径。
|
||||
*/
|
||||
private final String templatePath;
|
||||
|
||||
/**
|
||||
* 文件名称。
|
||||
*/
|
||||
private String filename = DEFAULT_SHEET_NAME;
|
||||
|
||||
/**
|
||||
* 模板模式。
|
||||
*/
|
||||
private TemplateMode mode;
|
||||
|
||||
/**
|
||||
* 数据内容。
|
||||
*/
|
||||
private Object data;
|
||||
|
||||
/**
|
||||
* 创建 Excel 模板导出构造器。
|
||||
*
|
||||
* @param templatePath 模板路径
|
||||
*/
|
||||
private TemplateBuilder(String templatePath) {
|
||||
this.templatePath = templatePath;
|
||||
}
|
||||
@@ -838,6 +1040,11 @@ public final class ExcelBuilder<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按模板模式填充数据。
|
||||
*
|
||||
* @param excelWriter Excel 写入器
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private void fill(ExcelWriter excelWriter) {
|
||||
if (mode == TemplateMode.LIST) {
|
||||
@@ -874,6 +1081,9 @@ public final class ExcelBuilder<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验模板导出数据。
|
||||
*/
|
||||
private void validateData() {
|
||||
if (mode == null || data == null) {
|
||||
throw new IllegalArgumentException("数据为空");
|
||||
|
||||
+3
@@ -17,6 +17,9 @@ public class BigNumberSerializer extends NumberSerializer {
|
||||
* 根据 JS Number.MAX_SAFE_INTEGER 与 Number.MIN_SAFE_INTEGER 得来
|
||||
*/
|
||||
private static final long MAX_SAFE_INTEGER = 9007199254740991L;
|
||||
/**
|
||||
* JavaScript 最小安全整数。
|
||||
*/
|
||||
private static final long MIN_SAFE_INTEGER = -9007199254740991L;
|
||||
|
||||
/**
|
||||
|
||||
+64
-2
@@ -19,32 +19,74 @@ import java.util.*;
|
||||
*/
|
||||
public final class MailBuilder {
|
||||
|
||||
/**
|
||||
* 邮件账户配置。
|
||||
*/
|
||||
private MailAccount mailAccount;
|
||||
|
||||
/**
|
||||
* 是否使用全局邮件会话。
|
||||
*/
|
||||
private boolean useGlobalSession = true;
|
||||
|
||||
/**
|
||||
* 收件人列表。
|
||||
*/
|
||||
private final List<String> tos = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 抄送人列表。
|
||||
*/
|
||||
private final List<String> ccs = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 密送人列表。
|
||||
*/
|
||||
private final List<String> bccs = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 内嵌图片集合。
|
||||
*/
|
||||
private final Map<String, InputStream> images = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* 附件文件集合。
|
||||
*/
|
||||
private File[] files = new File[0];
|
||||
|
||||
/**
|
||||
* 发件人。
|
||||
*/
|
||||
private String from;
|
||||
|
||||
/**
|
||||
* 登录用户。
|
||||
*/
|
||||
private String user;
|
||||
|
||||
/**
|
||||
* 登录密码。
|
||||
*/
|
||||
private String pass;
|
||||
|
||||
/**
|
||||
* 邮件主题。
|
||||
*/
|
||||
private String subject;
|
||||
|
||||
/**
|
||||
* 邮件内容。
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 是否 HTML 内容。
|
||||
*/
|
||||
private boolean html;
|
||||
|
||||
/**
|
||||
* 私有构造器,统一通过静态工厂创建。
|
||||
*/
|
||||
private MailBuilder() {
|
||||
}
|
||||
|
||||
@@ -220,7 +262,7 @@ public final class MailBuilder {
|
||||
* 设置正文。
|
||||
*
|
||||
* @param content 正文
|
||||
* @param html 是否 HTML
|
||||
* @param html 是否 HTML
|
||||
* @return 当前构建器
|
||||
*/
|
||||
public MailBuilder content(String content, boolean html) {
|
||||
@@ -232,7 +274,7 @@ public final class MailBuilder {
|
||||
/**
|
||||
* 添加内联图片。
|
||||
*
|
||||
* @param cid 图片 cid
|
||||
* @param cid 图片 cid
|
||||
* @param inputStream 图片输入流
|
||||
* @return 当前构建器
|
||||
*/
|
||||
@@ -297,6 +339,9 @@ public final class MailBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验邮件发送必填项。
|
||||
*/
|
||||
private void validate() {
|
||||
if (CollUtil.isEmpty(tos)) {
|
||||
throw new IllegalArgumentException("邮件收件人不能为空");
|
||||
@@ -309,6 +354,11 @@ public final class MailBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析实际使用的邮件账户。
|
||||
*
|
||||
* @return 邮件账户
|
||||
*/
|
||||
private MailAccount resolveMailAccount() {
|
||||
MailAccount account = mailAccount;
|
||||
if (account == null) {
|
||||
@@ -324,6 +374,12 @@ public final class MailBuilder {
|
||||
return copy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 拆分逗号或分号分隔的邮箱地址。
|
||||
*
|
||||
* @param addresses 邮箱地址字符串
|
||||
* @return 邮箱地址列表
|
||||
*/
|
||||
private List<String> splitAddress(String addresses) {
|
||||
if (StrUtil.isBlank(addresses)) {
|
||||
return Collections.emptyList();
|
||||
@@ -331,6 +387,12 @@ public final class MailBuilder {
|
||||
return normalizeAddresses(StrUtil.splitTrim(addresses.replace(';', ','), ','));
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤并标准化邮箱地址集合。
|
||||
*
|
||||
* @param addresses 邮箱地址集合
|
||||
* @return 标准化后的邮箱地址列表
|
||||
*/
|
||||
private List<String> normalizeAddresses(Collection<String> addresses) {
|
||||
if (CollUtil.isEmpty(addresses)) {
|
||||
return Collections.emptyList();
|
||||
|
||||
+10
-4
@@ -41,7 +41,7 @@ public class McpClientTemplate {
|
||||
/**
|
||||
* 调用所有 MCP Server 上的同名工具。
|
||||
*
|
||||
* @param toolName 工具名称
|
||||
* @param toolName 工具名称
|
||||
* @param arguments 工具参数
|
||||
* @return 各 Server 的工具调用结果
|
||||
*/
|
||||
@@ -58,8 +58,8 @@ public class McpClientTemplate {
|
||||
* 调用指定 MCP Server 上的工具。
|
||||
*
|
||||
* @param serverName Server 名称
|
||||
* @param toolName 工具名称
|
||||
* @param arguments 工具参数
|
||||
* @param toolName 工具名称
|
||||
* @param arguments 工具参数
|
||||
* @return 工具调用结果
|
||||
*/
|
||||
public Optional<McpToolCallResult> callTool(String serverName, String toolName, Map<String, Object> arguments) {
|
||||
@@ -86,7 +86,7 @@ public class McpClientTemplate {
|
||||
* 读取指定 MCP Server 上的资源。
|
||||
*
|
||||
* @param serverName Server 名称
|
||||
* @param uri 资源地址
|
||||
* @param uri 资源地址
|
||||
* @return 资源内容
|
||||
*/
|
||||
public Optional<McpResourceReadResult> readResource(String serverName, String uri) {
|
||||
@@ -115,6 +115,12 @@ public class McpClientTemplate {
|
||||
return mcpSyncClients;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 MCP Server 名称。
|
||||
*
|
||||
* @param client MCP 同步客户端
|
||||
* @return Server 名称
|
||||
*/
|
||||
private String getServerName(McpSyncClient client) {
|
||||
McpSchema.Implementation serverInfo = client.getServerInfo();
|
||||
if (serverInfo == null || serverInfo.name() == null) {
|
||||
|
||||
+20
-1
@@ -12,19 +12,38 @@ import org.tio.core.ChannelContext;
|
||||
*/
|
||||
@Slf4j
|
||||
public class MqttClientConnectListener implements IMqttClientConnectListener {
|
||||
//
|
||||
|
||||
private final MqttClientCreator mqttClientCreator;
|
||||
|
||||
/**
|
||||
* 创建 MQTT 客户端连接监听器。
|
||||
*
|
||||
* @param mqttClientCreator MQTT 客户端创建器
|
||||
*/
|
||||
public MqttClientConnectListener(MqttClientCreator mqttClientCreator) {
|
||||
this.mqttClientCreator = mqttClientCreator;
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端连接成功回调。
|
||||
*
|
||||
* @param context 通道上下文
|
||||
* @param isReconnect 是否重连
|
||||
*/
|
||||
@Override
|
||||
public void onConnected(ChannelContext context, boolean isReconnect) {
|
||||
// 创建连接
|
||||
log.info("MqttConnectedEvent:{}", context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 客户端断开连接回调。
|
||||
*
|
||||
* @param context 通道上下文
|
||||
* @param throwable 断开异常
|
||||
* @param remark 断开说明
|
||||
* @param isRemove 是否移除连接
|
||||
*/
|
||||
@Override
|
||||
public void onDisconnect(ChannelContext context, Throwable throwable, String remark, boolean isRemove) {
|
||||
// 离线时更新重连
|
||||
|
||||
+8
@@ -15,6 +15,14 @@ import java.nio.charset.StandardCharsets;
|
||||
@Slf4j
|
||||
public class MqttClientGlobalMessageListener implements IMqttClientGlobalMessageListener {
|
||||
|
||||
/**
|
||||
* 接收并记录全局 MQTT 消息。
|
||||
*
|
||||
* @param context 通道上下文
|
||||
* @param topic 主题
|
||||
* @param message MQTT 消息对象
|
||||
* @param payload 消息载荷
|
||||
*/
|
||||
@Override
|
||||
public void onMessage(ChannelContext context, String topic, MqttPublishMessage message, byte[] payload) {
|
||||
log.info("MqttGlobalMessageEvent => topic: {}, msg: {}", topic, new String(payload, StandardCharsets.UTF_8));
|
||||
|
||||
+504
-3
@@ -17,7 +17,10 @@ import software.amazon.awssdk.core.async.AsyncRequestBody;
|
||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.core.async.ResponsePublisher;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||
import software.amazon.awssdk.services.s3.model.*;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectRequest;
|
||||
import software.amazon.awssdk.services.s3.model.PutObjectResponse;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
import software.amazon.awssdk.transfer.s3.S3TransferManager;
|
||||
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
|
||||
@@ -38,8 +41,8 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.function.Consumer;
|
||||
@@ -90,7 +93,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
* 构造 S3 存储客户端基础实现。
|
||||
*
|
||||
* @param clientId 客户端 ID
|
||||
* @param config S3 存储客户端配置
|
||||
* @param config S3 存储客户端配置
|
||||
*/
|
||||
public AbstractOssClientImpl(String clientId, OssClientConfig config) {
|
||||
Assert.notNull(config, () -> S3StorageException.form("S3StorageClientConfig must not be null"));
|
||||
@@ -100,22 +103,40 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端 ID。
|
||||
*
|
||||
* @return 客户端 ID
|
||||
*/
|
||||
@Override
|
||||
public String clientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端配置副本。
|
||||
*
|
||||
* @return 客户端配置副本
|
||||
*/
|
||||
@Override
|
||||
public OssClientConfig config() {
|
||||
// 仅返回copy副本,防篡改
|
||||
return this.config.copy();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断客户端是否已经初始化。
|
||||
*
|
||||
* @return 是否已初始化
|
||||
*/
|
||||
@Override
|
||||
public boolean isInitialized() {
|
||||
return initialized.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化底层 S3 客户端资源。
|
||||
*/
|
||||
@Override
|
||||
public void initialize() {
|
||||
// 如果已经是初始化状态,则直接返回
|
||||
@@ -131,24 +152,52 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行客户端具体初始化逻辑。
|
||||
*/
|
||||
abstract void doInitialize();
|
||||
|
||||
/**
|
||||
* 使用回调校验当前客户端配置。
|
||||
*
|
||||
* @param verifyConfigAction 配置校验回调
|
||||
* @return 是否校验通过
|
||||
*/
|
||||
@Override
|
||||
public boolean verifyConfig(Function<OssClientConfig, Boolean> verifyConfigAction) {
|
||||
OssClientConfig config = config();
|
||||
return Boolean.TRUE.equals(verifyConfigAction.apply(config));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验当前客户端配置是否与指定配置一致。
|
||||
*
|
||||
* @param verifyConfig 待校验配置
|
||||
* @return 是否一致
|
||||
*/
|
||||
@Override
|
||||
public boolean verifyConfig(OssClientConfig verifyConfig) {
|
||||
return verifyConfig((config) -> Objects.equals(config, verifyConfig));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据文件名构建对象键。
|
||||
*
|
||||
* @param fileName 原始文件名
|
||||
* @return 对象键
|
||||
*/
|
||||
@Override
|
||||
public String buildPathKey(String fileName) {
|
||||
return buildPathKey(null, fileName);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据业务前缀和文件名构建对象键。
|
||||
*
|
||||
* @param businessPrefix 业务前缀
|
||||
* @param fileName 原始文件名
|
||||
* @return 对象键
|
||||
*/
|
||||
@Override
|
||||
public String buildPathKey(String businessPrefix, String fileName) {
|
||||
String defaultPrefix = config.prefix()
|
||||
@@ -161,6 +210,16 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return path + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自定义上传请求。
|
||||
*
|
||||
* @param body 上传请求体
|
||||
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
|
||||
* @param transferListeners 传输监听器集合
|
||||
* @param handleAsyncAction 上传完成处理函数
|
||||
* @param <T> 返回值类型
|
||||
* @return 上传处理结果
|
||||
*/
|
||||
@Override
|
||||
public <T> T doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, Collection<TransferListener> transferListeners, BiFunction<CompletedUpload, Throwable, T> handleAsyncAction) {
|
||||
try {
|
||||
@@ -177,11 +236,28 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自定义上传请求。
|
||||
*
|
||||
* @param body 上传请求体
|
||||
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
|
||||
* @param handleAsyncAction 上传完成处理函数
|
||||
* @param <T> 返回值类型
|
||||
* @return 上传处理结果
|
||||
*/
|
||||
@Override
|
||||
public <T> T doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, BiFunction<CompletedUpload, Throwable, T> handleAsyncAction) {
|
||||
return doCustomUpload(body, putObjectRequestBuilderConsumer, null, handleAsyncAction);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自定义上传请求并返回统一异步结果。
|
||||
*
|
||||
* @param body 上传请求体
|
||||
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
|
||||
* @param transferListeners 传输监听器集合
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer, Collection<TransferListener> transferListeners) {
|
||||
return doCustomUpload(body, putObjectRequestBuilderConsumer, transferListeners, (completedUpload, throwable) -> {
|
||||
@@ -192,6 +268,13 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自定义上传请求并返回统一异步结果。
|
||||
*
|
||||
* @param body 上传请求体
|
||||
* @param putObjectRequestBuilderConsumer PutObject 请求构建回调
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public HandleAsyncResult<PutObjectResponse> doCustomUpload(AsyncRequestBody body, Consumer<PutObjectRequest.Builder> putObjectRequestBuilderConsumer) {
|
||||
return doCustomUpload(body, putObjectRequestBuilderConsumer, null, (completedUpload, throwable) -> {
|
||||
@@ -202,28 +285,71 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传本地路径文件到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param path 文件路径
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, Path path, Options options) {
|
||||
AsyncRequestBody body = AsyncRequestBody.fromFile(path);
|
||||
return bucketUpload(bucket, key, body, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传本地路径文件到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param path 文件路径
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, Path path) {
|
||||
return bucketUpload(bucket, key, path, Options.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param file 文件对象
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, File file, Options options) {
|
||||
AsyncRequestBody body = AsyncRequestBody.fromFile(file);
|
||||
return bucketUpload(bucket, key, body, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param file 文件对象
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, File file) {
|
||||
return bucketUpload(bucket, key, file, Options.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传随机访问文件到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param file 随机访问文件
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file, Options options) {
|
||||
try {
|
||||
@@ -235,11 +361,29 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传随机访问文件到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param file 随机访问文件
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file) {
|
||||
return bucketUpload(bucket, key, file, Options.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传可读通道数据到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param channel 可读通道
|
||||
* @param contentLength 内容长度
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength, Options options) {
|
||||
// 让调用者自行处理通道的关闭
|
||||
@@ -256,11 +400,30 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传可读通道数据到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param channel 可读通道
|
||||
* @param contentLength 内容长度
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength) {
|
||||
return bucketUpload(bucket, key, channel, contentLength, Options.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传输入流数据到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param in 输入流
|
||||
* @param contentLength 内容长度
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength, Options options) {
|
||||
options.setLength(contentLength);
|
||||
@@ -268,11 +431,29 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return bucketUpload(bucket, key, body, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传输入流数据到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param in 输入流
|
||||
* @param contentLength 内容长度
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength) {
|
||||
return bucketUpload(bucket, key, in, contentLength, Options.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节数组到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param data 字节数组
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, byte[] data, Options options) {
|
||||
try (ByteArrayInputStream in = new ByteArrayInputStream(data)) {
|
||||
@@ -282,11 +463,28 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节数组到指定存储桶。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param data 字节数组
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult bucketUpload(String bucket, String key, byte[] data) {
|
||||
return bucketUpload(bucket, key, data, Options.builder());
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行指定存储桶的底层上传。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param body 上传请求体
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@NullMarked
|
||||
private PutObjectResult bucketUpload(String bucket, String key, AsyncRequestBody body, Options options) {
|
||||
// 优先使用body中的内容大小,如果不存在,再获取可选项中的
|
||||
@@ -321,6 +519,15 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return PutObjectResult.form("%s/%s".formatted(bucketUrl, key), key, response.eTag(), size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行自定义下载请求。
|
||||
*
|
||||
* @param getObjectRequestBuilderConsumer GetObject 请求构建回调
|
||||
* @param responseTransformer 下载响应转换器
|
||||
* @param transferListeners 传输监听器集合
|
||||
* @param <T> 下载结果类型
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public <T> T doCustomDownload(Consumer<GetObjectRequest.Builder> getObjectRequestBuilderConsumer, AsyncResponseTransformer<GetObjectResponse, T> responseTransformer, Collection<TransferListener> transferListeners) {
|
||||
try {
|
||||
@@ -338,6 +545,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定存储桶对象下载到订阅器。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param downloadSubscriber 下载订阅器
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult bucketDownload(String bucket, String key, OutputStreamDownloadSubscriber downloadSubscriber) {
|
||||
try {
|
||||
@@ -350,6 +565,15 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定存储桶对象下载到转换器。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param downloadTransformer 下载转换器
|
||||
* @param <T> 下载结果类型
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public <T> T bucketDownload(String bucket, String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) {
|
||||
try (ResponseInputStream<GetObjectResponse> responseInputStream = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toBlockingInputStream(), null)) {
|
||||
@@ -361,6 +585,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定存储桶对象下载到本地路径。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param path 本地路径
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult bucketDownload(String bucket, String key, Path path) {
|
||||
try (OutputStream out = Files.newOutputStream(path)) {
|
||||
@@ -370,6 +602,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定存储桶对象下载到文件。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param file 本地文件
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult bucketDownload(String bucket, String key, File file) {
|
||||
try (FileOutputStream out = new FileOutputStream(file)) {
|
||||
@@ -379,21 +619,52 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定存储桶对象下载到随机访问文件。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param file 随机访问文件
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult bucketDownload(String bucket, String key, RandomAccessFile file) {
|
||||
return bucketDownload(bucket, key, file.getChannel());
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定存储桶对象下载到可写通道。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param channel 可写通道
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult bucketDownload(String bucket, String key, WritableByteChannel channel) {
|
||||
return bucketDownload(bucket, key, OutputStreamDownloadSubscriber.create(channel));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定存储桶对象下载到输出流。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param out 输出流
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult bucketDownload(String bucket, String key, OutputStream out) {
|
||||
return bucketDownload(bucket, key, OutputStreamDownloadSubscriber.create(out));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 S3 响应构建下载结果。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param response S3 下载响应
|
||||
* @return 下载结果
|
||||
*/
|
||||
private GetObjectResult buildGetObjectResult(String key, GetObjectResponse response) {
|
||||
return GetObjectResult.form(
|
||||
key,
|
||||
@@ -409,6 +680,13 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定存储桶中的对象。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public boolean bucketDelete(String bucket, String key) {
|
||||
try {
|
||||
@@ -419,6 +697,14 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定存储桶对象的下载预签名 URL。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param expiredTime 过期时间
|
||||
* @return 预签名下载 URL
|
||||
*/
|
||||
@Override
|
||||
public String bucketPresignGetUrl(String bucket, String key, Duration expiredTime) {
|
||||
try {
|
||||
@@ -433,6 +719,15 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成指定存储桶对象的上传预签名 URL。
|
||||
*
|
||||
* @param bucket 存储桶名称
|
||||
* @param key 对象键
|
||||
* @param expiredTime 过期时间
|
||||
* @param metadata 对象元数据
|
||||
* @return 预签名上传 URL
|
||||
*/
|
||||
@Override
|
||||
public String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map<String, String> metadata) {
|
||||
try {
|
||||
@@ -447,122 +742,299 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传本地路径文件到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param path 文件路径
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, Path path, Options options) {
|
||||
return bucketUpload(defaultBucket(), key, path, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传本地路径文件到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param path 文件路径
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, Path path) {
|
||||
return bucketUpload(defaultBucket(), key, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param file 文件对象
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, File file, Options options) {
|
||||
return bucketUpload(defaultBucket(), key, file, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param file 文件对象
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, File file) {
|
||||
return bucketUpload(defaultBucket(), key, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传随机访问文件到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param file 随机访问文件
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, RandomAccessFile file, Options options) {
|
||||
return bucketUpload(defaultBucket(), key, file, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传随机访问文件到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param file 随机访问文件
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, RandomAccessFile file) {
|
||||
return bucketUpload(defaultBucket(), key, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传可读通道数据到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param channel 可读通道
|
||||
* @param contentLength 内容长度
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength, Options options) {
|
||||
return bucketUpload(defaultBucket(), key, channel, contentLength, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传可读通道数据到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param channel 可读通道
|
||||
* @param contentLength 内容长度
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength) {
|
||||
return bucketUpload(defaultBucket(), key, channel, contentLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传输入流数据到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param in 输入流
|
||||
* @param contentLength 内容长度
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, InputStream in, long contentLength, Options options) {
|
||||
return bucketUpload(defaultBucket(), key, in, contentLength, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传输入流数据到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param in 输入流
|
||||
* @param contentLength 内容长度
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, InputStream in, long contentLength) {
|
||||
return bucketUpload(defaultBucket(), key, in, contentLength);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节数组到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param data 字节数组
|
||||
* @param options 上传选项
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, byte[] data, Options options) {
|
||||
return bucketUpload(defaultBucket(), key, data, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传字节数组到默认存储桶。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param data 字节数组
|
||||
* @return 上传结果
|
||||
*/
|
||||
@Override
|
||||
public PutObjectResult upload(String key, byte[] data) {
|
||||
return bucketUpload(defaultBucket(), key, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储桶对象下载到订阅器。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param downloadSubscriber 下载订阅器
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult download(String key, OutputStreamDownloadSubscriber downloadSubscriber) {
|
||||
return bucketDownload(defaultBucket(), key, downloadSubscriber);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储桶对象下载到转换器。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param downloadTransformer 下载转换器
|
||||
* @param <T> 下载结果类型
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public <T> T download(String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) {
|
||||
return bucketDownload(defaultBucket(), key, downloadTransformer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储桶对象下载到本地路径。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param path 本地路径
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult download(String key, Path path) {
|
||||
return bucketDownload(defaultBucket(), key, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储桶对象下载到文件。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param file 本地文件
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult download(String key, File file) {
|
||||
return bucketDownload(defaultBucket(), key, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储桶对象下载到随机访问文件。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param file 随机访问文件
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult download(String key, RandomAccessFile file) {
|
||||
return bucketDownload(defaultBucket(), key, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储桶对象下载到可写通道。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param channel 可写通道
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult download(String key, WritableByteChannel channel) {
|
||||
return bucketDownload(defaultBucket(), key, channel);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将默认存储桶对象下载到输出流。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param out 输出流
|
||||
* @return 下载结果
|
||||
*/
|
||||
@Override
|
||||
public GetObjectResult download(String key, OutputStream out) {
|
||||
return bucketDownload(defaultBucket(), key, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除默认存储桶中的对象。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public boolean delete(String key) {
|
||||
return bucketDelete(defaultBucket(), key);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认存储桶对象的下载预签名 URL。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param expiredTime 过期时间
|
||||
* @return 预签名下载 URL
|
||||
*/
|
||||
@Override
|
||||
public String presignGetUrl(String key, Duration expiredTime) {
|
||||
return bucketPresignGetUrl(defaultBucket(), key, expiredTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成默认存储桶对象的上传预签名 URL。
|
||||
*
|
||||
* @param key 对象键
|
||||
* @param expiredTime 过期时间
|
||||
* @param metadata 对象元数据
|
||||
* @return 预签名上传 URL
|
||||
*/
|
||||
@Override
|
||||
public String presignPutUrl(String key, Duration expiredTime, Map<String, String> metadata) {
|
||||
return bucketPresignPutUrl(defaultBucket(), key, expiredTime, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认存储桶名称。
|
||||
*
|
||||
* @return 默认存储桶名称
|
||||
*/
|
||||
private String defaultBucket() {
|
||||
return config.bucket()
|
||||
.filter(bucket -> !bucket.isBlank())
|
||||
.orElseThrow(() -> S3StorageException.form("bucket is not configured."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并默认前缀与业务前缀。
|
||||
*
|
||||
* @param defaultPrefix 默认前缀
|
||||
* @param businessPrefix 业务前缀
|
||||
* @return 合并后的前缀
|
||||
*/
|
||||
private String mergePrefix(String defaultPrefix, String businessPrefix) {
|
||||
String left = normalizePrefix(defaultPrefix);
|
||||
String right = normalizePrefix(businessPrefix);
|
||||
@@ -575,6 +1047,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return left + StringUtils.SLASH + right;
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化对象键前缀。
|
||||
*
|
||||
* @param prefix 原始前缀
|
||||
* @return 规范化后的前缀
|
||||
*/
|
||||
private String normalizePrefix(String prefix) {
|
||||
if (prefix == null) {
|
||||
return "";
|
||||
@@ -589,6 +1067,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取文件扩展名。
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return 文件扩展名
|
||||
*/
|
||||
private String suffix(String fileName) {
|
||||
if (fileName == null) {
|
||||
return "";
|
||||
@@ -600,6 +1084,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return fileName.substring(index);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为统一的 S3 存储异常。
|
||||
*
|
||||
* @param e 原始异常
|
||||
* @return S3 存储异常
|
||||
*/
|
||||
private S3StorageException toStorageException(Throwable e) {
|
||||
Throwable cause = unwrapAsyncException(e);
|
||||
if (cause instanceof S3StorageException ex) {
|
||||
@@ -608,6 +1098,12 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return S3StorageException.form(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包异步执行异常。
|
||||
*
|
||||
* @param e 原始异常
|
||||
* @return 根因异常
|
||||
*/
|
||||
private Throwable unwrapAsyncException(Throwable e) {
|
||||
Throwable cause = e;
|
||||
while ((cause instanceof CompletionException || cause instanceof ExecutionException) && cause.getCause() != null) {
|
||||
@@ -616,6 +1112,11 @@ public abstract class AbstractOssClientImpl implements OssClient {
|
||||
return cause;
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭底层 S3 客户端资源。
|
||||
*
|
||||
* @throws Exception 关闭资源异常
|
||||
*/
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (s3TransferManager != null) {
|
||||
|
||||
+3
@@ -35,6 +35,9 @@ public class DefaultOssClientImpl extends AbstractOssClientImpl {
|
||||
super(clientId, config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认 S3 客户端、传输管理器和预签名生成器。
|
||||
*/
|
||||
@Override
|
||||
void doInitialize() {
|
||||
// 校验配置
|
||||
|
||||
+15
@@ -32,17 +32,32 @@ public record AccessControlPolicyConfig(
|
||||
.accessPolicy(AccessPolicy.PUBLIC_READ_WRITE)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 获取访问策略,未配置时返回默认策略。
|
||||
*
|
||||
* @return 访问策略
|
||||
*/
|
||||
@Override
|
||||
public @NonNull AccessPolicy accessPolicy() {
|
||||
return Optional.ofNullable(accessPolicy)
|
||||
.orElse(AccessPolicy.PUBLIC_READ_WRITE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制访问策略配置。
|
||||
*
|
||||
* @return 配置副本
|
||||
*/
|
||||
@Override
|
||||
public AccessControlPolicyConfig copy() {
|
||||
return toBuilder().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为构建器。
|
||||
*
|
||||
* @return 配置构建器
|
||||
*/
|
||||
@Override
|
||||
public AccessControlPolicyConfigBuilder toBuilder() {
|
||||
return builder()
|
||||
|
||||
+10
@@ -52,11 +52,21 @@ public record OssAsyncExecutorConfig(
|
||||
return corePoolSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制异步执行器配置。
|
||||
*
|
||||
* @return 配置副本
|
||||
*/
|
||||
@Override
|
||||
public OssAsyncExecutorConfig copy() {
|
||||
return toBuilder().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为构建器。
|
||||
*
|
||||
* @return 配置构建器
|
||||
*/
|
||||
@Override
|
||||
public OssAsyncExecutorConfigBuilder toBuilder() {
|
||||
return builder()
|
||||
|
||||
+34
@@ -226,6 +226,12 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
|
||||
return usePathStyleAccess ? BucketUrlUtil.getPathStyleBucketUrl(useHttps, url, bucket) : BucketUrlUtil.getSiteStyleBucketUrl(useHttps, url, bucket);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 S3 Region。
|
||||
*
|
||||
* @param regionString Region 字符串
|
||||
* @return Region 对象
|
||||
*/
|
||||
private static Region parseRegion(String regionString) {
|
||||
if (StringUtils.isBlank(regionString)) {
|
||||
return Region.US_EAST_1;
|
||||
@@ -233,11 +239,23 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
|
||||
return Region.of(regionString);
|
||||
}
|
||||
|
||||
/**
|
||||
* 兼容旧配置推断是否使用路径风格访问。
|
||||
*
|
||||
* @param properties OSS 配置属性
|
||||
* @return 是否使用路径风格访问
|
||||
*/
|
||||
private static boolean resolvePathStyleAccess(OssProperties properties) {
|
||||
// 旧配置没有显式路径风格字段,只能继续按内置云厂商 endpoint 做兼容推断。
|
||||
return !StringUtils.containsAny(properties.getEndpoint(), OssConstant.CLOUD_SERVICE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 ACL 访问策略配置。
|
||||
*
|
||||
* @param accessPolicyString 访问策略字符串
|
||||
* @return ACL 访问策略配置
|
||||
*/
|
||||
private static AccessControlPolicyConfig resolveAccessControlPolicy(String accessPolicyString) {
|
||||
// 绝大多数云厂商不允许操作 ACL,默认禁用;当前业务只用访问策略判断是否生成预签名 URL。
|
||||
if (StringUtils.isBlank(accessPolicyString)) {
|
||||
@@ -249,18 +267,34 @@ public class OssClientConfig implements Config<OssClientConfig, OssClientConfig.
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用于访问对象的基础 URL。
|
||||
*
|
||||
* @return 基础 URL
|
||||
*/
|
||||
private String getAccessBaseUrl() {
|
||||
return domain()
|
||||
.filter(OssClientConfig::hasHttpHeader)
|
||||
.orElseGet(this::getEndpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 endpoint 配置。
|
||||
*
|
||||
* @return endpoint
|
||||
*/
|
||||
private String getEndpoint() {
|
||||
return endpoint()
|
||||
.filter(s -> !s.isBlank())
|
||||
.orElseThrow(() -> S3StorageException.form("endpoint is not configured."));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 URL 是否包含 HTTP 协议头。
|
||||
*
|
||||
* @param url URL
|
||||
* @return 是否包含 HTTP 协议头
|
||||
*/
|
||||
private static boolean hasHttpHeader(String url) {
|
||||
return HttpUtil.isHttp(url) || HttpUtil.isHttps(url);
|
||||
}
|
||||
|
||||
+12
@@ -92,10 +92,22 @@ public class OssFactory {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定配置键对应的客户端锁。
|
||||
*
|
||||
* @param configKey 配置键
|
||||
* @return 客户端锁
|
||||
*/
|
||||
private static ReentrantLock getClientLock(String configKey) {
|
||||
return CLIENT_LOCKS.computeIfAbsent(configKey, key -> new ReentrantLock());
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭 OSS 客户端。
|
||||
*
|
||||
* @param configKey 配置键
|
||||
* @param client OSS 客户端
|
||||
*/
|
||||
private static void closeClient(String configKey, OssClient client) {
|
||||
try {
|
||||
client.close();
|
||||
|
||||
+22
@@ -20,11 +20,23 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
|
||||
|
||||
private final boolean allowAutoClose;
|
||||
|
||||
/**
|
||||
* 创建输出流下载订阅器。
|
||||
*
|
||||
* @param channel 可写通道
|
||||
* @param allowAutoClose 是否允许自动关闭通道
|
||||
*/
|
||||
private OutputStreamDownloadSubscriber(WritableByteChannel channel, boolean allowAutoClose) {
|
||||
this.channel = channel;
|
||||
this.allowAutoClose = allowAutoClose;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建输出流下载订阅器。
|
||||
*
|
||||
* @param out 输出流
|
||||
* @param allowAutoClose 是否允许自动关闭流
|
||||
*/
|
||||
private OutputStreamDownloadSubscriber(OutputStream out, boolean allowAutoClose) {
|
||||
this.allowAutoClose = allowAutoClose;
|
||||
// 创建可写入的字节通道
|
||||
@@ -36,6 +48,11 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入下载到的字节缓冲区。
|
||||
*
|
||||
* @param byteBuffer 字节缓冲区
|
||||
*/
|
||||
@Override
|
||||
public void accept(ByteBuffer byteBuffer) {
|
||||
try {
|
||||
@@ -47,6 +64,11 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按配置关闭底层通道。
|
||||
*
|
||||
* @throws Exception 关闭通道异常
|
||||
*/
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
if (channel.isOpen() && allowAutoClose) {
|
||||
|
||||
+7
@@ -85,6 +85,13 @@ public class RateLimiterAspect {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 组装限流缓存键。
|
||||
*
|
||||
* @param rateLimiter 限流注解配置
|
||||
* @param point 切点信息
|
||||
* @return 限流缓存键
|
||||
*/
|
||||
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
|
||||
String key = rateLimiter.key();
|
||||
// 判断 key 不为空 和 不是表达式
|
||||
|
||||
+8
-2
@@ -42,7 +42,7 @@ public class RepeatSubmitAspect {
|
||||
/**
|
||||
* 请求进入前校验是否重复提交。
|
||||
*
|
||||
* @param point 切点
|
||||
* @param point 切点
|
||||
* @param repeatSubmit 防重复提交注解
|
||||
*/
|
||||
@Before("@annotation(repeatSubmit)")
|
||||
@@ -111,6 +111,9 @@ public class RepeatSubmitAspect {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除当前请求写入的防重键。
|
||||
*/
|
||||
private void deleteRepeatKey() {
|
||||
String key = KEY_CACHE.get();
|
||||
if (StringUtils.isNotBlank(key)) {
|
||||
@@ -120,6 +123,9 @@ public class RepeatSubmitAspect {
|
||||
|
||||
/**
|
||||
* 参数拼装
|
||||
*
|
||||
* @param paramsArray 方法参数数组
|
||||
* @return 拼装后的参数字符串
|
||||
*/
|
||||
private String argsArrayToString(Object[] paramsArray) {
|
||||
StringJoiner params = new StringJoiner(" ");
|
||||
@@ -157,7 +163,7 @@ public class RepeatSubmitAspect {
|
||||
}
|
||||
}
|
||||
return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
|
||||
|| o instanceof BindingResult;
|
||||
|| o instanceof BindingResult;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+8
-5
@@ -18,7 +18,7 @@ public class CaffeineCacheDecorator implements Cache {
|
||||
/**
|
||||
* 创建带 Caffeine 一级缓存的缓存装饰器。
|
||||
*
|
||||
* @param name 缓存名称
|
||||
* @param name 缓存名称
|
||||
* @param cache 被装饰的缓存实例
|
||||
* @param caffeine 本地一级缓存实例
|
||||
*/
|
||||
@@ -73,7 +73,7 @@ public class CaffeineCacheDecorator implements Cache {
|
||||
/**
|
||||
* 按指定类型获取缓存值。
|
||||
*
|
||||
* @param key 缓存键
|
||||
* @param key 缓存键
|
||||
* @param type 值类型
|
||||
* @return 缓存值
|
||||
*/
|
||||
@@ -87,7 +87,7 @@ public class CaffeineCacheDecorator implements Cache {
|
||||
/**
|
||||
* 写入缓存值,并清理本地一级缓存。
|
||||
*
|
||||
* @param key 缓存键
|
||||
* @param key 缓存键
|
||||
* @param value 缓存值
|
||||
*/
|
||||
@Override
|
||||
@@ -99,7 +99,7 @@ public class CaffeineCacheDecorator implements Cache {
|
||||
/**
|
||||
* 当键不存在时写入缓存值。
|
||||
*
|
||||
* @param key 缓存键
|
||||
* @param key 缓存键
|
||||
* @param value 缓存值
|
||||
* @return 原缓存值包装对象
|
||||
*/
|
||||
@@ -160,7 +160,7 @@ public class CaffeineCacheDecorator implements Cache {
|
||||
/**
|
||||
* 获取缓存值,不存在时通过回调加载。
|
||||
*
|
||||
* @param key 缓存键
|
||||
* @param key 缓存键
|
||||
* @param valueLoader 回调加载器
|
||||
* @return 缓存值
|
||||
*/
|
||||
@@ -171,6 +171,9 @@ public class CaffeineCacheDecorator implements Cache {
|
||||
return (T) o;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理当前缓存命名空间下的本地一级缓存。
|
||||
*/
|
||||
private void clearLocalCache() {
|
||||
String prefix = name + ":";
|
||||
caffeine.asMap().keySet().removeIf(key -> key instanceof String cacheKey && cacheKey.startsWith(prefix));
|
||||
|
||||
+24
-4
@@ -1,12 +1,12 @@
|
||||
/**
|
||||
* Copyright (c) 2013-2021 Nikita Koksharov
|
||||
*
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@@ -169,6 +169,14 @@ public class PlusSpringCacheManager implements CacheManager {
|
||||
return createMapCache(cacheName, name, config, local);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析缓存配置,支持从模板配置复制并叠加缓存名称中的扩展参数。
|
||||
*
|
||||
* @param cacheName 完整缓存名称
|
||||
* @param name 基础缓存名称
|
||||
* @param array 缓存名称拆分参数
|
||||
* @return 缓存配置
|
||||
*/
|
||||
private CacheConfig resolveCacheConfig(String cacheName, String name, String[] array) {
|
||||
CacheConfig config = configMap.get(cacheName);
|
||||
if (config != null) {
|
||||
@@ -196,6 +204,12 @@ public class PlusSpringCacheManager implements CacheManager {
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析是否启用本地一级缓存。
|
||||
*
|
||||
* @param array 缓存名称拆分参数
|
||||
* @return 本地缓存开关
|
||||
*/
|
||||
private int resolveLocal(String[] array) {
|
||||
int local = 1;
|
||||
if (array.length > 4) {
|
||||
@@ -204,6 +218,12 @@ public class PlusSpringCacheManager implements CacheManager {
|
||||
return local;
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制 Redisson 缓存配置,避免修改模板配置。
|
||||
*
|
||||
* @param source 模板缓存配置
|
||||
* @return 新缓存配置
|
||||
*/
|
||||
private CacheConfig copyConfig(CacheConfig source) {
|
||||
CacheConfig target = new CacheConfig();
|
||||
target.setTTL(source.getTTL());
|
||||
|
||||
+6
@@ -57,6 +57,12 @@ public class CacheUtils {
|
||||
getRequiredCache(cacheNames).clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取必需存在的缓存实例。
|
||||
*
|
||||
* @param cacheNames 缓存组名称
|
||||
* @return 缓存实例
|
||||
*/
|
||||
private static Cache getRequiredCache(String cacheNames) {
|
||||
Cache cache = CacheManagerHolder.CACHE_MANAGER.getCache(cacheNames);
|
||||
if (cache == null) {
|
||||
|
||||
+5
@@ -21,6 +21,11 @@ import org.springframework.context.annotation.PropertySource;
|
||||
@PropertySource(value = "classpath:common-satoken.yml", factory = YmlPropertySourceFactory.class)
|
||||
public class SaTokenConfig {
|
||||
|
||||
/**
|
||||
* 创建 Sa-Token JWT 登录逻辑。
|
||||
*
|
||||
* @return JWT 登录逻辑
|
||||
*/
|
||||
@Bean
|
||||
public StpLogic getStpLogicJwt() {
|
||||
// Sa-Token 整合 jwt (简单模式)
|
||||
|
||||
+5
@@ -18,6 +18,11 @@ import java.lang.annotation.Target;
|
||||
@Documented
|
||||
public @interface Sensitive {
|
||||
|
||||
/**
|
||||
* 脱敏策略。
|
||||
*
|
||||
* @return 脱敏策略
|
||||
*/
|
||||
SensitiveStrategy strategy();
|
||||
|
||||
/**
|
||||
|
||||
+14
@@ -20,11 +20,25 @@ public class SensitiveJsonFieldProcessor implements JsonFieldProcessor {
|
||||
@Autowired(required = false)
|
||||
private SensitiveService sensitiveService;
|
||||
|
||||
/**
|
||||
* 判断字段是否声明了脱敏注解。
|
||||
*
|
||||
* @param fieldContext 字段上下文
|
||||
* @return 是否支持脱敏处理
|
||||
*/
|
||||
@Override
|
||||
public boolean supports(JsonFieldContext fieldContext) {
|
||||
return fieldContext.getAnnotation(Sensitive.class) != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按字段脱敏策略处理字符串值。
|
||||
*
|
||||
* @param fieldContext 字段上下文
|
||||
* @param value 字段原始值
|
||||
* @param context JSON 增强上下文
|
||||
* @return 脱敏后的字段值
|
||||
*/
|
||||
@Override
|
||||
public Object process(JsonFieldContext fieldContext, Object value, JsonEnhancementContext context) {
|
||||
Sensitive sensitive = fieldContext.getAnnotation(Sensitive.class);
|
||||
|
||||
+25
@@ -24,15 +24,34 @@ import me.zhyd.oauth.utils.UrlBuilder;
|
||||
*/
|
||||
public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultRequest {
|
||||
|
||||
/**
|
||||
* 创建企业微信认证请求。
|
||||
*
|
||||
* @param config 授权配置
|
||||
* @param source 授权来源
|
||||
*/
|
||||
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source) {
|
||||
super(config,source);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 创建企业微信认证请求。
|
||||
*
|
||||
* @param config 授权配置
|
||||
* @param source 授权来源
|
||||
* @param authStateCache 授权状态缓存
|
||||
*/
|
||||
public AbstractAuthWeChatEnterpriseRequest(AuthConfig config, AuthSource source, AuthStateCache authStateCache) {
|
||||
super(config, source, authStateCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业微信访问令牌。
|
||||
*
|
||||
* @param authCallback 授权回调参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
String response = doGetAuthorizationCode(accessTokenUrl(null));
|
||||
@@ -46,6 +65,12 @@ public abstract class AbstractAuthWeChatEnterpriseRequest extends AuthDefaultReq
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业微信用户信息。
|
||||
*
|
||||
* @param authToken 访问令牌
|
||||
* @return 授权用户信息
|
||||
*/
|
||||
@Override
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
String response = doGetUserInfo(authToken);
|
||||
|
||||
+29
@@ -26,14 +26,31 @@ import java.util.Map;
|
||||
*/
|
||||
public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
|
||||
/**
|
||||
* 创建钉钉 V2 认证请求。
|
||||
*
|
||||
* @param config 授权配置
|
||||
*/
|
||||
public AuthDingTalkV2Request(AuthConfig config) {
|
||||
super(config, AuthDefaultSource.DINGTALK_V2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建钉钉 V2 认证请求。
|
||||
*
|
||||
* @param config 授权配置
|
||||
* @param authStateCache 授权状态缓存
|
||||
*/
|
||||
public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
|
||||
super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成钉钉 V2 授权地址。
|
||||
*
|
||||
* @param state 授权状态
|
||||
* @return 授权地址
|
||||
*/
|
||||
@Override
|
||||
public String authorize(String state) {
|
||||
return UrlBuilder.fromBaseUrl(source.authorize())
|
||||
@@ -50,6 +67,12 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取钉钉 V2 访问令牌。
|
||||
*
|
||||
* @param authCallback 授权回调参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
@@ -70,6 +93,12 @@ public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取钉钉 V2 用户信息。
|
||||
*
|
||||
* @param authToken 访问令牌
|
||||
* @return 授权用户信息
|
||||
*/
|
||||
@Override
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
HttpHeader header = new HttpHeader();
|
||||
|
||||
+27
@@ -20,6 +20,9 @@ import org.dromara.common.json.utils.JsonUtils;
|
||||
@Slf4j
|
||||
public class AuthGiteaRequest extends AuthDefaultRequest {
|
||||
|
||||
/**
|
||||
* 服务器地址。
|
||||
*/
|
||||
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.gitea.server-url");
|
||||
|
||||
/**
|
||||
@@ -29,10 +32,22 @@ public class AuthGiteaRequest extends AuthDefaultRequest {
|
||||
super(config, AuthGiteaSource.GITEA);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 Gitea 认证请求。
|
||||
*
|
||||
* @param config 授权配置
|
||||
* @param authStateCache 授权状态缓存
|
||||
*/
|
||||
public AuthGiteaRequest(AuthConfig config, AuthStateCache authStateCache) {
|
||||
super(config, AuthGiteaSource.GITEA, authStateCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Gitea 访问令牌。
|
||||
*
|
||||
* @param authCallback 授权回调参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
String body = doPostAuthorizationCode(authCallback.getCode());
|
||||
@@ -54,6 +69,12 @@ public class AuthGiteaRequest extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用授权码换取 Gitea 访问令牌响应。
|
||||
*
|
||||
* @param code 授权码
|
||||
* @return 令牌接口响应体
|
||||
*/
|
||||
@Override
|
||||
protected String doPostAuthorizationCode(String code) {
|
||||
HttpRequest request = HttpRequest.post(source.accessToken())
|
||||
@@ -66,6 +87,12 @@ public class AuthGiteaRequest extends AuthDefaultRequest {
|
||||
return response.body();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Gitea 用户信息。
|
||||
*
|
||||
* @param authToken 访问令牌
|
||||
* @return 授权用户信息
|
||||
*/
|
||||
@Override
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
String body = doGetUserInfo(authToken);
|
||||
|
||||
+21
@@ -16,6 +16,9 @@ import org.dromara.common.json.utils.JsonUtils;
|
||||
*/
|
||||
public class AuthMaxKeyRequest extends AuthDefaultRequest {
|
||||
|
||||
/**
|
||||
* 服务器地址。
|
||||
*/
|
||||
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.maxkey.server-url");
|
||||
|
||||
/**
|
||||
@@ -25,10 +28,22 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
|
||||
super(config, AuthMaxKeySource.MAXKEY);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 MaxKey 认证请求。
|
||||
*
|
||||
* @param config 授权配置
|
||||
* @param authStateCache 授权状态缓存
|
||||
*/
|
||||
public AuthMaxKeyRequest(AuthConfig config, AuthStateCache authStateCache) {
|
||||
super(config, AuthMaxKeySource.MAXKEY, authStateCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MaxKey 访问令牌。
|
||||
*
|
||||
* @param authCallback 授权回调参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
String body = doPostAuthorizationCode(authCallback.getCode());
|
||||
@@ -50,6 +65,12 @@ public class AuthMaxKeyRequest extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 MaxKey 用户信息。
|
||||
*
|
||||
* @param authToken 访问令牌
|
||||
* @return 授权用户信息
|
||||
*/
|
||||
@Override
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
String body = doGetUserInfo(authToken);
|
||||
|
||||
+44
@@ -30,6 +30,9 @@ import static org.dromara.common.social.topiam.AuthTopIamSource.TOPIAM;
|
||||
@Slf4j
|
||||
public class AuthTopIamRequest extends AuthDefaultRequest {
|
||||
|
||||
/**
|
||||
* 服务器地址。
|
||||
*/
|
||||
public static final String SERVER_URL = SpringUtils.getProperty("justauth.type.topiam.server-url");
|
||||
|
||||
/**
|
||||
@@ -39,10 +42,22 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
|
||||
super(config, TOPIAM);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 TopIAM 认证请求。
|
||||
*
|
||||
* @param config 授权配置
|
||||
* @param authStateCache 授权状态缓存
|
||||
*/
|
||||
public AuthTopIamRequest(AuthConfig config, AuthStateCache authStateCache) {
|
||||
super(config, TOPIAM, authStateCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TopIAM 访问令牌。
|
||||
*
|
||||
* @param authCallback 授权回调参数
|
||||
* @return 访问令牌
|
||||
*/
|
||||
@Override
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
String body = doPostAuthorizationCode(authCallback.getCode());
|
||||
@@ -57,6 +72,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TopIAM 用户信息。
|
||||
*
|
||||
* @param authToken 访问令牌
|
||||
* @return 授权用户信息
|
||||
*/
|
||||
@Override
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
String body = doGetUserInfo(authToken);
|
||||
@@ -73,6 +94,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用授权码换取 TopIAM 访问令牌响应。
|
||||
*
|
||||
* @param code 授权码
|
||||
* @return 令牌接口响应体
|
||||
*/
|
||||
@Override
|
||||
protected String doPostAuthorizationCode(String code) {
|
||||
HttpRequest request = HttpRequest.post(source.accessToken())
|
||||
@@ -84,6 +111,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
|
||||
return response.body();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 TopIAM 用户信息接口响应。
|
||||
*
|
||||
* @param authToken 访问令牌
|
||||
* @return 用户信息响应体
|
||||
*/
|
||||
@Override
|
||||
protected String doGetUserInfo(AuthToken authToken) {
|
||||
return new HttpUtils(config.getHttpConfig()).get(source.userInfo(), null, new HttpHeader()
|
||||
@@ -92,6 +125,12 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成 TopIAM 授权地址。
|
||||
*
|
||||
* @param state 授权状态
|
||||
* @return 授权地址
|
||||
*/
|
||||
@Override
|
||||
public String authorize(String state) {
|
||||
return UrlBuilder.fromBaseUrl(super.authorize(state))
|
||||
@@ -99,6 +138,11 @@ public class AuthTopIamRequest extends AuthDefaultRequest {
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 TopIAM 接口响应。
|
||||
*
|
||||
* @param object 响应内容
|
||||
*/
|
||||
private static void checkResponse(Dict object) {
|
||||
// oauth/token 验证异常
|
||||
if (object.containsKey("error")) {
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ public class FilterConfig {
|
||||
/**
|
||||
* 注册 XSS 过滤器。
|
||||
*
|
||||
* @param xssProperties XSS 配置
|
||||
* @return XSS 请求过滤器实例
|
||||
*/
|
||||
@Bean
|
||||
|
||||
+30
@@ -21,6 +21,9 @@ import java.nio.charset.StandardCharsets;
|
||||
* @author ruoyi
|
||||
*/
|
||||
public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
|
||||
/**
|
||||
* 请求体字节数据。
|
||||
*/
|
||||
private final byte[] body;
|
||||
|
||||
/**
|
||||
@@ -59,26 +62,53 @@ public class RepeatedlyRequestWrapper extends HttpServletRequestWrapper {
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
final ByteArrayInputStream bais = new ByteArrayInputStream(body);
|
||||
return new ServletInputStream() {
|
||||
/**
|
||||
* 读取缓存请求体的下一个字节。
|
||||
*
|
||||
* @return 下一个字节
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return bais.read();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回缓存请求体剩余可读字节数。
|
||||
*
|
||||
* @return 剩余字节数
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return bais.available();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断缓存请求体是否已读取完毕。
|
||||
*
|
||||
* @return 是否读取完毕
|
||||
*/
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return bais.available() == 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断输入流是否可读。
|
||||
*
|
||||
* @return 固定为 true
|
||||
*/
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步读取监听器。
|
||||
*
|
||||
* @param readListener 读取监听器
|
||||
*/
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
|
||||
|
||||
+27
@@ -124,25 +124,52 @@ public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
byte[] jsonBytes = json.getBytes(StandardCharsets.UTF_8);
|
||||
final ByteArrayInputStream bis = IoUtil.toStream(jsonBytes);
|
||||
return new ServletInputStream() {
|
||||
/**
|
||||
* 判断清洗后的 JSON 流是否已读取完毕。
|
||||
*
|
||||
* @return 是否读取完毕
|
||||
*/
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断清洗后的 JSON 流是否可读。
|
||||
*
|
||||
* @return 固定为 true
|
||||
*/
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回清洗后的 JSON 字节数。
|
||||
*
|
||||
* @return JSON 字节数
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
return jsonBytes.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置异步读取监听器。
|
||||
*
|
||||
* @param readListener 读取监听器
|
||||
*/
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取清洗后的 JSON 流下一个字节。
|
||||
*
|
||||
* @return 下一个字节
|
||||
* @throws IOException IO 异常
|
||||
*/
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return bis.read();
|
||||
|
||||
+5
@@ -13,6 +13,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@SpringBootApplication
|
||||
public class MonitorAdminApplication {
|
||||
|
||||
/**
|
||||
* Admin 监控服务启动入口。
|
||||
*
|
||||
* @param args 启动参数
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MonitorAdminApplication.class, args);
|
||||
System.out.println("Admin 监控启动成功");
|
||||
|
||||
+8
@@ -21,8 +21,16 @@ import org.springframework.security.web.servlet.util.matcher.PathPatternRequestM
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* 监控后台上下文路径。
|
||||
*/
|
||||
private final String adminContextPath;
|
||||
|
||||
/**
|
||||
* 创建监控后台安全配置。
|
||||
*
|
||||
* @param adminServerProperties 监控后台配置
|
||||
*/
|
||||
public SecurityConfig(AdminServerProperties adminServerProperties) {
|
||||
this.adminContextPath = adminServerProperties.getContextPath();
|
||||
}
|
||||
|
||||
+5
@@ -20,6 +20,11 @@ import static de.codecentric.boot.admin.server.domain.values.StatusInfo.*;
|
||||
@Component
|
||||
public class CustomNotifier extends AbstractEventNotifier {
|
||||
|
||||
/**
|
||||
* 初始化自定义事件通知处理器。
|
||||
*
|
||||
* @param repository 实例仓库
|
||||
*/
|
||||
protected CustomNotifier(InstanceRepository repository) {
|
||||
super(repository);
|
||||
}
|
||||
|
||||
+23
@@ -20,7 +20,13 @@ import java.util.Base64;
|
||||
*/
|
||||
public class ActuatorAuthFilter implements Filter {
|
||||
|
||||
/**
|
||||
* 认证用户名。
|
||||
*/
|
||||
private final String username;
|
||||
/**
|
||||
* 认证密码。
|
||||
*/
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
@@ -34,6 +40,15 @@ public class ActuatorAuthFilter implements Filter {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Actuator Basic Auth 请求。
|
||||
*
|
||||
* @param servletRequest 原始请求
|
||||
* @param servletResponse 原始响应
|
||||
* @param filterChain 过滤器链
|
||||
* @throws IOException IO 异常
|
||||
* @throws ServletException Servlet 异常
|
||||
*/
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
@@ -63,10 +78,18 @@ public class ActuatorAuthFilter implements Filter {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化过滤器。
|
||||
*
|
||||
* @param filterConfig 过滤器配置
|
||||
*/
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁过滤器。
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
+6
@@ -13,8 +13,14 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* 认证用户名。
|
||||
*/
|
||||
@Value("${spring.boot.admin.client.username}")
|
||||
private String username;
|
||||
/**
|
||||
* 认证密码。
|
||||
*/
|
||||
@Value("${spring.boot.admin.client.password}")
|
||||
private String password;
|
||||
|
||||
|
||||
+5
@@ -11,6 +11,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@SpringBootApplication
|
||||
public class SnailAiServerApplication {
|
||||
|
||||
/**
|
||||
* Snail AI 服务启动入口。
|
||||
*
|
||||
* @param args 启动参数
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
com.aizuda.snail.ai.starter.SnailAiSpringbootApplication.main(args);
|
||||
}
|
||||
|
||||
+23
@@ -15,7 +15,13 @@ import java.util.Base64;
|
||||
*/
|
||||
public class ActuatorAuthFilter implements Filter {
|
||||
|
||||
/**
|
||||
* 认证用户名。
|
||||
*/
|
||||
private final String username;
|
||||
/**
|
||||
* 认证密码。
|
||||
*/
|
||||
private final String password;
|
||||
|
||||
/**
|
||||
@@ -29,6 +35,15 @@ public class ActuatorAuthFilter implements Filter {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验 Actuator Basic Auth 请求。
|
||||
*
|
||||
* @param servletRequest 原始请求
|
||||
* @param servletResponse 原始响应
|
||||
* @param filterChain 过滤器链
|
||||
* @throws IOException IO 异常
|
||||
* @throws ServletException Servlet 异常
|
||||
*/
|
||||
@Override
|
||||
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
|
||||
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||
@@ -64,10 +79,18 @@ public class ActuatorAuthFilter implements Filter {
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化过滤器。
|
||||
*
|
||||
* @param filterConfig 过滤器配置
|
||||
*/
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁过滤器。
|
||||
*/
|
||||
@Override
|
||||
public void destroy() {
|
||||
}
|
||||
|
||||
+6
@@ -13,8 +13,14 @@ import org.springframework.context.annotation.Configuration;
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* 认证用户名。
|
||||
*/
|
||||
@Value("${spring.boot.admin.client.username}")
|
||||
private String username;
|
||||
/**
|
||||
* 认证密码。
|
||||
*/
|
||||
@Value("${spring.boot.admin.client.password}")
|
||||
private String password;
|
||||
|
||||
|
||||
+5
@@ -12,6 +12,11 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@SpringBootApplication
|
||||
public class SnailJobServerApplication {
|
||||
|
||||
/**
|
||||
* SnailJob 服务启动入口。
|
||||
*
|
||||
* @param args 启动参数
|
||||
*/
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(com.aizuda.snailjob.server.SnailJobServerApplication.class, args);
|
||||
}
|
||||
|
||||
+43
-14
@@ -46,13 +46,22 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
||||
OpenApiConversationClient.class, OpenApiUserClient.class})
|
||||
public class SnailAiController extends BaseController {
|
||||
|
||||
/**
|
||||
* Snail AI 成功状态码。
|
||||
*/
|
||||
private static final int SNAIL_AI_SUCCESS = 1;
|
||||
/**
|
||||
* SSE 超时时间。
|
||||
*/
|
||||
private static final long SSE_TIMEOUT = 300000L;
|
||||
|
||||
private final OpenApiAgentClient agentClient;
|
||||
private final OpenApiChatClient chatClient;
|
||||
private final OpenApiConversationClient conversationClient;
|
||||
private final OpenApiUserClient userClient;
|
||||
/**
|
||||
* 聊天模式。
|
||||
*/
|
||||
@Value("${snail-ai.chat-mode:stream}")
|
||||
private String chatMode;
|
||||
|
||||
@@ -97,8 +106,8 @@ public class SnailAiController extends BaseController {
|
||||
*/
|
||||
@PostMapping("/agent/{agentId}/conversation")
|
||||
public R<OpenApiConversationVO> createConversation(
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@RequestBody OpenApiCreateConversationRequest request) {
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@RequestBody OpenApiCreateConversationRequest request) {
|
||||
request.setAgentId(agentId);
|
||||
request.setOpenId(ensureOpenId());
|
||||
return toR(conversationClient.createConversation(request));
|
||||
@@ -109,9 +118,9 @@ public class SnailAiController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/agent/{agentId}/conversations")
|
||||
public R<PageResult<OpenApiConversationVO>> listConversations(
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@Min(value = 1, message = "页码不能小于1") @RequestParam(defaultValue = "1") int page,
|
||||
@Min(value = 1, message = "每页条数不能小于1") @RequestParam(defaultValue = "10") int size) {
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@Min(value = 1, message = "页码不能小于1") @RequestParam(defaultValue = "1") int page,
|
||||
@Min(value = 1, message = "每页条数不能小于1") @RequestParam(defaultValue = "10") int size) {
|
||||
OpenApiConversationQueryRequest request = new OpenApiConversationQueryRequest();
|
||||
request.setAgentId(agentId);
|
||||
request.setOpenId(ensureOpenId());
|
||||
@@ -125,8 +134,8 @@ public class SnailAiController extends BaseController {
|
||||
*/
|
||||
@GetMapping("/agent/{agentId}/conversation/{conversationId}/messages")
|
||||
public R<List<OpenApiMessageVO>> getMessages(
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
|
||||
OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest();
|
||||
request.setAgentId(agentId);
|
||||
request.setConversationId(conversationId);
|
||||
@@ -139,8 +148,8 @@ public class SnailAiController extends BaseController {
|
||||
*/
|
||||
@DeleteMapping("/agent/{agentId}/conversation/{conversationId}")
|
||||
public R<Void> deleteConversation(
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@NotBlank(message = "会话ID不能为空") @PathVariable String conversationId) {
|
||||
OpenApiConversationIdentityRequest request = new OpenApiConversationIdentityRequest();
|
||||
request.setAgentId(agentId);
|
||||
request.setConversationId(conversationId);
|
||||
@@ -162,8 +171,8 @@ public class SnailAiController extends BaseController {
|
||||
*/
|
||||
@PostMapping("/agent/{agentId}/chat/sync")
|
||||
public R<OpenApiChatSyncResponse> chatSync(
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@RequestBody OpenApiChatRequest request) {
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@RequestBody OpenApiChatRequest request) {
|
||||
request.setAgentId(agentId);
|
||||
request.setOpenId(ensureOpenId());
|
||||
log.info("Sync chat request: agentId={}", agentId);
|
||||
@@ -175,9 +184,9 @@ public class SnailAiController extends BaseController {
|
||||
*/
|
||||
@PostMapping(value = "/agent/{agentId}/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter chatStream(
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@RequestBody OpenApiChatRequest request,
|
||||
HttpServletResponse response) {
|
||||
@NotNull(message = "智能体ID不能为空") @PathVariable Long agentId,
|
||||
@RequestBody OpenApiChatRequest request,
|
||||
HttpServletResponse response) {
|
||||
prepareSseResponse(response);
|
||||
SseEmitter emitter = new SseEmitter(SSE_TIMEOUT);
|
||||
AtomicBoolean closed = new AtomicBoolean(false);
|
||||
@@ -202,16 +211,31 @@ public class SnailAiController extends BaseController {
|
||||
log.info("Stream chat request: agentId={}", agentId);
|
||||
try {
|
||||
chatClient.chatStream(request, new SseEventListener() {
|
||||
/**
|
||||
* 推送普通文本片段。
|
||||
*
|
||||
* @param text 文本片段
|
||||
*/
|
||||
@Override
|
||||
public void onText(String text) {
|
||||
safeSend(emitter, closed, "text", text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送思考内容片段。
|
||||
*
|
||||
* @param thinking 思考内容
|
||||
*/
|
||||
@Override
|
||||
public void onThinking(String thinking) {
|
||||
safeSend(emitter, closed, "thinking", thinking);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流式对话完成事件。
|
||||
*
|
||||
* @param data 完成数据
|
||||
*/
|
||||
@Override
|
||||
public void onComplete(String data) {
|
||||
if (!closed.get()) {
|
||||
@@ -223,6 +247,11 @@ public class SnailAiController extends BaseController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理流式对话异常事件。
|
||||
*
|
||||
* @param errorMessage 错误信息
|
||||
*/
|
||||
@Override
|
||||
public void onError(String errorMessage) {
|
||||
log.error("Stream chat error: {}", errorMessage);
|
||||
|
||||
+13
@@ -12,9 +12,22 @@ import lombok.NoArgsConstructor;
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
public class PriorityDemo implements Comparable<PriorityDemo> {
|
||||
/**
|
||||
* 队列元素名称。
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 优先级排序值。
|
||||
*/
|
||||
private Integer orderNum;
|
||||
|
||||
/**
|
||||
* 按排序值比较优先级。
|
||||
*
|
||||
* @param other 另一个队列元素
|
||||
* @return 比较结果
|
||||
*/
|
||||
@Override
|
||||
public int compareTo(PriorityDemo other) {
|
||||
return Integer.compare(getOrderNum(), other.getOrderNum());
|
||||
|
||||
+9
@@ -18,11 +18,20 @@ import java.util.List;
|
||||
*/
|
||||
public class ExportDemoListener extends DefaultExcelListener<ExportDemoVo> {
|
||||
|
||||
/**
|
||||
* 创建下拉框导入解析监听器。
|
||||
*/
|
||||
public ExportDemoListener() {
|
||||
// 显示使用构造函数,否则将导致空指针
|
||||
super(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析并校验一行下拉框演示数据。
|
||||
*
|
||||
* @param data 行数据
|
||||
* @param context 解析上下文
|
||||
*/
|
||||
@Override
|
||||
public void invoke(ExportDemoVo data, AnalysisContext context) {
|
||||
// 先校验必填
|
||||
|
||||
@@ -23,12 +23,26 @@ import java.util.List;
|
||||
*/
|
||||
public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
|
||||
|
||||
/**
|
||||
* 自定义分页查询演示数据。
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param wrapper 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
@DataPermission({
|
||||
@DataColumn(key = "deptName", value = "dept_id"),
|
||||
@DataColumn(key = "userName", value = "user_id")
|
||||
})
|
||||
Page<TestDemoVo> customPageList(@Param("page") Page<TestDemo> page, @Param("ew") Wrapper<TestDemo> wrapper);
|
||||
|
||||
/**
|
||||
* 分页查询演示 VO 列表并应用数据权限。
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param wrapper 查询条件
|
||||
* @return 分页结果
|
||||
*/
|
||||
@Override
|
||||
@DataPermission({
|
||||
@DataColumn(key = "deptName", value = "dept_id"),
|
||||
@@ -38,6 +52,12 @@ public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
|
||||
return selectVoPage(page, wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询演示 VO 列表并应用数据权限。
|
||||
*
|
||||
* @param wrapper 查询条件
|
||||
* @return 演示 VO 列表
|
||||
*/
|
||||
@Override
|
||||
@DataPermission({
|
||||
@DataColumn(key = "deptName", value = "dept_id"),
|
||||
@@ -47,6 +67,12 @@ public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
|
||||
return selectVoList(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
* 按主键集合查询演示数据并应用数据权限。
|
||||
*
|
||||
* @param idList 主键集合
|
||||
* @return 演示数据列表
|
||||
*/
|
||||
@Override
|
||||
@DataPermission(value = {
|
||||
@DataColumn(key = "deptName", value = "dept_id"),
|
||||
@@ -54,6 +80,12 @@ public interface TestDemoMapper extends BaseMapperPlus<TestDemo, TestDemoVo> {
|
||||
}, joinStr = "AND")
|
||||
List<TestDemo> selectByIds(@Param(Constants.COLL) Collection<? extends Serializable> idList);
|
||||
|
||||
/**
|
||||
* 按主键更新演示数据并应用数据权限。
|
||||
*
|
||||
* @param entity 演示数据实体
|
||||
* @return 更新行数
|
||||
*/
|
||||
@Override
|
||||
@DataPermission({
|
||||
@DataColumn(key = "deptName", value = "dept_id"),
|
||||
|
||||
+12
-5
@@ -1,9 +1,9 @@
|
||||
package org.dromara.demo.mcp;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.mcp.core.McpClientTemplate;
|
||||
import org.dromara.common.mcp.core.McpResourceReadResult;
|
||||
import org.dromara.common.mcp.core.McpToolCallResult;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -42,7 +42,7 @@ public class McpDemoClientService {
|
||||
/**
|
||||
* 调用外部 MCP 工具接收数据。
|
||||
*
|
||||
* @param toolName 工具名称
|
||||
* @param toolName 工具名称
|
||||
* @param arguments 工具参数
|
||||
* @return 工具返回内容
|
||||
*/
|
||||
@@ -65,7 +65,7 @@ public class McpDemoClientService {
|
||||
* <p>
|
||||
* MCP 返回数据不建议直接入库,应先转换成业务 BO/DTO,再进入业务 Service。
|
||||
*
|
||||
* @param toolName 工具名称
|
||||
* @param toolName 工具名称
|
||||
* @param arguments 工具参数
|
||||
* @return 处理结果
|
||||
*/
|
||||
@@ -73,6 +73,13 @@ public class McpDemoClientService {
|
||||
return new McpDemoHandleResult("MCP", true, callRemoteTool(toolName, arguments));
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 MCP Client 操作。
|
||||
*
|
||||
* @param action MCP Client 操作
|
||||
* @param <T> 返回值类型
|
||||
* @return 操作结果
|
||||
*/
|
||||
private <T> T execute(Function<McpClientTemplate, T> action) {
|
||||
McpClientTemplate template = mcpClientTemplateProvider.getIfAvailable();
|
||||
if (template == null) {
|
||||
@@ -85,8 +92,8 @@ public class McpDemoClientService {
|
||||
* MCP 数据处理结果。
|
||||
*
|
||||
* @param sourceType 数据来源类型
|
||||
* @param handled 是否已处理
|
||||
* @param data MCP 原始返回数据
|
||||
* @param handled 是否已处理
|
||||
* @param data MCP 原始返回数据
|
||||
*/
|
||||
public record McpDemoHandleResult(
|
||||
String sourceType,
|
||||
|
||||
+17
@@ -28,6 +28,11 @@ import java.util.Map;
|
||||
@RequiredArgsConstructor
|
||||
public class ExportExcelServiceImpl implements IExportExcelService {
|
||||
|
||||
/**
|
||||
* 导出带下拉框选项的 Excel 示例文件。
|
||||
*
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
@Override
|
||||
public void exportWithOptions(HttpServletResponse response) {
|
||||
// 创建表格数据,业务中一般通过数据库查询
|
||||
@@ -106,6 +111,13 @@ public class ExportExcelServiceImpl implements IExportExcelService {
|
||||
.toResponse(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建下拉框显示值。
|
||||
*
|
||||
* @param cityDataList 城市演示数据列表
|
||||
* @param id 选项 id
|
||||
* @return 下拉框显示值
|
||||
*/
|
||||
private String buildOptions(List<DemoCityData> cityDataList, Integer id) {
|
||||
Map<Integer, List<DemoCityData>> groupByIdMap =
|
||||
StreamUtils.groupByKey(cityDataList, DemoCityData::getId);
|
||||
@@ -246,6 +258,11 @@ public class ExportExcelServiceImpl implements IExportExcelService {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 自定义写入多个工作表。
|
||||
*
|
||||
* @param response HTTP 响应
|
||||
*/
|
||||
@Override
|
||||
public void customExport(HttpServletResponse response) {
|
||||
ExcelBuilder.writer(ExportDemoVo.class).sheetName("自定义导出").toResponse(response, wrapper -> {
|
||||
|
||||
+19
-2
@@ -460,8 +460,8 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
/**
|
||||
* 模板渲染上下文。
|
||||
*
|
||||
* @param table 生成表信息
|
||||
* @param context 模板上下文
|
||||
* @param table 生成表信息
|
||||
* @param context 模板上下文
|
||||
* @param templates 待渲染模板
|
||||
*/
|
||||
private record RenderContext(GenTable table, Dict context, List<PathNamedTemplate> templates) {
|
||||
@@ -488,6 +488,11 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验生成选项中配置的字段是否存在。
|
||||
*
|
||||
* @param genTable 业务表信息
|
||||
*/
|
||||
private void validateOptionColumns(GenTable genTable) {
|
||||
Map<String, Object> params = genTable.getParams();
|
||||
if (CollUtil.isEmpty(params) || CollUtil.isEmpty(genTable.getColumns())) {
|
||||
@@ -510,6 +515,13 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验单个选项字段。
|
||||
*
|
||||
* @param validFields 有效字段集合
|
||||
* @param field 待校验字段
|
||||
* @param label 字段显示名称
|
||||
*/
|
||||
private void validateOptionField(Set<String> validFields, Object field, String label) {
|
||||
if (ObjectUtil.isNull(field)) {
|
||||
return;
|
||||
@@ -523,6 +535,11 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 规范化字段扩展配置。
|
||||
*
|
||||
* @param columns 表字段列表
|
||||
*/
|
||||
private void normalizeColumnOptions(List<GenTableColumn> columns) {
|
||||
if (CollUtil.isEmpty(columns)) {
|
||||
return;
|
||||
|
||||
@@ -134,6 +134,15 @@ public class GenUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析数值类型对应的 Java 类型。
|
||||
*
|
||||
* @param dataType 数据库字段类型
|
||||
* @param columnLength 字段长度
|
||||
* @param columnScale 小数位数
|
||||
* @param columnName 字段名称
|
||||
* @return Java 类型
|
||||
*/
|
||||
private static String resolveNumberJavaType(String dataType, Integer columnLength, Integer columnScale, String columnName) {
|
||||
if (isBooleanColumn(dataType, columnLength, columnScale, columnName)) {
|
||||
return GenConstants.TYPE_BOOLEAN;
|
||||
@@ -159,6 +168,12 @@ public class GenUtils {
|
||||
return GenConstants.TYPE_LONG;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据整数字段长度解析 Java 类型。
|
||||
*
|
||||
* @param columnLength 字段长度
|
||||
* @return Java 类型
|
||||
*/
|
||||
private static String resolveIntegerJavaType(Integer columnLength) {
|
||||
if (columnLength > 0 && columnLength <= 9) {
|
||||
return GenConstants.TYPE_INTEGER;
|
||||
@@ -166,6 +181,15 @@ public class GenUtils {
|
||||
return GenConstants.TYPE_LONG;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字段是否适合按布尔类型生成。
|
||||
*
|
||||
* @param dataType 数据库字段类型
|
||||
* @param columnLength 字段长度
|
||||
* @param columnScale 小数位数
|
||||
* @param columnName 字段名称
|
||||
* @return 是否布尔字段
|
||||
*/
|
||||
private static boolean isBooleanColumn(String dataType, Integer columnLength, Integer columnScale, String columnName) {
|
||||
if (columnScale > 0) {
|
||||
return false;
|
||||
@@ -180,6 +204,12 @@ public class GenUtils {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字段名称是否为开关类字段。
|
||||
*
|
||||
* @param columnName 字段名称
|
||||
* @return 是否开关类字段
|
||||
*/
|
||||
private static boolean isSwitchColumn(String columnName) {
|
||||
return StringUtils.endsWithAny(columnName, "status", "flag", "enabled", "disabled", "available", "visible")
|
||||
|| columnName.startsWith("is_")
|
||||
@@ -188,6 +218,12 @@ public class GenUtils {
|
||||
|| columnName.startsWith("disable_");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字段名称是否为排序字段。
|
||||
*
|
||||
* @param columnName 字段名称
|
||||
* @return 是否排序字段
|
||||
*/
|
||||
private static boolean isSortColumn(String columnName) {
|
||||
return StringUtils.endsWithAny(columnName, "sort", "order_num", "order", "rank", "seq", "sequence");
|
||||
}
|
||||
|
||||
+41
-5
@@ -140,9 +140,9 @@ public class TemplateEngineUtils {
|
||||
/**
|
||||
* 向树形模板上下文写入树字段相关变量。
|
||||
*
|
||||
* @param context 模板上下文
|
||||
* @param genTable 代码生成业务表对象
|
||||
* @param paramsObj 已解析的 options 参数(避免重复解析)
|
||||
* @param context 模板上下文
|
||||
* @param genTable 代码生成业务表对象
|
||||
* @param paramsObj 已解析的 options 参数(避免重复解析)
|
||||
*/
|
||||
public static void setTreeContext(Dict context, GenTable genTable, Dict paramsObj) {
|
||||
String treeCode = getTreeCode(paramsObj);
|
||||
@@ -328,7 +328,7 @@ public class TemplateEngineUtils {
|
||||
/**
|
||||
* 添加字典列表
|
||||
*
|
||||
* @param dicts 字典列表
|
||||
* @param dicts 字典列表
|
||||
* @param columns 列集合
|
||||
*/
|
||||
public static void addDicts(Set<String> dicts, List<GenTableColumn> columns) {
|
||||
@@ -408,7 +408,7 @@ public class TemplateEngineUtils {
|
||||
/**
|
||||
* 获取树根节点值。
|
||||
*
|
||||
* @param paramsObj 其他选项
|
||||
* @param paramsObj 其他选项
|
||||
* @param treeParentColumn 父节点字段
|
||||
* @return 树根节点值
|
||||
*/
|
||||
@@ -423,6 +423,14 @@ public class TemplateEngineUtils {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取布尔类型生成选项。
|
||||
*
|
||||
* @param paramsObj 生成选项
|
||||
* @param key 选项键
|
||||
* @param defaultValue 默认值
|
||||
* @return 选项值
|
||||
*/
|
||||
private static boolean getBooleanOption(Dict paramsObj, String key, boolean defaultValue) {
|
||||
if (CollUtil.isEmpty(paramsObj) || !paramsObj.containsKey(key)) {
|
||||
return defaultValue;
|
||||
@@ -430,6 +438,13 @@ public class TemplateEngineUtils {
|
||||
return Convert.toBool(paramsObj.get(key), defaultValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字段名查找业务表字段。
|
||||
*
|
||||
* @param genTable 业务表
|
||||
* @param field 字段名称或 Java 属性名
|
||||
* @return 业务表字段
|
||||
*/
|
||||
private static GenTableColumn getColumn(GenTable genTable, String field) {
|
||||
if (StringUtils.isBlank(field) || CollUtil.isEmpty(genTable.getColumns())) {
|
||||
return null;
|
||||
@@ -442,6 +457,13 @@ public class TemplateEngineUtils {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字段配置查找业务表字段列表。
|
||||
*
|
||||
* @param genTable 业务表
|
||||
* @param fieldValues 字段配置值
|
||||
* @return 业务表字段列表
|
||||
*/
|
||||
private static List<GenTableColumn> getColumns(GenTable genTable, Object fieldValues) {
|
||||
List<String> fields = new ArrayList<>();
|
||||
if (fieldValues instanceof Collection<?> collection) {
|
||||
@@ -459,6 +481,13 @@ public class TemplateEngineUtils {
|
||||
return columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 Java 字面量。
|
||||
*
|
||||
* @param column 业务表字段
|
||||
* @param value 字段值
|
||||
* @return Java 字面量
|
||||
*/
|
||||
private static String getJavaLiteral(GenTableColumn column, String value) {
|
||||
if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) {
|
||||
return "null";
|
||||
@@ -472,6 +501,13 @@ public class TemplateEngineUtils {
|
||||
return "\"" + value + "\"";
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 TypeScript 字面量。
|
||||
*
|
||||
* @param column 业务表字段
|
||||
* @param value 字段值
|
||||
* @return TypeScript 字面量
|
||||
*/
|
||||
private static String getTsLiteral(GenTableColumn column, String value) {
|
||||
if (ObjectUtil.isNull(column) || StringUtils.isBlank(value)) {
|
||||
return "undefined";
|
||||
|
||||
+32
-2
@@ -23,26 +23,56 @@ public class PathNamedTemplate implements Template {
|
||||
|
||||
private final Template delegate;
|
||||
|
||||
/**
|
||||
* 创建基于路径命名的模板。
|
||||
*
|
||||
* @param pathName 路径名称
|
||||
* @param delegate 委托模板
|
||||
*/
|
||||
private PathNamedTemplate(String pathName, Template delegate) {
|
||||
this.pathName = pathName;
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板到字符输出器。
|
||||
*
|
||||
* @param bindingMap 模板绑定数据
|
||||
* @param writer 字符输出器
|
||||
*/
|
||||
@Override
|
||||
public void render(Map<?, ?> bindingMap, Writer writer) {
|
||||
delegate.render(bindingMap, writer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板到输出流。
|
||||
*
|
||||
* @param bindingMap 模板绑定数据
|
||||
* @param out 输出流
|
||||
*/
|
||||
@Override
|
||||
public void render(Map<?, ?> bindingMap, OutputStream out) {
|
||||
delegate.render(bindingMap, out);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板到文件。
|
||||
*
|
||||
* @param bindingMap 模板绑定数据
|
||||
* @param file 输出文件
|
||||
*/
|
||||
@Override
|
||||
public void render(Map<?, ?> bindingMap, File file) {
|
||||
delegate.render(bindingMap, file);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染模板为字符串。
|
||||
*
|
||||
* @param bindingMap 模板绑定数据
|
||||
* @return 渲染结果
|
||||
*/
|
||||
@Override
|
||||
public String render(Map<?, ?> bindingMap) {
|
||||
return delegate.render(bindingMap);
|
||||
@@ -66,8 +96,8 @@ public class PathNamedTemplate implements Template {
|
||||
* @param pathName 路径名称
|
||||
* @return 带路径名称的模板
|
||||
*/
|
||||
public static PathNamedTemplate form(TemplateEngine templateEngine,String pathName) {
|
||||
return new PathNamedTemplate(pathName,templateEngine.getTemplate(pathName));
|
||||
public static PathNamedTemplate form(TemplateEngine templateEngine, String pathName) {
|
||||
return new PathNamedTemplate(pathName, templateEngine.getTemplate(pathName));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,6 +20,9 @@ import org.springframework.stereotype.Component;
|
||||
@JobExecutor(name = "testBroadcastJob")
|
||||
public class TestBroadcastJob {
|
||||
|
||||
/**
|
||||
* 客户端端口。
|
||||
*/
|
||||
@Value("${snail-job.port}")
|
||||
private int clientPort;
|
||||
|
||||
|
||||
+6
@@ -12,6 +12,12 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class TestClassJobExecutor extends AbstractJobExecutor {
|
||||
|
||||
/**
|
||||
* 执行测试类任务。
|
||||
*
|
||||
* @param jobArgs 任务参数
|
||||
* @return 任务执行结果
|
||||
*/
|
||||
@Override
|
||||
protected ExecuteResult doJobExecute(JobArgs jobArgs) {
|
||||
return ExecuteResult.success("TestJobExecutor测试成功");
|
||||
|
||||
@@ -37,23 +37,51 @@ public class MetaVo {
|
||||
*/
|
||||
private String activeMenu;
|
||||
|
||||
/**
|
||||
* 构造路由显示信息。
|
||||
*
|
||||
* @param title 路由标题
|
||||
* @param icon 路由图标
|
||||
*/
|
||||
public MetaVo(String title, String icon) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造路由显示信息。
|
||||
*
|
||||
* @param title 路由标题
|
||||
* @param icon 路由图标
|
||||
* @param noCache 是否不缓存
|
||||
*/
|
||||
public MetaVo(String title, String icon, Boolean noCache) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
this.noCache = noCache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带内链地址的路由显示信息。
|
||||
*
|
||||
* @param title 路由标题
|
||||
* @param icon 路由图标
|
||||
* @param link 内链地址
|
||||
*/
|
||||
public MetaVo(String title, String icon, String link) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
this.link = link;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带缓存配置和内链地址的路由显示信息。
|
||||
*
|
||||
* @param title 路由标题
|
||||
* @param icon 路由图标
|
||||
* @param noCache 是否不缓存
|
||||
* @param link 内链地址
|
||||
*/
|
||||
public MetaVo(String title, String icon, Boolean noCache, String link) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
@@ -63,6 +91,15 @@ public class MetaVo {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造带激活菜单的路由显示信息。
|
||||
*
|
||||
* @param title 路由标题
|
||||
* @param icon 路由图标
|
||||
* @param noCache 是否不缓存
|
||||
* @param link 内链地址
|
||||
* @param activeMenu 激活菜单路径
|
||||
*/
|
||||
public MetaVo(String title, String icon, Boolean noCache, String link, String activeMenu) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
|
||||
+5
@@ -37,6 +37,11 @@ public class DeptExcelConverter implements Converter<Long> {
|
||||
.expireAfterWrite(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
/**
|
||||
* 获取部门导入导出映射缓存。
|
||||
*
|
||||
* @return 部门 ID 与部门路径名称互转映射
|
||||
*/
|
||||
private DeptMaps getDeptMaps() {
|
||||
ISysDeptService deptService = SpringUtils.getBean(ISysDeptService.class);
|
||||
return DEPT_CACHE.get(CACHE_KEY, k -> {
|
||||
|
||||
+36
@@ -47,6 +47,11 @@ public class SysUserImportListener extends AnalysisEventListener<SysUserImportVo
|
||||
private final StringBuilder successMsg = new StringBuilder();
|
||||
private final StringBuilder failureMsg = new StringBuilder();
|
||||
|
||||
/**
|
||||
* 构造用户导入监听器。
|
||||
*
|
||||
* @param isUpdateSupport 是否允许更新已存在用户
|
||||
*/
|
||||
public SysUserImportListener(Boolean isUpdateSupport) {
|
||||
String initPassword = SpringUtils.getBean(ISysConfigService.class).selectConfigByKey("sys.user.initPassword");
|
||||
this.userService = SpringUtils.getBean(ISysUserService.class);
|
||||
@@ -55,6 +60,12 @@ public class SysUserImportListener extends AnalysisEventListener<SysUserImportVo
|
||||
this.operUserId = LoginHelper.getUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐行处理用户导入数据。
|
||||
*
|
||||
* @param userVo 导入用户数据
|
||||
* @param context Excel 解析上下文
|
||||
*/
|
||||
@Override
|
||||
public void invoke(SysUserImportVo userVo, AnalysisContext context) {
|
||||
SysUserVo sysUser = this.userService.selectUserByUserName(userVo.getUserName());
|
||||
@@ -95,15 +106,30 @@ public class SysUserImportListener extends AnalysisEventListener<SysUserImportVo
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 所有数据解析完成后的回调。
|
||||
*
|
||||
* @param context Excel 解析上下文
|
||||
*/
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext context) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户导入结果。
|
||||
*
|
||||
* @return Excel 导入结果
|
||||
*/
|
||||
@Override
|
||||
public ExcelResult<SysUserImportVo> getExcelResult() {
|
||||
return new ExcelResult<>() {
|
||||
|
||||
/**
|
||||
* 获取导入结果分析消息。
|
||||
*
|
||||
* @return 导入结果消息
|
||||
*/
|
||||
@Override
|
||||
public String getAnalysis() {
|
||||
if (failureNum > 0) {
|
||||
@@ -115,11 +141,21 @@ public class SysUserImportListener extends AnalysisEventListener<SysUserImportVo
|
||||
return successMsg.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入成功数据列表。
|
||||
*
|
||||
* @return 导入成功数据列表
|
||||
*/
|
||||
@Override
|
||||
public List<SysUserImportVo> getList() {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取导入错误信息列表。
|
||||
*
|
||||
* @return 导入错误信息列表
|
||||
*/
|
||||
@Override
|
||||
public List<String> getErrorList() {
|
||||
return null;
|
||||
|
||||
+3
@@ -19,6 +19,9 @@ public class FlowDefinitionVo implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 流程定义主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
|
||||
+3
@@ -23,6 +23,9 @@ public class FlowHisTaskVo implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 历史任务主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
|
||||
+23
-1
@@ -34,7 +34,11 @@ import org.dromara.workflow.service.IFlwNodeExtService;
|
||||
import org.dromara.workflow.service.IFlwTaskService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.*;
|
||||
import java.io.Serial;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 工作流全局监听器,处理任务流转中的扩展变量、消息和事件发布。
|
||||
@@ -48,6 +52,8 @@ import java.util.*;
|
||||
public class WorkflowGlobalListener implements GlobalListener {
|
||||
|
||||
private static final String NODE_KEY_SEPARATOR = ":";
|
||||
@Serial
|
||||
private static final long serialVersionUID = -5133036757491932497L;
|
||||
|
||||
private final IFlwTaskService flwTaskService;
|
||||
private final IFlwInstanceService flwInstanceService;
|
||||
@@ -256,6 +262,14 @@ public class WorkflowGlobalListener implements GlobalListener {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否需要发送后续待办消息。
|
||||
*
|
||||
* @param flowParams 流程参数
|
||||
* @param definition 流程定义
|
||||
* @param nextTasks 后续任务列表
|
||||
* @return 是否发送待办消息
|
||||
*/
|
||||
private boolean shouldSendTaskMessage(FlowParams flowParams, Definition definition, List<Task> nextTasks) {
|
||||
if (flowParams == null || !TaskStatusEnum.BACK.getStatus().equals(flowParams.getHisStatus())) {
|
||||
return true;
|
||||
@@ -268,6 +282,14 @@ public class WorkflowGlobalListener implements GlobalListener {
|
||||
return !StringUtils.equals(applyNodeCode, nextTasks.get(0).getNodeCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 在流程完成或退回时通知发起人。
|
||||
*
|
||||
* @param definition 流程定义
|
||||
* @param instance 流程实例
|
||||
* @param status 业务状态
|
||||
* @param variable 流程变量
|
||||
*/
|
||||
private void notifyInitiatorIfNeeded(Definition definition, Instance instance, String status, Map<String, Object> variable) {
|
||||
if (!StringUtils.equalsAny(status, BusinessStatusEnum.FINISH.getStatus(), BusinessStatusEnum.BACK.getStatus())) {
|
||||
return;
|
||||
|
||||
+9
@@ -24,6 +24,15 @@ import java.util.Map;
|
||||
*/
|
||||
public interface FlwHisTaskMapper extends BaseMapperPlus<FlowHisTask, FlowHisTaskVo>, MPJBaseMapper<FlowHisTask> {
|
||||
|
||||
/**
|
||||
* 分页查询已办任务列表。
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param bo 查询条件
|
||||
* @param categoryIds 流程分类 ID 集合
|
||||
* @param userId 当前用户 ID
|
||||
* @return 已办任务分页结果
|
||||
*/
|
||||
default Page<FlowHisTaskVo> getListFinishTask(Page<FlowHisTaskVo> page, FlowTaskBo bo, List<String> categoryIds, String userId) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
return QueryBuilder.lambdaJoin("a", FlowHisTask.class)
|
||||
|
||||
+18
@@ -22,6 +22,15 @@ import java.util.Map;
|
||||
*/
|
||||
public interface FlwTaskMapper extends BaseMapperPlus<FlowTask, FlowTaskVo>, MPJBaseMapper<FlowTask> {
|
||||
|
||||
/**
|
||||
* 分页查询运行中的待办任务。
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param bo 查询条件
|
||||
* @param categoryIds 流程分类 id 列表
|
||||
* @param userId 当前用户 id
|
||||
* @return 待办任务分页结果
|
||||
*/
|
||||
default Page<FlowTaskVo> getListRunTask(Page<FlowTaskVo> page, FlowTaskBo bo, List<String> categoryIds, String userId) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
return QueryBuilder.lambdaJoin("t", FlowTask.class)
|
||||
@@ -54,6 +63,15 @@ public interface FlwTaskMapper extends BaseMapperPlus<FlowTask, FlowTaskVo>, MPJ
|
||||
.page(page, FlowTaskVo.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询抄送任务。
|
||||
*
|
||||
* @param page 分页对象
|
||||
* @param bo 查询条件
|
||||
* @param categoryIds 流程分类 id 列表
|
||||
* @param userId 当前用户 id
|
||||
* @return 抄送任务分页结果
|
||||
*/
|
||||
default Page<FlowTaskVo> getTaskCopyByPage(Page<FlowTaskVo> page, FlowTaskBo bo, List<String> categoryIds, String userId) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
return QueryBuilder.lambdaJoin("a", FlowUser.class)
|
||||
|
||||
+28
@@ -89,6 +89,14 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
|
||||
sendMessage(messageType, message, subject, userList, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送流程结果通知。
|
||||
*
|
||||
* @param flowName 流程名称
|
||||
* @param status 业务状态
|
||||
* @param messageType 消息类型列表
|
||||
* @param userList 接收用户列表
|
||||
*/
|
||||
@Override
|
||||
public void sendResultMessage(String flowName, BusinessStatusEnum status, List<String> messageType, List<UserDTO> userList) {
|
||||
if (status == null || CollUtil.isEmpty(messageType) || CollUtil.isEmpty(userList)) {
|
||||
@@ -99,6 +107,15 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
|
||||
sendMessage(messageType, message, DEFAULT_SUBJECT, userList, PATH_MY_DOCUMENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给指定用户列表。
|
||||
*
|
||||
* @param messageType 消息类型列表
|
||||
* @param message 消息内容
|
||||
* @param subject 邮件标题
|
||||
* @param userList 接收用户列表
|
||||
* @param path 前端跳转路径
|
||||
*/
|
||||
@Override
|
||||
public void sendMessage(List<String> messageType, String message, String subject, List<UserDTO> userList, String path) {
|
||||
if (CollUtil.isEmpty(messageType) || CollUtil.isEmpty(userList)) {
|
||||
@@ -114,6 +131,17 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
|
||||
ThreadUtils.virtualInvokeAll(sendTasks);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按消息类型执行具体发送逻辑。
|
||||
*
|
||||
* @param code 消息类型编码
|
||||
* @param message 消息内容
|
||||
* @param subject 邮件标题
|
||||
* @param path 前端跳转路径
|
||||
* @param userIds 接收用户 id 列表
|
||||
* @param emails 接收邮箱集合
|
||||
* @param userCount 接收用户数量
|
||||
*/
|
||||
private void sendMessageByType(String code, String message, String subject, String path, List<Long> userIds, Set<String> emails, int userCount) {
|
||||
MessageTypeEnum messageTypeEnum = MessageTypeEnum.getByCode(code);
|
||||
if (ObjectUtil.isEmpty(messageTypeEnum)) {
|
||||
|
||||
+5
@@ -273,6 +273,11 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验当前用户是否有权限删除流程实例,并发布删除事件。
|
||||
*
|
||||
* @param flowInstances 待删除流程实例列表
|
||||
*/
|
||||
private void processDeleteHandler(List<FlowInstance> flowInstances) {
|
||||
|
||||
String userId = LoginHelper.getUserIdStr();
|
||||
|
||||
+14
-2
@@ -120,7 +120,7 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand
|
||||
/**
|
||||
* 根据办理人类型查询右侧候选数据。
|
||||
*
|
||||
* @param type 办理人类型
|
||||
* @param type 办理人类型
|
||||
* @param taskQuery 查询条件
|
||||
* @return 办理人数据
|
||||
*/
|
||||
@@ -180,7 +180,7 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand
|
||||
/**
|
||||
* 构建设计器右侧办理人列表数据。
|
||||
*
|
||||
* @param dto 办理人数据
|
||||
* @param dto 办理人数据
|
||||
* @param type 办理人类型
|
||||
* @return 办理人列表构建器
|
||||
*/
|
||||
@@ -218,6 +218,12 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按任务分配类型批量查询用户。
|
||||
*
|
||||
* @param typeIdMap 任务分配类型与 id 列表映射
|
||||
* @return 用户列表
|
||||
*/
|
||||
private List<UserDTO> getUsersByTypes(Map<TaskAssigneeEnum, List<String>> typeIdMap) {
|
||||
return typeIdMap.entrySet().stream()
|
||||
.map(entry -> this.getUsersByType(entry.getKey(), entry.getValue()))
|
||||
@@ -226,6 +232,12 @@ public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, Hand
|
||||
.toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按任务分配类型批量查询名称映射。
|
||||
*
|
||||
* @param typeIdMap 任务分配类型与 id 列表映射
|
||||
* @return 任务分配类型与名称映射
|
||||
*/
|
||||
private Map<TaskAssigneeEnum, Map<String, String>> getNamesByTypes(Map<TaskAssigneeEnum, List<String>> typeIdMap) {
|
||||
Map<TaskAssigneeEnum, Map<String, String>> nameMap = new EnumMap<>(TaskAssigneeEnum.class);
|
||||
typeIdMap.forEach((type, ids) -> nameMap.put(type, this.getNamesByType(type, ids)));
|
||||
|
||||
Reference in New Issue
Block a user