風控系統--收費數據源處理

收費數據源規則執行設計的演進

背景介紹

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

參考文檔

uml類圖製做

spel

相關文章
相關標籤/搜索