// +---------------------------------------------------------------------- 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®ions="); $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; } }