收费数据源规则执行设计的演进
背景介绍
风控系统每种场景 如现金贷 都需要跑很多规则
- 规则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);