'cashapp', '2' => 'paypal', '4' => 'apple', '8' => 'google', ]; private const CONSECUTIVE_FAIL_LIMIT = 20; private const RATE_WINDOW_SECONDS = 600; private const RATE_THRESHOLD = 30; private const RATE_MIN_COUNT = 20; private const ALERT_TTL_SECONDS = 600; public function checkAll(): int { $startTime = date('Y-m-d H:i:s', time() - self::RATE_WINDOW_SECONDS); $items = DB::connection('read')->table('agent.dbo.order') ->select('payment_code', 'order_title') ->where('created_at', '>=', $startTime) ->whereNotNull('payment_code') ->whereNotNull('order_title') ->where('payment_code', '<>', '') ->where('order_title', '<>', '') ->groupBy('payment_code', 'order_title') ->lock('with(nolock)') ->get(); $count = 0; foreach ($items as $item) { $paymentChannel = (string) $item->payment_code; $payMethod = (string) $item->order_title; if ($this->checkCombination($paymentChannel, $payMethod)) { $count++; } } return $count; } private function checkCombination(string $paymentChannel, string $payMethod): bool { if ($paymentChannel === '' || $payMethod === '') { return false; } $key = md5($paymentChannel . '|' . $payMethod); $this->checkConsecutiveFailedOrders($key, $paymentChannel, $payMethod); $this->checkSuccessRate($key, $paymentChannel, $payMethod); return true; } private function checkConsecutiveFailedOrders(string $key, string $paymentChannel, string $payMethod): void { $orders = DB::connection('read')->table('agent.dbo.order') ->select('order_sn', 'user_id', 'pay_status', 'created_at') ->where('payment_code', $paymentChannel) ->where('order_title', $payMethod) ->orderBy('created_at', 'desc') ->lock('with(nolock)') ->limit(self::CONSECUTIVE_FAIL_LIMIT) ->get(); if ($orders->count() < self::CONSECUTIVE_FAIL_LIMIT) { return; } foreach ($orders as $order) { if ((int) ($order->pay_status ?? 0) === self::SUCCESS_STATUS) { Redis::del($this->consecutiveAlertKey($key)); return; } } if (Redis::get($this->consecutiveAlertKey($key))) { return; } Redis::setex($this->consecutiveAlertKey($key), self::ALERT_TTL_SECONDS, 1); $latestOrder = $orders->first(); $message = sprintf( "%s 支付连续失败预警\n支付渠道: %s\n支付方式: %s\n支付方式值: %s\n连续未成功订单数: %d\n最新订单: %s\n最新用户ID: %s\n最新创建时间: %s", env('APP_ENV'), $paymentChannel, $this->payMethodName($payMethod), $payMethod, self::CONSECUTIVE_FAIL_LIMIT, $latestOrder->order_sn ?? '', $latestOrder->user_id ?? '', $latestOrder->created_at ?? '' ); $this->send($message); } private function checkSuccessRate(string $key, string $paymentChannel, string $payMethod): void { $windowStart = date('Y-m-d H:i:s', time() - self::RATE_WINDOW_SECONDS); $windowEnd = date('Y-m-d H:i:s'); $stats = DB::connection('read')->table('agent.dbo.order') ->selectRaw('count(1) as total, sum(case when pay_status = 1 then 1 else 0 end) as success_total') ->where('payment_code', $paymentChannel) ->where('order_title', $payMethod) ->where('created_at', '>=', $windowStart) ->where('created_at', '<=', $windowEnd) ->lock('with(nolock)') ->first(); $total = (int) ($stats->total ?? 0); if ($total < $this->rateMinCount()) { return; } $successTotal = (int) ($stats->success_total ?? 0); $failedTotal = $total - $successTotal; $rate = round($successTotal * 100 / $total, 2); if ($rate >= self::RATE_THRESHOLD || Redis::get($this->rateAlertKey($key))) { return; } Redis::setex($this->rateAlertKey($key), self::ALERT_TTL_SECONDS, 1); $message = sprintf( "%s 支付成功率预警\n支付渠道: %s\n支付方式: %s\n支付方式值: %s\n统计窗口: %s ~ %s\n成功率: %s%%\n成功/总数: %d/%d\n失败数: %d", env('APP_ENV'), $paymentChannel, $this->payMethodName($payMethod), $payMethod, $windowStart, $windowEnd, $rate, $successTotal, $total, $failedTotal ); $this->send($message); } private function rateMinCount(): int { return (int) env('PAYMENT_WARNING_RATE_MIN_COUNT', self::RATE_MIN_COUNT); } private function payMethodName(string $payMethod): string { return self::PAY_METHOD_NAMES[$payMethod] ?? $payMethod; } private function send(string $message): void { try { TelegramBot::getDefault()->sendMsgAndImportant($message); } catch (\Throwable $throwable) { Util::WriteLog('PaymentWarningService', $throwable->getMessage()); } } private function consecutiveAlertKey(string $key): string { return 'payment_warning:consecutive:' . $key; } private function rateAlertKey(string $key): string { return 'payment_warning:success_rate:' . $key; } }