dpl/app/Services/MailFlow/DailyReportService.php

100 lines
5.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Services\MailFlow;
use App\Models\MailDailyReport;
use App\Models\MailMessage;
use App\Models\MailQuotaBucket;
use Carbon\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
class DailyReportService
{
public function __construct(private ResendClient $resend) {}
public function snapshot(Carbon $end): MailDailyReport
{
$start = $end->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();
}
}
}