背景
最近公司想把游戏包上到各个渠道上,因此需要对接各种渠道,渠道如下,oppo、vivo、华为、小米、应用宝、taptap、荣耀、三星等应用渠道
主要就是对接登录、支付接口(后续不知道会不会有其他的),由于登录、支付前置都是一些通用的逻辑处理,所以一开始我就想到了要用设计模式来做,下面我画了一个图,大家可以参考看一下
这里值得一提的是目前oppo比常规的渠道多了一个发货通知的接口,我把几个接口都说说明下
登录:用户通过oppo、vivo等进行登录并绑定我们内部账号中
支付回调:用户使用oppo支付成功后要通知我们用户下单成功,我们需要进行处理发货
发货通知:我们发货成功后通知oppo发货成功了
前面两个接口其实很好理解,主要是发货通知,官方的文档如下,看了也许你就明白了,如下
如果 OPPO 服务端超过2 个小时仍未收到游戏服务端的发货结果请求,则代表该笔订单发货失败,将进入可退款状态,用户可自助选择是否退款,如果用户选择退款,将进入OPPO 退款流程
对了没错,用户可以退款,我认为就是在对标苹果吧,目前看小米和vivo都不需要有这个接口
流程图

代码逻辑
上面的就是设计图,这里再简单的说下代码的结构逻辑是咋样的,其实做过挺多次类似的处理了,这两种模式的设计还是挺有意义的,以下只是稍微说下大概的方式
AbstractChannelStrategy
java
@Slf4j
public abstract class AbstractChannelStrategy{
@Resource
protected ZXCUserParentDao userParentDao;
@Resource
protected ZXCUsermpl userRepo;
@Resource
private ZXCOrderoDao orderInfoDao;
@Resource
protected ApplicationContext applicationContext;
@Resource
protected ZXCAppRepoImpl gameAppRepo;
//获取用户注册类型
public abstract UserTypeEnum getUserRegisterTypeEnum();
//获取回调Url地址
protected abstract String getCallBackUrlSuffix();
//真正的支付成功处理
public abstract void doNotifyOrderHandle(ChannelNotifyBaseBo notifyBaseBo, ChannelNotifyOrderResultBO notifyOrderResultBO, OrderInfo orderInfo) throws Exception;
//游戏发货成功后的处理
protected abstract DeliveryCallBackResultBo doDeliveryCallback(OrderInfo orderInfo, GameApp gameApp) throws Exception;
//允许子类校验订单的其他信息
protected void checkOrderOtherInfo(OrderInfo orderInfo){}
public boolean notifyOrderHandle(ChannelNotifyBaseBo notifyBaseBo) {
try {
if(notifyBaseBo == null) {
return false;
}
//检查订单基础信息
OrderInfo orderInfo = commonOrderCheck(notifyBaseBo.getOrderNumber());
checkOrderOtherInfo(orderInfo);
ChannelNotifyOrderResultBO notifyOrderResultBO = new ChannelNotifyOrderResultBO();
doNotifyOrderHandle(notifyBaseBo, notifyOrderResultBO, orderInfo);
boolean success = notifyOrderResultBO.isSuccess();
//成功后处理后续
if(success) {
log.info("notifyOrderHandle-订单号{}处理成功,进行后续处理", orderInfo.getOrderNumber());
successOrder(orderInfo, notifyOrderResultBO.getTradeNo());
}
return success;
} catch (Exception e) {
log.error("doNotifyOrderHandle出现异常", e);
throw new AppException(ErrorCode.SYS_OPERATOR_ERROR.code(), "doNotifyOrderHandle调用时出现异常");
}
}
public boolean deliveryCallback(String orderNumber) {
OrderInfo orderInfo = assertOrderInfo(orderNumber);
GameApp gameApp = gameAppRepo.getByAppNumber(orderInfo.getAppNumber());
if(!Objects.equals(channelOrderRel.getRequestStatus(), 0)) {
log.info("AbstractChannelAccountStrategy-deliveryCallback订单号为{}的请求状态不为未处理,无须处理,此时状态为{},来源为{}", orderNumber, channelOrderRel.getRequestStatus(), getLogSourceName());
return true;
}
try {
DeliveryCallBackResultBo deliveryCallBackResultBo = doDeliveryCallback(orderInfo, gameApp);
boolean success = deliveryCallBackResultBo.isSuccess();
//更新状态和次数
channelOrderRel.setHttpContent(deliveryCallBackResultBo.getHttpContent());
channelOrderRel.setRequestStatus(success ? 1 : null);
int update = channelOrderRelService.update(channelOrderRel);
log.info("deliveryCallback-处理完成,更新的订单号为{},关联的channelOrderRel主键id为{},影响行数为{}", orderNumber, channelOrderRel.getId(), update);
return success;
} catch (Exception e) {
log.error("AbstractChannelAccountStrategy-deliveryCallback订单号为{}的请求处理出现异常,来源为{}", orderNumber, getLogSourceName(), e);
return false;
}
}
protected OrderInfo commonOrderCheck(String orderNumber) {
OrderInfo orderInfo = assertOrderInfo(orderNumber);
if(orderInfo.getPayWay() != null && !Objects.equals(getOrderPayWay().getCode(), orderInfo.getPayWay())) {
throw new ZXCException(ZXCCode.OPERATOR_ERROR.code(), "AbstractChannelAccountStrategy-订单号支付方式不对,此时订单号为" + orderNumber);
}
return orderInfo;
}
private OrderInfo assertOrderInfo(String orderNumber) {
OrderInfo orderInfo = orderInfoDao.getByOrderNumber(orderNumber);
if(orderInfo == null) {
throw new AppException(ErrorCode.NOT_FOUND_DATA.code(), "AbstractChannelAccountStrategy-找不到关联的订单号数据,此时订单号为" + orderNumber);
}
return orderInfo;
}
private void successOrder(OrderInfo orderInfo, String tradeNo) {
boolean updated = discountsService.updateOrderStatusAndDisCount(orderInfo, tradeNo);
if(updated){
// 发送事件
applicationContext.publishEvent(new PaySuccessEvent(this, new PaySuccessEvent.PaySuccessEventData(
orderInfo.getOrderNumber()
)));
}
}
public String buildCallBackUrl() {
//校验及简单处理一下数据
String callBackUrlSuffix = getCallBackUrlSuffix();
if(StringUtil.isBlank(callBackUrlSuffix)) {
throw new AppException("未配置回调地址");
}
if(!Objects.equals("/", callBackUrlSuffix.substring(0, 1))) {
callBackUrlSuffix = "/" + callBackUrlSuffix;
}
String callBackUrlPrefix = "https://test.cn/v1/pay/callback";
if(isLine()) {
callBackUrlPrefix = "https://prod.cn/v1/pay/callback";
}
return callBackUrlPrefix + callBackUrlSuffix;
}
虽然上面那个看起来很复杂,但是主要是子类方便,这里提供其中一个实现类
OppoStrategyImpl
java
@Slf4j
@Component
public class OppoStrategyImpl extends AbstractChannelAccountStrategy {
@Override
public UserTypeEnum getUserRegisterTypeEnum() {
return UserTypeEnum.OPPO_USER;
}
@Override
protected String getCallBackUrlSuffix() {
return "/oppo/callback";
}
@Override
public void doNotifyOrderHandle(ChannelNotifyBaseBo notifyBaseBo, ChannelNotifyOrderResultBO notifyOrderResultBO, OrderInfo orderInfo) throws Exception {
OppoOrderNotifyBo oppoOrderNotifyBo = (OppoOrderNotifyBo) notifyBaseBo; //强制对象,几乎是不可能报错的,除非调用端出了问题
//验证签名,这里是用oppo的公钥验签,因为私钥只有oppo有,所以别人无法伪造
String baseString = getBaseString(oppoOrderNotifyBo);
boolean check = OppoUtils.check(baseString, oppoOrderNotifyBo.getSign());
if(!check) {
log.info("OppoChannelAccountStrategyImpl-验签失败,此时订单号为{}", oppoOrderNotifyBo.getPartnerOrder());
return;
}
//代表成功了,对数据进行填充
notifyOrderResultBO.setSuccess(true);
notifyOrderResultBO.setTradeNo(oppoOrderNotifyBo.getNotifyId()); //可能没有
}
// 生成 baseString
private static String getBaseString(OppoOrderNotifyBo ne) {
StringBuilder sb = new StringBuilder();
sb.append("notifyId=").append(ne.getNotifyId());
sb.append("&partnerOrder=").append(ne.getPartnerOrder());
sb.append("&productName=").append(ne.getProductName());
sb.append("&productDesc=").append(ne.getProductDesc());
sb.append("&price=").append(ne.getPrice());
sb.append("&count=").append(ne.getCount());
sb.append("&attach=").append(ne.getAttach());
return sb.toString();
}
@Override
protected OppoDeliveryResult doDeliveryCallback(OrderInfo orderInfo, GameApp gameApp) throws Exception {
OppoDeliveryResult oppoDeliveryCallbackResult = OppoUtils.postDeliveryOppo(orderInfo, gameApp);
DeliveryCallBackResultBo deliveryCallBackResultBo = new DeliveryCallBackResultBo();
deliveryCallBackResultBo.setHttpContent(JsonUtils.Object2Json(oppoDeliveryCallbackResult));
deliveryCallBackResultBo.setSuccess(oppoDeliveryCallbackResult.isSuccess());
return deliveryCallBackResultBo;
}
}
看起来是不是简单多了,当然第一个意义是不大,主要是后面的vivo、小米、华为等只需要提供对应的实现类即可
而且其他的可能都不需要doDeliveryCallback这个接口的实现,因此其实还可以改进一下,在底级类上面直接提供个成功的回调实现,这里我就暂时不改了,可能改为在底层提供个propertect方法,然后直接调用也是可以的,就当保存一下所有的交互数据得了,而且可能有其他用途,就是用来主动查询订单的结果
至于引用就更简单了,通常都是有个类似于算门面的东西,如下
ChannelStrategyComponent
java
@Slf4j
@Component
public class ChannelStrategyComponent {
@Resource
private List<AbstractChannelStrategy> channelStrategyList;
@Resource
private ZXCUserParentDao userParentDao;
@Resource
private ZXCOrderDao orderInfoDao;
public AbstractChannelStrategy getChannelAccountStrategy(UserTypeEnum userTypeEnum) {
AbstractChannelStrategy channelAccountStrategy = checkChannelAccountStrategy(userTypeEnum);
if(channelAccountStrategy == null) {
throw new AppException(ErrorCode.SYS_ERROR.code(), "找不到关联的AbstractChannelAccountStrategy对象");
}
return channelAccountStrategy;
}
public AbstractChannelStrategy checkChannelAccountStrategyByOrderNumber(String orderNumber) {
Order order = orderDao.getByOrderNumber(orderNumber);
//用支付方式直接去找
for (AbstractChannelStrategy channelStrategy : channelStrategyList) {
if(Objects.equals(orderInfo.getPayWay(), channelStrategy.getOrderPayWay().getCode())) {
log.info("channelStrategy-从订单支付方式中找到处理器");
return channelStrategy;
}
}
//支付方式找不到再从用户注册类型去找,节省一部数据库查询
if(orderInfo != null) {
return checkChannelAccountStrategy(orderInfo.getUserName());
}
return null;
}
}
怎么样,看起来是不是很简单,顺带一提,上面的OppoUtils就是跟oppo对接的工具包,这个就不提供了,这部分肯定每个都不一样,需要单独写
这篇文章主要是想说明一种封装思路,而不是要说代码具体是怎么写的这个事
总结
其实这个东西并不算很复杂,可能跟我设计多次也有关系,这种思路其实我是多少有点借助spring的设计,你去看就会发现里面有很多类似这样的设计
后续补充
补充一
就像之前说的doDeliveryCallback方法并不是每个渠道都需要的,所以并不需要弄为抽血方法,可以提供默认实现,子类根据需要实现即可,以上是改动的代码
java
改之前的结构
protected abstract CallBackResultBo doDeliveryCallback() throws Exception
每个子类都得强制实现
改之后的结构
protected CallBackResultBo doDeliveryCallback() throws Exception {
return CallBackResultBo.buildDefaultSuccess();
}
默认提供成功的实现
这样一来,子类就可以根据所需来选择是否需要覆盖了,减少了不少代码
补充二
之前的登录接口是设计为多个来给安卓调用的,比如以上两个接口
oppo调用: https://zxc.com/oppo/login
vivo调用: https://zxc.com/vivo/login
但是安卓说不好区分,他想统一调一个接口,如果是以外就麻烦了,但是在设计模式的加持下,现在实现的功能就非常简单了,只需要做几个事
1.在抽象类 AbstractChannelStrategy添加 方法
2.在子类提供实现
3.在ChannelStrategyComponent提供获取方式
最后在接收的通用参数定义 channelSource来源,然后子类跟它绑定起来就可以,代码如下
java
抽象类添加
//sdk端定义的渠道来源
public abstract String getSdkChannelSource();
子类实现
@Override
public String getSdkChannelSource() {
return "oppo";
}
上下文中添加获取即可,省略了这里