fix(supplier): T6 online-mode 3 review-Important — tier-1-only docblock, partial-set re-attempt, per-platform DTO update

This commit is contained in:
Дмитрий
2026-05-20 13:29:08 +03:00
parent e994feb81b
commit 711c1927a4
2 changed files with 154 additions and 31 deletions
@@ -33,7 +33,8 @@ use Illuminate\Support\Facades\Mail;
use Throwable;
/**
* Daily 20:30 МСК cron-job: синхронизирует supplier_projects с поставщиком crm.bp-gr.ru.
* Daily 18:00 МСК cron-job: синхронизирует supplier_projects с поставщиком crm.bp-gr.ru
* (расписание перенесено 20:30 18:00, см. routes/console.php).
*
* Алгоритм (Plan 3 Task 5 per-subject grouping):
* 1. Загрузить активные Лидерра-projects (is_active=true, archived_at IS NULL).
@@ -269,24 +270,76 @@ class SyncSupplierProjectsJob implements ShouldQueue
$existingSps->push($sp);
}
} else {
// Update path: updateProject each external_id (R6: one shared limit)
$dto = new SupplierProjectDto(
platform: $existingSps->first()->platform,
signalType: $signalType,
uniqueKey: $identifier,
limit: $order,
workdays: $workdays,
regions: $regions,
regionsReverse: false,
status: 'active',
tag: $tag,
);
// Fix #3 (review-followup): partial-set recovery — если предыдущий run создал
// не все platforms (e.g. B1+B2 OK, B3 escalated), re-attempt missing via multi-flag
// save с platforms=$missingPlatforms. Throws пропагируют в outer handle() catch
// (SupplierAuth/Transient/Client) — full failover-counter semantics сохраняется.
$existingPlatforms = $existingSps->pluck('platform')->all();
$missingPlatforms = array_values(array_diff($platforms, $existingPlatforms));
if ($missingPlatforms !== []) {
$missingDto = new SupplierProjectDto(
platform: $missingPlatforms[0],
signalType: $signalType,
uniqueKey: $identifier,
limit: $order,
workdays: $workdays,
regions: $regions,
regionsReverse: false,
status: 'active',
tag: $tag,
platforms: $missingPlatforms,
);
$missingIdMap = $this->client->saveProjectMultiFlag($missingDto);
foreach ($missingPlatforms as $platform) {
$externalId = $missingIdMap[$platform] ?? null;
if ($externalId === null) {
continue;
}
$sp = SupplierProject::on(self::DB_CONNECTION)->forceCreate([
'platform' => $platform,
'signal_type' => $signalType,
'unique_key' => $identifier,
'subject_code' => $subjectCode,
'supplier_external_id' => (string) $externalId,
'current_limit' => $order,
'current_workdays' => $workdays,
'current_regions' => $regions,
'sync_status' => 'ok',
'last_synced_at' => now(),
]);
SupplierSyncLog::on(self::DB_CONNECTION)->create([
'supplier_project_id' => $sp->id,
'action' => 'create',
'http_status' => 200,
'created_at' => now(),
]);
$existingSps->push($sp);
}
}
// Fix #2 (review-followup): per-platform DTO в update-loop, чтобы portal получал
// правильные srcrt/srcbl/srcmt для конкретной редактируемой строки (не first()
// из mixed-platform existing set). R6 one shared limit/regions сохраняется.
foreach ($existingSps as $sp) {
if ($sp->supplier_external_id === null) {
continue;
}
$this->channel->updateProject((int) $sp->supplier_external_id, $dto);
$perPlatformDto = new SupplierProjectDto(
platform: $sp->platform,
signalType: $signalType,
uniqueKey: $identifier,
limit: $order,
workdays: $workdays,
regions: $regions,
regionsReverse: false,
status: 'active',
tag: $tag,
platforms: [$sp->platform],
);
$this->channel->updateProject((int) $sp->supplier_external_id, $perPlatformDto);
$sp->forceFill([
'current_limit' => $order,
'current_workdays' => $workdays,
+87 -17
View File
@@ -34,10 +34,17 @@ use Illuminate\Support\Facades\Log;
* batch «каркас»: создаёт supplier_projects с limit=0, без регионов
* (старый путь); ночной SyncSupplierProjectsJob дольёт полные параметры.
*
* Канал миграции SupplierProjectChannel (резолвится в FailoverProjectChannel:
* ярус 1 AJAX ярус 2 browser-form ярус 3 manual queue). При эскалации на
* ярус 3 / переносе по окну портала platform пропускается (FK остаётся NULL,
* ночной SyncSupplierProjectsJob подберёт после ручного вмешательства).
* Канал миграции:
* batch mode SupplierProjectChannel (FailoverProjectChannel: ярус 1 AJAX
* ярус 2 browser-form ярус 3 manual queue) для createProject.
* online mode multi-flag save идёт напрямую через SupplierPortalClient
* (tier-1 AJAX only multi-flag нет в tier-2 form по архитектуре
* портала). При любом transient/auth fail log warning + skip
* subject; Laravel retry (tries=3 backoff [15s,60s,300s]) ночной
* SyncSupplierProjectsJob подберёт с полным failover каналом.
* updateProject в online остаётся через $channel (полная схема failover).
* При эскалации на ярус 3 / переносе по окну портала platform/subject пропускается
* (FK/pivot остаётся пустым; ночной SyncSupplierProjectsJob восстанавливает).
*
* Retry: 3 попытки с backoff [15s, 60s, 300s].
*
@@ -132,6 +139,13 @@ class SyncSupplierProjectJob implements ShouldQueue
} catch (WindowDeferredException) {
Log::info("SyncSupplierProjectJob: project {$project->id} subject={$subject} deferred by portal window");
continue;
} catch (\Throwable $e) {
// Online multi-flag save bypasses FailoverProjectChannel (tier-1 only by design,
// см. class docblock). При transient/auth/client/network fail — log+skip; следующий
// tries-retry (15s, 60s, 300s) или ночной SyncSupplierProjectsJob подберёт.
Log::warning("SyncSupplierProjectJob: online multi-flag save failed for project {$project->id} subject={$subject} (".get_class($e).'): '.$e->getMessage());
continue;
}
@@ -157,24 +171,80 @@ class SyncSupplierProjectJob implements ShouldQueue
$existingSps->push($sp);
}
} else {
// Update path: updateProject each (R6: one shared limit)
$dto = new SupplierProjectDto(
platform: $existingSps->first()->platform,
signalType: (string) $project->signal_type,
uniqueKey: $identifier,
limit: (int) $project->daily_limit_target,
workdays: [1, 2, 3, 4, 5, 6, 7],
regions: $regions,
regionsReverse: false,
status: 'active',
tag: $tag,
);
// Fix #3 (review-followup): partial-set recovery — если предыдущий run создал
// не все platforms (e.g. B1+B2 OK, B3 escalated), re-attempt missing via
// multi-flag save с platforms=$missingPlatforms (srcrt/srcbl/srcmt только missing).
$existingPlatforms = $existingSps->pluck('platform')->all();
$missingPlatforms = array_values(array_diff($platforms, $existingPlatforms));
if ($missingPlatforms !== []) {
$missingDto = new SupplierProjectDto(
platform: $missingPlatforms[0],
signalType: (string) $project->signal_type,
uniqueKey: $identifier,
limit: (int) $project->daily_limit_target,
workdays: [1, 2, 3, 4, 5, 6, 7],
regions: $regions,
regionsReverse: false,
status: 'active',
tag: $tag,
platforms: $missingPlatforms,
);
try {
$missingIdMap = $client->saveProjectMultiFlag($missingDto);
} catch (TierEscalatedException $e) {
Log::info("SyncSupplierProjectJob: project {$project->id} subject={$subject} missing-platform re-attempt escalated #{$e->queueRowId}");
$missingIdMap = [];
} catch (WindowDeferredException) {
Log::info("SyncSupplierProjectJob: project {$project->id} subject={$subject} missing-platform deferred by portal window");
$missingIdMap = [];
} catch (\Throwable $e) {
Log::warning("SyncSupplierProjectJob: missing-platform multi-flag failed for project {$project->id} subject={$subject}: ".$e->getMessage());
$missingIdMap = [];
}
foreach ($missingPlatforms as $platform) {
$externalId = $missingIdMap[$platform] ?? null;
if ($externalId === null) {
continue;
}
$sp = SupplierProject::create([
'platform' => $platform,
'signal_type' => (string) $project->signal_type,
'unique_key' => $identifier,
'subject_code' => $subject,
'supplier_external_id' => (string) $externalId,
'current_limit' => (int) $project->daily_limit_target,
'current_workdays' => [1, 2, 3, 4, 5, 6, 7],
'current_regions' => $regions,
'sync_status' => 'ok',
'last_synced_at' => now(),
]);
$existingSps->push($sp);
}
}
// Fix #2 (review-followup): per-platform DTO в update-loop, чтобы portal
// получал корректные srcrt/srcbl/srcmt флаги для конкретной редактируемой строки
// (не первой из mixed-platform existing set). R6 one shared limit/regions сохраняется.
foreach ($existingSps as $sp) {
if ($sp->supplier_external_id === null) {
continue;
}
$channel->updateProject((int) $sp->supplier_external_id, $dto);
$perPlatformDto = new SupplierProjectDto(
platform: $sp->platform,
signalType: (string) $project->signal_type,
uniqueKey: $identifier,
limit: (int) $project->daily_limit_target,
workdays: [1, 2, 3, 4, 5, 6, 7],
regions: $regions,
regionsReverse: false,
status: 'active',
tag: $tag,
platforms: [$sp->platform],
);
$channel->updateProject((int) $sp->supplier_external_id, $perPlatformDto);
$sp->forceFill([
'current_limit' => (int) $project->daily_limit_target,
'current_regions' => $regions,