游戏开发物语|游戏开发中统一支付系统设计与实现及python实例代码

更新时间:2019-10-01    来源:python    手机版     字体:

【www.bbyears.com--python】

笔者公司的游戏产品已经有几款了,每次上各种渠道都是要搭配不同的计费方式,并且每开发游戏都要重复一遍痛苦的接入sdk流程

游戏的支付需要出各种报表以及统计,每个游戏单独去做对人力的消耗巨大

基于以上几点,我这边设计了统一支付系统。

这个系列一共会分两篇文章,分别对应系统的v1版和v2版,我们这一篇先从v1起介绍。

在仔细分析了国内的大多数支付sdk之后,我们梳理出游戏的支付流程大体可以实现为两类:

第三方sdk服务器进行支付结果通知

第三方sdk客户端直接返回支付结果通知,没有服务器支付结果通知。

对于调用方而言,这两种方式各有好处。

第一种方式更加安全,但是支付调用的时间相对较长

第二种方式速度更快,但是很容易被不怀好意的人破解。

接下来,我们来看一下我这边设计的统一支付流程。

客户端:

 

1

 

服务器端:

 

2

 

简单解释一下:

每次支付开始,都要让服务器生成一个订单作为此次支付的记录,订单的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包的话,维护成本过高

总体差不多就是这样。

本文来源:http://www.bbyears.com/jiaocheng/70641.html

猜你感兴趣

热门标签

更多>>

本类排行