风控系统每种场景 如现金贷 都须要跑不少规则html
规则1 申请人姓名身份证号实名验证java
规则2 申请人手机号码实名认证git
规则3 银行卡预留手机号码实名认证github
规则4 申请人银行卡预留手机号码在网状态检验spring
规则5 申请人银行借记卡有效性核验express
规则6 户籍地址与身份证号归属地比对app
...ide
而这些规则的验证都须要调用外部收费接口 鉴于外部接口调用逻辑不少能够复用 因而使用模板模式进行封装ui
组装入参 (不一样的接口有不一样的入参 由于接口有数十个 省去建立数十个对象 入参统一使用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...