This repository has been archived on 2024-07-11. You can view files and clone it, but cannot push or open issues or pull requests.
yoshop/source/application/common/library/wechat/WxPay.php
2020-04-25 22:20:29 +08:00

401 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\common\library\wechat;
use app\common\model\Wxapp as WxappModel;
use app\common\enum\OrderType as OrderTypeEnum;
use app\common\enum\order\PayType as PayTypeEnum;
use app\common\exception\BaseException;
/**
* 微信支付
* Class WxPay
* @package app\common\library\wechat
*/
class WxPay extends WxBase
{
// 微信支付配置
private $config;
// 订单模型
private $modelClass = [
OrderTypeEnum::MASTER => 'app\api\service\order\PaySuccess',
OrderTypeEnum::SHARING => 'app\api\service\sharing\order\PaySuccess',
OrderTypeEnum::RECHARGE => 'app\api\service\recharge\PaySuccess',
];
/**
* 构造函数
* WxPay constructor.
* @param $config
*/
public function __construct($config = false)
{
parent::__construct();
$this->config = $config;
$this->config !== false && $this->setConfig($this->config['app_id'], $this->config['app_secret']);
}
/**
* 统一下单API
* @param $order_no
* @param $openid
* @param $totalFee
* @param int $orderType 订单类型
* @return array
* @throws BaseException
*/
public function unifiedorder($order_no, $openid, $totalFee, $orderType = OrderTypeEnum::MASTER)
{
// 当前时间
$time = time();
// 生成随机字符串
$nonceStr = md5($time . $openid);
// API参数
$params = [
'appid' => $this->appId,
'attach' => json_encode(['order_type' => $orderType]),
'body' => $order_no,
'mch_id' => $this->config['mchid'],
'nonce_str' => $nonceStr,
'notify_url' => base_url() . 'notice.php', // 异步通知地址
'openid' => $openid,
'out_trade_no' => $order_no,
'spbill_create_ip' => \request()->ip(),
'total_fee' => $totalFee * 100, // 价格:单位分
'trade_type' => 'JSAPI',
];
// 生成签名
$params['sign'] = $this->makeSign($params);
// 请求API
$url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
$result = $this->post($url, $this->toXml($params));
$prepay = $this->fromXml($result);
// 请求失败
if ($prepay['return_code'] === 'FAIL') {
throw new BaseException(['msg' => "微信支付api{$prepay['return_msg']}", 'code' => -10]);
}
if ($prepay['result_code'] === 'FAIL') {
throw new BaseException(['msg' => "微信支付api{$prepay['err_code_des']}", 'code' => -10]);
}
// 生成 nonce_str 供前端使用
$paySign = $this->makePaySign($params['nonce_str'], $prepay['prepay_id'], $time);
return [
'prepay_id' => $prepay['prepay_id'],
'nonceStr' => $nonceStr,
'timeStamp' => (string)$time,
'paySign' => $paySign
];
}
/**
* 支付成功异步通知
* @throws BaseException
* @throws \Exception
* @throws \think\exception\DbException
*/
public function notify()
{
// $xml = <<<EOF
//<xml><appid><![CDATA[wx8908532a27c5dd4f]]></appid>
//<attach><![CDATA[{"order_type":10}]]></attach>
//<bank_type><![CDATA[CFT]]></bank_type>
//<cash_fee><![CDATA[1]]></cash_fee>
//<fee_type><![CDATA[CNY]]></fee_type>
//<is_subscribe><![CDATA[N]]></is_subscribe>
//<mch_id><![CDATA[1509822581]]></mch_id>
//<nonce_str><![CDATA[ca1fe6d2b4f667cf249bd1d7176c6178]]></nonce_str>
//<openid><![CDATA[oZDDE5JLnVyc6qe6nbNWdbFHtY5I]]></openid>
//<out_trade_no><![CDATA[2019040155491005]]></out_trade_no>
//<result_code><![CDATA[SUCCESS]]></result_code>
//<return_code><![CDATA[SUCCESS]]></return_code>
//<sign><![CDATA[3880232710B7328822D079DC405FB09D]]></sign>
//<time_end><![CDATA[20190401104804]]></time_end>
//<total_fee>1</total_fee>
//<trade_type><![CDATA[JSAPI]]></trade_type>
//<transaction_id><![CDATA[4200000265201904014227830207]]></transaction_id>
//</xml>
//EOF;
if (!$xml = file_get_contents('php://input')) {
$this->returnCode(false, 'Not found DATA');
}
// 将服务器返回的XML数据转化为数组
$data = $this->fromXml($xml);
// 记录日志
$this->doLogs($xml);
$this->doLogs($data);
// 实例化订单模型
$model = $this->getOrderModel($data['out_trade_no'], $data['attach']);
// 订单信息
$order = $model->getOrderInfo();
empty($order) && $this->returnCode(false, '订单不存在');
// 小程序配置信息
$wxConfig = WxappModel::getWxappCache($order['wxapp_id']);
// 设置支付秘钥
$this->config['apikey'] = $wxConfig['apikey'];
// 保存微信服务器返回的签名sign
$dataSign = $data['sign'];
// sign不参与签名算法
unset($data['sign']);
// 生成签名
$sign = $this->makeSign($data);
// 判断签名是否正确 判断支付状态
if (
($sign !== $dataSign)
|| ($data['return_code'] !== 'SUCCESS')
|| ($data['result_code'] !== 'SUCCESS')
) {
$this->returnCode(false, '签名失败');
}
// 订单支付成功业务处理
$status = $model->onPaySuccess(PayTypeEnum::WECHAT, $data);
if ($status == false) {
$this->returnCode(false, $model->getError());
}
// 返回状态
$this->returnCode(true, 'OK');
}
/**
* 申请退款API
* @param $transaction_id
* @param double $total_fee 订单总金额
* @param double $refund_fee 退款金额
* @return bool
* @throws BaseException
*/
public function refund($transaction_id, $total_fee, $refund_fee)
{
// 当前时间
$time = time();
// 生成随机字符串
$nonceStr = md5($time . $transaction_id . $total_fee . $refund_fee);
// API参数
$params = [
'appid' => $this->appId,
'mch_id' => $this->config['mchid'],
'nonce_str' => $nonceStr,
'transaction_id' => $transaction_id,
'out_refund_no' => $time,
'total_fee' => $total_fee * 100,
'refund_fee' => $refund_fee * 100,
];
// 生成签名
$params['sign'] = $this->makeSign($params);
// 请求API
$url = 'https://api.mch.weixin.qq.com/secapi/pay/refund';
$result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
// 请求失败
if (empty($result)) {
throw new BaseException(['msg' => '微信退款api请求失败']);
}
// 格式化返回结果
$prepay = $this->fromXml($result);
// 请求失败
if ($prepay['return_code'] === 'FAIL') {
throw new BaseException(['msg' => 'return_msg: ' . $prepay['return_msg']]);
}
if ($prepay['result_code'] === 'FAIL') {
throw new BaseException(['msg' => 'err_code_des: ' . $prepay['err_code_des']]);
}
return true;
}
/**
* 企业付款到零钱API
* @param $order_no
* @param $openid
* @param $amount
* @param $desc
* @return bool
* @throws BaseException
*/
public function transfers($order_no, $openid, $amount, $desc)
{
// API参数
$params = [
'mch_appid' => $this->appId,
'mchid' => $this->config['mchid'],
'nonce_str' => md5(uniqid()),
'partner_trade_no' => $order_no,
'openid' => $openid,
'check_name' => 'NO_CHECK',
'amount' => $amount * 100,
'desc' => $desc,
'spbill_create_ip' => \request()->ip(),
];
// 生成签名
$params['sign'] = $this->makeSign($params);
// 请求API
$url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
$result = $this->post($url, $this->toXml($params), true, $this->getCertPem());
// 请求失败
if (empty($result)) {
throw new BaseException(['msg' => '微信退款api请求失败']);
}
// 格式化返回结果
$prepay = $this->fromXml($result);
// 请求失败
if ($prepay['return_code'] === 'FAIL') {
throw new BaseException(['msg' => 'return_msg: ' . $prepay['return_msg']]);
}
if ($prepay['result_code'] === 'FAIL') {
throw new BaseException(['msg' => 'err_code_des: ' . $prepay['err_code_des']]);
}
return true;
}
/**
* 获取cert证书文件
* @return array
* @throws BaseException
*/
private function getCertPem()
{
if (empty($this->config['cert_pem']) || empty($this->config['key_pem'])) {
throw new BaseException(['msg' => '请先到后台小程序设置填写微信支付证书文件']);
}
// cert目录
$filePath = __DIR__ . '/cert/' . $this->config['wxapp_id'] . '/';
return [
'certPem' => $filePath . 'cert.pem',
'keyPem' => $filePath . 'key.pem'
];
}
/**
* 实例化订单模型 (根据attach判断)
* @param $orderNo
* @param null $attach
* @return mixed
*/
private function getOrderModel($orderNo, $attach = null)
{
$attach = json_decode($attach, true);
// 判断订单类型返回对应的订单模型
$model = $this->modelClass[$attach['order_type']];
return new $model($orderNo);
}
/**
* 返回状态给微信服务器
* @param boolean $returnCode
* @param string $msg
*/
private function returnCode($returnCode = true, $msg = null)
{
// 返回状态
$return = [
'return_code' => $returnCode ? 'SUCCESS' : 'FAIL',
'return_msg' => $msg ?: 'OK',
];
// 记录日志
log_write([
'describe' => '返回微信支付状态',
'data' => $return
]);
die($this->toXml($return));
}
/**
* 生成paySign
* @param $nonceStr
* @param $prepay_id
* @param $timeStamp
* @return string
*/
private function makePaySign($nonceStr, $prepay_id, $timeStamp)
{
$data = [
'appId' => $this->appId,
'nonceStr' => $nonceStr,
'package' => 'prepay_id=' . $prepay_id,
'signType' => 'MD5',
'timeStamp' => $timeStamp,
];
// 签名步骤一:按字典序排序参数
ksort($data);
$string = $this->toUrlParams($data);
// 签名步骤二在string后加入KEY
$string = $string . '&key=' . $this->config['apikey'];
// 签名步骤三MD5加密
$string = md5($string);
// 签名步骤四:所有字符转为大写
$result = strtoupper($string);
return $result;
}
/**
* 将xml转为array
* @param $xml
* @return mixed
*/
private function fromXml($xml)
{
// 禁止引用外部xml实体
libxml_disable_entity_loader(true);
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
}
/**
* 生成签名
* @param $values
* @return string 本函数不覆盖sign成员变量如要设置签名需要调用SetSign方法赋值
*/
private function makeSign($values)
{
//签名步骤一:按字典序排序参数
ksort($values);
$string = $this->toUrlParams($values);
//签名步骤二在string后加入KEY
$string = $string . '&key=' . $this->config['apikey'];
//签名步骤三MD5加密
$string = md5($string);
//签名步骤四:所有字符转为大写
$result = strtoupper($string);
return $result;
}
/**
* 格式化参数格式化成url参数
* @param $values
* @return string
*/
private function toUrlParams($values)
{
$buff = '';
foreach ($values as $k => $v) {
if ($k != 'sign' && $v != '' && !is_array($v)) {
$buff .= $k . '=' . $v . '&';
}
}
return trim($buff, '&');
}
/**
* 输出xml字符
* @param $values
* @return bool|string
*/
private 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;
}
}