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