option('min')); $limit = max(1, (int) $this->option('limit')); $onlyCustomer = (int) $this->option('customer'); $weightProductIds = $this->weightBasedProductIds(); if ($weightProductIds === []) { $this->warn('هیچ محصول وزنی یافت نشد.'); return self::SUCCESS; } $walletQuery = Wallet::query() ->whereNull('fund_id') ->whereNotNull('product_id') ->whereIn('product_id', $weightProductIds) ->where('owner_type', 2); if ($onlyCustomer > 0) { $walletQuery->where('owner', $onlyCustomer); } $rows = []; $walletQuery->orderBy('id')->chunkById(200, function ($wallets) use (&$rows, $min, $limit) { foreach ($wallets as $wallet) { if (count($rows) >= $limit) { return false; } $report = $this->inspectWallet($wallet, $min); if ($report !== null) { $rows[] = $report; } } return count($rows) < $limit; }); if ($rows === []) { $this->info('موردی با اختلاف معنادار پیدا نشد.'); return self::SUCCESS; } $this->table( ['customer_id', 'name', 'wallet_id', 'product_id', 'tx_net', 'wallet_gold', 'missing', 'matches_intval_bug'], array_map(static fn (array $r) => [ $r['customer_id'], $r['name'], $r['wallet_id'], $r['product_id'], $r['tx_net'], $r['wallet_gold'], $r['missing'], $r['matches_intval_bug'] ? 'yes' : 'maybe', ], $rows) ); $totalMissing = round(array_sum(array_column($rows, 'missing')), 3); $this->newLine(); $this->warn('تعداد کیف مشکوک: '.count($rows)); $this->warn("جمع طلای کم‌شده (تقریبی): {$totalMissing} گرم"); $this->line('این فقط گزارش است؛ برای برگرداندن موجودی باید جداگانه اصلاح شود.'); return self::SUCCESS; } /** @return list */ private function weightBasedProductIds(): array { $ids = []; Product::query() ->select(['id', 'unit', 'direct_price', 'group_type', 'amount', 'trade_by_quantity']) ->orderBy('id') ->chunkById(200, function ($products) use (&$ids) { foreach ($products as $product) { if (! Controller::isCountBasedProduct(Controller::productUnitPayload($product))) { $ids[] = (int) $product->id; } } }); return $ids; } private function inspectWallet(Wallet $wallet, float $min): ?array { $txs = Transaction::query() ->where('wallet', $wallet->id) ->whereNotNull('gold') ->where('gold', '!=', 0) ->orderBy('id') ->get(['id', 'gold', 'type', 'created_at']); if ($txs->isEmpty()) { return null; } $txNet = 0.0; $clean = 0.0; $buggy = 0.0; foreach ($txs as $tx) { $delta = (float) $tx->gold; $txNet += $delta; // الگوی سفارش: قبل از واریز طلای مثبت روی کیف محصول، intval می‌زد if ($delta > 0) { $buggy = (float) intval($buggy); } $buggy += $delta; $clean += $delta; } $walletGold = (float) $wallet->gold; $missing = round($txNet - $walletGold, 6); if ($missing < $min) { return null; } // اگر موجودی فعلی نزدیک مسیر باگ باشد، با اطمینان بالاتر «همان باگ» است $matchesIntvalBug = abs($walletGold - $buggy) < $min || abs($walletGold - round($buggy, 3)) < $min; $customer = Customer::query()->select(['id', 'name'])->find((int) $wallet->owner); return [ 'customer_id' => (int) $wallet->owner, 'name' => $customer?->name ?? '-', 'wallet_id' => (int) $wallet->id, 'product_id' => (int) $wallet->product_id, 'tx_net' => round($txNet, 3), 'wallet_gold' => round($walletGold, 3), 'missing' => round($missing, 3), 'matches_intval_bug' => $matchesIntvalBug, 'clean_replay' => round($clean, 3), 'buggy_replay' => round($buggy, 3), ]; } }