分账接口类 ProfitSharing.class.php
<?php require_once dirname(__FILE__) . "/ProfitSharingSign.class.php"; require_once dirname(__FILE__) . "/ProfitSharingCurl.class.php"; class ProfitSharing { private $wxConfig = null; private $sign = null; private $curl = null; public function __construct() { $this->wxConfig = $this->wxConfig(); $this->sign = new ProfitSharingSign(); $this->curl = new ProfitSharingCurl(); } /** * @function 发起请求所必须的配置参数 * @return mixed */ private function wxConfig() { $wxConfig['app_id'] = '';//服务商公众号AppID $wxConfig['mch_id'] = ''; //服务商商户号 $wxConfig['sub_app_id'] = '';//todo 子服务商公众号AppID $wxConfig['sub_mch_id'] = ''; //todo 子服务商商户号 $wxConfig['md5_key'] = ''; //md5 秘钥 $wxConfig['app_cert_pem'] = '';//证书路径 $wxConfig['app_key_pem'] = '';//证书路径 return $wxConfig; } /** * @function 请求多次分账接口 * @param $orders array 待分账订单 * @param $accounts array 分账接收方 * @return array * @throws Exception */ public function multiProfitSharing($orders,$accounts) { if(empty($orders)){ throw new Exception('没有待分帐订单'); } if(empty($accounts)){ throw new Exception('接收分账账户为空'); } //1.设置分账账号 $receivers = array(); foreach ($accounts as $account) { $tmp = array( 'type'=>$account['type'], 'account'=>$account['account'], 'amount'=>intval($account['amount']), 'description'=>$account['desc'], ); $receivers[] = $tmp; } $receivers = json_encode($receivers,JSON_UNESCAPED_UNICODE); $totalCount = count($orders); $successCount = 0; $failCount = 0; $now = time(); foreach ($orders as $order) { //2.生成签名 $postArr = array( 'appid'=>$this->wxConfig['app_id'], 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'sub_appid'=>$this->wxConfig['sub_app_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'transaction_id'=>$order['trans_id'], 'out_order_no'=>$order['order_no'].$order['ticket_no'], 'receivers'=>$receivers, ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //3.发送请求 $url = 'https://api.mch.weixin.qq.com/secapi/pay/multiprofitsharing'; $postXML = $this->toXml($postArr); Ilog::DEBUG("multiProfitSharing.postXML: " . $postXML); $opts = array( CURLOPT_HEADER => 0, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSLCERTTYPE => 'PEM', //默认支持的证书的类型,可以注释 CURLOPT_SSLCERT => $this->wxConfig['app_cert_pem'], CURLOPT_SSLKEY => $this->wxConfig['app_key_pem'], ); Ilog::DEBUG("multiProfitSharing.opts: " . json_encode($opts)); $curl_res = $this->curl->setOption($opts)->post($url,$postXML); Ilog::DEBUG("multiProfitSharing.curl_res: " . $curl_res); $ret = $this->toArray($curl_res); if($ret['return_code']=='SUCCESS' and $ret['result_code']=='SUCCESS') { //更新分账订单状态 $params = array(); $params['order_no'] = $order['order_no']; $params['trans_id'] = $order['trans_id']; $params['ticket_no'] = $order['ticket_no']; $data = array(); $data['profitsharing'] = $receivers; $data['state'] = 2; pdo_update('ticket_orders_profitsharing',$data,$params); $successCount++; }else{ $failCount++; } usleep(500000);//微信会报频率过高,所以停一下 } return array('processTime'=>date('Y-m-d H:i:s',$now),'totalCount'=>$totalCount,'successCount'=>$successCount,'failCount'=>$failCount); } /** * @function 请求单次分账接口 * @param $profitSharingOrders array 待分账订单 * @param $profitSharingAccounts array 分账接收方 * @return array * @throws Exception */ public function profitSharing($profitSharingOrders,$profitSharingAccounts) { if(empty($profitSharingOrders)){ throw new Exception('没有待分帐订单'); } if(empty($profitSharingAccounts)){ throw new Exception('接收分账账户为空'); } //1.设置分账账号 $receivers = array(); foreach ($profitSharingAccounts as $profitSharingAccount) { $tmp = array( 'type'=>$profitSharingAccount['type'], 'account'=>$profitSharingAccount['account'], 'amount'=>intval($profitSharingAccount['amount']), 'description'=>$profitSharingAccount['desc'], ); $receivers[] = $tmp; } $receivers = json_encode($receivers,JSON_UNESCAPED_UNICODE); $totalCount = count($profitSharingOrders); $successCount = 0; $failCount = 0; $now = time(); foreach ($profitSharingOrders as $profitSharingOrder) { //2.生成签名 $postArr = array( 'appid'=>$this->wxConfig['app_id'], 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'sub_appid'=>$this->wxConfig['sub_app_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'transaction_id'=>$profitSharingOrder['trans_id'], 'out_order_no'=>$profitSharingOrder['order_no'].$profitSharingOrder['ticket_no'], 'receivers'=>$receivers, ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //3.发送请求 $url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharing'; $postXML = $this->toXml($postArr); Ilog::DEBUG("profitSharing.postXML: " . $postXML); $opts = array( CURLOPT_HEADER => 0, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSLCERTTYPE => 'PEM', //默认支持的证书的类型,可以注释 CURLOPT_SSLCERT => $this->wxConfig['app_cert_pem'], CURLOPT_SSLKEY => $this->wxConfig['app_key_pem'], ); Ilog::DEBUG("profitSharing.opts: " . json_encode($opts)); $curl_res = $this->curl->setOption($opts)->post($url,$postXML); Ilog::DEBUG("profitSharing.curl_res: " . $curl_res); $ret = $this->toArray($curl_res); if($ret['return_code']=='SUCCESS' and $ret['result_code']=='SUCCESS') { //更新分账订单状态 $params = array(); $params['order_no'] = $profitSharingOrder['order_no']; $params['trans_id'] = $profitSharingOrder['trans_id']; $params['ticket_no'] = $profitSharingOrder['ticket_no']; $data = array(); $data['profitsharing'] = $receivers; $data['state'] = 2; pdo_update('ticket_orders_profitsharing',$data,$params); $successCount++; }else{ $failCount++; } } return array('processTime'=>date('Y-m-d H:i:s',$now),'totalCount'=>$totalCount,'successCount'=>$successCount,'failCount'=>$failCount); } /** * @function 查询分账结果 * @param $trans_id string 微信支付单号 * @param $out_order_no string 分账单号 * @return array|false * @throws Exception */ public function query($trans_id,$out_order_no) { //1.生成签名 $postArr = array( 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'transaction_id'=>$trans_id, 'out_order_no'=>$out_order_no, 'nonce_str'=>md5(time() . rand(1000, 9999)), ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //2.发送请求 $url = 'https://api.mch.weixin.qq.com/pay/profitsharingquery'; $postXML = $this->toXml($postArr); Ilog::DEBUG("query.postXML: " . $postXML); $curl_res = $this->curl->post($url,$postXML); Ilog::DEBUG("query.curl_res: " . $curl_res); $ret = $this->toArray($curl_res); return $ret; } /** * @function 添加分账接收方 * @param $profitSharingAccount array 分账接收方 * @return array|false * @throws Exception */ public function addReceiver($profitSharingAccount) { //1.接收分账账户 $receiver = array( 'type'=>$profitSharingAccount['type'], 'account'=>$profitSharingAccount['account'], 'name'=>$profitSharingAccount['name'], 'relation_type'=>$profitSharingAccount['relation_type'], ); $receiver = json_encode($receiver,JSON_UNESCAPED_UNICODE); //2.生成签名 $postArr = array( 'appid'=>$this->wxConfig['app_id'], 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'sub_appid'=>$this->wxConfig['sub_app_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'receiver'=>$receiver ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //3.发送请求 $url = 'https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver'; $postXML = $this->toXml($postArr); Ilog::DEBUG("addReceiver.postXML: " . $postXML); $curl_res = $this->curl->post($url,$postXML); Ilog::DEBUG("addReceiver.curl_res: " . $curl_res); $ret = $this->toArray($curl_res); return $ret; } /** * @function 删除分账接收方 * @param $profitSharingAccount array 分账接收方 * @return array|false * @throws Exception */ public function removeReceiver($profitSharingAccount) { //1.接收分账账户 $receiver = array( 'type'=>$profitSharingAccount['type'], 'account'=>$profitSharingAccount['account'], 'name'=>$profitSharingAccount['name'], ); $receiver = json_encode($receiver,JSON_UNESCAPED_UNICODE); //2.生成签名 $postArr = array( 'appid'=>$this->wxConfig['app_id'], 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'sub_appid'=>$this->wxConfig['sub_app_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'receiver'=>$receiver ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //3.发送请求 $url = 'https://api.mch.weixin.qq.com/pay/profitsharingremovereceiver'; $postXML = $this->toXml($postArr); Ilog::DEBUG("removeReceiver.postXML: " . $postXML); $curl_res = $this->curl->post($url,$postXML); Ilog::DEBUG("removeReceiver.curl_res: " . $curl_res); $ret = $this->toArray($curl_res); return $ret; } /** * @function 完结分账 * @param $profitOrder array 分账订单 * @param $description string 完结分账描述 * @return array|false * @throws Exception */ public function finish($profitOrder,$description='分账完结') { $ret = array(); if(!empty($profitOrder)) { //1.签名 $postArr = array( 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'appid'=>$this->wxConfig['app_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'transaction_id'=>$profitOrder['trans_id'], 'out_order_no'=>'finish'.'_'.$profitOrder['order_no'], 'description'=>$description, ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //2.请求 $url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharingfinish'; $postXML = $this->toXml($postArr); Ilog::DEBUG("finish.postXML: " . $postXML); $opts = array( CURLOPT_HEADER => 0, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSLCERTTYPE => 'PEM', //默认支持的证书的类型,可以注释 CURLOPT_SSLCERT => $this->wxConfig['app_cert_pem'], CURLOPT_SSLKEY => $this->wxConfig['app_key_pem'], ); Ilog::DEBUG("finish.opts: " . json_encode($opts)); $curl_res = $this->curl->setOption($opts)->post($url,$postXML); Ilog::DEBUG("finish.curl_res: " . $curl_res); $ret = $this->toArray($curl_res); } return $ret; } /** * @function 分账回退 * @param $profitOrder array 分账订单 * @return array * @throws Exception */ public function profitSharingReturn($profitOrder) { $ret = array(); if(!empty($profitOrder) and $profitOrder['channel']==1) { $accounts = json_decode($profitOrder['profitsharing'],true); foreach ($accounts as $account) { //1.签名 $postArr = array( 'appid'=>$this->wxConfig['app_id'], 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'sub_appid'=>$this->wxConfig['sub_app_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'out_order_no'=>$profitOrder['order_no'].$profitOrder['ticket_no'], 'out_return_no'=>'return_'.$profitOrder['order_no'].$profitOrder['ticket_no'].'_'.$account['account'], 'return_account_type'=>'MERCHANT_ID', 'return_account'=>$account['account'], 'return_amount'=>$account['amount'], 'description'=>'用户退款', 'sign_type'=>'HMAC-SHA256', ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //2.请求 $url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharingreturn'; $postXML = $this->toXml($postArr); Ilog::DEBUG("profitSharingReturn.postXML: " . $postXML); $opts = array( CURLOPT_HEADER => 0, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_SSLCERTTYPE => 'PEM', //默认支持的证书的类型,可以注释 CURLOPT_SSLCERT => $this->wxConfig['app_cert_pem'], CURLOPT_SSLKEY => $this->wxConfig['app_key_pem'], ); Ilog::DEBUG("profitSharingReturn.opts: " . json_encode($opts)); $curl_res = $this->curl->setOption($opts)->post($url,$postXML); Ilog::DEBUG("profitSharingReturn.curl_res: " . $curl_res); $ret[] = $this->toArray($curl_res); } } return $ret; } /** * @function 回退结果查询 * @param $order_no string 本地订单号 * @param $ticket_no string 本地票号 * @return array|false * @throws \Exception */ public function returnQuery($order_no,$ticket_no) { $ret = array(); $profitOrder = pdo_fetch("SELECT * FROM zc_ticket_orders_profitsharing WHERE order_no='{$order_no}' AND ticket_no='{$ticket_no}'"); if($profitOrder['channel']==1 and $profitOrder['state']==2) { $accounts = json_decode($profitOrder['profitsharing'],true); foreach ($accounts as $account) { //1.签名 $postArr = array( 'appid'=>$this->wxConfig['app_id'], 'mch_id'=>$this->wxConfig['mch_id'], 'sub_mch_id'=>$this->wxConfig['sub_mch_id'], 'nonce_str'=>md5(time() . rand(1000, 9999)), 'out_order_no'=>$profitOrder['order_no'].$profitOrder['ticket_no'], 'out_return_no'=>'return_'.$profitOrder['order_no'].$profitOrder['ticket_no'].'_'.$account['account'], 'sign_type'=>'HMAC-SHA256', ); $sign = $this->sign->getSign($postArr, 'HMAC-SHA256',$this->wxConfig['md5_key']); $postArr['sign'] = $sign; //2.请求 $url = 'https://api.mch.weixin.qq.com/pay/profitsharingreturnquery'; $postXML = $this->toXml($postArr); Ilog::DEBUG("returnQuery.postXML: " . $postXML); $curl_res = $this->curl->post($url,$postXML); Ilog::DEBUG("returnQuery.curl_res: " . $curl_res); $ret[] = $this->toArray($curl_res); } } return $ret; } /** * @function 将array转为xml * @param array $values * @return string|bool * @author xiewg **/ public function toXml($values) { if (!is_array($values) || count($values) <= 0) { return false; } $xml = "<xml>"; foreach ($values as $key => $val) { if (is_numeric($val)) { $xml.="<".$key.">".$val."</".$key.">"; } else { $xml.="<".$key."><![CDATA[".$val."]]></".$key.">"; } } $xml.="</xml>"; return $xml; } /** * @function 将xml转为array * @param string $xml * @return array|false * @author xiewg */ public function toArray($xml) { if (!$xml) { return false; } // 检查xml是否合法 $xml_parser = xml_parser_create(); if (!xml_parse($xml_parser, $xml, true)) { xml_parser_free($xml_parser); return false; } //将XML转为array //禁止引用外部xml实体 libxml_disable_entity_loader(true); $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true); return $data; } }发送请求类: ProfitSharingCurl.class.php
<?php /** * CUrl CURL请求类 * * 通过curl实现的快捷方便的接口请求类 * * <br>示例:<br> * * // 失败时再重试2次 * $curl = new CUrl(2); * * // GET * $rs = $curl->get('http://phalapi.oschina.mopaas.com/Public/demo/?service=Default.Index'); * * // POST * $data = array('username' => 'dogstar'); * $rs = $curl->post('http://phalapi.oschina.mopaas.com/Public/demo/?service=Default.Index', $data); * * @package PhalApi\CUrl * @license http://www.phalapi.net/license GPL 协议 * @link http://www.phalapi.net/ * @author dogstar <chanzonghuang@gmail.com> 2015-01-02 */ class ProfitSharingCurl { /** * 最大重试次数 */ const MAX_RETRY_TIMES = 10; /** * @var int $retryTimes 超时重试次数;注意,此为失败重试的次数,即:总次数 = 1 + 重试次数 */ protected $retryTimes; protected $header = array(); protected $option = array(); protected $hascookie = FALSE; protected $cookie = array(); /** * @param int $retryTimes 超时重试次数,默认为1 */ public function __construct($retryTimes = 1) { $this->retryTimes = $retryTimes < static::MAX_RETRY_TIMES ? $retryTimes : static::MAX_RETRY_TIMES; } /** ------------------ 核心使用方法 ------------------ **/ /** * GET方式的请求 * @param string $url 请求的链接 * @param int $timeoutMs 超时设置,单位:毫秒 * @return string 接口返回的内容,超时返回false */ public function get($url, $timeoutMs = 3000) { return $this->request($url, array(), $timeoutMs); } /** * POST方式的请求 * @param string $url 请求的链接 * @param array $data POST的数据 * @param int $timeoutMs 超时设置,单位:毫秒 * @return string 接口返回的内容,超时返回false */ public function post($url, $data, $timeoutMs = 3000) { return $this->request($url, $data, $timeoutMs); } /** ------------------ 前置方法 ------------------ **/ /** * 设置请求头,后设置的会覆盖之前的设置 * * @param array $header 传入键值对如: ``` * array( * 'Accept' => 'text/html', * 'Connection' => 'keep-alive', * ) ``` * * @return $this */ public function setHeader($header) { $this->header = array_merge($this->header, $header); return $this; } /** * 设置curl配置项 * * - 1、后设置的会覆盖之前的设置 * - 2、开发者设置的会覆盖框架的设置 * * @param array $option 格式同上 * * @return $this */ public function setOption($option) { $this->option = $option + $this->option; return $this; } /** * @param array $cookie */ public function setCookie($cookie) { $this->cookie = $cookie; return $this; } /** * @return array */ public function getCookie() { return $this->cookie; } public function withCookies() { $this->hascookie = TRUE; if (!empty($this->cookie)) { $this->setHeader(array('Cookie' => $this->getCookieString())); } $this->setOption(array(CURLOPT_COOKIEFILE => '')); return $this; } /** ------------------ 辅助方法 ------------------ **/ /** * 统一接口请求 * @param string $url 请求的链接 * @param array $data POST的数据 * @param int $timeoutMs 超时设置,单位:毫秒 * @return string 接口返回的内容,超时返回false * @throws Exception */ protected function request($url, $data, $timeoutMs = 3000) { $options = array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => TRUE, CURLOPT_HEADER => 0, CURLOPT_CONNECTTIMEOUT_MS => $timeoutMs, CURLOPT_HTTPHEADER => $this->getHeaders(), ); if (!empty($data)) { $options[CURLOPT_POST] = 1; $options[CURLOPT_POSTFIELDS] = $data; } $options = $this->option + $options; //$this->>option优先 $ch = curl_init(); curl_setopt_array($ch, $options); $curRetryTimes = $this->retryTimes; do { $rs = curl_exec($ch); $curRetryTimes--; } while ($rs === FALSE && $curRetryTimes >= 0); $errno = curl_errno($ch); if ($errno) { throw new InternalServerErrorException(sprintf("%s::%s(%d)\n", $url, curl_error($ch), $errno)); } //update cookie if ($this->hascookie) { $cookie = $this->getRetCookie(curl_getinfo($ch, CURLINFO_COOKIELIST)); !empty($cookie) && $this->cookie = $cookie + $this->cookie; $this->hascookie = FALSE; unset($this->header['Cookie']); unset($this->option[CURLOPT_COOKIEFILE]); } curl_close($ch); return $rs; } /** * * @return array */ protected function getHeaders() { $arrHeaders = array(); foreach ($this->header as $key => $val) { $arrHeaders[] = $key . ': ' . $val; } return $arrHeaders; } protected function getRetCookie(array $cookies) { $ret = array(); foreach ($cookies as $cookie) { $arr = explode("\t", $cookie); if (!isset($arr[6])) { continue; } $ret[$arr[5]] = $arr[6]; } return $ret; } protected function getCookieString() { $ret = ''; foreach ($this->getCookie() as $key => $val) { $ret .= $key . '=' . $val . ';'; } return trim($ret, ';'); } }签名类 ProfitSharing.class.php
<?php class ProfitSharingSign { /** * 根据Url传递的参数,生成签名字符串 * @param array $param * @param string $signType * @param $md5Key * @return string * @throws \Exception */ public function getSign(array $param, $signType = 'MD5', $md5Key) { $values = $this->paraFilter($param); $values = $this->arraySort($values); $signStr = $this->createLinkstring($values); $signStr .= '&key=' . $md5Key; switch ($signType) { case 'MD5': $sign = md5($signStr); break; case 'HMAC-SHA256': $sign = hash_hmac('sha256', $signStr, $md5Key); break; default: $sign = ''; } return strtoupper($sign); } /** * 移除空值的key * @param $para * @return array */ public function paraFilter($para) { $paraFilter = array(); while (list($key, $val) = each($para)) { if ($val == "") { continue; } else { if (! is_array($para[$key])) { $para[$key] = is_bool($para[$key]) ? $para[$key] : trim($para[$key]); } $paraFilter[$key] = $para[$key]; } } return $paraFilter; } /** * @function 对输入的数组进行字典排序 * @param array $param 需要排序的数组 * @return array * @author helei */ public function arraySort(array $param) { ksort($param); reset($param); return $param; } /** * @function 把数组所有元素,按照“参数=参数值”的模式用“&”字符拼接成字符串 * @param array $para 需要拼接的数组 * @return string * @throws \Exception */ public function createLinkString($para) { if (! is_array($para)) { throw new \Exception('必须传入数组参数'); } reset($para); $arg = ""; while (list($key, $val) = each($para)) { if (is_array($val)) { continue; } $arg.=$key."=".urldecode($val)."&"; } //去掉最后一个&字符 $arg = substr($arg, 0, count($arg) - 2); //如果存在转义字符,那么去掉转义 if (get_magic_quotes_gpc()) { $arg = stripslashes($arg); } return $arg; } }