travel/service/app/server/Douyin.php

594 lines
21 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\server;
use app\model\Orders;
use Exception;
use support\Log;
use support\Redis;
use think\db\exception\DataNotFoundException;
use think\db\exception\DbException;
use think\db\exception\ModelNotFoundException;
class Douyin
{
private $cookie = '';
private $id = '';
private $os = '';
private $token = '';
private $gate = 'https://life.douyin.com';
public $totalPage = 6;
private $keywords = ['泰国', '普吉岛', '西藏', 'S', 'G', 's', 'g'];
public function __construct($os = 3)
{
$this->cookie = $this->_cookie();
$this->id = $this->_id();
$this->os = $os;
$this->token = $this->_token('', $os);
Log::info("抖音 token{$this->token},os:{$this->os} \n\n"); ;
}
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) {
echo "抖音 订单号:{$order->order_info->order_id} \n\n";
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 = '', $osId = 3)
{
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, $osId);
if (empty($list->data->list) || $list->status_code !== 0) {
$this->totalPage = 1;
return $_list;
}
echo 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 . " \n\n";
foreach ($list->data->list as $order) {
echo "抖音 订单号:{$order->order_id} \n\n";
$orderDetail = $this->_orderDetail($order->order_id, $order->status);
if (empty($orderDetail)) {
throw new Exception("抖音拉单{$order->order_id}详情获取失败");
}
$item = new Orders();
$item->os = $osId;
$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 (!$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, $osId = '')
{
$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($osId), $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($osId), $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') {
echo "当前系统不是 Linux跳过执行。\n";
return false;
}
// 获取当前进程的 PID
$currentPid = getmypid();
// 获取进程列表
$command = "ps aux | grep '$pattern' | grep -v grep | grep -v '$currentPid' | awk '{print $2,$10}'";
$output = [];
exec($command, $output);
$processCount = count($output);
// 如果进程数量超过最大值,则终止中间部分的 x-12 个进程
if ($processCount > $maxProcesses) {
$processesToKill = $processCount - $maxProcesses;
$processesKilled = 0;
// 找到运行时间最短的不包含自己的进程并终止它
foreach ($output as $line) {
list($pid, $runtime) = explode(' ', $line);
$runtimeParts = explode(':', $runtime);
$hours = intval($runtimeParts[0]);
$minutes = intval($runtimeParts[1]);
$totalMinutes = $hours * 60 + $minutes;
if ($pid !== $currentPid) {
exec("kill -9 $pid");
$processesKilled++;
echo "Killed process with PID: $pid\n";
if ($processesKilled >= $processesToKill) {
break;
}
}
}
if ($processesKilled === 0) {
echo "No processes to kill.\n";
}
} else {
echo "Process count is not over the limit.\n";
}
return true;
}*/
public function _killMiddleProcesses($pattern, $maxProcesses)
{
// 检查操作系统是否为 Linux
if (PHP_OS !== 'Linux') {
echo "当前系统不是 Linux跳过执行。\n";
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++;
echo "Killed process with PID: {$process['pid']}\n";
}
if ($processesKilled === 0) {
echo "No processes to kill.\n";
}
} else {
echo "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()
]);
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 ($id == 5) {
return '7399206501845403686';
} else {
// return '7259680722519066679';
return Redis::get('Douyin:id');
}
}
protected $_try = 0;
public function _token($token = '', $os = 3)
{
$dyKey = sprintf('Douyin:token-%s', $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 ($this->os) {
echo "抖音 os{$this->os} \n\n";
echo "抖音 订单url{$url} \n\n";
echo "抖音 订单cookie{$this->_cookie()} \n\n";
echo "抖音 body{$body} \n\n";
}
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;
}
return json_decode($body);
}
}