发现自己经常会一篇文章写了(一)之后,很久都不写(二),搞得最后自己都快要忘记了,所以这次赶紧把统一支付的文章给补上。
上次的文章中将统一支付的v1版本已经讲解ok了,但是还剩下两个问题:
- 服务器端没有办法做分布式
- 客户端对支付sdk进行插件式管理十分困难
我们一个个来说
一. 解决服务器端分布式的问题
解决这个问题的核心思路比较简单:
之前我们是把event的通知放在进程内存中,现在我们做成网络通信
由于支付的请求量本身不属于高并发,所以就放弃了打算直接写通知server的想法,转而看一下有没有什么简单的解决方案。
而由于自己之前redis的使用经历,恰好知道redis有一个pubsub模式,很适合做这种监听和通知的工作。
python的实现示例代码如下:
import time
import config
from share.vals import rds
from share.utils import safe_str
from gevent.timeout import Timeout
from urllib import quote, unquote
class RedisPubSub(object):
"""
用redis订阅/发布消息
"""
# 订阅频道
sub_key = None
pub_sub = None
channel = None
__prefix = 'unity_pay:%s'
def __init__(self, channel):
self.channel = channel
self.sub_key = self.get_sub_key(self.channel)
def __del__(self):
self.unsubscribe()
def subscribe(self):
if self.pub_sub:
return
self.pub_sub = rds.pubsub()
self.pub_sub.subscribe(self.sub_key)
def unsubscribe(self):
if not self.pub_sub:
return
self.pub_sub.unsubscribe()
self.pub_sub = None
def set(self, *args):
"""
设置订阅消息
"""
rds.publish(self.sub_key, self.format(*args))
def get(self, timeout=config.BILL_RESULT_TIMEOUT):
"""
获取订阅消息
"""
self.subscribe()
stamp = time.time() + timeout
while time.time() < stamp:
message = self.pub_sub.get_message(True)
if message and message['type'] == 'message':
return self.parse(message.get('data', ''))
time.sleep(1)
raise Timeout
@classmethod
def format(cls, *args):
return ','.join([quote(safe_str(arg)) for arg in args])
@classmethod
def parse(cls, message):
args = message.split(',')
return [unquote(arg) for arg in args]
@classmethod
def get_sub_key(cls, channel):
return cls.__prefix % channel
代码很简单,就不多解释了。
这样做的坏处是redis会有热点问题,不过反正redis中也不存放数据,找台热备机随时能切换即可。
二. 客户端对支付sdk进行插件式管理十分困难
其实这个问题也不是很难,解决的关键是需要知道一个点:
jar包在编译的时候需要所有的类都存在,但是当程序调用这个jar包时,这个jar包有些类不存在,并不会崩溃,而是报可被捕获的异常
基于这一点,我们就可以做一个同一个工厂函数,将这个工厂函数类封装成一个jar包。
同时,我们对每一种支付方式,都封装出一个统一的接口,而工厂函数返回的即这样一个接口的实现。当某一种支付方式的封装类不存在时,就捕获这个异常,并返回NULL。
统一接口的代码如下:
public abstract class PaymentInterf {
/**
* 初始化统一方法
* @param context 上下文
* @param parameters 初始化时需要的参数 数组
* 1、移动 parameters--》mmid 、mmkey
* 2、联通 parameters:
* string appid 应用编号
* string cpCode 开发商编号
* string cpid 开发商VAC资质编号
* string company 开发者公司名字
* string phone 开发者客服电话号码
* string game 应用名称
* UnipayPayResultListener mCallBack 初始化函数回调结果(目前 只有联通多了一个非String类型的参数)
*
* 3、Amazon初始化
* suk
* callBack
*
*/
public abstract void init(Context context,Object...parameters);
/**
* 支付统一函数
* @param context 上下文
* @param parameters 支付时需要传递的参数 如 payCode billId 。。。
*/
public abstract void pay(Context context,Object...parameters);
public static void billDeliver(String appKey, String billID) {
HttpLobbiesService.g().billDeliver(appKey, billID, new HttpLobbiesService.Callback() {
@Override
public void onResult(ResultInfo info) {
if (info != null && info.getRetCode() == 0) {
Log.e("billDeliver", info.getRetCode() + "/:" + info.getObj());
}
}
});
}
}
工厂函数的代码如下:
public class PaymentFactoy {
public static PaymentInterf producePay(int billTypy){
try {
switch (billTypy) {
case Constant.BILL_TYPE_MMCNCCPAY:
return MMPayInstance.getInstance();
case Constant.BILL_TYPE_DIANXIN:
return CTEStoreInstance.getInstance();
case Constant.BILL_TYPE_UNIPAY:
return UnicomInstance.getInstance();
case Constant.BILL_TYPE_TAOBAO:
return TaoBaoInstance.getInstance();
case Constant.BILL_TYPE_WEIPAY:
return WeiPayInstance.getInstance();
case Constant.BILL_TYPE_WIMI:
return WeiMiInstance.getInstance();
default:
break;
}
} catch (Exception e) {
e.printStackTrace();
return null;
}
return null;
};
}
上面的方法是只封装了一个factory函数的jar包,其他的对每种支付的封装还是走源码的方式。
其实最早的时候,我是想将每种支付的封装也做成jar的方式,后来公司同事做成了现在的这种方式,我考虑了一下,可能确实用源码的方式更好,原因如下:
- 源码的方式,方便调试
- 源码的方式,当编译进游戏的时候,如果某种支付忘记引入对应的jar包,会直接报错提醒
- 每个支付方式一个jar包的话,维护成本过高
总体差不多就是这样。
Moses.Fu on #
nice essay,对我的android开发提供了很多值得借鉴的地方
Reply
jamesduan on #
大神,我是一名python初学者,我想问一下你上面写的代码中,@classmethod下面的代码和普通的方法有什么区别吗?
Reply
Dante on #
class Foo(object): passclassmethod 可以 Foo.func(),即第一个参数是 cls 而不是 self。
Reply
歪妖内涵网 on #
太厉害啦!值得我们学习
Reply