<?php
namespace service\weChat;
class WeChatPaySvc
{
//微信统一下单接口
private $unifiedOrder = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
//订单查询
private $orderQuery = 'https://api.mch.weixin.qq.com/pay/orderquery';
//退款
private $refund = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
const METHOD_PUT = 'PUT';
const METHOD_POST = 'POST';
const METHOD_GET = 'GET';
const secretKey = '';//微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
//微信统一下单参数
private $weChatUnifiedOrderParams = [
'appid' => '', //微信开放平台审核通过的应用APPID(请登录open.weixin.qq.com查看,注意与公众号的APPID不同)
'mch_id' => '', //微信支付分配的商户号
'device_info' => 'WEB', //终端设备号(门店号或收银设备ID),默认请传"WEB"
'nonce_str' => '', //随机字符串,不长于32位。 https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3
'sign_type' => '', //签名类型,目前支持HMAC-SHA256和MD5,默认为MD5
'body' => '', //商品描述交易字段格式根据不同的应用场景按照以下格式:
'detail' => '', //APP——需传入应用市场上的APP名字-实际商品名称,天天爱消除-游戏充值。
'attach' => '', //商品详细描述,对于使用单品优惠的商户,该字段必须按照规范上传,详见 https://pay.weixin.qq.com/wiki/doc/api/danpin.php?chapter=9_102&index=2
'out_trade_no' => '', //附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
'fee_type' => 'CNY', //商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*且在同一个商户号下唯一。详见商户订单号 https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_2
'total_fee' => '', //订单总金额,单位为分,详见支付金额 https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_2
'spbill_create_ip' => '', //支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
'time_start' => '', //订单生成时间,格式为yyyyMMddHHmmss,如2009年12月25日9点10分10秒表示为20091225091010。其他详见时间规则 //https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_2
'time_expire' => '', //订单失效时间,格式为yyyyMMddHHmmss,如2009年12月27日9点10分10秒表示为20091227091010。订单失效时间是针对订单号而言的,由于在请求支付的时候有一个必传参数prepay_id只有两小时的有效期,所以在重入时间超过2小时的时候需要重新请求下单接口获取新的prepay_id。其他详见时间规则 建议:最短失效时间间隔大于1分钟 https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_2
'goods_tag' => '', //订单优惠标记,代金券或立减优惠功能的参数,说明详见代金券或立减优惠 https://pay.weixin.qq.com/wiki/doc/api/tools/sp_coupon.php?chapter=12_1
'notify_url' => '', //接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。
'trade_type' => 'APP', //支付类型
'limit_type' => '', //no_credit--指定不能使用信用卡支付
'receipt' => 'N', //Y,传入Y时,支付成功消息和支付详情页将出现开票入口。需要在微信支付商户平台或微信公众平台开通电子发票功能,传此字段才可生效
];
//微信查询订单接口
private $weChatOrderQueryParams = [
'appid' => '', //微信开放平台审核通过的应用APPID(请登录open.weixin.qq.com查看,注意与公众号的APPID不同)
'mch_id' => '', //微信支付分配的商户号
'nonce_str' => '', //随机字符串,不长于32位。 https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=4_3
'transaction_id' => '',
'out_trade_no' => '',
];
//app调起微信支付参数
private $appPaymentParams = [
'appid' => '', //微信开放平台审核通过的应用APPID(请登录open.weixin.qq.com查看,注意与公众号的APPID不同)
'partnerid' => '', //微信支付分配的商户号
'prepayid' => '', //微信返回的支付交易会话ID
'package' => 'Sign=WXPay',
'noncestr' => '',
'timestamp' => '',
];
//微信退款
//注意refund_account参数
//策略一:当天支付的钱,从未结算中退;非当天支付的钱,从余额中退(结算的钱到余额中有个缓冲期1-3天,结算到余额要收千分之一的手续费)。确保退款正常,需要在余额中留有备用金。
//策略二:优先从未结算中退,未结算中余额不足,再从余额中退。(需要查询两次,比较消耗网络。好处就是可以节省被腾讯收取的千分之一的费用。)
private $weChatRefundParams = [
'appid' => '', //微信开放平台审核通过的应用APPID(请登录open.weixin.qq.com查看,注意与公众号的APPID不同)
'mch_id' => '', //微信支付分配的商户号
'nonce_str' => '', //随机字符串,不长于32位。推荐随机数生成算法
'sign' => '',
'sign_type' => 'MD5',//签名类型,目前支持HMAC-SHA256和MD5,默认为MD5
'transaction_id' => '',//微信生成的订单号,在支付通知中有返回 transaction_id和out_trade_no二选一
'out_trade_no' => '',//商户系统内部订单号,要求32个字符内,只能是数字、大小写字母_-|*@ ,且在同一个商户号下唯一。 transaction_id和out_trade_no二选一
'out_refund_no' => '',//商户系统内部的退款单号,商户系统内部唯一,只能是数字、大小写字母_-|*@ ,同一退款单号多次请求只退一笔。
'total_fee' => '',//订单总金额,单位为分,只能为整数,详见支付金额
'refund_fee' => '',//退款总金额,订单总金额,单位为分,只能为整数,详见支付金额
'refund_fee_type' => 'CNY',//退款货币类型,需与支付一致,或者不填。符合ISO 4217标准的三位字母代码,默认人民币:CNY,其他值列表详见货币类型
'refund_desc' => '',//若商户传入,会在下发给用户的退款消息中体现退款原因 注意:若订单退款金额≤1元,且属于部分退款,则不会在退款消息中体现退款原因
'refund_account' => '',//仅针对老资金流商户使用 REFUND_SOURCE_UNSETTLED_FUNDS---未结算资金退款(默认使用未结算资金退款) REFUND_SOURCE_RECHARGE_FUNDS---可用余额退款
'notify_url' => '',//异步接收微信支付退款结果通知的回调地址,通知URL必须为外网可访问的url,不允许带参数 如果参数中传了notify_url,则商户平台上配置的回调地址将不会生效。
];
/*
* https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1
* 微信统一下单
* */
public function weChatUnifiedOrder($orderNumber, $totalFee, $goodsName, $notifyUrl)
{
$conf = \Helper::getConfigPhp('system');
if(isset($conf['wxApp']['appid']) && !empty($conf['wxApp']['appid']))
{
$this->weChatUnifiedOrderParams['appid'] = $conf['wxApp']['appid'];
}
if(isset($conf['wxApp']['mch_id']) && !empty($conf['wxApp']['mch_id']))
{
$this->weChatUnifiedOrderParams['mch_id'] = $conf['wxApp']['mch_id'];
}
$this->weChatUnifiedOrderParams['appid'] = '';
$this->weChatUnifiedOrderParams['time_start'] = date('YmdHis');
$this->weChatUnifiedOrderParams['notify_url'] = $notifyUrl;
$this->weChatUnifiedOrderParams['total_fee'] = $totalFee;
$this->weChatUnifiedOrderParams['body'] = $goodsName;
$this->weChatUnifiedOrderParams['out_trade_no'] = $orderNumber;
$this->weChatUnifiedOrderParams['nonce_str'] = md5(uniqid().max(1000,9999));
$sign = self::sign($this->weChatUnifiedOrderParams);
$this->weChatUnifiedOrderParams['sign'] = $sign;
$xml = self::toXml($this->weChatUnifiedOrderParams);
$res = self::send($this->unifiedOrder, self::METHOD_POST, $xml);
if(empty($res) || empty($res['response'])) return false;
$res_xml = simplexml_load_string($res['response'], null, LIBXML_NOCDATA);
if(empty($res_xml)) return false;
$result = @json_decode(json_encode($res_xml), true);
return $result;
}
/*
* https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_2
* 微信查询订单
* */
public function weChatOrderQuery($orderNumber, $transactionId='')
{
$conf = \Helper::getConfigPhp('system');
if(isset($conf['wxApp']['appid']) && !empty($conf['wxApp']['appid']))
{
$this->weChatOrderQueryParams['appid'] = $conf['wxApp']['appid'];
}
if(isset($conf['wxApp']['mch_id']) && !empty($conf['wxApp']['mch_id']))
{
$this->weChatOrderQueryParams['mch_id'] = $conf['wxApp']['mch_id'];
}
$this->weChatOrderQueryParams['appid'] = '';
$this->weChatOrderQueryParams['transaction_id'] = $transactionId;
$this->weChatOrderQueryParams['out_trade_no'] = $orderNumber;
$this->weChatOrderQueryParams['nonce_str'] = md5(uniqid().max(1000,9999));
$sign = self::sign($this->weChatOrderQueryParams);
$this->weChatOrderQueryParams['sign'] = $sign;
$xml = self::toXml($this->weChatOrderQueryParams);
$res = self::send($this->orderQuery, self::METHOD_POST, $xml);
if(empty($res) || empty($res['response'])) return false;
$res_xml = simplexml_load_string($res['response'], null, LIBXML_NOCDATA);
if(empty($res_xml)) return false;
$result = @json_decode(json_encode($res_xml), true);
return $result;
}
/*
* 生成app调起微信支付的参数
* */
public function appPaymentParams($prepayid)
{
$conf = \Helper::getConfigPhp('system');
if(isset($conf['wxApp']['appid']) && !empty($conf['wxApp']['appid']))
{
$this->appPaymentParams['appid'] = $conf['wxApp']['appid'];
}
if(isset($conf['wxApp']['mch_id']) && !empty($conf['wxApp']['mch_id']))
{
$this->appPaymentParams['partnerid'] = $conf['wxApp']['mch_id'];
}
$this->appPaymentParams['appid'] = '';
$this->appPaymentParams['prepayid'] = $prepayid;
$this->appPaymentParams['noncestr'] = md5(uniqid().max(1000,9999));
$this->appPaymentParams['timestamp'] = time();
$sign = self::sign($this->appPaymentParams);
$this->appPaymentParams['sign'] = $sign;
return $this->appPaymentParams;
}
/*
* https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_4&index=6
* 退款
* $outTradeNo 商户订单号
* $transactionId 微信订单号
* $outRefundNo 商户退款单号
* $totalFee 订单金额
* $refundFee 退款金额
* $refundDesc 退款原因 若订单退款金额≤1元,且属于部分退款,则不会在退款消息中体现退款原因
*
* */
public function weChatRefund($transactionId='', $outTradeNo, $outRefundNo, $totalFee, $refundFee, $refundDesc='', $notifyUrl='')
{
$conf = \Helper::getConfigPhp('system');
if(isset($conf['wxApp']['appid']) && !empty($conf['wxApp']['appid']))
{
$this->weChatRefundParams['appid'] = $conf['wxApp']['appid'];
}
if(isset($conf['wxApp']['mch_id']) && !empty($conf['wxApp']['mch_id']))
{
$this->weChatRefundParams['mch_id'] = $conf['wxApp']['mch_id'];
}
$this->weChatRefundParams['appid'] = '';
$this->weChatRefundParams['transaction_id'] = $transactionId;
$this->weChatRefundParams['out_trade_no'] = $outTradeNo;
$this->weChatRefundParams['nonce_str'] = md5(uniqid().max(1000,9999));
$this->weChatRefundParams['out_refund_no'] = $outRefundNo;
$this->weChatRefundParams['total_fee'] = $totalFee;
$this->weChatRefundParams['refund_fee'] = $refundFee;
$this->weChatRefundParams['refund_desc'] = $refundDesc;
$this->weChatRefundParams['notify_url'] = $notifyUrl;
$sign = self::sign($this->weChatRefundParams);
$this->weChatRefundParams['sign'] = $sign;
$xml = self::toXml($this->weChatRefundParams);
$res = self::send($this->refund, self::METHOD_POST, $xml);
if(empty($res) || empty($res['response'])) return false;
$res_xml = simplexml_load_string($res['response'], null, LIBXML_NOCDATA);
if(empty($res_xml)) return false;
$result = @json_decode(json_encode($res_xml), true);
return $result;
}
static function sign($params)
{
ksort($params);
$string = '';
foreach($params as $key=>$value){
if(empty($value))
{
continue;
}
$string .= "$key=$value&";
}
$string = $string . "key=".self::secretKey;
$string = md5($string);
return strtoupper($string);
}
static function verify($params)
{
$sign = $params['sign'];
unset($params['sign']);
if(self::sign($params) != $sign)
{
return false;
}
return true;
}
static function toXml($data)
{
$xml = '<xml>';
foreach($data as $key=>$val){
$xml .= "<$key>$val</$key>";
}
$xml .= '</xml>';
return $xml;
}
/**
* zll 将信息提交到微信服务器,发起企业付款
*/
static public function send($url, $method='POST', $data=null,$admin = 0, $headers=[], $timeout=5){
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_MAXREDIRS, 3);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
switch($method){
case self::METHOD_POST:
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
break;
case self::METHOD_PUT:
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
break;
}
$header[] = 'X-HTTP-Method-Override: '.$method;
curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
curl_setopt($ch, CURLOPT_HEADER, 1);
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
curl_setopt($ch, CURLOPT_SSLCERTTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLCERT, APP_PATH.'/conf/cert/apiclient_cert.pem');
curl_setopt($ch, CURLOPT_SSLKEYTYPE, 'PEM');
curl_setopt($ch, CURLOPT_SSLKEY, APP_PATH.'/conf/cert/apiclient_key.pem');
$response = curl_exec($ch);
$headers = curl_getinfo($ch);
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$res_header = substr($response,0, $headerSize);
$res_body = substr($response, $headerSize);
$headers['res_header'] = $res_header;
curl_close($ch);
return ['response'=>$res_body, 'headers'=>$headers];
}
}
支付回调
<?php
use service\weChat\WeChatPaySvc;
use service\shop\ShopOrderSvc;
use shop\ShopOrderModel;
class WechatController extends BaseController
{
//初始化函数
public function init()
{
parent::init();
}
/*
* 商城支付微信回调
* */
public function wechatNotifyShopAction()
{
$xml = file_get_contents("php://input");
\MyLog::logAntiCheating('wechatNotifyShopAction:xml'.base64_encode($xml));
$jsonXml = json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA));
$data = json_decode($jsonXml, true);
\MyLog::logAntiCheating('wechatNotifyShopAction:data'.json_encode($data));
$returnData = [
'return_code' => 'SUCCESS',
'return_msg' => 'OK'
];
if($data['result_code'] != 'SUCCESS' || $data['return_code'] != 'SUCCESS')
{
//todo 微信错误记录
}
//验签
if(!WeChatPaySvc::verify($data))
{
$returnData['return_code'] = 'FAIL';
$returnData['return_msg'] = '签名验证失败';
$xml = WeChatPaySvc::toXml($returnData);
exit($xml);
}
//微信支付订单号
$transactionId = $data['transaction_id'];
$orderNumber = $data['out_trade_no'];
//查询订单是否已经支付
$shopOrderSvc = new ShopOrderSvc();
$orderDetail = $shopOrderSvc->orderDetail($orderNumber);
if($orderDetail->payStatus == \shop\ShopOrderModel::PAY_STATUS1)
{
$returnData['return_msg'] = '订单已支付';
$xml = WeChatPaySvc::toXml($returnData);
exit($xml);
}
//检查订单金额
if($orderDetail->payAmount*100 != $data['total_fee'])
{
$returnData['return_code'] = 'FAIL';
$returnData['return_msg'] = '订单金额错误';
$xml = WeChatPaySvc::toXml($returnData);
exit($xml);
}
//同步订单状态
$shopOrderModel = new ShopOrderModel();
$query = [
'_id' => $orderDetail->_id,
];
$update = [
'$set' => [
'payTime' => time(),
'payStatus' => ShopOrderModel::PAY_STATUS1,
'transactionId' => $data['transaction_id'],
]
];
$shopOrderModel->update($query, $update);
$xml = WeChatPaySvc::toXml($returnData);
exit($xml);
}
/*
* 退款
* */
public function wechatNotifyRefundAction()
{
$xml = file_get_contents("php://input");
\MyLog::logAntiCheating('wechatNotifyRefundAction:xml'.base64_encode($xml));
$jsonXml = json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA));
$data = json_decode($jsonXml, true);
\MyLog::logAntiCheating('wechatNotifyRefundAction:data'.json_encode($data));
$returnData = [
'return_code' => 'SUCCESS',
'return_msg' => 'OK'
];
//解密req_info
$reqInfo = $data['req_info'];
$decrypt = base64_decode($reqInfo, true);
$xml = openssl_decrypt($decrypt, 'aes-256-ecb', md5(WeChatPaySvc::secretKey), OPENSSL_RAW_DATA);
$jsonXml = json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA));
$data = json_decode($jsonXml, true);
//记录退款结果
$shopRefundModel = new \shop\ShopRefundModel();
$query = [
'refundNumber' => $data['out_refund_no'],
'orderNumber' => $data['out_trade_no'],
];
$param = [
'$set' =>[
'wxRefundReqInfo' => $data,
]
];
$shopRefundModel->update($query, $param);
//同步订单状态
$shopOrderModel = new ShopOrderModel();
$shopOrderModel->update(['orderNumber'=>$data['out_trade_no']], ['$set'=>['payStatus'=>ShopOrderModel::PAY_STATUS3]]);
$xml = WeChatPaySvc::toXml($returnData);
exit($xml);
}
}
网友评论