非全自研視覺化表達引擎-RuleLinK

2023-08-31 18:00:43

 說在前面

工作中經常會遇到這樣的場景:

  •  幫忙把小貝拉門店 商品金額在5w以內,產康訂單最多95折。

  • 幫忙把聖貝拉門店 開業時間在6個月內,折扣低於7折要發起審批

  • 幫忙把寧波太平洋店設定獨立合同模板

  • 幫忙把節假日成本變成1000

  • ...

有些三五月變一次,有些一月變一次,有些一週變幾次,每次修改都是去巴拉程式碼,不勝其煩。

系統中的規則分散在各各PRD,有些人腦袋裡也存了一份,但是隨著人員的迭代,需求的迭代,最新永遠散落在各初的程式碼中。這會帶來諸多問題:

  • 學習成本高

  • 維護成本高

  • 開發成本高

  • 測試成本高

於是有了建立一個規則的管理與運營平臺,以及支撐規則的解釋與執行的框架的想法。用以消除樣板式程式碼,解放生產力,讓規則的變化變得的簡單。

 

RuleLink的定位

RuleLink是一款視覺化表示式引擎。致力於解決業務開發過程中,規則變化成本高, 規則管理分散,維護成本高,學習成本高,大量樣板式程式碼等一系列的問題。

RuleLink名稱的由來

用規則連線業務邏輯,讓研發專注業務邏輯,讓規則的設定變得的簡單。

RuleLink 是基於 Aviatorscript  實現 規則的解析,基於開源專案 "rule-engine-builder-ui"實現表示式生成與渲染。(我只是程式碼搬運工,所以取名非全自研)

基於當下我們的業務規則量,RuleLink並未使用效能更好的 Rete演演算法,而是使用了傳統的模式匹配。

 

為什麼要建RuleLink

除了前面介紹的之外,另一個原因則是想彌補一下上一份的工作中的一點點小遺憾。

視覺化表示式引擎的建設有兩個難點:

  1. 表示式的解釋與執行

  2. 表示式的生成與解析

第1點相對簡單,於是在1月份時,搗鼓了一些基礎程式碼,做了一些嘗試,當時還因為部署Drools 的workbench 搞得停服20分鐘,拿了人生第一C績效。

到4月份接到這樣一個需求,某業務線不同訂單訂單需要接不同的聚合支付賬號。因為原來就換過一次了,為了支援快速切換,我們開始在Q1的程式碼基礎做了一些調整,開始有了RuleLink的雛形。後來又陸續接入了一些場景:

  • 新業務支付支援不同主體

  • 訂單折扣設定

  • ...

更多的場景的接入,讓視覺化設定的需求變得比較必要。於是開始正式著手構建RuleLink,解決這一類的問題。

整體結構

目前主要使用Aviatorscript 解析表示式,並支援SpE

 

這是從其他複製的一張圖(忘記出處),因為和自己的場景幾乎一模一樣,就直接參照了。

 

儲存模型

 

Rule_Scene 規則場景

定義場景,目前場景是固定,現在只支援支付,未來有新場景再加入

RuleFactObj 事實物件

RuleFactObjfield 事實物件欄位

定義場景下的事實欄位,主要用於將來前端可視覺化操作。

RuleBase 規則庫

定義規則,目前只支援表示式(未來考慮支援 特定指令碼,比如groovy),目前只是定義規則命中返回 簡單或者複雜資料型別,未來可以考慮執行某個執行(action)。

 

 

日期格式處理

日期格式原來的實現是SpEL的方式,這會引發一些問題,所以修改了其原始碼並重新編譯生成支援Aviatorscript 支援的自定義函數方式

 1 /**
 2  * @Author: jijunjian
 3  * @CreateTime: 2023-08-25  17:52
 4  * @Description: 自定義函數初始化
 5  */
 6 @Component
 7 @Slf4j
 8 public class CustomFunctionInitializer implements ApplicationContextAware {
 9 
10     @Override
11     public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
12         log.info("初始化自定義函數");
13         AviatorEvaluator.addFunction(new StringToTimestamp());
14         log.info("初始化自定義函數完成");
15     }
16 
17     /**
18      * 自定義函數
19      * 時間字串轉時間戳
20      */
21     class StringToTimestamp extends AbstractFunction {
22         @Override
23         public AviatorObject call(Map<String, Object> env,
24                                   AviatorObject arg1, AviatorObject arg2) {
25             String timeString = FunctionUtils.getStringValue(arg1, env);
26             String format = FunctionUtils.getStringValue(arg2, env);
27             // 10位時間戳
28             long timeStamp = DateUtil.parse(timeString, format).getTime()/1000;
29             return  AviatorLong.valueOf(timeStamp);
30         }
31 
32         @Override
33         public String getName() {
34             return "string_to_timestamp";
35         }
36     }
37 }

 

 

表示式解析

使用Aviatorscript解析比較簡單,直接上程式碼

 1   /**
 2      * 根據表示式執行
 3      * @author: jijunjian
 4      * @param factObj
 5      * @param expression
 6      * @return
 7      */
 8     @Override
 9     public boolean fire(Object factObj, String expression){
10         Map<String,Object> fact = new HashMap<>();
11         fact.put("data",factObj);
12         // 對於有字串的表示式,需要先編譯(並快取,減少生成的臨時類)
13         Expression compiledExpression = AviatorEvaluator.compile(expression,true);
14         log.info("開始執行表示式:{}, fact:{}", expression, JSONUtil.toJsonStr(factObj));
15         Boolean flag = (Boolean) compiledExpression.execute(fact);
16         log.info("開始執行表示式:{}, fact:{}, result:{}", expression, JSONUtil.toJsonStr(factObj), flag);
17 
18         return flag;
19     }
20 點選並拖拽以移動

 

互動介面

 

 

寫在最後

雖然現在的版本距離真正讓運營同學能直接用起來了,可能還有一定的距離。比如各種列舉支援選項,門店等動態資料支援選項,返回結果支援動態渲染和選擇等都還不不具備。但是1.0版本比原來寫死,甚至nacos設定已經強上許多了。畢竟還是和兩個小夥伴擠壓業餘時間開發,實為不易,於是來一次簡單的聚餐,於是我們預定了未來每次大的版本升級,都來一次

 

 

 

上一份工作時,就有要構建一個簡單易用的規則引擎,一直沒能實現,有些許遺憾,今天算是給補上了。

微信:jijunjian

成為一名優秀的程式設計師!