轉轉游戲的賬號訂單流程重構之路
1、背景
隨著需求的不斷迭代,項目代碼的復雜度也會越來越高,“屎山”也一天一天慢慢的堆積起來,對于游戲業務的賬號訂單流程也是如此。游戲訂單類型由原來的倆種增加到了現在的七種,早就已經到了需要重構的地步。但是由于牽涉流程長、影響范圍大、平時需求排期也比較緊張,所以便無法抽出大量的時間去進行重構。因為一開始就設計的不夠規范、合理,所以之前整個賬號訂單流程存在以下主要問題:
1.1 核心代碼分布散亂
除了按照原子層、服務層劃分之外,還有一個服務用于接收訂單mq進行大量的處理操作,訂單相關的接口分布于多個類甚至是一些名稱與訂單毫不相關的類當中。
1.2 缺少設計模式
缺少設計模式,各種節點、不同訂單類型的邏輯基本都是通過各種if-else進行處理,耦合度較高,可讀性、可擴展性和可維護性都較差,甚至會出現修改一種訂單流程反而影響到了其它訂單流程的情況。
1.3 影響交付效率
代碼分布在多個服務當中,開發一個相關需求時經常需要拉4、5個項目分支。由于容易修改到了其它訂單模式的代碼,所以在測試的時候往往又需要回歸其它模式的訂單流程是否有受影響。這些都大大影響了開發測試以及最終交付的效率。
現在業務趨于穩定,需求迭代也沒有這么快了,因此就有了重構訂單流程的想法。最終的目的就是為了保證良好的可讀性、可維護性和可擴展性。有了重構想法的之后,產生了許多問題,主要如下:
- 怎么進行重構呢,用什么設計模式?
- 重構后的測試上線怎么進行呢?
- 如果上線出現問題要怎么處理?
接下來就圍繞這幾個問題來敘述一下賬號訂單流程的重構之路。
2、如何重構
2.1 方案確定
先簡單介紹一下游戲賬號交易的流程,最開始的時候有兩種交易方式,分別是客服發貨交易和自主發貨交易。兩者最大的區別是是否需要第三角色客服的介入,后來七種訂單交易模式都是在這兩種模式基礎上誕生的。
既然有七種訂單類型,這好辦啊??梢圆捎貌呗?模板模式啊,一個抽象模板+七個子類就可以啦。但是后來仔細一想,如果將所有的處理邏輯都放在父類和子類當中,其實代碼整體也顯得十分臃腫。
為了想出更好的解決方案,于是對原有代碼和業務流程進行了深入的梳理和總結,主要有以下幾點:
- 所有訂單流程都是在客服發貨和自主發貨基礎上衍生出來的。
- 所有訂單流程都包含下單、支付、上傳賬密、發貨、確認收貨等節點。
- 在這些節點里不同訂單類型大多會有各自一些特定操作,但是這些操作其實并不屬于訂單的主流程。
通過以上分析,是不是可以將下單到確認收貨作為一層,將不同訂單類型的特定處理實現作為一層呢?這樣不就將訂單流程中各種特殊處理從訂單主流程剝離開了嗎,因此最終決定采用三層接口+策略模板的設計方案。
2.2 三層接口+策略模板模式
接口設計如下:
- 第一層接口
包含前端用戶進行交互、處理mq消息以及給其它服務調用的接口。
- 第二層接口
訂單核心主流程能力接口。將下單、支付到確認收貨等“不變”的基礎能力提供給頂層接口調用,這層接口有自主發貨流程和客服發貨流程兩個實現類。
public interface IGameAccountOrderDealProcess {
/**
* 處理下單未支付訂單
*/
int handlePlaceOrder(GameAccountOrderContext orderContext) throws Exception;
/**
* 處理支付成功訂單
*/
int handlePaySuccessOrder(GameAccountOrderContext orderContext) throws Exception;
/**
* 處理已發貨訂單
*/
int handleDeliverOrder(GameAccountOrderContext orderContext) throws Exception;
/**
* 處理支付前取消訂單
*/
int handleCancelBeforePayOrder(GameAccountOrderContext orderContext) throws Exception;
/**
* 處理支付后取消訂單
*/
int handleCancelAfterPayOrder(GameAccountOrderContext orderContext) throws Exception;
/**
* 處理交易成功訂單
*/
int handleConfirmReceiptOrder(GameAccountOrderContext orderContext) throws Exception;
/**
* 賬號交易窗數據
*/
<T extends TradeFlowData> T 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
*/
boolean deliverOrder(GameAccountOrderContext orderContext) throws Exception;
/**
* 訂單確認收貨
*/
ZZOpenScfBaseResult<String> confirmReceiptOrder(GameAccountOrderContext orderContext, Long uid, boolean needCheckRisk) throws Exception;
}
- 第三層接口
各種訂單類型的特殊處理,每一種訂單模式都對應一個實現類。
public interface ITradeSelfHandler {
GameAccountTradeFlow.GameAccountTradeType getOrderTrade();
/*------------處理mq消息相關---------------*/
/**
*1.插入表之前設置客服和extendInfo
*/
void fillExtraOrderInfoBeforeInsert(GameAccountOrderResultEntity orderEntity, GameAccountOrderContext orderContext);
/**
* 下單后處理
*/
void handleAfterPlaceOrder(GameAccountOrderContext orderContext);
/**
* 支付前取消處理
*/
void handleCancelBeforePay(GameAccountOrderContext orderContext);
/**
* 支付后取消處理
*/
int handleCancelAfterPay(GameAccountOrderContext orderContext) throws Exception;
/**
* 支付后一些額外處理
*/
int handleAfterPaySuccess(GameAccountOrderContext orderContext);
/**
* 確認收貨處理
*/
int handleAfterConfirmReceipt(GameAccountOrderContext orderContext) throws Exception;
/*---------------------------------*/
/**
* 獲取提現時間
*/
Date getWithDrawlTime();
/**
* 發送支付成功push
*/
void orderAlreadyPayPushMsgNew(GameAccountOrderContext orderContext, Pair<String, String> jumpUrl);
/**
* 獲取分帳賬戶、類別信息
*/
List<AccountOrderSplitModel> getOrderSplitModelList(GameAccountOrderContext orderContext, OrderMaxSettleInfo settleInfo);
/**
* 定制各自spiUi
*/
void buildOrderSpiUiData(GameAccountOrderContext orderContext, GameOrderSpiConfig bConfig, GameOrderSpiConfig sConfig, SpiUiData spiUiData) throws Exception;
/**
* 確認收貨后一些處理
*/
void otherOperationAfterReceipt(GameAccountOrderContext orderContext, Long uid);
}
2.3 具體實現
- 核心代碼收攏到一個服務,相關接口進行聚合
原先在客服后臺、定時任務、mq集群都有一些訂單的操作,但是這些代碼基本都是重復的,所以此次重構在訂單核心服務中新增相應的訂單操作功能,統一由其它服務進行RPC調用。
將訂單相關的接口、工具類集中到同一個包下,方便定位。
- 整體類圖及設計原則
- 命名規范:類名、變量名、方法名盡量見名知義。
- 單一職責:各個模塊各司其職,避免與其它模塊過度耦合。
- 準備訂單上下文,清除RPC重復調用問題。
//上下文實體
public class GameAccountOrderContext {
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、上線保障
訂單流程不管對于什么業務,基本都是最重要的一個環節,為了避免產生重大問題,需要做到以下兩點:
- 嚴格保證線下測試的準確性。
- 出現線上問題,影響范圍要盡可能小。
3.1 流程測試
根據賬號訂單流程的特點,在測試的時候遵循以下原則:
- 訂單流程正常跑通
- 訂單分帳正確
- 訂單保險正常
- 各個節點與原來保持一致
- 相關push、私信正常發送
- 統計日志正常打印
對于每一種訂單流程,同時進行新、老流程訂單的測試。逐一對比新、老流程的買家側和賣家側各個流程節點的頁面、按鈕、跳轉、push、私信等是否保持一致。
3.2 灰度策略
為了避免產生重大問題,上線后必須采取灰度策略,不然出了問題就可能就是事故了。本次采用的灰度策略是上線后按訂單類型、訂單量進行灰度,同時將灰度訂單落表記錄,配置如下:
[
{
"orderType": 6,//訂單類型
"dayNum": 50,//每日灰度量
"isTotalGray": true//是否全量
}
]
/**
* 判斷訂單是否走新交易流程
*/
public boolean isNewOrderProcess(String logStr, GameAccountOrderContext orderContext) {
Long orderId = orderContext.getOrderId();
try {
if (gameGrayTestService.isNewTradeProcessOrder(orderId)){
return true;
}
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())){
return true;
}
if (newAccountOrderTradeSwitch){
return true;
}
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()){
return true;
}
if (orderContext.getOrderStatus() != AccountOrderStatusEnum.place_order){//只處理新訂單
return false;
}
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;
}
return false;
}
} catch (Exception e) {
log.error("{} desc=isNewOrderProcess_error orderId={}", orderContext.getLogStr(), orderContext.getOrderId(), e);
}
return false;
}
3.3 異常機制
在一些重要的節點設置告警機制,比如上傳賬密、發貨、提現等節點出現異常時會發送企業微信告警通知,可以第一時間關閉灰度,查找問題。
不過對于分帳正確性保障這塊只是通過測試確保正確,這種最好是可以接入中臺的BCP(Business Check Platform)系統。它是一種標準化數據校對平臺,支持標準化數據源接入,基于事件觸發規則執行,進行業務數據校對,可以及時快速的發現業務異常數據并實時告警。
4 總結
在對訂單流程進行重構之后,新增或修改某種訂單模式,只需增改相應的訂單類型處理類就可以了,也不用擔心本次修改會影響到其它的訂單模式,大大提高了開發效率。此外,重構代碼可以幫助我們進一步深入了解整個業務流程,發現代碼的壞味道,提升代碼結構設計能力。