4. 规则模型设计:元数据、版本与优先级

规则模型是整个数据质量体系的骨架。骨架歪了,后面填再多肉也白搭。我这些年见过不少团队,一上来就写规则逻辑,结果规则多了以后,查都查不过来——哪个规则生效?哪个规则过期?规则之间打架怎么办?全乱套。

所以,咱们先把模型设计清楚。说白了,就是给每条规则办一张「身份证」,再管好它的「版本履历」和「执行顺序」。

4.1 规则元数据模型

每条规则,都得有自己的一套元数据。我习惯把它拆成五个核心字段:rule_id、rule_name、rule_type、condition、action。这五个字段,缺一个,规则就跑不顺畅。

字段 类型 说明 示例
rule_id string 全局唯一标识,建议用 UUID 或雪花算法生成 r_20250321_001
rule_name string 人类可读的名称,用于日志和告警 用户手机号格式校验
rule_type enum 规则分类:格式校验、空值检查、范围约束、唯一性等 format
condition json 触发条件表达式,支持嵌套逻辑 {"field":"phone","regex":"^1[3-9]\\d{9}$"}
action json 命中规则后的处理行为:拒绝、告警、替换、标记 {"type":"reject","message":"手机号格式错误"}

核心原则:condition 和 action 必须解耦。condition 只负责「判断」,action 只负责「执行」。我在项目中见过有人把判断逻辑写死在 action 里,结果想改个提示语都得改代码——千万别这么干。

举个例子。假设我们要校验用户注册接口里的手机号:

{
  "rule_id": "r_20250321_001",
  "rule_name": "手机号格式校验",
  "rule_type": "format",
  "condition": {
    "field": "phone",
    "operator": "regex_match",
    "value": "^1[3-9]\\d{9}$"
  },
  "action": {
    "type": "reject",
    "http_status": 400,
    "error_code": "INVALID_PHONE",
    "message": "手机号格式不正确,请检查后重试"
  }
}

你看,condition 只描述「什么情况下触发」,action 只描述「触发了怎么办」。这样设计,规则引擎就能通用化——不管什么接口,只要套上这套模型,都能跑。

4.2 规则版本管理

规则不是写一次就完事的。业务变了、字段改了、格式调整了,规则就得跟着变。但你不能直接覆盖旧规则——万一新规则有 bug,你连回滚的机会都没有。

我建议用语义化版本号来管理规则版本。格式很简单:major.minor.patch。

  • major:规则逻辑发生重大变更,比如从「拒绝」改成「告警」
  • minor:新增条件或调整阈值,比如放宽正则表达式
  • patch:修复 bug 或优化提示语,不影响判断逻辑

每个版本都存一份快照,包括完整的元数据。我习惯用时间戳 + 版本号作为版本标识:

{
  "rule_id": "r_20250321_001",
  "version": "2.1.0",
  "version_timestamp": "2025-03-21T14:30:00Z",
  "rule_name": "手机号格式校验",
  "rule_type": "format",
  "condition": { ... },
  "action": { ... },
  "changelog": "放宽对虚拟运营商号段的支持,新增 170、171 号段"
}

小技巧:版本快照建议存到独立的版本表里,别和当前生效的规则混在一起。查询当前规则时只查最新版本,回滚时直接从版本表里捞旧版本覆盖回去。我有个项目就是这么干的,上线两年,回滚过三次,每次都在 30 秒内搞定。

版本管理的另一个好处是灰度发布。你可以先让新规则在测试环境跑几天,确认没问题了再推到生产。万一出问题,切回旧版本就行。

4.3 规则优先级设计

规则多了,冲突是必然的。比如一条规则说「手机号必须 11 位」,另一条说「手机号不能以 0 开头」。两条都命中,先执行哪条?

优先级设计就是为了解决这个问题的。我一般用数字越小优先级越高的方式:

优先级 规则类型 典型规则
1 安全校验 SQL 注入检测、XSS 过滤
2 格式校验 手机号、邮箱、身份证格式
3 业务约束 金额范围、日期先后顺序
4 数据增强 默认值填充、字段格式化

为什么要这么排?你想想看,安全校验必须最先执行——如果数据里带了一段恶意脚本,后面的格式校验和业务约束根本没必要跑。格式校验次之,因为格式不对,业务约束也没意义。最后才是数据增强,比如给缺失的字段补默认值。

注意:优先级相同的规则,按 rule_id 的字典序执行。但我不建议依赖这个顺序——如果两条规则有依赖关系,应该显式设置不同的优先级,而不是靠排序碰运气。我曾经踩过这个坑,两条优先级相同的规则,一个先执行替换,一个后执行校验,结果替换后的数据把校验搞崩了。后来我强制要求:有依赖关系的规则,优先级必须差至少 1。

优先级信息直接嵌入规则元数据里:

{
  "rule_id": "r_20250321_001",
  "priority": 2,
  "rule_name": "手机号格式校验",
  "condition": { ... },
  "action": { ... }
}

规则引擎加载时,先按优先级排序,再按 rule_id 排序。这样执行顺序就确定了,不会出现「这次跑和上次跑结果不一样」的诡异问题。

4.4 整体模型结构图

说了这么多,咱们用一张图把整个模型串起来。这张图展示了规则从定义到执行的完整链路:

规则元数据模型结构图 规则元数据模型结构 规则元数据 (Rule Metadata) rule_id rule_name rule_type condition action 版本管理 (Version) 优先级 (Priority) 规则执行引擎 执行结果:通过 / 拒绝 / 告警 / 替换 设计要点 • condition 和 action 必须解耦,各自独立维护 • 版本管理支持灰度发布和快速回滚 • 优先级决定执行顺序,有依赖关系的规则必须显式设置不同优先级

从图里能看出来,规则元数据是核心,版本管理和优先级是它的两个「增强属性」。三者一起喂给规则执行引擎,引擎按优先级排序、按版本加载,最后输出执行结果。

4.5 落地建议

模型设计好了,怎么落地?我给出几个实操建议:

  • 元数据存数据库,版本存历史表:当前生效的规则放主表,历史版本放版本表。查询性能好,回滚也方便。
  • 优先级用数字,别用字符串:数字排序快,而且扩展性好。你想想,如果以后要插一条优先级在 2 和 3 之间的规则,数字 2.5 就能搞定,字符串就麻烦了。
  • 版本号自动递增:每次修改规则,系统自动生成新版本号,不允许手动指定。避免人为错误。
  • 规则变更走审批流程:尤其是生产环境的规则,改之前最好有人 review 一下。我见过有人把正则写错了,导致所有合法手机号都被拒绝——那场面,啧啧。

我的习惯:每次上线新规则,我都会先在测试环境跑一遍全量历史数据,看看有没有误杀。确认没问题了,再灰度推送到生产。这个流程虽然慢一点,但稳。数据质量这件事,稳比快重要。

好了,规则模型设计这块就聊到这儿。模型搭好了,后面写规则引擎、嵌入 API 层,都是水到渠成的事。