1. 修改 app/Models/Transaction/Order.php:新增訂單生命週期狀態常數與 TERMINAL_STATUSES 終態定義。 2. 修改 app/Services/Transaction/TransactionService.php:優化 processTransaction 與 finalizeTransaction 的冪等性邏輯,支援 pending 狀態訂單的狀態轉移,並拆分輔助函式 transitionOrder 與 createOrderItems。 3. 新增 app/Console/Commands/SweepAbandonedOrdersCommand.php:建立 orders:sweep-abandoned 指令,將停留在 pending 狀態超過 15 分鐘的逾時訂單標記為 abandoned。 4. 修改 app/Console/Kernel.php:將 orders:sweep-abandoned 定期清理指令註冊至 Schedule 排程中,設定每 5 分鐘執行一次。
55 lines
2.0 KiB
PHP
55 lines
2.0 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use App\Models\Transaction\Order;
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\Log;
|
||
|
||
/**
|
||
* 把「停留在 pending 太久」的交易標記為 abandoned(感應前取消 / 付款未完成)。
|
||
*
|
||
* 設計重點(過渡期安全):
|
||
* - 只動 status = 'pending' 的單。線上 main 機台(晟崴/中國醫)從不送 pending,
|
||
* 後台也就不會有它們的 pending 單 → 本命令永遠掃不到它們,零接觸。
|
||
* - 付款對話框逾時約 40 秒,預設門檻給到 15 分鐘,遠大於正常付款耗時;
|
||
* 超過此門檻仍 pending,代表機台沒回成功/失敗(取消、斷線、App 重啟),視為 abandoned。
|
||
* - 不碰庫存、不碰金流:pending 本來就還沒出貨/扣庫存。
|
||
*/
|
||
class SweepAbandonedOrdersCommand extends Command
|
||
{
|
||
protected $signature = 'orders:sweep-abandoned {--minutes=15 : 停留在 pending 超過幾分鐘視為 abandoned} {--limit=500 : 單次處理筆數上限}';
|
||
|
||
protected $description = '將逾時停留在 pending 的交易標記為 abandoned';
|
||
|
||
public function handle(): int
|
||
{
|
||
$minutes = max(1, (int) $this->option('minutes'));
|
||
$limit = max(1, (int) $this->option('limit'));
|
||
$threshold = now()->subMinutes($minutes);
|
||
|
||
$orders = Order::withoutGlobalScopes()
|
||
->where('status', Order::STATUS_PENDING)
|
||
->where('created_at', '<', $threshold)
|
||
->orderBy('id')
|
||
->limit($limit)
|
||
->get();
|
||
|
||
if ($orders->isEmpty()) {
|
||
$this->info('No stale pending orders to sweep.');
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$swept = 0;
|
||
foreach ($orders as $order) {
|
||
$order->update(['status' => Order::STATUS_ABANDONED]);
|
||
$swept++;
|
||
}
|
||
|
||
Log::info('Swept abandoned orders', ['count' => $swept, 'older_than_minutes' => $minutes]);
|
||
$this->info("Marked {$swept} pending order(s) as abandoned (older than {$minutes} min).");
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
}
|