积分系统是2c服务很重要的一部分,想运营好一个类似商城之类的网站,不断的调整积分活动可以增加用户的粘度。但是对于开发人员来说就比较痛苦了,积分规则复杂还好说,最麻烦是变化太频繁。
前言
比较常见的解决方式是使用规则引擎,很多很多年前用过 ILOG ,印象是比较重的一个软件,功能强大可以让业务人员来定义规则,但是用来对付积分系统,特别是笔者的项目不是一个真正的复杂的商城,应该用不上这么高射炮打蚊子。
再查了一下现在比较通用的 Java 规则引擎 Drools ,感觉还是比较复杂,需要学习和定义特定语法的规则文件,类似如下图:
我们的系统上线后,运营过程中不断的有积分规则变化的需求,我们不能不断的把 Java 项目重新编译发布生产环境。理想情况是把不断变化的规则定义和逻辑用自然语言的配置文件来描述,这样业务人员就可以去改了。
这个比较难,简化一点,不用自然语言配置文本,改用脚本语言 JavaScript 来描述,虽然不能完全由业务人员来改规则,但是可以由一般的开发人员来改,毕竟JavaScript简单而且会的人不要太多。另外就是改完规则,测试完直接就发布,不需要编译和重新发布。
补充一句,在 Java 中调用 JavaScript 以及在 JavaScript 里调用 Java 非常简单和方便。
设计
这样大概方案就是规则定义用 js 来描述,其他还是用 Java。整体设计如下图:
大概过程是:
- 业务活动,比如登录一次加积分,购买一个商品加一些积分等,都会触发赢取积分的服务。
- 赢取积分的服务并不会直接去计算积分,而是调用 JS 的规则定义函数去计算积分
- JS 在计算积分的过程中会不断的调用Java原生的方法,这些原生方法实现一些在积分计算中需要用到的辅助性功能
- 积分计算完成后,再调用Java的原生方法去修改数据库积分表。
实现
我们只实现一个登陆加积分的过程,比如在客户登陆成功后就会调用 earnPoint 函数来触发计算积分过程,必须传递一个规则名字 "login"及用户名和时间。
/**
* 赢取积分
* @param rule 积分规则名字
* @param user 用户名
* @param date 时间
* @throws ScriptException
* @throws NoSuchMethodException
*/
public void earnPoint(String rule, String user, Date date) throws ScriptException, NoSuchMethodException {
Invocable invocable = (Invocable) engine;
invocable.invokeFunction(rule, dateFormat.format(date), user);
}
以上代码主要就是根据规则名字,直接调用规则脚本里对应的 js 函数。
其中 engine 是加载了一个 rule.js 文件的 js 引擎,如下代码:
String rules = FileUtils.readFileToString(new File("./config/rules.js"));
ScriptEngineManager manager = new ScriptEngineManager();
engine = manager.getEngineByName("nashorn");
我们来看这个规则 js 文件的内容:
var ruleService = Java.type("d1.sample.gift.RuleService")
// 账号登录相关规则
// 1. 基本:每登录一次增加1分,一天最多5次
// 2. 特殊活动:10月1号-10月7号期间登录,每登录一次增加2分,一天最多20分
// 3. 特殊:注册时间是2017年的,登录不加分
function login(date, user) {
//Java.type("java.lang.System").out.println("debug"+date+user);
//1. 判断注册时间是否是2017年
if (ruleService.Instance().getRegistYear(user) == "2017") {
return {point: 0, desc: user + "在" + date + "登录,由于注册时间2017年,所以没有赚取积分"};
}
//2. 判断登录时间是否在特殊活动期间
if (ruleService.Instance().betweenDate(date, "2019-10-01", "2019-10-07")) {
if (ruleService.Instance().getTodayTotalPoint(user) >= 20) {
return {point: 0, desc: user + "在" + date + "登录,由于当天赚取积分已达到20分,所以没有赚取积分"};
}
return {point: 2, desc: user + "在" + date + "登录,赚取积分2"};
}
//3. 正常积分
if (ruleService.Instance().getTodayTotalPoint(user) >= 5) {
return {point: 0, desc: user + "在" + date + "登录,由于当天赚取积分已达到5分,所以没有赚取积分"};
}
return {point: 1, desc: user + "在" + date + "登录,赚取积分1"};
}
注释写的很详细,代码逻辑也很简单,其中对用户注册时间的查询以及对当前时间的判断都是调用 Java 提供的方法。
最后计算出积分后再写数据库和日志。
JSObject result = (JSObject) invocable.invokeFunction(rule, dateFormat.format(date), user);
//积分为0,只写日志不写数据库
Long point = ((Integer) result.getMember("point")).longValue();
String desc = (String) result.getMember("desc");
System.out.println("日志:" + desc);
if (point != 0) {
pointsService.addRulePoint(rule, user, point, date, desc);
}
示例系统模拟了2个用户不断的login,最后不断获取积分,由于代码里用了不少随机假的代码,所以积分结果会比较奇怪,先说明一下。
修改规则
如果在运营过程中需要修改规则,则只需要在后台管理界面里修改 rule.js, 然后推送到服务端,服务端的脚本引擎重新加载新的规则文件同时更新本地的 rule.js 文件。
以上设计和实现并不只限于积分系统,任何需要用到规则引擎的地方都可以参考这种方式。
源码下载参考Git
)