copy()->subDay(); $report = MailDailyReport::where('report_date', $start->toDateString())->first(); if ($report) { return $report; } $rows = MailMessage::whereIn('direction', ['in', 'out']) ->whereRaw('COALESCE(submitted_at, first_attempt_at, created_at) >= ?', [$start->copy()->setTimezone(config('app.timezone'))->toDateTimeString()]) ->whereRaw('COALESCE(submitted_at, first_attempt_at, created_at) < ?', [$end->copy()->setTimezone(config('app.timezone'))->toDateTimeString()])->get(['direction', 'purpose', 'channel', 'status', 'fallback_reason']); $totals = []; foreach ($rows as $row) { $key = $row->direction . '/' . ($row->channel ?: ($row->status === 'policy_blocked' ? 'policy' : 'pending')) . '/' . $row->status; $totals[$key] = ($totals[$key] ?? 0) + 1; } $usage = null; if (Settings::get('resend_enabled') && Settings::get('resend_key')) { try { $usage = $this->resend->usage(); } catch (\Throwable $e) { /* report local records with an explicit unavailable marker */ } } $summary = [ 'resend_usage' => $usage, 'resend_usage_checked_at' => $usage ? now()->toIso8601String() : null, 'from' => $start->toIso8601String(), 'to' => $end->toIso8601String(), 'counts' => $totals, 'fallbacks' => $rows->whereNotNull('fallback_reason')->count(), 'backlog' => MailMessage::where('direction', 'out')->whereIn('status', ['pending', 'sending', 'unknown'])->count(), 'quota' => MailQuotaBucket::where('resets_at', '>', now())->get(['period', 'limit', 'used', 'reserved', 'synced_at'])->toArray(), ]; return MailDailyReport::firstOrCreate(['report_date' => $start->toDateString()], ['summary' => $summary]); } public function send(MailDailyReport $report): void { $lock = Cache::lock('mailflow:report:' . $report->id, 60); if (!$lock->get()) { return; } try { $report->refresh(); if ($report->status === 'sending') { $report->update(['status' => 'unknown', 'error_code' => 'master_worker_interrupted']); } if (!in_array($report->status, ['pending', 'failed'], true)) { return; } $url = (string) Settings::get('master_url'); if (!str_starts_with($url, 'https://') || !filter_var($url, FILTER_VALIDATE_URL) || !Settings::get('master_bot_code') || !Settings::get('master_chat_code')) { $report->update(['error_code' => 'master_configuration_required']); return; } $report->increment('attempts'); $report->update(['status' => 'sending']); $summary = $report->summary; $text = '📧 ' . admin_setting('app_name', 'dpl') . ' 邮件日报(北京时间八点结算)'; foreach ($summary['counts'] as $key => $count) { [$direction, $channel, $status] = explode('/', $key); $labels = ['out' => '发送', 'in' => '收件', 'policy' => '邮件策略', 'resend' => 'Resend', 'conventional' => '常规邮箱', 'pending' => '待处理', 'submitted' => '已提交', 'delivered' => '已投递', 'delayed' => '投递延迟', 'bounced' => '退信', 'complained' => '投诉', 'suppressed' => '停止投递', 'failed' => '失败', 'unknown' => '结果未知', 'sending' => '提交中', 'received' => '已接收', 'simulated' => '模拟发送', 'expired' => '已过期', 'policy_blocked' => '因档位或套餐状态取消']; $text .= "\n" . ($labels[$direction] ?? $direction) . ' / ' . ($labels[$channel] ?? $channel) . ' / ' . ($labels[$status] ?? $status) . ':' . $count; } $text .= "\n回退常规邮箱:" . $summary['fallbacks'] . "\n待处理/结果未知:" . $summary['backlog']; if ($summary['resend_usage'] ?? null) { $monthly = $summary['resend_usage']['monthly'] ?? []; if (isset($monthly['used'], $monthly['limit'])) { $text .= "\nResend 官方本月已用:" . $monthly['used'] . ' / ' . $monthly['limit']; } } else { $text .= "\nResend 官方额度未能实时核对,以下为本地记录。"; } foreach ($summary['quota'] as $quota) { $text .= "\nResend " . $quota['period'] . ' 保守剩余额度:' . max(0, $quota['limit'] - $quota['used'] - $quota['reserved']); } try { // Master currently exposes no idempotency guarantee. An uncertain response is not auto-retried. $response = Http::connectTimeout(3)->timeout(8)->post(rtrim($url, '/') . '/api/telegram/post', [ 'sBotCode' => Settings::get('master_bot_code'), 'sChatCode' => Settings::get('master_chat_code'), 'sMsg' => $text, 'enableMasterQueue' => false, ]); } catch (\Throwable $e) { $report->update(['status' => 'unknown', 'error_code' => 'master_transport_unknown']); return; } if ($response->successful() && $response->json('isDone') === true) { $report->update(['status' => 'submitted', 'submitted_at' => now(), 'error_code' => null]); } else { $report->update(['status' => 'unknown', 'error_code' => 'master_response_unknown']); } } finally { $lock->release(); } } }