3. 策略模型设计:策略的定义、条件、动作与优先级

好,咱们进入策略引擎的核心地带——策略模型设计。

说白了,策略引擎就是个“如果…那么…”的机器。你给它一堆条件,它帮你匹配,然后执行对应的动作。但实际落地时,这里面的门道可不少。我最早做规则引擎时,以为就是写几个if-else,结果被生产环境教做人。

今天咱们就把策略的四个核心要素拆开揉碎:策略定义、条件、动作、优先级与冲突解决

3.1 策略的定义:一个完整的策略长什么样?

先看一个最简单的策略定义。我个人习惯用JSON或YAML来描述策略,因为可读性好,也方便动态加载。

{
  "strategyId": "S001",
  "name": "新用户首单折扣",
  "description": "注册7天内且首次下单的用户,享受8折优惠",
  "priority": 10,
  "conditions": [
    { "field": "user.registrationDays", "operator": "<=", "value": 7 },
    { "field": "order.isFirstOrder", "operator": "==", "value": true }
  ],
  "actions": [
    { "type": "applyDiscount", "params": { "rate": 0.8 } }
  ],
  "status": "active"
}

你看,一个策略包含几个关键字段:

  • strategyId:唯一标识,别用自增ID,用UUID或业务编码。我踩过坑,自增ID在分布式环境下容易冲突。
  • name / description:给人看的,方便排查。
  • priority:优先级,后面细说。
  • conditions:条件列表,多个条件之间默认是“与”关系。
  • actions:动作列表,按顺序执行。
  • status:控制策略是否启用。
我的习惯: 策略定义里最好加一个 version 字段。线上策略迭代快,没版本号你根本不知道哪个策略在生效。

3.2 策略的条件(Condition):如何灵活表达业务规则?

条件,就是策略的“触发器”。它决定了策略在什么场景下被激活。

我见过很多团队把条件写死在代码里,结果每次改规则都要发版。这其实违背了策略引擎的初衷——动态决策

3.2.1 条件的基本结构

一个条件通常由三部分组成:

  • 字段(field):比如用户年龄、订单金额、设备类型。
  • 操作符(operator):等于、大于、包含、在范围内等。
  • 值(value):比较的目标值。

举个例子:

// 条件:订单金额大于100元
{ "field": "order.amount", "operator": ">", "value": 100 }

// 条件:用户所在城市在列表中
{ "field": "user.city", "operator": "in", "value": ["北京", "上海", "深圳"] }

// 条件:用户标签包含"VIP"
{ "field": "user.tags", "operator": "contains", "value": "VIP" }

3.2.2 复合条件:与、或、非

实际业务中,条件往往是组合的。比如“新用户 首单 金额大于50”。

我建议用树形结构来表达复合条件:

{
  "logic": "AND",
  "conditions": [
    { "field": "user.registrationDays", "operator": "<=", "value": 7 },
    {
      "logic": "OR",
      "conditions": [
        { "field": "order.amount", "operator": ">", "value": 100 },
        { "field": "order.itemCount", "operator": ">=", "value": 3 }
      ]
    }
  ]
}

嗯,这里要注意:不要支持太深的嵌套。我曾经见过一个策略嵌套了7层,维护的人想死的心都有。建议限制在3层以内。

避坑指南: 条件中的字段名一定要和事件数据中的字段名严格对应。我遇到过因为大小写不一致(比如 "userName" vs "username"),导致策略永远匹配不上的情况。建议统一用下划线命名法,并在加载时做校验。

3.3 策略的动作(Action):匹配后做什么?

条件匹配了,就该执行动作了。动作是策略的“输出”。

动作的设计,我总结为三种类型:

动作类型 说明 示例
计算型 修改某个值或计算结果 打折、积分计算、价格调整
通知型 触发外部通知或消息 发送短信、推送、邮件
流程型 改变业务流转方向 转人工审核、拒绝订单、标记风控

动作的定义也很简单:

{
  "actions": [
    { "type": "applyDiscount", "params": { "rate": 0.8, "scope": "order" } },
    { "type": "sendNotification", "params": { "channel": "sms", "template": "welcome" } }
  ]
}

我个人建议:动作要支持幂等性。为什么?因为策略引擎可能会重试。如果同一个动作被执行两次,结果不能变。比如“发送短信”这种动作,一定要有去重机制。

3.4 策略的优先级与冲突解决

这是策略引擎里最头疼的部分。多个策略同时匹配,到底听谁的?

3.4.1 优先级怎么定?

我见过两种主流做法:

  • 数值优先级:给每个策略一个整数,数字越小优先级越高(或相反)。简单粗暴,但容易冲突。
  • 层级优先级:比如“全局策略 > 分组策略 > 个人策略”。这种更符合业务直觉。

我推荐混合使用:先按层级过滤,再按数值排序。

// 优先级计算示例
function getEffectivePriority(strategy) {
  let base = 0;
  if (strategy.level === 'global') base = 1000;
  else if (strategy.level === 'group') base = 500;
  else if (strategy.level === 'personal') base = 100;
  
  return base + strategy.priority; // priority 范围 0-99
}

3.4.2 冲突解决的三种策略

当两个策略都匹配,且动作冲突时(比如一个说打8折,一个说打9折),怎么办?

  1. 最高优先级胜出:只执行优先级最高的那个策略。简单,但可能丢失信息。
  2. 合并动作:把多个策略的动作合并执行。比如折扣取最低价,积分取最高值。这需要定义合并规则。
  3. 加权投票:每个策略投一票,按权重计算最终结果。适合风控场景。

我在项目中用得最多的是第一种。因为简单、可预测。但如果你做的是风控引擎,建议用加权投票。

核心原则: 冲突解决策略必须在设计阶段就定好,并且对业务方透明。我曾经见过一个项目,上线后才发现两个策略互相覆盖,导致用户同时收到了“恭喜你获得优惠”和“抱歉你不符合条件”两条矛盾的通知。那场面,尴尬到脚趾抠地。

3.5 策略模型的核心逻辑图

下面这张图,是我对策略模型设计的一个总结。它展示了从事件输入到策略匹配,再到动作执行的完整链路。

策略模型核心流程 事件输入 条件匹配 (与/或/非 组合) 优先级排序 (层级 + 数值) 冲突解决 (胜出/合并) 动作执行 (计算/通知/流程) 策略库 策略定义 + 条件 + 动作 动态加载 / 热更新 图:策略模型从事件输入到动作执行的完整链路 条件:灵活组合,支持与/或/非 优先级:层级 + 数值,避免冲突 动作:幂等设计,支持重试

3.6 小结

策略模型设计,说白了就是三件事:

  • 定义清楚:策略长什么样,字段怎么设计。
  • 条件灵活:支持组合、嵌套,但别太深。
  • 冲突可控:优先级和冲突解决策略,提前定好规则。

你想想看,如果这些基础没打好,后面做动态加载、性能优化、可视化编排,全都是空中楼阁。我见过太多团队一上来就搞高大上的可视化界面,结果底层策略模型一塌糊涂,最后全返工。

嗯,先把地基夯实了,后面的事就顺了。


公众号:蓝海资料掘金营,微信deep3321