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 总结
在对订单流程进行重构之后,新增或修改某种订单模式,只需增改相应的订单类型处理类就可以了,也不用担心本次修改会影响到其它的订单模式,大大提高了开发效率。此外,重构代码可以帮助我们进一步深入了解整个业务流程,发现代码的坏味道,提升代码结构设计能力。