金融产品费用规则相对于资金方维度和金融方案维度来讲,是金融产品系统中最小颗粒度单元,它为实现车金融产品运营多样化和差异化提供决策落地支撑,构成了金融产品金融方案试算的最核心部分,为支撑业务场景快速发展以及开展政策调整落地奠定基础。
温馨提示:全文共计3200余字,乃上班地铁途中码字编写,实属不易,感谢关注。起草于2020年11月27日,终稿于2020年12月5日。
一、导读
前两篇我们依次讲解了金融产品规则引擎的前后置规则以及资方准入规则,这一篇我们将重点讲述规则引擎最重要的核心构成-金融方案费用规则。
通过这一篇,你会了解到在金融产品中,更多的业务概念,包括:金融产品、经销商门店、费用规则、费用计算公式等,同时了解他们之间的联系,是如何为金融产品服务的,各自起到什么的职责。
相信通过这一篇,你会对金融产品有了一个全貌的了解,我想在这里强调一点,我们的金融产品是为车金融服务的。
二、发展历程
最初入职这家M公司,逐渐接手并负责金融产品系统,带领团队从老系统中通过技术重构,拆分出来金融产品平台和金融产品服务。后期带领团队进行了数据库的业务拆分,从而实现了金融产品业务模块的独立部署与运维,为车金融的业务稳定发展奠定基础。
金融产品系统,尽管实现了业务拆分,但是整个业务模型并没有本质的变化,只不过由原先的SpringMVC+JSP架构蜕变成前后端分离的技术架构SpringBoot+VueJs。虽然这不足以证明技术重构带来的业务价值,但是从业务长远发展来讲,为后续的平台系统优化和重塑业务模型奠定了基础,倘若没有最初的一小步,也不会有后续的所有研发成果。
三、研究创新
金融产品系统,从贷款总额构成来讲,包含车辆保险、人身险、盗抢险、续保押金、平台费、GPS费等一系列费用,对于是否融进金融方案总贷款,金融产品提供了零档位的选择,譬如人身险、盗抢险、续保押金、车辆保险等。
最初系统从业务模型设计上,只有平台费规则实现与金融产品和经销商门店的双层关联配置,而类似车辆保险、车辆贴息仅与金融产品实现了关联配置,其他费用项(包括利率规则、GPS费规则等)仅仅只实现与经销商门店关联配置。
从业务模型分析来讲,金融产品、经销商门店、费用规则都是独立的业务模型单元个体,为了实现业务的关联性,业务实体之间又存在关联关系。然而,纵观上述的现状,我们发现尽管是同一类费用规则,系统功能实现上依然却有很大差异,最初接触金融产品的我,对此也表示十分诧异,不知道为何设计如此。
伴随着车金融业务的发展,对于各地城市运营有着自己的迫切要求。譬如对于两个不同的地域城市,对于同一款运营的金融产品,倘若想要实现GPS费定价不同,由于金融产品系统上,从系统设计来讲,GPS费并没有和城市(经销商门店)配置关联,虽然金融产品实现了与城市的关联配置,倘若在建立这种间接关联关系的前提下,实现符合实际业务场景要求,对于金融产品平台的产品运营同学来讲,确实有些吃力。
1、内科“手术”
我们通过分析现有系统对于业务场景的支持不足,决定对金融产品系统从业务模型和系统交互进行一次内科“手术”,“手术”的目标就是要支持费用规则也要实现与金融产品与经销商门店的双层关联配置。
为了“手术”的顺利进行以及可以观察手术后的效果,我们对整体“手术”拆分两个阶段来规划执行。
- 一阶段实现利率规则、GPS费规则像平台费规则一样与金融产品和经销商门店双重关联。
- 二阶段实现其他费用(延保费,保险费等)与金融产品和经销商门店双重关联。
一阶段由于变动涉及范围小,我们通过自主研发,在“手术”进行之前,与产品运营同学进行了洽谈,期望“手术”后的金融产品平台可以满足他们的痛点诉求。
在业务技术实现上,我们通过引入了一个关联表,表字段包含费用规则主键id,金融产品主键id,费用类型三个字段实现了费用规则与金融产品的关联关系维护。同时,费用类型这个字段,为后续其他费用规则实现如此关联配置提供了扩展性。
然后,我们调整系统工程源代码mapper的SQL查询逻辑,在对外接口不修改的前提下实现了“手术”的第一阶段目标。事实上讲,一阶段“手术”后,对于产品运营同学来讲,无非增加了关联关系的入口配置;而对于客户端销售来讲,则没有任何交互上的感知差异,但是从接口数据稳定上讲得到有效提高。
2、APP交互
从销售端办单交互来讲,金融方案确定是在贷前环节。最初APP设计是在准入环节,由于订单在贷前环节之前由于审核要求不通过,导致退回时修改贷款意向信息需要重新选择金融方案,为了提高办单流程效率,后期业务优化把金融方案移到贷前环节。
金融方案的获取,从APP交互上来讲,UI呈现给用户自上而下依次排列有金融产品、利率档位、平台费率档位、GPS档位以及其他费用档位的下拉选择。
由于我们设计上实现了与金融产品和经销商门店的双重关联,因此在这个环节,只要用户确定金融产品,系统就可以确定用户可选的相关费用档位。最初,由于我们金融产品系统设计上并没有关联性,因此在用户交互上,只有触发相应费用档位下拉选择时,系统交互才能返回符合的费用档位数据集。金融产品系统经过“手术”后,就可以在一个接口打包返回一系列费用档位。
因此,“手术”的良好效果也有益于金融产品中心在接口设计上,从扩展性和维护性来讲,相比“手术”之前可谓优化了很多,这意味着后续业务场景增加或减少一种金融方案费用项,仅仅在这个接口中调整就可以满足需求扩展。
3、平台交互
伴随着业务的发展,金融产品系统的相关表数据量急剧上升。对于产品运营同学来讲,譬如新开城一个城市,就意味着至少新增一家门店,在产品费用规则配置方面就需要有繁重的工作量。
由于平台系统功能的简陋,我们为了支撑产品运营同学的日常运营诉求,对平台功能进行一系列交互优化。
我们实现了费用规则模块可以实现与经销商门店的关联,后续我们又增加了与金融产品关联入口配置。同时,对于经销商门店,我们也增加了与金融产品和费用规则的交互,这种交互从而方便快捷支撑用户的诉求需要。
上述配置面板既可以从经销商门店,也可以从金融产品维度进行统一配置管理,通过选项卡tab页面展示所有的费用规则,提供关联/解绑的快捷批量入口,实现日常运营配置。
上述配置面板既支持所有的费用规则实现批量运营门店配置,提供了多条件的快速检索。
批量关联设置面板,是为了解决日常产品运营批量上线或下线费用档位,并从门店或产品维度支持快捷关联绑定,辅助产品多元化运营决策。
四、核心解读
1、费用项的配置
譬如,平台费率是跟随着平台费规则配置的。本身这些费用规则在配置时,需要配置相关适用业务场景,业务场景包含还款期限,车类,首付比区间,贷款金额区间等。
费用规则从产品运营角度,可以通过费用规则名称进行区别,或者通过对数据设置标签进行分类,标签倘若使用良好的情况下,对于几千上万条规则可谓提供了一把利剑,能够帮你进行快速检索并支持日常工作运营。
此外费用规则配置的场景,从选择上倘若你配置了一个金融方案,本身金融方案就有场景条件,但是对于费用规则,你需要配置的场景条件应该必须满足金融方案的配置区间,这意味着费用规则从业务场景上,是对金融方案的业务最小划分单元。
2、费用项优先级
最初这些费用项规则是有优先级的,优先级意味着数据返回客户端的排列顺序,虽然优先级在设计上有一定的扩展性,但是对于金融方案试算场景时,最初订单中心没有存储费用规则主键id,这意味着数据返显会因为优先级调整而导致费用项金额不一样。
后来,对于解决这个问题,我们在数据存储时,要求订单中心需要存储金融产品相关费用规则id,以支持APP数据保存返显。
3、费用项的档位
对于一个订单,如果客户的首付比或者贷款总额不一样,或者不同省市地域政策不同场景下,从业务上,需要针对业务从金融产品上以档位进行划分,档位的不同,意味这对于总贷款金额的某笔费用金额也是不一样的。
譬如GPS费,平台费,个人保障计划等,它是以档位来满足业务场景的。
4、费用项的计算
在金融产品系统内部,对于车金融业务线,金融产品提供的试算,其实是为客户贷款意向计算出总贷款金额,对于一辆实际销售价的车辆,在分期还款的场景下,客户首付一次车款,剩下的尾款需要分摊到月供上进行偿还。
因此,从业务上讲,计算总贷款和生成还款计划,是对于金融产品来讲最核心的服务能力,而车金融的金融产品系统就仅仅只是计算总贷款,还款计划则不属于其职责,该职责由账务系统提供。
金融产品的费用项计算以及总贷款金额,从车金融业务角度来讲,无论什么资方,什么样的金融方案,其实计算公式都是不变的。
费用项在处理数值上,目前提供了进位方式和精度处理的设置。进位方式则是向上取整,向下取整,四舍五入三种方式,而精度处理则是小数或者整数的处理。
五、核心代码
1、费用项关联配置-抽象代码设计
QueryLink
/**
* @description: 查询费用规则关联产品接口
* @Date : 2018/11/20 下午2:07
* @Author : 石冬冬-Seig Heil
*/
public interface QueryLink<E extends BaseEntity,F extends PdRuleProductForm> {
/**
* 查询已关联集合
* @param form
* @return
*/
List<E> queryLink(PageForm<F> form);
/**
* 查询已关联条数
* @param form
* @return
*/
int queryLinkCount(PageForm<F> form);
}
AbstractQueryLinkHandler
/**
* @description: 抽象查询关联产品处理器
* @Date : 2018/11/20 下午2:14
* @Author : 石冬冬-Seig Heil
*/
public abstract class AbstractQueryLinkHandler<E extends BaseRule> implements QueryLink {
/**
* 处理器名称
*/
protected String handlerName;
/**
* 依赖查询service接口
*/
protected PdRuleProductService pdRuleProductService;
/**
* 依赖通用设置接口
*/
protected CommonComponent commonComponent;
/**
* 查询数量
*/
protected int count;
/**
* 查询实体对象
*/
protected List<E> entities;
/**
* VO实体集合对象
*/
protected List<BaseVo> voList;
/**
* 业务类型
*/
protected TagConstant.BuzTypeEnum buzTypeEnum;
/**
* 查询条件
*/
protected PageForm<PdRuleProductForm> form;
/**
* 默认构造函数
*/
public AbstractQueryLinkHandler() {
}
/**
* 构造函数
* @param handlerName 处理器名称
* @param buzTypeEnum 业务类型 已关联或未关联
*/
public AbstractQueryLinkHandler(String handlerName, TagConstant.BuzTypeEnum buzTypeEnum) {
this.handlerName = handlerName;
this.buzTypeEnum = buzTypeEnum;
}
/**
* 初始化相关成员变量
* @param context
*/
protected void prepare(QueryLinkContext context){
this.pdRuleProductService = context.getPdRuleProductService();
this.commonComponent = context.getCommonComponent();
this.form = context.getForm();
}
/**
* 查询关联数据
*/
protected void query(){
entities = queryLink(form);
count = queryLinkCount(form);
}
/**
* 相关设置调用
*/
protected void call(QueryLinkContext context){
context.setCount(count);
voList = convertVo();
context.setVoList(voList);
}
/**
* entities 转换 vo
* @return
*/
abstract List<BaseVo> convertVo();
/**
* 对查询的VO集合绑定标签字段
*/
protected void bindTags(){
if(null == voList || voList.isEmpty()){
return;
}
commonComponent.bindTags(TwoTuple.newInstance(buzTypeEnum.getIndex(),voList),
(rule) -> Integer.valueOf(((BaseRuleVo)rule).getRuleSeq()),(rule, tags) -> ((BaseRuleVo)rule).setTags(tags));
}
/**
* 外部调用公共方法
* @param context
*/
public final void execute(QueryLinkContext context){
prepare(context);
query();
call(context);
bindTags();
}
}
RateQueryLinkHandler
/**
* @description: 利率规则查询产品关联处理器
* @Date : 2018/11/20 下午5:47
* @Author : 石冬冬-Seig Heil
*/
public class RateQueryLinkHandler extends AbstractQueryLinkHandler {
/**
* 构造函数
*/
public RateQueryLinkHandler() {
super("利率规则", TagConstant.BuzTypeEnum.RATE_RULE);
}
@Override
public List<RateRule> queryLink(PageForm form) {
return pdRuleProductService.queryRateRules(form);
}
@Override
public int queryLinkCount(PageForm form) {
return pdRuleProductService.queryRateRulesCount(form);
}
@Override
List<BaseVo> convertVo() {
return new RateRuleVoConvertor().convertList(entities);
}
}
综述,费用规则绑定查询实现代码了进一步抽象封装,对于代码扩展性来讲,增加一个子类实现相应的查询方法既可以实现场景的扩展。
QueryLinkHandlerFactory
/**
* @description: 查询关联产品处理器工厂
* @Date : 2018/11/20 下午6:03
* @Author : 石冬冬-Seig Heil
*/
public final class QueryLinkHandlerFactory {
final static int SECOND_YEAR = 2;
final static int THIRD_YEAR = 3;
/**
* 创建处理器
* @param buzTypeEnum
* @return
*/
public static final AbstractQueryLinkHandler create(BuzTypeEnum buzTypeEnum){
switch (buzTypeEnum){
case SER_FIN_RULE:
return new SerFinQueryLinkHandler();
case RATE_RULE:
return new RateQueryLinkHandler();
case GPS_RULE:
return new GpsQueryLinkHandler();
case INSURANSE_SECOND_YEAR:
return new InsuranceQueryLinkHandler(SECOND_YEAR);
case INSURANSE_THIRD_YEAR:
return new InsuranceQueryLinkHandler(THIRD_YEAR);
case ACCOUNT_RULE:
return new AccountQueryLinkHandler();
default:
throw new BizException("illegal buzTypeEnum index = "+ buzTypeEnum.getIndex());
}
}
}
2、批量关联设置-抽象代码设计
上述是UI交互原型,每一个输入框是不同表的id,每个输入框非必填,填写的ID需要检查是否非法ID,过滤之后的合法ID才可以绑定写入到关联表中。
UML图
LinkProcess
/**
* @description: 数据关联处理接口
* @Date : 2018/10/17 下午6:34
* @Author : 石冬冬-Seig Heil
*/
public interface LinkProcess {
/**
* 真正要处理的规则ID或产品ID
* 该接口,需要子类实现查询库,根据页面输入规则ID条件,并返回查询对象集合获取对应主键ID集合
* @return
*/
List<Integer> dataList();
/**
* 处理数据
* @param c1 把处理完毕合法的规则ID回调给消费发使用
* @param c2 把处理完毕不合法的规则ID及操作日志对象回调给消费方使用
*/
void handleData(Consumer<List<Integer>> c1, BiConsumer<List<Integer>,JSONObject> c2);
/**
* 以门店维度-数据绑定关联
* @param dealerCodes 要关联的经销商门店编码
* @param linkList 要关联的费用规则ID或产品ID
*/
void bindDealer(List<Integer> dealerCodes, List<Integer> linkList);
/**
* 以产品维度-数据绑定关联
* @param productIds 关联的产品ID集合
* @param linkList 要关联的费用规则ID或产品ID
*/
void bindProduct(List<Integer> productIds, List<Integer> linkList);
/**
* 是否执行跳跃
* @return
*/
default boolean skip(){return false;}
}
AbstractLinkProcessor
/**
* @description: 抽象数据关联处理接口
* @Date : 2018/10/17 下午6:45
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
public abstract class AbstractLinkProcessor implements LinkProcess,BiConsumer<List<Integer>, JSONObject>{
protected final String LOG_TITLE = "批量关联设置";
/**
* 标示费用类型
* {@link TagConstant.BuzTypeEnum#getIndex()}
*/
protected Integer classify;
/**
* 处理器名称
*/
protected String processorName;
/**
*
*/
protected BaseService ruleService;
/**
*
*/
protected BaseService ruleDealerService;
/**
* 前端输入数据(规则ID或者产品ID)
*/
protected List<Integer> sourceList;
/**
* 真正要处理的合法运营门店
*/
protected List<Integer> dealerProcesses;
/**
* 真正要处理的合法产品ID
*/
protected List<Integer> productProcesses;
/**
* 上下文对象
*/
protected LinkProcessContext context;
/**
* 封装请求参数
*/
protected BatchLinkParam batchLinkParam;
/**
* 规则查询Form对象
*/
protected BaseRuleForm ruleForm;
/**
* 业务类型
*/
protected LinkProcessBuzType buzType;
/**
*
*/
protected DealerFeeRuleVo feeRuleVo;
/**
* 构造函数
* @param processorName
*/
public AbstractLinkProcessor(String processorName) {
this.processorName = processorName;
}
/**
* 子类初始化
*/
protected abstract void init();
/**
* 空项处理
*/
protected abstract void emptyInputItem();
/**
* 初始化
*/
protected void prepare(LinkProcessContext ctx){
context = ctx;
if(null == context.getOperateLog()){
context.setOperateLog(new JSONObject(true));
}
feeRuleVo = context.getFeeRuleVo();
buzType = context.getBuzType();
batchLinkParam = context.getBatchLinkParam();
dealerProcesses = context.getDealerScope();
productProcesses = context.getProductScope();
}
/**
* 对外部调用统一入口
*/
public final void execute(LinkProcessContext ctx){
prepare(ctx);
init();
if(skip()){
log.info("skip this process【{}】,buzType={}",processorName,buzType);
return;
}
if(null != sourceList && !sourceList.isEmpty()){
handleData(processList -> {
if(batchLinkParam.isBindDealer()){
bindDealer(dealerProcesses,processList);
}
if(batchLinkParam.isBindProduct()){
bindProduct(productProcesses,processList);
}
},(noExits,json) -> accept(noExits,json));
}else{
emptyInputItem();
}
ctx.getOperateLog().putAll(context.getOperateLog());
}
@Override
public void handleData( Consumer<List<Integer>> c1, BiConsumer<List<Integer>, JSONObject> c2) {
//不合法的规则ID
Set<Integer> difference = Sets.difference(Sets.newHashSet(sourceList),Sets.newHashSet(dataList()));
//真正要处理的规则ID
Set<Integer> intersection = Sets.intersection(Sets.newHashSet(sourceList),Sets.newHashSet(dataList()));
List<Integer> processList = Lists.newArrayList(intersection);
c1.accept(processList);
c2.accept(Lists.newArrayList(difference),context.getOperateLog());
if(batchLinkParam.isBindDealer()){
context.getOperateLog().put("dealerCodes",Optional.ofNullable(dealerProcesses).orElse(Collections.emptyList()).toString());
}
if(batchLinkParam.isBindProduct()){
context.getOperateLog().put("productIds", Optional.ofNullable(productProcesses).orElse(Collections.emptyList()).toString());
}
context.getOperateLog().put(processorName,processList.toString());
}
@Override
public void bindDealer(List<Integer> dealerCodes, List<Integer> linkList) {
if(CollectionsTools.isEmpty(dealerCodes) || CollectionsTools.isEmpty(linkList)){
return;
}
log.info("{},【{}】,linkList={},dealerCodes={},buzType={}", LOG_TITLE,processorName, linkList.toString(), dealerCodes.toString(), buzType);
dealerCodes.forEach(dealerCode -> {
List<BaseDealerRes> ruleDealers = new ArrayList<>(linkList.size());
Date now = TimeTools.createNowTime();
BaseDealerRes ruleDealer;
for(Integer ruleId : linkList){
ruleDealer = new BaseDealerRes();
ruleDealer.setRuleSeq(ruleId);
ruleDealer.setDealerCode(dealerCode);
ruleDealer.setUpdateTime(now);
ruleDealers.add(ruleDealer);
}
if(buzType == LinkProcessBuzType.INSERT){
ruleDealerService.batchInsertIgnore(ruleDealers);
}
if(buzType == LinkProcessBuzType.DELETE){
ruleDealerService.batchDelete(ruleDealers);
}
});
}
@Override
public void bindProduct(List<Integer> productIds, List<Integer> linkList) {
if(CollectionsTools.isEmpty(productIds) || CollectionsTools.isEmpty(linkList)){
return;
}
log.info("{},【{}】,linkList={},productIds={},buzType={}", LOG_TITLE,processorName, linkList.toString(), productProcesses.toString(), buzType);
productIds.forEach(productId -> {
List<PdRuleProduct> batchList = new ArrayList<>(linkList.size());
linkList.forEach(eachId -> batchList.add(PdRuleProduct.builder().classify(classify.byteValue()).productId(productId).ruleId(eachId).build()));
if(buzType == LinkProcessBuzType.INSERT){
context.getServiceMap().get(RuleServiceEnum.RULE_PRODUCT).batchInsertIgnore(batchList);
}
if(buzType == LinkProcessBuzType.DELETE){
context.getServiceMap().get(RuleServiceEnum.RULE_PRODUCT).batchDelete(batchList);
}
});
}
}
AbstractFeeRuleLinkProcessor
package com.creditease.heil.core.bindlink.feerule;
import com.alibaba.fastjson.JSONObject;
import com.creditease.heil.core.bindlink.AbstractLinkProcessor;
import com.creditease.heil.core.bindlink.LinkProcessBuzType;
import com.creditease.heil.core.bindlink.RuleServiceEnum;
import com.creditease.heil.entity.PdFeeDealer;
import com.creditease.heil.form.PdFeeDealerForm;
import com.creditease.heil.form.PdFeeProductForm;
import com.creditease.heil.form.PdFeeRuleForm;
import com.creditease.heil.service.rule.PdFeeDealerService;
import com.creditease.heil.service.rule.PdFeeProductService;
import com.creditease.heil.entity.PdFeeProduct;
import com.creditease.heil.service.rule.PdFeeRuleService;
import com.creditease.util.CollectionsTools;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @description: 抽象费用规则关联处理器
* {@link CarInsuranceRuleLinkProcessor}
* {@link CarDiscountRuleLinkProcessor}
* {@link LifeInsuranceRuleLinkProcessor}
* {@link RentalCommissionRuleLinkProcessor}
* {@link TheftProtectionRuleLinkProcessor}
* {@link EnjoyPackRuleLinkProcessor}
* @Date : 2018/11/13 上午11:39
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
public abstract class AbstractFeeRuleLinkProcessor extends AbstractLinkProcessor {
/**
* 构造函数
* @param processorName
*/
public AbstractFeeRuleLinkProcessor(String processorName) {
super(processorName);
}
/**
* 抽象获取页面输入规则ID
* @return
*/
abstract List<Integer> getInputRuleIds();
@Override
protected void init() {
sourceList = getInputRuleIds();
ruleService = context.getServiceMap().get(RuleServiceEnum.FEE_RULE);
ruleDealerService = context.getServiceMap().get(RuleServiceEnum.FEE_RULE_DEALER);
}
@Override
public List<Integer> dataList() {
ruleForm = new PdFeeRuleForm();
ruleForm.setRuleIds(sourceList);
List<Integer> des = ((PdFeeRuleService)ruleService).queryList((PdFeeRuleForm) ruleForm).stream().map(rule -> rule.getId()).collect(Collectors.toList());
return des;
}
@Override
public void accept(List<Integer> integers, JSONObject jsonObject) {
jsonObject.put("buzType",buzType);
}
@Override
public void bindDealer(List<Integer> dealerCodes, List<Integer> linkList) {
if(CollectionsTools.isEmpty(dealerCodes) || CollectionsTools.isEmpty(linkList)){
return;
}
log.info("{},【{}】,linkList={},dealerCodes={},buzType={}", LOG_TITLE,processorName, linkList.toString(), dealerCodes.toString(), buzType);
dealerCodes.forEach(dealerCode -> {
List<PdFeeDealer> ruleDealers = new ArrayList<>(linkList.size());
for(Integer ruleId : linkList){
ruleDealers.add(new PdFeeDealer(ruleId,dealerCode));
}
if(buzType == LinkProcessBuzType.INSERT){
ruleDealerService.batchInsertIgnore(ruleDealers);
}
});
if(buzType == LinkProcessBuzType.DELETE){
PdFeeDealerService pdFeeDealerService = (PdFeeDealerService)ruleDealerService;
pdFeeDealerService.batchDelete(PdFeeDealerForm.builder().dealerScopes(dealerCodes).ruleIdScope(linkList).build());
}
}
@Override
public void bindProduct(List<Integer> productIds, List<Integer> linkList) {
if(CollectionsTools.isEmpty(productIds) || CollectionsTools.isEmpty(linkList)){
return;
}
log.info("{},【{}】,linkList={},productIds={},buzType={}", LOG_TITLE,processorName, linkList.toString(), productProcesses.toString(), buzType);
productIds.forEach(productId -> {
List<PdFeeProduct> batchList = new ArrayList<>(linkList.size());
linkList.forEach(eachId -> batchList.add(PdFeeProduct.builder().productId(productId).resId(eachId).build()));
if(buzType == LinkProcessBuzType.INSERT){
context.getServiceMap().get(RuleServiceEnum.FEE_PRODUCT).batchInsertIgnore(batchList);
}
if(buzType == LinkProcessBuzType.DELETE){
PdFeeProductForm pdFeeProductForm = PdFeeProductForm.builder().productId(productId).ruleIdList(linkList).build();
((PdFeeProductService)context.getServiceMap().get(RuleServiceEnum.FEE_PRODUCT)).batchDelete(pdFeeProductForm);
}
});
}
}
CarInsuranceRuleLinkProcessor
/**
* @description: 车辆保险规则关联处理
* @Date : 2018/11/13 下午5:23
* @Author : 石冬冬-Seig Heil
*/
public class CarInsuranceRuleLinkProcessor extends AbstractFeeRuleLinkProcessor {
public CarInsuranceRuleLinkProcessor() {
super("车辆保险规则");
}
@Override
List<Integer> getInputRuleIds() {
return batchLinkParam.getCarInsuranceRuleIds();
}
@Override
protected void emptyInputItem() {
feeRuleVo.setCarInsuranceNotExist(sourceList);
}
@Override
public void accept(List<Integer> integers, JSONObject jsonObject) {
super.accept(integers, jsonObject);
feeRuleVo.setCarInsuranceNotExist(integers);
}
}
GpsRuleLinkProcessor
/**
* @description: GPS规则关联处理器
* @Date : 2018/10/19 下午6:04
* @Author : 石冬冬-Seig Heil
*/
public class GpsRuleLinkProcessor extends AbstractLinkProcessor {
/**
* 构造函数
*/
public GpsRuleLinkProcessor() {
super("GPS规则");
}
@Override
protected void init() {
classify = TagConstant.BuzTypeEnum.GPS_RULE.getIndex();
sourceList = batchLinkParam.getGpsRuleIds();
ruleService = context.getServiceMap().get(RuleServiceEnum.GPS);
ruleDealerService = context.getServiceMap().get(RuleServiceEnum.GPS_DEALER);
}
@Override
protected void emptyInputItem() {
feeRuleVo.setGpsNotExist(sourceList);
}
@Override
public List<Integer> dataList() {
ruleForm = new GpsRuleForm();
ruleForm.setRuleIds(batchLinkParam.getGpsRuleIds());
List<Integer> des = ((GpsRuleService)ruleService).queryList((GpsRuleForm) ruleForm).stream().map(rule -> rule.getRuleSeq()).collect(Collectors.toList());
return des;
}
@Override
public void accept(List<Integer> integers, JSONObject jsonObject) {
feeRuleVo.setGpsNotExist(integers);
jsonObject.put("buzType",buzType);
}
}
AbstractBatchLinkProcessMediator
/**
* @description: 抽象批量关联设置处理中介者
* @Date : 2018/11/12 下午3:44
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
public abstract class AbstractBatchLinkProcessMediator {
protected final String LOG_TITLE = "批量关联设置";
/**
* 上下文对象
*/
protected LinkProcessContext context;
/**
* 处理器集合
*/
protected List<AbstractLinkProcessor> processorList;
public AbstractBatchLinkProcessMediator() {
}
/**
* 构造函数
* @param context
*/
public AbstractBatchLinkProcessMediator(LinkProcessContext context) {
this.context = context;
}
/**
* 初始化
*/
protected void init(){
processorList = new ArrayList<AbstractLinkProcessor>(){{
add(new ProductLinkProcessor());
add(new DealerLinkProcessor());
add(new RateRuleLinkProcessor());
add(new GpsRuleLinkProcessor());
add(new SerFinRuleLinkProcessor());
add(new AccountRuleLinkProcessor());
add(new SecondYearPremiumRuleLinkProcessor());
add(new ThirdYearPremiumRuleLinkProcessor());
add(new LifeInsuranceRuleLinkProcessor());
add(new RentalCommissionRuleLinkProcessor());
add(new CarInsuranceRuleLinkProcessor());
add(new CarDiscountRuleLinkProcessor());
add(new TheftProtectionRuleLinkProcessor());
add(new EnjoyPackRuleLinkProcessor());
}};
}
/**
* 处理门店或者产品ID
*/
abstract void handleBatchSources();
/**
* 执行
*/
public final void execute(){
init();
handleBatchSources();
processorList.forEach(each -> each.execute(context));
}
public void setContext(LinkProcessContext context) {
this.context = context;
}
}
BatchLinkDealerProcessMediator
/**
* @description: 批量关联设置-门店编码关联-处理中介者对象
* @Date : 2018/11/12 下午3:43
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
public class BatchLinkDealerProcessMediator extends AbstractBatchLinkProcessMediator {
public BatchLinkDealerProcessMediator() {
}
public BatchLinkDealerProcessMediator(LinkProcessContext context) {
super(context);
}
@Override
void handleBatchSources() {
log.info("{}-门店关联-params:{}", LOG_TITLE,JSON.toJSON(context.getBatchLinkParam()));
List<Integer> dealerScope = context.getBatchLinkParam().getDealerCodes();
// 查询门店
TransferClient transferClient = context.getTransferClient();
List<BaseDealerRe> dealerReList = transferClient.queryBaseDealerList(DealerDTO.builder().dealerScopes(dealerScope).build());
Assert.notEmpty(dealerReList, "批量门店编码集合为空");
List<Integer> sources = dealerReList.stream().map(BaseDealerRe::getDealerCode).collect(Collectors.toList());
//非法数据
Set<Integer> difference = Sets.difference(Sets.newHashSet(dealerScope),Sets.newHashSet(sources));
//合法数据
Set<Integer> intersection = Sets.intersection(Sets.newHashSet(dealerScope),Sets.newHashSet(sources));
List<Integer> noExits = Lists.newArrayList(difference);
context.getFeeRuleVo().setDealerNotExist(noExits);
List<Integer> handleDealerScope = Lists.newArrayList(intersection);
context.setDealerScope(handleDealerScope);
log.info("{}-门店关联-合法数据:{},非法数据:{}", LOG_TITLE,JSON.toJSON(handleDealerScope), JSON.toJSON(noExits));
}
}
BatchLinkProductProcessMediator
/**
* @description: 批量关联设置-产品ID关联-处理中介者对象
* @Date : 2019/2/12 下午5:12
* @Author : 石冬冬-Seig Heil
*/
@Slf4j
public class BatchLinkProductProcessMediator extends AbstractBatchLinkProcessMediator {
public BatchLinkProductProcessMediator() {
}
public BatchLinkProductProcessMediator(LinkProcessContext context) {
super(context);
}
@Override
void handleBatchSources() {
log.info("{}-产品关联-params:{}", LOG_TITLE,JSON.toJSON(context.getBatchLinkParam()));
List<Integer> productScope = context.getBatchLinkParam().getpIds();
// 查询门店
ProductService productService = (ProductService)context.getServiceMap().get(RuleServiceEnum.PRODUCT);
//经过数据库过滤查询真正要处理的产品对象集合
List<Product> queryList = productService.queryList(ProductForm.builder().productIdList(productScope).build());
Assert.notEmpty(queryList,"批量产品ID集合为空");
List<Integer> sources = queryList.stream().map(each -> each.getpId()).collect(Collectors.toList());
//非法数据
Set<Integer> difference = Sets.difference(Sets.newHashSet(productScope),Sets.newHashSet(sources));
//合法数据
Set<Integer> intersection = Sets.intersection(Sets.newHashSet(productScope),Sets.newHashSet(sources));
List<Integer> noExits = Lists.newArrayList(difference);
context.getFeeRuleVo().setPIdNotExist(noExits);
List<Integer> handleScope = Lists.newArrayList(intersection);
context.setProductScope(handleScope);
log.info("{}-产品关联-合法数据:{},非法数据:{}", LOG_TITLE,JSON.toJSON(handleScope), JSON.toJSON(noExits));
}
}
BatchLinkProcessMediatorFactory
/**
* @description: 批量关联设置处理中介者工厂
* @Date : 2019/2/12 下午5:39
* @Author : 石冬冬-Seig Heil
*/
public final class BatchLinkProcessMediatorFactory {
/**
* 返回具体处理中介者类
* @param buzType
* @return
*/
public static AbstractBatchLinkProcessMediator create(BatchLinkParam.ChooseTypeEnum buzType){
switch (buzType){
case dealerScope:
return new BatchLinkDealerProcessMediator();
case productScope:
return new BatchLinkProductProcessMediator();
}
return null;
}
}
3、资方准入属性过滤-抽象代码设计
AbstractAccessPropHandler
/**
* @description: 抽象准入属性处理器
* @Date : 2019/7/4 23:25
* @Author : 尚凌宇 - lingyu.shang
*/
public abstract class AbstractAccessPropHandler {
/**
* 上下文对象
*/
FundAccessContext context;
/**
* 有效值集合
*/
List<String> validList;
/**
* 参数集合
*/
List<String> paramList;
/**
* 检查
* @param context
* @return
*/
public abstract boolean check(FundAccessContext context);
/**
* 参数过滤
* @return
*/
protected void paramFilter() {
List<String> illegalList = paramList.stream().filter(p -> !validList.contains(p)).collect(Collectors.toList());
if(CollectionsTools.isNotEmpty(illegalList)) {
paramList.removeAll(illegalList);
String paramValue = paramList.stream().collect(Collectors.joining(","));
context.getProp().setPropValue(paramValue);
}
}
}
DbAccessPropHandler
/**
* @description: 配置类型准入属性处理器
* @Date : 2019/7/4 23:27
* @Author : 尚凌宇 - lingyu.shang
*/
public class DbAccessPropHandler extends AbstractAccessPropHandler {
@Override
public boolean check(FundAccessContext context) {
this.context = context;
FundAccessConstant.FundPropNameEnum fundPropNameEnum = context.getFundPropNameEnum();
String value = context.getValue();
if(value == null) {
context.setMsg(MessageFormat.format("{0}属性值不能为空", fundPropNameEnum.name()));
return false;
}
this.paramList = Arrays.stream(value.split(",")).collect(Collectors.toList());
Map<String, List<String>> dictMap = context.getDictMap();
this.validList = dictMap.get(fundPropNameEnum.name());
paramFilter();
if(CollectionsTools.isEmpty(paramList)) {
context.setMsg(MessageFormat.format("{0}属性值不合法", fundPropNameEnum.name()));
return false;
}
return true;
}
}
EnumAccessPropHandler
/**
* @description: 枚举类型准入属性处理器
* @Date : 2019/7/4 23:26
* @Author : 尚凌宇 - lingyu.shang
*/
public class EnumAccessPropHandler extends AbstractAccessPropHandler {
@Override
public boolean check(FundAccessContext context) {
this.context = context;
FundAccessConstant.FundPropNameEnum fundPropNameEnum = context.getFundPropNameEnum();
String value = context.getValue();
if(value == null) {
context.setMsg(MessageFormat.format("{0} 属性值不能为空", fundPropNameEnum.name()));
return false;
}
this.paramList = Arrays.stream(value.split(",")).collect(Collectors.toList());
FundAccessConstant.EnumType enumType = fundPropNameEnum.getEnumType();
EnumValue[] enumValues = fundPropNameEnum.getEnumValues();
switch (enumType) {
case INDEX:
this.validList = Arrays.stream(enumValues).map(v -> v.getIndex()).map(i -> i.toString()).collect(Collectors.toList());
break;
case NAME:
this.validList = Arrays.stream(enumValues).map(v -> v.getName()).collect(Collectors.toList());
break;
case DESC:
this.validList = Arrays.stream(enumValues).map(v -> ((EnumDesc) v).getDesc()).collect(Collectors.toList());
break;
default:
context.setMsg("枚举获取字段配置有误,请联系开发人员");
return false;
}
paramFilter();
if(CollectionsTools.isEmpty(paramList)) {
context.setMsg(MessageFormat.format("{0}属性值不合法", fundPropNameEnum.name()));
return false;
}
return true;
}
}
NumberAccessPropHandler
/**
* @description: 资方准入属性数字类型处理器
* @Date : 2019/7/4 23:24
* @Author : 尚凌宇 - lingyu.shang
*/
public class NumberAccessPropHandler extends AbstractAccessPropHandler {
@Override
public boolean check(FundAccessContext context) {
boolean success = StringUtils.isNumeric(context.getValue());
if(!success) {
context.setMsg(context.getFundPropNameEnum().name() + "必须是数字");
}
return success;
}
}
六、尾语
通过前近三篇文章,我们全貌的讲解了金融产品的规则引擎的相关业务模型设计以及技术方案实现。规则引擎从设计之初,我们引入了开源的Google Aviator组件以实现我们的费用公式配置化,同时在代码实现上,通过遵循良好的设计原则以及引入相关设计模式,从而提高了系统的扩展性和伸缩性。
金融产品系统伴随着我从入职到离职,它陪伴了我两年的M公司职业生涯,我也见证了它的成长。感谢有这样一个平台,让我从零开始重新规划一个为业务发展应运而生的金融产品平台,通过我以及团队的技术能力和创新能力,最终打造出我们的车金融业务线金融产品平台,请记住,它有一个别致的名字-太极。