當前位置: 妍妍網 > 碼農

遊戲帳號的訂單流程重構之路

2024-03-21碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

  • 1 背景

  • 1.1 核心代分碼布散亂

  • 1.2 缺少設計模式

  • 1.3 影響交付效率

  • 2 如何重構

  • 2.1 方案確定

  • 2.2 三層介面+策略樣版模式

  • 2.3 具體實作

  • 3 上線保障

  • 3.1 流程測試

  • 3.2 灰度策略

  • 3.3 異常機制

  • 4 總結


  • 1 背景

    但是由於牽涉流程長、影響範圍大、平時需求排期也比較緊張,所以便無法抽出大量的時間去進行重構。因為一開始就設計的不夠規範、合理,所以之前整個帳號訂單流程存在以下主要問題:

    1.1 核心代分碼布散亂

    除了按照原子層、服務層劃分之外,還有一個服務用於接收訂單mq進行大量的處理操作,訂單相關的介面分布於多個類甚至是一些名稱與訂單毫不相關的類當中。

    1.2 缺少設計模式

    缺少設計模式,各種節點、不同訂單型別的邏輯基本都是透過各種if-else進行處理,耦合度較高,可讀性、可延伸性和可維護性都較差,甚至會出現修改一種訂單流程反而影響到了其它訂單流程的情況。

    1.3 影響交付效率

    代分碼布在多個服務當中,開發一個相關需求時經常需要拉4、5個計畫分支。由於容易修改到了其它訂單模式的程式碼,所以在測試的時候往往又需要回歸其它模式的訂單流程是否有受影響。這些都大大影響了開發測試以及最終交付的效率。

    現在業務趨於穩定,需求叠代也沒有這麽快了,因此就有了重構訂單流程的想法。最終的目的就是為了保證良好的 可讀性、可維護性和可延伸性 。有了重構想法的之後,產生了許多問題,主要如下:

  • 怎麽進行重構呢,用什麽設計模式?

  • 重構後的測試上線怎麽進行呢?

  • 如果上線出現問題要怎麽處理?

  • 接下來就圍繞這幾個問題來敘述一下帳號訂單流程的重構之路。

    2 如何重構

    2.1 方案確定

    先簡單介紹一下遊戲帳號交易的流程,最開始的時候有兩種交易方式,分別是客服發貨交易和自主發貨交易。兩者最大的區別是是否需要第三角色客服的介入,後來七種訂單交易模式都是在這兩種模式基礎上誕生的。

    既然有七種訂單型別,這好辦啊。可以采用策略+樣版模式啊,一個抽象樣版+七個子類別就可以啦。但是後來仔細一想,如果將所有的處理邏輯都放在父類和子類別當中,其實程式碼整體也顯得十分臃腫。

    為了想出更好的解決方案,於是對原有程式碼和業務流程進行了深入的梳理和總結,主要有以下幾點:

    1. 所有訂單流程都是在客服發貨和自主發貨基礎上衍生出來的。

    2. 所有訂單流程都包含下單、支付、上傳賬密、發貨、確認收貨等節點。

    3. 在這些節點裏不同訂單型別大多會有各自一些特定操作,但是這些操作其實並不屬於訂單的主流程。

    透過以上分析,是不是可以將下單到確認收貨作為一層,將不同訂單型別的特定處理實作作為一層呢?這樣不就將訂單流程中各種特殊處理從訂單主流程剝離開了嗎,因此最終決定采用 三層介面+策略樣版 的設計方案。

    2.2 三層介面+策略樣版模式

    介面設計如下:

  • 第一層介面

  • 包含前端使用者進行互動、處理mq訊息以及給其它服務呼叫的介面。

  • 第二層介面

  • 訂單核心主流程能力介面。將下單、支付到確認收貨等「不變」的基礎能力提供給頂層介面呼叫,這層介面有自主發貨流程和客服發貨流程兩個實作類。

    publicinterfaceIGameAccountOrderDealProcess{
    /**
    * 處理下單未支付訂單
    */

    inthandlePlaceOrder(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 處理支付成功訂單
    */

    inthandlePaySuccessOrder(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 處理已發貨訂單
    */

    inthandleDeliverOrder(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 處理支付前取消訂單
    */

    inthandleCancelBeforePayOrder(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 處理支付後取消訂單
    */

    inthandleCancelAfterPayOrder(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 處理交易成功訂單
    */

    inthandleConfirmReceiptOrder(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 帳號交易窗數據
    */

    <T extends TradeFlowData> getOrderTradeData(String logStr, Long orderId, Integer device, Long uid);
    /**
    * 上傳賬密
    */

    ZZOpenScfBaseResult<String> uploadAccountAndPwd(GameAccountSelfTrade.AccountPwdArg arg, long uid, String logStr, ServiceHeaderEntity header)throws Exception;
    /**
    * 發貨
    @param orderContext
    */

    booleandeliverOrder(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 訂單確認收貨
    */

    ZZOpenScfBaseResult<String> confirmReceiptOrder(GameAccountOrderContext orderContext, Long uid, boolean needCheckRisk)throws Exception;
    }

  • 第三層介面

  • 各種訂單型別的特殊處理,每一種訂單模式都對應一個實作類。

    publicinterfaceITradeSelfHandler{
    GameAccountTradeFlow.GameAccountTradeType getOrderTrade();
    /*------------處理mq訊息相關---------------*/
    /**
    *1.插入表之前設定客服和extendInfo
    */

    voidfillExtraOrderInfoBeforeInsert(GameAccountOrderResultEntity orderEntity, GameAccountOrderContext orderContext);
    /**
    * 下單後處理
    */

    voidhandleAfterPlaceOrder(GameAccountOrderContext orderContext);
    /**
    * 支付前取消處理
    */

    voidhandleCancelBeforePay(GameAccountOrderContext orderContext);
    /**
    * 支付後取消處理
    */

    inthandleCancelAfterPay(GameAccountOrderContext orderContext)throws Exception;
    /**
    * 支付後一些額外處理
    */

    inthandleAfterPaySuccess(GameAccountOrderContext orderContext);
    /**
    * 確認收貨處理
    */

    inthandleAfterConfirmReceipt(GameAccountOrderContext orderContext)throws Exception;
    /*---------------------------------*/
    /**
    * 獲取提現時間
    */

    Date getWithDrawlTime();
    /**
    * 發送支付成功push
    */

    voidorderAlreadyPayPushMsgNew(GameAccountOrderContext orderContext, Pair<String, String> jumpUrl);
    /**
    * 獲取分帳帳戶、類別資訊
    */

    List<AccountOrderSplitModel> getOrderSplitModelList(GameAccountOrderContext orderContext, OrderMaxSettleInfo settleInfo);
    /**
    * 客製各自spiUi
    */

    voidbuildOrderSpiUiData(GameAccountOrderContext orderContext, GameOrderSpiConfig bConfig, GameOrderSpiConfig sConfig, SpiUiData spiUiData)throws Exception;
    /**
    * 確認收貨後一些處理
    */

    voidotherOperationAfterReceipt(GameAccountOrderContext orderContext, Long uid);
    }

    2.3 具體實作

  • 核心程式碼收攏 到一個服務,相關介面進行聚合

  • 原先在客服後台、定時任務、mq集群都有一些訂單的操作,但是這些程式碼基本都是重復的,所以此次重構在訂單核心服務中新增相應的訂單操作功能,統一由其它服務進行RPC呼叫。

    將訂單相關的介面、工具類集中到同一個包下,方便定位。

  • 整體類圖及設計原則

    1. 命名規範 :類名、變量名、方法名盡量見名知義。

    2. 單一職責 :各個模組各司其職,避免與其它模組過度耦合。

    3. 準備訂單上下文, 清除RPC重復 呼叫問題。

    //上下文實體
    public classGameAccountOrderContext{
    private String logStr;
    private Long orderId;
    private Integer mqStatus;
    private Order order;
    private GameAccountOrderResultEntity accountOrderEntity;
    private AccountOrderStatusEnum orderStatus;
    private Boolean hasInsuranceService;//訂單是否有保險
    private GameAccountTradeFlow.GameAccountTradeType tradeType;
    private GameAccountProductData accountProductData;
    private ZZProduct product;
    private ZZProductExt productExt;
    private Map<String, String> extValueMap;
    private AccountHelpSaleClue helpSaleClue;//幫賣線索
    private DistributionShareInfoDTO distributionShareInfo;//分銷資訊
    private ITradeSelfHandler tradeSelfHandler;
    private Integer serviceUiStatus;//對應訂單spi狀態
    }
    //上下文準備
    GameAccountOrderContext orderContext = orderContextBuilder.buildAccountOrderContext(order, zzProduct, logStr);

    3 上線保障

    訂單流程不管對於什麽業務,基本都是最重要的一個環節,為了避免產生重大問題,需要做到以下兩點:

    1. 嚴格保證線下測試的準確性。

    2. 出現線上問題,影響範圍要盡可能小。

    3.1 流程測試

    根據帳號訂單流程的特點,在測試的時候遵循以下原則:

  • 訂單流程正常跑通

  • 訂單分帳正確

  • 訂單保險正常

  • 各個節點與原來保持一致

  • 相關push、私信正常發送

  • 統計日誌正常打印

  • 對於每一種訂單流程,同時進行新、老流程訂單的測試。逐一對比新、老流程的買家側和賣家側各個流程節點的頁面、按鈕、跳轉、push、私信等是否保持一致。

    3.2 灰度策略

    為了避免產生重大問題,上線後必須采取灰度策略,不然出了問題就可能就是事故了。本次采用的灰度策略是上線後按訂單型別、訂單量進行灰度,同時將灰度訂單落表記錄,配置如下:

    [
    {
    "orderType"6,//訂單型別
    "dayNum"50,//每日灰度量
    "isTotalGray"true//是否全量
    }
    ]
    /**
    * 判斷訂單是否走新交易流程
    */

    publicbooleanisNewOrderProcess(String logStr, GameAccountOrderContext orderContext){
    Long orderId = orderContext.getOrderId();
    try {
    if (gameGrayTestService.isNewTradeProcessOrder(orderId)){
    returntrue;
    }
    GameAccountOrderResultEntity orderEntity = accountOrderManage.getGameAccountOrderEntity(orderId, logStr);
    GameAccountTradeFlow.GameAccountTradeType orderTradeType = orderContext.getTradeType();
    String orderRedisSet = String.format("account_order_gray_set_%s_%s", Objects.nonNull(orderEntity) ? orderEntity.getSelfType() : orderTradeType.getSelfType(), DateUtil.format(new Date(), "yyyy-MM-dd"));
    if (ZZGameRedisUtil.sismember(orderRedisSet, orderId.toString())){
    returntrue;
    }
    if (newAccountOrderTradeSwitch){
    returntrue;
    }
    Optional<OrderGrayConfig> grayConfigOptional = grayConfigList.stream().filter(c->c.getOrderType() == orderTradeType.getSelfType()).findFirst();
    if (grayConfigOptional.isPresent()){
    OrderGrayConfig grayConfig = grayConfigOptional.get();
    if (Objects.nonNull(grayConfig.getIsTotalGray()) && grayConfig.getIsTotalGray()){
    returntrue;
    }
    if (orderContext.getOrderStatus() != AccountOrderStatusEnum.place_order){//只處理新訂單
    returnfalse;
    }
    String dayNumKey = String.format(NEW_ORDER_PROCESS_GRAY_NUM, DateUtil.format(new Date(), "yyyy-MM-dd"), orderTradeType.getSelfType());
    if (NumberUtils.toInt(ZZGameRedisUtil.get(dayNumKey)) < grayConfig.getDayNum()){
    int result = gameGrayTestService.insertNewTradeProcessOrder(orderId);
    log.info("{} desc=insert_gray_order_data orderId={} result={}", logStr, orderId, result);
    if (result > 0){
    ZZGameRedisUtil.increAndGet(dayNumKey, 1);
    ZZGameRedisUtil.expire(dayNumKey, 3600*24);
    ZZGameRedisUtil.sadd(orderRedisSet, orderId.toString());
    ZZGameRedisUtil.expire(orderRedisSet, 3600*24);
    }
    return result >= 0;
    }
    returnfalse;
    }
    catch (Exception e) {
    log.error("{} desc=isNewOrderProcess_error orderId={}", orderContext.getLogStr(), orderContext.getOrderId(), e);
    }
    returnfalse;
    }

    3.3 異常機制

    在一些重要的節點設定告警機制,比如上傳賬密、發貨、提現等節點出現異常時會發送企業微信告警通知,可以第一時間關閉灰度,尋找問題。 不過對於分帳正確性保障這塊只是透過測試確保正確,這種最好是可以接入中台的BCP(Business Check Platform)系統。它是一種標準化數據校對平台,支持標準化資料來源接入,基於事件觸發規則執行,進行業務數據校對,可以及時快速的發現業務異常數據並即時告警。

    4 總結

    在對訂單流程進行重構之後,新增或修改某種訂單模式,只需增改相應的訂單型別處理類就可以了,也不用擔心本次修改會影響到其它的訂單模式,大大提高了開發效率。此外,重構程式碼可以幫助我們進一步深入了解整個業務流程,發現程式碼的壞味道,提升程式碼結構設計能力。

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:董俊

    來源:轉轉技術

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]