<?php
|
|
namespace app\common\library\easywechat;
|
|
use app\admin\model\settings\Setting as SettingModel;
|
use app\api\service\order\paysuccess\type\PayTypeSuccessFactory;
|
use app\common\enum\order\OrderTypeEnum;
|
use app\common\enum\order\OrderPayTypeEnum;
|
use app\common\exception\BaseException;
|
use app\common\model\app\App as AppModel;
|
use app\common\model\app\AppWx as AppWxModel;
|
|
/**
|
* 微信支付
|
*/
|
class WxPay
|
{
|
// 微信支付配置
|
private $app;
|
|
/**
|
* 构造函数
|
*/
|
public function __construct($app)
|
{
|
$this->app = $app;
|
}
|
|
/**
|
* 统一下单API
|
*/
|
public function unifiedorder($order_no, $openid, $totalFee, $orderType = OrderTypeEnum::MASTER, $pay_source, $multiple, $app_id)
|
{
|
$data = [
|
'attach' => json_encode(['order_type' => $orderType, 'pay_source' => $pay_source, 'multiple' => $multiple]),
|
'body' => $order_no,
|
'out_trade_no' => $order_no,
|
'total_fee' => $totalFee * 100,// 价格:单位分
|
'spbill_create_ip' => \request()->ip(),
|
'notify_url' => base_url() . 'index.php/job/notify/wxpay', // 异步通知地址
|
'trade_type' => 'JSAPI', // 请对应换成你的支付方式对应的值类型
|
'openid' => $openid
|
];
|
//h5支付差异
|
if ($pay_source == 'h5') {
|
unset($data['openid']);
|
$data['trade_type'] = 'MWEB';
|
$data['scene_info'] = '{"h5_info":{"type":"Wap","wap_url":'.base_url().',"wap_name":"支付"}}';//场景信息 必要参数
|
}
|
if ($pay_source == 'android' || $pay_source == 'ios') {
|
$data['trade_type'] = 'APP';
|
$data['body'] = '订单支付';
|
}
|
$service_pay = false;
|
// 是否开启服务商支付、目前只验证了微信小程序
|
if($pay_source == 'wx'){
|
// 是否开启服务商支付
|
$service_pay = $this->isServicePay($app_id);
|
if($service_pay){
|
unset($data['openid']);
|
$data['sub_openid'] = $openid;
|
}
|
}
|
// 统一下单
|
$result = $this->app->order->unify($data);
|
// 请求失败
|
if ($result['return_code'] === 'FAIL') {
|
throw new BaseException(['msg' => "微信支付api:{$result['return_msg']}", 'code' => 0]);
|
}
|
if ($result['result_code'] === 'FAIL') {
|
throw new BaseException(['msg' => "微信支付api:{$result['err_code_des']}", 'code' => 0]);
|
}
|
//如果是微信小程序
|
if ($pay_source == 'wx' || $pay_source == 'android' || $pay_source == 'ios') {
|
$time = time();
|
if($pay_source == 'wx') {
|
// 二次签名的参数必须与下面相同
|
$params = [
|
'appId' => $result['appid'],//有所修改
|
'timeStamp' => $time,
|
'nonceStr' => $result['nonce_str'],
|
'package' => 'prepay_id=' . $result['prepay_id'],
|
'signType' => 'MD5',
|
];
|
if($service_pay){
|
$params['appId'] = $result['sub_appid'];
|
}else{
|
$params['appId'] = $result['appid'];
|
}
|
$result['paySign'] = $this->makeSign($params);
|
return [
|
'prepay_id' => $result['prepay_id'],
|
'nonceStr' => $result['nonce_str'],
|
'timeStamp' => (string)$time,
|
'paySign' => $result['paySign']
|
];
|
} else if ($pay_source == 'android' || $pay_source == 'ios') {
|
// 二次签名的参数必须与下面相同
|
$params = [
|
'appid' => $result['appid'],//有所修改
|
'partnerid' => $result['mch_id'],
|
'prepayid' => $result['prepay_id'],
|
'package' => 'Sign=WXPay',
|
'noncestr' => $result['nonce_str'],
|
'timestamp' => $time,
|
];
|
$result['paySign'] = $this->makeSign($params);
|
return [
|
'appid' => $result['appid'],
|
'partnerid' => $result['mch_id'],
|
'prepayid' => $result['prepay_id'],
|
'package' => 'Sign=WXPay',
|
'noncestr' => $result['nonce_str'],
|
'timestamp' => (string)$time,
|
'sign' => $result['paySign']
|
];
|
}
|
}
|
return $result;
|
}
|
|
/**
|
* 支付成功异步通知
|
*/
|
public function notify()
|
{
|
if (!$xml = file_get_contents('php://input')) {
|
log_write('Not found DATA');
|
$this->returnCode(false, 'Not found DATA');
|
}
|
// 将服务器返回的XML数据转化为数组
|
$data = $this->fromXml($xml);
|
// 记录日志
|
log_write($xml);
|
log_write($data);
|
$attach = json_decode($data['attach'], true);
|
// 实例化订单模型
|
$PaySuccess = PayTypeSuccessFactory::getFactory($data['out_trade_no'], $attach);
|
$app_id = $PaySuccess->isExist();
|
$app_id == 0 && $this->returnCode(false, '订单不存在');
|
// 支付配置信息
|
if($attach['pay_source'] == 'mp' || $attach['pay_source'] == 'payH5'){
|
$this->app = AppMp::getWxPayApp($app_id);
|
} else if($attach['pay_source'] == 'wx'){
|
$this->app = AppWx::getWxPayApp($app_id);
|
} else if($attach['pay_source'] == 'app'){
|
$this->app = AppOpen::getWxPayApp($app_id);
|
}
|
// 保存微信服务器返回的签名sign
|
$dataSign = $data['sign'];
|
// sign不参与签名算法
|
unset($data['sign']);
|
// 是否服务商支付,切换apikey验签
|
$this->isServicePay($app_id);
|
// 生成签名
|
$sign = $this->makeSign($data);
|
// 判断签名是否正确 判断支付状态
|
if (
|
($sign !== $dataSign)
|
|| ($data['return_code'] !== 'SUCCESS')
|
|| ($data['result_code'] !== 'SUCCESS')
|
) {
|
$this->returnCode(false, '签名失败');
|
}
|
// 订单支付成功业务处理
|
$status = $PaySuccess->onPaySuccess(OrderPayTypeEnum::WECHAT, $data);
|
if ($status == false) {
|
$this->returnCode(false, $PaySuccess->error);
|
}
|
// 返回状态
|
$this->returnCode(true, 'OK');
|
}
|
|
private function isServicePay($app_id)
|
{
|
$config = SettingModel::getSysConfig();
|
$app = AppModel::detail($app_id);
|
$app_wx = AppWxModel::detail($app_id);
|
if($config['weixin_service']['is_open'] == 1 && $app['weixin_service'] == 1){
|
$this->app->config['app_id'] = $config['weixin_service']['app_id'];
|
$this->app->config['mch_id'] = $config['weixin_service']['mch_id'];
|
$this->app->config['key'] = $config['weixin_service']['apikey'];
|
$filePath = root_path() . 'runtime/cert/appwx/10000/';
|
$this->app->config['cert_path'] = $filePath . 'cert.pem';
|
$this->app->config['key_path'] = $filePath . 'key.pem';
|
$this->app->setSubMerchant($app['mchid'], $app_wx['wxapp_id']);
|
return true;
|
}else{
|
return false;
|
}
|
}
|
|
/**
|
* 申请退款API
|
*/
|
public function refund($transaction_id, $total_fee, $refund_fee)
|
{
|
// 当前时间
|
$time = time();
|
$result = $this->app->refund->byTransactionId($transaction_id, $time, $total_fee * 100, $refund_fee * 100, [
|
// 可在此处传入其他参数,详细参数见微信支付文档
|
'refund_desc' => '用户申请取消',
|
]);
|
|
// 请求失败
|
if (empty($result)) {
|
throw new BaseException(['msg' => '微信退款api请求失败']);
|
}
|
// 请求失败
|
if ($result['return_code'] === 'FAIL') {
|
throw new BaseException(['msg' => 'return_msg: ' . $result['return_msg']]);
|
}
|
if ($result['result_code'] === 'FAIL') {
|
throw new BaseException(['msg' => 'err_code_des: ' . $result['err_code_des']]);
|
}
|
return true;
|
}
|
|
/**
|
* 企业付款到零钱API
|
*/
|
public function transfers($order_no, $openid, $amount, $desc)
|
{
|
$result = $this->app->transfer->toBalance([
|
'partner_trade_no' => $order_no, // 商户订单号,需保持唯一性(只能是字母或者数字,不能包含有符号)
|
'openid' => $openid,
|
'check_name' => 'NO_CHECK', // NO_CHECK:不校验真实姓名, FORCE_CHECK:强校验真实姓名
|
'amount' => $amount * 100, // 企业付款金额,单位为分
|
'desc' => $desc, // 企业付款操作说明信息。必填
|
]);
|
// 请求失败
|
if (empty($result)) {
|
throw new BaseException(['msg' => '微信提现到零钱api请求失败']);
|
}
|
// 请求失败
|
if ($result['return_code'] === 'FAIL') {
|
throw new BaseException(['msg' => 'return_msg: ' . $result['return_msg']]);
|
}
|
if ($result['result_code'] === 'FAIL') {
|
throw new BaseException(['msg' => 'err_code_des: ' . $result['err_code_des']]);
|
}
|
return true;
|
}
|
|
/**
|
* 返回状态给微信服务器
|
*/
|
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));
|
}
|
|
|
/**
|
* 生成签名
|
*/
|
private function makeSign($values)
|
{
|
//签名步骤一:按字典序排序参数
|
ksort($values);
|
$string = $this->toUrlParams($values);
|
//签名步骤二:在string后加入KEY
|
$string = $string . '&key=' . $this->app->config['key'];
|
//签名步骤三:MD5加密
|
$string = md5($string);
|
//签名步骤四:所有字符转为大写
|
$result = strtoupper($string);
|
return $result;
|
}
|
|
/**
|
* 格式化参数格式化成url参数
|
*/
|
private function toUrlParams($values)
|
{
|
$buff = '';
|
foreach ($values as $k => $v) {
|
if ($k != 'sign' && $v != '' && !is_array($v)) {
|
$buff .= $k . '=' . $v . '&';
|
}
|
}
|
return trim($buff, '&');
|
}
|
|
/**
|
* 将xml转为array
|
*/
|
private function fromXml($xml)
|
{
|
// 禁止引用外部xml实体
|
libxml_disable_entity_loader(true);
|
return json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
|
}
|
|
|
/**
|
* 输出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;
|
}
|
|
public function wechatTrans($pars, $app_id)
|
{
|
$url = 'https://api.mch.weixin.qq.com/v3/transfer/batches';
|
$http_method = 'POST';//请求方法(GET,POST,PUT)
|
$timestamp = time();//请求时间戳
|
$url_parts = parse_url($url);//获取请求的绝对URL
|
$nonce = $timestamp.rand('10000','99999');//请求随机串
|
$body = json_encode((object)$pars);//请求报文主体
|
$app = AppModel::detail($app_id);
|
|
$apiclient_cert_arr = openssl_x509_parse($app['cert_pem']);
|
$serial_no = $apiclient_cert_arr['serialNumberHex'];//证书序列号
|
$mch_private_key = $app['key_pem'];//密钥
|
$merchant_id = $app['mchid'];//商户id
|
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
|
$message = $http_method."\n".
|
$canonical_url."\n".
|
$timestamp."\n".
|
$nonce."\n".
|
$body."\n";
|
openssl_sign($message, $raw_sign, $mch_private_key, 'sha256WithRSAEncryption');
|
$sign = base64_encode($raw_sign);//签名
|
$schema = 'WECHATPAY2-SHA256-RSA2048';
|
$token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
|
$merchant_id, $nonce, $timestamp, $serial_no, $sign);//微信返回token
|
return $this->https_request(json_encode($pars), $token);;
|
}
|
|
|
public function https_request($data = null,$token){
|
$url = 'https://api.mch.weixin.qq.com/v3/transfer/batches';
|
$curl = curl_init();
|
curl_setopt($curl, CURLOPT_URL, (string)$url);
|
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, FALSE);
|
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, FALSE);
|
if (!empty($data)){
|
curl_setopt($curl, CURLOPT_POST, 1);
|
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
|
}
|
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
|
//添加请求头
|
$headers = [
|
'Authorization:WECHATPAY2-SHA256-RSA2048 '.$token,
|
'Accept: application/json',
|
'Content-Type: application/json; charset=utf-8',
|
'User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36',
|
];
|
if(!empty($headers)){
|
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
|
}
|
$output = curl_exec($curl);
|
curl_close($curl);
|
return $output;
|
}
|
}
|