Files
links/patches/application/common/service/SplitTicketRuleService.php
T

240 lines
7.9 KiB
PHP
Raw Normal View History

2026-06-09 03:36:30 +08:00
<?php
declare(strict_types=1);
namespace app\common\service;
use app\admin\model\split\Number;
use app\admin\model\split\Ticket;
/**
* 工单与号码业务规则(单号上限、下号比率、时间窗口、完成量自动开关)
*/
class SplitTicketRuleService
{
private SplitNumberRatioDeferredService $ratioDeferredService;
public function __construct(?SplitNumberRatioDeferredService $ratioDeferredService = null)
{
$this->ratioDeferredService = $ratioDeferredService ?? new SplitNumberRatioDeferredService();
}
2026-06-09 03:36:30 +08:00
/**
* 同步后应用工单开关规则;号码开关由 applyNumberRules 综合裁决(平台在线 + 单号上限 + 下号比率)
2026-06-09 03:36:30 +08:00
*/
public function applyAfterSync(Ticket $ticket, int $completeCount): void
{
$this->applyTicketStatusRules($ticket, $completeCount);
$fresh = Ticket::get((int) $ticket['id']);
if ($fresh) {
if ((string) $fresh['status'] === 'hidden') {
$this->cascadeTicketClosedToNumbers($fresh);
2026-07-01 01:26:27 +08:00
} else {
$this->applyNumberRules($fresh);
2026-06-09 03:36:30 +08:00
}
}
}
/**
* 同步流程:工单因时间/完成量等原因关闭时,联动关闭非手动号码
*/
public function cascadeTicketClosedToNumbers(Ticket $ticket): void
{
if ((string) ($ticket['status'] ?? 'hidden') !== 'hidden') {
return;
}
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('manual_manage', 0)
->update([
'status' => 'hidden',
'updatetime' => time(),
]);
}
/**
* 工单物理删除时,移除该工单同步导入的号码(manual_manage=0
*
* 关联键与 SplitTicketNumberSyncService 一致:admin_id + split_link_id + ticket_name
* manual_manage=1 的手动管理号码保留
*
* @return int 删除行数
*/
public function deleteSyncedNumbersForTicket(Ticket $ticket): int
{
$ticketName = trim((string) ($ticket['ticket_name'] ?? ''));
$linkId = (int) ($ticket['split_link_id'] ?? 0);
if ($ticketName === '' || $linkId <= 0) {
return 0;
}
return Number::where('admin_id', (int) $ticket['admin_id'])
->where('split_link_id', $linkId)
->where('ticket_name', $ticketName)
->where('manual_manage', 0)
->delete();
}
2026-06-09 03:36:30 +08:00
/**
* 手动切换工单状态时,非手动号码按业务规则对齐开关
2026-06-09 03:36:30 +08:00
*/
public function syncNumbersWithTicketStatus(Ticket $ticket): void
{
$ticketStatus = (string) ($ticket['status'] ?? 'hidden');
if ($ticketStatus === 'hidden') {
$this->cascadeTicketClosedToNumbers($ticket);
return;
}
$this->applyNumberRules($ticket);
2026-06-09 03:36:30 +08:00
}
/**
* 工单处于开启状态时,将云控在线的非手动号码设为开启
*
* @deprecated 请使用 applyNumberRules
2026-06-09 03:36:30 +08:00
*/
public function syncOnlineNumbersWhenTicketOpen(Ticket $ticket): void
{
$this->applyNumberRules($ticket);
2026-06-09 03:36:30 +08:00
}
/**
* 单号上限、下号比率、云控在线状态、工单开关综合决定号码 status;同步时清除或确认 ratio_deferred
2026-06-09 03:36:30 +08:00
*/
public function applyNumberRules(Ticket $ticket): void
{
$orderLimit = (int) ($ticket['order_limit'] ?? 0);
$assignRatio = (int) ($ticket['assign_ratio'] ?? 0);
$numbers = Number::where('admin_id', (int) $ticket['admin_id'])
->where('split_link_id', (int) $ticket['split_link_id'])
->where('ticket_name', (string) $ticket['ticket_name'])
->select();
foreach ($numbers as $number) {
$visitCount = (int) $number['visit_count'];
$inboundCount = (int) $number['inbound_count'];
$lastInbound = (int) ($number['last_sync_inbound_count'] ?? 0);
$streak = (int) ($number['no_inbound_click_streak'] ?? 0);
if ($visitCount <= 0) {
2026-06-09 03:36:30 +08:00
$streak = 0;
} else {
$streak = $this->ratioDeferredService->computeStreakForSync($number, $streak);
2026-06-09 03:36:30 +08:00
}
$update = [
'no_inbound_click_streak' => $streak,
'last_sync_visit_count' => $visitCount,
'last_sync_inbound_count' => $inboundCount,
'updatetime' => time(),
];
$clearDeferred = [
'ratio_deferred' => 0,
'ratio_deferred_at' => null,
];
// 手动管理:更新统计;无效降权(无落地页访问)一并清除
2026-06-09 03:36:30 +08:00
if ((int) $number['manual_manage'] === 1) {
if ($this->ratioDeferredService->isInvalidDeferredRecord($number) || $visitCount <= 0) {
$update = array_merge($update, $clearDeferred);
if ($visitCount <= 0) {
$update['no_inbound_click_streak'] = 0;
}
}
2026-06-09 03:36:30 +08:00
Number::where('id', (int) $number['id'])->update($update);
continue;
}
// 同步裁决 ratio_deferred:同步时清除访问侧降权,由 streak 决定是否关号
$update = array_merge($update, $clearDeferred);
2026-06-09 03:36:30 +08:00
$update['status'] = $this->resolveAutomatedStatus(
$ticket,
(string) ($number['platform_status'] ?? 'unknown'),
$inboundCount,
$streak,
$orderLimit,
$assignRatio
);
Number::where('id', (int) $number['id'])->update($update);
}
}
/**
* 非手动管理号码的自动开关判定
*/
private function resolveAutomatedStatus(
Ticket $ticket,
string $platformStatus,
int $inboundCount,
int $streak,
int $orderLimit,
int $assignRatio
): string {
if ((string) ($ticket['status'] ?? 'hidden') !== 'normal') {
return 'hidden';
}
if ($platformStatus !== 'online') {
return 'hidden';
}
if ($orderLimit > 0 && $inboundCount >= $orderLimit) {
return 'hidden';
}
if ($assignRatio > 0 && $streak >= $assignRatio) {
return 'hidden';
}
return 'normal';
}
/**
* 完成量、时间窗口决定工单开关(定时与手动同步均适用)
*/
public function applyTicketStatusRules(Ticket $ticket, int $completeCount): void
{
$status = $this->resolveTicketStatus($ticket, $completeCount);
if ($status !== (string) ($ticket['status'] ?? 'hidden')) {
Ticket::where('id', (int) $ticket['id'])->update([
'status' => $status,
'updatetime' => time(),
]);
$ticket['status'] = $status;
}
}
/**
* 是否处于允许同步/开启的时间窗口
*/
public function isWithinTimeWindow(Ticket $ticket, ?int $now = null): bool
{
$now = $now ?? time();
$start = $ticket['start_time'] ?? null;
$end = $ticket['end_time'] ?? null;
if ($start !== null && $start !== '' && (int) $start > 0 && $now < (int) $start) {
return false;
}
if ($end !== null && $end !== '' && (int) $end > 0 && $now > (int) $end) {
return false;
}
return true;
}
private function resolveTicketStatus(Ticket $ticket, int $completeCount): string
{
if (!$this->isWithinTimeWindow($ticket)) {
return 'hidden';
}
$ticketTotal = (int) ($ticket['ticket_total'] ?? 0);
if ($ticketTotal > 0) {
return $completeCount >= $ticketTotal ? 'hidden' : 'normal';
}
return 'normal';
}
}