完整版V1 加入爬虫功能
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
// A2c云控
|
||||
|
||||
require_once __DIR__ . '/AbstractScrmSpider.class.php';
|
||||
|
||||
class A2c extends AbstractScrmSpider
|
||||
{
|
||||
const API_LIST = '/api/talk/counter/share/record/list'; // 列表API地址
|
||||
const API_DETAILS = '/api/talk/counter/share/detail'; // 详情API地址
|
||||
const API_COUNT = ''; // 总数API地址
|
||||
const DEFAULT_PER_PAGE_COUNT = 20; // List默认每页显示的数量
|
||||
private $pageUrl;
|
||||
private $account;
|
||||
private $password;
|
||||
private $unifiedData;
|
||||
|
||||
// 实例化时动态传入账号和密码
|
||||
public function __construct($pageUrl, $account, $password, $nodeHost = 'http://127.0.0.1:3001')
|
||||
{
|
||||
parent::__construct($nodeHost);
|
||||
$this->account = $account;
|
||||
$this->password = $password;
|
||||
$this->pageUrl = $pageUrl;
|
||||
|
||||
$this->unifiedData = new UnifiedScrmData();
|
||||
}
|
||||
|
||||
protected function getSpiderConfig()
|
||||
{
|
||||
return [
|
||||
'pageUrl' => $this->pageUrl,
|
||||
'apiUrls' => [self::API_LIST, self::API_DETAILS],
|
||||
|
||||
// 明确指派角色
|
||||
'listApi' => self::API_LIST, // 必须
|
||||
'detailApi' => self::API_DETAILS, // 选填
|
||||
|
||||
'listMethod' => 'POST',
|
||||
|
||||
'paginationMode' => self::MODE_UI,
|
||||
|
||||
'authActions' => [
|
||||
// ['type' => 'type', 'selector' => 'input[type="password"]', 'value' => $this->password],
|
||||
// ['type' => 'type', 'selector' => '#username_input', 'value' => $this->account],
|
||||
// ['type' => 'press', 'key' => 'Enter'],
|
||||
|
||||
['type' => 'wait', 'ms' => 2000]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 只负责解析 List 的总页数
|
||||
protected function extractListTotalPages($listFirstPageData, $countData = null)
|
||||
{
|
||||
$default_per_page_count = self::DEFAULT_PER_PAGE_COUNT;
|
||||
$this->unifiedData->total = $listFirstPageData['data']['total'];
|
||||
|
||||
if($listFirstPageData['data']['total'] <= $default_per_page_count) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ceil($listFirstPageData['data']['total']/$default_per_page_count);
|
||||
}
|
||||
|
||||
// 只负责组装 List 的翻页参数
|
||||
protected function buildListPageParams($page)
|
||||
{
|
||||
return ['page' => $page, 'pageSize' => self::DEFAULT_PER_PAGE_COUNT];
|
||||
}
|
||||
|
||||
// 提供 List 的下一页按钮信息
|
||||
protected function getUiPaginationConfig()
|
||||
{
|
||||
return [
|
||||
'nextBtnSelector' => '.btn-next',
|
||||
'waitMs' => 2000
|
||||
];
|
||||
}
|
||||
|
||||
// 清爽至极的数据清洗:详情是详情,列表是列表
|
||||
protected function parseToUnifiedData($detailData, $allListPagesData)
|
||||
{
|
||||
$unifiedData = $this->unifiedData;
|
||||
|
||||
// 1. 如果捕获到了详情数据,提取今日新增
|
||||
if ($detailData) {
|
||||
$unifiedData->todayNewCount = (int)($detailData['data']['newFollowersToday'] ?? 0);
|
||||
}
|
||||
|
||||
// 2. 循环合并了所有页数的 List 数组
|
||||
foreach ($allListPagesData as $pageRaw) {
|
||||
$records = $pageRaw['data']['rows'] ?? [];
|
||||
foreach ($records as $item) {
|
||||
if(!empty($item['account'])) {
|
||||
$number = $item['account'] ?? null;
|
||||
$isOnline = (isset($item['numberStatus']) && $item['numberStatus'] == 1);
|
||||
|
||||
$unifiedData->addNumber($number, $isOnline, $item['newFollowersToday']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $unifiedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 美化打印变量(不终止程序)
|
||||
* 支持传入多个变量,如:dump($var1, $var2);
|
||||
*/
|
||||
if (!function_exists('dump')) {
|
||||
function dump(...$vars) {
|
||||
// 定义外层容器的 CSS 样式,使其在页面中醒目且整洁
|
||||
$containerStyle = "
|
||||
background-color: #282c34;
|
||||
color: #abb2bf;
|
||||
padding: 15px;
|
||||
margin: 10px 0;
|
||||
border-left: 5px solid #61afef;
|
||||
border-radius: 4px;
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
overflow-x: auto;
|
||||
text-align: left;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
z-index: 9999;
|
||||
position: relative;
|
||||
";
|
||||
|
||||
echo "<div style=\"{$containerStyle}\">";
|
||||
|
||||
foreach ($vars as $var) {
|
||||
echo "<pre style=\"margin: 0; padding: 5px 0;\">";
|
||||
|
||||
// 针对布尔值和 null 的特殊处理,因为 print_r 打印这些不直观
|
||||
if (is_bool($var)) {
|
||||
echo "<span style=\"color: #d19a66;\">bool</span>(" . ($var ? 'true' : 'false') . ")";
|
||||
} elseif (is_null($var)) {
|
||||
echo "<span style=\"color: #d19a66;\">null</span>";
|
||||
} else {
|
||||
// 使用 print_r 获取格式化字符串,并用 htmlspecialchars 防止 HTML 标签被浏览器解析
|
||||
$output = htmlspecialchars(print_r($var, true));
|
||||
|
||||
// 简单的高亮:给数组的键名加上颜色
|
||||
$output = preg_replace('/\[(.*?)\]/', '[<span style="color: #98c379;">$1</span>]', $output);
|
||||
|
||||
echo $output;
|
||||
}
|
||||
|
||||
echo "</pre>";
|
||||
|
||||
// 如果有多个参数,用分割线隔开
|
||||
if (count($vars) > 1 && $var !== end($vars)) {
|
||||
echo "<hr style=\"border: 0; border-bottom: 1px dashed #5c6370; margin: 10px 0;\">";
|
||||
}
|
||||
}
|
||||
|
||||
echo "</div>";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 美化打印变量并终止程序 (Dump and Die)
|
||||
*/
|
||||
if (!function_exists('dd')) {
|
||||
function dd(...$vars) {
|
||||
dump(...$vars);
|
||||
die(1); // 终止程序执行
|
||||
}
|
||||
}
|
||||
|
||||
// 文件名: AbstractScrmSpider.php
|
||||
|
||||
require_once __DIR__ . '/UnifiedScrmData.class.php';
|
||||
|
||||
abstract class AbstractScrmSpider
|
||||
{
|
||||
const MODE_FETCH = 'fetch'; // 极速并发拉取 (推荐默认)
|
||||
const MODE_UI = 'ui_click'; // 强制 UI 点击下一页
|
||||
|
||||
protected $nodeHost;
|
||||
|
||||
public function __construct($nodeHost = 'http://127.0.0.1:3001')
|
||||
{
|
||||
$this->nodeHost = $nodeHost;
|
||||
}
|
||||
|
||||
/** ================= 子类必须实现的契约 ================= **/
|
||||
abstract protected function getSpiderConfig();
|
||||
|
||||
// 以下方法的关注点,全部仅限于 List 接口!
|
||||
abstract protected function extractListTotalPages($listFirstPageData, $countData = null);
|
||||
abstract protected function buildListPageParams($page);
|
||||
abstract protected function getUiPaginationConfig();
|
||||
|
||||
// 清洗方法直接接收两个明确的变量,告别繁杂的数组解析
|
||||
abstract protected function parseToUnifiedData($detailData, $allListPagesData);
|
||||
|
||||
/** ================= 核心工作流 ================= **/
|
||||
public function run()
|
||||
{
|
||||
$config = $this->getSpiderConfig();
|
||||
|
||||
$listApi = $config['listApi'];
|
||||
$detailApi = $config['detailApi'] ?? null; // detail 接口是可选的
|
||||
$countApi = $config['countApi'] ?? null; // 有些在线客服的总数通过单独调用接口获取
|
||||
|
||||
// 组装需要 Node 拦截的目标数组
|
||||
$apiUrlsToIntercept = [$listApi];
|
||||
if ($detailApi) { $apiUrlsToIntercept[] = $detailApi; }
|
||||
if ($countApi) { $apiUrlsToIntercept[] = $countApi; }
|
||||
// var_dump($apiUrlsToIntercept);
|
||||
// 【阶段一】:初始化并首屏拦截
|
||||
$initResult = $this->requestNode('/api/auth-and-intercept', [
|
||||
'pageUrl' => $config['pageUrl'],
|
||||
'apiUrls' => $apiUrlsToIntercept,
|
||||
'authActions' => $config['authActions']
|
||||
]);
|
||||
|
||||
if (empty($initResult['success'])) {
|
||||
throw new Exception("初始化失败: " . ($initResult['error'] ?? '未知'));
|
||||
}
|
||||
|
||||
$interceptedApis = $initResult['interceptedApis'];
|
||||
$cookies = $initResult['cookies'];
|
||||
// dd($interceptedApis);
|
||||
// 必须拦截到 List 接口,否则无法继续
|
||||
if (!isset($interceptedApis[$listApi])) {
|
||||
throw new Exception("致命错误:未能拦截到必须的列表接口 [{$listApi}]");
|
||||
}
|
||||
|
||||
// 分离 Detail 和 List 的首屏数据
|
||||
$detailData = $detailApi && isset($interceptedApis[$detailApi]) ? $interceptedApis[$detailApi]['data'] : null;
|
||||
$countData = $countApi && isset($interceptedApis[$countApi]) ? $interceptedApis[$countApi]['data'] : null;
|
||||
// dd($detailData);
|
||||
// dd($countData);
|
||||
$listApiNode = $interceptedApis[$listApi];
|
||||
$allListPagesData = [$listApiNode['data']]; // 初始化列表容器,装入第一页数据
|
||||
|
||||
$totalPages = $this->extractListTotalPages($listApiNode['data'], $countData);
|
||||
|
||||
$mode = $config['paginationMode'] ?? self::MODE_FETCH;
|
||||
|
||||
// 如果总页数 > 1,触发【阶段二】
|
||||
if ($totalPages > 1 || $totalPages === null) {
|
||||
// 策略 1:极速 Fetch
|
||||
if ($mode === self::MODE_FETCH) {
|
||||
$paramList = [];
|
||||
for ($page = 2; $page <= $totalPages; $page++) {
|
||||
$paramList[] = $this->buildListPageParams($page);
|
||||
}
|
||||
|
||||
$fetchResult = $this->requestNode('/api/batch-fetch', [
|
||||
'tasks' => [[
|
||||
'apiPath' => $listApi,
|
||||
'fullUrl' => $listApiNode['url'],
|
||||
'headers' => $listApiNode['headers'] ?? "",
|
||||
'paramList' => $paramList,
|
||||
'method' => $config['listMethod'] ?? 'GET'
|
||||
]],
|
||||
'cookies' => $cookies
|
||||
], 120);
|
||||
|
||||
if (!empty($fetchResult['success'])) {
|
||||
foreach ($fetchResult['results'][$listApi] as $pResult) {
|
||||
if ($pResult['success']) $allListPagesData[] = $pResult['data'];
|
||||
}
|
||||
}
|
||||
}
|
||||
// 策略 2:强制 UI 点击
|
||||
elseif ($mode === self::MODE_UI) {
|
||||
$uiConfig = $this->getUiPaginationConfig();
|
||||
|
||||
// 🔥 核心修改:直接将第一页的数组传给 Node,让 Node 引擎自己去剔除时间戳并生成统一哈希
|
||||
$firstPageData = $listApiNode['data'];
|
||||
// 🔥 核心重构 2:如果是 null 盲点模式,下发 9999 次点击任务;否则按实际计算次数
|
||||
$clicksToPerform = ($totalPages === null) ? 9999 : ($totalPages - 1);
|
||||
|
||||
$uiResult = $this->requestNode('/api/ui-pagination', [
|
||||
'apiUrl' => $listApi,
|
||||
'pageUrl' => $config['pageUrl'],
|
||||
'nextBtnSelector' => $uiConfig['nextBtnSelector'],
|
||||
'waitMs' => $uiConfig['waitMs'] ?? 2000,
|
||||
'clicksToPerform' => $clicksToPerform,
|
||||
'cookies' => $cookies,
|
||||
'firstPageData' => $firstPageData, // 传递原始数据源
|
||||
// 🔥 核心补充:把登录剧本原封不动地传给翻页引擎!
|
||||
'authActions' => $config['authActions']
|
||||
], 1200); // 增加 PHP 端的 cURL 超时时间到 400 秒,以包容多次重试产生的耗时
|
||||
|
||||
if (!empty($uiResult['success']) && !empty($uiResult['data'])) {
|
||||
foreach ($uiResult['data'] as $pageData) {
|
||||
$allListPagesData[] = $pageData;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 注:如果 $totalPages <= 1,直接自然跳过上述区块,进入第三阶段。
|
||||
|
||||
// 【阶段三】:移交数据进行最终清洗
|
||||
// return $this->parseToUnifiedData($detailData, $allListPagesData);
|
||||
|
||||
// 【阶段三】:解析并打包数据
|
||||
$unifiedData = $this->parseToUnifiedData($detailData, $allListPagesData);
|
||||
|
||||
// 🔥 核心修改:引入业务对账雷达
|
||||
// if (!$this->validateDataIntegrity($unifiedData)) {
|
||||
// 方案 A:直接返回 false(符合你的特殊要求)
|
||||
// return false;
|
||||
|
||||
// 方案 B(工业级推荐):抛出自定义异常。
|
||||
// 因为返回 false 会让你在外部不知道到底是“网络超时”失败的,还是“数据对账没对上”失败的。
|
||||
// throw new Exception("抓取校验异常:数据未成功获取,或客服数量业务对账失败!");
|
||||
// }
|
||||
|
||||
return $unifiedData;
|
||||
}
|
||||
|
||||
private function requestNode($endpoint, $payload, $timeout = 60)
|
||||
{
|
||||
$ch = curl_init($this->nodeHost . $endpoint);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);
|
||||
$response = curl_exec($ch);
|
||||
if (curl_errno($ch)) throw new Exception(curl_error($ch));
|
||||
curl_close($ch);
|
||||
return json_decode($response, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔥 新增:数据完整性与业务对账校验
|
||||
* @param array $unifiedData 已经解析好的统一格式数据
|
||||
* @return bool 校验成功返回 true,失败返回 false
|
||||
*/
|
||||
protected function validateDataIntegrity($unifiedData)
|
||||
{
|
||||
// 1. 底线校验:如果最核心的列表数据或详情数据根本就是空的,直接判定失败
|
||||
if (empty($unifiedData) || !is_array($unifiedData)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 这里可以放一些全系统通用的基础校验(如果有的话)
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
// A2c云控
|
||||
|
||||
require_once __DIR__ . '/AbstractScrmSpider.class.php';
|
||||
|
||||
class Haiwang extends AbstractScrmSpider
|
||||
{
|
||||
const API_LIST = '/webApi/accountshow/list'; // 列表API地址
|
||||
const API_DETAILS = ''; // 详情API地址
|
||||
const API_COUNT = ''; // 总数API地址
|
||||
const DEFAULT_PER_PAGE_COUNT = 10; // List默认每页显示的数量
|
||||
private $pageUrl;
|
||||
private $account;
|
||||
private $password;
|
||||
private $unifiedData;
|
||||
|
||||
// 实例化时动态传入账号和密码
|
||||
public function __construct($pageUrl, $account, $password, $nodeHost = 'http://127.0.0.1:3001')
|
||||
{
|
||||
parent::__construct($nodeHost);
|
||||
$this->account = $account;
|
||||
$this->password = $password;
|
||||
$this->pageUrl = $pageUrl;
|
||||
|
||||
$this->unifiedData = new UnifiedScrmData();
|
||||
}
|
||||
|
||||
protected function getSpiderConfig()
|
||||
{
|
||||
return [
|
||||
'pageUrl' => $this->pageUrl,
|
||||
'apiUrls' => [self::API_LIST, self::API_DETAILS],
|
||||
|
||||
// 明确指派角色
|
||||
'listApi' => self::API_LIST, // 必须
|
||||
'detailApi' => self::API_DETAILS, // 选填
|
||||
'listMethod' => 'POST',
|
||||
'paginationMode' => self::MODE_UI,
|
||||
'authActions' => [
|
||||
['type' => 'type', 'selector' => 'input[type="password"]', 'value' => $this->password],
|
||||
// ['type' => 'type', 'selector' => '#username_input', 'value' => $this->account],
|
||||
['type' => 'press', 'key' => 'Enter'],
|
||||
|
||||
['type' => 'wait', 'ms' => 2000]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 只负责解析 List 的总页数
|
||||
protected function extractListTotalPages($listFirstPageData, $countData = null)
|
||||
{
|
||||
$default_per_page_count = self::DEFAULT_PER_PAGE_COUNT;
|
||||
$this->unifiedData->total = $listFirstPageData['data']['total'];
|
||||
|
||||
if($this->unifiedData->total <= $default_per_page_count) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ceil($this->unifiedData->total/$default_per_page_count);
|
||||
}
|
||||
|
||||
// 只负责组装 List 的翻页参数
|
||||
protected function buildListPageParams($page)
|
||||
{
|
||||
return ['page' => $page, 'limit' => self::DEFAULT_PER_PAGE_COUNT];
|
||||
}
|
||||
|
||||
// 提供 List 的下一页按钮信息
|
||||
protected function getUiPaginationConfig()
|
||||
{
|
||||
return [
|
||||
'nextBtnSelector' => '.btn-next',
|
||||
'waitMs' => 2000
|
||||
];
|
||||
}
|
||||
|
||||
// 清爽至极的数据清洗:详情是详情,列表是列表
|
||||
protected function parseToUnifiedData($detailData, $allListPagesData)
|
||||
{
|
||||
$unifiedData = $this->unifiedData;
|
||||
|
||||
// 循环合并了所有页数的 List 数组
|
||||
foreach ($allListPagesData as $pageRaw) {
|
||||
$records = $pageRaw['data']['items'] ?? [];
|
||||
foreach ($records as $item) {
|
||||
if(!empty($item['acclist_account'])) {
|
||||
$number = $item['acclist_account'] ?? null;
|
||||
$isOnline = (isset($item['acclist_status']) && $item['acclist_status'] == 2);
|
||||
|
||||
$unifiedData->addNumber($number, $isOnline, $item['account_statistics_today_effective']);
|
||||
}
|
||||
}
|
||||
}
|
||||
$unifiedData->todayNewCount = $allListPagesData[0]['data']['shareStatistics']['sharecode_statistics_today_contact_effective'];
|
||||
|
||||
return $unifiedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
// 火箭工单
|
||||
|
||||
require_once __DIR__ . '/AbstractScrmSpider.class.php';
|
||||
|
||||
class Huojian extends AbstractScrmSpider
|
||||
{
|
||||
const API_LIST = '/prod-api1/biz/counter/link/share/'; // 列表API地址
|
||||
const API_DETAILS = ''; // 详情API地址
|
||||
const API_COUNT = ''; // 总数API地址
|
||||
const DEFAULT_PER_PAGE_COUNT = 20; // List默认每页显示的数量
|
||||
private $pageUrl;
|
||||
private $account;
|
||||
private $password;
|
||||
private $unifiedData;
|
||||
|
||||
// 实例化时动态传入账号和密码
|
||||
public function __construct($pageUrl, $account, $password, $nodeHost = 'http://127.0.0.1:3001')
|
||||
{
|
||||
parent::__construct($nodeHost);
|
||||
$this->account = $account;
|
||||
$this->password = $password;
|
||||
$this->pageUrl = $pageUrl;
|
||||
|
||||
$this->unifiedData = new UnifiedScrmData();
|
||||
}
|
||||
|
||||
protected function getSpiderConfig()
|
||||
{
|
||||
return [
|
||||
'pageUrl' => $this->pageUrl,
|
||||
'apiUrls' => [self::API_LIST],
|
||||
|
||||
// 明确指派角色
|
||||
'listApi' => self::API_LIST, // 必须
|
||||
|
||||
'listMethod' => 'POST',
|
||||
'paginationMode' => self::MODE_UI,
|
||||
|
||||
'authActions' => [
|
||||
// 1. 填入密码:寻找 class 包含 el-message-box__input 下面的任意 input
|
||||
// Node.js 会自动扫描所有匹配项,并只把密码强行注入到那个“肉眼可见”的框里
|
||||
['type' => 'vue_fill', 'selector' => '.el-message-box__input input', 'value' => $this->password],
|
||||
|
||||
// 2. 停顿 500ms,让 Vue 绑定的 v-model 彻底反应过来
|
||||
['type' => 'wait', 'ms' => 500],
|
||||
|
||||
// 3. 点击确认:寻找 MessageBox 底部的蓝色 primary 确认按钮
|
||||
// 同样利用 vue_click 的可见性过滤,无视隐藏的旧弹窗按钮
|
||||
['type' => 'vue_click', 'selector' => '.el-message-box__btns .el-button--primary'],
|
||||
|
||||
// 4. 等待弹窗淡出,接口开始请求
|
||||
['type' => 'wait', 'ms' => 2000]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 只负责解析 List 的总页数
|
||||
protected function extractListTotalPages($listFirstPageData, $countData = null)
|
||||
{
|
||||
// 只有一页
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 没有分页返回空数组
|
||||
protected function buildListPageParams($page)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// 提供 List 的下一页按钮信息
|
||||
protected function getUiPaginationConfig()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
// 清爽至极的数据清洗:详情是详情,列表是列表
|
||||
protected function parseToUnifiedData($detailData, $allListPagesData)
|
||||
{
|
||||
$unifiedData = $this->unifiedData;
|
||||
|
||||
$unifiedData->todayNewCount = $allListPagesData[0]['data']['counterWorker']['newTodayFriend'];
|
||||
|
||||
// 2. 循环合并了所有页数的 List 数组
|
||||
$count = 0;
|
||||
foreach ($allListPagesData as $pageRaw) {
|
||||
$records = $pageRaw['data']['counterCsAccountVo'] ?? [];
|
||||
foreach ($records as $item) {
|
||||
if(!empty($item['accountLogin'])) {
|
||||
$number = $item['accountLogin'] ?? null;
|
||||
$isOnline = (isset($item['accountStatus']) && $item['accountStatus'] == 1);
|
||||
$count++;
|
||||
$unifiedData->addNumber($number, $isOnline, $item['newTodayFriend']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$unifiedData->total = $count;
|
||||
|
||||
return $unifiedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
<?php
|
||||
// SS云控(Customer)
|
||||
|
||||
require_once __DIR__ . '/AbstractScrmSpider.class.php';
|
||||
|
||||
// 可以获取每日具体进线数据
|
||||
class SsCustomer extends AbstractScrmSpider
|
||||
{
|
||||
const API_LIST = '/sys/share/report/get-customer-analysis-dimension-list'; // 列表API地址
|
||||
const API_DETAILS = '/sys/share/report/get-customer-analysis-statistics'; // 详情API地址
|
||||
const API_COUNT = ''; // 总数API地址
|
||||
const DEFAULT_PER_PAGE_COUNT = 20; // List默认每页显示的数量
|
||||
private $pageUrl;
|
||||
private $account;
|
||||
private $password;
|
||||
private $unifiedData;
|
||||
|
||||
// 实例化时动态传入账号和密码
|
||||
public function __construct($pageUrl, $account, $password, $nodeHost = 'http://127.0.0.1:3001')
|
||||
{
|
||||
parent::__construct($nodeHost);
|
||||
$this->account = $account;
|
||||
$this->password = $password;
|
||||
$this->pageUrl = $pageUrl;
|
||||
|
||||
$this->unifiedData = new UnifiedScrmData();
|
||||
}
|
||||
|
||||
protected function getSpiderConfig()
|
||||
{
|
||||
return [
|
||||
'pageUrl' => $this->pageUrl,
|
||||
'apiUrls' => [self::API_LIST, self::API_DETAILS, self::API_COUNT],
|
||||
|
||||
// 明确指派角色
|
||||
'listApi' => self::API_LIST, // 必须
|
||||
'detailApi' => self::API_DETAILS, // 选填
|
||||
'countApi' => self::API_COUNT,
|
||||
|
||||
'listMethod' => 'POST',
|
||||
|
||||
'paginationMode' => self::MODE_UI,
|
||||
|
||||
'authActions' => [
|
||||
['type' => 'wait', 'ms' => 2000],
|
||||
|
||||
['type' => 'type', 'selector' => 'input[type="password"]', 'value' => $this->password],
|
||||
|
||||
['type' => 'press', 'key' => 'Enter'],
|
||||
|
||||
['type' => 'wait', 'ms' => 3000],
|
||||
|
||||
[
|
||||
'type' => 'vue_click',
|
||||
'selector' => 'button[class*="reports-customers__dimension"]',
|
||||
'text' => 'social media accounts'
|
||||
],
|
||||
|
||||
['type' => 'wait', 'ms' => 3000],
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 只负责解析 List 的总页数
|
||||
protected function extractListTotalPages($listFirstPageData, $countData = null)
|
||||
{
|
||||
return null;
|
||||
|
||||
// $default_per_page_count = self::DEFAULT_PER_PAGE_COUNT;
|
||||
|
||||
// // 场景 A:如果单独的 Count 接口有数据返回
|
||||
// if ($countData) {
|
||||
// // 假设 count 接口返回的是 {"data": {"total": 312}}
|
||||
// $totalRecords = $countData['data']['total'];
|
||||
// } else {
|
||||
// // 场景 B
|
||||
// $totalRecords = $listFirstPageData['data']['total'];
|
||||
// }
|
||||
|
||||
// $this->unifiedData->total = $totalRecords;
|
||||
// if($totalRecords <= $default_per_page_count) {
|
||||
// return 1;
|
||||
// }
|
||||
|
||||
// return ceil($listFirstPageData['data']['total']/$default_per_page_count);
|
||||
}
|
||||
|
||||
// 只负责组装 List 的翻页参数
|
||||
protected function buildListPageParams($page)
|
||||
{
|
||||
return ['page' => $page, 'pageSize' => self::DEFAULT_PER_PAGE_COUNT];
|
||||
}
|
||||
|
||||
// 提供 List 的下一页按钮信息
|
||||
protected function getUiPaginationConfig()
|
||||
{
|
||||
return [
|
||||
'nextBtnSelector' => '.arco-pagination-item-next',
|
||||
'waitMs' => 2000
|
||||
];
|
||||
}
|
||||
|
||||
// 清爽至极的数据清洗:详情是详情,列表是列表
|
||||
protected function parseToUnifiedData($detailData, $allListPagesData)
|
||||
{
|
||||
$unifiedData = $this->unifiedData;
|
||||
|
||||
// 1. 如果捕获到了详情数据,提取今日新增
|
||||
if ($detailData) {
|
||||
$unifiedData->todayNewCount = (int)($detailData['data']['distinct_contacts_total'] ?? 0);
|
||||
}
|
||||
|
||||
// 2. 循环合并了所有页数的 List 数组
|
||||
foreach ($allListPagesData as $pageRaw) {
|
||||
$records = $pageRaw['data']['list'] ?? [];
|
||||
foreach ($records as $item) {
|
||||
if(!empty($item['channel_tag'])) {
|
||||
$number = $item['channel_tag'] ?? null;
|
||||
$isOnline = true; // 离线状态的账号同步不到
|
||||
|
||||
$unifiedData->addNumber($number, $isOnline, $item['distinct_contacts_total']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 🚀 3. 终极统计:所有翻页数据均已入库,此时再统计真实的 Total 总数
|
||||
$unifiedData->total = count($unifiedData->numbers);
|
||||
|
||||
return $unifiedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
// 文件名: UnifiedScrmData.php
|
||||
|
||||
class UnifiedScrmData
|
||||
{
|
||||
public $todayNewCount = 0;
|
||||
public $totalOnline = 0;
|
||||
public $totalOffline = 0;
|
||||
public $numbers = []; // 号码列表
|
||||
public $total = 0; // 号码总数
|
||||
|
||||
public function addNumber($number, $isOnline, $newFollowersToday)
|
||||
{
|
||||
$this->numbers[] = [
|
||||
'number' => $number,
|
||||
'status' => $isOnline ? 'online' : 'offline',
|
||||
'newFollowersToday' => $newFollowersToday
|
||||
];
|
||||
|
||||
if ($isOnline) {
|
||||
$this->totalOnline++;
|
||||
} else {
|
||||
$this->totalOffline++;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
// 星河工单
|
||||
|
||||
require_once __DIR__ . '/AbstractScrmSpider.class.php';
|
||||
|
||||
class Xinghe extends AbstractScrmSpider
|
||||
{
|
||||
const API_LIST = '/share/share/api_yinliu_count.html'; // 列表API地址
|
||||
const API_DETAILS = ''; // 详情API地址
|
||||
const API_COUNT = ''; // 总数API地址
|
||||
const DEFAULT_PER_PAGE_COUNT = 10; // List默认每页显示的数量
|
||||
private $pageUrl;
|
||||
private $account;
|
||||
private $password;
|
||||
private $unifiedData;
|
||||
|
||||
// 实例化时动态传入账号和密码
|
||||
public function __construct($pageUrl, $account, $password, $nodeHost = 'http://127.0.0.1:3001')
|
||||
{
|
||||
parent::__construct($nodeHost);
|
||||
$this->account = $account;
|
||||
$this->password = $password;
|
||||
$this->pageUrl = $pageUrl;
|
||||
|
||||
$this->unifiedData = new UnifiedScrmData();
|
||||
}
|
||||
|
||||
protected function getSpiderConfig()
|
||||
{
|
||||
return [
|
||||
'pageUrl' => $this->pageUrl,
|
||||
'apiUrls' => [self::API_LIST . "?page=1&limit=10"],
|
||||
'listApi' => self::API_LIST, // 必须,第一页的列表数据
|
||||
// 'detailApi' => self::API_DETAILS, // 选填
|
||||
// 'countApi' => self::API_COUNT,
|
||||
|
||||
'listMethod' => 'GET',
|
||||
|
||||
'paginationMode' => self::MODE_FETCH,
|
||||
|
||||
'authActions' => [
|
||||
// ['type' => 'type', 'selector' => 'input[type="password"]', 'value' => $this->password],
|
||||
// ['type' => 'type', 'selector' => '#username_input', 'value' => $this->account],
|
||||
// ['type' => 'press', 'key' => 'Enter'],
|
||||
|
||||
['type' => 'wait', 'ms' => 2000]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// 只负责解析 List 的总页数
|
||||
protected function extractListTotalPages($listFirstPageData, $countData = null)
|
||||
{
|
||||
$default_per_page_count = self::DEFAULT_PER_PAGE_COUNT;
|
||||
$this->unifiedData->total = $listFirstPageData['count'];
|
||||
$this->unifiedData->todayNewCount = $listFirstPageData['totalRow']['day_sum'];
|
||||
|
||||
if($listFirstPageData['count'] <= $default_per_page_count) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return ceil($listFirstPageData['count']/$default_per_page_count);
|
||||
}
|
||||
|
||||
// 只负责组装 List 的翻页参数
|
||||
protected function buildListPageParams($page)
|
||||
{
|
||||
return ['page' => $page, 'limit' => self::DEFAULT_PER_PAGE_COUNT];
|
||||
}
|
||||
|
||||
// 提供 List 的下一页按钮信息
|
||||
protected function getUiPaginationConfig()
|
||||
{
|
||||
return [
|
||||
'nextBtnSelector' => '.layui-laypage-next',
|
||||
'waitMs' => 2000
|
||||
];
|
||||
}
|
||||
|
||||
// 清爽至极的数据清洗:详情是详情,列表是列表
|
||||
protected function parseToUnifiedData($detailData, $allListPagesData)
|
||||
{
|
||||
$unifiedData = $this->unifiedData;
|
||||
|
||||
// 循环合并了所有页数的 List 数组
|
||||
foreach ($allListPagesData as $pageRaw) {
|
||||
$records = $pageRaw['data'] ?? [];
|
||||
foreach ($records as $item) {
|
||||
if(!empty($item['user'])) {
|
||||
$number = $item['user'] ?? null;
|
||||
$isOnline = (isset($item['online']) && $item['online'] == 1);
|
||||
|
||||
$unifiedData->addNumber($number, $isOnline, $item['day_sum']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $unifiedData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
// 文件名: run.php
|
||||
|
||||
require_once __DIR__ . '/A2c.php'; // A2c云控
|
||||
require_once __DIR__ . '/Xinghe.php'; // 星河云控
|
||||
require_once __DIR__ . '/Huojian.php'; // 火箭
|
||||
require_once __DIR__ . '/SsCustomer.php'; // SS云控(Customer)
|
||||
require_once __DIR__ . '/Haiwang.php'; // 海王
|
||||
|
||||
// try {
|
||||
// echo "🚀 开始执行<A2c云控>抓取任务 (多引擎智能调度)...\n\r";
|
||||
// $pageUrl = 'https://user.a2c.chat/visitors/counter/share?id=33e449dc83c24ee59275bf03a2d82234'; // PageUrl 入口授权页
|
||||
// $username = ""; // 登录账号
|
||||
// $password = ""; // 登录密码
|
||||
// $spider = new A2c($pageUrl, $username, $password);
|
||||
// $finalData = $spider->run();
|
||||
|
||||
// echo "✅ 任务完成!统一数据如下:\n\r";
|
||||
// echo "----------------------------------------\n\r";
|
||||
// echo "当日新增:{$finalData->todayNewCount} 人\n\r";
|
||||
// echo "在线号码:{$finalData->totalOnline} 个\n\r";
|
||||
// echo "离线号码:{$finalData->totalOffline} 个\n\r";
|
||||
// echo "Total:" . $finalData->total . " 个号码\n\r";
|
||||
// echo "实际总共抓取:" . count($finalData->numbers) . " 个号码\n\r";
|
||||
// echo "号码列表:\n\r";
|
||||
// echo dd($finalData->numbers);
|
||||
|
||||
// } catch (Exception $e) {
|
||||
// echo "🚨 抓取异常:" . $e->getMessage() . "\n\r";
|
||||
// }
|
||||
|
||||
// try {
|
||||
// echo "🚀 开始执行<星河云控>抓取任务 (多引擎智能调度)...\n\r";
|
||||
// $pageUrl = 'http://103.251.112.35:10158/share/share/index.html?token=pds65jl202t2kjis5firb8epu4d8a83ptfhc63d89l3mv11nwa'; // PageUrl 入口授权页
|
||||
// $username = ""; // 登录账号
|
||||
// $password = ""; // 登录密码
|
||||
// $spider = new Xinghe($pageUrl, $username, $password);
|
||||
// $finalData = $spider->run();
|
||||
|
||||
// echo "✅ 任务完成!统一数据如下:\n\r";
|
||||
// echo "----------------------------------------\n\r";
|
||||
// echo "当日新增:{$finalData->todayNewCount} 人\n\r";
|
||||
// echo "在线号码:{$finalData->totalOnline} 个\n\r";
|
||||
// echo "离线号码:{$finalData->totalOffline} 个\n\r";
|
||||
// echo "Total:" . $finalData->total . " 个号码\n\r";
|
||||
// echo "实际总共抓取:" . count($finalData->numbers) . " 个号码\n\r";
|
||||
// echo "号码列表:\n\r";
|
||||
// echo dd($finalData->numbers);
|
||||
|
||||
// } catch (Exception $e) {
|
||||
// echo "🚨 抓取异常:" . $e->getMessage() . "\n\r";
|
||||
// }
|
||||
|
||||
// try {
|
||||
// echo "🚀 开始执行<火箭工单>抓取任务 (多引擎智能调度)...\n\r";
|
||||
// $pageUrl = 'https://s.url99.me/ygn9zjr8'; // PageUrl 入口授权页
|
||||
// $username = ""; // 登录账号
|
||||
// $password = "123456"; // 登录密码
|
||||
// $spider = new Huojian($pageUrl, $username, $password);
|
||||
// $finalData = $spider->run();
|
||||
|
||||
// echo "✅ 任务完成!统一数据如下:\n\r";
|
||||
// echo "----------------------------------------\n\r";
|
||||
// echo "当日新增:{$finalData->todayNewCount} 人\n\r";
|
||||
// echo "在线号码:{$finalData->totalOnline} 个\n\r";
|
||||
// echo "离线号码:{$finalData->totalOffline} 个\n\r";
|
||||
// echo "Total:" . $finalData->total . " 个号码\n\r";
|
||||
// echo "实际总共抓取:" . count($finalData->numbers) . " 个号码\n\r";
|
||||
// echo "号码列表:\n\r";
|
||||
// echo dd($finalData->numbers);
|
||||
|
||||
// } catch (Exception $e) {
|
||||
// echo "🚨 抓取异常:" . $e->getMessage() . "\n\r";
|
||||
// }
|
||||
|
||||
// try {
|
||||
// echo "🚀 开始执行<SS云控(Customer)>抓取任务 (多引擎智能调度)...\n\r";
|
||||
// $pageUrl = 'https://app.salesmartly.vip/next/share/reports/verify/customer/fwt7cg'; // PageUrl 入口授权页
|
||||
// $username = ""; // 登录账号
|
||||
// $password = "123456"; // 登录密码
|
||||
// $spider = new SsCustomer($pageUrl, $username, $password);
|
||||
// $finalData = $spider->run();
|
||||
|
||||
// echo "✅ 任务完成!统一数据如下:\n\r";
|
||||
// echo "----------------------------------------\n\r";
|
||||
// echo "当日新增:{$finalData->todayNewCount} 人\n\r";
|
||||
// echo "在线号码:{$finalData->totalOnline} 个\n\r";
|
||||
// echo "离线号码:{$finalData->totalOffline} 个\n\r";
|
||||
// echo "Total:" . $finalData->total . " 个号码\n\r";
|
||||
// echo "实际总共抓取:" . count($finalData->numbers) . " 个号码\n\r";
|
||||
// echo "号码列表:\n\r";
|
||||
// echo dd($finalData->numbers);
|
||||
|
||||
|
||||
// } catch (Exception $e) {
|
||||
// echo "🚨 抓取异常:" . $e->getMessage() . "\n\r";
|
||||
// }
|
||||
|
||||
try {
|
||||
echo "🚀 开始执行<海王>抓取任务 (多引擎智能调度)...\n\r";
|
||||
$pageUrl = 'https://admin.haiwangweb.com/web#/accountshow/pZsEulYrb'; // PageUrl 入口授权页
|
||||
$username = ""; // 登录账号
|
||||
$password = "9999"; // 登录密码
|
||||
$spider = new Haiwang($pageUrl, $username, $password);
|
||||
$finalData = $spider->run();
|
||||
|
||||
echo "✅ 任务完成!统一数据如下:\n\r";
|
||||
echo "----------------------------------------\n\r";
|
||||
echo "当日新增:{$finalData->todayNewCount} 人\n\r";
|
||||
echo "在线号码:{$finalData->totalOnline} 个\n\r";
|
||||
echo "离线号码:{$finalData->totalOffline} 个\n\r";
|
||||
echo "Total:" . $finalData->total . " 个号码\n\r";
|
||||
echo "实际总共抓取:" . count($finalData->numbers) . " 个号码\n\r";
|
||||
echo "号码列表:\n\r";
|
||||
echo dd($finalData->numbers);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "🚨 抓取异常:" . $e->getMessage() . "\n\r";
|
||||
}
|
||||
|
||||
// try {
|
||||
// echo "🚀 开始执行<CEO SCRM>抓取任务 (多引擎智能调度)...\n\r";
|
||||
// $pageUrl = 'https://admin.scrmceo.com/#/workShareDetail?code=XgGTK5yN'; // PageUrl 入口授权页
|
||||
// $username = ""; // 登录账号
|
||||
// $password = "a2222"; // 登录密码
|
||||
// $spider = new SsCustomer($pageUrl, $username, $password);
|
||||
// $finalData = $spider->run();
|
||||
|
||||
// echo "✅ 任务完成!统一数据如下:\n\r";
|
||||
// echo "----------------------------------------\n\r";
|
||||
// echo "当日新增:{$finalData->todayNewCount} 人\n\r";
|
||||
// echo "在线号码:{$finalData->totalOnline} 个\n\r";
|
||||
// echo "离线号码:{$finalData->totalOffline} 个\n\r";
|
||||
// echo "Total:" . $finalData->total . " 个号码\n\r";
|
||||
// echo "实际总共抓取:" . count($finalData->numbers) . " 个号码\n\r";
|
||||
// echo "号码列表:\n\r";
|
||||
// echo dd($finalData->numbers);
|
||||
// } catch (Exception $e) {
|
||||
// echo "🚨 抓取异常:" . $e->getMessage() . "\n\r";
|
||||
// }
|
||||
Reference in New Issue
Block a user