Files
2026-09-01 22:15:50 +08:00

490 lines
17 KiB
PHP

<?php
// +----------------------------------------------------------------------
// | 萤火商城系统 [ 致力于通过产品和服务,帮助商家高效化开拓市场 ]
// +----------------------------------------------------------------------
// | Copyright (c) 2017~2023 https://www.yiovo.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed 这不是一个自由软件,不允许对程序代码以任何形式任何目的的再发行
// +----------------------------------------------------------------------
// | Author: 萤火科技 <admin@yiovo.com>
// +----------------------------------------------------------------------
declare (strict_types=1);
namespace app\api\model;
use app\common\model\Code as CodeModel;
use app\api\model\Order as OrderModel;
use app\common\library\helper;
use cores\exception\BaseException;
use think\facade\Cache;
use think\Request;
/**
* 订单模型
* Class Order
* @package app\api\model
*/
class Code extends CodeModel
{
/**
* 隐藏字段
* @var array
*/
protected $hidden = [
'store_id',
'update_time'
];
// 信息提示
private $message = '';
/**
* 等待支付产码列表
* @param $zone
* @return array
* @throws \think\exception\DbException
*/
public function getList($zone=1)
{
// 获取数据列表->where(" code_id in (select code_id from yoshop_order where order_status = 10) ")
return $this->where('status', '=', 2)
->where('order_id', '=', 0)
->where('zone', '=', $zone)
->order(['allot_time' => 'asc'])
->select()
->toArray();
}
/**
* 等待支付产码列表
* @param $page
* @return array
* @throws \think\exception\DbException
*/
/*public function getList($page=1)
{
// 获取数据列表
return $this->where('status', '=', 2)
->where('order_id', '=', 0)
->where(" code_id in (select code_id from yoshop_order where order_status = 10) ")
->order(['allot_time' => 'asc'])
->page($page, 20)
->select()
->toArray();
}*/
/**
* 等待支付产码列表倒序
* @param $page
* @return array
* @throws \think\exception\DbException
*/
public function getListDesc($page=1)
{
// 获取数据列表
return $this->where('status', '=', 2)
->where('order_id', '=', 0)
->where(" code_id in (select code_id from yoshop_order where order_status = 10) ")
->order(['allot_time' => 'desc'])
->page($page, 20)
->select()
->toArray();
}
/**
* 获取非失效码
* @return \think\Paginator
* @throws \think\exception\DbException
*/
public function getNonFailure()
{
// 获取数据列表
return $this->where('status != 3 ')
->where('order_id', '=', 0)
->where(" (((allot_time+360) < unix_timestamp(NOW()) and allot_time > 0) or (create_time+expiration_time+60) < unix_timestamp(NOW())) ")
->order(['allot_time' => 'asc'])
->paginate(1000);
}
/**
* 支付码
* @param $money
* @param $passageway
* @return null|static
* @throws \app\common\exception\BaseException
* @throws \think\exception\DbException
*/
public function getCodeDetailOld($money,$passageway)
{
//连接本地的 Redis 服务
// 字符串操作
$conn = new \think\cache\driver\Redis(config('cache.stores.redis'));
$codesIp = $conn->get('codesIp');
$codesPort = $conn->get('codesPort');
$codeNum = $conn->get('codeNum');
if(empty($codesIp) || empty($codeNum) || $codeNum <= 0){
//获取IP组
$iPGroup = $this->getIPGroup();
$codesIp = isset($iPGroup[0]['ip'])?$iPGroup[0]['ip']:'';
$codesPort = isset($iPGroup[0]['port'])?$iPGroup[0]['port']:'';
$codeNum = 20;
$conn->set('codesIp',$codesIp);
$conn->set('codesPort',$codesPort);
}
$filter = [];
$codeM = new CodeModel();
$zone = 0; //分区
//支付码未过期,并且过期剩余时间超过5分钟,并且退回码池超过2分钟
$where = " (create_time+expiration_time) > unix_timestamp(NOW()) and (create_time+expiration_time) - unix_timestamp(NOW()) > 300 and (back_time+120) < unix_timestamp(NOW()) ";
$filter[] = ['passageway', '=', $passageway]; //通道
$filter[] = ['status', '=', 1]; //未使用状态
$filter[] = ['state', '=', 1]; //启用状态
$filter[] = ['money', '=', $money]; //金额
$code = [];
//尝试三次
for ($i=0;$i<3;$i++) {
//随机起点
$offset = $this->field([
'FLOOR(RAND() * COUNT(*)) AS `offset`',
])->where($where)->where(
$filter
)->find();
$code = $this->field([
'order_no as order_no_sn',
'shop_id',
'code_id',
'store_user_id',
'search_url',
'link',
'alipay',
'passageway',
'num',
])->where($where)->where(
$filter
)->limit((int)$offset['offset'], 1)->select();
$code = isset($code[0]) ? $code[0] : [];
//未查到进入下一次尝试
if(!$code){
continue;
}
//查询队列
//if(!$this->checkQueue($code['code_id'])) continue;
//查询支付状态
if($passageway == 'ZFBH5'){
$res = $this->checkStatus(htmlspecialchars_decode($code['search_url']), $passageway, '订单未支付');
if ($res === false) { //订单不是未支付状态
continue;
}
}else{
if(!empty($codesIp)) {
$conn->set('codeNum',$codeNum-1);
$res = $this->checkStatusProxy(htmlspecialchars_decode($code['search_url']), $passageway, $codesIp, $codesPort, '该订单已完成支付');
}else {
$res = $this->checkStatus(htmlspecialchars_decode($code['search_url']), $passageway, '该订单已完成支付');
}
if ($res !== false) { //已支付 重新匹配
continue;
}
}
for ($j = 1; $j <= 6; $j++) {
$num = $this->getZoneCodeTotal(['status' => '2', 'order_id' => '0', 'zone' => $j]);
if ($num < 20) {
$zone = $j;
break 2;
}
}
}
if(!$code || $zone==0) {
throw new BaseException(['message' => '匹配支付码失败']);
}
$code['zone'] = $zone;
$codeM->update([
'num' => $code['num']+1,
'status' => 2,
'zone' => $zone,
'allot_time' => time(),
],['code_id'=>$code['code_id']]);
return $code;
}
/**
* 支付码
* @param $money
* @param $passageway
* @return null|array
* @throws \app\common\exception\BaseException
* @throws \think\exception\DbException
*/
public function getCodeDetail($money,$passageway)
{
if(empty($money) || empty($passageway)) {
throw new BaseException(['message' => '参数错误']);
}
$money = round($money,2);
//组成队列名称
$queue_name = $passageway.'_'.$money;
//连接本地的 Redis 服务
// 字符串操作-获取代理IP
/*$codesIp = Cache::store('redis')->get('codesIp');
$codesPort = Cache::store('redis')->get('codesPort');
$codeNum = Cache::store('redis')->get('codeNum');
if(empty($codesIp) || empty($codeNum) || $codeNum <= 0){
//获取IP组
$iPGroup = $this->getIPGroup();
$codesIp = isset($iPGroup[0]['ip'])?$iPGroup[0]['ip']:'';
$codesPort = isset($iPGroup[0]['port'])?$iPGroup[0]['port']:'';
$codeNum = 20;
Cache::store('redis')->set('codesIp',$codesIp);
Cache::store('redis')->set('codesPort',$codesPort);
}*/
// 获取分布式锁
$lock_key = $queue_name . '_lock';
$max_wait_time = 10; // 最大等待时间(秒)
$retry_interval = 100; // 重试间隔时间(毫秒)
$lock_ttl = 40; // 锁定时间(秒)
$lock_count = 0; // 锁的重试次数
// 获取分布式锁
$redis = Cache::store('redis');
$lock = false;
$wait_time = 0;
$lock_value = '';
while ($wait_time <= $max_wait_time) {
$lock_value = uniqid(); // 生成唯一的锁值
if ($redis->set($lock_key, $lock_value, 'EX', $lock_ttl, 'NX')) {
$lock = true;
break;
} else {
// 获取锁失败,等待一段时间后重试
$lock_count++;
usleep($retry_interval * 1000);
$wait_time += ($retry_interval / 1000);
if ($lock_count > 20) {
// 重试次数超过限制,抛出异常
throw new BaseException(['message' => '匹配支付码失败,超出重试次数']);
}
}
}
// 获取队列的长度
$queue_length = $redis->lLen($queue_name);
//数组初始化
$codeM = new CodeModel();
$zone = 0; //分区
$code = [];
// 获取锁成功
if ($lock && $queue_length > 0) {
try {
// TODO: 需要加锁的业务逻辑
// 开始匹配
for ($i = 0; $i < $queue_length; $i++) {
// 从队列中取出元素
$timeout = 0; // 不需要设置等待时间
$result_data = $redis->brpop($queue_name, $timeout);
// 反序列化产码信息
$code_data = json_decode($result_data[1],true);
// 判断码是否已经过期
if (time() > $code_data['expire_time']) {
continue;
}
if (empty($code_data['code_id'])) {
continue;
}
//匹配成功处理逻辑
$code = CodeModel::detail($code_data['code_id']);
//查询支付状态
if($passageway == 'ZFBH5'){
$res = $this->checkStatus(htmlspecialchars_decode($code['search_url']), $passageway, '订单未支付');
if ($res === false) { //订单不是未支付状态
continue;
}
}
/*else{
if(!empty($codesIp)) {
$redis->set('codeNum',$codeNum-1);
$res = $this->checkStatusProxy(htmlspecialchars_decode($code['search_url']), $passageway, $codesIp, $codesPort, '该订单已完成支付');
}else {
$res = $this->checkStatus(htmlspecialchars_decode($code['search_url']), $passageway, '该订单已完成支付');
}
if ($res !== false) { //已支付 重新匹配
continue;
}
}*/
for ($j = 1; $j <= 6; $j++) {
$num = $this->getZoneCodeTotal(['status' => '2', 'order_id' => '0', 'zone' => $j]);
if ($num < 20) {
$zone = $j;
break 2;
}
}
}
} finally {
// 释放锁
$lock_value_in_redis = $redis->get($lock_key);
if ($lock_value_in_redis === $lock_value) { // 如果当前锁的值和刚才设置的值相等,则说明当前线程持有该锁,可以进行释放
$redis->del($lock_key); // 删除锁
}else{
//throw new BaseException(['message' => '匹配支付码失败,锁竞争']);
}
}
}
if(!$code || $zone == 0) {
throw new BaseException(['message' => '匹配支付码失败']);
}
$code['zone'] = $zone;
$code['order_no_sn'] = $code['order_no'];
$codeM->update([
'num' => $code['num']+1,
'status' => 2,
'zone' => $zone,
'allot_time' => time(),
],['code_id'=>$code['code_id']]);
return $code;
}
public function checkStatus($url,$passageway,$text='该订单已完成支付'){
if(in_array($passageway,['WX','SNZFBH5'])){
$ch = curl_init();
curl_setopt ($ch, CURLOPT_URL, $url);
curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt ($ch, CURLOPT_CONNECTTIMEOUT,10);
curl_setopt ($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt ($ch, CURLOPT_MAXREDIRS, 3);
$content = curl_exec($ch);
}else {
$publicPath = app()->getRootPath() . 'public/';
$escaped_url = escapeshellarg($url);
$command = "node {$publicPath}myscript.js {$escaped_url} 2>&1";
$content = shell_exec($command);
}
$res = strpos((string)$content,$text);
return $res;
}
/**
* 按条件修改支付码状态为失效
* @param array $field
* @param array $filter
* @return false|int
*/
public function setStatusLose($field,$filter)
{
return $this->update(array_merge(['status' => 3],$field),$filter) !== false;
}
/**
* 等待支付产码列表
* @param $page
* @return array
* @throws \think\exception\DbException
*/
public function getSx($page=1)
{
// 获取数据列表
return $this->where('status', '=', 1)
->where(" num > 0 ")
->order(['back_time' => 'asc'])
->page($page, 20)
->select()
->toArray();
}
public function getIPGroup(){
$html = @file_get_contents("http://http.tiqu.letecs.com/getip3?num=1&type=2&pro=&city=0&yys=0&port=1&time=1&ts=0&ys=0&cs=0&lb=1&sb=0&pb=4&mr=1&regions=");
$result = json_decode($html,true);
$ips = is_array($result['data'])?$result['data']:[];
return $ips;
}
public function checkStatusProxy($targetUrl,$passageway,$ip,$port,$text='该订单已完成支付'){
if(in_array($passageway,['WX','SNZFBH5'])){
// 代理服务器
$proxyServer = "http://" . $ip . ":" . $port;
// 隧道身份信息
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $targetUrl);
curl_setopt($ch, CURLOPT_HTTPPROXYTUNNEL, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// 设置代理服务器
//curl_setopt($ch, CURLOPT_PROXYTYPE, 0); //http
curl_setopt($ch, CURLOPT_PROXYTYPE, 5); //sock5
curl_setopt($ch, CURLOPT_PROXY, $proxyServer);
// 设置隧道验证信息
curl_setopt($ch, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727;)");
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
curl_setopt($ch, CURLOPT_HEADER, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($ch);
curl_close($ch);
}else{
$publicPath = app()->getRootPath() . 'public/';
$escaped_url = escapeshellarg($targetUrl);
//$command = "node {$publicPath}myscript_proxy.js {$escaped_url} {$ip} {$port} 2>&1";
$command = "node {$publicPath}myscript.js {$escaped_url} 2>&1";
$result = shell_exec($command);
}
$res = strpos((string)$result,$text);
return $res;
}
/**
* 获取分区产码总数量
* @param array $filter
* @return int|string
*/
public function getZoneCodeTotal($filter = [])
{
return $this->where($filter)->count();
}
/**
* 插入队列判断当前是否可执行
* @param $code_id
* @return
*/
public function checkQueue($code_id)
{
$message = Cache::store('redis')->lPop("queue:$code_id");
// 如果队列为空,则写入队列
if ($message === null || $message === false) {
// 消息内容
$message = 1;
// 监控客户的消息 Hash
Cache::store('redis')->watch("code:$code_id");
// 开始事务
Cache::store('redis')->multi();
// 将消息添加到队列末尾
Cache::store('redis')->rPush("queue:$code_id", $message);
// 提交事务
$result = Cache::store('redis')->exec();
if ($result !== null && $result !== false) {
return true;
}
}
return false;
}
}