• Node.js躬行记(15)——活动规则引擎


      在日常的业务开发中,会包含许多的业务规则,一般就是用if-else硬编码的方式实现,这样就会增加逻辑的维护成本,若无注释,可能都无法理解规则意图。

      因为一旦规则有所改变,那么就需要修改代码再发布代码,而在日常的开发中唯一不变的就是变化,修改规则是很常见的。

      规则引擎的作用就是将决策逻辑从业务逻辑中抽离出来,使得两者可以独立于彼此,便于集中管理,减少硬编码的成本和风险,在不重启服务的情况下快速响应需求的变化。

      规则本质上就是一个函数,包括n个输入(决策因子),一个输出(结果)和一段计算规则三部分。

    decision = rule(factor1, factor2, …, factorn)

      计算规则包含LHS(Left Hand Side,条件分支逻辑)和RHS(Right Hand Side,执行逻辑)。换句话说就是如果XXX(规则),那么XXX(动作)。

      LHS比较容易实现,就是判断条件的组合,包括常规表达式(算数运算、关系运算等),简单规则(数组索引等),业务定制规则(观看直播时间等)。

      而RHS就比较复杂了,场景众多,可以是简单的算数运,也可以是单表查询,甚至是几张表的数据聚合,并且它的抽象程度直接会左右规则引擎的受众人员。

      如果设计的规则引擎是给产品或运营,那么就不能加入过多的编程概念,给他们用的应该是比较傻瓜的那种。

      如果是给开发人员用的,那么可以设计的更加自定义,并且还能添加编程语句进来。

      增加了编程性,就降低了可用性;增加了可用性,就降低了扩展性。在权衡后,决定先封装已经出现的执行逻辑,做成可配置的。

      例如有个活动规则,如果观看30分钟,那么赠送3天会员,其中30和3就是可配置的参数。这样就能保持一定程度的可扩展。

    一、界面

      与产品沟通后,让她给出些规则,在看到她的文档后,大大超出我的预期。

      她先分成了两个角色:主播和观众,然后根据这两种角色来设计动作,例如观众 - 观看直播 - 时长满XX。

      

      她还给出了统计粒度,分天、周、月和自定义,这也是我之前的盲点,奖励形式就是会员和兑换币。

      经过她的拆解后,我界面的设计也明朗了。顺带便,将之前打榜活动的规则也移植到该配置系统中。

      

      在此界面中,规则和奖励都是可以多条的,运算符就是大于、小于、等于等。

      规则关系就是与和或,由于不想实现太复杂,所以就降低了操作友好度,得手写关系。满足这层关系后,才会发放奖励或执行结果。

    二、Node.js

      核心逻辑就是运行规则,发放奖励,这些配置信息都存储在MongoDB中。

      首先根据名称找到这条配置,然后先解析统计粒度,按日、周、月或自定义,时间库采用了moment.js

    getInterval(type, start, end) {
      const date = {};
      switch (type) {
        case 1: //每日
          date.start = moment().startOf("day");
          date.end = moment().endOf("day");
          break;
        case 2: //每周
          date.start = moment().startOf("isoWeek");
          date.end = moment().endOf("isoWeek");
          break;
        case 3: //每月
          date.start = moment().startOf("month");
          date.end = moment().endOf("month");
          break;
        default:
          //自定义
          date.start = moment(start);
          date.end = moment(end);
          break;
      }
      return {
        start: date.start.format("YYYY-MM-DD HH:mm:ss"),
        end: date.end.format("YYYY-MM-DD HH:mm:ss")
      };
    }

      然后是遍历规则,每条规则会对应不同的方法,未来扩展就是扩展这些规则方法,得到的结果再由运算符计算。

    caculate(left, operator, right) {
      const hash = {
        lt: left < right,
        lte: left <= right,
        gt: left > right,
        gte: left >= right,
        equal: left == right,
        notEqual: left != right,
        allEqual: left === right,
        notAllEqual: left !== right
      };
      return hash[operator];
    }

      接着将规则关系中的数字替换成那几个运算结果,得到嘴周的规则结果。

    let expression;     //规则表达式结果,可能是布尔值,也可能是其他类型的值
    if (!row.relation) {
      expression = operators[1];
    } else {
      // 将匹配的数字替换成规则结果值
      expression = row.relation.replace(
        /(\d+)/g,
        function (match, p1, index, input) {
          return operators[match];
        }
      );
      expression = eval(expression);   //执行字符串代码
    }

      最后发放奖励,方法中包含Switch分支,未来就是完成这些分支中的逻辑。

    async giveRewards(params, type, value, project) {
      switch (type) {
        case "vip":  //会员
          break;
        case "gold":    //兑换币
          break;
        case "letter": //站内信
          break;
      }
    }

      完整的执行规则的逻辑如下所示。

    async runRule({ name, params }) {
      const row = await this.models.WebRule.findOne({ name });
      if (!row) return false;
      const { project } = row;   //项目类型
      const date = this.getInterval(row.statis_type, row.rule_start, row.rule_end);
      const operators = {};     //运算符
      // 遍历规则
      for (let i = 0; i < row.rules.length; i++) {
        const rule = row.rules[i];
        // 得到方法值
        const result = await this[rule.role[1]](params, date, project, rule.value);
        // 计算规则值
        operators[i + 1] = this.caculate(result, rule.operator, rule.value);
      }
      let expression;     //规则表达式结果,可能是布尔值,也可能是其他类型的值
      if (!row.relation) {
        expression = operators[1];
      } else {
        // 将匹配的数字替换成规则结果值
        expression = row.relation.replace(
          /(\d+)/g,
          function (match, p1, index, input) {
            return operators[match];
          }
        );
        expression = eval(expression);   //执行字符串代码
      }
      if (!expression) {
        return false;
      }
      // 发放奖励
      for (const data of row.awards) {
        await this.giveRewards(params, data.award[2], data.value, project);
      }
      return expression;
    }

    参考资料:

    从0到1:构建强大且易用的规则引擎

    手把手搭建业务规则引擎 Rule Engine

    规则引擎基础知识

    URule Pro

    规则引擎

    从产品角度看物联网平台的规则引擎

    复杂风控场景下,如何打造一款高效的规则引擎

    动手撸一个规则引擎(二):方案解析

  • 相关阅读:
    netcore跨域问题与接口请求错误400
    JAVA基础
    Codeforces Round #738 (Div. 2) E.Mocha and Stars 容斥+DP
    Codeforces Round #737 (Div. 2) D. Ezzat and Grid DP+线段树优化
    牛客多校第四场 H.Convolution 数论 推式子
    51Nod-1514 美妙的序列 多项式求逆模板题
    牛客多校第4场 G.Product EGF解决排列问题
    上帝与集合的正确做法 拓展欧拉定理 欧拉函数性质
    [Codeforces Round #736 (Div. 2)](https://codeforces.com/contest/1549) E.F1 题解
    洛谷P7725 珍珠帝王蟹 分类讨论
  • 原文地址:https://www.cnblogs.com/strick/p/15604092.html
Copyright © 2020-2023  润新知