docs 补充项目注释

This commit is contained in:
AprilWind
2026-06-01 13:58:13 +08:00
parent 107d3326b4
commit e49f3b2260
94 changed files with 2328 additions and 68 deletions
@@ -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;
@@ -14,7 +14,7 @@ import java.util.concurrent.*;
* 线程池配置
*
* @author Lion Li
**/
*/
@Slf4j
@AutoConfiguration
public class ThreadPoolConfig {
@@ -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",
@@ -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",
@@ -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)) {
@@ -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)) {
@@ -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;
@@ -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 {
}
}
@@ -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;
@@ -11,6 +11,11 @@ import org.dromara.common.encrypt.core.IEncryptor;
*/
public abstract class AbstractEncryptor implements IEncryptor {
/**
* 初始化加密执行者。
*
* @param context 加密上下文
*/
public AbstractEncryptor(EncryptContext context) {
// 用户配置校验与配置注入
}
@@ -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);
}
@@ -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) {
@@ -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) {
@@ -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) {
@@ -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() {
}
@@ -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) {
@@ -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);
@@ -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) {
@@ -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) {
}
@@ -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) {
@@ -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)) {
@@ -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));
}
@@ -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);
}
@@ -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);
}
}
/**
* 创建一个单元格合并处理器实例
*
@@ -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)) {
@@ -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;
@@ -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;
@@ -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);
@@ -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();
}
@@ -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)) {
@@ -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("数据为空");
@@ -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;
/**
@@ -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();
@@ -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) {
@@ -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) {
// 离线时更新重连
@@ -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));
@@ -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) {
@@ -35,6 +35,9 @@ public class DefaultOssClientImpl extends AbstractOssClientImpl {
super(clientId, config);
}
/**
* 初始化默认 S3 客户端、传输管理器和预签名生成器。
*/
@Override
void doInitialize() {
// 校验配置
@@ -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()
@@ -52,11 +52,21 @@ public record OssAsyncExecutorConfig(
return corePoolSize;
}
/**
* 复制异步执行器配置。
*
* @return 配置副本
*/
@Override
public OssAsyncExecutorConfig copy() {
return toBuilder().build();
}
/**
* 转换为构建器。
*
* @return 配置构建器
*/
@Override
public OssAsyncExecutorConfigBuilder toBuilder() {
return builder()
@@ -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);
}
@@ -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();
@@ -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) {
@@ -85,6 +85,13 @@ public class RateLimiterAspect {
}
}
/**
* 组装限流缓存键
*
* @param rateLimiter 限流注解配置
* @param point 切点信息
* @return 限流缓存键
*/
private String getCombineKey(RateLimiter rateLimiter, JoinPoint point) {
String key = rateLimiter.key();
// 判断 key 不为空 不是表达式
@@ -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;
}
}
@@ -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));
@@ -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());
@@ -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) {
@@ -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 (简单模式)
@@ -18,6 +18,11 @@ import java.lang.annotation.Target;
@Documented
public @interface Sensitive {
/**
* 脱敏策略
*
* @return 脱敏策略
*/
SensitiveStrategy strategy();
/**
@@ -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);
@@ -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);
@@ -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();
@@ -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);
@@ -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);
@@ -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")) {
@@ -23,6 +23,7 @@ public class FilterConfig {
/**
* 注册 XSS 过滤器
*
* @param xssProperties XSS 配置
* @return XSS 请求过滤器实例
*/
@Bean
@@ -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) {
@@ -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();
@@ -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 监控启动成功");
@@ -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();
}
@@ -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);
}
@@ -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() {
}
@@ -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;
@@ -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);
}
@@ -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() {
}
@@ -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;
@@ -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);
}
@@ -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);
@@ -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());
@@ -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"),
@@ -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,
@@ -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 -> {
@@ -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");
}
@@ -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";
@@ -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;
@@ -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;
@@ -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 -> {
@@ -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;
@@ -19,6 +19,9 @@ public class FlowDefinitionVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程定义主键
*/
private Long id;
/**
@@ -23,6 +23,9 @@ public class FlowHisTaskVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 历史任务主键
*/
private Long id;
/**
@@ -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;
@@ -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)
@@ -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)
@@ -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)) {
@@ -273,6 +273,11 @@ public class FlwInstanceServiceImpl implements IFlwInstanceService {
}
/**
* 校验当前用户是否有权限删除流程实例并发布删除事件
*
* @param flowInstances 待删除流程实例列表
*/
private void processDeleteHandler(List<FlowInstance> flowInstances) {
String userId = LoginHelper.getUserIdStr();
@@ -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)));