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

規則引擎

從產品角度看物聯網平台的規則引擎

複雜風控場景下,如何打造一款高效的規則引擎

動手擼一個規則引擎(二):方案解析