我在开发跨境物流系统中,遇到的一个真实场景是:
下单时要按目的国、重量、体积、货值自动匹配承运商(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}");
}
默认情况下,多个输入会按顺序命名为 input1、input2…;用 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在多家公司生产环境验证,业务规则改了又改,零更新、零发版;教会客户自己配置,开发可以做到零介入。