213 lines
6.9 KiB
PHP
Executable File
213 lines
6.9 KiB
PHP
Executable File
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace app\common\service;
|
||
|
||
use app\admin\model\split\Number;
|
||
use app\admin\model\split\Ticket;
|
||
use app\common\library\scrm\UnifiedScrmData;
|
||
use think\Db;
|
||
|
||
/**
|
||
* 工单同步结果写入号码表
|
||
*
|
||
* 同步策略(全量镜像):
|
||
* - 爬虫返回的号码:写入 platform_status / inbound_count;status 由后续 applyNumberRules 裁决
|
||
* - 爬虫未返回且 manual_manage=0:物理删除
|
||
* - manual_manage=1:不删除,仅当仍在爬虫结果中时更新 platform_status
|
||
*/
|
||
class SplitTicketNumberSyncService
|
||
{
|
||
/**
|
||
* 将蜘蛛返回的号码列表同步到号码管理(全量镜像,以爬虫为准)
|
||
*/
|
||
public function syncFromUnifiedData(Ticket $ticket, UnifiedScrmData $data): void
|
||
{
|
||
$adminId = (int) $ticket['admin_id'];
|
||
$linkId = (int) $ticket['split_link_id'];
|
||
$ticketName = (string) $ticket['ticket_name'];
|
||
if ($linkId <= 0 || $ticketName === '') {
|
||
return;
|
||
}
|
||
|
||
$randomShuffle = SplitNumberWeighService::isRandomShuffleEnabled($linkId);
|
||
|
||
// 使用独立 number 字段存储,避免纯数字号码作为数组 key 被 PHP 自动转为 int
|
||
$syncedNumbers = [];
|
||
foreach ($data->numbers as $row) {
|
||
$number = self::normalizeNumber($row['number'] ?? '');
|
||
if ($number === '') {
|
||
continue;
|
||
}
|
||
$syncedNumbers[] = [
|
||
'number' => $number,
|
||
'row' => $row,
|
||
];
|
||
}
|
||
|
||
$existingList = Number::where('admin_id', $adminId)
|
||
->where('split_link_id', $linkId)
|
||
->where('ticket_name', $ticketName)
|
||
->select();
|
||
|
||
$existingMap = [];
|
||
foreach ($existingList as $item) {
|
||
$existingMap[(string) $item['number']] = $item;
|
||
}
|
||
|
||
$syncedNumberSet = [];
|
||
$pendingInserts = [];
|
||
foreach ($syncedNumbers as $entry) {
|
||
$number = $entry['number'];
|
||
$row = $entry['row'];
|
||
$syncedNumberSet[$number] = true;
|
||
$platformStatus = ($row['status'] ?? '') === 'online' ? 'online' : 'offline';
|
||
$newFollowers = (int) ($row['newFollowersToday'] ?? 0);
|
||
|
||
if (isset($existingMap[$number])) {
|
||
$this->updateExistingNumber($ticket, $existingMap[$number], $platformStatus, $newFollowers);
|
||
continue;
|
||
}
|
||
|
||
$pendingInserts[] = [
|
||
'number' => $number,
|
||
'platform_status' => $platformStatus,
|
||
'new_followers' => $newFollowers,
|
||
];
|
||
}
|
||
|
||
if ($pendingInserts !== []) {
|
||
// 随机打乱:打乱待插入批次顺序,按随机顺序逐条 insert 以获得乱序自增 id
|
||
if ($randomShuffle && count($pendingInserts) > 1) {
|
||
shuffle($pendingInserts);
|
||
}
|
||
foreach ($pendingInserts as $item) {
|
||
$this->insertNumber(
|
||
$ticket,
|
||
$item['number'],
|
||
$item['platform_status'],
|
||
$item['new_followers']
|
||
);
|
||
}
|
||
}
|
||
|
||
$deleteIds = [];
|
||
foreach ($existingMap as $number => $item) {
|
||
if (isset($syncedNumberSet[$number])) {
|
||
continue;
|
||
}
|
||
if ((int) $item['manual_manage'] === 1) {
|
||
continue;
|
||
}
|
||
$deleteIds[] = (int) $item['id'];
|
||
}
|
||
if ($deleteIds !== []) {
|
||
Number::where('id', 'in', $deleteIds)->delete();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 工单手动开启等非同步场景:按业务规则对齐号码开关(平台 + 单号上限 + 下号比率)
|
||
*/
|
||
public function applyStatusFromPlatformSnapshot(Ticket $ticket): void
|
||
{
|
||
(new SplitTicketRuleService())->applyNumberRules($ticket);
|
||
}
|
||
|
||
/**
|
||
* 云控在线 → 开启;离线 → 关闭;工单关闭时一律关闭
|
||
*/
|
||
public static function resolveSwitchStatus(Ticket $ticket, string $platformStatus): string
|
||
{
|
||
if ((string) ($ticket['status'] ?? 'hidden') !== 'normal') {
|
||
return 'hidden';
|
||
}
|
||
|
||
return $platformStatus === 'online' ? 'normal' : 'hidden';
|
||
}
|
||
|
||
/**
|
||
* @param Number $row
|
||
*/
|
||
private function updateExistingNumber(
|
||
Ticket $ticket,
|
||
$row,
|
||
string $platformStatus,
|
||
int $newFollowers
|
||
): void {
|
||
$update = [
|
||
'platform_status' => $platformStatus,
|
||
'updatetime' => time(),
|
||
];
|
||
|
||
if ((int) $row['manual_manage'] === 1) {
|
||
Number::where('id', (int) $row['id'])->update($update);
|
||
return;
|
||
}
|
||
|
||
$update['inbound_count'] = max(0, $newFollowers);
|
||
Number::where('id', (int) $row['id'])->update($update);
|
||
}
|
||
|
||
private function insertNumber(
|
||
Ticket $ticket,
|
||
string $number,
|
||
string $platformStatus,
|
||
int $newFollowers
|
||
): void {
|
||
$now = time();
|
||
$data = [
|
||
'admin_id' => (int) $ticket['admin_id'],
|
||
'split_link_id' => (int) $ticket['split_link_id'],
|
||
'ticket_name' => (string) $ticket['ticket_name'],
|
||
'number' => $number,
|
||
'number_type' => (string) $ticket['number_type'],
|
||
'number_type_custom' => (string) ($ticket['number_type_custom'] ?? ''),
|
||
'visit_count' => 0,
|
||
'inbound_count' => max(0, $newFollowers),
|
||
'manual_manage' => 0,
|
||
'platform_status' => $platformStatus,
|
||
'ratio_deferred' => 0,
|
||
'createtime' => $now,
|
||
'updatetime' => $now,
|
||
];
|
||
|
||
try {
|
||
Db::name('split_number')->insert($data);
|
||
} catch (\Throwable $e) {
|
||
$exists = Number::where('split_link_id', (int) $ticket['split_link_id'])
|
||
->where('ticket_name', (string) $ticket['ticket_name'])
|
||
->where('number', $number)
|
||
->find();
|
||
if ($exists) {
|
||
$this->updateExistingNumber($ticket, $exists, $platformStatus, $newFollowers);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 统一号码为字符串(云控 API 可能返回 int)
|
||
*/
|
||
private static function normalizeNumber($value): string
|
||
{
|
||
if ($value === null || $value === '') {
|
||
return '';
|
||
}
|
||
return trim((string) $value);
|
||
}
|
||
|
||
/**
|
||
* 汇总工单进线人数(仅开启状态的号码)
|
||
*/
|
||
public function sumInboundForTicket(Ticket $ticket): int
|
||
{
|
||
$sum = Number::where('admin_id', (int) $ticket['admin_id'])
|
||
->where('split_link_id', (int) $ticket['split_link_id'])
|
||
->where('ticket_name', (string) $ticket['ticket_name'])
|
||
->where('status', 'normal')
|
||
->sum('inbound_count');
|
||
return (int) $sum;
|
||
}
|
||
}
|