isp_config = Setting::getItem('wxpayisp',0); $this->config = $config; } /********** 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); $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') { //接收微信服务器回调的数据流 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->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){ //服务商 $private_key = $this->isp_config['key_pem']; //API私有证书 }else{ //直连商户 $private_key = $this->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 = ""; foreach ($values as $key => $val) { if (is_numeric($val)) { $xml .= "<" . $key . ">" . $val . ""; } else { $xml .= "<" . $key . ">"; } } $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, '&'); } }