【www.bbyears.com--python】
笔者公司的游戏产品已经有几款了,每次上各种渠道都是要搭配不同的计费方式,并且每开发游戏都要重复一遍痛苦的接入sdk流程
游戏的支付需要出各种报表以及统计,每个游戏单独去做对人力的消耗巨大
基于以上几点,我这边设计了统一支付系统。
这个系列一共会分两篇文章,分别对应系统的v1版和v2版,我们这一篇先从v1起介绍。
在仔细分析了国内的大多数支付sdk之后,我们梳理出游戏的支付流程大体可以实现为两类:
第三方sdk服务器进行支付结果通知
第三方sdk客户端直接返回支付结果通知,没有服务器支付结果通知。
对于调用方而言,这两种方式各有好处。
第一种方式更加安全,但是支付调用的时间相对较长
第二种方式速度更快,但是很容易被不怀好意的人破解。
接下来,我们来看一下我这边设计的统一支付流程。
客户端:
服务器端:
简单解释一下:
每次支付开始,都要让服务器生成一个订单作为此次支付的记录,订单的id即为 bill_id。订单有4中状态:订单生成,支付失败,支付成功,发货成功。
pay_server即为统一支付系统的服务器端,考虑到调用量和方便调试,使用了简单的http协议+json+sign的方式
对于服务器内部,唯一麻烦的一点是,《等待pay_server支付结果通知》这个接口。因为这个http请求需要支持挂起,在第三方支付服务器通知了pay_server之后,pay_server 根据通知里面透传的bill_id 将订单状态修改后,再给客户端结果。
由于我们后端是用python实现的,所以用django+gevent的实现还是蛮简单的,示例代码如下:
if bill_id in bill_to_result:
result = bill_to_result[bill_id]
else:
result = AsyncResult()
bill_to_result[bill_id] = result
try:
ret, error = result.get(timeout=config.BILL_RESULT_TIMEOUT)
except gevent.Timeout:
logger.error("result wait timeout. bill_id: %s", bill_id)
return jsonify(
ret=config.RET_WAIT_BILL_RESULT_TIMEOUT,
msg=u"超时没有返回"
)
except:
logger.fatal("exc occur. bill_id: %s", bill_id, exc_info=True)
return jsonify(
ret=config.RET_SERVER_BUSY,
msg=u"服务器繁忙"
)
else:
logger.error("notify result succ. bill_id: %s", bill.id)
return jsonify(
ret=ret,
error=error,
sign=make_sign(secret, request.path, bill.id)
)
finally:
if bill_to_result.get(bill_id) == result:
bill_to_result.pop(bill_id, None)
当收到第三方支付服务器的结果通知时,添加如下代码即可:
def notify_bill_result(bill_id, ret, error):
result = bill_to_result.get(bill_id)
if result:
result.set((ret, error))
再来看一下支付存储需要的字段是哪些:
class Bill(models.Model):
userid = models.CharField(max_length=64)
# 支付物品唯一ID
item_id = models.CharField(max_length=255)
source = models.CharField(max_length=32)
# 渠道,类似 CN_91 这种
channel = models.CharField(max_length=64, null=True, blank=True)
create_time = models.DateTimeField(default=datetime.datetime.now)
state = models.IntegerField(default=config.BILL_STATE_INIT, choices=[
(config.BILL_STATE_INIT, u"订单生成"),
(config.BILL_STATE_FAIL, u"支付失败"),
(config.BILL_STATE_SUCC, u"支付成功"),
(config.BILL_STATE_DELIVERED, u"发货成功"),
])
bill_type = models.IntegerField(choices=config.BILL_TYPE_CHOICES)
# 只有当是储值类型的会生效
lz_amt = models.FloatField(null=True, blank=True)
# 币种
lz_cur = models.CharField(max_length=64, null=True, blank=True)
# 兑换的内部游戏币
lz_coin = models.IntegerField(null=True, blank=True)
# 会被透传的信息
passinfo = models.TextField(null=True, blank=True, default="")
# 系统 android/ios
os = models.CharField(max_length=16)
# 应用的版本
app_version = models.IntegerField(null=True, blank=True)
# sdk的版本
sdk_version = models.IntegerField(null=True, blank=True)
# 如果是映射的用户,比如平安这种
map_userid = models.CharField(max_length=255, null=True, blank=True)
# 不可以用dict(),默认参数的问题一定要牢记
extra = jsonfield.JSONField(default=dict, null=True)
需要注意的是extra字段,客户端可以将金额,描述等字段都放入其中。
整个支付的流程大体就是这样了,还需要多聊一点的是关于客户端支付sdk封装的问题。
目前是封装了一个统一支付sdk,将所有第三方sdk需要的参数传入,并根据传入的bill_type来启动对应的支付方式。
这样做的好处是:
只要调通一种支付方式,基本所有的支付都调试通过了
而缺点是:
依赖的第三方sdk太多,难以定制只使用几个sdk的版本
第三方sdk之间非常容易出现冲突
支持新的sdk非常麻烦,很容易影响其他支付
所以其实比较下来,这种客户端sdk的封装方式实际是比较差劲的,所以才会有了接下来的v2版本,升级的具体内容将会再下篇介绍。
一. 解决服务器端分布式的问题
解决这个问题的核心思路比较简单:
之前我们是把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包的话,维护成本过高
总体差不多就是这样。