Compare commits

..

8 Commits

26 changed files with 792 additions and 482 deletions

View File

@@ -1,3 +1,21 @@
## [v2.5.2](https://github.com/continew-org/continew-starter/compare/v2.5.1...v2.5.2) (2024-08-14)
### 💎 功能优化
- 【api-doc】重构接口文档枚举展示处理 ([bf51837](https://github.com/continew-org/continew-starter/commit/bf51837991746c5576d7baffcc2296fe8ca91586)) ([4c4f98a](https://github.com/continew-org/continew-starter/commit/4c4f98a86e6d10b2b52814406bd0dc3d248c8486))
- 【web】针对最新响应风格增加全局响应格式 ([bf51837](https://github.com/continew-org/continew-starter/commit/bf51837991746c5576d7baffcc2296fe8ca91586)) ([4c4f98a](https://github.com/continew-org/continew-starter/commit/4c4f98a86e6d10b2b52814406bd0dc3d248c8486))
### 🐛 问题修复
- 【extension/crud】重构排序字段处理预防 SQL 注入问题 ([c31fa75](https://github.com/continew-org/continew-starter/commit/c31fa753b6d9753d72a2e28bf3184981ed848ac2)) ([22ebdfe](https://github.com/continew-org/continew-starter/commit/22ebdfeb9f30a8832825424a05343acfaf31643b))
- 【security/crypto】修复 updateById 修改未正确加密的问题 ([b0a2a8c](https://github.com/continew-org/continew-starter/commit/b0a2a8c927171795e3ed89a820caf829f76c80ee))
## [v2.5.1](https://github.com/continew-org/continew-starter/compare/v2.5.0...v2.5.1) (2024-08-12)
### 🐛 问题修复
- 【data】移除 SQL 函数接口中的 SQL 拼接 ([6693cd4](https://github.com/continew-org/continew-starter/commit/6693cd49b93244b42dfadd4f4be28e526d764425))
## [v2.5.0](https://github.com/continew-org/continew-starter/compare/v2.4.0...v2.5.0) (2024-08-07)
### ✨ 新特性

View File

@@ -17,16 +17,11 @@
package top.continew.starter.apidoc.autoconfigure;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.SimpleType;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import jakarta.annotation.PostConstruct;
@@ -48,15 +43,13 @@ import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import top.continew.starter.apidoc.handler.BaseEnumParameterHandler;
import top.continew.starter.apidoc.handler.OpenApiHandler;
import top.continew.starter.apidoc.util.EnumTypeUtils;
import top.continew.starter.core.autoconfigure.project.ProjectProperties;
import top.continew.starter.core.enums.BaseEnum;
import top.continew.starter.core.util.GeneralPropertySourceFactory;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* API 文档自动配置
@@ -156,95 +149,14 @@ public class SpringDocAutoConfiguration implements WebMvcConfigurer {
}
/**
* 自定义参数配置(针对 BaseEnum 展示枚举值和描述)
* 自定义 BaseEnum 枚举参数配置(针对实现了 BaseEnum 的枚举,优化其枚举值和描述展示
*
* @return {@link BaseEnumParameterHandler }
* @since 2.4.0
*/
@Bean
public ParameterCustomizer customParameterCustomizer() {
return (parameterModel, methodParameter) -> {
Class<?> parameterType = methodParameter.getParameterType();
// 判断是否为 BaseEnum 的子类型
if (!ClassUtil.isAssignable(BaseEnum.class, parameterType)) {
return parameterModel;
}
String description = parameterModel.getDescription();
if (StrUtil.contains(description, "color:red")) {
return parameterModel;
}
// 封装参数配置
this.configureSchema(parameterModel.getSchema(), parameterType);
// 自定义枚举描述
parameterModel.setDescription(description + "<span style='color:red'>" + this
.getDescMap(parameterType) + "</span>");
return parameterModel;
};
}
/**
* 自定义参数配置(针对 BaseEnum 展示枚举值和描述)
*
* @since 2.4.0
*/
@Bean
public PropertyCustomizer customPropertyCustomizer() {
return (schema, type) -> {
Class<?> rawClass;
// 获取原始类的类型
if (type.getType() instanceof SimpleType) {
rawClass = ((SimpleType)type.getType()).getRawClass();
} else if (type.getType() instanceof CollectionType) {
rawClass = ((CollectionType)type.getType()).getContentType().getRawClass();
} else {
rawClass = Object.class;
}
// 判断是否为 BaseEnum 的子类型
if (!ClassUtil.isAssignable(BaseEnum.class, rawClass)) {
return schema;
}
// 封装参数配置
this.configureSchema(schema, rawClass);
// 自定义参数描述
schema.setDescription(schema.getDescription() + "<span style='color:red'>" + this
.getDescMap(rawClass) + "</span>");
return schema;
};
}
/**
* 封装 Schema 配置
*
* @param schema Schema
* @param enumClass 枚举类型
* @since 2.4.0
*/
private void configureSchema(Schema schema, Class<?> enumClass) {
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
// 设置枚举可用值
List<String> valueList = Arrays.stream(enums).map(e -> e.getValue().toString()).toList();
schema.setEnum(valueList);
// 设置枚举值类型和格式
String enumValueType = EnumTypeUtils.getEnumValueTypeAsString(enumClass);
schema.setType(enumValueType);
switch (enumValueType) {
case "integer" -> schema.setFormat("int32");
case "long" -> schema.setFormat("int64");
case "number" -> schema.setFormat("double");
default -> schema.setFormat(enumValueType);
}
}
/**
* 获取枚举描述 Map
*
* @param enumClass 枚举类型
* @return 枚举描述 Map
* @since 2.4.0
*/
private Map<Object, String> getDescMap(Class<?> enumClass) {
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
return Arrays.stream(enums)
.collect(Collectors.toMap(BaseEnum::getValue, BaseEnum::getDescription, (a, b) -> a, LinkedHashMap::new));
public BaseEnumParameterHandler customParameterCustomizer() {
return new BaseEnumParameterHandler();
}
@PostConstruct

View File

@@ -0,0 +1,118 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.apidoc.handler;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.databind.type.CollectionType;
import com.fasterxml.jackson.databind.type.SimpleType;
import io.swagger.v3.core.converter.AnnotatedType;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.parameters.Parameter;
import org.springdoc.core.customizers.ParameterCustomizer;
import org.springdoc.core.customizers.PropertyCustomizer;
import org.springframework.core.MethodParameter;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import top.continew.starter.apidoc.util.DocUtils;
import top.continew.starter.core.enums.BaseEnum;
/**
* 自定义 BaseEnum 枚举参数处理器
* <p>
* 针对实现了 BaseEnum 的枚举,优化其枚举值和描述展示
* </p>
*
* @author echo
* @since 2.5.2
*/
public class BaseEnumParameterHandler implements ParameterCustomizer, PropertyCustomizer {
@Override
public Parameter customize(Parameter parameterModel, MethodParameter methodParameter) {
Class<?> parameterType = methodParameter.getParameterType();
// 判断是否为 BaseEnum 的子类型
if (!ClassUtil.isAssignable(BaseEnum.class, parameterType)) {
return parameterModel;
}
String description = parameterModel.getDescription();
if (StrUtil.contains(description, "color:red")) {
return parameterModel;
}
// 自定义枚举描述并封装参数配置
configureSchema(parameterModel.getSchema(), parameterType);
parameterModel.setDescription(appendEnumDescription(description, parameterType));
return parameterModel;
}
@Override
public Schema customize(Schema schema, AnnotatedType type) {
Class<?> rawClass = resolveRawClass(type.getType());
// 判断是否为 BaseEnum 的子类型
if (!ClassUtil.isAssignable(BaseEnum.class, rawClass)) {
return schema;
}
// 自定义参数描述并封装参数配置
configureSchema(schema, rawClass);
schema.setDescription(appendEnumDescription(schema.getDescription(), rawClass));
return schema;
}
/**
* 封装 Schema 配置
*
* @param schema Schema
* @param enumClass 枚举类型
*/
private void configureSchema(Schema schema, Class<?> enumClass) {
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
List<String> valueList = Arrays.stream(enums).map(e -> e.getValue().toString()).toList();
schema.setEnum(valueList);
String enumValueType = DocUtils.getEnumValueTypeAsString(enumClass);
schema.setType(enumValueType);
schema.setFormat(DocUtils.resolveFormat(enumValueType));
}
/**
* 追加枚举描述
*
* @param originalDescription 原始描述
* @param enumClass 枚举类型
* @return 追加后的描述字符串
*/
private String appendEnumDescription(String originalDescription, Class<?> enumClass) {
return originalDescription + "<span style='color:red'>" + DocUtils.getDescMap(enumClass) + "</span>";
}
/**
* 解析原始类
*
* @param type 类型
* @return 原始类的 Class 对象
*/
private Class<?> resolveRawClass(Type type) {
if (type instanceof SimpleType simpleType) {
return simpleType.getRawClass();
} else if (type instanceof CollectionType collectionType) {
return collectionType.getContentType().getRawClass();
} else {
return Object.class;
}
}
}

View File

@@ -0,0 +1,118 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.apidoc.util;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.web.bind.annotation.RestController;
import top.continew.starter.core.enums.BaseEnum;
/**
* 接口文档工具类
*
* @author echo
* @since 2.5.2
*/
public class DocUtils {
private DocUtils() {
}
/**
* 获取枚举值类型
*
* @param enumClass 枚举类型
* @return 枚举值类型
*/
public static String getEnumValueTypeAsString(Class<?> enumClass) {
// 获取枚举类实现的所有接口
Type[] interfaces = enumClass.getGenericInterfaces();
// 定义枚举值类型的映射
Map<Class<?>, String> typeMap = Map
.of(Integer.class, "integer", Long.class, "long", Double.class, "number", String.class, "string");
// 遍历所有接口
for (Type type : interfaces) {
// 检查接口是否为参数化类型并且原始类型为 BaseEnum
if (type instanceof ParameterizedType parameterizedType && parameterizedType
.getRawType() == BaseEnum.class) {
Type actualType = parameterizedType.getActualTypeArguments()[0];
// 检查实际类型参数是否为类类型,并返回对应的字符串类型
if (actualType instanceof Class<?> actualClass) {
return typeMap.getOrDefault(actualClass, "string");
}
}
}
// 默认返回 "string" 类型
return "string";
}
/**
* 解析枚举值的格式
*
* @param enumValueType 枚举值类型
* @return String 格式化类型
*/
public static String resolveFormat(String enumValueType) {
return switch (enumValueType) {
case "integer" -> "int32";
case "long" -> "int64";
case "number" -> "double";
default -> enumValueType;
};
}
/**
* 具有 RestController 注释既检查是否继承了BaseController
*
* @param clazz clazz
* @return boolean
*/
public static boolean hasRestControllerAnnotation(Class<?> clazz) {
// 如果注释包含 RestController 注解,则返回 true
if (clazz.isAnnotationPresent(RestController.class)) {
return true;
}
// 递归检查父类
Class<?> superClass = clazz.getSuperclass();
// 循环检查父类
while (superClass != null && !superClass.equals(Object.class)) {
// 如果父类包含 RestController 注解,则返回 true
if (hasRestControllerAnnotation(superClass)) {
return true;
}
// 递归检查接口
superClass = superClass.getSuperclass();
}
return false;
}
/**
* 获取枚举描述 Map
*
* @param enumClass 枚举类型
* @return 枚举描述 Map
*/
public static Map<Object, String> getDescMap(Class<?> enumClass) {
BaseEnum[] enums = (BaseEnum[])enumClass.getEnumConstants();
return Arrays.stream(enums)
.collect(Collectors.toMap(BaseEnum::getValue, BaseEnum::getDescription, (a, b) -> a, LinkedHashMap::new));
}
}

View File

@@ -1,73 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.apidoc.util;
import top.continew.starter.core.enums.BaseEnum;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
/**
* 枚举类型工具
*
* @author echo
* @since 2.4.0
*/
public class EnumTypeUtils {
private EnumTypeUtils() {
}
/**
* 获取枚举值类型
*
* @param enumClass 枚举类型
* @return 枚举值类型
*/
public static String getEnumValueTypeAsString(Class<?> enumClass) {
try {
// 获取枚举类实现的所有接口
Type[] interfaces = enumClass.getGenericInterfaces();
// 遍历所有接口
for (Type type : interfaces) {
// 检查接口是否为参数化类型
if (type instanceof ParameterizedType parameterizedType) {
// 检查接口的原始类型是否为 BaseEnum
if (parameterizedType.getRawType() != BaseEnum.class) {
continue;
}
Type actualType = parameterizedType.getActualTypeArguments()[0];
// 检查实际类型参数是否为类类型
if (actualType instanceof Class<?> actualClass) {
if (actualClass == Integer.class) {
return "integer";
} else if (actualClass == Long.class) {
return "long";
} else if (actualClass == Double.class) {
return "number";
} else if (actualClass == String.class) {
return "string";
}
}
}
}
} catch (Exception ignored) {
// ignored
}
return "string";
}
}

View File

@@ -18,5 +18,10 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-db</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -18,8 +18,6 @@ package top.continew.starter.data.core.enums;
import top.continew.starter.data.core.function.ISqlFunction;
import java.io.Serializable;
/**
* 数据库类型枚举
*
@@ -33,8 +31,8 @@ public enum DatabaseType implements ISqlFunction {
*/
MYSQL("MySQL") {
@Override
public String findInSet(Serializable value, String set) {
return "find_in_set('%s', %s) <> 0".formatted(value, set);
public String findInSet() {
return "find_in_set({0}, {1}) <> 0";
}
},
@@ -43,8 +41,8 @@ public enum DatabaseType implements ISqlFunction {
*/
POSTGRE_SQL("PostgreSQL") {
@Override
public String findInSet(Serializable value, String set) {
return "(select position(',%s,' in ','||%s||',')) <> 0".formatted(value, set);
public String findInSet() {
return "(select position(',{0},' in ','||{1}||',')) <> 0";
}
},;

View File

@@ -16,8 +16,6 @@
package top.continew.starter.data.core.function;
import java.io.Serializable;
/**
* SQL 函数接口
*
@@ -29,9 +27,7 @@ public interface ISqlFunction {
/**
* find_in_set 函数
*
* @param value 值
* @param set 集合
* @return 函数实现
*/
String findInSet(Serializable value, String set);
String findInSet();
}

View File

@@ -0,0 +1,128 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.data.core.util;
import cn.hutool.core.text.CharSequenceUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.regex.Pattern;
/**
* SQL 注入验证工具类
*
* @author hubin<a href="https://github.com/baomidou/mybatis-plus">MyBatis Plus</a>
* @author zhoujf<a href="https://github.com/jeecgboot/JeecgBoot">JeecgBoot</a>
* @author Charles7c
* @since 2.5.2
*/
public class SqlInjectionUtils {
private static final Logger log = LoggerFactory.getLogger(SqlInjectionUtils.class);
/**
* SQL语法检查正则符合两个关键字有先后顺序才算匹配
*/
private static final Pattern SQL_SYNTAX_PATTERN = Pattern
.compile("(insert|delete|update|select|create|drop|truncate|grant|alter|deny|revoke|call|execute|exec|declare|show|rename|set)" + "\\s+.*(into|from|set|where|table|database|view|index|on|cursor|procedure|trigger|for|password|union|and|or)|(select\\s*\\*\\s*from\\s+)|(and|or)\\s+.*", Pattern.CASE_INSENSITIVE);
/**
* 使用'、;或注释截断SQL检查正则
*/
private static final Pattern SQL_COMMENT_PATTERN = Pattern
.compile("'.*(or|union|--|#|/\\*|;)", Pattern.CASE_INSENSITIVE);
/**
* SQL 语法关键字
*/
private static final String SQL_SYNTAX_KEYWORD = "and |exec |peformance_schema|information_schema|extractvalue|updatexml|geohash|gtid_subset|gtid_subtract|insert |select |delete |update |drop |count |chr |mid |master |truncate |char |declare |;|or |+|--";
/**
* SQL 函数检查正则
*/
private static final String[] SQL_FUNCTION_PATTERN = new String[] {"chr\\s*\\(", "mid\\s*\\(", " char\\s*\\(",
"sleep\\s*\\(", "user\\s*\\(", "show\\s+tables", "user[\\s]*\\([\\s]*\\)", "show\\s+databases",
"sleep\\(\\d*\\)", "sleep\\(.*\\)",};
private static final String MESSAGE_TEMPLATE = "SQL 注入检查: 检查值=>{}<=存在 SQL 注入关键字, 关键字=>{}<=";
private SqlInjectionUtils() {
}
/**
* 检查参数是否存在 SQL 注入
*
* @param value 检查参数
* @return true非法false合法
*/
public static boolean check(String value) {
return check(value, null);
}
/**
* 检查参数是否存在 SQL 注入
*
* @param value 检查参数
* @param customKeyword 自定义关键字
* @return true非法false合法
*/
public static boolean check(String value, String customKeyword) {
if (CharSequenceUtil.isBlank(value)) {
return false;
}
// 处理是否包含 SQL 注释字符 || 检查是否包含 SQL 注入敏感字符
if (SQL_COMMENT_PATTERN.matcher(value).find() || SQL_SYNTAX_PATTERN.matcher(value).find()) {
log.warn("SQL 注入检查: 检查值=>{}<=存在 SQL 注释字符或 SQL 注入敏感字符", value);
return true;
}
// 转换成小写再进行比较
value = value.toLowerCase().trim();
// 检查是否包含 SQL 语法关键字
if (checkKeyword(value, SQL_SYNTAX_KEYWORD.split("\\|"))) {
return true;
}
// 检查是否包含自定义关键字
if (CharSequenceUtil.isNotBlank(customKeyword) && checkKeyword(value, customKeyword.split("\\|"))) {
return true;
}
// 检查是否包含 SQL 注入敏感字符
for (String pattern : SQL_FUNCTION_PATTERN) {
if (Pattern.matches(".*" + pattern + ".*", value)) {
log.warn(MESSAGE_TEMPLATE, value, pattern);
return true;
}
}
return false;
}
/**
* 检查参数是否存在关键字
*
* @param value 检查参数
* @param keywords 关键字列表
* @return true非法false合法
*/
private static boolean checkKeyword(String value, String[] keywords) {
for (String keyword : keywords) {
if (value.contains(keyword)) {
log.warn(MESSAGE_TEMPLATE, value, keyword);
return true;
}
}
return false;
}
}

View File

@@ -14,21 +14,23 @@
* limitations under the License.
*/
package top.continew.starter.data.mybatis.flex.query;
package top.continew.starter.data.mybatis.flex.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import com.mybatisflex.core.query.QueryWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.text.CharSequenceUtil;
import org.springframework.data.domain.Sort;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.annotation.QueryIgnore;
import top.continew.starter.data.core.enums.QueryType;
import top.continew.starter.data.core.util.SqlInjectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
@@ -56,10 +58,9 @@ public class QueryWrapperHelper {
*
* @param query 查询条件
* @param <Q> 查询条件数据类型
* @param <R> 查询数据类型
* @return QueryWrapper
*/
public static <Q, R> QueryWrapper build(Q query) {
public static <Q> QueryWrapper build(Q query) {
QueryWrapper queryWrapper = QueryWrapper.create();
// 没有查询条件直接返回
if (null == query) {
@@ -70,6 +71,34 @@ public class QueryWrapperHelper {
return build(query, fieldList, queryWrapper);
}
/**
* 构建 QueryWrapper
*
* @param query 查询条件
* @param sort 排序条件
* @param <Q> 查询条件数据类型
* @return QueryWrapper
* @since 2.5.2
*/
public static <Q> QueryWrapper build(Q query, Sort sort) {
QueryWrapper queryWrapper = QueryWrapper.create();
// 没有查询条件直接返回
if (null == query) {
return queryWrapper;
}
// 设置排序条件
if (sort != null && sort.isSorted()) {
for (Sort.Order order : sort) {
String field = CharSequenceUtil.toUnderlineCase(order.getProperty());
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
queryWrapper.orderBy(field, order.isAscending());
}
}
// 获取查询条件中所有的字段
List<Field> fieldList = ReflectUtils.getNonStaticFields(query.getClass());
return build(query, fieldList, queryWrapper);
}
/**
* 构建 QueryWrapper
*

View File

@@ -14,7 +14,7 @@
* limitations under the License.
*/
package top.continew.starter.data.mybatis.plus.query;
package top.continew.starter.data.mybatis.plus.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -23,18 +23,17 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.data.domain.Sort;
import top.continew.starter.core.exception.BadRequestException;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.core.annotation.Query;
import top.continew.starter.data.core.annotation.QueryIgnore;
import top.continew.starter.data.core.enums.QueryType;
import top.continew.starter.data.core.util.SqlInjectionUtils;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.*;
import java.util.function.Consumer;
/**
@@ -60,11 +59,33 @@ public class QueryWrapperHelper {
* @return QueryWrapper
*/
public static <Q, R> QueryWrapper<R> build(Q query) {
return build(query, Sort.unsorted());
}
/**
* 构建 QueryWrapper
*
* @param query 查询条件
* @param sort 排序条件
* @param <Q> 查询条件数据类型
* @param <R> 查询数据类型
* @return QueryWrapper
* @since 2.5.2
*/
public static <Q, R> QueryWrapper<R> build(Q query, Sort sort) {
QueryWrapper<R> queryWrapper = new QueryWrapper<>();
// 没有查询条件直接返回
if (null == query) {
return queryWrapper;
}
// 设置排序条件
if (sort != null && sort.isSorted()) {
for (Sort.Order order : sort) {
String field = CharSequenceUtil.toUnderlineCase(order.getProperty());
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
queryWrapper.orderBy(true, order.isAscending(), field);
}
}
// 获取查询条件中所有的字段
List<Field> fieldList = ReflectUtils.getNonStaticFields(query.getClass());
return build(query, fieldList, queryWrapper);

View File

@@ -43,7 +43,7 @@
<properties>
<!-- 项目版本号 -->
<revision>2.5.0</revision>
<revision>2.5.2</revision>
<snail-job.version>1.1.0</snail-job.version>
<sa-token.version>1.38.0</sa-token.version>
<just-auth.version>1.16.6</just-auth.version>

View File

@@ -24,6 +24,12 @@
<artifactId>spring-data-commons</artifactId>
</dependency>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
</dependency>
<!-- 认证模块 - SaToken -->
<dependency>
<groupId>top.continew</groupId>
@@ -36,16 +42,16 @@
</exclusions>
</dependency>
<!-- 数据访问模块 - 核心模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-data-core</artifactId>
</dependency>
<!-- 文件处理模块 - Excel -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-file-excel</artifactId>
</dependency>
<!-- Web 模块 -->
<dependency>
<groupId>top.continew</groupId>
<artifactId>continew-starter-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -21,6 +21,8 @@ import cn.hutool.core.util.ArrayUtil;
import io.swagger.v3.oas.annotations.media.Schema;
import org.springframework.data.domain.Sort;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.core.util.SqlInjectionUtils;
import java.io.Serial;
import java.io.Serializable;
@@ -54,20 +56,17 @@ public class SortQuery implements Serializable {
if (ArrayUtil.isEmpty(sort)) {
return Sort.unsorted();
}
ValidationUtils.throwIf(sort.length < 2, "排序条件非法");
List<Sort.Order> orders = new ArrayList<>(sort.length);
if (CharSequenceUtil.contains(sort[0], StringConstants.COMMA)) {
// e.g "sort=createTime,desc&sort=name,asc"
for (String s : sort) {
List<String> sortList = CharSequenceUtil.splitTrim(s, StringConstants.COMMA);
Sort.Order order = new Sort.Order(Sort.Direction.valueOf(sortList.get(1).toUpperCase()), sortList
.get(0));
orders.add(order);
orders.add(this.getOrder(sortList.get(0), sortList.get(1)));
}
} else {
// e.g "sort=createTime,desc"
Sort.Order order = new Sort.Order(Sort.Direction.valueOf(sort[1].toUpperCase()), sort[0]);
orders.add(order);
orders.add(this.getOrder(sort[0], sort[1]));
}
return Sort.by(orders);
}
@@ -75,4 +74,16 @@ public class SortQuery implements Serializable {
public void setSort(String[] sort) {
this.sort = sort;
}
/**
* 获取排序条件
*
* @param field 字段
* @param direction 排序方向
* @return 排序条件
*/
private Sort.Order getOrder(String field, String direction) {
ValidationUtils.throwIf(SqlInjectionUtils.check(field), "排序字段包含非法字符");
return new Sort.Order(Sort.Direction.valueOf(direction.toUpperCase()), field);
}
}

View File

@@ -0,0 +1,73 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.resp;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 分页信息
*
* @param <T> 列表数据类型
* @author Charles7c
* @since 2.5.2
*/
@Schema(description = "分页信息")
public class BasePageResp<T> implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 列表数据
*/
@Schema(description = "列表数据")
private List<T> list;
/**
* 总记录数
*/
@Schema(description = "总记录数", example = "10")
private long total;
public BasePageResp() {
}
public BasePageResp(final List<T> list, final long total) {
this.list = list;
this.total = total;
}
public List<T> getList() {
return list;
}
public void setList(List<T> list) {
this.list = list;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
}
}

View File

@@ -22,7 +22,6 @@ import com.mybatisflex.core.paginate.Page;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@@ -35,22 +34,17 @@ import java.util.List;
* @since 1.0.0
*/
@Schema(description = "分页信息")
public class PageResp<L> implements Serializable {
public class PageResp<L> extends BasePageResp<L> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 列表数据
*/
@Schema(description = "列表数据")
private List<L> list;
public PageResp() {
}
/**
* 总记录数
*/
@Schema(description = "总记录数", example = "10")
private long total;
public PageResp(final List<L> list, final long total) {
super(list, total);
}
/**
* 基于 MyBatis Plus 分页数据构建分页信息,并将源数据转换为指定类型数据
@@ -65,10 +59,7 @@ public class PageResp<L> implements Serializable {
if (null == page) {
return empty();
}
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(BeanUtil.copyToList(page.getRecords(), targetClass));
pageResp.setTotal(page.getTotalRow());
return pageResp;
return new PageResp<>(BeanUtil.copyToList(page.getRecords(), targetClass), page.getTotalRow());
}
/**
@@ -82,10 +73,7 @@ public class PageResp<L> implements Serializable {
if (null == page) {
return empty();
}
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(page.getRecords());
pageResp.setTotal(page.getTotalRow());
return pageResp;
return new PageResp<>(page.getRecords(), page.getTotalRow());
}
/**
@@ -123,24 +111,6 @@ public class PageResp<L> implements Serializable {
* @return 分页信息
*/
private static <L> PageResp<L> empty() {
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(Collections.emptyList());
return pageResp;
}
public List<L> getList() {
return list;
}
public void setList(List<L> list) {
this.list = list;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
return new PageResp<>(Collections.emptyList(), 0L);
}
}

View File

@@ -20,7 +20,6 @@ import cn.crane4j.core.support.OperateTemplate;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.text.CharSequenceUtil;
@@ -35,7 +34,7 @@ import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.mybatis.flex.base.BaseMapper;
import top.continew.starter.data.mybatis.flex.query.QueryWrapperHelper;
import top.continew.starter.data.mybatis.flex.util.QueryWrapperHelper;
import top.continew.starter.data.mybatis.flex.service.impl.ServiceImpl;
import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.model.query.PageQuery;
@@ -73,6 +72,7 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
@Override
public PageResp<L> page(Q query, PageQuery pageQuery) {
QueryWrapper queryWrapper = this.buildQueryWrapper(query);
this.sort(queryWrapper, pageQuery);
Page<T> page = mapper.paginate(pageQuery.getPage(), pageQuery.getSize(), queryWrapper);
PageResp<L> pageResp = PageResp.build(page, listClass);
pageResp.getList().forEach(this::fill);
@@ -185,12 +185,12 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
* @param sortQuery 排序查询条件
*/
protected void sort(QueryWrapper queryWrapper, SortQuery sortQuery) {
Sort sort = Opt.ofNullable(sortQuery).orElseGet(SortQuery::new).getSort();
if (sortQuery == null || sortQuery.getSort().isUnsorted()) {
return;
}
Sort sort = sortQuery.getSort();
List<Field> entityFields = ReflectUtils.getNonStaticFields(this.entityClass);
for (Sort.Order order : sort) {
if (null == order) {
continue;
}
String property = order.getProperty();
String checkProperty;
// 携带表别名则获取 . 后面的字段名

View File

@@ -1,102 +0,0 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.extension.crud.model.query;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Min;
import org.hibernate.validator.constraints.Range;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.data.domain.Sort;
import java.io.Serial;
/**
* 分页查询条件
*
* @author Charles7c
* @since 1.0.0
*/
@ParameterObject
@Schema(description = "分页查询条件")
public class PageQuery extends SortQuery {
@Serial
private static final long serialVersionUID = 1L;
/**
* 默认页码1
*/
private static final int DEFAULT_PAGE = 1;
/**
* 默认每页条数10
*/
private static final int DEFAULT_SIZE = 10;
/**
* 页码
*/
@Schema(description = "页码", example = "1")
@Min(value = 1, message = "页码最小值为 {value}")
private Integer page = DEFAULT_PAGE;
/**
* 每页条数
*/
@Schema(description = "每页条数", example = "10")
@Range(min = 1, max = 1000, message = "每页条数(取值范围 {min}-{max}")
private Integer size = DEFAULT_SIZE;
/**
* 基于分页查询条件转换为 MyBatis Plus 分页条件
*
* @param <T> 列表数据类型
* @return MyBatis Plus 分页条件
*/
public <T> IPage<T> toPage() {
Page<T> mybatisPage = new Page<>(this.getPage(), this.getSize());
Sort pageSort = this.getSort();
if (CollUtil.isNotEmpty(pageSort)) {
for (Sort.Order order : pageSort) {
OrderItem orderItem = new OrderItem();
orderItem.setAsc(order.isAscending());
orderItem.setColumn(CharSequenceUtil.toUnderlineCase(order.getProperty()));
mybatisPage.addOrder(orderItem);
}
}
return mybatisPage;
}
public Integer getPage() {
return page;
}
public void setPage(Integer page) {
this.page = page;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
}

View File

@@ -22,8 +22,8 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import io.swagger.v3.oas.annotations.media.Schema;
import java.io.Serial;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
@@ -34,22 +34,17 @@ import java.util.List;
* @since 1.0.0
*/
@Schema(description = "分页信息")
public class PageResp<L> implements Serializable {
public class PageResp<L> extends BasePageResp<L> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 列表数据
*/
@Schema(description = "列表数据")
private List<L> list;
public PageResp() {
}
/**
* 总记录数
*/
@Schema(description = "总记录数", example = "10")
private long total;
public PageResp(final List<L> list, final long total) {
super(list, total);
}
/**
* 基于 MyBatis Plus 分页数据构建分页信息,并将源数据转换为指定类型数据
@@ -64,10 +59,7 @@ public class PageResp<L> implements Serializable {
if (null == page) {
return empty();
}
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(BeanUtil.copyToList(page.getRecords(), targetClass));
pageResp.setTotal(page.getTotal());
return pageResp;
return new PageResp<>(BeanUtil.copyToList(page.getRecords(), targetClass), page.getTotal());
}
/**
@@ -81,10 +73,7 @@ public class PageResp<L> implements Serializable {
if (null == page) {
return empty();
}
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(page.getRecords());
pageResp.setTotal(page.getTotal());
return pageResp;
return new PageResp<>(page.getRecords(), page.getTotal());
}
/**
@@ -122,24 +111,6 @@ public class PageResp<L> implements Serializable {
* @return 分页信息
*/
private static <L> PageResp<L> empty() {
PageResp<L> pageResp = new PageResp<>();
pageResp.setList(new ArrayList<>(0));
return pageResp;
}
public List<L> getList() {
return list;
}
public void setList(List<L> list) {
this.list = list;
}
public long getTotal() {
return total;
}
public void setTotal(long total) {
this.total = total;
return new PageResp<>(Collections.emptyList(), 0L);
}
}

View File

@@ -20,15 +20,15 @@ import cn.crane4j.core.support.OperateTemplate;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Opt;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.lang.tree.TreeNodeConfig;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.text.CharSequenceUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.data.domain.Sort;
import org.springframework.transaction.annotation.Transactional;
@@ -38,21 +38,24 @@ import top.continew.starter.core.util.ReflectUtils;
import top.continew.starter.core.util.validate.CheckUtils;
import top.continew.starter.core.util.validate.ValidationUtils;
import top.continew.starter.data.mybatis.plus.base.BaseMapper;
import top.continew.starter.data.mybatis.plus.query.QueryWrapperHelper;
import top.continew.starter.data.mybatis.plus.service.impl.ServiceImpl;
import top.continew.starter.data.mybatis.plus.util.QueryWrapperHelper;
import top.continew.starter.extension.crud.annotation.DictField;
import top.continew.starter.extension.crud.annotation.TreeField;
import top.continew.starter.extension.crud.model.entity.BaseIdDO;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.extension.crud.model.query.SortQuery;
import top.continew.starter.extension.crud.model.resp.LabelValueResp;
import top.continew.starter.extension.crud.model.resp.PageResp;
import top.continew.starter.extension.crud.service.BaseService;
import top.continew.starter.extension.crud.util.TreeUtils;
import top.continew.starter.extension.crud.model.query.PageQuery;
import top.continew.starter.file.excel.util.ExcelUtils;
import java.lang.reflect.Field;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* 业务实现基类
@@ -76,7 +79,8 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
@Override
public PageResp<L> page(Q query, PageQuery pageQuery) {
QueryWrapper<T> queryWrapper = this.buildQueryWrapper(query);
IPage<T> page = baseMapper.selectPage(pageQuery.toPage(), queryWrapper);
this.sort(queryWrapper, pageQuery);
IPage<T> page = baseMapper.selectPage(new Page<>(pageQuery.getPage(), pageQuery.getSize()), queryWrapper);
PageResp<L> pageResp = PageResp.build(page, this.getListClass());
pageResp.getList().forEach(this::fill);
return pageResp;
@@ -253,23 +257,24 @@ public abstract class BaseServiceImpl<M extends BaseMapper<T>, T extends BaseIdD
* @param sortQuery 排序查询条件
*/
protected void sort(QueryWrapper<T> queryWrapper, SortQuery sortQuery) {
Sort sort = Opt.ofNullable(sortQuery).orElseGet(SortQuery::new).getSort();
if (sortQuery == null || sortQuery.getSort().isUnsorted()) {
return;
}
Sort sort = sortQuery.getSort();
for (Sort.Order order : sort) {
if (null != order) {
String property = order.getProperty();
String checkProperty;
// 携带表别名则获取 . 后面的字段名
if (property.contains(StringConstants.DOT)) {
checkProperty = CollUtil.getLast(CharSequenceUtil.split(property, StringConstants.DOT));
} else {
checkProperty = property;
}
Optional<Field> optional = super.getEntityFields().stream()
.filter(field -> checkProperty.equals(field.getName()))
.findFirst();
ValidationUtils.throwIf(optional.isEmpty(), "无效的排序字段 [{}]", property);
queryWrapper.orderBy(true, order.isAscending(), CharSequenceUtil.toUnderlineCase(property));
String property = order.getProperty();
String checkProperty;
// 携带表别名则获取 . 后面的字段名
if (property.contains(StringConstants.DOT)) {
checkProperty = CollUtil.getLast(CharSequenceUtil.split(property, StringConstants.DOT));
} else {
checkProperty = property;
}
Optional<Field> optional = super.getEntityFields().stream()
.filter(field -> checkProperty.equals(field.getName()))
.findFirst();
ValidationUtils.throwIf(optional.isEmpty(), "无效的排序字段 [{}]", property);
queryWrapper.orderBy(true, order.isAscending(), CharSequenceUtil.toUnderlineCase(property));
}
}

View File

@@ -22,7 +22,9 @@ import cn.hutool.core.util.ReflectUtil;
import cn.hutool.extra.spring.SpringUtil;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.exception.BusinessException;
import top.continew.starter.security.crypto.annotation.FieldEncrypt;
@@ -46,39 +48,6 @@ public abstract class AbstractMyBatisInterceptor implements Interceptor {
private static final Map<String, Map<String, FieldEncrypt>> ENCRYPT_PARAM_CACHE = new ConcurrentHashMap<>();
/**
* 获取加密参数
*
* @param mappedStatementId 映射语句 ID
* @return 加密参数
*/
public Map<String, FieldEncrypt> getEncryptParams(String mappedStatementId) {
return getEncryptParams(mappedStatementId, null);
}
/**
* 获取加密参数
*
* @param mappedStatementId 映射语句 ID
* @param parameterIndex 参数索引
* @return 加密参数
*/
public Map<String, FieldEncrypt> getEncryptParams(String mappedStatementId, Integer parameterIndex) {
return ENCRYPT_PARAM_CACHE
.computeIfAbsent(mappedStatementId, key -> getEncryptParamsNoCached(mappedStatementId, parameterIndex));
}
/**
* 获取参数名称
*
* @param parameter 参数
* @return 参数名称
*/
public String getParameterName(Parameter parameter) {
Param param = parameter.getAnnotation(Param.class);
return null != param ? param.value() : parameter.getName();
}
/**
* 获取所有字符串类型、需要加/解密的、有值字段
*
@@ -114,13 +83,67 @@ public abstract class AbstractMyBatisInterceptor implements Interceptor {
}
/**
* 获取参数列表(无缓存)
* 获取加密参数
*
* @param mappedStatement 映射语句
* @return 加密参数
*/
public Map<String, FieldEncrypt> getEncryptParams(MappedStatement mappedStatement) {
return getEncryptParams(mappedStatement, null);
}
/**
* 获取加密参数
*
* @param mappedStatement 映射语句
* @param parameterCount 参数数量
* @return 加密参数
*/
public Map<String, FieldEncrypt> getEncryptParams(MappedStatement mappedStatement, Integer parameterCount) {
String mappedStatementId = mappedStatement.getId();
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
if (SqlCommandType.UPDATE != sqlCommandType) {
return ENCRYPT_PARAM_CACHE.computeIfAbsent(mappedStatementId, key -> this
.getEncryptParams(mappedStatementId, parameterCount));
} else {
return this.getEncryptParams(mappedStatementId, parameterCount);
}
}
/**
* 获取参数名称
*
* @param parameter 参数
* @return 参数名称
*/
public String getParameterName(Parameter parameter) {
Param param = parameter.getAnnotation(Param.class);
return null != param ? param.value() : parameter.getName();
}
/**
* 获取加密参数列表
*
* @param mappedStatementId 映射语句 ID
* @param parameterIndex 参数数量
* @return 参数列表
* @param parameterCount 参数数量
* @return 加密参数列表
*/
private Map<String, FieldEncrypt> getEncryptParamsNoCached(String mappedStatementId, Integer parameterIndex) {
private Map<String, FieldEncrypt> getEncryptParams(String mappedStatementId, Integer parameterCount) {
Method method = this.getMethod(mappedStatementId, parameterCount);
if (method == null) {
return Collections.emptyMap();
}
return this.getEncryptParams(method);
}
/**
* 获取映射方法
*
* @param mappedStatementId 映射语句 ID
* @param parameterCount 参数数量
* @return 映射方法
*/
private Method getMethod(String mappedStatementId, Integer parameterCount) {
try {
String className = CharSequenceUtil.subBefore(mappedStatementId, StringConstants.DOT, true);
String wrapperMethodName = CharSequenceUtil.subAfter(mappedStatementId, StringConstants.DOT, true);
@@ -131,35 +154,42 @@ public abstract class AbstractMyBatisInterceptor implements Interceptor {
.orElse(wrapperMethodName);
// 获取真实方法
Optional<Method> methodOptional = Arrays.stream(ReflectUtil.getMethods(Class.forName(className), m -> {
if (Objects.nonNull(parameterIndex)) {
return Objects.equals(m.getName(), methodName) && m.getParameterCount() == parameterIndex;
if (parameterCount != null) {
return Objects.equals(m.getName(), methodName) && m.getParameterCount() == parameterCount;
}
return Objects.equals(m.getName(), methodName);
})).findFirst();
if (methodOptional.isEmpty()) {
return Collections.emptyMap();
}
// 获取方法中的加密参数
Map<String, FieldEncrypt> map = MapUtil.newHashMap();
Parameter[] parameterArr = methodOptional.get().getParameters();
for (int i = 0; i < parameterArr.length; i++) {
Parameter parameter = parameterArr[i];
String parameterName = this.getParameterName(parameter);
FieldEncrypt fieldEncrypt = parameter.getAnnotation(FieldEncrypt.class);
if (null != fieldEncrypt) {
map.put(parameterName, fieldEncrypt);
if (String.class.equals(parameter.getType())) {
map.put("param" + (i + 1), fieldEncrypt);
}
} else if (parameterName.startsWith(Constants.ENTITY)) {
map.put(parameterName, null);
} else if (parameterName.startsWith(Constants.WRAPPER)) {
map.put(parameterName, null);
}
}
return map;
return methodOptional.orElse(null);
} catch (ClassNotFoundException e) {
throw new BusinessException(e.getMessage());
}
}
/**
* 获取加密参数列表
*
* @param method 方法
* @return 加密参数列表
*/
private Map<String, FieldEncrypt> getEncryptParams(Method method) {
// 获取方法中的加密参数
Map<String, FieldEncrypt> map = MapUtil.newHashMap();
Parameter[] parameterArr = method.getParameters();
for (int i = 0; i < parameterArr.length; i++) {
Parameter parameter = parameterArr[i];
String parameterName = this.getParameterName(parameter);
FieldEncrypt fieldEncrypt = parameter.getAnnotation(FieldEncrypt.class);
if (null != fieldEncrypt) {
map.put(parameterName, fieldEncrypt);
if (String.class.equals(parameter.getType())) {
map.put("param" + (i + 1), fieldEncrypt);
}
} else if (parameterName.startsWith(Constants.ENTITY)) {
map.put(parameterName, null);
} else if (parameterName.startsWith(Constants.WRAPPER)) {
map.put(parameterName, null);
}
}
return map;
}
}

View File

@@ -109,8 +109,10 @@ public class MyBatisEncryptInterceptor extends AbstractMyBatisInterceptor {
* @throws Exception /
*/
private void encryptMap(HashMap<String, Object> parameterMap, MappedStatement mappedStatement) throws Exception {
Map<String, FieldEncrypt> encryptParamMap = super.getEncryptParams(mappedStatement.getId(), parameterMap
.isEmpty() ? null : parameterMap.size() / 2);
Map<String, FieldEncrypt> encryptParamMap = super.getEncryptParams(mappedStatement);
if (encryptParamMap.isEmpty() && !parameterMap.isEmpty()) {
encryptParamMap = super.getEncryptParams(mappedStatement, parameterMap.size() / 2);
}
for (Map.Entry<String, FieldEncrypt> encryptParamEntry : encryptParamMap.entrySet()) {
String parameterName = encryptParamEntry.getKey();
if (parameterName.startsWith(Constants.ENTITY)) {

View File

@@ -0,0 +1,65 @@
/*
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <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.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package top.continew.starter.web.autoconfigure.response;
import org.apache.commons.lang3.reflect.TypeUtils;
import org.springdoc.core.parsers.ReturnTypeParser;
import org.springframework.core.MethodParameter;
import top.continew.starter.apidoc.util.DocUtils;
import top.continew.starter.web.model.R;
import java.lang.reflect.Type;
/**
* SpringDoc 全局响应处理器
* <p>
* 接口文档全局添加响应格式 {@link R}
* </p>
*
* @author echo
* @since 2.5.2
*/
public class ApiDocGlobalResponseHandler implements ReturnTypeParser {
private static final Class<R> R_TYPE = R.class;
/**
* 获取返回类型
*
* @param methodParameter 方法参数
* @return {@link Type }
*/
@Override
public Type getReturnType(MethodParameter methodParameter) {
// 获取返回类型
Type returnType = ReturnTypeParser.super.getReturnType(methodParameter);
// 判断是否具有 RestController 注解
if (!DocUtils.hasRestControllerAnnotation(methodParameter.getContainingClass())) {
return returnType;
}
// 如果为 R<T> 则直接返回
if (returnType.getTypeName().contains("top.continew.starter.web.model.R")) {
return returnType;
}
// 如果是 void类型则返回 R<Void>
if (returnType == void.class || returnType == Void.class) {
return TypeUtils.parameterize(R_TYPE, Void.class);
}
// 返回 R<T>
return TypeUtils.parameterize(R_TYPE, returnType);
}
}

View File

@@ -143,6 +143,17 @@ public class GlobalResponseAutoConfiguration {
return messageSource;
}
/**
* SpringDoc 全局响应处理器
*
* @return {@link ApiDocGlobalResponseHandler }
*/
@Bean
@ConditionalOnMissingBean
public ApiDocGlobalResponseHandler apiDocGlobalResponseHandler() {
return new ApiDocGlobalResponseHandler();
}
@PostConstruct
public void postConstruct() {
log.debug("[ContiNew Starter] - Auto Configuration 'Web-Global Response' completed initialization.");

View File

@@ -23,8 +23,6 @@ import com.feiniaojin.gracefulresponse.data.ResponseStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import top.continew.starter.web.autoconfigure.response.GlobalResponseProperties;
import java.util.Collections;
/**
* 响应信息
*
@@ -32,7 +30,7 @@ import java.util.Collections;
* @since 1.0.0
*/
@Schema(description = "响应信息")
public class R implements Response {
public class R<T> implements Response {
private static final GlobalResponseProperties PROPERTIES = SpringUtil.getBean(GlobalResponseProperties.class);
private static final String DEFAULT_SUCCESS_CODE = PROPERTIES.getDefaultSuccessCode();
@@ -49,7 +47,7 @@ public class R implements Response {
/**
* 状态信息
*/
@Schema(description = "状态信息", example = "操作成功")
@Schema(description = "状态信息", example = "ok")
private String msg;
/**
@@ -68,7 +66,7 @@ public class R implements Response {
* 响应数据
*/
@Schema(description = "响应数据")
private Object data = Collections.emptyMap();
private T data;
public R() {
}
@@ -78,7 +76,7 @@ public class R implements Response {
this.setMsg(msg);
}
public R(String code, String msg, Object data) {
public R(String code, String msg, T data) {
this(code, msg);
this.data = data;
}
@@ -97,7 +95,7 @@ public class R implements Response {
@Override
public void setPayload(Object payload) {
this.data = payload;
this.data = (T)payload;
}
@Override
@@ -123,11 +121,11 @@ public class R implements Response {
this.msg = msg;
}
public Object getData() {
public T getData() {
return data;
}
public void setData(Object data) {
public void setData(T data) {
this.data = data;
}