cookie = $this->_cookie(); $this->id = $this->_id(); $this->os = $os; $this->token = $this->_token('', $os); } public function login() { } public function voucher($check_sn) { //https://lvyou.meituan.com/nib/trade/b/voucher/show?voucherCode=130949862550187&yodaReady=h5&csecplatform=4&csecversion=2.4.0 //https://lvyou.meituan.com/nib/trade/b/voucher/show?voucherCode=130949862550187&yodaReady=h5&csecplatform=4&csecversion=2.4.0 $res = $this->_curl('/nib/trade/b/voucher/show', [ 'voucherCode' => $check_sn, 'yodaReady' => 'h5', 'csecplatform' => '4', 'csecversion' => '2.4.0' ]); return $res; } /*public function get($page, $start = null, $end = null, $orderId = '') { if (empty($start) || empty($end)) { $start = date('Y-m-d 00:00:00'); $end = date('Y-m-d 23:59:59'); } $start = strtotime($start) * 1000; $end = strtotime($end) * 1000 + 999; if ($start < strtotime('2024-05-23') * 1000) { $this->totalPage = 1; return []; } $_list = []; foreach (array_keys(Orders::DouyinStatus) as $status) { $params = [ 'page_index' => $page, 'page_size' => 20, 'status' => $status, 'order_id' => '', 'sku_name' => '', 'last_phone' => '', 'category' => [ "third_category_list" => [] ] ]; $list = $this->_curl('/life/trip/v2/travel_agency/book/search?root_life_account_id='.$this->_id(), $params, 'POST'); if (empty($list) || $list->status_code != 0) { throw new \Exception("抖音拉单失败,Err:" . json_encode($list)); } if ($list && $list->status_code == 0 && !empty($list->order_list)) { foreach ($list->order_list as $order) { if (empty($cert)) { $cert = $this->_cert($order->order_info->order_id); if (empty($cert)) { throw new \Exception("抖音拉单{$order->order_info->order_id}详情获取失败"); } } $item = new Orders(); $item->os = 3; $item->sn = $order->order_info->order_id; $item->product_id = $order->order_info->product_id; $item->product_name = $order->order_info->product_name; $item->category_id = $order->order_info->product_sub_type; $item->create_at = $order->order_info->pay_time * 1000; $item->mobile = $order->order_info->buyer_phone; $item->travel_date = $order->book_info->book_start_date ?? null; $item->unit_price = $order->order_info->pay_amount; $item->total_price = $order->order_info->pay_amount; $item->actual_price = $order->order_info->pay_amount; $item->quantity = $order->book_child_info->rec_person_num; $item->order_status = $status; $item->asset_status = $cert->status ?? 0; $item->category_desc = $order->order_info->sku_category_name; $item->asset_price = $cert->status == 2 || $cert->status == 5 ? $cert->amount->verify_amount : 0; //抖音的核销金额需要查核销订单 if ($item->create_at < strtotime('2024-05-23') * 1000) { $this->totalPage = 1; break; } //if(mb_strrpos($item->product_name, "S") === false) continue; if (!(mb_strrpos($item->product_name, "S") || mb_strrpos($item->product_name, "G"))) continue; $_list[] = $item; } } } return $_list; //返回半成品 }*/ /** * @throws ModelNotFoundException * @throws DataNotFoundException * @throws DbException * @throws Exception */ public function get($page, $start = null, $end = null, $orderId = '') { if (empty($start) || empty($end)) { $start = date('Y-m-d 00:00:00'); $end = date('Y-m-d 23:59:59'); } $start = strtotime($start); $end = strtotime($end); // if ($start < strtotime('2024-05-22')) { // $this->totalPage = 1; // return []; // } if ($orderId) { $start = $end = null; } $_list = []; $list = $this->_certificateList($start, $end, $page, $orderId); if (empty($list->data->list) || $list->status_code !== 0) { $this->totalPage = 1; return $_list; } Log::info(date('Y-m-d', $start) . '--' . date('Y-m-d', $end) . " 抖音 page:{$page} total_count:" . $list->data->pagination->total_count . " page_count:" . $list->data->pagination->page_count ); foreach ($list->data->list as $order) { Log::info("抖音 订单号:{$order->order_id} \n\n"); $orderDetail = $this->_orderDetail($order->order_id, $order->status); if (empty($orderDetail)) { Log::info("抖音 订单详情拉取失败:{$order->order_id}"); throw new Exception("抖音拉单{$order->order_id}详情获取失败"); } $item = new Orders(); $item->os = $this->os; $item->sn = $order->order_id; $item->product_id = $order->sku->sku_id; $item->product_name = $order->sku->title; $item->category_id = $order->product_sub_type; $item->create_at = $order->pay_time * 1000; $item->mobile = $orderDetail->order_info->buyer_phone ?? null; $item->travel_date = $orderDetail->book_info->book_start_date ?? null; $item->unit_price = $order->amount->pay_amount; $item->total_price = $order->amount->pay_amount; $item->actual_price = $order->amount->pay_amount; $item->quantity = $orderDetail->book_child_info->rec_person_num ?? null; $item->order_status = $order->status; $item->asset_status = $order->status ?? 0; $item->category_desc = $order->sku->title; $item->asset_price = $order->amount->verify_amount ?? 0; //抖音的核销金额需要查核销订单 if ($item->create_at < strtotime('2024-05-22') * 1000) { $this->totalPage = 1; break; } // $this->totalPage = intval($list->data->pagination->total_count / $list->data->pagination->page_count); // if (($list->data->pagination->total_count % $list->data->pagination->page_count) > 0) { // $this->totalPage += 1; // } //if(mb_strrpos($item->product_name, "S") === false) continue; G 达人 S 自己 // if (!(mb_strrpos($item->product_name, "S") || mb_strrpos($item->product_name, "G") || mb_strrpos($item->product_name, "甄"))) continue; $kw_match = false; foreach ($this->keywords as $kw) { if (mb_strpos($item->product_name, $kw) !== false) { $kw_match = true; break; } } if ($this->os == 3 && !$kw_match) { printf("skip order: %s %s\n", $item->sn, $item->product_name); continue; } $_list[] = $item; } return $_list; //返回半成品 } /** * @throws Exception */ public function _certificateList($start, $end, $page, $orderId) { $params = [ "filter" => [ "start_time" => $start,//开始时间 "end_time" => $end,//结束时间 时间范围限定一个月 "is_market" => false,//必带 /*"status" => 1,//不传 全部,1未核销 2已核销 3申请退款中 4已退款 5部分核销 "product_option" => [],// 1团购/代金券 13预售券 15次卡 18通兑券/品 26有价券包 1303日历票 1305日历房 1306演出预订 1309日历品 2201团购|线上约 "is_combo_product" => true,//是否组合品 "order_id" => "1061241582949143669",//订单号 "sku_id" => "7379541565490675752" //产品ID*/ ] ]; if ($orderId) { $params['filter']['order_id'] = $orderId; } // Log::error('===_certificateList查询page='.$page.'时间:'.date('Y-m-d H:i:s',$start).'--'.date('Y-m-d H:i:s',$end).'===='); $this->_getRetriesLock(); $list = $this->_curl("/life/trip/fulfilment/v1/query/certificate_list?end_time={$end}&page_index={$page}&page_size=50&start_time={$start}&root_life_account_id=" . $this->_id(), $params, 'POST'); if (empty($list) || $list->status_code != 0) { $this->_getRetriesLock(); $list = $this->_curl("/life/trip/fulfilment/v1/query/certificate_list?end_time={$end}&page_index={$page}&page_size=50&start_time={$start}&root_life_account_id=" . $this->_id(), $params, 'POST'); if (empty($list) || $list->status_code != 0) { Log::error('===查询时间:' . $start . '--' . $end . '====certificate_list: ' . json_encode($list)); throw new Exception("抖音拉单失败,Err:" . json_encode($list)); } } // Log::warning('====list=====',['list'=>$list->data->pagination]); return $list; } public function _getRetriesLock(): bool { $pattern = 'php webman spider:dy'; $maxProcesses = 8; $this->_killMiddleProcesses($pattern, $maxProcesses); $maxRetries = 5; $retries = 0; while ($retries < $maxRetries) { $back = Redis::set('SpiderMt:CertificateList:lock', time(), 'EX', 1, 'NX'); if ($back) { break; } $retries++; sleep(1); } return true; } public function _killMiddleProcesses($pattern, $maxProcesses) { // 检查操作系统是否为 Linux if (PHP_OS !== 'Linux') { return false; } // 获取当前进程的 PID $currentPid = getmypid(); // 获取进程列表 $command = "ps aux | grep '$pattern' | grep -v grep | grep -v '$currentPid' | awk '{print $2,$10}'"; $output = []; exec($command, $output); $processList = []; foreach ($output as $line) { list($pid, $runtime) = explode(' ', $line); $runtimeParts = explode(':', $runtime); $hours = intval($runtimeParts[0]); $minutes = intval($runtimeParts[1]); $totalMinutes = $hours * 60 + $minutes; $processList[] = ['pid' => $pid, 'runtime' => $totalMinutes]; } // 按运行时间排序 usort($processList, function ($a, $b) { return $a['runtime'] <=> $b['runtime']; }); // 过滤掉当前 PID 和运行时间最短的 PID if (!empty($processList)) { array_shift($processList); // 移除运行时间最短的 } $processList = array_filter($processList, function ($process) use ($currentPid) { return $process['pid'] != $currentPid; }); $processCount = count($processList); // 如果进程数量超过最大值,则终止中间部分的进程 if ($processCount > $maxProcesses) { $processesToKill = $processCount - $maxProcesses; $processesKilled = 0; // 杀死运行时间最短的进程 foreach ($processList as $process) { if ($processesKilled >= $processesToKill) { break; } exec("kill -9 {$process['pid']}"); $processesKilled++; Log::info("Killed process with PID: {$process['pid']}\n"); } if ($processesKilled === 0) { Log::info( "No processes to kill.\n"); } } else { Log::info( "Process count is not over the limit.\n"); } return true; } public function _orderDetail($orderId = null, $status = null) { $orderDetail = $this->_curl("/life/trip/v2/travel_agency/book/detail", [ 'order_id' => $orderId, 'book_order_id' => '', 'status' => $status, 'root_life_account_id' => $this->_id($this->os) ]); return $orderDetail->data ?? null; } /** * @param string $orderId * @return array|null */ public function _orderMakeAppointmentStatus(string $orderId) { if (empty($orderId)) return []; $params = [ 'page_index' => 1, 'page_size' => 50, 'status' => 3, 'order_id' => $orderId, 'sku_name' => '', 'last_phone' => '', 'category' => [ "third_category_list" => [] ] ]; $orderBookingManagement = $this->_curl('/life/trade_view/v1/travel_agency/book/search?root_life_account_id=' . $this->_id(), $params, 'POST'); return $orderBookingManagement->order_list ?? null; } public function _orderBookingManagement(string $orderId) { if (empty($orderId)) return []; foreach (array_keys(Orders::DouyinReservationStatus) as $status) { $params = [ 'page_index' => 1, 'page_size' => 50, 'status' => $status, 'order_id' => $orderId, 'sku_name' => '', 'last_phone' => '', 'category' => [ "third_category_list" => [] ] ]; $orderBookingManagement = $this->_curl('/life/trip/v2/travel_agency/book/search?root_life_account_id=' . $this->_id(), $params, 'POST'); if (!empty($orderBookingManagement->order_list)) break; } return $orderBookingManagement->order_list ?? null; } public function _cert($order_id = null) { $info = $this->_curl('/life/trip/fulfilment/v1/query/certificate_list?root_life_account_id=' . $this->_id(), [ 'filter' => [ "start_time" => strtotime(date('Y-m-d 00:00:00', time() - 5 * 24 * 3600)), "end_time" => strtotime(date('Y-m-d 23:59:59')), "is_market" => false, "order_id" => $order_id ] ], 'POST'); if ($info->status_code != 0 || empty($info->data->list)) return []; $i = $info->data->list[0]; if (empty($i)) return []; $more = $this->_curl('/life/trip/fulfilment/v1/query/certificate_by_id/', [ 'certificate_id' => $i->certificate_id, 'root_life_account_id' => $this->_id() ] ); if ($more->status_code == 0) return $more->data->certificate ?? []; return []; } public function _cookie($data = '') { $key = 'Douyin:cookie'; if ($this->os == 5) { $key = 'Douyin:cookie-' . $this->os; } if ($data) Redis::set($key, $data); return Redis::get($key); } public function _id($id = '') { if ($this->os == 5) { return '7399206501845403686'; } else { // return '7259680722519066679'; return Redis::get('Douyin:id'); } } protected $_try = 0; public function _token($token = '') { $dyKey = sprintf('Douyin:token-%s', $this->os); if ($token) Redis::set($dyKey, $token, 'ex', 3600 * 24 - 50); $token = Redis::get($dyKey); if (empty($token) && $this->_cookie()) { if ($this->_try > 3) { throw new Exception("cookie 失效"); } $id = $this->_curl('/life/gate/v1/user/detail', null, 'HEAD'); $this->_try++; if ($id) { Redis::set($dyKey, $id, 'ex', 3600 * 24 - 50); } } return Redis::get($dyKey); } public function _curl($url, $params, $method = 'GET') { // 请求次数统计 $key = sprintf("Douyin:ReqiestCount:%s", date('Y-m-d')); $requestCount = Redis::incrBy($key, 1); if ($requestCount == 1) { Redis::expire($key, 604800); // 7天 } $cookiePath = sprintf(runtime_path() . '%s_dy_cookie.txt', $this->os); $http = $this->gate . $url; if ($method == 'GET') { $http = $http . '?' . http_build_query($params); } $ch = curl_init($http); $header = [ 'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36', 'Sec-Ch-Ua-Platform: "Windows"', 'Host: life.douyin.com', 'Sec-Ch-Ua: "Google Chrome";v="123", "Not:A-Brand";v="8", "Chromium";v="123"', 'Accept-Language: zh-CN,zh;q=0.9', 'Content-type: application/json' ]; if ($method == 'GET' || $method == 'POST') { $header[] = 'x-secsdk-csrf-token: ' . $this->_token('', $this->os); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); } else { curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_COOKIEJAR, $cookiePath); $header[] = 'x-secsdk-csrf-request: 1'; } curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_COOKIE, $this->_cookie('')); curl_setopt($ch, CURLOPT_HTTPHEADER, $header); curl_setopt($ch, CURLOPT_COOKIEFILE, $cookiePath); if ($method == 'POST') { curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params)); } $body = curl_exec($ch); curl_close($ch); if (!in_array($method, ['GET', 'POST'])) { $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE); $header = substr($body, 0, $header_size); $in = explode("\n", $body); foreach ($in as $i) { if (stripos($i, 'x-ware-csrf-token') !== false) { $inn = explode(',', $i); return $inn[1]; } } return null; } $res = json_decode($body); if (!isset($res['status_code']) || $res['status_code'] != 0) { Log::info("抖音 os:{$this->os} \n\n"); Log::info( "抖音 body:{$body} \n\n"); (new ThirdApiService())->weComNotice($this->os); } return $res; } }