刚接手报表模块那阵子我遇到过最磨人的需求不是报表本身多复杂而是“查询条件能不能让用户自己组合”。表面听着简单无非是多几个搜索框但等需求细化下来就发现没那么轻松用户要支持多字段任意组合且组内关系有“且”有“或”组与组之间还能嵌套优先级还得跟手动加括号一样明确。最开始硬编码几个固定查询模板倒也能跑可一旦条件组合数量多起来代码就是一个巨大的if-else泥潭。后来我改用“动态SQL 条件编排器”这套思路把条件组合彻底解耦开报表查询模块一下子清爽了很多。这篇文章就把我落地这套方案的全过程、设计取舍和实际踩坑经验整理出来希望对做报表查询、后台筛选、数据导出的朋友有帮助。1. 为什么报表查询条件需要“编排器”而不是单纯拼SQL1.1 需求场景还原用户想要什么样的组合查询假设我们做一个订单报表后台查询条件可能有订单编号、客户名称、下单时间范围、订单金额范围、支付状态、商品分类等。普通做法是给一排固定搜索项用户填什么就查什么条件之间默认AND。但真实业务里用户经常提出这类问题“我想查最近30天要么是金额大于1万的订单要么是客户名里含‘科技’并且支付状态为已支付的订单”。这种需求翻译成SQL大概是WHERE create_time ? AND (amount 10000 OR (customer_name LIKE %科技% AND pay_status PAID))注意这里的括号金额条件是独立的客户名和支付状态是嵌套的一组“且”然后跟金额条件是“或”的关系。如果系统只支持条件列表平铺根本无法表达这种逻辑。条件编排器要解决的核心问题就是把这种带优先级和嵌套的逻辑结构从前端到后端完整表达、传输、解析最终安全地生成SQL。1.2 硬编码SQL与逐字段if判断的痛点我在初版实现里走过弯路每个条件用if判断是否为空然后拼接SQL片段。比如这样String sql SELECT * FROM t_order WHERE 11; if (StringUtils.hasText(orderNo)) { sql AND order_no orderNo ; } if (amount ! null) { sql AND amount amount; } // 更多条件...问题非常明显第一组合关系被写死。想调整条件间的AND/OR逻辑必须改Java代码无法由前端动态控制。第二嵌套关系难以维护。一旦需要“A且(B或C)”这种结构纯if拼接基本要靠临时变量和子串截断复杂得让人头皮发麻。第三存在SQL注入风险。早期代码直接字符串拼接虽然内部系统风险可控但一旦报表功能对外开放这就是致命漏洞。第四每个报表都要写一套类似的拼接逻辑报表一多代码复制粘贴严重维护成本飙升。1.3 条件编排器到底做了什么条件编排器本质是一个“条件逻辑解析引擎”它的输入是一棵结构化的条件树输出是安全的SQL片段和对应的参数列表。核心职责有三个把用户在前端勾选、输入的条件序列化成一种无歧义的树形结构通常是JSON。后端解析这棵树按照节点类型叶子条件、逻辑组递归生成SQL片段。所有用户输入值统一通过预编译占位符绑定不直接拼进SQL字符串。通过这套抽象新增查询条件不再需要改Java代码只需要在前端配置中心注册一个字段元信息字段名、运算符类型、值类型后端就能自动支持该字段的组合查询。编排器把“条件逻辑”与“具体SQL语句”彻底解耦这是它最大的价值。2. 条件树的数据结构与整体方案设计2.1 前端提交的条件JSON长什么样表达嵌套逻辑最自然的方式就是树形结构。一个逻辑节点包含一个logic字段AND或OR以及一组children子节点。子节点要么是叶子条件包含字段、运算符、值要么是另一个逻辑节点。这种结构可递归嵌套和SQL的括号逻辑完全对应。实际项目中我采用的结构如下{ logic: AND, children: [ { field: create_time, operator: RANGE, value: [2025-01-01 00:00:00, 2025-01-31 23:59:59] }, { logic: OR, children: [ { field: amount, operator: GT, value: 10000 }, { logic: AND, children: [ { field: customer_name, operator: LIKE, value: 科技 }, { field: pay_status, operator: IN, value: [PAID, REFUNDED] } ] } ] } ] }这个JSON里最外层是AND表示所有条件必须同时满足第二个子节点是OR组表示金额大于1万或者“客户名含科技且支付状态在(PAID, REFUNDED)中”。整个结构清晰对应SQL中的括号嵌套。2.2 后端条件树模型的Java设计在后端我定义了三个核心类来承载条件树public class ConditionGroup { private String logic; // AND / OR private ListConditionNode children; } public class ConditionNode { private String field; private String operator; private Object value; private ConditionGroup group; // 如果该节点是逻辑组group非空 }当然实际中为了让JSON反序列化更稳我通常直接用JSON树节点并在解析时做类型判断避免过多的DTO类。解析器递归遍历时会检查当前节点有group就走逻辑组递归有field就走叶子条件解析二者互斥。值得提醒的是条件树必须设计成可嵌套递归的否则表达不了优先级。这是整棵树的灵魂前面很多设计都围绕这个目标展开。2.3 为什么选用JSON而不是自研表达式字符串很多人会问为什么不直接用类似amount10000 and (customer_name like %科技% or pay_status1)这样的表达式字符串第一眼确实直观但实际落地有几个麻烦表达式字符串需要自己写词法解析器处理优先级、括号匹配、非法字符成本和出错率都高。前端生成表达式字符串时需要手动拼接很容易出现括号错位。表达式字符串没法天然携带类型信息值到底是字符串还是数字需要额外判断。树形JSON天然可递归、可校验、可扩展和前后端交互格式完全统一。相比之下JSON条件树结构稳定、校验简单Jackson就能直接反序列化。前端也只需要递归渲染组件即可生成这种结构不必担心字符串拼接出错。这个选择在长期维护中省下了大量时间。3. 核心实现从条件树到安全SQL片段3.1 叶子条件运算符的注册机制条件编排器里的每个叶子条件都需要一个“运算符处理器”来生成SQL片段。因为不同运算符的SQL模板、参数个数、值类型校验都不一样我采用枚举策略的方式实现public enum OperatorType { EQ(, 1) { public String toSql(String field, ListObject params) { params.add(value); return field ?; } }, GT(, 1), LIKE(LIKE, 1) { public String toSql(String field, ListObject params) { params.add(% value %); return field LIKE ?; } }, IN(IN, -1) { public String toSql(String field, ListObject params) { // 生成占位符列表 } }, RANGE(BETWEEN, 2); }每个运算符枚举里定义好“需要几个参数”“如何生成占位符”“值怎么预处理”解析器拿到字段和运算符后直接调用对应策略不需要一堆if-else。新增一个运算符只要加一个枚举值不会影响其他代码。这里有个细节特别值得注意LIKE操作符对应的值在业务层就要决定好是前后模糊还是仅后缀模糊并且位置参数要统一加到参数列表中。如果等SQL片段生成后再处理值容易出现参数个数和占位符对不上的问题。3.2 条件解析器的递归实现解析器的核心方法如下整个逻辑可概括为如果是逻辑组就递归解析子节点并用括号包裹如果是叶子条件就通过运算符枚举生成片段。public SqlParseResult parse(ConditionGroup group, ListObject paramList) { if (group null || CollectionUtils.isEmpty(group.getChildren())) { return SqlParseResult.EMPTY; } ListString sqlParts new ArrayList(); for (ConditionNode node : group.getChildren()) { if (node.isGroup()) { SqlParseResult sub parse(node.getGroup(), paramList); if (sub.isNotEmpty()) { sqlParts.add(( sub.getSql() )); } } else { String part parseLeaf(node, paramList); if (StringUtils.hasText(part)) { sqlParts.add(part); } } } if (sqlParts.isEmpty()) { return SqlParseResult.EMPTY; } String joined String.join( group.getLogic() , sqlParts); return SqlParseResult.of(joined, paramList); }解析时每个逻辑组生成的SQL片段都会包上一层括号这是还原条件优先级的关键。比如前面那个订单例子解析后得到的SQL片段大概就是(create_time BETWEEN ? AND ?) AND ((amount ?) OR ((customer_name LIKE ?) AND (pay_status IN (?, ?))))参数列表则按遇到参数的顺序依次追加[2025-01-01... , 2025-01-31... , 10000, %科技%, PAID, REFUNDED]。这段代码里有一个容易被忽略的细节当某个逻辑组的所有叶子条件均为空时比如值没填这个组应该整个跳过不能拼出一个孤零零的括号否则会破坏整体条件语义。我在解析时判断了isNotEmpty()如果子节点解析结果为空就不纳入拼接。对于AND组全空就直接返回空对于OR组全空也返回空这就避免了下一次拼接时出现“WHERE 11 AND ()”的尴尬SQL。3.3 参数占位符与防SQL注入的关键处理这条是整个方案的安全底线所有值一律通过占位符传递绝不拼进SQL字符串。为什么强调一遍我在初版实现中为了图方便把枚举值、排序字段用字符串拼接结果某次安全扫描直接报高危漏洞后来老老实实全部改成白名单占位符。具体做法是字段名校验所有字段必须来自后端维护的字段元信息注册表不直接信任前端传的field字段。这个元信息表里存了字段名、对应的真实数据库列名、允许的运算符集合、值类型等。值绑定拼SQL时统一使用?占位符值按顺序加入参数列表最终交给MyBatis或JdbcTemplate的预编译机制处理。排序字段与表名白名单校验只允许预先注册过的值不参与动态拼接。用MyBatis时如果是在XML里写动态SQL可以用foreach和if来组装但如果直接在Java代码中生成SQL片段再传给MyBatis的SelectProvider或JdbcTemplate就要注意参数顺序。我倾向于用SqlProviderListObject传参的方式保证占位符和参数列表严格对应。3.4 完整接入SpringBoot的一个最小示例为了说明集成方式我给出一个简化但可直接跑的示例。核心服务类如下Service public class ReportQueryService { private final JdbcTemplate jdbcTemplate; private final ConditionParser conditionParser; public ReportQueryService(JdbcTemplate jdbcTemplate, ConditionParser conditionParser) { this.jdbcTemplate jdbcTemplate; this.conditionParser conditionParser; } public ListMapString, Object query(String tableName, String baseSql, ConditionGroup conditionGroup, int pageNum, int pageSize) { // 1. 表名白名单校验 String safeTable TableWhitelist.check(tableName); // 2. 解析条件树 ListObject params new ArrayList(); SqlParseResult result conditionParser.parse(conditionGroup, params); // 3. 组装完整SQL String whereClause result.isNotEmpty() ? WHERE result.getSql() : ; String sql SELECT * FROM safeTable whereClause LIMIT pageSize OFFSET (pageNum - 1) * pageSize; // 4. 参数顺序执行预编译查询 return jdbcTemplate.queryForList(sql, params.toArray()); } }接口层用一个统一的查询入口接收条件树JSONRestController RequestMapping(/api/report) public class ReportController { PostMapping(/query) public ResultListMapString, Object query(RequestBody ReportQueryRequest request) { ConditionGroup conditionGroup request.getConditionGroup(); // 业务校验、权限校验、数据权限追加 ListMapString, Object data reportQueryService.query(t_order, null, conditionGroup, request.getPageNum(), request.getPageSize()); return Result.success(data); } }有个细节实际报表查询不可能只查一张表都是多表关联。这时baseSql通常是一段已经写好的“固定查询主体”包括join的部分条件编排器只负责生成WHERE子句并追加进去。这样既保留动态性又不至于让关联逻辑也变成动态可改的可控性更强。4. 条件编排器的进阶处理优先级还原与条件优化4.1 逻辑组递归与括号包裹的细节条件树解析成SQL时括号包裹必须严谨。我对每个逻辑组都强制加括号这样做的原因是AND的优先级本来就高于OR但在树形结构里每个节点的逻辑关系已经明确我们不需要依赖数据库的默认优先级。加上括号之后SQL语义完全由树的层级决定任何数据库上执行结果都一致。举两个容易出错的对比场景树结构是{logic: OR, children: [A, {logic: AND, children: [B, C]}]}期望SQL是A OR (B AND C)。如果解析时对子组不加括号就会变成A OR B AND C而数据库默认会先算AND结果变成A OR (B AND C)虽然碰巧一样但语义表达不清晰。树结构是{logic: AND, children: [A, {logic: OR, children: [B, C]}]}期望SQL是A AND (B OR C)。如果子组不加括号就会变成A AND B OR C数据库默认先算AND实际语义就变成(A AND B) OR C彻底错了。所以子组加括号不是可有可无的优化而是必须项。我在代码注释里特别备注了这一点防止后来维护的人“优化”掉括号。4.2 空条件裁剪避免无意义的SQL片段用户在前端可能只填了一部分条件其他留空。此时如果条件树里带着一堆value: null的叶子解析出来的SQL会非常臃肿甚至因为比较符带空值而查不出数据。我的做法是解析叶子条件时如果值不存在null、空字符串、空列表直接返回空片段并跳过该条件。特别要注意IN和RANGE运算符的边界处理IN的值如果是空数组整个条件应跳过而不是生成IN ()后者在MySQL里直接语法错误。RANGE需要两个值都非空才解析如果只填了一个日期可以按用户意图退化为单边范围即只生成 ?或 ?。这种“裁剪”逻辑放在解析器内部统一处理前端不用关心后端代码也不用到处判断。我在实现里专门抽了一个isBlankValue()方法用统一的规则判断“空”确保一致性。4.3 条件去重与同字段合并优化报表查询中常见一类场景同一个字段被加到条件树两个地方比如外层一个amount 10000内层or组里又有amount 5000。这种重复条件虽然SQL能执行但会降低查询效率也让最终SQL很难读。我在编排器里做了一个可选优化解析前先遍历整棵树对同一逻辑组内相同“字段运算符值”的叶子条件做去重。这个优化在条件多时效果明显能减少无用条件数量。不过要小心去重只能在同一个逻辑组内做跨组不能合并因为不同组的逻辑关系不同强行合并会改变语义。同字段合并还有一个场景同一字段的“区间枚举”条件比如status IN (PAID, REFUNDED)和status PAID同时出现。这种属于语义重叠能不能合并取决于业务逻辑确认我建议初期先不去做复杂的语义合并只做简单的重复项去重避免引入难以预料的bug。4.4 字段元信息注册表与运算符白名单我把字段元信息注册表设计成一个核心配置文件。每个报表或模块注册自己允许查询的字段包括字段编码、数据库列名、字段类型、允许的运算符集合。前端通过接口拉取这个元信息来渲染查询条件UI后端也用它来校验和拦截非法字段请求。public class FieldMeta { private String fieldCode; // 前端传的字段名 private String columnName; // 数据库真实列名 private Class? valueType; // 值类型 private ListOperatorType allowedOperators; }这样设计后前后端对“可查字段”的定义完全一致。更重要的是当一个字段被禁用比如某些敏感字段不对低权限用户开放时后端直接移除该字段元信息前端UI也会同步消失权限控制非常自然。5. 与SpringBoot / MyBatis生态的集成实践5.1 在MyBatis XML中使用动态SQL很多项目用的是MyBatis习惯在XML里写SQL。条件编排器生成的SQL片段如何接入常见有两种方式。第一种生成好的SQL片段作为参数传给XML中的script标签用${whereSql}直接嵌入。这种做法注意必须保证whereSql已经做了全部参数绑定和字段白名单校验。示例select idqueryReport resultTypemap SELECT * FROM t_order where ${whereSql} /where /select注意MyBatis里${}是直接字符串替换安全性完全依赖应用层的白名单和参数校验。我通常只在内部受控接口中用这种方式对外接口会优先考虑第二种。第二种把解析好的参数列表作为方法参数在XML里用foreach等标签循环构建。但这要求前端提交的条件结构能被MyBatis的OGNL表达式直接解析实现起来更绕。实测下来Java代码直接生成SQL片段加参数列表反而比在XML里动态拼条件更清晰。因为MyBatis XML里做复杂嵌套逻辑判断OGNL表达式写出来可读性很差出bug也难排查。5.2 使用SelectProvider实现动态查询如果你用的是MyBatis而且想保持Java代码可测试性SelectProvider是很合适的选择。我在部分项目里这样用SelectProvider(type ReportSqlProvider.class, method buildQuerySql) ListMapString, Object queryReport(MapString, Object params);Provider方法负责组装SQL和返回参数public class ReportSqlProvider { public String buildQuerySql(MapString, Object params) { ConditionGroup conditionGroup (ConditionGroup) params.get(conditionGroup); ListObject args (ListObject) params.get(args); SqlParseResult result conditionParser.parse(conditionGroup, args); String sql SELECT * FROM t_order; if (result.isNotEmpty()) { sql WHERE result.getSql(); } params.put(args, args); return sql; } }这边有个坑Provider方法里修改args列表后需要确保MyBatis在执行时能拿到完整参数列表。我的做法是统一在Service层解析完毕再传入Mapper不在Provider里临时改参数这样参数顺序更容易控制。总之条件编排器生成SQL的逻辑要放在Service层做单元测试Provider里只做简单组装别把复杂逻辑堆在MyBatis层。5.3 事务与多数据源场景下的注意事项报表查询通常只读一般不需要事务但如果查询里包含“先查汇总再查明细”或“先写临时表再查询”的场景事务边界需要明确。我的建议是查询方法默认不开启事务只有在一个请求里包含多次写操作时才加Transactional避免长事务占用数据库连接。多数据源场景下条件树解析出来的字段名必须和当前数据源的方言匹配。比如MySQL的LIMIT分页和Oracle的ROWNUM分页写法完全不同。我通常会把分页语句剥离出来由不同数据源的查询器各自实现条件编排器只负责生成WHERE部分这样方言差异被隔离在分页层。5.4 单元测试用例如何设计条件编排器这种逻辑密集的组件必须配套完善的单元测试否则后续加条件很容易改坏旧逻辑。我设计测试用例时重点覆盖以下场景单层AND多条件验证SQL片段和参数顺序。单层OR多条件验证括号正确。两层嵌套A AND (B OR C)验证语义正确。三层嵌套验证深层递归解析无误。空值为空的叶子条件被跳过。IN条件值为空数组时被跳过。RANGE字段只填一个值时退化为单边条件。非法字段名/运算符被拦截抛出业务异常。特殊字符注入尝试验证参数未被拼接到SQL。实际测试中我发现参数顺序错乱是最高频的bug来源。所以我的断言里既验证SQL字符串也验证参数列表的顺序和值两者缺一不可。这块测试代码虽然写起来繁琐但后续每次重构都靠它在兜底。6. 性能优化与SQL执行效率6.1 让条件树为索引优化服务动态查询最大的性能挑战是用户随意组合条件索引可能无法命中。条件编排器能做的是“不添乱”比如保持字段名与索引列一致避免对索引列做函数包裹。举个例子如果下单时间字段create_time上有索引而用户传入的条件被解析成DATE(create_time) ?那就无法走索引。所以字段元信息注册表里的columnName应该直接存原始列名解析时不要对列做任何函数处理。值格式化放在参数一侧比如日期范围在传给数据库前已经由业务层统一格式化成标准时间字符串SQL里只保留create_time BETWEEN ? AND ?。另一个索引优化点如果字段元信息允许配置“高频组合”可以把常用条件组合对应的索引建出来报表模块上线前用慢查询日志分析实际使用的高频字段组合。我在一个订单报表项目中最终为(pay_status, create_time)和(amount, create_time)建了联合索引查询效率提升非常明显。6.2 分页与COUNT查询的一条龙优化报表查询经常需要返回总数和分页数据常见的做法是两条SQL一条SELECT COUNT(*)一条SELECT * ... LIMIT ...。条件编排器解析出的条件树可以同时复用只需在拼接时把SELECT部分替换掉即可。我在Service里封装了一个PageResultT queryPage(...)方法内部先执行COUNT查询再执行数据查询。小细节是如果COUNT大于某个阈值比如10万可以提示用户缩小条件范围避免超大分页深度带来的性能问题。OFFSET深翻页在MySQL里随着偏移增大性能骤降必要时可以用“上次最大ID”的方式做游标分页不过我一般只在管理后台用普通分页因为这个场景的查询深度往往可控。6.3 缓存条件解析结果的可行性条件树的解析本身是纯计算不依赖运行时数据只要输入相同解析结果就相同。因此理论上可以为高频相同条件做缓存避免每次都递归生成SQL。但现实是报表模块的条件组合几乎每次都不同缓存命中率很低反而增加维护复杂度。我更推荐缓存“字段元信息注册表”而不是“解析结果”。因为元信息表相对稳定可以在应用启动时加载到本地内存查询时直接读取避免频繁查数据库。字段权限变更可以通过刷新机制定期同步这样既提升性能又不会因为缓存条件树带来数据一致性问题。6.4 慢SQL监控与条件命中分析上线报表查询功能后强烈建议接一套慢SQL日志或监控看板。条件编排器的好处是SQL全是动态生成的慢SQL出现时可以通过日志直接反推用户提交的条件树定位是哪个条件组合导致SQL走偏了索引。我的做法是打印一条结构化日志包含请求ID、条件树JSON、最终SQL、参数列表、执行耗时。这个日志在排查性能问题时是救命稻草。有一次线上出现某个组合条件查询要3秒多我看了日志发现用户在时间范围字段上同时选了多个区间条件导致优化器选错执行计划。后来通过调整字段元信息里的运算符组合限制同一字段的重复条件问题才解决。7. 踩坑记录条件编排器的典型故障与排查手册7.1 运算符与值类型不匹配故障现象前端传了amount字段的运算符是GT值却是字符串abcSQL执行时报类型转换错误甚至更隐蔽的是值被隐式转换后查询结果不符合预期。排查思路解析器必须做类型校验不能把校验责任全部推给数据库。我在parseLeaf中根据字段元信息里的valueType用统一的类型转换工具将值转为目标类型转换失败直接抛出带业务提示的异常。字段元信息里还应该配置“是否允许空字符串”比如姓名允许模糊搜索空字符串但金额不允许。7.2 嵌套层级过深导致SQL解析溢出如果前端构造了特别深的嵌套条件递归解析可能导致栈溢出或者SQL长度超限。我在解析器入口加了层级限制比如最多5层嵌套超过则拒绝查询并提示用户简化条件。这个限制通常不会影响正常业务因为现实中很少有人用超过3层的条件组合。7.3 参数占位符顺序错乱这个坑我踩得最深。尤其在IN条件和RANGE条件混用的时候参数加入列表的顺序和占位符出现顺序很容易不一致。解决办法是三管齐下一是解析器统一使用同一个参数列表递归时不断追加二是解析完成后用PreparedStatement实际执行一遍来验证三是单元测试里严格断言参数列表顺序。另外MyBatis的${}字符串替换模式下参数列表无法自动对应占位符必须自己在Service层把参数传对。我最终采用JdbcTemplate params.toArray()的方式逻辑直白测试也好写。7.4 非法字段名与SQL关键词绕过即使字段名走白名单也要处理用户传入field为order_no DESC; DROP TABLE这种恶意串。白名单校验会直接拦截掉因为注册表里根本没有这个字段。但如果开发者图省事用了field直接拼接列名风险就完全敞开了。务必在解析前根据fieldCode映射到columnName并且columnName也是白名单维护的不动态取前端传入值。7.5 空条件导致WHERE子句异常如果条件树所有叶子都为空解析器返回空片段。Service层要判断如果结果为空查询就退化为“无WHERE条件”的全表查询。这个退化在报表场景有风险比如用户未填任何条件点击查询可能导致返回全表数据。我的建议是报表列表接口强制要求至少有一个非空条件如果确实允许全表查询至少加上数据权限过滤比如只能查属于当前用户的数据并在日志中记录这类查询以便审计。8. 扩展思路与后续优化方向条件编排器这套模式不止能用在校报表查询上。凡是涉及“动态筛选、多维组合、用户自定义视图”的功能都可以复用这套“条件树解析字段元信息动态SQL”的架构。我在后续项目中还做了几个增强条件模板化把常用条件树保存为模板用户可以一键加载比如“最近30天高价值订单”“上月流失客户”等。时间条件快捷项前端封装“今天、本周、本月、自定义范围”等快捷按钮后端只需支持RANGE运算符值填充由前端完成。条件与权限联动在解析条件树之前自动追加数据权限组比如只能查某个部门的数据这组条件是后端强制加入的用户不可见也不可修改。实现方式是在解析器入口预先合并一个“权限条件组”和用户条件组一起解析这样既能保证权限不越界又不会让权限逻辑散落在业务代码里。查询结果列自定义条件树只解决“行”的过滤报表通常还牵扯“列”的展示。下一步可以设计“列编排器”允许用户选择展示哪些列、调整排序通过列元信息白名单来控制。这套东西迭代到后期基本就是一个轻量级的报表查询中台了新接入一个报表只需要配置字段元信息代码层面几乎零改动。我个人在实际操作中的体会是条件编排器的核心价值不在于“动态SQL拼接”这个技术点本身而在于通过“条件树”这个中立数据结构把前端交互、后端解析、SQL生成、权限控制全部串起来形成了统一的表达模型。前期多花点时间把字段元信息和解析器设计清楚后面每接入一个新报表都能省下大把时间。如果你们项目里也有大量报表查询需求而且查询组合越来越复杂这套方案很值得尝试。
阅读完成 · 觉得有帮助?