修改Whatshub工单的完成数量总计的计算方法
This commit is contained in:
Regular → Executable
Regular → Executable
Executable → Regular
+82
-6
@@ -31,6 +31,10 @@ class AntiBotConfigBuilder
|
|||||||
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($ticketType === 'whatshub' && $landingUrlHint === '') {
|
||||||
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
$sessionScope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
$sessionScope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
||||||
if ($sessionScope === '') {
|
if ($sessionScope === '') {
|
||||||
return null;
|
return null;
|
||||||
@@ -58,7 +62,7 @@ class AntiBotConfigBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会话 scope:A2C 业务域+分享 id;火箭长链用动态 host、短链用 share.{token}
|
* 会话 scope:A2C 业务域+分享 id;Whatshub host+shareCode/workId;火箭长链用动态 host
|
||||||
*/
|
*/
|
||||||
public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string
|
public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string
|
||||||
{
|
{
|
||||||
@@ -66,6 +70,10 @@ class AntiBotConfigBuilder
|
|||||||
return self::resolveA2cSessionScope($pageUrl, $landingUrlHint);
|
return self::resolveA2cSessionScope($pageUrl, $landingUrlHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($ticketType === 'whatshub') {
|
||||||
|
return self::resolveWhatshubSessionScope($pageUrl, $landingUrlHint);
|
||||||
|
}
|
||||||
|
|
||||||
if ($ticketType === 'huojian') {
|
if ($ticketType === 'huojian') {
|
||||||
return self::resolveHuojianSessionScope($pageUrl, $landingUrlHint);
|
return self::resolveHuojianSessionScope($pageUrl, $landingUrlHint);
|
||||||
}
|
}
|
||||||
@@ -92,6 +100,10 @@ class AntiBotConfigBuilder
|
|||||||
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($ticketType === 'whatshub' && $landingUrlHint === '') {
|
||||||
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
$scope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
$scope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
||||||
|
|
||||||
return $ticketType . ':' . ($scope !== '' ? $scope : 'unknown');
|
return $ticketType . ':' . ($scope !== '' ? $scope : 'unknown');
|
||||||
@@ -137,7 +149,8 @@ class AntiBotConfigBuilder
|
|||||||
return [
|
return [
|
||||||
'postCfReadyUrlContains' => 'work-order-sharing',
|
'postCfReadyUrlContains' => 'work-order-sharing',
|
||||||
'postCfReadySelector' => '.el-input__inner',
|
'postCfReadySelector' => '.el-input__inner',
|
||||||
'postCfReadyTimeoutMs' => 30000,
|
'postCfReadyApiUrl' => '/api/whatshub-counter/workShare/open/statistics',
|
||||||
|
'postCfReadyTimeoutMs' => 60000,
|
||||||
'postCfSettleMs' => 500,
|
'postCfSettleMs' => 500,
|
||||||
'postCfSpaWaitMs' => 4000,
|
'postCfSpaWaitMs' => 4000,
|
||||||
];
|
];
|
||||||
@@ -179,7 +192,7 @@ class AntiBotConfigBuilder
|
|||||||
|
|
||||||
private static function defaultCfClearanceRequired(string $ticketType): bool
|
private static function defaultCfClearanceRequired(string $ticketType): bool
|
||||||
{
|
{
|
||||||
if ($ticketType === 'a2c') {
|
if ($ticketType === 'a2c' || $ticketType === 'whatshub') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,11 +200,11 @@ class AntiBotConfigBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A2C 等为 CF Managed Challenge(5s 盾),非独立 Turnstile 挂件
|
* A2C / Whatshub 等为 CF Managed Challenge(5s 盾),非独立 Turnstile 挂件
|
||||||
*/
|
*/
|
||||||
private static function defaultCfChallengeMode(string $ticketType): string
|
private static function defaultCfChallengeMode(string $ticketType): string
|
||||||
{
|
{
|
||||||
if ($ticketType === 'a2c') {
|
if ($ticketType === 'a2c' || $ticketType === 'whatshub') {
|
||||||
return 'managed';
|
return 'managed';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +216,7 @@ class AntiBotConfigBuilder
|
|||||||
*/
|
*/
|
||||||
private static function defaultCfCloudflareSolver(string $ticketType): bool
|
private static function defaultCfCloudflareSolver(string $ticketType): bool
|
||||||
{
|
{
|
||||||
return $ticketType === 'a2c';
|
return $ticketType === 'a2c' || $ticketType === 'whatshub';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,6 +246,69 @@ class AntiBotConfigBuilder
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whatshub 会话 scope:host + shareCode 或 workId,避免多工单共用温池 Browser
|
||||||
|
*/
|
||||||
|
private static function resolveWhatshubSessionScope(string $pageUrl, string $landingUrlHint = ''): string
|
||||||
|
{
|
||||||
|
$host = strtolower(trim((string) parse_url($pageUrl, PHP_URL_HOST)));
|
||||||
|
if ($host === '' && $landingUrlHint !== '') {
|
||||||
|
$host = strtolower(trim((string) parse_url($landingUrlHint, PHP_URL_HOST)));
|
||||||
|
}
|
||||||
|
if ($host === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$shareCode = self::extractWhatshubShareCode($pageUrl);
|
||||||
|
if ($shareCode === '' && $landingUrlHint !== '') {
|
||||||
|
$shareCode = self::extractWhatshubShareCode($landingUrlHint);
|
||||||
|
}
|
||||||
|
if ($shareCode !== '') {
|
||||||
|
return $host . ':' . $shareCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workId = self::extractWhatshubWorkId($pageUrl);
|
||||||
|
if ($workId === '' && $landingUrlHint !== '') {
|
||||||
|
$workId = self::extractWhatshubWorkId($landingUrlHint);
|
||||||
|
}
|
||||||
|
if ($workId !== '') {
|
||||||
|
return $host . ':' . $workId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Whatshub 短链 /m/{shareCode}/ 提取 shareCode
|
||||||
|
*/
|
||||||
|
private static function extractWhatshubShareCode(string $url): string
|
||||||
|
{
|
||||||
|
$path = (string) parse_url($url, PHP_URL_PATH);
|
||||||
|
if ($path === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (preg_match('#/m/([^/]+)/#', $path, $matches)) {
|
||||||
|
return trim($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Whatshub 长链 work-order-sharing?workId= 提取 workId
|
||||||
|
*/
|
||||||
|
private static function extractWhatshubWorkId(string $url): string
|
||||||
|
{
|
||||||
|
$query = (string) parse_url($url, PHP_URL_QUERY);
|
||||||
|
if ($query === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
parse_str($query, $params);
|
||||||
|
$workId = isset($params['workId']) ? trim((string) $params['workId']) : '';
|
||||||
|
|
||||||
|
return $workId !== '' ? $workId : '';
|
||||||
|
}
|
||||||
|
|
||||||
private static function resolveA2cSessionHost(string $pageUrl, string $landingUrlHint = ''): string
|
private static function resolveA2cSessionHost(string $pageUrl, string $landingUrlHint = ''): string
|
||||||
{
|
{
|
||||||
$candidates = [];
|
$candidates = [];
|
||||||
|
|||||||
Executable → Regular
+66
-12
@@ -12,7 +12,7 @@ use app\common\service\SplitTicketSyncLogger;
|
|||||||
/**
|
/**
|
||||||
* Whatshub 云控蜘蛛
|
* Whatshub 云控蜘蛛
|
||||||
*
|
*
|
||||||
* 时序:detailByShareCode API 直抓(优先)→ 失败则 Real Browser + Turnstile + UI 翻页
|
* 时序:cURL detailByShareCode 拉号码 → Node 监听 statistics 算完成量 → 失败则完整 Node 爬虫
|
||||||
*/
|
*/
|
||||||
class WhatshubSpider extends AbstractScrmSpider
|
class WhatshubSpider extends AbstractScrmSpider
|
||||||
{
|
{
|
||||||
@@ -21,7 +21,7 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
|
|
||||||
/** 详情/统计 API 路径 */
|
/** 详情/统计 API 路径 */
|
||||||
private const API_DETAILS = '/api/whatshub-counter/workShare/open/statistics';
|
private const API_DETAILS = '/api/whatshub-counter/workShare/open/statistics';
|
||||||
|
|
||||||
/** shareCode + password 直连接口(一次返回全量号码,无需翻页) */
|
/** shareCode + password 直连接口(一次返回全量号码,无需翻页) */
|
||||||
private const API_DETAIL_BY_SHARE_CODE = '/api/whatshub-counter/workShare/open/detailByShareCode';
|
private const API_DETAIL_BY_SHARE_CODE = '/api/whatshub-counter/workShare/open/detailByShareCode';
|
||||||
|
|
||||||
@@ -52,25 +52,81 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 优先:detailByShareCode 直抓;失败则回退 Node 爬虫
|
* 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量;任一失败则完整 Node fallback
|
||||||
*/
|
*/
|
||||||
public function run(): UnifiedScrmData
|
public function run(): UnifiedScrmData
|
||||||
{
|
{
|
||||||
$apiResult = $this->tryFetchViaShareCodeApi();
|
$apiResult = $this->tryFetchViaShareCodeApi();
|
||||||
if ($apiResult instanceof UnifiedScrmData) {
|
$statsCount = $this->tryFetchStatisticsViaNode();
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub api path ok', [
|
|
||||||
'count' => count($apiResult->numbers),
|
if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) {
|
||||||
|
$apiResult->todayNewCount = $statsCount;
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
'todayNewCount' => $statsCount,
|
||||||
]);
|
]);
|
||||||
return $apiResult;
|
return $apiResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub api skipped or failed, fallback node', [
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [
|
||||||
'pageUrl' => $this->pageUrl,
|
'pageUrl' => $this->pageUrl,
|
||||||
|
'apiOk' => $apiResult instanceof UnifiedScrmData,
|
||||||
|
'statisticsOk' => $statsCount !== null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return parent::run();
|
return parent::run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null
|
||||||
|
*/
|
||||||
|
private function tryFetchStatisticsViaNode(): ?int
|
||||||
|
{
|
||||||
|
$config = $this->getSpiderConfig();
|
||||||
|
$antiBot = $config['antiBot'] ?? null;
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [
|
||||||
|
'pageUrl' => $this->pageUrl,
|
||||||
|
'api' => self::API_DETAILS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->requestNode('/api/auth-and-intercept', [
|
||||||
|
'pageUrl' => $config['pageUrl'],
|
||||||
|
'apiUrls' => [self::API_DETAILS],
|
||||||
|
'authActions' => $config['authActions'] ?? [],
|
||||||
|
'antiBot' => $antiBot,
|
||||||
|
], $this->resolveAuthInterceptTimeout(is_array($antiBot) ? $antiBot : null));
|
||||||
|
|
||||||
|
if (empty($result['success'])) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [
|
||||||
|
'error' => $result['error'] ?? '未知',
|
||||||
|
'code' => $result['code'] ?? null,
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intercepted = $result['interceptedApis'] ?? [];
|
||||||
|
if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [
|
||||||
|
'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []),
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$detailData = $intercepted[self::API_DETAILS]['data'];
|
||||||
|
$dayNewFans = (int) ($detailData['data']['dayNewFans'] ?? 0);
|
||||||
|
$reDayNewFans = (int) ($detailData['data']['reDayNewFans'] ?? 0);
|
||||||
|
$total = $dayNewFans + $reDayNewFans;
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node ok', [
|
||||||
|
'dayNewFans' => $dayNewFans,
|
||||||
|
'reDayNewFans' => $reDayNewFans,
|
||||||
|
'total' => $total,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从短链 pageUrl 解析 shareCode,例如 /m/KMYBBInp2066/1 → KMYBBInp2066
|
* 从短链 pageUrl 解析 shareCode,例如 /m/KMYBBInp2066/1 → KMYBBInp2066
|
||||||
*/
|
*/
|
||||||
@@ -202,7 +258,6 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
private function parseShareCodeApiResponse(array $payload): UnifiedScrmData
|
private function parseShareCodeApiResponse(array $payload): UnifiedScrmData
|
||||||
{
|
{
|
||||||
$unifiedData = new UnifiedScrmData();
|
$unifiedData = new UnifiedScrmData();
|
||||||
$todayNewSum = 0;
|
|
||||||
|
|
||||||
foreach ($payload['data'] as $item) {
|
foreach ($payload['data'] as $item) {
|
||||||
if (!is_array($item) || empty($item['account'])) {
|
if (!is_array($item) || empty($item['account'])) {
|
||||||
@@ -213,11 +268,10 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
$isOnline = isset($item['isOnline']) && (int) $item['isOnline'] === 1;
|
$isOnline = isset($item['isOnline']) && (int) $item['isOnline'] === 1;
|
||||||
$dayNewFans = (int) ($item['dayNewFans'] ?? 0);
|
$dayNewFans = (int) ($item['dayNewFans'] ?? 0);
|
||||||
|
|
||||||
|
// 号码同步仅使用 dayNewFans,不含 reDayNewFans;完成量由 Node statistics 提供
|
||||||
$unifiedData->addNumber($number, $isOnline, $dayNewFans);
|
$unifiedData->addNumber($number, $isOnline, $dayNewFans);
|
||||||
$todayNewSum += $dayNewFans;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$unifiedData->todayNewCount = $todayNewSum;
|
|
||||||
$unifiedData->total = count($unifiedData->numbers);
|
$unifiedData->total = count($unifiedData->numbers);
|
||||||
|
|
||||||
return $unifiedData;
|
return $unifiedData;
|
||||||
@@ -279,7 +333,7 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
$unifiedData = $this->unifiedData;
|
$unifiedData = $this->unifiedData;
|
||||||
|
|
||||||
if (is_array($detailData)) {
|
if (is_array($detailData)) {
|
||||||
$unifiedData->todayNewCount = (int) ($detailData['data']['dayNewFans'] ?? 0);
|
$unifiedData->todayNewCount = (int) ($detailData['data']['dayNewFans'] ?? 0) + (int) ($detailData['data']['reDayNewFans'] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($allListPagesData as $pageRaw) {
|
foreach ($allListPagesData as $pageRaw) {
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ class SplitNumberRatioDeferredService
|
|||||||
/**
|
/**
|
||||||
* 同步侧:根据 visit / inbound 与上次同步检查点重算 streak
|
* 同步侧:根据 visit / inbound 与上次同步检查点重算 streak
|
||||||
*
|
*
|
||||||
|
* 访问侧(recordVisitAfterRedirect)已按次 +1 累计 streak,此处不得再叠加 delta,否则首次同步会双倍计次导致提前关号。
|
||||||
|
* 取 max(访问侧 streak, 自上次同步以来的点击增量) 作为裁决值,并兜底访问钩子未触发的场景。
|
||||||
|
*
|
||||||
* @param array<string, mixed>|Number $number
|
* @param array<string, mixed>|Number $number
|
||||||
*/
|
*/
|
||||||
public function computeStreakForSync($number, int $currentStreak): int
|
public function computeStreakForSync($number, int $currentStreak): int
|
||||||
@@ -74,7 +77,9 @@ class SplitNumberRatioDeferredService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($visitCount > $lastVisit) {
|
if ($visitCount > $lastVisit) {
|
||||||
return $currentStreak + ($visitCount - $lastVisit);
|
$deltaSinceSync = $visitCount - $lastVisit;
|
||||||
|
|
||||||
|
return max($currentStreak, $deltaSinceSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $currentStreak;
|
return $currentStreak;
|
||||||
|
|||||||
Regular → Executable
Executable → Regular
+1
-2
@@ -36,12 +36,11 @@ class SplitScrmSpiderFactory
|
|||||||
/**
|
/**
|
||||||
* API 优先同步类型:常态走 PHP cURL,不占 Node Real Browser 槽位
|
* API 优先同步类型:常态走 PHP cURL,不占 Node Real Browser 槽位
|
||||||
*
|
*
|
||||||
* 调度层据此跳过 Node 队列门禁并允许并行投递;蜘蛛实现本身不在此修改。
|
* Whatshub 已改为混合同步(cURL 号码 + Node statistics),需走 Node 队列门禁。
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
private const API_FIRST_SYNC_TYPES = [
|
private const API_FIRST_SYNC_TYPES = [
|
||||||
'whatshub',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ return array (
|
|||||||
'name' => 'link管理系统',
|
'name' => 'link管理系统',
|
||||||
'beian' => '',
|
'beian' => '',
|
||||||
'cdnurl' => '',
|
'cdnurl' => '',
|
||||||
'version' => '1.0.1',
|
'version' => '1.0.9',
|
||||||
'timezone' => 'Asia/Shanghai',
|
'timezone' => 'Asia/Shanghai',
|
||||||
'forbiddenip' => '',
|
'forbiddenip' => '',
|
||||||
'languages' =>
|
'languages' =>
|
||||||
@@ -42,7 +42,7 @@ return array (
|
|||||||
'category2' => 'Category2',
|
'category2' => 'Category2',
|
||||||
'custom' => 'Custom',
|
'custom' => 'Custom',
|
||||||
),
|
),
|
||||||
'split_platform_domain' => '',
|
'split_platform_domain' => 'flowerbells.top',
|
||||||
'split_scrm_node_host' => 'http://127.0.0.1:3001',
|
'split_scrm_node_host' => 'http://127.0.0.1:3001',
|
||||||
'split_sync_interval_a2c' => '3',
|
'split_sync_interval_a2c' => '3',
|
||||||
'split_sync_interval_haiwang' => '3',
|
'split_sync_interval_haiwang' => '3',
|
||||||
@@ -53,14 +53,14 @@ return array (
|
|||||||
'split_sync_interval_taiji' => '0',
|
'split_sync_interval_taiji' => '0',
|
||||||
'split_sync_interval_ss_channel' => '0',
|
'split_sync_interval_ss_channel' => '0',
|
||||||
'split_sync_interval_yifafa' => '0',
|
'split_sync_interval_yifafa' => '0',
|
||||||
'split_sync_interval_whatshub' => '0',
|
'split_sync_interval_whatshub' => '3',
|
||||||
'split_sync_interval_sihai' => '0',
|
'split_sync_interval_sihai' => '0',
|
||||||
'split_sync_fail_pause_threshold' => '5',
|
'split_sync_fail_pause_threshold' => '6',
|
||||||
'split_sync_interval_chatknow' => '3',
|
'split_sync_interval_chatknow' => '5',
|
||||||
'split_sync_max_per_cron' => '5',
|
'split_sync_max_per_cron' => '27',
|
||||||
'split_sync_node_candidate_multiplier' => '10',
|
'split_sync_node_candidate_multiplier' => '13',
|
||||||
'split_sync_cron_lock_ttl' => '1800',
|
'split_sync_cron_lock_ttl' => '680',
|
||||||
'split_sync_node_queue_threshold' => '2',
|
'split_sync_node_queue_threshold' => '8',
|
||||||
'split_scrm_antibot_types' => 'whatshub,chatknow,a2c,huojian',
|
'split_scrm_antibot_types' => 'whatshub,chatknow,a2c,huojian',
|
||||||
'split_scrm_session_ttl_minutes' => '120',
|
'split_scrm_session_ttl_minutes' => '120',
|
||||||
);
|
);
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -18,7 +18,6 @@ use think\console\Output;
|
|||||||
* 用法:
|
* 用法:
|
||||||
* php think split:sync-tickets
|
* php think split:sync-tickets
|
||||||
* php think split:sync-tickets --ticket=12
|
* php think split:sync-tickets --ticket=12
|
||||||
* php think split:sync-tickets --ticket=12 --mode=auto
|
|
||||||
*/
|
*/
|
||||||
class SplitSyncTickets extends Command
|
class SplitSyncTickets extends Command
|
||||||
{
|
{
|
||||||
@@ -26,24 +25,21 @@ class SplitSyncTickets extends Command
|
|||||||
{
|
{
|
||||||
$this->setName('split:sync-tickets')
|
$this->setName('split:sync-tickets')
|
||||||
->addOption('ticket', 't', Option::VALUE_OPTIONAL, '指定工单 ID(强制同步,忽略周期)', '')
|
->addOption('ticket', 't', Option::VALUE_OPTIONAL, '指定工单 ID(强制同步,忽略周期)', '')
|
||||||
->addOption('mode', 'm', Option::VALUE_OPTIONAL, '同步方式: auto=定时自动, manual=手动触发', 'manual')
|
|
||||||
->setDescription('同步分流工单云控数据');
|
->setDescription('同步分流工单云控数据');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function execute(Input $input, Output $output): void
|
protected function execute(Input $input, Output $output): void
|
||||||
{
|
{
|
||||||
set_time_limit(0);
|
set_time_limit(0);
|
||||||
$syncMode = $this->resolveSyncMode($input);
|
|
||||||
SplitTicketSyncLogger::log('cli', 'command start', [
|
SplitTicketSyncLogger::log('cli', 'command start', [
|
||||||
'ticketOption' => trim((string) $input->getOption('ticket')),
|
'ticketOption' => trim((string) $input->getOption('ticket')),
|
||||||
'syncMode' => $syncMode,
|
|
||||||
'appDebug' => SplitTicketSyncLogger::isEnabled(),
|
'appDebug' => SplitTicketSyncLogger::isEnabled(),
|
||||||
]);
|
]);
|
||||||
$service = new SplitTicketSyncService();
|
$service = new SplitTicketSyncService();
|
||||||
$ticketId = trim((string) $input->getOption('ticket'));
|
$ticketId = trim((string) $input->getOption('ticket'));
|
||||||
|
|
||||||
if ($ticketId !== '' && ctype_digit($ticketId)) {
|
if ($ticketId !== '' && ctype_digit($ticketId)) {
|
||||||
$result = $service->syncOne((int) $ticketId, true, false, $syncMode);
|
$result = $service->syncOne((int) $ticketId, true);
|
||||||
if (!empty($result['skipped'])) {
|
if (!empty($result['skipped'])) {
|
||||||
$output->writeln('<comment>跳过: ' . ($result['message'] ?? '') . '</comment>');
|
$output->writeln('<comment>跳过: ' . ($result['message'] ?? '') . '</comment>');
|
||||||
return;
|
return;
|
||||||
@@ -75,14 +71,4 @@ class SplitSyncTickets extends Command
|
|||||||
$output->writeln('<comment>详细调试日志已写入 runtime/log/split_sync.log</comment>');
|
$output->writeln('<comment>详细调试日志已写入 runtime/log/split_sync.log</comment>');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析 CLI 同步方式,供 sync_success_mode 落库
|
|
||||||
*/
|
|
||||||
private function resolveSyncMode(Input $input): string
|
|
||||||
{
|
|
||||||
$mode = strtolower(trim((string) $input->getOption('mode')));
|
|
||||||
|
|
||||||
return in_array($mode, ['auto', 'manual'], true) ? $mode : 'manual';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,10 @@ class AntiBotConfigBuilder
|
|||||||
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($ticketType === 'whatshub' && $landingUrlHint === '') {
|
||||||
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
$sessionScope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
$sessionScope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
||||||
if ($sessionScope === '') {
|
if ($sessionScope === '') {
|
||||||
return null;
|
return null;
|
||||||
@@ -58,7 +62,7 @@ class AntiBotConfigBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 会话 scope:A2C 业务域+分享 id;火箭长链用动态 host、短链用 share.{token}
|
* 会话 scope:A2C 业务域+分享 id;Whatshub host+shareCode/workId;火箭长链用动态 host
|
||||||
*/
|
*/
|
||||||
public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string
|
public static function resolveSessionScope(string $ticketType, string $pageUrl, string $landingUrlHint = ''): string
|
||||||
{
|
{
|
||||||
@@ -66,6 +70,10 @@ class AntiBotConfigBuilder
|
|||||||
return self::resolveA2cSessionScope($pageUrl, $landingUrlHint);
|
return self::resolveA2cSessionScope($pageUrl, $landingUrlHint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($ticketType === 'whatshub') {
|
||||||
|
return self::resolveWhatshubSessionScope($pageUrl, $landingUrlHint);
|
||||||
|
}
|
||||||
|
|
||||||
if ($ticketType === 'huojian') {
|
if ($ticketType === 'huojian') {
|
||||||
return self::resolveHuojianSessionScope($pageUrl, $landingUrlHint);
|
return self::resolveHuojianSessionScope($pageUrl, $landingUrlHint);
|
||||||
}
|
}
|
||||||
@@ -92,6 +100,10 @@ class AntiBotConfigBuilder
|
|||||||
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($ticketType === 'whatshub' && $landingUrlHint === '') {
|
||||||
|
$landingUrlHint = SplitPageUrlResolver::resolveFinalUrl($pageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
$scope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
$scope = self::resolveSessionScope($ticketType, $pageUrl, $landingUrlHint);
|
||||||
|
|
||||||
return $ticketType . ':' . ($scope !== '' ? $scope : 'unknown');
|
return $ticketType . ':' . ($scope !== '' ? $scope : 'unknown');
|
||||||
@@ -137,7 +149,8 @@ class AntiBotConfigBuilder
|
|||||||
return [
|
return [
|
||||||
'postCfReadyUrlContains' => 'work-order-sharing',
|
'postCfReadyUrlContains' => 'work-order-sharing',
|
||||||
'postCfReadySelector' => '.el-input__inner',
|
'postCfReadySelector' => '.el-input__inner',
|
||||||
'postCfReadyTimeoutMs' => 30000,
|
'postCfReadyApiUrl' => '/api/whatshub-counter/workShare/open/statistics',
|
||||||
|
'postCfReadyTimeoutMs' => 60000,
|
||||||
'postCfSettleMs' => 500,
|
'postCfSettleMs' => 500,
|
||||||
'postCfSpaWaitMs' => 4000,
|
'postCfSpaWaitMs' => 4000,
|
||||||
];
|
];
|
||||||
@@ -179,7 +192,7 @@ class AntiBotConfigBuilder
|
|||||||
|
|
||||||
private static function defaultCfClearanceRequired(string $ticketType): bool
|
private static function defaultCfClearanceRequired(string $ticketType): bool
|
||||||
{
|
{
|
||||||
if ($ticketType === 'a2c') {
|
if ($ticketType === 'a2c' || $ticketType === 'whatshub') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,11 +200,11 @@ class AntiBotConfigBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A2C 等为 CF Managed Challenge(5s 盾),非独立 Turnstile 挂件
|
* A2C / Whatshub 等为 CF Managed Challenge(5s 盾),非独立 Turnstile 挂件
|
||||||
*/
|
*/
|
||||||
private static function defaultCfChallengeMode(string $ticketType): string
|
private static function defaultCfChallengeMode(string $ticketType): string
|
||||||
{
|
{
|
||||||
if ($ticketType === 'a2c') {
|
if ($ticketType === 'a2c' || $ticketType === 'whatshub') {
|
||||||
return 'managed';
|
return 'managed';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +216,7 @@ class AntiBotConfigBuilder
|
|||||||
*/
|
*/
|
||||||
private static function defaultCfCloudflareSolver(string $ticketType): bool
|
private static function defaultCfCloudflareSolver(string $ticketType): bool
|
||||||
{
|
{
|
||||||
return $ticketType === 'a2c';
|
return $ticketType === 'a2c' || $ticketType === 'whatshub';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,6 +246,69 @@ class AntiBotConfigBuilder
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whatshub 会话 scope:host + shareCode 或 workId,避免多工单共用温池 Browser
|
||||||
|
*/
|
||||||
|
private static function resolveWhatshubSessionScope(string $pageUrl, string $landingUrlHint = ''): string
|
||||||
|
{
|
||||||
|
$host = strtolower(trim((string) parse_url($pageUrl, PHP_URL_HOST)));
|
||||||
|
if ($host === '' && $landingUrlHint !== '') {
|
||||||
|
$host = strtolower(trim((string) parse_url($landingUrlHint, PHP_URL_HOST)));
|
||||||
|
}
|
||||||
|
if ($host === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$shareCode = self::extractWhatshubShareCode($pageUrl);
|
||||||
|
if ($shareCode === '' && $landingUrlHint !== '') {
|
||||||
|
$shareCode = self::extractWhatshubShareCode($landingUrlHint);
|
||||||
|
}
|
||||||
|
if ($shareCode !== '') {
|
||||||
|
return $host . ':' . $shareCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
$workId = self::extractWhatshubWorkId($pageUrl);
|
||||||
|
if ($workId === '' && $landingUrlHint !== '') {
|
||||||
|
$workId = self::extractWhatshubWorkId($landingUrlHint);
|
||||||
|
}
|
||||||
|
if ($workId !== '') {
|
||||||
|
return $host . ':' . $workId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $host;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Whatshub 短链 /m/{shareCode}/ 提取 shareCode
|
||||||
|
*/
|
||||||
|
private static function extractWhatshubShareCode(string $url): string
|
||||||
|
{
|
||||||
|
$path = (string) parse_url($url, PHP_URL_PATH);
|
||||||
|
if ($path === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (preg_match('#/m/([^/]+)/#', $path, $matches)) {
|
||||||
|
return trim($matches[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Whatshub 长链 work-order-sharing?workId= 提取 workId
|
||||||
|
*/
|
||||||
|
private static function extractWhatshubWorkId(string $url): string
|
||||||
|
{
|
||||||
|
$query = (string) parse_url($url, PHP_URL_QUERY);
|
||||||
|
if ($query === '') {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
parse_str($query, $params);
|
||||||
|
$workId = isset($params['workId']) ? trim((string) $params['workId']) : '';
|
||||||
|
|
||||||
|
return $workId !== '' ? $workId : '';
|
||||||
|
}
|
||||||
|
|
||||||
private static function resolveA2cSessionHost(string $pageUrl, string $landingUrlHint = ''): string
|
private static function resolveA2cSessionHost(string $pageUrl, string $landingUrlHint = ''): string
|
||||||
{
|
{
|
||||||
$candidates = [];
|
$candidates = [];
|
||||||
|
|||||||
Regular → Executable
Executable → Regular
+65
-11
@@ -12,7 +12,7 @@ use app\common\service\SplitTicketSyncLogger;
|
|||||||
/**
|
/**
|
||||||
* Whatshub 云控蜘蛛
|
* Whatshub 云控蜘蛛
|
||||||
*
|
*
|
||||||
* 时序:detailByShareCode API 直抓(优先)→ 失败则 Real Browser + Turnstile + UI 翻页
|
* 时序:cURL detailByShareCode 拉号码 → Node 监听 statistics 算完成量 → 失败则完整 Node 爬虫
|
||||||
*/
|
*/
|
||||||
class WhatshubSpider extends AbstractScrmSpider
|
class WhatshubSpider extends AbstractScrmSpider
|
||||||
{
|
{
|
||||||
@@ -52,25 +52,81 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* API 优先:detailByShareCode 直抓;失败则回退 Node 爬虫
|
* 混合同步:cURL 拉号码 + Node 监听 statistics 算完成量;任一失败则完整 Node fallback
|
||||||
*/
|
*/
|
||||||
public function run(): UnifiedScrmData
|
public function run(): UnifiedScrmData
|
||||||
{
|
{
|
||||||
$apiResult = $this->tryFetchViaShareCodeApi();
|
$apiResult = $this->tryFetchViaShareCodeApi();
|
||||||
if ($apiResult instanceof UnifiedScrmData) {
|
$statsCount = $this->tryFetchStatisticsViaNode();
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub api path ok', [
|
|
||||||
'count' => count($apiResult->numbers),
|
if ($apiResult instanceof UnifiedScrmData && $statsCount !== null) {
|
||||||
|
$apiResult->todayNewCount = $statsCount;
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid path ok', [
|
||||||
|
'numberCount' => count($apiResult->numbers),
|
||||||
|
'todayNewCount' => $statsCount,
|
||||||
]);
|
]);
|
||||||
return $apiResult;
|
return $apiResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
SplitTicketSyncLogger::log('spider', 'whatshub api skipped or failed, fallback node', [
|
SplitTicketSyncLogger::log('spider', 'whatshub hybrid incomplete, fallback full node', [
|
||||||
'pageUrl' => $this->pageUrl,
|
'pageUrl' => $this->pageUrl,
|
||||||
|
'apiOk' => $apiResult instanceof UnifiedScrmData,
|
||||||
|
'statisticsOk' => $statsCount !== null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return parent::run();
|
return parent::run();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Node 监听 statistics API,返回 dayNewFans + reDayNewFans 总和;失败返回 null
|
||||||
|
*/
|
||||||
|
private function tryFetchStatisticsViaNode(): ?int
|
||||||
|
{
|
||||||
|
$config = $this->getSpiderConfig();
|
||||||
|
$antiBot = $config['antiBot'] ?? null;
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node request', [
|
||||||
|
'pageUrl' => $this->pageUrl,
|
||||||
|
'api' => self::API_DETAILS,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$result = $this->requestNode('/api/auth-and-intercept', [
|
||||||
|
'pageUrl' => $config['pageUrl'],
|
||||||
|
'apiUrls' => [self::API_DETAILS],
|
||||||
|
'authActions' => $config['authActions'] ?? [],
|
||||||
|
'antiBot' => $antiBot,
|
||||||
|
], $this->resolveAuthInterceptTimeout(is_array($antiBot) ? $antiBot : null));
|
||||||
|
|
||||||
|
if (empty($result['success'])) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node failed', [
|
||||||
|
'error' => $result['error'] ?? '未知',
|
||||||
|
'code' => $result['code'] ?? null,
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$intercepted = $result['interceptedApis'] ?? [];
|
||||||
|
if (!isset($intercepted[self::API_DETAILS]['data']) || !is_array($intercepted[self::API_DETAILS]['data'])) {
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node missing payload', [
|
||||||
|
'intercepted' => array_keys(is_array($intercepted) ? $intercepted : []),
|
||||||
|
]);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$detailData = $intercepted[self::API_DETAILS]['data'];
|
||||||
|
$dayNewFans = (int) ($detailData['data']['dayNewFans'] ?? 0);
|
||||||
|
$reDayNewFans = (int) ($detailData['data']['reDayNewFans'] ?? 0);
|
||||||
|
$total = $dayNewFans + $reDayNewFans;
|
||||||
|
|
||||||
|
SplitTicketSyncLogger::log('spider', 'whatshub stats node ok', [
|
||||||
|
'dayNewFans' => $dayNewFans,
|
||||||
|
'reDayNewFans' => $reDayNewFans,
|
||||||
|
'total' => $total,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $total;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 从短链 pageUrl 解析 shareCode,例如 /m/KMYBBInp2066/1 → KMYBBInp2066
|
* 从短链 pageUrl 解析 shareCode,例如 /m/KMYBBInp2066/1 → KMYBBInp2066
|
||||||
*/
|
*/
|
||||||
@@ -202,7 +258,6 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
private function parseShareCodeApiResponse(array $payload): UnifiedScrmData
|
private function parseShareCodeApiResponse(array $payload): UnifiedScrmData
|
||||||
{
|
{
|
||||||
$unifiedData = new UnifiedScrmData();
|
$unifiedData = new UnifiedScrmData();
|
||||||
$todayNewSum = 0;
|
|
||||||
|
|
||||||
foreach ($payload['data'] as $item) {
|
foreach ($payload['data'] as $item) {
|
||||||
if (!is_array($item) || empty($item['account'])) {
|
if (!is_array($item) || empty($item['account'])) {
|
||||||
@@ -213,11 +268,10 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
$isOnline = isset($item['isOnline']) && (int) $item['isOnline'] === 1;
|
$isOnline = isset($item['isOnline']) && (int) $item['isOnline'] === 1;
|
||||||
$dayNewFans = (int) ($item['dayNewFans'] ?? 0);
|
$dayNewFans = (int) ($item['dayNewFans'] ?? 0);
|
||||||
|
|
||||||
|
// 号码同步仅使用 dayNewFans,不含 reDayNewFans;完成量由 Node statistics 提供
|
||||||
$unifiedData->addNumber($number, $isOnline, $dayNewFans);
|
$unifiedData->addNumber($number, $isOnline, $dayNewFans);
|
||||||
$todayNewSum += $dayNewFans;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$unifiedData->todayNewCount = $todayNewSum;
|
|
||||||
$unifiedData->total = count($unifiedData->numbers);
|
$unifiedData->total = count($unifiedData->numbers);
|
||||||
|
|
||||||
return $unifiedData;
|
return $unifiedData;
|
||||||
@@ -279,7 +333,7 @@ class WhatshubSpider extends AbstractScrmSpider
|
|||||||
$unifiedData = $this->unifiedData;
|
$unifiedData = $this->unifiedData;
|
||||||
|
|
||||||
if (is_array($detailData)) {
|
if (is_array($detailData)) {
|
||||||
$unifiedData->todayNewCount = (int) ($detailData['data']['dayNewFans'] ?? 0);
|
$unifiedData->todayNewCount = (int) ($detailData['data']['dayNewFans'] ?? 0) + (int) ($detailData['data']['reDayNewFans'] ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($allListPagesData as $pageRaw) {
|
foreach ($allListPagesData as $pageRaw) {
|
||||||
|
|||||||
Regular → Executable
+6
-1
@@ -56,6 +56,9 @@ class SplitNumberRatioDeferredService
|
|||||||
/**
|
/**
|
||||||
* 同步侧:根据 visit / inbound 与上次同步检查点重算 streak
|
* 同步侧:根据 visit / inbound 与上次同步检查点重算 streak
|
||||||
*
|
*
|
||||||
|
* 访问侧(recordVisitAfterRedirect)已按次 +1 累计 streak,此处不得再叠加 delta,否则首次同步会双倍计次导致提前关号。
|
||||||
|
* 取 max(访问侧 streak, 自上次同步以来的点击增量) 作为裁决值,并兜底访问钩子未触发的场景。
|
||||||
|
*
|
||||||
* @param array<string, mixed>|Number $number
|
* @param array<string, mixed>|Number $number
|
||||||
*/
|
*/
|
||||||
public function computeStreakForSync($number, int $currentStreak): int
|
public function computeStreakForSync($number, int $currentStreak): int
|
||||||
@@ -74,7 +77,9 @@ class SplitNumberRatioDeferredService
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($visitCount > $lastVisit) {
|
if ($visitCount > $lastVisit) {
|
||||||
return $currentStreak + ($visitCount - $lastVisit);
|
$deltaSinceSync = $visitCount - $lastVisit;
|
||||||
|
|
||||||
|
return max($currentStreak, $deltaSinceSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $currentStreak;
|
return $currentStreak;
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Executable → Regular
+1
-2
@@ -36,12 +36,11 @@ class SplitScrmSpiderFactory
|
|||||||
/**
|
/**
|
||||||
* API 优先同步类型:常态走 PHP cURL,不占 Node Real Browser 槽位
|
* API 优先同步类型:常态走 PHP cURL,不占 Node Real Browser 槽位
|
||||||
*
|
*
|
||||||
* 调度层据此跳过 Node 队列门禁并允许并行投递;蜘蛛实现本身不在此修改。
|
* Whatshub 已改为混合同步(cURL 号码 + Node statistics),需走 Node 队列门禁。
|
||||||
*
|
*
|
||||||
* @var list<string>
|
* @var list<string>
|
||||||
*/
|
*/
|
||||||
private const API_FIRST_SYNC_TYPES = [
|
private const API_FIRST_SYNC_TYPES = [
|
||||||
'whatshub',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Regular → Executable
+2
-14
@@ -373,13 +373,11 @@ class SplitTicketSyncDispatchService
|
|||||||
$logDir = $this->resolveLogDir();
|
$logDir = $this->resolveLogDir();
|
||||||
$logFile = $logDir . 'cli_' . $ticketId . '_' . date('YmdHis') . '.log';
|
$logFile = $logDir . 'cli_' . $ticketId . '_' . date('YmdHis') . '.log';
|
||||||
|
|
||||||
$syncMode = $this->normalizeSyncModeForCli($source);
|
|
||||||
$inner = sprintf(
|
$inner = sprintf(
|
||||||
'%s %s split:sync-tickets --ticket=%d --mode=%s >> %s 2>&1',
|
'%s %s split:sync-tickets --ticket=%d >> %s 2>&1',
|
||||||
escapeshellarg($php),
|
escapeshellarg($php),
|
||||||
escapeshellarg($think),
|
escapeshellarg($think),
|
||||||
$ticketId,
|
$ticketId,
|
||||||
$syncMode,
|
|
||||||
escapeshellarg($logFile)
|
escapeshellarg($logFile)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -405,13 +403,11 @@ class SplitTicketSyncDispatchService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$logFile = $logDir . 'cli_' . $ticketId . '_' . date('YmdHis') . '.log';
|
$logFile = $logDir . 'cli_' . $ticketId . '_' . date('YmdHis') . '.log';
|
||||||
$syncMode = $this->normalizeSyncModeForCli($source);
|
|
||||||
$parts[] = sprintf(
|
$parts[] = sprintf(
|
||||||
'%s %s split:sync-tickets --ticket=%d --mode=%s >> %s 2>&1',
|
'%s %s split:sync-tickets --ticket=%d >> %s 2>&1',
|
||||||
escapeshellarg($php),
|
escapeshellarg($php),
|
||||||
escapeshellarg($think),
|
escapeshellarg($think),
|
||||||
$ticketId,
|
$ticketId,
|
||||||
$syncMode,
|
|
||||||
escapeshellarg($logFile)
|
escapeshellarg($logFile)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -556,12 +552,4 @@ class SplitTicketSyncDispatchService
|
|||||||
{
|
{
|
||||||
return function_exists('shell_exec') && !in_array('shell_exec', $this->disabledFunctions(), true);
|
return function_exists('shell_exec') && !in_array('shell_exec', $this->disabledFunctions(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 投递 CLI 时携带 --mode,供 sync_success_mode 区分自动/手动
|
|
||||||
*/
|
|
||||||
private function normalizeSyncModeForCli(string $source): string
|
|
||||||
{
|
|
||||||
return in_array($source, ['auto', 'manual'], true) ? $source : 'manual';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
visible: false,
|
visible: false,
|
||||||
searchable: true,
|
searchable: true,
|
||||||
operate: '=',
|
operate: '=',
|
||||||
searchList: Controller.api.buildRatioDeferredSearchList()
|
searchList: Config.ratioDeferredList || {}
|
||||||
},
|
},
|
||||||
searchSelectMeta(false)
|
searchSelectMeta(false)
|
||||||
),
|
),
|
||||||
@@ -160,16 +160,6 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin
|
|||||||
}
|
}
|
||||||
return ext;
|
return ext;
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* PHP json_encode 会把 ['0'=>..,'1'=>..] 压成数组,FastAdmin 通用搜索会误用文案作 option value
|
|
||||||
*/
|
|
||||||
buildRatioDeferredSearchList: function () {
|
|
||||||
var list = Config.ratioDeferredList;
|
|
||||||
if ($.isArray(list)) {
|
|
||||||
return {0: list[0], 1: list[1]};
|
|
||||||
}
|
|
||||||
return list || {};
|
|
||||||
},
|
|
||||||
initCommonSearchSelectpicker: function (tbl) {
|
initCommonSearchSelectpicker: function (tbl) {
|
||||||
var $form = $('form.form-commonsearch', tbl.$container);
|
var $form = $('form.form-commonsearch', tbl.$container);
|
||||||
if ($form.length) {
|
if ($form.length) {
|
||||||
|
|||||||
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
Regular → Executable
@@ -227,17 +227,21 @@ function applyWhatshubPostCfDefaults(antiBot, pageUrl) {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(pageUrl || '');
|
const parsed = new URL(pageUrl || '');
|
||||||
if (!parsed.hostname.includes('whatshub.cc') || !parsed.pathname.includes('/m/')) {
|
const isWhatshubHost = parsed.hostname.includes('whatshub.cc');
|
||||||
|
const isShortLink = parsed.pathname.includes('/m/');
|
||||||
|
const isLongLink = pageUrl.includes('work-order-sharing');
|
||||||
|
if (!isWhatshubHost || (!isShortLink && !isLongLink)) {
|
||||||
return antiBot;
|
return antiBot;
|
||||||
}
|
}
|
||||||
console.log('[CF] Whatshub 短链自动启用 postCfReady 默认配置');
|
console.log('[CF] Whatshub 自动启用 postCfReady 默认配置');
|
||||||
return {
|
return {
|
||||||
...antiBot,
|
...antiBot,
|
||||||
postCfReadyUrlContains: 'work-order-sharing',
|
postCfReadyUrlContains: 'work-order-sharing',
|
||||||
postCfReadySelector: '.el-input__inner',
|
postCfReadySelector: '.el-input__inner',
|
||||||
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 30000,
|
postCfReadyApiUrl: '/api/whatshub-counter/workShare/open/statistics',
|
||||||
|
postCfReadyTimeoutMs: antiBot.postCfReadyTimeoutMs || 60000,
|
||||||
postCfSettleMs: antiBot.postCfSettleMs || 500,
|
postCfSettleMs: antiBot.postCfSettleMs || 500,
|
||||||
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 8000,
|
postCfSpaWaitMs: antiBot.postCfSpaWaitMs || 4000,
|
||||||
};
|
};
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return antiBot;
|
return antiBot;
|
||||||
@@ -1187,11 +1191,17 @@ async function runCloudflareWithBusinessGuard(page, antiBot, waitForUrlSettled,
|
|||||||
cfResult = await handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession);
|
cfResult = await handleCloudflareChallengeSafe(page, antiBot, waitForUrlSettled, storedSession);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 短链跨域落地(如 yyk.ink → user.a2c.chat)后,业务域可能仍有 CF
|
// 短链跨域落地(如 yyk.ink → user.a2c.chat、Whatshub /m/ → work-order-sharing)后,业务域可能仍有 CF
|
||||||
if (cfResult.success && !(await isCfTrulyComplete(page, antiBot))) {
|
if (cfResult.success && !(await isCfTrulyComplete(page, antiBot))) {
|
||||||
const crossState = await detectCloudflare(page, antiBot);
|
const crossState = await detectCloudflare(page, antiBot);
|
||||||
if (crossState.blocked || urlIndicatesCfChallenge(page.url())) {
|
if (crossState.blocked || urlIndicatesCfChallenge(page.url())) {
|
||||||
console.log(`[CF] 跨域落地后 CF 未完成,二次过盾 url=${page.url()}`);
|
const isWhatshubLongCf =
|
||||||
|
urlNeedle === 'work-order-sharing' && urlIndicatesCfChallenge(page.url());
|
||||||
|
if (isWhatshubLongCf) {
|
||||||
|
console.log(`[CF] Whatshub 长链二次过盾 url=${page.url()}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[CF] 跨域落地后 CF 未完成,二次过盾 url=${page.url()}`);
|
||||||
|
}
|
||||||
page = await maybeEnsureProxy(page);
|
page = await maybeEnsureProxy(page);
|
||||||
cfResult = await finalizeCfResult(
|
cfResult = await finalizeCfResult(
|
||||||
page,
|
page,
|
||||||
|
|||||||
Reference in New Issue
Block a user