首先 “苹果禁止在app中提供第三方支付” 所以但凡是做IOS App的,需要充值的地方都会遇到服务端充值的功能需要开发。

充值的大体流程是这样的,App内在购买成功后,会得到一个从苹果AppStore服务器得到一个本次交易数据的字符串,即收据(Receipt),Receipt中包含了本次交易的几乎所有信息,前面这部分的内容都是由App内实现,所以服务端不用去管。

App拿到Receipt之后,向应用的服务端(即用户自己的服务器)请求发送本次交易的Receipt及当前用户的唯一标识。服务端再从Receipt中获取到交易信息,并发往苹果的AppStore服务器验证交易信息的真伪,验证通过后则修改当前App用户的账户余额信息(为了防止越狱和IAP free等插件造成的欺诈性购买)。之后将增加后的用户余额信息返回给App引用,此时用户看到app上显示充值成功并且自己的账户余额也发生了变化。

与服务器端的通信,就是一个RPC的过程,服务器端写好一些供调用的API接口,在客户端联网调用,具体有什么xmlrpc, jsonrpc的,都有开源的框架可以使用。注意:客户端与游戏服务器端通信的时候,必需附带一个标识其身份的代码(UUID或者帐号名称),否则服务器端无法知道是谁进行了充值。

参见:接口开发文档

开发实例:

{"receipt-data" : "ewoJInNpZ25hdHVyZSIgPSAiQXJjNzIxdzJ0Z0JTNFluZ1BaQWlsemZDRVJVazdPUUpxTzJ6cHBSK3dKRTZhb1UxdlQyVTVqeGY3RVhzMy9udlo1RjljRFFPWFJCUE8rRTA5eFBrcU85ZDY0MDNJcys3cDllUW5yNVNxZ2NCK3BiOFl4MnNvd1BoMHBhNFMrcGJxTC9MekwvenJQNk1NclRpTTh3YmpyOHowZENRN21FcjQ0bUpLVUhTdHREaEFBQURWekNDQTFNd2dnSTdvQU1DQVFJQ0NCdXA0K1BBaG0vTE1BMEdDU3FHU0liM0RRRUJCUVVBTUg4eEN6QUpCZ05WQkFZVEFsVlRNUk13RVFZRFZRUUtEQXBCY0hCc1pTQkpibU11TVNZd0pBWURWUVFMREIxQmNIQnNaU0JEWlhKMGFXWnBZMkYwYVc5dUlFRjFkR2h2Y21sMGVURXpNREVHQTFVRUF3d3FRWEJ3YkdVZ2FWUjFibVZ6SUZOMGIzSmxJRU5sY25ScFptbGpZWFJwYjI0Z1FYVjBhRzl5YVhSNU1CNFhEVEUwTURZd056QXdNREl5TVZvWERURTJNRFV4T0RFNE16RXpNRm93WkRFak1DRUdBMVVFQXd3YVVIVnlZMmhoYzJWU1pXTmxhWEIwUTJWeWRHbG1hV05oZEdVeEd6QVpCZ05WQkFzTUVrRndjR3hsSUdsVWRXNWxjeUJUZEc5eVpURVRNQkVHQTFVRUNnd0tRWEJ3YkdVZ1NXNWpMakVMTUFrR0ExVUVCaE1DVlZNd2daOHdEUVlKS29aSWh2Y05BUUVCQlFBRGdZMEFNSUdKQW9HQkFNbVRFdUxnamltTHdSSnh5MW9FZjBlc1VORFZFSWU2d0Rzbm5hbDE0aE5CdDF2MTk1WDZuOTNZTzdnaTNvclBTdXg5RDU1NFNrTXArU2F5Zzg0bFRjMzYyVXRtWUxwV25iMzRucXlHeDlLQlZUeTVPR1Y0bGpFMU93QytvVG5STStRTFJDbWVOeE1iUFpoUzQ3VCtlWnRERWhWQjl1c2szK0pNMkNvZ2Z3bzdBZ01CQUFHamNqQndNQjBHQTFVZERnUVdCQlNKYUVlTnVxOURmNlpmTjY4RmUrSTJ1MjJzc0RBTUJnTlZIUk1CQWY4RUFqQUFNQjhHQTFVZEl3UVlNQmFBRkRZZDZPS2RndElCR0xVeWF3N1hRd3VSV0VNNk1BNEdBMVVkRHdFQi93UUVBd0lIZ0RBUUJnb3Foa2lHOTJOa0JnVUJCQUlGQURBTkJna3Foa2lHOXcwQkFRVUZBQU9DQVFFQWVhSlYyVTUxcnhmY3FBQWU1QzIvZkVXOEtVbDRpTzRsTXV0YTdONlh6UDFwWkl6MU5ra0N0SUl3ZXlOajVVUllISytIalJLU1U5UkxndU5sMG5rZnhxT2JpTWNrd1J1ZEtTcTY5Tkluclp5Q0Q2NlI0Szc3bmI5bE1UQUJTU1lsc0t0OG9OdGxoZ1IvMWtqU1NSUWNIa3RzRGNTaVFHS01ka1NscDRBeVhmN3ZuSFBCZTR5Q3dZVjJQcFNOMDRrYm9pSjNwQmx4c0d3Vi9abEwyNk0ydWVZSEtZQ3VYaGRxRnd4VmdtNTJoM29lSk9PdC92WTRFY1FxN2VxSG02bTAzWjliN1BSellNMktHWEhEbU9Nazd2RHBlTVZsTERQU0dZejErVTNzRHhKemViU3BiYUptVDdpbXpVS2ZnZ0VZN3h4ZjRjemZIMHlqNXdOelNHVE92UT09IjsKCSJwdXJjaGFzZS1pbmZvIiA9ICJld29KSW05eWFXZHBibUZzTFhCMWNtTm9ZWE5sTFdSaGRHVXRjSE4wSWlBOUlDSXlNREUxTFRBNUxUSXlJREF5T2pBME9qRXlJRUZ0WlhKcFkyRXZURzl6WDBGdVoyVnNaWE1pT3dvSkluVnVhWEYxWlMxcFpHVnVkR2xtYVdWeUlpQTlJQ0kzWmpVd1pqUmpOamt5WXpRek5qRTJNbVExTVRJMFlXVmlaREF3WTJRME9UWXpPVGMyT0dVeklqc0tDU0p2Y21sbmFXNWhiQzEwY21GdWMyRmpkR2x2YmkxcFpDSWdQU0FpTVRBd01EQXdNREUzTWpnek9EQTVOQ0k3Q2draVluWnljeUlnUFNBaU5TSTdDZ2tpZEhKaGJuTmhZM1JwYjI0dGFXUWlJRDBnSWpFd01EQXdNREF4TnpJNE16Z3dPVFFpT3dvSkluRjFZVzUwYVhSNUlpQTlJQ0l4SWpzS0NTSnZjbWxuYVc1aGJDMXdkWEpqYUdGelpTMWtZWFJsTFcxeklpQTlJQ0l4TkRReU9URXlOalV5TWpjNUlqc0tDU0oxYm1seGRXVXRkbVZ1Wkc5eUxXbGtaVzUwYVdacFpYSWlJRDBnSWtWR05qUkdNVGsyTFVNMVFqa3RORFEwTWkxQk1rWXlMVU15TkVSRlFUZ3dSamMyUXlJN0Nna2ljSEp2WkhWamRDMXBaQ0lnUFNBaVkyOXRMbVJwY0dsdVozaHBZVzR1YTNWcWFXRnVaeTVyZFdKcE5pSTdDZ2tpYVhSbGJTMXBaQ0lnUFNBaU1UQTBNakUzTWpnM055STdDZ2tpWW1sa0lpQTlJQ0pqYjIwdWEzVnFhV0Z1Wnk1a2FYTjBjbWxpZFhScGIyNGlPd29KSW5CMWNtTm9ZWE5sTFdSaGRHVXRiWE1pSUQwZ0lqRTBOREk1TVRJMk5USXlOemtpT3dvSkluQjFjbU5vWVhObExXUmhkR1VpSUQwZ0lqSXdNVFV0TURrdE1qSWdNRGs2TURRNk1USWdSWFJqTDBkTlZDSTdDZ2tpY0hWeVkyaGhjMlV0WkdGMFpTMXdjM1FpSUQwZ0lqSXdNVFV0TURrdE1qSWdNREk2TURRNk1USWdRVzFsY21sallTOU1iM05mUVc1blpXeGxjeUk3Q2draWIzSnBaMmx1WVd3dGNIVnlZMmhoYzJVdFpHRjBaU0lnUFNBaU1qQXhOUzB3T1MweU1pQXdPVG93TkRveE1pQkZkR012UjAxVUlqc0tmUT09IjsKCSJlbnZpcm9ubWVudCIgPSAiU2FuZGJveCI7CgkicG9kIiA9ICIxMDAiOwoJInNpZ25pbmctc3RhdHVzIiA9ICIwIjsKfQ=="}

这是一段AppStore返回给App的Receipt,拿到后App将他连同要充值用户的唯一标识发送给服务端,服务端对这段Receipt先进行解析初步验证判断

$receipt_data_content = $receipt_data?$this->iosstr_to_arr(base64_decode($receipt_data)):false;
if(!$receipt_data){
    throw new SDKRuntimeException('参数错误');
}
if($receipt_data_content['signing-status']){
    throw new SDKRuntimeException('充值失败');
}
$purchase_info = isset($receipt_data_content['purchase-info'])?$this->iosstr_to_arr(base64_decode($receipt_data_content['purchase-info'])):false;
if(!$purchase_info){
    throw new SDKRuntimeException('');
}
if(!isset($purchase_info['transaction-id'])||!$purchase_info['transaction-id']){
    throw new SDKRuntimeException('错误的订单标示号');
}

另外还有一个验证的内容,在产品列表部分,Receipt中会包含产品id,而产品id是原本在AppStore中原本事先定义好了的,所以可以再做一个产品id是否存在的验证。上面代码中的iosstr_to_arr方法为将Receipt解析成数组的方法,这个Receipt字符串的格式略不同于一般的json格式,所以需要自己写个方法另外处理下

function iosstr_to_arr($str){
    $str = trim(substr($str,1,strlen($str)-2));
    $tmp_arr = explode('";',$str);
    $arr = array();
    foreach($tmp_arr as $v){
        if(!$v){
            continue;
        }
        $v = trim($v);
        $v = trim(substr($v,1,strlen($v)-1));
        $tmp = explode('" = "',$v);
        $arr[$tmp[0]] = $tmp[1];
        unset($tmp);
    }
    return $arr;
}

通过初步验证之后再通过Receipt中的信息创建订单

$product = isset($purchase_info['product-id'])&&isset($this->product_list[$purchase_info['product-id']])?$purchase_info['product-id']:'';
$amount = $product?$this->product_list[$product]:0;
$transaction_id = isset($purchase_info['transaction-id'])?$purchase_info['transaction-id']:'';
$quantity = isset($purchase_info['quantity'])?$purchase_info['quantity']:0;
$amount = $amount*$quantity;

获取到这些信息之后就可以在本地DB中创建一条订单信息了,包含上面这些内容,当然也可以连同Receipt一起存起来,transaction-id作为订单唯一标识。接下来就是用App给过来的Receipt信息到AppStore去验证订单真伪及结果的部分了

appstore_verify(array('receipt-data'=>$receipt_data));

private function appstore_verify($receipt_data,$verify_environment = null){
    try{
        if(!$receipt_data){
            throw new SDKRuntimeException('参数错误');
        }
        if($verify_environment == null){
            $url = 'https://buy.itunes.apple.com/verifyReceipt';//正式接口
        }else{
            $url = 'https://sandbox.itunes.apple.com/verifyReceipt';//测试接口
        }

        $curl = curl_init($url);
        // curl_setopt($curl, CURLOPT_HEADER, 0 ); // 过滤HTTP头
        curl_setopt($curl,CURLOPT_RETURNTRANSFER, 1);// 显示输出结果
        curl_setopt($curl,CURLOPT_POST,true); // post传输数据
        curl_setopt($curl,CURLOPT_POSTFIELDS,json_encode($receipt_data));// post传输数据
        $responseText = curl_exec($curl);
        //var_dump( curl_error($curl) );//如果执行curl过程中出现异常,可打开此开关,以便查看异常内容
        curl_close($curl);
        return $responseText;
    }catch (SDKRuntimeException $e){

    }
}

方法中我加入了一个$verify_environment变量,来控制切换开发和正式环境的和测试环境的验证接口。而在充值的逻辑中会先拿Receipt信息到正式环境接口验证订单真伪,如果是测试环境的订单,再重新到测试环境重新做一遍验证,根据返回的status的状态码区分订单状态。获得返回值后将返回值更新到对应订单中,并更新订单状态。

if(isset($verify['status']) && intval($verify['status'])===0)


这个表示验证成功,订单支付成功。

if(isset($verify['status']) && intval($verify['status'])===21007)//Sandbox环境的receipt被发送至了生产系统的验证,

另外加上这样一个逻辑则会重新到到测试环境验证一遍,省去了后期上线后修改代码的麻烦并且在正式运行后仍可以用测试单进行测试或者其他的功能开发。

另附上状态码的编码

Status描述
21000AppStore不能读取你提供的JSON对象
21002receipt-data域的数据有问题
21003receipt无法通过验证
21004提供的shared secret不匹配你账号中的shared secret
21005receipt服务器当前不可用
21006receipt合法,但是订阅已过期。服务器收到这个状态码时,receipt数据仍然会解码并一起发送
21007receipt是Sandbox receipt,但却发送至生产系统的验证服务
21008receipt是生产receipt,但却发送至Sandbox环境的验证服务