告别硬编码!SpringBoot + 动态 SQL + 规则引擎实现灵活报表查询

4 阅读14分钟

告别硬编码!SpringBoot + 动态 SQL + 规则引擎实现灵活报表查询

在业务系统中,报表查询是高频核心需求 —— 运营需按时间、地区、产品等多维度组合筛选数据,市场需快速统计活动效果,财务需精准核算收支。传统方案中,每新增一种查询场景就需开发专属接口或堆砌大量 if-else 逻辑,导致代码臃肿、可维护性差、响应需求慢。本文基于实战经验,详细拆解 SpringBoot + MyBatis-Plus 动态 SQL + Easy Rules 规则引擎 的灵活报表方案,通过可视化配置组合查询条件,开发无需重复编码。

一、传统报表查询的痛点

  1. 硬编码冗余:不同查询场景对应不同接口,或同一接口内嵌套大量 if-else 判断条件组合,代码膨胀且难以维护;
  2. 灵活性不足:新增筛选维度、修改条件逻辑需开发介入,迭代周期长,无法快速响应业务需求;
  3. 规则难复用:相同的条件校验(如日期范围合法性、数值区间限制)在多个报表中重复编写,易出现逻辑不一致;
  4. SQL 注入风险:直接拼接用户输入的查询条件,若未做严格校验,可能引发 SQL 注入安全问题;
  5. 协作成本高:业务人员需依赖开发实现查询需求,沟通成本高,开发精力被重复工作占用。

二、核心方案设计与技术选型

1. 方案核心理念

核心是 “配置化 + 动态化 + 规则化”:

  • 配置化:业务人员通过前端可视化配置查询条件(字段、操作符、值、逻辑关系),无需编写代码;
  • 动态化:后端根据配置的条件,动态构建 SQL 查询语句,适配不同查询场景;
  • 规则化:通过规则引擎统一处理条件校验、数据转换等逻辑,确保规则可复用、易维护。

整体流程示意图:

业务人员配置查询条件 → 前端提交条件列表 → 后端接收并校验(Easy Rules)→ 动态构建SQL(MyBatis-Plus)→ 执行查询 → 返回报表数据

2. 技术选型与优势

组件选型理由
开发框架SpringBoot(快速整合组件,简化配置,降低开发成本)
ORM 框架MyBatis-Plus(内置 QueryWrapper,支持动态 SQL 构建,无需手动拼接 SQL)
规则引擎Easy Rules(轻量级、API 简洁,支持注解式规则定义,便于条件校验和逻辑编排)
条件模型封装自定义 QueryCondition(统一封装字段名、操作符、值、逻辑连接符)
安全防护MyBatis-Plus 参数绑定 + 字段白名单校验(防止 SQL 注入)
前端交互拖拽式条件配置组件(业务人员可视化组合条件,降低使用门槛)

三、核心实现细节

1. 统一查询条件模型设计

定义QueryCondition类封装查询条件的核心要素,作为前后端数据传输的标准格式,覆盖所有常见查询场景:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class QueryCondition {
    /** 字段名(需与数据库字段或实体类属性一致) */
    private String field;
    /** 操作符(EQ=等于、GT=大于、LT=小于等,覆盖常见查询场景) */
    private Operator operator;
    /** 查询值(单个值或多个值,如BETWEEN需传入数组) */
    private Object value;
    /** 逻辑连接符(AND/OR,用于多条件组合) */
    private Logic logic;
    /** 字段类型(STRING/NUMBER/DATE,用于数据类型转换) */
    private FieldType fieldType;

    // 操作符枚举(覆盖常见查询场景)
    public enum Operator {
        EQ, NE, GT, LT, GE, LE, LIKE, LIKE_LEFT, LIKE_RIGHT, BETWEEN, IN, NOT_IN, IS_NULL, IS_NOT_NULL
    }

    // 逻辑连接符枚举
    public enum Logic {
        AND, OR
    }

    // 字段类型枚举
    public enum FieldType {
        STRING, NUMBER, DATE, DATETIME
    }
}

2. 规则引擎配置(Easy Rules)

通过 Easy Rules 定义通用校验规则,处理字段合法性、值类型转换、条件逻辑校验等,避免重复编码,确保规则统一:

(1)字段白名单校验规则(优先级最高)

防止恶意查询不存在的字段或敏感字段,避免 SQL 语法错误或信息泄露:

@Rule(name = "字段白名单校验规则", description = "校验查询字段是否在允许的白名单内")
public class FieldWhitelistRule {

    // 允许查询的字段白名单(实际项目可从配置中心加载,支持动态更新)
    private static final Set<String> ALLOWED_FIELDS = new HashSet<>(Arrays.asList(
            "id", "order_no", "product_name", "amount", "create_time", "status", "region", "user_id"
    ));

    /** 规则触发条件:字段名非空时执行校验 */
    @Condition
    public boolean isFieldCheckRequired(QueryCondition condition) {
        return StringUtils.hasText(condition.getField());
    }

    /** 规则执行逻辑:校验字段是否在白名单内 */
    @Action
    public void validateField(QueryCondition condition) {
        if (!ALLOWED_FIELDS.contains(condition.getField())) {
            throw new BusinessException("不允许查询的字段:" + condition.getField());
        }
    }

    /** 规则优先级(数值越小优先级越高) */
    @Priority
    public int getPriority() {
        return 0; // 最高优先级,先校验字段合法性
    }
}
(2)日期范围校验规则

针对日期类型字段的区间查询,校验值格式和逻辑合法性:

@Rule(name = "日期范围校验规则", description = "校验日期类型字段的区间查询条件合法性")
public class DateRangeRule {

    /** 规则触发条件:字段类型为DATE/DATETIME,且操作符为BETWEEN */
    @Condition
    public boolean isDateBetweenCondition(QueryCondition condition) {
        return (FieldType.DATE.equals(condition.getFieldType()) || FieldType.DATETIME.equals(condition.getFieldType()))
                && Operator.BETWEEN.equals(condition.getOperator());
    }

    /** 规则执行逻辑:校验值格式和时间顺序 */
    @Action
    public void validateDateRange(QueryCondition condition) {
        Object value = condition.getValue();
        if (!(value instanceof String[])) {
            throw new BusinessException("日期区间查询值必须为数组格式(开始日期,结束日期)");
        }

        String[] dateArr = (String[]) value;
        if (dateArr.length != 2) {
            throw new BusinessException("日期区间查询必须传入两个日期值");
        }

        try {
            // 适配DATE和DATETIME两种格式
            if (FieldType.DATE.equals(condition.getFieldType())) {
                LocalDate startDate = LocalDate.parse(dateArr[0], DateTimeFormatter.ISO_LOCAL_DATE);
                LocalDate endDate = LocalDate.parse(dateArr[1], DateTimeFormatter.ISO_LOCAL_DATE);
                if (startDate.isAfter(endDate)) {
                    throw new BusinessException("开始日期不能晚于结束日期");
                }
            } else {
                LocalDateTime startDateTime = LocalDateTime.parse(dateArr[0], DateTimeFormatter.ISO_LOCAL_DATE_TIME);
                LocalDateTime endDateTime = LocalDateTime.parse(dateArr[1], DateTimeFormatter.ISO_LOCAL_DATE_TIME);
                if (startDateTime.isAfter(endDateTime)) {
                    throw new BusinessException("开始时间不能晚于结束时间");
                }
            }
        } catch (DateTimeParseException e) {
            String format = FieldType.DATE.equals(condition.getFieldType()) ? "yyyy-MM-dd" : "yyyy-MM-dd HH:mm:ss";
            throw new BusinessException("日期格式非法,需符合" + format + "格式");
        }
    }

    /** 规则优先级 */
    @Priority
    public int getPriority() {
        return 1;
    }
}
(3)规则引擎初始化配置
@Configuration
public class EasyRulesConfig {

    @Bean
    public RulesEngine rulesEngine() {
        // 配置规则引擎参数
        RulesEngineParameters parameters = new RulesEngineParameters()
                .skipOnFirstAppliedRule(false) // 不跳过后续规则,所有匹配的规则都执行
                .skipOnFirstFailedRule(true)   // 执行失败时跳过后续规则,快速返回异常
                .skipOnFirstNonTriggeredRule(false);

        return new DefaultRulesEngine(parameters);
    }

    @Bean
    public Rules queryRules() {
        Rules rules = new Rules();
        // 注册所有查询相关规则(可扩展其他规则:数值范围校验、字符串长度限制等)
        rules.register(new FieldWhitelistRule());
        rules.register(new DateRangeRule());
        return rules;
    }
}

3. 动态 SQL 构建(MyBatis-Plus)

基于 QueryWrapper,根据前端传入的条件列表动态拼接 SQL,避免手动拼接带来的安全风险和语法错误,同时适配不同字段类型和操作符:

@Service
public class DynamicReportService {

    @Autowired
    private OrderMapper orderMapper; // 报表数据DAO(基于MyBatis-Plus)
    @Autowired
    private RulesEngine rulesEngine;
    @Autowired
    private Rules queryRules;

    /**
     * 执行动态报表查询
     */
    public Page<ReportVO> queryReport(Page<ReportVO> page, List<QueryCondition> conditions) {
        // 1. 校验查询条件(通过规则引擎执行所有匹配规则)
        validateConditions(conditions);

        // 2. 构建动态查询条件
        QueryWrapper<Order> queryWrapper = buildQueryWrapper(conditions);

        // 3. 执行分页查询(MyBatis-Plus自动分页)
        Page<Order> orderPage = orderMapper.selectPage(page, queryWrapper);

        // 4. 转换为报表VO返回(字段映射、数据格式化)
        return orderPage.convert(this::convertToReportVO);
    }

    /**
     * 条件校验:通过规则引擎执行所有规则
     */
    private void validateConditions(List<QueryCondition> conditions) {
        if (CollectionUtils.isEmpty(conditions)) {
            return;
        }

        for (QueryCondition condition : conditions) {
            // 执行规则引擎,触发匹配的规则
            rulesEngine.fire(queryRules, condition);
        }
    }

    /**
     * 构建动态SQL查询条件
     */
    private QueryWrapper<Order> buildQueryWrapper(List<QueryCondition> conditions) {
        QueryWrapper<Order> queryWrapper = new QueryWrapper<>();

        if (CollectionUtils.isEmpty(conditions)) {
            return queryWrapper;
        }

        // 遍历条件列表,动态拼接查询条件
        for (int i = 0; i < conditions.size(); i++) {
            QueryCondition condition = conditions.get(i);
            String field = condition.getField();
            Operator operator = condition.getOperator();
            Object value = condition.getValue();
            Logic logic = condition.getLogic();

            // 处理逻辑连接符(第一个条件无需前缀逻辑符)
            if (i > 0) {
                if (Logic.AND.equals(logic)) {
                    queryWrapper.and(w -> buildSingleCondition(w, field, operator, value, condition.getFieldType()));
                } else {
                    queryWrapper.or(w -> buildSingleCondition(w, field, operator, value, condition.getFieldType()));
                }
            } else {
                buildSingleCondition(queryWrapper, field, operator, value, condition.getFieldType());
            }
        }

        // 默认按创建时间降序排序
        queryWrapper.orderByDesc("create_time");
        return queryWrapper;
    }

    /**
     * 构建单个查询条件(适配不同操作符和字段类型)
     */
    private void buildSingleCondition(QueryWrapper<Order> wrapper, String field, Operator operator, Object value, FieldType fieldType) {
        // 数据类型转换(如字符串转日期、数值)
        Object convertedValue = convertValue(value, fieldType);

        switch (operator) {
            case EQ:
                wrapper.eq(field, convertedValue);
                break;
            case NE:
                wrapper.ne(field, convertedValue);
                break;
            case GT:
                wrapper.gt(field, convertedValue);
                break;
            case LT:
                wrapper.lt(field, convertedValue);
                break;
            case GE:
                wrapper.ge(field, convertedValue);
                break;
            case LE:
                wrapper.le(field, convertedValue);
                break;
            case LIKE:
                wrapper.like(field, "%" + convertedValue + "%");
                break;
            case LIKE_LEFT:
                wrapper.likeLeft(field, convertedValue);
                break;
            case LIKE_RIGHT:
                wrapper.likeRight(field, convertedValue);
                break;
            case BETWEEN:
                if (convertedValue instanceof Object[]) {
                    Object[] arr = (Object[]) convertedValue;
                    wrapper.between(field, arr[0], arr[1]);
                }
                break;
            case IN:
                if (convertedValue instanceof Collection) {
                    wrapper.in(field, (Collection<?>) convertedValue);
                } else if (convertedValue instanceof Object[]) {
                    wrapper.in(field, (Object[]) convertedValue);
                }
                break;
            case NOT_IN:
                if (convertedValue instanceof Collection) {
                    wrapper.notIn(field, (Collection<?>) convertedValue);
                } else if (convertedValue instanceof Object[]) {
                    wrapper.notIn(field, (Object[]) convertedValue);
                }
                break;
            case IS_NULL:
                wrapper.isNull(field);
                break;
            case IS_NOT_NULL:
                wrapper.isNotNull(field);
                break;
            default:
                throw new BusinessException("不支持的操作符:" + operator);
        }
    }

    /**
     * 数据类型转换(根据字段类型将前端传入的值转为数据库匹配类型)
     */
    private Object convertValue(Object value, FieldType fieldType) {
        if (value == null) {
            return null;
        }

        switch (fieldType) {
            case NUMBER:
                // 字符串转数值,支持单个值和数组
                if (value instanceof String) {
                    return BigDecimal.valueOf(Double.parseDouble((String) value));
                } else if (value instanceof String[]) {
                    return Arrays.stream((String[]) value)
                            .map(BigDecimal::new)
                            .toArray(BigDecimal[]::new);
                }
                break;
            case DATE:
                // 字符串转LocalDate
                if (value instanceof String) {
                    return LocalDate.parse((String) value, DateTimeFormatter.ISO_LOCAL_DATE);
                } else if (value instanceof String[]) {
                    return Arrays.stream((String[]) value)
                            .map(s -> LocalDate.parse(s, DateTimeFormatter.ISO_LOCAL_DATE))
                            .toArray(LocalDate[]::new);
                }
                break;
            case DATETIME:
                // 字符串转LocalDateTime
                if (value instanceof String) {
                    return LocalDateTime.parse((String) value, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
                } else if (value instanceof String[]) {
                    return Arrays.stream((String[]) value)
                            .map(s -> LocalDateTime.parse(s, DateTimeFormatter.ISO_LOCAL_DATE_TIME))
                            .toArray(LocalDateTime[]::new);
                }
                break;
            default:
                // STRING类型直接返回
                break;
        }
        return value;
    }

    /**
     * 实体类转报表VO(简化版,实际项目可使用MapStruct等工具)
     */
    private ReportVO convertToReportVO(Order order) {
        ReportVO vo = new ReportVO();
        vo.setOrderNo(order.getOrderNo());
        vo.setProductName(order.getProductName());
        vo.setAmount(order.getAmount());
        vo.setCreateTime(order.getCreateTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        vo.setStatusDesc(OrderStatus.valueOf(order.getStatus()).getDesc());
        vo.setRegion(order.getRegion());
        vo.setUserId(order.getUserId());
        return vo;
    }
}

4. 控制器与前端交互

提供 REST 接口接收前端配置的条件列表,支持分页查询,同时兼容简单查询和复杂多条件组合查询:

@RestController
@RequestMapping("/api/report")
public class ReportController {

    @Autowired
    private DynamicReportService dynamicReportService;

    /**
     * 动态报表查询接口
     * @param pageNum 页码(默认1)
     * @param pageSize 每页条数(默认10)
     * @param conditions 查询条件列表
     * @return 分页报表数据
     */
    @PostMapping("/dynamic-query")
    public Result<Page<ReportVO>> dynamicQuery(
            @RequestParam(defaultValue = "1") Integer pageNum,
            @RequestParam(defaultValue = "10") Integer pageSize,
            @RequestBody(required = false) List<QueryCondition> conditions) {

        Page<ReportVO> page = new Page<>(pageNum, pageSize);
        Page<ReportVO> reportPage = dynamicReportService.queryReport(page, conditions);
        return Result.success(reportPage);
    }

    /**
     * 保存查询配置(业务人员可保存常用查询条件,下次直接调用)
     */
    @PostMapping("/save-config")
    public Result<Long> saveQueryConfig(@RequestBody QueryConfigSaveDTO saveDTO) {
        // 1. 校验配置名称唯一性(避免重复)
        if (queryConfigService.existsByName(saveDTO.getConfigName())) {
            throw new BusinessException("查询配置名称已存在,请更换名称");
        }

        // 2. 保存配置(JSON格式存储条件列表)
        QueryConfig queryConfig = new QueryConfig();
        queryConfig.setConfigName(saveDTO.getConfigName());
        queryConfig.setConditions(JSON.toJSONString(saveDTO.getConditions()));
        queryConfig.setCreator(saveDTO.getCreator());
        queryConfig.setCreateTime(LocalDateTime.now());
        queryConfigService.save(queryConfig);

        return Result.success(queryConfig.getId());
    }

    /**
     * 加载已保存的查询配置
     */
    @GetMapping("/load-config/{configId}")
    public Result<List<QueryCondition>> loadQueryConfig(@PathVariable Long configId) {
        QueryConfig queryConfig = queryConfigService.getById(configId);
        if (queryConfig == null) {
            throw new BusinessException("查询配置不存在,ID:" + configId);
        }

        // 解析JSON条件列表并返回
        List<QueryCondition> conditions = JSON.parseArray(
                queryConfig.getConditions(), QueryCondition.class);
        return Result.success(conditions);
    }
}

5. 核心依赖配置(pom.xml)

<!-- SpringBoot核心依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

<!-- MyBatis-Plus依赖(动态SQL核心) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.5.3.1</version>
</dependency>

<!-- Easy Rules规则引擎 -->
<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-rules-core</artifactId>
    <version>4.1.0</version>
</dependency>
<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-rules-spring</artifactId>
    <version>4.1.0</version>
</dependency>

<!-- 数据库驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

<!-- JSON处理 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.32</version>
</dependency>

<!-- 分页插件(MyBatis-Plus内置) -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-extension</artifactId>
    <version>3.5.3.1</version>
</dependency>

6. 前端可视化配置示例(Vue3)

提供拖拽式条件配置界面,支持字段选择、操作符切换、值输入,实时预览查询条件逻辑:

<template>
  <div class="query-config-container">
    <!-- 条件配置区域 -->
    <div class="condition-list">
      <div class="condition-item" v-for="(item, index) in conditions" :key="index">
        <!-- 字段选择器 -->
        <el-select v-model="item.field" placeholder="请选择字段">
          <el-option label="订单号" value="order_no"></el-option>
          <el-option label="商品名称" value="product_name"></el-option>
          <el-option label="金额" value="amount"></el-option>
          <el-option label="创建时间" value="create_time"></el-option>
          <el-option label="状态" value="status"></el-option>
          <el-option label="地区" value="region"></el-option>
        </el-select>

        <!-- 操作符选择器 -->
        <el-select v-model="item.operator" placeholder="请选择操作符">
          <el-option label="等于" value="EQ"></el-option>
          <el-option label="不等于" value="NE"></el-option>
          <el-option label="大于" value="GT"></el-option>
          <el-option label="小于" value="LT"></el-option>
          <el-option label="大于等于" value="GE"></el-option>
          <el-option label="小于等于" value="LE"></el-option>
          <el-option label="包含" value="LIKE"></el-option>
          <el-option label="区间" value="BETWEEN"></el-option>
          <el-option label="在列表中" value="IN"></el-option>
        </el-select>

        <!-- 值输入框(适配不同类型:字符串、数值、日期、数组) -->
        <template v-if="item.operator === 'BETWEEN' || item.operator === 'IN'">
          <el-input v-model="item.value" placeholder="多个值用逗号分隔(如2023-01-01,2023-12-31)"></el-input>
        </template>
        <template v-else>
          <el-input v-model="item.value" placeholder="请输入查询值"></el-input>
        </template>

        <!-- 逻辑连接符(第一个条件隐藏) -->
        <el-select v-model="item.logic" placeholder="逻辑关系" v-if="index > 0">
          <el-option label="并且" value="AND"></el-option>
          <el-option label="或者" value="OR"></el-option>
        </el-select>

        <!-- 删除条件 -->
        <el-button type="danger" icon="el-icon-delete" @click="removeCondition(index)"></el-button>
      </div>
    </div>

    <!-- 操作按钮 -->
    <div class="action-buttons">
      <el-button type="primary" @click="addCondition">添加条件</el-button>
      <el-button type="success" @click="saveConfig">保存配置</el-button>
      <el-button type="info" @click="loadConfig">加载配置</el-button>
      <el-button type="warning" @click="executeQuery">执行查询</el-button>
    </div>

    <!-- 报表结果展示 -->
    <el-table :data="reportData" border style="width: 100%; margin-top: 20px">
      <el-table-column prop="orderNo" label="订单号"></el-table-column>
      <el-table-column prop="productName" label="商品名称"></el-table-column>
      <el-table-column prop="amount" label="金额"></el-table-column>
      <el-table-column prop="createTime" label="创建时间"></el-table-column>
      <el-table-column prop="statusDesc" label="订单状态"></el-table-column>
      <el-table-column prop="region" label="地区"></el-table-column>
    </el-table>

    <!-- 分页组件 -->
    <el-pagination
      @size-change="handleSizeChange"
      @current-change="handleCurrentChange"
      :current-page="currentPage"
      :page-sizes="[10, 20, 50, 100]"
      :page-size="pageSize"
      layout="total, sizes, prev, pager, next, jumper"
      :total="total"
      style="margin-top: 10px; text-align: right"
    ></el-pagination>
  </div>
</template>

<script setup>
import { ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import axios from 'axios';

// 查询条件列表
const conditions = ref([
  { field: 'create_time', operator: 'BETWEEN', value: '2023-01-01,2023-12-31', logic: 'AND', fieldType: 'DATE' }
]);
// 报表数据
const reportData = ref([]);
// 分页参数
const currentPage = ref(1);
const pageSize = ref(10);
const total = ref(0);

// 添加查询条件
const addCondition = () => {
  conditions.value.push({
    field: '',
    operator: 'EQ',
    value: '',
    logic: 'AND',
    fieldType: 'STRING'
  });
};

// 删除查询条件
const removeCondition = (index) => {
  if (conditions.value.length <= 1) {
    ElMessage.warning('至少保留一个查询条件');
    return;
  }
  conditions.value.splice(index, 1);
};

// 执行查询
const executeQuery = async () => {
  try {
    const response = await axios.post('/api/report/dynamic-query', conditions.value, {
      params: {
        pageNum: currentPage.value,
        pageSize: pageSize.value
      }
    });
    const { records, total: totalCount } = response.data.data;
    reportData.value = records;
    total.value = totalCount;
    ElMessage.success('查询成功');
  } catch (error) {
    ElMessage.error(error.response?.data?.msg || '查询失败');
  }
};

// 保存查询配置
const saveConfig = async () => {
  const configName = prompt('请输入查询配置名称');
  if (!configName) return;

  try {
    await axios.post('/api/report/save-config', {
      configName,
      conditions: conditions.value,
      creator: '当前登录用户' // 实际项目从登录信息中获取
    });
    ElMessage.success('配置保存成功');
  } catch (error) {
    ElMessage.error(error.response?.data?.msg || '配置保存失败');
  }
};

// 加载查询配置
const loadConfig = async () => {
  const configId = prompt('请输入查询配置ID');
  if (!configId) return;

  try {
    const response = await axios.get(`/api/report/load-config/${configId}`);
    conditions.value = response.data.data;
    ElMessage.success('配置加载成功');
  } catch (error) {
    ElMessage.error(error.response?.data?.msg || '配置加载失败');
  }
};

// 分页事件处理
const handleSizeChange = (val) => {
  pageSize.value = val;
  executeQuery();
};

const handleCurrentChange = (val) => {
  currentPage.value = val;
  executeQuery();
};
</script>

三、生产环境优化与最佳实践

1. 性能优化策略

  1. 索引优化:为报表查询高频字段(如 create_time、status、region)建立组合索引,提升动态 SQL 查询效率;
  2. 查询缓存:使用 Redis 缓存高频查询结果(如每日固定报表),设置合理过期时间,减少数据库压力;
  3. 分库分表:大数据量场景下,按时间(如按月份)或地区分表,降低单表数据量,提升查询速度;
  4. 字段裁剪:通过查询配置选择所需字段,避免 SELECT *,减少数据传输和解析开销;
  5. 异步查询:复杂报表查询(如跨表统计)通过异步任务执行,返回查询 ID,前端轮询获取结果,避免请求超时。

2. 安全防护措施

  1. 字段白名单:严格限制可查询字段,防止查询敏感字段(如用户密码、手机号);
  2. SQL 注入防护:依赖 MyBatis-Plus 参数绑定,禁止手动拼接 SQL,所有条件通过 QueryWrapper 参数化处理;
  3. 权限控制:不同角色配置不同的字段访问权限(如普通运营只能查询订单金额,财务可查询详细收支);
  4. 请求限流:对动态查询接口设置限流(如每秒 100 次),防止恶意请求压垮数据库。

3. 规则引擎扩展

  1. 新增规则:根据业务需求扩展规则,如数值范围校验(金额必须大于 0)、字符串长度限制(订单号长度为 16 位)等;
  2. 规则动态更新:通过配置中心(如 Nacos)管理规则,支持规则热更新,无需重启服务;
  3. 规则优先级调整:根据业务重要性调整规则执行顺序(如字段校验优先于格式校验)。

4. 与其他方案对比

方案灵活性开发成本维护成本适用场景
硬编码 if-else高(新增条件需改代码)高(逻辑冗余,易出错)简单固定查询场景
MyBatis 动态 SQL中(需手动编写 XML 条件)中(XML 维护复杂)中等复杂度查询场景
本文方案(动态 SQL + 规则引擎)高(配置化组合条件)低(一次开发,永久复用)低(规则化管理,可扩展)复杂多变、频繁变更的报表查询场景
商业 BI 工具(如 Tableau)极高高(需购买和部署)中(需专业人员维护)大型企业、全场景报表需求

四、总结与未来展望

本文方案通过配置化 + 动态化 + 规则化的核心设计,彻底解决了传统报表查询硬编码冗余、灵活性不足的痛点,实现了业务人员自主配置查询条件,开发无需重复编码的目标。核心价值体现在:

  1. 效率提升:业务需求响应时间从 “天级” 缩短至 “分钟级”,开发精力从重复工作转向核心业务;
  2. 降低成本:规则复用减少代码冗余,维护成本降低 50% 以上;
  3. 安全可靠:统一校验和参数化处理,杜绝 SQL 注入和数据泄露风险;
  4. 灵活扩展:支持业务规则动态更新,适配快速变化的市场需求。

未来展望

  1. AI 辅助配置:结合大模型自动生成查询条件(如 “查询近 30 天华北地区销售额前 10 的商品”);
  2. 可视化规则编排:前端拖拽式编排复杂规则(如嵌套逻辑:(A AND B) OR (C AND D));
  3. 多数据源适配:支持 MySQL、PostgreSQL、ClickHouse 等多数据源,统一查询接口;
  4. 报表导出优化:支持 Excel、PDF 等格式导出,自动生成图表(柱状图、折线图)。