本文档包含 DynamicConditionUtil 工具类源码及详细注释,支持的查询参数说明,以及两个基于该工具类的 Spring Boot Controller 示例接口代码和使用说明。

一、DynamicConditionUtil 工具类源码及说明


package com.wpglb.dms.orders.utils;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import java.lang.reflect.Field;
import java.util.Map;



/**
 * 分页查询接口说明
 *
 * 支持的查询参数格式(字段名 + 操作符):
 * 
 * - field_eq=value          // 等于(=)
 * - field_ne=value          // 不等于(!=)
 * - field_gt=value          // 大于(>)
 * - field_lt=value          // 小于(<)
 * - field_ge=value          // 大于等于(>=)
 * - field_le=value          // 小于等于(<=)
 * - field_like=value        // 模糊匹配(LIKE '%value%')
 * - field_in=val1,val2      // IN 查询(多个值逗号分隔)
 * - field_between=val1,val2 // BETWEEN 范围查询(两个值逗号分隔)
 * 
 * 特殊说明:
 * - 支持字段前带表别名前缀,格式如:a.orderNo_eq、b.status_in
 *   用于多表关联查询,前缀会原样拼接到SQL字段前,如 a.order_no、b.status
 * - 参数字段名保持与实体字段名一致,支持驼峰或下划线格式
 * - 多个参数间自动以 AND 连接,构成复合查询条件
 * - 对于时间或数字类型的 BETWEEN 范围查询,前端需传递两个有效值,格式示例:completionTime_between=2024-01-01 00:00:00,2024-01-31 23:59:59
 * - LIKE 查询自动在两边添加百分号,进行模糊匹配
 * - IN 查询参数以英文逗号分隔多个值,自动转换成 SQL 的 IN ('val1','val2',...)
 *
 * 示例请求:
 * GET /truck-loading-list/page
 *   ?orderNo_eq=ABC123
 *   &status_in=NEW,FINISHED
 *   &completionTime_ge=2024-01-01
 *   &completionTime_le=2024-01-31
 *   &weight_between=10,50
 *   &a.createUserId_eq=1001
 *   &b.carrierName_like=顺丰
 *
 * 生成的 SQL 示例(假设表别名a对应主表,b为关联表):
 * 
 * SELECT a.*, b.carrier_name
 * FROM truck_loading_list a
 * LEFT JOIN carrier_info b ON a.carrier_id = b.id
 * WHERE a.order_no = 'ABC123'
 *   AND a.status IN ('NEW','FINISHED')
 *   AND a.completion_time >= '2024-01-01'
 *   AND a.completion_time <= '2024-01-31'
 *   AND a.weight BETWEEN 10 AND 50
 *   AND a.create_user_id = 1001
 *   AND b.carrier_name LIKE '%顺丰%'
 * ORDER BY a.id DESC
 * LIMIT 0, 10;
 * 
 * 实现原理:
 * 1. 前端传递参数时,参数名格式固定为【字段名_操作符】,如 orderNo_eq、status_in。
 * 2. 后端通过反射或手动解析参数名,拆分出字段名和操作符。
 * 3. 如果字段名前带表别名(如 a., b.),直接拼接到数据库字段前面,用于多表查询。
 * 4. 针对不同操作符,调用 MyBatis-Plus QueryWrapper 对应方法,如 eq(), ne(), gt(), lt(), ge(), le(), like(), in(), between()。
 * 5. 对于 between 操作,拆分传入的两个值,分别作为开始和结束边界传给 QueryWrapper.between()。
 * 6. 对于 like 操作,自动在值前后添加 `%` 以实现模糊匹配。
 * 7. 对于 in 操作,拆分逗号分隔的值构造集合传入 QueryWrapper.in()。
 * 8. 最终构造好的 QueryWrapper 传给 MyBatis-Plus 的分页查询方法,实现动态多条件组合查询。
 * 
 * 注意事项:
 * - 参数中的时间字符串需保证格式一致,建议后端统一解析为 LocalDateTime 或 Date 类型。
 * - 实体字段和数据库字段名应保持映射一致,或者通过@TableField注解指定。
 * - 支持多条件自动组合为 AND 关系,复杂 OR 条件需额外扩展。
 */
public class DynamicConditionUtil {

    private static final List<String> OPERATORS = Arrays.asList("eq", "ne", "gt", "lt", "ge", "le", "like", "in", "between");

    /**
     * 构建 QueryWrapper 查询条件
     */
    public static <T> QueryWrapper<T> buildWrapper(Class<T> entityClass) {
        QueryWrapper<T> wrapper = new QueryWrapper<>();
        Map<String, String[]> paramMap = getCurrentHttpServletRequest().getParameterMap();

        for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
            String rawKey = entry.getKey();
            String[] values = entry.getValue();
            if (values == null || values.length == 0 || StringUtils.isBlank(values[0])) {
                continue;
            }
            String value = values[0];

            // 处理前缀(如 a.fieldName_op)
            String tableAlias = null;
            String fieldAndOp = rawKey;

            if (rawKey.contains(".")) {
                String[] split = rawKey.split("\\.", 2);
                tableAlias = split[0];
                fieldAndOp = split[1];
            }

            String fieldName = fieldAndOp;
            String operator = "eq";

            for (String op : OPERATORS) {
                if (fieldAndOp.endsWith("_" + op)) {
                    fieldName = fieldAndOp.substring(0, fieldAndOp.length() - op.length() - 1);
                    operator = op;
                    break;
                }
            }

            String columnName = (tableAlias != null ? tableAlias + "." : "") + camelToUnderline(fieldName);

            // 构造条件
            switch (operator) {
                case "eq" -> wrapper.eq(columnName, value);
                case "ne" -> wrapper.ne(columnName, value);
                case "gt" -> wrapper.gt(columnName, value);
                case "lt" -> wrapper.lt(columnName, value);
                case "ge" -> wrapper.ge(columnName, value);
                case "le" -> wrapper.le(columnName, value);
                case "like" -> wrapper.like(columnName, value);
                case "in" -> wrapper.in(columnName, Arrays.asList(value.split(",")));
                case "between" -> {
                    String[] range = value.split(",");
                    if (range.length == 2) {
                        wrapper.between(columnName, range[0], range[1]);
                    }
                }
            }
        }

        return wrapper;
    }

    /**
     * 获取当前请求
     */
    private static HttpServletRequest getCurrentHttpServletRequest() {
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        return Objects.requireNonNull(requestAttributes).getRequest();
    }

    /**
     * 驼峰转下划线
     */
    private static String camelToUnderline(String str) {
        if (str == null) return null;
        Matcher matcher = Pattern.compile("[A-Z]").matcher(str);
        StringBuilder sb = new StringBuilder(str);
        int offset = 0;
        while (matcher.find()) {
            sb.insert(matcher.start() + offset, "_");
            offset++;
        }
        return sb.toString().toLowerCase();
    }

    public static <T> QueryWrapper<T> buildWrapper(Map<String, Object> filterMap, Class<T> clazz) {
        QueryWrapper<T> wrapper = new QueryWrapper<>();
        if (MapUtil.isEmpty(filterMap)) {
            return wrapper;
        }

        for (Map.Entry<String, Object> entry : filterMap.entrySet()) {
            String key = entry.getKey();
            Object value = entry.getValue();
            if (value == null || StrUtil.isBlank(value.toString())) continue;

            String fieldName = key;
            String operator = "eq";

            if (key.contains("_")) {
                String[] parts = key.split("_", 2);
                fieldName = parts[0];
                operator = parts[1].toLowerCase();
            }

            // 解析实体字段对应数据库列名(优先@TableField注解)
            String dbColumnName = getTableFieldName(clazz, fieldName);
            if (dbColumnName == null) {
                dbColumnName = StrUtil.toUnderlineCase(fieldName); // 驼峰转下划线
            }

            switch (operator) {
                case "like":
                    wrapper.like(dbColumnName, value);
                    break;
                case "in":
                    wrapper.in(dbColumnName, StrUtil.split(value.toString(), ","));
                    break;
                case "ge":
                    wrapper.ge(dbColumnName, value);
                    break;
                case "le":
                    wrapper.le(dbColumnName, value);
                    break;
                case "gt":
                    wrapper.gt(dbColumnName, value);
                    break;
                case "lt":
                    wrapper.lt(dbColumnName, value);
                    break;
                case "ne":
                    wrapper.ne(dbColumnName, value);
                    break;
                case "eq":
                default:
                    wrapper.eq(dbColumnName, value);
                    break;
            }
        }

        return wrapper;
    }

    private static <T> String getTableFieldName(Class<T> clazz, String fieldName) {
        try {
            Field field = clazz.getDeclaredField(fieldName);
            TableField tableField = field.getAnnotation(TableField.class);
            if (tableField != null && StrUtil.isNotBlank(tableField.value())) {
                return tableField.value();
            }
        } catch (NoSuchFieldException e) {
            // 字段不存在,忽略
        }
        return null;
    }
}

二、支持的查询参数说明

查询参数格式

参数名格式:字段名_操作符=值
操作符    说明    SQL 示例
eq    等于    field = value
ne    不等于    field != value
gt    大于    field > value
lt    小于    field < value
ge    大于等于    field >= value
le    小于等于    field <= value
like    模糊匹配    field LIKE '%value%'
in    IN查询    field IN ('val1','val2')
between    范围查询    field BETWEEN val1 AND val2
特殊说明
1. 表别名支持:支持字段前带表别名前缀,格式如:a.orderNo_eq、b.status_in
2. 字段名格式:参数字段名保持与实体字段名一致,支持驼峰或下划线格式
3. 条件连接:多个参数间自动以 AND 连接
4. 时间范围查询:时间或数字类型的 BETWEEN 查询需要传递两个有效值
5. LIKE查询:自动在值两边添加百分号
6. IN查询:参数以英文逗号分隔多个值
生成的SQL示例
sql
SELECT a.*, b.carrier_name
FROM truck_loading_list a
LEFT JOIN carrier_info b ON a.carrier_id = b.id
WHERE a.order_no = 'ABC123'
  AND a.status IN ('NEW','FINISHED')
  AND a.completion_time >= '2024-01-01'
  AND a.completion_time <= '2024-01-31'
  AND a.weight BETWEEN 10 AND 50
  AND a.create_user_id = 1001
  AND b.carrier_name LIKE '%顺丰%'
ORDER BY a.id DESC
LIMIT 0, 10;

三、示例接口代码

1. TruckLoadingListController 示例
    @GetMapping("/NewPage1")
    @Operation(summary = "NewPage1")
    public R<IPage<TruckLoadingList>> page(@Parameter(hidden = true) Query query, TruckLoadingList truckLoadingList) {
        QueryWrapper<TruckLoadingList> wrapper = DynamicConditionUtil.buildWrapper(TruckLoadingList.class);
        IPage<TruckLoadingList> pages = truckLoadingService.page(Condition.getPage(query), wrapper);
        return R.data(pages);
    }

    @PostMapping("NewPage2")
    @Operation(summary = "NewPage2")
    public R<IPage<TruckLoadingList>> page(@RequestBody QueryRequest<Map<String, Object>> request) {
        Query query = request.getQuery();
        Map<String, Object> filter = request.getFilter();
        QueryWrapper<TruckLoadingList> wrapper = DynamicConditionUtil.buildWrapper(filter, TruckLoadingList.class);
        IPage<TruckLoadingList> page = truckLoadingService.page(Condition.getPage(query), wrapper);
        return R.data(page);
    }

四、使用说明

1. 前端调用参数示例 GET
GET NewPage1
  ?orderNo_eq=ABC123
  &status_in=NEW,FINISHED
  &completionTime_ge=2024-01-01
  &completionTime_le=2024-01-31
  &weight_between=10,50
  &a.createUserId_eq=1001
  &b.carrierName_like=顺丰
2. 前端调用参数示例 POST
{
  "query": {
    "current": 1,
    "size": 10
  },
  "filter": {
    "loadingListId_in": "68",
    "truckId_eq":"28"
  }
}
3. 参数格式要求
● 参数名格式:字段名_操作符
● 支持的操作符:eq, ne, gt, lt, ge, le, like, in, between
● 字段名需对应实体字段名
● 支持驼峰和下划线形式
4. 注意事项
1. 字段名转换:
  ○ 默认会将驼峰命名字段转换为下划线命名(如 loadingListId → loading_list_id)
  ○ 可以通过 @TableField 注解自定义列名
2. 空值处理:
  ○ 值为 null 或空字符串的条件会被自动忽略
3. 类型转换:
  ○ 值会按照实体类字段类型自动转换
  ○ 日期时间类型需要确保格式一致
4. 安全性:
  ○ 所有输入值都会经过 MyBatis-Plus 的参数化处理,防止 SQL 注入
5. 性能建议:
  ○ 对于大数据量表,建议为常用查询字段添加索引
  ○ like 查询尽量避免以 % 开头,会导致索引失效
5. 常见问题
Q: 为什么我的条件没有生效?
A: 请检查:
1. 字段名是否正确(包括大小写)
2. 操作符是否正确
3. 值是否为 null 或空字符串
4. 实体类是否有对应的字段定义
Q: 如何实现 OR 条件?
A: 当前版本默认使用 AND 连接所有条件,如需 OR 条件,需要自定义实现或使用 MyBatis-Plus 的 or() 方法手动构建。
Q: 如何添加排序?
A: 可以通过 Query 对象的 ascs 和 descs 属性添加排序条件,或直接在 QueryWrapper 上调用
作者:陆飞  创建时间:2026-01-13 17:03
最后编辑:陆飞  更新时间:2026-03-03 10:08