4. 跨字段校验

单字段校验做完了,数据里很多问题才会露出来。我遇到过这样一个项目:客户说“总金额明明等于单价×数量,怎么还缺了50多万?”一查,字段本身都合法,但跨字段的逻辑炸了。说白了,跨字段校验才是数据质量的真正照妖镜。

这一章我们只聊三类最常见的跨字段场景:依赖关系、业务规则、条件组合。掌握它们,你就能堵住90%的数据逻辑漏洞。

依赖关系校验

依赖关系就是:某字段的值决定了另一个字段必须存在或必须满足某个条件。比如下单接口里选了“公司客户”,那“公司税号”就不能为空。你想想看,这种场景电商、金融、ERP里比比皆是。

我个人习惯把依赖关系分为两类:

  • 存在性依赖:字段A有值,字段B必须有值(或必须为空)。
  • 值域依赖:字段A等于某个特定值时,字段B的值必须落在某个范围内。

我曾在一家物流公司的数据管道里碰到过一个经典案例:订单表的“配送方式”字段,如果选“冷藏运输”,那么“温度范围”字段必须填写且不能超过4°C。但线上代码只检查了“温度范围”是否为空,没检查数值范围。结果一批生鲜直接送成了常温……嗯,从那以后我写依赖校验都带着值域条件。

核心原则:写依赖校验前,先画一张字段依赖图,把每个字段的“前置条件”列清楚。别等上线了才靠报警发现。

下面是一个简单的依赖规则配置示例,用JSON描述:

{
  “依赖规则列表”: [
    {
      “规则ID”: “DEP001”,
      “描述”: “公司客户必须填写税号”,
      “类型”: “存在性依赖”,
      “条件”: “客户类型 == '公司'”,
      “受影响字段”: “公司税号”,
      “期望”: “非空且符合税号格式”
    },
    {
      “规则ID”: “DEP002”,
      “描述”: “冷藏配送温度范围不能超4°C”,
      “类型”: “值域依赖”,
      “条件”: “配送方式 == '冷藏'”,
      “受影响字段”: “温度范围”,
      “期望”: “数值 >= -10 且 <= 4”
    }
  ]
}

代码解析时,遍历规则列表,先评估条件字段,再校验受影响字段。注意循环引用?我建议给依赖图做一次拓扑排序,否则容易死循环。

业务规则校验

业务规则比依赖关系更“主观”——它不来自字段本身的逻辑,而来自业务流程的约束。比如“订单金额低于100元时,运费不能为0”,或者“库存不足时不允许下单”。这些规则往往需要查询其他表甚至服务。

我记得在做一个电商中台项目时,产品经理提了个规则:“会员等级是VIP3以上的用户,享受免运费,但免运费金额上限是2000元。”听起来简单?如果用户在即将达到VIP3的临界点上下单,系统要同时判断会员等级和订单总额。这就是典型的跨字段+跨表业务规则。

场景 条件字段 目标字段 业务含义
VIP免运费 会员等级、订单总额 运费金额 等级≥VIP3 且 总额≤2000 时运费=0
大额订单审核 订单金额、用户信用分 审核状态 金额>10000 且 信用分<600 需人工审核
促销叠加限制 促销类型、会员等级、商品类别 优惠金额 新用户专享券不能和满减券同时使用

做这类校验时,我建议把业务规则独立成规则引擎,不要散落在各个服务里。规则引擎可以支持热更新,业务方自己就能改配置。我们团队曾经用Drools,后来换成自研的轻量级表达式解析器,配合测试用例跑回归。

避坑指南:我曾经把一个业务规则写死在if-else里,结果每次上线都要发布代码。后来改成配置中心+脚本引擎,效率提升5倍。如果你的规则变化频繁,一定不要硬编码。

条件组合校验

条件组合就是多个字段一起决定一个结果,而且这些字段之间没有明显的依赖关系,但组合起来就有意义。举个最简单的例子:在注册页,“我已阅读并同意用户协议”这个勾选框,必须处于选中状态才能提交。这就是一个单条件。但多条件组合呢?比如“性别为男”且“年龄≥60”且“从事高危职业”时,必须签署风险告知书。

条件组合校验最容易出现的问题是组合爆炸。我遇到过一张表有8个布尔字段,每个字段可以组合出256种情况,产品经理只定义了其中的20多种有效组合。剩下的怎么处理?要么打回,要么按默认逻辑走。这里我通常用一个决策矩阵(或叫真值表)来管理。

用代码实现时,可以用决策表驱动校验:

# 决策表:条件组合 -> 期望结果
decision_table = {
    (True,  True,  True):  “必须签署风险告知书”,
    (True,  True,  False): “无需额外操作”,
    (True,  False, True):  “无需额外操作”,
    (True,  False, False): “无需额外操作”,
    (False, True,  True):  “无需额外操作”,
    (False, True,  False): “无需额外操作”,
    (False, False, True):  “无需额外操作”,
    (False, False, False): “无需额外操作”,
}

def validate_condition_combinations(row):
    key = (row['性别为男'], row['年龄≥60'], row['高危职业'])
    if key in decision_table:
        result = decision_table[key]
        if result == “必须签署风险告知书”:
            if not row['已签署风险告知书']:
                raise ValidationError(“请签署风险告知书”)
    else:
        # 未覆盖的组合,走默认
        pass
警告:条件组合校验如果覆盖不全,会出现“用户提交了数据,但后台报了一个莫名其妙的错”的情况。建议用PICT(Pairwise Independent Combinatorial Testing)工具生成最小测试用例集,确保每个组合至少被测试一次。

三种校验的关系,我画了一张图帮你理清脉络:

跨字段校验三类核心模式 原始多字段数据 依赖关系校验 存在性 / 值域依赖 业务规则校验 流程约束、跨表逻辑 条件组合校验 多字段决策矩阵 综合校验结果:通过 / 拒绝(附错误码) 三种校验可组合运行,按优先级顺序执行

我个人的落地经验是:先跑依赖校验,再跑业务规则,最后跑条件组合。因为依赖校验失败的数据,后续校验基本没意义,提前拒绝能节省计算资源。你也别把三种校验写成一个巨无霸函数,拆成三个独立的校验器,每个只做一件事,可读性和维护性都好很多。

最后说一个让我印象深刻的教训。某次数据迁移,我漏掉了条件组合中的一种特殊情况——用户同时勾选了“到付”和“月结”,系统没有定义这个组合的校验规则,结果财务对账时发现两笔重复收款。从那以后,我每条新增组合规则都会强制要求写至少一个正例和反例的单元测试。