HaniBlindBox/server/php/app/common/server/platform/MiniProgramPlatform.php
2026-01-01 20:46:07 +08:00

1115 lines
38 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
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;
}
}