2026-09-02 15:55:03 +03:30
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Log;
|
|
|
|
|
use App\Models\PargarSyncLog;
|
|
|
|
|
use App\Models\PriceSyncLog;
|
|
|
|
|
use App\Models\PriceSyncLogItem;
|
|
|
|
|
use App\Models\Tahesab;
|
|
|
|
|
use App\Models\Zarin;
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
use Illuminate\Database\Eloquent\Builder;
|
2026-09-03 10:19:56 +03:30
|
|
|
use Illuminate\Support\Facades\DB;
|
2026-09-02 15:55:03 +03:30
|
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* پاکسازی شبانهٔ لاگهای حجیم — همان سقف نگهداری جدول prices (۳۱ روز).
|
|
|
|
|
*/
|
|
|
|
|
class DatabaseLogPruneService
|
|
|
|
|
{
|
|
|
|
|
/** همراستا با حذف قطعی در PriceSnapshotService::pruneHistory */
|
|
|
|
|
public const RETENTION_DAYS = 31;
|
|
|
|
|
|
2026-09-03 10:19:56 +03:30
|
|
|
/** سقف ردیف موفق مانده/سند ورودی پرگار — بقیه (سفارش/کیف) را نمیبُرد */
|
|
|
|
|
public const PARAGAR_INBOUND_KEEP = 2000;
|
|
|
|
|
|
2026-09-02 15:55:03 +03:30
|
|
|
private const CHUNK = 500;
|
|
|
|
|
|
|
|
|
|
public function pruneHistory(?Carbon $now = null): array
|
|
|
|
|
{
|
|
|
|
|
$cutoff = ($now ?? now())->copy()->subDays(self::RETENTION_DAYS);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'retention_days' => self::RETENTION_DAYS,
|
|
|
|
|
'cutoff' => $cutoff->toDateTimeString(),
|
2026-09-03 10:19:56 +03:30
|
|
|
'pargar_inbound' => $this->prunePargarInboundOverflow(),
|
2026-09-02 15:55:03 +03:30
|
|
|
'price_sync_logs' => $this->prunePriceSyncLogs($cutoff),
|
|
|
|
|
'logs' => $this->pruneGenericLogs($cutoff),
|
|
|
|
|
'pargar_sync_logs' => $this->pruneSuccessfulRows(
|
|
|
|
|
PargarSyncLog::query(),
|
|
|
|
|
'pargar_sync_logs',
|
|
|
|
|
PargarSyncService::STATUS_SUCCESS,
|
|
|
|
|
$cutoff
|
|
|
|
|
),
|
|
|
|
|
'zarin' => $this->pruneSuccessfulRows(
|
|
|
|
|
Zarin::query(),
|
|
|
|
|
'zarin',
|
|
|
|
|
ZarinSyncService::STATUS_SUCCESS,
|
|
|
|
|
$cutoff
|
|
|
|
|
),
|
|
|
|
|
'tahesab' => $this->pruneSuccessfulRows(
|
|
|
|
|
Tahesab::query(),
|
|
|
|
|
'tahesab',
|
|
|
|
|
TahesabSyncService::STATUS_SUCCESS,
|
|
|
|
|
$cutoff
|
|
|
|
|
),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return array{deleted: int, items_deleted: int} */
|
|
|
|
|
private function prunePriceSyncLogs(Carbon $cutoff): array
|
|
|
|
|
{
|
|
|
|
|
if (! Schema::hasTable('price_sync_logs')) {
|
|
|
|
|
return ['deleted' => 0, 'items_deleted' => 0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$deleted = 0;
|
|
|
|
|
$itemsDeleted = 0;
|
|
|
|
|
$hasItems = Schema::hasTable('price_sync_log_items');
|
|
|
|
|
|
|
|
|
|
PriceSyncLog::query()
|
|
|
|
|
->where('created_at', '<', $cutoff)
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->select(['id'])
|
|
|
|
|
->chunkById(self::CHUNK, function ($rows) use (&$deleted, &$itemsDeleted, $hasItems) {
|
|
|
|
|
$ids = $rows->pluck('id')->all();
|
|
|
|
|
if ($ids === []) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if ($hasItems) {
|
|
|
|
|
$itemsDeleted += (int) PriceSyncLogItem::query()->whereIn('log_id', $ids)->delete();
|
|
|
|
|
}
|
|
|
|
|
$deleted += (int) PriceSyncLog::query()->whereIn('id', $ids)->delete();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return ['deleted' => $deleted, 'items_deleted' => $itemsDeleted];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return array{deleted: int} */
|
|
|
|
|
private function pruneGenericLogs(Carbon $cutoff): array
|
|
|
|
|
{
|
|
|
|
|
if (! Schema::hasTable('logs')) {
|
|
|
|
|
return ['deleted' => 0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$deleted = 0;
|
|
|
|
|
|
|
|
|
|
Log::query()
|
|
|
|
|
->where('created_at', '<', $cutoff)
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->select(['id'])
|
|
|
|
|
->chunkById(self::CHUNK, function ($rows) use (&$deleted) {
|
|
|
|
|
$ids = $rows->pluck('id')->all();
|
|
|
|
|
if ($ids === []) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$deleted += (int) Log::query()->whereIn('id', $ids)->delete();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return ['deleted' => $deleted];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* فقط ردیفهای موفق حسابداری — pending/failed برای retry میمانند.
|
|
|
|
|
*
|
|
|
|
|
* @return array{deleted: int, skipped: bool}
|
|
|
|
|
*/
|
|
|
|
|
private function pruneSuccessfulRows(Builder $query, string $table, string $successStatus, Carbon $cutoff): array
|
|
|
|
|
{
|
|
|
|
|
if (! Schema::hasTable($table) || ! Schema::hasColumn($table, 'sync_status')) {
|
|
|
|
|
return ['deleted' => 0, 'skipped' => true];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (! Schema::hasColumn($table, 'created_at')) {
|
|
|
|
|
return ['deleted' => 0, 'skipped' => true];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$deleted = 0;
|
|
|
|
|
|
|
|
|
|
(clone $query)
|
|
|
|
|
->where('sync_status', $successStatus)
|
|
|
|
|
->where('created_at', '<', $cutoff)
|
|
|
|
|
->orderBy('id')
|
|
|
|
|
->select(['id'])
|
|
|
|
|
->chunkById(self::CHUNK, function ($rows) use (&$deleted, $query) {
|
|
|
|
|
$ids = $rows->pluck('id')->all();
|
|
|
|
|
if ($ids === []) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
$deleted += (int) (clone $query)->whereIn('id', $ids)->delete();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return ['deleted' => $deleted, 'skipped' => false];
|
|
|
|
|
}
|
2026-09-03 10:19:56 +03:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* فقط لاگ ورودی پرگار (mande/document). سفارش و سند کیف دست نخورده میماند.
|
|
|
|
|
*
|
|
|
|
|
* @return array{mande: int, document: int}
|
|
|
|
|
*/
|
2026-09-03 20:24:44 +03:30
|
|
|
/** هر بار حداکثر — برای afterResponse؛ تریگر MySQL هم LIMIT 4000 دارد */
|
|
|
|
|
private const PARAGAR_PRUNE_BATCH = 4000;
|
|
|
|
|
|
|
|
|
|
public function prunePargarInboundOverflow(?int $maxDeletePerType = null): array
|
2026-09-03 10:19:56 +03:30
|
|
|
{
|
|
|
|
|
if (! Schema::hasTable('pargar_sync_logs')) {
|
|
|
|
|
return ['mande' => 0, 'document' => 0];
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-03 20:24:44 +03:30
|
|
|
$batch = $maxDeletePerType ?? self::PARAGAR_PRUNE_BATCH;
|
|
|
|
|
|
2026-09-03 10:19:56 +03:30
|
|
|
ignore_user_abort(true);
|
|
|
|
|
if (function_exists('set_time_limit')) {
|
|
|
|
|
@set_time_limit(180);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->ensureTypeStatusIndex();
|
|
|
|
|
|
|
|
|
|
$locked = $this->acquirePruneLock();
|
|
|
|
|
if ($locked === false) {
|
|
|
|
|
return ['mande' => 0, 'document' => 0];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
return [
|
2026-09-03 20:24:44 +03:30
|
|
|
'mande' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_MANDE, $batch),
|
|
|
|
|
'document' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_DOCUMENT, $batch),
|
2026-09-03 10:19:56 +03:30
|
|
|
];
|
|
|
|
|
} finally {
|
|
|
|
|
if ($locked === true) {
|
|
|
|
|
$this->releasePruneLock();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function pruneOldestSuccessBeyond(string $type, int $maxDelete): int
|
|
|
|
|
{
|
|
|
|
|
$cutoffId = PargarSyncLog::query()
|
|
|
|
|
->where('type', $type)
|
|
|
|
|
->where('sync_status', PargarSyncService::STATUS_SUCCESS)
|
|
|
|
|
->orderByDesc('id')
|
|
|
|
|
->skip(self::PARAGAR_INBOUND_KEEP)
|
|
|
|
|
->value('id');
|
|
|
|
|
|
|
|
|
|
if (! $cutoffId) {
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$query = PargarSyncLog::query()
|
|
|
|
|
->where('type', $type)
|
|
|
|
|
->where('sync_status', PargarSyncService::STATUS_SUCCESS)
|
|
|
|
|
->where('id', '<=', $cutoffId)
|
|
|
|
|
->orderBy('id');
|
|
|
|
|
|
2026-09-03 20:24:44 +03:30
|
|
|
if ($maxDelete <= 0) {
|
|
|
|
|
$maxDelete = self::PARAGAR_PRUNE_BATCH;
|
2026-09-03 10:19:56 +03:30
|
|
|
}
|
|
|
|
|
|
2026-09-03 20:24:44 +03:30
|
|
|
return (int) $query->limit($maxDelete)->delete();
|
2026-09-03 10:19:56 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function ensureTypeStatusIndex(): void
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
$names = collect(DB::select('SHOW INDEX FROM `pargar_sync_logs`'))
|
|
|
|
|
->pluck('Key_name')
|
|
|
|
|
->all();
|
|
|
|
|
if (in_array('pargar_sync_logs_type_status_index', $names, true)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
DB::statement(
|
|
|
|
|
'ALTER TABLE `pargar_sync_logs` ADD INDEX `pargar_sync_logs_type_status_index` (`type`, `sync_status`)'
|
|
|
|
|
);
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** @return bool|null true=locked, false=busy, null=no lock support */
|
|
|
|
|
private function acquirePruneLock(): ?bool
|
|
|
|
|
{
|
|
|
|
|
try {
|
2026-09-03 20:24:44 +03:30
|
|
|
$row = DB::selectOne('SELECT GET_LOCK(?, 3) AS l', ['khanegi_pargar_inbound_prune']);
|
2026-09-03 10:19:56 +03:30
|
|
|
|
|
|
|
|
return (int) ($row->l ?? 0) === 1;
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private function releasePruneLock(): void
|
|
|
|
|
{
|
|
|
|
|
try {
|
|
|
|
|
DB::selectOne('SELECT RELEASE_LOCK(?) AS l', ['khanegi_pargar_inbound_prune']);
|
|
|
|
|
} catch (\Throwable) {
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-02 15:55:03 +03:30
|
|
|
}
|