Node.js躬行記(15)——活動規則引擎
- 2022 年 2 月 14 日
- 筆記
- Node.js躬行記
在日常的業務開發中,會包含許多的業務規則,一般就是用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; }
參考資料: