pay/includes/lib/Payment.php
2025-11-28 10:08:12 +08:00

553 lines
24 KiB
PHP

<?php
namespace lib;
use Exception;
class Payment {
//生成待签名字符串
static private function getSignContent($data){
ksort($data);
$signStr = '';
foreach ($data as $k => $v) {
if(is_array($v) || isEmpty($v) || $k == 'sign' || $k == 'sign_type') continue;
$signStr .= $k . '=' . $v . '&';
}
$signStr = substr($signStr, 0, -1);
return $signStr;
}
//生成签名
static public function makeSign($data, $md5key) {
$sign_type = $data['sign_type'] ? $data['sign_type'] : 'MD5';
$signStr = self::getSignContent($data);
if($sign_type == 'RSA'){
global $conf;
$private_key = base64ToPem($conf['private_key'], 'PRIVATE KEY');
$pkey = openssl_pkey_get_private($private_key);
if(!$pkey) return false;
openssl_sign($signStr, $sign, $pkey, OPENSSL_ALGO_SHA256);
return base64_encode($sign);
}else{
$sign = md5($signStr . $md5key);
return $sign;
}
}
//验证签名
static public function verifySign($data, $md5key, $publicKey) {
if(!isset($data['sign'])) throw new Exception('缺少签名参数');
$sign_type = $data['sign_type'] ? $data['sign_type'] : 'MD5';
if($sign_type == 'RSA'){
$public_key = base64ToPem($publicKey, 'PUBLIC KEY');
$pkey = openssl_pkey_get_public($public_key);
if(!$pkey) throw new Exception('签名校验失败,商户公钥错误');
$signStr = self::getSignContent($data);
$result = openssl_verify($signStr, base64_decode($data['sign']), $pkey, OPENSSL_ALGO_SHA256);
return $result === 1;
}else{
$sign = self::makeSign($data, $md5key);
return $sign === $data['sign'];
}
}
// 页面支付返回信息
static public function echoDefault($result){
global $cdnpublic,$order,$conf,$sitename,$ordername,$siteurl;
$type = $result['type'];
if(!$type) return false;
switch($type){
case 'jump': //跳转
$html_text = '<script>window.location.replace(\''.$result['url'].'\');</script>';
if(isset($result['submit']) && $result['submit']){
submitTemplate($html_text);
}else{
echo $html_text;
}
break;
case 'html': //显示html
$html_text = $result['data'];
if(isset($result['submit']) && $result['submit'] && substr($html_text, 0, 6) == '<form '){
submitTemplate($html_text);
}else{
echo $html_text;
}
break;
case 'json': //显示JSON
echo json_encode($result['data']);
break;
case 'page': //显示指定页面
include_once SYSTEM_ROOT.'txprotect.php';
if(isset($result['data'])) extract($result['data']);
if($conf['pageordername']==1)$order['name']=$ordername?$ordername:'onlinepay';
include PAYPAGE_ROOT.$result['page'].'.php';
break;
case 'qrcode': //扫码页面
if($result['page'] == 'alipay_qrcode' && !empty($conf['alipay_qrcode_url'])){
if(strpos($result['url'], $siteurl)===0){
$result['url'] = $conf['alipay_qrcode_url'].substr($result['url'], strlen($siteurl));
}elseif(!empty($conf['localurl_alipay']) && strpos($result['url'], $conf['localurl_alipay'])===0){
$result['url'] = $conf['alipay_qrcode_url'].substr($result['url'], strlen($conf['localurl_alipay']));
}
}
case 'scheme': //跳转urlscheme页面
if($result['page'] == 'wxpay_mini') $result['page'] = 'wxpay_h5';
include_once SYSTEM_ROOT.'txprotect.php';
$code_url = $result['url'];
if($conf['pageordername']==1)$order['name']=$ordername?$ordername:'onlinepay';
if($conf['wework_payopen'] == 1 && ($result['page'] == 'wxpay_wap' && strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger')===false || $result['page'] == 'wxpay_qrcode' && checkmobile())){
$code_url_wxkf = self::getWxkfPayUrl($code_url);
if($code_url_wxkf){
$code_url = $code_url_wxkf;
include PAYPAGE_ROOT.'wxpay_h5.php';
break;
}
}
include PAYPAGE_ROOT.$result['page'].'.php';
break;
case 'return': //同步回调
returnTemplate($result['url']);
break;
case 'error': //错误提示
sysmsg($result['msg']);
break;
default:break;
}
}
// API支付返回信息
static public function echoJson($result){
global $order,$siteurl;
if(!$result) return false;
$type = $result['type'];
if(!$type) return false;
if(defined('API_INIT')){
$json = ['code'=>0, 'trade_no'=>TRADE_NO];
switch($type){
case 'jump': //跳转URL
$json['pay_type'] = 'jump';
$json['pay_info'] = $result['url'];
break;
case 'html': //显示html跳转
$json['pay_type'] = 'html';
$json['pay_info'] = $result['data'];
break;
case 'qrcode': //扫码支付
$json['pay_type'] = 'qrcode';
$json['pay_info'] = $result['url'];
break;
case 'scheme': //小程序H5跳转
$json['pay_type'] = 'urlscheme';
$json['pay_info'] = $result['url'];
break;
case 'jsapi': //JSAPI支付
$json['pay_type'] = 'jsapi';
$json['pay_info'] = $result['data'];
break;
case 'app': //APP支付
$json['pay_type'] = 'app';
$json['pay_info'] = $result['data'];
break;
case 'scan': //付款码支付
$json['pay_type'] = 'scan';
$json['pay_info'] = $result['data'];
break;
case 'wxplugin': //微信小程序插件支付
$json['pay_type'] = 'wxplugin';
$json['pay_info'] = $result['data'];
break;
case 'wxapp': //跳转微信小程序支付
$json['pay_type'] = 'wxapp';
$json['pay_info'] = $result['data'];
break;
case 'error':
$json['code'] = -2;
$json['msg'] = $result['msg'];
break;
default:
$json['pay_type'] = 'jump';
$json['pay_info'] = $siteurl.'pay/submit/'.TRADE_NO.'/';
break;
}
if($json['code'] == 0){
if(is_array($json['pay_info'])) $json['pay_info'] = json_encode($json['pay_info']);
$json['timestamp'] = time().'';
$json['sign_type'] = 'RSA';
$json['sign'] = self::makeSign($json, null);
}
exit(json_encode($json));
}else{
$json = ['code'=>1, 'trade_no'=>TRADE_NO];
switch($type){
case 'jump': //跳转URL
$json['payurl'] = $result['url'];
break;
case 'html': //显示html跳转
$json['html'] = $result['data'];
break;
case 'qrcode': //扫码支付
$json['qrcode'] = $result['url'];
break;
case 'scheme': //小程序H5跳转
$json['urlscheme'] = $result['url'];
break;
case 'error':
$json['code'] = -2;
$json['msg'] = $result['msg'];
break;
default:
$json['payurl'] = $siteurl.'pay/submit/'.TRADE_NO.'/';
break;
}
exit(json_encode($json));
}
}
// 订单回调处理
static public function processOrder($isnotify, $order, $api_trade_no, $buyer = null, $bill_trade_no = null){
global $DB,$conf,$siteurl;
if($order['status']==0 || $order['status']==4){
if($DB->exec("UPDATE `pre_order` SET `status`=1 WHERE `trade_no`='".$order['trade_no']."'")){
$data = ['endtime'=>'NOW()', 'date'=>'CURDATE()'];
if(!empty($api_trade_no)) $data['api_trade_no'] = $api_trade_no;
if(!empty($buyer)) $data['buyer'] = $buyer;
if(!empty($bill_trade_no)) $data['bill_trade_no'] = $bill_trade_no;
if($order['settle']>0) $data['settle'] = $order['settle'];
$DB->update('order', $data, ['trade_no'=>$order['trade_no']]);
$order['api_trade_no'] = $api_trade_no;
processOrder($order, $isnotify);
}
}elseif(empty($order['api_trade_no']) && !empty($api_trade_no)){
$data = ['api_trade_no'=>$api_trade_no];
if(!empty($buyer)) $data['buyer'] = $buyer;
if(!empty($bill_trade_no)) $data['bill_trade_no'] = $bill_trade_no;
$DB->update('order', $data, ['trade_no'=>$order['trade_no']]);
}elseif(empty($order['buyer']) && !empty($buyer)){
$data['buyer'] = $buyer;
$DB->update('order', $data, ['trade_no'=>$order['trade_no']]);
}
if($isnotify && $order['settle']>0){
$DB->update('order', ['settle'=>$order['settle']], ['trade_no'=>$order['trade_no']]);
}
if(!$isnotify){
include_once SYSTEM_ROOT.'txprotect.php';
if($order['status'] == 2){
$jumpurl = '/payerr.html';
returnTemplate($jumpurl);
return;
}
// 支付完成5分钟后禁止跳转回网站
if(!empty($order['endtime']) && time() - strtotime($order['endtime']) > 300){
$jumpurl = '/payok.html';
}else{
$url=creat_callback($order);
$jumpurl = $url['return'];
}
returnTemplate($jumpurl);
}
}
// 更新订单信息
static public function updateOrder($trade_no, $api_trade_no, $buyer = null, $status = null){
global $DB;
$data = ['api_trade_no'=>$api_trade_no];
if(!empty($buyer)) $data['buyer'] = $buyer;
if($status) $data['status'] = $status;
$DB->update('order', $data, ['trade_no'=>$trade_no]);
}
// 更新订单扩展信息
static public function updateOrderExt($trade_no, $data){
global $DB;
$DB->update('order', ['ext'=>serialize($data)], ['trade_no'=>$trade_no]);
}
// 更新合单状态
static public function updateOrderCombine($trade_no, $sub_orders = null){
global $DB;
$DB->update('order', ['combine'=>1], ['trade_no'=>$trade_no]);
if(!empty($sub_orders)){
$DB->delete('suborder', ['trade_no'=>$trade_no]);
foreach($sub_orders as $data){
$data['trade_no'] = $trade_no;
$data['status'] = 0;
$DB->insert('suborder', $data);
}
}
}
// 更新订单分账接收人
static public function updateOrderProfits($order, $plugin){
global $DB;
$support_plugins = \lib\ProfitSharing\CommUtil::$plugins;
if(in_array($plugin, $support_plugins)){
$psreceiver = null;
if($order['subchannel'] > 0){
$psreceiver = $DB->getRow("SELECT * FROM `pre_psreceiver` WHERE `channel`='{$order['channel']}' AND `uid`='{$order['uid']}' AND `subchannel`='{$order['subchannel']}' AND `status`=1 ORDER BY id ASC LIMIT 1");
}
if(!$psreceiver) $psreceiver = $DB->getRow("SELECT * FROM `pre_psreceiver` WHERE `channel`='{$order['channel']}' AND `uid`='{$order['uid']}' AND `status`=1 ORDER BY id ASC LIMIT 1");
if(!$psreceiver) $psreceiver = $DB->getRow("SELECT * FROM `pre_psreceiver` WHERE `channel`='{$order['channel']}' AND `uid` IS NULL AND `status`=1 ORDER BY id ASC LIMIT 1");
if($psreceiver){
if(!$psreceiver['minmoney'] || $order['realmoney']>=$psreceiver['minmoney']){
$DB->update('order', ['profits'=>$psreceiver['id']], ['trade_no'=>$order['trade_no']]);
return intval($psreceiver['id']);
}
}
}
return 0;
}
// 更新订单分账接收人2
static public function updateOrderProfits2($order, $plugin){
return 0;
}
//支付宝直付通确认结算
public static function alipaydSettle($channel, $order){
$alipay_config = require(PLUGIN_ROOT.'alipayd/inc/config.php');
$alipaySevice = new \Alipay\AlipayTradeService($alipay_config);
if($order['combine'] == 1){
$sub_orders = self::getSubOrders($order['trade_no']);
if(empty($sub_orders)) throw new Exception('子订单数据不存在');
$failnum = 0;
$errmsg = '';
foreach($sub_orders as $sub_order){
if($sub_order['settle'] == 0){
$settle = 0;
try{
$alipaySevice->settle_confirm($sub_order['api_trade_no'], $sub_order['money']);
$settle = 1;
}catch(Exception $e){
if(strpos($e->getMessage(), 'ALREADY_CONFIRM_SETTLE')!==false){
$settle = 1;
}else{
$failnum++;
$errmsg .= $e->getMessage().',';
}
}
if($settle == 1) self::updateSubOrderSettle($sub_order['sub_trade_no'], 1);
}
}
if($failnum > 0) throw new Exception('部分子单结算失败,失败数量:'.$failnum.',失败原因:'.$errmsg);
return true;
}
try{
$alipaySevice->settle_confirm($order['api_trade_no'], $order['realmoney']);
return true;
}catch(Exception $e){
if(strpos($e->getMessage(), 'ALREADY_CONFIRM_SETTLE')!==false){
return true;
}else{
throw $e;
}
}
}
//微信收付通确认结算
public static function wxpaynpSettle($channel, $order){
$wechatpay_config = require(PLUGIN_ROOT.'/wxpaynp/inc/config.php');
if($wechatpay_config['ecommerce']){
if(!$order['profits']){
$client = new \WeChatPay\V3\ProfitsharingService($wechatpay_config);
return $client->unfreeze($order['trade_no'], $order['api_trade_no']);
}else{
throw new Exception('当前订单需要分账,请进入分账订单页面确认分账');
}
}else{
throw new Exception('非电商收付通订单');
}
}
//支付宝预授权资金支付
public static function alipayPreAuthPay($channel, $order){
global $conf;
$alipay_config = require(PLUGIN_ROOT.$channel['plugin'].'/inc/config.php');
$alipaySevice = new \Alipay\AlipayTradeService($alipay_config);
$trade_no = $order['trade_no'];
$bizContent = [
'out_order_no' => $trade_no,
'out_request_no' => $trade_no,
];
$result = $alipaySevice->preAuthQuery($bizContent);
//print_r($result);exit;
if(!isset($result['auth_no'])) throw new Exception('预授权订单查询失败');
if($result['rest_amount'] == 0) throw new Exception('剩余冻结金额为0');
if($result['order_status'] == 'AUTHORIZED'){
$auth_no = $result['auth_no'];
$ordername = !empty($conf['ordername'])?ordername_replace($conf['ordername'],$order['name'],$order['uid'],$trade_no):$order['name'];
$bizContent = [
'out_trade_no' => $trade_no,
'total_amount' => $result['rest_amount'],
'subject' => $ordername,
'product_code' => 'PREAUTH_PAY',
'auth_no' => $auth_no,
'auth_confirm_mode' => 'COMPLETE'
];
return $alipaySevice->scanPay($bizContent);
}else{
throw new Exception('该笔订单非已授权状态,无需支付');
}
}
//支付宝预授权资金解冻
public static function alipayUnfreeze($channel, $order){
$alipay_config = require(PLUGIN_ROOT.$channel['plugin'].'/inc/config.php');
$alipaySevice = new \Alipay\AlipayTradeService($alipay_config);
$trade_no = $order['trade_no'];
$bizContent = [
'out_order_no' => $trade_no,
'out_request_no' => $trade_no,
];
$result = $alipaySevice->preAuthQuery($bizContent);
//print_r($result);exit;
if(!isset($result['auth_no'])) throw new Exception('预授权订单查询失败');
if($result['rest_amount'] == 0) throw new Exception('剩余冻结金额为0');
if($result['order_status'] == 'AUTHORIZED'){
$auth_no = $result['auth_no'];
$bizContent = [
'auth_no' => $auth_no,
'out_request_no' => date("YmdHis").rand(11111,99999),
'amount' => $result['rest_amount'],
'remark' => '解冻资金'
];
return $alipaySevice->preAuthUnfreeze($bizContent);
}else{
throw new Exception('该笔订单非已授权状态,无需解冻');
}
}
//支付宝红包转账
public static function alipayRedPacketTransfer($channel, $payee_user_id, $money, $order_id){
$out_biz_no = date("YmdHis").rand(11111,99999);
$alipay_config = require(PLUGIN_ROOT.$channel['plugin'].'/inc/config.php');
$alipaySevice = new \Alipay\AlipayTransferService($alipay_config);
$alipaySevice->redPacketTansfer($out_biz_no, $money, $payee_user_id, $conf['sitename'], $order_id);
}
//支付宝红包资金退回
public static function alipayRedPacketRefund($channel, $trade_no, $money){
$out_biz_no = date("YmdHis").rand(11111,99999);
$alipay_config = require(PLUGIN_ROOT.$channel['plugin'].'/inc/config.php');
$alipaySevice = new \Alipay\AlipayTransferService($alipay_config);
$alipaySevice->redPacketRefund($out_biz_no, $trade_no, $money);
}
//加锁设置订单扩展数据
public static function lockPayData($trade_no, $func){
global $DB;
$DB->beginTransaction();
$data = $DB->getColumn("SELECT ext FROM pre_order WHERE trade_no=:trade_no FOR UPDATE", [':trade_no'=>$trade_no]);
if($data) {
$DB->rollBack();
return unserialize($data);
}
try{
$data = $func();
}catch(\Exception $e){
$DB->rollBack();
throw $e;
}
if($data){
$DB->update('order', ['ext'=>serialize($data)], ['trade_no' => $trade_no]);
}
$DB->commit();
return $data;
}
//获取微信客服跳转链接
public static function getWxkfPayUrl($pay_url){
global $order, $DB, $conf;
$cookiesid = $_COOKIE['mysid'];
if(!$cookiesid||!preg_match('/^[0-9a-z]{32}$/i', $cookiesid)){
$cookiesid = getSid();
setcookie("mysid", $cookiesid, time() + 2592000, '/');
}
if($conf['wework_paykfid'] > 0){
$wxkfaccount = $DB->getRow("SELECT * FROM pre_wxkfaccount WHERE id=:id", [':id'=>$conf['wework_paykfid']]);
}elseif($conf['wework_paymsgmode'] == 1){
$usekflist = $DB->getAll("SELECT DISTINCT aid FROM pre_wxkflog WHERE `sid`=:sid AND addtime>=:addtime AND status=1", [':sid'=>$cookiesid, ':addtime'=>date("Y-m-d H:i:s", strtotime('-48 hours'))]);
$usekfids = [0];
foreach($usekflist as $usekf){
$usekfids[] = intval($usekf['aid']);
}
$wxkfaccount = $DB->getRow("SELECT A.* FROM pre_wxkfaccount A LEFT JOIN pre_wework B ON A.wid=B.id WHERE A.id NOT IN (".implode(",", $usekfids).") AND B.status=1 ORDER BY A.usetime ASC LIMIT 1");
}else{
$wxkfaccount = $DB->getRow("SELECT A.* FROM pre_wxkfaccount A LEFT JOIN pre_wework B ON A.wid=B.id WHERE B.status=1 ORDER BY A.usetime ASC LIMIT 1");
}
if(!$wxkfaccount) return false;
$DB->insert('wxkflog', ['trade_no'=>$order['trade_no'], 'aid'=>$wxkfaccount['id'], 'sid'=>$cookiesid, 'payurl'=>$pay_url, 'addtime'=>'NOW()']);
$scene_param = 'orderid='.$order['trade_no'].'&money='.$order['realmoney'];
try{
if(!empty($wxkfaccount['url'])){
$kfurl = $wxkfaccount['url'];
$DB->update('wxkfaccount', ['usetime'=>'NOW()'], ['id'=>$wxkfaccount['id']]);
}else{
$wework = new wechat\WeWorkAPI($wxkfaccount['wid']);
$kfurl = $wework->getKFURL($wxkfaccount['openkfid'], 'pay');
$DB->update('wxkfaccount', ['url'=>$kfurl, 'usetime'=>'NOW()'], ['id'=>$wxkfaccount['id']]);
}
$kfurl = 'weixin://biz/ww/kefu/' . $kfurl . '&schema=1';
$kfurl .= '&scene_param='.urlencode($scene_param);
return $kfurl;
}catch(\Exception $e){
sysmsg($e->getMessage());
}
}
//支付宝直付通&微信收付通延迟结算处理
public static function settle_task(){
global $DB;
$orders = $DB->getAll("SELECT A.*,B.plugin FROM pre_order A LEFT JOIN pre_channel B ON A.channel=B.id WHERE A.status=1 AND A.settle=1 AND A.addtime<DATE_SUB(NOW(), INTERVAL 24 HOUR) AND B.plugin in ('alipayd','wxpaynp') ORDER BY A.trade_no ASC LIMIT 10");
foreach($orders as $row){
$trade_no = $row['trade_no'];
$channel = $row['subchannel'] > 0 ? \lib\Channel::getSub($row['subchannel']) : \lib\Channel::get($row['channel'], $DB->findColumn('user', 'channelinfo', ['uid'=>$row['uid']]));
if(!$channel) continue;
try{
if($row['plugin'] == 'alipayd'){
self::alipaydSettle($channel, $row);
}elseif($row['plugin'] == 'wxpaynp'){
self::wxpaynpSettle($channel, $row);
}
$DB->update('order', ['settle'=>2], ['trade_no'=>$trade_no]);
echo $trade_no.' 结算成功<br/>';
}catch(Exception $e){
$errmsg = $e->getMessage();
if(strpos($errmsg, 'ALREADY_CONFIRM_SETTLE')){
$DB->update('order', ['settle'=>2], ['trade_no'=>$trade_no]);
echo $trade_no.' 结算成功<br/>';
continue;
}
$DB->update('order', ['settle'=>3], ['trade_no'=>$trade_no]);
echo $trade_no.' 结算失败,'.$errmsg.'<br/>';
}
}
}
public static function getSubOrders($trade_no){
global $DB;
return $DB->getAll("SELECT * FROM pre_suborder WHERE trade_no=:trade_no", [':trade_no'=>$trade_no]);
}
public static function processSubOrders($trade_no, $sub_orders){
global $DB;
foreach($sub_orders as $data){
$DB->update('suborder', ['status'=>1, 'api_trade_no'=>$data['api_trade_no']], ['sub_trade_no'=>$data['sub_trade_no']]);
}
}
public static function refundSubOrder($sub_trade_no, $refundmoney = null){
global $DB;
$DB->update('suborder', ['status'=>2, 'refundmoney'=>$refundmoney], ['sub_trade_no'=>$sub_trade_no]);
}
public static function updateSubOrderSettle($sub_trade_no, $settle){
global $DB;
$DB->update('suborder', ['settle'=>$settle], ['sub_trade_no'=>$sub_trade_no]);
}
}