添加了sql解析器相关功能

This commit is contained in:
root 2026-04-04 10:56:57 +08:00
parent 905847a2d5
commit addb2b671d
4 changed files with 1021 additions and 0 deletions

5
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,5 @@
{
"editor.codeLens": true,
"java.test.editor.enableShortcuts": true,
"testing.gutterEnabled": true
}

View File

@ -0,0 +1,262 @@
package com.yfd.platform.common;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
/**
* 通用动态 SQL Mapper
* <p>
* 说明
* <p>
* 1. 该接口通过注解 SQL 直接执行动态查询适用于表名/联表 SQL/查询列需要运行期决定的场景<br>
* 2. {@code QueryWrapper} 的方法统一使用 {@code ew.customSqlSegment} 追加条件<br>
* 3. 传入的 {@code sql}/{@code select}/{@code tableName} 属于动态片段请在上层确保来源可信避免注入风险
*/
@Mapper
public interface DynamicSQLMapper<T> {
/**
* 分页执行完整 SQLSQL 内可使用 map 参数
*/
@Select({"<script>", "${sql}", "</script>"})
List<Map<String, Object>> pageAllList(Page<?> page,
@Param("sql") String sql,
@Param("map") Map<String, Object> map);
/**
* 分页执行完整 SQL泛型结果依赖 MyBatis 映射规则
*/
@Select({"<script>", "${sql}", "</script>"})
<R> List<R> pageAllListWithResultType(Page<?> page,
@Param("sql") String sql,
@Param("map") Map<String, Object> map,
@Param("resultType") Class<R> resultType);
/**
* 不分页执行完整 SQL
*/
@Select({"<script>", "${sql}", "</script>"})
List<Map<String, Object>> getAllList(@Param("sql") String sql,
@Param("map") Map<String, Object> map);
/**
* 不分页执行完整 SQL泛型结果依赖 MyBatis 映射规则
*/
@Select({"<script>", "${sql}", "</script>"})
<R> List<R> getAllListWithResultType(@Param("sql") String sql,
@Param("map") Map<String, Object> map,
@Param("resultType") Class<R> resultType);
/**
* 统计条数含动态条件
*/
@Select({
"<script>",
"select count(1) count from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
Integer count(@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 统计条数无动态条件
*/
@Select({"<script>", "select count(1) count from ${sql}", "</script>"})
Integer countNoWrapper(@Param("sql") String sql);
/**
* 分页查询含动态条件
*/
@Select({
"<script>",
"select ${select} from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
List<Map<String, Object>> pageList(Page<?> page,
@Param("select") String select,
@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 分页查询含动态条件泛型结果
*/
@Select({
"<script>",
"select ${select} from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
<R> List<R> pageListWithResultType(Page<?> page,
@Param("select") String select,
@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper,
@Param("resultType") Class<R> resultType);
/**
* 分页查询条件可选方法名保持兼容
*/
@Select({
"<script>",
"select ${select} from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
List<Map<String, Object>> pageNoFilterList(Page<?> page,
@Param("select") String select,
@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 分页查询无动态条件
*/
@Select({"<script>", "select ${select} from ${sql}", "</script>"})
List<Map<String, Object>> pageListNoWrapper(Page<?> page,
@Param("select") String select,
@Param("sql") String sql);
/**
* 非分页查询含动态条件
*/
@Select({
"<script>",
"select ${select} from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
List<Map<String, Object>> getList(@Param("select") String select,
@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 非分页查询含动态条件历史命名保留返回 Map 结构
*/
@Select({
"<script>",
"select ${select} from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
List<Map<String, Object>> getListWithResultType(@Param("select") String select,
@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 单表条件查询直接传 where 条件片段
*/
@Select({"<script>", "select ${select} from ${table} where ${condition}", "</script>"})
List<Map<String, Object>> getSingleTableList(@Param("select") String select,
@Param("table") String table,
@Param("condition") String condition);
/**
* 非分页查询条件可选方法名保持兼容
*/
@Select({
"<script>",
"select ${select} from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
List<Map<String, Object>> getNoFilterList(@Param("select") String select,
@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 非分页查询无动态条件
*/
@Select({"<script>", "select ${select} from ${sql}", "</script>"})
List<Map<String, Object>> getListNoWrapper(@Param("select") String select,
@Param("sql") String sql);
/**
* 汇总查询含动态条件返回单行汇总结果
*/
@Select({
"<script>",
"select ${select} from ${sql}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
Map<String, Object> totalSummary(@Param("select") String select,
@Param("sql") String sql,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 汇总查询无动态条件
*/
@Select({"<script>", "select ${select} from ${sql}", "</script>"})
Map<String, Object> totalSummaryNoWrapper(@Param("select") String select,
@Param("sql") String sql);
/**
* 单表汇总查询含动态条件
*/
@Select({
"<script>",
"select ${select} from ${tableName}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
Map<String, Object> totalSummarySingleTable(@Param("select") String select,
@Param("tableName") String tableName,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 单表汇总查询无动态条件
*/
@Select({"<script>", "select ${select} from ${tableName}", "</script>"})
Map<String, Object> totalSummaryNoWrapperSingleTable(@Param("select") String select,
@Param("tableName") String tableName);
/**
* 获取表中最大排序值
*/
@Select({"<script>", "select max(order_index) from ${tableName}", "</script>"})
Integer getOrderIndexMax(@Param("tableName") String tableName);
/**
* 按条件获取表中最大排序值
*/
@Select({
"<script>",
"select max(order_index) from ${tableName}",
"<if test='ew != null and ew.customSqlSegment != null and ew.customSqlSegment != \"\"'>",
"${ew.customSqlSegment}",
"</if>",
"</script>"
})
Integer getOrderIndexMaxByParentId(@Param("tableName") String tableName,
@Param("ew") QueryWrapper<?> queryWrapper);
/**
* 获取分组字段列表
*/
@Select({"<script>", "select ${info} from ${joinsql} group by ${info}", "</script>"})
List<Object> getGroup(@Param("info") String info, @Param("joinsql") String joinsql);
}

View File

@ -0,0 +1,634 @@
package com.yfd.platform.utils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* 动态查询/动态 SQL 片段包装工具
* <p>
* 主要用途
* <p>
* 1) 将前端 DevExtreme/Kendo 常见的 filter数组嵌套 + and/or解析为 MyBatis-Plus {@link QueryWrapper} 条件<br>
* 2) skip/take 转为 MyBatis-Plus {@link Page}<br>
* 3) 根据实体元数据把 Java 属性名映射为数据库列名{@link TableInfoHelper}<br>
* 4) 生成 group by / order by 片段<br>
* 5) 反射提取对象字段值用于调试/通用映射场景
* <p>
* 安全说明
* <p>
* - 本类只对字段名/列名做白名单校验{@link #SAFE_IDENTIFIER}以避免把不安全字符串拼接进 SQL<br>
* - 条件值value通过 MyBatis-Plus 参数化方式参与查询不做字符串拼接
*/
public class QueryWrapperUtil {
/**
* Jackson JSON 解析器用于把 DevExtreme/Kendo 风格的 filter 转为可遍历的 JsonNode
*/
private static final ObjectMapper MAPPER = new ObjectMapper();
/**
* SQL 标识符白名单仅允许字母/数字/下划线且支持 a.b 这种带前缀写法
* 用于防止把不安全字符串拼进 SQL字段名/列名场景
*/
private static final Pattern SAFE_IDENTIFIER = Pattern.compile("^[A-Za-z_][A-Za-z0-9_]*(\\.[A-Za-z_][A-Za-z0-9_]*)*$");
/**
* 针对前端传 null但后端需要特定默认值的字段列表历史兼容
*/
public static final List<String> FIELDNULL = new ArrayList<>();
/**
* 获取某些字段在 JSON 过滤条件为 null 时的替代值
* 例如某些 GUID 字段在前端传 null需要用全 0 GUID 参与过滤
*/
public static String getJsonFieldNull(String columnName) {
return FIELDNULL.contains(columnName) ? "00000000-0000-0000-0000-000000000000" : null;
}
/**
* DevExtreme 常用的分页参数 skip/take 转为 MyBatis-Plus Page
* current 1 开始current = skip / take + 1
*/
public static Page<?> getPage(Integer skip, Integer take) {
if (take == null || take <= 0) {
return null;
}
int s = skip == null ? 0 : Math.max(0, skip);
Page<Object> page = new Page<>();
page.setSize(take.longValue());
page.setCurrent(s / take + 1L);
return page;
}
/**
* 使用 {@link DataSourceRequest}take/skip构造分页对象
*/
public static Page<?> getPage(DataSourceRequest dataSourceRequest) {
if (dataSourceRequest == null) {
return null;
}
return getPage(dataSourceRequest.getSkip(), dataSourceRequest.getTake());
}
/**
* 根据分组描述拼接 SQL group by / order by 片段
* <p>
* 返回值示例{@code " group by dept_id, role_id order by dept_id asc, role_id desc"}
*
* @param dataSourceRequest 仅使用其 group 字段
* @return 可直接拼接到 SQL 末尾的片段无分组返回 null
*/
public static String getGroupBy(DataSourceRequest dataSourceRequest) {
if (dataSourceRequest == null) {
return null;
}
List<GroupDescriptor> groupDescriptorList = dataSourceRequest.getGroup();
if (groupDescriptorList == null || groupDescriptorList.isEmpty()) {
return null;
}
List<String> groupByParts = new ArrayList<>();
List<String> orderByParts = new ArrayList<>();
for (GroupDescriptor groupingInfo : groupDescriptorList) {
if (groupingInfo == null) {
continue;
}
String selector = requireSafeIdentifier(groupingInfo.getField());
groupByParts.add(selector);
if (groupingInfo.isNeedSortFlag()) {
String dir = groupingInfo.getDir();
if ("desc".equalsIgnoreCase(dir)) {
orderByParts.add(selector + " desc");
} else {
orderByParts.add(selector + " asc");
}
}
}
if (groupByParts.isEmpty()) {
return null;
}
StringBuilder groupResult = new StringBuilder();
groupResult.append(" group by ").append(String.join(", ", groupByParts));
if (!orderByParts.isEmpty()) {
groupResult.append(" order by ").append(String.join(", ", orderByParts));
}
return groupResult.toString();
}
/**
* 通过反射提取对象字段名与字段值组装为 Map
* <p>
* - 传入实例对象返回所有字段包含父类字段的值<br>
* - 传入 Class仅返回 static 字段的值
*
* @param obj 实例对象或 Class
* @return 字段名-字段值无字段或入参为空时返回 null
*/
public static Map<String, Object> getFieldValues(Object obj) {
if (obj == null) {
return null;
}
Class<?> type = (obj instanceof Class<?> clazz) ? clazz : obj.getClass();
List<Field> fields = getAllFields(type);
if (fields.isEmpty()) {
return null;
}
Map<String, Object> valueMap = new HashMap<>();
boolean isClassObject = obj instanceof Class<?>;
for (Field field : fields) {
field.setAccessible(true);
Object value;
try {
if (isClassObject) {
if (!Modifier.isStatic(field.getModifiers())) {
continue;
}
value = field.get(null);
} else {
value = field.get(obj);
}
} catch (IllegalAccessException e) {
continue;
}
valueMap.put(field.getName(), value);
}
return valueMap.isEmpty() ? null : valueMap;
}
/**
* DevExtreme/Kendo 风格 filter 中提取指定字段的过滤值多个值用逗号拼接
* <p>
* filter 形态通常是数组嵌套
* ["name","contains","abc"] [ ["a","=","1"], "and", ["b","=","2"] ]
*/
public static String getFilterFieldValue(Object filter, String fieldName) {
if (filter == null || fieldName == null || fieldName.isBlank()) {
return null;
}
JsonNode root = toJsonNode(filter);
if (root == null || root.isNull()) {
return null;
}
StringBuilder sb = new StringBuilder();
collectFieldValues(root, fieldName, sb);
return sb.isEmpty() ? null : sb.toString();
}
/**
* 便捷方法 filter 构建一个新的 QueryWrapper
*
* @param filter 前端 filter可传 JSON 字符串 / List / JsonNode / 任意对象
* @param modelClass MyBatis-Plus 实体类用于 property->column 映射
* @param validateColumn 是否校验字段必须存在于实体true 更安全
*/
public static <T> QueryWrapper<T> buildWrapperFromDevExtremeFilter(Object filter, Class<T> modelClass, boolean validateColumn) {
QueryWrapper<T> wrapper = new QueryWrapper<>();
return applyDevExtremeFilter(wrapper, filter, modelClass, validateColumn, null, null);
}
/**
* DevExtreme/Kendo 风格 filter 应用到已有的 QueryWrapper
*
* @param fieldsMap 允许自定义字段映射前端字段->数据库列名优先级高于实体映射
* @param removeFields 字段黑名单前端传了也忽略
*/
public static <T> QueryWrapper<T> applyDevExtremeFilter(
QueryWrapper<T> wrapper,
Object filter,
Class<T> modelClass,
boolean validateColumn,
Map<String, String> fieldsMap,
List<String> removeFields
) {
if (wrapper == null || filter == null) {
return wrapper;
}
JsonNode root = toJsonNode(filter);
if (root == null || root.isNull()) {
return wrapper;
}
Set<String> remove = removeFields == null ? Collections.emptySet() : new HashSet<>(removeFields);
Map<String, String> mapped = fieldsMap == null ? Collections.emptyMap() : new HashMap<>(fieldsMap);
applyFilterNode(root, wrapper, mapped, remove, modelClass, validateColumn);
return wrapper;
}
/**
* 根据实体属性名获取数据库列名默认要求属性必须存在于实体元数据中
*/
public static String getDBColumnName(Class<?> modelClass, String property) {
return getDBColumnName(modelClass, property, true);
}
/**
* 根据 MyBatis-Plus 元数据把实体属性名映射为数据库列名
* <p>
* validateColumn=false 找不到映射会直接返回传入值同时做一次安全校验
*/
public static String getDBColumnName(Class<?> modelClass, String property, boolean validateColumn) {
if (modelClass == null) {
throw new IllegalArgumentException("modelClass is null");
}
if (property == null || property.isBlank()) {
throw new IllegalArgumentException("property is blank");
}
TableInfo tableInfo = TableInfoHelper.getTableInfo(modelClass);
if (tableInfo == null) {
if (validateColumn) {
throw new IllegalArgumentException("TableInfo not found for " + modelClass.getName());
}
return requireSafeIdentifier(property);
}
if (property.equals(tableInfo.getKeyProperty())) {
return tableInfo.getKeyColumn();
}
List<TableFieldInfo> fieldInfos = tableInfo.getFieldList().stream()
.filter(f -> property.equals(f.getProperty()))
.collect(Collectors.toList());
if (!fieldInfos.isEmpty()) {
return fieldInfos.get(0).getColumn();
}
if (validateColumn) {
throw new IllegalArgumentException(property + "列找不到");
}
return requireSafeIdentifier(property);
}
/**
* 获取实体对应的数据库表名来自 MyBatis-Plus 元数据
*/
public static String getDBTableName(Class<?> modelClass) {
if (modelClass == null) {
throw new IllegalArgumentException("modelClass is null");
}
TableInfo tableInfo = TableInfoHelper.getTableInfo(modelClass);
if (tableInfo == null) {
throw new IllegalArgumentException("TableInfo not found for " + modelClass.getName());
}
return tableInfo.getTableName();
}
/**
* 驼峰转分隔符小写myFieldName -> my_field_namehyphenation 可传 "_" "-"
*/
public static String toHyphenation(String src, String hyphenation) {
if (src == null || src.isEmpty()) {
return src;
}
String h = hyphenation == null ? "" : hyphenation;
StringBuilder sb = new StringBuilder(src);
int cnt = 0;
for (int i = 1; i < src.length(); i++) {
if (Character.isUpperCase(src.charAt(i))) {
sb.insert(i + cnt, h);
cnt += h.length();
}
}
return sb.toString().toLowerCase(Locale.ROOT);
}
/**
* 驼峰转下划线
*/
public static String toUnderline(String src) {
return toHyphenation(src, "_");
}
/**
* 把不同类型的 filter 入参转成 JsonNode便于递归解析
*/
private static JsonNode toJsonNode(Object filter) {
try {
if (filter instanceof JsonNode n) {
return n;
}
if (filter instanceof String s) {
if (s.isBlank()) {
return null;
}
return MAPPER.readTree(s);
}
return MAPPER.valueToTree(filter);
} catch (Exception e) {
return null;
}
}
/**
* 递归遍历 filter提取目标字段的值多个值逗号拼接
*/
private static void collectFieldValues(JsonNode node, String fieldName, StringBuilder sb) {
if (node == null || node.isNull()) {
return;
}
if (node.isArray()) {
if (node.size() >= 3 && node.get(0).isTextual()) {
String field = node.get(0).asText();
if (fieldName.equals(field)) {
JsonNode valueNode = node.get(2);
String value = valueNode == null || valueNode.isNull() ? getJsonFieldNull(field) : valueNode.asText();
if (value != null && !value.isBlank()) {
if (!sb.isEmpty()) {
sb.append(',');
}
sb.append(value);
}
}
return;
}
for (int i = 0; i < node.size(); i++) {
collectFieldValues(node.get(i), fieldName, sb);
}
}
}
/**
* filterJsonNode 数组结构递归解析并应用到 QueryWrapper
* - 单条件["field","=",value]
* - 组合条件[ cond1, "and"/"or", cond2, ... ]
*/
private static <T> void applyFilterNode(
JsonNode node,
QueryWrapper<T> wrapper,
Map<String, String> fieldsMap,
Set<String> removeFields,
Class<?> modelClass,
boolean validateColumn
) {
if (node == null || node.isNull()) {
return;
}
if (!node.isArray()) {
return;
}
// 形如["field","=",value]
if (node.size() >= 3 && node.get(0).isTextual()) {
applySingleCondition(node, wrapper, fieldsMap, removeFields, modelClass, validateColumn);
return;
}
// 形如[ [ ... ] ]拆一层继续解析
if (node.size() == 1) {
applyFilterNode(node.get(0), wrapper, fieldsMap, removeFields, modelClass, validateColumn);
return;
}
// 先解析第 0 个条件然后按 and/or 递归追加后续条件
applyFilterNode(node.get(0), wrapper, fieldsMap, removeFields, modelClass, validateColumn);
for (int i = 2; i < node.size(); i += 2) {
JsonNode opNode = node.get(i - 1);
JsonNode exprNode = node.get(i);
String op = opNode == null ? null : opNode.asText();
if ("and".equalsIgnoreCase(op)) {
wrapper.and(w -> applyFilterNode(exprNode, w, fieldsMap, removeFields, modelClass, validateColumn));
} else if ("or".equalsIgnoreCase(op)) {
wrapper.or(w -> applyFilterNode(exprNode, w, fieldsMap, removeFields, modelClass, validateColumn));
}
}
}
/**
* 处理单个条件["field","operator",value] 并映射到 MyBatis-Plus 条件方法
*/
private static <T> void applySingleCondition(
JsonNode conditionNode,
QueryWrapper<T> wrapper,
Map<String, String> fieldsMap,
Set<String> removeFields,
Class<?> modelClass,
boolean validateColumn
) {
String field = conditionNode.get(0).asText();
String operator = conditionNode.get(1).asText();
JsonNode valueNode = conditionNode.get(2);
if (removeFields.contains(field)) {
return;
}
// 列名优先从 fieldsMap 其次从实体元数据映射并做一次安全校验
String databaseColumnName = fieldsMap.get(field);
if (databaseColumnName == null) {
databaseColumnName = getDBColumnName(modelClass, field, validateColumn);
} else {
databaseColumnName = requireSafeIdentifier(databaseColumnName);
}
Object value = convertJsonValue(valueNode, field);
// operator 统一为小写兼容前端大小写差异
String op = operator == null ? "" : operator.trim().toLowerCase(Locale.ROOT);
switch (op) {
case "contains" -> wrapper.like(databaseColumnName, value);
case "notcontains" -> wrapper.notLike(databaseColumnName, value);
case "startswith" -> wrapper.likeRight(databaseColumnName, value);
case "endswith" -> wrapper.likeLeft(databaseColumnName, value);
case "=" -> {
if (value == null) {
wrapper.isNull(databaseColumnName);
} else {
wrapper.eq(databaseColumnName, value);
}
}
case "!=",
"<>" -> {
if (value == null) {
wrapper.isNotNull(databaseColumnName);
} else {
wrapper.ne(databaseColumnName, value);
}
}
case "<" -> wrapper.lt(databaseColumnName, value);
case "<=" -> wrapper.le(databaseColumnName, value);
case ">" -> wrapper.gt(databaseColumnName, value);
case ">=" -> wrapper.ge(databaseColumnName, value);
}
}
/**
* JsonNode 转成 Java Boolean/Number/String/数组对象字符串化
* null 时按 getJsonFieldNull 做兼容替换
*/
private static Object convertJsonValue(JsonNode valueNode, String columnName) {
if (valueNode == null || valueNode.isNull()) {
return getJsonFieldNull(columnName);
}
if (valueNode.isBoolean()) {
return valueNode.asBoolean();
}
if (valueNode.isNumber()) {
return valueNode.numberValue();
}
if (valueNode.isTextual()) {
return valueNode.asText();
}
if (valueNode.isArray() || valueNode.isObject()) {
return valueNode.toString();
}
return valueNode.asText();
}
/**
* 校验列名/字段名为安全标识符避免拼接 SQL 注入
*/
private static String requireSafeIdentifier(String identifier) {
String id = identifier == null ? "" : identifier.trim();
if (id.isEmpty() || !SAFE_IDENTIFIER.matcher(id).matches()) {
throw new IllegalArgumentException("Unsafe SQL identifier: " + identifier);
}
return id;
}
/**
* 获取类及其父类直到 Object的所有声明字段
*/
private static List<Field> getAllFields(Class<?> type) {
if (type == null) {
return Collections.emptyList();
}
List<Field> result = new ArrayList<>();
Class<?> current = type;
while (current != null && current != Object.class) {
Field[] declared = current.getDeclaredFields();
if (declared != null) {
Collections.addAll(result, declared);
}
current = current.getSuperclass();
}
return result;
}
/**
* 通用请求 DTO用于承载分页与分组信息
* <p>
* 说明 DTO 仅用于内部/工具层做参数承载不绑定特定前端框架
*/
public static class DataSourceRequest {
/**
* 每页条数take
*/
private int take;
/**
* 跳过条数skip
*/
private int skip;
/**
* 分组描述列表用于生成 group by/order by
*/
private List<GroupDescriptor> group;
public DataSourceRequest() {
}
public DataSourceRequest(int take, int skip, List<GroupDescriptor> group) {
this.take = take;
this.skip = skip;
this.group = group;
}
public int getTake() {
return take;
}
public void setTake(int take) {
this.take = take;
}
public int getSkip() {
return skip;
}
public void setSkip(int skip) {
this.skip = skip;
}
public List<GroupDescriptor> getGroup() {
return group;
}
public void setGroup(List<GroupDescriptor> group) {
this.group = group;
}
}
/**
* 分组描述 DTO字段名 + 排序方向 + 是否需要排序
*/
public static class GroupDescriptor {
/**
* 分组字段名建议仅传安全标识符dept_id / t.dept_id
*/
private String field;
/**
* 排序方向asc/desc忽略大小写
*/
private String dir;
/**
* 是否需要把该分组字段加入 order by
*/
private boolean needSortFlag;
public GroupDescriptor() {
}
public GroupDescriptor(String field, String dir, boolean needSortFlag) {
this.field = field;
this.dir = dir;
this.needSortFlag = needSortFlag;
}
public String getField() {
return field;
}
public void setField(String field) {
this.field = field;
}
public String getDir() {
return dir;
}
public void setDir(String dir) {
this.dir = dir;
}
public boolean isNeedSortFlag() {
return needSortFlag;
}
public void setNeedSortFlag(boolean needSortFlag) {
this.needSortFlag = needSortFlag;
}
}
static {
// 历史兼容parentId null 时用全 0 GUID 参与过滤/比较
FIELDNULL.add("parentId");
}
}

View File

@ -0,0 +1,120 @@
package com.yfd.platform.utils;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class QueryWrapperUtilTest {
@Test
void getPage_shouldBuildPageBySkipAndTake() {
Page<?> page = QueryWrapperUtil.getPage(20, 10);
Assertions.assertNotNull(page);
Assertions.assertEquals(10L, page.getSize());
Assertions.assertEquals(3L, page.getCurrent());
}
@Test
void getPage_shouldBuildPageFromDataSourceRequest() {
QueryWrapperUtil.DataSourceRequest req = new QueryWrapperUtil.DataSourceRequest();
req.setSkip(0);
req.setTake(15);
Page<?> page = QueryWrapperUtil.getPage(req);
Assertions.assertNotNull(page);
Assertions.assertEquals(15L, page.getSize());
Assertions.assertEquals(1L, page.getCurrent());
}
@Test
void getGroupBy_shouldBuildGroupAndOrderSegment() {
QueryWrapperUtil.GroupDescriptor g1 = new QueryWrapperUtil.GroupDescriptor("dept_id", "asc", true);
QueryWrapperUtil.GroupDescriptor g2 = new QueryWrapperUtil.GroupDescriptor("role_id", "desc", true);
QueryWrapperUtil.DataSourceRequest req = new QueryWrapperUtil.DataSourceRequest(10, 0, List.of(g1, g2));
String sql = QueryWrapperUtil.getGroupBy(req);
Assertions.assertEquals(" group by dept_id, role_id order by dept_id asc, role_id desc", sql);
}
@Test
void getGroupBy_shouldRejectUnsafeIdentifier() {
QueryWrapperUtil.GroupDescriptor bad = new QueryWrapperUtil.GroupDescriptor("name;drop", "asc", true);
QueryWrapperUtil.DataSourceRequest req = new QueryWrapperUtil.DataSourceRequest(10, 0, List.of(bad));
Assertions.assertThrows(IllegalArgumentException.class, () -> QueryWrapperUtil.getGroupBy(req));
}
@Test
void getFieldValues_shouldReadInstanceAndSuperclassFields() {
Child child = new Child();
Map<String, Object> values = QueryWrapperUtil.getFieldValues(child);
Assertions.assertNotNull(values);
Assertions.assertEquals("parent", values.get("parentField"));
Assertions.assertEquals(7, values.get("childField"));
}
@Test
void getFieldValues_shouldReadOnlyStaticWhenInputIsClass() {
Map<String, Object> values = QueryWrapperUtil.getFieldValues(StaticHolder.class);
Assertions.assertNotNull(values);
Assertions.assertEquals("staticValue", values.get("S"));
Assertions.assertFalse(values.containsKey("normalField"));
}
@Test
void getFilterFieldValue_shouldExtractNestedFieldValues() {
String filter = "[[\"name\",\"contains\",\"alice\"],\"and\",[[\"name\",\"=\",\"bob\"],\"or\",[\"age\",\">\",18]]]";
String value = QueryWrapperUtil.getFilterFieldValue(filter, "name");
Assertions.assertEquals("alice,bob", value);
}
@Test
void applyDevExtremeFilter_shouldBuildExpectedSqlAndRespectRemoveFields() {
String filter = "[[\"username\",\"contains\",\"zhang\"],\"and\",[\"status\",\"=\",1],\"or\",[\"email\",\"=\",null]]";
Map<String, String> fieldsMap = Map.of(
"username", "USER_NAME",
"status", "STATUS",
"email", "EMAIL"
);
QueryWrapper<Object> wrapper = new QueryWrapper<>();
QueryWrapperUtil.applyDevExtremeFilter(
wrapper,
filter,
Object.class,
false,
fieldsMap,
List.of("status")
);
String sqlSegment = wrapper.getSqlSegment();
Assertions.assertNotNull(sqlSegment);
Assertions.assertTrue(sqlSegment.contains("USER_NAME"));
Assertions.assertFalse(sqlSegment.contains("STATUS"));
Assertions.assertTrue(sqlSegment.contains("EMAIL"));
}
@Test
void toUnderline_shouldConvertCamelCase() {
Assertions.assertEquals("user_name", QueryWrapperUtil.toUnderline("userName"));
}
private static class Parent {
private String parentField = "parent";
}
private static class Child extends Parent {
private int childField = 7;
}
private static class StaticHolder {
private static String S = "staticValue";
private String normalField = "normal";
}
}