精华内容
下载资源
问答
  • 微信服务商分账功能 PHP

    千次阅读 2020-03-25 09:12:05
    微信服务商分账接口说明: 链接: 微信服务商分账接口说明. 项目逻辑 开通分账功能 链接: 微信服务商分账接口说明. 服务商代子商户发起添加分账接收方请求 在统一下单API、付款码支付API、委托扣款API中上传...

    项目说明

    微信服务商分账接口说明:
    链接: 微信服务商分账接口说明.

    开通分账功能
    链接: 微信服务商分账接口说明.

    服务商代子商户发起添加分账接收方请求

    在统一下单API、付款码支付API、委托扣款API中上传新增参数profit_sharing,请求支付
    支付完成后,调用请求分账接口,完成分账

    注意事项

    1. 分账接口分为单次分账和多次分账
    2. 当用户需要退款时,单次分账调用接口的步骤是:分账回退->申请退款
    3. 当用户需要退款时,多次分账调用接口的步骤是:分账完结->分账回退->申请退款
    4. 商户系统内部的分账单号,在商户系统内部唯一(单次分账、多次分账、完结分账应使用不同的商户分账单号),同一分账单号多次请求等同一次
    5. Ilog是一个日志类
    6. ProfitSharingSign.class.php是用来生成签名的,跟微信支付、微信退款生成签名类是一样的
    7. ProfitSharingCurl.class.php是一个发送http请求的类
    8. pdo_*的方法是微擎框架自带的数据库操作

    PHP代码

    分账接口类 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;
        }
    }
    
    展开全文
  • <div><p>有服务商分账功能就好了.... https://pay.weixin.qq.com/wiki/doc/api/allocation_sl.php?chapter=24_1&index=1</p><p>该提问来源于开源项目:essensoft/payment</p></div>
  • 微信小程序后端java服务商分账实现

    千次阅读 2020-08-27 14:18:57
    微信小程序后端java服务商分账实现 最近公司申请微信服务商,需要给第三方提供支付、分账功能。 商户调用服务商统一支付 首先,服务商小程序支付,基本与普通商户小程序支付一致 支付使用服务商统一下单接口:微信...

    微信小程序后端java服务商分账实现

    最近公司申请微信服务商,需要给第三方提供支付、分账功能。

    商户调用服务商统一支付

    首先,服务商小程序支付,基本与普通商户小程序支付一致

    支付使用服务商统一下单接口:微信官方文档地址https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_sl_api.php?chapter=9_1
    在这里插入图片描述
    这里的商户号是服务商的商户号,小程序appid是商家的appid,子商户号是商家的商户号且商家是服务商的特约商户需要授权。
    在这里插入图片描述
    这两个openid建议使用sub_openid,商户小程序的唯一openid。

    在这里插入图片描述最重要的参数,传Y才能进行分账。

    服务商分账

    服务商分账有两种接口:单次分账和多次分账
    微信官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/allocation_sl.php?chapter=25_1&index=1

    多的不说直接上代码:
    引入maven 依赖

    <dependency>
                <groupId>org.jodd</groupId>
                <artifactId>jodd-core</artifactId>
                <version>5.1.5</version>
     </dependency>
     <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.55</version>
     </dependency>
     
    <dependency>
                <groupId>com.github.binarywang</groupId>
                <artifactId>weixin-java-pay</artifactId>
                <version>3.8.0</version>
            </dependency>
    

    分账请求类

    package com.linli.pay.service.pay.req;
    import lombok.Data;
    @Data
    public class WxSharingOrderRequest {
        /**
         * 服务商商户号
         */
        private String mch_id;
    
        /**
         * 子商户号
         */
        private String sub_mch_id;
    
        /**
         * 服务商appid
         */
        private String appid;
    
        /**
         * 子商户appid
         */
        private String sub_appid;
    
        /**
         * 随机字符串
         */
        private String nonce_str;
    
        /**
         * 签名
         */
        private String sign;
        /**
         * 签名类型(只支持HMAC-SHA256)
         */
        private String sign_type;
    
        /**
         * 微信订单号
         */
        private String transaction_id;
    
        /**
         * 商家订单号
         */
        private String out_trade_no;
    
        /**
         * 商户分账单号(同一个单号多次提交只算一次)
         */
        private String out_order_no;
    
        /**
         * 商户分账金额(小于等于订单金额*(1-手续费)*最大分账比例)
         */
        private Integer amount;
    
        /**
         * 分账接收方列表(单次分账不能即是支付商户又是接收商户,多次分账没有限制)
         */
        private String receivers;
    
    }
    

    分账返回类

    package com.linli.pay.service.pay.resp;
    import lombok.Data;
    
    @Data
    public class WxSharingOrderResp {
    
        //返回状态码,通信标识,SUCCESS/FAIL
        private String return_code;
        //返回信息,通信标识OK
        private String return_msg;
        //业务结果,交易标识,SUCCESS/FAIL
        private String result_code;
        //错误代码
        private String err_code;
        //错误代码描述
        private String err_code_des;
        //商户号
        private String mch_id;
        //子商户号
        private String sub_mch_id;
        //公众账号id
        private String appid;
    
        private String sub_appid;
    
        //随机字符串
        private String nonce_str;
        //签名
        private String sign;
        //微信支付订单号
        private String transaction_id;
        //商户分账单号(商户订单号)
        private String out_order_no;
        //商户分账单号
        private String order_id;
    
    }
    

    分账接收方

    package com.linli.pay.service.pay.req;
    
    import lombok.Data;
    
    @Data
    public class WxSharingReceiversVO {
        /**
         * 分账接收方类型
         */
        private String type;
    
        /**
         * 分账接收方帐号
         */
        private String account;
    
        /**
         * 分账金额
         */
        private Integer amount;
    
        /**
         * 分账描述
         */
        private String description;
    }
    

    application.yml配置类

    wx:
      mini:
        keyPath: classpath:cert/apiclient_cert.p12 # 商户p12证书的位置,可以指定绝对路径,也可以指定类路径(以classpath:开头)
        busAppId: 
        busMchId: 
        busMchKey: 
        busSubAppId: 
        busSubMchId: 
    

    在这里插入图片描述

    package com.linli.pay.config.wx;
    
    import com.github.binarywang.wxpay.config.WxPayConfig;
    import com.github.binarywang.wxpay.service.WxPayService;
    import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
    import lombok.Data;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    
    @Data
    @Configuration
    @ConfigurationProperties(prefix = "wx.mini")
    public class WxMiniPayProperties {
    
      /**
       * apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
       */
      private String keyPath;
    
      /**
       * 服务商appid
       */
      private String busAppId;
    
      /**
       *服务商商户号
       */
      private String busMchId;
    
      /**
       * 服务商商户密钥
       */
      private String busMchKey;
    
      /**
       * 调起支付的小程序APPID
       */
      private String busSubAppId;
    
      /**
       * 微信支付子商户号
       */
      private String busSubMchId;
    
    
      @Bean(name = "wxBusMiniPayService")
      public WxPayService wxBusMiniPayService() {
        WxPayConfig payConfig = new WxPayConfig();
        payConfig.setAppId(StringUtils.trimToNull(this.busAppId));
        payConfig.setMchId(StringUtils.trimToNull(this.busMchId));
        payConfig.setMchKey(StringUtils.trimToNull(this.busMchKey));
        payConfig.setSubAppId(StringUtils.trimToNull(this.busSubAppId));
        payConfig.setSubMchId(StringUtils.trimToNull(this.busSubMchId));
        payConfig.setKeyPath(StringUtils.trimToNull(this.keyPath));
    
        // 可以指定是否使用沙箱环境
        payConfig.setUseSandboxEnv(false);
    
        WxPayService wxPayService = new WxPayServiceImpl();
        wxPayService.setConfig(payConfig);
        return wxPayService;
      }
    
    }
    

    service实现类

    /**
         * 单次分账
         * @param data
         * @return
         */
        public WxSharingOrderResp oncePaySharing(WxSharingOrderRequest data)throws Exception{
            WxSharingOrderRequest wxSharingOrderRequest = new WxSharingOrderRequest();
            wxSharingOrderRequest.setAppid(wxBusMiniPayService.getConfig().getAppId());
            wxSharingOrderRequest.setMch_id(wxBusMiniPayService.getConfig().getMchId());
            wxSharingOrderRequest.setSub_mch_id(wxBusMiniPayService.getConfig().getSubMchId());
            wxSharingOrderRequest.setSub_appid(wxBusMiniPayService.getConfig().getSubAppId());
            wxSharingOrderRequest.setTransaction_id(data.getTransaction_id());
            wxSharingOrderRequest.setNonce_str(WxUtils.makeNonStr());
            wxSharingOrderRequest.setOut_order_no(data.getOut_order_no());
            List<WxSharingReceiversVO> list = Lists.newArrayList();
            WxSharingReceiversVO receiversVO = new WxSharingReceiversVO();
            receiversVO.setAccount(wxBusMiniPayService.getConfig().getSubMchId());
            receiversVO.setType("MERCHANT_ID");
            receiversVO.setAmount(1);
            receiversVO.setDescription("分到商户");
            list.add(receiversVO);
            wxSharingOrderRequest.setReceivers(FastJsonUtils.listToString(list));
    
            BeanMap beanMap = BeanMap.create(wxSharingOrderRequest);
            wxSharingOrderRequest.setSign(WxUtils.makeSign(beanMap,wxBusMiniPayService.getConfig().getMchKey(),"SHA256"));
            String xmlStr = WxUtils.truncateDataToXML(WxSharingOrderRequest.class, wxSharingOrderRequest).replace("&quot;","\"");
            String url = "https://api.mch.weixin.qq.com/secapi/pay/profitsharing";
            String result = WxCertHttpUtil.postData(url,xmlStr,wxBusMiniPayService.getConfig().getMchId(),wxBusMiniPayService.getConfig().getKeyPath());
            Object obj = WxUtils.truncateDataFromXML(WxSharingOrderResp.class, result);
            WxSharingOrderResp resp = new WxSharingOrderResp();
            BeanUtils.copyProperties(obj,resp);
            return resp;
    
        }
    
        /**
         * 多次分账
         * @param data
         * @return
         */
        public WxSharingOrderResp multiPaySharing(WxSharingOrderRequest data)throws Exception{
            WxSharingOrderRequest wxSharingOrderRequest = new WxSharingOrderRequest();
            wxSharingOrderRequest.setAppid(wxBusMiniPayService.getConfig().getAppId());
            wxSharingOrderRequest.setMch_id(wxBusMiniPayService.getConfig().getMchId());
            wxSharingOrderRequest.setSub_mch_id(wxBusMiniPayService.getConfig().getSubMchId());
            wxSharingOrderRequest.setSub_appid(wxBusMiniPayService.getConfig().getSubAppId());
            wxSharingOrderRequest.setTransaction_id(data.getTransaction_id());
            wxSharingOrderRequest.setNonce_str(WxUtils.makeNonStr());
            wxSharingOrderRequest.setOut_order_no(data.getOut_order_no());
            List<WxSharingReceiversVO> list = Lists.newArrayList();
            WxSharingReceiversVO receiversVO = new WxSharingReceiversVO();
            receiversVO.setAccount(wxBusMiniPayService.getConfig().getSubMchId());
            receiversVO.setType("MERCHANT_ID");
            receiversVO.setAmount(data.getAmount());
            receiversVO.setDescription("给商家分账");
            list.add(receiversVO);
            wxSharingOrderRequest.setReceivers(FastJsonUtils.listToString(list));
    
            BeanMap beanMap = BeanMap.create(wxSharingOrderRequest);
            wxSharingOrderRequest.setSign(WxUtils.makeSign(beanMap,wxBusMiniPayService.getConfig().getMchKey(),"SHA256"));
            String xmlStr = WxUtils.truncateDataToXML(WxSharingOrderRequest.class, wxSharingOrderRequest).replace("&quot;","\"");
            String url = "https://api.mch.weixin.qq.com/secapi/pay/multiprofitsharing";
    
            String result = WxCertHttpUtil.postData(url,xmlStr,wxBusMiniPayService.getConfig().getMchId(),wxBusMiniPayService.getConfig().getKeyPath());
            Object obj = WxUtils.truncateDataFromXML(WxSharingOrderResp.class, result);
            WxSharingOrderResp resp = new WxSharingOrderResp();
            BeanUtils.copyProperties(obj,resp);
            return resp;
    
        }
    

    微信工具类

    /**
     * 微信工具类
     */
    public class WxUtils {
    
        private static Logger logger = LoggerFactory.getLogger(WxUtils.class);
    
        private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    
        private static final Random RANDOM = new SecureRandom();
    
    
        /**
         * 数据转换为xml格式
         *
         * @param object
         * @param obj
         * @return
         */
        public static String truncateDataToXML(Class<?> object, Object obj) {
            XStream xStream = new XStream(new XppDriver(new NoNameCoder()));
            xStream.alias("xml", object);
            return xStream.toXML(obj);
        }
    
        /**
         * 数据转换为对象
         *
         * @param object
         * @param str
         * @return
         */
        public static Object truncateDataFromXML(Class<?> object, String str) {
            XStream xstream = new XStream(new StaxDriver());
            xstream.alias("xml", object);
            return xstream.fromXML(str);
        }
    
        /**
         * 获取随机字符串 Nonce Str
         *
         * @return String 随机字符串
         */
        public static String makeNonStr() {
            char[] nonceChars = new char[32];
            for (int index = 0; index < nonceChars.length; ++index) {
                nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
            }
            return new String(nonceChars);
        }
    
        public static String map2XmlString(Map<String, String> map) {
            String xmlResult = "";
    
            StringBuffer sb = new StringBuffer();
            sb.append("<xml>");
            for (String key : map.keySet()) {
                String value = "<![CDATA[" + map.get(key) + "]]>";
                sb.append("<" + key + ">" + value + "</" + key + ">");
                System.out.println();
            }
            sb.append("</xml>");
            xmlResult = sb.toString();
    
            return xmlResult;
        }
    
        /**
         * 拼接签名数据
         *
         * @return
         */
        public static String makeSign(BeanMap beanMap,String mchKey,String signType)throws Exception {
            SortedMap<String, String> signMaps = Maps.newTreeMap();
    
            for (Object key : beanMap.keySet()) {
                Object value = beanMap.get(key);
    
                // 排除空数据
                if (value == null) {
                    continue;
                }
                signMaps.put(key + "", String.valueOf(value));
            }
            if(signType.equals("MD5")) {
                // 生成签名
                return generateSign(signMaps, mchKey);
            }else if(signType.equals("SHA256")){
                return generateSignSHA256(signMaps, mchKey);
            }else{
                return null;
            }
        }
    
    
        /**
         * 生成签名
         *
         * @param signMaps
         * @return
         * @throws Exception
         */
        public static String generateSign(SortedMap<String, String> signMaps,String mchKey) {
            StringBuffer sb = new StringBuffer();
    
            // 字典序
            for (Map.Entry signMap : signMaps.entrySet()) {
                String key = (String) signMap.getKey();
                String value = (String) signMap.getValue();
    
                // 为空不参与签名、参数名区分大小写
                if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
                    sb.append(key).append("=").append(value).append("&");
                }
            }
    
            // 拼接key
            sb.append("key=").append(mchKey);
            // MD5加密
            String sign = MD5Encode(sb.toString(), "UTF-8").toUpperCase();
            return sign;
        }
    
        public static String generateSignSHA256(SortedMap<String, String> signMaps,String mchKey)throws Exception{
            StringBuffer sb = new StringBuffer();
    
            // 字典序
            for (Map.Entry signMap : signMaps.entrySet()) {
                String key = (String) signMap.getKey();
                String value = (String) signMap.getValue();
    
                // 为空不参与签名、参数名区分大小写
                if (null != value && !"".equals(value) && !"sign".equals(key) && !"key".equals(key)) {
                    sb.append(key).append("=").append(value).append("&");
                }
            }
    
            // 拼接key
            sb.append("key=").append(mchKey);
            // MD5加密
            String sign = HMACSHA256(sb.toString(), mchKey).toUpperCase();
            return sign;
        }
    
        private static String byteArrayToHexString(byte b[]) {
            StringBuffer resultSb = new StringBuffer();
            for (int i = 0; i < b.length; i++)
                resultSb.append(byteToHexString(b[i]));
    
            return resultSb.toString();
        }
    
        private static String byteToHexString(byte b) {
            int n = b;
            if (n < 0)
                n += 256;
            int d1 = n / 16;
            int d2 = n % 16;
            return hexDigits[d1] + hexDigits[d2];
        }
    
        public static String MD5Encode(String origin, String charsetname) {
            String resultString = null;
            try {
                resultString = new String(origin);
                MessageDigest md = MessageDigest.getInstance("MD5");
                if (charsetname == null || "".equals(charsetname))
                    resultString = byteArrayToHexString(md.digest(resultString
                            .getBytes()));
                else
                    resultString = byteArrayToHexString(md.digest(resultString
                            .getBytes(charsetname)));
            } catch (Exception exception) {
            }
            return resultString;
        }
    
        private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
                "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
    
       
    
        private static HashMap<String, String> sortAsc(Map<String, String> map) {
            HashMap<String, String> tempMap = new LinkedHashMap<String, String>();
            List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(map.entrySet());
            //排序
            Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() {
                @Override
                public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
                    return o1.getKey().compareTo(o2.getKey());
                }
            });
    
            for (int i = 0; i < infoIds.size(); i++) {
                Map.Entry<String, String> item = infoIds.get(i);
                tempMap.put(item.getKey(), item.getValue());
            }
            return tempMap;
        }
    
        private static String SHA1(String str) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-1"); //如果是SHA加密只需要将"SHA-1"改成"SHA"即可
            digest.update(str.getBytes());
            byte messageDigest[] = digest.digest();
            // Create Hex String
            StringBuffer hexStr = new StringBuffer();
            // 字节数组转换为 十六进制 数
            for (int i = 0; i < messageDigest.length; i++) {
                String shaHex = Integer.toHexString(messageDigest[i] & 0xFF);
                if (shaHex.length() < 2) {
                hexStr.append(0);
                }
                hexStr.append(shaHex);
            }
            return hexStr.toString();
    
            } catch (NoSuchAlgorithmException e) {
                e.printStackTrace();
            }
            return null;
        }
    
        /**
         * 生成 HMACSHA256
         * @param data 待处理数据
         * @param key 密钥
         * @return 加密结果
         * @throws Exception
         */
        public static String HMACSHA256(String data, String key) throws Exception {
            String hash = "";
            try {
                Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
                SecretKeySpec secret_key = new SecretKeySpec(key.getBytes(), "HmacSHA256");
                sha256_HMAC.init(secret_key);
                byte[] bytes = sha256_HMAC.doFinal(data.getBytes());
                hash = byteArrayToHexString(bytes);
            } catch (Exception e) {
                System.out.println("Error HmacSHA256 ===========" + e.getMessage());
            }
            return hash.toUpperCase();
        }
    }
    

    带双向证书的post请求工具类

    import jodd.util.ResourcesUtil;
    import org.apache.commons.lang3.RegExUtils;
    import org.apache.http.HttpEntity;
    import org.apache.http.HttpResponse;
    import org.apache.http.client.config.RequestConfig;
    import org.apache.http.client.methods.HttpPost;
    import org.apache.http.conn.ssl.NoopHostnameVerifier;
    import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
    import org.apache.http.entity.StringEntity;
    import org.apache.http.impl.client.CloseableHttpClient;
    import org.apache.http.impl.client.HttpClients;
    import org.apache.http.ssl.SSLContexts;
    import org.apache.http.util.EntityUtils;
    
    import javax.net.ssl.SSLContext;
    import java.io.IOException;
    import java.io.InputStream;
    import java.security.KeyStore;
    
    public class WxCertHttpUtil {
    
        private static int socketTimeout = 10000;// 连接超时时间,默认10秒
        private static int connectTimeout = 30000;// 传输超时时间,默认30秒
        private static RequestConfig requestConfig;// 请求配置
        private static CloseableHttpClient httpClient;// HTTP请求
    
        /**
    
         *
         * @param url API地址
         * @param xmlObj 要提交的XML
         * @param mchId 服务商商户ID
         * @param certPath证书路径
         * @return
         */
        public static String postData(String url, String xmlObj, String mchId, String certPath) {
            // 加载证书
            try {
                loadCert(mchId, certPath);
            } catch (Exception e) {
                e.printStackTrace();
            }
            String result = null;
            HttpPost httpPost = new HttpPost(url);
            StringEntity postEntity = new StringEntity(xmlObj, "UTF-8");
            httpPost.addHeader("Content-Type", "text/xml");
            httpPost.setEntity(postEntity);
            requestConfig = RequestConfig.custom().setSocketTimeout(socketTimeout).setConnectTimeout(connectTimeout).build();
    
            httpPost.setConfig(requestConfig);
            try {
                HttpResponse response = null;
                try {
                    response = httpClient.execute(httpPost);
                } catch (IOException e) {
                    e.printStackTrace();
                }
                HttpEntity entity = response.getEntity();
                try {
                    result = EntityUtils.toString(entity, "UTF-8");
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } finally {
                httpPost.abort();
            }
            return result;
        }
    
        /**
         *加载证书
         *
         * @param mchId 服务商商户ID
         * @param certPath 证书路径
         * @throws Exception
         */
        private static void loadCert(String mchId, String certPath) throws Exception {
            // 证书密码,默认为服务商商戶ID
            String key = mchId;
            // 证书路径
            String path = RegExUtils.removeFirst(certPath, "classpath:");
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            // 指定证书格式为PKCS12
            KeyStore keyStore = KeyStore.getInstance("PKCS12");
            // 读取PKCS12证书文件
            InputStream instream = ResourcesUtil.getResourceAsStream(path);
            try {
                // 指定PKCS12的密碼(商戶ID)
                keyStore.load(instream, key.toCharArray());
            } finally {
                instream.close();
            }
            SSLContext sslcontext = SSLContexts.custom().loadKeyMaterial(keyStore, key.toCharArray()).build();
            SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslcontext, NoopHostnameVerifier.INSTANCE);
            httpClient = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
        }
    }
    

    最后,可以通过微信工具检查是否签名正确。

    展开全文
  • 微信服务商分账功能开发(PHP)

    千次阅读 热门讨论 2020-08-09 17:34:11
    微信服务商分账功能开发(PHP) 博客说明 文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢! 设置分账参数 在调用支付的时候设置参数 添加分账...

    微信服务商分账功能开发(PHP)

    博客说明

    文章所涉及的资料来自互联网整理和个人总结,意在于个人学习和经验汇总,如有什么地方侵权,请联系本人删除,谢谢!

    设置分账参数

    在调用支付的时候设置参数

    image-20200809140655674

    添加分账参数

    image-20200809140739741

    在JDK中添加get,set方法

    //分账
        public function SetProfit_sharing($value)
        {
            $this->values['profit_sharing'] = $value;
        }
    
        public function GetProfit_sharing()
        {
            return $this->values['profit_sharing'];
        }
    
        public function IsProfit_sharingSet()
        {
            return array_key_exists('profit_sharing', $this->values);
        }

    添加分账接收方

    核心代码

    image-20200809155717580

    部分参数写在SDK的config文件中

    <?php
    /**
     * @name ProfitSharing.php
     * @author tanglei
     * @date 2020/8/9
     */
    
    namespace app\api\service;
    
    use think\facade\Env;
    require_once Env::get('root_path'). "extend/WxPay/WxPay.Api.php";
    
    class ProfitSharing
    {
        public static function addPro()
        {
            $receiver = [
                "type" => "MERCHANT_ID",
                "account" => "11111",
                "name" => "有限公司",
                "relation_type" => "SERVICE_PROVIDER"
            ];
            $WxOrderData = new \WxPayProfitSharing();
            //子商户号
            $WxOrderData->SetSub_Mch_id('11111');
            $WxOrderData->SetSubAppid(config('base.app_id'));
            $WxOrderData->SetReceiver(json_encode($receiver));
            $wxOrder = \WxPayApi::profitsharingAddReceiver($WxOrderData);
            return $wxOrder;
        }
    }

    要改一下SDK文件

    在Data的签名里面必须使用HMAC-SHA256

    public function MakeSignH()
        {
            //签名步骤一:按字典序排序参数
            ksort($this->values);
            $string = $this->ToUrlParams();
            //签名步骤二:在string后加入KEY
            $string = $string . "&key=".WxPayConfig::KEY;
            //签名
            $string = hash_hmac("sha256",$string,WxPayConfig::KEY);
            //签名步骤四:所有字符转为大写
            $result = strtoupper($string);
            return $result;
        }

    在Api文件中添加一个方法

    public static function profitsharingAddReceiver($inputObj, $timeOut = 6)
    {
        $url = "https://api.mch.weixin.qq.com/pay/profitsharingaddreceiver";
    
        $inputObj->SetAppid(WxPayConfig::APPID);//公众账号ID
        $inputObj->SetMch_id(WxPayConfig::MCHID);//商户号
        $inputObj->SetNonce_str(self::getNonceStr());//随机字符串
    
        //签名
        $inputObj->SetSignHash();
        $xml = $inputObj->ToXml();
    
        $startTimeStamp = self::getMillisecond();//请求开始时间
        $response = self::postXmlCurl($xml, $url, false, $timeOut);
        return $response;
        $result = WxPayResults::Init($response);
        self::reportCostTime($url, $startTimeStamp, $result);//上报请求花费时间
        return $result;
    }
    测试

    image-20200809161118770

    分账

    image-20200809173030172

    核心代码

    public static function profitSharing()
        {
            $receivers = [
                "type" => "MERCHANT_ID",
                "account" => "111111",
                "amount" => 2,
                "description" => "分到服务商"
            ];
            $orderNo = self::makeOrderNo();
            $WxOrderData = new \WxPayProfitSharing();
            $WxOrderData->SetSub_Mch_id('11111');
            $WxOrderData->SetTransaction_id('42789709008202008098970721631');
            $WxOrderData->SetOut_order_no($orderNo);
            $WxOrderData->SetSubAppid(config('base.app_id'));
            $WxOrderData->SetReceivers(json_encode($receivers));
            $wxOrder = \WxPayApi::profitsharing($WxOrderData);
            return json($wxOrder);
        }

    在Api文件中添加一个方法

    public static function profitsharing($inputObj, $timeOut = 6)
        {
            $url = "https://api.mch.weixin.qq.com/secapi/pay/profitsharing";
    
            $inputObj->SetAppid(WxPayConfig::APPID);//公众账号ID
            $inputObj->SetMch_id(WxPayConfig::MCHID);//商户号
            $inputObj->SetNonce_str(self::getNonceStr());//随机字符串
    
            //签名
            $inputObj->SetSignHash();
            $xml = $inputObj->ToXml();
    
            $startTimeStamp = self::getMillisecond();//请求开始时间
            $response = self::postXmlCurl($xml, $url, true, $timeOut);
            //return $response;
            $result = WxPayResults::Init($response);
            self::reportCostTime($url, $startTimeStamp, $result);//上报请求花费时间
            return $result;
        }
    

    测试

    image-20200809172857613

    感谢

    微信开放平台

    万能的网络

    以及勤劳的自己

    展开全文
  • 服务商请求单次分账与普通商户请求单次分账的区别。1 下预付单时 务必要添加 profit_sharing 为 Y 否则该笔订单不支持分账。 参考链接 https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=26_3 订单...

    注意事项

    服务商请求单次分账与普通商户请求单次分账的区别。1

    下预付单时 务必要添加 profit_sharing 为 Y 否则该笔订单不支持分账。 参考链接 https://pay.weixin.qq.com/wiki/doc/api/allocation.php?chapter=26_3
    订单支付成功后不能立刻执行分账逻辑 要任务操作 大于订单支付时间1分钟以上 有QPS限制 建议做好日志警告
    <?php
    
    namespace app\api\lib\Weixin;
    
    use think\Controller;
    
    class Weixin extends Controller
    {
        private $sep_url;                           // 单次分账请求URL
        private $mch_id;                            // 商户号
        private $appid;                             // 公众号appid
        private $mch_secrect;                       // 此处是商户key!!!
    
        function __construct()
        {
            $this->sep_url = 'https://api.mch.weixin.qq.com/secapi/pay/profitsharing';
            $this->mch_id = config('wechat.pay_config.mch_id');
            $this->appid = config('wechat.pay_config.app_id');
            $this->mch_secrect = config('wechat.pay_config.key');
        }
    
        /**
         * Notes: 请求单次分账
         * User: googol
         * Date: 2020-07-15
         * Time: 10:28
         * Url:
         * @param $transaction_id   微信支付交易单号
         * @param $out_order_no     商户系统内部的分账单号,在商户系统内部唯一(单次分账、多次分账、完结分账应使用不同的商户分账单号),同一分账单号多次请求等同一次。只能是数字、大小写字母_-|*@
         * @param $sub_mch_id       微信支付分配的子商户号
         */
        function requestsingleaccountsplitting($transaction_id, $out_order_no, $sub_mch_id)
        {
            $receivers = $this->receivers($out_order_no);
            if ($receivers['code'] == 0) return ['code' => '分账失败!'];
            $tmp_splitting_data = [
                'mch_id' => $this->mch_id,
                'sub_mch_id' => $sub_mch_id,    // 微信支付分配的子商户号
                'appid' => $this->appid,
                'sub_appid' => $this->appid,    // 微信分配的子商户公众账号ID
                'nonce_str' => $this->get_nonce_str(),
                'sign_type' => 'HMAC-SHA256',
                'transaction_id' => $transaction_id,
                'out_order_no' => $out_order_no,
                'receivers' => $receivers['res']
            ];
            $tmp_splitting_data['sign'] = $this->make_sign($tmp_splitting_data, $this->mch_secrect);
            $xml = $this->array_to_xml($tmp_splitting_data);
            $do_arr = $this->curl_post_ssl($this->sep_url, $xml);
            $result = $this->xml_to_array($do_arr);
            return $result;
        }
    
        /**
         * Notes: 获取随机数
         * @param int $length
         * @return string
         */
        private function get_nonce_str($length = 32)
        {
            $chars = "abcdefghijklmnopqrstuvwxyz0123456789";
            $str = "";
            for ($i = 0; $i < $length; $i++) {
                $str .= substr($chars, mt_rand(0, strlen($chars) - 1), 1);
            }
            return $str;
        }
    
        /**
         * Notes: 获取分账详细列表信息
         * User: googol
         * @param $out_order_no     商户内部的分账单号
         */
        private function receivers($out_order_no)
        {
    
            /*CREATE TABLE `m_separateaccounts_order` (
              `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
              `trade` varchar(32) NOT NULL DEFAULT '' COMMENT '分账单号',
              `m_id` int(11) NOT NULL DEFAULT '0' COMMENT '机器id',
              `s_id` int(11) NOT NULL DEFAULT '0' COMMENT '店铺id',
              `agent_id` int(11) NOT NULL COMMENT '代理id',
              `account` varchar(64) NOT NULL DEFAULT '' COMMENT '分账接收openid',
              `agent_type` tinyint(1) NOT NULL DEFAULT '3' COMMENT '0平台 1一级代理 2二级代理',
              `proportion` int(5) NOT NULL DEFAULT '0' COMMENT '比例',
              `payment` decimal(12,2) NOT NULL DEFAULT '0.00' COMMENT '实际付款金额',
              `shareamount` int(11) NOT NULL DEFAULT '0' COMMENT '分得金额 分',
              `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '结算状态 0待分账 1已分账 2分账失败',
              `cdate` varchar(12) NOT NULL DEFAULT '' COMMENT '日期',
              `regdate` int(11) NOT NULL COMMENT '时间',
              `shareddate` int(11) NOT NULL DEFAULT '0' COMMENT '分账时间',
              PRIMARY KEY (`id`)
            ) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8 COMMENT='分账订单';*/
    
            $out_order = db('separateaccounts_order')
                ->where(['trade' => $out_order_no, 'status' => 0])
                ->field('payment, account, shareamount')
                ->select();
            if (!empty($out_order)) {
                $receivers_arr = [];
                foreach ($out_order as $key => $val) {
                    $receivers_arr[$key]['type'] = 'PERSONAL_OPENID';
                    $receivers_arr[$key]['account'] = $val['account'];
                    $receivers_arr[$key]['amount'] = $val['shareamount'];
                    $receivers_arr[$key]['description'] = 'Agency sub account.';
                }
                return ['code' => 1, 'res' => json_encode($receivers_arr)];
            }
            return ['code' => 0];
        }
    
        /**
         * Notes: 生成sign
         * @param $arr
         * @param $secret
         * @return string
         */
        private function make_sign($arr, $secret)
        {
            //签名步骤一:按字典序排序参数
            ksort($arr);
            $str = $this->to_url_params($arr);
            //签名步骤二:在str后加入KEY
            $str = $str . "&key=" . $secret;
            //签名步骤三:HMAC-SHA256 类型  加密的字符串 key是商户秘钥
            $str = hash_hmac('sha256', $str, $this->mch_secrect);
            //签名步骤四:所有字符转为大写
            $result = strtoupper($str);
            return $result;
        }
    
        /**
         * Notes: 数组转字符串
         * @param $arr
         * @return string
         */
        private function to_url_params($arr)
        {
            $str = "";
            foreach ($arr as $k => $v) {
                if (!empty($v) && ($k != 'sign')) {
                    $str .= "$k" . "=" . $v . "&";
                }
            }
            $str = rtrim($str, "&");
            return $str;
        }
    
        /**
         * Notes: 数组转XML
         * @param $arr
         * @return string
         */
        private function array_to_xml($arr){
            $xml = '<?xml version="1.0" encoding="UTF-8"?><xml>';
            foreach ($arr as $key => $val) {
                $xml.="<".$key.">$val</".$key.">";
            }
            $xml.="</xml>";
            return $xml;
        }
    
        /**
         * Notes: XML转数组
         * @param $xml
         * @return mixed
         */
        private function xml_to_array($xml){
            libxml_disable_entity_loader(true);
            $arr= json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
            return $arr;
        }
    
        /**
         * Notes: POST 请求 此处需要证书
         * @param $xml
         * @param $url
         * @param int $second
         * @return bool|string
         */
        function curl_post_ssl($url, $vars, $second = 30, $aHeader = array())
        {
            $isdir = __DIR__ . "/../../../../cert/";//证书位置
    
            $ch = curl_init();//初始化curl
            curl_setopt($ch, CURLOPT_TIMEOUT, $second);//设置执行最长秒数
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//要求结果为字符串且输出到屏幕上
            curl_setopt($ch, CURLOPT_URL, $url);//抓取指定网页
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);// 终止从服务端进行验证
            curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);//
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');//证书类型
            curl_setopt($ch, CURLOPT_SSLCERT, $isdir . 'apiclient_cert.pem');//证书位置
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');//CURLOPT_SSLKEY中规定的私钥的加密类型
            curl_setopt($ch, CURLOPT_SSLKEY, $isdir . 'apiclient_key.pem');//证书位置
    //        curl_setopt($ch, CURLOPT_CAINFO, 'PEM');
    //        curl_setopt($ch, CURLOPT_CAINFO, $isdir . 'rootca.pem');
            if (count($aHeader) >= 1) {
                curl_setopt($ch, CURLOPT_HTTPHEADER, $aHeader);//设置头部
            }
            curl_setopt($ch, CURLOPT_POST, 1);//post提交方式
            curl_setopt($ch, CURLOPT_POSTFIELDS, $vars);//全部数据使用HTTP协议中的"POST"操作来发送
    
            $data = curl_exec($ch);//执行回话
            if ($data) {
                curl_close($ch);
                return $data;
            } else {
                $error = curl_errno($ch);
                echo "call faild, errorCode:$error\n";
                curl_close($ch);
                return false;
            }
        }
    }
    

    1. 服务商添加分账方与普通商户添加分账方大致一致 只需要额外注意服务商分账需要服务商添加子商户的子商户号与公众账号ID ↩︎

    展开全文
  • 微信服务商分账对接

    2021-04-23 15:55:06
    微信分账是为了解决员工抽成,服务商抽成的难题,而衍生的产品。下面我们简单介绍下 首先服务商需要在微信商户平台里面,替商户开通分账的权限。商户授权通过后,就可以正常使用了。 官方文档:...
  • ![图片说明](https://img-ask.csdn.net/upload/202006/29/1593416633_261346.png)
  • 签名参数://按照参数名ASCII字典序排序;... //服务商appid$post['body'] = $body; //商品描述$post['mch_id'] = $mch_id; //服务商商户号$post['nonce_str'] = $nonce_str;//随机字符串$post['noti...
  • //服务商appid $post['body'] = $body; //商品描述 $post['mch_id'] = $mch_id; //服务商商户号 $post['nonce_str'] = $nonce_str;//随机字符串 $post['notify_url'] = $notify_url; //支付
  • 微信服务商分账功能总结

    万次阅读 2019-03-06 17:01:22
    基于官方文档:服务商分账接口文档 ,根据我们自身的需求开发功能。此文为开发后的总结和思考。
  • 服务商分账 Step 2: ① 预约商户绑定微信会收到“微信支付商家助手欧”的授权信息进行设置分账比例后输入支付密码授权 ② 登录商户平台授权:产品中心 >> 我授权的产品 >> 服务商分账授权(右...
  • 服务商模式下调用单次分账接口

    千次阅读 2019-08-22 11:27:24
    接上一章,完成服务商模式下的微信支付之后,进行服务商分账。微信单次分账文档地址 首先在进行单次分账之前,需要设置一个分账比例,步骤如下: 登录特约商户号–产品中心–我授权的产品–服务商分账–产品配置 !...
  • 微信支付服务商分账-添加分账接收方写在前面:服务商官方文档SIGN值校验检查地址调试中可能遇到的问题直接上代码 服务商添加分账接收方与普通商户添加分账接收方的区别。1 写在前面: 服务商官方文档 ...
  • 服务商模式下添加分账接收方

    千次阅读 2019-08-27 10:55:13
    服务商模式下要想进行分账操作,必须先添加分账接收方,添加分账接收方有两种方式,第一种是在特约商户里添加,第二种是调用微信添加分账接收方接口 第一种实现方式: 登录特约商户商户号–交易中心–管理分账接收方...

空空如也

空空如也

1 2 3 4 5
收藏数 92
精华内容 36
关键字:

服务商分账