我在开发跨境物流系统中,遇到的一个真实场景是:

下单时要按目的国、重量、体积、货值自动匹配承运商(UPS/Fedex/DHL等)和附加费(旺季附加费、偏远地区、超尺寸等);淡旺季费率、可达分区经常变——难道把判断写死在代码里,业务每调一次规则就发一次版

上述场景每次靠开发改if-else、再发版明显都不合理;它们的本质都是:数学公式+逻辑符号组成逻辑表达式,而且会频繁修改;更科学的做法是:规则外置,运行时读取、动态执行。

实际生产环境做法:

这张图我写的Demo,用不同承运商的超尺寸规则配置来举例

这是我参与开发商业物流项目中的常规做法,适用于任何动态配置业务规则场景;文章最后会用Demo讲述具体操作。

上图核心使用的是微软开源规则引擎——RulesEngine,用JSON配置/数据库/ Redis(任何存储都可以)存规则,用熟悉的C#表达式写条件,实测性能比其它第三方包高很多,解析非常快;这样业务人员/非技术人员改配置即可,不必重新编译发布。

NuGet:RulesEngine
仓库:https://github.com/microsoft/RulesEngine
文档:https://microsoft.github.io/RulesEngine

它是怎么工作的?这里引用微软官方提供的流程图,一看就明白:


快速上手

安装

dotnet add package RulesEngine

用JSON定义规则

以包裹尺寸是否超限为例,把承运商规则写成工作流:

[
  {
    "WorkflowName": "CarrierLimit",
    "Rules": [
      {
        "RuleName": "UpsOversize",
        "SuccessEvent": "UPS_OVERSIZE",
        "ErrorMessage": "未命中 UPS 超限规则",
        "RuleExpressionType": "LambdaExpression",
        "Expression": "pkg.carrier == \"UPS\" AND (pkg.length > 108 OR pkg.weight > 70)"
      },
      {
        "RuleName": "FedexOversize",
        "SuccessEvent": "FEDEX_OVERSIZE",
        "ErrorMessage": "未命中 FedEx 超限规则",
        "RuleExpressionType": "LambdaExpression",
        "Expression": "pkg.carrier == \"FedEx\" AND (pkg.length > 165 OR pkg.weight > 68)"
      }
    ]
  }
]

字段含义:

字段 含义
WorkflowName 一组规则的命名空间,执行时按它查找
RuleName 单条规则名
Expression C# Lambda 风格表达式(支持 AND / OR
SuccessEvent 规则命中后可取出的事件标识

加载表达式并执行

using System.Text.Json;
using RulesEngine.Models;

// 反序列化规则
var json = await File.ReadAllTextAsync("carrier-limit.json");
var workflows = JsonSerializer.Deserialize<List<Workflow>>(json)!;

// 创建引擎
var engine = new RulesEngine.RulesEngine(workflows.ToArray());

// 准备输入(可用实体、匿名对象、dynamic)
var pkg = new RuleParameter("pkg", new
{
    carrier = "UPS",
    length = 120m,
    weight = 50m
});

// 执行指定 Workflow
var results = await engine.ExecuteAllRulesAsync("CarrierLimit", pkg);

foreach (var r in results)
{
    Console.WriteLine($"{r.Rule.RuleName} => {r.IsSuccess}");
    if (r.IsSuccess)
        Console.WriteLine($"事件: {r.Rule.SuccessEvent}");
}

默认情况下,多个输入会按顺序命名为 input1input2…;用 RuleParameter 指定名字后,表达式里就能写 pkg.xxx,可读性更好。


高级用法

业务规则通常不止一条表达式,RulesEngine还提供嵌套规则、参数别名、自定义类型注入等能力

嵌套规则(And / Or)

父规则可以不写 Expression,而是用Operator+子规则组合判断。例如设备预警:温度过高 卡壳次数超标,任一命中即告警:

{
  "WorkflowName": "DeviceAlarm",
  "Rules": [
    {
      "RuleName": "NeedAlarm",
      "Operator": "Or",
      "Rules": [
        {
          "RuleName": "HighTemperature",
          "Expression": "device.temperature >= 40"
        },
        {
          "RuleName": "TooManyJams",
          "Expression": "device.jamCount > 10"
        }
      ]
    }
  ]
}

Operator 支持 And / Or。若既要完整诊断树,又要性能,可通过 ReSettings.NestedRuleExecutionMode 控制:

  • All:子规则全部执行(默认,便于排查)
  • Performance:短路(And 遇失败即停,Or 遇成功即停)

ScopedParams:把长表达式拆开

规则一长就难维护。可以用 GlobalParams(整个 Workflow 可用)和 LocalParams(当前规则及其子规则可用)做中间变量,类似 C# 的局部变量:

{
  "WorkflowName": "CarrierLimitAdvanced",
  "GlobalParams": [
    {
      "Name": "isUps",
      "Expression": "pkg.carrier == \"UPS\""
    }
  ],
  "Rules": [
    {
      "RuleName": "UpsPeakSeasonLimit",
      "LocalParams": [
        {
          "Name": "overLength",
          "Expression": "pkg.length > 108"
        },
        {
          "Name": "overWeight",
          "Expression": "pkg.weight > 70"
        }
      ],
      "Expression": "isUps AND pkg.season == \"Peak\" AND (overLength OR overWeight)"
    }
  ]
}

ScopedParams 之间还可以引用:前面定义的参数,后面的参数表达式里可以直接用,适合把复杂判断拆成多步。

自定义类型增强

Lambda 再强,也盖不住所有领域逻辑。通过 ReSettings.CustomTypes 注入静态工具类,表达式里就能直接调用:

public static class RuleUtils
{
    public static bool InList(string value, string csv)
    {
        if (string.IsNullOrWhiteSpace(value) || string.IsNullOrWhiteSpace(csv))
            return false;

        return csv.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
                  .Contains(value, StringComparer.OrdinalIgnoreCase);
    }
}

var settings = new ReSettings
{
    CustomTypes = new[] { typeof(RuleUtils) }
};

var engine = new RulesEngine.RulesEngine(workflows.ToArray(), settings);
{
  "Expression": "RuleUtils.InList(pkg.carrier, \"UPS,FedEx,DHL\") AND pkg.weight > 50"
}

ReSettings常用参数

配置 默认 说明
CustomTypes 注入到表达式中的自定义类型
EnableScopedParams true 是否启用 Global/Local Params
IsExpressionCaseSensitive false 表达式是否大小写敏感
IgnoreException false 是否吞掉规则编译/执行异常
NestedRuleExecutionMode All 嵌套规则执行策略
UseFastExpressionCompiler true 是否用 FastExpressionCompiler 加速编译

物流超尺寸规则配置Demo

开篇第一个问题——承运商超尺寸规则常变——用这个Demo就能落地:规则进MySQL(实际运用要加缓存),业务用中文属性拼表达式,下单试算禁止下单并提示触发什么规则。

  • 规则存在biz_rule,即改即生效
  • 页面配置点击选择的业务属性是中文,对非技术人员友好

原理

业务表达式:件计费重 > 票实重
        ↓  中文 → Variable
引擎表达式:PieceChargeableWeight > TicketActualWeight
        ↓  RuleExpressionParser / RulesEngine
      true / false → 命中则「禁止下单:超尺寸:{规则名}」

固定映射(页面中文对应引擎变量):

中文 引擎变量 含义
票实重 TicketActualWeight 各箱件实重之和
票计费重 TicketChargeableWeight 各箱件计费重之和
件实重 PieceActualWeight 当前箱实重
件计费重 PieceChargeableWeight Max(件实重, 体积重)
长 / 宽 / 高 Length / Width / Height 当前箱尺寸(cm)

计费公式(按kg/cm举例,惯例材积率6000):体积重 = 长 × 宽 × 高 ÷ 6000件计费重 = Max(件实重, 体积重);多件时按件评估(实际可在页面配置按票或按件),任一件命中即整票命中。

最小实体

[SugarTable("biz_rule")]
public class BizRule
{
    public string Name { get; set; } = null!;       // 唯一,如「UPS 体积重超限」
    public string? Description { get; set; }
    public string Expression { get; set; } = null!; // 中文:件计费重 > 票实重
    public bool IsEnabled { get; set; } = true;
    public int Sort { get; set; }
}

保存时把表达式先校验,校验成功后存入;现在Unicode编码,中文表达式直接入库没问题,已在生产环境上线验证。

执行过程:中文 -> 变量 -> 执行

// 固定字段元数据(同时给前端chips用)
public static readonly FieldMetaDto[] Fields = [
    new() { Label = "票实重", Variable = "TicketActualWeight", Unit = "kg" },
    new() { Label = "票计费重", Variable = "TicketChargeableWeight", Unit = "kg" },
    new() { Label = "件实重", Variable = "PieceActualWeight", Unit = "kg" },
    new() { Label = "件计费重", Variable = "PieceChargeableWeight", Unit = "kg" },
    new() { Label = "长", Variable = "Length", Unit = "cm" },
    new() { Label = "宽", Variable = "Width", Unit = "cm" },
    new() { Label = "高", Variable = "Height", Unit = "cm" },
];

// 执行前:中文替换成变量,再交给 RuleExpressionParser
var engineExpr = ToEngineExpression("件计费重 > 票实重");
// → PieceChargeableWeight > TicketActualWeight
parser.Evaluate<bool>(engineExpr, ruleParameters);

执行方法:

public static bool Evaluate(string businessExpression, PackageFacts facts)
{
	var engineExpr = ToEngineExpression(businessExpression);
	var parser = new RuleExpressionParser(new ReSettings());
	var parameters = new[]
	{
		new RuleParameter("TicketActualWeight", facts.TicketActualWeight),
		new RuleParameter("TicketChargeableWeight", facts.TicketChargeableWeight),
		new RuleParameter("PieceActualWeight", facts.PieceActualWeight),
		new RuleParameter("PieceChargeableWeight", facts.PieceChargeableWeight),
		new RuleParameter("Length", facts.Length),
		new RuleParameter("Width", facts.Width),
		new RuleParameter("Height", facts.Height)
	};
	return parser.Evaluate<bool>(engineExpr, parameters);
}

页面操作

技术栈:ASP.NET Core WebAPI + MySQL + React。页面分三块——新建规则、规则列表、多箱试算,如图:

点中文属性拼表达式,支持 + - * / ()、比较、/,最终组成逻辑表达式

命中提示:

这样业务员一眼就能看懂

禁止下单: 超尺寸:UPS 体积重超限(规则名称)

不要把垃圾表达式写进库,一定要提前校验表达式:

  • 分词后只允许固定中文业务属性、数字、运算符、/
  • 两个属性相邻(件计费重票实重)直接拒绝——中间必须有运算符
  • 可以干跑一次 Evaluate,语法报错在保存阶段就拦掉

RulesEngine在多家公司生产环境验证,业务规则改了又改,零更新、零发版;教会客户自己配置,开发可以做到零介入。