1115 lines
38 KiB
PHP
1115 lines
38 KiB
PHP
<?php
|
||
namespace app\common\server\platform;
|
||
use app\common\model\OrderListSend;
|
||
use think\App;
|
||
use think\facade\Db;
|
||
use think\facade\Log;
|
||
use app\common\helper\WxPayHelper;
|
||
use think\facade\Request;
|
||
use app\common\model\User;
|
||
use app\common\model\Order;
|
||
use app\common\model\OrderNotify;
|
||
use app\common\model\DiamondOrder;
|
||
use app\common\helper\EnvHelper;
|
||
use Exception;
|
||
|
||
/**
|
||
* 多端平台支付抽象基类
|
||
*/
|
||
class MiniProgramPlatform extends BasePlatform
|
||
{
|
||
/**
|
||
* 小程序配置
|
||
* @var
|
||
*/
|
||
public $mp_miniprogram = null;
|
||
/**
|
||
* 微信app_id
|
||
* @var
|
||
*/
|
||
public $wx_appid = null;
|
||
/**
|
||
* 微信密钥
|
||
* @var
|
||
*/
|
||
public $wx_secret = null;
|
||
/**
|
||
* 商户配置
|
||
* @var
|
||
*/
|
||
public $mp_merchant = null;
|
||
/**
|
||
* 商户id
|
||
* @var
|
||
*/
|
||
private $mch_id = null;
|
||
/**
|
||
* 商户key
|
||
* @var
|
||
*/
|
||
private $key = null;
|
||
public function __construct()
|
||
{
|
||
parent::__construct();
|
||
$this->code = 'MP-WEIXIN';
|
||
$this->env = 'miniProgram';
|
||
|
||
$this->mp_miniprogram = \app\common\helper\MiniprogramHelper::getMiniprogramConfig();
|
||
|
||
if (!empty($this->mp_miniprogram['appid'])) {
|
||
$this->wx_appid = $this->mp_miniprogram['appid'];
|
||
$this->wx_secret = $this->mp_miniprogram['app_secret'];
|
||
}
|
||
}
|
||
/**
|
||
* 获取系统配置
|
||
* @return void
|
||
*/
|
||
public function get_config(): array
|
||
{
|
||
return ['isWebPay' => true];
|
||
}
|
||
/**
|
||
* Summary of pay [$user,$price,title,attach]
|
||
* @param mixed $data
|
||
* @return array{data: array, status: int|array{msg: string, status: int}}
|
||
*/
|
||
public function pay(array $data): array
|
||
{
|
||
$currentHour = (int) date('H');
|
||
$currentMinute = (int) date('i');
|
||
$currentTime = $currentHour * 100 + $currentMinute;
|
||
|
||
|
||
|
||
$data += ['user' => null, 'price' => 0, 'title' => '', 'attach' => 'order_wxs', 'pre' => 'MH_'];
|
||
[
|
||
'user' => $user,
|
||
'price' => $price,
|
||
'title' => $title,
|
||
'attach' => $attach,
|
||
'pre' => $pre
|
||
] = $data;
|
||
if ($user == null) {
|
||
//抛出异常
|
||
throw new \Exception('用户信息为空');
|
||
}
|
||
$project_prefix = $this->mp_miniprogram['order_prefix'];
|
||
if ($price <= 0) {
|
||
//生成订单号,订单号为:ML_DRAYDMP02025018.... ML_ 前缀(固定3位),DRA 商户号(固定3位),YD 项目,固定2位,MP0 表示微信小程序支付
|
||
$order_no = create_order_no_new($pre, "MON", $project_prefix, "MP0");
|
||
return [
|
||
'status' => 1,
|
||
'data' => [
|
||
'order_no' => $order_no,
|
||
'res' => []
|
||
]
|
||
];
|
||
}
|
||
|
||
|
||
// if ($currentTime < 800 || $currentTime >= 2200) {
|
||
// return [
|
||
// 'status' => 0,
|
||
// 'data' => [],
|
||
// 'msg' => '支付未开放,请在08:00-22:00范围内购买'
|
||
// ];
|
||
// }
|
||
$title = mb_substr($title, 0, 30);
|
||
|
||
$prefix = $this->GetPrefix();
|
||
$mp_config = $this->get_config();
|
||
if ($mp_config['isWebPay']) {
|
||
$domain = Request::domain();
|
||
// Request::param('return_url')
|
||
$host = parse_url($domain, PHP_URL_HOST);
|
||
$order_no = create_order_no_new($pre, "ZFA", 'H5', "ZFB");
|
||
$extend = [
|
||
'orderType' => $attach,
|
||
];
|
||
$extend_str = json_encode($extend, JSON_UNESCAPED_UNICODE);
|
||
// 将通知URL和随机字符串保存到order_notify表中
|
||
Db::name('order_notify')->insert([
|
||
'order_no' => $order_no,
|
||
'notify_url' => '',
|
||
'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'),
|
||
'extend' => $extend_str,
|
||
'title' => $title,
|
||
]);
|
||
return [
|
||
'status' => 1,
|
||
'data' => [
|
||
'order_no' => $order_no,
|
||
'res' => ['data' => ['order_num' => $order_no], 'requestPay' => $domain . '/api/send_web_pay_order', 'tips' => '您即将进入客服聊天界面完成支付,也可前往「我的」页面下载官方APP,享受更便捷的购物及充值服务。']
|
||
]
|
||
];
|
||
}
|
||
return [
|
||
'status' => 0,
|
||
'data' => [],
|
||
'msg' => '小程序支付通道维护中,请联系客服下载app。'
|
||
];
|
||
//生成订单号,订单号为:ML_DRAYDMP02025018.... ML_ 前缀(固定3位),DRA 商户号(固定3位),YD 项目,固定2位,MP0 表示微信小程序支付
|
||
$order_no = create_order_no_new($pre, $prefix, $project_prefix, "MP0");
|
||
$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'];
|
||
// $randomNumber = random_int(1, 10) / 100;
|
||
// if ($price > $randomNumber) {
|
||
// $price = $price - $randomNumber;
|
||
// }
|
||
// throw new \Exception('支付未开放');
|
||
// if ($is_test == 2) {
|
||
// $price = 0.01;
|
||
// }
|
||
$params['appid'] = $this->wx_appid;
|
||
$params['mch_id'] = $this->getMchId();
|
||
$params['nonce_str'] = $nonce_str;
|
||
$params['body'] = $title;
|
||
$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') {
|
||
// 将通知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'),
|
||
'title' => $title,
|
||
]);
|
||
$time = time();
|
||
$res['appId'] = $this->wx_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' => [
|
||
'order_no' => $order_no,
|
||
'res' => $res
|
||
]
|
||
];
|
||
}
|
||
|
||
// 记录支付失败日志
|
||
$error_message = '';
|
||
if (isset($result['return_code']) && $result['return_code'] === 'FAIL') {
|
||
$error_message = $result['return_msg'] ?? '未知错误';
|
||
} elseif (isset($result['result_code']) && $result['result_code'] === 'FAIL') {
|
||
$error_code = $result['err_code'] ?? '';
|
||
$error_message = $result['err_code_des'] ?? $this->error_code($error_code) ?? '未知错误';
|
||
} else {
|
||
$error_message = '微信支付接口返回异常';
|
||
}
|
||
|
||
// 获取Redis实例
|
||
$redis = (new \app\common\server\RedisHelper())->getRedis();
|
||
|
||
// 记录到ThinkPHP的日志系统中
|
||
Log::error('微信支付失败: ' . json_encode([
|
||
'order_no' => $order_no,
|
||
'user_id' => $user_id,
|
||
'openid' => $openid,
|
||
'price' => $price,
|
||
'error_message' => $error_message,
|
||
'result' => $result,
|
||
'time' => date('Y-m-d H:i:s')
|
||
], JSON_UNESCAPED_UNICODE));
|
||
|
||
// 获取当前商户ID
|
||
$merchant_id = $this->getMchId();
|
||
|
||
// 记录支付失败信息到Redis
|
||
if ($merchant_id) {
|
||
// 商户支付失败计数的key,使用12小时过期时间
|
||
$merchant_fail_key = 'merchant:payment:fail:' . $merchant_id;
|
||
|
||
// 将失败信息存入列表
|
||
$fail_info = [
|
||
'merchant_id' => $merchant_id,
|
||
'order_no' => $order_no,
|
||
'time' => time(),
|
||
'error' => $error_message
|
||
];
|
||
|
||
// 添加到失败列表
|
||
$redis->rPush($merchant_fail_key, json_encode($fail_info));
|
||
|
||
// 设置过期时间为12小时
|
||
$redis->expire($merchant_fail_key, 12 * 3600);
|
||
|
||
// 获取当前列表长度
|
||
$fail_count = $redis->lLen($merchant_fail_key);
|
||
|
||
// 如果12小时内失败次数达到10次,临时停用该商户
|
||
if ($fail_count >= 10) {
|
||
// 商户临时停用标记
|
||
$disable_key = 'merchant:payment:disabled:' . $merchant_id;
|
||
|
||
// 记录停用信息
|
||
$disable_info = [
|
||
'merchant_id' => $merchant_id,
|
||
'disable_time' => time(),
|
||
'disable_reason' => '12小时内支付失败次数超过10次,系统自动临时停用',
|
||
'auto_disabled' => true
|
||
];
|
||
|
||
// 设置商户停用标记,默认停用12小时
|
||
$redis->set($disable_key, json_encode($disable_info));
|
||
$redis->expire($disable_key, 12 * 3600);
|
||
|
||
// 记录停用日志
|
||
Log::warning('商户支付功能临时停用: ' . json_encode([
|
||
'merchant_id' => $merchant_id,
|
||
'fail_count' => $fail_count,
|
||
'disable_time' => date('Y-m-d H:i:s'),
|
||
'expire_time' => date('Y-m-d H:i:s', time() + 12 * 3600)
|
||
], JSON_UNESCAPED_UNICODE));
|
||
}
|
||
}
|
||
|
||
return ['status' => 0, 'msg' => '支付失败:' . $error_message];
|
||
}
|
||
|
||
/**
|
||
* 客服发送消息
|
||
* @param mixed $user_id
|
||
* @param mixed $order_num
|
||
* @return array{status: int, data: array, msg: string}
|
||
*/
|
||
public function sendCustomerServiceMessage($user, $order_num): array
|
||
{
|
||
// 获取订单编号
|
||
$return_url = Request::param('return_url', '');
|
||
|
||
|
||
$message = "用户ID:{$user['id']},订单号:{$order_num}";
|
||
$order_title = '';
|
||
$order_price = 0;
|
||
if ($user['openid'] == null) {
|
||
return ['status' => 0, 'msg' => '用户openid为空'];
|
||
}
|
||
$orderNotify = OrderNotify::where([
|
||
'order_no' => $order_num,
|
||
])->find();
|
||
$extend = [];
|
||
if ($orderNotify) {
|
||
if ($orderNotify['extend'] != '') {
|
||
$extend = json_decode($orderNotify['extend'], true);
|
||
}
|
||
$order_title = $orderNotify['title'];
|
||
$order_price = $orderNotify['pay_amount'];
|
||
}
|
||
if ($order_title == "") {
|
||
return ['status' => 0, 'msg' => '订单号错误'];
|
||
}
|
||
|
||
// $this->appid
|
||
$web_domain = EnvHelper::getWebDomain();
|
||
$extend['returnUrl'] = $return_url;
|
||
$extend['openId'] = $user['openid'];
|
||
$extend['userId'] = $user['id'];
|
||
$extend_str = json_encode($extend, JSON_UNESCAPED_UNICODE);
|
||
|
||
$access_token = $this->get_access_token();
|
||
$request_url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token=" . $access_token;
|
||
$param = [
|
||
'touser' => $user['openid'],
|
||
'msgtype' => 'link',
|
||
'link' => [
|
||
'title' => $order_title,
|
||
'description' => "¥{$order_price}【点击去支付】",
|
||
'thumb_url' => 'https://image.zfunbox.cn/icon_80.png',
|
||
'url' => $web_domain . '/pages/other/web-pay-order?order_num=' . $order_num
|
||
]
|
||
];
|
||
$postData = json_encode($param, JSON_UNESCAPED_UNICODE);
|
||
$result = [];
|
||
for ($i = 0; $i < 5; $i++) {
|
||
$result = curlPost($request_url, $postData);
|
||
$result = json_decode($result, true);
|
||
if (isset($result['errcode']) && $result['errcode'] == 0) {
|
||
//发送成功,退出循环
|
||
$extend_str = json_encode($extend, JSON_UNESCAPED_UNICODE);
|
||
OrderNotify::where('order_no', $order_num)->update(['extend' => $extend_str]);
|
||
return ['status' => 1, 'data' => ''];
|
||
|
||
}
|
||
// 记录响应结果日志
|
||
writelog('post_order_log', json_encode([
|
||
'method' => 'sendCustomerServiceMessage',
|
||
'order_num' => $order_num,
|
||
'response' => $result,
|
||
'time' => date('Y-m-d H:i:s')
|
||
]));
|
||
//延迟一秒
|
||
sleep(1);
|
||
// usleep(500000);
|
||
}
|
||
//发送成功,退出循环
|
||
$extend_str = json_encode($extend, JSON_UNESCAPED_UNICODE);
|
||
OrderNotify::where('order_no', $order_num)->update(['extend' => $extend_str]);
|
||
return ['status' => 1, 'msg' => $result['errmsg'] ?? '发送失败'];
|
||
|
||
}
|
||
|
||
/**
|
||
* 发货
|
||
* @param mixed $openid
|
||
* @param mixed $access_token
|
||
* @param mixed $order_num
|
||
* @param mixed $title
|
||
* @return int
|
||
*/
|
||
public function post_order($user, $order_num): int
|
||
{
|
||
$msg = "本单购买商品已发放至[小程序盒柜]";
|
||
if (strpos($order_num, 'FH_') === 0) {
|
||
$msg = "本单购买的商品正在打包,请联系客服获取物流信息";
|
||
}
|
||
$openid = $user['openid'];
|
||
// 根据订单号设置正确的商户配置
|
||
$this->setMerchantByOrderNo($order_num);
|
||
$date = new \DateTime();
|
||
// $this->appid
|
||
$access_token = $this->get_access_token();
|
||
//订单发货时间
|
||
$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->getMchId() . '",
|
||
"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->getMchId(),
|
||
'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->wx_appid,
|
||
// 不存储access_token,因为可能过期
|
||
'order_num' => $order_num,
|
||
'title' => '订单发货',
|
||
'merchant' => $this->getMchId(),
|
||
'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;
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 生成URL链接
|
||
* @param int $userId 用户ID
|
||
* @return array
|
||
*/
|
||
public function generateUrlLink($userId): array
|
||
{
|
||
// 获取access_token
|
||
$access_token = $this->get_access_token();
|
||
|
||
$url = "https://api.weixin.qq.com/wxa/generate_urllink?access_token=" . $access_token;
|
||
$param = [
|
||
'path' => 'pages/index/index',
|
||
'query' => 'user_id=' . $userId,
|
||
// 'expire_type' => 0,
|
||
// 'expire_time' => time() + 7 * 86400, // 7天后过期
|
||
// 'env_version' => 'release'
|
||
];
|
||
|
||
$result = curlPost($url, json_encode($param));
|
||
$result = json_decode($result, true);
|
||
if (isset($result['errcode']) && $result['errcode'] != 0) {
|
||
|
||
return ['status' => 0, 'msg' => $result['errmsg'] ?? '生成链接失败'];
|
||
}
|
||
|
||
return ['status' => 1, 'data' => $result];
|
||
}
|
||
/**
|
||
* 生成URL链接
|
||
* @param string $path 路径
|
||
* @param string $query 查询参数
|
||
* @return array
|
||
*/
|
||
public function getUrlLink($path, $query, $env_version = "release"): array
|
||
{
|
||
// 获取access_token
|
||
$access_token = $this->get_access_token();
|
||
|
||
$url = "https://api.weixin.qq.com/wxa/generate_urllink?access_token=" . $access_token;
|
||
$param = [
|
||
'path' => $path,
|
||
'query' => $query,
|
||
'env_version' => $env_version
|
||
];
|
||
|
||
$result = curlPost($url, json_encode($param));
|
||
$result = json_decode($result, true);
|
||
if (isset($result['errcode']) && $result['errcode'] != 0) {
|
||
|
||
return ['status' => 0, 'msg' => $result['errmsg'] ?? '生成链接失败'];
|
||
}
|
||
|
||
return ['status' => 1, 'data' => $result['url_link']];
|
||
}
|
||
|
||
/**
|
||
* 获取手机号
|
||
* @param string $code 手机号获取凭证
|
||
* @return array
|
||
*/
|
||
public function getMobile($code = ''): array
|
||
{
|
||
if (empty($code)) {
|
||
return ['status' => 0, 'msg' => '缺少code参数'];
|
||
}
|
||
|
||
// 获取access_token
|
||
$access_token = $this->get_access_token();
|
||
|
||
$url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=" . $access_token;
|
||
$param = [
|
||
'code' => $code
|
||
];
|
||
|
||
$result = $this->post_curl_data($url, $param);
|
||
// $result = json_decode($result, true);
|
||
if (isset($result['errcode']) && $result['errcode'] != 0) {
|
||
return ['status' => 0, 'msg' => $result['errmsg'] ?? '获取手机号失败'];
|
||
}
|
||
|
||
return ['status' => 1, 'data' => $result['phone_info'] ?? []];
|
||
}
|
||
|
||
/**
|
||
* 获取openid
|
||
* @param string $code 登录凭证
|
||
* @return array
|
||
*/
|
||
public function getOpenid($code): array
|
||
{
|
||
if (empty($code)) {
|
||
return ['status' => 0, 'msg' => '缺少code参数'];
|
||
}
|
||
|
||
$url = "https://api.weixin.qq.com/sns/jscode2session?appid=" . $this->wx_appid . "&secret=" . $this->wx_secret . "&js_code=" . $code . "&grant_type=authorization_code";
|
||
$result = $this->get_curl_data($url);
|
||
|
||
if (isset($result['errcode']) && $result['errcode'] != 0) {
|
||
return ['status' => 0, 'msg' => $result['errmsg'] ?? '获取openid失败'];
|
||
}
|
||
|
||
return ['status' => 1, 'data' => $result];
|
||
}
|
||
|
||
/**
|
||
* 根据订单号设置商户环境
|
||
*
|
||
* @param string $orderNo 订单号
|
||
* @return bool 设置成功返回 true,失败返回 false
|
||
*/
|
||
public function setMerchantByOrderNo(string $orderNo): bool
|
||
{
|
||
if (strlen($orderNo) < 6) {
|
||
return false;
|
||
}
|
||
|
||
$merchantCode = substr($orderNo, 3, 3); // 截取商户号部分
|
||
|
||
// 获取配置中的商户列表
|
||
$weixinpay_setting = getConfig('weixinpay_setting');
|
||
|
||
if (empty($weixinpay_setting['merchants']) || !is_array($weixinpay_setting['merchants'])) {
|
||
return false;
|
||
}
|
||
|
||
// 查找对应的商户配置
|
||
$merchant = null;
|
||
foreach ($weixinpay_setting['merchants'] as $item) {
|
||
if ($item['order_prefix'] == $merchantCode) {
|
||
$merchant = $item;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (!$merchant) {
|
||
return false;
|
||
}
|
||
|
||
// 设置商户环境
|
||
$this->mp_merchant = $merchant;
|
||
$this->mch_id = $merchant['mch_id'];
|
||
$this->key = $merchant['keys'];
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
/**
|
||
* 获取access_token
|
||
*/
|
||
public function get_access_token()
|
||
{
|
||
return $this->get_access_appid_token($this->wx_appid, $this->wx_secret);
|
||
|
||
}
|
||
|
||
/**
|
||
* 获取access_token
|
||
*/
|
||
public function get_access_appid_token($appid, $wx_secret)
|
||
{
|
||
$redis = (new \app\common\server\RedisHelper())->getRedis();
|
||
$redis_key = 'wx_access_token:' . $appid;
|
||
|
||
$access_token_info = $redis->get($redis_key);
|
||
if ($access_token_info) {
|
||
$access_token_info = json_decode($access_token_info, true);
|
||
if ($access_token_info['access_token_time'] > time()) {
|
||
return $access_token_info['access_token'];
|
||
}
|
||
}
|
||
|
||
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" . $appid . "&secret=" . $wx_secret;
|
||
$res_access_token = $this->get_curl_data($url);
|
||
if (isset($res_access_token['errcode'])) {
|
||
throw new \Exception('获取access_token异常');
|
||
}
|
||
|
||
$access_token = $res_access_token['access_token'];
|
||
$expires_in = $res_access_token['expires_in'];
|
||
$access_token_time = time() + $expires_in;
|
||
|
||
$data = [
|
||
'access_token' => $access_token,
|
||
'access_token_time' => $access_token_time,
|
||
];
|
||
|
||
$redis->set($redis_key, json_encode($data), $expires_in - 1800);
|
||
|
||
return $access_token;
|
||
}
|
||
|
||
|
||
/**
|
||
* @param $url 请求链接
|
||
*/
|
||
public function get_curl_data($url)
|
||
{
|
||
$headerArray = array("Content-type:application/json;", "Accept:application/json");
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $url);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
|
||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headerArray);
|
||
$response = curl_exec($ch);
|
||
if ($response) {
|
||
curl_close($ch);
|
||
return json_decode($response, true);
|
||
} else {
|
||
$error = curl_errno($ch);
|
||
curl_close($ch);
|
||
return ['errcode' => 1];
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @param $url 链接
|
||
* @param $data 参数
|
||
* @return mixed
|
||
*/
|
||
public function post_curl_data($url, $data)
|
||
{
|
||
$data = json_encode($data);
|
||
$ch = curl_init();
|
||
curl_setopt($ch, CURLOPT_URL, $url);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||
curl_setopt($ch, CURLOPT_POST, 1);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
|
||
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||
'Content-Type: application/json; charset=utf-8',
|
||
'Content-Length: ' . strlen($data)
|
||
));
|
||
$return_content = curl_exec($ch);
|
||
if ($return_content) {
|
||
curl_close($ch);
|
||
$return_content = json_decode($return_content, true);
|
||
return $return_content;
|
||
} else {
|
||
$error = curl_errno($ch);
|
||
curl_close($ch);
|
||
return ['errcode' => 1];
|
||
}
|
||
}
|
||
|
||
// region 商户初始化相关代码
|
||
/**
|
||
* 获取当前商户信息(延迟加载)
|
||
*/
|
||
public function getMerchant(): ?array
|
||
{
|
||
if ($this->mp_merchant === null) {
|
||
$this->initMerchant(); // 第一次访问时才加载
|
||
}
|
||
return $this->mp_merchant;
|
||
}
|
||
|
||
/**
|
||
* 获取 mch_id(延迟加载)
|
||
*/
|
||
public function getMchId(): ?string
|
||
{
|
||
if ($this->mch_id === null) {
|
||
$this->initMerchant();
|
||
}
|
||
return $this->mch_id;
|
||
}
|
||
|
||
/**
|
||
* 切换到备选商户
|
||
* 当当前商户被停用时调用此方法
|
||
*/
|
||
private function switchToAlternativeMerchant(): void
|
||
{
|
||
if (
|
||
empty($this->mp_miniprogram) ||
|
||
empty($this->mp_miniprogram['merchants']) ||
|
||
!is_array($this->mp_miniprogram['merchants'])
|
||
) {
|
||
return;
|
||
}
|
||
|
||
$weixinpay_setting = getConfig('weixinpay_setting');
|
||
if (empty($weixinpay_setting['merchants']) || !is_array($weixinpay_setting['merchants'])) {
|
||
return;
|
||
}
|
||
|
||
// 当前被停用的商户ID
|
||
$current_disabled_mch_id = $this->mch_id;
|
||
$redis = (new \app\common\server\RedisHelper())->getRedis();
|
||
|
||
// 过滤出启用且未被临时停用的商户
|
||
$available_merchants = array_values(array_filter($weixinpay_setting['merchants'], function ($merchant) use ($redis, $current_disabled_mch_id) {
|
||
// 跳过当前被停用的商户
|
||
if ($merchant['mch_id'] == $current_disabled_mch_id) {
|
||
return false;
|
||
}
|
||
|
||
// 检查商户是否被启用且在小程序配置中
|
||
if (
|
||
isset($merchant['is_enabled'], $merchant['mch_id']) &&
|
||
$merchant['is_enabled'] === "1" &&
|
||
in_array($merchant['mch_id'], $this->mp_miniprogram['merchants'])
|
||
) {
|
||
|
||
// 检查商户是否被临时停用
|
||
$disable_key = 'merchant:payment:disabled:' . $merchant['mch_id'];
|
||
if (!$redis->exists($disable_key)) {
|
||
return true; // 商户可用
|
||
}
|
||
}
|
||
|
||
return false; // 商户不可用
|
||
}));
|
||
|
||
// 如果有可用商户,随机选择一个
|
||
if (!empty($available_merchants)) {
|
||
$merchant = WxPayHelper::getRandomMerchant($available_merchants);
|
||
$this->mp_merchant = $merchant;
|
||
$this->mch_id = $merchant['mch_id'];
|
||
$this->key = $merchant['keys'];
|
||
|
||
// 记录商户切换日志
|
||
Log::info('成功切换到备选商户: ' . json_encode([
|
||
'from_merchant' => $current_disabled_mch_id,
|
||
'to_merchant' => $this->mch_id,
|
||
'switch_time' => date('Y-m-d H:i:s')
|
||
], JSON_UNESCAPED_UNICODE));
|
||
} else {
|
||
// 没有可用的备选商户,记录警告日志
|
||
Log::warning('没有可用的备选商户,将继续使用当前商户: ' . $current_disabled_mch_id);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取 key(延迟加载)
|
||
*/
|
||
public function getKey(): ?string
|
||
{
|
||
if ($this->key === null) {
|
||
$this->initMerchant();
|
||
}
|
||
return $this->key;
|
||
}
|
||
|
||
public function GetPrefix(): ?string
|
||
{
|
||
if ($this->mp_merchant === null) {
|
||
$this->initMerchant();
|
||
}
|
||
return $this->mp_merchant['order_prefix'];
|
||
|
||
}
|
||
public function verify($order_no, $data): bool
|
||
{
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 实际加载商户信息
|
||
*/
|
||
private function initMerchant(): void
|
||
{
|
||
if (
|
||
empty($this->mp_miniprogram) ||
|
||
empty($this->mp_miniprogram['merchants']) ||
|
||
!is_array($this->mp_miniprogram['merchants'])
|
||
) {
|
||
return;
|
||
}
|
||
|
||
$weixinpay_setting = getConfig('weixinpay_setting');
|
||
if (empty($weixinpay_setting['merchants']) || !is_array($weixinpay_setting['merchants'])) {
|
||
return;
|
||
}
|
||
|
||
// 获取Redis实例检查临时禁用状态
|
||
$redis = (new \app\common\server\RedisHelper())->getRedis();
|
||
|
||
// 过滤掉被禁用的商户
|
||
$associatedMerchants = array_values(array_filter($weixinpay_setting['merchants'], function ($merchant) use ($redis) {
|
||
// 基本条件:商户已启用且在小程序配置中
|
||
$basicCondition = isset($merchant['is_enabled'], $merchant['mch_id']) &&
|
||
$merchant['is_enabled'] === "1" &&
|
||
in_array($merchant['mch_id'], $this->mp_miniprogram['merchants']);
|
||
|
||
// 如果不满足基本条件,直接排除
|
||
if (!$basicCondition) {
|
||
return false;
|
||
}
|
||
|
||
// 检查商户是否被临时禁用
|
||
$disable_key = 'merchant:payment:disabled:' . $merchant['mch_id'];
|
||
if ($redis->exists($disable_key)) {
|
||
// 商户被临时禁用,记录日志
|
||
$disabled_info = $redis->get($disable_key);
|
||
$disabled_info = $disabled_info ? json_decode($disabled_info, true) : [];
|
||
|
||
Log::info('加载商户时跳过被临时禁用的商户: ' . json_encode([
|
||
'merchant_id' => $merchant['mch_id'],
|
||
'disabled_time' => isset($disabled_info['disable_time']) ? date('Y-m-d H:i:s', $disabled_info['disable_time']) : '未知',
|
||
'reason' => $disabled_info['disable_reason'] ?? '未知原因'
|
||
], JSON_UNESCAPED_UNICODE));
|
||
|
||
return false; // 商户被禁用,排除
|
||
}
|
||
|
||
return true; // 商户可用
|
||
}));
|
||
|
||
// 如果没有可用的未禁用商户,则使用所有启用的商户(忽略临时禁用状态)
|
||
if (empty($associatedMerchants)) {
|
||
Log::warning('没有未禁用的可用商户,将加载所有启用的商户(忽略临时禁用状态)');
|
||
|
||
// 重新筛选商户,只考虑是否启用,忽略临时禁用状态
|
||
$associatedMerchants = array_values(array_filter($weixinpay_setting['merchants'], function ($merchant) {
|
||
return isset($merchant['is_enabled'], $merchant['mch_id']) &&
|
||
$merchant['is_enabled'] === "1" &&
|
||
in_array($merchant['mch_id'], $this->mp_miniprogram['merchants']);
|
||
}));
|
||
|
||
// 记录被禁用但仍将使用的商户
|
||
foreach ($associatedMerchants as $merchant) {
|
||
$disable_key = 'merchant:payment:disabled:' . $merchant['mch_id'];
|
||
$redis = (new \app\common\server\RedisHelper())->getRedis();
|
||
|
||
if ($redis->exists($disable_key)) {
|
||
$disabled_info = $redis->get($disable_key);
|
||
$disabled_info = $disabled_info ? json_decode($disabled_info, true) : [];
|
||
|
||
Log::warning('由于没有可用商户,将使用被临时禁用的商户: ' . json_encode([
|
||
'merchant_id' => $merchant['mch_id'],
|
||
'disabled_time' => isset($disabled_info['disable_time']) ? date('Y-m-d H:i:s', $disabled_info['disable_time']) : '未知',
|
||
'reason' => $disabled_info['disable_reason'] ?? '未知原因',
|
||
'time' => date('Y-m-d H:i:s')
|
||
], JSON_UNESCAPED_UNICODE));
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($associatedMerchants)) {
|
||
$merchant = WxPayHelper::getRandomMerchant($associatedMerchants);
|
||
$this->mp_merchant = $merchant;
|
||
$this->mch_id = $merchant['mch_id'];
|
||
$this->key = $merchant['keys'];
|
||
|
||
// 检查选中的商户是否被临时禁用
|
||
$disable_key = 'merchant:payment:disabled:' . $this->mch_id;
|
||
$redis = (new \app\common\server\RedisHelper())->getRedis();
|
||
$isDisabled = $redis->exists($disable_key);
|
||
|
||
// 记录选择的商户
|
||
Log::debug('已选择支付商户: ' . json_encode([
|
||
'merchant_id' => $this->mch_id,
|
||
'order_prefix' => $merchant['order_prefix'] ?? '',
|
||
'is_disabled' => $isDisabled ? '是(临时禁用被忽略)' : '否',
|
||
'time' => date('Y-m-d H:i:s')
|
||
], JSON_UNESCAPED_UNICODE));
|
||
} else {
|
||
// 没有可用商户,记录警告
|
||
Log::warning('没有可用的支付商户,请检查商户配置或临时禁用状态');
|
||
}
|
||
}
|
||
|
||
// endregion
|
||
|
||
|
||
/**
|
||
* 生成签名
|
||
* @return 签名
|
||
*/
|
||
public function MakeSign($params)
|
||
{
|
||
//签名步骤一:按字典序排序数组参数
|
||
ksort($params);
|
||
$string = $this->ToUrlParams($params);
|
||
//签名步骤二:在string后加入KEY
|
||
$string = $string . "&key=" . $this->getKey();
|
||
//签名步骤三: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;
|
||
}
|
||
|
||
|
||
/**
|
||
* 以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;
|
||
}
|
||
|
||
} |