feat(pargar): cap inbound mande/document logs after insert
Keep the newest 2000 success rows per inbound type so dumps stop growing from Pargar webhooks, and store compact document payloads instead of full JSON. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -279,11 +279,7 @@ class PargarInboundController extends Controller
|
|||||||
'type' => PargarSyncService::TYPE_DOCUMENT,
|
'type' => PargarSyncService::TYPE_DOCUMENT,
|
||||||
'sync_status' => PargarSyncService::STATUS_SUCCESS,
|
'sync_status' => PargarSyncService::STATUS_SUCCESS,
|
||||||
'pargar_ref' => $accountId !== '' ? $accountId : null,
|
'pargar_ref' => $accountId !== '' ? $accountId : null,
|
||||||
'data' => json_encode(array_merge($data, [
|
'data' => $this->sync()->compactInboundDocumentData($data, $summary),
|
||||||
'_summary' => $summary,
|
|
||||||
'wallet_applied' => false,
|
|
||||||
'code_build' => 'docs-then-mande-auto',
|
|
||||||
]), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
\Illuminate\Support\Facades\Log::info('Pargar documents stored; queue auto mande sync', [
|
\Illuminate\Support\Facades\Log::info('Pargar documents stored; queue auto mande sync', [
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Services\DatabaseLogPruneService;
|
||||||
|
use App\Services\PargarSyncService;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
@@ -46,6 +48,26 @@ class PargarSyncLog extends Model
|
|||||||
return self::$hasConfirmedAtColumn;
|
return self::$hasConfirmedAtColumn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::created(function (self $log) {
|
||||||
|
if (! in_array($log->type, [PargarSyncService::TYPE_MANDE, PargarSyncService::TYPE_DOCUMENT], true)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(function () {
|
||||||
|
if (function_exists('fastcgi_finish_request')) {
|
||||||
|
@fastcgi_finish_request();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
app(DatabaseLogPruneService::class)->prunePargarInboundOverflow();
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// وبهوک پرگار نباید بهخاطر پاکسازی قطع شود
|
||||||
|
}
|
||||||
|
})->afterResponse();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function customer(): BelongsTo
|
public function customer(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(Customer::class);
|
return $this->belongsTo(Customer::class);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use App\Models\Tahesab;
|
|||||||
use App\Models\Zarin;
|
use App\Models\Zarin;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,6 +21,9 @@ class DatabaseLogPruneService
|
|||||||
/** همراستا با حذف قطعی در PriceSnapshotService::pruneHistory */
|
/** همراستا با حذف قطعی در PriceSnapshotService::pruneHistory */
|
||||||
public const RETENTION_DAYS = 31;
|
public const RETENTION_DAYS = 31;
|
||||||
|
|
||||||
|
/** سقف ردیف موفق مانده/سند ورودی پرگار — بقیه (سفارش/کیف) را نمیبُرد */
|
||||||
|
public const PARAGAR_INBOUND_KEEP = 2000;
|
||||||
|
|
||||||
private const CHUNK = 500;
|
private const CHUNK = 500;
|
||||||
|
|
||||||
public function pruneHistory(?Carbon $now = null): array
|
public function pruneHistory(?Carbon $now = null): array
|
||||||
@@ -29,6 +33,7 @@ class DatabaseLogPruneService
|
|||||||
return [
|
return [
|
||||||
'retention_days' => self::RETENTION_DAYS,
|
'retention_days' => self::RETENTION_DAYS,
|
||||||
'cutoff' => $cutoff->toDateTimeString(),
|
'cutoff' => $cutoff->toDateTimeString(),
|
||||||
|
'pargar_inbound' => $this->prunePargarInboundOverflow(),
|
||||||
'price_sync_logs' => $this->prunePriceSyncLogs($cutoff),
|
'price_sync_logs' => $this->prunePriceSyncLogs($cutoff),
|
||||||
'logs' => $this->pruneGenericLogs($cutoff),
|
'logs' => $this->pruneGenericLogs($cutoff),
|
||||||
'pargar_sync_logs' => $this->pruneSuccessfulRows(
|
'pargar_sync_logs' => $this->pruneSuccessfulRows(
|
||||||
@@ -137,4 +142,101 @@ class DatabaseLogPruneService
|
|||||||
|
|
||||||
return ['deleted' => $deleted, 'skipped' => false];
|
return ['deleted' => $deleted, 'skipped' => false];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* فقط لاگ ورودی پرگار (mande/document). سفارش و سند کیف دست نخورده میماند.
|
||||||
|
*
|
||||||
|
* @return array{mande: int, document: int}
|
||||||
|
*/
|
||||||
|
public function prunePargarInboundOverflow(int $maxDeletePerType = 0): array
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('pargar_sync_logs')) {
|
||||||
|
return ['mande' => 0, 'document' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
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 [
|
||||||
|
'mande' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_MANDE, $maxDeletePerType),
|
||||||
|
'document' => $this->pruneOldestSuccessBeyond(PargarSyncService::TYPE_DOCUMENT, $maxDeletePerType),
|
||||||
|
];
|
||||||
|
} 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');
|
||||||
|
|
||||||
|
if ($maxDelete > 0) {
|
||||||
|
return (int) $query->limit($maxDelete)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $query->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
$row = DB::selectOne('SELECT GET_LOCK(?, 0) AS l', ['khanegi_pargar_inbound_prune']);
|
||||||
|
|
||||||
|
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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -906,6 +906,28 @@ class PargarSyncService
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* وبهوک سند پرگار را کامل در LONGTEXT نگذار — همان جدول دامپ را باد میکند.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $data
|
||||||
|
* @param array<string, mixed> $summary
|
||||||
|
*/
|
||||||
|
public function compactInboundDocumentData(array $data, array $summary): string
|
||||||
|
{
|
||||||
|
$transactions = is_array($data['transactions'] ?? null) ? $data['transactions'] : [];
|
||||||
|
|
||||||
|
return (string) json_encode([
|
||||||
|
'accountName' => $data['accountName'] ?? null,
|
||||||
|
'accountId' => $data['accountId'] ?? null,
|
||||||
|
'transactions_count' => count($transactions),
|
||||||
|
'transactions_fp' => hash('sha256', (string) json_encode($transactions, JSON_UNESCAPED_UNICODE)),
|
||||||
|
'_summary' => $summary,
|
||||||
|
'wallet_applied' => false,
|
||||||
|
'code_build' => 'docs-then-mande-auto',
|
||||||
|
'payload_compacted' => true,
|
||||||
|
], JSON_UNESCAPED_UNICODE);
|
||||||
|
}
|
||||||
|
|
||||||
private function parsePargarNumber(mixed $value): float
|
private function parsePargarNumber(mixed $value): float
|
||||||
{
|
{
|
||||||
if ($value === null || $value === '') {
|
if ($value === null || $value === '') {
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! Schema::hasTable('pargar_sync_logs')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$indexNames = collect(DB::select('SHOW INDEX FROM `pargar_sync_logs`'))
|
||||||
|
->pluck('Key_name')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if (! in_array('pargar_sync_logs_type_status_index', $indexNames, true)) {
|
||||||
|
Schema::table('pargar_sync_logs', function ($table) {
|
||||||
|
$table->index(['type', 'sync_status'], 'pargar_sync_logs_type_status_index');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->dropEvents();
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::statement('SET GLOBAL event_scheduler = ON');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
DB::unprepared(<<<'SQL'
|
||||||
|
CREATE EVENT `khanegi_prune_pargar_inbound_docs`
|
||||||
|
ON SCHEDULE EVERY 1 HOUR
|
||||||
|
STARTS CURRENT_TIMESTAMP
|
||||||
|
ON COMPLETION PRESERVE
|
||||||
|
ENABLE
|
||||||
|
DO
|
||||||
|
DELETE FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'document' AND `sync_status` = 'success'
|
||||||
|
AND `id` <= (
|
||||||
|
SELECT `cutoff` FROM (
|
||||||
|
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'document' AND `sync_status` = 'success'
|
||||||
|
ORDER BY `id` DESC
|
||||||
|
LIMIT 1 OFFSET 2000
|
||||||
|
) AS `keep`
|
||||||
|
)
|
||||||
|
LIMIT 4000
|
||||||
|
SQL);
|
||||||
|
DB::unprepared(<<<'SQL'
|
||||||
|
CREATE EVENT `khanegi_prune_pargar_inbound_mande`
|
||||||
|
ON SCHEDULE EVERY 1 HOUR
|
||||||
|
STARTS CURRENT_TIMESTAMP
|
||||||
|
ON COMPLETION PRESERVE
|
||||||
|
ENABLE
|
||||||
|
DO
|
||||||
|
DELETE FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'mande' AND `sync_status` = 'success'
|
||||||
|
AND `id` <= (
|
||||||
|
SELECT `cutoff` FROM (
|
||||||
|
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'mande' AND `sync_status` = 'success'
|
||||||
|
ORDER BY `id` DESC
|
||||||
|
LIMIT 1 OFFSET 2000
|
||||||
|
) AS `keep`
|
||||||
|
)
|
||||||
|
LIMIT 4000
|
||||||
|
SQL);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// EVENT privilege / event_scheduler — پاکسازی روی INSERT لاگ ورودی میماند
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$this->dropEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dropEvents(): void
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
DB::unprepared('DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_docs`');
|
||||||
|
DB::unprepared('DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_mande`');
|
||||||
|
} catch (\Throwable) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
-- یکبار روی دیتابیس زنده (مثلاً kh_goldarvin) در Navicat اجرا شود.
|
||||||
|
-- سفارش / کیف / مشتری را پاک نمیکند؛ فقط لاگ ورودی مانده و سند.
|
||||||
|
-- بعد از اجرا دوباره دامپ بگیر؛ حجم باید از حدود ۲۳۰MB به حدود ۱۰–۲۰MB برسد.
|
||||||
|
|
||||||
|
-- ایندکس برای حذف سریع
|
||||||
|
SET @db = DATABASE();
|
||||||
|
SET @sql = IF(
|
||||||
|
(SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'pargar_sync_logs'
|
||||||
|
AND INDEX_NAME = 'pargar_sync_logs_type_status_index') = 0
|
||||||
|
AND (SELECT COUNT(*) FROM information_schema.TABLES
|
||||||
|
WHERE TABLE_SCHEMA = @db AND TABLE_NAME = 'pargar_sync_logs') > 0,
|
||||||
|
'ALTER TABLE `pargar_sync_logs` ADD INDEX `pargar_sync_logs_type_status_index` (`type`, `sync_status`)',
|
||||||
|
'SELECT ''skip pargar_sync_logs_type_status_index'' AS info'
|
||||||
|
);
|
||||||
|
PREPARE stmt FROM @sql;
|
||||||
|
EXECUTE stmt;
|
||||||
|
DEALLOCATE PREPARE stmt;
|
||||||
|
|
||||||
|
-- نگه داشتن ۲۰۰۰ سند ورودی تازه
|
||||||
|
DELETE FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'document' AND `sync_status` = 'success'
|
||||||
|
AND `id` <= (
|
||||||
|
SELECT `cutoff` FROM (
|
||||||
|
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'document' AND `sync_status` = 'success'
|
||||||
|
ORDER BY `id` DESC
|
||||||
|
LIMIT 1 OFFSET 2000
|
||||||
|
) AS `keep`
|
||||||
|
);
|
||||||
|
|
||||||
|
-- نگه داشتن ۲۰۰۰ مانده تازه
|
||||||
|
DELETE FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'mande' AND `sync_status` = 'success'
|
||||||
|
AND `id` <= (
|
||||||
|
SELECT `cutoff` FROM (
|
||||||
|
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'mande' AND `sync_status` = 'success'
|
||||||
|
ORDER BY `id` DESC
|
||||||
|
LIMIT 1 OFFSET 2000
|
||||||
|
) AS `keep`
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Event داخل خود MySQL — بدون کرون سیستم (Navicat → Events)
|
||||||
|
DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_docs`;
|
||||||
|
DROP EVENT IF EXISTS `khanegi_prune_pargar_inbound_mande`;
|
||||||
|
|
||||||
|
CREATE EVENT `khanegi_prune_pargar_inbound_docs`
|
||||||
|
ON SCHEDULE EVERY 1 HOUR
|
||||||
|
STARTS CURRENT_TIMESTAMP
|
||||||
|
ON COMPLETION PRESERVE
|
||||||
|
ENABLE
|
||||||
|
DO
|
||||||
|
DELETE FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'document' AND `sync_status` = 'success'
|
||||||
|
AND `id` <= (
|
||||||
|
SELECT `cutoff` FROM (
|
||||||
|
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'document' AND `sync_status` = 'success'
|
||||||
|
ORDER BY `id` DESC
|
||||||
|
LIMIT 1 OFFSET 2000
|
||||||
|
) AS `keep`
|
||||||
|
)
|
||||||
|
LIMIT 4000;
|
||||||
|
|
||||||
|
CREATE EVENT `khanegi_prune_pargar_inbound_mande`
|
||||||
|
ON SCHEDULE EVERY 1 HOUR
|
||||||
|
STARTS CURRENT_TIMESTAMP
|
||||||
|
ON COMPLETION PRESERVE
|
||||||
|
ENABLE
|
||||||
|
DO
|
||||||
|
DELETE FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'mande' AND `sync_status` = 'success'
|
||||||
|
AND `id` <= (
|
||||||
|
SELECT `cutoff` FROM (
|
||||||
|
SELECT `id` AS `cutoff` FROM `pargar_sync_logs`
|
||||||
|
WHERE `type` = 'mande' AND `sync_status` = 'success'
|
||||||
|
ORDER BY `id` DESC
|
||||||
|
LIMIT 1 OFFSET 2000
|
||||||
|
) AS `keep`
|
||||||
|
)
|
||||||
|
LIMIT 4000;
|
||||||
|
|
||||||
|
-- اگر دسترسی SUPER داری:
|
||||||
|
-- SET GLOBAL event_scheduler = ON;
|
||||||
|
-- SHOW VARIABLES LIKE 'event_scheduler';
|
||||||
|
-- SHOW EVENTS;
|
||||||
Reference in New Issue
Block a user