# 前提
获取openid：公众号可以通过微信网页授权机制，来获取用户基本信息。
文档地址：https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html

# 1 组装请求参数
文档地址：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1
由支付文档可知必填的参数有哪些，以下是组装好的请求参数：
```php
/**
 * 组装统一下单请求数据
 */
public function getOrderData()
{
  $postData = [
            'appid'            => '这里是填申请到的appid',               // 应用APPID
            'mch_id'           => '这里是填收款的商户号',                 // 商户号
            'nonce_str'        => (string)time(),                       // 随机字符串
            'body'             => "商品描述",                            // 商品描述
            'out_trade_no'     => time(),                               // 订单号
            'total_fee'        => 100,                                  // 订单总金额，单位为分
            'spbill_create_ip' => $this->getClientRealIp(),             // 终端IP
            'notify_url'       => 'https://your.domain.name/notify',    // 支付回调通知地址
            'trade_type'       => 'JSAPI',                              // 交易类型
			'openid'           => '这里是填在前提中获取的openid',         // 用户标识
        ];
		
  $postData['sign'] = $this->generateWxPaySign($postData, '这里是填商户的密钥'); // 使用商户密钥签名
  return $this->arrayToXml($postData);
}
```
- `getClientRealIp`：获取客户端IP，具体代码见 1.1。
- `generateWxPaySign()`：对请求参数使用商户密钥进行签名，具体代码见 1.2。
- `arrayToXml()`：将 array 转换为 xml 格式，具体代码见 1.3。

## 1.1 获取客户端IP
```php
    /**
     * 获取客户端IP
     */
    public function getClientRealIp()
    {
        if (isset($_SERVER['HTTP_CDN_SRC_IP']) && $_SERVER['HTTP_CDN_SRC_IP'] != 'unknown') {
            $realip = $_SERVER["HTTP_CDN_SRC_IP"];
        } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
            $realip = getenv('HTTP_X_FORWARDED_FOR');
        } elseif (getenv('HTTP_CLIENT_IP')) {
            $realip = getenv('HTTP_CLIENT_IP');
        } else {
            $realip = getenv('REMOTE_ADDR');
        }

        $realip = explode(',', $realip);
        if ($realip[0] === '::1') return '127.0.0.1';
        return $realip[0];
    }

```
## 1.2 对请求参数签名
```php
    /**
     * 使用商户平台密钥对数据进行签名
     *
     * @param $data       array  需要计算签名的数据
     * @param $privateKey string 商户平台密钥
     * @return string
     */
    public function generateWxPaySign($data, $privateKey)
    {
        ksort($data);
        $signStr = http_build_query($data).'&key='.$privateKey;
        return strtoupper(md5(urldecode($signStr)));
    }
```

## 1.3 array 转为 xml 格式
```php
    /**
     * 数组转为XML格式
     *
     * @param array $postData 需要转换的数组
     * @return string
     */
    public function arrayToXml($postData)
    {
        $xml = "<xml>";
        $xml .= $this->toXml($postData);
        $xml .= "</xml>";
        return $xml;
    }
	
	
    /**
     * 数组拼接为XML字符
     *
     * @param mixed  $data 数据
     * @param string $item 数字索引时的节点名称
     * @param string $id   数字索引key转换为的属性名
     * @return string
     */
    public function toXml($data, $item = 'item', $id = 'id')
    {
        $xml = $attr = '';
        foreach ($data as $key => $val) {
            if (is_numeric($key)) {
                $id && $attr = " {$id}=\"{$key}\"";
                $key = $item;
            }
            $xml .= "<{$key}{$attr}>";
            $xml .= (is_array($val) || is_object($val)) ? $this->toXml($val, $item, $id) : $val;
            $xml .= "</{$key}>";
        }
        return $xml;
    }
```

# 2 请求统一下单

```php
/**
 * 请求统一下单
  *
  * @param $xmlPostData array 请求参数
  * @return bool|string
 */
public function curlUnifiedOrder($xmlPostData)
{
  $curl = curl_init('https://api.mch.weixin.qq.com/pay/unifiedorder');
  curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($curl, CURLOPT_HEADER, 0);
  curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
  curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false);
  curl_setopt($curl, CURLOPT_POST, 1);
  curl_setopt($curl, CURLOPT_POSTFIELDS, $xmlPostData);

  $output = curl_exec($curl);
  return $output;
}
```
如果请求参数没错的话，会返回如下结果：
```xml
<xml>
<return_code><![CDATA[SUCCESS]]></return_code>
<return_msg><![CDATA[OK]]></return_msg>
<result_code><![CDATA[SUCCESS]]></result_code>
<mch_id><![CDATA[这里是商户号]]></mch_id>
<appid><![CDATA[这里是appid]]></appid>
<nonce_str><![CDATA[dg5KGqdSpaHFknzv]]></nonce_str>
<sign><![CDATA[3F227D06DCCD788DDFA281F28577DCC0]]></sign>
<prepay_id><![CDATA[wx04183050291852b897064cc0d6cec81111]]></prepay_id>
<trade_type><![CDATA[JSAPI]]></trade_type>
</xml>
```
# 3 获取预支付交易会话标识
从步骤2返回的结果中获取`prepay_id`。
```php
    /**
     * 获取prepay_id
     *
     * @param $output string 请求结果，xml格式
     * @return false|mixed
     */
    public function getPrepayId($output)
    {
        $output     = $this->xmlToArray($output);
        $conditions = is_array($output)
                      && isset($output['return_code'])
					  && $output['return_code'] == 'SUCCESS'
                      && isset($output['result_code'])
					  && $output['result_code'] == 'SUCCESS';

        return $conditions ? $output['prepay_id'] : false;
    }
```
- `xmlToArray()`：xml 格式转换为 array，具体代码见 3.1。

## 3.1 xml 转为 array 格式
```php
    /**
     * XML转为Array格式
     *
     * @param $data string Xml格式的数据
     * @return mixed
     */
    public function xmlToArray($data)
    {
		$loadXml = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
		return json_decode(json_encode($loadXml), true)
    }
```

# 4 调起支付
文档地址：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_7&index=6
组装前端调起微信支付需要的数据，如下所示：
```php
    /**
     * 组装JSAPI调起支付需要的数据
     *
     * @param $prepayid string 预支付交易会话标识
     * @return array
     */
    public function getPayData($prepayid)
    {
        $payData = [
            'appId'     => '这里是填申请到的appid',  // 公众号ID
            'timeStamp' => time(),                  // 时间戳
            'nonceStr'  => time(),                  // 随机字符串
            'package'   => 'prepay_id='.$prepayid,  // 扩展字段
            'signType'  => 'MD5',                   // 签名方式
        ];
        $payData['paySign'] = $this->generateWxPaySign($payData, '这里是填商户的密钥');// 使用商户密钥签名
        return $payData;
    }
```
- `generateWxPaySign()`：对请求参数使用商户密钥进行签名，具体代码见 1.2。

# 5 支付回调
文档地址：https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_7&index=8
用户支付完成后，微信会通知商户，商户需要在接受数据、处理数据后，返回应答。
回调地址是在步骤1中的`notify_url`字段填写的。
```php
    public function notify()
    {
        $notifyXml   = file_get_contents('php://input');
        $notifyArray = $this->xmlToArray($notifyXml);

        // 检查回调参数是否正确
        $checkParams = isset($notifyArray['sign'])
                       && isset($notifyArray['result_code']) 
					   && $notifyArray['result_code'] === 'SUCCESS'
                       && isset($notifyArray['return_code']) 
					   && $notifyArray['return_code'] === 'SUCCESS';
        if (!$checkParams) return false;

        // 验签
        $sign = $this->generateWxPaySign($notifyArray, '这里是填商户的密钥');
        if ($sign != $notifyArray['sign']) return false;

        // TODO：按照业务更新订单支付状态等等
    }
```
- `xmlToArray()`：xml 格式转换为 array，具体代码见 3.1。
- `generateWxPaySign()`：对请求参数使用商户密钥进行签名，具体代码见 1.2。
