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

代码逻辑
上面的就是设计图,这里再简单的说下代码的结构逻辑是咋样的,其实做过挺多次类似的处理了,这两种模式的设计还是挺有意义的,以下只是稍微说下大概的方式
AbstractChannelStrategy
@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
@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
@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方法并不是每个渠道都需要的,所以并不需要弄为抽血方法,可以提供默认实现,子类根据需要实现即可,以上是改动的代码
改之前的结构
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来源,然后子类跟它绑定起来就可以,代码如下
抽象类添加
//sdk端定义的渠道来源
public abstract String getSdkChannelSource();
子类实现
@Override
public String getSdkChannelSource() {
return "oppo";
}
上下文中添加获取即可,省略了这里
补充三
就如同补充2一样,有了设计模式的支持,后续的逻辑就相当好扩展了,因为vivo要求在前端下单时还要返回加密信息,所以在通用类提供了以下的方法,然后提供默认实现,目前只有vivo需要实现,其他渠道的得后面才知道了,代码如下
AbstractChannelStrategy中添加
public String getPaySignature(OrderInfoDto orderInfoDto) {
return doGetPaySignature(orderInfoDto);
}
//默认返回""
protected String doGetPaySignature(OrderInfoDto orderInfoDto) {
return "";
}
然后就是vivo的实现子类对doGetPaySignature进行覆盖实现即可
补充四
这点补充意义就比较大了,由于每个应用的appID和appSecret都是不一样的,而且每个渠道可能会有多个游戏,因此这个东西肯定是动态的,那么设计模式的意义就来了,只需要把这个数据统一查询后注入到子类中即可,子类就能直接使用
1. 在AbstractChannelStrategy添加方法quertChannelConfig获取配置信息
2. 在doGetChannelAccount进行注入,有类似方法时都是一样的
3. 子类直接使用注入类,如果其他地方需要用到,直接调用quertChannelConfig获取,目的是为了后面修改时好调整,代码如下
AbstractChannelStrategy
添加quertChannelConfig()获取配置信息
doGetChannelAccount(配置信息) 把配置信息注入进去
其他子类
1. doGetChannelAccount(配置信息)使用配置信息
2. 其他需要的地方调用quertChannelConfig获取配置信息使用
补充五-回调思路
接补充三来说,代码里面有 String doGetPaySignatur(OrderDto orderDto) 方法,但不是每个渠道都需要用到的,因此默认是返回了"",上层调用的是方法
public String getPaySignature(OrderDto orderDto) {
return doGetPaySignature(orderDto );
}
实现的子类需要用到Config配置信息,直接查询当然可以实现,如下
public String getPaySignature(OrderDto orderDto) {
//获取配置信息--注入
return doGetPaySignature(orderDto );
}但是这样一来所有不需要实现该方法的也会执行这个查询,虽然放在了缓存中,但是这个步骤仍然是可以省略的,这时就有一个很好的思路,用回调函数来做即可,改完如下
public String getPaySignature(OrderDto orderDto) {
//获取配置信息--注入
Supplier<Config> configSupplier = () -> getConfig(); //查询配置类信息
return doGetPaySignature(orderDto,configSupplier );
}子类使用时
String doGetPaySignatur(OrderDto orderDto,Supplier<Config> configSupplier ) {
configSupplier.get() ; // 进行使用
}
总结:这种方式可以更好的节省查询,因为只有需要的时候去调用才需要使用,默认不需要的根本不会触发这个查询
补充六
由于发货时有些参数需要依赖回帖回传的数据,所以增加了ext_json字段在回调处理成功后进行保存,然后发货的时候提取出来使用
同时有些数据是比较敏感需要加密的,因此父类提供方法用来判断数据是否需要加密,默认是不需要的
方法为:
protected boolean needEncry() {return false}
如果子类需要加密覆盖needEncry方法返回true即可,然后父类在入库时判断是否为true,是的话则加密后再入库,取出来的时候如果判断needEncry为true那么接解密完传给子类,这样的设计对于子类来说是透明的,无须关注,只要确定是否需要安全
比如华为的token就需要加密处理,因为覆盖了该方法返回true
对接遇到的问题
问题一
TapTap OAuth 接口 | TapTap 开发者文档,在对接taptap登录时我直接用代码去请求一直报错,但是用postman就正常,一直很好奇啥原因,我请求的代码如下,根据taptap要求只是需要设置请求头即可,这里就不细说了,请求后一直返回
java.io.IOException: Server returned HTTP response code: 403 for URL: https://open.tapapis.cn/account/profile/v1?client_id=xxxx
但是用postman去请求就正常,返回了
{
"data": {
"code": -1,
"msg": "当前用户未授权获取此产品(服务)",
"error": "insufficient_scope",
"error_description": "Insufficient Scope"
},
"now": 1755052576,
"success": false
}虽然也是用户信息问题,但是正常返回了
public static String get(String url, Map<String, String> params, Map<String, String> headers){
if (params != null && params.size()>0) {
url = url+"?"+toParams(params);
}
String result = "";
BufferedReader in = null;
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置请求头
for (String key : headers.keySet()){
connection.setRequestProperty(key,headers.get(key));
}
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
@SuppressWarnings("unused")
Map<String, List<String>> map = connection.getHeaderFields();
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
} catch (Exception e) {
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
排查思路
1. 刚开始以为是请求头问题,就把postman的请求头也放过去,仍然不行
2. 试了下okhttpclient事确实可以,但是对这个问题很好奇就很想看下
3. 最终找了挺多资料,发现原来的代码当返回码不是200时没有对错误码进行处理,因此代码加上错误的代码处理,如下,其实就改了两个东西
a. 获取了HttpURLConnection 并对成功响应进行判断
b. 错误时从错误流里面读数据connection.getErrorStream()
弄完以后调用就变成了下面的返回,这个才是对的
{"data":{"code":-1,"msg":"当前用户未授权获取此产品(服务)","error":"insufficient_scope","error_description":"Insufficient Scope"},"now":1755056755,"success":false}
这个问题并不复杂,只是习惯性还是记录一下
public static String getAllowError(String url, Map<String, String> params, Map<String, String> headers){
if (params != null && params.size()>0) {
url = url+"?"+toParams(params);
}
String result = "";
BufferedReader in = null;
try {
URL realUrl = new URL(url);
// 打开和URL之间的连接
HttpURLConnection connection = (HttpURLConnection) realUrl.openConnection();
// 设置请求头
for (String key : headers.keySet()){
connection.setRequestProperty(key,headers.get(key));
}
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
@SuppressWarnings("unused")
Map<String, List<String>> map = connection.getHeaderFields();
int code = connection.getResponseCode();
if(code == 200){
in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result += line;
}
}else{
if (code == 500) {
throw new IOException("HttpUtil报错信息:500错误,认证服务器发生内部错误!请检查远程接口");
}
//400异常进行处理
// 读取错误流
try (BufferedReader error = new BufferedReader(new InputStreamReader(connection.getErrorStream(), StandardCharsets.UTF_8))) {
String errorLine;
while ((errorLine = error.readLine()) != null) {
result += errorLine;
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
// 使用finally块来关闭输入流
finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
return result;
}
问题二-应用宝证书问题
应用宝回调时需要配置证书,按照官网的配置进行配置 腾讯开放平台 OPEN.QQ.COM,但是一直回调不回来,主要是出现了以下几个问题
1. 回调地址修改后证书要重新下载、环境必须发布,
2. nginx加载报ee too small 问题是因为版本问题,需要进行调整
上面两个问题官网有解决方案:腾讯开放平台 OPEN.QQ.COM
3. 回调的payItem出现了中文导致nginx无法处理从而我这边一直不能回调,后面是改为了如果有中文就URL编码后再去提交
注:这个中文问题可能正常会想不到,因为有证书问题所以一直在考虑是否证书的问题,后来找到他们说接口报400才逐步排查到这个问题上面来
在找的过程中刚开始还怀疑是否ca.crt也需要配置,参考了这文章,但是发现并不需要,官网也说不需要,可能是特定情况才要:
后面再整理一下其实并不复杂,就是有点麻烦,我的是Nginx,主要是要做几件事
1.配置新域名,因为原来的域名有证书,zxc.com
2.从官网下载证书,取出 xxx_xxx.crt 和 ttt_ttt.key, ca.crt目前我用不上
3.nginx做如下的配置
server {
listen 443 ssl;
server_name zxc.com;
ssl_session_timeout 5m;
ssl_certificate cert/tencent/xxx_xxx.crt;
ssl_certificate_key cert/tencent/ttt_ttt.key;
}4. 如果nginx是高版本需要进行相关的配置,可以参考官网方案,ee key too small问题
看起来也不麻烦是吧,麻烦的是每个游戏的证书都不一样,这也就是说每个游戏都得单独起一个域名来承载,虽然内部逻辑是一样的
注意点:回调接口变了证书要重新下载、回调验签需要 secret + "&",就是在你原来的密钥后面要加上 &,我也不知道为啥这么设计,因为请求他们的时候又不需要加这个东西
问题3-加密
在对接抖音小游戏的时候需要加密,但是官方文档没有很明显的说明算法,只是简单的说用HMAC-SHA256进行加密,用Java原生的逻辑发不过去,最终用
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.17.1</version>
</dependency>HmacUtils hmacUtils = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey); return hmacUtils.hmacHex(message); // 返回十六进制格式
才可以,最后排查问题是他要的是16进制的加密数据,但是官方文档并没有说明,但是也让我们知道了有时候加密的东西用现成封装好的工具类更好处理

1847

被折叠的 条评论
为什么被折叠?



