169 lines
4.7 KiB
PHP
169 lines
4.7 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
declare(strict_types=1);
|
||
|
|
|
||
|
|
namespace app\common\service;
|
||
|
|
|
||
|
|
use app\admin\model\split\Ticket;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 工单同步失败分阶段重试状态机
|
||
|
|
*
|
||
|
|
* 达连续失败阈值后按配置时间点(相对锚点的绝对分钟偏移)自动重试;
|
||
|
|
* 任一次成功则清零恢复常态,全部失败则永久终止自动同步。
|
||
|
|
*/
|
||
|
|
class SplitTicketSyncFailRetryService
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* 是否已永久终止自动同步(重试时间点已全部用尽)
|
||
|
|
*
|
||
|
|
* @param Ticket|array<string, mixed> $ticket
|
||
|
|
*/
|
||
|
|
public function isPermanentlyStopped($ticket): bool
|
||
|
|
{
|
||
|
|
return (int) ($this->readField($ticket, 'sync_auto_sync_stopped') ?? 0) === 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 是否处于失败重试等待/重试周期内(未永久终止)
|
||
|
|
*
|
||
|
|
* @param Ticket|array<string, mixed> $ticket
|
||
|
|
*/
|
||
|
|
public function isInRetryMode($ticket): bool
|
||
|
|
{
|
||
|
|
if ($this->isPermanentlyStopped($ticket)) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
return (int) ($this->readField($ticket, 'sync_fail_pause_at') ?? 0) > 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 首次达失败阈值时进入重试态(工单保持 normal,不关单)
|
||
|
|
*
|
||
|
|
* @return array<string, mixed> 需合并进 save 的字段;无变更时返回空数组
|
||
|
|
*/
|
||
|
|
public function enterRetryIfNeeded(Ticket $ticket, int $failCount, int $threshold): array
|
||
|
|
{
|
||
|
|
if ($failCount < $threshold || !SplitSyncConfigService::hasFailRetrySchedule()) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
if ($this->isInRetryMode($ticket)) {
|
||
|
|
return [];
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'sync_fail_pause_at' => time(),
|
||
|
|
'sync_fail_retry_index' => 0,
|
||
|
|
'sync_auto_sync_stopped' => 0,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 计划内重试失败后递进索引;用尽全部时间点后标记永久终止
|
||
|
|
*
|
||
|
|
* @return array<string, mixed>
|
||
|
|
*/
|
||
|
|
public function onRetryFailure(Ticket $ticket): array
|
||
|
|
{
|
||
|
|
$schedule = SplitSyncConfigService::getFailRetryMinutes();
|
||
|
|
$index = (int) ($ticket['sync_fail_retry_index'] ?? 0) + 1;
|
||
|
|
$update = ['sync_fail_retry_index' => $index];
|
||
|
|
|
||
|
|
if ($index >= count($schedule)) {
|
||
|
|
$update['sync_auto_sync_stopped'] = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $update;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 同步成功时清零失败计数与重试相关字段
|
||
|
|
*
|
||
|
|
* @return array<string, mixed>
|
||
|
|
*/
|
||
|
|
public function clearOnSuccessFields(): array
|
||
|
|
{
|
||
|
|
return [
|
||
|
|
'sync_fail_count' => 0,
|
||
|
|
'sync_fail_pause_at' => null,
|
||
|
|
'sync_fail_retry_index' => 0,
|
||
|
|
'sync_auto_sync_stopped' => 0,
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Cron 自动重试是否已到点;未到点返回 skip 原因,到点返回 null(允许执行)
|
||
|
|
*
|
||
|
|
* @param Ticket|array<string, mixed> $ticket
|
||
|
|
*/
|
||
|
|
public function shouldAllowAutoRetry($ticket, ?int $now = null): ?string
|
||
|
|
{
|
||
|
|
if (!$this->isInRetryMode($ticket)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
if ($this->isPermanentlyStopped($ticket)) {
|
||
|
|
return '自动同步已终止(重试已用尽)';
|
||
|
|
}
|
||
|
|
|
||
|
|
$nextAt = $this->getNextRetryAt($ticket);
|
||
|
|
if ($nextAt === null) {
|
||
|
|
return '自动同步已终止(重试已用尽)';
|
||
|
|
}
|
||
|
|
|
||
|
|
$now = $now ?? time();
|
||
|
|
if ($now < $nextAt) {
|
||
|
|
$attempt = (int) ($this->readField($ticket, 'sync_fail_retry_index') ?? 0) + 1;
|
||
|
|
|
||
|
|
return sprintf(
|
||
|
|
'失败重试等待中,将于 %s 进行第 %d 次重试',
|
||
|
|
date('Y-m-d H:i:s', $nextAt),
|
||
|
|
$attempt
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 下一次计划重试的 Unix 时间戳;无待重试时返回 null
|
||
|
|
*
|
||
|
|
* @param Ticket|array<string, mixed> $ticket
|
||
|
|
*/
|
||
|
|
public function getNextRetryAt($ticket): ?int
|
||
|
|
{
|
||
|
|
if (!$this->isInRetryMode($ticket) || $this->isPermanentlyStopped($ticket)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$schedule = SplitSyncConfigService::getFailRetryMinutes();
|
||
|
|
if ($schedule === []) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$index = (int) ($this->readField($ticket, 'sync_fail_retry_index') ?? 0);
|
||
|
|
if ($index >= count($schedule)) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
$pauseAt = (int) ($this->readField($ticket, 'sync_fail_pause_at') ?? 0);
|
||
|
|
if ($pauseAt <= 0) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $pauseAt + $schedule[$index] * 60;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @param Ticket|array<string, mixed> $ticket
|
||
|
|
*/
|
||
|
|
private function readField($ticket, string $field)
|
||
|
|
{
|
||
|
|
if ($ticket instanceof Ticket) {
|
||
|
|
return $ticket->getData($field) ?? ($ticket[$field] ?? null);
|
||
|
|
}
|
||
|
|
|
||
|
|
return $ticket[$field] ?? null;
|
||
|
|
}
|
||
|
|
}
|