连续同步失败后触发重试时间点
This commit is contained in:
@@ -24,6 +24,12 @@ class SplitSyncConfigService
|
||||
return $value !== '' ? rtrim($value, '/') : self::DEFAULT_NODE_HOST;
|
||||
}
|
||||
|
||||
/** 失败重试时间点最多配置条数 */
|
||||
private const FAIL_RETRY_SCHEDULE_MAX = 10;
|
||||
|
||||
/** 单条重试时间点上限(分钟) */
|
||||
private const FAIL_RETRY_MINUTE_MAX = 1440;
|
||||
|
||||
/**
|
||||
* 连续同步失败多少次后自动暂停工单(0 表示不因失败暂停)
|
||||
*/
|
||||
@@ -36,6 +42,50 @@ class SplitSyncConfigService
|
||||
return max(0, (int) $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 达失败暂停阈值后的重试时间点(分钟,相对锚点的绝对偏移,升序去重)
|
||||
*
|
||||
* @return list<int>
|
||||
*/
|
||||
public static function getFailRetryMinutes(): array
|
||||
{
|
||||
$raw = trim(self::getConfigValue('split_sync_fail_retry_minutes'));
|
||||
if ($raw === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$parts = preg_split('/[,,\s]+/', $raw) ?: [];
|
||||
$minutes = [];
|
||||
foreach ($parts as $part) {
|
||||
$part = trim((string) $part);
|
||||
if ($part === '' || !ctype_digit($part)) {
|
||||
continue;
|
||||
}
|
||||
$minute = (int) $part;
|
||||
if ($minute < 1 || $minute > self::FAIL_RETRY_MINUTE_MAX) {
|
||||
continue;
|
||||
}
|
||||
$minutes[$minute] = $minute;
|
||||
}
|
||||
|
||||
if ($minutes === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$list = array_values($minutes);
|
||||
sort($list, SORT_NUMERIC);
|
||||
|
||||
return array_slice($list, 0, self::FAIL_RETRY_SCHEDULE_MAX);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否配置了失败分阶段重试时间点
|
||||
*/
|
||||
public static function hasFailRetrySchedule(): bool
|
||||
{
|
||||
return self::getFailRetryMinutes() !== [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 指定工单类型的自动同步周期(分钟),0 表示不自动同步
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
@@ -23,12 +23,15 @@ class SplitTicketSyncService
|
||||
|
||||
private SplitTicketSyncDispatchService $dispatchService;
|
||||
|
||||
private SplitTicketSyncFailRetryService $failRetryService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->numberSync = new SplitTicketNumberSyncService();
|
||||
$this->ruleService = new SplitTicketRuleService();
|
||||
$this->lockService = new SplitTicketSyncLockService();
|
||||
$this->dispatchService = new SplitTicketSyncDispatchService();
|
||||
$this->failRetryService = new SplitTicketSyncFailRetryService();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -400,7 +403,13 @@ class SplitTicketSyncService
|
||||
->where('manual_manage', 0)
|
||||
->whereIn('ticket_type', $types);
|
||||
if ($failThreshold > 0) {
|
||||
$query->where('sync_fail_count', '<', $failThreshold);
|
||||
$query->where(function ($query) use ($failThreshold): void {
|
||||
$query->where('sync_fail_count', '<', $failThreshold)
|
||||
->whereOr(function ($query): void {
|
||||
$query->where('sync_fail_pause_at', '>', 0)
|
||||
->where('sync_auto_sync_stopped', 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
$query->order('sync_time', 'asc')->order('id', 'asc');
|
||||
if ($limit > 0) {
|
||||
@@ -613,6 +622,27 @@ class SplitTicketSyncService
|
||||
if ((string) $ticket['status'] === 'hidden') {
|
||||
return '工单已关闭';
|
||||
}
|
||||
|
||||
if ($this->failRetryService->isPermanentlyStopped($ticket)) {
|
||||
return '自动同步已终止(重试已用尽)';
|
||||
}
|
||||
|
||||
if ($this->failRetryService->isInRetryMode($ticket)) {
|
||||
$retrySkip = $this->failRetryService->shouldAllowAutoRetry($ticket);
|
||||
if ($retrySkip !== null) {
|
||||
return $retrySkip;
|
||||
}
|
||||
if (!SplitScrmSpiderFactory::isSupported((string) $ticket['ticket_type'])) {
|
||||
return '工单类型尚未实现';
|
||||
}
|
||||
$interval = SplitSyncConfigService::getIntervalMinutes((string) $ticket['ticket_type']);
|
||||
if ($interval <= 0) {
|
||||
return '该类型未配置自动同步周期';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$failThreshold = SplitSyncConfigService::getFailPauseThreshold();
|
||||
if ($failThreshold > 0 && (int) ($ticket['sync_fail_count'] ?? 0) >= $failThreshold) {
|
||||
return sprintf('连续同步失败超过%d次已暂停', $failThreshold);
|
||||
@@ -661,6 +691,7 @@ class SplitTicketSyncService
|
||||
if ($success) {
|
||||
$data['sync_success_time'] = $now;
|
||||
$data['sync_success_mode'] = in_array($syncMode, ['auto', 'manual'], true) ? $syncMode : 'manual';
|
||||
$data = array_merge($data, $this->failRetryService->clearOnSuccessFields());
|
||||
}
|
||||
if (!$ticket->allowField(array_keys($data))->save($data)) {
|
||||
throw new Exception('工单同步结果保存失败');
|
||||
@@ -679,6 +710,21 @@ class SplitTicketSyncService
|
||||
'sync_message' => $message,
|
||||
'sync_fail_count' => $failCount,
|
||||
];
|
||||
|
||||
if ($this->failRetryService->isInRetryMode($ticket)) {
|
||||
$update = array_merge($update, $this->failRetryService->onRetryFailure($ticket));
|
||||
$ticket->save($update);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($failThreshold > 0 && $failCount >= $failThreshold && SplitSyncConfigService::hasFailRetrySchedule()) {
|
||||
$update = array_merge($update, $this->failRetryService->enterRetryIfNeeded($ticket, $failCount, $failThreshold));
|
||||
$ticket->save($update);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($neverSyncedSuccessfully || ($failThreshold > 0 && $failCount >= $failThreshold)) {
|
||||
$update['status'] = 'hidden';
|
||||
}
|
||||
@@ -787,7 +833,7 @@ class SplitTicketSyncService
|
||||
|
||||
$openList = Ticket::where('status', 'normal')
|
||||
->whereIn('ticket_type', $supportedTypes)
|
||||
->field('id,ticket_type,ticket_url,sync_fail_count,sync_time,sync_status')
|
||||
->field('id,ticket_type,ticket_url,sync_fail_count,sync_time,sync_status,sync_fail_pause_at,sync_fail_retry_index,sync_auto_sync_stopped,status,manual_manage')
|
||||
->select();
|
||||
|
||||
$result = [];
|
||||
@@ -838,7 +884,16 @@ class SplitTicketSyncService
|
||||
$ticketType = (string) ($ticket['ticket_type'] ?? '');
|
||||
$failCount = (int) ($ticket['sync_fail_count'] ?? 0);
|
||||
|
||||
if ($failThreshold > 0 && $failCount >= $failThreshold) {
|
||||
if ($this->failRetryService->isPermanentlyStopped($ticket)) {
|
||||
return '自动同步已终止(重试已用尽)';
|
||||
}
|
||||
if ($this->failRetryService->isInRetryMode($ticket)) {
|
||||
$retrySkip = $this->failRetryService->shouldAllowAutoRetry($ticket);
|
||||
if ($retrySkip !== null) {
|
||||
return $retrySkip;
|
||||
}
|
||||
}
|
||||
if ($failThreshold > 0 && $failCount >= $failThreshold && !SplitSyncConfigService::hasFailRetrySchedule()) {
|
||||
return sprintf('连续同步失败超过%d次已暂停自动同步', $failThreshold);
|
||||
}
|
||||
if (!in_array($ticketType, $autoSyncTypes, true)) {
|
||||
|
||||
Reference in New Issue
Block a user