美團服務體驗平臺對接業務資料的最佳實踐-海盜中介軟體

美團技術團隊發表於2019-03-02

背景

移動網際網路時代,使用者體驗為王。美團服務體驗平臺希望能夠幫助客戶解決在選、購、用美團產品過程中遇到的各種問題,真正做到“以客戶為中心”,為客戶排憂解難。但服務體驗平臺內部只維護客戶的客訴資料,為了精準地預判和更好地解決客戶遇到的問題,系統必須依賴業務部門提供的一些業務資料,包括但不限於訂單資料、退款資料、產品資料等等。 本文會著重講一下在整個系統互動過程中遇到的一些問題,然後分享一下在實踐中探索出來的經驗和方法論,希望能夠給大家帶來一些啟發。

問題

對接場景廣而雜

首先,需要接入服務體驗平臺服務(包括直接面向使用者的C端服務、面向客服的工單服務等等)的業務方非常多且雜,而且在不斷擴充。美團有非常多的業務線,比如外賣、酒店、旅遊、叫車、交通、到店餐飲、到店綜合、貓眼等等。其中部分業務又延展出多條子業務線,比如大交通部門包含火車票、汽車票、國內機票、國際機票、船票等等。具體到每一條子業務線的每一個業務場景,客戶都有可能會遇到問題。對於這些場景,服務體驗平臺服務都需要呼叫對應的業務資料介面,來幫助使用者自助或者客服協助解決這些問題。就美團現有的業務而言,這樣的場景數量會達到萬級。而且業務形態在不斷迭代,還會有更多的場景被挖掘出來,這些都需要持續對接更多的業務資料來進行支撐。

接入場景定製化要求高

其次,接入服務體驗平臺服務的業務方定製化要求很高。因為業務場景的差異化非常大,不同的接入方都希望能夠定製特殊複雜邏輯,需要服務體驗平臺提供的服務解決方案與業務深度耦合。這就需要服務體驗平臺側對接入方業務邏輯和資料介面深入瞭解,並對這些業務資料進行組裝,針對每個場景進行定製開發。

方案

早期方案

為了解決上述問題,初期在做系統設計時候,考慮業務方多是既有系統,所以服務體驗平臺服務趨向平臺化設計,並引入了適配層。服務體驗平臺內部對所有的業務資料和邏輯進行統一抽象,對內標準化介面,遮蔽掉業務邏輯和介面的差異。所有的定製化邏輯都在適配層中封裝。但這需要客服側RD對所有的場景去編寫介面卡程式碼,將從一個或者多個業務部門介面中拿到的業務資料,轉成內部實際場景需要的資料。

其系統互動如下圖所示:

美團服務體驗平臺對接業務資料的最佳實踐-海盜中介軟體

缺點

雖然上述系統設計能滿足業務上的要求,但是存在兩個比較明顯的缺點:

  • 編碼工作量繁重 如上圖所示,每個業務場景都需要編寫介面卡來滿足需求,如果依賴的外部介面比較少,場景也比較單一,按照上述方案實施還可以接受。但業務接入非常多且雜,給客服側RD帶來了非常繁重的工作量,包括介面卡編寫以及後續維護過程中對下游業務介面的持續跟蹤和監控。
  • 客服側RD需要深入瞭解業務方邏輯 另外,由於客服側RD對於業務模型的不熟悉,解析業務模型然後組裝最終展示給客戶的資料,需要比業務方RD花更多的時間來梳理和實現,並且花費更多的時間來驗證正確性。比如下面是一個真實的組裝業務介面並對業務資料進行處理的案例:
public class TicketAdapterServiceImpl implements OrderAdapterService {

    @Resource(name = "tradeQueryClient")
    private TradeTicketQueryClient tradeTicketQueryClient;
    @Resource
    private ColumbusTicketService columbusTicketService;

    /** 
    * 根據訂單ID獲取門票相關的訂單資料、門票資料、退款資料等
    **/
    @Override
    public OrderInfoDTO handle(OrderRequestDTO orderRequestDTO) {
        List<ITradeTicketQueryService.TradeDetailField> tradeDetailFieldList = new ArrayList<ITradeTicketQueryService.TradeDetailField>();
        tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.ORDER);
        tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.TICKET);
        tradeDetailFieldList.add(ITradeTicketQueryService.TradeDetailField.REFUND_REQUEST);
        try {
            //通過介面A得到部分訂單資料、門票資料和退款資料
            RichOrderDetail richOrderDetail = tradeTicketQueryClient.getRichOrderDetailById(orderRequestDTO.getOrderId(), tradeDetailFieldList);
            if (richOrderDetail == null) {
                return null;
            }
            if (richOrderDetail.getOrderDetail() == null) {
                return null;
            }
            OrderDetail orderDetail = richOrderDetail.getOrderDetail();
            RefundDetail refundDetail = richOrderDetail.getRefundDetail();
            OrderInfoDTO orderInfoDTO = new OrderInfoDTO(); 

            //解析和處理介面A返回的欄位,得到客服側場景真正需要的資料
            orderInfoDTO.put("dealId", orderDetail.getMtDealId());
            orderInfoDTO.put(DomesticTicketField.VOUCHER_CODE.getValue(), getVoucherCode(richOrderDetail));
            orderInfoDTO.put(DomesticTicketField.REFUND_CHECK_DUE.getValue(), getRefundCheckDueDate(richOrderDetail));
            orderInfoDTO.put(DomesticTicketField.REFUND_RECEIVED_DUE.getValue(), getRefundReceivedDueDate(richOrderDetail));
    
            //根據介面B獲取另外一些訂單資料、門票詳情資料、退款資料
            ColumbusTicketDTO columbusTicketDTO = columbusTicketService.getByDealId((int) richOrderDetail.getOrderDetail().getMtDealId());
            if (columbusTicketDTO == null) {
                return orderInfoDTO;
            }
            //解析和處理介面B返回的欄位,得到客服側場景真正需要的資料
            orderInfoDTO.put(DomesticTicketField.REFUND_INFO.getValue(), columbusTicketDTO.getRefundInfo());
            orderInfoDTO.put(DomesticTicketField.USE_METHODS.getValue(), columbusTicketDTO.getUseMethods());
            orderInfoDTO.put(DomesticTicketField.BOOK_INFO.getValue(), columbusTicketDTO.getBookInfo());
            orderInfoDTO.put(DomesticTicketField.INTO_METHOD.getValue(), columbusTicketDTO.getIntoMethod());
      
            return orderInfoDTO;
        } catch (TException e) {
            Cat.logError("查詢不到對應的訂單詳情", e);
            return null;
        }
    }
}
複製程式碼

探索

將適配層交由業務方實現

為了克服早期方案的兩個缺點,最初,我們希望能夠把場景資料的準備和業務模型的解析工作,都交給對業務比較熟悉的團隊來處理,即將適配層交由業務方來實現。

美團服務體驗平臺對接業務資料的最佳實踐-海盜中介軟體

這樣做的話優勢和劣勢也比較明顯:

優勢

客服這邊關注自己的領域服務就好,做好平臺化,資料提供都交給業務團隊,解放了客服側RD。

劣勢

但對業務方來說帶來了比較大的工作量,業務方既有服務的複用性很低,對客服側每一個需要資料的場景,都要重新封裝新的服務。

更好的解決方案?

這個時候我們思考:是否可以既能讓業務方解析自己的業務資料,又能夠儘量利用既有服務呢?我們考慮把既有服務的組裝過程以及模型的轉換都讓一個服務編排的中介軟體來實現。但是使用這個中介軟體有一個前提,就是業務方提供出來的既有服務必須支援泛化呼叫,避免呼叫方直接依賴服務方客戶端(文章下一個小節也會補充下對於泛化呼叫的解釋)。其互動模型如下圖所示:

美團服務體驗平臺對接業務資料的最佳實踐-海盜中介軟體

結果-海盜中介軟體

簡介

什麼是海盜?

海盜就是一個用來對支援泛化呼叫(上述所說)的服務進行編排,然後獲取預期結果的一箇中介軟體。使用該中介軟體呼叫方可以根據場景來對目標服務進行編排,按需呼叫。

何為泛化呼叫?

通常服務提供方提供的服務都會有自己的介面協議,比如一個獲取訂單資料的服務:

    package com.dianping.demo;
    public interface DemoService{
          OrderDTO  getById(String orderId);
    }
複製程式碼

而呼叫方呼叫該服務需要引入該介面協議,即依賴該服務提供的JAR包。如果呼叫方需要整合多方資料,那就需要依賴非常多的API,同時服務方介面升級客戶端也需要隨之進行升級。而泛化呼叫就可以解決這個問題,通過泛化呼叫客戶端可以在服務方沒有提供介面協議和不依賴服務方API的情況下對服務進行呼叫,通過類似GenericService這樣一個介面來處理所有的服務請求。

如下是一個泛化呼叫的Demo:

    public class DemoInvoke{
        public void genericInvoke(){
             /** 呼叫方配置  **/ 
             InvokerConfig<GenericService> invokerConfig = new InvokerConfig("com.dianping.demo.DemoService", com.dianping.pigeon.remoting.common.service.GenericService.class);
             invokerConfig.setTimeout(1000);
             invokerConfig.setGeneric(GenericType.JSON.getName());
             invokerConfig.setCallType("sync");  
                 
             /** 泛化呼叫 **/
             final GenericService genericService = ServiceFactory.getService(invokerConfig);
             List<String> paramTypes = new ArrayList<String>();
             paramTypes.add("java.lang.String");
             List<String> paramValues = new ArrayList<String>();
             paramValues.add("0000000001");
             String result = genericService.$invoke("getById", paramTypes, paramValues);
        }
    }
複製程式碼

有了這個泛化呼叫的前提,我們就可以重點去思考如何對服務進行編排,然後對取得的結果進行處理了。

DSL設計

首先重新梳理一下海盜的設計目標:

  • 對既有服務進行編排呼叫
  • 對獲取的資料進行處理

而為了實現服務編排,需要定義一個資料結構來描述服務之間的依賴關係、呼叫順序、呼叫服務的入參和出參等等。之後對獲取的結果進行處理,也需要在這個資料結構中具體描述對什麼樣的資料進行怎麼樣的處理等等。 所以我們需要定義一套DSL(領域特定語言)來描述整個服務編排的藍圖,其語法如下:

{
    //定義好需要呼叫的介面以及介面之間的依賴關係,一個介面呼叫即為一個task
    "tasks": [   
        //第一個task
        {      
            "url": "http://helloWorld.test.hello",     //url 為pigeon釋出的遠端服務地址:
            "alias": "d1",   //別名,結果取值的時候可以通過別名引用
            "taskType": "PigeonGeneric",  //task的類別一般可以設定為PigeonGeneric,預設是pigeonAgent方式。
            "method": "getByDoubleRequest", //要呼叫的pigeon介面的方法名
            "timeout": 3000,  //task的超時時間
            "inputs": {      //入參情況,多個入參通過key:value的結構書寫,key的類別通過下面的inputsExtra定義。
                "helloWorld": {
                    "name": "csophys",    //可以通過#orderId,從上下文中獲取值,可以通過$d1.orderId的形式從其他的task中獲取值
                    "sex": "boy"
                },
                "name": "winnie"
            },
           "inputsExtra": {     //入參key的類別定義
                "helloWorld": "com.dianping.csc.pirate.remoting.pigeon.pigeon_generic_demo_service.HelloWorld",
                "name": "java.lang.String"
            }          
        },
        //另一個task
        {    
            "url": "http://helloWorld.test.hello",
            "alias": "d2",
            "taskType": "PigeonGeneric",
            "method": "getByDoubleRequest",
            "inputsExtra": {
                "helloWorld": "com.dianping.csc.pirate.remoting.pigeon.pigeon_generic_demo_service.HelloWorld",
                "name": "java.lang.String"
            },
            "timeout": 3000,
            "inputs": {
                "helloWorld": {
                    "name": "csophys",
                    "sex": "boy"
                },
                "name": "winnie"
            }
        }
    ],
    "name": "pigeonGenericUnitDemo",  //DSL的名稱定義,暫時沒有特別含義
    "description": "pigeon泛型呼叫測試",  //DSL的描述
    "outputs": {            //定義好最後輸出的資料模型
        "d1name": "$d1.name",
        "languages": "$d2.languages",
        "language1": "$d2.languages[0]",
        "name": "csophys"
    }
}
複製程式碼

架構設計

有了DSL來描述整個編排藍圖之後,海盜自然要對該DSL進行解析,然後對服務進行具體呼叫。其整體架構如下所示:

美團服務體驗平臺對接業務資料的最佳實踐-海盜中介軟體

其中涉及到幾個重點概念:

  • Facade:對外提供統一介面,供客戶端呼叫。
  • Parser:對於輸入的DSL進行解析,解析成內部流轉的資料結構,同時得到所有的task,並且構建task呼叫邏輯樹。
  • Executor:真實發起呼叫的模組,目前支援平臺內部的Pigeon和MTThrift呼叫方式,同時對HTTP等其他協議有良好的擴充套件性。
  • DataProcessor:資料後處理。這邊會把所有介面拿到的資料轉換層客服場景這邊需要的資料,並且通過設計的一些內部函式,可以支援一些如資料半脫敏等功能。
  • 元件外掛化:對日誌等功能實現可插拔,呼叫方可以自定義這些元件,即插即用。

主要Feature

海盜具有如下主要特點:

  • 採用去中心化的設計思路,引擎整合在SDK中。方案通用化,每一個需要業務資料的場景都可以通過海盜直接呼叫資料提供方。
  • 服務編排支援並行和序列呼叫,使用方可以根據實際場景自己構造服務呼叫樹。通過DSL的方式把之前硬編碼組裝的邏輯實現了配置化,然後通過海盜引擎把能並行呼叫的服務都執行了並行呼叫,資料使用方不用再自己處理效能優化。
  • 使用JSON DSL 描述整個工作藍圖,簡單易學。
  • 支援JSONPath語法對服務返回的結果進行取值。
  • 支援內建函式和自定義指令(語法參考ftl)對取到的後設資料進行處理,得到需要的最終結果。
  • 編排服務樹視覺化。
  • 目前集團內部RPC中介軟體包括Pigeon、MTThrift,已進行了泛化呼叫支援,可以通過海盜實現Pigeon服務和MTThrift的服務編排。不需要限制業務團隊的服務提供方式,但需要升級中介軟體版本。這裡特別感謝服務治理團隊的大力支援。

Tutorial

場景:需要根據訂單ID查詢訂單狀態和支付狀態,但目前沒有現成的介面支援該功能,但有兩個既有介面分別是:

  • 介面1:根據訂單ID,獲取到訂單狀態和支付流水號
  • 介面2:根據支付流水號獲取支付狀態

那我們可以對這兩個介面進行編排,編寫DSL如下:

{
  "tasks": [
    {
      "url": "http://test.service",
      "alias": "d1",
      "taskType": "PigeonGeneric",
      "method": "getByOrderId",
      "timeout": 3000,
      "inputs": {
        "orderId": "#orderId"
      },
      "inputsExtra": {
        "name": "java.lang.String"
      }
    },
    {
      "url": "http://test.service",
      "alias": "d2",
      "taskType": "PigeonGeneric",
      "method": "getPayStatus",
      "timeout": 3000,
      "inputs": {
        "paySerialNo": "$d1.paySerialNo"
      },
      "inputsExtra": {
        "time": "java.lang.String"
      }
    }
  ],
  "name": "test",
  "description": "組裝上述介面獲取訂單狀態和支付狀態",
  "outputs": {
    "orderStatus": "$d1.orderStatus",
    "payStatus": "$d2.payStatus"
  }
}

複製程式碼

然後客戶端進行呼叫:

    String DSL = "上述DSL檔案";
    String params  = "{\"orderId\":\"000000001\"}";
    Response resp = PirateEngine.invoke(DSL, params);
複製程式碼

最後得到的資料即為呼叫場景真正需要的資料:

{
   "orderStatus":1,
   "payStatus":2
}
複製程式碼

開發流程變化

因為獲取資料的架構產生了變化,開發流程也隨之發生改變。

美團服務體驗平臺對接業務資料的最佳實踐-海盜中介軟體
如圖所示,因為減少了客服側RD不斷去向業務方RD確認返回的資料含義和邏輯,雙方RD各自專注各自熟悉的領域,開發效率和最終結果準確性都有顯著提升。

總結和展望

最後總結一下使用海盜之後的優勢

  • 去中心化的設計,可用性得到保證。
  • 服務複用性高,領域劃分更加清晰,讓RD專注在自己熟悉的領域,降低研發成本。
  • 因為流程變化後,業務方可以提前驗證提供的資料,高質量交付。
  • 客服側對資料獲取進行統一收口,可以對所有呼叫服務統一監控並對資料統一處理。

展望

海盜的技術規劃

  • 豐富內部函式和運算表示式 目前海盜提供了一部分簡單的內部函式用來對取到的值進行簡單處理,同時正在實現支援呼叫方自定義運算表示式來支援複雜場景的資料處理,這部分需要持續完善。
  • 遮蔽遠端呼叫協議異構性 目前海盜只支援對美團Pigeon和MTThrift服務進行編排,這裡要對協議進行擴充套件,支援類似HTTP等通用協議,同時支援呼叫方自定義協議和呼叫實現。
  • 運營工具完善 提供一個比較完整的運營工具,呼叫方可以自行配置DSL並進行校驗,然後一鍵呼叫查詢最終結果。同時呼叫方可以通過該工具進行日誌、報表等相關資料查詢。
  • 自動生成單元測試 能夠把經過驗證的DSL生成相應的單元測試用例給到資料提供方,持續保障提供的DSL的可用性和正確性。

作者簡介

王彬,美團資深研發工程師,畢業於南京大學,2017年2月加入美團。目前主要專注於智慧客服領域,從事後端工作。

陳勝,海盜專案負責人,智慧客服技術負責人,2013年加入大眾點評。在未來智慧客服組會持續在平臺化和垂直領域方向深入下去,為消費者、商家、企業提供更加智慧的客戶服務體驗。

招聘廣告

服務體驗平臺可以深入接觸到公司的所有業務,推進業務改善產品。提升客戶的服務體驗。打造一個客戶貼身的智慧服務助手。通過技術的手段更快地解決客戶的問題,並且最大程度地節省客服的人力成本。歡迎有意向的同學加入服務體驗平臺,上海、北京都有需求。 簡歷請投遞至:sheng.chen#dianping.com

美團服務體驗平臺對接業務資料的最佳實踐-海盜中介軟體

相關文章