金融产品规则引擎从重构到不断迭代优化,围绕着宗旨就是如何设计更灵活更有扩展性的方案,最终设计面向产品运营研发测试多角色多层次用户群体,因此平台更低的学习成本和易用性成为我们不断努力的目标,我们为实现这一目标在重构优化的历程中不断努力。
温馨提示:全文共计3800余字,乃上班地铁途中码字编写,实属不易,感谢关注。起草于2020年11月23日,终稿于2020年11月28日。
车金融前世今生专辑连载系列,关注微信公众号【秋夜无霜】
文章目录
一、导读
金融产品平台业务规则从大类划分可以分为金融方案费用规则、业务场景前后置规则、资金方准入规则三大类,这三大类规则构成车金融业务线金融产品的规则引擎核心设计。金融方案费用规则为金融产品提供更灵活更有扩展性的金融方案奠定基础,而业务场景前后置规则是为了解决特定场景的业务拒绝与反算推演,进而弥补金融方案费用规则的不足,这两者关系相辅相成,共同铸造金融产品规则引擎的核心。同时,资金方准入规则是金融方案从资方维度更进一步的规则抽象集合,是金融方案差异化的共性规则特征集,这三者为支撑公司业务发展奠定核心基础。
金融产品前置规则从最初的存储过程实现,然后改造成Java实现,最终在金融产品系统重新设计实现。后置规则是为了像前置规则那样提供给产品运营人员统一标准化的用户交互,从审核系统迁移到金融产品平台。本文主要介绍车金融业务线金融产品前后置规则引擎的发展历程,以及相关技术方案的实现,通过本文希望你可以收获启迪,以帮助此刻道路上的你获得灵感。
本文将讲述这三大类规则中的前后置规则,通过讲述历程发展以及介绍技术方案设计,为您呈现背后的核心细节。
二、存储过程的时代
入职初期,我所在的车金融业务线的信贷研发团队有13个处理不同业务场景的存储过程,其中一个存储过程提供RR规则(前置规则)业务处理,存储过程基于服务器cron定时去调度执行调用,筛选符合状态的订单然后批量跑批处理,通过若干条规则验证后,执行通过或拒绝处理。
后来我负责金融产品,参与一个资方业务需求,需要调整前置规则。此刻我清晰地记得,我们把调整后的存储过程脚本在测试环境验证后,上线时回归验证发现执行跑批任务处理异常。鉴于当时的客观条件,对于MYSQL存储过程异常我们后端研发无法跟踪排查,我们不得退而求其次在后置规则增加处理,毕竟后置规则是在Java服务端实现,就这样勉强上线也能满足业务需求。
后来,老板也认识到存储过程的种种弊端,认为这样的技术栈对于后续业务发展肯定是站不住脚的,因此研发团队必须要客服一切困难在短期进行技术重构的。
鉴于老板赋予的使命,我带着团队从此开始13个存储过程的陆续技术改造,通过Java语言重新实现(搭建一个SpringBoot应用lyqc-sieg),然后一个个陆续研发测试上线,从而彻底消灭了存储过程,开启了金融产品团队重构的新的一大里程碑。
三、实时处理的必要性
2018年下半年,车金融业务从整体流程在进行相关优化效能的提升,考虑到订单准入审批时效,期望准入环节就可以知道是否通过,由于当日进件翌日跑批才能出结果,进而要求我们这个前置规则是否可以由定时任务跑批,改造成实时调用并立即返回准入结果。
由于最初和前置规则一起的跑批还有另外两个(重复性检验和渠道状态审批),这三个跑批是有顺序性和关联性,倘若其中一个跑批不通过的话,另外一个就不能进行跑批,这种时序性依赖使得我们想要优化,对于时间性能优化达到一个瓶颈点。
后来我们跟老板以及产品多次沟通此事宜,发现把其中一个耗时最长的存储过程(重复性检验)可以业务流程后置,无需准入环节执行跑批。这样以来,剩下的两个存储过程通过接口业务逻辑合并,通过消息机制通知,可以实现即时处理,同时保留定时任务,对于消息丢失场景或者业务逻辑处理异常时可以再次定时跑批。
四、规则配置化的趋势
这次技术优化,进一步提高了业务流程审批时效,但是前置规则技术架构设计依然有优化空间。由于业务逻辑处理对订单相关数据的依赖性,使得我们每次业务需求涉及到前置规则调整,依然需要修改现有业务逻辑代码。
2018年下半年,老板也多次提及我们金融产品团队,前置规则是否可以实现配置化,交付给产品运营同学使用,不需要每次需求迭代必须修改代码就可以完成需求上线呢?
我们通过技术调研,发现是可以尝试做到的。我们需要进一步明确规则引擎的系统职责,对于业务线各业务系统的场景使用,它只关注于场景规则特征定义以及接口输入参数定义。业务方根据场景选择根据我们规则引擎规范定义输入参数,然后调用规则引擎服务执行规则检验并返回结果。在技术方案设计中,我们把前置规则检验接口所需参数,参数编码,参数类型等一系列元数据定义,然后接口传入所需合法类型参数,通过这样的数据流程定义,就可以实现前置规则的配置化和上线免发布。
技术方案设计中,我们基于Google Aviator这个开源的计算引擎包扩展了一系列自定义函数,以支撑我们的业务场景需要。譬如,多个数字求和,本身这个包对于累计求和的相关数字没有传值,则会运行异常。因此我们进行了扩展兼容,参数没有传值时,我们视为零处理。
对于金融行业,数字在进位方式和取整当时同样业务场景颇多,我们也扩展自定义了相关函数,辅助我们的产品运营同学使用。
在考虑到产品运营同学的使用及学习成本时,在金融产品平台优化了功能交互,降低他们的学习成本,避免他们配置出错。
四、后置规则的必然迁移
后置规则最初在审核系统维护,最初的技术实现无非就是使用大量的if else条件语句控制,相对于最初的前置规则,优势在于毕竟是Java语言实现,出现问题也容易排查,需求变更更容易扩展。
自前置规则完成迁移到金融产品系平台进行运营化配置后,期间我们与产品运营同学多次沟通交流,对平台功能交互进行多次优化,使得他们逐渐熟练使用相关功能,他们期望后置规则也能像前置规则这样他们可以自主运营,需求一次次提需求,再研发上线,周期长,无法支撑业务政策的调整响应。
尽管后置规则吸收了前置规则的配置化技术方案设计思路,期间进行过一次技术重构,以进一步适应业务发展趋势。金融产品系统提供引擎包,提供给审核团队使用,这样的局面维持了相当一段时间。
后来,我们金融产品团队进一步考虑到,既然审核系统已经依赖了金融产品引擎包,为何不直接把后置规则的配置也移植到金融产品平台,审核系统只调用接口就行了。因此,我们经过沟通讨论,愿意帮助产品运营同学实现这一愿望,决定从审核系统迁移过来后置规则,同时复用前置规则的表结构以及引擎逻辑处理模块,最终实现后置规则对于产品同学的可视化与运营化。
五、技术方案的核心设计
我们在实现前置规则配置化的设计时,由于这是属于我们技术团队自研设计,因此可以最可能发挥我们的创新优势,以彰显我们的思维脑洞。坦白讲,我比较喜欢自主创新项目,无拘无束,没有羁绊。
前置规则有三种执行策略:删除、取消、拒绝。事实上,我们重构后,仅仅只会用到拒绝这种业务场景策略。
前置规则的业务场景需求逻辑,简言之,就是订单主贷人信息或者车辆相关信息,在满足什么条件前置下,不满足什么规则特征条件,执行规则策略。然而,若干条规则,有时候命中不仅仅是一条,而是多条,但多条执行策略到底该返回哪一条,这时候其实需要规则优先级或权重,进而解决这种业务场景。
事实上讲,我们的业务场景也只有拒绝场景,其他场景无需前置规则处理,从业务流程讲风控规则会前置处理,所以我们只关注准入环节的业务逻辑检验。
这样以来,我们不用考虑优先级的概念,接口内部设计就是从数据库取出所有的规则集合,然后循环逐条检验,倘若不满足,直接跳出循环返回规则提示文案。如果满足,则继续下一条检验。倘若某一条,出现运行时异常,比如规则表达式语法解析错误,或者零作为除数等,这时候则忽略该异常,继续循环走下去。
我们接口内部设计时,在解析规则出现异常时,也提供了几种策略模式实现,这一点设计上,吸收了Java线程池的拒绝策略设计,以满足我们业务场景的扩展需求。
然而,尽管我们提供了若干策略机制,实际上却用不到,但是提供了扩展性。我们从接口设计上,遵循一个基本原则是,不会因为产品运营配置规则导致的运行时异常,在业务方调用时,影响调用结果,所以直接简单处理方案就是异常时忽略即可。
参数元数据定义
我们把参数元数据定义通过配置中心配置,示例如下:
[{
"fieldName": "appCode",
"fieldType": "String",
"fieldDesc": "订单号",
"mustExpression": "true",
"belongGroup": [
"baseInfo"
]
},
{
"fieldName": "carLoanAmount",
"fieldType": "Number",
"fieldDesc": "车贷金额",
"mustExpression": "true",
"belongGroup": []
},
{
"fieldName": "loanRate",
"fieldType": "Number",
"fieldDesc": "贷款利率",
"mustExpression": "true",
"belongGroup": []
},
{
"fieldName": "evaCarPriceChange",
"fieldType": "Number",
"fieldDesc": "二手车调整价",
"mustExpression": "isOld != nil && string.contains(isOld,'1')",
"belongGroup": []
}
]
平台交互功能
接口设计
对外api接口
@RestController
@RequestMapping("/position")
public class PositionController{
@Resource
private PositionFacadeProvider positionFacadeProvider;
@ApiOperation("前置规则执行接口")
@PostMapping("/processPrePosition")
public Result<RuleCheckRe> processPrePosition(@RequestBody Map<String,Object> map){
return this.positionFacadeProvider.processPrePosition(map);
}
@ApiOperation("后置规则执行接口")
@PostMapping("/processPostPosition")
public Result<RuleCheckRe> processPostPosition(@RequestBody Map<String,Object> map) {
return this.positionFacadeProvider.processPostPosition(map);
}
}
从上述我们可以看到,接口输入参数是一个Map,也就是一个JSON对象,key为参数,value为参数值,但是参数以及参数值必须遵守配置约束。
核心代码
AbstractPositionRuleHandler
核心方法 ruleCheck
/**
* 外部调用,执行器入口
* @param context
*/
public void ruleCheck(PositionRuleContext context){
try {
// 1. 对象初始化
init(context);
// 2. 参数校验
boolean paramCheck = paramCheck();
if(!paramCheck) {
return;
}
context.setAuth(true);
// 3. 加载检查规则
loadCheckRule();
if(CollectionsTools.isEmpty(ruleList)) {
ruleCheckRe.setMessage("没有需要校验的规则");
return;
}
// 4. 规则校验
PositionContext positionContext = PositionContext.builder().strategy(PositionStrategy.THROWS_EXCEPTION).env(params).ruleList(ruleList).ruleCheckRe(ruleCheckRe).classifyEnum(classifyEnum).build();
AbstractRuleCheckExecutor executor = new PositionExecutor(positionContext);
executor.execute();
} catch (Exception e) {
log.error("{} 执行器入口调用异常, context={}", LOG_TITLE, JSON.toJSONString(context), e);
ruleCheckRe.setAccess(false);
ruleCheckRe.setMessage(MessageFormat.format("{0}系统异常", LOG_TITLE));
ruleCheckRe.setErrorRuleDesc(JSON.toJSONString(context));
context.setMessage(LOG_TITLE + "执行异常");
} finally {
context.setRuleCheckRe(ruleCheckRe);
saveLog();
}
}
核心方法 checkParamsTypeLegal
/**
* 判断所有入参类型是否合法
* @param params
* @param checkParamsEntity
*/
private boolean checkParamsTypeLegal(Map<String, Object> params, List<AutoPositionEntity> checkParamsEntity) {
if(CollectionsTools.isEmpty(checkParamsEntity)){
return true;
}
for(AutoPositionEntity entity : checkParamsEntity) {
//获取校验规范类型
PositionParamTypeEnum positionParamTypeEnum = PositionParamTypeEnum.getByName(entity.getFieldType());
if(positionParamTypeEnum == null) {
context.setMessage(LOG_TITLE + "参数配置有误,请联系技术支持");
return false;
}
Object paramsValue = params.get(entity.getFieldName());
AviatorContext ctx = AviatorContext.builder().expression(entity.getMustExpression()).env(params).build();
try {
boolean must = AviatorExecutor.executeBoolean(ctx);
if(must && paramsValue == null) {
context.setMessage(MessageFormat.format("请求参数:{0},不能为空", entity.getFieldName()));
return false;
}
} catch (Exception e) {
context.setMessage(LOG_TITLE + "字段必传属性配置异常,请联系技术支持");
log.error("{} , appCode:{}", LOG_TITLE, appCode, e);
return false;
}
if(paramsValue != null) {
String paramsType = paramsValue.getClass().getTypeName();
boolean checkType = positionParamTypeEnum.check(paramsValue);
if(!checkType) {
context.setMessage(MessageFormat.format("{0} 请求参数不合法,参数名称:{1}, 入参类型:{2}, 校验类型:{3}", LOG_TITLE, entity.getFieldName(), paramsType, positionParamTypeEnum));
return false;
}
// 需要转换一下,计算引擎如果是整数数值,会有向下取整问题,转换将会装换为小数形式,String 类型可以不做转换,此处为了统一
params.put(entity.getFieldName(), positionParamTypeEnum.convert(paramsValue));
}
}
return true;
}
AbstractRuleCheckExecutor
核心方法 call
/**
* 调用处理
*/
protected void call() {
AutoApprConstant.OpIdnEnum opIdnEnum;
try {
for (AutoApprRule ruleEntity : ruleList) {
this.executeRule = false;
this.ruleEntity = ruleEntity;
List<AutoApprRuleProp> propList = ruleEntity.getProps();
for (AutoApprRuleProp propEntity : propList) {
try {
boolean currentMatch;
opIdnEnum = AutoApprConstant.OpIdnEnum.getByName(propEntity.getOpIdn());
if (Objects.equals(AutoApprConstant.PropFieldExtendEnum.EXPRESSION.getName(), propEntity.getPropCode())) {
currentMatch = access(propEntity.getPropValue());
} else {
AutoApprConstant.PropFieldTypeEnum valueTypeEnum = AutoApprConstant.PropFieldTypeEnum.getByIndex(propEntity.getPropValueType());
currentMatch = OpSymbols.builder().env(env).opIdnEnum(opIdnEnum).valueTypeEnum(valueTypeEnum)
.propCode(propEntity.getPropCode()).propValue(propEntity.getPropValue()).build().access();
}
if (!currentMatch) {
executeRule = false;
break;
} else {
executeRule = true;
}
} catch (Exception e) {
errorRuleDesc.add(MessageFormat.format("规则[{0}]属性配置错误", ruleEntity.getRuleId()));
executeRule = false;
strategyHandle.handle(e, this, ruleEntity);
break;
}
}
boolean complete = after();
if(complete) {
return;
}
}
if(!ruleCheckRe.isAccess()) {
ruleCheckRe.setCalculateResult(calculateResult);
if(AutoApprConstant.BelongNameEnum.getByIndex(ruleCheckRe.getBelongType()) == AutoApprConstant.BelongNameEnum.COMPUTE && CollectionsTools.isEmpty(calculateResult)) {
ruleCheckRe.setBelongType(AutoApprConstant.BelongNameEnum.CHECK.getIndex());
}
}
ruleCheckRe.setErrorRuleDesc(errorRuleDesc.stream().collect(Collectors.joining(",")));
} catch (PositionStrategyException e) {
log.error("{} 异常, ruleIdList={} ", LOG_TITLE, JSON.toJSONString(errorRuleDesc), e);
ruleCheckRe.setAccess(false);
ruleCheckRe.setErrorRuleDesc(errorRuleDesc.stream().collect(Collectors.joining(",")));
ruleCheckRe.setBelongType(AutoApprConstant.BelongNameEnum.CHECK.getIndex());
ruleCheckRe.setMessage(e.getMessage());
}
}
核心方法 after
/**
* 后置处理
*/
protected boolean after() throws PositionStrategyException {
AutoApprConstant.BelongNameEnum belongNameEnum = Optional.ofNullable(AutoApprConstant.BelongNameEnum.getByName(ruleEntity.getBelongName()))
.orElseThrow(() -> new PositionStrategyException(MessageFormat.format("规则[{0}]类型配置错误,规则类型={1}", ruleEntity.getRuleId(), ruleEntity.getBelongName())));
if (executeRule) {
ruleCheckRe.setAccess(false);
ruleCheckRe.setRuleId(ruleEntity.getRuleId());
ruleCheckRe.setBelongType(belongNameEnum.getIndex());
switch (belongNameEnum) {
case CHECK:
ruleCheckRe.setMessage(ruleEntity.getMsgTemplate());
return true;
case COMPUTE:
calculateResult = Optional.ofNullable(calculateResult).orElse(Maps.newHashMapWithExpectedSize(10));
AutoApprFormulaBo autoApprFormulaBo = AutoApprFormulaBo.builder().ruleId(ruleEntity.getRuleId())
.type(AutoApprFormulaTypeEnum.BACK_CALCULATION.getIndex()).params(env).messageTemplate(ruleEntity.getMsgTemplate()).classifyEnum(classifyEnum).build();
List<RuleCalculateRe> calculateReList = autoApprFormulaComponent.calculateResult(autoApprFormulaBo);
if(CollectionsTools.isNotEmpty(calculateReList)) {
for(RuleCalculateRe calculateRe : calculateReList){
RuleCalculateRe ruleCalculateRe = calculateResult.get(calculateRe.getFieldName());
BigDecimal calculateReValue = calculateRe.getValue();
if(ruleCalculateRe == null || calculateReValue.compareTo(ruleCalculateRe.getValue()) < 0) {
ruleCheckRe.setMessage(ruleEntity.getMsgTemplate());
calculateResult.put(calculateRe.getFieldName(), calculateRe);
}
}
} else {
if(StringTools.isEmpty(ruleCheckRe.getMessage())) {
ruleCheckRe.setMessage(ruleEntity.getMsgTemplate());
}
errorRuleDesc.add(MessageFormat.format("规则[{0}]缺少反算配置", ruleEntity.getRuleId()));
}
return false;
default:
throw new PositionStrategyException(MessageFormat.format("规则[{0}]类型配置错误,规则类型={1}", ruleEntity.getRuleId(), belongNameEnum.getName()));
}
}
return false;
}
PositionStrategyHandle
PositionStrategyHandle
public interface PositionStrategyHandle {
/**
* 处理
* @param throwable 异常
* @param executor 执行器对象
* @param ruleEntity 规则实体对象
*/
void handle(Throwable throwable, AbstractRuleCheckExecutor executor, AutoApprRule ruleEntity) throws PositionStrategyException;
}
ThrowsExceptionStrategyHandle
/**
* @description: 多个规则执行时,如果当前规则执行出现运行时异常,则抛出该异常
* @Date : 2018/12/12 下午2:11
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
public class ThrowsExceptionStrategyHandle implements PositionStrategyHandle {
final String pattern = "规则[{0}]内部错误,规则名称={1}";
@Override
public void handle(Throwable throwable, AbstractRuleCheckExecutor executor, AutoApprRule ruleEntity) throws PositionStrategyException {
String message = MessageFormat.format(pattern, ruleEntity.getRuleId(), ruleEntity.getRuleName());
log.error("{0},ruleEntity={1}", message, JSON.toJSON(ruleEntity), throwable);
throw new PositionStrategyException(message, throwable);
}
}
StrictExecuteStrategyHandle
/**
* @description: 严谨执行所有规则
* @Date : 2018/12/12 下午2:11
* @Author : 石冬冬-Seig Heil
*/
public class StrictExecuteStrategyHandle extends ThrowsExceptionStrategyHandle implements PositionStrategyHandle {
@Override
public void handle(Throwable throwable, AbstractRuleCheckExecutor executor, AutoApprRule ruleEntity) throws PositionStrategyException {
super.handle(throwable,executor,ruleEntity);
}
}
IgnoreExceptionStrategyHandle
/**
* @description: 多个规则执行时,如果当前规则执行出现运行时异常,则忽略继续执行
* @Date : 2018/12/12 下午2:11
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
public class IgnoreExceptionStrategyHandle implements PositionStrategyHandle {
@Override
public void handle(Throwable throwable, AbstractRuleCheckExecutor executor, AutoApprRule ruleEntity) {
log.warn("规则支持执行警告,规则将忽略继续执行,ruleEntity={0}", JSON.toJSON(ruleEntity),throwable);
}
}
PositionStrategyHandleFactory
/**
* @description: 置入规则策略处理接口工厂类
* @Date : 2018/12/11 下午4:08
* @Author : 石冬冬-Seig Heil
*/
public final class PositionStrategyHandleFactory {
/**
* 工厂方法
* @param strategy
* @return
*/
public static PositionStrategyHandle create(PositionStrategy strategy){
switch (strategy){
case STRICT_EXECUTE:
return new StrictExecuteStrategyHandle();
case IGNORE_EXCEPTION:
return new IgnoreExceptionStrategyHandle();
case THROWS_EXCEPTION:
return new ThrowsExceptionStrategyHandle();
default:
throw new IllegalArgumentException("UNKNOWN strategy="+strategy.toString());
}
}
}
六、后置规则的反算机制
后来随着业务场景的发展,为了改善用户交互上的体验度,我们又研发设计出反算机制。
反算坦白讲,是为了解决,倘若若干输入参数如果命中一条配置,我们期望不是返回提示信息,而是引导用户调整相关输入项的大小,以达到最终满足业务场景规则,其宗旨就是让你的业务流程友好的走下去。
七、尾语
经过几次重构后,实现前置规则与后置规则在金融产品平台的配置化。我们不仅解放了研发人员,他们不用再面向存储过程或SQL开发;更方便了产品人员,他们可以快速支持业务调整。我们实现金融产品的前后置规则配置化,可视化,这是一个质的变化。