freeeyes 发表于 2011-12-14 17:45:16

关于苹果服务,用户付费的处理模式

最近忙着改东西,越来越发现ICE在高并发下,IO占用的问题。当然是可以通过配置文件解决,不过毕竟文档较少,很多东西需要尝试。ICE和Python结合,还是有界限的。python进程只能占据一个CPU,就算你有100个线程也是如此,更加确认了我对虚拟脚本语言的使用方法,那就是需要多线程的地方,让C++去做,逻辑具体实现,可以让脚本代劳,前提是逻辑调用次数不能过于频繁,如果很频繁,那就C++搞定。先说一个自己最近在搞的东西,以前一直踌躇苹果的用户购买了我的一个服务,我怎么在服务器进行验证的问题。
参考了一些游戏的扣费,感觉有的快,有的慢,不过,如果无法在服务器上验证一个用户付费行为的合法性,那么无论如何服务器的处理行为都是有问题的。查了一下APPLE API,查到了确实苹果提供了验证一个用户是否合法的接口。
具体可以参考:
http://developer.apple.com/library/ios/#documentation/NetworkingInternet/Conceptual/StoreKitGuide/VerifyingStoreReceipts/VerifyingStoreReceipts.html#//apple_ref/doc/uid/TP40008267-CH104-SW1
有了这个接口,我开始尝试,写一个通用的游戏充值接口。这个接口完成以下的需求:
1.公司的所有游戏都走这个接口(Http)的,根据这个接口,将充值内部调用不同游戏的指定充值接口。这样做,用户不在直接调用游戏充值接口。
2.在这里可以对游戏的充值进行统计,和查询,而各个游戏本身,充值将只记录log。
这样做,可以避免暴露出实际的游戏充值接口,也给充值本身带来一层保护。
于是和公司的几个游戏组商量,统一了一个接口。

GameID游戏IDstring
UserID用户IDstring
Param游戏参数string
IAPKey支付返回的Apple keystring
Channel渠道名称string
Currency币种(美元,人民币)int
Money货币钱数int
这是客户端传给我的,用于验证,这里有几个主要的参数,IAPKey是客户端向苹果付费后,所获得的Base64编码的结果。或许有人会问,既然有了key,其它的部分都是不需要的吧。这里我需要解释一下
Param是考虑到通用化,可能某次用户付费,客户端需要额外附加一些说明,那么就在这里给出。游戏验证服务不处理这个信息,而是直接抛给游戏充值接口,在那里做自己的逻辑判断。
Channel是充值渠道,考虑到以后可能有别的支付模式,比如支付宝,比如信用卡。这部分渠道,可能会需要区分开,另外,或许游戏策划会根据渠道给与不同的额外奖励,这部分也是原封不动的传给游戏充值接口。
那么,和游戏充值接口的对接是怎么样的呢?

GameID游戏IDstring
UserID用户IDstring
Param游戏参数string
Channel渠道名称string
Currency币种(美元,人民币)int
Money货币钱数int

这里,就是少了一个IAPKey,剩下的部分,全部给游戏逻辑处理,这部分要求所有游戏充值接口统一,提供给我Http调用接口,按照以上的规则。
运行顺序是 (客户端用户充值-》获得验证码-》调用统一充值接口-》去苹果验证-》验证后调用游戏充值接口-》返回操作结果)
那么,在这里,可以根据渠道,我做不同的逻辑处理,如果是Apple,那么我就会去走验证IAPKey的部分。
但是在写代码之前,要考虑的充分一些极端情况,这个里面主要有几个崩溃点。
(1)如果我的服务器和苹果服务器之间的链接断了,怎么办?
这种是一种很常见的情况,我的服务器链接不上苹果了,取不回相应的结果。那么,我必须告诉客户端,苹果验证目前有问题,请稍后重试。当然,这部分是不能展现给玩家看的。比较好的处理方法是,我默认这次操作是成功的,先把钱加上,然后后面有一个进程,独立去链接苹果服务器,取得验证结果,再修正对应的游戏充值结果,如果链接恢复,我会尝试重新验证没有验证过的IAPKey。
(2)如果我和游戏服务器充值接口之间的链接中断了,怎么办?
这一部的意思是,如果我从苹果获得了正确的验证,但是此时我和游戏服务器的http接口中断了,这时候怎么办?一样的道理,我必须得提供一个重试的机制,验证过的key,我会尝试充值,直到调用游戏充值接口返回完成。
(3)如果一个玩家反复拿一个可以通过验证的key进行充值,恶意的key,如何防止?
这里,要对每一个经过验证的key进行记录,当一个用户key请求的时候,我会先去查找,有没有一个与此匹配的key,如果有这个key,且在于充值完成状态,那么我不应该去验证苹果服务器,也不应该执行任何操作。那么有人会问,日积月累,我的key会有很多很多,如果你每次select所有的key,会不会很变态?我的回答是,不仅会很变态,还会让我的程序服务越来越慢。
那么,怎么处理呢?想一下,其实有很多轻盈的解决方法。
对于用户,我只想知道这个key处在什么状态,那么我假设,有三种状态,1:什么都没有进行状态,2:通过了苹果验证状态,3:通过了游戏充值并结束状态。这样我建立三个map,去命中,会非常合适,而且,我只关心是否在当前集合下,并不关心详细信息,这里很适合用一个单件搞定,如果你觉得还不够安全,那么用一个类似redis的也行,不过我觉得这样的需求,用缓冲层有点大材小用,必要性不大,毕竟,崩溃了我再重新加载一次就好了,这样的成本我现在的需求支付的起。
考虑到以上的情况,我的数据库构建如下,来解决三大部分的衔接。
CREATE TABLE `Game_XXXXX` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`GameID` varchar(20) DEFAULT NULL COMMENT '游戏产品ID',
`UserID` varchar(50) DEFAULT NULL COMMENT '玩家ID',
`Param` varchar(100) DEFAULT NULL COMMENT '产品附加参数',
`IAPKey` text COMMENT '苹果扣费ID',
`Currency` int(11) DEFAULT NULL COMMENT '当前币种',
`Money` int(11) DEFAULT NULL COMMENT '当前金额',
`OperationState` int(11) DEFAULT NULL COMMENT '操作状态',
`Channel` varchar(50) DEFAULT NULL COMMENT '渠道ID',
`IAPState` int(11) DEFAULT NULL COMMENT '苹果返回状态',
`IAPMessage` varchar(300) DEFAULT NULL COMMENT '苹果返回描述',
`GameState` int(11) DEFAULT NULL COMMENT '游戏返回状态',
`GameMessage` varchar(100) DEFAULT NULL COMMENT '游戏返回描述',
`GameParam` varchar(100) DEFAULT NULL COMMENT '游戏返回参数',
`CreateTime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`ID`)
) ENGINE=MyISAM AUTO_INCREMENT=2 DEFAULT CHARSET=utf8



XXXXX代表游戏的编号,这样我可以根据不同的游戏,建立若干个这样的表,分拆减少压力,便于统计。
由于我的这个系统实际运行过程中,并不会对这个库进行select,最多也就是insert和update,那么我完全可以用多写少读的引擎模式(我用的是mysql)。因为读我只在系统第一次启动的时候去读,慢一点就慢一点,不影响大局。只要不频繁崩溃。
那么,为了简洁一些,我写了三个存储过程,来解决三个状态的追加。
pay_Info_Insert
这个存储过程是当一个key到来的时候,如果不在我已有的key组中被命中,那么我就会把这个key插入到我的key组中,并在数据库同步插入一条。
pay_IAP_Insert
这个存储过程是当苹果验证服务器返回的时候,我需要在将这个key添加到我的IAP组里面去,同时在数据库里面更新这个key的状态。
pay_Game_Insert
当我的游戏服务器充值接口返回数据的时候,我会在Game的组里面添加进去,并更新数据库里面的这条key的状态。
这样的架构,就差不多了。
于是,我用python写了一个架构,我觉得,简单的东西,有时候可以不必用C++,脚本语言非常适合干这样的事情。
#获得支付信息,并实现和游戏的对接
def PayInfo(request):
    try:
      #获取参数   
      strGameID = request.POST.get('GameID')
      if strGameID is None:
            strGameID = request.GET.get('GameID')
      strUserID = request.POST.get('UserID')
      if strUserID is None:
            strUserID = request.GET.get('UserID')
      strParam = request.POST.get('Param')
      if strParam is None:
            strParam = request.GET.get('Param')
      strIAPKey = request.POST.get('IAPKey')
      if strIAPKey is None:
            strIAPKey = request.GET.get('IAPKey')
      strChannel = request.POST.get('Channel')
      if strChannel is None:
            strChannel = request.GET.get('Channel')
      strCurrency = request.POST.get('Currency')
      if strCurrency is None:
            strCurrency = request.GET.get('Currency')                     
      strMoney = request.POST.get('Money')
      if strMoney is None:
            strMoney = request.GET.get('Money')
      
      nOperation = 0
      nAppState= 1
      if strGameID is not None and strUserID is not None and strIAPKey is not None:
            #链接数据库
            objDBOption = CDBOption(G_PAYDBSERVER, G_PAYDB, G_PAYDBUSER, G_PAYDBPASS)
            
            #去数据库查询
            nNeedInsertBase = 0             #是否需要插入基本数据
            strSQLProc = "call pay_Info_Select('" + strIAPKey + "','" + strGameID + "');"
            objRecordSet = objDBOption.ExecuteSQL(strSQLProc)
            if ((objRecordSet is not None) and (objDBOption.GetCount() <> 0)):
                strGameState   = ""
                strGameMessage = ""
                strGameParam   = ""
                strIAPState    = ""
               
                #判断此key的状态
                for res in objRecordSet:
                  nOperation      = int(res)
                  nAppState       = int(res)
                  strGameState    = str(res)
                  strGameMessage= str(res)
                  strGameParam    = str(res)
                  print "strGameState=" + strGameState + ",strGameMessage=" + strGameMessage + ",strGameParam=" + strGameParam
                  
                  nNeedInsertBase = 1
                  break
               
                #如果加钱流程已经完成,则直接返回结果
                if nOperation == 3:
                  #已经验证,就不在返回了
                  objClientReturn = {}
                  objClientReturn["RetID"]      = strGameState
                  objClientReturn["RetMessage"] = strGameMessage
                  objClientReturn["Param"]      = strGameParam
                  
                  strClient = simplejson.dumps(objClientReturn)
                  return HttpResponse(strClient)
            
            #如果数据库中没有基本数据,则直接添加新项目
            if nOperation < 1:
                #是否插入基本数据
                if nNeedInsertBase == 0:
                  strSQLProc = "call pay_Info_Insert('" + strGameID + "','" + strUserID + "','" + strParam + "','" + strIAPKey + "','" + strCurrency + "','" + strMoney + "','" + strChannel + "');"
                  objDBOption.ExecuteSQL(strSQLProc)
            
            #如果没有去苹果验证过,则去验证
            if nOperation < 2:
                #向远端服务器发送信息
                objSendAppData = {}
                objSendAppData["receipt-data"] = strIAPKey
            
                #向苹果发送验证数据
                strPost      = simplejson.dumps(objSendAppData)
                response = urllib2.urlopen(BUY_APPLE, strPost)
                objAppleResult = simplejson.loads(response.read())
                nAppState   = int(objAppleResult.get("status", "0"))
                strAppMessage = str(objAppleResult.get("exception", ""))
               
                #插入数据库结果
                strSQLProc = "call pay_IAP_Insert('" + strIAPKey + "','" + strGameID + "','" + str(nAppState) + "','" + strAppMessage + "');"
                objDBOption.ExecuteSQL(strSQLProc)
               
            #获取一下最新的状态      
            if nAppState == 0:
                #验证成功,向相应的游戏调用充值接口
                strURL= ""
                strPost = ""
                if strGameID == GAMEID1:
                  strURL= GAME_URL_1
                  strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
                if strGameID == GAMEID2:
                  strURL= GAME_URL_2
                  strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
                if strGameID == GAMEID3:
                  strURL= GAME_URL_3
                  strPost = GAME_URL_PARAM % (strGameID, strUserID, strParam, strChannel, strCurrency, strMoney)
               
                print "strURL="+ strURL
                print "strPost=" + strPost
               
                response = urllib2.urlopen(strURL, strPost)
                objAppleResult = simplejson.loads(response.read())
                strGameState      = str(objAppleResult.get("RetID", "0"))
                strGameMessage    = str(objAppleResult.get("RetMessage", ""))
                strGameParam      = str(objAppleResult.get("Param", ""))
               
                #添加数据库返回结果
                strSQLProc = "call pay_Game_Insert('" + strIAPKey + "','" + strGameID + "','" + strGameState + "','" + strGameMessage + "','" + strGameParam + "');"
                strSQLProc = strSQLProc.decode("utf8")
                objDBOption.ExecuteSQL(strSQLProc)               
               
                objClientReturn = {}
                objClientReturn["RetID"]      = strGameState
                objClientReturn["RetMessage"] = strGameMessage
                objClientReturn["Param"]      = strGameParam               

                strClient = simplejson.dumps(objClientReturn)
                return HttpResponse(strClient)                                                                              
            else:
                #验证失败
                strGameState   = G_ERROR_1001
                strGameMessage = "unused Key"
                strGameParam   = ""
               
                strSQLProc = "call pay_Game_Insert('" + strIAPKey + "','" + strGameID + "','" + strGameState + "','" + strGameMessage + "','" + strGameParam + "');"
                strSQLProc = strSQLProc.decode("utf8")
                objDBOption.ExecuteSQL(strSQLProc)
               
                #返回失败信息
                objClientReturn = {}
                objClientReturn["RetID"]      = strGameState
                objClientReturn["RetMessage"] = strGameMessage
                objClientReturn["Param"]      = strGameParam
               
                strClient = simplejson.dumps(objClientReturn)
                return HttpResponse(strClient)
               
            
            return HttpResponse(simplejson.dumps(objAppleResult))
      else:
            #返回失败信息
            objClientReturn = {}
            objClientReturn["RetID"]      = G_ERROR_1002
            objClientReturn["RetMessage"] = ""
            objClientReturn["Param"]      = ""
            
            strClient = simplejson.dumps(objClientReturn)
            return HttpResponse(strClient)
                     
    except Exception:
      traceback.print_exc()
      objClientReturn = {}
      objClientReturn["RetID"]      = G_ERROR_1003
      objClientReturn["RetMessage"] = ""
      objClientReturn["Param"]      = ""
      
      strClient = simplejson.dumps(objClientReturn)      
      return HttpResponse(strClient)

就这么多,呵呵,不复杂吧,代码也就半天的工作量。全部代码不能贴出来,毕竟涉及到一些公司的一些技术机密,这里只做一个参考。
另外,追加一个遍历key组的进程,用于处理那些上述两种情况发生的"坏死"的key。
后来实际测试我发现,苹果的https服务真的不是一般的慢。一般5-10s才会返回,我后来把上述代码改造了一下,先给钱,不验证,后面有一个进程定时去做验证的事情。如果发现异常,就直接封禁这个用户的充值。
呵呵,其实,很多东西可以很简单的。
现在这套系统,我们的三个游戏项目组公用之。
在这里记录一下,给有兴趣的朋友做一个参考。
另外,开源服务器0.83版本我做了三大修改。
1.添加了当客户端block发生的时候,服务器不再阻塞当前发送线程等待,而是直接跳过,过一段时间定时重发。这样就能解决在网络环境不好的时候,慢客户端拖累服务器的问题。
2.添加了一个"性能追迹者",我把目前的框架分为三个部分,接受 工作逻辑 和发送。这三部分独立线程运作,互不干扰,并都有独立的时间统计,计数工具,超时的处理都会记录在日志中,便于开发查阅。主要的想法是解决"到底服务器在哪里慢了?",或者说"慢在哪个环节了?",一目了然。
3.重写了模块重加载代码,对于模块的卸载和加载,我以前的做法比较粗暴,等待一段时间强行杀死模块。这种方法野蛮而且绝对会引起出错。我最近腾出手来,重写了这部分,用引用计数,只有当前模块引用计数为0的时候,才会释放并加载,这样是比较合理的。
正在测试上述三部分,代码已经完成,简单的测试也通过了,争取在年底发布前,更详细的测试一下。
页: [1]
查看完整版本: 关于苹果服务,用户付费的处理模式