添加了sql解析器相关功能
This commit is contained in:
parent
905847a2d5
commit
addb2b671d
5
.vscode/settings.json
vendored
Normal file
5
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"editor.codeLens": true,
|
||||||
|
"java.test.editor.enableShortcuts": true,
|
||||||
|
"testing.gutterEnabled": true
|
||||||
|
}
|
||||||
@ -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> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分页执行完整 SQL(SQL 内可使用 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);
|
||||||
|
}
|
||||||
@ -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_name(hyphenation 可传 "_" 或 "-")。
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把 filter(JsonNode 数组结构)递归解析并应用到 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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user