风控系统建设,指标策略规则流程设计,LiteFlow隐式子流程,构造EL和Chain

个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview


简介

前面有很多文章已经说了,我要利用LiteFlow做风控系统。至于进度嘛,只能尽力而为,毕竟我的惰性也很强。

下面是目前Git的提交记录,代码托管在GithubGitee,但是是私有仓库,因为还不是开放的时候,还有很多要做的。

image

其中能梳理的仅是主流程的TODO都有下面这么多,更何况还有管理应用,es存储和检索设计开发还都没开始,更有前端规则/策略配置也还没做,还有在涉及画布的排版、数据的反显都还没有明确的解决方案。所以连个半成品都不算,不是开放的时候。

另外,也不知道我会不会中途放弃😝

image

最近刚好做一点值得说的,就讲一下吧!

主流程

顺带一提,不谈系统设计及目的直接摆出一堆东西都是耍流氓,但是这个扯起来又是一大堆,这次只能耍流氓了!

说明:只是现状,之后也许有可能推翻-重做-推翻-重做。

1、数据接入

所有数据通过此接入,接入服务配置服务名,输入参数,输出参数,运行的Chain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@RestController
@RequestMapping("/access")
@RequiredArgsConstructor
public class AccessController {

private final AccessService accessService;

/**
* 同步接入
*
* @param name 服务名
* @param params 参数
* @return map
*/
@PostMapping("/{name}/sync")
public CommonResult<AccessResponse> syncRisk(@PathVariable("name") String name, @RequestBody Map<String, String> params) {
return success(accessService.syncRisk(name, params));
}

/**
* 异步接入
*
* @param name 服务名
* @param params 参数
* @return map
*/
@PostMapping("/{name}/async")
public CommonResult<AccessResponse> asyncRisk(@PathVariable("name") String name, @RequestBody Map<String, String> params) {
return success(accessService.asyncRisk(name, params));
}

。。。
}

当前有Chain:THEN(a_icn,nf_cn,df_cn,i_rcn,ps_rcn,a_ocn);

分别表示

子流程/组件 说明
a_icn 接入服务输入处理node
nf_cn 系统字段处理node
df_cn 动态字段处理node
i_rcn 指标计算流程
ps_rcn 策略集流程
a_ocn 接入服务输出处理node

通过Apifox模拟的请求如下。

image

2、输入参数校验、系统字段/动态字段处理

接入服务是可配置输入输出和映射系统的。

通常,需要先创建系统字段和动态字段,动态字段是系统字段的补充,可以加入自定义的处理逻辑。配置了系统字段和动态字段后就可以将其配置在接入服务的输入和输出中,在这一步骤中就会进行映射转换,IP、身份证、GPS解析等。这样设计是为了适应风控多变的规则配置,随时可以在不更改代码的情况下,新增规则和数据的接入。而且后面会使用ES存储数据,这样也是可行的。

image

3、指标计算

所有符合本次请求的指标都要进行计算,所有指标计算可以并行。如何计算前面也有文章说明了使用Redis做时间窗口的方法。

指标有应用和策略集场景的区分,这里暂时使用代码构造ELChian,并同时使用隐式子流程运行。

以下仅供参考,大致思路是在创建指标时已经将指标按场景区分加入到对应的Chain中了,每次请求只会运行当次请求的应用和策略集场景下的指标。

1
2
3
4
5
6
7
8
9
10
11
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = LFUtil.INDICATOR_ROUTE_COMMON_NODE, nodeType = NodeTypeEnum.COMMON)
public void indicatorRoute(NodeComponent bindCmp) {
AccessRequest accessRequest = bindCmp.getContextBean(AccessRequest.class);
String appName = accessRequest.getStringData(FieldName.appName);
String policySetCode = accessRequest.getStringData(FieldName.policySetCode);
LiteFlowChainELBuilder.createChain().setChainId(LFUtil.INDICATOR_ROUTE_CHAIN).setEL(
StrUtil.format(LFUtil.INDICATOR_ROUTE_CHAIN_EL, appName, policySetCode)
).build();

bindCmp.invoke2Resp(LFUtil.INDICATOR_ROUTE_CHAIN, null);
}

最终的指标Chain是类似于IF(c_cn.tag("3"),i_tcn.tag("2"),i_fcn);这样的IF编排Chain

c_cn.tag("3")为条件组件,目前是数据库存储的,未来有可能使用组件参数将其替换掉;i_tcn.tag("2")为条件通过后指标计算组件;i_fcn为指标条件失败组件。

4、策略集-策略-规则

关于策略集/策略/规则的关系,前面也有文章说明了。

再讲一下就是,每次请求只能对应一个策略集,策略集下有多个策略(有权重、最坏、首次的模式之分),策略有多条规则(有运行、模拟、关闭的状态区别)。策略集可以编排策略与其他组件的运行流程。

首先通过应用名和策略集确定要运行策略集,然后执行策略集的决策流(当前还没做,默认所有策略并行),策略会因为模式(权重、首次、最坏)的不同采取命中规则处置方式也不同,最终将结果转换返回。

与指标类似在创建策略、规则时就已经将其ELChain创建了。

策略node加了isAccess方法,用于判断是否执行当前node,因为node设置了开启、关闭、模拟之类的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@LiteflowMethod(value = LiteFlowMethodEnum.IS_ACCESS, nodeId = LFUtil.POLICY_COMMON_NODE, nodeType = NodeTypeEnum.COMMON)
public boolean policyAccess(NodeComponent bindCmp) {
Policy policy = policyMapper.selectById(bindCmp.getTag());
return policy.getStatus();
}

@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = LFUtil.POLICY_COMMON_NODE, nodeType = NodeTypeEnum.COMMON)
public void policy(NodeComponent bindCmp) {
PolicyContext policyContext = bindCmp.getContextBean(PolicyContext.class);
Policy policy = policyMapper.selectById(bindCmp.getTag());
PolicyVO policyVO = PolicyConvert.INSTANCE.convert(policy);
policyContext.addPolicy(policyVO.getId(), policyVO);

log.info("当前策略(id:{}, name:{}, code:{})", policy.getId(), policy.getName(), policy.getCode());

policyContext.initRuleList(policy.getId());

bindCmp.invoke2Resp(StrUtil.format(LFUtil.POLICY_CHAIN, policy.getId()), null);
}

规则Chain与指标Chian类似IF(AND(c_cn.tag("4"),c_cn.tag("5")),r_tcn.tag("2"),r_fcn);,也是IF编排的Chain,目前只做了决策,未来可以加入打tag、加入名单等等。

5、存储ES

数据是非常重要的,对于未来的管理应用十分关键。但现在还不涉及,所以目前是单独创建一份日志文件存储了。还使用了Kafka,模拟了生产和消费。

格式化JSON后如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
{
"result": {
"name": "手机登录策略",
"code": "phone_login",
"disposalName": "通过",
"disposalCode": "pass",
"policyResults": [
{
"name": "手机登录最坏",
"code": "phone_login_worst",
"mode": "worst",
"disposalName": "通过",
"disposalCode": "pass",
"ruleResults": [
{
"name": "测试规则03",
"code": "352452",
"disposalName": "通过",
"disposalCode": "pass",
"score": 0
}
],
"mockRuleResults": [

]
},
{
"name": "手机登录顺序",
"code": "phone_login_order",
"mode": "order",
"disposalName": "通过",
"disposalCode": "pass",
"ruleResults": [
{
"name": "测试规则01",
"code": "123456",
"disposalName": "通过",
"disposalCode": "pass",
"score": 0
}
],
"mockRuleResults": [

]
}
]
},
"zbs": [
{
"id": "1",
"name": "24小时交易金额之和",
"type": "sum",
"version": 0,
"value": "39.57214"
},
{
"id": "2",
"name": "24小时交易金额最大",
"type": "max",
"version": 0,
"value": "39.57214"
}
],
"fields": {
"N_S_ipCity": "0",
"N_S_lonAndLat": "82.6013,58.37",
"N_S_payerType": "路它用决头此",
"N_S_idCardCity": "未知",
"N_S_payeeIDNumber": "450000199306286684",
"N_S_ipProvince": "0",
"N_S_payeeIDCountryRegion": "US",
"N_F_transAmount": 39.57214,
"N_S_payerAddress": "南竿乡 湖北省 衢州市",
"N_S_seqId": "629123892af54a4487b6063d49c51354",
"N_S_payeePhoneNumber": "18135474838",
"N_S_transSerialNo": "c156f461-f059-472a-96e4-919d07b04c87",
"N_S_payerAccount": "1234567890",
"N_S_payeeAccount": "1234567890",
"N_S_ip": "39.147.100.232",
"N_S_policyCode": "phone_login_worst",
"N_S_payeeType": "况压养又",
"N_S_payerIDNumber": "640000198603085417",
"N_S_payeeBankName": "ABC Bank",
"N_S_phoneNumberProvince": "未知",
"N_S_ipIsp": "移动",
"N_S_payerIDCountryRegion": "US",
"N_D_transTime": "1985-01-27 20:46:50",
"N_S_phoneNumberCity": "江门",
"N_S_phoneNumberIsp": "中国电信",
"N_S_payeeAddress": "和顺县 澳门特别行政区 恩施土家族苗族自治州",
"N_S_payeeRiskRating": "HIGH",
"N_S_policySetCode": "phone_login",
"N_S_payerBankName": "XYZ Bank",
"N_S_idCardDistrict": "未知",
"N_S_appName": "phone",
"N_S_idCardProvince": "宁夏回族自治区",
"N_S_ipCountry": "中国",
"N_S_payeeName": "孔静",
"N_S_payerName": "乔敏",
"N_S_payerPhoneNumber": "18128212188",
"N_S_payerRiskRating": "HIGH"
}
}

6、输出

请求完成后返回命中结果,大致如下。

image

小结

大致就介绍这么多了,因为这一片文章根本讲不完。

自己做项目确实不容易,尤其是我毅力/能力都没那么强,经常遇到问题,就卡一周的。

但同时也会因为解决了某些问题开心很久的!

写在最后

拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。


个人博客:无奈何杨(wnhyang)

个人语雀:wnhyang

共享语雀:在线知识共享

Github:wnhyang - Overview