HaniBlindBox/server/php/app/api/controller/Pay.php
2026-01-01 20:46:07 +08:00

685 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

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

<?php
declare(strict_types=1);
namespace app\api\controller;
use app\common\model\Order as OrderModel;
use app\common\model\ProductOrder;
use app\common\model\UserRecharge;
use app\common\helper\WxPayHelper;
use think\App;
use think\facade\Db;
use app\common\model\User;
class Pay extends Base
{
// 添加属性定义
protected $appid;
protected $merchant;
protected $secretKey;
protected $noticeurl;
public function __construct()
{
// 获取系统微信设置
$wechat_setting = getConfig('wechat_setting');
// 初始使用默认配置,具体支付时会根据订单号重新获取商户信息
$wxpayConfig = WxPayHelper::getWxPayConfig();
if (!empty($wxpayConfig['merchant'])) {
$this->appid = $wxpayConfig['appid'];
$this->merchant = $wxpayConfig['merchant']['mch_id'];
$this->secretKey = $wxpayConfig['merchant']['keys'];
} else {
// 如果没有获取到商户信息,则使用旧方式
$config = getConfig('weixinpay');
// 如果系统设置中存在微信配置,则优先使用
if (!empty($wechat_setting) && !empty($wechat_setting['appid']) && !empty($wechat_setting['appSecret'])) {
$this->appid = $wechat_setting['appid'];
} else {
$this->appid = $config['appid'];
}
$this->merchant = $config['mch_id'];
$this->secretKey = $config['keys'];
}
$this->noticeurl = request()->domain() . '/api/notify/order_notify';#订单回调URL
}
/**
* 根据订单号设置正确的商户配置
* 确保创建订单和支付使用的是同一个商户
*
* @param string $order_num 订单号
* @return bool 是否成功设置商户信息
*/
protected function setMerchantByOrderNum($order_num)
{
if (!$this->ish5() && (strpos($order_num, 'MH_') === 0 || strpos($order_num, 'FH_') === 0)) {
// 提取订单中的商户前缀和小程序前缀
$prefixInfo = WxPayHelper::extractOrderPrefix($order_num);
$merchant_prefix = null;
$miniprogram_prefix = null;
// 新格式返回为数组,包含商户前缀和可能的小程序前缀
if (is_array($prefixInfo)) {
$merchant_prefix = $prefixInfo['merchant_prefix'] ?? null;
$miniprogram_prefix = $prefixInfo['miniprogram_prefix'] ?? null;
} else {
// 兼容旧格式,直接作为商户前缀
$merchant_prefix = $prefixInfo;
}
// 优先根据小程序前缀获取配置
$wxpayConfig = null;
if (!empty($miniprogram_prefix)) {
// 通过小程序前缀获取小程序配置
$miniprogramConfig = \app\common\helper\MiniprogramHelper::getMiniprogramConfigByOrderPrefix($miniprogram_prefix);
if (!empty($miniprogramConfig)) {
// 使用小程序关联的商户配置
if (!empty($merchant_prefix)) {
$wxpayConfig = WxPayHelper::getFixedWxPayConfig($merchant_prefix);
// 确保使用小程序的appid
$wxpayConfig['appid'] = $miniprogramConfig['appid'];
} else {
// 没有商户前缀但有小程序配置,使用小程序默认关联的商户
$wxpayConfig = WxPayHelper::getWxPayConfig();
$wxpayConfig['appid'] = $miniprogramConfig['appid'];
}
}
}
// 如果没有通过小程序前缀获取到配置,则回退到商户前缀
if (empty($wxpayConfig) && !empty($merchant_prefix)) {
$wxpayConfig = WxPayHelper::getFixedWxPayConfig($merchant_prefix);
}
// 如果前两种方式都没有获取到配置,则使用默认配置
if (empty($wxpayConfig)) {
return false;
}
if (!empty($wxpayConfig['merchant'])) {
// 更新当前实例的商户信息
$this->appid = $wxpayConfig['appid'];
$this->merchant = $wxpayConfig['merchant']['mch_id'];
$this->secretKey = $wxpayConfig['merchant']['keys'];
return true;
}
}
return false;
}
//支付
/**
* [pay description]
* @param [type] $uid [用户 的id]
* @param [type] $order_num [订单号]
* @param [pay_type] [支付类型 1 微信 ]
* @return [type] [支付来源 1 盲盒订单 2运费 3 余额充值 4 积分商城支付]
*/
public function pay($order_num, $title, $openid, $type)
{
// 确保设置正确的商户配置
$this->setMerchantByOrderNum($order_num);
if ($type == 1) {
$order = OrderModel::getInfo(['order_num' => $order_num]);
if ($order['status'] != 1) {
return $this->renderError("参数错误");
}
$total = $order['total_price'];
if ($total > 0) {
$notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify1';
return $this->wxpay($order_num, $total, "支付订单-" . $title, $openid, $notify);
} else {
// if($order['money'] > 0){
// User::changeMoney($order['user_id'],'-'.$order['money'],3);
// }
// if($order['integral'] > 0){
// User::changeIntegral($order['user_id'],'-'.$order['integral'],3);
// }
$notify = new \app\api\controller\Notify($this->app);
$data = $notify->order_update($order_num, 1);
if ($data == 1) {
$data = [];
$data['is_weixin'] = 2;
return json_encode($data);
}
}
} elseif ($type == 2) {
//发货订单支付运费
$config = getConfig("base");
$total = $config['post_money']; //邮费
$notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify2';
return $this->wxpay($order_num, $total, $title, $openid, $notify);
} elseif ($type == 3) {
//余额充值
$order = UserRecharge::getInfo(['order_num' => $order_num]);
$total = $order['money'];
$notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify3';
return $this->wxpay($order_num, $total, $title, $openid, $notify);
} elseif ($type == 4) {
//积分 商城购买
$order = OrderModel::getInfo(['order_num' => $order_num]);
$total = $order['total_money'];
$notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify4';
return $this->wxpay($order_num, $total, $title, $openid, $notify);
} elseif ($type == 5) {
} elseif ($type == 6) {
$order = OrderModel::getInfo(['order_num' => $order_num]);
if ($order['status'] != 1) {
return $this->renderError("参数错误");
}
$total = $order['total_price'];
if ($total > 0) {
$notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify6';
return $this->wxpay($order_num, $total, "支付订单-" . $title, $openid, $notify);
} else {
$notify = new \app\api\controller\Notify($this->app);
$data = $notify->order_update($order_num, 6);
if ($data == 1) {
$data = [];
$data['is_weixin'] = 2;
return json_encode($data);
}
}
} elseif ($type == 7) {
$order = Db::name('kk_order')->where('order_no', $order_num)->find();
if ($order['status'] !== 0) {
return $this->renderError("参数错误");
}
$total = $order['price'];
if ($total > 0) {
$notify = 'https://' . $_SERVER['HTTP_HOST'] . '/api/notify/order_notify7';
return $this->wxpay($order_num, $total, "秒杀商城订单-" . $title, $openid, $notify);
} else {
// dd($this->app);
// $notify = new Notify($this->app);
$res = Notify::order_update($order_num, 7);
$data['res'] = $res;
$data['is_weixin'] = 2;
return json_encode($data);
}
}
}
//支付
public function wxpay($order_num, $money, $title, $openid, $notify)
{
// 根据订单号设置正确的商户配置
$this->setMerchantByOrderNum($order_num);
$openidx = $openid;
if ($this->ish5()) {
$user = User::getInfo(['openid' => $openidx]);
if ($user != null && $user['gzh_openid'] != null && $user['gzh_openid'] != "") {
$openidx = $user['gzh_openid'];
}
}
// $weixinpay = getConfig('weixinpay');
// //支付代码
// $url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
// $data['openid']=$openid;
// $data['appid']= $this->appid;
// $data['mch_id']= $this->merchant;
// $data['nonce_str']=$this->genRandomString();
// $data['body']=$title;
// $data['out_trade_no']=$order_num;
// $data['total_fee'] = $money*100;
// $data['spbill_create_ip'] = '127.0.0.1';
// $data['notify_url'] = $notify;
// $data['time_expire'] = date('YmdHis',time()+60);
// $data['trade_type'] = 'JSAPI';
// $sign = $this->MakeSign($data);
// $data['sign']=$sign;
// // dd($data);
// $dataxml = $this->data_to_xml($data);
// $resXml = $this->postXmlCurl($url,$dataxml);
// $resData = $this->xml_to_data($resXml);
$body = mb_substr($title, 0, 30);
// $notifyUrl = $this->noticeurl;
$nonce_str = $this->genRandomString();
$params['appid'] = $this->appid;
$params['mch_id'] = $this->merchant;
$params['nonce_str'] = $this->genRandomString();
$params['body'] = $body;
$params['attach'] = 'order_ckj';
$params['out_trade_no'] = $order_num;
$params['notify_url'] = $notify;
// $params['total_fee'] = round($money * 100, 2);
$params['total_fee'] = 1;
$params['spbill_create_ip'] = $this->get_client_ip();
$params['trade_type'] = 'JSAPI';
$params['openid'] = $openidx;
$params['sign'] = $this->MakeSign($params);
// dd($params);
$xml = $this->data_to_xml($params);
$url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
$response = $this->postXmlCurl($xml, $url);
$resData = $this->xml_to_data($response);
// p($resData);
if (!$resData || $resData['return_code'] != 'SUCCESS' || $resData['result_code'] != 'SUCCESS') {
$return['status'] = 0;
$return['msg'] = "网络故障,请稍后重试(支付参数错误)";
return json_encode($return);
} else {
$return['appId'] = $resData['appid'];
$return['nonceStr'] = $this->genRandomString();
$return['package'] = 'prepay_id=' . $resData['prepay_id'];
$return['signType'] = 'MD5';
$return['timeStamp'] = (string) time();
$return['paySign'] = $this->MakeSign($return);
$return['is_weixin'] = 1;
return json_encode($return);
}
}
/**
* 微信小程序下单方法
* @param $params 下单参数
*/
public function wxCreateOrder($order_no, $price, $user_id, $body, $attach)
{
// 根据订单号设置正确的商户配置
$this->setMerchantByOrderNum($order_no);
$body = mb_substr($body, 0, 30);
// 使用新的动态路由生成通知URL
$user = User::where('id', $user_id)->find();
$openid = $user['openid'];
$payment_type = 'wxpay';
$order_type = $attach;
$user_id = $user ? $user['id'] : 0;
// 支付使用的随机数
$nonce_str = $this->genRandomString();
// 回调使用的随机数(与支付随机数分离)
$callback_nonce_str = $this->genRandomString();
// 生成新的支付通知URL
$notifyUrl = generatePayNotifyUrl($payment_type, $order_type, $user_id, $order_no, $callback_nonce_str);
$is_test = $user['istest'];
// if ($is_test == 2) {
// $price = 0.01;
// }
// 将通知URL和随机字符串保存到order_notify表中
Db::name('order_notify')->insert([
'order_no' => $order_no,
'notify_url' => $notifyUrl,
'nonce_str' => $callback_nonce_str,
'pay_time' => date('Y-m-d H:i:s'),
'pay_amount' => $price,
'status' => 0,
'retry_count' => 0,
'create_time' => date('Y-m-d H:i:s'),
'update_time' => date('Y-m-d H:i:s')
]);
$params['appid'] = $this->appid;
$params['mch_id'] = $this->merchant;
$params['nonce_str'] = $nonce_str;
$params['body'] = $body;
$params['attach'] = $attach;
$params['out_trade_no'] = $order_no;
$params['notify_url'] = $notifyUrl;
$params['total_fee'] = round($price * 100, 2);
$params['spbill_create_ip'] = $this->get_client_ip();
$params['trade_type'] = 'JSAPI';
$params['openid'] = $openid;
$params['sign'] = $this->MakeSign($params);
$xml = $this->data_to_xml($params);
$url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
$response = $this->postXmlCurl($xml, $url);
$result = $this->xml_to_data($response);
if ($result['return_code'] === 'SUCCESS' && $result['result_code'] === 'SUCCESS') {
$time = time();
$res['appId'] = $this->appid;
$res["timeStamp"] = (string) $time;
$res["nonceStr"] = $nonce_str;
$res["package"] = "prepay_id=" . $result['prepay_id'];
$res["signType"] = 'MD5';
$res["paySign"] = $this->MakeSign($res);
return ['status' => 1, 'data' => $res];
} else {
return ['status' => 0, 'msg' => '支付失败'];
}
}
public function post_order($openid, $access_token, $order_num, $title = '订单发货')
{
$msg = "本单购买商品已发放至[小程序盒柜]";
if (strpos($order_num, 'FH_') === 0) {
$msg = "本单购买的商品正在打包,请联系客服获取物流信息";
}
// 根据订单号设置正确的商户配置
$this->setMerchantByOrderNum($order_num);
$date = new \DateTime();
// $this->appid
//订单发货时间
$formattedDate = $date->format('Y-m-d\TH:i:s');
$request_url = "https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=" . $access_token;
$param = '{
"order_key": {
"order_number_type": 1,
"mchid":"' . $this->merchant . '",
"out_trade_no":"' . $order_num . '"
},
"logistics_type": 4,
"delivery_mode": 1,
"shipping_list": [
{
"item_desc": "'.$msg.'"
}
],
"upload_time": "' . $formattedDate . '+08:00",
"payer": {
"openid":"' . $openid . '"
}
}';
// 记录请求参数日志
writelog('post_order_log', json_encode([
'method' => 'post_order',
'order_num' => $order_num,
'openid' => $openid,
'merchant' => $this->merchant,
'request_url' => $request_url,
'param' => $param,
'time' => date('Y-m-d H:i:s')
]));
$res = curlPost($request_url, $param);
$res = json_decode($res, true);
// 记录响应结果日志
writelog('post_order_log', json_encode([
'method' => 'post_order_response',
'order_num' => $order_num,
'response' => $res,
'time' => date('Y-m-d H:i:s')
]));
if ($res['errcode'] == 0 && $res['errmsg'] == 'ok') {
return 1;
} else {
// 发货失败将订单信息存入Redis等待定时任务重试
$redis = (new \app\common\server\RedisHelper())->getRedis();
$key = 'post_order:' . $order_num;
// 存储发货失败的订单信息
$orderData = [
'openid' => $openid,
'appid' => $this->appid,
// 不存储access_token因为可能过期
'order_num' => $order_num,
'title' => $title,
'merchant' => $this->merchant,
'error_code' => $res['errcode'] ?? 'unknown',
'error_msg' => $res['errmsg'] ?? 'unknown',
'retry_count' => 0,
'last_retry_time' => time(),
'create_time' => time()
];
// 存入Redis设置过期时间为3天
$redis->set($key, json_encode($orderData));
$redis->expire($key, 3 * 24 * 3600);
// 记录存入Redis的日志
writelog('post_order_log', json_encode([
'method' => 'post_order_redis_save',
'order_num' => $order_num,
'redis_key' => $key,
'data' => $orderData,
'time' => date('Y-m-d H:i:s')
]));
return 2;
}
}
/**
* 生成签名
* @return 签名
*/
public function MakeSign($params)
{
//签名步骤一:按字典序排序数组参数
ksort($params);
$string = $this->ToUrlParams($params);
//签名步骤二在string后加入KEY
$string = $string . "&key=" . $this->secretKey;
//签名步骤三MD5加密
$string = md5($string);
//签名步骤四:所有字符转为大写
$result = strtoupper($string);
return $result;
}
/**
* 将参数拼接为url: key=value&key=value
* @param $params
* @return string
*/
public function ToUrlParams($params)
{
$string = '';
if (!empty($params)) {
$array = array();
foreach ($params as $key => $value) {
$array[] = $key . '=' . $value;
}
$string = implode("&", $array);
}
return $string;
}
/**
* 输出xml字符
* @param $params 参数名称
* return string 返回组装的xml
**/
public function data_to_xml($params)
{
if (!is_array($params) || count($params) <= 0) {
return false;
}
$xml = "<xml>";
foreach ($params as $key => $val) {
if (is_numeric($val)) {
$xml .= "<" . $key . ">" . $val . "</" . $key . ">";
} else {
$xml .= "<" . $key . "><![CDATA[" . $val . "]]></" . $key . ">";
}
}
$xml .= "</xml>";
return $xml;
}
/**
* 将xml转为array
* @param string $xml
* return array
*/
public function xml_to_data($xml)
{
if (!$xml) {
return false;
}
//将XML转为array
//禁止引用外部xml实体
libxml_disable_entity_loader(true);
$data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
return $data;
}
/**
* 产生一个指定长度的随机字符串,并返回给用户
* @param type $len 产生字符串的长度
* @return string 随机字符串
*/
private function genRandomString($len = 32)
{
$chars = array(
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9"
);
$charsLen = count($chars) - 1;
// 将数组打乱
shuffle($chars);
$output = "";
for ($i = 0; $i < $len; $i++) {
$output .= $chars[mt_rand(0, $charsLen)];
}
return $output;
}
/**
* 以post方式提交xml到对应的接口url
*
* @param string $xml 需要post的xml数据
* @param string $url url
* @param bool $useCert 是否需要证书,默认不需要
* @param int $second url执行超时时间默认30s
* @throws WxPayException
*/
private function postXmlCurl($xml, $url, $second = 30)
{
$ch = curl_init();
//设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, $second);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
//设置header
curl_setopt($ch, CURLOPT_HEADER, FALSE);
//要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
//运行curl
$data = curl_exec($ch);
//返回结果
if ($data) {
curl_close($ch);
return $data;
} else {
$error = curl_errno($ch);
curl_close($ch);
return false;
}
}
/**
* 错误代码
* @param $code 服务器输出的错误代码
* return string
*/
public function error_code($code)
{
$errList = array(
'NOAUTH' => '商户未开通此接口权限',
'NOTENOUGH' => '用户帐号余额不足',
'ORDERNOTEXIST' => '订单号不存在',
'ORDERPAID' => '商户订单已支付,无需重复操作',
'ORDERCLOSED' => '当前订单已关闭,无法支付',
'SYSTEMERROR' => '系统错误!系统超时',
'APPID_NOT_EXIST' => '参数中缺少APPID',
'MCHID_NOT_EXIST' => '参数中缺少MCHID',
'APPID_MCHID_NOT_MATCH' => 'appid和mch_id不匹配',
'LACK_PARAMS' => '缺少必要的请求参数',
'OUT_TRADE_NO_USED' => '同一笔交易不能多次提交',
'SIGNERROR' => '参数签名结果不正确',
'XML_FORMAT_ERROR' => 'XML格式错误',
'REQUIRE_POST_METHOD' => '未使用post传递参数 ',
'POST_DATA_EMPTY' => 'post数据不能为空',
'NOT_UTF8' => '未使用指定编码格式',
);
if (array_key_exists($code, $errList)) {
return $errList[$code];
}
}
function get_client_ip()
{
if (isset($_SERVER['REMOTE_ADDR'])) {
$cip = $_SERVER['REMOTE_ADDR'];
} elseif (getenv("REMOTE_ADDR")) {
$cip = getenv("REMOTE_ADDR");
} elseif (getenv("HTTP_CLIENT_IP")) {
$cip = getenv("HTTP_CLIENT_IP");
} else {
$cip = "127.0.0.1";
}
return $cip;
}
}