test_service d63b889fd6 1
2023-12-05 16:00:42 +08:00

1181 lines
44 KiB
PHP
Executable File
Raw Permalink 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 hema\wechat;
use app\common\model\Setting;
use hema\wechat\AesUtil;
use think\facade\Cache;
use hema\Http;
use app\common\model\Applet;
use app\common\model\Record;
use app\common\model\DivideAccount;
use think\facade\Log;
/**
* 微信支付
*/
class Pay
{
private $api_url = 'https://api.mch.weixin.qq.com';//接口域名
private $version = 'v3'; //接口版本
private $isp_config; // 微信支付服务商参数
private $config; // 微信支付参数
private $error;
/**
* 构造方法
*/
public function __construct(array $config = [])
{
$this->isp_config = Setting::getItem('wxpayisp',0);
$this->config = Setting::getItem('wxpayisp',0);
$this->config['is_sub'] = 2;
}
/********** V3接口 **********/
/**
* H5下单API
* $out_trade_no=订单号, $total=支付金额,,$attach=订单描述
* $profit_sharing=是否分账(有配送费要分账时传递)
*/
public function h5($out_trade_no,$total,$notify_url,$attach='订单支付',$profit_sharing = false)
{
$params = [
'description' => $attach,//商品描述
'out_trade_no' => $out_trade_no,//商户订单号
'attach' => $attach,//附加数据
'notify_url' => base_url() . $notify_url, //通知地址
'amount' => [
'total' => intval($total * 100),//订单总金额,单位为分
],
'scene_info' => [
'payer_client_ip' => \request()->ip(),//用户终端IP
],
'h5_info' => [
'type' => 'Wap'
]
];
if($this->config['is_sub'] == 1){
//服务商
$params['sp_appid'] = $this->isp_config['app_id'];//服务商应用ID
$params['sp_mchid'] = $this->isp_config['mch_id'];//服务商商户号
$params['sub_appid'] = $this->config['app_id'];//子商户应用ID
$params['sub_mchid'] = $this->config['mch_id'];//子商户号
$url = $this->getUrl('pay/partner/transactions/h5');//服务商
$is_isp = true;
}else{
//直连商户
$params['appid'] = $this->config['app_id'];//小程序ID
$params['mchid'] = $this->config['mch_id'];//商户号
$url = $this->getUrl('pay/transactions/h5');//直连商户
$is_isp = false;
}
//判断是否开启分账
$divide = Setting::getItem('divide',0);
if($profit_sharing or $divide['extract'] > 0){
$params['settle_info']['profit_sharing'] = true; //开启分账
}
$params = hema_json($params);
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',$params,$is_isp),
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:' . $is_isp?$this->isp_config['mch_id']:$this->config['mch_id'],
];
$result = json_decode(Http::post($url, $params,[],$headers),true);
if(isset($result['code'])){
$this->error = 'code' . $result['code'] . 'msg' . $result['message'];
return false;
}
return $result['h5_url'];
}
//扣取手续费 按照0.6%计算
private function serviceFee($fee)
{
return intval(($fee - ($fee * 6 / 1000)) * 100);
}
/**
* 分账
* $data数组 =分账数据
* [
* out_order_no第三方订单号
* transaction_id:微信订单号
* total分账总金额
* ]
* $applet_id=小程序编号
* $delivery_fee=配送费分账金额
*/
public function divide($data,$applet_id='',$delivery_fee=0)
{
$is_divide = false;//是否分账
$total = $data['total'];//分账总金额
$service_fee = 0;//平台分账金额(单位分)
$agent_fee = 0;//代理分账金额(单位分)
$agent_openid = '';//代理收款账号
//判断外卖订单是否分账配送费
if($delivery_fee > 0){
$total = $total - $delivery_fee;
$delivery_fee = $this->serviceFee($delivery_fee);//去掉手续费
$is_divide = true; //配送费大于0 开启分账
}
$divide = Setting::getItem('divide',0); //分佣参数
$applet = Applet::get($applet_id);//获取商家应用
//如果开启分佣
if($divide['extract'] > 0){
$extract = $total * $divide['extract'] / 100;//抽取金额
$service_fee = $this->serviceFee($extract);//去掉手续费
if($divide['agent_extract'] > 0){
//判断商家是否有代理商
if($applet['agent_id'] > 0){
if($account = DivideAccount::withoutGlobalScope()->where('applet_id',$applet_id)->find()){
if(!empty($account['open_id'])){
$agent_openid = $account['open_id'];
$agent_fee = $this->serviceFee($extract * $divide['agent_extract'] / 100);//去掉手续费
$service_fee = $service_fee - $agent_fee;
}
}
}
}
$is_divide = true; //开启分账
}
//判断是否要进行分账
if(!$is_divide){
$this->error = '不用分账';
return false;
}
//***************** 添加分账接收方 *********************//
$receivers = [];//收款方账号列表
//添加平台收佣账号
if(($service_fee + $delivery_fee) > 0){
if(!$this->addReceivers($applet_id)) {
$this->error = '添加平台分账接收方失败';
return false;
}
$webpay = Setting::getItem('webpay',0)['wx']; //平台微信支付参数
$receivers[] = [
'type' => 'MERCHANT_ID',//分账接收方类型 MERCHANT_ID=商户号 PERSONAL_OPENID=个人openid
'account' => $webpay['mch_id'],//分账接收方账号
'amount' => $service_fee + $delivery_fee,//分账金额
'description' => '分佣给平台',//分账描述
];
}
//添加代理收佣账号
if($agent_fee > 0 and !empty($agent_openid)){
if(!$this->addReceivers($applet_id,false,$agent_openid)) {
$this->error = '添加代理分账接收方失败';
return false;
}
$receivers[] = [
'type' => $this->config['is_sub'] == 1 ? 'PERSONAL_SUB_OPENID':'PERSONAL_OPENID',//分账接收方类型
'account' => $agent_openid,//分账接收方账号
'amount' => $agent_fee,//分账金额
'description' => '分佣给代理',//分账描述
];
}
//***************** 请求分账 *********************//
if(sizeof($receivers) == 0){
$this->error = '收款方账号列表为空';
return false;
}
if(!$this->profitSharing($data['transaction_id'],$data['out_order_no'],$receivers)) {
$this->error = '请求分账失败';
return false;
}
//***************** 添加交易记录 *********************//
$record_log = [];//交易流水记录
//是否增加平台分红记录(分佣)
if(($service_fee - $agent_fee) > 0){
$money = sprintf("%.2f",$service_fee / 100);//计算金额
//平台分红(分佣)记录
array_push($record_log,[
'mode' => 40, //赠送
'type' => 30, //微信
'order_no' => $data['out_order_no'],
'money' => $money,
'remark' => '交易分佣'
]);
//商户扣费记录
array_push($record_log,[
'mode' => 50, //扣减
'type' => 30, //微信
'order_no' => $data['out_order_no'],
'money' => $money,
'user_id' => $applet['user_id'],
'remark' => '交易服务费'
]);
}
//是否增加配送费记录
if($delivery_fee > 0){
$money = sprintf("%.2f",$delivery_fee / 100);//计算金额
//平台收取记录
array_push($record_log,[
'mode' => 40, //赠送
'type' => 30, //微信
'order_no' => $data['out_order_no'],
'money' => $money,
'remark' => '第三方配送费'
]);
//商户扣费记录
array_push($record_log,[
'mode' => 50, //扣减
'type' => 30, //微信
'order_no' => $data['out_order_no'],
'money' => $money,
'user_id' => $applet['user_id'],
'remark' => '第三方配送费'
]);
}
//是否增加代理分佣记录
if($agent_fee > 0){
$money = sprintf("%.2f",$agent_fee / 100);//计算金额
//平台收取记录
array_push($record_log,[
'mode' => 40, //赠送
'type' => 30, //微信
'order_no' => $data['out_order_no'],
'money' => $money,
'user_id' => $applet['agent_id'],
'remark' => '交易分佣'
]);
}
//批量增加交易记录
$model = new Record;
if(!$model->saveAll($record_log)){
$this->error = '添加交易记录失败';
return false;
}
return true;
}
/**
* 请求分账API
*/
private function profitSharing($transaction_id,$out_order_no,$receivers)
{
//服务商
$params = [
'transaction_id' => $transaction_id,//微信订单号
'out_order_no' => $out_order_no,//商户分账单号
'receivers' => $receivers,
'unfreeze_unsplit' => true,//是否解冻剩余未分资金
];
if($this->config['is_sub'] == 1){
//服务商
$params['appid'] = $this->isp_config['app_id'];//服务商应用ID
$params['sub_appid'] = $this->config['app_id'];//子商户应用ID
$params['sub_mchid'] = $this->config['mch_id'];//子商户号
$is_isp = true;
}else{
//直连商户
$params['appid'] = $this->config['app_id'];//小程序ID
$is_isp = false;
}
$params = hema_json($params);
$url = $this->getUrl('profitsharing/orders');
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',$params,$is_isp),
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:' . $is_isp?$this->isp_config['mch_id']:$this->config['mch_id'],
'Wechatpay-Serial:' . $this->isp_config['serial_no'],
];
return $this->result(json_decode(Http::post($url, $params,[],$headers),true));
}
/**
* 添加分账接收方API
* $is_mchid = 接收方是否是商户 $account = 接收账号
*/
private function addReceivers($applet_id,$is_mchid=true,$account='')
{
if($this->config['is_sub'] == 1){
//服务商
$params['appid'] = $this->isp_config['app_id'];//服务商应用ID
$params['sub_appid'] = $this->config['app_id'];//子商户应用ID
$params['sub_mchid'] = $this->config['mch_id'];//子商户号
if($is_mchid){
$webpay = Setting::getItem('webpay',0)['wx'];
$params['type'] = 'MERCHANT_ID';//分账接收方类型
$params['account'] = $webpay['mch_id'];//分账接收方账号
$params['name'] = $this->getEncrypt($webpay['name']); //(加密)分账个人接收方姓名 分账接收方类型是MERCHANT_ID时是商户全称必传
}else{
$params['type'] = 'PERSONAL_SUB_OPENID';//分账接收方类型
$params['account'] = $account;//分账接收方账号
}
$params['relation_type'] = 'SERVICE_PROVIDER'; //与分账方的关系类型 服务商
$is_isp = true;
$serial_no = $this->isp_config['serial_no'];
}else{
//直连商户
$params['appid'] = $this->config['app_id'];//小程序ID
if($is_mchid){
$webpay = Setting::getItem('webpay',0)['wx'];
$params['type'] = 'MERCHANT_ID';//分账接收方类型
$params['account'] = $webpay['mch_id'];//分账接收方账号
$params['name'] = $this->getEncrypt($webpay['name'],false,$applet_id); //(加密)分账个人接收方姓名 分账接收方类型是MERCHANT_ID时是商户全称必传
}else{
$params['type'] = 'PERSONAL_OPENID';//分账接收方类型
$params['account'] = $account;//分账接收方账号
}
$params['relation_type'] = 'PARTNER'; ////与分账方的关系类型 合作伙伴
$is_isp = false;
$serial_no = $this->config['serial_no'];
}
$params = hema_json($params);
$url = $this->getUrl('profitsharing/receivers/add');
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',$params,$is_isp),
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:' . $is_isp?$this->isp_config['mch_id']:$this->config['mch_id'],
'Wechatpay-Serial:' . $serial_no,
];
return $this->result(json_decode(Http::post($url, $params,[],$headers),true));
}
/**
* 申请退款API
*/
public function refunds($transaction_id,$out_refund_no,$refund_fee,$total_fee,$notify_url='',$reason='')
{
$params = [
'transaction_id' => $transaction_id,//微信支付订单号
'out_refund_no' => $out_refund_no,//退款订单号
'amount' => [
'refund' => intval($refund_fee * 100), // 退款金额,价格:单位分
'total' => intval($total_fee * 100), // 订单金额,价格:单位分
'currency' => 'CNY', //退款币种 只支持人民币CNY
],
];
if($this->config['is_sub'] == 1){
//服务商
$params['sub_mchid'] = $this->config['mch_id'];//子商户号
$is_isp = true;
}else{
$is_isp = false;
}
!empty($reason) && $params['reason'] = $reason;//退款原因
!empty($notify_url) && $params['notify_url'] = base_url() . $notify_url; // 异步通知地址
$params = hema_json($params);
$url = $this->getUrl('refund/domestic/refunds');
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',$params,$is_isp),
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:' . $is_isp?$this->isp_config['mch_id']:$this->config['mch_id'],
];
return $this->result(json_decode(Http::post($url, $params,[],$headers),true));
}
/**
* 退款成功异步通知
*/
public function refundsNotify($Model,$applet_id='')
{
//接收微信服务器回调的数据流
if (!$json = file_get_contents('php://input')) {
$this->returnHttpCode(false);
}
// 将服务器返回的json数据转化为数组
$result = json_decode($json,true);
if(empty($applet_id)){
$this->config = Setting::getItem('webpay',0)['wx'];//平台商户支付参数
}else{
$this->config = Setting::getItem('wxpay',$applet_id);//商家商户支付参数
}
if($this->config['is_sub'] == 1){
//服务商
$api_key = $this->isp_config['api_key'];
//判断平台证书是否过期
if($this->isp_config['expire_time'] < time()){
//更新平台证书
if(!$this->certificates()){
$this->returnHttpCode(false,$this->error);//更新失败
}
}
}else{
//直连商户
$api_key = $this->config['api_key'];
//判断平台证书是否过期
if($this->config['expire_time'] < time()){
//更新平台证书
if(!$this->certificates(false,$applet_id)){
$this->returnHttpCode(false,$this->error);//更新失败
}
}
}
if(!$decrypt = new AesUtil($api_key)){
$this->returnHttpCode(false,$decrypt->getError());
}
if(!$res = $decrypt->decryptToString($result['resource']['associated_data'], $result['resource']['nonce'], $result['resource']['ciphertext'])){
$this->returnHttpCode(false,$decrypt->getError());
}
$data = json_decode($res,true);
// 订单信息
if(!$order = $Model->refundDetail($data['out_refund_no'])){
$this->returnHttpCode(false,'订单不存在');
}
if($data['refund_status'] == 'SUCCESS') {
// 更新订单状态
$order->updateRefundStatus($data['refund_id']);
$this->returnHttpCode(true);// 返回状态
}
$this->returnHttpCode(false, '退款失败');
}
/**
* Native下单API
* $out_trade_no=订单号, $total=支付金额,$attach=订单描述 $profit_sharing=是否分账
*/
public function native($out_trade_no,$total,$notify_url,$attach='订单支付',$profit_sharing = false)
{
$params = [
'description' => $attach,//商品描述
'out_trade_no' => $out_trade_no,//商户订单号
'attach' => $attach,//附加数据
'notify_url' => base_url() . $notify_url, //通知地址
'amount' => [
'total' => intval($total * 100),//订单总金额,单位为分
],
'scene_info' => [
'payer_client_ip' => \request()->ip(),//用户终端IP
],
];
if($this->config['is_sub'] == 1){
//服务商
$params['sp_appid'] = $this->isp_config['app_id'];//服务商应用ID
$params['sp_mchid'] = $this->isp_config['mch_id'];//服务商商户号
//$params['sub_appid'] = $this->config['app_id'];//子商户应用ID
$params['sub_mchid'] = $this->config['mch_id'];//子商户号
//$params['payer']['sub_openid'] = $openid; //子用户标识Openid
$url = $this->getUrl('pay/partner/transactions/native');//服务商
$is_isp = true;
}else{
//直连商户
$params['appid'] = $this->config['app_id'];//小程序ID
$params['mchid'] = $this->config['mch_id'];//商户号
$url = $this->getUrl('pay/transactions/native');//直连商户
$is_isp = false;
}
//判断是否开启分账
$divide = Setting::getItem('divide',0);
if($profit_sharing or $divide['extract'] > 0){
$params['settle_info']['profit_sharing'] = true; //开启分账
}
$params = hema_json($params);
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',$params,$is_isp),
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:' . $is_isp?$this->isp_config['mch_id']:$this->config['mch_id'],
];
$result = json_decode(Http::post($url, $params,[],$headers),true);
if(isset($result['code'])){
$this->error = 'code' . $result['code'] . 'msg' . $result['message'];
return false;
}
if(!isset($result['code_url'])){
$this->error = 'Native下单接口请求失败';
return false;
}
return $result['code_url'];
}
/**
* JSAPI下单API
* $out_trade_no=订单号, $total=支付金额,$openid=微信用户ID, ,$attach=订单描述
* $profit_sharing=是否分账(有配送费要分账时传递)
*/
public function jsapi($out_trade_no,$total,$openid,$notify_url,$attach='订单支付',$profit_sharing = false)
{
$params = [
'description' => $attach,//商品描述
'attach' => $attach,//附加数据
'out_trade_no' => $out_trade_no,//商户订单号
'notify_url' => base_url() . $notify_url, //通知地址
'amount' => [
'total' => intval($total * 100),//订单总金额,单位为分
],
'scene_info' => [
'payer_client_ip' => \request()->ip(),//用户终端IP
],
];
if($this->config['is_sub'] == 1){
//服务商
$params['sp_appid'] = $this->isp_config['app_id'];//服务商应用ID
$params['sp_mchid'] = $this->isp_config['mch_id'];//服务商商户号
$params['sub_appid'] = $this->config['app_id'];//子商户应用ID
$params['sub_mchid'] = $this->config['mch_id'];//子商户号
$params['payer']['sub_openid'] = $openid; //子用户标识Openid
$url = $this->getUrl('pay/partner/transactions/jsapi');//服务商
$is_isp = true;
}else{
//直连商户
$params['appid'] = $this->config['app_id'];//小程序ID
$params['mchid'] = $this->config['mch_id'];//商户号
$params['payer']['openid'] = $openid; //用户标识Openid
$url = $this->getUrl('pay/transactions/jsapi');//直连商户
$is_isp = false;
}
//判断是否开启分账
$divide = Setting::getItem('divide',0);
if($profit_sharing or $divide['extract'] > 0){
$params['settle_info']['profit_sharing'] = true; //开启分账
}
// $params = hema_json($params);
$params = json_encode($params);
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',$params,$is_isp),
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:' . $is_isp?$this->isp_config['mch_id']:$this->config['mch_id'],
];
$result = json_decode(Http::post($url, $params,[],$headers),true);
if(isset($result['code'])){
$this->error = 'code' . $result['code'] . 'msg' . $result['message'];
return false;
}
if(!isset($result['prepay_id'])){
$this->error = 'JSAPI下单接口请求失败';
return false;
}
$data = [
'timeStamp' => (string)time(),
'nonceStr' => $this->nonce(),
'package' => 'prepay_id=' . $result['prepay_id'],
'signType' => 'RSA',
];
$data['paySign'] = $this->paySign($data);
return $data;
}
/**
* 支付成功异步通知
*/
public function notify($Model,$applet_id,$method='edit')
{
Log::write('机333','notice');
//接收微信服务器回调的数据流
if (!$json = file_get_contents('php://input')) {
$this->returnHttpCode(false);
}
// 将服务器返回的json数据转化为数组
$result = json_decode($json,true);
Log::write($json,'notice');
if(empty($applet_id)){
$this->config = Setting::getItem('webpay',0)['wx'];//平台商户支付参数
}else{
$this->config = Setting::getItem('wxpayisp',0);
}
$this->config['is_sub']=2;
if($this->config['is_sub'] == 1){
//服务商
$api_key = $this->isp_config['api_key'];
//判断平台证书是否过期
if($this->isp_config['expire_time'] < time()){
//更新平台证书
if(!$this->certificates()){
$this->returnHttpCode(false,$this->error);//更新失败
}
}
}else{
//直连商户
$api_key = $this->config['api_key'];
//判断平台证书是否过期
if($this->config['expire_time'] < time()){
//更新平台证书
if(!$this->certificates(false,$applet_id)){
$this->returnHttpCode(false,$this->error);//更新失败
}
}
}
if(!$decrypt = new AesUtil($api_key)){
$this->returnHttpCode(false,$decrypt->getError());
}
if(!$res = $decrypt->decryptToString($result['resource']['associated_data'], $result['resource']['nonce'], $result['resource']['ciphertext'])){
$this->returnHttpCode(false,$decrypt->getError());
}
$data = json_decode($res,true);
// 订单信息
if(!$order = $Model->payDetail($data['out_trade_no'])){
$this->returnHttpCode(false,'订单不存在');
}
//判断支付状态
if($data['trade_state'] == 'SUCCESS') {
if($method == 'add'){
$Model->updatePayStatus($data['transaction_id'],$order);
Cache::delete($data['out_trade_no']);
}else{
// 更新订单状态
$order->updatePayStatus($data['transaction_id']);
}
// 返回状态
$this->returnHttpCode(true);
}
// 返回状态
$this->returnHttpCode(false, '支付失败');
}
/**
* 特约商户进件
* 频率限制15/s
*/
public function applyment($params)
{
//********************* 数据加密 *****************
//管理员姓名
if(!$result = $this->getEncrypt($params['contact_info']['contact_name'])){
return false;
}
$params['contact_info']['contact_name'] = $result;
//管理员电话
if(!$result = $this->getEncrypt($params['contact_info']['mobile_phone'])){
return false;
}
$params['contact_info']['mobile_phone'] = $result;
//管理员邮箱
if(!$result = $this->getEncrypt($params['contact_info']['contact_email'])){
return false;
}
$params['contact_info']['contact_email'] = $result;
//身份证姓名
if(!$result = $this->getEncrypt($params['subject_info']['identity_info']['id_card_info']['id_card_name'])){
return false;
}
$params['subject_info']['identity_info']['id_card_info']['id_card_name'] = $result;
//身份证号
if(!$result = $this->getEncrypt($params['subject_info']['identity_info']['id_card_info']['id_card_number'])){
return false;
}
$params['subject_info']['identity_info']['id_card_info']['id_card_number'] = $result;
//身份证居住地址
if(!$result = $this->getEncrypt($params['subject_info']['identity_info']['id_card_info']['id_card_address'])){
return false;
}
$params['subject_info']['identity_info']['id_card_info']['id_card_address'] = $result;
//银行开户名称
if(!$result = $this->getEncrypt($params['bank_account_info']['account_name'])){
return false;
}
$params['bank_account_info']['account_name'] = $result;
//银行账号
if(!$result = $this->getEncrypt($params['bank_account_info']['account_number'])){
return false;
}
$params['bank_account_info']['account_number'] = $result;
//********************* 上传图片 *****************
//营业执照
if(!$result = $this->upload($params['subject_info']['business_license_info']['license_copy'])){
return false;
}
$params['subject_info']['business_license_info']['license_copy'] = $result;
//身份证正面
if(!$result = $this->upload($params['subject_info']['identity_info']['id_card_info']['id_card_copy'])){
return false;
}
$params['subject_info']['identity_info']['id_card_info']['id_card_copy'] = $result;
//身份证反面
if(!$result = $this->upload($params['subject_info']['identity_info']['id_card_info']['id_card_national'])){
return false;
}
$params['subject_info']['identity_info']['id_card_info']['id_card_national'] = $result;
//特殊资质
if(!$result = $this->upload($params['settlement_info']['qualifications'][0])){
return false;
}
$params['settlement_info']['qualifications'][0] = $result;
//门头照片
if(!$result = $this->upload($params['business_info']['sales_info']['biz_store_info']['store_entrance_pic'][0])){
return false;
}
$params['business_info']['sales_info']['biz_store_info']['store_entrance_pic'][0] = $result;
//店内照片
if(!$result = $this->upload($params['business_info']['sales_info']['biz_store_info']['indoor_pic'][0])){
return false;
}
$params['business_info']['sales_info']['biz_store_info']['indoor_pic'][0] = $result;
$params = hema_json($params);
$url = $this->getUrl('applyment4sub/applyment/');
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',$params),
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:' . $this->isp_config['mch_id'],
'Wechatpay-Serial:' . $this->isp_config['serial_no'],
];
$result = json_decode(Http::post($url, $params,[],$headers),true);
if(isset($result['code'])){
$this->error = 'code' . $result['code'] . 'msg' . $result['message'];
return false;
}
return $result['applyment_id'];
}
/**
* 查询申请单状态
*/
public function queryApplyment($no,$is_applyment_id = false)
{
if($is_applyment_id){
$path = 'applyment_id/' . $no;//通过申请单号查询申请状态(官方返回的编号)
}else{
$path = 'business_code/' . $no;//通过业务申请编号查询申请状态(第三方自定义的编号)
}
$url = $this->getUrl('applyment4sub/applyment/'.$path);
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'GET'),
'Accept:application/json',
];
return $this->result(json_decode(Http::get($url, [],[],$headers),true));
}
/**
* 获取平台证书列表
*/
private function certificates($is_isp=true,$applet_id='')
{
$url = $this->getUrl('certificates');
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'GET','',$is_isp),
'Accept:application/json',
'User-Agent:https://zh.wikipedia.org/wiki/User_agent',
];
$result = json_decode(Http::get($url,[],[],$headers),true);
if(isset($result['code'])){
$this->error = 'code' . $result['code'] . 'msg' . $result['message'];
return false;
}
//验证是否获取到了数据
if(!isset($result['data']) and sizeof($result['data']) == 0){
$this->error = '未获取到可用的平台证书';
return false;
}
$result = $result['data'][0];//获取证书列表中的第一个数据
if($is_isp){
$api_key = $this->isp_config['api_key'];
}else{
$api_key = $this->config['api_key'];
}
if(!$decrypt = new AesUtil($api_key)){
$this->error = $decrypt->getError();
return false;
}
if(!$res = $decrypt->decryptToString($result['encrypt_certificate']['associated_data'], $result['encrypt_certificate']['nonce'], $result['encrypt_certificate']['ciphertext'])){
$this->error = $decrypt->getError();
return false;
}
//计算到期时间
$expire_time = explode('T',$result['expire_time']);
$expire_time = strtotime($expire_time[0]);
$model = new Setting;
//更新平台证书
if($is_isp){
$this->isp_config['certificates'] = $res;
$this->isp_config['serial_no'] = $result['serial_no'];
$this->isp_config['expire_time'] = $expire_time;
$model->edit('wxpayisp',$this->isp_config,0); //保存到数据库
return true;
}
//更新特约商户 平台证书
$this->config['certificates'] = $res;
$this->config['serial_no'] = $result['serial_no'];
$this->config['expire_time'] = $expire_time;
if(empty($applet_id)){
$config = Setting::getItem('webpay',0);
$config['wx']['certificates'] = $res;
$config['wx']['serial_no'] = $result['serial_no'];
$config['wx']['expire_time'] = $expire_time;
$model->edit('webpay',$config,0); //保存到数据库
}else{
$model->edit('wxpay',$this->config,$applet_id); //保存到数据库
}
return true;
}
/**
* 图片上传API
*/
private function upload($file_path)
{
$file = file_get_contents($file_path);//获取网络图片
//获取文件名称
$arr = explode('/',$file_path);
$filename = $arr[sizeof($arr)-1];
$meta =[
'filename' => $filename,
'sha256' => hash('sha256',$file),
];
$url = $this->getUrl('merchant/media/upload');
$boundary = uniqid();//随机数
$headers = [
'Authorization:WECHATPAY2-SHA256-RSA2048 ' . $this->sign($url,'POST',hema_json($meta)),
'Accept:application/json',
'Content-Type:multipart/form-data;boundary=' . $boundary,
];
$params = '--' . $boundary . "\r\n";
$params .= 'Content-Disposition:form-data; name="meta"' . "\r\n";
$params .= 'Content-Type:application/json' . "\r\n\r\n";
$params .= hema_json($meta) . "\r\n";
$params .= '--' . $boundary . "\r\n";
$params .= 'Content-Disposition:form-data;name="file";filename="' . $meta['filename'] . '"' . "\r\n";
$params .= 'Content-Type:image/jpg' . "\r\n\r\n";
$params .= $file . "\r\n";
$params .= '--' . $boundary . '--' . "\r\n";
$result = json_decode(Http::post($url, $params,[],$headers),true);
if(isset($result['code'])){
$this->error = 'code' . $result['code'] . 'msg' . $result['message'];
return false;
}
if(isset($result['media_id'])){
return $result['media_id'];
}
$this->error = '图片上传失败';
return false;
}
/**
* 调起支付签名
*/
private function paySign($data)
{
$params = $this->config['app_id'] . "\n" .
$data['timeStamp'] . "\n" .
$data['nonceStr'] . "\n" .
$data['package'] . "\n";
/*if($this->config['is_sub'] == 1){
//服务商
}else{
//直连商户
$private_key = $this->config['key_pem']; //API私有证书
}*/
$private_key = $this->isp_config['key_pem']; //API私有证书
$raw_sign = '';
openssl_sign($params, $raw_sign, $private_key, 'sha256WithRSAEncryption');
return base64_encode($raw_sign);
}
/**
* 生成签名
* $http_method = HTTP请求的方法GET,POST,PUT
* serial_no 为你的商户证书序列号
* $mch_private_key = 是商户API私钥在商户平台下载的证书文件包含该文件名称为apiclient_key.pem
* $is_isp 是否为服务商操作
*/
private function sign($url,$http_method,$body='',$is_isp=true)
{
$timestamp = time(); //时间戳
$nonce = $this->nonce(); //随机字符串
$url_parts = parse_url($url);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
$params = $http_method . "\n" .
$canonical_url . "\n" .
$timestamp . "\n" .
$nonce . "\n" .
$body . "\n";
if($is_isp){
$mchid = $this->isp_config['mch_id']; //商户号
$serial_no = $this->isp_config['api_serial_no']; //API证书序列号
$mch_private_key = $this->isp_config['key_pem']; //API私有证书
}else{
$mchid = $this->config['mch_id']; //商户号
$serial_no = $this->config['api_serial_no']; //API证书序列号
$mch_private_key = $this->config['key_pem']; //API私有证书
}
$raw_sign = '';
openssl_sign($params, $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"',$mchid, $nonce, $timestamp, $serial_no, $sign);
return $token;
}
/**
* 敏感信息加密
*/
private function getEncrypt($str,$is_isp = true,$applet_id='')
{
//判断平台证书是否过期
if($is_isp){
//服务商
if($this->isp_config['expire_time'] < time()){
//更新平台证书
if(!$this->certificates()){
return false;//更新失败
}
}
$public_key = $this->isp_config['certificates'];//平台证书
}else{
//直连商户
if($this->config['expire_time'] < time()){
//更新平台证书
if(!$this->certificates(false,$applet_id)){
return false;//更新失败
}
}
$public_key = $this->config['certificates'];//平台证书
}
$encrypted = '';
if (!openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)) {
$this->error = '敏感信息加密失败';
return false;
}
return base64_encode($encrypted);//base64编码
}
/**
* 生成随机字符串
*/
private function nonce()
{
return md5(uniqid());
}
/*
* 拼接请求域名接口
*/
private function getUrl($url)
{
return $this->api_url . '/' . $this->version . '/' . $url;
}
/**
* 获取Headers数据
*/
private function getHeaders()
{
$headers = array();
foreach ($_SERVER as $key => $value) {
if (substr($key, 0, 5) === 'HTTP_') {
$key = substr($key, 5);
$key = str_replace('_', ' ', $key);
$key = str_replace(' ', '-', $key);
$key = strtolower($key);
$headers[$key] = $value;
}
}
return $headers;
}
/**
* 返回状态给微信服务器
*/
private function returnHttpCode($is_success = true, $msg = '失败')
{
$json = hema_json([
'code' => $is_success ? 'SUCCESS' : 'FAIL',
'message' => $is_success ? '成功' : $msg,
]);
if($is_success){
header('HTTP/1.1 200 OK');
}else{
header('HTTP/1.1 404 Not Found');
}
die($json);
}
/**
* 请求数据验证
**/
private function result($result)
{
if(isset($result['code'])){
$this->error = 'code' . $result['code'] . 'msg' . $result['message'];
return false;
}
return $result;
}
public function getError()
{
return $this->error;
}
/********** V2接口 **********/
/**
* 付款码支付
* $auth_code=付款码 $order_no=订单号 $openid=微信用户ID, $total_fee=支付金额, ,$attach=订单描述 $divide=是否分账
*/
public function micropay($auth_code,$order_no, $total_fee,$profit_sharing = false,$attach = '订单支付')
{
// 当前时间
$time = time();
// 生成随机字符串
$nonceStr = md5($time);
// API参数
$params = [
'auth_code' => $auth_code,//付款码支付
'attach' => $attach,
'nonce_str' => $nonceStr,//随机字符串
'body' => $attach,//商品描述
'out_trade_no' => $order_no,//商户订单号
'total_fee' => intval($total_fee * 100), // 价格:单位分
'spbill_create_ip' => \request()->ip(),//服务终端IP
];
if($this->config['is_sub'] == 1){
//服务商统一下单
$values = Setting::getItem('wxpayisp',0);
$this->config['api_key'] = $this->isp_config['api_key'];//服务商商户的密钥
$params['appid'] = $this->isp_config['app_id'];//服务商商户的APPID
$params['mch_id'] = $this->isp_config['mch_id'];//服务商商户号
$params['sub_appid'] = $this->config['app_id'];//当前调起支付的小程序APPID
$params['sub_mch_id'] = $this->config['mch_id'];//服务商分配的子商户号
}else{
$params['appid'] = $this->config['app_id'];//小程序ID
$params['mch_id'] = $this->config['mch_id'];//商户号
}
//判断是否开启分账
$divide = Setting::getItem('divide',0);
if($profit_sharing or $divide['extract'] > 0){
$params['profit_sharing'] = 'Y';//开启分账
}
// 生成签名
$params['sign'] = $this->makeSign($params);
$url = 'https://api.mch.weixin.qq.com/pay/micropay';// 请求API
$result = $this->postXmlCurl($this->toXml($params), $url);
$prepay = $this->fromXml($result);
// 请求失败
if ($prepay['return_code'] === 'FAIL') {
die(hema_json(['code' => -10, 'msg' => $prepay['return_msg']]));
}
//判断付款码支付时,用户支付中,需要输入密码
if ($prepay['result_code'] === 'USERPAYING') {
return false;
}
if ($prepay['result_code'] === 'FAIL') {
die(hema_json(['code' => -10, 'msg' => $prepay['err_code_des']]));
}
return $prepay['transaction_id'];//支付交易号
}
/**
* 查询付款码支付结果是否成功
*/
public function orderquery($out_trade_no)
{
// 当前时间
$time = time();
// 生成随机字符串
$nonceStr = md5($time);
// API参数
$params = [
'out_trade_no' => $out_trade_no,
'nonce_str' => $nonceStr,//随机字符串
];
if($this->config['is_sub'] == 1){
//服务商统一下单
$this->config['api_key'] = $this->isp_config['api_key'];//服务商商户的密钥
$params['appid'] = $this->isp_config['app_id'];//服务商商户的APPID
$params['mch_id'] = $this->isp_config['mch_id'];//服务商商户号
$params['sub_appid'] = $this->config['app_id'];//当前调起支付的小程序APPID
$params['sub_mch_id'] = $this->config['mch_id'];//服务商分配的子商户号
}else{
$params['appid'] = $this->config['app_id'];//小程序ID
$params['mch_id'] = $this->config['mch_id'];//商户号
}
// 生成签名
$params['sign'] = $this->makeSign($params);
// 请求API
$url = 'https://api.mch.weixin.qq.com/pay/orderquery';
$result = $this->postXmlCurl($this->toXml($params), $url);
$prepay = $this->fromXml($result);
// 请求失败
if ($prepay['return_code'] === 'SUCCESS' AND $prepay['result_code'] === 'SUCCESS') {
return $prepay['trade_state'];
}
return 'ERROR';
}
/**
* 输出xml字符
*/
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;
}
/**
* 将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);
}
/**
* 以post方式提交xml到对应的接口url
*/
private function postXmlCurl($xml, $url, $cert = false, $second = 30)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_TIMEOUT, $second);// 设置超时时间
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);//https请求 不验证证书和host
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);//严格校验
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);// 要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_POST, TRUE);// post提交方式
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
curl_setopt($ch, CURLOPT_HEADER, FALSE);// 是否返回请求头
//判断是否使用证书
if($cert){
$path = root_path() . '/extend/hema/wechat/cert/';
file_put_contents($path . 'apiclient_cert.pem',$this->config['cert_pem']);
file_put_contents($path . 'apiclient_key.pem',$this->config['key_pem']);
curl_setopt($ch,CURLOPT_SSLCERTTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLCERT,$path . 'apiclient_cert.pem');
curl_setopt($ch,CURLOPT_SSLKEYTYPE,'PEM');
curl_setopt($ch,CURLOPT_SSLKEY,$path . 'apiclient_key.pem');
}
$data = curl_exec($ch);// 运行curl
curl_close($ch);
return $data;
}
/**
* 生成签名MD5
*/
private function makeSign($values)
{
//签名步骤一:按字典序排序参数
ksort($values);
$string = $this->toUrlParams($values);
//签名步骤二在string后加入KEY
$string = $string . '&key=' . $this->config['api_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, '&');
}
}