下号比率恢复 + 触线降权 + 同步裁决
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
-- 下号比率触线降权:访问侧标记 ratio_deferred,同步侧裁决 status
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `fa_split_number`
|
||||||
|
ADD COLUMN `ratio_deferred` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '下号比率触线降权:0=否,1=是(仍status=normal,选号时后置)' AFTER `no_inbound_click_streak`,
|
||||||
|
ADD COLUMN `ratio_deferred_at` bigint(16) DEFAULT NULL COMMENT '触线降权标记时间' AFTER `ratio_deferred`;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
use app\admin\model\split\Number;
|
||||||
|
use app\admin\model\split\Ticket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 落地页访问侧下号比率规则:更新连续无进线点击 streak,触线时仅降权(ratio_deferred),不关闭 status
|
||||||
|
*
|
||||||
|
* 最终关号由同步后 SplitTicketRuleService::applyNumberRules 裁决。
|
||||||
|
*/
|
||||||
|
class SplitNumberVisitRuleService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 访问计数已递增后调用:累加 streak,达到 assign_ratio 时标记 ratio_deferred=1
|
||||||
|
*/
|
||||||
|
public function recordVisitAfterRedirect(int $numberId): void
|
||||||
|
{
|
||||||
|
if ($numberId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Number|null $number */
|
||||||
|
$number = Number::get($numberId);
|
||||||
|
if ($number === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Ticket|null $ticket */
|
||||||
|
$ticket = Ticket::where('admin_id', (int) $number['admin_id'])
|
||||||
|
->where('split_link_id', (int) $number['split_link_id'])
|
||||||
|
->where('ticket_name', (string) $number['ticket_name'])
|
||||||
|
->find();
|
||||||
|
if ($ticket === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignRatio = (int) ($ticket['assign_ratio'] ?? 0);
|
||||||
|
$inboundCount = (int) $number['inbound_count'];
|
||||||
|
$lastInbound = (int) ($number['last_sync_inbound_count'] ?? 0);
|
||||||
|
$streak = (int) ($number['no_inbound_click_streak'] ?? 0);
|
||||||
|
|
||||||
|
// 自上次同步检查点以来进线未增长,则本次访问计入 streak
|
||||||
|
if ($inboundCount <= $lastInbound) {
|
||||||
|
$streak++;
|
||||||
|
} else {
|
||||||
|
$streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = [
|
||||||
|
'no_inbound_click_streak' => $streak,
|
||||||
|
'updatetime' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 非手动号码且达到下号比率:触线降权,不关 status,等待同步确认
|
||||||
|
if ((int) $number['manual_manage'] === 0 && $assignRatio > 0 && $streak >= $assignRatio) {
|
||||||
|
$update['ratio_deferred'] = 1;
|
||||||
|
$update['ratio_deferred_at'] = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
Number::where('id', $numberId)->update($update);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,11 +32,13 @@ class SplitNumberWeighService
|
|||||||
* @param int $linkId 分流链接 ID
|
* @param int $linkId 分流链接 ID
|
||||||
* @param int $numberCount 可用号码数量
|
* @param int $numberCount 可用号码数量
|
||||||
* @param SplitRoundRobinStore $roundRobinStore 关闭随机打乱时的轮转计数器
|
* @param SplitRoundRobinStore $roundRobinStore 关闭随机打乱时的轮转计数器
|
||||||
|
* @param string $poolKey 选号池标识(preferred / deferred 等)
|
||||||
*/
|
*/
|
||||||
public static function resolvePickIndex(
|
public static function resolvePickIndex(
|
||||||
int $linkId,
|
int $linkId,
|
||||||
int $numberCount,
|
int $numberCount,
|
||||||
SplitRoundRobinStore $roundRobinStore
|
SplitRoundRobinStore $roundRobinStore,
|
||||||
|
string $poolKey = 'main'
|
||||||
): int {
|
): int {
|
||||||
if ($numberCount <= 0) {
|
if ($numberCount <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -49,6 +51,26 @@ class SplitNumberWeighService
|
|||||||
return random_int(0, $numberCount - 1);
|
return random_int(0, $numberCount - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $roundRobinStore->nextIndex($linkId, $numberCount);
|
return $roundRobinStore->nextIndex($linkId, $numberCount, $poolKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从号码行列表中按链接配置选一条(严格轮转或随机)
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>|Number> $rows
|
||||||
|
* @return array<string, mixed>|Number|null
|
||||||
|
*/
|
||||||
|
public static function pickRowFromList(
|
||||||
|
int $linkId,
|
||||||
|
array $rows,
|
||||||
|
SplitRoundRobinStore $roundRobinStore,
|
||||||
|
string $poolKey = 'main'
|
||||||
|
) {
|
||||||
|
$count = count($rows);
|
||||||
|
if ($count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$index = self::resolvePickIndex($linkId, $count, $roundRobinStore, $poolKey);
|
||||||
|
return $rows[$index] ?? $rows[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,21 @@ use think\Collection;
|
|||||||
* 分流链接落地页:查链、跨工单合并选号、拼接跳转 URL、访问计数
|
* 分流链接落地页:查链、跨工单合并选号、拼接跳转 URL、访问计数
|
||||||
*
|
*
|
||||||
* 号码池为同一 split_link_id 下全部 status=normal 的号码;
|
* 号码池为同一 split_link_id 下全部 status=normal 的号码;
|
||||||
|
* 优先选用 ratio_deferred=0 的号码,全部触线降权时回退至降权池仍可轮转。
|
||||||
* random_shuffle 关闭时严格轮转,开启时每次访问随机选号。
|
* random_shuffle 关闭时严格轮转,开启时每次访问随机选号。
|
||||||
*/
|
*/
|
||||||
class SplitRedirectService
|
class SplitRedirectService
|
||||||
{
|
{
|
||||||
private SplitRoundRobinStore $roundRobinStore;
|
private SplitRoundRobinStore $roundRobinStore;
|
||||||
|
|
||||||
public function __construct(?SplitRoundRobinStore $roundRobinStore = null)
|
private SplitNumberVisitRuleService $visitRuleService;
|
||||||
{
|
|
||||||
|
public function __construct(
|
||||||
|
?SplitRoundRobinStore $roundRobinStore = null,
|
||||||
|
?SplitNumberVisitRuleService $visitRuleService = null
|
||||||
|
) {
|
||||||
$this->roundRobinStore = $roundRobinStore ?? new SplitRoundRobinStore();
|
$this->roundRobinStore = $roundRobinStore ?? new SplitRoundRobinStore();
|
||||||
|
$this->visitRuleService = $visitRuleService ?? new SplitNumberVisitRuleService();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,18 +59,16 @@ class SplitRedirectService
|
|||||||
$numbers = Number::where('split_link_id', (int) $link['id'])
|
$numbers = Number::where('split_link_id', (int) $link['id'])
|
||||||
->where('status', 'normal')
|
->where('status', 'normal')
|
||||||
->order('id', 'asc')
|
->order('id', 'asc')
|
||||||
->field('id,number,number_type,number_type_custom')
|
->field('id,number,number_type,number_type_custom,ratio_deferred')
|
||||||
->select();
|
->select();
|
||||||
|
|
||||||
$count = is_countable($numbers) ? count($numbers) : 0;
|
$list = $numbers instanceof Collection ? $numbers->all() : (array) $numbers;
|
||||||
if ($count === 0) {
|
if ($list === []) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$linkId = (int) $link->getAttr('id');
|
$linkId = (int) $link->getAttr('id');
|
||||||
$index = SplitNumberWeighService::resolvePickIndex($linkId, $count, $this->roundRobinStore);
|
$picked = $this->pickNumberRow($linkId, $list);
|
||||||
$list = $numbers instanceof \think\Collection ? $numbers->all() : (array) $numbers;
|
|
||||||
$picked = $list[$index] ?? $list[0] ?? null;
|
|
||||||
if ($picked === null) {
|
if ($picked === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -93,8 +97,38 @@ class SplitRedirectService
|
|||||||
$numberId = is_array($picked) ? (int) ($picked['id'] ?? 0) : (int) $picked->getAttr('id');
|
$numberId = is_array($picked) ? (int) ($picked['id'] ?? 0) : (int) $picked->getAttr('id');
|
||||||
if ($numberId > 0) {
|
if ($numberId > 0) {
|
||||||
Number::where('id', $numberId)->setInc('visit_count');
|
Number::where('id', $numberId)->setInc('visit_count');
|
||||||
|
$this->visitRuleService->recordVisitAfterRedirect($numberId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $redirectUrl;
|
return $redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双层选号:优先 ratio_deferred=0,否则回退至降权池
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>|Number> $rows
|
||||||
|
* @return array<string, mixed>|Number|null
|
||||||
|
*/
|
||||||
|
private function pickNumberRow(int $linkId, array $rows)
|
||||||
|
{
|
||||||
|
$preferred = [];
|
||||||
|
$deferred = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$deferredFlag = is_array($row)
|
||||||
|
? (int) ($row['ratio_deferred'] ?? 0)
|
||||||
|
: (int) $row->getAttr('ratio_deferred');
|
||||||
|
if ($deferredFlag === 1) {
|
||||||
|
$deferred[] = $row;
|
||||||
|
} else {
|
||||||
|
$preferred[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$picked = SplitNumberWeighService::pickRowFromList($linkId, $preferred, $this->roundRobinStore, 'preferred');
|
||||||
|
if ($picked !== null) {
|
||||||
|
return $picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SplitNumberWeighService::pickRowFromList($linkId, $deferred, $this->roundRobinStore, 'deferred');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,19 @@ class SplitRoundRobinStore
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取本次访问应使用的号码下标(0 .. count-1)
|
* 获取本次访问应使用的号码下标(0 .. count-1)
|
||||||
|
*
|
||||||
|
* @param string $poolKey 选号池标识(如 preferred / deferred),用于双层选号独立轮转
|
||||||
*/
|
*/
|
||||||
public function nextIndex(int $splitLinkId, int $numberCount): int
|
public function nextIndex(int $splitLinkId, int $numberCount, string $poolKey = 'main'): int
|
||||||
{
|
{
|
||||||
if ($splitLinkId <= 0 || $numberCount <= 0) {
|
if ($splitLinkId <= 0 || $numberCount <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$poolKey = $this->sanitizePoolKey($poolKey);
|
||||||
|
|
||||||
if ($this->ensureRedis()) {
|
if ($this->ensureRedis()) {
|
||||||
$key = self::KEY_PREFIX . $splitLinkId;
|
$key = self::KEY_PREFIX . $splitLinkId . ':' . $poolKey;
|
||||||
$seq = (int) $this->redis->incr($key);
|
$seq = (int) $this->redis->incr($key);
|
||||||
if ($seq <= 0) {
|
if ($seq <= 0) {
|
||||||
$seq = 1;
|
$seq = 1;
|
||||||
@@ -39,7 +43,16 @@ class SplitRoundRobinStore
|
|||||||
return ($seq - 1) % $numberCount;
|
return ($seq - 1) % $numberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->nextIndexFallback($splitLinkId, $numberCount);
|
return $this->nextIndexFallback($splitLinkId, $numberCount, $poolKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选号池 key 仅允许字母数字与下划线,避免 Redis key 注入
|
||||||
|
*/
|
||||||
|
private function sanitizePoolKey(string $poolKey): string
|
||||||
|
{
|
||||||
|
$poolKey = preg_replace('/[^a-zA-Z0-9_]/', '', $poolKey) ?? '';
|
||||||
|
return $poolKey !== '' ? $poolKey : 'main';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,14 +115,14 @@ class SplitRoundRobinStore
|
|||||||
/**
|
/**
|
||||||
* 无 Redis 时使用 runtime 文件锁递增(开发/单机可用;生产请启用 Redis)
|
* 无 Redis 时使用 runtime 文件锁递增(开发/单机可用;生产请启用 Redis)
|
||||||
*/
|
*/
|
||||||
private function nextIndexFallback(int $splitLinkId, int $numberCount): int
|
private function nextIndexFallback(int $splitLinkId, int $numberCount, string $poolKey = 'main'): int
|
||||||
{
|
{
|
||||||
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
||||||
$dir = $runtime . 'split_rr/';
|
$dir = $runtime . 'split_rr/';
|
||||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$file = $dir . $splitLinkId . '.cnt';
|
$file = $dir . $splitLinkId . '_' . $poolKey . '.cnt';
|
||||||
$fp = @fopen($file, 'c+');
|
$fp = @fopen($file, 'c+');
|
||||||
if ($fp === false) {
|
if ($fp === false) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use think\Db;
|
|||||||
* 工单同步结果写入号码表
|
* 工单同步结果写入号码表
|
||||||
*
|
*
|
||||||
* 同步策略(全量镜像):
|
* 同步策略(全量镜像):
|
||||||
* - 爬虫返回的号码:写入 platform_status / inbound_count,开关 status 与云控在线状态一致
|
* - 爬虫返回的号码:写入 platform_status / inbound_count;status 由后续 applyNumberRules 裁决
|
||||||
* - 爬虫未返回且 manual_manage=0:物理删除
|
* - 爬虫未返回且 manual_manage=0:物理删除
|
||||||
* - manual_manage=1:不删除,仅当仍在爬虫结果中时更新 platform_status
|
* - manual_manage=1:不删除,仅当仍在爬虫结果中时更新 platform_status
|
||||||
*/
|
*/
|
||||||
@@ -108,27 +108,11 @@ class SplitTicketNumberSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工单手动开启等非同步场景:按已存的 platform_status 对齐开关(不跑单号上限/下号比率)
|
* 工单手动开启等非同步场景:按业务规则对齐号码开关(平台 + 单号上限 + 下号比率)
|
||||||
*/
|
*/
|
||||||
public function applyStatusFromPlatformSnapshot(Ticket $ticket): void
|
public function applyStatusFromPlatformSnapshot(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
if ((string) ($ticket['status'] ?? 'hidden') !== 'normal') {
|
(new SplitTicketRuleService())->applyNumberRules($ticket);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$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'])
|
|
||||||
->where('manual_manage', 0)
|
|
||||||
->select();
|
|
||||||
|
|
||||||
foreach ($numbers as $number) {
|
|
||||||
$platformStatus = (string) ($number['platform_status'] ?? 'offline');
|
|
||||||
Number::where('id', (int) $number['id'])->update([
|
|
||||||
'status' => self::resolveSwitchStatus($ticket, $platformStatus),
|
|
||||||
'updatetime' => time(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,7 +147,6 @@ class SplitTicketNumberSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$update['inbound_count'] = max(0, $newFollowers);
|
$update['inbound_count'] = max(0, $newFollowers);
|
||||||
$update['status'] = self::resolveSwitchStatus($ticket, $platformStatus);
|
|
||||||
Number::where('id', (int) $row['id'])->update($update);
|
Number::where('id', (int) $row['id'])->update($update);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +168,7 @@ class SplitTicketNumberSyncService
|
|||||||
'inbound_count' => max(0, $newFollowers),
|
'inbound_count' => max(0, $newFollowers),
|
||||||
'manual_manage' => 0,
|
'manual_manage' => 0,
|
||||||
'platform_status' => $platformStatus,
|
'platform_status' => $platformStatus,
|
||||||
'status' => self::resolveSwitchStatus($ticket, $platformStatus),
|
'ratio_deferred' => 0,
|
||||||
'createtime' => $now,
|
'createtime' => $now,
|
||||||
'updatetime' => $now,
|
'updatetime' => $now,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use app\admin\model\split\Ticket;
|
|||||||
class SplitTicketRuleService
|
class SplitTicketRuleService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 同步后应用工单开关规则;号码开关以爬虫/platform 快照为准(不再跑单号上限/下号比率)
|
* 同步后应用工单开关规则;号码开关由 applyNumberRules 综合裁决(平台在线 + 单号上限 + 下号比率)
|
||||||
*/
|
*/
|
||||||
public function applyAfterSync(Ticket $ticket, int $completeCount): void
|
public function applyAfterSync(Ticket $ticket, int $completeCount): void
|
||||||
{
|
{
|
||||||
@@ -23,7 +23,7 @@ class SplitTicketRuleService
|
|||||||
if ((string) $fresh['status'] === 'hidden') {
|
if ((string) $fresh['status'] === 'hidden') {
|
||||||
$this->cascadeTicketClosedToNumbers($fresh);
|
$this->cascadeTicketClosedToNumbers($fresh);
|
||||||
} else {
|
} else {
|
||||||
(new SplitTicketNumberSyncService())->applyStatusFromPlatformSnapshot($fresh);
|
$this->applyNumberRules($fresh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ class SplitTicketRuleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动切换工单状态时,非手动号码按 platform_status 对齐开关
|
* 手动切换工单状态时,非手动号码按业务规则对齐开关
|
||||||
*/
|
*/
|
||||||
public function syncNumbersWithTicketStatus(Ticket $ticket): void
|
public function syncNumbersWithTicketStatus(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
@@ -79,21 +79,21 @@ class SplitTicketRuleService
|
|||||||
$this->cascadeTicketClosedToNumbers($ticket);
|
$this->cascadeTicketClosedToNumbers($ticket);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(new SplitTicketNumberSyncService())->applyStatusFromPlatformSnapshot($ticket);
|
$this->applyNumberRules($ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工单处于开启状态时,将云控在线的非手动号码设为开启
|
* 工单处于开启状态时,将云控在线的非手动号码设为开启
|
||||||
*
|
*
|
||||||
* @deprecated 请使用 applyStatusFromPlatformSnapshot(以 platform_status 为准)
|
* @deprecated 请使用 applyNumberRules
|
||||||
*/
|
*/
|
||||||
public function syncOnlineNumbersWhenTicketOpen(Ticket $ticket): void
|
public function syncOnlineNumbersWhenTicketOpen(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
(new SplitTicketNumberSyncService())->applyStatusFromPlatformSnapshot($ticket);
|
$this->applyNumberRules($ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单号上限、下号比率、云控在线状态、工单开关综合决定号码状态
|
* 单号上限、下号比率、云控在线状态、工单开关综合决定号码 status;同步时清除或确认 ratio_deferred
|
||||||
*/
|
*/
|
||||||
public function applyNumberRules(Ticket $ticket): void
|
public function applyNumberRules(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
@@ -125,12 +125,21 @@ class SplitTicketRuleService
|
|||||||
'updatetime' => time(),
|
'updatetime' => time(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 手动管理(含用户手动关闭)的号码:仅更新统计字段,不改状态
|
// 手动管理(含用户手动关闭)的号码:仅更新统计字段,不改状态与降权标记
|
||||||
if ((int) $number['manual_manage'] === 1) {
|
if ((int) $number['manual_manage'] === 1) {
|
||||||
Number::where('id', (int) $number['id'])->update($update);
|
Number::where('id', (int) $number['id'])->update($update);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步裁决 ratio_deferred:进线增长则解除降权;触线且无进线增长则关号并清除降权
|
||||||
|
if ($inboundCount > $lastInbound) {
|
||||||
|
$update['ratio_deferred'] = 0;
|
||||||
|
$update['ratio_deferred_at'] = null;
|
||||||
|
} else {
|
||||||
|
$update['ratio_deferred'] = 0;
|
||||||
|
$update['ratio_deferred_at'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
$update['status'] = $this->resolveAutomatedStatus(
|
$update['status'] = $this->resolveAutomatedStatus(
|
||||||
$ticket,
|
$ticket,
|
||||||
(string) ($number['platform_status'] ?? 'unknown'),
|
(string) ($number['platform_status'] ?? 'unknown'),
|
||||||
|
|||||||
@@ -555,6 +555,8 @@ class SplitTicketSyncService
|
|||||||
$freshTicket = Ticket::get((int) $ticket['id']) ?: $ticket;
|
$freshTicket = Ticket::get((int) $ticket['id']) ?: $ticket;
|
||||||
if ((string) $freshTicket['status'] === 'hidden') {
|
if ((string) $freshTicket['status'] === 'hidden') {
|
||||||
$this->ruleService->cascadeTicketClosedToNumbers($freshTicket);
|
$this->ruleService->cascadeTicketClosedToNumbers($freshTicket);
|
||||||
|
} else {
|
||||||
|
$this->ruleService->applyNumberRules($freshTicket);
|
||||||
}
|
}
|
||||||
$ticket = $freshTicket;
|
$ticket = $freshTicket;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
-- 下号比率触线降权:访问侧标记 ratio_deferred,同步侧裁决 status
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
|
||||||
|
ALTER TABLE `fa_split_number`
|
||||||
|
ADD COLUMN `ratio_deferred` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '下号比率触线降权:0=否,1=是(仍status=normal,选号时后置)' AFTER `no_inbound_click_streak`,
|
||||||
|
ADD COLUMN `ratio_deferred_at` bigint(16) DEFAULT NULL COMMENT '触线降权标记时间' AFTER `ratio_deferred`;
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace app\common\service;
|
||||||
|
|
||||||
|
use app\admin\model\split\Number;
|
||||||
|
use app\admin\model\split\Ticket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 落地页访问侧下号比率规则:更新连续无进线点击 streak,触线时仅降权(ratio_deferred),不关闭 status
|
||||||
|
*
|
||||||
|
* 最终关号由同步后 SplitTicketRuleService::applyNumberRules 裁决。
|
||||||
|
*/
|
||||||
|
class SplitNumberVisitRuleService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 访问计数已递增后调用:累加 streak,达到 assign_ratio 时标记 ratio_deferred=1
|
||||||
|
*/
|
||||||
|
public function recordVisitAfterRedirect(int $numberId): void
|
||||||
|
{
|
||||||
|
if ($numberId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Number|null $number */
|
||||||
|
$number = Number::get($numberId);
|
||||||
|
if ($number === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var Ticket|null $ticket */
|
||||||
|
$ticket = Ticket::where('admin_id', (int) $number['admin_id'])
|
||||||
|
->where('split_link_id', (int) $number['split_link_id'])
|
||||||
|
->where('ticket_name', (string) $number['ticket_name'])
|
||||||
|
->find();
|
||||||
|
if ($ticket === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$assignRatio = (int) ($ticket['assign_ratio'] ?? 0);
|
||||||
|
$inboundCount = (int) $number['inbound_count'];
|
||||||
|
$lastInbound = (int) ($number['last_sync_inbound_count'] ?? 0);
|
||||||
|
$streak = (int) ($number['no_inbound_click_streak'] ?? 0);
|
||||||
|
|
||||||
|
// 自上次同步检查点以来进线未增长,则本次访问计入 streak
|
||||||
|
if ($inboundCount <= $lastInbound) {
|
||||||
|
$streak++;
|
||||||
|
} else {
|
||||||
|
$streak = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$update = [
|
||||||
|
'no_inbound_click_streak' => $streak,
|
||||||
|
'updatetime' => time(),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 非手动号码且达到下号比率:触线降权,不关 status,等待同步确认
|
||||||
|
if ((int) $number['manual_manage'] === 0 && $assignRatio > 0 && $streak >= $assignRatio) {
|
||||||
|
$update['ratio_deferred'] = 1;
|
||||||
|
$update['ratio_deferred_at'] = time();
|
||||||
|
}
|
||||||
|
|
||||||
|
Number::where('id', $numberId)->update($update);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,11 +32,13 @@ class SplitNumberWeighService
|
|||||||
* @param int $linkId 分流链接 ID
|
* @param int $linkId 分流链接 ID
|
||||||
* @param int $numberCount 可用号码数量
|
* @param int $numberCount 可用号码数量
|
||||||
* @param SplitRoundRobinStore $roundRobinStore 关闭随机打乱时的轮转计数器
|
* @param SplitRoundRobinStore $roundRobinStore 关闭随机打乱时的轮转计数器
|
||||||
|
* @param string $poolKey 选号池标识(preferred / deferred 等)
|
||||||
*/
|
*/
|
||||||
public static function resolvePickIndex(
|
public static function resolvePickIndex(
|
||||||
int $linkId,
|
int $linkId,
|
||||||
int $numberCount,
|
int $numberCount,
|
||||||
SplitRoundRobinStore $roundRobinStore
|
SplitRoundRobinStore $roundRobinStore,
|
||||||
|
string $poolKey = 'main'
|
||||||
): int {
|
): int {
|
||||||
if ($numberCount <= 0) {
|
if ($numberCount <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -49,6 +51,26 @@ class SplitNumberWeighService
|
|||||||
return random_int(0, $numberCount - 1);
|
return random_int(0, $numberCount - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $roundRobinStore->nextIndex($linkId, $numberCount);
|
return $roundRobinStore->nextIndex($linkId, $numberCount, $poolKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从号码行列表中按链接配置选一条(严格轮转或随机)
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>|Number> $rows
|
||||||
|
* @return array<string, mixed>|Number|null
|
||||||
|
*/
|
||||||
|
public static function pickRowFromList(
|
||||||
|
int $linkId,
|
||||||
|
array $rows,
|
||||||
|
SplitRoundRobinStore $roundRobinStore,
|
||||||
|
string $poolKey = 'main'
|
||||||
|
) {
|
||||||
|
$count = count($rows);
|
||||||
|
if ($count === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
$index = self::resolvePickIndex($linkId, $count, $roundRobinStore, $poolKey);
|
||||||
|
return $rows[$index] ?? $rows[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,15 +12,21 @@ use think\Collection;
|
|||||||
* 分流链接落地页:查链、跨工单合并选号、拼接跳转 URL、访问计数
|
* 分流链接落地页:查链、跨工单合并选号、拼接跳转 URL、访问计数
|
||||||
*
|
*
|
||||||
* 号码池为同一 split_link_id 下全部 status=normal 的号码;
|
* 号码池为同一 split_link_id 下全部 status=normal 的号码;
|
||||||
|
* 优先选用 ratio_deferred=0 的号码,全部触线降权时回退至降权池仍可轮转。
|
||||||
* random_shuffle 关闭时严格轮转,开启时每次访问随机选号。
|
* random_shuffle 关闭时严格轮转,开启时每次访问随机选号。
|
||||||
*/
|
*/
|
||||||
class SplitRedirectService
|
class SplitRedirectService
|
||||||
{
|
{
|
||||||
private SplitRoundRobinStore $roundRobinStore;
|
private SplitRoundRobinStore $roundRobinStore;
|
||||||
|
|
||||||
public function __construct(?SplitRoundRobinStore $roundRobinStore = null)
|
private SplitNumberVisitRuleService $visitRuleService;
|
||||||
{
|
|
||||||
|
public function __construct(
|
||||||
|
?SplitRoundRobinStore $roundRobinStore = null,
|
||||||
|
?SplitNumberVisitRuleService $visitRuleService = null
|
||||||
|
) {
|
||||||
$this->roundRobinStore = $roundRobinStore ?? new SplitRoundRobinStore();
|
$this->roundRobinStore = $roundRobinStore ?? new SplitRoundRobinStore();
|
||||||
|
$this->visitRuleService = $visitRuleService ?? new SplitNumberVisitRuleService();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,18 +59,16 @@ class SplitRedirectService
|
|||||||
$numbers = Number::where('split_link_id', (int) $link['id'])
|
$numbers = Number::where('split_link_id', (int) $link['id'])
|
||||||
->where('status', 'normal')
|
->where('status', 'normal')
|
||||||
->order('id', 'asc')
|
->order('id', 'asc')
|
||||||
->field('id,number,number_type,number_type_custom')
|
->field('id,number,number_type,number_type_custom,ratio_deferred')
|
||||||
->select();
|
->select();
|
||||||
|
|
||||||
$count = is_countable($numbers) ? count($numbers) : 0;
|
$list = $numbers instanceof Collection ? $numbers->all() : (array) $numbers;
|
||||||
if ($count === 0) {
|
if ($list === []) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$linkId = (int) $link->getAttr('id');
|
$linkId = (int) $link->getAttr('id');
|
||||||
$index = SplitNumberWeighService::resolvePickIndex($linkId, $count, $this->roundRobinStore);
|
$picked = $this->pickNumberRow($linkId, $list);
|
||||||
$list = $numbers instanceof \think\Collection ? $numbers->all() : (array) $numbers;
|
|
||||||
$picked = $list[$index] ?? $list[0] ?? null;
|
|
||||||
if ($picked === null) {
|
if ($picked === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -93,8 +97,38 @@ class SplitRedirectService
|
|||||||
$numberId = is_array($picked) ? (int) ($picked['id'] ?? 0) : (int) $picked->getAttr('id');
|
$numberId = is_array($picked) ? (int) ($picked['id'] ?? 0) : (int) $picked->getAttr('id');
|
||||||
if ($numberId > 0) {
|
if ($numberId > 0) {
|
||||||
Number::where('id', $numberId)->setInc('visit_count');
|
Number::where('id', $numberId)->setInc('visit_count');
|
||||||
|
$this->visitRuleService->recordVisitAfterRedirect($numberId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $redirectUrl;
|
return $redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双层选号:优先 ratio_deferred=0,否则回退至降权池
|
||||||
|
*
|
||||||
|
* @param array<int, array<string, mixed>|Number> $rows
|
||||||
|
* @return array<string, mixed>|Number|null
|
||||||
|
*/
|
||||||
|
private function pickNumberRow(int $linkId, array $rows)
|
||||||
|
{
|
||||||
|
$preferred = [];
|
||||||
|
$deferred = [];
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
$deferredFlag = is_array($row)
|
||||||
|
? (int) ($row['ratio_deferred'] ?? 0)
|
||||||
|
: (int) $row->getAttr('ratio_deferred');
|
||||||
|
if ($deferredFlag === 1) {
|
||||||
|
$deferred[] = $row;
|
||||||
|
} else {
|
||||||
|
$preferred[] = $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$picked = SplitNumberWeighService::pickRowFromList($linkId, $preferred, $this->roundRobinStore, 'preferred');
|
||||||
|
if ($picked !== null) {
|
||||||
|
return $picked;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SplitNumberWeighService::pickRowFromList($linkId, $deferred, $this->roundRobinStore, 'deferred');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,15 +22,19 @@ class SplitRoundRobinStore
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取本次访问应使用的号码下标(0 .. count-1)
|
* 获取本次访问应使用的号码下标(0 .. count-1)
|
||||||
|
*
|
||||||
|
* @param string $poolKey 选号池标识(如 preferred / deferred),用于双层选号独立轮转
|
||||||
*/
|
*/
|
||||||
public function nextIndex(int $splitLinkId, int $numberCount): int
|
public function nextIndex(int $splitLinkId, int $numberCount, string $poolKey = 'main'): int
|
||||||
{
|
{
|
||||||
if ($splitLinkId <= 0 || $numberCount <= 0) {
|
if ($splitLinkId <= 0 || $numberCount <= 0) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$poolKey = $this->sanitizePoolKey($poolKey);
|
||||||
|
|
||||||
if ($this->ensureRedis()) {
|
if ($this->ensureRedis()) {
|
||||||
$key = self::KEY_PREFIX . $splitLinkId;
|
$key = self::KEY_PREFIX . $splitLinkId . ':' . $poolKey;
|
||||||
$seq = (int) $this->redis->incr($key);
|
$seq = (int) $this->redis->incr($key);
|
||||||
if ($seq <= 0) {
|
if ($seq <= 0) {
|
||||||
$seq = 1;
|
$seq = 1;
|
||||||
@@ -39,7 +43,16 @@ class SplitRoundRobinStore
|
|||||||
return ($seq - 1) % $numberCount;
|
return ($seq - 1) % $numberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->nextIndexFallback($splitLinkId, $numberCount);
|
return $this->nextIndexFallback($splitLinkId, $numberCount, $poolKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选号池 key 仅允许字母数字与下划线,避免 Redis key 注入
|
||||||
|
*/
|
||||||
|
private function sanitizePoolKey(string $poolKey): string
|
||||||
|
{
|
||||||
|
$poolKey = preg_replace('/[^a-zA-Z0-9_]/', '', $poolKey) ?? '';
|
||||||
|
return $poolKey !== '' ? $poolKey : 'main';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,14 +115,14 @@ class SplitRoundRobinStore
|
|||||||
/**
|
/**
|
||||||
* 无 Redis 时使用 runtime 文件锁递增(开发/单机可用;生产请启用 Redis)
|
* 无 Redis 时使用 runtime 文件锁递增(开发/单机可用;生产请启用 Redis)
|
||||||
*/
|
*/
|
||||||
private function nextIndexFallback(int $splitLinkId, int $numberCount): int
|
private function nextIndexFallback(int $splitLinkId, int $numberCount, string $poolKey = 'main'): int
|
||||||
{
|
{
|
||||||
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
$runtime = defined('RUNTIME_PATH') ? RUNTIME_PATH : (dirname(__DIR__, 3) . '/runtime/');
|
||||||
$dir = $runtime . 'split_rr/';
|
$dir = $runtime . 'split_rr/';
|
||||||
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
if (!is_dir($dir) && !@mkdir($dir, 0755, true)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
$file = $dir . $splitLinkId . '.cnt';
|
$file = $dir . $splitLinkId . '_' . $poolKey . '.cnt';
|
||||||
$fp = @fopen($file, 'c+');
|
$fp = @fopen($file, 'c+');
|
||||||
if ($fp === false) {
|
if ($fp === false) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use think\Db;
|
|||||||
* 工单同步结果写入号码表
|
* 工单同步结果写入号码表
|
||||||
*
|
*
|
||||||
* 同步策略(全量镜像):
|
* 同步策略(全量镜像):
|
||||||
* - 爬虫返回的号码:写入 platform_status / inbound_count,开关 status 与云控在线状态一致
|
* - 爬虫返回的号码:写入 platform_status / inbound_count;status 由后续 applyNumberRules 裁决
|
||||||
* - 爬虫未返回且 manual_manage=0:物理删除
|
* - 爬虫未返回且 manual_manage=0:物理删除
|
||||||
* - manual_manage=1:不删除,仅当仍在爬虫结果中时更新 platform_status
|
* - manual_manage=1:不删除,仅当仍在爬虫结果中时更新 platform_status
|
||||||
*/
|
*/
|
||||||
@@ -108,27 +108,11 @@ class SplitTicketNumberSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工单手动开启等非同步场景:按已存的 platform_status 对齐开关(不跑单号上限/下号比率)
|
* 工单手动开启等非同步场景:按业务规则对齐号码开关(平台 + 单号上限 + 下号比率)
|
||||||
*/
|
*/
|
||||||
public function applyStatusFromPlatformSnapshot(Ticket $ticket): void
|
public function applyStatusFromPlatformSnapshot(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
if ((string) ($ticket['status'] ?? 'hidden') !== 'normal') {
|
(new SplitTicketRuleService())->applyNumberRules($ticket);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$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'])
|
|
||||||
->where('manual_manage', 0)
|
|
||||||
->select();
|
|
||||||
|
|
||||||
foreach ($numbers as $number) {
|
|
||||||
$platformStatus = (string) ($number['platform_status'] ?? 'offline');
|
|
||||||
Number::where('id', (int) $number['id'])->update([
|
|
||||||
'status' => self::resolveSwitchStatus($ticket, $platformStatus),
|
|
||||||
'updatetime' => time(),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,7 +147,6 @@ class SplitTicketNumberSyncService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$update['inbound_count'] = max(0, $newFollowers);
|
$update['inbound_count'] = max(0, $newFollowers);
|
||||||
$update['status'] = self::resolveSwitchStatus($ticket, $platformStatus);
|
|
||||||
Number::where('id', (int) $row['id'])->update($update);
|
Number::where('id', (int) $row['id'])->update($update);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +168,7 @@ class SplitTicketNumberSyncService
|
|||||||
'inbound_count' => max(0, $newFollowers),
|
'inbound_count' => max(0, $newFollowers),
|
||||||
'manual_manage' => 0,
|
'manual_manage' => 0,
|
||||||
'platform_status' => $platformStatus,
|
'platform_status' => $platformStatus,
|
||||||
'status' => self::resolveSwitchStatus($ticket, $platformStatus),
|
'ratio_deferred' => 0,
|
||||||
'createtime' => $now,
|
'createtime' => $now,
|
||||||
'updatetime' => $now,
|
'updatetime' => $now,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ use app\admin\model\split\Ticket;
|
|||||||
class SplitTicketRuleService
|
class SplitTicketRuleService
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* 同步后应用工单开关规则;号码开关以爬虫/platform 快照为准(不再跑单号上限/下号比率)
|
* 同步后应用工单开关规则;号码开关由 applyNumberRules 综合裁决(平台在线 + 单号上限 + 下号比率)
|
||||||
*/
|
*/
|
||||||
public function applyAfterSync(Ticket $ticket, int $completeCount): void
|
public function applyAfterSync(Ticket $ticket, int $completeCount): void
|
||||||
{
|
{
|
||||||
@@ -23,7 +23,7 @@ class SplitTicketRuleService
|
|||||||
if ((string) $fresh['status'] === 'hidden') {
|
if ((string) $fresh['status'] === 'hidden') {
|
||||||
$this->cascadeTicketClosedToNumbers($fresh);
|
$this->cascadeTicketClosedToNumbers($fresh);
|
||||||
} else {
|
} else {
|
||||||
(new SplitTicketNumberSyncService())->applyStatusFromPlatformSnapshot($fresh);
|
$this->applyNumberRules($fresh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,7 +70,7 @@ class SplitTicketRuleService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 手动切换工单状态时,非手动号码按 platform_status 对齐开关
|
* 手动切换工单状态时,非手动号码按业务规则对齐开关
|
||||||
*/
|
*/
|
||||||
public function syncNumbersWithTicketStatus(Ticket $ticket): void
|
public function syncNumbersWithTicketStatus(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
@@ -79,21 +79,21 @@ class SplitTicketRuleService
|
|||||||
$this->cascadeTicketClosedToNumbers($ticket);
|
$this->cascadeTicketClosedToNumbers($ticket);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(new SplitTicketNumberSyncService())->applyStatusFromPlatformSnapshot($ticket);
|
$this->applyNumberRules($ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 工单处于开启状态时,将云控在线的非手动号码设为开启
|
* 工单处于开启状态时,将云控在线的非手动号码设为开启
|
||||||
*
|
*
|
||||||
* @deprecated 请使用 applyStatusFromPlatformSnapshot(以 platform_status 为准)
|
* @deprecated 请使用 applyNumberRules
|
||||||
*/
|
*/
|
||||||
public function syncOnlineNumbersWhenTicketOpen(Ticket $ticket): void
|
public function syncOnlineNumbersWhenTicketOpen(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
(new SplitTicketNumberSyncService())->applyStatusFromPlatformSnapshot($ticket);
|
$this->applyNumberRules($ticket);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 单号上限、下号比率、云控在线状态、工单开关综合决定号码状态
|
* 单号上限、下号比率、云控在线状态、工单开关综合决定号码 status;同步时清除或确认 ratio_deferred
|
||||||
*/
|
*/
|
||||||
public function applyNumberRules(Ticket $ticket): void
|
public function applyNumberRules(Ticket $ticket): void
|
||||||
{
|
{
|
||||||
@@ -125,12 +125,21 @@ class SplitTicketRuleService
|
|||||||
'updatetime' => time(),
|
'updatetime' => time(),
|
||||||
];
|
];
|
||||||
|
|
||||||
// 手动管理(含用户手动关闭)的号码:仅更新统计字段,不改状态
|
// 手动管理(含用户手动关闭)的号码:仅更新统计字段,不改状态与降权标记
|
||||||
if ((int) $number['manual_manage'] === 1) {
|
if ((int) $number['manual_manage'] === 1) {
|
||||||
Number::where('id', (int) $number['id'])->update($update);
|
Number::where('id', (int) $number['id'])->update($update);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同步裁决 ratio_deferred:进线增长则解除降权;触线且无进线增长则关号并清除降权
|
||||||
|
if ($inboundCount > $lastInbound) {
|
||||||
|
$update['ratio_deferred'] = 0;
|
||||||
|
$update['ratio_deferred_at'] = null;
|
||||||
|
} else {
|
||||||
|
$update['ratio_deferred'] = 0;
|
||||||
|
$update['ratio_deferred_at'] = null;
|
||||||
|
}
|
||||||
|
|
||||||
$update['status'] = $this->resolveAutomatedStatus(
|
$update['status'] = $this->resolveAutomatedStatus(
|
||||||
$ticket,
|
$ticket,
|
||||||
(string) ($number['platform_status'] ?? 'unknown'),
|
(string) ($number['platform_status'] ?? 'unknown'),
|
||||||
|
|||||||
@@ -555,6 +555,8 @@ class SplitTicketSyncService
|
|||||||
$freshTicket = Ticket::get((int) $ticket['id']) ?: $ticket;
|
$freshTicket = Ticket::get((int) $ticket['id']) ?: $ticket;
|
||||||
if ((string) $freshTicket['status'] === 'hidden') {
|
if ((string) $freshTicket['status'] === 'hidden') {
|
||||||
$this->ruleService->cascadeTicketClosedToNumbers($freshTicket);
|
$this->ruleService->cascadeTicketClosedToNumbers($freshTicket);
|
||||||
|
} else {
|
||||||
|
$this->ruleService->applyNumberRules($freshTicket);
|
||||||
}
|
}
|
||||||
$ticket = $freshTicket;
|
$ticket = $freshTicket;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user