发现自己经常会一篇文章写了(一)之后,很久都不写(二),搞得最后自己都快要忘记了,所以这次赶紧把统一支付的文章给补上。
上次的文章中将统一支付的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