風控系統每種場景 如現金貸 都須要跑不少規則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...