php wechat app payment

Keywords: xml PHP Mobile SDK

Scenario introduction
It is applicable for merchants to integrate wechat payment function in mobile APP.
When the merchant APP calls the SDK provided by wechat to call the wechat payment module, the merchant APP will jump to wechat to complete the payment, then jump back to the merchant APP after the payment, and finally display the payment results.
At present, wechat payment support mobile systems include IOS (Apple), Android (Android) and WP (Windows Phone).
Document reference
https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_1

Development steps
1. Generate client payment parameters
Call the unified single interface to obtain the pre authorization id, and use the pre authorization id to sign twice to generate the parameters required by the client to start wechat payment

2. Notice of payment result
After the payment is completed, wechat will send the relevant payment results and user information to the merchant, which needs to receive and process and return the response.
When interacting with back-end notifications, if wechat receives a response from a merchant that is not successful or overtime, wechat considers the notification as a failure, wechat will periodically re launch the notification through a certain strategy to improve the success rate of the notification as much as possible, but wechat does not guarantee that the notification will eventually succeed.
Original wechat app payment SDK

Note: official didn't provide

<?php
namespace app\pay\tool;
/**
 * Place an order on wechat payment server
 * @author        lzw     
 * Wechat APP payment document address: https://pay.weixin.qq.com/wiki/doc/api/APP.php? Chapter = 8_
 * Use example
 * $options = array(
 *        'appid'    =>    '**********',        //Fill in the public account ID assigned by wechat
 *        'mchid'    =>    '********',                //Fill in the merchant number assigned by wechat payment
 *        'notify_url'=>    'http://www.baidu.com/',    //Fill in wechat payment result callback address
 *        'key'        =>    ''**********''                //Fill in wechat merchant payment key
 *    );
 *    Unified order method
 *    $WechatAppPay = new wechatAppPay($options);
 *    $params['body'] = 'Product description '; / / product description
 *    $params['out_trade_no'] = '1217752501201407';    //Custom order number
 *    $params['total_fee'] = '100';                    //The order amount can only be an integer with the unit of cent
 *    $wechatAppPay->unifiedOrder( $params );
 */
class WxpayAppSDK
{
    //Interface API URL prefix
    const API_URL_PREFIX = 'https://api.mch.weixin.qq.com';
    //Order address URL
    const UNIFIEDORDER_URL = "/pay/unifiedorder";
    //Query order URL
    const ORDERQUERY_URL = "/pay/orderquery";
    //Close order URL
    const CLOSEORDER_URL = "/pay/closeorder";

    //Public account ID
    private $appid;
    //Merchant No
    private $mch_id;
    //Random string
    private $nonce_str;
    //autograph
    private $sign;
    //Product description
    private $body;
    //Merchant order number
    private $out_trade_no;
    //Total amount paid
    private $total_fee;
    //Terminal IP
    private $spbill_create_ip;
    //Payment result callback notification address
    private $notify_url;
    //Transaction type
    private $trade_type;
    //Payment key
    private $key;
    //Certificate path
    private $SSLCERT_PATH;
    private $SSLKEY_PATH;
//    Save error message
    public $errorMsg = '';
    //All parameters
    private $params = array();

    /**
     * Incoming configuration information
     * $options = array(
     *        'appid'    =>    '**********',        //Fill in the public account ID assigned by wechat
     *        'mchid'    =>    '********',                //Fill in the merchant number assigned by wechat payment
     *        'notify_url'=>    'http://www.baidu.com/',    //Fill in wechat payment result callback address
     *        'key'        =>    ''**********''                //Fill in wechat merchant payment key
     *    );
     * WxpayApp constructor.
     * @param $options
     */
    public function __construct($options)
    {
        $this->appid = isset($options['appid']) ? $options['appid'] : '';
        $this->mch_id = isset($options['mchid']) ? $options['mchid'] : '';
        $this->notify_url = isset($options['notify_url']) ? $options['notify_url'] : '';
        $this->key = isset($options['key']) ? $options['key'] : '';
    }


    /**
     * Order method - > unified order interface
     * @link https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1
     * @param    $params    Order parameters
     */
    public function unifiedOrder($params)
    {
        $this->body = $params['body'];
        $this->out_trade_no = $params['out_trade_no'];
        $this->total_fee = $params['total_fee'];
        $this->trade_type = 'APP';//Transaction type JSAPI | NATIVE |APP | WAP
        $this->nonce_str = $this->genRandomString();
        $this->spbill_create_ip = $_SERVER['REMOTE_ADDR'];

        $this->params['appid'] = $this->appid;
        $this->params['mch_id'] = $this->mch_id;
        $this->params['nonce_str'] = $this->nonce_str;
        $this->params['body'] = $this->body;
        $this->params['out_trade_no'] = $this->out_trade_no;
        $this->params['total_fee'] = $this->total_fee;
        $this->params['spbill_create_ip'] = $this->spbill_create_ip;
        $this->params['notify_url'] = $this->notify_url;
        $this->params['trade_type'] = $this->trade_type;

        //Get signature data
        $this->sign = $this->MakeSign($this->params);
        $this->params['sign'] = $this->sign;
        $xml = $this->data_to_xml($this->params);
        $response = $this->postXmlCurl($xml, self::API_URL_PREFIX . self::UNIFIEDORDER_URL);
        if (!$response) {
            return false;
        }
        $result = $this->xml_to_data($response);
        if (!empty($result['result_code']) && !empty($result['err_code'])) {
            $result['err_msg'] = $this->error_code($result['err_code']);
        }
        return $result;
    }

    /**
     * Query order information
     * @param $out_trade_no        order number
     * @return array
     */
    public function orderQuery($out_trade_no)
    {

        $this->params['appid'] = $this->appid;
        $this->params['mch_id'] = $this->mch_id;
        $this->params['nonce_str'] = $this->genRandomString();
        $this->params['out_trade_no'] = $out_trade_no;

        //Get signature data
        $this->sign = $this->MakeSign($this->params);
        $this->params['sign'] = $this->sign;
        $xml = $this->data_to_xml($this->params);
        $response = $this->postXmlCurl($xml, self::API_URL_PREFIX . self::ORDERQUERY_URL);
        if (!$response) {
            return false;
        }
        $result = $this->xml_to_data($response);
        if (!empty($result['result_code']) && !empty($result['err_code'])) {
            $result['err_msg'] = $this->error_code($result['err_code']);
        }
        return $result;
    }

    /**
     * Close order
     * @param $out_trade_no        order number
     * @return array
     */
    public function closeOrder($out_trade_no)
    {
        $this->params['appid'] = $this->appid;
        $this->params['mch_id'] = $this->mch_id;
        $this->params['nonce_str'] = $this->genRandomString();
        $this->params['out_trade_no'] = $out_trade_no;

        //Get signature data
        $this->sign = $this->MakeSign($this->params);
        $this->params['sign'] = $this->sign;
        $xml = $this->data_to_xml($this->params);
        $response = $this->postXmlCurl($xml, self::API_URL_PREFIX . self::CLOSEORDER_URL);
        if (!$response) {
            return false;
        }
        $result = $this->xml_to_data($response);
        return $result;
    }

    /**
     *
     * Get payment result notification data
     * return array
     */
    public function getNotifyData()
    {
        //Get notified data
        $xml = $GLOBALS['HTTP_RAW_POST_DATA'];
        $data = array();
        if (empty($xml)) {
            return false;
        }
        $data = $this->xml_to_data($xml);
        if (!empty($data['return_code'])) {
            if ($data['return_code'] == 'FAIL') {
                return false;
            }
        }
        return $data;
    }

    /**
     * Verify notification signature
     */
    public function verifyNotify($data)
    {
        $sign = $data['sign'];
        unset($data['sign']);
        if ($sign != $this->MakeSign($data)) {
            return false;
        } else {
            return true;
        }

    }

    /**
     * Reply and output XML data after receiving notification successfully
     * @param string $xml
     */
    public function replyNotifySuccess()
    {
        $data['return_code'] = 'SUCCESS';
        $data['return_msg'] = 'OK';
        $xml = $this->data_to_xml($data);
        echo $xml;
        die();
    }
    /**
     * Reply and output XML data after receiving notification failure
     * @param string $xml
     */
    public function replyNotifyFail()
    {
        $data['return_code'] = 'Fail';
        $data['return_msg'] = 'Failed to process order';
        $xml = $this->data_to_xml($data);
        echo $xml;
        die();
    }


    /**
     * The second signature is used by the client to call the wechat client
     * @link https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_7&index=3
     * Generate APP payment parameters
     * @param    $prepayid    Advance payment id
     */
    public function getAppPayParams($prepayid)
    {
        $data['appid'] = $this->appid;
        $data['partnerid'] = $this->mch_id;
        $data['prepayid'] = $prepayid;
        $data['package'] = 'Sign=WXPay';
        $data['noncestr'] = $this->genRandomString();
        $data['timestamp'] = time();
        $data['sign'] = $this->MakeSign($data);
        return $data;
    }

    /**
     * Get signature data to client quickly
     * @param $param
     *                  $params['body'] = 'Product description '; / / product description
     *                    $params['out_trade_no'] = '1217752501201407';    //Custom order number
     *                    $params['total_fee'] = '100';                    //The order amount can only be an integer with the unit of cent
     * @return mixed
     */
    public function getAppPaySign($params)
    {
        $result = $this->unifiedOrder($params);
        if ($result['return_code'] == 'SUCCESS' && $result['result_code'] == 'SUCCESS') {
            $data = $this->getAppPayParams($result['prepay_id']);
            return $data;
        } else {
//            $result ['return ﹐ MSG '] pay attention to the cause of tracking failure
            $this->errorMsg = $result['return_msg'];
            return false;
        }
    }


    /**
     * Generate signature
     * @return autograph
     */
    public function MakeSign($params)
    {
        //Signature step 1: sort array parameters in dictionary order
        ksort($params);
        $string = $this->ToUrlParams($params);
        //Signature step 2: add KEY after string
        $string = $string . "&key=" . $this->key;
        //Signature step 3: MD5 encryption
        $string = md5($string);
        //Step 4: change all characters to uppercase
        $result = strtoupper($string);
        return $result;
    }

    /**
     * Splicing parameters into URL: key = value & key = value
     * @param    $params
     * @return    string
     */
    public function ToUrlParams($params)
    {
        $string = '';
        if (!empty($params)) {
            $array = array();
            foreach ($params as $key => $value) {
                $array[] = $key . '=' . $value;
            }
            $string = implode("&", $array);
        }
        return $string;
    }

    /**
     * Output xml characters
     * @param    $params        Parameter name
     * return    string        Return the assembled xml
     **/
    public function data_to_xml($params)
    {
        if (!is_array($params) || count($params) <= 0) {
            return false;
        }
        $xml = "<xml>";
        foreach ($params as $key => $val) {
            if (is_numeric($val)) {
                $xml .= "<" . $key . ">" . $val . "</" . $key . ">";
            } else {
                $xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
            }
        }
        $xml .= "</xml>";
        return $xml;
    }


    /**
     * Convert xml to array
     * @param string $xml
     * return array
     */
    public function xml_to_data($xml)
    {
        if (!$xml) {
            return false;
        }
        //Convert XML to array
        //Prohibit reference to external xml entities
        libxml_disable_entity_loader(true);
        $data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
        return $data;
    }


    /**
     * Get timestamps at millisecond level
     */
    private static function getMillisecond()
    {
        //Get timestamps in milliseconds
        $time = explode(" ", microtime());
        $time = $time[1] . ($time[0] * 1000);
        $time2 = explode(".", $time);
        $time = $time2[0];
        return $time;
    }

    /**
     * Generates a random string of a specified length and returns it to the user
     * @param type $len Length of the resulting string
     * @return string Random string
     */
    private function genRandomString($len = 32)
    {
        $chars = array(
            "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
            "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
            "w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
            "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
            "S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
            "3", "4", "5", "6", "7", "8", "9"
        );
        $charsLen = count($chars) - 1;
        // Scramble array
        shuffle($chars);
        $output = "";
        for ($i = 0; $i < $len; $i++) {
            $output .= $chars[mt_rand(0, $charsLen)];
        }
        return $output;
    }

    /**
     * Submit xml to the corresponding interface url by post
     *
     * @param string $xml xml data requiring post
     * @param string $url url
     * @param bool $useCert Certificate required or not, not required by default
     * @param int $second url Execution timeout, default 30s
     * @throws WxPayException
     */
    private function postXmlCurl($xml, $url, $useCert = false, $second = 30)
    {
        $ch = curl_init();
        //Set timeout
        curl_setopt($ch, CURLOPT_TIMEOUT, $second);

        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
        //Set header
        curl_setopt($ch, CURLOPT_HEADER, FALSE);
        //The result is expected to be a string and output to the screen
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);

        if ($useCert == true) {
            //Set up certificate
            //Use certificate: cert and key belong to two. pem files respectively
            curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
            //curl_setopt($ch,CURLOPT_SSLCERT, WxPayConfig::SSLCERT_PATH);
            curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
            //curl_setopt($ch,CURLOPT_SSLKEY, WxPayConfig::SSLKEY_PATH);
        }
        //post submission method
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
        //Run curl
        $data = curl_exec($ch);
        //Return results
        if ($data) {
            curl_close($ch);
            return $data;
        } else {
            $error = curl_errno($ch);
            curl_close($ch);
            return false;
        }
    }

    /**
     * error code 
     * @param    $code        Error code output by server
     * return string
     */
    public function error_code($code)
    {
        $errList = array(
            'NOAUTH' => 'The merchant has not opened this interface authority',
            'NOTENOUGH' => 'Insufficient balance of user account',
            'ORDERNOTEXIST' => 'Order number does not exist',
            'ORDERPAID' => 'Merchant order paid, no need to repeat operation',
            'ORDERCLOSED' => 'The current order is closed and cannot be paid',
            'SYSTEMERROR' => 'System error!System timeout',
            'APPID_NOT_EXIST' => 'Missing in parameter APPID',
            'MCHID_NOT_EXIST' => 'Missing in parameter MCHID',
            'APPID_MCHID_NOT_MATCH' => 'appid and mch_id Mismatch',
            'LACK_PARAMS' => 'Missing required request parameters',
            'OUT_TRADE_NO_USED' => 'The same transaction cannot be submitted multiple times',
            'SIGNERROR' => 'Incorrect parameter signature result',
            'XML_FORMAT_ERROR' => 'XML Format error',
            'REQUIRE_POST_METHOD' => 'not used post Pass parameters ',
            'POST_DATA_EMPTY' => 'post Data cannot be empty',
            'NOT_UTF8' => 'The specified encoding format is not used',
        );
        if (array_key_exists($code, $errList)) {
            return $errList[$code];
        }
    }
}

Original address: https://www.kancloud.cn/weber_lzw/book/253125

Posted by vince251 on Mon, 04 May 2020 21:35:33 -0700