收费数据源规则执行设计的演进
背景介绍
风控系统每种场景 如现金贷 都需要跑很多规则
规则1 申请人姓名身份证号实名验证
规则2 申请人手机号码实名认证
规则3 银行卡预留手机号码实名认证
规则4 申请人银行卡预留手机号码在网状态检验
规则5 申请人银行借记卡有效性核验
规则6 户籍地址与身份证号归属地比对
...
而这些规则的验证都需要调用外部收费接口 鉴于外部接口调用逻辑很多可以复用 于是使用模板模式进行封装
调用外部接口模板
组装入参 (不同的接口有不同的入参 因为接口有数十个 省去创建数十个对象 入参统一使用Map)
发送请求 (可以统一)
返回内容解析 (不同的接口有不同的返回 返回对象统一继承
FeeData
)返回对象
AbstractFeeDataManagerTemplate
-
getFeeData(params)
// 得到接口返回数据
abstract buildParams(Map params)
// 组装该接口特有恒定的入参
private sendRequest(param)
// 发送请求
abstract FeeData resolveResponse(String response)
// 解析不同返回内容 统一返回FeeData
设计类图
伪代码
public abstract class AbstractFeeDataManagerTemplate {
protected abstract void buildParams(Map params);
public FeeData getFeeData(Map params){
buildParams(params);
String response = sendRequest(params);
return resolveResponse(response);
}
protected abstract FeeData resolveResponse(String response);
private String sendRequest(Map params) {
//使用HttpClient调用外部接口
}
}
public class NameIdCardVerificationManager extends AbstractFeeDataManagerTemplate {
protected void buildParams(Map params) {
// 组装此接口特有且恒定入参 如
params.put("code", "NAME_IDCARD_VERIFICATION");
}
protected FeeData resolveResponse(String response) {
// 解析接口返回 并组装成FeeData返回
}
}
外部接口门面
每种场景包含很多规则 每个规则逐个执行 怎么知道哪个规则要调用哪个接口呢?于是创建了一个门面 保存规则与具体的接口实现类的关联关系 但是考虑到很多规则会调用同一接口 如申请人手机号码实名验证、银行预留手机号码实名验证、第一联系人手机号码实名验证均是调用手机实名验证接口 于是实际保存的是接口编码与接口的映射关系
FeeDataManagerFacade
Map : code <--> Manager [考虑到多个规则会调用同一外部接口 定义了接口编码]
FeeData getFeeData(code,params)
伪代码
public class FeeDataManagerFacade {
private static final Map<String, AbstractFeeDataManagerTemplate> code2FeeDataManagerMap = new HashMap();
static{
code2FeeDataManagerMap.put("NAME_IDCARD_VERIFICATION", new NameIdCardVerificationManager());
//...
}
public FeeData getFeeData(String code, Map<String, Object> params){
return code2FeeDataManagerMap.get(code).getFeeData(params);
}
}
于是当执行规则1 -- 申请人姓名身份证号实名验证 时 这样调用
FeeDataManagerFacade feeDataManagerFacade = new FeeDataManagerFacade();
RuleContext ruleContext = ...;
String code = ruleContext.getRule().getFeeDataCode(); // 每个规则配置了其对应的收费数据源接口的Code
Map params = new HashMap<>();
params.put("name", "张三");
params.put("idcard", "123456199001011233");
try {
FeeData feeData = feeDataManagerFacade.getFeeData(code, params);
if(!feeData.isPass()){
// 校验未通过处理
ruleContext.setResult(ruleContext.getRule().getResult()); // 设置决策结果 来自规则配置 如拒绝 人工复核
ruleContext.setMessage(String.format("申请人姓名: %s 身份证号: %s 实名验证失败",params.get("name"),params.get("idcard")));
}
} catch (Exception e) {
// 接口调用异常 默认为人工复核
ruleContext.setResult("REVIEW"); // 设置决策结果:人工复核
ruleContext.setMessage(String.format("接口调用失败: %s",e.getMessage()));
}
规则处理模板
由于每个需要调用外部数据源的规则的处理逻辑类似
组装参数
调用该规则对应的外部接口
接口调用成功: 规则校验未通过处理
接口调用异常: 接口异常处理
同样可以采用模板模式进行封装 如下伪代码所示
public abstract class AbstractFeeRuleProcessServiceTemplate {
private static FeeDataManagerFacade facade = new FeeDataManagerFacade();
public void process(Map params, RuleContext ruleContext){
try {
FeeData feeData = facade.getFeeData(ruleContext.getRule().getFeeDataCode(), params);
if(!feeData.isPass()){
// 校验未通过处理
ruleContext.setResult(ruleContext.getRule().getResult());
ruleContext.setMessage(buildMessage());
}
} catch (Exception e) {
// 接口调用异常 默认为人工复核
ruleContext.setResult("REVIEW");
ruleContext.setMessage(String.format("接口调用失败: %s",e.getMessage()));
}
}
// 因为每个规则 返回提示信息不同 所以将提示信息提取出来作为抽象方法
protected abstract String buildMessage();
}
对应类图
此时规则1--申请人姓名身份证号实名验证的处理方式为
new AbstractFeeRuleProcessServiceTemplate(){
@Override
protected String buildMessage() {
return String.format("申请人姓名: %s 身份证号: %s 实名验证失败",params.get("name"),params.get("idcard"));
}
}.process(params,ruleContext);
即只需自定义规则未通过时的提示信息即可
总体设计类图
演进一
有些外部接口 并不是返回一个boolean类型的结果--校验通过或没通过 而是返回一个具体的信息 如身份证归属地、手机号码归属地 然后用用户提交的信息 如用户提交的户籍地址与身份证归属地进行比较
此时下面的代码就不合适了
if(!feeData.isPass()){
// 校验未通过处理
}
于是抽象了一个checkFeeData
方法 供规则覆盖
public abstract class AbstractFeeRuleProcessServiceTemplate {
public void process(Map params, RuleContext ruleContext){
try {
FeeData feeData = ...
if(!checkFeeData(feeData)){
// 校验未通过处理
// ...
}
} catch (Exception e) {
// 接口调用异常 默认为人工复核
// ...
}
}
protected abstract boolean checkFeeData(FeeData feeData);
protected abstract String buildMessage(FeeData feeData);
}
对应类图为
此时规则1--申请人姓名身份证号实名验证的处理方式为
new AbstractFeeRuleProcessServiceTemplate(){
@Override
protected boolean checkFeeData(FeeData feeData) {
return feeData.isPass();
}
@Override
protected String buildMessage(FeeData feeData) {
return String.format("申请人姓名: %s 身份证号: %s 实名验证失败",params.get("name"),params.get("idcard"));
}
}.process(params,ruleContext);
执行规则6--户籍地址与身份证号归属地比对 是这样校验
new AbstractFeeRuleProcessServiceTemplate(){
@Override
protected boolean checkFeeData(FeeData feeData) {
// 为了避免创建很多对象 使用Map保存接口返回信息
// 身份证归属地 如 河北省 邯郸市 临漳县 、重庆市綦江县
String location = (String) feeData.getExtra().get("location");
// applyInfo 用户申请信息
if (location.contains(applyInfo.getResidenceAddressProvinceName()) || location.contains(applyInfo.getResidenceAddressCountyName())) {
return true;
}
return false;
}
@Override
protected String buildMessage(FeeData feeData) {
String message = String.format("自述户籍地址:%s %s %s 与身份证归属地:%s 不一致",
applyInfo.getResidenceAddressProvinceName(),applyInfo.getResidenceAddressCityName()
,applyInfo.getResidenceAddressCountyName(),feeData.getExtra().get("location"));
return message;
}
}.process(params,ruleContext);
演进二 -- 批量查询
有些规则需要调用多次接口 如
注册手机号码归属地与申请人身份证号归属地、现居住地、单位地址、家庭地址、户籍地址的交叉验证
查询注册手机号码归属城市、申请人身份证号码归属地城市。将注册手机号码归属地城市与申请人的身份证归属地城市、现居地址城市、单位地址城市 、家庭地址城市、户籍地址城市进行比对,如果任意一项一致,则通过。否则拒绝或人工复核
上面的规则 需要调用手机归属地
和 身份证号码归属地
接口 此时已有的设计 -- 基于一个规则一个接口
组装参数
调用接口
规则校验
校验后处理
就不满足要求了 于是保持AbstractFeeRuleProcessServiceTemplate
接口不变的情况下 对Facade
做了如下修改:
增加了一个虚拟接口编码--
BATCH_QUERY_FEEDATA
表示批量查询接口每个接口的入参(params)中 添加实际的接口编码 如
MOBILE_LOCATION_QUERY
,IDCARD_LOCATION_QUERY
Facade入参变成 paramList : [param1,param2,...]
Facade返回结果 feeDataList : [feeData1, FeeData2, ...]
FeeDataManagerFacade
对应的代码为
public FeeData getFeeData(String code, Map<String, Object> params){
if("BATCH_QUERY_FEEDATA".equals(code)){ // 批量查询
List<Map<String,Object>> paramList = (List<Map<String, Object>>) params.get("paramList");
List<FeeData> feeDataList = new ArrayList<>();
for (Map<String, Object> param : paramList) {
String realCode = (String) param.get("code"); // 实际接口编码
Objects.requireNonNull(realCode,"接口编码不可为空");
FeeData feeData = code2FeeDataManagerMap.get(realCode).getFeeData(params);
feeDataList.add(feeData);
}
FeeData result = new FeeData();
result.setExtra(newHashMap("feeDataList",feeDataList));
return result;
}
// 单个查询
return code2FeeDataManagerMap.get(code).getFeeData(params);
}
执行规则--注册手机号码归属地与申请人身份证号归属地、现居住地、单位地址、家庭地址、户籍地址的交叉验证
Map param1 = newHashMap("code", "MOBILE_LOCATION_QUERY", "mobile", "13800138000");
Map param2 = newHashMap("code", "IDCARD_LOCATION_QUERY", "idcard", "123456199001011233");
Map params = newHashMap("paramList", newArrayList(param1, param2));
new AbstractFeeRuleProcessServiceTemplate2(){
@Override
protected boolean checkFeeData(FeeData feeData) {
List<FeeData> feeDataList = (List<FeeData>) feeData.getExtra().get("feeDataList");
String mobileLocation = (String) feeDataList.get(0).getExtra().get("location");
String idcardLocation = (String) feeDataList.get(1).getExtra().get("location");
// ... 规则校验
}
@Override
protected String buildMessage(FeeData feeData) {
//... 组装提示信息
// String message = "注册手机号码归属城市:%s ,注册手机号码归属地城市与申请人一系列地址城市都不一致";
}
}.process(params,ruleContext);
演进三--链式查询
如 规则 -- IP地址与三级商户地址的交叉验证
将IP地址和三级商户地址转为经纬度落在地图,如果两者相距半径小于 (2含)公里 则通过,否则拒绝或人工复核。
涉及到接口:
ip --> 地址
两个地址之间的距离
后一个接口 需要依赖前一个接口的返回信息--ip地址 此时第二个接口的参数以动态变量的形式定义 如
startAddress : 三级商户地址
endAddress : #{extra['address']} // 动态解析接口一返回的地址 使用了spel
于是对批量查询做了修改 以便支持链式查询
public FeeData getFeeData(String code, Map<String, Object> params){
if("BATCH_QUERY_FEEDATA".equals(code)){ // 批量查询
List<Map<String,Object>> paramList = (List<Map<String, Object>>) params.get("paramList");
List<FeeData> feeDataList = new ArrayList();
FeeData previous = null; // 保存前一接口的返回
for (Map<String, Object> param : paramList) {
String realCode = (String) param.get("code"); // 实际接口编码
Objects.requireNonNull(realCode,"接口编码不可为空");
// 若输入参数依赖前一查询结果
for (Map.Entry<String, Object> entry : param.entrySet()) {
String value = entry.getValue().toString();
if(value.startsWith("#{")){
// 表示动态变量 需要解析
String spel = value.replaceFirst("#\\{(.+)}", "$1");
Expression expression = expressionParser.parseExpression(spel);
Object resolvedValue = expression.getValue(previous);
entry.setValue(resolvedValue); // 实际值替换动态变量
}
}
FeeData feeData = code2FeeDataManagerMap.get(realCode).getFeeData(params);
feeDataList.add(feeData);
previous = feeData;
}
FeeData result = new FeeData();
result.setExtra(newHashMap("feeDataList",feeDataList));
return result;
}
// 单个查询
return code2FeeDataManagerMap.get(code).getFeeData(params);
}
执行规则 -- IP地址与三级商户地址的交叉验证
// IP地址与三级商户地址的交叉验证
Map param1 = newHashMap("code", "IP_ADDRESS_QUERY", "ip", "222.128.42.13");
Map param2 = newHashMap("code", "ADDRESSES_DISTANCE_QUERY", "startAddress", applyInfo.getThirdBusinessAddress(),"endAddress","#{extra['address']}");
Map params = newHashMap("paramList", newArrayList(param1, param2));
new AbstractFeeRuleProcessServiceTemplate2(){
@Override
protected boolean checkFeeData(FeeData feeData) {
List<FeeData> feeDataList = (List<FeeData>) feeData.getExtra().get("feeDataList");
double distance = Double.parseDouble(feeDataList.get(1).getExtra().get("distance").toString());
// 规则校验 ...
}
@Override
protected String buildMessage(FeeData feeData) {
// 组装提示信息
// String message=String.format("ip地址: %s与三级商户地址: %s 相距范围不符合要求。"...)
}
}.process(params,ruleContext);
源码
https://github.com/zhugw/anti...
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。