其实想跟大家分享这套支付系统的架构已经很久了,今天总算有时间写出来了。
先说说这套系统的需求由来吧:
- 笔者公司的游戏产品已经有几款了,每次上各种渠道都是要搭配不同的计费方式,并且每开发游戏都要重复一遍痛苦的接入sdk流程
- 游戏的支付需要出各种报表以及统计,每个游戏单独去做对人力的消耗巨大
基于以上几点,我这边设计了统一支付系统。
这个系列一共会分两篇文章,分别对应系统的v1版和v2版,我们这一篇先从v1起介绍。
在仔细分析了国内的大多数支付sdk之后,我们梳理出游戏的支付流程大体可以实现为两类:
- 第三方sdk服务器进行支付结果通知
- 第三方sdk客户端直接返回支付结果通知,没有服务器支付结果通知。
对于调用方而言,这两种方式各有好处。
- 第一种方式更加安全,但是支付调用的时间相对较长
- 第二种方式速度更快,但是很容易被不怀好意的人破解。参见之前的文章:google支付接口被刷以及解决方案
接下来,我们来看一下我这边设计的统一支付流程。
客户端:

服务器端:

简单解释一下:
- 每次支付开始,都要让服务器生成一个订单作为此次支付的记录,订单的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版本,升级的具体内容将会再下篇介绍。
评论
暂无评论