4. 规则定义语言设计:JSON Schema 定义规则

规则引擎的核心,说白了就是「怎么把业务语言翻译成机器能懂的结构」。我见过不少团队,一开始用 Excel 管理规则,后来用 YAML,最后都逃不过 JSON Schema。为什么?因为 JSON Schema 既能做校验,又能做文档,还能直接对接规则引擎的解析器。

我个人习惯把规则定义分成三层:元信息层(谁、什么时候、什么类型)、逻辑层(表达式怎么算)、动作层(触发了怎么办)。今天我们先聊前两层。

4.1 规则结构:骨架要稳

先看一个完整的规则定义长什么样。我在项目中踩过最大的坑,就是规则结构设计得太灵活——灵活到解析器都不知道该怎么处理。所以,我建议你一开始就定死结构。

{
  "ruleId": "DQ-001",
  "ruleName": "订单金额非负校验",
  "ruleType": "FIELD_CHECK",
  "description": "确保订单金额字段不为负数",
  "version": "1.0.0",
  "status": "ACTIVE",
  "expression": {
    "operator": "gte",
    "field": "order_amount",
    "value": 0
  },
  "threshold": {
    "type": "COUNT",
    "operator": "lte",
    "value": 10
  },
  "severity": "CRITICAL",
  "dimension": "COMPLETENESS",
  "dataSource": "order_db",
  "schedule": "0 0 2 * * ?"
}

嗯,这里要注意:ruleId 一定要有命名规范。我见过有人用自增 ID,结果规则一多,根本不知道 DQ-1024 是干什么的。我建议用「数据域-序号」的格式,比如 ORDER-001、USER-023。

核心字段说明:

  • ruleId:全局唯一,建议带业务前缀
  • ruleType:决定表达式怎么解析,常见类型有 FIELD_CHECK(字段级)、RECORD_CHECK(记录级)、CROSS_TABLE(跨表)、STATISTICAL(统计)
  • severity:严重级别,直接影响告警渠道和响应速度
  • threshold:阈值,不是所有规则都需要,但统计类规则必须有

4.2 规则类型:分而治之

规则类型决定了表达式的语法和校验逻辑。我把它分成四大类,你想想看,是不是覆盖了 90% 的数据质量场景?

规则类型 适用场景 示例
FIELD_CHECK 单字段值校验 年龄在 0-150 之间
RECORD_CHECK 记录内多字段关系 开始日期 ≤ 结束日期
CROSS_TABLE 表与表之间一致性 订单表的用户 ID 必须在用户表中存在
STATISTICAL 聚合结果异常检测 今日订单量相比昨日波动不超过 20%

我曾经在做一个金融项目时,把 CROSS_TABLE 和 FIELD_CHECK 混在一起定义,结果表达式解析器写得像意大利面条。后来我强制要求:每种规则类型对应一个独立的解析器实现,代码瞬间清爽了。

4.3 规则表达式语法设计:核心中的核心

表达式是规则引擎的大脑。我见过最糟糕的设计,是把表达式写成字符串,然后用 eval 执行——嗯,你懂的,那是灾难。我推荐用嵌套的 JSON 结构来表达逻辑,既安全又好解析。

4.3.1 基本操作符

{
  "operator": "and",
  "operands": [
    {
      "operator": "gte",
      "field": "age",
      "value": 18
    },
    {
      "operator": "lte",
      "field": "age",
      "value": 60
    }
  ]
}

这个结构表达的是:age >= 18 AND age <= 60。每个操作符都是一个节点,可以无限嵌套。我建议你只支持有限的操作符集合,别想着支持所有 SQL 操作符——没必要。

我建议的操作符清单:

  • 比较类:eq, neq, gt, gte, lt, lte
  • 逻辑类:and, or, not
  • 集合类:in, not_in, between
  • 空值类:is_null, is_not_null
  • 字符串类:contains, starts_with, ends_with, regex

4.3.2 字段引用与函数

规则里经常需要引用字段、调用函数。我设计了一套引用语法:

{
  "operator": "gte",
  "field": "order_amount",
  "value": {
    "function": "avg",
    "field": "order_amount",
    "window": "last_30_days"
  }
}

这个表达的意思是:当前订单金额 ≥ 过去 30 天的平均订单金额。函数调用也是 JSON 结构,而不是字符串拼接。为什么?因为这样解析器可以直接递归处理,不需要写复杂的字符串解析逻辑。

避坑指南:我曾经在表达式里支持了自定义函数,结果业务方写了一个死循环函数,直接把规则引擎跑挂了。后来我加了两个限制:

  1. 函数必须有超时机制(默认 5 秒)
  2. 函数不能修改外部状态(纯函数)

4.4 阈值与严重级别:告警的开关

阈值不是所有规则都需要,但一旦需要,它决定了「这条规则到底算不算触发」。我见过有人把阈值和表达式混在一起,结果逻辑变得特别拧巴。

我建议把阈值独立出来,专门处理「量」的概念:

{
  "threshold": {
    "type": "RATE",        // COUNT | RATE | VALUE
    "operator": "gte",     // 触发条件
    "value": 0.05,         // 5% 的错误率
    "window": "1h"         // 统计窗口
  }
}

严重级别我一般分四级,对应不同的响应策略:

级别 颜色 响应方式 示例
CRITICAL 红色 立即告警 + 阻断流程 主键重复、金额为负
HIGH 橙色 立即告警 + 人工确认 字段缺失率 > 10%
MEDIUM 黄色 定时汇总告警 格式不规范
LOW 蓝色 记录日志,周报体现 轻微偏差

我个人习惯把 CRITICAL 和 HIGH 的规则数量控制在 20% 以内。为什么?因为告警太多,团队会麻木。你想想看,如果每天收到 100 条 CRITICAL 告警,最后大家连看都不看了。

4.5 规则定义的整体架构

下面这张图,是我在项目中沉淀下来的规则定义架构。它把规则拆成了四个层次,每一层只关心自己的事。

规则定义语言架构 元信息层 ruleId | ruleName | ruleType | version | status | dataSource | schedule 表达式层 操作符嵌套 | 字段引用 | 函数调用 | 常量值 示例:{ operator: "and", operands: [...] } 阈值层 type (COUNT/RATE/VALUE) | operator | value | window 动作层 severity | 告警渠道 | 阻断策略 | 补偿动作

你看,从上到下,信息越来越具体。元信息层告诉引擎「这条规则是谁」;表达式层告诉引擎「怎么算」;阈值层告诉引擎「算到什么程度算触发」;动作层告诉引擎「触发了怎么办」。每一层都可以独立升级,互不影响。

我的经验:在设计规则定义语言时,一定要先写 JSON Schema 校验文件。这样规则一提交,就能自动校验结构对不对,而不是等到运行时才发现。我一般用 https://json-schema.org/draft/2020-12/schema 这个版本,支持条件校验和递归结构。